¿Cómo actualizo la GUI desde otro hilo?

Resuelto CruelIO asked hace 15 años • 47 respuestas

¿ Cuál es la forma más sencilla de actualizar uno Labeldesde otro Thread?

  • Tengo un Formhilo en ejecución thread1y a partir de ahí estoy iniciando otro hilo ( thread2).

  • Mientras thread2estoy procesando algunos archivos, me gustaría actualizar Labelel Formestado actual del thread2trabajo.

¿Cómo podría hacer eso?

CruelIO avatar Mar 19 '09 16:03 CruelIO
Aceptado

La forma más sencilla es pasar un método anónimo 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

Observe que Invokebloquea la ejecución hasta que se completa; este es un código sincrónico. La pregunta no se refiere al código asincrónico, pero hay mucho contenido en Stack Overflow sobre cómo escribir código asincrónico cuando desea obtener información al respecto.

Marc Gravell avatar Mar 19 '2009 10:03 Marc Gravell

Para .NET 2.0, aquí hay un buen código que escribí que hace exactamente lo que quieres y funciona para cualquier propiedad en 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 está utilizando .NET 3.0 o superior, puede reescribir el método anterior como un método de extensión de la Controlclase, lo que luego simplificaría la llamada a:

myLabel.SetPropertyThreadSafe("Text", status);

ACTUALIZACIÓN 10/05/2010:

Para .NET 3.0 debes 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 ||
      [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 });
  }
}

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

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

Ahora no solo se verifica el nombre de la propiedad en tiempo de compilación, sino que también se verifica 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 en tiempo de ejecución.

Desafortunadamente, esto no impide que nadie haga cosas estúpidas como pasar Controlla propiedad y el valor de otra persona, por lo que lo siguiente se compilará felizmente:

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

Por lo tanto, agregué comprobaciones de tiempo de ejecución para garantizar que la propiedad pasada realmente pertenezca al Controlmétodo al que se llama. No es perfecto, pero sigue siendo 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, ¡comente!

Ian Kemp avatar Mar 19 '2009 10:03 Ian Kemp

Manejando trabajos largos

Desde .NET 4.5 y C# 5.0, debe usar el patrón asincrónico basado en tareas (TAP) junto con async - await palabras clave en todas las áreas (incluida la GUI):

TAP es el patrón de diseño asincrónico recomendado para nuevos desarrollos

en lugar del Modelo de programación asincrónica (APM) y el Patrón asincrónico basado en eventos (EAP) (este último incluye la clase BackgroundWorker ).

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

  1. Implementación asincrónica 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 al hilo de la interfaz de usuario:

     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 corto y limpio escrito de manera secuencial sin devoluciones de llamada ni subprocesos explícitos.
  2. Tarea en lugar de Hilo .
  3. palabra clave async , que permite usar await, lo que a su vez evita que el controlador de eventos alcance el estado de finalización hasta que finalice la tarea y, mientras tanto, no bloquea el hilo de la interfaz de usuario.
  4. Clase de progreso (consulte Interfaz IProgress ) que admite el principio de diseño de separación de preocupaciones (SoC) y no requiere un despachador ni una invocación explícitos. Utiliza el SynchronizationContext actual desde su lugar de creación (aquí el hilo de la interfaz de usuario).
  5. TaskCreationOptions.LongRunning que sugiere no poner la tarea en cola en ThreadPool .

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

Consulte también sobre el concepto del modelo de subprocesamiento de interfaz de usuario .

Manejo de excepciones

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

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...");
    }
}
Ryszard Dżegan avatar Aug 03 '2013 13:08 Ryszard Dżegan