代码之家  ›  专栏  ›  技术社区  ›  Jeff

MVVMLight CanExecute在窗口单击之前不工作

  •  2
  • Jeff  · 技术社区  · 8 年前

    快速笔记,这样我就不会浪费任何人的时间。从nuget安装MVVMLight时,我最终收到一个错误 null : The term 'null' is not recognized as the name of a cmdlet, function, script file, or operable program .MVVMLight似乎可以正常工作,除了下面将描述的问题,但我想提一下这一点以防万一。


    问题

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

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

    我构建了一个将使用Thread的简单示例。睡眠模拟缓慢的操作,它很好地展示了问题。这个 WaitOneSecondCommand 似乎间歇性工作。这个 WaitTenSecondsCommand WaitThirtySecondsCommand 永远不要工作。

    通过 不工作 我的意思是,按钮一直处于禁用状态,直到我单击窗体上的某个位置。


    我尝试过的事情

    我已经做了大量的研究,迄今为止我尝试的解决方案并没有改变这种行为。我最终导致了暴力强迫所有不同的“修复”,以防我误解。

    我尝试的一件事是扩展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中,以为这会很垃圾,但我认为这会消除任何疑问

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


    完整代码

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

    主窗口.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的绑定,以清楚地显示它正在正确更新。这也是了解10秒和30秒命令何时完成的唯一可靠方法。我不希望任何MessageBox强制您单击“确定”导致CanExecute更新。这是一个创可贴。

    主视图模型.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;
        }
    }
    

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

    2 回复  |  直到 4 年前
        1
  •  3
  •   Jeff    4 年前

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

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

    2020年更新!

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

    相应的CommandExecute方法现在只使用异步等待

    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以支持异步任务而不是异步void

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

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

        2
  •  2
  •   user469104    8 年前

    您需要从UI线程触发“RaiseCanExecuteChanged”。对于可能不是这样的Task。要查看这是否能解决问题,最简单的方法是将其添加到IsBusy setter中。请注意,这不是您应该如何构造应用程序(在IsBusy setter中有此设置),而是您应该检测任务的完成并在那里执行。

    public bool IsBusy {
        get {
            return _isBusy;
        }
    
        set {
            _isBusy = value;
            RaisePropertyChanged();
            Application.Current.Dispatcher.Invoke(
                        DispatcherPriority.ApplicationIdle,
                        new Action(() => {
                            WaitOneSecondsCommand.RaiseCanExecuteChanged();
                            WaitTenSecondsCommand.RaiseCanExecuteChanged();
                            WaitThirtySecondsCommand.RaiseCanExecuteChanged();
                        }));        }
    }