Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@
<PackageReference Include="PublicApiSharp.Analyzers" PrivateAssets="all" />
</ItemGroup>

<ItemGroup Condition="'$(IsTestProject)' != 'true'">
<ItemGroup Condition="'$(IsTestProject)' != 'true' and '$(EnableSourceLink)' != 'false'">
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All"/>
</ItemGroup>
<PropertyGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<PropertyGroup Label="Shared Version Variables">
<SplatVersion>21.0.0</SplatVersion>
<PrimitivesVersion>8.2.0</PrimitivesVersion>
<BindingVersion>8.4.0</BindingVersion>
<BindingVersion>8.5.0</BindingVersion>
<TUnitVersion>1.69.16</TUnitVersion>
<!-- StyleSharp.Analyzers, PerformanceSharp.Analyzers and SecuritySharp.Analyzers ship from the
same release pipeline and always share a version. -->
Expand Down
8 changes: 8 additions & 0 deletions src/examples/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -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. -->
<TrackPublicApi>false</TrackPublicApi>

<!-- Examples are never published as packages, so they need no SourceLink. It also reports spurious warnings
when an example builds from a copy of the repository that has no .git folder. Set before the import, which
skips the SourceLink package when this is false. -->
<EnableSourceLink>false</EnableSourceLink>
<EnableSourceControlManagerQueries>false</EnableSourceControlManagerQueries>
</PropertyGroup>

<!-- Inherit all repository-wide settings from the root src/Directory.Build.props. -->
Expand All @@ -14,5 +20,7 @@
<PropertyGroup>
<IsPackable>false</IsPackable>
<LangVersion>14.0</LangVersion>
<PublishRepositoryUrl>false</PublishRepositoryUrl>
<EmbedUntrackedSources>false</EmbedUntrackedSources>
</PropertyGroup>
</Project>
22 changes: 22 additions & 0 deletions src/examples/Documentation/Pages/commands/BookReceiptPrinter.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// A receipt printer that watches a borrow command's results directly, by implementing <see cref="IObserver{T}"/>
/// itself, rather than through an operator such as <c>Subscribe(Action&lt;T&gt;)</c>.
/// </summary>
public sealed class BookReceiptPrinter : IObserver<Book>
{
/// <inheritdoc/>
public void OnNext(Book value) => Console.WriteLine($"Printed a receipt for {value.Title}");

/// <inheritdoc/>
public void OnError(Exception error) => Console.WriteLine($"Printer jammed: {error.Message}");

/// <inheritdoc/>
public void OnCompleted() => Console.WriteLine("No more receipts to print");
}
42 changes: 42 additions & 0 deletions src/examples/Documentation/Pages/commands/CommandTypeExamples.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,48 @@ public static async Task DeriveFromCommandBase()
// cannot announce
}

