RSS

Efficiency Strategies for PivotViewer

08 Sep

Joris Dries started a Twitter conversation this morning about getting decent performance out of the PivotViewer in SL5RC. So, I thought now would be a good time to gather a bunch of ideas I’ve had over the past few months and put them all in one post. This might not answer Joris’ question directly, but it does satisfy the title.

You can download the complete source code of the examples here.

What I’ll do is build a number of scenarios and compare them against a baseline. Measurements will not be empirical but rather based on when the user thinks the PivotViewer is ready for use. For background info this is the laptop I’m running these tests on:

  • Windows 7 Ultimate x64 SP1
  • Chrome 13
  • WEI: 5.0
  • Intel i5 2.27GHz
  • 4GB RAM
  • Non-SSD HDD

Setting the Baseline

I’m going to start with a data class and a method for creating a sample set from it. This will allow us to stress test the new PivotViewer control. I’m not going to include the code for this here as it’s rather long-winded and not the focus of this post. It’s all in the sample solution.

public class Person : INotifyPropertyChanged
{
    public string Forename;
    public string Surname;
    public int Age
    public string Address1;
    public string Address2;
    public string Address3;
    public string Address4;
    public string Postcode;
    public string Sex;
}

And here’s the footprint of the method for creating a pseudo-random set at runtime that we can databind with:

public static ObservableCollection<Person> GetSampleData(int sampleSize);

Now let’s build some simple xaml so we can show our data. This example uses the new PivotProperties and PivotViewerItemTemplate markup and just needs us to specify an ItemsSource at runtime:

<UserControl x:Class="XamlTileEfficiency.NoEfficiencies"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:pivot="clr-namespace:System.Windows.Controls.Pivot;assembly=System.Windows.Controls.Pivot"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">

    <Grid x:Name="LayoutRoot" Background="White">
        <pivot:PivotViewer x:Name="pv">
            <pivot:PivotViewer.ItemTemplates>
                <pivot:PivotViewerItemTemplate>
                    <StackPanel Orientation="Vertical" Height="150" Width="100" Background="LightBlue">
                        <TextBlock Text="{Binding Forename}" />
                        <TextBlock Text="{Binding Surname}" />
                        <TextBlock Text="{Binding Age}" />
                        <TextBlock Text="{Binding Sex}" />
                        <TextBlock Text="{Binding Address1}" />
                        <TextBlock Text="{Binding Address2}" />
                        <TextBlock Text="{Binding Address3}" />
                        <TextBlock Text="{Binding Address4}" />
                        <TextBlock Text="{Binding Postcode}" />
                    </StackPanel>
                </pivot:PivotViewerItemTemplate>
            </pivot:PivotViewer.ItemTemplates>
            <pivot:PivotViewer.PivotProperties>
                <pivot:PivotViewerStringProperty Binding="{Binding Forename}" DisplayName="First Name" Id="ForenameId" Options="CanFilter" />
                <pivot:PivotViewerStringProperty Binding="{Binding Surname}" DisplayName="Last Name" Id="SurnameId" Options="CanFilter" />
                <pivot:PivotViewerNumericProperty Binding="{Binding Age}" DisplayName="Age" Id="AgeId" Options="CanFilter" />
                <pivot:PivotViewerStringProperty Binding="{Binding Sex}" DisplayName="Sex" Id="SexId" Options="CanFilter" />
                <pivot:PivotViewerStringProperty Binding="{Binding Address1}" DisplayName="Address1" Id="Address1Id" Options="CanFilter" />
                <pivot:PivotViewerStringProperty Binding="{Binding Address2}" DisplayName="Address2" Id="Address2Id" Options="CanFilter" />
                <pivot:PivotViewerStringProperty Binding="{Binding Address3}" DisplayName="Address3" Id="Address3Id" Options="CanFilter" />
                <pivot:PivotViewerStringProperty Binding="{Binding Address4}" DisplayName="Address4" Id="Address4Id" Options="CanFilter" />
                <pivot:PivotViewerStringProperty Binding="{Binding Postcode}" DisplayName="Postcode" Id="PostcodeId" Options="CanFilter" />
            </pivot:PivotViewer.PivotProperties>
        </pivot:PivotViewer>
        <Button HorizontalAlignment="Left" VerticalAlignment="Top" Margin="5,2,0,0" Width="100" Height="25" Content="Stop the Clock!" Click="Button_Click" />
    </Grid>
</UserControl>

Lastly let’s write some client code to create the sample set and bind it to our PivotViewer. Note that I’ve also written a rudimentary stopwatch that we’ll use to time when our PivotViewer is ready for a user to interact with it.

public partial class NoEfficiencies : UserControl
    {
        private const int SAMPLE_SIZE = 1000;

        private DateTime _start;

        public NoEfficiencies()
        {
            InitializeComponent();

            Loaded += (sender, e) =>
                {
                    var data = Person.GetSampleData(SAMPLE_SIZE);
                    _start = DateTime.Now;
                    pv.ItemsSource = data;
                };
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            var timespan = TimeSpan.FromTicks(DateTime.Now.Ticks - _start.Ticks);

            MessageBox.Show(string.Format("Elapsed time is {0} seconds.", timespan.TotalSeconds.ToString()));
        }
    }
}

On my laptop it takes about 12 seconds for the items to appear in the PivotViewer.

So, what’s happening under the covers when we set the ItemsSource? First of all the control displays the Filter Panel on the left-hand side. This happens almost instantly and gives a good user experience. The delay is in producing the xaml tiles (or trading cards). This is because the runtime has to render static images from the databound markup. This is expensive.

Strategy 1: Use Pre-generated DeepZoom Images

You may have a situation where you can pre-build all your visuals and store them on a server. If you’ve spent any time working with PivotViewer v1 this will be very familiar territory. If your collections are going to be fairly static you can then just use the new CxmlCollectionSource class in combination with some dynamic features that I talked about in my previous post: Add, Remove & Change Cxml Items in PivotViewer.

Alternatively you can you the new PivotViewerMultiSizeImage control in your PivotViewerItemTemplates to render your pre-generated visuals. Either way, you’re swapping the cost of rendering the images from xaml with that of downloading them from the web.

Strategy 1 works well if your visuals are not very dynamic.

Strategy 2: Use Multiple Visual Templates

The PivotViewerItemTemplate I’ve used in my example is very inefficient for the number of tiles. Before I can interact with the control I have to wait for it to render 1000 tiles of information that I can’t possibly read at the default zoom level. So strategy 2 suggests defining multiple visual templates of increasing complexity. This helps by allowing the control to render simpler visual elements first and then the complex ones asynchronously. In the meantime, however, the user has been able to do some work.

            <pivot:PivotViewer.ItemTemplates>
                <!-- This new template will be shown up to a zoomed width of 100px -->
                <pivot:PivotViewerItemTemplate MaxWidth="100">
                    <StackPanel Orientation="Vertical" Height="75" Width="50" Background="LightGreen">
                        <TextBlock Text="{Binding Forename}" />
                        <TextBlock Text="{Binding Surname}" />
                    </StackPanel>
                </pivot:PivotViewerItemTemplate>
                <pivot:PivotViewerItemTemplate>
                    <StackPanel Orientation="Vertical" Height="150" Width="100" Background="LightBlue">
                        <TextBlock Text="{Binding Forename}" />
                        <TextBlock Text="{Binding Surname}" />
                        <TextBlock Text="{Binding Age}" />
                        <TextBlock Text="{Binding Sex}" />
                        <TextBlock Text="{Binding Address1}" />
                        <TextBlock Text="{Binding Address2}" />
                        <TextBlock Text="{Binding Address3}" />
                        <TextBlock Text="{Binding Address4}" />
                        <TextBlock Text="{Binding Postcode}" />
                    </StackPanel>
                </pivot:PivotViewerItemTemplate>
            </pivot:PivotViewer.ItemTemplates>

Once loaded, select an item by clicking on it. You’ll see the higher res template render as you zoom in. Now use the cursor keys to navigate. You’ll see flashes of green as new tiles come into view. Then, and only then, does the runtime render the higher res image.

Scenario 2 take about 8 seconds to load on my machine. 30% faster than our baseline!

Scenario 3: Stagger your loading

Hitting PivotViewer with 1000 items at once can be a bit overwhelming for it. It also means that your user has to wait until ALL 1000 items have been rendered before they can work. In the pre-beta versions of PivotViewer the team had investigated the idea of a BatchObservableCollection class. Unfortunately, this didn’t make it into the RC but we can still replicate the idea. Basically, we can drip feed batches of the whole over a period of time. If your scenario is very visual, this can be a really cool feature as the user gets to see data being assembled in front of them. Ultimately it takes more time, but might give your users the impression that it’s actually quicker!

First up, let’s add a new static method that will add items to an already existing collection.

public static void PopulateWithSampleData(ObservableCollection<Person> data, int sampleSize);

Next we’ll create a class that will handle the drip-feeding of the items. We need to specify an interval between batches so that a) PivotViewer thinks we’ve finished and starts to refresh the UI and b) so that the UI finishes its refresh before the next batch.

using System;
using System.Collections.ObjectModel;
using System.Windows.Threading;

namespace XamlTileEfficiency
{
    public class DripFeeder<T>
    {
        private readonly DispatcherTimer _timer;
        private readonly ObservableCollection<T> _data;
        private readonly int _sampleSize;
        private readonly int _batchSize;
        private readonly Action<ObservableCollection<T>, int> _populateAction;

        private int _counter;

        public DripFeeder(ObservableCollection<T> data, int sampleSize, int batchSize, TimeSpan interval, Action<ObservableCollection<T>, int> populateAction)
        {
            _timer = new DispatcherTimer { Interval = interval };
            _timer.Tick += _timer_Tick;

            _data = data;
            _sampleSize = sampleSize;
            _batchSize = batchSize;
            _populateAction = populateAction;
        }

        void _timer_Tick(object sender, EventArgs e)
        {
            _timer.Stop();

            if (_counter < _sampleSize)
            {
                // This won't take account of any remainders, but hey.
                _populateAction(_data, _batchSize);
                _counter += _batchSize;

                _timer.Start();
            }
        }

        public void Start()
        {
            _timer_Tick(null, EventArgs.Empty);
        }
   }
}

Now that we have all our building blocks we just need to change the client code from our baseline example like this. Notice that we do our databinding BEFORE we start to populate the collection.

            Loaded += (sender, e) =>
            {
                var data = new ObservableCollection<Person>();
                _start = DateTime.Now;
                pv.ItemsSource = data;

                var feeder = new DripFeeder<Person>(data, SAMPLE_SIZE, BATCH_SIZE, TimeSpan.FromSeconds(INTERVAL_SECONDS), Person.PopulateWithSampleData);
                feeder.Start();
            };

So, scenario 3 is great for situations where the gradual loading of data accompanied with the visual changes is OK. In public demos this might be impressive!

Scenario 4: Lazy Load your Facets

In combination with Scenario 2 this can be very powerful. It is also a great way to use PivotViewer with high-latency data. e.g. stuff gathered from multiple sources across the web. As long as our data items implement INotifyPropertyChanged we can lazy load the data asynchronously or triggered when we need it.

In this last example (LazyLoader.xaml) we’ll just load the Forename and Surname properties. Then, as the user selects items in the PivotViewer we’ll go and fetch the rest of the data. The control will handle the PropertyChanged events and re-render the visual elements for us. It’s not the most elegant example, but you should get the potential from it.

Wrap Up

The PivotViewer control is very powerful and flexible but you have to think about loading data in the right way for your situation. I’d love to hear of other scenarios that I haven’t covered here.

Finally, you can download the complete source code of the examples here.

Advertisements
 
11 Comments

Posted by on September 8, 2011 in PivotViewer

 

Tags: , , , ,

11 responses to “Efficiency Strategies for PivotViewer

  1. Joris Dries

    September 8, 2011 at 10:57 pm

    Great post, very useful, and I’ll implement some of this today to see how I can get it to hold up on our collections. The one thing that I noticed though is you haven’t mentioned the “zoom-lag”. The initial load-time is in a way less relevant, because we can use some elegant load-screen to hide the pv control. It’s the UX after that of “swimming” through the card that is stuttery. It stops for several seconds to render the card contents at a certain level, and this is quite distracting. But all in all, really good ideas and immediately applicable. I will get back to you on how I went with this.

     
    • Joris Dries

      September 8, 2011 at 10:58 pm

      Oh and my bad, I forgot to say THANKS MATE! šŸ˜€

       
  2. John collins

    September 14, 2011 at 11:12 pm

    Thanks!

    I was getting a null exception here:
    private void pv_SelectionChanged(object sender, EventArgs e)
    {
    if (pv.SelectedItem != null) // Added this line
    Person.CompletePerson((pv.SelectedItem as Person));
    }

    Also, it’s not always loading the details when I zoom in…is anyone else having issues with that?

     
    • goodcoffeegoodcode

      September 15, 2011 at 7:18 am

      Yep, I’d missed that step out of pv_SelectionChanged. Deselecting all tiles will trigger the event with SelectedItem set to null,

      Can you be more specific about the problems you’ve faced with zooming in please? I’m waiting to hear back from the Silverlight team after I sent them a long email on this topic.

      Thanks for contributing John :).

       
  3. Akshaya

    September 16, 2011 at 6:28 am

    Dude, superb post!

    Need some help…I am having serious issues with loading images on the items using the strategy you explained above, actually I am not able to load the images at all (unless I use CXML, which I dont want). Can you please tell me, how can I proceed with rendering images on my items using this Client Side API?

     
  4. wbjr4-business1@yahoo.com

    September 21, 2011 at 8:54 pm

    When I try to databind to the ItemTemplates property, it does not work. It does not even try to request the property. Does this work for you?

     
  5. Sunil garg

    January 25, 2012 at 12:53 pm

    Hi,

    I have images stored in database and accessing in silverlight as an array of bytes. how can I show this image in pivotviewer

     
    • goodcoffeegoodcode

      February 14, 2012 at 8:45 am

      Hi Sunil,

      Apologies for the slow response. You won’t be able to have a true deepzoom experience given your circumstances but you will be able to load ‘static’ images into the tiles. Firstly. create a template for the tiles (let me know if you need help with that), then dynamically create an inmemory image (bitmap) from the byte arrays that you have. Then, programmatically, you can set the source of each tile’s image control to the bitmap.

      Hope that helps, let me know if you need some code / examples?

      Chris

       
  6. asdf

    February 17, 2012 at 1:10 am

    Replace the Playing Card View with a DataGrid and take advantage of the great Filtering feature the PivotViewer provides.

     

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

 
%d bloggers like this: