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

面向对象程序员的 ADO.NET – 第二部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (57投票s)

2006年1月19日

CPOL

28分钟阅读

viewsIcon

352131

downloadIcon

3518

在本系列文章的第二部分中,我们将再次探讨 ADO.NET CRUD 操作。不过这次,我们将使用一个架构更优良的应用程序。完成之后,我们将继续学习对象的数据绑定。

引言

在本系列文章的第一部分中,我们丢弃了大约90%的 ADO.NET,因为作为面向对象的程序员,我们不需要它。对我们来说,ADO.NET 是一个薄数据传输层,而不是一个全功能的数据管理解决方案。该文章展示了如何使用 ADO.NET 执行基本的 CRUD 操作,以将信息从业务模型中获取和存入。

我在第一部分中特意将代码保持尽可能简单,以便读者能够专注于 ADO.NET 的使用方式。但是那段代码几乎不是面向对象的。或者更确切地说,它的架构很少。在本文中,我们将重新审视 ADO.NET CRUD 操作。不过这次,我们将使用一个架构更优良的应用程序。

完成之后,我们将继续学习对象的数据绑定。在某些圈子里,.NET 框架因“适合数据库前端,但其他方面不多”而受到诟病,因为它的大部分都围绕数据驱动编程构建。据说数据集很容易,而对象很难。虽然这在 .NET 1.x 中可能是真的,但现在已经不是了。

Visual Studio 2005 包含了新的“数据源”功能,允许对象像数据集一样轻松地进行数据绑定。该功能适用于几乎任何对象或集合,但它特别适用于实现某些关键接口的对象和集合。

本文附带的演示应用程序包含实现这些接口的基类。如果您的数据绑定对象和集合派生自这些类,您不仅将获得基本数据绑定,还将获得数据绑定排序和搜索功能。

演示程序

本文附带了一个演示程序。该程序使用一个 SQL Server 数据库,该数据库包含在“*Database Files*”文件夹中。使用 *SQL Server Management Studio*(或 *Management Studio Express*,可从 MSDN 免费下载)将这些文件附加到 SQL Server 2005 或 SQL Express 2005。

关于代码的几句话:为了简化,演示应用程序没有做一些在生产代码中始终应该做的事情。例如,它没有将数据库调用包装在 `try`-`catch` 块中,也没有转义它创建的内联 SQL 查询中的单引号。显然,开发人员在用于生产的代码中应该做所有这些事情,以及其他事情。

演示程序维护了一个简单的作者列表和他们撰写的书籍列表。其主窗口在两个网格中显示作者和书籍的主从列表。

可以使用正常的数据网格编辑操作添加或更改项目。可以通过在网格上选择一个项目,右键单击它,然后从上下文菜单中选择“删除”来删除项目。

主从视图可能是用于在包含层次结构中审查和更新对象的最常用视图。编写主从视图是繁琐且耗时的,并且容易出错。在 Visual Studio 2005 之前,没有简单的方法可以使用设计时数据绑定来减轻这项工作。

现在,通过对象数据绑定,您可以在大约十分钟内设置一个完整的主从视图,就像上面那个一样。您可以几乎同样快速地创建报告。右下角的“查看报告”按钮会打开一个报告窗口,显示包含相同信息的主从报告。我们稍后将查看该窗口。

架构——拼图的碎片

该示例应用程序围绕模型-视图-控制器 (MVC) 设计模式构建,其数据访问层基于数据访问对象 (DAO) 模式。总体方法类似于 Joe Hummel 博士在他当前的 MSDN 网络广播系列中教授的内容。[Hummel]

MVC 设计模式的目标是将应用程序的业务模型与其用户界面隔离开来。用户界面出了名的不稳定,并且它们有一种干扰业务模型的坏习惯。依赖关系变得纠缠不清,以至于当我们更改用户界面时,它会破坏我们的模型。

MVC 从假设业务模型是应用程序最稳定的部分开始。对于可能不熟悉 OOP 的人来说,业务模型被期望是应用程序代码中最稳定的部分。模型可能是我们应用程序中第一个用代码实现的部分。一旦完成,我们需要保护它免受应用程序其他部分的更改。*更改业务模型的唯一原因应该是更准确地反映底层业务流程*。换句话说,我们永远不应该因为用户界面的更改而需要“重新打开”我们的业务模型。

