The Prism region adapter I wrote for the xamDockManager has become much more popular than I would have ever thought.  My last update to the xamDockManger Prism region adapter was back in March.  That update included some refactoring and support for programmatic view activation and removal , and support for floating panes.  Originally the xamDockManager Prism region adapter was designed with View first MVVM development in mind.  Meaning that you would actually inject Views, in the form of a UI control, into the xamDockManager regions.  Well, I have gotten a number of requests to support ViewModel first MVVM development, in which you inject ViewModels into the xamDockManager region and you control the creation of the Views with DataTemplates.  I also had some bugs reported when dealing with nested regions within the xamDockManager.  So lets take a look at the updated xamDockManager Prism region adapter.

The structure remains unchanged:

  1. TabGroupPaneRegionAdapter
  2. TabGroupPaneRegionActiveAwareBehavior
  3. IDockAware

TabGroupPaneRegionAdapter

    public class TabGroupPaneRegionAdapter : RegionAdapterBase<TabGroupPane>
    {
        /// <summary>
        /// Used to determine what views were injected and ContentPanes were generated for
        /// </summary>
        private static readonly DependencyProperty IsGeneratedProperty = DependencyProperty.RegisterAttached(“IsGenerated”, typeof(bool), typeof(TabGroupPaneRegionAdapter), null);

        /// <summary>
        /// Used to track the region that a ContentPane belongs to so that we can access the region from within the ContentPane.Closed event handler
        /// </summary>
        private static readonly DependencyProperty RegionProperty = DependencyProperty.RegisterAttached(“Region”, typeof(IRegion), typeof(TabGroupPaneRegionAdapter), null);

        public TabGroupPaneRegionAdapter(IRegionBehaviorFactory regionBehaviorFactory)
            : base(regionBehaviorFactory)
        {
        }

        protected override void Adapt(IRegion region, TabGroupPane regionTarget)
        {
            if (regionTarget.ItemsSource != null)
                throw new InvalidOperationException(“ItemsSource property is not empty. This control is being associated with a region, but the control is already bound to something else. If you did not explicitly set the control’s ItemSource property, this exception may be caused by a change in the value of the inherited RegionManager attached property.”);

            SynchronizeItems(region, regionTarget);

            region.Views.CollectionChanged += (Object sender, NotifyCollectionChangedEventArgs e) =>
            {
                OnViewsCollectionChanged(sender, e, region, regionTarget);
            };
        }

        protected override void AttachBehaviors(IRegion region, TabGroupPane regionTarget)
        {
            base.AttachBehaviors(region, regionTarget);

            if (!region.Behaviors.ContainsKey(TabGroupPaneRegionActiveAwareBehavior.BehaviorKey))
                region.Behaviors.Add(TabGroupPaneRegionActiveAwareBehavior.BehaviorKey, new TabGroupPaneRegionActiveAwareBehavior { HostControl = regionTarget });
        }

        protected override IRegion CreateRegion()
        {
            return new SingleActiveRegion();
        }

        private void OnViewsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e, IRegion region, TabGroupPane regionTarget)
        {
            if (e.Action == NotifyCollectionChangedAction.Add)
            {
                //we want to add them behind any previous views that may have been manually declare in XAML or injected
                int startIndex = e.NewStartingIndex;
                foreach (object newItem in e.NewItems)
                {
                    ContentPane contentPane = PrepareContainerForItem(newItem, region);

                    if (regionTarget.Items.Count != startIndex)
                        startIndex = 0;

                    //we must make sure we bring the TabGroupPane into view.  If we don’t a System.StackOverflowException will occur in
                    //UIAutomationProvider.dll if trying to add a ContentPane to a TabGroupPane that is not in view.
                    //This is most common when using nested TabGroupPane regions. If you don’t need this, you can comment it out.
                    regionTarget.BringIntoView();

                    regionTarget.Items.Insert(startIndex, contentPane);
                }
            }
            else if (e.Action == NotifyCollectionChangedAction.Remove)
            {
                if (regionTarget.Items.Count == 0)
                    return;

                IEnumerable<ContentPane> contentPanes = XamDockManager.GetDockManager(regionTarget).GetPanes(PaneNavigationOrder.VisibleOrder);
                foreach (ContentPane contentPane in contentPanes)
                {
                    if (e.OldItems.Contains(contentPane) || e.OldItems.Contains(contentPane.Content))
                        contentPane.ExecuteCommand(ContentPaneCommands.Close);
                }
            }
        }

        /// <summary>
        /// Takes all the views that were declared in XAML manually and merges them with the region.
        /// </summary>
        private void SynchronizeItems(IRegion region, TabGroupPane regionTarget)
        {
            if (regionTarget.Items.Count > 0)
            {
                foreach (object item in regionTarget.Items)
                {
                    PrepareContainerForItem(item, region);
                    region.Add(item);
                }
            }
        }

        /// <summary>
        /// Prepares a view being injected as a ContentPane
        /// </summary>
        /// <param name=”item”>the view</param>
        /// <returns>The injected view as a ContentPane</returns>
        protected ContentPane PrepareContainerForItem(object item, IRegion region)
        {
            ContentPane container = item as ContentPane;

            if (container == null)
            {
                container = new ContentPane();
                container.Content = item; //the content is the item being injected
                container.DataContext = ResolveDataContext(item); //make sure the dataContext is the same as the item. Most likely a ViewModel
                container.SetValue(IsGeneratedProperty, true); //we generated this one
                CreateDockAwareBindings(container);
            }

            container.SetValue(RegionProperty, region); //let’s keep track of which region the container belongs to

            container.CloseAction = PaneCloseAction.RemovePane; //make it easy on ourselves and have the pane manage removing itself from the XamDockManager
            container.Closed += Container_Closed;

            return container;
        }

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

                IRegion region = contentPane.GetValue(RegionProperty) as IRegion; //get the region associated with the ContentPane so that we can remove it.
                if (region != null)
                {
                    if (region.Views.Contains(contentPane)) //we are dealing with a ContentPane directly
                        region.Remove(contentPane);

                    var item = contentPane.Content; //this view was injected and set as the content of our ContentPane
                    if (item != null && region.Views.Contains(item))
                        region.Remove(item);
                }

                ClearContainerForItem(contentPane); //reduce memory leaks
            }
        }

        /// <summary>
        /// Checks to see if the item being injected implements the IDockAware interface and creates the necessary data bindings.
        /// </summary>
        /// <remarks>
        /// First we will check if the ContentPane.Content property implements the IDockAware interface. Depending on the type of item being injected,
        /// this may be a View or a ViewModel. If no IDockAware interface is found to create a binding, we next move to the ContentPane.DataContext property, which will
        /// most likely be a ViewModel.
        /// </remarks>
        /// <param name=”container”></param>
        void CreateDockAwareBindings(ContentPane contentPane)
        {
            Binding binding = new Binding(“Header”);

            //let’s first check the item that was injected for IDockAware. This may be a View or ViewModel
            var dockAwareContent = contentPane.Content as IDockAware;
            if (dockAwareContent != null)
                binding.Source = dockAwareContent;

            //nothing was found on the item being injected, let’s check the DataContext
            if (binding.Source == null)
            {
                //fall back to data context of the content pane, which is most likely a ViewModel
                var dockAwareDataContext = contentPane.DataContext as IDockAware;
                if (dockAwareDataContext != null)
                    binding.Source = dockAwareDataContext;
            }

            contentPane.SetBinding(ContentPane.HeaderProperty, binding);
        }

        /// <summary>
        /// Sets the Content property of a generated ContentPane to null.
        /// </summary>
        /// <param name=”contentPane”>The ContentPane</param>
        protected void ClearContainerForItem(ContentPane contentPane)
        {
            if ((bool)contentPane.GetValue(IsGeneratedProperty))
            {
                contentPane.ClearValue(ContentPane.HeaderProperty); //remove any bindings
                contentPane.Content = null;
            }
        }

        /// <summary>
        /// Finds the DataContext of an item.
        /// </summary>
        /// <remarks>
        /// If we are injecting a View, the result will be the Views DataContext. If we are injecting a ViewModel, the result will be the ViewModel.
        /// </remarks>
        /// <param name=”item”>The item</param>
        /// <returns>A Views DataContext or a ViewModel</returns>
        private object ResolveDataContext(object item)
        {
            FrameworkElement frameworkElement = item as FrameworkElement;
            return frameworkElement == null ? item : frameworkElement.DataContext;
        }
    }
}

