I am trying to create an application using WPF. I am trying to fully build it using the MVVM model. However, I am puzzled on how to correctly display the error message. I thought it would be trivial step but seems to be the most complex.
I created the following view using xaml
<StackPanel Style="{StaticResource Col}">
<DockPanel>
<Grid DockPanel.Dock="Top">
<Grid.ColumnDefinitions >
<ColumnDefinition Width="*" ></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Style="{StaticResource Col}">
<Label Content="Name" Style="{StaticResource FormLabel}" />
<Border Style="{StaticResource FormInputBorder}">
<TextBox x:Name="Name" Style="{StaticResource FormControl}" Text="{Binding Name, ValidatesOnDataErrors=True, NotifyOnValidationError=True, UpdateSourceTrigger=PropertyChanged}" />
</Border>
</StackPanel>
<StackPanel Grid.Column="1" Style="{StaticResource Col}">
<Label Content="Phone Number" Style="{StaticResource FormLabel}" />
<Border Style="{StaticResource FormInputBorder}">
<TextBox x:Name="Phone" Style="{StaticResource FormControl}" Text="{Binding Phone, ValidatesOnDataErrors=True, NotifyOnValidationError=True, UpdateSourceTrigger=PropertyChanged}" />
</Border>
</StackPanel>
</Grid>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Style="{StaticResource PrimaryButton}" Command="{Binding Create}">Create</Button>
<Button>Reset</Button>
</StackPanel>
</DockPanel>
</StackPanel>
Then I created the following ViewModel
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(5, ErrorMessage = "Name must be more than or equal to 5 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; }
protected void AddVendor()
{
var vendor = new Vendor(Name, Phone);
UnitOfWork.Vendors.Add(vendor);
}
public ICommand Create
{
get
{
return new ActionCommand(p => AddVendor(),
p => IsValidRequest());
}
}
public bool IsValidRequest()
{
// There got to be a better way to check if everything passed or now...
return IsValid("Name") && IsValid("Phone");
}
}
Here is how my ViewModel
base class look like
public abstract class ViewModel : ObservableObject, IDataErrorInfo
{
/// <summary>
/// Gets the validation error for a property whose name matches the specified <see cref="columnName"/>.
/// </summary>
/// <param name="columnName">The name of the property to validate.</param>
/// <returns>Returns a validation error if there is one, otherwise returns null.</returns>
public string this[string columnName]
{
get { return OnValidate(columnName); }
}
/// <summary>
/// Validates a property whose name matches the specified <see cref="propertyName"/>.
/// </summary>
/// <param name="propertyName">The name of the property to validate.</param>
/// <returns>Returns a validation error, if any, otherwise returns null.</returns>
protected virtual string OnValidate(string propertyName)
{
var context = new ValidationContext(this)
{
MemberName = propertyName
};
var results = new Collection<ValidationResult>();
bool isValid = Validator.TryValidateObject(this, context, results, true);
if (!isValid)
{
ValidationResult result = results.SingleOrDefault(p => p.MemberNames.Any(memberName => memberName == propertyName));
if (result != null)
return result.ErrorMessage;
}
return null;
}
protected virtual bool IsValid(string propertyName)
{
return OnValidate(propertyName) == null;
}
/// <summary>
/// Not supported.
/// </summary>
[Obsolete]
public string Error
{
get
{
throw new NotSupportedException();
}
}
}
Here is my ObservableObject
class
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 = "")
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
My goal is to show a red border around the incorrect field then display the error message right underneath it to tell the use what went wrong.
How do I show the error correctly? Also, how to I not show any error when the view is first loaded?
Base on this blog I need to edit the Validation.ErrorTemplate
So I tried adding the following code to the App.xaml file
<!-- Style the error validation by showing the text message under the field -->
<Style TargetType="TextBox">
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<StackPanel>
<Border BorderThickness="1" BorderBrush="DarkRed">
<StackPanel>
<AdornedElementPlaceholder x:Name="errorControl" />
</StackPanel>
</Border>
<TextBlock Text="{Binding AdornedElement.ToolTip, ElementName=errorControl}" Foreground="Red" />
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="BorderBrush" Value="Red" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}" />
</Trigger>
</Style.Triggers>
</Style>
But that isn't showing the error message, also when the view is first loaded I get an error. Finally, even when the form become valid, the action button stay disabled.
UPDATED
After moving the Property="Validation.ErrorTemplate"
into the FormControl
group it worked. However, the error message seems to be going over the buttons instead of pushing the buttons down. Also, the text does not seems to be wrapping vertically allowing the border to strach over the other control as you can see in the following screen shows.