Binding to the TabControl’s ItemsSource (and more)

I couldn’t come up with a good title for this post but what I really want to cover is this…

I want to create view models and have a TabControl dynamically create the TabItems for the view models and then automatically assign/associate the corresponding view to the tab’s visual tree for each view model.

Often when we create a TabControl (by the way the concepts listed here work equally well for ItemsControl types), we usually declare everything in XAML, each TabItem is bound to our ViewModel, we supply the header etc. But what I require is a more dynamic way of creating the TabControl’s TabItems via the view model.

Let’s use an example. I’m going to create a TabControl with TabItems similar to the sections within an Outlook Contact details view. So we’ll have a tab for the Contact Details, the Internet Details, the Phone Numbers and the Addresses. In this example we will supply each of the relevant ViewModels via an ObservableCollection and bind this to the TabControls ItemsSource, but before that let’s see the example view models

public class ContactDetailsViewModel
{
   public static string Name
   {
      get { return "Contact Details"; }
   }

   public string Content
   {
      get { return "Contact Details Content"; }
   }
}

The other view models will all take the same form as this one (I’ll still list them for completeness). In these example view models we’ll assume the Content property may be one of many properties that the corresponding view will bind to. i.e. we’ll end up creating many properties which in turn will get bound to a view.

As we want the TabControl to dynamically create TabItems, we’ll also need to supply the TabItem Header via the view model (or some other mechanism), so this is where the Name property comes in.

Before we look at the “outer” view model that the TabControl itself binds to, I’ll list those other view models as mentioned previously

public class InternetViewModel
{
   public static string Name
   {
      get { return "Internet"; }
   }

   public string Content
   {
      get { return "Internet Content"; }
   }
}

public class PhoneNumbersViewModel
{
   public static string Name
   {
      get { return "Phone Numbers"; }
   }

   public string Content
   {
      get { return "Phone Numbers Content"; }
   }
}

public class AddressesViewModel
{
   public static string Name
   {
      get { return "Addresses"; }
   }

   public string Content
   {
      get { return "Addresses Content"; }
   }		
}

Now let’s look at the view model which supplies the TabControl with the ItemsSource data, i.e. it encapsulates the child view models.

public class ContactViewModel
{
   public ContactViewModel()
   {
      Details = new ObservableCollection<object>
      {
         new ContactDetailsViewModel(),
         new InternetViewModel(),
         new PhoneNumbersViewModel(),
         new AddressesViewModel()
      };
   }

   public ObservableCollection<object> Details { get; private set; }
}

As you can see, we create a collection of the view models which will be used to populate the ItemsSource on the TabControl.

Let’s take a look at how we might use this view model within the view (we’ll assume the view’s DataContext has been assigned a ContactViewModel instance by whatever means you like). So this first example of our view is very basic mainly to demonstrate the ItemsSource binding, this will simply display the correct number of TabItems and set the header to the Name property from our view models

<TabControl ItemsSource="{Binding Details}">
   <TabControl.ItemContainerStyle>
      <Style TargetType="{x:Type TabItem}">
         <Setter Property="Header" Value="{Binding Name}" />
      </Style>
   </TabControl.ItemContainerStyle>
</TabControl>

Pretty minimalist, but it’s a good starting point. If you were to run the code thus far, you’ll notice that initially no tab is selected, so we could add a SelectedIndex or SelectedItem property to the TabControl and the relevant binding and view model properties to enable the selected item to be set and tracked. But for now, just click on a tab yourself. When you select a tab you’ll notice the TabItem’s content is the TypeName of the selected view model. This is good as it tells us our view model’s are connected to the TabItems, but ofcourse it’s of no use in our end application, so let’s create four UserControls, named ContactDetailsView, InternetView, PhoneNumbersView and AddessesView and simply give each of them the following

<Grid>
   <TextBlock Text="{Binding Content}" />
</Grid>

Again, in our real world app. each view would differ as would the view model’s properties, but hopefully you get the idea.

We now need to associate the view with the selected view model, a simple way to do this is as follows. Add the following to the TabControl code that we wrote earlier

<TabControl.Resources>
   <DataTemplate DataType="{x:Type tabControlViewModel:ContactDetailsViewModel}">
      <tabControlViewModel:ContactDetailsView />
   </DataTemplate>
   <DataTemplate DataType="{x:Type tabControlViewModel:InternetViewModel}">
      <tabControlViewModel:InternetView/>
   </DataTemplate>
   <DataTemplate DataType="{x:Type tabControlViewModel:PhoneNumbersViewModel}">
      <tabControlViewModel:ContactDetailsView/>
   </DataTemplate>
   <DataTemplate DataType="{x:Type tabControlViewModel:AddressesViewModel}">
      <tabControlViewModel:ContactDetailsView/>
   </DataTemplate>
</TabControl.Resources>

Now if you run the code each selected TabItem will display the corresponding view for the selected view model, using the DataTemplate to select the correct view. If you’re not convinced, for each of the views, change the TextBlock’s ForeGround colour to see that each view is different and thus a different view is display for each view model.

This is a massive step forward and we could stop at this point and be happy but…

Taking it a stage further

Note: This has not been thoroughly tested so use at your own discretion

Using the resources and DataTemplates is a good solution, but I’d rather like something like Caliburn Micro etc. where a view locator creates the view automatically for me. So let’s have a go at producing something like this.

First off our TabControl now looks like this

<TabControl ItemsSource="{Binding Details}" 
      ContentTemplateSelector="{StaticResource Selector}">
   <TabControl.ItemContainerStyle>
      <Style TargetType="{x:Type TabItem}">
         <Setter Property="Header" Value="{Binding Name}" />
      </Style>
   </TabControl.ItemContainerStyle>
</TabControl>

The change from our first version is the ContentTemplateSelector. Obviously we’ll need to add the following to the Resource section of our Window or UserControl

   <tabControlViewModel:ViewTemplateSelector x:Key="Selector"/>

Now let’s write the ViewTemplateSelector. Basically the ViewTemplateSelector is derived from the DataTemplateSelector and what will happen is – when a tab is selected, the ContentTemplateSelector will be used to call our ViewTemplateSelector’s SelectTemplate method. Our ViewTemplateSelector will handle the calls to SelectTemplate and try to locate a view in the same assembly as the view model currently associated with a tab. We try to find a view with matches the name of the view model type but without the Model suffix. So for example for our InternetViewModel, we’ll try to find an InternetView type in the same assembly as the view model. We’ll then create an instance of this and attach to a DataTemplate before returning this to the TabControl.

One thing we need to be aware of is that unless we wish to instantiate a new instance of the view every time an item is selected, we’ll need to cache the data templates.

Anyway here’s the code as I currently have it

public class ViewTemplateSelector : DataTemplateSelector
{
   private readonly Dictionary<string, DataTemplate> dataTemplates = 
                 new Dictionary<string, DataTemplate>();

   public override DataTemplate SelectTemplate(object item, DependencyObject container)
   {
      var contentPresent = container as ContentPresenter;
      if (contentPresent != null)
      {
         const string VIEWMODEL = "ViewModel";
         const string MODEL = "Model";

         if (item != null)
         {
            var type = item.GetType();
            var name = type.Name;
            if (name.EndsWith(VIEWMODEL))
            {
               name = name.Substring(0, name.Length - MODEL.Length);
               if (dataTemplates.ContainsKey(name))
                  return dataTemplates[name];

               var match = type.Assembly.GetTypes().
                      FirstOrDefault(t => t.Name == name);
               if (match != null)
               {
                  var view = Activator.CreateInstance(match) as DependencyObject;
                  if (view != null)
                  {
                     var factory = new FrameworkElementFactory(match);
                     var dataTemplate = new DataTemplate(type)
                     {
                        VisualTree = factory
                     };
                     dataTemplates.Add(name, dataTemplate);
                     return dataTemplate;
                  }
               }
            }
         }
      }
      return base.SelectTemplate(item, container);
   }
}

Now we should have a TabControl whose TabItems are generated based upon the ItemsSource collection of view models. A view is now created based upon the view name and automatically inserted into corresponding TabItem.