diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs index 66e1c66f6916..dd1e8e8aef8e 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs @@ -210,20 +210,30 @@ private static Result GetLambdaReturnType(LambdaExpressionSyntax la var lambdaResultType = semanticModel.GetTypeInfo(lambdaBody, t).Type; if (lambdaResultType == null || lambdaResultType is IErrorTypeSymbol) { - // Try to infer the type from known patterns (e.g., RelayCommand properties) + // Try to infer the type from known patterns (e.g., RelayCommand, ObservableProperty) if (lambdaBody is MemberAccessExpressionSyntax memberAccess) { var memberName = memberAccess.Name.Identifier.Text; var expressionType = semanticModel.GetTypeInfo(memberAccess.Expression).Type; - - if (expressionType != null && - expressionType.TryGetRelayCommandPropertyType(memberName, semanticModel.Compilation, out var commandType) && - commandType != null) + + if (expressionType != null) { - return Result.Success(commandType); + // Check for RelayCommand-generated properties + if (expressionType.TryGetRelayCommandPropertyType(memberName, semanticModel.Compilation, out var commandType) && + commandType != null) + { + return Result.Success(commandType); + } + + // Check for ObservableProperty-generated properties + if (expressionType.TryGetObservablePropertyType(memberName, semanticModel.Compilation, out var propertyType) && + propertyType != null) + { + return Result.Success(propertyType); + } } } - + return Result.Failure(DiagnosticsFactory.LambdaResultCannotBeResolved(lambdaBody.GetLocation())); } diff --git a/src/Controls/src/BindingSourceGen/ITypeSymbolExtensions.cs b/src/Controls/src/BindingSourceGen/ITypeSymbolExtensions.cs index fcefb561e33c..43ea23d060b6 100644 --- a/src/Controls/src/BindingSourceGen/ITypeSymbolExtensions.cs +++ b/src/Controls/src/BindingSourceGen/ITypeSymbolExtensions.cs @@ -67,10 +67,10 @@ public static bool TryGetRelayCommandPropertyType(this ITypeSymbol symbol, strin // Extract the method name (property name without "Command" suffix) var methodName = propertyName.Substring(0, propertyName.Length - "Command".Length); - + // Look for a method with the base name - search in the type and base types var methods = GetAllMethods(symbol, methodName); - + foreach (var method in methods) { // Check if the method has the RelayCommand attribute @@ -93,6 +93,54 @@ public static bool TryGetRelayCommandPropertyType(this ITypeSymbol symbol, strin return false; } + /// + /// Checks if a property name could be generated by CommunityToolkit.Mvvm's [ObservableProperty] attribute, + /// and returns the inferred property type if found. + /// + /// The type to search + /// The name of the property to find + /// The compilation (can be null) + /// The inferred property type if an ObservableProperty field is found + /// True if an ObservableProperty field was found that would generate this property + public static bool TryGetObservablePropertyType(this ITypeSymbol symbol, string propertyName, Compilation? compilation, out ITypeSymbol? propertyType) + { + propertyType = null; + + if (compilation == null || string.IsNullOrEmpty(propertyName)) + return false; + + // ObservableProperty generates a PascalCase property from a camelCase, _camelCase, or m_camelCase field + // Try common field naming patterns + var possibleFieldNames = new[] + { + char.ToLowerInvariant(propertyName[0]) + propertyName.Substring(1), // name from Name + "_" + char.ToLowerInvariant(propertyName[0]) + propertyName.Substring(1), // _name from Name + "m_" + char.ToLowerInvariant(propertyName[0]) + propertyName.Substring(1) // m_name from Name + }; + + // Look for a field with one of the possible names - search in the type and base types + foreach (var fieldName in possibleFieldNames) + { + var fields = GetAllFields(symbol, fieldName); + + foreach (var field in fields) + { + // Check if the field has the ObservableProperty attribute + var hasObservableProperty = field.GetAttributes().Any(attr => + attr.AttributeClass?.Name == "ObservablePropertyAttribute" || + attr.AttributeClass?.ToDisplayString() == "CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute"); + + if (hasObservableProperty) + { + propertyType = field.Type; + return true; + } + } + } + + return false; + } + private static System.Collections.Generic.IEnumerable GetAllMethods(ITypeSymbol symbol, string name) { // Search in current type @@ -114,4 +162,17 @@ private static System.Collections.Generic.IEnumerable GetAllMetho baseType = baseType.BaseType; } } + + private static System.Collections.Generic.IEnumerable GetAllFields(ITypeSymbol? symbol, string name) + { + while (symbol != null) + { + foreach (var member in symbol.GetMembers(name)) + { + if (member is IFieldSymbol field) + yield return field; + } + symbol = symbol.BaseType; + } + } } diff --git a/src/Controls/src/BindingSourceGen/PathParser.cs b/src/Controls/src/BindingSourceGen/PathParser.cs index ce5c225aeda6..11a77e79c7c4 100644 --- a/src/Controls/src/BindingSourceGen/PathParser.cs +++ b/src/Controls/src/BindingSourceGen/PathParser.cs @@ -87,7 +87,7 @@ private bool TryHandleSpecialCases(string memberName, ITypeSymbol expressionType pathPart = null; // Check for RelayCommand-generated properties - if (expressionType.TryGetRelayCommandPropertyType(memberName, _context.SemanticModel.Compilation, out var commandType) + if (expressionType.TryGetRelayCommandPropertyType(memberName, _context.SemanticModel.Compilation, out var commandType) && commandType != null) { var memberType = commandType.CreateTypeDescription(_enabledNullable); @@ -105,6 +105,25 @@ private bool TryHandleSpecialCases(string memberName, ITypeSymbol expressionType return true; } + // Check for ObservableProperty-generated properties + if (expressionType.TryGetObservablePropertyType(memberName, _context.SemanticModel.Compilation, out var propertyType) + && propertyType != null) + { + var memberType = propertyType.CreateTypeDescription(_enabledNullable); + var containingType = expressionType.CreateTypeDescription(_enabledNullable); + + pathPart = new MemberAccess( + MemberName: memberName, + IsValueType: !propertyType.IsReferenceType, + ContainingType: containingType, + MemberType: memberType, + Kind: AccessorKind.Property, + IsGetterInaccessible: false, // Assume generated property is accessible + IsSetterInaccessible: false); // ObservableProperty properties have setters + + return true; + } + return false; } diff --git a/src/Controls/src/SourceGen/ITypeSymbolExtensions.cs b/src/Controls/src/SourceGen/ITypeSymbolExtensions.cs index 72c075f4b5c0..6067880c34ad 100644 --- a/src/Controls/src/SourceGen/ITypeSymbolExtensions.cs +++ b/src/Controls/src/SourceGen/ITypeSymbolExtensions.cs @@ -2,10 +2,9 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; - using Microsoft.CodeAnalysis; -using Microsoft.Maui.Controls.Xaml; using Microsoft.Maui.Controls.BindingSourceGen; +using Microsoft.Maui.Controls.Xaml; namespace Microsoft.Maui.Controls.SourceGen; @@ -85,15 +84,15 @@ public static IEnumerable GetAllProperties(this ITypeSymbol sym => symbol.GetAllMembers(name, context).OfType(); /// - /// Tries to get a property by name, and if not found, checks if it could be inferred from a RelayCommand method. + /// Tries to get a property by name, and if not found, checks if it could be inferred from a RelayCommand method or ObservableProperty field. /// Returns the property type if found or inferred. /// /// The type to search /// The name of the property to find /// The source generation context - /// The found property symbol (null if inferred from RelayCommand) - /// The property type (either from the property or inferred from RelayCommand) - /// True if property exists or can be inferred from RelayCommand + /// The found property symbol (null if inferred from RelayCommand or ObservableProperty) + /// The property type (either from the property or inferred from RelayCommand/ObservableProperty) + /// True if property exists or can be inferred from RelayCommand or ObservableProperty public static bool TryGetProperty( this ITypeSymbol symbol, string propertyName, @@ -103,7 +102,7 @@ public static bool TryGetProperty( { property = symbol.GetAllProperties(propertyName, context) .FirstOrDefault(p => p.GetMethod != null && !p.GetMethod.IsStatic); - + if (property != null) { propertyType = property.Type; @@ -117,6 +116,13 @@ public static bool TryGetProperty( return true; } + // If property not found, check if it could be an ObservableProperty-generated property + // Call the BindingSourceGen extension method directly + if (symbol.TryGetObservablePropertyType(propertyName, context?.Compilation, out propertyType)) + { + return true; + } + propertyType = null; return false; } diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/ObservablePropertyTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/ObservablePropertyTests.cs new file mode 100644 index 000000000000..2acc8570621e --- /dev/null +++ b/src/Controls/tests/BindingSourceGen.UnitTests/ObservablePropertyTests.cs @@ -0,0 +1,339 @@ +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.Maui.Controls.BindingSourceGen; +using Xunit; + +namespace BindingSourceGen.UnitTests; + +public class ObservablePropertyTests +{ + // Note: Expression-based bindings work with ObservableProperty through type inference in + // GetLambdaReturnType. The generated interceptor code will be correct. In tests, + // we'll see compilation errors because the actual generated property doesn't exist + // (CommunityToolkit.Mvvm's source generator isn't running in the test environment), + // but the interceptor code itself is generated correctly. + + [Fact] + public void GenerateBindingToObservablePropertyFromCamelCaseField() + { + var source = """ + using Microsoft.Maui.Controls; + using System.Collections.ObjectModel; + + namespace CommunityToolkit.Mvvm.ComponentModel + { + [System.AttributeUsage(System.AttributeTargets.Field)] + public class ObservablePropertyAttribute : System.Attribute { } + } + + namespace TestApp + { + public class MyViewModel + { + [CommunityToolkit.Mvvm.ComponentModel.ObservableProperty] + private string? name; + // Name property will be generated by CommunityToolkit.Mvvm + } + + public class TestCode + { + public void Test() + { + var label = new Label(); + label.SetBinding(Label.TextProperty, static (MyViewModel vm) => vm.Name); + } + } + } + """; + + var result = SourceGenHelpers.Run(source); + + // The binding should be generated successfully with ObservableProperty inference + Assert.NotNull(result.Binding); + + // Verify the generated interceptor code contains the correct getter and setter references + var allGeneratedCode = string.Join("\n\n", result.GeneratedFiles.Values); + // Check that the handler contains the property access + Assert.Contains("new(static source => source, \"Name\")", allGeneratedCode, System.StringComparison.Ordinal); + // Check that setter assigns to .Name + Assert.Contains("source.Name = value;", allGeneratedCode, System.StringComparison.Ordinal); + + // Note: There will be compilation errors because Name doesn't actually exist, + // but the interceptor code itself is generated correctly. In real usage with + // CommunityToolkit.Mvvm, the property would exist and compile successfully. + } + + [Fact] + public void GenerateBindingToObservablePropertyFromUnderscorePrefixedField() + { + var source = """ + using Microsoft.Maui.Controls; + using System.Collections.ObjectModel; + + namespace CommunityToolkit.Mvvm.ComponentModel + { + [System.AttributeUsage(System.AttributeTargets.Field)] + public class ObservablePropertyAttribute : System.Attribute { } + } + + namespace TestApp + { + public class MyViewModel + { + [CommunityToolkit.Mvvm.ComponentModel.ObservableProperty] + private string? _title; + // Title property will be generated by CommunityToolkit.Mvvm + } + + public class TestCode + { + public void Test() + { + var label = new Label(); + label.SetBinding(Label.TextProperty, static (MyViewModel vm) => vm.Title); + } + } + } + """; + + var result = SourceGenHelpers.Run(source); + + // The binding should be generated successfully with ObservableProperty inference + Assert.NotNull(result.Binding); + + // Verify the generated interceptor code contains the correct getter and setter references + var allGeneratedCode = string.Join("\n\n", result.GeneratedFiles.Values); + // Check that the handler contains the property access + Assert.Contains("new(static source => source, \"Title\")", allGeneratedCode, System.StringComparison.Ordinal); + // Check that setter assigns to .Title + Assert.Contains("source.Title = value;", allGeneratedCode, System.StringComparison.Ordinal); + } + + [Fact] + public void GenerateBindingToObservablePropertyCollection() + { + var source = """ + using Microsoft.Maui.Controls; + using System.Collections.ObjectModel; + + namespace CommunityToolkit.Mvvm.ComponentModel + { + [System.AttributeUsage(System.AttributeTargets.Field)] + public class ObservablePropertyAttribute : System.Attribute { } + } + + namespace TestApp + { + public class Tag { } + + public class MyViewModel + { + [CommunityToolkit.Mvvm.ComponentModel.ObservableProperty] + private ObservableCollection _tags = new(); + // Tags property will be generated by CommunityToolkit.Mvvm + } + + public class TestCode + { + public void Test() + { + var label = new Label(); + label.SetBinding(Label.BindingContextProperty, static (MyViewModel vm) => vm.Tags); + } + } + } + """; + + var result = SourceGenHelpers.Run(source); + + // The binding should be generated successfully with ObservableProperty inference + Assert.NotNull(result.Binding); + + // Verify the generated interceptor code contains the correct getter and setter references + var allGeneratedCode = string.Join("\n\n", result.GeneratedFiles.Values); + // Check that the handler contains the property access + Assert.Contains("new(static source => source, \"Tags\")", allGeneratedCode, System.StringComparison.Ordinal); + // Check that setter assigns to .Tags + Assert.Contains("source.Tags = value;", allGeneratedCode, System.StringComparison.Ordinal); + } + + [Fact] + public void DetectsObservablePropertyFromCamelCaseField() + { + // This test verifies that TryGetObservablePropertyType can detect ObservableProperty fields + var source = """ + using System.Collections.ObjectModel; + + namespace CommunityToolkit.Mvvm.ComponentModel + { + [System.AttributeUsage(System.AttributeTargets.Field)] + public class ObservablePropertyAttribute : System.Attribute { } + } + + namespace TestApp + { + public class MyViewModel + { + [CommunityToolkit.Mvvm.ComponentModel.ObservableProperty] + private string? name; + } + } + """; + + // Create a compilation to test the extension method directly + var compilation = Microsoft.CodeAnalysis.CSharp.CSharpCompilation.Create("test") + .AddSyntaxTrees(CSharpSyntaxTree.ParseText(source)) + .AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)); + + var myViewModelType = compilation.GetTypeByMetadataName("TestApp.MyViewModel"); + Assert.NotNull(myViewModelType); + + // Test that TryGetObservablePropertyType can detect the Name property from name field + var canInfer = myViewModelType.TryGetObservablePropertyType("Name", compilation, out var propertyType); + + Assert.True(canInfer, "Should be able to infer Name from name field with [ObservableProperty]"); + Assert.NotNull(propertyType); + Assert.Equal("string?", propertyType!.ToDisplayString()); + } + + [Fact] + public void DetectsObservablePropertyFromUnderscorePrefixedField() + { + var source = """ + using System.Collections.ObjectModel; + + namespace CommunityToolkit.Mvvm.ComponentModel + { + [System.AttributeUsage(System.AttributeTargets.Field)] + public class ObservablePropertyAttribute : System.Attribute { } + } + + namespace TestApp + { + public class MyViewModel + { + [CommunityToolkit.Mvvm.ComponentModel.ObservableProperty] + private int _count; + } + } + """; + + var compilation = Microsoft.CodeAnalysis.CSharp.CSharpCompilation.Create("test") + .AddSyntaxTrees(CSharpSyntaxTree.ParseText(source)) + .AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)); + + var myViewModelType = compilation.GetTypeByMetadataName("TestApp.MyViewModel"); + Assert.NotNull(myViewModelType); + + // Test that TryGetObservablePropertyType can detect the Count property from _count field + var canInfer = myViewModelType.TryGetObservablePropertyType("Count", compilation, out var propertyType); + + Assert.True(canInfer, "Should be able to infer Count from _count field with [ObservableProperty]"); + Assert.NotNull(propertyType); + Assert.Equal("int", propertyType!.ToDisplayString()); + } + + [Fact] + public void DoesNotDetectPropertyWithoutAttribute() + { + var source = """ + namespace TestApp + { + public class MyViewModel + { + // No [ObservableProperty] attribute + private string? name; + } + } + """; + + var compilation = Microsoft.CodeAnalysis.CSharp.CSharpCompilation.Create("test") + .AddSyntaxTrees(CSharpSyntaxTree.ParseText(source)) + .AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)); + + var myViewModelType = compilation.GetTypeByMetadataName("TestApp.MyViewModel"); + Assert.NotNull(myViewModelType); + + // Should not infer Name without [ObservableProperty] attribute + var canInfer = myViewModelType.TryGetObservablePropertyType("Name", compilation, out var propertyType); + + Assert.False(canInfer, "Should not infer Name without [ObservableProperty] attribute"); + Assert.Null(propertyType); + } + + [Fact] + public void DetectsObservablePropertyWithShortAttributeName() + { + // Test that we can detect the attribute even with short name (without "Attribute" suffix) + var source = """ + using System.Collections.ObjectModel; + + namespace CommunityToolkit.Mvvm.ComponentModel + { + [System.AttributeUsage(System.AttributeTargets.Field)] + public class ObservablePropertyAttribute : System.Attribute { } + } + + namespace TestApp + { + public class MyViewModel + { + [CommunityToolkit.Mvvm.ComponentModel.ObservableProperty] + private bool _isActive; + } + } + """; + + var compilation = Microsoft.CodeAnalysis.CSharp.CSharpCompilation.Create("test") + .AddSyntaxTrees(CSharpSyntaxTree.ParseText(source)) + .AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)); + + var myViewModelType = compilation.GetTypeByMetadataName("TestApp.MyViewModel"); + Assert.NotNull(myViewModelType); + + var canInfer = myViewModelType.TryGetObservablePropertyType("IsActive", compilation, out var propertyType); + + Assert.True(canInfer, "Should be able to infer IsActive from _isActive field"); + Assert.NotNull(propertyType); + Assert.Equal("bool", propertyType!.ToDisplayString()); + } + + [Fact] + public void DetectsObservablePropertyFromMUnderscorePrefixedField() + { + // Test that we can detect fields with m_ prefix + var source = """ + using System.Collections.ObjectModel; + + namespace CommunityToolkit.Mvvm.ComponentModel + { + [System.AttributeUsage(System.AttributeTargets.Field)] + public class ObservablePropertyAttribute : System.Attribute { } + } + + namespace TestApp + { + public class MyViewModel + { + [CommunityToolkit.Mvvm.ComponentModel.ObservableProperty] + private int m_count; + } + } + """; + + var compilation = Microsoft.CodeAnalysis.CSharp.CSharpCompilation.Create("test") + .AddSyntaxTrees(CSharpSyntaxTree.ParseText(source)) + .AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)); + + var myViewModelType = compilation.GetTypeByMetadataName("TestApp.MyViewModel"); + Assert.NotNull(myViewModelType); + + // Test that TryGetObservablePropertyType can detect the Count property from m_count field + var canInfer = myViewModelType.TryGetObservablePropertyType("Count", compilation, out var propertyType); + + Assert.True(canInfer, "Should be able to infer Count from m_count field with [ObservableProperty]"); + Assert.NotNull(propertyType); + Assert.Equal("int", propertyType!.ToDisplayString()); + } +} diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Maui13872.xaml.cs b/src/Controls/tests/Xaml.UnitTests/Issues/Maui13872.xaml.cs index 335b1a5d1f88..4774cccf1a62 100644 --- a/src/Controls/tests/Xaml.UnitTests/Issues/Maui13872.xaml.cs +++ b/src/Controls/tests/Xaml.UnitTests/Issues/Maui13872.xaml.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using NUnit.Framework; +using Xunit; namespace Microsoft.Maui.Controls.Xaml.UnitTests; @@ -7,23 +7,23 @@ public partial class Maui13872 : ContentPage { public Maui13872() => InitializeComponent(); - [TestFixture] - class Tests + public class Tests { - [Test] - public void CompiledBindingToIReadOnlyListCount([Values] XamlInflator inflator) + [Theory] + [XamlInflatorData] + internal void CompiledBindingToIReadOnlyListCount(XamlInflator inflator) { var page = new Maui13872(inflator); page.BindingContext = new Maui13872ViewModel(); // Uncompiled bindings (no x:DataType) - should work with all inflators - Assert.That(page.label0.Text, Is.EqualTo("3"), "Uncompiled binding to List.Count"); - Assert.That(page.label1.Text, Is.EqualTo("3"), "Uncompiled binding to ListCount"); + Assert.Equal("3", page.label0.Text); // Uncompiled binding to List.Count + Assert.Equal("3", page.label1.Text); // Uncompiled binding to ListCount // Compiled bindings (with x:DataType) - IReadOnlyList.Count should resolve correctly. // Count is defined on IReadOnlyCollection which IReadOnlyList inherits. - Assert.That(page.label2.Text, Is.EqualTo("3"), "Compiled binding to List.Count"); - Assert.That(page.label3.Text, Is.EqualTo("3"), "Compiled binding to ListCount"); + Assert.Equal("3", page.label2.Text); // Compiled binding to List.Count + Assert.Equal("3", page.label3.Text); // Compiled binding to ListCount } } } diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Maui31939.xaml.cs b/src/Controls/tests/Xaml.UnitTests/Issues/Maui31939.xaml.cs index 6a1400e86d4e..6f81e95d4bea 100644 --- a/src/Controls/tests/Xaml.UnitTests/Issues/Maui31939.xaml.cs +++ b/src/Controls/tests/Xaml.UnitTests/Issues/Maui31939.xaml.cs @@ -6,7 +6,7 @@ using Microsoft.Maui.Controls.Core.UnitTests; using Microsoft.Maui.Dispatching; using Microsoft.Maui.UnitTests; -using NUnit.Framework; +using Xunit; namespace Microsoft.Maui.Controls.Xaml.UnitTests; @@ -17,24 +17,22 @@ public partial class Maui31939 : ContentPage { public Maui31939() => InitializeComponent(); - [TestFixture] - class Tests + public class Tests : IDisposable { - [SetUp] - public void Setup() + public Tests() { Application.SetCurrentApplication(new MockApplication()); DispatcherProvider.SetCurrent(new DispatcherProviderStub()); } - [TearDown] - public void TearDown() + public void Dispose() { DispatcherProvider.SetCurrent(null); } - [Test] - public void CommandParameterTemplateBindingShouldNotBeNullWhenCanExecuteIsCalled([Values] XamlInflator inflator) + [Theory] + [XamlInflatorData] + internal void CommandParameterTemplateBindingShouldNotBeNullWhenCanExecuteIsCalled(XamlInflator inflator) { // Verify initial template binding works correctly: CommandParameter should be resolved // before CanExecute is called when template is first applied. @@ -42,17 +40,18 @@ public void CommandParameterTemplateBindingShouldNotBeNullWhenCanExecuteIsCalled var page = new Maui31939(inflator); page.BindingContext = viewModel; - Assert.That(viewModel.CanExecuteCalledWithNullParameter, Is.False, + Assert.False(viewModel.CanExecuteCalledWithNullParameter, "CanExecute was called with null parameter during template binding application"); var button = (Button)page.TestControl.GetTemplateChild("TestButton"); - Assert.That(button, Is.Not.Null); - Assert.That(button.CommandParameter, Is.EqualTo("TestValue")); - Assert.That(button.Command, Is.Not.Null); + Assert.NotNull(button); + Assert.Equal("TestValue", button.CommandParameter); + Assert.NotNull(button.Command); } - [Test] - public void CommandParameterTemplateBindingWorksAfterReparenting([Values] XamlInflator inflator) + [Theory] + [XamlInflatorData] + internal void CommandParameterTemplateBindingWorksAfterReparenting(XamlInflator inflator) { // Regression test: when elements are reparented within a ControlTemplate, bindings // are re-applied. Due to the async void ApplyRelativeSourceBinding path, Command may @@ -64,8 +63,8 @@ public void CommandParameterTemplateBindingWorksAfterReparenting([Values] XamlIn var grid = (Grid)page.TestControl.GetTemplateChild("MainLayout"); var button = (Button)page.TestControl.GetTemplateChild("TestButton"); - Assert.That(button, Is.Not.Null); - Assert.That(button.CommandParameter, Is.EqualTo("TestValue")); + Assert.NotNull(button); + Assert.Equal("TestValue", button.CommandParameter); // Simulate reparenting operation (like the issue describes) viewModel.ResetCanExecuteTracking(); @@ -74,9 +73,9 @@ public void CommandParameterTemplateBindingWorksAfterReparenting([Values] XamlIn // After reparenting, CommandParameter should still be bound correctly // and CanExecute should not have been called with null - Assert.That(viewModel.CanExecuteCalledWithNullParameter, Is.False, + Assert.False(viewModel.CanExecuteCalledWithNullParameter, "CanExecute was called with null parameter after reparenting"); - Assert.That(button.CommandParameter, Is.EqualTo("TestValue")); + Assert.Equal("TestValue", button.CommandParameter); } } }