打造迅速响应的用户界面

背景


最近抽时间开发了一个生成
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的方式,还是直接访问。

到这里,我们已经把所有面临的问题逐步剖析了一遍,相信你已经对类似的问题有了自己的解决办法。剩下的事情就是如何在实际的代码中运用这些方法了。

 

如何解决我的问题?


接下来,针对我的脚本辅助工具中遇到的问题,做一个阐述。由于我的工具里有两个比较耗时的操作,而解决它们的办法都较类似,所以,我就以脚本的产生为例子,说明一下我的解决之道。

首先,定义一个委托,用于异步执行耗时操作即产生脚本,代码如下:

 

private delegate string CreateSql(ArrayList list,BuildSQL.DataBase.SQLServer server);

 

然后,我需要做的是:构建进度条,启动耗时操作。在这里,我采用的是异步执行委托的方式启动耗时操作的,而我的耗时操作:产生脚本会在结束的时候返回产生的脚本。代码如下:

private void StartCreateSql(ArrayList list,BuildSQL.DataBase.SQLServer server)

         
{

              
//禁用产生脚本按钮

              
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(
thisnew EventArgs());

                   }


              }


         }


 

用户界面接收耗时操作的执行情况,并把这些信息发送给进度条,代码如下:

private void sqlScript_ProgressChangeEventHandler(object sender, BuildSQL.DTO.SQLServer2000.ProgressEventArgs e)
        
{
            
if(InvokeRequired)
            
{
                ShowProgressDelegate show 
= new ShowProgressDelegate(bar.ShowProgress);
                show.BeginInvoke(e.Max,e.Now,
null,null);
            }

            
else
            
{
                bar.ShowProgress(e.Max,e.Now);
            }

        }

 

最后,当耗时操作结束的时候,获取其执行的结果,在这里获取的是产生的脚本,如下:

private void CreateSqlFinish(IAsyncResult result)
        
{
            
if(bar != null)
            
{
                bar.Close();
            }

            
if(MessageBox.Show("脚本产生成功!"== DialogResult.OK)
            
{
                
this.txtScript.Text = ((CreateSql)result.AsyncState).EndInvoke(result);
                
//启用产生脚本按钮
                this.btnCreateSql.Enabled = true;
            }

        }

 


到此,我的这个工具中所要解决的问题已经解决了。在这里,我想提醒的是,你不必关注代码的细节,而应该关注代码的骨架。当然这些代码如果想运用在其它地方,可能需要重新编写,我想,如果你愿意,那么下一步考虑的事情应该是如何把这些代码抽取到一个类中,更容易的应用到类似这样的需求里。


最后,给大家推荐一下我的这个脚本辅助工具,希望它也可以给你的工作带来便利。文中如果有不对的地方,还请大家批评指正。

备注:如果你需要了解更多的信息,你可以阅读msdn的相关文章:通过多线程为基于 .NET 的应用程序实现响应迅速的用户

posted @ 2008-01-21 23:26  小罗  阅读(181)  评论(0编辑  收藏  举报