Thursday, April 23, 2009

How to controll the ASP.NET ListView's page size using a DropDownList with available page sizes

Download the source code for this article
Issue

It's fairly easy to get a paged ASP.NET ListView. All you have to do is to add an asp:DataPager into the list's template and you are done.

However, the page size is set statically inside the DataPager template:

   1: <asp:DataPager ID="DataPager1" runat="server" PagedControlID="ListView1" PageSize="30">
   2:      <Fields>
   3:          <asp:NumericPagerField ButtonCount="8" ButtonType="Link" />
   4:      </Fields>
   5:  </asp:DataPager>

This HOWTO explains how to controll the page size dynamically, for example using an external DropDownList with available page sizes. Functional requirements are as follows:



  • the DropDownList should be initially bound to a value corresponding to the initial page size of the ListView (for example: we will have 5 available sizes on the list, 10, 20, 30, 50, 100 but the initial page size is 30. The DropDown should be then initially bound to the value 30)
  • as the DropDownList's selection changes, the ListView should automatically set the new page size

Solution

First question: how are we supposed to get the actual page size of the ListView?


First answer: we have to search the List's template to get the DataPager and ask the pager for the list's size.


Second question: How do we set a new page size of the ListView?


Second answer: After we are able to find the current DataPager, we can use it's SetPageProperties method to set a new page size.


Third question: How do we set the initial value of the drop down list?


Third answer: We can bind the selection to the DataPager's PageSize property.


It seems then that the most important issue here to solve is to be able to retrieve the ListView's current DataPager. This is how it's done:



   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5:  
   6: using System.Web.UI.WebControls;
   7: using System.Web.UI;
   8:  
   9: namespace WebApplication24
  10: {
  11:     public static class ListViewHelper
  12:     {       
  13:         /// <summary>
  14:         /// Retrieve current DataPager of the ListView
  15:         /// </summary>
  16:         public static DataPager GetActivePager( this ListView ListView )
  17:         {
  18:             DataPager Pager =
  19:                     ControlHelper.Find( ListView,
  20:                        control => control is DataPager &&
  21:                        ( (DataPager)control ).PagedControlID == ListView.ID ) as DataPager;
  22:  
  23:             if ( Pager != null )
  24:                 return Pager;
  25:             else
  26:                 return new DataPager();
  27:         }
  28:     }
  29: }

What is ControlHelper.Find( )? Well, it seems that the Control's FindControl method does not always work as expected. I got plenty of cases where FindControl does not find anything, while my custom Find finds what I expect to be found. Here it is:



   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Web;
   5: using System.Web.UI;
   6:  
   7: namespace WebApplication24
   8: {
   9:     public class ControlHelper
  10:     {
  11:         /// <summary>
  12:         /// Find a control by its ID 
  13:         /// </summary>
  14:         /// <param name="control"></param>
  15:         /// <param name="IdToFind"></param>
  16:         /// <returns></returns>
  17:         public static Control Find( Control control, string IdToFind )
  18:         {
  19:             if ( !string.IsNullOrEmpty( IdToFind ) )
  20:             {
  21:                 if ( control.ID == IdToFind )
  22:                 {
  23:                     return control;
  24:                 }
  25:                 foreach ( Control child in control.Controls )
  26:                 {
  27:                     Control result = Find( child, IdToFind );
  28:                     if ( result != null )
  29:                     {
  30:                         return result;
  31:                     }
  32:                 }
  33:             }
  34:  
  35:             return null;
  36:         }
  37:  
  38:         /// <summary>
  39:         /// Find a control with a predicate
  40:         /// </summary>
  41:         public static Control Find( Control control, Predicate<Control> MatchPredicate )
  42:         {
  43:             if ( MatchPredicate( control ) )
  44:             {
  45:                 return control;
  46:             }
  47:             foreach ( Control child in control.Controls )
  48:             {
  49:                 Control result = Find( child, MatchPredicate );
  50:                 if ( result != null )
  51:                 {
  52:                     return result;
  53:                 }
  54:             }
  55:             return null;
  56:         }
  57:  
  58:         /// <summary>
  59:         /// Find all controls with a predicate
  60:         /// </summary>
  61:         public static Control[] FindAll( Control Parent, Predicate<Control> MatchPredicate )
  62:         {
  63:             List<Control> ret = new List<Control>();
  64:  
  65:             FindAllHelper( Parent, MatchPredicate, ret );
  66:  
  67:             return ret.ToArray();
  68:         }
  69:  
  70:         private static void FindAllHelper( 
  71:             Control Parent, 
  72:             Predicate<Control> MatchPredicate, 
  73:             List<Control> results )
  74:         {
  75:             if ( MatchPredicate( Parent ) )
  76:                 results.Add( Parent );
  77:             // rekursja
  78:             foreach ( Control child in Parent.Controls )
  79:             {
  80:                 FindAllHelper( child, MatchPredicate, results );
  81:             }
  82:         }
  83:     }
  84: }

After we've prepared the basics, the rest is rather simple. The DropDownList will have the AutoPostBack set to true. After the DropDownList postbacks to the server, we'll set a new page size of the ListView:



   1: protected void SetPageSize_SelectedChanged( object sender, EventArgs e )
   2:  {
   3:      DropDownList pageSize = (DropDownList)sender;
   4:  
   5:      int NewSize = Convert.ToInt32( pageSize.Text );
   6:  
   7:      ListView1.GetActivePager().SetPageProperties( 0, NewSize, true );
   8:  }

