如何使用 MVVM / ICommand 模式在 WPF 中正确实现 BackgroundWorker
How to Properly Implement BackgroundWorker in WPF with MVVM / ICommand Pattern
我有一个主要按照 MVVM 模式编写的小型 WPF 应用程序。该程序的要点是读取文本文件的行,从中解析数据,将该数据写入对象列表,然后将这些对象中的数据写入特定格式的 .CSV 文件。
尽管我已经实现了 BackgroundWorker class 本身,但这次我是从我的 ICommand 的 Execute() 方法中调用 RunWorkAsync() 方法。虽然最终输出是正确的并且应用程序实际提供了所需的结果,但 UI 仍然在 BackgroundWorker 处于 运行.
时锁定
我已经将我的 BackgroundWorker 成员和所有逻辑包装在一个名为 "ReaderWriter" 的 class 中,并使用一个将我的 ViewModel 作为参数的构造函数。
通过调用我的 ReaderWriter 实例的 "StartProcess" 方法,BackgroundWorker 的 RunWorkerAsync() 被调用——这是我希望它接管另一个线程并执行我的 long-运行 读取源文件、解析数据、写入新文件的过程;一直在定期执行 ReportProgress() 以更新 ProgressBar。
这是我的 ReaderWriter 的代码 class:
class ReaderWriter
{
private LogDataViewModel vm { get; set; }
private BackgroundWorker bw { get; set; }
public ReaderWriter(LogDataViewModel viewModel)
{
vm = viewModel;
}
public void StartProcess()
{
bw = new BackgroundWorker();
bw.WorkerReportsProgress = true;
bw.WorkerSupportsCancellation = true;
bw.DoWork += new DoWorkEventHandler(ReadFromSource);
bw.ProgressChanged += new ProgressChangedEventHandler(UpdateProgress_Read);
bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(Completed_Read);
bw.RunWorkerAsync();
}
private void ReadFromSource(object sender, DoWorkEventArgs e)
{
double records = 0;
string[] lines = File.ReadAllLines(vm.SourcePath);
int lineCount = lines.Length;
double currentLine = 0;
bw.ReportProgress(0, lineCount);
foreach (var line in lines)
{
if (line.Length > 0)
{
string syntax = line.Substring(17, 6);
switch (syntax)
{
case "$WIMDA":
string[] segments = line.Replace(": <- ", ",").Split(',');
vm.LineItems.Add(new LineItem()
{
Time = segments[0],
HgPressure = segments[2],
BarPressure = segments[4],
AirTemp = segments[6],
RelHumidity = segments[10],
TrueWindDir = segments[14],
KnotsWindSpeed = segments[18],
MpsWindSpeed = segments[20]
});
break;
case "$GPGGA":
break;
default:
break;
}
}
currentLine++;
bw.ReportProgress(1, currentLine);
}
using (StreamWriter writer = new StreamWriter(vm.OutputPath))
{
writer.WriteLine($"Time,Pressure(Bar),Pressure(Hg),AirTemp({((vm.ConvertTempSetting) ? "F" : "C")}),RelativeHumidity,TrueWindDirection,WindSpeed(Knots),WindSpeed(M/s)");
foreach (var lineItem in vm.LineItems)
{
writer.WriteLine($"{lineItem.Time},{lineItem.BarPressure},{lineItem.HgPressure},{((vm.ConvertTempSetting) ? Converters.ConvertFromCelcius(Convert.ToDouble(lineItem.AirTemp)).ToString() : lineItem.AirTemp)},{lineItem.RelHumidity},{lineItem.TrueWindDir},{lineItem.KnotsWindSpeed},{lineItem.MpsWindSpeed}");
records++;
}
}
e.Result = records;
}
private void UpdateProgress_Read(object sender, ProgressChangedEventArgs e)
{
vm.IncrementProgress();
switch (Type.GetTypeCode(e.UserState.GetType()))
{
case TypeCode.Double:
vm.IncrementProgress();
break;
case TypeCode.String:
break;
case TypeCode.Int32:
vm.AppendStatus(DateTime.Now, $"{(int)e.UserState} lines parsed from log file");
break;
default:
break;
}
if (vm.IsFirst)
{
vm.ProgressIsVisible = true;
vm.IncrementProgress();
vm.SetMaximum((int)e.UserState);
vm.IsFirst = false;
}
}
private void Completed_Read(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Cancelled)
{
vm.AppendStatus(DateTime.Now, $"Conversion was cancelled by user");
}
else
{
vm.AppendStatus(DateTime.Now, $"{(double)e.Result} records written to {vm.OutputPath}");
}
vm.LineItems.Clear();
}
}
对于我的 ViewModel:
public class LogDataViewModel : LogDataModel
{
#region Commands
public BeginProcessCommand BeginCommand { get; set; }
public SelectOutputPathCommand OutputCommand { get; set; }
public SelectSourceCommand SourceCommand { get; set; }
public ResetCommand ResetCommand { get; set; }
#endregion
public bool IsFirst { get; set; }
public LogDataViewModel()
{
BeginCommand = new BeginProcessCommand(this);
OutputCommand = new SelectOutputPathCommand(this);
SourceCommand = new SelectSourceCommand(this);
ResetCommand = new ResetCommand(this);
PrepareViewModel();
}
private void PrepareViewModel()
{
ProgressValue = 0;
ProgressMaximum = 0;
ProgressIsVisible = false;
IsFirst = true;
OutputPath = Properties.Settings.Default.RememberedSavePath;
if (LineItems == null) LineItems = new List<LineItem>();
if (StatusActions == null) StatusActions = new ObservableCollection<StatusAction>();
AppendStatus(DateTime.Now, "Initialized Program");
}
}
最后,这是命令:
public class BeginProcessCommand : ICommand
{
LogDataViewModel vm;
public BeginProcessCommand(LogDataViewModel viewModel)
{
vm = viewModel;
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public bool CanExecute(object parameter)
{
bool result = true;
if (!File.Exists(vm.SourcePath))
result = false;
try
{
if (!Directory.Exists(Path.GetDirectoryName(vm.SourcePath)))
result = false;
}
catch
{
result = false;
}
return result;
}
public void Execute(object parameter)
{
ReaderWriter rw = new ReaderWriter(vm);
rw.StartProcess();
}
}
非常感谢此时的任何帮助,因为我已经为此苦苦挣扎了一段时间,任何研究解决方案的尝试都对我的特殊情况没有帮助。这似乎是一个相当独特的场景,但我希望我们能够让它发挥作用。
谢谢!
您使用 ReportProgress 不正确且过于频繁(在文件的每一行)。它将受到重击,每次调用都会在您的 UI 中引起某种更新,因此将其锁定。
ReportProgress 传递一个百分比可能是最容易使用的。我不太确定你在 UpdateProgress_Read 中用开关做什么。最好只在您通过总行数的第 100 行时更新。
将您的进度条最大值设置为 100
ProgressMaximum = 100;
计算总行数的 1%
var x = lineCount / 100;
var y = 0;
并且仅在每通过 1% 时报告进度
currentLine++;
if((currentLine % x) == 0)
{
y++;
bw.ReportProgress(y);
}
并更改 UpdateProgress_Read 使其仅递增
private void UpdateProgress_Read(object sender, ProgressChangedEventArgs e)
{
vm.IncrementProgress();
}
你需要想出比 x 和 y 更好的变量名!如果文件中的行数少于 100 行,还要计算出该怎么做。
我有一个主要按照 MVVM 模式编写的小型 WPF 应用程序。该程序的要点是读取文本文件的行,从中解析数据,将该数据写入对象列表,然后将这些对象中的数据写入特定格式的 .CSV 文件。
尽管我已经实现了 BackgroundWorker class 本身,但这次我是从我的 ICommand 的 Execute() 方法中调用 RunWorkAsync() 方法。虽然最终输出是正确的并且应用程序实际提供了所需的结果,但 UI 仍然在 BackgroundWorker 处于 运行.
时锁定我已经将我的 BackgroundWorker 成员和所有逻辑包装在一个名为 "ReaderWriter" 的 class 中,并使用一个将我的 ViewModel 作为参数的构造函数。
通过调用我的 ReaderWriter 实例的 "StartProcess" 方法,BackgroundWorker 的 RunWorkerAsync() 被调用——这是我希望它接管另一个线程并执行我的 long-运行 读取源文件、解析数据、写入新文件的过程;一直在定期执行 ReportProgress() 以更新 ProgressBar。
这是我的 ReaderWriter 的代码 class:
class ReaderWriter
{
private LogDataViewModel vm { get; set; }
private BackgroundWorker bw { get; set; }
public ReaderWriter(LogDataViewModel viewModel)
{
vm = viewModel;
}
public void StartProcess()
{
bw = new BackgroundWorker();
bw.WorkerReportsProgress = true;
bw.WorkerSupportsCancellation = true;
bw.DoWork += new DoWorkEventHandler(ReadFromSource);
bw.ProgressChanged += new ProgressChangedEventHandler(UpdateProgress_Read);
bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(Completed_Read);
bw.RunWorkerAsync();
}
private void ReadFromSource(object sender, DoWorkEventArgs e)
{
double records = 0;
string[] lines = File.ReadAllLines(vm.SourcePath);
int lineCount = lines.Length;
double currentLine = 0;
bw.ReportProgress(0, lineCount);
foreach (var line in lines)
{
if (line.Length > 0)
{
string syntax = line.Substring(17, 6);
switch (syntax)
{
case "$WIMDA":
string[] segments = line.Replace(": <- ", ",").Split(',');
vm.LineItems.Add(new LineItem()
{
Time = segments[0],
HgPressure = segments[2],
BarPressure = segments[4],
AirTemp = segments[6],
RelHumidity = segments[10],
TrueWindDir = segments[14],
KnotsWindSpeed = segments[18],
MpsWindSpeed = segments[20]
});
break;
case "$GPGGA":
break;
default:
break;
}
}
currentLine++;
bw.ReportProgress(1, currentLine);
}
using (StreamWriter writer = new StreamWriter(vm.OutputPath))
{
writer.WriteLine($"Time,Pressure(Bar),Pressure(Hg),AirTemp({((vm.ConvertTempSetting) ? "F" : "C")}),RelativeHumidity,TrueWindDirection,WindSpeed(Knots),WindSpeed(M/s)");
foreach (var lineItem in vm.LineItems)
{
writer.WriteLine($"{lineItem.Time},{lineItem.BarPressure},{lineItem.HgPressure},{((vm.ConvertTempSetting) ? Converters.ConvertFromCelcius(Convert.ToDouble(lineItem.AirTemp)).ToString() : lineItem.AirTemp)},{lineItem.RelHumidity},{lineItem.TrueWindDir},{lineItem.KnotsWindSpeed},{lineItem.MpsWindSpeed}");
records++;
}
}
e.Result = records;
}
private void UpdateProgress_Read(object sender, ProgressChangedEventArgs e)
{
vm.IncrementProgress();
switch (Type.GetTypeCode(e.UserState.GetType()))
{
case TypeCode.Double:
vm.IncrementProgress();
break;
case TypeCode.String:
break;
case TypeCode.Int32:
vm.AppendStatus(DateTime.Now, $"{(int)e.UserState} lines parsed from log file");
break;
default:
break;
}
if (vm.IsFirst)
{
vm.ProgressIsVisible = true;
vm.IncrementProgress();
vm.SetMaximum((int)e.UserState);
vm.IsFirst = false;
}
}
private void Completed_Read(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Cancelled)
{
vm.AppendStatus(DateTime.Now, $"Conversion was cancelled by user");
}
else
{
vm.AppendStatus(DateTime.Now, $"{(double)e.Result} records written to {vm.OutputPath}");
}
vm.LineItems.Clear();
}
}
对于我的 ViewModel:
public class LogDataViewModel : LogDataModel
{
#region Commands
public BeginProcessCommand BeginCommand { get; set; }
public SelectOutputPathCommand OutputCommand { get; set; }
public SelectSourceCommand SourceCommand { get; set; }
public ResetCommand ResetCommand { get; set; }
#endregion
public bool IsFirst { get; set; }
public LogDataViewModel()
{
BeginCommand = new BeginProcessCommand(this);
OutputCommand = new SelectOutputPathCommand(this);
SourceCommand = new SelectSourceCommand(this);
ResetCommand = new ResetCommand(this);
PrepareViewModel();
}
private void PrepareViewModel()
{
ProgressValue = 0;
ProgressMaximum = 0;
ProgressIsVisible = false;
IsFirst = true;
OutputPath = Properties.Settings.Default.RememberedSavePath;
if (LineItems == null) LineItems = new List<LineItem>();
if (StatusActions == null) StatusActions = new ObservableCollection<StatusAction>();
AppendStatus(DateTime.Now, "Initialized Program");
}
}
最后,这是命令:
public class BeginProcessCommand : ICommand
{
LogDataViewModel vm;
public BeginProcessCommand(LogDataViewModel viewModel)
{
vm = viewModel;
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public bool CanExecute(object parameter)
{
bool result = true;
if (!File.Exists(vm.SourcePath))
result = false;
try
{
if (!Directory.Exists(Path.GetDirectoryName(vm.SourcePath)))
result = false;
}
catch
{
result = false;
}
return result;
}
public void Execute(object parameter)
{
ReaderWriter rw = new ReaderWriter(vm);
rw.StartProcess();
}
}
非常感谢此时的任何帮助,因为我已经为此苦苦挣扎了一段时间,任何研究解决方案的尝试都对我的特殊情况没有帮助。这似乎是一个相当独特的场景,但我希望我们能够让它发挥作用。
谢谢!
您使用 ReportProgress 不正确且过于频繁(在文件的每一行)。它将受到重击,每次调用都会在您的 UI 中引起某种更新,因此将其锁定。
ReportProgress 传递一个百分比可能是最容易使用的。我不太确定你在 UpdateProgress_Read 中用开关做什么。最好只在您通过总行数的第 100 行时更新。
将您的进度条最大值设置为 100
ProgressMaximum = 100;
计算总行数的 1%
var x = lineCount / 100;
var y = 0;
并且仅在每通过 1% 时报告进度
currentLine++;
if((currentLine % x) == 0)
{
y++;
bw.ReportProgress(y);
}
并更改 UpdateProgress_Read 使其仅递增
private void UpdateProgress_Read(object sender, ProgressChangedEventArgs e)
{
vm.IncrementProgress();
}
你需要想出比 x 和 y 更好的变量名!如果文件中的行数少于 100 行,还要计算出该怎么做。