├── .github └── FUNDING.yml ├── .gitignore ├── Architecture.md ├── Archive.md ├── CSharpWasmRecipes.sln ├── LICENSE ├── README.md ├── RecipesLibrary ├── Class1.cs ├── RecipesLibrary.csproj └── TODO Show Both syntaxes.cs ├── UnoBootstrap.Recipes.WasmClient ├── Advanced │ └── WrappingJSObjectHandle.cs ├── Basic │ ├── InvokeJSExamples.cs │ ├── JSImportExample.cs │ ├── JSObjectExample.cs │ ├── PromisesWNet7.cs │ ├── PromisesWUno.cs │ └── Security.cs ├── ExamplesJSShim - Working Export with GlobaTHis.js ├── Intermediate │ ├── InvokeJSHelpers.cs │ └── InvokeJSHelpersExamples.cs ├── Program.cs ├── Properties │ ├── AssemblyInfo.cs │ └── launchSettings.json └── UnoBootstrap.Recipes.WasmClient.csproj └── WasmBrowser.Recipes.WasmClient ├── Examples ├── DateProxy.cs ├── EventsProxy.cs ├── JSObjectBenchmark.cs ├── JSObjectProxy.cs ├── PrimitivesProxy.cs ├── PromisesProxy.cs ├── ProxylessWrapper.cs ├── RefactoredNamesProxy.cs ├── StronglyTypedEvents.cs └── StronglyTypedWrapper.cs ├── Program.cs ├── Properties ├── AssemblyInfo.cs ├── Resources.Designer.cs ├── Resources.resx └── launchSettings.json ├── README.md ├── Startup.cs ├── WasmBrowser.Recipes.WasmClient.csproj ├── WasmBrowserTemplateExample.cs ├── runtimeconfig.template.json └── wwwroot ├── DateShim.js ├── DomShim.js ├── EventsShim.js ├── JSObjectShim.js ├── PrimitivesShim.js ├── PromisesShim.js ├── ProxylessDomShim.js ├── RefactoredNamesShim.js ├── index.html └── main.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: SerratedSharp 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.tlog 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 298 | *.vbp 299 | 300 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 301 | *.dsw 302 | *.dsp 303 | 304 | # Visual Studio 6 technical files 305 | *.ncb 306 | *.aps 307 | 308 | # Visual Studio LightSwitch build output 309 | **/*.HTMLClient/GeneratedArtifacts 310 | **/*.DesktopClient/GeneratedArtifacts 311 | **/*.DesktopClient/ModelManifest.xml 312 | **/*.Server/GeneratedArtifacts 313 | **/*.Server/ModelManifest.xml 314 | _Pvt_Extensions 315 | 316 | # Paket dependency manager 317 | .paket/paket.exe 318 | paket-files/ 319 | 320 | # FAKE - F# Make 321 | .fake/ 322 | 323 | # CodeRush personal settings 324 | .cr/personal 325 | 326 | # Python Tools for Visual Studio (PTVS) 327 | __pycache__/ 328 | *.pyc 329 | 330 | # Cake - Uncomment if you are using it 331 | # tools/** 332 | # !tools/packages.config 333 | 334 | # Tabs Studio 335 | *.tss 336 | 337 | # Telerik's JustMock configuration file 338 | *.jmconfig 339 | 340 | # BizTalk build output 341 | *.btp.cs 342 | *.btm.cs 343 | *.odx.cs 344 | *.xsd.cs 345 | 346 | # OpenCover UI analysis results 347 | OpenCover/ 348 | 349 | # Azure Stream Analytics local run output 350 | ASALocalRun/ 351 | 352 | # MSBuild Binary and Structured Log 353 | *.binlog 354 | 355 | # NVidia Nsight GPU debugger configuration file 356 | *.nvuser 357 | 358 | # MFractors (Xamarin productivity tool) working folder 359 | .mfractor/ 360 | 361 | # Local History for Visual Studio 362 | .localhistory/ 363 | 364 | # Visual Studio History (VSHistory) files 365 | .vshistory/ 366 | 367 | # BeatPulse healthcheck temp database 368 | healthchecksdb 369 | 370 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 371 | MigrationBackup/ 372 | 373 | # Ionide (cross platform F# VS Code tools) working folder 374 | .ionide/ 375 | 376 | # Fody - auto-generated XML schema 377 | FodyWeavers.xsd 378 | 379 | # VS Code files for those working on multiple tools 380 | .vscode/* 381 | !.vscode/settings.json 382 | !.vscode/tasks.json 383 | !.vscode/launch.json 384 | !.vscode/extensions.json 385 | *.code-workspace 386 | 387 | # Local History for Visual Studio Code 388 | .history/ 389 | 390 | # Windows Installer files from build outputs 391 | *.cab 392 | *.msi 393 | *.msix 394 | *.msm 395 | *.msp 396 | 397 | # JetBrains Rider 398 | *.sln.iml 399 | -------------------------------------------------------------------------------- /Architecture.md: -------------------------------------------------------------------------------- 1 | 2 | # Architecture 3 | 4 | ## Runtime Dependencies 5 | 6 | WebAssembly/WASM support is dependent upon the browser's support for the WebAssembly standard, and is currently supported by the newest version of all major browsers: https://caniuse.com/?search=WebAssembly 7 | 8 | Otherwise at runtime the resulting WASM package is platform agnostic, and there is no external dependency on specific hosting technologies, web application technologies, or programming languages. At runtime, any HTML page that can load and execute the embedded.js bootstrapper will be able to successfully load and execute the WASM package. 9 | 10 | ## Interop 11 | 12 | WASM interfaces with the web application or HTML page client side through javascript, known as JS interop, and/or via traditional web requests against the backend host such as REST. Using native .NET 7 JS interop, static JS methods can be called from .NET. Arbitrary JS cannot be executed via this approach, thus typical implementations require a JS implementation to act as a shim for the .NET interop. In turn, JSExport'd methods can be accessed from an existing web application's client side JS as though they were static methods exposed from a traditional JS library. WASM also supports JS promises and events offering additional integration options. 13 | 14 | The WASM package does not have direct access to all browser capabilities, but JS shims can be used to access those capabilities indirectly. For example, access to the HTML DOM can be implemented by JSImport'ing JS shims calling native JS methods such as findElementById or creating a wrapper around a library such as jQuery as is done in SerratedJQ. 15 | 16 | .NET 7 WASM supports HttpClient, allowing requests to be made directly to the backend host using traditional web requests that would be compatible with any hosting technology exposing traditional HTTP endpoints. This could be used to retrieve HTML fragments, or JSON data models, either of which could be used to data driven logic or dynamically updating HTML. 17 | 18 | The Uno WebAssemblyRuntime library provides methods to execute arbitrary javascript from .NET, but this should generally only be used for static JS that is not parameterized nor dynamic, due to security risks of dynamic JS. It can be useful for creating JS declarations for the interop shims since these consist of static JS. 19 | 20 | ## Hosting 21 | 22 | At runtime, the WASM package files will be downloaded from the server the same way static files such as images or JS would be downloaded, and then executed within the browser. This is completely hosting platform agnostic, since from the host's perspective it is a simple file download request. Often the host will need to be configured to allow files with *.clr and *.dat extensions to be downloaded, typically accomplished by adding mime types. 23 | 24 | ## Loading the WASM Package at Runtime 25 | 26 | The HTML pages will need to reference the appropriate javascript to load the WASM packages. Javascript files included by Uno.Bootstrap handle the initial loading of the runtime. For example, if using EmbeddedMode, then a script tag referencing the WASM package's `embedded.js` would handle bootstrapping the WASM package, then execute our WASM entry point which is the console project's `Program.Main()`. 27 | 28 | The WASM package can either be hosted from the same site as the primary application, or in a separate application. Similar to any other javascript, it can be loaded from a relative URL (hosted in a subpath of the web app) or from another site. 29 | 30 | ## Solution Structure 31 | 32 | ### Single Page Application 33 | 34 | - Solution 35 | - \*.WasmClient Console Project 36 | - References Uno.Bootstrap.Wasm 37 | - In Dev/Debug environment, references Uno.Wasm.Bootstrap.DevServer to act as the static file host and support the debugging connection. 38 | - Hosts and serves its own index.html, which automatically includes and executes necessary javascript to load and execute the WASM package. 39 | - Optionally communicates with backend or remote APIs via HttpClient or ClientWebSocket using HTTP requests or WebSocket connections. Allowing communication with backend APIs that may or may not necessarily be .NET hosts. 40 | 41 | This application could be built and deployed to any host supporting static files, which means a .NET host or backend is not required. 42 | 43 | Without a backend API, such an application might still be useful as some sort of calculator that runs completely client side. [C# WASM JQuery Demo](https://serratedsharp.github.io/CSharpWasmJQueryDemo/) is a trivial example of an application with no backend host other than being served as static files from github.io, running client side in the browser, with no backend API communication. 44 | 45 | ### ASP.NET (MVC) Hosted Application 46 | 47 | - Solution 48 | - \*.WasmClient Console Project 49 | - References Uno.Bootstrap.Wasm 50 | - In Dev/Debug environment, references Uno.Wasm.Bootstrap.DevServer 51 | - Does **not** serve its index.html page. 52 | - Communicates with the ASP.NET backend or other remote APIs via HttpClient or ClientWebSocket using HTTP requests or WebSocket connections. 53 | - ASP.NET (MVC) Project 54 | - In Dev/Debug environment, loads the WasmClient via `` where :11111 is the port the WasmClient's Uno.Bootstrap.DevServer is listening on. This is found in WasmClient's launcSettings.json applicaitonUrl. 55 | - In Test/Production environment, loads the WasmClient via `` where the path is relative to the ASP.NET project's wwwroot folder and the WASM package's dist files have been copied at publish/deployment time into the ASP.NET's wwwroot. 56 | - Exposes API endpoints that the WasmClient calls to perform actions, return data models, or return HTML fragments (partial view). 57 | 58 | ### Optional Projects 59 | 60 | - WebAPI Project (optional) 61 | - Exposes API endpoints that the WasmClient calls to retrieve data models, HTML fragments, or perform operations. 62 | - \*.WasmShared Class Library Project (optional) 63 | - Contains shared code that is referenced by both the WasmClient and WebAPI projects. 64 | - Typically contains client API data models, client/server side dual validations, and other code that is used by both the client and server. 65 | - Reduces duplication of code that would often be replicated in both C# and Javascript. 66 | - Creates a clear delineation of what code is included in the client side package. 67 | 68 | 69 | 70 | ## Debugging 71 | 72 | There is a WASM debugger available within Chrome DevTools which is covered in Uno Bootstrap's [Using the browser debugger](https://platform.uno/docs/articles/debugging-wasm.html#using-the-browser-debugger) documentation. 73 | 74 | Debugging integration is also supported in Visual Studio 2022 with an experience more familiar to .NET developers. This supports breakpoints within the WASM project's C# code, stepping through code, and inspecting values. The following has been verified in VS 2022 >=17.8. 75 | 76 | In the local development environment a browser link websocket is used to communicate breakpoint and line number information between the browser where the code is executing and the Uno.Bootstrap.DevServer webhost, which in turn drives the debugging experiene in Visual Studio. Only one connection at a time will work, so launching multiple browsers will cause all but the first to fail to connect the for debugging. In both of the following scenarios, the inspectUrl connects to the WasmClient's Uno.Bootstrap.DevServer host since only that host has the capability to communicate debugging information. 77 | 78 | 79 | ### Debugging Config for Single Page Application 80 | 81 | The Properties/launchSettings.json file of the WASM console project would include an inspectUri that defines the webSocket that the browser will connect to for communicating debugging information: 82 | 83 | { 84 | "profiles": { 85 | "Sample.WasmClient": { 86 | "commandName": "Project", 87 | "launchBrowser": true, 88 | "environmentVariables": { 89 | "ASPNETCORE_ENVIRONMENT": "Development" 90 | }, 91 | "applicationUrl": "https://localhost:50044;http://localhost:50045", 92 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}" 93 | } 94 | } 95 | } 96 | 97 | When the WasmClient is launched, a browser is launched connecting to the index.html, and javascript will initiate a websocket connection to the inspectUri hosted by the Uno.Bootstrap.DevServer. 98 | 99 | For embdedded mode the inspectUri is instead set in the ASP.NET application, and points to the host:port of the WasmClient's Uno.Bootsrap.DevServer host. 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /Archive.md: -------------------------------------------------------------------------------- 1 | Access the table of contents via the ![image](https://github.com/SerratedSharp/CSharpWasmRecipes/assets/97156524/49113928-1bd7-4e7c-8c28-b1aafa035744) icon. 2 | 3 | To improve readability, navigate [here](README.md) then collapse the files drawer: 4 | 5 | ![image](https://github.com/SerratedSharp/CSharpWasmRecipes/assets/97156524/b850c806-7631-40b2-ac8d-e7cb4a10b386) 6 | 7 | ## Legacy Documentation Being Reworked 8 | 9 | ### JS Object References 10 | 11 | #### Using .NET 7 JSObject 12 | 13 | The System.Runtime.InteropServices.JavaScript.JSObject type can be used in function signatures as parameters or return types in `[JSImport]`/`[JSExport]` attributed methods and represents a reference to a javascript object. (Warning: Be mindful of using references as Uno.Foundation.Interop contains a different legacy JSObject type that will not work in the below examples.) 14 | 15 | ```C# 16 | using System.Runtime.InteropService.JavaScript; 17 | public partial class JSObjectExample 18 | { 19 | [JSImport("globalThis.JSON.parse")] 20 | public static partial JSObject GetJsonAsJSObject(string jsonString); 21 | 22 | [JSImport("globalThis.console.log")] 23 | public static partial void ConsoleLogJSObject(JSObject obj); 24 | } 25 | 26 | //Usage: 27 | JSObject jsObj = JSObjectExample.GetJsonAsJSObject(""" 28 | {"firstName":"Crow","middleName":"T","lastName":"Robot"} 29 | """); 30 | JSObjectExample.ConsoleLogJSObject(jsObj); 31 | ``` 32 | 33 | The GetJsonAsJSObject method takes a string, then deserializes it to an JS object, and returns the JSObject reference. 34 | 35 | Browser Console Output: 36 | 37 | ![image](https://github.com/SerratedSharp/CSharpWasmRecipes/assets/97156524/317a6793-2783-4ddd-a5ce-0df12acc5f1a) 38 | 39 | #### Accessing JSObject Properties 40 | 41 | Just about any property or method of a JSObject can be accessed by declaring a JSProxy and implementing custom JS: 42 | 43 | ```JS 44 | // Concatenate first, middle, and last name: 45 | globalThis.concatenateName = function (nameObject) { 46 | return obj.firstName + " " + obj.middleName + " " + obj.lastName; 47 | } 48 | ``` 49 | 50 | ```C# 51 | public partial class JSObjectExample 52 | { 53 | [JSImport("globalThis.concatenateName")] 54 | public static partial string ConcatenateName(JSObject nameObject); 55 | } 56 | // Usage: 57 | JSObject jsObj = JSObjectExample.GetJsonAsJSObject("""{"firstName":"Crow","middleName":"T","lastName":"Robot"}"""); 58 | string fullName = JSObjectExample.ConcatenateName(jsObj); 59 | ``` 60 | 61 | The above may be appropriate where multiple operations can occur in a single JS interop call. Alternatively, the JSObject exposes a series of methods for accessing or setting properties of the underlying type: 62 | 63 | ![image](https://github.com/SerratedSharp/CSharpWasmRecipes/assets/97156524/e24684e6-12be-4ab0-b972-f6e7a47d6bcb) 64 | 65 | ```C# 66 | JSObject jsObj = JSObjectExample.GetJsonAsJSObject( 67 | """ 68 | { 69 | "firstName":"Crow", 70 | "middleName":"T", 71 | "lastName":"Robot", 72 | "innerObj": 73 | { 74 | "prop1":"innerObj Prop1 Value", 75 | "prop2":"innerObj Prop2 Value" 76 | } 77 | } 78 | """); 79 | 80 | // Store a reference in JS globalThis to demonstrate by ref modification 81 | JSHost.GlobalThis.SetProperty("jsObj", jsObj); 82 | 83 | JSObjectExample.ConsoleLogJSObject(jsObj); 84 | string lastName = jsObj.GetPropertyAsString("lastName"); 85 | Console.WriteLine("LastName: " + lastName); // "LastName: Robot" 86 | Console.WriteLine("Type: " + jsObj.GetType()); // "Type: System.Runtime.InteropServices.JavaScript.JSObject" 87 | Console.WriteLine("lastName Type: " + jsObj.GetTypeOfProperty("lastName")); // "lastName Type: string" 88 | 89 | Console.WriteLine("innerObj Type: " + jsObj.GetTypeOfProperty("innerObj")); // "innerObj Type: object" 90 | JSObject innerObj = jsObj.GetPropertyAsJSObject("innerObj"); 91 | string innerProp1 = innerObj.GetPropertyAsString("prop1"); 92 | Console.WriteLine("innerProp1: " + innerProp1); // "innerProp1: innerObj Prop1 Value" 93 | 94 | innerObj.SetProperty("prop1", "Update Value"); 95 | Console.WriteLine("innerObj.innerProp1: " + innerObj.GetPropertyAsString("prop1")); // "innerObj.innerProp1: Update Value" 96 | 97 | innerObj.SetProperty("prop3", "Value of Added Property"); // Add new property 98 | Console.WriteLine("innerObj.innerProp3: " + innerObj.GetPropertyAsString("prop3")); // "innerObj.innerProp3: Value of Added Property" 99 | ``` 100 | 101 | Modifying or adding properties in this way via the JSObject reference will also affect the original JS object if there were references to it from JavaScript as we can see with the object reference we assigned to globalThis: 102 | ```C# 103 | JSObjectExample.Log("jsObj: ", jsObj); 104 | JSObject originalObj = JSHost.GlobalThis.GetPropertyAsJSObject("jsObj"); 105 | JSObjectExample.Log("originalObj: ", originalObj); 106 | ``` 107 | 108 | Comparing the output of the two `JSObjectExample.Log()` statements, we can see the reference we modified throughout the code matches the original reference we stored and retrieved from JS globalThis: 109 | 110 | ![image](https://github.com/SerratedSharp/CSharpWasmRecipes/assets/97156524/5554e8a5-0a36-47d8-8a15-3a85b4bebff2) 111 | 112 | ### Instances and Instance Methods 113 | 114 | .NET 7 provides the JSObject (in System.Runtime.InteropServices.JavaScript) type that represents a javascript object reference. Think of this type as being similar to an `object`, in that it is not strongly typed and can hold a reference to any JS type. Although the type exposes limited functionality, the ability to hold a reference to a JS Object from .NET and return/pass it across the interop layer opens up a multitude of capabilities. Wrappers composed of JSObject references and proxy methods/properties can present a strongly typed interface for JS types. For example, SerratedJQ's JQueryPlainObject is a strongly typed wrapper for JQuery objects and internally uses a JSObject for the reference to the JQuery object instance. 115 | 116 | Because .NET 7 doesn't currently support importing JS instance methods directly, we declare a static method in JS and import it into C#, with the first parameter being the JS instance we want to operate on. 117 | 118 | Note: VS2022 can often automatically add Uno.Foundation.Interop using the incorrect JSObject causing confusing compilation errors. 119 | 120 | #### JSObject Wrappers 121 | 122 | Let's look at developing an interface for interacting with JS types from C#. We'll use vanilla HTML elements and HTML DOM methods as our example, but this same approach can be applied to custom JS types. This demonstrates one opinionated approach to mapping JS instance methods, but demonstrates the fundamentals that would be used in some form by most approaches. 123 | 124 | Declaring static javascript methods: 125 | ```JS 126 | // findElement takes a string and returns an object (an HTML element reference) 127 | // getClass takes an object, calls an instance method on that object, and returns a string 128 | globalThis.findElement = function(id) { return document.getElementById(id); } 129 | globalThis.getClass = function(obj) { return obj.getAttribute('class'); } 130 | ``` 131 | 132 | The `getClass` method takes a JS object as a parameter, then calls an instance method on it. 133 | 134 | Import static JS methods into C#: 135 | ```C# 136 | using System.Runtime.InteropServices.JavaScript; 137 | public partial class HtmlElementProxy 138 | { 139 | [JSImport("globalThis.findElement")] 140 | public static partial JSObject FindElement(string id); 141 | 142 | [JSImport("globalThis.getClass")] 143 | public static partial string GetClass(JSObject obj); 144 | ``` 145 | 146 | Then call the methods from C# WASM through the JSObjectExample C# proxy, passing the JSObject reference into it so it can call an instance method on it: 147 | ```C# 148 | // Call the static JS functions from C# 149 | JSObject element = HtmlElementProxy.FindElement("uno-body"); 150 | // Pass the handle to another method 151 | var elementClasses = HtmlElementProxy.GetClass(element); 152 | Console.WriteLine("Class string: " + elementClasses); 153 | ``` 154 | 155 | Now any static method can implement instance semantics by taking a JSObject as its first parameter, and in turn calling the instance method on the JSObject. 156 | 157 | To create an interface that more closely resembles the native JS type with instance methods, we'll implement another layer to act as a wrapper. We'll also split static methods and instance methods into separate classes. 158 | 159 | ```C# 160 | public static class HtmlDomJS // static methods for interacting with the HTML DOM 161 | { 162 | public static HtmlElementObject FindElement(string id){ 163 | return new HtmlElementObject( HtmlElementProxy.FindElement(id)); 164 | } 165 | } 166 | 167 | public class HtmlElementObject // a strongly typed instance wrapping JSObject reference to an HTML element 168 | { 169 | public JSObject Handle { get; private set; } 170 | public HtmlElementObject(JSObject handle) { Handle = handle; } 171 | 172 | public string GetClass() => HtmlElementProxy.GetClass(Handle); 173 | } 174 | 175 | //Usage: 176 | HtmlElementObject element = HtmlDomJS.FindElement("uno-body"); 177 | string elementClasses = element.GetClass(); 178 | ``` 179 | 180 | Now we can work with HtmlElementObject using instance semantics we are more familiar with in .NET. The JS object's lifetime will be dependent on the C# object. If the C# object goes out of scope and is garbage collected, then the JS object will become available for garbage collection within JS (assuming no other JS references it). 181 | 182 | Let's review the above starting with the deepest JS layer and working up to C#: 183 | - Javascript Declaration: `globalThis.getClass` 184 | - Takes an instance of a JS object(HtmlElement) as a parameter, and calls an JS instance method on it. If this were a JS library wrapper, this is where we would call a JS library specific method on the instance. 185 | - Necessary because `[JSImport]` cannot directly import instance methods, so a static method declaration is needed. 186 | - Not necessary when mapping existing static JS methods, but could still be useful for coercing types or performing other mapping within JS. 187 | - JS instance method declarations can be eliminated and replaced with [Universal JS Instance Method](#Universal-JS-Instance-Method) or SerratedSharp.JSInteropHelpers 188 | - Interop Proxy: Static C# class declaration `HtmlElementProxy` 189 | - Proxies/marshals the call from C# to JS across the interop boundary, and function signatures must match the JS method signatures with compatible types. Other parameters can be added to the method signature, but by convention we use the first parameter as the instance to operate on. *Consider splitting such a class into HtmlElementStaticProxy containing static methods such as FindElement and HtmlElementInstanceProxy containing instance methods such as GetClass.* 190 | - When mapping only static methods, other layers could be omitted, but this layer would still be necessary. 191 | - Instance method proxies are implemented generically in SerratedSharp.JSInteropHelpers 192 | - .NET Wrappers: C# class declaration `HtmlElementObject` and static `HtmlDomJS` 193 | - The static `HtmlDomJS` class handles the JSObject reference returned internally, constructing a wrapping HtmlElementProxy. Having this additional layer also provides an opportunity for us to define a C# function signature that more closely matches .NET semantics, whereas `HtmlElementProxy` is forced by `[JSImport]` to use method signatures matching the JS method signatures. Additionally, due to the limited types supported by JS interop, we may need to coerce types to native .NET types or vice versa at this layer. 194 | - The instance declaration `HtmlElementObject` encapsulates a JSObject which is handled internally to avoid exposing the loosely typed JSObject to consumers. We choose to provide a public `Handle` property for consumer's edge cases where they may need the native JS reference for their own JS interop methods if need be. 195 | - Necessary if presenting strongly typed instance semantics is desired. 196 | 197 | Using this approach, the HtmlElementProxy would be an internal/private implementation with HtmlDomJS and HtmlElementObject exposing the functionality publicly. The \*JS suffix is used on the static class to indicate it wraps JS declarations, and calls to it result in JS interop calls. The \*Object suffix on the container for the JSObject indicates it is a wrapper for a JS type, holds a reference to a JS type, and calls result in interop calls. The naming convention is arbitrary, but is akin to suffixing classes where they represent proxies to other systems and hold unmanaged resources or calls that pass beyond the .NET runtime. 198 | 199 | The JS and proxy layers for instance methods can be eliminated and handled generically with SerratedSharp.JSInteropHelpers. See [Proxyless Instance Wrapper](#Proxyless-Instance-Wrapper) 200 | 201 | #### Memory Management of JSObject References 202 | 203 | JSObject implements IDisposable which serves to release memory in the JS layer. While unreferenced instances will be disposed automatically during a garbage collection, it is non-deterministic since the garbage collector may not run in a timely manner as JS memory pressure is not communicated to the .NET runtime's garbage collector. Wrappers for types that represent large or numerous JS allocations should implement IDisposable, proxy the calls to JSObject, and usage of the type should follow deterministic disposal patterns with `using` blocks. 204 | 205 | #### Universal JS Instance Method 206 | 207 | To reduce the amount of boilerplate JS code needed to map instance methods, we can use .apply to call JS methods of an arbitrary name and number of parameters [Mozilla Function.prototype.apply\(\)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply): 208 | 209 | ```js 210 | InstanceProxy.FuncByNameToObject = function (jsObject, funcName, params) { 211 | const rtn = jsObject[funcName].apply(jsObject, params); 212 | return rtn; 213 | }; 214 | ``` 215 | 216 | ```C# 217 | public static partial class JSInstanceProxy 218 | { 219 | private const string baseJSNamespace = "globalThis.Serrated.InstanceProxy"; 220 | 221 | [JSImport(baseJSNamespace + ".FuncByNameToObject")] 222 | [return: JSMarshalAs] 223 | public static partial 224 | object FuncByNameAsObject(JSObject jsObject, string funcName, [JSMarshalAs>] object[] parameters); 225 | } 226 | 227 | //Usage: 228 | FuncByNameAsObject(elementJSObject, "getAttribute", new object[] { "class" }); 229 | ``` 230 | 231 | #### Proxyless Instance Wrapper 232 | 233 | A generic approach to replace the JS and proxy layers is implemented in SerratedSharp.JSInteropHelpers, which leverages the `jsObject[funcName].apply()` technique in combination with mechanisms such as `[CallerMemberName]` to automatically call a JS function of the same name and parameters as the calling method's name. For example, `Last() => this.CallJSOfSameNameAsWrapped();` will call a JS method named `last()` on the instance of JSObject this type wraps. The IJSObjectWrapper interface is leveraged to access the JSObject handle that these instance methods operate on. 234 | This approach is demonstrated in [JQueryPlainObject.cs](https://github.com/SerratedSharp/SerratedJQ/blob/main/SerratedJQLibrary/SerratedJQ/Plain/JQueryPlainObject.cs) which wraps a javascript jQuery object with a strongly typed C# wrapper. Each of the below instance methods calls a JS method of the same name and parameters without the need to declare proxy JS or C# method/classes. Various overloads support variations of returning a JSObject wrapper or primitive value. 235 | 236 | ```C# 237 | using SerratedSharp.JSInteropHelpers; 238 | public class JQueryPlainObject : IJSObjectWrapper, IJQueryContentParameter 239 | { 240 | internal JSObject jsObject;// Handle to JS object, marked internal so other complementary static factory classes can wrap instances 241 | public JSObject JSObject { get { return jsObject; } } 242 | 243 | // Most constructors aren't called directly by consumers, but thru static methods such as .Select 244 | internal JQueryPlainObject() { } 245 | 246 | // Construct wrapper from JS object reference, not typically used and requires caller ensure the referenced instance is of the appropriate type 247 | public JQueryPlainObject(JSObject jsObject) { this.jsObject = jsObject; } 248 | 249 | // Factory constructor for interface used by JSInteropHelpers when methods return a new wrapped instance 250 | static JQueryPlainObject IJSObjectWrapper.WrapInstance(JSObject jsObject) 251 | { 252 | return new JQueryPlainObject(jsObject); 253 | } 254 | 255 | // Map instance methods to JS methods of the same name and parameters using JSInteropHelpers: 256 | public JQueryPlainObject Last() => this.CallJSOfSameNameAsWrapped(); 257 | public JQueryPlainObject Eq(int index) => this.CallJSOfSameNameAsWrapped(index); 258 | public JQueryPlainObject Slice(int start, int end) => this.CallJSOfSameNameAsWrapped(start, end); 259 | public bool Is(string selector) => this.CallJSOfSameName(selector); 260 | public string Attr(string attributeName) => this.CallJSOfSameName(attributeName); 261 | public void Attr(string attributeName, string value) => this.CallJSOfSameName(attributeName, value); 262 | public string Val() => this.CallJSOfSameName(); 263 | public T Val() => this.CallJSOfSameName(); 264 | //... 265 | } 266 | 267 | public static class JQueryPlain 268 | { 269 | // Example of a static method returning newly constructed/wrapped JS references 270 | public static JQueryPlainObject Select(string selector) 271 | { 272 | var managedObj = new JQueryPlainObject();// Leverages internal constructor 273 | // Then explicitly sets the JSObject reference retrieved through JS proxy 274 | // Note, using this approach static methods still have explicit proxies 275 | managedObj.jsObject = JQueryProxy.Select(selector); 276 | return managedObj; 277 | } 278 | } 279 | ``` 280 | 281 | ## Promises 282 | 283 | Approaches to exposing a JS promise, async method, or old style callback as an async method in C# that can be awaited. Demonstrates awaiting RequireJS dependency resolution where C# code needs to wait for a JS dependency to load. 284 | 285 | ### C# Awaiting a JS Promise 286 | 287 | JS Shim: 288 | ```JS 289 | globalThis.functionReturningPromisedString = function (url) { 290 | return fetch(url, {method: 'GET'}) // request URL 291 | .then(response => response.text()); 292 | // Note that .text() returns a promise. 293 | } 294 | ``` 295 | 296 | C# Proxy: 297 | ```C# 298 | internal static partial class RequestsProxy 299 | { 300 | // Match the above javascript function signature. 301 | [JSImport("globalThis.functionReturningPromisedString")] 302 | [return: JSMarshalAs>()] // JS function returns a promise that resolves to a string 303 | public static partial Task // the return type Task corresponds to the marshaled Promise 304 | FunctionReturningPromisedString(string url); 305 | } 306 | ``` 307 | 308 | Usage: 309 | ```C# 310 | string response = await RequestsProxy.FunctionReturningPromisedString("https://www.example.com"); 311 | ``` 312 | 313 | For demonstrating the fundamentals of awaiting a promise, we use a JS web request. However, you can make web requests using .NET HttpClient from the client side WebAssembly, which avoids interop and eliminates the need for JS shims/proxies. 314 | 315 | ### C# Awaiting an Event Exposed as a JS Promise 316 | 317 | Sometimes it is necessary to guarantee that an operation has completed before continuing execution, but some older JS APIs only signal completion using JS events rather than returning promises. We can wrap the event in a new JS promise, and then C# code can await the promise. Note this is not appropriate for all events. See **Events** section for methods of subscribing to JS events from C#. 318 | 319 | This JS creates a ` 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /WasmBrowser.Recipes.WasmClient/wwwroot/main.js: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | import { dotnet } from './_framework/dotnet.js' 5 | 6 | const { setModuleImports, getAssemblyExports, getConfig } = await dotnet 7 | .withDiagnosticTracing(false) 8 | .withApplicationArgumentsFromQuery() 9 | .create(); 10 | 11 | setModuleImports('main.js', { 12 | window: { 13 | location: { 14 | href: () => globalThis.window.location.href 15 | } 16 | } 17 | }); 18 | 19 | const config = getConfig(); 20 | const exports = await getAssemblyExports(config.mainAssemblyName); 21 | 22 | 23 | 24 | // Original Visual Studio WasmBrowser template example 25 | const text = exports.MyClass.Greeting(); 26 | console.log(text); 27 | document.getElementById('out').innerHTML = text; 28 | await dotnet.run(); --------------------------------------------------------------------------------