使用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秒,这取决于文档大小,因为它解析表单填充和纯文本。然而,即使有这个延迟,我的文本没有得到更新,直到这个子方法完成。

使用MVVM在一个WPF事件中观察属性的多次变化

我建议您使用异步命令实现,如在互联网上找到的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返回TaskTask<T>,那么WPF将异步运行它们。

你真的需要让确保满足这两个条件:

  1. 你的DoIt()方法有async关键字(c# 5.0/。.NET 4.5)(或返回Task而不是void,对于。NET 3.5和4.0)
  2. 您使用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代码并返回TaskTask可以等待。

基本上可以归结为:在可等待的方法上使用await(返回TaskTask<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参数。