您的位置:首页 > 其它

观察者模式的异步进度条

2015-01-18 11:25 162 查看

在 WinForm 中使用进度条展示长时间任务的执行进度

今天有人问道如何在 WinForm 程序中,使用进度条显示长时间任务的执行进度。
这个问题是一个开发中很常见的问题,正好也整理和总结一下。
这个问题我们从两个部分来看,第一,长时间执行的任务如何暴露出其执行进度,第二,WinForm 窗体如何显示执行进度。

第一部分. 对象如何提供其处理进度

先看第一个问题,如果希望一个长时间执行的任务能够展示其执行进度,显然它必须提供当前执行的进度值。但是,一般来说,一个任务通常是一个方法,执行完也就完了,怎么能在一个方法的执行过程中,向外界提供其执行的进度呢?
答案就是设计模式中的观察者模式。我们可以将任务的执行者看作观察者模式中的主题,而窗体就是观察者了。在方法的执行过程中,主题不断改变其状态,而观察者通过观察主题的状态来显示其执行进度。
在 .NET 中,典型的观察者模式是通过事件来实现的。事件参数则用来提供主题的状态,System.EventArgs 为事件参数提供了基类,我们实现的事件参数应当从这个基类派生,提供自定义的额外属性。
首先定义进度状态的事件参数类,其属性 Value 表示当前进度的百分比。

// 定义事件的参数类
public class ValueEventArgs
: EventArgs
{
public int Value { set; get;}
}


然后,定义事件所使用的委托。这个委托使用事件参数对象作为方法的参数。

// 定义事件使用的委托
public delegate void ValueChangedEventHandler( object sender, ValueEventArgs e);


最后,方法不能单独存在,我们定义业务对象,包含需要长时间执行的方法。

class LongTimeWork
{
// 定义一个事件来提示界面工作的进度
public event ValueChangedEventHandler ValueChanged;

// 触发事件的方法
protected void OnValueChanged( ValueEventArgs e)
{
if( this.ValueChanged != null)
{
this.ValueChanged( this, e);
}
}

public void LongTimeMethod()
{
for (int i = 0; i < 100; i++)
{
// 进行工作
System.Threading.Thread.Sleep(1000);

// 触发事件
ValueEventArgs e = new ValueEventArgs() { Value = i+1};
this.OnValueChanged(e);
}
}
}


注意,在这个类中,我们使用了典型的事件模式,OnValueChanged 在类中用来触发事件,将当前的进度状态提供给观察者。在 LongTimeMethod 方法中,通过调用这个方法将当前的进度提供给窗体。这个方法中通过使用 Sleep,共需花费 100 秒以上的时间才能执行完毕。

第二部分 窗体与线程问题

在项目中创建一个窗体,放置一个进度条和一个按钮。



双击按钮,就可以开始界面编程了。
在按钮的处理事件中,写下如下的代码,通过事件来获取主题的通知,在 ValueChanged 事件的处理方法中更新进度条。

private void button1_Click(object sender, EventArgs e)
{
// 禁用按钮
this.button1.Enabled = false;

// 实例化业务对象
LongTime.Business.LongTimeWork workder = new Business.LongTimeWork();

workder.ValueChanged += new Business.ValueChangedEventHandler(workder_ValueChanged);

workder.LongTimeMethod();
}


下面是 ValueChanged 事件的处理方法。

// 进度发生变化之后的回调方法
private void workder_ValueChanged(object sender, Business.ValueEventArgs e)
{
this.progressBar1.Value = e.Value;
}


点击按钮,看起来执行正常呀,在窗体上点一下鼠标,或者在标题栏拖动一下窗口,马上就会看到界面失去了反应。



为什么会这样的?我们使用的就是典型的事件处理模式呀?
问题出在界面的线程问题上,整个界面的操作运行在一个线程上,在 Win32 时代被称为消息循环,你可以将系统对窗体的处理看成一个无限的循环,不断地获取消息,处理消息。但是,不要忘了,在一个循环中,如果一个步骤卡在了那里,其它的步骤就不会有机会执行了。
对于我们这个长时间执行的方法来说,在开始调用这句代码的时候

workder.LongTimeMethod();


就已经阻塞了这个窗体的循环,使得 Windows 没有机会来处理用户的操作,不能处理按钮,不能处理菜单,也不能拖动,通常我们成为冻结了。
显然,我们不希望这样的结果。
解决的办法就是将这个长时间执行的方法在另外一个线程上执行,而不要占用我们窗体界面处理的宝贵时间。
在 .NET 实现异步的基本方式就是委托,我们可以将这个方法表示为一个委托,然后通过委托的 BeginXXX 来实现异步调用。这样按钮的点击事件处理就成为了下面的样子。

private void button1_Click(object sender, EventArgs e)
{
// 禁用按钮
this.button1.Enabled = false;

// 实例化业务对象
LongTime.Business.LongTimeWork workder = new Business.LongTimeWork();

workder.ValueChanged += new Business.ValueChangedEventHandler(workder_ValueChanged);

// 使用异步方式调用长时间的方法
Action handler = new Action(workder.LongTimeMethod);
handler.BeginInvoke(
new AsyncCallback(this.AsyncCallback),
handler
);
}


这里使用了系统定义的 Action 委托。由于使用 BeginInvoke 必须配合 EndInvoke , 而 EndInvoke 需要借助于开始的委托,所以在第二个参数中,将委托对象传递出去。
这里的 AsyncCallback 是异步处理完成之后的回调方法,如下所示

// 结束异步操作
private void AsyncCallback(IAsyncResult ar)
{
// 标准的处理步骤
Action handler = ar.AsyncState as Action;
handler.EndInvoke(ar);

MessageBox.Show("工作完成!");

this.button1.Enabled = true;
}


再次执行程序,看起来还不错。



不过,别高兴的太早,没准你现在就已经看到了这个异常。如果还没有看到,就在调试模式下看一看。



第三部分 回到 UI 线程

现在,我们的方法正在一步一步的进行,但是需要注意的是它工作在一个线程上,而 UI 工作在自己的线程上,这两个线程可能是同一个线程,更可能不是同一个线程。
在 Windows 中规定,对于窗体的处理,例如修改窗体控件的属性,必须在窗体的线程上才允许进行,不仅 Windows 界面,几乎所有的图形界面皆是如此,这关系到效率问题。
当我们在另外一个线程上修改窗体控件的属性的时候,异常被抛了出来。
难道还要回到 UI 线程上来执行我们长时间的方法吗?当然不是,Control 基类就提供了两个方法 Invoke 和 BeginInvoke ,允许我们以委托的形式将需要进行的处理排到 UI 的线程处理列表中,等待 UI 线程在适当的时候来执行。
使用什么委托呢?是委托都可以,Windows Forms 中提供了一个专用的委托,可以考虑使用一下。

public delegate void MethodInvoker()


其实跟 Action 一样,不过看起来专业一点,我们就使用它了。
不过,也有可能我们的线程与 UI 的线程正好是同一个线程,那我们就没有必要这么麻烦了,Control 还定义了一个属性 InvokeRequired 用来检查是否在同一个线程之上,不是则返回真,需要使用委托进行,否则返回假,可以直接处理控件。

[BrowsableAttribute(false)]
public bool InvokeRequired { get; }


这样,我们的方法,就可以修改为下面的形式

// 进度发生变化之后的回调方法
private void workder_ValueChanged(object sender, Business.ValueEventArgs e)
{
System.Windows.Forms.MethodInvoker invoker = ()=>this.progressBar1.Value = e.Value;

if (this.progressBar1.InvokeRequired)
{
this.progressBar1.Invoke(invoker);
}
else
{
invoker();
}
}


同样,结束异步的回调函数中,需要将按钮的状态重新启用,也如法炮制。

// 结束异步操作
private void AsyncCallback(IAsyncResult ar)
{
// 标准的处理步骤
Action handler = ar.AsyncState as Action;
handler.EndInvoke(ar);

MessageBox.Show("工作完成!");

// 重新启用按钮
System.Windows.Forms.MethodInvoker invoker = ()=>this.button1.Enabled = true;

if (this.InvokeRequired)
{
this.Invoke(invoker);
}
else
{
invoker();
}

}


完整的代码可以在这里下载:轻轻地点一下就可以下载了!
/article/4689606.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