dotnet / maui

.NET MAUI is the .NET Multi-platform App UI, a framework for building native device applications spanning mobile, tablet, and desktop.
https://dot.net/maui
MIT License
22.01k stars 1.72k forks source link

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

Open vsfeedback opened 2 years ago

vsfeedback commented 2 years ago

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)

janseris commented 2 years ago

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)

MackinnonBuck commented 2 years ago

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.).

JinShil commented 2 years ago

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();
    }
}
AntonEmelyancev commented 2 years ago

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.

janseris commented 2 years ago

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

Eilon commented 1 year ago

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

janseris commented 1 year ago

@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.

Eilon commented 1 year ago

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

JinShil commented 1 year ago

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.

Eilon commented 1 year ago

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.

SpikeThatMike commented 1 year ago

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

SaschaWegener commented 10 months ago

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.

Zhanglirong-Winnie commented 8 months ago

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

FelixLorenz commented 3 months ago

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?

TimWoerner commented 1 month ago

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