Вопрос: Как обновить графический интерфейс из другого потока?


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

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

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


1133


источник


Ответы:


Для .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 ||
      !@this.GetType().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.

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


679



самый простой путь - анонимный метод, переданный в 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блокирует выполнение, пока оно не завершится - это синхронный код. Вопрос не спрашивает об асинхронном коде, но есть много содержимого на переполнение стека о написании асинхронного кода, когда вы хотите узнать об этом.


924



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

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

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

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

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

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

    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";
    }
    
  2. Реализация второго потока, который уведомляет поток пользовательского интерфейса:

    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());
            }
        }
    }
    

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

  1. Короткий и чистый код, написанный последовательным образом без обратных вызовов и явных потоков.
  2. задача вместо Нить ,
  3. асинхронной ключевое слово, которое позволяет использовать Ждите что, в свою очередь, не позволяет обработчику событий достичь состояния завершения до завершения задачи и тем временем не блокирует поток пользовательского интерфейса.
  4. Прогресс (см. Интерфейс IProgress ), который поддерживает Разделение проблем (SoC) принцип проектирования и не требует явного диспетчера и вызова. Он использует текущий SynchronizationContext от места его создания (здесь поток пользовательского интерфейса).
  5. TaskCreationOptions.LongRunning что намекает не ставить задачу в очередь ThreadPool ,

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

См. Также Модель потоковой передачи 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...");
    }
}

325



Изменение Марк Гравелл самый простой решение для .NET 4:

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

Вместо этого используйте делегат Action:

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

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


194



Fire и забыть метод расширения для .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");

114



This is the classic way you should do this:

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;
        }
    }
}

Your worker thread has an event. Your UI thread starts off another thread to do the work and hooks up that worker event so you can display the state of the worker thread.

Then in the UI you need to cross threads to change the actual control... like a label or a progress bar.


56



The simple solution is to use Control.Invoke.

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

void updateGUI() {
    // update gui here
}

49



Threading code is often buggy and always hard to test. You don't need to write threading code to update the user interface from a background task. Just use the BackgroundWorker class to run the task and its ReportProgress method to update the user interface. Usually, you just report a percentage complete, but there's another overload that includes a state object. Here's an example that just reports a string object:

    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();
    }

That's fine if you always want to update the same field. If you've got more complicated updates to make, you could define a class to represent the UI state and pass it to the ReportProgress method.

One final thing, be sure to set the WorkerReportsProgress flag, or the ReportProgress method will be completely ignored.


40



The vast majority of answers use Control.Invoke which is a race condition waiting to happen. For example, consider the accepted answer:

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

If the user closes the form just before this.Invoke is called (remember, this is the Form object), an ObjectDisposedException will be likely fired.

The solution is to use SynchronizationContext, specifically SynchronizationContext.Current as hamilton.danielb suggests (other answers rely on specific SynchronizationContext implementations which is completely unnecessary). I would slightly modify his code to use SynchronizationContext.Post rather than SynchronizationContext.Send though (as there's typically no need for the worker thread to wait):

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

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

Note that on .NET 4.0 and up you should really be using tasks for async operations. See n-san's answer for the equivalent task-based approach (using TaskScheduler.FromCurrentSynchronizationContext).

Finally, on .NET 4.5 and up you can also use Progress<T> (which basically captures SynchronizationContext.Current upon its creation) as demonstrated by Ryszard Dżegan's for cases where the long-running operation needs to run UI code while still working.


31



You'll have to make sure that the update happens on the correct thread; the UI thread.

In order to do this, you'll have to Invoke the event-handler instead of calling it directly.

You can do this by raising your event like this:

(The code is typed here out of my head, so I haven't checked for correct syntax, etc., but it should get you going.)

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 ( ... );
      }
   }
}

Note that the code above will not work on WPF projects, since WPF controls do not implement the ISynchronizeInvoke interface.

In order to make sure that the code above works with Windows Forms and WPF, and all other platforms, you can have a look at the AsyncOperation, AsyncOperationManager and SynchronizationContext classes.

In order to easily raise events this way, I've created an extension method, which allows me to simplify raising an event by just calling:

MyEvent.Raise(this, EventArgs.Empty);

Of course, you can also make use of the BackGroundWorker class, which will abstract this matter for you.


29



You'll need to Invoke the method on the GUI thread. You can do that by calling Control.Invoke.

For example:

delegate void UpdateLabelDelegate (string message);

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

    MyLabelControl.Text = message;
}

25