.NET (293) administrative (41) Ajax (42) AngularJS (2) ASP.NET (144) bicycle (2) books (180) browser (8) C# (131) cars (1) chess (28) CodePlex (10) Coma (8) database (46) deployment (3) Entity Framework (2) essay (110) flash/shockwave (2) flex (1) food (3) friend (2) game (20) idea (5) IIS (8) javascript (82) LInQ (2) Linux (6) management (4) manga (42) misc (665) mobile (1) movies (89) MsAccess (1) murder (2) music (64) mysql (1) news (98) permanent (1) personal (67) PHP (1) physics (2) picture (307) places (12) politics (13) programming (499) rant (119) religion (3) science (40) Sharepoint (3) software (57) T4 (2) technology (11) Test Driven Development (4) translation (2) VB (2) video (97) Visual Studio (44) web design (45) Windows API (8) Windows Forms (3) Windows Server (4) WPF/Silverlight (63) XML (11)

Wednesday, December 01, 2010

Cloning a WPF ControlTemplate

I was exploring the option of not overwriting the ControlTemplate of a WPF Control when I try adding stuff to it. Instead, I tried to get the ControlTemplate and manipulate it before putting it back. It is not as easy as it seems. Even more, people stack over each other to advise everybody not to do it. I am not saying it is an easy option, so my advise is to try other alternatives, if you have them, but the idea is: it can be done!

Let's take it step by step. In order to get the control template we should get it as soon as it is available, but perhaps before applying it. One could override OnApplyTemplate and do it there or, as it is my case (trying to do it via attached properties and lacking an ApplingTemplate event), do it once when the control is initializing. The control template is easily obtained via the Template property. If you try to change anything in it, though, you will get an exception, because the template is sealed. So the only option is to clone it, change stuff in it, then set the control template to that clone.

The template is of type ControlTemplate, but it doesn't seem to contain much. It has Resources and Triggers properties, also a VisualTree property and a LoadContent method. There is also a Template property in the ControlTemplate class... try to set it and a null exception will be thrown, so forget it. The first two are easy to use, just itterate through the collections. VisualTree is of the weird and undocumented type FrameworkElementFactory, while LoadContent is a method that returns a DependencyObject.

Well, the idea is that LoadContent will return the content of the template which you should use to set the VisualTree property, but the process of getting a DependencyObject and getting a FrameworkElementFactory tree is not simple.

First things first: get a new ControlTemplate. Its contructor gets a Type parameter which we take from the TargetType property of the original template. We then add any resources from the original template to the resources of the new one. Next step is to take the content, using LoadContent, which will get us the first child of the element tree. In order to traverse it we will use the VisualTreeHelper static class which exposes the GetChildrenCount and GetChild methods.

The next step is to create a FrameworkElementFactory. It has a constructor which receives a Type and another which gets a Type and a name string. We will use the first, since the Name can be set afterwards. The type we get from the type of the DependencyObject returned by LoadContent. The VisualTree of the new control template will have to be this new factory object, but it also needs all the properties of the original object as well as all its children.

In order to get the dependency and attached properties of each element we will use the MarkupWriter.GetMarkupObjectFor method, which returns a MarkupObject. Each of its Properties will have a DependencyProperty property which will give us the properties. However, the value of the property is not so easy to get. If we use GetValue, any binding or markup extensions will be evaluated and probably give wrong results (since the control has not been initialized yet). Using ReadLocalValue brings us pretty close, only that for certain objects like Binding we don't get a BindingBase object, but a BindingExpressionBase. We need to cast the value we get to BindingExpressionBase and TemplateBindingExpression and get to the underlying binding object.

Now that we've got the properties and the correct values, we use the factory SetValue method to set it. A special case is Name which must be set directly to the Name property. We use AppendChild to add a factory to a parent factory.

The last step is to get the Triggers from the original template and copy it in the new one. Now Seal it and you have yourself a clone. Not sure how one would manipulate the template to get a usable and maintainable template manipulation, but this is how you start.

I know you are suckers for code, so here it is:

Update: Actually the collapsed code below doesn't work except for the simplest of templates. There are several reasons for it and I will explore them below.
Click to expand


