Condividi tramite


Interoperabilità JavaScript [JSImport]/[JSExport] in .NET WebAssembly

Nota

Questa non è la versione più recente di questo articolo. Per la versione corrente, vedere la versione .NET 9 di questo articolo.

Avviso

Questa versione di ASP.NET Core non è più supportata. Per altre informazioni, vedere i criteri di supporto di .NET e .NET Core. Per la versione corrente, vedere la versione .NET 9 di questo articolo.

Importante

Queste informazioni si riferiscono a un prodotto non definitive che può essere modificato in modo sostanziale prima che venga rilasciato commercialmente. Microsoft non riconosce alcuna garanzia, espressa o implicita, in merito alle informazioni qui fornite.

Per la versione corrente, vedere la versione .NET 9 di questo articolo.

Di Aaron Shumaker

Questo articolo illustra come interagire con JavaScript (JS) in WebAssembly sul lato client usando JS/[JSImport][JSExport] l'interoperabilità (System.Runtime.InteropServices.JavaScript API).

[JSImport]/[JSExport] l'interoperabilità è applicabile quando si esegue un modulo WebAssembly .NET in un JS host negli scenari seguenti:

Prerequisiti

.NET SDK (versione più recente)

Uno dei tipi di progetto seguenti:

Esempio di app

Visualizzare o scaricare il codice di esempio (come scaricare): selezionare una cartella di versione 8.0 o successiva corrispondente alla versione di .NET che si sta adottando. All'interno della cartella della versione accedere all'esempio denominato WASMBrowserAppImportExportInterop.

JS interoperabilità tramite [JSImport]/[JSExport] attributi

L'attributo [JSImport] viene applicato a un metodo .NET per indicare che è necessario chiamare un metodo corrispondente JS quando viene chiamato il metodo .NET. In questo modo gli sviluppatori .NET possono definire "importazioni" che consentono al codice .NET di chiamare in JS. Inoltre, un Action oggetto può essere passato come parametro e JS può richiamare l'azione per supportare un callback o un modello di sottoscrizione di eventi.

L'attributo [JSExport] viene applicato a un metodo .NET per esporlo al JS codice. In questo modo il codice può JS avviare chiamate al metodo .NET.

Metodi di JS importazione

L'esempio seguente importa un metodo predefinito standard (console.log) in JS C#. [JSImport] è limitato all'importazione di metodi di oggetti accessibili a livello globale. Ad esempio, log è un metodo definito nell'oggetto console , definito nell'oggetto globalThisaccessibile a livello globale . Il console.log metodo viene mappato a un metodo proxy C#, ConsoleLog, che accetta una stringa per il messaggio di log:

public partial class GlobalInterop
{
    [JSImport("globalThis.console.log")]
    public static partial void ConsoleLog(string text);
}

ConsoleLog In Program.Mainviene chiamato con il messaggio per registrare:

GlobalInterop.ConsoleLog("Hello World!");

L'output viene visualizzato nella console del browser.

Di seguito viene illustrata l'importazione di un metodo dichiarato in JS.

Il metodo personalizzato JS seguente (globalThis.callAlert) genera una finestra di dialogo di avviso (window.alert) con il messaggio passato in text:

globalThis.callAlert = function (text) {
  globalThis.window.alert(text);
}

Il globalThis.callAlert metodo viene mappato a un metodo proxy C# (CallAlert), che accetta una stringa per il messaggio:

using System.Runtime.InteropServices.JavaScript;

public partial class GlobalInterop
{
	[JSImport("globalThis.callAlert")]
	public static partial void CallAlert(string text);
}

CallAlert In Program.Mainviene chiamato , passando il testo per il messaggio della finestra di dialogo di avviso:

GlobalInterop.CallAlert("Hello World");

La classe C# che dichiara il [JSImport] metodo non ha un'implementazione. In fase di compilazione, una classe parziale generata dall'origine contiene il codice .NET che implementa il marshalling della chiamata e dei tipi per richiamare il metodo corrispondente JS . In Visual Studio, usando le opzioni Vai a definizione o Vai all'implementazione passa rispettivamente alla classe parziale generata dall'origine o alla classe parziale definita dallo sviluppatore.

Nell'esempio precedente viene usata la dichiarazione intermedia globalThis.callAlertJS per eseguire il wrapping del codice esistente JS . Questo articolo si riferisce in modo informale alla dichiarazione intermedia JS come JS shim. JS gli shim riempiono il divario tra l'implementazione di .NET e le funzionalità/librerie esistenti JS . In molti casi, ad esempio l'esempio semplice precedente, lo JS shim non è necessario e i metodi possono essere importati direttamente, come illustrato nell'esempio precedente ConsoleLog . Come illustrato in questo articolo nelle prossime sezioni, uno JS shim può:

  • Incapsulare logica aggiuntiva.
  • Mappare manualmente i tipi.
  • Ridurre il numero di oggetti o chiamate che superano il limite di interoperabilità.
  • Eseguire manualmente il mapping delle chiamate statiche ai metodi di istanza.

Caricamento di dichiarazioni JavaScript

JS le dichiarazioni che devono essere importate con [JSImport] vengono in genere caricate nel contesto della stessa pagina o JS host che ha caricato .NET WebAssembly. Questa operazione può essere eseguita con:

  • Blocco <script>...</script> che dichiara inline JS.
  • Dichiarazione di origine script () (<script src="./some.js"></script>src) che carica un file esterno JS (.js).
  • Modulo JS ES6 (<script type='module' src="./moduleName.js"></script>).
  • Un JS modulo ES6 caricato usando JSHost.ImportAsync da .NET WebAssembly.

