Chapter 25. Templates


Several programs in the first part of this book provided a little taste of templates. In Chapter 11, the BuildButtonFactory project showed how to create an object of type ControlTemplate and assign it to the Template property of a Button. This ControlTemplate object was essentially a complete description of the visual appearance of the button, including how its appearance changed when certain properties (such as IsMouseOver and IsPressed) changed values. All of the logic behind the button, and all the event handling, remained intact.

The ControlTemplate is one important type of template supported by the Windows Presentation Foundation. As its name suggests, you use the ControlTemplate to define the visual appearance of a control. The Control class defines the Template property that you set to a ControlTemplate object.

Although styles and templates may seem to overlap, they really have quite different roles. An element or control does not have a default Style property, and consequently, the Style property of an element is normally null. You use the Style property to define property settings or triggers that you want associated with that element.

All controls defined within the Windows Presentation Foundation that have a visual appearance already have a Template property that is set to an object of type ControlTemplate. A Button looks like a Button and a ScrollBar looks like a ScrollBar as a direct result of these ControlTemplate objects. The ControlTemplate object defines the entire visual appearance of the control, and you have the power to replace that object. This is what is meant when the controls in the WPF are referred to (rather awkwardly) as "lookless" controls. They certainly have a "look" but it's not intrinsic to the functionality of the control and it can be replaced.

The ListColorsEvenElegantlier project in Chapter 13 introduced the DataTemplate object. The ItemsControl class (from which ListBox descends) defines a property named ItemTemplate, and you set a DataTemplate object to that property to govern how the control displays each of the items in the ListBox. As you might recall, the DataTemplate in the ListColorsEvenElegantlier program defined a visual tree that contained a Rectangle element and a TextBlock element to display each color in the ListBox.

Chapter 13 also presented two custom controls named ColorGridBox and ColorWheel, both of which were derived from ListBox but which presented the items in a completely different format from the ListBox default. This was made possible by the ItemsPanel property defined by ItemsControl that you can set to an object of type ItemsPanelTemplate. As the name suggests, this object defines the type of panel used for displaying the items within the ListBox.

Templates also made an appearance in several projects in Chapter 16. The ListSortedSystemParameters project and the DependencyPropertyListView control both used ListView controls, and both set the View property of the ListView control to a GridView object. The GridView object lets the ListView control display objects in columns. Whenever a column doesn't display quite what you want, you can create a DataTemplate that defines the appearance of the object in that column, and set this object to the CellTemplate property of the GridViewColumn object. Also in Chapter 16, the TemplateTheTree project created an object of type HierarchicalDataTemplate and set it to the ItemTemplate property of the TreeViewItem objects to control how the items of the tree obtained child objects.

The various types of template objects all derive from the abstract FrameworkTemplate class, as shown in the following class hierarchy:

Object

       FrameworkTemplate (abstract)

                 ControlTemplate

                 DataTemplate

                         HierarchicalDataTemplate

                 ItemsPanelTemplate

Obviously some practice is required before these different types of templates (and the properties you set them to) assume individual and familiar personalities. Here's a brief summary:

You create objects of type ControlTemplate to set to the Template property defined by Control. These ControlTemplate objects define the entire visual appearance of the control, including triggered changes to the visual appearance. Setting the Template property is quite powerful but also (of course) involves much responsibility.

You use objects of type ItemsPanelTemplate to set to the ItemsPanel property defined by ItemsControl to specify the type of panel used to display the multiple items in an ItemsControl (such as a ListBox or ComboBox). This is the simplest type of template.

All other templates are of type DataTemplate, and in actual practice are certainly the most common form of template. DataTemplate objects show up in all controls that derive from ContentControl and ItemsControl and let you govern how content and listed items are displayed.

The abstract FrameworkTemplate class defines only three properties. The read-only Boolean IsSealed property indicates whether the template can be altered. The Resources property of type ResourceDictionary lets you define resources that are accessible only within the template. The most important of the three properties is VisualTree, which defines the layout of elements that make up the visual appearance of the control (or the content of the control, or the listed items in the control).

VisualTree is of type FrameworkElementFactory, and if you look back at the projects that used templates in Part I of this book, you'll be reminded just how strange and unwieldy this FrameworkElementFactory was. This class allows you to define a hierarchy of elements (including properties and triggers) without actually creating those elements. The resultant code wasn't very pretty.

When you define templates in XAML, FrameworkElementFactory works entirely behind the scenes. The XAML syntax used in defining templates is the same as that which you use when defining a layout of elements and their properties, except that these elements aren't actually created until they're required.

The ControlTemplate class adds two properties to those defined by FrameworkTemplate: TargetType indicates the type of the control for which the template is intended; Triggers is a collection of Trigger objects.

The following stand-alone XAML file is quite similar to the BuildButtonFactory program from Chapter 11 except that it doesn't attach a Click event handler to the Button.

ButtonWithTemplate.xaml

[View full width]

<!-- === ================================================== ButtonWithTemplate.xaml (c) 2006 by Charles Petzold ============================================= ======== --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml"> <Button HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="48" Padding="20"> Button with Custom Template <Button.Template> <ControlTemplate> <!-- VisualTree property of ControlTemplate. --> <Border Name="border" BorderThickness="3" BorderBrush="Red" Background="{DynamicResource {x:Static SystemColors .ControlLightBrushKey}}"> <TextBlock Name="txtblk" FontStyle="Italic" Text="{TemplateBinding ContentControl.Content}" Margin="{TemplateBinding Control.Padding}" /> </Border> <!-- Triggers property of ControlTemplate. --> <ControlTemplate.Triggers> <Trigger Property="UIElement. IsMouseOver" Value="True"> <Setter TargetName="border" Property="Border .CornerRadius" Value="24" /> <Setter TargetName="txtblk" Property="TextBlock .FontWeight" Value="Bold" /> </Trigger> <Trigger Property="Button .IsPressed" Value="True"> <Setter TargetName="border" Property="Border .Background" Value="{DynamicResource {x:Static SystemColors.ControlDarkBrushKey}}" /> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Button.Template> </Button> </Page>



The Button definition begins normally by defining several properties (HorizontalAlignment, VerticalAlignment, FontSize, and Padding) and the button content, which is the text string "Button with Custom Template."

The Button.Template property element encloses the definition of the Template property of the button. This property is of type ControlTemplate. The content property of FrameworkTemplate and all its derivatives is VisualTree, so the layout of the control can begin immediately after the ControlTemplate start tag. The control begins with a single top-level element. Any additional elements are children of that element. Very often the VisualTree begins with a Border element, but it could also begin with a panel of some sort or even just a single element like TextBlock.

The VisualTree in ButtonWithTemplate.xaml begins with a Border that is given a 3-unit thickness and colored red. The Background property is a system color retrieved using the DynamicResource markup extension. Within the Border element is a TextBlock with a FontStyle initialized to Italic.

By using a TextBlock, I'm implicitly restricting this button to the display of text. I want the Text property of this TextBlock to be set to the Content property of the Button. The markup extension TemplateBinding is defined specifically for the purpose of linking a property in the template's visual tree with a property defined by the control. The syntax for binding the Text property of the TextBlock to the Content property of the Button is:

Text="{TemplateBinding ContentControl.Content}" 


The argument of the TemplateBinding is really a dependency property, so it usually includes the class in which the property is defined or a class that inherits the property. You could use Button.Content here rather than ContentControl.Content and it would work the same way. Fortunately, the WPF template logic is flexible enough to allow a property of type string to have a TemplateBinding with a property of type object.

The next attribute in the TextBlock element is interesting:

Margin="{TemplateBinding Control.Padding}" 


The margin around the TextBlock (that is, the space between the TextBlock and the Border) is bound to the Padding property of the Button, which is intended to be the space inside the button that surrounds the content. This particular TemplateBinding is typical for a ContentControl. Of course, the Button itself still has its own Margin property that defines the space around the button.

After you define the VisualTree in the template, an optional property element of ControlTemplate.Triggers defines triggers. This particular template has two triggersone for the property IsMouseOver and one for the property IsPressed. In both cases, the Trigger comes into action when these properties are true. The Setter elements within these Trigger elements always affect a property of one of the elements of the VisualTree. That is why the two elements in the VisualTree were given names of border and txtblk. The Setter elements refer to these names with the TargetName attribute, and the Property and Value attributes work just as they do in styles.

It is possible to include a TargetType attribute in the ControlTemplate tag:

<ControlTemplate TargetType="{x:Type Button}"> 


In this case, you don't need to preface the dependency properties with class names in the TemplateBinding expressions, because the binding has the information it needs to reference these properties.

As ButtonWithTemplate.xaml demonstrates, you can define a ControlTemplate for just one control, and that's fine if all you want is one unique control. Generally you'll want to share templates, and one way to do that is by defining them as resources. Here's a stand-alone XAML file that contains a ControlTemplate defined as a resource with an x:Key of "btnCustom." I've used the TargetType attribute on ControlTemplate to avoid prefacing dependency properties and routed events with class names.

ResourceTemplate.xaml

[View full width]

<!-- === ================================================ ResourceTemplate.xaml (c) 2006 by Charles Petzold ============================================= ====== --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml"> <Page.Resources> <ControlTemplate x:Key="btnCustom" TargetType="{x:Type Button}"> <!-- VisualTree property of ControlTemplate. --> <Border Name="border" BorderThickness="3" BorderBrush="Red" Background="{TemplateBinding Foreground}"> <TextBlock Name="txtblk" FontStyle="Italic" Text="{TemplateBinding Content}" Margin="{TemplateBinding Padding}" Foreground="{TemplateBinding Background}" /> </Border> <!-- Triggers property of ControlTemplate. --> <ControlTemplate.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter TargetName="border" Property="CornerRadius" Value="12" /> <Setter TargetName="txtblk" Property="FontWeight" Value="Bold" /> </Trigger> <Trigger Property="IsPressed" Value="True"> <Setter TargetName="border" Property="Background" Value="{Binding Path=Background}" /> <Setter TargetName="txtblk" Property="Foreground" Value="{Binding Path=Foreground}" /> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Page.Resources> <StackPanel> <Button Template="{StaticResource btnCustom}" HorizontalAlignment="Center" Margin="24" FontSize="24" Padding="10" > Button with Custom Template </Button> <Button HorizontalAlignment="Center" Margin="24" FontSize="24" Padding="10" > Normal Button </Button> <Button Template="{StaticResource btnCustom}" HorizontalAlignment="Center" Margin="24" FontSize="24" Padding="10" > Another Button with Custom Template </Button> </StackPanel> </Page>



Toward the bottom of the file, a StackPanel contains three buttons, all with the same properties, except that the first and third buttons set their Template properties to the resource. This particular template switches around the meaning of the background and foreground colors. If you have your system colors set for a white background and black foreground, these two templated buttons display white text on a black background and then swap the colors when the buttons are pressed. This is not something you should normally do, but it suggests the type of flexibility you have.

You can combine a Style and a ControlTemplate in the same resource. The Style begins normally and then the Template property is set to an object of type ControlTemplate:

<Style ... >     ...     <Setter Property="Template">         <Setter.Value>             <ControlTemplate ...>                 ...             </ControlTemplate>         </Setter.Value>     </Setter> </Style> 


That way, you can give a control both a style and a template just by setting the Style property.

The first two templates I've shown you in this chapter make the outrageous assumption that the Content property of the Button is set to a text string. If that's not the case, these templates would generate run-time errors. Even if you're entirely redesigning button visuals, you probably want the buttons to host more than just text.

It so happens that all classes that derive from ContentControl use an object of type ContentPresenter to display their content. ContentPresenter derives from FrameworkElement, and you can include a ContentPresenter object within the visual tree of the template. This is a much better approach than assuming that the content will always be text.

