Click here to Skip to main content
Email Password   helpLost your password?

Introduction

Starting development of an application with a complex user interface, we always face the same problems: how to organize data presentation, change views, route events, share resources and so on. Badly planned project structure leads to a headache and extensive rework. That's why before starting a big project, I'd like to make a prototype of a WPF-based solution and share my small experience with you.

Developing an application, we confront increasing complexity - the more controls, views, menus we add, the more tangled application architecture becomes. And one fine day we understand that it is easy to throw away all we've done before, than add yet another module. But thanks to design patterns, the problem can be solved. All we need is Composite Application Library. With its help, we can split user interface into regions and dynamically load modules into them. We can organize event and command routing between different modules. And what is more important - a loosely coupled design of an application built with the Composite Application Library allows different teams to create and test modules independently.

Sounds good, but if you have just decided to use the Composite Application Library, the next question you ask yourself is: "well, samples work fine, but how can I build something more realistic?"

I decided to create a small application emulating work with a few servers. Toolbar depends on a server context. Menu bar contains menu items, depending on currently selected module (Documents, Users or Security). Central area contains a view presenting current module data:

Composite WPF Application

It is just a prototype. So, only one module was written more or less in more detail - Documents. My main goals were:

Project Structure

The main project is CompositeWpfApp. It contains the main application Window - Shell. That's the first and the last UI element in the project - all other UI views, controls and menus will be created in different projects.

Another set of projects is located in the Common folder:

These projects could be linked statically. But the projects in Modules folder will be loaded dynamically when they are needed.

Solution structure

In order to load these modules, they should be copied to the Modules directory in the CompositeWpfApp project. Open Properties of a "Module" project (e.g. CWA.Module.Documents) and enter the following Post-build event command line:

xcopy "$(TargetDir)*.*" "$(SolutionDir)CompositeWpfApp\bin\
				$(ConfigurationName)\Modules\" /Y

Besides, we should enumerate the modules in the App.config file in order that the ConfigurationModuleCatalog could be able to locate and load them:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="modules" 
	type="Microsoft.Practices.Composite.Modularity.ModulesConfigurationSection, 
	Microsoft.Practices.Composite"/>
  </configSections>
  <modules>
    <module assemblyFile="Modules/CWA.Module.DefaultModule.dll" 
	moduleType="CWA.Module.DefaultModule.DefaultModule, 
	CWA.Module.DefaultModule" moduleName="DefaultModule"/>
    <module assemblyFile="Modules/CWA.Module.ServerSelector.dll" 
	moduleType="CWA.Module.ServerSelector.ServerSelector, 
	CWA.Module.ServerSelector" moduleName="ServerSelectorModule"/>
    <module assemblyFile="Modules/CWA.Module.ModuleSelector.dll" 
	moduleType="CWA.Module.ModuleSelector.ModuleSelector, 
	CWA.Module.ModuleSelector" moduleName="ModuleSelectorModule"/>
    <module assemblyFile="Modules/CWA.Module.StatusArea.dll" 
	moduleType="CWA.Module.StatusArea.StatusArea, 
	CWA.Module.StatusArea" moduleName="StatusAreaModule"/>
    <module assemblyFile="Modules/CWA.Module.Documents.dll" 
	moduleType="CWA.Module.Documents.DocumentsModule, 
	CWA.Module.Documents" moduleName="Documents" startupLoaded="false"/>
    <module assemblyFile="Modules/CWA.Module.Users.dll" 
	moduleType="CWA.Module.Users.UsersModule, 
	CWA.Module.Users" moduleName="Users" startupLoaded="false"/>
    <module assemblyFile="Modules/CWA.Module.Security.dll" 
	moduleType="CWA.Module.Security.SecurityModule, 
	CWA.Module.Security" moduleName="Security" startupLoaded="false"/>
  </modules>
</configuration>

Pay attention to module names - they are defined in the CWA.Infrastructure project.

The ConfigurationModuleCatalog is defined as our module enumerator in the Bootstrapper class. It will be used by the Composite Application Library to get information about the modules and their location:

protected override IModuleCatalog GetModuleCatalog()
{
    // ConfigurationModuleCatalog class builds a catalog of modules from 
    // a configuration file
    return new ConfigurationModuleCatalog();
}

