4

我和我的团队正在开发一个显示多个并发 XamDataChart 控件的 WPF 应用程序(由 Infragistics 提供)。每个图表都绑定到一个不同的 ObservableCollection,最多可以包含 200 万个点。对于每个图表,DispatcherTimer 会定期检索要附加到集合的新项目(每 100 毫秒最多 1000 个)。每次新项目出现时,它们都会被添加到集合的“尾部”中,并且从“头部”中移除相同的数量,因此集合中的项目数量随着时间的推移保持不变。

我们面临的问题是添加/删除操作冻结了 GUI,因为集合只能由主线程修改。我们尝试了许多方法(BackgroundWorker、Application.Current.Dispatcher 和 DispatcherPriority.Background、Task.Factory 等),但似乎都没有解决问题,并且 GUI 一直冻结。

您能否就处理大量绑定数据同时保持 GUI 响应的最佳方法向我们提供建议?

更新

1) 正如我在下面的评论中所指出的,我们已经尝试在抑制 OnCollectionChanged 的​​同时添加和删除项目。即使它似乎对少量数据有效果,但在我们的场景中,这种解决方案的优势实际上是无法观察到的。

2) 数据在单独的线程中准备和收集。这是一个长时间运行的操作,但没有明显的缓慢或无响应。当数据传递到图表组件进行渲染时,应用程序会冻结。

3) 以下是生成数据(在单独的线程中)并在 UI 上显示数据的方法:

private void GenerateDataButtonClick(object sender, RoutedEventArgs e)
{
   Task<List<RealTimeDataPoint>> task = Task.Factory.StartNew(() =>           this.RealTimeDataPointGenerator.GenerateData(2000000));
   Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() =>  {                                                                                                    this.DataPoints.Clear();
                                                                                            this.DataPoints.AddRange(task.Result);
                                                                                                 if (!this.StartDataFeedButton.IsEnabled)
                                                                                                     this.StartDataFeedButton.IsEnabled = true;
                                                                                             }));
}

public void DispatcherTimerTick(object sender, EventArgs e)
{
   Task<List<RealTimeDataPoint>> task = Task.Factory.StartNew(() => this.RealTimeDataPointGenerator.GenerateData(1000));
   Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() => {                                                                                                  this.DataPoints.RemoveRange(0, task.Result.Count);                                                                                                  this.DataPoints.AddRange(task.Result);                                                                                              }));
}

提前致谢, 詹卢卡

4

2 回答 2

1

如果不了解您的场景,很难知道正确的建议,所以我建议您在 Infragistics 论坛上发布一个示例并寻求帮助,如果您还没有这样做,如果您有,请回复链接到帖子,我去看看。

如果您将许多点作为单独的操作一次更新,那么让您的集合只引发一个事件而不是每个单独的事件会很有帮助。例如,如果您通过重复调用 Add 来更新集合的全部内容,最好发送一个 Reset 事件,而不是发送所有单个事件。但似乎您已经在调用 AddRange,我相信它只会向图表发送一个通知。

如果您只有两个系列并且每 100 毫秒只更新一次,我认为这不会导致 UI 冻结,但是如果您有许多单独的系列,您正在通过单独的调度程序交互单独更新数据,您实际上会导致图表的更新次数可能超出了您的预期。

该图表将批量修改并限制刷新次数,但它使用调度程序执行此操作,因此如果您有 15 个不同的系列,您在不同的时间间隔更新,并且所有这些都是单独的调度程序 thunk,那么您将导致更多与通过在同一个调度程序 thunk 中更新多个系列数据源来限制更新次数相比,更新图表。

此外,如果您在上面的代码中使用 CategoryDateTimeXAxis,您可能会遇到它当前在修改时对日期列进行排序的限制,这将扼杀您在该规模上的表现。在这种情况下,我建议为该轴类型提交功能请求以支持预先排序的数据。

如果您的数据项支持 INotifyPropertyChanged,但您没有使用它来通知图表值更改,那么使用不实现 INotifyPropertyChanged 的​​项类型会好得多。如果您提交实现此接口的项目,图表假定它需要订阅此接口才能收到更改通知(您可能永远不会打算进行更改)。这听起来可能不是问题,但如果您有 200 万条记录在高频更新,那么就会无缘无故地进行大量事件订阅。

据我所知,该图表从属性绑定中检索值比字符串索引器快得多,因此请确保它是一个简单的属性,而不是 MemberPath 中的点属性路径。

希望这些信息对您有用。但是使用可运行的样本来诊断问题会容易得多,该样本提供了可能导致问题的所有上下文。

于 2012-07-24T13:49:03.730 回答
0

我也遇到过这个问题,我获取数据的代码通常需要一段时间,并且尽管使用DispatcherPriority.Background,它会在加载时冻结 UI,而且我无法使用后台线程,因为 WPF 无法修改对象在没有创建对象的线程上。

我最终做的实际上是使用两种方法:一个后台线程去获取数据,以及Dispatcher将项目添加到ObservableCollectionatDispatcherPriority.Background优先级。

我的代码通常如下所示:

Task<List<MyClass>> task = Task.Factory.StartNew(() => GetData());
App.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background, 
    new Action(delegate() 
    {
        MyObservableCollection.AddRange(task.Result);
    }));

我过去使用的另一个选项是await/来自Async CTP Refreshasync的关键字

async void SomeMethod()
{

    Task<List<MyClass>> task = Task.Factory.StartNew(() => GetData());
    MyObservableCollection.AddRange(await task);
}

注意:该AddRange()方法是扩展的自定义类的一部分ObservableCollection,尽管您也可以将其构建为扩展方法。

public void AddRange(IEnumerable<T> collection)
{
    foreach (var i in collection)
        Items.Add(i);

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

您可以为添加的CollectionChanged每个项目添加一个调用,而不是最后的单个 Reset 调用,但这可能会导致您添加的数据量出现性能问题。

于 2012-07-23T15:31:22.877 回答