2016年8月12日金曜日

[C#][UWP]ViewModelを別スレッドから更新する方法(2)

前回の記事はネット上調べて見つけた方法を利用したやり方だったのだが、
よくよく考えてみると、
 モデルクラス自身が変化を通知する時、
  ・UIスレッド上ならOK
  ・別スレッド上だとNG

という事をどう解決するかでTaskのAsync/Await問題にも波及するわずわらしい問題になっている。

どうやって切り分けたら良いかを考えたら、オブザーバーパターンが有効だという事に気が付いた。

モデルクラスは別スレッドで動いていて、モデルクラスの変化を一定周期で監視するクラスをUIスレッド上で動かしておく。
こうすることでモデルクラスは手を加えずにUIスレッド上で通知する事が可能になる。

色々モデリングして動作チェックして一番シンプルなのがこの方法だったので
同じく困っている人がいたらこの方法を採用してみてください。

---

    public interface IRaisePropertyChanged
    {
        void RaiseNotify(PropertyChangedEventArgs args);
    }

    public class PropertyWatcher
    {
        public PropertyChangedEventArgs args { get; }
        Func getValue { get; }
        object self { get; }
        object value { get; set; }

        public PropertyWatcher(object self, PropertyInfo pinfo)
        {
            this.self = self;
            args = new PropertyChangedEventArgs(pinfo.Name);
            getValue = pinfo.GetValue;
        }

        public bool Check()
        {
            var newValue = getValue(self);
            if (newValue != value)
            {
                value = newValue;
                return true;
            }
            return false;
        }
    }

    public class ModelWatcher
    {
        public IRaisePropertyChanged self { get; }
        public PropertyWatcher[] props { get; }

        public ModelWatcher(IRaisePropertyChanged instance)
        {
            self = instance;
            props = instance
                .GetType()
                .GetProperties(BindingFlags.Instance | BindingFlags.Public)
                .Select(_ => new PropertyWatcher(instance, _))
                .ToArray();
        }

        public void Polling()
        {
            foreach (var prop in props)
            {
                if (prop.Check())
                {
                    self.RaiseNotify(prop.args);
                }
            }
        }
    }

    public static class ViewModelUpdater
    {
        static CoreDispatcher Dispatcher { get; set; }
        static int uiThreadId { get; set; }
        static DispatcherTimer timer { get; } = new DispatcherTimer();
        static List models { get; } = new List();

        public static void Add(ModelWatcher model)
        {
            lock (models)
            {
                models.Add(model);
            }
        }

        public static void Remove(ModelWatcher model)
        {
            lock (models)
            {
                if (models.Contains(model))
                {
                    models.Remove(model);
                }
            }
        }

        public static void Initialize()
        {
            Dispatcher = Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher;
            uiThreadId = Environment.CurrentManagedThreadId;
            timer.Interval = TimeSpan.FromMilliseconds(20);
            timer.Tick += (s, e) =>
            {
                lock (models)
                {
                    foreach(var model in models)
                    {
                        model.Polling();
                    }
                }
            };
            timer.Start();
        }

        public static async void UIInvoke(Task action)
        {
            if (uiThreadId == Environment.CurrentManagedThreadId)
            {
                await action;
            }
            else
            {
                await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, new DispatchedHandler(() => { action.Wait(); }));
            }
        }

        public static async void UIInvoke(Action action)
        {
            if (uiThreadId == Environment.CurrentManagedThreadId)
            {
                action();
            }
            else
            {
                await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, new DispatchedHandler(action));
            }
        }
    }



  //テスト用モデルクラス

    public class testmodel : INotifyPropertyChanged, IRaisePropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        public void RaiseNotify(PropertyChangedEventArgs args)
        {
            Debug.WriteLine($"Update:{Environment.CurrentManagedThreadId}");
            PropertyChanged?.Invoke(this, args);
        }

        public int value1 { get; set; }
    }

---

使い方

ViewModelUpdater.Initialize();

初期化して

ViewModelUpdater.Add(new ModelWatcher(testmodel));

対象モデルクラスをModelWatcherクラスに渡してViewModelUpdaterに追加する。

監視対象から外す場合はRemoveを使う。








0 件のコメント:

Androider