MVC 通过在 UI 和业务模型之间放置一个“控制器”来将模型与用户界面隔离开来。所有从 UI 到模型的请求都通过控制器进行过滤。在某些版本的 MVC 中,所有来自业务模型的响应也通过控制器。其他版本允许 UI 直接响应业务模型触发的事件。我们将在演示应用程序中使用后一个版本。

应用程序不稳定的另一个来源是数据库后端。一个应用程序今天可能使用 SQL Server,但明年可能使用 Oracle。我们可能使用内联 SQL 查询来开发应用程序,但在应用程序投入生产时切换到存储过程。如果数据访问封装在我们的业务对象中,任何这些更改都会破坏业务模型。

面向对象的设计师通常将数据访问操作封装在业务对象中,理由是对象应该是自给自足的——一个对象应该知道如何加载和保存其数据。但是“单一职责原则”指出“一个类应该只有一个更改的理由。”[Martin] 如果我们将数据访问封装在业务对象中,它们就有两个更改的理由:

  • 业务模型的更改;或
  • 数据访问方法的更改。

为了遵守单一职责原则,我们需要打破封装。

DAO 模式在 Java 世界中被广泛用于解决这些问题。该模式看起来像这样:

业务对象将其数据访问操作委托给一个配套的数据访问对象,该数据访问对象又将其 CRUD 操作委托给一个数据提供程序对象。数据访问对象封装了更高级别的数据库调用,例如 SQL 查询或存储过程调用,而数据提供程序对象执行实际的数据库操作。数据提供程序对象构建在 .NET 数据提供程序之上,并与之紧密耦合。

数据提供程序对象包含我们使用的大部分 ADO.NET 代码。例如,在“获取”操作中,数据提供程序将创建并填充一个数据集,然后将其返回给 DAO 对象。DAO 的职责是将数据从数据集传输到其配套的业务对象。

MVC 和 DAO 模式很好地将业务模型与应用程序表示层和数据层的不稳定性隔离开来。但是 MVC 也有其自身的局限性。由于应用程序层之间的通信大部分都通过控制器进行路由,因此它有可能成为瓶颈。为了避免这个问题,大多数 MVC 设计都会为 UI 中的每个表单创建一个单独的控制器。然而,如果多个表单需要对业务模型发出相同的请求,则将该请求分派的代码将需要添加到两个控制器中。这种代码重复是一些作者所说的“代码异味”,就像“那段代码闻起来很糟糕”[Beck 和 Fowler]。

命令模式是最初的“四人帮”模式之一,解决了这些问题。UI 可以发出的每个请求都封装在一个单独的命令对象中。这些对象由控制器处理——控制器被简化为一个轻量级的命令处理器,可以处理整个应用程序。

同样,每个命令对象都是一个非常简单轻量级的对象。如果两个不同的表单需要传递相同的请求,它们只需实例化该消息的命令并将其传递给控制器。这种方法消除了代码重复——如果我们需要更改命令,我们只需在一个地方进行更改。我们的代码闻起来好多了。

除了清理设计之外,命令模式还使得实现无限撤销变得非常容易。我们不会在演示应用程序中实现此功能,但如果您想了解它是如何工作的,您可以在此处找到我关于该主题的文章。

架构——将所有内容整合在一起

正如我上面提到的,演示应用程序管理一个作者列表。该列表是一个两级包含层次结构,类似于我们在第一部分中查看的项目列表。作者集合位于层次结构的顶部,集合中的每个作者都有一个图书集合。

让我们来看看所有这些东西在代码中是如何工作的。处理来自 UI 的请求更像是通过我们应用程序架构的旅程,而不是简单地执行算法。我们从 FormMain,也就是我们应用程序的 UI 开始。快速检查代码会发现除了事件处理程序之外什么都没有,而且每个事件处理程序都只有几行代码。这与 UI 在 .NET 应用程序中的角色保持一致:管理显示并将用户输入分派给控制器。UI 绝不应该自己进行任何处理。

