Flash of unloaded content when navigating in Blazor Server - c#

I have some Blazor Server UI that's displaying some user-specific content:
<AuthorizeView>
<Authorizing>
<span class="spinner"></span>
</Authorizing>
<NotAuthorized>
not logged in
</NotAuthorized>
<Authorized>
logged in as #context.User.Identity!.Name
</Authorized>
</AuthorizeView>
I'm currently using a custom AuthenticationStateProvider to track the login session. It stores a user token in browser session storage on login and during first render it fetches the token and retrieves the session info on the backend. This much is all working fine.
If the service is recreated and the state queried before first render, since it can't access JS yet, it simply returns "not logged in" and then requeries after first render. (As far as I can tell it's not possible to keep it in the "authorizing" state by not completing the Task as this stalls the prerender.)
private readonly ProtectedSessionStorage _Storage;
private bool _HasLoaded;
public AuthorizationService(ProtectedSessionStorage storage) => _Storage = storage;
public void AfterFirstRender()
{
_HasLoaded = true;
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
if (!_HasLoaded) // prerendering
{
return new AuthenticationState(new ClaimsPrincipal());
}
// ... code to fetch the real identity from _Storage
}
The problem is that when the user clicks an ordinary local navigation link (page to page within the same app via <a href="pageX">), this appears to tear down and rebuild all the scoped services, so it loses the authentication state and has to query it again -- but since it can't do that before first render, there is always a flash of "not logged in" on every navigation before it finishes reloading the state and updates, which is ugly. (It's very fast, but still visible. It's more visible if you deliberately add a delay to the process.)
I do have prerendering enabled (as per default) but I'm surprised that it seems to be triggering a full prerender+reload on every navigation instead of preserving state. (I expect that on a full reload, or manual location edit, but not from internal navigation.)
When using WebAssembly, this just worked -- both singletons and scopes were per-session and navigations happened internally without discarding any state. Not so with Server, apparently; singletons survive but are larger than per-user or per-session, and scopes seem to be per-page, which is smaller than per-session.
Am I missing some key trick to get this to behave sensibly? Either to avoid the re-prerender on navigate or to keep scoped service states, or reload them reliably before JS render, or pass some kind of session id from pre-navigation to prerendering? The official docs basically just "this exercise is left to the reader", but without at least some kind of id preservation I don't see how it's possible.

Blazor server calls GetAuthenticationStateAsync() upon a new visit. This is the place to look for a previously saved cookie and authenticate the user accordingly. Next code will give you the clue.
First time it will silently catch an exception due to missing JS but it is immediately called a second time successfully:
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
//get claimsPrincipal from stored user if within a session
//or get it from cookie if a new session
//return AuthenticationState from claimsPrincipal
//or return anonymous if user throws error on modifying sessionstore by hand
try
{
var storedUser = await TryGetUserFromBrowserStorage();
if (storedUser == null) storedUser = await TryGetUserFromPersistedCookie();
var claimsPrincipal = IdentityFromUserAccount(storedUser);
return await Task.FromResult(new AuthenticationState(claimsPrincipal));
}
catch
{
//silently fails on prerrendering since JS is not available yet
return await Task.FromResult(new AuthenticationState(_anonymous));
}
}

Related

Is it ok to use OnAfterRenderAsync() instead of OnParametersSetAsync() for database calls in Blazor Server?

