Data binding in WPF in general and particularly working with data templates tends to be quite tricky some times. Here I'll present you some more advanced tips for dealing with data templates.
To start with, I want to refer to an article I read a while ago. It is about the basics of data templates and I want to use it as a starting point and a foundation to build on. If you are just starting to work with WPF or you have never met with data templates, please read it first to get acquainted with the idea. I'll use ItemsControl for my samples - however, you can apply the ideas with any control supporting data templates.
In short, you can use data templates to define how some item's "look and feel". The goodie of data templates is that you can define different template for every different data object you want to visualize. Let's say that in addition to the Person class from the sample, you have also Boy and Girl classes (they may or may not inherit from the person class).
3 public class Boy: Person
4 {
5 }
3 public class Girl: Person
4 {
5 }
Now lets define data templates and styles for those types too:
23 <Style x:Key="GirlBorderStyle" TargetType="Border">
24 <Setter Property="Background" Value="LightPink" />
25 <Setter Property="BorderBrush" Value="Black" />
26 <Setter Property="BorderThickness" Value="2" />
27 <Setter Property="CornerRadius" Value="8" />
28 <Setter Property="Margin" Value="2,4" />
29 </Style>
30 <Style x:Key="BoyBorderStyle" TargetType="Border">
31 <Setter Property="Background" Value="LightBlue" />
32 <Setter Property="BorderBrush" Value="Black" />
33 <Setter Property="BorderThickness" Value="2" />
34 <Setter Property="CornerRadius" Value="8" />
35 <Setter Property="Margin" Value="2,4" />
36 </Style>
37 <DataTemplate DataType="{x:Type local:Girl}">
38 <DataTemplate.Resources>
39 <local:PersonImageConverter x:Key="imageConverter" />
40 </DataTemplate.Resources>
41 <Border x:Name="girlBorder" Style="{StaticResource GirlBorderStyle}">
42 <Grid>
43 <StackPanel Orientation="Horizontal">
44 <Image Width="40" Height="40"
45 Source="{Binding Path=ImageRef,
46 Converter={StaticResource imageConverter}}">
47 <Image.BitmapEffect>
48 <DropShadowBitmapEffect />
49 </Image.BitmapEffect>
50 </Image>
51 <TextBlock x:Name="personName" Text="{Binding Name}" Padding="15,15" Foreground="Black" />
52 </StackPanel>
53 </Grid>
54 </Border>
55 </DataTemplate>
56 <DataTemplate DataType="{x:Type local:Boy}">
57 <DataTemplate.Resources>
58 <local:PersonImageConverter x:Key="imageConverter" />
59 </DataTemplate.Resources>
60 <Border x:Name="boyBorder" Style="{StaticResource BoyBorderStyle}">
61 <Grid>
62 <StackPanel Orientation="Horizontal">
63 <Image Width="40" Height="40" Source="{Binding Path=ImageRef, Converter={StaticResource imageConverter}}">
64 <Image.BitmapEffect>
65 <DropShadowBitmapEffect />
66 </Image.BitmapEffect>
67 </Image>
68 <TextBlock x:Name="personName" Text="{Binding Name}" Padding="15,15" Foreground="Black" />
69 </StackPanel>
70 </Grid>
71 </Border>
72 </DataTemplate>
Good! Now that we have object classes and templates for them, lets make some new objects :). We will change the CreatePersons() method in the following way:
36 private static System.Collections.IEnumerable CreatePersons()
37 {
38 List<Person> persons = new List<Person>
39 {
40 new Girl
41 {
42 Name = "Jane",
43 ImageRef = "P_1.jpg",
44 Age = 12
45 },
46 new Boy
47 {
48 Name = "Josh",
49 ImageRef = "P_2.jpg",
50 Age = 25
51 },
52 new Person
53 {
54 Name = "Matthias",
55 ImageRef = "P_3.jpg",
56 Age = 62
57 }
58 };
59
60 return persons;
61 }
And here we go - lets run the app! Now instead of having 3 items with the same look we have 3 different looks:
Cool, ah? But that's too simple. Can we get it to do more complex stuff? Sure! How about the case in which you don't care about different object types and want define different data templates only for the base type. You don't have Boys and Girls but only Persons. However, those persons have additional property Age, and you what to have a different data template for people that are old and for those that are not (this might be an over simplified example but still will give you the right idea). To be more specific - everyone that is over 59 will be considered as old (I hope no one gets offended, just giving an example ;) ). This time we have the following data templates:
6 <Style x:Key="PersonBorderStyle" TargetType="Border">
7 <Setter Property="Background" Value="LightPink" />
8 <Setter Property="BorderBrush" Value="Black" />
9 <Setter Property="BorderThickness" Value="2" />
10 <Setter Property="CornerRadius" Value="8" />
11 <Setter Property="Margin" Value="2,4" />
12 </Style>
13 <Style x:Key="OldPersonBorderStyle" TargetType="Border">
14 <Setter Property="Background" Value="Beige" />
15 <Setter Property="BorderBrush" Value="Black" />
16 <Setter Property="BorderThickness" Value="2" />
17 <Setter Property="CornerRadius" Value="8" />
18 <Setter Property="Margin" Value="2,4" />
19 </Style>
20 <DataTemplate x:Key="defaultPerson" DataType="{x:Type local:Person}">
21 <DataTemplate.Resources>
22 <local:PersonImageConverter x:Key="imageConverter" />
23 </DataTemplate.Resources>
24 <Border x:Name="personsBorder" Style="{StaticResource PersonBorderStyle}">
25 <Grid>
26 <StackPanel Orientation="Horizontal">
27 <Image Width="40" Height="40" Source="{Binding Path=ImageRef, Converter={StaticResource imageConverter}}">
28 <Image.BitmapEffect>
29 <DropShadowBitmapEffect />
30 </Image.BitmapEffect>
31 </Image>
32 <TextBlock x:Name="personName" Text="{Binding Name}" Padding="15,15" Foreground="Black" />
33 </StackPanel>
34 </Grid>
35 </Border>
36 </DataTemplate>
37 <DataTemplate x:Key="oldPerson" DataType="{x:Type local:Person}">
38 <DataTemplate.Resources>
39 <local:PersonImageConverter x:Key="imageConverter" />
40 </DataTemplate.Resources>
41 <Border x:Name="personsBorder" Style="{StaticResource OldPersonBorderStyle}">
42 <Grid>
43 <StackPanel Orientation="Horizontal">
44 <Image Width="40" Height="40" Source="{Binding Path=ImageRef, Converter={StaticResource imageConverter}}">
45 <Image.BitmapEffect>
46 <DropShadowBitmapEffect />
47 </Image.BitmapEffect>
48 </Image>
49 <TextBlock x:Name="personName" Text="{Binding Name}" Padding="15,15" Foreground="Black" />
50 </StackPanel>
51 </Grid>
52 </Border>
53 </DataTemplate>
Notice that now each data template has x:Key attribute defined. The reason for this is that now we have 2 data templates for the same data type - Person. In order for both of them to exist and work correctly you need to have a different key defined for them both.
In order to have the right template selected for the right person, we need the magic of data template selectors. The essence of data template selectors is that if you have one, it will be used to select the correct template. I will define my own data template selector that will incorporate the logic I want to apply. Here is how that class looks like:
1 using System.Windows;
2 using System.Windows.Controls;
3
4 namespace WpfDatabinding_Persons
5 {
6 public class MySelector : DataTemplateSelector
7 {
8 public override DataTemplate SelectTemplate(object item, DependencyObject container)
9 {
10 var person = item as Person;
11 ContentPresenter presenter = container as ContentPresenter;
12 if(presenter != null)
13 {
14 if (person != null && person.Age > 59)
15 {
16 return presenter.TryFindResource("oldPerson") as DataTemplate;
17 }
18 return presenter.TryFindResource("defaultPerson") as DataTemplate;
19
20 }
21 return base.SelectTemplate(item,container);
22 }
23 }
24 }
Of course we need to assign this template selector to the ItemsControl we use to display Person objects:
5 <local:MySelector x:Key="mySelector" />
....
33 <ItemsControl x:Name="personItems" Grid.Row="2"
34 HorizontalAlignment="Stretch"
35 Margin="10"
36 VerticalAlignment="Center"
37 ItemTemplateSelector="{StaticResource mySelector}"
38 />
And voa-la! Run the app and you have the behavior we wanted - Matthias (the old guy) is displayed differently than the other people.
Want more? Sure! Why not!
Let's look what will happen if we have the scenario of changing skins. Generally, this case is handled by loading different resource dictionaries from xaml files. I'll define just two but you can have as much as you want. I add a simple ComboBox to have a way to choose between skins:
22 <ComboBox IsSynchronizedWithCurrentItem="True" x:Name="SkinsComboBox" Height="25" VerticalAlignment="Top"
23 SelectionChanged="SkinsComboBox_SelectionChanged" >
24 <ComboBoxItem DataContext="Default.xaml" IsSelected="True" >
25 Default
26 </ComboBoxItem>
27 <ComboBoxItem DataContext="Fancy.xaml" >
28 Fancy
29 </ComboBoxItem>
30 </ComboBox>
And the method that handles the skin change:
63 private void SkinsComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
64 {
65 string resources = "Templates/" + ((ComboBoxItem) ((ComboBox) sender).SelectedItem).DataContext;
66 var uri = new Uri(resources, UriKind.Relative);
67 ResourceDictionary rd = Application.LoadComponent(uri) as ResourceDictionary;
68 Application.Current.Resources = rd;
69 if(personItems != null)
70 {
71 personItems.Items.Refresh();
72 }
73 }
I'm calling Items.Refresh() so that item templates are regenerated once the skin is changed (again Dr. WPF helped ;) )
The Fancy.xaml defines some more fancy border styles for the templates that I won't bother you with. At the end of the post I'll include a link to the source code of the example, so you will be able to look at it in details. The result is this:
Lovely!
How bad can we go? Very bad ! :)
Recently, one very nasty idea came to my mind. Is it possible to make my application work in such a way that it is independent of data template keys? To be more precise - I want to have many data templates for a particular data type and I want each data template to define for exactly what kind of objects it is for. So for our example, I would like data templates to define for which age they are for. In this way I would be able to have 2 data templates in one skin and 3 in another. Thinking on how to implement such kind of functionality the first and probably the worst idea would be to include any details into the data template key. Thus the template selector will extract all data templates for a specified data object (in our case Person), parse the key and extract any details for that template. For example a data template, defined for our example will look like this:
34 <DataTemplate x:Key="personTemplate_60" DataType="{x:Type local:Person}">
(Data template for a person object that has Age value over 60).
This looks very lame, doesn't it!
A better approach would be to have our own data template type, which extends the framework's DataTemplate class and has the necessary properties (like Age). If we want to take that approach a template definition would probably look like this:
34 <local:MyDataTemplate x:Key="personTemplate" Age="60" DataType="{x:Type local:Person}">
I tried this approach. Unfortunately, it did not work. Although it looks very natural and there shouldn't be any problems implementing it, a problem appeared. While from Xaml syntax and OO perspective it is correct, the Xaml (Baml) reader does not think so - an error occurs. Searching deeper I found that the reader recognizes only template types defined by the framework. If you define (inherit from existing) your own, it directly treats it as an object and not template, which results into an error. So, here is the work-around I made to implement what I wanted.
First and foremost I need a method that will extract all resources of a given type from all available into some resource dictionary. Do I hear extension method? Yes, that's it! I created an extension method for the ResourceDictionary class:
26 public static List<T> GetAllOfType<T>(this ResourceDictionary dictionary)
27 {
28 List<T> result = new List<T>();
29 foreach (var resource in dictionary.Values)
30 {
31 if (resource is T)
32 {
33 result.Add((T) resource);
34 }
35 }
36 if (dictionary.MergedDictionaries != null)
37 {
38 foreach (var mergedDictionary in dictionary.MergedDictionaries)
39 {
40 result.AddRange(mergedDictionary.GetAllOfType<T>());
41 }
42 }
43 return result;
44 }
Next I create MyDataTemplate class but not as an inheritor of DataTemplate. Rather it is a stand alone class. Its definition looks more or less like this:
7 [ContentProperty("Template")]
8 public class MyDataTemplate
9 {
10 public DataTemplate Template { get; set; }
11 public Type DataType { get; set; }
12 public int Age { get; set; }
13 }
So I have the following definition placed into the resource dictionary file:
35 <local:MyDataTemplate x:Key="personTemplate" Age="60" DataType="{x:Type local:Person}">
36 <DataTemplate >
37 <Border x:Name="personsBorder" Style="{StaticResource PersonBorderStyle}">
38 <Grid>
39 <StackPanel Orientation="Horizontal">
40 <Image Width="40" Height="40" Source="{Binding Path=ImageRef, Converter={StaticResource imageConverter}}">
41 <Image.BitmapEffect>
42 <DropShadowBitmapEffect />
43 </Image.BitmapEffect>
44 </Image>
45 <TextBlock x:Name="personName" Text="{Binding Name}" Padding="15,15" Foreground="Black" />
46 </StackPanel>
47 </Grid>
48 </Border>
49 </DataTemplate>
50 </local:MyDataTemplate>
Now that we have changed our resource dictionaries, we need to change the template selector. It get all necessary data that allows making the right choice of template. And what is more important, it can be guided only by object type and properties rather than resource keys.
8 public class MySelector : DataTemplateSelector
9 {
10 public override DataTemplate SelectTemplate(object item, DependencyObject container)
11 {
12 var person = item as Person;
13 if (person != null)
14 {
15 var allTemplates = Application.Current.Resources.GetAllOfType<MyDataTemplate>();
16 //From all custom templates, select only those that are for our element's type or are defined for some of its base types
17 var templates = from template in allTemplates
18 where template.DataType == item.GetType() || template.DataType.IsAssignableFrom(item.GetType())
19 select template;
20 //Filter only the template that is for age less than or equal to the item's age
21 var filteredTemplate = (from template in templates
22 where template.Age <= person.Age
23 orderby template.Age descending
24 select template).Take(1);
25
26 List<MyDataTemplate> templateList = filteredTemplate.ToList();
27 if (templateList.Count > 0)
28 {
29 return templateList[0].Template;
30 }
31 }
32 return base.SelectTemplate(item, container);
33 }
34 }
Now lets go crazy! In the Default.xaml we have 2 templates that define how the person should be presented if he/she is younger or older than 60. In the Fancy.xaml file we can define as many templates as we want (3,4 .. etc.) that can define how a person should look for some other age. Lets define one for the age of 20 - this way everyone, from the test people we created, will have different template:
68 <Style x:Key="TwentyYearsOldPersonBorderStyle" TargetType="Border">
69 <Setter Property="Background" >
70 <Setter.Value>
71 <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
72 <GradientStop Color="#FF000000" Offset="0"/>
73 <GradientStop Color="#FFF500DC" Offset="1"/>
74 </LinearGradientBrush>
75 </Setter.Value>
76 </Setter>
77 <Setter Property="BorderBrush" Value="Black" />
78 <Setter Property="BorderThickness" Value="2" />
79 <Setter Property="CornerRadius" Value="8" />
80 <Setter Property="Margin" Value="2,4" />
81 </Style>
82 <local:MyDataTemplate x:Key="twentyYearsOldPersonTemplate" Age="20" DataType="{x:Type local:Person}">
83 <DataTemplate >
84 <Border x:Name="personsBorder" Style="{StaticResource TwentyYearsOldPersonBorderStyle}">
85 <Grid>
86 <StackPanel Orientation="Horizontal">
87 <Image Width="40" Height="40" Source="{Binding Path=ImageRef, Converter={StaticResource imageConverter}}">
88 <Image.BitmapEffect>
89 <DropShadowBitmapEffect />
90 </Image.BitmapEffect>
91 </Image>
92 <TextBlock x:Name="personName" Text="{Binding Name}" Padding="15,15" Foreground="Black" />
93 </StackPanel>
94 </Grid>
95 </Border>
96 </DataTemplate>
97 </local:MyDataTemplate>
Visually the result is like this:
In conclusion, what we get is a template based visualization of our data object that allows us to create more flexible skins/themes. There, we can define not only how should our object be displayed, but also which object exactly we want to be customizes (how old should the person be). The example utilizes only one property - the Age, but in practice we can use as many as we'd like.
And here is the source for the example project.