在 FormMain 的 `FormMain_Load()` 处理程序的顶部设置一个断点,然后以调试模式启动应用程序。逐步进入处理程序中的每个调用,以跟踪“加载作者列表”请求,跨越参与完成请求的各种对象。请注意,每个对象只处理与其所属应用程序层相对应的请求部分。

表单加载处理程序通过创建一个命令对象来执行请求,并将该对象传递给控制器,从而启动加载列表的过程。

// Get authors list
CommandGetAuthors getAuthors = new CommandGetAuthors();
m_Authors = (AuthorList)m_AppController.ExecuteCommand(getAuthors);

控制器调用命令对象的 `Execute()` 方法,其中包含执行命令的代码。

public override object Execute()
{
    AuthorList authors = new AuthorList();
    return authors;
}

命令对象实例化一个新的 `AuthorList` 对象,该对象将控制权传递给该对象的构造函数。请注意,我们已从表示层进入业务模型。`AuthorList` 构造函数实例化一个配套的 DAO 对象,将自身的引用传递给该对象,并指示 DAO 对象从应用程序数据库中加载作者列表。

public AuthorList()
{
    AuthorListDAO dao = new AuthorListDAO();
    dao.LoadAuthorList(this);
}

`AuthorList` 对象将其控制权传递给其配套的 DAO 对象,这将我们从应用程序的业务层带入数据访问层,在那里我们终于找到了一些可以深入研究的代码。

public void LoadAuthorList(AuthorList authorList)
{
    // Build query to get authors and their books
    StringBuilder sqlQuery = new StringBuilder();
    sqlQuery.Append("Select AuthorID, LastName," + 
                    " FirstName, SSNumber From Authors; ");
    sqlQuery.Append("Select BookID, SkuNumber," + 
                    " AuthorID, Title, Price From Books");

    // Get a data set from the query
    DataSet dataSet = 
      DataProvider.GetDataSet(sqlQuery.ToString());

    // Create variables for data set tables
    DataTable authorsTable = dataSet.Tables[0];
    DataTable booksTable = dataSet.Tables[1];

    // Create a data relation from Authors
    // (parent table) to Books (child table)
    DataColumn parentColumn = authorsTable.Columns["AuthorID"];
    DataColumn childColumn = booksTable.Columns["AuthorID"];
    DataRelation authorsToBooks = new 
      DataRelation("AuthorsToBooks", parentColumn, childColumn);
    dataSet.Relations.Add(authorsToBooks);

    // Load our AuthorList from the data set
    AuthorItem nextAuthor = null;
    BookItem nextBook = null;
    foreach (DataRow parentRow in authorsTable.Rows)
    {
        // Create a new author
        bool dontCreateDatabaseRecord = false;
        nextAuthor = new AuthorItem(dontCreateDatabaseRecord);

        // Fill in author properties
        nextAuthor.ID = Convert.ToInt32(parentRow["AuthorID"]);
        nextAuthor.FirstName = parentRow["FirstName"].ToString();
        nextAuthor.LastName = parentRow["LastName"].ToString();
        nextAuthor.LastName = parentRow["LastName"].ToString();
        nextAuthor.SSNumber = parentRow["SSNumber"].ToString();

        // Get author's books
        DataRow[] childRows = parentRow.GetChildRows(authorsToBooks);

        // Create BookItem object for each of the authors books
        foreach (DataRow childRow in childRows)
        {
            // Create a new book
            nextBook = new BookItem();

            // Fill in book's properties
            nextBook.ID = Convert.ToInt32(childRow["BookID"]);
            nextBook.SkuNumber = childRow["SkuNumber"].ToString();
            nextBook.Title = childRow["Title"].ToString();
            nextBook.Price = Convert.ToDecimal(childRow["Price"]);

            // Add the book to the author
            nextAuthor.Books.Add(nextBook);
        }

        // Add the author to the author list
        authorList.Add(nextAuthor);
    }

    // Dispose of the data set
    dataSet.Dispose();
}

如果您阅读了本系列的第一部分,那么这段代码应该是不言自明的。`LoadAuthorList()` 方法遍历数据库中 Authors 表的记录,并为找到的每个作者创建一个作者记录。在此过程中,它为作者编写的每本书创建一个 `BookItem`,并将其添加到 `AuthorItem` 的 `Books` 集合属性中。

请注意,DAO 对象直接设置实例化并调用它的 `AuthorList` 对象的属性。因此,DAO 对象需要了解其同伴的结构;它与其业务对象紧密耦合。但是业务对象与其 DAO 对象的耦合度更松散。如果数据访问过程发生变化,我可以更改它而无需重新打开业务对象。通过打破封装来委派数据访问,我们已经将业务模型与数据访问模型的更改隔离开来。

现在,即使 DAO 对象是一个相当庞大的代码块,您可能已经注意到它从未创建过数据集或数据适配器。它只是将 SQL 查询传递给 `DataProvider` 对象,后者将数据集返回给 DAO 对象。换句话说,DAO 对象将实际的数据访问操作委托给一个 `DataProvider` 类,该类构建在 .NET 本机数据提供程序之上。这种方法集中了我们的数据访问代码并消除了重复。我们的代码因此变得更好。

委托给 `DataProvider` 对象还将我们的 DAO 对象与数据库的底层更改隔离开来。如果我们将数据库从 SQL Server 更改为 Oracle,我们不必重新打开我们的业务对象。而且,由于我们在 DAO 对象中使用相当通用的内联 SQL 查询,我们也可能无需重新打开我们的 DAO 对象。如果幸运的话,我们将能够通过修改 `DataProvider` 类中的三个方法来简单地进行更改。嘿,即使我也能在一个小时左右完成!

Joe Hummel 认为,这个好处为使用内联查询而不是存储过程提供了强有力的理由。我认为他可能有道理。Joe 确实警告说,内联查询需要正确转义以防止 SQL 注入攻击,并且他承认存储过程比内联查询具有性能优势。有关更多信息,请参阅他当前网络广播系列的第 6 节。

两级数据访问方法,使用内联查询或存储过程,对于您可能用于不同客户项目中的框架特别有用。考虑一个有三个安装点的客户:站点 A 使用 SQL Server,站点 B 使用 Oracle,而站点 C 的数据库管理员表示,他或她绝对不会将存储过程添加到站点的 SQL Server 中。我可以为前两个站点使用 SQL Server 和 Oracle 数据提供程序对象的存储过程,为站点 C 使用带有 SQL Server 数据提供程序的内联查询。而且我不需要触及业务模型或 UI 即可完成这些操作。

我们在这里不再赘述 `DataProvider` 代码,因为它与本系列第一部分中的代码大致相同。简单来说,DAO 对象使用 `DataProvider` 返回的数据集来填充其配套业务对象。然后它将控制权返回给业务对象,业务对象将控制权返回给命令对象,命令对象将控制权返回给 UI。换句话说,我们深入到应用程序层,然后又回到 UI。

创建、更新和删除操作的工作方式相同——我们创建一个命令对象,将其传递给控制器,然后控制器处理该命令。在大多数情况下,命令会向业务对象发出请求,业务对象将请求委托给 DAO 对象。

在演示应用程序中,CRUD 操作由主窗体上 `DataGridView` 控件中发生的事件触发。我们将在下面的“数据绑定”部分讨论这些事件以及如何将业务对象数据绑定到它们。

架构——重点是什么?

仅仅为了获取一个作者列表,就需要经过这么多对象。如果直接传递数据集,难道不会简单得多吗?在像演示应用程序这样的简单案例中,当然会简单得多。面向对象设计会带来一定的开销,在设计项目之前应予以考虑。OOP 在非常简单的应用程序中可能有点大材小用。

但考虑一个客户公司,它使用一个总账,其中有三四个级别的子分类账,它们来自许多不同的账户。那些编写财务和会计应用程序的人都知道这并非不寻常。我不想尝试使用数据集来处理这个业务流程。正如我妻子在第一部分中所说:“它们工作起来很好,但你不能用它们制造瑞士手表。”

当我的项目中发生变化时,这些变化应该局限于一个层。这意味着对一个层的更改不会影响任何其他层。我可以将此应用程序迁移到网络,并且对业务或数据层几乎不需要做任何事情。同样,我可以更改数据库,并且对 UI 和业务层几乎不需要做任何事情。业务层与所有更改隔离,除了涉及业务模型的更改。当然,如果业务模型发生重大变化,所有情况都会改变。但那是最不可能改变的层。

UI 层中的数据绑定

最后,我们终于将作者列表返回到用户界面。然后我们该怎么做呢?在 .NET 1.x 的旧时代,我们很多人都习惯于将列表重新倾倒到数据集中,然后将其绑定到网格进行显示和更新。那简直是疯了。或者,我们会使用一个具有良好非绑定模式的第三方网格,然后手动将其连接到列表。这至少可以说是痛苦的。

Visual Studio 2005 为 .NET 提供了新的“数据源”功能。它不是一个控件——`BindingSource` 控件用于执行实际的数据绑定。它也不是一个类。它是一个 XML 接口(一个带有 `.datasource` 扩展名的文件),它允许 .NET 将业务对象视为 ADO.NET 行对象,并将业务集合视为 ADO.NET 数据表。换句话说,业务对象和集合在 .NET 数据绑定世界中成为了一等公民。

这意味着,我们大部分情况下可以忘记编写数据绑定代码。我们不需要它。现在,显示业务模型所需工作的大约 90% 可以在设计时完成。这意味着,我们不再需要编写大量代码,而是可以在设计器中摆弄。我不知道您怎么看,但我知道我更愿意做哪一个。

由于工作是在设计模式下完成的,让我们逐步了解所涉及的步骤,而不是重新打印代码。首先,布局你的表单。对于演示应用程序,我们需要一个主从显示,因此我们在表单上使用两个 `DataGridView` 控件。

接下来,为您的业务对象创建数据源。我使用了“数据”菜单中的“添加新数据源”,尽管您也可以从“数据源”可停靠窗口执行相同的操作。从“数据”菜单中选择“显示数据源”以打开它。我为 `AuthorList` 和 `BookList` 对象创建了数据源。

接下来,将业务对象绑定到 `DataGridView` 控件。我将 `AuthorList` 数据源对象拖到作者网格上,并将 `BookList` 数据源对象拖到书籍网格上。当每个对象被拖放到其网格上时,相应的列会出现在网格上。请注意,当您拖放每个数据源对象时,Visual Studio 会向表单添加一个 `BindingSource` 组件。绑定源将充当数据绑定的中介。网格绑定到绑定源,绑定源绑定到数据源。

接下来,配置每个网格。从上下文菜单或 `DataGridView` 控件的智能面板中选择“编辑列”,然后将出现“编辑列”框。您可以使用该框设置列宽、标题文本等。

我绝对建议更改表单上每个 `DataGridView` 的每个列的 `Name` 属性。如果您在表单上有两个网格,例如在主从表单中,并且您的对象具有共同的属性名称(`AuthorItem` 和 `BookItem` 对象都有一个“`ID`”属性),那么设计器已知会为两个 `DataGridView` 控件的列分配相同的名称。如果发生这种情况,您的表单下次打开时会崩溃,您将不得不重新开始。

完成网格格式设置后,是时候编写几行代码了。我们之前进行的设计时绑定将网格绑定到我们的对象结构,但没有绑定其数据。用数据库术语来说,我们所做的只是绑定了对象的模式。所以,现在我们需要为运行时绑定:

// Bind grids
bindingSourceAuthors.DataSource = m_Authors;
bindingSourceBooks.DataSource = bindingSourceAuthors;
bindingSourceBooks.DataMember = "Books";

这些行出现在 `FormMain_Load()` 事件处理程序的末尾。第一行将 Authors 绑定源绑定到我们之前加载的 Authors 列表。第二行将 Books 绑定源绑定到 Authors 绑定源。这是一个重要的举动——它将 Books 网格与当前选定的作者同步。由于 Books 网格将显示 `AuthorItem` 的 `Books` 属性,因此第三行将 Books 绑定源的 `DataMember` 属性设置为当前 `AuthorItem` 的 `Books` 属性。

就这些了——三行代码。演示应用程序中的网格支持数据绑定添加、更改和删除,就像它们绑定到数据集一样。我们获得了所有好处,却没有带来任何痛苦。

数据绑定和 BindingList<T>

Visual Studio 2005 的 `DataSource` 功能的一个很棒之处在于它们不需要特殊的集合或对象即可工作。几乎任何集合都可以。在 .NET 1.x 的旧时代,只有实现了 `IBindingList` 接口的集合才能完全绑定。曾经尝试实现该接口一次,即使被枪指着我也不会再尝试。开枪吧;那痛苦更少。

