-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Add blazor test app #4279
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Add blazor test app #4279
Conversation
Chat works as expected
|
Hello @glennawatson. I am not sure if it is right |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Adds a new Blazor Server “test app” project to the repo to demonstrate ReactiveUI usage (chat/lobby sample), including UI, routing, state, and some cross-instance syncing plumbing.
Changes:
- Added
ReactiveUI.Builder.Blazorproject and wired it into the solution. - Implemented Blazor UI + ReactiveUI view models for lobby/rooms and chat room messaging.
- Added bundled static assets (Bootstrap) and app scaffolding/configuration.
Reviewed changes
Copilot reviewed 41 out of 85 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/reactiveui.slnx | Adds the Blazor test app project to the solution. |
| src/ReactiveUI.Builder.Blazor/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js | Vendored Bootstrap JS for the Blazor UI. |
| src/ReactiveUI.Builder.Blazor/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css | Vendored Bootstrap CSS (reboot RTL min). |
| src/ReactiveUI.Builder.Blazor/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css | Vendored Bootstrap CSS (reboot RTL). |
| src/ReactiveUI.Builder.Blazor/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css | Vendored Bootstrap CSS (reboot min). |
| src/ReactiveUI.Builder.Blazor/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css | Vendored Bootstrap CSS (reboot). |
| src/ReactiveUI.Builder.Blazor/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css | Vendored Bootstrap CSS (grid RTL min). |
| src/ReactiveUI.Builder.Blazor/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css | Vendored Bootstrap CSS (grid min). |
| src/ReactiveUI.Builder.Blazor/wwwroot/favicon.png | Adds favicon for the Blazor app. |
| src/ReactiveUI.Builder.Blazor/wwwroot/app.css | Adds app-specific CSS. |
| src/ReactiveUI.Builder.Blazor/appsettings.json | Adds production app settings. |
| src/ReactiveUI.Builder.Blazor/appsettings.Development.json | Adds development app settings. |
| src/ReactiveUI.Builder.Blazor/Views/LobbyView.razor.cs | Lobby view event handlers (create/delete/join room). |
| src/ReactiveUI.Builder.Blazor/Views/LobbyView.razor | Lobby UI markup. |
| src/ReactiveUI.Builder.Blazor/Views/ChatRoomView.razor.cs | Chat room view event handlers (send/back). |
| src/ReactiveUI.Builder.Blazor/Views/ChatRoomView.razor | Chat room UI markup. |
| src/ReactiveUI.Builder.Blazor/ViewModels/LobbyViewModel.cs | Lobby view model: room list, create/delete, sync. |
| src/ReactiveUI.Builder.Blazor/ViewModels/ChatRoomViewModel.cs | Chat room view model: messages and send logic. |
| src/ReactiveUI.Builder.Blazor/ViewModels/AppBootstrapper.cs | Sets up per-circuit routing bootstrapper. |
| src/ReactiveUI.Builder.Blazor/Services/RoomEventKind.cs | Defines room sync event types. |
| src/ReactiveUI.Builder.Blazor/Services/ReactiveUIAppHostedService.cs | Initializes app state & lifetime coordination. |
| src/ReactiveUI.Builder.Blazor/Services/FileJsonSuspensionDriver.cs | Adds a JSON suspension driver for persisted state. |
| src/ReactiveUI.Builder.Blazor/Services/AppLifetimeCoordinator.cs | Adds cross-process instance counter helper. |
| src/ReactiveUI.Builder.Blazor/Services/AppInstance.cs | Adds a unique app-instance ID holder. |
| src/ReactiveUI.Builder.Blazor/ReactiveUI.Builder.Blazor.csproj | Adds the new Blazor project definition. |
| src/ReactiveUI.Builder.Blazor/Properties/launchSettings.json | Adds launch profiles for the Blazor app. |
| src/ReactiveUI.Builder.Blazor/Program.cs | Configures DI, ReactiveUI builder, and Blazor server pipeline. |
| src/ReactiveUI.Builder.Blazor/Models/RoomEventMessage.cs | Defines room event/snapshot message payload. |
| src/ReactiveUI.Builder.Blazor/Models/ChatStateChanged.cs | Adds message type for local state refresh. |
| src/ReactiveUI.Builder.Blazor/Models/ChatState.cs | Defines persisted chat state model. |
| src/ReactiveUI.Builder.Blazor/Models/ChatRoom.cs | Defines chat room model. |
| src/ReactiveUI.Builder.Blazor/Models/ChatNetworkMessage.cs | Defines network message model. |
| src/ReactiveUI.Builder.Blazor/Models/ChatMessage.cs | Defines single chat message model. |
| src/ReactiveUI.Builder.Blazor/Components/_Imports.razor | Adds Razor imports for the app. |
| src/ReactiveUI.Builder.Blazor/Components/Routes.razor | Adds route configuration. |
| src/ReactiveUI.Builder.Blazor/Components/Pages/NotFound.razor | Adds NotFound page. |
| src/ReactiveUI.Builder.Blazor/Components/Pages/Error.razor | Adds Error page. |
| src/ReactiveUI.Builder.Blazor/Components/Layout/ReconnectModal.razor.js | Adds reconnect modal JS behavior. |
| src/ReactiveUI.Builder.Blazor/Components/Layout/ReconnectModal.razor.css | Adds reconnect modal styling. |
| src/ReactiveUI.Builder.Blazor/Components/Layout/ReconnectModal.razor | Adds reconnect modal markup. |
| src/ReactiveUI.Builder.Blazor/Components/Layout/ReactiveRouterHost.razor | Adds a router host that resolves views from view models. |
| src/ReactiveUI.Builder.Blazor/Components/Layout/NavMenu.razor.css | Adds nav menu styling. |
| src/ReactiveUI.Builder.Blazor/Components/Layout/NavMenu.razor | Adds nav menu markup. |
| src/ReactiveUI.Builder.Blazor/Components/Layout/MainLayout.razor.css | Adds main layout styling. |
| src/ReactiveUI.Builder.Blazor/Components/Layout/MainLayout.razor.cs | Wires ReactiveUI ViewModel into layout. |
| src/ReactiveUI.Builder.Blazor/Components/Layout/MainLayout.razor | Adds main layout markup. |
| src/ReactiveUI.Builder.Blazor/Components/App.razor | Adds HTML host + asset references for the app. |
| src/Directory.Packages.props | Adds package versions for Blazor dependencies and changes Splat version. |
Comments suppressed due to low confidence (5)
src/ReactiveUI.Builder.Blazor/ViewModels/ChatRoomViewModel.cs:1
ToPropertyis binding theboolobservable (canSend) to theSendMessagecommand property name, which is a type mismatch and will fail at runtime. Bind the OAPH to a boolean property (e.g.,CanSend) and haveCanSendDisabledderive from it (or bind directly toCanSendDisabledwith the correct semantics).
src/ReactiveUI.Builder.Blazor/ViewModels/ChatRoomViewModel.cs:1ToPropertyis binding theboolobservable (canSend) to theSendMessagecommand property name, which is a type mismatch and will fail at runtime. Bind the OAPH to a boolean property (e.g.,CanSend) and haveCanSendDisabledderive from it (or bind directly toCanSendDisabledwith the correct semantics).
src/ReactiveUI.Builder.Blazor/Views/LobbyView.razor.cs:1OnRoomClickedis markedasyncbut doesn’t await anything. Removeasyncand returnTask.CompletedTask, or change the handler to a synchronous method to avoid compiler warnings and reduce overhead.
src/ReactiveUI.Builder.Blazor/ViewModels/ChatRoomViewModel.cs:1- The comment says the message is posted on a “null contract”, but the code sends with
RoomNameas the contract. Update the comment to match the behavior (or change the code if the intent was truly a null/empty contract).
src/ReactiveUI.Builder.Blazor/Views/LobbyView.razor:1 - The UI mixes English labels with a Russian label (
Название:). For consistency (and easier future localization), consider keeping labels in one language or moving strings into a localization/resource mechanism.
| &[open] | ||
|
|
||
| { | ||
| animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s; | ||
| animation-fill-mode: both; | ||
| } | ||
|
|
||
| } | ||
|
|
||
| #components-reconnect-modal::backdrop { | ||
| background-color: rgba(0, 0, 0, 0.4); | ||
| animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out; | ||
| opacity: 1; | ||
| } |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The selector nesting (&[open]) and brace placement here is not valid in plain CSS and is likely to be ignored, breaking the “open” dialog animation. Use a standard selector like #components-reconnect-modal[open] { ... } instead of nested &[open].
| &[open] | |
| { | |
| animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s; | |
| animation-fill-mode: both; | |
| } | |
| } | |
| #components-reconnect-modal::backdrop { | |
| background-color: rgba(0, 0, 0, 0.4); | |
| animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out; | |
| opacity: 1; | |
| } | |
| } | |
| #components-reconnect-modal[open] { | |
| animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s; | |
| animation-fill-mode: both; | |
| } | |
| #components-reconnect-modal::backdrop { | |
| background-color: rgba(0, 0, 0, 0.4); | |
| animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out; | |
| opacity: 1; | |
| } | |
| background-color: rgba(0, 0, 0, 0.4); | |
| animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out; | |
| opacity: 1; | |
| } |
| public async Task StartAsync(CancellationToken cancellationToken) | ||
| { | ||
| RxApp.SuspensionHost.CreateNewAppState = static () => new ChatState(); | ||
|
|
||
| // Set an initial state instantly (same idea as WPF to avoid blocking) | ||
| RxApp.SuspensionHost.AppState = new ChatState(); | ||
|
|
||
| var lifetime = Locator.Current.GetService<AppLifetimeCoordinator>(); | ||
| var count = lifetime?.Increment() ?? 1; | ||
| Trace.WriteLine($"[Blazor] Instance started. Count={count} Id={AppInstance.Id}"); | ||
| } |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
StartAsync/StopAsync are marked async but have no awaits, which will produce compiler warnings and unnecessary state machines. Prefer returning Task.CompletedTask (and remove async) or actually await any async work (and consider using cancellationToken where applicable).
| public async Task StopAsync(CancellationToken cancellationToken) | ||
| { | ||
| var lifetime = Locator.Current.GetService<AppLifetimeCoordinator>(); | ||
| var remaining = lifetime?.Decrement() ?? 0; | ||
| Trace.WriteLine($"[Blazor] Instance exiting. Remaining={remaining} Id={AppInstance.Id}"); | ||
| } |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
StartAsync/StopAsync are marked async but have no awaits, which will produce compiler warnings and unnecessary state machines. Prefer returning Task.CompletedTask (and remove async) or actually await any async work (and consider using cancellationToken where applicable).
| </PropertyGroup> | ||
| <PropertyGroup> | ||
| <SplatVersion>19.2.1</SplatVersion> | ||
| <SplatVersion>17.1.1</SplatVersion> |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR globally downgrades SplatVersion from 19.2.1 to 17.1.1 for the entire repo, which can unintentionally affect unrelated projects and dependency graphs. Prefer keeping the existing repo-wide Splat version and (if needed) overriding only for the Blazor app (e.g., via project-level PackageReference version or a dedicated property for the sample).
| <SplatVersion>17.1.1</SplatVersion> | |
| <SplatVersion>19.2.1</SplatVersion> |
src/Directory.Packages.props
Outdated
| <PackageVersion Include="ReactiveUI.Blazor" Version="22.3.1" /> | ||
| <PackageVersion Include="Splat.Microsoft.Extensions.DependencyInjection" Version="$(SplatVersion)" /> |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR globally downgrades SplatVersion from 19.2.1 to 17.1.1 for the entire repo, which can unintentionally affect unrelated projects and dependency graphs. Prefer keeping the existing repo-wide Splat version and (if needed) overriding only for the Blazor app (e.g., via project-level PackageReference version or a dedicated property for the sample).
| // 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.Builder.Blazor.Services; |
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file is under Models/ but declares the ReactiveUI.Builder.Blazor.Services namespace. This mismatch makes the codebase harder to navigate (and is easy to misread when searching). Consider moving the file into the Services/ folder or changing the namespace to ReactiveUI.Builder.Blazor.Models (and updating references accordingly).
| namespace ReactiveUI.Builder.Blazor.Services; | |
| namespace ReactiveUI.Builder.Blazor.Models; |
|
@BekAllaev Thank you for this, please could you rebase this onto the latest main code. You will need to update a few things once you do this.
public sealed class ReactiveUiAppHostedService : IHostedService
{
private FileJsonSuspensionDriver? _driver;
/// <summary>
/// Initializes the application state and starts required services asynchronously.
/// </summary>
/// <remarks>This method loads any previously persisted application state and notifies listeners if the
/// state changes. It also starts network and lifetime coordination services required for the application's
/// operation. If loading the persisted state fails, the application continues with a new state instance.
/// </remarks>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous start operation.</returns>
public async Task StartAsync(CancellationToken cancellationToken)
{
RxSuspension.SuspensionHost.CreateNewAppState = static () => new ChatState();
// Set an initial state instantly (same idea as WPF to avoid blocking)
RxSuspension.SuspensionHost.AppState = new ChatState();
var statePath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"ReactiveUI.Builder.WpfApp",
"state.json");
Directory.CreateDirectory(Path.GetDirectoryName(statePath)!);
_driver = new FileJsonSuspensionDriver(statePath);
// Set an initial state instantly to avoid blocking UI
RxSuspension.SuspensionHost.AppState = new ChatState();
// Load persisted state asynchronously and update UI when ready
_ = _driver
.LoadState()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(
static stateObj =>
{
RxSuspension.SuspensionHost.AppState = stateObj;
MessageBus.Current.SendMessage(new ChatStateChanged());
Trace.WriteLine("[App] State loaded");
},
static ex => Trace.WriteLine($"[App] State load failed: {ex.Message}"));
var lifetime = Locator.Current.GetService<AppLifetimeCoordinator>();
var count = lifetime?.Increment() ?? 1;
Trace.WriteLine($"[Blazor] Instance started. Count={count} Id={AppInstance.Id}");
}
/// <summary>
/// Performs application shutdown tasks asynchronously, including saving application state and releasing network
/// resources.
/// </summary>
/// <remarks>If this is the last running instance, the method saves the current application state before
/// disposing of network resources. Subsequent calls after all instances have exited will not trigger additional
/// state saves.</remarks>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the shutdown operation.</param>
/// <returns>A task that represents the asynchronous shutdown operation.</returns>
public async Task StopAsync(CancellationToken cancellationToken)
{
var lifetime = Locator.Current.GetService<AppLifetimeCoordinator>();
var remaining = lifetime?.Decrement() ?? 0;
Trace.WriteLine($"[Blazor] Instance exiting. Remaining={remaining} Id={AppInstance.Id}");
// Only the last instance persists the final state to the central store
if (remaining == 0 && _driver is not null && RxSuspension.SuspensionHost.AppState is not null)
{
_driver.SaveState(RxSuspension.SuspensionHost.AppState).Wait();
}
}
}I believe at this point the application should build and run as expected |
…activeui#4278) <!-- Please be sure to read the [Contribute](https://github.com/reactiveui/reactiveui#contribute) section of the README --> **What kind of change does this PR introduce?** <!-- Bug fix, feature, docs update, ... --> Update **What is the new behavior?** <!-- If this is a feature change --> Simplifies command parameter subscription and usage by removing redundant local variables and consistently using 'latestParameter'. Updates CompositeDisposable initialization and improves code clarity in command binding logic. **What might this PR break?** None expected **Please check if the PR fulfills these requirements** - [ ] Tests for the changes have been added (for bug fixes / features) - [ ] Docs have been added / updated (for bug fixes / features) **Other information**:
Chat works as expected
Changed the chat room name label from Russian ("Название:") to English ("Room name:") for consistency in the LobbyView.razor file.
Upgraded Splat to v19.2.1 in central package props. Removed ReactiveUI.Blazor NuGet package references and replaced with a direct project reference in ReactiveUI.Builder.Blazor. No other package versions or settings changed.
Refactored LoadState and SaveState methods in FileJsonSuspensionDriver to support generic types and JsonTypeInfo for serialization. Replaced hardcoded ChatState logic with generic overloads (currently not implemented). Updated scheduler to RxSchedulers.TaskpoolScheduler and improved XML documentation.
Refactored RoomEventMessage from Services to Models namespace. Updated all references and using directives accordingly for better code organization. No changes to logic or functionality.
Removed an outdated comment from SendMessageImpl in ChatRoomViewModel.cs. Changed OnRoomClicked in LobbyView.razor.cs from async Task to synchronous void to better reflect its usage. No functional changes to message sending logic.
Replaced RxApp and RxApp.SuspensionHost with RxSuspension equivalents for app state management. Updated scheduler references to RxSchedulers.TaskpoolScheduler and RxSchedulers.MainThreadScheduler for consistency with newer ReactiveUI APIs. No changes to application logic.
|
@ChrisPulman, I broke my commit history while rebasing. Can I close this PR and open new one? I will backup my current working version, then I am going to create new branch and move my working version there. It is easier and faster for me to do it that way |
|
Sure re-create the PR no problem, if you don't mind add the prefix "feature: Add blazor example app" as the title for the next PR. |
|
The other option btw, with your other branch, is just to do |
|
Also: To correct my last comment. If you go that route, I’d recommend using: This updates the existing PR just like The PR will refresh to the new commit set and CI will re-run as usual (once we give approval). |

What kind of change does this PR introduce?
Adding Blazor test app as request in #2450
What is the current behavior?
What is the new behavior?
What might this PR break?
Please check if the PR fulfills these requirements
Other information:
I took WpfTest app as a sample and mostly used the code from it. Changes that were made was to adopt code from WpfTest to Blazor specifics. Here is demonstration of the test app
Demonstration.mp4