Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,15 @@ public static string CreateDefaultValueCallback(bool useWindowsUIXaml)
? $"{WindowsUIXamlNamespace}.{nameof(CreateDefaultValueCallback)}"
: $"{MicrosoftUIXamlNamespace}.{nameof(CreateDefaultValueCallback)}";
}

/// <summary>
/// Gets the fully qualified type name for the <c>XamlBindingHelper</c> type.
/// </summary>
/// <param name="useWindowsUIXaml"><inheritdoc cref="XamlNamespace(bool)" path="/param[@name='useWindowsUIXaml']/text()"/></param>
public static string XamlBindingHelper(bool useWindowsUIXaml)
{
return useWindowsUIXaml
? $"{WindowsUIXamlNamespace}.Markup.{nameof(XamlBindingHelper)}"
: $"{MicrosoftUIXamlNamespace}.Markup.{nameof(XamlBindingHelper)}";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,60 @@ public static bool IsSharedPropertyChangedCallbackImplemented(IPropertySymbol pr
return false;
}

/// <summary>
/// Checks whether the user has provided an implementation for the <c>On&lt;PROPERTY_NAME&gt;Set(ref object)</c> partial method.
/// </summary>
/// <param name="propertySymbol">The input <see cref="IPropertySymbol"/> instance to process.</param>
/// <returns>Whether the user has an implementation for the boxed set callback.</returns>
public static bool IsObjectSetCallbackImplemented(IPropertySymbol propertySymbol)
{
// Check for any 'On<PROPERTY_NAME>Set' methods with a single ref 'object' parameter
foreach (ISymbol symbol in propertySymbol.ContainingType.GetMembers($"On{propertySymbol.Name}Set"))
{
if (symbol is IMethodSymbol { IsStatic: false, ReturnsVoid: true, Parameters: [{ RefKind: RefKind.Ref, Type.SpecialType: SpecialType.System_Object }] })
{
return true;
}
}

return false;
}

/// <summary>
/// Gets the <c>XamlBindingHelper.SetPropertyFrom*</c> method name for a given property type, if supported.
/// </summary>
/// <param name="typeSymbol">The input <see cref="ITypeSymbol"/> to check.</param>
/// <returns>The method name to use, or <see langword="null"/> if the type is not supported.</returns>
public static string? GetXamlBindingHelperSetMethodName(ITypeSymbol typeSymbol)
{
// Check for well known primitive types first (these are the most common)
switch (typeSymbol.SpecialType)
{
case SpecialType.System_Boolean: return "SetPropertyFromBoolean";
case SpecialType.System_Byte: return "SetPropertyFromByte";
case SpecialType.System_Char: return "SetPropertyFromChar16";
case SpecialType.System_Double: return "SetPropertyFromDouble";
case SpecialType.System_Int32: return "SetPropertyFromInt32";
case SpecialType.System_Int64: return "SetPropertyFromInt64";
case SpecialType.System_Single: return "SetPropertyFromSingle";
case SpecialType.System_String: return "SetPropertyFromString";
case SpecialType.System_UInt32: return "SetPropertyFromUInt32";
case SpecialType.System_UInt64: return "SetPropertyFromUInt64";
case SpecialType.System_Object: return "SetPropertyFromObject";
default: break;
}

// Check for the remaining well known WinRT projected types
if (typeSymbol.HasFullyQualifiedMetadataName("System.DateTimeOffset")) return "SetPropertyFromDateTime";
if (typeSymbol.HasFullyQualifiedMetadataName("System.TimeSpan")) return "SetPropertyFromTimeSpan";
if (typeSymbol.HasFullyQualifiedMetadataName("Windows.Foundation.Point")) return "SetPropertyFromPoint";
if (typeSymbol.HasFullyQualifiedMetadataName("Windows.Foundation.Rect")) return "SetPropertyFromRect";
if (typeSymbol.HasFullyQualifiedMetadataName("Windows.Foundation.Size")) return "SetPropertyFromSize";
if (typeSymbol.HasFullyQualifiedMetadataName("System.Uri")) return "SetPropertyFromUri";

return null;
}

/// <summary>
/// Gathers all forwarded attributes for the generated property.
/// </summary>
Expand Down Expand Up @@ -708,21 +762,39 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility)
On{{propertyInfo.PropertyName}}Changing(__oldValue, value);

field = value;

object? __boxedValue = value;
""", isMultiline: true);
writer.WriteLineIf(propertyInfo.TypeName != "object", $"""

On{propertyInfo.PropertyName}Set(ref __boxedValue);
""", isMultiline: true);
writer.Write($$"""
// If an optimized 'XamlBindingHelper' method is available, use it directly
if (propertyInfo.XamlBindingHelperSetMethodName is string setMethodName)
{
writer.Write($$"""

SetValue({{propertyInfo.PropertyName}}Property, __boxedValue);
global::{{WellKnownTypeNames.XamlBindingHelper(propertyInfo.UseWindowsUIXaml)}}.{{setMethodName}}(this, {{propertyInfo.PropertyName}}Property, value);

On{{propertyInfo.PropertyName}}Changed(value);
On{{propertyInfo.PropertyName}}Changed(__oldValue, value);
}
""", isMultiline: true);
On{{propertyInfo.PropertyName}}Changed(value);
On{{propertyInfo.PropertyName}}Changed(__oldValue, value);
}
""", isMultiline: true);
}
else
{
writer.Write($$"""