尽管您可以使用几乎任何集合作为 `DataSource`,但实现 `IBindableList` 仍然有优势。幸运的是,Microsoft 通过一个新的泛型集合类 `BindingList` 使其变得简单。它类似于泛型类 `List`,但它实现了 `IBindingList` 接口,因此您无需自己实现。只需将您的类派生自 `BindingList`,您就可以开始使用了。

我不会花很多时间在 `BindingList` 上,因为 MSDN 上有一系列关于它的精彩文章:

演示应用程序包含两个抽象类 `FSBindingList` 和 `FSBindingItem`,它们作为我大多数集合的基类。`FSBindingList` 派生自 `BindingList`,它实现了您无法从 `BindingList` 获得的排序和搜索功能。因此,只需将您的集合派生自 `FSBindingList`,您的绑定 `DataGridView` 控件将支持网格上的排序和搜索。详细信息请参阅上面引用的 MSDN 文章。

`FSBindingItem` 类实现了 `IEditableObject` 接口。此接口支持 `DataGridView` 中的“行提交”编辑。当绑定到数据集时,`DataGridView` 允许用户通过按 Escape 键取消对当前行的任何更改。这是许多用户习惯并期望的行为,但对象必须实现 `IEditableObject` 才能支持此功能。`FSBindingItem` 实现了该接口,因此只需将您的业务对象从该类派生,即可在 `DataGridView` 控件中获得行提交编辑。

顺便说一下,任何支持数据绑定的 UI 控件也支持数据源绑定。这意味着业务对象属性可以绑定到文本框、标签和其他控件。关于 `DataGridView` 控件的最后一点说明:您可能会发现,在设计时将对象绑定到 `DataGridView` 后,该控件不显示用于添加新数据的空白行。这意味着它不允许用户在运行时添加新行,即使您已在其智能面板中指示它这样做。在大多数情况下,这是因为网格绑定的 `BindingSource` 组件的 `AllowNew` 属性设置为 `false`。在设计时将其重置为 `true`,空白的“添加新”行将重新出现。

数据绑定报告

Visual Studio 2005 包含一个新的 `ReportViewer` 控件和用于为查看器创建报告的 Report Designer。对象和集合可以数据绑定到 `ReportViewer` 报告,就像它们可以绑定到 `DataGridView` 和其他控件一样。设置比 `DataGridView` 稍微复杂一点,但不多。

演示应用程序包含一个主从报告,显示每个作者在一个单独的页面上,以及该作者撰写的书籍。您可以通过单击主窗体上的“查看报告”按钮查看该报告。

以下是如何构建像演示应用程序中那样的主从报告:首先创建报告。报告像任何其他项目一样添加到项目中。右键单击解决方案资源管理器中的项目名称,然后从上下文菜单中选择“添加”>“新建”。将出现“添加新项”框;选择“报告”并为您的报告命名。演示应用程序的报告名为“*AuthorsReport.rdlc*”。

空白报表打开后,从工具箱中将项目拖放到其上。首先,我们需要某种重复容器来保存作者信息。由于我们想要一个自由格式的页面,每个页面一个作者,而不是网格,我们将使用工具箱“报表项”部分中的 `List` 控件。我们将其拖到报表上,放下,并调整大小以适应页面。现在我们需要填充列表。

没有标签控件;标签是使用文本框控件添加的,您可以直接在其中输入。对象字段可以直接从“数据源”窗口拖到列表中。对于主报告,我拖出了一些标签和字段。然后我拖出一个“`子报告`”控件并将其放到列表中。我给它设置了您在表单上看到的尺寸,然后保存并关闭它。

为了显示应用程序,我们需要创建一个子报表——我们不能使用简单的分组。创建子报表的第一步是在主报表中放置一个占位符。将一个 `Subreport` 控件从工具箱拖到报表上。它将显示为一个灰色框。将该框调整为您计划插入的子报表的大小。在演示应用程序中,我们计划插入一个与报表宽度相同的表格,因此我们将 `Subreport` 控件调整为报表的宽度。

