Skip to content

Commit 66e73a4

Browse files
authored
docs: add runnable documentation examples for the public API (#4503)
## Summary **Runnable documentation examples now cover the public API of every lean ReactiveUI package, one page project per handbook page.** - **New page projects under `src/examples/Documentation/Pages`.** Each project matches one page of the handbook on reactiveui.net (view models, commands, bindings, activation, interactions, view location, routing, collections, message bus, scheduling, persistence, testing, the app builder, registration, reflection) and runs as a console app that prints what each example does. - **Platform page projects for WPF, WinForms, WinUI, Blend and Drawing, MAUI, Blazor and Android.** Each is a small realistic app (a school timetable, a weather station dashboard, a course list) that runs its scenario end to end and prints the result; the Windows apps expose `--smoke`. - **Every example states its output.** Each example method ends in an `// Output:` block, and the website quotes the examples verbatim. - **Examples follow the merged fixes.** Routing uses the #4497 navigation streams, WinUI shows the #4496 AOT-safe hosts with their `*Unsafe` twins as the explicit opt-in, and the builder, MAUI host, ReactiveRecord, suspension and Android orientation examples show the fixed behaviour. - **CI tests only the projects under `tests/`.** The solution now builds the example apps, and `dotnet test` over the whole solution tried to run them, including the Android and MAUI apps, which need a device. - **The examples build with the same rules as Refit's.** `src/examples/Documentation` carries Refit's `.editorconfig` (explicit types instead of `var`), and `src/examples/Directory.Build.props` marks the examples as not packable. ## Why **The handbook needs an example for every public member, and each example has to compile and run against the current code.** - The pages on reactiveui.net quote these projects, so a page cannot drift from the API without a build or output check failing. - Members that no example calls are reviewed one by one: the platform or the framework calls them, another ReactiveUI package uses them, or they need hardware the examples cannot run on. ## Breaking changes **None.** The examples are not packaged. ## How this was verified **Every page project builds with no warnings, and each project's printed output matches its `// Output:` blocks.** The Windows apps ran their `--smoke` scenario on Windows, and the Android app ran its full scenario on an emulator. iOS and Mac Catalyst were not run. ## Notes for the reviewer **The code is sample code, so review it for readability and realism, not for architecture.** - Start with any one page project's `Program.cs`, which lists that page's examples in reading order. - `src/examples/Documentation/Directory.Build.props`, `src/examples/Directory.Build.props`, `.editorconfig` and the `reactiveui.slnx` entries are the only build changes. - Overload families such as `WithInstance` are shown at one and two arguments only; the pages state how many the family supports. - The WinUI maintenance panel uses `RoutedViewHostUnsafe` and `ViewModelViewHostUnsafe` for a view added with `Map`, because the default hosts cannot find a mapped view until #4502 is fixed. ## Checklist - [x] I have read the [Contribute guide](https://www.reactiveui.net/contribute/index.html) - [x] The PR title follows [Conventional Commits](https://www.conventionalcommits.org/) - [x] Tests cover this change, or the summary says why they do not - [ ] New or changed public API has XML documentation
1 parent f3c4b26 commit 66e73a4

421 files changed

Lines changed: 18430 additions & 61 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/ci-build.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,7 @@ jobs:
2222
productNamespacePrefix: "ReactiveUI"
2323
installWorkloads: true
2424
installWindowsAppRuntime: true
25+
# Test only the test projects: the solution also builds the documentation example apps, which are not tests.
26+
testProjects: tests/**/*.csproj
2527
secrets:
2628
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

‎.github/workflows/sonarcloud.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ jobs:
1616
productNamespacePrefix: ReactiveUI
1717
installWorkloads: true
1818
installWindowsAppRuntime: true
19+
# Test only the test projects: the solution also builds the documentation example apps, which are not tests.
20+
testProjects: tests/**/*.csproj
1921
sonarProjectKey: reactiveui_ReactiveUI
2022
sonarOrganization: reactiveui
2123
sonarExclusions: '**/tests/**,**/integrationtests/**,**/benchmarks/**,**/examples/**,**/TestResults/**'

‎src/examples/Directory.Build.props‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,9 @@
1010

1111
1212
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
13+
14+
<PropertyGroup>
15+
<IsPackable>falseIsPackable>
16+
<LangVersion>14.0LangVersion>
17+
PropertyGroup>
1318
Project>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# This policy applies to the complete runnable documentation examples.
2+
# All compiler, correctness, security, performance and remaining style rules are inherited.
3+
4+
[*.cs]
5+
# Explicit local types let readers understand a website block without following another method.
6+
# Keep the style analyzer enabled and configure it to enforce explicit types in these samples only.
7+
stylesharp.use_var = never
8+
stylesharp.SST2271.use_var = never
9+
csharp_style_var_for_built_in_types = false:suggestion
10+
csharp_style_var_when_type_is_apparent = false:suggestion
11+
csharp_style_var_elsewhere = false:suggestion
12+
13+
# Documentation-specific exemptions belong only in the website documentation folder.
14+
# Complete source examples keep the inherited XML documentation diagnostics enabled.
15+
16+
# Readers learn from concrete values ("Ada", "/imports/records", 10). Naming every literal as a constant only for
17+
# the analyzer sends them scrolling away from the line they are reading.
18+
dotnet_diagnostic.SST1486.severity = none
19+
dotnet_diagnostic.SST1471.severity = none
20+
# Samples show results as comments beside the line that produces them, which can look like commented-out code.
21+
dotnet_diagnostic.SST1148.severity = none
22+
# A sample's own "throw if the result is wrong" check is not part of an API contract that needs docs.
23+
dotnet_diagnostic.SST1662.severity = none
24+
# Samples build a client inline so the reader sees where it comes from; IHttpClientFactory is covered separately.
25+
dotnet_diagnostic.PSH1418.severity = none
26+
27+
# Samples name the type they create ("new Person(1, \"Ada\")") so a reader never has to work out what "new(...)" builds.
28+
dotnet_diagnostic.SST2202.severity = none
29+
dotnet_diagnostic.IDE0090.severity = none
30+
# One initializer entry per line, with braces on their own lines, reads better in samples than one long line.
31+
dotnet_diagnostic.SST1531.severity = none
32+
# Samples call methods for their effect and ignore the value, as readers do; "_ =" discards only add noise here.
33+
dotnet_diagnostic.SST2221.severity = none
34+
dotnet_diagnostic.IDE0058.severity = none
35+
dotnet_diagnostic.CA1806.severity = none
36+
# [MethodImpl(AggressiveInlining)] on small sample helpers is performance tuning that distracts from what they show.
37+
dotnet_diagnostic.PSH1410.severity = none
38+
# A view starts its WhenActivated block in its constructor, which hands 'this' to ReactiveUI. That is the pattern
39+
# ReactiveUI documents for every view, so samples show it without a suppression attribute on each constructor.
40+
dotnet_diagnostic.SST2403.severity = none
41+
42+
[TestingFrameworks/**/*.cs]
43+
# These tests are written for a beginner reading real test-framework code, not for API consumers browsing
44+
# IntelliSense: a plain "// Problem: ..." comment naming what the test is for reads better here than a
45+
# required /// doc comment on every [Fact]/[Test]/[TestMethod], and public test classes/methods do not need
46+
# XML docs (GenerateDocumentationFile is also off for these projects; see their .csproj).
47+
dotnet_diagnostic.SST1600.severity = none
48+
dotnet_diagnostic.SST1601.severity = none
49+
dotnet_diagnostic.SST1663.severity = none
50+
# A tiny record and JSON context used only by these tests do not need a [DebuggerDisplay]; that is
51+
# production-strictness these samples do not need.
52+
dotnet_diagnostic.SST2334.severity = none
53+
# xUnit's "pass TestContext.Current.CancellationToken" suggestion would make the xUnit project's tests differ
54+
# from the otherwise-identical NUnit/MSTest/TUnit versions just to satisfy one framework's analyzer.
55+
dotnet_diagnostic.xUnit1051.severity = none

‎src/examples/Documentation/Common/ExampleApp.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public static class ExampleApp
2323
/// Adds the example's own registrations, such as its views.
2424
public static void Start(Action<IReactiveUIBuilder> configure)
2525
{
26-
var builder = RxAppBuilder.CreateReactiveUIBuilder().WithMainThreadScheduler(Sequencer.Immediate);
26+
IReactiveUIBuilder builder = RxAppBuilder.CreateReactiveUIBuilder().WithMainThreadScheduler(Sequencer.Immediate);
2727
_ = builder.WithCoreServices().BuildApp();
2828
configure(builder);
2929
}

‎src/examples/Documentation/Common/Todo/InMemoryTodoStore.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public async Task AddAsync(string title, CancellationToken cancellatio
5353
public async Task<TodoItem> CompleteAsync(int id, CancellationToken cancellationToken)
5454
{
5555
await Task.Delay(Latency, cancellationToken).ConfigureAwait(false);
56-
var row = _rows.Find(row => row.Id == id) ?? throw new TodoStoreException($"Item {id} does not exist.");
56+
TodoItem row = _rows.Find(row => row.Id == id) ?? throw new TodoStoreException($"Item {id} does not exist.");
5757
row.IsDone = true;
5858
return row.Clone();
5959
}

‎src/examples/Documentation/Common/Todo/TodoListViewModel.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public TodoListViewModel(ITodoStore store)
3636
Load = ReactiveCommand.CreateFromTask(_store.QueryAsync);
3737
_subscriptions.Add(Load.Subscribe(rows => AllItems = rows));
3838

39-
var canAdd = this.WhenAnyValue(x => x.NewTitle).Select(static title => !string.IsNullOrWhiteSpace(title));
39+
IObservable<bool> canAdd = this.WhenAnyValue(x => x.NewTitle).Select(static title => !string.IsNullOrWhiteSpace(title));
4040
Add = ReactiveCommand.CreateFromTask(cancellationToken => _store.AddAsync(NewTitle.Trim(), cancellationToken), canAdd);
4141
_subscriptions.Add(Add.Subscribe(added =>
4242
{

‎src/examples/Documentation/Directory.Build.props‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
<ItemGroup>
1818
<AssemblyAttribute Include="System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage" />
1919
<ProjectReference Include="$(MSBuildThisFileDirectory)../../ReactiveUI/ReactiveUI.csproj" />
20+
ItemGroup>
21+
22+
23+
<ItemGroup>
2024
<Compile Include="$(MSBuildThisFileDirectory)Common/**/*.cs" Link="Common/%(RecursiveDir)%(Filename)%(Extension)" />
2125
ItemGroup>
2226

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
2+
// Licensed to the .NET Foundation under one or more agreements.
3+
// The .NET Foundation licenses this file to you under the MIT license.
4+
// See the LICENSE file in the project root for full license information.
5+
6+
using System.Collections.ObjectModel;
7+
8+
namespace ReactiveUI.Documentation.Collections;
9+
10+
/// Shows ActOnEveryObject, which calls a method for every object already in a collection and for every
11+
/// one added or removed afterwards.
12+
public static class AutoPersistExamples
13+
{
14+
/// ActOnEveryObject reports the items already in the collection first, then every later add and remove.
15+
public static void TrackAddsAndRemovesOnAnObservableCollection()
16+
{
17+
ObservableCollection<Product> inventory = [new Product("Kettle", 4)];
18+
List<string> log = [];
19+
using IDisposable subscription = inventory.ActOnEveryObject(
20+
product => log.Add($"add {product.Name}"),
21+
product => log.Add($"remove {product.Name}"));
22+
23+
inventory.Add(new Product("Toaster", 2));
24+
inventory.RemoveAt(0);
25+
26+
foreach (string entry in log)
27+
{
28+
Console.WriteLine(entry);
29+
}
30+
31+
// Output:
32+
// add Kettle
33+
// add Toaster
34+
// remove Kettle
35+
}
36+
37+
/// ActOnEveryObject also works on the a view model exposes to a view.
38+
public static void TrackAddsAndRemovesOnAReadOnlyView()
39+
{
40+
ObservableCollection<Product> inventory = [new Product("Kettle", 4)];
41+
ReadOnlyObservableCollection<Product> readOnlyInventory = new(inventory);
42+
List<string> log = [];
43+
using IDisposable subscription = readOnlyInventory.ActOnEveryObject(
44+
product => log.Add($"add {product.Name}"),
45+
product => log.Add($"remove {product.Name}"));
46+
47+
inventory.Add(new Product("Toaster", 2));
48+
49+
foreach (string entry in log)
50+
{
51+
Console.WriteLine(entry);
52+
}
53+
54+
// Output:
55+
// add Kettle
56+
// add Toaster
57+
}
58+
59+
/// ActOnEveryObject also subscribes directly to a change-set stream produced by ToReactiveChangeSet.
60+
public static void TrackAddsAndRemovesFromAChangeSetStream()
61+
{
62+
ObservableCollection<Product> inventory = [new Product("Kettle", 4)];
63+
List<string> log = [];
64+
using IDisposable subscription = inventory.ToReactiveChangeSet().ActOnEveryObject(
65+
product => log.Add($"add {product.Name}"),
66+
product => log.Add($"remove {product.Name}"));
67+
68+
inventory.Add(new Product("Toaster", 2));
69+
70+
foreach (string entry in log)
71+
{
72+
Console.WriteLine(entry);
73+
}
74+
75+
// Output:
76+
// add Kettle
77+
// add Toaster
78+
}
79+
80+
/// A collection that only raises CollectionChanged itself also works with ActOnEveryObject.
81+
public static void TrackAddsAndRemovesOnACustomCatalog()
82+
{
83+
ShopCatalog catalog = new();
84+
catalog.Stock(new Product("Kettle", 4));
85+
86+
List<string> log = [];
87+
using IDisposable subscription = catalog.ActOnEveryObject<Product, ShopCatalog>(
88+
product => log.Add($"add {product.Name}"),
89+
product => log.Add($"remove {product.Name}"));
90+
91+
catalog.Stock(new Product("Toaster", 2));
92+
catalog.SellOut(catalog.First(static product => product.Name == "Kettle"));
93+
94+
foreach (string entry in log)
95+
{
96+
Console.WriteLine(entry);
97+
}
98+
99+
// Output:
100+
// add Kettle
101+
// add Toaster
102+
// remove Kettle
103+
}
104+
}

0 commit comments

Comments
 (0)