Introduction
This article and associated demo application will hopefully show the benefits of reusing control templates throughout an application that are defined in ResourceDictionary
objects.
Background
Recently I wrote a Windows application that required an Apple Mac OSX look and feel to it. Thanks to WPF and control templates, this was not too much of a problem.
What I discovered in the process of developing this application was how Resource Dictionaries alleviated me from having to write repetitive code and mark up. Using the ResourceDictionary
class, I was able to define my control template, event handlers, and other related resources in one place (in a separate assembly) and simply point the application's numerous windows' Style
properties to the Apple Mac-styled control template.
Defining the Control Template
In my experience, I have come to realize that reusable content should be defined as a separate component (well, duh!). As is with this demo application, the reusable Window control template has been defined in a seperate assembly, along with the resource dictionaries. This makes it easier for other developers to obtain and use the templates with minimal effort.
I need to mention here that I wrote the external assembly as a normal, bare-bones .NET class library. If you are going to follow the same route, one thing to note is you're going to have to add a few references to the project in order to work with resource dictionaries and other windows-type resources. The required references are:
PresentationCore
PresentationFramework
System.Xaml
WindowsBase
Ok, moving along. The Window template has been defined in a ResourceDictionary
named MacStyledWindow.xaml as follows:
<!---->
<ControlTemplate x:Key="MacWindowTemplate" TargetType="{x:Type Window}">
<Grid>
..
<!---->
<Border Grid.Row="1">
<AdornerDecorator>
<ContentPresenter />
</AdornerDecorator>
</Border>
..
</Grid>
</ControlTemplate>
The majority of the template's markup has been omitted above. What I wanted you to note is the AdornerDecorator
and ContentPresenter
tags. When you specify that a window in your application should use this window control template, these two tags are what make it possible for you to plug any custom window content (text blocks, buttons, menus, layout controls, etc.) into your windows.
In the same resource dictionary, I have created a Style
block, which I use to set a few common Window properties that will allow me to template the window with a Mac look. You will notice that one of the properties I set is the Template
property. This points to the Mac window template mentioned earlier.
<!---->
<Style x:Key="MacWindowStyle" TargetType="Window">
<Setter Property="Background" Value="Transparent" />
<Setter Property="WindowStyle" Value="None" />
<Setter Property="AllowsTransparency" Value="True" />
<Setter Property="Opacity" Value="0.95" />
<Setter Property="Template" Value="{StaticResource MacWindowTemplate}" />
</Style>
In addition to the MacStyledWindow
resource dictionary, the Demo.Common
project also contains two other resource dictionaries; one for button styles (MacStyledTitleBarButtons.xaml) and one for gradient brushes (MacStyledButtonBrushes.xaml). I won't cover these in depth. What I would like to point out is how to reference these resource dictionaries from another resource dictionary in the same project.
Near the top of the MacStyledWindow.xaml file, you will see the following markup:
<ResourceDictionary.MergedDictionaries>
-->
<ResourceDictionary Source="MacStyledTitleBarButtons.xaml" />
</ResourceDictionary.MergedDictionaries>
You use a MergedDictionaries
collection to add a resource dictionary reference. The markup above is an example of how to reference a resource dictionary in the same project. Later, I will show you how to reference a resource dictionary from an external project.
Creating Code-behind for a ResourceDictionary
So, we've defined a control template and a style with a few default properties that enable us to skin our window with a Mac look. This is great to a point; It gives us the ability to give our windows that Mac look, but at the same time we've removed some basic window functionality when applying the template; the user can no longer drag the window around the screen, nor do the pretty Close, Minimize, and Maximize buttons in the title bar do anything when clicked!
By default, when you create a ResourceDictionary
in Visual Studio, the wizard simply creates a single XAML file for you to contain your resource definitions. Usually this is all you need, but in our case we need to attach a few event handlers to make the templated window more interactive.
Event handlers most commonly live in code-behind files (I think...); classes that link up a UI control event to an event handler of some sort. It therefore makes sense to follow the same approach here to solve our event-handling problem.
What I did was added a new class to the project containing the resource dictionary that defines the control template, made the class partial
, changed the class definition so that it inherits from ResourceDictionary
, and named it the same as the resource dictionary, but with a .cs
extention. From what I've read, the naming convention is not required, but it seems to be the Microsoft standard of doing things, so, if it ain't broken, don't fix it, right?
So we have a resource dictionary named MacStyledWindow.xaml, and we have a code-behind file named MacStyledWindow.xaml.cs. Sorted? Not quite yet. The new code-behind file needs to be linked to the resource dictionary. This is done by specifying the code-behind class in the resource dictionary, using the x:Class="..."
syntax:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Demo_Common.Resource_Dictionaries.MacStyledWindow">
One last thing to do to finalize the setup of our code-behind class is to ensure that InitializeComponent
is called when instantiated. I created a constructor for the new class, and placed a call to InitializeComponent
in there:
public MacStyledWindow()
{
InitializeComponent();
}
Adding Event Handlers
Now we have our code-behind setup correctly, we can add the required event handlers. As I mentioned earlier, there is no support for dragging our templated window across the screen, nor do the title bar buttons respond to clicking. This is easily remedied in our new code-behind class.
The window control template I defined has a title bar that is comprised of a Border
and a TextBlock
:
<!---->
<Border MouseLeftButtonDown="titleBar_MouseLeftButtonDown"
Padding="15" CornerRadius="10, 10, 0, 0">
<Border.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FFABABAB"/>
<GradientStop Color="#FF202020" Offset="1"/>
</LinearGradientBrush>
</Border.Background>
</Border>
<TextBlock Foreground="White" Text="{TemplateBinding Title}"
MouseLeftButtonDown="titleBar_MouseLeftButtonDown"
HorizontalAlignment="Center" VerticalAlignment="Center" FontWeight="Normal" />
To facilitate the required dragging behaviour, I created an event handler in the code-behind that calls the border and textblock
's DragMove()
method:
private void titleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var window = (Window)((FrameworkElement)sender).TemplatedParent;
window.DragMove();
}
I'd come across the DragMove()
method previously in an unrelated project I worked on, so that was nothing new. What I did struggle with, however, was how to determine which templated window was raising the MouseLeftButtonDown
event. Turns out that was equally as simple. All I had to do was obtain a reference to the FrameworkElement
's TemplatedParent
, cast the reference to a Window
object, and call the DragMove()
method on that reference. Voila! All I still needed to do was wire up the border and textblock
's event handlers in the resource dictionary:
<Border MouseLeftButtonDown="titleBar_MouseLeftButtonDown"...
I used the same approach for handling click events on the title bar buttons:
private void closeButton_Click(object sender, RoutedEventArgs e)
{
var window = (Window)((FrameworkElement)sender).TemplatedParent;
window.Close();
}
private void minimizeButton_Click(object sender, RoutedEventArgs e)
{
var window = (Window)((FrameworkElement)sender).TemplatedParent;
window.WindowState = WindowState.Minimized;
}
private void maximizeButton_Click(object sender, RoutedEventArgs e)
{
var window = (Window)((FrameworkElement)sender).TemplatedParent;
if (window.WindowState == WindowState.Maximized)
window.WindowState = WindowState.Normal;
else window.WindowState = WindowState.Maximized;
}
Consuming Template from External Project
We have now completed pretty much all the work required on our template, and it is now ready for consumption. There are obviously a number of additional items and functionality not included in the attached template, but the idea here was to show how defining our templates in a single location can reduce the amount of repetitive code and XAML it would otherwise require.
And on that note, it is time for me to show you how our demo application implements the template in all its windows. First off, we add a reference to the assembly containing the template. In this case, it's a project in the same solution, so we simply add a reference to the Demo.Common
project.
Next, we add a global reference in the application's App.xaml to the resource dictionary containing the window template. We add the reference here so that the resource dictionary is available throughout the application. If you recall, earlier I showed you how to add a reference to a resource dictionary contained in the same project. Adding a reference to the application's App.xaml, which is in a separate project, is slightly different:
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/
Demo.Common;component/Resource Dictionaries/MacStyledWindow.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
The first difference is we need to add a root ResourceDictionary
tag. The second difference is the URI format we use when setting the ResourceDictionary
's Source
attribute. Previously, the resource dictionary was contained in the same project, and so we only had to specify the name of the file containing the resource dictionary. Here, however, the resource dictionary is defined in an external assembly, so we have to use the pack:
URI.
<ResourceDictionary Source="pack://application:,,,/Demo.Common;
component/Resource Dictionaries/MacStyledWindow.xaml" />
What the pack:
URI allows us to do is tell the ResourceDictionary
where, externally, to find the XAML file. Without going into too much detail, I will briefly explain how the URI works.
The first part of the pack:
URI specifies the authority
. In the case of our demo application, the resource file is compiled into a referenced assembly, and therefore the authority
is set as application:,,,
The second part of the pack:
URI specifies the path to the resource file. Here we specify that the AssemblyShortName
is Demo.Common
, we specify that the assembly being referenced to is referenced from the local assembly by using the ;component
keyword, and finally we specify the path to the resource file (including the sub folder name) in the referenced assembly.
With that out of the way, we can apply the template to our windows in the demo application. This is extremely straightforward. All we need to do is set the window's Style
property to point to the MacWindowStyle static
resource:
<Window x:Class="FortySixApplesResourceDictionaryDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Style="{StaticResource MacWindowStyle}"
>
Conclusion
I found that by defining my control templates and styles in resource dictionaries contained in separate assemblies reduced the amount of repetitive code and XAML I was having to write. There are other benefits to this approach too, of course, such as allowing for parallel development of an application; look and feel separated from business logic, one without affecting the other.
Additionally, one could define multiple "themes" this way, and dynamically load them at run time. If a theme needs to be added or changed, only the assembly containing the control template is affected, and perhaps a key or two in the application's config file...
Using the Code
The demo application is a Visual Studio 2010 solution that contains two projects:
- 46ApplesResourceDictionaryDemo.csproj
- This is the main project in the solution. It is a WPF Windows application.
- Demo.Common.csproj
- This project is a standard .NET class library, and is referenced by the WPF Window application
Extract the zip archive to your local hard disk. Open the 46ApplesResourceDictionaryDemo.sln solution file. If not already set, make sure the 46ApplesResourceDictionaryDemo
project is the startup project.
History
- 8th April, 2010: Initial post