| File: ComponentBase.cs | Web Access |
| Project: src\aspnetcore\src\Components\Components\src\Microsoft.AspNetCore.Components.csproj (Microsoft.AspNetCore.Components) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Components.Rendering; namespace Microsoft.AspNetCore.Components; // IMPORTANT // // Many of these names are used in code generation. Keep these in sync with the code generation code // See: src/Components/Analyzers/src/ComponentsApi.cs // Most of the developer-facing component lifecycle concepts are encapsulated in this // base class. The core components rendering system doesn't know about them (it only knows // about IComponent). This gives us flexibility to change the lifecycle concepts easily, // or for developers to design their own lifecycles as different base classes. /// <summary> /// Optional base class for components. Alternatively, components may /// implement <see cref="IComponent"/> directly. /// </summary> public abstract class ComponentBase : IComponent, IHandleEvent, IHandleAfterRender { private readonly RenderFragment _renderFragment; private (IComponentRenderMode? mode, bool cached) _renderMode; private RenderHandle _renderHandle; private bool _initialized; private bool _hasNeverRendered = true; private bool _hasPendingQueuedRender; private bool _hasCalledOnAfterRender; /// <summary> /// Constructs an instance of <see cref="ComponentBase"/>. /// </summary> public ComponentBase() { _renderFragment = builder => { _hasPendingQueuedRender = false; _hasNeverRendered = false; BuildRenderTree(builder); }; } /// <summary> /// Gets the <see cref="Components.RendererInfo"/> the component is running on. /// </summary> protected RendererInfo RendererInfo => _renderHandle.RendererInfo; /// <summary> /// Gets the <see cref="ResourceAssetCollection"/> for the application. /// </summary> protected ResourceAssetCollection Assets => _renderHandle.Assets; /// <summary> /// Gets the <see cref="IComponentRenderMode"/> assigned to this component. /// </summary> protected IComponentRenderMode? AssignedRenderMode { get { if (!_renderMode.cached) { _renderMode = (_renderHandle.RenderMode, true); } return _renderMode.mode; } } /// <summary> /// Renders the component to the supplied <see cref="RenderTreeBuilder"/>. /// </summary> /// <param name="builder">A <see cref="RenderTreeBuilder"/> that will receive the render output.</param> protected virtual void BuildRenderTree(RenderTreeBuilder builder) { // Developers can either override this method in derived classes, or can use Razor // syntax to define a derived class and have the compiler generate the method. // Other code within this class should *not* invoke BuildRenderTree directly, // but instead should invoke the _renderFragment field. } /// <summary> /// Method invoked when the component is ready to start, having received its /// initial parameters from its parent in the render tree. /// </summary> protected virtual void OnInitialized() { } /// <summary> /// Method invoked when the component is ready to start, having received its /// initial parameters from its parent in the render tree. /// /// Override this method if you will perform an asynchronous operation and /// want the component to refresh when that operation is completed. /// </summary> /// <returns>A <see cref="Task"/> representing any asynchronous operation.</returns> protected virtual Task OnInitializedAsync() => Task.CompletedTask; /// <summary> /// Method invoked when the component has received parameters from its parent in /// the render tree, and the incoming values have been assigned to properties. /// </summary> protected virtual void OnParametersSet() { } /// <summary> /// Method invoked when the component has received parameters from its parent in /// the render tree, and the incoming values have been assigned to properties. /// </summary> /// <returns>A <see cref="Task"/> representing any asynchronous operation.</returns> protected virtual Task OnParametersSetAsync() => Task.CompletedTask; /// <summary> /// Notifies the component that its state has changed. When applicable, this will /// cause the component to be re-rendered. /// </summary> protected void StateHasChanged() { if (_hasPendingQueuedRender) { return; } if (_hasNeverRendered || ShouldRender() || (_renderHandle.IsRenderingOnMetadataUpdate && _renderHandle.IsFirstHotReloadRender())) { _hasPendingQueuedRender = true; try { _renderHandle.Render(_renderFragment); } catch { _hasPendingQueuedRender = false; throw; } } } /// <summary> /// Returns a flag to indicate whether the component should render. /// </summary> /// <returns></returns> protected virtual bool ShouldRender() => true; /// <summary> /// Method invoked after each time the component has rendered interactively and the UI has finished /// updating (for example, after elements have been added to the browser DOM). Any <see cref="ElementReference" /> /// fields will be populated by the time this runs. /// /// This method is not invoked during prerendering or server-side rendering, because those processes /// are not attached to any live browser DOM and are already complete before the DOM is updated. /// </summary> /// <param name="firstRender"> /// Set to <c>true</c> if this is the first time <see cref="OnAfterRender(bool)"/> has been invoked /// on this component instance; otherwise <c>false</c>. /// </param> /// <remarks> /// The <see cref="OnAfterRender(bool)"/> and <see cref="OnAfterRenderAsync(bool)"/> lifecycle methods /// are useful for performing interop, or interacting with values received from <c>@ref</c>. /// Use the <paramref name="firstRender"/> parameter to ensure that initialization work is only performed /// once. /// </remarks> protected virtual void OnAfterRender(bool firstRender) { } /// <summary> /// Method invoked after each time the component has been rendered interactively and the UI has finished /// updating (for example, after elements have been added to the browser DOM). Any <see cref="ElementReference" /> /// fields will be populated by the time this runs. /// /// This method is not invoked during prerendering or server-side rendering, because those processes /// are not attached to any live browser DOM and are already complete before the DOM is updated. /// /// Note that the component does not automatically re-render after the completion of any returned <see cref="Task"/>, /// because that would cause an infinite render loop. /// </summary> /// <param name="firstRender"> /// Set to <c>true</c> if this is the first time <see cref="OnAfterRender(bool)"/> has been invoked /// on this component instance; otherwise <c>false</c>. /// </param> /// <returns>A <see cref="Task"/> representing any asynchronous operation.</returns> /// <remarks> /// The <see cref="OnAfterRender(bool)"/> and <see cref="OnAfterRenderAsync(bool)"/> lifecycle methods /// are useful for performing interop, or interacting with values received from <c>@ref</c>. /// Use the <paramref name="firstRender"/> parameter to ensure that initialization work is only performed /// once. /// </remarks> protected virtual Task OnAfterRenderAsync(bool firstRender) => Task.CompletedTask; /// <summary> /// Executes the supplied work item on the associated renderer's /// synchronization context. /// </summary> /// <param name="workItem">The work item to execute.</param> protected Task InvokeAsync(Action workItem) => _renderHandle.Dispatcher.InvokeAsync(workItem); /// <summary> /// Executes the supplied work item on the associated renderer's /// synchronization context. /// </summary> /// <param name="workItem">The work item to execute.</param> protected Task InvokeAsync(Func<Task> workItem) => _renderHandle.Dispatcher.InvokeAsync(workItem); /// <summary> /// Treats the supplied <paramref name="exception"/> as being thrown by this component. This will cause the /// enclosing ErrorBoundary to transition into a failed state. If there is no enclosing ErrorBoundary, /// it will be regarded as an exception from the enclosing renderer. /// /// This is useful if an exception occurs outside the component lifecycle methods, but you wish to treat it /// the same as an exception from a component lifecycle method. /// </summary> /// <param name="exception">The <see cref="Exception"/> that will be dispatched to the renderer.</param> /// <returns>A <see cref="Task"/> that will be completed when the exception has finished dispatching.</returns> protected Task DispatchExceptionAsync(Exception exception) => _renderHandle.DispatchExceptionAsync(exception); void IComponent.Attach(RenderHandle renderHandle) { // This implicitly means a ComponentBase can only be associated with a single // renderer. That's the only use case we have right now. If there was ever a need, // a component could hold a collection of render handles. if (_renderHandle.IsInitialized) { throw new InvalidOperationException($"The render handle is already set. Cannot initialize a {nameof(ComponentBase)} more than once."); } _renderHandle = renderHandle; } /// <summary> /// Sets parameters supplied by the component's parent in the render tree. /// </summary> /// <param name="parameters">The parameters.</param> /// <returns>A <see cref="Task"/> that completes when the component has finished updating and rendering itself.</returns> /// <remarks> /// <para> /// Parameters are passed when <see cref="SetParametersAsync(ParameterView)"/> is called. It is not required that /// the caller supply a parameter value for all of the parameters that are logically understood by the component. /// </para> /// <para> /// The default implementation of <see cref="SetParametersAsync(ParameterView)"/> will set the value of each property /// decorated with <see cref="ParameterAttribute" /> or <see cref="CascadingParameterAttribute" /> that has /// a corresponding value in the <see cref="ParameterView" />. Parameters that do not have a corresponding value /// will be unchanged. /// </para> /// </remarks> public virtual Task SetParametersAsync(ParameterView parameters) { parameters.SetParameterProperties(this); if (!_initialized) { _initialized = true; return RunInitAndSetParametersAsync(); } else { return CallOnParametersSetAsync(); } } private async Task RunInitAndSetParametersAsync() { OnInitialized(); var task = OnInitializedAsync(); if (task.Status != TaskStatus.RanToCompletion && task.Status != TaskStatus.Canceled) { // Call state has changed here so that we render after the sync part of OnInitAsync has run // and wait for it to finish before we continue. If no async work has been done yet, we want // to defer calling StateHasChanged up until the first bit of async code happens or until // the end. Additionally, we want to avoid calling StateHasChanged if no // async work is to be performed. if (task.Status != TaskStatus.Faulted) { StateHasChanged(); } try { await task; } catch // avoiding exception filters for AOT runtime support { // Ignore exceptions from task cancellations. // Awaiting a canceled task may produce either an OperationCanceledException (if produced as a consequence of // CancellationToken.ThrowIfCancellationRequested()) or a TaskCanceledException (produced as a consequence of awaiting Task.FromCanceled). // It's much easier to check the state of the Task (i.e. Task.IsCanceled) rather than catch two distinct exceptions. if (!task.IsCanceled) { throw; } } // Don't call StateHasChanged here. CallOnParametersSetAsync should handle that for us. } await CallOnParametersSetAsync(); } private Task CallOnParametersSetAsync() { OnParametersSet(); var task = OnParametersSetAsync(); // If no async work is to be performed, i.e. the task has already ran to completion // or was canceled by the time we got to inspect it, avoid going async and re-invoking // StateHasChanged at the culmination of the async work. var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion && task.Status != TaskStatus.Canceled; // We always call StateHasChanged here as we want to trigger a rerender after OnParametersSet and // the synchronous part of OnParametersSetAsync has run. if (task.Status != TaskStatus.Faulted) { StateHasChanged(); } return shouldAwaitTask ? CallStateHasChangedOnAsyncCompletion(task) : Task.CompletedTask; } private async Task CallStateHasChangedOnAsyncCompletion(Task task) { try { await task; } catch // avoiding exception filters for AOT runtime support { // Ignore exceptions from task cancellations, but don't bother issuing a state change. if (task.IsCanceled) { return; } throw; } StateHasChanged(); } Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg) { var task = callback.InvokeAsync(arg); var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion && task.Status != TaskStatus.Canceled; // After each event, we synchronously re-render (unless !ShouldRender()) // This just saves the developer the trouble of putting "StateHasChanged();" // at the end of every event callback. StateHasChanged(); return shouldAwaitTask ? CallStateHasChangedOnAsyncCompletion(task) : Task.CompletedTask; } Task IHandleAfterRender.OnAfterRenderAsync() { var firstRender = !_hasCalledOnAfterRender; _hasCalledOnAfterRender = true; OnAfterRender(firstRender); return OnAfterRenderAsync(firstRender); // Note that we don't call StateHasChanged to trigger a render after // handling this, because that would be an infinite loop. The only // reason we have OnAfterRenderAsync is so that the developer doesn't // have to use "async void" and do their own exception handling in // the case where they want to start an async task. } }