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

SqlWhereBuilder ASP.NET 服务器控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (135投票s)

2005年1月4日

13分钟阅读

viewsIcon

1016854

downloadIcon

14753

一个用户界面 Web 控件,用于构建适用于 SQL 语句的条件。

引言

SqlWhereBuilder 是一个 Web 控件,它提供了一个用户界面,用于生成自定义 SQL WHERE 子句。该控件旨在支持即席报表需求,用户通过界面添加条件,开发人员在回发时使用 GetWhereClause()GetWhereClauseWithParameters() 方法将选定的条件编译成适合包含在 SQL WHERE 子句中的文本字符串。SqlWhereBuilder 控件的开发考虑了以下因素:

  • 应在客户端处理用户交互,从而避免每次操作条件都需要服务器回发。
  • 在可能的情况下,利用 ASP.NET 提供的服务器端功能。

为了满足前一个考虑因素,客户端功能被开发为一个独立的 JavaScript 库。该控件已在 IE 6.0、FireFox 1.0 和 Netscape 7.1 中进行了测试,并且应该可以与任何支持 JavaScript 1.2、document.getElementById() 函数、innerHTML 属性以及 CSS 样式 display 属性(值为 noneinline)的浏览器一起使用。单击以下链接可下载客户端 JavaScript 库

在解决后一个考虑因素时,该控件支持通过 XML 文件进行开发人员配置,并且可以通过生成与 IDbCommand 对象兼容的 WHERE 子句语法来与 IDbCommand 类型集成。本文档介绍了该控件,描述了开发人员的配置任务,并演示了如何检索 WHERE 子句。还介绍了用于渲染的技术,以及用于在客户端 JavaScript 库和 ASP.NET 服务器端控件之间来回传递条件集的技巧。

关于控件

SqlWhereBuilder 用户界面由以下视觉元素组成:

  • 条件列表

    用户添加的条件在此区域中显示。

  • 字段下拉列表

    字段下拉列表提供了数据库字段列表;用户选择一个字段即可开始一个条件。

  • 运算符下拉列表

    选择字段后,运算符下拉列表将提供由开发人员配置的、适合该字段的比较运算符列表。例如,文本字段可能包含运算符 "Is"、"Is Not"、"Contains" 和 "Is Null"。数字字段可能包含运算符,如 "Equals" 和 "Is Greater Than"。运算符具有关联的sqlTemplates,用于将条件翻译成适合 SQL WHERE 子句的语法。

  • ValueEntry 区域

    选择运算符后,将显示其关联的ValueEntry 区域,为用户提供输入适合该运算符的比较值的方法。ValueEntry 区域被渲染为 <div> 标签,这些标签在客户端根据运算符的选择而显示或隐藏。ValueEntry 区域可能包含文字文本和 HTML;客户端库支持类型为 textradio<input> 标签,以及 <select> 标签。ValueEntry 区域也可以使用 UserControl (.ascx) 定义,前提是该控件渲染支持的表单输入。

上面的示例显示了一个由单个文本框组成的 ValueEntry 区域。下面的示例显示了一个为 Date 字段定义的 "is between" 运算符,其中有两个 <input type="text"> 标签用于 ValueEntry 区域。

下一个示例 ValueEntry 源自一个 UserControl,该控件使用 Northwind 数据库中的员工姓名填充 DropDownList

当用户通过界面添加条件时,这些条件会出现在条件列表区域,并且每个条件左侧都有删除和编辑按钮。添加一个条件后,输入表单中还会出现And/Or 下拉列表,允许用户选择适合其标准的 SQL 联接词。

当单击某个条件的编辑按钮时,通常位于底部用于添加新条件的输入表单将被移动到内联编辑选定的条件。

配置

要使用 SqlWhereBuilder Web 控件,开发人员必须将 JavaScript 库代码文件复制到服务器上的相应位置。开发人员还必须定义用户可用的 ValueEntry 区域、OperatorLists 和 Fields。这通常通过 XML 文件完成。

