¿Cómo actualizo la GUI desde otro hilo?

1393

¿Cuál es la forma más sencilla de actualizar un Labelde otro Thread?

  • Tengo una Formejecución thread1, y a partir de eso estoy comenzando otro hilo ( thread2).

  • Mientras thread2está procesando algunos archivos me gustaría actualizar una Labelen el Formcon el estado actual de thread2trabajo 's.

¿Cómo podría hacer eso?

CruelIO
fuente
25
.Net 2.0+ no tiene la clase BackgroundWorker solo para esto. Es UI hilo consciente. 1. Cree un BackgroundWorker 2. Agregue dos delegados (uno para procesar y otro para completar)
Preet Sangha
13
quizás un poco tarde: codeproject.com/KB/cs/Threadsafe_formupdating.aspx
MichaelD
44
Vea la respuesta para .NET 4.5 y C # 5.0: stackoverflow.com/a/18033198/2042090
Ryszard Dżegan
55
Esta pregunta no se aplica a Gtk # GUI. Para Gtk # vea esto y esta respuesta.
hlovdal 01 de
Cuidado: las respuestas a esta pregunta ahora son un desorden desordenado de OT ("esto es lo que hice para mi aplicación WPF") y artefactos históricos .NET 2.0.
Marc L.

Respuestas:

768

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, podría volver a escribir el método anterior como un método de extensión de la Controlclase, que luego simplificaría la llamada a:

myLabel.SetPropertyThreadSafe("Text", status);

ACTUALIZACIÓN 10/05/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

Ahora no solo se verifica el nombre de la propiedad en tiempo de compilación, el tipo de propiedad también lo es, por lo que es imposible (por ejemplo) asignar un valor de cadena a una propiedad booleana y, por lo tanto, causar 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é las comprobaciones de tiempo de ejecución para garantizar que la propiedad transferida realmente pertenezca a la Controlque se llama al 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, ¡comente!

Ian Kemp
fuente
3
Hay casos en los que this.GetType () se evalúa igual que propertyInfo.ReflectedType (por ejemplo, LinkLabel en WinForms). No tengo una gran experiencia en C #, pero creo que la condición para la excepción debería ser: if (propertyInfo == null || ([email protected] (). IsSubclassOf (propertyInfo.ReflectedType) && @ this.GetType ( )! = propertyInfo.ReflectedType) || @ this.GetType (). GetProperty (propertyInfo.Name, propertyInfo.PropertyType) == nulo)
Corvin
99
@lan esto puede SetControlPropertyThreadSafe(myLabel, "Text", status)ser llamado desde otro módulo, clase o forma
Smith
71
La solución proporcionada es innecesariamente compleja. Vea la solución de Marc Gravell, o la solución de Zaid Masud, si valora la simplicidad.
Frank Hileman
8
Esta solución desperdicia toneladas de recursos si actualiza varias propiedades, ya que cada invocación cuesta muchos recursos. No creo que así sea como se pretendía la característica de Seguridad de subprocesos. Encapsulte sus acciones de actualización de la interfaz de usuario e invoque UNA VEZ (y no por propiedad)
Consola
44
¿Por qué demonios usarías este código sobre el componente BackgroundWorker?
Andy
1080

La forma más simple 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

Observe que Invokebloquea la ejecución hasta que se complete; este es un código síncrono. La pregunta no se refiere al código asincrónico, pero hay un montón de contenido en Stack Overflow sobre la escritura de código asincrónico cuando desea obtener información al respecto.

Marc Gravell
fuente
8
Como el OP no ha mencionado ninguna clase / instancia, excepto el formulario, eso no es un mal defecto ...
Marc Gravell
39
No olvide que la palabra clave "this" hace referencia a una clase "Control".
AZ.
8
@codecompleting es seguro de cualquier manera, y ya sabemos que estamos en un trabajador, entonces, ¿por qué verificar algo que sabemos?
Marc Gravell
44
@Dragouf no realmente: uno de los puntos de usar este método es que ya sabes qué partes se ejecutan en el trabajador y cuáles se ejecutan en el hilo de la interfaz de usuario. No es necesario verificar.
Marc Gravell
3
@ Joan.bdm no hay un contexto lo suficientemente cercano para que pueda comentar sobre eso
Marc Gravell
401

