maui
maui copied to clipboard
Call Dispose() on Page and ViewModel when the page is popped if they implement IDisposable
Description
It would be amazing if MAUI framework could call the Dispose method on the Page and also on it's ViewModel when the page gets removed from the navigation stack so we developers can gracefully dispose object that we have used and will no longer be used. It would be even better if the same could be done on custom controls (Views) that we create.
Steps to Reproduce
Implement IDisposable on a given page;
Also Implement IDisposable on it's page's VM
Current behavior The Dispose() method is never called after we remove the page from the navigation stack.
Expected behavior: Dispose() to be called when the page is removed from the navigation stack;
Version with bug
Release Candidate 2 on .NET 8
Last version that worked well
Unknown/Other
Affected platforms
iOS, Android, Windows, macOS, Other (Tizen, Linux, etc. not supported by Microsoft directly)
Affected platform versions
Android 11 and iOS, Windows, Tizen, MacOS ...
Did you find any workaround?
No
Relevant log output
No response
Isn't the disposal of the Pages and ViewModels are controlled by DI in Maui ?
Also this one seems to be related https://github.com/dotnet/maui/issues/7329
@HobDev No, MAUI doesn't care if you implement IDisposable on your page or ViewModel.
It would make our lifes way more ease if MAUI could call the Dispose() method when the page is removed from the navigation stack, on both the Page and the ViewModel if they are implementing IDisposable, similar to what they do when you implement the IQueryAttributable interface on your Page or ViewModel to handle navigation parameters.
For now I'd recommend using the Loaded event
We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.
For now I'd recommend using the
Loadedevent
Could you please elaborate more about Loaded event? I mean, this is about releasing resources once the page is no longer needed (unsubscribe subscriptions), so I'm curious how the loaded event could help.
I used the Unloaded event on ContentPage which works a treat on Android, but on iOS it is too trigger happy.
Android appears to create an instance of ContentPage, trigger Loaded until the page is popped then trigger Unloaded. If the page participates in tabs, it is not unloaded if the user switches tabs as it is at the front of its logical stack of pages within the tab.
iOS is different, it is similar to the above except if I switch tabs, the page in the tab I am switching away from triggers Unload. When I switch back the same instance of ContentPage is used but a Loaded is triggered instead.
This can make it difficult to reason about the logical lifecycle of a page/control from a memory management point of view. If I have a viewmodel in the BindingContext of a page, I want it to hang around until the page is completely thrown away. I don't know of an event or other mechanism I can rely on to determine this. It does seem to invite memory leaks if one isn't very, very careful.
I'm going to have a play with the navigation side of things to see if there's any mileage there.
Has any further information been provided per the comment below?
https://github.com/dotnet/maui/issues/7329#issuecomment-1134349911
Came from https://github.com/dotnet/maui/issues/7329#issuecomment-1134349911
Registering page (and its ViewModel) as Transient when using Shell still does not create new instances upon visit.
I think it's clear we need to provide some more clarity on how this works exactly and we're working on it.
@jfversluis Is there any "clarification" now after 1 year?
I'm still in a position where I'm struggling to clean up view models as I can't detect when a page is truly finished with. Any guidance would be very welcome!
I found some answers in the source code and by doing some testing
- Singleton lives forever (Application level)
- Scoped means that the services are tight to the
Windowwhich for example on Android means theActivityLong story short: on Mobile this is equal toSingleton - Transient every time it is requested, creates a new instance: you need to dispose it yourself
I tested this with Shell and it creates a new instance every time, obviously both Page and ViewModel need to be registered as
Transient
Pay attention to this though: in contrast with ASP.NET Core you can request Scoped services from Singleton pages.
The singleton context has its own Scope which is different from the Scoped one.
I highly suggest to not use the Scoped lifetime as it works in unexpected ways (again, it shouldn't be possible to request a Scoped service from Singleton scope).
@albyrock87 So the MAUI framework never ever disposes any transient items for you? That seems insane. As @ederbond suggested, there should be something in the framework that disposes views when they are no longer in the navigation stack. It will take a lot of extra code and thought to correctly dispose manually all throughout the application.
I agree, it's seemingly a gaping hole but MAUI isn't the first to do this - WPF and others are the same. You tend to find it in UI frameworks and you end up with stuff like multiple implementations of "weak event handlers" etc.
We run into the event problem where a UI widget needs to listen to something but ended up getting around most of the time using the IMessenger stuff here.
However this does not help when UI components do stuff like create timers or other objects which need cleaning up. In this case you're basically SOL unless you use app-specific trickery to implement cleanup/disposal logic yourself. This is very, very error prone and even taking care throughout our app development, I'm fairly certain there'll be things we've missed.
This was obviously identified as an issue back with Xamarin, due to things such as the LifecycleEffect (see here) being created. As MAUI has rejected effects in favour of handler-based solutions this never came across, but amazingly no alternative was provided.
When I spotted the documentation on handlers I thought I'd found a solution as I assumed that you could correlate the removal of the handler to the "disposal" of the C#/MAUI backing object. However, as per my comment above this is not quite the case as on some platforms the handler can be detached but the C#/MAUI object re-used later, having another handler attached to the same instance. In addition I have seen handlers not be removed/set to null despite the object clearly not being used any longer. This behaviour is not specifically documented so I don't know if it is a bug with the framework or simply a documentation oversight.
It's all a bit confusing!! I think we just need guidance on how to tell when individual Views/Pages can be classed as "done with" so we can tear them down properly. To be honest I get the feeling this is an area the MAUI team have had trouble with themselves due to platform differences, and even they probably don't know all the nuances of it themselves.
@ksoftllc I will probably publish a tiny library to handle all of this in the proper way in a couple of weeks. I'm building something that sits on top of Microsoft implementation, without creating a whole new framework/pattern. As soon as it's ready I'll post here.
@albyrock87 That sounds really interesting - I'd like to see how you've overcome the issues we've been discussing. If you need testers let me know.
Another one for the seemingly endless backlog. They haven't released many MAUI updates the last few months, safe to say I'm a touch nervous.
Just pinged the team and I hope the come here and tell us a workaround or something... It's almost impossible to maintain the app state healthy if we can't dispose stuffs properly on VM. Specially when you're working with https://okazuki.jp/ReactiveProperty/features/ReactiveProperty.html which I love and I've been using since 2018 on all of my apps along with prism library which allowed me to correctly dispose my VM's. But now since prism is currently not working on MAUI with Shell I have no alternative.
Can you explain more about your scenario? What do you have on a viewmodel which requires explicit Disposal? Are you experiencing memory issues because of long-lived objects? Are you experiencing them with the latest .NET preview versions?
@hartez I do make a heavy use of reactive programing using these 2 libraries: https://github.com/dotnet/reactive https://okazuki.jp/ReactiveProperty/
So on my ViewModels, I have a lot of IObservable<T> and then inside of my VM's I do subscribe to these observables to react on changes between my VM and some business services that I have. The problem becomes when I have a Service that was registered as Singleton.
Follows a sample that reproduces the issue:
https://github.com/ederbond/PleaseDisposeMe/blob/master/README.md
Steps to reproduce the issue:
- Set a breakpoint on Page2ViewModel line 27
- Run the app on debug mode
- Click on the button called "Notify Now"
- You'll see that the label with the current time is updated on the screen. (cool)
- Click "Go To Page 2"
- You'll see the app will hit your break point a single time (cool)
- Go back to page 1
- Click on the button called "Notify Now"
- You'll see that your break point on Page2V is still getting called wich is bad cause that page was registered as Transient on MauiProgram
- Then Navigate again to Page 2
- You'll see that your break point will be hit one time again (cool)
- Go back to Page 1 and click on the button called "Notify Now"
You'll see that your breakpoint will stop 2x. And if you go back and forth you'll see that your break point will be hitted several times. That's because your Transient ViewModel is still live and listening for data coming from that observable. I my guess is that tha VM will never ever be disposed because it holds subscription from my SingletonService. In the past time of Xamarin.Forms it wasn't an issue for me cause I wasn't using Shell and I was using Prism Library which offers me an interface called IDestructible that when implemented on my VM was always calling a void Destroy() method for me. But since prism is not supporting Maui nor Shell (Dan Seigel already told me that they doesn't even have a plan to support shell in the future). So if someone decides to make Reactive Programing on Maui with Shell you're on a big trouble. This examples clearly shows that without a proper easy and reliable extension point from the MAUI framework to dispose objects inside a VM your app will have serious memory leaks.
Note that this problem has nothing to do specifically with these 3rd party libraries that I'm using. The same will happen whenever you try to implement the Observable design pattern on your own. Cause your Observable object (Usually a Singleton service) will hold references to it's observers (which are hold by VM).
Having walking dead View Models listening to events from long lived observers not just cause memory leak, but also causes serious business logic issues and random crashed depending of the state it will eventually takes.
Can you explain more about your scenario? What do you have on a viewmodel which requires explicit Disposal? Are you experiencing memory issues because of long-lived objects? Are you experiencing them with the latest .NET preview versions?
Some of our views might create graphics objects for effects, animations etc. which allocate memory and require clean up when they're done with.
In addition, view models might bind up to singleton service objects, bind to native handlers/create native objects which require clean-up, create timers, background threads or other unmanaged support components.
Not stuff which affects "Hello World" level apps, but as soon as your app goes beyond the basics, these things crop up pretty quickly.
@RobTF I haven't had enough time to finish my work as I wanted and probably I won't have time in the next few months, So for now, I'm making my repository public and eventually accepting PRs (I've reserved the namespace on NuGet but I haven't prepared the GitHub actions to publish the library).
This is my repo if you want to take a look: https://github.com/albyrock87/maux
So for now, if you need to solve this problem in your project you just need to:
- Register your page and view model as
Scoped- You can do the same for other services which needs to share the page lifetime
- In your page's constructor ask for the view model
public MyPage(MyPageViewModel viewModel)and set it to theBindingContext - Define a
ScopedRouteFactory(see here] - Use it:
- In case you're using
Shell=>Routing.RegisterRoute("MyRoute", ScopedRouteFactory<MyPage>.Instance); - In case you're using custom navigation =>
navigation.PushAsync(ScopedRouteFactory<MyPage>.Instance.GetOrCreate())
- In case you're using
This way all of the Scoped dependencies will be disposed when the page is unloaded, either by the Shell or the custom navigation.
Please be aware that Shell root pages will never be disposed, because Shell keeps them "cached" in the background.
@albyrock87 Consider naming your library MauiX. Maux is hard to pronounce and looks clunky. Thanks!
@albyrock87 It's great that you've provided the community with a repo to address this issue, however I'm always reluctant to use a library which doesn't describe exactly what it is doing and why...
You've mentioned in the readme that the repo addresses the problem of developers not fully understanding how or why DI scoping should be used, and at this point that includes myself...
But it would be valuable if your readme could explain what, but more importantly why the repo is doing, so as to plug the gap of understanding.
@albyrock87 It's great that you've provided the community with a repo to address this issue, however I'm always reluctant to use a library which doesn't describe exactly what it is doing and why...
You've mentioned in the readme that the repo addresses the problem of developers not fully understanding how or why DI scoping should be used, and at this point that includes myself...
But it would be valuable if your readme could explain what, but more importantly why the repo is doing, so as to plug the gap of understanding.
It is irrelevant why Dispose needs to be called. The point here is that it doesn't work properly in MAUI. There are countless options for using Dispose. Consider for example 3D rendering component or map component. Or page which should save state to hard drive when you exit.
@albyrock87 It's great that you've provided the community with a repo to address this issue, however I'm always reluctant to use a library which doesn't describe exactly what it is doing and why... You've mentioned in the readme that the repo addresses the problem of developers not fully understanding how or why DI scoping should be used, and at this point that includes myself... But it would be valuable if your readme could explain what, but more importantly why the repo is doing, so as to plug the gap of understanding.
It is irrelevant why Dispose needs to be called. The point here is that it doesn't work properly in MAUI. There are countless options for using Dispose. Consider for example 3D rendering component or map component. Or page which should save state to hard drive when you exit.
I didn't ask why dispose needs to be called.
I asked why I should use the repo supplied by @albyrock87 to solve this problem and why.
Did you actually read my post before you got angry about it?
@ryanlpoconnell @janseris you don't have to use my repo at all :) especially because right now it is abandoned. I just wanted to share some code which solves the issue which actually is just a few lines of code: https://github.com/albyrock87/maux/blob/main/Maux.Mvvm/ScopedRouteFactory.cs Anyway, I see that Shell navigation has many limitations that would frustrate me in a real world applications, so I'm not going to use that anymore.
Verified this issue with Visual Studio Enterprise 17.8.0 Preview 5.0. Can repro on windows platform.
Any plans around this @davidortinau @hartez @PureWeen @StephaneDelcroix ?
This isn't really a bug; it would be more of enhancement to have better control over service resolution so that you could fully leverage Service Scopes.
Implementing the dispose pattern against various navigation scenarios doesn't really scale. What if someone wants to reuse a VM or Page? We can't decide ourselves when to call dispose. If you want to use the dispose pattern, then you have to tie your life cycles to something you control, not that we control.
If you register your components as scoped service, those types will survive inside the DI container even if we were to call Dispose ourselves. The only way for something that's registered as a scoped service and that implements Idisposable to get collected by the garbage collector is to dispose the service container that you created the service from.
https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection-guidelines#disposal-of-services
One of the things we'd need to look at enhancing is the service resolution here https://github.com/dotnet/maui/blob/1269d81a741a8672894c0cf79b2b0cf84602731d/src/Controls/src/Core/Shell/ShellContent.cs#L76
We could also look at adding settings to shell so navigation gets scoped, but this all becomes a bit tricky with the MS.EXt.DI container because it doesn't support child scopes.
We could also look at adding life cycle interfaces that you could sprinkle around that would give contextual information to your View Models.
Let's build this issue into a spec :-)
There are probably a few other factors at play here. For example, we have been fixing various memory leaks in our components which were causing Pages to not get Garbage Collected. If you have a scenario where a popped page isn't getting Garbage collected, please log an issue with a repro and we can figure out what's causing the page to pin in memory.
@PureWeen I don't care about child scopes on msft.ext.di Just call Dispose() on Views and ViewModels that were registered as Transient AND implements IDisposable. So when I want to have a page + vm to be disposed when I navigate away from it I'll register it as transient, and when I want to have a page or vm to be reused I'll register it as singleton. Maui just need to call dispose on transient objects that implements IDisposable. That's it