C
C#•2mo ago
Jip

Question about reactivity in Blazor

Hi, I'm working on a Blazor project. I have some experience with C# in general and professionally I work with Angular. Specifically Angular recently tackled a big issue with reactivity with Signals. Before that I often used RxJS, and in .Net a similar implementation of RxJS is System.Reactive. In Blazor I do not fully understand how the reactivity works out of the box, without using any additional libraries. The goal of the project is to be able to parse replays of the game Supreme Commander: Forged Alliance and to show various information about the replay on different pages. Just like in Angular, one can use a service to deal with data that needs to exist across pages. To set that up, in program.cs I have the following snippet to make sure the replay service is a singleton:
builder.Services.AddSingleton<ReplayService>();
builder.Services.AddSingleton<ReplayService>();
The replay service itself is quite simple, just a C# class that uses a library to load in the replay from a binary blob:
namespace FAForever.Replay.Viewer.Services
{
public class ReplayService
{

public Replay? Replay { get; set; }

public void LoadReplay(MemoryStream stream)
{
this.Replay = ReplayLoader.LoadFAFReplayFromMemory(stream);
}
}
}
namespace FAForever.Replay.Viewer.Services
{
public class ReplayService
{

public Replay? Replay { get; set; }

public void LoadReplay(MemoryStream stream)
{
this.Replay = ReplayLoader.LoadFAFReplayFromMemory(stream);
}
}
}
Now, in a Blazor page I can do the following and that appears to work just fine:
@page "/"
@inject ReplayService replayService;

<PageTitle>Home</PageTitle>

<h1>Hello, world!</h1>

@if (replayService.Replay != null)
{
<p role="status">Current count: @replayService.Replay.Events.Count</p>
}
else
{
<p role="status">No replay loaded</p>
}

<p>@UserInputCount</p>


<InputFile OnChange="HandleFileSelected" /><br />

@code {

private int UserInputCount = 0;

public async Task HandleFileSelected(InputFileChangeEventArgs args)
{
if (args.File.Name.ToLower().EndsWith(".fafreplay"))

{
IBrowserFile file = args.File;
MemoryStream stream = new MemoryStream();
await file.OpenReadStream(4 * 512000).CopyToAsync(stream).ConfigureAwait(false);
stream.Position = 0;
replayService.LoadReplay(stream);
if (replayService.Replay != null){
UserInputCount = replayService.Replay.Events.Count;
} else {
UserInputCount = -1;
}
}
}
}
@page "/"
@inject ReplayService replayService;

<PageTitle>Home</PageTitle>

<h1>Hello, world!</h1>

@if (replayService.Replay != null)
{
<p role="status">Current count: @replayService.Replay.Events.Count</p>
}
else
{
<p role="status">No replay loaded</p>
}

<p>@UserInputCount</p>


<InputFile OnChange="HandleFileSelected" /><br />

@code {

private int UserInputCount = 0;

public async Task HandleFileSelected(InputFileChangeEventArgs args)
{
if (args.File.Name.ToLower().EndsWith(".fafreplay"))

{
IBrowserFile file = args.File;
MemoryStream stream = new MemoryStream();
await file.OpenReadStream(4 * 512000).CopyToAsync(stream).ConfigureAwait(false);
stream.Position = 0;
replayService.LoadReplay(stream);
if (replayService.Replay != null){
UserInputCount = replayService.Replay.Events.Count;
} else {
UserInputCount = -1;
}
}
}
}
However, in the navigational menu I want to hide some items until there is a replay in the service. It won't make a lot of sense to show the build orders tab when there is no replay. So I wrote it as such:
@inject ReplayService replayService;

(...)

<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
<nav class="flex-column">

(...)

@if (replayService.Replay != null)
{
<div class="nav-item px-3">
<NavLink class="nav-link" href="chat">
<span class="bi bi-activity" aria-hidden="true"></span> Build orders
</NavLink>
</div>
}
</nav>
</div>
@inject ReplayService replayService;

(...)

<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
<nav class="flex-column">

(...)

