【问题标题】:MVVMLight CanExecute not working until window clickMVVMLight CanExecute 在单击窗口之前不起作用
【发布时间】:2026-01-08 16:35:02
【问题描述】:

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


问题

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

我正在使用 Task 执行冗长的操作,以便 UI 可以更新。 Task的第一步也是最后一步是适当地翻转IsBusy(其中每个CanExecute方法returns !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 认为这会非常垃圾,但我认为它会消除任何疑问

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


完整代码

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

MainWindow.xaml

<Window x:Class="*Example.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 迫使您单击 OK 导致 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;
    }
}

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

【问题讨论】:

    标签: c# wpf task mvvm-light relaycommand


    【解决方案1】:

    这个问题的全部功劳归于 Viv:https://*.com/a/18385949/865868

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

    2020 年更新!

    我想重新审视这个答案,因为我的理解有缺陷,尽管我觉得还有更好的方法。目标应该是根本不修改 IsBusy 的设置或任何支持属性。此外,应避免使用 async 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】:

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

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

      【讨论】:

      • 这确实有效并解决了问题,我按照建议在 IsBusy 之外实现了它,但这不会破坏目的吗?我在应用程序中执行的每个任务都需要一个调用相同函数的 ContinueWith,该函数将包含调用 RaiseCanExecuteChanged 的​​代码。根据我阅读的内容,MVVMLight 不应该自己处理吗?
      • 不确定你所说的“打败目标”是什么意思。我相信你正在尝试做的一般模式是'1。对任务完成注册感兴趣的一方/各方通知”(使用 MVVMLight,您可以使用 MessengerInstance)。 '2。任务开始'。 '3。任务完成后,将引发事件以指示完成'。此时将通知步骤 1 中的各方,如果他们是视图模型,他们将提出适当的属性更改并在 UI 线程上执行更改。或者您可以根据您的情况在 UI 线程的 #3 上引发事件。
      • 好吧,我想我有点挑剔,我并不是有意对您的帮助回复感到粗鲁或忘恩负义,因为我很感激。我只是希望有其他答案。一旦你开始有多个命令,把它放在任何地方都会很麻烦。每个带有命令的 ViewModel 都需要一个类似的结构,我认为它是发生事故的地方。
      最近更新 更多