diff --git a/src/WinRT.Projection.Writer/Factories/MappedInterfaceStubFactory.cs b/src/WinRT.Projection.Writer/Factories/MappedInterfaceStubFactory.cs index b5f203846..44e4a4946 100644 --- a/src/WinRT.Projection.Writer/Factories/MappedInterfaceStubFactory.cs +++ b/src/WinRT.Projection.Writer/Factories/MappedInterfaceStubFactory.cs @@ -203,9 +203,13 @@ private static void EmitDictionary(IndentedTextWriter writer, ProjectionEmitCont string enumerableObjRefName = "_objRef_System_Collections_Generic_IEnumerable_" + IidExpressionGenerator.EscapeTypeNameForIdentifier(kvLong, stripGlobal: false) + "_"; writer.WriteLine(); + + // 'Keys'/'Values' take the projected runtime class directly (passed as 'this'), rather than the + // interface object reference like the other accessors. This lets the returned collection be cached + // in the public property's backing 'field' so it preserves reference identity across accesses. + EmitUnsafeAccessor(writer, "Keys", $"ICollection<{k}>", $"{prefix}Keys", interopType, "", receiver: "WindowsRuntimeObject windowsRuntimeObject"); + EmitUnsafeAccessor(writer, "Values", $"ICollection<{v}>", $"{prefix}Values", interopType, "", receiver: "WindowsRuntimeObject windowsRuntimeObject"); EmitUnsafeAccessors(writer, interopType, [ - new("Keys", $"ICollection<{k}>", $"{prefix}Keys", ""), - new("Values", $"ICollection<{v}>", $"{prefix}Values", ""), new("Count", "int", $"{prefix}Count", ""), new("Item", v, $"{prefix}Item", $", {k} key"), new("Item", "void", $"{prefix}Item", $", {k} key, {v} value"), @@ -223,8 +227,8 @@ private static void EmitDictionary(IndentedTextWriter writer, ProjectionEmitCont // GetEnumerator is NOT emitted here -- it's handled separately by IIterable's own // EmitGenericEnumerable invocation. writer.WriteLine(isMultiline: true, $$""" - public ICollection<{{k}}> Keys => {{prefix}}Keys(null, {{objRefName}}); - public ICollection<{{v}}> Values => {{prefix}}Values(null, {{objRefName}}); + public ICollection<{{k}}> Keys => field ??= {{prefix}}Keys(null, this); + public ICollection<{{v}}> Values => field ??= {{prefix}}Values(null, this); public int Count => {{prefix}}Count(null, {{objRefName}}); public bool IsReadOnly => false; public {{v}} this[{{k}} key] @@ -261,9 +265,13 @@ private static void EmitReadOnlyDictionary(IndentedTextWriter writer, Projection string prefix = "IReadOnlyDictionaryMethods_" + keyId + "_" + valId + "_"; writer.WriteLine(); + + // 'Keys'/'Values' take the projected runtime class directly (passed as 'this'), rather than the + // interface object reference like the other accessors. This lets the returned collection be cached + // in the public property's backing 'field' so it preserves reference identity across accesses. + EmitUnsafeAccessor(writer, "Keys", $"IEnumerable<{k}>", $"{prefix}Keys", interopType, "", receiver: "WindowsRuntimeObject windowsRuntimeObject"); + EmitUnsafeAccessor(writer, "Values", $"IEnumerable<{v}>", $"{prefix}Values", interopType, "", receiver: "WindowsRuntimeObject windowsRuntimeObject"); EmitUnsafeAccessors(writer, interopType, [ - new("Keys", $"ICollection<{k}>", $"{prefix}Keys", ""), - new("Values", $"ICollection<{v}>", $"{prefix}Values", ""), new("Count", "int", $"{prefix}Count", ""), new("Item", v, $"{prefix}Item", $", {k} key"), new("ContainsKey", "bool", $"{prefix}ContainsKey", $", {k} key"), @@ -273,8 +281,8 @@ private static void EmitReadOnlyDictionary(IndentedTextWriter writer, Projection // EmitGenericEnumerable invocation. writer.WriteLine(); writer.WriteLine($"public {v} this[{k} key] => {prefix}Item(null, {objRefName}, key);"); - writer.WriteLine($"public IEnumerable<{k}> Keys => {prefix}Keys(null, {objRefName});"); - writer.WriteLine($"public IEnumerable<{v}> Values => {prefix}Values(null, {objRefName});"); + writer.WriteLine($"public IEnumerable<{k}> Keys => field ??= {prefix}Keys(null, this);"); + writer.WriteLine($"public IEnumerable<{v}> Values => field ??= {prefix}Values(null, this);"); writer.WriteLine($"public int Count => {prefix}Count(null, {objRefName});"); writer.WriteLine($"public bool ContainsKey({k} key) => {prefix}ContainsKey(null, {objRefName}, key);"); writer.WriteLine($"public bool TryGetValue({k} key, out {v} value) => {prefix}TryGetValue(null, {objRefName}, key, out value);"); @@ -380,9 +388,12 @@ private static void EmitList(IndentedTextWriter writer, ProjectionEmitContext co /// /// Emits a single [UnsafeAccessor] static extern declaration that targets a method on a - /// WinRT.Interop helper type. The function signature is built from the supplied parts. + /// WinRT.Interop helper type. The function signature is built from the supplied parts. The + /// defaults to the interface object reference + /// (WindowsRuntimeObjectReference objRef); a few accessors (e.g. dictionary + /// Keys/Values) instead take the projected runtime class (WindowsRuntimeObject). /// - private static void EmitUnsafeAccessor(IndentedTextWriter writer, string accessName, string returnType, string functionName, string interopType, string extraParams) + private static void EmitUnsafeAccessor(IndentedTextWriter writer, string accessName, string returnType, string functionName, string interopType, string extraParams, string receiver = "WindowsRuntimeObjectReference objRef") { UnsafeAccessorFactory.EmitStaticMethod( writer, @@ -390,14 +401,14 @@ private static void EmitUnsafeAccessor(IndentedTextWriter writer, string accessN returnType: returnType, functionName: functionName, interopType: interopType, - parameterList: $"WindowsRuntimeObjectReference objRef{extraParams}"); + parameterList: $"{receiver}{extraParams}"); writer.WriteLine(); } /// /// Emits a sequence of [UnsafeAccessor] static extern declarations sharing the same /// . Each row of is forwarded to - /// . + /// . /// Used by the collection-stub emitters which emit table-shaped sets of accessors. /// private static void EmitUnsafeAccessors(