[WPF learning] Chapter 66 support visual state

Original text: [WPF learning] Chapter 66 support visual state

The ColorPicker control introduced in the previous chapter is the best example of control design. Because their behavior and visual appearance are carefully separated, other designers can develop new templates that dynamically change their appearance.

One reason the ColorPicker control is so simple is that it doesn't involve state. In other words, it doesn't differentiate its visual appearance based on whether it has focus, whether the mouse hovers over it, whether it is disabled, and so on. The FlipPanel custom controls described in this chapter are different.

The basic idea behind FlipPanel controls is to provide two surfaces for hosted content, but only one surface is visible at a time. To see the rest, you need to "flip" between the two surfaces. The flip effect can be customized through the control template, but the default effect uses the fade effect of the transition between the front and back. Depending on the application, FlipPanel controls can be used to combine data entry forms with some help documents to provide a simple or more complex attempt for the same data, or to fuse questions and answers in a simple game.

You can flip through code (by setting a property called IsFlipped), or you can flip a panel using a convenient button (unless the control uses this to remove the button from the template).

Obviously, the control template needs to have two independent parts: the front and back content areas of the FlipPanel control. However, there is another detail - FlipPanel controls need a way to switch between two states: flipped and not flipped. You do this by adding triggers to the template. When you click Yes, you can use one trigger to hide the front panel and show the second panel, and another trigger to flip the changes. Both triggers can use any animation they like. But through the use of visual state, it can be clearly indicated to the control that these two states are necessary parts of the template, not to write triggers for appropriate properties or events. The use of the control can manage only to fill in the appropriate state animation. If you use Expression Blend, the task becomes even simpler.

1, Start writing FlipPanel class

The basic framework of FlipPanel is very simple. Contains two content areas that users can populate with a single element, most likely a layout container that contains various elements. From a technical point of view, this means that FlipPanel controls are not real panels because you cannot use layout logic to organize a set of child elements. However, this will not cause problems. Because the structure of FlipPanel control is clear and intuitive. The FlipPanel control also includes a flip button that allows users to switch between two different content areas.

Although custom controls can be created by inheriting from Control classes such as ContentControl or Panel, FlipPanel directly inherits from the Control base class. This is the best place to start if you don't need the functionality of a specific Control class. You should not inherit from a simpler FrameworkElement class unless you want to create elements that do not use the standard controls and template infrastructure:

public class FlipPanel:Control
    {

    }

First create the properties for the FlipPanel class. Like almost all attributes in a WPF element, dependency attributes should be used. The following code shows how FlipPanel defines the FrontContent property, which remains on the elements displayed on the front surface.

public static readonly DependencyProperty FrontContentProperty =
            DependencyProperty.Register("FrontContent", typeof(object), typeof(FlipPanel), null);

You then need to call the regular. NET property procedures of the GetValue() and SetValue() methods of the base class to modify the dependency properties. The following is the implementation process of FrontContent property:

/// <summary>
/// Preceding contents
/// </summary>
public object FrontContent
{
   get { return GetValue(FrontContentProperty); }
   set { SetValue(FrontContentProperty, value); }
}

Similarly, you need a dependency property on the back of the store. As follows:

public static readonly DependencyProperty BackContentProperty =
            DependencyProperty.Register("BackContent", typeof(object), typeof(FlipPanel), null);

        /// <summary>
        /// Back content
        /// </summary>
        public object BackContent
        {
            get { return GetValue(BackContentProperty); }
            set { SetValue(BackContentProperty, value); }
        }

An important attribute needs to be added: IsFlipped. This Boolean type property keeps track of the current state of the FlipPanel control (facing the front or facing the back), enabling the control user to flip the state by programming:

public static readonly DependencyProperty IsFlippedProperty =
            DependencyProperty.Register("IsFlipped", typeof(bool), typeof(FlipPanel), null);

        /// <summary>
        /// Is it reversed?
        /// </summary>
        public bool IsFlipped
        {
            get { return (bool)GetValue(IsFlippedProperty); }
            set { 
                SetValue(IsFlippedProperty, value);
                ChangeVisualState(true);
            }
        }

The IsFlipped property setter calls the custom method ChangeVisualState(). This method ensures that the display is updated to match the current flipped state. The ChangeVisualState method is described later.

The FlipPanel class doesn't need more properties because it actually inherits almost everything it needs from the Control class. One exception is the corneraradius property. Although the Control class contains the BorderBrush and BorderThickness attributes, which can be used to draw a Border on the FlipPanel Control, there is a lack of the corneraradius attribute that turns the square edge into a smooth curve, as the Border element does. It is easy to achieve similar effect in FlipPanel Control, provided that the coreradius dependency property is added and used to configure the Border element in the default Control template of FlipPanel Control:

public static readonly DependencyProperty CornerRadiusProperty =
            DependencyProperty.Register("CornerRadius", typeof(CornerRadius), typeof(FlipPanel), null);

        /// <summary>
        /// Control border fillet
        /// </summary>
        public CornerRadius CornerRadius
        {
            get
            {
                return (CornerRadius)GetValue(CornerRadiusProperty);
            }
            set
            {
                SetValue(CornerRadiusProperty, value);
            }
        }

You also need to add a style for the FlipPanel control that applies the default template. Put this style in the generic.xaml resource dictionary, as you did when you developed the ColorPicker control. Here's the basic skeleton you need:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:CustomControls">
    <Style TargetType="{x:Type local:FlipPanel}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:FlipPanel">
                    ...
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

There is one last detail. To get the default style from the generic.xaml file for notification control, you need to call the DefaultStyleKeyProperty.OverrideMetadata() method in the static constructor of class FlipPanel:

DefaultStyleKeyProperty.OverrideMetadata(typeof(FlipPanel), new FrameworkPropertyMetadata(typeof(FlipPanel)));

2, Select part and status

Now that you have the basic structure, you are ready to determine the parts and states that will be used in the control template.

Obviously, FlipPanel needs two states:

  • Normal state. The storyboard ensures that only the front content is visible, and that the back content is flipped, diluted, or removed in an attempt.
  • Flip status. The storyboard ensures that only the back content is visible, and that the front content is moved out of the attempt by animation.

In addition, two components are required:

  •   FlipButton. This is a button that, when clicked, attempts to change from the front to the back (or from the back to the front). The FlipPanel control provides this service by handling the events of the button.
  •   FlipButtonAlternate. This is an optional element that works the same way as FlipButton. Allows control consumers to use two different methods in custom templates. One option is to use a single flip button outside the flip area, the other is to place separate flip buttons on both sides of the panel in the flip area.

You should also add parts for the front and back content areas. However, FlipPanel does not need to directly operate these areas, as long as the template contains the animation to hide and display them at the appropriate time (another option is to define these parts, so that you can explicitly use the code to change their visibility. This way, even if no animation is defined, the panel can still change between the front and back content areas by hiding one part and showing the other. For simplicity, FlipPanel does not take this option).

For the fact that FlipPanel uses these parts and states, the TemplatePart property should be applied to the custom control class as follows:

[TemplatePart(Name = "FlipButton", Type = typeof(ToggleButton))]
    [TemplatePart(Name = "FlipButtonAlternate", Type = typeof(ToggleButton))]
    [TemplateVisualState(Name = "Normal", GroupName = "ViewStates")]
    [TemplateVisualState(Name = "Flipped", GroupName = "ViewStates")]
    public class FlipPanel : Control
    {

    }

3, Default control template

Now you can put that into the default control template. The root element is a Grid panel with two rows, which contains a content area (at the top row) and a flip button (at the bottom row). Two overlapping Border elements are used to fill the content area, representing the front and back content, but only the front and back content are displayed at one time.

To fill the front and back content areas, the FlipPanel control uses the ContentControl element. This technique is almost the same as the custom button example, except that two ContentPresenter elements are required, which are used in front and back of the FlipPanel control respectively. FlipPanel controls also contain separate Border elements to encapsulate each ContentPresenter element. Thus, the control user can outline the flippable content area (BorderBrush, BorderThickness, Background and corneraradius) by setting several direct properties of FlipPanel, instead of manually adding the Border.

Here is the basic skeleton of the default control template:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:CustomControls">
    <Style TargetType="{x:Type local:FlipPanel}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:FlipPanel}">
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto"></RowDefinition>
                            <RowDefinition Height="Auto"></RowDefinition>
                        </Grid.RowDefinitions>

                        <!-- This is the front content. -->
                        <Border x:Name="FrontContent" BorderBrush="{TemplateBinding BorderBrush}"
               BorderThickness="{TemplateBinding BorderThickness}"
               CornerRadius="{TemplateBinding CornerRadius}"
               >
                            <ContentPresenter
                     Content="{TemplateBinding FrontContent}">
                            </ContentPresenter>
                        </Border>

                        <!-- This is the back content. -->
                        <Border x:Name="BackContent" BorderBrush="{TemplateBinding BorderBrush}"
           BorderThickness="{TemplateBinding BorderThickness}"
           CornerRadius="{TemplateBinding CornerRadius}"
           >
                            <ContentPresenter
                     Content="{TemplateBinding BackContent}">
                            </ContentPresenter>
                        </Border>

                        <!-- This the flip button. -->
                        <ToggleButton Grid.Row="1" x:Name="FlipButton" 
                     Margin="0,10,0,0" >
                        </ToggleButton>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

