Kael Rowan

Foundations of Elegant Code

ZoomableApplication2: A Million Items

ZoomableApplication2: A Million Items

Rate This
  • Comments 17

The original sample for Chris Lovett’s VirtualCanvas showed a random set of shapes on a canvas.  You could use a menu to choose whether to have 5, 500, 12500, or 50000 shapes “on the canvas”.  I use quotation marks because the point of the sample was to show that with UI virtualization you could have 50,000 items in your logical data model while only creating actual UIElements for those that were visible on the screen.  That made it possible to scroll back and forth blazingly fast (when zoomed in) since only a couple hundred UIElements were instantiated at any given time.

There are a few things to keep in mind with that original sample.  First, in order to get this UI virtualization, your data items were responsible for creating and disposing their own UIElements and visual trees directly from their code-behind, instead of the usual practice of defining <DataTemplate>s somewhere else in XAML.  Second, items had to be added to the VirtualCanvas directly, instead of being able to use data-binding with an ItemsControl like a <ListBox>.  Third, all data items had to be loaded and remain in RAM even when their respective UIElements are not.  50,000 classes or structs by themselves might not be a big deal, but if it takes over a millisecond to load each item then you’re talking about a very significant startup time.  Fourth, if the user zoomed all the way out then all of the UIElements were created at once which would cause the app to hang or simply crash altogether.

For the second post in the ZoomableCanvas series I’m going to recreate the original random shapes sample, but this time I’m going to use <DataTemplate>s and a <ListBox> so that we get things like selection, scrolling, and keyboard navigation for free.  I’m also going to show you how UI and data virtualization work, and how you can zoom all the way out and still have a responsive app even with 1,000,000 items.

I’m going to start by creating a brand new app called ZoomableApplication2 and adding a reference to the ZoomableCanvas.dll class library that I built for ZoomableApplication1.  Then I’m going to replace the default <Grid> with a <ListBox> and set the <ListBox.ItemsPanel> to a <ZoomableCanvas>, like this:

<Window x:Class="ZoomableApplication2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <ListBox x:Name="MyListBox">
        <ListBox.ItemsPanel>
            <ItemsPanelTemplate>
                <ZoomableCanvas/>
            </ItemsPanelTemplate>
        </ListBox.ItemsPanel>
    </ListBox>
</Window>

Now lets add a bunch of items to the <ListBox>.  Each of our items will have a top, left, width, and height that places them in a grid-like pattern, kind of like the original VirtualCanvas sample.  We’ll also give them some random control points for the curves and random colors to make them look pretty.  And since we’re going to be using {Binding}s when we write our XAML, our data items don’t even need to be strongly typed. They can just be anonymous bags of properties:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        DataItems = new ObservableCollection<object>();
        MyListBox.ItemsSource = DataItems;

        PopulateDataItems(100);
    }

    private ObservableCollection<object> DataItems;

    private void PopulateDataItems(int newCount)
    {
        // Remove items if our new count is less than our current count.
        while (newCount < DataItems.Count)
        {
            DataItems.RemoveAt(DataItems.Count - 1);
        }
        // Add items if our new count is more than our current count.
        while (newCount > DataItems.Count)
        {
            // Give each item a deterministic seed.
            var i = DataItems.Count;
            var rand = new Random(i);
            var sqrt = (int)Math.Sqrt(i);

            // With a random width and height between 50 and 100.
            var width = rand.Next(50, 100);
            var height = rand.Next(50, 100);

            // And place it at a random position in a grid-like pattern.
            var top = Math.Min(sqrt, i - Math.Pow(sqrt, 2))
                      * 100 + rand.Next(100 - height);
            var left = Math.Min(sqrt, sqrt * 2 - (i - Math.Pow(sqrt, 2)))
                       * 100 + rand.Next(100 - width);

            // Randomly generate the outline of the item.
            var type = rand.Next(3);
            var data = type == 0 ? "ellipse" :
                       type == 1 ? "rectangle" :
                       String.Format("M{0},{1} C{2},{3} {4},{5} {6},{7}",
                           rand.NextDouble(),
                           rand.NextDouble(),
                           rand.NextDouble(),
                           rand.NextDouble(),
                           rand.NextDouble(),
                           rand.NextDouble(),
                           rand.NextDouble(),
                           rand.NextDouble());

            // Give it random gradient colors.
            var brush = new LinearGradientBrush(
                            Color.FromScRgb(1f, (float)rand.NextDouble(),
                                                (float)rand.NextDouble(), 
                                                (float)rand.NextDouble()),
                            Color.FromScRgb(1f, (float)rand.NextDouble(),
                                                (float)rand.NextDouble(),
                                                (float)rand.NextDouble()),
                            180 * rand.NextDouble());

            // Add it to the pool of data items.
            DataItems.Add(new { top, left, width, height, data, brush, i });
        }
    }
}