Additionally, as a nice bonus, we'll fill a description of the ListView saying how many total rows are there and which rows are currently visible:



   1: protected void ListView1_DataBound( object sender, EventArgs e )
   2:  {
   3:      InitializeCountDescription();        
   4:  }
   5:  
   6:  private void InitializeCountDescription()
   7:  {
   8:      int _count = DataModel.Instance.Persons.Count;
   9:  
  10:      int start = ListView1.GetActivePager().StartRowIndex;
  11:      int end = Math.Min( _count, start + ListView1.GetActivePager().PageSize );
  12:  
  13:      Label lblItemCount = ControlHelper.Find( this, "lblListCount" ) as Label;
  14:      if ( lblItemCount != null )
  15:          lblItemCount.Text = string.Format( "{0}-{1} (from {2})", start + 1, end, _count );
  16:  }

Below is a screenshot from the example application.You can download the source code using the link provided at the top of the entry.


Monday, March 9, 2009

Valuable custom LINQ provider tutorials

There are plenty of useful tutorials on writing a custom LINQ Provider, however I find these two very valuable:

1. Old, good LINQ to LDAP by Bart De Smet

2. Surprisingly complete tutorial by Matt Warren where a LINQ-to-SQL-like custom provider is explained very thoroughly.

Do not also miss the LINQ's Dynamic Query helper and the MetaLINQ to discover other interesting variations on LINQ.

Friday, March 6, 2009

DevExpress XPO - incomplete Linq support

In general, XPO is great. I like it because of two features:

  • the ability to automatically update the database schema when connecting from the new version of the object model
  • the default "mark-as-deleted" way of removing objects 

There's however one scar on its beautiful face - incomplete Linq support.

No matter how fancy the Linq-To-Whatever implementation is - without Skip and Take it can never be fully adopted in enterprise systems. And XPO's implementation of Linq lacks the support of the Skip operation.

I am afraid that this is because XPO's implementation of paging is flawed by design: if you forget about Linq for a second, the only way to paginate is to use the XPPageSelector over your collection.

   1: /* initial collection*/
   2: XPCollection<TheType> ds = new XPCollection<TheType>(TheSession);
   3:  
   4: /* paging */
   5: XPPageSelector ps = new XPPageSelector(ds);
   6:  
   7: ps.PageSize = PageSize;
   8: ps.CurrentPage = PageNumber;
   9:  
  10: return ds;

If you run the above code and in the same time you trace queries which are executed, you'll learn that:



  1. first, the whole collection of objects' identifiers is retrieved from the data source

  2. the page selector selects the identifier subset which refers to the page you select

  3. then, another query retrieves all the columns but only from the selected page of items


   1: /* first query sent by xpo to retrieve all the data */
   2: select N0."OID" from 
   3: "dbo"."X_RejestrDostepu" N0 
   4: where N0."GCRecord" is null order by N0."Data" asc
   5:  
   6: /* xpo engine selects identifiers of objects in selected page */
   7:  
   8: /* another query - 
   9:    selected page is retrieved by manually providing identifiers 
  10: */
  11: exec sp_executesql N'select 
  12: N0."GCRecord",N0."OID", .... ,N0."OptimisticLockField" 
  13: from "dbo"."X_RejestrDostepu" N0
  14: where (N0."GCRecord" is null and N0."OID" 
  15: in (@p0,@p1,@p2,@p3,@p4,@p5,@p6,@p7,@p8,@p9))',
  16: N'@p0 int,@p1 int,@p2 int,@p3 int,@p4 int,@p5 int,@p6 
  17: int,@p7 int,@p8 int,@p9 int',@p0=31,@p1=32,@p2=33,@p3=34,@p4=35,@p5=36,@p6=37,@p7=38,@p8=39,@p9=40

And if you realize that there are in fact two queries required to retrieve selected page of data, then you imagine how ineffective (difficult?) this could be when implementing Linq's Skip this way!


It's then no surprise that the only answer to the "when the Skip operator will be supported?" is "sorry, Skip is not supported":


http://community.devexpress.com/forums/p/72977/248858.aspx


Edit: if you find this post in early 2010 or later please consider it outdated. Both skip and take are now correcly handled by XPO's Linq provider. I've blogged about it.

C# Puzzle No.13 (advanced)

Using LinqToSQL is fun and speeds up a lot of things. However, there are issues with LinqToSQL which confuse people. Ultimately, some of these people tend to think that LinqToSQL is useless because things that should be obvious are not such obvious. Over a year ago I wrote a short blog entry on one of such basic issues.

Another such "not-so-obvious" thing is the issue of "ordering". One of the common requirements is to be able to order by the name of the property and not by the property itself. For example, if you use ObjectDataSource, then you know that the sorting parameter is passed to the object responsible for data retrieving by name of the parameter.

Let's take a look at two LinqToSQL ordering attempts:

   1: ConcreteDataContext ctx = new ConcreteDataContext();
   2:  
   3: /* this is easy */
   4: var list = from elem in ctx.TheTable
   5:            orderby elem.Property
   6:            select elem;
   7:  
   8: /* this will compile
   9:    however it does not work
  10: */
  11: string PropertyName = "Property";
  12: var list = from elem in ctx.TheTable
  13:            orderby PropertyName
  14:            select elem;

In the example code above, the first linq clause is obvious - the ordering uses the property "Property" in a direct way. However, in in the second clause we use the name of the property.


You are to answer two following questions:


1. Why the second clause does not produce correct results, although it compiles correctly?


2. How is it possible then to build generic linq expressions which sort objects by names of their properties.


By generic I mean that following solution is not acceptable:



   1: switch ( PropertyName )
   2: {
   3:    case "Property":
   4:  
   5:       return from elem in ctx.TheTable
   6:              orderby elem.Property
   7:              select elem;
   8:  
   9:    case "AnotherProperty":
  10:  
  11:       return from elem in ctx.TheTable
  12:              orderby elem.AnotherProperty
  13:              select elem;
  14:  
  15:    ...
  16:  
  17: }