The first problem I found was dependency properties registered as read only that could only be set from XAML, like VisualStateManager.VisualStateGroups. When trying to use the FrameworkElementFactory SetValue method it would throw an error. Funny enough, the only reason that happened is because said method is checking if the property is read only and throws an exception. I had to use reflection to circumvent this, and it worked, albeit really ugly.

The second problem was more basic. Not every property is a dependency property. Such a simple property is Grid.ColumnDefinitions! Not only it is not a dependency property, but it is also read only. So I had to find another mechanism to fix this. At this point you probably realise this method is not a good one to employ, but if you are really desperate (or stubborn, like me) there is a way. The solution I found is to save all the properties that I need to set into a list and then set them in a RoutedEventHandler invoked from the Loaded event!

And if this is not enough, simply setting the value from the template in the control doesn't always work. In the generated template control the ColumnDefinition objects are already in the ColumnDefinitionCollection of the control. Adding them to a control that the factory generates results in an error. What I did here is a simple value=XamlReader.Parse(XamlWriter.Save(value)).

In other cases, like the Border.Child property, it must be completely ignored! So a list of properties to be ignored is needed.

Conclusion: Some improvements have been done in the code, but it's a little larger than before. The complicated way in which this works makes it cumbersome to be used, and I would not recommend it, but it works and it has extension points where errors with properties can be handled. Here is the new code:

#region Using directives

using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Markup;

#endregion

namespace BestPractices
{
/// <summary>
/// Base class for ControlTemplate transforming classes
/// </summary>
public abstract class BaseTemplateTransformer : BaseControlTransformer
{
#region Public Methods

/// <summary>
/// Clones the ControlTemplate of a control and allows for its manipulation
/// </summary>
/// <param name="control"></param>
public override void Transform(Control control)
{
ControlTemplate template = control.Template;
if (template == null)
{
return;
}
// create new template
ControlTemplate newTemplate = new ControlTemplate(template.TargetType);
// copy the resources
foreach (object key in template.Resources.Keys)
{
newTemplate.Resources.Add(key, template.Resources[key]);
}
//get the VisualTree factory from the original template content
DependencyObject content = template.LoadContent();
newTemplate.VisualTree = OnGetElementFactory(content);
// copy the triggers
foreach (TriggerBase trigger in template.Triggers)
{
newTemplate.Triggers.Add(trigger);
}
// allow for template manipulation
OnBeforeSeal(newTemplate);
// seal the template and set it back
newTemplate.Seal();
control.Template = newTemplate;
}

/// <summary>
/// Creates a custom ControlTransformFactory for the content object.
/// Override in order to replace elements in the initial template.
/// </summary>
/// <param name="content"></param>
/// <returns></returns>
public virtual ControlTransformFactory OnGetElementFactory(DependencyObject content)
{
if (content == null)
{
return null;
}
// use the object type
ControlTransformFactory factory = new ControlTransformFactory(content, this);
return factory;
}

/// <summary>
/// Returns a safe value for setting on the control
/// </summary>
/// <param name="item">The value from the template</param>
/// <returns></returns>
public virtual object GetSafeValue(object item)
{
return getSafeValue((dynamic) item);
}

/// <summary>
/// Returns true if a property needs to be saved and set when the control loads.
/// Defaults to false, except for ColumnDefinitions and RowDefinitions
/// </summary>
/// <param name="propertyDescriptor"></param>
/// <returns></returns>
public virtual bool MustSetProperty(PropertyDescriptor propertyDescriptor)
{
return sMustSetProperties.Contains(propertyDescriptor.Name);
}

#endregion

#region Protected Methods

/// <summary>
/// Allows for the manipulation of a control template
/// </summary>
/// <param name="newTemplate"></param>
protected virtual void OnBeforeSeal(ControlTemplate newTemplate)
{
}

#endregion

#region Statics

private static readonly List<string> sMustSetProperties = new List<string>
{
"ColumnDefinitions",
"RowDefinitions"
};

/// <summary>
/// Transforms a DependencyObject tree into a FrameworkElementFactory tree
/// </summary>
/// <param name="content"></param>
/// <returns></returns>
public static ControlTransformFactory CreateElementFactory(DependencyObject content)
{
if (content == null)
{
return null;
}
// use the object type
ControlTransformFactory factory = new ControlTransformFactory(content);
return factory;
}

/// <summary>
/// default return the same value
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
private static object getSafeValue(object value)
{
return value;
}

/// <summary>
/// try to clone ColumnDefinition objects
/// </summary>
/// <param name="columnDefinition"></param>
/// <returns></returns>
private static object getSafeValue(ColumnDefinition columnDefinition)
{
return clone(columnDefinition);
}

/// <summary>
/// try to clone RowDefinition objects
/// </summary>
/// <param name="rowDefinition"></param>
/// <returns></returns>
private static object getSafeValue(RowDefinition rowDefinition)
{
return clone(rowDefinition);
}

/// <summary>
/// Clones an item via Xaml write/parse
/// </summary>
/// <param name="element"></param>
/// <returns></returns>
private static object clone(object element)
{
string str = XamlWriter.Save(element);
return XamlReader.Parse(str);
}

#endregion
}
}

