ObservableCollection项目的异步更新

7

我刚开始接触多线程和WPF。

我有一个ObservableCollection<RSSFeed>,在应用程序启动时,从UI线程添加项目到这个集合中。RSSFeed的属性与WPF ListView绑定。稍后,我想异步更新每个RSSFeed。因此,我考虑实现类似于RSSFeed.FetchAsync()的东西,并在其更新的属性上引发PropertyChanged事件。

我知道ObservableCollection不支持来自除UI线程之外的线程的更新,会抛出NotSupportedException异常。但是由于我不是操作ObservableCollection本身而是更新其项目的属性,所以我可以期望它起作用并看到ListView项更新吗?或者由于PropertyChanged而仍然抛出异常?

编辑:代码

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
    }

}

谢谢。
6个回答

5

3
如果你正在使用WPF,你可以在后台线程更新单个绑定项的属性并引发PropertyChanged事件。与WinForms不同,WPF数据绑定机制可以检测到这一点,并为你调度到UI线程。当然,自动机制会有成本-每次单独属性更新将导致一个调度事件,因此如果你正在改变大量属性,则性能可能会受到影响,你应该考虑将UI调度作为单个批处理操作执行。
但是,你不能操纵集合(添加/删除项目)。因此,如果你的RSS提要包含嵌套集合并希望将其绑定到UI,则需要预先将整个更新提升到UI线程。

谢谢。我的RSSFeed类中有一个嵌套的集合。如果RSSFeed.FetchAsync()在完成时引发事件并通过EventArgs返回新的(更新的)RSSFeed实例,这是否是一个好的解决方案?稍后我将从UI线程更新集合中对应的项目。 - Martin
那是一个可能的解决方案。如果我们能看到一些代码,我可以给你一个更具体的答案。 - Chris Shain
@malymato 通常异步方法在完成时会提供一个回调函数。你使用的是哪种实现方式? - Jake Berger
我在考虑基于事件的异步模式。 - Martin
或许我可以将Dispatcher传递给我的异步方法,然后调用BeginInvoke()?我会编辑我的帖子并添加一些代码。 - Martin
您的后台线程异步更新将需要将所有新的RSSFeed实例移交给UI线程以添加到集合中。我建议尝试使用BackgroundWorker类来实现,就像@hbarck在下面建议的那样。 - Chris Shain

3
对于这种应用程序,我通常使用带有ReportsProgress设置为True的BackgroundWorker。然后,您可以在ReportProgress方法中将一个对象作为userState参数传递给每个调用。ProgressChanged事件将在UI线程上运行,因此您可以在事件处理程序中将该对象添加到ObservableCollection中。
否则,从后台线程更新属性将起作用,但是如果您正在过滤或排序ObservableCollection,则除非已引发一些集合更改通知事件,否则过滤器将不会重新应用。
您可以通过查找集合中项的索引(例如通过报告它作为progresspercentage)并将list.item(i) = e.userstate设置为自己来导致过滤器和排序被重新应用。即在ProgressChanged事件中替换列表中的项。这样,任何绑定到集合的控件的SelectedItem都将被保留,同时过滤和排序将尊重项目中任何已更改的值。

1

1
我曾经遇到过类似的情况,遇到了“ObservableCollection不支持来自UI线程以外的线程的更新”的问题。最终,通过参考Thomas Levesque博客中的AsyncObservableCollection implement得以解决,我认为这可能对您有所帮助。
在其更新版本中,使用SynchronizationContext来解决此问题。您可以参考SynchronizationContext的MSDN页面

1
这是一个简单的ObservableCollection,它在AddRange方法结束时发出通知,基于此文章:https://peteohanlon.wordpress.com/2008/10/22/bulk-loading-in-observablecollection/
此外,它也是异步的,并且可以跨线程进行修改,基于此文章:https://thomaslevesque.com/2009/04/17/wpf-binding-to-an-asynchronous-collection/。请注意保留HTML标签。
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);
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接