使用MVVM在一个WPF事件中观察属性的多次变化
本文关键字:属性 观察 变化 事件 WPF MVVM 一个 使用 | 更新日期: 2023-09-27 18:18:38
我想做一个代价高昂的操作,并将一个方法在操作状态下的位置发回给用户。基本上,我使用MVVM将iccommand绑定到按钮单击事件。该事件为用户触发一个对话框,然后他们选择的文件是一个被解析的word文档,然后用该word文档填充表单。我在标准操作中遇到的问题是,Text只显示对属性的最后一次更改。我已经设置了断点,我看到属性被提出,但是似乎iccommand参数等到所有工作完成,然后只更新最后一个属性。是否有一种方法可以在这个过程发生时向用户显示帖子?
**所以基本上我想要的是一个用户点击一个按钮,看到"获得的Word文档",(然后工作就完成了)"已解析的Word文档"一个接一个的过程完成。当i命令完成时,不是最后一次更改。我认为核心问题是,UI没有得到的变化,直到堆栈暂停,这是内部的"中继命令"/"异步中继命令"委托方法。* *
XAML:<TextBox Text="{Binding WordFileLocation}" />
<Button Content="Start Process" Height="20" Command="{Binding AsyncDoCommand}"/>
<TextBox Text="{Binding Text, IsAsync=True}" />
VIEWMODEL:
private Reader _wordReader = new Reader();
private string _ParsedWordString;
private AsyncRelayCommand _DoAsyncCommand;
private string _Text;
private string _WordFileLocation;
public string Text
{
get { return _Text; }
set
{
_Text = value;
RaisePropertyChanged("Text");
}
}
public string WordFileLocation
{
get { return _WordFileLocation; }
set
{
_WordFileLocation = value;
RaisePropertyChanged("WordFileLocation");
}
}
public ICommand AsyncDoCommand
{
get
{
if (_DoAsyncCommand == null)
{
_DoAsyncCommand = new AsyncRelayCommand(async () => await DoIt());
}
return _DoAsyncCommand;
}
}
public async Task DoIt()
{
WordFileLocation = "Somewhere a dialogue selected...";
Text = "Looking....";
await Task.Delay(2000);
Text = "Look at me"; // Works FINALLY....
await GetWordData();
// If I put in the delay below, the Text change will show up. If not it won't. For some reason my setting of Text DOES not show up till a delay is triggered.
//await Task.Delay(100);
await ParseWordData();
}
async Task ParseWordData()
{
try
{
_ParsedWordString = _wordReader.ReadWordDocWithForms(_WordFileLocation);
Text = "Parsed Word Document";
}
catch (Exception)
{
Text = "Could not parse Word Document";
}
}
async Task GetWordData()
{
OpenFileDialog dlg = new OpenFileDialog();
dlg.Multiselect = false;
dlg.Filter = "Doc Files (*.doc, *.docx)|*.doc;*.docx";
// open dialog
bool ok = (bool)dlg.ShowDialog();
if(ok)
{
try
{
// Get the location from the dialog
WordFileLocation = dlg.FileName;
Text = "Obtained Word Document.";
}
catch (Exception)
{
Text = "Failed Loading Document.";
}
}
else
{
Text = "Could Not Browse for Document.";
}
}
编辑8-20-14 12:45 PST:除了一件事之外,曾是正确的。我无法让UI接受异步更改,除非我强制执行"Task.Delay(100)"。就像堆栈想要通过我的两个子方法自动完成一样。我对。net 4.5异步方法完全是一个新手,但我想使用它们,因为它们似乎是首选的方式。我猜这是我对"任务"和它的作用的无知。我必须做一个任务返回,但似乎等待不喜欢做一些简单的事情,如等待"加载"或类似。所以我尝试了返回类型在我的签名方法,如'void',任务,任务与一个简单的'返回"获得的文档"'。这些都不会更新属性,直到我在子方法之后调用Task.Delay()。所以这是我对异步过程的无知,为什么我需要暂停来获得更新。'ParseWordDocument'是相当昂贵的,因为它解析长单词文档,平均需要2到5秒,这取决于文档大小,因为它解析表单填充和纯文本。然而,即使有这个延迟,我的文本没有得到更新,直到这个子方法完成。
我建议您使用异步命令实现,如在互联网上找到的AsyncRelayCommand
。
我将此实现用于我自己的MVVM项目之一。
public class AsyncRelayCommand : ICommand {
protected readonly Func<Task> _asyncExecute;
protected readonly Func<bool> _canExecute;
public event EventHandler CanExecuteChanged {
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public AsyncRelayCommand(Func<Task> execute)
: this(execute, null) {
}
public AsyncRelayCommand(Func<Task> asyncExecute, Func<bool> canExecute) {
_asyncExecute = asyncExecute;
_canExecute = canExecute;
}
public bool CanExecute(object parameter) {
if(_canExecute == null) {
return true;
}
return _canExecute();
}
public async void Execute(object parameter) {
await ExecuteAsync(parameter);
// notify the UI that the commands can execute changed may have changed
RaiseCanExecuteChanged();
}
protected virtual async Task ExecuteAsync(object parameter) {
await _asyncExecute();
}
public void RaiseCanExecuteChanged()
{
CommandManager.InvalidateRequerySuggested();
}
}
这还有一个额外的好处,你不仅可以异步运行命令并在其间进行UI操作(即添加到ObservableCollection),还可以在CanExecute状态可能发生变化时通知UI(即当命令完成时)。
示例用法:
public ICommand DoCommand
{
get
{
if(_DoCommand == null)
{
_DoCommand = new AsyncRelayCommand(DoIt);
}
return _DoCommand;
}
}
public async void DoIt() {
WordFileLocation = "Someplace a dialogue selected";
await ParseDocument();
Text = "Parsed Word Document";
await ObtainDocument();
Text = "Obtained Word Document.";
}
编辑:WPF命令绑定是异步/任务感知的。如果您的ICommand.Execute
返回Task
或Task<T>
,那么WPF将异步运行它们。
你真的需要让确保满足这两个条件:
- 你的
DoIt()
方法有async
关键字(c# 5.0/。.NET 4.5)(或返回Task
而不是void
,对于。NET 3.5和4.0) - 您使用
await
每长处理。如果你的方法返回awaitable
/Task
/Task<T>
,你可以等待它。如果你的方法没有,你仍然可以创建一个新的Task
并等待它
DoIt()
方法的另一个例子
public Task ParseDocumentAsync()
{
return Task.Run( () => {
// your long processing parsing code here
});
}
public async void DoIt() {
WordFileLocation = "Someplace a dialogue selected";
Text = "Begin";
await ParseDocumentAsync(); // public Task ParseDocumentAsync() { }
Text = "ParseDocumentDone()";
Text = "Wait 3 seconds";
await Task.Delay(3000);
Text = "Run non-Task methods";
Task.Run( () => LongRunningNonAsyncMethod(); );
Text = "LongRunningNonAsyncMethod() finished. Wait 2 seconds";
// DON'T DO THIS. It will block the UI thread!
// It has no await, it runs on the thread which started everything,
// which is UI Thread in this case, because the View invoked the command.
// That's why it locks the UI
Thread.Sleep(2000);
Text = "Waited 2 seconds. We won't see this, because UI is locked";
// DON'T DO THIS, it will ALSO block the UI Thread.
LongRunningNonAsyncMethod();
Text = "Finished";
}
旁注:如果你使用的是。net 4.5和c# 5.0,你可以使用async
/await
关键字进行异步操作。如果你被迫使用旧的框架(。. NET 3.5和4.0),你仍然可以使用Task t = Task.Run(...)
来启动它。ContinueWith(() => {Text = "Finished"})´在任务完成后执行代码。
Edit2: 很抱歉回复晚了,我一直忙于RL的工作,没有太多的时间在这里观看。我会更新你的ParseWordData()
方法,希望它能起作用。
// alternatively: async void ParseWordData().
// async void => Task as return type
// async Task => Task<Task> as return type
Task ParseWordData()
{
return Task.Run( () => {
try
{
_ParsedWordString = _wordReader.ReadWordDocWithForms(_WordFileLocation);
Text = "Parsed Word Document";
}
catch (Exception)
{
Text = "Could not parse Word Document";
}
});
}
这将在线程/任务中运行ReadWordDocWithForms代码并返回Task
。Task
可以等待。
基本上可以归结为:在可等待的方法上使用await(返回Task
或Task<T>
),如果你需要运行一个不可等待的方法,使用Task.Run(...)
并返回(或等待)这个Task
。
我无法添加评论,所以我将使用一个答案。iccommand将使用基本UI线程进行处理,因此,如果不设置某种任务,您将无法完成此操作。
听起来好像你知道怎么做,但以防万一,我将这样做:
Text = "Parsed Word Document";
Task.Factory.StartNew(() =>
{
//do your "DoIt" work here
Text = "Obtained Word Document.";
});
编辑:public ICommand DoCommand
{
get
{
if (_DoCommand == null)
{
_DoCommand = new RelayCommand(Param => NewMethod(new Action(()=>DoIt())));
}
return _DoCommand;
}
}
NewMethod(Action DoIt)
{
Task.Factory.StartNew(() =>
{
DoIt.Invoke();
});
}
relaycommand中的Linq语句有点乱,但这确实允许您在任何需要弹出任务的地方重用"NewMethod"。否则,您可以简单地从newmethod调用DoIt()并保存Action参数。