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