Показаны сообщения с ярлыком Windows Forms. Показать все сообщения
Показаны сообщения с ярлыком Windows Forms. Показать все сообщения

вторник, 17 марта 2009 г.

Многопоточность в Windows Forms. Control.Invoke().

Человек даже немного поработав с Windows Forms наверняка сталкивался с замечательным исключением System.InvalidOperationException с таким описанием: {"Cross-thread operation not valid: Control 'textBox1' accessed from a thread other than the thread it was created on."} Этим сообщением среда выполнения недвусмысленно дает понять, что обращаться к элементам управления пользовательского интерфейса можно только из того потока, который создал этот элемент управления. Такое поведение обусловлено деталями реализации, в частности использованием однопоточной модели аппартаментов (Single-threaded Apartment, STA) и механизма обработки оконных сообщений. Вот простенький пример кода, который приводит к генерации такого исключения:

public partial class Form1 : Form

{

    public Form1()

    {

        InitializeComponent();

        //на форме расположен TextBox и Button.

        //В TextBox будет выводиться количество сработок таймера,

        //таймер запускается по нажатию на кнопку Button

        timer = new Timer(AsyncHandler);

    }

 

    private void button1_Click(object sender, EventArgs e)

    {

        timer.Change(1000, 1000);

    }

    private void AsyncHandler(object data)

    {

        tickCount++;

        textBox1.Text = tickCount.ToString();

    }

 

    private readonly System.Threading.Timer timer;

    private int tickCount;

}

Существует вполне простой и понятный способ решения этой проблемы, путем проверки свойства элемента управления InvokeRequired с последующим вызовом метода Invoke.
Вот самый простой и примитивный способ:

private void AsyncHandler(object data)

{

    tickCount++;

    Action<int> action = DoChangeTicks;

    if (InvokeRequired)

    {

        Invoke(action, tickCount);

    }

    else

    {

        action(tickCount);

    }

 

}

private void DoChangeTicks(int count)

{

    textBox1.Text = tickCount.ToString();

}

Способ простой, но не совсем удобный. В данном случае вспомогательный метод только увеличивает связность и усложняет понимает решаемой задачи. Если воспользоваться новшествами C#3.0 в виде лямбда-выражений, можно сделать несколько более удобную реализацию следующего вида:

private void AsyncHandler(object data)

{

    tickCount++;

    Action action = () => textBox1.Text = tickCount.ToString();

    if (InvokeRequired)

    {

        Invoke(action);

    }

    else

    {

        action();

    }

}

Уже лучше. Мы избавились от необязательного метода, который неразрывно связан с тем действием, которое выполняет метод AsyncHandler. Но, все же, еще есть над чем подумать.
Следующий вариант решения был честно подсмотрен в неплохой книжке: Bill Wagner "More Effective C#".

/// <summary>

/// Расширения облегчающие работу с элементами управления в многопоточной среде.

/// </summary>

public static class ControlExtentions

{

    /// <summary>

    /// Вызов делегата через control.Invoke, если это необходимо.

    /// </summary>

    /// <param name="control">Элемент управления</param>

    /// <param name="doit">Делегат с некоторым действием</param>

    public static void InvokeIfNeeded(this Control control, Action doit)

    {

        if (control.InvokeRequired)

            control.Invoke(doit);

        else

            doit();

    }

    /// <summary>

    /// Вызов делегата через control.Invoke, если это необходимо.

    /// </summary>

    /// <typeparam name="T">Тип параметра делегата</typeparam>

    /// <param name="control">Элемент управления</param>

    /// <param name="doit">Делегат с некоторым действием</param>

    /// <param name="arg">Аргумент делагата с действием</param>

    public static void InvokeIfNeeded<T>(this Control control, Action<T> doit, T arg)

    {

        if (control.InvokeRequired)

            control.Invoke(doit, arg);

        else

            doit(arg);

    }

}

С помощью этого вспомогательного класса, реализация метода, взаимодействующего с элементами управления из других потоков, будет следующей:

private void AsyncHandler(object data)

{

    tickCount++;

    this.InvokeIfNeeded(

        () => textBox1.Text = tickCount.ToString()

            );

}