[WPF learning] Chapter 61 organization template resources

Keywords: Windows Attribute

The novel coronavirus pneumonia was issued today in April 4, 2020 to express the deep condolences of the people of all ethnic groups to the martyrs and their dead compatriots.

When using control templates, you need to decide how to share them more widely and whether you want to use them automatically or explicitly.

The first is about where you want to use the template. For example, are they limited to specific windows? In most cases, control templates are applied to multiple windows, or even to the entire Application. To avoid defining templates multiple times, you can define template Resources in the Resources collection of the Application class.

However, another matter needs to be considered for this purpose. In general, control templates are shared across multiple applications. A single application is likely to use a separately developed template. However, an application can only have one App.xaml file and one Application.Resources collection. Therefore, it is a better idea to define resources in a separate resource dictionary. This gives you the flexibility to use resources in specific windows or throughout your application. You can also use styles in combination, because any application can contain multiple resource dictionaries. To add a resource dictionary in Visual Studio, right-click the project in the Solution Explorer window, select the Add|New Item menu item, and then select the Resources Dictionary(WPF) template.

The resource dictionaries are introduced in the previous chapter. It is easy to use them. You only need to add a new XAML file with the following contents for the application:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    >
    <ControlTemplate x:Key="GradientButtonTemplate" TargetType="{x:Type Button}">
        ...
    </ControlTemplate>
</ResourceDictionary>

Although all templates can be combined into a single resource dictionary file, experienced developers prefer to create a separate resource dictionary for each control template. This is because control templates can quickly become too complex and may require the use of other related resources. Keeping them in a separate place and isolated from other controls is a good way to organize them.

To use resource dictionaries, you simply add them to the Resources collection for a specific window or application, which is more common. You can do this using the MergedDictionaries collection. For example, if the button template is in the Button.xaml file under the Resources subfolder of the project folder, you can use the following tags in the App.xaml file:

<Application x:Class="ControlTemplates.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    StartupUri="Menu.xaml"
    >
    <Application.Resources>
            <ResourceDictionary>
               <ResourceDictionary.MergedDictionaries>
                  <ResourceDictionary Source="Resources\Button.xaml"/>
               </ResourceDictionary.MergedDictionaries>
             </ResourceDictionary>
    </Application.Resources>
</Application>

1, Explode button control template

When you refine or extend a control template, you can find that it encapsulates a large number of different details, including specific shapes, geometry, and brushes. It's a good idea to extract these details from the control template and define them as separate resources. One reason is that through this step, it is more convenient to reuse these brushes in a group of related controls. For example, you might decide to create custom Button, CheckBox, and RadioButton controls that use the same. To make this work easier, create a separate resource dictionary for the brush (named Brushes.xaml) and merge the resource dictionary into the resource dictionary for each control, such as Button.xaml, CheckBox.xaml, and radiobutton.xaml.

To see how this technology works, analyze the tags below. These tags represent the complete resource dictionary of a button, including the resources used by the control template, the control template, and the style rules for applying the control template to each button in the application. Always follow this order, because resources need to be defined before they are used (if you define brushes after a template, you will receive an error message because the template cannot find the required brushes).

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <!-- Resources used by the template. -->
    <RadialGradientBrush RadiusX="1" RadiusY="5" GradientOrigin="0.5,0.3"
                         x:Key="HighlightBackground">
        <GradientStop Color="White" Offset="0"/>
        <GradientStop Color="Blue" Offset="0.4"/>
    </RadialGradientBrush>
    <RadialGradientBrush RadiusX="1" RadiusY="5" GradientOrigin="0.5,0.3"
                         x:Key="PressedBackground">
        <GradientStop Color="White" Offset="0"/>
        <GradientStop Color="Blue" Offset="1"/>
    </RadialGradientBrush>

    <SolidColorBrush Color="Blue" x:Key="DefaultBackground"></SolidColorBrush>
    <SolidColorBrush Color="Gray" x:Key="DisabledBackground"></SolidColorBrush>

    <RadialGradientBrush RadiusX="1" RadiusY="5" GradientOrigin="0.5,0.3"
                         x:Key="Border">
        <GradientStop Color="White" Offset="0"/>
        <GradientStop Color="Blue" Offset="1"/>
    </RadialGradientBrush>

    <!-- The button control template. -->
    <ControlTemplate x:Key="GradientButtonTemplate" TargetType="{x:Type Button}">
        <Border Name="Border" BorderBrush="{StaticResource Border}" BorderThickness="2"
       CornerRadius="2" Background="{StaticResource DefaultBackground}"
       TextBlock.Foreground="White">
            <Grid>
                <Rectangle Name="FocusCue" Visibility="Hidden" Stroke="Black"
           StrokeThickness="1" StrokeDashArray="1 2" SnapsToDevicePixels="True">
                </Rectangle>
                <ContentPresenter Margin="{TemplateBinding Padding}"
           RecognizesAccessKey="True"></ContentPresenter>
            </Grid>
        </Border>
        <ControlTemplate.Triggers>
            <Trigger Property="IsMouseOver" Value="True">
                <Setter TargetName="Border" Property="Background"
           Value="{StaticResource HighlightBackground}" />
            </Trigger>
            <Trigger Property="IsPressed" Value="True">
                <Setter TargetName="Border" Property="Background"
           Value="{StaticResource PressedBackground}" />
            </Trigger>
            <Trigger Property="IsKeyboardFocused" Value="True">
                <Setter TargetName="FocusCue" Property="Visibility"
            Value="Visible"></Setter>
            </Trigger>
            <Trigger Property="IsEnabled" Value="False">
                <Setter TargetName="Border" Property="Background"
           Value="{StaticResource DisabledBackground}"></Setter>
            </Trigger>
        </ControlTemplate.Triggers>
    </ControlTemplate>
</ResourceDictionary>

The following figure shows the buttons defined by the template. In this case, a gradient fill is used when the user moves the mouse over the button. However, the center of the gradient is in the center of the button. If you want to create more novel effects, such as gradients that follow the mouse position, you need to use animation or write code.

2, Apply template by style

There are limitations in this design. The control template essentially hardcodes some details, such as color scheme. This means that if you want to use the same combination of elements (Border, Grid, Rectangle, and ContentPresenter) in a button and arrange them in the same way, but want to provide a different color scheme, you must create a new template copy with different brush resources applied.

This is not necessarily a problem (after all, layout and formatting details can be so closely related that you don't want to isolate them in any way). But it does limit the ability to reuse control templates. If the template uses a composite arrangement of elements, and you want to reuse these elements with various formatting details, usually colors and fonts, you can extract these details from the template and put them in the style.

To do this, you need to rewrite the template. Instead of using hard coded colors this time, you need to extract information from control properties using template binding. The following example defines a thin template for the special button you saw earlier. The control template takes some details as the fixed elements of the base -- focus box and two unit wide rounded border. The background and border brushes are configurable. The only trigger you need to keep is the one that shows the focus box:

<ControlTemplate x:Key="GradientButtonTemplate" TargetType="{x:Type Button}">
        <Border Name="Border" BorderThickness="2" CornerRadius="2"
              Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}">
            <Grid>
                <Rectangle Name="FocusCue" Visibility="Hidden" Stroke="Black"
           StrokeThickness="1" StrokeDashArray="1 2" SnapsToDevicePixels="True">
                </Rectangle>
                <ContentPresenter Margin="{TemplateBinding Padding}"
           RecognizesAccessKey="True"></ContentPresenter>
            </Grid>
        </Border>
        <ControlTemplate.Triggers>
            <Trigger Property="IsKeyboardFocused" Value="True">
                <Setter TargetName="FocusCue" Property="Visibility"
            Value="Visible"></Setter>
            </Trigger>
        </ControlTemplate.Triggers>
    </ControlTemplate>

The associated style applies this control template, sets the border and background color, and adds triggers to change the background color according to the button's status:

<!-- The style that applies the button control template. -->
    <Style TargetType="{x:Type Button}">
        <Setter Property="Control.Template"
       Value="{StaticResource GradientButtonTemplate}"></Setter>
        <Setter Property="BorderBrush"
       Value="{StaticResource Border}"></Setter>
        <Setter Property="Background"
         Value="{StaticResource DefaultBackground}"></Setter>
        <Setter Property="TextBlock.Foreground"
           Value="White"></Setter>
        <Style.Triggers>
            <Trigger Property="IsMouseOver" Value="True">
                <Setter Property="Background"
           Value="{StaticResource HighlightBackground}" />
            </Trigger>
            <Trigger Property="IsPressed" Value="True">
                <Setter Property="Background"
           Value="{StaticResource PressedBackground}" />
            </Trigger>
            <Trigger Property="IsEnabled" Value="False">
                <Setter Property="Background"
           Value="{StaticResource DisabledBackground}"></Setter>
            </Trigger>
        </Style.Triggers>
    </Style>

Ideally, you should be able to keep all triggers in the control template because they represent the behavior of the control and use style simplicity to set basic properties. However, if you want the style to be able to set the color scheme here, it is impossible to achieve.

To use this new Template, you need to set the Style property of the button instead of the Template property:

<Button Margin="10" Padding="5" Style="{StaticResource CustomButtonStyle}"
              >A Simple Button with a Custom Template</Button>

You can now create new styles that use the same template, but in order to apply the new color scheme, bind the template to a different brush.

There are important limitations to using this method. The Setter.TargetName property cannot be used in this style because the style does not contain a control template (just a reference template). Therefore, styles and triggers have certain limitations. They cannot drill down into the visualization tree to change this aspect of nested elements. Instead, the style needs to set the properties of the control, and the elements in the control need to be bound using the template.

3, Auto apply template

In the current example, each button is responsible for associating itself to the appropriate Template using the Template or Style property. If you use control templates, it makes sense to create special effects in specific places in your application. But it's not very convenient if you want to change the skin of each button throughout the application with a custom look. In this case, you might prefer all the buttons in the application to automatically request a new Template. To achieve this function, you need to apply control templates through styles.

The trick is to use a type style that automatically affects the element type of the response and sets the Template property. The following is an example of a style that should be placed in the resource collection of the resource dictionary to give the button a new look:

<Style TargetType="{x:Type Button}">
        <Setter Property="Control.Template"
       Value="{StaticResource GradientButtonTemplate}"></Setter>
    </Style>

The above code works because the style does not specify a key name, which means using the element type (Button) instead.

Remember that you can still exit the Style by creating a button and explicitly setting its Style property to a null value:

<Button Style="{x:Null}" ...></Button>

Resource dictionaries that contain combinations of type based styles are often (informally) referred to as themes. Themes can achieve extraordinary results. Themes allow you to reapply skin to all controls of an existing application without changing the user interface markup at all. All you need to do is add a resource dictionary to the project and merge it into the Application.Resources collection of the App.xaml file.

If you search on the Web, you can find many themes that can be used to skin WPF applications, such as several sample themes in the downloadable WPF Futures version.

To use a theme, add an. XAML file for the project that contains the resource dictionary. For example, WPF futurers provides a theme file called ExpressionDark.xaml. Then you need to activate the style in the application. You can do this window by window, but a faster way is to import them at the application level by adding the tags as follows:

<Application ...>
    <Application.Resources>
        <ResourceDictionary Source="ExpressionDark.xaml"/>
    </Application.Resources>
</Application>

The type based style in the resource dictionary will now be fully implemented and will automatically change the appearance of each generic control for each window of the application. If you are an application developer searching for popular user interfaces, but you don't have the design skills to build such user interfaces yourself, you can easily insert wonderful interfaces from third parties with little effort.

4, Skin selected by user

In some applications, you may want to dynamically change the template, usually based on the user's personal preferences. This is easy to find, but the documentation is not detailed by comparison. The basic technology is to load a new resource dictionary at runtime and replace the current one with the new one (not all resources need to be replaced, words need to replace those brave skin resources).

The trick is to retrieve the ResourceDictionary object, which is compiled and embedded in the application as a resource. The easiest way is to use the ResourceManager class to load the required resources.

For example, suppose you have created two Resources that define alternative versions of the same button control template. One is saved in the GradientButton.xaml file, and the other in the GradientButtonVariant.xaml file. To better organize Resources, both files are located in the Resources subfolder of the current project.

