XAML Playground
about XAML and other Amenities

Hierarchical binding with Implicit Data Templates

2012-01-12T23:52:41+01:00 by codeblock

Implicit data templates are a new interesting feature of Silverlight 5.0. They are a new way to create templates that automatically applies to specific types. So, imagine you have a set of types defined, and you want to present a collection of them inside an ItemsControl, you can use an Implicit Template that apply to each one of the types you are binding. Automatically this template will be applied to the give type every time it appear in the collection.

As you can figure out if may become really useful to present every item in a different way. Let say you have an Event base class and two derived Alarm and Information. You can bind the ItemsControl to an ObservableCollection<Event> as usual. Then you specify a different template for each one of the derived types:

   1: <ItemsControl ItemsSource="{Binding Events}">
   2:     <ItemsControl.Resources>
   3:         <DataTemplate DataType="om:Alarm">
   4:             <TextBlock Text="{Binding Message}" Foreground="Red" />
   5:         </DataTemplate>
   6:         <DataTemplate DataType="om:Information">
   7:             <TextBlock Text="{Binding Message}" Foreground="Black" />
   8:         </DataTemplate>
   9:     </ItemsControl.Resources>
  10: </ItemsControl>

Running the example you will get Alarm instances written in Red and Information instances written in black. The example is trivial just because I'm pretty sure you already read about Implicit Data Templates. The interesting thing to observe in this example i that you put templates in a Resources Section. This section apply to each one of the instances created inside of the ItemsControl.

After thinking a lot about this detail I realized you can use Implict Templates to bind Hierarchical structures and present each node with a different aspect based on the type ot the node. So I've taken the most obvious hierarchical structure - the filesystem - and I created a WCF method on top of it:

   1: public class FileSystemService : IFileSystemService
   2: {
   3:     public const string Root = @"C:\";
   5:     public IEnumerable<FileSystemItem> GetItems(string path)
   6:     {
   7:         List<FileSystemItem> items = new List<FileSystemItem>();
   9:         DirectoryInfo directory = new DirectoryInfo(Root + path);
  11:         foreach (var item in directory.GetDirectories())
  12:         {
  13:             try
  14:             {
  15:                 items.Add(new Folder
  16:                 {
  17:                     Name = item.Name,
  18:                     HasChildren = item.GetFiles().Count() + item.GetDirectories().Count() > 0
  19:                 });
  20:             }
  21:             catch(UnauthorizedAccessException)
  22:             { }
  23:         }
  25:         foreach (var item in directory.GetFiles())
  26:         {
  27:             items.Add(new File
  28:             {
  29:                 Name = item.Name,
  30:                 Size = item.Length
  31:             });
  32:         }
  34:         return items;
  35:     }
  36: }

Two words about this code. It is a method that accept a string representing the path to retrieve. The method simply points to the specified path and enumerates Folders and Files adding a few information specific to the type. Size for the file and HasChildren for the Folder.

After this, the service is ready so it's time to create the proxy on the Silverlight project and then start creating the front end. For the best result I used the MVVM pattern and create a page with its ViewModel then a ViewModel for each of the types returned by the service.

   1: public class FileViewModel : FileSystemItemViewModel<File>
   2: { }
   4: public class FolderViewModel : FileSystemItemViewModel<Folder>
   5: { }

At the first sight it may seems you can use directly the types that comes from the service but usig a ViewModel is the best choice because I have to get commands from the nodes and I need to lazy load children items. So indise the MainPage view model I start the chain loading the first level to present in the hierarchy:

   1: public class MainPageViewModel : ViewModelBase
   2: {
   3:     public ObservableCollection<ViewModelBase> Items { get; set; }
   5:     public MainPageViewModel()
   6:     {
   7:         this.Items = new ObservableCollection<ViewModelBase>();
   8:         this.Load();
   9:     }
  11:     private void Load()
  12:     {
  13:         FileSystemServiceClient client = new FileSystemServiceClient();
  14:         client.GetItemsCompleted += new EventHandler<GetItemsCompletedEventArgs>(client_GetItemsCompleted);
  15:         client.GetItemsAsync(string.Empty);
  16:     }
  18:     private void client_GetItemsCompleted(object sender, GetItemsCompletedEventArgs e)
  19:     {
  20:         this.Items.Clear();
  22:         foreach (var item in e.Result)
  23:         {
  24:             if (item is File)
  25:                 this.Items.Add(new FileViewModel(item as File));
  26:             else if (item is Folder)
  27:                 this.Items.Add(new FolderViewModel(item as Folder, string.Empty));
  28:         }
  29:     }
  30: }

Since the File will not have any action in this example, but in an extended example it may trigger the download of the file. For the purpose of the article I will concentrate on the FolderViewModel. When the item is clicked I expect that the children is loaded and presented in the UI. So here is the code:

   1: public class FolderViewModel : FileSystemItemViewModel<Folder>
   2: {
   3:     public FolderViewModel(Folder folder, string path)
   4:         : base(folder)
   5:     {
   6:         this.Path = path + "/" + folder.Name;
   7:         this.Items = new ObservableCollection<ViewModelBase>();
   8:         this.ClickCommand = new RelayCommand(Click);
   9:     }
  11:     private void Click()
  12:     {
  13:         if (this.Items.Count == 0)
  14:         {
  15:             FileSystemServiceClient client = new FileSystemServiceClient();
  16:             client.GetItemsCompleted += new EventHandler<GetItemsCompletedEventArgs>(client_GetItemsCompleted);
  17:             client.GetItemsAsync(this.Path);
  18:         }
  19:         else
  20:             this.IsExpanded = !this.IsExpanded;
  21:     }
  23:     private void client_GetItemsCompleted(object sender, GetItemsCompletedEventArgs e)
  24:     {
  25:         this.Items.Clear();
  27:         foreach (var item in e.Result)
  28:         {
  29:             if (item is File)
  30:                 this.Items.Add(new FileViewModel(item as File));
  31:             else if (item is Folder)
  32:                 this.Items.Add(new FolderViewModel(item as Folder, this.Path));
  33:         }
  35:         this.IsExpanded = true;
  36:     }
  38:     // here starts the Binding properties...
  39: }

In this snipped I omitted the Binding properties but remember that IsExpanded, Items and ClickCommand are respectively a boolean that indicated if the node is open or not, a collection of the child nodes and the command the user triggers when he click the node. When this happens, the service is called, and the returned children are loaded into the Items properties. I think the logic is really straightforward.

Finally it is time to watch the XAML code. Using two Implicit Templates, It is really simple to create a hierarchical structure starting from a simple ItemsControl:

   1: <Grid.Resources>
   3:     <DataTemplate DataType="vm:FolderViewModel">
   4:         <StackPanel>
   5:             <StackPanel Orientation="Horizontal" Margin="2">
   6:                 <Image Source="/Images/folder.png" Width="16" Height="16" />
   7:                 <HyperlinkButton IsEnabled="{Binding Item.HasChildren}" 
   8:                                  Command="{Binding ClickCommand}" 
   9:                                  Content="{Binding Item.Name}" 
  10:                                  Foreground="Black" />
  11:             </StackPanel>
  12:             <ItemsControl Visibility="{Binding IsExpanded, Converter={StaticResource b2v}}" 
  13:                           Margin="20,0,0,0" ItemsSource="{Binding Items}" />
  14:         </StackPanel>
  15:     </DataTemplate>
  17:     <DataTemplate DataType="vm:FileViewModel">
  18:         <StackPanel Orientation="Horizontal" Margin="2">
  19:             <Image Source="/Images/file.png" Width="16" Height="16" />
  20:             <TextBlock><Run Text="{Binding Item.Name}" />(<Run Text="{Binding Item.Size}" /> Kb)</TextBlock>
  21:         </StackPanel>
  22:     </DataTemplate>
  24: </Grid.Resources>
  26: <ScrollViewer>
  27:     <ItemsControl ItemsSource="{Binding Items}" />
  28: </ScrollViewer>

The DataTamplate relative to the FileViewModel simply presents an image and the TextBlock for the name. The FolderViewModel instead refers to a DataTemplate containing another ItemsControl binded the the Items property. imageThanks to the propagation of the resources this ItemsControl again is able to generate children based on the same templates. Te result is a simple treeview that opens a node when you click on it. The Margin property indents the nodes and improve the TreeView aspect.

The figure on the side shows the final aspect of the treeview, decorated with beautiful images. The best thing is that you can create a lot of different nodes on the basis of the type you add to the collection.

I have to confess, at the first time I've tryied to generate this using a TreeView and a HierarchicalDataTemplate but I was unable to manage to have it working. It seems the HierarchicalDataTemplate does not support the implicit data templates.

The sole trick is given by the Converter I used to change the boolean type to a Visibility property. This let me to use the IExpanded property to show or not the ItemsControl.

If you are intersted in the working sample you can download it here. The code is complete and assumes you bind the service to the C:\ volume.

Download: XPG.ImplicitDataBinding (479Kb)

Categories:   Databinding
Actions:   E-mail | del.icio.us | Permalink | Comments (0) | Comment RSSRSS comment feed

Pingbacks and trackbacks (1)+