现在的位置: 首页 > 综合 > 正文

如何使得控件不需要在操作UI时检查InvokeRequired

2012年02月08日 ⁄ 综合 ⁄ 共 13330字 ⁄ 字号 评论关闭

第二个版本的介绍

This is a new, extended, improved version of my original article. This was (and is) my first (and only until now) CodeProject article. It was well rated, but at the same time a lot of people offered corrections, alternatives and improvements. So I will use
all this new info to show you a better way of doing the same thing. Every improvement in my code was suggested by someone else, so this is sort of collective writing. I'm just compiling things that you can read in the forum. But since I don't always read the
forum, it makes sense to me to make some "text oriented maintenance". I will try to give everyone his own credit.

对于我那个原始的文章来说,这是一个全新的,扩展的,改进版本。这曾是(现在也是)我的第一个(至今仍是)CodeProject 的文章。 它的评分很高,但是同时许多人提出了修正,其他选择和改进。所以我会把这些新的信息通过重构一个一模一样的东西,这个更好的办法展现给你们。在我的代码中每一个改进,都是他人建议的,所以这只是依样画葫芦。我只是把这个论坛里面你能读到的那些东西编译了一下。但是因为我不能一直逛论坛,所以我萌生了做点文字记录。我会合理给每个人适合的积分~

