Il ragazzo di Business Intelligence qui con abbastanza C# sotto la cintura è pericoloso.Aggiornamento del thread UI (casella di testo) tramite C#
Ho creato un'applicazione di winform homebrew che essenzialmente esegue uno strumento da riga di comando in un ciclo per "fare cose". Detto materiale potrebbe essere completato in secondi o minuti. Normalmente avrò bisogno di eseguire lo strumento una volta per ogni riga che si trova in un DataTable.
Devo reindirizzare l'output dello strumento da riga di comando e visualizzarlo nella "mia" app. Sto tentando di farlo tramite una casella di testo. Sto incontrando problemi relativi all'aggiornamento del thread dell'interfaccia utente che non riesco a chiarire da solo.
Per eseguire il mio strumento da riga di comando, ho preso in prestito il codice da qui: How to parse command line output from c#?
Qui è il mio equivalente:
private void btnImport_Click(object sender, EventArgs e)
{
txtOutput.Clear();
ImportWorkbooks(dtable);
}
public void ImportWorkbooks(DataTable dt)
{
ProcessStartInfo cmdStartInfo = new ProcessStartInfo();
cmdStartInfo.FileName = @"C:\Windows\System32\cmd.exe";
cmdStartInfo.RedirectStandardOutput = true;
cmdStartInfo.RedirectStandardError = true;
cmdStartInfo.RedirectStandardInput = true;
cmdStartInfo.UseShellExecute = false;
cmdStartInfo.CreateNoWindow = false;
Process cmdProcess = new Process();
cmdProcess.StartInfo = cmdStartInfo;
cmdProcess.ErrorDataReceived += cmd_Error;
cmdProcess.OutputDataReceived += cmd_DataReceived;
cmdProcess.EnableRaisingEvents = true;
cmdProcess.Start();
cmdProcess.BeginOutputReadLine();
cmdProcess.BeginErrorReadLine();
//Login
cmdProcess.StandardInput.WriteLine(BuildLoginString(txtTabCmd.Text, txtImportUserName.Text, txtImportPassword.Text, txtImportToServer.Text));
foreach (DataRow dr in dt.Rows)
{
cmdProcess.StandardInput.WriteLine(CreateServerProjectsString(dr["Project"].ToString(), txtTabCmd.Text));
//Import Workbook
cmdProcess.StandardInput.WriteLine(BuildPublishString(txtTabCmd.Text, dr["Name"].ToString(), dr["UID"].ToString(),dr["Password"].ToString(), dr["Project"].ToString()));
}
cmdProcess.StandardInput.WriteLine("exit"); //Execute exit.
cmdProcess.EnableRaisingEvents = false;
cmdProcess.WaitForExit();
}
private void cmd_DataReceived(object sender, DataReceivedEventArgs e)
{
//MessageBox.Show("Output from other process");
try
{
// I want to update my textbox here, and then position the cursor
// at the bottom ala:
StringBuilder sb = new StringBuilder(txtOutput.Text);
sb.AppendLine(e.Data.ToString());
txtOutput.Text = sb.ToString();
this.txtOutput.SelectionStart = txtOutput.Text.Length;
this.txtOutput.ScrollToCaret();
}
catch (Exception ex)
{
Console.WriteLine("{0} Exception caught.", ex);
}
}
Riferimento txtOuput.text quando un'istanza di mia StringBuilder in cmd_DataReceived() fa sì che l'app si blocchi appieno: sto indovinando una sorta di problema cross-thread.
Se rimuovo il riferimento alla txtOuput.text in StringBuilder e continuare il debug, ottengo una violazione cross-thread qui:
txtOutput.Text = sb.ToString();
Cross-thread operation not valid: Control 'txtOutput' accessed from a thread other than the thread it was created on.
OK, non sorpreso. Ho ipotizzato che cmd_DataReceived sia in esecuzione su un altro thread poiché lo sto colpendo come risultato di fare cose dopo un Process.Start() ... e se rimuovo TUTTI i riferimenti a txtOuput.Text in cmd_DataReceived() e semplicemente scarico il testo della riga di comando output alla console tramite Console.Write(), tutto funziona correttamente.
Così, la prossima ho intenzione di provare le tecniche standard per aggiornare il mio TextBox sul thread dell'interfaccia utente utilizzando le informazioni in http://msdn.microsoft.com/en-us/library/ms171728.aspx
aggiungo un delegato e filo per la mia classe:
delegate void SetTextCallback(string text);
// This thread is used to demonstrate both thread-safe and
// unsafe ways to call a Windows Forms control.
private Thread demoThread = null;
I aggiungere una procedura per aggiornare la casella di testo:
private void SetText(string text)
{
// InvokeRequired required compares the thread ID of the
// calling thread to the thread ID of the creating thread.
// If these threads are different, it returns true.
if (this.txtOutput.InvokeRequired)
{
SetTextCallback d = new SetTextCallback(SetText);
this.Invoke(d, new object[] { text });
}
else
{
this.txtOutput.Text = text;
}
}
aggiunge un'altra proc che richiama il filo-safe uno:
private void ThreadProcSafe()
{
// this.SetText(sb3.ToString());
this.SetText("foo");
}
... e alla fine io chiamo questo pasticcio all'interno cmd_DataReceived come questo:
private void cmd_DataReceived(object sender, DataReceivedEventArgs e)
{
//MessageBox.Show("Output from other process");
try
{
sb3.AppendLine(e.Data.ToString());
//this.backgroundWorker2.RunWorkerAsync();
this.demoThread = new Thread(new ThreadStart(this.ThreadProcSafe));
this.demoThread.Start();
Console.WriteLine(e.Data.ToString());
}
catch (Exception ex)
{
Console.WriteLine("{0} Exception caught.", ex);
}
}
... Quando ho eseguito questo testo, la casella di testo sta lì morto stecchito, non sempre aggiornato. La mia finestra della console continua ad aggiornarsi. Come potete vedere, ho provato a semplificare un po 'le cose semplicemente facendo in modo che la casella di testo visualizzasse "foo" rispetto all'output reale dello strumento, ma non alla gioia. La mia interfaccia utente è morta.
Quindi cosa dà? Non riesco a capire cosa sto facendo male. Non sono affatto sposato a mostrare risultati in una casella di testo, btw - Devo solo essere in grado di vedere cosa succede dentro l'applicazione e preferirei non aprire un'altra finestra per farlo.
Molte grazie.
Ma la casella di testo non verrà ancora aggiornata, perché il thread dell'interfaccia utente è bloccato dalla riga 'cmdProcess.WaitForExit();'. – Fischermaen
Grazie, proverò a combinare il tuo suggerimento con quello di Fisherman e KooKiz. –