4

First SO post so please let me know if my question is not adequately put together!

Use Case: The user opens the browser, and presses a "pay on device" button. I dispatch a PayOnDevice action which updates the UI to a loading state. I have a HandlePayOnDevice [Effect] method which pickups up that action and asynchronously starts the device to take a payment. When the user inserts the card into the device the payment succeeds/fails, the async method resolves and updates the UI to success/fail. However, if the browser closes before the async method resolves, I'd like to let the device finish and tell another service that a device payment succeeded or failed.

Issue: I'm able to technically do this by overriding the virtual Dispose(bool disposing) method of the FluxorComponent to dispatch an action "BrowserClosed". Then when the original device async method resolves, I can check the IState to see if the browser closed to know whether to update the UI or update some other system with the result. Here's that override Dispose method:

    protected override void Dispose(bool disposing)
    {
        Dispatcher.Dispatch(new BrowserClosedAction());
        base.Dispose(disposing);

    }

The issue with calling Dispatch in this dispose method is that something with the rendering logic breaks because the component is disposed so an error is thrown (but the IState is still updated so my Effect method can still discern whether to update UI or a different service):

Unhandled exception in circuit '2y97VfGYbGWD9xJIhtsbLOfj9PsQChpDlqBrGQdVXTQ'.
System.ObjectDisposedException: Cannot process pending renders after the renderer has been disposed.
Object name: 'Renderer'.
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessPendingRender()
   at Microsoft.AspNetCore.Components.Server.Circuits.RemoteRenderer.ProcessPendingRender()
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.AddToRenderQueue(Int32 componentId, RenderFragment renderFragment)
   at Microsoft.AspNetCore.Components.ComponentBase.StateHasChanged()
   at Microsoft.AspNetCore.Components.Rendering.RendererSynchronizationContextDispatcher.InvokeAsync(Action workItem)
   at Microsoft.AspNetCore.Components.ComponentBase.InvokeAsync(Action workItem)
   at Fluxor.Blazor.Web.Components.FluxorComponent.<OnInitialized>b__8_0(IState _)
   at Fluxor.StateSubscriber.<>c__DisplayClass2_1.<Subscribe>b__1(Object s, EventArgs a)
   at Fluxor.Feature`1.TriggerStateChangedCallbacks(TState newState)   at Fluxor.Feature`1.set_State(TState value)
   at Fluxor.Store.DequeueActions()
   at Fluxor.Store.Dispatch(Object action)
   at MyComponent.Dispose(Boolean disposing) in C:\MyComponentPath\MyComponent.razor:line XXX
   at Fluxor.Blazor.Web.Components.FluxorComponent.Dispose()        
   at Microsoft.AspNetCore.Components.Rendering.ComponentState.Dispose()
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.Dispose(Boolean disposing)

Technically even though this error is thrown, I can still do what I need to do, but I don't think I should go this route due to the error (which I hope is avoidable!) and am wondering if there is another way to accomplish this. I think there might be a way to create a Scoped CircuitHandler to dispatch an action, but I don't know how to get the IState/Dispatcher injected into that circuit handler. This Circuit handler fails:

public class BrowserClosedHandler : CircuitHandler
{
    BrowserClosedHandler(IState<S.AppState> appState, IDispatcher dispatcher)
    {
        AppState = appState;
        Dispatcher = dispatcher;
    } 
    private IState<S.AppState> AppState;
    private IDispatcher Dispatcher;
    public override Task OnConnectionDownAsync(Circuit circuit, 
        CancellationToken cancellationToken)
    {
        Dispatcher.Dispatch(new BrowserClosedAction());
        return Task.CompletedTask;
    }
}
...
// SERVICES SETUP
services.AddScoped<CircuitHandler, BrowserClosedHandler>();
services.AddFluxor(o => o
    .ScanAssemblies(typeof(Program).Assembly)
    .UseRouting()
);

This results in an error on startup:

The application failed to start correctly
System.AggregateException: Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: Microsoft.AspNetCore.Components.Server.Circuits.CircuitHandler Lifetime: Scoped ImplementationType: ECC.Startup+BrowserClosedHandler': A suitable constructor for type 'ECC.Startup+BrowserClosedHandler' could not be located. Ensure the type is concrete and services are registered for all parameters of a public constructor.)
 ---> System.InvalidOperationException: Error while validating the service descriptor 'ServiceType: Microsoft.AspNetCore.Components.Server.Circuits.CircuitHandler Lifetime: Scoped ImplementationType: ECC.Startup+BrowserClosedHandler': A suitable constructor for type 'ECC.Startup+BrowserClosedHandler' could not be located. Ensure the type 