#region Using directives

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
using System.Windows;
using System.Windows.Markup.Primitives;
using System.Windows.Media;

#endregion

namespace BestPractices
{
/// <summary>
/// FrameworkElementFactory used in control template transformers
/// </summary>
public class ControlTransformFactory : FrameworkElementFactory
{
#region Nested

internal class NoTemplateTransformer : BaseTemplateTransformer
{
}

#endregion

#region Instance fields

private readonly BaseTemplateTransformer mTemplateTransformer;

private RoutedEventHandler mLoadHandler;
private List<MarkupProperty> mSimpleProperties;

#endregion

#region Properties

/// <summary>
/// List of non dependency properties that will be set when the control loads
/// </summary>
protected List<MarkupProperty> SimpleProperties
{
get
{
if (mSimpleProperties == null)
{
mSimpleProperties = new List<MarkupProperty>();
}
return mSimpleProperties;
}
}

#endregion

#region Constructors

public ControlTransformFactory(DependencyObject content,
BaseTemplateTransformer templateTransformer = null)
: base(content.GetType())
{
mTemplateTransformer = templateTransformer ?? new NoTemplateTransformer();
// set its name
string name = content.GetValue(FrameworkElement.NameProperty) as string;
if (!string.IsNullOrWhiteSpace(name))
{
Name = name;
}
// copy the properties
foreach (MarkupProperty propertyItem in getProperties(content))
{
SetProperty(propertyItem);
}
// do it recursively
int count = VisualTreeHelper.GetChildrenCount(content);
for (int i = 0; i < count; i++)
{
DependencyObject child = VisualTreeHelper.GetChild(content, i);
AppendChild(mTemplateTransformer.OnGetElementFactory(child));
}
}

#endregion

#region Public Methods

/// <summary>
/// SetValue that uses reflection to force DependencyProperties that were registered as read only
/// </summary>
/// <param name="dependencyProperty"></param>
/// <param name="value"></param>
/// <param name="forceSetReadOnly"></param>
public void SetValue(DependencyProperty dependencyProperty, object value,
bool forceSetReadOnly = false)
{
if (!forceSetReadOnly)
{
base.SetValue(dependencyProperty, value);
}
else
{
forceSetValue(dependencyProperty, value);
}
}

/// <summary>
/// Sets a value from a MarkupProperty object.
/// Dependency properties will be set via SetValue and the others via a Loaded handler on the object
/// </summary>
/// <param name="propertyItem"></param>
public void SetProperty(MarkupProperty propertyItem)
{
DependencyProperty property = propertyItem.DependencyProperty;
if (property == null)
{
setSimpleProperty(propertyItem);
}
else
{
object value = propertyItem.Value;
if (value == DependencyProperty.UnsetValue)
{
return;
}
SetValue(property, value, property.ReadOnly);
}
}

#endregion

#region Private Methods

/// <summary>
/// Force set value in the factory, even if the property is ReadOnly
/// </summary>
/// <param name="dp"></param>
/// <param name="value"></param>
private void forceSetValue(DependencyProperty dp, object value)
{
object resourceKey = getResourceKey(value);
if (resourceKey == null)
{
updatePropertyValueList(dp, value is TemplateBindingExtension
? "TemplateBinding"
: "Set", value);
}
else
{
updatePropertyValueList(dp, "Resource", value);
}
}

/// <summary>
/// Invoke the private UpdatePropertyValueList on the factory
/// </summary>
/// <param name="dp"></param>
/// <param name="propertyValueTypeName"></param>
/// <param name="value"></param>
private void updatePropertyValueList(DependencyProperty dp,
string propertyValueTypeName, object value)
{
object propertyValueType = Enum.Parse(sPropertyValueTypeType, propertyValueTypeName);
sUpdatePropertyValueListProperty.Invoke(this, new[] {dp, propertyValueType, value});
}

/// <summary>
/// Set a property to have its value set at load time
/// </summary>
/// <param name="markupProperty"></param>
private void setSimpleProperty(MarkupProperty markupProperty)
{
if (markupProperty.PropertyDescriptor == null)
{
return;
}
if (!mTemplateTransformer.MustSetProperty(markupProperty.PropertyDescriptor))
{
return;
}
SimpleProperties.Add(markupProperty);
if (mLoadHandler == null)
{
mLoadHandler = new RoutedEventHandler(simplePropertyHandler);
AddHandler(FrameworkElement.LoadedEvent, mLoadHandler);
}
}

/// <summary>
/// Handler on the Loaded event of the control
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
private void simplePropertyHandler(object sender, RoutedEventArgs args)
{
foreach (MarkupProperty propertyItem in SimpleProperties)
{
PropertyDescriptor propertyDescriptor = propertyItem.PropertyDescriptor;
if (propertyDescriptor == null || propertyItem.Value == null)
{
continue;
}
if (propertyDescriptor.IsReadOnly)
{
IList list = propertyItem.Value as IList;
if (list != null)
{
IList destinationList = propertyDescriptor.GetValue(sender) as IList;
if (destinationList != null)
{
foreach (object item in list)
{
destinationList.Add(mTemplateTransformer.GetSafeValue(item));
}
}
else
{
//shouldn't happend
}
}
else
{
// what now?
}
}
else
{
propertyDescriptor.SetValue(sender, mTemplateTransformer.GetSafeValue(propertyItem.Value));
}
}
FrameworkElement element = (FrameworkElement) sender;
element.RemoveHandler(FrameworkElement.LoadedEvent, mLoadHandler);
}

#endregion

#region Statics

static ControlTransformFactory()
{
// Get the types and methods that will be used in Reflection scenarios
sUpdatePropertyValueListProperty =
typeof (FrameworkElementFactory).GetMethod("UpdatePropertyValueList",
BindingFlags.NonPublic | BindingFlags.Instance);
sPropertyValueTypeType =
typeof (FrameworkElementFactory).Assembly.GetType("System.Windows.PropertyValueType");
}

private static readonly MethodInfo sUpdatePropertyValueListProperty;
private static readonly Type sPropertyValueTypeType;

/// <summary>
/// get the properties set on a DependencyObject
/// </summary>
/// <param name="content"></param>
/// <returns></returns>
private static IEnumerable<MarkupProperty> getProperties(DependencyObject content)
{
MarkupObject markupObject = MarkupWriter.GetMarkupObjectFor(content);
return markupObject.Properties;
}

/// <summary>
/// Get the ResourceKey property from an object, if it exists
/// </summary>
/// <param name="target"></param>
/// <returns></returns>
private static object getResourceKey(object target)
{
if (target == null)
{
return null;
}
Type type = target.GetType();
PropertyInfo property = type.GetProperty("ResourceKey");
if (property == null)
{
return null;
}
return property.GetValue(target, new object[] {});
}

#endregion
}
}

2 comments:

icefront said...

Great post.
This was exactly what i was looking for.

I am trying to create a Custom Control which can manipulate its childs ControlTemplate. (basically, to enhance a third party control).

First, i learned about the XamlWriter limitations. Then i found a post claiming that it is possible to create xaml with binding expressions, but it fails it certain more complicated templates (like ItemsControl ItemsSource property).

I will try your solution, and see if can get it to work.

thanks again :)

Siderite said...

You are most welcome. The problems I couldn't get around were the ones related to normal properties, non-dependency ones.

As I said in the post, I wouldn't recommend this option. It would be interesting to explore the conversion of a template to XAML, perform some string magic, then create it back.