@@ -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 }
0 commit comments