├── .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  icon.
2 |
3 | To improve readability, navigate [here](README.md) then collapse the files drawer:
4 |
5 | 
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 | 
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 | 
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 | 
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