One of the great things about WPF is that it separates the functionality of a control from the way it looks, this has become known as “lookless controls”. Which is great, but how can we ensure that our custom controls behave and also have a default look in the first place. This mini article will try and show you what happens with lookless controls.
Ensuring Control Does What We Want It To Do
I will firstly talk a bit about how you can create a split designer/developer project that should work correctly.
The first things that a developer should do is create a TemplatePartAttribute
such that this is captured in the metadata which can be used by an documentation tool. By using this TemplatePartAttribute
, the developer is able to tell the designer what was intended for a correct control operation.
Here is an example for a small control that I have made:
1: [TemplatePart(Name = "PART_DropDown",
2: Type = typeof(ComboBox))]
3: public class DemoControl : Control
4: {
5: }
This should be an alert to the designer that they need to create a part of the control Template
that should be a ComboBox
and should be called “PART_DropDown
”. So that is part 1, next the developer should override the OnApplyTemplate
and look for any expected parts that are required to make the control work properly and wire up the events required. Here is an example.
1: public override void OnApplyTemplate()
2: {
3: base.OnApplyTemplate();
4:
5:
6: dropDown =
7: base.GetTemplateChild(
8: "PART_DropDown") as ComboBox;
9: if (dropDown != null)
10: dropDown.SelectionChanged +=
11: new SelectionChangedEventHandler(
12: dropDown_SelectionChanged);
13:
14:
15: }
16:
17: void dropDown_SelectionChanged(object sender,
18: SelectionChangedEventArgs e)
19: {
20:
21:
22: }
Another method is to rely on RoutedCommands
that should be used by the designer in the XAML control Template. These can then be used as follows:
1:
2:
3: CommandBindings.Add(new
4: CommandBinding(DemoCommands.SayHello,
5:
6: (s, e) => {
7: MessageBox.Show("Hello");
8: }));
Lookless Controls
In order to create a truly lookless control, we should do the following:
Override the default Style
associated with a control, this is done by changing the metadata. An example of which is as follows:
1: static DemoControl()
2: {
3:
4: DefaultStyleKeyProperty.OverrideMetadata(
5: typeof(DemoControl),
6: new FrameworkPropertyMetadata(
7: typeof(DemoControl)));
8: }
Next, we need to understand a few things about how Themes work in WPF. There is an assembly level attribute that is called ThemeInfoAttribute
, which is typically created as follows:
1: [assembly: ThemeInfo(
2: ResourceDictionaryLocation.None,
3:
4:
5:
6: ResourceDictionaryLocation.SourceAssembly
7:
8:
9:
10: )]
This could be used to indicate a location for a Style
for a control. More often than not, this is created as I have just shown. If you do not specify an external DLL to look in, the next place that is examined is Themes\generic.xaml, so this is where you should put your default Style
/Template
for your custom control.
So typically, you would create a generic.xaml file that held the default control Style
/Template
.
For the attached demo project, my generic.xaml simply contains a bunch of merged resource dictionary objects as follows:
1: <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
2: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
3:
4: <!– Merge in all the available themes –>
5: <ResourceDictionary.MergedDictionaries>
6: <ResourceDictionary
7: Source="/CustomControls;component/Themes/Default.xaml" />
8: <ResourceDictionary
9: Source="/CustomControls;component/Themes/Blue.xaml" />
10: <ResourceDictionary
11: Source="/CustomControls;component/Themes/Red.xaml" />
12: </ResourceDictionary.MergedDictionaries>
13:
14:
15:
16: </ResourceDictionary>
If we study one of these, a little more closely, say the “Blue
” one, we can see that also uses a ComponentResourceKey
markup extension.
1: <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
2: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
3: xmlns:local="clr-namespace:CustomControls">
4:
5: <Style x:Key="{ComponentResourceKey {x:Type local:DemoControl}, Blue }"
6: TargetType="{x:Type local:DemoControl}">
7: <Setter Property="Background" Value="Blue"/>
8: <Setter Property="Margin" Value="10″/>
9: <Setter Property="Template">
10: <Setter.Value>
11: <ControlTemplate TargetType="{x:Type local:DemoControl}" >
12: <Border Background="{TemplateBinding Background}"
13: CornerRadius="5″ BorderBrush="Cyan"
14: BorderThickness="2″>
15: <StackPanel Orientation="Vertical"
16: Margin="{TemplateBinding Margin}">
17: <Button x:Name="btnSayHello"
18: Margin="{TemplateBinding Margin}"
19: Background="LightBlue"
20: Foreground="Black"
21: Command="{x:Static
22: local:DemoCommands.SayHello}"
23: Content="Say Hello" Height="Auto"
24: Width="Auto" />
25: <ComboBox x:Name="PART_DropDown"
26: Margin="{TemplateBinding Margin}"
27: Background="LightBlue"
28: Foreground="Black">
29: <ComboBoxItem Content="Blue"/>
30: <ComboBoxItem Content="Red"/>
31: </ComboBox>
32: </StackPanel>
33: <Border.LayoutTransform>
34: <ScaleTransform CenterX="0.5″
35: CenterY="0.5″
36: ScaleX="3.0″
37: ScaleY="3.0″/>
38: </Border.LayoutTransform>
39: </Border>
40: </ControlTemplate>
41: </Setter.Value>
42: </Setter>
43: </Style>
44:
45:
46: </ResourceDictionary>
So let's get to the bottom of that. What does that do for us. Well quite simply, it allows us another way to select a resource, by using a Type/Id
to lookup the resource. Here is an example:
1: Style style = (Style)TryFindResource(
2: new ComponentResourceKey(
3: typeof(DemoControl),
4: styleToUseName));
5:
6: if (style != null)
7: this.Style = style;
The working app simply allows users to toggle between 3 different Styles for the lookless control. You can download it and play with using the demo project.
Enjoy!