1. Problem
You need to apply a custom UI
to data and specify how various parts of a complex data structure are
bound to various parts of your complex UI. You also need this
representation encapsulated so that it can be reused across your
application wherever the related data structure is employed.
2. Solution
Define a DataTemplate and specify appropriate bindings to bind parts of the backing data structure to elements of the data template. Apply the DataTemplate where possible to apply a consistent UI to the bound data.
3. How It Works
A DataTemplate
offers a way to provide a repeatable and consistent visual
representation for a portion or all of a specific application data
source within your UI. It encapsulates a portion of your UI and can be
defined in terms of any of the standard drawing primitives and controls
available, as well any custom controls you might write. Appropriate
bindings applied to various properties of the constituent elements ties
the DataTemplate to the backend application data source that it aims to provide the UI for.
3.1. Declaring a DataTemplate
Listing 1 shows a simple DataTemplate that binds the Text properties of several TextBlock controls to properties in a CLR type.
Listing 1. A Simple DataTemplate
<DataTemplate x:Key="dtAddress">
<Grid >
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock x:Name="tblkStreet" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" Text="{Binding Street}"
TextWrapping="Wrap" Foreground="White" FontSize="12"
FontWeight="Bold"/>
<StackPanel Grid.RowSpan="1" Orientation="Horizontal" Grid.Row="1"
VerticalAlignment="Stretch">
<TextBlock x:Name="tblkCity" Text="{Binding City}"
TextWrapping="Wrap" FontSize="12"
FontWeight="Bold" Foreground="White"/>
<TextBlock x:Name="tblkComma" Text="," TextWrapping="Wrap"
Margin="2,0,2,0" FontSize="12" FontWeight="Bold"
Foreground="White"/>
<TextBlock x:Name="tblkState" Text="{Binding State}"
TextWrapping="Wrap" FontSize="12"
FontWeight="Bold" Foreground="White"/>
<TextBlock x:Name="tblkZip" Text="{Binding ZipCode}"
TextWrapping="Wrap" Margin="3,0,0,0" FontSize="12"
FontWeight="Bold" Foreground="White"/>
</StackPanel>
</Grid>
</DataTemplate>
|
Note that a DataTemplate can be declared either as a resource that can be referenced using its x:Key value, as shown in Listing 1, or in place, as Listing 2 shows.
Listing 2. A DataTemplate Declared and Used in Place
<ContentControl x:Name="cntctrlEmployee" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Grid.Column="0" Background="Yellow" Margin="5,5,5,5"
Height="200">
<ContentControl.ContentTemplate>
<DataTemplate>
<TextBlock x:Name="tblkFirstName" Text="{Binding FirstName}"
TextWrapping="Wrap" FontSize="14" FontWeight="Bold"
Foreground="White" Margin="3,0,0,0"/>
</DataTemplate>
</ContentControl.ContentTemplate>
</ContentControl>
|
In Listing 4-5, you define and associate a DataTemplate to the ContentControl.ContentTemplate property in place. For in-place use, the DataTemplate is scoped to the containing element (in this case, the ContentControl.ContentTemplate) and is not available for use outside that scope.
You can also define a DataTemplate as a resource either in the resource section of the page or that of the application. In the former case, the DataTemplate is control scoped—that is, it is available for use anywhere on the MainPage (which is a UserControl).
In the latter case, it is available for use anywhere in the entire
application. In keeping with the rules, anything stored as a resource in
ResourceDictionaries, such a DataTemplate, needs an x:Key defined so that it can be referenced for use via the StaticResource extension.
3.2. Using a DataTemplate
So how do you use a DataTemplate? You can apply one to either a ContentControl (or a derived control, like Button) or an ItemsControl (or a derived control, like ListBox). To apply the DataTemplate, you set the ContentControl.ContentTemplate property or the ItemsControl.ItemTemplate property to the DataTemplate, as shown here:
<ContentControl ContentTemplate="{StaticResource dtAddress}" />
<ListBox ItemTemplate="{StaticResource dtAddress}" />
At runtime, the data bound to the ContentControl.Content property, or each data item in the data collection bound to the ItemsControl.ItemsSource property, is used to provide data for the bound properties in the DataTemplate.
4. The Code
Listing 3 shows code for the classes that provide the data for this sample.
Listing 3. Data Classes
namespace Recipe4_2
{
public class Employee
{
public string FirstName { get; set; }
public string LastName { get; set; }
public long PhoneNum { get; set; }
public string ImageUri
{
get
{
return "/" + FirstName + ".png";
}
}
public Address Address { get; set; }
}
public class Address
{
public string Street { get; set; }
public string City { get; set; }
public string State { get; set; }
public int ZipCode { get; set; }
}
}
|
Listing 4 shows the code to initialize the data, defined in the constructor of the MainPage class, in the codebehind file for the MainPage.
Listing 4. Data Initialization
using System.Collections.Generic;
using System.Windows.Controls;
namespace Recipe4_2
{
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
List<Employee> EmployeeList = new List<Employee>();
EmployeeList.Add(new Employee
{
FirstName = "Joe",
LastName = "Duffin",
PhoneNum = 2125551212,
Address = new Address { Street = "2000 Mott Street",
City = "New York", State = "NY", ZipCode = 10006 }
});
EmployeeList.Add(new Employee
{
FirstName = "Alex",
LastName = "Bleeker",
PhoneNum = 7185551212,
Address = new Address { Street = "11000 Clover Street",
City = "New York", State = "NY", ZipCode = 10007 }
});
EmployeeList.Add(new Employee
{
FirstName = "Nelly",
LastName = "Myers",
PhoneNum = 7325551212,
Address = new Address { Street = "12000 Fay Road",
City = "New York", State = "NY", ZipCode = 10016 }
});
cntctrlEmployee.Content = EmployeeList[0];
itmctrlEmployees.ItemsSource = EmployeeList;
}
}
}
|
You define two data templates, one each for the Address type and the Employee type in the MainPage.xaml file,
as shown in Listing 5.
Listing 5. DataTemplates for the Address and Employee Data Types
<UserControl.Resources>
<DataTemplate x:Key="dtAddress">
<Grid >
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock x:Name="tblkStreet" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" Text="{Binding Street}"
TextWrapping="Wrap" Foreground="White" FontSize="12"
FontWeight="Bold"/>
<StackPanel Grid.RowSpan="1" Orientation="Horizontal" Grid.Row="1"
VerticalAlignment="Stretch">
<TextBlock x:Name="tblkCity" Text="{Binding City}"
TextWrapping="Wrap" FontSize="12"
FontWeight="Bold" Foreground="White"/>
<TextBlock x:Name="tblkComma" Text="," TextWrapping="Wrap"
Margin="2,0,2,0" FontSize="12" FontWeight="Bold"
Foreground="White"/>
<TextBlock x:Name="tblkState" Text="{Binding State}"
TextWrapping="Wrap" FontSize="12"
FontWeight="Bold" Foreground="White"/>
<TextBlock x:Name="tblkZip" Text="{Binding ZipCode}"
TextWrapping="Wrap" Margin="3,0,0,0" FontSize="12"
FontWeight="Bold" Foreground="White"/>
</StackPanel>
</Grid>
</DataTemplate>
<DataTemplate x:Key="dtEmployee">
<Grid Height="Auto" Width="300" Margin="5,5,5,5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="0.508*"/>
<ColumnDefinition Width="0.492*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="0.801*" />
<RowDefinition Height="0.199*"/>
</Grid.RowDefinitions>
<Rectangle HorizontalAlignment="Stretch" Margin="0,−74.9660034179688,0,0"
Stroke="#FF000000" Grid.Row="1" Grid.RowSpan="1" RadiusX="3"
RadiusY="3" StrokeThickness="0" Fill="#FF9FA8E4"/>
<Rectangle HorizontalAlignment="Stretch" Margin="0,0,0,0"
Grid.ColumnSpan="2" Grid.RowSpan="1" RadiusX="3"
RadiusY="3" Stroke="#FF686868" StrokeThickness="0"
Width="Auto">
<Rectangle.Fill>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FF000000"/>
<GradientStop Color="#FF9FA8E4" Offset="1"/>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Rectangle HorizontalAlignment="Stretch" Margin="3,3,3,3"
Stroke="#FF0A28EE" Grid.RowSpan="1"
StrokeThickness="5" VerticalAlignment="Stretch"/>
<Image Margin="8,8,8,8" x:Name="imgEmployee"
Source="{Binding ImageUri}"
Stretch="Fill"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Grid.RowSpan="1"/>
<StackPanel Margin="0,−0.114000000059605,0,0" Orientation="Horizontal"
Grid.Row="1" Grid.ColumnSpan="1" VerticalAlignment="Stretch"
Grid.RowSpan="1">
<TextBlock x:Name="tblkFirstName" Text="{Binding FirstName}"
TextWrapping="Wrap" FontSize="14" FontWeight="Bold"
Foreground="White" Margin="3,0,0,0"/>
<TextBlock x:Name="tblkLastName" Text="{Binding LastName}"
TextWrapping="Wrap" FontSize="14" FontWeight="Bold"
Margin="3,0,0,0" Foreground="White"/>
</StackPanel>
<StackPanel Margin="0,0,0,0" Grid.Column="1">
<ContentControl ContentTemplate="{StaticResource dtAddress}"
Content="{Binding Address}" Foreground="#FF0A28EE" />
<TextBlock x:Name="tblkPhoneNum" Text="{Binding PhoneNum}"
TextWrapping="Wrap" FontSize="12" FontWeight="Bold"
Margin="0,5,0,0" Foreground="White"/>
</StackPanel>
</Grid>
</DataTemplate>
</UserControl.Resources>
|
You can see that a DataTemplate can, in turn, use another DataTemplate in a nested fashion. In dtEmployee earlier, you use a ContentControl to display an employee's address, and you reuse dtAddress as the ContentTemplate. This kind of reuse helps facilitate the consistency in UI representation of data, keeping in line with the promise of DataTemplates.
Applying the DataTemplate is simple. Let's apply it to a ContentControl like so
<ContentControl x:Name="cntctrlEmployee" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Grid.Column="0" Background="Yellow" Margin="5,5,5,5"
ContentTemplate="{StaticResource dtEmployee}" Height="200"/>
and bind it to the first Employee in the EmployeeList collection, as shown in the MainPage's constructor code in Listing 4-7, like so
cntctrlEmployee.Content = EmployeeList[0];
Figure 1 shows the DataTemplate in action.
Let's also apply the same DataTemplate to a ListBox, like so:
<ListBox x:Name="itmctrlEmployees"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Grid.Column="1"
Width="325"
ItemTemplate="{StaticResource dtEmployee}"
Height="400"/>
Them, you bind it to the entire EmployeeList collection, as shown in the MainPage's constructor code in Listing 4, like so:
itmctrlEmployees.ItemsSource = EmployeeList;
This time, you see the DataTemplate being applied to each item in the ListBox but producing a consistent UI, as shown in Figure 2.