しっぽを追いかけて

ぐるぐるしながら考えています

Unity と猫の話題が中心   掲載内容は個人の私見であり、所属組織の見解ではありません

Xamarin で Prism と UnityContainer を使ってみる

少し前の投稿 で DI コンテナ *1 である Unity のプレリリース版が Xamarin 対応していることをご紹介しました(ゲーム開発の方じゃないです;)

Prism との相性もよいので今度はこの Unity を Xamarin で使ってみようと思います

まずは Nuget でのインストールから

f:id:matatabi_ux:20141028073947p:plain

Nuget パッケージの管理ダイアログ左上のコンボボックスでリリース前のパッケージを表示するように選択し、「Unity」で検索すれば出てきます

ライセンスも Apache 2.0 なので安心して Xamarin で使えますね

Xamarin.Forms PCL プロジェクトだけでなく、iOSAndroid、WinPhone の各プラットフォームプロジェクトにもインストールが必要です

インストールが終わったら Xamarin.Forms PCL プロジェクトのアプリケーションクラスを修正

/// <summary>
/// 共通 アプリケーションクラス
/// </summary>
public class App
{
    /// <summary>
    /// アプリ内で管理するモジュールのコンテナ
    /// </summary>
    public static readonly UnityContainer Container = new UnityContainer();

    /// <summary>
    /// コンストラクタ
    /// </summary>
    static App()
    {
        // Prism.Mvvm.Xamarin アセンブリを読み込み可能にするおまじない
        var autoWired = ViewModelLocator.AutoWireViewModelProperty.DefaultValue;

        // ViewModel をインスタンス化するデフォルトメソッドを指定します
        ViewModelLocationProvider.SetDefaultViewModelFactory((type) => Container.Resolve(type));

        // View から対応する ViewModel を取得するロジックを設定します
        ViewModelLocationProvider.SetDefaultViewTypeToViewModelTypeResolver(viewType =>
        {
            var viewName = viewType.FullName;
            viewName = viewName.Replace(".Views.", ".ViewModels.");
            var viewAssemblyName = typeof(ViewModelBase).GetTypeInfo().Assembly.FullName;
            var viewModelName = string.Format(CultureInfo.InvariantCulture, "{0}ViewModel, {1}", viewName, viewAssemblyName);
            return Type.GetType(viewModelName);
        });
    }

    /// <summary>
    /// メイン画面を取得します
    /// </summary>
    /// <returns>メイン画面</returns>
    public static Page GetMainPage()
    {
        return new TopPage();
    }
}

UnityContainer を静的なシングルトンとして持たせて、ViewModelLocator のインスタンス化を UnityContainer.Resolve() メソッドで行うように変更します

これだけだと前のコードと大差ないように見えますが、実はグッと拡張性は上がります

UnityContainer には管理するインスタンスがいつ生成され破棄されるのかといったライフサイクルを制御したり、インスタンス化の際にコンストラクタの振るまいやプロパティの値、呼び出すメソッドなどを動的に設定する機能があるためです

アプリの規模や複雑さが増すにつれてその恩恵は大きくなるので、Xamarin のようなクロスプラットフォーム開発においてはきっと大きな威力を発揮するはず!

少しわかりづらいので音声合成による読み上げを行うというプラットフォーム固有の API を利用する機能を UnityContainer を使いながら追加してみます

f:id:matatabi_ux:20141028213125p:plain

ソリューション全体はこんな感じです

まずは各プラットフォーム共通のメソッドをもつインタフェースを Xamarin.Forms PCL プロジェクトに追加します

/// <summary>
/// 文字列読み上げサービスのインタフェース
/// </summary>
public interface ITextSpeechService
{
    /// <summary>
    /// 文字列を読み上げます
    /// </summary>
    /// <param name="text">読み上げる文字列</param>
    void Speak(string text);
}

なんてことないインタフェースですね

さらにプラットフォームごとにこのインタフェースの実装クラスを作ります

namespace PrismXamarin.iOS.Services
{
    using System;
    using System.Collections.Generic;
    using System.Text;
    using MonoTouch.AVFoundation;
    using PrismXamarin.Services;

    /// <summary>
    /// 文字列読み上げサービス
    /// </summary>    
    public class TextSpeechService : ITextSpeechService
    {
        /// <summary>
        /// 文字列を読み上げます
        /// </summary>
        /// <param name="text">読み上げる文字列</param>
        public void Speak(string text)
        {
            var synthesizer = new AVSpeechSynthesizer();

            synthesizer.SpeakUtterance(
                new AVSpeechUtterance(text)
                {
                    Rate = AVSpeechUtterance.MaximumSpeechRate / 4,
                    Voice = AVSpeechSynthesisVoice.FromLanguage("en-US"),
                    Volume = 0.5f,
                    PitchMultiplier = 1.0f
                });
        }
    }
}

これが iOS

namespace PrismXamarin.WinPhone.Services
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using PrismXamarin.Services;
    using Windows.Phone.Speech.Synthesis;

    /// <summary>
    /// 文字列読み上げサービス
    /// </summary>    
    public class TextSpeechService : ITextSpeechService
    {
        /// <summary>
        /// 文字列を読み上げます
        /// </summary>
        /// <param name="text">読み上げる文字列</param>
        public async void Speak(string text)
        {
            var synthesizer = new SpeechSynthesizer();
            await synthesizer.SpeakTextAsync(text);
        }
    }
}

