diff --git a/Directory.Build.props b/Directory.Build.props index 7f02eacdb..eb41eb24a 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,37 +1,41 @@ - - - 0 - 1 - - CommunityToolkit.Labs - $([MSBuild]::EnsureTrailingSlash('$(MSBuildThisFileDirectory)')) - $(RepositoryDirectory)tooling - true - - - - true - true - $(NoWarn);Uno0001 - - - NU1901;NU1902;NU1903;NU1904; - - - $(WarningsNotAsErrors);CS1591;CS1574; - - - $(WarningsNotAsErrors);CS0419;CS1570; - - - - - - Embedded - bin\Package - .pdb - true - true - - - + + + 0 + 1 + + CommunityToolkit.Labs + $([MSBuild]::EnsureTrailingSlash('$(MSBuildThisFileDirectory)')) + $(RepositoryDirectory)tooling + true + + + + true + true + $(NoWarn);Uno0001 + + + + NU1901;NU1902;NU1903;NU1904; + + + $(WarningsNotAsErrors);CS1591;CS1574; + + + $(WarningsNotAsErrors);CS0419;CS1570; + + + $(WarningsNotAsErrors);TKSMPL0014;TKSMPL0013; + + + + + + Embedded + bin\Package + .pdb + true + true + + + diff --git a/ReadMe.md b/ReadMe.md index d94b4e1a4..51aebc2ce 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -30,7 +30,7 @@ Otherwise, you can clone the repo, open the `components` directory, navigate wit - [CanvasView](https://github.com/CommunityToolkit/Labs-Windows/blob/main/components/CanvasView/samples/CanvasView.md) - [ColorAnalyzer](https://github.com/CommunityToolkit/Labs-Windows/tree/main/components/ColorAnalyzer) - [DataTable](https://github.com/CommunityToolkit/Labs-Windows/blob/main/components/DataTable/samples/DataTable.md) -- [DependencyPropertyGenerator](https://github.com/CommunityToolkit/Labs-Windows/tree/main/components/DependencyPropertyGenerator) +- [DependencyPropertyGenerator](https://github.com/CommunityToolkit/Labs-Windows/blob/main/components/DependencyPropertyGenerator/samples/DependencyPropertyGenerator.md) - [Extensions.DependencyInjection](https://github.com/CommunityToolkit/Labs-Windows/tree/main/components/Extensions.DependencyInjection) - [MarkdownTextBlock](https://github.com/CommunityToolkit/Labs-Windows/blob/main/components/MarkdownTextBlock/samples/MarkdownTextBlock.md) - [Marquee](https://github.com/CommunityToolkit/Labs-Windows/blob/main/components/Marquee/samples/Marquee.md) diff --git a/components/DependencyPropertyGenerator/.gitattributes b/components/DependencyPropertyGenerator/.gitattributes index 64d6ecc10..5a38756e7 100644 --- a/components/DependencyPropertyGenerator/.gitattributes +++ b/components/DependencyPropertyGenerator/.gitattributes @@ -7,4 +7,5 @@ *.cs text eol=lf *.csproj text eol=lf *.projitems text eol=lf -*.shprroj text eol=lf \ No newline at end of file +*.shprroj text eol=lf +*.xaml text eol=crlf \ No newline at end of file diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs index 239efcf6d..f16639625 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs @@ -307,7 +307,7 @@ public static DependencyPropertyDefaultValue GetDefaultValue( // Validate the method has a valid signature as well if (InvalidPropertyDefaultValueCallbackTypeAnalyzer.IsDefaultValueCallbackValid(propertySymbol, methodSymbol)) { - return new DependencyPropertyDefaultValue.Callback(methodName); + return new DependencyPropertyDefaultValue.Callback(methodName, RequiresBoxing: !methodSymbol.ReturnType.IsReferenceType); } } } @@ -690,10 +690,10 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) // Shared codegen { DefaultValue: DependencyPropertyDefaultValue.Null or DependencyPropertyDefaultValue.Default(_, true), IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: false } => "null", - { DefaultValue: DependencyPropertyDefaultValue.Callback(string methodName), IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: false } + { DefaultValue: DependencyPropertyDefaultValue.Callback callback, IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: false } => $""" global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}.Create( - createDefaultValueCallback: new {WellKnownTypeNames.CreateDefaultValueCallback(propertyInfo.UseWindowsUIXaml)}({methodName})) + createDefaultValueCallback: new {WellKnownTypeNames.CreateDefaultValueCallback(propertyInfo.UseWindowsUIXaml)}({callback})) """, { DefaultValue: { } defaultValue, IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: false } => $"new global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}({defaultValue})", @@ -701,22 +701,22 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) // Codegen for legacy UWP { IsAdditionalTypesGenerationSupported: false } => propertyInfo switch { - { DefaultValue: DependencyPropertyDefaultValue.Callback(string methodName), IsPropertyChangedCallbackImplemented: true, IsSharedPropertyChangedCallbackImplemented: false } + { DefaultValue: DependencyPropertyDefaultValue.Callback callback, IsPropertyChangedCallbackImplemented: true, IsSharedPropertyChangedCallbackImplemented: false } => $""" global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}.Create( - createDefaultValueCallback: new {WellKnownTypeNames.CreateDefaultValueCallback(propertyInfo.UseWindowsUIXaml)}({methodName}), + createDefaultValueCallback: new {WellKnownTypeNames.CreateDefaultValueCallback(propertyInfo.UseWindowsUIXaml)}({callback}), propertyChangedCallback: static (d, e) => (({typeQualifiedName})d).On{propertyInfo.PropertyName}PropertyChanged(e)) """, - { DefaultValue: DependencyPropertyDefaultValue.Callback(string methodName), IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: true } + { DefaultValue: DependencyPropertyDefaultValue.Callback callback, IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: true } => $""" global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}.Create( - createDefaultValueCallback: new {WellKnownTypeNames.CreateDefaultValueCallback(propertyInfo.UseWindowsUIXaml)}({methodName}), + createDefaultValueCallback: new {WellKnownTypeNames.CreateDefaultValueCallback(propertyInfo.UseWindowsUIXaml)}({callback}), propertyChangedCallback: static (d, e) => (({typeQualifiedName})d).OnPropertyChanged(e)) """, - { DefaultValue: DependencyPropertyDefaultValue.Callback(string methodName), IsPropertyChangedCallbackImplemented: true, IsSharedPropertyChangedCallbackImplemented: true } + { DefaultValue: DependencyPropertyDefaultValue.Callback callback, IsPropertyChangedCallbackImplemented: true, IsSharedPropertyChangedCallbackImplemented: true } => $$""" global::{{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}}.Create( - createDefaultValueCallback: new {{WellKnownTypeNames.CreateDefaultValueCallback(propertyInfo.UseWindowsUIXaml)}}({{methodName}}), + createDefaultValueCallback: new {{WellKnownTypeNames.CreateDefaultValueCallback(propertyInfo.UseWindowsUIXaml)}}({{callback}}), propertyChangedCallback: static (d, e) => { (({{typeQualifiedName}})d).On{{propertyInfo.PropertyName}}PropertyChanged(e); (({{typeQualifiedName}})d).OnPropertyChanged(e); }) """, { DefaultValue: { } defaultValue, IsPropertyChangedCallbackImplemented: true, IsSharedPropertyChangedCallbackImplemented: false } @@ -753,10 +753,10 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) defaultValue: null, propertyChangedCallback: global::{GeneratorName}.PropertyChangedCallbacks.{propertyInfo.PropertyName}()) """, - { DefaultValue: DependencyPropertyDefaultValue.Callback(string methodName) } + { DefaultValue: DependencyPropertyDefaultValue.Callback callback } => $""" global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}.Create( - createDefaultValueCallback: new {WellKnownTypeNames.CreateDefaultValueCallback(propertyInfo.UseWindowsUIXaml)}({methodName}), + createDefaultValueCallback: new {WellKnownTypeNames.CreateDefaultValueCallback(propertyInfo.UseWindowsUIXaml)}({callback}), propertyChangedCallback: global::{GeneratorName}.PropertyChangedCallbacks.{propertyInfo.PropertyName}()) """, { DefaultValue: { } defaultValue } and ({ IsPropertyChangedCallbackImplemented: true } or { IsSharedPropertyChangedCallbackImplemented: true }) @@ -818,8 +818,11 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) // We will never have the 'partial' modifier in the set of property modifiers processed above. writer.WriteLine($"partial {propertyInfo.TypeNameWithNullabilityAnnotations} {propertyInfo.PropertyName}"); - using (writer.WriteBlock()) + writer.WriteLine("{"); + { + writer.IncreaseIndent(); + // We need very different codegen depending on whether local caching is enabled or not if (propertyInfo.IsLocalCachingEnabled) { @@ -894,13 +897,6 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) """, isMultiline: true); } - // If the default value is not what the default field value would be, add an initializer - if (propertyInfo.DefaultValue is not (DependencyPropertyDefaultValue.Null or DependencyPropertyDefaultValue.Default or DependencyPropertyDefaultValue.Callback)) - { - writer.Write($" = {propertyInfo.DefaultValue};"); - } - - // Always leave a newline after the end of the property declaration, in either case writer.WriteLine(); } else if (propertyInfo.TypeName == "object") @@ -1010,7 +1006,20 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) } """, isMultiline: true); } + + writer.DecreaseIndent(); } + + writer.Write("}"); + + // The initializer follows the property's closing brace, not the setter's + if (propertyInfo.IsLocalCachingEnabled && + propertyInfo.DefaultValue is not (DependencyPropertyDefaultValue.Null or DependencyPropertyDefaultValue.Default or DependencyPropertyDefaultValue.Callback)) + { + writer.Write($" = {propertyInfo.DefaultValue};"); + } + + writer.WriteLine(); } // Next, emit all partial method declarations at the bottom of the file diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyDefaultValue.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyDefaultValue.cs index 108541a2e..5a842ab9e 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyDefaultValue.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyDefaultValue.cs @@ -90,12 +90,14 @@ public override string ToString() /// A type representing a callback. /// /// The name of the callback method to invoke. - public sealed record Callback(string MethodName) : DependencyPropertyDefaultValue + /// Whether the callback return value requires a boxing conversion to . + public sealed record Callback(string MethodName, bool RequiresBoxing) : DependencyPropertyDefaultValue { /// public override string ToString() { - return MethodName; + // Method-group conversions cannot box return values, including unconstrained type parameters + return RequiresBoxing ? $"static () => {MethodName}()" : MethodName; } } } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs index 1c4b5e10e..7ab80d1b8 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.Emit; using Microsoft.VisualStudio.TestTools.UnitTesting; using Windows.Foundation; using Windows.UI.ViewManagement; @@ -55,6 +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. public static void VerifySources(string source, (string Filename, string Source) result, LanguageVersion languageVersion = LanguageVersion.CSharp13) { RunGenerator(source, out Compilation compilation, out ImmutableArray diagnostics, languageVersion); @@ -70,6 +72,27 @@ public static void VerifySources(string source, (string Filename, string Source) Assert.AreEqual(expectedText, actualText); } + /// + /// Verifies that the resulting sources produced by a source generator can be emitted. + /// + /// The input source to process. + /// The language version to use to run the test. + /// 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); + + CollectionAssert.AreEquivalent(Array.Empty(), diagnostics); + + using MemoryStream stream = new(); + + EmitResult result = compilation.Emit(stream); + + Assert.IsTrue(result.Success, $"Failed to emit generated sources:{Environment.NewLine}{string.Join(Environment.NewLine, result.Diagnostics)}"); + + return compilation; + } + /// /// Verifies the incremental generator steps for a given source generator. /// diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.Compilation.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.Compilation.cs new file mode 100644 index 000000000..b81845868 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.Compilation.cs @@ -0,0 +1,158 @@ +// 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.Collections.Generic; +using System.IO; +using System.Linq; +using CommunityToolkit.GeneratedDependencyProperty.Tests.Helpers; +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] + [DataRow("int", "42")] + [DataRow("int?", "42")] + [DataRow("bool", "true")] + [DataRow("string", "\"Hello world!\"")] + [DataRow("int", null)] + [DataRow("int?", null)] + [DataRow("bool", null)] + [DataRow("string", null)] + public void SingleProperty_WithLocalCache_Compiles(string propertyType, string? defaultValue) + { + string defaultValueArgument = defaultValue is null ? "" : $", DefaultValue = {defaultValue}"; + + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty(IsLocalCacheEnabled = true{{defaultValueArgument}})] + public partial {{propertyType}} Value { get; set; } + } + """; + + Compilation compilation = CSharpGeneratorTest.VerifyCompiles(source, LanguageVersion.Preview); + + PropertyDeclarationSyntax property = compilation.SyntaxTrees + .Single(tree => Path.GetFileName(tree.FilePath) == "MyNamespace.MyControl.g.cs") + .GetRoot() + .DescendantNodes() + .OfType() + .Single(); + + Assert.AreEqual(defaultValue, property.Initializer?.Value.ToString()); + } + + [TestMethod] + [DynamicData(nameof(GetDefaultValueCallbackCompilationData), DynamicDataSourceType.Method)] + public void SingleProperty_WithDefaultValueCallback_Compiles( + string propertyType, + string returnType, + string returnValue, + string constraints, + bool requiresBoxing, + bool isGeneric, + bool hasPropertyChangedCallback, + bool hasSharedPropertyChangedCallback) + { + string typeParameters = isGeneric ? "" : ""; + string propertyChangedCallback = hasPropertyChangedCallback + ? "partial void OnValuePropertyChanged(DependencyPropertyChangedEventArgs e) { }" + : ""; + string sharedPropertyChangedCallback = hasSharedPropertyChangedCallback + ? "partial void OnPropertyChanged(DependencyPropertyChangedEventArgs e) { }" + : ""; + + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyNamespace; + + public partial class MyControl{{typeParameters}} : DependencyObject {{constraints}} + { + [GeneratedDependencyProperty(DefaultValueCallback = nameof(CreateValue))] + public partial {{propertyType}} Value { get; set; } + + private static {{returnType}} CreateValue() => {{returnValue}}; + + {{propertyChangedCallback}} + {{sharedPropertyChangedCallback}} + } + """; + + Compilation compilation = CSharpGeneratorTest.VerifyCompiles(source); + + ArgumentSyntax callbackArgument = compilation.SyntaxTrees + .SelectMany(tree => tree.GetRoot().DescendantNodes()) + .OfType() + .Single(argument => argument.NameColon?.Name.Identifier.ValueText == "createDefaultValueCallback"); + + string expectedCallback = requiresBoxing ? "static () => CreateValue()" : "CreateValue"; + + Assert.AreEqual($"new Windows.UI.Xaml.CreateDefaultValueCallback({expectedCallback})", callbackArgument.Expression.ToString()); + } + + public static IEnumerable GetDefaultValueCallbackCompilationData() + { + (string PropertyType, string ReturnType, string ReturnValue, string Constraints, bool RequiresBoxing)[] cases = + [ + ("int", "int", "42", "", true), + ("int", "object", "42", "", false), + ("int?", "int?", "42", "", true), + ("int?", "int?", "null", "", true), + ("int?", "int", "42", "", true), + ("int?", "object?", "null", "", false), + ("string", "string", "\"Hello world!\"", "", false), + ("string", "object", "\"Hello world!\"", "", false), + ("T", "T", "default!", "", true), + ("T", "T", "default", "where T : struct", true), + ("T?", "T?", "null", "where T : struct", true), + ("T?", "T", "default", "where T : struct", true), + ("T", "T", "default!", "where T : System.IComparable", true), + ("T", "T", "default!", "where T : class", false), + ("T", "T", "default!", "where T : System.IO.Stream", false) + ]; + + foreach (var (propertyType, returnType, returnValue, constraints, requiresBoxing) in cases) + { + foreach (bool isGeneric in new[] { false, true }) + { + if (!isGeneric && propertyType is "T" or "T?") + { + continue; + } + + foreach (bool hasPropertyChangedCallback in new[] { false, true }) + { + foreach (bool hasSharedPropertyChangedCallback in new[] { false, true }) + { + yield return + [ + propertyType, + returnType, + returnValue, + constraints, + requiresBoxing, + isGeneric, + hasPropertyChangedCallback, + hasSharedPropertyChangedCallback + ]; + } + } + } + } + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs index c56ca9688..c4b7250a5 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs @@ -372,8 +372,8 @@ public partial int Number OnNumberChanged(value); OnNumberChanged(__oldValue, value); - } = 42; - } + } + } = 42; /// Executes the logic for when the accessor is invoked /// The boxed property value that has been produced before assigning to . @@ -3751,10 +3751,10 @@ file sealed class PropertyChangedUnsafeAccessors } [TestMethod] - [DataRow("int")] - [DataRow("object")] - [DataRow("object?")] - public void SingleProperty_Int32_WithNoCaching_WithDefaultValueCallback(string returnType) + [DataRow("int", "static () => CreateNumber()")] + [DataRow("object", "CreateNumber")] + [DataRow("object?", "CreateNumber")] + public void SingleProperty_Int32_WithNoCaching_WithDefaultValueCallback(string returnType, string callback) { string source = $$""" using CommunityToolkit.WinUI; @@ -3790,7 +3790,7 @@ partial class MyControl propertyType: typeof(int), ownerType: typeof(MyControl), typeMetadata: global::Windows.UI.Xaml.PropertyMetadata.Create( - createDefaultValueCallback: new Windows.UI.Xaml.CreateDefaultValueCallback(CreateNumber))); + createDefaultValueCallback: new Windows.UI.Xaml.CreateDefaultValueCallback({{callback}}))); /// [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] @@ -3869,11 +3869,11 @@ public partial int Number } [TestMethod] - [DataRow("int")] - [DataRow("int?")] - [DataRow("object")] - [DataRow("object?")] - public void SingleProperty_NullableOfInt32_WithNoCaching_WithDefaultValueCallback(string returnType) + [DataRow("int", "static () => CreateNumber()")] + [DataRow("int?", "static () => CreateNumber()")] + [DataRow("object", "CreateNumber")] + [DataRow("object?", "CreateNumber")] + public void SingleProperty_NullableOfInt32_WithNoCaching_WithDefaultValueCallback(string returnType, string callback) { string source = $$""" using CommunityToolkit.WinUI; @@ -3909,7 +3909,7 @@ partial class MyControl propertyType: typeof(int?), ownerType: typeof(MyControl), typeMetadata: global::Windows.UI.Xaml.PropertyMetadata.Create( - createDefaultValueCallback: new Windows.UI.Xaml.CreateDefaultValueCallback(CreateNumber))); + createDefaultValueCallback: new Windows.UI.Xaml.CreateDefaultValueCallback({{callback}}))); /// [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator_Incrementality.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator_Incrementality.cs index eebe7591c..70546ac4e 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator_Incrementality.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator_Incrementality.cs @@ -12,6 +12,38 @@ namespace CommunityToolkit.GeneratedDependencyProperty.Tests; [TestClass] public class Test_DependencyPropertyGenerator_Incrementality { + [TestMethod] + [DataRow("int", "object")] + [DataRow("object", "int")] + public void ModifiedDefaultValueCallbackReturnType_ModifiesOutput(string returnType, string updatedReturnType) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty(DefaultValueCallback = nameof(CreateNumber))] + public partial int Number { get; set; } + + private static {{returnType}} CreateNumber() => 42; + } + """; + + string updatedSource = source.Replace( + $"private static {returnType} CreateNumber()", + $"private static {updatedReturnType} CreateNumber()"); + + CSharpGeneratorTest.VerifyIncrementalSteps( + source, + updatedSource, + executeReason: IncrementalStepRunReason.Modified, + outputReason: IncrementalStepRunReason.Modified, + sourceReason: IncrementalStepRunReason.Modified); + } + [TestMethod] public void ModifiedOptions_ModifiesOutput() { diff --git a/components/DependencyPropertyGenerator/samples/DependencyPropertyGenerator.Samples.csproj b/components/DependencyPropertyGenerator/samples/DependencyPropertyGenerator.Samples.csproj index c7af9907c..1d99cd0a1 100644 --- a/components/DependencyPropertyGenerator/samples/DependencyPropertyGenerator.Samples.csproj +++ b/components/DependencyPropertyGenerator/samples/DependencyPropertyGenerator.Samples.csproj @@ -3,6 +3,9 @@ DependencyPropertyGenerator + + + $(NoWarn);TKSMPL0014;TKSMPL0013; diff --git a/components/DependencyPropertyGenerator/samples/DependencyPropertyGenerator.md b/components/DependencyPropertyGenerator/samples/DependencyPropertyGenerator.md new file mode 100644 index 000000000..ddf1c94a1 --- /dev/null +++ b/components/DependencyPropertyGenerator/samples/DependencyPropertyGenerator.md @@ -0,0 +1,485 @@ +--- +title: DependencyPropertyGenerator +author: Sergio0694 +description: Generate dependency properties for WinUI 3 and UWP controls with partial properties, configurable defaults, callbacks, and optional local caching. +keywords: DependencyPropertyGenerator, GeneratedDependencyProperty, DependencyProperty, SourceGenerator, WinUI, UWP, XAML +dev_langs: + - csharp +category: Helpers +subcategory: Developer +experimental: true +discussion-id: 449 +issue-id: 621 +icon: Assets/icon.png +--- + +# DependencyPropertyGenerator + +The `DependencyPropertyGenerator` creates the registration, identifier field, and CLR property implementation for a XAML dependency property. Add `[GeneratedDependencyProperty]` to a partial property instead of writing the `DependencyProperty.Register`, `GetValue`, and `SetValue` boilerplate yourself. + +Generated properties participate in the existing XAML property system, including bindings, styles, and property-changed notifications. The generator also provides optional partial methods for customizing accessors and observing changes, configurable default values, and local caching for scenarios where all writes go through the CLR property. + +> [!NOTE] +> This component is an experiment in Windows Community Toolkit Labs. Its API and behavior can change before graduation to the Windows Community Toolkit. + +## Getting started + +Install the package that matches your application's XAML framework from the [Community Toolkit Labs NuGet feed](https://aka.ms/toolkit/wiki/previewpackages): + +| Framework | Package | XAML namespace | +| --- | --- | --- | +| WinUI 3 / Windows App SDK | `CommunityToolkit.Labs.WinUI.DependencyPropertyGenerator` | `Microsoft.UI.Xaml` | +| UWP | `CommunityToolkit.Labs.Uwp.DependencyPropertyGenerator` | `Windows.UI.Xaml` | + +Both packages expose the attribute in the `CommunityToolkit.WinUI` namespace. Install the package in the project that declares the generated properties, not just in an application that references that project. + +Use a compiler with Roslyn 4.12 or later, such as the .NET 9 SDK and Visual Studio 2022 version 17.12 or later. Partial properties require C# 13 or later. The compiler requirement does not mean your application must target .NET 9: the generator also supports the UWP and .NET target frameworks provided by the package. + +Set the language version explicitly if your project's target framework would otherwise select an older version: + +```xml + + 13.0 + enable + +``` + +The examples on this page use WinUI 3. For UWP, replace `Microsoft.UI.Xaml` and `Microsoft.UI.Xaml.Controls` with `Windows.UI.Xaml` and `Windows.UI.Xaml.Controls`. + +## Declaring a dependency property + +The containing class must be `partial` and derive from `DependencyObject`, directly or through a type such as `Control`, `UserControl`, or `Page`. Declare an instance partial property with `get;` and `set;` accessors and let the generator provide its implementation: + +```csharp +using CommunityToolkit.WinUI; +using Microsoft.UI.Xaml.Controls; + +namespace MyApp.Controls; + +public partial class CounterControl : Control +{ + [GeneratedDependencyProperty] + public partial int Count { get; set; } + + [GeneratedDependencyProperty(DefaultValue = "Ready")] + public partial string Caption { get; set; } +} +``` + +For `Count`, the generator registers a dependency property named `"Count"`, with `typeof(int)` as the property type and `typeof(CounterControl)` as the owner type. It also creates the public static readonly `CountProperty` identifier and implements the CLR accessors. `Caption` similarly gets a `CaptionProperty` identifier. + +Use the properties like any other dependency properties. For example, after mapping the `local` XAML namespace to `MyApp.Controls`: + +```xml + +``` + +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. + +## Attribute options + +| Option | Purpose | Default behavior | +| --- | --- | --- | +| `DefaultValue` | Specify a constant default value for the property's metadata. | Use the property's default value, as described below. | +| `DefaultValueCallback` | Name a static factory that supplies an instance's default value. | No factory. | +| `PropertyType` | Override the type used to register the property in metadata. | Match the declared property type. | +| `IsLocalCacheEnabled` | Store the CLR property's value in a compiler-generated backing field. | Disabled. | + +`DefaultValue` and `DefaultValueCallback` are mutually exclusive, including when `DefaultValue` is explicitly set to `null`. + +## Default values + +When neither default-value option is specified, reference types and nullable value types default to `null`, and non-nullable value types default to their zero-initialized value. For example, `int` defaults to `0`, `bool` defaults to `false`, and `int?` defaults to `null`. For generic properties, the generator accounts for the type parameter's constraints and uses `default(T)` where needed. + +The generator may omit explicit `PropertyMetadata` when the XAML property system already supplies the required default and there are no metadata callbacks. An omitted metadata object is an implementation optimization, not a request for an arbitrary default. + +### Constant defaults + +Use `DefaultValue` for values that can be represented in an attribute argument: + +```csharp +using CommunityToolkit.WinUI; +using Microsoft.UI.Xaml; + +public partial class DefaultValues : DependencyObject +{ + [GeneratedDependencyProperty(DefaultValue = 42)] + public partial int Count { get; set; } + + [GeneratedDependencyProperty(DefaultValue = true)] + public partial bool IsActive { get; set; } + + [GeneratedDependencyProperty(DefaultValue = "Ready")] + public partial string Caption { get; set; } + + [GeneratedDependencyProperty(DefaultValue = 42)] + public partial int? OptionalCount { get; set; } + + [GeneratedDependencyProperty(DefaultValue = Visibility.Collapsed)] + public partial Visibility DisplayVisibility { get; set; } +} +``` + +Match the constant's type to the declared property type, or to the underlying type of a nullable value type. For example, use `42d` for a `double` property rather than the `int` constant `42`. Boxing a constant into the attribute's `object` parameter does not change the type of that constant. + +Use a factory for defaults that cannot be expressed as attribute arguments, such as a new collection or a custom object. + +### Default-value factories + +`DefaultValueCallback` names a method in the same containing type. Use `nameof` so renaming the method also updates the attribute: + +```csharp +using System.Collections.ObjectModel; +using CommunityToolkit.WinUI; +using Microsoft.UI.Xaml; + +public partial class CollectionOwner : DependencyObject +{ + [GeneratedDependencyProperty(DefaultValueCallback = nameof(CreateItems))] + public partial ObservableCollection Items { get; set; } + + private static ObservableCollection CreateItems() => new(); + + [GeneratedDependencyProperty(DefaultValueCallback = nameof(CreateOptionalCount))] + public partial int? OptionalCount { get; set; } + + private static int CreateOptionalCount() => 42; +} +``` + +The factory must be static, parameterless, and callable without type arguments. Its return type must be the exact property type, `object`, or, for a nullable value-type property, the underlying value type. For example, an `int?` property can use an `int?`, `int`, or `object` factory. An object-returning factory is responsible for returning a value compatible with the property. + +The factory is registered with `PropertyMetadata.Create`; the XAML property system requests the default for each owning instance. This makes a factory suitable for defaults that should not be shared, such as the collection in this example. The factory is not a CLR getter callback, and it has no access to the owning instance through a `this` reference or a callback parameter. + +Value-type and unconstrained generic results are boxed by the generated callback adapter when necessary. Reference-type and object-returning factories can be used directly. A factory can be combined with either or both of the property-changed callbacks described below. + +Keep local caching disabled for properties that use `DefaultValueCallback`: the factory initializes dependency-property storage, not the CLR property's cached field. + +### The unset-value sentinel + +For advanced scenarios, use `GeneratedDependencyProperty.UnsetValue` as the `DefaultValue` argument: + +```csharp +using CommunityToolkit.WinUI; +using Microsoft.UI.Xaml; + +public partial class OptionalValueOwner : DependencyObject +{ + [GeneratedDependencyProperty(DefaultValue = GeneratedDependencyProperty.UnsetValue)] + public partial object? Value { get; set; } +} +``` + +The generator replaces this attribute-only placeholder with the XAML framework's actual `DependencyProperty.UnsetValue` sentinel. It is not equivalent to `DefaultValue = null`. Do not read or compare against `GeneratedDependencyProperty.UnsetValue` in application code; use `DependencyProperty.UnsetValue` there. + +This option cannot be combined with local caching. For a strongly typed property, ensure that its getter can handle any value supplied by the property system before casting it, using the boxed getter hook when necessary. + +## Customizing the metadata type + +`PropertyType` changes the type passed to `DependencyProperty.Register`, without changing the CLR property type: + +```csharp +using CommunityToolkit.WinUI; +using Microsoft.UI.Xaml; + +public partial class SelectionOwner : DependencyObject +{ + [GeneratedDependencyProperty(PropertyType = typeof(object))] + public partial bool? IsSelected { get; set; } +} +``` + +This can help with classic reflection-based XAML binding scenarios that need a nullable value-type property registered as `object`. Prefer the default registration unless a specific scenario requires an override. + +The metadata type must be compatible with the declared type. The analyzer rejects incompatible types and warns when an explicit type is redundant. The option does not add a value converter: in this example, values read through the CLR getter must still be compatible with `bool?`. + +## Receiving property-changed notifications + +There are two different kinds of notifications: callbacks registered with the dependency-property system, and hooks in the generated CLR accessors. Choose the dependency-property callbacks when changes from bindings, styles, animations, or direct `SetValue` calls must be observed. + +Implement `OnPropertyChanged` to observe one generated property. Implement `OnPropertyChanged` to observe all generated properties declared in the same containing type: + +```csharp +using System.Diagnostics; +using CommunityToolkit.WinUI; +using Microsoft.UI.Xaml; + +public partial class ObservedValues : DependencyObject +{ + [GeneratedDependencyProperty] + public partial int Count { get; set; } + + [GeneratedDependencyProperty(DefaultValue = "Ready")] + public partial string Caption { get; set; } + + partial void OnCountPropertyChanged(DependencyPropertyChangedEventArgs e) + { + Debug.WriteLine($"Count changed from {e.OldValue} to {e.NewValue}."); + } + + partial void OnPropertyChanged(DependencyPropertyChangedEventArgs e) + { + Debug.WriteLine($"{e.Property} changed."); + } +} +``` + +When both callbacks are implemented, the property-specific callback runs before the shared callback. `OnPropertyChanged` is not a subscription to every dependency property on the object, including unrelated or inherited properties: it is wired into the metadata for this type's generated properties. + +The generator only registers the callbacks you implement. Implement the partial methods without an explicit accessibility modifier, matching the generated signature. + +## Customizing CLR accessors + +Accessor hooks run only when application code invokes the generated CLR property. They are not a replacement for dependency-property callbacks, because the XAML property system can bypass CLR accessors. + +For an `int` property named `Count`, these hooks are available without local caching: + +| Hook | When it runs | +| --- | --- | +| `OnCountGet(ref object propertyValue)` | After `GetValue`, before the result is cast to `int`. | +| `OnCountGet(ref int propertyValue)` | After the cast, before the getter returns. | +| `OnCountSet(ref int propertyValue)` | Before the setter writes the value. | +| `OnCountSet(ref object propertyValue)` | After boxing, before `SetValue`, on the boxed setter path. | +| `OnCountChanged(int newValue)` | After the CLR setter writes the value. | + +Implement either, both, or none of the typed and boxed hooks. Hooks with `ref` parameters can replace the value. For example, this implementation normalizes assignments made through `Count`: + +```csharp +using System; +using CommunityToolkit.WinUI; +using Microsoft.UI.Xaml; + +public partial class NormalizedCounter : DependencyObject +{ + [GeneratedDependencyProperty] + public partial int Count { get; set; } + + partial void OnCountSet(ref int propertyValue) + { + propertyValue = Math.Max(0, propertyValue); + } +} +``` + +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. + +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. + +Without local caching, the setter does not compare old and new values before running `OnCountChanged(int newValue)`. That hook can therefore run on repeated assignments of the same value. In contrast, `OnCountPropertyChanged` follows the XAML property's effective-value change notifications. + +Unimplemented partial hooks and their calls are removed by the C# compiler. Do not call a hook directly to change a dependency property; assign the property or use the XAML property APIs. + +> [!IMPORTANT] +> Setter normalization is not system-wide dependency-property coercion. A binding, animation, or direct `SetValue` call can bypass `OnCountSet`. Keep this distinction in mind when enforcing invariants. + +## Local caching + +Local caching is an opt-in optimization for properties whose values are read and written exclusively through the generated CLR property. It stores the value in a compiler-generated backing field, avoids `GetValue` on reads, and skips equal-value writes. + +The current generator requires C# `preview` when `IsLocalCacheEnabled` is enabled, because the generated implementation uses the `field` keyword: + +```xml + + preview + +``` + +```csharp +using System.Diagnostics; +using CommunityToolkit.WinUI; +using Microsoft.UI.Xaml; + +public partial class CachedCounter : DependencyObject +{ + [GeneratedDependencyProperty(IsLocalCacheEnabled = true, DefaultValue = 42)] + public partial int Count { get; set; } + + partial void OnCountChanging(int newValue) + { + Debug.WriteLine($"About to assign {newValue}."); + } + + partial void OnCountChanging(int oldValue, int newValue) + { + Debug.WriteLine($"Changing from {oldValue} to {newValue}."); + } + + partial void OnCountChanged(int oldValue, int newValue) + { + Debug.WriteLine($"Assigned {newValue}, replacing {oldValue}."); + } +} +``` + +The generated setter first invokes the typed `OnCountSet` hook, then compares the new value with the cached value using `EqualityComparer.Default`. If they are equal, it returns without writing to the property system or invoking the changing/changed hooks. Otherwise, it invokes both `OnCountChanging` overloads, updates the field, writes to the dependency-property system, and invokes `OnCountChanged(newValue)` followed by `OnCountChanged(oldValue, newValue)`. + +The getter returns the cached field directly, so getter hooks are not generated in this mode. Constant defaults initialize both the metadata and the cached field. For reference types, the `oldValue` parameter in a two-value hook can be nullable even when the property is non-nullable; match the generated signature. + +> [!WARNING] +> Do not enable local caching when bindings, styles, animations, `SetValue`, `ClearValue`, or other code can change the value outside the CLR setter. Those changes do not synchronize the cached field, so the CLR getter can return a stale value. Registering a property-changed callback does not automatically synchronize the cache. + +Keep caching disabled for default-value factories, and do not combine it with `GeneratedDependencyProperty.UnsetValue`. If you customize a cached setter, prefer the typed `OnSet` hook so normalization happens before the field is updated. Changing only the boxed value later in the setter can make the cached and dependency-property values differ. + +Local caching does not remove the XAML property system or its threading requirements, and changed values still need to be written to that system. + +## Nullable reference types + +Nullability annotations describe a property's contract; they do not automatically prevent the XAML property system from storing `null`. For a non-nullable reference property, provide a non-null default, use a non-null-returning factory, mark the property `required` when appropriate, or make the getter handle null values. Otherwise, declare the property as nullable. + +The analyzers understand `[MaybeNull]`, `[NotNull]`, `[AllowNull]`, and `[DisallowNull]` from `System.Diagnostics.CodeAnalysis`. When a hook guarantees a non-null value, annotate its `ref` parameter with `[NotNull]` and implement the corresponding normalization: + +```csharp +using System.Diagnostics.CodeAnalysis; +using CommunityToolkit.WinUI; +using Microsoft.UI.Xaml; + +public partial class CaptionOwner : DependencyObject +{ + [GeneratedDependencyProperty(DefaultValue = "")] + [AllowNull] + public partial string Caption { get; set; } + + partial void OnCaptionGet([NotNull] ref string propertyValue) + { + propertyValue ??= string.Empty; + } + + partial void OnCaptionSet([NotNull] ref string propertyValue) + { + propertyValue ??= string.Empty; + } +} +``` + +The getter hook makes this CLR getter resilient even if a null value reaches dependency-property storage without passing through the CLR setter. It changes the returned value, not the value stored by the XAML property system. Keep local caching disabled when using getter hooks. + +Similarly, adding `[NotNull]` to a nullable property requires an implementation that actually guarantees a non-null result. Attributes alone do not add runtime checks. The `required` modifier enforces C# initialization rules; it does not prevent later writes through XAML property APIs. + +## Attributes on the generated identifier + +Normal property attributes apply to the CLR property. To forward an attribute to the generated dependency-property identifier field, use an attribute list with the `static:` target: + +```csharp +using System.ComponentModel; +using CommunityToolkit.WinUI; +using Microsoft.UI.Xaml; + +public partial class AttributedValueOwner : DependencyObject +{ + [GeneratedDependencyProperty] + [static: EditorBrowsable(EditorBrowsableState.Never)] + public partial string? Caption { get; set; } +} +``` + +Here, `EditorBrowsable` is applied to `CaptionProperty`, not `Caption`. Forwarded attributes must be valid for fields, and their types and arguments must resolve correctly. The package includes a diagnostic suppressor for the compiler warning caused by this generator-specific `static:` target. + +## Accessibility, modifiers, and generic types + +The generator preserves the CLR property's accessibility and supported modifiers, including `required`, `new`, `virtual`, `override`, and `sealed`, subject to normal C# rules. An accessor can have a more restrictive accessibility where C# permits it, such as `public partial int Count { get; private set; }`. The generated dependency-property identifier is still a public static readonly field. + +Generic containing types and nested types are supported. Every containing type must be partial. For example, a generic owner can use a factory returning its type parameter: + +```csharp +using CommunityToolkit.WinUI; +using Microsoft.UI.Xaml; + +public partial class ItemOwner : DependencyObject + where T : class, new() +{ + [GeneratedDependencyProperty(DefaultValueCallback = nameof(CreateItem))] + public partial T Item { get; set; } + + private static T CreateItem() => new(); +} +``` + +Support for a generic C# owner does not imply that the XAML markup language can instantiate that generic type. Normal framework and XAML restrictions still apply. + +Only instance partial property definitions with a getter and a non-init setter are supported. The generator does not generate attached properties, static properties, get-only properties, `init` accessors, or properties returning pointers, by-reference values, or byref-like types. Name a CLR property `Count`, not `CountProperty`; the generator adds the `Property` suffix to its identifier. + +## Advanced build configuration + +### XAML namespace selection + +The package selects UWP XAML automatically for UAP targets and projects with `UseUwp` enabled; otherwise it selects WinUI 3 XAML. If a custom build setup needs an explicit selection, set `DependencyPropertyGeneratorUseWindowsUIXaml`: + +| Value | Generated XAML types | +| --- | --- | +| `true` | `Windows.UI.Xaml` | +| `false` | `Microsoft.UI.Xaml` | + +For example: + +```xml + + true + +``` + +This setting controls generation and analysis; it does not add references to another XAML framework. It must match the framework used by the containing `DependencyObject`. + +### Embedded mode + +Library authors can use the generator without taking a runtime dependency on the package's attribute assembly. Exclude the runtime library from the package reference and enable generation of internal helper types in the consuming project. + +Update your existing package reference as follows, replacing `PACKAGE_VERSION` with the version you have selected. Use the UWP package name instead for a UWP project: + +```xml + + + + + + true + true + +``` + +`EnableGeneratedDependencyPropertyAttributeEmbeddedMode` emits the internal `GeneratedDependencyPropertyAttribute` type. `EnableGeneratedDependencyPropertyEmbeddedMode` emits the internal `GeneratedDependencyProperty` helper for the `UnsetValue` placeholder; it is only needed if you use that helper. Both options are disabled by default. + +Do not combine embedded types with a reference to the package's runtime attribute assembly. Embedded mode removes that Toolkit assembly dependency, not the dependency on your application's XAML framework. + +The embedded attribute's applications are omitted from compiled metadata by default. If you specifically need to preserve them, define `GENERATED_DEPENDENCY_PROPERTY_PRIVATE_ASSETS_ALL_PRESERVE_ATTRIBUTES`: + +```xml + + $(DefineConstants);GENERATED_DEPENDENCY_PROPERTY_PRIVATE_ASSETS_ALL_PRESERVE_ATTRIBUTES + +``` + +## Migrating existing dependency properties + +The package includes analyzers and code fixes for existing handwritten dependency properties. When a supported `DependencyProperty.Register` declaration and its CLR wrapper can be replaced, diagnostic `WCTDPG0017` offers the **Use a partial property** code fix. The fixer can carry supported default values, metadata types, and field attributes into the generated declaration. + +Use the code fix when it is offered rather than assuming every custom registration pattern is supported. Review any existing accessor logic and metadata callbacks, and move custom behavior into the appropriate hooks. In particular, preserve the distinction between CLR setter behavior and dependency-property change notifications. + +Additional code fixes offer **Declare dependency property as field** and **Declare dependency property field correctly** for handwritten identifiers that do not follow the recommended field declaration pattern. The analyzers also check registration names, owner types, property types, and default values on manual declarations. + +## Troubleshooting + +| Diagnostic or symptom | What to check | +| --- | --- | +| `WCTDPCFG0002`, or partial properties have no generated implementation | Use Roslyn 4.12 or later, ensure the generator package is referenced by the declaring project, and check earlier build diagnostics. | +| `WCTDPG0006` | Select C# 13 or later. | +| `WCTDPG0007` | Select `preview` when using local caching. | +| `WCTDPG0001` through `WCTDPG0005`, or `WCTDPG0012` | Use an incomplete instance partial property with `get; set;`, a supported property type, and a partial owner deriving from the correct `DependencyObject`. | +| `WCTDPG0008` | Avoid a property named `Property` with type `object` or `DependencyPropertyChangedEventArgs`, which would cause generated hook name collisions. | +| `WCTDPG0009`, `WCTDPG0024`, or `WCTDPG0025` | Make the declared nullability contract consistent with defaults and accessor implementations. | +| `WCTDPG0010` or `WCTDPG0011` | Use a compatible default-value type, including the correct numeric literal type. | +| `WCTDPG0013`, `WCTDPG0014`, or `WCTDPG0015` | Specify only one default-value option and use a static, parameterless factory in the same type with a supported return type. | +| `WCTDPG0018` or `WCTDPG0019` | Check the types and argument expressions in forwarded `static:` attributes. | +| `WCTDPG0022` or `WCTDPG0023` | Remove a redundant `PropertyType` override or replace an incompatible metadata type. | +| `WCTDPCFG0001` | Remove the runtime attribute assembly reference when enabling embedded mode. | +| Accessor hooks do not run for a binding update | Use `OnPropertyChanged` or `OnPropertyChanged` for property-system notifications. | +| The CLR getter returns an old value | Disable local caching if any writes can bypass the CLR setter. | + +Generated files are available under the generator's entry in your IDE's analyzer/generated-source view. Inspect the generated partial declaration when matching hook signatures, especially for nullable or generic properties. + +## See also + +- [UWP custom dependency properties](/windows/uwp/xaml-platform/custom-dependency-properties) +- [DependencyPropertyGenerator experiment](https://github.com/CommunityToolkit/Labs-Windows/issues/621) +- [DependencyPropertyGenerator design discussion](https://github.com/CommunityToolkit/Labs-Windows/discussions/449) diff --git a/components/DependencyPropertyGenerator/samples/PlaceholderSample.xaml b/components/DependencyPropertyGenerator/samples/PlaceholderSample.xaml new file mode 100644 index 000000000..261c20592 --- /dev/null +++ b/components/DependencyPropertyGenerator/samples/PlaceholderSample.xaml @@ -0,0 +1,9 @@ + + diff --git a/components/DependencyPropertyGenerator/samples/PlaceholderSample.xaml.cs b/components/DependencyPropertyGenerator/samples/PlaceholderSample.xaml.cs new file mode 100644 index 000000000..518bf2421 --- /dev/null +++ b/components/DependencyPropertyGenerator/samples/PlaceholderSample.xaml.cs @@ -0,0 +1,17 @@ +// 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. + +namespace DependencyPropertyExperiment.Samples; + +/// +/// An empty page that can be used on its own or navigated to within a Frame. +/// +[ToolkitSample(id: nameof(PlaceholderSample), "Placeholder sample", description: "A sample that does not appear in the sample gallery, acts as a placeholder to work around incremental build issue for no-sample components.")] +public sealed partial class PlaceholderSample : Page +{ + public PlaceholderSample() + { + this.InitializeComponent(); + } +}