バックグラウンド スレッドでビジネス オブジェクトのコレクションを更新すると、次のエラー メッセージが表示されます。
このタイプの CollectionView は、Dispatcher スレッドとは異なるスレッドからの SourceCollection への変更をサポートしていません。
わかりました、それは理にかなっています。ただし、CollectionView のどのバージョンが複数のスレッドをサポートしているのか、オブジェクトにそれを使用させるにはどうすればよいのかという疑問も生じます。
バックグラウンド スレッドでビジネス オブジェクトのコレクションを更新すると、次のエラー メッセージが表示されます。
このタイプの CollectionView は、Dispatcher スレッドとは異なるスレッドからの SourceCollection への変更をサポートしていません。
わかりました、それは理にかなっています。ただし、CollectionView のどのバージョンが複数のスレッドをサポートしているのか、オブジェクトにそれを使用させるにはどうすればよいのかという疑問も生じます。
使用する:
System.Windows.Application.Current.Dispatcher.Invoke(
System.Windows.Threading.DispatcherPriority.Normal,
(Action)delegate()
{
// Your Action Code
});
以下は、Jonathan によって発見された実装の改善です。最初に、イベント ハンドラーがすべて同じ (UI) ディスパッチャー上にあると想定するのではなく、関連付けられたディスパッチャー上で各イベント ハンドラーを実行します。次に、BeginInvoke を使用して、ディスパッチャーが使用可能になるまで処理を続行できるようにします。これにより、バックグラウンド スレッドが更新ごとに多くの処理を行っている状況で、ソリューションが大幅に高速化されます。おそらくもっと重要なのは、Invoke の待機中にブロックすることによって引き起こされる問題を克服することです (たとえば、ConcurrencyMode.Single で WCF を使用する場合にデッドロックが発生する可能性があります)。
public class MTObservableCollection<T> : ObservableCollection<T>
{
public override event NotifyCollectionChangedEventHandler CollectionChanged;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
NotifyCollectionChangedEventHandler CollectionChanged = this.CollectionChanged;
if (CollectionChanged != null)
foreach (NotifyCollectionChangedEventHandler nh in CollectionChanged.GetInvocationList())
{
DispatcherObject dispObj = nh.Target as DispatcherObject;
if (dispObj != null)
{
Dispatcher dispatcher = dispObj.Dispatcher;
if (dispatcher != null && !dispatcher.CheckAccess())
{
dispatcher.BeginInvoke(
(Action)(() => nh.Invoke(this,
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset))),
DispatcherPriority.DataBind);
continue;
}
}
nh.Invoke(this, e);
}
}
}
BeginInvoke を使用しているため、ハンドラーが呼び出される前に、通知された変更が取り消される可能性があります。これは通常、「インデックスが範囲外でした」という結果になります。イベント引数がリストの新しい (変更された) 状態に対してチェックされるときに例外がスローされます。これを回避するために、遅延イベントはすべてリセット イベントに置き換えられます。これにより、場合によっては過度の再描画が発生する可能性があります。
Bea Stollnitz によるこの投稿では、そのエラー メッセージと、そのように表現されている理由について説明しています。
編集: Beaのブログから
残念ながら、このコードは「NotSupportedException – このタイプの CollectionView は、Dispatcher スレッドとは異なるスレッドからの SourceCollection への変更をサポートしていません。」という例外が発生します。このエラー メッセージにより、使用している CollectionView がクロススレッドの変更をサポートしていない場合は、クロススレッドの変更をサポートするものを見つける必要があると人々が考えるようになることを理解しています。さて、このエラー メッセージは少し誤解を招きます。標準で提供されている CollectionViews のいずれも、クロススレッド コレクションの変更をサポートしていません。いいえ、残念ながら、現時点ではエラー メッセージを修正することはできません。
一つ見つかった。
public class MTObservableCollection<T> : ObservableCollection<T>
{
public override event NotifyCollectionChangedEventHandler CollectionChanged;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
var eh = CollectionChanged;
if (eh != null)
{
Dispatcher 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);
}
}
}
}
http://www.julmar.com/blog/mark/2009/04/01/AddingToAnObservableCollectionFromABackgroundThread.aspx
Sorry, can't add a comment but all this is wrong.
ObservableCollection is not thread safe. Not only because of this dispatcher issues, but it's not thread safe at all (from msdn):
Any public static (Shared in Visual Basic) members of this type are thread safe. Any instance members are not guaranteed to be thread safe.
Look here http://msdn.microsoft.com/en-us/library/ms668604(v=vs.110).aspx
There's also a problem when calling BeginInvoke with a "Reset" action. "Reset" is the only action where handler should look at the collection itself. If you BeginInvoke a "Reset" and then immediately BeginInvoke a couple of "Add" actions than handler will accept a "Reset" with already updated collection and next "Add"'s will create a mess.
Here's my implementation which works. Actually I'm thinking of removing BeginInvoke at all:
次のようにコレクションの同期を有効にすることで、コレクションへのクロススレッド変更を管理する wpf を取得できます。
BindingOperations.EnableCollectionSynchronization(collection, syncLock);
listBox.ItemsSource = collection;
これは、コレクションが UI スレッドから変更される可能性があることを WPF に伝え、UI の変更を適切なスレッドにマーシャリングする必要があることを認識します。
ロック オブジェクトがない場合に同期コールバックを提供するオーバーロードもあります。
WPF UI コントロールを定期的に更新し、同時に UI を使用する場合は、DispatcherTimerを使用できます。
XAML
<Grid>
<DataGrid AutoGenerateColumns="True" Height="200" HorizontalAlignment="Left" Name="dgDownloads" VerticalAlignment="Top" Width="548" />
<Label Content="" Height="28" HorizontalAlignment="Left" Margin="0,221,0,0" Name="lblFileCouner" VerticalAlignment="Top" Width="173" />
</Grid>
C#
public partial class DownloadStats : Window
{
private MainWindow _parent;
DispatcherTimer timer = new DispatcherTimer();
ObservableCollection<FileView> fileViewList = new ObservableCollection<FileView>();
public DownloadStats(MainWindow parent)
{
InitializeComponent();
_parent = parent;
Owner = parent;
timer.Interval = new TimeSpan(0, 0, 1);
timer.Tick += new EventHandler(timer_Tick);
timer.Start();
}
void timer_Tick(object sender, EventArgs e)
{
dgDownloads.ItemsSource = null;
fileViewList.Clear();
if (_parent.contentManagerWorkArea.Count > 0)
{
foreach (var item in _parent.contentManagerWorkArea)
{
FileView nf = item.Value.FileView;
fileViewList.Add(nf);
}
}
if (fileViewList.Count > 0)
{
lblFileCouner.Content = fileViewList.Count;
dgDownloads.ItemsSource = fileViewList;
}
}
}
これを試して:
this.Dispatcher.Invoke(DispatcherPriority.Background, new Action(
() =>
{
//Code
}));
Dispatcher.BeginInvoke を使用してください。