Gli esempi in questo articolo usano JSHost.ImportAsync. Quando si chiama ImportAsync, .NET WebAssembly sul lato client richiede il file usando il moduleUrl parametro e pertanto si prevede che il file sia accessibile come asset Web statico, molto allo stesso modo di un <script> tag recupera un file con un src URL. Ad esempio, il codice C# seguente all'interno di un progetto webAssembly Browser App mantiene il JS file (.js) nel percorso /wwwroot/scripts/ExampleShim.js:

await JSHost.ImportAsync("ExampleShim", "/scripts/ExampleShim.js");

A seconda della piattaforma che sta caricando WebAssembly, un URL con prefisso punto, ad esempio ./scripts/, potrebbe fare riferimento a una sottodirectory non corretta, ad esempio /_framework/scripts/, perché il pacchetto WebAssembly viene inizializzato dagli script del framework in /_framework/. In tal caso, il prefisso dell'URL con ../scripts/ fa riferimento al percorso corretto. Il prefisso con /scripts/ funziona se il sito è ospitato nella radice del dominio. Un approccio tipico prevede la configurazione del percorso di base corretto per l'ambiente specificato con un tag HTML <base> e l'uso del /scripts/ prefisso per fare riferimento al percorso relativo al percorso di base. I prefissi di notazione ~/ tilde non sono supportati da JSHost.ImportAsync.

Importante

Se JS viene caricato da un modulo JavaScript, [JSImport] gli attributi devono includere il nome del modulo come secondo parametro. Ad esempio, [JSImport("globalThis.callAlert", "ExampleShim")] indica che il metodo importato è stato dichiarato in un modulo JavaScript denominato "ExampleShim".

Mapping dei tipi

I parametri e i tipi restituiti nella firma del metodo .NET vengono convertiti automaticamente in o da tipi appropriati JS in fase di esecuzione se è supportato un mapping univoco. Ciò può comportare la conversione di valori in base al valore o ai riferimenti di cui è stato eseguito il wrapping in un tipo proxy. Questo processo è noto come marshalling dei tipi. Usare JSMarshalAsAttribute<T> per controllare la modalità di marshalling dei parametri del metodo importato e dei tipi restituiti.

Alcuni tipi non hanno un mapping dei tipi predefinito. Ad esempio, un long oggetto può essere sottoposto a marshalling come System.Runtime.InteropServices.JavaScript.JSType.Number o System.Runtime.InteropServices.JavaScript.JSType.BigInt, in modo che JSMarshalAsAttribute<T> sia necessario per evitare un errore in fase di compilazione.

Sono supportati gli scenari di mapping dei tipi seguenti:

  • Passaggio Action o Func<TResult> come parametri, di cui viene eseguito il marshalling come metodi chiamabili JS . Ciò consente al codice .NET di richiamare listener in risposta a JS callback o eventi.
  • Passaggio di JS riferimenti e riferimenti a oggetti gestiti .NET in entrambe le direzioni, che vengono sottoposto a marshalling come oggetti proxy e mantenuti attivi attraverso il limite di interoperabilità fino a quando il proxy non viene sottoposto a Garbage Collection.
  • Marshalling di metodi asincroni JS o con JS Promise un Task risultato e viceversa.

La maggior parte dei tipi con marshalling funziona in entrambe le direzioni, come parametri e come valori restituiti, sia nei metodi importati che in quello esportato.

La tabella seguente indica i mapping dei tipi supportati.

.NET JavaScript Nullable Task➔A Promise JSMarshalAs opzionale Array of
Boolean Boolean Supportata Supportata Supportata Non supportato
Byte Number Supportata Supportata Supportata Supportata
Char String Supportata Supportata Supportata Non supportato
Int16 Number Supportata Supportata Supportata Non supportato
Int32 Number Supportata Supportata Supportata Supportata
Int64 Number Supportata Supportata Non supportato Non supportato
Int64 BigInt Supportata Supportata Non supportato Non supportato
Single Number Supportata Supportata Supportata Non supportato
Double Number Supportata Supportata Supportata Supportata
IntPtr Number Supportata Supportata Supportata Non supportato
DateTime Date Supportata Supportata Non supportato Non supportato
DateTimeOffset Date Supportata Supportata Non supportato Non supportato
Exception Error Non supportato Supportata Supportata Non supportato
JSObject Object Non supportato Supportata Supportata Supportata
String String Non supportato Supportata Supportata Supportata
Object Any Non supportato Supportata Non supportato Supportata
Span<Byte> MemoryView Non supportato Non supportato Non supportato Non supportato
Span<Int32> MemoryView Non supportato Non supportato Non supportato Non supportato
Span<Double> MemoryView Non supportato Non supportato Non supportato Non supportato
ArraySegment<Byte> MemoryView Non supportato Non supportato Non supportato Non supportato
ArraySegment<Int32> MemoryView Non supportato Non supportato Non supportato Non supportato
ArraySegment<Double> MemoryView Non supportato Non supportato Non supportato Non supportato
Task Promise Non supportato Non supportato Supportata Non supportato
Action Function Non supportato Non supportato Non supportato Non supportato
Action<T1> Function Non supportato Non supportato Non supportato Non supportato
Action<T1, T2> Function Non supportato Non supportato Non supportato Non supportato
Action<T1, T2, T3> Function Non supportato Non supportato Non supportato Non supportato
Func<TResult> Function Non supportato Non supportato Non supportato Non supportato
Func<T1, TResult> Function Non supportato Non supportato Non supportato Non supportato
Func<T1, T2, TResult> Function Non supportato Non supportato Non supportato Non supportato
Func<T1, T2, T3, TResult> Function Non supportato Non supportato Non supportato Non supportato

