かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

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を使えるようになりそうです。