每周源代码47-ASP.NET 3.5动态数据:FilterRepeaters 和动态 Linq查询生成

[原文发表地址] The Weekly Source Code 47 - ASP.NET 3.5 Dynamic Data: FilterRepeaters and Dynamic Linq Query Generation

[原文发表时间] January 13, 2010 7:38 AM

首先,在发帖之前,让我表示对Tatham Oddie的感谢。他从澳大利亚帮助我和我的伙伴John Batdorf远程调试我们的问题。Tatha耐心友好、有见解,而且他有个很好的博客 您现在就应该订阅 我也从Stephen Naughton出色的博客中得到很多启发。他一直致力于推动使用ASP.NET 和Dynamic Data做有趣的事情,我觉得他95%的自动完成代码都能为我所用。最后,我将以Marcin Dobosz的博客动态数据实例 Filter Repeaters开始,并以动态数据功能实例 结束。

技术免责声明: 我只是把 .NET 3.5 SP1 动态数据实例随便摆弄了一下用来做非盈利的 .NET 3.5 。我的Filters来自2009年5月VS2008 SP1 动态数据功能 但是没有**功能,并且没有 .Visual Studio 2010 Beta 2中的 .NET4酷。 虽然一些方法名称和基类变了,但是实际上理念是一样的。如果正在使用 .NET 4 ,你会注意到当你执行文件 | 新项目选项时,得到的是 ~/DynamicData/Filters文件和一个新的 QueryableFilterRepeater ,而不是AdvancedFilterRepeater ,诸如此类。在我的3.5项目中,我在解决方案中包含了DynamicDataExtensions 项目,但在 .NET 4中,所有的这些功能都来自System.Web.DynamicData本身。当 .NET 4临近发布时,我会更新这个项目并在博客上发表一个实例。

好吧,故事是:在我们丰富的业余时间里,我和John在做一些志愿者工作并更新一个本地非营利性网站。他们的需求很简单,在前端他们要一个简单的自动完成文本框,或者可能要一两个能下拉的过滤器。这会将用户发送到一个结果网页,网页有不可编辑的分页网格和用于打印的动态生成的图像。在后台,他们要几个提供给管理员使用的下拉表单以及一个不错的可编辑网格供搜索,分页等。

我们都认为这将会是一个不错的机会,看看可不可以在短时间内把它放到一起,同时更多的了解ASP.NET Web Forms和动态数据。这是非常多变的,所以我们决定我们最好也要变化(记住,这实际上是说“非特定的解决方案”。我们想在其它项目中能够用我们为ASP.NET 动态数据创建的任何东西。)

Meta Meta

当用ASP.NET 动态数据创建一个网站时,你创建的网页是模板——它们是一种网页——但本身不是网页的实例。所以,我创建了一个List.aspx,它不特定用于(在我的例子中)Brick项目。它用于列我解决方案中想列的任意一种对象。

这是我动态数据解决方案中list.aspx的一个简化例子。我们关心的是它有:

  • LinqDataSource
  • GridView
  • AdvancedFilterRepeater——再说一下,这是在3.5 Dynamic Data Futures中的名字。它是ASP.NET标准Filter Repeater的扩展。在它ItemTemplate中有一个DelegatingFilter。

就是这样。

    1: <ContentTemplate>  
    2:   <asp:ValidationSummary ID="ValidationSummary1" runat="server" EnableClientScript="true"        HeaderText="List of validation errors" />  
    3:   <asp:DynamicValidator runat="server" ID="GridViewValidator" ControlToValidate="GridView1" Display="None" />    
    4:   <asp:AdvancedFilterRepeater id="AdvancedFilterRepeater" runat="server">   
    5:      <HeaderTemplate>     
    6:        <table>     
    7:      </HeaderTemplate>   
    8:      <ItemTemplate>       
    9:        <tr>          
   10:             <td valign="top"><%# Eval("DisplayName") %>:</td>   
   11:             <td><asp:DelegatingFilter runat="server" ID="DynamicFilter" OnSelectionChanged="OnFilterSelectedIndexChanged" />   
   12:        </tr>     
   13:      </ItemTemplate>     
   14:      <FooterTemplate>   
   15:          </table>      
   16:      </FooterTemplate> 
   17:    </asp:AdvancedFilterRepeater>  
   18:    <asp:GridView ID="GridView1" runat="server" DataSourceID="GridDataSource"        AllowPaging="True" AllowSorting="True" CssClass="gridview">  
   19:       <Columns>        
   20:           <asp:TemplateField>      
   21:                <ItemTemplate>       
   22:                     <asp:HyperLink ID="DetailsHyperLink" runat="server"                        NavigateUrl='<%# table.GetActionPath(PageAction.Details, GetDataItem()) %>'                        Text="Details" />    
   23:             </ItemTemplate>      
   24:       </asp:TemplateField>   
   25:      </Columns>       
   26:   <PagerStyle CssClass="footer"/>        
   27:         <PagerTemplate>      
   28:       <asp:GridViewPager runat="server" />   
   29:      </PagerTemplate>     
   30:    <EmptyDataTemplate>       
   31:      There are currently no items in this table.     
   32:    </EmptyDataTemplate>  
   33:   </asp:GridView>  
   34:    <asp:LinqDataSource ID="GridDataSource" runat="server" EnableDelete="false" EnableInsert="false" EnableUpdate="false"> 
   35:        <WhereParameters>      
   36:       <asp:DynamicControlParameter ControlID="AdvancedFilterRepeater" />   
   37:      </WhereParameters>  
   38:   </asp:LinqDataSource>     
   39:   </ContentTemplate>