Manejo de trabajo largo

Desde .NET 4.5 y C # 5.0 , debe usar un patrón asincrónico basado en tareas (TAP) junto con asíncrono : aguarde las 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 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 subproceso que notifica el subproceso 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 y subprocesos explícitos.
  2. Tarea en lugar de Hilo .
  3. palabra clave asíncrona , que permite utilizar wait, 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 de IProgress ) que admite el principio de diseño de Separación de preocupaciones (SoC) y no requiere un despachador e 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, ver: El futuro de C #: Las cosas buenas llegan a quienes 'esperan' por Joseph Albahari .

Consulte también sobre el concepto de modelo de subprocesos de IU

Manejo de excepciones

El fragmento a continuación es un ejemplo de cómo manejar excepciones y alternar la Enabledpropiedad 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...");
    }
}
Ryszard Dżegan
fuente
2
Si SecondThreadConcern.LongWork()arroja una excepción, ¿puede ser detectado por el hilo de la interfaz de usuario? Esta es una excelente publicación, por cierto.
kdbanman
2
He agregado una sección adicional a la respuesta para cumplir con sus requisitos. Saludos.
Ryszard Dżegan
3
La clase ExceptionDispatchInfo es responsable de ese milagro de volver a lanzar la excepción de fondo en el subproceso de la interfaz de usuario en el patrón de espera asíncrona.
Ryszard Dżegan
1
¡¿Soy solo yo al pensar que esta forma de hacer esto es mucho más detallada que simplemente invocar Invoke / Begin ?!
MeTitus
2
Task.Delay(500).Wait()? ¿Cuál es el punto de crear una tarea para simplemente bloquear el hilo actual? ¡Nunca debe bloquear un subproceso de grupo de subprocesos!
Yarik
236

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

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

O use Action delegate en su lugar:

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

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

Zaid Masud
fuente
1
¿Qué es 'control' en este ejemplo? ¿Mi control de IU? Intento implementar esto en WPF en un control de etiqueta, e Invoke no es miembro de mi etiqueta.
Dbloom
¿Qué pasa con el método de extensión como @styxriver stackoverflow.com/a/3588137/206730 ?
Kiquenet
declara 'Acción y;' dentro de la clase o método que cambia la propiedad de texto y actualiza el texto con este código 'yourcontrol.Invoke (y = () => yourcontrol.Text = "new text");'
Antonio Leite
44
@Dbloom no es miembro porque es solo para WinForms. Para WPF usa Dispatcher.Invoke
sLw
1
Estaba siguiendo esta solución, pero a veces mi interfaz de usuario no se estaba actualizando. He descubierto que tengo que this.refresh()a fuerza de invalidar y repintar la interfaz gráfica de usuario .. si es útil ..
Rakibul Haq
137

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");
StyxRiver
fuente
55
¿Cuál es el punto del uso de @this? ¿No sería "control" equivalente? ¿Hay algún beneficio para @this?
argyle
14
@jeromeyers - El @thises simplemente el nombre de la variable, en este caso la referencia al control actual que llama a la extensión. Podrías cambiarle el nombre a fuente, o lo que sea que flote en tu bote. Lo uso @this, porque se refiere a 'este Control' que llama a la extensión y es consistente (al menos en mi cabeza) con el uso de la palabra clave 'this' en el código normal (sin extensión).
StyxRiver
1
Esto es genial, fácil y para mí la mejor solución. Puede incluir todo el trabajo que tiene que hacer en el hilo de la interfaz de usuario. Ejemplo: this.UIThread (() => {txtMessage.Text = mensaje; listBox1.Items.Add (mensaje);});
Auto
1
Realmente me gusta esta solución. Minor nit: nombraría este método en OnUIThreadlugar de UIThread.
ToolmakerSteve
2
Por eso llamé a esta extensión RunOnUiThread. Pero eso es solo un gusto personal.
Grisgram
66

