![]() |
Platforms, Frameworks & Libraries »
Windows Presentation Foundation »
General
Intermediate
License: The Code Project Open License (CPOL)
WPF : How to create Styles in code/and magical ContentBy Sacha BarberWPF : How to create Styles in code/and magical Content |
C# (C# 3.0), .NET (.NET 3.0, .NET 3.5), WPF, Architect, Dev, Design
|
||||||||
|
Advanced Search |
|
|
|
||||||||||||||||
Over the past year I have been playing and working with WPF, and have published a few articles (though not as good as Josh Smiths / Karl Shiffletts) 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 codeprojects greatest authors, Marc Clifton
being particularly frustrated about Content, and Marc has actually created his own declarative XML markup MyXaml. So 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). I hope that
That's the plan at any rate
Here is what I will be covering in this article
In order to get the most out of this article, I would suggest you download the free .NET Dissambler, Reflector, which is freely available using the new Redgate download page. Redgate recently took over development of this product from Lutz Roeder. So 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 UIElements to.
Panel actually also uses IAddChild behind the scenes, which as we can see below it does
Which as we can see is where the real work of adding objects to the internal Children property of Panel.
These are the only methods within the IAddChild interface.
So that is how some of the standard System.Windows.Controls, that support children (such as Grid/StackPanel/Canvas etc 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 the 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
{
/// <summary>
/// This demonstrates how to use the ContentPropertyAttribute, which is
/// how some of the native Controls such as Grid/Canvas/StackPanel etc etc
/// know what to do with the content that is added to them in XAML.
/// Basically the ContentPropertyAttribute, tells the XAML parser,
/// what code behind property should be used when the XAML parser
/// finds some content.
/// </summary>
[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>();
// Allows binding to it's own properties
this.DataContext = this;
}
#endregion
}
}
I then use this ContentTestControl control within a XAML Window as follows. Notice that I am actually adding 4 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
the ContentPropertyAttribute, which is pointing to the ContentTestControl.SomeContent property. The XAML parser knows what to do with these 4 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 screen shot. 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 UIElements, within the screenshort
than you might have thought. You may have been expecting to see the 4 * StackPanelShown above. But the ContentTestControl.SomeContent is really just a property that
is holding the values specified by the ContentPropertyAttribute, it doesnt nessecarily 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 the ContentTestControl (where the ContentTestControl.xaml has the Binding set as follows {Binding SomeContent.Count}) shows a result of 4. This is interesting since the
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 Styles 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.
So the system is quite dynamic, so statically declared Styles 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. So consider the following section a small rant on how to create Template/Styles in code.
Let's start with a simple example, suppose I have the following Style, declared in XAML, which is applied to ListBoxItems.
<ListBox.ItemContainerStyle>
<!-- And how about a nice simple Style for a ListBoxItem -->
<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
//And here is the C# code to achieve the above
Style styleListBoxItem = new Style(typeof(ListBoxItem));
styleListBoxItem.Setters.Add(new Setter
{
Property=TextElement.FontSizeProperty,
Value=14.0
});
//Trigger
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)
};
//Trigger setters
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 Styles, 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>
<!-- And how about a nice simple Style for a ListBoxItem -->
<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 is
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 Styles, are pretty easy to do in code
Unfortunately, Templates are slightly harder to create in code than Styles. But they are still achievable. Let's consider the following DataTemplate that
I have declared in XAML to represent a simple Person object
<!-- Lets have a DataTemplate for a Person-->
<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
{
/// <summary>
/// A simple POCO, used for Binding in Window1
/// </summary>
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 Styles, you would think that there was a 1 to 1 mapping for DataTemplates as well. But there aint.
I think the best way to disect 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. So how about getting something in the DataTemplate. Mmm, well here is the code.
Is this what you were expecting? Probably not. So what the feck is going on here. Lets 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 F$*k!!!
To understand this, lets use our favourite tool Reflector, and get to the bottom of this. If we start with DataTemplate
Mmmm, No VisualTree property here (even though we can see that it depends on 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, its 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, its really just a question of creating as many of these FrameworkElementFactory objects, that you need to represent your required DataTemplates 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 DataTemplates 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
//And here is the C# code to achieve the above
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);
//FirstName
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);
//Ellipse
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);
//LastName
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);
//Add to outer StackPanel
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 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, but needs must
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.
| You must Sign In to use this message board. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 26 Sep 2008 Editor: |
Copyright 2008 by Sacha Barber Everything else Copyright © CodeProject, 1999-2009 Web15 | Advertise on the Code Project |