2

To learn WPF Command and CommandParameter I have a small WPF application with one TextBox and one Button. Whenever the button is pressed, ICommandTest should be called with the text of the text box as parameter.

This works fine. The next step is: if the text becomes too small, the button should be disabled.

I use MVVMLight to implement the command. The code below is enough to call method Test whenever the button is pressed.

Code so far

The following works: At startup the text box gets its proper initial text. The button asks the view model whether this text can be used as parameter for the test:

public class MyViewModel
{
    public ICommand CommandTest {get;}

    public MyViewModel()
    {
        this.CommandTest = new RelayCommand<string>(this.Test, this.CanTest); 
    }

    private bool CanTest(string text)
    {
        // text should have a minimum length of 4
        return text != null && text.Length >= 4;
    }
    private void Test(string text)
    {
        //...
    }

    // ...

}

XAML: An editable text box and a button in a horizontal StackPanel.

<StackPanel Name="Test" Orientation="Horizontal" Background="AliceBlue">
    <TextBox Name="ProposedTestValue"
             Text="Alle eendjes zwemmen in het water"
             Width="500" Height="20"/>

    <Button x:Name="ButtonTest" Content="Change"
                    Height="auto" Width="74"
                    Padding="5,2"
                    Command="{Binding Path=CommandTest}"
                    CommandParameter="{Binding ElementName=ProposedTestValue, Path=Text}"/>
</StackPanel>

Text Changes

If I change the text and press the button, the command is called with the changed text. So Command and CommandParameter work.

However, if the text becomes smaller than 4 characters, the button doesn't disable.. Every time that the value of the bound CommandParameter of the button changes, the button should ask its command if it can be executed.

How to do this?

NotifyOnSourceUpdated

Yosef Bernal suggested to add NotifyOnSourceUpdated:

<Button x:Name="ButtonChangeTestText" Content="Change"
                Height="30" Width="74" Padding="5,2"
                Command="{Binding Path=CommandTest}"
                CommandParameter="{Binding ElementName=ProposedTestTextValue,
                    Path=Text, NotifyOnSourceUpdated=True}"/>

Alas, that didn't change anything: at startup an initial CanTest is called, with the correct parameter. Changing the text doesn't cause a CanTest. If I press the button CanTest is called with the correct value. If the text is small, CanTest returns false, and thus the command is not execute. However, even though CanExecute returned false, the button remains enabled.

Should I tell the Button what to do if not CanExecute? Or is disabling the button the default behaviour?

Harald Coppoolse
  • 28,834
  • 7
  • 67
  • 116
  • There is no way for your command to know that the text in you `TextBox` has changed. Normally you would need to have `Text` property in your view model that is bound to your `TextBox`'s `Text` property, enabling updates on property change in your XAML. You could then test that property each time it gets updated. You view model will have to implement the INotifyPropertyChanged interface for this to work. I will submit a working example should you need it. – Yosef Bernal Jul 30 '20 at 14:13
  • @Yosef Bernal, You are not right.The command parameter is bound in XAML. And when it changes, the state of the command is checked again: the CanExecute method is automatically called. – EldHasp Jul 30 '20 at 14:19
  • Judging by the provided code, everything is done correctly. And the button should be disabled. I don't see what the mistake is. It is possible outside of this code. Upload the Solution to GitHub - I'll see what the problem is. – EldHasp Jul 30 '20 at 14:24
  • EldHasp: I've never updated a solution to GitHub before. Do you want the complete (minimal) visual studio solution? – Harald Coppoolse Jul 30 '20 at 14:24
  • @EldHasp thank you for your comment. I'm not a fan of using command parameters, that's why my comment purposes a solution that is independent of them. – Yosef Bernal Jul 30 '20 at 14:35
  • Which namespace are you using for relaycommand? The net core version doesn't have commandwpf and canexecute won't automatically be read. You want the commandwpf version of relaycommand. – Andy Jul 30 '20 at 17:32

4 Answers4

3

You can bind the Text property of your TextBox to a Text property on MyViewModel.

<TextBox Name="ProposedTestValue" Text="{Binding Text}" Width="500" Height="20"/>

Create a corresponding Text property in your MyViewModel with a backing field _text.

private string _text;

public string Text
{
   get => _text;
   set
   {
      if (_text != value)
      {
         _text = value;
         CommandTest.RaiseCanExecuteChanged();
      }
   }
}

The RaiseCanExecuteChanged method will force a re-evaluation of CanExecute whenever the Text property is updated, which depends on your UpdateSourceTrigger. You do not need the CommandParameter anymore, since you can use the Text property in your view model.

public MyViewModel()
{
   this.CommandTest = new RelayCommand(this.Test, this.CanTest); 
}

private bool CanTest()
{
   return Text != null && Text.Length >= 4;
}

private void Test()
{
   // ...use "Text" here.
}

Note: If you intend to update the Text property from your view model, you have to implement INotifyPropertyChanged, otherwise the changed value will not be reflected in the view.

