読者です 読者をやめる 読者になる 読者になる

いろいろ備忘録日記

主に .NET と Python絡みのメモを公開しています。

DevExpress奮闘記-108 (RealTimeSourceを試してみた, v12.xの新コンポーネント, データの変更を非同期でUI側に伝えるブリッジデータソース, asynchronous bridge source)


先日、リリースされたv12.1の中にRealTimeSourceという
コンポーネントが追加されています。


現状、またCTP版とのこと。なので、このコンポーネントに関するドキュメントが
存在しません。さらに、デモにも搭載されていません。恐らく、次のアップデートで
追加されるものと思います。


以下の機能を持つデータソースとのこと。以下、上記のURL内から引用。

The RealTimeSource component is an asynchronous bridge source for rapidly changing data (e.g., ten thousand changes per second).

データの変更を非同期でUI側に通知してくれるコンポーネントなのかしら?って感じです。
予想として、自身ではデータを持たずに、UIとデータソースの仲介を行う仕事をする
コンポーネントと予想。


上記What's Newのページにある、動画を見てもすごいスピードでUIが更新されています。


てことで、ちょっと試してみました。
以下、サンプルです。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using DevExpress.Skins;
using DevExpress.XtraEditors;

namespace RealtimeSourceSample
{
  public partial class Form1 : XtraForm
  {
    CancellationTokenSource _cts;

    public Form1()
    {
      SkinManager.EnableFormSkins();
      InitializeComponent();
    }

    void Form1_Load(object sender, EventArgs e)
    {
      bdsMain.DataSource = typeof(GridData);      

      rtsMain.DisplayableProperties = "Id;Name;Value";
      rtsMain.DataSource = bdsMain;

      grdMain.DataSource = rtsMain;

      btnStart.Enabled  = true;
      btnCancel.Enabled = false; 
    }

    void btnStart_Click(object sender, EventArgs e)
    {
      bdsMain.Clear();

      var insertWorkers = new List<IWorker>();
      var updateWorkers = new List<IWorker>();
      var deleteWorkers = new List<IWorker>();
    
      for (int i = 0; i < 10; i++)
      {
        insertWorkers.Add(new InsertWorker(tbcInsert.Value));
        updateWorkers.Add(new UpdateWorker(tbcUpdate.Value));
        deleteWorkers.Add(new DeleteWorker(tbcDelete.Value));
      }

      _cts = new CancellationTokenSource();
      insertWorkers.Concat(updateWorkers)
                   .Concat(deleteWorkers)
                   .Randomize()
                   .ToList()
                   .ForEach(x => Task.Factory.StartNew(() => x.Execute(bdsMain, _cts.Token)));

      btnStart.Enabled  = false;
      btnCancel.Enabled = true; 

      tbcInsert.Tag = insertWorkers;
      tbcUpdate.Tag = updateWorkers;
      tbcDelete.Tag = deleteWorkers;
    }

    void btnCancel_Click(object sender, EventArgs e)
    {
      _cts.Cancel();

      btnStart.Enabled  = true;
      btnCancel.Enabled = false; 
    }

    void ChangeIntervalOfWorkers(object sender, EventArgs e)
    {
      TrackBarControl trackBar = sender       as TrackBarControl;
      List<IWorker>   workers  = trackBar.Tag as List<IWorker>;

      if (workers == null)
      {
        return;
      }
      
      workers.ForEach(x => x.ChangeInterval(trackBar.Value));
    }
  }

  #region Extensions
  static class WorkerExtensions
  {
    public static IEnumerable<T> Randomize<T>(this IEnumerable<T> source)
    {
      var rnd = new Random();
      return source.OrderBy(x => rnd.Next());
    }
  }
  #endregion

  #region Workers
  interface IWorker
  {
    void Execute(BindingSource source, CancellationToken token);
    void ChangeInterval(int milliseconds);
  }

  class InsertWorker : IWorker
  {
    volatile int _interval;
    Random _rnd;

    public InsertWorker(int interval)
    {
      _interval = interval;
      _rnd = new Random(Guid.NewGuid().GetHashCode());
    }

    public void Execute(BindingSource source, CancellationToken token)
    {
      for (long l = 0; true; l++)
      {
        if (token.IsCancellationRequested)
        {
          break;
        }

        var newData = new GridData { Id = l, Name = string.Format("Name-{0}", l), Value = string.Format("Value-{0}", l) };
        lock (source.SyncRoot)
        {
          source.Add(newData);
        }

        Thread.Sleep(TimeSpan.FromMilliseconds(_interval + _rnd.Next(100)));
      }
    }

    public void ChangeInterval(int milliseconds)
    {
      _interval = milliseconds;
    }
  }

  class UpdateWorker : IWorker
  {
    volatile int _interval;
    Random _rnd;

    public UpdateWorker(int interval)
    {
      _interval = interval;
      _rnd = new Random(Guid.NewGuid().GetHashCode());
    }

    public void Execute(BindingSource source, CancellationToken token)
    {
      var rnd = new Random();
      while (true)
      {
        if (token.IsCancellationRequested)
        {
          break;
        }

        lock (source.SyncRoot)
        {
          if (source.Count != 0)
          {
            var index = rnd.Next(source.Count - 1);
            var data = source[index] as GridData;

            data.Name = string.Format("UPDATED-{0}", index);
            data.Value = string.Format("UPDATED-{0}", index);
          }
        }

        Thread.Sleep(TimeSpan.FromMilliseconds(_interval + _rnd.Next(100)));
      }
    }

    public void ChangeInterval(int milliseconds)
    {
      _interval = milliseconds;
    }
  }

  class DeleteWorker : IWorker
  {
    volatile int _interval;
    Random _rnd;

    public DeleteWorker(int interval)
    {
      _interval = interval;
      _rnd = new Random(Guid.NewGuid().GetHashCode());
    }

    public void Execute(BindingSource source, CancellationToken token)
    {
      var rnd = new Random();

      while (true)
      {
        if (token.IsCancellationRequested)
        {
          break;
        }

        lock (source.SyncRoot)
        {
          if (source.Count != 0)
          {
            source.RemoveAt(rnd.Next(source.Count - 1));
          }          
        }

        Thread.Sleep(TimeSpan.FromMilliseconds(_interval + _rnd.Next(100)));
      }
    }

    public void ChangeInterval(int milliseconds)
    {
      _interval = milliseconds;
    }
  }
  #endregion

  #region Data
  public class GridData : INotifyPropertyChanged
  {
    public event PropertyChangedEventHandler PropertyChanged;

    // Fields...
    private string _value;
    private string _name;
    private long _id;

    public long Id
    {
      get { return _id; }
      set
      {
        if (_id == value)
          return;
        _id = value;
        OnPropertyChanged(new PropertyChangedEventArgs("Id"));
      }
    }

    public string Name
    {
      get { return _name; }
      set
      {
        if (_name == value)
          return;
        _name = value;
        OnPropertyChanged(new PropertyChangedEventArgs("Name"));
      }
    }

    public string Value
    {
      get { return _value; }
      set
      {
        if (_value == value)
          return;
        _value = value;
        OnPropertyChanged(new PropertyChangedEventArgs("Value"));
      }
    }

    #region OnPropertyChanged
    /// <summary>
    /// Triggers the PropertyChanged event.
    /// </summary>
    public virtual void OnPropertyChanged(PropertyChangedEventArgs ea)
    {
      if (PropertyChanged != null)
        PropertyChanged(this, ea);
    }
    #endregion
  }
  #endregion

}


上記サンプルは、グリッドのデータソースにRealTimeSource、RealTimeSourceのデータソースに
BindingSourceを接続しています。コードでいうと、Form_Loadの以下の部分です。

    void Form1_Load(object sender, EventArgs e)
    {
      // BindingSourceの設定
      bdsMain.DataSource = typeof(GridData);      

      // RealTimeSourceの設定
      rtsMain.DisplayableProperties = "Id;Name;Value";
      rtsMain.DataSource = bdsMain;

      // GridControlの設定.
      grdMain.DataSource = rtsMain;

      btnStart.Enabled  = true;
      btnCancel.Enabled = false; 
    }

RealTimeSourceが仲介するような感じで各データソースを設定します。


起動すると、以下のUIが表示されます。


Startボタンをクリックすると、内部でInsert, Update, Deleteを行うワーカーオブジェクトを
非同期で実行して、データを更新しまくります。更新するスピードは、UI下部のトラックバーで
調整出来ます。


実際にアプリを起動して動かすとわかるのですが、データを常に更新してUI側に通知されている
はずなのに、グリッド上のデータはとてもスムーズに描画されます。


また、RealTimeSourceが仲介してくれているので、BindingSource側にデータを設定する際に
いちいち、Invokeする必要がなくなったのもとても楽。BindingSourceが依存しているのは
RealTimeSourceであって、GridControlではないからです。これをRealTimeSourceを仲介させずに
直接GridControl.DataSource = bdsMainとして動作させると、Invokeしてないのですぐに例外が
発生します。BindingSourceに対して、データの変更操作が行われているスレッドがUIスレッド
ではないからです。


(補足):
まだ、ドキュメントも存在しないコンポーネントなので
念のため、DevExpress側に「この使い方で合ってるの?」って聞きました。
答えは、「合ってる」だそうです。


今回のサンプルを、以下の場所にアップしました。
動きを確認される方は、よろしければご利用くださいませ。

================================
過去の記事については、以下のページからご参照下さい。