3

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.

enter image description here enter image description here enter image description here

Junior
  • 11,602
  • 27
  • 106
  • 212
  • Replace this part in you Error Template: – Gaurang Dave Feb 13 '18 at 07:14
  • That did not work. Where is `Validation.Errors` coming from? is that something I need to manually create and populate? – Junior Feb 13 '18 at 07:19
  • 1
    Put ValidatesOnDataErrors=True, while binding the data. Example : – Gaurang Dave Feb 13 '18 at 07:23
  • It is already there. Please review my `XAML` code is the question. – Junior Feb 13 '18 at 07:25
  • Sorry I missed it. Check this : https://blog.magnusmontin.net/2013/08/26/data-validation-in-wpf/ – Gaurang Dave Feb 13 '18 at 07:28
  • Not sure. It seems to be very similar to what I have. I seems that my validation template is not bonded to the view for some reason. My code is in the `Application.Resources` of App.xaml file – Junior Feb 13 '18 at 07:55
  • 1
    I have tested a simplified version of your code and it works. I would guess that your `FormControl` style overwrites the general `TextBox` style from your App.xaml. Can you move the `Validation.ErrorTemplate` code inside your `FormControl`style? – Bruno V Feb 13 '18 at 11:28
  • @BrunoV That indeed was the problem. However, the error text seems to go over the buttons instead of going between the TextBox and the Button. Also, the text seems to be stretching to the right "other the other field" instead of wrapping and expanding vertically. – Junior Feb 13 '18 at 15:28
  • They never do. They are rendered in a separate layer, the AdornerDecorator. –  Feb 13 '18 at 17:53

1 Answers1

1

I'll try to answer all your questions:

How do I show the error correctly?

The ErrorTemplate is not applied because the FormControl style on your TextBox has precedence over the style containing the Validation.ErrorTemplate. Moving the Validation.ErrorTemplate code into the FormControl style will fix this issue.

Also, how to I not show any error when the view is first loaded?

What is the use of a Required validation if it's not applied immediately? The MinLength and MaxLength validations will only be executed when you start typing in the field.

However, the error message seems to be going over the buttons instead of pushing the buttons down.

As Will pointed out, this is because the error messages are shown on the AdornerLayer, which cannot interfere with the layer your controls are on. You have the following options:

  • Use the AdornerLayer but leave some room between controls
  • Use a ToolTip to show the error messages
  • Use an extra TextBlock in the template of the TextBox that shows the error messages.

These options are described here

Bruno V
  • 1,721
  • 1
  • 14
  • 32