WPF – DataContext Virtualization With Paged Services

by Dean 27. April 2010 21:51

Many WPF applications need to handle a very large data collections – maybe the users really need a million rows in their GridView control. The way we cope with this is to ‘virtualize’ the data, and have it available to your control on an ‘as needed’ basis.

Most list controls in WPF (including the standard ListView/GridView) include the concept of a ‘viewport’ under the hood. A viewport is a virtual ‘window’ on the underlying data collection, which only requires the data currently being displayed – so if your collection is a million rows, and your ‘viewport’ is only 100 rows high, then you only need 100 rows (although they must be the ‘right’ rows).

In addition, if the data collection that is your DataContext implements the non-generic ‘IList’ interface, the viewport will optimize it access to the collection calling the ‘IList.this[index]’ for row enumeration rather than GetEnumerator().

This means that if we create a custom collection that implements the non-generic ‘IList’ interface, we can ‘virtualize’ access to the data – giving us an opportunity to put a million rows in our grid in the blink of an eye.

Also, this ‘million row’ scenario may need to retrieve data from a WCF service (for example), and the service method to retrieve data will most likely be paged, requiring page number and page size parameters to retrieve the correct page.

OK, that’s the intro over, so lets discuss a REALLY easy solution to this requirement

Firstly, I am assuming that you have a service reference to a WCF service that has 2 service methods

  1. A method that gets the total collection count
  2. A method that takes page number and page size parameters and returns a strongly-typed collection representing a single ‘page’ of data

As an example of what I mean, here is a sample service contract for a service I have created for this post

namespace WCFDataPaging.WCF
{
    [ServiceContract]
    public interface IService
    {
 
        [OperationContract]
        List<TestDataObject> GetPageData(int page, int pageSize);
 
        [OperationContract]
        int GetDataCount();
    }
}

and here is my service implementation

public class MainService : IService
{
    public List<TestDataObject> GetPageData(int page, int pageSize)
    {
        return Utility.TestData.Skip(page*pageSize).Take(pageSize).ToList();
    }
 
    public int GetDataCount()
    {
        return Utility.TestData.Count;
    }
}

Now the utility class simple generates a test collection of about a million rows. In real life, these services will be retrieving real data from some kind of data store.

Now I need a collection class on the WPF side, that can perform all of the necessary virtualization:

public sealed class VirtualServiceCollection<T> : IList<T>, IList
{
    private readonly Func<int, int, T[]> dataFunction;
    private readonly Func<int> countFunction;
    private readonly int pageSize;
    private readonly List<T> data;
    private int currentPage;
 
    public VirtualServiceCollection(Func<int,int,T[]> dataFunction, Func<int> countFunction, int pageSize)
    {
        this.dataFunction = dataFunction;
        this.countFunction = countFunction;
        this.pageSize = pageSize;
        data = new List<T>(dataFunction(0, pageSize));
    }
 
    public IEnumerator<T> GetEnumerator()
    {
        var count = countFunction();
        for (var i = 0; i < count; i++)
            yield return this[i];
    }
 
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
 
    public void Add(T item)
    {
        throw new NotImplementedException();
    }
 
    public int Add(object value)
    {
        throw new NotImplementedException();
    }
 
    public bool Contains(object value)
    {
        throw new NotImplementedException();
    }
 
    void IList.Clear()
    {
        DoClear();
    }
 
    public int IndexOf(object value)
    {
        return data.IndexOf((T)value);
    }
 
    public void Insert(int index, object value)
    {
        throw new NotImplementedException();
    }
 
    public void Remove(object value)
    {
        throw new NotImplementedException();
    }
 
    void IList.RemoveAt(int index)
    {
        throw new NotImplementedException();
    }
 
    private T GetItem(int index)
    {
        var bot = currentPage * pageSize;
        var top = Math.Min(bot + pageSize, countFunction());
        if (index >= bot && index < top)
            return data[index - bot];
        currentPage = (int)Math.Floor(index / (double)pageSize);
        data.Clear();
        data.AddRange(dataFunction(currentPage, pageSize));
        return data[index - (currentPage * pageSize)];
    }
 
    object IList.this[int index]
    {
        get { return GetItem(index); }
        set { throw new NotImplementedException(); }
    }
 
