Asynchronous update to ObservableCollection items

2020-07-10 09:06发布

问题:

I'm new to multithreading and WPF.

I have an ObservableCollection<RSSFeed>, at app startup items are added to this collection from UI thread. Properties of RSSFeed are bind to WPF ListView. Later, I want to update each RSSFeed asynchronously. So I'm thinking of implementing something like RSSFeed.FetchAsync() and raising PropertyChanged on its updated properties.

I know that ObservableCollection doesn't support updates from threads other than the UI thread, it throws NotSupportedException. But since I'm not manipulating the ObservableCollection itself but rather updating properties on its items, can I expect this to work and see ListView items updated? Or would it threw an exception anyway due to PropertyChanged?

Edit: code

RSSFeed.cs

public class RSSFeed
{
    public String Title { get; set; }
    public String Summary { get; set; }
    public String Uri { get; set; }        
    public String Encoding { get; set; }
    public List<FeedItem> Posts { get; set; }
    public bool FetchedSuccessfully { get; protected set; }        

    public RSSFeed()
    {
        Posts = new List<FeedItem>();
    }

    public RSSFeed(String uri)
    {
        Posts = new List<FeedItem>();
        Uri = uri;
        Fetch();
    }

    public void FetchAsync()
    { 
        // call Fetch asynchronously
    }

    public void Fetch()
    {
        if (Uri != "")
        {
            try
            {
                MyWebClient client = new MyWebClient();
                String str = client.DownloadString(Uri);

                str = Regex.Replace(str, "<!--.*?-->", String.Empty, RegexOptions.Singleline);
                FeedXmlReader reader = new FeedXmlReader();
                RSSFeed feed = reader.Load(str, new Uri(Uri));

                if (feed.Title != null)
                    Title = feed.Title;
                if (feed.Encoding != null)
                    Encoding = feed.Encoding;
                if (feed.Summary != null)
                    Summary = feed.Summary;
                if (feed.Posts != null)
                    Posts = feed.Posts;

                FetchedSuccessfully = true;
            }
            catch
            {
                FetchedSuccessfully = false;
            }

        }
    }

UserProfile.cs

public class UserProfile : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    public event CollectionChangeEventHandler CollectionChanged;

    private ObservableCollection<RSSFeed> feeds;
    public ObservableCollection<RSSFeed> Feeds 
    { 
        get { return feeds; }
        set { feeds = value; OnPropertyChanged("Feeds"); }
    }

    public UserProfile()
    {
        feeds = new ObservableCollection<RSSFeed>();
    }

    protected void OnPropertyChanged(string name)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(name));
        }
    }

    protected void OnCollectionChanged(RSSFeed feed)
    {
        CollectionChangeEventHandler handler = CollectionChanged;
        if (handler != null)
        {
            handler(this, new CollectionChangeEventArgs(CollectionChangeAction.Add, feed));
        }
    }
}

MainWindow.xaml.cs

public partial class MainWindow : Window, INotifyPropertyChanged
{
    // My ListView is bound to this
    // ItemsSource="{Binding Posts}
    public List<FeedItem> Posts
    {
        get 
        {
            if (listBoxChannels.SelectedItem != null)
                return ((RSSFeed)listBoxChannels.SelectedItem).Posts;
            else
                return null;
        }
    }

    private void Window_Loaded(object sender, RoutedEventArgs e)
    {
        // here I load cached feeds
        // called from UI thread

        // now I want to update the feeds
        // since network operations are involved, 
        // I need to do this asynchronously to prevent blocking the UI thread
    }

}

Thanks.

回答1:

For this kind of application, I usually use a BackgroundWorker with ReportsProgress set to True. Then you can pass one object for each call as the userState parameter in the ReportProgress method. The ProgressChanged event will run on the UI thread, so you can add the object to the ObservableCollection in the event handler.

Otherwise, updating the properties from a background thread will work, but if you are filtering or sorting the ObservableCollection, then the filter will not be reapplied unless some collection change notification event has been raised.

You can cause filters and sorts to be reapplied by finding the index of the item in the collection (e.g. by reporting it as progresspercentage) and setting the list.item(i) = e.userstate, i.e. replacing the item in the list by itself in the ProgressChanged event. This way, the SelectedItem of any controls bound to the collection will be preserved, while filter and sorting will respect any changed values in the item.



回答2:

With .Net 4.5, you can add support for background thread updates to an ObservableCollection by using BindingOperations.EnableCollectionSynchronization. This works great with MVVM.

See: BindingOperations.EnableCollectionSynchronization() equivalent for .net 4.0



回答3:

If you are using WPF you are allowed to update properties on individual bound items and raise PropertyChanged from a background thread. The WPF data binding mechanisms (unlike the WinForms equivalent) detect this and marshal to the UI thread for you. There is a cost to this of course- using the automatic mechanism, each individual property update will cause a marshalling event, so if you are changing lots of properties performance may suffer, and you should consider doing the UI marshalling yourself as a single batch operation.

You are not allowed to manipulate collections however (add/remove items), so if your RSS feeds contain nested collections that you want to bind to you need to hoist the whole update to the UI thread ahead of time.



回答4:

You may want to look at the ConcurrentCollections namespace in .Net.

http://msdn.microsoft.com/en-us/library/system.collections.concurrent.aspx

Here is another question that maybe helpful as well.

ObservableCollection and threading



回答5:

I had a similar scenario and met this "ObservableCollection doesn't support updates from threads other than the UI thread", finally it got solved by referring this AsyncObservableCollection implement in Thomas Levesque 's blog, I think it may be helpful to you.

In its Update version, SynchronizationContext is used to solve this problem. You can refer to the MSDN page of SynchronizationContext