From 511a88b7906e0ef44b5e9f49dbec0763d6acc1ee Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 21 Sep 2026 17:56:16 -0700 Subject: [PATCH 1/2] Fix null URI dependency property setters Guard optimized URI setters after typed hooks in both caching modes, preserve the string null/empty fallback and boxed set hooks, and audit all generated XAML helper paths with regression coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../DependencyPropertyGenerator.Execute.cs | 25 +- .../CSharpGeneratorTest{TGenerator}.cs | 34 +- ...yGeneratorAnalyzerConfigOptionsProvider.cs | 5 +- ...DependencyPropertyGenerator.Compilation.cs | 2 + ...encyPropertyGenerator.XamlBindingHelper.cs | 504 ++++++++++++++++++ .../samples/DependencyPropertyGenerator.md | 4 +- 6 files changed, 558 insertions(+), 16 deletions(-) create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.XamlBindingHelper.cs 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/Helpers/CSharpGeneratorTest{TGenerator}.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs index 7ab80d1b8..b15ead5f0 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs @@ -11,6 +11,7 @@ using CommunityToolkit.WinUI; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Emit; using Microsoft.VisualStudio.TestTools.UnitTesting; using Windows.Foundation; @@ -56,7 +57,7 @@ public static void VerifyDiagnostics(string source, params string[] diagnosticsI /// The input source to process. /// The expected source to be generated. /// The language version to use to run the test. - /// This also supports invalid inputs. Use to validate compilation. + /// This also supports invalid inputs. Use to validate compilation. public static void VerifySources(string source, (string Filename, string Source) result, LanguageVersion languageVersion = LanguageVersion.CSharp13) { RunGenerator(source, out Compilation compilation, out ImmutableArray diagnostics, languageVersion); @@ -80,7 +81,18 @@ public static void VerifySources(string source, (string Filename, string Source) /// The resulting compilation, for further assertions on the generated sources. public static Compilation VerifyCompiles(string source, LanguageVersion languageVersion = LanguageVersion.CSharp13) { - RunGenerator(source, out Compilation compilation, out ImmutableArray diagnostics, languageVersion); + return VerifyCompiles(CreateCompilation(source, languageVersion), DependencyPropertyGeneratorAnalyzerConfigOptionsProvider.Instance); + } + + /// + /// Verifies that the resulting sources can be emitted with the specified references and generator options. + /// + /// The input compilation to process. + /// The analyzer options to use for the generator. + /// The resulting compilation, for further assertions on the generated sources. + public static Compilation VerifyCompiles(Compilation originalCompilation, AnalyzerConfigOptionsProvider optionsProvider) + { + RunGenerator(originalCompilation, optionsProvider, out Compilation compilation, out ImmutableArray diagnostics); CollectionAssert.AreEquivalent(Array.Empty(), diagnostics); @@ -189,10 +201,26 @@ private static void RunGenerator( { Compilation originalCompilation = CreateCompilation(source, languageVersion); + RunGenerator(originalCompilation, DependencyPropertyGeneratorAnalyzerConfigOptionsProvider.Instance, out compilation, out diagnostics); + } + + /// + /// Runs a generator with the specified compilation and options. + /// + /// The input compilation to process. + /// The analyzer options to use for the generator. + /// The resulting compilation. + /// The resulting generator diagnostics. + private static void RunGenerator( + Compilation originalCompilation, + AnalyzerConfigOptionsProvider optionsProvider, + out Compilation compilation, + out ImmutableArray diagnostics) + { // Create the generator driver with the specified generator GeneratorDriver driver = CSharpGeneratorDriver.Create( generators: [new TGenerator().AsSourceGenerator()], - optionsProvider: DependencyPropertyGeneratorAnalyzerConfigOptionsProvider.Instance).WithUpdatedParseOptions(originalCompilation.SyntaxTrees.First().Options); + optionsProvider: optionsProvider).WithUpdatedParseOptions(originalCompilation.SyntaxTrees.First().Options); // Run all source generators on the input source code _ = driver.RunGeneratorsAndUpdateCompilation(originalCompilation, out compilation, out diagnostics); diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/DependencyPropertyGeneratorAnalyzerConfigOptionsProvider.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/DependencyPropertyGeneratorAnalyzerConfigOptionsProvider.cs index bf0f7458b..1e98ed21c 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/DependencyPropertyGeneratorAnalyzerConfigOptionsProvider.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/DependencyPropertyGeneratorAnalyzerConfigOptionsProvider.cs @@ -12,7 +12,8 @@ namespace CommunityToolkit.GeneratedDependencyProperty.Tests.Helpers; /// /// A custom providing the MSBuild properties needed by the dependency property generator. /// -internal sealed class DependencyPropertyGeneratorAnalyzerConfigOptionsProvider : AnalyzerConfigOptionsProvider +/// Whether to use the UWP XAML or WinUI 3 XAML namespaces. +internal sealed class DependencyPropertyGeneratorAnalyzerConfigOptionsProvider(bool useWindowsUIXaml = true) : AnalyzerConfigOptionsProvider { /// /// The singleton instance. @@ -21,7 +22,7 @@ internal sealed class DependencyPropertyGeneratorAnalyzerConfigOptionsProvider : /// public override AnalyzerConfigOptions GlobalOptions { get; } = new SimpleAnalyzerConfigOptions( - ImmutableDictionary.Empty.Add("build_property.DependencyPropertyGeneratorUseWindowsUIXaml", "true")); + ImmutableDictionary.Empty.Add("build_property.DependencyPropertyGeneratorUseWindowsUIXaml", useWindowsUIXaml ? "true" : "false")); /// public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.Compilation.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.Compilation.cs index b81845868..5d570939a 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.Compilation.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.Compilation.cs @@ -24,6 +24,8 @@ partial class Test_DependencyPropertyGenerator [DataRow("int?", null)] [DataRow("bool", null)] [DataRow("string", null)] + [DataRow("System.Uri", null)] + [DataRow("System.Uri?", null)] public void SingleProperty_WithLocalCache_Compiles(string propertyType, string? defaultValue) { string defaultValueArgument = defaultValue is null ? "" : $", DefaultValue = {defaultValue}"; diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.XamlBindingHelper.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.XamlBindingHelper.cs new file mode 100644 index 000000000..6658fc6f1 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.XamlBindingHelper.cs @@ -0,0 +1,504 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; +using Basic.Reference.Assemblies; +using CommunityToolkit.GeneratedDependencyProperty.Tests.Helpers; +using CommunityToolkit.WinUI; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace CommunityToolkit.GeneratedDependencyProperty.Tests; + +partial class Test_DependencyPropertyGenerator +{ + [TestMethod] + [DynamicData(nameof(GetReferenceSetterData), DynamicDataSourceType.Method)] + public void XamlBindingHelper_ReferenceValues( + string propertyType, + bool isLocalCacheEnabled, + bool useWindowsUIXaml) + { + Compilation compilation = CreateXamlBindingHelperCompilation( + CreateReferenceSetterSource(propertyType, isLocalCacheEnabled, useWindowsUIXaml), + useWindowsUIXaml); + + RunGeneratedSetter(compilation, type => + { + object control = Activator.CreateInstance(type)!; + PropertyInfo property = type.GetProperty("Value")!; + List events = (List)type.GetProperty("Events")!.GetValue(control)!; + object value = propertyType.StartsWith("System.Uri", StringComparison.Ordinal) + ? new Uri("https://example.com/first") + : "First"; + string setMethod = propertyType.TrimEnd('?') switch + { + "System.Uri" => "SetPropertyFromUri", + "string" => "SetPropertyFromString", + _ => "SetValue" + }; + + property.SetValue(control, null); + + CollectionAssert.AreEqual( + isLocalCacheEnabled ? new[] { "Set" } : new[] { "Set", "SetValue", "Changed" }, + events); + Assert.IsNull(property.GetValue(control)); + + events.Clear(); + property.SetValue(control, value); + + Assert.AreEqual(value, property.GetValue(control)); + Assert.AreEqual(value, type.GetProperty("LocalValue")!.GetValue(control)); + CollectionAssert.AreEqual(GetSetterEvents(isLocalCacheEnabled, setMethod), events); + + events.Clear(); + property.SetValue(control, null); + + Assert.IsNull(property.GetValue(control)); + Assert.IsNull(type.GetProperty("LocalValue")!.GetValue(control)); + Assert.AreEqual(value, type.GetProperty("OldValue")!.GetValue(control)); + Assert.IsNull(type.GetProperty("NewValue")!.GetValue(control)); + CollectionAssert.AreEqual(GetSetterEvents(isLocalCacheEnabled, "SetValue"), events); + + property.SetValue(control, value); + type.GetProperty("SetToNull")!.SetValue(control, true); + events.Clear(); + property.SetValue(control, value); + + Assert.IsNull(property.GetValue(control)); + Assert.IsNull(type.GetProperty("LocalValue")!.GetValue(control)); + CollectionAssert.AreEqual(GetSetterEvents(isLocalCacheEnabled, "SetValue"), events); + + if (propertyType.StartsWith("string", StringComparison.Ordinal)) + { + type.GetProperty("SetToNull")!.SetValue(control, false); + events.Clear(); + property.SetValue(control, ""); + + Assert.AreEqual("", property.GetValue(control)); + Assert.AreEqual("", type.GetProperty("LocalValue")!.GetValue(control)); + CollectionAssert.AreEqual(GetSetterEvents(isLocalCacheEnabled, "SetValue"), events); + } + }); + } + + [TestMethod] + [DynamicData(nameof(GetBoxedSetHookData), DynamicDataSourceType.Method)] + public void XamlBindingHelper_BoxedSetHook( + string propertyType, + bool isLocalCacheEnabled, + bool useWindowsUIXaml) + { + string source = CreateReferenceSetterSource(propertyType, isLocalCacheEnabled, useWindowsUIXaml, hasBoxedSetHook: true); + Compilation compilation = CreateXamlBindingHelperCompilation(source, useWindowsUIXaml); + + Assert.IsFalse(GetGeneratedSetter(compilation).Contains("XamlBindingHelper", StringComparison.Ordinal)); + + RunGeneratedSetter(compilation, type => + { + object control = Activator.CreateInstance(type)!; + PropertyInfo property = type.GetProperty("Value")!; + List events = (List)type.GetProperty("Events")!.GetValue(control)!; + object value = propertyType.StartsWith("System.Uri", StringComparison.Ordinal) + ? new Uri("https://example.com/first") + : "First"; + + property.SetValue(control, value); + + Assert.IsNull(type.GetProperty("LocalValue")!.GetValue(control)); + CollectionAssert.AreEqual( + isLocalCacheEnabled + ? new[] { "Set", "Changing", "ChangingWithOldValue", "BoxedSet", "SetValue", "Changed", "ChangedWithOldValue" } + : new[] { "Set", "BoxedSet", "SetValue", "Changed" }, + events); + }); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(false, true)] + [DataRow(true, false)] + [DataRow(true, true)] + public void XamlBindingHelper_Uri_Setter(bool isLocalCacheEnabled, bool useWindowsUIXaml) + { + string xamlNamespace = useWindowsUIXaml ? "Windows.UI.Xaml" : "Microsoft.UI.Xaml"; + string cachingPrefix = isLocalCacheEnabled ? """ + + if (global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + return; + } + + global::System.Uri? __oldValue = field; + + OnValueChanging(value); + OnValueChanging(__oldValue, value); + + field = value; + """ : ""; + string cachingSuffix = isLocalCacheEnabled ? "OnValueChanged(__oldValue, value);" : ""; + string expected = $$""" + { + OnValueSet(ref value); + {{cachingPrefix}} + + if (value is null) + { + SetValue(ValueProperty, value); + } + else + { + global::{{xamlNamespace}}.Markup.XamlBindingHelper.SetPropertyFromUri(this, ValueProperty, value); + } + + OnValueChanged(value); + {{cachingSuffix}} + } + """; + + foreach (string propertyType in new[] { "System.Uri", "System.Uri?" }) + { + Compilation compilation = CreateXamlBindingHelperCompilation( + CreateReferenceSetterSource(propertyType, isLocalCacheEnabled, useWindowsUIXaml), + useWindowsUIXaml); + + Assert.AreEqual(SyntaxFactory.ParseStatement(expected).NormalizeWhitespace().ToFullString(), GetGeneratedSetter(compilation)); + } + } + + [TestMethod] + [DynamicData(nameof(GetValueSetterData), DynamicDataSourceType.Method)] + public void XamlBindingHelper_ValueTypes( + string propertyType, + string? setMethod, + bool isLocalCacheEnabled, + bool useWindowsUIXaml, + bool isNullable) + { + string xamlNamespace = useWindowsUIXaml ? "Windows.UI.Xaml" : "Microsoft.UI.Xaml"; + string source = $$""" + using CommunityToolkit.WinUI; + using {{xamlNamespace}}; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty(IsLocalCacheEnabled = {{(isLocalCacheEnabled ? "true" : "false")}})] + public partial {{propertyType}}{{(isNullable ? "?" : "")}} Value { get; set; } + } + """; + Compilation compilation = CreateXamlBindingHelperCompilation(source, useWindowsUIXaml); + string setter = GetGeneratedSetter(compilation); + + if (isNullable || setMethod is null) + { + StringAssert.Contains(setter, "SetValue(ValueProperty, __boxedValue);"); + Assert.IsFalse(setter.Contains("XamlBindingHelper", StringComparison.Ordinal)); + } + else + { + StringAssert.Contains(setter, $"global::{xamlNamespace}.Markup.XamlBindingHelper.{setMethod}(this, ValueProperty, value);"); + Assert.IsFalse(setter.Contains("value is null", StringComparison.Ordinal)); + } + } + + public static IEnumerable GetReferenceSetterData() + { + foreach (string propertyType in new[] { "System.Uri", "System.Uri?", "string", "string?", "object", "object?" }) + { + foreach (bool isLocalCacheEnabled in new[] { false, true }) + { + foreach (bool useWindowsUIXaml in new[] { false, true }) + { + yield return [propertyType, isLocalCacheEnabled, useWindowsUIXaml]; + } + } + } + } + + public static IEnumerable GetBoxedSetHookData() + { + return GetReferenceSetterData().Where(static data => !((string)data[0]).StartsWith("object", StringComparison.Ordinal)); + } + + public static IEnumerable GetValueSetterData() + { + foreach (bool useWindowsUIXaml in new[] { false, true }) + { + foreach (var (propertyType, setMethod) in GetValueSetterTypes(useWindowsUIXaml)) + { + foreach (bool isLocalCacheEnabled in new[] { false, true }) + { + foreach (bool isNullable in new[] { false, true }) + { + bool isOptionalHelper = setMethod is "SetPropertyFromColor" or "SetPropertyFromCornerRadius" or "SetPropertyFromThickness"; + + yield return [propertyType, useWindowsUIXaml && isOptionalHelper ? null : setMethod, isLocalCacheEnabled, useWindowsUIXaml, isNullable]; + } + } + } + } + } + + private static (string Type, string Method)[] GetValueSetterTypes(bool useWindowsUIXaml) + { + string xamlNamespace = useWindowsUIXaml ? "Windows.UI.Xaml" : "Microsoft.UI.Xaml"; + + return + [ + ("bool", "SetPropertyFromBoolean"), + ("byte", "SetPropertyFromByte"), + ("char", "SetPropertyFromChar16"), + ("double", "SetPropertyFromDouble"), + ("int", "SetPropertyFromInt32"), + ("long", "SetPropertyFromInt64"), + ("float", "SetPropertyFromSingle"), + ("uint", "SetPropertyFromUInt32"), + ("ulong", "SetPropertyFromUInt64"), + ("System.DateTimeOffset", "SetPropertyFromDateTime"), + ("System.TimeSpan", "SetPropertyFromTimeSpan"), + ("Windows.Foundation.Point", "SetPropertyFromPoint"), + ("Windows.Foundation.Rect", "SetPropertyFromRect"), + ("Windows.Foundation.Size", "SetPropertyFromSize"), + ("Windows.UI.Color", "SetPropertyFromColor"), + ($"{xamlNamespace}.CornerRadius", "SetPropertyFromCornerRadius"), + ($"{xamlNamespace}.Thickness", "SetPropertyFromThickness") + ]; + } + + private static string[] GetSetterEvents(bool isLocalCacheEnabled, string setMethod) + { + return isLocalCacheEnabled + ? ["Set", "Changing", "ChangingWithOldValue", setMethod, "PropertyChanged", "SharedPropertyChanged", "Changed", "ChangedWithOldValue"] + : ["Set", setMethod, "PropertyChanged", "SharedPropertyChanged", "Changed"]; + } + + private static string CreateReferenceSetterSource( + string propertyType, + bool isLocalCacheEnabled, + bool useWindowsUIXaml, + bool hasBoxedSetHook = false) + { + string xamlNamespace = useWindowsUIXaml ? "Windows.UI.Xaml" : "Microsoft.UI.Xaml"; + string oldValueType = $"{propertyType.TrimEnd('?')}?"; + string cachingHooks = isLocalCacheEnabled ? $$""" + partial void OnValueChanging({{propertyType}} newValue) => Events.Add("Changing"); + partial void OnValueChanging({{oldValueType}} oldValue, {{propertyType}} newValue) => Events.Add("ChangingWithOldValue"); + partial void OnValueChanged({{oldValueType}} oldValue, {{propertyType}} newValue) => Events.Add("ChangedWithOldValue"); + """ : ""; + string boxedSetHook = hasBoxedSetHook ? $$""" + partial void OnValueSet(ref {{(propertyType.EndsWith("?", StringComparison.Ordinal) ? "object?" : "object")}} propertyValue) + { + Events.Add("BoxedSet"); + propertyValue = null!; + } + """ : ""; + + return $$""" + using CommunityToolkit.WinUI; + using {{xamlNamespace}}; + + #nullable enable + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty(IsLocalCacheEnabled = {{(isLocalCacheEnabled ? "true" : "false")}})] + public partial {{propertyType}} Value { get; set; } + + public bool SetToNull { get; set; } + public object? LocalValue => ReadLocalValue(ValueProperty); + public object? OldValue { get; private set; } + public object? NewValue { get; private set; } + + partial void OnValueSet(ref {{propertyType}} propertyValue) + { + Events.Add("Set"); + + if (SetToNull) + { + propertyValue = null!; + } + } + + {{boxedSetHook}} + {{cachingHooks}} + partial void OnValueChanged({{propertyType}} newValue) => Events.Add("Changed"); + + partial void OnValuePropertyChanged(DependencyPropertyChangedEventArgs e) + { + Events.Add("PropertyChanged"); + OldValue = e.OldValue; + NewValue = e.NewValue; + } + + partial void OnPropertyChanged(DependencyPropertyChangedEventArgs e) => Events.Add("SharedPropertyChanged"); + } + """; + } + + private static string GetGeneratedSetter(Compilation compilation) + { + return compilation.SyntaxTrees + .Single(tree => Path.GetFileName(tree.FilePath) == "MyNamespace.MyControl.g.cs") + .GetRoot() + .DescendantNodes() + .OfType() + .Single() + .AccessorList!.Accessors.Single(accessor => accessor.IsKind(SyntaxKind.SetAccessorDeclaration)) + .Body!.NormalizeWhitespace().ToFullString(); + } + + private static void RunGeneratedSetter(Compilation compilation, Action test) + { + using MemoryStream stream = new(); + + Assert.IsTrue(compilation.Emit(stream).Success); + + stream.Position = 0; + + AssemblyLoadContext context = new(name: null, isCollectible: true); + + try + { + Assembly assembly = context.LoadFromStream(stream); + + test(assembly.GetType("MyNamespace.MyControl", throwOnError: true)!); + } + finally + { + context.Unload(); + } + } + + private static Compilation CreateXamlBindingHelperCompilation(string source, bool useWindowsUIXaml) + { + string xamlNamespace = useWindowsUIXaml ? "Windows.UI.Xaml" : "Microsoft.UI.Xaml"; + string valueSetters = string.Join(Environment.NewLine, GetValueSetterTypes(useWindowsUIXaml).Select(static item => $$""" + public static void {{item.Method}}(object dependencyObject, DependencyProperty propertyToSet, {{item.Type}} value) + => ((DependencyObject)dependencyObject).SetValueCore(propertyToSet, value, "{{item.Method}}"); + """)); + + // Managed XAML doubles exercise generated setters without requiring a XAML application + string xamlTypes = $$""" + using System; + using System.Collections.Generic; + + #nullable enable + + namespace Windows.Foundation + { + public struct Point; + public struct Rect; + public struct Size; + } + + namespace Windows.UI + { + public struct Color; + } + + namespace {{xamlNamespace}} + { + public struct CornerRadius; + public struct Thickness; + public delegate void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e); + + public sealed class DependencyPropertyChangedEventArgs(object? oldValue, object? newValue) + { + public object? OldValue { get; } = oldValue; + public object? NewValue { get; } = newValue; + } + + public sealed class PropertyMetadata(object? defaultValue, PropertyChangedCallback? propertyChangedCallback = null) + { + public object? DefaultValue { get; } = defaultValue; + public PropertyChangedCallback? Callback { get; } = propertyChangedCallback; + } + + public sealed class DependencyProperty + { + public static object UnsetValue { get; } = new(); + public PropertyMetadata? Metadata { get; private init; } + + public static DependencyProperty Register(string name, Type propertyType, Type ownerType, PropertyMetadata? typeMetadata) + => new() { Metadata = typeMetadata }; + } + + public class DependencyObject + { + private readonly Dictionary values = new(); + + public List Events { get; } = new(); + + public object? GetValue(DependencyProperty property) + => values.TryGetValue(property, out object? value) ? value : property.Metadata?.DefaultValue; + + public object? ReadLocalValue(DependencyProperty property) + => values.TryGetValue(property, out object? value) ? value : DependencyProperty.UnsetValue; + + public void SetValue(DependencyProperty property, object? value) => SetValueCore(property, value, "SetValue"); + + public void SetValueCore(DependencyProperty property, object? value, string method) + { + object? oldValue = GetValue(property); + values[property] = value; + Events.Add(method); + + if (!Equals(oldValue, value)) + { + property.Metadata?.Callback?.Invoke(this, new(oldValue, value)); + } + } + } + } + + namespace {{xamlNamespace}}.Markup + { + public static class XamlBindingHelper + { + public static void SetPropertyFromUri(object dependencyObject, DependencyProperty propertyToSet, Uri value) + { + ArgumentNullException.ThrowIfNull(value); + ((DependencyObject)dependencyObject).SetValueCore(propertyToSet, value, "SetPropertyFromUri"); + } + + public static void SetPropertyFromString(object dependencyObject, DependencyProperty propertyToSet, string value) + { + ArgumentException.ThrowIfNullOrEmpty(value); + ((DependencyObject)dependencyObject).SetValueCore(propertyToSet, value, "SetPropertyFromString"); + } + + public static void SetPropertyFromObject(object dependencyObject, DependencyProperty propertyToSet, object value) + { + ArgumentNullException.ThrowIfNull(value); + ((DependencyObject)dependencyObject).SetValueCore(propertyToSet, value, "SetPropertyFromObject"); + } + + {{valueSetters}} + } + } + """; + CSharpParseOptions parseOptions = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Preview); + CSharpCompilation compilation = CSharpCompilation.Create( + "XamlBindingHelperTests", + [CSharpSyntaxTree.ParseText(source, parseOptions), CSharpSyntaxTree.ParseText(xamlTypes, parseOptions)], + [.. Net80.References.All, MetadataReference.CreateFromFile(typeof(GeneratedDependencyPropertyAttribute).Assembly.Location)], + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, allowUnsafe: true)); + + return CSharpGeneratorTest.VerifyCompiles( + compilation, + new DependencyPropertyGeneratorAnalyzerConfigOptionsProvider(useWindowsUIXaml)); + } +} 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. From 58cbe62ef2e3860e1215337ac9de246a27f40414 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 21 Sep 2026 18:17:58 -0700 Subject: [PATCH 2/2] Use full-output dependency property generator tests Replace the runtime/reflection harness and fragment assertions with existing VerifySources snapshots. Cover URI null fallbacks, null-producing typed hooks, and boxed string/URI hooks without changing the shared test helpers. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../CSharpGeneratorTest{TGenerator}.cs | 34 +- ...yGeneratorAnalyzerConfigOptionsProvider.cs | 5 +- ...DependencyPropertyGenerator.Compilation.cs | 2 - ...encyPropertyGenerator.XamlBindingHelper.cs | 504 ------------------ .../Test_DependencyPropertyGenerator.cs | 342 +++++++++++- 5 files changed, 331 insertions(+), 556 deletions(-) delete mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.XamlBindingHelper.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs index b15ead5f0..7ab80d1b8 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs @@ -11,7 +11,6 @@ using CommunityToolkit.WinUI; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Emit; using Microsoft.VisualStudio.TestTools.UnitTesting; using Windows.Foundation; @@ -57,7 +56,7 @@ public static void VerifyDiagnostics(string source, params string[] diagnosticsI /// The input source to process. /// The expected source to be generated. /// The language version to use to run the test. - /// This also supports invalid inputs. Use to validate compilation. + /// This also supports invalid inputs. Use to validate compilation. public static void VerifySources(string source, (string Filename, string Source) result, LanguageVersion languageVersion = LanguageVersion.CSharp13) { RunGenerator(source, out Compilation compilation, out ImmutableArray diagnostics, languageVersion); @@ -81,18 +80,7 @@ public static void VerifySources(string source, (string Filename, string Source) /// The resulting compilation, for further assertions on the generated sources. public static Compilation VerifyCompiles(string source, LanguageVersion languageVersion = LanguageVersion.CSharp13) { - return VerifyCompiles(CreateCompilation(source, languageVersion), DependencyPropertyGeneratorAnalyzerConfigOptionsProvider.Instance); - } - - /// - /// Verifies that the resulting sources can be emitted with the specified references and generator options. - /// - /// The input compilation to process. - /// The analyzer options to use for the generator. - /// The resulting compilation, for further assertions on the generated sources. - public static Compilation VerifyCompiles(Compilation originalCompilation, AnalyzerConfigOptionsProvider optionsProvider) - { - RunGenerator(originalCompilation, optionsProvider, out Compilation compilation, out ImmutableArray diagnostics); + RunGenerator(source, out Compilation compilation, out ImmutableArray diagnostics, languageVersion); CollectionAssert.AreEquivalent(Array.Empty(), diagnostics); @@ -201,26 +189,10 @@ private static void RunGenerator( { Compilation originalCompilation = CreateCompilation(source, languageVersion); - RunGenerator(originalCompilation, DependencyPropertyGeneratorAnalyzerConfigOptionsProvider.Instance, out compilation, out diagnostics); - } - - /// - /// Runs a generator with the specified compilation and options. - /// - /// The input compilation to process. - /// The analyzer options to use for the generator. - /// The resulting compilation. - /// The resulting generator diagnostics. - private static void RunGenerator( - Compilation originalCompilation, - AnalyzerConfigOptionsProvider optionsProvider, - out Compilation compilation, - out ImmutableArray diagnostics) - { // Create the generator driver with the specified generator GeneratorDriver driver = CSharpGeneratorDriver.Create( generators: [new TGenerator().AsSourceGenerator()], - optionsProvider: optionsProvider).WithUpdatedParseOptions(originalCompilation.SyntaxTrees.First().Options); + optionsProvider: DependencyPropertyGeneratorAnalyzerConfigOptionsProvider.Instance).WithUpdatedParseOptions(originalCompilation.SyntaxTrees.First().Options); // Run all source generators on the input source code _ = driver.RunGeneratorsAndUpdateCompilation(originalCompilation, out compilation, out diagnostics); diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/DependencyPropertyGeneratorAnalyzerConfigOptionsProvider.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/DependencyPropertyGeneratorAnalyzerConfigOptionsProvider.cs index 1e98ed21c..bf0f7458b 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/DependencyPropertyGeneratorAnalyzerConfigOptionsProvider.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/DependencyPropertyGeneratorAnalyzerConfigOptionsProvider.cs @@ -12,8 +12,7 @@ namespace CommunityToolkit.GeneratedDependencyProperty.Tests.Helpers; /// /// A custom providing the MSBuild properties needed by the dependency property generator. /// -/// Whether to use the UWP XAML or WinUI 3 XAML namespaces. -internal sealed class DependencyPropertyGeneratorAnalyzerConfigOptionsProvider(bool useWindowsUIXaml = true) : AnalyzerConfigOptionsProvider +internal sealed class DependencyPropertyGeneratorAnalyzerConfigOptionsProvider : AnalyzerConfigOptionsProvider { /// /// The singleton instance. @@ -22,7 +21,7 @@ internal sealed class DependencyPropertyGeneratorAnalyzerConfigOptionsProvider(b /// public override AnalyzerConfigOptions GlobalOptions { get; } = new SimpleAnalyzerConfigOptions( - ImmutableDictionary.Empty.Add("build_property.DependencyPropertyGeneratorUseWindowsUIXaml", useWindowsUIXaml ? "true" : "false")); + ImmutableDictionary.Empty.Add("build_property.DependencyPropertyGeneratorUseWindowsUIXaml", "true")); /// public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.Compilation.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.Compilation.cs index 5d570939a..b81845868 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.Compilation.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.Compilation.cs @@ -24,8 +24,6 @@ partial class Test_DependencyPropertyGenerator [DataRow("int?", null)] [DataRow("bool", null)] [DataRow("string", null)] - [DataRow("System.Uri", null)] - [DataRow("System.Uri?", null)] public void SingleProperty_WithLocalCache_Compiles(string propertyType, string? defaultValue) { string defaultValueArgument = defaultValue is null ? "" : $", DefaultValue = {defaultValue}"; diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.XamlBindingHelper.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.XamlBindingHelper.cs deleted file mode 100644 index 6658fc6f1..000000000 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.XamlBindingHelper.cs +++ /dev/null @@ -1,504 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Runtime.Loader; -using Basic.Reference.Assemblies; -using CommunityToolkit.GeneratedDependencyProperty.Tests.Helpers; -using CommunityToolkit.WinUI; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace CommunityToolkit.GeneratedDependencyProperty.Tests; - -partial class Test_DependencyPropertyGenerator -{ - [TestMethod] - [DynamicData(nameof(GetReferenceSetterData), DynamicDataSourceType.Method)] - public void XamlBindingHelper_ReferenceValues( - string propertyType, - bool isLocalCacheEnabled, - bool useWindowsUIXaml) - { - Compilation compilation = CreateXamlBindingHelperCompilation( - CreateReferenceSetterSource(propertyType, isLocalCacheEnabled, useWindowsUIXaml), - useWindowsUIXaml); - - RunGeneratedSetter(compilation, type => - { - object control = Activator.CreateInstance(type)!; - PropertyInfo property = type.GetProperty("Value")!; - List events = (List)type.GetProperty("Events")!.GetValue(control)!; - object value = propertyType.StartsWith("System.Uri", StringComparison.Ordinal) - ? new Uri("https://example.com/first") - : "First"; - string setMethod = propertyType.TrimEnd('?') switch - { - "System.Uri" => "SetPropertyFromUri", - "string" => "SetPropertyFromString", - _ => "SetValue" - }; - - property.SetValue(control, null); - - CollectionAssert.AreEqual( - isLocalCacheEnabled ? new[] { "Set" } : new[] { "Set", "SetValue", "Changed" }, - events); - Assert.IsNull(property.GetValue(control)); - - events.Clear(); - property.SetValue(control, value); - - Assert.AreEqual(value, property.GetValue(control)); - Assert.AreEqual(value, type.GetProperty("LocalValue")!.GetValue(control)); - CollectionAssert.AreEqual(GetSetterEvents(isLocalCacheEnabled, setMethod), events); - - events.Clear(); - property.SetValue(control, null); - - Assert.IsNull(property.GetValue(control)); - Assert.IsNull(type.GetProperty("LocalValue")!.GetValue(control)); - Assert.AreEqual(value, type.GetProperty("OldValue")!.GetValue(control)); - Assert.IsNull(type.GetProperty("NewValue")!.GetValue(control)); - CollectionAssert.AreEqual(GetSetterEvents(isLocalCacheEnabled, "SetValue"), events); - - property.SetValue(control, value); - type.GetProperty("SetToNull")!.SetValue(control, true); - events.Clear(); - property.SetValue(control, value); - - Assert.IsNull(property.GetValue(control)); - Assert.IsNull(type.GetProperty("LocalValue")!.GetValue(control)); - CollectionAssert.AreEqual(GetSetterEvents(isLocalCacheEnabled, "SetValue"), events); - - if (propertyType.StartsWith("string", StringComparison.Ordinal)) - { - type.GetProperty("SetToNull")!.SetValue(control, false); - events.Clear(); - property.SetValue(control, ""); - - Assert.AreEqual("", property.GetValue(control)); - Assert.AreEqual("", type.GetProperty("LocalValue")!.GetValue(control)); - CollectionAssert.AreEqual(GetSetterEvents(isLocalCacheEnabled, "SetValue"), events); - } - }); - } - - [TestMethod] - [DynamicData(nameof(GetBoxedSetHookData), DynamicDataSourceType.Method)] - public void XamlBindingHelper_BoxedSetHook( - string propertyType, - bool isLocalCacheEnabled, - bool useWindowsUIXaml) - { - string source = CreateReferenceSetterSource(propertyType, isLocalCacheEnabled, useWindowsUIXaml, hasBoxedSetHook: true); - Compilation compilation = CreateXamlBindingHelperCompilation(source, useWindowsUIXaml); - - Assert.IsFalse(GetGeneratedSetter(compilation).Contains("XamlBindingHelper", StringComparison.Ordinal)); - - RunGeneratedSetter(compilation, type => - { - object control = Activator.CreateInstance(type)!; - PropertyInfo property = type.GetProperty("Value")!; - List events = (List)type.GetProperty("Events")!.GetValue(control)!; - object value = propertyType.StartsWith("System.Uri", StringComparison.Ordinal) - ? new Uri("https://example.com/first") - : "First"; - - property.SetValue(control, value); - - Assert.IsNull(type.GetProperty("LocalValue")!.GetValue(control)); - CollectionAssert.AreEqual( - isLocalCacheEnabled - ? new[] { "Set", "Changing", "ChangingWithOldValue", "BoxedSet", "SetValue", "Changed", "ChangedWithOldValue" } - : new[] { "Set", "BoxedSet", "SetValue", "Changed" }, - events); - }); - } - - [TestMethod] - [DataRow(false, false)] - [DataRow(false, true)] - [DataRow(true, false)] - [DataRow(true, true)] - public void XamlBindingHelper_Uri_Setter(bool isLocalCacheEnabled, bool useWindowsUIXaml) - { - string xamlNamespace = useWindowsUIXaml ? "Windows.UI.Xaml" : "Microsoft.UI.Xaml"; - string cachingPrefix = isLocalCacheEnabled ? """ - - if (global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) - { - return; - } - - global::System.Uri? __oldValue = field; - - OnValueChanging(value); - OnValueChanging(__oldValue, value); - - field = value; - """ : ""; - string cachingSuffix = isLocalCacheEnabled ? "OnValueChanged(__oldValue, value);" : ""; - string expected = $$""" - { - OnValueSet(ref value); - {{cachingPrefix}} - - if (value is null) - { - SetValue(ValueProperty, value); - } - else - { - global::{{xamlNamespace}}.Markup.XamlBindingHelper.SetPropertyFromUri(this, ValueProperty, value); - } - - OnValueChanged(value); - {{cachingSuffix}} - } - """; - - foreach (string propertyType in new[] { "System.Uri", "System.Uri?" }) - { - Compilation compilation = CreateXamlBindingHelperCompilation( - CreateReferenceSetterSource(propertyType, isLocalCacheEnabled, useWindowsUIXaml), - useWindowsUIXaml); - - Assert.AreEqual(SyntaxFactory.ParseStatement(expected).NormalizeWhitespace().ToFullString(), GetGeneratedSetter(compilation)); - } - } - - [TestMethod] - [DynamicData(nameof(GetValueSetterData), DynamicDataSourceType.Method)] - public void XamlBindingHelper_ValueTypes( - string propertyType, - string? setMethod, - bool isLocalCacheEnabled, - bool useWindowsUIXaml, - bool isNullable) - { - string xamlNamespace = useWindowsUIXaml ? "Windows.UI.Xaml" : "Microsoft.UI.Xaml"; - string source = $$""" - using CommunityToolkit.WinUI; - using {{xamlNamespace}}; - - namespace MyNamespace; - - public partial class MyControl : DependencyObject - { - [GeneratedDependencyProperty(IsLocalCacheEnabled = {{(isLocalCacheEnabled ? "true" : "false")}})] - public partial {{propertyType}}{{(isNullable ? "?" : "")}} Value { get; set; } - } - """; - Compilation compilation = CreateXamlBindingHelperCompilation(source, useWindowsUIXaml); - string setter = GetGeneratedSetter(compilation); - - if (isNullable || setMethod is null) - { - StringAssert.Contains(setter, "SetValue(ValueProperty, __boxedValue);"); - Assert.IsFalse(setter.Contains("XamlBindingHelper", StringComparison.Ordinal)); - } - else - { - StringAssert.Contains(setter, $"global::{xamlNamespace}.Markup.XamlBindingHelper.{setMethod}(this, ValueProperty, value);"); - Assert.IsFalse(setter.Contains("value is null", StringComparison.Ordinal)); - } - } - - public static IEnumerable GetReferenceSetterData() - { - foreach (string propertyType in new[] { "System.Uri", "System.Uri?", "string", "string?", "object", "object?" }) - { - foreach (bool isLocalCacheEnabled in new[] { false, true }) - { - foreach (bool useWindowsUIXaml in new[] { false, true }) - { - yield return [propertyType, isLocalCacheEnabled, useWindowsUIXaml]; - } - } - } - } - - public static IEnumerable GetBoxedSetHookData() - { - return GetReferenceSetterData().Where(static data => !((string)data[0]).StartsWith("object", StringComparison.Ordinal)); - } - - public static IEnumerable GetValueSetterData() - { - foreach (bool useWindowsUIXaml in new[] { false, true }) - { - foreach (var (propertyType, setMethod) in GetValueSetterTypes(useWindowsUIXaml)) - { - foreach (bool isLocalCacheEnabled in new[] { false, true }) - { - foreach (bool isNullable in new[] { false, true }) - { - bool isOptionalHelper = setMethod is "SetPropertyFromColor" or "SetPropertyFromCornerRadius" or "SetPropertyFromThickness"; - - yield return [propertyType, useWindowsUIXaml && isOptionalHelper ? null : setMethod, isLocalCacheEnabled, useWindowsUIXaml, isNullable]; - } - } - } - } - } - - private static (string Type, string Method)[] GetValueSetterTypes(bool useWindowsUIXaml) - { - string xamlNamespace = useWindowsUIXaml ? "Windows.UI.Xaml" : "Microsoft.UI.Xaml"; - - return - [ - ("bool", "SetPropertyFromBoolean"), - ("byte", "SetPropertyFromByte"), - ("char", "SetPropertyFromChar16"), - ("double", "SetPropertyFromDouble"), - ("int", "SetPropertyFromInt32"), - ("long", "SetPropertyFromInt64"), - ("float", "SetPropertyFromSingle"), - ("uint", "SetPropertyFromUInt32"), - ("ulong", "SetPropertyFromUInt64"), - ("System.DateTimeOffset", "SetPropertyFromDateTime"), - ("System.TimeSpan", "SetPropertyFromTimeSpan"), - ("Windows.Foundation.Point", "SetPropertyFromPoint"), - ("Windows.Foundation.Rect", "SetPropertyFromRect"), - ("Windows.Foundation.Size", "SetPropertyFromSize"), - ("Windows.UI.Color", "SetPropertyFromColor"), - ($"{xamlNamespace}.CornerRadius", "SetPropertyFromCornerRadius"), - ($"{xamlNamespace}.Thickness", "SetPropertyFromThickness") - ]; - } - - private static string[] GetSetterEvents(bool isLocalCacheEnabled, string setMethod) - { - return isLocalCacheEnabled - ? ["Set", "Changing", "ChangingWithOldValue", setMethod, "PropertyChanged", "SharedPropertyChanged", "Changed", "ChangedWithOldValue"] - : ["Set", setMethod, "PropertyChanged", "SharedPropertyChanged", "Changed"]; - } - - private static string CreateReferenceSetterSource( - string propertyType, - bool isLocalCacheEnabled, - bool useWindowsUIXaml, - bool hasBoxedSetHook = false) - { - string xamlNamespace = useWindowsUIXaml ? "Windows.UI.Xaml" : "Microsoft.UI.Xaml"; - string oldValueType = $"{propertyType.TrimEnd('?')}?"; - string cachingHooks = isLocalCacheEnabled ? $$""" - partial void OnValueChanging({{propertyType}} newValue) => Events.Add("Changing"); - partial void OnValueChanging({{oldValueType}} oldValue, {{propertyType}} newValue) => Events.Add("ChangingWithOldValue"); - partial void OnValueChanged({{oldValueType}} oldValue, {{propertyType}} newValue) => Events.Add("ChangedWithOldValue"); - """ : ""; - string boxedSetHook = hasBoxedSetHook ? $$""" - partial void OnValueSet(ref {{(propertyType.EndsWith("?", StringComparison.Ordinal) ? "object?" : "object")}} propertyValue) - { - Events.Add("BoxedSet"); - propertyValue = null!; - } - """ : ""; - - return $$""" - using CommunityToolkit.WinUI; - using {{xamlNamespace}}; - - #nullable enable - - namespace MyNamespace; - - public partial class MyControl : DependencyObject - { - [GeneratedDependencyProperty(IsLocalCacheEnabled = {{(isLocalCacheEnabled ? "true" : "false")}})] - public partial {{propertyType}} Value { get; set; } - - public bool SetToNull { get; set; } - public object? LocalValue => ReadLocalValue(ValueProperty); - public object? OldValue { get; private set; } - public object? NewValue { get; private set; } - - partial void OnValueSet(ref {{propertyType}} propertyValue) - { - Events.Add("Set"); - - if (SetToNull) - { - propertyValue = null!; - } - } - - {{boxedSetHook}} - {{cachingHooks}} - partial void OnValueChanged({{propertyType}} newValue) => Events.Add("Changed"); - - partial void OnValuePropertyChanged(DependencyPropertyChangedEventArgs e) - { - Events.Add("PropertyChanged"); - OldValue = e.OldValue; - NewValue = e.NewValue; - } - - partial void OnPropertyChanged(DependencyPropertyChangedEventArgs e) => Events.Add("SharedPropertyChanged"); - } - """; - } - - private static string GetGeneratedSetter(Compilation compilation) - { - return compilation.SyntaxTrees - .Single(tree => Path.GetFileName(tree.FilePath) == "MyNamespace.MyControl.g.cs") - .GetRoot() - .DescendantNodes() - .OfType() - .Single() - .AccessorList!.Accessors.Single(accessor => accessor.IsKind(SyntaxKind.SetAccessorDeclaration)) - .Body!.NormalizeWhitespace().ToFullString(); - } - - private static void RunGeneratedSetter(Compilation compilation, Action test) - { - using MemoryStream stream = new(); - - Assert.IsTrue(compilation.Emit(stream).Success); - - stream.Position = 0; - - AssemblyLoadContext context = new(name: null, isCollectible: true); - - try - { - Assembly assembly = context.LoadFromStream(stream); - - test(assembly.GetType("MyNamespace.MyControl", throwOnError: true)!); - } - finally - { - context.Unload(); - } - } - - private static Compilation CreateXamlBindingHelperCompilation(string source, bool useWindowsUIXaml) - { - string xamlNamespace = useWindowsUIXaml ? "Windows.UI.Xaml" : "Microsoft.UI.Xaml"; - string valueSetters = string.Join(Environment.NewLine, GetValueSetterTypes(useWindowsUIXaml).Select(static item => $$""" - public static void {{item.Method}}(object dependencyObject, DependencyProperty propertyToSet, {{item.Type}} value) - => ((DependencyObject)dependencyObject).SetValueCore(propertyToSet, value, "{{item.Method}}"); - """)); - - // Managed XAML doubles exercise generated setters without requiring a XAML application - string xamlTypes = $$""" - using System; - using System.Collections.Generic; - - #nullable enable - - namespace Windows.Foundation - { - public struct Point; - public struct Rect; - public struct Size; - } - - namespace Windows.UI - { - public struct Color; - } - - namespace {{xamlNamespace}} - { - public struct CornerRadius; - public struct Thickness; - public delegate void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e); - - public sealed class DependencyPropertyChangedEventArgs(object? oldValue, object? newValue) - { - public object? OldValue { get; } = oldValue; - public object? NewValue { get; } = newValue; - } - - public sealed class PropertyMetadata(object? defaultValue, PropertyChangedCallback? propertyChangedCallback = null) - { - public object? DefaultValue { get; } = defaultValue; - public PropertyChangedCallback? Callback { get; } = propertyChangedCallback; - } - - public sealed class DependencyProperty - { - public static object UnsetValue { get; } = new(); - public PropertyMetadata? Metadata { get; private init; } - - public static DependencyProperty Register(string name, Type propertyType, Type ownerType, PropertyMetadata? typeMetadata) - => new() { Metadata = typeMetadata }; - } - - public class DependencyObject - { - private readonly Dictionary values = new(); - - public List Events { get; } = new(); - - public object? GetValue(DependencyProperty property) - => values.TryGetValue(property, out object? value) ? value : property.Metadata?.DefaultValue; - - public object? ReadLocalValue(DependencyProperty property) - => values.TryGetValue(property, out object? value) ? value : DependencyProperty.UnsetValue; - - public void SetValue(DependencyProperty property, object? value) => SetValueCore(property, value, "SetValue"); - - public void SetValueCore(DependencyProperty property, object? value, string method) - { - object? oldValue = GetValue(property); - values[property] = value; - Events.Add(method); - - if (!Equals(oldValue, value)) - { - property.Metadata?.Callback?.Invoke(this, new(oldValue, value)); - } - } - } - } - - namespace {{xamlNamespace}}.Markup - { - public static class XamlBindingHelper - { - public static void SetPropertyFromUri(object dependencyObject, DependencyProperty propertyToSet, Uri value) - { - ArgumentNullException.ThrowIfNull(value); - ((DependencyObject)dependencyObject).SetValueCore(propertyToSet, value, "SetPropertyFromUri"); - } - - public static void SetPropertyFromString(object dependencyObject, DependencyProperty propertyToSet, string value) - { - ArgumentException.ThrowIfNullOrEmpty(value); - ((DependencyObject)dependencyObject).SetValueCore(propertyToSet, value, "SetPropertyFromString"); - } - - public static void SetPropertyFromObject(object dependencyObject, DependencyProperty propertyToSet, object value) - { - ArgumentNullException.ThrowIfNull(value); - ((DependencyObject)dependencyObject).SetValueCore(propertyToSet, value, "SetPropertyFromObject"); - } - - {{valueSetters}} - } - } - """; - CSharpParseOptions parseOptions = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Preview); - CSharpCompilation compilation = CSharpCompilation.Create( - "XamlBindingHelperTests", - [CSharpSyntaxTree.ParseText(source, parseOptions), CSharpSyntaxTree.ParseText(xamlTypes, parseOptions)], - [.. Net80.References.All, MetadataReference.CreateFromFile(typeof(GeneratedDependencyPropertyAttribute).Assembly.Location)], - new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, allowUnsafe: true)); - - return CSharpGeneratorTest.VerifyCompiles( - compilation, - new DependencyPropertyGeneratorAnalyzerConfigOptionsProvider(useWindowsUIXaml)); - } -} 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); + } }