    bool IList.IsReadOnly
    {
        get { return false; }
    }
 
    public bool IsFixedSize
    {
        get { return false; }
    }
 
    private void DoClear()
    {
        currentPage = 0;
        data.Clear();
    }
 
    void ICollection<T>.Clear()
    {
        DoClear();
    }
 
    public bool Contains(T item)
    {
        throw new NotImplementedException();
    }
 
    public void CopyTo(T[] array, int arrayIndex)
    {
        throw new NotImplementedException();
    }
 
    public bool Remove(T item)
    {
        throw new NotImplementedException();
    }
 
    public void CopyTo(Array array, int index)
    {
        throw new NotImplementedException();
    }
 
    int ICollection.Count
    {
        get { return countFunction(); }
    }
 
    public object SyncRoot
    {
        get { return this; }
    }
 
    public bool IsSynchronized
    {
        get { return false; }
    }
 
    int ICollection<T>.Count
    {
        get { return countFunction(); }
    }
 
    bool ICollection<T>.IsReadOnly
    {
        get { return false; }
    }
 
    public int IndexOf(T item)
    {
        return data.IndexOf(item);
    }
 
    public void Insert(int index, T item)
    {
        throw new NotImplementedException();
    }
 
    void IList<T>.RemoveAt(int index)
    {
        throw new NotImplementedException();
    }
 
    public T this[int index]
    {
        get { return GetItem(index); }
        set { throw new NotImplementedException(); }
    }
}

One of the things that’s interesting about this is that it takes two Func delegates in its constructor – one to get the collection count, and one to retrieve a page

now, we need to wire it all up

First here’s my WPF code-behind:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        Loaded += DoLoaded;
    }
 
    private void DoLoaded(object sender, RoutedEventArgs e)
    {
        var s = new ServiceClient();
        var coll = new VirtualServiceCollection<TestDataObject>(s.GetPageData, s.GetDataCount, 100);
        DataContext = coll;
    }
}

As you can see, we pass into our collection the to service methods that get the data we need

And finally – here’s the XAML

<Window x:Class="WCFDataPaging.WPF.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">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <StackPanel Orientation="Horizontal">
            <TextBlock Text="Count : " />
            <TextBlock Text="{Binding Path=Count}" />
        </StackPanel>
        <ListView ItemsSource="{Binding}" Grid.Row="1">
            <ListView.View>
                <GridView>
                    <GridView.Columns>
                        <GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}" />
                        <GridViewColumn Header="ID" DisplayMemberBinding="{Binding ID}" />
                        <GridViewColumn Header="Start Date" DisplayMemberBinding="{Binding StartDate}" />
                        <GridViewColumn Header="Price" DisplayMemberBinding="{Binding Price}" />
                    </GridView.Columns>
                </GridView>
            </ListView.View>
        </ListView>
    </Grid>
</Window>

We’ve got a textbox at the top for the count, and the columns for our strongly-typed object

And here it is in action:

virtualdata

This took about half a second to load, and scrolls through the million rows pretty smoothly. FYI the first part of the name column data in my test collection is the row numer, so I can check it’s working :)

 

Dean

Tags: , ,

DataBinding | WCF | XAML | WPF

Enumeration Binding in Silverlight

by Dean 31. January 2009 10:57

