Search code examples
c#wpfvalidationinotifydataerrorinfo

INotifyDataErrorInfo (sometimes) does not work


i wrote a control, derived from Textbox, where I can enter numbers in a special format.

To ensure, this format is correct I also implemented INotifyDataErrorInfo for validation. However, after a few tests everything seems fine. The validaton pops up and also disappears again when the error has been fixed.

But now, I wanted to use the same control in another window and there it doesn't work anymore. The validation happens, the error is added to the dictionary and OnErrorsChanged is called, but after the ErrorHandler gets invoked neither the HasError property gets updated nor the GetErrors method is called and I cannot find out why this is the case. In the other window, as said, everything works as expected.

Here is the important part of the control

    public class UnitedStatesCustomaryUnitTextBox : TextBox, INotifyDataErrorInfo
    {
        static UnitedStatesCustomaryUnitTextBox()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(UnitedStatesCustomaryUnitTextBox), new FrameworkPropertyMetadata(typeof(UnitedStatesCustomaryUnitTextBox)));
        }

        private readonly Dictionary<string, List<string>> _propertyErrors = new Dictionary<string, List<string>>();

        #region property Notifications

        public static readonly DependencyProperty NotificationsProperty = DependencyProperty.Register(
            "Notifications",
            typeof(List<Notification>),
            typeof(UnitedStatesCustomaryUnitTextBox),
            new PropertyMetadata(default(List<Notification>), OnNotificationsChanged));

        public List<Notification> Notifications
        {
            get
            {
                var result = (List<Notification>)GetValue(NotificationsProperty);
                if (result != null)
                    return result;

                result = new List<Notification>();
                SetValue(NotificationsProperty, result);

                return result;
            }
            set { SetValue(NotificationsProperty, value); }
        }

        private static void OnNotificationsChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            var ctl = sender as UnitedStatesCustomaryUnitTextBox;
            if (ctl == null)
                return;

            var oldValue = e.OldValue as List<Notification>;
            var newValue = e.NewValue as List<Notification>;

            ctl.OnNotificationsChanged(oldValue, newValue);
        }

        private void OnNotificationsChanged(List<Notification> oldValue, List<Notification> newValue)
        {
            Client.Controls.UnitedStatesCustomaryUnitTextBox.UnitedStatesCustomaryUnitTextBox.OnNotificationsChanged
        }

        #endregion

        #region property LengthUomId

        public static readonly DependencyProperty LengthUomIdProperty = DependencyProperty.Register(
            "LengthUomId",
            typeof(int?),
            typeof(UnitedStatesCustomaryUnitTextBox),
            new PropertyMetadata(default(int?), OnLengthUomIdChanged));

        public int? LengthUomId
        {
            get => (int?)GetValue(LengthUomIdProperty);
            set => this.SetValue(LengthUomIdProperty, value);
        }
        
        #endregion

        #region property Value

        public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
            "Value",
            typeof(decimal?),
            typeof(UnitedStatesCustomaryUnitTextBox),
            new PropertyMetadata(default(decimal?), OnValueChanged));

        public decimal? Value
        {
            get => (decimal?)GetValue(ValueProperty);
            set => this.SetValue(ValueProperty, value);
        }

        private static void OnValueChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            var ctl = sender as UnitedStatesCustomaryUnitTextBox;
            if (ctl == null)
            {
                return;
            }

            var oldValue = e.OldValue as decimal?;
            var newValue = e.NewValue as decimal?;

            ctl.OnValueChanged(oldValue, newValue);
        }

        private void OnValueChanged(decimal? oldValue, decimal? newValue)
        {
            if (!this._isCalculating)
                this.SetCurrentValue(TextProperty, this.CalculateFeetInchSixteenth(newValue));
        }

        #endregion

        private bool IsFeetInchSixteenth => Defaults.UomDefaults.DefaultLengthUomId == this.LengthUomId;

        protected override void OnTextChanged(TextChangedEventArgs e)
        {
            this._isCalculating = true;

            if (!this.IsFeetInchSixteenth)
            {
                if (decimal.TryParse(this.Text, out decimal d))
                    this.Value = d;
                return;
            }

            if (this.ValidateText(this.Text))
                this.CalculateValue(this.Text);

            this._isCalculating = false;

            base.OnTextChanged(e);
        }

        private bool _isCalculating { get; set; }

        private void CalculateValue(string text)
        {
            var numbers = text.Split('-');

            this.Value = Convert.ToDecimal(
                int.Parse(numbers[0]) * 192 +
                (int.Parse(numbers[1]) * 16) +
                (int.Parse(numbers[2]) * 1));
        }

        private string CalculateFeetInchSixteenth(decimal? value)
        {
            if (value == null)
                return "0-0-0";

            var feet = Math.Truncate(value.Value / 192);
            var inch = Math.Truncate((value.Value - (feet * 192)) / 16);
            var sixteenth = Math.Truncate(value.Value - (feet * 192) - (inch * 16));

            return $"{feet}-{inch}-{sixteenth}";
        }

        private bool ValidateText(string text)
        {
            this._propertyErrors.Clear();
            this.Notifications?.Clear();
            this.OnErrorsChanged(nameof(this.Text));

            if (string.IsNullOrWhiteSpace(text))
                return false;

            var numbers = text.Split('-');
            if (numbers.Length != 3)
            {
                var notification = new Notification(
                    NotificationType.Error,
                    "FISC0001",
                    NotificationResources.FISC0001,
                    NotificationLocalizer.Localize(() => NotificationResources.FISC0001, new object[] { string.Empty }),
                    null);
                this.AddError(nameof(this.Text), notification);
                return false;
            }

            if (!this.CheckNumberRange(numbers))
            {
                var notification = new Notification(
                    NotificationType.Error,
                    "FISC0002",
                    NotificationResources.FISC0002,
                    NotificationLocalizer.Localize(() => NotificationResources.FISC0002, new object[] { string.Empty }),
                    null);

                this.AddError(nameof(this.Text), notification);
                return false;
            }

            return true;
        }

        private bool CheckNumberRange(string[] numbers)
        {
            if (!int.TryParse(numbers[0], out int number1))
                return false;
            if (!int.TryParse(numbers[1], out int number2))
                return false;
            if (!int.TryParse(numbers[2], out int number3))
                return false;

            return this.IsBetween(number2, 0, 11) && this.IsBetween(number3, 0, 15);
        }

        [DebuggerStepThrough]
        private bool IsBetween(int number, int min, int max)
        {
            return number >= min && number <= max;
        }

        public IEnumerable GetErrors(string propertyName)
        {
            this._propertyErrors.TryGetValue(propertyName, out List<string> errors);

            return errors;
        }

        public bool HasErrors => this._propertyErrors.Any();

        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

        public void AddError(string propertyName, Notification notification)
        {
            if (!this._propertyErrors.ContainsKey(propertyName))
                this._propertyErrors.Add(propertyName, new List<string>());

            this._propertyErrors[propertyName].Add(notification.LocalizedMessage ?? notification.Message);
            this.OnErrorsChanged(propertyName);

            this.Notifications.Add(notification);
        }

        private void OnErrorsChanged(string propertyName)
        {
            ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
        }
    }

In the xaml I'm using the control like this

    <unitedStatesCustomaryUnitTextBox:UnitedStatesCustomaryUnitTextBox 
                HorizontalAlignment="Stretch"
                Visibility="{Binding IsLengthUnitedStatesCustomaryUnit.Value, UpdateSourceTrigger=PropertyChanged, Converter={StaticResource FalseToCollapsedConverter}}"
                Value="{Binding MeasuredLiquidLevelValue.Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                LengthUomId="{Binding MeasuredLiquidLevelUom.Value.Id}"
                Notifications="{Binding LengthErrors}"
                Margin="{DynamicResource DefaultMarginAll}">
                    <i:Interaction.Behaviors>
                        <ncshared:LostFocusToCommandBehavior Command="{Binding CalculationRelatedControlLostFocusCommand}" />
                    </i:Interaction.Behaviors>
                </unitedStatesCustomaryUnitTextBox:UnitedStatesCustomaryUnitTextBox>

I also tried setting the properties like ValidateOnDataErrors to true with no effect (they are true by default I think)


Solution

  • Thanks for the answers. I implemented the changes you suggested regarding the DependencyProperty getters.

    Also I found a solution for my problem. Setting ValidateOnNotifyDataErrors resolved the problem with the GetErrors method not being called. It also seems that I just forgot to set a style for the control (derived from Textbox), so the application didn't know how to render the validation error.

    Although it shouldn't it works fine now

    enter image description here