What Happens When No One Takes Out the Garbage?
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:
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.
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!
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)
{
this.SendObjectDisappearingMessage();
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.
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. {The companion solution, SafeDIContainer
, is a work in progress. Please ignore for now.}
The code is published as open source and without encumbrance.
The post Taking Control Of Variable Lifecycle appeared first on Marcus Technical Services.