かずきのBlog@hatena

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

Expression BlendのMVVMサポート機能とMEFの連携

Expression blendを見てると、MVVMをサポートした形のUserControlのテンプレートがあります。こいつは以下のようなコードを作成してくれます。

<UserControl
	xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
	xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
	xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
	xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
	mc:Ignorable="d"
	xmlns:local="clr-namespace:SilverlightApplication1"
	x:Class="SilverlightApplication1.View1"
	d:DesignWidth="640" d:DesignHeight="480">
	<UserControl.Resources>
		<local:View1Model x:Key="View1ModelDataSource" />
	</UserControl.Resources>

	<Grid x:Name="LayoutRoot" DataContext="{Binding Source={StaticResource View1ModelDataSource}}"/>
</UserControl>
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Data;
using System.ComponentModel;

namespace SilverlightApplication1
{
	public class View1Model : INotifyPropertyChanged
	{
		public View1Model()
		{
			
		}

		#region INotifyPropertyChanged
		public event PropertyChangedEventHandler PropertyChanged;

		private void NotifyPropertyChanged(String info)
		{
			if (PropertyChanged != null)
			{
				PropertyChanged(this, new PropertyChangedEventArgs(info));
			}
		}
		#endregion
	}
}

ViewModelは、INotifyPropertyChangedインターフェースを実装しただけのシンプルなクラスで、そのクラスをUserControlのResourcesに登録してLayoutRootのDataContextにバインドしています。この形だと、Blendの支援機能がフルに受けることが出来るのでBlendを使う場合は、基本的にこのテンプレートを使うと楽そうです。


このテンプレートを使うとなると、MEF(Managed Extensibility Framework)とどう連携させたものか悩みものです。ViewはMEFで管理しても、あまり意味は無さそうです。(View自体をアドインするようなシステム作るなら意味あるかも)なんたってViewのインスタンスがつくられると自動的にViewModelのインスタンスがひもづいてきます。これを分離しようとするとBlendの支援機能をフルに使って開発することは難しくなるような気がします。


よく考えると、Viewは実行して見てみないことには確認出来ないのでViewModelを差し替え可能にしても単体テスト的には、あまりうまいことにはならないような気がします。ということなので、ViewとViewModelはがっちり結合してもらったままにしよう。

次にViewModelですが、こいつは単体テストしたいです。なのでViewModelが依存するクラスはMEFにインジェクションしてもらうほうが楽そうです。でも、Viewのインスタンス化と同時にViewModelもインスタンス化されてしまうため、ViewModelもMEFで管理することは難しそうです。


そんな時のために、SilverlightのMEFにはSystem.ComponentModel.Composition.ComositionInitializerクラスのSatisfyImportsメソッドがあります。こいつは、引数で渡されたオブジェクトにMEFで管理しているオブジェクトを注入してくれる動きをします。この時に使われるMEFのコンテナは、どこかでstaticに管理されていてSystem.ComponentModel.Composition.Hosting.CompositionHostクラスのInitializeメソッドを使ってコンテナの初期化をすることが出来ます。この機能WPFにもほしかった・・・。ということでSilverlightでは、この機能を使うのが良さげです。


SatisfyImportsをするタイミングですが、UserControlのLoadedイベントかコンストラクタあたりでやっておけば問題ないと思われます。コードとしては以下のような一行になると思います。

CompositionInitializer.SatisfyImports(LayoutRoot.DataContext);


大した手間ではないような気もしますが、Behavior化してBlendからポトペタでできるようにします。

namespace SilverlightApplication1
{
    using System;
    using System.ComponentModel.Composition;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Interactivity;

    public class MEFBehavior : Behavior<UserControl>
    {
        public MEFBehavior()
        {
        }

        protected override void OnAttached()
        {
            base.OnAttached();
            this.AssociatedObject.Loaded += this.LayoutRootLoaded;
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();
            this.AssociatedObject.Loaded -= this.LayoutRootLoaded;
        }

        private void LayoutRootLoaded(object sender, EventArgs e)
        {
            var root = this.AssociatedObject.Content as FrameworkElement;
            if (root == null)
            {
                return;
            }

            CompositionInitializer.SatisfyImports(root.DataContext);
        }
    }
}

こういうのを作っておけば、Blendで、このビヘイビアをUserControlにぽとっとするだけで対応するViewModelにMEFで管理するオブジェクトが注入されます。


小道具として加えておこう。