Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/Components/Web.JS/src/Rendering/ElementReferenceCapture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ function getCaptureIdAttributeName(referenceCaptureId: string) {
return `_bl_${referenceCaptureId}`;
}

function getCaptureIdFromElement(element: Element): string | null {
for (let i = 0; i < element.attributes.length; i++) {
const attr = element.attributes[i];
if (attr.name.startsWith('_bl_')) {
return attr.name.substring(4);
}
}
return null;
}

// Support receiving ElementRef instances as args in interop calls
const elementRefKey = '__internalId'; // Keep in sync with ElementRef.cs
DotNet.attachReviver((key, value) => {
Expand All @@ -25,3 +35,14 @@ DotNet.attachReviver((key, value) => {
return value;
}
});

// Support return of the ElementRef from JS to .NET
DotNet.attachReplacer((key, value) => {
if (value instanceof Element) {
const captureId = getCaptureIdFromElement(value);
if (captureId) {
return { [elementRefKey]: captureId };
}
}
return value;
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is adding some public API that I don't know we want (anything in DotNet is public API)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of doing it like this, could we "hijack" JSObjectReference to achieve this?

By that, I mean using the same key as

const jsObjectIdKey = "__jsObjectId";
const dotNetObjectRefKey = "__dotNetObject";
const byteArrayRefKey = "__byte[]";
const dotNetStreamRefKey = "__dotNetStream";
const jsStreamReferenceLengthKey = "__jsStreamReferenceLength";

And then on the C# side (if necessary) have a custom converter for ElementReference, or have it look for the JSObjectIdKey too if there's already one.

Copy link
Member

@oroztocil oroztocil Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is more about adding an extensibility mechanism to processing values sent from JS to .NET that is symmetrical to "JSON revivers" used in the other direction (when handling arguments coming from .NET to JS). I am also not sure if we want to add that but we should evaluate it in these terms.

Alternatively, if we only care about supporting this use case with ElementReference, we can reuse the JSObjectReference ID representation and then special-case handling of Element instances in createJSCallResult in Microsoft.JSInterop.ts (or some other related function, I haven't look at it in detail).

1 change: 1 addition & 0 deletions src/Components/test/E2ETest/Tests/InteropTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ public void CanInvokeInteropMethods()
["testDtoAsync"] = "Same",
["returnPrimitiveAsync"] = "123",
["returnArrayAsync"] = "first,second",
["elementReference"] = "Success",
["jsObjectReference.identity"] = "Invoked from JSObjectReference",
["jsObjectReference.nested.add"] = "5",
["addViaJSObjectReference"] = "5",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@
<h2>@nameof(JSObjectReferenceInvokeNonFunctionException)</h2>
<p id="@nameof(JSObjectReferenceInvokeNonFunctionException)">@JSObjectReferenceInvokeNonFunctionException?.Message</p>
</div>

<p @ref="element">Element reference.</p>

@if (DoneWithInterop)
{
<p id="done-with-interop">Done with interop.</p>
Expand All @@ -70,6 +73,8 @@

public bool DoneWithInterop { get; set; }

public ElementReference element;

public async Task InvokeInteropAsync()
{
var shouldSupportSyncInterop = RuntimeInformation.IsOSPlatform(OSPlatform.Create("BROWSER"));
Expand Down Expand Up @@ -167,6 +172,19 @@
ReturnValues["invokeAsyncThrowsSerializingCircularStructure"] = $"Failure: {ex.Message}";
}

try
{
var elementReference = await JSRuntime.InvokeAsync<ElementReference>("returnElementReference", element);
ReturnValues["elementReference"] = "Success";
}
catch (JSException ex)
{
ReturnValues["elementReference"] = $"Failure: {ex.Message}";
}
catch (Exception ex)
{
ReturnValues["elementReference"] = $"Failure: {ex.Message}";
}

var jsObjectReference = await JSRuntime.InvokeAsync<IJSObjectReference>("returnJSObjectReference");
ReturnValues["jsObjectReference.identity"] = await jsObjectReference.InvokeAsync<string>("identity", "Invoked from JSObjectReference");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ window.jsInteropTests = {
receiveDotNetObjectByRefAsync: receiveDotNetObjectByRefAsync,
receiveDotNetStreamReference: receiveDotNetStreamReference,
receiveDotNetStreamWrapperReference: receiveDotNetStreamWrapperReference,
returnElementReference: returnElementReference,
TestClass: TestClass,
nonConstructorFunction: () => { return 42; },
testObject: testObject,
Expand Down Expand Up @@ -373,6 +374,10 @@ function returnJSObjectReference() {
};
}

function returnElementReference(element) {
return element;
}

function addViaJSObjectReference(jsObjectReference, a, b) {
return jsObjectReference.nested.add(a, b);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
export module DotNet {
export type JsonReviver = ((key: any, value: any) => any);
const jsonRevivers: JsonReviver[] = [];
export type JsonReplacer = ((key: any, value: any) => any);
const jsonReplacers: JsonReplacer[] = [];

const jsObjectIdKey = "__jsObjectId";
const dotNetObjectRefKey = "__dotNetObject";
Expand Down Expand Up @@ -118,6 +120,14 @@ export module DotNet {
jsonRevivers.push(reviver);
}

/**
* Adds a JSON replacer callback that will be used when serializing arguments sent to .NET.
* @param replacer The replacer to add.
*/
export function attachReplacer(replacer: JsonReplacer) {
jsonReplacers.push(replacer);
}

/**
* Invokes the specified .NET public method synchronously. Not all hosting scenarios support
* synchronous invocation, so if possible use invokeMethodAsync instead.
Expand Down Expand Up @@ -808,16 +818,21 @@ export module DotNet {
}

function argReplacer(key: string, value: any) {
if (value instanceof DotNetObject) {
return value.serializeAsArg();
} else if (value instanceof Uint8Array) {
const processedValue = jsonReplacers.reduce(
(currentValue, replacer) => replacer(key, currentValue),
value
);

if (processedValue instanceof DotNetObject) {
return processedValue.serializeAsArg();
} else if (processedValue instanceof Uint8Array) {
const dotNetCallDispatcher = currentCallDispatcher!.getDotNetCallDispatcher();
dotNetCallDispatcher!.sendByteArray(nextByteArrayIndex, value);
dotNetCallDispatcher!.sendByteArray(nextByteArrayIndex, processedValue);
const jsonValue = { [byteArrayRefKey]: nextByteArrayIndex };
nextByteArrayIndex++;
return jsonValue;
}

return value;
return processedValue;
}
}
Loading