InvokeHelper:多线程修改主界面控件属性并调用其中方法

© 野比 2012

源代码:点击下载

下面是动画,演示在多线程(无限循环+Thread.Sleep)情况下主界面操作不受影响。


多线程是一种提高程序运行效率和性能的常用技术。随着我们学习工作的深入,在编程中或多或少会涉及到需要多线程的情况。多数时候,我们的操作模式是后台线程中处理数据,计算结果,然后在前台界面(GUI)中更新显示。

在.NET Framework中,为了保证线程安全,避免出现访问竞争等问题,是不允许跨线程访问窗体控件的。如果强行访问,则会引发InvalidOperationException无效操作异常,如下图:


为了实现跨线程访问控件,.NET Framework为每个控件提供了InvokeRequired属性和Invoke方法。使用这些技巧,就可以实现我们在其他线程中直接修改界面的需要。看起来似乎很简单,但实际每次调用都有不少代码需要编写,还需要自行处理各种异常。下面是典型的调用例子:

public void DoWork()
{
    if (control.InvokeRequired)
    {
        control.Invoke(DoWork);
    }
    else
    {
        // do work
    }
}

为了便于使用,我封装了实现细节,在这里给出一个 InvokeHelper类,使用该类即可方便地实现跨线程调用主界面控件方法、获取/设置控件属性等功能。

该类实现非常简单,有效代码约150行,主要有以下3个方法:

1.Invoke

该方法可以调用主界面控件的某个方法,并返回方法执行结果。用法如下:

InvokeHelper.Invoke(<控件>, "<方法名称>", <参数>);

其中“参数”为参数列表,支持0个或多个参数。

2.Get

该方法可以获取主界面控件的某个属性。用法如下:

InvokeHelper.Get(<控件>, "<属性名称>");

3. Set

该方法可以设置主界面控件的某个属性。用法如下:

InvokeHelper.Set(<控件>, "<属性名称>", <属性值>);

下面是整个类的实现代码。文章最后是一个演示用的例子。

/*******************************************************************************
 * InvokeHelper.cs
 * A thread-safe control invoker helper class.
 * -----------------------------------------------------------------------------
 * Project:Conmajia.Controls
 * Author:Conmajia
 * Url:conmajia@gmail.com
 * History:
 *      4th Aug., 2012
 *      Added support for "Non-control" controls (such as ToolStripItem).
 *      
 *      4th Aug., 2012
 *      Initiated.
 ******************************************************************************/
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using System.Windows.Forms;

namespace InvokerHelperDemo
{
    /// <summary>
    /// A thread-safe control invoker helper class.
    /// </summary>
    public class InvokeHelper
    {
        #region delegates
        private delegate object MethodInvoker(Control control, string methodName, params object[] args);

        private delegate object PropertyGetInvoker(Control control, object noncontrol, string propertyName);
        private delegate void PropertySetInvoker(Control control, object noncontrol, string propertyName, object value);
        #endregion

        #region static methods
        // helpers
        private static PropertyInfo GetPropertyInfo(Control control, object noncontrol, string propertyName)
        {
            if (control != null && !string.IsNullOrEmpty(propertyName))
            {
                PropertyInfo pi = null;
                Type t = null;

                if (noncontrol != null)
                    t = noncontrol.GetType();
                else
                    t = control.GetType();

                pi = t.GetProperty(propertyName);

                if (pi == null)
                    throw new InvalidOperationException(
                        string.Format(
                        "Can't find property {0} in {1}.",
                        propertyName,
                        t.ToString()
                        ));

                return pi;
            }
            else
                throw new ArgumentNullException("Invalid argument.");
        }

