用于编辑 SQL 表的 ASP.NET Web 组件





5.00/5 (4投票s)
ASP.NET C# 组件,采用插件式列格式适配器架构,用于编辑 SQL 表。
引言
我对使用 ASP.NET 数据组件(如 DataGrid)的复杂性感到沮丧,并希望对布局、字段格式化和错误检测有更多控制权,因此我决定“自己动手”。我希望能够查看分页的行列表,编辑或删除任何行,并添加新行,并且组件能自动处理关键列和各种标准数据类型。我还希望能够以最小的自定义编码实现下拉列表输入。
随着项目的演进,它被分为两个层:一个层是 System.Data.Common.DbConnection 的包装器和实用工具,用于构建具有完整格式和错误检查的 SQL 操作语句;另一个层是一个用于编辑表行的 GUI 组件。第一个组件可以在代码中操作 SQL 数据表时通用,而第二个组件则设计用于基于 Web 的数据库维护站点。
下一个开发周期涉及创建一种在列级别自定义 GUI 的方法,以便使用不同类型的 Web 控件进行编辑,并提供自定义格式化和解析。开发了一种插件适配器架构,为常见数据类型提供了标准行为,并且还可以在项目级别为特定的表列进行扩展。此机制的一个常见用途是提供一个下拉框作为输入方式,使用值表或文字值列表来填充控件。客户端 JavaScript 验证也可以包含在这些适配器中。
使用代码
EditTable 组件设计为与其他 ASP.NET 控件一样使用。Web 项目应引用 EditTable 项目,任何页面都应包含对 BFCS.Data.Common 命名空间的引用。您的项目 Bin 文件夹必须包含 Connection.dll,EditTable 类库在内部会使用它。
这是一个包含该控件的示例标记片段
//
// Sample EditTable control markup
//
<@ Register assembly="EditTable" namespace="BFCS.Data.Common" tagprefix="data" %>
//
// Style definitions are used here for setting appearance properties. You can also use literal styles.
//
//
// EditTable control instance
//
<data:EditTable ID="EditTable1" runat="server" 
    RowsPerPage="12" ScrollBars="Auto" BorderStyle="None" 
    FooterClass="th" HeadlineClass="th" 
    ItemClass="td" style="width: 700px;" 
    TableClass="table" Visible="False" 
    onerror_changed="EditTable1_Error_Changed" 
    SelectedClass="tds" KeyHeadlineClass="thk" 
    ErrorClass="tdsError" CellSpacing="0">
</data:EditTable>
为了有资格使用此控件,表必须具有单个主键列,该列可以是自动生成的,也可以由用户编辑。不支持多列主键和没有主键列的表。
组件属性、方法和事件
EditTable 组件派生自 System.Web.UI.WebControls.Table,这是一个普通的 HTML Table 标签的包装器。这意味着 Border、CellPadding 和 CellSpacing 等属性都可用。此外,EditTable 控件还提供了一套丰富的样式属性,此处有详细介绍。
组件显示的数据由另一组属性控制,此处有详细介绍。这组属性用于定义数据库连接字符串和提供程序类型(例如,对于 Microsoft SQL Server,可能是 System.Data.SqlClient)、要修改的表或视图的名称,以及用于选择要显示和/或过滤行的实际列的 SQL SELECT 语句。在示例应用程序中,连接值是从 Web.Config 文件的 ConnnectionStrings 部分读取的,而表名和 SQL SELECT 语句是通过从预定义列表中选择要编辑的表来构建的。
此外,还有行为属性,用于打开或关闭编辑、删除和添加行的能力,并控制是否显示初始化错误以及每页显示的行数。
上述属性在 Visual Studio 的设计窗格属性表中可用。此外,还有一个 CallingAssembly 属性,必须在代码中设置(通常在页面的 Page_Load 事件处理程序中),如下面的示例所示
//
// Example of setting CallingAssembly property from Page_Load event handler
//
using System.Reflection;
using BFCS.Data.Common;
namespace EditTableDemo
{
    public partial class TableEditor : System.Web.UI.Page
    {
        protected void Page_Load(object Sender, EventArgs e)
        {
            Type pageType = Page.GetType();
            EditTable1.CallingAssembly = 
                Assembly.GetAssembly(pageType.BaseType == null || 
                pageType.BaseType == typeof(Page) ? pageType : pageType.BaseType);
//
// ...
//
        }
此属性的目的是告知控件它正在运行的程序集。(有没有更简单的方法?请告诉我!)这样做的原因在下一节中描述,用于检测自定义表适配器。
最后有一个属性 LastError,读取该属性会生成在尝试修改表时检测到的最新错误或错误组。更改时,此属性会触发 Error_Changed 事件,您的项目可以使用该事件刷新错误显示标签。这些错误可能在尝试修改表数据之前就被捕获,例如无效的字符串格式或超出范围的值,也可能由 SQL 提供程序本身返回,例如索引冲突。
提供给调用代码的主要方法是 Refresh,它的作用如其名:从数据存储中重新检索数据并重新显示表。
用户界面

表以标准格式显示,数据源的实际列名用于标记顶行。最初,所有行都显示为“显示行”。如果启用,每行都有该行的“编辑”和“删除”功能的按钮,顶部会出现一个单一的“添加”按钮。如果启用,“查找”字段会出现在底部,并附有一个搜索输入值的按钮。根据表的大小和顶行的位置,将有“上一个”和“下一个”按钮用于分页浏览表。
| Add | 显示一个单独的添加行,带有“确定”和“取消”按钮,所有字段为空。确定按钮保存添加的行,取消按钮放弃它 | 
| 未使用。 | 将选定的行更改为编辑行,带有“确定”和“取消”按钮,如上 | 
| 删除 | 请求确认,然后删除当前行,拉取后续行 | 
| 查找 | 如果输入了值,则尝试将表定位到主键列值与输入字符串匹配的最近行,或定位到该行 | 
| 上一个/下一个 | 分页浏览表数据 | 
自定义表列适配器
在许多情况下,您可能希望微调编辑器与表中的特定列的交互方式。一个常见的情况是,一列包含一个对用户没有意义的数字代码,但有一个查找表为有效代码分配了提示值 - 这是下拉框编辑控件的自然用途。您可能希望使用 DateTime 控件来显示和编辑数据的仅时间部分。您可能希望为少量互斥的选择使用单选按钮控件。更简单地说,可能存在一个必须强制执行的合法数值范围,或者必须检查其格式的格式,例如税号或社会安全号码。
为了处理这些情况,我创建了一个表列适配器架构。Connection 项目提供了一套标准适配器,这些适配器默认用于常见的 SQL 数据类型。对于更专业的情况,您的项目定义了自己的适配器,并将其分配给特定的表/列组合。这些类通常作为 Web 项目中的类来编写。这些类必须遵守以下条件
- 该类必须是 BFCS.Data.Common.TableAdapter类的子类
- 它必须实现 BFCS.Data.Common.ITableAdapter接口
- 它必须有一个合适的构造函数,如下所述
BFCS.Data.Common.ITableAdapter 接口指定了处理字段数据的方法,如下面的摘录所示
public interface ITableAdapter
{
    /// Given a value retrieved from a data source, return a System.Web.Control to display it
    /// in a non-edit, non-append row
    Control DisplayValue(object value);
    /// Given a value retrieved from a data source, return a System.Web.Control to display it
    /// in an edit row
    Control EditValue(object value);
    /// Given a value retrieved from a data source, return a System.Web.Control to display it
    /// in an edit or append row. Appending flag informs routine that it is being used to
    /// display an append row; if false, it is an edit row.
    Control EditValue(object value, bool Appending);
    /// Given a value entered as a string, check for validity, modify as necessary to use
    /// in an action SQL statement. Set 'error' value to non-empty, non-null string if an error is detected
    /// in the supplied value.
    string SaveValue(string setting, out string error);
}
前三个方法生成一个 System.Web.UI.Control 派生实例,该实例将在表填充并返回之前插入到表中。在上面,“非编辑行”指的是显示但未打开编辑的行,“编辑行”指的是单击行上的“编辑”按钮后显示的行,或者在显示添加行时显示的行。SaveValue 方法用于解析在 PostBack 过程中由编辑控件返回的输入数据,检查其有效性,并在必要时重新格式化它以包含在 SQL 操作语句(INSERT INTO 或 UPDATE)中。如果此例程检测到提交值中的错误,则应将 out 参数 error 设置为用户可读的错误消息,描述错误并建议如何纠正它。
出现在 Web 应用程序中的自定义表列适配器必须用 BFCS.Data.Common.CustomTableAdapter 属性进行装饰,如下面的示例所示
[CustomTableAdapter("Calendar", "StartTime")]
正如示例所示,该属性有两个强制参数。第一个是表名,第二个是要应用适配器的列名。如果相同的适配器要应用于多个列,则必须为每个表/列组合声明一个单独的类,并让它们继承自基适配器类。
为了进一步微调表列适配器的行为,还有一个可选的 String 属性 Modifiers,可以设置为命名参数。这允许传递附加参数,编码到一个字符串中,供适配器使用。
根据装饰中是否出现 Modifiers 参数,表列适配器类必须提供一个遵循以下模板之一的构造函数
//
// Constructor for table column adapter that does not use Modifiers argument in decorating attribute
// NOTE on third argument of base constructor: this argument is used for type checking only. If used,
// type passed should match the type of the column, after mapping. To disable the type checking, pass null.
// 
//
public MyTableAdapter(BFCS.Data.Common.DbValidator validator, string ColumnName)
: base(validator, ColumnName, typeof(String))
{
// ...
}
//
// Constructor for table column adapter that uses Modifiers argument
//
private string m_Modifiers;
public MyTableAdapter(BFCS.Data.Common.DbValidator validator, string ColumnName, string Modifiers)
: base(validator, ColumnName, typeof(String)
{
    m_Modifiers = Modifiers;        // Save for use by adapter
// ..
}
当然,您可以提供两个构造函数。组件根据 Modifiers 参数是否存在来选择使用哪个。Modifiers 参数的一些用法是
- 定义数值或日期字段的下限和上限
- 声明下拉框的合法值的文字列表,或
- 声明一个查找表来为下拉框提供值的规范
- 定义一个正则表达式,文本输入必须满足该正则表达式
如上所述,单个适配器代码负责解释和使用 Modifiers 值。
在运行时,如果调用页面代码设置了 EditTable 控件的 CallingAssembly 属性(如上所示),控件会搜索页面程序集中带有 CustomTableAdapter 属性装饰的类型,其属性与正在初始化的表和列匹配。如果找到匹配的适配器,组件会搜索一个构造函数来生成适配器实例,该实例附加到列并用于将数据移入和移出数据库存储,以及进行错误检查。
概括自定义表列适配器的使用规则,它们必须
- 派生自 TableAdapter
- 适当地实现 ITableAdapter接口
- 用 CustomTableAdapter属性进行装饰,参数指示表和列,以及可选的修饰符
- 根据属性参数提供适当的构造函数
- 通过设置 EditTable控件的CallingAssembly属性使其可见
幕后
该项目封装了多年来与 SQL 和 ASP.NET 合作的经验。EditTable 控件的源代码是许多自定义控件编码功能的详尽、近乎痛苦的示例,而 Connection 项目则封装了使 SQL 工作起来轻松无误的丰富经验。
Connection 项目采用分层实现。DataConnection 类简化了建立数据库连接以及处理问题和错误的通常混乱的过程。它允许您多次“打开”而不会报错,并推迟关闭连接(如果需要),以应对嵌套例程。提供了简单的方法来生成数据源无关的 DataReader、Command 和 DataAdapter 对象。
DbValidator 类检索表的架构,并使用它来验证,并在必要时重新格式化字符串值以用于操作 SQL 语句。SQLStatement 类派生自 DbValidator,它就像一个智能的 SQL 语句 StringBuilder,检查有效性并轻松地重新格式化字符串值以确保它们能够执行,在尝试对数据库提供程序执行 SQL 语句之前捕获许多常见问题(例如过长的字符串、必需列值缺失以及重复键设置)。
EditTable 类展示了控件编码中的一系列概念。ViewState 在内部用于在 postback 之间维护控件的位置和其他状态。最棘手的特性之一是让动态输入的按钮控件的事件正确触发。解决方案归结为在 postback 之后重建控件树,复制此类控件的控件 ID,处理由此检测到的事件,然后根据事件处理程序的动作修改控件树,并在 Page 控件的正确生命周期阶段完成所有这些操作。
附录:EditTable 属性
EditTable 控件外观属性
| 属性名称 | 描述 | 
| TableStyle | 应用于整个表的样式 | 
| TableClass | 应用于整个表的预定义类 | 
| HeadlineStyle | 应用于表标题的样式 | 
| HeadlineClass | 应用于表标题的预定义类 | 
| ItemStyle | 应用于显示行单元格的样式 | 
| ItemClass | 应用于显示行单元格的预定义类 | 
| SelectedStyle | 应用于选定的编辑行或添加行的样式 | 
| SelectedClass | 应用于选定的编辑行或添加行的预定义类 | 
| ErrorStyle | 应用于格式错误的编辑单元格的样式 | 
| ErrorClass | 应用于格式错误的编辑单元格的预定义类 | 
| FooterStyle | 应用于表页脚的样式 | 
| FooterClass | 应用于表页脚的预定义类 | 
EditTable 控件数据属性
| 属性名称 | 描述 | 
| DataProvider | 数据连接提供程序的名称(例如,System.Data.SqlClient) | 
| ConnectionString | 数据源的连接字符串 | 
| TableName | 要修改的表名 | 
| TableSQL | 用于检索表行的 SQL SELECT 语句 | 
EditTable 控件行为属性
| 属性名称 | 描述 | 
| RowsPerPage | 要显示的表的最大行数 | 
| AllowEdit | 标志:启用行的编辑 | 
| AllowDelete | 标志:启用行的删除 | 
| AllowAdd | 标志:启用新行的添加 | 
| AllowFind | 标志:启用 FIND 功能 | 


