Log in to like this post! XamDockManager–An Updated Prism Region Adapter Brian Lagunas / Saturday, March 16, 2013 UPDATE: See the new version of the XamDockManager Prism Region Adapter Last September, I wrote what has become a very popular Prism region adapter for the Infragistics XamDockManager control. As pointed out in the post, this original XamDockManager Prism region adapter didn’t support all scenarios. Frankly, it’s difficult to write a custom region adapter without knowing every usage of the control. After receiving tons of requests for features and questions on how to implement certain scenarios, I have updated and refactored the XamDockManager Prism region adapter to support the most common requests. So what was added? Support for Activation – Before, there region adaptor supported IActiveAware from the View and ViewModel perspective. Whenever a View or ViewModel was activated, the IActiveAware interface members would be invoked. Unfortunately, the activated View would not become the active docking tab. Now when you use the Region.Activate method within your code, the view being activated will now become the active docking tab. Support for Remove – Before, when you would call the Region.Remove method, the view would be removed from the region, but the docking pane would still be visible. The view would not be removed from the XamDockManager control itself. This was because initially the requirements specifically didn’t support this. I assumed closing of the panes would occur by the user clicking on the close button of the pane. Now, whenever you invoke the Region.Remove method, the view will be removed from the region as well as the XamDockManager. This was a highly request feature. Support for floating panes – Before, the adapter didn’t have any support for floating panes. Basically everything would work fine until you started tearing off panes and placing them in a floating state, or started to create complex nesting and stacking of panes. Now, no matter how you have your panes organized, Region.Activate and Region.Remove will properly activate or remove the View form the region as well as the XamDockManager control. This was by far the most requested feature. The Old RegionAdapter This was the structure before: TabGroupPaneRegionAdapter TabGroupRegionBehavior IDockAware The bulk of the work occurred in the TabGroupRegionBehavior class. Well, that isn’t the recommended way to write region adapters. It only turned out that way because I started to write it to get it to work, and never went back to change it. I just kept writing code and didn’t want to take the time to refactor it to the way I preach writing region adapters. So I just posted it as it was. Well, as it turns out, this example was used more as gospel, rather than a simple “here is an example”. Meaning, that people would use it as “this is how you write all region adapters”. The New RegionAdapter Here is the new structure: TabGroupPaneRegionAdapter TabGroupPaneRegionActiveAwareBehavior IDockAware As you can see, the only thing that really changed was the removal of the TabGroupRegionBehavior. It was replaced with the TabGroupPaneRegionActiveAwareBehavior which I will explain in a little bit. This is the recommended way to create a region adapter. You want to actually handling the adding of views in the Adapt method of your region adapter. TabGroupPaneRegionAdapter The TabGroupPaneRegionAdapter is the actual RegionAdapter that gets registered in the Bootstrapper of your prism application. Now the bulk of the work is move here. Where it belongs. It’s implementation is as follows: public class TabGroupPaneRegionAdapter : RegionAdapterBase<TabGroupPane>{ /// /// Used to determine what views were injected and ContentPanes were generated for /// private static readonly DependencyProperty IsGeneratedProperty = DependencyProperty.RegisterAttached("IsGenerated", typeof(bool), typeof(TabGroupPaneRegionAdapter), null); private IRegion _region; private TabGroupPane _regionTarget; private XamDockManager _parentDockManager; 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."); _region = region; _regionTarget = regionTarget; _parentDockManager = XamDockManager.GetDockManager(regionTarget); SynchronizeItems(); region.Views.CollectionChanged += Views_CollectionChanged; } 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(); } void Views_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { 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); if (_regionTarget.Items.Count != startIndex) startIndex = 0; _regionTarget.Items.Insert(startIndex, contentPane); } } else if (e.Action == NotifyCollectionChangedAction.Remove) { IEnumerable<ContentPane> contentPanes = _parentDockManager.GetPanes(PaneNavigationOrder.VisibleOrder); foreach (ContentPane contentPane in contentPanes) { if (e.OldItems.Contains(contentPane) || e.OldItems.Contains(contentPane.Content)) contentPane.ExecuteCommand(ContentPaneCommands.Close); } } } /// /// Takes all the views that were declared in XAML manually and merges them with the region. /// private void SynchronizeItems() { if (_regionTarget.Items.Count > 0) { foreach (object item in _regionTarget.Items) { PrepareContainerForItem(item); _region.Add(item); } } } /// /// Prepares a view being injected as a ContentPane /// /// the view /// The injected view as a ContentPane protected virtual ContentPane PrepareContainerForItem(object item) { ContentPane container = item as ContentPane; if (container == null) { container = new ContentPane(); container.Content = item; //the content is the view being injected container.DataContext = ResolveDataContext(item); //make sure the dataContext is the same as the view. Most likely a ViewModel container.SetValue(IsGeneratedProperty, true); //we generated this one CreateDockAwareBindings(container); } 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; } /// /// Executes when a ContentPane is closed. /// /// Responsible for removing the ContentPane from the region, any event handlers, and clears the content as well as any bindings from the ContentPane to prevent memory leaks. /// /// void Container_Closed(object sender, Infragistics.Windows.DockManager.Events.PaneClosedEventArgs e) { ContentPane contentPane = sender as ContentPane; if (contentPane != null) { contentPane.Closed -= Container_Closed; //no memory leaks 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 } } /// /// Checks to see if the View or the View's DataContext (Most likely a ViewModel) implements the IDockAware interface and creates the necessary data bindings. /// /// void CreateDockAwareBindings(ContentPane contentPane) { Binding binding = new Binding("Header"); //let's first check the view that was injected for IDockAware var dockAwareContent = contentPane.Content as IDockAware; if (dockAwareContent != null) binding.Source = dockAwareContent; //fall back to data context of the content pane. var dockAwareDataContext = contentPane.DataContext as IDockAware; if (dockAwareDataContext != null) binding.Source = dockAwareDataContext; contentPane.SetBinding(ContentPane.HeaderProperty, binding); } /// /// Sets the Content property of a generated ContentPane to null. /// /// The ContentPane protected virtual void ClearContainerForItem(ContentPane contentPane) { if ((bool)contentPane.GetValue(IsGeneratedProperty)) { contentPane.ClearValue(ContentPane.HeaderProperty); //remove any bindings contentPane.Content = null; } } /// /// Finds the DataContext of the view. /// /// /// private object ResolveDataContext(object item) { FrameworkElement frameworkElement = item as FrameworkElement; return frameworkElement == null ? item : frameworkElement.DataContext; }} TabGroupPaneRegonActiveAwareBehavior The TabGroupPaneRegionActiveAwareBehavior is responsible for supporting Activation and Deactivation. 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) { 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(); } } }} IDockAware Hasn’t changed a bit. public interface IDockAware{ string Header { get; set; }} The New Region Adapter in Action Nothing here has really changed from the original post either. You register the region adapter the same way as before in your bootstrapper. protected override Microsoft.Practices.Prism.Regions.RegionAdapterMappings ConfigureRegionAdapterMappings(){ RegionAdapterMappings mappings = base.ConfigureRegionAdapterMappings(); mappings.RegisterMapping(typeof(TabGroupPane), Container.Resolve<TabGroupPaneRegionAdapter>()); return mappings;} I did update the sample application to make it a little more involved. As you can see, there is now a list of data in a XamDataGrid. When you double click on a row, a view will be injected into the XamDockManager. There are buttons in the menu that will allow you to select a view in the XamDataGrid and activate it, as well as remove it from the region. Feel fee to rearrange your panes to make them as complicated and nested as you want. Disclaimer: the sample app is just a demo application that is meant to show the functionality of the XamDockManager region adapter and is not meant to mimic a production application with coding best practices or guidance. It’s coded to just make it work. Watch out for this Gotcha! There is one thing you need to be aware of when declaring a TabGroupPane as a region. Let’s assume you define your region like this: <igWPF:TabGroupPane prism:RegionManager.RegionName="{x:Static inf:KnownRegionNames.TabGroupPaneOne}"> </igWPF:TabGroupPane> Now you start injection views into your cool region, and you remove a couple and add some more. Everything seems to be working fine until you remove all views from the region. Now, the next time you try to add a view to this empty region you will get an exception. Why? When you remove all views from the TabGroupPane, then pane is removed from the XamDockManager, hence effectively deleting the region you defined. So how do you get around that? Easy! Just give it a name. <!-- Give the TabGroupPane a name with the x:Name attribute so that the region is not destroyed when all views have been removed from the region --> <igWPF:TabGroupPane x:Name="_tabGroupPaneOne" prism:RegionManager.RegionName="{x:Static inf:KnownRegionNames.TabGroupPaneOne}"> </igWPF:TabGroupPane> Giving the TabGroupPane a name will prevent the pane from being removed from the XamDockManager when it is empty. Now you can continue to add and remove views without fear of crashing your application. Feel free to download the new and improved XamDockManager Prism region adapter with sample source code. If you have any questions feel free to contact me through my blog, on twitter (@BrianLagunas), or leave a comment below;