Instead of the ConfigurationModuleCatalog you can use DirectoryModuleCatalog to discover modules in assemblies stored in a particular folder, or specify modules in your code or in a XAML file. DirectoryModuleCatalog could be particularly useful for applications with plug-ins.

Now our spade-work is completed and we can proceed with UI.

Regions and Views

Regions are used to define a layout for a view. If you look at Shell.xaml, you will see region names in its markup:

<ItemsControl Name="MainMenuRegion" 
	cal:RegionManager.RegionName="{x:Static inf:RegionNames.MainMenuRegion}"
    	DockPanel.Dock="Top" Focusable="False" />
<ItemsControl Name="ServerSelectorRegion" 
	cal:RegionManager.RegionName="{x:Static inf:RegionNames.ServerSelectorRegion}"
    	DockPanel.Dock="Top" Focusable="False" />
<ItemsControl Name="ModuleSelectorRegion" 
	cal:RegionManager.RegionName="{x:Static inf:RegionNames.ModuleSelectorRegion}"
    	DockPanel.Dock="Top" Focusable="False"/>
<ItemsControl Name="StatusRegion" 
	cal:RegionManager.RegionName="{x:Static inf:RegionNames.StatusRegion}"
    	DockPanel.Dock="Bottom" Focusable="False" />
<ItemsControl Name="MainRegion" 
	cal:RegionManager.RegionName="{x:Static inf:RegionNames.MainRegion}" 
	Focusable="False">

These regions will be used to load modules into them:

Regions and Views

Let's see how to load modules into the Main Region. ModuleController is responsible for changing views in this region. First of all, we should register this class in the Bootstraper class of the Shell project:

Container.RegisterType<IGeneralController, ModuleController>
	(ControllerNames.ModuleController, new ContainerControlledLifetimeManager());

The ModuleController class implements IGeneralController interface with the single method Run(). CreateShell() method of the Bootstraper finds classes implementing IGeneralController interface and invokes their Run() methods. As a result, the ModuleController subscribes to the ModuleChangeEvent:

public void Run()
{
    eventAggregator.GetEvent<ModuleChangeEvent>().Subscribe
			(DisplayModule, ThreadOption.UIThread, true);
}

When we click on the "Documents", "Users" or "Security" button, ModuleSelectorPresententaionModel publishes ModuleChangeEvent with the corresponding module name. ModuleController catches the event and displays the module in the following method (shortened version):

private void DisplayModule(string moduleName)
{
    try
    {
        moduleManager.LoadModule(moduleName);

        IModulePresentation module = TryResolve<IModulePresentation>(moduleName);

        if (module != null)
        {
            IRegion region = regionManager.Regions[RegionNames.MainRegion];
            currentView = region.GetView(RegionNames.MainRegion);

            if (currentView != null)
                region.Remove(currentView);

            currentView = module.View;
            region.Add(currentView, RegionNames.MainRegion);
            region.Activate(currentView);
        }
    }
}

The RegionManager is responsible for creating and managing regions - a kind of container for controls implementing IRegion interface. Our duty is removing previously loaded content from the region and addition of new module view to it. The only requirement to the module view is implementation of the IModulePresentation interface exposing a View property.

Model-View-Presenter

Model-View-Presentation Model pattern is intended to separate data model from its presentation and business logic. On practice, that means that Presentation Model provides content for visual display (View) and tracks changes in visual content and data model.

MVP pattern

Modules, loaded into the Main Region, should implement this pattern. Really, in this demo solution, only CWA.Module.Documents follows this pattern. This module contains DocumentsPresentationModel and DocumentsView. Separate data model class is not implemented due to simplicity of the sample.

Documents Project

DocumentsView code-behind does not contain any business logic. Instead, all processing is performed in the DocumentsPresentationModel. To bind a View to the Presentation Model, we initialize the view in the DocumentsPresentationModel constructor and make it publicly available as a View property:

public object View
{
    get { return view; }
}

public DocumentsPresentationModel(IUnityContainer container, 
				IServerContextService serverContextService)
{
    this.container = container;
    this.serverContextService = serverContextService;
    view = container.Resolve<DocumentsView>();
}

This View will be passed from the DocumentsPresentationModel to the DocumentsModule and later loaded into a region (see DocumentsModule.cs):