is concrete and services are registered for all parameters of a public constructor.
 ---> System.InvalidOperationException: A suitable constructor for type 'ECC.Startup+BrowserClosedHandler' could not be located. Ensure 
the type is concrete and services are registered for all parameters 
of a public constructor.
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateConstructorCallSite(ResultCache lifetime, Type serviceType, Type implementationType, CallSiteChain callSiteChain)        
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact(ServiceDescriptor descriptor, Type serviceType, CallSiteChain callSiteChain, Int32 slot)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.GetCallSite(ServiceDescriptor serviceDescriptor, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine.ValidateService(ServiceDescriptor descriptor)        
   --- End of inner exception stack trace ---
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine.ValidateService(ServiceDescriptor descriptor)        
   at Microsoft.Extensions.DependencyInjection.ServiceProvider..ctor(IEnumerable`1 serviceDescriptors, ServiceProviderOptions options)  
   --- End of inner exception stack trace ---
   at Microsoft.Extensions.DependencyInjection.ServiceProvider..ctor(IEnumerable`1 serviceDescriptors, ServiceProviderOptions options)  
   at Microsoft.Extensions.DependencyInjection.ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(IServiceCollection services, ServiceProviderOptions options)
   at Microsoft.Extensions.DependencyInjection.DefaultServiceProviderFactory.CreateServiceProvider(IServiceCollection containerBuilder) 
   at Microsoft.Extensions.Hosting.Internal.ServiceFactoryAdapter`1.CreateServiceProvider(Object containerBuilder)
   at Microsoft.Extensions.Hosting.HostBuilder.CreateServiceProvider()
   at Microsoft.Extensions.Hosting.HostBuilder.Build()
   at ECC.Program.Main(String[] args) in C:\SomePath\Program.cs:line XXX

Any help would be greatly appreciated!

  • 1
    So just blindly looking (and not testing), could it be as simple as your BrowserClosedHandler's constructor is not public but, in fact, internal? – Ben Sampica Sep 30 '20 at 21:51
  • 1
    Have you tried calling base.Dispose after instead of before? If that doesn't work, can you email me with a link to a repro so I can try it? I'm not seeing the behaviour you describe - mrpmorris@gmail.com – Peter Morris Oct 01 '20 at 08:43
  • @BenSampica that was it! Thank you so much this helps greatly. – Richard McDonald Oct 01 '20 at 12:32
  • @PeterMorris thank you for the comment! I tried what you suggested, the placement of base.Dispose didn't effect the error. Would you like me to email a repo to look at this situation anyway? – Richard McDonald Oct 01 '20 at 12:35
  • Yes, please send me something – Peter Morris Oct 01 '20 at 16:22

1 Answers1

1

When you use FluxorComponent as a base it will scan the component for all IState<T> properties and subscribe to them. Whenever the value of this state changes the FluxorComponent will call StateHasChanged to re-render the component.

When the component is Disposed, FluxorComponent will remove all subscriptions to avoid memory leaks and attempts to render a disposed component.

In your case this is what is happeneing

  1. Blazor deactivates the component so it can no longer render
  2. It calls Dispose
  3. Your overridden Dispose dispatches an action that gives a new state
  4. The subscription to IState<T> fires, and StateHasChanged is executed against a component that cannot render.

The fix is to call base.Dispose(disposing) first so that the FluxorComponent base class can unsubscribe from the state before you dispatch your action.

Peter Morris
  • 20,174
  • 9
  • 81
  • 146
  • Thank you for following up! I replied to you via email with an updated repo to reproduce the issue, but just so anyone who might be reading this SO thread, the problem I'm seeing has to do with a nested FluxorComponent which also overrides the Dispose(disposing). When a nested component does this, the render error is always thrown in the Parent disposing method, regardless of the location of Dispatch(). – Richard McDonald Oct 02 '20 at 13:41
  • 1
    @RichardMcDonald There was additionally a bug in Fluxor that occurs when you call base.Dispose in the correct place but also dispatch actions with a consuming child component. I've uploaded 3.7.1-beta1 packages for you to test (it's a pre-release, so allow VS to include pre-releases when searching for Nuget Package updates) – Peter Morris Oct 02 '20 at 17:41
  • 1
    Thank you @PeterMorris! That beta release did the trick. – Richard McDonald Oct 02 '20 at 18:49