「玩一玩」InvokeHelper,让跨线程访问/修改主界面控件不再麻烦

Conmajia 2012-08-05 01:06:57
加精
事实上,本文内容很简单且浅显,所以取消前戏,直接开始。。

源代码:在本文最后

这里是一张动画,演示在多线程(无限循环+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是无限循环的线程,但主界面并不受其阻塞,操作一切正常。


源代码:点击下载
...全文
11548 173 打赏 收藏 转发到动态 举报
写回复
用AI写文章
173 条回复
切换为时间正序
请发表友善的回复…
发表回复
ihambert 2013-09-11
  • 打赏
  • 举报
回复
引用 77 楼 nada123456789 的回复:
this.Control.CheckForIllegalCrossThreadCalls = false; 夸线程访问控件,这一句代码不是就完了么。。
这句在跨线程访问控件一般是不会出问题的,但是某些情况下是会出异常的,我知道一些情况下会出异常。也就是说不是线程安全的做法。
yueyangniao 2013-06-25
  • 打赏
  • 举报
回复
不懂 我是来水经验的
_小黑_ 2013-06-24
  • 打赏
  • 举报
回复
我去 楼主 太伟大了,我正好需呀,顶一个
橘色的喵 2013-06-24
  • 打赏
  • 举报
回复
mark一下,赞成使用新技术!
nikolay 2013-04-28
  • 打赏
  • 举报
回复
非常实用~~ 感谢!
machealx 2013-04-28
  • 打赏
  • 举报
回复
引用 81 楼 sp1234 的回复:
[Quote=引用 70 楼 的回复:] 学习到了,但是还是想问,多个线程同时使用一个控件,不会死锁吗 [/Quote] 你运行了这里的程序了吗?它死锁了吗? 自己动手测试才是硬道理。
已经测试。。两个控件同一个线程里同时运行不会死锁。。。哎,为什么我前段时间一直看的都不是这么优雅的代码。。
machealx 2013-04-28
  • 打赏
  • 举报
回复
引用 8 楼 sp1234 的回复:
当然,使用lamda是我的一个“坏毛病”。其实这里完全可以使用传统的匿名委托写法:
using System;
using System.Threading;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            ThreadPool.QueueUserWorkItem(h =>
            {
                for (var i = 0; i < 10000000; i++)
                {
                    label1.SafeCall(delegate()
                    {
                        label1.Text = i.ToString();
                    });
                    Thread.Sleep(100);
                }
            });
        }

    }
}
太牛B了。。之前一直用委托的。。我都差点要疯了。。现在有这么好的方法。太棒了!
本拉灯 2013-04-25
  • 打赏
  • 举报
回复
用这个吧, /// <summary> /// 获取或设置当前UI线程,设置以后无需调用委托 /// <para>1.在主窗体的构造方法内调用</para> /// <para>2.在 Program.cs上调用方法如下</para> /// <para>Form form1=new Form1();</para> /// <para>NetworkCore.SynchronizationContext = SynchronizationContext.Current;</para> /// <para>Application.Run(form1);</para> /// </summary> public static SynchronizationContext SynchronizationContext { get; set; } /// <summary> /// 将异步消息调度到一个同步上下文(线程池) /// </summary> /// <param name="callback">要调用的 System.Threading.SendOrPostCallback 委托</param> /// <param name="obj">传递给委托的对象</param> public static void Post(SendOrPostCallback callback, object obj) { if (SynchronizationContext != null) { SynchronizationContext.Post(callback, obj); } else { callback(obj); } }
Conmajia 2013-04-25
  • 打赏
  • 举报
回复
CSDN的各种大水逼,装尼玛!还不是只IT狗!
csdsuper 2013-01-17
  • 打赏
  • 举报
回复
没有什么,多了一个反射封装。
srhouyu 2013-01-16
  • 打赏
  • 举报
回复
引用 楼主 conmajia 的回复:
本帖最后由 conmajia 于 2012-08-05 01:30:52 编辑 事实上,本文内容很简单且浅显,所以取消前戏,直接开始。。 源代码:在本文最后 这里是一张动画,演示在多线程(无限循环+Thread.Sleep)情况下主界面操作不受影响。 多线程是一种提高程序运行效率和性能的常用技术。随着我们学习工作的……
楼主你的代码对我非常有用。我遇到了一个数据绑定时候的跨线程访问界面的问题。麻烦在于,数据绑定的过程是自动赋值的,我不知道该在在哪个部位插入InvokeHelper的代码。所以向你请教。帖子在这里: http://bbs.csdn.net/topics/390347959?page=1#post-393482654
Conmajia 2012-12-22
  • 打赏
  • 举报
回复
引用 156 楼 qiaohuyue 的回复:
没有必要那么麻烦,UI就那么几个 控件, 直接 controls 递归就 可以完成 invoke了
祝你成功。 。 ( ̄. ̄)
milo_w 2012-12-21
  • 打赏
  • 举报
回复
受益匪浅,MARK 一下,留着备用~~
ebigking 2012-12-21
  • 打赏
  • 举报
回复
谢了,很好用。
XBodhi. 2012-10-29
  • 打赏
  • 举报
回复
没有必要那么麻烦,UI就那么几个 控件, 直接 controls 递归就 可以完成 invoke了
stone_soup 2012-10-29
  • 打赏
  • 举报
回复
mark
白云任去留 2012-10-29
  • 打赏
  • 举报
回复
看看 !
小枪 2012-10-27
  • 打赏
  • 举报
回复
现在的C#程序员 都是手上有货的 鉴定技术我觉得都可以拿个人类库来鉴定
小枪 2012-10-27
  • 打赏
  • 举报
回复
最近也是在纠结这个 多线程下要动控件值 好麻烦 一些一大堆
sveqtuerfh 2012-10-27
  • 打赏
  • 举报
回复
还是用委托好啊。
加载更多回复(120)

111,077

社区成员

发帖
与我相关
我的任务
社区描述
.NET技术 C#
社区管理员
  • C#
  • AIGC Browser
  • by_封爱
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

让您成为最强悍的C#开发者

试试用AI创作助手写篇文章吧