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

出于这个问题的目的,我有一个简单Window的以下 XAML:


<StackPanel>

    <TextBox Text="{Binding MyText}" />

    <CheckBox IsChecked="{Binding IsChecked}">Check</CheckBox>

</StackPanel>

每当用户在TextBox或中输入文本时CheckBox,我想执行一个缓慢的任务(例如将模型的状态保存到磁盘)。这是视图模型:


public class ViewModel : ViewModelBase  // using GalaSoft.MvvmLight

{

    private string _myText;

    public string MyText

    {

        get => _myText;

        set

        {

            if (Set(ref _myText, value))

                Save();

        }

    }


    private bool _isChecked;

    public bool IsChecked

    {

        get => _isChecked;

        set

        {

            if (Set(ref _isChecked, value))

                Save();

        }

    }


    private async void Save()

    {

        var id = Guid.NewGuid();

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

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

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

    }

}

该Save方法模拟一个缓慢的任务,例如保存到磁盘。出于调试目的,它会在执行此操作之前和之后输出一个唯一 ID。此外,该方法是异步的,因为我不希望 UI 在操作期间冻结。


问题是,在用户在 中键入内容TextBox,然后检查 后CheckBox,属性会很快更新。这会导致以下调试示例输出:


Starting save 6ea6c102-cbe7-472f-b8b8-249499ff7f64

Starting save c77b4478-14ca-4243-a45b-7b35b5663d49

Finished save 6ea6c102-cbe7-472f-b8b8-249499ff7f64

Finished save c77b4478-14ca-4243-a45b-7b35b5663d49

如您所见,第一个保存操作(来自)在第二个保存操作(来自)开始MyText之前没有完成。IsChecked这让我有点害怕,因为我想,数据可能会以错误的顺序保存并损坏。


是否有处理此类问题的良好做法?


我想了几个可能的解决方案。Delay=100首先是在TextBox绑定中使用类似的东西。这将导致在Save用户停止输入 100 毫秒后调用该方法。由于各种原因,这是一个丑陋的解决方案。


第二种是使用SemaphoreSlim. 在Save方法内部,我可以用try/将代码括起来finally以使用此处所述的信号量。这实际上有效,但我不确定这是否是处理此问题的最佳方法。


一只甜甜圈
浏览 124回答 3
3回答

慕桂英546537

是否有处理此类问题的良好做法?如果您希望两个保存都发生,那么使用锁(或SemaphoreSlim)对它们进行序列化是一种方法。如果您想阻止第二次保存开始,那么通常的方法是在保存过程中禁用这些控件,例如,通过IsBusy数据绑定到您的 UI 的属性。注意事项:IsBusy同步设置您的属性。这会立即禁用控件。IsBusy在 a中取消finally设置,以确保它始终未设置,即使发生错误也是如此。

三国纷争

正如您所说,我还认为锁是最好的方法。我推荐使用 Stepehen Cleary straigthforward 解决方案:https ://github.com/StephenCleary/AsyncEx因此,您的代码看起来像private readonly AsyncLock _lock = new AsyncLock();private async void Save(){&nbsp; &nbsp; var id = Guid.NewGuid();&nbsp; &nbsp; using (await _lock.LockAsync())&nbsp; &nbsp; &nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // It's safe to await while the lock is held&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Debug.WriteLine($"Starting save {id}");&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; await Task.Delay(100);&nbsp; // Simulate slow task&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Debug.WriteLine($"Finished save {id}");&nbsp; &nbsp; &nbsp; &nbsp; }}这并不会真正影响代码的可读性,并且您最终会得到一个异步方法队列。

凤凰求蛊

这是 Rx 的绝佳样本。如果你不想使用ReactiveUI(它可以很容易地与 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 实现,它支持开箱即用的异步工作,包括在工作期间禁用命令。
打开App,查看更多内容
随时随地看视频慕课网APP