I've created a UserControl, PortStatusButton, that contains nothing but a RadButton for which I've modified its standard style to incorporate my own VisualStateGroup with its VisualStates. Its purpose is to be a multistate button with logic based on the values of several of its properties. I have written a custom VisualStateManager to process that logic. I have placed the PortStatusButton into the ItemTemplate of a RadListBox and bound its properties to data in my Model.
The basic method I've used to incorporate my visual states into the button is to create a separate VisualStateGroup and then have my VisualStateManager watch for a call to GoToStateCore for a CommonStates.Normal VisualState. I then check to see what state my button should be in and then call GoToState to go to my desired state. This allows me to have the PortStatusButton show my states, but still let the RadButton control the overall functioning of the button.
It's all working quite well, with one exception. When the PortStatusButton first appears, it fails to display my correct VisualState, but displays the CommonStates.Normal state instead. I think I know why and I'd like to ask you to verify that I'm correct and suggest what I might do about it.
The mode of failure is that the first time a property gets set in PortStatusButton, the UpdateButtonAppearance method that is called by its OnPropertyChanged handler fails to obtain a reference to the the current state of my VisualStateGroup because it can't find the child Grid of the RadButton that contains the VisualStateManager. Thus, it never calls GoToState because it doesn't know where to go, and the RadButton first displays in its CommonStates.Normal state.
The data that is feeding the bound PortStatusButton properties is coming from my ViewModel, which is defined as the DataContext in the main Grid that contains the RadListBox, and which loads the source collection in its constructor. My suspicion is that as each PortStatusButton is instantiated by the RadListBox, the Grid in the RadButton's control template has not yet been instantiated, so it's not yet there to be found. By the time the user starts clicking on the buttons, the Grid has been instantiated and the PortStatusButtons will function as they should.
Is that correct?
What would you suggest I do to allow the buttons to display my correct state when they first appear?
I have included excerpts from my code below:
First, excerpt from my PortStatusButton xaml:
<UserControl.Resources> <Style x:Key="PortStatusButtonStyle" TargetType="{x:Type telerik:RadButton}"> <Setter Property="BorderThickness" Value="1"/> <Setter Property="Foreground" Value="Black"/> <Setter Property="Background"> <Setter.Value> <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0"> <GradientStop Color="White" Offset="0"/> <GradientStop Color="Gainsboro" Offset="0.5"/> <GradientStop Color="#FFADADAD" Offset="0.5"/> <GradientStop Color="#FFD4D4D4" Offset="1"/> </LinearGradientBrush> </Setter.Value> </Setter> <Setter Property="BorderBrush" Value="#FF848484"/> <Setter Property="CornerRadius" Value="1"/> <Setter Property="Padding" Value="3"/> <Setter Property="FocusVisualStyle" Value="{x:Null}"/> <Setter Property="HorizontalContentAlignment" Value="Center"/> <Setter Property="VerticalContentAlignment" Value="Center"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type telerik:RadButton}"> <Grid Name="theStateGroupsRoot" SnapsToDevicePixels="True"> <VisualStateManager.CustomVisualStateManager> <local:PortStatusButtonVisualStateManager/> </VisualStateManager.CustomVisualStateManager> <VisualStateManager.VisualStateGroups> <!-- RadButton VisualStateGroups--> <VisualStateGroup x:Name="ConditionStates"> <VisualState x:Name="Inactive"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="InactiveBorder"> <DiscreteObjectKeyFrame KeyTime="0"> <DiscreteObjectKeyFrame.Value> <Visibility>Visible</Visibility> </DiscreteObjectKeyFrame.Value> </DiscreteObjectKeyFrame> </ObjectAnimationUsingKeyFrames> <DoubleAnimation Duration="0" To="0.5" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="InactiveBorder"/> <DoubleAnimation Duration="0" To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="InactiveContent"/> <DoubleAnimation Duration="0" To="0" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="ActiveContent"/> <DoubleAnimation Duration="0" To="0" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="ConnectedContent"/> </Storyboard> </VisualState> <VisualState x:Name="Active"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="ActiveBorder"> <DiscreteObjectKeyFrame KeyTime="0"> <DiscreteObjectKeyFrame.Value> <Visibility>Visible</Visibility> </DiscreteObjectKeyFrame.Value> </DiscreteObjectKeyFrame> </ObjectAnimationUsingKeyFrames> <DoubleAnimation Duration="0" To="0.5" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="ActiveBorder"/> <DoubleAnimation Duration="0" To="0" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="InactiveContent"/> <DoubleAnimation Duration="0" To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="ActiveContent"/> <DoubleAnimation Duration="0" To="0" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="ConnectedContent"/> </Storyboard> </VisualState> <VisualState x:Name="Connected"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="ConnectedBorder"> <DiscreteObjectKeyFrame KeyTime="0"> <DiscreteObjectKeyFrame.Value> <Visibility>Visible</Visibility> </DiscreteObjectKeyFrame.Value> </DiscreteObjectKeyFrame> </ObjectAnimationUsingKeyFrames> <DoubleAnimation Duration="0" To="0.5" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="ConnectedBorder"/> <DoubleAnimation Duration="0" To="0" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="InactiveContent"/> <DoubleAnimation Duration="0" To="0" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="ActiveContent"/> <DoubleAnimation Duration="0" To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="ConnectedContent"/> </Storyboard> </VisualState> </VisualStateGroup> </VisualStateManager.VisualStateGroups> <!-- RadButton border declarations --> <Border x:Name="InactiveBorder" BorderBrush="#FF848484" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}" Opacity="0"> <Border.Background> <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0"> <GradientStop Color="White" Offset="0"/> <GradientStop Color="#FF7E7E7E" Offset="0.5"/> <GradientStop Color="#FF747474" Offset="0.5"/> <GradientStop Color="#FFA0A0A0" Offset="1"/> </LinearGradientBrush> </Border.Background> <Border BorderBrush="White" BorderThickness="{TemplateBinding BorderThickness}" Background="{x:Null}" CornerRadius="{TemplateBinding InnerCornerRadius}"/> </Border> <Border x:Name="ActiveBorder" BorderBrush="#FF848484" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}" Opacity="0"> <Border.Background> <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0"> <GradientStop Color="White" Offset="0"/> <GradientStop Color="#FFFF7171" Offset="0.5"/> <GradientStop Color="Red" Offset="0.5"/> <GradientStop Color="#FFFF9090" Offset="1"/> </LinearGradientBrush> </Border.Background> <Border BorderBrush="White" BorderThickness="{TemplateBinding BorderThickness}" Background="{x:Null}" CornerRadius="{TemplateBinding InnerCornerRadius}"/> </Border> <Border x:Name="ConnectedBorder" BorderBrush="#FF848484" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}" Opacity="0"> <Border.Background> <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0"> <GradientStop Color="White" Offset="0"/> <GradientStop Color="#FF59FD59" Offset="0.5"/> <GradientStop Color="Lime" Offset="0.5"/> <GradientStop Color="#FF96FF96" Offset="1"/> </LinearGradientBrush> </Border.Background> <Border BorderBrush="White" BorderThickness="{TemplateBinding BorderThickness}" Background="{x:Null}" CornerRadius="{TemplateBinding InnerCornerRadius}"/> </Border> <!-- More RadButton Border declarations --> <ContentPresenter x:Name="Content" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" ContentStringFormat="{TemplateBinding ContentStringFormat}" TextElement.Foreground="{TemplateBinding Foreground}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/> <ContentPresenter x:Name="InactiveContent" ContentTemplate="{TemplateBinding ContentTemplate}" Content="Inactive" ContentStringFormat="{TemplateBinding ContentStringFormat}" TextElement.Foreground="{TemplateBinding Foreground}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" Opacity="0"/> <ContentPresenter x:Name="ActiveContent" ContentTemplate="{TemplateBinding ContentTemplate}" Content="Active" ContentStringFormat="{TemplateBinding ContentStringFormat}" TextElement.Foreground="{TemplateBinding Foreground}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" Opacity="0"/> <ContentPresenter x:Name="ConnectedContent" ContentTemplate="{TemplateBinding ContentTemplate}" Content="Connected" ContentStringFormat="{TemplateBinding ContentStringFormat}" TextElement.Foreground="{TemplateBinding Foreground}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" Opacity="0"/> <Border x:Name="CommonStatesWrapper"> <Border x:Name="FocusVisual" BorderBrush="#FFFFC92B" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}" Visibility="Collapsed"> <Border BorderBrush="Transparent" BorderThickness="1" CornerRadius="{TemplateBinding InnerCornerRadius}"/> </Border> </Border> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style> </UserControl.Resources> <Grid Name="theGrid"> <telerik:RadButton Name="theButton" Style="{DynamicResource PortStatusButtonStyle}" Click="TheButton_Click"/> </Grid></UserControl>
And an excerpt from my PortStatusButton code behind:
public partial class PortStatusButton : UserControl { // Constants // ConditionStates VisualStateGroup constants public const string cConditionVisualStateGroup = "ConditionStates"; public const string cActiveVisualState = "Active"; public const string cInactiveVisualState = "Inactive"; public const string cConnectedVisualState = "Connected"; // CommonStates VisualStateGroup constants public const string cCommonGroupVisualStateGroup = "CommonStates"; public const string cNormalVisualState = "Normal"; // Properties private bool IsSwitchOn { get; set; } public int PortNumber { get { return (int)GetValue(PortNumberProperty); } set { SetValue(PortNumberProperty, value); } } public static readonly DependencyProperty PortNumberProperty = DependencyProperty.Register("PortNumber", typeof(int), typeof(PortStatusButton), new PropertyMetadata(OnPortNumberChanged)); public int RuleIndex { get { return (int)GetValue(RuleIndexProperty); } set { SetValue(RuleIndexProperty, value); } } public static readonly DependencyProperty RuleIndexProperty = DependencyProperty.Register("RuleIndex", typeof(int), typeof(PortStatusButton), new PropertyMetadata(OnRuleIndexChanged)); // Constructors public PortStatusButton() { IsSwitchOn = false; InitializeComponent(); } // Methods private void UpdateButtonAppearance(PortStatusButton control) { // Rules: // +------------+-----------+--------++===========+ // | PortNumber | RuleIndex | Switch || Button | // | Assigned | Assigned | On || State | // +------------+-----------+--------++===========+ // | 0 | 0 | 0 || Inactive | // +------------+-----------+--------++===========+ // | 0 | 0 | 1 || Inactive | // +------------+-----------+--------++===========+ // | 0 | 1 | 1 || Inactive | // +------------+-----------+--------++===========+ // | 0 | 1 | 0 || Inactive | // +------------+-----------+--------++===========+ // | 1 | 1 | 0 || Inactive | // +------------+-----------+--------++===========+ // | 1 | 1 | 1 || Connected | // +------------+-----------+--------++===========+ // | 1 | 0 | 1 || Active | // +------------+-----------+--------++===========+ // | 1 | 0 | 0 || Inactive | // +------------+-----------+--------++===========+ // // Boolean equations: // Inactive = Not(PortNumber Assigned) + ((PortNumber Assigned) * (Not(Switch On)) // Active = (PortNumber Assigned) * NOT(RuleIndex Assigned) * (Switch On) // Connected = (PortNumberAssigned) * (RuleIndex Assigned) * (Switch On) bool IsPortNumberAssigned = PortNumber > PortConstants.UnassignedPortNumber; bool IsRuleIndexAssigned = RuleIndex > PortConstants.UnassignedRuleIndex; string NewStateName = cInactiveVisualState; if (IsPortNumberAssigned) { if (IsSwitchOn) { if (IsRuleIndexAssigned) { NewStateName = cConnectedVisualState; } else { NewStateName = cActiveVisualState; } } } FrameworkElement Root = VisualStateManagerHelper.FindVisualStateGroupsRoot(control.theButton); if(Root != null) { VisualStateGroup Group = VisualStateManagerHelper.GetVisualStateGroup(cConditionVisualStateGroup, Root); VisualState CurrentState = Group.CurrentState; bool StateWasChanged = false; if (CurrentState == null) { StateWasChanged = VisualStateManager.GoToState(control.theButton, NewStateName, false); CurrentState = Group.CurrentState; } if (NewStateName != CurrentState.Name) { StateWasChanged = VisualStateManager.GoToState(control.theButton, NewStateName, false); } } } private static void OnPortNumberChanged (DependencyObject d, DependencyPropertyChangedEventArgs e) { PortStatusButton Me = (d is PortStatusButton) ? (PortStatusButton)d : null; if (Me != null) { Me.UpdateButtonAppearance(d as PortStatusButton); } } private static void OnRuleIndexChanged (DependencyObject d, DependencyPropertyChangedEventArgs e) { PortStatusButton Me = (d is PortStatusButton) ? (PortStatusButton)d : null; if (Me != null) { Me.UpdateButtonAppearance(d as PortStatusButton); } } private void TheButton_Click(object sender, RoutedEventArgs e) { PortStatusButton Ancestor = ZWVisualTreeHelper.FindParent<PortStatusButton>(sender as DependencyObject); IsSwitchOn = !IsSwitchOn; UpdateButtonAppearance(Ancestor); } }
And the custom VisualStateManager:
public class PortStatusButtonVisualStateManager : VisualStateManagerHelper{ // Methods protected override bool GoToStateCore(FrameworkElement control, FrameworkElement stateGroupsRoot, string stateName, VisualStateGroup group, VisualState state, bool useTransitions) { bool IsGood = false; string NewStateName = ""; bool ShouldTransitionToNewState = false; if ((group != null) && (state != null)) { PortStatusButton Me = ZWVisualTreeHelper.FindParent<PortStatusButton>(control); // The basic premise on how this works is that the ConditionGroup visual states override the Normal state of the CommonStates group. // // The RadButton CommonGroup visual states control the button in its "steady state" condition. Normal is its default visual state and the button is in this state unless something else it happening. // The other Common states handle it when disabled, pressed, or the mouse is hovering over it. These are all mutually exclusive and form the base appearance of the button. // // The other state groups likewise each contain mutually exclusive visual states, but they modify the appearance of the button when it's any of the Common states. So, the final appearance of the // button is the result of the Common state it in, as modified by the states of the other groups. // // I wanted to take advantage of this already-existing functionality and tie into the Common group, but I felt it was important to keep my states separate because I wasn't sure how they would "play" // with the other groups when in a non-normal state. After all, I'm not sure yet how I want the button to appear when it's disabled, for instance. Will it depend on the state of the Condition? // // What I arrived at is that I want to override the CommonGroup.Normal state with one of the ConditionGroup states, but leave the other CommonGroup states alone to do their own thing. So, here // we will examine the requested new state to see if it is CommonGroup.Normal, and if it is, we'll chage the state to the current state of the ConditionGroup. if ((group.Name == PortStatusButton.cCommonGroupVisualStateGroup) && (state.Name == PortStatusButton.cNormalVisualState)) { VisualStateGroup NewGroup = GetVisualStateGroup(PortStatusButton.cConditionVisualStateGroup, stateGroupsRoot); VisualState NewState = null; if (NewGroup != null) { NewState = NewGroup.CurrentState; if (NewState == null) { } else { NewStateName = NewState.Name; ShouldTransitionToNewState = true; } } } IsGood = base.GoToStateCore(control, stateGroupsRoot, stateName, group, state, useTransitions); if (ShouldTransitionToNewState) { IsGood = GoToState(control, NewStateName, false); } } return IsGood; }}
And an excerpt from the control containing the RadListBox:
<UserControl.Resources> <Style x:Key="DraggableListBoxItem" TargetType="telerik:RadListBoxItem" BasedOn="{StaticResource RadListBoxItemStyle}"> <Setter Property="telerik:DragDropManager.AllowCapturedDrag" Value="True" /> </Style> </UserControl.Resources> <Grid IsSharedSizeScope="True" Width="Auto"> <Grid.DataContext> <vm:InputPortViewModel/> </Grid.DataContext> <Grid.RowDefinitions> <RowDefinition SharedSizeGroup="ConfiguratorMajorHeaderRowGroup"/> <RowDefinition SharedSizeGroup="ConfiguratorHeaderRowGroup"/> <RowDefinition SharedSizeGroup="ConfiguratorListRowGroup"/> <RowDefinition SharedSizeGroup="ConfiguratorFooterRowGroup"/> </Grid.RowDefinitions> <TextBlock Grid.Row="0" Text="Input Ports" HorizontalAlignment="Center"/> <Border Grid.Row="0" BorderBrush="Black" BorderThickness="0,0,1,1"/> <Grid Grid.Row="1"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition SharedSizeGroup="InputPortsStatusButtonColumnGroup"/> <ColumnDefinition SharedSizeGroup="InputPortNumberColumnGroup"/> <ColumnDefinition SharedSizeGroup="InputPortHyphenColumnGroup"/> <ColumnDefinition SharedSizeGroup="InputPortNameColumnGroup"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Border Grid.Column="1" Grid.ColumnSpan="6" BorderBrush="Black" BorderThickness="0,0,1,0"/> <TextBlock Grid.Column="1" Text="Status" HorizontalAlignment="Center"/> <TextBlock Grid.Column="2" Text="#" HorizontalAlignment="Center"/> <TextBlock Grid.Column="3" Text=""/> <TextBlock Grid.Column="4" Text="Name" HorizontalAlignment="Center"/> </Grid> <telerik:RadListBox Grid.Row="2" Name="PortListBox" ItemsSource="{Binding Ports}" ItemContainerStyle="{StaticResource DraggableListBoxItem}"> <telerik:RadListBox.ItemTemplate> <DataTemplate> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition SharedSizeGroup="InputPortsStatusButtonColumnGroup"/> <ColumnDefinition SharedSizeGroup="InputPortNumberColumnGroup"/> <ColumnDefinition SharedSizeGroup="InputPortHyphenColumnGroup"/> <ColumnDefinition SharedSizeGroup="InputPortNameColumnGroup"/> </Grid.ColumnDefinitions> <local:PortStatusButton Grid.Column="0" x:Name="thePortStatusButton" PortNumber="{Binding PortNumber}" RuleIndex="{Binding RuleIndex}"/> <TextBlock Grid.Column="1" Name="thePortNumber" Text="{Binding PortNumber}" FontWeight="Bold" HorizontalAlignment="Right" Margin="5,0,0,0"/> <TextBlock Grid.Column="2" Text="-" HorizontalAlignment="Center" Margin="5,0,5,0"/> <TextBlock Grid.Column="3" Name="theDisplayName" Text="{Binding DisplayName}" Margin="0,0,5,0"/> </Grid> </DataTemplate> </telerik:RadListBox.ItemTemplate> <telerik:RadListBox.DragDropBehavior> <telerik:ListBoxDragDropBehavior /> </telerik:RadListBox.DragDropBehavior> <telerik:RadListBox.DragVisualProvider> <telerik:ListBoxDragVisualProvider /> </telerik:RadListBox.DragVisualProvider> </telerik:RadListBox> <StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Center"> <TextBlock Name="theCount" Text="{Binding Ports.Count}"/> <TextBlock Text="Input Ports" Margin="5,0,0,0"/> </StackPanel> <Border Grid.Row="3" BorderBrush="Black" BorderThickness="0,0,1,1"/> </Grid></UserControl>