UI 对长时间的 for 循环没有反应

UI unresposive on a long-time for loop

我有一个 WinForm 应用程序,它使用 Spreadsheetlight 从 excel 文件中读取数据,有一种方法可以读取信息然后进行一些计算,然后我创建一个新的 excel 文件。

问题是这个文件有大约 100,000 条记录,我在 UI 中有一个标签,我想在每次读取 excel 行时更新它,但标签似乎没有响应;

 public List<DataRow> GeExcelData(string filePath)
 {
    var myTable = new List<DataRow>();
    int row = 0;

    using (var input = new SLDocument(filePath))
    {
        SLWorksheetStatistics stats = input.GetWorksheetStatistics();
        int iStartColumnIndex = stats.StartColumnIndex;

        progressBar1.Minimum = 0;
        progressBar1.Maximum = stats.EndRowIndex;
        progressBar1.Step = 1;

        for (row = stats.StartRowIndex + 1; row <= stats.EndRowIndex; ++row)
        {
            //Here is the label
            lblStatus.Text = "Reading row " + row + " of " + stats.EndRowIndex;

            var dataRowTmp = new DataRow()
            {
                name = input.GetCellValueAsString(row, iStartColumnIndex),
                sku = input.GetCellValueAsString(row, iStartColumnIndex + 1),
                value2 = input.GetCellValueAsString(row, iStartColumnIndex + 2),
                value3 = input.GetCellValueAsString(row, iStartColumnIndex + 3)
            };

            progressBar1.PerformStep();
            myTable.Add(dataRowTmp);
        }

        lblStatus.Text = "";
        progressBar1.Value = 0;
    }

    return myTable;
}

有没有办法在执行一些长 运行 任务的同时有效地更新 UI。 因为只是一个标签我不想写一些复杂的代码。 我也有工作正常的 progressBar1。

您需要在后台线程上执行处理并使用进度信息更新标签直到完成。尝试创建一个 BackgroundThread 实例,然后在线程处理中执行循环。为了更新进度标签,您可以传入 SynchronizationContext 并使用它来 post 返回 UI 线程上的更新调用。

试试这段代码,声明将在主线程上更新控件的委托方法,请参阅下面的实现,

delegate void SetStatus(string value);
delegate void SetProgress(int value);

启动一个线程来处理数据并更新控件,

Thread t  = new Thread(StartProc);
t.Start();


public void StartProc()
{
  using (var input = new SLDocument(filePath))
  {
    SLWorksheetStatistics stats = input.GetWorksheetStatistics();

    int iStartColumnIndex = stats.StartColumnIndex;
    for (row = stats.StartRowIndex + 1; row <= stats.EndRowIndex; ++row)
    {
        int percent = (row+1)*100/stats.EndRowIndex;
        //Here is the label
        setStatus("Reading row " + row + " of " + stats.EndRowIndex);
        var dataRowTmp = new DataRow()
        {
            name = input.GetCellValueAsString(row, iStartColumnIndex),
            sku = input.GetCellValueAsString(row, iStartColumnIndex + 1),
            value2 = input.GetCellValueAsString(row, iStartColumnIndex + 2),
            value3 = input.GetCellValueAsString(row, iStartColumnIndex + 3)
        };
        setProgress(percent);
        myTable.Add(dataRowTmp);

    }
    setStatus("");
    setProgress(0);
  }
}
private void SetStatus(string value)
{
  if (this.InvokeRequired)
  {
    SetStatusDelg dlg = new SetStatusDelg(this.SetStatus);
    this.Invoke(value);
    return;
  }
  lblStatus.Text = value;
}
private void SetProgress(int value)
{
  if (this.InvokeRequired)
  {
    SetProgressDelg dlg = new SetProgressDelg(this.SetStatus);
    this.Invoke(value);
    return;
  }
  progressBar1.value = value;
}

最好将 UI 和数据处理代码分开。您可以通过使用 TaskProgress<T> 来解决这个问题以及 UI 阻塞问题。不幸的是,你没有提供完整的代码示例,所以我不知道你是如何调用这个方法的。但我将提供一个基本模板,您可以修改它以适合您的实际情况,假设操作是从单击 UI:

中的某个按钮开始的
private async void button1_Click(object sender, EventArgs e)
{
    Progress<Tuple<int, int>> progress = new Progress<Tuple<int, int>>();

    progressBar1.Minimum = 0;
    progressBar1.Maximum = stats.EndRowIndex;
    progressBar1.Step = 1;

    progress.ProgressChanged += (sender, progressInfo) =>
    {
        lblStatus.Text = "Reading row " + progressInfo.Item1 + " of " + progressInfo.Item2;
        progressBar1.PerformStep();
    };

    // I don't know where filePath comes form...initialize to suit your needs
    string filePath = ...;

    List<DataRow> table = await Task.Run(() => GeExcelData(filePath, progress));

    lblStatus.Text = "";
    progressBar1.Value = 0;

    // do something with table
}

public List<DataRow> GeExcelData(string filePath, IProgress<Tuple<int, int>> progress)
{
    var myTable = new List<DataRow>();
    int row = 0;

    using (var input = new SLDocument(filePath))
    {
        SLWorksheetStatistics stats = input.GetWorksheetStatistics();
        int iStartColumnIndex = stats.StartColumnIndex;

        for (row = stats.StartRowIndex + 1; row <= stats.EndRowIndex; ++row)
        {
            progress.Report(Tuple.Create(row, stats.EndRowIndex));
            var dataRowTmp = new DataRow()
            {
                name = input.GetCellValueAsString(row, iStartColumnIndex),
                sku = input.GetCellValueAsString(row, iStartColumnIndex + 1),
                value2 = input.GetCellValueAsString(row, iStartColumnIndex + 2),
                value3 = input.GetCellValueAsString(row, iStartColumnIndex + 3)
            };

            myTable.Add(dataRowTmp);
        }
    }

    return myTable;
}

以上完成了几件事:

  • 它从实际执行工作的方法中删除了所有 UI 特定代码。这样可以保证更好"separation of concerns",让代码更简单,更容易维护。
  • 它创建一个 Progress<T> 对象,用于将工作方法的进度传达给调用者。请注意,可以以多种方式使用此进度信息;它不是 UI 特定的。这样做允许调用代码接收进度信息并根据需要进行处理,而无需使工作方法担心细节。
  • Progress<T> class 不仅提供有用的 shim 来报告进度。它包括将进度报告正确调用回原始 UI 线程所需的逻辑,其中可以安全地使用 UI 个对象。
  • 它使用awaitTask实际上是运行的工作方法。 await 方法将导致 button1_Click() 方法在该点变为 return,注册代表该方法其余部分的 "continuation"。当工作方法完成时,将使用原始 IO 线程执行该延续,将工作方法的结果分配给 table 变量。

这可以用来更新标签值。

lblStatus.Refresh();

不过,我更希望它在后台线程上完成。