通过C#更新UI线程(文本框)

5

我是一名业务智能专家,具有足够的C#技术知识。我构建了一个自制的winforms应用程序,基本上执行一个命令行工具循环进行“操作”。 这些操作可能在几秒钟或几分钟内完成。通常,我需要为DataTable中的每一行执行一次该工具。

我需要重定向命令行工具的输出并在“我的”应用程序中显示它。我试图通过文本框来实现。但我遇到了更新UI线程的问题,无法自己解决。

为了执行我的命令行工具,我从这里借鉴了代码:如何解析C#中的命令行输出?

以下是我的等效代码:

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

            }

        }

当我在cmd_DataReceived()中实例化我的StringBuilder时,引用txtOuput.text会导致应用程序停滞不前:我猜测是某种跨线程问题。

如果我从StringBuilder中删除对txtOuput.text的引用并继续调试,则会出现跨线程违规:

txtOutput.Text = sb.ToString();

Cross-thread operation not valid: Control 'txtOutput' accessed from a thread other than the thread it was created on.

好的,不意外。我假设cmd_DataReceived正在另一个线程上运行,因为我是在Process.Start()后执行一些操作后才触发它... 如果我删除cmd_DataReceived()中所有对txtOuput.Text的引用,并只通过Console.Write()将命令行文本输出到控制台,那么一切都可以正常工作。
所以,接下来我打算尝试使用http://msdn.microsoft.com/en-us/library/ms171728.aspx中的信息,使用标准技术在UI线程上更新我的TextBox。
我向我的类添加了一个委托和线程:
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;

我添加了一个更新文本框的步骤:
 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;
        }
    }

我添加了另一个过程,调用了线程安全的过程:
 private void ThreadProcSafe()
    {
       // this.SetText(sb3.ToString());
        this.SetText("foo");

    }

最后,我在cmd_DataReceived中调用这个混乱的东西:
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);


    }

}

当我运行这段文本时,文本框却像一块死板木头一样不更新。我的控制台窗口继续更新。你可以看到,我试图简化一下,只是让文本框显示“foo”而不是工具的实际输出 - 但是没有成功。我的用户界面已经无响应了。
那么出了什么问题呢?我想不出我做错了什么。顺便说一句,我并不一定非要在文本框中显示结果 - 我只需要能够看到应用程序内部发生了什么,最好不要弹出另一个窗口来实现。
非常感谢。
3个回答

4

我认为问题出在这一行代码中:

cmdProcess.WaitForExit(); 

这段话涉及到IT技术,具体是在ImportWorkbooks方法中被调用,该方法被btnImport的Click事件方法调用。因此,在后台处理完成之前,您的UI线程将被阻塞。


3
你正在从UI线程调用ImportWorkbooks。然后,在此方法中,你调用了"cmdProcess.WaitForExit()"。因此基本上你会阻塞UI线程,直到进程执行完毕。请在一个线程中执行ImportWorkbooks,这样它应该可以工作,或者删除WaitForExit并使用进程的“Exited”事件代替。

谢谢,KooKiz!我能否使用与cmd_DataReceived()相同的基本技术,在不同的线程上启动ImportWorkbooks()?基本上是Thread.Start()吗? - Russell Christopher
@RussellChristopher 是的,由于ImportWorkbooks不与UI交互,您可以在“简单”线程中执行它,使用Thread.Start。 - Kevin Gosse

0

你的文本框没有更新的原因之一是因为你没有将字符串传递给SetText方法。

你不需要创建一个线程。你的SetText实现将处理从工作线程(其中调用cmd_DataReceived)传递调用到UI线程。

这是我建议你做的:

private void cmd_DataReceived(object sender, DataReceivedEventArgs e)
{
    //MessageBox.Show("Output from other process");
    try
    {


        string str = e.Data.ToString();
        sb3.AppendLine(str);
        SetText(str); //or use sb3.ToString if you need the entire thing   

        Console.WriteLine(str);

    }
    catch (Exception ex)
    {
         Console.WriteLine("{0} Exception caught.", ex);


    }

}

此外,当您调用WaitForExit时,您会像@Fischermaen所提到的那样阻塞UI线程,因此您不需要它。

我还建议您在工作线程上运行ImportWorkbooks,如下所示: (如果这样做,您可以保留对WaitForExit的调用)

private void btnImport_Click(object sender, EventArgs e)
{
     txtOutput.Clear();
     ThreadPool.QueueUserWorkItem(ImportBooksHelper, dtTable);
}

private ImportBooksHelper(object obj)
{
    DataTable dt = (DataTable)obj;
    ImportWorkbooks(dtable);
}

但是文本框仍然无法更新,因为UI线程被“cmdProcess.WaitForExit();”这行代码阻塞了。 - Fischermaen
谢谢,我会尝试将您的建议与Fisherman和KooKiz的建议结合起来。 - Russell Christopher

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接