4

I have 250 objects which are visual which I need to add to a WrapPanel. When I do this the UI locks for a few seconds which is not what I want. Is there a way to stop this?

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

From my knowledge I can't create a new Task.Run() => ButtonClick to perform this on as the Task wont be able to access the UI.

Liz Ard
  • 43
  • 3
  • 2
    Did you materialize the `ListOfVisuals` or is that creating new elements as you access them? i.e. can you optimize the creation before adding them to your wrappanel? – default Jan 09 '15 at 13:09
  • 2
    You should profile to see what exactly is consuming all of that time. 250 objects doesn't seem like all that much, I'm surprised its taking as long as a few seconds. I suspect that either enumerating `ListOfVisuals` is relatively time consuming, or something about your layout (e.g. a data binding or a large number of layout calculations) is causing the slowdown. – Justin Jan 09 '15 at 13:12

3 Answers3

2

Problem is more-likely because each Add call will cause re-layouting and redrawing.

In winforms solution is to use SuspendLayout/ResumeLayout when adding multiple controls and, typically, enable double-buffering to avoid flickering.

You can search for how it is done in wpf (to example, here is something). But I'd suggest you to simply hide (Visibility.Collapsed) container until you finish adding 250 controls. It would be cool to display some progress indicator (vector visuals animation or a simple gif), like

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

Community
  • 1
  • 1
Sinatr
  • 20,892
  • 15
  • 90
  • 319
  • Nope. Collapse will cause a redraw. – Gusdor Jan 12 '15 at 09:00
  • @Gusdor, yep, one redraw. Instead of 250. Isn't that beautiful? Or you mean something else? – Sinatr Jan 12 '15 at 09:06
  • the cost of that one redraw depends on your scene graph. A large implicitly sized graph will be a expensive to resize. I threw in an answer with my thoughts on the solution. – Gusdor Jan 12 '15 at 09:10
  • It doesn't matter what is on screen, but you need at least **one redraw** (following layoting of course). It's not possible to prevent it. You mentioning *resizing*, but I don't see where OP mentioning it. He has problem with initial displaying. For resizing of complicated layout different solution should be applied (using some sort of virtualizing). Regarding your solution: databinding is preferred in wpf, so it's better, but for existing code-behind code hiding container is cheap and should do the trick (emphasis on *should*, I don't really bother to test it =P). – Sinatr Jan 12 '15 at 09:15
  • Let me get this straight, you are telling me that for WPF performance it doesn't matter what is onscreen, it _should_ work, but you haven't tested it. I suggest you run that test. Ill go so far as to brand this solution a **hack**. Sorry :( – Gusdor Jan 12 '15 at 09:36
  • @Gusdor, but why? I am quite confident in my conclusions =). It doesn't makes sense to render/layout invisible. You have doubts - you test =P. – Sinatr Jan 12 '15 at 09:39
2

The actual issue here is that a change notification is being raised for each item you add.

You need to databind WrapPAnel.Children to an INotifyCollectionChanged instance that implements an AddRange. The goal should be that the propery changed event is only raise once for the entire collection.

The obvious answer is, of course, just write a method which iterates over the input collection and calls Add for each. But this really isn’t the answer, because the question really isn’t about AddRange – the question is really about raising a single CollectionChanged event when adding multiple items

How do you implement this?

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

    }

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

But wait, you cannot bind Wrappanel like that! You will need to use an ItemsControl whose ItemsPanelTemplate is set to be a WrapPanel. See this example 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> 
Gusdor
  • 14,001
  • 2
  • 52
  • 64
0

Try this :

this.Dispatcher.BeginInvoke(new Action(delegate
{
    foreach(Visual item in ListOfVisuals)
    {
        WrapPAnel.Children.Add(item);
    }
}), DispatcherPriority.Background);
Amol Bavannavar
  • 2,062
  • 2
  • 15
  • 36