diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs index f16639625..8b395e858 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs @@ -802,6 +802,14 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) { string oldValueTypeNameAsNullable = GetOldValueTypeNameAsNullable(propertyInfo); + // These helpers reject null values, and empty 'string' values also marshal to a null 'HSTRING' + string? xamlBindingHelperFallbackCondition = propertyInfo.XamlBindingHelperSetMethodName switch + { + "SetPropertyFromString" => "value is null || value.Length == 0", + "SetPropertyFromUri" => "value is null", + _ => null + }; + // Declare the property writer.WriteLine(skipIfPresent: true); writer.WriteLine("/// "); @@ -845,19 +853,18 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) field = value; """, isMultiline: true); - // If the property is of type 'string', we need a special path. That is because 'XamlBindingHelper.SetPropertyFromString' - // doesn't work correctly for 'null' or empty strings, so we need to fall back to 'SetValue' in those cases. - if (propertyInfo.TypeName == "string") + // Fall back to 'SetValue' for values rejected by the selected helper + if (xamlBindingHelperFallbackCondition is not null) { writer.Write($$""" - if (value is null || value.Length == 0) + if ({{xamlBindingHelperFallbackCondition}}) { SetValue({{propertyInfo.PropertyName}}Property, value); } else { - global::{{WellKnownTypeNames.XamlBindingHelper(propertyInfo.UseWindowsUIXaml)}}.SetPropertyFromString(this, {{propertyInfo.PropertyName}}Property, value); + global::{{WellKnownTypeNames.XamlBindingHelper(propertyInfo.UseWindowsUIXaml)}}.{{propertyInfo.XamlBindingHelperSetMethodName}}(this, {{propertyInfo.PropertyName}}Property, value); } On{{propertyInfo.PropertyName}}Changed(value); @@ -942,21 +949,21 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) } """, isMultiline: true); - // For 'string' properties, we need a specialized path (see comment in the local caching branch above) - if (propertyInfo.TypeName == "string") + // Match the 'string' and 'Uri' fallback from the local caching branch + if (xamlBindingHelperFallbackCondition is not null) { writer.WriteLine($$""" {{GetExpressionWithTrailingSpace(propertyInfo.SetterAccessibility)}}set { On{{propertyInfo.PropertyName}}Set(ref value); - if (value is null || value.Length == 0) + if ({{xamlBindingHelperFallbackCondition}}) { SetValue({{propertyInfo.PropertyName}}Property, value); } else { - global::{{WellKnownTypeNames.XamlBindingHelper(propertyInfo.UseWindowsUIXaml)}}.SetPropertyFromString(this, {{propertyInfo.PropertyName}}Property, value); + global::{{WellKnownTypeNames.XamlBindingHelper(propertyInfo.UseWindowsUIXaml)}}.{{setMethodName}}(this, {{propertyInfo.PropertyName}}Property, value); } On{{propertyInfo.PropertyName}}Changed(value); diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs index c4b7250a5..3bfcb46d1 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs @@ -4125,6 +4125,12 @@ public partial string? Name [DataRow("string", "string", "object", "null", "", "SetPropertyFromString")] [DataRow("string", "string?", "object?", "null", "", "SetPropertyFromString")] + // The 'Uri' helper also requires a fallback for 'null' values + [DataRow("global::System.Uri", "global::System.Uri", "object", "null", "", "SetPropertyFromUri")] + [DataRow("global::System.Uri", "global::System.Uri?", "object?", "null", "", "SetPropertyFromUri")] + [DataRow("global::System.Uri", "global::System.Uri", "object", "null", "public partial class MyControl { partial void OnNameSet(ref global::System.Uri propertyValue) { propertyValue = null!; } }", "SetPropertyFromUri")] + [DataRow("global::System.Uri", "global::System.Uri?", "object?", "null", "public partial class MyControl { partial void OnNameSet(ref global::System.Uri? propertyValue) { propertyValue = null; } }", "SetPropertyFromUri")] + // Well known WinRT primitive types [DataRow("int", "int", "object", "null", "", "SetPropertyFromInt32")] [DataRow("byte", "byte", "object", "null", "", "SetPropertyFromByte")] @@ -4181,9 +4187,7 @@ public void SingleProperty_MultipleTypes_WithNoCaching_DefaultValueIsOptimized( string? typeDefinition = "", string? setMethodName = null) { - // Compute the setter body and partial method block based on whether the optimization is used. - // The 'string' type needs a special path, since 'XamlBindingHelper.SetPropertyFromString' doesn't - // handle 'null' or empty strings correctly, so we need to fall back to 'SetValue' in those cases. + // The 'string' and 'Uri' helpers need fallbacks for values they cannot accept string setterBody = setMethodName switch { "SetPropertyFromString" => """ @@ -4196,6 +4200,16 @@ public void SingleProperty_MultipleTypes_WithNoCaching_DefaultValueIsOptimized( global::Windows.UI.Xaml.Markup.XamlBindingHelper.SetPropertyFromString(this, NameProperty, value); } """, + "SetPropertyFromUri" => """ + if (value is null) + { + SetValue(NameProperty, value); + } + else + { + global::Windows.UI.Xaml.Markup.XamlBindingHelper.SetPropertyFromUri(this, NameProperty, value); + } + """, not null => $""" global::Windows.UI.Xaml.Markup.XamlBindingHelper.{setMethodName}(this, NameProperty, value); """, @@ -6023,26 +6037,36 @@ public partial T2? Number } [TestMethod] - public void SingleProperty_Int32_WithNoCaching_WithObjectSetCallback() + [DataRow("int", "int", "object")] + [DataRow("string", "string", "object")] + [DataRow("string", "string?", "object?")] + [DataRow("global::System.Uri", "global::System.Uri", "object")] + [DataRow("global::System.Uri", "global::System.Uri?", "object?")] + public void SingleProperty_MultipleTypes_WithNoCaching_WithObjectSetCallback( + string dependencyPropertyType, + string propertyType, + string boxedPropertyType) { - const string source = """ + string source = $$""" using CommunityToolkit.WinUI; using Windows.UI.Xaml; + #nullable enable + namespace MyNamespace; public partial class MyControl : DependencyObject { [GeneratedDependencyProperty] - public partial int Number { get; set; } + public partial {{propertyType}} Number { get; set; } - partial void OnNumberSet(ref object propertyValue) + partial void OnNumberSet(ref {{boxedPropertyType}} propertyValue) { } } """; - const string result = """ + string result = $$""" // #pragma warning disable #nullable enable @@ -6058,7 +6082,7 @@ partial class MyControl [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( name: "Number", - propertyType: typeof(int), + propertyType: typeof({{dependencyPropertyType}}), ownerType: typeof(MyControl), typeMetadata: null); @@ -6066,7 +6090,7 @@ partial class MyControl [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] [global::System.Diagnostics.DebuggerNonUserCode] [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public partial int Number + public partial {{propertyType}} Number { get { @@ -6074,7 +6098,7 @@ public partial int Number OnNumberGet(ref __boxedValue); - int __unboxedValue = (int)__boxedValue; + {{propertyType}} __unboxedValue = ({{propertyType}})__boxedValue; OnNumberGet(ref __unboxedValue); @@ -6098,31 +6122,31 @@ public partial int Number /// The raw property value that has been retrieved from . /// This method is invoked on the boxed value retrieved via on . [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] - partial void OnNumberGet(ref object propertyValue); + partial void OnNumberGet(ref {{boxedPropertyType}} propertyValue); /// Executes the logic for when the accessor is invoked /// The unboxed property value that has been retrieved from . /// This method is invoked on the unboxed value retrieved via on . [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] - partial void OnNumberGet(ref int propertyValue); + partial void OnNumberGet(ref {{propertyType}} propertyValue); /// Executes the logic for when the accessor is invoked /// The boxed property value that has been produced before assigning to . /// This method is invoked on the boxed value that is about to be passed to on . [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] - partial void OnNumberSet(ref object propertyValue); + partial void OnNumberSet(ref {{boxedPropertyType}} propertyValue); /// Executes the logic for when the accessor is invoked /// The property value that is being assigned to . /// This method is invoked on the raw value being assigned to , before is used. [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] - partial void OnNumberSet(ref int propertyValue); + partial void OnNumberSet(ref {{propertyType}} propertyValue); /// Executes the logic for when has just changed. /// The new property value that has been set. /// This method is invoked right after the value of is changed. [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] - partial void OnNumberChanged(int newValue); + partial void OnNumberChanged({{propertyType}} newValue); /// Executes the logic for when has just changed. /// Event data that is issued by any event that tracks changes to the effective value of this property. @@ -6141,4 +6165,290 @@ public partial int Number CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); } + + [TestMethod] + [DataRow("global::System.Uri", "object")] + [DataRow("global::System.Uri?", "object?")] + [DataRow("global::System.Uri", "object", true)] + [DataRow("global::System.Uri?", "object?", true)] + public void SingleProperty_Uri_WithLocalCache( + string propertyType, + string boxedPropertyType, + bool implementSetCallback = false) + { + string setCallback = implementSetCallback ? $$""" + partial void OnNameSet(ref {{propertyType}} propertyValue) + { + propertyValue = null!; + } + """ : ""; + + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty(IsLocalCacheEnabled = true)] + public partial {{propertyType}} Name { get; set; } + + {{setCallback}} + } + """; + + string result = $$""" + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Name", + propertyType: typeof(global::System.Uri), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial {{propertyType}} Name + { + get => field; + set + { + OnNameSet(ref value); + + if (global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + return; + } + + global::System.Uri? __oldValue = field; + + OnNameChanging(value); + OnNameChanging(__oldValue, value); + + field = value; + + if (value is null) + { + SetValue(NameProperty, value); + } + else + { + global::Windows.UI.Xaml.Markup.XamlBindingHelper.SetPropertyFromUri(this, NameProperty, value); + } + + OnNameChanged(value); + OnNameChanged(__oldValue, value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref {{boxedPropertyType}} propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref {{propertyType}} propertyValue); + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameChanging({{propertyType}} newValue); + + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameChanging(global::System.Uri? oldValue, {{propertyType}} newValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameChanged({{propertyType}} newValue); + + /// Executes the logic for when has just changed. + /// The previous property value that has been replaced. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameChanged(global::System.Uri? oldValue, {{propertyType}} newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + [DataRow("string", "string", "object")] + [DataRow("string", "string?", "object?")] + [DataRow("global::System.Uri", "global::System.Uri", "object")] + [DataRow("global::System.Uri", "global::System.Uri?", "object?")] + public void SingleProperty_ReferenceType_WithLocalCache_WithObjectSetCallback( + string dependencyPropertyType, + string propertyType, + string boxedPropertyType) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty(IsLocalCacheEnabled = true)] + public partial {{propertyType}} Name { get; set; } + + partial void OnNameSet(ref {{boxedPropertyType}} propertyValue) + { + propertyValue = null!; + } + } + """; + + string result = $$""" + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Name", + propertyType: typeof({{dependencyPropertyType}}), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial {{propertyType}} Name + { + get => field; + set + { + OnNameSet(ref value); + + if (global::System.Collections.Generic.EqualityComparer<{{dependencyPropertyType}}?>.Default.Equals(field, value)) + { + return; + } + + {{dependencyPropertyType}}? __oldValue = field; + + OnNameChanging(value); + OnNameChanging(__oldValue, value); + + field = value; + + object? __boxedValue = value; + OnNameSet(ref __boxedValue); + + SetValue(NameProperty, __boxedValue); + + OnNameChanged(value); + OnNameChanged(__oldValue, value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref {{boxedPropertyType}} propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref {{propertyType}} propertyValue); + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameChanging({{propertyType}} newValue); + + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameChanging({{dependencyPropertyType}}? oldValue, {{propertyType}} newValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameChanged({{propertyType}} newValue); + + /// Executes the logic for when has just changed. + /// The previous property value that has been replaced. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameChanged({{dependencyPropertyType}}? oldValue, {{propertyType}} newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } } diff --git a/components/DependencyPropertyGenerator/samples/DependencyPropertyGenerator.md b/components/DependencyPropertyGenerator/samples/DependencyPropertyGenerator.md index ddf1c94a1..0c1c694d7 100644 --- a/components/DependencyPropertyGenerator/samples/DependencyPropertyGenerator.md +++ b/components/DependencyPropertyGenerator/samples/DependencyPropertyGenerator.md @@ -76,7 +76,7 @@ Use the properties like any other dependency properties. For example, after mapp You can also use `CounterControl.CountProperty` with APIs such as `GetValue`, `SetValue`, and `ClearValue`. Do not declare the identifier field or another implementation of the partial property yourself. -Without local caching, the getter reads the effective value from the XAML property system. The setter writes to that system, using optimized `XamlBindingHelper` APIs where available and appropriate. The generator handles the details, including falling back to `SetValue` for null or empty strings. +Without local caching, the getter reads the effective value from the XAML property system. The setter writes to that system, using optimized `XamlBindingHelper` APIs where available and appropriate. The generator falls back to `SetValue` for `null` URI values and `null` or empty strings, in both caching modes. These checks use the value after the typed setter hook, even for properties not annotated as nullable. ## Attribute options @@ -263,7 +263,7 @@ public partial class NormalizedCounter : DependencyObject } ``` -For ordinary properties without local caching, implementing the boxed setter hook makes the generator use the `SetValue` path instead of a typed `XamlBindingHelper` optimization, so the hook can inspect or replace the boxed value. The boxed getter hook can normalize values before a cast that would otherwise fail. Replacements must remain compatible with the declared property and metadata types. +Implementing the boxed setter hook makes the generator use the `SetValue` path instead of a typed `XamlBindingHelper` optimization, so the hook can inspect or replace the boxed value. This applies with and without local caching. The boxed getter hook can normalize values before a cast that would otherwise fail. Replacements must remain compatible with the declared property and metadata types. An object-typed property has only one getter hook and one setter hook, rather than duplicate typed and boxed signatures. Nullable annotations on hook parameters follow the generated property's signatures.