/// <summary>
/// A View associated with the module.
/// </summary>
public object View
{
    get
    {
        // Each ServerContext shall have a PresentationModel
        DocumentsPresentationModel presentationModel =
            (DocumentsPresentationModel)TryResolve<IPresentationModel>
		(serverContextService.CurrentServerContext.Uid);

        if (presentationModel == null)
        {
            // If there is no a PresentationModel associated with the ServerContext
            // (i.e. the module was not called/displayed 
	   // for the currently selected Server), create it.
            container.RegisterType<IPresentationModel, DocumentsPresentationModel>
			(serverContextService.CurrentServerContext.Uid,
                 new ContainerControlledLifetimeManager());

            // Create a PresentationModel
            presentationModel = (DocumentsPresentationModel)container.Resolve
	      <IPresentationModel>(serverContextService.CurrentServerContext.Uid);
        }

        return presentationModel.View;
    }
}

Now we can operate with the view in the DocumentsPresentationModel - for example, to bind some data to visual controls or, if necessary, display a view of another type. In the last case, Supervising Controller pattern would be more suitable.

Menus

Often an application can have menus containing some constant set of general items like "Help", "Exit", "About program", and context-dependent menu items. It makes sense to process general commands in the Shell project, while view-dependent commands should be processed in the corresponding module.

So, we have a few requirements:

First requirement can be easily met. There is a MenuController in the Shell project, which is very similar to ModuleController described above. Its purpose is to display menu views in the Main Menu Region in response to the MainMenuChangeEvent. A view generates this event when it is loaded and displayed, i.e., activated. Each module view is derived from the ModuleViewBase class, that defines virtual event handler ViewActivated(). Overridden method looks like this one:

protected override void ViewActivated(object sender, EventArgs e)
{
    base.ViewActivated(sender, e);

    if (Menu != null)
        eventAggregator.GetEvent<MainMenuChangeEvent>().Publish(Menu);
}

Where the Menu is a UserControl initiated in the view's constructor:

Menu = container.Resolve<DocumentsMainMenuView>();

If you look at DocumentsMainMenuView.xaml, you will see the following XAML code:

<UserControl x:Class="CWA.Module.Documents.DocumentsMainMenuView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:ctl="clr-namespace:CWA.UIControls.Menus;assembly=CWA.UIControls"
    Height="Auto" Width="Auto" Name="DocumentsMainMenu">

    <Menu>
        <ctl:MainMenuControl />
        <MenuItem Header="Documents">
            <MenuItem Header="New Document" Command="{Binding NewDocumentCommand}" />
            <MenuItem Header="Cut" Command="{Binding CutCommand}" />
            <MenuItem Header="Copy" Command="{Binding CopyCommand}" />
            <MenuItem Header="Delete" Command="{Binding DeleteCommand}" />
            <MenuItem Header="Rename" Command="{Binding RenameCommand}" />
            <Separator />
            <MenuItem Header="Properties" Command="{Binding PropertiesCommand}" />
        </MenuItem>
        <ctl:HelpMenuControl />
    </Menu>
</UserControl>

It is our markup for menus loaded when a user clicks on "Documents". Do you remember we promised not to duplicate general menu items? We keep our promises - MainMenuControl and HelpMenuControl are defined in the CWA.UIControls project. We'll return to them a bit later.

Now we have to provide a way to process menu commands in the Presentation Model, not in the Menu View class. To do that, we have to bind menu's DataContext to the Presentation Model. Let's return to the DocumentsPresentationModel constructor:

public DocumentsPresentationModel(IUnityContainer container, 
				IServerContextService serverContextService)
{
    this.container = container;
    this.serverContextService = serverContextService;

    view = container.Resolve<DocumentsView>();
    view.Menu.DataContext = this;

    view.Text = serverContextService.CurrentServerContext.Name;

    NewDocumentCommand = new DelegateCommand<object>(NewDocument, CanExecuteCommand);
    CutCommand = new DelegateCommand<object>(Cut, CanExecuteCommand);
    CopyCommand = new DelegateCommand<object>(Copy, CanExecuteCommand);
    DeleteCommand = new DelegateCommand<object>(Delete, CanExecuteCommand);
    RenameCommand = new DelegateCommand<object>(Rename, CanExecuteCommand);
    PropertiesCommand = new DelegateCommand<object>(Properties, CanExecuteCommand);
}

