Forum Xamarin.iOS

How to handle async calls if the controller no longer exists

jbrightjbright USMember ✭✭
edited October 2013 in Xamarin.iOS

Scenario: suppose that you have a remote service that you call and it processes the request asynchronously. When the data is returned, you'll refresh your local UI. One example:

public async override void ViewDidLoad()
{
    base.ViewDidLoad();

    // Start the spinner.
    NetworkActivity.Start();

    // Don't block the user.
    await Task.Run(() =>
    {
        _myRemoteData = FooService.GetMyData(someParams);
    });

    // Picks back up here
    NetworkActivity.Stop();

    // Populate our TableView...
    var dataSource = new DataSource(_myRemoteData);
    // do some other work, then we'll reload our table view
    TableView.Source = _dataSource;
    TableView.ReloadData();
}

The code above is essentially a cleaner version of this code:

public override void ViewDidLoad()
{
    base.ViewDidLoad();

    // Start the spinner.
    NetworkActivity.Start();

    // Don't block the user.
    ThreadPool.QueueUserWorkItem((cb) =>
    {
        _myRemoteData = FooService.GetMyData(someParams);
        InvokeOnMainThread(() =>
        {
            // Picks back up here
            NetworkActivity.Stop();

            // Populate our TableView...
            var dataSource = new DataSource(_myRemoteData);
            // do some other work, then we'll reload our table view
            TableView.Source = _dataSource;
            TableView.ReloadData();
        }
    });
}

My question is: What happens if this object (ViewController) is disposed before the service returns? When InvokeOnMainThread gets called or the await Task finishes, what happens? Do I need some defensive coding in place or will it just not get called? I don't recall seeing this addressed in any of the demos.

TIA.

JB

Posts

  • adamkempadamkemp USInsider, Developer Group Leader mod

    It will get called, and that will probably cause problems. You can work around this in a few ways:

    1. Don't call Dispose() on your view controller. Let the GC take care of it when there are no more references (the async task should have a reference to it). In this case you still have to guard against the view no longer being on the screen, but at least it's safe to call methods on the view controller.
    2. When you view controller leaves the screen (ViewWillDisappear) cancel the async task and do any synchronization necessary to ensure that its cancellation has been acknowledged by the view controller before disposing it.

    The second option may be safer, but is also harder to accomplish.

  • jbrightjbright USMember ✭✭

    Right, because .net will still have a reference to the task. In cases where I can use a cancellation token, I will. But in some cases I can't. When I can't, I was going to use a flag that states that the view is no longer valid.

    I see that ViewDidUnload is depreciated. ViewWillDisappear may not be the right method to use (the view is still around and may be re-presented as in a TabController design).

    I am not calling the Dispose on the ViewController myself, but I'm assuming that it gets called pretty quickly after a modal is popped. Another approach would be to put the flag there and treat that similar to a cancellation token.

    Thanks. This helps and gives me some ideas.

    JB

  • adamkempadamkemp USInsider, Developer Group Leader mod

    Dispose will only be called in one of two situations:
    1. You called it in your code.
    2. The finalizer called it.

    The second one only happens when there are no references to it. So you can be sure that Dispose will not be called outside of your code as long as you have a reference to it from some other live object. Once Dispose is called, though, you should not call any methods on it or you might crash. Even doing something like checking whether the view is loaded will crash if Dispose has been called.

    In iOS > 6 the view for a view controller will never unload once it is loaded so you can't use that as a flag. You can, however, check whether the view is in a window (View.Window != null), which is a simple way to check if it's at least visible. The caveat is that this may return false if the view controller is being entirely covered by a full-screen modal view controller (you will also get a ViewWillDisappear call in that case).

    The most sane thing to do would probably be to have some flag that your code explicitly sets when you are done with it rather than trying to rely on some state from UIViewController.

  • jbrightjbright USMember ✭✭

    Thanks Adam. I was specifically thinking about cases where something like this might be in the frameworking:

    // dunno... maybe framework code works like this.
    using (var vc = new FooViewController) 
    {
         // vc code is called, does its thing
         // ...
         // about to exit scope, and IDispose called
    }
    

    (Not saying that this is how it works) But I think you're right in that the ViewController would still have a reference from the task. So dispose shouldn't get called until that async completes.

    At any rate, thanks. Trying to make sure that I am coding using a reasonable pattern as I switch over to async/await.

    JB

  • HermanSchoenfeldHermanSchoenfeld AUMember ✭✭
    edited September 2014

    So in other words there is no answer to this and it's yet another source of crashes in Xamarin iOS apps.

    If I don't call Dispose on every UIView in the object graph, they never get collected (due to other reasons).

    If I call Dispose, sometimes this issue occurs.

    Xamarin's "memory management" page recommends calling Dispose on views.

    Xamarin also recommends await/async.

    They are incompatiable obviously.

  • adamkempadamkemp USInsider, Developer Group Leader mod

    There are established patterns for avoiding most memory leaks in Xamarin. Specifically, you have to detach all event handlers on UI objects (and gesture recognizers) when your view is leaving the screen. Have you done that throughout your app? If you do then calling Dispose should not be necessary in nearly every case.

  • adamkempadamkemp USInsider, Developer Group Leader mod

    I should note that my recommendation not to call Dispose is not a Xamarin recommendation. It's my own recommendation based on my experience with using Xamarin.iOS for a few years and seeing the crashes that result. Honestly I think some of the people who wrote the documentation for Xamarin (especially the early documentation) knew less about how this stuff works at the time than I do. It's not that they were doing something dumb. They were being reasonable, like you. They saw an object that implements IDisposable and so they wrote a call to Dispose. So did I when I first started. And most of the time that works. But then I found that sometimes it causes crashes, and there is no way to avoid those crashes short of not calling Dispose. Now that I understand the system more fully, and understand how it interacts with Objective-C land, I have concluded that it is only safe to call Dispose on NSObjects in a few narrow use cases. Otherwise it is usually unnecessary and possibly dangerous. So I recommend that people not do it except in those rare cases, like with images that are used only for the duration of a stack frame or with CGContext objects.

Sign In or Register to comment.