Windows Forms Modular App Using MEF






4.93/5 (34 votes)
Creating a modular Windows Forms app using MEF.
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;
//MEF Reference
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
{
//Create a new instance of the ModuleCommandDispatcher to store the commands
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.
//return a module instance based on it's name
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.
//The MEF Export attribute is used to export the class to the other modules
[Export(typeof(IModuleHandler))]
public class ModuleHandler : IDisposable, IModuleHandler
{
//static variable to be sure that only on instance will exist
private static IDataModule _dataModule;
//The ImportMany attribute allows us to import many instances of classes that
//implements the IModule interface
// The AllowRecomposition makes it possible
// to update the module list during runtime
[ImportMany(typeof(IModule), AllowRecomposition = true)]
// The ModuleList will be filled with the imported modules
public List<Lazy<IModule, IModuleAttribute>> ModuleList
{ get; set; }
[ImportMany(typeof(IMenu), AllowRecomposition = true)]
// The MenuList will be filled with the imported Menus
public List<Lazy<IMenu, IModuleAttribute>> MenuList
{ get; set; }
[Import(typeof(IHost))]
// The imported host form
public IHost Host
{ get; set; }
//Expose the DataAccess module
public IDataModule DataModule
{
get
{
if (_dataModule == null)
_dataModule = new SqlDataModule();
return _dataModule;
}
}
// AggregateCatalog stores the MEF Catalogs
AggregateCatalog catalog = new AggregateCatalog();
public void InitializeModules()
{
// Create a new instance of ModuleList
ModuleList = new List<Lazy<IModule, IModuleAttribute>>();
// Create a new instance of MenuList
MenuList = new List<Lazy<IMenu, IModuleAttribute>>();
// Foreach path in the main app App.Config
foreach (var s in ConfigurationManager.AppSettings.AllKeys)
{
if (s.StartsWith("Path"))
{
// Create a new DirectoryCatalog with the path loaded from the App.Config
catalog.Catalogs.Add(new DirectoryCatalog(
ConfigurationManager.AppSettings[s], "*.dll"));
}
}
// Create a new catalog from the main app, to get the Host
catalog.Catalogs.Add(new AssemblyCatalog(
System.Reflection.Assembly.GetCallingAssembly()));
// Create a new catalog from the ModularWinApp.Core
catalog.Catalogs.Add(new DirectoryCatalog(
System.IO.Path.GetDirectoryName(
System.Reflection.Assembly.GetExecutingAssembly().Location), "*.dll"));
// Create the CompositionContainer
CompositionContainer cc = new CompositionContainer(catalog);
// Do the MEF Magic
cc.ComposeParts(this);
}
//Verify if a specific module is imported
public bool ContainsModule(string moduleName_)
{
bool ret = false;
foreach (var l in ModuleList)
{
if (l.Metadata.ModuleName == moduleName_)
{
ret = true;
break;
}
}
return ret;
}
//return a module instance based on it's name
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
{
//Create a new instance of ModuleHandler. Only one must exist.
public static ModuleHandler _modHandler = new ModuleHandler();
[STAThread]
static void Main()
{
...
//Initialize the modules. Now the modules will be loaded.
_modHandler.InitializeModules();
//Create a new database connection that will be used by the modules.
_modHandler.DataModule.CreateSharedConnection(
ConfigurationManager.ConnectionStrings[
"dbOrdersSampleConnectionString"].ConnectionString);
//Start the Host Form
Application.Run(_modHandler.Host as Form);
}
}
}
The Host Form
namespace ModularWinApp
{
//Exports the Host, so it can be imported into the Modules
[Export(typeof(IHost))]
public partial class frmHost : Form, IHost
{
public frmHost()
{
InitializeComponent();
}
private void frmHost_Load(object sender, EventArgs e)
{
//Here, the exported menus will be attached to the main menu.
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 byMenu
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; }
//Method called by the Menu
public static void MenuNovo(object sender, EventArgs e)
{
NewClient();
}
//Method called by the Menu
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();
//
// MenuClientsMain
//
this.ClientsMainMenu.DropDownItems.AddRange(
new System.Windows.Forms.ToolStripItem[] {
this.ClientsConsultarMenu,
this.ClientsNovoMenu});
this.ClientsMainMenu.Name = "MenuClientsMain";
this.ClientsMainMenu.Text = "Clients";
//
// MenuClientsConsultar
//
this.ClientsConsultarMenu.Name = "MenuClientsConsultar";
this.ClientsConsultarMenu.Text = "Consultar";
this.ClientsConsultarMenu.Click += new EventHandler(
ClientsCommandFacade.MenuConsultar);
//
// MenuClientsNovo
//
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
{
//Create a new instance
//of the ModuleCommandDispatcher to store the commands
ICommandDispatcher _commands = new ModuleCommandDispatcher();
public string Name
{
get { return "Clients"; }
}
public ICommandDispatcher Commands
{
get { return _commands; }
}
public IModuleHandler ModuleHandler
{
get { return ClientsCommandFacade.ModuleHandler; }
}
//When the module instace is created,
//the reference to the ModuleHandler is injected
[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
{
//A module command concrete class with parameter and output type
public class ModuleCommand<InType, OutType> : ICommand
{
//This delegate will point to a function
private Func<InType, OutType> _paramAction;
//Constructor receiving the delegate as parameter
public ModuleCommand(Func<InType, OutType> paramAction_)
{
_paramAction = paramAction_;
}
//Execute the delegate
public OutType Execute(InType param_)
{
if (_paramAction != null)
return _paramAction.Invoke(param_);
else
return default(OutType);
}
}
//A module command concrete class with only output type
public class ModuleCommand<OutType> : ICommand
{
//This delegate will point to a function
private Func<OutType> _action;
//Constructor receiving the delegate as parameter
public ModuleCommand(Func<OutType> action_)
{
_action = action_;
}
//Execute the delegate
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
{
//A dictionary to store the module commands
private Dictionary<string, ICommand> _commands =
new Dictionary<string, ICommand>();
//Register the command into the _commands dictionary
public void Register(string key_, ICommand command_)
{
_commands.Add(key_, command_);
}
//Execute the command without parameters
public OutType Execute<OutType>(string key_)
{
if (CanExecute(key_))
{
ModuleCommand<OutType> _cmd =
(ModuleCommand<OutType>)_commands[key_];
return _cmd.Execute();
}
else
return default(OutType);
}
//Execute the command with parameters
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);
}
//Verify if the command is registered
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;
// MEF References
using System.ComponentModel.Composition;
//Core Reference
using ModularWinApp.Core.Interfaces;
namespace ModularWinApp.Core
{
public class SqlDataModule: IDataModule, IDisposable
{
//Singleton Connection. Must be only one
private static IDbConnection _sharedConnection;
//Singleton Transaction. Must be only one
private static IDbTransaction _sharedTransaction;
//Create the connection that will be shared across the modules
public void CreateSharedConnection(string connectionString_)
{
//If the connection instance is not created, create the instance
if (_sharedConnection == null)
{
_sharedConnection =
new System.Data.SqlClient.SqlConnection(connectionString_);
}
else
{
throw new Exception("The connection is already created!");
}
}
//Open the shared connection
public void OpenSharedConnection()
{
if (_sharedConnection.State == ConnectionState.Closed)
_sharedConnection.Open();
}
//Close the shared connection
public void CloseSharedConnection()
{
if (_sharedConnection.State == ConnectionState.Open)
_sharedConnection.Close();
}
//Begin a transaction on shared connection
public void BeginTransaction()
{
if (_sharedTransaction == null)
_sharedTransaction = _sharedConnection.BeginTransaction();
}
//Commit a transaction on shared connection
public void CommitTransaction()
{
if (_sharedTransaction != null)
_sharedTransaction.Commit();
_sharedTransaction = null;
}
//Rollback a transaction on shared connection
public void RollbackTransaction()
{
if (_sharedTransaction != null)
_sharedTransaction.Rollback();
_sharedTransaction = null;
}
//Return the shared connection
public IDbConnection GetSharedConnection()
{
if (_sharedConnection != null)
return _sharedConnection;
else
throw new Exception("The connection is not created!");
}
//Return the shared transaction
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.