c# - traves - la aplicación llamó a una interfaz que se aplanó para un diferente subproceso




¿Cómo actualizo la GUI desde otro hilo? (20)

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

Tengo un Form en thread1 , y a partir de eso estoy empezando otro thread ( thread2 ). Mientras que thread2 está procesando algunos archivos, me gustaría actualizar una Label en el Form con el estado actual del thread2 de thread2 .

¿Cómo puedo hacer eso?


Manejando trabajo largo

Desde .NET 4.5 y C # 5.0 , debe usar un patrón asíncrono 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 asíncrono recomendado para nuevos desarrollos.

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

Entonces, la solución recomendada para nuevos desarrollos 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 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());
            }
        }
    }
    

Note lo siguiente:

  1. Código corto y limpio escrito de manera secuencial sin devoluciones de llamada y subprocesos explícitos.
  2. Task lugar de Thread .
  3. async clave async , que permite utilizar await que a su vez evita que el controlador de eventos alcance el estado de finalización hasta que la tarea finalice y, mientras tanto, no bloquee el subproceso de la interfaz de usuario.
  4. Clase de progreso (ver Interfaz de IProgress ) que admite el principio de diseño de Separación de Preocupaciones (SoC) y no requiere un despachador explícito ni invocación. Utiliza el actual SynchronizationContext desde su lugar de creación (aquí, el subproceso de la interfaz de usuario).
  5. TaskCreationOptions.LongRunning que sugiere no hacer cola la tarea en ThreadPool .

Para ver ejemplos más detallados, vea: El futuro de C #: Las cosas buenas llegan a aquellos que "esperan" de Joseph Albahari .

Véase también sobre el concepto UI Threading Model .

Manejo de excepciones

El siguiente fragmento de código es un ejemplo de cómo manejar las excepciones y cambiar la propiedad Enabled del botón para evitar múltiples clics 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...");
    }
}

Cuando encontré el mismo problema, busqué la ayuda de Google, pero en lugar de darme una solución simple, me confundió más dando ejemplos de MethodInvoker y blah blah blah. Así que decidí resolverlo por mi cuenta. Aquí está mi solución:

Haz un delegado como este:

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
}

Puedes llamar a esta función en un nuevo hilo como este.

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

No se confunda con Thread(() => .....). Utilizo una función anónima o una expresión lambda cuando trabajo en un hilo. Para reducir las líneas de código, también puede utilizar el ThreadStart(..)método que no debo explicar aquí.


Dispara y olvida el método de extensión 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");

El código de subprocesamiento es a menudo defectuoso y siempre es difícil de probar. No necesita escribir código de subprocesos para actualizar la interfaz de usuario desde una tarea en segundo plano. Simplemente use la clase BackgroundWorker para ejecutar la tarea y su método ReportProgress para actualizar la interfaz de usuario. Por lo general, solo se 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 interfaz de usuario y pasarla al método ReportProgress.

Una última cosa, asegúrese de establecer el indicador WorkerReportsProgress , o el método ReportProgress se ignorará por completo.


Esta es similar a la solución anterior utilizando .NET Framework 3.0, pero resolvió el problema del soporte de seguridad en tiempo de compilación .

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

Usar:

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

El compilador fallará si el usuario pasa el tipo de datos incorrecto.

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

Esto en mi variación C # 3.0 de la solución de Ian Kemp:

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

Lo llamas así:

myButton.SetPropertyInGuiThread(b => b.Text, "Click Me!")
  1. Añade una comprobación nula al resultado de "como MemberExpression".
  2. Mejora la seguridad de tipo estático.

De lo contrario, el original es una solución muy agradable.


La gran mayoría de las respuestas usa Control.Invoke que es una condición de carrera que está por ocurrir . Por ejemplo, considere la respuesta aceptada:

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

Si el usuario cierra el formulario justo antes de this.Invoke Se llama a this.Invoke (recuerde, this es el objeto de Form ), ObjectDisposedException se ObjectDisposedException una ObjectDisposedException .

La solución es utilizar SynchronizationContext , específicamente SynchronizationContext.Current como sugiere hamilton.danielb (otras respuestas se basan en implementaciones específicas de SynchronizationContext que son completamente innecesarias). Sin embargo, modificaría ligeramente su código para usar SynchronizationContext.Post lugar de SynchronizationContext.Send (ya que normalmente no es necesario que el subproceso de trabajo espere):

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

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

Tenga en cuenta que en .NET 4.0 y superiores debería estar usando tareas para operaciones asíncronas. Vea n-san's respuesta de n-san's para el enfoque equivalente basado en tareas (utilizando TaskScheduler.FromCurrentSynchronizationContext ).

Finalmente, en .NET 4.5 y superiores, también puede usar Progress<T> (que básicamente captura SynchronizationContext.Current sobre su creación) como lo demostró Ryszard Dżegan's para los casos en que la operación de ejecución prolongada necesita ejecutar el código UI mientras aún funciona.


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
}

Para .NET 2.0, aquí hay una buena parte del código que escribí que hace exactamente lo que quieres, y funciona para cualquier propiedad en un 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 volver a escribir el método anterior como un método de extensión de la clase Control , lo que simplificaría la llamada a:

myLabel.SetPropertyThreadSafe("Text", status);

ACTUALIZACIÓN 05/10/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 clara, más sencilla y más segura:

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

No solo se comprueba el nombre de la propiedad en el momento de la compilación, sino 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 pasar la propiedad y el valor de otro Control , por lo que lo siguiente compilará con gusto:

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

