如何根据WPF应用程序中的验证启用/禁用提交按钮?

时间:2018-02-16 18:40:21

标签: c# wpf xaml

我正在学习WPF。所以..我正在尝试使用MVVM设计模式创建一个小型WPF应用程序。我能够创建数据验证过程并相应地在屏幕上显示错误。但是,我需要禁用提交按钮,直到加载窗体并验证所有内容。另外,我不希望在表单加载时处理请求,因此默认情况下不会显示错误。

这就是我所做的。我首先创建了一个名为ErrorObserver的类来处理错误。然后我创建了一个名为ViewModel的类,它扩展了ErrorObserver类。我将这两个类分开是因为我将在ViewModel中添加共享视图特定代码。最后,我为每个视图模型提供了特定于视图的代码。为简单起见,我正在分享一个名为VendorViewModel的小视图,该视图扩展了ViewModel

问题 即使表单没有错误,也始终禁用操作按钮。我希望按钮状态随IsValidated属性更改而更改。我正在使用ICommand,所以我希望按钮会随着CanExecute方法的更改而改变。

问题 如何根据IsValidated属性的值启用/禁用按钮?

如果你在我的代码中看到改进的空间,我将非常感谢你的指导,所以我正在以最好的方式学习WPF与MVVM。

我的ObservableObject课程非常简单,看起来像这样

    /// <summary>
    /// An object that supports change notification.
    /// </summary>
    public class ObservableObject : INotifyPropertyChanged
    {
        /// <summary>
        /// Raised when the value of a property has changed.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Raises <see cref="PropertyChanged"/> for the property whose name matches <see cref="propertyName"/>.
        /// </summary>
        /// <param name="propertyName">Optional. The name of the property whose value has changed.</param>
        protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
        {        
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }

ErrorObserver类看起来像这样

public class ErrorObserver : ObservableObject, INotifyDataErrorInfo
{
    private Dictionary<string, List<string>> _errors = new Dictionary<string, List<string>>();
    private bool ValidationTriggred = false;

    protected bool IsValidationTriggred
    {
        get
        {
            return ValidationTriggred;
        }
    }

    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

    /// <summary>
    /// Get any errors associated with the giving property name
    /// </summary>
    /// <param name="propertyName"></param>
    /// <returns></returns>
    public IEnumerable GetErrors(string propertyName)
    {
        if (propertyName != null)
        {
            var messages = new List<string>();

            _errors.TryGetValue(propertyName, out messages);

            if (messages.Any())
            {
                return messages;
            }
        }

        return null;
    }
    /// <summary>
    /// Check if the model has any errors
    /// </summary>
    public bool HasErrors
    {
        get
        {
            return _errors.Any();
        }
    }

    /// <summary>
    /// Validates the property
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    protected void Validate(object sender, PropertyChangedEventArgs e)
    {
        // At this point we know that the property changed
        ValidationTriggred = true;

        var context = new ValidationContext(this)
        {
            MemberName = e.PropertyName
        };

        RemoveError(e.PropertyName);

        var results = new Collection<ValidationResult>();
        bool isValid = Validator.TryValidateObject(this, context, results, true);

        if (isValid)
        {
            return;
        }

        List<string> errors = results.Where(x => x.MemberNames.Contains(e.PropertyName))
                                            .Select(x => x.ErrorMessage)
                                            .ToList();
        if (errors.Any())
        {
            AddError(e.PropertyName, errors);
        }
    }

    /// <summary>
    /// Add the error messages to the errors colelction
    /// </summary>
    /// <param name="propertyName">The property name</param>
    /// <param name="errorMessages">The errors messages</param>
    private void AddError(string propertyName, List<string> errorMessages)
    {
        _errors[propertyName] = errorMessages;
    }

    /// <summary>
    /// Remove the error message from the error collections.
    /// </summary>
    /// <param name="propertyName">The property name</param>
    private void RemoveError(string propertyName)
    {
        if (_errors.ContainsKey(propertyName))
        {
            _errors.Remove(propertyName);
        }
    }
}

这是我的ViewModel

public abstract class ViewModel : ErrorObserver
{
    public virtual bool IsValidated
    {
        get
        {
            return IsValidationTriggred && !HasErrors;
        }

    }

    public ViewModel()
    {
        PropertyChanged += Validate;
    }

    protected ICommand Fire(Action<Object> action)
    {
        return new ActionCommand(action, p => IsValidated);
    }
}

最后我的VendorViewModel类是特定于视图的代码。

public class VendorViewModel : ViewModel
{
    protected readonly IUnitOfWork UnitOfWork;
    private string _Name { get; set; }
    private string _Phone { get; set; }

    public VendorViewModel()
        : this(new UnitOfWork())
    {
    }

    public VendorViewModel(IUnitOfWork unitOfWork)
    {
        UnitOfWork = unitOfWork;
    }

    [Required(ErrorMessage = "The name is required")]
    [MinLength(3, ErrorMessage = "Name must be more than or equal to 3 letters")]
    [MaxLength(50, ErrorMessage = "Name must be less than or equal to 50 letters")]
    public string Name
    {
        get
        {
            return _Name;
        }
        set
        {
            _Name = value;
            NotifyPropertyChanged();
        }
    }

    public string Phone
    {
        get
        {
            return _Phone;
        }
        set
        {
            _Phone = value;
            NotifyPropertyChanged();
        }
    }

    /// <summary>
    /// Gets the collection of customer loaded from the data store.
    /// </summary>
    public ICollection<Vendor> Vendors { get; private set; }

    public ICommand Create
    {
        get
        {
            return Fire(p => AddVendor());
        }
    }

    protected void AddVendor()
    {
        var vendor = new Vendor(Name, Phone);

        UnitOfWork.Vendors.Add(vendor);
    }
}

以下是xaml

VendorView代码
<UserControl x:Class="WindowsClient.Views.VendorView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mui="http://firstfloorsoftware.com/ModernUI"
             xmlns:vm="clr-namespace:WindowsClient.ViewModels"
             xmlns:views="clr-namespace:WindowsClient.Views">

    <DockPanel Style="{StaticResource ContentRoot}">
        <Grid DockPanel.Dock="Top">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"></ColumnDefinition>
            </Grid.ColumnDefinitions>
            <StackPanel>

                <StackPanel>
                    <Label Content="Name" />
                    <TextBox x:Name="Name" Text="{Binding Name, ValidatesOnNotifyDataErrors= true, NotifyOnValidationError=True, UpdateSourceTrigger=PropertyChanged}" />
                </StackPanel>

                <StackPanel>
                    <Label Content="Phone Number" />
                    <TextBox x:Name="Phone" Text="{Binding Phone, ValidatesOnNotifyDataErrors= true, NotifyOnValidationError=True, UpdateSourceTrigger=PropertyChanged}" />
                </StackPanel>

            </StackPanel>
        </Grid>

        <StackPanel Orientation="Horizontal"
                    HorizontalAlignment="Center"
                    DockPanel.Dock="Bottom"
                    Height="30"
                    VerticalAlignment="Bottom">
            <Button Command="{Binding Create}"
                    IsEnabled="{Binding IsValidated}">Create</Button>
            <Button>Reset</Button>
        </StackPanel>

    </DockPanel>

</UserControl>

已更新

以下是我ICommand

的实现
public sealed class ActionCommand : ICommand
{
    private readonly Action<Object> Action;
    private readonly Predicate<Object> Allowed;
    public event EventHandler CanExecuteChanged;

    /// <summary>
    /// Initializes a new instance of the <see cref="ActionCommand"/> class.
    /// </summary>
    /// <param name="action">The <see cref="Action"/> delegate to wrap.</param>
    public ActionCommand(Action<Object> action)
        : this(action, null)
    {
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="ActionCommand"/> class.
    /// </summary>
    /// <param name="action">The <see cref="Action"/> delegate to wrap.</param>
    /// <param name="predicate">The <see cref="Predicate{Object}"/> that determines whether the action delegate may be invoked.</param>
    public ActionCommand(Action<Object> action, Predicate<Object> allowed)
    {
        if (action == null)
        {
            throw new ArgumentNullException("action", "You must specify an Action<T>.");
        }

        Action = action;
        Allowed = allowed;
    }

    /// <summary>
    /// Defines the method that determines whether the command can execute in its current state.
    /// </summary>
    /// <returns>
    /// true if this command can be executed; otherwise, false.
    /// </returns>
    /// <param name="parameter">Data used by the command.  If the command does not require data to be passed, this object can be set to null.</param>
    public bool CanExecute(object parameter)
    {
        if (Allowed == null)
        {
            return true;
        }

        return Allowed(parameter);
    }

    /// <summary>
    /// Defines the method to be called when the command is invoked.
    /// </summary>
    /// <param name="parameter">Data used by the command.  If the command does not require data to be passed, this object can be set to null.</param>
    public void Execute(object parameter)
    {
        Action(parameter);
    }

    /// <summary>
    /// Executes the action delegate without any parameters.
    /// </summary>
    public void Execute()
    {
        Execute(null);
    }
}

2 个答案:

答案 0 :(得分:1)

似乎问题是VM属性 IsValidated ,绑定到您的按钮 IsEnabled 属性,无法正确通知视图已更新。因此,视图不知道IsValidated值已更改,因此按钮状态不会更改。你可以很容易地看到使用debbuger并在IsValidated getter中添加一个breakpoing。

要解决此问题,您要么绑定到一个属性,该属性会通知视图有关更改(如名称和电话道具)。 另一种已经建议的方法是使用具有 CanExecute 操作的Command进行评估。这样您可能需要强制 RaiseCanExecuteChange 。这是完整的代码示例。

 /// <summary>
/// Class for binding commands
/// </summary>
public class RelayCommand : ICommand
{
    /// <summary>
    /// The delegate for execution logic.
    /// </summary>
    private readonly Action<object> execute;

    /// <summary>
    /// The delegate for execution availability status logic.
    /// </summary>
    private readonly Predicate<object> canExecute;

    /// <summary>
    /// Initializes a new instance of the <see cref="RelayCommand"/> class that can always execute.
    /// </summary>
    /// <param name="execute">The execution logic.</param>
    public RelayCommand(Action<object> execute) : this(execute, null)
    {
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="RelayCommand"/> class.
    /// </summary>
    /// <param name="execute">The execution logic.</param>
    /// <param name="canExecute">The execution status logic.</param>
    public RelayCommand(Action<object> execute, Predicate<object> canExecute)
    {
        if (execute != null)
        {
            this.execute = execute;
        }
        else
        {
            throw new ArgumentNullException(nameof(execute));
        }

        this.canExecute = canExecute;
    }

    /// <summary>
    /// Occurs when changes occur that affect whether the command should execute.
    /// </summary>
    public event EventHandler CanExecuteChanged
    {
        add
        {
            if (this.canExecute != null)
            {
                CommandManager.RequerySuggested += value;
            }
        }
        remove
        {
            if (this.canExecute != null)
            {
                CommandManager.RequerySuggested -= value;
            }
        }
    }

    /// <summary>
    /// Raises the <see cref="CanExecuteChanged" /> event.
    /// </summary>
    public void RaiseCanExecuteChanged()
    {
        CommandManager.InvalidateRequerySuggested();
    }

    /// <summary>
    /// Checks if command can be executed
    /// </summary>
    /// <param name="parameter">Command parameter</param>
    /// <returns>True if command can be executed, false otherwise</returns>
    public bool CanExecute(object parameter)
    {
        return this.canExecute == null || this.canExecute(parameter);
    }

    /// <summary>
    /// Executes command
    /// </summary>
    /// <param name="parameter">Command parameter</param>
    public void Execute(object parameter)
    {
        this.execute(parameter);
    }
}

答案 1 :(得分:0)

您无需将IsEnabled绑定到IsValidated。我不确定您的ActionCommand是什么样的,但RelayCommandICommand的常见实现。使用RelayCommand,您只需提供一个谓词。

类似的东西:

<强> ViewModel.cs

public ViewModel()
{
    CreateCommand = new RelayCommand(x => Create(), CanCreate);
}

protected bool CanCreate(object sender)
{
    if (Validator.TryValidateObject(this, new ValidationContext(this, null, null), new List<ValidationResult>(), true))
        return true;
    else
        return false;
}

protected void Create()
{
    var vendor = new Vendor(Name, Phone);

    UnitOfWork.Vendors.Add(vendor);
}

<强> View.xaml

<Button Command="{Binding CreateCommand}" Content="Create" />