简体   繁体   中英

Disable Submit button if input in Textbox's Text inside UserControl is invalid

  1. I have a User Control that takes input from user. This user control informs the user whether input value within the allowable range.

  2. In the main Window, I will use several of this User Control to get values of different parameters from the User.

  3. At the end, there will be a submit button.

  4. Now, I want to disable the button, if any one of the input values in the user control is out of range. How do I do that? I am stuck here.

  5. If this is the wrong way to do it, please do correct me as well. I would love to follow a best practice method.

Many many thanks in advance.

XAML Code for User Control "UserInputFieldUC":

<UserControl x:Class="TestUserInput.UserInputFieldUC"
             ...
             xmlns:local="clr-namespace:TestUserInput"
             x:Name="parent">
    <StackPanel Orientation="Horizontal" DataContext="{Binding ElementName=parent}" >

        <TextBlock Name="UserLabel" Width="50" Text="{Binding Path=Label}"/>

        <TextBox Name="MetricValue" Width="50" Text="{Binding Path=Value}" TextChanged="MetricValue_TextChanged"/>
        <TextBlock Name="MetricUnits" Width="50" Text="{Binding Path=Units}" VerticalAlignment="Center"/>

        <TextBlock Name="ErrorDisplay" Width="50" VerticalAlignment="Center"/>        
    </StackPanel>
</UserControl>

Code-behind for "UserInputFieldUC":

namespace TestUserInput
{
    /// <summary>
    /// Interaction logic for UserInputFieldUC.xaml
    /// </summary>
    public partial class UserInputFieldUC : UserControl
    {
        public UserInputFieldUC()
        {
            InitializeComponent();
        }
        #region Label DP
        /// <summary>
        /// Label dependency property
        /// </summary>
        public static readonly DependencyProperty LPShowFieldUCPercentCheck =
            DependencyProperty.Register("Label",
                typeof(string),
                typeof(UserInputFieldUC),
                new PropertyMetadata(""));
        /// <summary>
        /// Gets or sets the Label which is displayed to the field
        /// </summary>
        public string Label
        {
            get { return GetValue(LPShowFieldUCPercentCheck) as String; }
            set { SetValue(LPShowFieldUCPercentCheck, value); }
        }
        #endregion // Label DP
        #region Value DP
        /// <summary>
        /// Value dependency property.  
        /// </summary>
        public static readonly DependencyProperty ValueProp =
            DependencyProperty.Register("Value",
                typeof(string),
                typeof(UserInputFieldUC),
                new PropertyMetadata(""));
        /// <summary>
        /// Gets or sets the value being displayed
        /// </summary>
        public string Value
        {
            get { return GetValue(ValueProp) as String; }
            set { SetValue(ValueProp, value); }
        }
        #endregion // Value DP
        #region Units DP
        /// <summary>
        /// Units dependency property
        /// </summary>
        public static readonly DependencyProperty UnitsProperty =
            DependencyProperty.Register("Units",
                typeof(string),
                typeof(UserInputFieldUC),
                new PropertyMetadata(""));
        /// <summary>
        /// Gets or sets the Units which is displayed to the field
        /// </summary>
        public string Units
        {
            get { return GetValue(UnitsProperty) as String; }
            set { SetValue(UnitsProperty, value); }
        }
        #endregion // Units DP
        #region Maximum Allowable Input Value DP
        public static readonly DependencyProperty MaxInputProperty =
            DependencyProperty.Register("UpperLimit",
                typeof(string),
                typeof(UserInputFieldUC), new PropertyMetadata(""));
        public string UpperLimit
        {
            get { return GetValue(MaxInputProperty) as String; }
            set { SetValue(MaxInputProperty, value); }
        }
        #endregion // Max Value DP
        #region Minimum Allowable Input DP
        public static readonly DependencyProperty MinInputProperty =
            DependencyProperty.Register("LowerLimit",
                typeof(string),
                typeof(UserInputFieldUC), new PropertyMetadata(""));
        public string LowerLimit
        {
            get { return GetValue(MinInputProperty) as String; }
            set { SetValue(MinInputProperty, value); }
        }
        #endregion // Max Value DP
        #region Display Error DP
        public static readonly DependencyProperty ErrorProperty =
            DependencyProperty.Register("ShowErr",
                typeof(string),
                typeof(UserInputFieldUC),
                new PropertyMetadata(""));
        public string ShowErr
        {
            get { return GetValue(ErrorProperty) as String; }
            set { SetValue(ErrorProperty, value); }
        }
        #endregion // Display Error DP