As to general menu items defined in the MainMenuControl and HelpMenuControl - they are sources of commands of RoutedUICommand type. The commands are bubbling up to a window, containing command binding for those type of commands. Our responsibility is to create such a binding somewhere in the Shell project. For that purpose, I created a few command controllers, registered and started them in the Bootstrapper:

private void RegisterCommandControllers()
{
    Container.RegisterType<IGeneralController, ExitCommandController>
      (ControllerNames.ExitCommandController, new ContainerControlledLifetimeManager());
    Container.RegisterType<IGeneralController, SkinCommandController>
      (ControllerNames.SkinCommandController, new ContainerControlledLifetimeManager());
    Container.RegisterType<IGeneralController, AboutCommandController>
      (ControllerNames.AboutCommandController, new ContainerControlledLifetimeManager());
    Container.RegisterType<IGeneralController, HelpCommandController>
      (ControllerNames.HelpCommandController, new ContainerControlledLifetimeManager());
    Container.RegisterType<IGeneralController, SettingsCommandController>
   	(ControllerNames.SettingsCommandController, 
	new ContainerControlledLifetimeManager());
}

These controllers add command binding to the main window and now we are able to process these command events like in the HelpCommandController:

public void Run()
{
    // Bind "Help" command to the MainWindow
    CommandBinding binding = new CommandBinding
	(GlobalCommands.HelpCommand, Command_Executed, Command_CanExecute);
    Application.Current.MainWindow.CommandBindings.Add(binding);
}

private void Command_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
    e.CanExecute = true;
    e.Handled = true;
}

private void Command_Executed(object sender, ExecutedRoutedEventArgs e)
{
    MessageBox.Show("HELP!!!");
}

That's it then.

Skins

It is good practice to keep application resources (brushes, styles, control templates) in one place. For that purpose, I created CWA.ResourceLibrary project. The main application makes reference to it in the ResourceDictionary element of the App.xaml file:

<Application x:Class="CompositeWpfApp.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="/CWA.ResourceLibrary;
				component/Skins/DefaultSkin.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

DefaultSkin.xaml contains some brushes and a reference to another, style-independent resource file - Resources.xaml.

If we wish to change the appearance of the controls dynamically, we have to fulfill two conditions. First, we should use DynamicResource references for skin-depending properties like in the sample below:

<Border Background="{DynamicResource ServerSelectorBackgroundBrush}" 
					BorderThickness="0,1,0,0"
     BorderBrush="{DynamicResource ServerSelectorBorderBrush}">

The second condition is a bit tricky. If you look at DefaultSkin, you will notice that it is derived from the ResourceDictionary class. To do that, I recommend you first create a UserControl and then change its base class to ResourceDictionary. And don't forget about UserControl element in the XAML!

Now we can change the skin. SkinCommandController is responsible for that:

private void ChangeSkin(string skinName)
{
    if (string.IsNullOrEmpty(skinName))
        throw new ArgumentException("Skin Name is empty.", "skinName");

    if (string.Compare(skinName, currentSkinName, true) != 0)
    {
        // Change the skin if it differs from the current one
        Application.Current.Resources.MergedDictionaries[0] = 
				SkinFactory.GetResourceDictionary(skinName);
        currentSkinName = skinName;
    }
}

It replaces the application resource dictionary with the new one, returned by the SkinFactory. That's the case when inheritance of DefaultSkin from ResourceDictionary comes in handy:

public static ResourceDictionary GetResourceDictionary(string skinName)
{
    if (string.IsNullOrEmpty(skinName))
        throw new ArgumentException("Skin Name is empty.", "skinName");

    if (skinTable.ContainsKey(skinName))
        return (ResourceDictionary)skinTable[skinName];

    ResourceDictionary resourceDictionary = null;

    switch (skinName)
    {
        case SkinNames.DefaultSkin:
            resourceDictionary = (ResourceDictionary)new DefaultSkin();
            break;

        case SkinNames.BlueSkin:
            resourceDictionary = (ResourceDictionary)new BlueSkin();
            break;

        default:
            throw new ArgumentException("Invalid Skin Name.");
    }

    if (resourceDictionary != null)
    {
        skinTable.Add(skinName, resourceDictionary);
    }

    return resourceDictionary;
}

Now, when a user selects "Blue" style, he or she will see new colors:

Skins

My sample application is very simple. In the "real" world, skinning is very non-trivial task - just glance at Menu.xaml file.

References

History

