Reactive-ui 与异步 viewmodelupdate 绑定崩溃

问题描述 投票:0回答:1

我有一个关于 Reactive ui、它的绑定以及它如何处理 ui 更新的问题。我一直认为使用 ReactiveUi 会处理 ui 线程上的所有 ui 更新。但我最近发现情况并非总是如此。

简而言之,问题是:如何使用reactiveui来双向模型绑定视图模型和视图,并确保在与ui线程不同的线程上运行时更新ViewModel不会崩溃?无需手动订阅更改并在 uiThread 上显式更新,因为这违背了 Reactui 的目的,并且使得将所有逻辑封装在 PCL 中变得更加困难。

下面我提供了一个使用 Xamaring 和 Reactiveui 的非常简单的(Android)项目,用于执行以下操作:

  • 带有文字“Hello World”的按钮
  • 单击它会将“a”附加到按钮的文本中。
  • 我让 Activity 实现 IViewFor,并使用从 ReactiveObject 派生的 ViewModel,其中包含我想要更改的文本。
  • 我将Activity的button.Text绑定到ViewModel.Text,让reactiveui处理所有更改和ui更新。
  • 最后,我向按钮的 onclick 添加一个函数,以将“a”附加到 ViewModel。

我遇到的问题如下:

button.Click += delegate
    {
        this.ViewModel.Text += "a";                         // does not crash
        Task.Run(() => { this.ViewModel.Text += "a"; });    // crash
    };

直接附加“a”不是问题。但是,在不同的线程上添加“a”会导致众所周知的 Java 异常:异常:只有创建视图层次结构的原始线程才能触及其视图。

我了解异常及其来源。事实上,如果我要在不同的线程上附加“a”,我已经可以简单地不绑定文本了。而是通过订阅更改并使用 RunOnUiThread 方法对 ui 进行更改。但这种情况有点违背了使用 ReactiveUi 的目的。我真的很喜欢简单语句“this.Bind(ViewModel, x => x.Text, x => x.button.Text);”的干净编码方式,但是如果这个has在uiThread上运行,我不知道如何让它发挥作用。

当然,这是显示问题的最低限度。我提出这个问题的实际问题是因为我想使用 akavache 的“GetAndFetchLatest”方法。它异步获取数据并缓存它,并执行一个函数(正在更新 ViewModel)。如果数据已经在缓存中,它将使用缓存的结果执行 ViewModel 更新,并在不同的线程中执行计算逻辑,然后在完成后再次调用相同的函数(导致崩溃,因为这是在不同的线程上)线程,更新 ViewModel,这会导致崩溃)。

请注意,即使显式使用 RunOnUiThread 有效,我真的不想(甚至不能)在 ViewModel 中调用它。因为我有一段更复杂的代码,其中一个按钮只是告诉 ViewModel 去获取数据并更新自身。如果我被要求在 uiThread 上执行此操作(即在我获取数据后,我更新 ViewModel),那么我无法再将 iOS 绑定到同一个 ViewModel。

最后,这是使其崩溃的完整代码。我见过 Task.Run 部分有时可以工作,但如果您添加更多任务并不断更新其中的 ViewModel,它最终肯定会在 UI 线程上崩溃。

public class MainActivity : Activity, IViewFor<MainActivity.RandomViewModel>
{
    public RandomViewModel ViewModel { get; set; }
    private Button button;

    protected override void OnCreate(Bundle bundle)
    {
        base.OnCreate(bundle);

        SetContentView(Resource.Layout.Main);

        this.button = FindViewById<Button>(Resource.Id.MyButton);

        this.ViewModel = new RandomViewModel { Text = "hello world" };
        this.Bind(ViewModel, x => x.Text, x => x.button.Text);

        button.Click += delegate
            {
                this.ViewModel.Text += "a";                         // does not crash
                Task.Run(() => { this.ViewModel.Text += "a"; });    // crash
            };
    }

    public class RandomViewModel : ReactiveObject
    {
        private string text;

        public string Text
        {
            get
            {
                return text;
            }
            set
            {
                this.RaiseAndSetIfChanged(ref text, value);
            }
        }
    }

    object IViewFor.ViewModel
    {
        get
        {
            return ViewModel;
        }
        set
        {
            ViewModel = value as RandomViewModel;
        }
    }
}
c# android xamarin reactiveui
1个回答
1
投票

这已经在herethere进行了讨论,简短的答案是“出于性能原因而设计”。

我个人不太相信后者(在设计 API 时,性能通常是一个糟糕的驱动因素),但我会尝试解释为什么我认为这个设计是正确的:

将对象绑定到视图时,您通常期望视图出现并在对象属性处达到峰值(读取),并且它是从 UI 线程执行此操作的。

一旦您承认这一点,修改此对象(从 UI 线程达到峰值)的唯一明智的(如线程安全且保证工作)方法就是从 UI 线程执行此操作。

来自其他线程的修改可能会起作用,但仅在特定条件下有效,开发人员通常不关心(直到他们获得 UI 工件,在这种情况下他们......执行刷新......)。

例如,如果您使用INPCand您的属性值是不可变的(例如字符串),and您的视图在收到通知之前观察到值更改不会感到难过(简单的控件)可能可以,具有过滤/排序功能的网格可能不行,除非它们完全深度复制其源)。

您在设计 ViewModel 时应该考虑到它存在于 UI 上下文中这一事实。

对于 Rx,这意味着在 ViewModel 修改代码之前有

.ObserveOn(RxApp.MainThreadScheduler)

© www.soinside.com 2019 - 2024. All rights reserved.