user3605194
user3605194

Reputation: 51

WPF iDataErrorInfo (validating textboxes) not working as intended

UPDATE: I fixed this by improving my switch statement. Made use of nameof!

I'm trying to validate a set of user inputs from textboxes.

I have my class with interface setup. Snippet of it below:

public class PatientValidation : INotifyPropertyChanged, IDataErrorInfo
    {
        private string _id;
        private string _fname;

        public event PropertyChangedEventHandler PropertyChanged;

        private void OnPropertyChanged(string p)
        {   
            PropertyChangedEventHandler ph = PropertyChanged;
            if (ph != null)
            {
                ph(this, new PropertyChangedEventArgs(p));
            }

        }

        public string Id
        {
            get
            {
                return _id;
            }    
            set
            {
                _id = value;
            }
        }
        public string Fname
        {
            get
            {
                return _fname;
            }    
            set
            {
                _fname = value;
            }
        }

And a switch statement to return error message based on user input:

public string this[string PropertyName]
        {
            get
            {
                string result = null;

                switch (PropertyName)
                {
                    case "Id":
                        if (string.IsNullOrEmpty(Id))
                            result = "ID number is required.";
                        break;

                    case "fname":
                        if (string.IsNullOrEmpty(Fname))
                            result = "First name is required.";
                        break;
                  }
                    return result;
             }
          }

My relevant code in XAML:

<Style TargetType="TextBox">
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="true">
                    <Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors).CurrentItem.ErrorContent}"/>
                </Trigger>
            </Style.Triggers>
        </Style>

<TextBox x:Name="textBox_IDNumber" Text="{Binding Id, Mode=TwoWay, ValidatesOnDataErrors=True}"/>
<TextBox x:Name="textBox_FirstName" Text="{Binding Fname, Mode=TwoWay, ValidatesOnDataErrors=True"}/>

Here's the problem I've run into: ONLY the first textbox (ID) is validated correctly and the error tooltip is shown. None of the other textboxes. Different bindings and triggers hasn't solved the issue. Any help would be much appreciated.

Upvotes: 0

Views: 1348

Answers (3)

Smagin Alexey
Smagin Alexey

Reputation: 345

You can using:

public class DataErrorInfoWrapper : DynamicObject, IDataErrorInfo, INotifyPropertyChanged
{
    private static readonly ConcurrentDictionary<Type, Dictionary<string, PropertyInfo>> Properties = new ConcurrentDictionary<Type, Dictionary<string, PropertyInfo>>();
    private readonly Dictionary<string, PropertyInfo> _typeProperties;
    private readonly Func<string, string> _error;
    private readonly object _target;

    public string this[string columnName] => _error(columnName);

    public string Error { get; }

    public event PropertyChangedEventHandler PropertyChanged;

    public DataErrorInfoWrapper(object target, Func<string, string> error)
    {
        _error = error;
        _target = target;
        _typeProperties = Properties.GetOrAdd(_target.GetType(), t => t.GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(i => i.SetMethod != null && i.GetMethod != null).ToDictionary(i => i.Name));
    }

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        result = null;
        if (!_typeProperties.TryGetValue(binder.Name, out var property))
            return false;
        var getter = property.CreateGetter();
        result = getter.DynamicInvoke(_target);
        return true;
    }

    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
        if (!_typeProperties.TryGetValue(binder.Name, out var property))
            return false;
        var setter = property.CreateSetter();
        setter.DynamicInvoke(_target, value);
        RaisePropertyChanged(binder.Name);
        return true;
    }

    protected virtual bool SetProperty<TValue>(ref TValue storage, TValue value, [CallerMemberName] string propertyName = null)
    {
        if (EqualityComparer<TValue>.Default.Equals(storage, value))
            return false;
        storage = value;
        RaisePropertyChanged(propertyName);
        return true;
    }

    protected virtual bool SetProperty<TValue>(ref TValue storage, TValue value, Action onChanged, [CallerMemberName] string propertyName = null)
    {
        if (EqualityComparer<TValue>.Default.Equals(storage, value))
            return false;
        storage = value;
        onChanged?.Invoke();
        RaisePropertyChanged(propertyName);
        return true;
    }

    protected void RaisePropertyChanged([CallerMemberName] string propertyName = null)
    {
        OnPropertyChanged(propertyName);
    }

    protected virtual void OnPropertyChanged(PropertyChangedEventArgs args)
    {
        PropertyChanged?.Invoke(this, args);
    }

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
    }
}

And extensions methods:

public static class TypeExtensions
{
    private static readonly ConcurrentDictionary<PropertyInfo, Delegate> Getters = new ConcurrentDictionary<PropertyInfo, Delegate>();
    private static readonly ConcurrentDictionary<PropertyInfo, Delegate> Setters = new ConcurrentDictionary<PropertyInfo, Delegate>();

    public static Delegate CreateGetter(this PropertyInfo property)
    {
        return Getters.GetOrAdd(property, p =>
        {
            var parameter = Expression.Parameter(p.DeclaringType, "o");
            var delegateType = typeof(Func<,>).MakeGenericType(p.DeclaringType, p.PropertyType);
            var lambda = Expression.Lambda(delegateType, Expression.Property(parameter, p.Name), parameter);
            return lambda.Compile();
        });
    }

    public static Delegate CreateSetter(this PropertyInfo property)
    {
        return Setters.GetOrAdd(property, p =>
        {
            var parameter = Expression.Parameter(p.DeclaringType, "o");
            var valueParm = Expression.Parameter(p.PropertyType, "value");
            var delegateType = typeof(Action<,>).MakeGenericType(p.DeclaringType, p.PropertyType);
            var lambda = Expression.Lambda(delegateType, Expression.Assign(Expression.Property(parameter, p.Name), valueParm), parameter, valueParm);
            return lambda.Compile();
        });
    }
}

Upvotes: 0

William Lehman
William Lehman

Reputation: 11

It doesn't seem that you are calling OnPropertyChanged(nameof(FName)); or OnPropertyChanged(nameof(ID)); within the setters of your properties - thus there will be no notification that the binding has updated, and the IDataErrorInfo.<propertyName> will not be called.

Upvotes: 1

Colin Smith
Colin Smith

Reputation: 12540

Typo error (you used lowercase in your switch):

case "fname":

but in your Binding:

Text="{Binding Fname

Upvotes: 1

Related Questions