Click here to Skip to main content
11,432,168 members (55,788 online)
Click here to Skip to main content

Create Data and Control Templates using Delegates

, 21 Aug 2014 CPOL
Rate this:
Please Sign up or sign in to vote.
Avoid generating strings as templates. Avoid the complicated FrameworkElementFactory. Create templates that use a delegate to create new instances, so you can use normal code to do the job.

Introduction

I don't know how many of you agree with me, but I think that no framework should force us to use a single string as input when more specific objects could be used and, in my opinion, data-templates fall in this category.

Actually we have two options to create data-templates. We can use the FrameworkElementFactory, which MSDN says to be deprecated, or we can load a data-template using a string or an XamlReader, which is considered the "right" way to do it.

Expected solution

Well, to me the right way to create a date-template in code should be a method like this:

DataTemplate.Create(Func<DependencyObject> factory);

So, if our template simply needs to create a TextBox, we will be able to create a Data-Template like this:

DataTemplate template = DataTemplate.Create(() => new TextBox());

Of course, as it will be code, we will be able to set properties, set-up bindings and even use context data (things like static or thread-static properties and methods). In fact, I think that such delegate should receive the owner control, so we can verify the actual control state before doing any action, yet the DataTemplate.LoadContent() method doesn't receive any parameters and that's why I decided the delegate should not receive any parameters either.

So, if we had data-templates that invoked a delegate to do the job we will be able to use any code during the LoadContent() call, being able to use ifs to choose which controls to generate, being able to use "context" variables and, in fact, this would even make data-template selectors useless, as a delegate will be able to choose any other existing data-template and call its LoadContent() method.

In my opinion, the only thing that we need to do this is to have a virtual LoadContent method. As it is not virtual, I will be happy to have something like a virtual OnLoadContent, which the LoadContent invokes, maybe validate the result, and finally return. Unfortunately, if we sub-class the DataTemplate class we have no useful methods to override. So, isn't there a way to create a template using a simple delegate?

The solution

I initially though it was impossible to have a solution as all the interesting methods to override in the DataTemplate class are internal, but actually there's a work-around. It is not 100% what I wanted, but it allows us to create a data-template that will invoke a delegate to do its job.

To do this, I use the FrameworkElementFactory (yeah, the "deprecated" class) to create a helper control. I can't make the FrameworkElementFactory execute code directly, but I can set a dependency property on the control it generates with any value I want, which in my case is the delegate I want to execute. And the dependency properties can be configured to execute code when they are set and so, I use the delegate that it receives to create the control and set as its inner control.

In the end, I have a data-template that executes my delegate to create the control I want when it is invoked. I don't like the fact that I actually don't end with the control I want directly, I receive a ContentControl that has the object created by my delegate as its Content.

Well, I said it is a work-around, not the perfect solution, and it doesn't affect the visual or the behavior of the generated control, so I will only worry if that extra control is really causing performance problems.

Why is the FrameworkElementFactory deprecated?

The MSDN documentation says that the FrameworkElementFactory is deprecated but doesn't give an explanation aside "not all of the template functionality is available when you create a template using this class".

When I did a research to better understand this, I found this page http://www.ikriv.com/dev/wpf/DataTemplateCreation/ in which the author shows a situation that causes a binding problem.

Well, even if my solution uses the FrameworkElementFactory, it doesn't share such problem. Maybe it is the moment in which the delegate is executed, maybe it is simply because it is normal code that does the object creation. In any case, it doesn't have such problem and I even changed that sample to show that this new solution works.

Using parent information to correctly create the control

The LoadContent() method doesn't receive the container control and I think this is pretty bad, as we can't extract information from the parent. Because of such limitation I made the delegate without parameters.

Yet, the delegate that creates the new control is free to post messages (call Dispatcher.BeginInvoke()) to the control it just created. When the Dispatcher executes such code, the control will usually be attached to its parent, so it will be possible to get that extra information at this moment, which is still before the control is shown in the screen, so there will be no flickering by doing this.

I am not saying this is the best thing to do, but I believe it is a valid work-around for the problem.

ControlTemplate

Well, the technique I used for data-templates works for control-templates too, so I decided to create a method for them too.

The Code

The entire code of this helper class is this:

/// <summary>
/// Class that helps the creation of control and data templates by using delegates.
/// </summary>
public static class TemplateGenerator
{
  private sealed class _TemplateGeneratorControl:
    ContentControl
  {
    internal static readonly DependencyProperty FactoryProperty = DependencyProperty.Register("Factory", typeof(Func<object>), typeof(_TemplateGeneratorControl), new PropertyMetadata(null, _FactoryChanged));

    private static void _FactoryChanged(DependencyObject instance, DependencyPropertyChangedEventArgs args)
    {
      var control = (_TemplateGeneratorControl)instance;
      var factory = (Func<object>)args.NewValue;
      control.Content = factory();
    }
  }

  /// <summary>
  /// Creates a data-template that uses the given delegate to create new instances.
  /// </summary>
  public static DataTemplate CreateDataTemplate(Func<object> factory)
  {
    if (factory == null)
      throw new ArgumentNullException("factory");

    var frameworkElementFactory = new FrameworkElementFactory(typeof(_TemplateGeneratorControl));
    frameworkElementFactory.SetValue(_TemplateGeneratorControl.FactoryProperty, factory);
      
    var dataTemplate = new DataTemplate(typeof(DependencyObject));
    dataTemplate.VisualTree = frameworkElementFactory;
    return dataTemplate;
  }

  /// <summary>
  /// Creates a control-template that uses the given delegate to create new instances.
  /// </summary>
  public static ControlTemplate CreateControlTemplate(Type controlType, Func<object> factory)
  {
    if (controlType == null)
      throw new ArgumentNullException("controlType");

    if (factory == null)
      throw new ArgumentNullException("factory");

    var frameworkElementFactory = new FrameworkElementFactory(typeof(_TemplateGeneratorControl));
    frameworkElementFactory.SetValue(_TemplateGeneratorControl.FactoryProperty, factory);
      
    var controlTemplate = new ControlTemplate(controlType);
    controlTemplate.VisualTree = frameworkElementFactory;
    return controlTemplate;
  }
}

And you can use it like this:

DataTemplate template = TemplateGenerator.CreateDataTemplate(() => new TextBox());

Or, if you want something a little more complex, you can create it with a binding, like this:

DataTemplate template = 
  TemplateGenerator.CreateDataTemplate
  (
    () =>
    {
      var result = new TextBox()
      result.SetBinding(TextBox.TextProperty, "BindingPathHere");
      return result;
    }
  );

You are free to use real methods instead of lambdas and to use any code you see fit to create the control, the same way you can do when you create it by code but outside of a data-template. In this case I considered the lambda as the best option, but for complex creations I will dedicate a method for that.

Sample

The sample application is a modified copy of the sample found in the page http://www.ikriv.com/dev/wpf/DataTemplateCreation/ that adds a fourth control in the main window, which uses the TemplateGenerator class to do its job.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Paulo Zemek
Engineer Microsoft Corporation
United States United States
I started to program computers when I was 11 years old, as a hobbist, programming in AMOS Basic and Blitz Basic for Amiga.
At 12 I had my first try with assembler, but it was too difficult at the time. Then, in the same year, I learned C and, after learning C, I was finally able to learn assembler (for Motorola 680x0).
Not sure, but probably between 12 and 13, I started to learn C++. I always programmed "in an object oriented way", but using function pointers instead of virtual methods.

At 15 I started to learn Pascal at school and to use Delphi. At 16 I started my first internship (using Delphi). At 18 I started to work professionally using C++ and since then I've developed my programming skills as a professional developer in C++ and C#, generally creating libraries that help other developers do they work easier, faster and with less errors.

Now I just started working as a Senior Software Engineer at Microsoft.

Want more info or simply want to contact me?
Take a look at: http://paulozemek.azurewebsites.net/
Or e-mail me at: paulozemek@outlook.com

Codeproject MVP 2012, 2015
Microsoft MVP 2013-2014

Comments and Discussions

 
Questioncould you show an example with.more meat Pin
Sacha Barber18-Aug-14 20:18
mvpSacha Barber18-Aug-14 20:18 
AnswerRe: could you show an example with.more meat Pin
Paulo Zemek19-Aug-14 3:27
professionalPaulo Zemek19-Aug-14 3:27 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.150428.2 | Last Updated 21 Aug 2014
Article Copyright 2014 by Paulo Zemek
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid