Wednesday, February 13, 2013

A second take on Constructor Injection IoC in MvvmCross - and a Badge of Awesomeness

Received this today - fab contribution from @Asudbury

By changing some of the OpenNetCf IoC files (by removing some of the changes I made!), Adrian's managed to get 'proper' IoC working - i.e. he's using code that has constructors like:

        ///

        /// Initializes a new instance of the class.
        ///

        /// The service.
        /// The translator.
        [MvxOpenNetCfInjectionAttribute]
        public LoginModel(
            IAuthenticationService service,
            ITranslator translator)
        {
            this.service = service;
            this.translator = translator;
        }

This isn't the only way to do this sort of thing - I know that someone else has also got TinyIoC working really cleanly - using some techniques built around http://slodge.blogspot.co.uk/2013/01/navigating-between-viewmodels-by-more.html - hopefully will share that with you soon too.

The code changes from Adrian are in the attached gist....
But in the meantime... Adrian - thanks - a badge of awesomeness is your's :)





// MvxOpenNetCfContainer.cs
// (c) Copyright Cirrious Ltd. http://www.cirrious.com
// MvvmCross is licensed using Microsoft Public License (Ms-PL)
// Contributions and inspirations noted in readme.md and license.txt
//
// Project Lead - Stuart Lodge, @slodge, me@slodge.com
#region Credit - OpenNetCf
// This file is based on the OpenNetCf IoC container - used under free license -see http://ioc.codeplex.com
// Note that this is not the standard OpenNetCf IoC container - we've removed their instance caching
#endregion
#region
using System;
using System.Collections.Generic;
using System.Linq;
using Cirrious.MvvmCross.Core;
using Cirrious.MvvmCross.Exceptions;
#endregion
namespace Cirrious.MvvmCross.OpenNetCfIoC
{
using System.Reflection;
public sealed class MvxOpenNetCfContainer
: MvxSingleton<MvxOpenNetCfContainer>
{
private readonly Dictionary<string, object> _items = new Dictionary<string, object>();
private readonly object _syncRoot = new object();
private readonly Dictionary<Type, Type> _toResolve = new Dictionary<Type, Type>();
/// <summary>
/// Gets the current instance of the IOC container.
/// </summary>
/// <value>The current.</value>
public static MvxOpenNetCfContainer Current
{
get { return Instance ?? new MvxOpenNetCfContainer(); }
}
/// <summary>
/// Registers the type.
/// </summary>
/// <typeparam name = "TFrom">The type of from.</typeparam>
/// <typeparam name = "TTo">The type of to.</typeparam>
public void RegisterServiceType<TFrom, TTo>()
{
if (_toResolve.ContainsKey(typeof (TFrom)))
throw new MvxException("Type already register");
Type type = typeof(TTo);
_toResolve.Add(typeof (TFrom), type);
//// Adrian Sudbury extended code 31st Jan 2013
//// Here we are looking for constructors decorated with the attribute MvxOpenNetCfInjectionAttribute.
IEnumerable<ConstructorInfo> constructors = (type.GetConstructors(
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
.Where(
c =>
c.IsPublic &&
c.GetCustomAttributes(typeof (MvxOpenNetCfInjectionAttribute), true).Any()));
if (constructors.Any())
{
//// If we find decorated constructor build type now.
//// This was the only way i could get it to work -
//// i couldnt see how to do deffered object construction.
MvxOpenNetCfObjectBuilder.CreateObject(type);
}
//// End of extended code.
}
/// <summary>
/// Registers the type.
/// </summary>
/// <param name="typeFrom">The type from.</param>
/// <param name="typeTo">The type to.</param>
public void RegisterServiceType(Type typeFrom, Type typeTo)
{
if (_toResolve.ContainsKey(typeFrom))
throw new MvxException("Type already register");
_toResolve.Add(typeFrom, typeTo);
}
public bool CanResolve<TToResolve>() where TToResolve : class
{
var typeToBuild = typeof (TToResolve);
if (_items.ContainsKey(typeToBuild.FullName))
return true;
return CanCreateInstance(typeToBuild);
}
public TToResolve Resolve<TToResolve>() where TToResolve : class
{
TToResolve toReturn;
if (!TryResolve(out toReturn))
{
throw new MvxException("Unable to Resolve IoC type {0}", typeof (TToResolve));
}
return toReturn;
}
public bool TryResolve<TToResolve>(out TToResolve instance) where TToResolve : class
{
var typeToBuild = typeof (TToResolve);
object objectReference;
if (_items.TryGetValue(typeToBuild.FullName, out objectReference))
{
instance = objectReference as TToResolve;
return true;
}
if (!CanCreateInstance(typeof (TToResolve)))
{
instance = null;
return false;
}
instance = CreateInstance<TToResolve>(typeToBuild);
return true;
}
private bool CanCreateInstance(Type typeToBuild)
{
#if NETFX_CORE
if (typeToBuild.GetTypeInfo().IsInterface)
return _toResolve.ContainsKey(typeToBuild);
#else
if (typeToBuild.IsInterface)
return _toResolve.ContainsKey(typeToBuild);
#endif
// note - the original opennetCf container supported direct type creation - but Mvx supports interfaces only
throw new MvxException("Unexpected non interface type in call to CanCreateInstance " + typeToBuild.Name);
}
private TToResolve CreateInstance<TToResolve>(Type typeToBuild) where TToResolve : class
{
#if NETFX_CORE
if (typeToBuild.GetTypeInfo().IsInterface)
typeToBuild = _toResolve[typeToBuild];
#else
if (typeToBuild.IsInterface)
typeToBuild = _toResolve[typeToBuild];
#endif
var instance = MvxOpenNetCfObjectBuilder.CreateObject(typeToBuild);
return (TToResolve) instance;
}
/// <summary>
/// Create a new instance at each time
/// </summary>
/// <returns></returns>
public object GetInstance(Type typeToBuild)
{
var instance = MvxOpenNetCfObjectBuilder.CreateObject(typeToBuild);
return instance;
}
/// <summary>
/// Create a new instance at each time
/// </summary>
/// <typeparam name = "T"></typeparam>
/// <returns></returns>
public T GetInstance<T>()
{
var typeToBuild = typeof (T);
return (T) GetInstance(typeToBuild);
}
/// <summary>
/// Create an instance of the required type
/// </summary>
/// <param name="typeToBuild">type</param>
/// <returns></returns>
public object Resolve(Type typeToBuild)
{
if (_items.ContainsKey(typeToBuild.FullName))
return _items[typeToBuild.FullName];
return CreateInstance<object>(typeToBuild);
}
/// <summary>
/// Registers the instance.
/// </summary>
/// <typeparam name="TInterface">The type of the interface.</typeparam>
/// <param name="instance">The instance.</param>
public void RegisterServiceInstance<TInterface>(TInterface instance)
{
if (_items.ContainsKey(typeof (TInterface).FullName))
return;
Add(instance, typeof (TInterface).FullName);
}
/// <summary>
/// Registers the instance.
/// </summary>
/// <param name="interfaceToRegister">The interface to register.</param>
/// <param name="instance">The instance.</param>
public void RegisterServiceInstance(Type interfaceToRegister, object instance)
{
if (_items.ContainsKey(interfaceToRegister.FullName))
_items.Remove(interfaceToRegister.FullName);
Add(instance, interfaceToRegister.FullName);
}
/// <summary>
/// Adds the specified item.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="id">The id.</param>
internal void Add(object item, string id)
{
if (id == null)
throw new ArgumentNullException("id");
if (item == null)
throw new ArgumentNullException("item");
lock (_syncRoot)
{
_items.Add(id, item);
}
}
/// <summary>
/// Finds the type of the by.
/// </summary>
/// <typeparam name="TSearchType">The type of the search type.</typeparam>
/// <returns></returns>
public IEnumerable<TSearchType> FindByType<TSearchType>() where TSearchType : class
{
return (_items.Where(i => i.Value is TSearchType).Select(i => i.Value as TSearchType));
}
/// <summary>
/// Finds the type of the by.
/// </summary>
/// <param name="searchType">Type of the search.</param>
/// <returns></returns>
public IEnumerable<object> FindByType(Type searchType)
{
#if NETFX_CORE
if (searchType.GetTypeInfo().IsValueType)
throw new ArgumentException("searchType must be a reference type");
if (searchType.GetTypeInfo().IsInterface)
{
return (_items.Where(i => i.Value.GetType().GetTypeInfo().ImplementedInterfaces.Contains(searchType)).Select(i => i.Value));
}
return (_items.Where(i => i.Value.GetType().Equals(searchType)).Select(i => i.Value));
#else
if (searchType.IsValueType)
throw new ArgumentException("searchType must be a reference type");
if (searchType.IsInterface)
{
return (_items.Where(i => i.Value.GetType().GetInterfaces().Contains(searchType)).Select(i => i.Value));
}
return (_items.Where(i => i.Value.GetType().Equals(searchType)).Select(i => i.Value));
#endif
}
/// <summary>
/// Gets the type of the resolved.
/// </summary>
/// <param name="toResolve">To resolve.</param>
/// <returns></returns>
internal object GetResolvedType(Type toResolve)
{
if (!_toResolve.ContainsKey(toResolve))
throw new MvxException("Type is not register");
if (!_items.ContainsKey(toResolve.FullName))
return Resolve(toResolve);
return _toResolve[toResolve];
}
}
}
// MvxOpenNetCfObjectBuilder.cs
// (c) Copyright Cirrious Ltd. http://www.cirrious.com
// MvvmCross is licensed using Microsoft Public License (Ms-PL)
// Contributions and inspirations noted in readme.md and license.txt
//
// Project Lead - Stuart Lodge, @slodge, me@slodge.com
#region Credit - OpenNetCf
// This file is based on the OpenNetCf IoC container - used under free license -see http://ioc.codeplex.com
#endregion
#region
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Cirrious.MvvmCross.Exceptions;
#endregion
namespace Cirrious.MvvmCross.OpenNetCfIoC
{
internal class MvxOpenNetCfObjectBuilder
{
private static readonly Dictionary<Type, InjectionConstructor> ConstructorCache =
new Dictionary<Type, InjectionConstructor>();
/// <summary>
/// Creates the object.
/// </summary>
/// <param name = "type">The type.</param>
/// <returns></returns>
internal static object CreateObject(Type type)
{
if (type == null)
throw new ArgumentNullException("type");
object instance = null;
// first check the cache
if (ConstructorCache.ContainsKey(type))
return CreateObjectFromCache(type);
#if NETFX_CORE
if (type.GetTypeInfo().IsInterface)
#else
if (type.IsInterface)
#endif
throw new MvxException(string.Format("Cannot create an instance of an interface ({0}).", type.Name));
#if NETFX_CORE
var ctors =
(type.GetTypeInfo().DeclaredConstructors
.Where(
c =>
c.IsPublic && c.GetCustomAttributes(typeof (MvxOpenNetCfInjectionAttribute), true).Count() > 0));
#else
var ctors =
(type.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
.Where(
c =>
c.IsPublic && c.GetCustomAttributes(typeof (MvxOpenNetCfInjectionAttribute), true).Count() > 0));
#endif
if (!ctors.Any())
{
// no injection ctor, get the default, parameterless ctor
#if NETFX_CORE
var parameterlessCtors =
(type.GetTypeInfo().DeclaredConstructors.Where(c => c.IsPublic && c.GetParameters().Length == 0));
#else
var parameterlessCtors =
(type.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance).Where(c => c.GetParameters().Length == 0));
#endif
if (!parameterlessCtors.Any())
{
throw new ArgumentException(
string.Format(
"Type '{0}' has no public parameterless constructor or injection constructor.\r\nAre you missing the InjectionConstructor attribute?",
type));
}
// create the object
var constructorInfo = parameterlessCtors.First();
try
{
instance = constructorInfo.Invoke(null);
ConstructorCache.Add(type, new InjectionConstructor {ConstructorInfo = constructorInfo});
}
catch (TargetInvocationException ex)
{
throw ex.InnerException;
}
}
else if (ctors.Count() == 1)
{
// call the injection ctor
var constructorInfo = ctors.First();
var parameterInfos = constructorInfo.GetParameters();
var parameters = GetParameterObjectsForParameterList(parameterInfos, type.Name);
try
{
instance = constructorInfo.Invoke(parameters.ToArray());
ConstructorCache.Add(type,
new InjectionConstructor
{ConstructorInfo = constructorInfo, ParameterInfos = parameterInfos});
}
catch (TargetInvocationException ex)
{
throw ex.InnerException;
}
}
else
{
throw new ArgumentException(
string.Format("Type '{0}' has {1} defined injection constructors. Only one is allowed", type.Name,
ctors.Count()));
}
//// Adrian Sudbury 31st Jan 2013 - this line is commented out in the original version!
DoInjections(instance);
return instance;
}
/// <summary>
/// Does the injections.
/// </summary>
/// <param name="instance">The instance.</param>
internal static void DoInjections(object instance)
{
var t = instance.GetType();
// look for service dependecy setters
#if NETFX_CORE
var serviceDependecyProperties = t.GetTypeInfo().DeclaredProperties
#else
var serviceDependecyProperties = t.GetProperties(BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance)
#endif
.Where(p =>
p.GetCustomAttributes(
typeof (MvxOpenNetCfDependencyAttribute), true).
Count() > 0);
foreach (var pi in serviceDependecyProperties)
{
var resolved = MvxOpenNetCfContainer.Current.GetResolvedType(pi.PropertyType);
if (resolved != null)
pi.SetValue(instance, resolved, null);
else
pi.SetValue(instance, CreateObject(pi.PropertyType), null);
}
}
/// <summary>
/// Creates the object from cache.
/// </summary>
/// <param name="type">The type.</param>
/// <returns></returns>
private static object CreateObjectFromCache(Type type)
{
if (type == null)
throw new ArgumentNullException("type");
var injectionConstructor = ConstructorCache[type];
try
{
if ((injectionConstructor.ParameterInfos == null) || (injectionConstructor.ParameterInfos.Length == 0))
{
return injectionConstructor.ConstructorInfo.Invoke(null);
}
var parameters = GetParameterObjectsForParameterList(
injectionConstructor.ParameterInfos, type.Name);
return injectionConstructor.ConstructorInfo.Invoke(parameters.ToArray());
}
catch (TargetInvocationException ex)
{
throw ex.InnerException;
}
}
/// <summary>
/// Gets the parameter objects for parameter list.
/// </summary>
/// <param name="parameterInfos">The parameter infos.</param>
/// <param name="typeName">Name of the type.</param>
/// <returns></returns>
private static IEnumerable<object> GetParameterObjectsForParameterList(
IEnumerable<ParameterInfo> parameterInfos, string typeName)
{
if (parameterInfos == null)
throw new ArgumentNullException("parameterInfos");
if (string.IsNullOrEmpty(typeName))
throw new ArgumentNullException("typeName");
var paramObjects = new List<object>();
foreach (var pi in parameterInfos)
{
#if NETFX_CORE
if (pi.ParameterType.GetTypeInfo().IsValueType)
#else
if (pi.ParameterType.IsValueType)
#endif
{
throw new ArgumentException(
string.Format("Injection on type '{0}' cannot have value type parameters",
typeName));
}
if (Equals(pi.ParameterType.FullName, typeof (MvxOpenNetCfContainer).FullName))
{
paramObjects.Add(MvxOpenNetCfContainer.Current);
continue;
}
// see if there is an item that matches the type
var itemList = MvxOpenNetCfContainer.Current.FindByType(pi.ParameterType).ToList();
if (!itemList.Any())
itemList.Add(MvxOpenNetCfContainer.Current.GetResolvedType(pi.ParameterType));
paramObjects.Add(itemList.First());
}
return paramObjects.ToArray();
}
#region Nested InjectionConstructor
private struct InjectionConstructor
{
public ConstructorInfo ConstructorInfo { get; set; }
public ParameterInfo[] ParameterInfos { get; set; }
}
#endregion
}
}

3 comments:

  1. Are you going to merge this into MvvmCross so it works like this by default?

    ReplyDelete
  2. The code will get merged back in, yes.

    Currently undecided on where ioc should go in v3. A bit of me likes the idea of switching constructor and/or property injection, but also I toyed with the idea of moving the current Get and Register extension methods to Object.

    Interested to hear opinions. 100% sure that there isn't one right answer.

    ReplyDelete
  3. I prefer construction injection over property injection.

    Maybe that's because in the main that's what I always use - its like asking a Java programmer 'do you prefer Java or C#?'

    To me if a class really needs something to get its job done it should be passed on the constructor - its much clearer - its saying to use me I want these things passing to me when you construct me.

    I once worked on someone else's code that used property injection and totally missed that it was done this way and wasted hours on a problem just because to me it was unclear the way in which the class should be constructed/used.

    maybe the last paragraph says more about me instead of coding styles :-(

    asudbury

    ReplyDelete