Using Ninject and XAML
Since we’re trying to make vNext more testable we need to have a nice IoC implementation that makes it easy to wire everything together and still leverage the full power of XAML. The good news is this is really easy to do. Simon Ferquel implemented an IoC-enabled Xaml parser that used Unity as the IoC container. This was easy enough to modify to use Ninject instead.
Basically there are three classes that we need to provide implementations of:
- XamlSchemaContext
- XamlType
- XamlTypeInvoker
The first class, XamlSchemaContext, provides the entry point to implementing an IoC container. This provides a context for any XAML operations including retrieving types to be instantiated. The full implementation of the class is as follows:
public class NinjectXamlSchemaContext
: XamlSchemaContext
{
private readonly IKernel kernel;
public NinjectXamlSchemaContext(IKernel kernel)
{
this.kernel = kernel;
}
protected override XamlType GetXamlType(string xamlNamespace,
string name, params XamlType[] typeArguments)
{
var retval = base.GetXamlType(xamlNamespace, name, typeArguments);
return retval == null ?
null :
new NinjectXamlType(this.kernel, retval.UnderlyingType, this);
}
}
The important part is the GetXamlType() override. This allows us to return a custom XAML type that uses Ninject instead of activation via reflection. I also added a constructor that takes in the Ninject kernel to allow storing the kernel externally but this could be moved internally if there is no need to pass the kernel around.
The second type to implement is XamlType. This provides a set of methods for hooking into the initialisation process. This implementation is:
public class NinjectXamlType
: XamlType
{
private readonly NinjectXamlTypeInvoker invoker;
public NinjectXamlType(IKernel kernel, Type type, XamlSchemaContext context)
: base(type, context)
{
this.invoker = new NinjectXamlTypeInvoker(kernel, type, this);
}
protected override XamlTypeInvoker LookupInvoker()
{
return this.invoker;
}
protected override bool LookupIsConstructible()
{
return true;
}
}
The main override is LookupInvoker(). This returns our custom invoker for new types. By itself XamlType doesn’t seem to do much it mainly appears to act as a coordinator for various look-ups, hence overriding the invoker method.
The final class is XamlTypeInvoker, which is the workhorse for our example. This type actually generates the various object instances. Thus our implementation is:
public class NinjectXamlTypeInvoker
: XamlTypeInvoker
{
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
private readonly IKernel kernel;
private readonly Type typeToResolve;
public NinjectXamlTypeInvoker(IKernel kernel, Type typeToResolve, XamlType xamlType)
: base(xamlType)
{
this.kernel = kernel;
this.typeToResolve = typeToResolve;
}
public override object CreateInstance(object[] arguments)
{
logger.Trace("Generating an instance of '{0}'", this.typeToResolve.FullName);
return this.kernel.Get(this.typeToResolve);
}
}
Again there is one method we are overriding – CreateInstance(). In our case all we are doing is going to the kernel which creates the instance for us and wires everything up. I’ve also added so logging so we can hook in later to see what is actually happening.
The last piece of the puzzle is how to call these classes. To make this easier I have added a wrapper class:
public class ConfigurationService
{
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
private readonly IKernel kernel;
private readonly XamlSchemaContext context;
public ConfigurationService()
{
this.kernel = new StandardKernel(new CoreModule());
this.context = new NinjectXamlSchemaContext(this.kernel);
}
public Server Load(Stream stream)
{
logger.Debug("Loading configuration from stream");
var reader = new XamlXmlReader(stream, this.context);
var server = XamlServices.Load(reader) as Server;
return server;
}
}
At the moment there is only one thing we are loading – the server configuration – but later this may change. To use this class all we need to do is create a new instance, open a stream containing the configuration and call Load().
To use dependency injection with a configuration class we just need to reference one of the standard interfaces and put an Inject attribute on it:
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[EditorBrowsable(EditorBrowsableState.Never)]
[Inject]
public IFileSystem FileSystem { get; set; }
I also add DesignerSerializationVisibility and EditorBrowsable to make sure it does not appear in any outputted XAML and the designer.
Finally the class that wires together the various interfaces and their implementations is CoreModule. This is just a standard Ninject kernel implementation so I won’t go over it here.
Hopefully this covers all the details around how we can use Ninject together with Xaml.