programacion - xaml c#
Actualizando GUI(WPF) usando un hilo diferente (7)
Solo tengo un problema aquí que no tengo ni idea de cómo solucionarlo. Estoy haciendo un pequeño proyecto que implica una GUI y datos en serie. La GUI está siendo ejecutada por el hilo principal y dado que las variables de datos que contienen mis datos seriales de entrada deben actualizarse continuamente, estas se están actualizando en un segundo hilo. El problema es cuando necesito actualizar algunos cuadros de texto en la GUI, estos deben actualizarse con datos del hilo secundario y ahí es donde reside mi problema. No puedo actualizarlos directamente desde el hilo secundario y no tengo idea de cómo transferiría los datos de mi hilo secundario y encontraré un sistema para actualizarlos desde el hilo principal. He puesto mi código a continuación:
Cualquier ayuda sería genial.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.IO;
using System.IO.Ports;
using System.Threading;
namespace GUIBike
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public static string inputdata;
public static int MaximumSpeed, maximumRiderInput, RiderInput, Time, CurrentSpeed, DistanceTravelled, MaximumMotorOutput, MotorOutput, InputSpeed;
public static string SaveDataString;
public Thread Serial;
public static SerialPort SerialData;
public static string[] portlist = SerialPort.GetPortNames();
public static string[] SaveData = new string[4];
public static string directory = "C://";
public MainWindow()
{
Serial = new Thread(ReadData);
InitializeComponent();
int Count = 0;
for (Count = 0; Count < portlist.Length; Count++)
{
ComPortCombo.Items.Add(portlist[Count]);
}
}
private void StartDataButton_Click(object sender, RoutedEventArgs e)
{
SerialData = new SerialPort(ComPortCombo.Text, 19200, Parity.None, 8, StopBits.One);
SerialData.Open();
SerialData.WriteLine("P");
Serial.Start();
StartDataButton.IsEnabled = false;
EndDataButton.IsEnabled = true;
ComPortCombo.IsEnabled = false;
CurrentSpeed = 0;
MaximumSpeed = 0;
Time = 0;
DistanceTravelled = 0;
MotorOutput = 0;
RiderInput = 0;
SaveData[0] = "";
SaveData[1] = "";
SaveData[2] = "";
SaveData[3] = "";
SaveDataButton.IsEnabled = false;
if (SerialData.IsOpen)
{
ComPortStatusLabel.Content = "OPEN";
SerialData.NewLine = "/n";
SerialData.WriteLine("0");
SerialData.WriteLine("/n");
}
}
private void EndDataButton_Click(object sender, RoutedEventArgs e)
{
SerialData.Close();
SaveDataButton.IsEnabled = true;
SerialData.WriteLine("1");
SerialData.WriteLine("0");
if (!SerialData.IsOpen)
{
ComPortStatusLabel.Content = "CLOSED";
}
int i = 0;
for (i = 0; i < 4; i++)
{
if (i == 0)
{
SaveDataString = "MaximumSpeed during the Ride was = " + Convert.ToString(MaximumSpeed) + "m/h";
SaveData[i] = SaveDataString;
}
if (i == 1)
{
SaveDataString = "Total Distance Travelled = " + Convert.ToString(DistanceTravelled) + "m";
SaveData[i] = SaveDataString;
}
if (i == 2)
{
SaveDataString = "Maximum Rider Input Power = " + Convert.ToString(maximumRiderInput) + "Watts";
SaveData[i] = SaveDataString;
}
if (i == 3)
{
SaveDataString = "Maximum Motor Output Power = " + Convert.ToString(MaximumMotorOutput) + "Watts";
SaveData[i] = SaveDataString;
}
}
}
private void SaveDataButton_Click(object sender, RoutedEventArgs e)
{
//File.WriteAllBytes(directory + "image" + imageNO + ".txt", ); //saves the file to Disk
File.WriteAllLines(directory + "BikeData.txt", SaveData);
}
public void ReadData()
{
int counter = 0;
while (SerialData.IsOpen)
{
if (counter == 0)
{
//try
//{
InputSpeed = Convert.ToInt16(SerialData.ReadChar());
CurrentSpeed = InputSpeed;
if (CurrentSpeed > MaximumSpeed)
{
MaximumSpeed = CurrentSpeed;
}
SpeedTextBox.Text = "Current Wheel Speed = " + Convert.ToString(CurrentSpeed) + "Km/h";
DistanceTravelled = DistanceTravelled + (Convert.ToInt16(CurrentSpeed) * Time);
DistanceTravelledTextBox.Text = "Total Distance Travelled = " + Convert.ToString(DistanceTravelled) + "Km";
//}
//catch (Exception) { }
}
if (counter == 1)
{
try
{
RiderInput = Convert.ToInt16(SerialData.ReadLine());
if (RiderInput > maximumRiderInput)
{
maximumRiderInput = RiderInput;
}
RiderInputTextBox.Text = "Current Rider Input Power =" + Convert.ToString(RiderInput) + "Watts";
}
catch (Exception) { }
}
if (counter == 2)
{
try
{
MotorOutput = Convert.ToInt16(SerialData.ReadLine());
if (MotorOutput > MaximumMotorOutput)
{
MaximumMotorOutput = MotorOutput;
}
MotorOutputTextBox.Text = "Current Motor Output = " + Convert.ToString(MotorOutput) + "Watts";
}
catch (Exception) { }
}
counter++;
if (counter == 3)
{
counter = 0;
}
}
}
private void ComPortCombo_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
StartDataButton.IsEnabled = true;
}
private void Window_Closed(object sender, RoutedEventArgs e)
{
if (SerialData.IsOpen)
{
SerialData.Close();
}
}
Aquí tienes un par de opciones, creo.
Una sería usar un BackgroundWorker. Este es un ayudante común para multihilo en aplicaciones. Expone un evento DoWork que se maneja en un hilo de fondo del grupo de subprocesos y un evento RunWorkerCompleted que se invoca en el subproceso principal cuando se completa el subproceso en segundo plano. También tiene el beneficio de intentar / capturar el código que se ejecuta en el hilo de fondo para que una excepción no controlada no mate la aplicación.
Si no desea seguir esa ruta, puede usar el objeto despachador de WPF para invocar una acción y actualizar la GUI nuevamente en el hilo principal. Referencia aleatoria:
http://www.switchonthecode.com/tutorials/working-with-the-wpf-dispatcher
También hay muchas otras opciones, pero estas son las dos más comunes que se me ocurren.
Como akjoshi y Julio dicen que se trata de despachar una Acción para actualizar la GUI en el mismo hilo que el elemento de la GUI, pero del método que maneja los datos de fondo. Puedes ver este código en forma específica en la respuesta de akjoshi anterior. Esta es una versión general.
myTextBlock.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal,
new Action(delegate()
{
myTextBlock.Text = Convert.ToString(myDataObject.getMeData());
}));
La parte crítica es llamar al despachador de su objeto UI, eso asegura que tiene el hilo correcto.
Desde la experiencia personal, parece mucho más fácil crear y utilizar la Acción en línea de esta manera. Declararlo a nivel de clase me dio muchos problemas con contextos estáticos / no estáticos.
Necesita usar Dispatcher.BeginInvoke
. No lo probé, pero puedes consultar this enlace (este es el mismo enlace proporcionado por Julio G) para tener una mejor comprensión sobre cómo actualizar los controles de la interfaz de usuario desde diferentes hilos. He modificado tu código ReadData()
public void ReadData()
{
int counter = 0;
while (SerialData.IsOpen)
{
if (counter == 0)
{
//try
//{
InputSpeed = Convert.ToInt16(SerialData.ReadChar());
CurrentSpeed = InputSpeed;
if (CurrentSpeed > MaximumSpeed)
{
MaximumSpeed = CurrentSpeed;
}
SpeedTextBox.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal,
new Action(delegate() { SpeedTextBox.Text = "Current Wheel Speed = " + Convert.ToString(CurrentSpeed) + "Km/h"; });//update GUI from this thread
DistanceTravelled = DistanceTravelled + (Convert.ToInt16(CurrentSpeed) * Time);
DistanceTravelledTextBox.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal,
new Action(delegate() {DistanceTravelledTextBox.Text = "Total Distance Travelled = " + Convert.ToString(DistanceTravelled) + "Km"; });//update GUI from this thread
//}
//catch (Exception) { }
}
if (counter == 1)
{
try
{
RiderInput = Convert.ToInt16(SerialData.ReadLine());
if (RiderInput > maximumRiderInput)
{
maximumRiderInput = RiderInput;
}
RiderInputTextBox.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal,
new Action(delegate() { RiderInputTextBox.Text = "Current Rider Input Power =" + Convert.ToString(RiderInput) + "Watts"; });//update GUI from this thread
}
catch (Exception) { }
}
if (counter == 2)
{
try
{
MotorOutput = Convert.ToInt16(SerialData.ReadLine());
if (MotorOutput > MaximumMotorOutput)
{
MaximumMotorOutput = MotorOutput;
}
MotorOutputTextBox.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal,
new Action(delegate() { MotorOutputTextBox.Text = "Current Motor Output = " + Convert.ToString(MotorOutput) + "Watts"; });//update GUI from this thread
}
catch (Exception) { }
}
counter++;
if (counter == 3)
{
counter = 0;
}
}
}
Puede usar Dispatcher.Invoke para actualizar su GUI desde un hilo secundario.
Aquí hay un ejemplo:
private void Window_Loaded(object sender, RoutedEventArgs e)
{
new Thread(DoSomething).Start();
}
public void DoSomething()
{
for (int i = 0; i < 100000000; i++)
{
this.Dispatcher.Invoke(()=>{
textbox.Text=i.ToString();
});
}
}
Puede usar un delegado para resolver este problema. Aquí hay un ejemplo que muestra cómo actualizar un cuadro de texto usando un hilo diferente
public delegate void UpdateTextCallback(string message);
private void TestThread()
{
for (int i = 0; i <= 1000000000; i++)
{
Thread.Sleep(1000);
richTextBox1.Dispatcher.Invoke(
new UpdateTextCallback(this.UpdateText),
new object[] { i.ToString() }
);
}
}
private void UpdateText(string message)
{
richTextBox1.AppendText(message + "/n");
}
private void button1_Click(object sender, RoutedEventArgs e)
{
Thread test = new Thread(new ThreadStart(TestThread));
test.Start();
}
El método TestThread se usa por subprocesos llamado test para actualizar textBox
Use el siguiente método para actualizar la GUI.
Public Void UpdateUI()
{
//Here update your label, button or any string related object.
//Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Background, new ThreadStart(delegate { }));
Application.Current.Dispatcher.Invoke(DispatcherPriority.Background, new ThreadStart(delegate { }));
}
Téngalo en cuenta cuando use este método en ese momento. No actualice el mismo objeto directamente desde el hilo del despachador; de lo contrario, obtendrá solo esa cadena actualizada y este método es inútil / inútil. Si aún no funciona, entonces Comente que la línea dentro del método y el comentario no comentado tienen el mismo efecto, solo una forma diferente de acceder a él.
ahí.
También estoy desarrollando una herramienta de prueba de puertos en serie usando WPF, y me gustaría compartir alguna experiencia mía.
Creo que debería refactorizar su código fuente según el patrón de diseño de MVVM.
Al principio, me encontré con el mismo problema que conociste, y lo resolví con este código:
new Thread(() =>
{
while (...)
{
SomeTextBox.Dispatcher.BeginInvoke((Action)(() => SomeTextBox.Text = ...));
}
}).Start();
Esto funciona, pero es demasiado feo. No tengo idea de cómo refactorizarlo, hasta que vi esto: http://www.codeproject.com/Articles/165368/WPF-MVVM-Quick-Start-Tutorial
Este es un tutorial MVVM muy amablemente paso a paso para principiantes. Sin IU brillante, sin lógica compleja, solo lo básico de MVVM.