【问题标题】:Asynchronous update to ObservableCollection items异步更新 ObservableCollection 项
【发布时间】:2012-05-09 15:04:18
【问题描述】:

我是多线程和 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
    }

}

谢谢。

【问题讨论】:

    标签: c# wpf asynchronous observablecollection


    【解决方案1】:

    使用 .Net 4.5,您可以使用 BindingOperations.EnableCollectionSynchronization 添加对后台线程更新到 ObservableCollection 的支持。这对 MVVM 非常有效。

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

    【讨论】:

      【解决方案2】:

      对于这种应用程序,我通常使用将 ReportsProgress 设置为 True 的 BackgroundWorker。然后,您可以为每个调用传递一个对象作为 ReportProgress 方法中的 userState 参数。 ProgressChanged 事件将在 UI 线程上运行,因此您可以将对象添加到事件处理程序中的 ObservableCollection。

      否则,从后台线程更新属性将起作用,但如果您正在过滤或排序 ObservableCollection,则除非引发某些集合更改通知事件,否则不会重新应用过滤器。

      您可以通过查找集合中项目的索引(例如,将其报告为进度百分比)并设置 list.item(i) = e.userstate 来重新应用过滤器和排序,即替换在 ProgressChanged 事件中单独列出。这样,任何绑定到该集合的控件的 SelectedItem 都将被保留,而过滤和排序将尊重项目中任何更改的值。

      【讨论】:

        【解决方案3】:

        如果您使用 WPF,您可以更新单个绑定项目的属性并从后台线程引发 PropertyChanged。 WPF 数据绑定机制(与 WinForms 等效的不同)检测到这一点并为您编组到 UI 线程。这当然是有代价的——使用自动机制,每个单独的属性更新都会导致一个编组事件,所以如果你要更改很多属性,性能可能会受到影响,你应该考虑将自己的 UI 编组为单个批处理操作.

        但是,您不能操作集合(添加/删除项目),因此如果您的 RSS 提要包含要绑定到的嵌套集合,您需要提前将整个更新提升到 UI 线程。

        【讨论】:

        • 谢谢。我的 RSSFeed 类中有一个嵌套集合。如果 RSSFeed.FetchAsync() 在完成时引发事件并通过 EventArgs 返回新的(更新的)RSSFeed 实例,这会是一个好的解决方案吗?稍后我会从 UI 线程更新集合中的相应项目。
        • 这是一个可能的解决方案。如果我们能看到一些代码,我可以给你一个更具体的答案。
        • @malymato 通常异步方法在完成时提供回调。您使用的是什么类型的实现?
        • 我在考虑基于事件的异步模式。
        • 或者我可以将 Dispatcher 传递给我的异步方法,然后调用 BeginInvoke()?我将编辑我的帖子并添加一些代码。
        【解决方案4】:

        您可能想查看 .Net 中的 ConcurrentCollections 命名空间。

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

        这是另一个可能也有帮助的问题。

        ObservableCollection and threading

        【讨论】:

          【解决方案5】:

          我有一个类似的场景,遇到了这个“ObservableCollection 不支持来自 UI 线程以外的线程的更新”,最后通过在 Thomas Levesque 的博客中引用这个 AsyncObservableCollection implement 解决了这个问题,我认为它可能会有所帮助给你。

          在其更新版本中,SynchronizationContext 用于解决此问题。可以参考MSDN page of SynchronizationContext

          【讨论】:

            【解决方案6】:

            这是一个简单的 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/

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

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2014-01-29
              • 2012-07-04
              • 2011-07-02
              • 2015-04-09
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多