Click here to Skip to main content
15,861,168 members
Articles / DevOps / Testing

Composite Application Reloaded

Rate me:
Please Sign up or sign in to vote.
4.88/5 (38 votes)
11 May 2011CPOL12 min read 116.2K   1.5K   95   44
A much simpler composite application library.

Introduction

As I was starting a new project I was looking for a library that will help me write a composite application, i.e., an application with a main shell (window) and pluggable extensions (DLLs / modules) to be added to it dynamically. A library like Prism, but hopefully much simpler.

Many pieces of the puzzle could already be found elsewhere. The application had to have a clear separation between data and view, i.e., an MVVM approach. Services had to be linked automatically with something like MEF. Data validation should be automatic (thanks to ValidationAttribute(s)).

But improvement had to be made regarding disconnected messaging and view resolution. About the view resolution, i.e., the process of finding the appropriate view to represent a given business data object, I wanted to just tag the view with an attribute, such as DataView(typeof(BusinessData1)), and let the library take care of the rest. This is where this library came from.

What's new?

Performance improvements (in Composition.GetView()), Commands simplification, POCO Bindings, WP7 support, and MEF for WP7. Here is the CodePlex project.

Table of contents

Samples

Image 1

To test if my library was up to its goal, I have ported three samples to it. In all cases, I was able to reduce the application size and maintain functionality.

  • Josh Smith MVVM Demo. This is the best sample, as it is small and simple yet it covers almost all the features of the library (after some modifications) and is a real composite application. I was able to get rid of the hand written validation code and use ValidationAttribute instead. And I tweaked the MainWindow and App classes to make it a composite application, and used the DataControl in the TabItem to bind multiple controls to the same model with different views.
  • Prism’s main sample, the StockTraderApp project (huge sample). I removed the presenters (code which were used to bind Views and View Models, now replaced with a call to Composition.GetView() and DataControl), the EventAggregator, and custom prism events (replaced by the Notifications static method). The most challenging and interesting part was to get rid of the RegionManager and replace it with the IShellView which explicitly exposes the area of the shell that can be used and gets rid of the RegionManager’s magic string approach.
  • MEFedMVVM library demo. The application is relatively simple but it makes extensive usage of design time support, and the design time experience is a joy to behold.

The unit tests illustrate how the most important features are working (i.e., Composition, Notification, ViewModelBase, and Command).

About MEF and MVVM

Josh Smith has talked about MVVM extensively on MSDN already. But, to summarize, MVVM is a View Model approach where all the logic and information is in the models. And by all, I mean all, to the extent of including the selected element of a list, or the position of the caret, if need be.

In MVVM, the view is nothing more than some declarative XAML (and possibly some UI specific code if need be, with no business code at all, just pure UI logic). And because business data might not express all the information present in a view (such as selected area, incorrect value in a text box, etc...), business models might be wrapped in View Models. View Models are business model wrappers with a few extra, View-friendly properties. This offers various advantages including increased testability, better separation of concerns, and possibility to have an independent team for business data and UI.

MEF, or Managed Extensibility Framework, solves the problem of passing services and other shared objects around in a very neat way. It also enables to find interface implementations easily. Basically “consumer” objects declare what they need with the Import attribute, like so:

C#
[Import(typeof(ISomeInterface)]
public ISomeInterface MySome { get; set; }

Somewhere else in the code, the exporting object is declared with the Export attribute.

C#
[Export(typeof(ISomeInterface))]
public class SomeInterfaceImplentation : ISomeInterface
{ /** */ }

Remark: Properties and methods can be exported. Import can be a single object (Import) or many (ImportMany). I strongly recommend that you read the MEF documentation. To find the implementation for all imports needed by your objects, there are two actions to be done:

  1. At the start of the program, a “catalog” of types for MEF should be initialized from a list of types, assemblies, and / or directories, which will be where MEF will look for locating exports. It’s where you opt-in for the modules of interest. With this library, you’ll call the method Composition.Register(), as shown below.
  2. You “compose” the objects that need to be resolved (i.e., which contain imports). With this library, you’ll use the method Composition.Compose().

MEF contains various tags to control whether instances are shared or not, whether multiple implementations of an export are valid or not. Again, this is covered in the documentation.

What does a composite application look like

A composite application is an application where there is a well-known top level UI element, typically a window for a desktop application or a Page for Silverlight. This top level UI element is called the “Shell”.

The Shell contains multiple areas where pluggable content will be hosted. Content that is not defined by the Shell but in modules that are loaded dynamically (with MEF, for example).

For example, in the sample below, a Shell is defined with two areas:

  • A list box, showing a single list of documents.
  • An ItemsControl (a TabControl) which can contain numerous items.
XML
<Window 
    x:Class="DemoApp.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <ListBox
            Grid.Column="0"
            Items="{Binding Documents}"
            Header="Documents"
            />
        <TabControl 
            Grid.Column="1"
            IsSynchronizedWithCurrentItem="True" 
            ItemsSource="{Binding Workspaces}" 
            ItemContainerStyle="{StaticResource ClosableTabItemStyle}"/>
    </ Grid >
</Window>

Remark: Don’t worry too much about the Shell when you create it; it is relatively easy to add or move areas later in the development, if need be. But it’s harder to remove an area in use!

Once a Shell has been defined, an interface to it should be exposed via a common library. It could be a IShellInterface (see the IShellView in the StockTraderApp, for example) or its View Model (see MainViewModel in DemoApp, for example), or a combination of the two!

As an example, here is the IShellView interface from the StockTraderApp sample:

C#
public enum ShellRegion
{
    MainToolBar,
    Secondary,
    Action,
    Research,
    // this will set the currently selected item
    Main,
}
public interface IShellView
{
    void ShowShell(); // this will show the shell window
    void Show(ShellRegion region, object data);
    ObservableCollection<object> MainItems { get; }
}

Once a shell has been defined, the composite application can be written. Four short steps are involved:

  1. Define the DLLs that are to be dynamically loaded
  2. Create the Shell (or skip and import it in 3)
  3. Export the Shell and import the modules
  4. Start the app / initialize the modules

For example, here is what the StockTraderApp simplified App code could look like:

C#
public partial class App
{
    public App()
    {
        // 1. Opt-in for the DLL of interest (for import-export resolution)
        Composition.Register(
            typeof(Shell).Assembly
            , typeof(IShellView).Assembly
            , typeof(MarketModule).Assembly
            , typeof(PositionModule).Assembly
            , typeof(WatchModule).Assembly
            , typeof(NewsModule).Assembly
            );
        // 2. Create the shell
        Logger = new TraceLogger();
        Shell = new Shell();
    }
 
    [Export(typeof(IShellView))]
    public IShellView Shell { get; internal set; }
 
    [Export(typeof(ILoggerFacade))]
    public ILoggerFacade Logger { get; private set; }
 
    [ImportMany(typeof(IShellModule))]
    public IShellModule[] Modules { get; internal set; }
 
    public void Run()
    {
        // 3. export the shell, import the modules
        Composition.Compose(this);
        Shell.ShowShell();
 
        // 4. Start the modules, they would import the shell
        // and use it to appear on the UI
        foreach (var m in Modules)
            m.Init();
    }
}

Core features

The library grew quite a lot from its humble beginnings. It consists of two main parts. Features which are critical to MVMM and composite development, and optional features which are useful additions.

The central class for most features of this library is the Composition class. It also contains two important properties, Catalog and Container, which are used by MEF to resolve imports and exports. You need to fill the Catalog at the start of the application with Composition.Register(), for example:

C#
static App() // init catalog in App’s static constructor
{
    Composition.Register(
        typeof(MapPage).Assembly
        , typeof(TitleData).Assembly
        , typeof(SessionInfo).Assembly
    );
}

Later, service imports and exports can be solved with MEF by calling Composition.Compose().

Composition GetView

When an MVVM development pattern is followed, we write the business model and / or View Models and Views for these data models. Typically, these Views will only consist of “XAML code”, and their DataContext property will be the business model. Often, MVVM helper libraries will provide some ways of finding and loading these Views.

In this library, the Views need to be tagged with a DataViewAttribute which specifies for which model type this View is for:

C#
[DataView(typeof(CustomerViewModel))]
public partial class CustomerView : UserControl
{
    public CustomerView()
    {
        InitializeComponent();
    }
}

Then, from a data model, you can automatically load the appropriate View (and set its DataContext) with a call to Composition.GetView(), for example:

C#
public void ShowPopup(object message, object title)
{
    var sDialog = new MsgBox();
    sDialog.Message = Composition.GetView(message);
    sDialog.Title = Composition.GetView(title);
    sDialog.Show();
}

Often models are not displayed as a result of some method call but simply because they are an item in an ItemsControl or the content of a ContentControl. In this case, the DataControl control can be used in XAML to display the item by calling Composition.GetView().

Remark: It also brings a DataTemplate like functionality to Silverlight.

Because we use a View-Model approach, the same data model can be shown in multiple places at the same time, hence Composition.GetView(), DataViewAttribute, and the DataControl have an optional location parameter.

In the example below, the same UserViewModel instance (subclass of WorkspaceViewModel) is used to display both the TabItem header and the content using different location parameters (note: location is not set, i.e., it is null, in the second template).

XML
<Style x:Key="ClosableTabItemStyle" TargetType="TabItem" 
             BasedOn="{StaticResource {x:Type TabItem}}">
    <Setter Property="HeaderTemplate">
        <Setter.Value>
            <DataTemplate>
                <g:DataControl Data="{Binding}" Location="header"/>
            </DataTemplate>
        </Setter.Value>
    </Setter>
    <Setter Property="ContentTemplate">
        <Setter.Value>
            <DataTemplate>
                <g:DataControl Data="{Binding}"/>
            </DataTemplate>
        </Setter.Value>
    </Setter>
</Style>

Both Views were defined like this (note: the first View is the default View, i.e., location is not set, it is null):

C#
[DataView(typeof(CustomerViewModel))]
public partial class CustomerView : System.Windows.Controls.UserControl
{
    public CustomerView()
    {
        InitializeComponent();
    }
}
[DataView(typeof(WorkspaceViewModel), "header")]
public partial class CustomerHeaderView : UserControl
{
    public CustomerHeaderView()
    {
        InitializeComponent();
    }
}

Validation and ViewModelBase

Inspired by Rob Eisenberg’s talk, I created a ViewModelBase class which implementd two important interfaces for WPF development: INotifyPropertyChanged and IDataErrorInfo.

The INotifyPropertyChanged implementation is strongly typed (refactor friendly):

C#
public class Person : ViewModelBase
{
    public string Name
    {
        get { return mName; }
        set
        {
            if (value == mName)
                return;
            mName = value;
            OnPropertyChanged(() => Name); // See, no magic string!
        }
    }
    string mName;
}

The IDataErrorInfo interface allows the WPF bindings to validate the properties they are bound to (if NotifyOnValidationError=true). The implementation in ViewModelBase validates the properties using ValidationAttribute(s) on the properties themselves. For example:

C#
public class Person : ViewModelBase
{
    [Required]
    public string Name { get; set; }
 
    [Required]
    public string LastName { get; set; }
 
    [OpenRangeValidation(0, null)]
    public int Age { get; set; }
 
    [PropertyValidation("Name")]
    [PropertyValidation("LastName")]
    [DelegateValidation("InitialsError")]
    public string Initials { get; set; }
 
    public string InitialsError()
    {
        if (Initials == null || Initials.Length != 2)
            return "Initials is not a 2 letter string";
        return null;
    }
}

The example above also illustrates some of the new ValidationAttribute subclasses provided in this library, in the Galador.Applications.Validation namespace, i.e.:

C#
ConversionValidationAttribute
DelegateValidationAttribute
OpenRangeValidationAttribute
PropertyValidationAttribute

A control with an invalid binding will automatically be surrounded by a red border (default style), but the error feedback can be customized as shown in this XAML fragment below, which displays the error message below the validated text:

XML
<!-- FIRST NAME-->
<Label 
    Grid.Row="2" Grid.Column="0" 
    Content="First _name:" 
    HorizontalAlignment="Right"
    Target="{Binding ElementName=firstNameTxt}"
    />
<TextBox 
    x:Name="firstNameTxt"
    Grid.Row="2" Grid.Column="2" 
    Text="{Binding Path=Customer.FirstName, ValidatesOnDataErrors=True, 
          UpdateSourceTrigger=PropertyChanged, BindingGroupName=CustomerGroup}" 
    Validation.ErrorTemplate="{x:Null}"
    />