Now lets define the <Style> for our ListBoxItems and the <DataTemplate>s for the three different data “types”.  The first part of the <Style> will be hooking up the Canvas.Top, Canvas.Left, Width, and Height properties for each item, as well as setting the content alignment to Stretch (since by default ListBoxItems left-justify their content).  I’ll also give each ListBoxItem some Padding so that the blue selection highlight is clearly visible around an item when it’s selected.  The second part of the <Style> will be defining our <DataTemplate>s based on the value of the data property.  Since in this case our data already represents simple shapes, our templates are going to be pretty straightforward:

<ListBox.ItemContainerStyle>
    <Style TargetType="ListBoxItem">

        <Setter Property="Canvas.Top" Value="{Binding top}"/>
        <Setter Property="Canvas.Left" Value="{Binding left}"/>
        <Setter Property="Width" Value="{Binding width}"/>
        <Setter Property="Height" Value="{Binding height}"/>
        
        <Setter Property="VerticalContentAlignment" Value="Stretch"/>
        <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
        <Setter Property="Padding" Value="5"/>
        
        <Setter Property="ContentTemplate">
            <Setter.Value>
                <DataTemplate>
                    <Grid>
                        <Path Data="{Binding data}"
                              Stroke="{Binding brush}"
                              StrokeThickness="2"
                              Stretch="Fill"/>
                        <TextBlock Text="{Binding i}"
                                   HorizontalAlignment="Center"
                                   VerticalAlignment="Center"/>
                    </Grid>
                </DataTemplate>
            </Setter.Value>
        </Setter>
        
        <Style.Triggers>
            <DataTrigger Binding="{Binding data}" Value="rectangle">
                <Setter Property="ContentTemplate">
                    <Setter.Value>
                        <DataTemplate>
                            <Grid>
                                <Rectangle Fill="{Binding brush}"
                                           RadiusX="10"
                                           RadiusY="10"/>
                                <TextBlock Text="{Binding i}"
                                           HorizontalAlignment="Center"
                                           VerticalAlignment="Center"/>
                            </Grid>
                        </DataTemplate>
                    </Setter.Value>
                </Setter>
            </DataTrigger>

            <DataTrigger Binding="{Binding data}" Value="ellipse">
                <Setter Property="ContentTemplate">
                    <Setter.Value>
                        <DataTemplate>
                            <Grid>
                                <Ellipse Fill="{Binding brush}"/>
                                <TextBlock Text="{Binding i}"
                                           HorizontalAlignment="Center"
                                           VerticalAlignment="Center"/>
                            </Grid>
                        </DataTemplate>
                    </Setter.Value>
                </Setter>
            </DataTrigger>
            
        </Style.Triggers>
    </Style>
</ListBox.ItemContainerStyle>

And if you run the app with just the code I’ve shown you so far, you already get a pretty cool little canvas application showing 100 random shapes with selection, scroll bars, and keyboard navigation:


