如何使得控件不需要在操作UI時檢查InvokeRequired

轉載自:http://blog.csdn.net/norsd/article/details/7710075

查找相關問題時搜到,沒太多時間看,保存一下。感謝譯者

正文

第二個版本的介紹

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. 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章