Le condizioni seguenti si applicano al mapping dei tipi e ai valori con marshalling:

  • La Array of colonna indica se il tipo .NET può essere sottoposto a marshalling come JSArray. Esempio: C# int[] (Int32) mappato a JSArray s Number.
  • Quando si passa un JS valore a C# con un valore di tipo errato, il framework genera un'eccezione nella maggior parte dei casi. Il framework non esegue il controllo dei tipi in fase di compilazione in JS.
  • JSObject, ExceptionTask e ArraySegment creare GCHandle e un proxy. È possibile attivare l'eliminazione nel codice dello sviluppatore o consentire a .NET Garbage Collection (GC) di eliminare gli oggetti in un secondo momento. Questi tipi comportano un sovraccarico significativo delle prestazioni.
  • Array: il marshalling di una matrice crea una copia della matrice in JS o .NET.
  • MemoryView
    • MemoryView è una JS classe per il runtime .NET WebAssembly di effettuare il marshalling Span e ArraySegment.
    • A differenza del marshalling di una matrice, il marshalling di un Span oggetto o ArraySegment non crea una copia della memoria sottostante.
    • MemoryView può essere creata correttamente dall'istanza del runtime .NET WebAssembly. Non è quindi possibile importare un JS metodo come metodo .NET con un parametro di Span o ArraySegment.
    • MemoryView creato per un Span oggetto è valido solo per la durata della chiamata di interoperabilità. Poiché Span viene allocato nello stack di chiamate, che non viene salvato in modo permanente dopo la chiamata di interoperabilità, non è possibile esportare un metodo .NET che restituisce un oggetto Span.
    • MemoryView creato per un oggetto ArraySegment sopravvive dopo la chiamata di interoperabilità ed è utile per la condivisione di un buffer. La chiamata dispose() a un MemoryView oggetto creato per un ArraySegment oggetto elimina il proxy e rimuove la matrice .NET sottostante. È consigliabile chiamare dispose() in un try-finally blocco per MemoryView.

Alcune combinazioni di mapping dei tipi che richiedono tipi generici annidati in JSMarshalAs non sono attualmente supportate. Ad esempio, il tentativo di materializzare una matrice da un Promise oggetto, ad [return: JSMarshalAs<JSType.Promise<JSType.Array<JSType.Number>>>()] esempio, genera un errore in fase di compilazione. Una soluzione alternativa appropriata varia a seconda dello scenario, ma questo scenario specifico viene esaminato ulteriormente nella sezione Limitazioni del mapping dei tipi .

JS Primitive

L'esempio seguente illustra l'uso [JSImport] dei mapping dei tipi di diversi tipi primitivi JS e l'uso di , in cui i mapping espliciti sono necessari in fase di JSMarshalAscompilazione.

PrimitivesShim.js:

globalThis.counter = 0;

// Takes no parameters and returns nothing.
export function incrementCounter() {
  globalThis.counter += 1;
};

// Returns an int.
export function getCounter() { return globalThis.counter; };

// Takes a parameter and returns nothing. JS doesn't restrict the parameter type, 
// but we can restrict it in the .NET proxy, if desired.
export function logValue(value) { console.log(value); };

// Called for various .NET types to demonstrate mapping to JS primitive types.
export function logValueAndType(value) { console.log(typeof value, value); };

PrimitivesInterop.cs:

using System;
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;

public partial class PrimitivesInterop
{
    // Importing an existing JS method.
    [JSImport("globalThis.console.log")]
    public static partial void ConsoleLog([JSMarshalAs<JSType.Any>] object value);

    // Importing static methods from a JS module.
    [JSImport("incrementCounter", "PrimitivesShim")]
    public static partial void IncrementCounter();

    [JSImport("getCounter", "PrimitivesShim")]
    public static partial int GetCounter();

    // The JS shim method name isn't required to match the C# method name.
    [JSImport("logValue", "PrimitivesShim")]
    public static partial void LogInt(int value);

    // A second mapping to the same JS method with compatible type.
    [JSImport("logValue", "PrimitivesShim")]
    public static partial void LogString(string value);

    // Accept any type as parameter. .NET types are mapped to JS types where 
    // possible. Otherwise, they're marshalled as an untyped object reference 
    // to the .NET object proxy. The JS implementation logs to browser console 
    // the JS type and value to demonstrate results of marshalling.
    [JSImport("logValueAndType", "PrimitivesShim")]
    public static partial void LogValueAndType(
        [JSMarshalAs<JSType.Any>] object value);

    // Some types have multiple mappings and require explicit marshalling to the 
    // desired JS type. A long/Int64 can be mapped as either a Number or BigInt.
    // Passing a long value to the above method generates an error at runtime:
    // "ToJS for System.Int64 is not implemented." ("ToJS" means "to JavaScript")
    // If the parameter declaration `Method(JSMarshalAs<JSType.Any>] long value)` 
    // is used, a compile-time error is generated:
    // "Type long is not supported by source-generated JS interop...."
    // Instead, explicitly map the long parameter to either a JSType.Number or 
    // JSType.BigInt. Note that runtime overflow errors are possible in JS if the 
    // C# value is too large.
    [JSImport("logValueAndType", "PrimitivesShim")]
    public static partial void LogValueAndTypeForNumber(
        [JSMarshalAs<JSType.Number>] long value);

    [JSImport("logValueAndType", "PrimitivesShim")]
    public static partial void LogValueAndTypeForBigInt(
        [JSMarshalAs<JSType.BigInt>] long value);
}

public static class PrimitivesUsage
{
    public static async Task Run()
    {
        // Ensure JS module loaded.
        await JSHost.ImportAsync("PrimitivesShim", "/PrimitivesShim.js");

        // Call a proxy to a static JS method, console.log().
        PrimitivesInterop.ConsoleLog("Printed from JSImport of console.log()");

        // Basic examples of JS interop with an integer.
        PrimitivesInterop.IncrementCounter();
        int counterValue = PrimitivesInterop.GetCounter();
        PrimitivesInterop.LogInt(counterValue);
        PrimitivesInterop.LogString("I'm a string from .NET in your browser!");

        // Mapping some other .NET types to JS primitives.
        PrimitivesInterop.LogValueAndType(true);
        PrimitivesInterop.LogValueAndType(0x3A); // Byte literal
        PrimitivesInterop.LogValueAndType('C');
        PrimitivesInterop.LogValueAndType((Int16)12);
        // JS Number has a lower max value and can generate overflow errors.
        PrimitivesInterop.LogValueAndTypeForNumber(9007199254740990L); // Int64/Long
        // Next line: Int64/Long, JS BigInt supports larger numbers.
        PrimitivesInterop.LogValueAndTypeForBigInt(1234567890123456789L);// 
        PrimitivesInterop.LogValueAndType(3.14f); // Single floating point literal
        PrimitivesInterop.LogValueAndType(3.14d); // Double floating point literal
        PrimitivesInterop.LogValueAndType("A string");
    }
}

In Program.Main:

await PrimitivesUsage.Run();

Nell'esempio precedente viene visualizzato l'output seguente nella console di debug del browser:

Printed from JSImport of console.log()
1
I'm a string from .NET in your browser!
boolean true
number 58
number 67
number 12
number 9007199254740990
bigint 1234567890123456789n
number 3.140000104904175
number 3.14
string A string

Oggetti JSDate

Nell'esempio riportato in questa sezione vengono illustrati i metodi di importazione che hanno un JS Date oggetto come parametro o restituito. Le date vengono marshallate in base al valore di interoperabilità, ovvero vengono copiate nello stesso modo delle JS primitive.

Un Date oggetto è indipendente dal fuso orario. Un oggetto .NET DateTime viene modificato rispetto al relativo DateTimeKind quando viene eseguito il marshalling in un Dateoggetto , ma le informazioni sul fuso orario non vengono mantenute. Prendere in considerazione l'inizializzazione di un DateTime oggetto con un DateTimeKind.Utc oggetto o DateTimeKind.Local coerente con il valore rappresentato.

DateShim.js:

export function incrementDay(date) {
  date.setDate(date.getDate() + 1);
  return date;
}

export function logValueAndType(value) {
  console.log("Date:", value)
}

DateInterop.cs:

using System;
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;

public partial class DateInterop
{
    [JSImport("incrementDay", "DateShim")]
    [return: JSMarshalAs<JSType.Date>] // Explicit JSMarshalAs for a return type
    public static partial DateTime IncrementDay(
        [JSMarshalAs<JSType.Date>] DateTime date);

    [JSImport("logValueAndType", "DateShim")]
    public static partial void LogValueAndType(
        [JSMarshalAs<JSType.Date>] DateTime value);
}

public static class DateUsage
{
    public static async Task Run()
    {
        // Ensure JS module loaded.
        await JSHost.ImportAsync("DateShim", "/DateShim.js");

        // Basic examples of interop with a C# DateTime and JS Date.
        DateTime date = new(1968, 12, 21, 12, 51, 0, DateTimeKind.Utc);
        DateInterop.LogValueAndType(date);
        date = DateInterop.IncrementDay(date);
        DateInterop.LogValueAndType(date);
    }
}

In Program.Main:

await DateUsage.Run();

Nell'esempio precedente viene visualizzato l'output seguente nella console di debug del browser:

Date: Sat Dec 21 1968 07:51:00 GMT-0500 (Eastern Standard Time)
Date: Sun Dec 22 1968 07:51:00 GMT-0500 (Eastern Standard Time)

Le informazioni sul fuso orario precedente (GMT-0500 (Eastern Standard Time)) dipendono dal fuso orario locale del computer o del browser.

JS riferimenti all'oggetto

Ogni volta che un JS metodo restituisce un riferimento a un oggetto, viene rappresentato in .NET come .JSObject L'oggetto originale JS continua la durata all'interno del JS limite, mentre il codice .NET può accedervi e modificarlo facendo riferimento tramite .JSObject Mentre il tipo stesso espone un'API limitata, la possibilità di contenere un riferimento a un JS oggetto e restituirla o passarla attraverso il limite di interoperabilità consente il supporto per diversi scenari di interoperabilità.

JSObject fornisce metodi per accedere alle proprietà, ma non fornisce l'accesso diretto ai metodi di istanza. Come illustrato nel metodo seguente Summarize , è possibile accedere indirettamente ai metodi di istanza implementando un metodo statico che accetta l'istanza come parametro.

JSObjectShim.js:

export function createObject() {
  return {
    name: "Example JS Object",
    answer: 41,
    question: null,
    summarize: function () {
      return `Question: "${this.question}" Answer: ${this.answer}`;
    }
  };
}

export function incrementAnswer(object) {
  object.answer += 1;
  // Don't return the modified object, since the reference is modified.
}

// Proxy an instance method call.
export function summarize(object) {
  return object.summarize();
}

