しっぽを追いかけて

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

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

Prism を MVPVM 化する(1)ViewModel と Model を Portable Class Library プロジェクトに配置する

ユニバーサル Windows アプリが発表され、Xamarin も普及し始めていることから今後は WinRT アプリに限らず C#iOSAndroid や IoT などマルチデバイスクロスプラットフォームの対応ができるようになりそうです

アプリを中心としたビジネス的な戦略として最初は特定のデバイスで提供し、その後段階的に他のデバイス、プラットフォームに展開していくといった取り組みが増えるかもしれません

そうなるとできる限り多くの部分をクロスプラットフォームで利用できる Portable Class Library(以降 PCL)にしたいところ・・・ただし、現在の Windows ストアアプリ用の Prism フレームワークでは ViewModel と Model がストアアプリ依存のコードになっているのでそのままでは利用できないです

・・・ユニバーサル Windows アプリの流れを見る限り将来的には Prism の中の人が何らかの形でクロスプラットフォーム対応をしてくれそうですが;

Xamarin 用に発表されている MvvmCross というフレームワークを利用するという方法もありますが、暗黙的制約による実装が多く少し拡張性に不安があるのでやはり Prism を利用したい!

そこで Prism を MVVM から MVPVM に拡張しようと思います!

f:id:matatabi_ux:20140413073047p:plain

なぜ MVPVM にするのかというと、現在の Prism は ViewModel に画面遷移処理が組み込まれていて PCL 化ができないので、Presenter を追加してそこに画面遷移処理を移す必要があると思うからです

スポンサードリンク

とりあえず ViewModel と Model を PCL プロジェクト化するところから

Prism のサンプルアプリケーション AdventureWorks Shopper を見習って ~.UILogic という名前の PCL プロジェクトを追加します

f:id:matatabi_ux:20140406175406p:plain

Microsoft.BCL.Build.Tasks.dll(ビルド コンポーネント) は ObservableCollection を利用するために参照しています

[2014/4/9 追記] ビルドターゲットを .NET Framework 4.5 以上にすれば不要でした

Model と ViewModel は Command や Messenger などのプラットフォーム依存機能を排除し、純粋なデータの入れものにしています

これによって Templates 配下の .ttinclude をテンプレートとする T4 によるコードの自動生成が簡単にできるようになっています

ViewModels 配下の RestorableDataAttribute は中断とシャットダウン(Terminated 状態)から復帰する際に復元する必要のある ViewModel のプロパティに付加する属性クラスです

Prism だと同じ機能をもつ RestorableAttribute という属性がすでにあるのですが、PCL では参照できなかったので新たに追加しました

/// <summary>
/// Terminate から復帰時に復元されるプロパティに付加する属性
/// </summary>
[AttributeUsage(System.AttributeTargets.Property, AllowMultiple = false)]
public class RestorableDataAttribute : Attribute
{
}

目印でしかないので中身は空です!

また、ViewModel の基底クラスとして下記のような ViewModelBase を作成しています

/// <summary>
/// ViewModel 基底クラス
/// </summary>
public class ViewModelBase : INotifyPropertyChanged
{
    #region INotifyPropertyChanged

    /// <summary>
    /// プロパティ変更イベント
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;

    /// <summary>
    /// プロパティが変化する場合のみ値を更新しプロパティ変更イベントを発生させる
    /// </summary>
    /// <typeparam name="T">プロパティの型</typeparam>
    /// <param name="storage">プロパティを持つインスタンス</param>
    /// <param name="value">変更後のプロパティ値</param>
    /// <param name="propertyName">プロパティの名前 アクセサメソッド内で呼び出された場合は自動で設定される</param>
    /// <returns>値は変更された場合は<c>true</c>、それ以外は<c>false</c></returns>
    protected virtual bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
    {
        if (object.Equals(storage, value))
        {
            return false;
        }

        storage = value;
        this.OnPropertyChanged(propertyName);

        return true;
    }

    /// <summary>
    /// プロパティの変更を通知する
    /// </summary>
    /// <param name="propertyName">変更されるプロパティ名</param>
    protected void OnPropertyChanged(string propertyName)
    {
        var eventHandler = this.PropertyChanged;
        if (eventHandler != null)
        {
            eventHandler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    #endregion //INotifyPropertyChanged

    #region RestorableData

    /// <summary>
    /// プロパティの値を状態データに複製する
    /// </summary>
    /// <param name="state">状態データ</param>
    public virtual void FillState(IDictionary<string, object> state)
    {
        if (state == null)
        {
            return;
        }

        var properties = this.GetType().GetRuntimeProperties()
            .Where(c => c.GetCustomAttributes(typeof(RestorableDataAttribute), false).FirstOrDefault() != null);

        foreach (PropertyInfo info in properties)
        {
            state[info.Name] = info.GetValue(this, null);
        }
    }

    /// <summary>
    /// プロパティの値を復元する
    /// </summary>
    /// <param name="state">状態データ</param>
    public virtual void Restore(Dictionary<string, object> state)
    {
        if (state == null)
        {
            return;
        }

        var properties = this.GetType().GetRuntimeProperties()
            .Where(c => c.GetCustomAttributes(typeof(RestorableDataAttribute), false).FirstOrDefault() != null);

        foreach (PropertyInfo info in properties)
        {
            if (state.ContainsKey(info.Name))
            {
                info.SetValue(this, state[info.Name], null);
            }
        }
    }

    #endregion //RestorableData
}

Prism の ViewModel クラス(ViewModel の基底クラス)から画面遷移処理の部分を削除したような構造にすることで PCL で利用できるようになっています

INotifyPropertyChanged インタフェース実装と、データのコピーと復元処理部分だけを残した感じですね

これだけでは MVPVM 化は完了しないので(2)に続きます・・・!