<!-- Display the error string to the user -->
<ContentPresenter 
    Grid.Row="3" Grid.Column="2"
    Content="{Binding ElementName=firstNameTxt, Path=(Validation.Errors).CurrentItem}"
    />

Disconnected messaging

In a composite application, there is a need for components to send messages to each other without knowing each other. The Notifications class and its static methods are here to solve this problem.

First, a common message type should be defined in a common library; objects can:

  • Subscribe to messages for this type (with the static Subscribe() and Register() methods).
  • Publish messages (with Publish()).
  • Unsubscribe from messages if they are no longer interested in them (with Unsubscribe()).

Remark: The subscription thread is an optional parameter that can be either the original thread, the UI thread, or a background thread.

To illustrate these functionalities, here is a snippet of the code from the Notifications’ unit test class.

C#
public void TestSubscribe()
{
    // subscribe to a message
    Notifications.Subscribe<NotificationsTests, MessageData>(
                  null, StaticSubscribed, ThreadOption.PublisherThread);

    // publish a message
    Notifications.Publish(new MessageData { });

    // unsubscribe to a message below
    Notifications.Unsubscribe<NotificationsTests, MessageData>(null, StaticSubscribed);
}
 
static void StaticSubscribed(NotificationsTests t, MessageData md)
{
    // message handling
}

Arguably, the Notifications.Subscribe() syntax is a bit cumbersome. It’s why an object can also subscribe to multiple message types in one swoop by calling Notifications.Register(this), which will subscribe all its methods with one argument and tagged with NotificationHandlerAttribute, as in:

C#
public void TestRegister()
{
    // register to multiple message type (1 shown below)
    Notifications.Register(this, ThreadOption.PublisherThread);

    // publish a message
    Notifications.Publish(new MessageData { Info = "h" });
}
 
[NotificationHandler]
public void Listen1(MessageData md)
{
    // message handling
}

Commands

To avoid the need for code in the UI, yet handle code triggering controls such as a Button or a MenuItem, WPF (and Silverlight 4) came up with commands (ICommand to be exact). When a button is clicked, the control action is triggered, and if it has a Command property, it will call Command.Execute(parameter), where the parameter is the Control.CommandParameter property.

ViewModels need to expose a Command property whose Execute() method will call one of their methods. For this purpose, there is the DelegateCommand.

A delegate command can be created by passing a method to execute and an optional method to check if the method can be executed (which will enable / disable the command source, i.e., the button). For example:

C#
var p = new Person();
var save = new DelegateCommand(p, () => p.CanSave);

Remark: The second argument is an expression. The command will automatically INotifyPropertyChanged properties and register to the PropertyChanged event to update its CanExecute() status.

Remark: Sometimes you need commands such as “DoAll” as in “CancelAll” or “BuyAll” hence the support of ForeachCommand class, which is an ICommand itself and can watch a list of ICommands, set its status to CanBeExecuted if all its commands can be executed.

Utilities

A few other non-essential features found their way into this library.

UIThread Invoke

There is the Invoker, which assists in running code on the GUI thread. It can be used in both WPF and Silverlight.

C#
public class Invoker
{
    public static void BeginInvoke(Delegate method, params object[] args)
    public static void DelayInvoke(TimeSpan delay, Delegate method, params object[] args)
}

Design time model initialization

There is design time support for the data views. Using the attached property Composition.DesignerDataContext on a data view sets its DataContext at design time:

XML
<UserControl x:Class="MEFedMVVMDemo.Views.SelectedUser"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x=http://schemas.microsoft.com/winfx/2006/xaml
        xmlns:g="http://schemas.galador.net/xaml/libraries"
        xmlns:models="clr-namespace:MEFedMVVMDemo.ViewModels"
        g:Composition.DesignerDataContext="models:SelectedUserViewModel"
        >

These View Models (i.e., DataView’s DataContext) can compose themselves (i.e., call Composition.Compose(this)) to import some other services.

Remark: Having a design time DataContext makes the experience of writing a DataTemplate a whole lot better.

Remark: These View Models can be made aware that they are in Design mode if they implement the IDesignAware interface.

Remark: The services loaded by the models can be different in runtime and design time if they are exported with ExportService instead of Export, like so:

C#
[ExportService(ServiceContext.DesignTime, typeof(IUsersService))]
public class DesignTimeUsersService : IUsersService

POCO Bindings

There are times when you want to synchronize two values. Two classes assist in this endeavor: PropertyPath and POCOBinding. While these classes are not strongly typed, they have a strongly typed public static Create<T> method to help alleviate errors creating them. Here is how two integer properties can be synchronized:

C#
var p1 = new Person();
var p2 = new Person();
POCOBinding.Create<Person, Person, int>(p1, aP => aP.Boss.Age, p2, aP2 => aP2.Age);

And here is how a value can be set through a PropertyPath:

C#
var p1 = new Person();
var pp = PropertyPath.Create<string>(() => p1.Boss.Name);

//.....
pp.Value = "Foo";
Assert.AreEqual(p1.Boss.Name, "Foo");

Remark: PropertyPath implements INotifyPropertyChanged.

Foreach

There are multiple variations of the Foreach class which can be used to observe an IEnumerable or an ObservableCollection and take whatever action is appropriate when something in the collection changes.

Summary