Now you can create a simple window to use one of these Resources through the Resources collection, as follows:

<Window.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Resources/GradientButton.xaml"></ResourceDictionary>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Window.Resources>

Now you can use different resource dictionaries with the following code:

ResourceDictionary resourceDictionary = new ResourceDictionary();
            resourceDictionary.Source = new Uri(
              "Resources/GradientButtonVariant.xaml", UriKind.Relative);
            this.Resources.MergedDictionaries[0] = resourceDictionary;

The above code loads the GradientButtonVariant resource dictionary and places it in the first location of the MergedDictionaries collection. The MergedDictionaries collection or any other window's resources are not cleared here because they may be linked to resource dictionaries that other seats continue to use. No new entries were added for the MergedDictionaries collection either, because this may conflict with a resource with the same name that is in a different collection.

If you are changing the skin for the entire application, you can use the same method, but you should use the application's resource dictionary, which can be updated with the following code:

Application.Current.Resources.MergedDictionaries[0]=newDictionary;

You can also use the pack URI syntax to load a resource dictionary in another assembly:

ResourceDictionary resourceDictionary = new ResourceDictionary();
            resourceDictionary.Source = new Uri(
              "ControlTemplates;component/GradientButtonVariant.xaml", UriKind.Relative);
            this.Resources.MergedDictionaries[0] = resourceDictionary;

When a new resource dictionary is loaded, all buttons are automatically updated with the new template. If you don't need to completely change the skin when you modify a control, you can also provide a basic style for the skin.

This example assumes that the GradientButton.xaml and GradientButtonVariant.xaml resources use element type styles to automatically change buttons. Another way to choose a new Template is to manually set the Template or Style property of the Button object. If you use this method, be sure to use the DynamicResource application instead of the StaticResource. If you use Resource, the Button Template is not updated when you switch skins.

There is also a way to load a resource dictionary by writing code. You can create a code behind class for the resource dictionary using almost the same method as creating a hidden class with mom for the window. You can then instantiate the class directly instead of using the ResourceDictionary.Source property. This method has the advantage that it is strongly typed (there is no chance to enter an invalid URI for the source property), and can add properties, methods, and other functions to the resource class. For example, you can use this method to create resources with event handling code for custom window templates.

Although it is easy to create code hiding classes for resource dictionaries, Visual Studio can not automatically complete the work, adding code files to some classes inherited from ResourceDictionary, and calling InitializeComponent() methods for constructors:

public partial class GradientButtonVariant : ResourceDictionary
    {
        public GradientButtonVariant()
        {
            InitializeComponent();
        }
    }

The class used here is called GradientButtonVariant, and it is stored in the GradientButtonVariant.xaml.cs file. The XAML file containing the resource is named GradientButtonVariant.xaml. It's not necessary to use a consistent name, but it's a good idea, and the conventions used by Visual Studio are followed when creating windows and pages.

Next, link the Class to the resource dictionary. This is done by adding the Class attribute to the root element of the resource dictionary, just like applying the Class attribute to a window, and applying the Class attribute to any XAML Class. Then provide the fully qualified Class name. In this example, the project name is ControlTemplates, so this is the default namespace, and the final label might look like this:

<ResourceDictionary x:Class="ControlTemplates.GradientButtonVariant">

Now you can use this code to create a resource dictionary and apply it to the window:

GradientButtonVariant newDict = new GradientButtonVariant();
this.Resources.MergedDictionaries[0] = newDict;

In Solution Explorer, if you want the GradientButtonVariant.xaml.cs file to be nested under the GradientButtonVariant.xaml file, you need to modify the. csproj project file in a text editor. In the < itemgroup > section, locate the code so file and change the following code:

<Compile Include="Resources\GradientButtonVariant.xaml.cs"/>

Amend to read:

<Compile Include="Resources\GradientButtonVariant.xaml.cs">
    <DependentUpon>Resources\GradientButtonVariant.xaml</DependentUpon>
</Compile>

Posted by nick_whitmarsh on Sun, 05 Apr 2020 22:48:51 -0700