¿Cómo actualizo la GUI desde otro hilo?
¿ Cuál es la forma más sencilla de actualizar uno Label
desde otro Thread
?
Tengo un
Form
hilo en ejecuciónthread1
y a partir de ahí estoy iniciando otro hilo (thread2
).Mientras
thread2
estoy procesando algunos archivos, me gustaría actualizarLabel
elForm
estado actual delthread2
trabajo.
¿Cómo podría hacer eso?
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 Invoke
bloquea 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.
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 Control
clase, 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 Control
la 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 Control
mé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!
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:
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"; }
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:
- Código corto y limpio escrito de manera secuencial sin devoluciones de llamada ni subprocesos explícitos.
- Tarea en lugar de Hilo .
- 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.
- 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).
- 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 Enabled
la 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...");
}
}