Introduction
Over the past year, I have been playing and working with WPF, and have published a few articles (though not as good as Josh Smith's / Karl Shifflett's) on WPF. As such, there have been a few questions/comments/queries posted in the forums attached to these articles. One of the most common questions/puzzlements I see is that people seem to be honestly lost by the differences between doing something in XAML opposed to doing something in code (C# | VB.NET), and how the heck Content works. I recall one of CodeProject's greatest authors, Marc Clifton, being particularly frustrated about Content, and Marc has actually created his own declarative XML markup: MyXaml. When someone like that is getting frustrated, it must be confusing.
This article will attempt to outline several areas that people seem to struggle with the most (based on the questions I have been asked in my own articles).
Contents
Here is what I will be covering in this article:
Prerequisites
In order to get the most out of this article, I would suggest you download the free .NET Disassembler, Reflector, which is freely available using the new RedGate download page. RedGate recently took over development of this product from Lutz Roeder. Thanks Lutz, it is, and always has been, a great product.
If we consider the following section of XAML:
<Window x:Class="WPF_Mysteries.Window2"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window2" Height="300" Width="300">
<StackPanel Orientation="Vertical">
<Button Background="Aqua" Margin="10" Width="Auto" Content="1" />
<Button Background="Green" Margin="10" Width="Auto" Content="2" />
<Button Background="Pink" Margin="10" Width="Auto" Content="3" />
</StackPanel>
</Window>
We can see that this produces the following screenshot:
But how does this work? Is it magic? Well, actually no. If we go and look up a StackPanel
in Reflector, and have a look at its definition:
This doesn't tell us too much, but we can see that this inherits from Panel
, which in turn looks like the following:
What is very interesting here is an attribute, namely the ContentPropertyAttribute
, which here is shown as being Children
. Also worth a mention is the interface IAddChild
. MSDN states the following: IAddChild
provides a means to parse elements which permit child elements or text. The main use of IAddChild
is to support FrameworkElementFactory
.
For purposes of establishing or defining a content property or content model, IAddChild
is obsolete. Apply the ContentPropertyAttribute
to a custom class instead.
OK, so Panel
has both a ContentPropertyAttribute
and implements IAddChild
. If we look at the Children
property, which is a GET only property, as shown below:
We can see that we can use this to obtain a UIElementCollection
which is what is used to add child UIElement
s to. Panel
actually also uses IAddChild
behind the scenes, as we can see below:
We can see this is where the real work of adding objects to the internal Children
property of Panel
happens.
These are the only methods within the IAddChild
interface.
That is how some of the standard System.Windows.Control
s that support children (such as Grid
/StackPanel
/Canvas
etc.) deal with Content. However, if the control is supposed to have a single piece of Content, you will find that it still implements the IAddChild
interface and has a Content
property, much the same as described above. This is shown below for the System.Windows.Controls.Label
control, which in turn inherits from System.Windows.Controls.ContentControl
.
In order to demonstrate ContentPropertyAttribute
further, let's soldier on and try and get to the bottom of the magical Content
property. I have a small UserControl
that I have declared as follows:
<UserControl x:Class="WPF_Mysteries.ContentTestControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="auto" Width="auto">
<StackPanel Orientation="Vertical">
<Label Content="There are currently"/>
<Label Content="{Binding SomeContent.Count}"/>
<Label Content="elements, on the SomeContent property"/>
</StackPanel>
</UserControl>
And here is the related C# code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.ComponentModel;
using System.Windows.Markup;
using System.Collections.ObjectModel;
namespace WPF_Mysteries
{
[ContentProperty("SomeContent")]
public partial class ContentTestControl : UserControl
{
#region Public Properties
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public ObservableCollection<UIElement> SomeContent { get; set; }
#endregion
#region Ctor
public ContentTestControl()
{
InitializeComponent();
SomeContent = new ObservableCollection<UIElement>();
this.DataContext = this;
}
#endregion
}
}
I then use this ContentTestControl
control within a XAML Window as follows. Notice that I am actually adding four controls here, but I do not tell it what property I am using to add these controls to. This is done via the magic of ContentPropertyAttribute
, which is pointing to the ContentTestControl.SomeContent
property. The XAML parser knows what to do with these four controls, they are simply added to the ContentTestControl.SomeContent
property (which is after all a ObservableCollection<UIElement>
, so should support Adds; it's a collection, is it not?).
<local:ContentTestControl x:Name="testControl">
<StackPanel Orientation="Vertical">
<Label>one</Label>
<Label>two</Label>
</StackPanel>
<StackPanel Orientation="Vertical">
<Label>3</Label>
<Label>4</Label>
</StackPanel>
<StackPanel Orientation="Vertical">
<Label>5</Label>
<Label>6</Label>
</StackPanel>
<StackPanel Orientation="Vertical">
<Label>7</Label>
<Label>8</Label>
</StackPanel>
</local:ContentTestControl>
Which results in the following screenshot. It should, however, be noted that since my ContentTestControl
UserControl inherits from UserControl
, it is UserControl
that actually adds the elements as actual UI elements, which is why we are seeing different UIElement
s within the screenshot than you might have thought. You may have been expecting to see the four * StackPanel
s shown above. But ContentTestControl.SomeContent
is really just a property that is holding the values specified by ContentPropertyAttribute
; it doesn't necessarily do anything with these values. In the standard System.Windows.Controls
controls, the Content
property probably would affect the UI as well.
It can be seen from this screenshot that the Binding
that was set up in ContentTestControl
(where ContentTestControl.xaml has the Binding
set as follows: {Binding SomeContent.Count}
) shows a result of 4. This is interesting since ContentTestControl
didn't use the IAddChild
interface at all, but still worked just fine; this is all thanks to the ContentPropertyAttribute
usage on this class.
One thing that seems to crop up over and over again is people really don't know how to create Style
s or templates in code. Obviously, XAML is better suited to doing this than code, so why the hell would you want to do this in code anyway? Well, consider a system which is quite dynamic, and may be driven by meta driven objects, where what objects and properties you may be using are not known at design time. I am actually working on a system like this right now, where everything (most screens) are driven by metadata, where the metadata contains crucial information about the objects the metadata is associated with.
The system is quite dynamic, so statically declared Style
s or templates just don't cut the mustard, so we have to create them in code. I am not saying this is normal, but I have been asked how to do this enough times to warrant a small rant on the subject. Consider the following section a small rant on how to create Template/Styles in code.
Creating Styles in Code
Let's start with a simple example. Suppose I have the following Style
, declared in XAML, which is applied to ListBoxItem
s:
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="TextElement.FontSize" Value="14"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter Property="Foreground" Value="Black" />
<Setter Property="Background">
<Setter.Value>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<LinearGradientBrush.GradientStops>
<GradientStop Color="#0E4791" Offset="0"/>
<GradientStop Color="#468DE2" Offset="1"/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Setter.Value>
</Setter>
<Setter Property="Cursor" Value="Hand"/>
</Trigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
I think this is pretty self-explanatory. But how about the code equivalent?
Style styleListBoxItem = new Style(typeof(ListBoxItem));
styleListBoxItem.Setters.Add(new Setter
{
Property=TextElement.FontSizeProperty,
Value=14.0
});
Trigger triggerIsMouseOver =
new Trigger { Property=ListBoxItem.IsMouseOverProperty, Value=true };
triggerIsMouseOver.Setters.Add(
new Setter(ListBoxItem.ForegroundProperty, Brushes.Black));
GradientStopCollection gradientStopsLinearBrush = new GradientStopCollection();
gradientStopsLinearBrush.Add(
new GradientStop((Color)ColorConverter.ConvertFromString("#0E4791"), 0.0));
gradientStopsLinearBrush.Add(
new GradientStop((Color)ColorConverter.ConvertFromString("#468DE2"), 1.0));
LinearGradientBrush backgroundLinearBrush =
new LinearGradientBrush(gradientStopsLinearBrush)
{
StartPoint = new Point(0,0),
EndPoint = new Point(0,1)
};
triggerIsMouseOver.Setters.Add(
new Setter(ListBoxItem.BackgroundProperty, backgroundLinearBrush));
triggerIsMouseOver.Setters.Add(
new Setter(ListBoxItem.CursorProperty, Cursors.Hand));
styleListBoxItem.Triggers.Add(triggerIsMouseOver);
As you can see, for Style
s, there is pretty much a 1 to 1 mapping to the XAML code, it's not that bad really. The only thing worth a mention is the part where we have a XAML Setter
, which looks like the following:
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
....
<Setter Property="Cursor" Value="Hand"/>
....
</Style>
</ListBox.ItemContainerStyle>
This may not have been obvious to you, but the Cursor
property here actually refers to the ListBoxItem
, so when we do this in code, we must make sure to use the ListBoxItem.Cursor
Dependency Property. This is the case with all Setter
properties, unless they state another fully qualified property such as TextElement.FotSize
.
triggerIsMouseOver.Setters.Add(
new Setter(ListBoxItem.CursorProperty, Cursors.Hand));
styleListBoxItem.Triggers.Add(triggerIsMouseOver);
Other than that, I think Style
s are pretty easy to do in code.
Creating Templates in Code
Unfortunately, Templates are slightly harder to create in code than Style
s. But they are still achievable. Let's consider the following DataTemplate that I have declared in XAML to represent a simple Person
object:
<DataTemplate DataType="{x:Type local:Person}">
<StackPanel x:Name="spOuter"
Orientation="Horizontal" Margin="10">
<Path Name="pathSelected" Fill="Orange"
Stretch="Fill" Stroke="Orange" Width="15"
Height="20" Data="M0,0 L 0,10 L 5,5"
Visibility="Hidden"/>
<StackPanel x:Name="spInner" Orientation="Horizontal">
<Label Content="{Binding FirstName}" Foreground="Black"/>
<Ellipse Fill="Black" Height="5" Width="5"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
<Label Content="{Binding LastName}" Foreground="Black"/>
</StackPanel>
</StackPanel>
<DataTemplate.Triggers>
<DataTrigger
Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType={x:Type ListBoxItem}, AncestorLevel=1},
Path=IsSelected}"
Value="True">
<Setter TargetName="pathSelected"
Property="Visibility" Value="Visible" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
Where a Person
object looks like the following code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace WPF_Mysteries
{
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
}
Based on what we know about WPF/XAML and what we have just seen about Style
s, you would think that there was a 1 to 1 mapping for DataTemplates as well. But there isn't.
I think the best way to dissect this DataTemplate
is piece by piece. So let's grab a section of it at a time, and then at the end, I'll show the whole code section for the code created DataTemplate
that does the same as the XAML version. Let's start with the actual creation of a DataTemplate
object:
DataTemplate dataTemplate = new DataTemplate(typeof(Person));
That's pretty easy, right? How about getting something in the DataTemplate
? Mmm, well, here is the code:
Is this what you were expecting? Probably not. So what the heck is going on here? Let's start at the end to understand the beginning. We can see there is a VisualTree
being set to a strange looking object of Type FrameworkElementFactory
. What the @$*^!!!
To understand this, let's use our favourite tool, Reflector, and get to the bottom of this. If we start with the DataTemplate
:
Mmmm, no VisualTree
property here (even though we can see that it depends on the VisualTree
property), but we can see this inherits from FrameworkTemplate
, so let's continue to look at that.
Where the VisualTree
property looks like this:
Aha, it's becoming a little clearer. So we have a VisualTree
property that we need to supply a FrameworkElementFactory
to. Cool. But what are these FrameworkElementFactory
things? This class is a deprecated way to programmatically create templates, which are subclasses of FrameworkTemplate
, such as ControlTemplate
or DataTemplate
. MSDN actually states "The recommended way to programmatically create a template is to load XAML from a string or a memory stream using the Load
method of the XamlReader
class." But when you have to do it in code, this is your only option.
So carrying on, it's really just a question of creating as many of these FrameworkElementFactory
objects that you need to represent your required DataTemplate
s VisualTree
. In my example, this would be as follows:
<StackPanel x:Name="spOuter">
<Path Name="pathSelected"/>
<StackPanel x:Name="spInner" >
<Label />
<Ellipse />
<Label />
</StackPanel>
</StackPanel>
For which there is a a bunch of code-behind created FrameworkElementFactory
objects, and they are wired up as required with all the relevant properties/relationships set in code. Finally, the top level FrameworkElementFactory
is set as the DataTemplate
s VisualTree
property. So that's how the VisualTree
stuff works. But what about Triggers?
Luckily, Triggers are pretty easy to do in code. Here is how:
DataTrigger dataTrigger = new DataTrigger();
dataTrigger.Binding = new Binding {
Path = new PropertyPath(ListBoxItem.IsSelectedProperty),
RelativeSource =
new RelativeSource(RelativeSourceMode.FindAncestor,
typeof(ListBoxItem), 1)
};
dataTrigger.Value = true;
dataTrigger.Setters.Add(
new Setter(FrameworkElement.VisibilityProperty,
Visibility.Visible, "pathSelected"));
dataTemplate.Triggers.Add(dataTrigger);
Putting all this together, we end up with a code-behind DataTemplate
that looks like this:
DataTemplate dataTemplate = new DataTemplate(typeof(Person));
FrameworkElementFactory spOuterFactory =
new FrameworkElementFactory(typeof(StackPanel));
spOuterFactory.SetValue(
StackPanel.OrientationProperty, Orientation.Horizontal);
spOuterFactory.SetValue(
StackPanel.MarginProperty, new Thickness(10));
#region Path
FrameworkElementFactory pathSelectedFactory =
new FrameworkElementFactory(typeof(Path), "pathSelected");
pathSelectedFactory.SetValue(Path.FillProperty, Brushes.Orange);
pathSelectedFactory.SetValue(Path.StretchProperty, Stretch.Fill);
pathSelectedFactory.SetValue(Path.StrokeProperty, Brushes.Orange);
pathSelectedFactory.SetValue(Path.WidthProperty, 15.0);
pathSelectedFactory.SetValue(Path.HeightProperty, 20.0);
pathSelectedFactory.SetValue(Path.VisibilityProperty, Visibility.Hidden);
PathGeometry pathGeometry = new PathGeometry();
pathGeometry.Figures = new PathFigureCollection();
PathFigure pathFigure = new PathFigure();
pathFigure.StartPoint = new Point(0, 0);
pathFigure.Segments = new PathSegmentCollection();
pathFigure.Segments.Add(new LineSegment() { Point = new Point(0, 10) });
pathFigure.Segments.Add(new LineSegment() { Point = new Point(5, 5) });
pathGeometry.Figures.Add(pathFigure);
pathSelectedFactory.SetValue(Path.DataProperty, pathGeometry);
spOuterFactory.AppendChild(pathSelectedFactory);
#endregion
#region Inner StackPanel
FrameworkElementFactory spInnerFactory =
new FrameworkElementFactory(typeof(StackPanel));
spInnerFactory.SetValue(StackPanel.OrientationProperty,
Orientation.Horizontal);
FrameworkElementFactory labelFNFactory =
new FrameworkElementFactory(typeof(Label));
Binding bindingFirstName = new Binding();
bindingFirstName.Path = new PropertyPath("FirstName");
labelFNFactory.SetBinding(Label.ContentProperty, bindingFirstName);
labelFNFactory.SetValue(Label.ForegroundProperty, Brushes.Black);
spInnerFactory.AppendChild(labelFNFactory);
FrameworkElementFactory ellipseFactory =
new FrameworkElementFactory(typeof(Ellipse));
ellipseFactory.SetValue(Ellipse.FillProperty, Brushes.Black);
ellipseFactory.SetValue(Ellipse.HeightProperty, 5.0);
ellipseFactory.SetValue(Ellipse.WidthProperty, 5.0);
ellipseFactory.SetValue(Ellipse.HorizontalAlignmentProperty,
HorizontalAlignment.Center);
ellipseFactory.SetValue(Ellipse.VerticalAlignmentProperty,
VerticalAlignment.Center);
spInnerFactory.AppendChild(ellipseFactory);
FrameworkElementFactory labelLNFactory =
new FrameworkElementFactory(typeof(Label));
Binding bindingLastName = new Binding();
bindingLastName.Path = new PropertyPath("LastName");
labelLNFactory.SetBinding(Label.ContentProperty, bindingLastName);
labelLNFactory.SetValue(Label.ForegroundProperty, Brushes.Black);
spInnerFactory.AppendChild(labelLNFactory);
spOuterFactory.AppendChild(spInnerFactory);
#endregion
#region DataTrigger
DataTrigger dataTrigger = new DataTrigger();
dataTrigger.Binding = new Binding {
Path = new PropertyPath(ListBoxItem.IsSelectedProperty),
RelativeSource =
new RelativeSource(RelativeSourceMode.FindAncestor,
typeof(ListBoxItem), 1)
};
dataTrigger.Value = true;
dataTrigger.Setters.Add(
new Setter(FrameworkElement.VisibilityProperty,
Visibility.Visible, "pathSelected"));
dataTemplate.Triggers.Add(dataTrigger);
#endregion
dataTemplate.VisualTree = spOuterFactory;
return dataTemplate;
When I run the attached app, the code-behind version does exactly the same as the XAML defined version:
It is more code when compared to the XAML.
That's it
That's all I wanted to say this time, I hope it helps some of you. Could I just ask, if you liked this article, please vote for it.