具有 ADO.NET 和自定义对象的数据绑定应用程序

具有 ADO.NET 和自定义对象的数据绑定应用程序
Windows Forms 绑定控件显著改进了过去的数据绑定控件。它们可使您快速处理与设置窗体有关的冗余任务,您也可以对其行为进行广泛地自定义和扩展。数据可在各种容器中传输,包括 DataSet 和自定义类实体,Windows ® Forms 绑定工具使您能够绑定到所有这些类型的对象。如果不想使用 DataSet,可以创建自定义实体以用作您的应用程序的数据存储,并可以使用 List<T> 和其他集合类型来存储自定义实体集。可使用 BindingSource 和 BindingNavigator 轻松地绑定这些类型的自定义实体。在本专栏中,我将说明如何使用 Microsoft ® .NET Framework 2.0 中的现有绑定工具绑定业务实体的自定义列表,我也将为此而编写一个功能完善的数据驱动 Windows Forms 应用程序。
首 先介绍此应用程序,特别是它的 DataGridView、BindingSource 和 BindingNavigator 绑定控件的使用。然后介绍较低的层次并演示它们的体系结构以及如何对数据进行检索、保留、访问和发回数据库。示例应用程序的所有代码都包括在本期的下载文 件中。
测试驱动应用程序
此应用程序将允许用户查看、添加、删除、更新、查找和导航记录。它会将 Northwind 订单数据加载到 DataGridView,如 图 1 所示。选择订单后,窗体右侧的 TextBox、ComboBox 和其他控件会填充所选订单的信息。所有控件都可通过 BindingSource 控件绑定到同一个数据源。
图 1  在 DataGridView 中查看 Northwind 订单  (单击该图像获得较大视图)
图 1 中,BindingNavigator 控件是一个跨窗体顶部显示的工具栏。它包含标准的导航按钮,用于更改屏幕将显示的订单记录。导航按钮应与左侧的网格结合使用,该网格可使这些按钮与当前记 录保持同步。工具栏还包含一些用于执行添加、删除和更新订单信息的事件处理程序的按钮。最后,应用程序允许您搜索特定订单(注意望远镜图标)。
可 使用 ComboBox 控件显示代表外键引用的订单记录的字段。例如,ComboBox 可用来显示销售人员(即雇员)的名单。特定订单的销售人员将在 ComboBox 中进行选择。这一方法比显示“雇员 ID”要更好一些,因为后者很可能对应用程序的用户没有什么意义。在 图 1 中,请注意,ComboBox 中显示的是雇员名称,而不是雇员 ID。ComboBox 中还将显示客户名称。
实现自定义实体和接口
虽 然 DataSet 是数据访问库中一个功能强大的工具,但在应用程序中使用自定义类管理和表示该数据模型也是很有效的。有关这两种方法的优缺点的讨论非常多,DataSet 或自定义类两大阵营各自都有大批的坚守者。事实上,在企业体系结构中这两种方法都是可行的。另外,ADO.NET 工具可与 DataSet 和自定义类配合使用来创建表示数据对象的实体。关键在于您必须具有某种数据存储才能包含您的数据。在本应用程序中,我将使用自定义实体。
此示例应用程序包含两个项目:一个用于表示数据,另一个用于业务逻辑和数据访问。在较低层创建自定义实体时,您必须为该实体创建属性。例如,Customer 类具有 CustomerID 属性和 CompanyName 属性。 图 2 显示表示 CustomerID 和 CompanyName 的私有字段和公共属性。尽管键入此代码会让人有些乏味,尤其与使用 DataSet 相比更是如此,但使用一些可即时生成属性的重构工具甚至代码生成工具来生成整个类,可使类的创建非常简单。

public event PropertyChangedEventHandler PropertyChanged;

private string _CustomerID;
public string CustomerID
{
    get { return _CustomerID; } set { _CustomerID = value; }
}

