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 の値を変える場合であっても、描画は定期的にしか再表示されないので、間引き処理とか、画面を描画しないとか、いうテクニックを使わずに済むのでコードが簡単になるかと。
ちなみに、重たい処理の中で進捗を表示する場合は、Task.Run と IProgress を使うのがベター
Nine Works: async awaitとIProgressを使ってみる
http://nine-works.blog.ocn.ne.jp/blog/2012/10/async_awaitipro.html