When creating a default control template, it is best to avoid the details that hardcoded control consumers may want to customize. Instead, you need to use a template binding expression. In this example, a template binding expression is used to set several properties: BorderBrush, BorderThickness, corneraradius, Background, FrontContent, and BackContent. To set the default values for these properties (so that even if the control consumer does not set them, he or she can still ensure the correct visual appearance), additional setters must be added to the default style of the control.

1. Flip button

In the example above, the displayed control template contains a ToggleButton button. However, the button uses the default appearance of ToggleButton, which makes the ToggleButton button look like a universal button with a completely traditional shadow background. This is not appropriate for FlipPanel controls.

Although anything in ToggleButton can be replaced, FlipPanel needs to go further. It needs to remove the standard background and change the appearance of its internal elements based on the state of the toggle button.

To create this effect, you need to set a custom control template for ToggleButton. The control template can contain shape elements that draw the required arrows. In this example, ToggleButton is drawn using the Ellipse element for drawing circles and the Path element for drawing arrows, both of which are placed in a Grid panel with a single cell, as well as the RotateTransform object that needs to change the direction of the arrows:

<ToggleButton Grid.Row="1" x:Name="FlipButton" RenderTransformOrigin="0.5,0.5"
                     Margin="0,10,0,0" Width="19" Height="19">
                            <ToggleButton.Template>
                                <ControlTemplate>
                                    <Grid>
                                        <Ellipse Stroke="#FFA9A9A9"  Fill="AliceBlue"  />
                                        <Path Data="M1,1.5L4.5,5 8,1.5"
                             Stroke="#FF666666" StrokeThickness="2"
                             HorizontalAlignment="Center" VerticalAlignment="Center">
                                        </Path>
                                    </Grid>
                                </ControlTemplate>
                            </ToggleButton.Template>
                            <ToggleButton.RenderTransform>
                                <RotateTransform x:Name="FlipButtonTransform" Angle="-90"></RotateTransform>
                            </ToggleButton.RenderTransform>
                        </ToggleButton>

2. Define state animation

State animation is the most interesting part of the control template. They are the elements that provide flipping behavior, and they are also the details that are most likely to be modified by developers who create custom templates for FlipPanel.

To define a state group, you must add the VisualStateManager.VisualStateGroups element to the root element of the control template as follows:

<ControlTemplate TargetType="{x:Type local:FlipPanel}">
                    <Grid>
                        <VisualStateManager.VisualStateGroups>
                            ...
                        </VisualStateManager.VisualStateGroups>
                    </Grid>
</ControlTemplate>

A state group can be created inside the VisualStateGroups element using a VisualStateGroup element with the appropriate name. Within each VisualStateGroup element, add a VisualState element for each state. For the FlipPanel panel, there is a group with two visualization states:

<VisualStateManager.VisualStateGroups>
   <VisualStateGroup x:Name="ViewStates">
       <VisualState x:Name="Normal">
            <Storyboard>
               ...
            </Storyboard>
       </VisualState>
   </VisualStateGroup>
   <VisualStateGroup x:Name="FocusStates">
       <VisualState x:Name="Flipped">
            <Storyboard>
               ...
            </Storyboard>
       </VisualState>
   </VisualStateGroup>
</VisualStateManager.VisualStateGroups>

