MahApps.Metro icon indicating copy to clipboard operation
MahApps.Metro copied to clipboard

Validation error popup on TextBox briefly flashes at the top of screen

Open Reeceeboii opened this issue 2 years ago • 0 comments

Describe the bug

When using data validation against a user-entered value via a TextBox with UpdateSourceTrigger=PropertyChanged, I have noticed that whenever the user enters an incorrect value, the error popup will actually appear briefly at the top of the screen before returning to the correct place besides the offending TextBox.

This looks extremely similar, if not identical, to #4120. But confusingly, I get the same behaviour even if I remove ValidatesOnExceptions=True.

Another issue, #3809, which is for a different bug, however there was a screenshot included in the replies that perfectly demonstrates what is occurring for me. However, here is a video recorded from my machine. This occurs when running in Debug and Release.

https://github.com/MahApps/MahApps.Metro/assets/42159320/1057965a-e999-4930-a445-469c81996316

Steps to reproduce

  1. Have a model for data validation that implements INotifyDataErrorInfo:
    public abstract class ValidationModelBase : INotifyDataErrorInfo
    {
        private readonly IDictionary<string, List<string>> propertyErrors;

        protected ValidationModelBase()
        {
            this.propertyErrors = new Dictionary<string, List<string>>();
        }

        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

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

        public bool IsValid => !this.HasErrors;

        public IEnumerable GetErrors(string propertyName)
        {
            if (propertyName == null)
            {
                return null;
            }

            return this.propertyErrors.TryGetValue(propertyName, out List<string> errors) ? errors : null;
        }

        public void AddErrors(string propertyName, string message)
        {
            if (!this.propertyErrors.ContainsKey(propertyName))
            {
                this.propertyErrors.Add(propertyName, new List<string>());
            }

            this.propertyErrors[propertyName].Add(message);
            this.RaiseErrorsChangedEvent(propertyName);
        }

        public void RemoveErrors(string propertyName)
        {
            if (this.propertyErrors.Remove(propertyName))
            {
                this.RaiseErrorsChangedEvent(propertyName);
            }
        }

        private void RaiseErrorsChangedEvent(string propertyName)
        {
            this.ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
        }
    }
    public class Router : ValidationModelBase
    {
        private string routerIpv4Address;

        public string RouterIpv4Address
        {
            get => this.routerIpv4Address;
            set
            {
                if (value != this.routerIpv4Address)
                {
                    this.RemoveErrors(nameof(this.RouterIpv4Address));
                    if (!NetworkService.VerifyIpv4Address(value)) // hardcode true/false here to mimic actual verification for the purpose of duplication
                    {
                        this.AddErrors(nameof(this.RouterIpv4Address), "Not a valid IPv4 value");
                    }
                    this.routerIpv4Address = value;
                } 
            }
        }
    }
  1. Use this model as the backing field for a property you intend to validate inside a ViewModel:
    public class RouterSetupViewModel : ObservableObject, INotifyDataErrorInfo
    {
        private readonly Router router;

        public RouterSetupViewModel()
        {
            this.router = new Router();
            this.router.ErrorsChanged += this.RouterErrorsChanged;
        }

        public bool HasErrors => this.router.HasErrors;

        public IEnumerable GetErrors(string propertyName)
        {
            return this.router.GetErrors(propertyName);
        }

        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

        public string RouterIpv4Address
        {
            get => this.router.RouterIpv4Address;
            set {
                this.router.RouterIpv4Address = value;
                this.OnPropertyChanged();
            }
        }

        private void RouterErrorsChanged(object sender, DataErrorsChangedEventArgs e)
        {
            this.ErrorsChanged?.Invoke(this, e);
        }
    }
  1. Bind to property in view:
<TextBox
    Text="{ Binding RouterIpv4Address, 
    UpdateSourceTrigger=PropertyChanged,
    NotifyOnValidationError=True,
    ValidatesOnExceptions=True,
    Delay=400}"
    mah:TextBoxHelper.ClearTextButton="True"
    mah:TextBoxHelper.UseFloatingWatermark="True"
    mah:TextBoxHelper.Watermark="Router's IP address"/>

Expected behavior

The error only appears alongside the TextBox.

Actual behavior

The error briefly flashes at the top of the screen.

Environment

MahApps.Metro version: 2.4.9
Windows build number: Win11 22H2 [Version 22621.1778]
Visual Studio Enterprise 2022 17.6.2
Target Framework: .NET Framework 4.8

Screenshots

Video included above.

Reeceeboii avatar May 29 '23 03:05 Reeceeboii