INotifyDataErrorInfo(有时)不起作用

INotifyDataErrorInfo (sometimes) does not work

我写了一个控件,派生自文本框,我可以在其中输入特殊格式的数字。

为确保此格式正确,我还实施了 INotifyDataErrorInfo 以进行验证。 然而,经过几次测试后,一切似乎都很好。错误修复后,验证会弹出并再次消失。

但是现在,我想在另一个 window 中使用相同的控件,但它不再起作用了。验证发生,错误被添加到字典中并调用 OnErrorsChanged,但是在调用 ErrorHandler 之后,既没有更新 HasError 属性 也没有调用 GetErrors 方法,我无法找出为什么会这样。 如前所述,在另一个 window 中,一切正常。

这里是控件的重要部分

    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));
        }
    }

在xaml中我是这样使用控件的

    <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>

我还尝试将 ValidateOnDataErrors 等属性设置为 true 但没有任何效果(我认为它们默认为 true)

感谢您的回答。 我实施了您建议的有关 DependencyProperty getter 的更改。

我也找到了解决问题的办法。 设置 ValidateOnNotifyDataErrors 解决了未调用 GetErrors 方法的问题。 似乎我只是忘记为控件设置样式(派生自 Textbox),所以应用程序不知道如何呈现验证错误。

虽然不应该现在可以正常工作了