As discussed earlier, one of the most powerful features of WPF is the ability to go beyond setting basic properties of a control to change its appearance and behavior, yet still not needing to create a custom control. The styling, data binding, and trigger features are made possible by the WPF property system and the WPF event system. If you implement dependency properties and routed events in your control, users of your custom control can use these features just as they would for a control that ships with WPF, regardless of the model you use to create the custom control.
Use Dependency Properties
When a property is a dependency property, it is possible to do the following:
Set the property in a style.
Bind the property to a data source.
Use a dynamic resource as the property's value.
Animate the property.
If you want a property of your control to support any of this functionality, you should implement it as a dependency property. The following example defines a dependency property called Value by doing the following:
Define a DependencyProperty identifier called ValueProperty as a public static readonly field.
Register the property name with the property system, by calling DependencyProperty..::.Register, to specify the following:
The name of the property.
The type of the property.
The type that owns the property.
The metadata for the property. The metadata contains the property's default value, a CoerceValueCallback and a PropertyChangedCallback.
Define a CLR "wrapper" property called Value, which is the same name that is used to register the dependency property, by implementing the property's get and set accessors. Note that the get and set accessors only call GetValue and SetValue respectively. It is recommended that the accessors of dependency properties not contain additional logic because clients and WPF can bypass the accessors and call GetValue and SetValue directly. For example, when a property is bound to a data source, the property's set accessor is not called. Instead of adding additional logic to the get and set accessors, use the ValidateValueCallback, CoerceValueCallback, and PropertyChangedCallback delegates to respond to or check the value when it changes. For more information on these callbacks, see Dependency Property Callbacks and Validation.
Define a method for the CoerceValueCallback called CoerceValue. CoerceValue ensures that Value is greater or equal to MinValue and less than or equal to MaxValue.
Define a method for the PropertyChangedCallback, called OnValueChanged. OnValueChanged creates a RoutedPropertyChangedEventArgs<(Of <(T>)>) object and prepares to raise the ValueChanged routed event. Routed events are discussed in the next section.
/// <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),
new CoerceValueCallback(CoerceValue)));
/// <summary>
/// Gets or sets the value assigned to the control.
/// </summary>
public decimal Value
{
get { return (decimal)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
private static object CoerceValue(DependencyObject element, object value)
{
decimal newValue = (decimal)value;
NumericUpDown control = (NumericUpDown)element;
newValue = Math.Max(MinValue, Math.Min(MaxValue, newValue));
return newValue;
}
private static void OnValueChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
NumericUpDown control = (NumericUpDown)obj;
RoutedPropertyChangedEventArgs<decimal> e = new RoutedPropertyChangedEventArgs<decimal>(
(decimal)args.OldValue, (decimal)args.NewValue, ValueChangedEvent);
control.OnValueChanged(e);
}
For more information, see Custom Dependency Properties.
Use Routed Events
Just as dependency properties extend the notion of CLR properties with additional functionality, routed events extend the notion of standard CLR events. When you create a new WPF control, it is also good practice to implement your event as a routed event because a routed event supports the following behavior:
Events can be handled on a parent of multiple controls. If an event is a bubbling event, a single parent in the element tree can subscribe to the event. Then application authors can use one handler to respond to the event of multiple controls. For example, if your control is a part of each item in a ListBox (because it is included in a DataTemplate), the application developer can define the event handler for your control's event on the ListBox. Whenever the event occurs on any of the controls, the event handler is called.
Routed events can be used in an EventSetter, which enables application developers to specify the handler of an event within a style.
Routed events can be used in an EventTrigger, which is useful for animating properties by using XAML. For more information, see Animation Overview.
The following example defines a routed event by doing the following:
Define a RoutedEvent identifier called ValueChangedEvent as a public static readonly field.
Register the routed event by calling the EventManager..::.RegisterRoutedEvent method. The example specifies the following information when it calls RegisterRoutedEvent:
The name of the event is ValueChanged.
The routing strategy is Bubble, which means that an event handler on the source (the object that raises the event) is called first, and then event handlers on the source's parent elements are called in succession, starting with the event handler on the closest parent element.
The type of the event handler is RoutedPropertyChangedEventHandler<(Of <(T>)>), constructed with a Decimal type.
The owning type of the event is NumericUpDown.
Declare a public event called ValueChanged and includes event-accessor declarations. The example calls AddHandler in the add accessor declaration and RemoveHandler in the remove accessor declaration to use the WPF event services.
Create a protected, virtual method called OnValueChanged that raises the ValueChanged event.
/// <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);
}
For more information, see Routed Events Overview and How to: Create a Custom Routed Event.
Use Binding
To decouple the UI of your control from its logic, consider using data binding. This is particularly important if you define the appearance of your control by using a ControlTemplate. When you use data binding, you might be able to eliminate the need to reference specific parts of the UI from the code. It's a good idea to avoid referencing elements that are in the ControlTemplate because when the code references elements that are in the ControlTemplate and the ControlTemplate is changed, the referenced element needs to be included in the new ControlTemplate.
The following example updates the TextBlock of the NumericUpDown control, assigning a name to it and referencing the textbox by name in code.
<Border BorderThickness="1" BorderBrush="Gray" Margin="2"
Grid.RowSpan="2" VerticalAlignment="Center" HorizontalAlignment="Stretch">
<TextBlock Name="valueText" Width="60" TextAlignment="Right" Padding="5"/>
</Border>
private void UpdateTextBlock()
{
valueText.Text = Value.ToString();
}
The following example uses binding to accomplish the same thing.
<Border BorderThickness="1" BorderBrush="Gray" Margin="2"
Grid.RowSpan="2" VerticalAlignment="Center" HorizontalAlignment="Stretch">
<!--Bind the TextBlock to the Value property-->
<TextBlock
Width="60" TextAlignment="Right" Padding="5"
Text="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type local:NumericUpDown}},
Path=Value}"/>
</Border>
For more information about data binding, see Data Binding Overview.
Define and Use Commands
Consider defining and using commands instead of handling events to provide functionality. When you use event handlers in your control, the action done within the event handler is inaccessible to applications. When you implement commands on your control, applications can access the functionality that would otherwise be unavailable.
The following example is part of a control that handles the click event for two buttons to change the value of the NumericUpDown control. Regardless of whether the control is a UserControl or a Control with a ControlTemplate, the UI and logic are tightly coupled because the control uses event handlers.
<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>
private void upButton_Click(object sender, EventArgs e)
{
Value++;
}
private void downButton_Click(object sender, EventArgs e)
{
Value--;
}
The following example defines two commands that change the value of the NumericUpDown control.
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()
{
Value++;
}
protected virtual void OnDecrease()
{
Value--;
}
private static RoutedCommand _increaseCommand;
private static RoutedCommand _decreaseCommand;
The elements in the template can then reference the commands, as shown 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>
Now applications can reference the bindings to access the functionality that was inaccessible when the control used event handlers. For more information about commands, see Commanding Overview.
Specifying That an Element Is Required in a ControlTemplate
Designing for Designers
To receive support for custom WPF controls in the Windows Presentation Foundation (WPF) Designer for Visual Studio (for example, property editing with the Properties window), follow these guidelines. For more information on developing for the WPF Designer, see WPF Designer.
Dependency Properties
Be sure to implement CLR get and set accessors as described earlier, in "Use Dependency Properties." Designers may use the wrapper to detect the presence of a dependency property, but they, like WPF and clients of the control, are not required to call the accessors when getting or setting the property.
Attached Properties
You should implement attached properties on custom controls using the following guidelines:
Have a public static readonly DependencyProperty of the form PropertyNameProperty that was creating using the RegisterAttached method. The property name that is passed to RegisterAttached must match PropertyName.
Implement a pair of public static CLR methods named SetPropertyName and GetPropertyName. Both methods should accept a class derived from DependencyProperty as their first argument. The SetPropertyName method also accepts an argument whose type matches the registered data type for the property. The GetPropertyName method should return a value of the same type. If the SetPropertyName method is missing, the property is marked read-only.
SetPropertyName and GetPropertyName must route directly to the GetValue and SetValue methods on the target dependency object, respectively. Designers may access the attached property by calling through the method wrapper or making a direct call to the target dependency object.
For more information on attached properties, see Attached Properties Overview.
Defining and Using Shared Resources for Your Control
You can include your control in the same assembly as your application, or you can package your control in a separate assembly that can be used in multiple applications. For the most part, the information discussed in this topic applies regardless of the method you use. There is one difference worth noting, however. When you put a control in the same assembly as an application, you are free to add global resources to the app.xaml file. But an assembly that contains only controls does not have an Application object associated with it, so an app.xaml file is not available.
When an application looks for a resource, it looks at three levels in the following order:
The element level—The system starts with the element that references the resource and then searches resources of the logical parent and so forth until the root element is reached.
The application level—Resources defined by the Application object.
The theme level—Theme-level dictionaries are stored in a subfolder called Themes. The files in the Themes folder correspond to themes. For example, you might have Aero.NormalColor.xaml, Luna.NormalColor.xaml, Royale.NormalColor.xaml, and so on. You can also have a file called generic.xaml. When the system looks for a resource at the themes level, it first looks for it in the theme-specific file and then looks for it in generic.xaml.
When your control is in an assembly that is separate from the application, you must put your global resources at the element level or at the theme level. Both methods have their advantages.
Defining Resources at the Element Level
You can define shared resources at the element level by creating a custom resource dictionary and merging it with your control’s resource dictionary. When you use this method, you can call your resource file anything you want, and it can be in the same folder as your controls. Resources at the element level can also use simple strings as keys. The following example creates a LinearGradientBrush resource file called Dictionary1.XAML.
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<LinearGradientBrush
x:Key="myBrush"
StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="Red" Offset="0.25" />
<GradientStop Color="Blue" Offset="0.75" />
</LinearGradientBrush>
</ResourceDictionary>
Once you've defined your dictionary, you need to merge it with your control's resource dictionary. You can do this using XAML or code.
The following example merges a resource dictionary by using XAML.
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Dictionary1.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
The disadvantage to this approach is that a ResourceDictionary object is created each time you reference it. For example, if you have 10 custom controls in your library and merge the shared resource dictionaries for each control by using XAML, you create 10 identical ResourceDictionary objects. You can avoid this by creating a static class that merges the resources in code and returns the resulting ResourceDictionary.
The following example creates a class that returns a shared ResourceDictionary.
internal static class SharedDictionaryManager
{
internal static ResourceDictionary SharedDictionary
{
get
{
if (_sharedDictionary == null)
{
System.Uri resourceLocater =
new System.Uri("/ElementResourcesCustomControlLibrary;component/Dictionary1.xaml",
System.UriKind.Relative);
_sharedDictionary =
(ResourceDictionary)Application.LoadComponent(resourceLocater);
}
return _sharedDictionary;
}
}
private static ResourceDictionary _sharedDictionary;
}
The following example merges the shared resource with the resources of a custom control in the control's constructor before it calls InitilizeComponent. Because the SharedDictionaryManager.SharedDictionary is a static property, the ResourceDictionary is created only once. Because the resource dictionary was merged before InitializeComponent was called, the resources are available to the control in its XAML file.
public NumericUpDown()
{
this.Resources.MergedDictionaries.Add(SharedDictionaryManager.SharedDictionary);
InitializeComponent();
}
Defining Resources at the Theme Level
WPF enables you to create resources for different Windows themes. As a control author, you can define a resource for a specific theme to change your control's appearance depending on what theme is in use. For example, the appearance of a Button in the Windows Classic theme (the default theme for Windows 2000) differs from a Button in the Windows Luna theme (the default theme for Windows XP) because the Button uses a different ControlTemplate for each theme.
Resources that are specific to a theme are kept in a resource dictionary with a specific file name. These files must be in a folder called Themes that is a subfolder of the folder that contains the control. The following table lists the resource dictionary files and the theme that is associated with each file:
Resource dictionary file name
|
Windows theme
|
|---|
Classic.xaml
|
“Classic” Windows 9x/2000 look on Windows XP
|
Luna.NormalColor.xaml
|
Default blue theme on Windows XP
|
Luna.Homestead.xaml
|
Olive theme on Windows XP
|
Luna.Metallic.xaml
|
Silver theme on Windows XP
|
Royale.NormalColor.xaml
|
Default theme on Windows XP Media Center Edition
|
Aero.NormalColor.xaml
|
Default theme on Windows Vista
|
You do not need to define a resource for every theme. If a resource is not defined for a specific theme, then the control uses the generic resource, which is in a resource dictionary file called generic.xaml in the same folder as the theme-specific resource dictionary files. Although generic.xaml does not correspond to a specific Windows theme, it is still a theme-level dictionary.
NumericUpDown Custom Control with Theme and UI Automation Support Sample contains two resource dictionaries for the NumericUpDown control: one is in generic.xaml and one is in Luna.NormalColor.xaml. You can run the application and switch between the Silver theme in Windows XP and another theme to see the difference between the two control templates. (If you are running Windows Vista, you can rename Luna.NormalColor.xaml to Aero.NormalColor.xaml and switch between two themes, such as Windows Classic and the default theme for Windows Vista.)
When you put a ControlTemplate in any of the theme-specific resource dictionary files, you must create a static constructor for your control and call the OverrideMetadata(Type, PropertyMetadata) method on the DefaultStyleKey, as shown in the following example.
static NumericUpDown()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(NumericUpDown),
new FrameworkPropertyMetadata(typeof(NumericUpDown)));
}
Defining and Referencing Keys for Theme Resources
When you define a resource at the element level, you can assign a string as its key and access the resource via the string. When you define a resource at the theme level, you must use a ComponentResourceKey as the key. The following example defines a resource in generic.xaml.
<LinearGradientBrush
x:Key="{ComponentResourceKey TypeInTargetAssembly={x:Type local:Painter},
ResourceId=MyEllipseBrush}"
StartPoint="0,0" EndPoint="1,0">
<GradientStop Color="Blue" Offset="0" />
<GradientStop Color="Red" Offset="0.5" />
<GradientStop Color="Green" Offset="1"/>
</LinearGradientBrush>
The following example references the resource by specifying the ComponentResourceKey as the key.
<RepeatButton
Grid.Column="1" Grid.Row="0"
Background="{StaticResource {ComponentResourceKey
TypeInTargetAssembly={x:Type local:NumericUpDown},
ResourceId=ButtonBrush}}">
Up
</RepeatButton>
<RepeatButton
Grid.Column="1" Grid.Row="1"
Background="{StaticResource {ComponentResourceKey
TypeInTargetAssembly={x:Type local:NumericUpDown},
ResourceId=ButtonBrush}}">
Down
</RepeatButton>
Specifying the Location of Theme Resources
To find the resources for a control, the hosting application needs to know that the assembly contains control-specific resources. You can accomplish that by adding the ThemeInfoAttribute to the assembly that contains the control. The ThemeInfoAttribute has a GenericDictionaryLocation property that specifies the location of generic resources, and a ThemeDictionaryLocation property that specifies the location of the theme-specific resources.
The following example sets the GenericDictionaryLocation and ThemeDictionaryLocation properties to SourceAssembly, to specify that the generic and theme-specific resources are in the same assembly as the control.
[assembly: ThemeInfo(ResourceDictionaryLocation.SourceAssembly,
ResourceDictionaryLocation.SourceAssembly)]