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