打造迅速响应的用户界面
背景
最近抽时间开发了一个生成SQL脚本和执行脚本的小工具,基本已经完成,但由于生成脚本和执行脚本相对而言是比较耗时的操作,所以原先的单线程模式会短暂的冻结用户界面,由此,为了更好的用户体验,针对这两个耗时的操作引进了多线程模式。我的目标是给这两个操作添加对应的进度条(与用户界面处在不同的Form),显示目前的进度情况,及时把脚本的执行情况反馈给用户。下面,我就依托这个小小的背景,谈一下自己的见解吧。
需要做的工作
首先,需要做的事情是,搞清楚用户界面、进度条以及耗时的操作之间的关系,可以用UML的序列图表示,如下图所示:
从图中已经可以清晰地看到它们三者之间的关系,不过我还是要简单的叙述一下:
1) 用户界面构造并显示进度条。
2) 用户界面异步调用耗时操作。
3) 耗时操作需要及时地把执行情况反馈给用户界面。
4) 用户界面把目前的执行情况通知给进度条。
5) 耗时操作完成时,通知用户界面。
6) 用户界面销毁进度条,提示任务执行完毕。
明确了做什么之后,下一步就可以思考有哪些方法可以帮助我们解决这个问题了。
有哪些方法
不管采取哪些方法,你都需要思考如下几个问题:
1) 用户界面和耗时操作如何异步调用?
2) 耗时操作如果需要参数,那么如何传递参数?
3) 耗时操作如何把当前的执行情况发送给用户界面?
4) 如果耗时操作有返回值(分为每一个小的耗时操作的返回值和整个耗时操作的返回值),那么如何把执行的结果返回给用户界面?
5) 用户界面如何把执行的情况反馈给进度条?
如果这些问题你都已经考虑清楚了,那么这里要解决的问题也就不再是问题了。让我们逐个解答吧。
用户界面和耗时操作如何异步调用?
针对这个问题,我想解决办法有两种,一是自己利用多线程的相关类构建工作者线程,把耗时操作分配给工作者线程,然后在UI线程里开启工作者线程。第二种方法,使用异步执行委托,这是在UI线程里处理此类问题所特有的方法,使用此方法免去了很多代码,简单好用。
耗时操作如果需要参数,那么如何传递参数?
这个问题的答案就跟你上面选择的方式有关系了,如果你采用的是自己构建多线程,并且需要传递给工作者线程一些参数,你可以采取的办法有:
l 通过耗时操作的公开属性给其传值。
l 通过耗时操作类的构造函数的参数传值。
l 借助于第三方给其传值,如:你可以把耗时操作需要的数据预先存储到一个文件里或者数据库里。
l 混合使用以上的方法。
如果你采用的是异步执行委托的方式,除了以上的办法外,你还可以采取的办法有:
l 通过BeginInvoke传给耗时操作所需要的数据。
借助这些方法均可以帮助你顺利地把参数传递给耗时操作,我想说的是,你的需求决定了你的办法,你的办法决定了你的代码量以及性能,所以,要三思而后行。
耗时操作如何把当前的执行情况发送给用户界面?
由于耗时操作与UI处于不同的线程,所以UI不能直接得到耗时操作的执行情况,在这里,我们可以利用的办法有:
l 利用耗时操作的事件,把执行情况发送给事件处理者(用户界面)。
l 借助于第三方,把耗时操作的执行情况存储到一个文件里或者数据库里,供用户界面获取。
l 构建消息通道,通过消息通道,把消息发送给对消息感兴趣的接收者。
如何把执行的结果返回给用户界面?
这个问题与上一问题的解决方式差不多,只不过,如果用户界面采用的是异步委托执行的方式与耗时操作衔接的话,可以使用另外一种接收返回的执行结果,设置BeginInvoke的回调函数,然后在回调函数里调用EndInvoke接收耗时操作返回的结果。
用户界面如何把执行的情况反馈给进度条?
当用户界面得到执行的情况后,它需要把这些信息传给进度条,通过进度条,向用户展示目前耗时操作的执行情况,那么,用户界面是如何把执行的情况反馈给进度条呢?
在这里,我们需要判断用户界面与进度条,是否处在同一个线程里(使用InvokeRequired判断),决定是采用Invoke的方式,还是直接访问。
到这里,我们已经把所有面临的问题逐步剖析了一遍,相信你已经对类似的问题有了自己的解决办法。剩下的事情就是如何在实际的代码中运用这些方法了。
如何解决我的问题?
接下来,针对我的脚本辅助工具中遇到的问题,做一个阐述。由于我的工具里有两个比较耗时的操作,而解决它们的办法都较类似,所以,我就以脚本的产生为例子,说明一下我的解决之道。
首先,定义一个委托,用于异步执行耗时操作即产生脚本,代码如下:
然后,我需要做的是:构建进度条,启动耗时操作。在这里,我采用的是异步执行委托的方式启动耗时操作的,而我的耗时操作:产生脚本会在结束的时候返回产生的脚本。代码如下:
{
//禁用产生脚本按钮
this.btnCreateSql.Enabled = false;
//启动进度条
bar = new ProgressBar();
bar.Title = "正在生成脚本";
bar.StartPosition = FormStartPosition.CenterParent;
bar.Show();
bar.ShowProgress(list.Count,0);
//开始产生脚本
BuildSQL.DTO.SQLServer2000.SQLScript sqlScript = new BuildSQL.DTO.SQLServer2000.SQLScript();
//绑定事件,获取耗时操作执行的情况
sqlScript.ProgressChangeEventHandler+=new BuildSQL.DTO.SQLServer2000.SQLScript.ProgressDelegate(sqlScript_ProgressChangeEventHandler);
CreateSql sql = new CreateSql(sqlScript.CreateSQLScript);
//异步执行委托,并绑定回调函数CreateSqlFinish,目的是在回调函数里获取产生的脚本
sql.BeginInvoke(list,server,new AsyncCallback(CreateSqlFinish),sql);
}
为了获取耗时操作的执行情况,我采用了事件机制,封装在耗时操作类里的代码为:
public delegate void ProgressDelegate(object sender,ProgressEventArgs e);
public event ProgressDelegate ProgressChangeEventHandler;
//触发事件
public int Now
{
get{return m_now;}
set
{
m_now = value;
//触发事件
if(ProgressChangeEventHandler != null)
{
ProgressEventArgs e = new ProgressEventArgs(m_max,value);
ProgressChangeEventHandler(this,e);
}
//任务完成事件
if(value == m_max)
{
if(ProgressFinishEventHandler != null)
ProgressFinishEventHandler(this, new EventArgs());
}
}
}
用户界面接收耗时操作的执行情况,并把这些信息发送给进度条,代码如下:
{
if(InvokeRequired)
{
ShowProgressDelegate show = new ShowProgressDelegate(bar.ShowProgress);
show.BeginInvoke(e.Max,e.Now,null,null);
}
else
{
bar.ShowProgress(e.Max,e.Now);
}
}
最后,当耗时操作结束的时候,获取其执行的结果,在这里获取的是产生的脚本,如下:
{
if(bar != null)
{
bar.Close();
}
if(MessageBox.Show("脚本产生成功!") == DialogResult.OK)
{
this.txtScript.Text = ((CreateSql)result.AsyncState).EndInvoke(result);
//启用产生脚本按钮
this.btnCreateSql.Enabled = true;
}
}
到此,我的这个工具中所要解决的问题已经解决了。在这里,我想提醒的是,你不必关注代码的细节,而应该关注代码的骨架。当然这些代码如果想运用在其它地方,可能需要重新编写,我想,如果你愿意,那么下一步考虑的事情应该是如何把这些代码抽取到一个类中,更容易的应用到类似这样的需求里。
最后,给大家推荐一下我的这个脚本辅助工具,希望它也可以给你的工作带来便利。文中如果有不对的地方,还请大家批评指正。
备注:如果你需要了解更多的信息,你可以阅读msdn的相关文章:通过多线程为基于 .NET 的应用程序实现响应迅速的用户