AdvancedFilter repeater很棒,在Marcin的博客上有介绍,而且在结尾还附有样例Stephen Naughton在他精采的“Dynamic Data Futures”系列博文中进一步扩展了FilterRepeater

FilterRepeater是一个特殊的Repeater控件,可以绑定一个表(list<object>)中列的集合(属性),并且动态产生允许过滤的控件。

例如,这是我的Brick对象模型(实际上是我Brick对象的“buddy metadata class”)。它是数据库里边东西的镜像。

    1: [MetadataType(typeof(Brick_MD))]
    2: public partial class Brick
    3: {  
    4:   public class Brick_MD   
    5:   {      
    6:        [Filter(FilterControl = "AutoComplete")]  
    7:        public object Name { get; set; }   
    8:  
    9:        [Filter(FilterControl = "Integer", AuthenticatedOnly = true)]  
   10:        public object Square { get; set; }       
   11:  
   12:        [Filter(FilterControl = "Integer")]      
   13:        public object Year { get; set; }     
   14:        public object Side { get; set; }    
   15:        public object Row { get; set; }   
   16:        public object BrickNum { get; set; } 
   17:        public object Order { get; set; } 
   18:    } 
   19: }

注意Filter属性。我已经用叫做AuthenticatedOnly的属性扩展过它的属性,这样只有验证过的用户(非匿名)可以看见过滤器。我也说过一个属性/列需要一个AutoComplete样式的过滤器(不用对执行特定化)和其它两个Integer样式(也就是,执行非特定元数据。可以是组合、移动,随便一个。)

ASP.NET 动态数据使用一个习惯上称为DynamicData的特殊文件夹,该文件夹有子文件夹,比如:存放特定种类的域(Integer, DateTime, Person等)的FieldTemplates,以及存放特定网页的(List, Edit, Details 等)的PageTemplate。

FilterRepeater样本用一个~/DynamicData/Filters文件夹扩展了这个概念。如果你想看看 “Filters”目录在代码中的什么地方被用到,它是在FilterFactor里的GetFilterControl方法中设置的。需要注意的是,在我的元-模型中有个写着“AutoComplete”的字符串,按照惯例该字符串映射到~/DynamicData/Filters/AutoComplete.ascx控件,就像“Integer”映射到Integer.ascx,等等。

例如,整个Integer.ascx是这样的:

    1: <%@ Control Language="C#" AutoEventWireup="true" CodeFile="Integer.ascx.cs" Inherits="AdvancedFilterRepeaterSite.Integer_Filter" %>
    2: <asp:DropDownList ID="DropDownList1" runat="server" AutoPostBack="true" EnableViewState="false">   
    3:     <asp:ListItem Text="All" Value="" />
    4: </asp:DropDownList>

这只是个下拉列表。当FilterRepeater在Brick_MD对象检查列/属性的时候,它会说,“oh,我要在这放一个Integer.ascx”,它就会有想要的数据。像Integer这样的所有控件都由FilterUserControlBase派生出来,只需重写他们想要改变的行为的属性或者方法。

所有这些都很简单,你可以用它做出不错的东西。

动态生成特定的泛型LINQ查询

但是,当我们发现创建的DropDownList没有事先按值排序时,事情就有些令人不快了。这会产生奇怪的下拉列表,intergers的顺序是数据库中已有的顺序。

你会认为在你的Integer Filter Control后面,需要一些像这样的代码:

    1: protected void Page_Init(object sender, EventArgs e)
    2: {
    3:     var items = Column.Table.GetQuery(); 
    4:     //magic psuedo code here
    5:     var result = items.Select(row => row.Property).Distinct.OrderBy(colvalue => colvalue)
    6:      foreach (var item in result) { 
    7:        if (item != null) DropDownList1.Items.Add(item.ToString());
    8:     }
    9: }

