Reputation: 40289
I am in the process of learning WPF. So..I am trying to create a small WPF app using MVVM design pattern. I was able to create the data validation process and displaying the errors on the screen accordingly. However, I need to disable the submit button until after the form is loaded and everything is validated. Additionally, I don't want the request to be processed on form load so by default the errors don't show up.
Here is what I have done. I first created a class called ErrorObserver
which handles the errors. I then created a class called ViewModel
which extends the ErrorObserver
class. I have these 2 classes separated just because I am going to be adding shared view-specific code in the ViewModel
. Finally, I have view-specific code for each view-model. For the sake of simplicity, I am sharing a small view called VendorViewModel
which extends ViewModel
.
Problem
The action button is always disabled even when the form has not errors. I want the button state to change as the IsValidated
property change. I am using ICommand
so I am expecting the button to change as the CanExecute
method changes.
Question
How do I enable/disable the button based on the value of the IsValidated
property?
If you see room for improvment in my code I would appreciate your guidance so I am learning WPF with MVVM the best way possible.
My ObservableObject
class is very simple and looks like this
/// <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));
}
}
}
The ErrorObserver
class look like this
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);
}
}
}
Here is my 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);
}
}
Finally my VendorViewModel
class which is View-specific code.
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);
}
}
Here is my xaml
code for the 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>
UPDATED
Here is my implementation of the 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);
}
}
Upvotes: 0
Views: 1772
Reputation: 26
Seems like the problem is that VM property IsValidated, that is binded to your button IsEnabled property, does not properly notify the view that it has been updated. So, the view has no idea that IsValidated value changed, thus button state does not change. You can easily see that using a debbuger and adding a breakpoing at IsValidated getter.
To fix this, you either bind to a property that notify the view about the changes (like Name and Phone props). Another way that was already suggested, is to use Command that has CanExecute action to evaluate. This way all you may need is to force RaiseCanExecuteChange. Here is complete code sample for you.
/// <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);
}
}
Upvotes: 1
Reputation: 4950
You don't need to bind IsEnabled
to IsValidated
. I'm not sure what your ActionCommand
looks like, but RelayCommand
is a common implementation of ICommand
. Using RelayCommand
you can simply supply a predicate.
Something like:
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" />
Upvotes: 0