Search code examples
c#androidtimermaui

How do I avoid Task.Delay (in a Timer's Elapsed method) throwing a NullReferenceException, the second time?


I'm trying to take pictures using a timer in a .Net Maui app. I have the pictures working just fine, and can get 5-10 pictures on the first run of the timer, but not on a second call to the timer method. Running the "StartTimer" method (in the code below) more than once throws "System.NullReferenceException: 'Object reference not set to an instance of an object.'" on the Task.Delay call or subsequent firings of a System.Timers.Timer event. This crashes the app, even though I have try...catch blocks.

I've also tried using a System.Threading.Timer, but that never fires.

I know why the exception happens. Well, sort of. Even if I don't include a CancellationToken or if I create a new token, it still throws the error.

I'm using the Camera.Maui NuGet plugin for the camera functions, if that matters. If I could get the AutoSnapShot functionality to work, I probably wouldn't need an answer here, but that's a different question.

This is a .Net 7 Maui app targeting Android API 32. I've only tested this in Android so far, but I've tested it in both Release and Debug modes on real devices running API versions 28, 31, and 33. The only difference is the API 28 phone takes 1 picture on the second run, then throw the exception, and then takes another picture. Also, the exception doesn't crash the app. This allows me to try running the method multiple times, but each time has the same issue as the second run.

So, what am I doing wrong and/or how do I avoid the NullReferenceException?

Code

The problem code in its own class. I'm calling it from an async method in MainPage.xaml.cs. Even if I create a new instance of the class for the "timer" variable, it still throws the NullReferenceException. I also changed the class to implement IDisposable to try to flush out any residual... whatever that could be affecting the reuse of the class or method, and it still is throwing the exception.

Edit: I've done some more tests with the "timer" variable being local with and without implementing IDisposable as well as not using the using statement while implementing IDisposable, and all tests still throw the NullReferenceException when running the "StartTimer" more than once.

private readonly TimerTrigger timer = new();

    private async Task<bool> StartCapture()
    {
        try 
        {
            ...
            /*
            using TimerTrigger timer = new();
            */
            timer.StartTimer(quantity, timerDelay);
            ...
        }
        catch
        {
            return false;
        }

        return true;
    }

Version 1, with Task.Delay:

    public async void StartTimer(int quantity, int timerDelay)
    {
        try
        {
            CancellationTokenSource wtoken = new();
            for (int i = 0; i < quantity; i++)
            {
                if (i > 0)
                {
                    try
                    {
                        // It doesn't matter if I use a token or not, the exception is thrown
                        // await Task.Delay(timerDelay);
                        // await Task.Delay(timerDelay, CancellationToken.None);
                        await Task.Delay(timerDelay, wtoken.Token);
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine(ex.Message); // This is only hit in API 28
                    }
                }

                // Take picture
            }
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message); // This is never hit
        }
    }

Version 2, with a System.Timers.Timer:

private System.Timers.Timer aTimer;
private int totalQuantity = 0;
private int totalTaken = 0;

    public void StartTimer(int quantity, int timerDelay)
    {
        totalQuantity = quantity;
        aTimer = new()
        {
            Interval = timerDelay
        };

        aTimer.Elapsed += SnapPic;
        aTimer.AutoReset = true;
        aTimer.Enabled = true;
    }

    private void SnapPic(object sender, ElapsedEventArgs e)
    {
        try
        {
            // Take picture
            totalTaken++;
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message); // This is never hit
            EndCapture(false);
        }

        EndCapture(true);
    }
    
    private void EndCapture(bool success)
    {
        if (!success || totalTaken >= totalQuantity)
        {
            aTimer.AutoReset = false;
            aTimer.Enabled = false;
        }
    }

Version 3, using System.Threading.Timer:

private int totalQuantity = 0;
private int totalTaken = 0;

    public void StartTimer(int quantity, int timerDelay)
    {
        AutoResetEvent autoEvent = new(true);
        totalQuantity = quantity;
        using Timer bTimer = new(SnapPic, autoEvent, 0, timerDelay);
        // using Timer bTimer = new(SnapPic, null, 0, timerDelay); // This doesn't run, either
    }

    private void SnapPic(object sender)
    {
        // This method is apparently never run
        try
        {
            // Take picture
            totalTaken++;
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
            EndCapture(false); // Reusing this method from Version 2
        }

        EndCapture(true); // Reusing this method from Version 2
    }

Solution

  • After reading some more about timers and playing with another project that uses a System.Timers.Timer, I've found a solution.

    In Version 2, instead of setting properties to start the timer, I needed to call the Start method. And instead of setting properties to end the timer, I needed to call the Close and Dispose methods.

    Old and broken:

    // Start timer
    aTimer.AutoReset = true;
    aTimer.Enabled = true;
    
    // End timer
    aTimer.AutoReset = false;
    aTimer.Enabled = false;
    

    New and working:

    // Start timer, I mean what else would we do, right?
    aTimer.Start();
    
    // End timer, obviously... {facepalm}
    aTimer.Close();
    aTimer.Dispose();
    

    And I had to add "totalTaken = 0;" to the "EndCapture" method, if anyone cares. Not resetting this variable caused me to think the problem was still happening when putting "timer" back as a property of the "MainPage" class.

    Now I just need to rename that "aTimer" variable. I've already stripped out a bunch of the non-working test code and versions 1 and 3.