/// <summary>
/// A witness that implements <see cref="IObserver{T}"/> directly, such as <see cref="BookReceiptPrinter"/>, can
/// subscribe to a command's results the same way any other observer does.
/// </summary>
/// <returns>A task that completes once both books have been borrowed.</returns>
public static async Task SubscribeAWitnessToACommandsResults()
{
LibraryDesk desk = new();
BookReceiptPrinter witness = new();

using ReactiveCommand<int, Book> borrow = ReactiveCommand.Create<int, Book>(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
}

/// <summary>
/// Subscribing a witness through <see cref="ReactiveCommandBase{TParam, TResult}"/> works whether the command was
/// built with a factory or, like <see cref="DeskAnnouncementCommand"/>, by hand; a display board only needs to
/// know a command produces <c>string</c> results, not which subclass built it.
/// </summary>
/// <returns>A task that completes once the announcement has posted to the board.</returns>
public static async Task SubscribeAWitnessThroughTheBaseType()
{
LibraryDesk desk = new();
DeskAnnouncementBoard board = new();
using DeskAnnouncementCommand closingSoon = new(desk, static () => "The desk closes in ten minutes.");

ReactiveCommandBase<RxVoid, string> command = closingSoon;
using IDisposable subscription = command.Subscribe(board);

_ = await command.Execute();

// Output:
// Board: The desk closes in ten minutes.
}

/// <summary>
/// Write your own command type from <c>CombinedReactiveCommand&lt;TParam, TResult&gt;</c>'s three constructors
/// when every combined command of a kind needs the same behaviour on each run;
Expand Down
22 changes: 22 additions & 0 deletions src/examples/Documentation/Pages/commands/DeskAnnouncementBoard.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// A display board that posts each announcement it receives directly, by implementing <see cref="IObserver{T}"/>
/// itself. It only needs a command that produces <c>string</c> results, not any particular command subclass.
/// </summary>
public sealed class DeskAnnouncementBoard : IObserver<string>
{
/// <inheritdoc/>
public void OnNext(string value) => Console.WriteLine($"Board: {value}");

/// <inheritdoc/>
public void OnError(Exception error) => Console.WriteLine($"Board offline: {error.Message}");

/// <inheritdoc/>
public void OnCompleted() => Console.WriteLine("Board cleared");
}
4 changes: 4 additions & 0 deletions src/examples/Documentation/Pages/commands/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@

await CommandTypeExamples.DeriveFromCommandBase();

await CommandTypeExamples.SubscribeAWitnessToACommandsResults();

await CommandTypeExamples.SubscribeAWitnessThroughTheBaseType();

await CommandTypeExamples.DeriveCombinedCommand();

await CombinedCommandMembersExamples.ReadCombinedCommandMembersDirectly();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>Asks whether to add a note to an absence report. This activity predates AndroidX, like
/// <see cref="AbsenceActivity"/>: it derives from the plain <see cref="ReactiveActivity{TViewModel}"/>, so
/// <see cref="AbsenceActivity"/> can start it with that base's own <c>ActivityResult</c> and
/// <c>StartActivityForResultAsync</c> members rather than the AndroidX ones <see cref="MainActivity"/> already
/// uses.</summary>
[Activity(Label = "Add a note")]
public sealed class AbsenceNotePromptActivity : ReactiveActivity<AbsenceViewModel>
{
/// <inheritdoc/>
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<TextView>(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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -30,6 +31,13 @@ public sealed class LessonDetailFragment : AndroidX.ReactiveFragment<LessonViewM
// property this fragment might gain later.
AndroidX.ControlFetcherMixins.WireUpControls(this, view, ControlFetcherMixins.ResolveStrategy.ExplicitOptIn);

// The name SubjectLabel was wired under: WireUpResourceAttribute.ResourceNameOverride, read directly
// rather than through GetResourceName().
WireUpResourceAttribute? subjectAttribute = typeof(LessonDetailFragment)
.GetProperty(nameof(SubjectLabel))!
.GetCustomAttribute<WireUpResourceAttribute>();
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.");
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>Shows a revision note for a lesson, hosted as an <see cref="AndroidX.ReactiveFragment{TViewModel}"/>.
/// Wires its own label with the two-argument, implicit-strategy overload of <c>WireUpControls</c>, rather than
/// stating a strategy explicitly the way <see cref="LessonDetailFragment"/> does.</summary>
[System.Diagnostics.DebuggerDisplay("{ViewModel}")]
public sealed class LessonNotesFragment : AndroidX.ReactiveFragment<LessonViewModel>
{
/// <summary>Gets or sets the label showing the revision note; wired by naming convention to <c>notesLabel</c>.</summary>
public TextView? NotesLabel { get; set; }

/// <inheritdoc/>
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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}.");
}

/// <summary>Gets or sets the label showing the lesson's subject.</summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,20 @@ namespace ReactiveUI.Documentation.PlatformAndroid;
[System.Diagnostics.DebuggerDisplay("ItemCount = {ItemCount}")]
public sealed class LessonsRecyclerAdapter(ObservableCollection<LessonViewModel> lessons) : ReactiveRecyclerViewAdapter<LessonViewModel, ObservableCollection<LessonViewModel>>(lessons)
{
/// <summary>The view type for a lesson held in the science lab, room 7.</summary>
private const int LabViewType = 1;

/// <summary>The view type for every other lesson.</summary>
private const int StandardViewType = 0;

/// <summary>Picks a view type for a lesson: the science lab (room 7) gets its own type, so
/// <see cref="OnCreateViewHolder"/> can tell a lab lesson apart from any other.</summary>
/// <param name="position">The position of the lesson in the list.</param>
/// <param name="viewModel">The lesson at that position, or <see langword="null"/> if the position is out of range.</param>
/// <returns>An ID identifying the view type to use for the lesson.</returns>
public override int GetItemViewType(int position, LessonViewModel? viewModel) =>
viewModel?.Room == "7" ? LabViewType : StandardViewType;

/// <inheritdoc/>
public override RecyclerView.ViewHolder OnCreateViewHolder(ViewGroup parent, int viewType)
{
Expand All @@ -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);
}
}
Loading
Loading