I am currently writing a small Blazor Web UI. I am trying to adopt / implement an MVVM pattern. For this I have created two component base classes. One which simply handles the Blazor life-cycle methods (adds some exception handling) and one which does handle ViewModel initialization based on the execution of those life-cycle methods. The component does also implement the IDisposable interface, which is automatically called by Blazor when the component becomes invisible.
Here a code snippet out of my WebComponentBase class and the ViewModelAwareComponent class, to roughly give an idea about the pattern:
public abstract class WebFpComponentBase : ComponentBase, IDisposable, IHtmlStyles
{
private const string DEFAULT_DIM_VALUE = "auto";
[Inject]
protected IExceptionUiHandler<ErrorRedirectViewModel> ExceptionUiHandler { get; set; }
//fields and parameters omitted for brevity
#region Blazor Component LifeCycle
/// <summary>
/// OnInitialized is called after OnInitialized, when the component has received all initial parameters. Place any asynchronous operations here,
/// which require the component to re-render.
/// </summary>
/// <returns></returns>
protected override async Task OnInitializedAsync()
{
try
{
await base.OnInitializedAsync();
_logger.Info($"{nameof(OnInitializedAsync)} - method invoked in component of type ${this.GetType().FullName}");
await OnInitializedInternalAsync();
_logger.Info($"{nameof(OnInitializedAsync)} - method finished in component of type ${this.GetType().FullName}");
} catch(Exception ex)
{
//Exception, if any happend, is forwared using the IExceptionUiHandler in the AfterRenderAsync() method
_forwardException = ex;
_logger.Error($"{nameof(OnInitializedAsync)}: Catching and forwarding exception of type {_forwardException.GetType().FullName}");
}
}
protected abstract Task OnInitializedInternalAsync();
//other methods of the class omitted for brevity
}
Next my ViewModelAwareComponent, which does have a property containing the ViewModel and does automatically fire ViewModel initialization and deinitialization (closing any service connections, resetting any values etc.) by implementing the [BlazorLifecycleMethod]Internal abstract methods.
public abstract class ViewModelAwareComponent<TViewModel> : WebFpComponentBase where TViewModel : BaseViewModel
{
private Logger _logger = LogManager.GetCurrentClassLogger();
[Parameter]
public virtual TViewModel ViewModel { get; set; }
protected override async Task OnInitializedInternalAsync()
{
await ViewModel.InitializeAsync();
ViewModel.PropertyChanged += this.FireComponentStateHasChanged;
}
public override async void Dispose()
{
base.Dispose();
await ViewModel.DeactivateAsync();
}
protected virtual async void FireComponentStateHasChanged(object sender, PropertyChangedEventArgs e)
{
_logger.Trace($"FireComponentStateHasChanged: property {e.PropertyName} has changed!");
await InvokeAsync(this.StateHasChanged);
}
protected override async Task OnParametersSetAsyncInternal()
{
if (ViewModel == null)
{
throw new ArgumentNullException($"{nameof(ViewModel)}", "Parameter must be supplied!");
}
}
}
The BaseViewModel-Type only implements INotifyPropertyChanged in a typical fashion. I do have a "MainViewModel" which should only be instantiated once for the whole connection (Blazor Circuit). Hence I added it via services.AddScoped()
in the Startup.cs
. As it is not bound to any particular component, I am injecting it into my MainLayout.razor
which is the layout for every Razor component I wrote.
The layout does implement protected override async Task OnInitializedAsync()
like listed below:
protected override async Task OnInitializedAsync()
{
MainViewModel.PropertyChanged += (obj, args) => InvokeAsync(StateHasChanged);
await MainViewModel.InitializeAsync();
//some other stuff happening afterwards
}
My problem now is, that the initialization and is done twice every time I startup the app, instead of only once per connection. Same is true for the deinitialization, because the Dispose() of the component is called also twice.
When debugging I noticed that the OnInitializedAsync
is not re-called when switching between my two existing pages (routed components). It is only called twice on startup.
Do you maybe have some suggestions for this behaviour? Is there some better way to achieve my desired behaviour for the MainViewModel?
Best Regards,
tilt
Answered by the comment of dani herrera:
Maybe is it about pre-render? See my answer here: https://stackoverflow.com/a/57696602/842935
Disabled the pre-rendering by changing the
@(await Html.RenderComponentAsync(RenderMode.ServerPrerendered))
to
@(await Html.RenderComponentAsync(RenderMode.Server))
Now the OnInitializedAsync() of the MainLayout.razor is only called once.