I have a Control defined by a ControlTemplate shown below:
<ControlTemplate x:Key="UXIconComboBoxControlTemplate" TargetType="{x:Type controls:UXIconComboBox}" >
<ControlTemplate.Resources>
<converters:IconComboBoxDropdownHorizontalOffsetMultiConverter x:Key="ComboBoxDropdownHorizontalOffsetConverter"/>
</ControlTemplate.Resources>
<Grid
x:Name="rootGrid"
Width="{TemplateBinding HitAreaWidth}"
Height="{TemplateBinding HitAreaHeight}"
Background="{TemplateBinding Background}"
>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<controls:ExtendedHitAreaButton
Grid.Row="0"
x:Name="PopupExpanderButton" << Popup Button
IsTabStop="True"
AutomationProperties.AutomationId="UXIconComboBox"
Style="{StaticResource PopupExpanderButtonStyle}">
</controls:ExtendedHitAreaButton>
<Popup
x:Name="IconComboBoxPopup"
Grid.Row="1"
AutomationProperties.AutomationId="UXIconComboBoxPopup"
Placement="Bottom"
PlacementTarget="{Binding ElementName=PopupExpanderButton}"
MinWidth="200"
StaysOpen="False">
<Popup.HorizontalOffset>
<MultiBinding Converter="{StaticResource ComboBoxDropdownHorizontalOffsetConverter}">
<Binding ElementName="PopupExpanderButton" Path="ActualWidth"/>
<Binding ElementName="IconComboBoxPopup_ListBox" Path="ActualWidth"/>
</MultiBinding>
</Popup.HorizontalOffset>
<ListBox
x:Name="IconComboBoxPopup_ListBox"
AutomationProperties.AutomationId="UXIconComboBoxListBox"
ItemsSource="{Binding ItemsSource, RelativeSource={RelativeSource TemplatedParent}}"
IsSynchronizedWithCurrentItem="True"
Focusable="True"
KeyboardNavigation.TabNavigation="Cycle"
KeyboardNavigation.DirectionalNavigation="Cycle"
BorderThickness="0"
ItemContainerStyle="{StaticResource ListBoxContainerStyle}"
ItemTemplate="{Binding ItemTemplate, RelativeSource={RelativeSource TemplatedParent}}">
</ListBox>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="PopupStates">
<VisualState x:Name="PopupClosed">
<Storyboard>
<BooleanAnimationUsingKeyFrames Storyboard.TargetName="IconComboBoxPopup"
Storyboard.TargetProperty="IsOpen">
<DiscreteBooleanKeyFrame KeyTime="0:0:0.25"
Value="False" />
</BooleanAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="rootGrid"
Storyboard.TargetProperty="(Panel.Background)">
<DiscreteObjectKeyFrame KeyTime="0:0:0.25" Value="{DynamicResource BrushIconComboBox_Popup_Closed_Background}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="PopupOpen">
<Storyboard>
<BooleanAnimationUsingKeyFrames Storyboard.TargetName="IconComboBoxPopup"
Storyboard.TargetProperty="IsOpen">
<DiscreteBooleanKeyFrame KeyTime="0:0:0.25"
Value="True" />
</BooleanAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="rootGrid"
Storyboard.TargetProperty="(Panel.Background)">
<DiscreteObjectKeyFrame KeyTime="0:0:0.25" Value="{DynamicResource BrushIconComboBox_Popup_Open_Background}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Popup>
</Grid>
</ControlTemplate>
I also have a Click event handler for the PopupExpanderButton as follows:
/// <summary>
/// Handler for the ExtendedHitAreaButton click event
/// </summary>
/// <param name="sender">listBoxItem info</param>
/// <param name="e">routed event info</param>
public void PopupExpanderButton_Click(object sender, RoutedEventArgs e)
{
Debug.WriteLine($"PopupExpanderButton_Click called - popup state {IconComboBoxPopup.IsOpen}");
var popupState = IconComboBoxPopup.IsOpen ? PopupClosed : PopupOpen;
var result = VisualStateManager.GoToState(this, popupState, true);
e.Handled = true;
}
If I click on the PopupExpanderButton, the Click event above is executed and the Popup window is displayed. NOTE that the PopupExpanderButton is simply a subclass of Button that includes a HitTestCore function that calls PointHitTestResult.
Without moving the mouse away from over the PopupExpanderButton, if I click on the PopupExpanderButton again, the popup window gets a lost-focus event and closes as it should, but then the Click event above is executed, IconComboBoxPopup.IsOpen is false, the GoToState is executed with a PopupOpen value .... but the Popup window does not open. The result value of the GoToState call is always True. Changing the method to GoToElementState makes no difference.
If I keep clicking on the PopupExpanderButton, the Click event above is executed with a PopupOpen value, but the Popup window will not open again.
If I click away from the PopupExpanderButton to some other part of the dialog, and then go back and click the PopupExpanderButton, the Popup window WILL appear!!
Can anyone tell me why the GoToState method is not working after the first Click event?
Thanks in advance!!
It's recommended to attach the VisualStateManager.VisualStateGroups
attached property to the root element of the ControlTemplate
, which is the Grid
in your case. It's currently attached to the Popup
.
It's also recommended that you don't set RoutedEventArgs.Handled
to true
. This would stop the RoutedEvent
from traversing the element tree, which could introduce unexpected behavior.
Usually client code of your control would expect such events to traverse. In your case a reason to stop this event would be if you want to convert the Button.Click
event to a more meaningful IconComboBoxPopupOpened
event. In this case you would first handle the Button.Click
, mark it as handled to stop it and raise the IconComboBoxPopupOpened
instead.
The problem you are experienceing is becuase your visual states are not transitioned correctly. You forgot to transition the state from "PopupOpen"
back to "PopupClosed"
.
Currently, although the Popup
is closed the control's state is still "PopipOpen"
- that's why when trying to transition the state to "PopupOpen"
does nothing, but VisualStateManager.GoToState
returns true
.
The return value is always true
when the transition was successful or the control is already in the desired state.
Because you allow the Popup
to close itself implicitly by losing focus, you must track the Popup.IsOpen
for this particular case.
In fact you must track every close event that is not explicitly triggered by the hosting control (e.g. by handling key gestures) and handle it by transitioning the visual state to "PopupClosed"
.
Same applies for open events in case the Popup
was opened by an unknown actor.
MyCustomControl.cs
// State property
public bool IsIconComboBoxPopupOpen { get; private set; }
// Set this property to 'true' right before this control
// explicitly opens or closes the Popup.
// The Popup.Closed and Popup.Opened handlers will reset it.
private bool IsPopupOpenChangedInternally { get; set; }
private Popup PART_IconComboBoxPopup { get; set; }
protected override OnApplyTemplate()
{
base.OnApplyTemplate();
this.PART_IconComboBoxPopup = GetTemplateChild("IconComboBoxPopup") as Popup;
if (this.PART_IconComboBoxPopup is not null)
{
this.PART_IconComboBoxPopup.Opened += OnPART_IconComboBoxPopupOpened;
this.PART_IconComboBoxPopup.Closed += OnPART_IconComboBoxPopupClosed;
}
}
protected override OnPART_IconComboBoxPopupOpened(object sender, EventArgs e)
{
// Ignore event if Popup was opened or closed
// by this control e.g. by a KeyUp handler
if (this.IsPopupOpenChangedInternally)
{
this.IsPopupOpenChangedInternally = false;
return;
}
// Popup was opened by some event that was not triggered by this control.
// We need to update this control's visual state accordingly.
this.IsIconComboBoxPopupOpen = true;
UpdatePopupStates();
}
protected override OnPART_IconComboBoxPopupClosed(object sender, EventArgs e)
{
// Ignore event if Popup was closed by this control e.g. by a KeyUp handler
if (this.IsPopupOpenChangedInternally)
{
this.IsPopupOpenChangedInternally = false;
return;
}
// Popup was closed by some event that was not triggered by this control.
// We need to update this control's visual state accordingly.
this.IsIconComboBoxPopupOpen = false;
UpdatePopupStates();
}
private bool UpdatePopupStates()
{
var nextState = this.IsIconComboBoxPopupOpen
? "PopupOpen"
: "PopupClosed";
VisualStateManager.GotoState(this, nextState, true);
}
public void PopupExpanderButton_Click(object sender, RoutedEventArgs e)
{
// Flag that this control is the trigger of following Popup events
this.IsPopupOpenChangedInternally = true;
// Toggle visual state using the XOR operator
this.IsIconComboBoxPopupOpen ^= true;
UpdatePopupStates();
// Avoid setting Handled to true. Allow traversal of RoutedEvents.
//e.Handled = true;
}
// Example of this control explicitly closing the Popup
protected void IconComboBoxPopup_PreviewKeyUp(object sender, KeyEventArgs e)
{
if (e.Key == Key.Escape)
{
// Flag that this control is the trigger of following Popup events
this.IsPopupOpenChangedInternally = true;
this.IsIconComboBoxPopupOpen = false;
UpdatePopupStates();
}
}