I am reading that the recommendation is to use OnParametersSetAsync() for any async calls (such as database calls) inside a Blazor server component. The problem though is that OnParametersSetAsync() will fire twice in a default Blazor Server application, and I obviously don't want to access the database twice.
To avoid this problem, some people recommend replacing the "ServerPrerendered" render-mode in _Host.cshtml to "Server", which will avoid OnParametersSetAsync() firing twice. However, I don't like this solution, because the application will take longer to load for the user if we removing the initial static HTML phase.
So, my solution so far has been to put my database access calls inside OnAfterRenderAsync(), and to call StateHasChanged() once I am done. It looks something like this:
public partial class ExampleComponent
{
[Inject]
public IUserDataAccess UserAccess { get; set; }
[Parameter]
public string UserEmail { get; set; }
public IUser User { get; set; }
private bool _isLoading = true;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender == false)
return;
User = await UserAccess.GetOneAsync(UserEmail);
_isLoading = false;
StateHasChanged();
}
}
The database call gets made only once, because of the "if (firstRender == false)" condition.
This approach has been working well for me so far, but I suspect that there is something wrong with this way of proceeding, because the examples given online of a valid call to OnAfterRenderAsync() usually only mention JavaScript calls. I don't why that is though. Is there anything wrong with the example code I am giving above? And if this approach is not recommended for some reason, then how can we avoid the double call to the database if we use OnParametersSetAsync() (excluding the server-mode change discussed above)?
Thanks.
Everyone gets hung up on the initial double load and make huge design compromises that they don't need to. [Sorry, and please don't take this the wrong way]This is a probably a good example.
If you're using Blazor Server then, yes the SPA "Start page" gets rendered twice - once on the static first load and once when the SPA starts in the page.
But that's it. Everything after that only gets rendered once. If you are so hung up on that initial load, don't do "too much" in the landing page, make it "light".
To restate - This only happens once, every other routing component load in your SPA only happens once unless you force a re-load or hit F5.
On putting lots of stuff in OnAfterRender{Async}, in general DON'T. It's designed to handle after render activity such as JSInterop stuff. Doing things that then need a call to StateAndChanged is a bit self defeating!
All your once only Db stuff should be in OnInitialized{Async} - normally OnInitializedAsync as it will be async code. You have little control over when OnParametersSet{async} gets run: code in here gets called whenever the component is re-rendered.

How can i navigate to another blazor page but without state reset

I have a script that does a while loop and does some requests
<p>Result #Content</p>
private string Content;
public async Task StartAsync()
{
while(!taskCanel) {
Content = await webSocket.SendAsync(..);
StateHasChanged();
await Task.Delay(15000);
}
}
but whenever I switch another page with my navbar then go back to this page it reset everything like it creates a new instance of my razor page.
How can I navigate to the page without losing all states and content?
While you are navigating to another page all variables from the current page should be disposed. It is the standard procedure. The state management works with a different approach. If you want to retain the state of the current page then you have to subscribe to the changes in a global object and pull the change onInit hook or onafterrender hook. There is many more library for state management. Some of them are introduced here
You need to separate your data and your presentation. Your data, how you get it and manage it belongs in a scoped DI service. Your UI accesses this DI Service through inject and displays information from the service.

Using async-await to wait for another view to load

I'm writing a Windows Phone 8 app that makes use of a REST API to gather data. The API requires authentication, so whenever I try to call the REST API, I have to check whether the user is authenticated. If the user isn't authenticated, then I want to bring up a login view and let the user authenticate. It's not HTTP authentication, but it uses a custom login screen.
My problem is: I would like to use await to wait for the authentication to happen, but I don't know how to do that since I have to bring up another view. Here's pseudo-code for what I would like to do:
The LoadData method:
async Task LoadDataAsync() {
bool authenticated = await AuthenticateAsync();
if (authenticated) {
// do REST API stuff
}
}
And the AuthenticateAsync method:
async Task<bool> AuthenticateAsync() {
if (alreadyAuthenticated)
return true;
// not authenticated, so bring up a login view to let the user log in
// How do I do this in the context of async-await?
}
So in the AuthenticateAsync method I would like to bring up a login view if the user needs to authenticate. But I can't call NavigationService.Navigate() because that's not async-await friendly and I wouldn't be able to await anything.
What's the right way to do this?
I found an answer to this. I ended up using the answer here: https://stackoverflow.com/a/12858633/225560
Specifically, I used a TaskCompletionSource<bool> object, which I could then await. I then navigated to the login view, and it called me back when login was successful. I then set the result of the TaskCompletionSource, which freed up my await and let the method continue. This ended up working almost like a delegate in objective-C, in that the Login view called a method back on the initial class when login completed.

CurrentUser custom identity and attributes

