ChristianD
ChristianD

Reputation: 69

WPF Enable/Disable button based on validation item in ItemsControl

I'm trying to set the enable state of a button depending on the validation result of the items in an ItemsControl (The user should not be able to save if any validation errors exist).

I've done this successfully with other input fields like so:

   <Style x:Key="SaveButtonStyle" TargetType="Button">
        <Style.Triggers>
             <DataTrigger Binding="{Binding ElementName=MinValueTextBox, Path=(Validation.HasError)}" Value="True">
                  <Setter Property="IsEnabled" Value="False"/>
              </DataTrigger>
        </Style.Triggers>
   </Style>

However now I also have an ItemsControl bound to a collection which I wish to validate in the same manner. The collection objects implement IDataErrorInfo.

How would I do this since I can't specify ElementName in the DataTrigger of the button style?

I want to disable the save button if not all input fields of all the collection items are valid.

Here is the ItemsControl code with DataTemplate:

<ItemsControl ItemsSource="{Binding FeedTypes}" ItemTemplate="{StaticResource FeedTypeDataTemplate}"/>

<DataTemplate x:Key="FeedTypeDataTemplate">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="3*"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="3*"/>
        </Grid.ColumnDefinitions>

        <Label Content="{x:Static p:Resources.FeedTypeNameLabel}"/>

        <TextBox Name="FeedTypeNameTxtBox" Text="{Binding UpdateSourceTrigger=PropertyChanged, Path=Name,
            ValidatesOnDataErrors=true, NotifyOnValidationError=true}" Grid.Column="1"/>

        <Label Content="{x:Static p:Resources.KgPerLitreLabel}" Grid.Column="2"/>

        <TextBox x:Name="FeedTypeKgPerLitreTxtBox" Text="{Binding UpdateSourceTrigger=PropertyChanged, Path=KgPerLitre,
            ValidatesOnDataErrors=true, NotifyOnValidationError=true}" Grid.Column="3"/>
    </Grid>
</DataTemplate>

regards!

Upvotes: 3

Views: 1145

Answers (1)

toadflakz
toadflakz

Reputation: 7944

I would push the validation into the ViewModel using attribute-based validation and check the validation status as part of the CanExecute method implementation for the Command of your button.

This gives the greatest flexibility to then simply mark UI-editable properties with attributes for validation purposes and use a simple interface to check that all the ViewModel properties (including any nested collection based items) are valid.

As a bonus, the validation is also unit-testable.

For collections, I typically use a CustomValidation attribute with a method inside the same ViewModel as the collection property that is being validated.

If you need a basic working example, have a look at this class and interface:

using System;
using System.ComponentModel;
using System.Linq.Expressions;

namespace MwoCWDropDeckBuilder.Infrastructure.Interfaces
{
    public interface IValidatingBaseViewModel : IDataErrorInfo
    {
        bool IsValid();
        bool IsValid<T>(Expression<Func<T>> propertyExpression);
    }
}


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Linq.Expressions;
using MwoCWDropDeckBuilder.Infrastructure.Interfaces;

namespace MwoCWDropDeckBuilder.Infrastructure
{
    public class ValidatingBaseViewModel<TModelType> : ValidatingBaseViewModel, IBaseViewModel<TModelType>
        where TModelType : class
    {
        public TModelType Model { get; private set; }

        public ValidatingBaseViewModel(TModelType modelObject)
        {
            Model = modelObject;
        }
    }


    public class ValidatingBaseViewModel : BaseViewModel, IValidatingBaseViewModel
    {
        private Dictionary<string, bool> _validationResults = new Dictionary<string, bool>();

        public string Error
        {
            get { return null; }
        }

        public string this[string propertyName]
        {
            get { return OnValidate(propertyName); }
        }

        public bool IsValid()
        {
            var t = GetType();
            var props = t.GetProperties().Where(
                prop => Attribute.IsDefined(prop, typeof(ValidationAttribute)));
            return props.All(x => IsValid(x.Name));

        }

        public bool IsValid<T>(Expression<Func<T>> propertyExpression)
        {
            var propertyName = GetPropertyName(propertyExpression);
            return IsValid(propertyName);
        }

        private bool IsValid(string propertyName)
        {
            OnValidate(propertyName);
            return _validationResults[propertyName];
        }

        private string OnValidate(string propertyName)
        {
            if (string.IsNullOrEmpty(propertyName))
            {
                throw new ArgumentException("Invalid property name", propertyName);
            }

            string error = string.Empty;
            var value = GetValue(propertyName);

            var t = GetType();
            var props = t.GetProperties().Where(
                prop => Attribute.IsDefined(prop, typeof(ValidationAttribute)));
            if (props.Any(x => x.Name == propertyName))
            {

                var results = new List<ValidationResult>(1);
                var result = Validator.TryValidateProperty(
                    value,
                    new ValidationContext(this, null, null)
                    {
                        MemberName = propertyName
                    },
                    results);

                StoreValidationResult(propertyName, result);

                if (!result)
                {
                    var validationResult = results.First();
                    error = validationResult.ErrorMessage;
                }
            }
            return error;
        }

        private void StoreValidationResult(string propertyName, bool result)
        {
            if (_validationResults.ContainsKey(propertyName) == false)
                _validationResults.Add(propertyName, false);
            _validationResults[propertyName] = result;
        }

        #region Privates

        private string GetPropertyName<T>(Expression<Func<T>> propertyExpression)
        {
            var memberExpression = propertyExpression.Body as MemberExpression;
            if (memberExpression == null)
            {
                throw new InvalidOperationException();
            }

            return memberExpression.Member.Name;
        }

        private object GetValue(string propertyName)
        {
            object value;
            var propertyDescriptor = TypeDescriptor.GetProperties(GetType()).Find(propertyName, false);
            if (propertyDescriptor == null)
            {
                throw new ArgumentException("Invalid property name", propertyName);
            }

            value = propertyDescriptor.GetValue(this);
            return value;
        }

        #endregion

    }
}

The full project is at: WPF Project using attribute validation

Upvotes: 2

Related Questions