Tuesday, April 23, 2019

Dependency Injection for Idiots

I really wish there was a book with the title "Dependency Injection for Idiots". Almost all resources I can find online assume you know already what it is as they explain it to you. But I did find this blog entry, which is a good place to start.

I put together a project that demonstrates non-dependency-injection, then manual dependency injection, and finally Ninject. Curiously, the Ninject website has no free demos or walkthroughs.

Here's an example of defining a class that will print the current date time on the MainWindow without using Dependency Injection.

Without Dependency Injection

Start a new WPF, C# project called NinjectDemo. Add a class called cLogTime which contains the following code. It defines a single void method that replaces the content of MainWindow with the current date and time.

using System;
using System.Windows;

namespace NinjectDemo
{
    class cLogTime
    {
        public void LogTime()
        {
            Application.Current.MainWindow.Content = DateTime.Now.ToString();
        }
    }
}

The code behind instantiates the class and calls the LogTime method.

using System.Windows;

namespace NinjectDemo
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            cLogTime TimeLogger = new cLogTime();
            TimeLogger.LogTime();
        }
    }
}


When you run the application, the result is...


But perhaps there are better ways to show the time to the user. Perhaps a message box, or you could write to the output window in Visual Studio. To change the way the time is displayed should not require changes to the cLogTime class. Dependency Injection provides a way for the calling code to tell cLogTime how the time should be logged so cLogTime can concentrate on determining the time.

Manual Dependency Injection

Let's create an Interface that defines a Print method and then write some classes that implement that method in different ways.

Add an Interface file to the project and call it ILogger. For the purpose of brevity we will include the implementations of the interface in the same file, although normally we would not.

using System;
using System.Diagnostics;
using System.Windows;

namespace NinjectDemo
{
    interface ILogger
    {
        void Print(String Msg);
    }

    public class LogToMsgBox : ILogger
    {
        public void Print(string Msg)
        {
            MessageBox.Show(Msg);
        }
    }

    public class LogToOutput : ILogger
    {
        public void Print(string Msg)
        {
            Debug.WriteLine(Msg);
        }
    }

    public class LogToWindow : ILogger
    {
        public void Print(string Msg)
        {
            Application.Current.MainWindow.Content = Msg;
        }
    }
}

We can now pass the implementation of ILogger we want cLogTime to use through the constructor (or via a property is also popular). Add a constructor to cLogTime - it will take an ILogger parameter and store it locally. When the LogTime method is called, we will call the Print method on the specified ILogger. Replace cLogTime.cs with this code.

using System;

namespace NinjectDemo
{
    class cLogTime
    {
        ILogger Logger = null;
        public cLogTime(ILogger LogDevice)
        {
            Logger = LogDevice;
        }

        public void LogTime()
        {
            Logger.Print(DateTime.Now.ToString());
        }
    }
}


MainWindow now instantiates an implementation of ILogger and passes it to the cLogTime constructor. When we call LogTime now, cLogTime will use the Logger we passed.

using System.Windows;

namespace NinjectDemo
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            ILogger Logger = new LogToWindow();
            cLogTime TimeLogger = new cLogTime(Logger);
            TimeLogger.LogTime();
        }
    }
}


If you run this you will see exactly the same result as before. But if you replace

ILogger Logger = new LogToWindow();

with

ILogger Logger = new LogToMsgBox();

you will see the date/time in a popup window instead.


This technique is also called IoC (Inversion of Control) because the calling code (MainWindow) is saying to cLogTime "You will use this thing" instead of cLogTime saying "I will use this thing". You will also hear the term SoC (Separation of Concerns) because cLogTime is only concerned with getting the time and can leave the act of displaying it to ILogger.

This is manual Dependency Injection and it can be very useful, but as projects get larger it can be difficult to manage which is where a framework like Ninject comes in useful.

Dependency Injection using Ninject

Start by using Nuget to fetch Ninject. Click Tools -> Nuget Package Manager -> Manage Nuget Packages for Solution...

Click Browse and search for Ninject. Install it for the project. This creates a packages.config file and adds a reference.


Add a new class called Bindings which will contain bindings between interfaces and implementations. The class can be called anything and be anywhere in the project as long as it implements NinjectModule. This example says that whenever anything needs an implementation of ILogger, return an instance of LogToWindow. It can get much more complex than this!

using Ninject.Modules;
using Ninject;

namespace NinjectDemo
{
    public class Bindings : NinjectModule
    {
        public override void Load()
        {
            Bind<ILogger>().To<LogToWindow>();
        }
    }
}


We need to modify MainWindow to use this Ninject binding. The syntax is quite simple.

using System.Reflection;
using System.Windows;
using Ninject;

namespace NinjectDemo
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            var kernel = new StandardKernel();
            kernel.Load(Assembly.GetExecutingAssembly());

            ILogger Logger = kernel.Get<ILogger>();
            cLogTime TimeLogger = new cLogTime(Logger);
            TimeLogger.LogTime();
        }
    }
}


We don't have to make any changes to cLogTime to implement this. Run it again and you'll see the date/time in the window. Change the binding to see the date/time in a popup instead.

My biggest complaint about Ninject is the lack of official documentation. The Ninject.org website has links to blogs that no longer exist, their documentation page doesn't load, and there's a link to an eBook that I don't want to pay for. Unfortunately these problems make it look as though Ninject is no longer being actively supported.

Ninject's "Documentation" page never loads
So I ask myself, why not use MEF to locate code resources? It's part of the framework already.

Dependency Injection using MEF

The MEF version of this demo adds the ability to dynamically select which logger to use at run time. It starts with the Ninject demo we completed above.

Remove reference to Ninject and add a reference to System.ComponentModel.Composition. Remove packages.config.

In ILogger.cs add an ILoggerData interface - MEF uses this to help use decide which logger to use. Then decorate the three logger implementations with Export and ExportMetadata tags. We will create an enumeration to list the types of logger that are available.

Note both interfaces must be public. The end result looks like this.

using System;
using System.Diagnostics;
using System.Windows;
using System.ComponentModel.Composition;

namespace NinjectDemo
{
    public interface ILogger
    {
        void Print(String Msg);
    }

    public interface ILoggerData
    {
        eLogType Type { get;}
    }

    public enum eLogType
    {
        Window,
        MsgBox,
        Output
    }

    [Export(typeof(ILogger))]
    [ExportMetadata("Type"eLogType.MsgBox)]
    public class LogToMsgBox : ILogger
    {
        public void Print(string Msg)
        {
            MessageBox.Show(Msg);
        }
    }

    [Export(typeof(ILogger))]
    [ExportMetadata("Type"eLogType.Output)]
    public class LogToOutput : ILogger
    {
        public void Print(string Msg)
        {
            Debug.WriteLine(Msg);
        }
    }

    [Export(typeof(ILogger))]
    [ExportMetadata("Type"eLogType.Window)]
    public class LogToWindow : ILogger
    {
        public void Print(string Msg)
        {
            Application.Current.MainWindow.Content = Msg;
        }
    }
}

The kernal code in MainWindow will now be replaced by container code. The concepts are similar although the MEF approach requires more code. The end results is that we have a collection of ILogger implementations and we can chose which one to use by looking at the Metadata.Type. Here is the new MainWindow.cs.

using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.Linq;
using System.Reflection;
using System.Windows;

namespace NinjectDemo
{
    public partial class MainWindow : Window
    {
        private CompositionContainer _container;
        [ImportMany] IEnumerable<Lazy<ILogger, ILoggerData>> Loggers;
        const eLogType LogType = eLogType.Window;

        public MainWindow()
        {
            AggregateCatalog catalog = new AggregateCatalog();
            catalog.Catalogs.Add(new AssemblyCatalog(System.Reflection.Assembly.GetExecutingAssembly()));
            _container = new CompositionContainer(catalog);
            this._container.ComposeParts(this);
           
            InitializeComponent();

            ILogger Logger = Loggers.FirstOrDefault(l => (l.Metadata.Type == LogType)).Value;
            cLogTime TimeLogger = new cLogTime(Logger);
            TimeLogger.LogTime();
        }
    }
}

In this demo I have not checked to ensure we find a Logger. The LogTime method should check that it is passed a valid Logger before it attempts to use it. If you run this demo, you will see it looks exactly the same as the Ninject version.


Try changing the LogType const to eLogType.MsgBox to see Dependency Injection in all its glory!

Conclusion

Dependency Injection is a good thing. You have many options for finding classes and injecting them. Is Ninject or MEF the better of the two? Given that MEF is Microsoft's solution and is part of the framework whereas Ninject's website needs attention - I would prefer to use MEF. But don't forget Unity, CastleWindsor, and the other Dependency Injection frameworks out there.

Dependency Injection with MEF Solution

No comments:

Post a Comment