“实体框架”提供了与数据库的双向通信通道。前面已经讲述了如何使用“实体框架”获 取数据,现在来看看如何修改获取的信息,并将改动发送回数据库。
26.2.1 更新现有数据
使用一个 ObjectContext 对象获取数据时,根据数据创建的对象位于应用程序的内存缓存中。 为了更改缓存中的对象的值,采取的方式和修改任何普通对象中的值一样——设置它们的 属性。然而,更新内存中的对象不会更新数据库。要在数据库中反映出这个改动(换言之, 将改动“持久化”到数据库中),需要生成恰当的 SQL UPDATE 命令,并指示数据库服务器 执行这些命令。使用“实体框架”可以轻松做到这一点。以下代码展示了一个 LINQ to Entities 查询,它获取 ID 为 14 的产品,将它的名称更改为"Bean Curd"(产品 14 最初在 Northwind 数据库中命名为"Tofu"),再将更改发送回数据库。
NorthwindEntities northwindContext = new NorthwindEntities();
Product product = northwindContext.Products.Single(p => p.ProductID == 14);
product.ProductName = "Bean Curd";
northwindContext.SaveChanges();
在上述代码中,关键语句是对 ObjectContext 对象的 SaveChanges 方法的调用。(记住, NorthwindEntities 派生自 ObjectContext。)对通过运行查询来填充的一个实体对象中的信息 进行修改时,对连接(原始的查询就是在这个连接上运行的)进行管理的 ObjectContext 对象 会跟踪数据发生的改变。SaveChanges 方法会将这些改变传回数据库。在幕后,ObjectContext 对象会构建并运行一个 SQL UPDATE 语句。
如果获取并修改了几个产品,只需在最后一次修改之后调用一次 SaveChanges 就可以了。 SaveChanges 方法一次性提交所有更新。ObjectContext 对象创建一个数据库事务处理 (database transaction),并在这个事务处理中执行所有 SQL UPDATE 语句。任何一个更新失败, 都 会 取 消 事 务 处 理 —— SaveChanges 方 法 在 数 据 库 中 进 行 的 所 有 更 改 都 会 回 滚 , SaveChanges 方法最后将抛出一个异常。如果所有更新成功,事务处理就生效,所有更改都 在数据库中变成永久性的更改。注意,假如 SaveChanges 方法失败,只有数据库才会回滚,
对内存中的实体对象进行的更改仍然存在。SaveChanges 失败时抛出的异常会提供与失败原
因有关的一些信息。可以根据这些信息来修正错误,并再次调用 SaveChanges。
ObjectContext 类还提供了 Refresh 方法。使用这个方法,可以利用数据库中的数据来重新填 充缓存中的 EntityObject 集合,并放弃进行的任何更改。要像下面这样使用该方法:
northwindContext.Refresh(RefreshMode.StoreWins, northwindContext.Products);
第 一 个 参 数 是 System.Data.Objects.RefreshMode 枚 举 的 一 个 成 员 , 如 果 指 定 的 值 是 RefreshMode. StoreWins,就会强迫用数据库中的数据来进行刷新。第二个参数是缓存中要 要刷新的实体。
提示 对更改进行跟踪是 ObjectContext 对象执行的一个代价不菲的操作。如果确定以 后 不 需 要 修 改 数 据 ( 例 如 , 假 定 程 序 生 成 的 是 一 份 只 读 的 报 表 ) , 就 可 以 将 MergeOption 属性设为 MergeOption.NoTracking,从而禁止对 EntityObject 的更改 进行跟踪。如下所示:
northwindContext.Suppliers.MergeOption = MergeOption.NoTracking;
可对一个禁止了‚更改跟踪‛的实体进行更改,但在调用 SaveChanges 时,这些更改不会 保存。应用程序退出时,它们会丢失。
26.2.2 处理冲突的更新
一个更新操作可能出于多种原因而失败,其中最常见的原因就是发生了冲突。两个用户试 图同时更新相同的数据,就会发生冲突。仔细思考一下在应用程序中使用“实体框架”的 后果,就会发现其实有许多地方都可能发生冲突。通过一个 ObjectContext 对象获取数据时, 这些数据被缓存应用程序的内存中。与此同时,另一个用户执行相同的查询,并获取相同 的数据。如果两个人都修改了数据,而且都调用了 SaveChanges 方法,那么在数据库中, 一个人就会覆盖另一个人的修改。这种现象称为 lost update。
之所以发生这种现象,是由于“实体框架”实现了“开发式并发”(optimistic concurrency)。 换言之,当它从数据库获取数据时,不会在数据库中锁定那些数据。这种形式的并发性允 许其他用户同时访问相同的数据,但它假定两个用户同时更改相同数据的机率非常小(这 正是“开发式并发”一词的来历59)。
与开放式并发相反的是“封闭式并发”(pessimistic concurrency)。在这个方案中,所有数据 都在获取时锁定,其他用户不能同时访问它们。这样一来,就保证了不会丢失任何更改。 但是,这个方案过于极端。
59 “开放式并发”原文是“optimistic concurrency”,这里只是按照 Microsoft 的文档把它翻译为“开放式并发”。其实,更贴 切的翻译是“乐观并发”。对应地,“封闭式并发”(pessimistic concurrency)更贴切的翻译是“悲观并发”。——译者注
“实体框架”不直接支持封闭式并发。相反,它提供了一个折衷方案。EntityObject 类中的
每一项都有一个名为“并发模式”的属性,它默认设为 None,但可使用“实体框架”设计 器把它更改为 Fixed。下图展示了早先构建的实体模型。
单击 Product 实体中的 ProductName 这一项,然后在属性窗口中,将它的“并发模式”属 性更改为 Fixed。
应用程序修改 Products EntityObject 类的一个实例的 ProductName 属性的值时,“实体框架” 会在缓存中保留这个属性的原始值的一个副本。设置了一个属性的“并发模式”后,当应 用程序调用 ObjectContext 对象的 SaveChanges 方法时,“实体框架”会检查缓存的原始值副 本,验证数据库中对应行的那一列自从取回之后没有被另一个用户更改。如果列没有更改, 行 就 会 更 新 。 如 果 列 已 经 更 改 , SaveChanges 方 法 会 停 止 , 并 抛 出 一 个 OptimisticConcurrencyException 异常。一旦发生这个情况,SaveChanges 在数据库中进行的 所有更改都会被撤消(undone)。不过,在应用程序的缓存中,这些更改仍然会被保留下来。
引发一个 OptimisticConcurrencyException 异常时,可以检查异常对象的 StateEntries 属性, 判 断 是 哪 个 实 体 造 成 了 冲 突 。 这 个 属 性 容 纳 着 一 个 ObjectStateEntry 对 象 集 合 。 ObjectStateEntry 类本身包含大量属性。其中最重要的属性包括:Entity 属性,它包含对造
成 冲 突 的 实 体 的 一 个 引 用 ; CurrentValues 属性, 它 包 含 实 体 修 改 过 的 数 据 ; 以 及
OriginalValues 属性,它包含最初从数据库获取的实体数据。
为了解决冲突,推荐的方案是使用 Refresh 方法从数据库中获取最新的数据,并用这些数据 重新加载缓存。然后,再次调用 SaveChanges。Refresh 方法会用数据库中的最新的值重新 填充一个指定实体(作为第二个参数传递)。但是,如果用户已经进行了大量更改,你可能 不想强迫用户重新输入它们。幸好,Refresh 方法的 RefreshMode 参数也允许你应对这种情 况。RefreshMode 是一个枚举类型的参数,它定义了两个值:
l StoreWins 实体中已被更改的值被数据库中的最新值覆盖。用户对实体的任何更改都 会丢失。
l ClientWins 实体中已被更改的值不被数据库中的值覆盖。用户对实体的任何更改都会 保留在缓存中,会在下次调用 SaveChanges 时传送回数据库。
在下面的代码中,尝试对 Products ObjectSet 中的 ProductID 为 14 的产品的名称进行修改, 然 后 将 这 个 更 改 保 存 到 数 据 库 。 如 果 另 一 个 用 户 已 经 修 改 了 相 同 的 数 据 , OptimisticConcurrencyException 处理程序会刷新缓存中的原始值,但修改过的数据会在缓存 中保留修改过的样子,然后再次调用 SaveChanges。
NorthwindEntities northwindContext = new NorthwindEntities();
try
{
Product product = northwindContext.Products.Single(p => p.ProductID == 14);
product.ProductName = "Bean Curd";
northwindContext.SaveChanges();
}
catch (OptimisticConcurrencyException ex)
{
northwindContext.Refresh(RefreshMode.ClientWins, northwindContext.Products);
northwindContext.SaveChanges();
}
重要提示:
“实体框架”在检测到第一个冲突时,会停止并抛出 OptimisticConcurrencyException 异常。
如果更改了多行,后续的 SaveChanges 调用可能检测到更多的冲突。
除此之外,在 OptimisticConcurrencyException 异常处理程序中,在 Refresh 和 SaveChanges
这两个调用之间,有极小的机率另一个用户更改了数据。在商业应用程序中,这个异常也 应该捕捉。
26.2.3 添加和删除数据
除了修改现有的数据,“实体框架”还允许将新项添加到一个 ObjectSet 集合中,以及从
ObjectSet 集合删除项。
用“实体框架”生成一个实体模型时,每个实体的定义都包括一个名为 CreateXXX 的工厂 方法(其中 XXX 是实体类的名称)。可利用这个工厂方法创建一个新实体。创建新实例时, 这个方法要求为基础数据库的每个必须的(非 NULL 的)列传递实参。然后,可用实体类公 开的属性设置附加的列值(下例中的 UnitPrice 和 QuantityPerUnit)。为了将新实体添加到一 个 ObjectSet 集合中,要使用 AddObject 方法。要将新实体保存到数据库中,要在 ObjectContext 对象上调用 SaveChanges 方法。
下例新建一个 Product 实体,并把它添加到由 NorthwindEntities 上下文对象维护的集合中的 产品列表中。代码还为 SupplierID 为 1 的供货商添加了对新对象的一个引用。(“实体框架” 提供 Add 方法的目的是帮助维护实体之间的关系。)SaveChanges 方法将新产品插入数据库。
NorthwindEntities northwindContext = new NorthwindEntities();
Product newProduct = Product,CreateProduct(0, "Fried Bread", false);
newProduct.UnitPrice = 55;
newProduct.QuantityPerUnit = "10 boxes";
ObjectSet<Product> products = northwindContext.Products;
products.AddObject(newProduct);
Supplier supplier = northwindContext.Suppliers.Single(s => s.SupplierID == 1);
supplier.Products.Add(newProduct);
northwindContext.SaveChanges();
注意 在这个例子中,CreateProduct 方法的第一个参数是 ProductID。在 Northwind 数据 库中,ProductID 是一个 IDENTITY 列。调用 SaveChanges 时,SQL Server 会为 这个列生成它自己的唯一的值,并丢弃你指定的值。
从 ObjectSet 集合中删除实体对象是非常简单的。只需调用 DeleteObject 方法,并指定要删
除的实体。以下代码删除 ProductID 大于或等于 79 的所有产品。产品是在调用 SaveChanges
方法时,才实际地从数据库中删除的。
NorthwindEntities northwindContext = new NorthwindEntities();
var productList = from p in northwindContext.Products where p.productID >= 79
select p;
ObjectSet<Product> products = northwindContext.Products;
foreach (var product in productList)
{
}
northwindContext.SaveChanges();
如果表和其他表有关系,那么在删除行时要小心,否则可能在更新数据库时产生引用完整 性错误。例如,在 Northwind 数据库中,如果删除当前正在供应产品的一个供货商,更新 就会失败。必须先删除供货商的所有产品。为此,可以使用 Supplier 类的 Remove 方法。(和 Add 方法相似,Remove 方法也是由“实体框架”提供的。) 添加或删除了数据之后,如果在保存更改时发生错误,SaveChanges 方法会抛出一个 UpdateException 异常。应该准备好捕捉这个异常。
现在,我们已掌握了足够多的知识来完成 Suppliers 应用程序。
Ø 写代码来修改、删除和创建产品
1. 返回正在编辑 Suppliers 应用程序的 Visual Studio 2010 窗口。在设计视图中显示
SupplierInfo.xaml。
2. 在 XAML 窗格中,修改 productsList 控件的定义,让它捕捉 KeyDown 事件,并调用一 个名为 productsList_KeyDown 的事件方法(这是该事件方法的默认名称)。
3. 在代码和文本编辑器窗口中,为 productsList_KeyDown 方法添加以下加粗的代码:
private void productsList_KeyDown(object sender, KeyEventArgs e)
{
switch (e.Key)
{
case Key.Enter: editProduct(this.productsList.SelectedItem as Product);
break;
case Key.Insert: addNewProduct();
break;
case Key.Delete: deleteProduct(this.productsList.SelectedItem as Product);
break;
}
}
这个方法检查用户按下的键。如果按的是 Enter 键,代码就调用 editProduct 方法,将
产品细节作为参数传递。如果按的是 Insert 键,代码就调用 addNewProduct 方法,在 当前供货商的产品列表中创建并添加一个新产品。如果按的是 Delete 键,就调用 deleteProduct 方法删除产品。 后面 的步骤 会 写 editProduct 、 addNewProduct 和
deleteProduct 方法。
4. 返 回 设 计 视 图 。 在 XAML 窗 格 中 , 修 改 productsList 控 件 的 定 义 来 捕 捉 MouseDoubleClick(鼠标双击)事件,并调用一个名为 productsList_MouseDoubleClick 的事件方法。(同样地,这是事件方法的默认名称。)
5. 在 “ 代码和文本编辑器 ” 窗口 中,将以下加粗的语句添加到
productsList_MouseDoubleClick 方法中:
private void productsList_MouseDoubleClick(object sender, KeyEventArgs e)
{
editProduct(this.productsList.SelectedItem as Product);
}
这个方法直接调用 editProducts 方法。根据习惯,用户在双击了数据之后,自然就是想 编辑它。
6. 将 deleteProduct 方法添加到 SupplierInfo 类,如下所示:
private void deleteProduct(Product product)
{
MessageBoxResult response = MessageBox.Show( String.Format("Delete {0}", product.ProductName),
"Confirm", MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No);
if (response == MessageBoxResult.Yes)
{
this.northwindContext.Products.DeleteObject(product);
saveChanges.IsEnabled = true;
}
}
这个方法提示用户确认删除当前选中的产品。if 语句调用 Products ObjectSet 集合的
DeleteObject 方法。最后,该方法激活 saveChanges 按钮。在稍后的一步中,将为这个 按钮添加功能,将 Products ObjectSet 中的更改发送回数据库。
添加类”命令。在“添加新项–Suppliers”对话框中,选择“窗口(WPF)”
有几个办法都可以添加和编辑产品;ListView 控件中的列是只读的文本项,但可以创 建一个自定义列表视图,在其中包含文本框或其他控件来允许用户输入。然而,最简 单的办法是创建另一个窗体让用户编辑或添加产品细节。
8. 在设计视图中,单击 ProductForm 窗体。在“属性”窗口中,将 ResizeMode 属性设为
NoResize,Height 设为 225,Width 设为 515。
9. 在窗体上添加三个 Label 控件、三个 TextBox 控件和两个 Button 控件。在“属性”窗 口中,根据下表来设置这些控件的属性。
控 件 | 属 性 | 值 |
Label1 |
Content |
Product Name |
Height | 23 | |
Width | 120 |
Margin |
17, 20, 0, 0 |
|
VerticalAlignment |
Top |
|
HorizontalAlignment |
Left |
|
label2 | Content | Quantity Per Unit |
Height |
23 |
|
Width | 120 | |
Margin | 17, 60, 0, 0 | |
VerticalAlignment |
Top |
|
HorizontalAlignment | Left | |
label3 | Content | Unit Price |
Height |
23 |
|
Width | 120 | |
Margin | 17, 100, 0, 0 | |
VerticalAlignment |
Top |
|
HorizontalAlignment | Left | |
textBox1 | Name | productName |
Height |
21 |
|
Width | 340 | |
Margin | 130, 24, 0, 0 | |
VerticalAlignment |
Top |
|
HorizontalAlignment | Left | |
textBox2 | Name | quantityPerUnit |
Height |
21 |
|
Width | 340 | |
Margin | 130, 64, 0, 0 | |
VerticalAlignment |
Top |
|
HorizontalAlignment | Left | |
textBox3 | Name | unitPrice |
Height |
21 |
|
Width | 120 | |
Margin | 130, 104, 0, 0 | |
VerticalAlignment |
Top |
|
HorizontalAlignment | Left | |
button1 | Name | ok |
Content |
OK |
|
Height | 23 | |
Width | 75 |
Margin |
130, 150, 0, 0 |
|
VerticalAlignment |
Top |
|
HorizontalAlignment |
Left |
|
button2 | Name | cancel |
Content |
Cancel |
|
Height | 23 | |
Width | 75 | |
Margin |
300, 150, 0, 0 |
|
VerticalAlignment | Top | |
HorizontalAlignment | Left |
现在,Supplier Information 窗体在设计视图中的样子应该如下图所示。
10. 双击 OK 按钮为 Click 事件创建处理程序。在显示了 ProductForm.xaml.cs 的“代码和 文本编辑器”窗口中,添加以下加粗显示的代码:
private void ok_Click(object sender, RoutedEventArgs e)
{
if (String.IsNullOrEmpty(this.productName.Text))
{
MessageBox.Show("The product must have a name", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
decimal result;
if (!Decimal.TryParse(this.unitPrice.Text, out result))
{
MessageBox.Show("The price must be a valid number", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
if (result < 0)
{
MessageBox.Show("The price must not be less than zero", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
this.DialogResult = true;
}
应用程序将调用 ShowDialog 方法,将窗体显示成一个模态对话框。用户单击窗体上的
一个按钮后,如果 Click 事件代码设置了 DialogResult 属性,窗体就会自动关闭。 用户单击 OK 按钮后,这个方法会对用户输入的信息执行一些简单的校验。数据库的
Quantity Per Unit 列接受空值,所以用户在窗体上可以不填写这个字段。如果输入一个
有效的产品名和价格,方法会将窗体的 DialogResult 属性设为 true。这个值会传回给
ShowDialog 方法调用。
11. 回到正在显示 ProductForm.xaml 的设计视图。从中选择 Cancel 按钮,在“属性”窗口 中将它的 IsCancel 属性设为 true(勾选复选框)。
如果用户单击了 Cancel 按钮,它会自动关闭窗体,并将其值为 false 的一个 DialogResult
返回给 ShowDialog 方法。
12. 切换到正在显示 SupplierInfo.xaml.cs 文件的“代码和文本编辑器”窗口,将以下
addNewProduct 方法添加到 SupplierInfo 类:
private void addNewProduct()
{
ProductForm pf = new ProductForm();
pf.Title = "New Product for " + supplier.CompanyName;
if (pf.ShowDialog().Value)
{
Product newProd = new Product(); newProd.ProductName = pf.productName.Text; newProd.QuantityPerUnit = pf.quantityPerUnit.Text; newProd.UnitPrice = Decimal.Parse(pf.unitPrice.Text); this.supplier.Products.Add(newProd); this.productsInfo.Add(newProd);
saveChanges.IsEnabled = true;
}
}
addNewProduct 方法创建 ProductForm 窗体的一个新实例,在这个窗体的 Title 属性中
添加供货商的名称,然后调用 ShowDialog 方法将窗体显示成一个模态对话框。如果用 户输入了有效的数据,并单击 OK 按钮,则 if 块中的代码会创建一个新的 Product 对
象,并在其中填充来自 ProductForm 实例的信息。然后,方法把这这个对象添加到当
前供货商的 Products 集合中,同时把它添加到窗体的 ListView 控件显示的列表中。最 后,代码激活 Save Changes 按钮。在后面的一步中,将为这个按钮的 Click 事件添加
处事件处理程序,以便将更改存回数据库。
13. 将以下 editProduct 方法添加到 SupplierInfo 类:
private void editProduct(Product prod)
ProductForm pf = new ProductForm(); pf.Title = "Edit Product Details"; pf.productName.Text = product.ProductName; pf.quantityPerUnit.Text = product.QuantityPerUnit; pf.unitPrice.Text = product.UnitPrice.ToString();
if (pf.ShowDialog().Value)
{
product.ProductName = pf.productName.Text; product.QuantityPerUnit = pf.quantityPerUnit.Text; product.UnitPrice = Decimal.Parse(pf.unitPrice.Text); saveChanges.IsEnabled = true;
}
}
editProduct 方法也创建 ProductForm 窗体的一个实例。这一次除了设置 Title 属性,还
要使用当前所选产品的信息填充窗体上的字段。窗体显示之后,用户可以编辑这些值。 如果单击 OK 按钮关闭窗体,if 块中的代码会将新值复制回当前选定的产品,然后激
活 Save Changes 按钮。注意,这一次不需要手动更新 productsInfo 列表中的当前项,
因为 Product 类会将它的数据发生的变化自动通知给 ListView 控件。
14. 回到正在显示 SupplierInfo.xaml 文件的设计视图。双击 Save Changes 按钮创建 Click
事件处理方法。
15. 在“代码和文本编辑器”窗口中,将以下 using 语句添加到文件顶部:
using System.Data;
using System.Data.Objects;
这些命名空间包含了“实体框架”使用的许多类型。
16. 将加粗显示的代码添加到 saveChanges_Click 方法:
private void saveChanges_Click(object sender, RoutedEventArgs e)
{
{
}
this.northwindContext.SaveChanges();
saveChanges.IsEnabled = false;
{
this.northwindContext.Refresh(RefreshMode.ClientWins, northwindContext.Products); this.northwindContext.SaveChanges();
}
catch (UpdateException uEx)
{
MessageBox.Show(uEx.InnerException.Message, "Error saving changes");
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Error saving changes");
this.northwindContext.Refresh(RefreshMode.StoreWins, northwindContext.Products);
}
}
这个方法调用 ObjectContext 对象的 SaveChanges 方法,将所有更改发送回数据库。异常处
理程序捕捉可能发生的任何异常。OptimisticConcurrencyException 处理程序使用早先描述的 策略刷新缓存,并再次保存更改。UpdateException 处理程序向用户报告错误,然后指定 RefreshMode.StoreWins 参数,用数据库的信息刷新缓存。(这导致用户的更改被丢弃。)注 意,这个异常最有意义的数据保留在异常的 InnerException 属性中(虽然你可能不想向用户 显示信息的类型)。如果发生其他类型的异常,Exception 处理程序显示一条简单消息并用 来自数据库的信息刷新缓存。
Ø 测试 Suppliers 应用程序
1. 在“调试”菜单中选择“开始执行(不调试)”命令,从而生成并运行应用程序。等窗 体显示由 Exotic Liquids 提供的产品后,单击产品 3(Aniseed Syrup)并按 Enter 键或双击 行。随后应出现 Edit Product Details 窗体。将 Unit Price 值更改为 12.5 并单击 OK 按钮。 验证新的价格被复制回列表视图。
2. 按 Insert 键。随后应出现 New Product for Exotic Liquids 窗体。随便输入一个产品名、 单位量(quantity per unit)和价格(unit price),然后单击 OK 按钮。验证新产品已添加到 列表视图中。
Product ID 列中的值应该是 0。这个值是数据库的一个 identity 列,所以在保存更改时,
SQL Server 会为这个列生成它自己的一个唯一值。
3. 单击 Save Changes 按钮。数据保存后,新产品的 ID 应该在列表视图中显示。
4. 单击新产品并按 Delete 键。在确认对话框中单击“是”。验证产品从窗体中消失。再 次单击 Save Changes 按钮,验证操作成功。 试验为其他供货商添加、删除和编辑产品。可连续进行多个修改,最后再单击 Save Changes 按钮。SaveChanges 方法会保存自从数据取回或者上一次保存以来的所有更 改。
提示 如果不慎删除或覆盖了想要保留的一个产品,请直接关闭应用程序,不要单击 Save Changes 按钮。注意,如果直接退出而不保存更改,程序不会向用户发出警告。 另外,也可以在程序中添加一个 Discard Changes(放弃更改)按钮,让它调用 northwindContext ObjectContext 对象的 Refresh 方法,从而用数据库中的值重新填
充表。如上一个练习中的异常处理程序所示。
5. 关闭窗体,返回 Visual Studio 2010。
本章讲述了如何使用“实体框架”为数据库生成一个实体模型。讲述了如何将窗体控件绑 定到实体集合,从而在 WPF 应用程序中使用实体模型。还讲述了如何使用 LINQ to Entities,通过一个实体模型访问数据
l 如果希望继续学习第 27 章的学习,请继续运行 Visual Studio 2010,然后阅读第 27 章。
l 如果希望现在就退出 Visual Studio 2010,请选择“文件”|“退出”命令。如果看到 “保存”对话框,请单击“是”按钮保存项目。
第 26 章快速参考
目 标 | 操 作 |
使用“实体框架”创建实体类 | 使用“ADO.NET 实体数据模型”模板在项目中添加一个新类。 使用“实体模型向导”连接数据库,并从中选择想要建模的表 |
在 WPF 控件中显示来自实体对象或集 合的数据 | 为控件的一个恰当的属性定义绑定。如果控件显示一个对象列 表,就将控件的 DataContext 属性设为一个实体对象集合。如 果控件显示单个对象的数据,就将控件的 DataContext 属性设 为一个实体对象,并在绑定的 Path 属性中指定要显示实体对象 的哪个属性的值 |
使用“实体框架”修改数据库中的信 息 | 首先采取以下操作之一: ● 要更新数据库的表中的一行,请将该行的数据取回到一个 实体对象中,然后将新值赋给实体对象的恰当属性 ● 要在数据库的表中插入一个新行,请使用为实体类生成的 CreateXXX 工厂方法创建对应的实体对象的一个新实例(其中 XXX 是实体名称)。设置它的属性,然后调用恰当的 ObjectSet 集合的 AddObject 方法,将新的实体对象作为参数传递 ● 要从数据库的表中删除一行,请调用恰当的 ObjectSet 集合 的 DeleteObject 方法,将要删除的实体对象作为参数传递 之后,在完成了所有更改之后,调用 ObjectContext 对象的 SaveChanges 方法,将这些更改送回数据库 |
使用“实体框架”检测更新数据库时 的冲突 | 写一个处理程序处理 OptimisticConcurrencyException.异常。在 异常处理程序中,调用 ObjectContext 对象的 Refresh 方法,从 数据库获取最新的信息来刷新缓存中的原始值,最后再次调用 |
SaveChanges 方法
第 VI 部分 使用 Visual Studio 2010 构
建专业解决方案
4 | 第 27 章 | 任务并行库入门 |
4 | 第 28 章 | 执行并行数据访问 |
4 | 第 29 章 | 创建和使用 Web 服务 |
第 27 章 任务并行库入门
本章旨在教会你:
l 理解在应用程序中实现并行操作所带来的好处
l 了解为什么说任务并行库(TPL)提供了一个理想的平台来实现多线程应用程序
l 在应用程序中使用 Task 类创建和运行并行操作
l 使用 Parallel 类并行化一些常用的编程构造
l 为线程使用任务,增强 GUI 应用程序的响应能力和吞吐能力
l 取消长时间运行的任务,处理并行操作引发的异常
到目前为止,你已学习了如何使用 Microsoft V isual C#构建应用程序提供一个图形用户界面, 并对数据库中的数据进行管理。这些是现代系统的常用功能。然而,随着技术的进步,用 户的需求也在进步。用户赖以执行日常操作的应用程序需要提供更复杂的解决方案。在本 章最后一部分,将探讨.NET Framework 4.0 引入的一些高级功能。具体地说,本章会讲述如 何使用“任务并行库”(Task Parallel Library)增强应用程序的并发性。下一章将讲述如何 将.NET Framework 提供的并行扩展与 LINQ 组合使用,以提高数据吞吐能力。在最后一章, 将探讨如何使用 Windows Communication Foundation(WCF)构建分布式解决方案,以便集
成多台计算机上运行的服务。作为一个附赠,我在电子版(英文)形式的附录中60描述了 如何使用“动态语言运行时”(Dynamic Language Runtime)生成 C#应用程序和组件,它们
能和使用其他语言生成的服务进行互操作。这些语言在.NET Framework 提供的结构的外部 工作,包括 Python 和 Ruby 等。
通过本章前面的学习,你知道了如何使用 C#编写以单线程形式运行的程序。所谓“单线程”, 是指在任何给定的时刻,一个程序只能执行一条指令。这并非总是应用程序的最优运行方 式。例如,第 23 章讲过,如果程序等待用户单击 WPF 窗体上的一个按钮,那么等待期间 也许能做其他一些工作。然而,如果单线程程序必须执行一个耗时的、处理器密集型的计 算,便不能响应用户在窗体上输入数据或者单击一个菜单项的操作。对于应用程序来说, 应用程序就像是死掉了一样。只有在计算完成之后,UI 才会重新有响应。能同时运行多个 任务的应用程序可以更好地利用计算机的可用资源,可以运行得更快,而且能保证响应能 力。除此之外,有的任务如果划分为并行的执行路径,那么运行速度也许会更快。第 23 章 讲述了 WPF 如何利用线程提升图形用户界面中的响应能力。在本章中,将介绍如何利用“任 务并行库”在程序中实现多任务处理的一个更常规的形式,它能应用于计算密集型的应用 程序,而非只能应用于那些关心 UI 管理的应用程序。
27.1 为何使用并行处理来执行多任务处理
如前所述,在应用程序中执行多任务处理主要是出于两个方面的原因:
l 增强响应能力 将程序划分到并发执行的线程中,并允许每个线程轮流运行一小段时 间,就可以向应用程序的用户营造出程序一次执行多个任务的“假象”。这是传统的协 作式多线程模型,许多有经验的 Windows 用户都很熟悉它。然而,它并不是真正的多 任务处理,因为处理器是在多个线程之间共享的。另外,协作式的本质要求每个线程 执行的代码都要具有恰当的行为方式。如果一个线程垄断了 CPU 和资源,让其他线程 一直没有机会运行,这种方式的好处就丧失殆尽了。有的时候,很难按照这个模型写 出一个具有良好行为的应用程序。
l 增强伸缩性 由于能有效地利用处理资源,并用这些资源减少执行一个应用程序的各 个不同所需的时间,所以能增强伸缩性。61开发者可以判断应用程序的哪些部分能并 行执行,并相应作出安排。随着计算资源越来越多,更多的任务可以并行运行。就在 不久之前,这个模型还只适用于安装了多个 CPU 的系统,或者能够在联网的计算机之 间进行分布处理的系统。在这两种情况下,都必须使用一个模型对并行任务进行协调。 Microsoft 提供了 Windows 的一个特别版本,名为 High Performance Compute (HPC)
Server 2008,它允许企业构建服务器集群,以便并行分布和执行任务。开发人员可使 用 Microsoft 实现的 Message Passing Interface (MPI),基于并行任务来构建应用程序。
60 这个附录在译者博客上提供。网址是 http://transbot.blog.163.com——译者注
61 在少量时间里做更多工作的能力,就是所谓的“伸缩性”。作为一个伸缩性好的服务器,理论上应该 CPU 越多,一个耗时 操作所需的时间就越短。通俗地说,在多个 CPU 之间并行执行,执行时间将根据 CPU 的数量成比例地缩短。——译者注
MPI 是一种著名的、跟语言无关的通信协议。这些并行任务相互之间通过发送消息来
进行协作。对于大规模的、计算限制(compute-bound)的工程和科学应用程序来说, 基于 Windows HPC Server 2008 和 MPI 的解决方案是非常理想的。但是,对于小规模的 桌面(台式机)系统来说,它们显得过于昂贵。
根据以上描述,你可能觉得为桌面应用程序构建多任务解决方案时,成本效益最好的 方式就是使用协作式多线程模型。然而,多线程方案的主要目的是增强应用程序的响应能 力——在单处理器计算机上,它确保每个任务都公平地获得处理器时间。在多处理器系统 上,这个方案并不十分恰当,因为它不具备在不同处理器上分布负载的能力,所以伸缩性 很差。虽然安装多个处理器的台式机还十分昂贵,而且极其少见,但这并不是一个问题, 情况正在逐渐发生改变,下一节将进一步解释这个问题。