4

In my project I want to show a progress indicator, retrieve the data from the webservice and hide the progress indicator. If I do this I currently have to wait until I retrieved the data from the webservice and then the UI with the results is immediately updated. But the progress indicator never appears. So it seems, that I'm working or blocking on the UI thread. But where?

This is a simple version of my code:

SomePage

private async void OnItemSelected(object sender, SelectedItemChangedEventArgs e)
{
    await ShowDetailView();
}

private async Task ShowDetailView()
{
    var view = new MyView();
    this.detailView = view;
    await view.LoadContent();
}

SomeView

public async Task LoadContent()
{
    var success = await LoadData();
    if (success)
    {
        ShowInformation();
    }
}

public async Task<bool> LoadData()
{
    try
    {
        ShowLoadingProcess();
        await Task.Delay(5000);
        this.itemList = await WebService.Instance.GetData();

        return true;
    }
    catch (Exception ex)
    {
        System.Diagnostics.Debug.WriteLine(ex.Message);
        return false;
    }
    finally
    {
        HideLoadingProcess();
    }
}

private void ShowInformation()
{
    Device.BeginInvokeOnMainThread(() =>
    {
        this.grid.Clear();

        foreach(var item in this.itemList)
        {
            GridItem contentItem = new GridItem(item);
            this.grid.Children.Add(contentItem);
        }
    }
}

private void ShowLoadingProcess()
{
    Device.BeginInvokeOnMainThread(() => BringProgressIndicatorToFront());
}

private void HideLoadingProcess()
{
    Device.BeginInvokeOnMainThread(() => BringProgressIndicatorToBack());
}

I tried different things, where I got out of sync between the UI and the background thread. E.g. ShowInformation() was called, before LoadData() finished.

Can someone give me a hint about what's wrong here?

Timo Salomäki
  • 7,099
  • 3
  • 25
  • 40
testing
  • 19,681
  • 50
  • 236
  • 417
  • You're looking for a [BackgroundWorker](https://developer.xamarin.com/api/type/System.ComponentModel.BackgroundWorker/). This allows you to run an asynchronous task and report progress from it. – Luke Caswell Samuel Mar 16 '17 at 14:41
  • 3
    @LukeSamuel There's no real need for a BGW if you're using the TPL properly. – Servy Mar 16 '17 at 16:12

3 Answers3

1

I'm not fully sure why your code acts the way it does but you should take into account that Device.BeginInvokeOnMainThread() doesn't give any promises about when the action is executed. Internally it does the following:

// Simplified
s_handler = new Handler(Looper.MainLooper);
s_handler.Post(action);

It passes your action to the message queue which then executes the action at an unknown point of time in the future.

However, what I would suggest you is to rethink the architecture of your application. By taking advantage of the MVVM pattern and data binding you would be able to control the visibility of the progress indicator very easily.

In the ViewModel you would have the following:

private bool isBusy;

public bool IsBusy() {
    get { return isBusy; }
    set { SetProperty(ref isBusy, value); }
}

In the View that has the above ViewModel as its BindingContext, you would have something like the following:

<ActivityIndicator IsRunning="{Binding IsBusy}" IsVisible="{Binding IsBusy}" />

Now, whenever you'd start a long running operation, you'd simply change isBusy to true and the View would automatically show the running ActivityIndicator due to the binding.

Timo Salomäki
  • 7,099
  • 3
  • 25
  • 40
  • I checked if [my code runs on the UI thread](https://forums.xamarin.com/discussion/comment/200008/#Comment_200008) and it does. So I learned async/await does not generate a new thread. Don't fully understand why sometimes the non awaitable method was called, before the awaitable method has finished ... But I found my issue and your tip with MVVM was helpful. Thanks. – testing Mar 17 '17 at 14:27
  • @testing Good to hear that you found the issue. I think that threads and their secret life is the greatest mystery in software development. ;) – Timo Salomäki Mar 17 '17 at 14:34
0

I'm not familiar with xamarin.forms, so the following could be utterly rubbish. If so, please do tell me, I'll delete the answer.

To me it seems you have some Form with an event handler OnItemSelected that should Show a DetailView, which is some other form. After creation of the DetailView object you want to load the detail view. This loading takes some time, and in the mean time you want to inform the operator that the form is loading.

I'm not sure where you want the visual feedback: on SomePage or on SomeView. The answer does not really matter, except that the command to show and hide the visual feedback is best written in the class that shows the feedback.

Show the feedback on SomeView

In this case SomeView has something to show and hide the visual feedback, like a progress bar, or an ajax loader. Code would be like:

As I am familiar with Forms, I'll write it as a Form. Your code will be very similar

class SomeView : Form
{
    void ShowVisualFeedBack()
    {
        ...
    }
    void HideVisualFeedBack()
    {
        ...
    }

    public async Task LoadDataAsync()
    {
        bool result = false;
        this.ShowVisualFeedBack()
        try
        {
            this.itemList = await WebService.Instance.GetData();
            result = true;
        }
        catch (Exception ex)
        {
            System.Diagnostics.Debug.WriteLine(ex.Message);
        }
        this.HideVisualFeedBack();
        return result;
    }
}

Or if you really need to wait 5 seconds before you start getting the data:

 try
 {
      await Task.Delay(TimeSpan.FromSeconds(5));
      this.itemList = await WebService.Instance.GetData();
      result = true;
  }

Show the feedback on SomePage

 class SomePage : Form
{
    void ShowVisualFeedBack()
    {
        ...
    }
    void HideVisualFeedBack()
    {
        ...
    }

    private async Task ShowDetailView()
    {
        this.ShowVisualFeedBack();
        this.DetailView = new MyView();
        await view.LoadContent();
        this.HideVisualFeedBack();
     }
}

By the way, while your thread is awaiting GetData(), it can't update your visual feedback. Some visual feedback methods like GIF don't need updates by your thread, but if you have something like a progress bar, your thread needs to update it. You can do this by awaiting a maximum time until GetData is completed, update the progress, and await again

var taskWaitASec = Task.Delay(TimeSpan.FromSeconds(1));
var taskGetData = WebService.Instance.GetData();

// note: you are not awaiting yet, so you program continues:

while (!taskGetData.IsCompleted)
{
    var myTasks = new Task[] {taskWaitASec, taskGetData}
    var completedTask = await Task.WhenAny(myTasks);
    if (completedTask == taskWaitASec)
    {
        UpdateProgress();
        taskWaitASec = Task.Delay(TimeSpan.FromSeconds(1));
    }
}
Harald Coppoolse
  • 28,834
  • 7
  • 67
  • 116
  • I'm showing a spinner and so I don't need to update the UI in between. Currently, I have multiple spinners on *SomePage* (main) and on *SomeView* for the separate loaded elements from the webservice. I tried to restructure my code after your example, but the behavior stayed the same. Now I found the issue. Nevertheless, thanks for your help. – testing Mar 17 '17 at 14:17
0

Very silly mistake. I had something like this:

private async Task ShowDetailView()
{
    var view = new MyView();
    this.detailView = view;
    await view.LoadContent();
    this.mainGrid.Children.Add(this.detailView);
}

Of course he waits until the operation finishes and then the results are shown. This is the correct way:

private async Task ShowDetailView()
{
    var view = new MyView();
    this.detailView = view;
    this.mainGrid.Children.Add(this.detailView);
    await view.LoadContent();
}

Should I delete my question? Perhaps, because sometimes you miss the forest for the trees.

testing
  • 19,681
  • 50
  • 236
  • 417