1

I have a DataGrid like so:

<DataGrid CanUserSortColumns="False" CanUserAddRows="True" ItemsSource="{Binding Vertices}" AutoGenerateColumns="False">
    <DataGrid.Columns>
        <DataGridTextColumn x:Name="YColumn" Width="*" Header="Latitude">
            <DataGridTextColumn.Binding>
                <Binding Path="Y">
                    <Binding.ValidationRules>
                        <validation:DoubleValidationRule />
                    </Binding.ValidationRules>
                </Binding>
            </DataGridTextColumn.Binding>
        </DataGridTextColumn>
        <DataGridTextColumn x:Name="XColumn" Width="*" Header="Longitude">
            <DataGridTextColumn.Binding>
                <Binding Path="X">
                    <Binding.ValidationRules>
                        <validation:DoubleValidationRule />
                    </Binding.ValidationRules>
                </Binding>
            </DataGridTextColumn.Binding>
        </DataGridTextColumn>
    </DataGrid.Columns>
</DataGrid>

I have two columns that have the same validation rule (checking to see if the value in the cell is a double):

public class DoubleValidationRule : ValidationRule
{
    public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
    {
        if (value != null)
        {
            double proposedValue;
            if (!double.TryParse(value.ToString(), out proposedValue))
            {
                return new ValidationResult(false, "'" + value.ToString() + "' is not a whole double.");
            }
        }
        return new ValidationResult(true, null);
    }
}

This works fine, and a red border is displayed around the cells if the user entered value is not a double. Now I would like to disable a button if there is a validation error with any of the cells.

Following some other posts on this topic, I achieved this using MultiDataTriggers:

<Button>
    <Button.Style>
        <Style TargetType="Button">
            <Setter Property="IsEnabled" Value="False" />
            <Style.Triggers>
                <MultiDataTrigger>
                    <MultiDataTrigger.Conditions>
                        <Condition Binding="{Binding Path=(Validation.HasError), ElementName=XColumn}" Value="False" />
                        <Condition Binding="{Binding Path=(Validation.HasError), ElementName=YColumn}" Value="False" />
                    </MultiDataTrigger.Conditions>
                    <Setter Property="IsEnabled" Value="True" />
                </MultiDataTrigger>
            </Style.Triggers>
        </Style>
    </Button.Style>
</Button>

This isn't working though. The button never disables even when there is a validation error. What am I doing wrong?

Edit: Here's my model and related code in the view model:

public class CustomVertex
{
    public double X { get; set; }

    public double Y { get; set; }

    public CustomVertex()
    { }
}

public class CustomPolygonViewModel : ViewModelBase
{
    public ObservableCollection<CustomVertex> Vertices { get; set; }

    public CustomPolygonViewModel()
    {
        Vertices = new ObservableCollection<CustomVertex>();
    }
}

My DataContext is set up correctly and I verified that the model's x and y are being updated on changing the value. The validation rule is being hit properly.

BionicCode
  • 1
  • 4
  • 28
  • 44
pfinferno
  • 1,779
  • 3
  • 34
  • 62
  • I don't see anything directly sticking out. I would double check that your bindings on your button are correct. Also check to make sure that you are notifying of those properties changing. My guess is that either the bindings are incorrect or those properties are not correctly notifying of changes. – Sudsy1002 Jan 03 '19 at 13:57
  • Updated my question with relevant code from model and viewmodel. Could it have something to do with the element name being on the column instead of the cell? – pfinferno Jan 03 '19 at 14:09
  • The binding that you should check is the `Validation.HasError`. Is it bound correctly and how is it notifying changes? – Sudsy1002 Jan 03 '19 at 14:23
  • You're right it isn't bound correctly, but I'm not exactly sure how to fix it. I think I have a misunderstanding with how Validation works. Do I need a property in the viewmodel to bind Validation.HasError to? – pfinferno Jan 03 '19 at 14:51
  • 1
    Validation.HasErrors cannot be bound to. See [this post](https://stackoverflow.com/questions/22827437/binding-validation-haserror-property-in-mvvm) for a proper solution. – Sudsy1002 Jan 03 '19 at 15:08
  • So I have to add a "HasError" property in each model I have and want to do validation on this way? It just seems like a lot for a simple enable/disable. – pfinferno Jan 03 '19 at 15:18
  • Personally I would probably do my own validation in the view model and then bind to my own properties XHasErrors and YHasErrors. I think that would be better than using the validation in your case. – Sudsy1002 Jan 03 '19 at 15:20

1 Answers1

3

You have to let your view model implement INotifyDataErrorInfo MSDN. Example. Example from MSDN (Silverlight). Since .Net 4.5 this is the recommended way to introduce validation to your view models and will help you to solve your propblem. When implementing this interface you will have to provide a HasErrors property that you can bind to. INotifyDataErrorInfo replaces the obsolete IDataErrorInfo.

Binding to the Validation.HasError directly, as you did in your triggers, will not work since Validation.HasError is a read-only attached property and therefore doesn't support binding. To prove this I found this statement on MSDN:

... read-only dependency properties aren't appropriate for many of the scenarios for which dependency properties normally offer a solution (namely: data binding, directly stylable to a value, validation, animation, inheritance).


How INotifyDataErrorInfo works

When the ValidatesOnNotifyDataErrors property of Binding is set to true, the binding engine will search for an INotifyDataErrorInfo implementation on the binding source to subscribe to the ErrorsChanged event.

If the ErrorsChanged event is raised and HasErrors evaluates to true, the binding will invoke the GetErrors() method for the actual property to retrieve the particular error message and apply the customizable validation error template to visualize the error. By default a red border is drawn around the validated element.

How to implement INotifyDataErrorInfo

The CustomVertex class is actually the ViewModel for the DataGrid columns since you are binding to it's properties. So it has to implement the INotifyDataErrorInfo. It could look like this:

public class CustomVertex : INotifyPropertyChanged, INotifyDataErrorInfo
{
    public CustomVertex()
    {
      this.errors = new Dictionary<string, List<string>>();
      this.validationRules = new Dictionary<string, List<ValidationRule>>();

      this.validationRules.Add(nameof(this.X), new List<ValidationRule>() {new DoubleValidationRule()});
      this.validationRules.Add(nameof(this.Y), new List<ValidationRule>() {new DoubleValidationRule()});
    }


    public bool ValidateProperty(object value, [CallerMemberName] string propertyName = null)  
    {  
        lock (this.syncLock)  
        {  
            if (!this.validationRules.TryGetValue(propertyName, out List<ValidationRule> propertyValidationRules))
            {
              return;
            }  

            // Clear previous errors from tested property  
            if (this.errors.ContainsKey(propertyName))  
            {
               this.errors.Remove(propertyName);  
               OnErrorsChanged(propertyName);  
            }

            propertyValidationRules.ForEach(
              (validationRule) => 
              {
                ValidationResult result = validationRule.Validate(value, CultuteInfo.CurrentCulture);
                if (!result.IsValid)
                {
                  AddError(propertyName, result.ErrorContent, false);
                } 
              }               
        }  
    }   

    // Adds the specified error to the errors collection if it is not 
    // already present, inserting it in the first position if isWarning is 
    // false. Raises the ErrorsChanged event if the collection changes. 
    public void AddError(string propertyName, string error, bool isWarning)
    {
        if (!this.errors.ContainsKey(propertyName))
        {
           this.errors[propertyName] = new List<string>();
        }

        if (!this.errors[propertyName].Contains(error))
        {
            if (isWarning) 
            {
              this.errors[propertyName].Add(error);
            }
            else 
            {
              this.errors[propertyName].Insert(0, error);
            }
            RaiseErrorsChanged(propertyName);
        }
    }

    // Removes the specified error from the errors collection if it is
    // present. Raises the ErrorsChanged event if the collection changes.
    public void RemoveError(string propertyName, string error)
    {
        if (this.errors.ContainsKey(propertyName) &&
            this.errors[propertyName].Contains(error))
        {
            this.errors[propertyName].Remove(error);
            if (this.errors[propertyName].Count == 0)
            {
              this.errors.Remove(propertyName);
            }
            RaiseErrorsChanged(propertyName);
        }
    }

    #region INotifyDataErrorInfo Members

    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

    public System.Collections.IEnumerable GetErrors(string propertyName)
    {
        if (String.IsNullOrEmpty(propertyName) || 
            !this.errors.ContainsKey(propertyName)) return null;
        return this.errors[propertyName];
    }

    public bool HasErrors
    {
        get { return errors.Count > 0; }
    }

    #endregion

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
      this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    private double x;
    public double X 
    { 
      get => x; 
      set 
      { 
        if (ValidateProperty(value))
        {
          this.x = value; 
          OnPropertyChanged();
        }
      }
    }

    private double y;
    public double Y 
    { 
      get => this.y; 
      set 
      { 
        if (ValidateProperty(value))
        {
          this.y = value; 
          OnPropertyChanged();
        }
      }
    }


    private Dictionary<String, List<String>> errors;

    // The ValidationRules for each property
    private Dictionary<String, List<ValidationRule>> validationRules;
    private object syncLock = new object();
}

The View:

<DataGrid CanUserSortColumns="False" CanUserAddRows="True" ItemsSource="{Binding Vertices}" AutoGenerateColumns="False">
    <DataGrid.Columns>
        <DataGridTextColumn x:Name="YColumn" 
                            Width="*" 
                            Header="Latitude" 
                            Binding="{Binding Y, ValidatesOnNotifyDataErrors=True}" 
                            Validation.ErrorTemplate="{DynamicResource ValidationErrorTemplate}" />
        <DataGridTextColumn x:Name="XColumn" 
                            Width="*" 
                            Header="Longitude" 
                            Binding="{Binding X, ValidatesOnNotifyDataErrors=True}" 
                            Validation.ErrorTemplate="{DynamicResource ValidationErrorTemplate}" />            
    </DataGrid.Columns>
</DataGrid>

The following is the validation error template, in case you like to customize the visual representation (optional). It is set on the validated element (in this case the DataGridTextColumn) via the attached property Validation.ErrorTemplate (see above):

<ControlTemplate x:Key=ValidationErrorTemplate>
    <StackPanel>
        <!-- Placeholder for the DataGridTextColumn itself -->
        <AdornedElementPlaceholder x:Name="textBox"/>
        <ItemsControl ItemsSource="{Binding}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding ErrorContent}" Foreground="Red"/>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </StackPanel>
</ControlTemplate>

The Button that will be disabled when the validation fails (since I don't know where this button is located in the visual tree I will assume that it shares the DataContext of a DataGrid column, the CustomVertex data model):

<Button>
    <Button.Style>
        <Style TargetType="Button">
            <Setter Property="IsEnabled" Value="True" />
            <Style.Triggers>
                <DataTrigger Binding="{Binding Path=HasErrors}" Value="True">
                    <Setter Property="IsEnabled" Value="False" />
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </Button.Style>
</Button>

There are many examples on the web. I updated the links to provide some content to start with.

I recommend moving the implementation of INotifyDataErrorInfo into a base class together with INotifyPropertyChanged and let all your view models inherit it. This makes the validation logic reusable and keeps your view model classes clean.

You can change the implementation details of INotifyDataErrorInfo to meet requirements.

Remarks: The code is not tested. The snippets should work, but are intended to provide an example how the INotifyDataErrorInfo interface could be implemented.

BionicCode
  • 1
  • 4
  • 28
  • 44
  • So I can't use validation rules in the xaml and bind the return bool of the validation rule to a property? – pfinferno Jan 03 '19 at 16:27
  • 'ValidationRule' doesn't implement 'INotifyPropertyChanged'. So changes to 'ValdationRule.HasError' won't update your bindings. Try 'INotifyErrorDataInfo'. The property you have to implement must use 'INotifyPropertyChanged' and all will work. I added a link to a tutorial showing how to use it. It's very simple and intuitive. – BionicCode Jan 03 '19 at 16:41
  • Thanks for the update. Looked at the example you sent, I sort of get it, but for my example when dealing with a collection of models, where the cells in the datagrid correspond to properties in the model, would I be implementing the `INotifyErrorDataInfo` in my VM or model? I guess I'm having some issues seeing how this would be implemented properly in my view model. – pfinferno Jan 03 '19 at 17:00
  • Validation is part of the ViewModel. You want to validate and coerce the data before passing it to the model. Only valid data should enter the model. So you have to implement it alongside INotifyPropertyChanged. I will give you an example. – BionicCode Jan 03 '19 at 19:53
  • 1
    I corrected a statement in my answer because I misread yours. I thought you were binding to ValidationRule.HasError but then I realized that there is no property HasError on that type. You were binding to the Validation type and the property HasError is an attached read-only property. The correct explaination why your binding is not working is that you can't bind to a read-only DependencyProperty (or AttachedProperty). – BionicCode Jan 03 '19 at 21:57
  • I tried to copy this code but it fails to compile because OnErrorsChanged and RaiseErrorsChanged are undefined and there is no return from ValidateProperty except an empty return at the beginning. – Kevin Burton Apr 25 '21 at 17:59
  • @Kevin Burton Thank you. I will fix the example to make it compile. I wrote the code without any editor. I just wanted to show the basic pattern. Meanwhile, you can follow [this working example](https://stackoverflow.com/a/56608064/3141792) to know how to implement error validation. – BionicCode Apr 26 '21 at 05:04