The CircledRadioButtons.xaml file defines a ControlTemplate resource for objects of type RadioButton. Notice the empty ContentPresenter element within the Border element.

CircledRadioButtons.xaml

[View full width]

<!-- === =================================================== CircledRadioButtons.xaml (c) 2006 by Charles Petzold ============================================= ========= --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml"> <Page.Resources> <ControlTemplate x:Key="newradio" TargetType="{x:Type RadioButton}"> <Border Name="border" BorderBrush="{DynamicResource {x:Static SystemColors .ControlTextBrushKey}}" Padding="10" CornerRadius="100"> <ContentPresenter /> </Border> <ControlTemplate.Triggers> <Trigger Property="IsChecked" Value="True"> <Setter TargetName="border" Property="BorderThickness" Value="1" /> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Page.Resources> <GroupBox Header="Options" FontSize="12pt" HorizontalAlignment="Center" VerticalAlignment="Center"> <StackPanel> <RadioButton Template="{StaticResource newradio}" HorizontalAlignment="Center" Content="RadioButton 1" /> <RadioButton Template="{StaticResource newradio}" HorizontalAlignment="Center" Content="RadioButton 2" IsChecked="True" /> <RadioButton Template="{StaticResource newradio}" HorizontalAlignment="Center" Content="RadioButton 3" /> <RadioButton Template="{StaticResource newradio}" HorizontalAlignment="Center" Content="RadioButton 4" /> </StackPanel> </GroupBox> </Page>



You can bind the Content property of the ContentPresenter to the Content property of the control with markup like this:

<ContentPresenter Content="{TemplateBinding ContentControl.Content}" /> 


But this XAML file indicates that this binding occurs automatically. The Border element in the template defines a CornerRadius of 100, which should be sufficient to mimic an ellipse in most reasonably sized content of radio buttons. By default, the BorderThickness of a Border element is 0. The Triggers section of the template changes that to 1 only when the item is selected. The result is a RadioButton indicating that it is selected with an ellipse around the entire content, rather than with a little filled-in circle.

The following XAML file goes even further in redefining the appearance of a control. It uses the logic of a CheckBox but displays a little visual switch that flips a lever between the words Off and On.

ToggleSwitch.xaml

[View full width]

<!-- =============================================== ToggleSwitch.xaml (c) 2006 by Charles Petzold ============================================= == --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml"> <Page.Resources> <ControlTemplate x:Key="switch" TargetType="{x:Type CheckBox}"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <Border Width="96" Height="48" BorderBrush="Black" BorderThickness="1"> <Canvas Background="LightGray"> <TextBlock Canvas.Left="0" Canvas.Top="0" Foreground="Black" Text="Off" Margin="2" /> <TextBlock Canvas .Right="0" Canvas.Top="0" Foreground="Black" Text="On" Margin="2" /> <Line Name="lineOff" StrokeThickness="8" Stroke="Black" X1="48" Y1="40" X2="20" Y2="16" StrokeStartLineCap="Round" StrokeEndLineCap="Round" /> <Line Name="lineOn" StrokeThickness="8" Stroke="Black" X1="48" Y1="40" X2="76" Y2="16" StrokeStartLineCap="Round" StrokeEndLineCap="Round" Visibility="Hidden" /> </Canvas> </Border> <ContentPresenter Grid.Row="1" Content="{TemplateBinding Content}" HorizontalAlignment="Center" /> </Grid> <ControlTemplate.Triggers> <Trigger Property="IsChecked" Value="True"> <Setter TargetName="lineOff" Property="Visibility" Value="Hidden" /> <Setter TargetName="lineOn" Property="Visibility" Value="Visible" /> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Page.Resources> <CheckBox Template="{StaticResource switch}" Content="Master Switch" HorizontalAlignment="Center" VerticalAlignment="Center" /> </Page>



The visual tree begins with a two-row Grid. The bottom row is for the ContentPresenter. The top row is the graphical switch. It begins with a rectangle Border with a gray background containing a Canvas. The Canvas has two TextBlock elements for the Off and On labels and two thick lines with Name attributes of "lineOff" and "lineOn." Notice that the lineOn Line has its Visibility property initially set to Hidden.

The Triggers section contains a Trigger for the IsChecked property. When this property is true, the Visibility property of the lineOff Line changes to Hidden and the Visiblity property of lineOn changes to Visible, making it appear as if a line jumps between the two options.

I mentioned earlier that any control defined by the WPF that has a visual appearance already has its Template property set to an object of type ControlTemplate. When you set the Template property to a ControlTemplate of your own invention, you are essentially replacing the original template. If you want to create ControlTemplate objects that come up to the standards of the original templates, you'll want to examine those templates, and that's the entire purpose of the following program.

The DumpControlTemplate program has a top-level menu item labeled Control that displays all the public classes that derive from Control in a hierarchy that mimics the inheritance hierarchy. You select one of these controls, and the program creates an object of that type and displays it in the top part of a splitter-separated Grid. You then select the Template Property item from the Dump menu, and the program uses the bottom part of the Grid to display the ControlTemplate object in convenient XAML format. The XAML is displayed in a TextBox, so you can copy and paste it into Windows Notepad (or NotepadClone) for printing.

The DumpControlTemplate project has three source code files. The ControlMenuItem class inherits from MenuItem and constructs a menu containing all the classes that derive from Control. The Header of each item displays the name of the class; the Tag property stores the Type object for that class.

ControlMenuItem.cs

[View full width]

//------------------------------------------------ // ControlMenuItem.cs (c) 2006 by Charles Petzold //------------------------------------------------ using System; using System.Collections.Generic; using System.Reflection; using System.Windows; using System.Windows.Controls; namespace Petzold.DumpControlTemplate { public class ControlMenuItem : MenuItem { public ControlMenuItem() { // Obtain the assembly in which the Control class is defined. Assembly asbly = Assembly.GetAssembly (typeof(Control)); // This is an array of all the types in that class. Type[] atype = asbly.GetTypes(); // We're going to store descendants of Control in a sorted list. SortedList<string, MenuItem> sortlst = new SortedList<string, MenuItem>(); Header = "Control"; Tag = typeof(Control); sortlst.Add("Control", this); // Enumerate all the types in the array. // For Control and its descendants, create menu items and // add to the SortedList object. // Notice the menu item Tag property is the associated Type object. foreach (Type typ in atype) { if (typ.IsPublic && (typ .IsSubclassOf(typeof(Control)))) { MenuItem item = new MenuItem(); item.Header = typ.Name; item.Tag = typ; sortlst.Add(typ.Name, item); } } // Go through the sorted list and set menu item parents. foreach (KeyValuePair<string, MenuItem> kvp in sortlst) { if (kvp.Key != "Control") { string strParent = ((Type)kvp .Value.Tag).BaseType.Name; MenuItem itemParent = sortlst[strParent]; itemParent.Items.Add(kvp.Value); } } // Scan through again: // If abstract and selectable, disable. // If not abstract and not selectable , add a new item. foreach (KeyValuePair<string, MenuItem> kvp in sortlst) { Type typ = (Type)kvp.Value.Tag; if (typ.IsAbstract && kvp.Value. Items.Count == 0) kvp.Value.IsEnabled = false; if (!typ.IsAbstract && kvp.Value. Items.Count > 0) { MenuItem item = new MenuItem(); item.Header = kvp.Value.Header as string; item.Tag = typ; kvp.Value.Items.Insert(0, item); } } } } }



The DumpControlTemplate.xaml file contains the layout of the program's window. Notice that the menu begins with the ControlMenuItem object, which is assigned a Click event handler for all items in the submenu. The Dump menu item has two items on its submenu: one to dump the Template property and the other to dump the ItemsPanel property. (The latter is applicable only for controls that derive from ItemsControl.) The file concludes with a Grid that occupies the rest of the program's window.

DumpControlTemplate.xaml

[View full width]

<!-- === =================================================== DumpControlTemplate.xaml (c) 2006 by Charles Petzold ============================================= ========= --> <Window xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" xmlns:src="/books/4/266/1/html/2/clr-namespace:Petzold .DumpControlTemplate" x:0" width="14" height="9" align="left" src="/books/4/266/1/html/2/images/ccc.gif" />.DumpControlTemplate" Title="Dump Control Template - no control"> <DockPanel> <Menu DockPanel.Dock="Top"> <src:ControlMenuItem MenuItem .Click="ControlItemOnClick"/> <MenuItem Header="Dump" SubmenuOpened="DumpOnOpened"> <MenuItem Header="Template property (type ControlTemplate)" Name="itemTemplate" Click="DumpTemplateOnClick" /> <MenuItem Header="ItemsPanel property (type ItemsPanelTemplate)" Name="itemItemsPanel" Click="DumpItemsPanelOnClick" /> </MenuItem> </Menu> <Grid Name="grid"> <Grid.RowDefinitions> <RowDefinition Height="2*" /> <RowDefinition Height="Auto" /> <RowDefinition Height="8*" /> </Grid.RowDefinitions> <GridSplitter Grid.Row="1" Height="6" HorizontalAlignment="Stretch" VerticalAlignment="Center" /> <TextBox Grid.Row="2" Name="txtbox" FontFamily="Lucida Console" AcceptsReturn="True" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" /> </Grid> </DockPanel> </Window>



The following DumpControlTemplate.cs file is mostly devoted to the Click handlers for the menu. The ControlItemOnClick handler creates a control of the selected type and puts it in the top cell of the Grid. (In my experience, the Template property only becomes non-null when the control has a parent.) The Dump menu items convert the object to XAML for display.

DumpControlTemplate.cs

[View full width]

//---------------------------------------------------- // DumpControlTemplate.cs (c) 2006 by Charles Petzold //---------------------------------------------------- using System; using System.Reflection; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Markup; using System.Xml; namespace Petzold.DumpControlTemplate { public partial class DumpControlTemplate : Window { Control ctrl; [STAThread] public static void Main() { Application app = new Application(); app.Run(new DumpControlTemplate()); } public DumpControlTemplate() { InitializeComponent(); } // Event handler for item clicked on Control menu. void ControlItemOnClick(object sender, RoutedEventArgs args) { // Remove any existing child from the first row of the Grid. for (int i = 0; i < grid.Children .Count; i++) if (Grid.GetRow(grid.Children[i]) == 0) { grid.Children.Remove(grid .Children[i]); break; } // Clear the TextBox. txtbox.Text = ""; // Get the Control class of the clicked menu item. MenuItem item = args.Source as MenuItem; Type typ = (Type)item.Tag; // Prepare to create an object of that type. ConstructorInfo info = typ .GetConstructor(System.Type.EmptyTypes); // Try creating an object of that type. try { ctrl = (Control)info.Invoke(null); } catch (Exception exc) { MessageBox.Show(exc.Message, Title); return; } // Try putting it in the grid. // If that doesn't work, it's probably a Window. try { grid.Children.Add(ctrl); } catch { if (ctrl is Window) (ctrl as Window).Show(); else return; } Title = Title.Remove(Title.IndexOf ('-')) + "- " + typ.Name; } // When Dump menu is opened, enable items. void DumpOnOpened(object sender, RoutedEventArgs args) { itemTemplate.IsEnabled = ctrl != null; itemItemsPanel.IsEnabled = ctrl != null && ctrl is ItemsControl; } // Dump Template object attached to ControlTemplate property. void DumpTemplateOnClick(object sender, RoutedEventArgs args) { if (ctrl != null) Dump(ctrl.Template); } // Dump ItemsPanelTemplate object attached to ItemsPanel property. void DumpItemsPanelOnClick(object sender, RoutedEventArgs args) { if (ctrl != null && ctrl is ItemsControl) Dump((ctrl as ItemsControl) .ItemsPanel); } // Dump the template. void Dump(FrameworkTemplate template) { if (template != null) { // Dump the XAML into the TextBox. XmlWriterSettings settings = new XmlWriterSettings(); settings.Indent = true; settings.IndentChars = new string (' ', 4); settings.NewLineOnAttributes = true; StringBuilder strbuild = new StringBuilder(); XmlWriter xmlwrite = XmlWriter .Create(strbuild, settings); try { XamlWriter.Save(template, xmlwrite); txtbox.Text = strbuild.ToString(); } catch (Exception exc) { txtbox.Text = exc.Message; } } else txtbox.Text = "no template"; } } }