Hopefully this article and the samples it contains will have shown what a composite application architecture looks like and how this library makes it easy to solve the key problems most often met by a composite application:

  • Resolving service dependencies using MEF.
  • Finding a DataView for DataModel with DataControl or Composition.GetView().
  • Implement the common MVVM pattern: the ICommand (with DelegateCommand and ForeachCommand) and disconnected messaging (with Notifications).
  • Implement data binding validation with ValidationAttribute in a subclass of ViewModelBase.

The last version of this library can be found on CodePlex.

Compatibility

This library will work with the Client Profile for .NET 4 and Silverlight 4.

If it needs to be ported to .NET 3.5, there are two obstacles:

  • MEF, which is on CodePlex.
  • And the Validator class, used in the ViewModelBase to validate the properties from the ValidationAttribute(s), i.e., implement the IDataErrorInfo interface. Only two methods need to be re-implemented from the Validator.

References

License

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


Written By
Software Developer (Senior) http://www.ansibleww.com.au
Australia Australia
The Australia born French man who went back to Australia later in life...
Finally got over life long (and mostly hopeless usually, yay!) chronic sicknesses.
Worked in Sydney, Brisbane, Darwin, Billinudgel, Darwin and Melbourne.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Manoj Kumar Choubey15-Jul-12 18:23
professionalManoj Kumar Choubey15-Jul-12 18:23 
GeneralMy vote of 5 Pin
Jay R. Wren17-May-11 3:59
Jay R. Wren17-May-11 3:59 
GeneralRe: My vote of 5 Pin
Super Lloyd20-May-11 4:23
Super Lloyd20-May-11 4:23 
GeneralMy vote of 5 Pin
Rhuros12-May-11 1:40
professionalRhuros12-May-11 1:40 
GeneralRe: My vote of 5 Pin
Super Lloyd12-May-11 4:22
Super Lloyd12-May-11 4:22 
GeneralMy vote of 5 Pin
Nagy Vilmos11-May-11 1:02
professionalNagy Vilmos11-May-11 1:02 
GeneralRe: My vote of 5 Pin
Super Lloyd11-May-11 15:13
Super Lloyd11-May-11 15:13 
GeneralMy vote of 5 Pin
Sarath Reviuri12-Apr-11 23:07
Sarath Reviuri12-Apr-11 23:07 
QuestionCan pluggable extensions call legacy C dlls? Pin
Philip Chang10-Feb-11 13:09
Philip Chang10-Feb-11 13:09 
AnswerRe: Can pluggable extensions call legacy C dlls? Pin
Super Lloyd10-Feb-11 13:20
Super Lloyd10-Feb-11 13:20 
GeneralUsing this library for view composition along side cinch v2 for SL... Pin
ACanadian19-Jan-11 17:45
ACanadian19-Jan-11 17:45 
GeneralRe: Using this library for view composition along side cinch v2 for SL... Pin
Super Lloyd20-Jan-11 10:31
Super Lloyd20-Jan-11 10:31 
GeneralVery nice! Pin
FantasticFiasco17-Nov-10 18:58
FantasticFiasco17-Nov-10 18:58 
GeneralRe: Very nice! Pin
Super Lloyd17-Nov-10 22:17
Super Lloyd17-Nov-10 22:17 
GeneralGreat Article! The next step in MVVM Pin
Your Display Name Here12-Nov-10 8:36
Your Display Name Here12-Nov-10 8:36 
GeneralRe: Great Article! The next step in MVVM Pin
Super Lloyd12-Nov-10 13:57
Super Lloyd12-Nov-10 13:57 
GeneralMy vote of 5 Pin
jsumit7-Nov-10 14:29
jsumit7-Nov-10 14:29 
GeneralRe: My vote of 5 Pin
Super Lloyd8-Nov-10 16:39
Super Lloyd8-Nov-10 16:39 
GeneralMy vote of 5 Pin
neildavidyoung4-Nov-10 16:16
neildavidyoung4-Nov-10 16:16 
GeneralMy vote of 5 Pin
Patrick Klug3-Nov-10 16:10
Patrick Klug3-Nov-10 16:10 
GeneralRe: My vote of 5 Pin
Super Lloyd3-Nov-10 17:50
Super Lloyd3-Nov-10 17:50 
GeneralMy vote of 5 Pin
linuxjr3-Nov-10 5:43
professionallinuxjr3-Nov-10 5:43 
GeneralRe: My vote of 5 Pin
Super Lloyd4-Nov-10 0:32
Super Lloyd4-Nov-10 0:32 
GeneralMy vote of 5 Pin
pyropace2-Nov-10 14:58
pyropace2-Nov-10 14:58 
GeneralRe: My vote of 5 Pin
Super Lloyd2-Nov-10 15:26
Super Lloyd2-Nov-10 15:26 

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.