提问人:MatrixRonny 提问时间:2/8/2018 更新时间:2/8/2018 访问量:2004
WPF ViewModel 在另一个线程中运行方法,报告进度,更改 Button 的 IsEnabled 并显示结果
WPF ViewModel running method in another thread, reporting progress, changing Button's IsEnabled and displaying result
问:
我正在尝试找到一种使用在 ViewModel 中创建的线程运行现有方法的正确方法。主要目的是提供响应式 UI。我决定使用基于任务的异步模式,但我需要将其与 WPF 和 MVVM 正确集成。
到目前为止,我找到了一种方法,可以在另一个线程中运行冗长的任务并报告其进度。但是,我找不到更新启动任务的 Button 的方法,以便仅在任务未运行时才启用它。下面的 ViewModel 描述了我所做的工作:
public class MainViewModel : INotifyPropertyChanged
{
public void NotifyPropertyChanged(string info)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
public event PropertyChangedEventHandler PropertyChanged;
// Do some time consuming work.
int SomeTask()
{
//SCENARIO: Consider that it takes longer than usual to start the worker thread.
Thread.Sleep(1000);
// Prevent executing the task by two threads at the same time.
lock ("IsReady")
{
if (IsReady == false)
throw new ApplicationException("Task is already running");
IsReady = false;
}
// The actual work that this task consists of.
TaskProgress = 0;
for (int i = 1; i <= 100; i++)
{
Thread.Sleep(50);
TaskProgress = i;
}
// Mark task as completed to allow rerunning it.
IsReady = true;
return 123;
}
// True when not started or completed.
bool _isReady = true;
public bool IsReady
{
get { return _isReady; }
set
{
_isReady = value;
NotifyPropertyChanged("IsReady");
StartTaskCommand.RaiseCanExecuteChanged();
}
}
// Indicate the current progress when running SomeTask.
int _taskProgress;
public int TaskProgress
{
get { return _taskProgress; }
set
{
_taskProgress = value;
NotifyPropertyChanged("TaskProgress");
}
}
// ICommand to start task asynchronously.
RelayCommand _startTask;
public RelayCommand StartTaskCommand
{
get
{
if (_startTask == null)
{
_startTask = new RelayCommand(
obj =>
{
Task<int> task = Task.Run((Func<int>)SomeTask);
task.ContinueWith(t =>
{
// SomeTask method may throw an ApplicationException.
if (!t.IsFaulted)
Result = t.Result.ToString();
});
},
obj => IsReady);
}
return _startTask;
}
}
string _result;
public string Result
{
get { return _result; }
set { _result = value; NotifyPropertyChanged("Result"); }
}
}
我使用以下 RelayCommand 实现:
public class RelayCommand : ICommand
{
private Action<object> execute;
private Func<object, bool> canExecute;
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public void RaiseCanExecuteChanged()
{
CommandManager.InvalidateRequerySuggested();
}
public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
{
this.execute = execute;
this.canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return this.canExecute == null || this.canExecute(parameter);
}
public void Execute(object parameter)
{
this.execute(parameter);
}
}
主要问题是执行命令的 Button 不会根据 更新其状态。我也尝试用 显式设置它,但它仍然不起作用。我发现与此问题相关的最好的文章是:Raising CanExecuteChanged。IsReady
IsEnabled="{Binding IsReady}"
XAML 非常简单:
<DockPanel Margin="4">
<TextBox DockPanel.Dock="Right" Text="{Binding Result}" Width="100"/>
<Button DockPanel.Dock="Right" Content="Start" Margin="5,0"
Command="{Binding StartTaskCommand}"/>
<ProgressBar Value="{Binding TaskProgress}"/>
</DockPanel>
如何修复 IsReady 以反映在 Button 的状态中?
有没有人推荐一个简约的工作实现来满足我的需求?
感谢您抽出宝贵时间阅读。
答:
1赞
taquion
2/8/2018
#1
您必须在 UI 线程上更新 IsRady 标志。我修改您的示例以实现预期行为:
int SomeTask()
{
// Prevent executing the task by two threads at the same time.
lock ("IsReady")
{
if (IsReady == false)
throw new ApplicationException("Task is already running");
Application.Current.Dispatcher.Invoke(() => { IsReady = false; });
}
//SCENARIO: Consider that it takes longer than usual to start the worker thread.
Thread.Sleep(1000);
// The actual work that this task consists of.
TaskProgress = 0;
for (int i = 1; i <= 100; i++)
{
Thread.Sleep(50);
TaskProgress = i;
}
// Mark task as completed to allow rerunning it.
Application.Current.Dispatcher.Invoke(() => { IsReady = true; });
return 123;
}
您从其他线程触发了 PropertyChanged 事件,这就是问题所在。
我还把线程放在锁下面,因为我得到了你的 ApplicationExceptions,哈哈
编辑
由于您正在引发 canExecuteChanged,您也可以通过在 UI 线程上引发该事件来解决此问题:
public bool IsReady
{
get { return _isReady; }
set
{
_isReady = value;
NotifyPropertyChanged("IsReady");
Application.Current.Dispatcher.Invoke(() =>
{
StartTaskCommand.RaiseCanExecuteChanged();
});
}
}
评论
0赞
MatrixRonny
2/8/2018
谢谢你的回答。事实上,这就是问题所在。我试图重构您的魔术代码,以便在 IsReady setter 中使用它一次,但它不起作用。知道为什么吗?另外,我无法弄清楚为什么TaskProgress没有遇到这个问题并且工作正常。Application.Current.Dispatcher.Invoke(() => NotifyPropertyChanged("IsReady"))
0赞
taquion
2/8/2018
@MatrixRonny,对不起,我以为按钮的 IsEnabled 已绑定,但您正在从 setter 中获取 canExecuteChanged。我已经编辑了答案以显示替代解决方案
0赞
MatrixRonny
2/8/2018
是的,我想出了相同的 IsReady 版本。还有一件事,如果我绑定,有没有更简单的方法?我读到我在 RaiseCanExecuteChanged 中使用的 InvalidateRequerySuggested 是一项成本相当高昂的操作。IsEnabled="{Binding IsReady}"
3赞
mm8
2/8/2018
#2
在启动任务之前,将属性设置为 UI 线程上,然后将其设置回任务完成后:IsReady
false
true
public RelayCommand StartTaskCommand
{
get
{
if (_startTask == null)
{
_startTask = new RelayCommand(
obj =>
{
if (IsReady)
{
//1. Disable the button on the UI thread
IsReady = false;
//2. Execute SomeTask on a background thread
Task.Factory.StartNew(SomeTask)
.ContinueWith(t =>
{
//3. Enable the button back on the UI thread
if (!t.IsFaulted)
Result = t.Result.ToString();
IsReady = true;
}, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.FromCurrentSynchronizationContext());
}
},
obj => IsReady);
}
return _startTask;
}
}
int SomeTask()
{
Thread.Sleep(1000);
TaskProgress = 0;
for (int i = 1; i <= 100; i++)
{
Thread.Sleep(50);
TaskProgress = i;
}
return 123;
}
评论
0赞
MatrixRonny
2/8/2018
这是摆脱 lock(“IsSelected”) 的一种更优雅的方法,因为 IsReady 属性是在创建线程之前设置的。有一个小问题,按钮在完成后无法重新启用。我错过了什么吗?我签入了调试器,确实 IsReady 从主线程设置为 true。
0赞
mm8
2/8/2018
在 ContinueWith 中将 IsReady 属性设置为 true 后,是否调用了 IsReady 属性的 getter?
0赞
MatrixRonny
2/8/2018
是的,它是。它由 NotifyPropertyChanged 调用,也来自 Main Thread。我头晕吗?
0赞
MatrixRonny
2/8/2018
它是在 VS 立即切换到 MainWindow 并再次切换回来之后调用的,所以我想这是由于窗口焦点的变化。我试过有和没有.无论哪种方式都不起作用。IsEnabled="{Binding IsReady}"
0赞
mm8
2/8/2018
VS与此有何关系?如果在未连接调试器的情况下运行它,它是否按预期工作?
评论