下载包括2个solution , 一个VS2008下的,有4个project ( Desktop and Compact Framework.Net C# 3.5和2.0) , 另一个是VS2005下的4个project(Desktop and Compact Framework C#2.0,2种方法分别解决问题)

在文章最后你会发现我所显得正是微软的解决方法。我是有些人把这些代码贴到论坛上之后才知道这个方法的。可以读一下 (点击). 但是我仍然坚持自己的解决方法。下面的文字几乎和原文相同;第二个横线后才是新的文字。Enjoy

概述

正如你所知的那样,使用Windows.Forms 在多线程下访问UI非常丑陋。在下看来,为什么不能这么写?(当然,这样写是绝对有漏洞的):

this.text = "New Text";

Windows.Forms.Control 类应该可以在任何线程下操作。但是它没有做。我会展示几种解决拌饭,最终,将有我发现的最简单的方法。等着到最后发现宝物吧!有件事情值得知道:当你通过VS运行程序在线程上操作UI,它总是会抛出一个异常。同样的程序在标准EXE下不一定会这样。这也就是说开发环境比.NET Framework更严格。这是一件好事,开发的时候出现问题总比产品运行时随机出现问题好。这是我的第一篇文章而且英语不是我的母语,大家轻点拍砖!

“标准”模式

我不知道谁第一个这么写的,但是这已经是在多线程模式下的标准处理方法:

public delegate void DelegateStandardPattern();
private void SetTextStandardPattern()
{
    if (this.InvokeRequired)
    {
        this.Invoke(new DelegateStandardPattern(SetTextStandardPattern));
        return;
    }
    this.text = "New Text";
}

优点:

  • 的确有效
  • 在C# 1.0,2.0,3.0,3.5,标准和压缩版.net( CF1.0没有InvokeRequired)
  • 每个人都这样用,所以你看到这个就知道这有可能从其他线程调用这个方法。

坏处:

  • 为了更新一个text就花费那么多代码
  • 你需要copy/paste,还不能用泛型解决。
  • 如果你需要有参数的方法,还不能复用这个delegate,你必须另外声明新的delegate.
  • 丑陋,我知道这很主观,但是就是这样,我特别讨厌需要在方法外部声明delegate.

这里有一些聪明的解决办法,比如 这个是用了AOP(动态代理), and 这个使用了反射
. 但是我希望更简单的实现。一个方法

There are some clever solutions out there, like
this one using AOP
, and
this one using Reflection
. But I wanted something easier to implement. One way to go could be aSurroundWith code snippet, but I like my code issues to be solved by the language, not by the IDE. Also, it will only solve the copy/paste problem,
it will still be a lot of code for something really simple.

Why can't we generalize the standard pattern? Because there is no way in .NET 1.0 to pass a block of code as a parameter, because when C# started it had almost no support for a functional programming style.

“匿名委托” 模式

随着 C# 2.0 得到了, 我们可以把标准模式通过匿名函数和MethodInvoker类简化成这样:

private void SetTextAnonymousDelegatePattern()
{
    if (this.InvokeRequired)
    {
        MethodInvoker del = delegate { SetTextAnonymousDelegatePattern(); };
        this.Invoke(del);
        return;
    }
    this.text = "New Text";
}

这明显是一个更好的解决方案,我还从没看过有人用。但是如果执行 this.text = "New Text" 会发生什么?你需要调用一个有参数的方法?就好像:

private void MultiParams(string text, int number, DateTime dateTime);

这不是大问题,因为delegates可以访问外部变量。所以,你可以这么写:

private void SetTextDelegatePatternParams(string text, int number, DateTime datetime)
{
    if (this.InvokeRequired)
    {
        MethodInvoker del = delegate { 
		SetTextDelegatePatternParams(text, number, datetime); };
        this.Invoke(del);
        return;
    }
    MultiParams(text, number, datetime);
}

这个匿名delegate模式可以缩小许多,让你忘记需要invoke,那就是:

最小匿名函数模式

这个很棒:

//No parameters
private void SetTextAnonymousDelegateMiniPattern()
{
    Invoke(new MethodInvoker(delegate
    {
    	this.text = "New Text";
    }));
}
//With parameters
private void SetTextAnonymousDelegateMiniPatternParams
		(string text, int number, DateTime dateTime)
{
    Invoke(new MethodInvoker(delegate
    {
    	MultiParams(text, number, dateTime);
    }));
}

It works, it's easy to write, it's only a few lines away from perfect. The first time I saw this, I thought that's what I was looking for. So what's the problem? Well, we forgot to ask if Invoke was required. And since this is not the standard way to do
it, it will not be clear to others (or to ourselves in a couple of months) why we are doing this. We could be nice and comment the code, but let's be honest, we all know we won't. At least I prefer my code to be more "intention revealing". So, we have...

它有效,容易书写,只需要几行很完美。我第一次看到它就认为这就是我寻找的方法。那么这有什么问题?好吧,我们忘了问是否需要invoke。既然这不是标准方法,其他人就不怎么容易读懂为什么我们这么做。我们可以写漂亮的代码和注释,但是我们必须诚实,我们都知道这不是。至少我希望代码更加

“UIThread”模式,即我已经解决的问题

First I show you the rabbit:

//No parameters
private void SetTextUsingPattern()
{
    this.UIThread(delegate
    {
    	this.text = "New Text";
    });
}
//With parameters
private void SetTextUsingPatternParams(string text, int number, DateTime dateTime)
{
    this.UIThread(delegate
    {
    	MultiParams(text, number, dateTime);
    });
}

And now I'll show you the trick. It's a simple static class with only one method. It's an extension method, of course, so if you have some objections like "extension methods are not pure object orientated programming" I recommend you to use
Smalltalk and stop complaining. Or use a standard helper class, as you wish. Without comments, namespace and using, the class looks like this:

static class FormExtensions
{
    static public void UIThread(this Form form, MethodInvoker code)
    {
        if (form.InvokeRequired)
        {
            form.Invoke(code);
            return;
        }
        code.Invoke();
    }
}

That was how far I've gone by myself. But then I got the following suggestions from the developers in the forum:

  • was333 said: Why justForm? Why not
    Control? He was right. There's even a more abstract interface (ISynchronizeInvoke) thatRob Smiley suggested, but I feel it is way too strange, and is not present in Compact
    Framework
  • Borlip pointed that
    MethodInvoker
    isn't present in CompactFramework but Action is, so it's more portable to useAction
  • tzach shabtay has linked tothis article pointing that it's better to useBeginInvoke than
    Invoke when posible. Sometimes that could be a problem, so we need two versions. But you should preferBeginInvoke.

So this is, until now, the final version

static class ControlExtensions
{
    static public void UIThread(this Control control, Action code)
    {
        if (control.InvokeRequired)
        {
            control.BeginInvoke(code);
            return;
        }
        code.Invoke();
    }
	
    static public void UIThreadInvoke(this Control control, Action code)
    {
        if (control.InvokeRequired)
        {
            control.Invoke(code);
            return;
        }
        code.Invoke();
    }
}

You can use it this way

this.UIThread(delegate
{
   textBoxOut.Text = "UIThread pattern was used";
});

As you can see, is just the standard pattern, as generalized as possible. Good points about this solution:

  • It does the job
  • It works the same with Full and Compact Framework
  • It's simple (almost looks like a using{} block!)
  • It doesn't care if you have parameters or not
  • If you read it again in three months, it will still look clear
  • It uses a lot of what modern .NET has to offer: Anonymous delegates, extension methods, lambda expressions (if you want, see later)

Bad points:

  • Er ... waiting for your comments. Again.

Points of Interest

You can write even less code using lambda style, if you only need to write one line you can do something as small as

private void SetTextUsingPatternParams(string text, int number, DateTime dateTime)
{
    this.UIThread(()=> MultiParams(text, number, dateTime));
}

and still be clear! If you need to read from the Form, you need to useUIThreadInvoke, or you will find starge results.

private void Read()
{
     string textReaded;
     this.UIThreadInvoke(delegate
     {
        textReaded = this.Text;
     });
}

But I'm pretty sure that if you are reading the screen from another thread, you are making a mistake somewhere.

For C# 2.0 and Visual Studio 2008

This code needs .NET Framework 3.5 to work. It works out of the box with both Desktop and Compact Framework. You have a working sample for both in the downloadable code. Some people asked about a .NET 2.0 version. There are two things from .NET 3.5 that
we miss in 2.0:

1 - Action class: We have Action<T>, but there is no simple-without-parameter-typeAction, because
Action is in System.Core.dll. That's easy, we just create the delegate insideSystem namespace

namespace System
{
    public delegate void Action();
}

2 - Extension Methods: Thanks to
Kwan Fu Sit
who pointed to
this article
, there is a clever way to do that if you can use Visual Studio 2008. Since Extension methods are just a compiler trick, the only thing you need to add to you project is a new class

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Method|AttributeTargets.Class|AttributeTargets.Assembly)]
    public sealed class ExtensionAttribute : Attribute
    {

    }
}

