Reputation: 20289
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?
Upvotes: 4
Views: 2716
Reputation: 20289
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.
Upvotes: 0
Reputation: 30492
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));
}
}
Upvotes: 0
Reputation: 7189
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.
Upvotes: 1