我正在学习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);
}
}
答案 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
是什么样的,但RelayCommand
是ICommand
的常见实现。使用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" />