在现代的Web应用中,实时数据流是一个重要的功能,它允许客户端及时接收并展示服务器端的数据。本文将探讨如何使用ASP.NET Web API在后台线程中将大型结果集流式传输到WPF客户端,并在内容接收后立即在屏幕上显示。此外,本文还将展示如何使用Dapper,一个简单的ORM,逐条返回记录的结果。
本文将通过示例代码来演示这一概念,该代码允许用户通过输入姓名的首字母来搜索人员,并显示匹配搜索条件的人员列表。
使用代码
解决方案包含两个项目:服务项目和服务端项目。解决方案应包含运行示例代码所需的一切,只需确保已安装.NET Framework 4.5,因为代码使用了新的async和await关键字。为了调试目的,同时启动两个项目。
服务项目是一个Web API MVC4项目。有两个控制器,HomeController和NameController。HomeController是在创建项目时默认创建的,在这个案例中不会使用它,所以会忽略它。Name控制器包含将展示Web API流式传输能力的服务。
在Name控制器中有两个动作:SimulateLargeResultSet动作和UsingDapper动作。为了正确地将请求路由到相应的动作,需要添加以下路由到WebApiConfig.cs中。
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "Api",
routeTemplate: "api/{controller}/{action}",
defaults: new { first = string.Empty }
);
}
SimulateLargeResultSet动作展示了当从SQL Server返回大型结果集时会发生什么。检索大型结果集需要时间。为了模拟延迟,在for循环的末尾添加了Thread.Sleep(250)。它将创建一个带有Id和名称的Person对象,以返回给客户端。
UsingDapper动作默认不使用,因为它需要设置SQL Server数据库。逻辑与SimulateLargeResultSet动作非常相似,但它包含连接到SQL数据库并从Name表中读取的代码。需要创建数据库结构并更改连接字符串才能使其工作。使用Dapper v1.12.0.0执行SQL查询,并逐条返回person对象。这样做的好处是不必等待整个结果返回后再将输出推送回客户端。将buffered参数设置为false,并将commandtimeout设置为无限期。
WPF客户端项目使用Prism 4.1和MVVM方法。运行示例代码时,将看到一个FirstName文本框,用户可以在其中输入要搜索的人的姓名,以及一个datagrid,它将显示服务项目中的所有结果。
MainWindow.xaml有一个ViewModel,名为MainWindowViewModel,其构造函数中有以下代码行。
BindingOperations.EnableCollectionSynchronization(Persons, _personsLock);
这行代码允许从后台线程修改Observable集合,而不会抛出WPF错误。WPF要求所有UI更改代码必须在调度线程上运行,即UI线程。在.NET Framework的较新版本中,它会自动将所有INotifyPropertyChanged.PropertyChanged事件委托到UI线程,但它不会为INotifyCollectionChanged.CollectionChanged做这件事。必须使用EnableCollectionSynchronization(...)或手动将添加/删除操作分派回UI线程。发现使用BindingOperations比使用调度器要快得多。
DataGrid绑定到MainWindowViewModel类中的Persons属性,该属性是ObservableCollection
public ObservableCollection Persons
{
get
{
if (_persons == null)
{
_persons = new ObservableCollection();
}
return _persons;
}
set
{
_persons = value;
RaisePropertyChanged(() => Persons);
}
}
在ViewModel中,有两个命令,搜索命令和取消命令。搜索命令简单地启动一个后台线程,调用Web API服务流,并返回一个Person列表。然后,它将person对象添加到Persons ObservableCollection中,这将实时反映在UI中。
using (Stream stream = await response.Content.ReadAsStreamAsync())
{
byte[] readBuffer = new byte[512];
int bytesRead = 0;
while ((bytesRead = stream.Read(readBuffer, 0, readBuffer.Length)) != 0)
{
ct.ThrowIfCancellationRequested();
string personString = Encoding.UTF8.GetString(readBuffer, 0, bytesRead);
var person = JsonConvert.DeserializeObject(personString);
persons.Add(person);
}
}
在While循环的开始,它检查是否已请求取消令牌:
ct.ThrowIfCancellationRequested()