Condividi tramite


Control Authoring Overview

The extensibility of the Windows Presentation Foundation (WPF) control model greatly reduces the need to create a new control. However, in certain cases you may still need to create a custom control. This topic discusses the features that minimize your need to create a custom control and the different control authoring models in Windows Presentation Foundation (WPF). This topic also demonstrates how to create a new control.

This topic contains the following sections:

  • When to Write a New Control
  • Models for Control Authoring
  • Creating a UserControl
  • Control Authoring Basics
  • Creating a Custom Control
  • Related Topics

When to Write a New Control

Historically, if you wanted to get a customized experience from an existing control, you were limited to changing the standard properties of the control, such as background color, border width, and font size. If you wished to extend the appearance or behavior of a control beyond these predefined parameters, you would need to create a new control.

Controls in WPF support rich content, Styles, Triggers, and templates. In many cases, these features allow you to create custom and consistent experiences without having to create a new control.

  • Rich Content. Many of the standard WPF controls support rich content. These controls support the display of arbitrary content, including both WPF visual elements and arbitrary data, reducing the need to create a new control or to modify an existing control to support a complex visualization. For more information, see Content Models.

  • Styles. A Style is a collection of values that represent properties for a control. By using styles, you can create a reusable representation of a desired control appearance and behavior without writing a new control.

  • Triggers. Triggers allow you to apply property values to change the appearance and behavior of a control in a declarative manner, again without creating a new control.

  • Templates. Many controls in WPF support templating. A template allows a control to provide extensibility for its visual representation without requiring an application author to write code. A template is simply a representation of the visual elements that the control is composed of.

For more information about styles, triggers, and templates, see Styling and Templating.

These features minimize the need for you to create a new control. However, if there is a need to create a new control, it is important to understand the different control authoring models in WPF, which is discussed in the next section.

Models for Control Authoring

WPF provides three general models for creating a control, each of which provides a different set of features and level of flexibility. It is important to understand these models before you begin writing a new control.

Deriving from UserControl

The simplest way to create a control in WPF is to create a subclass of UserControl. The development model for a UserControl is very similar to the model used for application development in WPF: create visual elements, give them names and reference event handlers in Extensible Application Markup Language (XAML). You can then reference the named elements in code.

If built correctly, a UserControl can take advantage of the benefits of rich content, styles, and triggers. However, in order to support templates, it is necessary to derive from Control to create a custom Control.

Benefits of Deriving from UserControl

Consider deriving from UserControl if:

  • You want to build your control similar to how you build an application.

  • Your control consists of only existing components.

  • You don't need to support complex customization.

Deriving from Control

Deriving from the Control class is the model used by most of the existing WPF controls. A custom Control is designed to separate the operational logic from the visual representation by using templates. Although building a custom Control is not as simple as building a UserControl, custom Controls provide the most flexibility.

Benefits of Deriving from Control

Consider deriving from Control instead of using the UserControl class if one or more of the following applies to you:

  • You would like the appearance of your control to be customizable via the ControlTemplate.

  • You would like your control to support different themes.

Deriving from FrameworkElement

Controls that derive from both UserControl and Control rely upon composing existing elements. There are times when the fine-grain control required for a custom component surpasses the functionality of simple element composition. For these scenarios, basing a component on FrameworkElement is the right choice. There are two standard methods for building FrameworkElement-based components: direct rendering and custom element composition. Direct rendering involves overriding the OnRender method of FrameworkElement and providing DrawingContext operations that explicitly define the component visuals. This is the method used by Image and Border. Custom element composition involves instantiating and using objects of type Visual to compose appearance of your component. See Using DrawingVisual Objects for an example. Track is an example of a control in WPF that uses custom element composition. It is also possible to mix direct rendering and customer element composition in the same control.

Benefits of Deriving from FrameworkElement

Consider deriving from FrameworkElement if one or more of the following applies:

  • You want to have fine-grain control over the appearance of your control beyond what is provided by simple element composition.

  • You want to define the appearance of your control by defining your own render logic.

  • You want to compose existing elements in novel ways that go beyond what is possible with UserControl and Control.

Creating a UserControl

As mentioned above, the simplest way to create a control in WPF is to create a subclass of UserControl. The following example shows the XAML that defines the user interface (UI) of a NumericUpDown UserControl:

<UserControl x:Class="MyUserControl.NumericUpDown"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
  <Grid Margin="3">
    <Grid.RowDefinitions>
      <RowDefinition/>
      <RowDefinition/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
      <ColumnDefinition/>
      <ColumnDefinition/>
    </Grid.ColumnDefinitions>

    <Border BorderThickness="1" BorderBrush="Gray" Margin="2" Grid.RowSpan="2" VerticalAlignment="Center" HorizontalAlignment="Stretch">
      <TextBlock Name="valueText" Width="60" TextAlignment="Right" Padding="5"/>
    </Border>
    <RepeatButton Name="upButton" Click="upButton_Click" Grid.Column="1" Grid.Row="0">Up</RepeatButton>
    <RepeatButton Name="downButton" Click="downButton_Click" Grid.Column="1" Grid.Row="1">Down</RepeatButton>

  </Grid>
</UserControl>

The following example shows the logic of this UserControl:

using System;
using System.Windows;
using System.Windows.Controls;

namespace MyUserControl
{
    public partial class NumericUpDown : System.Windows.Controls.UserControl
    {
        /// <summary>
        /// Initializes a new instance of the NumericUpDownControl.
        /// </summary>
        public NumericUpDown()
        {
            InitializeComponent();

            UpdateTextBlock();
        }

        /// <summary>
        /// Gets or sets the value assigned to the control.
        /// </summary>
        public decimal Value
        {
            get { return _value; }
            set
            {
                if (value != _value)
                {
                    if (MinValue <= value && value <= MaxValue)
                    {
                        _value = value;
                        UpdateTextBlock();
                        OnValueChanged(EventArgs.Empty);
                    }
                }
            }
        }


        private decimal _value = MinValue;


        /// <summary>
        /// Occurs when the Value property changes.
        /// </summary>
        public event EventHandler<EventArgs> ValueChanged;


        /// <summary>
        /// Raises the ValueChanged event.
        /// </summary>
        /// <param name="args">An EventArgs that contains the event data.</param>
        protected virtual void OnValueChanged(EventArgs args)
        {
            EventHandler<EventArgs> handler = ValueChanged;
            if (handler != null)
            {
                handler(this, args);
            }
        }

        private void upButton_Click(object sender, EventArgs e)
        {
            if (Value < MaxValue)
            {
                Value++;
            }
        }

        private void downButton_Click(object sender, EventArgs e)
        {
            if (Value > MinValue)
            {
                Value--;
            }
        }

        private void UpdateTextBlock()
        {
            valueText.Text = Value.ToString();
        }

        private const decimal MinValue = 0, MaxValue = 100;
    }
}

As demonstrated by this example, the development model for a custom UserControl is very similar to the model used for application development.

Control Authoring Basics

The properties and events defined by the UserControl example in the previous section are standard common language runtime (CLR) properties and events, which do not take advantage of functionalities of the platform. This section discusses the general guidelines for a WPF control regardless of the control authoring model.

Back Properties with DependencyProperty

In general, it is good practice to back all the properties in your new control by a DependencyProperty. For more information, see Custom Dependency Properties.

Use RoutedEvent

Similar to dependency properties, which extend the notion of CLR properties with additional functionalities, routed events extend the notion of standard CLR events. When you create a new WPF control, it is also good practice to implement your events as RoutedEvents. For more information, see Routed Events Overview and How to: Create a Custom Routed Event.

Changing the UserControl to use DependencyProperty and RoutedEvent

You can change the previous UserControl example to use DependencyProperty and RoutedEvent, as shown in the following example:

using System;
using System.Windows;
using System.Windows.Controls;

namespace MyUserControl
{
    public partial class NumericUpDown : System.Windows.Controls.UserControl
    {
        /// <summary>
        /// Initializes a new instance of the NumericUpDownControl.
        /// </summary>
        public NumericUpDown()
        {
            InitializeComponent();

            UpdateTextBlock();
        }

        /// <summary>
        /// Gets or sets the value assigned to the control.
        /// </summary>
        public decimal Value
        {          
            get { return (decimal)GetValue(ValueProperty); }
            set { SetValue(ValueProperty, value); }
        }

        /// <summary>
        /// Identifies the Value dependency property.
        /// </summary>
        public static readonly DependencyProperty ValueProperty =
            DependencyProperty.Register(
                "Value", typeof(decimal), typeof(NumericUpDown), 
                new FrameworkPropertyMetadata(MinValue,new PropertyChangedCallback(OnValueChanged)));

        private static void OnValueChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            NumericUpDown control = (NumericUpDown)obj;         
            control.UpdateTextBlock();

            RoutedPropertyChangedEventArgs<decimal> e = new RoutedPropertyChangedEventArgs<decimal>(
                (decimal)args.OldValue, (decimal)args.NewValue, ValueChangedEvent);
            control.OnValueChanged(e);
        }

        /// <summary>
        /// Identifies the ValueChanged routed event.
        /// </summary>
        public static readonly RoutedEvent ValueChangedEvent = EventManager.RegisterRoutedEvent(
            "ValueChanged", RoutingStrategy.Bubble, 
            typeof(RoutedPropertyChangedEventHandler<decimal>), typeof(NumericUpDown));

        /// <summary>
        /// Occurs when the Value property changes.
        /// </summary>
        public event RoutedPropertyChangedEventHandler<decimal> ValueChanged
        {
            add { AddHandler(ValueChangedEvent, value); }
            remove { RemoveHandler(ValueChangedEvent, value); }
        }

        /// <summary>
        /// Raises the ValueChanged event.
        /// </summary>
        /// <param name="args">Arguments associated with the ValueChanged event.</param>
        protected virtual void OnValueChanged(RoutedPropertyChangedEventArgs<decimal> args)
        {
            RaiseEvent(args);
        }

        private void upButton_Click(object sender, EventArgs e)
        {
            if (Value < MaxValue)
            {
                Value++;
            }
        }

        private void downButton_Click(object sender, EventArgs e)
        {
            if (Value > MinValue)
            {
                Value--;
            }
        }

        private void UpdateTextBlock()
        {
            valueText.Text = Value.ToString();
        }

        private const decimal MinValue = 0, MaxValue = 100;
    }
}

For the full sample, see NumericUpDown UserControl with DependencyProperty and RoutedEvent Sample.

Creating a Custom Control

Building a Control to Support Templates

UserControl offers a simple way to build a reusable piece of functionality in WPF, but to take advantage of templating and to support different themes, deriving from Control is the model to use. This section converts the UserControl example in the previous section to a custom Control.

Changing the Base Class

First, replace the UserControl base class with Control.

Moving to Templates

Once you have updated the base class, you need to move the content of the control into a template. The template is defined in a style that can reside in a number of places in the application. For this example, it is placed in the application resources.

In the UserControl example, the TextBlock and the RepeatButtons were assigned names. The RepeatButtons also referred to event handlers defined in the code. You can remove them in this custom Control because you will instead use binding and commands to get the same behavior, but in a more loosely-coupled manner.

<Application x:Class="MyCustomControl.MyApp"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    StartupUri="Window1.xaml"
    xmlns:local="clr-namespace:MyCustomControl">
  <Application.Resources>

    <Style TargetType="{x:Type local:NumericUpDown}">
      <Setter Property="HorizontalAlignment" Value="Center"/>
      <Setter Property="VerticalAlignment" Value="Center"/>
      <Setter Property="Template">
        <Setter.Value>
          <ControlTemplate TargetType="{x:Type local:NumericUpDown}">
            <Grid Margin="3">
              <Grid.RowDefinitions>
                <RowDefinition/>
                <RowDefinition/>
              </Grid.RowDefinitions>
              <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
              </Grid.ColumnDefinitions>

              <Border BorderThickness="1" BorderBrush="Gray" Margin="2" Grid.RowSpan="2" VerticalAlignment="Center" HorizontalAlignment="Stretch">
                <TextBlock Width="60" TextAlignment="Right" Padding="5"/>
              </Border>
              <RepeatButton Grid.Column="1" Grid.Row="0">Up</RepeatButton>
              <RepeatButton Grid.Column="1" Grid.Row="1">Down</RepeatButton>
            </Grid>

          </ControlTemplate>
        </Setter.Value>
      </Setter>
    </Style>

  </Application.Resources>
</Application>

Changing the Value Property

The UpdateTextBlock method in the UserControl example changes the context of the TextBlock in the UI when the Value property changes. You can remove this method. Instead, you can bind the TextBlock to the Value of the control directly. This separation allows the template to be changed without affecting the control implementation. The following example shows the TextBlock with the Binding declaration:

<TextBlock Text="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Value}"  Width="60" TextAlignment="Right" Padding="5"/>

For more information about data binding, see the Data Binding Overview.

Handling Input

In the UserControl example, the RepeatButtons referred directly to event handlers defined in code. For a custom Control, a command is a more flexible way to accomplish the same behavior. A control can define commands, as in the following example:

public static RoutedCommand IncreaseCommand
{
    get
    {
        return _increaseCommand;
    }
}
public static RoutedCommand DecreaseCommand
{
    get
    {
        return _decreaseCommand;
    }
}

private static void InitializeCommands()
{
    _increaseCommand = new RoutedCommand("IncreaseCommand", typeof(NumericUpDown));
    CommandManager.RegisterClassCommandBinding(typeof(NumericUpDown), new CommandBinding(_increaseCommand, OnIncreaseCommand));
    CommandManager.RegisterClassInputBinding(typeof(NumericUpDown), new InputBinding(_increaseCommand, new KeyGesture(Key.Up)));

    _decreaseCommand = new RoutedCommand("DecreaseCommand", typeof(NumericUpDown));
    CommandManager.RegisterClassCommandBinding(typeof(NumericUpDown), new CommandBinding(_decreaseCommand, OnDecreaseCommand));
    CommandManager.RegisterClassInputBinding(typeof(NumericUpDown), new InputBinding(_decreaseCommand, new KeyGesture(Key.Down)));
}

private static void OnIncreaseCommand(object sender, ExecutedRoutedEventArgs e)
{
    NumericUpDown control = sender as NumericUpDown;
    if (control != null)
    {
        control.OnIncrease();
    }
}
private static void OnDecreaseCommand(object sender, ExecutedRoutedEventArgs e)
{
    NumericUpDown control = sender as NumericUpDown;
    if (control != null)
    {
        control.OnDecrease();
    }
}

protected virtual void OnIncrease()
{
    if (this.Value < MaxValue)
    {
        this.Value += 1;
    }
}
protected virtual void OnDecrease()
{
    if (this.Value > MinValue)
    {
        this.Value -= 1;
    }
}

private static RoutedCommand _increaseCommand;
private static RoutedCommand _decreaseCommand;

The elements in the template can then reference the commands, as in the following example:

<RepeatButton Command="{x:Static local:NumericUpDown.IncreaseCommand}"  Grid.Column="1" Grid.Row="0">Up</RepeatButton>
<RepeatButton Command="{x:Static local:NumericUpDown.DecreaseCommand}"  Grid.Column="1" Grid.Row="1">Down</RepeatButton>

For more information about commands, see Commanding Overview.

By defining a template and using binding and commands, you have changed the NumericUpDown control from a static UserControl with a fixed visualization to a custom Control that is flexible and customizable.

For the complete sample, see NumericUpDown Custom Control in One Project Sample.

External Control Library

The final step is to package the NumericUpDown control into its own assembly to make it easy to reuse.

Creating a Theme File

Once you have moved the NumericUpDown class the library assembly, you need to move the style definition. First, you need to create a "themes" folder to store all of the theme files. Next, create a file with the name generic.xaml. This file will serve as the fallback for all resource look-ups for this assembly.

Define a ResourceDictionary in generic.xaml and place the style for the NumericUpDown control within the ResourceDictionary.

Defining the ThemeInfo Attribute

In order for the Style in generic.xaml to be found, the hosting application needs to know that the assembly contains control-specific resources. You can accomplish that by adding an assembly attribute to the class. Because the assembly only contains generic resources, set the GenericDictionaryLocation property SourceAssembly. The following shows the content of the AssemblyInfo.cs file:

[assembly: ThemeInfo(
    ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
    //(used if a resource is not found in the page, 
    // or application resource dictionaries)
    ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
    //(used if a resource is not found in the page, 
    // app, or any theme specific resource dictionaries)
)]

For the complete example, see NumericUpDown Custom Control in an External Library Sample.

Supporting Windows Themes

One control may have a different appearance across a number of different WPF Themes. To support multiple themes, a theme file needs to be defined with the correct styles, templates, and other recourses required by your control. You also need to set the ResourceDictionaryLocation parameter of the ThemeInfo attribute to reference the source assembly:

[assembly: ThemeInfo(ResourceDictionaryLocation.SourceAssembly, ResourceDictionaryLocation.SourceAssembly)]

For the complete sample, see NumericUpDown Custom Control with Theme and Automation Support Sample.

See Also

Other Resources

Control Customization
Control Customization Samples