C# 面向对象编程入门指南(三)
十、实现数据访问层
在过去的几章中,您已经了解了各种面向对象的编程结构,如类、继承和多态,因为它们是在 C# 代码中实现的。已经向您介绍并练习了如何使用 Visual Studio 集成开发环境。您还应该对类结构和对象协作是如何实现的有一个牢固的理解。
现在,您已经准备好将这些部分放在一起,开发一个可工作的应用。因为大多数业务应用都涉及处理和更新后端关系数据库中的数据,所以您将看到 .NET Framework 提供了处理关系数据的功能。
读完本章后,你将理解以下内容:
- 如何使用
Connection对象建立到数据库的连接 - 如何使用一个
Command对象来执行 SQL 查询 - 如何使用一个
Command对象来执行存储过程 - 如何用
DataReader对象检索记录 - 如何填充数据表和数据集
- 如何在数据集中的表之间建立关系
- 如何编辑和更新数据集中的数据
- 如何创建实体数据模型
- 如何使用语言集成查询(LINQ)到实体框架(EF)来查询数据
- 如何使用实体框架更新数据
介绍 ADO.NET
为企业开发的大多数应用都需要与数据存储设备进行交互。数据存储可以以多种不同的形式出现:例如,在平面文件系统中,许多传统的大型机系统就是这种情况;或者在关系数据库管理系统中,如 SQL Server、Oracle 或 MySQL。您还可以在分层的文本文件结构中维护数据,如 XML。为了在这些不同的数据存储区中以一致的方式访问和使用数据 .NET Framework 提供了一组组织到System.Data名称空间中的类。这个类的集合被称为 ADO.NET。
回顾微软数据访问技术的历史,可以发现从连接模式到非连接模式的演变。在开发 20 世纪 80 年代和 90 年代早期流行的传统两层客户机-服务器应用时,打开与数据库的连接,使用实现服务器端游标的数据,并在处理完数据后关闭连接通常会更有效。这种方法的问题在 20 世纪 90 年代后期变得很明显,因为公司试图将其数据驱动的应用从传统的两层客户端-服务器应用发展到多层基于 web 的模型:打开并保持连接直到处理完成是不可伸缩的。可伸缩性是指应用在不明显降低性能的情况下处理数量不断增加的并发客户端的能力。微软将 ADO.NET 设计成高度可伸缩的。为了实现可伸缩性,微软围绕一个断开的模型设计了 ADO.NET。与数据库建立连接,在本地检索和缓存数据和元数据,然后关闭连接。
在此期间开发的传统数据访问技术的另一个问题是缺乏互操作性。具有高度互操作性的系统可以很容易地来回交换数据,而不管各种系统的实现技术如何。传统的数据访问技术依赖于专有的数据交换方法。使用这些技术,对于使用 Microsoft 技术(如 ADO(pre-1)构建的系统来说是很困难的 .NET)和 DCOM 与使用 Java 技术(如 JDBC 和 CORBA)构建的系统交换数据。整个行业都意识到,为不同系统之间的数据交换开发开放标准符合各方的最佳利益。微软已经接受了这些标准,并将对这些标准的支持纳入了 .NET 框架。
使用数据提供者
要建立到一个数据源的连接,比如一个 SQL Server 数据库,并处理它的数据,必须使用适当的 .NET 提供程序类。SQL Server 提供程序类位于System.Data.SqlClient名称空间中。还存在其他数据提供者,比如位于System.Data.OleDb名称空间中的 OleDb data provider for Oracle classes。这些提供程序中的每一个都实现了一个相似的类结构,您可以用它来与预期的数据源进行交互。表 10-1 总结了System.Data.SqlClient提供者名称空间的主要类。
表 10-1。系统中的类。Data.SqlClient 命名空间
| 班级 | 责任 |
|---|---|
SqlConnection | 与数据库建立连接和唯一会话。 |
SqlCommand | 表示要在数据库中执行的 Transact-SQL 语句或存储过程。 |
SqlDataReader | 提供从数据库中读取只进行流的方法。 |
SqlDataAdapter | 填充数据集并将更改更新回数据库。 |
SqlParameter | 表示用于在存储过程之间传递信息的参数。 |
SqlTransaction | 表示要在数据库中进行的 Transact-SQL 事务。 |
SqlError | 收集与数据库服务器返回的警告或错误相关的信息。 |
SqlException | 定义当数据库服务器返回警告或错误时引发的异常。 |
类似的一组类存在于System.Data.OleDb提供者名称空间中。例如,您有一个OleDbConnection类,而不是SqlConnection类。
建立连接
从数据库中检索数据的第一步是建立一个连接,,这是使用一个基于所使用的提供者类型的Connection对象来完成的。为了建立到 SQL Server 的连接,您实例化了一个类型为SqlConnection的Connection对象。您还需要为Connection对象提供一个ConnectionString。ConnectionString由一系列分号分隔的名称-值对组成,提供连接数据库服务器所需的信息。ConnectionString通常传递的一些信息是目标服务器的名称、数据库的名称和安全信息。以下代码演示了一个用于连接到 SQL Server 数据库的ConnectionString:
"Data Source=TestServer;Initial Catalog=Pubs;User ID=Dan;Password=training"
您需要通过 ConnectionString 提供的属性取决于您使用的数据提供程序。下面的代码演示了一个 ConnectionString,该 ConnectionString 使用用于 Access 的 OleDb 访问接口连接到 Access 数据库:
"Provider=Microsoft.Jet.OleDb.4.0;Data Source=D:\Data\Northwind.mdb"
下一步是调用Connection对象的Open方法。这将导致Connection对象加载适当的驱动程序并打开到数据源的连接。一旦连接打开,您就可以处理数据了。完成与数据库的交互后,调用Connection对象的Dispose方法很重要,因为当Connection对象超出范围或被垃圾收集时,连接不会被隐式释放。以下代码演示了在 SQL Server 中打开与 Pubs 数据库的连接、处理数据以及释放连接的过程:
SqlConnection pubConnection = new SqlConnection();
string connString;
try
{
connString = "Data Source=drcsrv01;Initial Catalog=pubs;Integrated Security=True";
pubConnection.ConnectionString = connString;
pubConnection.Open();
//work with data
}
catch (SqlException ex)
{
throw ex;
}
finally
{
pubConnection.Dispose();
}
C# 提供了一个using语句来帮助确保连接被关闭,资源被正确处理。当执行离开using语句的范围时,连接会自动关闭,连接对象会被高效地处理掉。前面的代码可以重写以利用using语句,如下所示:
string connString = "Data Source=drcsrv01;Initial Catalog=pubs;Integrated Security=True";
using(SqlConnection pubConnection = new SqlConnection())
{
try
{
pubConnection.ConnectionString = connString;
pubConnection.Open();
//work with data
}
catch (SqlException ex)
{
throw ex;
}
}
执行命令
一旦应用建立并打开了与数据库的连接,就可以对其执行 SQL 语句。一个Command对象存储并执行针对数据库的命令语句。您可以使用Command对象来执行数据存储理解的任何有效的 SQL 语句。对于 SQL Server,这些语句可以是数据操作语言语句(Select、Insert、Update 和 Delete)、数据定义语言语句(Create、Alter 和 Drop)或数据控制语言语句(Grant、Deny 和 Revoke)。Command对象的CommandText属性保存将要提交的 SQL 语句。根据返回的内容,Command对象包含三种向数据库提交 CommandText的方法。如果记录被返回,就像执行Select语句的情况一样,那么您可以使用ExecuteReader。如果返回单个值——例如,Select Count聚合函数的结果——您应该使用ExecuteScalar方法。当查询没有返回记录时——例如,从Insert语句中——您应该使用ExecuteNonQuery方法。下面的代码演示了如何使用一个Command对象对 Pubs 数据库执行一条 SQL 语句,返回雇员的数量。注意嵌套的using子句的使用,一个用于连接,一个用于命令。
public int GetEmployeeCount()
{
string connString = "Data Source=drcsrv01;Initial Catalog=pubs;Integrated Security=True";
using (SqlConnection pubConnection = new SqlConnection())
{
using (SqlCommand pubCommand = new SqlCommand())
{
try
{
pubConnection.ConnectionString = connString;
pubConnection.Open();
pubCommand.Connection = pubConnection;
pubCommand.CommandText = "Select Count(emp_id) from employee";
return (int)pubCommand.ExecuteScalar();
}
catch (SqlException ex)
{
throw ex;
}
}
}
}
使用存储过程
在许多应用设计中,客户端必须执行存储过程,而不是直接执行 SQL 语句。存储过程是封装数据库逻辑、提高可伸缩性和增强多层应用安全性的一种极好的方式。要执行一个存储过程,可以使用一个Command对象,将其CommandType属性设置为StoredProcedure,将其CommandText属性设置为存储过程的名称。下面的代码执行一个存储过程,返回 Pubs 数据库中的雇员人数:
public int GetEmployeeCount()
{
string connString = "Data Source=drcsrv01;Initial Catalog=pubs;Integrated Security=True";
using (SqlConnection pubConnection = new SqlConnection())
{
using (SqlCommand pubCommand = new SqlCommand())
{
try
{
pubConnection.ConnectionString = connString;
pubConnection.Open();
pubCommand.Connection = pubConnection;
pubCommand.CommandType = CommandType.StoredProcedure;
pubCommand.CommandText = "GetEmployeeCount";
return (int)pubCommand.ExecuteScalar();
}
catch (SqlException ex)
{
throw ex;
}
}
}
}
执行存储过程时,通常必须提供输入参数。您可能还需要通过输出参数来检索存储过程的结果。要使用参数,您需要实例化一个类型为SqlParameter的参数对象,然后将其添加到Command对象的Parameters集合中。构造参数时,需要提供参数名称和 SQL Server 数据类型。对于某些数据类型,还需要提供大小。如果参数是输出、输入输出或返回参数,则必须指示参数方向。以下示例调用接受字母输入参数的存储过程。该过程返回姓氏以给定字母开头的雇员数。计数以输出参数的形式返回。
public int GetEmployeeCount(string lastInitial)
{
string connString = "Data Source=drcsrv01;Initial Catalog=pubs;Integrated Security=True";
using (SqlConnection pubConnection = new SqlConnection(connString))
{
using (SqlCommand pubCommand = new SqlCommand())
{
try
{
pubConnection.Open();
pubCommand.Connection = pubConnection;
pubCommand.CommandText = "GetEmployeeCountByLastInitial";
SqlParameter inputParameter = pubCommand.Parameters.Add
("@LastInitial", SqlDbType.NChar, 1);
inputParameter.Value = lastInitial.ToCharArray()[0];
SqlParameter outputParameter = pubCommand.Parameters.Add
("@EmployeeCount", SqlDbType.Int);
outputParameter.Direction = ParameterDirection.Output;
pubCommand.CommandType = CommandType.StoredProcedure;
pubCommand.ExecuteNonQuery();
return (int)outputParameter.Value;
}
catch (SqlException ex)
{
throw ex;
}
}
}
}
使用 DataReader 对象检索数据
一个DataReader对象通过一个只进、只读的流访问数据。通常,您会希望遍历一组记录并按顺序处理结果,而不需要在缓存中维护数据。这方面的一个很好的例子是用数据库返回的值加载一个列表或数组。在声明了一个类型为SqlDataReader的对象后,通过调用一个Command对象的ExecuteReader方法来实例化它。DataReader对象的Read方法访问返回的记录。在处理完记录后,调用DataReader对象的Close方法。以下代码演示了如何使用DataReader对象从 SQL Server 数据库中检索姓名列表并将其返回给客户端:
public ArrayList ListNames()
{
string connString = "Data Source=LocalHost;Initial Catalog=pubs;Integrated Security=True";
using (SqlConnection pubConnection = new SqlConnection(connString))
{
using (SqlCommand pubCommand = new SqlCommand())
{
try
{
pubConnection.ConnectionString = connString;
pubConnection.Open();
pubCommand.Connection = pubConnection;
pubCommand.CommandText =
"Select lname from employee";
using (SqlDataReader employeeDataReader = pubCommand.ExecuteReader())
{
ArrayList nameArray = new ArrayList();
while (employeeDataReader.Read())
{
nameArray.Add(employeeDataReader["lname"]);
}
return nameArray;
}
}
catch (SqlException ex)
{
throw ex;
}
}
}
}
使用 DataAdapter 检索数据
在许多情况下,您需要从数据库中检索一组数据,处理这些数据,并将对数据的任何更新返回给数据库。在这种情况下,您使用一个DataAdapter作为数据源和数据的内存缓存之间的桥梁。这种数据的内存缓存包含在独立的 DataTable 或 DataSet 中,DataSet 是 DataTable 的集合。
注意
DataTable和DataSet对象将在“使用数据表和数据集”一节中详细讨论。
要从数据库中检索一组数据,首先要实例化一个DataAdapter对象。然后将DataAdapter的SelectCommand属性设置为现有的Command对象。最后,执行Fill方法,传递要填充的DataSet对象的名称。如果调用 fill 方法时 connection 对象没有打开,则它会打开以检索数据,然后关闭。如果调用 fill 方法时它是打开的,则在检索数据后它将保持打开状态。在这里,您将看到如何使用一个DataAdapter来填充一个DataTable并将DataTable传递回客户端:
public DataTable GetEmployees()
{
string connString = "Data Source=LocalHost;Initial Catalog=pubs;Integrated Security=True";
using (SqlConnection pubConnection = new SqlConnection(connString))
{
using (SqlCommand pubCommand = new SqlCommand())
{
pubCommand.Connection = pubConnection;
pubCommand.CommandText = "Select emp_id, lname, Hire_Date from employee";
using (SqlDataAdapter employeeAdapter = new SqlDataAdapter())
{
employeeAdapter.SelectCommand = pubCommand;
DataTable employeeDataTable = new DataTable();
employeeAdapter.Fill(employeeDataTable);
return employeeDataTable;
}
}
}
}
您可能会发现,与传入 SQL 语句相比,您需要通过执行存储过程来检索一组数据。下面的代码演示如何执行接受输入参数并返回一组记录的存储过程。记录被加载到一个DataSet对象中并返回给客户端。
public DataSet GetEmployees(string lastInitial)
{
string connString = "Data Source=LocalHost;Initial Catalog=pubs;Integrated Security=True";
using (SqlConnection pubConnection = new SqlConnection(connString))
{
using (SqlCommand pubCommand = new SqlCommand())
{
pubCommand.Connection = pubConnection;
pubCommand.CommandText = "Select emp_id, lname, Hire_Date from employee";
pubCommand.CommandText = "GetEmployeesByLastInitial";
SqlParameter inputParameter = pubCommand.Parameters.Add
("@LastInitial", SqlDbType.NChar, 1);
inputParameter.Value = lastInitial.ToCharArray()[0];
pubCommand.CommandType = CommandType.StoredProcedure;
using (SqlDataAdapter employeeAdapter = new SqlDataAdapter())
{
employeeAdapter.SelectCommand = pubCommand;
DataSet employeeDataSet = new DataSet();
employeeAdapter.Fill(employeeDataSet);
return employeeDataSet;
}
}
}
}
活动 10-1。从 SQL SERVER 数据库中检索数据
在本活动中,您将熟悉以下内容:
- 建立与 SQL Server 数据库的连接
- 通过一个
Command对象执行查询 - 使用
DataReader对象检索数据 - 使用
Command对象执行存储过程
注意要完成本章中的活动,您必须能够访问安装了 Microsoft Pubs 和 Northwind 示例数据库的 SQL Server 2005 或更高版本的数据库服务器。您必须使用被授予访问这些数据库的适当权限的 Windows 帐户登录。根据您的设置,您可能需要更改
ConnectionString。有关更多信息,请参考简介和附录 c 中的“软件要求”一节。
创建连接并执行 SQL 查询
要创建连接并执行 SQL 查询,请按照下列步骤操作:
-
启动 Visual Studio。选择文件
新建
项目。
-
选择控制台应用项目。将项目命名为
Activity10_1。 -
项目打开后,向名为
Author的项目添加一个新类。 -
Open the
Authorclass code in the code editor. Add the following using statements at the top of the file:using System.Data;using System.Data.SqlClient; -
Add code to declare a private class-level variable containing the connection string:
class Author{string _connString = "Data Source=localhost;Initial Catalog=pubs;Integrated Security=True"; -
Add a method to the class that will use a
Commandobject to execute a query to count the number of authors in theAuthorstable. Because you are only returning a single value, you will use theExecuteScalarmethod of theCommandobject.public int CountAuthors(){using (SqlConnection pubConnection = new SqlConnection(_connString)){using (SqlCommand pubCommand = new SqlCommand()){pubCommand.Connection = pubConnection;pubCommand.CommandText = "Select Count(au_id) from authors";pubConnection.Open();return (int)pubCommand.ExecuteScalar();}}} -
Add the following code to the
Mainmethod of theProgramclass, which will execute theGetAuthorCountmethod defined in theAuthorclass:static void Main(string[] args){try{Author author = new Author();Console.WriteLine(author.CountAuthors());Console.ReadLine();}catch (Exception ex){Console.WriteLine(ex.Message);Console.ReadLine();}} -
选择调试
开始运行项目。控制台窗口应该会启动,并显示作者的数量。查看输出后,停止调试器。
使用 DataReader 对象检索记录
要使用DataReader对象来检索记录,请遵循以下步骤:
-
在代码编辑器中打开
Author类代码。 -
Add a public method to the class definition called
GetAuthorListthat returns a genericListof strings.public List<string> GetAuthorList(){} -
Add the following code, which executes a SQL Select statement to retrieve the authors’ last names. A
DataReaderobject then loops through the records and creates a list of names that gets returned to the client.List<string> nameList = new List<string>();using (SqlConnection pubConnection = new SqlConnection(_connString)){using (SqlCommand authorsCommand = new SqlCommand()){authorsCommand.Connection = pubConnection;authorsCommand.CommandText = "Select au_lname from authors";pubConnection.Open();using (SqlDataReader authorDataReader = authorsCommand.ExecuteReader()){while (authorDataReader.Read() == true){nameList.Add(authorDataReader.GetString(0));}return nameList;}}} -
Change the code in the
Mainmethod of theProgramclass to show the list of names in the console window.static void Main(string[] args){try{Author author = new Author();foreach (string name in author.GetAuthorList()){Console.WriteLine(name);}Console.ReadLine();}catch (Exception ex){Console.WriteLine(ex.Message);Console.ReadLine();}} -
选择调试
开始运行项目。控制台窗口应该会启动,并显示作者的姓名。查看输出后,停止调试器。
使用命令对象执行存储过程
要使用Command对象执行存储过程,请遵循以下步骤:
-
在代码编辑器中打开
Author类代码。 -
Add a public method that overloads the
GetAuthorListmethod by accepting an integer parameter namedRoyalty. This function will call the stored procedure by royalty in the Pubs database. The procedure takes an integer input of the royalty percentage and returns a list of author IDs with the percentage.public List<string> GetAuthorList(int royalty){List<string> nameList = new List<string>();using (SqlConnection pubConnection = new SqlConnection(_connString)){using (SqlCommand authorsCommand = new SqlCommand()){authorsCommand.Connection = pubConnection;authorsCommand.CommandType = CommandType.StoredProcedure;authorsCommand.CommandText = "byroyalty";SqlParameter inputParameter = new SqlParameter();inputParameter.ParameterName = "@percentage";inputParameter.Direction = ParameterDirection.Input;inputParameter.SqlDbType = SqlDbType.Int;inputParameter.Value = royalty;authorsCommand.Parameters.Add(inputParameter);pubConnection.Open();using (SqlDataReader authorDataReader = authorsCommand.ExecuteReader()){while (authorDataReader.Read() == true){nameList.Add(authorDataReader.GetString(0));}}return nameList;}}} -
In the
Mainmethod of theProgramclass, supply an input parameter of 25 to theGetAuthorListmethod.foreach (string name in author.GetAuthorList(25)) -
选择调试
开始运行项目。控制台窗口应该启动,并显示作者的 id。查看输出后,停止调试器。
-
完成测试后,退出 Visual Studio。
使用数据表和数据集
数据表和数据集是数据的内存缓存,提供一致的关系编程模型来处理数据,而不管数据源是什么。数据表代表一个关系数据表,由列、行和约束组成。您可以将数据集视为一个小型关系数据库,它包括数据表和它们之间的关系完整性约束。如果从单个表中检索数据,可以直接填充和使用 DataTable,而无需先创建数据集。创建数据表或数据集有几种方法。最明显的方法是从现有的关系数据库管理系统(RDBMS )(如 SQL Server 数据库)中填充 DataTable 或 DataSet。如前所述,DataAdapter对象提供了 RDBMS 和数据表或数据集之间的桥梁。通过使用一个DataAdapter对象,数据表或数据集完全独立于数据源。尽管您需要使用一组特定的提供者类来加载任一类型的对象,但是您使用相同的一组 .NET Framework 类来处理数据表或数据集,而不管它是如何创建和填充的。System.Data名称空间包含处理数据表或数据集对象的框架类。表 10-2 列出了一些包含在System.Data名称空间中的主要类。
表 10-2。系统的主要成员。数据命名空间
| 班级 | 描述 |
|---|---|
DataSet | 表示一组DataTable和DataRelation对象。组织关系数据的内存缓存。 |
DataTable | 代表一个由DataColumn、DataRow和Constraint对象组成的集合。组织与数据实体相关的记录和字段。 |
DataColumn | 表示DataTable中某一列的模式。 |
DataRow | 代表DataTable中的一行数据。 |
Constraint | 表示可以在DataColumn对象上实施的约束。 |
ForeignKeyConstraint | 在两个DataTable对象之间实施父/子关系的参照完整性。 |
UniqueConstraint | 强制一个DataColumn或一组DataColumns的唯一性。这是在父/子关系中实施参照完整性所必需的。 |
DataRelation | 表示两个DataTable对象之间的父/子关系。 |
从 SQL Server 数据库填充数据表
要从数据库中检索数据,需要使用一个Connection对象建立与数据库的连接。建立连接后,创建一个Command对象从数据库中检索数据。如前所述,如果您从单个表或结果集中检索数据,您可以直接填充并使用DataTable,而无需创建DataSet对象。DataTable的 Load 方法用一个DataReader对象的内容填充表格。下面的代码用 Pubs 数据库的 publishers 表中的数据填充了一个DataTable:
public DataTable GetPublishers()
{
string connString = "Data Source=drcsrv01;" +
"Initial Catalog=pubs;Integrated Security=True";
DataTable pubTable;
using (SqlConnection pubConnection = new SqlConnection(connString))
{
using (SqlCommand pubCommand = new SqlCommand())
{
pubCommand.Connection = pubConnection;
pubCommand.CommandText =
"Select pub_id, pub_name, city from publishers";
pubConnection.Open();
using (SqlDataReader pubDataReader = pubCommand.ExecuteReader())
{
pubTable = new DataTable();
pubTable.Load(pubDataReader);
return pubTable;
}
}
}
}
从 SQL Server 数据库填充数据集
当您需要将数据加载到多个表中并维护表之间的引用完整性时,您需要使用DataSet对象作为DataTables的容器。为了从数据库中检索数据并填充DataSet,使用Connection对象建立与数据库的连接。建立连接后,创建一个Command对象从数据库中检索数据,然后创建一个DataAdapter来填充DataSet,将之前创建的Command对象设置为DataAdapter的SelectCommand属性。为每个DataTable创建一个单独的DataAdapter。最后一步是通过执行DataAdapter的Fill方法用数据填充DataSet。下面的代码演示了用 Pubs 数据库的 publishers 表和 titles 表中的数据填充一个DataSet:
public DataSet GetPubsDS()
{
string connString = "Data Source=Localhost;" +
"Initial Catalog=pubs;Integrated Security=True";
DataSet bookInfoDataSet= new DataSet();
using (SqlConnection pubConnection = new SqlConnection(connString))
{
//Fill pub table
using (SqlCommand pubCommand = new SqlCommand())
{
pubCommand.Connection = pubConnection;
pubCommand.CommandText =
"Select pub_id, pub_name, city from publishers";
using (SqlDataAdapter pubDataAdapter = new SqlDataAdapter())
{
pubDataAdapter.SelectCommand = pubCommand;
pubDataAdapter.Fill(bookInfoDataSet, "Publishers");
}
}
//Fill title table
using (SqlCommand titleCommand = new SqlCommand())
{
titleCommand.Connection = pubConnection;
titleCommand.CommandText =
"Select pub_id, title, ytd_sales from titles";
using (SqlDataAdapter titleDataAdapter = new SqlDataAdapter())
{
titleDataAdapter.SelectCommand = titleCommand;
titleDataAdapter.Fill(bookInfoDataSet, "Titles");
}
}
return bookInfoDataSet;
}
}
在数据集中的表之间建立关系
在 RDBMS 系统中,表之间的引用完整性是通过主键和外键关系来实现的。使用一个DataRelation对象,您可以在DataSet中的表之间实施数据引用完整性。该对象包含一个DataColumn对象数组,这些对象定义了父表和子表之间用于建立关系的公共字段。本质上,父表中标识的字段是主键,子表中标识的字段是外键。建立关系时,为每个表中的公共列创建两个DataColumn对象。接下来,创建一个DataRelation对象,为DataRelation、传递一个名称,并将DataColumn对象传递给DataRelation对象的构造函数。最后一步是将DataRelation添加到DataSet对象的Relations集合中。下面的代码在出版商和上一节中创建的bookInfoDataSet的标题表之间建立了一个关系:
//Create relationahip between tables
DataRelation Pub_TitleRelation;
DataColumn Pub_PubIdColumn;
DataColumn Title_PubIdColumn;
Pub_PubIdColumn = bookInfoDataSet.Tables["Publishers"].Columns["pub_id"];
Title_PubIdColumn = bookInfoDataSet.Tables["Titles"].Columns["pub_id"];
Pub_TitleRelation = new DataRelation("PubsToTitles", Pub_PubIdColumn, Title_PubIdColumn);
bookInfoDataSet.Relations.Add(Pub_TitleRelation);
编辑数据集中的数据
客户端通常需要能够更新数据集。他们可能需要添加记录、删除记录或更新现有记录。因为数据集对象在设计上是断开连接的,所以对数据集所做的更改不会自动传播回数据库。它们保存在本地,直到客户机准备好将更改复制回数据库。要复制更改,您需要调用DataAdapter的Update方法,该方法确定对记录做了什么更改,并实现适当的 SQL 命令(Update、Insert 或 Delete ),该命令已被定义为将更改复制回数据库。
当您创建一个Update命令时,命令文本引用命令的Parameters集合中与数据表中的字段相对应的参数。
SqlCommand updateCommand = new SqlCommand();
string updateSQL = "Update publishers set pub_name = @pub_name," +
" city = @city where pub_id = @pub_id";
updateCommand = new SqlCommand(updateSQL, pubConnection);
updateCommand.CommandType = CommandType.Text;
对于Update语句中的每个参数,Parameter对象被添加到Command对象的Parameter集合中。向Parameters集合的Add方法传递关于参数名、SQL 数据类型、大小和数据集的源列的信息。
updateCommand.Parameters.Add("@pub_id", SqlDbType.Char, 4, "pub_id");
updateCommand.Parameters.Add("@city", SqlDbType.VarChar, 20, "city");
updateCommand.Parameters.Add("@pub_name", SqlDbType.VarChar, 40, "pub_name");
Once the parameters are created, the updateCommand object is set to the UpdateCommand property of the DataAdapter object.
pubDataAdapter.UpdateCommand = updateCommand;
现在已经设置了 SqlDataAdapter,您调用 SQLDataAdapter 的Update方法,传入DataSet和要更新的表的名称。SQLDataAdapter 检查表中行的RowState属性,并对要更新的每一行迭代执行 update 语句。
pubDataAdapter.Update(bookInfoDataSet, "Publishers");
以类似的方式,您可以实现DataAdapter的InsertCommand和DeleteCommand属性,以允许客户端在数据库中插入新记录或删除记录。
注意对于数据源中单个表的简单更新 .NET 框架提供了一个
CommandBuilder类来自动创建DataAdapter的InsertCommand、UpdateCommand和DeleteCommand属性。
活动 10-2。使用数据集对象
在本活动中,您将熟悉以下内容:
- 从 SQL Server 数据库填充一个
DataSet - 编辑
DataSet中的数据 - 将更改从
DataSet更新到数据库 - 在
DataSet中建立表之间的关系
从 SQL Server 数据库填充数据集
要从 SQL Server 数据库填充一个DataSet,请执行以下步骤:
-
启动 Visual Studio。选择文件
新建
项目。
-
选择 Windows 窗体应用。将项目重命名为 Activity10_2,然后单击“确定”按钮。
-
项目打开后,向名为
Author的项目添加一个新类。 -
Open the
Authorclass code in the code editor. Add the followingusingstatements at the top of the file:using System.Data;using System.Data.SqlClient; -
Add the following code to declare a private class level variable for the connection string.
class Author{string _connString = "Data Source=localhost;" + "Initial Catalog=pubs;Integrated Security=True"; -
Create a method of the
Authorclass calledGetDatathat will use aDataAdapterobject to fill theDataSetand return it to the client.public DataSet GetData(){DataSet authorDataSet;using (SqlConnection pubConnection = new SqlConnection()){pubConnection.ConnectionString = _connString;using(SqlCommand selectCommand = new SqlCommand()){selectCommand.CommandText = "Select au_id, au_lname,au_fname from authors";selectCommand.Connection = pubConnection;using (SqlDataAdapter pubDataAdapter = new SqlDataAdapter()){pubDataAdapter.SelectCommand = selectCommand;authorDataSet = new DataSet();pubDataAdapter.Fill(authorDataSet, "Author");}}return authorDataSet;} -
构建项目并修复任何错误。
-
Add the controls listed in Table 10-3 to Form1 and set the properties as shown.
表 10-3。表格 1 控件
控制 财产 价值 DataGridViewNamedgvAuthorsAllowUserToAddRowsFalseAllowUserToDeleteRowsFalseReadOnlyFalseButtonNamebtnGetDataTextGet DataButtonNamebtnUpdateTextUpdate -
Open the
Form1class code file in the code editor. Declare a class-levelDataSetobject after the class declaration.public partial class Form1 : Form{private DataSet _pubDataSet; -
在窗体设计器中打开 Form1。双击“获取数据”按钮,在代码编辑器中打开按钮单击事件方法。
-
Add the following code to the
btnGetDataclick event procedure, which will execute theGetDatamethod defined in theAuthorclass. This dataset is then loaded into the grid using theDataSourceproperty.
`private void btnGetData_Click(object sender, EventArgs e)`
`{`
`Author author = new Author();`
`_pubDataSet = author.GetData();`
`dgvAuthors.DataSource = _pubDataSet.Tables["Author"];`
`}`
12. 构建项目并修复任何错误。一旦项目构建完成,在调试模式下运行项目并测试GetData方法。您应该会看到网格中充满了作者信息。测试后,停止调试器。
编辑和更新数据集中的数据
要编辑和更新DataSet中的数据,请遵循以下步骤:
-
在代码编辑器中打开
Author类代码。 -
Create a method of the
Authorclass calledUpdateDatathat will use theUpdatemethod of theDataAdapterobject to pass updates made to theDataSetto the Pubs database.public void UpdateData(DataSet changedData){using (SqlConnection pubConnection = new SqlConnection()){pubConnection.ConnectionString = _connString;using (SqlCommand updateCommand = new SqlCommand()){updateCommand.CommandText = "Update authors set au_lname = @au_lname," +"au_fname = @au_fname where au_id = @au_id";updateCommand.Parameters.Add ("@au_id", SqlDbType.VarChar, 11, "au_id");updateCommand.Parameters.Add ("@au_lname", SqlDbType.VarChar, 40, "au_lname");updateCommand.Parameters.Add ("@au_lname", SqlDbType.VarChar, 40, "au_lname");updateCommand.Connection = pubConnection;using (SqlDataAdapter pubDataAdapter = new SqlDataAdapter()){pubDataAdapter.UpdateCommand = updateCommand;pubDataAdapter.Update(changedData, "Author");}}}} -
构建项目并修复任何错误。
-
在表单设计器中打开
Form1。双击“更新数据”按钮,在代码编辑器中打开按钮 click 事件方法。 -
Add the following code to the
btnUpdateclick event procedure, which will execute theUpdateDatamethod defined in theAuthorclass. By using theGetChangesmethod of theDataSetobject, only data that has changed is passed for updating.private void btnUpdate_Click(object sender, EventArgs e){Author author = new Author();author.UpdateData(_pubDataSet.GetChanges());} -
构建项目并修复任何错误。一旦项目构建完成,在调试模式下运行项目并测试
Update方法。首先,单击“获取数据”按钮。更改几个作者的姓氏,然后单击更新按钮。再次单击“获取数据”按钮,从数据库中检索已更改的值。测试后,停止调试器。
建立数据集中表格之间的关系
要在一个DataSet中建立表之间的关系,请遵循以下步骤:
-
将名为
StoreSales的新类添加到项目中。 -
Open the
StoreSalesclass code in the code editor. Add the followingusingstatements at the top of the file:using System.Data;using System.Data.SqlClient; -
Add the following code to declare private class level variables for the connection string and DataSet.
class StoreSales{string _connString = "Data Source=localhost;" + "Initial Catalog=pubs;Integrated Security=True"; -
Create a method of the
StoreSalesclass calledGetDatathat will select store information and sales information and establish a relationship between them. This information is used to fill aDataSetand return it to the client.public DataSet GetData(){DataSet storeSalesDataSet;storeSalesDataSet = new DataSet();using (SqlConnection pubConnection = new SqlConnection(_connString)){//Fill store tableusing (SqlCommand storeCommand = new SqlCommand()){storeCommand.Connection = pubConnection;storeCommand.CommandText ="SELECT [stor_id],[stor_name],[city],[state] FROM [stores]";using (SqlDataAdapter storeDataAdapter = new SqlDataAdapter()){storeDataAdapter.SelectCommand = storeCommand;storeDataAdapter.Fill(storeSalesDataSet, "Stores");}}//Fill sales table commandusing (SqlCommand salesCommand = new SqlCommand()){salesCommand.Connection = pubConnection;salesCommand.CommandText ="SELECT [stor_id],[ord_num],[ord_date],[qty] FROM [sales]";using (SqlDataAdapter salesDataAdapter = new SqlDataAdapter()){salesDataAdapter.SelectCommand = salesCommand;salesDataAdapter.Fill(storeSalesDataSet, "Sales");}}//Create relationahip between tablesDataColumn Store_StoreIdColumn =storeSalesDataSet.Tables["Stores"].Columns["stor_id"];DataColumn Sales_StoreIdColumn =storeSalesDataSet.Tables["Sales"].Columns["stor_id"];DataRelation StoreSalesRelation = new DataRelation ("StoresToSales", Store_StoreIdColumn, Sales_StoreIdColumn);``storeSalesDataSet.Relations.Add(StoreSalesRelation);`
return storeSalesDataSet;}}``* 构建项目并修复任何错误。* Add a second form to the project. Add the controls listed in Table 10-4 to Form2 and set the properties as shown.表 10-4。Form2 控件
控制 财产 价值 DataGridViewNamedgvStoresDataGridViewNamedgvSalesButtonNamebtnGetDataTextGet Data- Open the
Form2class code file in the code editor. Declare a class-levelDataSetobject after the class declaration.
public partial class Form2 : Form{DataSet StoreSalesDataSet;- 在窗体设计器中打开 Form2。双击“获取数据”按钮,在代码编辑器中打开按钮单击事件方法。* Add the following code to the
btnGetDataclick event procedure, which will execute theGetDatamethod defined in theStoreSalesclass. This Stores table is then loaded into the Stores grid using theDataSourceproperty. Setting theDataMemberproperty of the Sales grid loads it with the sales data of the store selected in the Stores grid.
private void btnGetData_Click(object sender, EventArgs e){StoreSales storeSales = new StoreSales();StoreSalesDataSet = storeSales.GetData();dgvStores.DataSource = StoreSalesDataSet.Tables["Stores"];dgvSales.DataSource =StoreSalesDataSet.Tables["Stores"];dgvSales.DataMember = "StoresToSales";}- Open the
Programclass in the code editor. Change the code to launch Form2 when the form loads.
Application.Run(new Form2());- 表单加载后,单击“获取数据”按钮加载网格。在商店网格中选择一个新行应该会更新销售网格,以显示商店的销售额。完成测试后,停止调试器并退出 Visual Studio。`
- Open the
`使用实体框架
实体框架(EF) 是内置于 ADO.NET 的对象关系映射(ORM)技术。EF 消除了 .NET 语言和数据库系统的关系数据结构。例如,要加载和使用客户对象,开发人员必须向数据库引擎发送一个 SQL 字符串。这要求开发人员熟悉数据的关系模式。此外,SQL 被硬编码到应用中,应用不会受到底层模式变化的影响。另一个缺点是,由于应用将 SQL 语句作为字符串发送到数据库引擎进行处理,Visual Studio 无法实现语法检查并发出警告和构建错误来提高程序员的工作效率。
实体框架提供了映射模式,允许程序员在更高的抽象层次上工作。他们可以使用面向对象的结构编写代码来查询和加载实体(由类定义的对象)。映射模式将针对实体的查询转换为针对数据执行 CRUD(创建、读取、更新和删除)操作所需的特定于数据库的语言。
为了在您的应用中使用实体框架,您必须首先将 ADO.NET 实体数据模型添加到您的应用中。这一步启动实体数据模型向导,,它允许您从头开始开发您的模型,或者从现有的数据库中生成它。选择从现有数据库生成它,可以创建到数据库的连接,并选择要包含在模型中的表、视图和存储过程。向导的输出是一个.edmx文件。这个文件是一个基于 XML 的文件,有三个部分。第一种由商店模式定义语言(SSDL) 组成;这描述了存储数据的表格和关系。以下代码显示了从 Pubs 数据库生成的数据模型的 SSDL 的一部分:
<EntityContainer Name="pubsModelStoreContainer">
<EntitySet Name="sales" EntityType="pubsModel.Store.sales"
store:Type="Tables" Schema="dbo" />
<EntitySet Name="stores" EntityType="pubsModel.Store.stores"
store:Type="Tables" Schema="dbo" />
<AssociationSet Name="FK__sales__stor_id__1273C1CD"
Association="pubsModel.Store.FK__sales__stor_id__1273C1CD">
<End Role="stores" EntitySet="stores" />
<End Role="sales" EntitySet="sales" />
</AssociationSet>
</EntityContainer>
<EntityType Name="sales">
<Key>
<PropertyRef Name="stor_id" />
<PropertyRef Name="ord_num" />
<PropertyRef Name="title_id" />
</Key>
<Property Name="stor_id" Type="char" Nullable="false" MaxLength="4" />
<Property Name="ord_num" Type="varchar" Nullable="false" MaxLength="20" />
<Property Name="ord_date" Type="datetime" Nullable="false" />
<Property Name="qty" Type="smallint" Nullable="false" />
<Property Name="payterms" Type="varchar" Nullable="false" MaxLength="12" />
<Property Name="title_id" Type="varchar" Nullable="false" MaxLength="6" />
</EntityType>
第二部分由概念图式定义语言(CSDL);它指定了实体以及它们之间的关系。这些实体用于处理应用中的数据。以下代码来自 Pubs 数据库生成的数据模型的 CSDL 部分:
<EntityContainer Name="pubsEntities" annotation:LazyLoadingEnabled="true">
<EntitySet Name="sales" EntityType="pubsModel.sale" />
<EntitySet Name="stores" EntityType="pubsModel.store" />
<AssociationSet Name="FK__sales__stor_id__1273C1CD"
Association="pubsModel.FK__sales__stor_id__1273C1CD">
<End Role="stores" EntitySet="stores" />
<End Role="sales" EntitySet="sales" />
</AssociationSet>
</EntityContainer>
<EntityType Name="sale">
<Key>
<PropertyRef Name="stor_id" />
<PropertyRef Name="ord_num" />
<PropertyRef Name="title_id" />
</Key>
<Property Name="stor_id" Type="String" Nullable="false"
MaxLength="4" Unicode="false" FixedLength="true" />
<Property Name="ord_num" Type="String" Nullable="false"
MaxLength="20" Unicode="false" FixedLength="false" />
<Property Name="ord_date" Type="DateTime" Nullable="false" />
<Property Name="qty" Type="Int16" Nullable="false" />
<Property Name="payterms" Type="String" Nullable="false"
MaxLength="12" Unicode="false" FixedLength="false" />
<Property Name="title_id" Type="String" Nullable="false"
MaxLength="6" Unicode="false" FixedLength="false" />
<NavigationProperty Name="store"
Relationship="pubsModel.FK__sales__stor_id__1273C1CD"
FromRole="sales" ToRole="stores" />
</EntityType>
.edmx文件的最后一部分由用映射规范语言(MSL) 编写的代码组成。MSL 将概念模型映射到存储模型。以下代码显示了从 Pubs 数据库生成的数据模型的 MSL 部分的一部分:
<EntityContainerMapping StorageEntityContainer="pubsModelStoreContainer"
CdmEntityContainer="pubsEntities">
<EntitySetMapping Name="sales"><EntityTypeMapping TypeName="pubsModel.sale">
<MappingFragment StoreEntitySet="sales">
<ScalarProperty Name="stor_id" ColumnName="stor_id" />
<ScalarProperty Name="ord_num" ColumnName="ord_num" />
<ScalarProperty Name="ord_date" ColumnName="ord_date" />
<ScalarProperty Name="qty" ColumnName="qty" />
<ScalarProperty Name="payterms" ColumnName="payterms" />
<ScalarProperty Name="title_id" ColumnName="title_id" />
</MappingFragment></EntityTypeMapping></EntitySetMapping>
Visual Studio 提供了一个可视化设计器来处理实体模型。使用这个设计器,你可以更新实体,添加关联,实现继承。在可视化模型中所做的更改被转换回。相应更新的 edmx 文件。图 10-1 显示了可视化设计器中从 pubs 数据库生成的实体。
图 10-1 。实体模型设计器中的实体
从 LINQ 到英孚查询实体
当使用实体数据模型向导创建 ADO.NET 实体数据模型时,会创建一个代表模型中定义的实体容器的ObjectContext类。ObjectContext类支持针对实体模型的基于 CRUD 的查询。针对ObjectContext类编写的查询是使用 LINQ 到 EF 编写的。如前所述,LINQ 代表语言集成查询。LINQ 允许开发人员用 C# 语法编写查询,当执行时,查询被转换成数据提供者的查询语法。一旦执行了查询并返回了数据,实体框架就将结果转换回实体对象模型。
下面的代码使用Select方法返回 Stores 表中的所有行,并将结果作为一列Store实体返回。商店名称随后被写入控制台窗口。
var context = new pubsEntities();
var query = from s in context.stores
select s;
var stores = query.ToList();
foreach (store s in stores)
{
Console.WriteLine(s.stor_name);
}
Console.ReadLine();
LINQ 到 EF 提供了一组丰富的查询操作,包括过滤、排序和分组操作。下面的代码演示了按状态过滤商店:
var context = new pubsEntities();
var query = from s in context.stores
where s.state == "WA"
select s;
var stores = query.ToList();
以下代码选择订购了 25 个以上对象的销售实体,然后按降序对它们进行排序:
var context = new pubsEntities();
var query = from s in context.sales
where s.qty > 25
orderby s.ord_date descending
select s;
var sales = query.ToList();
由于实体框架包括实体之间的导航属性,您可以轻松地基于相关实体构建复杂的查询。以下查询选择有五个以上销售订单的商店:
var context = new pubsEntities();
var query = from s in context.stores
where s.sales.Count > 5
select s;
var stores = query.ToList();
注关于 LINQ 查询语言的更多信息,请参考位于
http://msdn.microsoft.com的 MSDN 图书馆。
使用实体框架更新实体
实体框架跟踪对在Context对象中表示的实体类型所做的改变。您可以添加、更新或删除实体对象。当您准备好将更改保存回数据库时,您可以调用context对象的SaveChanges方法。EF 创建并执行针对数据库的插入、更新或删除语句。您还可以显式映射存储过程来实现数据库命令。以下代码使用商店 ID 选择一个商店,更新商店名称,并将其发送回数据库:
var context = new pubsEntities();
var store = (from s in context.stores
where s.stor_id == storeId
select s).First();
store.stor_name = "DRC Books";
context.SaveChanges();
活动 10-3。使用实体框架检索数据
在本活动中,您将熟悉以下内容:
- 创建实体数据模型
- 使用 LINQ 到 EF 执行查询
创建实体数据模型
要创建实体数据模型,请遵循以下步骤:
-
启动 Visual Studio。选择文件
新建
项目。
-
选择控制台应用。将项目重命名为 Activity10_3,然后单击“确定”按钮。
-
在解决方案资源管理器中右键单击项目节点,并选择 Add
New Item。
-
在“添加新项”窗口的“数据”节点下,选择一个 ADO.NET 实体数据模型。将模型命名为 Pubs.edmx,然后单击 Add。
-
在选择模型内容屏幕中,选择从数据库生成,然后单击下一步。
-
In the Choose Your Data Connection screen, create a connection to the Pubs database and choose Next. (See Figure 10-2)
图 10-2 。使用实体数据模型向导创建数据库连接
-
In the Choose Your Database Objects screen, expand the Tables node and select the Sales, Stores, and Titles tables, as shown in Figure 10-3. Click Finish.
图 10-3 。为实体数据模型选择数据库对象
-
You are presented with the Entity Model Designer containing the sales, store, and title entities, as shown in Figure 10-4.
图 10-4 。实体模型设计器
-
在实体模型设计器中,右击
title实体并选择重命名。改名为book。在图书实体中,将title1属性重命名为title。
查询实体数据模型
要使用 LINQ 查询该实体数据模型,请按照下列步骤操作:
-
在代码编辑器窗口中打开 Program.cs 文件。
-
Add the following method to select the book entities and write their titles to the Console window:
private static void GetTitles(){var context = new pubsEntities();var query = from b in context.books select b;var books = query.ToList();foreach (book b in books){Console.WriteLine(b.title);}Console.ReadLine();} -
Call the
GetTitlesmethod from theMainmethod.static void Main(string[] args){GetTitles();} -
在调试模式下运行程序。您应该会看到控制台窗口中列出的标题。完成测试后,停止调试器。
-
Add the following method that gets books in the 10 to 20 dollar range and orders them by price:
private static void GetTitlesByPrice(){var context = new pubsEntities();var query = from b in context.bookswhere b.price >= (decimal)10.00&& b.price <= (decimal)20.00orderby b.priceselect b;var books = query.ToList();foreach (book b in books){Console.WriteLine(b.price + " -- " + b.title);}Console.ReadLine();} -
Call the GetTitlesByPrice method from the Main method.
static void Main(string[] args){//GetTitles();GetTitlesByPrice();} -
在调试模式下运行程序。您应该会在控制台窗口中看到标题和价格。完成测试后,停止调试器。
-
Add the following method to list the book titles and the sum of their sales amount. Notice that this query gets the sales amount by adding up the book’s related sales entities.
private static void GetBooksSold(){var context = new pubsEntities();var query = from b in context.booksselect new{BookID = b.title_id,TotalSold = b.sales.Sum(s =>(int?) s.qty)};foreach (var item in query){Console.WriteLine(item.BookID + " -- " + item.TotalSold);}Console.ReadLine();} -
Call the
GetBooksSoldmethod from theMainmethod.static void Main(string[] args){//GetTitles();//GetTitlesByPrice();GetBooksSold();} -
在调试模式下运行程序。您应该会在控制台窗口中看到图书 id 和销售量。完成测试后,停止调试器并退出 Visual Studio。
摘要
本章是向您展示如何构建 OOP 应用各层的系列文章的第一章。为了实现应用的数据访问层,您学习了 about 和用于处理关系数据源的类。您看到了组成System.Data.SqlClient名称空间的各种类;这些类检索和更新存储在 SQL Server 数据库中的数据。您还研究了处理断开连接的数据的System.Data名称空间类。此外,您还接触了实体框架和 LINQ,看到了它们如何允许您使用 OOP 结构查询数据。您根据实体编写查询,框架将查询翻译成数据源的查询语法,检索数据,并加载实体。
在第十一章中,你将看到如何实现 Windows 应用的用户界面层。在这个过程中,您将进一步了解 .NET Framework 用于创建丰富的基于 Windows 的用户界面。`
十一、开发 WPF 应用
在第十章中,你学习了如何构建应用的数据访问层。为了实现它的逻辑,您使用了System.Data名称空间的类。这些类检索和处理关系数据,这是许多业务应用的常见需求。现在,您已经准备好了解用户将如何与您的应用进行交互。用户通过用户界面层与应用进行交互。这一层又与业务逻辑层交互,业务逻辑层又与数据访问层交互。在本章中,您将学习如何使用?NET Windows 演示基础(WPF)。WPF 通常用于开发在 Windows 7 及更高版本上运行的桌面业务生产力应用。商业生产力应用面向需要查询和更新存储在后端数据库中的数据的商业用户。它由一套全面的应用开发功能组成,包括可扩展应用标记语言(XAML) 、控件、数据绑定和布局。本章讨论的概念也将为第十三章奠定基础,在第十三章中,我们将着眼于为 Windows 8 创建新的 Windows Store 应用用户界面。这些应用使用类似的模型来创建具有 XAML 和数据绑定控件的界面。
阅读本章后,您将能够轻松执行以下任务:
- 使用 XAML 标记设计用户界面
- 使用布局控件
- 使用显示控件
- 响应控制事件
- 使用数据绑定控件
- 创建和使用控件模板
Windows 基础
窗口是具有可视界面的对象,绘制在屏幕上,为用户提供与程序交互的方式。像面向对象语言中的大多数对象一样 .NET 窗口公开属性、方法和事件。窗口的属性定义了它的外观。例如,它的Background属性决定了它的颜色。窗口的方法定义了它的行为。例如,调用它的Hide方法对用户隐藏它。窗口的事件定义了与用户(或其他对象)的交互。例如,当用户在窗口上单击鼠标右键时,您可以使用MouseDown事件来启动一个动作。
控件是具有可视化界面的组件,为用户提供了与程序交互的方式。窗口是一种特殊类型的控件,称为容器控件,它承载其他控件。您可以在窗口上放置许多不同类型的控件。在 windows 上使用的一些常见控件有文本框、标签、选项按钮、列表框和复选框。除了提供的控件之外 .NET Framework,您也可以创建自己的自定义控件或从第三方供应商处购买控件。
介绍 XAML
WPF 用户界面是使用一种称为 XAML 的声明性标记语言构建的。 XAML 声明组成界面的控件。左尖括号(<)后跟控件类型名称和右尖括号定义控件。例如,下面的标记在网格中定义了一个按钮控件。
```cs`
请注意,该网格需要一个正式的结束标记,因为它包含按钮控件。因为按钮控件不包含任何其他元素,所以可以在结束括号前使用正斜杠(`/`)来关闭它。
下一步是定义控件的属性。例如,你可能想将按钮的背景颜色设置为红色,并在上面写一些文字。使用属性语法设置控件的属性,属性语法由属性名、等号和引号中的属性值组成。以下标记显示了添加了一些属性的按钮控件:
```cs
<Grid>
<Button Content="Click Me" Background="Red"/>
</Grid>
对于对象元素的某些属性,使用一种称为属性元素语法的语法。属性元素开始标签的语法是<typeName.propertyName>。例如,您可以在布局网格中创建行和列,以控制网格中控件的放置,如下所示:
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="25" />
<RowDefinition Height="25" />
<RowDefinition Height="25" />
</Grid.RowDefinitions>
控件通过包含Grid.Row和Grid.Column属性、在网格中定位,如下所示:
<Label Grid.Column="0" Grid.Row="0" Content="Name:" />
<Label Grid.Column="0" Grid.Row="1" Content="Password:" />
<TextBox Name="txtName" Grid.Column="1" Grid.Row="0"/>
<TextBox Name="txtPassword" Grid.Column="1" Grid.Row="1"/>
<Button Grid.Column="1" Grid.Row="3"
Content="Click Me" HorizontalAlignment="Right"
MinWidth="80" Background="Red"/>
图 11-1 显示了由先前的 XAML 代码创建的带有两个文本框的窗口。
图 11-1 。用 XAML 创造的窗户
使用布局控件
虽然您可以使用固定位置在 WPF 窗口上放置控件,但不建议这样做。使用固定定位通常适用于固定的分辨率大小,但不适用于不同的分辨率和设备。为了克服固定定位的局限性,WPF 提供了几个布局控件。布局控件允许您使用相对定位格式在其中定位其他控件。用于定位其他控件的主要布局控件之一是网格。如前所述,一个网格控件包含列和行来控制其子控件的位置。列和行的高度和宽度可以设置为固定值、自动或*。自动设置占用所包含控件所需的空间。*设置占用尽可能多的可用空间。网格控件通常用于布局数据输入表单。下面的代码展示了一个用于收集用户信息的简单数据输入表单。结果表单(在 Visual Studio 设计器中)如图 11-2 中的所示。
图 11-2 。输入表单窗口
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="28" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="200" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="Name:"/>
<Label Grid.Row="1" Grid.Column="0" Content="Old Password:"/>
<Label Grid.Row="2" Grid.Column="0" Content="New Password:"/>
<Label Grid.Row="3" Grid.Column="0" Content="Confirm Password:"/>
<TextBox Grid.Column="1" Grid.Row="0" Margin="3" />
<TextBox Grid.Column="1" Grid.Row="1" Margin="3" />
<TextBox Grid.Column="1" Grid.Row="2" Margin="3" />
<TextBox Grid.Column="1" Grid.Row="3" Margin="3" />
<Button Grid.Column="1" Grid.Row="4" HorizontalAlignment="Right"
MinWidth="80" Margin="0,0,0,8" Content="Submit" />
</Grid>
另一个有用的布局控件是 StackPanel。它根据方向设置垂直或水平布局子控件。下面的代码显示了 StackPanel 控件中的两个按钮:
<StackPanel Grid.Column="1" Grid.Row="4" Orientation="Horizontal" >
<Button MinWidth="80" Margin="0,0,0,8" Content="Submit" />
<Button MinWidth="80" Margin="0,0,0,8" Content="Cancel" />
</StackPanel>
其他一些可用的布局控件有 DockPanel、WrapPanel 和 Canvas。dock panel 用于将元素停靠在面板的左侧、右侧、顶部、底部或中心。 WrapPanel 的行为类似于 StackPanel,但是如果没有剩余空间,它会将子控件换行。画布控件用于以相对于其一侧的绝对定位来布局其子元素。它通常用于图形元素,而不是布局用户界面控件。
添加显示控件
大多数业务应用的目标是向用户呈现数据,并允许他们更新数据并将其保存回数据库。用于简化这一过程的一些常见控件有文本框、列表框、组合框、复选框、日期选择器和数据网格。您已经看到了在窗口上使用的 TextBox 下面的代码演示了如何向窗口中添加 ListBox 和 ComboBox。图 11-3 显示了窗口是如何呈现的。
图 11-3 。包含列表框和组合框的窗口
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ListBox Margin="20" Grid.Column="0">
<ListBoxItem>Red</ListBoxItem>
<ListBoxItem>Blue</ListBoxItem>
<ListBoxItem>Green</ListBoxItem>
<ListBoxItem>Yellow</ListBoxItem>
</ListBox>
<ComboBox Grid.Column="1" VerticalAlignment="Top">
<ComboBoxItem>Small</ComboBoxItem>
<ComboBoxItem>Medium</ComboBoxItem>
<ComboBoxItem>Large</ComboBoxItem>
<ComboBoxItem>X-Large</ComboBoxItem>
</ComboBox>
</Grid>
虽然可以直接在 XAML 标记中对这些控件中显示的项进行编码,但更有可能的是使用数据绑定来显示它们的值。您将很快看到数据绑定。
使用 Visual Studio 设计器
尽管使用文本编辑器完全通过代码创建窗口是很有可能的,但是您可能会发现这个过程非常乏味,并且不是非常有效地利用您的时间。幸运的是,Visual Studio IDE 包含了一个优秀的设计器来创建你的 WPF 窗口。使用设计器,您可以将控件从工具箱拖放到 Visual Studio 设计器,使用 Visual Studio 属性窗口设置其属性,并在使用 XAML 编辑器输入代码时获得自动完成和语法检查的好处。图 11-4 显示了 Visual Studio 设计器中的一个窗口。
图 11-4 。在 Visual Studio 中设计窗口
处理控制事件
Windows 图形用户界面(GUI)程序是事件驱动的。事件是由用户或系统发起的动作,例如,每当用户点击一个按钮,或者一个SqlConnection对象发出一个StateChange事件。事件驱动的应用通过执行您指定的代码来响应发生的各种事件。为了响应事件,您定义了在特定事件发生时要执行的事件处理程序。正如你在第八章中看到的 .NET Framework 使用委托来绑定事件,并编写事件处理程序来响应事件。一个委托对象维护一个方法调用列表,这些方法已经订阅了在事件发生时接收通知。当事件发生时(例如,单击按钮),控件将通过调用事件的委托来引发事件,该事件的委托又将调用已订阅接收事件通知的事件处理程序方法。虽然这听起来很复杂,但是框架类为您完成了大部分工作。
在 Visual Studio 中,可以通过编写 XAML 代码或在控件的“属性”窗口中选择事件来将事件添加到 WPF 控件中。图 11-5 显示了在 XAML 编辑器窗口中连接一个事件处理器;图 11-6 显示了使用属性窗口的事件选项卡连接事件处理程序。注意,当在代码中使用控件时,您需要使用Name属性给它们一个唯一的名称。
图 11-5 。在 XAML 编辑器中连接事件处理程序
图 11-6 。在“属性”窗口中连接事件处理程序
下面的代码显示了按钮单击事件的代码隐藏文件中插入的事件处理程序方法:
private void btnSubmit_Click(object sender, RoutedEventArgs e)
{
}
按照惯例,事件处理程序方法的名称以发出事件的对象的名称开头,后面跟着一个下划线(_)和事件的名称。然而,事件处理程序的实际名称并不重要。XAML 代码中的Click属性将这个方法添加到事件委托对象的调用列表中。
所有事件处理程序都必须提供两个参数,这两个参数在触发事件时传递给方法。第一个参数是 sender,它表示启动事件的对象。类型为System.Windows.RoutedEventArgs的第二个参数是一个对象,用于传递特定事件的任何特定信息。
因为 .NET Framework 使用委托进行事件通知,如果事件具有相同的签名,您可以使用相同的方法处理多个事件。例如,您可以用同一事件处理程序处理按钮单击事件和菜单单击事件,但不能处理文本框按键事件,因为它具有不同的签名。下面的代码演示如何处理使用相同处理程序方法的两个按钮的按钮单击事件。sender参数被转换为Button类型,并被询问以确定哪个按钮触发了事件。
private void Button_Click(object sender, RoutedEventArgs e)
{
Button btn = (Button)sender;
if (btn.Name == "btnCancel")
{
//Cancel code goes here
}
else if (btn.Name == "btnSubmit")
{
//Submit code goes here
}
}
在下面的活动中,您将使用表单和控件构建一个简单的备忘录查看器应用,该应用允许用户加载和查看备忘录文档。
活动 11-1。使用窗口和控件
在本活动中,您将熟悉以下内容:
- 创建基于 Windows 窗体的 GUI 应用
- 使用菜单、状态条和对话框控件
- 使用控制事件
创建备忘录查看器界面
要创建备忘录查看器界面,请遵循以下步骤:
-
启动 Visual Studio。选择文件
新建
项目。
-
选择 C# 项目文件夹下的 WPF 应用。将项目重命名为 Activity11_1,然后单击“OK”按钮。
-
该项目包含一个 MainWindow.xaml 文件。这个文件是你设计用户界面的地方。该项目还包含一个 MainWindow.xaml.cs 文件。这是代码隐藏文件,您将在其中添加代码来响应事件。如果尚未打开,请在 xaml 编辑器窗口中打开 MainWindow.xaml 文件。
-
In the Window tag of the XAMLmarkup, add a Name attribute with a value of “MemoViewer”. Change the Title attribute to “Memo Viewer”.
<Window x:Name="Memoviewer" x:Class="Act11_1.MainWindow"xmlns="``http://schemas.microsoft.com/winfx/2006/xaml/presentationxmlns:x="``http://schemas.microsoft.com/winfx/2006/xamlTitle="Memo Viewer" Height="350" Width="525"> -
Add a DockPanel control in the Grid control.
<Grid><DockPanel LastChildFill="True"></DockPanel></Grid> -
Add a Menu control inside the DockPanel and dock it to the top using the following XAML:
<DockPanel LastChildFill="True"><Menu DockPanel.Dock="Top"><MenuItem Header="_File"><MenuItem Name="mnuNew" Header="_New. . ." /><Separator /><MenuItem Name="mnuOpen" Header="_Open. . ." /><Separator /><MenuItem Name="mnuSave" Header="_Save" /><MenuItem Name="mnuSaveAs" Header="_Save As. . ." /><Separator /><MenuItem Name="mnuExit" Header="_Exit" /></MenuItem><MenuItem Header="_Edit"><MenuItem Header="_Cut. . ." /><MenuItem Header="_Copy. . ." /><MenuItem Header="_Paste" /></MenuItem></Menu></DockPanel> -
Add a StatusBar control by inserting the following code between the ending
Menutag and the endingDockPaneltag. Note that you are using a Grid control inside the StatusBar control to lay out the items in the StatusBar.<StatusBar DockPanel.Dock="Bottom"><Grid><Grid.RowDefinitions><RowDefinition Height="*"/></Grid.RowDefinitions><Grid.ColumnDefinitions><ColumnDefinition Width="4*"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions></Grid><StatusBarItem Grid.Column="0" HorizontalAlignment="Left"><TextBlock Name="sbTextbox1">File Name</TextBlock></StatusBarItem><StatusBarItem Grid.Column="1" HorizontalAlignment="Right"><TextBlock Name="sbTextbox2">Date</TextBlock></StatusBarItem></StatusBar> -
Add a RichTextBox control after the
StatusBarend tag and before theDockPanelend tag.</StatusBar><RichTextBox Name="rtbMemo" /></DockPanel> -
Note that as you add the XAML, the Visual Designer updates the appearance of the window. The Memo Viewer window should look similar to the one shown Figure 11-7.
图 11-7 。完整的 MemoEditor 窗口
-
构建解决方案。如果有任何错误,请修复并重新构建。
编码控制事件
要对控制事件进行编码,请遵循以下步骤:
-
In the XAML Editor window, add the
Loadedevent attribute to the Window, as shown:<Window x:Class="Act11_1.MainWindow"xmlns="``http://schemas.microsoft.com/winfx/2006/xaml/presentationxmlns:x="``http://schemas.microsoft.com/winfx/2006/xamlName="MemoViewer" Title="Memo Viewer" Height="350" Width="525"Loaded="MemoViewer_Loaded"> -
Open the codebehind file by right-clicking the XAML code editor and selecting View Code. Add the following code to the
MemoViewer_Loadedevent handler. When the window loads, it should show the message on the left side of the StatusPanel and the date on the right.private void MemoViewer_Loaded(object sender, RoutedEventArgs e){sbTextbox1.Text = "Ready to load file";sbTextbox2.Text = DateTime.Today.ToShortDateString();} -
In the XAML editor, add the
Clickevent to the mnuOpen control.<MenuItem Name="mnuOpen" Header="_Open. . ."Click="mnuOpen_Click"/> -
In the Code Editor window of the codebehind file, add the following code to the menu click event. This code configures and launches an Open File Dialog box, which returns the file path. The file path is then passed to a
FileStreamobject, which loads the file into the RichTextBox. The file path is also loaded into the StatusBar TextBox.private void mnuOpen_Click(object sender, RoutedEventArgs e){// Configure open file dialog boxMicrosoft.Win32.OpenFileDialog dlg = new Microsoft.Win32.OpenFileDialog();dlg.FileName = "Document"; // Default file namedlg.DefaultExt = ".txt"; // Default file extensiondlg.Filter = "Text documents (.txt)|*.txt"; // Filter files by extension// Show open file dialog boxNullable<bool> result = dlg.ShowDialog();// Process open file dialog box resultsif (result == true){// Open document and load RichTextBoxstring fileName = dlg.FileName;TextRange range;System.IO.FileStream fStream;if (System.IO.File.Exists(fileName)){range = new TextRange(rtbMemo.Document.ContentStart,rtbMemo.Document.ContentEnd);fStream = new System.IO.FileStream(fileName,System.IO.FileMode.OpenOrCreate);range.Load(fStream, System.Windows.DataFormats.Text );fStream.Close();}sbTextbox1.Text = fileName;}} -
Add a click event for the mnuExit control with the following code to close the window:
private void mnuExit_Click(object sender, RoutedEventArgs e){this.Close();} -
生成解决方案并修复任何错误。
-
在 c 盘上创建一个备忘录文件夹。使用记事本,创建一个包含测试消息的文本文件。将文件另存为 Test.txt。
-
选择调试
开始。通过加载 Test.txt 文件来测试应用。查看文件后,单击退出菜单关闭窗口。
-
测试完应用后,退出 Visual Studio。
创建和使用对话框
对话框 是基于 windows 的 GUI 应用中常用的特殊窗口,用于显示或检索用户的信息。普通窗口和对话框的区别在于对话框是有模式显示的。在对话框关闭之前,模式窗口阻止用户在应用中执行其他任务。在 Visual Studio 中启动一个新项目时,会出现一个新项目对话框,如图图 11-8 所示。您还可以使用对话框向用户提供关键信息,并询问他们是否做出响应。例如,如果你试图在调试模式下运行一个应用,并且遇到一个编译错误,Visual Studio IDE 会显示一个对话框询问你是否要继续(参见图 11-9 )。
图 11-8 。“新建项目”对话框
图 11-9 。使用对话框显示关键信息
向用户显示一个消息框
图 11-9 所示的对话框是一种特殊的预定义类型,称为消息框。MessageBox类是System.Windows名称空间的一部分。这个MessageBox类可以显示一个标准的 Windows 消息对话框。为了向用户显示一个 MessageBox,您调用 MessageBox 的静态Show方法,就像这样:
MessageBox.Show("File Saved");
Show方法被重载,以便您可以选择显示 MessageBox 图标、显示标题、更改显示的按钮以及设置默认按钮。唯一需要的设置是要在表单上显示的文本消息。图 11-10 显示了前面代码显示的消息框。
图 11-10 。一个基本的消息框
下面的代码使用一些其他参数调用Show方法。图 11-11 显示了显示的结果消息框。有关各种可用参数和设置的更多信息,请在 Visual Studio 帮助文件中查找MessageBox类。
MessageBox.Show("Are you sure you want to quit?",
"Closing Application",MessageBoxButton.OKCancel,
MessageBoxImage.Question);
图 11-11 。更复杂的消息框
您将经常使用 MessageBox 来查询用户对某个问题的响应。用户通过点击按钮来响应。结果以MessageBoxResult枚举的形式作为MessageBox.Show方法的返回值传递回来。以下代码捕获用户输入的对话框结果,并根据结果关闭(或不关闭)窗口:
MessageBoxResult result = MessageBox.Show("Are you sure you want to quit?",
"Closing Application",MessageBoxButton.OKCancel,
MessageBoxImage.Question);
if (result == MessageBoxResult.OK)
{
this.Close();
}
创建自定义对话框
最激动人心的特性之一 .NET Framework 就是它的扩展性。尽管有许多类型的对话框,你可以使用“开箱即用”的来完成打印、保存文件和加载文件等任务。您还可以构建自己的自定义对话框。创建自定义对话框的第一步是在应用中添加一个新窗口。接下来,添加与用户交互所需的任何控件。图 11-12 显示了一个对话框,你可以用它来验证用户的身份。
图 11-12 。自定义对话框
将 Cancel 按钮的IsCancel属性设置为 true 会将其与 ESC 键的快捷键相关联。将登录按钮的IsDefault属性设置为 true 会将其与键盘回车键相关联。这显示在下面的 XAML 代码中:
<StackPanel Grid.Column="1" Grid.Row="3" Orientation="Horizontal">
<Button Name="loginButton" IsDefault="True">Login</Button>
<Button Name="cancelButton" IsCancel="True">Cancel</Button>
</StackPanel>
当单击登录按钮时,按钮的 click 事件负责验证用户输入并将DialogResult属性设置为 true 或 false。这个值被返回给调用 DialogWindow 的Show方法的窗口,以便进一步处理。下面的代码显示了被调用的 LoginDialog 窗口和被询问的DialogResult属性。请注意,调用窗口可以访问 DialogWindow 上定义的对象。在本例中,它正在询问txtName文本框的Text属性。
LoginDialog dlg = new LoginDialog();
dlg.Owner = this;
dlg.ShowDialog();
if (dlg.DialogResult == false)
{
string user = dlg.txtName.Text;
MessageBox.Show("Invalid login for " + user, "Warning",
MessageBoxButton.OK, MessageBoxImage.Exclamation);
this.Close();
}
基于 Windows 的图形用户界面中的数据绑定
一旦从业务逻辑层检索到数据,就必须将其呈现给用户。用户可能需要通读数据、编辑数据、添加记录或删除记录。您想要添加到窗口中的许多控件都可以显示数据。选择使用哪种控件通常取决于要显示的数据类型、操作数据的方式以及界面的设计。在控件中 .NET 开发人员通常用来显示数据的是文本框、数据网格、标签、列表框、复选框和日历。当数据源的不同字段在单独的控件中呈现给用户时(例如,名文本框和姓文本框),控件保持同步以显示相同的记录是很重要的。
那个 .NET Framework 通过称为数据绑定的过程封装了将控件同步到数据源的大部分复杂性。当您在控件和某些数据之间创建绑定时,您正在将绑定目标绑定到绑定源。绑定对象处理绑定源和绑定目标之间的交互。单向绑定导致对源属性的更改自动更新目标属性,但对目标属性的更改不会传播回源属性。这对于只读情况很有用。双向绑定导致源属性或目标属性的改变自动更新另一个。这对于完整的数据更新场景非常有用。
使用 DataContext 绑定控件
若要将控件绑定到数据,您需要一个数据源对象。容器控件的 DataContext 允许子控件从其父控件继承有关用于绑定的数据源的信息。下面的代码设置顶层窗口控件的DataContext属性。它使用 DataSet 和 TableAdapter 填充一个Table对象,并将其设置为窗口的 DataContext。
private void Window_Loaded(object sender, RoutedEventArgs e)
{
pubsDataSet dsPubs = new pubsDataSet();
pubsDataSetTableAdapters.storesTableAdapter taStores =
new pubsDataSetTableAdapters.storesTableAdapter();
taStores.Fill(dsPubs.stores);
this.DataContext = dsPubs.stores.DefaultView;
}
下面的 XAML 代码使用Path属性将数据网格列绑定到商店表列。使用Binding作为源意味着“查找容器层次结构,直到找到 DataContext。”在这种情况下,它是窗口容器的 DataContext。
<DataGrid AutoGenerateColumns="False" ItemsSource="{Binding}">
<DataGrid.Columns>
<DataGridTextColumn x:Name="stor_idColumn"
Binding="{Binding Path=stor_id}" Header="Id" />
<DataGridTextColumn x:Name="stor_nameColumn"
Binding="{Binding Path=stor_name}" Header="Name" />
<DataGridTextColumn x:Name="stateColumn"
Binding="{Binding Path=state}" Header="State" />
<DataGridTextColumn x:Name="zipColumn"
Binding="{Binding Path=zip}" Header="Zip" />
</DataGrid.Columns>
</DataGrid>
加载了商店数据的结果数据网格如图 11-13 所示。
图 11-13 。用数据网格显示商店数据
在以下活动中,您将把 DataGrid 控件绑定到包含 Pubs 数据库中的数据的 DataTable。您还将使用 DataAdapter 将 DataGrid 控件中所做的数据更改更新回 Pubs 数据库。
活动 11-2。使用数据绑定控件
在本活动中,您将熟悉以下内容:
- 将数据网格绑定到数据表
- 使用 DataAdapter 更新数据
将数据网格绑定到数据表
要将 DataGrid 绑定到 DataTable 对象,请执行以下步骤:
创建数据集
- 启动 Visual Studio。选择文件
新建
项目。
- 选择 WPF 应用。将项目重命名为 Activity11_2,然后单击“确定”按钮。
- 项目加载后,找到位于屏幕左侧的“数据源”窗口。单击添加新数据源链接。
- 在数据源配置向导中,选择数据库的数据源类型,然后单击下一步继续。
- 在“选择数据库模型”窗口中,选择数据集,然后单击“下一步”。
- 在“选择您的数据连接”窗口中,选择或创建到 Pubs 数据库的连接,然后单击“下一步”。
- 在下一个屏幕上,保存到应用配置文件的连接。
- 在“选择您的数据库对象”窗口中,展开表的节点并选择 authors 表。单击完成按钮。
- 注意在“解决方案资源管理器”窗口中,pubsDataSet.xsd 文件已添加到该文件中。该文件表示基于 pubs 数据库的强类型 dataset 对象。在解决方案资源管理器中双击文件节点以启动数据集可视化编辑器。
- The visual editor contains an authors table. Select the authorsTableAdapter, as shown in Figure 11-14. In the Properties window, notice that the select, insert, update, and delete commands have been generated for you (see Figure 11-15).

图 11-14 。选择 authorsTableAdapter

图 11-15 。查看生成的命令文本
创建窗口布局
-
在 XAML 编辑器窗口中打开主窗口。将窗口标题更改为“电话列表”。
-
Inside the
Gridtags, add a DockPanel control. Inside the DockPanel, add a StackPanel.<Grid><DockPanel><StackPanel DockPanel.Dock="Top" Orientation="Horizontal"></StackPanel></DockPanel></Grid> -
Inside the StackPanel, add two buttons—one for getting data and one for updating data. Add a
Clickevent handler for each button.<StackPanel DockPanel.Dock="Top" Orientation="Horizontal"><Button Name="btnGetData" Content="Get Data"Click="btnGetData_Click" /><Button Name="btnSaveData" Content="Save Data" /></StackPanel> -
Outside the StackPanel but inside the DockPanel, add a DataGrid.
<DataGrid Name="dgAuthors" AutoGenerateColumns="True"DockPanel.Dock="Bottom" /></DockPanel></Grid>
加载数据网格
-
在代码编辑器窗口中打开 MainWindow.xaml.cs 文件。
-
Add three class level variables of type
pubsDataset,authorsTableAdapter, andauthorsDataTable.public partial class MainWindow : Window{pubsDataSet _dsPubs;pubsDataSetTableAdapters.authorsTableAdapter _taAuthors;pubsDataSet.authorsDataTable _dtAuthors; -
In the
btnGetData_Clickevent, add code to fill the _taAuthors table and set it equal to the DataContext of the dgAuthors grid.private void btnGetData_Click(object sender, RoutedEventArgs e){_dsPubs = new pubsDataSet();_taAuthors = new pubsDataSetTableAdapters.authorsTableAdapter();_dtAuthors = new pubsDataSet.authorsDataTable();_taAuthors.Fill(_dtAuthors);this.dgAuthors.DataContext = _dtAuthors;} -
Switch back to the XAML Editor Window and add the ItemsSource binding to the DataGrid’s XAML code. This will bind it to the DataContext.
<DataGrid Name="dgAuthors" AutoGenerateColumns="True"DockPanel.Dock="Bottom" ItemsSource="{Binding}" /> -
选择调试
开始。单击“获取数据”按钮测试应用。数据网格将加载作者的数据(见图 11-16 )。请注意,由于 DataGrid 的
AutoGenerateColumns属性被设置为 true,因此网格将加载表中的所有列。网格列的标题也与作者的表格列同名。 -
After viewing the window, stop the debugger.
图 11-16 。作者数据网格
更新数据
-
Open the MainWindow.xaml.cs file in the Code Editor window. Add the following code to update the data in the
btnSaveData_Clickevent handler. This code uses the table adapter’s update command to send the changes back to the database. You must also alter the btnSaveData’s XAML to call this event when it is clicked.private void btnSaveData_Click(object sender, RoutedEventArgs e){try{_taAuthors.Update(_dtAuthors);MessageBox.Show("Data Saved.","Information", MessageBoxButton.OK,MessageBoxImage.Information);}catch (Exception ex){MessageBox.Show("Could not save data!","Warning",MessageBoxButton.OK,MessageBoxImage.Warning);}} -
Update the Grid’s XAML code to only show the first name, last name, and phone columns.
<DataGrid Name="dgAuthors" AutoGenerateColumns="False"DockPanel.Dock="Bottom" ItemsSource="{Binding}"><DataGrid.Columns><DataGridTextColumn Header="Last Name"Binding="{Binding Path='au_lname'}" /><DataGridTextColumn Header="First Name"Binding="{Binding Path='au_fname'}" /><DataGridTextColumn Header="Phone"Binding="{Binding Path='phone'}" /></DataGrid.Columns></DataGrid> -
选择调试
开始。单击“获取数据”按钮测试应用。更新一些名字。单击“保存数据”按钮,然后单击“获取数据”按钮,验证名称是否已保存到数据库中。
-
测试后,停止调试器并退出 Visual Studio。
创建和使用控件和数据模板
在 WPF 中,每个控件都有一个管理其视觉外观的模板。如果你没有显式地设置它的Style属性,那么它使用一个默认的模板。创建一个定制模板并将其分配给Style属性是改变应用外观的一个很好的方法。图 11-17 显示了一个使用控件模板创建的圆形按钮。
图 11-17 。使用自定义模板创建圆形按钮
以下 XAML 是定义自定义模板的标记,该模板用于创建图 11-17 中的圆形按钮。
<Window.Resources>
<Style x:Key="RoundedButtonStyle" TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Grid>
<Ellipse Fill="{TemplateBinding Background}"
Stroke="{TemplateBinding BorderBrush}"/>
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
以下 XAML 代码用于使用按钮的Style属性将自定义样式绑定到按钮:
<Button Content="Rounded Button" Style="{StaticResource RoundedButtonStyle}"
除了控件样式模板,您还可以创建数据模板。当您在 UI 中绑定业务对象时,数据模板允许您自定义业务对象的外观。需要使用自定义数据模板的一个很好的例子是列表框。默认情况下,它将数据呈现为单行文本。当您试图将它绑定到 employee 对象列表时,它会调用ToString()方法并将其写出到显示器上。正如你在图 11-18 中看到的,这显然不是你想要的。
图 11-18 。使用默认数据模板的列表框
通过向 ListBox 控件添加 DataTemplate,不仅可以显示雇员数据,还可以控制数据的显示方式。下面的 XAML 向列表框添加了一个数据模板,图 11-19 显示了结果:
<ListBox ItemsSource="{Binding}" >
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock FontWeight="Bold" Text="{Binding Path='lname'}" />
<TextBlock Text=", " />
<TextBlock Text="{Binding Path='fname'}" />
<TextBlock Text=" " />
<TextBlock Text="{Binding Path = 'minit'}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
图 11-19 。使用自定义数据模板的列表框
在以下活动中,您将使用实体数据模型将 ListBox 控件绑定到从 Pubs 数据库创建的实体。您还将通过同步 ListBox 控件和 DataGrid 控件来创建主从视图。
活动 11-3。使用数据模板
在本活动中,您将熟悉以下内容:
- 将列表框绑定到实体
- 创建数据模板
- 创建主详细视图
将列表框绑定到实体
要将一个列表框绑定到一个实体对象,请遵循以下步骤:
- 启动 Visual Studio。选择文件
新建
项目。
- 选择 Windows 模板下的 WPF 应用。将项目重命名为 Activity11_3,然后单击“确定”按钮。
- 项目加载后,定位“数据源”窗口(它应该在设计器窗口的左侧)。单击添加新数据源链接。
- 在数据源配置向导中,选择数据库的数据源类型。
- 在“选择数据库模型”窗口中,选择实体数据模型。
- 在“选择模型内容”窗口中,选择“从数据库生成”选项。
- 在“选择您的数据连接”窗口中,选择或创建到 Pubs 数据库的连接。将连接保存到应用配置文件。
- 在“选择数据库对象”窗口中,展开“表”节点,然后选择“商店”和“销售”表。单击完成按钮。
- 请注意,在“解决方案资源管理器”窗口中,Model1.edmx 文件已添加到项目中。这个文件包含 pubs 数据库中实体和表之间的关系映射。
创建数据模板
-
Add a DockPanel and a ListBox control in the XAML Editor window between the default Grid tags.
<Grid><DockPanel><ListBox Name="StoresList" DockPanel.Dock="Left" ItemsSource="{Binding}"></ListBox></DockPanel></Grid> -
Add a window Loaded event attribute to the Window’s opening tag.
<Window x:Class="Activity11_3.MainWindow"xmlns="``http://schemas.microsoft.com/winfx/2006/xaml/presentationxmlns:x="``http://schemas.microsoft.com/winfx/2006/xamlTitle="MainWindow" Height="350" Width="525"Loaded="Window_Loaded_1"> -
Add a
Window_Loadedevent handler in the code file that sets the DataContext of the ListBox to the stores entities.private void Window_Loaded_1(object sender, RoutedEventArgs e){pubsEntities pEntities = new pubsEntities();this.StoresGrid.DataContext = pEntities.stores.ToList<store>();} -
Add a DataTemplate to display the store name in a TextBlock control inside the ListBox control.
<ListBox Name="StoresList" DockPanel.Dock="Left"ItemsSource="{Binding}"><ListBox.ItemTemplate><DataTemplate><TextBlock FontWeight="Bold" Text="{Binding Path='stor_name'}" /></DataTemplate></ListBox.ItemTemplate></ListBox> -
选择调试
开始。确保列表框显示商店名称。查看完列表框后,停止调试器。
-
To implement a master/detail data view, add a DataGrid control to the DockPanel control after the ListBox control. The Binding of the grid is set to the same as the list box, which is the store entity, but the binding path is set to the sales entity. This will cause the data grid to show the sales items of the store selected in the list box.
<DataGrid Name="SalesGrid" DockPanel.Dock="Right"ItemsSource="{Binding Path='sales'}" AutoGenerateColumns="False"><DataGrid.Columns><DataGridTextColumn Header="Order Number"Binding ="{Binding Path ='ord_num'}"/><DataGridTextColumn Header="Order Date"Binding="{Binding Path='ord_date'}"/></DataGrid.Columns></DataGrid> -
Add the following property to the ListBox control in the XAML code. This will ensure that the ListBox control and DataGrid control will remain in sync.
IsSynchronizedWithCurrentItem="True" -
Launch the application in the debugger. Your window should look similar to Figure 11-20. Click on different stores in the list box. You should see the data grid update with the store’s sales data. After testing, stop the debugger and close Visual Studio.
图 11-20 。查看主/详细数据
摘要
在这一章中,你看到了应用接口层的实现。您通过基于 WPF 的应用前端实现了用户界面。在这个过程中,您仔细研究了 .NET Framework 用于实现丰富的基于 Windows 的用户界面。您看到了如何使用 XAML 语法来定义界面的控件和布局。您还看到了将控件绑定到数据并呈现给用户是多么容易。在第十三章中,您将重新审视这些概念,并将它们应用于开发新的 Windows 8 和 Windows Store 应用。
在下一章中,你将再次访问一个. NET 应用的 UI 层,但是不是使用 WPF 实现 GUI,而是将 GUI 实现为一个基于 web 的应用。在这个过程中,您将进一步了解可用于创建基于 web 的 GUI 应用的名称空间,以及实现这些名称空间中包含的类所涉及的技术。`