Tuesday 25 August 2009

Layout Manager for Prism v2

One of the issues you may encounter when working on a Prism project is the management of regions and views within your application. While the RegionManager does an adequate job of managing regions, the orchestration of views and regions is pretty much left up to the developer.

A common approach is to define string constants in a common infrastructure assembly and injecting views into regions using these constants. This gets the job done, but adds rigidity to your application. For applications which require multiple layouts, coordinating regions and views can be a bit tedious.

One common approach I would not recommend is injecting your views in your module's Initialize method.
public void Initialize()
{
    var view = new MyView();
    _Container.RegisterInstance<IMyView>(view);
    _RegionManager.Regions[RegionNames.Shell].Add(view);
}
This violates the encapsulation of the module, restricting the reuse of the module.

On one project, we opted to create a "layout module". The sole purpose of this module was to load a layout UserControl into the Shell region of the main application window, and injecting the views into its own defined regions. Definitely a step in the right direction by decoupling the module views from the regions. The layout module was defined and loaded like any other module, but had to be the last module loaded due to its dependencies. One drawback to this approach was the increasing number of dependencies. The layout module had to reference all the infrastructure assemblies of the views it was required to manage.

Still this solution felt a bit too purpose-built. And other issues quickly arose, such as multiple layout support.  Ideally we were looking for a complete decoupling of regions and views with the ability to dynamically load layouts as required.

We quite liked the idea of using layout views, views whose sole purpose was to define regions, and providing no business or UI logic. But, the source and introduction of these views needed to be dynamic and flexible. The LayoutManager is my first attempt at tackling this issue. Its purpose is to dynamically manage one or more layout configurations for a Prism application.

To compile and run the LayoutManager you will need Visual Studio 2008 SP1 and the latest version of the Composite Application Guidance for WPF and Silverlight - February 2009.

The solution is fairly standard Prism solution, consisting of an Infrastructure, Shell and Modules projects. For the sake of simplicity, I've only included a single Modules project, where normally there would be more.

The LayoutManager maintains a collection of Layout objects, which define layout controls, along with the views that will reside in the layout.

Configuration
The LayoutManager is configured by a LayoutProvider specified in your app.config file.

<section name="layoutProvider" type="Composite.Layout.Configuration.LayoutProviderSection, Composite.Layout"/>

Currently, two providers are available: ConfigLayoutProvider and XamlLayoutProvider. Custom providers can be used by inheriting from LayoutProviderBase.

ConfigLayoutProvider
Defines the LayoutManager in the app.config file as shown below:
<layoutProvider name="ConfigLayoutProvider" type="Composite.Layout.Configuration.ConfigLayoutProvider, Composite.Layout">
    <layoutManager shellName="Shell" >
      <layouts>
        <layout name="FirstLayout" 
              filename="Layouts\FirstLayout.xaml" 
              fullname="First Layout" 
              isDefault="True"
              description="This is the default layout" 
              thumbnailSource="pack://application:,,,/LayoutManager.Infrastructure;component/Resources/Images/layout1.png">
          <views>
            <view typeName="LayoutManager.Infrastructure.IViewA, LayoutManager.Infrastructure" regionName="Left"  />
            <view typeName="LayoutManager.Infrastructure.IViewB, LayoutManager.Infrastructure" regionName="Right" />
            <viewModel typeName="LayoutManager.Infrastructure.IMenuViewModel, LayoutManager.Infrastructure" regionName="Menu"  viewProperty="View"/>
          </views>
        </layout>
        ...
          </layouts>
    </layoutManager>
</layoutProvider>

XamlLayoutProvider
Defines the LayoutManager in Xaml

<layoutProvider name="XamlLayoutProvider"
type="Composite.Layout.Configuration.XamlLayoutProvider, Composite.Layout"
filename="Layouts\LayoutConfiguration.xaml"/>

The source of the Xaml can be specified by type or by filename.
<Layout:LayoutManager xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                      xmlns:Layout="clr-namespace:Composite.Layout;assembly=Composite.Layout"
                      xmlns:Infrastructure="clr-namespace:LayoutManager.Infrastructure;assembly=LayoutManager.Infrastructure"
                      ShellName="Shell">
    <Layout:LayoutManager.Layouts>
        <Layout:Layout x:Name="FirstLayout"
                       Fullname="First Layout"
                       Filename="Layouts\FirstLayout.xaml"
                       Description="This is the default layout"
                       ThumbnailSource="pack://application:,,,/LayoutManager.Infrastructure;component/Resources/Images/layout1.png"
                       IsDefault="True">
            <Layout:Layout.Views>
                <Layout:ViewModel RegionName="Menu"
                                  Type="{x:Type Infrastructure:IMenuViewModel}"
                                  ViewProperty="View" />
                <Layout:View RegionName="Left"
                             Type="{x:Type Infrastructure:IViewA}" />
                <Layout:View RegionName="Right"
                             Type="{x:Type Infrastructure:IViewB}" />
            </Layout:Layout.Views>
        </Layout:Layout>
 ...
    </Layout:LayoutManager.Layouts>
</Layout:LayoutManager>

Each Layout contains a Views collection. The views collection accommodates both Views and ViewModels. The View specifies what view control is to be loaded and what region it is to be placed in. You can also set the visibility for the view. Use the ViewProperty of the ViewModel to specify the name of the property on your ViewModel which holds the View.

The LayoutManager is loaded after all of the modules have initialized. In the Bootstrapper.cs:

protected override void InitializeModules()
{
     base.InitializeModules();
     InitializeLayoutManager();
}

