【问题标题】:UI doesn't update until async method finishesUI 在异步方法完成之前不会更新
【发布时间】:2017-03-16 14:37:42
【问题描述】:

在我的项目中,我想显示一个进度指示器,从 web 服务中检索数据并隐藏进度指示器。如果我这样做,我目前必须等到我从 web 服务中检索到数据,然后立即更新带有结果的 UI。但进度指示器永远不会出现。看来,我正在 UI 线程上工作或阻塞。但是在哪里?

这是我的代码的简单版本:

一些页面

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。这允许您运行异步任务并从中报告进度。
  • @LukeSamuel 如果您正确使用 TPL,则不需要 BGW。

标签: c# xamarin xamarin.forms async-await ui-thread


【解决方案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,由于绑定,视图会自动显示正在运行的 ActivityIndi​​cator。

【讨论】:

  • 我检查了my code runs on the UI thread 是否确实如此。所以我了解到 async/await 不会生成新线程。不完全理解为什么有时在可等待方法完成之前调用不可等待的方法......但我发现了我的问题,你的 MVVM 提示很有帮助。谢谢。
  • @testing 很高兴听到您发现了问题。我认为线程及其秘密生命是软件开发中最大的谜团。 ;)
【解决方案2】:

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

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

我不确定您想要视觉反馈的位置:在 SomePage 上还是在 SomeView 上。答案并不重要,只是显示和隐藏视觉反馈的命令最好写在显示反馈的类中。

在 SomeView 上显示反馈

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

由于我对 Forms 很熟悉,所以我会把它写成 Form。您的代码将非常相似

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 上有多个微调器,用于从 web 服务中单独加载的元素。我试图在您的示例之后重组我的代码,但行为保持不变。现在我发现了问题。不过,感谢您的帮助。
【解决方案3】:

非常愚蠢的错误。我有这样的事情:

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

我应该删除我的问题吗?也许是因为有时你会只见树木不见森林。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多