MAUI stops keeping the display up to date when an HTTP request is made

Boucourt 105 Reputation points
2024-12-24T18:56:53.3366667+00:00

I am new to MAUI programming as well as parallel Thread programming.

I have retrieved a MAUI application that I am trying to modify.

The application captures a sound sample and transmits it to a server to identifies its origin among a set of determined audio bands.

I display on the waiting screen an animation as well as an indication that the application is running and is trying to identify the sound.

The ViewModel calls SyncAudioThread.

This class starts 2 Threads

1- The first manages a Timer that requests the display of the status every second. The status is maintained in a static Class updated by the second Thread

2- The second Thread captures the sound file, transmits a request to the server and processes the response.

I have placed 3 Actions BreakPoint that write a trace to the output Window

BreakPoint 1 SyncAudioThread.TimerTic()

BreakPoint 2 SyncAudioThread.RelaseAccessUpdateData()

BreakPoint 3 ViewModel.UpdateStatus()

We can verify that the 3 methods are called in sequence and the data is updated.

The display progresses normally until I start the HTTP request.

From this moment on, even if the 3 methods continue to be called, the display is no longer updated.

Can someone explain to me why?

The SyncAudioThread Class

namespace PAL_FE.BaseService
{
    public class SyncAudioThread<T>
                    where T : class, ISoundCaptureViewModel
    {
        private readonly Thread _soundThread;
        private readonly Thread _timerThread;
        private static bool _shouldStop = false;
        private static T _viewModel;
        //Data to control sharing with other thread
        private static int usingRessource = 0; // pour Interlock
        public SyncAudioThread(T viewModel)
        {
            _viewModel = viewModel;
            //Démarre le Thread pour le Timer avant 
            _timerThread = new Thread(TimerTic);
            _timerThread.Start();
            // Maintenant Capture un échantillon et recherche le son
            _soundThread = new Thread(Run) { IsBackground = true };
            _soundThread.Start();
        }
        public void Stop()
        {
            _shouldStop = true;
        }
        /// <summary>
        /// This Thread will
        ///    Capture sound
        ///    Send Request
        ///    while updating the status
        /// </summary>
        public async void Run()
        {
            IDispatcherTimer soundCaptureTimer;
            // Lance l'enregistrement
            try
            {
                GainAccess();
                AudioResultAndProgressionStatus.SoundsampleFilePath = string.Empty;
                AudioResultAndProgressionStatus.RetryNumber = 1;
                AudioResultAndProgressionStatus.ElapseTime = 0;
                AudioResultAndProgressionStatus.Phase = PhaseValue.Recording;
                RelaseAccessUpdateData();
            }
            catch(Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
            bool started = await AudioServices.CaptureAudio();
            if (!started)
            {
                AudioResultAndProgressionStatus.Phase = PhaseValue.RecordingFail;
                _viewModel.UpdateStatus();
                Stop();
                return;
            }
            // Prépare et lance le soundCaptureTimer
            soundCaptureTimer = Application.Current!.Dispatcher.CreateTimer();
            soundCaptureTimer.Interval = TimeSpan.FromSeconds(AudioServices.RecordingTime);
            soundCaptureTimer.IsRepeating = false;
            soundCaptureTimer.Tick += (s, e) =>
            {
                    //soundCaptureTimer.IsRepeating = false;
                    //soundCaptureTimer.Stop();
                    string? audioPath = AudioServices.StopRecordingAndSaveAsync().Result;
                    if (audioPath != null)
                    {
                        try
                        {
                            GainAccess();
                            AudioResultAndProgressionStatus.SoundsampleFilePath = audioPath;
                            AudioResultAndProgressionStatus.RetryNumber = 1;
                            AudioResultAndProgressionStatus.ElapseTime = 0;
                            AudioResultAndProgressionStatus.Phase = PhaseValue.WaitingHttpResponse;
                            RelaseAccessUpdateData();
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine(ex.Message);
                        }
                    ApiResponseModel<AudioResponseData> result = AudioServices.FindEpisode(audioPath).Result;
                    try
                        {
                            GainAccess();
                            AudioResultAndProgressionStatus.ElapseTime = 0;
                            AudioResultAndProgressionStatus.Phase = PhaseValue.Idle;
                            RelaseAccessUpdateData();
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine(ex.Message);
                        }
                        _viewModel.ThreadConplete(result);
                    }
                    _shouldStop = false;
                    soundCaptureTimer.IsRepeating = false;
            };
            soundCaptureTimer.IsRepeating = false;
            soundCaptureTimer.Start();
        }
        /// <summary>
        /// Permet de maintenir et d'afficher le temps
        /// </summary>
        private static void TimerTic()
        {
            while (!_shouldStop)
            {
                Thread.Sleep(1000);
                try
                {
                    GainAccess();
                    AudioResultAndProgressionStatus.ElapseTime++;
                    RelaseAccessUpdateData();
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.Message);
                }
            }
        }
        /// <summary>
        /// Take the Interlock to allow the caller to modify SharedData with other Thread
        /// It is CRUCIAL to call RelaseAccessUpdateData
        /// </summary>
        private static void GainAccess()
        {
            bool gotIt = false;
            int nbRetry = 0; 
            while (!gotIt)
            {
                if (0 == Interlocked.Exchange(ref usingRessource, 1))
                {
                    gotIt = true;
                }
                else
                {
                    gotIt = false;
                    nbRetry++;
                    if (nbRetry >= 3) throw new Exception("Not able to gain access to SharedRessource");
                    Thread.Sleep(1);
                }
            }
        }
        /// <summary>
        /// Update Shared Data and release the Interlock
        /// </summary>
        private static void RelaseAccessUpdateData()
        {
            _viewModel.UpdateStatus();
            Interlocked.Exchange(ref usingRessource, 0);
        }
    }
}