@if (replayService.Replay != null)
{
<div class="nav-item px-3">
<NavLink class="nav-link" href="chat">
<span class="bi bi-activity" aria-hidden="true"></span> Build orders
</NavLink>
</div>
}
</nav>
</div>
Now back to reactivity - I noticed that the page updates immediately once the replay is loaded. All good there. But the navigational menu is only updated after interacting with it in some fashion, such as by clicking on it. That's of course not what I want - once a replay is available, it should update automatically. Secretly I was hoping that Blazor was as reactive as the Angular Signals, without the boilerplate of an RxJS-like library. But the current behavior is not sufficient. Where can I read up about reactivity in Blazor, and in general, why is the navigational menu not updating in my example? And should I still use a reactive library? Or am I just making a rookie mistake? 😄
GitHub
Build software better, together
GitHub is where people build software. More than 100 million people use GitHub to discover, fork, and contribute to over 420 million projects.
From An unknown user
From An unknown user
From An unknown user
14 Replies
Joschi
Joschi•2mo ago
The problem is, that your parent component / layout cannot know that your state has changed. A simple but heavy handed approach would be to call StateHasChanged() after loading the file inside your page code. But there are nearly always better solutions. I suspect establishing an CascadingParameter wrapping your entire app instead of using a service may work.
Jip
Jip•2mo ago
ASP.NET Core Blazor cascading values and parameters
Learn how to flow data from an ancestor Razor component to descendent components.
Joschi
Joschi•2mo ago
Yes, but thats just a guess.
Jip
Jip•2mo ago
I'm trying to understand it, thank you for your time. I'm reading up on this: - https://chrissainty.com/understanding-cascading-values-and-cascading-parameters/ I think cascading values work between components that are in a hierarchy. But my service is injected and therefore not part of the hierarchy. I'm trying to apply it there too, but it's not quite working out. You mentioned that StateHasChanged is a heavy-handed approach, what makes you say that?
Chris Sainty - Building with Blazor
Understanding Cascading Values & Cascading Parameters
In this post I provide an overview of cascading values and cascading parameters, what they are, how you can use them and some potential drawbacks.
Jip
Jip•2mo ago
Oh wait, maybe I missunderstood the cascading parameters. Let me re-try.
Jip
Jip•2mo ago
ASP.NET Core Blazor cascading values and parameters
Learn how to flow data from an ancestor Razor component to descendent components.
Jip
Jip•2mo ago
Pharaz Fadaei
Automatic Component State Management in Blazor using INotifyPropert...
In Blazor, changes to the state of a component become visible to the user when the component re-renders. Blazor has some conventions to make a component automatically re-render when it thinks there is a chance for the component's state to be changed…
Joschi
Joschi•2mo ago
https://jonhilton.net/blazor-rendering/ Maybe this is of interest. Tbh I may just misremember an article I have read some time ago, about StateHasChanged being quite expensive. Need to read more about ist myself.
State Hasn't Changed? Why and when Blazor components re-render
Sure you could just keep throwing StateHasChanged calls at your component until it finally re-renders, but what's really going on behind the scenes?
Jip
Jip•2mo ago
Article is book marked, I'm also looking at: - https://www.youtube.com/watch?v=EUOimtP78jQ
dotnet
YouTube
On .NET Live - Building Reactive UIs with Blazor
In this live session, Rodney Littles II shows us how to build Blazor applications with the ReactiveUI framework Featuring: Rodney Littles II (@rlittlesii), Cecil Phillip (@cecilphillip) Get your questions answered on the Microsoft Q&A for .NET - https://aka.ms/dotnetqa ​ Learn .NET with free self-guided learning from Microsoft Learn: http://ak...
Jip
Jip•2mo ago
This article is excellent and it helped a lot with understanding change detection I also understand now why you mentioned calling StateHasChanged manually, since that is what happens in the background I also now better understand why this library exists: - https://github.com/phorks/phork-blazor-reactivity/ And in action: - https://github.com/phorks/phork-blazor-reactivity/blob/main/docs/IN-ACTION.md I do feel this perhaps should've been part of Blazor to begin with 🤔 , it requires a lot of boiler plate to have global state in the current default setup I've implemented the Phork library, see also: - https://github.com/Garanas/scfa-cs-replay/pull/3/commits/69330fb632854a457b1481c2c06078680f44cb87 Using the getting started guide (the readme) and the action guide of the repository. It now works as I'd expect it to and the syntax is relatively minimal Thank you @Joschi for taking the time for this little journey here, I'd like to credit you in the readme. Is there a name that you'd like me to use, or shall I go with your Discord tag? Assuming here that you'd like to be mentioned 🙂
Joschi
Joschi•2mo ago
Thanks for offering. I don't think that you have to credit me for writing a few tidbits and throwing you a link. But if you insist feel free to link my github account. https://github.com/JoschiZ
GitHub
JoschiZ - Overview
JoschiZ has 23 repositories available. Follow their code on GitHub.
Jip
Jip•2mo ago
I've mentioned you in the credits section of the readme: https://github.com/Garanas/scfa-cs-replay I may update to make it look better in the future 🙂 I'll be keeping track of various references that I found useful on the Wiki: - https://github.com/Garanas/scfa-cs-replay/wiki/Blazor And the page exists and is 'functional': - https://garanas.github.io/scfa-cs-replay/ Where 'functional' means that all the complicated unknowns are resolved, and now it's about filling in the details 😄
GitHub
GitHub - Garanas/scfa-cs-replay: A library to read and interpret re...
A library to read and interpret replay files of Supreme Commander: Forged Alliance (Forever) - Garanas/scfa-cs-replay
GitHub
Blazor
A library to read and interpret replay files of Supreme Commander: Forged Alliance (Forever) - Garanas/scfa-cs-replay
Jip
Jip•2mo ago
Thanks again!
Want results from more Discord servers?
Add your server