A contractor colleague of mine (Phil Steel) had an interesting Silverlight problem yesterday. He wanted to populate a Silverlight ComboBox with an enumeration, and implement 2-way binding to a property on his Data class (i.e. binding his property to the ‘SelectedItem’ on the ComboBox.

His requirements were as follows:

  1. The solution must have full design-time support in Expression Blend
  2. The code must give the developer an opportunity to create metadata for the enumeration that can be used for ‘user friendly’ visual values in the ComboBox
  3. There must be full 2-way binding, so no need to tap into the ‘SelectionChanged’ event to update the data object.
  4. Minimal C# code, with as much as possible being re-useable ‘as is’ for any enumeration.

Ok, so I googled around and only found fragments of a possible solution, so I decided to engineer one myself.

The details are below

Task 1 : Create reusable code that converts a .NET enumeration into a collection

 

There are 2 classes we need as part of this task, and the code is below

public sealed class EnumContainer
{
    public int EnumValue { get; set; }
    public string EnumDescription { get; set; }
    public object EnumOriginalValue { get; set; }
    public override string ToString()
    {
        return EnumDescription;
    }
 
    public override bool Equals(object obj)
    {
        if (obj == null)
            return false;
        return EnumValue.Equals((int)obj);
    }
 
    public override int GetHashCode()
    {
        return EnumValue.GetHashCode();
    }
 
}
 
public class EnumCollection<T> : List<EnumContainer> where T : struct 
{
    public EnumCollection()
    {
        var type = typeof (T);
        if (!type.IsEnum)
            throw new ArgumentException("This class only supports Enum types");
        var fields = typeof (T).GetFields(BindingFlags.Static | BindingFlags.Public);
        foreach(var field in fields)
        {
            var container = new EnumContainer();
            container.EnumOriginalValue = field.GetValue(null);
            container.EnumValue = (int) field.GetValue(null);
            container.EnumDescription = field.Name;
            var atts = field.GetCustomAttributes(false);
            foreach (var att in atts)
                if (att is DescriptionAttribute)
                {
                    container.EnumDescription = ((DescriptionAttribute) att).Description;
                    break;
                }
            Add(container);
        }
        
    }
}

 

These 2 classes represent a bindable version of our enumeration. One class is an object that represents the enumeration values (EnumContainer), and the other is the collection.

(The part of the code above describing ‘CustomAttributes’ relates to the task below.

 

Task 2 (optional) : Add ‘user friendly’ metadata to your enumerations

 

This is a well known solution using the ‘DescriptionAttribute’ class

 

    public enum CustomerStatus
    {
        [Description("Not Yet Approved")]
        UnApproved,
 
        [Description("Pending Approval")]
        PendingApproval,
 
        [Description("Fully Approved")]
        Approved
    }

These descriptions will appear in your combobox

 

Task 3 : Create an IValueConverter to support 2-way binding of the ComboBox to the data object.

 

public class EnumValueConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, 
                CultureInfo culture)
    {
        return (int) value;
    }
 
    public object ConvertBack(object value, Type targetType, object parameter, 
CultureInfo culture)
    {
        if (value == null)
            return null;
        if (value.GetType() == targetType)
            return value;
        return ((EnumContainer) value).EnumOriginalValue;
    }
}

 

Task 4 : Implement a solution to suit your needs

 

Ok, now that we have all the base code added to our project, it’s time to implement a solution to our problem. The only c# class that needs to be created is a simple collection class that inherits from EnumCollection and allows full designer support in Expression Blend

So here (for example) are our data classes that we will use to demonstrate our solution

1) Our example ‘Customer’ class

public class Customer
{
    public string CustomerName { get; set; }
    public CustomerStatus Status { get; set; }
}

2) Our example Enumeration (notice the use of the DescriptionAttribute, which gives the ComboBox user friendly test)

 

    public enum CustomerStatus
    {
        [Description("Not Yet Approced")]
        UnApproved,
 
        [Description("Pending Approval")]
        PendingApproval,
 
        [Description("Fully Approved")]
        Approved
    }

 

3) Our collection to provide Blend support (as you can see, its just a simple inheritor as XAML doesnt easily support generics).

public class CustomerStatusEnumeration : EnumCollection<CustomerStatus>
{
 
}

 

4) Here's an example of wiring up the data object in the code

 

private void PageLoaded(object sender, RoutedEventArgs e)
{
    customer = new Customer() { CustomerName = "Customer1", 
        Status = CustomerStatus.PendingApproval };
    DataContext = customer;
}

 

Task 5 : Wire it all up in the Xaml

<UserControl x:Class="TestEnum.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    Width="400" Height="300" xmlns:TestEnum="clr-namespace:TestEnum" 
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d">
    <UserControl.Resources>
        <TestEnum:CustomerStatusEnumeration 
            x:Key="CustomerStatusEnumerationDS" 
            d:IsDataSource="True"/>
        <TestEnum:EnumValueConverter  
            x:Key="EnumConverter" />
    </UserControl.Resources>
    <Grid x:Name="LayoutRoot" Background="White">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <TextBox Text="{Binding Mode=TwoWay, Path=CustomerName}" 
                 Margin="5" HorizontalAlignment="Left" VerticalAlignment="Top" />
        <ComboBox Name="Combo" HorizontalAlignment="Left" 
                  VerticalAlignment="Top" 
                  ItemsSource="{Binding Mode=OneWay, 
                        Source={StaticResource CustomerStatusEnumerationDS}}" 
                  SelectedItem="{Binding Status, Mode=TwoWay, 
                        Converter={StaticResource EnumConverter}}" 
                  Margin="5" Grid.Column="1"/>
        <Button Content="Update" Click="UpdateCustomer" Margin="5" 
                HorizontalAlignment="Left" 
                VerticalAlignment="Top" Grid.Column="2" />
    </Grid>
</UserControl>

An here’s a screenshot of the above solution

enumexample

As you can see, it’s a very simple and effective solution. And should we require to wire up a second enum to our Silverlight app then the only class we’d need to create (assuming the enum already exists) is the empty collection class as described above.

 

I hope you like the solution, any comments about how to improve it then please let me know.

 

Dean

Tags: , ,

WCF | Silverlight | DataBinding

Fixed - Issues With WCF and Mixed VS/Blend Development

by Dean 25. January 2009 12:37

Some of you may have come across an issue when developing ‘fast and dirty’ demo apps in Silverlight that have a WCF backend service on the web application.

When developing throwaway demo apps for clients, you need to take all of the shortcuts you can get, so I always use the ‘Add service reference’ feature of Visual Studio to add a service reference within my Silverlight app to the host ASP.NET service (not advisable for production-quality apps though). This is a great feature because as you change the service interface you can keep in sync on your Silverlight app using a single service ‘Update’ button.

However, the big drawback of this approach is that the Visual Studio tool that creates the service reference hard-codes the service Uri into the generated classes.

If you were just using Visual Studio then this wouldn't be a problem, but unfortunately VS and Blend use different development servers that cannot co-exist on the same TCP port, so for one of the two development environments the Uri of the service is going to be wrong.

for example, you create the service reference in Visual Studio, which is configured to run the website on url ‘http://localhost:43667/DemoApp’ , then the hard-coded Uri for the service will be something like ‘http://localhost:43667/DemoApp/Service1.svc’.

However, Expression Blend will always run the solution on a different port, so it will run the app on a base url of something like ‘http://localhost:52234/DemoApp/’ which means that the Silverlight app wont access the service and your app will not be able to access the service when run from Blend.

Your gut reaction might be to try the following:

  1. Fix the port in Visual Studio to be the same as the one in Blend. – This wont work because Blend will detect VS is on it’s preferred port and will automatically switch to an alternative port.
  2. Use your local version of IIS to avoid the clash of the ports. – This will work but doesn’t really fit in with the idea of creating a portable throwaway demo app.
  3. Work in both VS and Blend, but only press F5 while in VS. This works but is a pain in the butt.
  4. Play with crossdomain policy files etc. – Not really a solution, especially as VS always tears-down the dev server when in DEBUG mode.

However, there is a solution for this scenario that only requires changing a single line of code to your Silverlight project.

Below is the some ‘Before’ sample code

private void PageLoaded(object sender, RoutedEventArgs e)
{
    var service = new Service1Client();
    service.DoWorkCompleted += (source, args) => MessageBox.Show("DONE");
    service.DoWorkAsync();
}


And now here is the same code with an added line that fixes this issue.

 

private void PageLoaded(object sender, RoutedEventArgs e)
{
    var service = new Service1Client(new BasicHttpBinding(BasicHttpSecurityMode.None), 
        new EndpointAddress(Application.Current.Host.Source.AbsoluteUri.Replace(
            Application.Current.Host.Source.AbsolutePath,"/Service1.svc")));
    service.DoWorkCompleted += (source, args) => MessageBox.Show("DONE");
    service.DoWorkAsync();
}

 

Basically, we’ve just changed the way we’ve constructed the service object, and passed in the necessary parameters to make the service reference relative rather than absolute. (You may need to replace the ‘Service1.svc’ parameter with one that is relevant to your project).

This is a neat little code snippet to keep on hand when developing these kinds of apps

I hope this helps you fix this annoying issue in your Silverlight development.

If you have any further suggestions then let me know.

 

Dean

Tags: , ,

Silverlight | DataBinding | WCF

Disclaimer
The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

© Copyright 2010 Dean Chalk's Blog