The ViewModel Class

namespace PAL_FE.ViewModels.Synchronization
{
    public partial class SynchronizationPageViewModel : BaseViewModel, ISoundCaptureViewModel
    {
        [ObservableProperty]
        string progression = string.Empty;
        #region Constructor
        public SynchronizationPageViewModel(INavigation nav)
        {
            try
            {
                Navigation = nav;
            }
            catch (Exception ex)
            {
            }
            Progression = $"Initialisation";
        }
        #endregion
        #region Properties
        private AudioResponseData _EpisodeDetail;
        public AudioResponseData EpisodeDetail
        {
            get { return _EpisodeDetail; }
            set
            {
                if (_EpisodeDetail != value)
                {
                    _EpisodeDetail = value;
                    OnPropertyChanged("EpisodeDetail");
                }
            }
        }
        private bool _IsLoading;
        public bool IsLoading
        {
            get { return _IsLoading; }
            set
            {
                if (_IsLoading != value)
                {
                    _IsLoading = value;
                    OnPropertyChanged("IsLoading");
                }
            }
        }
        private int _audioSampleCount = 0;
        public int AudioSampleCount
        {
            get { return _audioSampleCount; }
            set
            {
                if (_audioSampleCount != value)
                {
                    _audioSampleCount = value;
                    OnPropertyChanged("AudioSampleCount");
                }
            }
        }
        private int tryNumber = 0;
        private string _action = string.Empty;
        private int _timer = 0;
        public void UpdateStatus()
        {
            Progression = $"Try: {AudioResultAndProgressionStatus.RetryNumber} {AudioResultAndProgressionStatus.Phase}  Timer: {AudioResultAndProgressionStatus.ElapseTime}";
            OnPropertyChanged("Progression");
        }
        private int _elapsedTimeInSeconds = 0;
        public int ElapsedTimeInSeconds
        {
            get { return _elapsedTimeInSeconds; }
            set
            {
                if (_elapsedTimeInSeconds != value)
                {
                    _elapsedTimeInSeconds = value;
                    OnPropertyChanged("ElapsedTimeInSeconds");
                }
            }
        }
        #endregion
        #region Command
        #endregion
        #region Methods
        /// <summary>
        /// Ceci démarrera un Thread en background pour capturer une sequence audio
        /// et demander au serveur d'identifier l'épisode
        /// </summary>
        /// <returns></returns>
        public async Task FindEpisode()
        {
            var _thread = new SyncAudioThread<SynchronizationPageViewModel>(this);
        }
        public void ThreadConplete(Object resp)
        {
            ApiResponseModel<AudioResponseData?>? _response = resp as ApiResponseModel<AudioResponseData?>;
            if (_response != null && _response.Success )
            {
                Navigation.PushModalAsync(new Views.MainScreen.EpisodePage(EpisodeDetail), false);
            }
            else
            {
                bool answer = App.Current.MainPage.DisplayAlert("FindEpisode Fail", "Would you like to retry?", "Yes", "No").Result;
                if (answer)
                {
                    Navigation.PopAsync();
                }
            }
        }
    }
}

The Output Windows

11:00:02:065 Method: TimerTic Phase:"Recording" Timer: 1 11:00:02:065 Method: ReleaseAccessUpdateData Phase: "Recording" Timer: 1 11:00:02:065 Method viewModel.UpdateStatus Phase:"Recording" Timer: 1 11:00:03:061 Method: TimerTic Phase:"Recording" Timer: 2 11:00:03:061 Method: ReleaseAccessUpdateData Phase: "Recording" Timer: 2 11:00:03:061 Method viewModel.UpdateStatus Phase:"Recording" Timer: 2 11:00:04:075 Method: TimerTic Phase:"Recording" Timer: 3 11:00:04:075 Method: ReleaseAccessUpdateData Phase: "Recording" Timer: 3 11:00:04:075 Method viewModel.UpdateStatus Phase:"Recording" Timer: 3 11:00:04:371 [InsetsSourceConsumer] applyRequestedVisibilityToControl: visible=true, type=statusBars, host=com.companyname.pal_fe/crc64f68ab4d1bcdd83f6.MainActivity 11:00:05:313 Method: TimerTic Phase:"Recording" Timer: 4 11:00:05:313 Method: ReleaseAccessUpdateData Phase: "Recording" Timer: 4 11:00:05:313 Method viewModel.UpdateStatus Phase:"Recording" Timer: 4 11:00:06:310 Method: TimerTic Phase:"Recording" Timer: 5 11:00:06:310 Method: ReleaseAccessUpdateData Phase: "Recording" Timer: 5 11:00:06:310 Method viewModel.UpdateStatus Phase:"Recording" Timer: 5 11:00:06:475 [monodroid-assembly] open_from_bundles: failed to load assembly NAudio.Core.dll 11:00:06:475 [monodroid-assembly] open_from_bundles: failed to load assembly netstandard.dll 11:00:06:531 Loaded assembly: /data/user/0/com.companyname.pal_fe/files/.override/NAudio.Core.dll [External] 11:00:06:531 Loaded assembly: /data/user/0/com.companyname.pal_fe/files/.override/netstandard.dll [External] 11:00:06:721 [panyname.pal_fe] Explicit concurrent copying GC freed 543KB AllocSpace bytes, 128(4636KB) LOS objects, 82% free, 5316KB/29MB, paused 65us,30us total 27.641ms 11:00:06:815 [panyname.pal_fe] Explicit concurrent copying GC freed 53KB AllocSpace bytes, 0(0B) LOS objects, 82% free, 5294KB/29MB, paused 45us,40us total 33.108ms 11:00:06:914 Resolved pending breakpoint at 'AudioServices.cs:126,1' to void PAL_FE.BaseService.AudioServices.<FindEpisode>d__6.MoveNext () [0x00025]. 11:00:06:972 Resolved pending breakpoint at 'AudioServices.cs:179,1' to void PAL_FE.BaseService.AudioServices.<>c__DisplayClass6_0.<<FindEpisode>b__0>d.MoveNext () [0x0027b]. 11:00:06:972 Resolved pending breakpoint at 'AudioServices.cs:185,1' to void PAL_FE.BaseService.AudioServices.<>c__DisplayClass6_0.<<FindEpisode>b__0>d.MoveNext () [0x002cd]. 11:00:06:972 Resolved pending breakpoint at 'AudioServices.cs:189,1' to void PAL_FE.BaseService.AudioServices.<>c__DisplayClass6_0.<<FindEpisode>b__0>d.MoveNext () [0x002ec]. 11:00:07:027 [DOTNET] System.Net.Http.MultipartFormDataContent 11:00:07:027 Resolved pending breakpoint at 'ApiServices.cs:141,1' to void PAL_FE.BaseService.ApiServices.<FindEpisodeAsync>d__4.MoveNext () [0x001f2]. 11:00:07:027 Resolved pending breakpoint at 'ApiServices.cs:147,1' to void PAL_FE.BaseService.ApiServices.<FindEpisodeAsync>d__4.MoveNext () [0x00211]. 11:00:07:027 Resolved pending breakpoint at 'ApiServices.cs:137,1' to void PAL_FE.BaseService.ApiServices.<FindEpisodeAsync>d__4.MoveNext () [0x001e9]. 11:00:07:027 Resolved pending breakpoint at 'ApiServices.cs:131,1' to void PAL_FE.BaseService.ApiServices.<FindEpisodeAsync>d__4.MoveNext () [0x001c8]. 11:00:07:072 Method: ReleaseAccessUpdateData Phase: "WaitingHttpResponse" Timer: 0 11:00:07:072 Method viewModel.UpdateStatus Phase:"WaitingHttpResponse" Timer: 0 11:00:07:090 [TrafficStats] tagSocket(147) with statsTag=0xffffffff, statsUid=-1 11:00:07:320 Method: TimerTic Phase:"WaitingHttpResponse" Timer: 1 11:00:07:320 Method: ReleaseAccessUpdateData Phase: "WaitingHttpResponse" Timer: 1 11:00:07:567 Method viewModel.UpdateStatus Phase:"WaitingHttpResponse" Timer: 1 11:00:08:566 Method: TimerTic Phase:"WaitingHttpResponse" Timer: 2 11:00:08:566 Method: ReleaseAccessUpdateData Phase: "WaitingHttpResponse" Timer: 2 11:00:08:566 Method viewModel.UpdateStatus Phase:"WaitingHttpResponse" Timer: 2 11:00:09:560 Method: TimerTic Phase:"WaitingHttpResponse" Timer: 3 11:00:09:560 Method: ReleaseAccessUpdateData Phase: "WaitingHttpResponse" Timer: 3 11:00:09:560 Method viewModel.UpdateStatus Phase:"WaitingHttpResponse" Timer: 3 11:00:10:572 Method: TimerTic Phase:"WaitingHttpResponse" Timer: 4 11:00:10:823 Method: ReleaseAccessUpdateData Phase: "WaitingHttpResponse" Timer: 4 11:00:10:823 Method viewModel.UpdateStatus Phase:"WaitingHttpResponse" Timer: 4 11:00:11:809 Method: TimerTic Phase:"WaitingHttpResponse" Timer: 5 11:00:11:809 Method: ReleaseAccessUpdateData Phase: "WaitingHttpResponse" Timer: 5 11:00:11:809 Method viewModel.UpdateStatus Phase:"WaitingHttpResponse" Timer: 5 11:00:12:814 Method: TimerTic Phase:"WaitingHttpResponse" Timer: 6 11:00:12:814 Method: ReleaseAccessUpdateData Phase: "WaitingHttpResponse" Timer: 6 11:00:12:814 Method viewModel.UpdateStatus Phase:"WaitingHttpResponse" Timer: 6 11:00:14:069 Method: TimerTic Phase:"WaitingHttpResponse" Timer: 7 11:00:14:069 Method: ReleaseAccessUpdateData Phase: "WaitingHttpResponse" Timer: 7 11:00:14:069 Method viewModel.UpdateStatus Phase:"WaitingHttpResponse" Timer: 7 11:00:15:059 Method: TimerTic Phase:"WaitingHttpResponse" Timer: 8 11:00:15:059 Method: ReleaseAccessUpdateData Phase: "WaitingHttpResponse" Timer: 8 11:00:15:059 Method viewModel.UpdateStatus Phase:"WaitingHttpResponse" Timer: 8 11:00:16:074 Method: TimerTic Phase:"WaitingHttpResponse" Timer: 9 11:00:16:074 Method: ReleaseAccessUpdateData Phase: "WaitingHttpResponse" Timer: 9 11:00:16:316 Method viewModel.UpdateStatus Phase:"WaitingHttpResponse" Timer: 9 11:00:17:318 Method: TimerTic Phase:"WaitingHttpResponse" Timer: 10 11:00:17:318 Method: ReleaseAccessUpdateData Phase: "WaitingHttpResponse" Timer: 10 11:00:17:318 Method viewModel.UpdateStatus Phase:"WaitingHttpResponse" Timer: 10 11:00:48:879 Method: TimerTic Phase:"WaitingHttpResponse" Timer: 11 11:00:51:408 Method: ReleaseAccessUpdateData Phase: "WaitingHttpResponse" Timer: 11 11:00:51:408 Method viewModel.UpdateStatus Phase:"WaitingHttpResponse" Timer: 11

.NET MAUI
.NET MAUI
A Microsoft open-source framework for building native device applications spanning mobile, tablet, and desktop.
3,777 questions
C#
C#
An object-oriented and type-safe programming language that has its roots in the C family of languages and includes support for component-oriented programming.
11,158 questions
0 comments No comments
{count} votes

2 answers

Sort by: Most helpful
  1. Bruce (SqlWork.com) 68,876 Reputation points
    2024-12-24T19:06:46.83+00:00

    your Gain and Release access methods limit thread processing to one at a time.


  2. Boucourt 105 Reputation points
    2024-12-30T19:46:16.46+00:00

    I managed to get the UI to continue working during the Http request.

    After intensive research, I modified the method that call the Http service .

    Instead of just calling the service,

    ApiResponseModel<AudioResponseData> finalresult = ApiServices.FindEpisodeAsync(streamForAudio, refs).Result;

    I did the following

    `try`
    

    {

    await Task.Run(async () =>

    {

    ApiResponseModel<AudioResponseData> finalresult = ApiServices.FindEpisodeAsync(streamForAudio, refs).Result;

    }).ConfigureAwait(false);

    }

    But, I don't really understand why I have to do this, I was already in another Thread.

    If someone can explain to me

    0 comments No comments

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.