Home > Mobile >  Binding Button CommandParameter to DataGrid SelectedItem off by one change
Binding Button CommandParameter to DataGrid SelectedItem off by one change

Time:10-04

I have a (WPF) Catel DataWindow with a DataGrid, where the SelectedItem property is bound to a VM property, and has two buttons indented to launch different Catel TaskCommands on the selected data grid item.

Note the CommandParameters are bound in different ways to what seems - but isn't - the same value:

<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>

For completeness, the VM code is given below.

When the Window is first shown, no data grid row is selected, and as expected both buttons are disabled. If the first data grid row gets selected (by a mouse click), only the one button binding to the SelectedPerson property of the VM gets enabled while to my surprise the other one stays disabled. On selecting a different item, both buttons get enabled and on Crtl-Clicking unselecting the selected line, the VM bound button gets disabled while the one binding through the ElementName mechanism doesn't.

Using Debugger breakpoints, I proved that both CanExecute functions are called on window initialization and on each item selection change. Yet the parameter for the Button binding via ElementName reference is one click behind.

If I change the VM SelectedPerson property in one of those commands, both buttons update as expected, their CanExecute handlers get the correct item value.

I see that binding to the VM property isn't bad, as it will be useful elsewhere in the business logic, yet I like learn why the two approaches behave so different.

What is going on with the 'ElementName' binding, why is it one click behind?

Finally, this is the VM

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
    }
}

CodePudding user response:

I think this is caused by the moment the commands get (re)evaluated.

You probably have the InvalidateCommandsOnPropertyChange property set to true (default value). For more info, see https://github.com/catel/catel/blob/develop/src/Catel.MVVM/MVVM/ViewModels/ViewModelBase.cs#L1016

At this stage, the binding probably didn't have a chance yet to update itself and is thus sending the previous version.

A workaround for this issue could be to use the dispatcher service inside the VM to re-evaluate the commands yourself:

In the ctor:

private readonly IDispatcherService _dispatcherService;

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

    _dispatcherService = dispatcherService;
}

Override the 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();
    });
}

Dislaimer: this is code written without an editor, but hopefully it gives you the idea what it should do

  • Related