通过一次更改将按钮 CommandParameter 绑定到 DataGrid SelectedItem

Binding Button CommandParameter to DataGrid SelectedItem off by one change

我有一个带有 DataGrid 的 (WPF) Catel DataWindow,其中 SelectedItem 属性 绑定到 VM 属性,并且缩进了两个按钮以启动不同的 Catel所选数据网格项上的 TaskCommands。

请注意,CommandParameters 以不同的方式绑定到看起来 - 但不是 - 相同的值:

<catel:DataWindow x:Class="CatelWPFApplication1.Views.MainWindow"
                  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                  xmlns:catel="http://schemas.catelproject.com"
                  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
                  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                  xmlns:viewModels="clr-namespace:CatelWPFApplication1.ViewModels"
                  mc:Ignorable="d"
                  ShowInTaskbar="True" 
                  ResizeMode="NoResize" 
                  SizeToContent="WidthAndHeight" 
                  WindowStartupLocation="Manual" 
                  d:DataContext="{d:DesignInstance Type=viewModels:MainWindowViewModel, IsDesignTimeCreatable=True}"                  
                >
     <StackPanel Orientation="Vertical">

         <StackPanel Orientation="Horizontal" Margin="0,6">

             <Button Command="{Binding ViaVmCommand}" 
                     CommandParameter="{Binding SelectedPerson, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" 
                     Content="Binding via VM" />

             <Button Command="{Binding ViaElementNameCommand}" 
                     CommandParameter="{Binding SelectedItem, ElementName=PersonDataGrid, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" 
                     Content="Binding via ElementName" 
                     x:Name="ViaElementButton"/>

         </StackPanel>
         
         <DataGrid x:Name="PersonDataGrid"
                   ItemsSource="{Binding PersonList}" 
                   SelectedItem="{Binding SelectedPerson, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" 
                   AutoGenerateColumns="True" />
        
    </StackPanel>
</catel:DataWindow>

为了完整起见,VM 代码如下。

首次显示 Window 时,未选择任何数据网格行,并且正如预期的那样,两个按钮都被禁用。如果第一个数据网格行被选中(通过单击鼠标),只有绑定到 VM 的 SelectedPerson 属性 的一个按钮被启用,而令我惊讶的是另一个按钮保持禁用状态。选择不同的项目时,两个按钮都会启用,而在 Crtl-Clicking 取消选择所选行时,VM 绑定按钮会被禁用,而通过 ElementName 机制绑定的按钮不会。

使用调试器断点,我证明了两个 CanExecute 函数在 window 初始化和每个项目选择更改时都会被调用。然而,通过 ElementName 引用绑定 Button 的参数却落后了一次点击。

如果我在其中一个命令中更改 VM SelectedPerson 属性,两个按钮都会按预期更新,它们的 CanExecute 处理程序会获得正确的项目值。

我发现绑定到 VM 属性 不错,因为它在业务逻辑的其他地方很有用,但我喜欢了解为什么这两种方法的行为如此不同。

'ElementName'绑定是怎么回事,为什么一键后退?

最后,这是虚拟机

using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Catel.Data;
using Catel.MVVM;
using CatelWPFApplication1.Models;

namespace CatelWPFApplication1.ViewModels
{
    /// <summary>
    /// MainWindow view model.
    /// </summary>
    public class MainWindowViewModel : ViewModelBase
    {
        #region Fields
        #endregion

        #region Constructors
        public MainWindowViewModel()
        {
            ViaVmCommand = new TaskCommand<Person>(OnViaVmCommandExecute, OnViaVmCommandCanExecute);
            ViaElementNameCommand = new TaskCommand<Person>(OnViaElementNameCommandExecute, OnViaElementNameCommandCanExecute);


            PersonList = new List<Person>();
            PersonList.Add(new Person(){FirstName = "Albert", LastName = "Abraham"});
            PersonList.Add(new Person(){FirstName = "Betty", LastName = "Baboa"});
            PersonList.Add(new Person(){FirstName = "Cherry", LastName="Cesar"});
        }

        #endregion

        #region Properties
        /// <summary>
        /// Gets the title of the view model.
        /// </summary>
        /// <value>The title.</value>
        public override string Title { get { return "View model title"; } }

        public List<Person> PersonList { get; }

        // classic Catel property, avoiding any magic with Fody weavers
        public Person SelectedPerson
        {
            get { return GetValue<Person>(SelectedPersonProperty); }
            set
            {
                SetValue(SelectedPersonProperty, value);
            }
        }

        public static readonly PropertyData SelectedPersonProperty = RegisterProperty(nameof(SelectedPerson), typeof(Person), null);

        #endregion

        #region Commands

        public TaskCommand<Person> ViaElementNameCommand { get; }

        
        private bool OnViaElementNameCommandCanExecute(Person person)
        {
            return person is not null;
        }

        private async Task OnViaElementNameCommandExecute(Person person)
        {
            SelectedPerson = null;
        }
        public TaskCommand<Person> ViaVmCommand { get; }

        private bool OnViaVmCommandCanExecute(Person person)
        {
            return person is not null;
        }

        private async Task OnViaVmCommandExecute(Person person)
        {
            SelectedPerson = PersonList.FirstOrDefault();
        }
        #endregion
    }
}

我认为这是由命令得到(重新)评估的时刻引起的。

您可能将 InvalidateCommandsOnPropertyChange 属性 设置为 true(默认值)。有关详细信息,请参阅 https://github.com/catel/catel/blob/develop/src/Catel.MVVM/MVVM/ViewModels/ViewModelBase.cs#L1016

在这个阶段,绑定可能还没有机会更新自己,因此发送以前的版本。

此问题的解决方法是使用 VM 内的调度程序服务自行重新评估命令:

在构造函数中:

private readonly IDispatcherService _dispatcherService;

public MainWindowViewModel(IDispatcherService dispatcherService)
{
    Argument.IsNotNull(() => dispatcherService);

    _dispatcherService = dispatcherService;
}

覆盖 OnPropertyChanged:

protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{
    // Don't alter current behavior by calling base
    base.OnPropertyChanged(e);

    // Dispatcher so the bindings get a chance to update
    _dispatcherService.BeginInvoke(await () => 
    {
        await Task.Delay(10);

        ViewModelCommandManager.InvalidateCommands();
    });
}

免责声明:这是在没有编辑器的情况下编写的代码,但希望它能让您了解它应该做什么