Taking Control Of Variable Lifecycle - Marcus Technical Services
I recently wrestled with an app that simply would not behave. The view models seemed hyperactive. When I pressed the HOME button, and the app went to sleep, it often came back hung. I dug and dug and could not find anything overtly wrong. So I created an experiment to see what happens when we change pages and their view models in a simple app:
private void StartFirstTest() { TestCompleted += StartSecondTest; Device.BeginInvokeOnMainThread(async () => { var secondViewModel = SetUpTest(); var firstPage = new FirstPage { BindingContext = new FirstViewModel() }; Debug.WriteLine("About to assign the main page to the first page."); SetMainPage(firstPage); Debug.WriteLine("Finished assigning the main page to the first page."); await Task.Delay(5000); Debug.WriteLine("About to assign the main page to the second page."); SetMainPage(new SecondPage { BindingContext = secondViewModel }); secondViewModel.Message = "Working..."; Debug.WriteLine("Finished assigning the main page to the second page."); Debug.WriteLine("The first view model is now OUT OF SCOPE and should not be active."); }); }
In SetUpTest, I just created a view model that would finish the test, along with a timer:
private SecondViewModel SetUpTest() { var secondViewModel = new SecondViewModel { TimeRemaining = TOTAL_BROADCAST_TIME }; _timer = new Timer(DELAY_BETWEEN_BROADCASTS); var timeToStop = DateTime.Now + TOTAL_BROADCAST_TIME; _timer.Elapsed += (sender, args) => { FormsMessengerUtils.Send(new TestPingMessage()); secondViewModel.TimeRemaining -= TimeSpan.FromMilliseconds(DELAY_BETWEEN_BROADCASTS); if (DateTime.Now >= timeToStop) { secondViewModel.Message = "FINISHED"; secondViewModel.TimeRemaining = TimeSpan.FromSeconds(0); Debug.WriteLine("Starting garbage collection"); GC.Collect(); Debug.WriteLine("Finished garbage collection"); _timer.Stop(); _timer.Dispose(); TestCompleted?.Invoke(); } }; _timer.Start(); return secondViewModel; }
It’s much simpler than it looks:
- I create a FirstViewModel that listens to global messages. It has no “hooks” to the outside world except that it is the BindingContext of the FirstPage.
- I set the app’s MainPage to the FirstPage and the FirstViewModel.
- I change the MainPage to the SecondPage and its view model.
The question is, whatever happens to the first view model? This one’s easy: it goes out of scope, so it stops functioning (“sleeps”) until the garbage collector comes along and sweeps it up. Right?
_ Not quite. _Here’s the output:
// This was expected –- the first view model is still active The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! About to assign the main page to the second page. Finished assigning the main page to the second page. The first view model is now OUT OF SCOPE and should not be active. // I do not want the view model doing anything at this point! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! The First View Model is still listening to events! // The only reason this dies is that I force-call GC.Collect(). // Otherwise it would have gone on forever. Starting garbage collection
The FirstViewModel is orphaned but _ wide-awake _ and as _ hungry as an infant _. The Xamarin.Forms garbage collector is in another part of town. Maybe it will stop by later; maybe tomorrow. No one knows.
When you see the end of the output, that is not the true end of the FirstViewModel. I manually kill it at the end of the timed test by calling the garbage collector myself. Once the GC arrives, it dispatches the FirstViewModel _ immediately _.
We spend a lot of time writing source code with a sense that whatever we do is intuitively logical. It makes sense that when we change pages, everything just stops. How else would an app work? Wouldn’t that cause an app to become unstable due to hyperactivity and reaction? Woudn’t it tend to hang? Welcome to Xamarin.Forms.
We could nullify all of our variables after using them, but that is considered very bad practice, and can confuse the garbage collector. And the sheer hassle! Imagine inserting lines like this everywhere:
SetMainPage(new SecondPage { BindingContext = secondViewModel }); firstPage.BindingContext = null; firstPage = null;
Everything Needs a Beginning and an End
Any class that contains activities should contain a _ lifecycle _for starting and stopping them safely. Currently, the garbage collector is responsible for “stopping” things, but it is too lazy to be reliable. That is how it is designed.
Xamarin.Forms created one obvious end-of-life override at the ContentPage — but that does not exist _ anywhere** else**_.
Creating and Wiring Up a Complete Lifecycle
These are the only reliable overrides available. When a page becomes visible, OnAppearing fires. When it goes away, OnDisappearing fires. Since pages are the basis of an app, this is a good place to start.
Here are some interfaces to create clean contracts:
public interface IReportEndOfLifecycle { event EventUtils.GenericDelegate<object> IsDisappearing; } public interface IReportLifecycle : IReportEndOfLifecycle { event EventUtils.GenericDelegate<object> IsAppearing; }
Then the ContextPageWithLifecycle itself:
public interface IContentPageWithLifecycle : IReportLifecycle { } public class ContentPageWithLifecycle : ContentPage, IContentPageWithLifecycle { public event EventUtils.GenericDelegate<object> IsAppearing; public event EventUtils.GenericDelegate<object> IsDisappearing; protected override void OnAppearing() { base.OnAppearing(); IsAppearing?.Invoke(this); } protected override void OnDisappearing() { base.OnDisappearing(); IsDisappearing?.Invoke(this); this.SendObjectDisappearingMessage(); } }
The ContentPage raises an event in each case.
There are two possible consumers of these events:
- A ContentView that is generally nested inside a ContentPage
- A view model that provides business logic for the ContentView or for the page itself
Interface contracts:
public interface IHostLifecycleReporter { IReportLifecycle LifecycleReporter { get; set; } } public interface ICanCleanUp { bool IsCleaningUp { get; set; } }
And the ContentViewWithLifecycle, in sections for clarity:
public interface IHostLifecycleReporter { IReportLifecycle LifecycleReporter { get; set; } } public interface IContentViewWithLifecycle : IHostLifecycleReporter, IReportEndOfLifecycle, ICanCleanUp { } public class ContentViewWithLifecycle : ContentView, IContentViewWithLifecycle { public static BindableProperty PageLifecycleReporterProperty = CreateContentViewWithLifecycleBindableProperty ( nameof(LifecycleReporter), default(IReportLifecycle), BindingMode.OneWay, (contentView, oldVal, newVal) => { contentView.LifecycleReporter = newVal; } );
The IReportLifestyle contract means that we have to maintain a reference to any entity that provides us with page appearing and disappearing events. It’s a light contact point — only a few events — but it does “couple” us a bit. This is unavoidable.
public ContentViewWithLifecycle(IReportLifecycle lifeCycleReporter = null) { LifecycleReporter = lifeCycleReporter; }
Our new ContentView must report page ending to others, so provides an event for that:
private IReportLifecycle _lifecycleReporter; public ContentViewWithLifecycle(IReportLifecycle lifeCycleReporter = null) { LifecycleReporter = lifeCycleReporter; } public IReportLifecycle LifecycleReporter { get => _lifecycleReporter; set { _lifecycleReporter = value; if (_lifecycleReporter != null) { this.SetAnyHandler ( handler => _lifecycleReporter.IsDisappearing += OnDisappearing, handler => _lifecycleReporter.IsDisappearing -= OnDisappearing, (lifecycle, args) => { } ); } } }
In the example above, we listen with weak events to reduce the drag created by the reference to our “parent” ContentPage.
Finally, we add an IsCleaningUp Boolean to flag this class to halt its activities in preparation for the garbage collector.
~ContentViewWithLifecycle() { if (!IsCleaningUp) { IsCleaningUp = true; } } private bool _isCleaningUp; public bool IsCleaningUp { get => _isCleaningUp; set { if (_isCleaningUp != value) { _isCleaningUp = value; if (_isCleaningUp) { // Notifies the safe di container and other concerned foreign members this.SendObjectDisappearingMessage(); // Notifies close relatives like view models IsDisappearing?.Invoke(this); } } } } protected virtual void OnDisappearing(object val) { IsCleaningUp = true; }
In the code above, I have added a finalizer (“destructor”) — ~ContentViewWithLifecycle() — just in case the IsCleaningUp fails to be called before garbage collection. That is an optional, extreme precaution.
The ViewModelWithLifecyle is not shown here because it is the same as ContentView except that it is the “end of the line” so does not have a responsibility to notify classes below it to shut down.
Here is how these three classes interact at run-time:
Consuming and Managing the Lifecycle
In the ContentView and ViewModel , whenever IsCleaningUp is set to true, we stop all activity _ immediately: _
public class FirstViewModelWithLifecycle : ViewModelWithLifecycle, IFirstViewModelWithLifecycle { public FirstViewModelWithLifecycle() { Debug.WriteLine("The first view model with lifecycle is being created."); FormsMessengerUtils.Subscribe<TestPingMessage>(this, OnTestPing); } private void OnTestPing(object sender, TestPingMessage args) { if (!IsCleaningUp) { Debug.WriteLine("The first view model with lifecycle is still listening to events!"); } } ~FirstViewModelWithLifecycle() { FormsMessengerUtils.Unsubscribe<TestPingMessage>(this); Debug.WriteLine("The first view model with lifecycle is FINALIZED."); } }
We also need to “wire up” these classes so they know about each other. Here is the expanded test app:
var firstViewModelWithLifecycle = new FirstViewModelWithLifecycle(); var firstPageWithLifecycle = new FirstPageWithLifecycle { BindingContext = firstViewModelWithLifecycle }; firstViewModelWithLifecycle.LifecycleReporter = firstPageWithLifecycle; Debug.WriteLine("About to assign the main page to the first page with Lifecycle."); SetMainPage(firstPageWithLifecycle); Debug.WriteLine("Finished assigning the main page to the first page with Lifecycle."); await Task.Delay(5000); Debug.WriteLine("About to assign the main page to the second page."); SetMainPage(new SecondPage { BindingContext = secondViewModel }); secondViewModel.Message = "Working..."; Debug.WriteLine("Finished assigning the main page to the second page."); Debug.WriteLine("The first view model with Lifecycle is now OUT OF SCOPE and should not be active.");
Notice this line. We pass the firstPageWithLifecycle to the firstViewModelWithLifecycle :
firstViewModelWithLifecycle.LifecycleReporter = firstPageWithLifecycle;
There’s no ContentView in this example, but it could be added easily using the same baton-passing technique.
Drum roll, please!
The first view model with lifecycle is being created. About to assign the main page to the first page with Lifecycle. Finished assigning the main page to the first page with Lifecycle. The first view model with lifecycle is still listening to events! The first view model with lifecycle is still listening to events! The first view model with lifecycle is still listening to events! The first view model with lifecycle is still listening to events! The first view model with lifecycle is still listening to events! About to assign the main page to the second page. Finished assigning the main page to the second page. The first view model with Lifecycle is now OUT OF SCOPE and should not be active. // SUCCESS !
Hard Proofs
I created a Xamarin.Forms mobile app to demonstrate the source code in this article. The source is available on GitHub at https://github.com/marcusts/SafeDiContainer . The solution is called LifecycleAware.sln. For unit tests and a complete running example, see SmartDIWithLifeCycle.sln.
The code is published as open source and without encumbrance.