View を遅延更新する MVVM を作ってみる

MVVM のメリットは、Model のプロパティを変更することによって、View の画面の更新が自動的にされる、というものがあります。まぁ、他にもメリットがあるんだけど、View のもろもろの構造は関係なく、好きなように Model を構築することができる(場合によってはデータベース周りに特化させても ok)ってのが良いのですが、その反面、Model のプロパティへの更新が、そのまま View に伝達されてしまうために、その「即時性」が、かえって View の描画の重さになってしまうってのが、デメリットといえばデメリットです。描画速度が遅くならない程度に、頻繁に Model を更新しなければよいだけの話なんですが…ふと、C++/CX のゲームアプリの説明では、描画のためのデータの更新は、描画をするタイミングまでためておいて、フレームレートの間で描画できる処理をする、というのがあったんので、では、MVVM の View の更新をフレームレート単位で更新するようにすれば、Model の頻繁な更新に耐えられるのでは?と思ったのが、次のアイデアです。

フレームレートっていうよりも、単純にタイマーで定期的に View の更新をしているだけなのです。ちょっとだけ、Model の INotifyPropertyChanged に手を加えます。

■ひとまず全文のコード

遅延用の Model と View のコードはこんな感じ。
スピードを重視するくせに、Queue のコードが遅すぎるだろう、という意見は却下で(苦笑)。

public sealed partial class MainPage : Page
{
    public MainPage()
    {
        this.InitializeComponent();

        // 動的バインド
        this.textX.SetBinding( TextBlock.TextProperty, new Binding() { Path = new PropertyPath("X") });
        this.textMsg.SetBinding( TextBlock.TextProperty, new Binding() { Path = new PropertyPath("Msg") });

        // 遅延 View 更新タイマーを作成
        _timer = new DispatcherTimer();
        _timer.Interval = TimeSpan.FromSeconds(5.0);    // 5秒ごとに更新
        _timer.Tick += _timer_Tick;

        _model = new Model();
		// Queue クラスを作成
        _vq = new ViewQueue();
		// 遅延バインド
        _model.PropertyChangedQueue += _vq.PropertyChanged;
        _timer.Start();
        this.DataContext = _model;
    }

    DispatcherTimer _timer;
    Model _model;
    ViewQueue _vq;


    /// <summary>
    /// このページがフレームに表示されるときに呼び出されます。
    /// </summary>
    /// <param name="e">このページにどのように到達したかを説明するイベント データ。Parameter 
    /// プロパティは、通常、ページを構成するために使用します。</param>
    protected override void OnNavigatedTo(NavigationEventArgs e)
    {
    }

	/// <summary>
	/// View 更新用のタイマー
	/// </summary>
	/// <param name="sender"></param>
	/// <param name="e"></param>
    void _timer_Tick(object sender, object e)
    {
        while (true)
        {
            ViewQueue.PropChange prop = _vq.GetProperty();
            if (prop == null)
                break;

			// model の更新を通知
            var model = (INotifyPropertyChangedQueue)prop.Sender;
            model.OnPropertyChanged(prop.Name);
        }
    }

	/// <summary>
	/// ボタンをクリックして Model を更新
	/// </summary>
	/// <param name="sender"></param>
	/// <param name="e"></param>
    private void SetClick(object sender, RoutedEventArgs e)
    {
		// この時点では、View は更新されない。
        _model.X++;
        _model.Msg = string.Format("{0} カウント {1}", DateTime.Now, _model.X);
    }
}

/// <summary>
/// View の遅延更新用のインターフェース
/// </summary>
public interface INotifyPropertyChangedQueue : INotifyPropertyChanged
{
    event PropertyChangedEventHandler PropertyChangedQueue;
    void OnPropertyChanged(string name);
}

/// <summary>
/// モデル
/// </summary>
public class Model : INotifyPropertyChangedQueue
{
    private int _x;
    public int X
    {
        get { return _x; }
        set
        {
            _x = value;
			// Change イベントは、Queue のほうに通知
            OnPropertyChangedQueue("X");
        }
    }
    private string _msg;
    public string Msg
    {
        get { return _msg; }
        set
        {
            _msg = value;
            OnPropertyChangedQueue("Msg");
        }
    }

    public event PropertyChangedEventHandler PropertyChangedQueue;
    /// <summary>
    /// 遅延させるために Queue に配置
    /// </summary>
    /// <param name="name"></param>
    void OnPropertyChangedQueue(string name)
    {
        if (PropertyChangedQueue != null)
        {
            PropertyChangedQueue(this, new PropertyChangedEventArgs(name));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    /// <summary>
    /// Queue から View にイベントを発生させる
    /// </summary>
    /// <param name="name"></param>
    public void OnPropertyChanged(string name)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged( this, new PropertyChangedEventArgs(name));
        }
    }
}
public class ViewQueue
{
    public class PropChange
    {
        public object Sender { get; set; }
        public string Name { get; set; }
    }