Esta es la forma clásica de 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 trabajo para que pueda mostrar el estado del subproceso de trabajo.

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

Ha
fuente
62

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
}
OregonGhost
fuente
bien hecho por la simplicidad! ¡No solo es simple, sino que también funciona bien! ¡Realmente no entendí por qué Microsoft no podía simplificarlo como debe ser! para llamar a 1 línea en el hilo principal, deberíamos escribir un par de funciones
MBH
1
@MBH De acuerdo. Por cierto, ¿notó stackoverflow.com/a/3588137/199364 respuesta anterior, que define un método de extensión? Hacer esto una vez en una clase de utilidades personalizadas, entonces no tiene que preocuparse más que Microsoft no lo haga por nosotros :)
ToolmakerSteve
@ToolmakerSteve ¡Eso es exactamente lo que significa ser! tiene razón, podemos encontrar una manera, pero quiero decir desde el punto de vista DRY (no se repita), el problema que tiene una solución común, puede ser resuelto por ellos con el mínimo esfuerzo de Microsoft, lo que ahorrará mucho tiempo para programadores :)
MBH
47

El código de subprocesos suele tener errores 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 informa un porcentaje completo, 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 realizar 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 la WorkerReportsProgressbandera, o el ReportProgressmétodo será completamente ignorado.

Don Kirkby
fuente
2
Al final del procesamiento, también es posible actualizar la interfaz de usuario a través de backgroundWorker1_RunWorkerCompleted.
DavidRR
41

La gran mayoría de las respuestas usan, lo Control.Invokecual es una condición de carrera que espera suceder . 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.Invokellamarlo (recuerde, thises el Formobjeto), ObjectDisposedExceptiones probable que se active un.

La solución es utilizarla SynchronizationContext, específicamente SynchronizationContext.Currentcomo sugiere hamilton.danielb (otras respuestas dependen de SynchronizationContextimplementaciones específicas que son completamente innecesarias). Modificaría ligeramente su código para usarlo en SynchronizationContext.Postlugar de hacerlo SynchronizationContext.Send(ya que generalmente no hay necesidad de que el hilo 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 versiones posteriores realmente debería estar utilizando tareas para operaciones asíncronas. Vea la respuesta de n-san para el enfoque basado en tareas equivalente (usando TaskScheduler.FromCurrentSynchronizationContext).

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

Ohad Schneider
fuente
37

Deberá asegurarse de que la actualización se realice en el hilo correcto; el hilo de la interfaz de usuario.

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

Puedes hacer esto levantando tu evento así:

(El código está escrito aquí fuera de mi cabeza, por lo que no he verificado la sintaxis correcta, etc., pero debería hacerlo funcionar).

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 proyectos de WPF, ya que los controles de WPF no implementan la ISynchronizeInvokeinterfaz.

Con el fin de asegurarse de que el código de arriba funciona con Windows Forms y WPF, y todas las demás plataformas, se puede echar un vistazo a las AsyncOperation, AsyncOperationManagery SynchronizationContextlas clases.

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

MyEvent.Raise(this, EventArgs.Empty);

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

Frederik Gheysels
fuente
De hecho, pero no me gusta 'saturar' mi código GUI con este asunto. Mi GUI no debería importarme si necesita invocar o no. En otras palabras: no creo que sea responsabilidad de la GUI realizar el cambio de contexto.
Frederik Gheysels
1
Romper el delegado aparte, etc. parece excesivo, ¿por qué no solo: SynchronizationContext.Current.Send (delegate {MyEvent (...);}, null);
Marc Gravell
¿Siempre tiene acceso al SynchronizationContext? ¿Incluso si tu clase está en una clase lib?
Frederik Gheysels
29

Deberá invocar el método en el hilo de la GUI. Puede 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;
}
Kieron
fuente
1
La línea de invocación me da un error de compilación. La mejor coincidencia de método sobrecargado para 'System.Windows.Forms.Control.Invoke (System.Delegate, object [])' tiene algunos argumentos no válidos
CruelIO
28

