如何确保 Xamarin/MAUI ViewModel 数据在 UI 尝试使用它之前已加载?

huangapple go评论73阅读模式
英文:

How do I ensure Xamarin/MAUI ViewModel data has loaded before the UI attempts to consume it?

问题

在我的应用程序中,我从一个REST API请求一组车辆并将它们存储到一个属性中。这是通过使用异步任务完成的。我的页面使用一个选择器来消耗这个可观察集合。当前,当页面加载时,取决于使用的速度,选择器可能尚未填充。这通过等待一秒钟然后重新打开选择器来解决。

我的假设是任务尚未完成,而异步运行意味着UI已经继续运行。然而,我不能确定,因为我不确定如何在UI加载时测试其值。

我想知道可以采取哪些措施,以确保用户不会遇到这种数据未加载的情况。

简化的视图模型

internal class BaseVehicleViewModel : BaseViewModel
{
    public BaseVehicleViewModel()
    {
        Task.Run(async () => await GetAssets()).ConfigureAwait(false);
    }

    protected ObservableCollection<Asset> vehicles;

    public ObservableCollection<Asset> Vehicles
    {
        get => vehicles;
        set => SetProperty(ref vehicles, value);
    }

    protected async Task GetAssets()
    {
        var vehicles = await App.AppServiceClient.AssetsAsync();
        if (vehicles != null)
        {
            Vehicles = new ObservableCollection<Asset>(vehicles.Where(v => v.Active == 1));
        }
    }
}

简化的视图

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="App.Views.Page"
             xmlns:vm="clr-namespace:App.ViewModels">

    <ContentPage.BindingContext>
        <vm:ViewModel />
    </ContentPage.BindingContext>

    <ContentPage.Content>
        <Picker Title="Select a vehicle"
                ItemsSource="{Binding Vehicles}"
                ItemDisplayBinding="{Binding Reg}"
                SelectedItem="{Binding SelectedVehicle, Mode=TwoWay}"/>
    </ContentPage.Content>
</ContentPage>
英文:

In my application, I request a collection of vehicles back from a REST API and store them to a property. This is done using an async task. My page consumes this with a picker which binds to this observable collection. Currently when the page loads, depending on how quickly it is used, the picker has not populated yet. This resolves it's self by waiting for a second and re-opening the picker.

My assumption is that the task has not finished and it running async means the UI has continued without it. However I couldn't say for certain because I'm not sure how I could test its value at time of UI load.

I would like to know what measures I can take to ensure that the user will not experience this type of data not loading.

