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

声明式 QueryString 参数绑定

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (69投票s)

2004 年 3 月 3 日

10分钟阅读

viewsIcon

236435

downloadIcon

1125

介绍如何使用反射从 Form 和 Querystring 自动填充成员参数。

引言

本文讨论了一种策略,用于从 ASP.NET 页面通过 `Request` 对象(`.QueryString`、`.Form` 等)的属性自动填充实例字段和属性。

问题

如果您有参数化的 ASP.NET 页面——即根据请求的 GET(`Request.QueryString`)或 POST(`Request.Form`)部分的一些输入来修改其输出的页面——那么您可能会花费大量时间编写参数检索代码,这些代码可能一开始是这样的:

protected string FirstName{
    get{ return Request.QueryString["FirstName"]; }
}

...但是您必须处理 `FirstName` 在代码其余部分中可能为 `null` 的情况,因此您可能通过重写为(注意:方便的 `IsNull` 方法,我们将在整个代码中使用):

protected static string IsNull(string test, string defaultValue){
    return test==null ? defaultValue : test;
}

protected string FirstName_NullSafe{
   get{ return IsNull(Request.QueryString["FirstName"],""); }
}

这种方法对于非字符串类型也同样适用,您只需要在属性中添加 `Convert.ToXXX()` 语句,并决定当参数为 `null` 时该怎么做——提供默认值,或抛出异常。

protected int CustomerID{
    get{
        object o=Request.QueryString["CustomerID"];
        if (o==null)
            throw new 
               ApplicationException("Customer ID is required to be passed");
        else
            try{
                return Convert.ToInt32(o,10);
            }catch(Exception err){
                throw new ApplicationException("Invalid CustomerID", err);
            }
        }
    }
}

真糟糕。原本简单的访问器开始变得臃肿。而且这段代码每次访问 `CustomerID` 时都会运行,因此这是不必要的开销——而且直到您第一次访问它时才运行,所以您会在(昂贵的?)处理过程中途被那个异常打断。

很可能在这个阶段,我们会重构整个混乱的代码,并在 `Page_Load`(或 `OnInit` 的末尾)执行所有这些工作。

private void Page_Load(object sender, System.EventArgs e) {
    string o;

    // This was the one we set a default for

    FirstName2 =IsNull(Request.QueryString["FirstName"], "");

    // This one is required

    o =Request.QueryString["CustomerID"];
    if (o==null)
        throw new 
          ApplicationException("Customer ID is required to be passed");
    else
        try{
            CustomerID2 = Convert.ToInt32(o,10);
        }catch(Exception err){
            throw new ApplicationException("Invalid CustomerID", err);
        }

        // This one's an enum (just to make life interesting)

        o =Request.QueryString["Gender"];
        if (o==null)
            throw new ApplicationException("Gender is required");
        else
            try{
                Gender =(Gender)Enum.Parse(typeof(Gender), o, true);
            }catch(Exception err){
                throw new ApplicationException("Invalid Gender", err);
            }
}

现在,您只需要这样做几次,就会出现一个清晰的模式。无论您是填充字段还是属性(即 ViewState 包装器),都会涉及到几个标准的行为:

  • 根据键(可能与字段名相同)从 `Request.QueryString`(或 `Request.Form`)检索值。
  • 如果未提供值,则抛出适当的异常,或提供默认值。
  • 将值转换为字段/属性的适当类型。
  • 如果值转换不正确,则抛出适当的异常(或者再次,提供默认值)。

好了,如果我不够懒惰,那我就不是一个合格的程序员了,而对于这种枯燥重复的工作,我可是非常懒惰的。那么有什么替代方案呢?

解决方案:声明式参数绑定

嗯,一个解决方案就是“标记”我们想要加载的字段和属性,并加上适当的元数据(指定默认值、要绑定的键以及在哪个集合中),然后让某些库代码来执行实际工作。

有点像这样:

[WebParameter()]
protected string FirstName;

[WebParameter("Last_Name")]
protected string LastName;

[WebParameter(IsRequired=true)]
protected int CustomerID;

可选的构造函数参数仅提供在 `Request.QueryString` 集合中查找参数的键,而 `IsRequired` 属性则切换缺失值是异常条件还是跳过条件。

突然之间,大量的代码被浓缩到几个属性中,这使得一眼就能看清发生了什么,并且更容易维护。现在我们只需要在某种辅助类中实现我们刚刚裁剪掉的所有通用逻辑。这会使用反射来检查 `Page` 类,找到所有用属性标记的属性(或字段),并自动从 `QueryString` 进行相关赋值。

(如果您以前从未在 .NET 中使用过反射,那么以下是您需要了解的内容才能理解代码:

  • 在运行时,每个对象都有一个 `Type`,从中我们可以获得暴露该 `Type` 的字段和属性的表示。
  • 这些表示——`FieldInfo` 和 `PropertyInfo`——共享一个共同的基类——`MemberInfo`。
  • `MemberInfo` 的子类提供了一种与对象成员进行“后期绑定”交互的方式,而不是必须在编译时预先指定将执行什么。因此,`PropertyInfo` 允许您在对象上进行该属性的 `get`/`set` 操作,而无需在源代码中硬编码 `object1.property1=myValue`。

如果您有兴趣,网上有很多教程。)

第一步就是这样做,只需遍历类(此处分配给 `target`)中的属性和成员,然后将每个成员(以及当前的 `HttpRequest`(`request`))传递给另一个将实际执行工作的方​​法。

public static void SetValues(object target, System.Web.HttpRequest request)
{
    System.Type type =target.GetType();
    FieldInfo[] fields = 
        type.GetFields(BindingFlags.Instance | BindingFlags.Public);
    PropertyInfo[] properties = 
        type.GetProperties(BindingFlags.Instance | BindingFlags.Public);
    MemberInfo[] members =new MemberInfo[fields.Length + properties.Length];
    fields.CopyTo(members, 0);
    properties.CopyTo(members, fields.Length);

    for(int f=0;f<members.Length;f++)
        SetValue(members[f], target, request);
}

这一步唯一有点令人困惑的是,我们将 `properties` 和 `fields` 数组合并到一个 `members` 数组中,这只是意味着我们只需要循环一次。所有实际工作都在 `SetValue`(单数!)中完成,我们将在下一节讨论。现在这里有很多代码,但一旦拆解开,它的作用就很清楚了。

首先,我们排除没有用 `WebParameterAttribute`(最外层的 `if`)标记的成员,由于没有简单的方法来处理索引参数,我们会在标记了它们时抛出错误(您也可以选择静默跳过该成员)。

public static void SetValue(MemberInfo member, 
        object target, System.Web.HttpRequest request)
{
    WebParameterAttribute[] attribs;
    WebParameterAttribute attrib;
    TypeConverter converter;
    string paramValue;
    string paramName;
    object typedValue;
    bool usingDefault;

    try
    {
        attribs = (WebParameterAttribute[])
          member.GetCustomAttributes(typeof(WebParameterAttribute), true);
        if(attribs!=null && attribs.Length>0)
        {
            // Just make sure we're not going after an indexed property

            if (member.MemberType==MemberTypes.Property)
            {
                ParameterInfo[] ps =
                  ((PropertyInfo)member).GetIndexParameters();
                if (ps!=null && ps.Length>0)
                  throw new NotSupportedException("Cannot apply " + 
                  "WebParameterAttribute to indexed property");
            }

现在我们获取来自属性的各种设置,以及(字符串)参数值本身。

            // There should only be one

            // WebParameterAttribute (it's a single-use attribute)

            attrib =attribs[0];
            paramName =(attrib.ParameterName!=null) ? 
                            attrib.ParameterName : member.Name;
            paramValue =attrib.GetValue(paramName, request);

请注意,是属性本身提供了我们想要的实际参数值,它自己决定使用 `HttpRequest` 的哪个部分。将像这样非核心活动(如选择使用 `Request.Form` 或 `Request.QueryString`)的责任委托出去,可以实现更灵活的解决方案。在我们的例子中,`WebParameter`——我们到目前为止唯一讨论过的属性——根据提交表单的方法选择 `Request.Form` 或 `Request.QueryString`。或者,像 `QueryParameter` 和 `FormParameter` 这样的其他专用子类可以执行与请求特定部分的绑定,并且可以根据需要轻松创建其他子类,用于 `Request.Cookies` 或 `Request.Session`。

如果属性返回 `null`,则表示在 `Request` 中未找到该参数。我们的选择相当简单:

  • 使用属性的 `DefaultValue`(如果已提供)。
  • 如果属性标记为 `IsRequired`,则抛出错误。
  • 通过返回调用方法来完全跳过此成员。
            // Handle default value assignment, if required

            usingDefault =false;
            if (paramValue==null)
            {
                if (attrib.DefaultValue!=null)
                {
                    paramValue =attrib.DefaultValue;
                    usingDefault =true;
                }
                else if (!attrib.IsRequired)
                    return; // Just skip the member

                else
                    throw new 
                        ApplicationException(String.Format("Missing " + 
                        "required parameter '{0}'", paramName));
                }

现在(终于),我们可以实际获取字符串并将其赋给成员。我们这里有几个辅助方法只是为了让代码更简单(我不会在此处重现完整的源代码,但所有内容都在示例文件中)。

  • GetMemberUnderlyingType(...) 返回 `MemberInfo` 表示的 `Type`,即返回 `.FieldType` 或 `.PropertyType`。
  • SetMemberValue(...) 封装了执行反射字段/属性赋值所需的代码。

我使用 `TypeConverter` 来执行实际的字符串转换,因为它将字符串解析的责任委托给了外部世界(这总是一个好主意)。如果您以前没遇到过它们,它们的要点是它们是一个完全可扩展的方案,用于将类型转换例程与它们转换到的/从它们转换来的类型关联起来。它们主要用于控件开发(这就是 VS.NET IDE 的属性浏览器的工作方式),但它们太有用了,不能仅用于设计器,我已经开始到处使用它们了。

                // Now assign the loaded value onto the member,

                // using the relevant type converter

                // Have to perform the assignment slightly

                // differently for fields and properties

                converter = 
                 TypeDescriptor.GetConverter(GetMemberUnderlyingType(member));
                if (converter==null || 
                  !converter.CanConvertFrom(paramValue.GetType()))
                    throw new 
                      ApplicationException(String.Format("Could not" + 
                      " convert from {0}", paramValue.GetType()));

                try
                {
                    typedValue =converter.ConvertFrom(paramValue);
                    SetMemberValue(member, target, typedValue);
                }
                catch
                {
                    // We catch errors both from the type converter

                    // and from any problems in setting the field/property

                    // (eg property-set rules, security, readonly properties)


                    // If we're not already using the default, but there

                    // is one, and we're allowed to use it for invalid data, 

                    // give it a go, otherwise just propagate the error


                    if (!usingDefault && attrib.IsDefaultUsedForInvalid 
                                                && attrib.DefaultValue!=null)
                    {
                      typedValue =converter.ConvertFrom(attrib.DefaultValue);
                      SetMemberValue(member, target, typedValue);
                    }
                    else
                        throw;
                }
            }

最后,我们捕获在此字段处理过程中可能出现的任何异常,并将其包装在标准错误消息中。

        }
        catch(Exception err)
        {
            throw new ApplicationException("Property/field " + 
               "{0} could not be set from request - " + err.Message, err);
        }
    }

呼。

现在,所有这些代码的显而易见的位置是放在一个基类 `Page` 中,但我对创建不必要的 `Page` 子类有 真正的问题,所以我的实现的“引擎”只是 `WebParameter` 类本身的一个静态方法。我将这么多代码放在一个属性类上感觉有点不妥,但也不是世界末日。

private void Page_Load(object sender, System.EventArgs e)
{
    WebParameterAttribute.SetValues(this, Request);
}

需要显式调用它也节省了在不需要时产生的开销,并允许您选择何时绑定:第一次;回发;每次请求——您的选择将取决于您的页面做什么以及您是绑定到字段还是视图状态属性访问器。

完整示例

在页面中导入了相关的命名空间(和程序集)后,实际需要的所有代码就是非常简短的代码(这是从包含的示例中提取的):

public class WebParameterDemo : System.Web.UI.Page
{
    [QueryParameter("txtFirstName")]
    public string FirstName ="field default";

    [QueryParameter("txtLastName", DefaultValue="attribute default")]
    public string LastName{
        get{ return (string)ViewState["LastName"]; }
        set{ ViewState["LastName"]=value; }
    }

    [QueryParameter("txtCustomerID", DefaultValue="0")]
    public int CustomerID;

    private void Page_Load(object sender, System.EventArgs e)
    {
        WebParameterAttribute.SetValues(this, Request);
    }
}

优点

  • 简洁、简单的方法提高了可读性和可维护性。
  • 集中式模式意味着在 `Page` 层面需要测试的事情少了一项。
  • 页面的 API 是“自文档化的”。
    • 用于纸质文档。
    • 用于应用程序中的其他页面,这些页面可能希望构造对您的页面的请求(前提是它们知道要设置的属性/字段)。
    • 自动化测试(例如:构造请求以使页面崩溃)。

另请注意,不一定只能绑定到 `Page` 类,任何类都可以被“绑定”,尽管除了 `UserControl` 之外,很少有类会直接绑定到 `Request.Form` 或 `Request.QueryString`(特别是因为您可以提供一个页面级属性来间接绑定)。

您可能想绑定的 `Request` 的其他部分:

  • Session
  • ServerVariables
  • Headers
  • 当前的 `HttpContext.Items`

我曾考虑为控制台应用程序使用类似的方法,但事实证明 已经有人做了(或非常类似的东西),所以如果我让您信服了,请查看 CommandLineOptions 并放弃所有那些 `args[]` 解析的废话。谢谢 Gert。

缺点

我非常喜欢这种方法,它给了我更多时间来编写让我兴奋的复杂、曲折的代码。然而,它有一个缺点,那就是性能。就像任何基于反射的东西一样,它的速度会比同等的直接调用慢;我遍历了大量的不必要属性来查找被绑定的属性(而 `Page` 类中的公共属性并不少);并且我使用 `TypeConverter` 来进行字符串转换,这又是更多的反射。

话虽如此,反射只比正常情况慢一点。当然,您不会在 3D 渲染例程中使用它,但与数据库访问、网络延迟以及 Web 应用程序固有的其他问题相比,它算不了什么。您正在使用的所有 ASP.NET 数据绑定——都是基于反射的。不过,如果您实在着急,您可以通过以下方式加速:

  • 在 `WebParameterAttribute.SetValues()` 中限制 `GetFields()` / `GetProperties()` 调用,使其仅返回您的代码隐藏中声明的成员(`BindingFlags.DeclaredOnly`),而不是包含所有基类成员(注意:由于您的代码隐藏本身就是子类以构建最终页面,这意味着您也必须将其 `Type` 传递给 `GetValues`)。
  • 提供一个接口来检索要绑定的 `MemberInfo[]` 成员数组,而不是通过反射来查找它们。
  • 查看使用带属性过滤器的 `TypeDescriptor.GetProperties()` 是否比“原始”反射更快。
  • 在您的页面中逐个成员显式调用 `SetValue()`,而不是调用 `SetValues()`。
  • 在默认使用 `TypeConverter` 之前,提供几个“内置”的类型转换步骤。

最后,我应该指出,在实际应用中,我不会像这样硬编码异常消息——我会将它们放在资源文件中。即使我无意全局化我的应用程序,这也使重用该程序集的人(包括我自己)更容易修改消息以适应他们的需求,而无需搜索源代码。

历史

2004-02-15 - 首次发布,延迟多时。

附录 1 - 为什么不继承 `System.Web.UI.Page`

在 OO 编程的“正常”世界中,人们倾向于构建小的辅助对象,这些对象可以在其他类中被使用/聚合以执行特定的功能。然而,当涉及到 ASP.NET 时,会存在太多的诱惑,将您方便的通用功能写成一个基页类,其明显的缺点是,要使用它,您必须继承它,这意味着您不能继承其他东西。

此外,`Page` 类很难继承——它有很多属性和方法,您不能使任何东西成为抽象的(或者拥有返回 `null` 的属性,除非您适当地标记它们),否则设计器就会出问题。所以您的对象层次结构会变成一堆虚方法,这些方法会抛出类似“此方法应该在子类中被重写”的异常。真糟糕。最重要的是,设计器会变得非常缓慢,因为它必须创建您的页面创建的所有东西(如果涉及数据库访问,这尤其慢),并且您可能无法获得所有依赖程序集的预期的 `.config` 文件。

我的建议是,可以有几个基页类,但要保持简单,并将大部分更复杂的东西聚合为辅助对象(即使它们必须传递对其包含的 `Page` 的引用)。

© . All rights reserved.