【问题标题】:Updating UI thread (textbox) via C#通过 C# 更新 UI 线程(文本框)
【发布时间】:2011-11-25 13:25:22
【问题描述】:

这里的商业智能家伙拥有足够多的 C# 来很危险。

我已经构建了一个自制的 winforms 应用程序,它本质上是在循环中执行一个命令行工具来“做事”。所说的东西可能在几秒钟或几分钟内完成。通常,我需要为 DataTable 中的每一行执行一次该工具。

我需要重定向命令行工具的输出并将其显示在“我的”应用程序中。我正在尝试通过文本框这样做。我在更新 UI 线程时遇到了一些我自己无法解决的问题。

为了执行我的命令行工具,我从这里借用了代码:How to parse command line output from 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”与工具的实际输出来稍微简化一些事情 - 但没有乐趣。我的用户界面已经死了。

那是什么?无法弄清楚我做错了什么。顺便说一句,我根本不喜欢在文本框中显示结果 - 我只需要能够看到应用程序内部发生的事情,而且我不想弹出另一个窗口来这样做。

非常感谢。

【问题讨论】:

    标签: c# user-interface multithreading


    【解决方案1】:

    我认为问题出在这一行:

    cmdProcess.WaitForExit(); 
    

    它在方法ImportWorkbooks 中,从btnImport 的Click 事件方法中调用。所以你的 UI 线程会被阻塞,直到后台进程完成。

    【讨论】:

      【解决方案2】:

      您正在从 UI 线程调用 ImportWorkbooks。然后,在此方法中,您将调用“cmdProcess.WaitForExit()”。所以基本上你会阻塞 UI 线程,直到进程完成执行。从线程执行 ImportWorkbooks 应该可以工作,或者删除 WaitForExit 并改用进程的“Exited”事件。

      【讨论】:

      • 谢谢,KooKiz! - 我可以使用与 cmd_DataReceived() 中相同的基本技术在不同的线程上启动 ImportWorkbooks() 吗? Thread.Start(),本质上是什么?
      • @RussellChristopher 是的,由于 ImportWorkbooks 不与 UI 交互,您可以使用 Thread.Start 在“简单”线程中执行它。
      【解决方案3】:

      您的文本框不更新的原因之一是您没有将字符串传递给您的 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);
      
      
          }
      
      }
      

      此外,正如@Fischermaen 在调用 WaitForExit 时提到的那样,您正在阻塞 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();这行阻塞了。
      • 谢谢,我会尝试将您的建议与 Fisherman 和 KooKiz 的建议结合起来。
      猜你喜欢
      • 2015-01-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-11-16
      • 2015-06-29
      • 1970-01-01
      • 1970-01-01
      • 2023-04-02
      相关资源
      最近更新 更多