Aspnetcore: Add StateHasChanged(async: true) that guarantees never to run synchronously

Created on 22 May 2020  路  47Comments  路  Source: dotnet/aspnetcore

The StateHasChanged method is supposed to flag the component to be re-rendered, so if you call this method multiple times from the same call, it should render the component only once.

Actually, this is working ok when the call is performed from a Blazor event callback.

If I have a component names Component1 and the following markup in Index.razor

@page "/"

<Component1 @ref="component1" />
<button class="btn btn-primary" @onclick="UpdateComponent">Update Component (From Blazor)</button>
@code {

    Component1 component1;

    private void UpdateComponent()
    {
        component1.UpdateTheComponent();
    }
}

and the component code is the following

<h3>Component1</h3>

@{
    System.Diagnostics.Debug.WriteLine("ComponentRendered");
}
@code {

    public void UpdateTheComponent()
    {
        for (int i = 0; i < 100; i++)
        {
            StateHasChanged();
        }
    }
}

The text written in the output of visual studio is

"ComponentRendered"

Only one time.

If instead of calling the UpdateTheComponent() method from the Blazor button handler, it is called from JavaScript, the component is updated multiple times.

To call the UpdateTheComponent() method from javascript, I will alter the component to pass the component reference to a JavaScript method.

@inject IJSRuntime JS

<h3>Component1</h3>

<button @ref="button" class="btn btn-primary" onclick="window.Component1.updateComponent(this)">Update Component (From JS)</button>

@{
    System.Diagnostics.Debug.WriteLine("ComponentRendered");
}
@code {
    ElementReference button;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            var ComponentReference = new Component1Reference(this);
            await JS.InvokeVoidAsync("Component1.init", button, DotNetObjectReference.Create(ComponentReference));
        }
    }

    public void UpdateTheComponent()
    {
        for (int i = 0; i < 100; i++)
        {
            StateHasChanged();
        }
    }

    public class Component1Reference
    {
        private Component1 Component1;

        internal Component1Reference(Component1 scrollViewer)
        {
            Component1 = scrollViewer;
        }

        [JSInvokable]
        public void UpdateTheComponent()
        {
            Component1.UpdateTheComponent();
        }
    }

}

and the scripts.js javascript having the following

window.Component1 = {
    init: function (elementReference, componentReference) {
        elementReference.Component1 = componentReference;
    },
    updateComponent: function (element) {
        element.Component1.invokeMethodAsync('UpdateTheComponent');
    }
}

When I press the button, the javascript obtains the component reference and call to the Blazor method one time.

But this time, the component is rendered multiple times

ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered

In many scenarios this causes dramatic performance degradation, as the rendering is executed multiple times unnecessarily.

BlazorApp8.zip

affected-few area-blazor enhancement severity-major

Most helpful comment

Hi @SteveSandersonMS

I too consider it a bug, simply because it is inconsistent. I think it should work the same whether it's before or after an await, regardless of which approach is decided upon (multiple renders or a single render).

At the moment it is inconsistent and unexpected. It's taken a long time for people to notice, and it will definitely trip people up.

I like the idea of having another method. But I think instead of a parameter named async perhaps it should be something like bool forceImmediateRender?

Maybe mark StateHasChange() as [Obsolete] so that people will instead call StateHasChanged(bool forceImmediateRender). Over time you can remove the parameterless version and then default forceImmediateRender to false?

For example, if your JS-side code expects the DOM to be updated in response to rendering, you'd have no way to do that if rendering was delayed by an unspecified asynchronous period.

Could you have a task that is completed once the render happens, and any calls marshalled from JS->C# will await that task before completing the JS promise and letting the JS code continue?

Same for unit testing, if it could access the queue then it too could await the task?

PS: Thanks for the interesting discussion 馃憤

All 47 comments

You likely have to switch to the UI context prior to invoking StateHasChanged - https://docs.microsoft.com/en-us/aspnet/core/blazor/components?view=aspnetcore-3.1#invoke-component-methods-externally-to-update-state. Nevermind, I didn't read through this correctly.

Well, I really wasn't expecting that. Good find!

The reason for the different behaviours is that the call from JS is asynchronous and so is the renderer, so the component can render between iterations of the loop.

You can prove this by changing your call from Blazor to be asynchronous like this in Index.razor

C# private async Task UpdateComponent() { await Task.Delay(1); component1.UpdateTheComponent(); }

If you make this change you will see the same behaviour for both buttons.

The reason for the different behaviours is that the call from JS is asynchronous and so is the renderer, so the component can render between iterations of the loop.

I don鈥檛 get this. The JavaScript invocation is async, but not the c# method I鈥檓 calling. Can you clarify? The async call continues in the same thread it started, so I don鈥檛 see why it is different

@SQL-MisterMagoo StateHasChanged is called directly from a C# loop

@arivoir The Blazor button calls UpdateTheComponent synchronously, blocking the renderer so it cannot re-render the component until the loop completes.

When you change the button click handler to be async, the renderer can continue to process even while the loop is iterating - and so you get a number (between 1 and 100) of re-renders.

Original:
--- Renderer ---| Blocked by looping code |--- Render ---
....Looping Code...SHC..SHC...SHC...SHC...SHC...
As per previous comment
--- Renderer ------Render---Render---Render--- Render ---
....Looping Code...SHC..SHC...SHC...SHC...SHC...

It might help if you provided a reason why you want to call SHC that many times in a loop - it seems odd and the contrived example doesn't indicate any real-life intent

It's a proof of principle.

In order to save processing time
When calling StateHasChanged many times
Then the component should only be marked as needing to be rerendered
And should render only once when the next render process occurrs

Here is a more simple demonstration of the problem

In _Host.cshtml add

  <script>
    window.callBackBlazor = function (blazorComponent) {
      blazorComponent.invokeMethodAsync("ExecuteStateHasChangedInALoop");
        }
  </script>

Then click the button in the following component

@page "/"
@inject IJSRuntime JSRuntime

@{ 
    System.Diagnostics.Debug.WriteLine(DateTime.UtcNow + " Rendering");
}
<button @onclick=CallJs>Call JS</button>

@code
{
    DotNetObjectReference<Index> ObjRef;

    protected override void OnAfterRender(bool firstRender)
    {
        ObjRef = DotNetObjectReference.Create(this);
    }

    private async Task CallJs()
    {
        await JSRuntime.InvokeVoidAsync("callBackBlazor", ObjRef).ConfigureAwait(false);
    }

    [JSInvokable]
    public async Task ExecuteStateHasChangedInALoop()
    {
        for (int i = 0; i < 3; i++)
            StateHasChanged();
    }
}

It seems that neither void or async/await or await Task.Delay(1) make any difference.

I'm sorry I don't understand why you are creating these loops to call StateHasChanged over and over?
I'm not suggesting you "tweak" the code but stop writing loops that call StateHasChanged, especially from JS

@SQL-MisterMagoo

When you change the button click handler to be async, the renderer can continue to process even while the loop is iterating - and so you get a number (between 1 and 100) of re-renders.

The fact that the method, where the loop is called, is async shouldn鈥檛 change anything, the loop is still synchronous. No rendering should happen between StateHasChanged invocations, as this method is supposed to flag the rendering and it shouldn鈥檛 execute the rendering directly. That鈥檚 how UI invalidation works in any platform and what the documentation claims Blazor should do.

The case you are describing, where renderings happen between the calls to StateHasChanged, correspond to a code like this

public async Task UpdateTheComponent()
{
     for (int i = 0; i < 100; i++)
     {
         StateHasChanged();
         await Task.Delay(1000);
     }
 }

The repro sample I created is obviously not real, but calling StateHasChanged multiple times in the same call it鈥檚 something normal in UI development, and it should work, otherwise it introduces serious performance problems.

@arivoir I don't think I am explaining this well

  • your button click handler in Blazor is synchronous and invoked on the main "thread" where the renderer executes - so it blocks the renderer, and hence you can only get a render after the code completes.
  • your JS invoked method is async, not blocking the renderer, so every time you call StateHasChanged it flags the component for re-rendering in the next render batch. That can happen during the timespan of the execution of the loop, so you can get multiple renders before the loop completes - but they are not synchronous and not 1-1 with the calls to StateHasChanged. You will get between 1 and 100 renders - depending on how quickly each render happens. This is possible because the renderer is not blocked.

Changing your Blazor button handler to be async is the right thing to do - you should not block the renderer - and if you do that then the behaviour is the same in both Blazor code and JS invoked code.

Why are you calling StateHasChanged more than once ? That is the real problem as far as I see it.

You have stated it is "normal" to call it multiple times - but the only time I would do that is precisely because I have a long-running async process that is updating the component state and I want it to re-render - what other reason is there to call it multiple times?

Calling StateHasChanged in a loop isn't the goal, it just demonstrates that the component is being rendered immediately rather than simply being flagged.

For example, if a single operation causes multiple changes to shared state that a component is subscribed to then it will receive multiple notifications to rerender itself.

@SQL-MisterMagoo, all your reasoning is based on a mistake, it isn鈥檛 calling the StateHasChanged in an asynchronous method. It is synchronous. It鈥檚 a synchronous for, there is no await, therefore it shouldn鈥檛 execute the rendering multiple times. It should flag it and execute it just one time. Other behavior not obeying this is a major design failure in the platform and should be the priority 1 of the team because this affects the performance tremendously.

When developing any component with a relative degree of complexity, the ui can be invalidated for diverse causes, coming from different parts of the code, if the component can鈥檛 rely in the platform to invalidate the ui correctly without causing a massive numbers of renderings, the component itself needs to do it, which is frankly inviable.

I think @SQL-MisterMagoo is right..

even though the method you have marked as [JSInvokable] is public void the method is invoked async.. Just look at your JS code

invokeMethodAsync('UpdateTheComponent');

The call from JS is asynchronous and the component can render between iterations of the loop.

That shouldn鈥檛 cause a massive performance degradation. Let鈥檚 invert the discussion, how do I avoid the rendering to be executed multiple times in that scenario?

@arivoir I go back to my previous question. Why are you calling StateHasChanged multiple times? Why not call it when you want a render? What is the situation where a call from JS requires you to call StateHasChanged repeatedly?

The same component can be invalidated for many reasons, in different parts of the code, it can happens 1 to X times.

@arivoir I go back to my previous question. Why are you calling StateHasChanged multiple times? Why not call it when you want a render? What is the situation where a call from JS requires you to call StateHasChanged repeatedly?

I answered that above.

For example, if a complex UI subscribes to 10 pieces of state and all 10 update then it will cause 10 renders.

I guess I'll just leave you to it as I don't think this is going anywhere.

The renderer dispatches the Blazor click event through Renderer.DispatchAsync, which sets _isBatchInProgress = true;

https://github.com/dotnet/aspnetcore/blob/c74e9efd0e6ea5b4d6737d23b92042772a50a249/src/Components/Components/src/RenderTree/Renderer.cs#L213-L256

This makes the render requests batch all 100 calls immediately.

Howevever, the codepath for JSInvokable does not go through Renderer.DispatchAsync, which makes the renderer set _isBatchInProgress
only during the actual the actual processing of the batch, which is a direct result of the StateHasChanged() call. So the render calls are
handled for every StateHasChanged() until MaxBufferedUnacknowledgedRenderBatches is hit in the RemoteRenderer. That's after 10 render calls. The rest is
simply dropped.
I'm not sure whether this is intended behaviour and you just should not call StateHasChanged multiple times during JSInvoke, or that it's a bug.

BTW, this has nothing to do with the fact that it's async. Everything is executed on the current synchronizationcontext, there is no dispatching. You can even rewrite the UpdateComponent method as:

 public void UpdateTheComponent()
    {
        InvokeAsync(() =>
        {
            for (int i = 0; i < 100; i++)
            {
                StateHasChanged();
            }
        }
    }

And it will still happen, even though we are sure now that StateHasChanged happens within the render context.

To illustrate my point: I've wrapped the UpdateTheComponent with reflection code that roughly does the code in dispatchEvent:

    public void UpdateTheComponent()
    {
        for (int i = 0; i < 1000; i++)
        {
            StateHasChanged();
        }
    }

    public void WrapUpdateTheComponent()
    {
        // set BatchInProgress true through reflection
        var h = this.GetType().BaseType.GetField("_renderHandle", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
        RenderHandle handle = (RenderHandle)h.GetValue(this);
        var rp = typeof(RenderHandle).GetField("_renderer", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
        object renderer = rp.GetValue(handle);
        var ipp = renderer.GetType().BaseType.GetField("_isBatchInProgress", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);

        ipp.SetValue(renderer, true);

        UpdateTheComponent();

        // set back to false.
        ipp.SetValue(renderer, false);

        // call ProcessPendingRender
        var gr = renderer.GetType().BaseType.GetMethod("ProcessPendingRender", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
        gr.Invoke(renderer, null);
    }

    public class Component1Reference
    {
        private Component1 Component1;

        internal Component1Reference(Component1 scrollViewer)
        {
            Component1 = scrollViewer;
        }

        [JSInvokable]
        public void UpdateTheComponent()
        {
            Component1.WrapUpdateTheComponent();
        }
    }

This resolves the issue.

@SQL-MisterMagoo from your example above, I think you found a simpler repro case of the bug. No Javascript involved.

    private async void UpdateComponent()
    {
        System.Diagnostics.Debug.WriteLine("Before First Update");
        component1.UpdateTheComponent();
        System.Diagnostics.Debug.WriteLine("After First Update");
        await Task.Delay(1000);
        System.Diagnostics.Debug.WriteLine("Before Second Update");
        component1.UpdateTheComponent();
        System.Diagnostics.Debug.WriteLine("After Second Update");
    }

In the code above the are 2 identical calls to the method _UpdateTheComponent()_. The first call is correctly executed and the second shows the bug.
Lets see the log

Before First Update
After First Update
ComponentRendered
Before Second Update
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
ComponentRendered
After Second Update

When executing the first time, the StateHasChanged correctly flag the component and the render is correctly executed when the thread is released (because of the await Task.Delay). The second time the StateHasChanged executes the render directly which is the bug. The StateHasChanged should never execute the render directly, it should always flag it and queue it so the render is executed when the render thread is free.

In case anyone wants to justify this is by design, notice that component developers can't expose api that reliably render the component without causing incomprehensible performance degradation. We should document in the api of every method that it must be called before any "await" otherwise the performance will suffer a bunch of repeated rendering. It is a complete nonsense. The platform must be reliable. This is a huge bug that must be fixed soon.

@jspuij The reflection workaround looks useful to patch some cases, until this bug is fixed. Thank you.

Anyway, it is incomprehensible every developer having to write this reflection code every time they want to update the UI after invoking an async function.

I agree. I had thought of a timer as another case where a render is not the result of an event dispatch, but async is a nice find as well. This deserves a closer look quickly.

@arivoir Your C# only case makes perfect sense. You wouldn't want to block the rendering for 10 seconds just because multiple long-running tasks haven't completed. We'd want to see the UI update incrementally in that case.

Nobody said the rendering would be blocked for 10 seconds, that would be a terrible bug. The point is the second call should just perform 1 render.

@arivoir I agree. I initially misunderstood your example and posted an erroneous response. Upon realising my error I deleted the comment :)

The StateHasChanged method is supposed to flag the component to be re-rendered, so if you call this method multiple times from the same call, it should render the component only once.

That is true, but there's a subtlety about what exactly "from the same call" really means, and that's the root of the matter here. We should probably update our docs to clarify this.

What StateHasChanged really does is:

  1. If the component isn't already queued to be rendered, adds it to the render queue
  2. If the queue isn't already being processed, starts processing the queue

When calling StateHasChanged from a JS interop call, there's no active render batch, and hence step 2 has to start one. This is why the component is rendered immediately and synchronously, as many times as you call StateHasChanged from a JS interop call.

I understand that's not exactly what you want in this particular case, but the question would be: when should Blazor start processing the render queue then? We can't just wait an indeterminate period (e.g., await Task.Yield) because that would wreck many more scenarios. For example, if your JS-side code expects the DOM to be updated in response to rendering, you'd have no way to do that if rendering was delayed by an unspecified asynchronous period. Similarly, things like unit testing would be massively more difficult if you had no way to know when the rendering was actually going to happen.

The good news though is there is a way to get the behavior you want, which is simply to make the rendering async on the calling side. Blazor gives you the flexibility to make it work either way. For example, in the .NET method you're calling via JS interop:

bool hasScheduledRender;

[JSInvokable]
public async Task ReceiveCallFromJS()
{
    // Put whatever logic you wanted the call to do here
    Console.WriteLine("In ReceiveCallFromJS");

    // Now we schedule an async render, but only if one isn't already queued
    if (!hasScheduledRender)
    {
        hasScheduledRender = true;
        await Task.Yield();
        hasScheduledRender = false;
        StateHasChanged();
    }
}

I hope it makes sense that Blazor must default to synchronously starting the rendering, otherwise there'd be no way of making it do that if you needed to. Whereas async rendering is something you can cause from your own code as in the example here.

@SteveSandersonMS Firstly, let me explain this is a problem that must be fixed by the framework, neither component developers nor app developers can make this work fine.

Imagine there is a grid control with a public collection of rows. When an app developer using the component writes the following code

grid.Rows.Add(...);

The grid component needs to call the StateHasChanged of the panel of cells. So far so good. One call, one invalidation, one render.

Then a customer realizes it needs to add a bunch of rows instead. And writes this code

for(int i=0; i<100;i++)
{
    grid.Rows.Add(...);
}

And let's suppose it is invoked from a button event callback.

The grid component will call StateHasChanged 100 times, and only one render will be executed. Good. The framework works fine in this case.

Now imagine the developer needs to call a rest api to get information from the server to update the model of the grid, and writes this code

await CallRestApiToGetItemsAsync();
for(int i=0; i<100;i++)
{
    grid.Rows.Add(...);
}

Here the grid component will do the same as previously (there is no way to know there was an await call before), the StateHasChanged will be called 100 times, but this time the render will be executed up to 100 times. This brings 2 problems, the poor performance it will have, and the lack of reliability of the framework, and the components developed with it. It is impossible to develop good performance components that act differently depending if there was an await call before or not. This is not something you can not document and turn the page, it is a huge design issue that must be addressed.

Like this there are tons of examples that can be given to demonstrate this problem. It's not only about this specific case.

Secondly, I want to give my opinion about how this should work, and I will not reinvent the wheel, all the UI frameworks I've worked on have an invalidation system that works in the following way. There is an Invalidation method that just mark part of the visual tree and a subsequent render ends up running the layout. The principle is: the invalidation method never runs the render, therefore the invalidation method can be call as many times as necessary. If this is not given by the platform, it is impossible to develop it inside a component or app, because they don't have access to the code that schedules the renders, and shouldn't, it should be provided by the platform.

WPF, UWP etc. https://docs.microsoft.com/en-us/dotnet/api/system.windows.uielement.invalidatearrange?view=netcore-3.1
Android https://developer.android.com/reference/android/view/View#requestLayout()
iOS https://developer.apple.com/documentation/uikit/uiview/1622601-setneedslayout
XF https://docs.microsoft.com/en-us/dotnet/api/xamarin.forms.layout.invalidatelayout?view=xamarin-forms

Notice all of them speaks of the next layout pass, or things like that

Invalidates the arrange state (layout) for the element. After the invalidation, the element will have its layout updated, which will occur asynchronously

Call this when something has changed which has invalidated the layout of this view. This will schedule a layout pass of the view tree.

Invalidates the current layout of the receiver and triggers a layout update during the next update cycle.

Calling this method will invalidate the measure and triggers a new layout cycle.

Next, you're right there must be a way, a method, to force the layout to be processed. It's necessary for scenarios that need the ui elements to be in place.

WPF, UWP, etc. https://docs.microsoft.com/en-us/dotnet/api/system.windows.uielement.updatelayout?view=netcore-3.1#System_Windows_UIElement_UpdateLayout
Android https://developer.android.com/reference/android/view/View#layout(int,%20int,%20int,%20int)
iOS https://developer.apple.com/documentation/uikit/uiview/1622507-layoutifneeded
XF https://docs.microsoft.com/en-us/dotnet/api/xamarin.forms.layout.forcelayout?view=xamarin-forms

Regarding how to implement this, it is something I can not tell you, I don't know these details of the platform, but it looks the platform must have a scheduler to queue render pass, and this must run asynchronously. Again, the StateHasChanged should never call a render.

Hope it helps to make Blazor a top UI framework.

@arivoir Thanks for the additional information.

I see this is inconvenient in your case.

Again, the StateHasChanged should never call a render.

Overall I think there's a mismatch of expectations here. You want StateHasChanged to do something different from what it does in current releases. For back-compatibility reasons, I don't think we would change the meaning of StateHasChanged.

Would it be fair to say that you are looking for some new API which means "schedule a render asynchronously but definitely don't do it right now"? If so that's a totally reasonable request, and we could definitely look at adding that in the future. It might look like this:

StateHasChanged(async: true);

We could add a feature like that, but we wouldn't take away the ability to trigger a synchronous render, as that is needed in cases like unit testing or certain JS interop scenarios.

In the meantime, component/app developers can work around it by adding that sort of API themselves. For example, on your component base class, add a method like this:

TaskCompletionSource<bool> scheduledRenderTcs;

protected async Task StateHasChangedAsync()
{
    if (scheduledRenderTcs == null)
    {
        // No render is scheduled, so schedule one now
        var tcs = scheduledRenderTcs = new TaskCompletionSource<bool>();
        await Task.Yield();
        StateHasChanged();
        scheduledRenderTcs = null;
        tcs.SetResult(true);
    }
    else
    {
        // Just return the task corresponding to the existing scheduled render
        await scheduledRenderTcs.Task;
    }
}

This is a bit of a fancy way of doing it (simpler ways are possible), but this way gives you back a Task that you can optionally await on the caller side if you want to know when it's actually scheduled the render.

So with this, you can replace calls to StateHasChanged with StateHasChangedAsync and get the behavior you want, I hope!

Does that sound reasonable? I know you want the default behavior to change, but like mentioned above, that's almost certainly not something we'd do because of the back-compatibility issues and the fact that we do want there to be a way of starting the render batch synchronously.

We can look into adding a StateHasChanged(async: true) method in the future if there's community demand for it.

@SteveSandersonMS The StateHasChangedAsync workaround looks working, for some cases at least. Thanks for it.

we do want there to be a way of starting the render batch synchronously.

How? The StateHasChanged is not reliable to perform a render synchronously. Is there more api to reliably perform a render?

IMO, the current StateHasChanged implementation is a bug. It's inconceivable one method does one thing if you execute it either before or after an await call. I don't imagine having to create documentation to explain every developer this intricate behavior when it should work just flagging the layout. Personally, it took me a lot of time realizing why the components were executing a lot of times unexpectedly. And there are more things that are affecting the rendering of our components, like event-callbacks. I'm looking to get rid of them because they also trigger the rendering unexpectedly. For example listening to a click performs a rendering.

Having a StateHasChangedAsync built-in in the platform is a must, also having a method to perform the render synchronously is a must. I understand your point about back-compatibility, but the current StateHasChanged should be discouraged to be used, an Obsolete attribute would explain developers what to use instead, and the documentation should say not to use it because of all the problems it brings.

Hi @SteveSandersonMS

I too consider it a bug, simply because it is inconsistent. I think it should work the same whether it's before or after an await, regardless of which approach is decided upon (multiple renders or a single render).

At the moment it is inconsistent and unexpected. It's taken a long time for people to notice, and it will definitely trip people up.

I like the idea of having another method. But I think instead of a parameter named async perhaps it should be something like bool forceImmediateRender?

Maybe mark StateHasChange() as [Obsolete] so that people will instead call StateHasChanged(bool forceImmediateRender). Over time you can remove the parameterless version and then default forceImmediateRender to false?

For example, if your JS-side code expects the DOM to be updated in response to rendering, you'd have no way to do that if rendering was delayed by an unspecified asynchronous period.

Could you have a task that is completed once the render happens, and any calls marshalled from JS->C# will await that task before completing the JS promise and letting the JS code continue?

Same for unit testing, if it could access the queue then it too could await the task?

PS: Thanks for the interesting discussion 馃憤

This issue has been resolved and has not had any activity for 1 day. It will be closed for housekeeping purposes.

See our Issue Management Policies for more information.

Should this still be closed?

Actually, what would be really nice would be if there were two options in the parameters
1: Queue or RenderImmediately
2: Ignore or honour ShouldRender (Default / Forced)

That way when using something like Reactive patterns we could have a base component that always returns false from ShouldRender but then when a reactive subscription fires the base component does a Queue + Forced render. That way we get optimised rendering (Queue) and only rendering when the state changes via RX.

Another important point the method that updates the ui synchronously, whatever name, to do nothing if the layout didn鈥檛 need any update, otherwise calling this method could also end up causing performance problems.

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. However, keep in mind that there are many other high priority features with which it will be competing for resources.

@SteveSandersonMS

The API would be a lot clearer if the signalling of a render is separated from beginning / ending a render batch. I'd propose keeping StateHasChanged() for adding a component to the RenderQueue, but having a Begin/EndRender to start and stop a render batch. Dispatched events can still start their own batch, and calling StateHasChanged() without a BeginRender() can implicitly add to the renderqueue AND process immediately, kind of like an implicit transaction in SQL. This is both backwards compatible and gives control to the developer.

The workaround is nice, but it does not solve the problem when you have multiple components, that have to be updated in a single batch after an async operation. And this is a very common scenario when managing state.

I don't know how I'd write the code to fix it, but I know how I'd expect it to work.

StateHasChanged should not perform an actual render until the method completes or hits an await.

After a couple of days of error and trails.
I found that you must Add await Task.Delay(1); after your processing and before StateHasChanged()
This is silly, and I don't like it at all.
the code will look something like this

async Task CompleteSomething()
{
    StuffFromDatabase = context.Stuff.ToList();
    await Task.Delay(1);
    StateHasChanged();
}

@elnemerahmed Generally that shouldn't be necessary. I'm unsure why it's making a difference in your scenario, but if you're able to post minimal repro code (as a separate issue please - this one is too long already to keep track of), we can investigate and let you know if there's a better way or if it's an issue in the framework.

After a couple of days of error and trails.
I found that you must Add await Task.Delay(1); after your processing and before StateHasChanged()
This is silly, and I don't like it at all.
the code will look something like this

async Task CompleteSomething()
{
    StuffFromDatabase = context.Stuff.ToList();
    await Task.Delay(1);
    StateHasChanged();
}

@elnemerahmed This can be a patch for a specific case, but it doesn't work as a general solution, the framework itself is calling StateHasChanged without the delay, so the components will end up being updated at different times resulting in a trembling UI.

@SteveSandersonMS How is this going? any advance?

@arivoir This has been moved to the backlog. I might pick it up on a future blazor community sprint however.

@arivoir This has been moved to the backlog. I might pick it up on a future blazor community sprint however.

This is extremely important for everyone using the platform, the performance is fundamental for every medium size project. It should be between the top priorities IMO. I think most people is expecting the promised AOT runtime will fix the problems, but they don't know that Blazor layout has not good performance because of bad design decisions. The sooner these problems are addressed the less people affected, and the more chances Blazor has to become a real option.

I am facing the same problem in my project. I had to use the same await Task.Delay(x); workaround to solve it. I hope there will be a real solution in the future.

@elnemerahmed @boukenka Would you please file a separate issue for the Task.Delay issue. It's almost certainly not related to the original issue.

I created the original issue, and one of the first things I tried was the Task.Delay(1) (I didn't knew the Task.Yield() exist) I don't find strange they are talking of the same issue. It's natural people trying to dodge the synchronous rendering by executing that before calling StateHasChanged.

Was this page helpful?
0 / 5 - 0 ratings