Composite WPF (PRISM) and docking

It seems that I've found an easy solution for using every existing Visual Studio-style docking library for WPF (AvalonDock, SandDock, Infragistics xamDockManager etc) with Composite WPF library (aka CAL aka PRISM). The idea is quite simple: to extract visual region handling from CAL code and use CAL regions as a metadata for further processing. As a result we have:

  1. No changes in a specific modules code - they remain as CAL-compliant as they could be.
  2. No changes in CAL sources except UnityBootstrapper class, which is easily separatable from the rest of the library - and should be used only in application-level project anyway.
  3. We could design any complex tabs\panes initial configuration, all views will find their way in GUI.
  4. Dynamic view creation\activation\removal through traditional Composite WPF mechanisms also supported.

avd.JPG

Why should we change anything in a CAL code?

Unfortunately, the VS-style docking is too complex to be adequately described by a RegionAdapter architecture purposed by a patterns&practices team as a standard approach for region's customization. As a result, the existing docking adapters are not sufficient: adapter for Infragistics refers only to a very specific one-tabpanel-for-all-tabs case, and adapter for Actipro introduces some obscure metadata into specific view's code, which contradicts the whole paradigm of PRISM: modules shouldn't know anything about where they should be placed. My idea was to remove all RegionAdapter logics for one specific case of project with docking, leaving all modules code - and all CAL library code - intact and reusable in other, non-docking incarnations of our application.

Custom DockRegion class

This is were all docking library specifics goes. I provide here a sample for AvalonDock, surely it's very easy to write a similar class for every other docking library in existence.

using System.Collections.Generic;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using AvalonDock;
using Microsoft.Practices.Composite.Presentation.Regions;
 
namespace CalToAvalonDockProxy
{
 
public class AvalonDockRegion : Region
{
private Selector pane;
private readonly Dictionary<object, ManagedContent> viewToPane =
new Dictionary<object, ManagedContent>();
 
public AvalonDockRegion()
{
Views.CollectionChanged += Views_CollectionChanged;
}
 
private void Views_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if ((pane != null)&&(e.Action == NotifyCollectionChangedAction.Add))
{
ManagedContent dc = null;
object view = e.NewItems[0];
string header = GetHeaderInfoFromView(view);
if (pane is DockablePane)
{
dc = new DockableContent
{
  Name = Name,
  Content = view,
  DockableStyle = DockableStyle.Document,
  Title = header,
  Background = null,
  IsEnabled = true
};
}
else if (pane is DocumentPane)
{
var docCont = new DocumentContent
       {
           Name = Name,
           Content = view,
           Title = header,
           Background = null,
           IsEnabled = true
       };
docCont.Closed += docCont_Closed;
dc = docCont;
}
 
if (dc != null)
{
viewToPane.Add(view,dc);
pane.Items.Add(dc);
}
}
}
 
/// 
/// should queue some inteface like IHeaderInfoProvider to get from a view information about tab's header
/// Unfortunately, IHeaderInfoProvider wasn't included into CAL library (only in examples),
/// so we have to inherit from AvalonDockRegion in order to be independant from project's business logics
/// 
protected virtual string GetHeaderInfoFromView(object view)
{
return "";
}
 
private void docCont_Closed(object sender, System.EventArgs e)
{
foreach (var pair in viewToPane)
{
if (pair.Value == sender)
{
Remove(pair.Key);
break;
}
}
}
 
public void Bind(UIElement content)
{
if (content is ContentControl)
{
foreach (var view in Views)
{
((ContentControl)content).Content = view;
break;
}
}
else if (content is Selector)
{
pane = (Selector)content;
}
}
 
public override void Activate(object view)
{
base.Activate(view);
ManagedContent content;
if (viewToPane.TryGetValue(view,out content))
{
pane.SelectedItem = content;
}
}
}
}

Note: in our AvalonDockRegion implementation, when a region is bound to a Selector (DockablePane or DocumentPane), views are added to them dynamically (as new documents/panes) when they added to a module's view collection. When a region is bound to a specific pane, region assumes the first view as pane's content.

Custom bootstrapper

The only thing we need to change in standard UnityBootstrapper code is to extract all WPF-based region drawing specifics from method Run() into a new virtual function ConfigureRegions() to allow additional customization in UnityBootstrapper's inheritors:

public abstract class CustomRegionsBootstrapper
{
//just a copy of UnityBootstrapper class, except:
....
//code also taken from UnityBootstrapper class, but
//1. Part of code extracted to a virtual ConfigureRegions function 
//2. Removed references to private CAL project resources in exceptions, replaced by a simple text strings.
public virtual void Run(bool runWithDefaultConfiguration)
{
this.useDefaultConfiguration = runWithDefaultConfiguration;
ILoggerFacade logger = this.LoggerFacade;
if (logger == null)
{
throw new InvalidOperationException("The ILoggerFacade is required and cannot be null.");
}
 
logger.Log("Creating Unity container", Category.Debug, Priority.Low);
this.Container = this.CreateContainer();
if (this.Container == null)
{
throw new InvalidOperationException("The IUnityContainer is required and cannot be null.");
}
 
this.Container.AddNewExtension();
this.ConfigureContainer();
 
ConfigureRegions();
 
logger.Log("Initializing modules", Category.Debug, Priority.Low);
this.InitializeModules();
 
logger.Log("Bootstrapper sequence completed", Category.Debug, Priority.Low);
}
 
//just a piece of code from method Run moved here
protected virtual void ConfigureRegions()
{
loggerFacade.Log("Configuring region adapters", Category.Debug, Priority.Low);
 
this.ConfigureRegionAdapterMappings();
this.ConfigureDefaultRegionBehaviors();
 
loggerFacade.Log("Creating shell", Category.Debug, Priority.Low);
DependencyObject shell = this.CreateShell();
if (shell != null)
{
RegionManager.SetRegionManager(shell, this.Container.Resolve());
RegionManager.UpdateRegions();
}
}
}

Please note that I use bootstrapper code from PRISM 2.0 alpha, but surely it's possible to reuse code from CAL 1.0 in the same way.

Using it in application

As it was recommended by Composite UI Guidance, we create our own bootstrapper class for our application. In case of docking, it also contains some docking-specific region handling:

   class MyDockingClientBootstrapper : CustomRegionsBootstrapper
{
//just a traditional module registatration
protected override IModuleCatalog GetModuleCatalog()
{
var catalog = new ModuleCatalog();
catalog.AddModule(typeof(Module1))
.AddModule(typeof(Module2))
.AddModule(typeof(Module3))
 
return catalog;
}
 
private IRegionManager regionManager;
 
protected void RegisterRegion(string regionName)
{
var reg = new AvalonDockRegion { Name = regionName };
regionManager.Regions.Add(reg);
}
 
//overriding the region creation logics - creates a region for every module
protected override void ConfigureRegions()
{
regionManager = Container.Resolve();
RegisterRegion("Module1");
RegisterRegion("Module2");
RegisterRegion("Module3");
}
 
public void BindRegionToGui(string regionName, UIElement content)
{
var reg = (AvalonDockRegion)regionManager.Regions[regionName];
reg.Bind(content);
}
}

Now the only thing left is to bind regions to specific panes (or pane groups). You may do it any way you wish, for example by reading RegionManager.RegionName from XAML (a standard way to do it), or just by hardcode:

public partial class MainWindow
{
public MainWindow()
{
InitializeComponent();
 
App.Bootstrapper.BindRegionToGui("Module1", module1Pane);
App.Bootstrapper.BindRegionToGui("Module2", module2Pane);
App.Bootstrapper.BindRegionToGui("Module3", module3Pane);
}
}

Conclusion

Please don't regard this unpolished samples of code as a final solution to all your PRISM\docking compatibility problems. The main purpose of this post was to overcome the common fear of PRISM-and-docking-not-working-together. Actually, they work together just fine, and I strongly encourage using both of these libraries. With docking there are even more reasons to use PRISM, 'cause docking does not work well in browser (XBAP or Silverlight) applications. Using PRISM, we can separate specific modules from the application that binds them together, and it's very easy to develop "rich" (with docking) and "light" (with traditional WPF containers) versions of your application, both with 100% reusable code.

AttachmentSize
PrismAvalonDock.zip266.05 KB

Comments

hello

Thanks, for the good articles...I am very intiresting..

Why not use an adapter?

Hi,

I'm posting a few month late but i thought I'd ask.

I think I'm missing the point of ditching the Adapters and not creating one. The AvalonDockRegion is nice. But The addition of the Bind(UIElement element) method in the Region implementation breaks the Prism design and makes it less reusable. (ie the bootstraper now needs to worry about specific region implementation).

Wouldn't be more aligned with Prism to implement an AvalonDockRegionAdapter, that creates the AvalonDockRegion on the CreateRegion() override and does everything that Bind is doing right now in the Adapt() override?

This way, the bootstraper only needs to concern with:

regionAdapterMappings.RegisterMapping(typeof(DockablePane), this.Container.Resolve());
regionAdapterMappings.RegisterMapping(typeof(CocumentPane), this.Container.Resolve());

I don't see a compelling reason why not to create the adapter... What were your thoughts?

Thanks,
k

Re: Why not use an adapter?

