XamDockManager - Data Binding ContentPanes with MVVM

Brian Lagunas / Tuesday, September 24, 2013

If you are using the Infragistics xamDockManager control and using MVVM to compose your views, then you have probably asked yourself the question, “How do I data bind a collection of objects, which represent my Views, from my ViewModel to various areas of the xamDockManager?”.  You are asking yourself this question because the xamDockManager doesn’t support this data binding out of the box.  The good news is that this is relatively easy to accomplish.  You just have to write a little code.  As with most solutions, there are always more than one way to solve a problem.  There are actually solutions to this specific problem that have already been posted.  For example, this post titled “ItemsSource for xamDockManager”, provides an alternative to the solution I am going to show you.  So why am I showing you another approach?  Well, I prefer something a little more simple and straight forward.  You can choose which solution you prefer.

In this post, I will be adding the required MVVM support to the WPF version of the xamDockManager, and we will be using a Behavior to do it.  My Behavior is going to target a TabGroupPane as my container of choice.  What I mean by “container of choice”, is that I am going to have all my Views data bound and hosted inside of a TabGroupPane.  You may choose something different, such as adding support to the xamDockManager directly, or maybe use the DocumentContentHost.  What ever floats your boat!

Now, this behavior should support a couple of different scenarios.

  • I should be able to data bind a collection of objects, which will represent a view, from a ViewModel
  • I should be able to add new objects to this collection and have the View be shown in a new tab in the xamDockManager
  • I should be able to remove an existing object from the collection and have the View removed from the xamDockManager
  • I should also be able to close a tab and have the object automatically removed from the collection
  • I should be able to provide a DataTemplate to define the structure of the object as the View (implicit or explicit)
  • I should be able to provide a property path to use as the tab header text
  • I should be able to provide a DataTemplate to define the structure of the tab header

That about wraps it up.  Wow, that’s a lot of stuff, but nothing a simple Behavior can’t solve for us.  I would like to note that I am not concerned with removing objects from one collection and adding them to another collection in my ViewModel when dragging and dropping tabs around the xamDockManager.  If you want that, you will have to add that functionality yourself.  I am just going to provide you with the behavior, then talk about it.

public class TabGroupPaneItemsSourceBehavior : Behavior<TabGroupPane>
{
    public static readonly DependencyProperty HeaderMemberPathProperty = DependencyProperty.Register("HeaderMemberPath", typeof(string), typeof(TabGroupPaneItemsSourceBehavior));
    public string HeaderMemberPath
    {
        get { return (string)GetValue(HeaderMemberPathProperty); }
        set { SetValue(HeaderMemberPathProperty, value); }
    }

    public static readonly DependencyProperty HeaderTemplateProperty = DependencyProperty.Register("HeaderTemplate", typeof(DataTemplate), typeof(TabGroupPaneItemsSourceBehavior), new PropertyMetadata(null));
    public DataTemplate HeaderTemplate
    {
        get { return (DataTemplate)GetValue(HeaderTemplateProperty); }
        set { SetValue(HeaderTemplateProperty, value); }
    }