private string _CompanyName;
public string CompanyName
{
    get { return _CompanyName; }
    set
    {
        if (!value.Equals(_CompanyName))
        {
            _CompanyName = value;
            if (PropertyChanged != null)
            {
                PropertyChanged(this, 
                    new PropertyChangedEventArgs("CompanyName"));
            }
        }
    }
}

请看一下 图 2 中的代码。在本例中,Customer 类实现了一个 INotifyPropertyChanged 接口,如果 CompanyName 属性发生变化,该接口会触发名为 PropertyChanged 的事件。请注意,CompanyName 属性的 set 访问器将进行检查,以确保此属性值在设置新值前已经发生实际变化。如果是这样,PropertyChanged 事件会被触发且该类会通知所有侦听此更改的对象。在我的应用程序中,BindingSource 将在被通知更改时自动用新值更新窗体上的控件。
应用程序还包含以下三个实体类:Order、Customer 和 Employee。所有这些类都实现 INotifyPropertyChanged 接口并包含用于处理属性值的获取和设置的 Property 访问器。
泛型和 ADO.NET
建立实体后,必须创建一些方法用来检索和保存这些实体。此应用程序的实体实现了一个包含以下部分或全部静态方法的标准列表:GetEntity、GetEntityList、SaveEntity 和 DeleteEntity。
可为这些实体创建一个类并创建一个单独的类用来包含数据访问方法。通常,只有在体系结构显示实体与用于对其进行保存和检索的方法并不是紧密耦合时,我才会将这些类分离。在本示例应用程序中,由于方法紧密耦合,所以我选择将这些方法置于一个单独的类并使其成为静态方法。
图 3 显示 GetEntityList<Order> 方法,该方法返回表示 Northwind 数据库中的所有订单的 Order 实体列表。(当然,如果我们已针对特定客户或日期范围添加了订单参数,系统可能会对该列表进行筛选。)通过使用泛型并返回 List<T>(而不是 ArrayList),代码可保证 List 中所包含的任何对象均为类型 T。这也意味着您可以访问 List<T> 中的实体的属性,而不必将其转换为该实体类型,因为如果实体存储在非泛型列表中,您必须执行此操作。例如,您可使用以下代码从列表中的第一个订单获取 OrderID:
List<Order> orderList = GetMyListOfOrders();
int orderID = orderList[0].OrderID;

public static List<Order> GetEntityList()
{
    List<Order> OrderList = new List<Order>();

    using (SqlConnection cn = new SqlConnection(Common.ConnectionString))
    {
        string proc = "pr_Order_GetList";
        using (SqlCommand cmd = new SqlCommand(proc, cn))
        {
            cmd.CommandType = CommandType.StoredProcedure;
            cn.Open();
            SqlDataReader rdr = cmd.ExecuteReader(
                CommandBehavior.CloseConnection);
            while (rdr.Read())
            {
                Order order = FillOrder(rdr);
                OrderList.Add(order);
            }
            if (!rdr.IsClosed) rdr.Close();
        }
    }
    return OrderList;
}

如果实体存储在非泛型列表中,那么必须将对象转换为该类型:
ArrayList orderList = GetMyListOfOrders();
int orderID = ((Order)orderList[0])).OrderID;
我本可以使用订单创建和填充 DataSet 或 DataTable。但我却选择了使用 SqlDataReader,因为我更喜欢更快速地访问它所提供的数据。如 图 3 所示,我通过 SqlDataReader 获取数据,为每个行实例化并填充 Order 实体,然后将该实体添加到 List<Order> 中,并在 SqlDataReader 中对每行重复这一过程。我本来还可以使用 DataTable 并对 DataRow 进行迭代。性能差异是微不足道的,但对于本例来说,造成 DataTable 额外开销确实没有什么好处,因为我只是迭代了各个行和填充自己的自定义实体列表。FillOrder 方法执行以下代码,从而创建 Order 实例并从 SqlDataReader 设置其属性:
Order order = new Order();
order.OrderID = Convert.ToInt32(rdr["OrderID"]);
order.CustomerID = rdr["CustomerID"].ToString();
order.EmployeeID = Convert.ToInt32(rdr["EmployeeID"]);
order.OrderDate = Convert.ToDateTime(rdr["OrderDate"]);
order.ShipVia = rdr["ShipVia"].ToString();
order.ShipName = rdr["ShipName"].ToString();
order.ShipAddress = rdr["ShipAddress"].ToString();
order.ShipCity = rdr["ShipCity"].ToString();
order.ShipCountry = rdr["ShipCountry"].ToString();
return order;
请注意,在 图 3 中,我将 CommandBehavior.CloseConnection 传递给了 ExecuteReader 方法。这将使 SqlConnection 对象在 SqlDataReader 关闭时立即关闭。
实体中还有一些静态方法可用于插入、更新和删除数据。我已创建了一个名为 SaveEntity 的公共静态方法,它将接受实体并确定是新实体还是现有实体,然后调用相应的存储过程以执行动作查询。Customer 类的静态 AddEntity(在 图 4 中显示)接受 Order 实体,然后将该实体的值映射到存储过程的相应参数。

private static Order AddEntity(Order order)
{
    int orderID = 0;
    using (SqlConnection cn = new SqlConnection(Common.ConnectionString))
    {
        string proc = "pr_Order_Add";
        using (SqlCommand cmd = new SqlCommand(proc, cn))
        {
            cmd.CommandType = CommandType.StoredProcedure;
            cmd.Parameters.AddWithValue("@customerID", order.CustomerID);
            cmd.Parameters.AddWithValue("@employeeID", order.EmployeeID);
            cmd.Parameters.AddWithValue("@orderDate", order.OrderDate);
            cmd.Parameters.AddWithValue("@shipVia", order.ShipVia);
            cmd.Parameters.AddWithValue("@shipName", 
                GetValue(order.ShipName));
            cmd.Parameters.AddWithValue("@shipAddress", 
                GetValue(order.ShipAddress));
            cmd.Parameters.AddWithValue("@shipCity", 
                GetValue(order.ShipCity));
            cmd.Parameters.AddWithValue("@shipCountry", 
                GetValue(order.ShipCountry));
            cmd.Parameters.Add(new SqlParameter("@orderID", 
                SqlDbType.Int));
            cmd.Parameters["@orderID"].Direction = 
                ParameterDirection.Output;
            cn.Open();
            cmd.ExecuteNonQuery();
            orderID = Convert.ToInt32(cmd.Parameters["@orderID"].Value);
        }
        order = GetEntity(orderID);
    }
    return order;
}

ADO.NET 和自定义实体都是此体系结构的重要部分。检索方法使用 ADO.NET 从数据库获取数据;然后填充自定义实体并将其返回到表示层。SaveEntity 和 DeleteEntity 方法接受自定义实体,然后提取这些实体的值以对数据库应用更改。
数据源
我已经介绍了如何创建、填充和返回自定义实体,下面让我们看一下表示层。由于我的类库项目中含有 Customer、Order 和 Employee 类,所以我可以使用这些类在窗体中创建绑定控件。“数据源”窗口显示可供项目使用的所有数据源。在 图 5 中,您可以看到我已经将在类库项目中创建的三个实体添加到 UI 项目的“数据源”窗口中。
图 5  数据源窗口 
可 从 Web 服务、对象或数据库获取数据源。在本例中,我已通过使用类实体添加了对象数据源。我已通过完成相应向导添加了数据源,该向导会提示您选择要作为数据源添加 的命名空间和类。如果已经引用类库项目,将显示 Customer、Employee 和 Order 类。
添加数据源后,数据源将显示在“数据源”窗口中。 图 5 中的 Order 数据源显示 Order 类的所有公共属性。每个属性名旁边都有一个图标,表示将用于显示各属性值的控件的类型。OrderDate 属性显示 DateTimePicker 控件,ShipAddress 显示 TextBox 控件。可通过单击“数据源”窗口中的属性名称文本并从下拉列表中选择其他控件来更改这些控件。例如,我已将 OrderID 属性的控件改为 Label 控件,因为我希望它是只读的。
我 不希望代表指向其他实体的引用字段的属性(如 Order.CustomerID 属性)直接显示出来。而是想为其显示更具描述性的值。例如,我已将 CustomerID 属性的控件类型改为 ComboBox,以在列表中填充所有客户数据并显示相应客户的 CompanyName 属性。我已将 CustomerID 和 EmployeeID 属性改为 ComboBox。也可以通过从列表中选择 [无] 选项(而不是控件类型)指定不想在窗体中显示属性。
设 置完我的属性和要在其中显示这些属性的控件后,我单击“数据源”窗口中的 Order 实体,然后从列表中选择“详细信息”。这允许我将 Order 数据源拖至窗体,并在窗体中自动生成显示控件和绑定控件(BindingSource 和 BindingNavigator)。这就是 图 1 中窗体右侧控件的创建方式。
绑定控件
BindingSource 是链接在 Order 实体和窗体控件之间的一个不可见控件。BindingSource 中的当前 Order 实体将显示在屏幕(可在此屏幕中查看或编辑这些控件)右侧的控件中。在本应用程序中,该实体将绑定到 List<Order> 实体(我将从 Order 类的静态方法 GetEntityList 向其传递信息)。
BindingNavigator 控件也可通过将 Order 数据源拖至窗体而创建。此控件将在窗体的顶部显示一个工具栏,默认情况下,此工具栏中将包含几个按钮,用于添加、保存和删除记录以及进行导航。由于 BindingNavigator 与 BindingSource 控件互相同步,所以当某个记录被重新定位在其中一个控件时,另一个控件将自动反映位置更改。例如,如果用户在 BindingNavigator 控件中单击“下一个”按钮,BindingSource 控件便会将其当前实体更改为下一个实体,而且右侧控件将相应显示当前实体的属性值。
BindingNavigator 的按钮也属于控件,因此它们也有一些属性和事件可以进行设置。我已为工具栏上的“保存”和“删除”按钮添加了事件处理程序,因此我可以在将更改后的数据保留在数据库之前通过先前创建的实体静态数据访问方法添加验证和其他逻辑。
BindingNavigator 控件会处理记录移动并可提供一种保存数据更改的简便方法,您不需要包含它。通过将数据源拖动到窗体而自动创建控件后,您可以将其从窗体中删除,而不会产生 任何不良影响。当然,之后您必须编写您自己的代码以实现在整个订单列表中移动和保存更改。是否应执行此操作完全取决于您的 UI 要求。
CustomerID 的 ComboBox 当前被绑定到 Order 数据源的 CustomerID 属性(通过 BindingSource 控件)。我仍需要使用客户列表来填充 ComboBox,并显示客户的 CompanyName。我在创建了自己的类库项目后,创建了一个 Customer 实体并在其中提供了 GetEntityList<Customer> 方法。然后,我为 Customer 实体添加了数据源。我所需要做的只是将 Customer 数据源拖放到 CustomerID ComboBox 上。这将创建第二个 BindingSource 控件。ComboBox 使用这一新的 BindingSource 来加载客户列表以进行显示。因此,所选的 ComboBox 值仍绑定到 Order 的 BindingSource 控件,客户列表及其 CompanyName 属性显示在此控件中。重复这一过程以填充 Employee ComboBox 的列表。由于我将数据源放到了 ComboBox 上,因此这足以说明我并不需要其他 BindingNavigator 控件。
不 过,除了 BindingNavigator 的工具栏之外,我还想为用户提供第二种导航订单记录的方法。所以我返回到了“数据源”窗口,单击 Order 数据源,将选项改为 DataGridView 并将 Order 数据源拖至窗体中。这样便在窗体上创建了一个 DataGridView 控件,其中包含一个表示 Order 实体的各个属性的列。然后,我删除了大多数列,以便仅在网格中显示重要信息。由于窗体中已经存在一个绑定 Order 数据源的 BindingSource 控件,所以不会创建任何其他绑定控件。此时,所有控件的当前订单都完全相同,因为这些控件均链接到 Order 的 BindingSource 控件。
绑定代码
现 在,我已经设计了一些控件,但还没有为窗体编写任何代码。客户、雇员和订单的 BindingSource 控件全部设置完毕,即刻可以使用,但我尚未向其传递相应的数据。可通过首先调用每个实体的静态方法 GetEntityList,然后再检索实体的 List<T> 轻松执行此操作。之后 List<T> 会被转换为 BindingList<T>,并被设置为各个相应 BindingSource 控件的数据源。
图 6 显示如何将这三个 BindingSource 控件中的每一个设置为各自的列表。这就是用户在 UI 中导航和查看数据时所需的全部代码。我在窗体的构造函数中调用 SetupBindings 方法,因此在窗体初次加载时检索、绑定和显示数据。然后,用户可使用 BindingNavigator 工具栏上的导航按钮或通过在 DataGridView 控件中选择一行来浏览记录。但是,我们仍必须编写允许用户进行更改的事件处理程序。

private void SetupBindings()
{
    BindingList<Order> orderList = 
        new BindingList<Order>(Order.GetEntityList());
    orderBindingSource.DataSource = orderList;

    BindingList<Customer> customerList = 
        new BindingList<Customer>(Customer.GetEntityList());
    customerBindingSource.DataSource = customerList;

    BindingList<Employee> empList = 
        new BindingList<Employee>(Employee.GetEntityList());
    employeeBindingSource.DataSource = empList;
}

保存数据
我 希望应用程序允许用户通过单击工具栏上的“删除”按钮删除当前选择和显示的订单。首先,我将“删除”按钮的 DeleteItem 属性设置为“无”,以强制其使用自定义代码来执行删除。接下来,我添加执行删除的事件处理程序,它将调用名为 Delete 的私有方法,如 图 7 中所示。

private void Delete()
{
    Order order = orderBindingSource.Current as Order;
    int orderID = order.OrderID;
    DialogResult dlg = MessageBox.Show(
        string.Format("是否确实要删除订单 {0}?", 
        orderID.ToString()));
    if (dlg == System.Windows.Forms.DialogResult.OK)
    {
        Order.DeleteEntity(order);
        orderBindingSource.RemoveCurrent();
        MessageBox.Show(string.Format(
            "订单 {0} 已删除。", orderID.ToString()));
    }
}

Delete 方法从 BindingSource 的 Current 属性获取当前订单,并将其转换为实体类型 Order。然后,该方法会询问用户是否确定要删除订单。如果用户单击“确定”,Order 实体将被传递给类库项目中的 DeleteEntity 静态方法。最后,通过执行 BindingSource 控件的 RemoveCurrent 方法从 BindingSource 删除订单。
BindingNavigator 的工具栏还有一个“添加”按钮,用于将新行添加到 DataGridView 并清除窗口右侧的控件。然后,用户可输入和选择新订单的值,然后单击 BindingNavigator 工具栏上的“保存”按钮。我已向此“保存”按钮添加了一个事件处理程序,以将订单实体传递给 Order 实体的 SaveEntity 静态方法。用户还可以编辑当前的订单记录,并单击同一“保存”按钮以将编辑后的订单实体传递给 Order 实体的 SaveEntity 静态方法。SaveEntity 方法将通过检查 Order 实体的 OrderID 属性的值处理插入和更新。插入或更新实体后,通过重新获取订单列表并重置 BindingSource 的数据源来重新加载 DataGridView,从而使数据最新。
使用谓词查找实体
创 建一个友好的用户界面非常重要。如果用户知道 Order ID,就可能想要跳到特定订单。我通过将 ToolStripTextBox 控件添加到 BindingNavigator(用户可在其中输入要查找的完整或部分 OrderID)处理这种情况。然后,我添加了一个新的工具栏按钮和一个用于启动订单记录搜索的事件处理程序。 图 1 显示了这些控件以及用户可在其中按 Order ID 查找订单的工具提示文本。
使 用 Select 方法或 Find 方法在 DataTable 内查找 DataRow 非常简单。但是仅仅因为我使用自定义实体并不意味着我必须放弃类似的功能。在本例中,我必须在 List<Order> 中查找以用户在搜索控件中输入的值开头的 Order 实体。List<T> 提供了一些方法来协助在其列表中查找一个或多个项目,其中包括 Find、FindAll、FindIndex、FindLast 和 FindLastIndex 方法。我将使用 Find 方法,它接受 Predicate<T> 作为其唯一的参数。
Predicate<T> 必须为 Predicate<Order>,因为我有一个 List<Order>。Predicate 是一个委托类型,可在 List<Order> 中搜索与所定义条件(即以在搜索字段中输入的搜索值开头)相匹配的 Order 实体。创建 Predicate 之前,我首先创建了一个类以便来协助搜索(如 图 8 所示)。OrderFilter 类在其构造函数中接受此订单以进行搜索。该类还具有两个用于查找特定实体的方法。每个方法都返回一个布尔值,指示是否存在匹配项。这些方法是传递给 Predicate 的委托的基础。

private class OrderFilter
{
    private int orderID = 0;
    
    public OrderFilter(int orderID)
    {
        this.orderID = orderID;
    }

    public bool MatchesOrderID(Order order)
    {
        return order.OrderID == orderID;
    }

    public bool BeginsWithOrderID(Order order)
    {
        return order.OrderID.ToString().StartsWith(orderID.ToString());
    }
}

图 9 显示了用于查找订单和重新定位 BindingSource 的代码。首先,我从 BindingSource 获取对 Order 实体列表的引用,以使代码更易读。然后,我会创建一个 OrderFilter 类的实例,并用要搜索的订单 ID 对其进行初始化。接下来,我创建 Predicate 并向其传递 OrderFilter 类的 BeginsWithOrderID 方法。最后,我执行 List<Order> 的 Find 方法并向其传递刚刚创建的 Predicate。这将依次迭代 Order 实体的列表并将其全部传递给 OrderFilter.BeginsWithOrderID 方法。返回 true 的第一个实体将被返回,然后用于将 BindingSource 重新定位到其索引。

private void toolBtnFindOrderNumber_Click(object sender, EventArgs e)
{
    List<Order> orderList = new List<Order>(
        orderBindingSource.DataSource as BindingList<Order>);
    OrderFilter orderFilter = new OrderFilter(
        Convert.ToInt32(toolTxtFindOrderNumber.Text));
    Predicate<Order> filterByOrderID = 
        new Predicate<Order>(orderFilter.BeginsWithOrderID);
    Order order = orderList.Find(filterByOrderID);
    if (order == null)
        MessageBox.Show("未找到相符的订单", 
                         "未找到", MessageBoxButtons.OK);
    else
    {
        int index = orderBindingSource.IndexOf(order);
        orderBindingSource.Position = index;
    }
}


总结
在 本专栏中,我演示了 .NET Windows Forms 绑定控件的强大功能。绑定控件可与体系结构中的现有实体或 DataSet 一起用来快速创建绑定窗体。如果您发现这些控件缺少任何功能,可通过对窗体进行自定义来得到所需的功能。无论您是喜欢自定义实体还是 DataSet,两个工具都可通过使用数据绑定在 .NET 企业应用程序中实现。

猜你喜欢

转载自k10692081.iteye.com/blog/1542139
今日推荐