0

I'm building a wpf application and I've implemented IDataErrorInfo interface in my classes. Everything is working great, textboxs are bound correctly, the tooltip with error message is displayed correctly and border becomes red on property changed.

But when I click save button (that creates my entity and saves it into my database) even if the value inside my textbox is wrong, it will save my wrong value inside the database.

In the past I've used ArgumentException in property setters and then, on save button click, a message box was displayed with an error message within a try/catch and my wrong value wasn't saved.

Is there a similar way to implement a message box like the try/catch one but using IDataErrorInfo?

I'm not looking for complicated solutions, since I'm a beginner and this is my first try with IDataErrorInfo.

Dennis
  • 37,026
  • 10
  • 82
  • 150
Koosshh56
  • 317
  • 3
  • 17
  • IDataErrorInfo is not meant to do what you are looking for, it is a simple scenario of having check validity of your object not individual properties. IDataErrorInfo will deal with property errrors and will have a message related to that. if you need something object wide, you will have to run validation for all properties before you save, as any stale data is not good ! – Muds Dec 03 '15 at 16:04

1 Answers1

2

Due to comments, I've updated the answer to more detailed one.

First, Newspeak is Ingsoc and Ingsoc is Newspeak XAML is MVVM and MVVM is XAML.

To write something distinctive from "Hello, world", you have to learn MVVM and commands in particular. The code below uses following ICommand implementation:

public sealed class RelayCommand : ICommand
{
    private readonly Action execute;
    private readonly Func<bool> canExecute;

    public RelayCommand(Action execute, Func<bool> canExecute)
    {
        this.execute = execute;
        this.canExecute = canExecute;
    }

    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }

    public bool CanExecute(object parameter)
    {
        return canExecute();
    }

    public void Execute(object parameter)
    {
        execute();
    }
}

In real world applications, WPF validation uses some well-known validation framework undercover, and almost never is being implemented by hand in every view model you need validation.

Here's example of IDataErrorInfo validation, implemented in base class, that uses data annotations:

public abstract class ViewModel : INotifyPropertyChanged, IDataErrorInfo
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName]string propertyName = null)
    {
        // updating property-bound controls
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        // updating command-bound controls like buttons
        CommandManager.InvalidateRequerySuggested();
    }

    private readonly ObservableCollection<ViewModelError> validationErrors = new ObservableCollection<ViewModelError>();

    private void RemoveValidationErrors(string propertyName)
    {
        var propertyValidationErrors = validationErrors
            .Where(_ => _.PropertyName == propertyName)
            .ToArray();

        foreach (var error in propertyValidationErrors)
        {
            validationErrors.Remove(error);
        }
    }

    private string ValidateProperty(string propertyName)
    {
        // we need localized property name
        var property = GetType().GetProperty(propertyName);
        var displayAttribute = property.GetCustomAttribute<DisplayAttribute>();
        var propertyDisplayName = displayAttribute != null ? displayAttribute.GetName() : propertyName;

        // since validation engine run all validation rules for property,
        // we need to remove validation errors from the previous validation attempt
        RemoveValidationErrors(propertyDisplayName);

        // preparing validation engine
        var validationContext = new ValidationContext(this, null, null) { MemberName = propertyName };
        var validationResults = new List<ValidationResult>();

        // running validation
        if (!Validator.TryValidateProperty(property.GetValue(this), validationContext, validationResults))
        {
            // validation is failed;
            // since there could be several validation rules per property, 
            // validation results can contain more than single error
            foreach (var result in validationResults)
            {
                validationErrors.Add(new ViewModelError(propertyDisplayName, result.ErrorMessage));
            }

            // to indicate validation error, it's enough to return first validation message
            return validationResults[0].ErrorMessage;
        }

        return null;
    }

    public IEnumerable<ViewModelError> ValidationErrors
    {
        get { return validationErrors; }
    }

    public string this[string columnName]
    {
        get { return ValidateProperty(columnName); }
    }

    public string Error
    {
        get { return null; }
    }
}

ViewModelError is just a container:

public sealed class ViewModelError
{
    public ViewModelError(string propertyName, string errorMessage)
    {
        PropertyName = propertyName;
        ErrorMessage = errorMessage;
    }

    public string PropertyName { get; private set; }
    public string ErrorMessage { get; private set; }
}

Let's look at model, that represents some person data, corresponding view model and view:

a) model

public class Person
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
}

b) view model

public class PersonViewModel : ViewModel
{
    private void HandleSave()
    {
        var person = new Person
        {
            Id = Guid.NewGuid(),
            Name = Name,
            Age = Age
        };

        // save person using database, web service, file...
    }

    private bool CanSave()
    {
        return !ValidationErrors.Any();
    }

    public PersonViewModel()
    {
        SaveCommand = new RelayCommand(HandleSave, CanSave);
    }

    [Display(Name = "Full name of person")]
    [Required(AllowEmptyStrings = false)]
    [MaxLength(50)]
    public string Name
    {
        get { return name; }
        set
        {
            if (name != value)
            {
                name = value;
                OnPropertyChanged();
            }
        }
    }
    private string name;

    [Display(Name = "Age of person")]
    [Range(18, 65)]
    public int Age
    {
        get { return age; }
        set
        {
            if (age != value)
            {
                age = value;
                OnPropertyChanged();
            }
        }
    }
    private int age;

    public ICommand SaveCommand { get; private set; }
}

c) view (WPF window)

<Window x:Class="Wpf_IDataErrorInfoSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Wpf_IDataErrorInfoSample"
        mc:Ignorable="d"
        Title="MainWindow" Height="300" Width="400">

    <Window.DataContext>
        <local:PersonViewModel />
    </Window.DataContext>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <!-- Validation summary -->
        <ItemsControl ItemsSource="{Binding ValidationErrors}">
            <ItemsControl.ItemTemplate>
                <DataTemplate DataType="{x:Type local:ViewModelError}">
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding PropertyName}"/>
                        <TextBlock Text=" : "/>
                        <TextBlock Text="{Binding ErrorMessage}"/>
                    </StackPanel>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>

        <!-- Editable fields -->
        <StackPanel Grid.Row="1" Margin="4, 10, 4, 4">
            <!-- Name -->
            <TextBlock Text="Name:"/>
            <TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}"/>

            <!-- Age -->
            <TextBlock Text="Age:"/>
            <Slider Minimum="0" Maximum="120" TickPlacement="BottomRight" Value="{Binding Age, ValidatesOnDataErrors=True}"/>
        </StackPanel>

        <Button Grid.Row="2" Content="Save" Command="{Binding SaveCommand}"/>
    </Grid>
</Window>

If you'll run this code, initial picture will look like this:

enter image description here

As you can see, if there are validation errors, button is disabled and you can't save invalid data. If you'll fix errors, button will become enabled, and you can click it to save data:

enter image description here

Dennis
  • 37,026
  • 10
  • 82
  • 150