Wednesday, April 7, 2010

Changing Data-Templates at run-time from the VM

[Updated – Fixed code samples]
What do you do when you have a list box or data grid that has a collection of items to display - but some items need to be presented differently than others? For example, I may have a collection of company staff members to display in a single ListBox, but the managerial staff need to have extra information displayed (they always do!), and each level is styled slightly differently like this for example:


In this post I'll describe a technique I have used successfully before - I'll stick with the list of staff example above to illustrate the solution, but the class types are going to be a little contrived for the sake of simplicity.

The Data
Lets define a StaffMember class like this:
   1: public enum StaffRoleType { Manager, MiddleManager, LowerManager, Pleb, Contractor }
   2:  
   3: public enum CoffeeType { Espresso, Cappuccino, HotChocolate, Other }
   4:  
   5: public class StaffMemeber  
   6: {
   7:     public string Title { get; set; }
   8:     public string FirstName { get; set; }
   9:     public string LastName { get; set; }
  10:     public StaffRoleType StaffRole { get; set; }
  11:     public CoffeeType CoffeeType { get; set; }
  12:     public Color FavouriteColor { get; set; }
  13:     public string CarType { get; set; }
  14: }

The StaffRoleType will be used to differentiate the managers from the plebs so we can display the really important information for managers like what kind of coffee they drink and what their favourite color is. If this was real code we might have a different class for each role type, and they would all implement the INotifyPropertyChanged interface. But that class will do for demonstration purposes.


Different Data = Different Data Templates

A nice way to solve this problem would be if we could somehow use different data templates to display each type of staff- but do this inside the existing controls like a ListBox. Ideally we could use Blend to design these different data types too. Of course, the ListBox doesn't let us define more than one data template for the ListBoxItem; but rather than try and rewrite the ListBox, we can achieve our goals with a couple of simple additions to the ContentControl:




   1: using System.Collections.ObjectModel;
   2: using System.Linq;
   3: using System.Windows;
   4: using System.Windows.Controls;
   5: public class SelectableContentControl : ContentControl
   6: {
   7:   public static readonly DependencyProperty TemplateNameProperty = DependencyProperty.Register(
   8:      "TemplateName",
   9:      typeof(string),
  10:      typeof(SelectableContentControl),
  11:      new PropertyMetadata(string.Empty, TemplateNameChanged));
  12:   
  13:   private readonly ObservableCollection<DataTemplate> templateCollection = new ObservableCollection<DataTemplate>();
  14:   
  15:   /// <summary>
  16:   /// Gets the collection of templates
  17:   /// </summary>
  18:   public ObservableCollection<DataTemplate> Templates
  19:   {
  20:      get
  21:      {
  22:          return this.templateCollection;
  23:      }
  24:   }
  25:   
  26:   /// <summary>
  27:   /// Gets or sets the name of the template to use
  28:   /// </summary>
  29:   public string TemplateName
  30:   {
  31:      get
  32:      {
  33:          return (string)GetValue(TemplateNameProperty);
  34:      }
  35:   
  36:      set
  37:      {
  38:          SetValue(TemplateNameProperty, value);
  39:      }
  40:   }
  41:   
  42:   /// <summary>
  43:   /// Select the appropriate DataTemplate when the Content changes.
  44:   /// </summary>
  45:   /// <param name="oldContent">The old Content value.</param>
  46:   /// <param name="newContent">The new Content value.</param>
  47:   protected override void OnContentChanged(object oldContent, object newContent)
  48:   {
  49:      base.OnContentChanged(oldContent, newContent);
  50:      this.SelectTemplate();
  51:   }
  52:   
  53:   private static void TemplateNameChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  54:   {
  55:      ((SelectableContentControl)d).SelectTemplate();
  56:   }
  57:   
  58:   private void SelectTemplate()
  59:   {
  60:      if (!string.IsNullOrEmpty(this.TemplateName))
  61:      {
  62:          DataTemplate namedTemplate =
  63:              this.Templates.FirstOrDefault(
  64:                  t => t.GetValue(FrameworkElement.NameProperty).Equals(this.TemplateName));
  65:          if (null != namedTemplate)
  66:          {
  67:              this.ContentTemplate = namedTemplate;
  68:              return;
  69:          }
  70:      }
  71:   
  72:      // default to the first template
  73:      if (this.Templates.Count > 0)
  74:      {
  75:          this.ContentTemplate = this.Templates[0];
  76:      }
  77:   }
  78: }

 

The new control has two key properties:



  • Templates - an ObservableCollection of DataTemplate
  • TemplateName - a string representing the name of the template to choose.
The Templates property allows us to declare multiple DataTemplates in the XAML, and the TemplateName property allows us to bind to a property on the data to select which template to use. If we had multiple class types in our list, such as a Manager class etc, then we could bind on the ClassName, or even the class itself with a value converter.



This is what the XAML for a ListBox may look like:


   1: <ListBox ItemsSource="{Binding StaffList}" >
   2:     <ListBox.ItemTemplate>
   3:         <ss:SelectableContentControl TemplateName="{Binding StaffRole}" Content="{Binding}">
   4:             <ss:SelectableContentControl.Templates>
   5:  
   6:                 <!-- default for unmatched StaffRoleType values -->
   7:                 <DataTemplate>
   8:                     <local:PlebianUC />
   9:                 </DataTemplate>
  10:  
  11:                 <!-- Contractor template -->
  12:                 <DataTemplate x:Name="Contractor">
  13:                     <local:ContractorUC />
  14:                 </DataTemplate>
  15:  
  16:                 <!-- Contractor template -->
  17:                 <DataTemplate x:Name="MiddleManager">
  18:                     <local:MiddleManagerUC />
  19:                 </DataTemplate>
  20:  
  21:                 <!-- Contractor template -->
  22:                 <DataTemplate x:Name="Manager">
  23:                     <local:ManagerUC />
  24:                 </DataTemplate>
  25:             </ss:SelectableContentControl.Templates>
  26:         </ss:SelectableContentControl>
  27:     </ListBox.ItemTemplate>
  28: </ListBox>



So what's going on here?



The ListBox has it's ItemsSource bound to a property on the DataContext called StaffList, which contains a collection of StaffMember instances. The ListBox.ItemTemplate contains a single SelectableContentControl with its TemplateName property set to "StaffRole" and it's Content bound to the DataContext of the ListBoxItem. At run time when a DataTemplate is being created for each item in the listbox and the item is databound to it, the SelectableContentControl will read the StaffRole property and find a template with the same name as the StaffRole value. The first DataTemplate in the Templates collection will be used for any data items that have a StaffRole value that doesn't match a Template name. We don't even need a value converter in this example, since the StaffRole property will convert to a string without one.



In order to make this technique friendly to Blend you need to create a UserControl for each of the DataTemplates so that the content can be styled in Blend. If you are more comfortable writing XAML than using Blend then you can skip the UserControls and put the content directly in the DataTemplates. Either way, you will still need to specify the DataTemplate collection by hand in XAML since Blend doesn't provide and easy way to edit a collection of DataTemplate instances (at least that I know of)



Feel free to copy the code above and use it in your own projects. Let me know if you find it useful.

18 comments:

  1. Wow !!! Brilliant !!! Thx for Sharing !!!

    ReplyDelete
  2. Your welcome! Thanks for the feedback.

    ReplyDelete
  3. I made VERY similar myself but called it DataTemplateContentControl. I used the default DataTemplateSelector to select the template rather than built in (more WPF like). In any case it seems that many of us are addressing this glaring omission from SL (WPF has functionality for this built in)

    ReplyDelete
  4. What does "ss" stands for??

    ReplyDelete
  5. the code was messed up when I changed blog-templates. I'm going to re-post it with the code fixed up.

    ReplyDelete
  6. I'm having an issue with this code when I try to element bind back into the main page from one of my data templates.

    However if I just copy and paste the code for my datatemplate into it's datatemplate tag my element binding works fine.

    Ideas?

    ReplyDelete
  7. Please re-post the complete code as soon as possible. i will be waiting for it.
    Thanx a lot.

    ReplyDelete
  8. Code fixed now. "ss" refers to the xmlns name for the namespace that the SelectableContentControl was compiled into. You can change this to match whatever namespace you place the class into.

    testblogbrett: I have never tried element binding back into the main page with this code. That seems a fairly unusual thing to do inside a data template, but nevertheless I don't know why it won't do that for you. Try putting the information on the data object that the data template is binding to as its DataContext, then bind to that.

    ReplyDelete
  9. It is a bit unusual to be sure, and maybe I'm just approaching this problem in a slightly odd way.

    It's an MVVM project. I have a bunch of views that contain little more than a listbox. Each view can contain several different types of data ( hence why I really liked your solution ). Now the display of those datatypes is essentially a big button. You should be able to mouse over them and hilight them and click on them and cause things to happen. Easy enough, I'll just use the SelectedItem on the listbox and some styles.

    HOWEVER the designer would like to effect certain elements in the datatemplate. Something we can't seem to get to work quite right. The ItemContainerStyle can't target things internal to the data template.

    So we went the other way and contained the bulk of the datatemplate in a button so that we could get mouse events internal to the datatemplate. But now I can't "climb back up the well" to the VM to deal with the item selection.

    It's incredibly frustrating, i've been going around and around for days. I can't help but think that there is an easy way out. Thoughts?

    ReplyDelete
  10. Brett - do you have a sample project you can put up somewhere (or just send it to my gmail email - phil.middlemiss)? I'll take a look at it.

    ReplyDelete
  11. Great work! It helped me a lot, but I still have an issue when changing the TemplateName. I binded the Name to an Enumaration-Value similar to your example. But whenever I try to change the value in the code-behind the trigger doesn't seem to work and the SelectTemplate()-Function won't be called. Do you have any Ideas?

    ReplyDelete
  12. Sheena - hard to say without seeing what you have done. Double check the names of the templates inside the control; make sure they contain no spelling mistakes and are the same case as the enumeration.

    Installing the Silverlight 5 beta may be helpful here - it adds the ability to debug bindings in the XAML in Visual Studio and works with earlier versions of Silverlight.

    ReplyDelete
  13. Hello Phil, I checked the names - they are correct. But I try the "Silverlight 5 Thing". Maybe this will help...thanks for the advice!

    ReplyDelete
  14. Hey there! Just want to let you know, that I kind of solved the issue. I couldn't get the dynamic template changing to work, but I found a solution that fits my szenario (Take a look at http://blog.flexforcefive.com/?p=360). Maybe someday (with a litte bit more experience with Silverlight) I will figure out how to get your approach to work with a dynamic template switch, because I'm pretty sure that this approach also should do the trick. Have a nice day!

    ReplyDelete
  15. This comment has been removed by the author.

    ReplyDelete
  16. You've also missed off the DataTemplate tag below the ListBox.ItemTemplate tag

    ReplyDelete
  17. Also, if you have several StaffMemeberModel and some have the same StaffRoleType in the list that's bound to the List control you get an error....
    Error: Unhandled Error in Silverlight Application
    Code: 2028
    Category: ParserError
    Message: The name already exists in the tree: Manager.
    File:
    Line: 0
    Position: 0

    ReplyDelete
  18. Far better one...that works at

    http://www.codeproject.com/Articles/92439/Silverlight-DataTemplateSelector

    ReplyDelete