注意代码中“magic”的部分。就是我们希望能导出的LINQ查询,但是不要忘记我不想查询一个特定的表或者特定的列。如果我这样做的话,那就不是Integer Filter Control,而是Year Filter Control 或者 Month Filter Control。这是特定的,不是泛型。

我们需要动态创建一个LINQ查询。但是就像你不想通过连接字符串制作一个动态SQL,LINQ也是这样。我们会用System.Linq.Expressions.Expression基类创建代表我们LINQ查询的树。另一个隐喻/例子是用XmlDocuments 和 DOM 而不是用一串字符串制作 XML。

我们实际想让这个查询具体化。每次Integer Filter Control运行都是具体到我们目前正在为之所做控件的列(可以说,甚至类型都可能是除整数之外的其他东西)。

    1: Items.Select(row => row.Property).Distinct.OrderBy(colvalue => colvalue)

我们需要为此查询构建基块,使其可以从任何“行”开始。

    1: // row
    2: var entityParam = Expression.Parameter(Column.Table.Ent   1: 2 ityType, "row"); 

因为是FilterUserControlBase, 我们可以为Column.Table.EntityType中的typeof(row)请求动态数据。我们会用动态数据提供给我们的类型和信息创建这个查询。

下一步,我们通过传递(编译)我们刚刚做的entityParam得到行的属性。现在“columnLambda”是 LambdaExpression类型。

    1: // row => row.Property
    2: var columnLambda = Expression.Lambda(Expression.Property(entityParam, Column.EntityTypeProperty), entityParam);

再下一步是Select。在这我们动态生成一个LINQ函数调用。实际上,这是LINQ的Reflection概念。Type[]是参数。记住Select调用看起来是这样的:Select<TSource, TResult>(IEnumerable<TSource>, Func<TSource, TResult>) 我们只提供它需要的东西。

    1: // Items.Select(row => row.Property)
    2: var selectCall = Expression.Call(typeof(Queryable), "Select", new Type[] { items.ElementType, columnLambda.Body.Type }, items.Expression, columnLambda);

现在,我们只添加Distinct到先前的调用。看注释。看见我们是怎么在先前selectCall expression基础上构建的了吗?

    1: // Items.Select(row => row.Property).Distinct
    2: var distinctCall = Expression.Call(typeof(Queryable), "Distinct", new Type[] { Column.EntityTypeProperty.PropertyType }, selectCall);

这是Tatham让我们免于遭受的损失。我得以转身,他把我疯狂的单行方式破成更清晰的代码。Eilon Lipton也帮助我直接得到这些。

在第一个表达式,“sortValue”字符串不起作用。只是要有个东西,我们可以做一个x=>xlambda表达式。

第二步,我们调用标准LINQ OrderBy,赋予它(x=>x),或者在这个例子中是year=>year,因此真正是,

Items.Select(brick => brick.Property).Distinct.OrderBy(year => year)

    1: // colvalue => colvalue
    2: var sortParam = Expression.Parameter(Column.EntityTypeProperty.PropertyType, "sortValue");
    3: var columnResultLambda = Expression.Lambda(sortParam, sortParam);
    1: // Items.Select(row => row.Property).Distinct.OrderBy(colvalue => colvalue)
    2: var ordercall = Expression.Call(typeof(Queryable), "OrderBy",  
    3:           new Type[] { Column.EntityTypeProperty.PropertyType, columnResultLambda.Body.Type },
    4:           distinctCall, columnResultLambda);

然后我们真实调用查询,填下拉表单,这是整个过程:

    1: protected void Page_Init(object sender, EventArgs e) {
    2:     var items = Column.Table.GetQuery();
    3:     var entityParam = Expression.Parameter(Column.Table.EntityType, "row");
    4:      
    5:     // row => row.Property
    6:     var columnLambda = Expression.Lambda(Expression.Property(entityParam, Column.EntityTypeProperty), entityParam)
    7:  
    8:     // Items.Select(row => row.Property)
    9:     var selectCall = Expression.Call(typeof(Queryable), "Select", new Type[] { items.ElementType, columnLambda.Body.Type }, items.Expression, columnLambda);
   10:  
   11:     // Items.Select(row => row.Property).Distinct
   12:     var distinctCall = Expression.Call(typeof(Queryable), "Distinct", new Type[] { Column.EntityTypeProperty.PropertyType }, selectCall);
   13:  
   14:     // colvalue => colvalue
   15:     var sortParam = Expression.Parameter(Column.EntityTypeProperty.PropertyType, "sortValue");    var columnResultLambda = Expression.Lambda(sortParam, sortParam);
   16:      
   17:     // Items.Select(row => row.Property).Distinct.OrderBy(colvalue => colvalue)
   18:     var ordercall = Expression.Call(typeof(Queryable), "OrderBy",
   19:                 new Type[] { Column.EntityTypeProperty.PropertyType, columnResultLambda.Body.Type },
   20:                 distinctCall, columnResultLambda);
   21:  
   22:      var result = items.Provider.CreateQuery(ordercall);
   23:  
   24:      foreach (var item in result) {
   25:         if (item != null) DropDownList1.Items.Add(item.ToString());
   26:     }
   27: }

