かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

WinRTのMEFでServiceLocator実装してみた

お約束:Windows 8 CP時点の内容です。正式版では変わってるかもしれません。

WinRTのMEFってCompositionContainerクラスがいなくなってCompositionServiceクラスでSatisfyImportsOnceメソッド使って対象クラスに何かをImportするということしかできないっぽいです。なのでMVVM Light 4のDIコンテナにMEF使ってやろうと思ったらいきなり挫折してしまいましたorz

それじゃぁ悔しいので、無理やりCompositionServiceクラスを使った状態でIServiceLocatorインターフェースを実装してみました。ちなみにMVVM Light 4に添付されてるMicrosoft.Practices.ServiceLocation.dllを使います。

実装

ということでさくっと実装してみました。SatisfyImportsOnceメソッドにRegistrationBuilderのインスタンスを渡せば実行時にImportの挙動とかをいじれるみたいなので、それを使ってごにょごにょっとしてます。

namespace SampleImplementation
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.Composition;
    using System.ComponentModel.Composition.Hosting;
    using System.ComponentModel.Composition.Primitives;
    using System.ComponentModel.Composition.Registration;
    using System.Linq;
    using System.Reflection;
    using Microsoft.Practices.ServiceLocation;

    public class MefServiceLocator : IServiceLocator
    {
        private CompositionService service;

        public MefServiceLocator(params ComposablePartCatalog[] catalogs)
        {
            // カタログからCompositionServiceを作成
            var catalog = new AggregateCatalog(catalogs);
            this.service = catalog.CreateCompositionService();
        }

        private IEnumerable<object> DoGetAllInstances(Type serviceType)
        {
            // ManyValueHolder<T>型を作成
            var holderType = typeof(ManyValueHolder<>).MakeGenericType(serviceType);
            // ValuesプロパティにImportManyをつけてるイメージ
            var b = new RegistrationBuilder();
            b.ForType(holderType)
                .ImportProperties(p => true, (p, c) => c.AsMany(true));

            // ManyValueHolder<T>型のインスタンスを作成してImport
            var holderInstance = Activator.CreateInstance(holderType);
            this.service.SatisfyImportsOnce(holderInstance, b);

            // Valuesプロパティの値を取得して返却
            var valuePropertyInfo = holderType.GetTypeInfo().GetDeclaredProperty("Values");
            return (IEnumerable<object>) valuePropertyInfo.GetValue(holderInstance);
        }

        private object DoGetInstance(Type serviceType, string key)
        {
            // SingleValueHolder<T>型を作成
            var holderType = typeof(SingleValueHolder<>).MakeGenericType(serviceType);
            // ValueプロパティにImport属性をつけてるイメージ
            // コントラクト名が指定されている場合はそれも追加する
            var b = new RegistrationBuilder();
            b.ForType(holderType)
                .ImportProperties(p => true, (p, c) =>
                {
                    if (!string.IsNullOrEmpty(key))
                    {
                        c.AsContractName(key);
                    }
                });

            // SingleValueHolder<T>型のインスタンスを作成してImport
            var holderInstance = Activator.CreateInstance(holderType);
            this.service.SatisfyImportsOnce(holderInstance, b);

            // Valueプロパティの値を取得して返却
            var valuePropertyInfo = holderType.GetTypeInfo().GetDeclaredProperty("Value");
            return valuePropertyInfo.GetValue(holderInstance);
        }

        public IEnumerable<TService> GetAllInstances<TService>()
        {
            return DoGetAllInstances(typeof(TService)).Cast<TService>();
        }

        public IEnumerable<object> GetAllInstances(Type serviceType)
        {
            return DoGetAllInstances(serviceType);
        }

        public TService GetInstance<TService>(string key)
        {
            return (TService)DoGetInstance(typeof(TService), key);
        }

        public TService GetInstance<TService>()
        {
            return (TService) DoGetInstance(typeof(TService), null);
        }

        public object GetInstance(Type serviceType, string key)
        {
            return DoGetInstance(serviceType, key);
        }

        public object GetInstance(Type serviceType)
        {
            return DoGetInstance(serviceType, null);
        }
    }

    /// <summary>
    /// 単一のインスタンスをImportするためのクラス
    /// </summary>
    /// <typeparam name="T"></typeparam>
    class SingleValueHolder<T>
    {
        public T Value { get; set; }
    }

    /// <summary>
    /// 複数のインスタンスをImportするためのクラス
    /// </summary>
    /// <typeparam name="T"></typeparam>
    class ManyValueHolder<T>
    {
        public IEnumerable<T> Values { get; set; }
    }
}

こんな感じで実装完了!通したテストは以下のような感じ。

namespace MefServiceLocatorImpl.Test
{
    using System.ComponentModel.Composition;
    using System.ComponentModel.Composition.Hosting;
    using System.Linq;
    using System.Reflection;
    using Microsoft.VisualStudio.TestTools.UnitTesting;
    using SampleImplementation;

    [TestClass]
    public class MefServiceLocatorTest
    {
        private MefServiceLocator locator;

        [TestInitialize]
        public void Initialize()
        {
            this.locator = new MefServiceLocator(
                new AssemblyCatalog(typeof(MefServiceLocatorTest).GetTypeInfo().Assembly));
        }

        [TestCleanup]
        public void Cleanup()
        {
            this.locator = null;
        }

        [TestMethod]
        public void InitTest()
        {
            Assert.IsNotNull(this.locator);
        }

        [TestMethod]
        public void SingleExportTest()
        {
            var target = locator.GetInstance<ExportTarget>();
            Assert.IsNotNull(target);
        }

        [TestMethod]
        public void NamedSingleExportTest()
        {
            var target = locator.GetInstance<NamedExportTarget>("Sample");
            Assert.IsNotNull(target);
        }

        [TestMethod]
        [ExpectedException(typeof(CompositionException))]
        public void NamedSingleExportMissingTest()
        {
            locator.GetInstance<NamedExportTarget>("MissingName");
        }

        [TestMethod]
        public void ManyExportTest()
        {
            var targets = locator.GetAllInstances<IManyExportTarget>();
            Assert.IsNotNull(targets);
            Assert.AreEqual(targets.Count(), 3);
        }
    }

    [Export]
    public class ExportTarget
    {
    }

    [Export("Sample")]
    public class NamedExportTarget { }

    public interface IManyExportTarget { }

    [Export(typeof(IManyExportTarget))]
    public class ManyExportTarget1 : IManyExportTarget { }

    [Export(typeof(IManyExportTarget))]
    public class ManyExportTarget2 : IManyExportTarget { }
    
    [Export(typeof(IManyExportTarget))]
    public class ManyExportTarget3 : IManyExportTarget { }
}

これでオールグリーン!ということでMVVM Light 4のSimpleIoCじゃなくてMEFを使えるようになりそうです。