C# 如何从ViewModel正确执行异步方法?

C# 如何从ViewModel正确执行异步方法?,c#,wpf,asynchronous,mvvm,C#,Wpf,Asynchronous,Mvvm,对于这个问题,我有一个简单的窗口,其中包含以下XAML: <StackPanel> <TextBox Text="{Binding MyText}" /> <CheckBox IsChecked="{Binding IsChecked}">Check</CheckBox> </StackPanel> Save方法模拟缓慢的任务,例如保存到磁盘。出于调试目的,它在执行此操作之前和之后都会输出一个唯一的ID。另外,该方法是

对于这个问题,我有一个简单的
窗口
,其中包含以下XAML:

<StackPanel>
    <TextBox Text="{Binding MyText}" />
    <CheckBox IsChecked="{Binding IsChecked}">Check</CheckBox>
</StackPanel>
Save
方法模拟缓慢的任务,例如保存到磁盘。出于调试目的,它在执行此操作之前和之后都会输出一个唯一的ID。另外,该方法是异步的,因为我不希望UI在操作期间冻结

问题是,在用户在
文本框中键入内容,然后选中
复选框后,属性会很快更新。这将导致以下调试示例输出:

开始保存6ea6c102-cbe7-472f-b8b8-249499ff7f64
启动保存c77b4478-14ca-4243-a45b-7b35b5663d49
已完成保存6ea6c102-cbe7-472f-b8b8-249499ff7f64
已完成保存c77b4478-14ca-4243-a45b-7b35b5663d49
如您所见,第一次保存操作(来自
MyText
)在第二次保存操作(来自
IsChecked
)启动之前没有完成。这让我有点害怕,因为我想,数据可能会以错误的顺序保存并被破坏

有没有处理此类问题的良好做法

我已经考虑了几个可能的解决方案。第一种是在
文本框
绑定中使用类似
Delay=100的东西。这将导致在用户停止键入100毫秒后调用
Save
方法。由于各种原因,这是一个丑陋的解决方案


第二种方法是使用
信号量lim
。在
Save
方法中,我可以使用
try
/
finally
来包围代码,以使用所述的信号量。这实际上是可行的,但我不确定这是否是处理此问题的最佳方法。

正如您所述,我还认为锁是最好的方法

我建议使用Stepehen Cleary straigthforward解决方案:

因此,您的代码如下所示

private readonly AsyncLock _lock = new AsyncLock();
private async void Save()
{
    var id = Guid.NewGuid();

    using (await _lock.LockAsync())
        {
            // It's safe to await while the lock is held
            Debug.WriteLine($"Starting save {id}");

            await Task.Delay(100);  // Simulate slow task

            Debug.WriteLine($"Finished save {id}");
        }
}

这实际上并不会对代码的可读性产生太大的影响,最终只会得到一个异步方法队列

正如你所说,我也认为锁是最好的方法

我建议使用Stepehen Cleary straigthforward解决方案:

因此,您的代码如下所示

private readonly AsyncLock _lock = new AsyncLock();
private async void Save()
{
    var id = Guid.NewGuid();

    using (await _lock.LockAsync())
        {
            // It's safe to await while the lock is held
            Debug.WriteLine($"Starting save {id}");

            await Task.Delay(100);  // Simulate slow task

            Debug.WriteLine($"Finished save {id}");
        }
}
这实际上并不会对代码的可读性产生太大的影响,最终只会得到一个异步方法队列

有没有处理此类问题的良好做法

如果希望两个保存都发生,那么可以使用锁(或
信号量lim
)序列化它们。如果要阻止启动第二次保存,通常的方法是在保存过程中禁用这些控件,例如,通过绑定到UI的数据的
IsBusy
属性

注意事项:

  • 同步设置
    IsBusy
    属性。这会立即禁用控件
  • Unset
    finally
    中处于忙碌状态,以确保即使发生错误也始终处于未设置状态
有没有处理此类问题的良好做法

如果希望两个保存都发生,那么可以使用锁(或
信号量lim
)序列化它们。如果要阻止启动第二次保存,通常的方法是在保存过程中禁用这些控件,例如,通过绑定到UI的数据的
IsBusy
属性

注意事项:

  • 同步设置
    IsBusy
    属性。这会立即禁用控件
  • Unset
    finally
    中处于忙碌状态,以确保即使发生错误也始终处于未设置状态

这是Rx的绝佳样本。如果您不想使用(可以很容易地在MVVMLight旁边使用),您所需要的只是一个属性已更改的信号

使用RxUI:

this.WhenAnyValue(x => x.MyText, x => x.IsChecked) // this you will need to emulate if you don't want RxUI
.Throttle(TimeSpan.FromMilliseconds(150)) // wait for 150ms after last signal, if there isn't any, send your own further into pipeline
.Synchronize()
.Do(async _ => await Save()) // we have to await so that Synchronize can work
.Subscribe();
这将在上次MyText更改或IsChecked更改后等待150毫秒,然后执行一次保存


此外,RxUI有非常聪明的ICommand实现,支持异步开箱即用,包括在工作期间禁用命令

这是Rx的绝佳样本。如果您不想使用(可以很容易地在MVVMLight旁边使用),您所需要的只是一个属性已更改的信号

使用RxUI:

this.WhenAnyValue(x => x.MyText, x => x.IsChecked) // this you will need to emulate if you don't want RxUI
.Throttle(TimeSpan.FromMilliseconds(150)) // wait for 150ms after last signal, if there isn't any, send your own further into pipeline
.Synchronize()
.Do(async _ => await Save()) // we have to await so that Synchronize can work
.Subscribe();
这将在上次MyText更改或IsChecked更改后等待150毫秒,然后执行一次保存


此外,RxUI有非常聪明的ICommand实现,支持异步开箱即用,包括在工作期间禁用命令

这是可行的,我只需要使用
语句将
Debug.WriteLine
放在
中,这样“Starting”输出就会正确显示。否则,两个“起始”行将依次打印。@redcurry您完全正确。我将对其进行编辑以备将来搜索。这样做有效,我只需要使用
语句将
Debug.WriteLine
放在
中,以便“Starting”输出正确显示。否则,两个“起始”行将依次打印。@redcurry您完全正确。我将编辑它以备将来搜索。对于这种特殊情况,我不能使用
IsBusy
方法的原因是,在单击复选框之前,文本不会通过数据绑定进行更新,因此文本和复选框状态都会通过单用户交互进行更新。换句话说,禁用复选框将为时已晚。对于这种特殊情况,我不能使用
IsBusy
方法的原因是,在单击复选框之前,文本不会通过数据绑定进行更新,因此文本和复选框状态都会通过单用户交互进行更新。换句话说,禁用复选框将为时已晚。非常有趣!我一定会查看ReactiveUI。谢谢,很有趣!我一定会查看ReactiveUI。谢谢