Debido a la trivialidad del escenario, realmente tendría la encuesta de subprocesos de la interfaz de usuario para el estado. Creo que encontrarás que puede ser bastante elegante.

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

El enfoque evita la operación de clasificación requerida cuando se utilizan los métodos ISynchronizeInvoke.Invokey ISynchronizeInvoke.BeginInvoke. No hay nada de malo en usar la técnica de clasificación, pero hay algunas advertencias que debes tener en cuenta.

  • Asegúrese de no llamar con BeginInvokedemasiada frecuencia o podría sobrepasar la bomba de mensajes.
  • Llamar Invokeal subproceso de trabajo es una llamada de bloqueo. Se detendrá temporalmente el trabajo que se realiza en ese hilo.

La estrategia que propongo en esta respuesta invierte los roles de comunicación de los hilos. En lugar de que el subproceso de trabajo empuje los datos, el subproceso de la interfaz de usuario lo busca. Este es un patrón común utilizado en muchos escenarios. Dado que todo lo que desea hacer es mostrar información de progreso del subproceso de trabajo, creo que encontrará que esta solución es una gran alternativa a la solución de cálculo de referencias. Tiene las siguientes ventajas.

  • La interfaz de usuario y los subprocesos de trabajo permanecen ligeramente acoplados en lugar del enfoque Control.Invokeo el Control.BeginInvokeenfoque que los une estrechamente.
  • El subproceso de la interfaz de usuario no impedirá el progreso del subproceso de trabajo.
  • El subproceso de trabajo no puede dominar el tiempo que el subproceso de interfaz de usuario pasa la actualización.
  • Los intervalos en los que la UI y los subprocesos de trabajo realizan operaciones pueden permanecer independientes.
  • El subproceso de trabajo no puede desbordar la bomba de mensajes del subproceso de interfaz de usuario.
  • El subproceso de la interfaz de usuario puede determinar cuándo y con qué frecuencia se actualiza la interfaz de usuario.
Brian Gideon
fuente
3
Buena idea. Lo único que no mencionó es cómo disponer correctamente el temporizador una vez que el WorkerThread haya terminado. Tenga en cuenta que esto puede causar problemas cuando finaliza la aplicación (es decir, el usuario cierra la aplicación). ¿Tienes una idea de cómo resolver esto?
Matt
@Matt En lugar de usar un controlador anónimo para el Elapsedevento, usa un método de miembro para poder quitar el temporizador cuando se desecha el formulario ...
Phil1970
@ Phil1970 - Buen punto. Querías decir System.Timers.ElapsedEventHandler handler = (s, a) => { MyProgressLabel.Text = m_Text; };y asignarlo a través de m_Timer.Elapsed += handler;, más adelante en el contexto de disposición haciendo un ¿ m_Timer.Elapsed -= handler;estoy en lo cierto? Y para la disposición / cierre siguiendo los consejos como se discute aquí .
Matt
27

Ninguna de las cosas Invocar en las respuestas anteriores es necesaria.

Debe mirar 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
}
Jon H
fuente
44
¿Qué crees que usa el método Post debajo del capó? :)
increddibelly
23

Esta es similar a la solución anterior usando .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");
Francis
fuente
23

Salvete! Después de buscar esta pregunta, las respuestas de FrankG y Oregon Ghost me parecieron las más fáciles y útiles. Ahora, codifico en Visual Basic y ejecuté este fragmento a través de un convertidor; así que no estoy seguro de cómo resulta.

Tengo un formulario de diálogo llamado form_Diagnostics,que tiene un cuadro de texto enriquecido, llamado updateDiagWindow,que estoy usando como una especie de pantalla de registro. Necesitaba poder actualizar su texto desde todos los hilos. Las líneas adicionales permiten que la ventana se desplace automáticamente a las líneas más nuevas.

Y así, ahora puedo actualizar la pantalla con una línea, desde cualquier parte del programa completo de la manera que creas que funcionaría sin ningún subproceso:

  form_Diagnostics.updateDiagWindow(whatmessage);

Código principal (ponga esto dentro del código de clase de su formulario):

