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:
{
///<summary>
/// Used to determine what views were injected and ContentPanes were generated for
///</summary>
privatestaticreadonlyDependencyProperty IsGeneratedProperty = DependencyProperty.RegisterAttached("IsGenerated", typeof(bool), typeof(TabGroupPaneRegionAdapter), null);
privateIRegion _region;
privateTabGroupPane _regionTarget;
privateXamDockManager _parentDockManager;
public TabGroupPaneRegionAdapter(IRegionBehaviorFactory regionBehaviorFactory)
: base(regionBehaviorFactory)
{
}
protectedoverridevoid Adapt(IRegion region, TabGroupPane regionTarget)
{
if (regionTarget.ItemsSource != null)
thrownewInvalidOperationException("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;
}
protectedoverridevoid AttachBehaviors(IRegion region, TabGroupPane regionTarget)
{
base.AttachBehaviors(region, regionTarget);
if (!region.Behaviors.ContainsKey(TabGroupPaneRegionActiveAwareBehavior.BehaviorKey))
region.Behaviors.Add(TabGroupPaneRegionActiveAwareBehavior.BehaviorKey, newTabGroupPaneRegionActiveAwareBehavior { HostControl = regionTarget });
}
protectedoverrideIRegion CreateRegion()
{
returnnewSingleActiveRegion();
}
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);
}
}
elseif (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);
}
}
}
///<summary>
/// Takes all the views that were declared in XAML manually and merges them with the region.
///</summary>
privatevoid SynchronizeItems()
{
if (_regionTarget.Items.Count > 0)
{
foreach (object item in _regionTarget.Items)
{
PrepareContainerForItem(item);
_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>
protectedvirtualContentPane PrepareContainerForItem(object item)
{
ContentPane container = item asContentPane;
if (container == null)
{
container = newContentPane();
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;
}
///<summary>
/// Executes when a ContentPane is closed.
///</summary>
///<remarks>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.</remarks>
///<param name="sender"></param>
///<param name="e"></param>
void Container_Closed(object sender, Infragistics.Windows.DockManager.Events.PaneClosedEventArgs e)
{
ContentPane contentPane = sender asContentPane;
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
}
}
///<summary>
/// 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.
///</summary>
///<param name="container"></param>
void CreateDockAwareBindings(ContentPane contentPane)
{
Binding binding = newBinding("Header");
//let's first check the view that was injected for IDockAware
var dockAwareContent = contentPane.Content asIDockAware;
if (dockAwareContent != null)
binding.Source = dockAwareContent;
//fall back to data context of the content pane.
var dockAwareDataContext = contentPane.DataContext asIDockAware;
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>
protectedvirtualvoid ClearContainerForItem(ContentPane contentPane)
{
if ((bool)contentPane.GetValue(IsGeneratedProperty))
{
contentPane.ClearValue(ContentPane.HeaderProperty); //remove any bindings
contentPane.Content = null;
}
}
///<summary>
/// Finds the DataContext of the view.
///</summary>
///<param name="item"></param>
///<returns></returns>
privateobject ResolveDataContext(object item)
{
FrameworkElement frameworkElement = item asFrameworkElement;
return frameworkElement == null ? item : frameworkElement.DataContext;
}
}
TabGroupPaneRegonActiveAwareBehavior
The TabGroupPaneRegionActiveAwareBehavior is responsible for supporting Activation and Deactivation.
{
publicconststring BehaviorKey = "TabGroupPaneRegionActiveAwareBehavior";
XamDockManager _parentDockManager;
TabGroupPane _hostControl;
publicDependencyObject HostControl
{
get { return _hostControl; }
set { _hostControl = valueasTabGroupPane; }
}
protectedoverridevoid 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 asContentControl;
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 asContentControl;
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] asFrameworkElement;
if (frameworkElement != null)
{
ContentPane contentPane = frameworkElement asContentPane;
if (contentPane == null)
contentPane = frameworkElement.Parent asContentPane;
if (contentPane != null&& !contentPane.IsActivePane)
contentPane.Activate();
}
}
}
}
IDockAware
Hasn’t changed a bit.
{
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.
{
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>
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.
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.