diff --git a/content/guide/extending-classes-and-implementing-interfaces-windows.md b/content/guide/extending-classes-and-implementing-interfaces-windows.md
index 6a6fdc46..74a1b734 100644
--- a/content/guide/extending-classes-and-implementing-interfaces-windows.md
+++ b/content/guide/extending-classes-and-implementing-interfaces-windows.md
@@ -1,88 +1,153 @@
---
-title: Extending WinRT classes and implementing interfaces
-description: Subclass Windows Runtime classes and implement WinRT interfaces from JavaScript.
+title: Extending WinRT and .NET classes and implementing interfaces
+description: Subclass WinRT and .NET classes and implement interfaces from JavaScript.
contributors:
- triniwiz
---
::: warning Experimental
-Subclassing and interface implementation on Windows are experimental and more limited than on Android and iOS. For most use cases prefer composition (wrapping native controls, as `@nativescript/core` does), or implement the native part in [C# or C++/WinRT](/guide/native-code/windows) and call it from JavaScript.
+The Windows platform is experimental. See [Developing for Windows](/guide/windows/).
:::
-On Windows, extending a native class or implementing a native interface creates a real .NET type behind the scenes. That type forwards the members you override to your JavaScript implementation, while all other members keep their native behavior.
+On Windows you can extend unsealed WinRT classes (for example WinUI's `Panel` or `Control`) and .NET classes (including your own [C# code](/guide/native-code/windows#adding-your-own-c-code)), and implement WinRT and .NET interfaces. When native code calls a member you override, your JavaScript runs. Members you don't override keep their native behavior.
-## Implementing WinRT interfaces
+## Extending classes
-Use `Object.extend` with an `interfaces` list, and implement the interface members using their WinRT names:
+Extend a class with the `class` syntax:
```ts
-const Stringable = Object.extend({
- interfaces: [Windows.Foundation.IStringable],
- ToString() {
- return 'Hello from JavaScript'
- },
-})
+class FixedPanel extends Microsoft.UI.Xaml.Controls.Panel {
+ constructor() {
+ super()
+ this.measures = 0
+ }
-const instance = new Stringable()
-console.log(instance.ToString()) // Hello from JavaScript
-```
+ MeasureOverride(availableSize) {
+ this.measures++
+ return { Width: 120, Height: 40 }
+ }
-Multiple interfaces can be listed, and all of their members implemented in the same object.
+ ArrangeOverride(finalSize) {
+ return super.ArrangeOverride(finalSize)
+ }
+}
-## Extending WinRT classes
+const panel = new FixedPanel()
+container.Children.Append(panel) // XAML now calls MeasureOverride/ArrangeOverride
+```
-Call `extend` on the base class and pass the members to override. An optional name can be passed as the first argument:
+Or call `extend` on the class, optionally passing a name first:
```ts
-const MyClass = SomeNamespace.SomeUnsealedClass.extend('MyClass', {
+const FixedPanel = Microsoft.UI.Xaml.Controls.Panel.extend('FixedPanel', {
init() {
- // called after the instance is constructed
+ // called after construction, with the constructor arguments
},
- SomeVirtualMethod(arg) {
- // override
+ MeasureOverride(availableSize) {
+ return { Width: 120, Height: 40 }
+ },
+ ArrangeOverride(finalSize) {
+ return this.super.ArrangeOverride(finalSize)
},
})
+```
+
+The same works for .NET classes:
+
+```cs
+// App_Resources/Windows/src/Animal.cs
+namespace MyCompany.Native;
+
+public class Animal
+{
+ public Animal(string name) { Name = name; }
+ public string Name { get; }
+ public virtual string Speak() => "...";
+ public string Describe() => $"{Name} says {Speak()}";
+}
+```
+
+```ts
+class Cat extends MyCompany.Native.Animal {
+ Speak() {
+ return 'Meow'
+ }
+}
-const instance = new MyClass()
+console.log(new Cat('Tom').Describe()) // Tom says Meow
```
-- Only the members listed in the overrides object are overridden, all other members use the base implementation.
-- `init(...args)` is called after the instance is constructed, with the constructor arguments.
-- Getters and setters in the overrides object override the corresponding WinRT properties.
+- Override methods and properties with their WinRT/.NET names. Override properties with `get`/`set` accessors.
+- Constructor arguments passed to `super(...)` (or to `new` for classes made with `extend`) select and call the matching base constructor.
+- `super.Member(...)` (or `this.super.Member(...)` in `extend`) calls the base implementation.
+- You can override protected members and implement the abstract members of abstract classes.
+- Fields you set on `this` are visible when native code calls your overrides, and `instanceof` works for your class and its native base classes.
+- When native code hands an instance back to JavaScript (for example `panel.Children.GetAt(0)`), you get the same JavaScript object.
+- Classes can be extended again, from JavaScript.
::: warning Sealed classes
-Only classes that can be derived from (unsealed, "composable" classes) can be extended. Most WinRT runtime classes, for example `Windows.Data.Json.JsonObject`, are **sealed**. Overrides passed to `extend` on a sealed class have no effect.
+Only unsealed classes can be extended. Most WinRT runtime classes, for example `Windows.Data.Json.JsonObject`, are **sealed**. A JavaScript subclass of a sealed class gets your JavaScript members, but native code never calls them.
:::
-### Using TypeScript classes
+### TypeScript and `@NativeClass()`
-With TypeScript, decorate a class that extends a WinRT class with `@NativeClass()`, and optionally with the `@Interfaces` and `@CSharpProxy` decorators:
+Code written for Android and iOS works unchanged: `@NativeClass()` classes, TypeScript's ES5 output, and constructors ending with `return global.__native(this)` are all supported. On Windows the decorator isn't required.
```ts
@NativeClass()
+class FixedPanel extends Microsoft.UI.Xaml.Controls.Panel {
+ MeasureOverride(availableSize: Windows.Foundation.Size) {
+ return { Width: 120, Height: 40 }
+ }
+}
+```
+
+## Implementing interfaces
+
+Implement an interface by passing its members to the interface constructor:
+
+```ts
+const calculator = new MyCompany.Native.ICalculator({
+ Compute(a, b) {
+ return a * b
+ },
+ get Name() {
+ return 'multiply'
+ },
+})
+
+MyCompany.Native.Runner.Run(calculator, 6, 7) // 42
+```
+
+To implement interfaces on a class, list them with `@Interfaces`, or with an `interfaces` key when using `extend`:
+
+```ts
@Interfaces([Windows.Foundation.IStringable])
-@CSharpProxy('MyApp.Native.MyStringable')
-class MyStringable extends SomeNamespace.SomeUnsealedClass {
+class Named extends System.Object {
ToString() {
- return 'MyStringable'
+ return 'named'
}
}
+
+const Stringable = Object.extend({
+ interfaces: [Windows.Foundation.IStringable],
+ ToString() {
+ return 'Hello from JavaScript'
+ },
+})
```
-- `@Interfaces([...])` lists the interfaces implemented by the class.
-- `@CSharpProxy(name)` sets the full name of the generated .NET type.
+## Lifetime
-::: tip Note
-As on Android and iOS, `@NativeClass()` makes sure the class is compiled in a way the runtime can intercept. See [the NativeClass decorator](/best-practices/native-class).
-:::
+An instance of a JavaScript subclass is a JavaScript object together with the native object it drives. It is freed when neither your JavaScript nor native code references it.
-## How it works
+As on Android, keep a reference to an instance that is only used from native code if its fields matter. If native code calls into an instance whose JavaScript object was garbage collected, a new JavaScript object of the same class is created for it, without the fields its constructor set. Instances of UI classes stay alive, with their fields, while they are in the visual tree.
-At build time the CLI scans your bundled JavaScript for `extend` calls and decorated classes, and generates matching C# proxy types that are compiled into the app. This is similar to the static binding generator used on Android.
+## How it works
-In development builds, types that weren't found at build time are generated at runtime instead.
+The runtime creates a .NET type for each subclass at runtime, deriving from the C#/WinRT projection of the base class (the way a C# subclass does). That type forwards the members you override, and the interface members you implement, to your JavaScript object. No build step or generated code is involved.
## Limitations
- Sealed classes can't be extended.
-- JavaScript overrides are only called on the UI thread, see [Multithreading](/guide/multithreading#windows).
+- JavaScript runs on the UI thread. Native code that calls an override on a background thread waits while it runs there, see [Multithreading](/guide/multithreading#windows).
diff --git a/content/guide/multithreading.md b/content/guide/multithreading.md
index cd745ce0..241df389 100644
--- a/content/guide/multithreading.md
+++ b/content/guide/multithreading.md
@@ -372,5 +372,5 @@ On Windows, JavaScript runs on the WinUI UI thread, so WinRT and XAML APIs are c
Keep the following in mind:
- Messages are serialized using the structured clone algorithm, so values such as `Date`, `Map`, `Set`, `ArrayBuffer` and typed arrays can be sent. Native (WinRT) objects can't be sent.
-- JavaScript callbacks only run on the thread that created them. If WinRT invokes a callback (event handler, delegate or async completion) on a background thread, it is not delivered to JavaScript. Subscribe to events and await async operations on the thread that uses the results.
+- JavaScript callbacks run on the thread that created them. When WinRT or .NET invokes a callback created on the UI thread (an event handler, delegate, overridden member or async completion) from a background thread, it runs on the UI thread while the calling thread waits for it, so return values and exceptions reach the caller, as on iOS and Android. As on those platforms, a UI thread blocked waiting for that background thread deadlocks. Callbacks created in a worker are only delivered on the worker's thread.
- To get results from a worker back to the UI, `postMessage` them to the main thread. `Utils.isMainThread()` tells you whether the current code runs on the UI thread.
diff --git a/content/guide/native-code/windows.md b/content/guide/native-code/windows.md
index 1204a8d6..74878d6a 100644
--- a/content/guide/native-code/windows.md
+++ b/content/guide/native-code/windows.md
@@ -26,6 +26,7 @@ App_Resources/
│ ├─ before-plugins.props # imported before plugin files
│ ├─ after-plugins.props # imported after plugin files
│ ├─ Package.appxmanifest
+│ ├─ src/ # C# sources, compiled into the app
│ └─ Assets/
└─ ... more
```
@@ -73,49 +74,48 @@ Add a `PackageReference` to `App_Resources/Windows/app.csproj`:
```
-Then register the root namespace of the library with the assembly that contains it, and use it from JavaScript:
+Then use it from JavaScript. The root namespaces of the app's assemblies (`Newtonsoft` here) are globals, like `System`:
```ts
-NSWinRT.dotnet.registerNamespace('Newtonsoft', 'Newtonsoft.Json')
-
const json = Newtonsoft.Json.JsonConvert.SerializeObject({ hello: 'world' })
```
-`registerNamespace(root, assemblyName)` defines a global for the namespace root (`Newtonsoft` above), which resolves types from the given assembly. Assemblies are loaded from the app's output folder, including its `libs` and `plugins` subfolders.
+Assemblies are loaded from the app's output folder, including its `libs` and `plugins` subfolders.
### Adding your own C# code
-To add your own C# code, create a .NET class library targeting `net10.0` (or `net10.0-windows10.0.xxxxx.0` if it uses WinRT APIs) in your project, for example in `native/windows/MyLibrary`, and reference it from `App_Resources/Windows/app.csproj`:
-
-```xml
-
-
-
-
-
-```
+Add C# files to `App_Resources/Windows`, for example in `App_Resources/Windows/src`. They are compiled into the app, the way Java/Kotlin files in `App_Resources/Android/src` and Objective-C/Swift files in `App_Resources/iOS/src` are, and their types are available from JavaScript by namespace:
```cs
-// native/windows/MyLibrary/Greeter.cs
+// App_Resources/Windows/src/Greeter.cs
namespace MyCompany.Native;
-public static class Greeter
+public class Greeter
{
- public static string Hello(string name) => $"Hello {name} from C#!";
+ public string Name { get; set; } = "C#";
+
+ public string Hello(string who) => $"Hello {who} from {Name}!";
+
+ public static int Add(int a, int b) => a + b;
}
```
```ts
-NSWinRT.dotnet.registerNamespace('MyCompany', 'MyLibrary')
-
-console.log(MyCompany.Native.Greeter.Hello('NativeScript'))
-// prints: Hello NativeScript from C#!
+const greeter = new MyCompany.Native.Greeter()
+console.log(greeter.Hello('NativeScript')) // Hello NativeScript from C#!
+console.log(MyCompany.Native.Greeter.Add(2, 3)) // 5
```
-### .NET tasks and delegates
+Public members are available, including those of internal types. Add the NuGet packages your code needs to `App_Resources/Windows/app.csproj`. JavaScript classes can also [extend your C# classes](/guide/extending-classes-and-implementing-interfaces-windows).
+
+For a larger code base, a separate .NET class library works too: reference it from `app.csproj` with a `ProjectReference` (`$(MSBuildProjectDirectory)\..\..\..\` is your project root).
+
+### .NET tasks, delegates and structs
-- Convert a returned `Task` to a promise with `NSWinRT.toPromise(task)`.
-- Create a .NET delegate (for example a `System.Action`) with `NSWinRT.dotnet.asDelegate('System.Action', fn)`. For WinRT delegates use `NSWinRT.asDelegate` instead, see [Windows Marshalling › Events](/guide/windows-marshalling#events).
+- A method returning a `Task` returns an awaitable: `const data = await MyCompany.Native.Api.LoadAsync()`. `NSWinRT.toPromise(task)` converts it explicitly.
+- Pass a JavaScript function where a method expects a delegate (`Func<>`, `Action<>` or another delegate type). Its return value is returned to the caller. `NSWinRT.dotnet.asDelegate(typeName, fn)` creates one explicitly; for WinRT delegates see [Windows Marshalling › Events](/guide/windows-marshalling#events).
+- Subscribe to .NET events with their `add_`/`remove_` methods: `greeter.add_Greeted((sender, message) => {})`.
+- Pass a plain object where a struct is expected (`{ Width: 120, Height: 40 }`). Structs passed to JavaScript callbacks arrive as plain objects.
- .NET objects are released when they are garbage collected. Call `obj.release()` to release one immediately.
## Adding C++/WinRT components
@@ -220,10 +220,11 @@ my-plugin/
├─ plugin.targets # optional, imported by the host project
└─ platforms/
└─ windows/
+ ├─ src/ # C# sources, compiled into the app
├─ x64/
└─ arm64/
```
-The CLI copies the contents of `platforms/windows` into the host project (under `plugins//`) and imports the plugin's `plugin.props` and `plugin.targets` files. Use them to add package references, copy native files to the output folder or register activatable classes, the same way an app does with `app.csproj`. When a plugin has no `plugin.props`/`plugin.targets`, the CLI generates default ones that copy the plugin's files into `plugins\` in the app output folder.
+The CLI copies the contents of `platforms/windows` into the host project (under `plugins//`) and imports the plugin's `plugin.props` and `plugin.targets` files. C# files are compiled into the app, so a plugin can ship its native code as source, as Android and iOS plugins do. Use them to add package references, copy native files to the output folder or register activatable classes, the same way an app does with `app.csproj`. When a plugin has no `plugin.props`/`plugin.targets`, the CLI generates default ones that copy the plugin's files into `plugins\` in the app output folder.
See [`@nativescript/core`'s `plugin.targets`](https://github.com/NativeScript/NativeScript/blob/main/packages/core/plugin.targets) for a complete example that deploys a C++/WinRT component and registers its classes.
diff --git a/content/guide/windows-marshalling.md b/content/guide/windows-marshalling.md
index 8cb7182d..e5a74d26 100644
--- a/content/guide/windows-marshalling.md
+++ b/content/guide/windows-marshalling.md
@@ -262,6 +262,16 @@ datePicker.SelectedDateChanged = NSWinRT.asDelegate(
Keep a reference to handlers you create with `NSWinRT.asDelegate` (for example on your view instance) for as long as the subscription is needed.
:::
+Static events are assigned on the class the same way:
+
+```ts
+Microsoft.UI.Xaml.Media.CompositionTarget.Rendering = () => {
+ // called once per frame
+}
+// ...
+Microsoft.UI.Xaml.Media.CompositionTarget.Rendering = null
+```
+
## Async operations
WinRT async operations (`IAsyncAction`, `IAsyncOperation`) are not promises. Convert them with `NSWinRT.toPromise`:
@@ -285,7 +295,7 @@ await NSWinRT.toPromise(operation, { timeoutMs: 10000 })
## .NET types
-.NET libraries are available through the `System` global and other registered namespaces. .NET `Task` objects are converted with `NSWinRT.toPromise` as well:
+.NET libraries are available through the `System` global and the root namespaces of the app's other assemblies. Methods returning a .NET `Task` return an awaitable, which `NSWinRT.toPromise` also accepts:
```ts
const stopwatch = System.Diagnostics.Stopwatch.StartNew()
@@ -313,8 +323,6 @@ Supported types are `void`, `bool`, `i8`, `i16`, `i32`, `i64`, `u8`, `u16`, `u32
## Threading
-JavaScript runs on the WinUI UI thread, so WinRT and XAML APIs can be called directly. JavaScript callbacks can only run on that thread: a delegate invoked by WinRT on a background thread is not delivered to JavaScript. See [Multithreading](/guide/multithreading#windows).
+JavaScript runs on the WinUI UI thread, so WinRT and XAML APIs can be called directly. A callback that WinRT or .NET invokes on a background thread (an event, a delegate, an overridden member, a `Task` continuation) runs on the UI thread, while the calling thread waits for it. Its return value or exception is passed back to the caller. See [Multithreading](/guide/multithreading#windows).
-::: danger Don't change the UI during layout
-Adding, removing or reparenting XAML elements synchronously inside `CompositionTarget.Rendering`, `LayoutUpdated` or `SizeChanged` handlers crashes the app (error `0xC000027B`). Defer those changes with `setTimeout(() => { ... }, 0)`.
-:::
+You can add, remove and reparent XAML elements from any handler, including `CompositionTarget.Rendering`, `LayoutUpdated` and `SizeChanged`.