JSObjectInterop.cs:

using System;
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;

public partial class JSObjectInterop
{
    [JSImport("createObject", "JSObjectShim")]
    public static partial JSObject CreateObject();

    [JSImport("incrementAnswer", "JSObjectShim")]
    public static partial void IncrementAnswer(JSObject jsObject);

    [JSImport("summarize", "JSObjectShim")]
    public static partial string Summarize(JSObject jsObject);

    [JSImport("globalThis.console.log")]
    public static partial void ConsoleLog([JSMarshalAs<JSType.Any>] object value);
}

public static class JSObjectUsage
{
    public static async Task Run()
    {
        await JSHost.ImportAsync("JSObjectShim", "/JSObjectShim.js");

        JSObject jsObject = JSObjectInterop.CreateObject();
        JSObjectInterop.ConsoleLog(jsObject);
        JSObjectInterop.IncrementAnswer(jsObject);
        // An updated object isn't retrieved. The change is reflected in the 
        // existing instance.
        JSObjectInterop.ConsoleLog(jsObject);

        // JSObject exposes several methods for interacting with properties.
        jsObject.SetProperty("question", "What is the answer?");
        JSObjectInterop.ConsoleLog(jsObject);

        // We can't directly JSImport an instance method on the jsObject, but we 
        // can pass the object reference and have the JS shim call the instance 
        // method.
        string summary = JSObjectInterop.Summarize(jsObject);
        Console.WriteLine("Summary: " + summary);
    }
}

In Program.Main:

await JSObjectUsage.Run();

Nell'esempio precedente viene visualizzato l'output seguente nella console di debug del browser:

{name: 'Example JS Object', answer: 41, question: null, Symbol(wasm cs_owned_js_handle): 5, summarize: ƒ}
{name: 'Example JS Object', answer: 42, question: null, Symbol(wasm cs_owned_js_handle): 5, summarize: ƒ}
{name: 'Example JS Object', answer: 42, question: 'What is the answer?', Symbol(wasm cs_owned_js_handle): 5, summarize: ƒ}
Summary: Question: "What is the answer?" Answer: 42

Interoperabilità asincrona

Molte JS API sono asincrone e segnalano il completamento tramite un callback, un Promiseo un metodo asincrono. Ignorare le funzionalità asincrone spesso non è un'opzione, perché il codice successivo può dipendere dal completamento dell'operazione asincrona e deve essere atteso.

JS i metodi che usano la async parola chiave o la restituzione di un Promise oggetto possono essere attesi in C# da un metodo che restituisce un oggetto Task. Come illustrato di seguito, la async parola chiave non viene usata nel metodo C# con l'attributo [JSImport] perché non usa la await parola chiave al suo interno. Tuttavia, l'utilizzo del codice che chiama il metodo usa in genere la await parola chiave e viene contrassegnata come async, come illustrato nell'esempio PromisesUsage .

JS con un callback, ad esempio , setTimeoutpuò essere sottoposto a wrapping in un Promise oggetto prima di restituire da JS. Il wrapping di un callback in un Promiseoggetto , come illustrato nella funzione assegnata a Wait2Seconds, è appropriato solo quando il callback viene chiamato esattamente una volta. In caso contrario, è possibile passare un C# Action all'ascolto di un callback che può essere chiamato zero o più volte, come illustrato nella sezione Sottoscrizione agli JS eventi .

PromisesShim.js:

export function wait2Seconds() {
  // This also demonstrates wrapping a callback-based API in a promise to
  // make it awaitable.
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(); // Resolve promise after 2 seconds
    }, 2000);
  });
}

// Return a value via resolve in a promise.
export function waitGetString() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("String From Resolve"); // Return a string via promise
    }, 500);
  });
}

export function waitGetDate() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(new Date('1988-11-24')); // Return a date via promise
    }, 500);
  });
}

// Demonstrates an awaitable fetch.
export function fetchCurrentUrl() {
  // This method returns the promise returned by .then(*.text())
  // and .NET awaits the returned promise.
  return fetch(globalThis.window.location, { method: 'GET' })
    .then(response => response.text());
}

// .NET can await JS methods using the async/await JS syntax.
export async function asyncFunction() {
  await wait2Seconds();
}

// A Promise.reject can be used to signal failure and is bubbled to .NET code
// as a JSException.
export function conditionalSuccess(shouldSucceed) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (shouldSucceed)
        resolve(); // Success
      else
        reject("Reject: ShouldSucceed == false"); // Failure
    }, 500);
  });
}

Non usare la async parola chiave nella firma del metodo C#. Restituzione Task o Task<TResult> è sufficiente.

Quando si chiamano metodi asincroni JS , spesso si vuole attendere il completamento dell'esecuzione del JS metodo. Se si carica una risorsa o si effettua una richiesta, è probabile che il codice seguente presupporrà che l'azione venga completata.

Se lo JS shim restituisce un Promiseoggetto , C# può considerarlo come awaitable Task/Task<TResult>.

PromisesInterop.cs:

using System;
using System.Diagnostics;
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;

public partial class PromisesInterop
{
    // For a promise with void return type, declare a Task return type:
    [JSImport("wait2Seconds", "PromisesShim")]
    public static partial Task Wait2Seconds();

    [JSImport("waitGetString", "PromisesShim")]
    public static partial Task<string> WaitGetString();

    // Some return types require a [return: JSMarshalAs...] declaring the
    // Promise's return type corresponding to Task<T>.
    [JSImport("waitGetDate", "PromisesShim")]
    [return: JSMarshalAs<JSType.Promise<JSType.Date>>()]
    public static partial Task<DateTime> WaitGetDate();

    [JSImport("fetchCurrentUrl", "PromisesShim")]
    public static partial Task<string> FetchCurrentUrl();

    [JSImport("asyncFunction", "PromisesShim")]
    public static partial Task AsyncFunction();

    [JSImport("conditionalSuccess", "PromisesShim")]
    public static partial Task ConditionalSuccess(bool shouldSucceed);
}

public static class PromisesUsage
{
    public static async Task Run()
    {
        await JSHost.ImportAsync("PromisesShim", "/PromisesShim.js");

        Stopwatch sw = new();
        sw.Start();

        await PromisesInterop.Wait2Seconds(); // Await Promise
        Console.WriteLine($"Waited {sw.Elapsed.TotalSeconds:#.0}s.");

        sw.Restart();
        string str =
            await PromisesInterop.WaitGetString(); // Await promise (string return)
        Console.WriteLine(
            $"Waited {sw.Elapsed.TotalSeconds:#.0}s for WaitGetString: '{str}'");

        sw.Restart();
        // Await promise with string return.
        DateTime date = await PromisesInterop.WaitGetDate();
        Console.WriteLine(
            $"Waited {sw.Elapsed.TotalSeconds:#.0}s for WaitGetDate: '{date}'");

        // Await a JS fetch.
        string responseText = await PromisesInterop.FetchCurrentUrl();
        Console.WriteLine($"responseText.Length: {responseText.Length}");

        sw.Restart();

        await PromisesInterop.AsyncFunction(); // Await an async JS method
        Console.WriteLine(
            $"Waited {sw.Elapsed.TotalSeconds:#.0}s for AsyncFunction.");

        try
        {
            // Handle a promise rejection. Await an async JS method.
            await PromisesInterop.ConditionalSuccess(shouldSucceed: false);
        }
        catch (JSException ex) // Catch JS exception
        {
            Console.WriteLine($"JS Exception Caught: '{ex.Message}'");
        }
    }
}

In Program.Main:

await PromisesUsage.Run();

Nell'esempio precedente viene visualizzato l'output seguente nella console di debug del browser:

Waited 2.0s.
Waited .5s for WaitGetString: 'String From Resolve'
Waited .5s for WaitGetDate: '11/24/1988 12:00:00 AM'
responseText.Length: 582
Waited 2.0s for AsyncFunction.
JS Exception Caught: 'Reject: ShouldSucceed == false'

Limitazioni del mapping dei tipi di dati

Alcuni mapping dei tipi che richiedono tipi generici annidati nella JSMarshalAs definizione non sono attualmente supportati. Ad esempio, la restituzione di per Promise una matrice, ad [return: JSMarshalAs<JSType.Promise<JSType.Array<JSType.Number>>>()] esempio, genera un errore in fase di compilazione. Una soluzione alternativa appropriata varia a seconda dello scenario, ma un'opzione consiste nel rappresentare la matrice come JSObject riferimento. Ciò può essere sufficiente se l'accesso a singoli elementi all'interno di .NET non è necessario e il riferimento può essere passato ad altri JS metodi che agiscono sulla matrice. In alternativa, un metodo dedicato può accettare il JSObject riferimento come parametro e restituire la matrice materializzata, come illustrato nell'esempio seguente UnwrapJSObjectAsIntArray . In questo caso, il JS metodo non ha alcun controllo dei tipi e lo sviluppatore ha la responsabilità di assicurarsi che venga passato un JSObject wrapping del tipo di matrice appropriato.

export function waitGetIntArrayAsObject() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve([1, 2, 3, 4, 5]); // Return an array from the Promise
    }, 500);
  });
}

export function unwrapJSObjectAsIntArray(jsObject) {
  return jsObject;
}
// Not supported, generates compile-time error.
// [JSImport("waitGetArray", "PromisesShim")]
// [return: JSMarshalAs<JSType.Promise<JSType.Array<JSType.Number>>>()]
// public static partial Task<int[]> WaitGetIntArray();

// Workaround, take the return the call and pass it to UnwrapJSObjectAsIntArray.
// Return a JSObject reference to a JS number array.
[JSImport("waitGetIntArrayAsObject", "PromisesShim")]
[return: JSMarshalAs<JSType.Promise<JSType.Object>>()]
public static partial Task<JSObject> WaitGetIntArrayAsObject();

// Takes a JSObject reference to a JS number array, and returns the array as a C# 
// int array.
[JSImport("unwrapJSObjectAsIntArray", "PromisesShim")]
[return: JSMarshalAs<JSType.Array<JSType.Number>>()]
public static partial int[] UnwrapJSObjectAsIntArray(JSObject intArray);
//...

In Program.Main:

JSObject arrayAsJSObject = await PromisesInterop.WaitGetIntArrayAsObject();
int[] intArray = PromisesInterop.UnwrapJSObjectAsIntArray(arrayAsJSObject);

Considerazioni sulle prestazioni

Il marshalling delle chiamate e l'overhead di rilevamento degli oggetti attraverso il limite di interoperabilità è più costoso rispetto alle operazioni .NET native, ma deve comunque dimostrare prestazioni accettabili per un'app Web tipica con una domanda moderata.

