Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion XAMLTest.Tests/VisualElementTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
Expand Down Expand Up @@ -310,4 +310,62 @@ static void ChangeTitle(Window window)
window.Title = "Test Title";
}
}

[TestMethod]
public async Task MarkInvalid_WhenPropertyIsDependencyObjectWithoutExistingBinding_SetsValidationError()
{
// Arrange
await using TestRecorder recorder = new(App);

const string expectedErrorMessage = "Custom validation error";
await Window.SetXamlContent(@"<TextBox x:Name=""TextBox"" />");
IVisualElement<TextBox> textBox = await Window.GetElement<TextBox>("TextBox");

//Act (set validation)
await textBox.MarkInvalid(TextBox.TextProperty, expectedErrorMessage);
var result1 = await textBox.GetProperty<bool>(System.Windows.Controls.Validation.HasErrorProperty);
var validationError1 = await textBox.GetValidationErrorContent<string>(TextBox.TextProperty);

//Act (clear validation)
await textBox.ClearInvalid(TextBox.TextProperty);
var result2 = await textBox.GetProperty<bool>(System.Windows.Controls.Validation.HasErrorProperty);
var validationError2 = await textBox.GetValidationErrorContent<string>(TextBox.TextProperty);

//Assert
Assert.AreEqual(true, result1);
Assert.AreEqual(expectedErrorMessage, validationError1);
Assert.AreEqual(false, result2);
Assert.IsNull(validationError2);

recorder.Success();
}

[TestMethod]
public async Task MarkInvalid_WhenPropertyIsDependencyObjectWithExistingBinding_SetsValidationError()
{
// Arrange
await using TestRecorder recorder = new(App);

const string expectedErrorMessage = "Custom validation error";
await Window.SetXamlContent(@"<TextBox x:Name=""TextBox"" Text=""{Binding RelativeSource={RelativeSource Self}, Path=Tag}"" />");
IVisualElement<TextBox> textBox = await Window.GetElement<TextBox>("TextBox");

//Act (set validation)
await textBox.MarkInvalid(TextBox.TextProperty, expectedErrorMessage);
var result1 = await textBox.GetProperty<bool>(System.Windows.Controls.Validation.HasErrorProperty);
var validationError1 = await textBox.GetValidationErrorContent<string>(TextBox.TextProperty);

//Act (clear validation)
await textBox.ClearInvalid(TextBox.TextProperty);
var result2 = await textBox.GetProperty<bool>(System.Windows.Controls.Validation.HasErrorProperty);
var validationError2 = await textBox.GetValidationErrorContent<string>(TextBox.TextProperty);

//Assert
Assert.AreEqual(true, result1);
Assert.AreEqual(expectedErrorMessage, validationError1);
Assert.AreEqual(false, result2);
Assert.IsNull(validationError2);

recorder.Success();
}
}
157 changes: 157 additions & 0 deletions XAMLTest/Host/VisualTreeService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Markup;
using System.Windows.Media;
using XamlTest.Internal;
Expand Down Expand Up @@ -275,6 +276,131 @@ await Application.Dispatcher.InvokeAsync(() =>
}
}

public override async Task<MarkInvalidResult> MarkInvalid(MarkInvalidRequest request, ServerCallContext context)
{
MarkInvalidResult reply = new();
await Application.Dispatcher.InvokeAsync(() =>
{
try
{
DependencyObject? element = GetCachedElement<DependencyObject>(request.ElementId);
if (element is null)
{
reply.ErrorMessages.Add("Could not find element");
return;
}

if (!string.IsNullOrWhiteSpace(request.OwnerType))
{
if (DependencyPropertyHelper.TryGetDependencyProperty(request.Name, request.OwnerType, out DependencyProperty? dependencyProperty))
{
BindingExpressionBase? bindingExpression = BindingOperations.GetBindingExpression(element, dependencyProperty);
if (bindingExpression == null)
{
var binding = new Binding
{
Path = new PropertyPath(ValidationErrorDummyProperty),
RelativeSource = new RelativeSource(RelativeSourceMode.Self)
};
bindingExpression = BindingOperations.SetBinding(element, dependencyProperty, binding);
}
var validationError = new ValidationError(new DummyValidationRule(request.ValidationError), bindingExpression)
{
ErrorContent = request.ValidationError
};
System.Windows.Controls.Validation.MarkInvalid(bindingExpression, validationError);
}
else
{
reply.ErrorMessages.Add($"Could not find dependency property '{request.Name}' on '{request.OwnerType}'");
}
}
}
catch (Exception e)
{
reply.ErrorMessages.Add(e.ToString());
}
});
return reply;
}

public override async Task<ClearInvalidResult> ClearInvalid(ClearInvalidRequest request, ServerCallContext context)
{
ClearInvalidResult reply = new();
await Application.Dispatcher.InvokeAsync(() =>
{
try
{
DependencyObject? element = GetCachedElement<DependencyObject>(request.ElementId);
if (element is null)
{
reply.ErrorMessages.Add("Could not find element");
return;
}

if (!string.IsNullOrWhiteSpace(request.OwnerType))
{
if (DependencyPropertyHelper.TryGetDependencyProperty(request.Name, request.OwnerType, out DependencyProperty? dependencyProperty))
{
var bindingExpression = BindingOperations.GetBindingExpression(element, dependencyProperty);
if (bindingExpression != null)
{
// Clear the invalidation
System.Windows.Controls.Validation.ClearInvalid(bindingExpression);
}
}
else
{
reply.ErrorMessages.Add($"Could not find dependency property '{request.Name}' on '{request.OwnerType}'");
}
}
}
catch (Exception e)
{
reply.ErrorMessages.Add(e.ToString());
}
});
return reply;
}

public override async Task<GetValidationErrorContentResult> GetValidationErrorContent(GetValidationErrorContentRequest request, ServerCallContext context)
{
GetValidationErrorContentResult reply = new();
await Application.Dispatcher.InvokeAsync(() =>
{
try
{
DependencyObject? element = GetCachedElement<DependencyObject>(request.ElementId);
if (element is null)
{
reply.ErrorMessages.Add("Could not find element");
return;
}

if (!string.IsNullOrWhiteSpace(request.OwnerType))
{
if (DependencyPropertyHelper.TryGetDependencyProperty(request.Name, request.OwnerType, out DependencyProperty? dependencyProperty))
{
var errors = System.Windows.Controls.Validation.GetErrors(element);
if (errors.Any() && errors[0].ErrorContent is string errorContent)
{
reply.Value = errorContent;
}
}
else
{
reply.ErrorMessages.Add($"Could not find dependency property '{request.Name}' on '{request.OwnerType}'");
}
}
}
catch (Exception e)
{
reply.ErrorMessages.Add(e.ToString());
}
});
return reply;
}

public override async Task<ElementResult> SetXamlProperty(SetXamlPropertyRequest request, ServerCallContext context)
{
ElementResult reply = new();
Expand Down Expand Up @@ -799,4 +925,35 @@ private static (double ScaleX, double ScaleY) GetScalingFromVisual(Visual visual
}
return (1.0, 1.0);
}


// TODO The stuff below probably needs to go elsewhere. A simple internal attached DP used to create a binding when one is not present, and a custom ValidationRule to apply the validation error.

internal static readonly DependencyProperty ValidationErrorDummyProperty = DependencyProperty.RegisterAttached(
"ValidationErrorDummy", typeof(object), typeof(VisualTreeService), new PropertyMetadata(default(object)));

internal static void SetValidationErrorDummy(DependencyObject element, object value)
{
element.SetValue(ValidationErrorDummyProperty, value);
}

internal static object GetValidationErrorDummy(DependencyObject element)
{
return (object)element.GetValue(ValidationErrorDummyProperty);
}

internal class DummyValidationRule : ValidationRule
{
private readonly string _validationError;

public DummyValidationRule(string validationError)
{
_validationError = validationError;
}

public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
return new ValidationResult(false, _validationError);
}
}
}
50 changes: 50 additions & 0 deletions XAMLTest/Host/XamlTestSpec.proto
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,16 @@ service Protocol {

//Sets a property
rpc SetProperty (SetPropertyRequest) returns (PropertyResult);

//Marks a property as having a validation error
rpc MarkInvalid (MarkInvalidRequest) returns (MarkInvalidResult);

//Clear a validation error from a property
rpc ClearInvalid (ClearInvalidRequest) returns (ClearInvalidResult);

//Marks a property as having a validation error
rpc GetValidationErrorContent (GetValidationErrorContentRequest) returns (GetValidationErrorContentResult);

//Set a property with XAML content
rpc SetXamlProperty (SetXamlPropertyRequest) returns (ElementResult);

Expand Down Expand Up @@ -111,6 +120,25 @@ message PropertyResult {
Element element = 5;
}

message MarkInvalidResult {
repeated string errorMessages = 1;
string propertyType = 2;
Element element = 3;
}

message GetValidationErrorContentResult {
repeated string errorMessages = 1;
string value = 2;
string valueType = 3;
string propertyType = 4;
}

message ClearInvalidResult {
repeated string errorMessages = 1;
string propertyType = 2;
Element element = 3;
}

message EffectiveBackgroundQuery {
string elementId = 1;
string toElementId = 2;
Expand All @@ -132,6 +160,28 @@ message SetPropertyRequest {
string ownerType = 5;
}

message MarkInvalidRequest {
string elementId = 1;
string name = 2;
string validationError = 3;
string valueType = 4;
string ownerType = 5;
}

message ClearInvalidRequest {
string elementId = 1;
string name = 2;
string valueType = 3;
string ownerType = 4;
}

message GetValidationErrorContentRequest {
string elementId = 1;
string name = 2;
string valueType = 3;
string ownerType = 4;
}

message XamlNamespace {
string prefix = 1;
string uri = 2;
Expand Down
28 changes: 28 additions & 0 deletions XAMLTest/IVisualElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,34 @@ public interface IVisualElement : IEquatable<IVisualElement>
/// <returns></returns>
Task<IValue> SetProperty(string name, string value, string? valueType, string? ownerType);

/// <summary>
/// Marks a property as invalid using an internal (XAMLTest specific) validation rule.
/// </summary>
/// <param name="name">The name of the property</param>
/// <param name="validationError">The validation error to set on the property binding</param>
/// <param name="valueType">The assembly qualified type of the value</param>
/// <param name="ownerType">The assembly qualified name of the type that owns the property</param>
/// <returns></returns>
Task<IValue> MarkInvalid(string name, string? validationError, string? valueType, string? ownerType);

/// <summary>
/// Clears validation errors for a property (if any).
/// </summary>
/// <param name="name">The name of the property</param>
/// <param name="valueType">The assembly qualified type of the value</param>
/// <param name="ownerType">The assembly qualified name of the type that owns the property</param>
/// <returns></returns>
Task<IValue> ClearInvalid(string name, string? valueType, string? ownerType);

/// <summary>
/// Gets the validation error content for a property (if any).
/// </summary>
/// <param name="name">The name of the property</param>
/// <param name="valueType">The assembly qualified type of the value</param>
/// <param name="ownerType">The assembly qualified name of the type that owns the property</param>
/// <returns></returns>
Task<IValue> GetValidationErrorContent(string name, string? valueType, string? ownerType);

/// <summary>
/// Sets the value of a property by loading XAML content.
/// </summary>
Expand Down
Loading