Skip to content

Commit 4d085d3

Browse files
committed
Improve codegen for property changed callbacks
1 parent e48226c commit 4d085d3

File tree

3 files changed

+1291
-66
lines changed

3 files changed

+1291
-66
lines changed

components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs

Lines changed: 100 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -812,6 +812,9 @@ public static bool RequiresAdditionalTypes(EquatableArray<DependencyPropertyInfo
812812
/// <param name="writer">The <see cref="IndentedTextWriter"/> instance to write into.</param>
813813
public static void WriteAdditionalTypes(EquatableArray<DependencyPropertyInfo> propertyInfos, IndentedTextWriter writer)
814814
{
815+
string fullyQualifiedTypeName = propertyInfos[0].Hierarchy.GetFullyQualifiedTypeName();
816+
817+
// Define the 'PropertyChangedCallbacks' type
815818
writer.WriteLine("using global::System.Runtime.CompilerServices;");
816819
writer.WriteLine($"using global::{WellKnownTypeNames.XamlNamespace(propertyInfos[0].UseWindowsUIXaml)};");
817820
writer.WriteLine();
@@ -821,72 +824,146 @@ public static void WriteAdditionalTypes(EquatableArray<DependencyPropertyInfo> p
821824
/// </summary>
822825
""", isMultiline: true);
823826
writer.WriteGeneratedAttributes(GeneratorName);
824-
writer.WriteLine("file static class PropertyChangedCallbacks");
827+
writer.WriteLine("file sealed class PropertyChangedCallbacks");
825828

826829
using (writer.WriteBlock())
827830
{
828-
string fullyQualifiedTypeName = propertyInfos[0].Hierarchy.GetFullyQualifiedTypeName();
831+
// Shared dummy instance field (to make delegate invocations faster)
832+
writer.WriteLine("""
833+
/// <summary>Shared <see cref="PropertyChangedCallbacks"/> instance, used to speedup delegate invocations (avoids the shuffle thunks).
834+
private static readonly PropertyChangedCallbacks Instance = new();
835+
""", isMultiline: true);
836+
837+
int numberOfSharedPropertyCallbacks = propertyInfos.Count(static property => !property.IsPropertyChangedCallbackImplemented && property.IsSharedPropertyChangedCallbackImplemented);
838+
bool shouldCacheSharedPropertyChangedCallback = numberOfSharedPropertyCallbacks > 1;
839+
bool shouldGenerateSharedPropertyCallback = numberOfSharedPropertyCallbacks > 0;
840+
841+
// If the shared callback should be cached, do that here
842+
if (shouldCacheSharedPropertyChangedCallback)
843+
{
844+
writer.WriteLine();
845+
writer.WriteLine("""
846+
/// <summary>Shared <see cref="PropertyChangedCallback"/> instance, for all properties only using the shared callback.
847+
private static readonly PropertyChangedCallback SharedPropertyChangedCallback = new(Instance.OnPropertyChanged);
848+
""", isMultiline: true);
849+
}
829850

830851
// Write the public accessors to use in property initializers
831-
writer.WriteLineSeparatedMembers(propertyInfos.AsSpan(), (propertyInfo, writer) =>
852+
foreach (DependencyPropertyInfo propertyInfo in propertyInfos)
832853
{
854+
if (!propertyInfo.IsPropertyChangedCallbackImplemented && !propertyInfo.IsSharedPropertyChangedCallbackImplemented)
855+
{
856+
continue;
857+
}
858+
859+
writer.WriteLine();
833860
writer.WriteLine($$"""
834861
/// <summary>
835862
/// Gets a <see cref="PropertyChangedCallback"/> value for <see cref="{{fullyQualifiedTypeName}}.{{propertyInfo.PropertyName}}Property"/>.
836863
/// </summary>
837864
/// <returns>The <see cref="PropertyChangedCallback"/> value with the right callbacks.</returns>
838865
public static PropertyChangedCallback {{propertyInfo.PropertyName}}()
839866
{
840-
static void Invoke(object d, DependencyPropertyChangedEventArgs e)
841-
{
842-
{{fullyQualifiedTypeName}} __this = ({{fullyQualifiedTypeName}})d;
843-
844867
""", isMultiline: true);
845868
writer.IncreaseIndent();
846-
writer.IncreaseIndent();
847869

848-
// Per-property callback, if present
870+
// There are 3 possible scenarios to handle:
871+
// 1) The property uses a dedicated property changed callback. In this case we always need a dedicated stub.
872+
// 2) The property uses the shared callback only, and there's more than one property like this. Reuse the instance.
873+
// 3) This is the only property using the shared callback only. In that case, create a new delegate over it.
849874
if (propertyInfo.IsPropertyChangedCallbackImplemented)
850875
{
851-
writer.WriteLine($"On{propertyInfo.PropertyName}PropertyChanged(__this, e);");
876+
writer.WriteLine($"return new(Instance.On{propertyInfo.PropertyName}PropertyChanged);");
877+
}
878+
else if (shouldCacheSharedPropertyChangedCallback)
879+
{
880+
writer.WriteLine("return SharedPropertyChangedCallback;");
881+
}
882+
else
883+
{
884+
writer.WriteLine("return new(Instance.OnPropertyChanged);");
885+
}
886+
887+
writer.DecreaseIndent();
888+
writer.WriteLine("}");
889+
}
890+
891+
// Write the private combined
892+
foreach (DependencyPropertyInfo propertyInfo in propertyInfos)
893+
{
894+
if (!propertyInfo.IsPropertyChangedCallbackImplemented)
895+
{
896+
continue;
852897
}
853898

854-
// Shared callback, if present
899+
writer.WriteLine();
900+
writer.WriteLine($$"""
901+
/// <inheritdoc cref="cref="{{fullyQualifiedTypeName}}.OnPropertyChanged""/>
902+
private void On{{propertyInfo.PropertyName}}PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
903+
{
904+
{{fullyQualifiedTypeName}} __this = ({{fullyQualifiedTypeName}})d;
905+
906+
PropertyChangedUnsafeAccessors.On{{propertyInfo.PropertyName}}PropertyChanged(__this, e);
907+
""", isMultiline: true);
908+
909+
// Shared callback, if needed
855910
if (propertyInfo.IsSharedPropertyChangedCallbackImplemented)
856911
{
857-
writer.WriteLine("OnPropertyChanged(__this, e);");
912+
writer.IncreaseIndent();
913+
writer.WriteLine($"PropertyChangedUnsafeAccessors.On{propertyInfo.PropertyName}PropertyChanged(__this, e);");
914+
writer.DecreaseIndent();
858915
}
859916

860-
// Close the method and return the 'Invoke' method as a delegate (just one allocation here)
861-
writer.DecreaseIndent();
862-
writer.DecreaseIndent();
863-
writer.WriteLine("""
864-
}
917+
writer.WriteLine("}");
918+
}
865919

866-
return new(Invoke);
920+
// If we need to generate the shared callback, let's also generate its target method
921+
if (shouldGenerateSharedPropertyCallback)
922+
{
923+
writer.WriteLine();
924+
writer.WriteLine($$"""
925+
/// <inheritdoc cref="cref="{{fullyQualifiedTypeName}}.OnPropertyChanged""/>
926+
private void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
927+
{
928+
{{fullyQualifiedTypeName}} __this = ({{fullyQualifiedTypeName}})d;
929+
930+
PropertyChangedUnsafeAccessors.OnPropertyChanged(__this, e);
867931
}
868932
""", isMultiline: true);
869-
});
933+
}
934+
}
935+
936+
// Define the 'PropertyChangedAccessors' type
937+
writer.WriteLine();
938+
writer.WriteLine($"""
939+
/// <summary>
940+
/// Contains all unsafe accessors for <see cref="{propertyInfos[0].Hierarchy.Hierarchy[0].QualifiedName}"/>.
941+
/// </summary>
942+
""", isMultiline: true);
943+
writer.WriteGeneratedAttributes(GeneratorName);
944+
writer.WriteLine("file sealed class PropertyChangedUnsafeAccessors");
870945

946+
using (writer.WriteBlock())
947+
{
871948
// Write the accessors for all WinRT-based callbacks (not the shared one)
872949
foreach (DependencyPropertyInfo propertyInfo in propertyInfos.Where(static property => property.IsPropertyChangedCallbackImplemented))
873950
{
874-
writer.WriteLine();
951+
writer.WriteLine(skipIfPresent: true);
875952
writer.WriteLine($"""
876953
/// <inheritdoc cref="{fullyQualifiedTypeName}.On{propertyInfo.PropertyName}PropertyChanged"/>
877954
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "On{propertyInfo.PropertyName}PropertyChanged")]
878-
private static extern void On{propertyInfo.PropertyName}PropertyChanged({fullyQualifiedTypeName} _, DependencyPropertyChangedEventArgs e);
955+
public static extern void On{propertyInfo.PropertyName}PropertyChanged({fullyQualifiedTypeName} _, DependencyPropertyChangedEventArgs e);
879956
""", isMultiline: true);
880957
}
881958

882959
// Also emit one for the shared callback, if it's ever used
883960
if (propertyInfos.Any(static property => property.IsSharedPropertyChangedCallbackImplemented))
884961
{
885-
writer.WriteLine();
962+
writer.WriteLine(skipIfPresent: true);
886963
writer.WriteLine($"""
887964
/// <inheritdoc cref="{fullyQualifiedTypeName}.OnPropertyChanged"/>
888965
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnPropertyChanged")]
889-
private static extern void OnPropertyChanged({fullyQualifiedTypeName} _, DependencyPropertyChangedEventArgs e);
966+
public static extern void OnPropertyChanged({fullyQualifiedTypeName} _, DependencyPropertyChangedEventArgs e);
890967
""", isMultiline: true);
891968
}
892969
}

components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/IndentedTextWriter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ public void WriteIf(bool condition, [InterpolatedStringHandlerArgument("", nameo
227227
/// <param name="skipIfPresent">Indicates whether to skip adding the line if there already is one.</param>
228228
public void WriteLine(bool skipIfPresent = false)
229229
{
230-
if (skipIfPresent && this.builder.WrittenSpan is [.., '\n', '\n'])
230+
if (skipIfPresent && this.builder.WrittenSpan is [.., '\n' or '{', '\n'])
231231
{
232232
return;
233233
}

0 commit comments

Comments
 (0)