Click here to Skip to main content
16,018,653 members
Articles / Desktop Programming / WPF
Article

Composite Application Library in WPF Application

Rate me:
Please Sign up or sign in to vote.
4.82/5 (30 votes)
2 Mar 2009CPOL8 min read 97.2K   3.1K   110   14
WPF application built on the Composite Application Library. How to start, organize solution projects, distribute resources and build complex UI using the Presentation Model pattern.

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:

  • to study how to load modules and change views dynamically
  • to separate presentation from logic using Model-View-Presentation Model (MVP) pattern
  • to find a way to display and process general menu items (like "Help") and module-specific menu items
  • to share resources in such a way, that modules can be developed independently and styling with skins would be easy

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:

  • CWA.ResourceLibrary - Contains shared resources: images, resource dictionaries, skins
  • CWA.UIControls - Contains custom UI controls and general menu items
  • CWA.Infrastructure - Contains classes and interfaces that should be easily accessible to all projects

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
<?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:

C#
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:

XML
<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:

C#
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:

C#
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):

C#
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:

C#
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):

C#
/// <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:

  • we have to be able to change menus dynamically
  • general commands should be processed in the Shell project
  • view-dependent commands should be processed in the corresponding Presentation Model class
  • we should not duplicate general menu items in each module

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:

C#
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:

C#
Menu = container.Resolve<DocumentsMainMenuView>();

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

XML
<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:

C#
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:

C#
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:

C#
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:

XML
<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:

XML
<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:

C#
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:

C#
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

  • 1st March, 2009: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Latvia Latvia
Jevgenij lives in Riga, Latvia. He started his programmer's career in 1983 developing software for radio equipment CAD systems. Created computer graphics for TV. Developed Internet credit card processing systems for banks.
Now he is System Analyst in Accenture.

Comments and Discussions

 
GeneralWell done Pin
TechnicalAli3-Jun-11 5:39
TechnicalAli3-Jun-11 5:39 
GeneralThanks for the sample Pin
kfrosty12-Mar-10 3:41
kfrosty12-Mar-10 3:41 
GeneralNice Article Pin
BlueFusion4-Jan-10 10:43
BlueFusion4-Jan-10 10:43 
GeneralVery Helpful - THANKS! Pin
Dana LeBeau7-Nov-09 17:27
Dana LeBeau7-Nov-09 17:27 
GeneralThank you Pin
yem5835-Aug-09 1:33
yem5835-Aug-09 1:33 
GeneralGreat article Pin
MarkGwilliam24-Jul-09 23:36
MarkGwilliam24-Jul-09 23:36 
GeneralRe: Great article Pin
Jevgenij Pankov25-Jul-09 8:59
Jevgenij Pankov25-Jul-09 8:59 
GeneralWPF browser application, CAL, Publish, Permissions Pin
ksureshreddy273-Jun-09 0:27
ksureshreddy273-Jun-09 0:27 
GeneralMohammed Salah Pin
ms_soft8916-Mar-09 2:46
ms_soft8916-Mar-09 2:46 
GeneralSome input Pin
GerhardKreuzer10-Mar-09 6:06
GerhardKreuzer10-Mar-09 6:06 
GeneralMy vote of 2 Pin
Kjetil Klaussen9-Mar-09 20:29
Kjetil Klaussen9-Mar-09 20:29 
Generalif only it could support vs2005 .net3.0 Pin
_kummer9-Mar-09 17:24
_kummer9-Mar-09 17:24 
GeneralCool Pin
Patrick Blackman3-Mar-09 1:19
professionalPatrick Blackman3-Mar-09 1:19 
very cool, finally got it (WFP).
GeneralCongrats Pin
aSarafian2-Mar-09 21:32
aSarafian2-Mar-09 21:32 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.