You must Sign In to use this message board.
 
 
Per page   
 FirstPrevNext
GeneralNice Article
BlueFusion
11:43 4 Jan '10  
This is very good example of how a solution can be put together with the CAL. It is easy to find tutorials and articles that address the various patterns that are found in the library, but I have found the resources on solution/project structure to be somewhat sparse.

Also, I found the method for handling skins particularly useful.

Thanks for posting this Eugene
GeneralVery Helpful - THANKS!
Dana LeBeau
18:27 7 Nov '09  
This is one of the few samples that uses the CAL post-Feb'09 drop and actually makes sense to me. I'm still trying to get my head around all the patterns but I'm finding the MVP pattern as you present it here really easy to get started with.

I see the potential for some of the code-behinds (xyzView.xaml.cs) becoming unwieldy but I think it might just be a matter of keeping the modules simple and maybe breaking them into separate modules if they start becoming too sophisticated.

If people don't take the opportunity to dig into this code, they may not realize the powerful potential with this as a starting point for a general line-of-biz app. Those "server" tabs can really be any dynamically loaded module instance hosted in a tab.

Then those buttons (prototyped here as "document", "users", and "security") can be thought of as "sub-modules" that are also dynmically loaded and as general or specific as needed for the parent module.

I'm looking forward to working more with this.

Thanks for posting your work!!!
GeneralThank you
yem583
2:33 5 Aug '09  
This article provides a wealth of information in a very clear and concise manner. Just enough information to follow the concepts demonstrated in the sample project/code.

Thanks for saving me several hours/days of project structure refactoring down the road.

JK
GeneralGreat article
MarkGwilliam
0:36 25 Jul '09  
Thanks for submitting this article!
You've obviously put in a lot of effort to share this with the community Big Grin
GeneralRe: Great article
Eugene Pankov
9:59 25 Jul '09  
Thank you Mark!
I just wanted to popularize a good technology. And, besides, to find out how it's working Big Grin
GeneralWPF browser application, CAL, Publish, Permissions
ksureshreddy27
1:27 3 Jun '09  
Hi,
I have been working on developing a WPF browser application using Composite application library.
On publishing it to the IIS its throwing Permission errors.
Here i have strong named all the modules of my application and also the CAL.

If any one can provide the steps to follow for publishing the Composite application library based WPF browser application.

Thanks in advance
GeneralMohammed Salah
ms_soft89
3:46 16 Mar '09  
Excellent Article , thanx

Most software today is very much like an Egyptian pyramid with millions of bricks piled on top of each other, with no structural integrity, but just done by brute force and thousands of slaves

GeneralSome input
GerhardKreuzer
7:06 10 Mar '09  
Hi Eugene,

I am just involved in the same process like you.

Here some suggestions, maybe wrong, I only fly over the text and dont have a look at the sources.

As far as I can see, each presenter/controller has only one view. But whats about a controller/presenter which want to act like a wizard?
Whats about the idea, to set a reference to some region, when the controller/presenter is created? The code inside the controller/presenter can switch around the active view as he likes. At the end of the processing a global event is generated and some navigator modul catches this event and navigate to some other controller/presenter and his view(s).

This scenario probably offers more advantages if you want to came up with new modules which want to integrate in the system like plugins.

You use MVP, did you have a look at MVVM? I think, its suits much better for WPF centric (and Silverlight?) applications than MVP. If you take advantage of the two way data binding mechanism, controller and presenter can be one object. There are some good articels around here by Josh Smith.

Happy coding

Gerhard
GeneralMy vote of 2
Kjetil Klaussen
21:29 9 Mar '09  
I believe you mix up MVP and PresentationModel a bit here; The two patterns are really two different patterns.
Generalif only it could support vs2005 .net3.0
Member 4364321
18:24 9 Mar '09  
But it only support vs2008 + .net3.5
GeneralCool
Patrick Blackman
2:19 3 Mar '09  
very cool, finally got it (WFP).
GeneralCongrats
Sarafian
22:32 2 Mar '09  
You are the first one, that I see addressing this problem.
Congratulations.

I've been developing for my company a framework for modular, N-tier, smart client and language independent application.
It is very defficult and obviously far from the mainstream 5 step easy microsoft implementations.

Congratulations again.


Last Updated 2 Mar 2009 | Advertise | Privacy | Terms of Use | Copyright © CodeProject, 1999-2010