thatguy
  • 21,059
  • 6
  • 30
  • 40
  • Agreed, this works. But why is there a parameter in `ICommand.Execute(...)` if I can use the property. And is it normal that I should change my ViewModel if any of my thousand views decide that it will have an extra textbox, especially if the data in the textbox is not data of my viewModel – Harald Coppoolse Jul 30 '20 at 14:48
  • [`ICommand`](https://docs.microsoft.com/en-us/dotnet/api/system.windows.input.icommand?view=netcore-3.1) is an interface that defines the `Execute` and `CanExecute` methods to have a parameter, but it is optional, it depends on your use-case if you use it or not. That is why MVVM-Light also has a `RelayCommand` and a `RelayCommand` that implement `ICommand`. Even though `RelayCommand` has no parameter, internally it implements `Execute(object parameter)` with `null`. Sometimes you even want to use multiple parameters or ones that live only in your view model, which favors this approach. – thatguy Jul 30 '20 at 15:05
1

Harald Coppoolse, there is no error in your code! It's outside of the code you've posted. Possibly in the wrong implementation of the RelayCommand.

Here is an example of the implementation I am using:
RelayCommand

using System;
using System.Windows;
using System.Windows.Input;

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(object 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(object parameter);
    #endregion

    #region Class commands - RelayCommand
    /// <summary>A class that implements the ICommand interface for creating WPF commands.</summary>
    public class RelayCommand : ICommand
    {
        private readonly CanExecuteHandler _canExecute;
        private readonly ExecuteHandler _onExecute;
        private readonly EventHandler _requerySuggested;

        public event EventHandler CanExecuteChanged;

        /// <summary>Command constructor.</summary>
        /// <param name="execute">Executable command method.</param>
        /// <param name="canExecute">Method allowing command execution.</param>
        public RelayCommand(ExecuteHandler execute, CanExecuteHandler canExecute = null)
        {
            _onExecute = execute;
            _canExecute = canExecute;

            _requerySuggested = (o, e) => Invalidate();
            CommandManager.RequerySuggested += _requerySuggested;
        }

        public void Invalidate()
            => Application.Current.Dispatcher.BeginInvoke
            (
                new Action(() => CanExecuteChanged?.Invoke(this, EventArgs.Empty)),
                null
            );

        public bool CanExecute(object parameter) => _canExecute == null ? true : _canExecute.Invoke(parameter);

        public void Execute(object parameter) => _onExecute?.Invoke(parameter);
    }

    #endregion

}

RelayCommand<T>

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(p is T t ? t : default), p => p is T t && (canExecute?.Invoke(t) ?? true)) { }

    }
}

BaseINPC

using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace Common
{
    /// <summary>Base class implementing INotifyPropertyChanged.</summary>
    public abstract class BaseINPC : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>Called AFTER the property value changes.</summary>
        /// <param name="propertyName">The name of the property.
        /// In the property setter, the parameter is not specified. </param>
        public void RaisePropertyChanged([CallerMemberName] string propertyName = "")
            => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

        /// <summary> A virtual method that defines changes in the value field of a property value. </summary>
        /// <typeparam name = "T"> Type of property value. </typeparam>
        /// <param name = "oldValue"> Reference to the field with the old value. </param>
        /// <param name = "newValue"> New value. </param>
        /// <param name = "propertyName"> The name of the property. If <see cref = "string.IsNullOrWhiteSpace (string)" />,
        /// then ArgumentNullException. </param> 
        /// <remarks> If the base method is not called in the derived class,
        /// then the value will not change.</remarks>
        protected virtual void Set<T>(ref T oldValue, T newValue, [CallerMemberName] string propertyName = "")
        {
            if (string.IsNullOrWhiteSpace(propertyName))
                throw new ArgumentNullException(nameof(propertyName));

            if ((oldValue == null && newValue != null) || (oldValue != null && !oldValue.Equals(newValue)))
                OnValueChange(ref oldValue, newValue, propertyName);
        }

        /// <summary> A virtual method that changes the value of a property. </summary>
        /// <typeparam name = "T"> Type of property value. </typeparam>
        /// <param name = "oldValue"> Reference to the property value field. </param>
        /// <param name = "newValue"> New value. </param>
        /// <param name = "propertyName"> The name of the property. </param>
        /// <remarks> If the base method is not called in the derived class,
        /// then the value will not change.</remarks>
        protected virtual void OnValueChange<T>(ref T oldValue, T newValue, string propertyName)
        {
            oldValue = newValue;
            RaisePropertyChanged(propertyName);
        }

    }
}

MyViewModel

using Common;

namespace RenderCanCommand
{
    public class MyViewModel : BaseINPC
    {
        private string _text;
        public string Text { get => _text; private set => Set(ref _text, value); }

        public RelayCommand<string> CommandTest { get; }

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

        private bool CanTest(string text)
        {
            // text should have a minimum length of 4
            return text != null && text.Length >= 4 && text != Text;
        }
        private void Test(string text)
        {
            Text = text;

        }
    }
}

Window XAML

<Window x:Class="RenderCanCommand.TestWind"
        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:RenderCanCommand"
        mc:Ignorable="d"
        Title="TуstWind" Height="450" Width="800">
    <Window.DataContext>
        <local:MyViewModel/>
    </Window.DataContext>
    <StackPanel Orientation="Horizontal" Background="AliceBlue">
        <TextBox Name="ProposedTestValue"
             Text="Alle eendjes zwemmen in het water"
             Width="500" Height="20"/>

        <Button Content="Change"
                Height="auto" Width="74"
                Padding="5,2"
                Command="{Binding Path=CommandTest}"
                CommandParameter="{Binding ElementName=ProposedTestValue, Path=Text}"/>
        <TextBox Text="{Binding Text, Mode=OneWay}" IsReadOnly="True" Width="500" Height="20"/>
    </StackPanel>
</Window>

Everything is working. If the length of the text is less than four or the text is the same, then the button becomes inactive.

EldHasp
  • 6,079
  • 2
  • 9
  • 24
  • 1
    @aepot, most of the code is comments (XML documentation). If you remove them, very few lines will remain there. The only extension to ICommand is the Invalidate () method. It method is needed to update the command state from the ViewModel. Asynchronous commands can used. But don't abuse them. A command is a View of a component. And WPF View is always run in one thread (Dispatcher). It is better to create other flows in the Model. Therefore, the ViewModel uses the usual commands. And the methods in them, if necessary, refer to the asynchronous methods of the Model. – EldHasp Jul 30 '20 at 19:10
  • 1
    Ok, that makes sense. – aepot Jul 31 '20 at 09:33
  • This works. The minimum is the part where you pass `CanExecutChanged` to `CommandManager.RequerySuggested`, which is in fact what you do. This is suggested in [Allowing CommandManager to query your ICommand objects](https://joshsmithonwpf.wordpress.com/2008/06/17/allowing-commandmanager-to-query-your-icommand-objects/). The strange thing is, that I can see something similar in [MVVMLight source code](https://github.com/lbugnion/mvvmlight/blob/master/GalaSoft.MvvmLight/GalaSoft.MvvmLight%20(PCL)/Command/RelayCommandGeneric.cs). So still don't know why it doesn't work – Harald Coppoolse Aug 03 '20 at 13:18
  • From what you have outlined here, I cannot understand the reasons for your problems. I use MVVMLight occasionally and have never had any problems with commands. More information is needed to understand the causes of the problems. If you can, please post an example on GitHub that demonstrates your problem. – EldHasp Aug 03 '20 at 14:46
0

A simple solution below.

Some suggested to add a property ProposedTestValue to the ViewModel, and use that value instead of the CommandParameter to update the actual accepted Value, the value after the button is pressed.

The latter solution seems a bit strange: my model does not have a notion of a proposed value that would eventually turn out to be an accepted after-button-press value. Besides this, it would mean that I would have to change my ViewModel whenever I wanted to add a textbox-button combination.

I've tested the solution of EldHasp, and it works. Thanks EldHasp for your extensive answer.

However, I don't want to deviate from MvvmLight too much, just for this apparently rare problem. Besides: I would never be able to convince my project leader to do this! :(

Fairly Simple Solution

ICommand has an event CanExecuteChanged. Whenever the text in the text box changes, an event handler should raise this event. Luckily RelayCommand<...> has a method to do this.

XAML

<TextBox Name="ProposedTestValue" Width="500" Height="20"
         Text="Alle eendjes zwemmen in het water"
         TextChanged="textChangedEventHandler"/>

Code Behind

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

These few lines are enough code to make sure that CanTest(...) is checked whenever the text changes.

I always feel a bit uneasy if I have to write code behind, I only see this in WPF tutorials. So if someone sees a better solution without a lot of code, Please do, if it is cleaner than this one, I'll be happy to select yours as solution.

Harald Coppoolse
  • 28,834
  • 7
  • 67
  • 116
  • I can't figure out why MVVMLight doesn't work for you. Maybe you installed the wrong library, version? If I implement my example with the types offered by MVVMLight, then everything works for me too. A forced call to RaiseCanExecuteChanged is not required. Why is this different for you ... I dont know. – EldHasp Aug 04 '20 at 14:32
  • Visual Studio - References - Manage Nuget Packages - Installed Packages: MvvmLight by Laurent Bugnion 5.4.1.1; same for MvvmLighLibs; CommonServiceLocator Microsoft.Practices.ServiceLocation v2.0.5. After installing the Nuget package I had a problem in the `ViewModelLocator`: `using Microsoft.Practices.ServiceLocation`; had to comment this out and add `using CommonServiceLocator`. Can this latter be the cause of the problem? – Harald Coppoolse Aug 04 '20 at 15:29
  • I will try to reproduce the same error and understand the cause. Another topic also discusses the incorrect operation of the RelayCommand from MVVMLight. https://stackoverflow.com/questions/63248032/wpf-eventhandler-for-textbox-textchanged-in-xaml-or-code-behind – EldHasp Aug 04 '20 at 15:35
0

It turns out that 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 it works similarly to my examples. The state of the command is updated according to the predetermined logic.

EldHasp
  • 6,079
  • 2
  • 9
  • 24