Simplied View Model

    internal class BaseVehicleViewModel : BaseViewModel
    {
        public BaseVehicleViewModel()
        {
            Task.Run(async () =&gt; await GetAssets()).ConfigureAwait(false);

        }

        protected ObservableCollection&lt;Asset&gt; vehicles;

        public ObservableCollection&lt;Asset&gt; Vehicles
        {
            get =&gt; vehicles;
            set =&gt; SetProperty(ref vehicles, value);
        }

        protected async Task GetAssets()
        {
            var vehicles = await App.AppServiceClient.AssetsAsync();
            if (vehicles != null)
            {
                Vehicles =
                    new ObservableCollection&lt;Asset&gt;(vehicles.Where(v =&gt; v.Active == 1));
            }
        }

Simplified View

&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot; ?&gt;
&lt;ContentPage xmlns=&quot;http://xamarin.com/schemas/2014/forms&quot;
             xmlns:x=&quot;http://schemas.microsoft.com/winfx/2009/xaml&quot;
             x:Class=&quot;App.Views.Page&quot;
             xmlns:vm=&quot;clr-namespace:App.ViewModels&quot;&gt;

    &lt;ContentPage.BindingContext&gt;
        &lt;vm:ViewModel /&gt;
    &lt;/ContentPage.BindingContext&gt;

    &lt;ContentPage.Content&gt;
        &lt;Picker Title=&quot;Select a vehicle&quot;
                ItemsSource=&quot;{Binding Vehicles}&quot;
                ItemDisplayBinding=&quot;{Binding Reg}&quot;
                SelectedItem=&quot;{Binding SelectedVehicle, Mode=TwoWay}&quot;/&gt;
    &lt;/ContentPage.Content&gt;
&lt;/ContentPage&gt;

答案1

得分: 3

> My assumption is that the task has not finished and it running async means the UI has continued without it.

我假设任务尚未完成,而异步运行意味着UI已经继续而不等待它。

That is correct; the code currently executes the loading as a fire-and-forget method. Fire-and-forget has several problems: one is that it's not easy/possible to detect when it's complete; another is that (in this case) exceptions are ignored.

这是正确的;当前的代码执行加载操作作为“发射并忘记”的方式。这种方式有几个问题:一个问题是很难/不可能检测它何时完成;另一个问题是(在这种情况下)异常被忽略。

> I would like to know what measures I can take to ensure that the user will not experience this type of data not loading.

我想知道我可以采取哪些措施来确保用户不会遇到数据加载不成功的情况。

The user will always experience the data loading in some way. I recommend that you design a "loading" state for your page.

用户总会以某种方式经历数据加载。我建议你为页面设计一个“加载中”状态。

I do not recommend blocking on the asynchronous work. This will block the UI thread, providing a very poor user experience. The user will just see a noninteractive blank/empty screen, and the OS will treat the app as not responsive. If this is a mobile app, that's even worse, since you've made the entire screen unresponsive just with your one app; some app stores will automatically test for this behavior and reject your app if it does this kind of thing.

不建议在异步工作上进行阻塞。这将阻塞UI线程,提供非常糟糕的用户体验。用户将只看到一个不可交互的空白屏幕,操作系统将视应用程序为不响应。如果这是一个移动应用程序,情况会更糟,因为你的应用程序会使整个屏幕都无响应;一些应用商店会自动测试这种行为,并拒绝你的应用程序。

Instead of blocking or fire-and-forget, you need to design UX to handle the asynchronous nature of your code. This usually means synchronously and immediately initializing to a "loading" state, and then later updating the UI with the results (or an error message).

而不是阻塞或使用“发射并忘记”,你需要设计用户体验以处理你的代码的异步性质。通常,这意味着同步和立即初始化到“加载中”状态,然后稍后更新UI以显示结果(或错误消息)。

I have an old article that goes into more detail and suggests a type that acts like a data-bindable Task&lt;T&gt;. This idea has been copied into several libraries:

我有一篇旧文章,其中有更多详细信息,并建议一种类似于可绑定数据的 Task&lt;T&gt; 的类型。这个想法已经被复制到几个库中:

这些库的大多数使用方式都相同,只是API上有些许不同。例如,使用 NotifyTask&lt;T&gt;

public BaseVehicleViewModel()
{
  Vehicles = NotifyTask.Create(async () =&gt; await GetAssetsAsync());
}

public NotifyTask&lt;ObservableCollection&lt;Asset&gt;&gt; Vehicles { get; }

protected async Task&lt;ObservableCollection&lt;Asset&gt;&gt; GetAssetsAsync()
{
  var vehicles = await App.AppServiceClient.AssetsAsync();
  if vehicles == null
    return new ObservableCollection&lt;Asset&gt;();
  return new ObservableCollection&lt;Asset&gt;(vehicles.Where(v =&gt; v.Active == 1));
}

现在,你的 Vehicles 是一个 NotifyTask&lt;ObservableCollection&lt;Asset&gt;&gt;,而不是 ObservableCollection&lt;Asset&gt;。你的数据绑定代码可以绑定到 Vehicles.Result 来绑定到 ObservableCollection&lt;Asset&gt;(在加载资产之前为 null)。它还可以绑定到 Vehicles.IsNotCompleted 来显示加载状态,或者绑定到 Vehicles.ErrorMessage 如果加载失败,等等。

英文:

> My assumption is that the task has not finished and it running async means the UI has continued without it.

That is correct; the code currently executes the loading as a fire-and-forget method. Fire-and-forget has several problems: one is that it's not easy/possible to detect when it's complete; another is that (in this case) exceptions are ignored.

> I would like to know what measures I can take to ensure that the user will not experience this type of data not loading.

The user will always experience the data loading in some way. I recommend that you design a "loading" state for your page.

I do not recommend blocking on the asynchronous work. This will block the UI thread, providing a very poor user experience. The user will just see a noninteractive blank/empty screen, and the OS will treat the app as not responsive. If this is a mobile app, that's even worse, since you've made the entire screen unresponsive just with your one app; some app stores will automatically test for this behavior and reject your app if it does this kind of thing.

Instead of blocking or fire-and-forget, you need to design UX to handle the asynchronous nature of your code. This usually means synchronously and immediately initializing to a "loading" state, and then later updating the UI with the results (or an error message).

I have an old article that goes into more detail and suggests a type that acts like a data-bindable Task&lt;T&gt;. This idea has been copied into several libraries:

The general usage of most of them are the same, with minor API differences. E.g., using NotifyTask&lt;T&gt;:

public BaseVehicleViewModel()
{
  Vehicles = NotifyTask.Create(async () =&gt; await GetAssetsAsync());
}

public NotifyTask&lt;ObservableCollection&lt;Asset&gt;&gt; Vehicles { get; }

protected async Task&lt;ObservableCollection&lt;Asset&gt;&gt; GetAssetsAsync()
{
  var vehicles = await App.AppServiceClient.AssetsAsync();
  if (vehicles == null)
    return new ObservableCollection&lt;Asset&gt;();
  return new ObservableCollection&lt;Asset&gt;(vehicles.Where(v =&gt; v.Active == 1));
}

Now your Vehicles is a NotifyTask&lt;ObservableCollection&lt;Asset&gt;&gt; instead of a ObservableCollection&lt;Asset&gt;. Your data binding code can bind to Vehicles.Result to bind to the ObservableCollection&lt;Asset&gt; (which is null until the assets are loaded). It can also bind to Vehicles.IsNotCompleted to show a spinner/skeleton instead of the full UI, Vehicles.ErrorMessage if the loading failed, etc.

答案2

得分: 0

首先,你可以尝试将异步方法同步运行。例如:

public BaseVehicleViewModel()
{
   var task = Task.Run(async () => await GetAssets()).ConfigureAwait(false);
   task.Wait();
   // GetAssets().GetAwaiter().GetResult();
   // Task.Run(() => GetAssets().Wait());
}

此外,你还可以使用 EventToCommand 将页面的 OnAppearing 事件转换为 ViewModel 中的命令。

你还可以在 ViewModel 中声明一个标志来检查数据是否已加载,例如:

internal class BaseVehicleViewModel : BaseViewModel
{
    public bool finished = false;
    public BaseVehicleViewModel()
    {
       Task.Run(async () => await GetAssets()).ContinueWith(t => { finished = true; });
       // 当任务完成时,finished 标志将设置为 true;
    }

    protected ObservableCollection<Asset> vehicles;
    public ObservableCollection<Asset> Vehicles
    {
        get => vehicles;
        set => SetProperty(ref vehicles, value);
    }

    protected async Task GetAssets()
    {
        var vehicles = await App.AppServiceClient.AssetsAsync();
        if (vehicles != null)
        {
            Vehicles = new ObservableCollection<Asset>(vehicles.Where(v => v.Active == 1));
        }
    }
}

最后,你可以参考这个关于在 Xamarin Android 应用程序中异步加载 ViewModel 数据的案例:when to load the ViewModel for asynchronous data in a Xamarin Android app.

英文:

First of all, you can try to make the async method run synchronously. Such as:

public BaseVehicleViewModel()
{
   var task = Task.Run(async () =&gt; await GetAssets()).ConfigureAwait(false);
   task.Wait();
   // GetAssets().GetAwaiter().GetResult();
   // Task.Run(() =&gt; GetAssets().Wait());
}

In addition, you can also use the EventToCommand to convert the Page's OnAppearing event to the command in the viewmodel.

And you can declare a flag in the ViewModel to check if the data has been loaded or not. Such as:

 internal class BaseVehicleViewModel : BaseViewModel
    {
        public bool finished = false;
        public BaseVehicleViewModel()
        {
           Task.Run(async () =&gt; await GetAssets()).ContinueWith(t=&gt; { finished=true;});
           // when the task compete the finished flag will be set as true;
        }
        protected ObservableCollection&lt;Asset&gt; vehicles;
        public ObservableCollection&lt;Asset&gt; Vehicles
        {
            get =&gt; vehicles;
            set =&gt; SetProperty(ref vehicles, value);
        }

        protected async Task GetAssets()
        {
            var vehicles = await App.AppServiceClient.AssetsAsync();
            if (vehicles != null)
            {
                Vehicles =
                    new ObservableCollection&lt;Asset&gt;(vehicles.Where(v =&gt; v.Active == 1));
            }
        }

Finally, you can refer to this case about when to load the ViewModel for asynchronous data in a Xamarin Android app.

huangapple
  • 本文由 发表于 2023年3月10日 01:26:11
  • 转载请务必保留本文链接:https://go.coder-hub.com/75688056.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定