dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.19k stars 9.93k forks source link

Blazor & SignalR - ObservableCollection UI notification issue #21398

Closed kaniosm closed 4 years ago

kaniosm commented 4 years ago

When binding to an observable collection, adding to the list from a local method triggers a UI refresh (not really a refresh, but rather a render of the added component/html). The same will not happen immediately after calling the same method from a SignalR message.

The behavior can be observed in the sample project attached. The same happens for both Server-side Blazor and WebAssembly, and especially for the first it is quite strange since the mechanics to update the UI are already based on SignalR. BlazorServerApp1.zip

davidfowl commented 4 years ago

@kaniosm did you call StateChanged?

kaniosm commented 4 years ago

Hi David,

No I didn't. If I do it will come up, but this will force a complete UI refresh.

danroth27 commented 4 years ago

Hi @kaniosm. I think the behavior you're seeing is expected. In general Blazor has no direct way of knowing when a component's state has changed - it can only make educated guesses. For example, Blazor will assume that the state has changed after a lifecycle event or UI event. Otherwise, you need to signal when the component's state has changed by calling StateHasChanged. This will trigger the component to render, but it won't force a complete UI refresh. Blazor will keep track of which parts of the UI have changed and updated just those parts of the DOM accordingly. Also note that when a component must be updated based on an external event, such as a timer or other notifications, use the InvokeAsync method, which will dispatch to Blazor's SynchronizationContext. See Invoke component methods externally to update state for details.

kaniosm commented 4 years ago

Hi @danroth27. Thx for the update. I understand that StateHasChanged will just trigger a component refresh and not a complete UI refresh... this means that any data kept at the component will be lost. For example, imagine a grid component operating in batch mode (i.e. it will connect the changes done by the user and batch them back to server). In the meantime, you would like to fetch any new records added to the underline data model and therefore the binded list. Triggering the StateHasChanged will force the component (grid) to re-render and therefore lose the temporary changes done on the client side.

danroth27 commented 4 years ago

StateHasChanged will just trigger a component refresh and not a complete UI refresh... this means that any data kept at the component will be lost

Calling StateHasChanged shouldn't change the state of the component. It just signals that the component state has changed and should be rendered. Rendering the component shouldn't change the component's state either. In your component's implementation you should be able to maintain whatever state you'd like for pending changes and reconcile those changes with any updates/refreshes however you'd like. I can understand that there is likely complexity involved in reconciling pending changes to some state that was then updated or changed, but that's true regardless of anything Blazor does.

I also only just noticed that this is a Blazor Server app. Using SignalR in a Blazor Server app is a bit awkward because the app is already running on the server. You end up setting up a hub connection back to yourself! Instead, you could use the underlying mechanism that's triggering the messages from the hub to trigger updates to your components.

kaniosm commented 4 years ago

Hi @danroth27 ,

Yes, you are right! It is indeed awkward! The reason for this that the initial project was in WebAssembly :). I just wanted to make sure I could reproduce the same issue with a stable release of Blazor. I spotted this while I was using a third-party component (datagrid from Syncfusion), which I don't have access to the component's state. Initially this seemed wrong to me because the UI update will happen if you click the button multiple times. After your explanation above, I realized that the update was always happening the second time I click the button, which essentially means that immediately after clicking the button, Blazor guesses there is a UI change and tries to identify the changes, but the rendering will happen before SignalR's message comes back (which is again awkward because you wouldn't trigger a message the application is self, but expect this to come from another app). This is just an experiment identify how realtime data can be pushed to the UI, while a user can manipulate the existing data, while we allow batching of updates. The general idea is that new items will be coming to the list while the underline data will be populated by other users/sources. When I tested the same app, but initiated the messages from a different client application I was having more consistent results (i.e. the UI was never updated), which essentially proves what you are stating above. That Blazor will re-render the component, or identify what needs to be updated, after an event on the UI.

Anyway, It all clear to me now! Thx again for the info.

danroth27 commented 4 years ago

@kaniosm Cool, I'm glad you were able to resolve your issue. Thanks for using Blazor, and let us know if there's anything else we can do to help!

kaniosm commented 4 years ago

No actually resolve, but understand better how Blazor works :) It's one of those times that you realize there's more road till the end. Thx @danroth27