private void InitializeLayoutManager()
{
     var layoutManager = LayoutConfigurationManager.LayoutManager;
     layoutManager.Initialize(Container);
     Container.RegisterInstance(layoutManager, new ContainerControlledLifetimeManager());
     //parameterless LoadLayout loads the default Layout into the Shell
     layoutManager.LoadLayout();
}
The LayoutManager requires use of the Container. Once your layouts have been loaded, call the Initialize method passing in the container.

Once that is done, you can register the LayoutManager in the container making it accessible to other modules.

Loading a Layout
Layouts are loaded by calling the LoadLayout method of the LayoutManager.

LoadLayout() loads the default layout in the Shell

LoadLayout(string layoutName) loads the named layout in the Shell

The MenuViewModel.cs illustrates the use of LoadLayout:
private void LayoutCommandExecute(ILayout layout)
{
var layoutManager = _Container.Resolve<ILayoutManager>(); 
layoutManager.LoadLayout(layout.Name);
}

The basic sequence of loading a layout is:
  1. If there is a current layout, remove it from the RegionManager.
  2. Clear out any controls that were bound to any regions. This step is necessary otherwise you will get an InvalidOperationException ("This control is being associated with a region, but the control is already bound to something else") when you try to reload it in the future. Currently, the LayoutManager only supports ItemsControls, ContentControls and Panels using the RegionManager.RegionName attached property.
  3. Add the new Layout Control to the RegionManager.
  4. Register any Regions contained within the Layout Control.
  5. Load any views associated with the new layout.
Events
There are several events raised by the LayoutManager:

  • LayoutManagerInitializedEvent raised at the end of Initialize (see MenuViewModel.cs for an example of subscribing to this event)
  • LayoutLoadingEvent raised at the beginning of LoadLayout
  • LayoutLoadedEvent raised at the end of LoadLayout
  • LayoutUnloadingEvent raised before the current layout is about to be unloaded
  • LayoutUnloadedEvent raised after the current layout has been unloaded
All of these events are published through the EventAggregator.

Limitations
Currently there are several limitations with the LayoutManager, these are:
LayoutManager currently only supports UserControls as layout controls. There is also the basic assumption that your application main window has a single region defined, where layout controls are injected. Regions must be defined in XAML using the RegionManager.RegionName attached property.

Other Considerations
While the LayoutManager does decouple the regions from the views, it does not entirely do away with string-based region names. Dynamic manipulation of regions and views in code will still rely on region names (see the AddCommandExecute method in MenuViewModel.cs on how to programmatically add layouts). And region name attributes must match actual region names in the Layout control.

A possible approach to addressing this dependency may be to introduce a RegionType enumeration such as Top, Bottom, Left, Right, Center, StatusBar, Menu, Toolbar, etc. In which case, the LayoutManager could resolve these regions regardless of string names.

I have not tested the LayoutManager in all possible scenarios, such as nested layouts and custom RegionAdapters, or with Silverlight.

You can download the LayoutManager source code here.

11 comments:

Unknown said...

Just wanted to say thanks for sharing the layout manager - I'm learning a lot from it.

rgramann said...

Thanks headbiznatch, glad to see it's of some use.

mahe said...

Hi Ron,

First of all, thanks a ton for the layout manager in Prism V2. Its an eye opener for us. We are trying to use nested layouts in our application (a layout's view can contain another layout or a view/module). With minimal code change to layout manager its working fine - just made sure the remove layout is not called if the current layout is shell. Now, wanted to know the challenges we might face in terms of dealing with all the child layouts and modules (esp. handling events and context across the tree(parent-child layouts)) It would be of great help if you can throw some light on this.
Thanks in advance,
Mahendran

rgramann said...

Hi Mahendran,

Thanks for your comments. I'm glad you found it useful.
Regarding the events with child layouts, I wouldn't think there would be too much difficulty if you were using the EventAggregator. My personal preference is to do as much with Commands (like the DelegateCommand) as possible, and publish events for system-wide notifications.
For sharing context you may want to consider implementing a Controller.

mahe said...

Thanks Gramann.

We are using EventAggregator and exploring Delegatecommands and controllers. If you have any reference implementations in this regard, please share with us.

Thanks again,
Mahendran

rgramann said...

Hi Mahendran,

Unfortunately all the code I have which uses these is confidential client code. I'm in the middle of a large Prism project, which is consuming most of my time. If I get a free moment I will put together a sample project which illustrates the use of these technologies.

mahe said...

Thanks for all your replies amidst your busy schedule. Also good to know that you are in big prism project. I will be checking this post for your sample project (hoping that you will get some free time :-)). Thanks again.

Anonymous said...

Hi Ron,
Is it possible to just load the layout(which will have regions defined) without the views, and then we can load views into these regions from within other modules.

rgramann said...

One of the design goals with the Layout Manager is to take the responsibility of seating the views into regions away from Modules or the Shell and place it with an external mechanism.

Seating views into regions from within Modules greatly reduces the reusablility of the Module.

Anonymous said...

Thanks so much Ron, that's correct, however i am dealing with a specific problem right now, we have to support multiple layouts but we already have code written that defines multiple regions in Shell ( basically they have placed many into the single column of a grid and somehow managing multiple layouts). My idea is to load the required layout in Shell before any of the views are loaded, and then allow these Module/Controllers to continue loading the views into the regions as they are doing already.

Anonymous said...

Thanks so much, this article is eye opener for me, there is so much to learn. I have downloaded the code for the article and ran it it's working fine.
One thing i do not understand clearly is how Visual Studio Intellisence is working fine if we try to add Layouts in the file -LayoutConfiguration.xaml, it works fine in the original project that i downloaded, however i have crated another project and using my LayoutConfiguration.xaml file but there the Intellisence is not working. Just wondering how VS-2008 is able to proviode intellisence in this case since we do not have specified any dtd or schema for LayoutConfiguration.xaml. If you can please comment on this that will be great.