        // outlines
        public static object Invoke(Control control, string methodName, params object[] args)
        {
            if (control != null && !string.IsNullOrEmpty(methodName))
                if (control.InvokeRequired)
                    return control.Invoke(
                        new MethodInvoker(Invoke),
                        control,
                        methodName,
                        args
                        );
                else
                {
                    MethodInfo mi = null;

                    if (args != null && args.Length > 0)
                    {
                        Type[] types = new Type[args.Length];
                        for (int i = 0; i < args.Length; i++)
                        {
                            if (args[i] != null)
                                types[i] = args[i].GetType();
                        }

                        mi = control.GetType().GetMethod(methodName, types);
                    }
                    else
                        mi = control.GetType().GetMethod(methodName);

                    // check method info you get
                    if (mi != null)
                        return mi.Invoke(control, args);
                    else
                        throw new InvalidOperationException("Invalid method.");
                }
            else
                throw new ArgumentNullException("Invalid argument.");
        }

        public static object Get(Control control, string propertyName)
        {
            return Get(control, null, propertyName);
        }
        public static object Get(Control control, object noncontrol, string propertyName)
        {
            if (control != null && !string.IsNullOrEmpty(propertyName))
                if (control.InvokeRequired)
                    return control.Invoke(new PropertyGetInvoker(Get),
                        control,
                        noncontrol,
                        propertyName
                        );
                else
                {
                    PropertyInfo pi = GetPropertyInfo(control, noncontrol, propertyName);
                    object invokee = (noncontrol == null) ? control : noncontrol;

                    if (pi != null)
                        if (pi.CanRead)
                            return pi.GetValue(invokee, null);
                        else
                            throw new FieldAccessException(
                                string.Format(
                                "{0}.{1} is a write-only property.",
                                invokee.GetType().ToString(),
                                propertyName
                                ));

                    return null;
                }
            else
                throw new ArgumentNullException("Invalid argument.");
        }

        public static void Set(Control control, string propertyName, object value)
        {
            Set(control, null, propertyName, value);
        }
        public static void Set(Control control, object noncontrol, string propertyName, object value)
        {
            if (control != null && !string.IsNullOrEmpty(propertyName))
                if (control.InvokeRequired)
                    control.Invoke(new PropertySetInvoker(Set),
                        control,
                        noncontrol,
                        propertyName,
                        value
                        );
                else
                {
                    PropertyInfo pi = GetPropertyInfo(control, noncontrol, propertyName);
                    object invokee = (noncontrol == null) ? control : noncontrol;

                    if (pi != null)
                        if (pi.CanWrite)
                            pi.SetValue(invokee, value, null);
                        else
                            throw new FieldAccessException(
                                string.Format(
                                "{0}.{1} is a read-only property.",
                                invokee.GetType().ToString(),
                                propertyName
                                ));
                }
            else
                throw new ArgumentNullException("Invalid argument.");
        }
        #endregion
    }
}

下面是一个演示用的例子。在该例子中,创建了一个永久循环的线程,该线程每隔500毫秒修改一次界面显示。主要代码如下:

Thread t;
private void button1_Click(object sender, EventArgs e)
{
    if (t == null)
    {
        t = new Thread(multithread);
        t.Start();
        label4.Text = string.Format(
            "Thread state:\n{0}",
            t.ThreadState.ToString()
            );
    }
}

public void DoWork(string msg)
{
    this.label3.Text = string.Format("Invoke method: {0}", msg);
}

int count = 0;
void multithread()
{
    while (true)
    {
        InvokeHelper.Set(this.label1, "Text", string.Format("Set value: {0}", count));
        InvokeHelper.Set(this.label1, "Tag", count);
        string value = InvokeHelper.Get(this.label1, "Tag").ToString();
        InvokeHelper.Set(this.label2, "Text",
            string.Format("Get value: {0}", value));

        InvokeHelper.Invoke(this, "DoWork", value);

        Thread.Sleep(500);
        count++;
    }
}

详细代码请参阅文前给出的源代码。运行后效果正常,尽管线程t是无限循环的线程,但主界面并不受其阻塞,操作一切正常。



参考文献

[1] Sergiu Josan, Making Controls Thread-safely, May 2009

[2] vicoB, Extension of safeInvoke, July 2010

© 野比 2012

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页