    public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof(IList), typeof(TabGroupPaneItemsSourceBehavior), new PropertyMetadata(null, new PropertyChangedCallback(OnItemsSourcePropertyChanged)));
    public IList ItemsSource
    {
        get { return (IList)GetValue(ItemsSourceProperty); }
        set { SetValue(ItemsSourceProperty, value); }
    }

    private static void OnItemsSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        TabGroupPaneItemsSourceBehavior behavior = d as TabGroupPaneItemsSourceBehavior;
        if (behavior != null)
            behavior.OnItemsSourcePropertyChanged((IList)e.OldValue, (IList)e.NewValue);
    }

    void OnItemsSourcePropertyChanged(IList oldValue, IList newValue)
    {
        AssociatedObject.Items.Clear();

        if (oldValue != null)
        {
            var oldCollectionChanged = oldValue as INotifyCollectionChanged;
            if (oldCollectionChanged != null)
                oldCollectionChanged.CollectionChanged -= CollectionChanged_CollectionChanged;
        }

        if (newValue != null)
        {
            var collectionChanged = newValue as INotifyCollectionChanged;
            if (collectionChanged != null)
                collectionChanged.CollectionChanged += CollectionChanged_CollectionChanged;

            foreach (var item in newValue)
            {
                ContentPane contentPane = PrepareContainerForItem(item);
                AssociatedObject.Items.Add(contentPane);
            }
        }
    }

    public static readonly DependencyProperty ItemTemplateProperty = DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(TabGroupPaneItemsSourceBehavior), new PropertyMetadata(null));
    public DataTemplate ItemTemplate
    {
        get { return (DataTemplate)GetValue(ItemTemplateProperty); }
        set { SetValue(ItemTemplateProperty, value); }
    }

    void CollectionChanged_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.Action == NotifyCollectionChangedAction.Remove)
        {
            IEnumerable<ContentPane> contentPanes = XamDockManager.GetDockManager(AssociatedObject).GetPanes(PaneNavigationOrder.VisibleOrder);
            foreach (ContentPane contentPane in contentPanes)
            {
                var dc = contentPane.DataContext;
                if (dc != null && e.OldItems.Contains(dc))
                {
                    contentPane.ExecuteCommand(ContentPaneCommands.Close);
                }
            }
        }
        else if (e.Action == NotifyCollectionChangedAction.Add)
        {
            foreach (var item in e.NewItems)
            {
                ContentPane contentPane = PrepareContainerForItem(item);
                if (contentPane != null)
                    AssociatedObject.Items.Add(contentPane);
            }
        }
    }

    protected ContentPane PrepareContainerForItem(object item)
    {
        ContentPane container = new ContentPane();

        container.Content = item;
        container.DataContext = item;

        if (HeaderTemplate != null)
            container.HeaderTemplate = HeaderTemplate;

        if (ItemTemplate != null)
            container.ContentTemplate = ItemTemplate;

        container.CloseAction = PaneCloseAction.RemovePane;
        container.Closed += Container_Closed;

        CreateBindings(item, container);

        return container;
    }

    private void Container_Closed(object sender, PaneClosedEventArgs e)
    {
        ContentPane contentPane = sender as ContentPane;
        if (contentPane != null)
        {
            contentPane.Closed -= Container_Closed; //no memory leaks

            var item = contentPane.DataContext;

            if (ItemsSource != null && ItemsSource.Contains(item))
                ItemsSource.Remove(item);

            RemoveBindings(contentPane);
        }
    }

    private void CreateBindings(object item, ContentPane contentPane)
    {
        if (!String.IsNullOrWhiteSpace(HeaderMemberPath))
        {
            Binding headerBinding = new Binding(HeaderMemberPath);
            headerBinding.Source = item;
            contentPane.SetBinding(ContentPane.HeaderProperty, headerBinding);
        }
    }

    private void RemoveBindings(ContentPane contentPane)
    {
        contentPane.ClearValue(ContentPane.HeaderProperty);
    }
}

As you can see we have only a couple of properties.  The HeaderMemberPath is used to specify the property path of the underlying object to use as the text for the tab header.  The HeaderTemplate property is used to control the structure of the tab header.  For example, maybe we want to add images or change other aspects of the tab header.  Next, we have the ever popular ItemsSource property.  You will use this property to data bind your collection of objects from your ViewModel to the TabGroupPane of the xamDockManager.  Notice how I am using IList as the property type.  This allows us to add and remove items from the collection.  Lastly, we have the ItemTemplate property.  This property will allow us to provide a DataTemplate to define the structure of each of the items in the collection.  Now keep in mind, the ItemTemplate will only really work if all the objects in your collection are the same.  If you have a collection of different object types, then you will not be using the ItemTemplate property.  Feel free to add more properties and modify this behavior to fit your specific needs.

The Sample

So how do you use this behavior?  Well let’s start with a ViewModel.

public class ViewModel
{
    public ObservableCollection<object> People { get; set; }

    public DelegateCommand InsertCommand { get; set; }
    public DelegateCommand RemoveCommand { get; set; }

    public ViewModel()
    {
        People = new ObservableCollection<object>();
        People.Add(new Person() { FirstName = "Brian", LastName = "Lagunas", Age = 66 });

        InsertCommand = new DelegateCommand(Insert);
        RemoveCommand = new DelegateCommand(Remove);
    }

    public void Insert(object param)
    {
        People.Add(new Person() { FirstName = String.Format("First {0}", DateTime.Now.Second), LastName = String.Format("Last {0}", DateTime.Now.Second), Age = DateTime.Now.Millisecond });
    }

    public void Remove(object param)
    {
        var person = param as Person;
        if (person != null)
            People.Remove(person);
    }
}

This is a very straight forward ViewModel.  It has a single collection and two commands.  One command will insert a new Person object into my collection and the other command will remove an instance of a Person object from the collection.  Speaking of the Person object, let’s take a look at it.

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string FullName { get { return String.Format("{0}, {1}", LastName, FirstName); } }
    public int Age { get; set; }
}

Notice how I am not implementing INotifyPropertyChanged.  This is only because this is meant to be sample code and not meant to replicate a production system.  When you do this, make you’re your ViewModels and POCOs implement INotifyPropertyChanged. 

Next, let’s define our behavior in our View.  Since we are using a Behavior, you need to add a reference to System.Windows.Interactivity to your WPF application and add an namespace in your XAML.

<Window x:Class="XamDockManager_MVVM.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:igWPF="http://schemas.infragistics.com/xaml/wpf"
        xmlns:local="clr-namespace:XamDockManager_MVVM"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Menu>
            <MenuItem Header="Insert" Command="{Binding InsertCommand}" />
            <MenuItem Header="Remove" Command="{Binding RemoveCommand}" CommandParameter="{Binding ActivePane.DataContext, ElementName=_dock}" />
        </Menu>

        <igWPF:XamDockManager x:Name="_dock" Grid.Row="1" >
            <igWPF:XamDockManager.Panes>
                <igWPF:SplitPane>
                    <igWPF:TabGroupPane x:Name="_dynamicTab">
                        <i:Interaction.Behaviors>
                            <local:TabGroupPaneItemsSourceBehavior ItemsSource="{Binding People}"/>
                        </i:Interaction.Behaviors>
                    </igWPF:TabGroupPane>
                </igWPF:SplitPane>
            </igWPF:XamDockManager.Panes>
        </igWPF:XamDockManager>

    </Grid>
</Window>

Let’s run the application and see what we have so far.

image

That’s cool and all, but our objects don’t really looks like views right now, and what’s up with the tab header?  Let’s start using some of those properties we created to make this look a litle better.  Let’s start with the header.  I want to bind the tab header to the FullName property of our Person object.  I also want to make some changes to the formatting of the tab header so I am going to create a new DataTemplate for it.  I am also going to define an DataTemplate to use as the ItemTemplate for my Person object.

<Window x:Class="XamDockManager_MVVM.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:igWPF="http://schemas.infragistics.com/xaml/wpf"
        xmlns:local="clr-namespace:XamDockManager_MVVM"
        Title="MainWindow" Height="350" Width="525">

    <Window.Resources>

        <DataTemplate x:Key="HeaderTemplate">
            <TextBlock Text="{Binding}" FontSize="18" FontWeight="Bold" />
        </DataTemplate>

        <DataTemplate x:Key="PersonTemplate">
            <Border BorderBrush="Blue" BorderThickness="2">
                <Grid Margin="10">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="Auto"/>
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="Auto"/>
                        <ColumnDefinition Width="*"/>
                    </Grid.ColumnDefinitions>
                    <TextBlock Text="First Name: " />
                    <TextBox Grid.Column="1" Text="{Binding FirstName}" />
                    <TextBlock Grid.Row="1" Text="Last Name: " />
                    <TextBox Grid.Row="1" Grid.Column="1" Text="{Binding LastName}" />
                    <TextBlock Grid.Row="2" Text="Age: " />
                    <TextBox Grid.Row="2" Grid.Column="1" Text="{Binding Age}" />
                </Grid>
            </Border>
        </DataTemplate>

    </Window.Resources>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Menu>
            <MenuItem Header="Insert" Command="{Binding InsertCommand}" />
            <MenuItem Header="Remove" Command="{Binding RemoveCommand}" CommandParameter="{Binding ActivePane.DataContext, ElementName=_dock}" />
        </Menu>

        <igWPF:XamDockManager x:Name="_dock" Grid.Row="1" >
            <igWPF:XamDockManager.Panes>
                <igWPF:SplitPane>
                    <igWPF:TabGroupPane x:Name="_dynamicTab">
                        <i:Interaction.Behaviors>
                            <local:TabGroupPaneItemsSourceBehavior HeaderMemberPath="FullName"
                                                                   HeaderTemplate="{StaticResource HeaderTemplate}"
                                                                   ItemsSource="{Binding People}"
                                                                   ItemTemplate="{StaticResource PersonTemplate}"/>
                        </i:Interaction.Behaviors>
                    </igWPF:TabGroupPane>
                </igWPF:SplitPane>
            </igWPF:XamDockManager.Panes>
        </igWPF:XamDockManager>

    </Grid>
</Window>

Let’s see what we have with our changes.

image

Now that’s better!  You don’t have to use the ItemTemplate property.  If you have a collection of objects, for example ObservableCollection<object>, you can provide an implicit DataTemplate to the various types you are adding to the collection.  Maybe you have a DataTemplate for Person, and a different one for Car, and a different one for Pet.  You can create a different DataTemplate for each of these types and they will be rendered properly for each corresponding tab.

That’s wraps it up for this post.  Keep in mind that this post is mainly to help guide you in implementing MVVM with the xamDockManager control and you will probably have to modify this code to meet your specific needs.  Go ahead and download the source code for this post.  Feel free contact me on my blog, connect with me on Twitter (@brianlagunas), or leave a comment below for any questions or comments you may have.