    List<PropChange> _lst = new List<PropChange>();
	/// <summary>
	/// Queue に溜め込んでおく
	/// </summary>
	/// <param name="sender"></param>
	/// <param name="args"></param>
    public void PropertyChanged(object sender, PropertyChangedEventArgs args)
    {
        var prop = _lst.Find(p => p.Name == args.PropertyName);
        if (prop == null)
        {
            _lst.Add(new PropChange() { Sender = sender, Name = args.PropertyName });
        }
    }
	/// <summary>
	/// View から更新依頼があったときに通知する
	/// </summary>
	/// <returns></returns>
    public PropChange GetProperty()
    {
        if (_lst.Count > 0)
        {
            PropChange prop = _lst[0];
            _lst.RemoveAt(0);
            return prop;
        }
        else
        {
            return null;
        }
    }
}

ボタンを押すと、Model を更新するのですが、View の更新タイミングは5秒間毎になっています。なので、ボタンをクリックしたときに「反応がにぶい」というかって変な感じになるのですが、Model の変更を即時更新させるか、遅延更新させるかで、プロパティ単位で即時反映(OnPropertyChanged)と遅延反映(OnPropertyChangedQueue)を選択すればよいかと。

■進捗描画に引きずられない MVVM が作れる?

作ってはみたけれど、いまいち使いどころはどうなのか?って疑問はあるのですが、まあ、実験として MVVM モデルで遅延描画できるかどうか?という答えとしては、意外とシンプルに実装できるっていう実例です。

for ( int i=0; i<MAX; i++ ) {
	// 何かの処理
	Run();
	// 進捗を通知
	Model.ProgressRatio = i*100/MAX;
}

な感じで進捗率を画面に表示するための Model を作ったとしましょう。あるいは、Run の中で Model のプロパティを更新しているとか。
この場合、Model のプロパティへの更新が、即座に View に OnPropertyChanged で通知されるために、View が更新されるまで Model が待たされます。これは View の描画処理のスピードもあるのですが、せっかく Run の処理を非常に高速にしているのに、MVVM を使っているがために、View の描画処理に引きずられてしまうのも、おかしな話ですね~、という具合です。DataGridView の DataSource の場合でも、列を Add するたびに View が更新されるものだから、妙に遅くなってしまうという現在の View の実装が問題というものあるのですが。

方法としては、描画している間は、View を更新させないとか、

// View を更新させない
View.Update = false;
for ( int i=0; i<MAX; i++ ) {
	// 何かの処理
	Run();
	// 進捗を通知
	Model.ProgressRatio = i*100/MAX;
}
// 処理がおわったら View を更新させる
View.Update = true;

Model のプロパティを更新するタイミングを間引きするとか、

for ( int i=0; i<MAX; i++ ) {
	// 何かの処理
	Run();
	// 進捗を通知を間引きする
	if ( MAX % 100 == 0 ) {
		Model.ProgressRatio = i*100/MAX;
	}
}

もろもろのテクニックがあるのですが、Model のどのプロパティが、どのように View に影響を与えるのか?逆に View の速度がどうやって、Model のコーディングに影響を与えてしまうのか?という問題が出てきてしまうこと自体が「問題」ではないか?と思ったわけです。Model と View を分離するのだから、Model を更新するタイミングと View を更新するタイミングはずれても構わないだろう、という思惑です。

View の遅延をさせるのですが、Model から View への通知を Queue に貯めますが、実際の値は View を更新するときに取ってきています。

List<PropChange> _lst = new List<PropChange>();
/// <summary>
/// Queue に溜め込んでおく
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
public void PropertyChanged(object sender, PropertyChangedEventArgs args)
{
    var prop = _lst.Find(p => p.Name == args.PropertyName);
    if (prop == null)
    {
        _lst.Add(new PropChange() { Sender = sender, Name = args.PropertyName });
    }
}

なので、キューには「通知があった」ことだけを貯めればよいので、List を使う必要もないのですが、まあ、試作品というとで。
Model のプロパティを変更するためのボタンを連打しても、遅延更新の場合は最後のひとつだけが Quene にたまるので描画が1回しか行われません。CPU/GPU に負担をかけにくい実装、ってことなんですが、本当に負担を掛けないかどうかは、DataGridView とかで実装してみないとわかりません。

使いどころとしては、DataGridView に 2000行ぐらいのデータを表示しようとして、妙に遅くて困るとか、DataGridView を頻繁に更新しているためか画面が固まった感じがする、ってのが解消されるといいかなと。
DataSource プロパティに設定して、コレクションの内容を変えるたびに画面が切り替わる(描画的には、更新された描画部分だけ切り替わっているのでしょうが)ってのが解消されるかなと。

また、コーディング上としては、Model に View が連結されているか否かに関係なく、Model のプロパティを自由に設定できます。先の進捗率の表示のように、頻繁に Model の値を変える場合であっても、描画は定期的にしか再表示されないので、間引き処理とか、画面を描画しないとか、いうテクニックを使わずに済むのでコードが簡単になるかと。

カテゴリー: C# パーマリンク

View を遅延更新する MVVM を作ってみる への1件のコメント

  1. masuda のコメント:

    ちなみに、重たい処理の中で進捗を表示する場合は、Task.Run と IProgress を使うのがベター
    Nine Works: async awaitとIProgressを使ってみる
    http://nine-works.blog.ocn.ne.jp/blog/2012/10/async_awaitipro.html

コメントは停止中です。