简体   繁体   English

如果 UserControl 中文本框的文本输入无效,则禁用提交按钮

[英]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.在主要的 Window 中,我将使用其中的几个用户控件从用户那里获取不同参数的值。

  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": XAML 用户控件“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": “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: XAML 显示用户控件和提交按钮的“MainWindow”代码:

<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主要 Window 的视图模型实现了通常的 INotifyPropertyChangedview 接口

    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.您会将 ICommand 的实现公开为 window 视图模型上的公共属性。 Call that SubmitCommand.调用该 SubmitCommand。 Often a relaycommand or delegatecommand but that's perhaps slightly off the subject.通常是中继命令或委托命令,但这可能稍微偏离主题。

One of the things icommand offers is canexecute. icommand 提供的功能之一是 canexecute。 This is a method returning true or false.这是一个返回 true 或 false 的方法。 If it returns false, a button with this command bound will be disabled.如果它返回 false,绑定了这个命令的按钮将被禁用。

Your mainwindowviewmodel would also implement one of the validation interfaces.您的 mainwindowviewmodel 还将实现验证接口之一。 InotifyDataErrorinfo being the recommended one. InotifyDataErrorinfo 是推荐的一种。

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.这将是在文本框中键入的文本,该文本框绑定到 double 例如。 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.我发现的样本是 .Net old with mvvmlight,可能不是直接粘贴到 .net 核心。 It still illustrates the principle though.它仍然说明了原理。

In a parent panel ( eg the main grid of your window ).在父面板中(例如 window 的主网格)。 You want some event handlers.您需要一些事件处理程序。 My sample uses interactions ( now xaml behaviors nuget ).我的示例使用交互(现在 xaml 行为 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.您的 viewmodel 的属性是一个复杂的 object 并且绑定了另一层的属性将需要更复杂的处理。

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.正如您将看到的,它实现了 INotifyDataErrorInfo。 Validator.Validate is used on the viewmodel which will check dataannotations on it. Validator.Validate 用于将检查其上的数据注释的视图模型。 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.所有这些都用于驱动一个属性 IsValid。 When that is false your canexecute on your submitcommand should return false.当它为假时,您的 canexecute 在您的 submitcommand 上应该返回 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 https://gallery.tec.net.microsoft.com/WPF-Entity-Framework-MVVM-78cdc204

The sample exposes EF model types via viewmodels.该示例通过视图模型公开 EF model 类型。

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.我建议不要直接绑定到 model 属性并将验证属性放在“伙伴”类中,而是从 model class 复制到视图模型,然后再次返回以提交。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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