Search code examples
blazorblazor-server-side

How to call a method before the EventCallback gets executed


My Blazor server application has many pages and many EventCallbacks like onclick, onkeypress, etc. I want to call a method before all these EventCallbacks get executed.

I haven't found a method that I could override from ComponentBase. Or is there any middleware or filter like in MVC in Blazor server?

Edit

I have written a compile-time AOP component, Rougamo.Fody, similar to PostSharp.

// define an aspect class
public class TestAttribute : MoAttribute
{
    public override void OnEntry(MethodContext context)
    {
        // OnEntry corresponds to before method execution
    }

    public override void OnException(MethodContext context)
    {
        // OnException corresponds to after method throws an exception
    }

    public override void OnSuccess(MethodContext context)
    {
        // OnSuccess corresponds to after method execution successfully
    }

    public override void OnExit(MethodContext context)
    {
        // OnExit corresponds to when method exits
    }
}

public class TestService
{
    [Test]
    public async Task MAsync() => await Task.Yield();

    [Test]
    public static void M() { }
}

Recently, more and more people have been asking for access to IServiceProvider in the aspect class. But the aspect class could be applied to a static method like the method M above, so I cannot resolve the IServiceProvider from the target instance, like the instance of the class TestService above.

Currently, my solution is to save the IServiceScope into a static AsyncLocal field when creating a scope. Now, you can get the current scope from the static field in the aspect class.

// For the full implementation: https://github.com/inversionhourglass/DependencyInjection.StaticAccessor/blob/master/src/DependencyInjection.StaticAccessor/PinnedScope.cs
public class PinnedScope
{
    public static readonly AsyncLocal<IServiceScope?> Scope = new();
}

public class TestAttribute : MoAttribute
{
    public override void OnEntry(MethodContext context)
    {
        // get IServiceProvider from the static field
        var serviceProvider = PinnedScope.Scope.Value?.ServiceProvider;
    }
}

It works fine in generic host and Web API / MVC projects, but Blazor server is different. Each SignalR connection has its own service scope, and the pages are created within this scope. But the EventCallBack executes in another scope that is created in DefaultHubDispatcher<>.Invoke. So when I try to get the current scope from the static field PinnedScope.Scope in the event callback method, it actually returns the scope that DefaultHubDispatcher<>.Invoke created.

@page "/counter"
@rendermode InteractiveServer
@attribute [StreamRendering]

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Increase</button>

@code {
    private int currentCount = 0;

    [Inject]
    public IServiceProvider Services { get; set; }

    private async Task IncrementCount()
    {
        currentCount++;
        await Task.Yield();
        WhatEver();
    }

    [Test]
    private static void WhatEver() { }

    public class TestAttribute : MoAttribute
    {
        public override void OnEntry(MethodContext context)
        {
            // serviceProvider is not equal to the injected property Services
            var serviceProvider = PinnedScope.Scope.Value?.ServiceProvider;
        }
    }
}`  

I'm trying to find a way, like middleware or a filter, to set the PinnedScope.Scope before the event callback method executes.


Solution

  • I haven't gone through all your code [I got a bit lost on the context].

    But based on the primary question:

    I want to call a method before all these EventCallbacks get executed ........ I haven't found a method that I could override from ComponentBase.

    Here I believe is the answer to that question.

    When the Renderer receives an event from the browser it checks if the component implements IHandleEvent. If it does it passes the callback and the event args to

    IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg)
    

    If it doesn't it executes the mapped callback directly.

    ComponentBase implements IHandleEvent here https://github.com/dotnet/aspnetcore/blob/951b6ead6510409a7847481f818963e696686ca9/src/Components/Components/src/ComponentBase.cs#L349

    You can override this in your own base component:

    public class MyComponentBase : ComponentBase, IHandleEvent
    {
        protected virtual Task OnBeforeHandleEvent(object? arg)
        {
            return Task.CompletedTask;
        }
    
        async Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg)
        {
            await this.OnBeforeHandleEvent(arg);
    
            var task = callback.InvokeAsync(arg);
            var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion && task.Status != TaskStatus.Canceled;
    
            StateHasChanged();
    
            if (!shouldAwaitTask)
                return;
    
            await CallStateHasChangedOnAsyncCompletion(task);
        }
    
        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();
        }
    }
    

    Which you can the use like this:

    @page "/counter"
    @inherits MyComponentBase
    
    <PageTitle>Counter</PageTitle>
    
    <h1>Counter</h1>
    
    <p role="status">Current count: @currentCount</p>
    
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
    
    @code {
        private int currentCount = 0;
    
        private void IncrementCount()
        {
            Console.WriteLine("IncrementCount executed");
            currentCount++;
        }
        protected override Task OnBeforeHandleEvent(object? arg)
        {
            // Can detect the type of event based on the args
            Console.WriteLine("OnBeforeHandleEvent executed");
            return Task.CompletedTask;
        }
    }
    

    The result is:

    info: Microsoft.Hosting.Lifetime[0]
          Content root path: C:\Users\shaun\source\repos\SO78968957\SO78968957
    OnBeforeHandleEvent executed
    IncrementCount executed
    

    PS

    In the below code from your Counter, [StreamRendering] does nothing.

    @rendermode InteractiveServer  // Trumps [StreamRendering] 
    @attribute [StreamRendering]