こちらが Windows Phone 版

※WMAppManifest.xml の ID_CAP_SPEECH_RECOGNITION の機能の有効化も必要です

それぞれのサービスクラスを UnityContainer に登録します

/// <summary>
/// アプリケーションクラス
/// </summary>
public class Application
{
    /// <summary>
    /// 起動処理
    /// </summary>
    /// <param name="args">起動パラメータ</param>
    static void Main(string[] args)
    {
        // 音声読み上げサービスの登録
        PrismXamarin.App.Container.RegisterType(
            typeof(ITextSpeechService),
            typeof(TextSpeechService),
            null,
            new ContainerControlledLifetimeManager(),
            new InjectionMember[0]);

        // if you want to use a different Application Delegate class from "AppDelegate"
        // you can specify it here.
        UIApplication.Main(args, null, "AppDelegate");
    }
}

これは iOS のアプリケーションクラスです

ContainerControlledLifetimeManager はコンテナごとに1インスタンスを維持するライフサイクル管理オプションです

InjectionMember の配列はインスタンス化時のコンストラクタの振るまい、プロパティ値、呼び出すメソッドなどを指定できるのですが今回は特に何も指定していません

/// <summary>
/// アプリケーションクラス
/// </summary>
public partial class App : Application
{
    /// <summary>
    /// Provides easy access to the root frame of the Phone Application.
    /// </summary>
    /// <returns>The root frame of the Phone Application.</returns>
    public static PhoneApplicationFrame RootFrame { get; private set; }

    /// <summary>
    /// Constructor for the Application object.
    /// </summary>
    public App()
    {
        // Global handler for uncaught exceptions.
        UnhandledException += Application_UnhandledException;

        // 音声読み上げサービスの登録
        PrismXamarin.App.Container.RegisterType(
            typeof(ITextSpeechService),
            typeof(TextSpeechService),
            null,
            new ContainerControlledLifetimeManager(),
            new InjectionMember[0]);

        // Standard XAML initialization
        InitializeComponent();

        // Phone-specific initialization
        InitializePhoneApplication();

        // Language display initialization
        InitializeLanguage();
        
        ~ 以下略 ~
    }
}

こっちは Windows Phone のアプリケーションクラスです

あとは Xamarin.Forms の PCL プロジェクトで View と ViewModel をちょいちょいっと

<?xml version="1.0" encoding="utf-8" ?>
<local:AwareContentPage xmlns="http://xamarin.com/schemas/2014/forms"
      xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
      xmlns:local="clr-namespace:PrismXamarin.Views;assembly=PrismXamarin"
      xmlns:prismmvvm="clr-namespace:Prism.Mvvm;assembly=Prism.Mvvm.Xamarin"
      prismmvvm:ViewModelLocator.AutoWireViewModel="true"
      x:Class="PrismXamarin.Views.TopPage">
  <Button Text="{Binding Message}" VerticalOptions="Center" HorizontalOptions="Center" Command="{Binding SpeakCommand}"  />
</local:AwareContentPage>

ボタンに文字列を表示してクリック時の処理をコマンドでバインディングしてるだけ

/// <summary>
/// トップ画面の ViewModel
/// </summary>
public class TopPageViewModel : ViewModelBase
{
    /// <summary>
    /// 文字列読み上げサービス
    /// </summary>
    private ITextSpeechService textSpeechService = null;

    #region Message プロパティ

    /// <summary>
    /// Message
    /// </summary>
    private string message = string.Empty;

    /// <summary>
    /// Message の取得と設定
    /// </summary>
    public string Message
    {
        get { return this.message; }
        set { this.SetProperty<string>(ref this.message, value); }
    }

    #endregion //Message プロパティ

    #region SpeakCommand

    /// <summary>
    /// 読み上げコマンド
    /// </summary>
    private ICommand speakCommand;

    /// <summary>
    /// 読み上げコマンド
    /// </summary>
    public ICommand SpeakCommand
    {
        get { return this.speakCommand ?? (this.speakCommand = new DelegateCommand(this.Speak)); }
    }

    /// <summary>
    /// 読み上げする
    /// </summary>
    private void Speak()
    {
        this.textSpeechService.Speak(this.Message);
    }

    #endregion //SpeakCommand

    /// <summary>
    /// コンストラクタ
    /// </summary>
    /// <param name="textSpeechService">文字列読み上げサービス</param>
    [InjectionConstructor]
    public TopPageViewModel(ITextSpeechService textSpeechService)
    {
        this.textSpeechService = textSpeechService;
        this.message = "Hello! Prism MVVM!";
    }
}

ViewModel はコンストラクタに InjectionConstructor の属性を付与して、引数に文字列読み上げサービスを持たせました

なんと上記のコードで UnityContainer が各プラットフォームごとに自動的に ViewModel に渡す読み上げサービスを切り替えてくれるようになるので、すっきりとクロスプラットフォーム対応ができてしまいます

クロスプラットフォームごとの分岐を行うような if 文はいっさいありません!すばらしい!

Xamarin.iOS で Prism × Unity - YouTube

試しに動かしてみました

いつか Unity もクラスプラットフォーム開発での応用方法をもっと考えてみたい

*1:Dependecy Injection Container(依存性注入コンテナ)のこと あるクラスの外部から動的に振る舞いを切り替えることがができるようになります