65.9K
CodeProject 正在变化。 阅读更多。
Home

如何使用自定义表达式生成器提供类型和成员的声明式、强类型引用

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (16投票s)

2009年2月4日

CPOL

8分钟阅读

viewsIcon

59972

downloadIcon

466

本文提供了一个如何实现和使用自定义表达式生成器的示例,以扩展 ASP.NET 页面的编译时支持。

引言

ASP.NET 2.0 引入了一种新的声明式表达式语法,可以在页面标记中为控件属性指定值时使用。大多数 ASP.NET 开发人员都曾见过或写过类似的代码

<asp:Label Text="<%$ Resources: Headings, CustomerDetailsPage %>" />

<asp:Literal Text="<%$ AppSettings: SiteName %>" />

像这样的声明式表达式在编译时被解析,然后在运行时计算生成的代码表达式并绑定到目标属性。表达式的前缀映射到一个已知的表达式生成器,该生成器负责解析和计算表达式的所有工作,表达式是冒号后面的部分。ASP.NET 2.0 中有三个内置的表达式生成器:

  • AppSettingsExpressionBuilder:允许在运行时通过设置的键获取配置中 AppSettings 的设置。
  • ConnectionStringsExpressionBuilder:允许在运行时通过设置的键获取配置中 ConnectionStrings 的设置。
  • ResourceExpressionBuilder:在运行时计算为当前文化的全局或本地资源字符串,使用资源的键和类名。

其中,ResourceExpressionBuilder 可能是最常用的,因为它提供了简单的、声明式的服务器控件本地化,无需额外的代码或数据绑定。

表达式生成器的一个隐藏优势是,它可以用于在编译时执行自定义代码,为页面编译过程提供一个钩子,用于执行诸如反射、文件验证、URL 验证等任务。在需要尽量消除否则只能在运行时捕获的错误的情况下,提供编译时支持很有用。

与 ASP.NET 2.0 中的几乎所有其他功能一样,您也可以使用自己的自定义表达式生成器来扩展表达式生成器框架,而这正是我们可以真正开始使用表达式强大功能的地方。

教程

让我们从 ASP.NET 中的一个已知问题开始,看看如何开发一个自定义表达式生成器来解决它。有几个控件定义了引用类型名称和成员名称的属性,其中最值得注意的是 ObjectDataSource 控件,它将用作本文其余部分的示例。这些属性作为字符串在标记或代码隐藏中指定,控件在运行时解析类型和成员信息,如果类型信息不正确,则会抛出错误。

如果我们以原始字符串的形式在标记或代码隐藏中定义类型和成员名称,那么我们就无法获得编译器的支持,错误直到页面执行时才能被捕获。当然,我们也可以在代码隐藏中使用静态引用来引用类型

myDataSource.TypeName = typeof(MyComponents.MyBusinessObject).FullName;

只要类型已为编译器所知,这实际上就能让我们在不使用反射的情况下对类型本身进行编译时验证。然而,此方法无法验证成员,并且强制使用代码隐藏。

理想的解决方案应该是声明式的,并在 IDE 和目标应用程序编译上下文中提供对类型和成员的编译时验证——幸运的是,这正是自定义表达式生成器可以提供的。

首先,我们定义要使用的表达式语法、表达式解析和计算时的预期行为以及返回值。表达式总是以这种形式出现:

<%$ [prefix]: [expression] %>

其中 [prefix] 是映射到可用表达式生成器的关键字,[expression] 是将由目标生成器计算的原始字符串数据。表达式数据可以是任何有效的字符串,以我们选择的任何格式。对于此示例,我们将使用以下语法:

<%$ Reflect: TypeName[, MemberName] %>

其中 TypeName 是强制性的,必须是类型的完整名称MemberName 是成员的名称。MemberName 是可选的。如果只提供类型名称,则表达式将类型名称作为字符串返回;否则,如果提供了成员名称,则返回该成员。这意味着,我们希望我们的标记看起来像这样:

<asp:ObjectDataSource ID="MySource" Runat="server"
    TypeName="<%$ Reflect: MyComponents.MyBusinessObject %>" 
    SelectMethod="<%$ Reflect: MyComponents.MyBusinessObject, GetSearchResults %>"
    ... />

表达式计算后,将生成以下标记:

<asp:ObjectDataSource ID="MySource" Runat="server"
    TypeName="MyComponents.MyBusinessObject" 
    SelectMethod="GetSearchResults"
    ... />

前缀的选择完全是任意的——我选择了最能识别表达式生成器所执行功能的名称,但您可以在自己的实现中选择任何名称。此外,我们只期望以这种方式解析公共类型和成员,并且我们应该确保类型解析是区分大小写的。如果无法解析类型或成员名称,我们希望在编译时看到异常,或者在页面未编译时在执行时看到异常。

让我们先实现自定义表达式生成器的类。

using System;
using System.CodeDom;
using System.Linq;
using System.Security.Permissions;
using System.Web;
using System.Web.Compilation;
using System.Web.UI;

namespace CustomExpressionBuilderSample
{
    [ExpressionPrefix("Reflect")]
    [AspNetHostingPermission(SecurityAction.InheritanceDemand, 
                             Level=AspNetHostingPermissionLevel.Minimal)]
    [AspNetHostingPermission(SecurityAction.LinkDemand, 
                             Level=AspNetHostingPermissionLevel.Minimal)]
    public class ReflectExpressionBuilder : ExpressionBuilder    
    {    

    }
}

主要要求是我们继承自 ExpressionBuilder,它定义在 System.Web 程序集中的 System.Web.Compilation 命名空间中。我们还用 ExpressionPrefixAttribute 装饰该类,传入我们用于映射到表达式生成器的前缀,这为这些表达式提供了有用的设计时支持。我添加了宿主属性来充实生成器,但它们不是必需的。

下一步是决定是否需要在计算表达式之前解析它。在这种情况下,我们将解析表达式以确保它匹配我们预期的格式,并执行类型和成员验证。每当编译页面时,都会为表达式前缀创建一个表达式生成器,并在生成器上调用 ParseExpression 方法,该方法会传递原始表达式,除非我们重写此方法并提供自己的解析逻辑。

让我们添加 ParseExpression 实现,它使用自定义方法 ValidateExpression 来验证类型信息。

/// <summary>
/// Parses and validates the expression data and returns a canonical type or member name, 
/// or throws an exception if the expression is invalid.
/// </summary>
/// <param name="expression">The raw expression to parse.</param>
/// <param name="propertyType">The target property type.</param>
/// <param name="context">Contextual information for the expression builder.</param>
/// <returns>A string representing the target type or member name for binding.</returns>
public override object ParseExpression(string expression, 
                Type propertyType, ExpressionBuilderContext context)
{
    bool parsed = false;
    string typeName = null;
    string memberName = null;
    
    if (!String.IsNullOrEmpty(expression))
    {
        var parts = expression.Split(',');
        if (parts.Length > 0 && parts.Length < 3)
        {
            if (parts.Length == 1)
            {
                typeName = parts[0].Trim();
            }
            else if (parts.Length == 2)
            {
                typeName = parts[0].Trim();
                memberName = parts[1].Trim();
            }
        
            parsed = true;
        }
    }
    
    if (!parsed)
    {
        throw new HttpException(String.Format("Invalid Reflect" + 
                  " expression - '{0}'.", expression));
    }
    
    // now validate the expression fields
    return ValidateExpression(typeName, memberName);
}

/// <summary>
/// Validates that the specified type and member name
/// can be resolved in the current context.
/// Member name resolution is optional.
/// </summary>
/// <param name="typeName">The full name of the type.</param>
/// <param name="memberName">The member name to resolve, or null to ignore.</param>
/// <returns>The type or member name as a string
///        for binding to the target property.</returns>
private string ValidateExpression(string typeName, string memberName)
{
    // resolve type name first
    Type resolvedType = null;
    foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
    {
        resolvedType = assembly.GetType(typeName, false, false);
        if (resolvedType != null)
        {
            break;
        }
    }
    
    // if type was not resolved then raise error
    if (resolvedType == null)
    {
        var message = String.Format("Reflect Expression: Type '{0}' could " + 
                      "not be resolved in the current context.", typeName);
        throw new HttpCompileException(message);
    }
    
    // resolve the member name if provided - don't care about multiple matches
    string bindingValue = typeName;
    if (!String.IsNullOrEmpty(memberName))
    {
        bindingValue = memberName;
        if (!resolvedType.GetMember(memberName).Any())
        {
            var message = String.Format("Reflect Expression: Member '{0}' " + 
                          "'for type '{1}' does not exist.", memberName, typeName);
            throw new HttpCompileException(message);    
        }
    }
    
    return bindingValue;
}

通过添加这两个方法,我们实际上已经完成了大部分工作——ParseExpression 确保表达式声明有效,而 ValidateExpression 确保类型和成员参数可以解析,并返回相应的绑定值。

接下来,我们需要提供一种方法在编译时计算表达式生成器。一旦 ParseExpression 返回表达式值,就会调用 GetCodeExpression 方法,该方法需要返回一个 CodeDom 元素,该元素可以包含在页面的编译树中,当执行时,它会呈现表达式的值。关于 CodeDom 本身的讨论远远超出了本文的范围,但幸运的是,对于本示例,我们只需要一个非常简单的代码表达式,因为我们不需要在表达式被解析和验证后执行任何运行时计算。

GetCodeExpression 方法如下所示:

/// <summary>
/// Returns a CodeDom expression for invoking the expression from a compiled page at runtime.
/// </summary>
/// <param name="entry">The entry for the bound property.</param>
/// <param name="parsedData">The parsed expression data.</param>
/// <param name="context">The expression builder context.</param>
/// <returns>A <see cref="CodeExpression" /> for invoking the expression.</returns>
public override CodeExpression GetCodeExpression(BoundPropertyEntry entry, 
                object parsedData, ExpressionBuilderContext context)
{
    return new CodePrimitiveExpression((string) parsedData);
}

确实非常简单。parsedData 参数是调用 ParseExpression 的结果,在我们的例子中,它是字符串形式的类型名称或成员名称,所以我们所要做的就是将其包含在输出编译树中作为一个原始文字。我们使用 CodePrimitiveExpression 类的新实例来实现这一点。

到目前为止,我们的实现已经完成,我们现在可以在 Web 应用程序中使用此表达式生成器了。但是,为了完整起见,我们还应该支持根本不编译页面,而是在运行时解析、验证和计算表达式的情况。这不会是我们表达式生成器最有价值的用法,但为了本文的目的,我们还是会涵盖它。

为了支持运行时计算,我们需要重写基类的一个属性,并重写 EvaluateExpression 方法,如下所示:

/// <summary>
/// Gets a flag that indicates whether the expression builder
/// supports no-compile evaluation.
/// Returns true, as the target type can be validated at runtime as well.
/// </summary>
public override bool SupportsEvaluate
{
    get { return true; }
}

/// <summary>
/// Evaluates the expression at runtime.
/// </summary>
/// <param name="target">The target object.</param>
/// <param name="entry">The entry for the property bound to the expression.</param>
/// <param name="parsedData">The parsed expression data.</param>
/// <param name="context">The current expression builder context.</param>
/// <returns>A string representing the target type or member.</returns>
public override object EvaluateExpression(object target, BoundPropertyEntry entry, 
                object parsedData, ExpressionBuilderContext context)
{
    return (string) parsedData;
}

由于我们使用反射来验证类型和成员,因此我们可以固有地提供运行时支持。我们重写 SupportsEvaluate 属性以返回 true(默认为 false),并提供 EvaluateExpression 的实现,在这种情况下,它只需要返回解析数据的字符串强制转换。

这完成了我们自定义表达式生成器的实现——最后一步是将生成器集成到我们的 web.config 文件中,并开始编写使用它的表达式。可以如下配置生成器:

<system.web>
    <compilation>
        <expressionBuilders>
            <add expressionPrefix="Reflect" 
               type="CustomExpressionBuilderSample.ReflectExpressionBuilder, 
                     CustomExpressionBuilderSample" />
        </expressionBuilders>
    </compilation>
</system.web>

现在,您可以开始在任何期望类型和成员名称的控件上添加对类型和成员的强类型引用,并且知道任何相关错误将在编译页面时被捕获。我在 Web 应用程序项目中的一个技巧是在我的生成脚本中将 ASP.NET 编译器作为后期生成操作调用,以便网站中的所有页面都得到编译。

C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\aspnet_compiler.exe 
     -m /LM/W3SVC/1/ROOT -f PrecompiledOutput

这与任何反射表达式都能很好地配合使用,并且有助于防止可能 unnoticed 地进入生产环境的错误。

进一步调查

自定义表达式生成器可以重写的其他方法有很多,但我尚未看到它们的实际用途。还可以通过将 ExpressionDesignerAttribute 添加到表达式生成器来扩展自定义表达式的设计时支持,该属性引用一个提供表达式值组合设计时 UI 的类型。对于本示例,您可以创建一个控件,允许用户在程序集列表中搜索类型,或搜索类型上定义的特定成员。您可以使用 Reflector 检查 System.Web.UI.Design 中的现有表达式设计器,以获取有关实现方式的更多信息。

返回值格式很大程度上是根据与 ObjectDataSource 控件的交互推断出来的,但您可以随时更改表达式语法以支持更多返回值的格式,具体取决于您的需求。

我还建议查看 System.CodeDom 命名空间,并使用 Reflector 检查 AppSettingsExpressionBuilderResourceExpressionBuilderConnectionStringsExpressionBuilder 的内置实现,以获取更多关于如何实现自己的自定义表达式生成器的想法。

© . All rights reserved.