        /// <summary>
        /// Check user input
        /// </summary>
        private void MetricValue_TextChanged(object sender, TextChangedEventArgs e)
        {
            string inputText = MetricValue.Text;
            if (inputText == "")
                inputText = "0";
            double inputValue = Convert.ToDouble(inputText);

            double maxValue = Convert.ToDouble(UpperLimit);
            double minValue = Convert.ToDouble(LowerLimit);

            ErrorDisplay.Text = "OK";            
            if (inputValue <= minValue)
            {
                ErrorDisplay.Text = "Err";
            }
            if (inputValue >= maxValue)
            {
                ErrorDisplay.Text = "Err";
            }        
        }
    }
}

XAML code for "MainWindow" displaying User Controls and submit button:

<Window x:Class="TestUserInput.MainWindow"
        ...
        xmlns:local="clr-namespace:TestUserInput"
        Title="MainWindow" Height="450" Width="350">
    <Grid>
        <StackPanel Margin="5">
            <local:UserInputFieldUC Margin="5" Label="Param1" Value="{Binding Path=Param1}" Units="m" LowerLimit="5" UpperLimit="10"/>
            <local:UserInputFieldUC Margin="5" Label="Param2" Value="{Binding Path=Param2}" Units="m" LowerLimit="50" UpperLimit="100"/>
            <Button Content="Submit"/>
        </StackPanel>

    </Grid>
</Window>

and the viewmodel for the main Window implements the usual INotifyPropertyChangedview interface

    public class MainWindowViewModel : INotifyPropertyChanged
    {
        public double Param1 { get; set; }
        public double Param2 { get; set; }
        ...
        ...
    }

The usual approach is validation combined with canexecute of a command.

You would expose an implementation of ICommand as a public property on your window viewmodel. Call that SubmitCommand. Often a relaycommand or delegatecommand but that's perhaps slightly off the subject.

One of the things icommand offers is canexecute. This is a method returning true or false. If it returns false, a button with this command bound will be disabled.

Your mainwindowviewmodel would also implement one of the validation interfaces. InotifyDataErrorinfo being the recommended one.

A full complete working sample would be quite involved.

Essentially, you need two things.

Your view needs to tell the viewmodel if there's a conversion failure from view to bound property. This would be text typed in a textbox bound to a double eg. The viewmodel wouldn't know about these failures otherwise.

The viewmodel also needs to check values for properties when they change. Looks like a range attribute could be used in this specific case but you also usually need a series of funcs.

I'll see if I can find some code.

In the view you want to handle binding transfer error events which will bubble up. You get a notification when an error is added and also when one is removed.

The sample I've found is.Net old with mvvmlight and probably isn't a direct paste to .net core. It still illustrates the principle though.

In a parent panel ( eg the main grid of your window ). You want some event handlers. My sample uses interactions ( now xaml behaviors nuget ).

    <i:Interaction.Triggers>
        <UIlib:RoutedEventTrigger RoutedEvent="{x:Static Validation.ErrorEvent}">
            <e2c:EventToCommand
                 Command="{Binding ConversionErrorCommand, Mode=OneWay}"
                 EventArgsConverter="{StaticResource BindingErrorEventArgsConverter}"
                 PassEventArgsToCommand="True" />
        </UIlib:RoutedEventTrigger>
        <UIlib:RoutedEventTrigger RoutedEvent="{x:Static Binding.SourceUpdatedEvent}">
            <e2c:EventToCommand
                 Command="{Binding SourceUpdatedCommand, Mode=OneWay}"
                 EventArgsConverter="{StaticResource BindingSourcePropertyConverter}"
                 PassEventArgsToCommand="True" />
        </UIlib:RoutedEventTrigger>
    </i:Interaction.Triggers>