and that's it! It's very usefull, not only for this UIThread trick. I've added bothExtensionAttribute and
Action in the same file CSharp35Extras.cs. Check the details in the respective projects of the same solution. Once again, the exact same code works in both Desktop and Compact framework.

For C# 2.0 and Visual Studio 2005

I've found basically three ways to make it work in VS2005 and none of them are very elegant. In all of them I useMethodInvoker instead of
Action because MethodInvoker is present in the desktop .NET Framework 2.0. You still need to declare theMethodInvoker class somewhere if you work in Compact Framework. For simple one-window-project, (or one-window-with-multithreading-issues,
to be precise), just copy a method inside theFormWathever.cs

private void UIThread(MethodInvoker code)
{
    if (this.InvokeRequired)
    {
        this.BeginInvoke(code);
        return;
    }
    this.Invoke();
}

You can use it like this

UIThread(delegate
{
   textBoxOut.Text = "UIThread pattern was used";
});

I think this is a good enough solution for simple projects. But when you have the same problem in a second window and you start copy/pasting the method, it's not so good.

Another option is to create a helper class like this:

static public class UIHelper
{
    static public void UIThread(Control control, MethodInvoker code)
    {
        if (control.InvokeRequired)
        {
            control.BeginInvoke(code);
            return;
        }
        control.Invoke();
    }	
}

And then invoke the UIThread like this:

UIHelper.UIThread(this, delegate
{
   textBoxOut.Text = "New text";
});

