Click here to Skip to main content
Click here to Skip to main content

Building professional .NET applications using asynchronous message communication based on MCM-Framework.Net

, 4 Jan 2013 CPOL
Rate this:
Please Sign up or sign in to vote.
How to effectively build modular .NET applications of any size using messages, commands and components of MCM-Framework.Net?

Introduction 

Building a simple .NET application is easy. You create an UI and a model. The UI updates the model, the model notifies about its change using events. The interaction works synchronously, in a single thread. 

A professional application should offer much more: 

  • long lasting operations cannot freeze the UI
  • the application must be maintenaned over years
  • test environment must be provided

Some applications have additional requirements like:

  • plug-in architecture 
  • retry, abort & timeouts
  • undo & transactions
  • tutorial mode 
  • macro recording

MCM (Message-Command-Message) is an open source framework providing the basics for a professional application.

The framework is more a philosophy than a software. It provides only basic infrastructure. Advanced scenarios should be created as an additional layer according to the user needs. This makes the learn effort low and offers great customization possibilities. There are no special requirements for the framework and you can use it parallel with your existing system.

Additionally there is no XML configuration in the framework. Almost all classes use generics. Creating source code is supported by Intellisense - contextual help system of Visual Studio. 

In this article I'll show you first steps with MCM-Framework.Net and a WinForms application but this pattern can be used in any .NET API.

Basic elements 

The most important elements of MCM-Framework.Net are messages, commands and components.

Message   

A message could be any .NET object. The message contains only information, no logic. Messages are sent asynchronously using a message channel. After creating the message its data should not change anymore. Therefore storing service references or global application variables in the message should be avoided. 

Command  

A command is responsible for a single task or a single job (eg. load users, connect, get weather). Commands can be combined and executed one after another. They can be repeated at a later time (repeat, abort pattern). Their order can be reverted (undo) or stored (macro). 

Each command has its argument and a result. The command should not access external services and components. The whole environment required to execute a command should be defined in the command argument. Due to this requirement it's possible to test commands using unit tests.

Component   

A component can be any .NET object. The component is a controller or a mediator between the messages and the commands. It handles incoming messages, executes commands, actualizes its state and generates response messages. UI controls can also be components.

Basic operations

Loose coupling

Due to the message communication the system components don't have to know anything about each other. They only have to understand the messages. Reducing dependencies between the system components simplifies the maintenance. New components can be added, old ones can be removed. All this without redesigning the system architecture. This programming model offers great extensibility of plug-in systems. 

Env.ComponentContainer = new MyComponentContainer();
Env.ComponentContainer.Add(new Component1());
Env.ComponentContainer.Add(new Component2());
Env.ComponentContainer.Add(this); 

Each component can send messages to a message channel. The message channel broadcasts messages to its  subscribers.  

var message = new HelloRequestMessage("John");
Env.ComponentContainer.Messages.Post(message); 

Each component is examined, after adding it to the component container, if it contains methods marked with MessageSubscriberAttribute. Only these methods are added to the subscriber list of the message channel. 

[MessageSubscriber("Messages")]
private void handleHelloRequestMessage(HelloRequestMessage m) { } 

 In case the method must be invoked in the UI thread, an overloaded attribute constructor is used. 

[MessageSubscriber("Messages", UIThreadSynchronizationMode.PostAsynchronousInUIThread)]
private void handleHelloRequestMessage(HelloRequestMessage m) { }

Asynchronous work

Components are not workers. The work is done by commands. Components create and execute commands. A command can be executed synchronously or asynchronously using CommandAsyncExecutor.

var commandExecutor = new CommandAsyncExecutor<HelloCommand>();
commandExecutor.ExecuteCompleted += on_commandExecutor_ExecuteCompleted;

var cmd = new HelloCommand("John");
commandExecutor.ExecuteAsync(cmd);

The CommandAsyncExecutor.ExecuteCompleted event informs about the command result or any exception occured during the execution. The component can send a response message and update its state after the execution is completed.

Testing

Each command has an argument. The argument contains complete environment for the command. The environment contains parameters needed for the computation and services. Representing services as abstract classes or interfaces and replacing them with mocks makes testing easy. 

var arg = new HelloCommandArgument();
arg.Name = "John";
arg.TimeProvider = new ConstTimeProvider(2012, 12, 1);
var cmd = new HelloCommand(arg);
commandExecutor.ExecuteAsync(cmd);

Debugging  

Trasporting messages using message channels is a great advantage for the debugging. The message flow is centralized. You can attach a listener to the message channel and intercept every incoming message. Debugging components can be added additionally to the component container. Specially prepared messages with extended debugging information can be injected to the message channel forcing the system to behave in a particular way.

Retry, Abort & Timeouts  

In case there is no expected response message the request message can be resent after some time.  The complicated for/foreach loops and counters are no more needed.

Undo and Transactions  

Commands can store changes they make. Command descendants can offer additional Commit() and Rollback() methods. The order the commands are executed can be reversed.  

Macro recording  

