WPF 中使用 Async/Await 调用 WCF 并带有忙碌指示器和中止功能





5.00/5 (10投票s)
在 WPF 中实现 WCF Web 服务访问,并带有忙碌指示器和提前取消功能,无需使用委托、后台工作者或单独的事件过程。
介绍
如果您曾编写过 Silverlight,您会注意到 Silverlight 中固有的 Web 服务访问是异步的。另一方面,在 WPF 中,默认模型是同步的。在同步模式下,当进行与服务器的往返以获取数据时,您的 UI 将保持锁定状态,这可能会给用户留下应用程序已挂起的印象。一些用户可能会在窗口中四处点击,尝试使应用程序响应。有些用户可能会通过按 Ctrl+Alt+Delete 来“结束任务”。
幸运的是,.NET 4.5 引入了两个关键字 `async` 和 `await`,它们可以神奇地将一个方法转换为异步方法。在该方法的体内,您可以通过在调用前加上 `await` 关键字来调用长时间运行的进程,例如 Web 服务。在此之后的每一行代码都将被延迟执行,直到该操作完成。在此期间,控制权将返回到 `async` 方法的原始调用者。一旦长时间运行的进程完成执行,`await` 方法之后的其余代码将接管并完成其执行。
本文档适用于那些过去在 Winforms/WPF 中使用后台工作者组件、另一个线程或甚至 Web 服务代理生成的异步方法以及操作完成事件来编写过此类场景的开发人员。 .NET 4.5 中引入的功能为开发人员提供了一种更好、更简洁的替代方案来处理事件过程等,并将操作集中在一个地方进行管理——例如按钮的点击事件。
背景
在我们组织中设计 WPF + WCF 应用程序时,我们的政策是应用程序应用户友好,并且在后台忙于处理某些事情时应尽可能保持响应。我们决定利用 .NET Framework 4.5 的 `async`/`await` 功能,这样我们就可以避免使用后台工作者来实现异步,这在我们使用 ASMX Web 服务时是常用的做法。为了获得与 Silverlight 工具包中提供的忙碌指示器类似的功能,我们使用了 Codeplex 的 Extended WPF Toolkit 中提供的忙碌指示器。
使用代码
这里展示的示例包含两个项目
- 一个 WCF 服务应用程序项目,其中包含一个方法 (GetBooks) 用于检索书籍及其作者的信息。
- 一个消耗该服务的 WPF 客户端应用程序。WPF 应用程序应为 .NET 4.5,否则您将无法使用 `async`/`await` 关键字。
要求
- Visual Studio 2012
- 下载 Extended WPF Kit 并将其添加到工具箱。
虽然完整的可运行示例已作为 zip 文件随文章一起提供,但我将简要介绍涉及的主要步骤。
步骤 I:创建 WCF 服务应用程序:
打开 Visual Studio 2012 并创建一个 WCF 服务应用程序,将其命名为 **MyService**。在解决方案资源管理器中,右键单击 **IService1.cs** 文件并将其重命名为 **IBookService.cs**。Visual Studio 会询问您是否要重命名所有引用。选择“是”。现在,右键单击 **Service1.svc** 文件并将其重命名为 **BookService.svc**。打开 BookService.svc 文件,右键单击单词 **Service1**,选择“重构”=>“重命名”,将其重命名为 **BookService**。这将确保 Visual Studio 正确重命名 service1 的所有引用为 BookService。
删除/修改 IBookService.cs 和 BookService.cs 中的所有代码,使其看起来如下:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.Text;
namespace MyService
{
[ServiceContract]
public interface IBookService
{
[OperationContract]
ObservableCollection<Book> GetBooks();
}
[DataContract]
public class Book
{
[DataMember]
public int BookId { get; set; }
[DataMember]
public string BookName { get; set; }
[DataMember]
public string Author { get; set; }
}
}
现在,删除/修改 BookService.cs 中的所有代码,使其看起来如下:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.Text;
namespace MyService
{
public class BookService : IBookService
{
public ObservableCollection<Book> GetBooks()
{
//This lines simulates the lag in roundtrip to Server.
System.Threading.Thread.Sleep(5000); //puts 5-second lag.
//Create a list and convert it into an observable collection and return back to the client.
return new ObservableCollection<Book>(
new List<Book>
{
new Book{ BookId = 1, BookName="Learning C#", Author = "Nejimon CR"},
new Book{ BookId = 2, BookName="Introuction to ADO.NET", Author = "Nejimon CR"},
new Book{ BookId = 3, BookName="Lambda Made Easy", Author = "Nejimon CR"},
new Book{ BookId = 4, BookName="Robinson Crusoe", Author = "Daniel Defoe"},
new Book{ BookId = 5, BookName="The White Tiger", Author = "Aravind Adiga"},
new Book{ BookId = 6, BookName="The God of Small Things", Author = "Arunthati Roy"},
new Book{ BookId = 7, BookName="Midnight's Children", Author = "Salman Rushdie"},
new Book{ BookId = 8, BookName="Hamlet", Author = "William Shakespeare"},
new Book{ BookId = 9, BookName="Paradise Lost", Author = "John Milton"},
new Book{ BookId = 10, BookName="Waiting for Godot", Author = "Samuel Beckett"},
}
);
}
}
}
我们本质上做的是定义一个接口,定义一个将用作数据协定的类以将图书数据返回给客户端,并在服务类中实现该接口。GetBooks 方法返回几本书的书名及其作者。当然,在实际应用程序中,您可能会从数据库中检索数据。
我还把下面这行放到了顶部:
System.Threading.Thread.Sleep(5000);
这将模拟通过 Internet 访问 Web 服务时遇到的延迟,以便我能够演示忙碌指示器和中止功能的使用。
准备好 Web 服务后,构建它,看看它是否能成功构建。
步骤 II:创建 WPF 客户端应用程序:
下一步,向解决方案添加一个 WPF 项目(文件 => 新建项目),并将其命名为 **AsyncAwaitDemo**。请记住将目标框架版本保留为 .NET 4.5。在解决方案资源管理器中,右键单击 WPF 项目并将其设置为启动项目。
再次右键单击 WPF 项目并打开“添加服务引用”对话框。单击对话框底部的“高级”按钮,并确保选中“允许生成异步操作”。在您的解决方案中查找服务,将命名空间保留为“BookService”,然后单击“确定”。现在您已添加了对 **BookService** 的引用。
打开 MainWindow.xaml 并添加一个 Button、一个 DataGrid 和一个 Busy Indicator。请参考标记中的控件正确放置。
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit" x:Class="AsyncAwaitDemo.MainWindow"
Title="MainWindow" Height="350" Width="525">
<Grid>
<xctk:BusyIndicator Name="busyIndicator">
<xctk:BusyIndicator.BusyContent>
<StackPanel>
<TextBlock HorizontalAlignment="Center">Please wait...</TextBlock>
<Button Content="Abort" Name="btnAbort" HorizontalAlignment="Center"/>
</StackPanel>
</xctk:BusyIndicator.BusyContent>
<xctk:BusyIndicator.Content>
<StackPanel>
<Button Content="Get Data" Name="btnGetData" HorizontalAlignment="Center"/>
<DataGrid Name="grdData" AutoGenerateColumns="True"/>
</StackPanel>
</xctk:BusyIndicator.Content>
</xctk:BusyIndicator>
</Grid>
</Window>
打开 `MainWindow.xaml` 的代码窗口(按 F7),并修改代码使其看起来如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using AsyncAwaitDemo.BookService;
namespace AsyncAwaitDemo
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
//Create event procs.
btnGetData.Click += btnGetData_Click;
btnAbort.Click += btnAbort_Click;
grdData.AutoGeneratingColumn += grdData_AutoGeneratingColumn;
}
//Service Proxy
BookServiceClient client;
//this will make sure that the "ExtentionData" column added as part of Serialization is prevented from showing up on the grid.
void grdData_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
if (e.Column.Header.ToString() == "ExtensionData")
{
e.Cancel = true;
}
}
//If the request is still being executed, this gives a chance to abort it.
void btnAbort_Click(object sender, RoutedEventArgs e)
{
if (client != null)
{
if (client.State == System.ServiceModel.CommunicationState.Opened)
{
client.Abort();
}
}
}
//Async method to get the data from web service.
async void btnGetData_Click(object sender, RoutedEventArgs e)
{
try
{
busyIndicator.IsBusy = true;
client = new BookServiceClient();
var result = await client.GetBooksAsync();
client.Close();
grdData.ItemsSource = result;
busyIndicator.IsBusy = false;
}
catch (Exception ex)
{
busyIndicator.IsBusy = false;
if (!ex.Message.Contains("The request was aborted: The request was canceled."))
{
MessageBox.Show("Unexpected error: " + ex.Message,
"Async Await Demo", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
this.UpdateLayout();
}
}
}
编码完成。构建解决方案。如果一切顺利,应用程序将启动并显示 UI。单击“获取数据”按钮,您将看到以下内容:
关注点
有趣的是下面这行:
async void btnGetData_Click(object sender, RoutedEventArgs e)
`async` 关键字将事件处理程序转换为异步执行的方法,从而防止在执行长时间运行时应用程序 UI 无响应。
var result = await client.GetBooksAsync();
当遇到上述行时,在进入 GetBooksAsync 的执行后,无需等待完成,控制权就会交还给调用者,从而保持 UI 响应。另一方面,GetBooksAsync 之后的任何行都会被延迟(因此使用了 `await` 关键字)。因此,以下行将从执行中暂停,直到 Web 服务调用返回。
client.Close();
grdData.ItemsSource = result;
busyIndicator.IsBusy = false;
也许还有一点值得注意的是,在序列化期间,WCF 可能会在您的数据协定对象中添加一个额外的列。为了防止它显示在数据网格中(当然,如果您打开了 AutoGenerateColumns),您可能需要添加以下代码:
void grdData_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
if (e.Column.Header.ToString() == "ExtensionData")
{
e.Cancel = true;
}
}
历史
无。