Force Blazor component to rerender when url query changes - c#

Everything I've read points me to using NavigationManager.LocationChanged event, however that does not detect when only the query string changes. Therefore, nothing fires.
I've tried what is suggested here and that does not do what I am looking for.
I need my component to rerender everytime the query parameters in the current route changes. Has anyone been able to do this?
UPDATE:
Here is my code example:
#page "/accounts"
#if (Accounts != null)
{
<table>
<tbody>
#foreach (var account in Accounts)
{
<tr>
<td>
<NavLink href="/accounts/?accountId={account.AccountId}">#Account.Name</NavLink>
</td>
</tr>
}
</tbody>
</table>
}
#if (Account != null)
{
<div>
<h2>#Account.Name</h2>
</div>
}
#implements IDisposable
#inject NavigationManger NavManager
#inject AccountService AccountService
#code {
protected Guid Id { get; set; } = Guid.NewGid();
protected Guid AccountId { get; set; } = Guid.Empty;
protected List<AccountModel> Accounts { get; set; }
protected AccountModel Account { get; set; }
protected override void OnInitialized()
{
NavManager.LocationChanged += LocationChanged;
}
protected async override Task OnParametersSetAsync()
{
if (AccountId != Guid.Empty)
{
Account = await AccountService.GetAsync(AccountId);
}
else
{
Accounts = await AccountService.GetAllAsync();
}
StateHasChanged();
}
void LocationChanged(object sender, LocationChangedEventArgs e)
{
Id = Guid.NewGuid();
StateHasChanged();
}
void IDisposable.Dispose()
{
NavManager.LocationChanged -= LocationChanged;
}
}
The problem here is LocationChanged(object, LocationChangedEventArgs) does not fire when only the query parameters change. Which makes sense, the route is the same /accounts.
Bascically I have one page to show a list of Accounts and view a single account. I want the route to be /accounts when view the list and '/accounts?accountId=someAccountId' to be the route when im viewing one. Using standard NavLink component does not trigger a rerender of the component and neither does the LocationChanged event. Not even with setting a new Id value. Seeing as to how StateHasChanged only rerenders the component if a value has changed.

I ended up getting what I wanted to happen work like this:
#page "/accounts"
#if (Accounts != null)
{
<table>
<tbody>
#foreach (var account in Accounts)
{
<tr>
<td>
<NavLink href="/accounts/?accountId={account.AccountId}">#Account.Name</NavLink>
</td>
</tr>
}
</tbody>
</table>
}
#if (Account != null)
{
<div>
<h2>#Account.Name</h2>
</div>
}
#implements IDisposable
#inject NavigationManger NavManager
#inject AccountService AccountService
#code {
protected Guid Id { get; set; } = Guid.NewGid();
protected Guid AccountId { get; set; } = Guid.Empty;
protected List<AccountModel> Accounts { get; set; }
protected AccountModel Account { get; set; }
protected override void OnInitialized()
{
NavManager.LocationChanged += LocationChanged;
}
protected async override Task OnParametersSetAsync()
{
if (AccountId != Guid.Empty)
{
Account = await AccountService.GetAsync(AccountId);
}
else
{
Accounts = await AccountService.GetAllAsync();
}
StateHasChanged();
}
async void LocationChanged(object sender, LocationChangedEventArgs e)
{
if (AccountId != Guid.Empty)
{
Account = await AccountService.GetAsync(AccountId);
}
else
{
Accounts = await AccountService.GetAllAsync();
}
StateHasChanged();
}
void IDisposable.Dispose()
{
NavManager.LocationChanged -= LocationChanged;
}
}
I ended up just calling all the same code I want to run on a first render manually. I was hoping (falsely assuming) that I could tell it to just rereun the component lifecycle from the top automatically by changing one local property and then calling StateHasChanged.
Now when I toggle between "/accounts" and "/accounts?accountId=someId" the component rerenders as I want.

I do this in a wasm app works well
set this on your page where you want to receive the route parameter
#page "/pagename/{text?}"
<div>#Text</div>
then
#code {
[Parameter]
public string? Text { get; set; }
protected override void OnInitialized()
{
Text = Text ?? "fantastic";
}
protected override void OnParametersSet()
{
Text = Text ?? "Name Not Supplied";
}
}
The will re-render any data changes, AFAIK you can't force a component to re-render without reloading it, only the changed subscribers, which makes sense, as re-renders are expensive.
Doc's look under router parameters

You can bind the StateHasChanged() to the function/logic which changes your query string, which will re-render your component.

Related

How to fetch another url after the first is complete and display both in a foreach loop

I have a beginner question about Blazor with which I am struggling a bit.
The following is a sample code I just made to show the problem:
<ul>
#foreach(var student in classroom) {
}
</ul>
ClassRoomModel classroom;
protected override async Task OnInitializedAsync()
{
classroom = await ClassRommService.GetClassRoomDetails();
}
But inside my foreach loop, I want to make a second query that uses student.url to fetch all student details.
I tried creating another method GetStudentDetailsById(string url){} and tried to place it inside the foreach loop as:
#foreach(var student in classroom)
{
await GetStudentDetailsById(student.url)
...
}
But Blazor complains saying that it can not use the function inside the loop, and I think the approach might be an antipattern.
How do I create a function that waits until the classroom function returns a JSON and then sends another query to student.url to use in the for each loop.
Thank
You can create a new component called for example StudentListItem.razor and add the logic of fetching the student details inside it. Something like this:
StudentListItem.razor:
#if (studentDetails != null)
{
<li>
...
</li>
}
#code {
[Parameter]
public string Url { get; set; }
private string currentUrl;
private StudentDetails studentDetails;
protected override async Task OnParametersSetAsync()
{
if (currentUrl != Url)
{
currentUrl = Url;
studentDetails = await GetStudentDetailsById(currentUrl);
}
}
}
Then use like this:
<ul>
#foreach (var student in classroom) {
<StudentListItem Url="#student.url" />
}
</ul>
#code {
ClassRoomModel classroom;
protected override async Task OnInitializedAsync()
{
classroom = await ClassRommService.GetClassRoomDetails();
}
}
Edit:
Alternatively you can fetch all student details inside OnInitializedAsync and create a list of the results.
#if (studentDetailsList != null)
{
<ul>
#foreach (var studentDetails in studentDetailsList)
{
...
}
</ul>
}
else
{
<p>Loading...</p>
}
#code {
ClassRoomModel classroom;
IEnumerable<StudentDetails> studentDetailsList;
protected override async Task OnInitializedAsync()
{
classroom = await ClassRommService.GetClassRoomDetails();
var fetchStudentDetailsTasks = classroom.Select(student => GetStudentDetailsById(student.url));
studentDetailsList = await Task.WhenAll(fetchStudentDetailsTasks);
}
}

How to get the userId of logged in user blazor server in CODE BEHIND or .CS file