Incoming messages can be stored and resent at a later time.

Tutorials   

Sending messages simulating user interaction makes tutorials easy. The UI updates itself according to the incoming messages.

Hello World with MCM-Framework.Net

You can read the following paragraph or you can watch the video tutorial.

Creating the folder structure  

For better readability the following folder structure and naming conventions are recommended.  

  • Commands - all commands should have the suffix Command
  • Components - all components should have the suffix Component
  • Messaging - in most cases messages should have the suffix RequestMessage or ResponseMessage. However in some cases other suffixes are better, e.g., MyPropertyChangedMessage, EnvironmentInitializedMessage.    
  • Services - contains global providers, managers, handlers, persistors etc.

In case there are nested namespaces with additional use cases (eg. Editing, Configuration, UserManagment), the above folder structure should be maintained in the nested namespaces. This way you can group the functionality for each use case.

Recreating the folder structure of the main project in the test project simplifies testing.  

Defining Environment 

Create a static class Env in the root folder. This class will be accessible as a singleton for every component and user control of our application. 

public static class Env
{
    public const string MessagesChannelName = "MyMessages";
}

Creating ComponentContainer 

Make reference to Polenter.Mcm.dll and create your own MyComponentContainer in the Components namespace deriving it from Polenter.Mcm.ComponentContainer.

Create a property named Messages as an instance of Polenter.Mcm.MessageChannel. In some scenarios creating more than one message channel (eg. important system messages, error handling) could be recommended. In this simple case one message channel will do.

public class MyComponentContainer : ComponentContainer
{
    private MessageChannel _messages1;

    public MessageChannel Messages
    {
        get
        {
            if (_messages1 == null)
            {
                _messages1 = new MessageChannel(Env.MessagesChannelName);
            }
            return _messages1;
        }
    }
}  

Define the property Env.Components.

    public static class Env
{
    (...)
    public static MyComponentContainer Components { get; set; }
}

Instantiate the Components property and dispose it. In case of a WinForms application the creatingComponents() method must be invoked after the SynchronizationContext of the UI thread is created. The right place is the constructor of the main form or its OnLoad() method. 

The disposeComponents() method terminates working thread of the message channel. Without disposing the ComponentContainer the application could not be closed.

public partial class Form1 : Form 
{   
    (...)
    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);
        if (DesignMode)
        {
            return;
        }
        createComponents();
    }

    protected override void OnClosed(EventArgs e)
    {
        disposeComponents();
        base.OnClosed(e);
    }

    private void disposeComponents()
    {
        Env.Components.Dispose();
    }

    private void createComponents()
    {
        Env.Components = new MyComponentContainer();
        (...)
    }
    (...)
} 

Creating Messages  

Create the HelloRequestMessage in the namespace Messaging. It contains only one property, the name. 

public class HelloRequestMessage
{
    public HelloRequestMessage(string name)
    {
        Name = name;
    }

    public string Name { get; private set; }
}

Create HelloResponseMessage in the same namespace. The response message is a result of an asynchronous operation. Therefore it should be derived from Polenter.Mcm.AsyncOperationResponseMessage which offers properties Error and Cancelled

public class HelloResponseMessage : AsyncOperationResponseMessage
{
    public HelloResponseMessage(Exception error) : base(error)
    {
    }

    public string Response { get; set; }
}

Creating Command

Create HelloCommand in the Commands namespace. Derive the command from Polenter.Mcm.Command<TArgument, TResult>. Override the abstract method ExecuteCore().

public class HelloCommand : Command<string, string>
{
    public HelloCommand(string argument) : base(argument)
    {
    }

    protected override void ExecuteCore()
    {
        Thread.Sleep(2000);

        if (string.IsNullOrEmpty(Argument))
        {
            throw new InvalidOperationException("Please enter your name.");
        }

        Result = string.Format("Hello {0}", Argument);
    }
}

For test purposes simulate a long lasting operation with Thread.Sleep(). Additionally check if the command argument is not empty.

Creating Component 

Create HelloComponent in the Components namespace. Create _helloCommandExecutor as an instance of Polenter.Mcm.CommandAsyncExecutor<TCommand> in the component constructor.  

public class HelloComponent
{
    private readonly CommandAsyncExecutor<HelloCommand> _helloCommandExecutor;

    public HelloComponent()
    {
        _helloCommandExecutor = new CommandAsyncExecutor<HelloCommand>();
        _helloCommandExecutor.ExecuteCompleted +=
            on_helloCommandExecutor_ExecuteCompleted;
    }

    private void on_helloCommandExecutor_ExecuteCompleted(object sender, CommandEventArgs<HelloCommand> e)
    {
    }
}

Add the component to Env.Components. In our example it will be made in the Form1.createComponents() method. In the real life this could be done using MEF (Managed Extensibility Framework). Please refer to the video tutorial for more details.

public partial class Form1 : Form
{
    (...)
    private void createComponents()
    {
        Env.Components = new MyComponentContainer();
        Env.Components.Add(new HelloComponent());
        Env.Components.Add(this);
    }
}

Create the method  handleHelloRequestMessage() and mark it with the attribute Polenter.Mcm.MessageSubscriber. After the component is added to the component container its methods are examined using reflection. Each method marked with this attribute is added to the subscriber list of Env.Components.Messages and is invoked each time the HelloRequestMessage is posted to this particular message channel. 

public class HelloComponent
{
    (...)
    [MessageSubscriber(Env.MessagesChannelName)]
    private void handleHelloRequestMessage(HelloRequestMessage m)
    {
        var cmd = new HelloCommand(m.Name);
        _helloCommandExecutor.ExecuteAsync(cmd);
    }
}

The method handleHelloRequestMessage() creates a new instance of HelloCommand and executes it asynchronously using the CommandAsyncExecutor.ExecuteAsync() method. The CommandAsyncExecutor uses ThreadPool for executing. After the execution is completed it invokes the CommandAsyncExecutor.ExecuteCompleted event. 

In the event delegate create an instance of HelloResponseMessage and post this message to Env.Components.Messages.  

public class HelloComponent
{
    (...)
    private void on_helloCommandExecutor_ExecuteCompleted(object sender, CommandEventArgs<HelloCommand> e)
    {
        var message = new HelloResponseMessage(e.Command.Error);
        if (!message.HasError)
        {
            message.Response = e.Command.Result;
        }

        Env.Components.Messages.Post(message);
    }
}

Creating UI  

Create a simple UI with nameTextBox, responseTextBox and the workButton.


Subscribe the OnClick event of the workButton. Create there an instance of HelloRequestMessage and post it to Env.Components.Messages.

public partial class Form1 : Form
{
    (...)
    void on_workButton_Click(object sender, EventArgs e)
    {
        var message = new HelloRequestMessage(nameTextBox.Text);
        Env.Components.Messages.Post(message);
    }
}  

Updating the UI requires subscribing messages from the message channel. It should be made using the UI thread, Therefore we use the overloaded constructor of MessageSubscriberAttribute with UIThreadSynchronizationMode.PostAsynchronousInUIThread as parameter.  

    public partial class Form1 : Form
{
    (...)
    [MessageSubscriber(Env.MessagesChannelName,UIThreadSynchronizationMode.PostAsynchronousInUIThread)]
    private void handleHelloResponseMessage(HelloResponseMessage m)
    {
        workButton.Enabled = true;
        responseTextBox.Text = string.Empty;

        if (m.HasError)
        {
            MessageBox.Show(m.Error.Message, Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error);
            return;
        }

        responseTextBox.Text = m.Response;
    }

    [MessageSubscriber(Env.MessagesChannelName, UIThreadSynchronizationMode.PostAsynchronousInUIThread)]
    private void handleHelloRequestMessage(HelloRequestMessage m)
    {
        workButton.Enabled = false;
        responseTextBox.Text = "Working...";
    }
}

The method handleHelloRequestMessage() disables the UI preventing the system from starting the work again. The handleHelloResponseMessage() enables UI and shows an error message if an error has occured otherwise it updates the responseTextBox.

The both methods are added to the subscriber list of the message channel as a result of the Env.Components.Add(this) executed in the createComponents() method.

Understanding Hello World

After pressing F5 in Visual Studio the application is started. The Form1.OnLoad() method is executed, This invokes the method createComponents(). First the component container is created. Adding components with the Env.Components.Add() method examines each added component and adds all marked with MessageSubscriberAttribute methods to the subscriber list of Env.Components.Messages.

After pressing the workButton a HelloRequestMessage is generated and posted to Env.Components.Messages. The message channel broadcasts this message to all subscribers.

One of the subscribers - Form1.handleHelloRequestMessage() - disables UI preventing the program from starting the work again.

Another subscriber - HelloComponent.handleHelloRequestMessage() - creates the HelloCommand and executes it asynchronously using CommandAsyncExecutor. After the command is executed the event ComandAsyncExecutor.ExecuteCompleted is invoked. The event delegate creates HelloResponseMessage and posts it again to the message channel.

The Form1.handleHelloResponseMessage() waits for the response message and updates the UI according to the message content.

The methods Form1.handleHelloRequestMessage() and Form1.handleHelloResponseMessage() must be invoked in the UI thread. Therefore they are marked with the overloaded constructor of the MessageSubscriberAttribute with ThreadSynchronizationMode.PostAsynchronousInUIThread as parameter.

References 

mcmframework.codeplex.com

Video tutorial

History      

  • Jan'4th 2013 - extended introduction
  • Jan'2nd 2013 - first release

License

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

Share

About the Author

Pawel idzikowski
Software Developer (Senior) Polenter - Software Solutions
Germany Germany
I'm C# developer from Cologne, Germany. Here I owe a small software company. My hobby is general optimization - natural talent of all lazy guys Wink | ;-)

Comments and Discussions

 
-- There are no messages in this forum --
| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.1411022.1 | Last Updated 4 Jan 2013
Article Copyright 2013 by Pawel idzikowski
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid