// Copyright Keysight Technologies 2012-2019 // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, you can obtain one at http://mozilla.org/MPL/2.0/. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; namespace OpenTap { /// /// Only works on properties, not fields. The functionality is implemented primarily with a GUI in mind. /// /// /// Runtime property value error-checking system. /// Can be used to validate an object before run or to display messages through a GUI. /// public abstract class ValidatingObject : IDataErrorInfo, INotifyPropertyChanged, IValidatingObject { /// /// All the validation rules. Add new rules to this in order to get runtime value validation. /// [AnnotationIgnore] [SettingsIgnore] public ValidationRuleCollection Rules { get { if (rules == null) rules = new ValidationRuleCollection(); return rules; } } private ValidationRuleCollection rules; // thread static to avoid locking everything and having a HashSet on each ValidationObject [ThreadStatic] static HashSet traversed = null; class DynamicRule : DelegateValidationRule { public DynamicRule(IsValidDelegateDefinition isValid, string propertyName, CustomErrorDelegateDefinition errorDelegate) : base(isValid, propertyName, errorDelegate) { } } void UpdateEmbeddedAndDynamicRules() { // Only update if the key has been changed (dynamic members added or removed). var updateKey = DynamicMember.GetTypeDataKey(this); if (Rules.UpdateKey == updateKey) return; lock (Rules) { if (Rules.UpdateKey == updateKey) return; Rules.UpdateKey = updateKey; rules?.RemoveIf(x => x is DynamicRule); var td = TypeData.GetTypeData(this); { foreach (var member in td.GetMembers()) { if (member.TypeDescriptor.DescendsTo(typeof(IValidatingObject))) { var rule = new DynamicRule(() => { if (member.GetValue(this) is IValidatingObject value) { return string.IsNullOrEmpty(value.Error); } return true; }, member.Name, () => { if (member.GetValue(this) is IValidatingObject value) { return value.Error; } return null; }); Rules.Add(rule); } } } { if (td is EmbeddedTypeData) { foreach (var member in td.GetMembers().OfType()) { if (member.OwnerMember.TypeDescriptor.DescendsTo(typeof(IValidatingObject))) { var rule = new DynamicRule(() => { var src = (IValidatingObject)member.OwnerMember.GetValue(this); if (src == null) return false; var err = src[member.InnerMember.Name]; return string.IsNullOrEmpty(err); }, member.Name, () => { var src = (IValidatingObject)member.OwnerMember.GetValue(this); if (src == null) return "Instance is null"; return src[member.InnerMember.Name]; }); Rules.Add(rule); } } } } } } /// /// Return the error for a given property /// protected virtual string GetError(string propertyName = null) { UpdateEmbeddedAndDynamicRules(); List errors = null; void pushError(string error) { if (errors == null) errors = new List(); if (!errors.Contains(error)) errors.Add(error); } foreach (var rule in Rules) { try { if (propertyName != null && rule.PropertyName != propertyName) continue; if (rule.IsValid()) continue; var error = rule.ErrorMessage; if (string.IsNullOrEmpty(error)) continue; pushError(error); } catch(Exception ex) { pushError(ex.Message); } } foreach (var fwd in Rules.ForwardedRules) { if (propertyName != null && fwd.Name != propertyName) continue; var obj = fwd.GetValue(this) as IValidatingObject; if (obj == null) continue; if (traversed == null) { traversed = new HashSet(); } else if (traversed.Contains(obj)) continue; traversed.Add(obj); traversed.Add(this); try { var err = obj.Error; if (string.IsNullOrWhiteSpace(err)) continue; pushError(err.TrimEnd()); } finally { traversed.Remove(obj); } } if (errors == null) return ""; return string.Join(Environment.NewLine, errors).TrimEnd(); } /// /// Gets the error messages for each invalid rule and joins them with a newline. /// public string Error => GetError(null); /// /// Gets the error(s) for a given property as a concatenated string. /// /// /// string concatenated errors. string IDataErrorInfo.this[string propertyName] => GetError(propertyName); /// /// Checks all validation rules on this object () and throws an AggregateException on errors. /// /// If true, ignores related to properties that are disabled or hidden as a result of or . /// Thrown when any on this object are invalid. This exception contains an ArgumentException for each invalid setting. protected void ThrowOnValidationError(bool ignoreDisabledProperties) { var propertyNames = new HashSet(); TestStepExtensions.GetObjectSettings(this, ignoreDisabledProperties, (object o, IMemberData pi) => pi.Name, propertyNames); foreach (ValidationRule rule in Rules) { if (!rule.IsValid() && propertyNames.Contains(rule.PropertyName)) { throw new Exception($"The Property [{rule.PropertyName}] is invalid. Details: {rule.ErrorMessage}"); } } } #region OnPropertyChanged /// /// Standard PropertyChanged event object. /// public event PropertyChangedEventHandler PropertyChanged; /// /// Triggers the PropertyChanged event. /// /// string name of which property has been changed. public void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); UserInput.NotifyChanged(this, propertyName); } #endregion } /// /// Delegate for checking validation rule. /// /// True if valid, false if not. public delegate bool IsValidDelegateDefinition(); /// /// Delegate for returning a custom error message from a validation rule. /// /// public delegate string CustomErrorDelegateDefinition(); /// /// Validates settings at runtime. A validation rule is attached to an object of type and is used to validate the property value of that object. /// Also see /// public class ValidationRule { /// /// Name of the property affected by this rule. /// public string PropertyName { get; set; } /// /// Error message to use if the property does not follow the rule. /// public virtual string ErrorMessage { get; set; } /// /// Rule function following the signature () -> bool. /// public IsValidDelegateDefinition IsValid { get; set; } /// /// /// Property IsValid /// Property ErrorMessage- /// Property PropertyName public ValidationRule(IsValidDelegateDefinition isValid, string errorMessage, string propertyName) { ErrorMessage = errorMessage; PropertyName = propertyName; IsValid = isValid; } } /// /// Validation rule that takes a delegate as an argument. Used for writing error messages. /// public class DelegateValidationRule : ValidationRule { static readonly TraceSource log = OpenTap.Log.CreateSource("Validation"); /// /// The error calculated from ErrorDelegate. /// public override string ErrorMessage { get { try { return ErrorDelegate(); } catch (Exception e) { log.Error("Exception caught from error handling function."); log.Debug(e); return "Exception caught from error handling function."; } } set { } } /// /// The delegate producing the error message. /// public CustomErrorDelegateDefinition ErrorDelegate; /// /// Constructor for DelegateValidationRule. /// /// Validation delegate. /// Target property. /// Function creating the error message. public DelegateValidationRule(IsValidDelegateDefinition isValid, string propertyName, CustomErrorDelegateDefinition errorDelegate) : base(isValid, "", propertyName) { ErrorDelegate = errorDelegate; } } /// /// Collection of validation rules. /// Simplifies adding new rules by abstracting the use of ValidationRule objects. /// public class ValidationRuleCollection : Collection { internal int UpdateKey; /// /// Add a new rule to the collection. /// /// Rule checking function. /// Error if rule checking function returns false. /// Name of the property it affects. public void Add(IsValidDelegateDefinition isValid, string errorMessage, string propertyName) { this.Add(new ValidationRule(isValid, errorMessage, propertyName)); } /// /// This overload of ValidationRuleCollection.Add should not be used. This placeholder method is added to provide a warning. /// /// Rule checking function. /// Error if rule checking function returns false. [Obsolete("No property names are specified for this validation rule. Please specify which properties are affected by this rule or explicitly add Array.Empty()")] [EditorBrowsable(EditorBrowsableState.Never)] public void Add(IsValidDelegateDefinition isValid, string errorMessage) { } internal readonly List ForwardedRules = new List(); /// /// Dynamically adds a sub-objects rules to the collection of rules. /// /// internal void Forward(IMemberData member) => ForwardedRules.Add(member); /// /// Adds a new rule to the collection. /// /// /// /// public void Add(IsValidDelegateDefinition isValid, CustomErrorDelegateDefinition errorDelegate, string propertyName) { this.Add(new DelegateValidationRule(isValid, propertyName, errorDelegate)); } /// /// Add a new rule to the collection for multiple properties. /// Internally a new rule is created for each property. /// /// Rule checking function. /// Error if rule checking function returns false. /// Names of the properties it affects. public void Add(IsValidDelegateDefinition isValid, string errorMessage, params string[] propertyNames) { if (propertyNames == null) throw new ArgumentNullException(nameof(propertyNames)); foreach (string propertyName in propertyNames) { this.Add(isValid, errorMessage, propertyName); } } /// /// Adds a new rule to the collection. /// /// /// /// Names of the properties it affects. public void Add(IsValidDelegateDefinition isValid, CustomErrorDelegateDefinition errorDelegate, params string[] propertyNames) { if (propertyNames == null) throw new ArgumentNullException(nameof(propertyNames)); foreach (string propertyName in propertyNames) { this.Add(isValid, errorDelegate, propertyName); } } } }