c# - MVVMLight CanExecute 在单击窗口之前不起作用

标签 c# wpf task mvvm-light relaycommand

快速注释,这样我就不会浪费任何人的时间。从 nuget 安装 MVVMLight 时,我最终收到错误 null :术语“null”未被识别为 cmdlet、函数、脚本文件或可操作程序的名称。尽管如此,MVVMLight 似乎工作得很好,除了下面将要描述的问题,但我想提一下以防万一。


问题

我遇到了命令执行完成后按钮无法重新启用的问题。如果执行速度非常快,它们有时似乎会起作用,但任何需要一段时间的操作似乎永远不会起作用。这是竞争条件的尖叫。

我正在使用任务来执行冗长的操作,以便 UI 可以更新。任务的第一步和最后一步是适当翻转 IsBusy(其中每个 CanExecute 方法返回 !IsBusy;)

我构建了一个简单的示例,它将使用 Thread.Sleep 来模拟缓慢的操作,它很好地展示了该问题。 WaitOneSecondCommand 似乎间歇性地工作。 WaitTenSecondsCommandWaitThirtySecondsCommand 永远不会工作。

所谓不起作用,我的意思是按钮保持禁用状态,直到我单击表单上的某个位置。


我尝试过的事情

我已经做了相当多的研究,到目前为止我尝试过的解决方案并没有改变这种行为。我最终导致暴力强制所有不同的“修复”,以防我误解。

我尝试的一件事是扩展 RelayCommands 以引发属性更改。举个例子:

来自:

public RelayCommand WaitOneSecondCommand {
    get; set;
}

致:

public RelayCommand WaitTenSecondsCommand {
    get {
        return _waitTenSecondsCommand;
    }

    set {
        _waitTenSecondsCommand = value;
        RaisePropertyChanged();
    }
}

我没想到它会起作用,但我想尝试一下。我还尝试添加 WaitTenSecondsCommand.RaiseCanExecuteChanged();

我还尝试将 WaitTenSecondsCommand.RaiseCanExecuteChanged() 添加到 CommandExecute 方法,但这也没有改变任何内容。

private void WaitTenSecondsCommandExecute() {
    Task.Run(() => {
        IsBusy = true;
        Thread.Sleep(10000);
        IsBusy = false;
        WaitTenSecondsCommand.RaiseCanExecuteChanged();
    });
}

我还了解了 CommandManager,因此我还添加了 CommandManager.InvalidateRequerySuggested()。我将其添加到 IsBusy 中,认为这会是非常垃圾邮件,但我认为这会消除任何疑问

这又有点竞争条件的味道,我在这里使用任务,但这些任务不能同时运行,并且由于使用了 IsBusy 标志,因此不能相互冲突。


完整代码

这是一个基本的 WPF 应用程序,使用 .Net 4.6.1 并从 Nuget 安装了 MVVMLight 5.2.0。

MainWindow.xaml

<Window x:Class="StackOverflowExample.View.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="MainWindow" MinHeight="100" Width="150" ResizeMode="NoResize" SizeToContent="Height"
        DataContext="{Binding Source={StaticResource Locator}, Path=Main}">
    <StackPanel>
    <Button Height="70" Margin="5" Command="{Binding WaitOneSecondCommand}">Wait 1 Second</Button>
    <Button Height="70" Margin="5" Command="{Binding WaitTenSecondsCommand}">Wait 10 Seconds</Button>
    <Button Height="70" Margin="5" Command="{Binding WaitThirtySecondsCommand}">Wait 30 Seconds</Button>
    </StackPanel>
    <Grid>
        <Label HorizontalAlignment="Left" Width="84">IsBusy:</Label>
        <TextBox IsReadOnly="True" HorizontalAlignment="Right" Width="45" Text="{Binding IsBusy}" Margin="4,5,5,5" />
    </Grid>
</Window>

我添加了 IsBusy 的绑定(bind),以清楚地表明它正在正确更新。这也是了解 10 秒和 30 秒命令何时完成的唯一可靠方法。我不希望任何 MessageBox 强制您单击“确定”,从而导致 CanExecute 更新。这是一个创可贴修复。

MainViewModel.cs

public class MainViewModel : ViewModelBase {
    private bool _isBusy;
    private RelayCommand _waitTenSecondsCommand;

    public MainViewModel() {
        WaitOneSecondCommand = new RelayCommand(WaitOneSecondCommandExecute, WaitOneSecondCommandCanExecute);
        WaitTenSecondsCommand = new RelayCommand(WaitTenSecondsCommandExecute, WaitTenSecondsCommandCanExecute);
        WaitThirtySecondsCommand = new RelayCommand(WaitThirtySecondsCommandExecute, WaitThirtySecondsCommandCanExecute);
    }

    public RelayCommand WaitOneSecondCommand {
        get; set;
    }

    public RelayCommand WaitTenSecondsCommand {
        get {
            return _waitTenSecondsCommand;
        }

        set {
            _waitTenSecondsCommand = value;
            RaisePropertyChanged();
            WaitTenSecondsCommand.RaiseCanExecuteChanged();
        }
    }

    public RelayCommand WaitThirtySecondsCommand {
        get; set;
    }

    public bool IsBusy {
        get {
            return _isBusy;
        }

        set {
            _isBusy = value;
            RaisePropertyChanged();
            CommandManager.InvalidateRequerySuggested();
        }
    }

    private void WaitOneSecondCommandExecute() {
        Task.Run(() => {
            IsBusy = true;
            Thread.Sleep(1000);
            IsBusy = false;
        });
    }

    private void WaitTenSecondsCommandExecute() {
        Task.Run(() => {
            IsBusy = true;
            Thread.Sleep(10000);
            IsBusy = false;
            WaitTenSecondsCommand.RaiseCanExecuteChanged();
        });
    }

    private void WaitThirtySecondsCommandExecute() {
        Task.Run(() => {
            IsBusy = true;
            Thread.Sleep(30000);
            IsBusy = false;
        });
    }

    private bool WaitOneSecondCommandCanExecute() {
        return !IsBusy;
    }

    private bool WaitTenSecondsCommandCanExecute() {
        return !IsBusy;
    }

    private bool WaitThirtySecondsCommandCanExecute() {
        return !IsBusy;
    }
}

请注意,在 View 模型中,我只在 WaitTenSeconds 上放置了一个支持字段,以表明它不会改变行为。

最佳答案

此问题完全归功于 Viv:https://stackoverflow.com/a/18385949/865868

我面临的问题是我在一个单独的线程中更新 IsBusy,而主 UI 线程显然依赖它来更新按钮。

2020 年更新!

我想重新审视这个答案,因为我的理解有缺陷,尽管我觉得还有更好的方法。目标应该是不修改 IsBusy 的设置或任何支持属性。另外,应该避免 async void。

相应的 CommandExecute 方法现在只需使用 async wait

private async Task WaitOneSecondCommandExecute() {
    CanWaitOneSecond = true;
    await Task.Delay(1000);
    CanWaitOneSecond = false;
}

我对“CanWaitOneSecond”进行了更改

public bool CanWaitOneSecond {
    get => _canWaitOneSecond ;
    set {
        _canWaitOneSecond  = value;
        Set(() => CanWaitOneSecond , ref _canWaitOneSecond , value);
        RaiseAllCanExecuteChanged();
     }
}

public void RaiseAllCanExecuteChanged() {
    foreach (var command in Commands) {
        command.RaiseCanExecuteChanged();
    }
}

public List<RelayCommand> Commands {
    get;
}

我还对构造函数进行了轻微更改,在其中设置 RelayCommand 以支持 async Task 而不是 async void

Commands = new List<RelayCommand>();
Commands.Add(WaitOneSecondCommand = new RelayCommand(async () => await WaitOneSecondCommandExecute(), WaitOneSecondCommandCanExecute));

我希望这可以节省人们大量的研究时间,如果此实现有任何问题,请告诉我。我还是希望能把这个清理干净

关于c# - MVVMLight CanExecute 在单击窗口之前不起作用,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/35923686/

相关文章:

c# - telerik tabstrip 控件显示在其他选项卡中

c# - 如何防止应用程序在任务管理器中被杀死?

c# - x :Bind to DependencyProperty not working (classic Binding works fine)

wpf - 可滚动文本 block 大小恰好为 2 行高

.net - 限制许多 .NET 任务实例的最佳方法是什么?

c# - xamarin 表单中的异步 Task<T> 问题

c# - 异步运行同步代码

c# - 使用 Prism 库将 View 注入(inject) TabControl

c# - 如何让重力在Unity3D的编辑器模式下工作

c# - 一般比较两个对象