TabGroupPaneRegionActiveAwareBehavior

public class TabGroupPaneRegionActiveAwareBehavior : RegionBehavior, IHostAwareRegionBehavior
{
    public const string BehaviorKey = “TabGroupPaneRegionActiveAwareBehavior”;

    XamDockManager _parentDockManager;

    TabGroupPane _hostControl;
    public DependencyObject HostControl
    {
        get { return _hostControl; }
        set { _hostControl = value as TabGroupPane; }
    }

    protected override void OnAttach()
    {
        _parentDockManager = XamDockManager.GetDockManager(_hostControl);
        if (_parentDockManager != null)
            _parentDockManager.ActivePaneChanged += DockManager_ActivePaneChanged;

        Region.ActiveViews.CollectionChanged += ActiveViews_CollectionChanged;
    }

    void DockManager_ActivePaneChanged(object sender, RoutedPropertyChangedEventArgs<ContentPane> e)
    {
        if (e.OldValue != null)
        {
            var item = e.OldValue;

            //are we dealing with a ContentPane directly
            if (Region.Views.Contains(item) && Region.ActiveViews.Contains(item))
            {
                Region.Deactivate(item);
            }
            else
            {
                //now check to see if we have any views that were injected
                var contentControl = item as ContentControl;
                if (contentControl != null)
                {
                    var injectedView = contentControl.Content;
                    if (Region.Views.Contains(injectedView) && Region.ActiveViews.Contains(injectedView))
                        Region.Deactivate(injectedView);
                }
            }
        }

        if (e.NewValue != null)
        {
            var item = e.NewValue;

            //are we dealing with a ContentPane directly
            if (Region.Views.Contains(item) && !this.Region.ActiveViews.Contains(item))
            {
                Region.Activate(item);
            }
            else
            {
                //now check to see if we have any views that were injected
                var contentControl = item as ContentControl;
                if (contentControl != null)
                {
                    var injectedView = contentControl.Content;
                    if (Region.Views.Contains(injectedView) && !this.Region.ActiveViews.Contains(injectedView))
                        Region.Activate(injectedView);
                }
            }
        }
    }