These templates provide a complete description of the visuals in every control defined by the Windows Presentation Foundation, including references to the SystemParameters and SystemColors classes. You can get lots of ideas about designing your own templates, and perhaps pick up a few tricks of the masters. It's the closest you can get to seeing actual WPF source code.

This is the general way in which the template draws small graphical objects, such as the RadioButton circle or the CheckBox square. You'll learn about the format of these strings in Chapter 28.

Some controls use other classes to render their visuals. The Button class makes extensive use of the ButtonChrome class defined in the Microsoft.Windows.Themes namespace. The ScrollBar class uses the ScrollChrome class defined there as well. The TextBox and RichTextBox controls have relatively simple templates because the controls are just Border elements enclosing ScrollViewer controls enclosing the actual editor, which is obviously implemented entirely in code.

So far in this chapter you've seen how you can set the Template property of a Button (or RadioButton or CheckBox) to an object of type ControlTemplate to completely redefine the visual appearance of the control. In most cases, this is probably a bigger job than you want to take on. More common, perhaps, will be a desire to monkey with the display of content in the control.

The ContentPresenter element is responsible for displaying content in all controls that derive from ContentControl. ContentPresenter divides the world into two types of objects: those that inherit from UIElement and those that don't. For those that don't, ContentPresenter uses the object's ToString method to display a text representation of the object. This feature allows controls that derive from ContentControl to be very versatile in displaying almost any type of content, but perhaps you'd like a bit more flexibility. Perhaps you'd like to set the content of a ContentControl to an object that does not derive from UIElement, but you want this object to be rendered in a way that's more sophisticated than ToString.

For example, consider the following class that defines a couple of simple properties that you might retain for each of your employees.

Employee.cs

[View full width]

//----------------------------------------- // Employee.cs (c) 2006 by Charles Petzold //----------------------------------------- using System; namespace Petzold.ContentTemplateDemo { public class Employee { // Private fields. string name; string face; DateTime birthdate; bool lefthanded; // Parameterless constructor used in XAML. public Employee() { } // Constructor with parameter used in C# code. public Employee(string name, string face, DateTime birthdate, bool lefthanded) { Name = name; Face = face; BirthDate = birthdate; LeftHanded = lefthanded; } // Public properties. public string Name { set { name = value; } get { return name; } } public string Face { set { face = value; } get { return face; } } public DateTime BirthDate { set { birthdate = value; } get { return birthdate; } } public bool LeftHanded { set { lefthanded = value; } get { return lefthanded; } } } }



The Face property is defined as a string but it's actually a file name that references a bitmap file of the employee's face. Otherwise, these properties are fairly straightforward. (Why would you need to know if an employee is left-handed? Perhaps you run a factory where machines are designed to be operated by either a right-handed or left-handed person, and you need to pair the right employee with the right machine.)

Suppose you have several objects of type Employee, and you'd like to display those objects in buttons. Of course, you can simply set the Content property of the Button to the Employee object, but Employee doesn't even have a ToString method, so what you're going to get is the string "Petzold.ContentTemplateDemo.Employee", and that doesn't tell you much. (As the namespace declaration indicates, Employee.cs is the first file of a project named ContentTemplateDemo.)

You already know how to display a complex object in a Button. You first set the Content property of the Button to a panel of some sort. As children of this panel, you add other elements (probably TextBlock and Image, in this case) for the various properties of the Employee class. If you were doing this in C# code, you'd probably use this solution because everything can go in a for or foreach loop.

With XAML, however, you can take another approach. You can define a template specifically for the display of Employee objects and then just set the Content property of the Button to the Employee object.

As you know, ContentControl uses a ContentPresenter object to display content. ContentPresenter defines a property named ContentTemplate of type DataTemplate for defining a template used to display content. This ContentTemplate property is also exposed by ContentControl itself. By default, ContentTemplate is null.

You can define this ContentTemplate property in the actual Button element just as you define the Template property. Or, you can create a resource of type DataTemplate and assign that to the ContentTemplate property of the Button. Or, you can create a new class, named perhaps EmployeeButton, and define the ContentTemplate within that class, as is done here.

EmployeeButton.xaml

[View full width]

<!-- ================================================= EmployeeButton.xaml (c) 2006 by Charles Petzold ============================================= ==== --> <Button xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" xmlns:src="/books/4/266/1/html/2/clr-namespace:Petzold .ContentTemplateDemo" x:0" width="14" height="9" align="left" src="/books/4/266/1/html/2/images/ccc.gif" />.EmployeeButton"> <Button.ContentTemplate> <DataTemplate DataType="{x:Type src :Employee}"> <DockPanel> <Image DockPanel.Dock="Left" Stretch="None" Source="{Binding Path=Face}" /> <UniformGrid Rows="2" VerticalAlignment="Center" Margin="12"> <TextBlock FontSize="16pt" TextAlignment="Center" Text="{Binding Path=Name}"/> <StackPanel Orientation="Horizontal" TextBlock .FontSize="12pt"> <TextBlock Text="{Binding Path=BirthDate.Month}" /> <TextBlock Text="/" /> <TextBlock Text="{Binding Path=BirthDate.Day}" /> <TextBlock Text="/" /> <TextBlock Text="{Binding Path=BirthDate.Year}" /> </StackPanel> </UniformGrid> </DockPanel> </DataTemplate> </Button.ContentTemplate> </Button>



You can tell that this file defines a new class by the x:Class attribute in the root element. A Button.ContentTemplate property element surrounds a DataTemplate definition. The DataType of the object we're interested in displaying is Employee, which requires an XML namespace prefix of src.

As with ControlTemplate objects, the DataTemplate customarily begins with a visual tree. The tree in this case consists of a DockPanel with an Image element docked at the left and a UniformGrid occupying the interior. The UniformGrid has a TextBlock for the employee's name and then a StackPanel with six TextBlock elements that effectively format the employee's birth date.

In the Image and TextBlock elements, various properties of the Employee class are referenced with bindings. These bindings are the key to getting the information from an Employee object into the elements of the button's visual true in exactly the way you want.The Image element has the following attribute:

Source="{Binding Path=Face}" 


Only the property name of Face is required here because the property will be accessed from the object set to the Content of the Button. The first part of the birth date display is a little more elaborate because it references a particular property of the BirthDate property:

<TextBlock Text="{Binding Path=BirthDate.Month}" /> 


The EmployeeButton class has the following code-behind file.

EmployeeButton.cs

//----------------------------------------------- // EmployeeButton.cs (c) 2006 by Charles Petzold //----------------------------------------------- namespace Petzold.ContentTemplateDemo {     public partial class EmployeeButton     {         public EmployeeButton()         {             InitializeComponent();         }     } } 



Since EmployeeButton.xaml contains no Name attributes and no events, I'm not exactly sure why this file is needed, but I've included it for the best reason ever: The program doesn't work without it.

We are now ready to display some of our employees with these buttons. For the employee portraits, the ContentTemplateDemo project includes royalty-free image files obtained from the stock.xchng Web site (www.sxc.hu).

ContentTemplateDemo.xaml

[View full width]

<!-- === =================================================== ContentTemplateDemo.xaml (c) 2006 by Charles Petzold ============================================= ========= --> <Window xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" xmlns:src="/books/4/266/1/html/2/clr-namespace:Petzold .ContentTemplateDemo" x:0" width="14" height="9" align="left" src="/books/4/266/1/html/2/images/ccc.gif" />.ContentTemplateDemo" Title="ContentProperty Demo"> <Window.Resources> <Style TargetType="{x:Type src :EmployeeButton}"> <Setter Property="HorizontalAlignment" Value="Center" /> <Setter Property="VerticalAlignment" Value="Center" /> <Setter Property="Margin" Value="12" /> </Style> </Window.Resources> <StackPanel Name="stack" Button.Click="EmployeeButtonOnClick"> <src:EmployeeButton> <src:EmployeeButton.Content> <src:Employee Name="Betty" BirthDate="8/31/1970" Face="Betty.png"/> </src:EmployeeButton.Content> </src:EmployeeButton> <src:EmployeeButton> <src:EmployeeButton.Content> <src:Employee Name="Edgar" BirthDate="2/2/1965" Face="Edgar.png"/> </src:EmployeeButton.Content> </src:EmployeeButton> <src:EmployeeButton> <src:EmployeeButton.Content> <src:Employee Name="Sally" BirthDate="7/12/1980" Face="Sally.png"/> </src:EmployeeButton.Content> </src:EmployeeButton> </StackPanel> </Window>



The Content property of EmployeeButton appears as the property element EmployeeButton.Content. The Content is set to an Employee object with three properties assigned. Because EmployeeButton includes a non-null ContentTemplate property, that template is used to display the Employee object. In reality, a program of this sort would probably access some database to obtain the employee information, so the following ContentTemplateDemo.cs code-behind file demonstrates the code that might be involved by adding another EmployeeButton to the StackPanel. The file also handles Click events from the button.

ContentTemplateDemo.cs

[View full width]

//---------------------------------------------------- // ContentTemplateDemo.cs (c) 2006 by Charles Petzold //---------------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.ContentTemplateDemo { public partial class ContentTemplateDemo : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new ContentTemplateDemo()); } public ContentTemplateDemo() { InitializeComponent(); // Add another EmployeeButton just to demonstrate the code. EmployeeButton btn = new EmployeeButton(); btn.Content = new Employee("Jim", "Jim .png", new DateTime(1975, 6, 15), false); stack.Children.Add(btn); } // Click event handler for button. void EmployeeButtonOnClick(object sender, RoutedEventArgs args) { Button btn = args.Source as Button; Employee emp = btn.Content as Employee; MessageBox.Show(emp.Name + " button clicked!", Title); } } }



With any more than a few employees, you'll probably also want to put that StackPanel in the XAML file inside a ScrollViewer. In the next chapter, I'll show you some other techniques for accessing data and displaying it in controls.

Back in the EmployeeButton.xaml file, I set the DataType property of DataTemplate to the Employee class. It may have seemed as if this was necessary to match up the various properties of the bindings with the correct class, but that's not the case. You can actually remove the DataType attribute from the DataTemplate element and the program still runs fine.

Certainly the DataType attribute provides some helpful information for people who must examine the XAML file. But it actually works in reverse from what you might expect. It's actually used for locating a resource that matches the data type of a Content property.

The following stand-alone XAML file defines four DataTemplate objects as resources. Each of the templates has a different DataType (Int32, Double, String, and DateTime) and each of the templates indicates that particular data type when displaying data, so it's easy to determine which template is being used.

AutoTemplateSelection.xaml

[View full width]

