diff --git a/CLAUDE.md b/CLAUDE.md index 02d63da6..95895096 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -114,6 +114,41 @@ The model is [`ReactiveUI.Binding.SourceGenerators/README.md`](https://github.co the problem first, then a numbered walkthrough of the first use, then reference sections. A reader who stops after the walkthrough can already do the thing. +### Diagrams: soft-tone Material 3 + +The site renders ```` ```mermaid ```` fences (NuStreamDocs.Mermaid, Mermaid 10). Every diagram uses the same palette, +taken from Material 3 container roles and seeded from the site's ReactiveUI blue (`#319af3`). Fills are light tones with +dark text, so a diagram reads in both the light and dark site themes. Copy the `init` line and the `classDef` lines +exactly. Do not invent colours. + +```mermaid +%%{init: {"theme": "base", "themeVariables": {"fontFamily": "Roboto, Helvetica, Arial, sans-serif", "fontSize": "15px", "primaryColor": "#DCE9FF", "primaryBorderColor": "#6C8EC4", "primaryTextColor": "#0B2447", "secondaryColor": "#E3F2E8", "secondaryBorderColor": "#7FA88C", "secondaryTextColor": "#12301C", "tertiaryColor": "#F3E5F5", "tertiaryBorderColor": "#A98BB0", "tertiaryTextColor": "#2E1437", "lineColor": "#7B8699", "textColor": "#1B1F27", "noteBkgColor": "#FFF4D6", "noteBorderColor": "#C9A94F", "noteTextColor": "#3A2A00", "actorBkg": "#DCE9FF", "actorBorder": "#6C8EC4", "actorTextColor": "#0B2447", "signalColor": "#7B8699", "signalTextColor": "#1B1F27", "labelBoxBkgColor": "#F1F3F8", "labelBoxBorderColor": "#A7AEBB", "edgeLabelBackground": "#F7F9FC", "clusterBkg": "#F7F9FC", "clusterBorder": "#C9D1DE"}}}%% +flowchart LR + classDef view fill:#DCE9FF,stroke:#6C8EC4,color:#0B2447 + classDef vm fill:#E3F2E8,stroke:#7FA88C,color:#12301C + classDef model fill:#F3E5F5,stroke:#A98BB0,color:#2E1437 + classDef warn fill:#FDE7E4,stroke:#C98A82,color:#410E0B + classDef neutral fill:#F1F3F8,stroke:#A7AEBB,color:#1B1F27 + View(["View"]):::view -- "binds to" --> VM(["View model"]):::vm + VM -- "reads and writes" --> Model(["Model"]):::model +``` + +| Class | Tone | Use for | +| --- | --- | --- | +| `view` | soft blue (primary container) | views, controls, anything the user sees | +| `vm` | soft green (secondary container) | view models, commands, reactive objects | +| `model` | soft lavender (tertiary container) | models, stores, services, data | +| `warn` | soft rose (error container) | errors, exceptions, failure paths | +| `neutral` | soft grey (surface variant) | infrastructure: sequencers, the builder, the resolver | + +- Use a diagram only where it shows something faster than words: data flowing between view, view model and model; a + command's life cycle; a navigation stack; activation and deactivation; where work runs; suspension states. Most + pages need at most two. +- Use rounded shapes (`(["text"])` or `("text")`) and `flowchart LR` unless the flow is a stack or a timeline. Use + `sequenceDiagram` for "who calls whom, in what order" and `stateDiagram-v2` for life cycles. +- Keep to about eight nodes. Labels are plain words a grade 8 reader follows, in the page's own terms. +- Put each diagram after the paragraph that introduces it, and follow it with one sentence saying what to notice. + ## In Flight: Per-Operator Pages For ReactiveUI.Primitives The site is to carry one page per operator family for `ReactiveUI.Primitives`, with an example for every diff --git a/docs/documentation/reactiveui/getting-started/installation/avalonia.md b/docs/documentation/reactiveui/getting-started/installation/avalonia.md index c09a6de0..e1220b43 100644 --- a/docs/documentation/reactiveui/getting-started/installation/avalonia.md +++ b/docs/documentation/reactiveui/getting-started/installation/avalonia.md @@ -3,8 +3,9 @@ Order: 2 --- # Avalonia -`ReactiveUI.Avalonia` lives in its own repository, [reactiveui/ReactiveUI.Avalonia](https://github.com/reactiveui/ReactiveUI.Avalonia), -not in `reactiveui/ReactiveUI`. It has its own target frameworks, release schedule and documentation. +`ReactiveUI.Avalonia` lives in its own repository, not in `reactiveui/ReactiveUI`. That repository is +[reactiveui/ReactiveUI.Avalonia](https://github.com/reactiveui/ReactiveUI.Avalonia). It has its own supported +.NET versions. It has its own release schedule and its own docs too. Add `ReactiveUI.Avalonia` to your Avalonia application project. @@ -12,11 +13,11 @@ Add `ReactiveUI.Avalonia` to your Avalonia application project. dotnet add package ReactiveUI.Avalonia ``` -A dependency-injection container of your choice adds one more package: `ReactiveUI.Avalonia.Autofac`, +Pick a dependency-injection container, and add one more package for it: `ReactiveUI.Avalonia.Autofac`, `ReactiveUI.Avalonia.DryIoc`, `ReactiveUI.Avalonia.Microsoft.Extensions.DependencyInjection` or -`ReactiveUI.Avalonia.Ninject`. Each also ships as a `.Reactive` package, built from the same source, for an app -that uses System.Reactive. +`ReactiveUI.Avalonia.Ninject`. Each of these also ships as a `.Reactive` package, built from the same source, +for an app that uses System.Reactive. See the [ReactiveUI.Avalonia repository](https://github.com/reactiveui/ReactiveUI.Avalonia) for setup and its -own examples, and [Data Binding in AvaloniaUI with ReactiveUI](../../handbook/data-binding/avalonia.md) for +own examples. See [Data Binding in AvaloniaUI with ReactiveUI](../../handbook/data-binding/avalonia.md) for binding a view to its view model with `WhenActivated`. diff --git a/docs/documentation/reactiveui/getting-started/installation/maui.md b/docs/documentation/reactiveui/getting-started/installation/maui.md index e72f4e4f..ecdc15e7 100644 --- a/docs/documentation/reactiveui/getting-started/installation/maui.md +++ b/docs/documentation/reactiveui/getting-started/installation/maui.md @@ -17,9 +17,9 @@ System.Reactive. ## Configure ReactiveUI at startup -Call `UseReactiveUI` on the `MauiAppBuilder` in `MauiProgram.CreateMauiApp`, with a delegate that configures the -`IReactiveUIBuilder` the way `RxAppBuilder` would elsewhere. `WithMauiConverters` here registers MAUI's -boolean-to-visibility converters; a real app's delegate also registers its views and services. +Call `UseReactiveUI` on the `MauiAppBuilder` in `MauiProgram.CreateMauiApp`. Pass it a delegate that configures +the `IReactiveUIBuilder`, the same way `RxAppBuilder` would elsewhere. `WithMauiConverters` here registers +MAUI's boolean-to-visibility converters. A real app's delegate also registers its views and services. ```csharp _ = mauiBuilder.UseReactiveUI(builder => @@ -30,11 +30,11 @@ _ = mauiBuilder.UseReactiveUI(builder => ``` `UseReactiveUI` registers MAUI's platform module, converters and dispatcher-backed main-thread sequencer. -[RxAppBuilder](../../handbook/rxappbuilder.md) covers the builder itself, and [.NET MAUI](../../handbook/platforms/maui.md) -covers everything you build with the package: `ReactiveContentPage` and the other reactive page bases, binding -a page to its view model, showing a router's current page, and navigating between pages. +[RxAppBuilder](../../handbook/rxappbuilder.md) covers the builder itself. [.NET MAUI](../../handbook/platforms/maui.md) +covers everything you build with the package. That includes `ReactiveContentPage` and the other reactive page +bases, binding a page to its view model, showing a router's current page, and navigating between pages. -A view model implementing `IScreen` gives it a router to navigate; see [Routing](../../handbook/routing.md). -The [compelling example](../compelling-example.md) walks through a first view model and view. There is no -`ReactiveUI.Validation` code in this installation's example project; see [Validation](../../../validation.md) +A view model that implements `IScreen` gets a router to navigate with. See [Routing](../../handbook/routing.md). +The [compelling example](../compelling-example.md) walks through a first view model and view. This +installation's example project has no `ReactiveUI.Validation` code. See [Validation](../../../validation.md) for validation rules on a view model. diff --git a/docs/documentation/reactiveui/guidelines/debugging/debug-symbols.md b/docs/documentation/reactiveui/guidelines/debugging/debug-symbols.md index 9d961167..d494639e 100644 --- a/docs/documentation/reactiveui/guidelines/debugging/debug-symbols.md +++ b/docs/documentation/reactiveui/guidelines/debugging/debug-symbols.md @@ -1,12 +1,12 @@ # Debugging ReactiveUI Every ReactiveUI package on NuGet ships with [SourceLink](https://docs.microsoft.com/en-us/dotnet/standard/library-guidance/sourcelink) -metadata embedded in the assembly's PDB (`embedded` + -`Microsoft.SourceLink.GitHub`, configured once in -[`src/Directory.Build.props`](https://github.com/reactiveui/reactiveui/blob/main/src/Directory.Build.props)). -That means you can step from your own code straight into the framework source -on GitHub, at the exact commit your installed version was built from. You need -no matching local clone and no symbol-server hunting. +metadata. The metadata is embedded in the assembly's PDB, a file that holds debug information. This comes from +`embedded` and `Microsoft.SourceLink.GitHub`, configured once in +[`src/Directory.Build.props`](https://github.com/reactiveui/reactiveui/blob/main/src/Directory.Build.props). +That means you can step from your own code straight into the framework source on GitHub. You land at the exact +commit your installed version was built from. You need no matching local clone. You need no symbol-server +hunting either. ## Visual Studio (2022 and newer) @@ -14,32 +14,31 @@ Two debugger settings unlock SourceLink stepping. Both live under **Tools → Options → Debugging**: 1. **General** page - - **Enable Just My Code** — *uncheck*. With Just My Code on, the debugger - never asks the SourceLink resolver for framework frames; turning it off + - **Enable Just My Code** — *uncheck*. When Just My Code is on, the debugger + never asks the SourceLink resolver for framework frames. Turning it off lets the resolver fetch the matching ReactiveUI source on demand. - **Enable Source Link support** — *check*. This is the master switch that wires `Microsoft.VisualStudio.Debugger.SourceLink` into the symbol load path. - - *(Recommended)* **Enable source server support** + **Suppress JIT - optimization on module load (Managed only)** — gives you readable + - *(Recommended)* **Enable source server support** and **Suppress JIT + optimization on module load (Managed only)**. These give you readable locals when stepping into release-built framework code. 2. **Symbols** page - - Make sure **Microsoft Symbol Servers** (or another server hosting the - ReactiveUI PDBs — typically not needed since `embedded` ships the PDB - inside the .dll itself) is enabled. With embedded PDBs, no extra - server entry is required. + - Make sure **Microsoft Symbol Servers** is enabled, or another server + that hosts the ReactiveUI PDBs. You usually don't need this step, + because the embedded PDB already ships inside the .dll. Set a breakpoint, hit it, then *Step Into* (`F11`) any ReactiveUI call (e.g. `this.WhenAnyValue(...)`). Visual Studio prompts once with *"Source Link will download -— OK?"*; accept and you land in the framework's source file at the exact +— OK?"* Accept it, and you land in the framework's source file at the exact commit your NuGet package was published from. ## Rider / VS Code (C# Dev Kit) -Rider honors SourceLink out of the box — make sure +Rider honors SourceLink out of the box. Make sure **Settings → Build, Execution, Deployment → Debugger → Enable external source -debug** is on, and disable Just My Code under the same screen. +debug** is on. Disable Just My Code under the same screen. In VS Code with the C# Dev Kit, set in `launch.json`: @@ -55,19 +54,19 @@ In VS Code with the C# Dev Kit, set in `launch.json`: ## Quick demo -
+[Watch the SourceLink debugging demo on YouTube](https://www.youtube.com/watch?v=gyRGhCQPkB4) ## Troubleshooting -- **Step Into still skips framework frames.** Confirm Just My Code is off - and that the package version on disk matches a published release tag (the - SourceLink URL embeds the commit SHA — local builds without a release - commit can't be resolved). +- **Step Into still skips framework frames.** Confirm Just My Code is off. + Also confirm the package version on disk matches a published release tag. + The SourceLink URL embeds the commit SHA, so a local build without a + release commit can't be resolved. - **`The remote endpoint could not be reached`.** Your network blocks - `raw.githubusercontent.com`. Either whitelist that host or run with the + `raw.githubusercontent.com`. Either allow that host, or run with the source already on disk and point Visual Studio at it via **Debug → Options → Symbols → Specify excluded modules**. - **Mobile heads (iOS / Android via .NET MAUI).** The platform debuggers - honor SourceLink for managed code today, but native interop frames stay - opaque. For pure-managed ReactiveUI calls the experience matches WPF / - WinForms. + honor SourceLink for managed code today. Native interop frames stay + opaque, though. For pure-managed ReactiveUI calls, the experience matches + WPF and WinForms. diff --git a/docs/documentation/reactiveui/guidelines/debugging/disable-just-my-code.md b/docs/documentation/reactiveui/guidelines/debugging/disable-just-my-code.md index 91aa511d..57812371 100644 --- a/docs/documentation/reactiveui/guidelines/debugging/disable-just-my-code.md +++ b/docs/documentation/reactiveui/guidelines/debugging/disable-just-my-code.md @@ -1,9 +1,9 @@ # Disable Just My Code Turn off Visual Studio's [Just My Code](https://learn.microsoft.com/visualstudio/debugger/just-my-code) feature. -With it on, the debugger steps over framework code, including `IObservable` pipelines and `async`/`await` -state machines, so **Step Into** on a `WhenAnyValue` or `Subscribe` call skips straight past it. Turning it off -lets you step through the framework's own code, which is where most reactive bugs actually show up. +With it on, the debugger steps over framework code. This includes `IObservable` pipelines and `async`/`await` +state machines. So **Step Into** on a `WhenAnyValue` or `Subscribe` call skips straight past it. Turning it off +lets you step through the framework's own code. That is where most reactive bugs actually show up. See [debugging ReactiveUI](debug-symbols.md) for the full set of debugger settings SourceLink needs, including this one. @@ -19,6 +19,6 @@ Reactive debugging tip: turn on breaking on thrown exceptions before they are ha [Understanding exceptions while debugging with Visual Studio](https://devblogs.microsoft.com/devops/understanding-exceptions-while-debugging-with-visual-studio) -An exception inside a stream's callback ends that subscription: the stream calls `OnError` instead of `OnNext` -or `OnCompleted`, and stops. Breaking on the exception itself, rather than after it becomes an unhandled error, -puts you at the exact line that failed. +An exception inside a stream's callback ends that subscription. The stream calls `OnError` instead of `OnNext` +or `OnCompleted`, and it stops. Break on the exception itself, rather than after it becomes an unhandled error. +That puts you at the exact line that failed. diff --git a/docs/documentation/reactiveui/guidelines/platform/android.md b/docs/documentation/reactiveui/guidelines/platform/android.md index eeba77bc..79370aa6 100644 --- a/docs/documentation/reactiveui/guidelines/platform/android.md +++ b/docs/documentation/reactiveui/guidelines/platform/android.md @@ -3,22 +3,22 @@ `ReactiveUI.AndroidX` connects ReactiveUI to Android through the AndroidX support libraries. Add it by following [Installation](../../getting-started/installation/androidx.md). `ReactiveUI.AndroidX` also ships as `ReactiveUI.AndroidX.Reactive`, built from the same source, for an app that uses System.Reactive. -[Android](../../handbook/platforms/android.md) walks through every pattern below with a school timetable app. The core -`ReactiveUI` package's Android build adds `ReactiveActivity`, `SharedPreferencesExtensions` and -`UsbManagerExtensions` for apps that do not use AndroidX. +[Android](../../handbook/platforms/android.md) walks through every pattern below with a school timetable app. For +an app that does not use AndroidX, the core `ReactiveUI` package's Android build adds `ReactiveActivity`, +`SharedPreferencesExtensions` and `UsbManagerExtensions`. ## Guidelines -- **View models inherit from `ReactiveObject`.** They stay free of any Android type, so you can unit test them +- **View models inherit from `ReactiveObject`.** They stay free of any Android type. So you can unit test them without a device or an emulator. - **Activities, fragments and dialogs inherit from the reactive base classes.** `ReactiveAppCompatActivity`, `ReactiveFragmentActivity`, `ReactiveFragment`, `ReactiveDialogFragment` and `ReactivePreferenceFragment` each implement `IViewFor`. Each one exposes a `ViewModel` - property, so you never wire one up by hand. + property. You never wire one up by hand. - **Wire up controls with `WireUpControls()`, not `FindViewById`.** `ControlFetcherMixins.WireUpControls` reads a - view's properties by reflection and finds each one's matching resource by name, once, instead of a - `FindViewById` call per control. Its `ResolveStrategy` decides which properties it wires: `Implicit` wires every - writable `View`-typed property, `ExplicitOptIn` wires only properties marked to opt in, and `ExplicitOptOut` + view's properties by reflection. It finds each one's matching resource by name, once, instead of a + `FindViewById` call per control. Its `ResolveStrategy` decides which properties it wires. `Implicit` wires every + writable `View`-typed property. `ExplicitOptIn` wires only properties marked to opt in. `ExplicitOptOut` wires every property except those marked to opt out. - **Use `IActivatableViewModel` and `WhenActivated` for lifecycle.** See [When Activated](../../handbook/when-activated.md) for how a subscription's lifetime follows the view's. @@ -26,11 +26,11 @@ and [Disposables](../../../primitives/disposables.md). - **Convert Android events to streams instead of attaching event handlers.** The extension methods in `ReactiveUI.Primitives.ObservableEvents` turn an event into an `IObservable`. Compose it with the rest of a - view model's streams; see [Events](../../handbook/events.md) and + view model's streams. See [Events](../../handbook/events.md) and [ObservableEvents](../../../primitives/observable-events/index.md). - **Bind a list without a hand-written adapter.** `ReactiveRecyclerViewAdapter` and `ReactiveRecyclerViewViewHolder` bind a `RecyclerView` to a collection of view models. `ReactivePagerAdapter` does the same for a `ViewPager`. - **Await an activity result instead of overriding `OnActivityResult`.** `StartActivityForResultAsync` on the reactive - activity base classes turns a launched activity's result into a `Task`, so the calling code reads like ordinary + activity base classes turns a launched activity's result into a `Task`. The calling code then reads like ordinary asynchronous code. diff --git a/docs/documentation/reactiveui/guidelines/platform/windows-presentation-framework.md b/docs/documentation/reactiveui/guidelines/platform/windows-presentation-framework.md index 451e4b82..fa813b83 100644 --- a/docs/documentation/reactiveui/guidelines/platform/windows-presentation-framework.md +++ b/docs/documentation/reactiveui/guidelines/platform/windows-presentation-framework.md @@ -1,6 +1,6 @@ # Windows Presentation Framework guidelines -See [WPF](wpf-overview.md) for an introduction to the package, and +See [WPF](wpf-overview.md) for an introduction to the package. See [WPF](../../handbook/platforms/wpf.md) for a full walkthrough, built around a university grade book. This page lists the WPF-specific practices worth calling out on their own. @@ -9,16 +9,16 @@ lists the WPF-specific practices worth calling out on their own. - **Keep every subscription disposed.** See [Cleaning up subscriptions](../../../reactive-programming/observables.md#cleaning-up) and [Disposables](../../../primitives/disposables.md). - **Convert WPF events to streams instead of attaching event handlers.** The extension methods in - `ReactiveUI.Primitives.ObservableEvents` turn an event into an `IObservable`; see + `ReactiveUI.Primitives.ObservableEvents` turn an event into an `IObservable`. See [Events](../../handbook/events.md) and [ObservableEvents](../../../primitives/observable-events/index.md). - **A window, page or control that needs a view model derives from `ReactiveWindow`, - `ReactivePage` or `ReactiveUserControl`.** Each implements `IViewFor` with a - `ViewModel` dependency property. -- **Host a routed page stack with `RoutedViewHost`, and a single view model with `ViewModelViewHost`.** + `ReactivePage` or `ReactiveUserControl`.** Each of these implements + `IViewFor` with a `ViewModel` dependency property. +- **Host a routed page stack with `RoutedViewHost`.** Host a single view model with `ViewModelViewHost`. - **Starting WPF from something other than its own `Application` entry point needs a scheduler set by hand.** A console host that creates the WPF application on its own thread has no dispatcher yet when the process starts. - Once that thread's dispatcher exists, set `RxSchedulers.MainThreadScheduler` to a dispatcher-backed sequencer - before anything schedules work onto it. `WithWpfScheduler` (part of `WithWpf`) needs the same dispatcher to - already exist. -- **A control bound without a source-generated `Bind`** — one whose target control is resolved by name at run - time — uses `BindWithValidation` instead of `Bind`. + Once that thread's dispatcher exists, set `RxSchedulers.MainThreadScheduler` to a dispatcher-backed sequencer. + Do this before anything schedules work onto it. `WithWpfScheduler` (part of `WithWpf`) needs that same + dispatcher to already exist. +- **A control bound without a source-generated `Bind` uses `BindWithValidation` instead.** This applies to a + control whose target is resolved by name at run time. diff --git a/docs/documentation/reactiveui/handbook/commands/advanced.md b/docs/documentation/reactiveui/handbook/commands/advanced.md index 5db76bbc..ec6054bd 100644 --- a/docs/documentation/reactiveui/handbook/commands/advanced.md +++ b/docs/documentation/reactiveui/handbook/commands/advanced.md @@ -7,10 +7,10 @@ Order: 3 The [basics page](index.md) creates commands with the static factory methods on `ReactiveCommand`. Those factories cover almost every case, because they call the same constructors this page covers directly. Write your own command -type when every command of a kind needs behavior the factories cannot give you: recording every parameter a -command has run with, logging every run of a combined command, or wrapping a non-reactive command source. This -page also covers the interfaces a view model exposes a command through, and `SwitchSubscribe`, for following a -command property that gets replaced. +type when every command of a kind needs behavior the factories cannot give you. Examples are recording every +parameter a command has run with, logging every run of a combined command, or wrapping a non-reactive command +source. This page also covers the interfaces a view model exposes a command through, and `SwitchSubscribe`, for +following a command property that gets replaced. ## Derive from `ReactiveCommand` @@ -77,8 +77,8 @@ Console.WriteLine(string.Join(", ", borrow.History)); ``` The second constructor is for execution logic that cancels through a callback instead of by unsubscribing. It -takes a `Func Result, Action Cancel)>>`: an observable of a tuple -holding the result observable and a `Cancel` action. `ReactiveCommand` calls `Cancel` when the execution is +takes a `Func Result, Action Cancel)>>`. That is an observable of a +tuple holding the result observable and a `Cancel` action. `ReactiveCommand` calls `Cancel` when the execution is disposed, instead of unsubscribing from `Result` directly. Use this when the work you are wrapping only knows how to stop through a callback of its own, such as a printer driver. @@ -105,7 +105,7 @@ Console.WriteLine(string.Join(", ", printReceipt.History)); Disposing `execution` calls `printerToldToStop`'s setter rather than unsubscribing from `printerOutput`, because this constructor cancels through the `Cancel` callback the execute function supplied. -A failed execution's exception reaches `ThrownExceptions` even when nothing awaits `Execute` — a plain +A failed execution's exception reaches `ThrownExceptions` even when nothing awaits `Execute`. A plain subscription sees it too, because `ThrownExceptions` is a separate stream from the command's result. ```csharp @@ -208,7 +208,7 @@ Console.WriteLine(thirdRun[0]); // 4 ``` -`withCanExecuteAndScheduler` combines `takeBackLoans` and `countShelf`, so its list has both results; the other +`withCanExecuteAndScheduler` combines `takeBackLoans` and `countShelf`, so its list has both results. The other two combine a single child, so their list holds one. ## Derive from `ReactiveCommandBase` @@ -217,7 +217,7 @@ two combine a single child, so their list holds one. `CombinedReactiveCommand` both build on. Deriving from it directly means implementing `CanExecute`, `IsExecuting`, `ThrownExceptions`, `Execute()`, `Execute(TParam)` and `Subscribe` yourself. It also means implementing two protected hooks, `ICommandCanExecute` and `ICommandExecute`. A `System.Windows.Input.ICommand` -caller — such as XAML binding infrastructure — routes through these hooks instead of calling `CanExecute` and +caller, such as XAML binding infrastructure, routes through these hooks instead of calling `CanExecute` and `Execute` directly. A realistic reason to derive this way is adapting an existing, non-reactive command source into ReactiveUI's contract, rather than writing execution logic that itself produces an `IObservable`. @@ -261,6 +261,45 @@ Closing the desk raises `CanExecuteChanged`, which the sample's handler turns in `CanExecute(null)` again. `DeskAnnouncementCommand` raises that event itself, by calling the protected `OnCanExecuteChanged` method `ReactiveCommandBase` defines for exactly this purpose. +## Subscribe a witness directly + +A command is an `IObservable`. A witness is a class that implements `IObserver` itself, instead of +using an operator such as `Subscribe(Action)`. A witness can subscribe to a command the normal way, whether +the command is a `ReactiveCommand` or a hand-written subclass of +`ReactiveCommandBase`. `BookReceiptPrinter` prints a receipt for every book a command produces: + +```csharp +LibraryDesk desk = new(); +BookReceiptPrinter witness = new(); + +using ReactiveCommand borrow = ReactiveCommand.Create(desk.Borrow); +using IDisposable subscription = borrow.Subscribe(witness); + +_ = await borrow.Execute(1); +_ = await borrow.Execute(4); + +// Output: +// Printed a receipt for Clean Code +// Printed a receipt for Refactoring +``` + +Code that only knows a command through the abstract `ReactiveCommandBase` subscribes a witness the +same way, whether the command underneath was built by a factory or, like `DeskAnnouncementCommand`, by hand: + +```csharp +LibraryDesk desk = new(); +DeskAnnouncementBoard board = new(); +using DeskAnnouncementCommand closingSoon = new(desk, static () => "The desk closes in ten minutes."); + +ReactiveCommandBase command = closingSoon; +using IDisposable subscription = command.Subscribe(board); + +_ = await command.Execute(); + +// Output: +// Board: The desk closes in ten minutes. +``` + ## Exposing a command from a view model A view model property typed as a concrete `ReactiveCommand` or `CombinedReactiveCommand DescribeAsync(IReactiveCommand command) ## Following a command property that gets replaced -A clerk's console screen might swap in a new search command each time the clerk switches catalogue: `SearchCommand` -on `DeskConsoleViewModel` is an `IReactiveCommand?` property that gets reassigned, not a fixed -field. Subscribing to `this.WhenAnyValue(x => x.SearchCommand)` gives you the command instances themselves, one +A clerk's console screen might swap in a new search command each time the clerk switches catalogue. +`SearchCommand` on `DeskConsoleViewModel` is an `IReactiveCommand?` property that gets +reassigned, not a fixed field. Subscribing to `this.WhenAnyValue(x => x.SearchCommand)` gives you the command instances themselves, one per assignment. You would still have to subscribe to whichever one is current by hand, and re-subscribe every -time it changes. `SwitchSubscribe` does that for you. Given an observable of commands, or of any inner -observable, it follows whichever one the property currently holds, dropping the old one and subscribing to the -new one the moment the property changes. +time it changes. `SwitchSubscribe` does that for you. Give it an observable of commands, or of any inner +observable. It follows whichever one the property currently holds. It drops the old one, and it subscribes to +the new one the moment the property changes. ```mermaid %%{init: {"theme": "base", "themeVariables": {"fontFamily": "Roboto, Helvetica, Arial, sans-serif", "fontSize": "15px", "primaryColor": "#DCE9FF", "primaryBorderColor": "#6C8EC4", "primaryTextColor": "#0B2447", "secondaryColor": "#E3F2E8", "secondaryBorderColor": "#7FA88C", "secondaryTextColor": "#12301C", "tertiaryColor": "#F3E5F5", "tertiaryBorderColor": "#A98BB0", "tertiaryTextColor": "#2E1437", "lineColor": "#7B8699", "textColor": "#1B1F27", "noteBkgColor": "#FFF4D6", "noteBorderColor": "#C9A94F", "noteTextColor": "#3A2A00", "actorBkg": "#DCE9FF", "actorBorder": "#6C8EC4", "actorTextColor": "#0B2447", "signalColor": "#7B8699", "signalTextColor": "#1B1F27", "labelBoxBkgColor": "#F1F3F8", "labelBoxBorderColor": "#A7AEBB", "edgeLabelBackground": "#F7F9FC", "clusterBkg": "#F7F9FC", "clusterBorder": "#C9D1DE"}}}%% @@ -396,7 +435,7 @@ result stream completes. Swapping in a new command does not call it. A `Reactive fact, never completes on its own. So `onCompleted` is reachable only if the outer `WhenAnyValue` observable ends. **3. Pick one of the command's own observables with a selector.** `SwitchSubscribe(selector, onNext)` projects -each command to one of its own observables — `IsExecuting`, here — before switching and subscribing. That lets you +each command to one of its own observables, `IsExecuting` here, before switching and subscribing. That lets you drive a busy indicator from whichever command is current. ```csharp @@ -421,8 +460,8 @@ The selector overload also takes error and completion handlers, for a command ty same three-callback shape as step 2. `SwitchSubscribe` is not limited to command properties. Any property typed as an `IObservable` can be -swapped and followed the same way: `DeskConsoleViewModel.Progress` is a running match count that gets replaced -each time a new search starts. +swapped and followed the same way. `DeskConsoleViewModel.Progress` is one example: a running match count that +gets replaced each time a new search starts. ```csharp DeskConsoleViewModel console = new(); @@ -446,8 +485,8 @@ Console.WriteLine(string.Join(", ", matchCounts)); // 1, 2, 5 ``` -Once `console.Progress` is reassigned to `secondSearch`, further values from `firstSearch` (`3`) are dropped; only -`secondSearch`'s values (`5`) reach `matchCounts`. +Once `console.Progress` is reassigned to `secondSearch`, further values from `firstSearch` (`3`) are dropped. +Only `secondSearch`'s values (`5`) reach `matchCounts`. A selector overload also works on a plain `IObservable` property, when the value you want is not the property itself but something it exposes. `ActiveSession` holds a `SearchSession` record whose `MatchCount` is the stream @@ -509,10 +548,14 @@ source, for apps that use System.Reactive instead. | Member | What it does | | --- | --- | -| `ReactiveCommand` constructor (result observable) | Base constructor a subclass uses to intercept every run; `ReactiveCommand.Create*` calls the same one. | +| `ReactiveCommand` constructor (result observable) | Base constructor a subclass uses to intercept every run. `ReactiveCommand.Create*` calls the same one. | | `ReactiveCommand` constructor (cancel callback) | Base constructor for execution logic that cancels through a callback rather than by unsubscribing. | | `CombinedReactiveCommand` constructors | Three base constructors a subclass uses, with `canExecute`, an `ISequencer`, or both. | -| `ReactiveCommandBase` | Abstract base every command type shares: `CanExecute`, `IsExecuting`, `ThrownExceptions`, `Execute`, `Subscribe`, plus the `ICommand` hooks a subclass overrides. | +| `ReactiveCommandBase` | Abstract base every command type shares. That includes `CanExecute`, `IsExecuting`, `ThrownExceptions`, `Execute`, `Subscribe`, plus the `ICommand` hooks a subclass overrides. | +| `ReactiveCommandBase.ICommandCanExecute(object?)` | Protected hook a subclass overrides. An `ICommand` caller's `CanExecute` routes here. | +| `ReactiveCommandBase.ICommandExecute(object?)` | Protected hook a subclass overrides. An `ICommand` caller's `Execute` routes here. | +| `ReactiveCommandBase.Subscribe(IObserver)` | Subscribes a witness directly to a command's results. | +| `ReactiveCommand.Subscribe(IObserver)` | The same subscription on a plain `ReactiveCommand`. | | `IReactiveCommand` | The interface to expose a command through when a view model should not commit to a concrete command type. | | `IReactiveCommand` | The non-generic interface for code that only needs `CanExecute` and `IsExecuting`. | | `SwitchSubscribe` | Subscribes to the inner observable a property currently holds, following each replacement. | diff --git a/docs/documentation/reactiveui/handbook/data-binding/android/wire-up-controls.md b/docs/documentation/reactiveui/handbook/data-binding/android/wire-up-controls.md index b2f3be10..36516482 100644 --- a/docs/documentation/reactiveui/handbook/data-binding/android/wire-up-controls.md +++ b/docs/documentation/reactiveui/handbook/data-binding/android/wire-up-controls.md @@ -6,17 +6,17 @@ that gap. Call it once, and it finds every eligible property on your `Activity`, `ILayoutViewHost`. It assigns each one the control with the matching resource ID, instead of a `FindViewById` call per field. It works the way [Butterknife](https://jakewharton.github.io/butterknife/) does for Java Android code. -The [school timetable app](../../platforms/android.md) uses it throughout; the examples below are all from that +The [school timetable app](../../platforms/android.md) uses it throughout. The examples below are all from that app. ## Naming policy `WireUpControls` builds a dictionary of every resource ID name in your layouts, lowercased, mapped to the actual -resource ID; the mapping mirrors the generated `Resource.designer.cs` file. Because the lookup is +resource ID. The mapping mirrors the generated `Resource.designer.cs` file. Because the lookup is case-insensitive, you cannot reuse the same resource name with two different casings across your layouts. Android generates a distinct ID for each casing, so there is no single ID `WireUpControls` could map both to. Layout files commonly use a lower-case-first name such as `subjectLabel`, while a C# property uses an -upper-case-first name such as `SubjectLabel`; the lowercased lookup is why that difference does not need a +upper-case-first name such as `SubjectLabel`. The lowercased lookup is why that difference does not need a `[WireUpResource]` override. ## Resolve strategies @@ -50,8 +50,8 @@ foreach (PropertyInfo member in wiredMembers) WireUpControls found 3 members to wire. ``` -`GetWireUpMembers` returns the properties a strategy would wire without wiring them, and `GetResourceName` -reads the resource name one property resolves to; both are useful for a diagnostic like the one above. The +`GetWireUpMembers` returns the properties a strategy would wire without wiring them. `GetResourceName` +reads the resource name one property resolves to. Both are useful for a diagnostic like the one above. The activity's layout, `activity_main.xml`, declares the three resources those properties resolve to: ```xml @@ -88,7 +88,7 @@ activity's layout, `activity_main.xml`, declares the three resources those prope ``` `LessonsRecyclerView`, `BadgeContainer` and `DetailContainer` are the three writable `View`-typed properties -`MainActivity` declares; `FeaturedLessonCard` and `PeeksRow` are assigned separately in code, so `Implicit` +`MainActivity` declares. `FeaturedLessonCard` and `PeeksRow` are assigned separately in code. So `Implicit` wires only the properties the activity actually declares as `View` subtypes. `LessonCardView`, a compound view inflated into the top of that layout, wires its own two labels the same way, from inside its own constructor: @@ -127,9 +127,26 @@ against the inflated row view, from its constructor body: this.WireUpControls(); ``` +A fragment calls a two-argument overload instead, since it wires against its inflated view rather than itself. +`LessonNotesFragment` uses the convenience form of that overload, which takes only the view and applies +`Implicit` without naming it: + +```csharp +View view = inflater!.Inflate(Resource.Layout.fragment_lesson_notes, container, false) + ?? throw new InvalidOperationException("Inflating fragment_lesson_notes produced no view."); + +// The convenience overload: no resolve strategy to state, since Implicit is what most fragments want. +AndroidX.ControlFetcherMixins.WireUpControls(this, view); +``` + +The core, non-AndroidX `ReactiveUI.ControlFetcherMixins` declares the same two overloads for the platform's own +`Android.App.Fragment`. This page does not show them. Attaching a classic `Fragment` to an activity needs the +classic `Activity.FragmentManager`. The Android platform itself deprecated that at API level 28, and the +timetable app targets a newer API than that. + ### Explicit opt-in -Under `ExplicitOptIn`, only a property that carries `[WireUpResource]` is wired; every other property is +Under `ExplicitOptIn`, only a property that carries `[WireUpResource]` is wired. Every other property is ignored, with no type check. `[WireUpResource]` takes an optional resource name, for a property whose name does not match its resource. `AbsenceStatusView`, a compound view, opts in its one label from its constructor body: @@ -166,6 +183,23 @@ instead of as an extension method: AndroidX.ControlFetcherMixins.WireUpControls(this, view, ControlFetcherMixins.ResolveStrategy.ExplicitOptIn); ``` +`[WireUpResource]`'s constructor argument is stored on `ResourceNameOverride`. `GetResourceName()` reads it as a +fallback when a property carries no override. `LessonDetailFragment` also reads it directly, off the attribute +itself: + +```csharp +// The name SubjectLabel was wired under: WireUpResourceAttribute.ResourceNameOverride, read directly +// rather than through GetResourceName(). +WireUpResourceAttribute? subjectAttribute = typeof(LessonDetailFragment) + .GetProperty(nameof(SubjectLabel))! + .GetCustomAttribute(); +TimetableLog.Info($"SubjectLabel's resource name override: {subjectAttribute?.ResourceNameOverride}."); +``` + +```text +SubjectLabel's resource name override: detailSubject. +``` + The fragment's layout carries a third control, `detailIcon`, that no property wires. `GetControl` fetches it directly instead, resolving it against a `View` and the fragment's assembly: @@ -220,7 +254,7 @@ public TextView? UnusedLabel { get; set; } Without `[IgnoreResource]`, `ExplicitOptOut` would try to wire `UnusedLabel` to a resource named `unusedlabel` and throw a `MissingFieldException`, since `activity_absence.xml` declares no such resource. `GetControl` above fetches `extraLabel` directly on the activity, the same way `LessonDetailFragment` fetched `detailIcon` on a -`View`; called with no argument, it resolves the resource named after the calling member instead. +`View`. Called with no argument, it resolves the resource named after the calling member instead. ```xml @@ -249,17 +283,18 @@ fetches `extraLabel` directly on the activity, the same way `LessonDetailFragmen `LessonCountBadgeHost`, a host with no view model, wires its one label through the auto-wireup constructor of `LayoutViewHostUnsafe` instead of a call to `WireUpControls` in its own body. Its constructor passes `attachToRoot: false`, `performAutoWireup: true` and `resolveStrategy: ControlFetcherMixins.ResolveStrategy.Implicit` -to the base constructor, which performs the same `Implicit` wire-up once inflation finishes; the constructor's -own body adds nothing further. `ReactiveViewHostUnsafe` offers the same constructor for a host with a view -model. The reflection-free `LayoutViewHost` and `ReactiveViewHost` have no such constructor. +to the base constructor. That base constructor calls the `ILayoutViewHost` overload of `WireUpControls` itself +once inflation finishes. The constructor's own body adds nothing further. `ReactiveViewHostUnsafe` +offers the same constructor for a host with a view model. `FirstLessonPeekHost` passes the same two flags to +it. The reflection-free `LayoutViewHost` and `ReactiveViewHost` have no such constructor. A host that wires its child without reflection, and stays trim- and AOT-safe, uses the `bind`-callback constructor instead and calls `FindViewById` itself. `WeekdayViewHost`, one page of the weekday pager, passes `bind: static (host, view) => ((WeekdayViewHost)host)._weekdayLabel = view.FindViewById(Resource.Id.weekdayLabel)` to the base constructor this way. -Every `WireUpControls` overload, and `GetControl`, carries `RequiresUnreferencedCode` and `RequiresDynamicCode`: -both reflect over your assembly's generated resource type to resolve a name to an integer ID. The `bind`-callback +Every `WireUpControls` overload, and `GetControl`, carries `RequiresUnreferencedCode` and `RequiresDynamicCode`. +Both reflect over your assembly's generated resource type to resolve a name to an integer ID. The `bind`-callback constructor above needs neither. `MissingFieldException` is what any `WireUpControls` overload throws when a property cannot be resolved to a matching resource, whichever strategy is in effect. diff --git a/docs/documentation/reactiveui/handbook/data-binding/avalonia.md b/docs/documentation/reactiveui/handbook/data-binding/avalonia.md index d5fc98f8..b556263d 100644 --- a/docs/documentation/reactiveui/handbook/data-binding/avalonia.md +++ b/docs/documentation/reactiveui/handbook/data-binding/avalonia.md @@ -1,72 +1,71 @@ --- Order: 3 --- -# Data Binding in AvaloniaUI with ReactiveUI +# Data Binding in Avalonia -> **Note** First of all, ensure you install the `Avalonia.ReactiveUI` package and *add a call* to `UseReactiveUI()` to your `AppBuilder` — that's super important. Note, that `Avalonia.ReactiveUI` package is supported by AvaloniaUI team, so if anything goes wrong, head over to [AvaloniaUI Gitter](https://gitter.im/AvaloniaUI/Avalonia). Also see [Avalonia.ReactiveUI Docs](https://docs.avaloniaui.net/guides/deep-dives/reactiveui). +An Avalonia view binds to its view model with the same `Bind`, `OneWayBind` and `BindCommand` calls as any other +platform. The Avalonia pieces, such as the view base classes and the start-up call, ship in the `ReactiveUI.Avalonia` +package. That package lives in its own repository, [reactiveui/ReactiveUI.Avalonia](https://github.com/reactiveui/ReactiveUI.Avalonia), +so the code on this page is quoted from its +[example app](https://github.com/reactiveui/ReactiveUI.Avalonia/tree/main/src/examples/ReactiveUI.Avalonia.Example). +[Installing for Avalonia](../../getting-started/installation/avalonia.md) lists the packages. -For [WhenActivated](../when-activated.md) to work, you need to use custom base classes from `Avalonia.ReactiveUI` package, such as `ReactiveWindow` or `ReactiveUserControl`. Of course, you can also implement the `IViewFor` interface by hand in your class, but ensure to store the `ViewModel` inside an `AvaloniaProperty`. If you wish to add activation support to your view models, then implement the `IActivatableViewModel` interface. A view model implementing `IActivatableViewModel` might look like the one below: +## Bind a view in three steps -```cs -public class ViewModel : ReactiveObject, IActivatableViewModel -{ - public ViewModelActivator Activator { get; } = new ViewModelActivator(); +1. **Start ReactiveUI with the app.** Call `UseReactiveUI` on Avalonia's `AppBuilder`. It registers the main-thread + sequencer, activation and binding support. `RegisterReactiveUIViewsFromEntryAssembly` registers each view in your + app, so view location can find it. - public ViewModel() - { - this.WhenActivated(disposables => - { - /* Handle activation */ - Disposable - .Create(() => { /* Handle deactivation */ }) - .DisposeWith(disposables); - }); - } -} -``` + ```cs + public static AppBuilder BuildAvaloniaApp() => AppBuilder + .Configure() + .UsePlatformDetect() + .UseReactiveUI(rxui => + { + // Optional: add custom registration here via rxui.WithRegistration(...) + }) + .RegisterReactiveUIViewsFromEntryAssembly(); + ``` -Remember to place a call to `WhenActivated` to the code-behind of your view, otherwise your view model's `WhenActivated` block won't get called. +2. **Derive the view from a ReactiveUI base class.** `ReactiveWindow` and `ReactiveUserControl` + implement `IViewFor`. They store the view model in an Avalonia property, so XAML bindings see it too. + Avalonia generates a field for each control that has an `x:Name` in the markup, so the view can name its controls + directly. -```cs -// This represents the View.xaml.cs code-behind file. -public partial class View : ReactiveWindow -{ - public View() - { - // If you put a WhenActivated block here, your activatable view model - // will also support activation, otherwise it won't. - this.WhenActivated(disposables => { /* Handle interactions etc. */ }); - AvaloniaXamlLoader.Load(this); - } -} -``` +3. **Bind inside `WhenActivated`.** [WhenActivated](../when-activated.md) runs its block when the view joins the + visual tree and disposes everything the block added when the view leaves it. Add each binding to the block's + `MultipleDisposable`, so no binding outlives the view. -```xml - - - - -``` + ```cs + public sealed partial class CommandLabView : ReactiveUserControl + { + public CommandLabView() + { + InitializeComponent(); + _ = this.WhenActivated(BindView); + } -The `WhenActivated` block is called when the `AttachedToVisualTree` event is published, and the disposable is disposed of when the `DetachedFromVisualTree` event is published. The behavior is the same for both the views and the view models. Unfortunately, Avalonia won't generate strongly typed `x:Name` references to controls, so the only way for now is to use the `FindControl` method that will find a control by the name specified in XAML, or to use `{Binding Path}` syntax. + private void BindView(ReactiveUI.Primitives.Disposables.MultipleDisposable disposables) + { + disposables.Add(this.Bind(ViewModel, static viewModel => viewModel.WorkItemText, static view => view.WorkItemTextBox.Text)); + disposables.Add(this.OneWayBind(ViewModel, static viewModel => viewModel.LastResult, static view => view.LastResultText.Text)); + disposables.Add(this.BindCommand(ViewModel, static viewModel => viewModel.RunWork, static view => view.RunWorkButton)); + } + } + ``` -> **Note** The `FindControl` method shouldn't be used inside an expression. Instead, create a custom property which calls the `FindControl` method, or store the control in a variable. See an example of how to use ReactiveUI type-safe bindings with AvaloniaUI. +`WorkItemTextBox`, `LastResultText` and `RunWorkButton` are the `x:Name` values in the view's `.axaml` file. The +lambdas are `static` because they capture nothing. See +[Dispose your subscriptions](../../guidelines/framework/dispose-your-subscriptions.md) for why every binding goes +into the activation block. -```cs -public partial class View : ReactiveWindow -{ - // Assume the Button control has the Name="ExampleButton" attribute defined in XAML. - public Button ExampleButton => this.FindControl