在上一个任务成功给团队demo之后,大家同意了我进一步的考虑——让下载数据这个进程可控,能够根据用户的需要关闭下载进程,进行其他任务。
我的想法来自于之前学Android中的AsyncTask:
AsyncTask enables proper and easy use of the UI thread. This class allows you to perform background operations and publish results on the UI thread without having to manipulate threads and/or handlers.
如定义所述,AsyncTask能够让我们在Android程序编写中无需使用多线程,即可使后台任务独立运行,并将结果反馈给UI进程。
这里将老师的课件对比案例引用如下:
与一般的Sync相比为什么要使用Async呢,相信大家看完上面的栗子已经有所了解,我们再现来看看Microsoft给出的解释,注意一些关键词:
Async Improves Responsiveness
Asynchrony is essential for activities that are potentially blocking, such as when your application accesses the web. Access to a web resource sometimes is slow or delayed. If such an activity is blocked within a synchronous process, the entire application must wait. In an asynchronous process, the application can continue with other work that doesn't depend on the web resource until the potentially blocking task finishes.
由于需要改进的项目是C#编写的Window Form Application,在网上找到,在.NET的Toolbox中,有一个BackgroundWorker控件也能够让我们“proper and easy use of the UI thread"
BackgroundWorker makes threads easy to implement in Windows Forms. Intensive tasks need to be done on another thread so the UI does not freeze. It is necessary to post messages and update the user interface when the task is done.
是不是和AsyncTask的思想是一致的~~
BackgroundWorker通过DoWork,ProgressChanged,RunWorkerCompleted这三个EventHandler,分别控制程序的后台运行,进程的更新和结束后,我们只需要把任务分类到这三个EventHandler中,然后在UI线程中调用即可。
为了控制在程序在后台的运行状况和是否可被取消,需要设置它的两个属性True/False:WorkerReportsProgress, WorkerSupportsCancellation。
关于BackgroundWorker就不赘述了,在学习使用的时候,这两篇教程相见恨晚:
BackgroundWorker作为Asynchronous Programming的基础,可以为设计结构较为简单的程序实现后台任务的运行,和Thread相比能够更方便地和UI进程进行信息交互。然而它们都比较繁琐。
在C# 4.0之后,Microsoft推出了Task。
O'REILLY出版的《C# 5.0 IN A NUTSHELL》 中指出Task弥补了Thread的不足:
A thread is a low-level tool for creating concurrency, and as such it has limitations. In particular:
- While it's easy to pass data into a thread that you start, there's no easy way to get a "return value" back from a thread that you Join. You have to set up some kind of shared field. And if the operation throws an exception, catching and propagating that exception is equally painful
- You can't tell a thread to start something else when it's finished; instead you must Join it (blocking your own thread in the process)
C#5.0之后推出了async和await关键词:
These keywords let you write asynchronous code that has the same structure and simplicity as synchronous code, as well as eliminating the "plumbing" of asynchronous programming
Task与async/await关键词两者的结合使用,让Asynchronous Programming能够在Synchronous代码的基础快速改写完成,换言之,就是简单易用。
那么剩下最后一个核心问题,如何取消与数据库连接下载数据的这个进程?
方案一:像关闭一个asynchronous method一样,将控制数据下载的进程关闭。
根据这个思路,在《Entity Framework Core Cookbook》中指出:
All asynchronous methods take an optional CancellationToken parameter. This parameter, when supplied, provides a way for the caller method to cancel the asynchronous execution.
var source = new CancelationTokenSource();
var cancel = source.Token;
cancel.Register(()=>{
//cancelled
});
ctx.MyEntities.TolistAsync(cancel);
if(!cancel.WaitHandle.WaitOne(TimeSpan.FromSeconds(5))){
source.Cancel();
}
案例中的TolistAsync()方法称为API Async Methods,标志是以“Async”后缀结尾,它们的返回类型是Task(参见API Async Methods)
我的项目使用的是Entity Framework,于是引入CancellationToken,调用.ToListAsync(),将SqlQuery改写如下:
IList<print_pack_list_ext>query =
SysVariable.Database.SqlQuery<print_pack_list_ext>(sql.ToString(), parameters).ToListAsync(token);
应用这个方法后,并没有成功终止。网站上也有人遇到的了同样的问题,听说微软团队针对Entity Framework尚未解决这个问题。无奈之下,当时也有几分自豪,居然被我找到了Bug~~
方案二:是否存在一个终止的方法,直接作用在SqlQuery上面呢?
有,他就是:SqlCommand.Cancel Method (),正如他的使命:
Tries to cancel the execution of a SqlCommand.
啊,终于找到了,他就是我的韩信。
那什么时候调用这名大将呢?应该在用户取消下载任务,使CancellationToken值为False之后被Invoke。正如CancellationToken.Register Method (Action)的使命:
Registers a delegate that will be called when this CancellationToken is canceled.
所以,将SqlCommand.Cancel注册在CancelToken中即可:
using (CancellationTokenRegistration ctr = token.Register(() => cmd.Cancel()))
{
...
}
至此,所有的疑惑都找到了答案。
从底层向上走,首先DAO改写如下:
public async Task<IList<print_pack_list_ext>> GetPackByDate(DateTime datefrom, DateTime dateto, CancellationToken token)
{
IList<print_pack_list_ext> list = new List<print_pack_list_ext>();
try
{
await Task<IList<print_pack_list_ext>>.Run(() =>
{
using (SqlConnection conn = new SqlConnection(getConnectionstring()))
{
conn.Open();
var cmd = conn.CreateCommand();
using (CancellationTokenRegistration ctr = token.Register(() => cmd.Cancel()))
{
#region sql string
string sqlString = "select a.pps_number,a.created_by from pack_list a where convert(datetime, a.created_datetime, 120) > convert(datetime, @dateFrom0, 120) and convert(datetime, a.created_datetime, 120) < convert(datetime, @dateFrom1, 120)"
#endregion
cmd.Parameters.AddWithValue("dateFrom0", datefrom);
cmd.Parameters.AddWithValue("dateFrom1", dateto);
cmd.CommandTimeout = 0;
cmd.CommandType = CommandType.Text;
cmd.CommandText = sqlString;
DataSet ds = new DataSet();
DataTable table = new DataTable();
table.Load(cmd.ExecuteReader());
ds.Tables.Add(table);
#region fill model
list = ds.Tables[0]
.AsEnumerable()
.Select(dataRow =>
new print_pack_list_ext
{
pps_number = dataRow.Field<string>("pps_number"),
created_by = dataRow.Field<string>("created_by")
}).ToList();
#endregion
}
}
}, token);
return list;
}
catch (SqlException ex)
{
return list;
}
}
在Controller中调用:
public async Task<IList<print_pack_list_ext>> GetPackByDate(DateTime datefrom, DateTime dateto, CancellationToken token)
{
return await _printPackListDAO.GetPackByDate(datefrom, dateto, token);
}
在View中实现:
using System.Threading.Tasks;
using Solution.BusinessLayer;
using Solution.ExtendedEntity;
namespace Solution.Forms
{
public partial class FormLabelExportLog : Form
{
#region property
CancellationTokenSource tokenSource;
CancellationToken token;
LogController _logController = new LogController();
#endregion
#region event_button
//Code behind 'Generate Button'
private async void myBtnGenReport_Click(object sender, EventArgs e)
{
setProgressBarStyle_start();
myLabelInfo.Text = "Loading...";
string selectedItem = this.myListBox1.SelectedItem.ToString();
tokenSource = new CancellationTokenSource();
token = tokenSource.Token;
DataTable taskGetData = await Task.Factory.StartNew(() => loadData(selectedItem, token), token);
if (token.IsCancellationRequested)
MessageBox.Show("Cancelled Successfully!");
else
processData(taskGetData);
tokenSource.Dispose();
this.myLabelInfo.Text = "";
}
//Code behind 'Cancel' Button
private void myBtnCancelReport_Click(object sender, EventArgs e)
{
tokenSource.Cancel();
setProgressBarStyle_end();
}
#endregion
#region method_data
private DataTable loadData(string selectedItem, CancellationToken token)
{
#region initialization
DataTable dt = new DataTable();
DataRow dr;
...
#endregion
#region _PackListLog
if (selectedItem == _PackListLog)
{
IList<print_pack_list_ext> real = new List<print_pack_list_ext>();
real = _logController.GetPackByDate(datefrom, dateto, token).Result;
if (!token.IsCancellationRequested)
{
...
}
}
#endregion
return dt
}
private void processData(DataTable dt)
{
if (dt != null)
{
ExportToExcel(dt);
}
}
#endregion
}
}
就这样愉快地完成了。虽然是个小功能,牵扯到的很多知识点都没有学过,翻阅了很多资料,虽然耗时较长,但也是实习期间收获最多,最有意义的时间。
P.S.
所有O'RELLY的书在SafariOnline都有,邮箱注册无需信用卡绑定免费使用10天!!真是良心~~
P.P.S
整理一下上面没有提到的学习资料:
- 关于Task:
https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/task-based-asynchronous-programming - 当不明白Task中括号里面的表达式,参见Lambda以及C# Delegate
- 什么是Async和Await,它们的实现原理是什么?
https://msdn.microsoft.com/library/hh191443(vs.110).aspx
https://msdn.microsoft.com/en-us/magazine/jj991977.aspx - 如何实现UI线程和非UI线程之间的交互?
https://msdn.microsoft.com/en-us/library/system.invalidoperationexception(v=vs.110).aspx - 什么是Callback
https://en.wikipedia.org/wiki/Callback_(computer_programming)
就用StackOverflow上的关于delegate,event,callback的经典回答结束这一篇啦,下期再见~~
I just met you
And this is crazy
But here's my number (delegate)
So if something happens (event)
Call me (callback)
Idea Matters