3

I have up to 70000 Shape derived objects to add to a Canvas. Evidently this causes the UI to be unresponsive, and in my case for around 20 seconds. I have tried using the Dispatcher method to try to overcome this but I still have problems.

The first way I implemented it is as such:

foreach (var shape in ShapeList)
{
    Dispatcher.BeginInvoke(DispatcherPriority.Input, new Action(() =>
            {
                drawingCanvas.Children.Add(shape);
            }));
}

This does make the make the UI responsive but on the downside, it is taking forever (like 10 mins?) for it to completely add all the shapes. It is also immediately displaying the Shape as it is added.

The second way I implemented it is:

Dispatcher.BeginInvoke(DispatcherPriority.Input, new Action(() =>
            {
                foreach (var shape in ShapeList)
                {
                    drawingCanvas.Children.Add(shape);
                }
            }));

And well this seems to have the same effect as not using the dispatcher at all.

I've tried both implementations on a separate thread and there is no difference from the main thread. Also, I have to add them all as separate Shape objects because each of them have to support individual MouseClickEvents hence I'm not using a drawingVisual.

So is there any way to keep the main window responsive while still keeping the adding process relatively quick?

Thanks.

*Edit I am aware of a CanvasVirtualization but it seems a bit complex for me to understand at the moment. Is there any other methods to employ?

Enriel
  • 51
  • 3
  • 70,000 is a lot of stuff to put on a Canvas object. If these objects are read-only / non-interactive, would you consider using something like a WriteableBitmap instead? – McGarnagle Jul 23 '14 at 16:16
  • [Adding them on another thread](http://stackoverflow.com/questions/11358647/how-to-access-separate-thread-generated-wpf-ui-elements-from-the-dispatcher-thre/16963932#16963932) sounded like a good idea, I couldn't get it to work maybe you could. – ziya Jul 23 '14 at 19:29
  • I had the same problem. Your approach with Dispatcher is correct and the only way since you must create UI elements on the UI thread. I'm afraid that for so many objects you will have to use canvas virtualization. However, approach that worked for me was defining template for Canvas with ItemsControl and represent my shapes with view models which I put in a collection and bind to ItemsControl. Then I used templates to define appearance for the shapes. I don't know why but, that approach gave me better performance than adding items to Canvas.Children. I can post a code if you want to try it. – user1018735 Jul 23 '14 at 21:26
  • @McGarnagle: All the objects are interactive. They all can be selected and have a mousebuttondown event handler attached to them. Is it still possible to use a writeable bitmap in that case? – Enriel Jul 24 '14 at 02:12
  • I guess not, if they are interactive. And I guess I might be wrong, 70,000 isn't all *that* many ... but I still think you may end up having to do some kind of optimization like the virtual canvas ... – McGarnagle Jul 24 '14 at 17:46

2 Answers2

2

The two problems that you mention with using the Dispatcher are fairly easily solved or mitigated.

  1. drastic slowdown (20sec -> 10 mins), and
  2. shapes added piecemeal

For 1), you can speed up the process, while still retaining responsiveness, by adding the items in batches rather than one-at-a-time. For 2), a simple solution is just to hide the canvas, and show it when you're done drawing.

So something like this:

// group shapes into batches
const int batchSize = 100;
var batches = ShapeList.Select((item, index) => new { index = index, item = item })
    .GroupBy(item => item.index / batchSize)
    .ToArray();

// function to create a local closure for each batch
Action<int> addBatch = batchNumber => {
    var batch = batches[batchNumber];
    Dispatcher.BeginInvoke(DispatcherPriority.Input, new Action(() =>
    {
        foreach (var shape in batch)
        {
            drawingCanvas.Children.Add(shape.item);
        };
        if (batchNumber >= batches.Length - 1)
            drawingCanvas.Visibility = Visibility.Visible;
    }));
};

drawingCanvas.Visibility = Visibility.Collapsed;
for (int i=0 ; i < batches.Length ; i++)
{
    addBatch(i);
}
McGarnagle
  • 96,448
  • 30
  • 213
  • 255
  • I will definitely try this out. I did try to hide the canvas before but I used Visibility.Hidden. I never really knew what Visibility.Collapsed was until now. – Enriel Jul 25 '14 at 02:30
0

Ideally you should be using a virtualisation technique and only drawing what is actually currently visible rather than drawing all 70,000 items. There is a blog from MSDN which includes source code for a VirtualCanvas class which can be adapted to pretty much any use if you are using a Canvas already.

Be warned - the virtualisation has some specific conditions under which it can virtualise the items.

http://blogs.msdn.com/b/jgoldb/archive/2008/03/08/performant-virtualized-wpf-canvas.aspx

toadflakz
  • 7,314
  • 1
  • 21
  • 38