maui icon indicating copy to clipboard operation
maui copied to clipboard

Dispose() on Razor component not called on application close (Blazor .MAUI Windows app)

Open vsfeedback opened this issue 3 years ago β€’ 13 comments
trafficstars

This issue has been moved from a ticket on Developer Community.


[severity:I'm unable to use this version] I am having serious component-lifetime issue. The problem is with implementing Dispose from IDisposable on a razor component. The method is perfectly called one the page gets off from viewing, ex. when switching to other view. Also, the OnInitialized() is being called. But, once clicking the [x] close of the entire application, the Dispose() does not happen. As a result null-reference exceptions appear whenever the background tasks not being stop before application close try to access not existing objects. It is not much seen in example below, but it happens in a larger application when a timer is hit during application close.

How to reproduce:

  1. Create a new application from template
  2. Add a System.Threading.Timer object to SurveyPrompt.razor and init it in OnInitialized()
  3. add @implements IDisposable
  4. Add Dispose() => timer?.Dispose();
  5. Run the application
  6. The Dispose() / OnInitialized() works perfectly when switching between Home/Counter menu items, but when closing application with top-right [X] the Dispose() is not being hit.

IMHO this issue prevents MAUI to go public, I cannot control background tasks and cannot dispose resources properly.


Original Comments

Feedback Bot on 5/16/2022, 01:47 AM:

(private comment, text removed)


Original Solutions

(no solutions)

vsfeedback avatar May 17 '22 22:05 vsfeedback

This is required e.g. to save content to disk when application closes and restore state from disk when application starts in any MAUI Blazor application. Glad to see it fixed for future release!

Is there any suggested way to detect that the Dispose() is called because the application closes in Blazor components and pages? (Dispose is called as well when the page is leaved)

janseris avatar May 20 '22 09:05 janseris

We've decided to hold off on addressing this directly for now.

It's not currently possible to determine with complete certainty when a MAUI control's ViewHandler can be safely disposed, and this extends to the BlazorWebView control. We're still investigating the best techniques for managing handler life cycle (see #7381). We may revisit this topic in a future date if we are confident there is a way to manage ViewHandler disposal without breaking the less conventional uses of BlazorWebView. For more information on the reasoning behind this decision, refer to this thread.

To fix the specific case reported in this issue, consider subscribing to the Window.Destoying event that disposes resources that absolutely must be cleaned up before the application closes. You can register a singleton service to share state between MAUI and Blazor contexts and track which objects are awaiting disposal.

Persisting state by saving data to disk, on the other hand, should ideally be done before the application is on its way out. Saving to disk periodically (or when there is a notable change to the application's state) would be a more reliable approach because the application is not guaranteed to close gracefully (process gets terminmated, devices loses power, etc.).

MackinnonBuck avatar May 24 '22 23:05 MackinnonBuck

This caused me a lot of grief recently. Here's a hack that worked for my unique circumstance:

public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();
    }

    public BlazorWebView WebView
    {
        get => _webView;
    }
}
public partial class App : Application
{
    public App()
    {
        InitializeComponent();

        var mainPage = new MainPage();
        _webView = mainPage.WebView;

        MainPage = mainPage;
    }

    readonly BlazorWebView _webView;

    protected override Window CreateWindow(IActivationState? activationState)
    {
        var window = base.CreateWindow(activationState);

        window.Destroying += Window_Destroying;

        return window;
    }

    private void Window_Destroying(object? sender, EventArgs e)
    {
        _webView.Handler?.DisconnectHandler();
    }
}

JinShil avatar Jun 08 '22 07:06 JinShil

We've decided to hold off on addressing this directly for now.

It's not currently possible to determine with complete certainty when a MAUI control's ViewHandler can be safely disposed, and this extends to the BlazorWebView control. We're still investigating the best techniques for managing handler life cycle (see #7381). We may revisit this topic in a future date if we are confident there is a way to manage ViewHandler disposal without breaking the less conventional uses of BlazorWebView. For more information on the reasoning behind this decision, refer to this thread.

To fix the specific case reported in this issue, consider subscribing to the Window.Destoying event that disposes resources that absolutely must be cleaned up before the application closes. You can register a singleton service to share state between MAUI and Blazor contexts and track which objects are awaiting disposal.

Persisting state by saving data to disk, on the other hand, should ideally be done before the application is on its way out. Saving to disk periodically (or when there is a notable change to the application's state) would be a more reliable approach because the application is not guaranteed to close gracefully (process gets terminmated, devices loses power, etc.).

I understand those reasons very well being one of those who tried to create video player with full-screen mode in Xamarin Forms back then. However, persisting handlers is very advanced scenario and if developer wants it - he knows what he's doing. What currently happens is framework not working as expected - I'm not too sure average engineer should care if some sort of Handler is released - it is responsibility of the framework. I'm wondering if it makes sense to allow developer to opt if he/she wants to persist specific handler via property/method override: default value will disconnect WebView's handler as soon as hosting component is destroying - it will make sure 95% of scenarios will be covered and resources (being entire Blazor web app!!!! in case of Blazor Desktop) are released. For those who wants to persist handler will just override default value and tinker with component/handler as needed.

AntonEmelyancev avatar Jul 06 '22 13:07 AntonEmelyancev

@MackinnonBuck hi pleased what is the state of this issue? It is important for MAUI Blazor

janseris avatar Sep 23 '22 10:09 janseris

@janseris does a solution like mentioned in this earlier comment work for you?

Eilon avatar Sep 23 '22 16:09 Eilon

@janseris does a solution like mentioned in this earlier comment work for you?

I don't know. I haven't tried. It is a workaround.

Here's a hack...

I am waiting for a fix.

janseris avatar Sep 23 '22 17:09 janseris

I think the hack is in fact not a hack at all. It's just disposing a disposable thing when the app shuts down.

Eilon avatar Sep 23 '22 17:09 Eilon

I think the hack is in fact not a hack at all. It's just disposing a disposable thing when the app shuts down.

Actually, it's disconnecting a handler, and that indirectly causes the object to be disposed. But, the fundamental problem is that is not the responsibility of the user to implement. Whatever connected the handler and created the object assumes the responsibility to disconnect the handler and dispose the object.

JinShil avatar Sep 23 '22 21:09 JinShil

OK I do admit it's a bit hacky 😁 But it should be completely safe to do in this case when the app is shutting down, because disposing is idempotent, meaning it is acceptable to dispose more than one time.

I agree this should be done entirely automatically by the system, but that is not a viable option right now.

Eilon avatar Sep 23 '22 22:09 Eilon

I have encountered the same issue, I'm trying to remove an event when the secondary window is closed using the dispose event since there is an event which still fires off in the background which is causing NullReferenceException errors. The fix mentioned above also doesn't work for me since after _webView.Handler?.DisconnectHandler(); is called, the dispose method is called, but then a NullReferenceException error is raised from Microsoft.AspNetCore.Components.WebView.Maui.dll

SpikeThatMike avatar Jun 27 '23 13:06 SpikeThatMike

I was able to fix a similar issue with the following code (just as a workaround):

BlazorWebViewHandler.BlazorWebViewMapper.AppendToMapping("Dispose", (handler, view) =>
{
    if (view is View v)
    {
        v.Unloaded += (s, e) =>
        {
            var disconnectMethod = handler.GetType().GetMethod("DisconnectHandler", BindingFlags.NonPublic | BindingFlags.Instance);

            disconnectMethod?.Invoke(handler, new[] { handler.PlatformView });
        };
    }
});

In my case we navigated between pages within the application and we had no issue when closing the application.

SaschaWegener avatar Nov 01 '23 16:11 SaschaWegener

Verified this issue with Visual Studio Enterprise 17.9.0 Preview 2. Can repro this issue.

Zhanglirong-Winnie avatar Dec 28 '23 06:12 Zhanglirong-Winnie

We have a great experience with Blazor-Hybrid approach; even with Multi-Window. But there is a catch that brought me here: If a maui window gets destroyed, we are trying to trigger disposing of the BlazorWebView and all its components. Combining hints from @PureWeen to use Unloaded and @MackinnonBuck information to mark end of life via blazorWebView.Handler?.DisconnectHandler(); we ran into "Window was already deactivated" exception (maybe related to #22406 ).

Our final workaround to prevent memory leak now relies on a manual call to the GC when the BlazorWebView calls back to Unloaded, like so:

// MainPage.xaml.cs (one and only page per window that contains the BlazorWebView)
private void BlazorWebView_Unloaded(object sender, EventArgs e)
{
    // Trying to trigger disposing of the blazor webview....

    BlazorWebView.RootComponents.Clear();

    BlazorWebView = null;

    // platform specific (basically casting the BlazorWebView for example to Microsoft.UI.Xaml.Controls.WebView2 and calling Close() on it as suggested by @Eilon 
    WindowService.UnloadWebView(blazorWebView);

    // !!! leads to exception "Window was already deactivated"
    //BlazorWebView.Handler?.DisconnectHandler(); 

    // Since we are not able to find a proper way to trigger the disposing....

    var timeStamp = Stopwatch.GetTimestamp();

    GC.Collect();
    GC.WaitForPendingFinalizers(); // TODO ? is not necessary to prevent the leak

    var elapsed = Stopwatch.GetElapsedTime(timeStamp);

    Log.Debug("BlazorWebView unloaded. GC collected in {elapsed} ms", elapsed.Milliseconds);
}

This basically removes the memory leak.

Maybe this approach helps, or someone can improve it?

FelixLorenz avatar Jun 01 '24 09:06 FelixLorenz

Since we use Multiple windows for our Application, we can't migrate from wpf to .maui, please fix, else we can't dispose properly since window and blazor do not share the same servicescope

TimWoerner avatar Jul 29 '24 13:07 TimWoerner