UI在异步方法完成之前不会更新。

4
在我的项目中,我想展示一个进度指示器,从Web服务检索数据并隐藏进度指示器。如果我这样做,我目前必须等到从Web服务检索到数据,然后立即更新带有结果的UI。但是进度指示器从未出现。所以看起来,我正在UI线程上工作或阻塞。但是在哪里呢?
这是我的代码的简单版本:
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());
}

我尝试了不同的方法,但在UI界面和后台线程之间出现了不同步的情况。例如,在LoadData()完成之前就调用了ShowInformation()

有人可以给我一个提示,这里出了什么问题吗?


你正在寻找一个BackgroundWorker。这可以让你运行异步任务并从中报告进度。 - Luke Caswell Samuel
3
如果您正确使用 TPL,则没有真正需要 BGW。 - Servy
3个回答

1
我不确定你的代码为什么会出现这种情况,但你应该考虑到 Device.BeginInvokeOnMainThread() 并不保证何时执行操作。在内部它执行以下操作:
// Simplified
s_handler = new Handler(Looper.MainLooper);
s_handler.Post(action);

它将您的操作传递到消息队列,然后在未来的某个时间执行该操作。
但是,我建议您重新思考应用程序的架构。通过利用MVVM模式和数据绑定,您将能够轻松控制进度指示器的可见性。
在ViewModel中,您将拥有以下内容:
private bool isBusy;

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

在具有上述ViewModel作为其BindingContext的视图中,您将拥有以下内容:
<ActivityIndicator IsRunning="{Binding IsBusy}" IsVisible="{Binding IsBusy}" />

现在,每当您启动长时间运行的操作时,只需将isBusy更改为true即可,由于绑定,视图会自动显示正在运行的ActivityIndicator。

我检查了一下我的代码是否在UI线程上运行,它确实是。所以我学到了async/await不会生成新的线程。有时候不可等待的方法被调用,而等待的方法还没有完成,我并不完全理解为什么会这样...但我找到了我的问题,并且你关于MVVM的提示很有帮助。谢谢。 - testing
@testing 很高兴听到您找到了问题所在。我认为线程及其神秘的生命周期是软件开发中最大的谜团。 ;) - Timo Salomäki

0

我对xamarin.forms不熟悉,所以以下内容可能完全是垃圾。如果是这样,请告诉我,我会删除答案。

在我看来,您有一个带有事件处理程序OnItemSelected的表单,应该显示一个DetailView,这是另一个表单。创建DetailView对象后,您想要加载详细视图。这个加载需要一些时间,在此期间,您希望通知操作员表单正在加载。

我不确定您想要在哪里进行视觉反馈:在SomePage上还是在SomeView上。答案并不重要,除了显示和隐藏视觉反馈的命令最好写在显示反馈的类中之外。

在SomeView上显示反馈

在这种情况下,SomeView有一些东西可以显示和隐藏视觉反馈,例如进度条或ajax加载器。代码将如下:

由于我熟悉Forms,我将其编写为一个表单。您的代码将非常相似。

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;
    }
}

或者如果你真的需要在开始获取数据之前等待5秒钟:

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

在SomePage上显示反馈

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

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

顺便提一下,当您的线程等待GetData()时,它无法更新您的视觉反馈。一些视觉反馈方法,如GIF,不需要由您的线程更新,但如果您有像进度条这样的东西,您的线程需要更新它。您可以通过等待最长时间直到GetData完成,更新进度并再次等待来实现这一点。
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));
    }
}

我正在展示一个旋转器,因此我不需要在中间更新UI。目前,在SomePage(主要)和SomeView上有多个旋转器,用于从网络服务加载的单独元素。我尝试按照您的示例重构我的代码,但行为保持不变。现在我找到了问题所在。尽管如此,还是谢谢你的帮助。 - testing

0
非常愚蠢的错误。我有类似这样的东西:
private async Task ShowDetailView()
{
    var view = new MyView();
    this.detailView = view;
    await view.LoadContent();
    this.mainGrid.Children.Add(this.detailView);
}

当然,他会等待操作完成,然后显示结果。这是正确的方式:
private async Task ShowDetailView()
{
    var view = new MyView();
    this.detailView = view;
    this.mainGrid.Children.Add(this.detailView);
    await view.LoadContent();
}

我应该删除我的问题吗?也许是的,因为有时你会把树木当成森林。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接