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

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

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

■ひとまず全文のコード

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
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 モデルで遅延描画できるかどうか?という答えとしては、意外とシンプルに実装できるっていう実例です。

1
2
3
4
5
6
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 を更新させないとか、

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

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

1
2
3
4
5
6
7
8
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 を更新するときに取ってきています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

コメントは停止中です。