Mike Ormond's Blog

Musings on mobile development and Windows Phone 7 in particular.

ASP.NET 4.0 Enhancements to Data Controls

ASP.NET 4.0 Enhancements to Data Controls

  • Comments 6

Picking up where I left off some time back on ASP.NET 4.0, today we take a look at some of the enhancements we’re making to data controls in ASP.NET 4.0. Let’s start with the easy stuff.

Cleaner HTML

Over the years we’ve made significant advances in making the markup generated by ASP.NET server controls standards compliant, flexible and easy to style the way you want with CSS. Whether it be enhancements to the controls themselves, the introduction of new controls such as the ListView control or the creation of the CSS Friendly Control Adapters, all have contributed to greater flexibility and standards compliance in the generated markup.

One control that still suffers from enforcing a table-based layout is the FormView. The markup:

<asp:FormView ID="FormView1" runat="server">
  <ItemTemplate>
    <div>
      <%# Eval("MyData") %>
    </div>
  </ItemTemplate>
</asp:FormView>

Results in the following HTML:

<TABLE style='BORDER-COLLAPSE: collapse' id=TABLE1 border=0 cellSpacing=0>
  <TBODY>
    <TR>
      <TD colSpan=2>
        <DIV>Some Data </DIV>
      </TD>
    </TR>
  </TBODY>
</TABLE>

ie my ItemTemplate is wrapped in a <table> element and there’s nothing I can do about it. The FormView in ASP.NET 4.0 has a new property called RenderTable:

<asp:FormView ID="FormView1" RenderTable="false" runat="server">
  <ItemTemplate>
    <div>
      <%# Eval("MyData") %>
    </div>
  </ItemTemplate>
</asp:FormView>

Which results in the following when the page renders:

<div>
  Some Data
</div>

Simpifying Your Markup

The ListView control was introduced in ASP.NET 3.5 as a powerful, flexible alternative to the GridView control. It gives you complete control over the generated markup. It does have an obsession with LayoutTemplates and ItemPlaceholders though.

The following markup works fine in ASP.NET 4.0 but results in the YSOD in ASP.NET 3.5, complaining that “An item placeholder must be specified on ListView 'ListView1'. Specify an item placeholder by setting a control's ID property to "itemPlaceholder". The item placeholder control must also specify runat="server".

<asp:ListView ID="ListView1" runat="server">
  <ItemTemplate>
    <div>
      <%# Eval("MyData") %></div>
  </ItemTemplate>
</asp:ListView>

It just makes your life that little bit easier.

Persisted Selection

Persisted selection provides a more intuitive user selection experience on both the GridView and ListView controls. In ASP.NET 3.5, if the user selects an item presented in a GridView or ListView and then pages the control, a corresponding item will be selected on the new page. This is because the row selection is simply based on the row index on the page. Thus this ListView control:

<asp:ListView ID="ListView1" runat="server" DataSourceID="SqlDataSource1" DataKeyNames="HomeID">
  <ItemTemplate>
    <div>
      <asp:LinkButton CommandName="Select" runat="server">Select</asp:LinkButton>
      <%# Eval("ImageURL") %>
    </div>
  </ItemTemplate>
  <SelectedItemTemplate>
    <div>
      <em><%# Eval("ImageURL") %></em>
    </div>
  </SelectedItemTemplate>
</asp:ListView>

Results in the following paging experience for the user:

PersistedSelectionFalse1 PersistedSelectionFalse2 PersistedSelectionFalse3 PersistedSelectionFalse4

Setting EnablePersistedSelection="true" on the ListView (or GridView) changes that to the more intuitive experience below:

PersistedSelectionTrue1 PersistedSelectionTrue2 PersistedSelectionTrue3 PersistedSelectionTrue4

QueryExtenders

The QueryExtender control works in conjunction with either the LinqDataSource or EntityDataSource controls to modify the query expression generated by the DataSource control to apply additional filter operations (where clause) such as search, range and property matching. You can also include ordering and build your own custom expressions. Let’s take a look at an example starting with this simple LinqDataSource control which queries against my database of fictitious properties

<asp:LinqDataSource ID="LinqDataSource1" runat="server" ContextTypeName="DataClassesDataContext"
  EntityTypeName="" 
  Select="new (HomeID, Bedrooms, ImageURL, Price, Available, Description)" 
  TableName="Homes">
</asp:LinqDataSource>

Using SQL Server Profiler, I can see that this results in the following query against my DB

SELECT 
  [t0].[HomeID], 
  [t0].[Bedrooms], 
  [t0].[ImageURL], 
  [t0].[Price], 
  [t0].[Available]
FROM [dbo].[Homes] AS [t0]

By adding a QueryExtender I can modify the query expression and therefore the query that ultimately runs against my DB

<asp:QueryExtender ID="QueryExtender1" TargetControlID="LinqDataSource1" runat="server"> 
  <asp:OrderByExpression DataField="Price" Direction="Descending"></asp:OrderByExpression>
</asp:QueryExtender>

This results in

SELECT 
  [t0].[HomeID], 
  [t0].[Bedrooms], 
  [t0].[ImageURL], 
  [t0].[Price], 
  [t0].[Available], 
  [t0].[Description]
FROM [dbo].[Homes] AS [t0]
ORDER BY [t0].[Price] DESC

Of course I can take things a lot further by adding additional expressions to my QueryExtender thus

