From 94c519f4d30cdb15d72a996a1ebd3a8fcb4a17f0 Mon Sep 17 00:00:00 2001
From: Glenn Watson <5834289+glennawatson@users.noreply.github.com>
Date: Sun, 27 Sep 2026 16:12:10 +1000
Subject: [PATCH 01/10] docs(examples): show IActivatableViewModel.Activator
and witnesses subscribed to commands
---
.../Pages/commands/BookReceiptPrinter.cs | 22 ++++++++++
.../Pages/commands/CommandTypeExamples.cs | 42 +++++++++++++++++++
.../Pages/commands/DeskAnnouncementBoard.cs | 22 ++++++++++
.../Documentation/Pages/commands/Program.cs | 4 ++
.../Pages/when-activated/Program.cs | 2 +
.../when-activated/WhenActivatedExamples.cs | 17 ++++++++
6 files changed, 109 insertions(+)
create mode 100644 src/examples/Documentation/Pages/commands/BookReceiptPrinter.cs
create mode 100644 src/examples/Documentation/Pages/commands/DeskAnnouncementBoard.cs
diff --git a/src/examples/Documentation/Pages/commands/BookReceiptPrinter.cs b/src/examples/Documentation/Pages/commands/BookReceiptPrinter.cs
new file mode 100644
index 0000000000..2160d9bef0
--- /dev/null
+++ b/src/examples/Documentation/Pages/commands/BookReceiptPrinter.cs
@@ -0,0 +1,22 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace ReactiveUI.Documentation.Commands;
+
+///
+/// A receipt printer that watches a borrow command's results directly, by implementing
+/// itself, rather than through an operator such as Subscribe(Action<T>) .
+///
+public sealed class BookReceiptPrinter : IObserver
+{
+ ///
+ public void OnNext(Book value) => Console.WriteLine($"Printed a receipt for {value.Title}");
+
+ ///
+ public void OnError(Exception error) => Console.WriteLine($"Printer jammed: {error.Message}");
+
+ ///
+ public void OnCompleted() => Console.WriteLine("No more receipts to print");
+}
diff --git a/src/examples/Documentation/Pages/commands/CommandTypeExamples.cs b/src/examples/Documentation/Pages/commands/CommandTypeExamples.cs
index 4621a7a50e..4dc5e3aeb9 100644
--- a/src/examples/Documentation/Pages/commands/CommandTypeExamples.cs
+++ b/src/examples/Documentation/Pages/commands/CommandTypeExamples.cs
@@ -125,6 +125,48 @@ public static async Task DeriveFromCommandBase()
// cannot announce
}
+ ///
+ /// A witness that implements directly, such as , can
+ /// subscribe to a command's results the same way any other observer does.
+ ///
+ /// A task that completes once both books have been borrowed.
+ public static async Task SubscribeAWitnessToACommandsResults()
+ {
+ 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
+ }
+
+ ///
+ /// Subscribing a witness through works whether the command was
+ /// built with a factory or, like , by hand; a display board only needs to
+ /// know a command produces string results, not which subclass built it.
+ ///
+ /// A task that completes once the announcement has posted to the board.
+ public static async Task SubscribeAWitnessThroughTheBaseType()
+ {
+ 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.
+ }
+
///
/// Write your own command type from CombinedReactiveCommand<TParam, TResult> 's three constructors
/// when every combined command of a kind needs the same behaviour on each run;
diff --git a/src/examples/Documentation/Pages/commands/DeskAnnouncementBoard.cs b/src/examples/Documentation/Pages/commands/DeskAnnouncementBoard.cs
new file mode 100644
index 0000000000..6995ae049b
--- /dev/null
+++ b/src/examples/Documentation/Pages/commands/DeskAnnouncementBoard.cs
@@ -0,0 +1,22 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace ReactiveUI.Documentation.Commands;
+
+///
+/// A display board that posts each announcement it receives directly, by implementing
+/// itself. It only needs a command that produces string results, not any particular command subclass.
+///
+public sealed class DeskAnnouncementBoard : IObserver
+{
+ ///
+ public void OnNext(string value) => Console.WriteLine($"Board: {value}");
+
+ ///
+ public void OnError(Exception error) => Console.WriteLine($"Board offline: {error.Message}");
+
+ ///
+ public void OnCompleted() => Console.WriteLine("Board cleared");
+}
diff --git a/src/examples/Documentation/Pages/commands/Program.cs b/src/examples/Documentation/Pages/commands/Program.cs
index 57d01aa20a..0e1311c286 100644
--- a/src/examples/Documentation/Pages/commands/Program.cs
+++ b/src/examples/Documentation/Pages/commands/Program.cs
@@ -74,6 +74,10 @@
await CommandTypeExamples.DeriveFromCommandBase();
+await CommandTypeExamples.SubscribeAWitnessToACommandsResults();
+
+await CommandTypeExamples.SubscribeAWitnessThroughTheBaseType();
+
await CommandTypeExamples.DeriveCombinedCommand();
await CombinedCommandMembersExamples.ReadCombinedCommandMembersDirectly();
diff --git a/src/examples/Documentation/Pages/when-activated/Program.cs b/src/examples/Documentation/Pages/when-activated/Program.cs
index c2387b45fe..a5289c5731 100644
--- a/src/examples/Documentation/Pages/when-activated/Program.cs
+++ b/src/examples/Documentation/Pages/when-activated/Program.cs
@@ -14,6 +14,8 @@
WhenActivatedExamples.ActivateAView();
+WhenActivatedExamples.ActivateAScreenThroughTheInterface();
+
await WhenActivatedExamples.ReactivateToRefresh();
ScoreBoardActivationExamples.UpdateTheTitleWithAFunctionBlock();
diff --git a/src/examples/Documentation/Pages/when-activated/WhenActivatedExamples.cs b/src/examples/Documentation/Pages/when-activated/WhenActivatedExamples.cs
index 4eb75b9b3d..60d1464150 100644
--- a/src/examples/Documentation/Pages/when-activated/WhenActivatedExamples.cs
+++ b/src/examples/Documentation/Pages/when-activated/WhenActivatedExamples.cs
@@ -54,6 +54,23 @@ public static void ActivateAView()
// Book electrician
}
+ ///
+ /// A navigation host holds each screen through , so it does not need to know the
+ /// concrete view model type of whichever screen is on top; it reads
+ /// through the interface to activate it.
+ ///
+ public static void ActivateAScreenThroughTheInterface()
+ {
+ using TodoListViewModel viewModel = new(InMemoryTodoStore.CreateSeeded());
+ List screenStack = [viewModel];
+
+ using IDisposable activation = screenStack[0].Activator.Activate();
+ Console.WriteLine(viewModel.Items.Count);
+
+ // Output:
+ // 4
+ }
+
/// Each time the screen is shown again, its view model is activated again and reloads what changed while it was away.
/// A task that completes when the store has been changed behind the screen.
public static async Task ReactivateToRefresh()
From 1ab0d4809131ba85789ec9934698d33978eacce8 Mon Sep 17 00:00:00 2001
From: Glenn Watson <5834289+glennawatson@users.noreply.github.com>
Date: Sun, 27 Sep 2026 16:27:27 +1000
Subject: [PATCH 02/10] docs(examples): cover Android, MAUI, WinForms and WinUI
API gaps and extending IViewFor
---
.../Pages/platform-android/AbsenceActivity.cs | 13 ++
.../AbsenceNotePromptActivity.cs | 35 +++++
.../platform-android/LessonDetailFragment.cs | 8 +
.../platform-android/LessonNotesFragment.cs | 31 ++++
.../platform-android/LessonViewHolder.cs | 18 +++
.../LessonsRecyclerAdapter.cs | 19 +++
.../Pages/platform-android/MainActivity.cs | 48 +++++-
.../layout/activity_absence_note_prompt.xml | 11 ++
.../layout/fragment_lesson_notes.xml | 11 ++
.../platform-android/WeekdayPagerActivity.cs | 5 +-
.../platform-maui/LoggingRecipeViewHost.cs | 24 +++
.../platform-maui/LoggingViewModelViewHost.cs | 28 ++++
.../ViewModelViewHostExamples.cs | 30 ++++
.../VisibilityConverterExamples.cs | 14 ++
.../ContentSetMethodConvertersExamples.cs | 77 ++++++++++
.../AnalyticsViewModelViewHost.cs | 25 ++++
.../AnalyticsViewModelViewHost{TViewModel}.cs | 29 ++++
.../SensorMaintenanceCompactView.cs | 38 +++++
.../platform-winui/ViewContractExamples.cs | 141 ++++++++++++++++++
.../VisibilityConverterExamples.cs | 56 +++++++
.../WeatherReadingCompactRowView.cs | 35 +++++
.../Pages/platform-winui/WeatherStationApp.cs | 6 +
.../platform-winui/WinUIStartupExamples.cs | 4 +
.../Pages/view-location/BookReturnDialog.cs | 32 ++++
.../view-location/BookReturnViewModel.cs | 22 +++
.../ExtendingIViewForExamples.cs | 60 ++++++++
.../Pages/view-location/Program.cs | 9 +-
.../ReactiveVendorDialog{TViewModel}.cs | 52 +++++++
.../Pages/view-location/VendorDialog.cs | 46 ++++++
.../VendorDialogActivationFetcher.cs | 40 +++++
30 files changed, 963 insertions(+), 4 deletions(-)
create mode 100644 src/examples/Documentation/Pages/platform-android/AbsenceNotePromptActivity.cs
create mode 100644 src/examples/Documentation/Pages/platform-android/LessonNotesFragment.cs
create mode 100644 src/examples/Documentation/Pages/platform-android/Resources/layout/activity_absence_note_prompt.xml
create mode 100644 src/examples/Documentation/Pages/platform-android/Resources/layout/fragment_lesson_notes.xml
create mode 100644 src/examples/Documentation/Pages/platform-maui/LoggingRecipeViewHost.cs
create mode 100644 src/examples/Documentation/Pages/platform-maui/LoggingViewModelViewHost.cs
create mode 100644 src/examples/Documentation/Pages/platform-winforms/ContentSetMethodConvertersExamples.cs
create mode 100644 src/examples/Documentation/Pages/platform-winui/AnalyticsViewModelViewHost.cs
create mode 100644 src/examples/Documentation/Pages/platform-winui/AnalyticsViewModelViewHost{TViewModel}.cs
create mode 100644 src/examples/Documentation/Pages/platform-winui/SensorMaintenanceCompactView.cs
create mode 100644 src/examples/Documentation/Pages/platform-winui/ViewContractExamples.cs
create mode 100644 src/examples/Documentation/Pages/platform-winui/VisibilityConverterExamples.cs
create mode 100644 src/examples/Documentation/Pages/platform-winui/WeatherReadingCompactRowView.cs
create mode 100644 src/examples/Documentation/Pages/view-location/BookReturnDialog.cs
create mode 100644 src/examples/Documentation/Pages/view-location/BookReturnViewModel.cs
create mode 100644 src/examples/Documentation/Pages/view-location/ExtendingIViewForExamples.cs
create mode 100644 src/examples/Documentation/Pages/view-location/ReactiveVendorDialog{TViewModel}.cs
create mode 100644 src/examples/Documentation/Pages/view-location/VendorDialog.cs
create mode 100644 src/examples/Documentation/Pages/view-location/VendorDialogActivationFetcher.cs
diff --git a/src/examples/Documentation/Pages/platform-android/AbsenceActivity.cs b/src/examples/Documentation/Pages/platform-android/AbsenceActivity.cs
index 7bda16dfd6..e47e8de277 100644
--- a/src/examples/Documentation/Pages/platform-android/AbsenceActivity.cs
+++ b/src/examples/Documentation/Pages/platform-android/AbsenceActivity.cs
@@ -92,6 +92,19 @@ protected override async void OnCreate(Bundle? savedInstanceState)
await Task.Delay(TimeSpan.FromMilliseconds(100));
+ // This activity's own ActivityResult and StartActivityForResultAsync overloads, on the plain
+ // ReactiveActivity base rather than the AndroidX one MainActivity uses.
+ _ = ActivityResult.Subscribe(static result =>
+ TimetableLog.Info($"AbsenceActivity's own ActivityResult observed request {result.RequestCode}: {result.ResultCode}."));
+
+ Intent notePromptIntent = new(this, typeof(AbsenceNotePromptActivity));
+ notePromptIntent.PutExtra(SubjectExtra, ViewModel!.Subject);
+ (Android.App.Result ResultCode, Intent? Intent) notePromptByIntent = await StartActivityForResultAsync(notePromptIntent, 400);
+ TimetableLog.Info($"Note prompt (by intent) finished with {notePromptByIntent.ResultCode}.");
+
+ (Android.App.Result ResultCode, Intent? Intent) notePromptByType = await StartActivityForResultAsync(typeof(AbsenceNotePromptActivity), 401);
+ TimetableLog.Info($"Note prompt (by type) finished with {notePromptByType.ResultCode}.");
+
Intent resultIntent = new();
resultIntent.PutExtra(SubjectExtra, ViewModel!.Subject);
SetResult(Android.App.Result.Ok, resultIntent);
diff --git a/src/examples/Documentation/Pages/platform-android/AbsenceNotePromptActivity.cs b/src/examples/Documentation/Pages/platform-android/AbsenceNotePromptActivity.cs
new file mode 100644
index 0000000000..5c7e80b03a
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-android/AbsenceNotePromptActivity.cs
@@ -0,0 +1,35 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using Android.Content;
+
+namespace ReactiveUI.Documentation.PlatformAndroid;
+
+/// Asks whether to add a note to an absence report. This activity predates AndroidX, like
+/// : it derives from the plain , so
+/// can start it with that base's own ActivityResult and
+/// StartActivityForResultAsync members rather than the AndroidX ones already
+/// uses.
+[Activity(Label = "Add a note")]
+public sealed class AbsenceNotePromptActivity : ReactiveActivity
+{
+ ///
+ protected override async void OnCreate(Bundle? savedInstanceState)
+ {
+ base.OnCreate(savedInstanceState);
+ SetContentView(Resource.Layout.activity_absence_note_prompt);
+
+ string subject = Intent?.GetStringExtra(AbsenceActivity.SubjectExtra) ?? "this lesson";
+ TextView promptLabel = FindViewById(Resource.Id.notePromptLabel)!;
+ promptLabel.Text = $"Add a note about {subject}?";
+
+ await Task.Delay(TimeSpan.FromMilliseconds(100));
+
+ Intent resultIntent = new();
+ resultIntent.PutExtra(AbsenceActivity.SubjectExtra, subject);
+ SetResult(Android.App.Result.Ok, resultIntent);
+ Finish();
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-android/LessonDetailFragment.cs b/src/examples/Documentation/Pages/platform-android/LessonDetailFragment.cs
index a9372cfa78..864854431b 100644
--- a/src/examples/Documentation/Pages/platform-android/LessonDetailFragment.cs
+++ b/src/examples/Documentation/Pages/platform-android/LessonDetailFragment.cs
@@ -3,6 +3,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.
+using System.Reflection;
using Android.Views;
namespace ReactiveUI.Documentation.PlatformAndroid;
@@ -30,6 +31,13 @@ public sealed class LessonDetailFragment : AndroidX.ReactiveFragment();
+ TimetableLog.Info($"SubjectLabel's resource name override: {subjectAttribute?.ResourceNameOverride}.");
+
// A control the fragment fetches directly, rather than wiring to a property.
View? icon = view.GetControl(GetType().Assembly, "detailIcon");
TimetableLog.Info(icon is null ? "detailIcon was not found." : "detailIcon resolved directly with GetControl.");
diff --git a/src/examples/Documentation/Pages/platform-android/LessonNotesFragment.cs b/src/examples/Documentation/Pages/platform-android/LessonNotesFragment.cs
new file mode 100644
index 0000000000..fb3470c654
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-android/LessonNotesFragment.cs
@@ -0,0 +1,31 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using Android.Views;
+
+namespace ReactiveUI.Documentation.PlatformAndroid;
+
+/// Shows a revision note for a lesson, hosted as an .
+/// Wires its own label with the two-argument, implicit-strategy overload of WireUpControls , rather than
+/// stating a strategy explicitly the way does.
+[System.Diagnostics.DebuggerDisplay("{ViewModel}")]
+public sealed class LessonNotesFragment : AndroidX.ReactiveFragment
+{
+ /// Gets or sets the label showing the revision note; wired by naming convention to notesLabel .
+ public TextView? NotesLabel { get; set; }
+
+ ///
+ public override View? OnCreateView(LayoutInflater? inflater, ViewGroup? container, Bundle? savedInstanceState)
+ {
+ 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);
+
+ NotesLabel!.Text = ViewModel is null ? "No notes yet." : $"Revise {ViewModel.Subject} before the next lesson.";
+ return view;
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-android/LessonViewHolder.cs b/src/examples/Documentation/Pages/platform-android/LessonViewHolder.cs
index 95f058924d..053776ee9e 100644
--- a/src/examples/Documentation/Pages/platform-android/LessonViewHolder.cs
+++ b/src/examples/Documentation/Pages/platform-android/LessonViewHolder.cs
@@ -36,6 +36,24 @@ public LessonViewHolder(View itemView)
_subscriptions.Add(LongClickedWithViewModel.Subscribe(static lesson => TimetableLog.Info($"Long-clicked lesson: {lesson?.Subject}.")));
_subscriptions.Add(Activated.Subscribe(static _ => TimetableLog.Info("Lesson row attached to the window.")));
_subscriptions.Add(Deactivated.Subscribe(static _ => TimetableLog.Info("Lesson row detached from the window.")));
+
+ // The IReactiveObject surface every reactive Android base class carries: Changed and Changing report every
+ // property this holder itself raises a notification for (here, ViewModel, whenever RecyclerView recycles
+ // this row onto a new item), and ThrownExceptions reports anything an OnNext handler on those streams throws.
+ _subscriptions.Add(Changed.Subscribe(static change => TimetableLog.Info($"Lesson row property changed: {change.PropertyName}.")));
+ _subscriptions.Add(Changing.Subscribe(static change => TimetableLog.Info($"Lesson row property changing: {change.PropertyName}.")));
+ _subscriptions.Add(ThrownExceptions.Subscribe(static error => TimetableLog.Info($"Lesson row reported an exception: {error.Message}.")));
+
+ // SuppressChangeNotifications pauses Changed/Changing for its duration - useful when several properties
+ // are about to be filled in at once and only the final state matters. AreChangeNotificationsEnabled()
+ // reports whether a suppression is currently active.
+ using (SuppressChangeNotifications())
+ {
+ TimetableLog.Info($"Change notifications enabled while suppressed: {AreChangeNotificationsEnabled()}.");
+ }
+
+ TimetableLog.Info($"Change notifications enabled once the suppression ends: {AreChangeNotificationsEnabled()}.");
+ TimetableLog.Info($"Lesson row view: {View.GetType().Name}.");
}
/// Gets or sets the label showing the lesson's subject.
diff --git a/src/examples/Documentation/Pages/platform-android/LessonsRecyclerAdapter.cs b/src/examples/Documentation/Pages/platform-android/LessonsRecyclerAdapter.cs
index c817c2257e..2591ce52af 100644
--- a/src/examples/Documentation/Pages/platform-android/LessonsRecyclerAdapter.cs
+++ b/src/examples/Documentation/Pages/platform-android/LessonsRecyclerAdapter.cs
@@ -15,6 +15,20 @@ namespace ReactiveUI.Documentation.PlatformAndroid;
[System.Diagnostics.DebuggerDisplay("ItemCount = {ItemCount}")]
public sealed class LessonsRecyclerAdapter(ObservableCollection lessons) : ReactiveRecyclerViewAdapter>(lessons)
{
+ /// The view type for a lesson held in the science lab, room 7.
+ private const int LabViewType = 1;
+
+ /// The view type for every other lesson.
+ private const int StandardViewType = 0;
+
+ /// Picks a view type for a lesson: the science lab (room 7) gets its own type, so
+ /// can tell a lab lesson apart from any other.
+ /// The position of the lesson in the list.
+ /// The lesson at that position, or if the position is out of range.
+ /// An ID identifying the view type to use for the lesson.
+ public override int GetItemViewType(int position, LessonViewModel? viewModel) =>
+ viewModel?.Room == "7" ? LabViewType : StandardViewType;
+
///
public override RecyclerView.ViewHolder OnCreateViewHolder(ViewGroup parent, int viewType)
{
@@ -25,6 +39,11 @@ public override RecyclerView.ViewHolder OnCreateViewHolder(ViewGroup parent, int
View itemView = inflater.Inflate(Resource.Layout.lesson_item, parent, false)
?? throw new InvalidOperationException("Inflating lesson_item produced no view.");
+ if (viewType == LabViewType)
+ {
+ TimetableLog.Info("Lesson row created for the science lab (room 7).");
+ }
+
return new LessonViewHolder(itemView);
}
}
diff --git a/src/examples/Documentation/Pages/platform-android/MainActivity.cs b/src/examples/Documentation/Pages/platform-android/MainActivity.cs
index 3fa21a0fa9..efa1117270 100644
--- a/src/examples/Documentation/Pages/platform-android/MainActivity.cs
+++ b/src/examples/Documentation/Pages/platform-android/MainActivity.cs
@@ -5,6 +5,7 @@
using System.Reflection;
using Android.Content;
+using Android.Hardware.Usb;
using AndroidX.RecyclerView.Widget;
namespace ReactiveUI.Documentation.PlatformAndroid;
@@ -50,7 +51,18 @@ protected override async void OnCreate(Bundle? savedInstanceState)
TimetableLog.Info($" {member.Name} -> resource '{member.GetResourceName()}'.");
}
- ViewModel = new TimetableViewModel();
+ // The IReactiveObject surface every reactive Android base class carries: Changed and Changing report every
+ // property change this activity itself raises (here, ViewModel), and ThrownExceptions reports anything an
+ // OnNext handler on those streams throws. SuppressChangeNotifications pauses Changed/Changing for its
+ // duration - useful while several properties are about to be filled in at once.
+ _subscriptions.Add(Changed.Subscribe(static change => TimetableLog.Info($"MainActivity property changed: {change.PropertyName}.")));
+ _subscriptions.Add(Changing.Subscribe(static change => TimetableLog.Info($"MainActivity property changing: {change.PropertyName}.")));
+ _subscriptions.Add(ThrownExceptions.Subscribe(static error => TimetableLog.Info($"MainActivity reported an exception: {error.Message}.")));
+
+ using (SuppressChangeNotifications())
+ {
+ ViewModel = new TimetableViewModel();
+ }
// GetOrientation() returns the display's current rotation by name, such as "Rotation0" for the natural orientation.
PlatformOperations platformOperations = new();
@@ -76,7 +88,11 @@ protected override async void OnCreate(Bundle? savedInstanceState)
_ = new LessonPeekHost(this, PeeksRow!, attachToRoot: true) { ViewModel = ViewModel.Lessons[2] };
LessonsRecyclerView!.SetLayoutManager(new LinearLayoutManager(this));
- LessonsRecyclerView!.SetAdapter(new LessonsRecyclerAdapter(ViewModel.Lessons));
+ LessonsRecyclerAdapter lessonsAdapter = new(ViewModel.Lessons);
+ LessonsRecyclerView!.SetAdapter(lessonsAdapter);
+ TimetableLog.Info($"Lessons adapter reports {lessonsAdapter.ItemCount} items.");
+
+ RequestAttendanceScannerPermission();
_subscriptions.Add(Activated.Subscribe(static _ => TimetableLog.Info("MainActivity activated.")));
_subscriptions.Add(Deactivated.Subscribe(static _ => TimetableLog.Info("MainActivity deactivated.")));
@@ -97,6 +113,28 @@ protected override void Dispose(bool disposing)
base.Dispose(disposing);
}
+ /// Some timetable devices use a USB barcode scanner to check students in as they arrive. If one is
+ /// already plugged in, asks the system for permission to talk to it.
+ private void RequestAttendanceScannerPermission()
+ {
+ if (GetSystemService(UsbService) is not UsbManager usbManager)
+ {
+ return;
+ }
+
+ foreach (UsbDevice device in usbManager.DeviceList?.Values ?? [])
+ {
+ _subscriptions.Add(usbManager.PermissionRequested(this, device)
+ .Subscribe(granted => TimetableLog.Info($"USB device {device.DeviceName} permission granted: {granted}.")));
+ }
+
+ foreach (UsbAccessory accessory in usbManager.GetAccessoryList() ?? [])
+ {
+ _subscriptions.Add(usbManager.PermissionRequested(this, accessory)
+ .Subscribe(granted => TimetableLog.Info($"USB accessory {accessory.Model} permission granted: {granted}.")));
+ }
+ }
+
/// Drives the whole demo end to end: pick a lesson, report an absence, show its detail, confirm the
/// absence, page the weekdays, then open notification settings.
/// A task that completes once every screen in the scenario has run.
@@ -139,6 +177,12 @@ private async Task RunScenario()
.CommitNowAllowingStateLoss();
TimetableLog.Info("Showing notification settings.");
+ LessonNotesFragment notesFragment = new() { ViewModel = firstLesson };
+ SupportFragmentManager!.BeginTransaction()!
+ .Replace(DetailContainer!.Id, notesFragment)!
+ .CommitNowAllowingStateLoss();
+ TimetableLog.Info($"Showing notes for {firstLesson.Subject}.");
+
TimetableLog.Info("=== Scenario complete ===");
}
}
diff --git a/src/examples/Documentation/Pages/platform-android/Resources/layout/activity_absence_note_prompt.xml b/src/examples/Documentation/Pages/platform-android/Resources/layout/activity_absence_note_prompt.xml
new file mode 100644
index 0000000000..5047b3c000
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-android/Resources/layout/activity_absence_note_prompt.xml
@@ -0,0 +1,11 @@
+
+
+
+
diff --git a/src/examples/Documentation/Pages/platform-android/Resources/layout/fragment_lesson_notes.xml b/src/examples/Documentation/Pages/platform-android/Resources/layout/fragment_lesson_notes.xml
new file mode 100644
index 0000000000..e384e68fb3
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-android/Resources/layout/fragment_lesson_notes.xml
@@ -0,0 +1,11 @@
+
+
+
+
diff --git a/src/examples/Documentation/Pages/platform-android/WeekdayPagerActivity.cs b/src/examples/Documentation/Pages/platform-android/WeekdayPagerActivity.cs
index 108d079d1d..d328550803 100644
--- a/src/examples/Documentation/Pages/platform-android/WeekdayPagerActivity.cs
+++ b/src/examples/Documentation/Pages/platform-android/WeekdayPagerActivity.cs
@@ -102,7 +102,10 @@ private View CreatePage(WeekdayViewModel weekday, ViewGroup parent)
{
_ = weekday;
WeekdayViewHost host = new(this, parent);
- View view = host.ToView()!;
+
+ // LayoutViewHost's implicit operator to View, used here instead of the ToView() alternate.
+ View? convertedView = host;
+ View view = convertedView ?? throw new InvalidOperationException("WeekdayViewHost converted to a null View.");
WeekdayViewHost? typedHost = view.GetViewHost();
ILayoutViewHost? untypedHost = view.GetViewHost();
diff --git a/src/examples/Documentation/Pages/platform-maui/LoggingRecipeViewHost.cs b/src/examples/Documentation/Pages/platform-maui/LoggingRecipeViewHost.cs
new file mode 100644
index 0000000000..438cc26e08
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-maui/LoggingRecipeViewHost.cs
@@ -0,0 +1,24 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Diagnostics;
+using ReactiveUI.Maui;
+
+namespace ReactiveUI.Documentation.PlatformMaui;
+
+/// A for that counts each resolution.
+[DebuggerDisplay("LoggingRecipeViewHost: {ResolutionCount} resolutions")]
+public sealed class LoggingRecipeViewHost : ViewModelViewHost
+{
+ /// Gets the number of times this host has resolved a view.
+ public int ResolutionCount { get; private set; }
+
+ ///
+ protected override void ResolveViewForViewModel(object? viewModel, string? contract)
+ {
+ ResolutionCount++;
+ base.ResolveViewForViewModel(viewModel, contract);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-maui/LoggingViewModelViewHost.cs b/src/examples/Documentation/Pages/platform-maui/LoggingViewModelViewHost.cs
new file mode 100644
index 0000000000..8e0137d16a
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-maui/LoggingViewModelViewHost.cs
@@ -0,0 +1,28 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Diagnostics;
+using ReactiveUI.Maui;
+
+namespace ReactiveUI.Documentation.PlatformMaui;
+
+/// A that records the view model type it resolved a view for each time.
+[DebuggerDisplay("LoggingViewModelViewHost: {ResolvedViewModelTypes.Count} resolved")]
+public sealed class LoggingViewModelViewHost : ViewModelViewHost
+{
+ /// Gets the view model type names resolved so far, in order.
+ public List ResolvedViewModelTypes { get; } = [];
+
+ ///
+ protected override void ResolveViewForViewModel(object? viewModel, string? contract)
+ {
+ if (viewModel is not null)
+ {
+ ResolvedViewModelTypes.Add(viewModel.GetType().Name);
+ }
+
+ base.ResolveViewForViewModel(viewModel, contract);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-maui/ViewModelViewHostExamples.cs b/src/examples/Documentation/Pages/platform-maui/ViewModelViewHostExamples.cs
index 06b9133ad7..7b1c9eee95 100644
--- a/src/examples/Documentation/Pages/platform-maui/ViewModelViewHostExamples.cs
+++ b/src/examples/Documentation/Pages/platform-maui/ViewModelViewHostExamples.cs
@@ -159,4 +159,34 @@ public static void GenericHostTypesTheViewModelProperty()
// 3
// True
}
+
+ /// Overriding ResolveViewForViewModel lets a host observe each resolution before the base class runs it.
+ public static void OverridingResolveViewForViewModelObservesEachResolution()
+ {
+ AppLocator.CurrentMutable.Register>(static () => new RecipeListContentView());
+
+ LoggingViewModelViewHost host = new();
+ host.ViewModel = new RecipeListViewModel(new RecipeBookScreen());
+ host.ViewModel = null;
+
+ Console.WriteLine(string.Join(", ", host.ResolvedViewModelTypes));
+
+ // Output:
+ // RecipeListViewModel
+ }
+
+ /// overrides the same method, so a generic host can observe resolutions too.
+ public static void GenericHostOverridesResolveViewForViewModelToo()
+ {
+ AppLocator.CurrentMutable.Register>(static () => new RecipeListContentView());
+
+ LoggingRecipeViewHost host = new();
+ host.ViewModel = new RecipeListViewModel(new RecipeBookScreen());
+ host.ViewModel = new RecipeListViewModel(new RecipeBookScreen());
+
+ Console.WriteLine(host.ResolutionCount);
+
+ // Output:
+ // 2
+ }
}
diff --git a/src/examples/Documentation/Pages/platform-maui/VisibilityConverterExamples.cs b/src/examples/Documentation/Pages/platform-maui/VisibilityConverterExamples.cs
index 07696ab95a..ecbc3278cb 100644
--- a/src/examples/Documentation/Pages/platform-maui/VisibilityConverterExamples.cs
+++ b/src/examples/Documentation/Pages/platform-maui/VisibilityConverterExamples.cs
@@ -55,4 +55,18 @@ public static void VisibilityToBooleanConvertsBack()
// False
// False
}
+
+ /// GetAffinityForObjects reports how well each converter matches a binding; ReactiveUI's binding resolution calls it to pick a converter.
+ public static void GetAffinityForObjectsReportsTheBuiltInScore()
+ {
+ BooleanToVisibilityTypeConverter toVisibility = new();
+ VisibilityToBooleanTypeConverter toBoolean = new();
+
+ Console.WriteLine(toVisibility.GetAffinityForObjects());
+ Console.WriteLine(toBoolean.GetAffinityForObjects());
+
+ // Output:
+ // 2
+ // 2
+ }
}
diff --git a/src/examples/Documentation/Pages/platform-winforms/ContentSetMethodConvertersExamples.cs b/src/examples/Documentation/Pages/platform-winforms/ContentSetMethodConvertersExamples.cs
new file mode 100644
index 0000000000..a21d4b9204
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-winforms/ContentSetMethodConvertersExamples.cs
@@ -0,0 +1,77 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using ReactiveUI.Winforms;
+
+namespace ReactiveUI.Documentation.PlatformWinforms;
+
+///
+/// Shows and , the
+/// set-method converters Bind uses to fill a 's or 's
+/// Controls collection from a source, since neither collection has a normal property setter.
+///
+public static class ContentSetMethodConvertersExamples
+{
+ /// The panel converter matches a source that holds s and a target of ; nothing else.
+ public static void PanelConverterMatchesAControlCollection()
+ {
+ PanelSetMethodBindingConverter converter = new();
+
+ Console.WriteLine(converter.GetAffinityForObjects(typeof(List), typeof(Control.ControlCollection)));
+ Console.WriteLine(converter.GetAffinityForObjects(typeof(string), typeof(Control.ControlCollection)));
+
+ // Output:
+ // 10
+ // 0
+ }
+
+ /// Performing the set clears the panel and adds every control from the new value.
+ public static void PanelConverterReplacesTheControls()
+ {
+ using Panel panel = new();
+ using Label first = new() { Text = "Chicken soup" };
+ using Label second = new() { Text = "Bread rolls" };
+
+ PanelSetMethodBindingConverter converter = new();
+ converter.PerformSet(panel.Controls, new List { first, second }, arguments: null);
+
+ Console.WriteLine(panel.Controls.Count);
+ Console.WriteLine(((Label)panel.Controls[0]).Text);
+
+ // Output:
+ // 2
+ // Chicken soup
+ }
+
+ /// The table converter matches the same kind of source, but against a instead.
+ public static void TableConverterMatchesATableLayoutCollection()
+ {
+ TableContentSetMethodBindingConverter converter = new();
+
+ Console.WriteLine(converter.GetAffinityForObjects(typeof(List), typeof(TableLayoutControlCollection)));
+ Console.WriteLine(converter.GetAffinityForObjects(typeof(List), typeof(Control.ControlCollection)));
+
+ // Output:
+ // 10
+ // 0
+ }
+
+ /// Performing the set clears the table and adds every control from the new value.
+ public static void TableConverterReplacesTheControls()
+ {
+ using TableLayoutPanel table = new();
+ using Label first = new() { Text = "Roast dinner" };
+
+ TableContentSetMethodBindingConverter converter = new();
+ converter.PerformSet(table.Controls, new List { first }, arguments: null);
+
+ Console.WriteLine(table.Controls.Count);
+ Console.WriteLine(((Label)table.Controls[0]).Text);
+
+ // Output:
+ // 1
+ // Roast dinner
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-winui/AnalyticsViewModelViewHost.cs b/src/examples/Documentation/Pages/platform-winui/AnalyticsViewModelViewHost.cs
new file mode 100644
index 0000000000..26da007b19
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-winui/AnalyticsViewModelViewHost.cs
@@ -0,0 +1,25 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace ReactiveUI.Documentation.PlatformWinui;
+
+///
+/// A that records which view it resolved for each reading, standing in for the
+/// dashboard's usage analytics. It overrides ResolveViewForViewModel to run the base resolution first, then
+/// note the view it chose.
+///
+[System.Diagnostics.DebuggerDisplay("AnalyticsViewModelViewHost")]
+public sealed class AnalyticsViewModelViewHost : ViewModelViewHost
+{
+ /// Gets the resolved view names recorded so far, in order.
+ public List ResolvedViews { get; } = [];
+
+ ///
+ protected override void ResolveViewForViewModel(object? viewModel, string? contract)
+ {
+ base.ResolveViewForViewModel(viewModel, contract);
+ ResolvedViews.Add(Content?.GetType().Name ?? "(nothing)");
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-winui/AnalyticsViewModelViewHost{TViewModel}.cs b/src/examples/Documentation/Pages/platform-winui/AnalyticsViewModelViewHost{TViewModel}.cs
new file mode 100644
index 0000000000..5c23c24f54
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-winui/AnalyticsViewModelViewHost{TViewModel}.cs
@@ -0,0 +1,29 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace ReactiveUI.Documentation.PlatformWinui;
+
+///
+/// The generic twin of , typed to one view model so no cast is needed to
+/// read ViewModel back. It overrides the generic ResolveViewForViewModel the same way: run the base
+/// resolution, then note the view it chose.
+///
+/// The type of the view model the host shows.
+[System.Diagnostics.DebuggerDisplay("AnalyticsViewModelViewHost")]
+public sealed class AnalyticsViewModelViewHost<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TViewModel> : ViewModelViewHost
+ where TViewModel : class
+{
+ /// Gets the resolved view names recorded so far, in order.
+ public List ResolvedViews { get; } = [];
+
+ ///
+ protected override void ResolveViewForViewModel(TViewModel? viewModel, string? contract)
+ {
+ base.ResolveViewForViewModel(viewModel, contract);
+ ResolvedViews.Add(Content?.GetType().Name ?? "(nothing)");
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-winui/SensorMaintenanceCompactView.cs b/src/examples/Documentation/Pages/platform-winui/SensorMaintenanceCompactView.cs
new file mode 100644
index 0000000000..b0919bb501
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-winui/SensorMaintenanceCompactView.cs
@@ -0,0 +1,38 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using Microsoft.UI.Xaml.Controls;
+
+namespace ReactiveUI.Documentation.PlatformWinui;
+
+///
+/// A shorter view of a , for a host whose ViewContract asks for the
+/// "Compact" layout. It implements only the non-generic , the same way
+/// does, so the source generator writes no lookup entry for it either; an
+/// example maps it to the "Compact" contract explicitly.
+///
+[System.Diagnostics.DebuggerDisplay("SensorMaintenanceCompactView")]
+public sealed class SensorMaintenanceCompactView : UserControl, IViewFor
+{
+ /// The label showing the visit, in one shorter line.
+ private readonly TextBlock _label = new();
+
+ /// Initializes a new instance of the class.
+ public SensorMaintenanceCompactView() => Content = _label;
+
+ ///
+ public object? ViewModel
+ {
+ get;
+ set
+ {
+ field = value;
+ _label.Text = value is SensorMaintenanceViewModel visit ? visit.StationName : "(no visit)";
+ }
+ }
+
+ /// Gets the text the view shows for its visit.
+ public string Summary => _label.Text;
+}
diff --git a/src/examples/Documentation/Pages/platform-winui/ViewContractExamples.cs b/src/examples/Documentation/Pages/platform-winui/ViewContractExamples.cs
new file mode 100644
index 0000000000..4ae669a0b2
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-winui/ViewContractExamples.cs
@@ -0,0 +1,141 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace ReactiveUI.Documentation.PlatformWinui;
+
+///
+/// Shows the contract members every view host adds beyond what already exercises:
+/// ViewContract , ViewContractObservable and the ViewContractObservableProperty dependency
+/// property, plus the protected ResolveViewForViewModel a host calls each time its view model or contract
+/// changes. Each host below builds its own and view locator, so it does not touch the
+/// dashboard's own registrations.
+///
+public static class ViewContractExamples
+{
+ ///
+ /// ResolveViewForViewModel runs once for the empty host and again for each ViewModel change;
+ /// overrides it to record what it resolved. ViewContract
+ /// republishes as ViewContractObservable , the same stream ViewContractObservableProperty holds.
+ ///
+ public static void ViewModelViewHostResolvesEachViewModelChangeAndRepublishesItsContract()
+ {
+ DefaultViewLocator locator = new();
+ locator.Map();
+
+ AnalyticsViewModelViewHost host = new() { ViewLocator = locator };
+ WeatherReading riverside = new("Riverside", 18.5, isStormy: false);
+ WeatherReading highlands = new("Highlands", 9.0, isStormy: true);
+
+ host.ViewModel = riverside;
+ host.ViewModel = highlands;
+
+ Console.WriteLine(string.Join(", ", host.ResolvedViews));
+
+ host.ViewContract = "Wide";
+ Console.WriteLine(host.ViewContract);
+
+ string? published = null;
+ using IDisposable subscription = host.ViewContractObservable.Subscribe(contract => published = contract);
+ Console.WriteLine(published);
+
+ bool sameObservable = ReferenceEquals(host.ViewContractObservable, host.GetValue(ViewModelViewHost.ViewContractObservableProperty));
+ Console.WriteLine(sameObservable);
+
+ // Output:
+ // (nothing), WeatherReadingRowView, WeatherReadingRowView
+ // Wide
+ // Wide
+ // True
+ }
+
+ /// adds the same three contract members, typed to one view model.
+ public static void GenericViewModelViewHostResolvesEachViewModelChangeAndRepublishesItsContract()
+ {
+ DefaultViewLocator locator = new();
+ locator.Map();
+
+ AnalyticsViewModelViewHost host = new() { ViewLocator = locator };
+ WeatherReading harbor = new("Harbor", 21.0, isStormy: false);
+ WeatherReading highlands = new("Highlands", 9.0, isStormy: true);
+
+ host.ViewModel = harbor;
+ host.ViewModel = highlands;
+
+ Console.WriteLine(string.Join(", ", host.ResolvedViews));
+
+ host.ViewContract = "Wide";
+ Console.WriteLine(host.ViewContract);
+
+ string? published = null;
+ using IDisposable subscription = host.ViewContractObservable.Subscribe(contract => published = contract);
+ Console.WriteLine(published);
+
+ bool sameObservable = ReferenceEquals(
+ host.ViewContractObservable,
+ host.GetValue(ViewModelViewHost.ViewContractObservableProperty));
+ Console.WriteLine(sameObservable);
+
+ // Output:
+ // (nothing), WeatherReadingRowView, WeatherReadingRowView
+ // Wide
+ // Wide
+ // True
+ }
+
+ /// Setting ViewContract before the router navigates picks the view mapped to that contract.
+ public static void RoutedViewHostViewContractSelectsTheContractView()
+ {
+ DefaultViewLocator locator = new();
+ locator.Map();
+ locator.Map("Compact");
+
+ WeatherShell defaultShell = new();
+ RoutedViewHost defaultHost = new() { ViewLocator = locator, Router = defaultShell.Router };
+ _ = defaultShell.Router.Navigate.Execute(new SensorMaintenanceViewModel(defaultShell, "Harbor", "Replace the wind vane")).Subscribe();
+ Console.WriteLine(((SensorMaintenanceView)defaultHost.Content).Summary);
+
+ WeatherShell compactShell = new();
+ RoutedViewHost compactHost = new() { ViewLocator = locator, Router = compactShell.Router, ViewContract = "Compact" };
+ _ = compactShell.Router.Navigate.Execute(new SensorMaintenanceViewModel(compactShell, "Riverside", "Clean the rain gauge")).Subscribe();
+ Console.WriteLine(((SensorMaintenanceCompactView)compactHost.Content).Summary);
+ Console.WriteLine(compactHost.ViewContract);
+
+ string? published = null;
+ using IDisposable subscription = ((IObservable)compactHost.GetValue(RoutedViewHost.ViewContractObservableProperty))
+ .Subscribe(contract => published = contract);
+ Console.WriteLine(published);
+
+ // Output:
+ // Harbor: Replace the wind vane
+ // Riverside
+ // Compact
+ // Compact
+ }
+
+ /// adds the same three contract members, typed to one routable view model.
+ public static void GenericRoutedViewHostViewContractSelectsTheContractView()
+ {
+ DefaultViewLocator locator = new();
+ locator.Map();
+ locator.Map("Compact");
+
+ WeatherShell shell = new();
+ RoutedViewHost host = new() { ViewLocator = locator, Router = shell.Router, ViewContract = "Compact" };
+ _ = shell.Router.Navigate.Execute(new SensorMaintenanceViewModel(shell, "Highlands", "Inspect the anemometer")).Subscribe();
+
+ Console.WriteLine(((SensorMaintenanceCompactView)host.Content).Summary);
+ Console.WriteLine(host.ViewContract);
+
+ bool sameObservable = ReferenceEquals(
+ host.ViewContractObservable,
+ host.GetValue(RoutedViewHost.ViewContractObservableProperty));
+ Console.WriteLine(sameObservable);
+
+ // Output:
+ // Highlands
+ // Compact
+ // True
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-winui/VisibilityConverterExamples.cs b/src/examples/Documentation/Pages/platform-winui/VisibilityConverterExamples.cs
new file mode 100644
index 0000000000..b8ffca3655
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-winui/VisibilityConverterExamples.cs
@@ -0,0 +1,56 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using Microsoft.UI.Xaml;
+
+namespace ReactiveUI.Documentation.PlatformWinui;
+
+///
+/// Shows the parts of ,
+/// and does not already exercise through a
+/// binding: the affinity score ReactiveUI's binding resolution asks each converter for, and the one hint WinUI's
+/// has no member for.
+///
+public static class VisibilityConverterExamples
+{
+ ///
+ /// asks for Visibility.Hidden instead of
+ /// Visibility.Collapsed for the non-visible value. WinUI's enum has no
+ /// Hidden member, so the converter ignores the hint there and always returns Collapsed .
+ ///
+ public static void UseHiddenHasNoEffectOnWinUI()
+ {
+ BooleanToVisibilityTypeConverter converter = new();
+
+ _ = converter.TryConvert(false, BooleanToVisibilityHint.None, out Visibility none);
+ _ = converter.TryConvert(false, BooleanToVisibilityHint.UseHidden, out Visibility useHidden);
+
+ Console.WriteLine(none);
+ Console.WriteLine(useHidden);
+ Console.WriteLine(none == useHidden);
+
+ // Output:
+ // Collapsed
+ // Collapsed
+ // True
+ }
+
+ ///
+ /// GetAffinityForObjects reports how well a converter matches a binding; ReactiveUI's binding resolution
+ /// calls it on every registered converter and picks the one with the highest score.
+ ///
+ public static void GetAffinityForObjectsReportsTheBuiltInScore()
+ {
+ BooleanToVisibilityTypeConverter toVisibility = new();
+ VisibilityToBooleanTypeConverter toBoolean = new();
+
+ Console.WriteLine(toVisibility.GetAffinityForObjects());
+ Console.WriteLine(toBoolean.GetAffinityForObjects());
+
+ // Output:
+ // 2
+ // 2
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-winui/WeatherReadingCompactRowView.cs b/src/examples/Documentation/Pages/platform-winui/WeatherReadingCompactRowView.cs
new file mode 100644
index 0000000000..c1d4a70890
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-winui/WeatherReadingCompactRowView.cs
@@ -0,0 +1,35 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using Microsoft.UI.Xaml.Controls;
+
+namespace ReactiveUI.Documentation.PlatformWinui;
+
+///
+/// A shorter view of a , for a host whose ViewContract asks for the "Compact"
+/// layout. keeps it out of the generated view lookup, so it only
+/// shows up where an example maps it to the "Compact" contract explicitly.
+///
+[ExcludeFromViewRegistration]
+[System.Diagnostics.DebuggerDisplay("WeatherReadingCompactRowView")]
+public sealed class WeatherReadingCompactRowView : ReactiveUserControl
+{
+ /// The label showing the compact reading.
+ private readonly TextBlock _label = new();
+
+ /// Initializes a new instance of the class.
+ public WeatherReadingCompactRowView()
+ {
+ Content = _label;
+
+ _ = RegisterPropertyChangedCallback(ViewModelProperty, static (sender, _) =>
+ {
+ WeatherReadingCompactRowView view = (WeatherReadingCompactRowView)sender;
+ view._label.Text = view.BindingRoot is WeatherReading reading
+ ? $"{reading.StationName}: {reading.TemperatureCelsius:0.0} C"
+ : "(no reading)";
+ });
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-winui/WeatherStationApp.cs b/src/examples/Documentation/Pages/platform-winui/WeatherStationApp.cs
index 12bc6b9718..3c2e4c7184 100644
--- a/src/examples/Documentation/Pages/platform-winui/WeatherStationApp.cs
+++ b/src/examples/Documentation/Pages/platform-winui/WeatherStationApp.cs
@@ -47,6 +47,12 @@ protected override void OnLaunched(LaunchActivatedEventArgs args)
WinUIStartupExamples.ShowTheIndividualExtensions();
WinUIStartupExamples.AddTheUnsafeTemplateHook();
WinUIStartupExamples.MapAViewFromTheServiceLocator();
+ VisibilityConverterExamples.UseHiddenHasNoEffectOnWinUI();
+ VisibilityConverterExamples.GetAffinityForObjectsReportsTheBuiltInScore();
+ ViewContractExamples.ViewModelViewHostResolvesEachViewModelChangeAndRepublishesItsContract();
+ ViewContractExamples.GenericViewModelViewHostResolvesEachViewModelChangeAndRepublishesItsContract();
+ ViewContractExamples.RoutedViewHostViewContractSelectsTheContractView();
+ ViewContractExamples.GenericRoutedViewHostViewContractSelectsTheContractView();
_mainWindow = new MainWindow(SeedReadings);
diff --git a/src/examples/Documentation/Pages/platform-winui/WinUIStartupExamples.cs b/src/examples/Documentation/Pages/platform-winui/WinUIStartupExamples.cs
index 8785e97338..1238f92582 100644
--- a/src/examples/Documentation/Pages/platform-winui/WinUIStartupExamples.cs
+++ b/src/examples/Documentation/Pages/platform-winui/WinUIStartupExamples.cs
@@ -31,6 +31,10 @@ public static void ShowTheIndividualExtensions()
Console.WriteLine($"WithWinUIConverters/WithWinUIScheduler/Registrations configured: {configured is not null}");
Console.WriteLine($"WinUI main-thread scheduler: {WinUIMainThreadScheduler.GetType().Name}");
+
+ // Output:
+ // WithWinUIConverters/WithWinUIScheduler/Registrations configured: True
+ // WinUI main-thread scheduler: DispatcherQueueSequencer
}
///
diff --git a/src/examples/Documentation/Pages/view-location/BookReturnDialog.cs b/src/examples/Documentation/Pages/view-location/BookReturnDialog.cs
new file mode 100644
index 0000000000..a9819e8043
--- /dev/null
+++ b/src/examples/Documentation/Pages/view-location/BookReturnDialog.cs
@@ -0,0 +1,32 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using ReactiveUI.Documentation.Controls;
+
+namespace ReactiveUI.Documentation.ViewLocation;
+
+///
+/// A library desk's book-return dialog, built on rather than on a
+/// ReactiveUI base, because is the base the (pretend) dialog library requires.
+///
+[System.Diagnostics.DebuggerDisplay("BookReturnDialog ViewModel = {ViewModel}")]
+public sealed class BookReturnDialog : ReactiveVendorDialog
+{
+ /// Initializes a new instance of the class.
+ public BookReturnDialog() =>
+ this.WhenActivated(
+ disposables =>
+ {
+ disposables(this.OneWayBind(ViewModel, x => x.BookTitle, v => v.TitleLabel.Text));
+ disposables(this.Bind(ViewModel, x => x.IsDamaged, v => v.DamagedCheckBox.IsChecked));
+ },
+ this.WhenAnyValue(x => x.ViewModel));
+
+ /// Gets the label that shows the title of the book being returned.
+ public Label TitleLabel { get; } = new();
+
+ /// Gets the check box the librarian ticks when the book comes back damaged.
+ public CheckBox DamagedCheckBox { get; } = new();
+}
diff --git a/src/examples/Documentation/Pages/view-location/BookReturnViewModel.cs b/src/examples/Documentation/Pages/view-location/BookReturnViewModel.cs
new file mode 100644
index 0000000000..63bb11fbef
--- /dev/null
+++ b/src/examples/Documentation/Pages/view-location/BookReturnViewModel.cs
@@ -0,0 +1,22 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace ReactiveUI.Documentation.ViewLocation;
+
+/// Confirms a book's return at the library desk, including whether it came back damaged.
+/// The title of the book being returned.
+[System.Diagnostics.DebuggerDisplay("BookReturnViewModel BookTitle = {BookTitle}")]
+public sealed class BookReturnViewModel(string bookTitle) : ReactiveObject
+{
+ /// Gets the title of the book being returned.
+ public string BookTitle => bookTitle;
+
+ /// Gets or sets a value indicating whether the returned book is damaged.
+ public bool IsDamaged
+ {
+ get;
+ set => this.RaiseAndSetIfChanged(ref field, value);
+ }
+}
diff --git a/src/examples/Documentation/Pages/view-location/ExtendingIViewForExamples.cs b/src/examples/Documentation/Pages/view-location/ExtendingIViewForExamples.cs
new file mode 100644
index 0000000000..8b0a7cd7f8
--- /dev/null
+++ b/src/examples/Documentation/Pages/view-location/ExtendingIViewForExamples.cs
@@ -0,0 +1,60 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace ReactiveUI.Documentation.ViewLocation;
+
+///
+/// Shows extending onto a base class this app does not control:
+/// stands in for a base a third-party dialog library requires, and shows one built
+/// on it. , registered when the app starts, is what lets
+/// WhenActivated find out when a opens and closes.
+///
+public static class ExtendingIViewForExamples
+{
+ ///
+ /// Setting binds the dialog's controls; showing and
+ /// closing the dialog activates and deactivates them through , the
+ /// same way a platform's own activation fetcher drives its own base classes.
+ ///
+ public static void ConfirmABookReturnThroughAVendorDialog()
+ {
+ BookReturnViewModel viewModel = new("Clean Code");
+ BookReturnDialog dialog = new() { ViewModel = viewModel };
+
+ dialog.Show();
+ Console.WriteLine(dialog.TitleLabel.Text);
+
+ dialog.DamagedCheckBox.IsChecked = true;
+ Console.WriteLine(viewModel.IsDamaged);
+
+ dialog.Close();
+ dialog.DamagedCheckBox.IsChecked = false;
+ Console.WriteLine(viewModel.IsDamaged);
+
+ // Output:
+ // Clean Code
+ // True
+ // True
+ }
+
+ ///
+ /// finds for a
+ /// the same way it finds any other : implementing the interface is all a view needs,
+ /// whatever it derives from.
+ ///
+ public static void LocateTheVendorDialogLikeAnyOtherView()
+ {
+ BookReturnViewModel viewModel = new("Refactoring");
+
+ IViewFor? view = ViewLocator.GetCurrent().ResolveView(viewModel);
+
+ Console.WriteLine(view?.GetType().Name);
+ Console.WriteLine(ReferenceEquals(view?.ViewModel, viewModel));
+
+ // Output:
+ // BookReturnDialog
+ // True
+ }
+}
diff --git a/src/examples/Documentation/Pages/view-location/Program.cs b/src/examples/Documentation/Pages/view-location/Program.cs
index 194447f069..c62b398504 100644
--- a/src/examples/Documentation/Pages/view-location/Program.cs
+++ b/src/examples/Documentation/Pages/view-location/Program.cs
@@ -3,11 +3,14 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.
+using ReactiveUI;
using ReactiveUI.Builder;
using ReactiveUI.Documentation;
using ReactiveUI.Documentation.ViewLocation;
-ExampleApp.Start(static builder => _ = builder.WithViewModule());
+ExampleApp.Start(static builder => builder
+ .WithViewModule()
+ .WithRegistration(static resolver => resolver.RegisterConstant(new VendorDialogActivationFetcher())));
ViewLocationExamples.FindTheViewForAViewModel();
@@ -24,3 +27,7 @@
SchoolTimetableExamples.MapViewsWithABuilderIncludingFromTheServiceLocator();
SchoolTimetableExamples.BuildAViewLocatorNotFoundException();
+
+ExtendingIViewForExamples.ConfirmABookReturnThroughAVendorDialog();
+
+ExtendingIViewForExamples.LocateTheVendorDialogLikeAnyOtherView();
diff --git a/src/examples/Documentation/Pages/view-location/ReactiveVendorDialog{TViewModel}.cs b/src/examples/Documentation/Pages/view-location/ReactiveVendorDialog{TViewModel}.cs
new file mode 100644
index 0000000000..f31d345391
--- /dev/null
+++ b/src/examples/Documentation/Pages/view-location/ReactiveVendorDialog{TViewModel}.cs
@@ -0,0 +1,52 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.ComponentModel;
+
+namespace ReactiveUI.Documentation.ViewLocation;
+
+///
+/// Bridges , a base class from a library this app does not control, to ReactiveUI by
+/// implementing . This is the pattern for any base class an app must derive from
+/// that ReactiveUI does not already provide: a third-party dialog, popup or window base. Keeping
+/// and in step, in both directions, and raising
+/// for is what lets
+/// this.WhenAnyValue(x => x.ViewModel) and the binding methods work on a dialog built this way.
+///
+/// The type of view model the dialog displays.
+[System.Diagnostics.DebuggerDisplay("ReactiveVendorDialog ViewModel = {ViewModel}")]
+public class ReactiveVendorDialog : VendorDialog, IViewFor, INotifyPropertyChanged
+ where TViewModel : class
+{
+ /// Initializes a new instance of the class.
+ protected ReactiveVendorDialog() => DialogContextChanged += (_, _) => ViewModel = DialogContext as TViewModel;
+
+ ///
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ /// Gets or sets the view model the dialog displays.
+ public TViewModel? ViewModel
+ {
+ get;
+ set
+ {
+ if (ReferenceEquals(field, value))
+ {
+ return;
+ }
+
+ field = value;
+ DialogContext = value;
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ViewModel)));
+ }
+ }
+
+ ///
+ object? IViewFor.ViewModel
+ {
+ get => ViewModel;
+ set => ViewModel = (TViewModel?)value;
+ }
+}
diff --git a/src/examples/Documentation/Pages/view-location/VendorDialog.cs b/src/examples/Documentation/Pages/view-location/VendorDialog.cs
new file mode 100644
index 0000000000..ea5aa94f67
--- /dev/null
+++ b/src/examples/Documentation/Pages/view-location/VendorDialog.cs
@@ -0,0 +1,46 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace ReactiveUI.Documentation.ViewLocation;
+
+///
+/// A stand-in for a base class a third-party dialog library ships: an app must derive from it to show a dialog, and
+/// it comes with its own loosely typed context object, the way a host framework's DataContext or
+/// BindingContext works.
+///
+[System.Diagnostics.DebuggerDisplay("VendorDialog DialogContext = {DialogContext}")]
+public class VendorDialog
+{
+ /// Raised when changes.
+ public event EventHandler? DialogContextChanged;
+
+ /// Raised when the dialog is shown.
+ public event EventHandler? Opened;
+
+ /// Raised when the dialog is closed.
+ public event EventHandler? Closed;
+
+ /// Gets or sets the context object the dialog's controls bind against.
+ public object? DialogContext
+ {
+ get;
+ set
+ {
+ if (ReferenceEquals(field, value))
+ {
+ return;
+ }
+
+ field = value;
+ DialogContextChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+
+ /// Shows the dialog, raising .
+ public void Show() => Opened?.Invoke(this, EventArgs.Empty);
+
+ /// Closes the dialog, raising .
+ public void Close() => Closed?.Invoke(this, EventArgs.Empty);
+}
diff --git a/src/examples/Documentation/Pages/view-location/VendorDialogActivationFetcher.cs b/src/examples/Documentation/Pages/view-location/VendorDialogActivationFetcher.cs
new file mode 100644
index 0000000000..81305c1508
--- /dev/null
+++ b/src/examples/Documentation/Pages/view-location/VendorDialogActivationFetcher.cs
@@ -0,0 +1,40 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace ReactiveUI.Documentation.ViewLocation;
+
+///
+/// Plugs into ReactiveUI's activation pipeline, the way a platform package's own
+/// activation fetcher adapts its base classes: it turns the dialog's plain and
+/// events into the stream
+/// and its overloads rely on.
+/// Without this fetcher registered, WhenActivated on a never
+/// fires, because nothing tells it when the dialog opens or closes.
+///
+public sealed class VendorDialogActivationFetcher : IActivationForViewFetcher
+{
+ ///
+ public int GetAffinityForView(Type view) => typeof(VendorDialog).IsAssignableFrom(view) ? BindingAffinity.ExactType : 0;
+
+ ///
+ public IObservable GetActivationForView(IActivatableView view)
+ {
+ VendorDialog dialog = (VendorDialog)view;
+ return Signal.Create(witness =>
+ {
+ EventHandler onOpened = (_, _) => witness.OnNext(true);
+ EventHandler onClosed = (_, _) => witness.OnNext(false);
+
+ dialog.Opened += onOpened;
+ dialog.Closed += onClosed;
+
+ return new ActionDisposable(() =>
+ {
+ dialog.Opened -= onOpened;
+ dialog.Closed -= onClosed;
+ });
+ });
+ }
+}
From 18759b8e48477c1fd71b27c3aa0518911096552a Mon Sep 17 00:00:00 2001
From: Glenn Watson <5834289+glennawatson@users.noreply.github.com>
Date: Sun, 27 Sep 2026 16:35:26 +1000
Subject: [PATCH 03/10] docs(examples): cover WPF and Blend API gaps; smoke run
waits for the first transition
---
.../Pages/platform-blend-drawing/SmokeTest.cs | 7 +++
.../Pages/platform-wpf/App.xaml.cs | 1 +
.../Pages/platform-wpf/AppShell.cs | 3 ++
.../Pages/platform-wpf/MainWindow.xaml | 5 ++
.../Pages/platform-wpf/MainWindow.xaml.cs | 53 +++++++++++++++++--
.../Pages/platform-wpf/SmokeTest.cs | 9 ++++
.../ViewModels/OfficeHoursBannerViewModel.cs | 17 ++++++
.../platform-wpf/Views/CourseListView.xaml | 2 +-
.../platform-wpf/Views/CourseListView.xaml.cs | 8 +++
.../Views/LoggingViewModelViewHost.cs | 22 ++++++++
.../Views/NoticeBoardStatusIndicator.cs | 15 ++++++
.../Views/OfficeHoursBannerView.cs | 31 +++++++++++
.../WpfBuilderExtensionsExamples.cs | 28 ++++++++++
13 files changed, 196 insertions(+), 5 deletions(-)
create mode 100644 src/examples/Documentation/Pages/platform-wpf/ViewModels/OfficeHoursBannerViewModel.cs
create mode 100644 src/examples/Documentation/Pages/platform-wpf/Views/LoggingViewModelViewHost.cs
create mode 100644 src/examples/Documentation/Pages/platform-wpf/Views/NoticeBoardStatusIndicator.cs
create mode 100644 src/examples/Documentation/Pages/platform-wpf/Views/OfficeHoursBannerView.cs
diff --git a/src/examples/Documentation/Pages/platform-blend-drawing/SmokeTest.cs b/src/examples/Documentation/Pages/platform-blend-drawing/SmokeTest.cs
index e423966f16..1bc3488ec3 100644
--- a/src/examples/Documentation/Pages/platform-blend-drawing/SmokeTest.cs
+++ b/src/examples/Documentation/Pages/platform-blend-drawing/SmokeTest.cs
@@ -52,7 +52,14 @@ public static void Run()
private static void ShowSchedulerOverrideForTesting()
{
Border probeElement = new();
+ Border targetElement = new();
FollowObservableStateBehavior stateBehavior = new() { SchedulerOverride = Sequencer.Immediate };
+
+ // A test that builds the behavior in code, rather than XAML, sets TargetObject through its dependency
+ // property field with SetValue; the window's XAML sets the same property with a binding instead.
+ stateBehavior.SetValue(FollowObservableStateBehavior.TargetObjectProperty, targetElement);
+ Console.WriteLine($"TargetObject via SetValue: {stateBehavior.GetValue(FollowObservableStateBehavior.TargetObjectProperty) == targetElement}");
+
stateBehavior.Attach(probeElement);
stateBehavior.StateObservable = Signal.Emit("Stormy");
Console.WriteLine("FollowObservableStateBehavior.SchedulerOverride delivered without a dispatcher pump.");
diff --git a/src/examples/Documentation/Pages/platform-wpf/App.xaml.cs b/src/examples/Documentation/Pages/platform-wpf/App.xaml.cs
index 230b7d55ec..adbc4a09bc 100644
--- a/src/examples/Documentation/Pages/platform-wpf/App.xaml.cs
+++ b/src/examples/Documentation/Pages/platform-wpf/App.xaml.cs
@@ -37,6 +37,7 @@ protected override void OnStartup(StartupEventArgs e)
WpfBuilderExtensionsExamples.ShowTheAppBuilderOverload();
WpfBuilderExtensionsExamples.ShowTheIndividualExtensions();
WpfBuilderExtensionsExamples.AddTheUnsafeTemplateHook();
+ WpfBuilderExtensionsExamples.ShowTheDefaultItemTemplate();
AutoSuspendHelper autoSuspendHelper = new(this) { IdleTimeout = IdleTimeout };
Console.WriteLine($"Auto-suspend idle timeout: {autoSuspendHelper.IdleTimeout}");
diff --git a/src/examples/Documentation/Pages/platform-wpf/AppShell.cs b/src/examples/Documentation/Pages/platform-wpf/AppShell.cs
index a103d03734..7e06ac83c3 100644
--- a/src/examples/Documentation/Pages/platform-wpf/AppShell.cs
+++ b/src/examples/Documentation/Pages/platform-wpf/AppShell.cs
@@ -11,4 +11,7 @@ public sealed class AppShell : ReactiveObject, IScreen
{
///
public RoutingState Router { get; } = new();
+
+ /// Gets how long the window's page-name banner takes to fade between pages.
+ public TimeSpan PageBannerDuration => TimeSpan.FromMilliseconds(200);
}
diff --git a/src/examples/Documentation/Pages/platform-wpf/MainWindow.xaml b/src/examples/Documentation/Pages/platform-wpf/MainWindow.xaml
index 6c51e71a11..6b2df1f2f4 100644
--- a/src/examples/Documentation/Pages/platform-wpf/MainWindow.xaml
+++ b/src/examples/Documentation/Pages/platform-wpf/MainWindow.xaml
@@ -12,9 +12,14 @@
+
+
+
+
+
diff --git a/src/examples/Documentation/Pages/platform-wpf/MainWindow.xaml.cs b/src/examples/Documentation/Pages/platform-wpf/MainWindow.xaml.cs
index d99cdcb46a..2c96acffa6 100644
--- a/src/examples/Documentation/Pages/platform-wpf/MainWindow.xaml.cs
+++ b/src/examples/Documentation/Pages/platform-wpf/MainWindow.xaml.cs
@@ -10,8 +10,10 @@ namespace ReactiveUI.Documentation.PlatformWpf;
///
/// The app's only window. It hosts a that slides between the course list and a
-/// student's grade page as the router navigates, and a notice board that shows the school office's notices through
-/// and .
+/// student's grade page as the router navigates, a notice board that shows the school office's notices through
+/// and , and a page-name banner and a
+/// handful of status labels that show every other transition and every
+/// WhenActivated overload the ones above don't already cover.
///
[DebuggerDisplay("MainWindow")]
public partial class MainWindow : ReactiveWindow
@@ -30,6 +32,7 @@ public MainWindow()
ViewModel = new AppShell();
IReadOnlyList courses = GradeBookStore.CreateSeeded();
+ CourseListViewModel courseList = new(ViewModel, courses);
AboutButton.Click += (_, _) => ShowAboutPage(courses);
Host.Transition = TransitioningContentControl.TransitionType.Slide;
@@ -39,21 +42,63 @@ public MainWindow()
Host.TransitionStarted += static (_, _) => Console.WriteLine("Transition started.");
Host.TransitionCompleted += static (_, _) => Console.WriteLine("Transition completed.");
+ // Force a fixed layout contract instead of the default one RoutedViewHost builds from the window's
+ // orientation, the way an app that never changes layout with orientation would.
+ Host.ViewContractObservable = Signal.Emit("desktop");
+ Console.WriteLine($"Host view contract observable set: {Host.GetValue(RoutedViewHost.ViewContractObservableProperty) is not null}");
+
+ // PageNameBanner is built entirely in code, the way a control assembled outside XAML would be: its
+ // direction and duration are set through the raw dependency properties, with SetValue and SetBinding,
+ // rather than the Direction and Duration wrapper properties Host uses above.
+ PageNameBanner.Transition = TransitioningContentControl.TransitionType.Bounce;
+ PageNameBanner.SetValue(TransitioningContentControl.TransitionDirectionProperty, TransitioningContentControl.TransitionDirection.Down);
+ _ = PageNameBanner.SetBinding(
+ TransitioningContentControl.TransitionDurationProperty,
+ new System.Windows.Data.Binding(nameof(AppShell.PageBannerDuration)) { Source = ViewModel });
+
+ // NoticeBoardHost and LatestNoticeHost each pick a transition style and direction Host and SummaryHost
+ // don't already use, so the project shows every combination the type supports.
+ NoticeBoardHost.Transition = TransitioningContentControl.TransitionType.Drop;
+ NoticeBoardHost.Direction = TransitioningContentControl.TransitionDirection.Right;
+ LatestNoticeHost.Transition = TransitioningContentControl.TransitionType.Fade;
+
// OfficeNoticeView is registered only with the service locator, so the notice board uses the Unsafe twins.
NoticeBoard noticeBoard = new();
NoticeBoardHost.Router = noticeBoard.Router;
LatestNoticeHost.ViewModel = new OfficeNoticeViewModel(noticeBoard, "Reports are due on Friday.");
+ // NoticeStatusText, CommandStatusText and PageSummaryText are plain labels, not IViewFor instances of
+ // their own, so each ties its activation to this window through an explicit-view WhenActivated overload.
+ _ = NoticeStatusText.WhenActivated(
+ d => d(noticeBoard.Router.CurrentViewModel.Subscribe(page =>
+ NoticeStatusText.Text = $"Notice board page: {page?.UrlPathSegment ?? "(none)"}")),
+ this);
+
+ _ = CommandStatusText.WhenActivated(
+ d => d.Add(courseList.OpenStudent.IsExecuting.Subscribe(executing =>
+ CommandStatusText.Text = executing ? "Opening student..." : "Idle")),
+ this);
+
+ _ = PageSummaryText.WhenActivated(
+ () =>
+ [
+ ViewModel!.Router.CurrentViewModel.CombineLatest(
+ noticeBoard.Router.CurrentViewModel,
+ static (course, notice) => $"{course?.UrlPathSegment ?? "start"} / {notice?.UrlPathSegment ?? "none"}")
+ .Subscribe(text => PageSummaryText.Text = text)
+ ],
+ this);
+
// The "d(...)" style registers one disposable at a time, rather than collecting them into a
// MultipleDisposable first; RoutedViewHost's own constructor uses the same style internally.
_ = this.WhenActivated(d =>
{
Host.Router = ViewModel!.Router;
Host.ViewLocator = ViewLocator.GetCurrent();
- d(ViewModel.Router.Navigate.Execute(new CourseListViewModel(ViewModel, courses))
- .Subscribe());
+ d(ViewModel.Router.Navigate.Execute(courseList).Subscribe());
d(noticeBoard.Router.Navigate.Execute(new OfficeNoticeViewModel(noticeBoard, "Parent evening is on Tuesday."))
.Subscribe());
+ d(ViewModel.Router.CurrentViewModel.Subscribe(page => PageNameBanner.Content = page?.UrlPathSegment ?? "(none)"));
Console.WriteLine($"View contract: {Host.ViewContract ?? "(none)"}");
// BindingRoot is the same view model as ViewModel, exposed under the name every ReactiveUI view uses.
diff --git a/src/examples/Documentation/Pages/platform-wpf/SmokeTest.cs b/src/examples/Documentation/Pages/platform-wpf/SmokeTest.cs
index bc598cbf91..f7512df9c4 100644
--- a/src/examples/Documentation/Pages/platform-wpf/SmokeTest.cs
+++ b/src/examples/Documentation/Pages/platform-wpf/SmokeTest.cs
@@ -17,7 +17,12 @@ public static class SmokeTest
public static void Run()
{
MainWindow window = new();
+
+ // Wait, as a person watching the screen would, until the first page has finished sliding in.
+ DispatcherFrame firstPageShown = new();
+ window.Host.TransitionCompleted += (_, _) => firstPageShown.Continue = false;
window.Show();
+ Dispatcher.PushFrame(firstPageShown);
PumpDispatcher();
AppShell shell = window.ViewModel!;
@@ -26,6 +31,10 @@ public static void Run()
Console.WriteLine($"Students listed: {courseList.Students.Count}");
Console.WriteLine($"RoutedViewHostUnsafe shows: {DescribeNotice(window.NoticeBoardHost.Content)}");
Console.WriteLine($"ViewModelViewHostUnsafe shows: {DescribeNotice(window.LatestNoticeHost.Content)}");
+ Console.WriteLine($"Page name banner: {window.PageNameBanner.Content}");
+ Console.WriteLine($"Notice status: {window.NoticeStatusText.Text}");
+ Console.WriteLine($"Command status: {window.CommandStatusText.Text}");
+ Console.WriteLine($"Page summary: {window.PageSummaryText.Text}");
Student student = courseList.Students[0];
courseList.SelectedStudent = student;
diff --git a/src/examples/Documentation/Pages/platform-wpf/ViewModels/OfficeHoursBannerViewModel.cs b/src/examples/Documentation/Pages/platform-wpf/ViewModels/OfficeHoursBannerViewModel.cs
new file mode 100644
index 0000000000..4cb1f5a228
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-wpf/ViewModels/OfficeHoursBannerViewModel.cs
@@ -0,0 +1,17 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace ReactiveUI.Documentation.PlatformWpf;
+
+/// A window-wide reminder with no bindable state; only its activation lifecycle matters.
+[System.Diagnostics.DebuggerDisplay("OfficeHoursBannerViewModel")]
+public sealed class OfficeHoursBannerViewModel : IActivatableViewModel, IDisposable
+{
+ ///
+ public ViewModelActivator Activator { get; } = new();
+
+ ///
+ public void Dispose() => Activator.Dispose();
+}
diff --git a/src/examples/Documentation/Pages/platform-wpf/Views/CourseListView.xaml b/src/examples/Documentation/Pages/platform-wpf/Views/CourseListView.xaml
index 68a2ae5ae2..2b3c8bf35f 100644
--- a/src/examples/Documentation/Pages/platform-wpf/Views/CourseListView.xaml
+++ b/src/examples/Documentation/Pages/platform-wpf/Views/CourseListView.xaml
@@ -11,7 +11,7 @@
-
+
diff --git a/src/examples/Documentation/Pages/platform-wpf/Views/CourseListView.xaml.cs b/src/examples/Documentation/Pages/platform-wpf/Views/CourseListView.xaml.cs
index cf38ca0717..cc0c2df360 100644
--- a/src/examples/Documentation/Pages/platform-wpf/Views/CourseListView.xaml.cs
+++ b/src/examples/Documentation/Pages/platform-wpf/Views/CourseListView.xaml.cs
@@ -23,6 +23,14 @@ public CourseListView()
SummaryHost.Transition = TransitioningContentControl.TransitionType.Move;
SummaryHost.Direction = TransitioningContentControl.TransitionDirection.Up;
+ // ContractFallbackByPass stops the host from falling back to an uncontracted view when a contracted one
+ // is missing; here it's harmless, since the summary panel only ever asks for the default contract.
+ SummaryHost.ContractFallbackByPass = true;
+ Console.WriteLine($"Contract fallback bypass (via field): {(bool)SummaryHost.GetValue(ViewModelViewHost.ContractFallbackByPassProperty)}");
+
+ SummaryHost.ViewContractObservable = Signal.Emit(null);
+ Console.WriteLine($"Summary host contract observable set: {SummaryHost.GetValue(ViewModelViewHost.ViewContractObservableProperty) is not null}");
+
_ = this.WhenActivated(d =>
{
SummaryHost.ViewLocator = ViewLocator.GetCurrent();
diff --git a/src/examples/Documentation/Pages/platform-wpf/Views/LoggingViewModelViewHost.cs b/src/examples/Documentation/Pages/platform-wpf/Views/LoggingViewModelViewHost.cs
new file mode 100644
index 0000000000..73656f3b3d
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-wpf/Views/LoggingViewModelViewHost.cs
@@ -0,0 +1,22 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace ReactiveUI.Documentation.PlatformWpf;
+
+///
+/// A that logs every view it resolves, by overriding
+/// . uses it
+/// for its student summary panel.
+///
+[System.Diagnostics.DebuggerDisplay("LoggingViewModelViewHost")]
+public sealed class LoggingViewModelViewHost : ViewModelViewHost
+{
+ ///
+ protected override void ResolveViewForViewModel(object? viewModel, string? contract)
+ {
+ base.ResolveViewForViewModel(viewModel, contract);
+ Console.WriteLine($"Summary panel resolved: {(viewModel is null ? "(none)" : Content?.GetType().Name)}");
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-wpf/Views/NoticeBoardStatusIndicator.cs b/src/examples/Documentation/Pages/platform-wpf/Views/NoticeBoardStatusIndicator.cs
new file mode 100644
index 0000000000..2cad966da1
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-wpf/Views/NoticeBoardStatusIndicator.cs
@@ -0,0 +1,15 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Windows.Controls;
+
+namespace ReactiveUI.Documentation.PlatformWpf;
+
+///
+/// A plain status label. It is not itself an , so ties its
+/// activation lifecycle to its own through the WhenActivated overloads that take an explicit view.
+///
+[System.Diagnostics.DebuggerDisplay("NoticeBoardStatusIndicator")]
+public sealed class NoticeBoardStatusIndicator : TextBlock, IActivatableView;
diff --git a/src/examples/Documentation/Pages/platform-wpf/Views/OfficeHoursBannerView.cs b/src/examples/Documentation/Pages/platform-wpf/Views/OfficeHoursBannerView.cs
new file mode 100644
index 0000000000..fdaef28333
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-wpf/Views/OfficeHoursBannerView.cs
@@ -0,0 +1,31 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Diagnostics;
+using System.Windows.Controls;
+
+namespace ReactiveUI.Documentation.PlatformWpf;
+
+///
+/// A fixed reminder banner with nothing to bind. It uses the parameterless WhenActivated overload purely to
+/// trigger 's activation lifecycle, which logs when the banner comes on
+/// screen, without the empty WhenActivated(_ => { }) boilerplate a view with real bindings would use.
+///
+[DebuggerDisplay("OfficeHoursBannerView")]
+public sealed class OfficeHoursBannerView : ReactiveUserControl
+{
+ /// The label the banner shows.
+ private readonly TextBlock _text = new() { Text = "Office hours: 9am-4pm, Monday to Friday." };
+
+ /// Initializes a new instance of the class.
+ public OfficeHoursBannerView()
+ {
+ Content = _text;
+ ViewModel = new OfficeHoursBannerViewModel();
+ ViewModel.Activator.Activated.Subscribe(static _ => Console.WriteLine("Office hours banner activated."));
+
+ _ = this.WhenActivated();
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-wpf/WpfBuilderExtensionsExamples.cs b/src/examples/Documentation/Pages/platform-wpf/WpfBuilderExtensionsExamples.cs
index 76c626ce28..e35d35293f 100644
--- a/src/examples/Documentation/Pages/platform-wpf/WpfBuilderExtensionsExamples.cs
+++ b/src/examples/Documentation/Pages/platform-wpf/WpfBuilderExtensionsExamples.cs
@@ -3,6 +3,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.
+using System.Windows;
using ReactiveUI.Builder;
using Splat;
using Splat.Builder;
@@ -28,6 +29,9 @@ public static void ShowTheAppBuilderOverload()
ReactiveUIBuilder builder = new(resolver, resolver);
IReactiveUIBuilder configured = ((IAppBuilder)builder).WithWpf();
Console.WriteLine($"IAppBuilder.WithWpf() configured: {configured is not null}");
+
+ // Output:
+ // IAppBuilder.WithWpf() configured: True
}
///
@@ -44,6 +48,10 @@ public static void ShowTheIndividualExtensions()
Console.WriteLine($"WithWpfConverters/WithWpfScheduler configured: {configured is not null}");
Console.WriteLine($"WPF main-thread scheduler: {WpfMainThreadScheduler.GetType().Name}");
+
+ // Output:
+ // WithWpfConverters/WithWpfScheduler configured: True
+ // WPF main-thread scheduler: DispatcherSequencer
}
///
@@ -67,4 +75,24 @@ public static void AddTheUnsafeTemplateHook()
// Binding hook: AutoDataTemplateBindingHook
// Binding hook: AutoDataTemplateBindingHookUnsafe
}
+
+ ///
+ /// is the template WithWpf 's hook assigns to
+ /// an ItemsControl with no template of its own; an app can inspect or reuse it directly.
+ /// is the same idea for the Unsafe twin.
+ ///
+ public static void ShowTheDefaultItemTemplate()
+ {
+ DataTemplate template = AutoDataTemplateBindingHook.DefaultItemTemplate.Value;
+ DependencyObject root = template.LoadContent();
+ Console.WriteLine($"Default item template hosts each item in a: {root.GetType().Name}");
+
+ DataTemplate unsafeTemplate = AutoDataTemplateBindingHookUnsafe.DefaultItemTemplate.Value;
+ DependencyObject unsafeRoot = unsafeTemplate.LoadContent();
+ Console.WriteLine($"Unsafe default item template hosts each item in a: {unsafeRoot.GetType().Name}");
+
+ // Output:
+ // Default item template hosts each item in a: ViewModelViewHost
+ // Unsafe default item template hosts each item in a: ViewModelViewHostUnsafe
+ }
}
From ff645e9c9f43a2f8f837a94ebf09c1a89fa322af Mon Sep 17 00:00:00 2001
From: Glenn Watson <5834289+glennawatson@users.noreply.github.com>
Date: Sun, 27 Sep 2026 16:56:18 +1000
Subject: [PATCH 04/10] build(examples): turn SourceLink off for the example
projects
---
src/Directory.Build.props | 2 +-
src/examples/Directory.Build.props | 8 ++++++++
2 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index f35492fb05..75ac8a0949 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -206,7 +206,7 @@
-
+
diff --git a/src/examples/Directory.Build.props b/src/examples/Directory.Build.props
index 25d87170fd..82d2a58426 100644
--- a/src/examples/Directory.Build.props
+++ b/src/examples/Directory.Build.props
@@ -6,6 +6,12 @@
the root defaults the value and derives $(EnablePublicApiBaseline) and the analyzer
reference from it, so setting it afterwards leaves tracking half on. -->
false
+
+
+ false
+ false
@@ -14,5 +20,7 @@
false
14.0
+ false
+ false
From 24c9429b0c96eef4c8cfa020a1db1520c34528f8 Mon Sep 17 00:00:00 2001
From: Glenn Watson <5834289+glennawatson@users.noreply.github.com>
Date: Sun, 27 Sep 2026 17:14:15 +1000
Subject: [PATCH 05/10] docs(examples): add the Apple (iOS and macOS) page
project with controllers, views, hosts and suspension
---
.../LinuxPlaceholder/Program.cs | 7 ++
.../Pages/platform-apple/Shared/Book.cs | 40 +++++++++
.../platform-apple/Shared/LibraryAppState.cs | 15 ++++
.../Shared/LibraryAppStateJsonContext.cs | 15 ++++
.../Pages/platform-apple/Shared/Member.cs | 12 +++
.../Shared/ViewModels/BookCatalogViewModel.cs | 55 ++++++++++++
.../ViewModels/LibraryShellViewModel.cs | 14 +++
.../Shared/ViewModels/LoanViewModel.cs | 66 ++++++++++++++
.../Shared/ViewModels/MembersViewModel.cs | 34 +++++++
.../Pages/platform-apple/iOS/AppDelegate.cs | 84 +++++++++++++++++
.../iOS/BookCoverPagerViewController.cs | 76 ++++++++++++++++
.../iOS/BookDetailViewController.cs | 87 ++++++++++++++++++
.../iOS/BookListViewController.cs | 70 +++++++++++++++
.../platform-apple/iOS/LibraryComposition.cs | 43 +++++++++
.../iOS/LibrarySplitViewController.cs | 52 +++++++++++
.../iOS/LibraryTabBarController.cs | 51 +++++++++++
.../platform-apple/iOS/LoanViewController.cs | 89 +++++++++++++++++++
.../iOS/MembersNavigationController.cs | 17 ++++
.../iOS/MembersPlaceholderViewController.cs | 46 ++++++++++
.../Pages/platform-apple/iOS/Program.cs | 17 ++++
.../iOS/Views/BookCoverImageView.cs | 26 ++++++
.../platform-apple/iOS/Views/BookRowView.cs | 72 +++++++++++++++
.../iOS/Views/StarRatingControl.cs | 73 +++++++++++++++
.../Pages/platform-apple/macOS/AppDelegate.cs | 79 ++++++++++++++++
.../macOS/BookDetailViewController.cs | 78 ++++++++++++++++
.../macOS/BookListViewController.cs | 69 ++++++++++++++
.../macOS/LibrarySplitViewController.cs | 52 +++++++++++
.../macOS/MainWindowController.cs | 58 ++++++++++++
.../macOS/MembersPlaceholderViewController.cs | 44 +++++++++
.../Pages/platform-apple/macOS/Program.cs | 25 ++++++
.../macOS/Views/BookCoverImageView.cs | 25 ++++++
.../platform-apple/macOS/Views/BookRowView.cs | 74 +++++++++++++++
.../macOS/Views/StarRatingControl.cs | 73 +++++++++++++++
.../platform-apple/platform-apple.csproj | 61 +++++++++++++
src/reactiveui.slnx | 1 +
35 files changed, 1700 insertions(+)
create mode 100644 src/examples/Documentation/Pages/platform-apple/LinuxPlaceholder/Program.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/Shared/Book.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/Shared/LibraryAppState.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/Shared/LibraryAppStateJsonContext.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/Shared/Member.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/BookCatalogViewModel.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/LibraryShellViewModel.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/LoanViewModel.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/MembersViewModel.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/iOS/AppDelegate.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/iOS/BookCoverPagerViewController.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/iOS/BookDetailViewController.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/iOS/BookListViewController.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/iOS/LibraryComposition.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/iOS/LibrarySplitViewController.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/iOS/LibraryTabBarController.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/iOS/LoanViewController.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/iOS/MembersNavigationController.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/iOS/MembersPlaceholderViewController.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/iOS/Program.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/iOS/Views/BookCoverImageView.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/iOS/Views/BookRowView.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/iOS/Views/StarRatingControl.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/macOS/AppDelegate.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/macOS/BookDetailViewController.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/macOS/BookListViewController.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/macOS/LibrarySplitViewController.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/macOS/MainWindowController.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/macOS/MembersPlaceholderViewController.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/macOS/Program.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/macOS/Views/BookCoverImageView.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/macOS/Views/BookRowView.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/macOS/Views/StarRatingControl.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/platform-apple.csproj
diff --git a/src/examples/Documentation/Pages/platform-apple/LinuxPlaceholder/Program.cs b/src/examples/Documentation/Pages/platform-apple/LinuxPlaceholder/Program.cs
new file mode 100644
index 0000000000..ccb5263961
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/LinuxPlaceholder/Program.cs
@@ -0,0 +1,7 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+// The iOS and macOS workloads exist only on Windows and macOS. Build this project there to compile the examples.
+Console.WriteLine("The Apple examples build on Windows and macOS.");
diff --git a/src/examples/Documentation/Pages/platform-apple/Shared/Book.cs b/src/examples/Documentation/Pages/platform-apple/Shared/Book.cs
new file mode 100644
index 0000000000..36e41a7e41
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/Shared/Book.cs
@@ -0,0 +1,40 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// A book the library owns. Bound labels and controls refresh whenever or changes.
+[System.Diagnostics.DebuggerDisplay("Book {Title} IsOnLoan = {IsOnLoan}")]
+public sealed class Book : ReactiveObject
+{
+ /// Initializes a new instance of the class.
+ /// The book's title.
+ /// The book's author.
+ public Book(string title, string author)
+ {
+ Title = title;
+ Author = author;
+ }
+
+ /// Gets the book's title.
+ public string Title { get; }
+
+ /// Gets the book's author.
+ public string Author { get; }
+
+ /// Gets or sets a value indicating whether a member currently has the book on loan.
+ public bool IsOnLoan
+ {
+ get;
+ set => this.RaiseAndSetIfChanged(ref field, value);
+ }
+
+ /// Gets or sets the book's star rating, from 0 to 5.
+ public int Rating
+ {
+ get;
+ set => this.RaiseAndSetIfChanged(ref field, value);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/Shared/LibraryAppState.cs b/src/examples/Documentation/Pages/platform-apple/Shared/LibraryAppState.cs
new file mode 100644
index 0000000000..49d6f1d1f2
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/Shared/LibraryAppState.cs
@@ -0,0 +1,15 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The application state saves under Application Support and
+/// restores from it, so a relaunched app picks up where the last one left off.
+[System.Diagnostics.DebuggerDisplay("{LastViewedBook}")]
+public sealed class LibraryAppState
+{
+ /// Gets or sets the title of the book the member last looked at.
+ public string LastViewedBook { get; set; } = string.Empty;
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/Shared/LibraryAppStateJsonContext.cs b/src/examples/Documentation/Pages/platform-apple/Shared/LibraryAppStateJsonContext.cs
new file mode 100644
index 0000000000..d7d2f88171
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/Shared/LibraryAppStateJsonContext.cs
@@ -0,0 +1,15 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Text.Json.Serialization;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// Source-generated serialization metadata for , so
+/// 's trim- and AOT-safe LoadState{T}(JsonTypeInfo{T}) and
+/// SaveState{T}(T, JsonTypeInfo{T}) overloads never need reflection-based serialization.
+[JsonSerializable(typeof(LibraryAppState))]
+[System.Diagnostics.DebuggerDisplay("LibraryAppStateJsonContext")]
+public sealed partial class LibraryAppStateJsonContext : JsonSerializerContext;
diff --git a/src/examples/Documentation/Pages/platform-apple/Shared/Member.cs b/src/examples/Documentation/Pages/platform-apple/Shared/Member.cs
new file mode 100644
index 0000000000..100d4f1544
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/Shared/Member.cs
@@ -0,0 +1,12 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// A library member who can borrow books.
+/// The member's card number.
+/// The member's name.
+[System.Diagnostics.DebuggerDisplay("Member {Name} ({Id})")]
+public sealed record Member(string Id, string Name);
diff --git a/src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/BookCatalogViewModel.cs b/src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/BookCatalogViewModel.cs
new file mode 100644
index 0000000000..be945599c5
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/BookCatalogViewModel.cs
@@ -0,0 +1,55 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The catalog page: every book the library owns, plus the members who can borrow one.
+[System.Diagnostics.DebuggerDisplay("BookCatalogViewModel Books = {Books.Count}")]
+public sealed class BookCatalogViewModel : ReactiveObject, IRoutableViewModel, IDisposable
+{
+ /// The members who can borrow a book, carried forward to the loan page.
+ private readonly IReadOnlyList _members;
+
+ /// Initializes a new instance of the class.
+ /// The shell that owns the router.
+ /// The library's catalog.
+ /// The members who can borrow a book.
+ public BookCatalogViewModel(IScreen hostScreen, IReadOnlyList books, IReadOnlyList members)
+ {
+ HostScreen = hostScreen;
+ Books = books;
+ _members = members;
+
+ IObservable canLoan = this.WhenAnyValue(
+ static x => x.SelectedBook,
+ static book => book is not null && !book.IsOnLoan);
+
+ OpenLoan = ReactiveCommand.CreateFromObservable(
+ () => HostScreen.Router.Navigate.Execute(new LoanViewModel(HostScreen, SelectedBook!, _members)),
+ canLoan);
+ }
+
+ ///
+ public string UrlPathSegment => "catalog";
+
+ ///
+ public IScreen HostScreen { get; }
+
+ /// Gets the library's catalog.
+ public IReadOnlyList Books { get; }
+
+ /// Gets or sets the book the member picked from the catalog.
+ public Book? SelectedBook
+ {
+ get;
+ set => this.RaiseAndSetIfChanged(ref field, value);
+ }
+
+ /// Gets the command that opens the loan page for .
+ public ReactiveCommand OpenLoan { get; }
+
+ ///
+ public void Dispose() => OpenLoan.Dispose();
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/LibraryShellViewModel.cs b/src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/LibraryShellViewModel.cs
new file mode 100644
index 0000000000..e0099b060e
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/LibraryShellViewModel.cs
@@ -0,0 +1,14 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The library app's shell. It owns the router that RoutedViewHost follows on iOS.
+[System.Diagnostics.DebuggerDisplay("Pages = {Router.NavigationStack.Count}")]
+public sealed class LibraryShellViewModel : ReactiveObject, IScreen
+{
+ ///
+ public RoutingState Router { get; } = new();
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/LoanViewModel.cs b/src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/LoanViewModel.cs
new file mode 100644
index 0000000000..9244cc5c5e
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/LoanViewModel.cs
@@ -0,0 +1,66 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The loan page: pick a member for , then confirm or cancel.
+[System.Diagnostics.DebuggerDisplay("LoanViewModel Book = {Book.Title}")]
+public sealed class LoanViewModel : ReactiveObject, IRoutableViewModel, IDisposable
+{
+ /// Initializes a new instance of the class.
+ /// The shell that owns the router.
+ /// The book being loaned.
+ /// The members who can borrow the book.
+ public LoanViewModel(IScreen hostScreen, Book book, IReadOnlyList members)
+ {
+ HostScreen = hostScreen;
+ Book = book;
+ Members = members;
+
+ IObservable canConfirm = this.WhenAnyValue(static x => x.SelectedMember).Select(static member => member is not null);
+
+ ConfirmLoan = ReactiveCommand.CreateFromObservable(
+ () =>
+ {
+ Book.IsOnLoan = true;
+ return HostScreen.Router.NavigateBack.Execute();
+ },
+ canConfirm);
+
+ Cancel = ReactiveCommand.CreateFromObservable(() => HostScreen.Router.NavigateBack.Execute());
+ }
+
+ ///
+ public string UrlPathSegment => "loan";
+
+ ///
+ public IScreen HostScreen { get; }
+
+ /// Gets the book being loaned.
+ public Book Book { get; }
+
+ /// Gets the members who can borrow the book. The view shows a row per member.
+ public IReadOnlyList Members { get; }
+
+ /// Gets or sets the member the librarian picked from .
+ public Member? SelectedMember
+ {
+ get;
+ set => this.RaiseAndSetIfChanged(ref field, value);
+ }
+
+ /// Gets the command that marks the book on loan and returns to the catalog.
+ public ReactiveCommand ConfirmLoan { get; }
+
+ /// Gets the command that returns to the catalog without loaning the book.
+ public ReactiveCommand Cancel { get; }
+
+ ///
+ public void Dispose()
+ {
+ ConfirmLoan.Dispose();
+ Cancel.Dispose();
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/MembersViewModel.cs b/src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/MembersViewModel.cs
new file mode 100644
index 0000000000..5d7b9468c5
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/MembersViewModel.cs
@@ -0,0 +1,34 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The members tab: every library member. The second agent's table/collection-source example lists them; this view model only carries the data.
+[System.Diagnostics.DebuggerDisplay("MembersViewModel Members = {Members.Count}")]
+public sealed class MembersViewModel : ReactiveObject, IRoutableViewModel
+{
+ /// Initializes a new instance of the class.
+ /// The shell that owns the router.
+ /// Every library member.
+ public MembersViewModel(IScreen hostScreen, IReadOnlyList members)
+ {
+ HostScreen = hostScreen;
+ Members = members;
+ MemberCount = members.Count;
+ }
+
+ ///
+ public string UrlPathSegment => "members";
+
+ ///
+ public IScreen HostScreen { get; }
+
+ /// Gets every library member.
+ public IReadOnlyList Members { get; }
+
+ /// Gets how many members the library has. A plain so a one-way binding can observe
+ /// it directly, rather than chaining through the non-reactive list.
+ public int MemberCount { get; }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/AppDelegate.cs b/src/examples/Documentation/Pages/platform-apple/iOS/AppDelegate.cs
new file mode 100644
index 0000000000..3307657ea5
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/AppDelegate.cs
@@ -0,0 +1,84 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using Foundation;
+using ReactiveUI.Builder;
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The app's UIApplicationDelegate: builds ReactiveUI, wires up state suspension, and shows the shell.
+[Register(nameof(AppDelegate))]
+[System.Diagnostics.DebuggerDisplay("AppDelegate")]
+public sealed class AppDelegate : UIApplicationDelegate
+{
+ /// Keeps UIKit lifecycle callbacks translated into suspend/resume signals for as long as the process lives.
+ private AutoSuspendHelper? _autoSuspendHelper;
+
+ ///
+ public override UIWindow? Window { get; set; }
+
+ ///
+ public override bool FinishedLaunching(UIApplication application, NSDictionary? launchOptions)
+ {
+ ReactiveUIBuilder builder = RxAppBuilder.CreateReactiveUIBuilder();
+ _ = builder.WithPlatformModule().BuildApp();
+
+ _autoSuspendHelper = new AutoSuspendHelper(this);
+ _autoSuspendHelper.FinishedLaunching(application, launchOptions!);
+ Console.WriteLine($"FinishedLaunching captured {_autoSuspendHelper.LaunchOptions?.Count ?? 0} launch option(s).");
+
+ RxSuspension.SuspensionHost.CreateNewAppState = static () => new LibraryAppState();
+
+ AppSupportJsonSuspensionDriver driver = new("Library");
+ RxSuspension.SuspensionHost.SetupDefaultSuspendResume(driver);
+
+ _ = driver.SaveState(new LibraryAppState { LastViewedBook = "Pride and Prejudice" }, LibraryAppStateJsonContext.Default.LibraryAppState)
+ .Subscribe(static _ => Console.WriteLine("AppSupportJsonSuspensionDriver saved the app state."));
+ _ = driver.LoadState(LibraryAppStateJsonContext.Default.LibraryAppState)
+ .Subscribe(
+ static state => Console.WriteLine($"AppSupportJsonSuspensionDriver loaded state for {state?.LastViewedBook}."),
+ static error => Console.WriteLine($"AppSupportJsonSuspensionDriver had nothing to load yet: {error.Message}."));
+ _ = driver.InvalidateState().Subscribe(static _ => Console.WriteLine("AppSupportJsonSuspensionDriver invalidated the saved state."));
+
+ // GetOrientation() returns the device's current rotation by name, such as "Portrait".
+ PlatformOperations platformOperations = new();
+ string? orientation = platformOperations.GetOrientation();
+ Console.WriteLine($"Device orientation: {orientation}.");
+
+ if (OperatingSystem.IsIOSVersionAtLeast(26))
+ {
+ // iOS 26 deprecates window construction outside a UIWindowScene; this sample targets 15 through 25 and
+ // stays on the classic, scene-less API below that ceiling.
+ throw new PlatformNotSupportedException("This sample targets iOS 15 through 25.");
+ }
+
+ UIWindow window = new(UIScreen.MainScreen.Bounds);
+ window.RootViewController = LibraryComposition.CreateRootViewController();
+ window.MakeKeyAndVisible();
+ Window = window;
+
+ return true;
+ }
+
+ ///
+ public override void OnActivated(UIApplication application) =>
+ _autoSuspendHelper?.OnActivated(application);
+
+ ///
+ public override void DidEnterBackground(UIApplication application) =>
+ _autoSuspendHelper?.DidEnterBackground(application);
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _autoSuspendHelper?.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/BookCoverPagerViewController.cs b/src/examples/Documentation/Pages/platform-apple/iOS/BookCoverPagerViewController.cs
new file mode 100644
index 0000000000..cfc089d2cf
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/BookCoverPagerViewController.cs
@@ -0,0 +1,76 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// Pages through one per book, left to right.
+[System.Diagnostics.DebuggerDisplay("BookCoverPagerViewController")]
+public sealed class BookCoverPagerViewController : ReactivePageViewController, IUIPageViewControllerDataSource
+{
+ /// Initializes a new instance of the class with a scroll transition.
+ public BookCoverPagerViewController()
+ : base(UIPageViewControllerTransitionStyle.Scroll, UIPageViewControllerNavigationOrientation.Horizontal)
+ {
+ }
+
+ ///
+ public override void ViewDidLoad()
+ {
+ base.ViewDidLoad();
+
+ DataSource = this;
+
+ _ = this.WhenActivated((Action onDispose) =>
+ {
+ _ = onDispose;
+ if (ViewModel!.Books.Count == 0)
+ {
+ return;
+ }
+
+ SetViewControllers([CreatePage(ViewModel.Books[0])], UIPageViewControllerNavigationDirection.Forward, false, null);
+ });
+ }
+
+ ///
+ public new UIViewController GetPreviousViewController(UIPageViewController pageViewController, UIViewController referenceViewController)
+ {
+ Book current = ((BookDetailViewController)referenceViewController).ViewModel!;
+ int index = IndexOf(ViewModel!.Books, current) - 1;
+ return index >= 0 ? CreatePage(ViewModel.Books[index]) : null!;
+ }
+
+ ///
+ public new UIViewController GetNextViewController(UIPageViewController pageViewController, UIViewController referenceViewController)
+ {
+ Book current = ((BookDetailViewController)referenceViewController).ViewModel!;
+ int index = IndexOf(ViewModel!.Books, current) + 1;
+ return index < ViewModel.Books.Count ? CreatePage(ViewModel.Books[index]) : null!;
+ }
+
+ /// Creates the detail page for one book, with its ViewModel set before the pager displays it.
+ /// The book the page shows.
+ /// The page.
+ private static BookDetailViewController CreatePage(Book book) => new() { ViewModel = book };
+
+ /// Finds a book's position in the catalog by reference, since has no IndexOf .
+ /// The catalog to search.
+ /// The book to find.
+ /// The book's index, or -1 when it is not in .
+ private static int IndexOf(IReadOnlyList books, Book book)
+ {
+ for (int i = 0; i < books.Count; i++)
+ {
+ if (ReferenceEquals(books[i], book))
+ {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/BookDetailViewController.cs b/src/examples/Documentation/Pages/platform-apple/iOS/BookDetailViewController.cs
new file mode 100644
index 0000000000..3776e79aca
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/BookDetailViewController.cs
@@ -0,0 +1,87 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// One book's detail: cover, title, author, and a star rating a librarian can adjust.
+[System.Diagnostics.DebuggerDisplay("BookDetailViewController")]
+public sealed class BookDetailViewController : ReactiveViewController
+{
+ /// Hosts the cover and rating controls, added once the view model is set on them first.
+ private readonly UIStackView _layout;
+
+ /// Initializes a new instance of the class.
+ public BookDetailViewController() =>
+ _layout = new UIStackView([TitleLabel, AuthorLabel])
+ {
+ Axis = UILayoutConstraintAxis.Vertical,
+ Alignment = UIStackViewAlignment.Center,
+ Spacing = 12,
+ TranslatesAutoresizingMaskIntoConstraints = false,
+ };
+
+ /// Gets the label showing the book's title. Internal so the binding source generator can observe it.
+ internal UILabel TitleLabel { get; } = new() { Font = UIFont.PreferredTitle1! };
+
+ /// Gets the label showing the book's author. Internal so the binding source generator can observe it.
+ internal UILabel AuthorLabel { get; } = new() { Font = UIFont.PreferredBody!, TextColor = UIColor.SecondaryLabel };
+
+ ///
+ public override void ViewDidLoad()
+ {
+ base.ViewDidLoad();
+
+ View!.BackgroundColor = UIColor.SystemBackground;
+ View!.AddSubview(_layout);
+ NSLayoutConstraint.ActivateConstraints(
+ [
+ _layout.CenterXAnchor.ConstraintEqualTo(View!.CenterXAnchor),
+ _layout.TopAnchor.ConstraintEqualTo(View!.SafeAreaLayoutGuide.TopAnchor, 24),
+ ]);
+
+ _ = this.WhenActivated(d =>
+ {
+ Title = ViewModel!.Title;
+
+ // Setting ViewModel before adding each control to the stack means it already has a book to bind to the
+ // moment it gains a superview and its own activation runs.
+ BookCoverImageView cover = new() { ViewModel = ViewModel, TranslatesAutoresizingMaskIntoConstraints = false };
+ NSLayoutConstraint.ActivateConstraints(
+ [
+ cover.WidthAnchor.ConstraintEqualTo(120),
+ cover.HeightAnchor.ConstraintEqualTo(160),
+ ]);
+ _layout.InsertArrangedSubview(cover, 0);
+
+ StarRatingControl rating = new() { ViewModel = ViewModel };
+ _layout.AddArrangedSubview(rating);
+
+ d(this.OneWayBind(ViewModel, static vm => vm.Title, static v => v.TitleLabel.Text));
+ d(this.OneWayBind(ViewModel, static vm => vm.Author, static v => v.AuthorLabel.Text));
+ d(new ActionDisposable(() =>
+ {
+ cover.RemoveFromSuperview();
+ cover.Dispose();
+ rating.RemoveFromSuperview();
+ rating.Dispose();
+ }));
+ });
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ TitleLabel.Dispose();
+ AuthorLabel.Dispose();
+ _layout.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/BookListViewController.cs b/src/examples/Documentation/Pages/platform-apple/iOS/BookListViewController.cs
new file mode 100644
index 0000000000..f09d1b5bae
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/BookListViewController.cs
@@ -0,0 +1,70 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using CoreGraphics;
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The catalog page: one row per book, and a button that starts a loan for the selected one.
+[System.Diagnostics.DebuggerDisplay("BookListViewController")]
+public sealed class BookListViewController : ReactiveViewController
+{
+ /// The stack that hosts one per book. A real catalog would use a reactive table source instead.
+ private readonly UIStackView _rows = new() { Axis = UILayoutConstraintAxis.Vertical, Spacing = 8, TranslatesAutoresizingMaskIntoConstraints = false };
+
+ /// Gets the button that opens the loan page for the selected book. Internal for the binding source generator.
+ internal UIButton LoanButton { get; } = UIButton.FromType(UIButtonType.System);
+
+ ///
+ public override void ViewDidLoad()
+ {
+ base.ViewDidLoad();
+
+ Title = "Catalog";
+ View!.BackgroundColor = UIColor.SystemBackground;
+ LoanButton.SetTitle("Loan selected book", UIControlState.Normal);
+
+ UIStackView layout = new([_rows, LoanButton])
+ {
+ Axis = UILayoutConstraintAxis.Vertical,
+ Spacing = 16,
+ TranslatesAutoresizingMaskIntoConstraints = false,
+ };
+ View!.AddSubview(layout);
+ NSLayoutConstraint.ActivateConstraints(
+ [
+ layout.LeadingAnchor.ConstraintEqualTo(View!.SafeAreaLayoutGuide.LeadingAnchor, 16),
+ layout.TrailingAnchor.ConstraintEqualTo(View!.SafeAreaLayoutGuide.TrailingAnchor, -16),
+ layout.TopAnchor.ConstraintEqualTo(View!.SafeAreaLayoutGuide.TopAnchor, 16),
+ ]);
+
+ _ = this.WhenActivated(d =>
+ {
+ foreach (Book book in ViewModel!.Books)
+ {
+ // The frame constructor pre-sizes the row; Auto Layout resizes it once ViewModel bindings run.
+ BookRowView row = new(CGRect.Empty) { ViewModel = book };
+ UITapGestureRecognizer tap = new(() => ViewModel!.SelectedBook = book);
+ row.AddGestureRecognizer(tap);
+ _rows.AddArrangedSubview(row);
+ }
+
+ d(this.BindCommand(ViewModel, static vm => vm.OpenLoan, static v => v.LoanButton));
+ });
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _rows.Dispose();
+ LoanButton.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/LibraryComposition.cs b/src/examples/Documentation/Pages/platform-apple/iOS/LibraryComposition.cs
new file mode 100644
index 0000000000..d64a2fa3f8
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/LibraryComposition.cs
@@ -0,0 +1,43 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// Builds the sample library data and the app's root view controller.
+internal static class LibraryComposition
+{
+ /// Creates the root view controller: a split view on iPad, a tab bar everywhere else.
+ /// The root view controller for .
+ internal static UIViewController CreateRootViewController()
+ {
+ Member ada = new("M-1", "Ada");
+ Member grace = new("M-2", "Grace");
+ List members = [ada, grace];
+
+ List books =
+ [
+ new("Pride and Prejudice", "Jane Austen"),
+ new("The Hobbit", "J. R. R. Tolkien"),
+ new("Dune", "Frank Herbert"),
+ ];
+
+ LibraryShellViewModel shell = new();
+ BookCatalogViewModel catalog = new(shell, books, members);
+ MembersViewModel membersViewModel = new(shell, members);
+
+ if (UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Pad)
+ {
+ return new LibrarySplitViewController(shell, catalog);
+ }
+
+ DefaultViewLocator catalogViewLocator = new();
+ catalogViewLocator.Map();
+ catalogViewLocator.Map();
+
+ return new LibraryTabBarController(shell, catalogViewLocator, catalog, membersViewModel);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/LibrarySplitViewController.cs b/src/examples/Documentation/Pages/platform-apple/iOS/LibrarySplitViewController.cs
new file mode 100644
index 0000000000..661be376b4
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/LibrarySplitViewController.cs
@@ -0,0 +1,52 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The app's root on iPad: the catalog as the master column, a as the detail.
+[System.Diagnostics.DebuggerDisplay("LibrarySplitViewController")]
+public sealed class LibrarySplitViewController : ReactiveSplitViewController
+{
+ /// Shows the detail view for whichever book the master column selects.
+ private readonly ViewModelViewHost _detailHost;
+
+ /// Initializes a new instance of the class.
+ /// The shell view model.
+ /// The catalog shown in the master column.
+ public LibrarySplitViewController(LibraryShellViewModel shell, BookCatalogViewModel catalog)
+ {
+ ViewModel = shell;
+ PreferredDisplayMode = UISplitViewControllerDisplayMode.OneBesideSecondary;
+
+ DefaultViewLocator detailLocator = new();
+ detailLocator.Map();
+ _detailHost = new ViewModelViewHost
+ {
+ DefaultContent = new UIViewController(),
+ ViewLocator = detailLocator,
+ };
+
+ BookListViewController master = new() { ViewModel = catalog };
+ ViewControllers = [master, _detailHost];
+
+ _ = this.WhenActivated(d =>
+ d(catalog.WhenAnyValue(static vm => vm.SelectedBook)
+ .WhereNotNull()
+ .Subscribe(book => _detailHost.ViewModel = book)));
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _detailHost.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/LibraryTabBarController.cs b/src/examples/Documentation/Pages/platform-apple/iOS/LibraryTabBarController.cs
new file mode 100644
index 0000000000..3c6449ac85
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/LibraryTabBarController.cs
@@ -0,0 +1,51 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The app's root on iPhone: a catalog tab, a members tab, and a cover-paging tab.
+[System.Diagnostics.DebuggerDisplay("LibraryTabBarController")]
+public sealed class LibraryTabBarController : ReactiveTabBarController
+{
+ /// Initializes a new instance of the class.
+ /// The shell that owns the catalog's router.
+ /// The view locator the catalog tab's uses to
+ /// resolve a view for each pushed view model.
+ /// The catalog view model shown first in the catalog tab.
+ /// The members view model shown in the members tab.
+ public LibraryTabBarController(LibraryShellViewModel shell, IViewLocator catalogViewLocator, BookCatalogViewModel catalog, MembersViewModel members)
+ {
+ ViewModel = shell;
+
+ RoutedViewHost catalogHost = new()
+ {
+ Router = shell.Router,
+ ViewLocator = catalogViewLocator,
+ };
+ catalogHost.TabBarItem = new UITabBarItem("Catalog", null, 0);
+ _ = shell.Router.Navigate.Execute(catalog).Subscribe();
+
+ MembersNavigationController membersNav = new(members)
+ {
+ TabBarItem = new UITabBarItem("Members", null, 1),
+ };
+
+ BookCoverPagerViewController coverPager = new()
+ {
+ ViewModel = catalog,
+ TabBarItem = new UITabBarItem("Covers", null, 2),
+ };
+
+ ViewControllers = [catalogHost, membersNav, coverPager];
+
+ _ = this.WhenActivated(d =>
+ {
+ d(Activated.Subscribe(static _ => Console.WriteLine("LibraryTabBarController activated.")));
+ d(Deactivated.Subscribe(static _ => Console.WriteLine("LibraryTabBarController deactivated.")));
+ });
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/LoanViewController.cs b/src/examples/Documentation/Pages/platform-apple/iOS/LoanViewController.cs
new file mode 100644
index 0000000000..df43547ff7
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/LoanViewController.cs
@@ -0,0 +1,89 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The loan page: pick a member, then confirm or cancel. Pushed by .
+[System.Diagnostics.DebuggerDisplay("LoanViewController")]
+public sealed class LoanViewController : ReactiveViewController
+{
+ /// Names the book being loaned.
+ private readonly UILabel _bookLabel = new() { Font = UIFont.PreferredHeadline! };
+
+ /// Hosts one button per member.
+ private readonly UIStackView _memberButtons = new() { Axis = UILayoutConstraintAxis.Vertical, Spacing = 8, TranslatesAutoresizingMaskIntoConstraints = false };
+
+ /// Gets the label naming the picked member. Internal for the binding source generator.
+ internal UILabel SelectedMemberLabel { get; } = new() { TextColor = UIColor.SecondaryLabel };
+
+ /// Gets the button that confirms the loan. Internal so the binding source generator can observe it.
+ internal UIButton ConfirmButton { get; } = UIButton.FromType(UIButtonType.System);
+
+ /// Gets the button that cancels the loan. Internal so the binding source generator can observe it.
+ internal UIButton CancelButton { get; } = UIButton.FromType(UIButtonType.System);
+
+ ///
+ public override void ViewDidLoad()
+ {
+ base.ViewDidLoad();
+
+ Title = "Loan";
+ View!.BackgroundColor = UIColor.SystemBackground;
+ ConfirmButton.SetTitle("Confirm loan", UIControlState.Normal);
+ CancelButton.SetTitle("Cancel", UIControlState.Normal);
+
+ UIStackView layout = new([_bookLabel, _memberButtons, SelectedMemberLabel, ConfirmButton, CancelButton])
+ {
+ Axis = UILayoutConstraintAxis.Vertical,
+ Spacing = 12,
+ TranslatesAutoresizingMaskIntoConstraints = false,
+ };
+ View!.AddSubview(layout);
+ NSLayoutConstraint.ActivateConstraints(
+ [
+ layout.LeadingAnchor.ConstraintEqualTo(View!.SafeAreaLayoutGuide.LeadingAnchor, 16),
+ layout.TrailingAnchor.ConstraintEqualTo(View!.SafeAreaLayoutGuide.TrailingAnchor, -16),
+ layout.TopAnchor.ConstraintEqualTo(View!.SafeAreaLayoutGuide.TopAnchor, 16),
+ ]);
+
+ _ = this.WhenActivated(d =>
+ {
+ _bookLabel.Text = $"Loaning: {ViewModel!.Book.Title}";
+
+ foreach (Member member in ViewModel.Members)
+ {
+ UIButton button = UIButton.FromType(UIButtonType.System);
+ button.SetTitle(member.Name, UIControlState.Normal);
+ button.TouchUpInside += (_, _) => ViewModel.SelectedMember = member;
+ _memberButtons.AddArrangedSubview(button);
+ }
+
+ d(this.OneWayBind(
+ ViewModel,
+ static vm => vm.SelectedMember,
+ static v => v.SelectedMemberLabel.Text,
+ static member => member is null ? "(no member selected)" : $"To: {member.Name}"));
+ d(this.BindCommand(ViewModel, static vm => vm.ConfirmLoan, static v => v.ConfirmButton));
+ d(this.BindCommand(ViewModel, static vm => vm.Cancel, static v => v.CancelButton));
+ });
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _bookLabel.Dispose();
+ _memberButtons.Dispose();
+ SelectedMemberLabel.Dispose();
+ ConfirmButton.Dispose();
+ CancelButton.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/MembersNavigationController.cs b/src/examples/Documentation/Pages/platform-apple/iOS/MembersNavigationController.cs
new file mode 100644
index 0000000000..1dde721edd
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/MembersNavigationController.cs
@@ -0,0 +1,17 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// Wraps in a navigation bar, its own ViewModel set once.
+[System.Diagnostics.DebuggerDisplay("MembersNavigationController")]
+public sealed class MembersNavigationController : ReactiveNavigationController
+{
+ /// Initializes a new instance of the class.
+ /// The members data the root page shows.
+ public MembersNavigationController(MembersViewModel viewModel)
+ : base(new MembersPlaceholderViewController { ViewModel = viewModel }) =>
+ ViewModel = viewModel;
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/MembersPlaceholderViewController.cs b/src/examples/Documentation/Pages/platform-apple/iOS/MembersPlaceholderViewController.cs
new file mode 100644
index 0000000000..e164a8742b
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/MembersPlaceholderViewController.cs
@@ -0,0 +1,46 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The members tab: counts members. Listing each in a reactive table is a different chunk of the surface.
+[System.Diagnostics.DebuggerDisplay("MembersPlaceholderViewController")]
+public sealed class MembersPlaceholderViewController : ReactiveViewController
+{
+ /// Gets the label naming how many members the library has. Internal for the binding source generator.
+ internal UILabel CountLabel { get; } = new() { Font = UIFont.PreferredBody! };
+
+ ///
+ public override void ViewDidLoad()
+ {
+ base.ViewDidLoad();
+
+ Title = "Members";
+ View!.BackgroundColor = UIColor.SystemBackground;
+ View!.AddSubview(CountLabel);
+ CountLabel.TranslatesAutoresizingMaskIntoConstraints = false;
+ NSLayoutConstraint.ActivateConstraints(
+ [
+ CountLabel.CenterXAnchor.ConstraintEqualTo(View!.CenterXAnchor),
+ CountLabel.TopAnchor.ConstraintEqualTo(View!.SafeAreaLayoutGuide.TopAnchor, 24),
+ ]);
+
+ _ = this.WhenActivated(d =>
+ d(this.OneWayBind(ViewModel, static vm => vm.MemberCount, static v => v.CountLabel.Text, static count => $"{count} member(s)")));
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ CountLabel.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/Program.cs b/src/examples/Documentation/Pages/platform-apple/iOS/Program.cs
new file mode 100644
index 0000000000..aceaa41adc
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/Program.cs
@@ -0,0 +1,17 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The iOS entry point.
+internal static class Program
+{
+ /// Hands control to UIKit, which creates and calls its lifecycle methods.
+ /// The process arguments UIKit forwards to UIApplicationMain .
+ private static void Main(string[] args) =>
+ UIApplication.Main(args, null, typeof(AppDelegate));
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/Views/BookCoverImageView.cs b/src/examples/Documentation/Pages/platform-apple/iOS/Views/BookCoverImageView.cs
new file mode 100644
index 0000000000..b38cbc61df
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/Views/BookCoverImageView.cs
@@ -0,0 +1,26 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// A book's cover. This app has no bundled artwork, so a blank image stands in and the accessibility label
+/// carries the book's title instead.
+[System.Diagnostics.DebuggerDisplay("BookCoverImageView")]
+public sealed class BookCoverImageView : ReactiveImageView
+{
+ /// Initializes a new instance of the class.
+ public BookCoverImageView()
+ {
+ Image ??= new UIImage();
+
+ _ = this.WhenActivated(d =>
+ {
+ d(this.OneWayBind(ViewModel, static vm => vm.Title, static v => v.AccessibilityLabel));
+ d(ThrownExceptions.Subscribe(static error => Console.WriteLine($"BookCoverImageView threw: {error.Message}")));
+ });
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/Views/BookRowView.cs b/src/examples/Documentation/Pages/platform-apple/iOS/Views/BookRowView.cs
new file mode 100644
index 0000000000..36824388ad
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/Views/BookRowView.cs
@@ -0,0 +1,72 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using CoreGraphics;
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// One row in the catalog: title, author, and loan status. A real catalog would use a reactive table source instead.
+[System.Diagnostics.DebuggerDisplay("BookRowView")]
+public sealed class BookRowView : ReactiveView
+{
+ /// Initializes a new instance of the class.
+ public BookRowView() => Initialize();
+
+ /// Initializes a new instance of the class with an explicit frame.
+ /// The row's initial frame.
+ public BookRowView(CGRect frame)
+ : base(frame) =>
+ Initialize();
+
+ /// Gets the book's title. Internal so the binding source generator can observe it.
+ internal UILabel TitleLabel { get; } = new() { Font = UIFont.PreferredHeadline! };
+
+ /// Gets the book's loan status. Internal so the binding source generator can observe it.
+ internal UILabel StatusLabel { get; } = new() { Font = UIFont.PreferredSubheadline!, TextColor = UIColor.SecondaryLabel };
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ TitleLabel.Dispose();
+ StatusLabel.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+
+ /// Lays out the labels and binds them to the book set as the view model before this row is added to a superview.
+ private void Initialize()
+ {
+ UIStackView stack = new([TitleLabel, StatusLabel])
+ {
+ Axis = UILayoutConstraintAxis.Vertical,
+ Spacing = 2,
+ TranslatesAutoresizingMaskIntoConstraints = false,
+ };
+ AddSubview(stack);
+ NSLayoutConstraint.ActivateConstraints(
+ [
+ stack.LeadingAnchor.ConstraintEqualTo(LeadingAnchor),
+ stack.TrailingAnchor.ConstraintEqualTo(TrailingAnchor),
+ stack.TopAnchor.ConstraintEqualTo(TopAnchor),
+ stack.BottomAnchor.ConstraintEqualTo(BottomAnchor),
+ ]);
+
+ _ = this.WhenActivated(d =>
+ {
+ d(this.OneWayBind(ViewModel, static vm => vm.Title, static v => v.TitleLabel.Text));
+ d(this.OneWayBind(
+ ViewModel,
+ static vm => vm.IsOnLoan,
+ static v => v.StatusLabel.Text,
+ static onLoan => onLoan ? "On loan" : "Available"));
+ d(Activated.Subscribe(static _ => Console.WriteLine("BookRowView activated.")));
+ d(Deactivated.Subscribe(static _ => Console.WriteLine("BookRowView deactivated.")));
+ });
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/Views/StarRatingControl.cs b/src/examples/Documentation/Pages/platform-apple/iOS/Views/StarRatingControl.cs
new file mode 100644
index 0000000000..46c56e3423
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/Views/StarRatingControl.cs
@@ -0,0 +1,73 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// A star-rating control wrapping a that binds two-way to .
+[System.Diagnostics.DebuggerDisplay("StarRatingControl {Stars}")]
+public sealed class StarRatingControl : ReactiveControl
+{
+ /// The stepper this control wraps. UIKit has no built-in star control, so a stepper stands in for one.
+ private readonly UIStepper _stepper = new() { MinimumValue = 0, MaximumValue = 5, TranslatesAutoresizingMaskIntoConstraints = false };
+
+ /// Initializes a new instance of the class.
+ public StarRatingControl() => Initialize();
+
+ /// Raised whenever changes, so the binding layer's naming convention (a
+ /// StarsChanged event for a Stars property) can drive a two-way Bind .
+ public event EventHandler? StarsChanged;
+
+ /// Gets or sets the rating, from 0 to 5.
+ public int Stars
+ {
+ get => (int)_stepper.Value;
+ set => _stepper.Value = value;
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _stepper.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+
+ /// Builds the stepper, wires its native event to , and binds it to .
+ private void Initialize()
+ {
+ AddSubview(_stepper);
+ NSLayoutConstraint.ActivateConstraints(
+ [
+ _stepper.LeadingAnchor.ConstraintEqualTo(LeadingAnchor),
+ _stepper.TopAnchor.ConstraintEqualTo(TopAnchor),
+ _stepper.BottomAnchor.ConstraintEqualTo(BottomAnchor),
+ ]);
+ _stepper.ValueChanged += (_, _) => StarsChanged?.Invoke(this, EventArgs.Empty);
+
+ // A rating of 0 never fires a change notification on its own, so start silent rather than log a phantom change.
+ using (SuppressChangeNotifications())
+ {
+ Stars = 0;
+ }
+
+ PropertyChanging += static (_, e) => Console.WriteLine($"StarRatingControl.{e.PropertyName} is changing.");
+ PropertyChanged += (_, e) => Console.WriteLine($"StarRatingControl.{e.PropertyName} changed to {Stars}.");
+
+ _ = this.WhenActivated(d =>
+ {
+ d(this.Bind(ViewModel, static vm => vm.Rating, static v => v.Stars));
+ d(Changed.Subscribe(static change => Console.WriteLine($"StarRatingControl.{change.PropertyName} changed (Changed stream).")));
+ d(Changing.Subscribe(static change => Console.WriteLine($"StarRatingControl.{change.PropertyName} is changing (Changing stream).")));
+ d(ThrownExceptions.Subscribe(static error => Console.WriteLine($"StarRatingControl threw: {error.Message}")));
+ d(Activated.Subscribe(static _ => Console.WriteLine("StarRatingControl activated.")));
+ d(Deactivated.Subscribe(static _ => Console.WriteLine("StarRatingControl deactivated.")));
+ });
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/macOS/AppDelegate.cs b/src/examples/Documentation/Pages/platform-apple/macOS/AppDelegate.cs
new file mode 100644
index 0000000000..748fa9e877
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/macOS/AppDelegate.cs
@@ -0,0 +1,79 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using AppKit;
+using Foundation;
+using ReactiveUI.Builder;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The app's NSApplicationDelegate: builds ReactiveUI, wires up state suspension, and shows the window.
+[Register(nameof(AppDelegate))]
+[System.Diagnostics.DebuggerDisplay("AppDelegate")]
+public sealed class AppDelegate : NSApplicationDelegate
+{
+ /// Keeps AppKit lifecycle notifications translated into suspend/resume signals for as long as the process lives.
+ private AutoSuspendHelper? _autoSuspendHelper;
+
+ /// Keeps the window controller alive for as long as the app runs.
+ private MainWindowController? _mainWindowController;
+
+ ///
+ public override void DidFinishLaunching(NSNotification notification)
+ {
+ ReactiveUIBuilder builder = RxAppBuilder.CreateReactiveUIBuilder();
+ _ = builder.WithPlatformModule().BuildApp();
+
+ _autoSuspendHelper = new AutoSuspendHelper(this);
+ _autoSuspendHelper.DidFinishLaunching(notification);
+ RxSuspension.SuspensionHost.CreateNewAppState = static () => new LibraryAppState();
+
+ AppSupportJsonSuspensionDriver driver = new();
+ RxSuspension.SuspensionHost.SetupDefaultSuspendResume(driver);
+
+ _ = driver.SaveState(new LibraryAppState { LastViewedBook = "Dune" }, LibraryAppStateJsonContext.Default.LibraryAppState)
+ .Subscribe(static _ => Console.WriteLine("AppSupportJsonSuspensionDriver saved the app state."));
+ _ = driver.LoadState(LibraryAppStateJsonContext.Default.LibraryAppState)
+ .Subscribe(
+ static state => Console.WriteLine($"AppSupportJsonSuspensionDriver loaded state for {state?.LastViewedBook}."),
+ static error => Console.WriteLine($"AppSupportJsonSuspensionDriver had nothing to load yet: {error.Message}."));
+ _ = driver.InvalidateState().Subscribe(static _ => Console.WriteLine("AppSupportJsonSuspensionDriver invalidated the saved state."));
+
+ PlatformOperations platformOperations = new();
+ string? orientation = platformOperations.GetOrientation();
+ Console.WriteLine($"Device orientation reported by AppKit: {orientation ?? "(none; orientation is a UIKit concept)"}.");
+
+ _mainWindowController = new MainWindowController();
+ _mainWindowController.ShowWindow(this);
+ }
+
+ ///
+ public override void DidBecomeActive(NSNotification notification) =>
+ _autoSuspendHelper?.DidBecomeActive(notification);
+
+ ///
+ public override void DidResignActive(NSNotification notification) =>
+ _autoSuspendHelper?.DidResignActive(notification);
+
+ ///
+ public override void DidHide(NSNotification notification) =>
+ _autoSuspendHelper?.DidHide(notification);
+
+ ///
+ public override NSApplicationTerminateReply ApplicationShouldTerminate(NSApplication sender) =>
+ _autoSuspendHelper?.ApplicationShouldTerminate(sender) ?? NSApplicationTerminateReply.Now;
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _autoSuspendHelper?.Dispose();
+ _mainWindowController?.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/macOS/BookDetailViewController.cs b/src/examples/Documentation/Pages/platform-apple/macOS/BookDetailViewController.cs
new file mode 100644
index 0000000000..c7d327392e
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/macOS/BookDetailViewController.cs
@@ -0,0 +1,78 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using AppKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// One book's detail: cover, title, author, and a star rating. Hosted as the split view's detail pane.
+[System.Diagnostics.DebuggerDisplay("BookDetailViewController")]
+public sealed class BookDetailViewController : ReactiveViewController
+{
+ /// Hosts the cover and rating controls, added once the view model is set on them first.
+ private NSStackView _layout = new();
+
+ /// Gets the label showing the book's title. Internal so the binding source generator can observe it.
+ internal NSTextField TitleLabel { get; } = NSTextField.CreateLabel(string.Empty);
+
+ /// Gets the label showing the book's author. Internal so the binding source generator can observe it.
+ internal NSTextField AuthorLabel { get; } = NSTextField.CreateLabel(string.Empty);
+
+ ///
+ public override void LoadView()
+ {
+ NSView view = new();
+ View = view;
+
+ _layout = new NSStackView { Orientation = NSUserInterfaceLayoutOrientation.Vertical, Spacing = 12, TranslatesAutoresizingMaskIntoConstraints = false };
+ _layout.AddArrangedSubview(TitleLabel);
+ _layout.AddArrangedSubview(AuthorLabel);
+ view.AddSubview(_layout);
+ NSLayoutConstraint.ActivateConstraints(
+ [
+ _layout.CenterXAnchor.ConstraintEqualTo(view.CenterXAnchor),
+ _layout.TopAnchor.ConstraintEqualTo(view.TopAnchor, 24),
+ ]);
+
+ _ = this.WhenActivated(d =>
+ {
+ // Setting ViewModel before adding each control to the stack means it already has a book to bind to the
+ // moment it gains a superview and its own activation runs.
+ BookCoverImageView cover = new() { ViewModel = ViewModel, TranslatesAutoresizingMaskIntoConstraints = false };
+ NSLayoutConstraint.ActivateConstraints(
+ [
+ cover.WidthAnchor.ConstraintEqualTo(120),
+ cover.HeightAnchor.ConstraintEqualTo(160),
+ ]);
+ _layout.InsertArrangedSubview(cover, 0);
+
+ StarRatingControl rating = new() { ViewModel = ViewModel };
+ _layout.AddArrangedSubview(rating);
+
+ d(this.OneWayBind(ViewModel, static vm => vm.Title, static v => v.TitleLabel.StringValue));
+ d(this.OneWayBind(ViewModel, static vm => vm.Author, static v => v.AuthorLabel.StringValue));
+ d(new ActionDisposable(() =>
+ {
+ cover.RemoveFromSuperview();
+ cover.Dispose();
+ rating.RemoveFromSuperview();
+ rating.Dispose();
+ }));
+ });
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ TitleLabel.Dispose();
+ AuthorLabel.Dispose();
+ _layout.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/macOS/BookListViewController.cs b/src/examples/Documentation/Pages/platform-apple/macOS/BookListViewController.cs
new file mode 100644
index 0000000000..144f12de85
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/macOS/BookListViewController.cs
@@ -0,0 +1,69 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using AppKit;
+using CoreGraphics;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The catalog page: one row per book with a "Select" button, and a button that loans the selected book.
+[System.Diagnostics.DebuggerDisplay("BookListViewController")]
+public sealed class BookListViewController : ReactiveViewController
+{
+ /// The stack that hosts one row per book. A real catalog would use a reactive table source instead.
+ private readonly NSStackView _rows = new() { Orientation = NSUserInterfaceLayoutOrientation.Vertical, Spacing = 8, TranslatesAutoresizingMaskIntoConstraints = false };
+
+ /// Gets the button that opens the loan page for the selected book. Internal for the binding source generator.
+ internal NSButton LoanButton { get; } = new() { Title = "Loan selected book", BezelStyle = NSBezelStyle.Rounded };
+
+ ///
+ public override void LoadView()
+ {
+ NSView view = new();
+ View = view;
+
+ NSStackView layout = new() { Orientation = NSUserInterfaceLayoutOrientation.Vertical, Spacing = 16, TranslatesAutoresizingMaskIntoConstraints = false };
+ layout.AddArrangedSubview(_rows);
+ layout.AddArrangedSubview(LoanButton);
+ view.AddSubview(layout);
+ NSLayoutConstraint.ActivateConstraints(
+ [
+ layout.LeadingAnchor.ConstraintEqualTo(view.LeadingAnchor, 16),
+ layout.TopAnchor.ConstraintEqualTo(view.TopAnchor, 16),
+ ]);
+
+ _ = this.WhenActivated(d =>
+ {
+ foreach (Book book in ViewModel!.Books)
+ {
+ // The frame constructor pre-sizes the row; Auto Layout resizes it once ViewModel bindings run.
+ BookRowView row = new(CGRect.Empty) { ViewModel = book };
+ NSButton selectButton = new() { Title = $"Select {book.Title}", BezelStyle = NSBezelStyle.Rounded };
+ selectButton.Activated += (_, _) => ViewModel!.SelectedBook = book;
+
+ NSStackView rowLayout = new() { Orientation = NSUserInterfaceLayoutOrientation.Horizontal, Spacing = 8 };
+ rowLayout.AddArrangedSubview(row);
+ rowLayout.AddArrangedSubview(selectButton);
+ _rows.AddArrangedSubview(rowLayout);
+ }
+
+ // BindCommand (the generated overload) mis-emits its NSButton dispatch on macOS today; BindCommandUnsafe
+ // uses the reflection-based path instead. See the report to the ReactiveUI.Binding.SourceGenerators team.
+ d(this.BindCommandUnsafe(ViewModel, static vm => vm.OpenLoan, static v => v.LoanButton, toEvent: null));
+ });
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _rows.Dispose();
+ LoanButton.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/macOS/LibrarySplitViewController.cs b/src/examples/Documentation/Pages/platform-apple/macOS/LibrarySplitViewController.cs
new file mode 100644
index 0000000000..ce4f964c3b
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/macOS/LibrarySplitViewController.cs
@@ -0,0 +1,52 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using AppKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The window's content: the catalog as one split item, a as the other.
+[System.Diagnostics.DebuggerDisplay("LibrarySplitViewController")]
+public sealed class LibrarySplitViewController : ReactiveSplitViewController
+{
+ /// Shows the detail view for whichever book the catalog selects.
+ private readonly ViewModelViewHost _detailHost;
+
+ /// Initializes a new instance of the class.
+ /// The shell view model.
+ /// The catalog shown in the first split item.
+ public LibrarySplitViewController(LibraryShellViewModel shell, BookCatalogViewModel catalog)
+ {
+ ViewModel = shell;
+
+ DefaultViewLocator detailLocator = new();
+ detailLocator.Map();
+ _detailHost = new ViewModelViewHost
+ {
+ DefaultContent = new NSViewController(),
+ ViewLocator = detailLocator,
+ };
+
+ BookListViewController master = new() { ViewModel = catalog };
+ AddSplitViewItem(NSSplitViewItem.FromViewController(master));
+ AddSplitViewItem(NSSplitViewItem.FromViewController(_detailHost));
+
+ _ = this.WhenActivated(d =>
+ d(catalog.WhenAnyValue(static vm => vm.SelectedBook)
+ .WhereNotNull()
+ .Subscribe(book => _detailHost.ViewModel = book)));
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _detailHost.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/macOS/MainWindowController.cs b/src/examples/Documentation/Pages/platform-apple/macOS/MainWindowController.cs
new file mode 100644
index 0000000000..040d038552
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/macOS/MainWindowController.cs
@@ -0,0 +1,58 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using AppKit;
+using CoreGraphics;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The app's single window, hosting the library split view.
+[System.Diagnostics.DebuggerDisplay("MainWindowController")]
+public sealed class MainWindowController : ReactiveWindowController
+{
+ /// Initializes a new instance of the class with the library's sample data.
+ public MainWindowController()
+ : base(CreateWindow())
+ {
+ }
+
+ ///
+ public override void WindowDidLoad()
+ {
+ base.WindowDidLoad();
+
+ Member ada = new("M-1", "Ada");
+ Member grace = new("M-2", "Grace");
+ List members = [ada, grace];
+ List books =
+ [
+ new("Pride and Prejudice", "Jane Austen"),
+ new("The Hobbit", "J. R. R. Tolkien"),
+ new("Dune", "Frank Herbert"),
+ ];
+
+ LibraryShellViewModel shell = new();
+ BookCatalogViewModel catalog = new(shell, books, members);
+
+ Window!.ContentViewController = new LibrarySplitViewController(shell, catalog);
+
+ // ReactiveWindowController does not implement IActivatableView, unlike the view and controller types, so
+ // it subscribes to Activated/Deactivated directly rather than through WhenActivated.
+ _ = Activated.Subscribe(static _ => Console.WriteLine("MainWindowController activated."));
+ _ = Deactivated.Subscribe(static _ => Console.WriteLine("MainWindowController deactivated."));
+ }
+
+ /// Creates the window this controller manages. AppKit needs the window before runs.
+ /// A borderless-menu, resizable window sized for the catalog.
+ private static NSWindow CreateWindow() =>
+ new(
+ new CGRect(0, 0, 640, 480),
+ NSWindowStyle.Titled | NSWindowStyle.Closable | NSWindowStyle.Miniaturizable | NSWindowStyle.Resizable,
+ NSBackingStore.Buffered,
+ false)
+ {
+ Title = "Library",
+ };
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/macOS/MembersPlaceholderViewController.cs b/src/examples/Documentation/Pages/platform-apple/macOS/MembersPlaceholderViewController.cs
new file mode 100644
index 0000000000..2400a0507e
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/macOS/MembersPlaceholderViewController.cs
@@ -0,0 +1,44 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using AppKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The members placeholder: counts members. Listing each in a reactive table is a different chunk of the surface.
+[System.Diagnostics.DebuggerDisplay("MembersPlaceholderViewController")]
+public sealed class MembersPlaceholderViewController : ReactiveViewController
+{
+ /// Gets the label naming how many members the library has. Internal for the binding source generator.
+ internal NSTextField CountLabel { get; } = NSTextField.CreateLabel(string.Empty);
+
+ ///
+ public override void LoadView()
+ {
+ NSView view = new();
+ View = view;
+ view.AddSubview(CountLabel);
+ CountLabel.TranslatesAutoresizingMaskIntoConstraints = false;
+ NSLayoutConstraint.ActivateConstraints(
+ [
+ CountLabel.CenterXAnchor.ConstraintEqualTo(view.CenterXAnchor),
+ CountLabel.TopAnchor.ConstraintEqualTo(view.TopAnchor, 24),
+ ]);
+
+ _ = this.WhenActivated(d =>
+ d(this.OneWayBind(ViewModel, static vm => vm.MemberCount, static v => v.CountLabel.StringValue, static count => $"{count} member(s)")));
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ CountLabel.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/macOS/Program.cs b/src/examples/Documentation/Pages/platform-apple/macOS/Program.cs
new file mode 100644
index 0000000000..e9dba423d8
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/macOS/Program.cs
@@ -0,0 +1,25 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using AppKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The macOS entry point.
+internal static class Program
+{
+ ///
+ /// Starts AppKit directly instead of calling NSApplication.Main , because this app builds its window and
+ /// menu in code and has no storyboard to wire up as NSApplication.Delegate .
+ ///
+ /// The process arguments. Unused: this app takes no command-line configuration.
+ private static void Main(string[] args)
+ {
+ _ = args;
+ NSApplication.Init();
+ NSApplication.SharedApplication.Delegate = new AppDelegate();
+ NSApplication.Main([]);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/macOS/Views/BookCoverImageView.cs b/src/examples/Documentation/Pages/platform-apple/macOS/Views/BookCoverImageView.cs
new file mode 100644
index 0000000000..ce8ec972e5
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/macOS/Views/BookCoverImageView.cs
@@ -0,0 +1,25 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using AppKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// A book's cover. This app has no bundled artwork, so a blank image stands in and the tooltip carries the book's title.
+[System.Diagnostics.DebuggerDisplay("BookCoverImageView")]
+public sealed class BookCoverImageView : ReactiveImageView
+{
+ /// Initializes a new instance of the class.
+ public BookCoverImageView()
+ {
+ Image ??= new NSImage();
+
+ _ = this.WhenActivated(d =>
+ {
+ d(this.OneWayBind(ViewModel, static vm => vm.Title, static v => v.ToolTip));
+ d(ThrownExceptions.Subscribe(static error => Console.WriteLine($"BookCoverImageView threw: {error.Message}")));
+ });
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/macOS/Views/BookRowView.cs b/src/examples/Documentation/Pages/platform-apple/macOS/Views/BookRowView.cs
new file mode 100644
index 0000000000..c374b31be8
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/macOS/Views/BookRowView.cs
@@ -0,0 +1,74 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using AppKit;
+using CoreGraphics;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// One row in the catalog: title, author, and loan status. A real catalog would use a reactive table source instead.
+[System.Diagnostics.DebuggerDisplay("BookRowView")]
+public sealed class BookRowView : ReactiveView
+{
+ /// Initializes a new instance of the class.
+ public BookRowView() => Initialize();
+
+ /// Initializes a new instance of the class with an explicit frame.
+ /// The row's initial frame.
+ public BookRowView(CGRect frame)
+ : base(frame) =>
+ Initialize();
+
+ /// Gets the book's title label. Internal so the binding source generator can observe it.
+ internal NSTextField TitleLabel { get; } = NSTextField.CreateLabel(string.Empty);
+
+ /// Gets the book's loan status label. Internal so the binding source generator can observe it.
+ internal NSTextField StatusLabel { get; } = NSTextField.CreateLabel(string.Empty);
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ TitleLabel.Dispose();
+ StatusLabel.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+
+ /// Lays out the labels and binds them to the book set as the view model before this row is added to a superview.
+ private void Initialize()
+ {
+ NSStackView stack = new()
+ {
+ Orientation = NSUserInterfaceLayoutOrientation.Vertical,
+ Spacing = 2,
+ TranslatesAutoresizingMaskIntoConstraints = false,
+ };
+ stack.AddArrangedSubview(TitleLabel);
+ stack.AddArrangedSubview(StatusLabel);
+ AddSubview(stack);
+ NSLayoutConstraint.ActivateConstraints(
+ [
+ stack.LeadingAnchor.ConstraintEqualTo(LeadingAnchor),
+ stack.TrailingAnchor.ConstraintEqualTo(TrailingAnchor),
+ stack.TopAnchor.ConstraintEqualTo(TopAnchor),
+ stack.BottomAnchor.ConstraintEqualTo(BottomAnchor),
+ ]);
+
+ _ = this.WhenActivated(d =>
+ {
+ d(this.OneWayBind(ViewModel, static vm => vm.Title, static v => v.TitleLabel.StringValue));
+ d(this.OneWayBind(
+ ViewModel,
+ static vm => vm.IsOnLoan,
+ static v => v.StatusLabel.StringValue,
+ static onLoan => onLoan ? "On loan" : "Available"));
+ d(Activated.Subscribe(static _ => Console.WriteLine("BookRowView activated.")));
+ d(Deactivated.Subscribe(static _ => Console.WriteLine("BookRowView deactivated.")));
+ });
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/macOS/Views/StarRatingControl.cs b/src/examples/Documentation/Pages/platform-apple/macOS/Views/StarRatingControl.cs
new file mode 100644
index 0000000000..c79c95d633
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/macOS/Views/StarRatingControl.cs
@@ -0,0 +1,73 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using AppKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// A star-rating control wrapping an that binds two-way to .
+[System.Diagnostics.DebuggerDisplay("StarRatingControl {Stars}")]
+public sealed class StarRatingControl : ReactiveControl
+{
+ /// The stepper this control wraps. AppKit has no built-in star control, so a stepper stands in for one.
+ private readonly NSStepper _stepper = new() { MinValue = 0, MaxValue = 5, TranslatesAutoresizingMaskIntoConstraints = false };
+
+ /// Initializes a new instance of the class.
+ public StarRatingControl() => Initialize();
+
+ /// Raised whenever changes, so the binding layer's naming convention (a
+ /// StarsChanged event for a Stars property) can drive a two-way Bind .
+ public event EventHandler? StarsChanged;
+
+ /// Gets or sets the rating, from 0 to 5.
+ public int Stars
+ {
+ get => (int)_stepper.DoubleValue;
+ set => _stepper.DoubleValue = value;
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _stepper.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+
+ /// Builds the stepper, wires its action to , and binds it to .
+ private void Initialize()
+ {
+ AddSubview(_stepper);
+ NSLayoutConstraint.ActivateConstraints(
+ [
+ _stepper.LeadingAnchor.ConstraintEqualTo(LeadingAnchor),
+ _stepper.TopAnchor.ConstraintEqualTo(TopAnchor),
+ _stepper.BottomAnchor.ConstraintEqualTo(BottomAnchor),
+ ]);
+ _stepper.Activated += (_, _) => StarsChanged?.Invoke(this, EventArgs.Empty);
+
+ // A rating of 0 never fires a change notification on its own, so start silent rather than log a phantom change.
+ using (SuppressChangeNotifications())
+ {
+ Stars = 0;
+ }
+
+ PropertyChanging += static (_, e) => Console.WriteLine($"StarRatingControl.{e.PropertyName} is changing.");
+ PropertyChanged += (_, e) => Console.WriteLine($"StarRatingControl.{e.PropertyName} changed to {Stars}.");
+
+ _ = this.WhenActivated(d =>
+ {
+ d(this.Bind(ViewModel, static vm => vm.Rating, static v => v.Stars));
+ d(Changed.Subscribe(static change => Console.WriteLine($"StarRatingControl.{change.PropertyName} changed (Changed stream).")));
+ d(Changing.Subscribe(static change => Console.WriteLine($"StarRatingControl.{change.PropertyName} is changing (Changing stream).")));
+ d(ThrownExceptions.Subscribe(static error => Console.WriteLine($"StarRatingControl threw: {error.Message}")));
+ d(Activated.Subscribe(static _ => Console.WriteLine("StarRatingControl activated.")));
+ d(Deactivated.Subscribe(static _ => Console.WriteLine("StarRatingControl deactivated.")));
+ });
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/platform-apple.csproj b/src/examples/Documentation/Pages/platform-apple/platform-apple.csproj
new file mode 100644
index 0000000000..679949dec9
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/platform-apple.csproj
@@ -0,0 +1,61 @@
+
+
+
+
+ net10.0-ios;net10.0-macos
+
+ net.reactiveui.documentation.platformapple
+ ReactiveUI Apple Examples
+ 1
+ 1.0
+
+ false
+
+ false
+ false
+ false
+ false
+
+
+
+ 15.0
+
+ iossimulator-arm64
+ iossimulator-x64
+
+
+
+ 12.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/reactiveui.slnx b/src/reactiveui.slnx
index 65518adee4..f5cf97bf4f 100644
--- a/src/reactiveui.slnx
+++ b/src/reactiveui.slnx
@@ -39,6 +39,7 @@
+
From 029fd31bc7590757e4c8c7f55a54b3bb7afea12b Mon Sep 17 00:00:00 2001
From: Glenn Watson <5834289+glennawatson@users.noreply.github.com>
Date: Sun, 27 Sep 2026 17:53:23 +1000
Subject: [PATCH 06/10] docs(examples): cover the Apple table and collection
sources, cells and section information
---
.../Shared/ViewModels/BookCatalogViewModel.cs | 10 ++-
.../Shared/ViewModels/MembersViewModel.cs | 12 ++-
.../iOS/BookCoverPagerViewController.cs | 21 +-----
.../iOS/BookListViewController.cs | 8 +-
.../iOS/BookShelfCollectionViewSource.cs | 37 ++++++++++
.../iOS/BookShelfViewController.cs | 62 ++++++++++++++++
.../platform-apple/iOS/LibraryComposition.cs | 5 +-
.../iOS/LibraryTabBarController.cs | 8 +-
.../iOS/MembersPlaceholderViewController.cs | 65 ++++++++++++----
.../platform-apple/iOS/Views/BookCoverCell.cs | 63 ++++++++++++++++
.../platform-apple/iOS/Views/LoanBoardView.cs | 29 ++++++++
.../iOS/Views/LoanedBookCell.cs | 44 +++++++++++
.../platform-apple/iOS/Views/MemberCell.cs | 74 +++++++++++++++++++
.../iOS/Views/MemberChipCell.cs | 46 ++++++++++++
.../iOS/Views/MemberChipStripView.cs | 29 ++++++++
.../iOS/Views/ShelfHeaderView.cs | 47 ++++++++++++
16 files changed, 516 insertions(+), 44 deletions(-)
create mode 100644 src/examples/Documentation/Pages/platform-apple/iOS/BookShelfCollectionViewSource.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/iOS/BookShelfViewController.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/iOS/Views/BookCoverCell.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/iOS/Views/LoanBoardView.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/iOS/Views/LoanedBookCell.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/iOS/Views/MemberCell.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/iOS/Views/MemberChipCell.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/iOS/Views/MemberChipStripView.cs
create mode 100644 src/examples/Documentation/Pages/platform-apple/iOS/Views/ShelfHeaderView.cs
diff --git a/src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/BookCatalogViewModel.cs b/src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/BookCatalogViewModel.cs
index be945599c5..867a0bc2de 100644
--- a/src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/BookCatalogViewModel.cs
+++ b/src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/BookCatalogViewModel.cs
@@ -3,6 +3,8 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.
+using System.Collections.ObjectModel;
+
namespace ReactiveUI.Documentation.PlatformApple;
/// The catalog page: every book the library owns, plus the members who can borrow one.
@@ -19,7 +21,7 @@ public sealed class BookCatalogViewModel : ReactiveObject, IRoutableViewModel, I
public BookCatalogViewModel(IScreen hostScreen, IReadOnlyList books, IReadOnlyList members)
{
HostScreen = hostScreen;
- Books = books;
+ Books = books as ObservableCollection ?? new ObservableCollection(books);
_members = members;
IObservable canLoan = this.WhenAnyValue(
@@ -37,8 +39,10 @@ public BookCatalogViewModel(IScreen hostScreen, IReadOnlyList books, IRead
///
public IScreen HostScreen { get; }
- /// Gets the library's catalog.
- public IReadOnlyList Books { get; }
+ /// Gets the library's catalog. A reactive table or collection source binds to this directly: because it
+ /// implements , the source refreshes the
+ /// view whenever a book is added or removed.
+ public ObservableCollection Books { get; }
/// Gets or sets the book the member picked from the catalog.
public Book? SelectedBook
diff --git a/src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/MembersViewModel.cs b/src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/MembersViewModel.cs
index 5d7b9468c5..ee75b9a882 100644
--- a/src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/MembersViewModel.cs
+++ b/src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/MembersViewModel.cs
@@ -3,9 +3,11 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.
+using System.Collections.ObjectModel;
+
namespace ReactiveUI.Documentation.PlatformApple;
-/// The members tab: every library member. The second agent's table/collection-source example lists them; this view model only carries the data.
+/// The members tab: every library member, as the change-set source a reactive table or collection source reads from.
[System.Diagnostics.DebuggerDisplay("MembersViewModel Members = {Members.Count}")]
public sealed class MembersViewModel : ReactiveObject, IRoutableViewModel
{
@@ -15,7 +17,7 @@ public sealed class MembersViewModel : ReactiveObject, IRoutableViewModel
public MembersViewModel(IScreen hostScreen, IReadOnlyList members)
{
HostScreen = hostScreen;
- Members = members;
+ Members = members as ObservableCollection ?? new ObservableCollection(members);
MemberCount = members.Count;
}
@@ -25,8 +27,10 @@ public MembersViewModel(IScreen hostScreen, IReadOnlyList members)
///
public IScreen HostScreen { get; }
- /// Gets every library member.
- public IReadOnlyList Members { get; }
+ /// Gets every library member. A reactive table or collection source binds to this directly: because it
+ /// implements , the source refreshes the
+ /// view whenever a member is added or removed.
+ public ObservableCollection Members { get; }
/// Gets how many members the library has. A plain so a one-way binding can observe
/// it directly, rather than chaining through the non-reactive list.
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/BookCoverPagerViewController.cs b/src/examples/Documentation/Pages/platform-apple/iOS/BookCoverPagerViewController.cs
index cfc089d2cf..3fcca1c50f 100644
--- a/src/examples/Documentation/Pages/platform-apple/iOS/BookCoverPagerViewController.cs
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/BookCoverPagerViewController.cs
@@ -40,7 +40,7 @@ public override void ViewDidLoad()
public new UIViewController GetPreviousViewController(UIPageViewController pageViewController, UIViewController referenceViewController)
{
Book current = ((BookDetailViewController)referenceViewController).ViewModel!;
- int index = IndexOf(ViewModel!.Books, current) - 1;
+ int index = ViewModel!.Books.IndexOf(current) - 1;
return index >= 0 ? CreatePage(ViewModel.Books[index]) : null!;
}
@@ -48,7 +48,7 @@ public override void ViewDidLoad()
public new UIViewController GetNextViewController(UIPageViewController pageViewController, UIViewController referenceViewController)
{
Book current = ((BookDetailViewController)referenceViewController).ViewModel!;
- int index = IndexOf(ViewModel!.Books, current) + 1;
+ int index = ViewModel!.Books.IndexOf(current) + 1;
return index < ViewModel.Books.Count ? CreatePage(ViewModel.Books[index]) : null!;
}
@@ -56,21 +56,4 @@ public override void ViewDidLoad()
/// The book the page shows.
/// The page.
private static BookDetailViewController CreatePage(Book book) => new() { ViewModel = book };
-
- /// Finds a book's position in the catalog by reference, since has no IndexOf .
- /// The catalog to search.
- /// The book to find.
- /// The book's index, or -1 when it is not in .
- private static int IndexOf(IReadOnlyList books, Book book)
- {
- for (int i = 0; i < books.Count; i++)
- {
- if (ReferenceEquals(books[i], book))
- {
- return i;
- }
- }
-
- return -1;
- }
}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/BookListViewController.cs b/src/examples/Documentation/Pages/platform-apple/iOS/BookListViewController.cs
index f09d1b5bae..a47e996618 100644
--- a/src/examples/Documentation/Pages/platform-apple/iOS/BookListViewController.cs
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/BookListViewController.cs
@@ -18,6 +18,9 @@ public sealed class BookListViewController : ReactiveViewControllerGets the button that opens the loan page for the selected book. Internal for the binding source generator.
internal UIButton LoanButton { get; } = UIButton.FromType(UIButtonType.System);
+ /// Gets the plain table listing books currently on loan, hosted below the button.
+ internal LoanBoardView LoanBoard { get; } = new(CGRect.Empty);
+
///
public override void ViewDidLoad()
{
@@ -26,8 +29,10 @@ public override void ViewDidLoad()
Title = "Catalog";
View!.BackgroundColor = UIColor.SystemBackground;
LoanButton.SetTitle("Loan selected book", UIControlState.Normal);
+ LoanBoard.ViewModel = ViewModel;
+ LoanBoard.HeightAnchor.ConstraintEqualTo(140).Active = true;
- UIStackView layout = new([_rows, LoanButton])
+ UIStackView layout = new([_rows, LoanButton, LoanBoard])
{
Axis = UILayoutConstraintAxis.Vertical,
Spacing = 16,
@@ -63,6 +68,7 @@ protected override void Dispose(bool disposing)
{
_rows.Dispose();
LoanButton.Dispose();
+ LoanBoard.Dispose();
}
base.Dispose(disposing);
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/BookShelfCollectionViewSource.cs b/src/examples/Documentation/Pages/platform-apple/iOS/BookShelfCollectionViewSource.cs
new file mode 100644
index 0000000000..491cdab636
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/BookShelfCollectionViewSource.cs
@@ -0,0 +1,37 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using Foundation;
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// A that also supplies the shelf's section header. The
+/// ReactiveCollectionViewSourceExtensions.BindTo family always builds a plain ,
+/// which has no header hook of its own, so a grid that needs one subclasses the source directly instead, the same
+/// way that extension method does internally.
+internal sealed class BookShelfCollectionViewSource : ReactiveCollectionViewSource
+{
+ /// The reuse identifier the shelf registers under.
+ internal static readonly NSString HeaderKey = new(nameof(ShelfHeaderView));
+
+ /// The catalog the header's book count comes from.
+ private readonly BookCatalogViewModel _catalog;
+
+ /// Initializes a new instance of the class.
+ /// The grid this source drives.
+ /// The catalog the header's book count comes from.
+ internal BookShelfCollectionViewSource(UICollectionView collectionView, BookCatalogViewModel catalog)
+ : base(collectionView) =>
+ _catalog = catalog;
+
+ ///
+ public override UICollectionReusableView GetViewForSupplementaryElement(UICollectionView collectionView, NSString elementKind, NSIndexPath indexPath)
+ {
+ ShelfHeaderView header = (ShelfHeaderView)collectionView.DequeueReusableSupplementaryView(UICollectionElementKindSection.Header, HeaderKey, indexPath);
+ header.ViewModel = _catalog;
+ return header;
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/BookShelfViewController.cs b/src/examples/Documentation/Pages/platform-apple/iOS/BookShelfViewController.cs
new file mode 100644
index 0000000000..36e24744c6
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/BookShelfViewController.cs
@@ -0,0 +1,62 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using CoreGraphics;
+using Foundation;
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The shelf tab: every book as a cover tile in a grid.
+[System.Diagnostics.DebuggerDisplay("BookShelfViewController")]
+public sealed class BookShelfViewController : ReactiveCollectionViewController
+{
+ /// The reuse identifier every cover shares; the shelf never needs a different cell per book.
+ private static readonly NSString CoverCellKey = new(nameof(BookCoverCell));
+
+ /// Initializes a new instance of the class with a two-column grid layout.
+ public BookShelfViewController()
+ : base(new UICollectionViewFlowLayout
+ {
+ ItemSize = new CGSize(140, 90),
+ MinimumInteritemSpacing = 12,
+ MinimumLineSpacing = 12,
+ SectionInset = new UIEdgeInsets(12, 12, 12, 12),
+ HeaderReferenceSize = new CGSize(0, 32),
+ })
+ {
+ }
+
+ ///
+ public override void ViewDidLoad()
+ {
+ base.ViewDidLoad();
+
+ Title = "Shelf";
+ CollectionView!.BackgroundColor = UIColor.SystemBackground;
+ CollectionView.RegisterClassForCell(typeof(BookCoverCell), CoverCellKey);
+ CollectionView.RegisterClassForSupplementaryView(typeof(ShelfHeaderView), UICollectionElementKindSection.Header, BookShelfCollectionViewSource.HeaderKey);
+
+ _ = this.WhenActivated(d =>
+ {
+ CollectionViewSectionInformation section = new(
+ ViewModel!.Books,
+ static _ => CoverCellKey,
+ static cell => Console.WriteLine($"Initializing a {cell.GetType().Name}."));
+ IReadOnlyList> sections = [section];
+
+ BookShelfCollectionViewSource source = new(CollectionView!, ViewModel);
+ source.Data = sections;
+ CollectionView!.Source = source;
+
+ CollectionViewSectionInformation readBack = source.Data[0];
+ Console.WriteLine(
+ $"Shelf has {source.Data.Count} section(s); every cover shares one reuse key: {readBack.CellKeySelector is not null}.");
+
+ d(source);
+ d(source.ElementSelected.Subscribe(static item => Console.WriteLine($"Selected cover {((Book)item!).Title}.")));
+ });
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/LibraryComposition.cs b/src/examples/Documentation/Pages/platform-apple/iOS/LibraryComposition.cs
index d64a2fa3f8..7f3422e1c3 100644
--- a/src/examples/Documentation/Pages/platform-apple/iOS/LibraryComposition.cs
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/LibraryComposition.cs
@@ -3,6 +3,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.
+using System.Collections.ObjectModel;
using UIKit;
namespace ReactiveUI.Documentation.PlatformApple;
@@ -16,9 +17,9 @@ internal static UIViewController CreateRootViewController()
{
Member ada = new("M-1", "Ada");
Member grace = new("M-2", "Grace");
- List members = [ada, grace];
+ ObservableCollection members = [ada, grace];
- List books =
+ ObservableCollection books =
[
new("Pride and Prejudice", "Jane Austen"),
new("The Hobbit", "J. R. R. Tolkien"),
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/LibraryTabBarController.cs b/src/examples/Documentation/Pages/platform-apple/iOS/LibraryTabBarController.cs
index 3c6449ac85..7e4dce285d 100644
--- a/src/examples/Documentation/Pages/platform-apple/iOS/LibraryTabBarController.cs
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/LibraryTabBarController.cs
@@ -40,7 +40,13 @@ public LibraryTabBarController(LibraryShellViewModel shell, IViewLocator catalog
TabBarItem = new UITabBarItem("Covers", null, 2),
};
- ViewControllers = [catalogHost, membersNav, coverPager];
+ BookShelfViewController shelf = new()
+ {
+ ViewModel = catalog,
+ TabBarItem = new UITabBarItem("Shelf", null, 3),
+ };
+
+ ViewControllers = [catalogHost, membersNav, coverPager, shelf];
_ = this.WhenActivated(d =>
{
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/MembersPlaceholderViewController.cs b/src/examples/Documentation/Pages/platform-apple/iOS/MembersPlaceholderViewController.cs
index e164a8742b..5d34a441c5 100644
--- a/src/examples/Documentation/Pages/platform-apple/iOS/MembersPlaceholderViewController.cs
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/MembersPlaceholderViewController.cs
@@ -3,16 +3,28 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.
+using CoreGraphics;
using UIKit;
namespace ReactiveUI.Documentation.PlatformApple;
-/// The members tab: counts members. Listing each in a reactive table is a different chunk of the surface.
+/// The members tab: a chip strip of every member above a table of the same members, one row each.
[System.Diagnostics.DebuggerDisplay("MembersPlaceholderViewController")]
-public sealed class MembersPlaceholderViewController : ReactiveViewController
+public sealed class MembersPlaceholderViewController : ReactiveTableViewController
{
- /// Gets the label naming how many members the library has. Internal for the binding source generator.
- internal UILabel CountLabel { get; } = new() { Font = UIFont.PreferredBody! };
+ /// Gets the horizontal chip strip above the table. Internal so the binding source generator can reach it.
+ internal MemberChipStripView ChipStrip { get; } = new(
+ new CGRect(0, 0, 320, 56),
+ new UICollectionViewFlowLayout
+ {
+ ScrollDirection = UICollectionViewScrollDirection.Horizontal,
+ ItemSize = new CGSize(48, 48),
+ MinimumInteritemSpacing = 12,
+ SectionInset = new UIEdgeInsets(8, 16, 8, 16),
+ });
+
+ /// Gets the reactive table source bound in .
+ internal ReactiveTableViewSource? Source { get; private set; }
///
public override void ViewDidLoad()
@@ -20,17 +32,42 @@ public override void ViewDidLoad()
base.ViewDidLoad();
Title = "Members";
- View!.BackgroundColor = UIColor.SystemBackground;
- View!.AddSubview(CountLabel);
- CountLabel.TranslatesAutoresizingMaskIntoConstraints = false;
- NSLayoutConstraint.ActivateConstraints(
- [
- CountLabel.CenterXAnchor.ConstraintEqualTo(View!.CenterXAnchor),
- CountLabel.TopAnchor.ConstraintEqualTo(View!.SafeAreaLayoutGuide.TopAnchor, 24),
- ]);
+ ChipStrip.ViewModel = ViewModel;
+ TableView.RegisterClassForCellReuse(typeof(MemberCell), MemberCell.Key);
+ TableView.TableHeaderView = ChipStrip;
_ = this.WhenActivated(d =>
- d(this.OneWayBind(ViewModel, static vm => vm.MemberCount, static v => v.CountLabel.Text, static count => $"{count} member(s)")));
+ {
+ TableSectionInformation section = new(
+ ViewModel!.Members,
+ MemberCell.Key,
+ sizeHint: 56F,
+ static cell => Console.WriteLine($"Initializing a {cell.GetType().Name}."))
+ {
+ Header = new TableSectionHeader("Members"),
+ Footer = new TableSectionHeader(
+ static () => new UILabel
+ {
+ Text = "Tap a member to see their card.",
+ TextAlignment = UITextAlignment.Center,
+ Font = UIFont.PreferredFootnote!,
+ TextColor = UIColor.SecondaryLabel,
+ },
+ 24F),
+ };
+ IReadOnlyList> sections = [section];
+
+ d(Signal.Emit(sections).BindTo(TableView, source =>
+ {
+ Source = source;
+ return source.ElementSelected.Subscribe(static item => Console.WriteLine($"Selected member {((Member)item!).Name}."));
+ }));
+
+ TableSectionInformation readBack = Source!.Data[0];
+ Console.WriteLine(
+ $"{readBack.Header?.Title}: {Source.Data.Count} section(s), row height {readBack.SizeHint}, "
+ + $"has a collection {readBack.Collection is not null}, has an init action {readBack.InitializeCellAction is not null}.");
+ });
}
///
@@ -38,7 +75,7 @@ protected override void Dispose(bool disposing)
{
if (disposing)
{
- CountLabel.Dispose();
+ ChipStrip.Dispose();
}
base.Dispose(disposing);
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/Views/BookCoverCell.cs b/src/examples/Documentation/Pages/platform-apple/iOS/Views/BookCoverCell.cs
new file mode 100644
index 0000000000..3705fffdbc
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/Views/BookCoverCell.cs
@@ -0,0 +1,63 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// One cover tile in the book-cover grid: title and loan status.
+[System.Diagnostics.DebuggerDisplay("BookCoverCell")]
+public sealed class BookCoverCell : ReactiveCollectionViewCell
+{
+ /// Initializes a new instance of the class from a native handle.
+ /// The native handle UIKit hands the base constructor.
+ public BookCoverCell(IntPtr handle)
+ : base(handle)
+ {
+ ContentView.BackgroundColor = UIColor.SecondarySystemBackground;
+
+ UIStackView stack = new([TitleLabel, StatusLabel])
+ {
+ Axis = UILayoutConstraintAxis.Vertical,
+ Spacing = 2,
+ TranslatesAutoresizingMaskIntoConstraints = false,
+ };
+ ContentView.AddSubview(stack);
+ NSLayoutConstraint.ActivateConstraints(
+ [
+ stack.LeadingAnchor.ConstraintEqualTo(ContentView.LeadingAnchor, 8),
+ stack.TrailingAnchor.ConstraintEqualTo(ContentView.TrailingAnchor, -8),
+ stack.CenterYAnchor.ConstraintEqualTo(ContentView.CenterYAnchor),
+ ]);
+
+ _ = this.WhenActivated(d =>
+ {
+ d(this.OneWayBind(ViewModel, static vm => vm.Title, static v => v.TitleLabel.Text));
+ d(this.OneWayBind(
+ ViewModel,
+ static vm => vm.IsOnLoan,
+ static v => v.StatusLabel.Text,
+ static onLoan => onLoan ? "On loan" : "Available"));
+ });
+ }
+
+ /// Gets the book's title. Internal so the binding source generator can observe it.
+ internal UILabel TitleLabel { get; } = new() { Font = UIFont.PreferredCaption1!, Lines = 2 };
+
+ /// Gets the book's loan status. Internal so the binding source generator can observe it.
+ internal UILabel StatusLabel { get; } = new() { Font = UIFont.PreferredCaption2!, TextColor = UIColor.SecondaryLabel };
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ TitleLabel.Dispose();
+ StatusLabel.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/Views/LoanBoardView.cs b/src/examples/Documentation/Pages/platform-apple/iOS/Views/LoanBoardView.cs
new file mode 100644
index 0000000000..ba35031727
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/Views/LoanBoardView.cs
@@ -0,0 +1,29 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.Linq;
+using CoreGraphics;
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// A plain hosted directly as a subview, listing books currently on loan.
+[System.Diagnostics.DebuggerDisplay("LoanBoardView")]
+public sealed class LoanBoardView : ReactiveTableView
+{
+ /// Initializes a new instance of the class.
+ /// The board's initial frame.
+ public LoanBoardView(CGRect frame)
+ : base(frame, UITableViewStyle.Plain)
+ {
+ _ = this.WhenActivated(d =>
+ {
+ ObservableCollection onLoan = new(ViewModel!.Books.Where(static book => book.IsOnLoan));
+ d(Signal.Emit(onLoan).BindTo(this, sizeHint: 44F));
+ });
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/Views/LoanedBookCell.cs b/src/examples/Documentation/Pages/platform-apple/iOS/Views/LoanedBookCell.cs
new file mode 100644
index 0000000000..c12e9f74de
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/Views/LoanedBookCell.cs
@@ -0,0 +1,44 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// One row in the on-loan board: a book's title.
+[System.Diagnostics.DebuggerDisplay("LoanedBookCell")]
+public sealed class LoanedBookCell : ReactiveTableViewCell
+{
+ /// Initializes a new instance of the class from a native handle.
+ /// The native handle UIKit hands the base constructor.
+ public LoanedBookCell(IntPtr handle)
+ : base(handle)
+ {
+ ContentView.AddSubview(TitleLabel);
+ TitleLabel.TranslatesAutoresizingMaskIntoConstraints = false;
+ NSLayoutConstraint.ActivateConstraints(
+ [
+ TitleLabel.LeadingAnchor.ConstraintEqualTo(ContentView.LeadingAnchor, 16),
+ TitleLabel.CenterYAnchor.ConstraintEqualTo(ContentView.CenterYAnchor),
+ ]);
+
+ _ = this.WhenActivated(d =>
+ d(this.OneWayBind(ViewModel, static vm => vm.Title, static v => v.TitleLabel.Text)));
+ }
+
+ /// Gets the book's title. Internal so the binding source generator can observe it.
+ internal UILabel TitleLabel { get; } = new() { Font = UIFont.PreferredBody! };
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ TitleLabel.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/Views/MemberCell.cs b/src/examples/Documentation/Pages/platform-apple/iOS/Views/MemberCell.cs
new file mode 100644
index 0000000000..b22eaaaae1
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/Views/MemberCell.cs
@@ -0,0 +1,74 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using Foundation;
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// One row of the members table: a member's name, and the one cell that shows the six classic IReactiveObject members every reactive Apple type carries.
+[System.Diagnostics.DebuggerDisplay("MemberCell")]
+public sealed class MemberCell : ReactiveTableViewCell
+{
+ /// The reuse identifier registers this cell under.
+ internal static readonly NSString Key = new(nameof(MemberCell));
+
+ /// Initializes a new instance of the class from a native handle.
+ /// The native handle UIKit hands the base constructor.
+ public MemberCell(IntPtr handle)
+ : base(handle)
+ {
+ ContentView.AddSubview(NameLabel);
+ NameLabel.TranslatesAutoresizingMaskIntoConstraints = false;
+ NSLayoutConstraint.ActivateConstraints(
+ [
+ NameLabel.LeadingAnchor.ConstraintEqualTo(ContentView.LeadingAnchor, 16),
+ NameLabel.CenterYAnchor.ConstraintEqualTo(ContentView.CenterYAnchor),
+ ]);
+
+ // The classic events fire for any property change; a cell can use them without going through Changed/Changing.
+ PropertyChanged += static (_, e) => Console.WriteLine($"MemberCell.{e.PropertyName} changed (classic event).");
+ PropertyChanging += static (_, e) => Console.WriteLine($"MemberCell.{e.PropertyName} changing (classic event).");
+
+ _ = this.WhenActivated(d =>
+ {
+ // Member is an immutable record, not a ReactiveObject, so the label follows ViewModel itself changing
+ // (which the cell base class does raise) rather than a OneWayBind on one of its properties.
+ d(this.WhenAnyValue(static v => v.ViewModel).Subscribe(member => NameLabel.Text = member?.Name));
+ d(Changing.Subscribe(static _ => Console.WriteLine("MemberCell changing.")));
+ d(Changed.Subscribe(static _ => Console.WriteLine("MemberCell changed.")));
+ d(ThrownExceptions.Subscribe(static error => Console.WriteLine($"MemberCell binding failed: {error.Message}")));
+ d(Activated.Subscribe(static _ => Console.WriteLine("MemberCell activated.")));
+ d(Deactivated.Subscribe(static _ => Console.WriteLine("MemberCell deactivated.")));
+ });
+ }
+
+ /// Gets the member's name. Internal so the binding source generator can observe it.
+ internal UILabel NameLabel { get; } = new() { Font = UIFont.PreferredBody! };
+
+ ///
+ public override void PrepareForReuse()
+ {
+ // Clearing the binding target before reuse should not itself look like a change to any observer, so it runs
+ // inside a suppression scope. Only the real value set once the cell is dequeued again should notify.
+ using (SuppressChangeNotifications())
+ {
+ ViewModel = null;
+ }
+
+ base.PrepareForReuse();
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ NameLabel.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/Views/MemberChipCell.cs b/src/examples/Documentation/Pages/platform-apple/iOS/Views/MemberChipCell.cs
new file mode 100644
index 0000000000..c42a0129d1
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/Views/MemberChipCell.cs
@@ -0,0 +1,46 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using CoreGraphics;
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// One chip in the horizontal member strip: the member's initial in a circle.
+[System.Diagnostics.DebuggerDisplay("MemberChipCell")]
+public sealed class MemberChipCell : ReactiveCollectionViewCell
+{
+ /// Initializes a new instance of the class from a native handle.
+ /// The native handle UIKit hands the base constructor.
+ public MemberChipCell(IntPtr handle)
+ : base(handle)
+ {
+ ContentView.BackgroundColor = UIColor.SystemGray5;
+ ContentView.Layer.CornerRadius = 20;
+ ContentView.AddSubview(InitialLabel);
+ InitialLabel.Frame = new CGRect(0, 0, 40, 40);
+ InitialLabel.AutoresizingMask = UIViewAutoresizing.FlexibleDimensions;
+
+ // Member is an immutable record, not a ReactiveObject, so the label follows ViewModel itself changing rather
+ // than a OneWayBind on one of its properties.
+ _ = this.WhenActivated(d =>
+ d(this.WhenAnyValue(static v => v.ViewModel)
+ .Subscribe(member => InitialLabel.Text = member?.Name[..1].ToUpperInvariant())));
+ }
+
+ /// Gets the member's initial. Internal so the binding source generator can observe it.
+ internal UILabel InitialLabel { get; } = new() { TextAlignment = UITextAlignment.Center, Font = UIFont.PreferredHeadline! };
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ InitialLabel.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/Views/MemberChipStripView.cs b/src/examples/Documentation/Pages/platform-apple/iOS/Views/MemberChipStripView.cs
new file mode 100644
index 0000000000..1e87bc26ee
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/Views/MemberChipStripView.cs
@@ -0,0 +1,29 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Collections.Specialized;
+using CoreGraphics;
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// A plain : no collection view controller owns it, so
+/// hosts it directly as the members table's header, listing the same
+/// members as a horizontal strip of chips.
+[System.Diagnostics.DebuggerDisplay("MemberChipStripView")]
+public sealed class MemberChipStripView : ReactiveCollectionView
+{
+ /// Initializes a new instance of the class.
+ /// The strip's initial frame.
+ /// The horizontal flow layout the strip scrolls under.
+ public MemberChipStripView(CGRect frame, UICollectionViewLayout layout)
+ : base(frame, layout)
+ {
+ BackgroundColor = UIColor.SystemBackground;
+
+ _ = this.WhenActivated(d =>
+ d(Signal.Emit(ViewModel!.Members).BindTo(this)));
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/Views/ShelfHeaderView.cs b/src/examples/Documentation/Pages/platform-apple/iOS/Views/ShelfHeaderView.cs
new file mode 100644
index 0000000000..c9e948e51b
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/Views/ShelfHeaderView.cs
@@ -0,0 +1,47 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The single section header above the book-cover grid, naming how many books it shows.
+[System.Diagnostics.DebuggerDisplay("ShelfHeaderView")]
+public sealed class ShelfHeaderView : ReactiveCollectionReusableView
+{
+ /// Initializes a new instance of the class from a native handle.
+ /// The native handle UIKit hands the base constructor.
+ public ShelfHeaderView(IntPtr handle)
+ : base(handle)
+ {
+ AddSubview(CountLabel);
+ CountLabel.TranslatesAutoresizingMaskIntoConstraints = false;
+ NSLayoutConstraint.ActivateConstraints(
+ [
+ CountLabel.LeadingAnchor.ConstraintEqualTo(LeadingAnchor, 16),
+ CountLabel.CenterYAnchor.ConstraintEqualTo(CenterYAnchor),
+ ]);
+
+ // A supplementary view is dequeued and given its ViewModel before UIKit adds it to the view hierarchy, so it
+ // reacts to the property directly rather than through WhenActivated.
+ _ = this.WhenAnyValue(static v => v.ViewModel)
+ .WhereNotNull()
+ .Subscribe(vm => CountLabel.Text = $"{vm.Books.Count} book(s)");
+ }
+
+ /// Gets the label naming how many books the shelf has. Internal so the binding source generator can observe it.
+ internal UILabel CountLabel { get; } = new() { Font = UIFont.PreferredHeadline! };
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ CountLabel.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+}
From 97a6d56e6e59c1f2a70de010ffb852ecab6de399 Mon Sep 17 00:00:00 2001
From: Glenn Watson <5834289+glennawatson@users.noreply.github.com>
Date: Sun, 27 Sep 2026 18:38:24 +1000
Subject: [PATCH 07/10] build(examples): Apple page disables trimming the way
the Apple SDKs require and names its macOS runtime
---
.../Pages/platform-apple/platform-apple.csproj | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/src/examples/Documentation/Pages/platform-apple/platform-apple.csproj b/src/examples/Documentation/Pages/platform-apple/platform-apple.csproj
index 679949dec9..883dc97699 100644
--- a/src/examples/Documentation/Pages/platform-apple/platform-apple.csproj
+++ b/src/examples/Documentation/Pages/platform-apple/platform-apple.csproj
@@ -16,8 +16,8 @@
RoutedViewHostUnsafe/ViewModelViewHostUnsafe, and AppSupportJsonSuspensionDriver's untyped LoadState()/
SaveState(T) overloads, all of which carry RequiresUnreferencedCode/RequiresDynamicCode. Building for
"Run" otherwise enables the trim/AOT analyzers and turns every one of those calls into a build error under
- this repository's TreatWarningsAsErrors, exactly as on the Android page. -->
- false
+ this repository's TreatWarningsAsErrors, exactly as on the Android page. The Apple SDKs require
+ PublishTrimmed, so trimming itself is turned off per platform below (MtouchLink, LinkMode). -->
false
false
false
@@ -28,10 +28,15 @@
iossimulator-arm64
iossimulator-x64
+ None
12.0
+
+ osx-arm64
+ osx-x64
+ None
From 9e0c55e8c41268e3d157091cf652e2b6b63b3c66 Mon Sep 17 00:00:00 2001
From: Glenn Watson <5834289+glennawatson@users.noreply.github.com>
Date: Sun, 27 Sep 2026 19:44:23 +1000
Subject: [PATCH 08/10] build(deps): bump ReactiveUI.Binding to 8.5.0
---
src/Directory.Packages.props | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index d12241609c..4d297703c3 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -7,7 +7,7 @@
21.0.0
8.2.0
- 8.4.0
+ 8.5.0
1.69.16
From d5d2d1b6fa52f9b777c687b2be2f3a44e7acf473 Mon Sep 17 00:00:00 2001
From: Glenn Watson <5834289+glennawatson@users.noreply.github.com>
Date: Sun, 27 Sep 2026 19:44:23 +1000
Subject: [PATCH 09/10] docs(examples): macOS binds its NSButton with the
generated BindCommand
---
.../Pages/platform-apple/macOS/BookListViewController.cs | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/src/examples/Documentation/Pages/platform-apple/macOS/BookListViewController.cs b/src/examples/Documentation/Pages/platform-apple/macOS/BookListViewController.cs
index 144f12de85..b67be5cb0e 100644
--- a/src/examples/Documentation/Pages/platform-apple/macOS/BookListViewController.cs
+++ b/src/examples/Documentation/Pages/platform-apple/macOS/BookListViewController.cs
@@ -49,9 +49,7 @@ public override void LoadView()
_rows.AddArrangedSubview(rowLayout);
}
- // BindCommand (the generated overload) mis-emits its NSButton dispatch on macOS today; BindCommandUnsafe
- // uses the reflection-based path instead. See the report to the ReactiveUI.Binding.SourceGenerators team.
- d(this.BindCommandUnsafe(ViewModel, static vm => vm.OpenLoan, static v => v.LoanButton, toEvent: null));
+ d(this.BindCommand(ViewModel, static vm => vm.OpenLoan, static v => v.LoanButton));
});
}
From 827ae2320759601f09aab6e19f9c297e3c99aefb Mon Sep 17 00:00:00 2001
From: Glenn Watson <5834289+glennawatson@users.noreply.github.com>
Date: Sun, 27 Sep 2026 20:07:29 +1000
Subject: [PATCH 10/10] docs(examples): WinUI ViewModelViewHost switches views
by contract; macOS window uses ReactiveWindowController
---
.../ViewModels/LibraryShellViewModel.cs | 3 +
.../macOS/MainWindowController.cs | 16 +++--
.../platform-winui/ViewContractExamples.cs | 69 +++++++++----------
.../Pages/platform-winui/WeatherStationApp.cs | 4 +-
4 files changed, 48 insertions(+), 44 deletions(-)
diff --git a/src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/LibraryShellViewModel.cs b/src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/LibraryShellViewModel.cs
index e0099b060e..fa46532a46 100644
--- a/src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/LibraryShellViewModel.cs
+++ b/src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/LibraryShellViewModel.cs
@@ -11,4 +11,7 @@ public sealed class LibraryShellViewModel : ReactiveObject, IScreen
{
///
public RoutingState Router { get; } = new();
+
+ /// Gets the title the macOS window shows for the shell.
+ public string Title => "Library";
}
diff --git a/src/examples/Documentation/Pages/platform-apple/macOS/MainWindowController.cs b/src/examples/Documentation/Pages/platform-apple/macOS/MainWindowController.cs
index 040d038552..14cf421cd6 100644
--- a/src/examples/Documentation/Pages/platform-apple/macOS/MainWindowController.cs
+++ b/src/examples/Documentation/Pages/platform-apple/macOS/MainWindowController.cs
@@ -8,9 +8,13 @@
namespace ReactiveUI.Documentation.PlatformApple;
-/// The app's single window, hosting the library split view.
+///
+/// The app's single window, hosting the library split view. It derives from
+/// , the form of the non-generic
+/// , so it has a ViewModel and can use WhenActivated .
+///
[System.Diagnostics.DebuggerDisplay("MainWindowController")]
-public sealed class MainWindowController : ReactiveWindowController
+public sealed class MainWindowController : ReactiveWindowController
{
/// Initializes a new instance of the class with the library's sample data.
public MainWindowController()
@@ -35,13 +39,13 @@ public override void WindowDidLoad()
LibraryShellViewModel shell = new();
BookCatalogViewModel catalog = new(shell, books, members);
+ ViewModel = shell;
Window!.ContentViewController = new LibrarySplitViewController(shell, catalog);
- // ReactiveWindowController does not implement IActivatableView, unlike the view and controller types, so
- // it subscribes to Activated/Deactivated directly rather than through WhenActivated.
- _ = Activated.Subscribe(static _ => Console.WriteLine("MainWindowController activated."));
- _ = Deactivated.Subscribe(static _ => Console.WriteLine("MainWindowController deactivated."));
+ // ViewModel is set above, so the binding below has a shell to read Title from the moment activation runs.
+ _ = this.WhenActivated(d =>
+ d(this.OneWayBind(ViewModel, static vm => vm.Title, static v => v.Window!.Title)));
}
/// Creates the window this controller manages. AppKit needs the window before runs.
diff --git a/src/examples/Documentation/Pages/platform-winui/ViewContractExamples.cs b/src/examples/Documentation/Pages/platform-winui/ViewContractExamples.cs
index 4ae669a0b2..db6ed97b5a 100644
--- a/src/examples/Documentation/Pages/platform-winui/ViewContractExamples.cs
+++ b/src/examples/Documentation/Pages/platform-winui/ViewContractExamples.cs
@@ -15,73 +15,70 @@ namespace ReactiveUI.Documentation.PlatformWinui;
public static class ViewContractExamples
{
///
- /// ResolveViewForViewModel runs once for the empty host and again for each ViewModel change;
- /// overrides it to record what it resolved. ViewContract
- /// republishes as ViewContractObservable , the same stream ViewContractObservableProperty holds.
+ /// Setting ViewContract after the host already has a ViewModel resolves the view registered under
+ /// that contract, the same way it does at construction. The contract then stays in force for the next
+ /// ViewModel change. overrides ResolveViewForViewModel to
+ /// record what it resolved each time.
///
- public static void ViewModelViewHostResolvesEachViewModelChangeAndRepublishesItsContract()
+ public static void ViewModelViewHostViewContractSetAfterConstructionChoosesTheContractView()
{
DefaultViewLocator locator = new();
locator.Map();
+ locator.Map("Compact");
AnalyticsViewModelViewHost host = new() { ViewLocator = locator };
WeatherReading riverside = new("Riverside", 18.5, isStormy: false);
- WeatherReading highlands = new("Highlands", 9.0, isStormy: true);
host.ViewModel = riverside;
- host.ViewModel = highlands;
+ Console.WriteLine(host.Content?.GetType().Name);
- Console.WriteLine(string.Join(", ", host.ResolvedViews));
+ host.ViewContract = "Compact";
+ Console.WriteLine(host.Content?.GetType().Name);
- host.ViewContract = "Wide";
+ WeatherReading highlands = new("Highlands", 9.0, isStormy: true);
+ host.ViewModel = highlands;
+ Console.WriteLine(host.Content?.GetType().Name);
Console.WriteLine(host.ViewContract);
- string? published = null;
- using IDisposable subscription = host.ViewContractObservable.Subscribe(contract => published = contract);
- Console.WriteLine(published);
-
- bool sameObservable = ReferenceEquals(host.ViewContractObservable, host.GetValue(ViewModelViewHost.ViewContractObservableProperty));
- Console.WriteLine(sameObservable);
+ Console.WriteLine(string.Join(", ", host.ResolvedViews));
// Output:
- // (nothing), WeatherReadingRowView, WeatherReadingRowView
- // Wide
- // Wide
- // True
+ // WeatherReadingRowView
+ // WeatherReadingCompactRowView
+ // WeatherReadingCompactRowView
+ // Compact
+ // (nothing), WeatherReadingRowView, WeatherReadingCompactRowView, WeatherReadingCompactRowView
}
- /// adds the same three contract members, typed to one view model.
- public static void GenericViewModelViewHostResolvesEachViewModelChangeAndRepublishesItsContract()
+ /// follows a contract set after construction the same way the non-generic host does.
+ public static void GenericViewModelViewHostViewContractSetAfterConstructionChoosesTheContractView()
{
DefaultViewLocator locator = new();
locator.Map();
+ locator.Map("Compact");
AnalyticsViewModelViewHost host = new() { ViewLocator = locator };
WeatherReading harbor = new("Harbor", 21.0, isStormy: false);
- WeatherReading highlands = new("Highlands", 9.0, isStormy: true);
host.ViewModel = harbor;
- host.ViewModel = highlands;
+ Console.WriteLine(host.Content?.GetType().Name);
- Console.WriteLine(string.Join(", ", host.ResolvedViews));
+ host.ViewContract = "Compact";
+ Console.WriteLine(host.Content?.GetType().Name);
- host.ViewContract = "Wide";
+ WeatherReading highlands = new("Highlands", 9.0, isStormy: true);
+ host.ViewModel = highlands;
+ Console.WriteLine(host.Content?.GetType().Name);
Console.WriteLine(host.ViewContract);
- string? published = null;
- using IDisposable subscription = host.ViewContractObservable.Subscribe(contract => published = contract);
- Console.WriteLine(published);
-
- bool sameObservable = ReferenceEquals(
- host.ViewContractObservable,
- host.GetValue(ViewModelViewHost.ViewContractObservableProperty));
- Console.WriteLine(sameObservable);
+ Console.WriteLine(string.Join(", ", host.ResolvedViews));
// Output:
- // (nothing), WeatherReadingRowView, WeatherReadingRowView
- // Wide
- // Wide
- // True
+ // WeatherReadingRowView
+ // WeatherReadingCompactRowView
+ // WeatherReadingCompactRowView
+ // Compact
+ // (nothing), WeatherReadingRowView, WeatherReadingCompactRowView, WeatherReadingCompactRowView
}
/// Setting ViewContract before the router navigates picks the view mapped to that contract.
diff --git a/src/examples/Documentation/Pages/platform-winui/WeatherStationApp.cs b/src/examples/Documentation/Pages/platform-winui/WeatherStationApp.cs
index 3c2e4c7184..950b1c635e 100644
--- a/src/examples/Documentation/Pages/platform-winui/WeatherStationApp.cs
+++ b/src/examples/Documentation/Pages/platform-winui/WeatherStationApp.cs
@@ -49,8 +49,8 @@ protected override void OnLaunched(LaunchActivatedEventArgs args)
WinUIStartupExamples.MapAViewFromTheServiceLocator();
VisibilityConverterExamples.UseHiddenHasNoEffectOnWinUI();
VisibilityConverterExamples.GetAffinityForObjectsReportsTheBuiltInScore();
- ViewContractExamples.ViewModelViewHostResolvesEachViewModelChangeAndRepublishesItsContract();
- ViewContractExamples.GenericViewModelViewHostResolvesEachViewModelChangeAndRepublishesItsContract();
+ ViewContractExamples.ViewModelViewHostViewContractSetAfterConstructionChoosesTheContractView();
+ ViewContractExamples.GenericViewModelViewHostViewContractSetAfterConstructionChoosesTheContractView();
ViewContractExamples.RoutedViewHostViewContractSelectsTheContractView();
ViewContractExamples.GenericRoutedViewHostViewContractSelectsTheContractView();