使用async viewmodelupdate的react -ui绑定会崩溃



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

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

下面我提供了一个非常简单的(Android)项目,使用Xamaring和Reactiveui,做以下事情:

  • 带有文字"Hello World"的按钮
  • 点击它会在按钮的文本后面附加"a"。
  • 我让活动实现IViewFor,我使用从ReactiveObject派生的ViewModel,包含我想要更改的文本。
  • 我绑定了Activity的按钮。文本到ViewModel。文本,让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',我已经让它工作了,只是不绑定文本。而是通过订阅更改,并使用runonuthread方法对ui进行更改。但是这种情况有点违背了使用ReactiveUi的目的。我真的很喜欢简单语句"this"的简洁编码方式。Bind(ViewModel, x => x. text, x => x.button. text);',但是如果这个在uiThread上运行,我看不出如何使它工作。

很自然,这是显示问题的最小值。至于我为什么提出这个问题的实际问题是因为我想使用'GetAndFetchLatest'-方法从akavache。它异步获取数据并缓存数据,并执行一个函数(正在更新ViewModel)。如果数据已经在缓存中,它将使用缓存的结果执行ViewModel-update,并在另一个线程中执行计算逻辑,然后在完成后再次调用相同的函数(导致崩溃,因为这是在另一个线程上,更新ViewModel,从而导致崩溃)。

注意,即使显式地使用runonuthread工作,我真的不想(甚至不能)在ViewModel中调用它。因为我有一段更复杂的代码,其中一个按钮只是告诉ViewModel去获取数据并更新自己。如果我被要求在uiThread上这样做(即在我得到数据后,我更新ViewModel),那么我就不能再将iOS绑定到相同的ViewModel了。

最后,这是使它崩溃的整个代码。我看过任务。运行部分有时可以工作,但如果你添加更多的任务并不断更新其中的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;
        }
    }
}

这已经在这里和那里讨论过了,简短的回答是"按照设计,出于性能原因"。

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

当一个对象绑定到一个视图时,你通常期望视图到达你的对象属性的峰值(读取),它是在UI线程中完成的。

一旦你承认了这一点,唯一相同的(线程安全的,保证工作的)修改这个对象的方法(它正在从UI线程被峰值化)就是从UI线程也这样做。

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

例如,如果你使用INPC, 你的属性值是不可变的(例如string), 你的视图在收到通知之前不会因为观察到值的变化而感到糟糕(简单的控件可能可以接受,具有过滤/排序功能的网格可能不可以,除非它们完全深度复制它们的源)。

你应该在设计ViewModel时考虑到它存在于UI上下文中。

对于Rx,这意味着在ViewModel修改代码之前有.OnserverOn(/* ui sheduler */)

最新更新