#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
bgmCoder
fuente
21

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 la posibilidad de un choque entre hilos que acceden a ellos que podría causar inestabilidad. Use BeginInvoke en lugar de Invoke si el hilo que no es GUI es crítico en el tiempo (teniendo en cuenta la advertencia de Brian Gideon).

Frankg
fuente
21

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. Agrega verificación nula al resultado de "as MemberExpression".
  2. Mejora la seguridad de tipo estático.

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

Rotaerk
fuente
21
Label lblText; //initialized elsewhere

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

   lblText.Text = text;           
}

Tenga en cuenta que BeginInvoke()se prefiere antes que Invoke()porque es menos probable que cause puntos muertos (sin embargo, esto no es un problema aquí cuando solo se asigna texto a una etiqueta):

Al usar Invoke(), está esperando que regrese el método. Ahora, puede ser que haga algo en el código invocado que tendrá que esperar al hilo, lo que puede no ser inmediatamente obvio si está oculto en algunas funciones que está llamando, lo que en sí mismo puede suceder indirectamente a través de controladores de eventos. Entonces estarías esperando el hilo, el hilo te estaría esperando y estás estancado.

En realidad, esto provocó que se bloqueara parte de nuestro software lanzado. Fue bastante fácil de solucionar reemplazando Invoke()por BeginInvoke(). A menos que necesite una operación sincrónica, que puede ser el caso si necesita un valor de retorno, úselo BeginInvoke().

ILoveFortran
fuente
20

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

Haga 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 te confundas con Thread(() => .....). Uso 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 usar el ThreadStart(..)método que no se supone que explique aquí.

ahmar
fuente
17

Simplemente use algo como esto:

 this.Invoke((MethodInvoker)delegate
            {
                progressBar1.Value = e.ProgressPercentage; // runs on UI thread
            });
Hassan Shouman
fuente
Si es así e.ProgressPercentage, ¿no estás ya en el hilo de la interfaz de usuario del método al que llamas esto?
LarsTech
El evento ProgressChanged se ejecuta en el subproceso de la interfaz de usuario. Esa es una de las comodidades de usar BackgroundWorker. El evento Completado también se ejecuta en la interfaz gráfica de usuario. Lo único que se ejecuta en el subproceso sin interfaz de usuario es el método DoWork.
LarsTech
15

Puede usar el delegado ya existente Action:

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

Mi versión es insertar una línea de "mantra" recursivo:

Sin argumentos:

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

        // Your code!
    }

Para una función que tiene argumentos:

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

Eso es todo .


Algunos argumentos : por lo general, es malo para la legibilidad del código poner {} después de una if ()declaración en una línea. Pero en este caso es el "mantra" de rutina. No interrumpe la legibilidad del código si este método es consistente durante el proyecto. Y ahorra su código de basura (una línea de código en lugar de cinco).

Como puede ver if(InvokeRequired) {something long}, simplemente sabe que "esta función es segura para llamar desde otro hilo".

MajesticRa
fuente
13

Intenta actualizar la etiqueta usando esto

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

    public static void Refresh(this UIElement uiElement)
    {
        uiElement.Dispatcher.Invoke(DispatcherPriority.Render, EmptyDelegate);
    }
}
Ivaylo Slavov
fuente
¿Es para Windows Forms ?
Kiquenet
13

Crea una variable de clase:

SynchronizationContext _context;

Configúralo en el constructor que crea tu interfaz de usuario:

var _context = SynchronizationContext.Current;

Cuando desee actualizar la etiqueta:

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

Debe usar invocar y delegar

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