    void ActiveViews_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.Action == NotifyCollectionChangedAction.Add)
        {
            //are we dealing with a view
            FrameworkElement frameworkElement = e.NewItems[0] as FrameworkElement;
            if (frameworkElement != null)
            {
                ContentPane contentPane = frameworkElement as ContentPane;
                if (contentPane == null)
                    contentPane = frameworkElement.Parent as ContentPane;

                if (contentPane != null && !contentPane.IsActivePane)
                    contentPane.Activate();
            }
            else
            {
                //must be a viewmodel
                object viewModel = e.NewItems[0];
                var contentPane = GetContentPaneFromFromViewModel(viewModel);
                if (contentPane != null)
                    contentPane.Activate();
            }
        }
    }

    ContentPane GetContentPaneFromFromViewModel(object viewModel)
    {
        var panes = XamDockManager.GetDockManager(_hostControl).GetPanes(PaneNavigationOrder.VisibleOrder);
        foreach (ContentPane contentPane in panes)
        {
            if (contentPane.DataContext == viewModel)
                return contentPane;               
        }

        return null;
    }
}

IDockAware

public interface IDockAware
{
    string Header { get; set; }
}

The Updated Region Adapter in Action

Nothing here has really changed from the previous posts.  The main difference is now you can inject ViewModels and use DataTemplates to control your Views.  Let’s take a look at the ShellViewModel.

public class ShellViewModel : INotifyPropertyChanged
{
    IRegionManager _regionManager;
    IUnityContainer _container;

    private ObservableCollection<ViewAViewModel> _views;
    public ObservableCollection<ViewAViewModel> Views
    {
        get { return _views; }
        set
        {
            _views = value;
            NotifyPropertyChanged(“Views”);
        }
    }

    public DelegateCommand<object> ActivateCommand { get; set; }
    public DelegateCommand<object> OpenCommand { get; set; }
    public DelegateCommand<object> CloseCommand { get; set; }

    public ShellViewModel(IUnityContainer container, IRegionManager regionManager)
    {
        _container = container;
        _regionManager = regionManager;

        ActivateCommand = new DelegateCommand<object>(Activate);
        OpenCommand = new DelegateCommand<object>(Open, CanOpen);
        CloseCommand = new DelegateCommand<object>(Close);

        //generate some data
        Views = new ObservableCollection<ViewAViewModel>();
        Views.Add(_container.Resolve<ViewAViewModel>());
        Views.Add(_container.Resolve<ViewAViewModel>());
        Views.Add(_container.Resolve<ViewAViewModel>());
        Views.Add(_container.Resolve<ViewAViewModel>());
        Views.Add(_container.Resolve<ViewAViewModel>());
        Views.Add(_container.Resolve<ViewAViewModel>());
        Views.Add(_container.Resolve<ViewAViewModel>());
        Views.Add(_container.Resolve<ViewAViewModel>());
        Views.Add(_container.Resolve<ViewAViewModel>());
    }

    void Activate(object viewModel)
    {
        if (viewModel == null)
            return;

        IRegion region = _regionManager.Regions[KnownRegionNames.TabGroupPaneOne]; //get the region
        if (region.Views.Contains(viewModel))
            region.Activate(viewModel); //activate the viewModel
    }

    void Close(object viewModel)
    {
        if (viewModel == null)
            return;

        IRegion region = _regionManager.Regions[KnownRegionNames.TabGroupPaneOne]; //get the region
        if (region.Views.Contains(viewModel))
            region.Remove(viewModel); //remove the viewModel
    }

    void Open(object viewModel)
    {
        IRegion region = _regionManager.Regions[KnownRegionNames.TabGroupPaneOne]; //get the region

        if (!region.Views.Contains(viewModel))
            region.Add(viewModel); //add the viewModel

        region.Activate(viewModel); //active the viewModel
    }

    bool CanOpen(object viewModel)
    {
        return viewModel != null;
    }

    #region INotifyPropertyChanged

    public event PropertyChangedEventHandler PropertyChanged;
    protected void NotifyPropertyChanged(string propertyName)
    {
        var handler = PropertyChanged;
        if (handler != null)
            handler(this, new PropertyChangedEventArgs(propertyName));
    }

    #endregion //INotifyPropertyChanged
}

As you can see, we are no longer referencing any type of View in the ViewModel.  You are only acting against the ViewModels.  This is a good thing, and I always suggest removing any type of View related objects out of your ViewModel.  To render our View, we just create a DataTemplate that defines how the View should look for each instance of the inject ViewModel.

<Window.Resources>
    <DataTemplate DataType=”{x:Type moduleA:ViewAViewModel}“>
        <StackPanel TextBlock.TextAlignment=”Center” TextBlock.FontSize=”24″ VerticalAlignment=”Center”>
            <TextBlock Text=”{Binding Number}” />
            <TextBlock Text=”{Binding IsActive, StringFormat=IsActive: {0}}” />
        </StackPanel>
    </DataTemplate>
</Window.Resources>

This DataTemplate is being applied implicitly to all instances of type ViewAViewModel.  There is not a single View defined in the module.  Now you can deliver a much more MVVM friendly application using the xamDockManager in using Prism.

xamDockManager prism region adapter supporting ViewModel first

Feel free to download the new and improved XamDockManager Prism region adapter with sample source code.  I probably haven’t covered every possible use of this adapter, so if you find a scenario that should be support let me know.  If you have any questions feel free to contact me through my blog, on twitter (@BrianLagunas), or leave a comment below.

Brian Lagunas

View all posts

19 comments

  • Hello,
    If I’ve multiple TabGroupPane opened (multiple screens) how can I choose on which one to create the next ContentPane.
    I’m using _regionManager.RequestNavigate but the ContentPane is always created on the first TabGroupPane.

    Thanks

    • In order to inject your views into different TabGroupPanes, those panes have to be prism regions. If you are dynamically creating these, you should used scoped regions, and use the region manager of the scoped region to navigate the views to the proper TabGroupPane.

      If your TabeGroupPanes are static, you can simply give those different region names and navigate to those regions instead.

      • What I’d like to have is the following :
        When one ContentPane (viewmodel) does a request to open a new one, the last would be added into the current TabGroupPane.
        How can I retrieve the current scoped RegionManager ?

        • I would recommend using custom region behavior that will create the scoped region for you and attach the region manager with an attached property. This way you can access it directly from the object being injected..

  • Hi Brian,

    In order to display different views in TabGroupPanes, i inject ViewModels and use DataTemplates to control the view, i have tested using the solution provided here and i made some changes. i have changed the DataTemplates to use the custom user control i created.

    I used the memory profiler to monitor the memory usage, i found that when i closed the ContentPane, the user control and the associated view model instances do not destroy/ dispose properly.

    In the codes, when we closed one of the content pane, the associated ContentPane.Content will be set to null and the ContentPane will be removed from the Region. But this does not destroy/dispose the view models and user control properly.

    How can i ensure that the view model is disposed correctly when it’s closed? Please advice.

      • Thanks for the reply.

        I am using ANTS Memory Profiler 7. Actually the TabItemAutomationPeer & ContentPane are holding the UserControl and ViewModel objects. Is there a way to dispose the ContentPane when it’s closed?

        I have posted the same problem in http://www.infragistics.com/community/forums/p/77267/436367.aspx#436367. I have also uploaded the source codes which i used for memory testing. You could find this attachment in the same web site.

        Looking forward to hearing from you. Thank you.

  • Hi Brian,

    when i add many contentpanes to the xamDockManager (XMD). How i can place this contentpane to the right of the XDM?
    When there are many contentPanes, the next contentPane will be placed on the left (default behaviour??)…

    Thank you.

    • The panes should be added to the right by default, as I am incrementing the index to insert the pane by one each time. If you want to change this behavior, you just need to modify the TabGroupPaneRegionAdapter at line 66. Simply calculate your own insertion index before calling the Insert method.

  • I test with your sample. When adding the 11th contentpane, it’s be placed on the right before ‘default 1’ content pane (the insertion index = 10). My screen have a resolution of 1280*1024 and this sample is running in maximized mode. Can you check this behavior?

    In minimized mode, the 8th contentpane will be placed on the left….

    Many thanks

    • I think I understand what you mean now. Keep in mind, my adapter showed an example of modifying the insert index to be on the right of the previous tab, which is not the default behavior. The default behavior is to open new tabs to the left of the previous tab just like Visual Studio. To keep the default behavior, simply remove all the “startIndex” code, and at line 79 just use regionTarget.Items.Insert(0, contentPane);. Now all tabs will be added to the left and in view, while pushing all the other tabs to the right.

  • Hi Brian,
    This is just awesome. We are planning to use this in a trading application that we will be writing for a bank. Could you please help us out with the layout persistence.
    Thanks a ton.

  • Hi Brian,

    How could I add a tooltip on the tab header ?
    If I use contentPane.SetBinding(ContentPane.ToolTipProperty, binding); it adds a tooltip in the body of the view not on the header.

    Thanks in advance

    • You have to set the tooltip on the header of the content pane. So, thinking off the top of my head, you would need to create an implicit style that would provide a new HeaderTemplate with the binding for the Tooltip added.

    • I just got a question from our support team asking the same problem, so I figured I would provide the Tooltip solution here too. This is Assuming that there is a “HeaderToolTip” property on the bound ViewModel.

      var headerTemplate = new DataTemplate();

      FrameworkElementFactory textBlock = new FrameworkElementFactory(typeof(TextBlock));
      textBlock.SetBinding(TextBlock.TextProperty, new Binding(“”));
      textBlock.SetBinding(TextBlock.ToolTipProperty, new Binding(“DataContext.DataContext.HeaderToolTip”) { RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(ContentControl), 1) });
      headerTemplate.VisualTree = textBlock;

      //if inside a DocumentContentHost
      contentPane.TabHeaderTemplate = headerTemplate;

  • Thanks
    I modified TabGroupPaneRegionAdapter like the following

    void CreateDockAwareBindings(object item, ContentPane contentPane)
    {
    contentPane.HeaderTemplate = contentPane.FindResource(“HeaderTemplate”) as DataTemplate;

    Binding bindingHeader = new Binding(“Header”);
    Binding bindingAllowClose = new Binding(“AllowClose”);
    Binding bindingImage = new Binding(“Image”);
    Binding bindingFullHeader = new Binding(“FullHeader”);

    //let’s first check the view that was injected for IDockAware
    var dockAwareContent = contentPane.Content as IDockAware;
    if (dockAwareContent != null)
    {
    bindingHeader.Source = dockAwareContent;
    bindingAllowClose.Source = dockAwareContent;
    bindingImage.Source = dockAwareContent;
    bindingFullHeader.Source = dockAwareContent;
    }

    //fall back to data context of the content pane.
    var dockAwareDataContext = contentPane.DataContext as IDockAware;
    if (dockAwareDataContext != null)
    {
    bindingHeader.Source = dockAwareDataContext;
    bindingAllowClose.Source = dockAwareDataContext;
    bindingImage.Source = dockAwareDataContext;
    bindingFullHeader.Source = dockAwareDataContext;
    }

    contentPane.SetBinding(ContentPane.HeaderProperty, bindingHeader);
    contentPane.SetBinding(ContentPane.AllowCloseProperty, bindingAllowClose);
    contentPane.SetBinding(ContentPane.ImageProperty, bindingImage);
    }

    public interface IDockAware
    {
    string Header { get; set; }
    string FullHeader { get; set; }
    bool AllowClose { get; set; }
    ImageSource Image { get; }
    }

    Unfortunately, I has no effect on the header. I must be missing a detail.

  • Hi,
    With XamDockManager when I try to drag a tab it take around 5 seconds before the tab gets detached.
    Once the tab is detached the performance is ok.
    Do you know where that time is spend and how I can improve ?

    Thank in advance.

Follow Me

Follow me on Twitter, subscribe to my YouTube channel, and watch me stream live on Twitch.