<!-- === ===================================================== AutoTemplateSelection.xaml (c) 2006 by Charles Petzold ============================================= =========== --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml" xmlns:s="clr-namespace :System;assembly=mscorlib"> <Page.Resources> <DataTemplate DataType="{x:Type s:Int32}"> <StackPanel Orientation="Horizontal"> <TextBlock Text="Integer: " /> <TextBlock Text="{Binding}" /> </StackPanel> </DataTemplate> <DataTemplate DataType="{x:Type s:Double}"> <StackPanel Orientation="Horizontal"> <TextBlock Text="Double: " /> <TextBlock Text="{Binding}" /> </StackPanel> </DataTemplate> <DataTemplate DataType="{x:Type s:String}"> <StackPanel Orientation="Horizontal"> <TextBlock Text="String: " /> <TextBlock Text="{Binding}" /> </StackPanel> </DataTemplate> <DataTemplate DataType="{x:Type s:DateTime}"> <StackPanel Orientation="Horizontal"> <TextBlock Text="DateTime: " /> <TextBlock Text="{Binding Path=Month}" /> <TextBlock Text="/" /> <TextBlock Text="{Binding Path=Day }" /> <TextBlock Text="/" /> <TextBlock Text="{Binding Path=Year}" /> </StackPanel> </DataTemplate> <Style TargetType="{x:Type Button}"> <Setter Property="HorizontalAlignment" Value="Center" /> <Setter Property="Margin" Value="12" /> <Setter Property="FontSize" Value="12pt" /> <Setter Property="Padding" Value="10" /> </Style> </Page.Resources> <StackPanel> <Button> <s:Int32> 27 </s:Int32> </Button> <Button> <s:Double>27.543</s:Double> </Button> <Button> 27.543 </Button> <Button> <x:Static Member="s:DateTime.Now" /> </Button> </StackPanel> </Page>



Toward the bottom of the file, four Button objects have four different types of Content. These are successfully matched up with the four DataTemplate objects stored as resources without any direct connections between the buttons and the resources. A ContentControl object automatically searches resources for a DataTemplate matching the type of its content, just as the Button itself searches for a Style that has a TargetType of Button defined.

That's one way to have different types of presentation of different types of content. A more flexible approach is provided by the ContentTemplateSelector property defined by ContentControl. This property is of type DataTemplateSelector, a class that contains a virtual method named SelectTemplate. You derive a class from DataTemplateSelector and override SelectTemplate. The first argument is the object being displayed by the ContentControl. The job of this method is to return an object of type DataTemplate for displaying the content.

The SelectDataTemplate project contains the following class that derives from DataTemplateSelector and overrides SelectTemplate.

EmployeeTemplateSelector.cs

[View full width]

//--------- ------------------------------------------------ // EmployeeTemplateSelector.cs (c) 2006 by Charles Petzold //------------------------------------------------ --------- using Petzold.ContentTemplateDemo; using System; using System.Windows; using System.Windows.Controls; namespace Petzold.SelectDataTemplate { public class EmployeeTemplateSelector : DataTemplateSelector { public override DataTemplate SelectTemplate(object item, DependencyObject container) { Employee emp = item as Employee; FrameworkElement el = container as FrameworkElement; return (DataTemplate) el.FindResource( emp.LeftHanded ? "templateLeft" : "templateRight"); } } }



In the context of this project, the first argument to SelectTemplate is an object of type Employee, and the second argument is an object of type ContentPresenter. The method uses FindResource to choose between templates with the keys of "templateLeft" and "templateRight" based in the LeftHanded property of Employee.

The following XAML file has a Resources section that associates the EmployeeTemplateSelector class with a key of "selectTemplate". The two templates are next, with keys of "templateRight" and "templateLeft". The templates are the same except that the Image is on the right side for right-handed employees and on the left side for left-handed employees.

SelectDataTemplate.xaml

[View full width]

<!-- === ================================================== SelectDataTemplate.xaml (c) 2006 by Charles Petzold ========================================== =========== --> <Window xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" xmlns:emp="clr-namespace:Petzold .ContentTemplateDemo" xmlns:src="/books/4/266/1/html/2/clr-namespace:Petzold .SelectDataTemplate" x:0" width="14" height="9" align="left" src="/books/4/266/1/html/2/images/ccc.gif" />.SelectDataTemplate" Title="Select DataTemplate"> <Window.Resources> <src:EmployeeTemplateSelector x :Key="selectTemplate" /> <DataTemplate x:Key="templateRight"> <DockPanel> <Image DockPanel.Dock="Right" Stretch="None" Source="{Binding Path=Face}" /> <UniformGrid Rows="2" VerticalAlignment="Center" Margin="12"> <TextBlock FontSize="16pt" TextAlignment="Center" Text="{Binding Path=Name}"/> <StackPanel Orientation="Horizontal" TextBlock .FontSize="12pt"> <TextBlock Text="{Binding Path=BirthDate.Month}" /> <TextBlock Text="/" /> <TextBlock Text="{Binding Path=BirthDate.Day}" /> <TextBlock Text="/" /> <TextBlock Text="{Binding Path=BirthDate.Year}" /> </StackPanel> </UniformGrid> </DockPanel> </DataTemplate> <DataTemplate x:Key="templateLeft"> <DockPanel> <Image DockPanel.Dock="Left" Stretch="None" Source="{Binding Path=Face}" /> <UniformGrid Rows="2" VerticalAlignment="Center" Margin="12"> <TextBlock FontSize="16pt" TextAlignment="Center" Text="{Binding Path=Name}"/> <StackPanel Orientation="Horizontal" TextBlock .FontSize="12pt"> <TextBlock Text="{Binding Path=BirthDate.Month}" /> <TextBlock Text="/" /> <TextBlock Text="{Binding Path=BirthDate.Day}" /> <TextBlock Text="/" /> <TextBlock Text="{Binding Path=BirthDate.Year}" /> </StackPanel> </UniformGrid> </DockPanel> </DataTemplate> <Style TargetType="{x:Type Button}"> <Setter Property="HorizontalAlignment" Value="Center" /> <Setter Property="VerticalAlignment" Value="Center" /> <Setter Property="Margin" Value="12" /> </Style> </Window.Resources> <StackPanel> <Button ContentTemplateSelector="{StaticResource selectTemplate}"> <Button.Content> <emp:Employee Name="Betty" BirthDate="8/31/1970" Face="Betty.png" LeftHanded="False"/> </Button.Content> </Button> <Button ContentTemplateSelector="{StaticResource selectTemplate}"> <Button.Content> <emp:Employee Name="Edgar" BirthDate="2/2/1965" Face="Edgar.png" LeftHanded="True"/> </Button.Content> </Button> <Button ContentTemplateSelector="{StaticResource selectTemplate}"> <Button.Content> <emp:Employee Name="Sally" BirthDate="7/12/1980" Face="Sally.png" LeftHanded="True"/> </Button.Content> </Button> <Button ContentTemplateSelector="{StaticResource selectTemplate}"> <Button.Content> <emp:Employee Name="Jim" BirthDate="6/15/1975" Face="Jim.png" LeftHanded="False"/> </Button.Content> </Button> </StackPanel> </Window>



The remainder of the XAML file defines the layout of the window. The four buttons require only that the ContentTemplateSelector be set to the "selectTemplate" resource (the EmployeeTemplateSelector object), which then indicates which template to use for displaying the content. The following file rounds out the project.

SelectDataTemplate.cs

//--------------------------------------------------- // SelectDataTemplate.cs (c) 2006 by Charles Petzold //--------------------------------------------------- using System; using System.Windows; namespace Petzold.SelectDataTemplate {     public partial class SelectDataTemplate : Window     {         [STAThread]         public static void Main()         {             Application app = new Application();             app.Run(new SelectDataTemplate());         }         public SelectDataTemplate()         {             InitializeComponent();         }     } } 



Although the SelectDataTemplate project shows the most versatile approach for selecting templates used to display content, using the ContentTemplateSelector property isn't actually required for this particular job. The next project, called TriggerDataTemplate, shows an alternative approach. As with the previous project, the C# part of the Window class is fairly trivial.

TriggerDataTemplate.cs

//---------------------------------------------------- // TriggerDataTemplate.cs (c) 2006 by Charles Petzold //---------------------------------------------------- using System; using System.Windows; namespace Petzold.TriggerDataTemplate {     public partial class TriggerDataTemplate : Window     {         [STAThread]         public static void Main()         {             Application app = new Application();             app.Run(new TriggerDataTemplate());         }         public TriggerDataTemplate()         {             InitializeComponent();         }     } } 



The following XAML file defines just one DataTemplate, which is given a key name of "template". The Image element is given a name of img and has its DockPanel.Dock property set to Left. This is the template for left-handed employees. The DataTemplate also includes a Triggers section with a Binding that references the LeftHanded property of the Employee object. When that value is false (indicating a right-handed employee), the Setter sets the DockPanel.Dock property of the Image object to a value of Right.

TriggerDataTemplate.xaml

[View full width]

<!-- === =================================================== TriggerDataTemplate.xaml (c) 2006 by Charles Petzold ============================================= ========= --> <Window xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" xmlns:emp="clr-namespace:Petzold .ContentTemplateDemo" x:0" width="14" height="9" align="left" src="/books/4/266/1/html/2/images/ccc.gif" />.TriggerDataTemplate" Title="Trigger DataTemplate"> <Window.Resources> <DataTemplate x:Key="template"> <DockPanel> <Image Name="img" DockPanel .Dock="Left" Stretch="None" Source="{Binding Path=Face}" /> <UniformGrid Rows="2" VerticalAlignment="Center" Margin="12"> <TextBlock FontSize="16pt" TextAlignment="Center" Text="{Binding Path=Name}"/> <StackPanel Orientation="Horizontal" TextBlock .FontSize="12pt"> <TextBlock Text="{Binding Path=BirthDate.Month}" /> <TextBlock Text="/" /> <TextBlock Text="{Binding Path=BirthDate.Day}" /> <TextBlock Text="/" /> <TextBlock Text="{Binding Path=BirthDate.Year}" /> </StackPanel> </UniformGrid> </DockPanel> <DataTemplate.Triggers> <DataTrigger Binding="{Binding Path=LeftHanded}" Value="False"> <Setter TargetName="img" Property="DockPanel .Dock" Value="Right" /> </DataTrigger> </DataTemplate.Triggers> </DataTemplate> <Style TargetType="{x:Type Button}"> <Setter Property="HorizontalAlignment" Value="Center" /> <Setter Property="VerticalAlignment" Value="Center" /> <Setter Property="Margin" Value="12" /> </Style> </Window.Resources> <StackPanel> <Button ContentTemplate="{StaticResource template}"> <Button.Content> <emp:Employee Name="Betty" BirthDate="8/31/1970" Face="Betty.png" LeftHanded="False"/> </Button.Content> </Button> <Button ContentTemplate="{StaticResource template}"> <Button.Content> <emp:Employee Name="Edgar" BirthDate="2/2/1965" Face="Edgar.png" LeftHanded="True"/> </Button.Content> </Button> <Button ContentTemplate="{StaticResource template}"> <Button.Content> <emp:Employee Name="Sally" BirthDate="7/12/1980" Face="Sally.png" LeftHanded="True"/> </Button.Content> </Button> <Button ContentTemplate="{StaticResource template}"> <Button.Content> <emp:Employee Name="Jim" BirthDate="6/15/1975" Face="Jim.png" LeftHanded="False"/> </Button.Content> </Button> </StackPanel> </Window>



Keep in mind that you can use the ContentTemplate property to define a custom layout for any control that derives from ContentControl, which is certainly a significant proportion of WPF controls. After ContentControl, the ItemsControl class is the basis of the next most significant subset of controls. ItemsControl is the root of all classes that display multiple objects, including MenuItem, TreeViewItem, ListBox, ComboBox, and ListView.

The ItemsControl class inherits the normal Template property, of course. For ListBox, the Template property is fairly simple: a Border enclosing a ScrollViewer enclosing an ItemsPresenter that is responsible for displaying the items.

ItemsControl defines another template-related property, the ItemsPanel property, which is by far the simplest type of template. The property is of type ItemsPanelTemplate, and the entire visual tree must consist of only a single element that derives from Panel. You can use the DumpControlTemplate program described earlier in this chapter for examining the ItemsPanel property of ItemsControl and the classes that derive from ItemsControl. For most of these classes, the template is a StackPanel. For Menu, it's a WrapPanel. (That allows the top-level menu items to wrap from one line to the next.) For StatusBar, it's a DockPanel. For ListBox and ListView, it's a VirtualizingStackPanel, which is similar to the normal StackPanel but is optimized for large numbers of items.

