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:
- No changes in a specific modules code - they remain as CAL-compliant as they could be.
- 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.
- We could design any complex tabs\panes initial configuration, all views will find their way in GUI.
- Dynamic view creation\activation\removal through traditional Composite WPF mechanisms also supported.
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.
| Attachment | Size |
|---|---|
| PrismAvalonDock.zip | 266.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.