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:
{
/// <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);
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);
}
}
}
/// <summary>
/// Takes all the views that were declared in XAML manually and merges them with the region.
/// </summary>
private void 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>
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;
}
/// <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 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
}
}
/// <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 = 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);
}
/// <summary>
/// Sets the Content property of a generated ContentPane to null.
/// </summary>
/// <param name="contentPane">The ContentPane</param>
protected virtual void 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>
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 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.
{
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.
<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.
[…] UPDATE: See the new version of the XamDockManager Prism Region Adapter […]
Hello Brian,
many thanks for making this code available. I have a quick question:
Does setting the “Name” property also prevent SplitPanes from getting removed from the XamDockManager after all content has been closed?
Some testing suggests a “Yes”, but i’d like to be sure that I can really rely on this.
Thanks,
Tjark
Yes, if you close all panes inside a SplitPane that has an x:Name, the SplitPane will not be removed from the xamDockManager.
Thanks a lot!
It is a wonderful adapter and it saved a lot of time. There is a little bug though. If I add another DocManager with another region inside all the new tabs would be added to the new tab group, instead of the right region. Could you please fix it? Thanks.
Do you have a sample that reproduces your issue that you can share?
I wanted to upload the sample project but I don’t know how to upload it. Anyway, I changed your Shell.xaml file a little and paste the file here. All the new tab items would be added to the second Tabgroup (tabGroupPane2), instead of the first Tabgroup (_tabGroupPaneOne).
I don’t know why the xaml file was not shown in my reply from last comment but here it comes again. Although I put another docmanager inside your original one we had the same issue as long as I put another docmanger with prism regions in the appliction, even in different module.
It seems the site does not like XML documents. Could you please tell me how to upload the sample project. or can directly email me.
Send it to “me [at symbol] brianlagunas dotty comm”.
I sent you a sample app through email yesterday. Please take a look. Thanks a lot.
I see what you mean now. Yes, this is a bug and I will fix it. I will update the post with the correct code as soon as I have time. Thanks for submitting it,
Hi,
I’m using the adapter for my new project and it should go into production in a few months.
I’d like to add a close button on my usercontrol contained in the ContentPane.
The button should follow the behavior (AllowClose = true/false) of the x button found on the DockManager.
I found an extension for the tab header style and maybe with a few changes it could work for my usercontrol.
Thanks
I’m not sure what the question is, but the easiest way to close a pane from a button on your view is to search the visual tree for the parent ContentPane and then use contentPane.ExecuteCommand(ContentPaneCommands.Close) to close the pane. Hope that helps.
Hi,
Thanks for this, I’m about to start evaluating this for my application, could you please tell me how easy layout persistence is using this adapter? Do you have any pointers before I start?
Cheers
Richard