Skip to content

Commit 342ba5b

Browse files
committed
winui: add shared validation helpers
Add property and model validation helpers, which will be used by Bitbucket's Windows UI helper in subsequent commits.
1 parent 8e95c36 commit 342ba5b

File tree

6 files changed

+356
-0
lines changed

6 files changed

+356
-0
lines changed
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
using System;
4+
using System.Diagnostics;
5+
using System.Windows;
6+
using System.Windows.Controls;
7+
using System.Windows.Media;
8+
using Microsoft.Git.CredentialManager.UI.ViewModels.Validation;
9+
10+
namespace Microsoft.Git.CredentialManager.UI.Controls
11+
{
12+
public class ValidationMessage : UserControl
13+
{
14+
private const double DefaultTextChangeThrottle = 0.2;
15+
16+
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
17+
{
18+
if (e.Property.Name == nameof(Validator))
19+
{
20+
ShowError = Validator.ValidationResult.Status == ValidationStatus.Invalid;
21+
Visibility = ShowError ? Visibility.Visible : Visibility.Hidden;
22+
Text = Validator.ValidationResult.Message;
23+
24+
// This might look like an event handler leak, but we're making sure Validator can
25+
// only be set once. If we ever want to allow it to be set more than once, we'll need
26+
// to make sure to unsubscribe this event.
27+
Validator.ValidationResultChanged += (s, vrce) =>
28+
{
29+
ShowError = Validator.ValidationResult.Status == ValidationStatus.Invalid;
30+
Visibility = ShowError ? Visibility.Visible : Visibility.Hidden;
31+
Text = Validator.ValidationResult.Message;
32+
};
33+
}
34+
35+
base.OnPropertyChanged(e);
36+
}
37+
38+
public static readonly DependencyProperty TextProperty = DependencyProperty.Register(nameof(Text), typeof(string), typeof(ValidationMessage));
39+
40+
public string Text
41+
{
42+
get { return (string)GetValue(TextProperty); }
43+
private set { SetValue(TextProperty, value); }
44+
}
45+
46+
public static readonly DependencyProperty ShowErrorProperty = DependencyProperty.Register(nameof(ShowError), typeof(bool), typeof(ValidationMessage));
47+
48+
public bool ShowError
49+
{
50+
get { return (bool)GetValue(ShowErrorProperty); }
51+
set { SetValue(ShowErrorProperty, value); }
52+
}
53+
54+
public static readonly DependencyProperty TextChangeThrottleProperty = DependencyProperty.Register(nameof(TextChangeThrottle), typeof(double), typeof(ValidationMessage), new PropertyMetadata(DefaultTextChangeThrottle));
55+
56+
public double TextChangeThrottle
57+
{
58+
get { return (double)GetValue(TextChangeThrottleProperty); }
59+
set { SetValue(TextChangeThrottleProperty, value); }
60+
}
61+
62+
public static readonly DependencyProperty ValidatorProperty = DependencyProperty.Register(nameof(Validator), typeof(PropertyValidator), typeof(ValidationMessage));
63+
64+
public PropertyValidator Validator
65+
{
66+
get { return (PropertyValidator)GetValue(ValidatorProperty); }
67+
set
68+
{
69+
if (value == null) throw new ArgumentNullException(nameof(ValidatorProperty));
70+
Debug.Assert(Validator == null, "Only set this property once for now. If we really need it to be set more than once, we need to make sure we're not leaking event handlers");
71+
SetValue(ValidatorProperty, value);
72+
}
73+
}
74+
75+
public static readonly DependencyProperty FillProperty =
76+
DependencyProperty.Register(nameof(Fill), typeof(Brush), typeof(ValidationMessage), new PropertyMetadata(new SolidColorBrush(Color.FromRgb(0xe7, 0x4c, 0x3c))));
77+
78+
public Brush Fill
79+
{
80+
get { return (Brush)GetValue(FillProperty); }
81+
set { SetValue(FillProperty, value); }
82+
}
83+
84+
public static readonly DependencyProperty ErrorAdornerTemplateProperty = DependencyProperty.Register(nameof(ErrorAdornerTemplate), typeof(string), typeof(ValidationMessage), new PropertyMetadata("validationTemplate"));
85+
86+
public string ErrorAdornerTemplate
87+
{
88+
get { return (string)GetValue(ErrorAdornerTemplateProperty); }
89+
set { SetValue(ErrorAdornerTemplateProperty, value); }
90+
}
91+
92+
private bool IsAdornerEnabled()
93+
{
94+
return !string.IsNullOrEmpty(ErrorAdornerTemplate)
95+
&& !ErrorAdornerTemplate.Equals("None", StringComparison.OrdinalIgnoreCase);
96+
}
97+
}
98+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
7+
namespace Microsoft.Git.CredentialManager.UI.ViewModels.Validation
8+
{
9+
/// <summary>
10+
/// A validator that binds to the validation state of a <see cref="ViewModel"/>.
11+
/// It is valid if all child <see cref="PropertyValidator"/>s are valid.
12+
/// </summary>
13+
public class ModelValidator
14+
{
15+
private readonly IList<PropertyValidator> _validators = new List<PropertyValidator>();
16+
public event EventHandler IsValidChanged;
17+
18+
private bool _isValid;
19+
public bool IsValid
20+
{
21+
get => _isValid;
22+
private set
23+
{
24+
if (_isValid != value)
25+
{
26+
_isValid = value;
27+
IsValidChanged?.Invoke(this, EventArgs.Empty);
28+
}
29+
}
30+
}
31+
32+
public void Add(PropertyValidator validator)
33+
{
34+
EnsureArgument.NotNull(validator, nameof(validator));
35+
36+
_validators.Add(validator);
37+
validator.ValidationResultChanged += (s, e) => Evaluate();
38+
}
39+
40+
private void Evaluate()
41+
{
42+
IsValid = _validators.All(x => x.ValidationResult.IsValid);
43+
}
44+
}
45+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
4+
namespace Microsoft.Git.CredentialManager.UI.ViewModels.Validation
5+
{
6+
public class PropertyValidationResult
7+
{
8+
public static readonly PropertyValidationResult Success = new PropertyValidationResult(ValidationStatus.Valid);
9+
public static readonly PropertyValidationResult Unvalidated = new PropertyValidationResult(ValidationStatus.Unvalidated);
10+
11+
/// <summary>
12+
/// Describes if the property passes validation
13+
/// </summary>
14+
public bool IsValid { get; }
15+
16+
/// <summary>
17+
/// Describes which state we are in - Valid, Not Validated, or Invalid
18+
/// </summary>
19+
public ValidationStatus Status { get; }
20+
21+
/// <summary>
22+
/// An error message to display
23+
/// </summary>
24+
public string Message { get; }
25+
26+
/// <summary>
27+
/// Describes if we should show this error in the UI We only show errors which have been
28+
/// marked specifically as Invalid and we do not show errors for inputs which have not yet
29+
/// been validated.
30+
/// </summary>
31+
public bool DisplayValidationError { get; }
32+
33+
public PropertyValidationResult(ValidationStatus validationStatus, string message = null)
34+
{
35+
Status = validationStatus;
36+
IsValid = validationStatus == ValidationStatus.Valid;
37+
DisplayValidationError = validationStatus == ValidationStatus.Invalid;
38+
Message = message ?? string.Empty;
39+
}
40+
}
41+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
using System;
4+
using System.Collections.Generic;
5+
using System.ComponentModel;
6+
using System.Diagnostics;
7+
using System.Linq.Expressions;
8+
using System.Reflection;
9+
10+
namespace Microsoft.Git.CredentialManager.UI.ViewModels.Validation
11+
{
12+
public abstract class PropertyValidator
13+
{
14+
public event EventHandler ValidationResultChanged;
15+
16+
/// <summary>
17+
/// Creates a validator for a property. This validator is the starting point to attach other
18+
/// validations to the property. This method itself doesn't apply any validations.
19+
/// </summary>
20+
/// <typeparam name="TObject">Type of the object with the property to validate.</typeparam>
21+
/// <typeparam name="TProperty">The type of the property to validate.</typeparam>
22+
/// <param name="source">The object with the property to validate.</param>
23+
/// <param name="property">An expression for the property to validate</param>
24+
/// <returns>A property validator</returns>
25+
public static PropertyValidator<TObject, TProperty> For<TObject, TProperty>(TObject source, Expression<Func<TObject, TProperty>> property)
26+
where TObject : INotifyPropertyChanged
27+
{
28+
return new PropertyValidator<TObject, TProperty>(source, property);
29+
}
30+
31+
private PropertyValidationResult _validationResult = PropertyValidationResult.Unvalidated;
32+
33+
/// <summary>
34+
/// The current validation result for this validator.
35+
/// </summary>
36+
public PropertyValidationResult ValidationResult
37+
{
38+
get => _validationResult;
39+
protected set
40+
{
41+
_validationResult = value;
42+
ValidationResultChanged?.Invoke(this, EventArgs.Empty);
43+
}
44+
}
45+
}
46+
47+
public class PropertyValidator<TProperty> : PropertyValidator
48+
{
49+
private readonly IList<Func<TProperty, PropertyValidationResult>> _rules =
50+
new List<Func<TProperty, PropertyValidationResult>>();
51+
52+
public void AddRule(Func<TProperty, PropertyValidationResult> predicate)
53+
{
54+
_rules.Add(predicate);
55+
}
56+
57+
protected void Evaluate(TProperty property)
58+
{
59+
PropertyValidationResult result = PropertyValidationResult.Unvalidated;
60+
61+
foreach (Func<TProperty, PropertyValidationResult> rule in _rules)
62+
{
63+
result = rule(property);
64+
65+
// An invalid validation rule means we stop evaluating more rules
66+
// and return the current result.
67+
if (result.Status == ValidationStatus.Invalid)
68+
{
69+
break;
70+
}
71+
}
72+
73+
ValidationResult = result;
74+
}
75+
}
76+
77+
/// <summary>
78+
/// This validator watches the target property for changes and then propagates that change up the chain.
79+
/// </summary>
80+
/// <typeparam name="TObject"></typeparam>
81+
/// <typeparam name="TProperty"></typeparam>
82+
public class PropertyValidator<TObject, TProperty> : PropertyValidator<TProperty> where TObject : INotifyPropertyChanged
83+
{
84+
internal PropertyValidator(TObject source, Expression<Func<TObject, TProperty>> propertyExpression)
85+
{
86+
EnsureArgument.NotNull(source, nameof(source));
87+
EnsureArgument.NotNull(propertyExpression, nameof(propertyExpression));
88+
89+
Func<TObject, TProperty> compiledProperty = propertyExpression.Compile();
90+
PropertyInfo propertyInfo = GetPropertyInfo(propertyExpression);
91+
92+
// Start watching for changes to this property and propagate those changes to the chained validators.
93+
source.PropertyChanged += (s, e) =>
94+
{
95+
if (e.PropertyName == propertyInfo.Name)
96+
{
97+
Evaluate(compiledProperty(source));
98+
}
99+
};
100+
}
101+
102+
private static PropertyInfo GetPropertyInfo(Expression<Func<TObject, TProperty>> propertyExpression)
103+
{
104+
var member = propertyExpression.Body as MemberExpression;
105+
Debug.Assert(member != null, "Property expression doesn't refer to a member.");
106+
107+
var propertyInfo = member.Member as PropertyInfo;
108+
Debug.Assert(propertyInfo != null, "Property expression does not refer to a property.");
109+
110+
var propertyType = typeof(TObject);
111+
Debug.Assert(propertyInfo.ReflectedType != null
112+
&& (propertyType == propertyInfo.ReflectedType ||
113+
propertyType.IsSubclassOf(propertyInfo.ReflectedType)),
114+
"Property expression is not of the specified type");
115+
116+
return propertyInfo;
117+
}
118+
}
119+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
using System;
4+
using System.Security;
5+
6+
namespace Microsoft.Git.CredentialManager.UI.ViewModels.Validation
7+
{
8+
public static class PropertyValidatorExtensions
9+
{
10+
public static PropertyValidator<string> Required(this PropertyValidator<string> validator, string errorMessage)
11+
{
12+
return validator.ValidIfTrue(value => !string.IsNullOrEmpty(value), errorMessage);
13+
}
14+
15+
public static PropertyValidator<SecureString> Required(this PropertyValidator<SecureString> validator, string errorMessage)
16+
{
17+
return validator.ValidIfTrue(value => value is null ? (bool?) null : value.Length > 0, errorMessage);
18+
}
19+
20+
public static PropertyValidator<TProperty> ValidIfTrue<TProperty>(
21+
this PropertyValidator<TProperty> validator,
22+
Func<TProperty, bool?> predicate,
23+
string errorMessage)
24+
{
25+
validator.AddRule(value =>
26+
{
27+
bool? result = predicate(value);
28+
if (result.HasValue)
29+
{
30+
return result.Value
31+
? PropertyValidationResult.Success
32+
: new PropertyValidationResult(ValidationStatus.Invalid, errorMessage);
33+
}
34+
35+
return PropertyValidationResult.Unvalidated;
36+
});
37+
38+
return validator;
39+
}
40+
}
41+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
4+
namespace Microsoft.Git.CredentialManager.UI.ViewModels.Validation
5+
{
6+
public enum ValidationStatus
7+
{
8+
Unvalidated = 0,
9+
Invalid = 1,
10+
Valid = 2,
11+
}
12+
}

0 commit comments

Comments
 (0)