客户端 JavaScript 库

SqlWhereBuilder 控件的所有客户端功能都构建在 JavaScript 文件 SqlWhereBuilder.js 中。该控件期望在以下位置找到该文件(其中 wwwroot 是服务器的 Web 根文件夹):

wwwroot/aspnet_client/UNLV_IAP_WebControls/SqlWhereBuilder

将文件 SqlWhereBuilder.js 复制到上述文件夹路径,它将可用于每个 SqlWhereBuilder 实例。要指定客户端 JavaScript 文件的备用位置,请相应地设置 SqlWhereBuilder 实例的 ClientCodeLocation 属性(有关更多信息,请参阅控件文档)。

XML 配置文件

除了识别客户端库之外,SqlWhereBuilder 实例的配置通常通过 XML 文件处理。配置文件通过 ValueEntryFileOperatorListsFileFieldsFile 属性进行标识。虽然存在将这些作为单独文件提供的灵活性,但这并非严格必要;所有配置标记都可以放在一个文件中,并且该文件可以根据需要标识在三个属性中的每个属性中。

ValueEntry 区域使用具有以下属性的 <valueEntry> 标签定义:

  • id - 此 ValueEntry 区域的唯一标识符,供运算符引用。
  • userControl - (可选) 要为此 ValueEntry 区域渲染的 UserControl (.ascx) 的虚拟路径。

此示例显示了一个 ValueEntryFile,其中定义了四个条目区域:一个用于单个文本框,一个为空(用于不需要额外用户输入的运算符),一个带有用于选择的下拉框,还有一个由外部 UserControl 定义。

<configuration>

    <valueEntry id="onetext">
        <input type="text" id="onetext_1" size="10" />
    </valueEntry>
    
    <valueEntry id="blank">
        <!-- left intentionally blank -->
    </valueEntry>
    
    <valueEntry id="region">
        <select id="region_select1">
            <option value="N">North</option>
            <option value="S">South</option>
            <option value="E">East</option>
            <option value="W">West</option>
        </select>
    </valueEntry>
    
    <valueEntry id="customers"
                userControl="CustomersDropdown.ascx" />

</configuration>

定义 ValueEntry 区域时,重要的是为每个表单输入提供一个 id 属性(例如,上面示例中 "onetext" 输入的 "onetext_1")。表单输入 id 由运算符的 sqlTemplate 属性引用。对于单选按钮组,则由 sqlTemplate 引用 name 属性。

运算符分组到OperatorLists 中,分别通过 <operator><operatorList> 标签定义。OperatorList 提供一组适合特定字段的运算符。OperatorLists 可以被认为是松散地绑定到特定的数据类型(如文本、数字或日期数据类型),并为该数据类型的字段提供适当的运算符选择。也可以定义自定义 OperatorLists,例如,限制标准数据类型的选择,或为派生自 UserControl 的 ValueEntry 区域提供自定义选择。OperatorLists 有一个属性:

  • id - 此 OperatorList 的唯一标识符,供字段引用。

运算符使用以下属性定义:

  • id - 此运算符的唯一标识符。
  • text - SqlWhereBuilder 输入表单中“运算符”下拉列表的显示文本。
  • valueEntry - 关联的 ValueEntry 区域的 id;从下拉列表中选择此运算符时,将同时显示关联的 ValueEntry 区域。
  • sqlTemplate - 用于定义使用此运算符的条件如何转换为有效 SQL 语法的模板字符串。

sqlTemplate 属性在符合 SQL 的条件中使用了占位符。字面占位符 #FIELD# 代替条件中的字段名。ValueEntry 区域中的表单输入使用源自输入 id 属性(或单选按钮组情况下的 name 属性)的占位符,并使用井号 (#) 作为分隔符。例如,如果 ValueEntry 区域定义了一个文本输入 id="onetext_1",则 sqlTemplate 中的占位符将是 #onetext_1#

设计 sqlTemplate 时还有一个重要的考虑因素:WHERE 子句是构建为文字字符串(使用 GetWhereClause() 方法)还是带参数占位符的字符串(使用 GetWhereClauseWithParameters() 方法)。如果使用前者,则应在运算符的 sqlTemplate 属性中包含数据类型的适当分隔符(例如,文本类型的单引号)。如果使用后者,则不需要数据类型分隔符;在编译的 WHERE 子句中,valueEntry 输入占位符将被替换为适当的 IDbDataParameter 占位符。以下示例显示了一个用于文本数据类型的 "equals" 比较运算符,使用前者方法,包含文本分隔符的单引号:

<operator id="text_is" text="Is" valueEntry="onetext" 
          sqlTemplate="#FIELD# = '#onetext_1#'" />

为与 IDbDataParameter 对象一起使用而定义的相同运算符(GetWhereClauseWithParameters() 方法)将如下所示(不带单引号作为分隔符):

<operator id="text_is" text="Is" valueEntry="onetext" 
          sqlTemplate="#FIELD# = #onetext_1#" />

以下示例显示了一个 OperatorListsFile,其中定义了五个列表:一个用于通用文本数据类型,一个用于布尔条件,一个用于数字数据类型,一个用于区域选择,还有一个利用上面 ValueEntry 示例中定义的 "customers" ValueEntry 区域。这些运算符假定将使用 GetWhereClauseWithParameters() 方法来编译 WHERE 子句,因此不使用数据类型分隔符。

<configuration>
  <operatorList id="opList_text">
    <operator id="opList_text_is" text="Is" valueEntry="onetext"
              sqlTemplate="#FIELD# = #onetext_1#" />

    <operator id="opList_text_isnot" text="Is Not" valueEntry="onetext"
              sqlTemplate="#FIELD# != #onetext_1#" />

    <operator id="opList_text_isnull" text="Is Null" valueEntry="blank"
              sqlTemplate="#FIELD# IS NULL" />
  </operatorList>

  <operatorList id="opList_boolean">
    <operator id="opList_boolean_true" text="Is True" valueEntry="blank"
              sqlTemplate="#FIELD# = 1" />

    <operator id="opList_boolean_false" text="Is False" valueEntry="blank"
              sqlTemplate="#FIELD# = 0" />

    <operator id="opList_boolean_null" text="Is Null" valueEntry="blank"
              sqlTemplate="#FIELD# IS NULL" />

    <operator id="opList_boolean_notnull" text="Is Not Null" valueEntry="blank"
              sqlTemplate="#FIELD# IS NOT NULL" />
  </operatorList>

  <operatorList id="opList_numeric">
    <operator id="opList_numeric_equals" text="Equals" valueEntry="onetext"
              sqlTemplate="#FIELD# = #onetext_1#" />

    <operator id="opList_numeric_notequals" text="Does Not Equal" 
              valueEntry="onetext"
              sqlTemplate="#FIELD# != #onetext_1#" />

    <operator id="opList_numeric_gt" text="Is Greater Than"
              valueEntry="onetext"
              sqlTemplate="#FIELD# &gt; #onetext_1#" />

    <operator id="opList_numeric_lt" text="Is Less Than" valueEntry="onetext"
              sqlTemplate="#FIELD# &lt; #onetext_1#" />
  </operatorList>

  <operatorList id="opList_region">
    <operator id="opList_region_is" text="Is" valueEntry="region"
              sqlTemplate="#FIELD# = #region_select1#" />

    <operator id="opList_region_isnot" text="Is Not" valueEntry="region"
              sqlTemplate="#FIELD# != #region_select1#" />
  </operatorList>

  <operatorList id="opList_customers">
    <operator id="opList_customers_is" text="Is" valueEntry="customers"
              sqlTemplate="#FIELD# = #customers_ddCustomers#" />

    <operator id="opList_customers_isnot" text="Is Not" valueEntry="customers"
              sqlTemplate="#FIELD# != #customers_ddCustomers#" />
  </operatorList>

</configuration>

字段通过具有以下属性的 <field> 标签定义:

  • id - 此字段的唯一标识符;id 应与数据库中的字段名相同。
  • text - SqlWhereBuilder 输入表单中“字段”下拉列表的显示文本。
  • operatorList - 关联的 OperatorList 的 id;在输入表单中选择此字段时,运算符下拉列表将填充由 operatorList 定义的运算符组。
  • parameterDataType - 使用 GetWhereClauseWithParameters() 方法时要包含的 IDbDataParameter 对象的 System.Data.DbType;如果改为使用 GetWhereClause(),则不需要此属性。

以下示例显示了一个 FieldsFile,其中定义了六个字段,并利用了上面定义的 operatorLists

<configuration>

  <field id="Text1" text="My First Text Field"    
         operatorList="opList_text"          
         parameterDataType="String" />

  <field id="Bool1" text="My Boolean Field"
         operatorList="opList_boolean"       
         parameterDataType="Boolean"/>

  <field id="Region1" text="My Region"
         operatorList="opList_region"        
         parameterDataType="String" />

  <field id="Text2"  text="My Second Text Field"
         operatorList="opList_text"          
         parameterDataType="String" />

  <field id="Customer" text="Customer"
         operatorList="opList_customers" 
         parameterDataType="String" />

  <field id="IntField" text="My Integer Field"
         operatorList="opList_numeric"       
         parameterDataType="Int16" />

</configuration>

准备好 XML 配置文件后,开发人员可以使用类似下面示例的语法在 .aspx 页面中声明 SqlWhereBuilder 控件:

<%@ Register TagPrefix="cc1" Namespace="UNLV.IAP.WebControls" 
             Assembly="SqlWhereBuilder" %>

<html>
  <head>
    <title>SqlWhereBuilder example</title>
  </head>

  <body>
    <form runat="server">
      <h3>SqlWhereBuilder example</h3>
      <cc1:SqlWhereBuilder id="SqlWhereBuilder1" runat="server"
                           FieldsFile="fields.config"
                           OperatorListsFile="operatorLists.config"
                           ValueEntryFile="valueEntry.config"         
                           />
    </form>
  </body>
</html>

作为使用 XML 配置文件的替代方法,开发人员可以通过代码将适当的对象添加到 ValueEntryDivsOperatorListsFields 的集合属性中。还有许多影响控件外观的属性,包括按钮标签、CSS 类和样式。有关 SqlWhereBuilder 控件使用的集合和对象的更多信息,以及外观属性的完整列表,请参阅控件文档。

检索 WHERE 子句

要从已发布的条件集中生成 SQL WHERE 子句,开发人员可以使用 GetWhereClause()GetWhereClauseWithParameters() 方法。两者都返回一个 SQL 语法字符串,该字符串使用每个 supplied 条件的运算符的 sqlTemplate 属性进行编译。该字符串返回时不包含 "WHERE" 一词,以提高其使用的灵活性。

GetWhereClause() 方法

此方法将 WHERE 子句作为纯字符串返回,并假定适当的数据类型分隔符(例如,字符类型的单引号)已嵌入到运算符的 sqlTemplate 属性中。请注意,此方法可能容易受到 SQL 注入式攻击。尽管该控件尝试通过为其调用的每个提交值调用 ValidateValue() 方法来减轻这种可能性,但开发人员可能希望自行验证返回的字符串。ValidateValue() 方法定义为 virtual,允许开发人员在需要时重写此方法。

GetWhereClauseWithParameters() 方法

当意图生成用于 IDbCommand 对象(如 SqlCommandOleDbCommand)的 WHERE 子句时,这是推荐使用的方法。此命令使用适合特定 IDbCommand 实现的参数占位符来编译 WHERE 子句,并将特定于类型的 IDbDataParameter 对象添加到 IDbCommand 中。下面是一个在按钮单击提交响应中检索基于用户提供的条件的 WHERE 子句的示例。该 WHERE 子句连同参数一起添加到 OleDbCommand 对象中,然后执行该对象。

