适用环境:.net 2.0+的Winform项目。
创新互联公司专注于企业全网营销推广、网站重做改版、民乐网站定制设计、自适应品牌网站建设、HTML5、商城网站开发、集团公司官网建设、成都外贸网站建设公司、高端网站制作、响应式网页设计等建站业务,价格优惠性价比高,为民乐等各大城市提供网站开发制作服务。
先解释一下我所谓的【带等待窗体的任务执行器】是个什么鬼,就是可以用该类执行任意耗时方法(下文将把被执行的方法称为任务或任务方法),执行期间会显示一个模式等待窗体,让用户知道任务正在得到执行,程序并没有卡死。先看一下效果:
功能:
等待窗体可以使用执行器自带的默认窗体(就上图的样子),嫌丑你也可以使用自己精心设计的窗体,甚至基于Devexpress、C1等第三方漂亮窗体打造也是完全可以的
在任务中可以更新等待窗体上的Label、ProgressBar之类的控件以提供进度反馈。懒得反馈的话,就默认“请稍候...”+Marquee式滚动
如果任务允许被终止,用户可以通过某些操作终止任务执行(例如点击上图中的【取消】按钮);如果不允许,你可以把取消按钮隐藏了,或者在任务中不响应用户的终止请求就好
任务的执行结果(包括ref/out参数)、是否出现异常、是否被取消等情况都可以得到
原理:
调用任务所属委托的BeginInvoke,让任务在后台线程执行,随即在UI线程(通常就是主线程)调用等待窗体的ShowDialog弹出模式窗体,让用户知道任务正在执行的同时阻止用户进行其他操作。由于任务和等待窗体分别在不同的线程跑,所以等待窗体不会被卡住
任务执行期间可以通过执行器提供的一组属性和方法操作等待窗体上的控件,这组属性和方法内部是通过调用等待窗体的Invoke或BeginInovke对控件进行操作,实现跨线程访问控件
任务执行期间用户可以通过点击等待窗体上的【取消】按钮(如果你让它显示的话)或点击右上角关闭按钮发出终止任务的请求(等待窗体会拦截关闭操 作),其结果是执行器的UserCancelling属性会置为true,所以在任务中可以访问该属性得知用户是否请求了取消操作,如果你同意终止的话, 需设置执行器的Cancelled=true,并随即return出任务方法
任务执行完后(无论成功、异常、取消)会自动进入异步回调方法,回调方法中会首先访问Cancelled获知任务是否已取消,如果已取消,则直接 return出回调方法;如果未取消,则调用任务所属委托的EndInvoke得到任务执行结果或异常。最后不管取消与否,finally块中会设置等待 窗体的DialogResult属性,以确保关闭等待窗体
等待窗体关闭后,执行器会继续执行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
(Foo), 33, 66);//执行委托 - //WaitUI.RunAction(new MyWaitForm(), Foo);//指定自定义等待窗体执行任务,几个RunXXX方法都有可指定自定义窗体的重载
- MessageBoxEx.Show("任务完成。" + r);
- }
- catch (WorkCancelledException)//任务被取消是通过抛出该异常来报告
- {
- MessageBoxEx.Show("任务已取消!");
- }
- catch (Exception ex)//任务抛出的异常
- {
- MessageBoxEx.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(100);
- }
- 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参数的任务方法,如果你想在任务执行后取回,请注意要这样:
- //正确
- object[] prms = { a, b };
- WaitUI.RunDelegate(new Action(Foo), prms);
- a = prms[0];
- b = prms[1];
- //错误
- WaitUI.RunDelegate(new Action(Foo), a, b);
即要先构造好参数数组(哪怕只有1个参数),完了传入数组,最后从数组中取出的元素才是被蹂蹑过的。不能直接传入单个参数变量,那样你是不能通过该变量取回改动过的值的,具体原因自己悟
方案源码:
WaitUI.cs包含class WaitUI和3个异常类WaitFormNullException、WorkIsBusyException、WorkCancelledException
- using System;
- using System.Reflection;
- using System.Windows.Forms;
- namespace AhDung.WinForm
- {
- ///
- /// 执行任务并显示等候窗体
- ///
- public static class WaitUI
- {
- static IWaitForm waitForm; //等待窗体
- static object result; //任务返回结果
- static Exception exception;//任务执行异常
- static object[] parmsInput; //调用者传入的参数
- static ParameterInfo[] parmsMethod;//任务所需的参数
- static bool isCallBackCompleted; //指示回调方法是否已执行完毕
- ///
- /// 指示用户是否已请求取消任务
- ///
- public static bool UserCancelling
- {
- get;
- private set;
- }
- ///
- /// 指示任务是否已取消
- ///
- public static bool Cancelled
- {
- private get;
- set;
- }
- ///
- /// 指示任务是否正在执行中
- ///
- public static bool IsBusy
- {
- get;
- private set;
- }
- #region 一系列操作等候窗体UI的属性/方法
- ///
- /// 获取或设置进度描述
- ///
- public static string WorkMessage
- {
- get
- {
- if (waitForm.InvokeRequired)
- {
- return waitForm.Invoke(new Func
- }
- return waitForm.WorkMessage;
- }
- set
- {
- if (waitForm == null) { return; }
- if (waitForm.InvokeRequired)
- {
- waitForm.BeginInvoke(new Action(() => waitForm.WorkMessage = value));
- return;
- }
- waitForm.WorkMessage = value;
- }
- }
- ///
- /// 获取或设置进度条可见性
- ///
- public static bool BarVisible
- {
- get
- {
- if (waitForm.InvokeRequired)
- {
- return Convert.ToBoolean(waitForm.Invoke(new Func
- }
- return waitForm.BarVisible;
- }
- set
- {
- if (waitForm == null) { return; }
- if (waitForm.InvokeRequired)
- {
- waitForm.BeginInvoke(new Action(() => waitForm.BarVisible = value));
- return;
- }
- waitForm.BarVisible = value;
- }
- }
- ///
- /// 获取或设置进度条动画样式
- ///
- public static ProgressBarStyle BarStyle
- {
- get
- {
- if (waitForm.InvokeRequired)
- {
- return (ProgressBarStyle)(waitForm.Invoke(new Func
- }
- return waitForm.BarStyle;
- }
- set
- {
- if (waitForm == null) { return; }
- if (waitForm.InvokeRequired)
- {
- waitForm.BeginInvoke(new Action(() => waitForm.BarStyle = value));
- return;
- }
- waitForm.BarStyle = value;
- }
- }
- ///
- /// 获取或设置进度值
- ///
- public static int BarValue
- {
- get
- {
- if (waitForm.InvokeRequired)
- {
- return Convert.ToInt32(waitForm.Invoke(new Func
- }
- return waitForm.BarValue;
- }
- set
- {
- if (waitForm == null) { return; }
- if (waitForm.InvokeRequired)
- {
- waitForm.BeginInvoke(new Action(() => waitForm.BarValue = value));
- return;
- }
- waitForm.BarValue = value;
- }
- }
- ///
- /// 获取或设置进度条步进值
- ///
- public static int BarStep
- {
- get
- {
- if (waitForm.InvokeRequired)
- {
- return Convert.ToInt32(waitForm.Invoke(new Func
- }
- return waitForm.BarStep;
- }
- set
- {
- if (waitForm == null) { return; }
- if (waitForm.InvokeRequired)
- {
- waitForm.BeginInvoke(new Action(() => waitForm.BarStep = value));
- return;
- }
- waitForm.BarStep = value;
- }
- }
- ///
- /// 使进度条步进
- ///
- public static void BarPerformStep()
- {
- if (waitForm == null) { return; }
- if (waitForm.InvokeRequired)
- {
- waitForm.BeginInvoke(new Action(() => waitForm.BarPerformStep()));
- return;
- }
- waitForm.BarPerformStep();
- }
- ///
- /// 获取或设置进度条上限值
- ///
- public static int BarMaximum
- {
- get
- {
- if (waitForm.InvokeRequired)
- {
- return Convert.ToInt32(waitForm.Invoke(new Func
- }
- return waitForm.BarMaximum;
- }
- set
- {
- if (waitForm == null) { return; }
- if (waitForm.InvokeRequired)
- {
- waitForm.BeginInvoke(new Action(() => waitForm.BarMaximum = value));
- return;
- }
- waitForm.BarMaximum = value;
- }
- }
- ///
- /// 获取或设置进度条下限值
- ///
- public static int BarMinimum
- {
- get
- {
- if (waitForm.InvokeRequired)
- {
- return Convert.ToInt32(waitForm.Invoke(new Func
- }
- return waitForm.BarMinimum;
- }
- set
- {
- if (waitForm == null) { return; }
- if (waitForm.InvokeRequired)
- {
- waitForm.BeginInvoke(new Action(() => waitForm.BarMinimum = value));
- return;
- }
- waitForm.BarMinimum = value;
- }
- }
- ///
- /// 获取或设置取消任务的控件的可见性
- ///
- public static bool CancelControlVisible
- {
- get
- {
- if (waitForm.InvokeRequired)
- {
- return Convert.ToBoolean(waitForm.Invoke(new Func
- }
- return waitForm.CancelControlVisible;
- }
- set
- {
- if (waitForm == null) { return; }
- if (waitForm.InvokeRequired)
- {
- waitForm.BeginInvoke(new Action(() => waitForm.CancelControlVisible = value));
- return;
- }
- waitForm.CancelControlVisible = value;
- }
- }
- #endregion
- #region 公共方法:无返回值+默认窗体
- ///
- /// 执行方法并显示默认等候窗体
- ///
- public static void RunAction(Action method)
- {
- RunDelegate(method);
- }
- ///
- /// 执行方法并显示默认等候窗体
- ///
- public static void RunAction
(Action method, T arg) - {
- RunDelegate(method, arg);
- }
- ///
- /// 执行方法并显示默认等候窗体
- ///
- public static void RunAction
(Action method, T1 arg1, T2 arg2) - {
- RunDelegate(method, arg1, arg2);
- }
- ///
- /// 执行方法并显示默认等候窗体
- ///
- public static void RunAction
(Action method, T1 arg1, T2 arg2, T3 arg3) - {
- RunDelegate(method, arg1, arg2, arg3);
- }
- ///
- /// 执行方法并显示默认等候窗体
- ///
- public static void RunAction
(Action method, T1 arg1, T2 arg2, T3 arg3, T4 arg4) - {
- RunDelegate(
当前文章:【C#】分享带等待窗体的任务执行器一枚
本文路径:http://www.shufengxianlan.com/qtweb/news7/47607.html网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联