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.52k stars 10.04k forks source link

Improvement to Lifecycle Events by having an overridable method called BeforeRender #29798

Open musictopia2 opened 3 years ago

musictopia2 commented 3 years ago

There are cases where there can be code that a person wants ran before the rendering just like there is AfterRender.

The purpose would be if a person wants to use code behind and run some methods to prepare for rendering. I have found many cases where StateHasChanged was called but did not invoke propertyset but the code should run in the code behind instead of razor file.

javiercn commented 3 years ago

@musictopia2 thanks for contacting us.

Can you give us examples where and how you would use something like this?

We are incredibly reluctant to add additional lifecycle methods so unless there's something that can't be achieved in any other way, we are not eager to add new methods.

musictopia2 commented 3 years ago

An example of where it would be used is in something like this.

You need an async call that runs just before it renders content. There are many times when because either statehaschanged or the parent control caused the child components to render, it does not fire onparametersset because parameters did not change. However, the state of something may have changed but it can only refresh async. A good example of where async is required is if javascript interop had to be used. This is a case where if it runs afterrender, its too late. Obviously a person would be responsible to make sure if it requires it be rendered at least once they handle that case though. I know that at the beginning, they can run a method. The bad news is they don't allow await methods to be done and if the method was async void, then it finishes rendering before the method was done which is bad.

javiercn commented 3 years ago

@musictopia2 I'm struggling to understand your sample, could you be a bit more concrete?

An actual "mockup sample situation" would help us better understand what the value here is.

musictopia2 commented 3 years ago

Here is a mock now https://github.com/musictopia2/BeforeRenderLifeCycleSamples

For this case, you can see that if I had to run an async method before it renders, it even gives compile errors. Its also possible for a base class to have an overridable method that needs to run before anything can render. Needs async in addition to regular versions of the method or just async version. If a person always calls statehaschanged after running the async method, then you get never ending loop which is bad.

musictopia2 commented 3 years ago

Since the demo has compile errors to demostrate the problem, in order to run it, you have to comment out the sample for the async method at the beginning of the razor file. Another case is allows more to be done from code behind instead of having to do the @{ } block as well.

javiercn commented 3 years ago

@musictopia2 thanks for the additional details.

I now better understand what you are trying to accomplish but it is not possible to do so today nor it will be possible in the future unless we decide to write a different framework that operates in a vastly different way.

Rendering in Blazor is synchronous and that's not something that is going to change. When a component renders we produce a RenderTree out of it, as a result of a component rendering, other child components can also be rendered. We put those in a queue and process them until no more components want to render.

When we have all the render trees we diff the current trees against the old trees and produce a render batch. Then we send that render batch to the browser (JS side of things) to be applied.

It is only at that point where the changes are actually applied to the DOM and element references "Materialize".

There is no option to have something like OnBeforeRender because all the changes to the DOM happen simultaneously. If the goal of doing something like OnBeforeRender is to invoke some JS interop to get some DOM element information, it's not possible because the element has not yet been inserted into the DOM.

In a similar way, it is not possible to get a reference to a component before the render happens, because its the rendering process what triggers the creation and initialization of the child component.

As I mentioned, this is not possible with the way the current rendering system in Blazor is designed and that is not likely something we are planning to change. It would be a massive behavior change that would break pretty much every app out there and it would also have potential performance implications.

I hope this helps clarify why something like this is not possible today and why we wouldn't want to change the rendering system to make it possible, at that point Blazor would be a completely different framework.

/cc: @dotnet/aspnet-blazor-eng in case anyone wants to add something.

musictopia2 commented 3 years ago

Is it possible to have at least a non async version. Would be useful in cases where it uses services that may have updated information. That would at least make it so instead of having to start with the @{ } then that can be code behind as well. I do understand why the async version would not be possible.

musictopia2 commented 3 years ago

If you want me to create a sample of where state can be different that requires extra variable initialization, i can do so as well.

javiercn commented 3 years ago

@musictopia2 I'm not sure I completely understand your follow up, but I believe it falls on to this

In a similar way, it is not possible to get a reference to a component before the render happens, because its the rendering process what triggers the creation and initialization of the child component.

If you are rendering a parent component the child component doesn't exist until after the parent render has been produced, so no information can be changed.

Would be useful in cases where it uses services that may have updated information.

You can already do this today and you don't need any additional lifecycle method:

If you want to clarify things further that will help us, but it is likely we have alternative recommendations on how to achieve the same scenario.

PD: You don't need to create an entire repo, if you create snippets with the relevant code and an explanation that likely works for us and is less work for you.

Hope this helps

musictopia2 commented 3 years ago

I did find it easier to explain with a repo instead of figuring out how to do a code snippet. Often times a service will call StateHasChanged on the parent component. That in turns calls it on its children. I do know that ElementReference it would not have. The cases I am talking about now is a case where there is no elementreference so that would not be a problem. I know it can currently be done by using @{ }

However, if that event was done, then even that would not be necessary in that case. I can update the repo to show a case where a service may update something and only the parent updates statehaschanged.

javiercn commented 3 years ago

@musictopia2 thanks for taking the time to create a repo.

If you update your sample to describe what you are suggesting that'll help us understand.

musictopia2 commented 3 years ago

I updated the sample now. It was fast creating the repo anyways. Especially with the latest version of visual studio where through visual studio, you can create a repo and upload to github. Used to be much harder.

For the sample its a case where there is a service the parent subscribes to statehaschanged. Then the parent calls statehaschanged which all the children updates. As you can see even though i got the desired results, i had to have code like this.

@{ RunProcess(); }

If the new life cycle event was done, then it gives more ways to organize the code so if you have any cleanup work to do before rendering you can do. For this case, its easier doing this way and more efficient than all the children having to subscribe to the event which means has to be event and not simple delegate and then has to dispose as well. Its also possible another component changed the value as well.

ghost commented 3 years ago

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. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

mrpmorris commented 3 years ago

One scenario I can think of is having to access device hardware such as geo-location, compass, etc.

At the moment we'd have to wait until after firstRender (in case we are server-side rendering), then call the JS to grab any values we will need to render in the UI (long/lat for example), and then call StateHasChanged to render a second time and make those retrieved values get rendered.

I see you live in @LocationInfo.LocationDescription - which is at longitude @LocationInfo.Longitude and latitude @LocationInfo.Latitude

@code {
  private LocationInfo LocationInfo;

  protected override async Task OnAfterRenderAsync(bool firstRender)
  {
    if (firstRender)
    {
      LocationInfo = Js.InvokeAsync<MyStats>("GetGeoStats");
      StateHasChanged();
    }
  }
}

I have had one scenario where I've needed to do this, and have had to force a second render.

javiercn commented 3 years ago

@mrpmorris You can already do that today using a service and a circuit handler to initialize the value when the circuit starts. Then your component can just read the location data from the service. Alternatively you can use JS interop within a try..catch inside OnInitializedAsync/OnParametersSetAsync

SteveSandersonMS commented 3 years ago

@mrpmorris As for avoiding a second render, I somewhat see where you're going, but it's not hugely meaningful in most cases. It is meaningful for prerendering (as Javier mentions above) but not really once the application has become interactive.

Once the application is running, conceptually there has to be something in the space where each component goes. There are some pixels on the user's display to fill in, and something has to get painted there, even if it's emptiness. If you do want to render "nothingness" until you have obtained some data, you can put an @if (ready) { ... } around your markup. But I'm not keen on creating new implicit ways to render nothing, when there's already a perfectly good way to do that, and when it's not really a good UX pattern anyway. It's nearly always better to render something like "Loading..." or "I see you live in ... which is at longitude ..., latitude ..." until the data is ready, as then the UI isn't flashing and jumping around so much.

musictopia2 commented 3 years ago

My purposes are not to avoid a second render. my purposes is to run some process before it renders and being able to do all in code behind. However, the results of the process can influence what will actually get rendered.

mrpmorris commented 3 years ago

@SteveSandersonMS I see what you mean. My gut said calling a render after a render is bad, but this is effectively what happens with OnInitializedAsync implicitly anyway :)

giulianob commented 2 years ago

@SteveSandersonMS I'm trying to find a way to automatically find what parts of my state have been read during render so it can automatically subscribe for updates. The only way I can see to achieve this is by having a BeforeRender or being able to override StateHasChanged to know when rendering is taking place. I'm trying to find ways to hack w/ Fody or find some other integration point to achieve this.

SteveSandersonMS commented 2 years ago

@giulianob I'm not sure how general you want your solution to be, but it sounds like you not only need a synchronous call before rendering, you also need a synchronous call after rendering (which also doesn't exist in general, e.g., because Blazor Server's post-render notification has to be async, because it's telling you when the remote client has confirmed the UI is updated).

If you're OK with dropping some unusual code into your components, you could put a @{ ... } block at the top and bottom of your markup, e.g.:

@{ MyThing.RenderingIsStarting(); }

<h1>Hello, world!</h1>
...

@{ MyThing.RenderingIsFinishing(); }
@code {
   // ... logic here ...
}

Alternatives

Even if you could override StateHasChanged, that wouldn't help you because that's not where the rendering happens. That method only enqueues the rendering, which may happen later if there are already other things on the queue.

If you did some private reflection, you could technically overwrite the _renderFragment field on ComponentBase to give your components some other RenderFragment delegate that wraps your own logic around the default _renderFragment callback. That would actually happen at the right time during rendering. But of course we don't support private reflection, so I couldn't possibly advise you to do that :)

mrpmorris commented 2 years ago

@giulianob Sounds to me like it's not the rendering you need to change, but your data source.

If your data source has a way of recording which properties are read, then you'll know. Am I missing something?

SteveSandersonMS commented 2 years ago

If your data source has a way of recording which properties are read, then you'll know. Am I missing something?

I was guessing they are trying to do something like Knockout.js-style dependency detection. This involves knowing when to start collecting a list of the reads, and when to stop collecting the list.

giulianob commented 2 years ago

If your data source has a way of recording which properties are read, then you'll know. Am I missing something?

I was guessing they are trying to do something like Knockout.js-style dependency detection. This involves knowing when to start collecting a list of the reads, and when to stop collecting the list.

That's right. There are a bunch of projects that have done this. I actually ended up hacking the render fragment like you said. Issue is that only works for my components or if I do it in IComponentActivator then for any components that extend ComponentBase. Would be great if we could subscribe to render events to know when a component is starting/finished rendering. Thanks for the replies!

ghost commented 1 year ago

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. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.