那最后结果怎样呢?

最后结果

现在我可以添加 [Filter(FilterControl = "Integer")] 到我任意一个ASP.NET 动态数据网站的任意一个元模型,自动得到一个不错的简单Fliter设置,完全由属性驱动,操作独立于我的逻辑且对我的表单完全通用。

要想看完整的实例,请查看VS2008 SP1 Dynamic Data Futures from May of 2009。为了增加使我烦恼的OrderBy,我修改(其实很勉强)了Integer.acsx.cs。我需要查看所有代码以找出这几行。

我也修改了FilterAttribute以增加AuthenticatedOnly属性,像 [Filter(FilterControl = "Integer", AuthenticatedOnly = true)] :

    1: using System;
    2:  
    3: namespace Microsoft.Web.DynamicData.Extensions {
    4:     [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple=false)]
    5:     public sealed class FilterAttribute : Attribute {
    6:  
    7:          public FilterAttribute() {
    8:             Order = Int32.MaxValue;
    9:             Enabled = true;
   10:         }
   11:          public string FilterControl { get; set; }
   12:  
   13:          // Lower values take precedence before greater values
   14:          public int Order { get; set; }
   15:               
   16:          public bool Enabled { get; set; }     
   17:          public bool AuthenticatedOnly { get; set; }
   18:     }
   19: }

……以及AdvancedFilterRepeater来按照新属性执行:

    1: using System;
    2: using System.Collections.Generic;
    3: using System.Linq;
    4: using System.Web.DynamicData;
    5: using System.Web.UI.WebControls;
    6: using System.Web;
    7:  
    8: namespace Microsoft.Web.DynamicData.Extensions {
    9:     public class AdvancedFilterRepeater : FilterRepeater {
   10:  
   11:         protected override IEnumerable<MetaColumn> GetFilteredColumns() {
   12:             // sort the filters by their filter order as specified in FilterAttribute.
   13:             return Table.Columns.Where(c => IsFilterableColumn(c)).OrderBy(column => column, new FilterOrderComparer());
   14:         }
   15:  
   16:         protected bool IsFilterableColumn(MetaColumn column) {
   17:             if (column.IsCustomProperty) return false;
   18:  
   19:             var filterAttribute = column.Attributes.OfType<FilterAttribute>().FirstOrDefault();
   20:             if (filterAttribute != null)
   21:             {
   22:                 if (filterAttribute.AuthenticatedOnly == true && (HttpContext.Current.User == null || HttpContext.Current.User.Identity.IsAuthenticated == false)) return false;
   23:  
   24:                 return filterAttribute.Enabled;
   25:             }
   26:  
   27:             if (column is MetaForeignKeyColumn) return true;
   28:  
   29:             if (column.ColumnType == typeof(bool)) return true;
   30:  
   31:             return false;
   32:         }
   33:  
   34:         private class FilterOrderComparer : IComparer<MetaColumn> {
   35:             public int Compare(MetaColumn x, MetaColumn y) {
   36:                 return GetWeight(x) - GetWeight(y);
   37:             }
   38:  
   39:             private int GetWeight(MetaColumn column) {
   40:                 var filterAttribute = column.Attributes.OfType<FilterAttribute>().FirstOrDefault();
   41:                 return filterAttribute != null ? filterAttribute.Order : Int32.MaxValue;
   42:             }
   43:         }
   44:  
   45:     }
   46: }

声明:我是在 .NET 3.5 SP1上做的这些。 我自己仍在学习这些更深的东西,我要说的是尽管这些元-LINQ很强大,但太复杂不能生成动态LINQ。也请记住本帖只是你想做的最最外层。它不代表是典型的ASP.NET Dynamic Data经验,也不代表典型LINQ经验。但是,得知整件事情是可扩展的,如果进一步研究,是可能的,真实太好了。我要去跟那个团队交谈,看看他们未来有什么计划!如果我们漏掉了某些东西,留在评论里或发邮件给我,我会更新本帖。我会找出ASP.NET Dynamic Data 4 C# 4有哪些改进了。

相关链接

好的ASP.NET WebForms资源和博客

Webforms Q&A