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 和数据处理代码分开。您可以通过使用 Task
和 Progress<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 个对象。
- 它使用
await
和Task
实际上是运行的工作方法。 await
方法将导致 button1_Click()
方法在该点变为 return,注册代表该方法其余部分的 "continuation"。当工作方法完成时,将使用原始 IO 线程执行该延续,将工作方法的结果分配给 table
变量。
这可以用来更新标签值。
lblStatus.Refresh();
不过,我更希望它在后台线程上完成。
我有一个 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 和数据处理代码分开。您可以通过使用 Task
和 Progress<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 个对象。 - 它使用
await
和Task
实际上是运行的工作方法。await
方法将导致button1_Click()
方法在该点变为 return,注册代表该方法其余部分的 "continuation"。当工作方法完成时,将使用原始 IO 线程执行该延续,将工作方法的结果分配给table
变量。
这可以用来更新标签值。
lblStatus.Refresh();
不过,我更希望它在后台线程上完成。