如何在父类实现IDataErrorInfo验证子对象并完成验证绑定?附代码实现困惑求助
Hi there! No worries at all—validation with nested objects can feel confusing when you're starting out. Let's break down exactly how to use that DataErrorInfo abstract class with your AddEditItemVM and nested models step by step.
Step 1: Make Your ViewModel Inherit from DataErrorInfo
First, update your AddEditItemVM to inherit from the DataErrorInfo abstract class. Also, ensure you have the necessary using directives for the validation library (the one that provides Validator, ValidationSpecificationSource, etc.—this is typically the Enterprise Library Validation Application Block or a similar framework):
using System.ComponentModel; using System.Reflection; // Add your validation library's using directive here, e.g.: // using Microsoft.Practices.EnterpriseLibrary.Validation; public class AddEditItemVM : DataErrorInfo { // Your existing properties public Category SelectedCategory { get => _SelectedCategory; set { _SelectedCategory = value; OnPropertyChanged(nameof(SelectedCategory)); } } private Category _SelectedCategory = new Category(); public Items Source { get => _Source; set { _Source = value; OnPropertyChanged(nameof(Source)); // Raise change notifications for nested properties to trigger validation OnPropertyChanged(nameof(Source.Name)); OnPropertyChanged(nameof(Source.Price)); OnPropertyChanged(nameof(Source.TaxPerc)); } } private Items _Source = new Items(); // INotifyPropertyChanged implementation (critical for validation triggers) public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
Step 2: Add Validation Rules to Your Model Properties
The DataErrorInfo class relies on validation attributes (or configuration) to define rules. Add these attributes to your Category and Items classes to enforce your required checks:
Updated Category Class
using System.ComponentModel.DataAnnotations; // Adjust if using a different validation library public class Category : INotifyPropertyChanged { // Validate that a category is selected (Id can't be 0) [Range(1, long.MaxValue, ErrorMessage = "Please select a valid category")] public long Id { get => _Id; set { _Id = value; OnPropertyChanged(nameof(Id)); } } long _Id; public string Name { get => _Name; set { _Name = value; OnPropertyChanged(nameof(Name)); } } private string _Name; // INotifyPropertyChanged implementation public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
Updated Items Class
using System.ComponentModel.DataAnnotations; public class Items : INotifyPropertyChanged { public int CategoryID { get => _CategoryID; set { _CategoryID = value; OnPropertyChanged(nameof(CategoryID)); } } int _CategoryID; // Validate Name: Required + max 20 characters [Required(ErrorMessage = "Item name cannot be empty")] [StringLength(20, ErrorMessage = "Item name cannot exceed 20 characters")] public string Name { get => _Name; set { _Name = value; OnPropertyChanged(nameof(Name)); } } string _Name = String.Empty; // Validate TaxPerc: Required + between 1-100 [Required(ErrorMessage = "Tax percentage is required")] [Range(1, 100, ErrorMessage = "Tax percentage must be between 1 and 100")] public decimal? TaxPerc { get => _TaxPerc; set { _TaxPerc = value; // Reverted to use the passed value (adjust if you still need the Settings.Default logic) OnPropertyChanged(nameof(TaxPerc)); OnPropertyChanged(nameof(PricePlsTax)); OnPropertyChanged(nameof(TaxAmount)); } } decimal? _TaxPerc = null; // Validate Price: Required + cannot be zero [Required(ErrorMessage = "Price cannot be empty")] [Range(0.01, decimal.MaxValue, ErrorMessage = "Price cannot be zero")] public decimal? Price { get => _Price; set { _Price = value; OnPropertyChanged(nameof(Price)); OnPropertyChanged(nameof(PricePlsTax)); OnPropertyChanged(nameof(TaxAmount)); } } decimal? _Price = null; // Computed properties (assumed implementation) public decimal PricePlsTax => Price.GetValueOrDefault() * (1 + TaxPerc.GetValueOrDefault() / 100); public decimal TaxAmount => Price.GetValueOrDefault() * (TaxPerc.GetValueOrDefault() / 100); // INotifyPropertyChanged implementation public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
Step 3: Extend Validation to Handle Nested Properties
The default DataErrorInfo implementation only validates properties directly on the parent class. To validate nested properties like Source.Name or SelectedCategory.Id, override the indexer in your AddEditItemVM to traverse nested paths:
First, update the abstract DataErrorInfo class to make the validator method accessible:
public abstract class DataErrorInfo : IDataErrorInfo { string IDataErrorInfo.Error { get { return string.Empty; } } // Make the indexer virtual so we can override it virtual string IDataErrorInfo.this[string columnName] { get { var prop = this.GetType().GetProperty(columnName); return GetErrorInfo(prop); } } private string GetErrorInfo(PropertyInfo prop) { var validator = GetPropertyValidator(prop); if (validator != null) { var results = validator.Validate(this); if (!results.IsValid) { return string.Join(" ", results.Select(r => r.Message).ToArray()); } } return string.Empty; } // Make this method protected so child classes can reuse it protected virtual Validator GetPropertyValidator(PropertyInfo prop) { string ruleset = string.Empty; var source = ValidationSpecificationSource.All; var builder = new ReflectionMemberValueAccessBuilder(); return PropertyValidationFactory.GetPropertyValidator( prop.DeclaringType, prop, ruleset, source, builder); } }
Then, override the indexer in AddEditItemVM to handle nested properties:
public class AddEditItemVM : DataErrorInfo { // ... Your existing properties and INotifyPropertyChanged ... string IDataErrorInfo.this[string columnName] { get { // Handle nested properties (e.g., "Source.Name") if (columnName.Contains('.')) { var propertyParts = columnName.Split('.'); object currentObject = this; // Traverse each part of the nested path foreach (var part in propertyParts) { var prop = currentObject.GetType().GetProperty(part); if (prop == null) return string.Empty; currentObject = prop.GetValue(currentObject); if (currentObject == null) return "Required field cannot be empty"; } // Validate the final nested property var nestedProp = currentObject.GetType().GetProperty(propertyParts.Last()); if (nestedProp != null) { var validator = GetPropertyValidator(nestedProp); if (validator != null) { var results = validator.Validate(currentObject); if (!results.IsValid) { return string.Join(" ", results.Select(r => r.Message).ToArray()); } } } return string.Empty; } // Use base implementation for non-nested properties return base[columnName]; } } }
Step 4: Enable Validation in Your View
In your XAML, bind controls to the nested properties and enable validation feedback with ValidatesOnDataErrors=True:
<!-- Category Selection --> <ComboBox ItemsSource="{Binding Categories}" SelectedItem="{Binding SelectedCategory, ValidatesOnDataErrors=True}" DisplayMemberPath="Name" /> <!-- Item Name --> <TextBox Text="{Binding Source.Name, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" /> <!-- Item Price --> <TextBox Text="{Binding Source.Price, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" /> <!-- Tax Percentage --> <TextBox Text="{Binding Source.TaxPerc, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" />
Key Reminders
- All classes must implement
INotifyPropertyChangedto trigger validation when properties update. - Adjust validation attributes if you're using a library other than
System.ComponentModel.DataAnnotations(e.g., Enterprise Library'sNotNullValidatororRangeValidator). - When updating the
Sourceproperty, raisePropertyChangedfor all nested properties to ensure validation refreshes immediately.
内容的提问来源于stack exchange,提问作者Muhammed Ali

