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