多くのItemControls(データグリッドとリストビュー)を使用するアプリケーションを構築しています。これらのリストをバックグラウンドスレッドから簡単に更新するために、この拡張機能をObservableCollectionsに使用しました。
.NET 4.5用に作成されたコンポーネントを使用したいので、今日はVS12(.NET 4.5もインストール)をインストールしました。プロジェクトを.NET 4.5(4.0から)にアップグレードする前でも、ワーカースレッドから更新されると、データグリッドがInvalidOperationExceptionをスローし始めました。例外メッセージ:
この例外は、名前 '(unnamed)'のコントロール 'System.Windows.Controls.DataGrid Items.Count:5'のジェネレーターが、Itemsコレクションの現在の状態と一致しないCollectionChangedイベントのシーケンスを受け取ったためにスローされました。次の違いが検出されました:累積カウント4は実際のカウント5とは異なります。[累積カウントは(最後のリセット時のカウント+#追加-#最後のリセット以降の削除数)です。]
再現コード:
XAML:
<Window x:Class="Test1.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>
<DataGrid ItemsSource="{Binding Items, Mode=OneTime}" PresentationTraceSources.TraceLevel="High"/>
</Grid>
</Window>
コード:
public partial class MainWindow : Window
{
public ExtendedObservableCollection<int> Items { get; private set; }
public MainWindow()
{
InitializeComponent();
Items = new ExtendedObservableCollection<int>();
DataContext = this;
Loaded += MainWindow_Loaded;
}
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
Task.Factory.StartNew(() =>
{
foreach (var item in Enumerable.Range(1, 500))
{
Items.Add(item);
}
});
}
}
WPF 4.5は、非UIスレッドのコレクションにアクセスするためのいくつかの新機能を提供します。
WPFを使用すると、コレクションを作成したスレッド以外のスレッドのデータコレクションにアクセスして変更できます。これにより、バックグラウンドスレッドを使用して、データベースなどの外部ソースからデータを受信し、UIスレッドにデータを表示できます。別のスレッドを使用してコレクションを変更することにより、ユーザーインターフェイスはユーザーの操作に応答し続けます。
これは、BindingOperations
クラスで静的メソッド EnableCollectionSynchronization を使用して実行できます。
収集または変更するデータが大量にある場合は、バックグラウンドスレッドを使用してデータを収集および変更し、ユーザーインターフェイスが入力に対して反応し続けるようにすることができます。複数のスレッドがコレクションにアクセスできるようにするには、EnableCollectionSynchronizationメソッドを呼び出します。 EnableCollectionSynchronization(IEnumerable、Object)メソッドのこのオーバーロードを呼び出すと、システムはコレクションにアクセスするときにコレクションをロックします。コレクションを自分でロックするコールバックを指定するには、EnableCollectionSynchronization(IEnumerable、Object、CollectionSynchronizationCallback)オーバーロードを呼び出します。
使い方は以下の通りです。コレクションの同期のロックとして使用されるオブジェクトを作成します。次に、BindingsOperationsのEnableCollectionSynchronizationメソッドを呼び出して、同期するコレクションとロックに使用されるオブジェクトを渡します。
コードを更新し、詳細を追加しました。また、競合を回避するために、コレクションを通常のObservableCollectionに変更しました。
public partial class MainWindow : Window{
public ObservableCollection<int> Items { get; private set; }
//lock object for synchronization;
private static object _syncLock = new object();
public MainWindow()
{
InitializeComponent();
Items = new ObservableCollection<int>();
//Enable the cross acces to this collection elsewhere
BindingOperations.EnableCollectionSynchronization(Items, _syncLock);
DataContext = this;
Loaded += MainWindow_Loaded;
}
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
Task.Factory.StartNew(() =>
{
foreach (var item in Enumerable.Range(1, 500))
{
lock(_syncLock) {
Items.Add(item);
}
}
});
}
}
参照: http://10rem.net/blog/2012/01/20/wpf-45-cross-thread-collection-synchronization-redux
このトピックを要約すると、このAsyncObservableCollection
は.NET 4および.NET 4.5 WPFアプリで動作します。
using System;
using System.Collections;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Windows.Data;
using System.Windows.Threading;
namespace WpfAsyncCollection
{
public class AsyncObservableCollection<T> : ObservableCollection<T>
{
public override event NotifyCollectionChangedEventHandler CollectionChanged;
private static object _syncLock = new object();
public AsyncObservableCollection()
{
enableCollectionSynchronization(this, _syncLock);
}
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
using (BlockReentrancy())
{
var eh = CollectionChanged;
if (eh == null) return;
var dispatcher = (from NotifyCollectionChangedEventHandler nh in eh.GetInvocationList()
let dpo = nh.Target as DispatcherObject
where dpo != null
select dpo.Dispatcher).FirstOrDefault();
if (dispatcher != null && dispatcher.CheckAccess() == false)
{
dispatcher.Invoke(DispatcherPriority.DataBind, (Action)(() => OnCollectionChanged(e)));
}
else
{
foreach (NotifyCollectionChangedEventHandler nh in eh.GetInvocationList())
nh.Invoke(this, e);
}
}
}
private static void enableCollectionSynchronization(IEnumerable collection, object lockObject)
{
var method = typeof(BindingOperations).GetMethod("EnableCollectionSynchronization",
new Type[] { typeof(IEnumerable), typeof(object) });
if (method != null)
{
// It's .NET 4.5
method.Invoke(null, new object[] { collection, lockObject });
}
}
}
}
Jehofからの答えは正しいです。
まだ4.5をターゲットにすることはできず、(イベント通知中にDispatcherを使用して)バックグラウンド更新をすでに許可しているカスタムの監視可能なコレクションでこの問題が発生しました。
誰かが便利だと思った場合、.NET 4.0を対象とするアプリケーションで次のコードを使用して、実行環境が.NET 4.5の場合にこの機能を使用できるようにしました。
public static void EnableCollectionSynchronization(IEnumerable collection, object lockObject)
{
// Equivalent to .NET 4.5:
// BindingOperations.EnableCollectionSynchronization(collection, lockObject);
MethodInfo method = typeof(BindingOperations).GetMethod("EnableCollectionSynchronization", new Type[] { typeof(IEnumerable), typeof(object) });
if (method != null)
{
method.Invoke(null, new object[] { collection, lockObject });
}
}
これは、この問題が発生する可能性があるVS 2017のリリースバージョンを使用しているWindows 10 Version 1607ユーザー向けです。
Microsoft Visual Studio Community 2017
Version 15.1 (26403.3) Release
VisualStudio.15.Release/15.1.0+26403.3
Microsoft .NET Framework
Version 4.6.01586
lockもEnableCollectionSynchronizationも必要ありませんでした。
<ListBox x:Name="FontFamilyListBox" SelectedIndex="{Binding SelectedIndex, Mode=TwoWay}" Width="{Binding FontFamilyWidth, Mode=TwoWay}"
SelectedItem="{Binding FontFamilyItem, Mode=TwoWay}"
ItemsSource="{Binding FontFamilyItems}"
diag:PresentationTraceSources.TraceLevel="High">
<ListBox.ItemTemplate>
<DataTemplate DataType="typeData:FontFamilyItem">
<Grid>
<TextBlock Text="{Binding}" diag:PresentationTraceSources.TraceLevel="High"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
public ObservableCollection<string> fontFamilyItems;
public ObservableCollection<string> FontFamilyItems
{
get { return fontFamilyItems; }
set { SetProperty(ref fontFamilyItems, value, nameof(FontFamilyItems)); }
}
public string fontFamilyItem;
public string FontFamilyItem
{
get { return fontFamilyItem; }
set { SetProperty(ref fontFamilyItem, value, nameof(FontFamilyItem)); }
}
private List<string> GetItems()
{
List<string> fonts = new List<string>();
foreach (System.Windows.Media.FontFamily font in Fonts.SystemFontFamilies)
{
fonts.Add(font.Source);
....
other stuff..
}
return fonts;
}
public async void OnFontFamilyViewLoaded(object sender, EventArgs e)
{
DisposableFontFamilyViewLoaded.Dispose();
Task<List<string>> getItemsTask = Task.Factory.StartNew(GetItems);
try
{
foreach (string item in await getItemsTask)
{
FontFamilyItems.Add(item);
}
}
catch (Exception x)
{
throw new Exception("Error - " + x.Message);
}
...
other stuff
}