Question

I want to give this question as much context as possible, but, in summary, I'm basically asking two questions:

  • Does WPF always call the getter after setting a bound property when the setter doesn't throw an exception?
  • Is it possible to prevent the getter of a bound property from being called after an error has occurred in the setter when the ViewModel implements IDataErrorInfo?

I currently have a Model class that implements validation by throwing an exception from the property setter. Additionally, many of the properties are coupled, so modifying the value of one of them may cause several others to be recalculated. It implements INotifyPropertyChanged to alert outside listeners whenever a recalculation has occurred. It looks something like this:

public class Model : INotifyPropertyChanged
{
    private double property1;
    public double Property1
    {
        get { return property1; }
        set
        {
            if (value < 0.0)
                throw new Exception("Property1 cannot be less than 0.0.");

            property1 = value;
            OnPropertyChanged(new PropertyChangedEventArgs("Property1"));
        }
    }

    // ...Everything needed to support INotifyPropertyChanged...
}

Initially, I implemented the ViewModel for this class to act as a thin wrapper around the model properties, providing additional view-specific behaviors whenever an error occurs (flagging invalid data, disabling buttons, etc.):

public class ViewModel : INotifyPropertyChanged
{
    private readonly Model model;

    public ViewModel()
    {
        model = new Model();
        model.PropertyChanged += (sender, args) => OnPropertyChanged(args);
    }

    public string Property1
    {
        get { return model.Property1.ToString(); }
        set
        {
            try
            {
                model.Property1 = Double.Parse(value);
            }
            catch (Exception)
            {
                // Perform any view-specific actions
                throw;
            }
        }
    }

    // ...Everything needed to support INotifyPropertyChanged
}

Notably, the ViewModel doesn't have any additional backing fields; all of its properties are directly linked to the corresponding properties in the Model, and any PropertyChanged notifications from the Model are passed along by the ViewModel.

But, I've frequently heard that using exceptions for validation can be limiting, and I'm starting to realize the same, specifically as the need for cross-coupled validation rules has increased in this application.

I didn't want to change behaviors of the Model, since it is already being used in several other places, but I went about changing the ViewModel to implement IDataErrorInfo:

public class ViewModel : INotifyPropertyChanged, IDataErrorInfo
{
    private readonly Model model;

    public ViewModel()
    {
        model = new Model();
        model.PropertyChanged += (sender, args) => 
        {
            errorList.Remove(args.PropertyName);
            OnPropertyChanged(args);
        };
    }

    public string Property1
    {
        get { return model.Property1.ToString(); }
        set
        {
            try
            {
                model.Property1 = Double.Parse(value);
            }
            catch(Exception ex)
            {
                // Perform any view-specific actions                    
                errorList["Property1"] = ex.Message;
            }
        }
    }

    private readonly Dictionary<string, string> errorList = new Dictionary<string, string>();

    public string this[string propertyName]
    {
        get
        {
            string errorMessage;
            if (errorList.TryGetValue(propertyName, out errorMessage))
                return errorMessage;
            return String.Empty;
        }
    }

    public string Error { get { return String.Empty; } }

    // ...Everything needed to support INotifyPropertyChanged
}

However, this caused a drastic unwanted change in behavior. When using exceptions, after the user entered an invalid value, the Validation.ErrorTemplate for the control is displayed and the invalid value remains in the control, thus giving the user the opportunity to correct their mistake. When using IDataErrorInfo, WPF seems to call the property getter after the setter has completed. Since the Model hasn't changed whenever an error occurs, the invalid value is replaced by the previous value. So now I have a control displaying the Validation.ErrorTemplate but with a VALID being value displayed!

It seems crazy to me that WPF would automatically call a property getter without receiving a PropertyChanged notification (after the window has been initialized). And it doesn't attempt to call the getter after an exception is thrown, so why would it do it when IDataErrorInfo is used?

Am I doing something wrong that's causing this behavior? Is there a way to prevent WPF from calling the property getter in the ViewModel after an error has occurred in the setter when the ViewModel implements IDataErrorInfo?

I've tried adding backing fields to the ViewModel to store the invalid value, but due to the fact Model properties can be modified outside of the ViewModel (that's the reason it implements INotifyPropertyChanged in the first place), the solution ends up being quite complex and basically unsustainable in an environment with programmers of varying skill levels.

Was it helpful?

Solution 2

Although it doesn't answer the questions I asked, my coworker suggested that the desired behavior could be achieved by adding the invalid value to the errorList and modifying the getter in the ViewModel to return the invalid value whenever there is an error.

So the properties in the ViewModel look like this:

public string Property1
{
    get
    {
        ErrorInfo error;
        if (errorList.TryGetValue("Property1", out error))
            return error.Value;

        return model.Property1.ToString();
    }
    set
    {
        try
        {
            model.Property1 = Double.Parse(value);
        }
        catch (Exception ex)
        {
            // Perform any view-specific actions
            errorList["Property1"] = new ErrorInfo(value, ex.Message);
        }
    }
}

With the following updates to the IDataErrorInfo methods:

private struct ErrorInfo
{
    public readonly string Value;
    public readonly string Message;

    public ErrorInfo(string value, string message)
    {
        Value = value;
        Message = message;
    }
}

private readonly Dictionary<string, ErrorInfo> errorList = new Dictionary<string, ErrorInfo>();

public string this[string propertyName]
{
    get
    {
        ErrorInfo error;
        if (errorList.TryGetValue(propertyName, out error))
            return error.Message;
        return String.Empty;
    }
}

public string Error { get { return String.Empty; } }

This allows the PropertyChanged event handler to stay the same, and only requires small changes to the property getters and setters.

OTHER TIPS

Here's the approach you want to have for form validation in MVVM

Your model

public class Product:IDataErrorInfo
{
    public string ProductName {get;set;}

    public string this[string propertyName]
    {
       get 
       {
           string validationResult = null;
           switch (propertyName)
           {
               case "ProductName":
               validationResult = ValidateName();
           }
       }
     }
}

Then in your ViewModel

public string ProductName
{
  get { return currentProduct.ProductName; }
  set 
  {
    if (currentProduct.ProductName != value)
    {
      currentProduct.ProductName = value;
      base.OnPropertyChanged("ProductName");
    }
  }  
}

As another consideration, when I want to validate numbers (such as your double validation), keep the model as having a double instead of a string

public double? Property1 {get;set;}

then you can do this

<Textbox Name="myDoubleTextbox" >
    <Binding ValidatesOnDataErrors="true" Path="Property1" TargetValueNull="" />
/>

so when they type something incorrect into the double box, it sends null to your model and you can check against that.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top