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

Using a Plugin-Based Application

, 3 Jul 2014
Rate this:
Please Sign up or sign in to vote.
How to create a Plugin Based Shell

Update:
There is a small Tutorial for this topic on YT:
​https://www.youtube.com/watch?v=pvXi1lbLz-s

HINT !

Please read the article Carefully because some guys tried to use it and did not understand how does it works. For example: in some cases you are forced to setup VS to recompile all DLL's on every build! that is imported because due the loose of Dependency between the start project and the Module containing DLL VS does not recognize that the DLL has changed so it WOULD build it again. To enable this behavior go to TOOLS - PROJECTS AND SOLUTIONS -> BUILD AND RUN and set the option "On Run, When projects are out of date" To "Always Build".

Source includes

  • JPB.Shell.exe
  • JPB.Shell.Contracts.dll
  • JPB.Shell.MEF.dll
  • JPB.Shell.CommonApplicationContainer.dll
  • JPB.Shell.Common.Contracts.dll
  • JPB.Shell.VisualServiceScheduler.dll
  • Default application host
  • Shell Interfaces and Attributes host
  • The Framework itself
  • A default Implementation that is using the shell for business applications (WPF only)
  • The Extension of Shell.Contracts
  • A small example Module that allows user to exclude Services during runtime

Introduction

What can the shell do for me? That is easy to say, It can help you to extend your program on a very easy way, just by pushing DLL's into a folder and it allows you to simply develop new, powerful apps

Background

The reason why this program was developed by me was very simple: How to create a program that allows more than one developer to work on a program that has more than ¬ one "module" but without let the other run into crazy subversion conflicts and all these other nice ¬ things when more than one developer work on a project. The Problem was found: We need an "independent" modules for each function. Then the idea called "Generic shell" was born. Since then I worked on it and now (thanks to them) my company allows me to share my solution with ¬ the world

Using the code

The usage of the code is not that easy if you do it the first time. The basic things are all located in the project JPB.Shell.exe (this is the main ¬ application). You could create a new one to Support things like Click Once Deployment, but even for Resources in your AppDomain, there is an other way inside the Shell.

Ok, now set up a new Solution and add the shell projects to it. In order to use MEF you also need to add a project that is called JPB.Shell.Contracts. This project contains all interfaces and attributes that the shell is using. It also contains one static class with extensions. But back to that later...

Do not Reference the Shell.exe or one of the Modules. There is no need to do that. All Functions and Pools are passed through to your code. What is a Pool? A Pool contains and manage several things. Eg the ServicePool that usages MEF to include your Modules into the project or the ¬ ImportPool that logs all activety, done by the ServicePool. The only ¬ thing your modules need to know are the Contracts. With that, the shell knows you and what you want. Because you are communicating over the Contracts. Let's say we want to create an application that has 2 modules, one monitor and one client for an ¬ application that is monitoring a database and submit into it. We are adding our 2 modules to it and than we got 4 projects in our solution:

  • JPB.Shell
  • JPB.Shell.Contracts
  • Foo.Client
  • Foo.Monitor

If you now try to compile the shell it will throw an NotImplementedException. Why? Because the shell needs more than what you currently provide to it. For clarification:

The shell does not provide you any Visual Window and it does not ¬ provide any of your business logic.

What you need is an Application Container! What the heck is that? An Application Container provides your main window and the business part of the shell. I recommend that you and your company write one container that is valid for every project, if ¬ possible or even a template because this container takes the most time in development. There is one common application Container in the downloadable archive file, one with WPF and a Ribbon (I won't talk now about guidelines or ways to set up an new Application Container because ¬ this is a very complex topic. If you want to set up a new one by yourself please take a look into the common ones in the downloadable archive file). We will now use the common one.

To add the Foo.Client to the Application Container it is required to create a class that implements the

JPB.Shell.Contracts.Interfaces.Services.ModuleServices.IVisualService

inferface and also Flag the class with the

JPB.Shell.Contracts.Attributes.VisualServiceExportAttribute

attribute. Then your class should looks like this:

#region Jean-Pierre Bachmann
 
// Erstellt von Jean-Pierre Bachmann am 18:38
 
#endregion
 
using JPB.Shell.CommonContracts.Attributes;
using JPB.Shell.Contracts.Interfaces;
using JPB.Shell.Contracts.Interfaces.Services.ModuleServices;
using JPB.Shell.VisualServiceScheduler.ViewModel;

namespace JPB.Foo.Client
{
[RibbonMetadata("Shell.VisualServiceScheduler", 0, 0, "VisualServiceScheduler", typeof(IVisualService))]
    public class Module : IVisualService
    {
        public static IApplicationContext Context;

        #region Implementation of IVisualModule

        public object View
        {
            get { return new FooView() { DataContext = ViewModel }; }
        }

        public object ViewModel
        {
            get { return new FooViewModel(); }
        }

        public bool OnEnter()
        {
            return true;
        }

        public bool OnLeave()
        {
            return true;
        }

        #endregion

        #region Implementation of IService

        public void OnStart(IApplicationContext application)
        {
            Context = application;
        }

        #endregion
    }
} 
 

But stop! i did say that i want to flag the class with the VisualServiceExport Attribute and not with the RibbonMetadata I did not lie because RibbonMetadata inherts from VisualServiceExport and that's all we need so far to.

VisualServiceExports constructor ( That is the base class attribute that is over the class declaration ) contains two parameters in this case [VisualServiceExport("VisualServiceScheduler", typeof (IVisualService))]

But RibbonMetadata extents this with some new parameter like "string descriptor", "int pageNumber" and so on. This parameter are used from the CommonAppContainer to customize the Ribbon.

As i said, the RibbonMetadata is not inside the JPB.Shell.Contracts DLL. Its inside our "own" DLL that just extend the standard one.

Lets take a look inside the very base class for VisualExports VisualServiceExportAttribute. There are Minimum of 2 Parameter. The name to Identify it over a string and a interface array that implements IService to ¬ identify it over one or more types. This attribute is used to load it into the Shell by using a lasy MEF Adaptations that uses metadata.

There are 3 constructors for the Attribute.

public VisualServiceExportAttribute(string descriptor, params Type[] contract)
    : this(descriptor, false, contract)
{

}

public VisualServiceExportAttribute(string descriptor, string imageuri, params Type[] contract)
    : this(descriptor, contract)
{
    ImageUri = imageuri;
}

public VisualServiceExportAttribute(string descriptor, bool isDefauld, params Type[] contract)
    : base(typeof(IService))
{
    IsDefauldService = false;
    IsDefauldService = isDefauld;
    Contracts = contract;
    Descriptor = descriptor;
} 

The first and the second constructor are realy self-describin. Only the 3th one is interesting because it contains the isDefauld parameter. What does that mean? if one service is flagged with this parameter, it shows that this implementation of the Contract ( see the Type contract parameter ) is a Special one. This is only a way to "mark" a Implementation as special and let your program code separate it from other imp of this Contract. You can check if there is one service that has set this flag by calling:

var defaultSingelService
= ServicePool.Instance.GetDefaultSingelService<IApplicationContainer>();

As seen in the OnStartup function in the JPB.Shell.Application.cs to get the ¬ default Application Container. This attribute Contains our Metadata. This is loaded on Startup to iditify the class so everything you set here will be seen by the ¬ ServicePool.

To support the MVVM approach, every Visual module must implementing the IVisualService ¬ interface.

public interface IVisualService : IService
{
    object View { get; }
    object ViewModel { get; }
    bool OnEnter();
    bool OnLeave();
}
 

This interface is used to identify visual services. What are visual services? These special services are only for the Application Container and the meaning of the term ¬ "visual" is very easy. This services are contain a View and a ViewModel property to support the MVVM ¬ approach. More about services and the IService interface in general follows later.

After we did this, we are nearly done with the heavy part. The only thing to support a simple debugging is to set the modules output path to the shell one. After that you are done. Press F5 to let the compiler have his fun and than you should see your Modules in action.

VERY VERY VERY VERY VERY2 IMPORTANT

To ensure that your application and you Models goes into the same folder ( that is very importend because how should MEF know where are your DLLs? Wink | ;-) , set the BuildPath to the same as the Shell ( Default is "..\..\..\..\bin\VS Output\" for Release and "..\..\..\..\bin\VS Debug\" for Debug. Than ensure that your set the "On Run,When Projects out of date" option to Always build.

MEF Services

The shell contains just one really important class called ¬ JPB.Shell.Services.ServicePool. This class manages all imports/exports so it does most of the work. It contains several methods and implements the ¬ JPB.Shell.Contracts.Interfaces.Services.ShellServices.IServicePool interface. What does this implementation do for me?

  1. Enumerate all DLLs in your shell directory
  2. Search for a specific part in the name (Current="*Module*") and Flag them as high priority of the ¬ name to improve Performance (That just means that DLLs with a name like "FooClientModule.dll" are ¬ loaded at Startup but DLLs like "FooClient.dll" not! )
  3. Search those high priority assemblies for exports and add them into my ¬ StrongNameCatalog, skip all non high priority assemblies
  4. Wait for ending of 3
  5. Start the default service IApplicationContainer
  6. The main window is opening
  7. All assemblies without the high priority flag (Current="*Module*") will be searched for exports

Every service is a class that is shared with everyone within the service Tree. You can access it over the Interface by using the IApplicationContext instance that is ¬ passed to every service on its startup. So it is not necessary that any assembly, which uses a service or implements a ¬ service interface, knows the shell, because the communication only happens over ¬ that interface since the interface is passed to the service. Don't call us, we call you! . If you want something from the ServicePool, like a Service than Implement ISerivce and ¬ wait for the OnStart functions paramter of type ¬ JPB.Shell.Contracts.Interfaces.IApplicationContext or if you are within a Visual ¬ Module just store the parameter in a Static Variable like this:

//Static to allow all other classes access to it
//eg to use services
public static IApplicationContext Context;
 
#region Implementation of IService
 
public void OnStart(IApplicationContext application)
{
   Context = application;
}
 
#endregion 
 

The IApplicationContext interface provieds you everything that the shell does.

public interface IApplicationContext
{
    IDataBroker DataBroker { get; }
    IServicePool ServicePool { get; }
    IMessageBroker MessageBroker { get; }
    IImportPool ImportPool { get; }
    IVisualModuleManager VisualModuleManager { get; }
}
 

With that, there is no need to referance the shell or any other module because everyone is just communication over this way.

If you try to access a service like a Database access provider that is located in the you do not need a reference to the Assembly because IApplicationContext provides you a way to get the Service.

Lets Imagine we Try to Access a Database, we do not know what kind of Database in our Foo.Monitor.So we Setup a new Interface called IFooDatabaseService

using System;
using JPB.Shell.Contracts.Interfaces.Services;

namespace Foo.Shell.FooContracts.Interfaces.Services.ContainerService
{
    public interface IFooDatabaseService : IService
    {
        IDatabase Database { get; }

        //CRUD
        void Insert<T>(T entity);
        T Select<T>(string where);
        void Update<T>(T entity);
        bool Delete<T>(T entity);
    }
}

What just wait a second. This interface is not in the same namespace as all other interfaces we are dealing with like IService ! As i said, this is the maybe most powerful thing, because MEF is just working/knowing IService and for the shell this is the only impotent thing. The ServicePool is looking into every DLL and than includes every Implementation of IService. The pool does not care about other interfaces just since IFooDatabaseService is extending ISerivce the Pool will take care of it and provide the Information in its Pool.

Ok say, we just created a Assembly that has a Imp of IFooDatabaseService and we want to access that now from the FooMonitor.

            //Module is my VisualModule that is invoked before
            var infoservice = Module
                //Context is my variable of IApplicationContext
            .Context
                //ServicePool is simply the current instance of ISerivcePool
            .ServicePool
                //GetSingelService gets a singel service of the requestet Interface it has the FirstOrDefauld behavior
            .GetSingelService<IFooDatabaseService>();

            //Check if null
            //VERY VERY VERY Important
            //Because you CAN NOT KNOW that the service exits in ANY of the assemblies so deal with it
            //if you want to get allways a service mark them as Defauld and call the Defaulf function
            if (infoservice == null)
                return;
            
            //For example get the last Entry in the ImportPool that logs every activety that is done be ServicePool
            var logEntry = Module.Context.ImportPool.LogEntries.Last();

            //Call a Function on that service that we got
            //in that case we want to insert the Log into the DB
            infoservice.Insert(logEntry); 

Add your own service

This is more easy than you may guess. Lets set up a new Interface that just shows a Messagebox. First create a Interface that extents IService.

public interface IFooMessageBox : IService
{
    void showMessageBox(string text);
} 

Step one is done! . Step two is simple as step one. Implement the Service and flag it with the ServiceExport attribute:

[ServiceExport("Foo.MessageBoxExportName", typeof (IFooMessageBox)]
public class FooMessageBox : IFooMessageBox
{
    private Contracts.Interfaces.IApplicationContext Context;

    public void showMessageBox(string text)
    {
        MessageBox.Show("Message from Service: " + text);
    }

    public void OnStart(Contracts.Interfaces.IApplicationContext application)
    {
        Context = application;
    }
} 

And thats it.

Run you program, be sure that the assambly is in the same directory as your shell and you can ¬
access this service by

 var messageService = VisualMainWindow
    .ApplicationProxy
    .ServicePool
    .GetSingelService<IFooMessageBox>();
if (messageService == null)
    return;
messageService.showMessageBox("Foo Message From Everywere in you program"); 

Add your own service with InheritedExportAttribute

As i mentioned, i take a look into the InheritedExportAttribute. And finaly i implemented the support for it into the program. What does this attribute do? It inverts the Metadata location away from the service with the ServiceExportAttribute ¬ and the Service self to the Interface or base class.

The current way to work with the metadata:

as we see, the service self contains his own independent Metadata. And more than one Implementation could provide multiple Metadata. With the InheritedExportAttribute we invert this dependency:

This offers as the folloring usage:

public class FooMessageBox : IFooMessageBox
{
    Contracts.Interfaces.IApplicationContext Context;
 
    public void showMessageBox(string text)
    {
        MessageBox.Show("Message from Service: " + text);
    }
 
    public void OnStart(Contracts.Interfaces.IApplicationContext application)
    {
        Context = application;
    }
} 

As you see, we are get (nearly) totaly rid of the Service Export attribute. EVERY implementation of IFooMessageBox will be included by the shell no matter what it ¬ does or what it provide. That would be nice, if there wouldn't be that small word "nearly". To allow this bevavior you need to modify your interface, because you MUST provide some metadata. And now the interface does that:

[InheritedServiceExport("FooExport", typeof(IFooMessageBox))]
public interface IFooMessageBox : IService
{
    void showMessageBox(string text);
}

This has good and bad sides:

Good:
We do not need to care about the Metadata beacuse we did that once while we created the Interface.

Bad:

We can not longer control or modify the Metadata. Since the Attribute needs constant values every implementation of IFooMessageBox ¬ provides the same Metadata and for the shell, there all the same. So we got this problem:

You can see since we are using the InheritedServiceExportAttribute we don't know who ¬ is who because there all offers as the same Metadata.

Features that you should know

There are 2 Preprocessor Directives that controls some of the behavior.

#define PARALLEL4TW

Allows the StrongNameCatalog the using of PLinq to search and include the Assamblys into the ¬ Catalog with a mix of Eager and Lazy loading. For my opinion you should not disable this in your version because it is Tested, Save and very fast. Just for debugging purposes does this make sence.

#define SECLOADING

As in this link described, the Catalog contains a way to garantie a small amount of security. I never used this so i can not say anything to that.

Points of Interest

This is a huge project with a lot of functions and I am just a jr developer, so please be lenient that I am not able to explain every big or small function. I had a lot of fun while creating this project and I guess it is worth it, to share not only the ¬ idea with other developers and everyone interested in it. When I started with MEF in .NET 4.0 I was driven crazy because the pure MEF system is very Complex and just a couple of days ago I found a lot of new information like the ¬ InheritedExport attribute. Unfortunately it is not possible to add .NET 4.5 Modules with the current state of the project ¬ because the Shell is compiled with .NET 4. Of course if you change the Target Framework to .NET 4.5, it will. But I am not familiar with the .NET 4.5 MEF Framework.

I would highly appreciate any new ideas and impressions from you.

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)

About the Author

Jean-Pierre Bachmann
Software Developer (Junior) IEA DPC
Germany Germany
I am a Young German Developer. I am now for 2 Years in this Job and i Love it.
My main field of Work is in C# .net 4 and WPF and DevExpress.

Comments and Discussions

 
GeneralMy vote of 5 PinmemberpeteSJ3-Jul-14 6:57 
GeneralRe: My vote of 5 PinprofessionalJean-Pierre Bachmann3-Jul-14 8:28 
GeneralMy vote of 2 PinprofessionalAbhishek Kumar Goswami31-Mar-14 23:39 
GeneralRe: My vote of 2 PinprofessionalJean-Pierre Bachmann28-May-14 8:54 
QuestionWhat is the advantage of plugin based apps development PinmemberTridip Bhattacharjee31-Mar-14 20:55 
AnswerRe: What is the advantage of plugin based apps development PinmemberSuresh Priyadarshana31-Mar-14 21:36 
GeneralRe: What is the advantage of plugin based apps development PinprofessionalJean-Pierre Bachmann31-Mar-14 21:48 
Questiontypo ? ;} Pinmemberfukehot31-Mar-14 15:25 
AnswerRe: typo ? ;} PinprofessionalJean-Pierre Bachmann31-Mar-14 21:43 
GeneralMy vote of 1 Pinmemberarpit007-from-New-Delhi27-Mar-14 22:44 
GeneralRe: My vote of 1 [modified] PinprofessionalJean-Pierre Bachmann28-Mar-14 1:28 
QuestionNothing PinmemberBoer Coene4-Mar-14 9:00 
AnswerRe: Nothing [modified] PinprofessionalJean-Pierre Bachmann4-Mar-14 10:01 
QuestionCan't run PinmemberAndre De Beer21-Nov-13 20:06 
AnswerRe: Can't run PinprofessionalJean-Pierre Bachmann21-Nov-13 22:11 
GeneralRe: Can't run PinmemberAndre De Beer21-Nov-13 22:37 
GeneralRe: Can't run PinprofessionalJean-Pierre Bachmann21-Nov-13 23:12 
QuestionMy vote of 5 Pinmemberbæltazor21-Nov-13 20:05 
QuestionThe tag 'DXWindow' does not exist in XML namespace PinmemberSumeet Kumar G21-Nov-13 5:23 
AnswerRe: The tag 'DXWindow' does not exist in XML namespace PinprofessionalJean-Pierre Bachmann21-Nov-13 5:33 
QuestionDownload does not work PinmemberBoer Coene19-Nov-13 7:19 
AnswerRe: Download does not work PinprofessionalJean-Pierre Bachmann19-Nov-13 7:20 
GeneralRe: Download does not work PinmemberBoer Coene19-Nov-13 7:30 
GeneralRe: Download does not work [modified] PinprofessionalJean-Pierre Bachmann19-Nov-13 7:32 
GeneralRe: Download does not work PinmemberBoer Coene19-Nov-13 7:48 
GeneralRe: Download does not work PinprofessionalJean-Pierre Bachmann19-Nov-13 7:50 
GeneralRe: Download does not work PinmemberBoer Coene19-Nov-13 8:06 
GeneralRe: Download does not work [modified] PinprofessionalJean-Pierre Bachmann19-Nov-13 21:50 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140709.1 | Last Updated 3 Jul 2014
Article Copyright 2013 by Jean-Pierre Bachmann
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid