Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Using Ninject to produce a loosely-coupled modular WPF Application

0.00/5 (No votes)
29 Sep 2014 1  
This article discusses how to use the Ninject and Prism frameworks to produce a loosely-coupled modular WPF Application.

Introduction

A modular design adopts the Lego approach to building an application. There are lots of small blocks loosely coupled together with each block knowing little about its neighbours other than how it connects with them. This greatly facilitates project maintenance and testing as each block can be easily swapped out and replaced without disrupting the other modules. This article discusses how to use the Ninject and Prism frameworks to produce a loosely-coupled modular WPF Application.

Loose Coupling.

One of the ways that loose coupling can be achieved is by using the Inversion of Conrol (IOC) design pattern. With this pattern the bit that’s inverted is the way a class plugs into, or connects with, a dependent object. Instead of the class plugging into the object, by using the object’s New method, the object is plugged into the utilizing class, usually through the class’s constructor. A dependency injection framework is responsible both for the assembly of the object and making it available to the class that uses it. The class’s constructor often takes an Interface as a parameter so that the concrete Type passed to it can be determined by the framework and all object creation can be done at a central location.

The Ninject Dependency Injection Framework.

Ninject’s Kernel class creates the objects. This class has a container that stores references to objects using a Dictionary type List. The references are stored in the container using this sort of syntax.

Kernel.Bind<Idirector>().To<director>();

Objects are then assembled using

Kernel.Get<IDirector>();

The object returned will be the Director class. The idea behind this is that you should program to Interfaces rather than concrete classes and let the Kernel determine the concrete class that's returned. A different concrete class can be returned for testing or updating purposes by simply changing the binding in the Kernel. In the sample application, the HeadViewViewModel takes an IDirector type class as a parameter in its constructor. The HeadViewViewModel is added to the Kernel like this

//This binds the ViewModel to itself rather than an Interface.
Kernel.Bind<HeadViewViewModel>().ToSelf();

With all the above bindings in place, a call to Kernel.Get<HeadViewViewModel>(); will run the class’s constructor. Ninject will see that the constructor needs a parameter of type IDirector, it will then search its container to find the appropriate binding and inject the Director class into the constructor.

Why Use Ninject?

The decision regarding which dependency injection framework to use is mainly a matter of taste, Ninject is only one of many and they all have advantages and disadvantages. As well as being well supported and intuitive to use, Ninject has a couple of particularly attractive features. First off, there is no need to pollute dependent methods with adornments and secondly the error messages are comprehensive, detailing what has gone wrong , the possible causes and suggested remedies. The following paragraphs describe a simple way of using Ninject with Windows Presentation Foundation (WPF).

Configuring The Main Window

The ‘out of the box’ WPF application has a MainWindow as a view onto which numerous controls can be placed. The enabling code for the controls resides in the window’s code behind or its DataContext. A cleaner approach is to have a Shell window that just defines regions within it. Modules (dlls) can then mount views in a particular region without knowing anything about the Shell other than a region’s name. A RegionManager is needed to pull off this trick. This is where Ninject comes in useful as it can make the RegionManager available to any module that needs it. Here’s an example Shell window that defines two regions, a HeadRegion and a BodyRegion.

<Window x:Class="WpfNinjectMef.Shell"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:cal="clr-namespace:Microsoft.Practices.Prism.Regions;assembly=Microsoft.Practices.Prism" 
        Title="Shell"  SizeToContent="WidthAndHeight"   WindowStartupLocation="CenterScreen">
    <Grid>
        <StackPanel >
            <ItemsControl  cal:RegionManager.RegionName="HeadRegion"  />
            <ItemsControl Margin="0,20,0,0"  cal:RegionManager.RegionName="BodyRegion"   />
          
        </StackPanel>

    </Grid>
    </Window>

With the Shell window in place, it’s time to define some modules that can interact with it and with each other.

The Module

A module is nothing more than Class Library (Dll) defined in its own project within the Solution for the application. With Ninject, a module has a Module class derived from NinjectModule. This class has a Load method that enables the module to be initialised when it is first loaded.

Activating a View Region.

A RegionManager class is used to inject a view from a module into a region in the Shell window. This is usually done in the module’s Load method. The method is called when the module is loaded. In this example, a HeadView is injected into the HeadRegion of the Shell window. A view is simply a WPF UserControl


            var regionManager = this.Kernel.Get<IRegionManager>();
            var headView = this.Kernel.Get<HeadView>();
            IRegion region = regionManager.Regions["HeadRegion"];
            region.Add(headView, "HeadView");
            region.Activate(headView);

Communicating between modules.

It’s often necessary to pass objects between modules for further processing or to send notification that an event has occurred. The pattern used in the example employs the Microsoft.Practices.Prism.PubSubEvents EventAggregator. The procedure for its use is quite straightforward. First, define an event by sub-classing the PubSubEvent<T> class

  //The message sent here is of Type string but it can be any object
    public class HeadMessageSentEvent : PubSubEvent<string> 
    {
        
    }

The publisher then publishes the event as follows


aggregator.GetEvent<HeadMessageSentEvent>().Publish(this.message);

The observer subscribes to the event using something like this


aggregator.GetEvent<HeadMessageSentEvent>()
                .Subscribe(this.MessageReceived);
......
private void MessageReceived(string msg)
        {
            this.ReceivedMessage = msg;
        }

That’s it, there is no need to worry about having to decorate methods with attributes or bother with unsubscribing.

Setting the DataContext.

Ninject provides a neat way of setting a View’s DataContext. It is possible to bind a class type to a method so that, when Kernel.Get<classType>() is called, the method runs. Here’s how.

  Kernel.Bind<HeadView>()
                .ToMethod(context => new HeadView { DataContext = this.Kernel.Get<HeadViewViewModel>() });

With this binding in place, calling Kernel.Get<HeadView>(); will create a new HeadView and set its DataContext to a HeadViewViewModel class by using an object initializer.

Booting Up the Application

Before the application can run, the Ninject Kernel container has to be configured, the modules initialized and the Shell window created. This is the job of a Bootstrapper class that has the NinjectBootstrapper class as its base. The NinjectBootstrapper class is part of a Ninject extension for Prism available here.


  public class WpfNinjectBootstrapper : NinjectBootstrapper
    {
        #region Methods

        protected override void ConfigureContainer()
        {
            base.ConfigureContainer();
//The aggregator needs to be a singleton otherwise each module will get a different copy
            this.Kernel.Bind<IEventAggregator>().To<EventAggregator>().InSingletonScope();
        }

        protected override DependencyObject CreateShell()
        {
            return new Shell();
        }

        protected override void InitializeModules()
        {
            base.InitializeModules();
//Load all NinjectModule type dlls in the current directory into the Kernel
// and call  each module's Load method
//Need to add references in this project to the modules that are situated in other 
//projects for this to work.
            Kernel.Load("*.dll");
            }

        protected override void InitializeShell()
        {
            base.InitializeShell();
            Application.Current.MainWindow = (Shell)this.Shell;
            Application.Current.MainWindow.Show();
        }

        #endregion
    }

Configuring App.Xaml to run the Bootstrapper.

The default App.xaml has the following in the opening Application tag.


StartupUri="MainWindow.xaml"

This needs to be deleted and the method below inserted into App.xaml.cs.


 protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);

            var bootstrapper = new WpfNinjectBootstrapper();
            bootstrapper.Run(true);

        }

Everything is now ready to go. It’s a good idea to keep a weather eye on the output window when running in debug mode. Some errors can lead to a severe loss of functionality without actually breaking the application. Tracking them down in the output window can save a lot of head scratching.

The Sample Applications.

There are two sample demos, one using WPF and the other Silverlight. Each application is very basic, they only have two NinjectModules. Each module mounts a view in a separate region of the Shell window. A nonsensical message is exchanged between the modules and displayed in the window when either one of the two buttons is pressed. The application uses the Prism.NinjectExtension. This extension seems to depend on historical versions of Prism and Ninject so the extension source code has been imported into the solution in order that the latest version of its dependencies can be used. I am grateful to rhyswalden for making the code available.

Conclusion.

There is a lot more to Ninject and the modular design pattern than is presented here but it is hoped that this brief and somewhat over-simplistic introduction can form a basis for further reading.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here