diff --git a/src/Authoring/WinRT.SourceGenerator/AotOptimizer.cs b/src/Authoring/WinRT.SourceGenerator/AotOptimizer.cs index a901ed3fa..8c5e5ba05 100644 --- a/src/Authoring/WinRT.SourceGenerator/AotOptimizer.cs +++ b/src/Authoring/WinRT.SourceGenerator/AotOptimizer.cs @@ -101,6 +101,15 @@ public void Initialize(IncrementalGeneratorInitializationContext context) GenerateCCWForGenericInstantiation); context.RegisterImplementationSourceOutput(vtablesToAddOnLookupTable.Collect().Combine(properties), GenerateVtableLookupTable); + + var bindableCustomPropertyAttributes = context.SyntaxProvider.CreateSyntaxProvider( + static (n, _) => NeedCustomPropertyImplementation(n), + static (n, _) => n) + .Select((data, _) => GetBindableCustomProperties(data)) + .Where(static bindableCustomProperties => bindableCustomProperties != default) + .Collect() + .Combine(properties); + context.RegisterImplementationSourceOutput(bindableCustomPropertyAttributes, GenerateBindableCustomProperties); } // Restrict to non-projected classes which can be instantiated @@ -123,6 +132,14 @@ private static bool IsComponentType(SyntaxNode node) !GeneratorHelper.IsWinRTType(declaration); // Making sure it isn't an RCW we are projecting. } + private static bool NeedCustomPropertyImplementation(SyntaxNode node) + { + return node is ClassDeclarationSyntax declaration && + !declaration.Modifiers.Any(m => m.IsKind(SyntaxKind.StaticKeyword) || m.IsKind(SyntaxKind.AbstractKeyword)) && + declaration.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)) && + GeneratorHelper.HasBindableCustomPropertyAttribute(declaration); + } + private static (VtableAttribute, EquatableArray) GetVtableAttributeToAdd( GeneratorSyntaxContext context, TypeMapper typeMapper, @@ -224,6 +241,117 @@ private static (VtableAttribute, VtableAttribute) GetVtableAttributesForTaskAdap return default; } +#nullable enable + private static BindableCustomProperties GetBindableCustomProperties(GeneratorSyntaxContext context) + { + var symbol = context.SemanticModel.GetDeclaredSymbol((ClassDeclarationSyntax)context.Node)!; + INamedTypeSymbol bindableCustomPropertyAttributeSymbol = context.SemanticModel.Compilation.GetTypeByMetadataName("WinRT.BindableCustomPropertyAttribute")!; + + if (bindableCustomPropertyAttributeSymbol is null || + !symbol.TryGetAttributeWithType(bindableCustomPropertyAttributeSymbol, out AttributeData? attributeData)) + { + return default; + } + + List bindableCustomProperties = new(); + + // Make all public properties in the class bindable including ones in base type. + if (attributeData.ConstructorArguments.Length == 0) + { + for (var curSymbol = symbol; curSymbol != null; curSymbol = curSymbol.BaseType) + { + foreach (var propertySymbol in curSymbol.GetMembers(). + Where(m => m.Kind == SymbolKind.Property && + m.DeclaredAccessibility == Accessibility.Public)) + { + AddProperty(propertySymbol); + } + } + } + // Make specified public properties in the class bindable including ones in base type. + else if (attributeData.ConstructorArguments is + [ + { Kind: TypedConstantKind.Array, Values: [..] propertyNames }, + { Kind: TypedConstantKind.Array, Values: [..] propertyIndexerTypes } + ]) + { + for (var curSymbol = symbol; curSymbol != null; curSymbol = curSymbol.BaseType) + { + foreach (var member in curSymbol.GetMembers()) + { + if (member is IPropertySymbol propertySymbol && + member.DeclaredAccessibility == Accessibility.Public) + { + if (!propertySymbol.IsIndexer && + propertyNames.Any(p => p.Value is string value && value == propertySymbol.Name)) + { + AddProperty(propertySymbol); + } + else if (propertySymbol.IsIndexer && + // ICustomProperty only supports single indexer parameter. + propertySymbol.Parameters.Length == 1 && + propertyIndexerTypes.Any(p => p.Value is ISymbol typeSymbol && typeSymbol.Equals(propertySymbol.Parameters[0].Type, SymbolEqualityComparer.Default))) + { + AddProperty(propertySymbol); + } + } + } + } + } + + var typeName = ToFullyQualifiedString(symbol); + bool isGlobalNamespace = symbol.ContainingNamespace == null || symbol.ContainingNamespace.IsGlobalNamespace; + var @namespace = symbol.ContainingNamespace?.ToDisplayString(); + if (!isGlobalNamespace) + { + typeName = typeName[(@namespace!.Length + 1)..]; + } + + EquatableArray classHierarchy = ImmutableArray.Empty; + + // Gather the type hierarchy, only if the type is nested (as an optimization) + if (symbol.ContainingType is not null) + { + List hierarchyList = new(); + + for (ITypeSymbol parent = symbol; parent is not null; parent = parent.ContainingType) + { + hierarchyList.Add(new TypeInfo( + parent.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), + parent.TypeKind, + parent.IsRecord)); + } + + classHierarchy = ImmutableArray.CreateRange(hierarchyList); + } + + return new BindableCustomProperties( + @namespace, + isGlobalNamespace, + typeName, + classHierarchy, + ToFullyQualifiedString(symbol), + bindableCustomProperties.ToImmutableArray()); + + void AddProperty(ISymbol symbol) + { + if (symbol is IPropertySymbol propertySymbol) + { + bindableCustomProperties.Add(new BindableCustomProperty( + propertySymbol.MetadataName, + ToFullyQualifiedString(propertySymbol.Type), + // Make sure the property accessors are also public even if property itself is public. + propertySymbol.GetMethod != null && propertySymbol.GetMethod.DeclaredAccessibility == Accessibility.Public, + propertySymbol.SetMethod != null && propertySymbol.SetMethod.DeclaredAccessibility == Accessibility.Public, + propertySymbol.IsIndexer, + propertySymbol.IsIndexer ? ToFullyQualifiedString(propertySymbol.Parameters[0].Type) : "", + propertySymbol.IsStatic + )); + } + } + } +#nullable disable + private static string ToFullyQualifiedString(ISymbol symbol) { // Used to ensure class names within generics are fully qualified to avoid @@ -1278,6 +1406,119 @@ private static string LookupRuntimeClassName(Type type) addSource($"WinRT{classPrefix}GlobalVtableLookup.g.cs", source.ToString()); } } + + private static void GenerateBindableCustomProperties( + SourceProductionContext sourceProductionContext, + (ImmutableArray bindableCustomProperties, (bool isCsWinRTAotOptimizerEnabled, bool isCsWinRTComponent, bool isCsWinRTCcwLookupTableGeneratorEnabled) properties) value) + { + if (!value.properties.isCsWinRTAotOptimizerEnabled || value.bindableCustomProperties.Length == 0) + { + return; + } + + StringBuilder source = new(); + + foreach (var bindableCustomProperties in value.bindableCustomProperties) + { + if (!bindableCustomProperties.IsGlobalNamespace) + { + source.AppendLine($$""" + namespace {{bindableCustomProperties.Namespace}} + { + """); + } + + var escapedClassName = GeneratorHelper.EscapeTypeNameForIdentifier(bindableCustomProperties.ClassName); + + ReadOnlySpan classHierarchy = bindableCustomProperties.ClassHierarchy.AsSpan(); + // If the type is nested, correctly nest the type definition + for (int i = classHierarchy.Length - 1; i > 0; i--) + { + source.AppendLine($$""" + partial {{classHierarchy[i].GetTypeKeyword()}} {{classHierarchy[i].QualifiedName}} + { + """); + } + + source.AppendLine($$""" + partial class {{(classHierarchy.IsEmpty ? bindableCustomProperties.ClassName : classHierarchy[0].QualifiedName)}} : global::Microsoft.UI.Xaml.Data.IBindableCustomPropertyImplementation + { + global::Microsoft.UI.Xaml.Data.BindableCustomProperty global::Microsoft.UI.Xaml.Data.IBindableCustomPropertyImplementation.GetProperty(string name) + { + """); + + foreach (var property in bindableCustomProperties.Properties.Where(p => !p.IsIndexer)) + { + var instanceAccessor = property.IsStatic ? bindableCustomProperties.QualifiedClassName : $$"""(({{bindableCustomProperties.QualifiedClassName}})instance)"""; + + source.AppendLine($$""" + if (name == "{{property.Name}}") + { + return new global::Microsoft.UI.Xaml.Data.BindableCustomProperty( + {{GetBoolAsString(property.CanRead)}}, + {{GetBoolAsString(property.CanWrite)}}, + "{{property.Name}}", + typeof({{property.Type}}), + {{ (property.CanRead ? $$"""static (instance) => {{instanceAccessor}}.{{property.Name}}""" : "null") }}, + {{ (property.CanWrite ? $$"""static (instance, value) => {{instanceAccessor}}.{{property.Name}} = ({{property.Type}})value""" : "null") }}, + null, + null); + } + """); + } + + source.AppendLine($$""" + return default; + } + + global::Microsoft.UI.Xaml.Data.BindableCustomProperty global::Microsoft.UI.Xaml.Data.IBindableCustomPropertyImplementation.GetProperty(global::System.Type indexParameterType) + { + """); + + foreach (var property in bindableCustomProperties.Properties.Where(p => p.IsIndexer)) + { + var instanceAccessor = property.IsStatic ? bindableCustomProperties.QualifiedClassName : $$"""(({{bindableCustomProperties.QualifiedClassName}})instance)"""; + + source.AppendLine($$""" + if (indexParameterType == typeof({{property.IndexerType}})) + { + return new global::Microsoft.UI.Xaml.Data.BindableCustomProperty( + {{GetBoolAsString(property.CanRead)}}, + {{GetBoolAsString(property.CanWrite)}}, + "{{property.Name}}", + typeof({{property.Type}}), + null, + null, + {{ (property.CanRead ? $$"""static (instance, index) => {{instanceAccessor}}[({{property.IndexerType}})index]""" : "null") }}, + {{ (property.CanWrite ? $$"""static (instance, value, index) => {{instanceAccessor}}[({{property.IndexerType}})index] = ({{property.Type}})value""" : "null") }}); + } + """); + } + + source.AppendLine($$""" + return default; + } + } + """); + + // Close all brackets + for (int i = classHierarchy.Length - 1; i > 0; i--) + { + source.AppendLine("}"); + } + + if (!bindableCustomProperties.IsGlobalNamespace) + { + source.AppendLine($@"}}"); + } + + source.AppendLine(); + } + + sourceProductionContext.AddSource("WinRTCustomBindableProperties.g.cs", source.ToString()); + + static string GetBoolAsString(bool value) => value ? "true" : "false"; + } } internal readonly record struct GenericParameter( @@ -1303,6 +1544,23 @@ internal sealed record VtableAttribute( bool IsPublic, string RuntimeClassName = default); + internal readonly record struct BindableCustomProperty( + string Name, + string Type, + bool CanRead, + bool CanWrite, + bool IsIndexer, + string IndexerType, + bool IsStatic); + + internal readonly record struct BindableCustomProperties( + string Namespace, + bool IsGlobalNamespace, + string ClassName, + EquatableArray ClassHierarchy, + string QualifiedClassName, + EquatableArray Properties); + /// /// A model describing a type info in a type hierarchy. /// diff --git a/src/Authoring/WinRT.SourceGenerator/Helper.cs b/src/Authoring/WinRT.SourceGenerator/Helper.cs index 7cc07fca9..f176d92e1 100644 --- a/src/Authoring/WinRT.SourceGenerator/Helper.cs +++ b/src/Authoring/WinRT.SourceGenerator/Helper.cs @@ -450,6 +450,27 @@ public static bool IsWinRTType(MemberDeclarationSyntax node) return isProjectedType; } + public static bool HasBindableCustomPropertyAttribute(MemberDeclarationSyntax node) + { + return node.AttributeLists.SelectMany(list => list.Attributes).Any(IsBindableCustomPropertyAttribute); + + // Check based on identifier name if this is the BindableCustomProperty attribute. + // Technically this can be a different namespace, but we will confirm later once + // we have access to the semantic model. + static bool IsBindableCustomPropertyAttribute(AttributeSyntax attribute) + { + var nameSyntax = attribute.Name; + if (nameSyntax is QualifiedNameSyntax qualifiedName) + { + // Right would have the attribute while left is the namespace. + nameSyntax = qualifiedName.Right; + } + + return nameSyntax is IdentifierNameSyntax name && + name.Identifier.ValueText == "BindableCustomProperty"; + } + } + /// /// Checks whether or not a given symbol has an attribute with the specified type. /// diff --git a/src/Tests/AuthoringTest/Program.cs b/src/Tests/AuthoringTest/Program.cs index ca38bcea8..e68fc8444 100644 --- a/src/Tests/AuthoringTest/Program.cs +++ b/src/Tests/AuthoringTest/Program.cs @@ -199,6 +199,13 @@ public sealed class CustomWWW : IWwwFormUrlDecoderEntry public string Value => "CsWinRT"; } + [BindableCustomProperty] + public sealed partial class CustomProperty + { + public int Number { get; } = 4; + public string Value => "CsWinRT"; + } + [Version(3u)] public interface IDouble { diff --git a/src/Tests/FunctionalTests/CCW/Program.cs b/src/Tests/FunctionalTests/CCW/Program.cs index cb84cae01..271e1eb99 100644 --- a/src/Tests/FunctionalTests/CCW/Program.cs +++ b/src/Tests/FunctionalTests/CCW/Program.cs @@ -203,6 +203,114 @@ } } + +// Test ICustomProperty +Language language = new Language(); +language.Value = 42; +language[1] = "Bindable"; + +// Used for non-indexer types to avoid passing null. +// Note this isn't checked in non-indexer scenarios. +var ignoredType = typeof(Language); +if (!instance.ValidateBindableProperty(language, "Name", ignoredType, false, true, false, false, typeof(string), null, null, out var retrievedValue) || + !instance.ValidateBindableProperty(language, "Value", ignoredType, false, true, true, false, typeof(int), null, 22, out var retrievedValue2) || + !instance.ValidateBindableProperty(language, "Item", typeof(int), false, true, true, true, typeof(string), 1, "Language", out var retrievedValue3)) +{ + return 125; +} + +// Validate previous values +if ((string)retrievedValue != "Language" || (int)retrievedValue2 != 42 || (string)retrievedValue3 != "Bindable") +{ + return 126; +} + +// Validate if values got set during ValidateBindableProperty via ICustomProperty +if (language.Value != 22 || language[1] != "Language") +{ + return 127; +} + +Language2 language2 = new Language2(); +// Test private property not found +if (instance.ValidateBindableProperty(language2, "Number", ignoredType, true, true, true, false, typeof(int), null, null, out _)) +{ + return 128; +} + +// Test private accessors not found +if (!instance.ValidateBindableProperty(language2, "SetOnly", ignoredType, false, false, true, false, typeof(string), null, "One", out _) || + !instance.ValidateBindableProperty(language2, "PrivateSet", ignoredType, false, true, false, false, typeof(string), null, "Two", out var retrievedValue4) || + !instance.ValidateBindableProperty(language2, "StaticDouble", ignoredType, false, true, true, false, typeof(double), null, 5.0, out var retrievedValue11)) +{ + return 129; +} + +// Set during SetOnly call. +if ((string)retrievedValue4 != "One" || + (double)retrievedValue11 != 4.0 || Language2.StaticDouble != 5.0) +{ + return 130; +} + +Language4 language4 = new Language4(); +// Test internal property not found +if (instance.ValidateBindableProperty(language4, "Name", ignoredType, true, true, false, false, typeof(string), null, null, out _)) +{ + return 131; +} + +// Validate generic scenarios +Language5 language5 = new Language5(); +language5.Value = 5; +if (!instance.ValidateBindableProperty(language5, "Value", ignoredType, false, true, true, false, typeof(int), null, 2, out var retrievedValue5)) +{ + return 132; +} + +if ((int)retrievedValue5 != 5 || language5.Value != 2) +{ + return 133; +} + +Language5 language6 = new Language5(); +language6.Value = language2; +language6.Number = 4; +if (!instance.ValidateBindableProperty(language6, "Value", ignoredType, false, true, true, false, typeof(object), null, language, out var retrievedValue6) || + !instance.ValidateBindableProperty(language6, "Number", ignoredType, false, true, true, false, typeof(int), null, 2, out var retrievedValue7)) +{ + return 133; +} + +if (retrievedValue6 != language2 || language6.Value != language || + (int)retrievedValue7 != 4 || language6.Number != 2) +{ + return 134; +} + +// Validate dervied scenarios +LanguageDervied languageDervied = new LanguageDervied(); +languageDervied.Value = 22; +LanguageDervied2 languageDervied2 = new LanguageDervied2(); +languageDervied2.Value = 11; +languageDervied2.Derived = 22; + +if (!instance.ValidateBindableProperty(languageDervied, "Derived", ignoredType, false, true, false, false, typeof(int), null, null, out var retrievedValue8) || + // Not projected as custom property + instance.ValidateBindableProperty(languageDervied, "Value", ignoredType, true, true, true, false, typeof(int), null, 33, out var _) || + !instance.ValidateBindableProperty(languageDervied2, "Derived", ignoredType, false, true, true, false, typeof(int), null, 2, out var retrievedValue9) || + !instance.ValidateBindableProperty(languageDervied2, "Name", ignoredType, false, true, false, false, typeof(string), null, null, out var retrievedValue10)) +{ + return 135; +} + +if ((int)retrievedValue8 != 4 || + (int)retrievedValue9 != 22 || languageDervied2.Derived != 2 || + (string)retrievedValue10 != "Language") +{ + return 136; +} + return 100; @@ -527,4 +635,93 @@ public void Execute(object parameter) { throw new NotImplementedException(); } +} + +[BindableCustomProperty([nameof(Name), nameof(Value)], [typeof(int)])] +partial class Language +{ + private readonly string[] _values = new string[4]; + + public string Name { get; } = "Language"; + public int Value { get; set; } + public string this[int i] + { + get => _values[i]; + set => _values[i] = value; + } +} + +[global::WinRT.BindableCustomProperty([nameof(Name), nameof(Derived)], [typeof(int)])] +partial class LanguageDervied : Language +{ + public int Derived { get; } = 4; +} + +[WinRT.BindableCustomProperty] +partial class LanguageDervied2 : Language +{ + public int Derived { get; set; } +} + +[BindableCustomProperty] +sealed partial class Language2 +{ + public string Name { get; } = "Language2"; + public string[] Value { get; set; } + private int Number { get; set; } + public string SetOnly + { + set + { + PrivateSet = value; + } + } + public string PrivateSet { get; private set; } = "PrivateSet"; + public static double StaticDouble { get; set; } = 4.0; +} + +[BindableCustomProperty] +sealed partial class Language4 +{ + internal string Name { get; } + private int Number { get; set; } +} + +[BindableCustomProperty] +sealed partial class Language5 +{ + private readonly Dictionary _values = new(); + + public T Value { get; set; } + public int Number { get; set; } + public T this[T i] + { + get => _values[i]; + set => _values[i] = value; + } +} + +namespace Test +{ + namespace Test2 + { + sealed partial class Nested + { + [BindableCustomProperty([nameof(Value)], [])] + sealed partial class Language3 : IProperties2 + { + private readonly string[] _values = new string[4]; + + public string Name { get; } + public int Value { get; set; } + public string this[int i] + { + get => _values[i]; + set => _values[i] = value; + } + private int Number { get; set; } + public int ReadWriteProperty { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + } + } + } } \ No newline at end of file diff --git a/src/Tests/FunctionalTests/Directory.Build.props b/src/Tests/FunctionalTests/Directory.Build.props index 06c31e158..83fea6fc7 100644 --- a/src/Tests/FunctionalTests/Directory.Build.props +++ b/src/Tests/FunctionalTests/Directory.Build.props @@ -15,11 +15,6 @@ "CS0067: The event 'CustomCommand.CanExecuteChanged' is never used." "CA1416: This call site is reachable on all platforms." - "IL2075: ABI.Microsoft.UI.Xaml.Data.ManagedCustomPropertyProviderVftbl.Do_Abi_GetCustomProperty_0(IntPtr, IntPtr, IntPtr*): - 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicProperties' in call to - System.Type.GetProperty(String, BindingFlags)'. The return value of method 'System.Object.GetType()' does not have - matching annotations. The source value must declare at least the same requirements as those declared on the target - location it is assigned to." "IL2087: .ABI.Windows.Foundation.AsyncActionProgressHandler..cctor(): 'type' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicFields' in call to 'WinRT.GuidGenerator.GetSignature(Type)'. The generic parameter '!0' of @@ -27,7 +22,7 @@ matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to." --> - $(WarningsNotAsErrors);CS0067;CA1416;IL2075;IL2087 + $(WarningsNotAsErrors);CS0067;CA1416;IL2087 diff --git a/src/Tests/TestComponentCSharp/Class.cpp b/src/Tests/TestComponentCSharp/Class.cpp index 16e7c0af3..250a47d27 100644 --- a/src/Tests/TestComponentCSharp/Class.cpp +++ b/src/Tests/TestComponentCSharp/Class.cpp @@ -1461,6 +1461,11 @@ namespace winrt::TestComponentCSharp::implementation _intColl = value; } + void Class::SetCharIterable(IIterable const& value) + { + _charColl = value; + } + IBindableIterable Class::BindableIterableProperty() { return _bindableIterable; @@ -1553,6 +1558,67 @@ namespace winrt::TestComponentCSharp::implementation return winrt::make(vector); } + bool Class::ValidateBindableProperty( + WF::IInspectable const& bindableObject, + hstring property, + Windows::UI::Xaml::Interop::TypeName const& indexerType, + bool validateOnlyExists, + bool canRead, + bool canWrite, + bool isIndexer, + Windows::UI::Xaml::Interop::TypeName const& type, + WF::IInspectable const& indexerValue, + WF::IInspectable const& setValue, + WF::IInspectable& retrievedValue) + { + auto customPropertyProvider = bindableObject.as(); + auto customProperty = !isIndexer ? customPropertyProvider.GetCustomProperty(property) : customPropertyProvider.GetIndexedProperty(property, indexerType); + if (customProperty == nullptr) + { + return false; + } + + if (validateOnlyExists) + { + return true; + } + + if (customProperty.Name() != property || + customProperty.CanRead() != canRead || + customProperty.CanWrite() != canWrite || + customProperty.Type() != type) + { + return false; + } + + if (!isIndexer) + { + if (customProperty.CanRead()) + { + retrievedValue = customProperty.GetValue(bindableObject); + } + + if (customProperty.CanWrite()) + { + customProperty.SetValue(bindableObject, setValue); + } + } + else + { + if (customProperty.CanRead()) + { + retrievedValue = customProperty.GetIndexedValue(bindableObject, indexerValue); + } + + if (customProperty.CanWrite()) + { + customProperty.SetIndexedValue(bindableObject, setValue, indexerValue); + } + } + + return true; + } + void Class::CopyProperties(winrt::TestComponentCSharp::IProperties1 const& src) { ReadWriteProperty(src.ReadWriteProperty()); diff --git a/src/Tests/TestComponentCSharp/Class.h b/src/Tests/TestComponentCSharp/Class.h index 573d32ac2..83b619204 100644 --- a/src/Tests/TestComponentCSharp/Class.h +++ b/src/Tests/TestComponentCSharp/Class.h @@ -62,6 +62,7 @@ namespace winrt::TestComponentCSharp::implementation ComposedNonBlittableStruct _nonBlittableStruct{}; std::vector _ints{ 1, 2, 3 }; Windows::Foundation::Collections::IIterable _intColl; + Windows::Foundation::Collections::IIterable _charColl; Microsoft::UI::Xaml::Interop::IBindableIterable _bindableIterable; Microsoft::UI::Xaml::Interop::IBindableVector _bindableVector; Microsoft::UI::Xaml::Interop::IBindableObservableVector _bindableObservable; @@ -289,6 +290,7 @@ namespace winrt::TestComponentCSharp::implementation Windows::Foundation::Collections::IIterable GetIntIterable(); void SetIntIterable(Windows::Foundation::Collections::IIterable const& value); + void SetCharIterable(Windows::Foundation::Collections::IIterable const& value); Microsoft::UI::Xaml::Interop::IBindableIterable BindableIterableProperty(); void BindableIterableProperty(Microsoft::UI::Xaml::Interop::IBindableIterable const& value); @@ -306,6 +308,19 @@ namespace winrt::TestComponentCSharp::implementation void BindableObservableVectorProperty(Microsoft::UI::Xaml::Interop::IBindableObservableVector const& value); Microsoft::UI::Xaml::Interop::IBindableObservableVector GetBindableObservableVector(Microsoft::UI::Xaml::Interop::IBindableObservableVector vector); + bool ValidateBindableProperty( + IInspectable const& bindableObject, + hstring property, + Windows::UI::Xaml::Interop::TypeName const& indexerType, + bool validateOnlyExists, + bool canRead, + bool canWrite, + bool isIndexer, + Windows::UI::Xaml::Interop::TypeName const& type, + IInspectable const& indexerValue, + IInspectable const& setValue, + IInspectable& retrievedValue); + void CopyProperties(TestComponentCSharp::IProperties1 const& src); void CopyPropertiesViaWeakReference(TestComponentCSharp::IProperties1 const& src); diff --git a/src/Tests/TestComponentCSharp/TestComponentCSharp.idl b/src/Tests/TestComponentCSharp/TestComponentCSharp.idl index b1c42c45f..491868f30 100644 --- a/src/Tests/TestComponentCSharp/TestComponentCSharp.idl +++ b/src/Tests/TestComponentCSharp/TestComponentCSharp.idl @@ -340,6 +340,7 @@ namespace TestComponentCSharp Windows.Foundation.Collections.IIterable GetIntIterable(); void SetIntIterable(Windows.Foundation.Collections.IIterable value); + void SetCharIterable(Windows.Foundation.Collections.IIterable value); // Bindable Microsoft.UI.Xaml.Interop.IBindableIterable BindableIterableProperty; @@ -355,6 +356,19 @@ namespace TestComponentCSharp Microsoft.UI.Xaml.Interop.IBindableObservableVector BindableObservableVectorProperty; Microsoft.UI.Xaml.Interop.IBindableObservableVector GetBindableObservableVector(Microsoft.UI.Xaml.Interop.IBindableObservableVector vector); + Boolean ValidateBindableProperty( + Object bindableObject, + String property, + Windows.UI.Xaml.Interop.TypeName indexerType, + Boolean validateOnlyExists, + Boolean canRead, + Boolean canWrite, + Boolean isIndexer, + Windows.UI.Xaml.Interop.TypeName type, + Object indexerValue, + Object setValue, + out Object retrievedValue); + void CopyProperties(IProperties1 src); void CopyPropertiesViaWeakReference(IProperties1 src); diff --git a/src/WinRT.Runtime/Attributes.cs b/src/WinRT.Runtime/Attributes.cs index ef1270c06..fdcf99610 100644 --- a/src/WinRT.Runtime/Attributes.cs +++ b/src/WinRT.Runtime/Attributes.cs @@ -230,6 +230,39 @@ public WinRTAssemblyExportsTypeAttribute(Type type) public Type Type { get; } } + /// + /// An attribute used to indicate the properties which are bindable via the implementation provided for use in WinUI scenarios. + /// The type which this attribute is placed on also needs to be marked partial and needs to be non-generic. + /// + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] +#if EMBED + internal +#else + public +#endif + sealed class BindableCustomPropertyAttribute : Attribute + { + /// + /// Marks all public properties as bindable. + /// + public BindableCustomPropertyAttribute() + { + } + + /// + /// Marks the specified public properties as bindable. + /// + /// The name of the non-indexer public properties to mark as bindable. + /// The parameter type of the indexer public properties to mark as bindable. + public BindableCustomPropertyAttribute(string[] propertyNames, Type[] indexerPropertyTypes) + { + PropertyNames = propertyNames; + IndexerPropertyTypes = indexerPropertyTypes; + } + + internal string[] PropertyNames { get; } + internal Type[] IndexerPropertyTypes { get; } + } #endif } diff --git a/src/WinRT.Runtime/MatchingRefApiCompatBaseline.txt b/src/WinRT.Runtime/MatchingRefApiCompatBaseline.txt index 93a825414..601e328e3 100644 --- a/src/WinRT.Runtime/MatchingRefApiCompatBaseline.txt +++ b/src/WinRT.Runtime/MatchingRefApiCompatBaseline.txt @@ -268,4 +268,7 @@ MembersMustExist : Member 'public System.Guid ABI.System.EventHandler.IID.get MembersMustExist : Member 'public System.Guid ABI.System.Collections.Specialized.NotifyCollectionChangedEventHandler.IID.get()' does not exist in the reference but it does exist in the implementation. MembersMustExist : Member 'public System.Guid ABI.System.ComponentModel.PropertyChangedEventHandler.IID.get()' does not exist in the reference but it does exist in the implementation. TypesMustExist : Type 'WinRT.WinRTAssemblyExportsTypeAttribute' does not exist in the reference but it does exist in the implementation. -Total Issues: 269 +TypesMustExist : Type 'Microsoft.UI.Xaml.Data.BindableCustomProperty' does not exist in the reference but it does exist in the implementation. +TypesMustExist : Type 'WinRT.BindableCustomPropertyAttribute' does not exist in the reference but it does exist in the implementation. +TypesMustExist : Type 'Microsoft.UI.Xaml.Data.IBindableCustomPropertyImplementation' does not exist in the reference but it does exist in the implementation. +Total Issues: 272 diff --git a/src/WinRT.Runtime/Projections/ICustomPropertyProvider.net5.cs b/src/WinRT.Runtime/Projections/ICustomPropertyProvider.net5.cs index b982623e9..7b8605e80 100644 --- a/src/WinRT.Runtime/Projections/ICustomPropertyProvider.net5.cs +++ b/src/WinRT.Runtime/Projections/ICustomPropertyProvider.net5.cs @@ -2,7 +2,9 @@ // Licensed under the MIT License. using System; +using System.Diagnostics.CodeAnalysis; using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using WinRT; @@ -13,7 +15,7 @@ namespace Microsoft.UI.Xaml.Data [global::WinRT.WindowsRuntimeType] [global::WinRT.WindowsRuntimeHelperType(typeof(global::ABI.Microsoft.UI.Xaml.Data.ICustomProperty))] [Guid("30DA92C0-23E8-42A0-AE7C-734A0E5D2782")] - interface ICustomProperty + internal interface ICustomProperty { object GetValue(object target); void SetValue(object target, object value); @@ -23,6 +25,105 @@ interface ICustomProperty bool CanWrite { get; } string Name { get; } global::System.Type Type { get; } + } + + /// + /// An interface complementing providing the implementation to expose the specified properties. + /// +#if EMBED + internal +#else + public +#endif + interface IBindableCustomPropertyImplementation + { + /// + /// Get the generated implementation representing the specified property name. + /// + /// The name of the property to get. + /// The implementation for the property specified by . + public Microsoft.UI.Xaml.Data.BindableCustomProperty GetProperty(string name); + + /// + /// Get the generated implementation representing the specified index property type. + /// + /// The index property to get. + /// The implementation for the property specified by . + public Microsoft.UI.Xaml.Data.BindableCustomProperty GetProperty(Type indexParameterType); + } + + /// + /// An implementation that relies on a source generation approach for its implememtation + /// rather than reflection. This is used by the source generator generating the implementation for . + /// + [global::WinRT.WinRTExposedType(typeof(global::ABI.Microsoft.UI.Xaml.Data.ManagedCustomPropertyWinRTTypeDetails))] +#if EMBED + internal +#else + public +#endif + sealed class BindableCustomProperty : ICustomProperty + { + private readonly bool _canRead; + private readonly bool _canWrite; + private readonly string _name; + private readonly Type _type; + private readonly Func _getValue; + private readonly Action _setValue; + private readonly Func _getIndexedValue; + private readonly Action _setIndexedValue; + + public BindableCustomProperty( + bool canRead, + bool canWrite, + string name, + Type type, + Func getValue, + Action setValue, + Func getIndexedValue, + Action setIndexedValue) + { + _canRead = canRead; + _canWrite = canWrite; + _name = name; + _type = type; + _getValue = getValue; + _setValue = setValue; + _getIndexedValue = getIndexedValue; + _setIndexedValue = setIndexedValue; + } + + bool ICustomProperty.CanRead => _canRead; + + bool ICustomProperty.CanWrite => _canWrite; + + string ICustomProperty.Name => _name; + + Type ICustomProperty.Type => _type; + + object ICustomProperty.GetIndexedValue(object target, object index) => _getIndexedValue != null ? _getIndexedValue(target, index) : throw new NotImplementedException(); + + object ICustomProperty.GetValue(object target) => _getValue != null ? _getValue(target) : throw new NotImplementedException(); + + void ICustomProperty.SetIndexedValue(object target, object value, object index) + { + if (_setIndexedValue == null) + { + throw new NotImplementedException(); + } + + _setIndexedValue(target, value, index); + } + + void ICustomProperty.SetValue(object target, object value) + { + if (_setValue == null) + { + throw new NotImplementedException(); + } + + _setValue(target, value); + } } } @@ -289,25 +390,47 @@ static unsafe ManagedCustomPropertyProviderVftbl() [UnmanagedCallersOnly] - private static unsafe int Do_Abi_GetCustomProperty_0(IntPtr thisPtr, IntPtr name, IntPtr* result) { global::Microsoft.UI.Xaml.Data.ICustomProperty __result = default; try { string _name = MarshalString.FromAbi(name); - object target = global::WinRT.ComWrappersSupport.FindObject(thisPtr); - PropertyInfo propertyInfo = target.GetType().GetProperty( - _name, - BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public); - - if (propertyInfo is object) - { - __result = new ManagedCustomProperty(propertyInfo); - } - - *result = MarshalInterface.FromManaged(__result); - + object target = global::WinRT.ComWrappersSupport.FindObject(thisPtr); + + if (target is global::Microsoft.UI.Xaml.Data.IBindableCustomPropertyImplementation bindableCustomPropertyImplementation) + { + __result = bindableCustomPropertyImplementation.GetProperty(_name); + *result = MarshalInterface.FromManaged(__result); + return 0; + } + + if (!RuntimeFeature.IsDynamicCodeCompiled) + { + throw new NotSupportedException( + $"ICustomProperty support used by XAML binding for '{target.GetType()}' requires the type to marked with 'WinRT.BindableCustomPropertyAttribute'. " + + $"If this is a built-in type or a type that can't be marked, a wrapper type should be used around it that is marked to enable this support."); + } + + GetCustomPropertyForJit(target, _name, result); + + [UnconditionalSuppressMessage("Trimming", "IL2075", Justification = "Fallback method for JIT environments that is not trim-safe by design.")] + [MethodImpl(MethodImplOptions.NoInlining)] + static void GetCustomPropertyForJit(object target, string name, IntPtr* result) + { + global::Microsoft.UI.Xaml.Data.ICustomProperty __result = default; + + PropertyInfo propertyInfo = target.GetType().GetProperty( + name, + BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public); + + if (propertyInfo is not null) + { + __result = new ManagedCustomProperty(propertyInfo); + } + + *result = MarshalInterface.FromManaged(__result); + } } catch (Exception __exception__) { @@ -318,29 +441,54 @@ private static unsafe int Do_Abi_GetCustomProperty_0(IntPtr thisPtr, IntPtr name } [UnmanagedCallersOnly] - private static unsafe int Do_Abi_GetIndexedProperty_1(IntPtr thisPtr, IntPtr name, global::ABI.System.Type type, IntPtr* result) { global::Microsoft.UI.Xaml.Data.ICustomProperty __result = default; try { string _name = MarshalString.FromAbi(name); - object target = global::WinRT.ComWrappersSupport.FindObject(thisPtr); - PropertyInfo propertyInfo = target.GetType().GetProperty( - _name, - BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public, - null, // default binder - null, // ignore return type - new Type[] { global::ABI.System.Type.FromAbi(type) }, // indexed parameter type - null // ignore type modifier - ); - - if (propertyInfo is object) - { - __result = new ManagedCustomProperty(propertyInfo); - } + Type _type = global::ABI.System.Type.FromAbi(type); - *result = MarshalInterface.FromManaged(__result); + object target = global::WinRT.ComWrappersSupport.FindObject(thisPtr); + + if (target is global::Microsoft.UI.Xaml.Data.IBindableCustomPropertyImplementation bindableCustomPropertyImplementation) + { + __result = bindableCustomPropertyImplementation.GetProperty(_type); + *result = MarshalInterface.FromManaged(__result); + return 0; + } + + if (!RuntimeFeature.IsDynamicCodeCompiled) + { + throw new NotSupportedException( + $"ICustomProperty support used by XAML binding for '{target.GetType()}' requires the type to marked with 'WinRT.BindableCustomPropertyAttribute'. " + + $"If this is a built-in type or a type that can't be marked, a wrapper type should be used around it that is marked to enable this support."); + } + + GetCustomPropertyForJit(target, _name, _type, result); + + [UnconditionalSuppressMessage("Trimming", "IL2075", Justification = "Fallback method for JIT environments that is not trim-safe by design.")] + [MethodImpl(MethodImplOptions.NoInlining)] + static void GetCustomPropertyForJit(object target, string name, Type type, IntPtr* result) + { + global::Microsoft.UI.Xaml.Data.ICustomProperty __result = default; + + PropertyInfo propertyInfo = target.GetType().GetProperty( + name, + BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public, + null, // default binder + null, // ignore return type + new Type[] { type }, // indexed parameter type + null // ignore type modifier + ); + + if (propertyInfo is not null) + { + __result = new ManagedCustomProperty(propertyInfo); + } + + *result = MarshalInterface.FromManaged(__result); + } } catch (Exception __exception__) { @@ -351,7 +499,6 @@ private static unsafe int Do_Abi_GetIndexedProperty_1(IntPtr thisPtr, IntPtr nam } [UnmanagedCallersOnly] - private static unsafe int Do_Abi_GetStringRepresentation_2(IntPtr thisPtr, IntPtr* result) { string __result = default; @@ -369,7 +516,6 @@ private static unsafe int Do_Abi_GetStringRepresentation_2(IntPtr thisPtr, IntPt } [UnmanagedCallersOnly] - private static unsafe int Do_Abi_get_Type_3(IntPtr thisPtr, global::ABI.System.Type* value) { global::System.Type __value = default;