winform 异步

异步:

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

转载于http://www.cnblogs.com/haogj/archive/2012/12/13/2817047.html

今天有人问道如何在 WinForm 程序中,使用进度条显示长时间任务的执行进度。

这个问题是一个开发中很常见的问题,正好也整理和总结一下。

这个问题我们从两个部分来看,第一,长时间执行的任务如何暴露出其执行进度,第二,WinForm 窗体如何显示执行进度。

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

先看第一个问题,如果希望一个长时间执行的任务能够展示其执行进度,显然它必须提供当前执行的进度值。但是,一般来说,一个任务通常是一个方法,执行完也就完了,怎么能在一个方法的执行过程中,向外界提供其执行的进度呢?

答案就是设计模式中的观察者模式。我们可以将任务的执行者看作观察者模式中的主题,而窗体就是观察者了。在方法的执行过程中,主题不断改变其状态,而观察者通过观察主题的状态来显示其执行进度。

在.NET 中,典型的观察者模式是通过事件来实现的。事件参数则用来提供主题的状态,System.EventArgs为事件参数提供了基类,我们实现的事件参数应当从这个基类派生,提供自定义的额外属性。

首先定义进度状态的事件参数类,其属性 Value 表示当前进度的百分比。

// 定义事件的参数类

publicclass ValueEventArgs

    : EventArgs

{

    public int Value {set; get;}

}

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

// 定义事件使用的委托

publicdelegate void ValueChangedEventHandler(object sender, ValueEventArgs e);

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

class LongTimeWork
{
    // 定义一个事件来提示界面工作的进度
    public event ValueChangedEventHandler ValueChanged;
    // 触发事件的方法
    protected void OnValueChanged( ValueEventArgs e)
    {
        ifthis.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 委托。不用Action也可以自己定义一个方法的委托。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; }
这样,我们的方法,就可以修改为下面的形式

// 进度发生变化之后的回调方法

privatevoidworkder_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();
    }
}
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值