Friday, 23 March 2012

Styling user controls in WPF with EventSetter


It is possible to style a WPF control not only with a shared style, often put into a ResourceDictionary, but also attach some behavior to the applied style, using EventSetter element in XAML. This makes it possible to quickly apply shared behavior of a WPF control in the ResourceDictionary. The ResourceDictionary with this custom Style can then be inserted into the App.xaml (application wide) file, such that all controls of that given type will have this custom style and behavior. If a style is created without a x:Key element, it is called an implicit style. This means that the user control in WPF will "inherit" this style automatically, if the control does not override the style. This again means that the Setter and EventSetter of the Style will be applied to the user control in WPF. It is also possible to set a lot of other things in a style, such as triggers, but this blog article will focus on custom code in the EventSetter element inside a WPF style.

Example - setting an EventSetter on a RadTimePicker control
The RadTimePicker is a stylish user control to set the time of the day. This user control is not part of standard WPF but created by Telerik. I worked with this control today and there was an error which actually Telerik admitted was present in their forums. If you type the text "0130" for instance, this is interpreted to "13:00", which means it is hard to type correct hours between 00:00 and 09:59. There was obviously an error when the text contains a leading zero. The suggestion from Telerik was to handle the ParseDateTimeValue event handler in the control. The problem for me was that already multiple usages of the RadTimePicker was present in the solution I was working on. I wanted to "patch" all the RadTimePicker controls with handling the ParseDateTimeHandler. The quickest way, it turned out, was to use the already added implicit style of the RadTimePicker. The style was in a resource file which was added in the App.xaml file and I just added an EventSetter element to the style element.

I will now present this technique next. First off, lets create a resource dictionary with an implicit style for the RadTimePicker element.


<ResourceDictionary x:Class="EventSetterWPF.ControlStyles"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:tc="clr-namespace:Telerik.Windows.Controls;assembly=Telerik.Windows.Controls.Input">


<Style TargetType="{x:Type tc:RadTimePicker}">
<EventSetter Event="ParseDateTimeValue" Handler="RadTimePicker_ParseDateTimeValue" />
</Style>

</ResourceDictionary>


Make note that I use x:Class here, this is a resource dictionary with a code behind. To achieve this, add a file with the same name as the resource dictionary, but with the .cs extension. E.g. the resource dictionary is called ControlStyles.xaml, then add a class called ControlStyles.xaml.cs

Add this to the class:



using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using Telerik.Windows.Controls;

namespace EventSetterWPF
{


public partial class ControlStyles : ResourceDictionary
{

public ControlStyles()
{
InitializeComponent();
}

public void RadTimePicker_ParseDateTimeValue(object sender, ParseDateTimeEventArgs e)
{
if (e.IsParsingSuccessful)
{
string originalString = e.TextToParse.Replace(":", "");
if (!string.IsNullOrEmpty(originalString) && originalString.Length == 4 && originalString.StartsWith("0"))
{
int enteredHour = -1;
int enteredMinutes = -1;
if (int.TryParse(originalString.Substring(0, 2), out enteredHour) &&
(int.TryParse(originalString.Substring(2, 2), out enteredMinutes)) &&
e.Result.HasValue)
{
DateTime originalValue = e.Result.Value;
DateTime? newValue = new DateTime(originalValue.Year, originalValue.Month, originalValue.Day,
enteredHour, enteredMinutes, 0);
e.Result = newValue;
}
}
}
}

}

}




There are two important things to consider her first. The class is made partial, which is necessary for a code behind class. The code behind is referenced by the x:Class attribute in the XAML code of the resource dictionary (ControlStyles.xaml).
Further, the resource dictionary must inherit from ResourceDictionary as its base class.

Then the EventHandler is set up in the Style of the RadTimePicker with the EventSetter:



<Style TargetType="{x:Type tc:RadTimePicker}">
<EventSetter Event="ParseDateTimeValue" Handler="RadTimePicker_ParseDateTimeValue" />
</Style>



The EventSetter element has got an attribute called Handler which points to the event handler (Callback for event) that will handle an event in the styled WPF user control. The code behind will then parse the datetime as shown in the event RadTimePicker_ParseDateTimeValue.

This is a very powerful technique in WPF. Using an implicit style for a WPF user control not only is limited to style setters, triggers or even using a control template. It is also possible to use EventSetter to have common behavior and specify the common behavior in a code behind.

The final step is just to add in the resource dictionary in the App.xaml file to make it available for all RadTimePicker controls in the application.



<Application x:Class="EventSetterWPF.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="TestRadTimePicker.xaml">
<Application.Resources>
<ResourceDictionary Source="ControlStyles.xaml" />
</Application.Resources>
</Application>




Understanding how we can "style" WPF user controls using EventSetter and a codebehind file opens up a whole new world of possibilites for creating advanced WPF user controls.

3 comments:

  1. Telerik WPF user controls are sometimes pretty and nice to have, it would though be nice if they worked a bit better out of the box without such patching being necessary.

    ReplyDelete
  2. In Telerik's defence, the RadTimePicker actually works ok when the users enters "01:30" for example, note the ":" character. However "0130" is interpreted into "13:00", which of course is a bit illogical parsing. Hence, I worked with this example.

    ReplyDelete
  3. Big Thanks!!!
    Very useful article

    ReplyDelete