<asp:QueryExtender ID="QueryExtender1" TargetControlID="LinqDataSource1" runat="server"> 
  <asp:OrderByExpression DataField="Price" Direction="Descending"></asp:OrderByExpression>
  <asp:RangeExpression DataField="Price" MinType="Inclusive" MaxType="Inclusive">
    <asp:Parameter DefaultValue="200000" DbType="Int32" />
    <asp:Parameter DefaultValue="750000" DbType="Int32" />
  </asp:RangeExpression>
  <asp:PropertyExpression>
    <asp:Parameter Name="Available" DefaultValue="True" DbType="Boolean" />
  </asp:PropertyExpression>
  <asp:SearchExpression DataFields="Description" SearchType="Contains">
    <asp:Parameter DefaultValue="private" DbType="String" />
  </asp:SearchExpression>
</asp:QueryExtender>

Which results in the following T-SQL executing against my DB

exec sp_executesql 
N'SELECT [t0].[HomeID], 
  [t0].[Bedrooms], 
  [t0].[ImageURL], 
  [t0].[Price], 
  [t0].[Available], 
  [t0].[Description]
FROM 
  [dbo].[Homes] AS [t0]
WHERE 
  ([t0].[Description] LIKE @p0) AND 
    ([t0].[Available] = @p1) AND 
    ([t0].[Price] >= @p2) AND ([t0].[Price] <= @p3)
ORDER BY 
  [t0].[Price] DESC',
N'@p0 nvarchar(4000),@p1 int,@p2 int,@p3 int',
@p0=N'%private%',
@p1=1,
@p2=200000,
@p3=750000

So the QueryExtender gives me a simple, but powerful declarative mechanism for customising my queries with either the EntityDataSource or LinqDataSource controls by taking advantage of the the deferred execution and extensible nature of LINQ expressions.

Technorati Tags: ,,
  • Excellent news!

    Presumably the RenderTable property will be extended to the Login, PasswordRecovery and ChangePassword controls? The control adapter to remove the unit table from these controls under .NET 2 / 3.5 is a mess of reflection calls.

  • Hi Richard. I've just checked and that's not the case in the beta build. Of course that may change between now and release. Mike

  • I hope it does. For reference, this is the simplest workaround I could come up with:

    using System;

    using System.Reflection;

    using System.Security;

    using System.Security.Permissions;

    using System.Web;

    using System.Web.UI;

    using System.Web.UI.Adapters;

    using System.Web.UI.WebControls;

    namespace Web.UI.Adapters

    {

       using FCCSS = Func<ControlCollection, string, string>;

       [AspNetHostingPermission(SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)]

       public sealed class ChangePasswordAdapter : ControlAdapter

       {

           private static readonly Type ContainerType = FindContainerType();

           private static readonly FCCSS SetCollectionReadOnly = FindSetCollectionReadOnlyMethod();

           private static Type FindContainerType()

           {

               Assembly asm = typeof(HttpContext).Assembly;

               return asm.GetType("System.Web.UI.WebControls.LoginUtil+GenericContainer`1", false, true);

           }

           private static FCCSS FindSetCollectionReadOnlyMethod()

           {

               const BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic;

               MethodInfo method = typeof(ControlCollection).GetMethod("SetCollectionReadOnly",

                   flags, null, new Type[] { typeof(string) }, null);

               if (null == method) return null;

               return (FCCSS)Delegate.CreateDelegate(typeof(FCCSS), method, true);

           }

           protected override void Render(HtmlTextWriter writer)

           {

               if (null != ContainerType && null != SetCollectionReadOnly)

               {

                   Control control = this.Control;

                   if (control is ChangePassword || control is Login || control is PasswordRecovery)

                   {

                       // NB: Setting the render method delegate sets the child control

                       // collection to read-only. Since the login controls create their

                       // child controls from the render method, we need to clear the

                       // read-only flag to avoid an exception.

                       control.SetRenderMethodDelegate(RenderChildren);

                       SetCollectionReadOnly(control.Controls, null);

                   }

               }

               base.Render(writer);

           }

           private static void RenderChildren(HtmlTextWriter output, Control container)

           {

               if (container.HasControls())

               {

                   foreach (Control child in container.Controls)

                   {

                       if (null != child && child.Visible)

                       {

                           Type childType = child.GetType().BaseType;

                           if (childType.IsGenericType && ContainerType == childType.GetGenericTypeDefinition())

                           {

                               if (child.HasControls())

                               {

                                   foreach (Control c in child.Controls)

                                   {

                                       c.RenderControl(output);

                                   }

                               }

                           }

                           else

                           {

                               child.RenderControl(output);

                           }

                       }

                   }

               }

           }

       }

    }

  • What's the limitation / reason you're not just using the CSS Friendly Control Adapters implementation? Mike

  • The last time I looked at the CSS Friendly adapters for the Login, ChangePassword and PasswordRecovery controls, they seemed to be completely replicating or replacing the functionality of the controls they were adapting. I just wanted something that would remove the unit table from the control, whilst leaving the rest of the functionality intact. ~1200 lines of C# plus nearly 100 lines of javascript seems a bit overkill for that!

    I've posted a suggestion on Connect for the RenderTable property to be added to these controls:

    https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=471384

  • I've just been notified that this option will be extended to the security controls for beta 2:

    "We are providing the option to remove wrapping outer table that you see on all the controls you listed in .NET 4. FormView was all that we shipped in Beta one but the following beta release will add this to the other controls as well."

Page 1 of 1 (6 items)