放浪軍師のXamarin.Formsアプリ開発局

Xamarin.Forms+Prism+ReactivePropertyで素人がAndroidやUWPのアプリを右往左往しながら開発している様を発信していきます。性質上間違いも多いのでご注意ください。

乱ちゃんProjectその5(MVVMのModel部分の記述方法修正)

放浪軍師のXamarin.Formsによるアプリ開発
久々に本編を進めます。進言ランダム作成の続きです。
まず、初めて訪問された方は以下をお読みください。

www.gunshi.info


MVVMのModel部分の記述方法が間違っているっぽい

いつものようにかずきさんのブログを見ながら勉強していたのですが、どうもまだMVVMを正しく理解できていなかったらしく、Modelの記述方法に大きな誤りがあるという事に気が付きましたので修正したいと思います。
blog.okazuki.jp

現状を紹介(一部抜粋)

まずはViewModelがこちら

namespace RanCyan.ViewModels
{
    public class RanShikaPageViewModel : BindableBase
    {
        //進言ランダム
        public ReactiveProperty<string> SingenLabel { get; } = new ReactiveProperty<string>();
        public ReactiveCommand SingenRandomCommand { get;} = new ReactiveCommand();

        public RanShikaPageViewModel()
        {
            //・進言コマンド
            SingenRandomCommand.Subscribe(_ => ShingenRundom());
        }

        /// <summary>
        /// 進言コマンドを実行
        /// </summary>
        private async void ShingenRundom()
        {
            //数字を範囲指定
            var r1 = new RandomData(1, 3);

            int loopNo = 10; //ループ回数(回)
            int loopTime = 1000; //ループ時間(msec)

            //基準となるウェイト時間(msec)
            float oneWaitTime = loopTime / (((loopNo - 1) * loopNo) / 2);

            for (int i = 1; i <= loopNo; i++)
            {
                r1.SetRandomData();
                SingenLabel.Value = r1.RandomValue;
                if (i < loopNo)
                {
                    //少しずつウェイト時間を長くする
                    await System.Threading.Tasks.Task.Delay((int)(oneWaitTime * i));
                }
            }
        }

    }
}

以前とは一部変更されていて、ReactiveCommandを使用した記述にしています。
また、前回の非同期処理学習のお陰でawaitの部分がちゃんと理解できますね。

次にModelです。

namespace RanCyan.Models
{
    class RandomData
    {
        /// <summary>
        /// プロパティ ランダム値
        /// </summary>
        public string RandomValue { get; private set; }

        public List<string> RandomList { get; set; } = new List<string>();

        /// <summary>
        /// コンストラクタ ランダム取得範囲を指定
        /// </summary>
        /// <param name="fast">〇以上</param>
        /// <param name="rast">〇以下</param>
        public RandomData(int fast, int rast)
        {
            for (int i = fast; i <= rast; i++)
            {
                //リストに追加
                RandomList.Add(i.ToString());
            }
        }

        /// <summary>
        /// コンストラクタ ランダム取得範囲を指定 配列要素 
        /// </summary>
        /// <param name="s">配列で要素を指定</param>
        public RandomData(string[] s)
        {
            foreach(var i in s)
            {
                RandomList.Add(i);
            }
        }

        /// <summary>
        /// ランダム値を生成
        /// </summary>
        public void SetRandomData()
        {
            //シード値を取得(乱数固定化の阻止)
            int seed = Environment.TickCount;
            // Randomクラスのインスタンス生成
            Random rnd = new System.Random(seed);
            // listの数からダンダムで値を取得
            int i = rnd.Next(0, RandomList.Count);
            // 代入
            RandomValue = RandomList[i];
        }
    }

数値または文字列型の配列を渡した後、SetRandomDataでランダムで値を取得するような感じです。
まぁ通常のC#ならこんな感じの記述でいいんじゃないかと思うんですが、どうなんでしょ?なんせC#での開発も経験ほぼゼロだからな!
でもMVVMの場合、前述のかずきさんのブログによれば…

1.ModelはINotifyPropertyChangedを実装した値の入れ物のクラス
2.ViewModelはそれをラップしてVからの入力値を保持するクラス
3.ViewModelでは入力値の検証を行う
4.ViewModelではModelのプロパティが変わったらそれを直ちに反映する
5.ViewModelではVからの入力値にエラーがない状態でCommitメソッドを呼ばれたらModelに値を書き戻す

との事なので、完全に間違ってますね。現段階では5.の意味がさっぱりわからないけどいつものようにとりあえずやってみようと思います。

やってみた

まずはModel側の修正です。

namespace RanCyan.Models
{
    class RandomData : BindableBase
    {
        //以下を変更した。
        //public string RandomValue { get; private set; }
        private string randomValue;
        public string RandomValue
        {
            get { return this.randomValue; }
            set { this.SetProperty(ref this.randomValue, value); }
        }

        ~以下同じ~
}

まぁ殆ど同じですが、ViewModelで監視すべきrandomValueの部分はSetPropertyを用いた形に変更しています。


次にViewModel。こんな感じにしてみましたがどうでしょ?

namespace RanCyan.ViewModels
{
    public class RanShikaPageViewModel : BindableBase, IDisposable
    {
        private CompositeDisposable Disposable { get; } = new CompositeDisposable();

        //進言ランダム
        public ReactiveProperty<string> SingenLabel { get; } 
        public ReactiveCommand SingenRandomCommand { get;} = new ReactiveCommand();
        private RandomData singenRandomData = new RandomData(1,3);

        public RanShikaPageViewModel()
        {
            //・進言コマンド
            this.SingenLabel = singenRandomData
                .ObserveProperty(x => x.RandomValue) 
                .ToReactiveProperty()
                .AddTo(this.Disposable); 
            SingenRandomCommand.Subscribe(_ => ShingenRundom());
        }

        public void Dispose()
        {
            this.Disposable.Dispose();
        }

        /// <summary>
        /// 進言コマンドを実行
        /// </summary>
        private async void ShingenRundom()
        {
            int loopNo = 10; //ループ回数(回)
            int loopTime = 1000; //ループ時間(msec)
            //基準となるウェイト時間(msec)
            float oneWaitTime = loopTime / (((loopNo - 1) * loopNo) / 2);
            for (int i = 1; i <= loopNo; i++)
            {
                singenRandomData.SetRandomData();
                if (i < loopNo)
                {
                    //少しずつウェイト時間を長くする
                    await System.Threading.Tasks.Task.Delay((int)(oneWaitTime * i));
                }
            }
        }
    }
}

なんか不思議な感じですがとりあえずはちゃんと動いてくれました。ちなみに上記コードが完成するのに数時間かかりました…
f:id:roamschemer:20180818014419g:plain

要所をみてみましょ
public ReactiveProperty<string> SingenLabel { get; } 
public ReactiveCommand SingenRandomCommand { get;} = new ReactiveCommand();
private RandomData singenRandomData = new RandomData(1,3);

まずは宣言の部分。SingenLabel のnewは行わないでおきます。で、ModelにあるRandomDataクラスのsingenRandomDataを宣言。範囲は1~3です。

this.SingenLabel = singenRandomData
    .ObserveProperty(x => x.RandomValue) 
    .ToReactiveProperty()
    .AddTo(this.Disposable); 

SingenLabelはsingenRandomData.RandomValueの変化を即座に反映しますよ(ReactiveProperty化)。ついでにDisposeでの後処理もやるよ。という記述。

public void Dispose()
{
    this.Disposable.Dispose();
}

メモリリークを防ぐ処理でViewModelとModelと繋ぐなら必須みたいです。IDisposableを継承しておく必要がある。

singenRandomData.SetRandomData();

これが実行されたらsingenRandomData.RandomValueが変化するので、その変化が拾われ即座に反映する。

まとめ

本当はエラーを拾った場合の処理とか単方向や双方向の処理もあるみたいなんですが、まぁとりあえずならこれでいいんじゃないかと思いますがどうなんだろうか。あんまり自信はないがちゃんと動きます。
SingenLabel.Value = 〇〇;
みたいに書かないで良いのは不思議な感じがしますね。また、表示をループさせるところのfor{}文もまるっとModel側に移すことでViewModelを簡潔にかけるかもしれませんが、それはまた今度実験してみようかと思います。眠いし。