Each state corresponds to a storyboard with one or more animations. If these storyboards exist, they are triggered at the right time (if they don't exist, the control will degrade in the normal way without causing an error).

In the default control template, the animation changes from one content area to another using a simple fade effect, and uses a rotation transform to flip the ToggleButton arrow to point in another direction. Here are the markers to complete these two tasks:

<VisualState x:Name="Normal">
     <Storyboard>
           <DoubleAnimation Storyboard.TargetName="BackContent" 
                       Storyboard.TargetProperty="Opacity" To="0" Duration="0" ></DoubleAnimation>
     </Storyboard>
</VisualState>

<VisualState x:Name="Flipped">
    <Storyboard>
        <DoubleAnimation Storyboard.TargetName="FlipButtonTransform"
       Storyboard.TargetProperty="Angle" To="90" Duration="0">    
        </DoubleAnimation>
     <DoubleAnimation Storyboard.TargetName="FrontContent" 
                       Storyboard.TargetProperty="Opacity" To="0" Duration="0"></DoubleAnimation>
    </Storyboard>
</VisualState>

Through the above marking, it is found that the visual state duration is set to 0, which means that the animation immediately applies its effect. This may seem odd - after all, doesn't it take a more gradual change to be able to notice the animation?

Timing is right, because the visual state is used to represent the appearance of the control in the appropriate state. For example, when a flipped panel is in a flipped state, simply display its back contents. The flipping process is a transition before the FlipPanel control enters the flipping state, not a part of the flipping state itself.

3. Define state transition

Transitions are animations from the current state to the new state. One of the advantages of transforming a model is that you don't need to create a storyboard for the animation. For example, if you add the following tags, WPF creates an animation with a duration of 0.7 seconds to change the transparency of the FlipPanel control, creating the desired pleasing fade effect:

<VisualStateGroup x:Name="ViewStates">
      <VisualStateGroup.Transitions>
           <VisualTransition GeneratedDuration="0:0:0.7">    
           </VisualTransition>
      </VisualStateGroup.Transitions>
      <VisualState x:Name="Normal">
       ...
      </VisualState>
</VisualStateGroup>

Transitions are applied to state groups and must be added to the VisualStateGroup.Transitions collection when defining transitions. This example uses the simplest transition type: the default transition. The default transition applies to all state changes in the group.

The default transition is convenient, but solutions for all situations are not always appropriate. For example, you might want the FlipPanel control to transition at different speeds depending on the state it enters. To achieve this effect, you need to define multiple transitions, and you need to set the to property to indicate when the transition effect is applied.

For example, if there is a transition:

<VisualStateGroup.Transitions>
       <VisualTransition To="Flipped" GeneratedDuration="0:0:0.5"></VisualTransition>
       <VisualTransition To="Normal" GeneratedDuration="0:0:0.1"></VisualTransition>
</VisualStateGroup.Transitions>

FlipPanel will switch to Flipped in 0.5 seconds and enter Normal in 0.1 seconds.

This example shows the transition applied when entering a specific state, but you can also use the From property To create the transition applied when leaving a state, and you can use the To and From properties To create a more special transition that will only be applied when moving between specific two states. When the transition is applied, WPF traverses the transition set, finds the most special transition among all the transitions of applications, and only uses the most special transition.

For further control, you can create custom transition animations to replace the auto generated transitions that WPF typically uses. Custom transitions may be created for several reasons. Here are some examples: use more complex animations to control the step size of an animation, use animation to jog, run several animations in succession, or play sounds while the animation is running.

To define a custom transition, place a storyboard with one or more animations in the VisualTransition element. In the FlipPanel example, you can use a custom transition to ensure that the ToggleButton arrow rotates itself more quickly and the dilution process is slower:

<VisualStateGroup.Transitions>
                                    <VisualTransition GeneratedDuration="0:0:0.7" To="Flipped">
                                        <Storyboard>
                                            <DoubleAnimation Storyboard.TargetName="FlipButtonTransform"
       Storyboard.TargetProperty="Angle" To="90" Duration="0:0:0.2"></DoubleAnimation>
                                        </Storyboard>
                                    </VisualTransition>
                                    <VisualTransition GeneratedDuration="0:0:0.7" To="Normal">
                                        <Storyboard>
                                            <DoubleAnimation Storyboard.TargetName="FlipButtonTransform"
       Storyboard.TargetProperty="Angle" To="-90" Duration="0:0:0.2"></DoubleAnimation>
                                        </Storyboard>
                                    </VisualTransition>
                                </VisualStateGroup.Transitions>

But many controls need custom transition, and writing custom transition is very tedious work. You still need to keep the zero length state animation, which will inevitably reproduce some details between the state and transition.

4. Related elements

Through the above operations, a fairly good control template has been created. You need to add some content to the FlipPanel control to make the template work.

The trick is to use the OnApplyTemplate() method, which repayment is used to set the binding in the ColorPicker control. For FlipPanel controls, the OnApplyTemplate() method is used to retrieve ToggleButton for FlipButton and FlipButtonAlternate widgets, and associate event handlers for each widget, so that users can respond when they click to flip the control. Finally, the OnApplyTemplate() method calls a custom method called ChangeVisualState(), which ensures that the visual appearance of the control matches its current state:

public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            ToggleButton flipButton = base.GetTemplateChild("FlipButton") as ToggleButton;
            if (flipButton != null) flipButton.Click += flipButton_Click;

            // Allow for two flip buttons if needed (one for each side of the panel).
            // This is an optional design, as the control consumer may use template
            // that places the flip button outside of the panel sides, like the 
            // default template does.
            ToggleButton flipButtonAlternate = base.GetTemplateChild("FlipButtonAlternate") as ToggleButton;
            if (flipButtonAlternate != null)
                flipButtonAlternate.Click += flipButton_Click;

            this.ChangeVisualState(false);
        }

Here is a very simple event handler that allows the user to click the toggle button and flip the panel:

private void flipButton_Click(object sender, RoutedEventArgs e)
        {
            this.IsFlipped = !this.IsFlipped;
        }

Fortunately, there is no need to manually trigger the state animation. You don't need to create or trigger a transition animation. Instead, to change from one state to another, just call the static method VisualStateManager.GoToState(). When called, passes a reference to the control object that is changing state, the name of the new state, and a Boolean value that determines whether the transition is displayed. This value should be true if the change is caused by the user (for example, when the user clicks the toggle button), or false if the change is caused by the property setting (for example, if the initial value of the IsFlipped property is set using the tag of the page).

Handling all the different states supported by the control can become messy. To avoid calling GoToState() method in the whole control code dispersedly, most controls add methods similar to ChangeVisualState() added in FlipPanel control. This method is responsible for applying the correct state in each state group. The code in this method uses the If statement block (or switch statement) to apply the current state of each state group. This method works because it can call the gotostate () method with the name of the current state. In this case, nothing happens If the current state and the requested state are the same.

Here is the ChangeVisualState() method for the FlipPanel control:

private void ChangeVisualState(bool useTransitions)
{
            if (!this.IsFlipped)
            {
                VisualStateManager.GoToState(this, "Normal", useTransitions);
            }
            else
            {
                VisualStateManager.GoToState(this, "Flipped", useTransitions);
            }
}

The ChangeVisualState() method or its equivalent is usually called in the following location:

  • At the end of the OnApplyTemplate() method, after initializing the control.
  • When responding to an event that represents a change in state, such as a mouse move or clicking the toggle button.
  • When a method is triggered in response to a property change or through code (for example, the IsFlipped property setter calls the changevisualstate() method and always provides true, the transition animation is displayed. If you want to give the control consumer the opportunity not to show transitions, add the Flip() method, which takes the same Boolean parameter as passed for the changevisualstate () method.

As mentioned above, FlipPanel controls are very flexible. For example, you can use the control and not use the toggle button to flip through code (perhaps when the user clicks a different control). You can also include one or two flip buttons in the control template and allow the user to control them.

4, Using FlipPanel controls

Using the FlipPanel control is relatively simple. The markings are as follows:

<Window x:Class="CustomControlsClient.FlipPanelTest"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="FlipPanelTest" Height="300" Width="300" 
        xmlns:lib="clr-namespace:CustomControls;assembly=CustomControls" >
    <Grid x:Name="LayoutRoot" Background="White">
        <lib:FlipPanel x:Name="panel" BorderBrush="DarkOrange" BorderThickness="3" IsFlipped="True"
         CornerRadius="4" Margin="10">
            <lib:FlipPanel.FrontContent>
                <StackPanel Margin="6">
                    <TextBlock TextWrapping="Wrap" Margin="3" FontSize="16" Foreground="DarkOrange">This is the front side of the FlipPanel.</TextBlock>
                    <Button Margin="3" Padding="3" Content="Button One"></Button>
                    <Button Margin="3" Padding="3" Content="Button Two"></Button>
                    <Button Margin="3" Padding="3" Content="Button Three"></Button>
                    <Button Margin="3" Padding="3" Content="Button Four"></Button>
                </StackPanel>
            </lib:FlipPanel.FrontContent>
            <lib:FlipPanel.BackContent>
                <Grid Margin="6">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto"></RowDefinition>
                        <RowDefinition></RowDefinition>
                    </Grid.RowDefinitions>
                    <TextBlock TextWrapping="Wrap" Margin="3" FontSize="16" Foreground="DarkMagenta">This is the back side of the FlipPanel.</TextBlock>
                    <Button Grid.Row="2" Margin="3" Padding="10" Content="Flip Back to Front" HorizontalAlignment="Center" VerticalAlignment="Center" Click="cmdFlip_Click"></Button>
                </Grid>
            </lib:FlipPanel.BackContent>
        </lib:FlipPanel>
    </Grid>
</Window>

When you click the button on the back of FlipPanel, flip the panel by programming:

private void cmdFlip_Click(object sender, RoutedEventArgs e)
        {
            panel.IsFlipped = !panel.IsFlipped;
        }

Source code of this example: FlipPanel.zip

Tags: Programming Attribute REST

Posted on Sun, 12 Apr 2020 17:23:13 -0700 by bruce080