接下来,创建实际的子报表。它只是另一个报表文件,并且使用与主报表相同的技术进行设置。我们的演示应用程序子报表名为“*BooksSubreport.rdlc*”,我们使用工具箱中的 `Table` 控件对其进行设置,因为我们希望在一个页面上显示多本书。我们通过将字段从数据源窗口拖到子报表上 `Table` 控件的数据行来设置表格列。最后,我们将 `Table` 控件调整到与主报表相同的宽度,并调整报表使其只包含表格。

接下来,设置从主报告到子报告的通信。Visual Studio 2005 没有直接从报告传递字段到子报告的功能,因此我们需要提供一些管道来完成此操作。首先,向子报告添加一个参数。该参数通常是主报告中当前对象的 `ID` 属性。

要添加参数,请确保子报表在设计器中可见。从“报表”菜单中选择“报表参数”,将出现“报表参数”框。在框左侧的列表中为 ID 添加一个参数。我在演示应用程序中的子报表中添加了一个名为“`AuthorID`”的参数。完成后,保存并关闭子报表。

现在将主报表连接到子报表。为此,请重新打开主报表。右键单击灰色子报表块,然后从上下文菜单中选择“属性”。在“常规”选项卡上,从“子报表”下拉列表中选择子报表文件。两个报表现在已连接。

接下来,切换到“参数”选项卡。我们将把我们在子报表中创建的参数链接到主报表当前记录中的一个字段。在“参数名称”列中输入我们的子报表参数名称 (`AuthorID`)。在“参数值”列中,从下拉列表中选择“`Fields!Id.Value`”。主报表现在将使用子报表的 `AuthorID` 参数将当前作者的 ID 传递给子报表。保存并关闭主报表。

我们快完成了。下一步是为报告创建一个查看器。每个报告都绑定到一个单独的报告查看器控件。对于演示应用程序,我只是将一个 `ReportViewer` 控件添加到了报告窗体中,并将该控件停靠以填充窗体。将控件绑定到报告的最简单方法是使用控件的智能面板。从面板的“选择报告”下拉列表中选择报告,然后控件就绑定到报告了。请注意,`ReportViewer` 创建了一个 `BindingSource` 控件来促进绑定。还要注意,`ReportViewer` 在设计时除了其导航控件之外不显示任何内容,即使它已经绑定。

与 `DataGridView` 控件一样,我们的设计时绑定只是将报告绑定到 Authors 列表的结构,而不是其数据。我们需要在代码中绑定报告以实现运行时绑定。该任务由 `FormReport_Load()` 事件处理程序执行。

private void FormReport_Load(object sender, EventArgs e)
{
    // Get author list
    CommandGetAuthors getAuthors = new CommandGetAuthors();
    m_Authors = (AuthorList)m_AppController.ExecuteCommand(getAuthors);

    // Subscribe to report viewer's SubreportProcessing event
    reportViewer1.LocalReport.SubreportProcessing += new 
      SubreportProcessingEventHandler(LocalReport_SubreportProcessing);

    // Bind report to authors list
    bindingSourceAuthors.DataSource = m_Authors;

    // Refresh report
    this.reportViewer1.RefreshReport();
}

请注意,该处理程序使用与主窗体相同的 Command 类来获取作者列表。如果使用传统的 MVC,两个窗体都有单独的控制器,就会出现代码重复,并且可以看到代码可能会开始变得混乱。就目前而言,我们创建一个 `CommandGetAuthors` 对象并将其传递给 Controller。

还要注意,我们必须订阅 `ReportViewer` 的 `SubreportProcessing` 事件。我们将使用此事件来拦截从主报告传递到子报告的 ID。使用该 ID,我们将获取主报告当前正在处理的 `AuthorList` 对象。然后,我们将该对象的 `Books` 集合作为数据源传递给子报告。

`SubreportProcessing` 事件非常奇特——它是我见过的唯一一个拥有父级的事件(实际事件名称是“`LocalReport.SubreportProcessing`”)。而且,它是唯一一个无法通过双击 Windows Form Designer 的“属性”窗口中的事件来创建事件处理程序的事件。这就是为什么我们将代码添加到窗体加载事件处理程序中的原因,以及其他原因。

订阅 `SubreportProcessing` 事件后,将运行时列表绑定到主报告的绑定源控件。请注意,我们只绑定主报告,并将其绑定到层次结构顶部的列表。我们不直接绑定子报告。相反,我们在主报告中处理每个对象时设置其数据源。

这项工作在 `LocalReport_SubreportProcessing` 事件处理程序中完成。代码如下:

private void LocalReport_SubreportProcessing(object sender, 
                         SubreportProcessingEventArgs e)
{
    // Use author ID from event args to get project
    int authorID = Int32.Parse(e.Parameters["AuthorID"].Values[0]);
    AuthorItem currentAuthor = m_Authors.GetAuthor(authorID);

    // Set author's Books property as subreport data source
    ReportDataSource dataSourceAuthorsBooks = new 
       ReportDataSource("ObjectDataBinding_Model_BookList", 
       currentAuthor.Books);
    e.DataSources.Add(dataSourceAuthorsBooks);
}

`SubreportProcessing` 事件在 `ReportViewer` 处理主报告中的页面时,每次到达子报告块时都会触发。它使用其 `EventArgs` 将设计时指定的任何参数传递给子报告。我们在事件处理程序中通过读取 `EventArgs` 的 `Parameters` 属性来拦截这些参数。在演示应用程序中,我们拦截 `AuthorID` 参数。

接下来,我们使用 ID 来获取与主报表当前正在处理的作者对应的 `AuthorItem` 对象。最后,我们为子报表创建一个新的 `ReportDataSource` 对象,并使用它来传递 `AuthorItem.Books` 属性。子报表使用 `Books` 集合来填充其表格。

请注意,我们传递给 `ReportDataSource` 构造函数的名称必须与设计时赋予子报表的数据源名称相同。为什么?因为我们正在用运行时数据源替换该设计时数据源。要查找设计时名称,请在报表设计器中打开子报表,然后从“报表”菜单中选择“数据源”。设计时数据源应该是“报表数据源”列表中唯一的项。

总而言之,这有点像一场火灾演习,也不是微软最优雅的作品。但是当你设置了几个主从报告之后,这就不算什么了。毕竟,我们只谈论四行代码!而且结果绝对值得。

结论

至此,我们对面向对象程序员的 ADO.NET 之旅就结束了。在本文中,我们看到了:

  • 一个基于 MVC、DAO 和命令模式的简单灵活的应用程序架构。通过序列化,该架构可以扩展到各种分布式计算环境中工作。
  • 如何在 Visual Studio 2005 中像数据集一样轻松地数据绑定对象,以及如何为更新和报告创建主从视图。
  • 提供对扩展数据绑定功能(如排序和搜索)支持的基集合类。

欢迎您在自己的应用程序中使用 `FSBindingList` 和 `FSBindingObject` 类,只要您注明这些类的来源(我和上面引用的 MSDN 文章)。通过节省编写 UI 管道代码的时间,也许我们都能花更多时间陪伴家人。我妻子说她会很喜欢这样。

参考文献

  • *代码异味 (Code Smells)*:该术语归因于 Kent Beck 和 Martin Fowler,并在 Fowler 的著作《重构——改善现有代码的设计》(Addison-Wesley 2000) 第 3 章中进行了解释。
  • *四人帮 (The Gang of Four)*:撰写软件设计模式奠基性著作的四位作者。Gamma 等人,《设计模式——可复用面向对象软件的元素》(Addison Wesley 1995)。该书常被称为“GoF 书”。
  • *Hummel 网络广播系列*:来自 Lake Forest College 的 Joe Hummel 博士正在为 MSDN 呈现一系列非常棒的关于应用程序架构的网络广播。这些网络广播已存档供按需观看,可在此处访问这里
  • *单一职责原则 (The Single Responsibility Principle)*:Martin, Robert C., Agile Software Development (Prentice-Hall 2003),第 95 页。
  • *太妃糖棒棒糖 (Tootsie-Roll Pop)*:为了那些不住在美国的人,太妃糖棒棒糖是一种圆形的有嚼劲的巧克力块,外面裹着大约五厘米厚的硬糖壳。美国孩子们争论是最好含着硬糖直到它溶解,还是直接咬碎它以吃到巧克力。这让我想起了关于 Java 与 C# 的争论。
© . All rights reserved.