WPF: Update button if text in TextBox changes

守給你的承諾、 提交于 2020-08-06 04:47:27

问题


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?


回答1:


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.




回答2:


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

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

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.




回答3:


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.




回答4:


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.



来源:https://stackoverflow.com/questions/63174956/wpf-update-button-if-text-in-textbox-changes

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!