如何防止在向UI添加内容时UI被锁定

4

我有250个需要添加到WrapPanel中的可视化对象。当我这样做时,UI会锁定几秒钟,这不是我想要的。有没有办法阻止这种情况发生?

private void ButtonClick()
{
   Foreach(Visual item in ListOfVisuals)
   {
      WrapPAnel.Children.Add(item);
   }
}

据我所知,我无法创建一个新的 Task.Run() => ButtonClick 来执行此操作,因为任务将无法访问用户界面。


2
你是否已经实现了 ListOfVisuals,还是在访问它们时创建了新元素?也就是说,在将它们添加到你的 wrappanel 之前,你能否优化它们的创建? - default
2
你应该进行性能分析,以确定到底是什么在消耗所有时间。250个对象似乎并不算太多,我很惊讶它需要几秒钟的时间。我怀疑枚举 ListOfVisuals 要相对耗费一些时间,或者你的布局中有一些问题(例如数据绑定或大量的布局计算)导致了减速。 - Justin
3个回答

2
实际问题在于每添加一个项目都会引发一次更改通知。
你需要将WrapPanel.Children数据绑定到实现了AddRangeINotifyCollectionChanged实例。目标应该是仅为整个集合引发一次属性更改事件。

显而易见的答案当然是编写一个方法,遍历输入集合并为每个调用Add。但这并不是答案,因为问题实际上并不是关于AddRange - 问题实际上是关于在添加多个项时引发单个CollectionChanged事件。

你如何实现这个?
    public void AddRange(IEnumerable<T> dataToAdd)

    {

        this.CheckReentrancy();



        //

        // We need the starting index later

        //

        int startingIndex = this.Count;



        //

        // Add the items directly to the inner collection



        //

        foreach (var data in dataToAdd)

        {

            this.Items.Add(data);

        }



        //

        // Now raise the changed events

        //

        this.OnPropertyChanged("Count");

        this.OnPropertyChanged("Item[]");



        //

        // We have to change our input of new items into an IList since that is what the

        // event args require.

        //

        var changedItems = new List<T>(dataToAdd);

        this.OnCollectionChanged(changedItems, startingIndex);

    }

来源: http://blogs.msdn.com/b/nathannesbit/archive/2009/04/20/addrange-and-observablecollection.aspx

但是等等,你不能这样绑定Wrappanel!你需要使用一个ItemsControl,将它的ItemsPanelTemplate设置为一个WrapPanel。参见这个示例:http://tech.pro/tutorial/830/wpf-tutorial-using-an-itemspanel

<ItemsControl>
  <ItemsControl.ItemsPanel>
    <ItemsPanelTemplate>
      <WrapPanel/>
    </ItemsPanelTemplate>
  </ItemsControl.ItemsPanel>
  <Image Source="Images\Aquarium.jpg" Width="100"/>
  <Image Source="Images\Ascent.jpg" Width="50"/>
  <Image Source="Images\Autumn.jpg" Width="200"/>
  <Image Source="Images\Crystal.jpg" Width="75"/>
  <Image Source="Images\DaVinci.jpg" Width="125"/>
  <Image Source="Images\Follow.jpg" Width="100"/>
  <Image Source="Images\Friend.jpg" Width="50"/>
  <Image Source="Images\Home.jpg" Width="150"/>
  <Image Source="Images\Moon flower.jpg" Width="100"/>
</ItemsControl> 

2
问题很可能是每次调用Add都会导致重新布局和重绘。
winforms中,解决方案是在添加多个控件时使用SuspendLayout/ResumeLayout,通常启用双缓冲以避免闪烁。
你可以搜索一下在wpf中如何实现(例如,here),但我建议你在添加完250个控件之前将容器隐藏(Visibility.Collapsed)。最好显示一些进度指示器(向量视觉动画或简单的gif),比如:

http://www.sponlytix.com/Images/Others/Loading.gif


不行。折叠会导致重绘。 - Gusdor
@Gusdor,没错,只需要重绘一次。不是很美妙吗?或者你指的是其他什么? - Sinatr
那个重新绘制的成本取决于你的场景图。一个大的隐式大小的图形将会很昂贵地调整大小。我在答案中加入了我的解决思路。 - Gusdor
屏幕上显示的内容并不重要,但是你至少需要进行一次重绘(当然是在布局之后)。这是无法避免的。你提到了调整大小,但我没有看到OP提到它。他的问题是初始显示问题。对于复杂布局的调整大小,应该采用不同的解决方案(使用某种虚拟化)。关于你的解决方案:在wpf中,数据绑定是首选,所以更好,但对于现有的代码隐藏容器是便宜的,应该可以解决问题(强调应该,我真的不打算测试它=P)。 - Sinatr
让我明确一下,你是在告诉我对于 WPF 性能来说屏幕上显示的内容并不重要,它应该可以正常工作,但你还没有测试过。我建议你运行一下测试。我甚至会把这个解决方案称为“hack”。很抱歉 :( - Gusdor
@Gusdor,但为什么?我对我的结论非常有信心=)。将渲染/布局设置为不可见是没有意义的。你有疑问-你就测试=P。 - Sinatr

0

试试这个:

this.Dispatcher.BeginInvoke(new Action(delegate
{
    foreach(Visual item in ListOfVisuals)
    {
        WrapPAnel.Children.Add(item);
    }
}), DispatcherPriority.Background);

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