いろいろ備忘録日記

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

BindingOperations.EnableCollectionSynchronizationについて (.NET 4.5, WPF, 非UIスレッドからアクセス)

.NET 4.5 からWPFに以下のメソッドが追加されました。

public static void EnableCollectionSynchronization(
    IEnumerable collection,
    Object lockObject
)

public static void EnableCollectionSynchronization(
    IEnumerable collection,
    Object context,
    CollectionSynchronizationCallback synchronizationCallback
)

名前のまんまなのですが、非UIスレッドからバインディングされているコレクションに
対して安全にアクセスできるようにしてくれるメソッドです。これまでは、WinFormsと
同じ要領でDispacherを利用してアクセスするコードが書いていました(WPFもWinFormsも
SwingもJavaFXもシングルスレッドモデル)が、これが大分楽になります。

やり方は、コレクションに対してアクセスする前に、上記のメソッドを呼んでおくことです。
また、Enableの逆のDisableするためのメソッドもあります。

public static void DisableCollectionSynchronization(
    IEnumerable collection
)

以下、サンプルです。サンプルでは二つのボタンがあって
片方は、OFFで片方はONにして動作するようになってます。

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
    <Button x:Name="btnOff" Content="EnableCollectionSync=OFF" HorizontalAlignment="Left" Margin="9,10,0,0" VerticalAlignment="Top" Height="44" Click="btnOff_Click"/>
    <Button x:Name="btnOn" Content="EnableCollectionSync=On" HorizontalAlignment="Left" Margin="194,10,0,0" VerticalAlignment="Top" Height="44" Click="btnOn_Click"/>
    <ListBox x:Name="lstResults" Margin="10,86,10,10" ItemsSource="{Binding DataSource}"/>

  </Grid>
</Window>
namespace WpfApplication1
{
  using System;
  using System.Collections.Generic;
  using System.Collections.ObjectModel;
  using System.Linq;
  using System.Threading.Tasks;
  using System.Windows;
  using System.Windows.Data;

  public partial class MainWindow : Window
  {
    readonly object _lock = new object();

    public MainWindow()
    {
      InitializeComponent();
      DataContext = new MyDataContext();
    }

    internal async void btnOff_Click(object sender, RoutedEventArgs e)
    {
      //
      // BindingOperations.EnableCollectionSynchronizationを設定せずに
      // 非同期でデータにアクセス.
      //
      var dataSource = (DataContext as MyDataContext).DataSource;
      BindingOperations.DisableCollectionSynchronization(dataSource);

      try
      {
        await CreateDataSourceAsync().ConfigureAwait(false);
      }
      catch (InvalidOperationException invalidOpEx)
      {
        MessageBox.Show(invalidOpEx.Message);
      }
      catch (NotSupportedException notSupportEx)
      {
        MessageBox.Show(notSupportEx.Message);
      }
    }

    internal async void btnOn_Click(object sender, RoutedEventArgs e)
    {
      //
      // BindingOperations.EnableCollectionSynchronizationを設定して
      // 非同期でデータにアクセス.
      //
      var dataSource = (DataContext as MyDataContext).DataSource;
      BindingOperations.EnableCollectionSynchronization(dataSource, _lock);

      await CreateDataSourceAsync().ConfigureAwait(false);
    }

    internal Task CreateDataSourceAsync()
    {
      var dataSource = (DataContext as MyDataContext).DataSource;

      return Task.Run(() => {

        dataSource.Clear();

        for (int i = 0; i < 20; i++)
        {
          dataSource.Add(
            new Data
            {
              Id = i,
              Name = string.Format("Name-{0:00}", i)
            }
          );
        }
      });
    }
  }

  class MyDataContext
  {
    public MyDataContext()
    {
      DataSource = new ObservableCollection<Data>();
    }

    public ObservableCollection<Data> DataSource
    {
      get;
      private set;
    }
  }

  class Data
  {
    public int Id
    {
      get;
      set;
    }

    public string Name
    {
      get;
      set;
    }

    public override string ToString()
    {
      return String.Format("Id={0} Name={1}", Id, Name);
    }
  }
}

実行すると、OFFにしている方のボタンをクリックすると今まで通り
非UIスレッドでコレクションに対して普通にアクセスしているので
エラーとなりますが、ONにしている方はちゃんとデータが表示されます。

以下、参考にした情報です。

  

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

サンプルコードは、以下の場所で公開しています。