Search code examples
c#wpfvalidationmvvminotifydataerrorinfo

How to validate Observable Collection with INotifyDataErrorInfo interface


I'm developing WPF application with MVVM pattern and using Prism Framework.

I have a basic data class as follow.

public class ProductDecorator : DecoratorBase<Product>
{
    private string _ProductShortName;
    private Boolean _IsSelected = false;

    // I have omitted some code for clarity here.

    [Required]
    public int ProductID
    {
        get { return BusinessEntity.ProductID; }
        set
        {
            SetProperty(() => BusinessEntity.ProductID == value,
                            () => BusinessEntity.ProductID = value);
        }
    }

    public Boolean IsSelected
    {
        get { return _IsSelected; }
        set
        {
            SetProperty(ref _IsSelected, value);
        }
    }
 }

I create the observable collection of the above data class in the ViewModel.

public class SaleInvoiceViewModel {

    private ObservableCollection<ProductDecorator> _productDecorators;
    public ObservableCollection<ProductDecorator> ProductDecorators
    {
        get { return _productDecorators; }
        set { SetProperty(ref _productDecorators, value); }
    }
}

And I bounded this observable collection to the listbox in the View.

<telerik:RadListBox ItemsSource="{Binding ProductDecorators}" HorizontalAlignment="Stretch" Margin="5,10,5,5" Grid.Column="1" VerticalAlignment="Top">
  <telerik:RadListBox.ItemTemplate>
     <DataTemplate>
         <StackPanel Orientation="Horizontal">
             <CheckBox Margin="2" IsChecked="{Binding IsSelected}" />
             <TextBlock Text="{Binding ProductShortName}" FontSize="14" />
         </StackPanel>
     </DataTemplate>
  </telerik:RadListBox.ItemTemplate>
</telerik:RadListBox>

From the above context, I want to validate "the user must select at least one item in the list box". In other words, IsSelected property must be true in one of the ProductUmDecorator class from the observable collection ProductUmDecorators.

Currently I use INotifyDataErrorInfo Interface and Data Annotations for validation rule. I've lost that how should I implement my problem to achieve this validation?


Solution

  • There are a lot of questions on stackoverflow related with this topic but no solid answer. So I decided to post my solution as the answer to this problem. The context of the problem is to check "the user must select one item in the listbox which is bound with observable collection".

    First step, the item (entity) in the ObservableCollection need IsSelected property.

    public class ProductDecorator : DecoratorBase<Product>
    {
         private string _ProductShortName;
         private Boolean _IsSelected = false;
    
         // I have omitted some code for clarity here.
    
         public Boolean IsSelected
         {
             get { return _IsSelected; }
             set
             {
                 SetProperty(ref _IsSelected, value);
             }
         }
    }
    

    Second step, each item in the ObservableCollection must implement INotifyPropertyChanged interface. Then you can access the PropertyChanged eventhandler.

    Third step, you need to attach the following method to CollectionChanged event handler of ObservableCollection.

    public class SaleInvoiceViewModel {
    
         private ObservableCollection<ProductDecorator> _productDecorators;
         public ObservableCollection<ProductDecorator> ProductDecorators
         {
              get { return _productDecorators; }
              set { SetProperty(ref _productDecorators, value); }
         }
    
         public SaleInvoiceViewModel() {
               _productDecorators= new ObservableCollection<ProductDecorator>();
               _productDecorators.CollectionChanged += ContentCollectionChanged;
         }
    
         public void ContentCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
         {
              if (e.Action == NotifyCollectionChangedAction.Remove)
              {
                  foreach(ProductDecorator item in e.OldItems)
                  {
                       //Removed items
                       item.PropertyChanged -= EntityPropertyChanged;
                  }
              }
             else if (e.Action == NotifyCollectionChangedAction.Add)
             {
                  foreach(ProductDecorator item in e.NewItems)
                  {
                      //Added items
                      item.PropertyChanged += EntityPropertyChanged;
                  }     
             }       
        }
    }
    

    Look carefully EntityPropertyChanged method in the above code. This method will trigger whenever any properties changed in any items in the ObservableCollection. Then, you can simply call Validate method in the EntityPropertyChanged method.

    private void EntityPropertyChanged( object sender, PropertyChangedEventArgs e )
    {
          if (e.PropertyName == "IsSelected")
                this.Product.ValidateProperty("ProductUmDecorators");
    }
    

    If the changed property is IsSelected, the ValidatedProperty method will be run. I will omit the detail implementation of ValidateProperty method. This method will try to validate the property with DataAnnotations and trigger the ErrorChanged event when there is any errors. You can learn detail here.

    Finally, you need to implement custom DataAnnotation ValidationAttribute in your Entity/ViewModel/Decorator class where your ObservableCollection property existed as the following code.

    public class SaleInvoiceViewModel {
    
         private ObservableCollection<ProductDecorator> _productDecorators;
         [AtLeastChooseOneItem(ErrorMessage = "Choose at least one item in the following list.")]
         public ObservableCollection<ProductDecorator> ProductDecorators
         {
             get { return _productDecorators; }
             set { SetProperty(ref _productDecorators, value); }
         }
    }
    
    public class AtLeastChooseOneItem : ValidationAttribute
    {
        protected override ValidationResult IsValid( object value, ValidationContext validationContext )
        {
            ProductDecorator tmpEntity = (ProductDecorator) validationContext.ObjectInstance;
            var tmpCollection = (ObservableCollection<ProductUmDecorator>) value;
    
            if ( tmpCollection.Count == 0 )
                return ValidationResult.Success;
    
            foreach ( var item in tmpCollection )
            {
                if ( item.IsSelected == true )
                    return ValidationResult.Success;
            }
    
            return new ValidationResult( ErrorMessage );
        }
    }
    

    That is a little complex solution but that is the most solid solution I find out so far.