(I’ve embedded the sample as an XBAP in an <iframe> above this line, so you should see a live sample and be able to interact with it if you’re using Internet Explorer and have .NET 4.0 installed.)

You may have noticed that if you select an item with the mouse and keep holding down the mouse button while moving the mouse off the canvas, it will start to pan automatically, even though we haven’t hooked up any zooming or panning code yet!  It’s actually the <ListBox> that’s doing this, since it knows how to talk to the IScrollInfo interface that the ZoomableCanvas implements internally.  When you are selecting items by holding the mouse and dragging, the <ListBox> automatically tells the ZoomableCanvas to scroll to the next item.  ZoomableCanvas then does this by setting its Offset property accordingly, which results in the automatic “pan”.

Unfortunately the built-in <ListBox> “panning” is a little clunky, since it does it on an item-by-item basis instead of a pixel-by-pixel basis.  You can of course use the scroll bars instead, but that’s not as cool as dragging the canvas to pan, and there’s no way to zoom.  So lets hook up our own mouse handlers just like we did in ZoomableApplication1, but this time we’ll get even fancier by zooming in and out “around the mouse” instead of the top-left.

If you remember from ZoomableApplication1, we were able to set the Scale and Offset of the ZoomableCanvas by setting its x:Name="MyCanvas" in the XAML.  Unfortunately this feature doesn’t work in WPF when dealing with <ItemsPanelTemplate>s, and the ItemsHost property on ListBox is internal.  So we’ll either have to search down the visual tree to find the canvas, or simply assign it to a local variable when it’s created.  I think the latter is cleaner, so lets add a ZoomableCanvas_Loaded handler and capture it at that point.  And while we’re capturing things, lets capture the mouse this time too:

<ListBox.ItemsPanel>
    <ItemsPanelTemplate>
        <ZoomableCanvas Loaded="ZoomableCanvas_Loaded"/>
    </ItemsPanelTemplate>
</ListBox.ItemsPanel>
ZoomableCanvas MyCanvas;
Point LastMousePosition;

private void ZoomableCanvas_Loaded(object sender, RoutedEventArgs e)
{
    // Store the canvas in a local variable since x:Name doesn't work.
    MyCanvas = (ZoomableCanvas)sender;
}

protected override void OnPreviewMouseMove(MouseEventArgs e)
{
    var position = e.GetPosition(MyListBox);
    if (e.LeftButton == MouseButtonState.Pressed
        && !(e.OriginalSource is Thumb)) // Don't block the scrollbars.
    {
        CaptureMouse();
        MyCanvas.Offset -= position - LastMousePosition;
        e.Handled = true;
    }
    else
    {
        ReleaseMouseCapture();
    }
    LastMousePosition = position;
}

protected override void OnPreviewMouseWheel(MouseWheelEventArgs e)
{
    var x = Math.Pow(2, e.Delta / 3.0 / Mouse.MouseWheelDeltaForOneLine);
    MyCanvas.Scale *= x;

    // Adjust the offset to make the point under the mouse stay still.
    var position = (Vector)e.GetPosition(MyListBox);
    MyCanvas.Offset = (Point)((Vector)
        (MyCanvas.Offset + position) * x - position);

    e.Handled = true;
}

Before I embed another sample, let’s do one more thing.  Instead of having a fixed set of 100 shapes, let’s dock a <Slider> to the top that lets us increase and decrease the number of shapes at will.  We’ll remove the call to PopulateDataItems(100) that we initially had above, and we’ll call it from the MySlider_ValueChanged instead:

<DockPanel>
    <Slider x:Name="MySlider" DockPanel.Dock="Top"
            Maximum="10000" AutoToolTipPlacement="BottomRight"
            ValueChanged="MySlider_ValueChanged"/>

    <ListBox x:Name="MyListBox">

        . . .

    </ListBox>
</DockPanel>
private void MySlider_ValueChanged(object sender, EventArgs e)
{
    PopulateDataItems((int)MySlider.Value);
}