La mayoría de las otras respuestas son un poco complejas para mí en esta pregunta (soy nuevo en C #), así que estoy escribiendo la mía:

Tengo una aplicación WPF y he definido un trabajador de la siguiente manera:

Problema:

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

Solución:

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

Todavía tengo que descubrir qué significa la línea anterior, pero funciona.

Para WinForms :

Solución:

txtLog.Invoke((MethodInvoker)delegate
{
    txtLog.Text = "my message";
});
Manohar Reddy Poreddy
fuente
La pregunta era sobre Winforms, no sobre WPF.
Marc L.
Gracias. Se agregó la solución WinForms anterior.
Manohar Reddy Poreddy
... que es solo una copia de muchas otras respuestas a esta misma pregunta, pero está bien. ¿Por qué no ser parte de la solución y simplemente eliminar su respuesta?
Marc L.
hmm, correcto eres, si solo, lees mi respuesta con atención, la parte inicial (la razón por la que escribí la respuesta), y espero que con un poco más de atención veas que hay alguien que tuvo exactamente el mismo problema y votó hoy por mi respuesta simple, y con aún más atención si pudieras prever la verdadera historia de por qué sucedió todo esto, ese google me envía aquí incluso cuando busco wpf. Claro, ya que te perdiste estas 3 razones más o menos obvias, puedo entender por qué no eliminarás tu voto negativo. En lugar de limpiar el correcto, cree algo nuevo que sea mucho más difícil.
Manohar Reddy Poreddy
8

La forma más fácil que pienso:

   void Update()
   {
       BeginInvoke((Action)delegate()
       {
           //do your update
       });
   }
Vasily Semenov
fuente
8

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

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

Hay lblThresholduna etiqueta y Speed_Thresholdes una variable global.

Da Xiong
fuente
8

Cuando esté en el subproceso de la interfaz de usuario, puede pedirle 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 hilo 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 .

nosalan
fuente
1
Llamar Task.Startno suele ser una buena práctica blogs.msdn.com/b/pfxteam/archive/2012/01/14/10256832.aspx
Ohad Schneider
8

Acabo de leer las respuestas y este 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 todo el grupo.

¿Qué pasa si la manija aún no se ha creado?

La propiedad InvokeRequired , como se describe aquí (referencia de propiedad Control.InvokeRequired a MSDN) devuelve verdadero si la llamada se realizó desde un subproceso que no es el subproceso GUI, falso si la llamada se realizó desde el subproceso GUI o si el identificador fue No creado aún.

Puede encontrar una excepción si desea que otro hilo muestre y actualice un formulario modal. Debido a que desea que el formulario se muestre modalmente, puede 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" (lean y lo interpretan 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 debe verificar 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 para llevar a cabo si el controlador aún no se ha creado: 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 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();
  }
}

Alimento mis formularios que son actualizados por otro hilo con una instancia de este ThreadSafeGuiCommand , y defino métodos que actualizan la GUI (en mi formulario) de esta manera:

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

De esta manera, estoy bastante seguro de que tendré mi GUI actualizada independientemente del hilo que haga la llamada, opcionalmente esperando una cantidad de tiempo bien definida (el tiempo de espera).

Sume
fuente
1
Vine aquí para encontrar esto, ya que también verifico IsHandleCreated. Otra propiedad para verificar es IsDisposed. Si su formulario está dispuesto, no puede llamar a Invoke () en él. Si el usuario cerró el formulario antes de que su hilo de fondo pudiera completarse, no desea que intente volver a llamar a la interfaz de usuario cuando se elimine el formulario.
Jon
Diría que es una mala idea comenzar con ... Normalmente, mostraría el formulario secundario de inmediato y tendría una barra de progreso o algún otro comentario mientras realiza el procesamiento en segundo plano. O haría todo el procesamiento primero y luego pasaría el resultado al nuevo formulario en la creación. Hacer ambas cosas al mismo tiempo generalmente tendría beneficios marginales pero mucho menos código mantenible.
Phil1970
El escenario descrito tiene en cuenta una forma modal utilizada como vista de progreso de un trabajo de subproceso en segundo plano. Debido a que debe ser modal, debe mostrarse llamando al método Form.ShowDialog () . Al hacer esto, evita que su código que sigue a la llamada se ejecute hasta que se cierre el formulario. Por lo tanto, a menos que pueda iniciar el hilo de fondo de manera diferente al ejemplo dado (y, por supuesto, puede), este formulario debe mostrarse modalmente después de que se haya iniciado el hilo de fondo. En este caso, debe verificar la creación del controlador. Si no necesita una forma modal, entonces es otra historia.
Sume