Reputation: 7403
I've been struggling with a couple of page navigation issues when developing a blazor webassembly.
every time I go from page1 to page2 and then I press the browser's back button the page1 is basically reset, this because the on_initialized is called, how do you guys keep the status of the page when pressing the browser's back button?
if I have a page like @page '/person/{id}'
, if from that page I have a link like <NavLink href="/person/person-id-2">Another person</NavLink>
. When I click on the link I see the URL updating but the actual page does not refresh, how do you guys go around this matter?
=== EDIT ===
after the answer from @mrc-aka-shaun-curtis I looked back to my code, at to fix #2 I moved some code around. Before I had something like
protected override async Task OnInitializedAsync()
{
PersonModel = await PersonService.Get<Person>(PersonId);
editContext = new EditContext(PersonModel);
}
I now moved this to
protected override async Task OnParametersSetAsync()
{
PersonModel = await PersonService.Get<Person>(PersonId);
editContext = new EditContext(PersonModel);
StateHasChanged();
}
This sorted #2, I can have a <NavLink href="/person/2">Father</NavLink>
in the component "person"
Upvotes: 6
Views: 7474
Reputation: 5227
I feel like using the state management is an overkill in this scenario. I am willing to create the same functionality basically.
list of clickable items. you click on the item, then go back and brought to the exact same spot without page reload.
to me it looks like you could implement this by having 2 different sections on the same page
when traversing from 1 section to another we could use JS to push the new browser history state and then subscribe to state pop event. Here is the minimal working solution (I use the standard WASM .Net 5 template with counter page).
put this into index.html
<script>
var _instanceRef;
function setInstance(instanceRef) {
_instanceRef = instanceRef;
}
function setLocation(url) {
window.history.pushState('todo meta information', 'Title', url);
}
window.onpopstate = function (e) {
_instanceRef.invokeMethodAsync('GoBack');
};
</script>
and this is the modified counter.razor file
@page "/counter"
@inject NavigationManager NavigationManager
@inject IJSRuntime JSRuntime
<h1>Counter</h1>
<button class="btn btn-primary @IsActive(isFeedActive)" id="pills-contact-tab" type="button" aria-selected="@isFeedActive" @onclick="NavigateToFeed">
Feed
</button>
<button class="btn btn-primary @IsActive(isFeedItemActive)" id="pills-contact-tab" type="button" aria-selected="@isFeedItemActive" @onclick="NavigateToItem">
Feed item
</button>
<div class="tab-content" id="pills-tabContent">
<div class="tab-pane fade @IsShowActive(isFeedActive)" id="pills-home" role="tabpanel" aria-labelledby="pills-home-tab">
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button></div>
<div class="tab-pane fade @IsShowActive(isFeedItemActive)" id="pills-profile" role="tabpanel" aria-labelledby="pills-profile-tab">
loadable item contents here.
</div>
</div>
@code {
public bool isFeedActive = true;
public bool isFeedItemActive = false;
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
protected override async Task OnInitializedAsync()
{
await JSRuntime.InvokeVoidAsync("setInstance", DotNetObjectReference.Create(this));
}
public async Task NavigateToFeed()
{
isFeedActive = true;
isFeedItemActive = false;
await JSRuntime.InvokeVoidAsync("setLocation", $"/counter");
}
public async Task NavigateToItem()
{
isFeedActive = false;
isFeedItemActive = true;
await JSRuntime.InvokeVoidAsync("setLocation", $"/counter/item/56");
}
[JSInvokable]
public async Task GoBack()
{
isFeedActive = true;
isFeedItemActive = false;
StateHasChanged();
}
private string IsActive(bool check)
{
return check ? "active" : "";
}
private string IsShowActive(bool check)
{
return check ? "show active" : "";
}
}
steps are
note that I am also using bootstrap 5 pills in this example. Follow the official website for latest CSS and JS links from CDN and place them into index.html
This solution requires some (a complete) rework. Adding validations etc. Also instance should be disposed so probably JS ES6 module shall be used instead (google JS isolation in Blazor
). But it's only PoC at this point so please don't throw rocks at me :)
edit:
also please note that window.history.pushState
is probably HTML5 only function (double check this).
Also, the page that you are "locating to" should be reachable after you refresh the page! i.e. if you open the "single item" section at location "/counter/pagethatdoesnotexists" and user refreshes the page - the user will be presented with page not found error. This is not really an issue if you back your logic up with pages that actually exist.
I have just figured that this solution will only work when user goes back in time 1 time. If user clicks forward after he clicked backward button - this will trigger the page loading and the state will be lost.. Still better than nothing, but if anybody has any improvement suggestions I would like to know them :)
edit2 sorry I think I have overly complicated this task. We could simply use optional route parameters to set the item id.
url
@page "/counter2/{Id:long?}"
and the rest of the code
[Parameter]
public long? Id { get; set; }
protected override async Task OnParametersSetAsync()
{
if (Id != null)
{
isFeedActive = false;
isFeedItemActive = true;
}
else
{
isFeedActive = true;
isFeedItemActive = false;
}
}
public async Task NavigateToFeed()
{
NavigationManager.NavigateTo("counter2");
}
public async Task NavigateToItem()
{
NavigationManager.NavigateTo("counter2/5");
}
this way the page reload is not happening (i.e. you keep the state), same as in solution 1. ALSO, if user clicks on forward button in browser - same page will be loaded. When user opens the link directly, while no "item list" content is loaded, you might want to exclude initial page contents loading depending on your requirements. Alternatively redirect user to dedicated item details page in this case.
Upvotes: 1
Reputation: 2800
Use a state manager (with a storage or not, is the same)
Look at Carl Franklin Blazor Train episode about application state https://youtu.be/BB4lK2kfKf0 and Microsoft official documentation https://learn.microsoft.com/en-us/aspnet/core/blazor/state-management?view=aspnetcore-5.0&pivots=webassembly
The page is the same (like said Shaun), in case you need control on this I think you need to use OnAfterRender, and obviously, see Shaun, the OnParameter
Upvotes: 2
Reputation: 30046
Question 1 - you haven't made clear the urls for page 1 and page 2, so it's hard to answer the question. But if the "page" changes i.e. the razor component, then the full lifecycle of page 1 gets called.
Question 2 - /person/1
and /person/2
are the same page if @page "/person/{id}"
. The component is the same, only the parameter is changing. RouteView
in App.razor
is being passed the same component by the router so only OnParametersSet{Async}
is called as only the parameter has changed. It's only when the component defined in RouteData
changes that the new component gets loaded and OnInitialized{Async}
on that component is called.
Upvotes: 4