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

SourceChord

C#とXAML好きなプログラマの備忘録。最近はWPF系の話題が中心です。

バインドしてるObservableCollectionを非UIスレッドから操作する

WPF4.5では、複数スレッドからのコレクション操作のサポートのために以下のメソッドが追加されました。
BindingOperations.EnableCollectionSynchronization メソッド (System.Windows.Data)

これを使って、非UIスレッドからのObservableCollecion更新処理を書いてみたいと思います。

バインディングしているプロパティの更新処理について

WPFではUI上のコントロールのプロパティにバインドしている値を、非UIスレッドから更新通知をしても、その結果を正しく反映してくれます。
(TextBlockのテキストとか、コレクションじゃない普通のプロパティの場合。)

しかし、コレクション操作の場合には注意が必要で、ListBoxとかのItemsSourceにObservableCollecionのプロパティをバインドし、このコレクションに非UIスレッドから要素の追加・削除などをすると例外を吐いてしまいます。

PropertyChangedイベントを非UIスレッドから実行

まずは、コレクションではない普通のプロパティを、非UIスレッドから更新した場合の動作を見てみます。
string型のプロパティをTextBlockにバインドして、バックグラウンドスレッドから非同期で値の更新(PropertyChangedイベントの発行)をしてます。
これは何の問題もなく実行できます。
f:id:minami_SC:20140202145938p:plain

MainWindow.xaml
<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"
        Width="525"
        Height="350">
    <Grid>
        <TextBlock Margin="10,10,0,0"
                   HorizontalAlignment="Left"
                   VerticalAlignment="Top"
                   Text="{Binding Message}"
                   TextWrapping="Wrap" />
        <Button Width="75"
                Margin="10,30,0,0"
                HorizontalAlignment="Left"
                VerticalAlignment="Top"
                Click="Button_Click"
                Content="Button" />
    </Grid>
</Window>
MainWindow.xaml.cs
    public partial class MainWindow : Window
    {
        private MainWindowViewModel vm;
        public MainWindow()
        {
            InitializeComponent();
            vm = new MainWindowViewModel();
            this.DataContext = vm;
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            vm.ChangeMessage();
        }
    }
MainWindowViewModel.cs
    class MainWindowViewModel : BindableBase
    {
        private string message;
        public string Message
        {
            get { return message; }
            set { this.SetProperty(ref this.message, value); }
        }

        public void ChangeMessage()
        {
            Task.Run(() =>
                {
                    this.Message = "Start";
                    for (int i = 0; i < 10; i++)
                    {
                        Thread.Sleep(1000);
                        this.Message = i.ToString();
                    }
                });
        }
    }

※BindableBaseは、↓の記事で書いたコードを利用してます。
WPFでもBindableBaseを使ってINotifyPropertyChangedを実装する - SourceChord

バックグラウンドスレッドからObservableCollectionの更新をする

で、前述のサンプルと同じノリで、ObservableCollecitonの更新を使用とすると引っかかります。
非同期処理で、ObservableCollectionに要素の追加・削除などを使用とすると、例外を吐いてしまいます。
(ただし、バックグラウンドスレッドが例外で止まるだけで、アプリ自体は落ちません。で、リストが更新されない、、、なぜ、みたいに悩んでしまったりww)
f:id:minami_SC:20140202145958p:plain

デバッガで実行していると、ObservableCollectionのAddメソッド呼び出し時に例外が出て実行が止まるので、以下のように確認できます。
f:id:minami_SC:20140202150005p:plain

MainWindow.xaml
<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"
        Width="525"
        Height="350">
    <Grid>
        <ListBox Width="138"
                 Height="299"
                 Margin="10,10,0,0"
                 HorizontalAlignment="Left"
                 VerticalAlignment="Top"
                 ItemsSource="{Binding List}" />
        <Button Width="75"
                Margin="153,10,0,0"
                HorizontalAlignment="Left"
                VerticalAlignment="Top"
                Click="Button_Click"
                Content="Button" />
    </Grid>
</Window>
MainWindow.xaml.cs
    public partial class MainWindow : Window
    {
        private MainWindowViewModel vm;
        public MainWindow()
        {
            InitializeComponent();
            vm = new MainWindowViewModel();
            this.DataContext = vm;
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            vm.AddList();
        }
    }
MainWindowViewModel.cs
    class MainWindowViewModel : BindableBase
    {
        private ObservableCollection<string> list;
        public ObservableCollection<string> List
        {
            get { return list; }
            set { this.SetProperty(ref this.list, value); }
        }

        public MainWindowViewModel()
        {
            List = new ObservableCollection<string>()
            {
                "123",
                "456",
                "789"
            };
        }

        public void AddList()
        {
            Task.Run(() =>
            {
                for (int i = 0; i < 10; i++)
                {
                    Task.Delay(1000);
                    this.List.Add(i.ToString());
                }
            });
        }
    }

解決策

コンストラクタに以下の1文を加えると、ObservableCollectionのプロパティを、複数スレッドからコレクション操作できるようにできます。

        public MainWindowViewModel()
        {
            List = new ObservableCollection<string>()
            {
                "123",
                "456",
                "789"
            };
            // 複数スレッドからコレクション操作できるようにする
            BindingOperations.EnableCollectionSynchronization(this.List, new object());
        }

こうすると、バックグラウンドスレッドからも正しくコレクション操作ができます。


別の解決策

そもそも今回のサンプル程度の内容だったら、非同期処理をasync/awaitを使って書けばスッキリできたりします。

わざわざバックグラウンドスレッドからのアクセスをできるようにするのではなく、async/await構文で↓のように非同期処理を書けば、コレクション操作をUIスレッドから実行でき、複数スレッドからのアクセスを考える必要がなくなります。。

MainWindow.xaml.cs
        private async void Button_Click(object sender, RoutedEventArgs e)
        {
            await vm.AddListAsync();
        }
MainWindowViewModel.cs
        public async Task AddListAsync()
        {
            for (int i = 0; i < 10; i++)
            {
                await Task.Delay(1000);
                this.List.Add(i.ToString());
            }
        }