Làm cách nào để cập nhật một ObservableCollection thông qua một chuỗi công nhân?


83

Tôi có một ObservableCollection<A> a_collection;Bộ sưu tập chứa 'n' mục. Mỗi mục A trông như thế này:

public class A : INotifyPropertyChanged
{

    public ObservableCollection<B> b_subcollection;
    Thread m_worker;
}

Về cơ bản, tất cả được kết nối với một chế độ xem danh sách WPF + một điều khiển chế độ xem chi tiết hiển thị b_subcollectionmục đã chọn trong một chế độ xem danh sách riêng biệt (liên kết 2 chiều, cập nhật về thuộc tính được thay đổi, v.v.).

Vấn đề xuất hiện đối với tôi khi tôi bắt đầu triển khai luồng. Toàn bộ ý tưởng là a_collectionsử dụng toàn bộ luồng công nhân của nó để "thực hiện công việc" và sau đó cập nhật tương ứng của họ b_subcollectionsvà để gui hiển thị kết quả trong thời gian thực.

Khi tôi thử nó, tôi nhận được một ngoại lệ nói rằng chỉ luồng Dispatcher mới có thể sửa đổi ObservableCollection và công việc tạm dừng.

Bất cứ ai có thể giải thích vấn đề, và làm thế nào để giải quyết vấn đề?


Hãy thử liên kết sau cung cấp giải pháp an toàn cho chuỗi hoạt động từ bất kỳ chuỗi nào và có thể được liên kết với nhiều chuỗi giao diện người dùng: codeproject.com/Articles/64936/…
Anthony

Câu trả lời:


74

Về mặt kỹ thuật, vấn đề không phải là bạn đang cập nhật ObservableCollection từ một chuỗi nền. Vấn đề là khi bạn làm như vậy, bộ sưu tập tăng sự kiện CollectionChanged của nó trên cùng một chuỗi đã gây ra thay đổi - có nghĩa là các điều khiển đang được cập nhật từ một chuỗi nền.

Để điền một bộ sưu tập từ một chuỗi nền trong khi các điều khiển bị ràng buộc với nó, bạn có thể phải tạo loại bộ sưu tập của riêng mình từ đầu để giải quyết vấn đề này. Tuy nhiên, có một tùy chọn đơn giản hơn có thể phù hợp với bạn.

Đăng lời gọi Thêm lên chuỗi giao diện người dùng.

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());

Phương thức này sẽ trả về ngay lập tức (trước khi mục thực sự được thêm vào bộ sưu tập) sau đó trên chuỗi giao diện người dùng, mục sẽ được thêm vào bộ sưu tập và mọi người nên vui vẻ.

Tuy nhiên, thực tế là giải pháp này có thể sẽ sa lầy khi chịu tải nặng do tất cả các hoạt động xuyên luồng. Một giải pháp hiệu quả hơn sẽ tổng hợp một loạt các mục và đăng chúng lên chuỗi giao diện người dùng theo định kỳ để bạn không gọi qua các chuỗi cho từng mục.

Lớp BackgroundWorker triển khai một mẫu cho phép bạn báo cáo tiến trình thông qua phương thức ReportProgress của nó trong quá trình hoạt động nền. Tiến trình được báo cáo trên chuỗi giao diện người dùng thông qua sự kiện ProgressChanged. Đây có thể là một lựa chọn khác cho bạn.


còn về runWorkerAsyncCompleted của BackgroundWorker thì sao? có bị ràng buộc với chuỗi giao diện người dùng không?
Maciek

1
Vâng theo cách BackgroundWorker được thiết kế là sử dụng SynchronizationContext.Current để nâng cao các sự kiện hoàn thành và tiến độ của nó. Sự kiện DoWork sẽ chạy trên chuỗi nền. Đây là một bài viết hay về phân luồng trong WPF thảo luận về BackgroundWorker cũng vậy msdn.microsoft.com/en-us/magazine/cc163328.aspx#S4
Josh,

5
Câu trả lời này rất đẹp trong sự đơn giản của nó. Cảm ơn vì đã chia sẻ nó!
Beaker

@Michael Trong phần lớn các trường hợp, luồng nền không nên bị chặn và chờ cập nhật trên giao diện người dùng. Sử dụng Dispatcher.Invoke có nguy cơ bị khóa chết nếu hai luồng chờ nhau và tốt nhất sẽ làm giảm hiệu suất của mã của bạn một cách đáng kể. Trong trường hợp cụ thể của bạn, bạn có thể cần phải làm theo cách này, nhưng đối với phần lớn các tình huống, câu cuối cùng của bạn chỉ đơn giản là không đúng.
Josh

@Josh Tôi đã xóa câu trả lời của mình, vì trường hợp của tôi có vẻ đặc biệt. Tôi sẽ nhìn xa hơn trong thiết kế của mình và suy nghĩ lại, những gì có thể được thực hiện tốt hơn.
Michael

125

Tùy chọn mới cho .NET 4.5

Bắt đầu từ .NET 4.5 có một cơ chế tích hợp để tự động đồng bộ hóa quyền truy cập vào các CollectionChangedsự kiện thu thập và gửi đến chuỗi giao diện người dùng. Để bật tính năng này, bạn cần gọi từ trong chuỗi giao diện người dùng của mình .BindingOperations.EnableCollectionSynchronization