I proxy degli oggetti, ad esempio JSObject, che mantengono i riferimenti oltre il limite di interoperabilità, hanno un sovraccarico di memoria aggiuntivo e influiscono sul modo in cui il Garbage Collection influisce su questi oggetti. Inoltre, la memoria disponibile potrebbe essere esaurita senza attivare Garbage Collection in alcuni scenari perché la pressione della memoria da JS e .NET non è condivisa. Questo rischio è significativo quando viene fatto riferimento a un numero eccessivo di oggetti di grandi dimensioni attraverso il limite di interoperabilità da oggetti relativamente piccoli JS o viceversa, in cui a oggetti .NET di grandi dimensioni viene fatto riferimento da JS proxy. In questi casi, è consigliabile seguire modelli di eliminazione deterministici con using ambiti che sfruttano l'interfaccia IDisposable sugli JS oggetti.

I benchmark seguenti, che sfruttano il codice di esempio precedente, dimostrano che le operazioni di interoperabilità sono approssimativamente un ordine di grandezza più lento rispetto a quelle che rimangono entro il limite .NET, ma le operazioni di interoperabilità rimangono relativamente veloci. Tenere inoltre presente che le funzionalità del dispositivo di un utente influisce sulle prestazioni.

JSObjectBenchmark.cs:

using System;
using System.Diagnostics;

public static class JSObjectBenchmark
{
    public static void Run()
    {
        Stopwatch sw = new();
        var jsObject = JSObjectInterop.CreateObject();

        sw.Start();

        for (int i = 0; i < 1000000; i++)
        {
            JSObjectInterop.IncrementAnswer(jsObject);
        }

        sw.Stop();

        Console.WriteLine(
            $"JS interop elapsed time: {sw.Elapsed.TotalSeconds:#.0000} seconds " +
            $"at {sw.Elapsed.TotalMilliseconds / 1000000d:#.000000} ms per " +
            "operation");

        var pocoObject =
            new PocoObject { Question = "What is the answer?", Answer = 41 };
        sw.Restart();

        for (int i = 0; i < 1000000; i++)
        {
            pocoObject.IncrementAnswer();
        }

        sw.Stop();

        Console.WriteLine($".NET elapsed time: {sw.Elapsed.TotalSeconds:#.0000} " +
            $"seconds at {sw.Elapsed.TotalMilliseconds / 1000000d:#.000000} ms " +
            "per operation");

        Console.WriteLine($"Begin Object Creation");

        sw.Restart();

        for (int i = 0; i < 1000000; i++)
        {
            var jsObject2 = JSObjectInterop.CreateObject();
            JSObjectInterop.IncrementAnswer(jsObject2);
        }

        sw.Stop();

        Console.WriteLine(
            $"JS interop elapsed time: {sw.Elapsed.TotalSeconds:#.0000} seconds " +
            $"at {sw.Elapsed.TotalMilliseconds / 1000000d:#.000000} ms per " +
            "operation");

        sw.Restart();

        for (int i = 0; i < 1000000; i++)
        {
            var pocoObject2 =
                new PocoObject { Question = "What is the answer?", Answer = 0 };
            pocoObject2.IncrementAnswer();
        }

        sw.Stop();
        Console.WriteLine(
            $".NET elapsed time: {sw.Elapsed.TotalSeconds:#.0000} seconds at " +
            $"{sw.Elapsed.TotalMilliseconds / 1000000d:#.000000} ms per operation");
    }

    public class PocoObject // Plain old CLR object
    {
        public string Question { get; set; }
        public int Answer { get; set; }

        public void IncrementAnswer() => Answer += 1;
    }
}

In Program.Main:

JSObjectBenchmark.Run();

Nell'esempio precedente viene visualizzato l'output seguente nella console di debug del browser:

JS interop elapsed time: .2536 seconds at .000254 ms per operation
.NET elapsed time: .0210 seconds at .000021 ms per operation
Begin Object Creation
JS interop elapsed time: 2.1686 seconds at .002169 ms per operation
.NET elapsed time: .1089 seconds at .000109 ms per operation

Sottoscrizione di JS eventi

Il codice .NET può sottoscrivere JS eventi e gestire JS gli eventi passando un C# Action a una JS funzione per fungere da gestore. Il JS codice shim gestisce la sottoscrizione all'evento.

Avviso

L'interazione con le singole proprietà del DOM tramite JS interoperabilità, come illustrato nella guida descritta in questa sezione, è relativamente lenta e può causare la creazione di molti proxy che creano un elevato utilizzo di Garbage Collection. Il modello seguente non è in genere consigliato. Usare il modello seguente per non più di alcuni elementi. Per altre informazioni, vedere la sezione Considerazioni sulle prestazioni .

Una sfumatura di removeEventListener è che richiede un riferimento alla funzione passata in precedenza a addEventListener. Quando un C# Action viene passato attraverso il limite di interoperabilità, viene eseguito il wrapping in un JS oggetto proxy. Pertanto, passando lo stesso C# Action a e removeEventListener addEventListener comporta la generazione di due diversi JS oggetti proxy di wrapping di Action. Questi riferimenti sono diversi, pertanto removeEventListener non è in grado di trovare il listener di eventi da rimuovere. Per risolvere questo problema, gli esempi seguenti escludono C# Action in una JS funzione e restituiscono il riferimento come oggetto JSObject dalla chiamata di sottoscrizione per passare successivamente alla chiamata di annullamento della sottoscrizione. Poiché viene restituito e passato C# Action come , JSObjectlo stesso riferimento viene usato per entrambe le chiamate e il listener di eventi può essere rimosso.

EventsShim.js:

export function subscribeEventById(elementId, eventName, listenerFunc) {
  const elementObj = document.getElementById(elementId);

  // Need to wrap the Managed C# action in JS func (only because it is being 
  // returned).
  let handler = function (event) {
    listenerFunc(event.type, event.target.id); // Decompose object to primitives
  }.bind(elementObj);

  elementObj.addEventListener(eventName, handler, false);
  // Return JSObject reference so it can be used for removeEventListener later.
  return handler;
}