The thing to notice here (other than the fun panning and zooming) is that no matter how many shapes you create, you can scroll back and forth extremely smoothly as long as your are zoomed in enough so that only a few hundred shapes are on your screen at a time.  This is because of the built-in UI virtualization in ZoomableCanvas.  

It’s nice that we get virtualization for free without doing any extra work, but you probably noticed it took a really long time to get that slider all the way over to the right.  This is because the ZoomableCanvas had to create and measure each of those shapes (albeit just once) in order to determine where they would end up on the canvas.  We can skip this step by having our ItemsSource implement ZoomableCanvas.ISpatialItemsSource.  This will cause the ZoomableCanvas to ask our ItemsSource which items are in view instead of the ZoomableCanvas keeping track of them internally, which means the ZoomableCanvas doesn’t need to know where all the shapes are up front.  It’s pretty easy to implement ISpatialItemsSource; you just need to know how to figure out the index of the items that intersect a given Rect, and you need to know the overall bounding box (extent) of all the items in your set.  Since all of our items are in a grid-like pattern, it’s very quick to determine both of these things.  I’m just going to create a new class that derives from ObservableCollection<object> (since that’s what we were using before), and implement ISpatialItemsSource by using some grid-like math:

public class GridLikeItemsSource : 
    ObservableCollection<object>,
    ZoomableCanvas.ISpatialItemsSource
{
    public Rect Extent
    {
        get
        {
            var sqrt = Math.Sqrt(Count);
            return new Rect(0, 0, 
                100 * (Math.Ceiling(sqrt)), 
                100 * (Math.Round(sqrt)));
        }
    }

    public IEnumerable<int> Query(Rect rectangle)
    {
        rectangle.Intersect(Extent);

        var top = Math.Floor(rectangle.Top / 100);
        var left = Math.Floor(rectangle.Left / 100);
        var right = Math.Ceiling(rectangle.Right / 100);
        var bottom = Math.Ceiling(rectangle.Bottom / 100);

        for (var x = Math.Max(left, 0); x <= right; x++)
        {
            for (var y = Math.Max(top, 0); y <= bottom; y++)
            {
                var i = x > y ?
                    Math.Pow(x, 2) + y :
                    Math.Pow(y, 2) + 2 * y - x;
                if (i < Count)
                {
                    yield return (int)i;
                }
            }
        }
    }

    public event EventHandler ExtentChanged;

    public event EventHandler QueryInvalidated;
}

I’ll also change the corresponding type in MainWindow, but I’ll leave everything else the same.

public MainWindow()
{
    InitializeComponent();

    DataItems = new GridLikeItemsSource();
    MyListBox.ItemsSource = DataItems;
}

private GridLikeItemsSource DataItems;

Here is the sample again, but you should be able to feel a big difference when sliding the slider across:

Moving the slider all the way to the right is still not instantaneous, but that’s because it still takes a little bit of time to create each data item and add it to the ObservableCollection.  It’s also not free since each item takes a little bit of RAM as well.  This is fine for 10,000 items, but what about 100,000?  Or a million?  Or a billion?  What if I wanted to show a node-link diagram for every web page on the internet?  There’s over 150 billion web pages according to www.archive.org, and who knows how many links!  You wouldn’t even be able to fit all of that data into RAM!

With data virtualization, you can have a canvas with as many items as you want.  Well, ok, that’s not entirely true.  You can actually only have up to 2,147,483,647 items per canvas, because that’s the upper limit of the int indexer on IList.  The key to data virtualization is that when you implement ISpatialItemsSource, your list is never actually enumerated.  The only thing that is called is ISpatialItemsSource.Query (and Extent), so your list is only asked for the items that are actually on the screen.  So instead of deriving from ObservableCollection and adding items to it when the slider value changes, let’s implement a custom IList that fetches items on-demand.

public class GridLikeItemsSource : 
    IList,
    ZoomableCanvas.ISpatialItemsSource
{
    public Rect Extent
    {
        . . . (same code as above) . . .
    }

    public IEnumerable<int> Query(Rect rectangle)
    {
        . . . (same code as above) . . .
    }

    public event EventHandler ExtentChanged;

    public event EventHandler QueryInvalidated;

    private int count;
    
    public int Count
    {
        get
        {
            return count;
        }
        set
        {
            count = value;
            ExtentChanged(this, EventArgs.Empty);
            QueryInvalidated(this, EventArgs.Empty);
        }
    }

    public object this[int i]
    {
        get
        {
            // Give the item a deterministic seed.
            var rand = new Random(i);
            var sqrt = (int)Math.Sqrt(i);

            // With a random width and height between 50 and 100.

            . . . (same code as PopulateDataItems above) . . .

            return new { top, left, width, height, data, brush, i };
        }
        set
        {
        }
    }

    #region Irrelevant IList Members . . .
}

Notice that this time I’m actually raising the ExtentChanged and QueryInvalidated events.  This wasn’t necessary before because ObservableCollection already raises enough events to broadcast to the world that everything has changed.  But now that we’re trimmed down, we raise ExtentChanged to let the ZoomableCanvas know to update its scrollbar information, and we raise QueryInvalidated to let the ZoomableCanvas know to call Query again.  If we wanted to be totally optimized then we would only call QueryInvalidated when we know that another call to Query with the same Rect would give different results.  But our Query method runs practically instantaneously so it doesn’t really matter if it’s called tons of times.

Now lets update our <Slider> to have a million items and replace the PopulateDataItems code with a simple setter to DataItems.Count:

<Slider x:Name="MySlider" DockPanel.Dock="Top"
        Maximum="1000000" AutoToolTipPlacement="BottomRight"
        ValueChanged="MySlider_ValueChanged"/>
private void MySlider_ValueChanged(object sender, EventArgs e)
{
    DataItems.Count = (int)MySlider.Value;
}

One more thing before we run the app.  I know that you’re going to slide the slider all the way to the right (which will be instantaneous with our data-virtualized IList), but then the first thing you’re going to do after that is zoom out!  But we haven’t done anything to prevent WPF from trying to show all 1,000,000 shapes, so the app will hang and/or crash just like every other VirtualCanvas sample out there, and my blog will probably crash and burn right along with it.  So before I let you unleash your mouse wheel on the next sample, I’m going to put in some safeguards.

WPF can only handle between about 1,000 to 10,000 primitives on the screen while still remaining responsive, depending on your CPU and graphics card.  10,000 primitives isn’t a lot when you realize that even our simple shapes use 5 primitives each (you can use Snoop to see why).  ZoomableCanvas doesn’t have any magic to get around that, but it does have convenient ways to limit the number of elements on the screen, and ways to control the time it takes to create them.  The first property, RealizationLimit, is the maximum number of elements that ZoomableCanvas will try to maintain on the screen at any given time.  The second property, RealizationRate, is the number of elements that ZoomableCanvas will create and/or dispose in a single “batch”.  This is really only useful when you combine it with the third property, RealizationPriority, which controls when these batches take place.  By default the RealizationPriority is DispatcherPriority.Normal, which basically means it performs the batches right away even if it means hanging the UI.

For our particular sample, I’m going to set RealizationLimit to 1000, RealizationRate to 10, and RealizationPriority to Background.  This means that the ZoomableCanvas will try to have a maximum of 1000 elements on the screen at a time (5,000 primitives in our case), it will create and dispose 10 of them at a time until it gets to that number, and it will process input events like the mouse and keyboard before each batch.  This will let the app remain responsive and allow you to pan and zoom way out, with the tradeoff being that you’ll see a bit of a “flicker” while the shapes are being created and disposed and you’ll see big gaps between the 1,000 items on the screen.

Here is the code:

<ListBox.ItemsPanel>
    <ItemsPanelTemplate>
        <ZoomableCanvas Loaded="ZoomableCanvas_Loaded"
                        RealizationLimit="1000"
                        RealizationRate="10"
                        RealizationPriority="Background"/>
    </ItemsPanelTemplate>
</ListBox.ItemsPanel>

And finally, here is the app!

The distribution of elements on the canvas when there is a RealizationLimit is not random.  When you’re not implementing ISpatialItemsSource yourself, the ZoomableCanvas will by default show larger items (width + height) before showing smaller ones.  In other words, if your RealizationLimit is 1000, the ZoomableCanvas will create elements for the 1000 biggest items (within the viewbox) first, and then it will dispose the rest.  But if you are implementing ISpatialItemsSource yourself, then ZoomableCanvas will create elements for the first 1000 items that you return from the Query method, in the order you return them, so it’s up to you to prioritize which ones show up on the canvas first.  You’re welcome to use PriorityQuadTree if you’d like, but for this particular sample I was able to come up with a more efficient technique to prioritize evenly-distributed items since the elements are in a grid-like pattern:

public IEnumerable<int> Query(Rect rectangle)
{
    rectangle.Intersect(Extent);

    var top = Math.Floor(rectangle.Top / 100);
    var left = Math.Floor(rectangle.Left / 100);
    var right = Math.Ceiling(rectangle.Right / 100);
    var bottom = Math.Ceiling(rectangle.Bottom / 100);
    var width = Math.Max(right - left, 0);
    var height = Math.Max(bottom - top, 0);

    foreach (var cell in Quadivide(new Rect(left, top, width, height)))
    {
        var x = cell.X;
        var y = cell.Y;
        var i = x > y ?
            Math.Pow(x, 2) + y :
            Math.Pow(y, 2) + 2 * y - x;
        if (i < Count)
        {
            yield return (int)i;
        }
    }
}

private IEnumerable<Point> Quadivide(Rect area)
{
    if (area.Width > 0 && area.Height > 0)
    {
        var center = area.GetCenter();
        var x = Math.Floor(center.X);
        var y = Math.Floor(center.Y);
        yield return new Point(x, y);

        var quad1 = new Rect(area.TopLeft, new Point(x, y + 1));
        var quad2 = new Rect(area.TopRight, new Point(x, y));
        var quad3 = new Rect(area.BottomLeft, new Point(x + 1, y + 1));
        var quad4 = new Rect(area.BottomRight, new Point(x + 1, y));

        var quads = new Queue<IEnumerator<Point>>();
        quads.Enqueue(Quadivide(quad1).GetEnumerator());
        quads.Enqueue(Quadivide(quad2).GetEnumerator());
        quads.Enqueue(Quadivide(quad3).GetEnumerator());
        quads.Enqueue(Quadivide(quad4).GetEnumerator());
        while (quads.Count > 0)
        {
            var quad = quads.Dequeue();
            if (quad.MoveNext())
            {
                yield return quad.Current;
                quads.Enqueue(quad);
            }
        }
    }
}

So there you have it!  Hopefully this (rather long) blog post gave you a good idea of how use the built-in UI virtualization, how to implement your own data data virtualization, and how to put on limits to keep your app responsive as a last resort.  I’ve attached the final ZoomableApplication2 project to this post.  Phew!

Attachment: ZoomableApplication2.zip
  • I'm off to build you a pedestal...

    Thank you so much!

    Alex

  • Kael, this sample is just amazing, I'm loading the 1,000,000 Data Items without any significant delay.

    After trying the Virtual Canvas and this sample, I can see the main difference between both: while Virtual Canvas tends to crash or get really slow (for the mentioned objects loading method) Zoomable Canvas brings a smoother experience.

    I have few feedback items, with all due respect:

    -There are some times that I get a little "lost" on the canvas, without any X/Y current position or zoom level indicators (I'm currently trying to add this feature on my own application)

    -The rendering method is always random? I mean, can we modify the order of the items rendering to provide some alternatives, like "Centre to Edges" or "Horizontal/Vertical Scanning"

    -I have a random behaviour, while zoom out the Canvas first render a lot of items, and then,depending on zoom level, they are suddenly "erased" from the screen

    Well, thank you very much again to share this pieces of code, I'm trying to figure out how to implement the Semantic Zoom with Zoomable Canvas right now!

    Best Regards!

  • Excellent blog post. Data Virtualization FTW.

  • @Ignatio: You could certainly add X/Y/Zoom indicators or minimaps or landmarks to your own app. You could also limit the Scale and Offset in your mouse handlers so that the user is never able to pan or zoom too far away.

    I'm not sure what you mean by the 'rendering method'. The items are being populated in the same order as the Query method returns them, and any randomness in the layout that you're seeing is done on purpose to mimic the original VirtualCanvas sample.

    The reason you see items suddenly being 'erased' is because each realization pass starts by creating new elements before disposing old ones. The RealizationLimit is 1000, but each time you pan or zoom it stops the realization/virtualization pass (because the viewbox has changed) before it had a chance to dispose enough elements to bring the count back down to 1000. Since it starts over at the creation phase each time, eventually you will build up much more than 1000 elements on the screen before it gets to the cleanup phase. But when you finally stop interrupting it and let the canvas remain still, it will finally get to the cleanup phase and start disposing enough items to bring you back down to 1000.

    I hope that explains things!  :)

  • First, thanks for an excellent new control!

    What is the reason that the first sample allows continuous scroll (when you keep mouse button pressed on top or bottom scrollbar arrows), while the latter samples two do not? Dragging the thumbtrack works fine in all three.

    Also, in the last sample keyboard navigation (keeping down arrow pressed, for example) works fine for a while and then it just stops. If then I press left or right arrow to move to another item, and then again press down arrow, I can continue moving from the point where it got stuck. Altogether, I can get from the top to to the bottom of the canvas, or vice versa, using keyboard arrow keys only if I zig-zag.

  • @Tonko: The reason the thumbtrack works but the scrollbar arrows do not is because my OnPreviewMouseMove handler ignores Thumb elements but not RepeatButton elements.  If it were a real application, I would actually look up the visual tree and ignore anything within a ScrollBar.

    The reason keyboard navigation is sporadic is because of the random placement of the shapes.  Sometimes there are larger gaps between shapes than others.  If there is a large enough gap, then one shape will be on-screen but its neighbor will be off-screen.  Shapes are not actually created when they are more than 10% off-screen, so you can't use the arrow keys to move to them.  Fortunately, most of the gaps are smaller than 10% of the total size, so when a shape is fully visible then its neighbors will be realized and the arrow keys can jump to them as expected.

  • One thing I noticed when testing the GridLikeItemSource - with the ICollection.Count returning the maximum value you will receive a hanging behavior if you press enter on a ListItem within the list box.

    I modified the property to return the count:

           int ICollection.Count

           {

               get { return count; }

           }

    And that behavior went away.

  • Hey Kael,

    Thanks for posting this, good work.

    Just wondering how you can turn off panning and zooming in the example you posted?

    Thanks.

  • This is gerat stuff it makes me remember pivot viwer, but event pivot dosen't handle so many items easily.

    Thanks for the post and code :)

  • Hi Kael,

    I’ve been experimenting with your very useful ZoomableCanvas control. I’ve been testing some simple uses of the control, as outlined in your example 2. I’m able to add 1000 random shapes and pan, zoom, and scroll around.

    Everything works fine until I start to remove items from the DataItems ObservableCollection. I’m using this event handler:

               CompositionTarget.Rendering += Update;

    protected void Update(object sender, EventArgs e) {

               TimeSpan now = ((RenderingEventArgs) e).RenderingTime;

               // already done an update this frame?

               if (lastUpdate == now)

                   return;

               lastUpdate = now;

               TimeSpan delta = now - lastFrameRateUpdate;

               if (delta.TotalMilliseconds < 1000)

                   return;

               lastFrameRateUpdate = now;

               DataItems.RemoveAt(0);

    }

    …It gets fired once per frame and every time changes to the visual tree force an update to the composition tree. Every second, the 0th element of the DataItems ObservableCollection is removed. I’m getting an “Index out of range” exception at:

    System.Windows.Controls.ZoomableCanvas.PrivateSpatialIndex.this[int].get(int index) Line 800:

       return _items[index].Bounds;

    System.Windows.Controls.ZoomableCanvas.ArrangeOverride(System.Windows.Size finalSize) Line 1373:

       Rect oldBounds = PrivateIndex[index];

    …because index = 999 and _items.Count = 999, so _items[index] is out of range.

    I thought perhaps it’s not a good time to remove an item during the Rendering event, so I tried using a DispatcherTimer.Tick event every 100ms instead, but it also fails. I also tried to remove the last item (as you do in your example code), instead of the 0th item, but that fails too. I even tried to only remove an item in response to a mouse click, but that fails too.

    Am I supposed to inform ZoomableCanvas that I’ve removed an item from the DataItems ObservableCollection so it can adjust its internal spatial index, or is there something else I’m doing wrong?

    Thanks

  • Hi

    Really an impressive control!!

    Question:

    Do you have the same control or a similar control also for Silverlight. I have performance problm there as well

    That would be really great!

    Thanks

  • Thanks Francesco.  I haven't ported it to Silverlight but the concept should be the same.

  • I'm considering a port of an application that has a very large grid of information that needs to be displayed. This seems like the perfect place to start.

    Any thoughts on how quickly a pinch/zoom would respond using this on a WinRT tablet OR Windows8 phone?

  • Hello Kael,

    Thanks for the excellent control. It works perfectly !

    In my WPF application I have ItemsControl which has ZoomableCanvas as Panel which provides the Virtualization mechanism for the Canvas. This is working perfectly. This Canvas needs to have a Graph like background. When I use the xaml below, the grid is generated but only to the extent of the Viewport (at load time). So when scrolled you see the background grid getting clipped vertically and horizontally. In the VirtualCanvas (blogs.msdn.com/.../performant-virtualized-wpf-canvas.aspx) this was handled by adding child Visuals to the canvas as Backdrop.

       <ListBox.ItemsPanel>

                       <ItemsPanelTemplate>

                           <ZoomableCanvas x:Name="ZCanvas" Loaded="ZoomableCanvas_Loaded" >

                               <ZoomableCanvas.Background>

                                   <DrawingBrush TileMode="Tile" Stretch="Fill"  

                                                 Viewport="0 0 100 60" ViewportUnits="Absolute"

                                                 ViewboxUnits="Absolute" >

                                       <DrawingBrush.Drawing>

                                           <GeometryDrawing Geometry="M0,0 L0,1 0.03,1 0.03,0.03 1,0.03 1,0 Z" Brush="Gray" />

                                       </DrawingBrush.Drawing>

                                   </DrawingBrush>

                               </ZoomableCanvas.Background>

                           </ZoomableCanvas>

                       </ItemsPanelTemplate>

                   </ListBox.ItemsPanel>

    How can we have a Grid background which stretches the entire width of ZoomableCanvas or translates with the scroll?

    Any help / hints sincerely appreciated.

    Thanks again.

  • The way I typically implement backgrounds is to use code-behind to dynamically change the background when the scale and offset change.  This is because I've usually always wanted something more than just a simple grid as a background.  At a minimum, I want the background to look "hazy" the more and more the user zooms out so that the background gives a feeling of getting "farther away".  In more complex apps I actually draw gridlines with text next to them (for example showing latitude/longitude) so that the background gives an indication of "where" you are on it.  I don't have an example offhand that uses a pure XAML-only solution.

Page 1 of 2 (17 items) 12
Leave a Comment
  • Please add 7 and 5 and type the answer here:
  • Post