chr
2026-04-05 fe750b791d5b517cc4e9bc8e99a9a75139a0cfba
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
//            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
{
    /// <remarks>
    /// Only works on properties, not fields. The functionality is implemented primarily with a GUI in mind.
    /// </remarks>
    /// <summary>
    /// Runtime property value error-checking system. 
    /// Can be used to validate an object before run or to display messages through a GUI.
    /// </summary>
    public abstract class ValidatingObject : IDataErrorInfo, INotifyPropertyChanged, IValidatingObject
    {
        /// <summary>
        /// All the validation rules. Add new rules to this in order to get runtime value validation.
        /// </summary>
        [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<object> 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<EmbeddedMemberData>())
                        {
                            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);
 
                            }
                        }
                    }
                }
            }
        }
        
        /// <summary>
        /// Return the error for a given property
        /// </summary>
        protected virtual string GetError(string propertyName = null)
        {
            UpdateEmbeddedAndDynamicRules();
            
            List<string> errors = null;
            void pushError(string error)
            {
                if (errors == null) errors = new List<string>();
                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<object>();
                }
                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();
        }
 
        /// <summary>
        /// Gets the error messages for each invalid rule and joins them with a newline.
        /// </summary>
        public string Error => GetError(null);
 
        /// <summary>
        /// Gets the error(s) for a given property as a concatenated string.
        /// </summary>
        /// <param name="propertyName"></param>
        /// <returns>string concatenated errors.</returns>
        string IDataErrorInfo.this[string propertyName] => GetError(propertyName);
 
        /// <summary>
        /// Checks all validation rules on this object (<see cref="Rules"/>) and throws an AggregateException on errors.
        /// </summary>
        /// <param name="ignoreDisabledProperties">If true, ignores <see cref="Rules"/> related to properties that are disabled or hidden as a result of <see cref="EnabledIfAttribute"/> or <see cref="Enabled{T}"/>.</param>
        /// <exception cref="AggregateException">Thrown when any <see cref="Rules"/> on this object are invalid. This exception contains an ArgumentException for each invalid setting.</exception>
        protected void ThrowOnValidationError(bool ignoreDisabledProperties)
        {
            var propertyNames = new HashSet<string>();
            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
        /// <summary>
        /// Standard PropertyChanged event object.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;
        
        /// <summary>
        /// Triggers the PropertyChanged event.
        /// </summary>
        /// <param name="propertyName">string name of which property has been changed.</param>
        public void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
            UserInput.NotifyChanged(this, propertyName);
        }
 
 
        #endregion
    }
    /// <summary>
    /// Delegate for checking validation rule.
    /// </summary>
    /// <returns>True if valid, false if not.</returns>
    public delegate bool IsValidDelegateDefinition();
    /// <summary>
    /// Delegate for returning a custom error message from a validation rule.
    /// </summary>
    /// <returns></returns>
    public delegate string CustomErrorDelegateDefinition();
 
    /// <summary>
    /// Validates settings at runtime. A validation rule is attached to an object of type <see cref="ValidatingObject"/> and is used to validate the property value of that object. 
    /// Also see <see cref="ValidatingObject.Rules"/>
    /// </summary>
    public class ValidationRule
    {
        /// <summary>
        /// Name of the property affected by this rule.
        /// </summary>
        public string PropertyName { get; set; }
        /// <summary>
        /// Error message to use if the property does not follow the rule.  
        /// </summary>
        public virtual string ErrorMessage { get; set; }
 
        /// <summary>
        /// Rule function following the signature () -> bool.  
        /// </summary>
        public IsValidDelegateDefinition IsValid { get; set; }
 
        /// <summary>
        /// </summary>
        /// <param name="isValid">Property IsValid</param>
        /// <param name="errorMessage">Property ErrorMessage-</param>
        /// <param name="propertyName">Property PropertyName</param>
        public ValidationRule(IsValidDelegateDefinition isValid, string errorMessage, string propertyName)
        {
            ErrorMessage = errorMessage;
            PropertyName = propertyName;
            IsValid = isValid;
        }
    }
 
    /// <summary>
    /// Validation rule that takes a delegate as an argument.  Used for writing error messages.
    /// </summary>
    public class DelegateValidationRule : ValidationRule
    {
        static readonly TraceSource log =  OpenTap.Log.CreateSource("Validation");
        /// <summary>
        /// The error calculated from ErrorDelegate.
        /// </summary>
        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
            {
 
            }
        }
 
        /// <summary>
        /// The delegate producing the error message.
        /// </summary>
        public CustomErrorDelegateDefinition ErrorDelegate;
 
        /// <summary>
        /// Constructor for DelegateValidationRule.
        /// </summary>
        /// <param name="isValid">Validation delegate.</param>
        /// <param name="propertyName">Target property.</param>
        /// <param name="errorDelegate">Function creating the error message.</param>
        public DelegateValidationRule(IsValidDelegateDefinition isValid, string propertyName, CustomErrorDelegateDefinition errorDelegate) : base(isValid, "", propertyName)
        {
            ErrorDelegate = errorDelegate;
        }
 
    }
    /// <summary>
    /// Collection of validation rules.
    /// Simplifies adding new rules by abstracting the use of ValidationRule objects.
    /// </summary>
    public class ValidationRuleCollection : Collection<ValidationRule>
    {
        internal int UpdateKey;
        /// <summary>
        /// Add a new rule to the collection.
        /// </summary>
        /// <param name="isValid">Rule checking function.</param>
        /// <param name="errorMessage"> Error if rule checking function returns false.</param>
        /// <param name="propertyName">Name of the property it affects.</param>
        public void Add(IsValidDelegateDefinition isValid, string errorMessage, string propertyName)
        {
            this.Add(new ValidationRule(isValid, errorMessage, propertyName));
        }
        
        /// <summary>
        /// This overload of ValidationRuleCollection.Add should not be used. This placeholder method is added to provide a warning.
        /// </summary>
        /// <param name="isValid">Rule checking function.</param>
        /// <param name="errorMessage"> Error if rule checking function returns false.</param>
        [Obsolete("No property names are specified for this validation rule. Please specify which properties are affected by this rule or explicitly add Array.Empty<string>()")]
        [EditorBrowsable(EditorBrowsableState.Never)]
        public void Add(IsValidDelegateDefinition isValid, string errorMessage)
        {
            
        }
 
        internal readonly List<IMemberData> ForwardedRules = new List<IMemberData>(); 
 
        /// <summary>
        /// Dynamically adds a sub-objects rules to the collection of rules.
        /// </summary>
        /// <param name="member"></param>
        internal void Forward(IMemberData member) => ForwardedRules.Add(member);
        
        /// <summary>
        /// Adds a new rule to the collection.
        /// </summary>
        /// <param name="isValid"></param>
        /// <param name="errorDelegate"></param>
        /// <param name="propertyName"></param>
        public void Add(IsValidDelegateDefinition isValid, CustomErrorDelegateDefinition errorDelegate, string propertyName)
        {
            this.Add(new DelegateValidationRule(isValid, propertyName, errorDelegate));
        }
 
        /// <summary>
        /// Add a new rule to the collection for multiple properties. 
        /// Internally a new rule is created for each property.
        /// </summary>
        /// <param name="isValid">Rule checking function.</param>
        /// <param name="errorMessage"> Error if rule checking function returns false.</param>
        /// <param name="propertyNames">Names of the properties it affects.</param>
        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);
            }
        }
 
        /// <summary>
        /// Adds a new rule to the collection.
        /// </summary>
        /// <param name="isValid"></param>
        /// <param name="errorDelegate"></param>
        /// <param name="propertyNames">Names of the properties it affects.</param>
        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);
            }
        }
    }
}