Introduction
The idea of this article is to build a system that is completely modular. The MEF provides us with a complete framework to achieve this goal, however, how these modules will behave depends on our implementation.
I used the classic Customers, Orders, and Products example. Thus it's easy to understand the purpose of the system and how it works.
I believe that implementations are not best practices, but I tried to get as close as possible to the design patterns. If anyone knows a better implementation, please, instead of simply saying this is not good, give an example of what would be the best implementation. So everyone learns.
Background
MEF allows us to work with the concept of plug-ins. It provides a framework that allows us to specify the points where the application may be extended, exposing modules that can be plugged by external components. Rather than explicitly referencing the components in the application, MEF allows our application to find components at runtime, through the composition of parts, managing what it takes to keep these extensions. Thus, our application does not depend on an implementation, but an abstraction, and can add new features to a program without the need to recompile, or even, without interrupting its execution.
MEF architecture (quite simple...)
To build a pluggable application using MEF, we must follow these steps:
The extension points are the "parts" of our application where we want to allow extensions.
For each extension point set, we need to define an MEF contract that can be a delegate or an interface. In our example, we will create a DLL containing the interface that defines our MEF Contract.
To inform MEF how to manage our plug-ins, we use the Import
and ImportMany
attributes.
To create an extension, we should first implement the MEF Contract to the desired extension point. This extension is known as an MEF Composable Part. To define a class as a Composable Part, we use the attribute Export
.
The Catalog holds, at runtime, a list of all imported Composable Parts. The Catalogs can be of type AssemblyCatalog
, DirectoryCatalog
, TypeCatalog
, and DeploymentCatalog
.
- Define extension points
- Define an MEF Contract for each extension point
- Define a class to manage the extension point
- Create the plug-in
- Define a Catalog
Requirements
For the design of this solution, the following requirements are mandatory:
- The system should be modular, where the modules are independent of one another;
- The modules should be loaded dynamically at application startup;
- The system should allow modules to register their menus in the Host form;
- The system should allow a module to interact with other modules without having to reference the other module;
- The system should look for modules in different folders;
- Each module must implement its own graphical user interface in the form of windows (MDI children);
- The modules must share the same connection to the database.
Using the code
Our solution will consist of five projects:
Project Name |
Project Type |
Description |
ModularWinApp |
Windows Forms |
Host Windows Forms |
ModularWinApp.Core |
Class Library |
Core of the application |
ModularWinApp.Modules.Clients |
Class Library |
Clients Module |
ModularWinApp.Modules.Orders |
Class Library |
Orders Module |
ModularWinApp.Modules.Products |
Class Library |
Products Module |
The solution is based on the following concept:
ModularWinApp.Core contains interfaces and classes that make up the core of the application. This assembly will be referenced by all projects of our solution.
ModularWinApp contains the Host form and the Host app settings. The entry class Program
will create a single instance of the class ModuleHandler
and will initialize the modules and the connection that will be shared across the modules.
The modules will expose two classes, one that implements the IModule
interface, that serves as the Facade that allows other modules to have access to the module commands, and another which implements the IMenu
interface which supplies access to the menu of the module.
The core
The core of the application consists of common interfaces, and concrete classes that implement the features that should be used by the Host and the modules to which the requirements are met.
Interfaces
Interfaces to implement the commands system:
ModularWinApp.Core.Interfaces.ICommand
ModularWinApp.Core.Interfaces.ICommandDispatcher
Interface to implement the database access:
ModularWinApp.Core.Interfaces.IDataModule
Interface to implement the Host Form:
ModularWinApp.Core.Interfaces.IHost
Interface to implement the Menus:
ModularWinApp.Core.Interfaces.IMenu
Interface to implement the Modules:
- ModularWinApp.Core.Interfaces.IModule
Interface to implement the MEF Attributes:
ModularWinApp.Core.Interfaces.IModuleAttribut
e
Interface to implement the ModuleHandler:
ModularWinApp.Core.Interfaces.IModuleHandler
Concrete classes
The MEF Custom Attributes classes:
ModularWinApp.Core.MenuAttibute
ModularWinApp.Core.ModuleAttribute
Commands System classes:
ModularWinApp.Core.ModuleCommand
ModularWinApp.Core.ModuleCommandDispatcher
The Core main class:
ModularWinApp.Core.ModuleHandler
The database access class:
ModularWinApp.Core.SqlDataModule
The custom attributes
The custom attributes are used to help us provide metadata to our modules, so after MEF loads them, we can identify our modules.
Metadata is optional in MEF. Using metadata requires that the exporter defines which metadata is available for importers to look at and the importer who is able to access the metadata at the time of import.
In our case, we want our modules to provide us a type of the module, as a Clients Module, a Products Module, or on Orders Module.
Lazy<T>
is a new type in the .NET 4 BCL that allows you to delay the creation of an instance. As MEF supports Lazy
, you can import classes, but instantiate them later.
The custom attributes class defines the metadata.
using System;
using System.ComponentModel.Composition;
using ModularWinApp.Core.Interfaces;
namespace ModularWinApp.Core
{
[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class ModuleAttibute : ExportAttribute, IModuleAttribute
{
public ModuleAttibute(string moduleName_)
: base(typeof(IModule))
{
ModuleName = moduleName_;
}
public string ModuleName { get; private set; }
}
}
The usage:
....
namespace ModularWinApp.Modules.Clients
{
[ModuleAttibute("Clients")]
public class ClientsModule : IModule
{
ICommandDispatcher _commands = new ModuleCommandDispatcher();
....
Declaring ImportMany
to use Lazy
:
[ImportMany(typeof(IModule), AllowRecomposition = true)]
public List<Lazy<IModule, IModuleAttribute>> ModuleList
{ get; set; }
When we want to get a specific module, we use its metadata to identify it.
public IModule GetModuleInstance(string moduleName_)
{
IModule instance = null;
foreach (var l in ModuleList)
{
if (l.Metadata.ModuleName == moduleName_)
{
instance = l.Value;
break;
}
}
return instance;
}
The ModuleHandler
The ModuleHandler
class is the main class of the core. It is responsible for loading modules, exposing the list of loaded modules, exposing the list of loaded menus, and exposing the host to the modules. This class also provides methods to check and retrieve the instance of each loaded module.
[Export(typeof(IModuleHandler))]
public class ModuleHandler : IDisposable, IModuleHandler
{
private static IDataModule _dataModule;
[ImportMany(typeof(IModule), AllowRecomposition = true)]
public List<Lazy<IModule, IModuleAttribute>> ModuleList
{ get; set; }
[ImportMany(typeof(IMenu), AllowRecomposition = true)]
public List<Lazy<IMenu, IModuleAttribute>> MenuList
{ get; set; }
[Import(typeof(IHost))]
public IHost Host
{ get; set; }
public IDataModule DataModule
{
get
{
if (_dataModule == null)
_dataModule = new SqlDataModule();
return _dataModule;
}
}
AggregateCatalog catalog = new AggregateCatalog();
public void InitializeModules()
{
ModuleList = new List<Lazy<IModule, IModuleAttribute>>();
MenuList = new List<Lazy<IMenu, IModuleAttribute>>();
foreach (var s in ConfigurationManager.AppSettings.AllKeys)
{
if (s.StartsWith("Path"))
{
catalog.Catalogs.Add(new DirectoryCatalog(
ConfigurationManager.AppSettings[s], "*.dll"));
}
}
catalog.Catalogs.Add(new AssemblyCatalog(
System.Reflection.Assembly.GetCallingAssembly()));
catalog.Catalogs.Add(new DirectoryCatalog(
System.IO.Path.GetDirectoryName(
System.Reflection.Assembly.GetExecutingAssembly().Location), "*.dll"));
CompositionContainer cc = new CompositionContainer(catalog);
cc.ComposeParts(this);
}
public bool ContainsModule(string moduleName_)
{
bool ret = false;
foreach (var l in ModuleList)
{
if (l.Metadata.ModuleName == moduleName_)
{
ret = true;
break;
}
}
return ret;
}
public IModule GetModuleInstance(string moduleName_)
{
IModule instance = null;
foreach (var l in ModuleList)
{
if (l.Metadata.ModuleName == moduleName_)
{
instance = l.Value;
break;
}
}
return instance;
}
public void Dispose()
{
_dataModule = null;
catalog.Dispose();
catalog = null;
ModuleList.Clear();
ModuleList = null;
}
}
}
The Host
The Host is the main form; in this case, it is an MDI parent. In this form, there is a MainMenu
where the modules will insert the menus. The module can decide whether the form it presents is an MDI child or not.
The application entry point is in the Program
static class. This static class will keep a single instance of ModuleHandler
. Before calling the main form, the InitializeModules
method of the ModuleHandler
is called to load the modules, the core, and the Host.
The Program.cs class
namespace ModularWinApp
{
static class Program
{
public static ModuleHandler _modHandler = new ModuleHandler();
[STAThread]
static void Main()
{
...
_modHandler.InitializeModules();
_modHandler.DataModule.CreateSharedConnection(
ConfigurationManager.ConnectionStrings[
"dbOrdersSampleConnectionString"].ConnectionString);
Application.Run(_modHandler.Host as Form);
}
}
}
The Host Form
namespace ModularWinApp
{
[Export(typeof(IHost))]
public partial class frmHost : Form, IHost
{
public frmHost()
{
InitializeComponent();
}
private void frmHost_Load(object sender, EventArgs e)
{
foreach (var menu in Program._modHandler.MenuList)
{
this.menuStrip1.Items.Add(menu.Value.WinFormsMenu);
}
}
...
}
}
The modules
The modules are designed to be independent of one another. Each module exposes two concrete classes, one that implements the IModule
interface and another that implements the IMenu
interface.
The class that implements the interface IModule
serves as a Facade for other modules to have access to commands, while the class that implements the interface IMenu
serves to expose the menu, which is inserted into the MainMenu Host.
Although a module can be different from another in terms of business rules, the structure must be equal for all modules.
The structure of a module
Each module must implement the following classes:
CommandFacade
: This is a static class that will handle all the methods that are used by Menu
and used by other modules;
Menu
: This class will build the Windows Forms menu that will be exposed to the Host;
Module
: This class will handle the module instance and the commands exposed by the module;
Service
: This class will handle all the database operations for the module.
The code below is extracted from the Clients Module.
ModularWinApp.Modules.Clients.ClientsCommandFacade
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.Windows.Forms;
using ModularWinApp.Core;
using ModularWinApp.Core.Interfaces;
namespace ModularWinApp.Modules.Clients
{
public static class ClientsCommandFacade
{
public static IModuleHandler ModuleHandler { get; set; }
public static void MenuNovo(object sender, EventArgs e)
{
NewClient();
}
public static void MenuConsultar(object sender, EventArgs e)
{
ViewClient();
}
public static bool NewClient()
{
frmClient f = new frmClient();
f.MdiParent = (Form)ModuleHandler.Host;
f.Show();
return true;
}
public static bool ViewClient()
{
frmClients f = new frmClients();
f.MdiParent = (Form)ModuleHandler.Host;
f.Show();
return true;
}
public static DataSet SelectClients()
{
frmClients f = new frmClients();
f.ShowDialog();
DataSet l = f.SelectedClients;
return l;
}
public static string GetClientName(int id_)
{
ClientsService _cs = new ClientsService();
return _cs.GetClientName(id_);
}
public static DataTable GetClientList()
{
ClientsService _cs = new ClientsService();
return _cs.GetClientsDataTable();
}
}
}
ModularWinApp.Modules.Clients.ClientsMenu
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using ModularWinApp.Core.Interfaces;
using ModularWinApp.Core;
using System.ComponentModel.Composition;
namespace ModularWinApp.Modules.Clients
{
[MenuAttibute("Clients")]
public class ClientsMenu : IMenu
{
private ToolStripMenuItem ClientsMainMenu;
private ToolStripMenuItem ClientsConsultarMenu;
private ToolStripMenuItem ClientsNovoMenu;
private ToolStripMenuItem CreateMenu()
{
this.ClientsMainMenu = new System.Windows.Forms.ToolStripMenuItem();
this.ClientsConsultarMenu = new System.Windows.Forms.ToolStripMenuItem();
this.ClientsNovoMenu = new System.Windows.Forms.ToolStripMenuItem();
this.ClientsMainMenu.DropDownItems.AddRange(
new System.Windows.Forms.ToolStripItem[] {
this.ClientsConsultarMenu,
this.ClientsNovoMenu});
this.ClientsMainMenu.Name = "MenuClientsMain";
this.ClientsMainMenu.Text = "Clients";
this.ClientsConsultarMenu.Name = "MenuClientsConsultar";
this.ClientsConsultarMenu.Text = "Consultar";
this.ClientsConsultarMenu.Click += new EventHandler(
ClientsCommandFacade.MenuConsultar);
this.ClientsNovoMenu.Name = "MenuClientsNovo";
this.ClientsNovoMenu.Text = "Novo";
this.ClientsNovoMenu.Click += new EventHandler(ClientsCommandFacade.MenuNovo);
return ClientsMainMenu;
}
[ImportingConstructor()]
public ClientsMenu([Import(typeof(IModuleHandler))] IModuleHandler moduleHandler_)
{
CreateMenu();
ClientsCommandFacade.ModuleHandler = moduleHandler_;
}
public ToolStripMenuItem WinFormsMenu
{
get { return ClientsMainMenu; }
}
}
}
ModularWinApp.Modules.Clients.ClientsModule
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.ComponentModel.Composition;
using ModularWinApp.Core;
using ModularWinApp.Core.Interfaces;
namespace ModularWinApp.Modules.Clients
{
[ModuleAttibute("Clients")]
public class ClientsModule : IModule
{
ICommandDispatcher _commands = new ModuleCommandDispatcher();
public string Name
{
get { return "Clients"; }
}
public ICommandDispatcher Commands
{
get { return _commands; }
}
public IModuleHandler ModuleHandler
{
get { return ClientsCommandFacade.ModuleHandler; }
}
[ImportingConstructor()]
public ClientsModule([Import(typeof(IModuleHandler))]
IModuleHandler moduleHandler_)
{
ClientsCommandFacade.ModuleHandler = moduleHandler_;
RegisterCommands();
}
public void RegisterCommands()
{
_commands.Register("Clients.GetClientList",
new ModuleCommand<DataTable>(ClientsCommandFacade.GetClientList));
}
}
}
ModularWinApp.Modules.Clients.ClientsService
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.Data.SqlClient;
using ModularWinApp.Core;
using ModularWinApp.Core.Interfaces;
namespace ModularWinApp.Modules.Clients
{
public class ClientsService
{
IDataModule _dataModule;
public ClientsService()
{
_dataModule = ClientsCommandFacade.ModuleHandler.DataModule;
}
public DataRow GetClient(int id_)
{
try
{
SqlConnection _conn = (SqlConnection)_dataModule.GetSharedConnection();
SqlDataAdapter _da = new SqlDataAdapter(
"Select * from tb_Clients Where ID = " +
id_.ToString(), _conn);
DataSet _ds = new DataSet();
_dataModule.OpenSharedConnection();
_da.Fill(_ds);
if (_ds.Tables.Count > 0 && _ds.Tables[0].Rows.Count > 0)
{
return _ds.Tables[0].Rows[0];
}
else
return null;
}
catch (Exception ex)
{
throw;
}
finally
{
_dataModule.CloseSharedConnection();
}
}
public string GetClientName(int id_)
{
try
{
SqlConnection _conn = (SqlConnection)_dataModule.GetSharedConnection();
SqlDataAdapter _da = new SqlDataAdapter(
"Select * from tb_Clients Where ID = " +
id_.ToString(), _conn);
DataSet _ds = new DataSet();
_dataModule.OpenSharedConnection();
_da.Fill(_ds);
if (_ds.Tables.Count > 0 && _ds.Tables[0].Rows.Count > 0)
{
return _ds.Tables[0].Rows[0]["Name"].ToString();
}
else
return "";
}
catch (Exception ex)
{
throw;
}
finally
{
_dataModule.CloseSharedConnection();
}
}
public DataTable GetClientsDataTable()
{
try
{
SqlConnection _conn = (SqlConnection)_dataModule.GetSharedConnection();
SqlDataAdapter _da = new SqlDataAdapter(
"Select * from tb_Clients ", _conn);
DataSet _ds = new DataSet();
_dataModule.OpenSharedConnection();
_da.Fill(_ds);
if (_ds.Tables.Count > 0)
return _ds.Tables[0];
else
return null;
}
catch (Exception ex)
{
throw;
}
finally
{
_dataModule.CloseSharedConnection();
}
}
public int Insert(string name_, DateTime dateOfBirth_, string fone_)
{
try
{
SqlConnection _conn = (SqlConnection)_dataModule.GetSharedConnection();
SqlCommand _cmd = new SqlCommand();
_cmd.CommandText = "INSERT INTO tb_Clients (ID, Name, DateOfBirth, Fone)" +
" VALUES (@ID, @Name, @DateOfBirth, @Fone)";
int newID = NewID();
SqlParameter _paramID = new SqlParameter("@ID", newID);
SqlParameter _paramName = new SqlParameter("@Name", name_);
SqlParameter _paramDateOfBirth =
new SqlParameter("@DateOfBirth", dateOfBirth_);
SqlParameter _paramFone = new SqlParameter("@Fone", fone_);
_cmd.Parameters.AddRange(new SqlParameter[] {
_paramID, _paramName, _paramDateOfBirth, _paramFone });
_cmd.Connection = _conn;
_dataModule.OpenSharedConnection();
_cmd.ExecuteNonQuery();
return newID;
}
catch (Exception ex)
{
throw;
}
finally
{
_dataModule.CloseSharedConnection();
}
}
private int NewID()
{
try
{
SqlConnection _conn = (SqlConnection)_dataModule.GetSharedConnection();
SqlCommand _cmd = new SqlCommand();
_cmd.CommandText = "SELECT IsNull(MAX(ID), 0) + 1 FROM tb_Clients";
_cmd.Connection = _conn;
_dataModule.OpenSharedConnection();
return (int)_cmd.ExecuteScalar();
}
catch (Exception ex)
{
throw;
}
finally
{
_dataModule.CloseSharedConnection();
}
}
}
}
Modules communication
Communication between modules is made by commands. Each command has a name and a delegate that points to a function. By default, the functions are on a CommandFacade
within each module.
There are two types of commands, one that takes no parameters and one that takes parameters. The two types implement the same interface.
The commands are recorded and stored in the class CommandDispatcher
that is responsible to fire commands.
Each module must expose a CommandDispatcher
so that other modules can trigger the commands registered. Thus, it is possible to communicate between modules.
ModularWinApp.Core.ModuleCommand
There are two Commands classes. There are two types of commands, differentiated by their generic types.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ModularWinApp.Core.Interfaces;
namespace ModularWinApp.Core
{
public class ModuleCommand<InType, OutType> : ICommand
{
private Func<InType, OutType> _paramAction;
public ModuleCommand(Func<InType, OutType> paramAction_)
{
_paramAction = paramAction_;
}
public OutType Execute(InType param_)
{
if (_paramAction != null)
return _paramAction.Invoke(param_);
else
return default(OutType);
}
}
public class ModuleCommand<OutType> : ICommand
{
private Func<OutType> _action;
public ModuleCommand(Func<OutType> action_)
{
_action = action_;
}
public OutType Execute()
{
if (_action != null)
return _action.Invoke();
else
return default(OutType);
}
}
}
ModularWinApp.Core.ModuleCommandDispatcher
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ModularWinApp.Core.Interfaces;
namespace ModularWinApp.Core
{
public class ModuleCommandDispatcher : ICommandDispatcher
{
private Dictionary<string, ICommand> _commands =
new Dictionary<string, ICommand>();
public void Register(string key_, ICommand command_)
{
_commands.Add(key_, command_);
}
public OutType Execute<OutType>(string key_)
{
if (CanExecute(key_))
{
ModuleCommand<OutType> _cmd =
(ModuleCommand<OutType>)_commands[key_];
return _cmd.Execute();
}
else
return default(OutType);
}
public OutType Execute<InType, OutType>(string key_, InType params_)
{
if (CanExecute(key_))
{
ModuleCommand<InType, OutType> _cmd =
(ModuleCommand<InType, OutType>)_commands[key_];
return _cmd.Execute(params_);
}
else
return default(OutType);
}
public bool CanExecute(string key_)
{
return _commands.ContainsKey(key_);
}
}
}
Data access
Each module can implement data access regardless of the other modules, however, one requirement is that the modules use the same connection to the database system, so I created an interface and a concrete class that exposes the connection as well as the transaction that will be shared between modules.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.ComponentModel.Composition;
using ModularWinApp.Core.Interfaces;
namespace ModularWinApp.Core
{
public class SqlDataModule: IDataModule, IDisposable
{
private static IDbConnection _sharedConnection;
private static IDbTransaction _sharedTransaction;
public void CreateSharedConnection(string connectionString_)
{
if (_sharedConnection == null)
{
_sharedConnection =
new System.Data.SqlClient.SqlConnection(connectionString_);
}
else
{
throw new Exception("The connection is already created!");
}
}
public void OpenSharedConnection()
{
if (_sharedConnection.State == ConnectionState.Closed)
_sharedConnection.Open();
}
public void CloseSharedConnection()
{
if (_sharedConnection.State == ConnectionState.Open)
_sharedConnection.Close();
}
public void BeginTransaction()
{
if (_sharedTransaction == null)
_sharedTransaction = _sharedConnection.BeginTransaction();
}
public void CommitTransaction()
{
if (_sharedTransaction != null)
_sharedTransaction.Commit();
_sharedTransaction = null;
}
public void RollbackTransaction()
{
if (_sharedTransaction != null)
_sharedTransaction.Rollback();
_sharedTransaction = null;
}
public IDbConnection GetSharedConnection()
{
if (_sharedConnection != null)
return _sharedConnection;
else
throw new Exception("The connection is not created!");
}
public IDbTransaction GetSharedTransaction()
{
if (_sharedTransaction != null)
return _sharedTransaction;
else
throw new Exception("The transaction is not created!");
}
public void Dispose()
{
if (_sharedTransaction != null)
{
_sharedTransaction.Dispose();
_sharedTransaction = null;
}
if (_sharedConnection != null)
{
_sharedConnection.Dispose();
_sharedConnection = null;
}
}
}
}
Points of interest
Here is the MEF site on CodePlex: http://mef.codeplex.com/documentation.
And a very good article on MEF: http://www.codeproject.com/KB/aspnet/DOTNETMEF4_0.aspx.