REWRITTEN QUESTION
I have an ASP.NET MVC 4 site that uses forms auth.
It also needs to retrieve custom user object from a service call and then set it to the HttpCurrent.User.Context.
this works fine but I realised that when it hits the post authenticate request that it will hit it several times per request - not good.
Global.asax.cs:
protected void Application_PostAuthenticateRequest(object sender, EventArgs e)
{
if (User.Identity.IsAuthenticated)
{
IIdentity ui = HttpContext.Current.User.Identity;
MyMembershipUser myUser = new MyMembershipUser (ui.Name);
MyCustomPrincipal myPrincipal = new MyCustomPrincipal (ui, myUser);
HttpContext.Current.User = myPrincipal;
}
}
I cant entirely cache the user for a number of reasons so lets not go there.
so this gets executed a few times per request. This means for every hit, it calls the DB.
Some views on the site use the custom principal to display some user specific details only if they are authenticated. if they aren't, then it wont display it. But if they are authenticated, it gets the principal and casts it to "MyCustomPrincipal" so I can grab the properties I need to display.
How can I prevent these multiple hits?
I tried creating a custom Authorize attribute and doing the above code in there, it works but fails when it renders the view which can see the user is authenticated but fails to do the cast because at that point, the User Identity/principal is still set to the Generic principal.
typical code in the view:
#if (Helpers.UserContext.IsAuthenticated)
{
#: tmpStatus = '#Helpers.UserContext.User.Status';
}
UserContext.IsAuthenticated just returns HttpContext.Current.User.Identity.IsAuthenticated
User in UserContext does the casting:
return HttpContext.Current.User as MyCustomPrincipal
I hope this clarifies the question more!
I want to avoid multiple hits happening on the PostAuthenticateRequest but not sure why those hits are happening. I am not even sure if it is the right place to place it. I want to make sure that the Context User is all setup for subsequent accesses/requests to it without having to call the service layer to get the details again.
thanks
you minimise some action by check if authenticated
//assuming something like....
public override void Init() {
base.Init();
// handlers managed by ASP.Net during Forms authentication
PostAuthorizeRequest += new EventHandler(PostAuthHandler);
}
// try screen out some calls that arent authenticated yet.
public void PostAuthHandler(object sender, EventArgs e) {
if (Request.IsAuthenticated) {
//.... try a break to see how often
}
}
EDIT: But careful of multiple hits due to script and content bundling / loading.
Check the Request.Url value. Is it changing.
Also Note the thread Id. See Debug.Windows.Threads
The thread may also be changing.
Consider thread safety before you attempt any caching / global singletons etc.
You may wish consider moving some code to a controller Or Base Controller

FormsAuthentication.RedirectToLoginPage() does nothing

I have the following problem: republishing ASP.NET web app causes (as expected) session resetting where I keep additional user info (what on access attempt will cause NullReferenceException).
To avoid that, my page checks this info existence and in case of null redirects user to the login page (forms auth), so I'm calling:
void LogOut()
{
Session.Clear();
Session.Abandon();
User = null;
FormsAuthentication.SignOut();
FormsAuthentication.RedirectToLoginPage()
}
But sometimes it doesn't help, so I found a workaround:
Response.Redirect(FormsAuthentication.LoginUrl);
but it doesn't add returnUrl, what I wish it were (I don't want to emulate this behavior manually).
So want to figure out why does the first way doesn't work as expected.
Have you tried calling Response.End() after FormsAuthentication.RedirectToLoginPage() ?
I have the following problem: republishing ASP.NET web app causes (as expected) session resetting where I keep additional user info (what on access attempt will cause NullReferenceException).
But sometimes it doesn't help
I'm not sure what you mean by "sometimes it doesn't help" - you don't say what exactly happens.
But you should remember that expiration of a Forms Authentication ticket and expiration of a Session timeout are completely independent. A user's session can timeout while his Forms Authentication ticket is still valid and vice versa.
In general, when accessing data from Session, you should always test for existence first, and refresh it if necessary:
object o = Session["Whatever"];
if (o == null)
{
o = ... refresh it e.g. from the database
Session["Whatever"] = o;
}
...
Often it's useful to use a helper class to encapsulate this.
In your case you refer to "additional user info" - so you'll probably be able to retrieve this using HttpContext.Current.User.Identity.Name as a key.
Forcing a user to log in again because a Session has expired, e.g. because of an Application Pool recycle on the server, is very unfriendly.
UPDATE
The MSDN documentation for RedirectToLoginPage states that:
Unlike the HttpResponse.Redirect method, this method does not end the request by calling HttpResponse.End. This means that code that follows the RedirectToLoginPage method call will run.
This probably explains what you're seeing: code in the Page life cycle after your call to RedirectToLoginPage is running, and throwing a NullReferenceException.
You could call Response.End after RedirectToLoginPage to avoid this.
My Session timeout set = Forms auth timeout (via web.config).
I would re-iterate that Session expiry and FormsAuthentication expiry are unrelated, even if the timeouts happen to be the same. A FormsAuthentication cookie will survive an application pool recycle on the server; a Session will not.

Categories

Resources