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 の値を変える場合であっても、描画は定期的にしか再表示されないので、間引き処理とか、画面を描画しないとか、いうテクニックを使わずに済むのでコードが簡単になるかと。



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