object? __boxedValue = value;
""", isMultiline: true);
writer.WriteLineIf(propertyInfo.TypeName != "object", $"""

On{propertyInfo.PropertyName}Set(ref __boxedValue);
""", isMultiline: true);
writer.Write($$"""

SetValue({{propertyInfo.PropertyName}}Property, __boxedValue);

On{{propertyInfo.PropertyName}}Changed(value);
On{{propertyInfo.PropertyName}}Changed(__oldValue, value);
}
""", 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))
Expand Down Expand Up @@ -758,9 +830,34 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility)
}
""", isMultiline: true);
}
else
else if (propertyInfo.XamlBindingHelperSetMethodName is string setMethodName)
{
// Same as above but with the extra typed hook for both accessors
writer.WriteLine($$"""
{{GetExpressionWithTrailingSpace(propertyInfo.GetterAccessibility)}}get
{
object? __boxedValue = GetValue({{propertyInfo.PropertyName}}Property);

On{{propertyInfo.PropertyName}}Get(ref __boxedValue);

{{propertyInfo.TypeNameWithNullabilityAnnotations}} __unboxedValue = ({{propertyInfo.TypeNameWithNullabilityAnnotations}})__boxedValue;

On{{propertyInfo.PropertyName}}Get(ref __unboxedValue);

return __unboxedValue;
}
{{GetExpressionWithTrailingSpace(propertyInfo.SetterAccessibility)}}set
{
On{{propertyInfo.PropertyName}}Set(ref value);

global::{{WellKnownTypeNames.XamlBindingHelper(propertyInfo.UseWindowsUIXaml)}}.{{setMethodName}}(this, {{propertyInfo.PropertyName}}Property, value);

On{{propertyInfo.PropertyName}}Changed(value);
}
""", isMultiline: true);
}
else
{
writer.WriteLine($$"""
{{GetExpressionWithTrailingSpace(propertyInfo.GetterAccessibility)}}get
{
Expand All @@ -787,7 +884,6 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility)
On{{propertyInfo.PropertyName}}Changed(value);
}
""", isMultiline: true);

}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,26 @@ public void Initialize(IncrementalGeneratorInitializationContext context)

token.ThrowIfCancellationRequested();

// Get the optimized XamlBindingHelper method name for the property type, if applicable.
// This is only used when the property type is not 'object' (which would gain nothing),
// and the user hasn't provided their own 'On<PROPERTY_NAME>Set(ref object)' implementation.
string? xamlBindingHelperSetMethodName = Execute.GetXamlBindingHelperSetMethodName(propertySymbol.Type);

if (xamlBindingHelperSetMethodName is not null)
{
// Skip the optimization for the 'object' type (it gains nothing)
if (propertySymbol.Type.SpecialType == SpecialType.System_Object)
{
xamlBindingHelperSetMethodName = null;
}
else if (Execute.IsObjectSetCallbackImplemented(propertySymbol))
{
xamlBindingHelperSetMethodName = null;
}
}

token.ThrowIfCancellationRequested();

// We're using IsValueType here and not IsReferenceType to also cover unconstrained type parameter cases.
// This will cover both reference types as well T when the constraints are not struct or unmanaged.
// If this is true, it means the field storage can potentially be in a null state (even if not annotated).
Expand Down Expand Up @@ -160,6 +180,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
IsSharedPropertyChangedCallbackImplemented: isSharedPropertyChangedCallbackImplemented,
IsAdditionalTypesGenerationSupported: isAdditionalTypesGenerationSupported,
UseWindowsUIXaml: useWindowsUIXaml,
XamlBindingHelperSetMethodName: xamlBindingHelperSetMethodName,
StaticFieldAttributes: staticFieldAttributes);
})
.WithTrackingName(WellKnownTrackingNames.Execute)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ namespace CommunityToolkit.GeneratedDependencyProperty.Models;
/// <param name="IsSharedPropertyChangedCallbackImplemented">Indicates whether the WinRT-based shared property changed callback is implemented.</param>
/// <param name="IsAdditionalTypesGenerationSupported">Indicates whether additional types can be generated.</param>
/// <param name="UseWindowsUIXaml">Whether to use the UWP XAML or WinUI 3 XAML namespaces.</param>
/// <param name="XamlBindingHelperSetMethodName">The name of the <c>XamlBindingHelper.SetPropertyFrom*</c> method to use for optimized setters, if available.</param>
/// <param name="StaticFieldAttributes">The attributes to emit on the generated static field, if any.</param>
internal sealed record DependencyPropertyInfo(
HierarchyInfo Hierarchy,
Expand All @@ -44,4 +45,5 @@ internal sealed record DependencyPropertyInfo(
bool IsSharedPropertyChangedCallbackImplemented,
bool IsAdditionalTypesGenerationSupported,
bool UseWindowsUIXaml,
string? XamlBindingHelperSetMethodName,
EquatableArray<AttributeInfo> StaticFieldAttributes);
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ internal sealed class CSharpAnalyzerTest<TAnalyzer> : CSharpAnalyzerTest<TAnalyz
private CSharpAnalyzerTest(LanguageVersion languageVersion)
{
this.languageVersion = languageVersion;

TestState.AnalyzerConfigFiles.Add(("/.globalconfig", """
is_global = true
build_property.DependencyPropertyGeneratorUseWindowsUIXaml = true
"""));
}

/// <inheritdoc/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ public CSharpCodeFixTest(LanguageVersion languageVersion)
TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(DependencyProperty).Assembly.Location));
TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(GeneratedDependencyPropertyAttribute).Assembly.Location));
TestState.AnalyzerConfigFiles.Add(("/.editorconfig", "[*]\nend_of_line = lf"));
TestState.AnalyzerConfigFiles.Add(("/.globalconfig", """
is_global = true
build_property.DependencyPropertyGeneratorUseWindowsUIXaml = true
"""));
}

