Reputation: 83
I am struggling with responsiveness of our server-side app. I am sure there's something inherently bad with my design and cannot figure it out. I am providing a sample razor page for this post which is a simplified version of my application.
There is a typeahead (blazored typeahead control by Chris Sainty) control for rooms list and a button to join a room. Users will basically select a room from the typeahead control and click the join button. There is a also a section in the same page where I display all the rooms that user has joined in a table control.
Typeahead's room list is populated using a database query.
public async Task JoinRoom()
{
//this adds the row to database table using async operation.
}
List<Rooms> GetAvailableRooms()
{
//this is a synchronous operation
var t = context.Rooms.ToList();
return t;
}
List<Rooms> GetMyRooms()
{
//this is a synchronous operation that returns list of user rooms
}
razor:
.
.
<RoomSelectorControl Operation="GetAvailableRooms" />
.
.
@if (SelectedRoom != null)
{
<div class="col-sm-1">
<div class="form-group">
<button type="button" class="btn btn-info" @onclick="JoinRoom"> Join </button>
</div>
</div>
}
var myRoomsList = GetMyRooms();
foreach (Room myRoom in myRoomsList)
{
//displays the list in a table control
}
Also, I have two razor pages 1) rooms page (above) 2) seats page.
My issue is whenever I load rooms page or switch between pages and end up in rooms page, I am noticing unresponsive UI until both the rooms typeahead control and "my rooms" section is displayed. On top of that, rooms list and "my rooms" section is built from scratch every single time. I think that is also part of the issue.
So my questions:
Upvotes: 0
Views: 676
Reputation: 3063
There is a lot of meat surrounding your 2 part question, but I'll give it a go. This shaped up to be a bit long, fair warning.
The first point to make is that you should keep in mind that Blazor Server Side doesn't work like traditional in-browser javascript applications. Everything the user does is being communicated back to the server through a SignalR connection, and the server then needs to process it on a UI thread, calculate a DOM diff, and send the diff back up the wire. If you are doing any synchronous operations, those run on the same thread, so if you have a blocking DB or API call going on at the server end, this will force your UI to lock up and wait for the thread to complete the long running procedure on the back end. To get around this, you can use asynchronous programming techniques to free up anything that is expected to take any amount of time from the UI thread. I'll apply this in a minute.
The second thing to consider is how you are handling concurrency in the app. Are there multiple users, and can data that is added, modified, or deleted by one user affect what should be populated in the lists? You may have already thought about this but it's worth considering.
So to your first point of the UI lockup, I'll make some assumptions on your property names but you should get the point.
First, your list backing field.
public List<Room> AllRooms { get; set; } = new List<Room>();
This will initialize your list ahead of the application running and avoid needing a null check for this item.
Then, let's change your method to a Task so it can be awaited, which will apply in a minute when we execute it to load your data.
Task<List<Rooms>> GetMyRooms()
{
//this is a synchronous operation that returns list of user rooms
}
Now to load up your data. You mention that you want to restrict the data load to either once per browser session or once per application lifetime. I'll stick with the browser session example since if the application is running for a long time on the server (hours, days, weeks) and serving multiple users, you could end up with stale data fairly easily and I assume MyRooms
is unique to each user.
Either way you'll want to take advantage of the Blazor Lifecycle methods, and for this one we'll use the OnAfterRenderAsync() method.
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if(firstRender)
{
// await loading of rooms from DB or other persistance,
// then assign result to AllRooms property
AllRooms = await GetAvailableRooms();
// Call this only if the list doesn't populate after GetAvailableRooms
// completes, which it might not.
StateHasChanged()
}
}
The big takeaway on the above is that the initialization logic only runs once when the page is loaded, after the first render. This gets the page awake and responsive and then loads data. You could also use the OnInitializedAsync
method and have the first line in the body be await Task.Delay(1)
which would break initialization free from the UI thread and allow the UI to finish rendering, however the initialized methods get called twice on page load, so you'd have 2 calls running to your data store. For this reason I tend to prefer the way I did it above, i.e. load a static initial state that will respond, then load the data.
Building the list for MyRooms
will work in a similar manner. Change the call to a task, have it update a backing property in the OnAfterRenderAsync
method, then call StateHasChanged
at the end of the initialization if needed.
So now you are asking "What about loading the data multiple times?" Well, for that you can look toward service injection. For this, we'll use a scoped service which has a default lifetime of the life of the SignalR circuit, which is suitable for your needs.
So add something like the following class to your application:
public class RoomsService
{
public RoomsService(//whatever you need to inject)
{
// Attach injected items to backing fields
}
// Whatever entity access you are using, initialized through constructor DI
private Entity _entity;
// Flags to determine if items need load from entity store
private bool allRoomsInitialLoad = true;
private bool myRoomsInitialLoad = true;
// Backing list of all rooms
private List<Room> _rooms;
public List<Room> GetAllRooms()
{
if(!allRoomsInitialLoad )
return _rooms;
_rooms = _entity.WhateverMethodGetstheRooms();
allRoomsInitialLoad = false;
return _rooms;
}
// Backing list of my rooms
private List<Room> _myRooms;
public List<Room> GetMyRooms(int userId)
{
if(!myRoomsInitialLoad)
return _myRooms;
_rooms = _entity.WhateverMethodGetsMyRoomsById(userId);
myRoomsInitialLoad = false;
return _myRooms;
}
public List<Room> JoinRoom(int roomId, int userId)
{
_entity.WhateverMethodAddsYouToRoomByIds(roomId, userId);
var room _entity.GetRoomById(roomId);
_myRooms.Add(room);
return _myRooms;
}
}
The gist of this service is that the public methods will only call the persistence layer for rooms on first load, otherwise they will return an in-memory representation of the persistance layer list. You may need to check this against concurrency concerns for stale data but that is up to you.
Let's add it to the Startup.cs services collection:
public void ConfigureServices(IServiceCollection services)
{
// other services
services.AddScoped<RoomsService>();
}
and then at the top of the razor file:
@inject RoomsService RoomService
and now your page methods can consume the service for data:
Task<List<Rooms>> GetMyRooms()
{
return RoomService.GetAllRooms();
}
You could also turn the service methods into tasks and await them directly if you want to cut out some of the call chain for this, but that again is up to you.
There are a few things to keep in mind with this approach.
RoomsService
class you can look to that cache first before hitting your persistence layer on first load. This may be way overkill for what you have, but it could also reduce the wait time on that first load enough to make a difference. This was quite a bit to throw at you, hope it helps!
Upvotes: 3