XamDockManager - Data Binding ContentPanes with MVVM

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.


Comments  (10 )

Anton Golovanov
on Tue, Jan 21 2014 8:41 AM

Hi Brian,

thank you for a such elegant solution! But, it seems like I've found potential bug in XamDockManager. To reproduce it just remove x:Name attribute from the TabGroupPane tag.

Now, do the following:

1. Close default tab by using x.

2. Try to click on Insert menu item.

What I see at the moment is that after last tab is removed it's not possible to add tabs anymore. When x:Name attribute is returned back everything is OK. I'm testing it on 13.1 (13.1.20131.1009) version.

Brian Lagunas
on Tue, Jan 21 2014 10:46 AM

That's not a bug, it is by design.  By giving the TabGroupPane an x:Name, it prevents it from being destroyed by the xamDockManager.  If no x:Name is provided, the TabGroupPane will be removed from the xamDockManager and collected to reduce the memory footprint.

Anton Golovanov
on Wed, Jan 22 2014 3:36 AM

Good to know. Is it written somewhere in documentation or is it standard WPF behavior? Because such things are not obvious.

Brian Lagunas
on Wed, Jan 22 2014 11:22 AM

Well, it is not easily found as it is not talked about specifically.  It is discussed a little in the Saving/Loading customizations, but even then you wouldn't know that it behaved that way.  I agree, it is definitely not obvious.

Gagan Kapoor
on Tue, Nov 11 2014 11:22 AM

Hi Brian

How do I set the newly created Tab as Active ? I would also need a way to indicate which tab is currently active. I have seen your post which explains use of IActiveAware but I am not using prism regions and using above approach to add new tabs.

Brian Lagunas
on Tue, Nov 11 2014 1:39 PM

It's been awhile since I have written that code, but I would think that a simple call to contentPane.Activate(); right after AssociatedObject.Items.Add(contentPane); would do the trick.  In order to track which tab is Active as you change tabs, you could simply create another behavior which tracks the active content pane, and then use your own interface to access that information in your ViewModels.  Shouldn't be difficult.

Jon Heaton
on Wed, Dec 3 2014 1:24 PM

Awesome post!  Very useful.  Thanks!

Jon Heaton
on Mon, Dec 8 2014 4:50 PM

If I change your XamDockManager.Panes to DocumentContentHost, the HeaderTemplate is not used.  What I want to do is put the close tab onto the tab instead of all the way to the right.  How can I have the header template applied on those tab types?

The way you have it currently set up I am able to put whatever I want in that HeaderTemplate and it works.  The issue is the Tabbed Document type that is created when using a DocumentContentHost.

Thanks!

Brian Lagunas
on Tue, Dec 9 2014 11:41 AM

Try modifying the behavior to support this:

www.infragistics.com/.../17769.aspx

Jon Heaton
on Thu, Dec 11 2014 2:02 PM

Thank you for the response!  I ended up just overriding the whole template because I need to support rename header in those tabs too, but I will take a look at that post and see if I can improve on what I have.

In your behavior class in the method OnItemsSourcePropertyChanged, I get a null pointer exception when calling AssociatedObject.Items.Clear().  The ItemCollection HAS items before the call, but it still encounters the null pointer exception.  Do you have any ideas on this?  I can share my project with you if it would help.

Thanks!

Add a Comment

Please Login or Register to add a comment.