private void Button1_Click(object sender, System.EventArgs e)
{

  OleDbConnection con = null;
  OleDbCommand cmd = null;
  OleDbDataAdapter da = null;
  DataSet ds = new DataSet();

  try
  {
    // GetConnection() is a method defined elsewhere which 
    // returns an OleDbConnection object
    con = GetConnection();
    cmd = new OleDbCommand("SELECT * FROM MyTable", con);
    
    // inspect the SqlWhereBuilder.Conditions property to see if any
    // conditions were supplied
    if (SqlWhereBuilder1.Conditions.Count > 0)
    {
        // retrieve the WHERE clause and add parameters to the 
        // OleDbCommand object
        string sWhere = SqlWhereBuilder1.GetWhereClauseWithParameters(cmd);
        
        // add the WHERE clause to the command text; we could throw 
        // in additional WHERE clause criteria here if we wanted
        cmd.CommandText += " WHERE " + sWhere;
    }
  
    // execute the query
    da = new OleDbDataAdapter(cmd);
    da.Fill(ds);
    
    // bind results to a datagrid on the page
    dgResults.DataSource = ds;
    dgResults.DataBind();

  }
  catch (Exception ex)
  {
      // do something with exceptions...
  }
  finally
  {
      if (ds != null) ds.Dispose();
      if (da != null) da.Dispose();
      if (cmd != null) cmd.Dispose();
      if (con != null) con.Dispose();
  }
}

GetWhereClauseWithParameters() 方法会自动为 SqlCommandOleDbCommandOdbcCommand 对象生成适当的占位符。有关将 GetWhereClauseWithParameters() 与其他 IDbCommand 类型一起使用的其他说明,请参阅控件文档。

渲染

SqlWhereBuilder 控件的实际渲染是通过客户端 JavaScript 函数完成的。因此,重写的服务器端方法 OnPreRenderRender 通过调用 Page.RegisterStartupScript()Page.RegisterClientScriptBlock() 来生成和输出适当的客户端脚本。例程会遍历所有 Field 和 Operator 对象,注册客户端库所需的 JavaScript 代码以初始化 SqlWhereBuilder 对象。

重写的 OnPreRender 还调用 PrepareValueEntryDivs() 方法。此方法解释内部的ValueEntry 对象集合,以确定哪些是文字 HTML/文本,哪些源自 UserControl。每个对象随后成为 SqlWhereBuilder 对象的子控件,并通过重写的 Render 方法输出到客户端。

条件进行客户端/服务器通信

SqlWhereBuilder 中的任何现有条件也通过注册对客户端 JavaScript 函数 AddCondition() 的调用来进行渲染。这成为在服务器回发之间维护条件状态的关键元素。同样,由于条件完全在客户端进行操作,我们需要一种方法将条件集传回服务器。在这种情况下,普通的 ViewState 机制无法提供帮助。如果我们尝试在客户端修改隐藏的 __VIEWSTATE <input> 标签,回发到服务器时会抛出异常——服务器会认为(并且正确地)其 ViewState 机制已损坏。

解决方案是渲染我们自己的隐藏 <input> 标签,专门用于将条件集传回服务器。UpdateConditionsDisplay() 客户端方法,每次修改、添加或删除条件时都会被调用,包含一行额外的代码,在纯 JavaScript 环境中这本来是不必要的:

this.hiddenConditionsXml.value = escape(this.SerializeConditions());

this.hiddenConditionsXml 引用是我们将从服务器端查询的隐藏表单输入。SerializeConditions() 客户端方法生成一个表示条件集合的 XML 字符串。

function SQLWB_SqlWhereBuilder_SerializeConditions()
 {
    var sXml = "<conditions>";
    
    for (var i=0; i<this.conditions.length; i++)
    {
      sXml = sXml + this.conditions[i].Serialize();
    }
    
    sXml = sXml + "</conditions>";
    
    return sXml;
 }

SQLWB_Condition 客户端对象定义其 Serialize() 方法如下:

function SQLWB_Condition_Serialize()
 {
   var sXml = "<condition"
            + " field=\"" + this.field.id + "\""
            + " operator=\"" + this.operator.id + "\""
            + " andOr=\"" + this.andOr + "\""
            + ">"
            + "<values>";
            
   for (var i=0; i<this.values.length; i++)
   {
     sXml = sXml + this.values[i].Serialize();
   }
  
   sXml = sXml + "</values></condition>";
   return sXml;
 }

然后通过客户端对象 SQLWB_Value 将(通过ValueEntry 区域的表单输入输入)单个序列化为 <value> 标签。

function SQLWB_Value_Serialize()
{
 var sXml = "<value name=\"" + this.name + "\""
              + " value=\"" + this.value.replace(/"/g, '&quot;') + "\""
              + " friendlyValue=\"" 
                + this.friendlyValue.replace(/"/g, '&quot;') + "\" />";
 
 return sXml;
}

最终结果是,随着条件在客户端发生变化,隐藏的表单输入会用适当的 <condition> 标签的 XML 字符串重新填充。

就其本身而言,SqlWhereBuilder 服务器控件被标记为 IPostBackDataHandler 接口。它通过提供 LoadPostData() 方法的以下代码来满足该合同。这段代码会检查客户端在隐藏表单输入中提供的 XML,并反序列化条件集合。

public bool LoadPostData(string postDataKey, NameValueCollection postCollection)
{
  // get the conditions passed in through the hidden field
  string sHidden = this.GetID(kHIDDEN_CONDITIONS);
  string sData = postCollection[sHidden];

  // the data is escaped on the client end; decode it here
  sData = this.Page.Server.UrlDecode(sData);

  // treat it like a real Xml document and deserialize from there
  XmlDocument x = new XmlDocument();
  x.LoadXml(sData);
  SqlWhereBuilderConditionCollection cNew 
    = new SqlWhereBuilderConditionCollection(x.DocumentElement);

  // test if the conditions have changed; this will let us
  // fire the ConditionsChanged event later
  bool retValue = !(this.Conditions.Equals(cNew));

  this.Conditions = cNew;

  return retValue;
}

客户端代码通过隐藏的表单输入和 XML 文本字符串将其条件集通信给服务器端代码。服务器端代码通过注册对客户端函数 AddCondition() 的调用,将其条件集合重新渲染给客户端。通过这种往返通信,条件的状态可以在服务器回发之间得到维护,而不会损坏 ASP.NET 的 ViewState。

摘要

SqlWhereBuilder Web 控件为用户提供了一个友好的界面来输入即席查询条件,这些条件可以在回发时编译成 SQL WHERE 子句。作为一个封装 JavaScript 库的 ASP.NET 服务器控件,用户交互完全在客户端进行,而在服务器端,则实现了额外的功能,如基于 XML 的配置和与 IDbCommand 对象的集成。通过 GetWhereClause() 方法生成带有嵌入式数据类型分隔符和文字值的直接 WHERE 子句。要将 WHERE 子句字符串与 IDbCommand 对象集成,请改用 GetWhereClauseWithParameters() 方法。后者是首选方法,因为它减轻了 SQL 注入攻击的可能性。

在客户端库负责控件显示的情况下,服务器端渲染方法将 ValueEntry 区域渲染为 <div> 标签,并注册适当的客户端函数调用。条件的状态通过隐藏的表单输入在回发到服务器之间得到维护,客户端代码在此输入中将条件序列化为 XML 表示形式。然后,服务器在 LoadPostData() 方法中反序列化此字符串。总而言之,SqlWhereBuilder Web 控件为开发即席报表应用程序提供了一个工具。

© . All rights reserved.