Por lo tanto, agregué los controles de tiempo de ejecución para asegurar que la propiedad pasada en realidad pertenece al Control que se está solicitando el método. 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, ¡por favor comente!


Para muchos propósitos es tan simple como esto:

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

"serviceGUI ()" es un método de nivel de GUI dentro del formulario (esto) que puede cambiar tantos controles como desee. Llame a "updateGUI ()" desde el otro hilo. Se pueden agregar parámetros para pasar valores, o (probablemente más rápido) usar variables de alcance de clase con bloqueos según sea necesario si existe alguna posibilidad de un conflicto entre los hilos que acceden a ellos que podría causar inestabilidad. Use BeginInvoke en lugar de Invoke si el subproceso que no es de GUI es crítico en el tiempo (teniendo en cuenta la advertencia de Brian Gideon).


Tendrá que asegurarse de que la actualización suceda en el hilo correcto; el hilo de la interfaz de usuario.

Para hacer esto, tendrás que invocar el controlador de eventos en lugar de llamarlo directamente.

Puedes hacer esto levantando tu evento de esta manera:

(El código se escribe aquí fuera de mi cabeza, por lo que no he comprobado la sintaxis correcta, etc., pero debería comenzar).

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

Tenga en cuenta que el código anterior no funcionará en los proyectos de WPF, ya que los controles de WPF no implementan la interfaz ISynchronizeInvoke .

Para asegurarse de que el código anterior funciona con Windows Forms y WPF, y todas las demás plataformas, puede echar un vistazo a las AsyncOperation , AsyncOperationManager y SynchronizationContext .

Para poder generar eventos fácilmente de esta manera, he creado un método de extensión, que me permite simplificar el evento simplemente llamando:

MyEvent.Raise(this, EventArgs.Empty);

Por supuesto, también puede hacer uso de la clase BackGroundWorker, que resumirá este asunto por usted.


Tendrá que invocar el método en el hilo GUI. Puedes hacerlo llamando a Control.Invoke.

Por ejemplo:

delegate void UpdateLabelDelegate (string message);

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

    MyLabelControl.Text = message;
}

Crear una variable de clase:

SynchronizationContext _context;

Establézcalo en el constructor que crea su interfaz de usuario:

var _context = SynchronizationContext.Current;

Cuando quieras actualizar la etiqueta:

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

Debes utilizar invocar y delegar.

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

La forma más fácil que pienso:

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

Por ejemplo, acceder a un control que no sea en el hilo actual:

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

Ahí lblThresholdhay una etiqueta y Speed_Thresholdes una variable global.


Acabo de leer las respuestas y esto parece ser un tema muy candente. Actualmente estoy usando .NET 3.5 SP1 y Windows Forms.

La conocida fórmula ampliamente descrita en las respuestas anteriores que hace uso de la propiedad InvokeRequired cubre la mayoría de los casos, pero no la agrupación completa.

¿Qué pasa si el Mango no ha sido creado todavía?

La propiedad InvokeRequired , como se describe aquí (Control.InvokeRequired Property reference to MSDN) devuelve true si la llamada se realizó desde un subproceso que no es el subproceso de la GUI, false si la llamada se realizó desde el subproceso de la GUI, o si el identificador era aún no creado.

Puede encontrar una excepción si desea que otro hilo muestre y actualice un formulario modal. Debido a que desea que la forma se muestre de manera modal, podría hacer lo siguiente:

private MyForm _gui;

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

Y el delegado puede actualizar una etiqueta en la GUI:

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

Esto puede causar una InvalidOperationException si las operaciones antes de la actualización de la etiqueta "toman menos tiempo" (leerlo e interpretarlo como una simplificación) que el tiempo que tarda el hilo de interfaz gráfica de usuario para crear el formulario 's de la manija . Esto sucede dentro del método ShowDialog () .

También deberías revisar el Mango de esta manera:

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

Puede manejar la operación que se realizará si el identificador aún no se ha creado: simplemente puede ignorar la actualización de la GUI (como se muestra en el código anterior) o puede esperar (más riesgoso). Esto debería responder a la pregunta.

Cosas opcionales: Personalmente se me ocurrió codificar lo siguiente:

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

Alimino mis formularios que se actualizan mediante otro hilo con una instancia de este ThreadSafeGuiCommand , y defino métodos que actualizan la GUI (en mi formulario) de la siguiente manera:

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

De esta manera, estoy bastante seguro de que mi GUI se actualizará en cualquier subproceso que haga la llamada, opcionalmente esperando un tiempo bien definido (el tiempo de espera).


Cuando se encuentre en el subproceso de la interfaz de usuario, puede solicitar su programador de tareas de contexto de sincronización. Le daría un TaskScheduler que programa todo en el hilo de la interfaz de usuario.

Luego puede encadenar sus tareas para que cuando el resultado esté listo, otra tarea (que está programada en el subproceso de la interfaz de usuario) lo elija y lo asigne a una etiqueta.

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

Esto funciona para tareas (no hilos) que son la forma preferida de escribir código concurrente ahora .


Puede utilizar el delegado ya existente Action:

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

Quería agregar una advertencia porque noté que algunas de las soluciones simples omiten la InvokeRequiredverificación.

Noté que si su código se ejecuta antes de que se haya creado el controlador de ventana del control (por ejemplo, antes de que se muestre el formulario), se produce Invokeuna excepción. Así que recomiendo siempre comprobar InvokeRequiredantes de llamar Invokeo BeginInvoke.





user-interface