Thanks for the comments. As I said before, "The main purpose of this post was to overcome the common fear of PRISM-and-docking-not-working-together". At the time of this article, many people were doubting if it's possible at all to combine docking with PRISM. Of course I was planning to write more on this matter, but, unfortunately, shortly after this article was written, project requirements were changed, and we aren't using any VS-style docking for our projects at all now. That is why I'm not continuing blogging about docking any more, although may do so in the future.

Implementing for SandDock (ManagedContent problem)

Hi,
I'm trying to implement this code for SandDock from DivElememtns (www.divelements.co.uk) for SilverLight.
I'm pretty new to PRISM so maybe my question is stupid. However I get a compilation error on "ManagedContent" in
private readonly Dictionary

In your code the MangedContent seems to be generated class form some metadata. How should I implement it for SandDoc?
...or can you explain how the ManagedContent class is generated?

Re: Implementing for SandDock

ManagedContent is a class specific for AvalonDock. For another docking library (SandDock for example), AvalonDockRegion has to be completely rewritten as SandDockRegion using SandDock-specific classes and methods. I'm pretty sure this is possible for SandDock, all you need is just to be able to create panes and tabs on-the-fly.

DockableContent

I hope you might get this. Your work has inspired me to mesh PRISM and AvalonDock.

While this gives great guidance on making PRISM work with "DocumentContent" elements...what advice could you give to make it also work with DockableContent elements?

Thanks in advance for your help.

Paul Reed
Houston, Tx.

Re: DockableContent

Thanks for your comments. About your question: sample already works with DockableContent, handling them as named containers for views. If you are asking about creating DockableContent dynamically (like DocumentContent is handled in the sample), I guess this is already done or could be done by a little additional coding, see AvalonDockRegion code (if (pane is DockablePane){dc = new DockableContent)...)

Title

Hi

Merging with Prism of ... using it

Instead of merging AvalonDock and Prism we used Prism architecture and inserted AvalonDock as a module:
- First we embedded AvalonDock in a UserControl, which is Sofa (sofawpf.codeplex.com).
- Then we used the Prism RegionManager to add a module based on Sofa.
- And at last we moved some of the modules from the RegionManager to AvalonDock/Sofa.

See more here: http://sofawpf.codeplex.com/wikipage?title=Prism&referringTitle=Documentation

The Sofa Team

Some good news for me but not enaugh

I followed your post, I can say very nicely made prism working with Avalon Docking library.
I try to implement the same thing using the Telerik docking library.More or less Avalon docking and Telerik's library are a least conceptually the same.I followed a different approach.
So let me start first present how the Telerik Docking library is designed.
On top it has the RadDocking (the main dock panel), which can have RadSplitContainers (a container which can present other RadSplitContainers or RadPaneGroups).
The RadPaneGroups are also containers which can hold only RadPanes (RadPaneGroup is a sort of Tab control and the RadPane is a sort of TabItem).You got the idea, much more the same as the Avalon Docking.
I wrote adapters over the 3 main controls (the RadDocking, RadSplitContainer and the RadPaneGroup), but the problem is that I can't make it right because when user is dragging the Pane and docking it in other container (lets say from left side to the right side) I don't know what to do.
Should I remove the pane from the region it sits an add it to new region (where it was dragged).
Everything is dynamic when using dock panels, groups and split containers are created and removed dynamically by the Telerik's dock controller when the user drags the panels etc.
So my question is should synchronize regions with the docking control (create and remove regions on the fly) ?

I think the region part of prism was never designed to work with a tree like scenario (which is the case of the docking) and everything I try to make is a complete hack.

Please contact me at adrianaxente@yahoo.com
Best regards,
Axente Adrian

Another Thing

I think this is the same as nesting tabs in tab items of another tab (nested scenario). but however this can be done with RegionContexs but that involves more then one IRegionManager (which I don't like).

So please someone help me before I blow my head off :))

Re: Another Thing

Thanks for your interest in my article. Sorry, I've never worked with Telerik docking before, so I can't be of much help here. I feel it might be a good topic for another article, but currently I'm busy with other projects, however I may return to this topic somewhere in the future, especially because Telerik controls are very popular.

This article is outdated now,

This article is outdated now, please refer to newer Prism/AvalonDock integration sample, written by the creator of AvalonDock

http://stackoverflow.com/questions/11518810/avalondock-2-0-with-prism-4-...

Comments to this particular article are disabled because it accumulates a large number of spam

Make sure you assist me with this particular

Hey there! Easy issue that's absolutely off subject.
Have you any idea how to make simple an internet site mobile favorable? My internet site appears peculiar when searching from my cellphone.
I'm trying to find a motif or plug-in that could possibly solve this matter. Please share if you have any suggestions.
Many thanks. Make sure you assist me with this particular