Large Lists in WPF and WCF – Part Three

by Chad 25. January 2010 09:37

Paging Your Results
In part one of this series I gave an overview of data binding lists in WPF. In part two, I showed how to asynchronously consume a WCF service so that your WPF user interface remained responsive. In this post, I’m going show an example for paging your results to control the message size and to get results to the client faster.

Let’s Get Started
In the last post, I mentioned that eventually your result set will grow large enough that the default WCF configuration will prevent your message from being received. With the default settings, once the size of the message exceeds ~65K, you will receive this exception:

“The maximum message size quota for incoming messages (65536) has been exceeded. To increase the quota, use the MaxReceivedMessageSize property on the appropriate binding element.”

The simplest fix is to increase the quota for MaxRecievedMessageSize in the client’s service configuration. This is only a temporary fix as eventually your message size may exceed the new limit. To help control the message size, a service should allow the client to page large sets of data.

Update the Service
To enable the client to page data, we need to allow them to specify a record offset and a page size. I prefer the LINQ to SQL terms “Skip” and “Take”. Skip is the zero-based record offset and Take is the maximum number of records per page. An alternative would be to specify a page size and page index, but I prefer the Skip-Take approach because it allows me to dynamically change the page size to control perceived performance.

So the first change to our NutrientDB service is to add a data contract to describe paging context.

[DataContract(Namespace=ServiceConstants.NAMESPACE)]

public class PagingContext

{

    [DataMember]

    public long Skip { get; set; }

    [DataMember]

    public int Take { get; set; }

}

Next we add the PagingContext to our GetAllFoodsRequest.

[MessageContract(WrapperNamespace=ServiceConstants.NAMESPACE)]

public class GetAllFoodsRequest

{

    [MessageBodyMember]

    public PagingContext PageInfo { get; set; }

}

Then we add the PagingContext to GetAllFoodsResponse. This allows a consumer of the service to know which page of results are being returned.

[MessageContract(WrapperNamespace = ServiceConstants.NAMESPACE)]

public class GetAllFoodsResponse

{

    [MessageBodyMember]

    public IEnumerable<Food> Results { get; set; }

    [MessageBodyMember]

    public PagingContext PageInfo { get; set; }

}

And finally, update our service operation to implement the paging.

public GetAllFoodsResponse GetAllFoods(GetAllFoodsRequest request)

{

    if (request == null) { throw new ArgumentNullException("request"); }

 

    PagingContext ctx = request.PageInfo

        ?? new PagingContext() {Skip = 0, Take = int.MaxValue};

 

    return new GetAllFoodsResponse()

    {

       Results = NutrientDataAccess.GetAllFoods(ctx.Skip, ctx.Take),

       PageInfo = ctx

    };

}

Update the Client
In the WPF client project, I right-click my service reference and choose “Update Service Reference”. This will break my existing code since the message contracts for my service changed. I now have to specify a PagingContext to the GetAllFoods methods. Also, I removed the 100 record limit from the database – it now returns the full ~7500 records.

Returning ~7500 Food objects in one request would throw the MaxReceivedMessageSize quota exception mentioned above. To prevent this (without having to update the client service configuration,) I update the client to retrieve all Food objects from the service 100 records at a time. I want to do this asynchronously so that the UI remains responsive.

private void LoadWCFAsync_Click(object sender, RoutedEventArgs e)

{

    NutrientDBClient client = new NutrientDBClient();

    client.GetAllFoodsCompleted +=

        new EventHandler<GetAllFoodsCompletedEventArgs>(

            client_GetAllFoodsCompleted);

 

    // Assign a new ObservableCollection to the ListView;

    // When items are later added to the ObservableCollection,

    // the ListView will automatically display them.

    _foodItems = new ObservableCollection<Food>();

    FoodList.ItemsSource = _foodItems;

 

    // Request the first 100 Food objects

    var pageInfo = new PagingContext() {Skip = 0, Take = 100};

    client.GetAllFoodsAsync(pageInfo);

}

 

void client_GetAllFoodsCompleted(object sender, GetAllFoodsCompletedEventArgs e)

{

    NutrientDBClient client = (NutrientDBClient)sender;

    if (e.Error == null)

    {

        // Use the PagingContext from the response to determine

        // if this is the last page of results

        PagingContext pageInfo = e.Result;

        if (e.Results.Length < pageInfo.Take)

        {

            client.Close();

        }

        else

        {

            // Request the next 100 Food objects asynchronously

            pageInfo.Skip += e.Results.Length;

            client.GetAllFoodsAsync(pageInfo);

        }

 

        // Update the ObservableCollection<Food> that has been

        // data bound to the ItemsSource property of the ListView.

        // Because _foodItems is an ObservableCollection, WPF will

        // automatically update the ListView with the new items.

        foreach (var food in e.Results)

        {

            _foodItems.Add(food);

        }

    }

    else

    {

        MessageBox.Show(e.Error.Message);

        client.Close();

    }

}

image image image

 

 

 

 

Now, instead of waiting for all 7539 records to load before beginning their work, a user can begin to browse the Food items after the first 100 results are returned. I load all pages at once, but this code could be easily modified to only request the next page when the user scrolls near the bottom of the list. Another modification that you might want to make is to request a smaller first page and larger subsequent pages. This would improve the perceived performance since the user will nearly instantly see their first page of results.

Summary
In this post I showed how you can add paging to your services to give clients better control over message size. At the end I touched on how paging can enable you to solve our third problem with large lists - “A noticeable delay to load data will begin to slow your users down.”

In the final post of this series, I will take this example to the next level to show how to abstract the paging logic out of your UI code, making it easily usable from anywhere in your client application.

Source code for this post can be downloaded here.

Tags:

WCF | WPF

Comments

1/31/2010 4:11:03 PM #

Matt Lowrance

Chad great post.

Our WCF datasets haven't gotten very large yet, but I can imagine over time that as the DBs increase in size this will be a problem.  

I hadn't thought about using LINQ style Skip & Take, I will definitely use this technique when the time comes.

Matt Lowrance United States

2/1/2010 6:08:27 AM #

Chad

Thanks for the comment Matt! I'm glad that you found this post useful.

Chad United States

2/14/2010 11:29:19 PM #

trackback

Windows Client Developer Roundup for 2/15/2010

This is Windows Client Developer roundup #11. The Windows Client Developer Roundup aggregates information

Community Blogs

Comments are closed

Powered by BlogEngine.NET 1.5.0.7
Theme by Mads Kristensen

About the author

Chad

Meeeee!!!

Hi, my name is Chad Boschert and I'm a software developer in Springfield, Missouri. I've been developing .NET applications in C# since 2002.

Recent Comments

Comment RSS

Calendar

<<  May 2013  >>
MoTuWeThFrSaSu
293012345
6789101112
13141516171819
20212223242526
272829303112
3456789

View posts in large calendar