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 <code>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