Search code examples
c#androidxamarinmessagingcenter

Issue with MessagingCenter firing multiple times Xamarin


I have looked all over for a solution to an issue. I have noticed that in my android app, every time I fire an event from <button Clicked="GPSToggle_Clicked">, for some reason it increments the number of times my methods get called. So after I compile and load this on my phone, I hit my "GPSToggle_Clicked" button, and then to stop hit that button again. On the first "stop", I'll get a single instance of the below output:

    ---------------------------------------------------------------Attempting string parsing
---------------------------------------------------------------Sending string to SubmitGeneratedGPX
---------------------------------------------------------------path: /storage/emulated/0/Download/GPX/2022-10-27-02-44-06.gpx
---------------------------------------------------------------GPX File creation success
---------------------------------------------------------------:RawBufferToJsonString: [{"Timestamp":"2022-10-27T18:43:52.849+00:00","Latitude":41.5263818,"Longitude":-81.6507923,"Altitude":153.29998779296875,"Accuracy":20.0,"VerticalAccuracy":1.7990270853042603,"Speed":null,"Course":null,"IsFromMockProvider":false,"AltitudeReferenceSystem":2},{"Timestamp":"2022-10-27T18:43:53.696+00:00","Latitude":41.5263819,"Longitude":-81.6507921,"Altitude":153.29998779296875,"Accuracy":20.0,"VerticalAccuracy":1.7697961330413818,"Speed":null,"Course":null,"IsFromMockProvider":false,"AltitudeReferenceSystem":2},{"Timestamp":"2022-10-27T18:43:54.526+00:00","Latitude":41.5263819,"Longitude":-81.6507921,"Altitude":153.29998779296875,"Accuracy":20.0,"VerticalAccuracy":1.7697961330413818,"Speed":null,"Course":null,"IsFromMockProvider":false,"AltitudeReferenceSystem":2},{"Timestamp":"2022-10-27T18:43:55.374+00:00","Latitude":41.5263819,"Longitude":-81.6507921,"Altitude":153.29998779296875,"Accuracy":20.0,"VerticalAccuracy":1.7697961330413818,"Speed":null,"Course":null,"IsFromMockProvider":false,"AltitudeReferenceSystem":2},{"Timestamp":"2022-10-27T18:43:56.21+00:00","Latitude":41.5263811,"Longitude":-81.650792,"Altitude":153.29998779296875,"Accuracy":20.0,"VerticalAccuracy":1.7160584926605225,"Speed":null,"Course":null,"IsFromMockProvider":false,"AltitudeReferenceSystem":2}]

Every subsequent time I hit start/stop on the app, I get the real-time data in the output multiplied by the number of times I've started/stopped since the last compiling.

the main app page button event thats fired:

private async void GPSToggle_Clicked(object sender, EventArgs e)
    {
        var LocationPermissionStatus = await Xamarin.Essentials.Permissions.RequestAsync<Xamarin.Essentials.Permissions.LocationAlways>();
        var FileReadPermissionStatus = await Xamarin.Essentials.Permissions.RequestAsync<Xamarin.Essentials.Permissions.StorageRead>();
        var FileWritePermissionStatus = await Xamarin.Essentials.Permissions.RequestAsync<Xamarin.Essentials.Permissions.StorageWrite>();
        if(LocationPermissionStatus == Xamarin.Essentials.PermissionStatus.Denied)
        {
            // TODO
            return;
        }
        
        // run if device is android
        if(Device.RuntimePlatform == Device.Android)
        {
            if (!CrossGeolocator.Current.IsGeolocationAvailable || !CrossGeolocator.Current.IsGeolocationEnabled)
            {
                // gps is not enabled, throw alert
                Console.WriteLine("---------------------------------------------------------------GPS is DISABLED");
                await DisplayAlert("Error", "GPS is not enabled. You must enable GPS to use this feature", "Ok");
            }
            else
            {
                // set our IsTracking = true flag
                if (!IsTracking)
                {
                    // start background listening for GPS
                    await StartListening();
                    Console.WriteLine("---------------------------------------------------------------Listening: " + CrossGeolocator.Current.IsListening);
                    StartService();
                    Console.WriteLine("---------------------------------------------------------------Service initiated");
                    IsTracking = true;
                    Console.WriteLine("---------------------------------------------------------------Tracking initiated");
                    GPSToggle.Text = "Stop Tracking";
                }
                else
                {
                    //
                    // verify that the submittal wasn't done in error, before stopping services and submitting data
                    bool DoneInError = await DisplayAlert("Finish?", "Are you sure you want to stop services and submit?", "No", "Yes");
                    if (!DoneInError)
                    {
                        await StopListening();
                        Console.WriteLine("---------------------------------------------------------------listening:" + CrossGeolocator.Current.IsListening);
                        IsTracking = false;
                        Console.WriteLine("---------------------------------------------------------------Tracking ceased");
                        // stop the gps service
                        StopService();
                        Console.WriteLine("---------------------------------------------------------------Service ceased");
                        // stop the background listening for gps
                        Console.WriteLine("---------------------------------------------------------------Attempt GPX parse from buffer obj");

                        GPSToggle.Text = "Start Tracking";
                    }
                }
            }
        }
    }

Specifically the line:

StartService();

Fires this method off within the same class, specifically the MessagingCenter.Send<>, which initiates my foreground service to handle logging the gps data into a buffer:

private void StartService()
    {            
        var startServiceMessage = new StartServiceMessage();
        MessagingCenter.Send(startServiceMessage, "ServiceStarted");
        Preferences.Set("LocationServiceRunning", true);
        StatusLabel.Text = "Location service has been started";
        Console.WriteLine("---------------------------------------------------------------location service has been started. preferences saved");
    }

and

StopService(); 

Fires this method off to stop the services and retrieve the gps buffer data from the foreground to the main thread:

private void StopService()
    {
        var stopServiceMessage = new StopServiceMessage();
        MessagingCenter.Unsubscribe<App, List<Location>>(this, "GPXBufferData");
        MessagingCenter.Subscribe<App, List<Location>>(this, "GPXBufferData", (sender, args) =>
        {
            RawGPXData = args;
            Generate_CreateGPX_File(RawGPXData);
            RawBufferToJsonString = GPXParse.GenerateJSON_GPXPoints(RawGPXData);
            Console.WriteLine("---------------------------------------------------------------:RawBufferToJsonString: " + RawBufferToJsonString);
            PromptForSubmission_GPXPoints_API();
        });
        Console.WriteLine("--------------------------------------------------------------------------");
        MessagingCenter.Send(stopServiceMessage, "ServiceStopped");
        Preferences.Set("LocationServiceRunning", false);
        Console.WriteLine("---------------------------------------------------------------Location service stopped. preferences saved");
    }

In the above snippet, this line is subscribed to in the GPSLocationService.cs file:

MessagingCenter.Send(stopServiceMessage, "ServiceStopped");

This is a portion of my GPSLocationService.cs file that is relevant to this:

public async Task Run(CancellationToken token)
    {
        int ObjCount = 0;
        await Task.Run(async () => {
            // if the task was stopped
            // check the buffer for data, if data, send to GPXGenerator
            MessagingCenter.Subscribe<StopServiceMessage>(this, "ServiceStopped",
            message =>
            {
                if (GPSBufferObj != null)
                {
                    Device.BeginInvokeOnMainThread(() =>
                    {
                        MessagingCenter.Unsubscribe<App, List<Location>>((App)Xamarin.Forms.Application.Current, "GPXBufferData");
                        MessagingCenter.Send<App, List<Location>>((App)Xamarin.Forms.Application.Current, "GPXBufferData", GPSBufferObj);

                    });
                }
            });

            return;
        }, token);
    }

I believe I have tracked down where the issue is starting. In my StopService() method, I have the following line (just to keep track of where Im at in the buffer) and it is only sent to output once.

Console.WriteLine("--------------------------------------------------------------------------");

BUT if I place that same line within the pasted portion of my GPSLocationService.cs file, I will get the incremented output. I'm leaning towards the nested task being the issue, I wrote this based losely off of this example repro: https://github.com/jfversluis/XFBackgroundLocationSample


Solution

  • You don't have MessagingCenter.Unsubscribe<StopServiceMessage> anywhere in your code. StopServiceMessage is what you are accumulating subscriptions to.


    You need to make sure Unsubscribe is unsubscribing the instance that was previously subscribed to. It sounds to me like there are multiple instances of GPSLocationService. [In which case, this is no longer referring to the original instance. Unsubscribe won't do anything, unless you have the this that was originally Subscribed.]

    If so, instead create an instance of GPSLocationService ONCE, and store it in a static variable. Re-use it. start/stop it, but don't discard it.


    Alternatively, if you only want a message ONE TIME from each Subscribe, then Unsubscribe as soon as you receive each message:

    MessagingCenter.Subscribe<StopServiceMessage>(this, "ServiceStopped",
      message =>
      {
        MessagingCenter.Unsubscribe<StopServiceMessage>(this, "ServiceStopped");
        ... your code ...
      });
    

    Use this same pattern EVERYWHERE you have a Subscribe (unless you Subscribe ONLY ONE TIME at app start, as Jason suggested.)