Those converters:

    public object Convert(object value, object parameter)
    {
        ValidationErrorEventArgs e = (ValidationErrorEventArgs)value;
        PropertyError err = new PropertyError();
        err.PropertyName = ((System.Windows.Data.BindingExpression)(e.Error.BindingInError)).ResolvedSourcePropertyName;
        err.Error = e.Error.ErrorContent.ToString();
        // Validation.ErrorEvent fires both when an error is added AND removed
        if (e.Action == ValidationErrorEventAction.Added)
        {
            err.Added = true;
        }
        else
        {
            err.Added = false;
        }
        return err;
    }

As I mentioned, you get an event for an error added and another when removed. Hence the if else.

The other converter is telling the viewmodel which property changed. There are other options like acting in a generic base method used for property change notification from all property setters.

public class BindingSourcePropertyConverter : IEventArgsConverter
{
    public object Convert(object value, object parameter)
    {
        DataTransferEventArgs e = (DataTransferEventArgs)value;
        Type type = e.TargetObject.GetType();
        BindingExpression binding = ((FrameworkElement)e.TargetObject).GetBindingExpression(e.Property);
        return binding.ParentBinding.Path.Path ?? "";
        //return binding.ResolvedSourcePropertyName ?? "";
    }
}

Notice this works straight out the box with a viewmodel just has value type properties. A property of your viewmodel which is a complex object and has bound properties another layer down will need more sophistication.

In the view, you also need to tell it to notify from each and every binding you want to validate data on:

            <TextBox Text="{Binding FirstName
                                  , UpdateSourceTrigger=PropertyChanged
                                  , NotifyOnSourceUpdated=True}"

The base viewmodel is kind of complicated. As you'll see, it implements INotifyDataErrorInfo. Validator.Validate is used on the viewmodel which will check dataannotations on it. There's also a list of predicates used which handle complicated validation which isn't suited to annotations.

All this is used to drive a property IsValid. When that is false your canexecute on your submitcommand should return false.

public class BaseValidVM :  BaseNotifyUI, INotifyDataErrorInfo, INotifyPropertyChanged
{
    // From Validation Error Event
    private RelayCommand<PropertyError> conversionErrorCommand;
    public RelayCommand<PropertyError> ConversionErrorCommand
    {
        get
        {
            return conversionErrorCommand
                ?? (conversionErrorCommand = new RelayCommand<PropertyError>
                    (PropertyError =>
                    {
                        if (PropertyError.Added)
                        {
                            AddError(PropertyError.PropertyName, PropertyError.Error, ErrorSource.Conversion);
                        }
                        FlattenErrorList();
                    }));
        }
    }
    // From Binding SourceUpdate Event
    private RelayCommand<string> sourceUpdatedCommand;
    public RelayCommand<string> SourceUpdatedCommand
    {
        get
        {
            return sourceUpdatedCommand
                ?? (sourceUpdatedCommand = new RelayCommand<string>
                    (Property =>
                    {
                        ValidateProperty(Property);
                    }));
        }
    }
    private RelayCommand validateCommand;
    public RelayCommand ValidateCommand
    {
        get
        {
            return validateCommand
               ?? (validateCommand = new RelayCommand
                    (() =>
                    {
                        bool isOk = IsValid;
                        RaisePropertyChanged("IsValid");
                    }));
        }
    }

    private ObservableCollection<PropertyError> errorList = new ObservableCollection<PropertyError>();
    public ObservableCollection<PropertyError> ErrorList
    {
        get
        {
            return errorList;
        }
        set
        {
            errorList = value;
            RaisePropertyChanged();
        }
    }

    protected Dictionary<string, List<AnError>> errors = new Dictionary<string, List<AnError>>();

    protected bool isBusy = false;
    public bool IsBusy
    {
        get { return isBusy; }
        set { isBusy = value;  RaisePropertyChanged("IsBusy"); }
    }
    
    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

     public IEnumerable GetErrors(string property)
    {
        if (string.IsNullOrEmpty(property))
        {
            return null;
        }
        if (errors.ContainsKey(property) && errors[property] != null && errors[property].Count > 0)
        {
            return errors[property].Select(x => x.Text).ToList();
        }
        return null;
    }
    public bool HasErrors
    {
        get { return errors.Count > 0; }
    }
    public void NotifyErrorsChanged(string propertyName)
    {
        if (ErrorsChanged != null)
        {
            ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
        }
    }

