Bir ObservableCollection bir işçi iş parçacığı aracılığıyla nasıl güncellerim?


83

Bir var ObservableCollection<A> a_collection;koleksiyon 'n' öğeleri içerir. Her bir A öğesi şuna benzer:

public class A : INotifyPropertyChanged
{

    public ObservableCollection<B> b_subcollection;
    Thread m_worker;
}

Temel olarak, hepsi bir WPF liste görünümüne + b_subcollectionseçilen öğenin ayrı bir liste görünümünde (2 yönlü bağlamalar, değiştirilen özelliklerle ilgili güncellemeler vb.) Gösteren bir ayrıntı görünümü denetimine bağlıdır .

Diş açmaya başladığımda sorun benim için ortaya çıktı. Tüm fikir, a_collection"iş yapmak" için çalışan iş parçacığının tüm kullanımına sahip olmak ve ardından bunları güncellemek b_subcollectionsve gui'nin sonuçları gerçek zamanlı olarak göstermesini sağlamaktı.

Bunu denediğimde, yalnızca Dispatcher iş parçacığının bir ObservableCollection'ı değiştirebileceğini ve işin durma noktasına geldiğini söyleyen bir istisna aldım.

Sorunu ve bunun üstesinden nasıl gelineceğini açıklayan var mı?


Herhangi bir iş parçacığından çalışan ve birden çok UI iş parçacığı aracılığıyla bağlanabilen iş parçacığı güvenli bir çözüm sağlayan aşağıdaki bağlantıyı deneyin: codeproject.com/Articles/64936/…
Anthony

Yanıtlar:


74

Teknik olarak sorun, ObservableCollection'ı bir arka plan iş parçacığından güncellemeniz değildir. Sorun şu ki, bunu yaptığınızda, koleksiyon, CollectionChanged olayını değişikliğe neden olan aynı iş parçacığı üzerinde yükseltir - bu, kontrollerin bir arka plan iş parçacığından güncellendiği anlamına gelir.

Kontroller ona bağlıyken bir arka plan iş parçacığından bir koleksiyonu doldurmak için, bunu ele almak için muhtemelen sıfırdan kendi koleksiyon türünüzü oluşturmanız gerekir. Yine de sizin için çalışabilecek daha basit bir seçenek var.

Çağrıları UI iş parçacığına gönderin

public static void AddOnUI<T>(this ICollection<T> collection, T item) {
    Action<T> addMethod = collection.Add;
    Application.Current.Dispatcher.BeginInvoke( addMethod, item );
}

...

b_subcollection.AddOnUI(new B());

Bu yöntem hemen (öğe koleksiyona eklenmeden önce) geri dönecek ve ardından UI iş parçacığında öğe koleksiyona eklenecek ve herkes mutlu olacaktır.

Ancak gerçek şu ki, bu çözümün tüm çapraz iş parçacığı etkinliği nedeniyle ağır yük altında batması muhtemeldir. Daha verimli bir çözüm, bir grup öğeyi gruplandırır ve bunları düzenli olarak UI iş parçacığına gönderir, böylece her öğe için iş parçacıkları arasında arama yapmazsınız.

BackgroundWorker sınıfının uyguladığı onun aracılığıyla ilerleme kaydedilmesine olanak sağlayan bir desen ReportProgress bir arka plan işlemi sırasında yöntemle. İlerleme, ProgressChanged olayı aracılığıyla UI iş parçacığında bildirilir. Bu sizin için başka bir seçenek olabilir.


BackgroundWorker'ın runWorkerAsyncCompleted'i ne olacak? bu da UI iş parçacığına bağlı mı?
Maciek

1
Evet, BackgroundWorker'ın tasarlanma şekli, tamamlama ve ilerleme olaylarını yükseltmek için SynchronizationContext.Current'ı kullanmaktır. DoWork olayı arka plan iş parçacığında çalışacaktır. İşte BackgroundWorker'ı da tartışan WPF'de
Josh

5
Bu cevap basitliği içinde güzeldir. Paylaştığınız için teşekkürler!
Beaker

@Michael Vakaların çoğunda, arka plan iş parçacığı engellenmemeli ve kullanıcı arayüzünün güncellenmesini beklememelidir. Dispatcher.Invoke kullanmak, iki iş parçacığı birbirini beklerse ve en iyi ihtimalle kodunuzun performansını önemli ölçüde düşürürse, kilitlenme riskini taşır. Sizin özel durumunuzda, bunu bu şekilde yapmanız gerekebilir, ancak çoğu durumda son cümleniz doğru değildir.
Josh

@Josh Cevabımı sildim çünkü davam özel görünüyor. Tasarımıma daha fazla bakacağım ve daha iyi ne yapılabilirdi diye tekrar düşüneceğim.
Michael

125

.NET 4.5 için yeni seçenek

NET 4.5'ten başlayarak, koleksiyona erişimi otomatik olarak senkronize etmek ve CollectionChangedolayları UI iş parçacığına göndermek için yerleşik bir mekanizma vardır . Bu özelliği etkinleştirmek için UI iş parçacığınızın içinden aramanız gerekir .BindingOperations.EnableCollectionSynchronization

EnableCollectionSynchronization iki şey yapar:

  1. Çağrıldığı iş parçacığını hatırlar ve veri bağlama işlem hattının CollectionChangedbu iş parçacığı üzerindeki olayları sıraya koymasına neden olur .
  2. Koleksiyonda sıralanan olay işlenene kadar bir kilit alır, böylece UI iş parçacığını çalıştıran olay işleyicileri koleksiyonu bir arka plan iş parçacığından değiştirilirken okumaya çalışmaz.

Çok önemli olarak, bu her şeyi halletmez : Doğası gereği iş parçacığı güvenli olmayan bir koleksiyona iş parçacığı açısından güvenli erişim sağlamak için , koleksiyon değiştirilmek üzereyken arka plan iş parçacıklarınızdan aynı kilidi alarak çerçeve ile işbirliği yapmanız gerekir .

Bu nedenle, doğru işlem için gerekli adımlar şunlardır:

1. Ne tür bir kilit kullanacağınıza karar verin

Bu, hangi aşırı yükün EnableCollectionSynchronizationkullanılması gerektiğini belirleyecektir . Çoğu zaman basit bir lockifade yeterli olacaktır , bu nedenle bu aşırı yükleme standart seçimdir, ancak bazı süslü senkronizasyon mekanizmaları kullanıyorsanız , özel kilitler için de destek vardır .

2. Koleksiyonu oluşturun ve senkronizasyonu etkinleştirin

Seçilen kilit mekanizmasına bağlı olarak, UI iş parçacığında uygun aşırı yüklemeyi çağırın . Standart bir lockifade kullanıyorsanız , kilit nesnesini bağımsız değişken olarak sağlamanız gerekir. Özel senkronizasyon kullanıyorsanız, bir CollectionSynchronizationCallbacktemsilci ve bir bağlam nesnesi (bu olabilir null) sağlamanız gerekir . Çağrıldığında, bu delege Actiongeri dönmeden önce özel kilidinizi almalı, ona geçirileni çağırmalı ve kilidi serbest bırakmalıdır.

3. Koleksiyonu değiştirmeden önce koleksiyonu kilitleyerek işbirliği yapın

Kendiniz değiştirmek üzereyken de koleksiyonu aynı mekanizmayı kullanarak kilitlemelisiniz; bunu basit senaryoda lock()iletilen aynı kilit nesnesi EnableCollectionSynchronizationile veya özel senaryodaki aynı özel senkronizasyon mekanizması ile yapın.


2
Bu, koleksiyon güncellemelerinin UI iş parçacığı bunları işlemeye başlayana kadar engellenmesine neden olur mu? Değişmez nesnelerin tek yönlü veri bağlantılı koleksiyonlarını içeren senaryolarda (nispeten yaygın bir senaryo), her bir nesnenin "son görüntülenen sürümünü" ve bir değişiklik kuyruğunu saklayacak bir koleksiyon sınıfına sahip olmak mümkün gibi görünüyor. , ve BeginInvokeUI iş parçacığındaki tüm uygun değişiklikleri gerçekleştirecek bir yöntemi çalıştırmak için kullanın [ BeginInvokeherhangi bir zamanda en fazla biri beklemede olacaktır.
supercat

16
Küçük bir örnek bu cevabı çok daha kullanışlı hale getirebilir. Muhtemelen doğru çözüm olduğunu düşünüyorum, ancak nasıl uygulanacağı hakkında hiçbir fikrim yok.
RubberDuck

3
@Kohanz UI iş parçacığı dağıtıcısına çağrılmanın birçok dezavantajı vardır. En büyüğü, koleksiyonunuzun UI iş parçacığı gönderimi gerçekten işleyene kadar güncellenmeyeceği ve ardından yanıt verme sorunlarına neden olabilecek UI iş parçacığı üzerinde çalıştırılacak olmanızdır. Öte yandan kilitleme yöntemiyle, koleksiyonu hemen güncellersiniz ve UI iş parçacığının hiçbir şey yapmasına bağlı kalmadan arka plan iş parçacığınız üzerinde işlem yapmaya devam edebilirsiniz. UI iş parçacığı, gerektiğinde sonraki oluşturma döngüsündeki değişiklikleri yakalayacaktır.
Mike Marynowski

2
Yaklaşık bir aydır 4.5'teki koleksiyon senkronizasyonuna bakıyorum ve bu yanıtın bazılarının doğru olduğunu düşünmüyorum. Yanıt, enable çağrısının UI iş parçacığında gerçekleşmesi gerektiğini ve geri çağrının UI iş parçacığında gerçekleştiğini belirtir. Bunların hiçbiri doğru görünmüyor. Bir arka plan iş parçacığında koleksiyon senkronizasyonunu etkinleştirebiliyorum ve yine de bu mekanizmayı kullanabiliyorum. Ayrıca, çerçeve içinde derin aramalar (. ViewManager.AccessCollection cf herhangi sıralanırken yapmayın referencesource.microsoft.com/#PresentationFramework/src/... )
Reginald Mavi

2
EnableCollectionSynchronization hakkında bu ileti dizisinin yanıtından daha fazla bilgi var: stackoverflow.com/a/16511740/2887274
Matthew S

22

.NET 4.0 ile şu tek satırları kullanabilirsiniz:

.Add

Application.Current.Dispatcher.BeginInvoke(new Action(() => this.MyObservableCollection.Add(myItem)));

.Remove

Application.Current.Dispatcher.BeginInvoke(new Func<bool>(() => this.MyObservableCollection.Remove(myItem)));

11

Gelecek nesil için koleksiyon senkronizasyon kodu. Bu, koleksiyon senkronizasyonunu etkinleştirmek için basit kilit mekanizmasını kullanır. UI iş parçacığında koleksiyon senkronizasyonunu etkinleştirmeniz gerekeceğine dikkat edin.

public class MainVm
{
    private ObservableCollection<MiniVm> _collectionOfObjects;
    private readonly object _collectionOfObjectsSync = new object();

    public MainVm()
    {

        _collectionOfObjects = new ObservableCollection<MiniVm>();
        // Collection Sync should be enabled from the UI thread. Rest of the collection access can be done on any thread
        Application.Current.Dispatcher.BeginInvoke(new Action(() => 
        { BindingOperations.EnableCollectionSynchronization(_collectionOfObjects, _collectionOfObjectsSync); }));
    }

    /// <summary>
    /// A different thread can access the collection through this method
    /// </summary>
    /// <param name="newMiniVm">The new mini vm to add to observable collection</param>
    private void AddMiniVm(MiniVm newMiniVm)
    {
        lock (_collectionOfObjectsSync)
        {
            _collectionOfObjects.Insert(0, newMiniVm);
        }
    }
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.