/// <inheritdoc/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ public static void VerifyIncrementalSteps(

GeneratorDriver driver = CSharpGeneratorDriver.Create(
generators: [new TGenerator().AsSourceGenerator()],
optionsProvider: DependencyPropertyGeneratorAnalyzerConfigOptionsProvider.Instance,
driverOptions: new GeneratorDriverOptions(IncrementalGeneratorOutputKind.None, trackIncrementalGeneratorSteps: true));

// Run the generator on the initial sources
Expand Down Expand Up @@ -166,7 +167,9 @@ private static void RunGenerator(
Compilation originalCompilation = CreateCompilation(source, languageVersion);

// Create the generator driver with the specified generator
GeneratorDriver driver = CSharpGeneratorDriver.Create(new TGenerator()).WithUpdatedParseOptions(originalCompilation.SyntaxTrees.First().Options);
GeneratorDriver driver = CSharpGeneratorDriver.Create(
generators: [new TGenerator().AsSourceGenerator()],
optionsProvider: DependencyPropertyGeneratorAnalyzerConfigOptionsProvider.Instance).WithUpdatedParseOptions(originalCompilation.SyntaxTrees.First().Options);

// Run all source generators on the input source code
_ = driver.RunGeneratorsAndUpdateCompilation(originalCompilation, out compilation, out diagnostics);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ public CSharpSuppressorTest(
TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(ApplicationView).Assembly.Location));
TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(DependencyProperty).Assembly.Location));
TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(GeneratedDependencyPropertyAttribute).Assembly.Location));
TestState.AnalyzerConfigFiles.Add(("/.globalconfig", """
is_global = true
build_property.DependencyPropertyGeneratorUseWindowsUIXaml = true
"""));
}

/// <inheritdoc/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// 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.Immutable;
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

namespace CommunityToolkit.GeneratedDependencyProperty.Tests.Helpers;

/// <summary>
/// A custom <see cref="AnalyzerConfigOptionsProvider"/> providing the MSBuild properties needed by the dependency property generator.
/// </summary>
internal sealed class DependencyPropertyGeneratorAnalyzerConfigOptionsProvider : AnalyzerConfigOptionsProvider
{
/// <summary>
/// The singleton <see cref="DependencyPropertyGeneratorAnalyzerConfigOptionsProvider"/> instance.
/// </summary>
public static DependencyPropertyGeneratorAnalyzerConfigOptionsProvider Instance { get; } = new();

/// <inheritdoc/>
public override AnalyzerConfigOptions GlobalOptions { get; } = new SimpleAnalyzerConfigOptions(
ImmutableDictionary<string, string>.Empty.Add("build_property.DependencyPropertyGeneratorUseWindowsUIXaml", "true"));

/// <inheritdoc/>
public override AnalyzerConfigOptions GetOptions(SyntaxTree tree)
{
return SimpleAnalyzerConfigOptions.Empty;
}

/// <inheritdoc/>
public override AnalyzerConfigOptions GetOptions(AdditionalText textFile)
{
return SimpleAnalyzerConfigOptions.Empty;
}

/// <summary>
/// A simple <see cref="AnalyzerConfigOptions"/> implementation backed by an immutable dictionary.
/// </summary>
/// <param name="options">The dictionary of options.</param>
private sealed class SimpleAnalyzerConfigOptions(ImmutableDictionary<string, string> options) : AnalyzerConfigOptions
{
/// <summary>
/// An empty <see cref="SimpleAnalyzerConfigOptions"/> instance.
/// </summary>
public static SimpleAnalyzerConfigOptions Empty { get; } = new(ImmutableDictionary<string, string>.Empty);

/// <inheritdoc/>
public override bool TryGetValue(string key, [NotNullWhen(true)] out string? value)
{
return options.TryGetValue(key, out value);
}
}
}
Loading
Loading