I have retrieved the userId in a Blazor component using the code below:
[CascadingParameter]
public Task AuthenticationStateTask { get; set; }
protected override async Task OnInitializedAsync()
{
YoogieContext cxt = new YoogieContext();
var authState = await AuthenticationStateTask;
var user = authState.User;
var UserStringId = user.FindFirst(c => c.Type.Contains("nameidentifier"))?.Value;
}
How do I get the userId from a code behind (.cs) file? Using Blazor Server.
Revised Answer
The cascaded Task<AuthenticationStateTask> is derived from the AuthenticationStateProvider so you might as well use it in code behind classes. You have to use it in service classes, there's no cacaded value to access.
The advantage of dropping back to the underlying AuthenticationStateProvider is you get access to the identity change events, so can react when the identity changes.
Here's a simple service to demonstrate interacting with the AuthenticationStateProvider:
public class MyIdentityService : IDisposable
{
private AuthenticationStateProvider _authenticationStateProvider;
public event EventHandler<IdentityChangedEventArgs>? IdentityChanged;
public MyIdentityService(AuthenticationStateProvider authenticationStateProvider)
{
// The dependancy container will inject it's AuthenticationStateProvider instance here
_authenticationStateProvider = authenticationStateProvider;
// register for the state changed event
_authenticationStateProvider.AuthenticationStateChanged += NotifyIdentityChanged;
}
// Method to get the current Identity
public async ValueTask<ClaimsPrincipal> GetCurrentIdentity()
{
AuthenticationState state = await _authenticationStateProvider.GetAuthenticationStateAsync();
return state.User;
}
// Fire and forget async event handler that waits on the Task completion before invoking the Event
private async void NotifyIdentityChanged(Task<AuthenticationState> newAuthStateTask)
{
AuthenticationState state = await newAuthStateTask;
this.IdentityChanged?.Invoke(null, new() { Identity = state.User});
}
// de-register for the state changed event
public void Dispose()
=> _authenticationStateProvider.AuthenticationStateChanged -= NotifyIdentityChanged;
}
// Custom Event Args class to pass the new identity
public class IdentityChangedEventArgs : EventArgs
{
public ClaimsPrincipal Identity { get; init; } = new ClaimsPrincipal();
}
registered in Program:
builder.Services.AddSingleton<WeatherForecastService>();
builder.Services.AddScoped<MyIdentityService>();
My Index.razor
#page "/"
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<SurveyPrompt Title="How is Blazor working for you?" />
<div class="alert alert-info">
Identity 1: #(this.Identity1.Identity?.Name ?? "No User Logged In")
</div>
<div class="alert alert-success">
Identity 2: #(this.Identity2.Identity?.Name ?? "No User Logged In")
</div>
And a code behind file that shows using the AuthenticationStateProvider directly and the MyIdentityService.
public partial class Index : ComponentBase, IDisposable
{
private ClaimsPrincipal Identity1 = new ClaimsPrincipal();
private ClaimsPrincipal Identity2 = new ClaimsPrincipal();
[Inject] private AuthenticationStateProvider _authenticationStateProvider { get; set; } = default!;
[Inject] private MyIdentityService _myService { get; set; } = default!;
protected async override Task OnInitializedAsync()
{
AuthenticationState state = await _authenticationStateProvider.GetAuthenticationStateAsync();
Identity1 = state.User;
_authenticationStateProvider.AuthenticationStateChanged += OnAuthenticationStateChanged;
Identity2 = await _myService.GetCurrentIdentity();
this._myService.IdentityChanged += OnIdentityChanged;
}
private async void OnAuthenticationStateChanged(Task<AuthenticationState> newAuthStateTask)
{
AuthenticationState state = await newAuthStateTask;
Identity1 = state.User;
// if you want to update the component when the identity changes
await this.InvokeAsync(StateHasChanged);
}
private async void OnIdentityChanged(object? sender, IdentityChangedEventArgs e)
{
Identity2 = e.Identity;
// if you want to update the component when the identity changes
await this.InvokeAsync(StateHasChanged);
}
public void Dispose()
{
_authenticationStateProvider.AuthenticationStateChanged -= OnAuthenticationStateChanged;
this._myService.IdentityChanged -= OnIdentityChanged;
}
}
Here's my test page - I've used Windows authentication for simplicity.

Issues with using cascading parameters for passing values

I have two blazor components the first component calls a method
<CascadingValue Value="this">
#ChildContent
</CascadingValue>
#code{
[Parameter] public RenderFragment? ChildContent { get; set; }
public Debtor? CurrentDebtor { get; private set; }
public async Task<Debtor> ConsolitdateCurrentDebtor(int debtorID)
{
Debtor? currentDebtor = null;
if (DebtorStoreList != null && DebtorStoreList.Count > 0)
{
currentDebtor = DebtorStoreList.Where(d => d.ID == debtorID).Single();
}
else
{
currentDebtor = await _debtorService.GetDebtorInformation();
}
CurrentDebtor = currentDebtor;
// StateHasChanged(); unless called no update takes place
return currentDebtor;
}
}
Now I have a property called CurrentDebtor in this AppState class. One blazor component comes and calls this method and sets the Current Debtor.
From the other component, i am doing something like
#code {
Debtor? debtor;
[CascadingParameter]
AppState AppState{ get; set; }
protected override async Task OnInitializedAsync()
{
debtor = AppState.CurrentDebtor;
}
}
However the CurrentDebtor is coming null, the only way it doesn't come null is if StateHasChanged() is called. Is there anyway without forcing it to call Statehaschanged to populate it correctly?
Trying to "wire" multiple components together using cascading parameters and two way binding isn't always the best way to do things, particularly sub-component to sub-component within a page (which it appears you are trying to do from the information shown in the question).
There is an alternative approach using the standard event model.
My Debtor class
public class Debtor
{
public Guid Id { get; set; }
public string? Name { get; set; }
}
A Debtor view service:
public class DebtorViewService
{
public Debtor Record { get; private set; } = default!;
public event EventHandler<Debtor>? DebtorChanged;
public DebtorViewService()
{
// Get and assign the Debtor Store DI service
}
public async ValueTask GetCurrentDebtor(Guid debtor)
{
// Emulate getting the Debtor from the Debtor Service
await Task.Delay(1000);
this.Record = new Debtor() { Name=$"Fred {DateTime.Now.ToLongTimeString()}", Id = Guid.NewGuid()};
this.DebtorChanged?.Invoke(this, this.Record);
}
}
Registered in services like this:
builder.Services.AddScoped<DebtorViewService>();
A Component to get the Debtor:
GetDebtor.razor
#inject DebtorViewService Service
<div class="m-2 p-3 bg-dark text-white">
<h5>GetDebtor Component</h5>
<div>
<button class="btn btn-secondary" #onclick=this.GetCurrentDebtor>Get Debtor</button>
</div>
</div>
#code {
private async Task GetCurrentDebtor()
=> await Service.GetCurrentDebtor(Guid.Empty);
}
A component to show the Debtor:
ShowDebtor.razor
#inject DebtorViewService Service
#implements IDisposable
<div class="m-2 p-3 bg-info">
<h5>ShowDebtor Component</h5>
#if (this.Service.Record is not null)
{
<div>
Id : #this.Service.Record.Id
</div>
<div>
Name : #this.Service.Record.Name
</div>
}
</div>
#code {
protected override void OnInitialized()
=> this.Service.DebtorChanged += this.OnDebtorChanged;
private void OnDebtorChanged(object? sender, Debtor value)
=> this.InvokeAsync(this.StateHasChanged);
public void Dispose()
=> this.Service.DebtorChanged -= this.OnDebtorChanged;
}
And a demo page:
#page "/"
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<GetDebtor />
<ShowDebtor />
<SurveyPrompt Title="How is Blazor working for you?" />
#code{
}
The data and the change event is now in a single shared (Scoped) DI service instance that everyone has access to.
You should be able to build your components based on this code.

Having trouble getting Blazor to rerender component on change in state shared between components

Context
I'm following a pattern that's something like https://chrissainty.com/3-ways-to-communicate-between-components-in-blazor/ or https://jonhilton.net/blazor-state-management/
So I have two razor components Hen.razor and Basket.razor as child components inside index.razor. A button inside Hen adds to the number of eggs displayed inside Basket.
The button calls a service I've injected called EggService that handles the number of eggs in Basket by storing it in local storage using Blazored.LocalStorage.
Problem
Clicking the button increases the number of eggs in local storage but doesn't update the Basket component unless I refresh.
Code
Repository for convenience: https://github.com/EducatedStrikeCart/EggTest/
EggService:
using Blazored.LocalStorage;
namespace BlazorSandbox.Services
{
public class EggService
{
public event Action OnChange;
private readonly ILocalStorageService _localStorageService;
public EggService(ILocalStorageService localStorageService)
{
this._localStorageService = localStorageService;
}
public async Task<int> GetEggs()
{
int currentEggs = await _localStorageService.GetItemAsync<int>("Eggs");
return currentEggs;
}
public async Task AddEgg()
{
int newEggs = await GetEggs();
if (newEggs == null)
{
newEggs = 0;
} else
{
newEggs += 1;
}
await _localStorageService.SetItemAsync("Eggs", newEggs);
OnChange?.Invoke();
}
}
}
Hen:
#using BlazorSandbox.Services
#inject EggService EggService
<div>
<h3>Hen</h3>
<button #onclick="TakeAnEgg">Take an egg</button>
</div>
#code {
public async Task TakeAnEgg()
{
await EggService.AddEgg();
}
}
Egg:
#using BlazorSandbox.Services
#inject EggService EggService
#implements IDisposable
<div>
<h3>Basket</h3>
Eggs: #Eggs
</div>
#code {
public int Eggs { get; set; }
protected override async Task OnInitializedAsync()
{
Eggs = await EggService.GetEggs();
EggService.OnChange += StateHasChanged;
}
public void Dispose()
{
EggService.OnChange -= StateHasChanged;
}
}
Index:
#page "/"
#using BlazorSandbox.Services
#inject EggService EggService
<h1>
Eggs!
</h1>
<div class="d-flex flex-row justify-content-around">
<Hen />
<Basket />
</div>
#code {
}
Program.cs:
using BlazorSandbox;
using BlazorSandbox.Services;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Blazored.LocalStorage;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<EggService>();
builder.Services.AddBlazoredLocalStorage();
await builder.Build().RunAsync();
Solution
Special thank you to person who deleted their comment. I'm kind of new to asking questions on StackOverflow so I'm sorry if I should've selected your answer as the Answer!
#code {
public int Eggs { get; set; }
protected override void OnInitialized()
{
//Subscribe
EggService.OnChange += UpdateEgg;
//Set value of Eggs on load
UpdateEgg();
}
public void UpdateEgg()
{
// Set value of Eggs to new value and trigger component rerender
InvokeAsync(async () => {Eggs = await EggService.GetEggs(); StateHasChanged(); });
}
public void Dispose()
{
// Unsubscribe
EggService.OnChange -= UpdateEgg;
}
}
There are a few oddities in your code.
if (newEggs == null)
This is an int, so it can never be null. The default value for int is 0. You should be seeing a warning for this.
Eggs = await EggService.GetEggs();
After you set Eggs here, you never update it anywhere in your code! So even if you call StateHasChanged, there is nothing to update.
What you will want to do is keep track of the egg count inside of your EggService and then inside of your Basket component you will need a way to know that the egg count has increased so you can update your Egg property and then call StateHasChanged. Let me know if you need help with this.

How to implement custom authorization filter for Blazor page

Blazor server-side, .NET Core 3.1.x
Look over the examples on authorization, I am trying to get a solution for a custom authorization filter/attribute. I simply need to check the user identity during Authorization.
https://learn.microsoft.com/en-us/aspnet/core/security/blazor/?view=aspnetcore-3.1
At the top of a Blazor page, after #page
#attribute [MyAuthFilter]
The filter. OnAuthorization never gets hit however.
public class MyAuthFilter: AuthorizeAttribute,IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
var httpContext = context.HttpContext;
// get user name
string userName = httpContext.User.Identity.Name;
// todo - call method to check user access
// check against list to see if access permitted
//context.Result = new UnauthorizedResult();
}
}
The following code snippet describes how you can perform the authorization process, and how and where to display content for authorized users. You can built your own component based on the code shown here:
Profile.razor
#page "/profile"
#page "/profile/{id}"
<AuthorizeView Policy="CanEditProfile" Resource="#ID">
<NotAuthorized>
<h2 class="mt-5">You are not authorized to view this page</h2>
</NotAuthorized>
<Authorized>
<div class="container my-profile">
<h2>My Profile</h2>
--- Place here all the content you want your user to view ----
</div>
</Authorized>
</AuthorizeView>
#code {
[Parameter]
public string ID { get; set; }
}
ProfileHandler.cs
public class ProfileHandler : IAuthorizationHandler
{
public Task HandleAsync(AuthorizationHandlerContext context)
{
if (context.User != null)
{
var pendingRequirements = context.PendingRequirements.ToList();
foreach (var requirement in pendingRequirements)
{
if (requirement is ProfileOwnerRequirement)
{
// get profile id from resource, passed in from blazor
// page component
var resource = context.Resource?.ToString();
var hasParsed = int.TryParse(resource, out int
profileID);
if (hasParsed)
{
if (IsOwner(context.User, profileID))
{
context.Succeed(requirement);
}
}
}
}
}
return Task.CompletedTask;
}
private bool IsOwner(ClaimsPrincipal user, int profileID)
{
// compare the requested memberId to the user's actual claim of
// memberId
// var isAuthorized = context.User.GetMemberIdClaim();
// now we know if the user is authorized or not, and can act
// accordingly
var _profileID = user.GetMemberIDClaim();
return _profileID == profileID;
}
}
ProfileOwnerRequirement.cs
public class ProfileOwnerRequirement : IAuthorizationRequirement
{
public ProfileOwnerRequirement() { }
}
Startup class
services.AddSingleton<IAuthorizationHandler, ProfileHandler>();
services.AddAuthorization(config =>
{
config.AddPolicy("CanEditProfile", policy =>
policy.Requirements.Add(new ProfileOwnerRequirement()));
});
Hope this helps!
If you just want to validade one kind of requirement you can simplify #enet response replaceing ProfileHandler.cs with this:
public class ProfileHandler : AuthorizationHandler<ProfileOwnerRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ProfileOwnerRequirement requirement)
{
if (context.User.Identity.IsAuthenticated)
{
var resource = context.Resource?.ToString();
var hasParsed = int.TryParse(resource, out int
profileID);
if (hasParsed)
{
if (IsOwner(context.User, profileID))
{
context.Succeed(requirement);
}
}
}
return Task.CompletedTask;
}
private bool IsOwner(ClaimsPrincipal user, int profileID)
{
var _profileID = user.GetMemberIDClaim();
return _profileID == profileID;
}
}
With HandleRequirementAsync, it only gets called once while if you use HandleAsync, it gets called multiple times, of course for simple validations makes no difference but if you have complex validations they will be run multiple times.

Categories