It's fairly easy to replace the panel in a ListBox (or other class that derives from ItemsControl) with something else. Here's a simple example.

ListBoxWithItemsPanel.xaml

[View full width]

<!-- === ===================================================== ListBoxWithItemsPanel.xaml (c) 2006 by Charles Petzold ============================================= =========== --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml"> <ListBox HorizontalAlignment="Center" VerticalAlignment="Center"> <ListBox.ItemsPanel> <ItemsPanelTemplate> <UniformGrid /> </ItemsPanelTemplate> </ListBox.ItemsPanel> <ListBoxItem>Whatever Item 1</ListBoxItem> <ListBoxItem>Whatever Item 2</ListBoxItem> <ListBoxItem>Whatever Item 3</ListBoxItem> <ListBoxItem>Whatever Item 4</ListBoxItem> <ListBoxItem>Whatever Item 5</ListBoxItem> <ListBoxItem>Whatever Item 6</ListBoxItem> <ListBoxItem>Whatever Item 7</ListBoxItem> <ListBoxItem>Whatever Item 8</ListBoxItem> <ListBoxItem>Whatever Item 9</ListBoxItem> </ListBox> </Page>



All that's being done here is replacing the VirtualizingStackPanel normally used to display ListBox items with a UniformGrid. The ListBox still has a border, and if you make the page small enough, you'll see that the panel is enclosed in a ScrollViewer. All the selection logic is still intact.

Because the ListBox has nine items, the UniformGrid automatically determines that the number of rows and columns should both be three. You can override that in the UniformGrid element:

<UniformGrid Columns="2" /> 


A little problem with this ListBox is that all the items are smashed up against each other. Perhaps the best way to avoid that problem is to simply add a little Style to the ListBoxItem elements. You can include the following block of XAML within the ListBox element:

<ListBox.Resources>     <Style TargetType="{x:Type ListBoxItem}">         <Setter Property="Padding" Value="3" />     </Style> </ListBox.Resources> 


You can use Margin here rather than Padding, but the effect is not quite as good. When an item is selected, the Padding is included in the selection but the Margin is not.

For displaying the individual items, ItemsControl defines a property named ItemTemplate of type DataTemplate. You use this property in a similar way that you use the ContentTemplate property defined by ContentControl, except that the ItemTemplate property governs the appearance of all items in the ListBox, and it references properties of the data stored in the ListBox.

You might recall the ListColorsEvenElegantlier project from Chapter 13 that defined a DataTemplate in code for displaying colors and their names in a ListBox. The ColorListBoxTemplate project coming up next is functionally equivalent to that program but does it all in XAML (except for the link to the NamedBrush class from the ListNamedBrushes project in Chapter 13). The project begins with the following application definition file.

ColorListBoxTemplateApp.xaml

[View full width]

<!-- === ======================================================= ColorListBoxTemplateApp.xaml (c) 2006 by Charles Petzold ============================================= ============= --> <Application xmlns="http://schemas.microsoft.com/ winfx/2006/xaml/presentation" StartupUri="ColorListBoxTemplateWindow.xaml" />



The XAML file for the program's window defines a DataTemplate in its Resources section. The DataType is the NamedBrush class. The visual tree is a horizontal StackPanel containing a Rectangle and a TextBlock. The Fill property of the Rectangle is bound to the Brush property of NamedBrush, and the Text property of the TextBlock is bound to the Text property of NamedBrush.

ColorListBoxTemplateWindow.xaml

[View full width]

<!-- === =========== =============================================== ColorListBoxTemplateWindow.xaml (c) 2006 by Charles Petzold ============================================= ================ --> <Window xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" xmlns:nb="clr-namespace:Petzold .ListNamedBrushes" Title="Color ListBox Template" Background="{Binding ElementName=lstbox, Path=SelectedValue}"> <Window.Resources> <DataTemplate x:Key="clrlstbox" DataType="NamedBrush"> <StackPanel Orientation="Horizontal"> <Rectangle Width="16" Height="16" Margin="2" Stroke="{DynamicResource {x:Static SystemColors.WindowTextBrushKey}}" Fill="{Binding Path=Brush}" /> <TextBlock VerticalAlignment="Center" Text="{Binding Path=Name}" /> </StackPanel> </DataTemplate> </Window.Resources> <ListBox Name="lstbox" ItemTemplate="{StaticResource clrlstbox}" ItemsSource="{x:Static nb:NamedBrush .All}" Width="150" Height="150" HorizontalAlignment="Center" VerticalAlignment="Center" SelectedValuePath="Brush" /> </Window>



The window contains a single ListBox with its ItemTemplate set to the DataTemplate defined as a resource. Each item in the ListBox will therefore be displayed as a horizontal StackPanel with a Rectangle and TextBlock. The ItemsSource property of ListBox is set to the static All property of NamedBrush, which is an array of all the NamedBrush objects generated from the Brushes class in System.Windows.Media.

Also notice that the SelectedValuePath property of ListBox is assigned to the Brush property of NamedBrush. This means that the SelectedValue property of ListBox will be the Brush property of the selected NamedBrush. The start tag of the Window element defines a Binding between that SelectedValue property and its own Background property.

While I was planning Chapter 26, I knew I wanted a DatePicker control, which the first release of the WPF doesn't have. It suddenly occurred to me that a calendar month could be displayed using a ListBox with a seven-column UniformGrid as its ItemsPanelTemplate. The ListBox would always contain 28, 29, 30, or 31 items, depending on the month, and the items themselves would just be numbers starting at 1. The FirstColumn property of UniformGrid is ideal for beginning the month on a day of the week other than Sunday, and actually seems designed for that very purpose.

The resultant DatePicker control displays a whole month and lets you click on one day of the month. The control also has repeat buttons for scrolling forward or back by month. But otherwise the control is fairly rudimentary. It isn't retractable into a single line, and it doesn't have much of a keyboard interface. You can't type numbers for the day, month, or year, for example. To compensate, I decided that the PageUp and PageDown keys would duplicate the repeat buttons, and that both the keys and the buttons would be affected by the Shift and Ctrl keys. When neither the Shift nor Ctrl key is pressed, the buttons and the PageUp and PageDown keys cause the calendar to go forward or backward by a month. With the Shift key down, the calendar goes forward or backward by a year. With the Ctrl key, it's by a decade, and with both Shift and Ctrl, it's by a century.

Here's the XAML portion of the DatePicker control. You'll notice the x:Class attribute in the root element that indicates a definition of a new class based on UserControl, which is essentially the same as ControlControl. The file begins with a Resources section containing a few Style definitions. The two RepeatButton controls get a style that makes the buttons square. The other two Style definitions are for StatusBarItem and ListBoxItem, neither of which explicitly appears in the XAML file. However, there is a StatusBar and there is a ListBox, so these styles apply to the items displayed by these controls.

Following the Resources section, the definition of the layout of the control begins. Overall, it's a four-row Grid. The first row displays the month and year with RepeatButton controls on each side. The second row shows the days of the week. The third row is the ListBox with the days of the month, and the fourth row is a CheckBox labeled "Not applicable." (More on this later.)

DatePicker.xaml

[View full width]

<!-- ============================================= DatePicker.xaml (c) 2006 by Charles Petzold ============================================= --> <UserControl xmlns="http://schemas.microsoft.com/ winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" xmlns:g="clr-namespace:System .Globalization;assembly=mscorlib" x:0" width="14" height="9" align="left" src="/books/4/266/1/html/2/images/ccc.gif" />.DatePicker"> <UserControl.Resources> <!-- Make the RepeatButton square. --> <Style TargetType="{x:Type RepeatButton}"> <Setter Property="Width" Value="{Binding RelativeSource={RelativeSource self}, Path=ActualHeight}" /> <Setter Property="Focusable" Value="False" /> <Style.Triggers> <DataTrigger Binding="{Binding ElementName=chkboxNull, Path=IsChecked}" Value="True"> <Setter Property="IsEnabled" Value="False" /> </DataTrigger> </Style.Triggers> </Style> <!-- Styles for StatusBarItem (days of the week). --> <Style TargetType="{x:Type StatusBarItem}"> <Setter Property="Margin" Value="1" /> <Setter Property="HorizontalAlignment" Value="Center" /> <Setter Property="VerticalAlignment" Value="Center" /> </Style> <!-- Style for ListBoxItem (days of the month). --> <Style TargetType="{x:Type ListBoxItem}"> <Setter Property="BorderThickness" Value="1" /> <Setter Property="BorderBrush" Value="Transparent" /> <Setter Property="Margin" Value="1" /> <Setter Property="HorizontalContentAlignment" Value="Center" /> <Style.Triggers> <MultiTrigger> <MultiTrigger.Conditions> <Condition Property="IsSelected" Value="True" /> <Condition Property="Selector.IsSelectionActive" Value="False" /> </MultiTrigger.Conditions> <Setter Property="BorderBrush" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" /> </MultiTrigger> <DataTrigger Binding="{Binding ElementName=chkboxNull, Path=IsChecked}" Value="True"> <Setter Property="IsEnabled" Value="False" /> </DataTrigger> </Style.Triggers> </Style> </UserControl.Resources> <!-- Border contains main four-row Grid. --> <Border BorderThickness="1" BorderBrush="{DynamicResource {x:Static SystemColors .WindowTextBrushKey}}"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <!-- Grid for Label and Buttons. --> <Grid Background="{DynamicResource {x:Static SystemColors .ControlDarkDarkBrushKey}}" TextBlock .Foreground="{DynamicResource {x:Static SystemColors .ControlLightLightBrushKey}}"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="*" /> <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> <RepeatButton Grid.Column="0" Content="&lt;" FontWeight="Bold" Click="ButtonBackOnClick" /> <TextBlock Name="txtblkMonthYear" Grid.Column="1" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="3" /> <RepeatButton Grid.Column="2" Content="&gt;" FontWeight="Bold" Click="ButtonForwardOnClick" /> </Grid> <!-- StatusBar with UniformGrid for days of the week. --> <StatusBar Grid.Row="1" ItemsSource="{Binding Source={x:Static g :DateTimeFormatInfo.CurrentInfo}, Path=AbbreviatedDayNames}"> <StatusBar.ItemsPanel> <ItemsPanelTemplate> <UniformGrid Rows="1" /> </ItemsPanelTemplate> </StatusBar.ItemsPanel> </StatusBar> <!-- ListBox with UniformGrid for days of the month. --> <Border Grid.Row="2" BorderThickness="0 1 0 1" BorderBrush="{DynamicResource {x:Static SystemColors .WindowTextBrushKey}}"> <ListBox Name="lstboxMonth" SelectionChanged="ListBoxOnSelectionChanged"> <ListBox.ItemsPanel> <ItemsPanelTemplate> <UniformGrid Name="unigridMonth" Columns="7" Rows="6" IsItemsHost="True" Background="{DynamicResource {x:Static SystemColors.ControlLightBrushKey}}" /> </ItemsPanelTemplate> </ListBox.ItemsPanel> <ListBoxItem>dummy item</ ListBoxItem> </ListBox> </Border> <!-- CheckBox for making Null dates. --> <CheckBox Name="chkboxNull" Grid .Row="3" Margin="3" HorizontalAlignment="Center" VerticalAlignment="Center" Checked="CheckBoxNullOnChecked" Unchecked="CheckBoxNullOnUnchecked"> Not applicable </CheckBox> </Grid> </Border> </UserControl>



The first row of the main Grid is another Grid with three columns containing the two RepeatButton controls and a TextBlock for the month and year.

The second row of the main Grid displays the days of the week. I decided to use a StatusBar for this part of the calendar, mainly because a StatusBar displays multiple items but doesn't allow the items to be selected. Normally, the StatusBar uses a DockPanel for displaying its items, but I redefined the ItemsPanel of the StatusBar to be a seven-column UniformGrid. The ItemsSource is set to a Binding that references the AbbreviatedDaysNames property of the DateTimeFormatInfo class. This allows the days of the week to appear in the user's own language, but unfortunately, it doesn't work right for countries whose weeks don't begin with Sunday (like France). Accounting for those anomalies would require accessing the FirstDayOfWeek property of DateTimeFormatInfo, and making some manual adjustments (probably in C# code).

The third row of the main Grid is a ListBox with its ItemsPanel template set to another seven-column UniformGrid. This is for the days of the month, and is filled by C# code. For that reason, the ListBox has a Name attribute of lstboxMonth. As I mentioned earlier, C# code must also set the FirstColumn property of the UniformGrid, so the UniformGrid is given a Name attribute of unigridMonth.

The fourth and final row is a CheckBox control labeled "Not applicable." I wanted a way to have a null date, and for this particular DatePicker control, having such a CheckBox seemed to me the most simple and direct approach.

As you know, when you use XAML to define elements and controls normally in a window or page or on a panel, and you assign a string to the Name attribute of a particular element, that string becomes a variable name that you can use in the code portion of the class. However, Name attributes in templates don't work that way. These names do not automatically become variable names, mainly because the same template can be used in multiple controls. Instead, in the C# portion of the class, you must use the FindName method defined by FrameworkTemplate to find a particular named element within a template. FindName will return null if the element within the template has not yet been turned into an actual object. The process of creating objects from templates occurs when the control is being loaded and rendered. To ensure that FindName returns an actual value, you can preface calls to FindName with ApplyTemplate, a method defined by FrameworkElement that builds the visual tree.

At least that's the way ...it's supposed... to work. But the most problematic aspect of the DatePicker turned out to be the UniformGrid that displays the days of the month. This UniformGrid has a Name attribute (unigridMonth) but it's buried inside an ItemsPanel template in a ListBox. After trying a number of different approaches, I finally resorted to a recursive method that used VisualTreeHelper to find the UniformGrid. This code had to be delayed until the window hosting the control had been loaded.

DatePicker.cs is the code-behind file for DatePicker. The class defines a property named Date backed by the dependency property DateProperty. This property is a nullable DateTime object, where a null value indicates that the DateTime is inapplicable in a particular context. In the next chapter, I use the DatePicker on data-entry panels for people's birth dates and death dates. If the person is still alive, the death date is a null value.

Using a type of nullable DateTime rather than a simple DateTime presents some of its own difficulties. You can't simply access the properties of the nullable DateTime object. You must cast it to a regular DateTime first, and the cast will fail if the value is null.

DatePicker defines an event named DateChanged that is backed by the routed event DateChangedEvent. As the name suggests, this event is triggered when the Date property changes.

DatePicker.cs

[View full width]

//------------------------------------------- // DatePicker.cs (c) 2006 by Charles Petzold //------------------------------------------- using System; using System.Globalization; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Media; namespace Petzold.CreateDatePicker { public partial class DatePicker { UniformGrid unigridMonth; DateTime datetimeSaved = DateTime.Now.Date; // Define DateProperty dependency property. public static readonly DependencyProperty DateProperty = DependencyProperty.Register("Date", typeof (DateTime?), typeof(DatePicker), new PropertyMetadata(new DateTime(), DateChangedCallback)); // Define DateChangedEvent routed event. public static readonly RoutedEvent DateChangedEvent = EventManager.RegisterRoutedEvent("DateChanged", RoutingStrategy.Bubble, typeof(RoutedPropertyChangedEventHandler<Date Time?>), typeof(DatePicker)); // Constructor. public DatePicker() { InitializeComponent(); // Initialize Date property. Date = datetimeSaved; // Attach handler for Loaded event. Loaded += DatePickerOnLoaded; } // Handler for window Loaded event. void DatePickerOnLoaded(object sender, RoutedEventArgs args) { unigridMonth = FindUniGrid(lstboxMonth); if (Date != null) { DateTime dt = (DateTime)Date; unigridMonth.FirstColumn = (int)(new DateTime(dt.Year, dt.Month, 1) .DayOfWeek); } } // Recursive method to find UniformGrid. UniformGrid FindUniGrid(DependencyObject vis) { if (vis is UniformGrid) return vis as UniformGrid; for (int i = 0; i < VisualTreeHelper. GetChildrenCount(vis); i++) { Visual visReturn = FindUniGrid(VisualTreeHelper .GetChild(vis, i)); if (visReturn != null) return visReturn as UniformGrid; } return null; } // Date property backed by DateProperty dependency property. public DateTime? Date { set { SetValue(DateProperty, value); } get { return (DateTime?)GetValue(DateProperty); } } // DateChanged event backed by DateChangedEvent routed event. public event RoutedPropertyChangedEventHandler<DateTime?> DateChanged { add { AddHandler(DateChangedEvent, value); } remove { RemoveHandler(DateChangedEvent, value); } } // Back and Forward repeat buttons. void ButtonBackOnClick(object sender, RoutedEventArgs args) { FlipPage(true); } void ButtonForwardOnClick(object sender, RoutedEventArgs args) { FlipPage(false); } // Buttons are duplicated by PageDown and PageUp keys. protected override void OnPreviewKeyDown (KeyEventArgs args) { base.OnKeyDown(args); if (args.Key == Key.PageDown) { FlipPage(true); args.Handled = true; } else if (args.Key == Key.PageUp) { FlipPage(false); args.Handled = false; } } // Flip the page to the next month, year, decade, etc. void FlipPage(bool isBack) { if (Date == null) return; DateTime dt = (DateTime)Date; int numPages = isBack ? -1 : 1; // If Shift down, go by years. if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)) numPages *= 12; // If Ctrl down, go by decades. if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) numPages = Math.Max(-1200, Math.Min (1200, 120 * numPages)); // Calculate new DateTime. int year = dt.Year + numPages / 12; int month = dt.Month + numPages % 12; while (month < 1) { month += 12; year -= 1; } while (month > 12) { month -= 12; year += 1; } // Set Date property (generates DateChangedCallback). if (year < DateTime.MinValue.Year) Date = DateTime.MinValue.Date; else if (year > DateTime.MaxValue.Year) Date = DateTime.MaxValue.Date; else Date = new DateTime(year, month, Math.Min(dt.Day, DateTime .DaysInMonth(year, month))); } // If CheckBox being checked, save the Date property and set to null. void CheckBoxNullOnChecked(object sender, RoutedEventArgs args) { if (Date != null) { datetimeSaved = (DateTime)Date; Date = null; } } // If CheckBox being unchecked, restore the Date property. void CheckBoxNullOnUnchecked(object sender , RoutedEventArgs args) { Date = datetimeSaved; } // For ListBox selection change, change the day of the month. void ListBoxOnSelectionChanged(object sender, SelectionChangedEventArgs args) { if (Date == null) return; DateTime dt = (DateTime)Date; // Set new Date property (generates DateChangedCallback). if (lstboxMonth.SelectedIndex != -1) Date = new DateTime(dt.Year, dt.Month, Int32.Parse (lstboxMonth.SelectedItem as string)); } // This method is triggered by the DateProperty. static void DateChangedCallback (DependencyObject obj, DependencyPropertyChangedEventArgs args) { // Call OnDateChange for the object being changed. (obj as DatePicker).OnDateChanged( (DateTime?)args.OldValue, (DateTime?)args.NewValue); } // OnDateChanged changes the visuals to match the new value of Date. protected virtual void OnDateChanged (DateTime? dtOldValue, DateTime? dtNewValue) { chkboxNull.IsChecked = dtNewValue == null; if (dtNewValue != null) { DateTime dtNew = (DateTime)dtNewValue; // Set the month and year text. txtblkMonthYear.Text = dtNew.ToString( DateTimeFormatInfo .CurrentInfo.YearMonthPattern); // Set the first day of the month. if (unigridMonth != null) unigridMonth.FirstColumn = (int)(new DateTime(dtNew.Year, dtNew .Month, 1).DayOfWeek); int iDaysInMonth = DateTime .DaysInMonth(dtNew.Year, dtNew.Month); // Fill up the ListBox if the number of days isn't right. if (iDaysInMonth != lstboxMonth .Items.Count) { lstboxMonth.BeginInit(); lstboxMonth.Items.Clear(); for (int i = 0; i < iDaysInMonth; i++) lstboxMonth.Items.Add((i + 1).ToString()); lstboxMonth.EndInit(); } lstboxMonth.SelectedIndex = dtNew .Day - 1; } // Now trigger the DateChangedEvent. RoutedPropertyChangedEventArgs<DateTime?> args = new RoutedPropertyChangedEventArgs<DateTime?>(dtOldValue, dtNewValue, DatePicker.DateChangedEvent); args.Source = this; RaiseEvent(args); } } }



Following the constructor and the definitions of the Date property and DateChanged event, the DatePicker.cs file contains several methods for handling user input. Clicks on the RepeatButton controls and keyboard presses of the PageUp and PageDown keys all serve to move the calendar to a new month. The main logic occurs in the FlipPage method and concludes with setting a new Date property. The handler for the SelectionChanged event of the ListBox also changes the Date property.

The definition of the DateProperty dependency property at the beginning of the file indicates that the static DateChangedCallback method is called whenever the Date property changes. This method calls OnDateChanged, which is responsible for updating the visual display with the new date and firing the DateChanged routed event.

For testing the code, I constructed this small window. One TextBlock has a binding with the Date property of the DatePicker control. Another gets the value from a handler installed for the DateChanged event.

CreateDatePickerWindow.xaml

[View full width]

<!-- === ====================================================== CreateDatePickerWindow.xaml (c) 2006 by Charles Petzold ============================================= ============ --> <Window xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" xmlns:src="/books/4/266/1/html/2/clr-namespace:Petzold .CreateDatePicker" x:0" width="14" height="9" align="left" src="/books/4/266/1/html/2/images/ccc.gif" />.CreateDatePickerWindow" Title="Create DatePicker" SizeToContent="WidthAndHeight" ResizeMode="CanMinimize"> <StackPanel> <src:DatePicker x:Name="datepick" HorizontalAlignment="Center" Margin="12" DateChanged="DatePickerOnDateChanged" /> <StackPanel Orientation="Horizontal" Margin="12"> <TextBlock Text="Bound value: " /> <TextBlock Text="{Binding ElementName=datepick, Path=Date}" /> </StackPanel> <StackPanel Orientation="Horizontal" Margin="12"> <TextBlock Text="Event handler value: " /> <TextBlock Name="txtblkDate" /> </StackPanel> </StackPanel> </Window>



The event handler is located in the code-behind file and updates the TextBlock with either a date or a blank indicating a null value.

CreateDatePickerWindow.cs

[View full width]

//--------- ---------------------------------------------- // CreateDatePickerWindow.cs (c) 2006 by Charles Petzold //------------------------------------------------ ------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.CreateDatePicker { public partial class CreateDatePickerWindow : Window { public CreateDatePickerWindow() { InitializeComponent(); } // Handler for DatePicker DateChanged event. void DatePickerOnDateChanged(object sender, RoutedPropertyChangedEventArgs<DateTime?> args) { if (args.NewValue != null) { DateTime dt = (DateTime)args.NewValue; txtblkDate.Text = dt.ToString("d"); } else txtblkDate.Text = ""; } } }



The application definition file completes the project.

CreateDatePickerApp.xaml

[View full width]

<!-- === =================================================== CreateDatePickerApp.xaml (c) 2006 by Charles Petzold ============================================= ========= --> <Application xmlns="http://schemas.microsoft.com/ winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" x:0" width="14" height="9" align="left" src="/books/4/266/1/html/2/images/ccc.gif" />.CreateDatePickerApp" StartupUri="CreateDatePickerWindow .xaml" />



Although the DatePicker control demonstrates that the ListBox can be put to unusual purposes, most often the ListBox displays data, and these days data frequently comes in the form of XML files. In the next chapter I'll show you how to create and flexibly view such files. For a little preview, let's assume that your six employees are stored in the following XML file. The goal of this exercise is to create a ListBox that displays these six employees.

Employees.xml

<!-- ===========================================       Employees.xml (c) 2006 by Charles Petzold      =========================================== --> <Employees xmlns="">     <Employee Name="Betty">         <BirthDate>8/31/1970</BirthDate>         <Face>Betty.png</Face>         <LeftHanded>False</LeftHanded>     </Employee>     <Employee Name="Edgar">         <BirthDate>2/2/1965</BirthDate>         <Face>Edgar.png</Face>         <LeftHanded>True</LeftHanded>     </Employee>     <Employee Name="Sally">         <BirthDate>7/12/1980</BirthDate>         <Face>Sally.png</Face>         <LeftHanded>True</LeftHanded>     </Employee>     <Employee Name="Jim">         <BirthDate>6/15/1975</BirthDate>         <Face>Jim.png</Face>         <LeftHanded>False</LeftHanded>     </Employee>     <Employee Name="Anne">         <BirthDate>4/7/1975</BirthDate>         <Face>Anne.png</Face>         <LeftHanded>True</LeftHanded>     </Employee>     <Employee Name="John">         <BirthDate>12/2/1955</BirthDate>         <Face>John.png</Face>         <LeftHanded>False</LeftHanded>     </Employee> </Employees> 



This file is part of a project named EmployeeWheel and has a Build Action of Resource. As you'll note, each Employee tag in this XML file contains the same information as the Employee class shown earlier in this chapter. However, that Employee class is not required in the EmployeeWheel project. The ListBox and its templates will get everything they need from the Employees.xml file.

The ListBox in the EmployeeWheel project will display these six employees in a circle using the RadialPanel class created in Chapter 12. The EmployeeWheel project requires the RadialButton.cs and RadialButtonOrientation.cs files from the CircleTheButtons project in that chapter.

You can access an XML file in the Resources section of a XAML file by defining an object of type XmlDataProvider with a Source attribute referencing the URI of the file:

<XmlDataProvider x:Key="emps" Source="Employees.xml" XPath="Employees" /> 


The XPath property is a string defined in XML Path Language as documented at www.w3.org/TR/xpath, except that no functions are allowed.

You can alternatively embed the XML data directly in the XAML file by use of an XML data island, which is enclosed in an x:XData element, like so:

<XmlDataProvider x:Key="emps" XPath="Employees">     <x:XData>         <Employees xmlns="">             ...         </Employees>     </x:XData> </XmlDataProvider> 


Either way, you can set the ItemsSource property of a ListBox to a Binding that references this resource:

ItemsSource="{Binding Source={StaticResource emps}, XPath=Employee}" 


The XPath property here indicates that each item is an Employee node. The complete EmployeeWheelWindow.xaml file using this technique is shown here.

EmployeeWheelWindow.xaml

[View full width]

<!-- === =================================================== EmployeeWheelWindow.xaml (c) 2006 by Charles Petzold ============================================= ========= --> <Window xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" xmlns:rp="clr-namespace:Petzold .CircleTheButtons" x: Title="Employee Wheel"> <Window.Resources> <XmlDataProvider x:Key="emps" Source="Employees.xml" XPath="Employees" /> </Window.Resources> <Grid> <ListBox Name="lstbox" HorizontalAlignment="Center" VerticalAlignment="Center" ItemsSource="{Binding Source={StaticResource emps}, XPath=Employee}" SelectedValuePath="Face"> <!-- Panel used for displaying the items. --> <ListBox.ItemsPanel> <ItemsPanelTemplate> <rp:RadialPanel Orientation="ByHeight" /> </ItemsPanelTemplate> </ListBox.ItemsPanel> <!-- Template used for displaying each item. --> <ListBox.ItemTemplate> <DataTemplate> <DockPanel Margin="3"> <Image DockPanel .Dock="Right" Stretch="None" Source="{Binding XPath=Face}" /> <UniformGrid Rows="3" VerticalAlignment="Center" Margin="12"> <TextBlock FontSize="16pt" TextAlignment="Center" Text="{Binding XPath=@Name}"/> <TextBlock FontSize="12pt" TextAlignment="Center" Text="{Binding XPath=BirthDate}" /> <TextBlock Name="txtblkHanded" FontSize="12pt" TextAlignment="Center" Text="Right-Handed" /> </UniformGrid> </DockPanel> <!-- DataTrigger to display "Left-Handed." --> <DataTemplate.Triggers> <DataTrigger Binding="{Binding XPath=LeftHanded}" Value="True"> <Setter TargetName="txtblkHanded" Property="Text" Value="Left-Handed" /> </DataTrigger> </DataTemplate.Triggers> </DataTemplate> </ListBox.ItemTemplate> </ListBox> <!-- Image in center shows selected item from ListBox. --> <Image HorizontalAlignment="Center" VerticalAlignment="Center" Stretch="None" Source="{Binding ElementName=lstbox , Path=SelectedValue}" /> </Grid> </Window>



The resources section contains the XmlDataProvider object referencing the Employees.xml file with an XPath of Employees. The ItemsSource property of ListBox is a binding to that resource with an XPath of Employee. Following the ListBox start tag, the ItemsPanel property of ListBox defines the panel used for displaying the items. This is the RadialPanel.

The ItemTemplate property of the ListBox defines the way that each Employee node will be displayed. The Source property of the Image and the Text properties of each TextBlock are all assigned bindings whose definitions contain only XPath properties. For subnodes of the Employee node, the XPath property simply references the name of the node. Here's the binding for the Image element:

Source="{Binding XPath=Face}" 


The Employees.xml file has subnodes for BirthDate, Face, and LeftHanded, but Name is an attribute of the Employee element. For referencing attributes of XML data, you must precede the attribute name with an @ symbol:

Text="{Binding XPath=@Name}" 


I decided not to display the items in different formats based on the value of LeftHanded. Instead, each item displays the text "Right-Handed" or "Left-Handed." The TextBlock displays the text "Right-Handed" by default:

<TextBlock Name="txtblkHanded"            FontSize="12pt" TextAlignment="Center"            Text="Right-Handed" /> 


The DataTemplate also contains a Triggers section, and if the LeftHanded element is true, the Text property of the TextBlock is set to "Left-Handed":

<DataTrigger Binding="{Binding XPath=LeftHanded}"              Value="True">     <Setter TargetName="txtblkHanded" Property="Text"             Value="Left-Handed" /> </DataTrigger> 


The EmployeeWheelApp.xaml application definition file completes the EmployeeWheel project.

EmployeeWheelApp.xaml

[View full width]

<!-- === ================================================ EmployeeWheelApp.xaml (c) 2006 by Charles Petzold ============================================= ====== --> <Application xmlns="http://schemas.microsoft.com/ winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" x:0" width="14" height="9" align="left" src="/books/4/266/1/html/2/images/ccc.gif" />.EmployeeWheelApp" StartupUri="EmployeeWheelWindow.xaml" />



The only class that derives from DataTemplate is HierarchicalDataTemplate. As the name suggests, you use this class in conjunction with Menu or (more commonly) TreeView to display hierarchically arranged data. Earlier in this chapter, the AutoTemplateSelection.xaml file demonstrated that an element can access an appropriate DataTemplate by matching the type of data the element is displaying with the DataType property of the DataTemplate resources. This facility is key to using multiple HierarchicalDataTemplate definitions in displaying different types of data items.

The following stand-alone XAML file contains an XML data island that contains a few nineteenth-century British authors and their books. In this XML file, Author elements have Name attributes and children named BirthDate, DeathDate, and Books. The Books element contains multiple Book children. Each Book element has a Title attribute and a PubDate ("publication date") attribute. The goal is to display this data in a TreeView control.

HierarchicalTemplates.xaml

[View full width]

<!-- === ===================================================== HierarchicalTemplates.xaml (c) 2006 by Charles Petzold ============================================= =========== --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml" Title="Authors and Their Books"> <Page.Resources> <!-- XML data. --> <XmlDataProvider x:Key="data" XPath="Authors"> <x:XData> <Authors xmlns=""> <Author Name="Jane Austen"> <BirthDate>1775</BirthDate> <DeathDate>1817</DeathDate> <Books> <Book Title="Sense and Sensibility"> <PubDate>1811</ PubDate> </Book> <Book Title="Pride and Prejudice"> <PubDate>1813</ PubDate> </Book> </Books> </Author> <Author Name="George Eliot"> <BirthDate>1819</BirthDate> <DeathDate>1880</DeathDate> <Books> <Book Title="Adam Bede"> <PubDate>1859</ PubDate> </Book> </Books> <Books> <Book Title="Middlemarch"> <PubDate>1872</ PubDate> </Book> </Books> </Author> <Author Name="Anthony Trollope"> <BirthDate>1815</BirthDate> <DeathDate>1882</DeathDate> <Books> <Book Title="Barchester Towers"> <PubDate>1857</ PubDate> </Book> <Book Title="The Way We Live Now"> <PubDate>1875</ PubDate> </Book> </Books> </Author> </Authors> </x:XData> </XmlDataProvider> <!-- Template for Author elements. --> <HierarchicalDataTemplate DataType="Author" ItemsSource="{Binding XPath=Books/Book}"> <StackPanel Orientation="Horizontal" TextBlock.FontSize="12pt"> <TextBlock Text="{Binding XPath=@Name}" /> <TextBlock Text=" (" /> <TextBlock Text="{Binding XPath=BirthDate}" /> <TextBlock Text="-" /> <TextBlock Text="{Binding XPath=DeathDate}" /> <TextBlock Text=")" /> </StackPanel> </HierarchicalDataTemplate> <!-- Template for Book elements. --> <HierarchicalDataTemplate DataType="Book"> <StackPanel Orientation="Horizontal" TextBlock.FontSize="10pt"> <TextBlock Text="{Binding XPath=@Title}" /> <TextBlock Text=" (" /> <TextBlock Text="{Binding XPath=PubDate}" /> <TextBlock Text=")" /> </StackPanel> </HierarchicalDataTemplate> </Page.Resources> <!-- The TreeView control. --> <TreeView ItemsSource="{Binding Source={StaticResource data}, XPath=Author}" /> </Page>



Let's begin examining this file at the bottom with the TreeView control. The ItemsSource property is a binding with the resource that has a key of "data." That's the XmlDataProvider object defined at the beginning of the resource section. The XmlDataProvider has an XPath property of Authors, and the ItemsSource binding of the TreeView has an XPath of Author. Alternatively, you can define one of these XPath properties as Authors/Author and leave out the other. Whichever way you do it, the top-level items on the TreeView will be the three Author elements from the XML data.

How will these Author elements be displayed? The TreeView control searches the resources for a DataTemplate or HierarchicalDataTemplate with a DataType of Author, and it finds onethe first HierarchicalDataTemplate defined in the file's resources section. This template contains a visual tree describing how to display the items. Six TextBlock elements with a font size of 12 points display the Name attribute of the Author element, and the BirthDate and DeathDate elements surrounded by parentheses and separated by a dash. Keep in mind that you're specifying these elements and attributes using a Binding, so your template can include a reference to a data converter if you need one for proper formatting. Using such a data converter is probably a better approach to formatting dates, but I wanted to restrict this example to a single XAML file.

That first HierarchicalDataTemplate not only describes how to display the Author element, but the ItemsSource property indicates the XPath of the items to be displayed under each author. This XPath is Books/Book. Therefore, under each author in the TreeView will appear items for each of the author's books.

How are these Book elements to be displayed? The second HierarchicalDataTemplate has a DataType of Book, so that's the one. This time the visual tree contains four TextBlock elements to display the Title attribute and the PubDate element. The second HierarchicalDataTemplate has no ItemsSource property, so the hierarchy ends here. But it could go much deeper.

Although it's convenient to define hierarchical data in XML, you can use objects as well. An Author class might have properties named Name, BirthDate, and DeathDate, and a Books property, perhaps of type List<Book>, referring to a Book class. The bindings are nearly the same, except that you use Path rather than XPath, and ItemsSource always refers to a collection.

Many real-life applications use a hierarchy in at least one area of the program, and that's the Help window. The Contents section of the Help window is very often a hierarchy of topics. The YellowPad program in Chapter 22 had a Help window that used navigation to display Help topics. I'd like to generalize that Help window a bit so that you can use it in your own applications, or enhance it if you'd like.

In any project in which you want to use this simple Help system, you use Visual Studio to create a directory named Help in the project. That directory must include a file named HelpContents.xml with a Build Action of Resource. A sample HelpContents.xml file is shown here. This file and all files until the end of this chapter can be found in the FlowDocumentHelp project.

HelpContents.xml

[View full width]

<!-- ============================================== HelpContents.xml (c) 2006 by Charles Petzold ============================================= = --> <HelpContents xmlns=""> <Topic Header="Copyright Information" Source="Help/Copyright.xaml" /> <Topic Header="Program Overview" Source="Help/ Overview.xaml" /> <Topic Header="The Menu"> <Topic Header="The File Menu" Source="Help /FileMenu.xaml" /> <Topic Header="The Help Menu" Source="Help /HelpMenu.xaml" /> </Topic> </HelpContents>



The file consists of nested Topic elements with a required Header attribute and an optional Source attribute. Generally, the Source element is missing only from Topic elements that have child topics. Each Source element indicates a XAML file, also located in the Help directory. Here's a sample.

Overview.xaml

[View full width]

<!-- =========================================== Overview.xaml (c) 2006 by Charles Petzold =========================================== --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft .com/winfx/2006/xaml" Title="Program Overview"> <FlowDocumentReader> <FlowDocument> <Paragraph Style="{StaticResource Header}"> Program Overview </Paragraph> <Paragraph> This file presents an overview of the program. </Paragraph> <Paragraph> The description is probably several paragraphs in length. Perhaps it makes reference to the <Hyperlink NavigateUri="FileMenu. xaml">File Menu</Hyperlink> and <Hyperlink NavigateUri="HelpMenu. xaml">Help Menu</Hyperlink>. </Paragraph> </FlowDocument> </FlowDocumentReader> </Page>



Each help topic is a file with a Page element containing a FlowDocumentReader and a FlowDocument. You'll notice that the first Paragraph element makes reference to a Style with a tag of "Header." The Help directory also contains a file named HelpStyles.xaml that you can make as simple or as elaborate as you want.

HelpStyles.xaml

[View full width]

<!-- ============================================= HelpStyles.xaml (c) 2006 by Charles Petzold ============================================= --> <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006 /xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml"> <Style TargetType="{x:Type FlowDocumentReader}"> <Setter Property="ViewingMode" Value="Scroll" /> </Style> <Style x:Key="Center" TargetType="{x:Type Paragraph}"> <Setter Property="TextAlignment" Value="Center" /> </Style> <Style x:Key="ProgramName" TargetType="{x:Type Paragraph}" BasedOn="{StaticResource Center}"> <Setter Property="FontSize" Value="32" /> <Setter Property="FontStyle" Value="Italic" /> <Setter Property="LineHeight" Value="24" /> </Style> <Style x:Key="Header" TargetType="{x:Type Paragraph}"> <Setter Property="FontSize" Value="20" /> <Setter Property="FontWeight" Value="Bold" /> <Setter Property="LineHeight" Value="16" /> </Style> </ResourceDictionary>



You reference this file in the Resources section of the application definition file, as shown in the following file for an application named Generic.

GenericApp.xaml

[View full width]

<!-- ============================================= GenericApp.xaml (c) 2006 by Charles Petzold ============================================= --> <Application xmlns="http://schemas.microsoft.com/ winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" x: StartupUri="GenericWindow.xaml"> <Application.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="Help/ HelpStyles.xaml" /> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Application.Resources> </Application>



Besides including the HelpStyles.xaml file as a resource, the application definition file also starts up the GenericWindow.xaml file, which lays out a simple window that includes a menu with a Help item.

GenericWindow.xaml

[View full width]

<!-- ================================================ GenericWindow.xaml (c) 2006 by Charles Petzold ============================================= === --> <Window xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" x: Title="Generic"> <DockPanel> <Menu DockPanel.Dock="Top"> <MenuItem Header="_Help"> <MenuItem Header="_Help for Generic..." Command="Help" /> </MenuItem> </Menu> <TextBox AcceptsReturn="True" /> </DockPanel> <Window.CommandBindings> <CommandBinding Command="Help" Executed="HelpOnExecuted" /> </Window.CommandBindings> </Window>



The HelpOnExecuted event handler for the Help menu item is located in the code-behind file for the window.

GenericWindow.cs

[View full width]

//---------------------------------------------- // GenericWindow.cs (c) 2006 by Charles Petzold //---------------------------------------------- using Petzold.FlowDocumentHelp; using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.Generic { public partial class GenericWindow : Window { public GenericWindow() { InitializeComponent(); } void HelpOnExecuted(object sender, ExecutedRoutedEventArgs args) { HelpWindow win = new HelpWindow(); win.Owner = this; win.Title = "Help for Generic"; win.Show(); } } }



And finally we reach the HelpWindow class, which displays the Help information. The HelpWindow.xaml file makes reference to three image files, which are also located in the Help directory. These images are those commonly seen in Help tables of content. I obtained the images from the library included with Visual Studio, but I renamed them and in one case converted an image from an icon to a PNG file.

HelpWindow.xaml

[View full width]

<!-- ============================================= HelpWindow.xaml (c) 2006 by Charles Petzold ============================================= --> <Window xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" x: Title="Help" Width="600" Height="500" WindowStartupLocation="CenterScreen" ShowInTaskbar="False"> <Window.Resources> <XmlDataProvider x:Key="data" Source="Help/HelpContents .xml" XPath="HelpContents" /> <HierarchicalDataTemplate DataType="Topic" ItemsSource="{Binding XPath=Topic}" > <!-- Each TreeViewItem is an Image and a TextBlock. --> <StackPanel Orientation="Horizontal"> <Image Name="img" Source="Help /HelpImageQuestionMark.png" Margin="2" Stretch="None" /> <TextBlock Text="{Binding XPath=@Header}" FontSize="10pt" VerticalAlignment="Center" /> </StackPanel> <HierarchicalDataTemplate.Triggers> <!-- Use closed-book image for items that have children. --> <DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type TreeViewItem}}, Path=HasItems}" Value="True"> <Setter TargetName="img" Property="Image.Source" Value="Help /HelpImageClosedBook.png" /> </DataTrigger> <!-- Use open-book image for items that are expanded. --> <DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type TreeViewItem}}, Path=IsExpanded}" Value="True"> <Setter TargetName="img" Property="Image.Source" Value="Help /HelpImageOpenBook.png" /> </DataTrigger> </HierarchicalDataTemplate.Triggers> </HierarchicalDataTemplate> </Window.Resources> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="33*" /> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="67*" /> </Grid.ColumnDefinitions> <!-- Contents TreeView on the left. --> <TreeView Name="treevue" Grid.Column="0" ItemsSource="{Binding Source={StaticResource data}, XPath=Topic}" SelectedValuePath="@Source" SelectedItemChanged="TreeViewOnSelectedItemChanged" /> <!-- GridSplitter in the middle. --> <GridSplitter Grid.Column="1" Width="6" HorizontalAlignment="Center" VerticalAlignment="Stretch" /> <!-- Frame on the right. --> <Frame Name="frame" Grid.Column="2" Navigated="FrameOnNavigated" /> </Grid> </Window>



Approximately the first half of the file is devoted to a Resources section; the remainder is the layout of the Window, which is a three-column Grid with a TreeView on the left, a Frame on the right, and a GridSplitter in the middle.

The Resources section begins with an XmlDataProvider element that references the HelpContents.xml file with a key of "data" and an XPath attribute of HelpContents, which is the root element of the HelpContents.xml file. Two DataTrigger elements select the proper images for the TreeView items.

Toward the bottom of the file, the TreeView control binds its ItemsSource property to this data and indicates that its SelectedValuePath is the Source attribute of the contents file. I originally bound the Source property of the Frame to the SelectedValue property of the TreeView, but it didn't quite provide the functionality I desired, so I decided to link the two controls in the code-behind file.

HelpWindow.cs

[View full width]

//------------------------------------------- // HelpWindow.cs (c) 2006 by Charles Petzold //------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Input; using System.Windows.Media; namespace Petzold.FlowDocumentHelp { public partial class HelpWindow { public HelpWindow() { InitializeComponent(); treevue.Focus(); } public HelpWindow(string strTopic): this() { if (strTopic != null) frame.Source = new Uri(strTopic, UriKind.Relative); } // When TreeView selected item changes, set the source of the Frame, void TreeViewOnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> args) { if (treevue.SelectedValue != null) frame.Source = new Uri(treevue .SelectedValue as string, UriKind .Relative); } // When the Frame has navigated to a new source, synchronize TreeView. void FrameOnNavigated(object sender, NavigationEventArgs args) { if (args.Uri != null && args.Uri. OriginalString != null && args.Uri .OriginalString.Length > 0) { FindItemToSelect(treevue, args.Uri .OriginalString); } } // Search through items in TreeView to select one. bool FindItemToSelect(ItemsControl ctrl, string strSource) { foreach (object obj in ctrl.Items) { System.Xml.XmlElement xml = obj as System.Xml.XmlElement; string strAttribute = xml .GetAttribute("Source"); TreeViewItem item = (TreeViewItem) ctrl.ItemContainerGenerator .ContainerFromItem(obj); // If the TreeViewItem matches the Frame URI, select the item. if (strAttribute != null && strAttribute.Length > 0 && strSource.EndsWith(strAttribute)) { if (item != null && !item. IsSelected) item.IsSelected = true; return true; } // Expand the item to search nested items. if (item != null) { bool isExpanded = item.IsExpanded; item.IsExpanded = true; if (item.HasItems && FindItemToSelect(item, strSource)) return true; item.IsExpanded = isExpanded; } } return false; } } }



A second constructor allows a program using the HelpWindow class to initialize the Frame control with one of the documents. The handler for the SelectedItemChanged event obtains the selected value from the TreeView and sets it in the Frame. Getting the table of contents to synchronize with the document proved to be a little messy. Although TreeView defines SelectedItem and SelectedValue properties, they are read-only. To select an item in the TreeView, you must set the IsSelected property of the individual TreeViewItem. This required searching through all the items to find the one to select. Still, doing the job "manually" gave me a valuable feature, and I'm glad I did it. Bindings are powerful, but sometimes they're no substitute for actual code.

Despite the length of this chapter, the subject of templates is not yet exhausted. Toward the end of the next chapter, I'll show you how to group items in a ListBox and use templates to display headings on these groups.




Applications = Code + Markup. A Guide to the Microsoft Windows Presentation Foundation
Applications = Code + Markup: A Guide to the Microsoft Windows Presentation Foundation (Pro - Developer)
ISBN: 0735619573
EAN: 2147483647
Year: 2006
Pages: 72

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net