// Param listenerHandler must be the JSObject reference returned from the prior 
// SubscribeEvent call.
export function unsubscribeEventById(elementId, eventName, listenerHandler) {
  const elementObj = document.getElementById(elementId);
  elementObj.removeEventListener(eventName, listenerHandler, false);
}

export function triggerClick(elementId) {
  const elementObj = document.getElementById(elementId);
  elementObj.click();
}

export function getElementById(elementId) {
  return document.getElementById(elementId);
}

export function subscribeEvent(elementObj, eventName, listenerFunc) {
  let handler = function (e) {
    listenerFunc(e);
  }.bind(elementObj);

  elementObj.addEventListener(eventName, handler, false);
  return handler;
}

export function unsubscribeEvent(elementObj, eventName, listenerHandler) {
  return elementObj.removeEventListener(eventName, listenerHandler, false);
}

export function subscribeEventFailure(elementObj, eventName, listenerFunc) {
  // It's not strictly required to wrap the C# action listenerFunc in a JS 
  // function.
  elementObj.addEventListener(eventName, listenerFunc, false);
  // If you need to return the wrapped proxy object, you will receive an error 
  // when it tries to wrap the existing proxy in an additional proxy:
  // Error: "JSObject proxy of ManagedObject proxy is not supported."
  return listenerFunc;
}

EventsInterop.cs:

using System;
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;

public partial class EventsInterop
{
    [JSImport("subscribeEventById", "EventsShim")]
    public static partial JSObject SubscribeEventById(string elementId,
        string eventName,
        [JSMarshalAs<JSType.Function<JSType.String, JSType.String>>]
        Action<string, string> listenerFunc);

    [JSImport("unsubscribeEventById", "EventsShim")]
    public static partial void UnsubscribeEventById(string elementId,
        string eventName, JSObject listenerHandler);

    [JSImport("triggerClick", "EventsShim")]
    public static partial void TriggerClick(string elementId);

    [JSImport("getElementById", "EventsShim")]
    public static partial JSObject GetElementById(string elementId);

    [JSImport("subscribeEvent", "EventsShim")]
    public static partial JSObject SubscribeEvent(JSObject htmlElement,
        string eventName,
        [JSMarshalAs<JSType.Function<JSType.Object>>]
        Action<JSObject> listenerFunc);

    [JSImport("unsubscribeEvent", "EventsShim")]
    public static partial void UnsubscribeEvent(JSObject htmlElement,
        string eventName, JSObject listenerHandler);
}

public static class EventsUsage
{
    public static async Task Run()
    {
        await JSHost.ImportAsync("EventsShim", "/EventsShim.js");

        Action<string, string> listenerFunc = (eventName, elementId) =>
            Console.WriteLine(
                $"In C# event listener: Event {eventName} from ID {elementId}");

        // Assumes two buttons exist on the page with ids of "btn1" and "btn2"
        JSObject listenerHandler1 =
            EventsInterop.SubscribeEventById("btn1", "click", listenerFunc);
        JSObject listenerHandler2 =
            EventsInterop.SubscribeEventById("btn2", "click", listenerFunc);
        Console.WriteLine("Subscribed to btn1 & 2.");
        EventsInterop.TriggerClick("btn1");
        EventsInterop.TriggerClick("btn2");

        EventsInterop.UnsubscribeEventById("btn2", "click", listenerHandler2);
        Console.WriteLine("Unsubscribed btn2.");
        EventsInterop.TriggerClick("btn1");
        EventsInterop.TriggerClick("btn2"); // Doesn't trigger because unsubscribed
        EventsInterop.UnsubscribeEventById("btn1", "click", listenerHandler1);
        // Pitfall: Using a different handler for unsubscribe silently fails.
        // EventsInterop.UnsubscribeEventById("btn1", "click", listenerHandler2);

        // With JSObject as event target and event object.
        Action<JSObject> listenerFuncForElement = (eventObj) =>
        {
            string eventType = eventObj.GetPropertyAsString("type");
            JSObject target = eventObj.GetPropertyAsJSObject("target");
            Console.WriteLine(
                $"In C# event listener: Event {eventType} from " +
                $"ID {target.GetPropertyAsString("id")}");
        };

        JSObject htmlElement = EventsInterop.GetElementById("btn1");
        JSObject listenerHandler3 = EventsInterop.SubscribeEvent(
            htmlElement, "click", listenerFuncForElement);
        Console.WriteLine("Subscribed to btn1.");
        EventsInterop.TriggerClick("btn1");
        EventsInterop.UnsubscribeEvent(htmlElement, "click", listenerHandler3);
        Console.WriteLine("Unsubscribed btn1.");
        EventsInterop.TriggerClick("btn1");
    }
}

In Program.Main:

await EventsUsage.Run();

Nell'esempio precedente viene visualizzato l'output seguente nella console di debug del browser:

Subscribed to btn1 & 2.
In C# event listener: Event click from ID btn1
In C# event listener: Event click from ID btn2
Unsubscribed btn2.
In C# event listener: Event click from ID btn1
Subscribed to btn1.
In C# event listener: Event click from ID btn1
Unsubscribed btn1.

JS[JSImport]/[JSExport] scenari di interoperabilità

Gli articoli seguenti sono incentrati sull'esecuzione di un modulo WebAssembly .NET in un JS host, ad esempio un browser: