Как мне обновить GUI из другого потока?

1222

Каков самый простой способ обновить Label из другого потока?

У меня есть Form на thread1, и из этого я запускаю другой поток (thread2). Хотя thread2 обрабатывает некоторые файлы, я хотел бы обновить Label в Form с текущим статусом работы thread2.

Как я могу это сделать?

  • 24
    Разве .net 2.0+ не имеет класса BackgroundWorker только для этого. Это пользовательский интерфейс потока в курсе. 1. Создайте BackgroundWorker 2. Добавьте два делегата (один для обработки, а другой для завершения)
  • 12
    может быть немного поздно: codeproject.com/KB/cs/Threadsafe_formupdating.aspx
Показать ещё 3 комментария
Теги:
multithreading
winforms
user-interface

46 ответов

683
Лучший ответ

Для .NET 2.0 здесь хороший код кода, который я написал, который делает именно то, что вы хотите, и работает для любого свойства на Control:

private delegate void SetControlPropertyThreadSafeDelegate(
    Control control, 
    string propertyName, 
    object propertyValue);

public static void SetControlPropertyThreadSafe(
    Control control, 
    string propertyName, 
    object propertyValue)
{
  if (control.InvokeRequired)
  {
    control.Invoke(new SetControlPropertyThreadSafeDelegate               
    (SetControlPropertyThreadSafe), 
    new object[] { control, propertyName, propertyValue });
  }
  else
  {
    control.GetType().InvokeMember(
        propertyName, 
        BindingFlags.SetProperty, 
        null, 
        control, 
        new object[] { propertyValue });
  }
}

Назовите его следующим образом:

// thread-safe equivalent of
// myLabel.Text = status;
SetControlPropertyThreadSafe(myLabel, "Text", status);

Если вы используете .NET 3.0 или выше, вы можете переписать вышеупомянутый метод как метод расширения класса Control, который затем упростит вызов:

myLabel.SetPropertyThreadSafe("Text", status);

ОБНОВЛЕНИЕ 05/10/2010:

Для .NET 3.0 вы должны использовать этот код:

private delegate void SetPropertyThreadSafeDelegate<TResult>(
    Control @this, 
    Expression<Func<TResult>> property, 
    TResult value);

public static void SetPropertyThreadSafe<TResult>(
    this Control @this, 
    Expression<Func<TResult>> property, 
    TResult value)
{
  var propertyInfo = (property.Body as MemberExpression).Member 
      as PropertyInfo;

  if (propertyInfo == null ||
      [email protected]().IsSubclassOf(propertyInfo.ReflectedType) ||
      @this.GetType().GetProperty(
          propertyInfo.Name, 
          propertyInfo.PropertyType) == null)
  {
    throw new ArgumentException("The lambda expression 'property' must reference a valid property on this Control.");
  }

  if (@this.InvokeRequired)
  {
      @this.Invoke(new SetPropertyThreadSafeDelegate<TResult> 
      (SetPropertyThreadSafe), 
      new object[] { @this, property, value });
  }
  else
  {
      @this.GetType().InvokeMember(
          propertyInfo.Name, 
          BindingFlags.SetProperty, 
          null, 
          @this, 
          new object[] { value });
  }
}

который использует выражения LINQ и лямбда, чтобы обеспечить гораздо более чистый, простой и безопасный синтаксис:

myLabel.SetPropertyThreadSafe(() => myLabel.Text, status); // status has to be a string or this will fail to compile

Не только имя свойства теперь проверяется во время компиляции, но и тип свойства, поэтому невозможно (например) присвоить значение строки логическому свойству и, следовательно, вызвать исключение во время выполнения.

К сожалению, это не мешает кому-либо делать глупые вещи, такие как передача в другое свойство и значение Control, поэтому следующее будет с удовольствием скомпилировать:

myLabel.SetPropertyThreadSafe(() => aForm.ShowIcon, false);

Следовательно, я добавил проверки времени выполнения, чтобы убедиться, что свойство pass-in действительно принадлежит Control, что вызываемый метод. Не идеально, но все же намного лучше, чем версия .NET 2.0.

Если у кого-то есть дополнительные предложения по улучшению этого кода для безопасности во время компиляции, прокомментируйте!

  • 3
    Существуют случаи, когда this.GetType () оценивается так же, как propertyInfo.ReflectedType (например, LinkLabel в WinForms). У меня нет большого опыта в C #, но я думаю, что условие для исключения должно быть: if (propertyInfo == null || ([email protected] (). IsSubclassOf (propertyInfo.ReflectedType) && @ this.GetType ( )! = propertyInfo.ReflectedType) || @ this.GetType (). GetProperty (propertyInfo.Name, propertyInfo.PropertyType) == null)
  • 8
    @lan может вызывать этот SetControlPropertyThreadSafe(myLabel, "Text", status) из другого модуля, класса или формы
Показать ещё 7 комментариев
1026

Простейший способ - анонимный метод, переданный в Label.Invoke:

// Running on the worker thread
string newText = "abc";
form.Label.Invoke((MethodInvoker)delegate {
    // Running on the UI thread
    form.Label.Text = newText;
});
// Back on the worker thread

Обратите внимание, что Invoke блокирует выполнение до его завершения - это синхронный код. Вопрос не спрашивает об асинхронном коде, но есть много контента в Stack Overflow о написании асинхронного кода, когда вы хотите узнать об этом.

  • 1
    Но тогда ваша функция обработки должна быть методом-членом формы GUI?
  • 5
    Поскольку OP не упомянул ни один класс / экземпляр, кроме формы, это неплохое значение по умолчанию ...
Показать ещё 13 комментариев
339

Обработка длительной работы

Поскольку .NET 4.5 и С# 5.0, вы должны использовать Асинхронный шаблон на основе задач (TAP) вместе с async - ожидание ключевые слова во всех областях (включая графический интерфейс):

TAP - рекомендуемый асинхронный шаблон проектирования для новой разработки

вместо Модель асинхронного программирования (APM) и Асинхронный на основе событий Шаблон (EAP) (последний включает BackgroundWorker Class).

Тогда рекомендуемое решение для новой разработки:

  • Асинхронная реализация обработчика событий (да, все):

    private async void Button_Clicked(object sender, EventArgs e)
    {
        var progress = new Progress<string>(s => label.Text = s);
        await Task.Factory.StartNew(() => SecondThreadConcern.LongWork(progress),
                                    TaskCreationOptions.LongRunning);
        label.Text = "completed";
    }
    
  • Реализация второго потока, который уведомляет поток пользовательского интерфейса:

    class SecondThreadConcern
    {
        public static void LongWork(IProgress<string> progress)
        {
            // Perform a long running work...
            for (var i = 0; i < 10; i++)
            {
                Task.Delay(500).Wait();
                progress.Report(i.ToString());
            }
        }
    }
    

Обратите внимание на следующее:

  • Короткий и чистый код, написанный последовательным образом без обратных вызовов и явных потоков.
  • Task вместо Thread.
  • async ключевое слово, которое позволяет использовать ждать, который в свою очередь, не позволяет обработчику события достичь состояния завершения до завершения задачи и тем временем не блокирует поток пользовательского интерфейса.
  • Класс выполнения (см. IProgress Interface), который поддерживает Разделение проблем ( SoC) и не требует явного диспетчера и вызова. Он использует текущий SynchronizationContext из своего места создания (здесь поток пользовательского интерфейса).
  • TaskCreationOptions.LongRunning, который намекает, чтобы не ставить задачу в ThreadPool.

Для более подробных примеров см.: Будущее С#: хорошие вещи приходят тем, кто "ждет" Джозеф Альбахари.

См. также о Концепции потоковой модели UI.

Обработка исключений

Ниже приведен фрагмент примера обработки исключений и свойства кнопки Enabled для предотвращения множественных щелчков во время выполнения фона.

private async void Button_Click(object sender, EventArgs e)
{
    button.Enabled = false;

    try
    {
        var progress = new Progress<string>(s => button.Text = s);
        await Task.Run(() => SecondThreadConcern.FailingWork(progress));
        button.Text = "Completed";
    }
    catch(Exception exception)
    {
        button.Text = "Failed: " + exception.Message;
    }

    button.Enabled = true;
}

class SecondThreadConcern
{
    public static void FailingWork(IProgress<string> progress)
    {
        progress.Report("I will fail in...");
        Task.Delay(500).Wait();

        for (var i = 0; i < 3; i++)
        {
            progress.Report((3 - i).ToString());
            Task.Delay(500).Wait();
        }

        throw new Exception("Oops...");
    }
}
  • 1
    Если SecondThreadConcern.LongWork() генерирует исключение, может ли оно быть SecondThreadConcern.LongWork() потоком пользовательского интерфейса? Это отличный пост, кстати.
  • 1
    Я добавил дополнительный раздел к ответу, чтобы выполнить ваши требования. С уважением.
Показать ещё 10 комментариев
200

Изменение Marc Gravell простейшее решение для .NET 4:

control.Invoke((MethodInvoker) (() => control.Text = "new text"));

Или используйте вместо него делегат действий:

control.Invoke(new Action(() => control.Text = "new text"));

См. здесь для сравнения двух: MethodInvoker против Action для Control.BeginInvoke

  • 1
    что такое «контроль» в этом примере? Мой интерфейс управления? Попытка реализовать это в WPF на элементе управления меткой, и Invoke не является членом моей метки.
  • 0
    Как насчет метода расширения, такого как @styxriver stackoverflow.com/a/3588137/206730 ?
Показать ещё 3 комментария
123

Пожар и забыть метод расширения для .NET 3.5 +

using System;
using System.Windows.Forms;

public static class ControlExtensions
{
    /// <summary>
    /// Executes the Action asynchronously on the UI thread, does not block execution on the calling thread.
    /// </summary>
    /// <param name="control"></param>
    /// <param name="code"></param>
    public static void UIThread(this Control @this, Action code)
    {
        if (@this.InvokeRequired)
        {
            @this.BeginInvoke(code);
        }
        else
        {
            code.Invoke();
        }
    }
}

Это можно вызвать, используя следующую строку кода:

this.UIThread(() => this.myLabel.Text = "Text Goes Here");
  • 3
    Какой смысл использовать @this? Разве «контроль» не будет эквивалентен? Есть ли какие-то преимущества у @this?
  • 13
    @jeromeyers - @this - это просто имя переменной, в данном случае ссылка на текущий элемент управления, вызывающий расширение. Вы можете переименовать его в источник, или что-то еще, что плавает на вашей лодке Я использую @this , потому что он ссылается на «этот @this управления», который вызывает расширение и согласуется (по крайней мере, в моей голове) с использованием ключевого слова «это» в обычном (не являющемся расширением) коде.
Показать ещё 4 комментария
63

Это классический способ сделать это:

using System;
using System.Windows.Forms;
using System.Threading;

namespace Test
{
    public partial class UIThread : Form
    {
        Worker worker;

        Thread workerThread;

        public UIThread()
        {
            InitializeComponent();

            worker = new Worker();
            worker.ProgressChanged += new EventHandler<ProgressChangedArgs>(OnWorkerProgressChanged);
            workerThread = new Thread(new ThreadStart(worker.StartWork));
            workerThread.Start();
        }

        private void OnWorkerProgressChanged(object sender, ProgressChangedArgs e)
        {
            // Cross thread - so you don't get the cross-threading exception
            if (this.InvokeRequired)
            {
                this.BeginInvoke((MethodInvoker)delegate
                {
                    OnWorkerProgressChanged(sender, e);
                });
                return;
            }

            // Change control
            this.label1.Text = e.Progress;
        }
    }

    public class Worker
    {
        public event EventHandler<ProgressChangedArgs> ProgressChanged;

        protected void OnProgressChanged(ProgressChangedArgs e)
        {
            if(ProgressChanged!=null)
            {
                ProgressChanged(this,e);
            }
        }

        public void StartWork()
        {
            Thread.Sleep(100);
            OnProgressChanged(new ProgressChangedArgs("Progress Changed"));
            Thread.Sleep(100);
        }
    }


    public class ProgressChangedArgs : EventArgs
    {
        public string Progress {get;private set;}
        public ProgressChangedArgs(string progress)
        {
            Progress = progress;
        }
    }
}

В вашей рабочей ветке есть событие. Ваш поток пользовательского интерфейса запускает другой поток для выполнения этой работы и подключает это рабочее событие, чтобы вы могли отображать состояние рабочего потока.

Затем в пользовательском интерфейсе вам нужно пересечь потоки, чтобы изменить фактический элемент управления... как метку или индикатор выполнения.

54

Простым решением является использование Control.Invoke.

void DoSomething()
{
    if (InvokeRequired) {
        Invoke(new MethodInvoker(updateGUI));
    } else {
        // Do Something
        updateGUI();
    }
}

void updateGUI() {
    // update gui here
}
  • 0
    Это не идеально. при обновлении какого-либо элемента управления другой элемент управления не отвечает.
  • 0
    хорошо сделано для простоты! не только просто, но и хорошо работает! Я действительно не понимал, почему Microsoft не может сделать это проще, чем должно быть! для вызова 1 строки в главном потоке мы должны написать пару функций!
Показать ещё 2 комментария
44

Код Threading часто глючит и всегда трудно проверить. Вам не нужно писать код потока для обновления пользовательского интерфейса из фоновой задачи. Просто используйте класс BackgroundWorker для запуска задачи и ReportProgress для обновления пользовательского интерфейса. Обычно вы просто сообщаете о проценте в полном объеме, но там есть другая перегрузка, которая включает объект состояния. Вот пример, который просто сообщает строковый объект:

    private void button1_Click(object sender, EventArgs e)
    {
        backgroundWorker1.WorkerReportsProgress = true;
        backgroundWorker1.RunWorkerAsync();
    }

    private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
    {
        Thread.Sleep(5000);
        backgroundWorker1.ReportProgress(0, "A");
        Thread.Sleep(5000);
        backgroundWorker1.ReportProgress(0, "B");
        Thread.Sleep(5000);
        backgroundWorker1.ReportProgress(0, "C");
    }

    private void backgroundWorker1_ProgressChanged(
        object sender, 
        ProgressChangedEventArgs e)
    {
        label1.Text = e.UserState.ToString();
    }

Это прекрасно, если вы всегда хотите обновить одно и то же поле. Если у вас есть более сложные обновления, вы можете определить класс для представления состояния пользовательского интерфейса и передать его методу ReportProgress.

Наконец, обязательно установите флаг WorkerReportsProgress, или метод ReportProgress будет полностью проигнорирован.

  • 1
    В конце обработки также возможно обновить пользовательский интерфейс через backgroundWorker1_RunWorkerCompleted .
35

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

string newText = "abc"; // running on worker thread
this.Invoke((MethodInvoker)delegate { 
    someLabel.Text = newText; // runs on UI thread
});

Если пользователь закрывает форму перед вызовом this.Invoke (помните, this является объектом Form), вероятно, будет запущен ObjectDisposedException.

Решение заключается в использовании SynchronizationContext, особенно SynchronizationContext.Current, поскольку hamilton.danielb предлагает (другие ответы полагаются на конкретные реализации SynchronizationContext, которые совершенно не нужны), Я бы немного изменил его код, чтобы использовать SynchronizationContext.Post, а не SynchronizationContext.Send хотя (как обычно нет необходимости ждать рабочий поток):

public partial class MyForm : Form
{
    private readonly SynchronizationContext _context;
    public MyForm()
    {
        _context = SynchronizationContext.Current
        ...
    }

    private MethodOnOtherThread()
    {
         ...
         _context.Post(status => someLabel.Text = newText,null);
    }
}

Обратите внимание, что на .NET 4.0 и выше вы действительно должны использовать задачи для асинхронных операций. См. n-san для эквивалентного подхода на основе задач (используя TaskScheduler.FromCurrentSynchronizationContext).

Наконец, на .NET 4.5 и выше вы также можете использовать Progress<T> (который в основном захватывает SynchronizationContext.Current при его создании), как показано Ryszard Dżegan для случаев где длительная работа должна запускать код пользовательского интерфейса при работе.

33

Вам нужно убедиться, что обновление происходит в правильном потоке; поток пользовательского интерфейса.

Чтобы сделать это, вам придется вызывать обработчик событий вместо прямого вызова.

Вы можете сделать это, создав свое событие следующим образом:

(Код напечатан здесь из моей головы, поэтому я не проверял правильность синтаксиса и т.д., но он должен вас заставить.)

if( MyEvent != null )
{
   Delegate[] eventHandlers = MyEvent.GetInvocationList();

   foreach( Delegate d in eventHandlers )
   {
      // Check whether the target of the delegate implements 
      // ISynchronizeInvoke (Winforms controls do), and see
      // if a context-switch is required.
      ISynchronizeInvoke target = d.Target as ISynchronizeInvoke;

      if( target != null && target.InvokeRequired )
      {
         target.Invoke (d, ... );
      }
      else
      {
          d.DynamicInvoke ( ... );
      }
   }
}

Обратите внимание, что приведенный выше код не будет работать в проектах WPF, поскольку элементы управления WPF не реализуют интерфейс ISynchronizeInvoke.

Чтобы убедиться, что приведенный выше код работает с Windows Forms и WPF и всеми другими платформами, вы можете посмотреть классы AsyncOperation, AsyncOperationManager и SynchronizationContext.

Чтобы легко поднять события таким образом, я создал метод расширения, который позволяет мне упростить создание события, просто позвонив:

MyEvent.Raise(this, EventArgs.Empty);

Конечно, вы также можете использовать класс BackGroundWorker, который абстрагирует этот вопрос для вас.

  • 0
    Действительно, но я не люблю «загромождать» мой код GUI этим вопросом. Мой GUI не должен заботиться о том, нужно ли ему вызывать или нет. Другими словами: я не думаю, что именно GUI отвечает за выполнение context-swithc.
  • 1
    Разбить делегата на части и т. Д. Кажется излишним - почему бы не просто: SynchronizationContext.Current.Send (делегат {MyEvent (...);}, ноль);
Показать ещё 1 комментарий
27

Вам нужно вызвать метод в потоке графического интерфейса. Вы можете сделать это, вызвав Control.Invoke.

Например:

delegate void UpdateLabelDelegate (string message);

void UpdateLabel (string message)
{
    if (InvokeRequired)
    {
         Invoke (new UpdateLabelDelegate (UpdateLabel), message);
         return;
    }

    MyLabelControl.Text = message;
}
  • 1
    Строка invoke дает мне ошибку компилятора. Наилучшее совпадение перегруженного метода для 'System.Windows.Forms.Control.Invoke (System.Delegate, object [])' имеет недопустимые аргументы
26

Из-за тривиальности сценария у меня был бы фактический опрос пользовательского интерфейса для статуса. Я думаю, вы обнаружите, что он может быть довольно элегантным.

public class MyForm : Form
{
  private volatile string m_Text = "";
  private System.Timers.Timer m_Timer;

  private MyForm()
  {
    m_Timer = new System.Timers.Timer();
    m_Timer.SynchronizingObject = this;
    m_Timer.Interval = 1000;
    m_Timer.Elapsed += (s, a) => { MyProgressLabel.Text = m_Text; };
    m_Timer.Start();
    var thread = new Thread(WorkerThread);
    thread.Start();
  }

  private void WorkerThread()
  {
    while (...)
    {
      // Periodically publish progress information.
      m_Text = "Still working...";
    }
  }
}

Этот подход позволяет избежать операции маршалинга, требуемой при использовании методов ISynchronizeInvoke.Invoke и ISynchronizeInvoke.BeginInvoke. Нет ничего плохого в использовании техники маршалинга, но есть несколько предостережений, о которых вам нужно знать.

  • Убедитесь, что вы не вызываете BeginInvoke слишком часто, или он может переполнять насос сообщений.
  • Вызов Invoke в рабочем потоке - это блокирующий вызов. Он временно остановит работу, выполняемую в этом потоке.

Стратегия, которую я предлагаю в этом ответе, отменяет роли связи потоков. Вместо того, чтобы рабочий поток толкал данные, опрос пользовательского интерфейса. Это общий шаблон, используемый во многих сценариях. Поскольку все, что вы хотите сделать, это отобразить информацию о ходе работы из рабочего потока, тогда я думаю, вы обнаружите, что это решение является отличной альтернативой решению маршалинга. Он имеет следующие преимущества.

  • Пользовательский интерфейс и рабочие потоки остаются слабо связанными, в отличие от подхода Control.Invoke или Control.BeginInvoke, который плотно соединяет их.
  • Нить пользовательского интерфейса не будет препятствовать прогрессу рабочего потока.
  • Рабочий поток не может доминировать во время обновления потока пользовательского интерфейса.
  • Интервалы, на которых выполняются операции пользовательского интерфейса и рабочего потока, могут оставаться независимыми.
  • Рабочий поток не может переполнять насос сообщений потока пользовательского интерфейса.
  • В потоке пользовательского интерфейса определяется, когда и как часто обновляется пользовательский интерфейс.
  • 2
    Отличная идея. Единственное, что вы не упомянули, это то, как вы правильно располагаете таймер после завершения WorkerThread. Обратите внимание, что это может вызвать проблемы, когда приложение заканчивается (т.е. пользователь закрывает приложение). У вас есть идея, как это решить?
  • 0
    @Matt Вместо использования анонимного обработчика для события Elapsed , вы используете метод member, чтобы вы могли удалить таймер, когда форма удаляется ...
Показать ещё 1 комментарий
24

Не требуется никаких элементов Invoke в предыдущих ответах.

Вам нужно посмотреть на WindowsFormsSynchronizationContext:

// In the main thread
WindowsFormsSynchronizationContext mUiContext = new WindowsFormsSynchronizationContext();

...

// In some non-UI Thread

// Causes an update in the GUI thread.
mUiContext.Post(UpdateGUI, userData);

...

void UpdateGUI(object userData)
{
    // Update your GUI controls here
}
  • 3
    Как вы думаете, что метод Post использует под капотом? :)
22

Salvete! Искав этот вопрос, я нашел ответы FrankG и Oregon Ghost самыми легкими для меня самыми полезными. Теперь я кодирую Visual Basic и запускаю этот фрагмент через конвертер; поэтому я не совсем уверен, как это получается.

У меня есть диалоговая форма с именем form_Diagnostics,, у которой есть поле richtext, называемое updateDiagWindow,, которое я использую в качестве своего рода журнала. Мне нужно было обновить его текст из всех потоков. Дополнительные строки позволяют окну автоматически прокручиваться до новейших строк.

Итак, теперь я могу обновить отображение одной строкой из любой точки всей программы так, как вы думаете, она будет работать без потоковой передачи:

  form_Diagnostics.updateDiagWindow(whatmessage);

Основной код (поместите это внутри вашего кода класса формы):

#region "---------Update Diag Window Text------------------------------------"
// This sub allows the diag window to be updated by all threads
public void updateDiagWindow(string whatmessage)
{
    var _with1 = diagwindow;
    if (_with1.InvokeRequired) {
        _with1.Invoke(new UpdateDiagDelegate(UpdateDiag), whatmessage);
    } else {
        UpdateDiag(whatmessage);
    }
}
// This next line makes the private UpdateDiagWindow available to all threads
private delegate void UpdateDiagDelegate(string whatmessage);
private void UpdateDiag(string whatmessage)
{
    var _with2 = diagwindow;
    _with2.appendtext(whatmessage);
    _with2.SelectionStart = _with2.Text.Length;
    _with2.ScrollToCaret();
}
#endregion
22

Это похоже на решение выше, использующее .NET Framework 3.0, но оно решило проблему поддержки безопасности во время компиляции.

public  static class ControlExtension
{
    delegate void SetPropertyValueHandler<TResult>(Control souce, Expression<Func<Control, TResult>> selector, TResult value);

    public static void SetPropertyValue<TResult>(this Control source, Expression<Func<Control, TResult>> selector, TResult value)
    {
        if (source.InvokeRequired)
        {
            var del = new SetPropertyValueHandler<TResult>(SetPropertyValue);
            source.Invoke(del, new object[]{ source, selector, value});
        }
        else
        {
            var propInfo = ((MemberExpression)selector.Body).Member as PropertyInfo;
            propInfo.SetValue(source, value, null);
        }
    }
}

Для использования:

this.lblTimeDisplay.SetPropertyValue(a => a.Text, "some string");
this.lblTimeDisplay.SetPropertyValue(a => a.Visible, false);

Компилятор не сработает, если пользователь передает неправильный тип данных.

this.lblTimeDisplay.SetPropertyValue(a => a.Visible, "sometext");
19

Это в моем варианте С# 3.0 решения Яна Кемпа:

public static void SetPropertyInGuiThread<C,V>(this C control, Expression<Func<C, V>> property, V value) where C : Control
{
    var memberExpression = property.Body as MemberExpression;
    if (memberExpression == null)
        throw new ArgumentException("The 'property' expression must specify a property on the control.");

    var propertyInfo = memberExpression.Member as PropertyInfo;
    if (propertyInfo == null)
        throw new ArgumentException("The 'property' expression must specify a property on the control.");

    if (control.InvokeRequired)
        control.Invoke(
            (Action<C, Expression<Func<C, V>>, V>)SetPropertyInGuiThread,
            new object[] { control, property, value }
        );
    else
        propertyInfo.SetValue(control, value, null);
}

Вы называете это следующим образом:

myButton.SetPropertyInGuiThread(b => b.Text, "Click Me!")
  • Он добавляет нулевую проверку к результату "как выражение MemberExpression".
  • Это улучшает безопасность статического типа.

В противном случае оригинал - очень приятное решение.

19

Для многих целей это так просто:

public delegate void serviceGUIDelegate();
private void updateGUI()
{
  this.Invoke(new serviceGUIDelegate(serviceGUI));
}

"serviceGUI()" - это метод уровня GUI в форме (это), который может изменять столько элементов управления, сколько вы хотите. Вызовите "updateGUI()" из другого потока. Параметры могут быть добавлены для передачи значений или (возможно, быстрее) использования переменных класса класса с блокировками на них по мере необходимости, если есть какая-либо возможность столкновения между потоками, обращающимися к ним, что может вызвать нестабильность. Используйте BeginInvoke вместо Invoke, если поток без GUI является критическим по времени (учитывая предупреждение Брайана Гидеона).

18

Когда я столкнулся с той же проблемой, я обратился за помощью к Google, но вместо того, чтобы дать мне простое решение, я больше смутил меня, представив примеры MethodInvoker и бла-бла-бла. Поэтому я решил решить это самостоятельно. Вот мое решение:

Сделайте делегат следующим образом:

Public delegate void LabelDelegate(string s);

void Updatelabel(string text)
{
   if (label.InvokeRequired)
   {
       LabelDelegate LDEL = new LabelDelegate(Updatelabel);
       label.Invoke(LDEL, text);
   }
   else
       label.Text = text
}

Вы можете вызвать эту функцию в новом потоке, подобном этому

Thread th = new Thread(() => Updatelabel("Hello World"));
th.start();

Не путайте с Thread(() => .....). Я использую анонимную функцию или лямбда-выражение при работе над потоком. Чтобы уменьшить строки кода, вы можете использовать метод ThreadStart(..), который я не должен здесь объяснять.

18
Label lblText; //initialized elsewhere

void AssignLabel(string text)
{
   if (InvokeRequired)
   {
      BeginInvoke((Action<string>)AssignLabel, text);
      return;
   }

   lblText.Text = text;           
}

Обратите внимание, что BeginInvoke() предпочтительнее, чем Invoke(), поскольку он менее вероятно вызывает взаимоблокировки (однако это не проблема при просто назначении текста метке):

При использовании Invoke() вы ожидаете возвращения метода. Теперь может быть, что вы что-то делаете в вызываемом коде, который должен будет ждать потока, что может быть не сразу очевидным, если оно захоронено в некоторых функциях, которые вы вызываете, что само по себе может происходить косвенно через обработчики событий. Таким образом, вы будете ждать потока, поток будет ждать вас, и вы зашли в тупик.

Это фактически привело к зависанию некоторых из наших выпущенных программ. Это было довольно легко исправить, заменив Invoke() на BeginInvoke(). Если вам не нужна синхронная операция, которая может иметь место, если вам нужно вернуть значение, используйте BeginInvoke().

14

Просто используйте что-то вроде этого:

 this.Invoke((MethodInvoker)delegate
            {
                progressBar1.Value = e.ProgressPercentage; // runs on UI thread
            });
  • 0
    Если у вас есть e.ProgressPercentage , вы уже не в потоке пользовательского интерфейса из метода, который вы вызываете это?
  • 0
    Событие ProgressChanged запускается в потоке пользовательского интерфейса. Это одно из удобных условий использования BackgroundWorker. Событие Completed работает и на графическом интерфейсе. Единственное, что выполняется в потоке, не связанном с пользовательским интерфейсом, - это метод DoWork.
Показать ещё 1 комментарий
14

Вы можете использовать уже существующий делегат Action:

private void UpdateMethod()
{
    if (InvokeRequired)
    {
        Invoke(new Action(UpdateMethod));
    }
}
13

Моей версией является вставка одной строки рекурсивной мантры:

Без аргументов:

    void Aaaaaaa()
    {
        if (InvokeRequired) { Invoke(new Action(Aaaaaaa)); return; } //1 line of mantra

        // Your code!
    }

Для функции с аргументами:

    void Bbb(int x, string text)
    {
        if (InvokeRequired) { Invoke(new Action<int, string>(Bbb), new[] { x, text }); return; }
        // Your code!
    }

ЭТО.


Некоторая аргументация. Обычно это плохо для чтения кода, чтобы поместить {} после оператора if () в одну строку. Но в этом случае это обычная все-таки "мантра". Он не нарушает читаемость кода, если этот метод согласован над проектом. И это избавляет ваш код от мусора (одна строка кода вместо пяти).

Как вы видите if(InvokeRequired) {something long}, вы просто знаете, что "эта функция безопасна для вызова из другого потока".

12

Создайте переменную класса:

SynchronizationContext _context;

Установите его в конструкторе, который создает ваш пользовательский интерфейс:

var _context = SynchronizationContext.Current;

Если вы хотите обновить метку:

_context.Send(status =>{
    // UPDATE LABEL
}, null);
12

Попробуйте обновить ярлык, используя этот

public static class ExtensionMethods
{
    private static Action EmptyDelegate = delegate() { };

    public static void Refresh(this UIElement uiElement)
    {
        uiElement.Dispatcher.Invoke(DispatcherPriority.Render, EmptyDelegate);
    }
}
  • 0
    Это для Windows Forms ?
11

Вы должны использовать invoke и делегировать

private delegate void MyLabelDelegate();
label1.Invoke( new MyLabelDelegate(){ label1.Text += 1; });
7

Большинство других ответов для меня немного сложны в этом вопросе (я новичок в С#), поэтому я пишу:

У меня есть приложение WPF и определил рабочего, как показано ниже:

Выпуск:

BackgroundWorker workerAllocator;
workerAllocator.DoWork += delegate (object sender1, DoWorkEventArgs e1) {
    // This is my DoWork function.
    // It is given as an anonymous function, instead of a separate DoWork function

    // I need to update a message to textbox (txtLog) from this thread function

    // Want to write below line, to update UI
    txt.Text = "my message"

    // But it fails with:
    //  'System.InvalidOperationException':
    //  "The calling thread cannot access this object because a different thread owns it"
}

Решение:

workerAllocator.DoWork += delegate (object sender1, DoWorkEventArgs e1)
{
    // The below single line works
    txtLog.Dispatcher.BeginInvoke((Action)(() => txtLog.Text = "my message"));
}

Мне еще предстоит выяснить, что означает вышеуказанная линия, но это работает.

Для WinForms:

Решение:

txtLog.Invoke((MethodInvoker)delegate
{
    txtLog.Text = "my message";
});
  • 0
    Вопрос был о Winforms, а не о WPF.
  • 0
    Благодарю. Добавлено решение WinForms выше.
Показать ещё 2 комментария
7

Я просто прочитал ответы, и это, кажется, очень горячая тема. В настоящее время я использую .NET 3.5 SP1 и Windows Forms.

Хорошо известная формула, описанная в предыдущих ответах, использующая свойство InvokeRequired, охватывает большинство случаев, но не весь пул.

Что делать, если Ручка еще не создана?

Свойство InvokeRequired, как описано здесь (ссылка на свойство Control.InvokeRequired для MSDN) возвращает true, если вызов был сделан из потока, который не является потоком графического интерфейса пользователя, ложным либо, если вызов был выполнен из потока графического интерфейса пользователя, либо еще не создан Handle.

Вы можете встретить исключение, если хотите, чтобы модальная форма была показана и обновлена ​​другим потоком. Поскольку вы хотите, чтобы форма показала модально, вы можете сделать следующее:

private MyForm _gui;

public void StartToDoThings()
{
    _gui = new MyForm();
    Thread thread = new Thread(SomeDelegate);
    thread.Start();
    _gui.ShowDialog();
}

И делегат может обновить ярлык в графическом интерфейсе:

private void SomeDelegate()
{
    // Operations that can take a variable amount of time, even no time
    //... then you update the GUI
    if(_gui.InvokeRequired)
        _gui.Invoke((Action)delegate { _gui.Label1.Text = "Done!"; });
    else
        _gui.Label1.Text = "Done!";
}

Это может привести к InvalidOperationException, если операции перед обновлением метки "занимают меньше времени" (прочитайте ее и интерпретируйте как упрощение), чем время, необходимое для создания потока GUI для Форма Ручка. Это происходит в методе ShowDialog().

Вы также должны проверить Ручку следующим образом:

private void SomeDelegate()
{
    // Operations that can take a variable amount of time, even no time
    //... then you update the GUI
    if(_gui.IsHandleCreated)  //  <---- ADDED
        if(_gui.InvokeRequired)
            _gui.Invoke((Action)delegate { _gui.Label1.Text = "Done!"; });
        else
            _gui.Label1.Text = "Done!";
}

Вы можете обработать операцию, если Ручка еще не создана: вы можете просто игнорировать обновление графического интерфейса (как показано в приведенном выше коде), или вы можете ждать (более рискованно). Это должно ответить на вопрос.

Дополнительные вещи: Лично я придумал следующее:

public class ThreadSafeGuiCommand
{
  private const int SLEEPING_STEP = 100;
  private readonly int _totalTimeout;
  private int _timeout;

  public ThreadSafeGuiCommand(int totalTimeout)
  {
    _totalTimeout = totalTimeout;
  }

  public void Execute(Form form, Action guiCommand)
  {
    _timeout = _totalTimeout;
    while (!form.IsHandleCreated)
    {
      if (_timeout <= 0) return;

      Thread.Sleep(SLEEPING_STEP);
      _timeout -= SLEEPING_STEP;
    }

    if (form.InvokeRequired)
      form.Invoke(guiCommand);
    else
      guiCommand();
  }
}

Я передаю свои формы, которые обновляются другим потоком с экземпляром этой ThreadSafeGuiCommand, и я определяю методы, которые обновляют GUI (в моей форме) следующим образом:

public void SetLabeTextTo(string value)
{
  _threadSafeGuiCommand.Execute(this, delegate { Label1.Text = value; });
}

Таким образом, я уверен, что у меня будет обновлен мой графический интерфейс, независимо от того, какой поток выполнит вызов, опционально ожидая определенного времени (таймаут).

  • 1
    Пришел сюда, чтобы найти это, так как я также проверяю IsHandleCreated. Еще одно свойство для проверки - IsDisposed. Если ваша форма удалена, вы не можете вызвать Invoke () для нее. Если пользователь закрыл форму до того, как ваш фоновый поток мог завершиться, вы не захотите, чтобы он пытался перезвонить в пользовательский интерфейс при удалении формы.
  • 0
    Я бы сказал, что начинать с этого - плохая идея. Обычно, вы бы сразу показывали дочернюю форму и имели индикатор выполнения или какую-то другую обратную связь при выполнении фоновой обработки. Или вы должны сначала выполнить всю обработку, а затем передать результат новой форме при создании. Выполнение обоих одновременно, как правило, будет иметь незначительные преимущества, но гораздо менее обслуживаемый код.
Показать ещё 1 комментарий
7

Когда вы находитесь в потоке пользовательского интерфейса, вы можете запросить у него планировщик задач контекста синхронизации. Это даст вам TaskScheduler, который планирует все в потоке пользовательского интерфейса.

Затем вы можете связать свои задачи так, чтобы, когда результат был готов, другая задача (которая запланирована в потоке пользовательского интерфейса) выбрала ее и присвоила метке.

public partial class MyForm : Form
{
  private readonly TaskScheduler _uiTaskScheduler;
  public MyForm()
  {
    InitializeComponent();
    _uiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
  }

  private void buttonRunAsyncOperation_Click(object sender, EventArgs e)
  {
    RunAsyncOperation();
  }

  private void RunAsyncOperation()
  {
    var task = new Task<string>(LengthyComputation);
    task.ContinueWith(antecedent =>
                         UpdateResultLabel(antecedent.Result), _uiTaskScheduler);
    task.Start();
  }

  private string LengthyComputation()
  {
    Thread.Sleep(3000);
    return "47";
  }

  private void UpdateResultLabel(string text)
  {
    labelResult.Text = text;
  }
}

Это работает для задач (не потоков), которые сейчас являются предпочтительным способом написания параллельного кода.

7

Например, обратитесь к элементу управления, отличному от текущего потока:

Speed_Threshold = 30;
textOutput.Invoke(new EventHandler(delegate
{
    lblThreshold.Text = Speed_Threshold.ToString();
}));

Здесь lblThreshold - это метка, а Speed_Threshold - глобальная переменная.

6

Самый простой способ:

   void Update()
   {
       BeginInvoke((Action)delegate()
       {
           //do your update
       });
   }
5

Самый простой способ в приложениях WPF:

this.Dispatcher.Invoke((Action)(() =>
{
    // This refers to a form in a WPF application 
    val1 = textBox.Text; // Access the UI 
}));
  • 3
    Это правильно, если вы используете приложение WPF. Но он использует Windows Forms.
5

Я не мог получить логику Microsoft за этой уродливой реализацией, но у вас должны быть две функции:

void setEnableLoginButton()
{
  if (InvokeRequired)
  {
    // btn_login can be any conroller, (label, button textbox ..etc.)

    btn_login.Invoke(new MethodInvoker(setEnable));

    // OR
    //Invoke(new MethodInvoker(setEnable));
  }
  else {
    setEnable();
  }
}

void setEnable()
{
  btn_login.Enabled = isLoginBtnEnabled;
}

Эти фрагменты работают для меня, поэтому я могу что-то сделать в другом потоке, а затем обновляю графический интерфейс:

Task.Factory.StartNew(()=>
{
    // THIS IS NOT GUI
    Thread.Sleep(5000);
    // HERE IS INVOKING GUI
    btn_login.Invoke(new Action(() => DoSomethingOnGUI()));
});

private void DoSomethingOnGUI()
{
   // GUI
   MessageBox.Show("message", "title", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
}

Еще проще:

btn_login.Invoke(new Action(()=>{ /* HERE YOU ARE ON GUI */ }));
5

Даже если операция занимает много времени (thread.sleep в моем примере) - этот код НЕ блокирует ваш интерфейс:

 private void button1_Click(object sender, EventArgs e)
 {

      Thread t = new Thread(new ThreadStart(ThreadJob));
      t.IsBackground = true;
      t.Start();         
 }

 private void ThreadJob()
 {
     string newValue= "Hi";
     Thread.Sleep(2000); 

     this.Invoke((MethodInvoker)delegate
     {
         label1.Text = newValue; 
     });
 }
5

Я хотел добавить предупреждение, потому что заметил, что некоторые простые решения опускают проверку InvokeRequired.

Я заметил, что если ваш код выполняет до того, как дескриптор окна элемента управления был создан (например, до отображения формы), Invoke выдает исключение. Поэтому я рекомендую всегда проверять InvokeRequired перед вызовом Invoke или BeginInvoke.

4

Еще один пример темы: Я создал абстрактный класс, UiSynchronizeModel, который содержит общую реализацию метода:

public abstract class UiSynchronizeModel
{
    private readonly TaskScheduler uiSyncContext;
    private readonly SynchronizationContext winformsOrDefaultContext;

    protected UiSynchronizeModel()
    {
        this.winformsOrDefaultContext = SynchronizationContext.Current ?? new SynchronizationContext();
        this.uiSyncContext = TaskScheduler.FromCurrentSynchronizationContext();
    }

    protected void RunOnGuiThread(Action action)
    {
        this.winformsOrDefaultContext.Post(o => action(), null);
    }

    protected void CompleteTask(Task task, TaskContinuationOptions options, Action<Task> action)
    {
        task.ContinueWith(delegate
        {
            action(task);
            task.Dispose();
        }, CancellationToken.None, options, this.uiSyncContext);
    }
}

Ваша модель или класс контроллера должны быть получены из этого абстрактного класса. Вы можете использовать любой шаблон (задачи или управляемые вручную потоки фона) и использовать эти методы следующим образом:

public void MethodThatCalledFromBackroundThread()
{
   this.RunOnGuiThread(() => {
       // Do something over UI controls
   });
}

Пример задач:

var task = Task.Factory.StartNew(delegate
{
    // Background code
    this.RunOnGuiThread(() => {
        // Do something over UI controls
    });
});

this.CompleteTask(task, TaskContinuationOptions.OnlyOnRanToCompletion, delegate
{
    // Code that can safely use UI controls
});
4

Вот новый взгляд на вековую проблему с использованием более функционального стиля. Если вы сохраняете класс TaskXM во всех своих проектах, у вас есть только одна строка кода, чтобы больше не беспокоиться о обновлениях кросс-потоков.

public class Example
{
    /// <summary>
    /// No more delegates, background workers, etc. Just one line of code as shown below.
    /// Note it is dependent on the Task Extension method shown next.
    /// </summary>
    public async void Method1()
    {
        // Still on the GUI thread here if the method was called from the GUI thread
        // This code below calls the extension method which spins up a new task and calls back.
        await TaskXM.RunCodeAsync(() =>
        {
            // Running an asynchronous task here
            // Cannot update the GUI thread here, but can do lots of work
        });
        // Can update GUI on this line
    }
}


/// <summary>
/// A class containing extension methods for the Task class
/// </summary>
public static class TaskXM
{
    /// <summary>
    /// RunCodeAsyc is an extension method that encapsulates the Task.run using a callback
    /// </summary>
    /// <param name="Code">The caller is called back on the new Task (on a different thread)</param>
    /// <returns></returns>
    public async static Task RunCodeAsync(Action Code)
    {
        await Task.Run(() =>
        {
            Code();
        });
        return;
    }
}
  • 0
    На самом деле спрашиваю: чем Task.Run обёртывание Task.Run отличается от простого вызова await Task.Run(() => {...}); ? В чем здесь преимущество косвенности?
  • 0
    Внешне без разницы. Если взглянуть глубже, это показывает силу функционального программирования. В частности, это обертывание статического метода, который помогает в одиночной ответственности. Что делать, если вы сейчас хотите реализовать ConfigureAwait (false) или добавить оператор ведения журнала. Теперь вы можете сделать это только один раз.
4

Может быть, немного передозировка, но это тот способ, которым я обычно решаю это:

Вызов здесь не требуется из-за синхронизации. BasicClassThreadExample - это просто макет для меня, поэтому измените его в соответствии с вашими реальными потребностями.

Это просто, потому что вам не нужно обрабатывать материал в потоке пользовательского интерфейса!

public partial class Form1 : Form
{
    BasicClassThreadExample _example;

    public Form1()
    {
        InitializeComponent();
        _example = new BasicClassThreadExample();
        _example.MessageReceivedEvent += _example_MessageReceivedEvent;
    }

    void _example_MessageReceivedEvent(string command)
    {
        listBox1.Items.Add(command);
    }

    private void button1_Click(object sender, EventArgs e)
    {
        listBox1.Items.Clear();
        _example.Start();
    }
}

public class BasicClassThreadExample : IDisposable
{
    public delegate void MessageReceivedHandler(string msg);

    public event MessageReceivedHandler MessageReceivedEvent;

    protected virtual void OnMessageReceivedEvent(string msg)
    {
        MessageReceivedHandler handler = MessageReceivedEvent;
        if (handler != null)
        {
            handler(msg);
        }
    }

    private System.Threading.SynchronizationContext _SynchronizationContext;
    private System.Threading.Thread _doWorkThread;
    private bool disposed = false;

    public BasicClassThreadExample()
    {
        _SynchronizationContext = System.ComponentModel.AsyncOperationManager.SynchronizationContext;
    }

    public void Start()
    {
        _doWorkThread = _doWorkThread ?? new System.Threading.Thread(dowork);

        if (!(_doWorkThread.IsAlive))
        {
            _doWorkThread = new System.Threading.Thread(dowork);
            _doWorkThread.IsBackground = true;
            _doWorkThread.Start();
        }
    }

    public void dowork()
    {
        string[] retval = System.IO.Directory.GetFiles(@"C:\Windows\System32", "*.*", System.IO.SearchOption.TopDirectoryOnly);
        foreach (var item in retval)
        {
            System.Threading.Thread.Sleep(25);
            _SynchronizationContext.Post(new System.Threading.SendOrPostCallback(delegate(object obj)
            {
                OnMessageReceivedEvent(item);
            }), null);
        }
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                _doWorkThread.Abort();
            }
            disposed = true;
        }
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    ~BasicClassThreadExample() { Dispose(false); }

}
3

В принципе, способ решить эту проблему независимо от версии фреймворка или типа базовой библиотеки GUI состоит в том, чтобы сохранить элемент управления, создающий поток. Синхронизация контекста для рабочего потока, который будет маршалировать связанное с контролем взаимодействие из рабочего потока в поток GUI очереди сообщений.

Пример:

SynchronizationContext ctx = SynchronizationContext.Current; // From control
ctx.Send\Post... // From worker thread
2

Поместите некоторую общую переменную в отдельный класс, чтобы сохранить значение.

Пример:

public  class data_holder_for_controls
{
    // It will hold the value for your label
    public string status = string.Empty;
}

class Demo
{
    public static  data_holder_for_controls d1 = new data_holder_for_controls();

    static void Main(string[] args)
    {
        ThreadStart ts = new ThreadStart(perform_logic);
        Thread t1 = new Thread(ts);
        t1.Start();
        t1.Join();
        //your_label.Text=d1.status; --- can access it from any thread
    }

    public static void perform_logic()
    {
        // Put some code here in this function
        for (int i = 0; i < 10; i++)
        {
            // Statements here
        }
        // Set the result in the status variable
        d1.status = "Task done";
    }
}
2

Сначала получите экземпляр вашей формы (в данном случае mainForm), а затем просто используйте этот код в другом потоке.

mainForm.Invoke(new MethodInvoker(delegate () 
{
    // Update things in my mainForm here
    mainForm.UpdateView();
}));
2

Я предпочитаю это:

private void UpdateNowProcessing(string nowProcessing)
{
    if (this.InvokeRequired)
    {
        Action<string> d = UpdateNowProcessing;
        Invoke(d, nowProcessing);
    }
    else
    {
        this.progressDialog.Next(nowProcessing);
    }            
}
1

И еще одно общее расширение Control Control..

Сначала добавьте метод расширения для объектов типа Control

public static void InvokeIfRequired<T>(this T c, Action<T> action) where T : Control
{
    if (c.InvokeRequired)
    {
        c.Invoke(new Action(() => action(c)));
    }
    else
    {
        action(c);
    }
}

и вызовите это из другого потока, чтобы получить доступ к элементу управления с именем object1 в потоке пользовательского интерфейса:

object1.InvokeIfRequired(c => { c.Visible = true; });
object1.InvokeIfRequired(c => { c.Text = "ABC"; });

.. или вот так

object1.InvokeIfRequired(c => 
  { 
      c.Text = "ABC";
      c.Visible = true; 
  }
);
1

В моем случае (WPF) решение прост как это:

private void updateUI()
{
    if (!Dispatcher.CheckAccess())
    {
        Dispatcher.BeginInvoke(updateUI);
        return;
    }

    // Update any number of controls here
}
  • 0
    Вопрос для Winforms.
0

Самый простой способ заключается в следующем:

 Application.Current.Dispatcher.Invoke(new Action(() =>
             {
                    try
                    {
                        ///
                    }
                    catch (Exception)
                    {
                      //
                    }


                    }
     ));
  • 1
    За исключением того, что вопрос для WinForms.
  • 0
    Не знаю, почему за это проголосовали !? это поможет мне с моей проблемой в приложении WPF. Не у всех читателей этого поста будет точно такая же проблема, как у OP, иногда часть решения может помочь читателю за более короткое время, чем полное решение уникальной проблемы.
-1

Вы можете сделать это, используя следующее выражение Lambda:

label1.Invoke(() => { label1.Text += "status or what ever you want can be assigned to label1.text here"; });
-4

Чтобы добиться этого в WPF, я делаю это следующим образом.

 new Thread(() => 
 {
     while (...)
     {
         SomeLabel.Dispatcher.BeginInvoke((Action)(() => SomeLabel.Text = ...));
     }
 }).Start();
  • 4
    Кстати, вопрос о [winforms].

Ещё вопросы

Сообщество Overcoder
Наверх
Меню