Pregunta ¿Cómo actualizo la GUI de otro hilo?


¿Cuál es la forma más sencilla de actualizar un Label de otro hilo?

tengo un Form en thread1, y a partir de eso estoy comenzando otro hilo (thread2) Mientras thread2 está procesando algunos archivos Me gustaría actualizar una Label sobre el Form con el estado actual de thread2trabajo de

¿Cómo puedo hacer eso?


1144
2018-03-19 09:37


origen


Respuestas:


Para .NET 2.0, aquí hay un buen código que escribí y que hace exactamente lo que quiere, y funciona para cualquier propiedad en una 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 });
  }
}

Llámalo así:

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

Si usa .NET 3.0 o superior, puede volver a escribir el método anterior como un método de extensión del Control clase, que luego simplificaría la llamada a:

myLabel.SetPropertyThreadSafe("Text", status);

ACTUALIZACIÓN 05/10/2010:

Para .NET 3.0 debe usar este código:

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

que usa expresiones LINQ y lambda para permitir una sintaxis mucho más limpia, simple y segura:

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

No solo se verifica el nombre de la propiedad en tiempo de compilación, sino que también el tipo de propiedad, por lo que es imposible (por ejemplo) asignar un valor de cadena a una propiedad booleana y, por lo tanto, provocar una excepción de tiempo de ejecución.

Desafortunadamente, esto no impide que nadie haga cosas estúpidas como pasarle a otro Controlpropiedad y valor, por lo que los siguientes se compilarán felizmente:

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

Por lo tanto, agregué los controles de tiempo de ejecución para garantizar que la propiedad transferida realmente pertenezca a la Control que el método está siendo llamado. No es perfecto, pero es mucho mejor que la versión .NET 2.0.

Si alguien tiene más sugerencias sobre cómo mejorar este código para la seguridad en tiempo de compilación, ¡por favor comente!


688
2018-03-19 10:37



los más simple manera es un método anónimo pasado a 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

Darse cuenta de Invoke bloquea la ejecución hasta que se complete - esto es código síncrono. La pregunta no pregunta sobre el código asincrónico, pero hay muchas contenido en desbordamiento de pila sobre cómo escribir un código asíncrono cuando quiera aprender sobre él.


938
2018-03-19 10:17



Manejando un largo trabajo

Ya que .NET 4.5 y C # 5.0 Deberías usar Patrón asincrónico basado en tareas (TAP) junto con asincrónico-esperar palabras clave en todas las áreas (incluida la GUI):

TAP es el patrón de diseño asíncrono recomendado para el nuevo desarrollo

en lugar de Modelo de programación asíncrono (APM) y Patrón asincrónico basado en eventos (EAP) (este último incluye el Clase de trabajo en segundo plano)

Entonces, la solución recomendada para un nuevo desarrollo es:

  1. Implementación asíncrona de un controlador de eventos (Sí, eso es todo):

    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. Implementación del segundo hilo que notifica el hilo de UI:

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

Observe lo siguiente:

  1. Código breve y limpio escrito de forma secuencial sin devoluciones de llamadas e hilos explícitos.
  2. Tarea en lugar de Hilo.
  3. asincrónico palabra clave, que permite usar esperar lo que a su vez impide que el controlador de eventos alcance el estado de finalización hasta que la tarea finalice y, mientras tanto, no bloquea el hilo de la interfaz de usuario.
  4. Clase de progreso (ver Interfaz IProgress) que apoya Separación de preocupaciones (SoC) principio de diseño y no requiere despachador e invocación explícitos. Utiliza la corriente SynchronizationContext desde su lugar de creación (aquí el hilo de UI).
  5. TaskCreationOptions.LongRunning que insinúa no hacer cola en la tarea ThreadPool.

Para ver ejemplos más detallados, consulte: El futuro de C #: las cosas buenas llegan a los que 'esperan' por Joseph Albahari.

Ver también sobre UI modelo de enhebrado concepto.

Manejo de excepciones

El siguiente fragmento es un ejemplo de cómo manejar excepciones y alternar el botón Enabled propiedad para evitar clics múltiples durante la ejecución del fondo.

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

333
2017-08-03 13:09



Variación de Marc Gravell's más simple solución para .NET 4:

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

O use el delegado de Acción en su lugar:

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

Vea aquí para una comparación de los dos: MethodInvoker vs Action para Control.BeginInvoke


195
2018-05-29 18:51



Método de extensión Fire and forget para .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();
        }
    }
}

Esto se puede llamar usando la siguiente línea de código:

this.UIThread(() => this.myLabel.Text = "Text Goes Here");

115
2017-08-27 21:10



Esta es la forma clásica en que debes hacer esto:

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

Su hilo de trabajo tiene un evento. Su subproceso de interfaz de usuario comienza otro subproceso para hacer el trabajo y conecta ese evento de trabajador para que pueda mostrar el estado del subproceso de trabajo.

Luego, en la interfaz de usuario, debe cruzar los hilos para cambiar el control real ... como una etiqueta o una barra de progreso.


57
2018-03-19 10:31



La solución simple es usar Control.Invoke.

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

void updateGUI() {
    // update gui here
}

50
2018-03-19 09:46



El código de enhebrado a menudo tiene errores y es siempre difícil de probar. No es necesario que escriba el código de enhebrado para actualizar la interfaz de usuario de una tarea en segundo plano. Solo usa el BackgroundWorker clase para ejecutar la tarea y su ReportProgress método para actualizar la interfaz de usuario. Normalmente, solo informa un porcentaje completado, pero hay otra sobrecarga que incluye un objeto de estado. Aquí hay un ejemplo que solo informa un objeto de cadena:

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

Eso está bien si siempre quieres actualizar el mismo campo. Si tiene que hacer actualizaciones más complicadas, puede definir una clase para representar el estado de la IU y pasarla al método ReportProgress.

Una última cosa, asegúrese de configurar el WorkerReportsProgress bandera, o el ReportProgress método será completamente ignorado.


41
2017-08-27 20:42