0

Description

In WPF, using MvvmLight, I have a viewModel with an integer property SelectedIndex. Changing the value of this property is an expensive operation, so I only want to update the property if the operator is fairly certain that he finished typing.

I have a TextBox and a button. The operator types a number, and presses the button. This should lead to a command that updates the property.

Standard WPF MvvmLight solution for this

class MyViewModel
{
    private int selectedIndex;

    public MyViewModel()
    {
        this.CommandSelectIndex = new RelayCommand(ExecuteSelectIndex, CanSelectIndex);
    }

    public public RelayCommand<int> CommandSelectIndex { get; }

    public int SelectedIndex
    {
        get => this.selectedIndex;
        set => base.Set(nameof(SelectedIndex), ref this.selectedIndex, value);
    }

    private bool CanSelectIndex(int proposedIndex)
    {
         return proposedIndex > 0 && proposedIndex < MyData.Count;
    }

    private void ExecuteSelectIndex(int proposedIndex)
    {
        this.SelectedIndex = proposedIndex;
        ProcessSelectedIndex(proposedIndex);  // Expensive!
    }
}

For those who know MvvmLight, this is fairly straightforward.

So while the operator is typing a number, I only want to update the button. I don't want to do anything with the intermediate values:

1 --> 12 --> 123 --> (typing error, backspace) --> 124 [press button]

XAML

<StackPanel Name="Test1" Orientation="Horizontal">
    <TextBox Name="ProposedValue1" Text="1234" Width="300" Height="20"/>
    <Button x:Name="ButtonChangeText1" Content="Change"
                    Height="30" Width="74" Padding="5,2"
                    Command="{Binding Path=CommandSelectedIndex}"
                    CommandParameter="{Binding ElementName=ProposedValue1, Path=Text}"/>
</StackPanel>

This works partly: at startup CanSelectIndex(1234) is called; If the button is pressed ExecuteSelectedIndex(1234) is called.

Problem

However, if the text of the TextBox changes, CanSelectIndex is not called.

The reason is because event ICommand.CanExecuteChanged is not raised when the textbox changes.

Solution:

Add an event handler:

XAML:

<TextBox Name="ProposedValue1" Text="1234" Width="300" Height="20"
         TextChanged="textChangedEventHandler"/>

Code behind:

private void textChangedEventHandler(object sender, TextChangedEventArgs args)
{
    ((MyViewModel)this.DataContext).CommandSelectedIndex.RaiseCanExecuteChanged();
}

I always feel a bit uneasy whenever I have to write code behind. Is it standard to write eventhandlers in code behind, or is that a simplification that I only see in tutorials.

Is there a method that I can do this in XAML? Something with Binding?

TextChanged="TextChanged="{Binding Path=CommandSelectIndex ??? RaiseCanExecuteChanged() }
Harald Coppoolse
  • 28,834
  • 7
  • 67
  • 116
  • This is the second topic about CanExecute in RelayCommant from MVVMLight not working correctly. Haven't used MVVMLight for a long time. I'll have to check it - maybe bugs have appeared in it. https://stackoverflow.com/questions/63174956/wpf-update-button-if-text-in-textbox-changes/63179286?noredirect=1#comment111809794_63179286 – EldHasp Aug 04 '20 at 14:40
  • Oh dear, I hardly there to tell you now that the solution with the RaiseCanExecuteChanged works with a `RelayCommand` and a Textbox, but not with a `RelayCommand` and a textBox. Apparently the translation from the text of the textbox to the int doesn't work fine – Harald Coppoolse Aug 04 '20 at 15:24
  • @HaraldCoppoolse: The solution of what exactly? – mm8 Aug 05 '20 at 13:34

3 Answers3

1

The RelayCommand class in MvvmLight has two implementations. In the GalaSoft.MvvmLight.Command namespace and in the GalaSoft.MvvmLight.CommandWpf namespace.

You've probably used from namespace GalaSoft.MvvmLight.Command. And this type doesn't actually update the state of the command.

If used from the GalaSoft.MvvmLight.CommandWpf namespace, then the state of the command is updated according to the predetermined logic.

EldHasp
  • 6,079
  • 2
  • 9
  • 24
  • This was the problem. Changing to CommandWpf was enough to make sure that CanSelectIndex(string) is called. In this method I have to check if the passed string is an int before I can call CanSelectIndex(int). Now all I have to do is how to bind a text box to an integer, so that I don't have to check whether it is an int myself. – Harald Coppoolse Aug 10 '20 at 06:56
  • There should be no problem whatsoever with binding text to a command parameter that accepts int. The binding should provide automatic conversion. Have you checked how the RelayCommand from the GalaSoft.MvvmLight.CommandWpf namespace works? If you still have problems - write. I'll check again. – EldHasp Aug 10 '20 at 14:56
  • If you look at the [source code of RelayCommandGeneric](https://github.com/lbugnion/mvvmlight/blob/master/GalaSoft.MvvmLight/GalaSoft.MvvmLight%20(PCL)/Command/RelayCommandGeneric.cs), you'll see in method `CanExecute(object)`, that if the object is not of type T, that it returns false. So if I have a `RelayCommand`, and the input is property `Text` of a TextBlock, then the input is of type string, which is not an int, so it returns false, even though the string could have been parsed to an int. The code does not try to parse the text to an int – Harald Coppoolse Aug 10 '20 at 15:24
0

Is there a method that I can do this in XAML? Something with Binding?

Just bind the Text property of the TextBox to a string source property of the view model and raise call the RaiseCanExecuteChanged method of the command from the setter of this one.

If you really want to handle an actual event for some reason, you should look into interaction triggers.

mm8
  • 163,881
  • 10
  • 57
  • 88
0

@Harald Coppulse, you are absolutely right!

Here is my test code for MvvmLight.

ViewModel:

using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.CommandWpf;

namespace InvalidateCommandMvvmLight.ViewModel
{
    public class MyViewModel : ViewModelBase
    {
        private string _text;
        private int _number;

        public string Text { get => _text; private set => Set(ref _text, value); }

        public int Number { get => _number; set => Set(ref _number, value); }

        public RelayCommand<string> CommandTest { get; }
        public RelayCommand<int> CommandNumber { get; }

        public MyViewModel()
        {
            CommandTest = new RelayCommand<string>(Test, CanTest);
            CommandNumber = new RelayCommand<int>(IntTest, CanIntTest);
        }

        private bool CanTest(string text)
        {
            // the text must have a minimum length of 4 
            // and be different from the current one
            return text != null && text.Length >= 4 && text != Text;
        }
        private void Test(string text)
        {
            Text = text;

        }

        private bool CanIntTest(int num)
        {
            // The "num" parameter must be positive, less than 100
            // and is not equal to the Number property
            return num > 0 && num <100 && num != Number;
        }
        private void IntTest(int num)
        {
            Number = num;
        }
    }
}

XAML:

<Window x:Class="InvalidateCommandMvvmLight.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"
        xmlns:local="clr-namespace:InvalidateCommandMvvmLight"
        xmlns:vm="clr-namespace:InvalidateCommandMvvmLight.ViewModel"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <vm:MyViewModel/>
    </Window.DataContext>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <TextBox x:Name="tbText"
                Text="Alle eendjes zwemmen in het water" VerticalAlignment="Center"
                />

        <Button Content="Change Text"
                Grid.Column="1"
                Margin="5"
                Padding="5,2"
                Command="{Binding Path=CommandTest}"
                CommandParameter="{Binding ElementName=tbText, Path=Text}"/>
        <TextBox Text="{Binding Text, Mode=OneWay}" Grid.Column="2" IsReadOnly="True" VerticalAlignment="Center"/>
        <TextBox x:Name="tbNumber"
                Grid.Row="1"
                Text="55" VerticalAlignment="Center"/>

        <Button Content="Change Number"
                Grid.Row="1" Grid.Column="1"
                Margin="5"
                Padding="5,2"
                Command="{Binding Path=CommandNumber}"
                CommandParameter="{Binding ElementName=tbNumber, Path=Text}"/>
        <TextBox Text="{Binding Number, Mode=OneWay}" IsReadOnly="True"
                 Grid.Row="1" Grid.Column="2" VerticalAlignment="Center"/>
    </Grid>
</Window>

Unfortunately, the CommandsWpf.RelayCommand class in MvvmLight is implemented not correctly.
It does not take into account the peculiarities of working with values ​​of different types in WPF.

To work in a typical for WPF way, an implementation should have something like this:

using System.ComponentModel;

namespace Common
{
    #region Delegates for WPF Command Methods
    /// <summary>Delegate of the executive team method.</summary>
    /// <param name="parameter">Command parameter.</param>
    public delegate void ExecuteHandler<T>(T parameter);
    /// <summary>Command сan execute method delegate.</summary>
    /// <param name="parameter">Command parameter.</param>
    /// <returns><see langword="true"/> if command execution is allowed.</returns>
    public delegate bool CanExecuteHandler<T>(T parameter);
    #endregion

    /// <summary>Class for typed parameter commands.</summary>
    public class RelayCommand<T> : RelayCommand
    {

        /// <summary>Command constructor.</summary>
        /// <param name="execute">Executable command method.</param>
        /// <param name="canExecute">Method allowing command execution.</param>
        public RelayCommand(ExecuteHandler<T> execute, CanExecuteHandler<T> canExecute = null)
            : base
        (
                  p => execute(TypeDescriptor.GetConverter(typeof(T)).IsValid(p) ? (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFrom(p) : default),
                  p => (canExecute == null) || (TypeDescriptor.GetConverter(typeof(T)).IsValid(p) && canExecute((T)TypeDescriptor.GetConverter(typeof(T)).ConvertFrom(p)))
        )
        {}

    }
}

Unless you have the ability to change the RelayCommand implementation, you need to somehow use Binding's ability to auto-convert values.

One variant.
Create a property of the desired type in the ViewModel and use it as a proxy for autoconversion.
But if a non-numeric value is entered, then the command will not be able to define it.
You also need to check Validation.HasError.

ViewModel:

using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.CommandWpf;

namespace InvalidateCommandMvvmLight.ViewModel
{
    public class MyViewModel : ViewModelBase
    {
        private string _text;
        private int _number;
        private int _numberView;

        public string Text { get => _text; private set => Set(ref _text, value); }

        public int Number { get => _number; set => Set(ref _number, value); }
        public int NumberView { get => _numberView; set => Set(ref _numberView, value); }

        public RelayCommand<string> CommandTest { get; }
        public RelayCommand<int> CommandNumber { get; }

        public MyViewModel()
        {
            CommandTest = new RelayCommand<string>(Test, CanTest);
            CommandNumber = new RelayCommand<int>(IntTest, CanIntTest);
        }

        private bool CanTest(string text)
        {
            // the text must have a minimum length of 4 
            // and be different from the current one
            return text != null && text.Length >= 4 && text != Text;
        }
        private void Test(string text)
        {
            Text = text;

        }

        private bool CanIntTest(int num)
        {
            // The "num" parameter must be positive, less than 100
            // and is not equal to the Number property
            return num > 0 && num <100 && num != Number;
        }
        private void IntTest(int num)
        {
            Number = num;
        }
    }
}

XAML:

<Window x:Class="InvalidateCommandMvvmLight.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"
        xmlns:local="clr-namespace:InvalidateCommandMvvmLight"
        xmlns:vm="clr-namespace:InvalidateCommandMvvmLight.ViewModel"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <vm:MyViewModel NumberView="55"/>
    </Window.DataContext>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <TextBox x:Name="tbText"
                Text="Alle eendjes zwemmen in het water" VerticalAlignment="Center"
                />

        <Button Content="Change Text"
                Grid.Column="1"
                Margin="5"
                Padding="5,2"
                Command="{Binding Path=CommandTest}"
                CommandParameter="{Binding ElementName=tbText, Path=Text}"/>
        <TextBox Text="{Binding Text, Mode=OneWay}" Grid.Column="2" IsReadOnly="True" VerticalAlignment="Center"/>
        <TextBox x:Name="tbNumber"
                Grid.Row="1"
                Text="{Binding NumberView, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Center"/>

    <Button Content="Change Number"
                Grid.Row="1" Grid.Column="1"
                Margin="5"
                Padding="5,2"
                Command="{Binding Path=CommandNumber}"
                CommandParameter="{Binding NumberView}">
        <Button.Style>
            <Style TargetType="Button">
                <Style.Triggers>
                    <DataTrigger Binding="{Binding Path=(Validation.HasError), ElementName=tbNumber}"
                                 Value="True">
                        <Setter Property="IsEnabled" Value="False"/>
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </Button.Style>
    </Button>
        <TextBox Text="{Binding Number, Mode=OneWay}" IsReadOnly="True"
                 Grid.Row="1" Grid.Column="2" VerticalAlignment="Center"/>
    </Grid>
</Window>

Second variant.
Create an explicit proxy converter.

Converter:

using System;
using System.ComponentModel;
using System.Windows;

namespace InvalidateCommandMvvmLight
{
    public class ProxyBinding : Freezable
    {
        public Type Type
        {
            get { return (Type)GetValue(TypeProperty); }
            set { SetValue(TypeProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Type.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty TypeProperty =
            DependencyProperty.Register(nameof(Type), typeof(Type), typeof(ProxyBinding), new PropertyMetadata(typeof(object), ChangedValueOrType));

        private static void ChangedValueOrType(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ProxyBinding proxy = (ProxyBinding)d;
            if (proxy.Type == null)
            {
                proxy.Value = null;
                return;
            }
            if (proxy.Source == null)
                return;

            if (proxy.Type == proxy.Source.GetType())
                return;

            if (TypeDescriptor.GetConverter(proxy.Type).IsValid(proxy.Source))
                proxy.Value = TypeDescriptor.GetConverter(proxy.Type).ConvertFrom(proxy.Source);
            else
                proxy.Value = null;
        }

        public object Source
        {
            get { return GetValue(SourceProperty); }
            set { SetValue(SourceProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Value.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty SourceProperty =
            DependencyProperty.Register(nameof(Source), typeof(object), typeof(ProxyBinding), new PropertyMetadata(null, ChangedValueOrType));

        public object Value
        {
            get { return GetValue(ValueProperty); }
            protected  set { SetValue(ValuePropertyKey, value); }
        }

        // Using a DependencyProperty as the backing store for readonly Value.  This enables animation, styling, binding, etc...
        protected static readonly DependencyPropertyKey ValuePropertyKey =
            DependencyProperty.RegisterReadOnly(nameof(Value), typeof(object), typeof(ProxyBinding), new PropertyMetadata(null));
        public static readonly DependencyProperty ValueProperty = ValuePropertyKey.DependencyProperty;

        protected override Freezable CreateInstanceCore()
        {
            return new ProxyBinding();
        }
    }
}

XAML:

<Window x:Class="InvalidateCommandMvvmLight.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"
            xmlns:local="clr-namespace:InvalidateCommandMvvmLight"
            xmlns:vm="clr-namespace:InvalidateCommandMvvmLight.ViewModel"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        mc:Ignorable="d"
            Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <vm:MyViewModel/>
    </Window.DataContext>
    <Window.Resources>
        <local:ProxyBinding x:Key="ProxyInt"
                Type="{x:Type sys:Int32}"
                Source="{Binding ElementName=tbNumber, Path=Text, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"/>
    </Window.Resources>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <TextBox x:Name="tbText"
                    Text="Alle eendjes zwemmen in het water" VerticalAlignment="Center"
                    />

        <Button Content="Change Text"
                    Grid.Column="1"
                    Margin="5"
                    Padding="5,2"
                    Command="{Binding Path=CommandTest}"
                    CommandParameter="{Binding ElementName=tbText, Path=Text}"/>
        <TextBox Text="{Binding Text, Mode=OneWay}" Grid.Column="2" IsReadOnly="True" VerticalAlignment="Center"/>
        <TextBox x:Name="tbNumber"
                    Grid.Row="1"
                    Text="55" VerticalAlignment="Center"/>

        <Button Content="Change Number"
                    Grid.Row="1" Grid.Column="1"
                    Margin="5"
                    Padding="5,2"
                    Command="{Binding Path=CommandNumber}"
                    CommandParameter="{Binding Value, Source={StaticResource ProxyInt}}">
        </Button>
        <TextBox Text="{Binding Number, Mode=OneWay}" IsReadOnly="True"
                     Grid.Row="1" Grid.Column="2" VerticalAlignment="Center"/>
        <TextBlock Grid.Row="2" Text="{Binding Value,Source={StaticResource proxy}}"/>
    </Grid>
</Window>

Another variant.
Create converter for bindings:

using System;
using System.ComponentModel;
using System.Globalization;
using System.Windows.Data;

namespace InvalidateCommandMvvmLight
{
    public class ValueTypeConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (parameter is Type type && TypeDescriptor.GetConverter(type).IsValid(value))
                return TypeDescriptor.GetConverter(type).ConvertFrom(value);
            return null;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

XAML:

<Window x:Class="InvalidateCommandMvvmLight.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"
            xmlns:local="clr-namespace:InvalidateCommandMvvmLight"
            xmlns:vm="clr-namespace:InvalidateCommandMvvmLight.ViewModel"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        mc:Ignorable="d"
            Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <vm:MyViewModel/>
    </Window.DataContext>
    <Window.Resources>
    <local:ValueTypeConverter x:Key="ValueTypeConverter"/>
</Window.Resources>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <TextBox x:Name="tbText"
                    Text="Alle eendjes zwemmen in het water" VerticalAlignment="Center"
                    />

        <Button Content="Change Text"
                    Grid.Column="1"
                    Margin="5"
                    Padding="5,2"
                    Command="{Binding Path=CommandTest}"
                    CommandParameter="{Binding ElementName=tbText, Path=Text}"/>
        <TextBox Text="{Binding Text, Mode=OneWay}" Grid.Column="2" IsReadOnly="True" VerticalAlignment="Center"/>
        <TextBox x:Name="tbNumber"
                    Grid.Row="1"
                    Text="55" VerticalAlignment="Center"/>

        <Button Content="Change Number"
                    Grid.Row="1" Grid.Column="1"
                    Margin="5"
                    Padding="5,2"
                    Command="{Binding Path=CommandNumber}"
                    CommandParameter="{Binding Text, Converter={StaticResource ValueTypeConverter}, ConverterParameter={x:Type sys:Int32}, ElementName=tbNumber}">
        </Button>
        <TextBox Text="{Binding Number, Mode=OneWay}" IsReadOnly="True"
                     Grid.Row="1" Grid.Column="2" VerticalAlignment="Center"/>
    </Grid>
</Window>
EldHasp
  • 6,079
  • 2
  • 9
  • 24