EnableCollectionSynchronization làm hai điều:

  1. Ghi nhớ luồng mà từ đó nó được gọi và gây ra đường ống liên kết dữ liệu với CollectionChangedcác sự kiện thống nhất trên luồng đó.
  2. Có được một khóa đối với bộ sưu tập cho đến khi xử lý xong sự kiện được điều chỉnh, để các trình xử lý sự kiện đang chạy chuỗi giao diện người dùng sẽ không cố đọc bộ sưu tập khi nó đang được sửa đổi từ một chuỗi nền.

Rất quan trọng, điều này không quan tâm đến tất cả mọi thứ : để đảm bảo luồng truy cập an toàn vào bộ sưu tập vốn dĩ không an toàn cho luồng, bạn phải hợp tác với khuôn khổ bằng cách lấy cùng một khóa từ các luồng nền của bạn khi bộ sưu tập sắp được sửa đổi.

Do đó, các bước cần thiết để vận hành chính xác là:

1. Quyết định loại khóa bạn sẽ sử dụng

Điều này sẽ xác định quá tải của EnableCollectionSynchronizationphải được sử dụng. Hầu hết thời gian một lockcâu lệnh đơn giản sẽ đủ để quá tải này là lựa chọn tiêu chuẩn, nhưng nếu bạn đang sử dụng một số cơ chế đồng bộ hóa ưa thích thì cũng có hỗ trợ cho các khóa tùy chỉnh .

2. Tạo bộ sưu tập và bật đồng bộ hóa

Tùy thuộc vào cơ chế khóa đã chọn, gọi quá tải thích hợp trên chuỗi giao diện người dùng . Nếu sử dụng một lockcâu lệnh chuẩn, bạn cần cung cấp đối tượng khóa làm đối số. Nếu sử dụng đồng bộ hóa tùy chỉnh, bạn cần cung cấp một CollectionSynchronizationCallbackđại biểu và một đối tượng ngữ cảnh (có thể là null). Khi được gọi, đại biểu này phải có được khóa tùy chỉnh của bạn, gọi khóa Actionđược truyền cho nó và nhả khóa trước khi quay lại.

3. Hợp tác bằng cách khóa bộ sưu tập trước khi sửa đổi nó

Bạn cũng phải khóa bộ sưu tập bằng cơ chế tương tự khi bạn chuẩn bị tự sửa đổi nó; thực hiện điều này với lock()cùng một đối tượng khóa được chuyển đến EnableCollectionSynchronizationtrong kịch bản đơn giản hoặc với cùng một cơ chế đồng bộ hóa tùy chỉnh trong kịch bản tùy chỉnh.


2
Điều này có khiến các bản cập nhật bộ sưu tập bị chặn cho đến khi chuỗi giao diện người dùng xử lý chúng không? Trong các tình huống liên quan đến bộ sưu tập liên kết dữ liệu một chiều của các đối tượng bất biến (một kịch bản tương đối phổ biến), có vẻ như có thể có một lớp bộ sưu tập sẽ giữ "phiên bản hiển thị cuối cùng" của mỗi đối tượng cũng như một hàng đợi thay đổi và sử dụng BeginInvokeđể chạy một phương pháp sẽ thực hiện tất cả các thay đổi thích hợp trong chuỗi giao diện người dùng [nhiều nhất một phương thức BeginInvokesẽ đang chờ xử lý tại bất kỳ thời điểm nào.
supercat

1
Thậm chí không bao giờ biết điều này tồn tại! Cảm ơn vì đã viết cái này!
Kelly,

15
Một ví dụ nhỏ sẽ làm cho câu trả lời này hữu ích hơn nhiều. Tôi nghĩ đó có lẽ là giải pháp đúng, nhưng tôi không biết làm thế nào để thực hiện nó.
RubberDuck

2
@Kohanz Việc mời đến trình điều phối chuỗi giao diện người dùng có một số nhược điểm. Điều lớn nhất là bộ sưu tập của bạn sẽ không được cập nhật cho đến khi chuỗi giao diện người dùng thực sự xử lý công văn và sau đó bạn sẽ chạy trên chuỗi giao diện người dùng có thể gây ra các vấn đề về phản hồi. Mặt khác, với phương pháp khóa, bạn ngay lập tức cập nhật bộ sưu tập và có thể tiếp tục xử lý trên chuỗi nền của mình mà không phụ thuộc vào chuỗi giao diện người dùng làm bất cứ điều gì. Chuỗi giao diện người dùng sẽ bắt kịp các thay đổi trong chu kỳ hiển thị tiếp theo nếu cần.
Mike Marynowski

2
Có cái nhìn sâu sắc hơn từ những câu trả lời cho chủ đề này về EnableCollectionSynchronization: stackoverflow.com/a/16511740/2887274
Matthew S

22

Với .NET 4.0, bạn có thể sử dụng một lớp lót sau:

.Add

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

.Remove

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

11

Mã đồng bộ sưu tập cho hậu thế. Điều này sử dụng cơ chế khóa đơn giản để kích hoạt đồng bộ hóa bộ sưu tập. Lưu ý rằng bạn sẽ phải bật đồng bộ hóa bộ sưu tập trên chuỗi giao diện người dùng.

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);
        }
    }
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.