I have no problem having a UIHelper class, I always end up using aUIHelper class for one reason or another, but I don't like the
UIHelper.UIThread(this,... part. It's too verbose to me. But it works, and at least you are not copy/pasting code.

Another way is to create a FormBase class like this

public class FormBase : Form
{
   public void UIThread(MethodInvoker code)
   {
       if (this.InvokeRequired)
       {
           this.BeginInvoke(code);
           return;
       }
       code.Invoke();
   }
}

then inherit all your forms from FormBase, and then invoke like this

UIThread(delegate
{
   textBoxOut.Text = "New text";
});

The invoking part is fine, but I don't enjoy inheriting all my forms from
FormBase
, specially because sometimes, when I am using visual inheritance, and I switch to design mode, VisualStudio shows me really horrible screens like this one

(Regarding this problem, the only solution I know when it happens is to close all Design tabs, then Build-Clear Solution, then close Visual Studio, then delete all files under bin and obj folders, reopen Visual Studio and Rebuild Solution and then reopen
the FormWhatever in design view)

You are also loosing the Control generalization; this way only works forForms. Is up to you to choose one of these partial solutions, or migrate to VS2008, or to put pressure in your boss to migrate to VS2008. Came on! VS2010 is
just around the corner.

For C# 1.0 and Visual Studio 2003

Are you kidding me? (I mean, I don't have a solution for that environment, and I don't think it's possible.)

Alternatives

When I wrote the first version of this article, I was aware of some alternatives to avoid copy/pasting code. I didn't like any of them, that was my motivation, but I listed that alternatives at the beggining of the article in order to show them to everyone.
However, there was another alternative that I didn't think about:
Alomgir Miah A
suggested to use BackgroundWorker. I think it's too verbose, and it doesn't exist in Compact Framework. But sure, it exists in the full framework, and it can be used to avoid threading issues.

Two people suggested to use Control.CheckForIllegalCrossThreadCalls = false;.DON'T DO THAT! That is terribly wrong! From the MSDN documentation:illegal cross-thread calls will always raise an exception when an application
is started outside the debugger.
. SettingControl.CheckForIllegalCrossThreadCalls = false; will only disable the debugger capability to detect all posible threading issues. So, you may not detect them when debbuging, but when your app is running
you may have horrible exceptions killing your app and have never been able to reproduce them. You are choosing to close your eyes when crossing the street. It's easy to do, but risky. Again,DON'T DO THAT!. Use whatever solution works for you,
never ever writeControl.CheckForIllegalCrossThreadCalls = false;.

The "Official" Pattern

Finally, two other people (Islam ElDemery and Member 170334, who has no name and no friendly URL) showed me what I think is the official solution that Microsoft has developed for this problem,
and so it's probably better than mine: The SynchronizationContext class. I have to admit I didn't know about that, and it's been available since .NET Framework 2.0! It can be used very much like my own solution, and it's probably faster, since
it's included in the framework, and it offers you more options. I am adult enough to show this solution here, even when it makes my own work pretty useless, and I am kid enough to reject it later. It's a two step solution: First, you need aSynchronizationContext
member that must be initialized inside the constructor:

class FormWathever
{
    private SynchronizationContext synchronizationContext ;
	
	public FormWathever()
	{
	    this.synchronizationContext  = SynchronizationContext.Current;
		//the rest of your code
	}
}

and then, when you need to do some thread-unsafe form actualizations you should use something like this:

synchronizationContext.Send(new SendOrPostCallback( 
    delegate(object state) 
    {    
        textBoxOut.Text = "New text";
    } 
), null);

It works, it's incorporated into the framework, I'm sure it's fast, and has some extra options. Why not to use it? I only can think in four reasons, and not very good ones. They look more like excuses.

  1. I don't like the initialization part. I don't understand why Microsoft didn't include aSynchronizationContext property inside the
    Form class, automatically initializated in the base constructor. In order to skip initialization by yourself, you need to inherit all your forms from a FormBase or something like this.
  2. It's kind of verbose. You need to create that SendOrPostCallback object, and pass that extra null parameter, and the extraobject state. You could avoid this extra work by using another helper method, but in this case I'll stick
    to UIThread
  3. It's not "intention revealing code". And since it's not very popular, it makes your code harder to understand and mantain by others. (But not too much, let's be honest.)
  4. It doesn't exist in Compact Framework.

But if you don't need to care about Compact Framework, and you think that some extra typing will not kill you, that's probably the way to go. 

抱歉!评论已关闭.