Sunday, November 6, 2011

WPF: TabControl and DataTemplate

This example show how to use DataTemplate to render TabControl pages from a collection of objects based on the data type of each object. This code can be used to create a simple tabbed shell for an MVVM application.

Model Views

The MainViewModel is the application's shell view model. It instantiates the tab page view models collection that shall be rendered by the TabControl on the MainView.

namespace TabControlDataTemplate
{
   public class MainViewModel
   {
      private TabPageViewModel[] pages;
 
      public TabPageViewModel[] Pages
      {
         get
         {
            if (this.pages == null)
            {
               this.pages = new TabPageViewModel[]
               {
                  new FooPageViewModel(), 
                  new BarPageViewModel() };
               }
            }
            return this.pages;
         }
      }
   }
}
Now we'll create an ancestor class for our tab page view models. I called it TabPageViewModel.
namespace TabControlDataTemplate
{
   public abstract class TabPageViewModel
   {
      public virtual string DisplayName { get; protected set; }
   }
}
There will be two sample model views, named FooPageModelView and BarPageModelView
namespace TabControlDataTemplate
{
   public class FooPageViewModel : TabPageViewModel
   {
      public string Message
      {
         get { return "FOO Content" }
      }
      public override string DisplayName
      {
         get { return "Foo Header"; }
      }
   }
}
namespace TabControlDataTemplate
{
   public class BarPageViewModel : TabPageViewModel
   {
      private string message;
       
      public BarPageViewModel()
      {
         this.message = "BAR Content";
      }
 
      public string Message
      {
         get { return this.message; }
      }
       
      public override string DisplayName
      {
         get { return "BAR Header"; }
         protected set { base.DisplayName = value; }
      }
   }
}

Page Views

The page views are very simple. There is only one TextArea which binds its Text property to the Message property of each view model.
<UserControl 
   HorizontlAlignment="Stretch" VerticalAlignment="Stretch"
   x:Class="TabControlDataTemplate.FooPageView"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
   <Grid>
      <TextBlock FontSize="16" ForeGround="Red" MinWidth="100" 
      Text="{Binding Path=Message}" />
   </Grid>
</UserControl>

<UserControl 
   HorizontlAlignment="Stretch" VerticalAlignment="Stretch"
   x:Class="TabControlDataTemplate.BarPageView"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
   <Grid>
      <TextBlock FontSize="16" ForeGround="Blue" MinWidth="100"
      Text="{Binding Path=Message}" />
   <Grid>
<UserControl>

Application

The application object is responsible for initializing the MainView and the MainViewModel and bind them by setting the DataContext property of the MainView to the MainViewModel. Finally, it displays the window.
namespace TabControlDataTemplate
{
   using System.Windows;
   ///
   /// Interaction logic for App.xaml
   ///
   public partial class App : Application
   {
      protected override void OnStartup(StartupEventArgs e)
      {
         base.OnStartup(e);
          
         MainWindow mainView = new MainWindow();
         MainViewModel mainViewModel = new MainViewModel();
         mainView.DataContext = mainViewModel;
         mainView.Show();
      }
   }
}

MainView

<Window x:Class="TabControlDataTemplate.MainWindow"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml\"
   Title="TabControl and DataTemplate Example - YShDev" 
   Height="150" Width="350"
   WindowStartupLocation="CenterScreen">
    
   <Window.Resources>
      <ResourceDictionary Source="MainViewRD.xaml" />
   <Window.Resources>
   <DockPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch" >
            <TabControl ItemsSource="{Binding Pages}">
            </TabControl>
        </DockPanel>
<Window>   

You can see that the ItemSource of the TabControl is set to the Pages property of the DataContext object. The TabControl has two templates: ItemTemplate for pages headers and ContentTemplate for pages contents. When the WPF renderer renders the TabControl, it traverses every page model in the Pages collection and foreach page model it creates a TabPage and sets its header text to the DisplayName of the page model. But what about the ContentTemplate of the TabControl? If you look closely, you can see that no ContentTemplate is mentioned in the TabControl's XAML. How can it be? When the WPF renderer tries to render the contents of a page model, since there is no explicit template given, it will search its database for a proper template by the page type. You can see in the MainView XAML code, that a ResourceDictionary is included in the window resources. Let's see what it contains:
<ResourceDictionary
   xmlns:my="clr-namespace:TabControlDataTemplate"   
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml\" 
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
   <DataTemplate DataType="{x:Type my:BarPageViewModel}"> 
      <my:BarPageView> 
   </DataTemplate>
   <DataTemplate DataType={x:Type my:FooPageViewModel}">
      <my:FooPageView>>  
   </DataTemplate>
<ResourceDictionary>
You can see that the resource dictionary contains matches between a tab page view models and their corresponding page views. This is how the WPF renderer knows what template to choose for each page model view type. So, like I said in the begining, this example can be used as a simple shell for a tabbed application. I order to add more functionality, all you have to do is create a TabPageViewModel and page view and add a type to DataTemplate match in the resource dictionary.