    public virtual Dictionary<string, List<PredicateRule>> ValiditionRules { get; set; }
    private List<string> lastListFailures = new List<string>();
    public bool IsValid {
        get
        {
            // Clear only the errors which are from object Validation
            // Conversion errors won't be detected here
            RemoveValidationErrorsOnly();

            var vContext = new ValidationContext(this, null, null);
            List<ValidationResult> vResults = new List<ValidationResult>();
            Validator.TryValidateObject(this, vContext, vResults, true);
            TransformErrors(vResults);

            // Iterating the dictionary allows you to check the rules for each property which has any rules
            if(ValiditionRules != null)
            {
                foreach (KeyValuePair<string, List<PredicateRule>> ppty in ValiditionRules)
                {
                    ppty.Value.Where(x => x.IsOK(this) == false)
                               .ToList()
                               .ForEach(x =>
                                     AddError(ppty.Key, x.Message, ErrorSource.Validation)
                                );
                }
            }

            var propNames = errors.Keys.ToList();
            propNames.Concat(lastListFailures)
                     .Distinct()
                     .ToList()
                     .ForEach(pn => NotifyErrorsChanged(pn));
            lastListFailures = propNames;

            FlattenErrorList();

            //foreach (var item in errors)
            //{
            //    Debug.WriteLine($"Errors on {item.Key}");
            //    foreach (var err in item.Value)
            //    {
            //        Debug.WriteLine(err.Text);
            //    }
            //}

            if (propNames.Count > 0)
            {
                return false;
            }
            return true;
        }
    }
    private void RemoveValidationErrorsOnly()
    {
        foreach (KeyValuePair<string, List<AnError>> pair in errors)
        {
            List<AnError> _list = pair.Value;
            _list.RemoveAll(x => x.Source == ErrorSource.Validation);
        }

        var removeprops = errors.Where(x => x.Value.Count == 0)
            .Select(x => x.Key)
            .ToList();
        foreach (string key in removeprops)
        {
            errors.Remove(key);
        }
    }
    public void ValidateProperty(string propertyName)
    {
        errors.Remove(propertyName);
        lastListFailures.Add(propertyName);

        if(!propertyName.Contains("."))
        {
            var vContext = new ValidationContext(this, null, null);
            vContext.MemberName = propertyName;
            List<ValidationResult> vResults = new List<ValidationResult>();
            Validator.TryValidateProperty(this.GetType().GetProperty(propertyName).GetValue(this, null), vContext, vResults);

            TransformErrors(vResults);
        }

        // Apply Predicates
        // ****************
        if (ValiditionRules !=null && ValiditionRules.ContainsKey(propertyName))
        {
            ValiditionRules[propertyName].Where(x => x.IsOK(this) == false)
                                         .ToList()
                                         .ForEach(x =>
                                          AddError(propertyName, x.Message, ErrorSource.Validation)
                                           );
        }
        FlattenErrorList();
        NotifyErrorsChanged(propertyName);
        RaisePropertyChanged("IsValid");
    }
    private void TransformErrors(List<ValidationResult> results)
    {
        foreach (ValidationResult r in results)
        {
            foreach (string ppty in r.MemberNames)
            {
                AddError(ppty, r.ErrorMessage, ErrorSource.Validation);
            }
        }
    }
    private void AddError(string ppty, string err, ErrorSource source)
    {
        List<AnError> _list;
        if (!errors.TryGetValue(ppty, out _list))
        {
            errors.Add(ppty, _list = new List<AnError>());
        }
        if (!_list.Any(x => x.Text == err))
        {
            _list.Add(new AnError { Text = err, Source = source });
        }
    }
    private void FlattenErrorList()
    {
        ObservableCollection<PropertyError> _errorList = new ObservableCollection<PropertyError>();
        foreach (var prop in errors.Keys)
        {
            List<AnError> _errs = errors[prop];
            foreach (AnError err in _errs)
            {
                _errorList.Add(new PropertyError { PropertyName = prop, Error = err.Text });
            }
        }
        ErrorList = _errorList;
    }
    public void ClearErrors()
    {
        List<string> oldErrorProperties = errors.Select(x => x.Key.ToString()).ToList();
        errors.Clear();
        ErrorList.Clear();
        foreach (var p in oldErrorProperties)
        {
            NotifyErrorsChanged(p);
        }
        NotifyErrorsChanged("");
    }
}

An example viewmodel:

public class UserControl1ViewModel : BaseValidVM
{
    private string firstName;
    [Required]
    [StringLength(20, MinimumLength = 2, ErrorMessage = "Invalid length for first name")]
    public string FirstName
    {
        get { return firstName; }
        set { firstName = value; RaisePropertyChanged(); }
    }
    private string surName;

    [Required]
    [StringLength(40, MinimumLength = 4, ErrorMessage = "Invalid length for last name")]
    public string SurName
    {
        get { return surName; }
        set { surName = value; RaisePropertyChanged(); }
    }

    private Decimal amount = 1;
    [Required]
    [MaxDecimalPlaces(MantissaDigits = 1)]
    public Decimal Amount
    {
        get { return amount; }
        set { amount = value; RaisePropertyChanged(); }
    }

    private Decimal amount2 = 2;
    [Required]
    [MaxDecimalPlaces(ErrorMessage = "Amount 2 is money, it can have up to 2 numbers after the decimal place.")]
    public Decimal Amount2
    {
        get { return amount2; }
        set { amount2 = value; RaisePropertyChanged(); }
    }

    private DateTime orderDate = DateTime.Now.Date;
    [Required]
    public DateTime OrderDate
    {
        get { return orderDate; }
        set { orderDate = value; RaisePropertyChanged(); }
    }


    private RelayCommand saveCommand;
    // If IsValid is false then a button bound to savecommand will be disabled.
    public RelayCommand SaveCommand
    {
        get
        {
            return saveCommand
               ?? (saveCommand = new RelayCommand
                    (async () =>
                    {
                        if (IsBusy)
                        {
                            return;
                        }
                        IsBusy = true;
                        Debug.WriteLine("Would have saved");
                        await Task.Delay(2000); // Simulates a delay doing something DO NOT USE LIVE
                        IsBusy = false;
                        SaveCommand.RaiseCanExecuteChanged(); // Force UI to requery canexecute
                    },
                     () => IsValid && !IsBusy  // CanExecute when valid and not busy
                    ));
        }
    }

    // Empty string is validation which is not at all property specific
    // Otherwise.
    // Add an entry per property you need validation on with the list of PredicateRule containing the validation(s)
    // to apply.
    public override Dictionary<string, List<PredicateRule>> ValiditionRules { get; set; } 

    public UserControl1ViewModel()
    {
        // Constructor of the inheriting viewmodel adds any rules which do not suit annotations
        // Note
        // Two alternative styles of writing rules:
        ValiditionRules = new Dictionary<string, List<PredicateRule>>
        {
            {"Amount2",
                new List<PredicateRule>
                {
                  new PredicateRule
                  {
                      Message ="Amount2 must be greater than Amount",
                      IsOK = x => Amount2 > Amount
                  }
                }
            },
            {"OrderDate",
                new List<PredicateRule>
                {
                  new PredicateRule
                  {
                      Message ="Amount must be greater than 1 if Order Date is in future",
                      IsOK = x =>
                      {
                          if(OrderDate.Date > DateTime.Now.Date)
                          {
                              if(Amount <= 1)
                              {
                                  return false;
                              }
                          }
                          return true;
                      }
                  },
                  new PredicateRule
                  {
                      Message ="Order Date may only be a maximum of 31 days in the future",
                      IsOK = x => (OrderDate - DateTime.Now.Date).Days < 31
                  }
                }
            }
        };
    }
}

This code is based on a sample I wrote:

https://gallery.tec.net.microsoft.com/WPF-Entity-Framework-MVVM-78cdc204

The sample exposes EF model types via viewmodels.

I planned a third in the series but never got the time.

Instead of binding directly to model properties and putting validation attributes in "buddy" classes I recommend copying from a model class to a viewmodel and then back again to commit.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM