【C#】分享带等待窗体的任务执行器一枚-------------201504161039更新-------------
更新内容:
- IWaitForm接口删除System.Windows.Forms.DialogResult DialogResult属性。即隐藏等待窗体的方式不再分为设置DialogResult和调用Hide()两种,改为仅调用Hide()一种,简化设计。由于Hide()属于访问控件,执行器需根据自身是否会跨线程调用该方法而做出相应处理
- WaitUI增加私有方法HideWaitForm,用于隐藏等待窗体(由于会在后台线程调用该方法,故内部有跨线程处理),替代原来的设置DialogResult的做法
- WaitForm的FormClosing事件由注册该事件改为重写OnFormClosing方法,对阻止窗体关闭的条件增加了Visible,即当窗体处于可见时,才会阻止窗体关闭和触发UserCancelling事件,这是为了更准确的区分是执行器调用Hide()隐藏等待窗体,还是用户关闭等待窗体,仅通过e.CloseReason是不可靠的,因为当用户点过关闭按钮后,e.CloseReason就会是UserClosing,稍后执行器在调用Hide隐藏窗体时,仍然会进入OnFormClosing,此时e.CloseReason仍然是UserClosing,就会再次触发UserCancelling事件,虽然没什么影响,但实属不应该,加了Visible的话,执行器Hide窗体后,Visible就为false,就不会再次触发UserCancelling事件。当然,仍然建议自定义等待窗体屏蔽关闭按钮,让用户只能通过点击取消控件来取消任务,就没那么多事了。但不建议通过把ControlBox=false来整个隐藏右上角那仨按钮,因为我始终认为要给用户最小化的权利,我作为用户使用其它软件的时候,是很痛恨这种限制的
- 等待窗体的【取消】按钮单击后不再将Enabled置为false。原因是在基于BackgroundWorker的方案中,等待窗体Hide后有可能再次ShowDialog,也就是再次执行任务时依然要保证可取消
- 将WaitFormNullException异常的定义移至WaitForm.cs文件中(原先在WaitUI.cs中)。原因是等待窗体相关的东西应该与执行器保持独立
-------------20150415原文(已更新)-------------
适用环境:.net 2.0+的Winform项目。
先解释一下我所谓的【带等待窗体的任务执行器】是个什么鬼,就是可以用该类执行任意耗时方法(下文将把被执行的方法称为任务或任务方法),执行期间会显示一个模式等待窗体,让用户知道任务正在得到执行,程序并没有卡死。先看一下效果:
功能:
- 等待窗体可以使用执行器自带的默认窗体(就上图的样子),嫌丑你也可以使用自己精心设计的窗体,甚至基于DevexPRess、C1等第三方漂亮窗体打造也是完全可以的
- 在任务中可以更新等待窗体上的Label、ProgressBar之类的控件以提供进度反馈。懒得反馈的话,就默认“请稍候...”+Marquee式滚动
- 如果任务允许被终止,用户可以通过某些操作终止任务执行(例如点击上图中的【取消】按钮);如果不允许,你可以把取消按钮隐藏了,或者在任务中不响应用户的终止请求就好
- 任务的执行结果(包括ref/out参数)、是否出现异常、是否被取消等情况都可以得到
原理:
- 调用任务所属委托的BeginInvoke,让任务在后台线程执行,随即在UI线程(通常就是主线程)调用等待窗体的ShowDialog弹出模式窗体,让用户知道任务正在执行的同时阻止用户进行其他操作。由于任务和等待窗体分别在不同的线程跑,所以等待窗体不会被卡住
- 任务执行期间可以通过执行器提供的一组属性和方法操作等待窗体上的控件,这组属性和方法内部是通过调用等待窗体的Invoke或BeginInovke对控件进行操作,实现跨线程访问控件
- 任务执行期间用户可以通过点击等待窗体上的【取消】按钮(如果你让它显示的话)或点击右上角关闭按钮发出终止任务的请求(等待窗体会拦截关闭操作),其结果是执行器的UserCancelling属性会置为true,所以在任务中可以访问该属性得知用户是否请求了取消操作,如果你同意终止的话,需设置执行器的Cancelled=true,并随即return出任务方法
- 任务执行完后(无论成功、异常、取消)会自动进入异步回调方法,回调方法中会首先访问Cancelled获知任务是否已取消,如果已取消,则直接return出回调方法;如果未取消,则调用任务所属委托的EndInvoke得到任务执行结果或异常。最后不管取消与否,finally块中会调用HideWaitForm(),以确保关闭等待窗体
- 等待窗体关闭后,执行器会继续执行ShowDialog后面的语句。如果任务已取消,则抛出特定异常报告调用者;如果任务存在异常,则抛出该异常;以上情况都不存在的话,返回任务结果
如述,功能简单,实现容易,我只是把这种需求通用化了一下,让还没有类似轮子的朋友可以拿去就用。另外还有个基于BackgroundWorker的实现方式,我可能会在下一篇文章分享。
先看一下大致的使用示例:
//WaitUI就是执行器private void button1_Click(object sender, EventArgs es){ //可检测执行器是否正在执行另一个任务。其实基本不可能出现IsBusy=true,因为执行器工作时,用户做不了其它事 //老实说这个IsBusy要不要公开我还纠结了一下,因为公开了没什么用,但也没什么坏处,因为setter是private的 //Whatever~最后我还是选择公开,可能~因为爱情 //if (WaitUI.IsBusy) { return; } try { //WaitUI.RunXXX方法用于执行任务 //该方法的返回值就是任务的返回值 //任务抛出的异常会通过RunXXX方法抛出 //WaitUI.RunAction(Foo, 33, 66); //执行无返回值的方法 int r = WaitUI.RunFunc(Foo, 33, 66); //执行有返回值的方法 //object r = WaitUI.RunDelegate(new Func<int, int, int>(Foo), 33, 66);//执行委托 //WaitUI.RunAction(new MyWaitForm(), Foo);//指定自定义等待窗体执行任务,几个RunXXX方法都有可指定自定义窗体的重载 MessageBox.Show("任务完成。" + r); } catch (WorkCancelledException)//任务被取消是通过抛出该异常来报告 { MessageBox.Show("任务已取消!"); } catch (Exception ex)//任务抛出的异常 { MessageBox.Show("任务出现异常!" + ex.Message); }}//耗时任务。因为该方法会在后台线程执行,所以方法中不可以有访问控件的代码int Foo(int a, int b){ //可以通过执行器的一系列公开属性和方法间接操作等待窗体的UI元素 WaitUI.CancelControlVisible = true;//设置取消任务的控件的可见性,即是否允许用户取消任务(默认是false:不可见) WaitUI.BarStyle = ProgressBarStyle.Continuous;//设置滚动条样式(默认是Marquee:循环梭动式) WaitUI.BarMaximum = 100; //设置滚动条值上限(默认是100) WaitUI.BarMinimum = 0; //设置滚动条值下限(默认是0) WaitUI.BarStep = 1; //设置滚动条步进幅度(默认是10) WaitUI.BarVisible = true; //设置滚动条是否可见(默认是true:可见) int i; for (i = a; i < b; i++) { if (WaitUI.UserCancelling)//响应用户的取消请求 { WaitUI.Cancelled = true;//告诉执行器任务已取消 return 0; } //可以抛个异常试试 //if (i == 43) { throw new NotSupportedException("异常测试"); } //可以随时再次操作等待窗体的各种UI元素 //if (i % 10 == 0) { WaitUI.CancelControlVisible = false; } //隐藏取消控件 //else if (i % 5 == 0) { WaitUI.CancelControlVisible = true; }//显示取消控件 WaitUI.WorkMessage = "正在XXOO,已完成 " + i + " 下..."; //更新进度描述 WaitUI.BarValue = i;//更新进度值 //WaitUI.BarPerformStep();//步进进度条 Thread.Sleep(50); } return i;}
使用示例看完示例,熟悉套路的你可能都已经能洞悉实现细节了,不过作为方案分享文章,我还是照章讲一下使用说明先,后面再掰扯设计说明,先看类图:
使用说明:
- WaitUI通过RunAction、RunFunc、RunDelegate这3个基本方法和它们的重载执行任务,看名字就知道,它们依次是执行无返回值方法、有返回值方法和自定义委托,每个方法都有不指定等待窗体和指定等待窗体两种重载形态,不指定时就使用方案自带的WaitForm作为等待窗体。自定义等待窗体需实现IWaitForm接口,详情在后面的设计说明部分有说。这里就表示等待窗体是在执行任务时才传进去的,任务执行完成后,WaitUI会销毁等待窗体,这是为了让WaitUI作为一个静态类,尽量短暂的持有对象,节约内存。所以如果传入的是自定义等待窗体的变量,请注意不要在WaitRun之后再次使用该变量,因为它已经被销毁,推荐的做法是直接在RunXXX中new一个自定义等待窗体。当然如果不嫌弃自带等待窗体,直接就不用传入,自然不会有这个问题。前两种方法是泛型方法,根据Action和Func这俩泛型委托重载,这俩委托支持到最多16个参数,但为了节约篇幅,方案中只重载了0~8个参数的情况,用户可以根据需要增加重载。它俩可以执行任意不多于8个参数的有返回或无返回方法,得益于编译器的智能推断,使用时非常方便,直接RunAction(Foo, arg1, arg2, ...)就好了,根本不用纠结到底要使用哪个重载。对于RunDelegate方法,接受的是一个委托实例,也就是不能直接传入方法,必须要用委托把方法套上才行。任何委托都可以传入,所以RunDelegate是最应万变的方法,当你的方法存在ref/out参数,或者参数个数变态到超过16个时,你还可以也只可以选用RunDelegate。但有个限制,委托有且只有绑定一个方法,RunXXX拒绝执行委托链
- RunFunc和RunDelegate方法有返回值,前者的返回类型与任务方法的返回类型一致,后者则是object。它俩的返回值就是任务方法的返回值。同样,任务抛出的异常一样会在3种RunXXX方法中抛出来,等于用RunXXX执行任务与直接调用任务相比,除了写法上有所不同:
int a = WaitUI.RunFunc(Foo,33);int b = Foo(33);
调用体验是一样一样的,所以你拿去就可以用,不需要对任务方法做什么修改,带个套就完事儿,除非任务方法存在下面这种情况
- 原理中说过,RunXXX方法实际上是调用任务所属委托的BeginInvoke方法,也就是异步执行任务,也就是任务会在另一个线程执行。所以任务中不能访问控件,这恐怕是该方案最大的不便,但确实原理所限,所以如果你的任务有访问控件的代码,还得做出改动才行。要问为什么非得让任务在后台,而等待窗体在前台,不可以调换过来吗?那样不就没这个不便了吗?那是因为等待窗体如果不在主线程ShowDialog,它就达不到模式的效果,用户仍然可以唱歌跳舞,这恐怕是你不愿意的
- 任务中可以通过WaitUI的一组属性和方法(WorkMessage、BarValue、BarPerformStep等)更新等待窗体中的文本呈现控件和进度指示控件(不限于Label和ProgressBar,取决于等待窗体的设计),用来向用户报告任务执行进度。当然不想做任何报告也可以,就让用户面对一个“请稍候...”和循环滚动条也无不可,具体文字和滚动条样式取决于等待窗体的默认设置
- WaitUI有个CancelControlVisible属性,可以设置为true/false控制等待窗体上是否显示【取消】按钮之类的控件(不限于Button,取决于等待窗体的设计,所以下文不说取消按钮,说取消控件)。这里需要详细说一下该方案的取消任务的机制,其实与BackgroundWorker的机制一致(好吧是我借鉴了它),熟悉bgw的老鸟请略过。显示取消控件只代表用户可以请求终止任务,至于你(或者说任务)是否响应这个请求(同意终止与否)是另一回事。什么意思,就是用户点击取消控件后,不是说任务就会自动终止了~凭什么会终止嘛对吧,任务在线程池,又不可能Abort,所以任务是否终止完全取决你在任务代码中的处理,比如你在任务中段就来个return或throw ex,这个就叫终止,任由代码执行下去,就叫做没终止,所以用户请求终止与任务是不是真的终止了没有必然联系。说这么多是什么意思,就是如果你要让用户看到取消控件,那么你就应该响应用户的请求,反过来如果不想任务被终止,那么就别让用户有发起请求的可能,当然这是与技术无关的纯人机交互理念的东西,没有对错,反正我是建议不要欺骗用户,下面说说如何响应终止请求。当用户发起终止请求后,WaitUI的UserCancelling会变为true,在任务中你可以根据这个值来做出终止任务的处理,但是在终止之前,还得麻烦你设置一个标记,千万别忘记,就是让WaitUI.Cancelled = true,这等于告诉执行器任务确实终止了,在设置完标记后,最好紧跟终止代码,不要再做其它事,让Cancelled与事实一致。执行器中根据Cancelled来获知任务是否已终止,进而做出相应的处理和返回。为什么不根据UserCancelling而是Cancelled相信你已经明白了,前者是用户的意愿,后者是开发者的决定,当然是决定靠谱。回到CancelControlVisible属性,这个属性建议在任务方法顶部就设置,因为一个任务是否可终止应该是确定的,通常来说,循环类任务是可以终止的,例如批量处理图片,一圈处理一张,那这种任务是可以也应该允许用户终止的;而非循环类任务,或者原子性比较强的任务,开始了就只能等待结果或报错,这种任务一方面可能就不允许用户终止,另一方面则是想终止都终止不了(比如WebRequest.GetResponse、SqlCommand.ExecuteNonQuery之类),这种任务最好就不提供取消控件给用户。不过CancelControlVisible也像WorkMessage之类的属性一样,是可以在任务中随时+反复设置的,所以你的任务可能有些阶段可被终止,有时则不允许终止,开开合合都是可以的,as you wish
- RunXXX有3种执行结果:①成功执行任务,返回任务返回值~如果任务有返回值的话;②任务产生异常,RunXXX会原样抛出该异常;③任务被终止,抛出WorkCancelledException异常(后面有关于为什么选择抛异常这种方式的说明)。你自行根据不同结果做相应处理
- 对于有ref/out参数的任务方法,如果你想在任