7

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.

Martin
  • 1,877
  • 5
  • 21
  • 37

6 Answers6

5

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

Community
  • 1
  • 1
KTCO
  • 2,115
  • 23
  • 21
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.

Chris Shain
  • 50,833
  • 6
  • 93
  • 125
  • Thanks. I have a nested collection in my RSSFeed class. Would it be a good soulution if RSSFeed.FetchAsync() raised an event on its completion and returned new (updated) RSSFeed instance through EventArgs? Later I would update corresponding item in collection from UI thread. – Martin May 09 '12 at 15:18
  • That's a possible solution. If we could see some code I could give you a more concrete answer. – Chris Shain May 09 '12 at 15:20
  • @malymato usually Async methods provide a callback when they complete. What type of implementation are you using? – Jake Berger May 09 '12 at 15:23
  • I'm thinking of event-based asynchronous pattern. – Martin May 09 '12 at 15:25
  • Or perhaps I could pass Dispatcher to my async method and then call BeginInvoke()? I will edit my post and add some code. – Martin May 09 '12 at 15:32
  • Your async update from the background thread will need to hand off all of the new `RSSFeed` instances to the UI thread for addition to the collection. I'd recommend trying to implement this using the `BackgroundWorker` class as @hbarck suggests below. – Chris Shain May 09 '12 at 17:31
3

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.

hbarck
  • 2,934
  • 13
  • 16
1

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

Community
  • 1
  • 1
tsells
  • 2,751
  • 1
  • 18
  • 20
1

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

djones
  • 2,338
  • 1
  • 20
  • 25
lichcat
  • 11
  • 2
1

this a simple observablecollection that notifies at the end of AddRange method, based on this post https://peteohanlon.wordpress.com/2008/10/22/bulk-loading-in-observablecollection/

it is also asynchronous and modifiable across threads, based on this post https://thomaslevesque.com/2009/04/17/wpf-binding-to-an-asynchronous-collection/

public class ConcurrentObservableCollection<T> : ObservableCollection<T>
{
    private SynchronizationContext _synchronizationContext = SynchronizationContext.Current;

    private bool _suppressNotification = false;

    public ConcurrentObservableCollection()
        : base()
    {
    }
    public ConcurrentObservableCollection(IEnumerable<T> list)
        : base(list)
    {
    }

    public void AddRange(IEnumerable<T> collection)
    {
        if (collection != null)
        {
            _suppressNotification = true;
            foreach (var item in collection)
            {
                this.Add(item);
            }
            _suppressNotification = false;

            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        }
    }
    public void RemoveRange(IEnumerable<T> collection)
    {
        if (collection != null)
        {
            _suppressNotification = true;
            foreach (var item in collection)
            {
                this.Remove(item);
            }
            _suppressNotification = false;

            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        }
    }

    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        if (SynchronizationContext.Current == _synchronizationContext)
        {
            // Execute the CollectionChanged event on the current thread
            RaiseCollectionChanged(e);
        }
        else
        {
            // Raises the CollectionChanged event on the creator thread
            _synchronizationContext.Send(RaiseCollectionChanged, e);
        }
    }
    protected override void OnPropertyChanged(PropertyChangedEventArgs e)
    {
        if (SynchronizationContext.Current == _synchronizationContext)
        {
            // Execute the PropertyChanged event on the current thread
            RaisePropertyChanged(e);
        }
        else
        {
            // Raises the PropertyChanged event on the creator thread
            _synchronizationContext.Send(RaisePropertyChanged, e);
        }
    }

    private void RaiseCollectionChanged(object param)
    {
        // We are in the creator thread, call the base implementation directly
        if (!_suppressNotification)
            base.OnCollectionChanged((NotifyCollectionChangedEventArgs)param);
    }
    private void RaisePropertyChanged(object param)
    {
        // We are in the creator thread, call the base implementation directly
        base.OnPropertyChanged((PropertyChangedEventArgs)param);
    }
}
jrivam
  • 1,081
  • 7
  • 12