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: }

Wednesday, February 18, 2009

Beware when you upgrade ...

It's a real pity when you realize that you are truly disappointed by a new version of a software you use.

When Windows Media Player 11 came out, I've upgraded from WMP 10 immediately. To me, a new version always means new functions, enhanced compatibility, stability and security.

I got no problems with WMP 11 until I've bought two great soundtracks by Jeremy Soule from DirectSong. These soundtracks are protected by the DRM - you are required to obtain a valid license from the soundtrack provider to be able to play it with the Windows Media Player.

To be able to backup your downloads, DirectSong says that:

Our current licensing is set up to allow unlimited Soundtrack CDs to be burned per machine download.

Great, this does not work in WMP 11 - you learn from the WMP that the license does not allow you to burn, synchronize or even play the music (which does not stop the music from actually playing!).

After few nervous hours spent trying to find out what's the problem, I've learned from someone's blog that you have to downgrade to WMP 10 to get this to work.

I don't remeber beeing so disappointed by a new version of the Microsoft's software for a long time. A new version which breaks the backward compatibility is not something which I could accept.

Friday, February 6, 2009

Inject application-specific code into ASP.NET module

We often write ASP.NET modules to customize ASP.NET processing pipeline - extend security, etc.

   1: public class ExampleModule : IHttpModule
   2:  {
   3:      #region IHttpModule Members
   4:  
   5:      public void Dispose() { }
   6:  
   7:      public void Init( HttpApplication context )
   8:      {
   9:          context.BeginRequest += new EventHandler( context_BeginRequest );
  10:      }
  11:  
  12:      void context_BeginRequest( object sender, EventArgs e )
  13:      {
  14:          // extend the processing pipeline by providing
  15:          // custom code for particular events, in this case
  16:          // the BeginRequest event
  17:      }
  18: }

However, as modules are static, in a sense that a module is active or inactive in processing pipeline, applications which "host" modules are dynamic, in a sense that different applications work under different conditions.


The issue we've faced recently was to inject a custom, application specific code into module:



   1: void context_BeginRequest( object sender, EventArgs e )
   2: {
   3:     // how to call an application-defined code
   4:     // but do it so that the module is not
   5:     // rewritten everytime application changes
   6:     
   7:     // ?
   8: }

The solution we've came up with was rather simple: we've used a custom provider to provide application-specific code.



   1: <system.web>
   2:     <specificProvider defaultProvider="ApplicationSpecificProvider">
   3:         <providers>
   4:             <add name="ApplicationSpecificProvider" type="Namespace.ApplicationSpecificProvider" />
   5:         </providers>
   6:     </specificProvider>

This way we can configure different providers on the application level but still be able to write generic module code:



   1: void context_BeginRequest( object sender, EventArgs e )
   2: {
   3:     HttpApplication app = (HttpApplication)sender;
   4:  
   5:     SpecificProvider.DoSpecificOperation( app );
   6: }