刷新 WPF 命令。

75
有人知道如何在自定义命令(Josh Smith的RelayCommand)上强制调用CanExecute吗?
通常,只要UI发生交互,就会调用CanExecute。如果我点击某些东西,我的命令就会更新。
我遇到了这样一种情况,CanExecute的条件由后台定时器开关控制。因为这不是由用户交互驱动的,所以CanExecute直到用户与UI交互才会被调用。最终结果是我的Button保持启用/禁用状态,直到用户点击它。点击后,它会正确地更新。有时,Button看起来已启用,但当用户点击它时,它会变成禁用而不是触发。
当定时器更改影响CanExecute的属性时,我该如何强制进行代码更新?我尝试在影响CanExecute的属性上触发PropertyChangedINotifyPropertyChanged),但没有帮助。
XAML示例:
<Button Content="Button" Command="{Binding Cmd}"/>

后台示例代码:

private ICommand m_cmd;
public ICommand Cmd
{
    if (m_cmd == null)
        m_cmd = new RelayCommand(
            (param) => Process(),
            (param) => EnableButton);

    return m_cmd;
}

// Gets updated from a timer (not direct user interaction)
public bool EnableButton { get; set; }

你尝试过为Command引发INotifyPropertyChanged吗?你不需要为Command拥有一个字段,只需每次返回新的即可。这种组合应该可以工作。或者仅在需要强制时创建新的Command。 - egaga
7个回答

105

调用 System.Windows.Input.CommandManager.InvalidateRequerySuggested() 方法会强制 CommandManager 触发 RequerySuggested 事件。

备注:CommandManager 仅在确定命令目标已更改的某些条件(例如键盘焦点的更改)时才会关注。在CommandManager不能足够地确定导致命令无法执行的条件发生变化的情况下,可以调用 InvalidateRequerySuggested 方法来强制 CommandManager 触发 RequerySuggested 事件。


1
你建议从ViewModel类中调用这个吗? - Josh G
3
不一定需要这样做,因为这可能会使您的类难以测试。可以尝试一下,如果必要,将其移入服务中。另一个选项是向RelayCommand添加一个方法,允许您仅针对该命令引发CanExecuteChanged(CommandManager.InvalidRequerySuggested会使所有命令无效,这有点过度)。 - Kent Boogaart
26
有趣...它有效,但必须在UI线程上调用。我并不感到惊讶。 - Josh G
4
我使用MVVM-Light messenger并创建了一个简单的RefreshCommandStatus消息,现在我的ViewModels可以发送它。主窗口监听此消息并调用CommandManager.InvalidateRequerySuggest()。 - cordialgerm
6
必须在 UI 线程上运行,因此最好这样做:UI.Dispatcher.Invoke(() => { CommandManager.InvalidateRequerySuggested(); }); - Fandi Susanto
显示剩余9条评论

29

我很早就知道CommandManager.InvalidateRequerySuggested(),并使用过它,但有时它不起作用。最终我弄清了为什么会这样!即使它不像某些其他操作那样抛出异常,你仍然必须在主线程上调用它。

在后台线程上调用它似乎有效,但有时会导致UI禁用。我真的希望这可以帮助到某些人,并节省他们浪费的时间。


17

解决这个问题的方法是将IsEnabled绑定到一个属性:

<Button Content="Button" Command="{Binding Cmd}" IsEnabled="{Binding Path=IsCommandEnabled}"/>

然后在您的ViewModel中实现此属性。这也使得单元测试更容易使用属性而不是命令,以查看在某个时间点上是否可以执行命令。

我个人认为这更方便。


你如何刷新 IsEnabled? - visc
我会避免使一切无效,这是一个更好、更容易的解决方案。 - Asheh
#PersistentWPFIssues12YearAnniversary #NoWonderEverybodyFledToJS - Christoph Wolf

6
也许这个变体更适合您:
 public interface IRelayCommand : ICommand
{
    void UpdateCanExecuteState();
}

实现:

 public class RelayCommand : IRelayCommand
{
    public event EventHandler CanExecuteChanged;


    readonly Predicate<Object> _canExecute = null;
    readonly Action<Object> _executeAction = null;

   public RelayCommand( Action<object> executeAction,Predicate<Object> canExecute = null)
    {
        _canExecute = canExecute;
        _executeAction = executeAction;
    }


    public bool CanExecute(object parameter)
    {
       if (_canExecute != null)
            return _canExecute(parameter);
        return true;
    }

    public void UpdateCanExecuteState()
    {
        if (CanExecuteChanged != null)
            CanExecuteChanged(this, new EventArgs());
    }



    public void Execute(object parameter)
    {
        if (_executeAction != null)
            _executeAction(parameter);
        UpdateCanExecuteState();
    }
}

使用Simple:

public IRelayCommand EditCommand { get; protected set; }
...
EditCommand = new RelayCommand(EditCommandExecuted, CanEditCommandExecuted);

 protected override bool CanEditCommandExecuted(object obj)
    {
        return SelectedItem != null ;
    }

    protected override void EditCommandExecuted(object obj)
    {
        // Do something
    }

   ...

    public TEntity SelectedItem
    {
        get { return _selectedItem; }
        set
        {
            _selectedItem = value;

            //Refresh can execute
            EditCommand.UpdateCanExecuteState();

            RaisePropertyChanged(() => SelectedItem);
        }
    }

XAML:

<Button Content="Edit" Command="{Binding EditCommand}"/>

1
这并不理想,因为它会创建对处理程序的强引用,从而导致内存泄漏。 - Bartosz Wójtowicz

4

感谢大家提供的建议。这里有一段关于如何将后台线程的调用转移到UI线程的代码:

private SynchronizationContext syncCtx; // member variable

构造函数中:

syncCtx = SynchronizationContext.Current;

在后台线程中触发重新查询的方法是:
syncCtx.Post( delegate { CommandManager.InvalidateRequerySuggested(); }, null );

希望这能有所帮助。——迈克尔

3
似乎最好调用 Dispatcher.BeginInvoke()。 - Josh G
嗨,乔希。也许这样会更好。在内部,Dispatcher.BeginInvoke()使用SynchronizationContextSwitcher类,该类委托给SynchronizationContext... - Michael Kennedy

0

如果您只想更新单个GalaSoft.MvvmLight.CommandWpf.RelayCommand,您可以使用以下方法:

mycommand.RaiseCanExecuteChanged();

对我来说,我创建了一个扩展方法:

public static class ExtensionMethods
    {
        public static void RaiseCanExecuteChangedDispatched(this RelayCommand cmd)
        {
            System.Windows.Application.Current.Dispatcher.Invoke(DispatcherPriority.Normal, new Action(() => { cmd.RaiseCanExecuteChanged(); }));
        }

        public static void RaiseCanExecuteChangedDispatched<T>(this RelayCommand<T> cmd)
        {
            System.Windows.Application.Current.Dispatcher.Invoke(DispatcherPriority.Normal, new Action(() => { cmd.RaiseCanExecuteChanged(); }));
        }
    }

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接