我有一个关于 Reactive ui、它的绑定以及它如何处理 ui 更新的问题。我一直认为使用 ReactiveUi 会处理 ui 线程上的所有 ui 更新。但我最近发现情况并非总是如此。
简而言之,问题是:如何使用reactiveui来双向模型绑定视图模型和视图,并确保在与ui线程不同的线程上运行时更新ViewModel不会崩溃?无需手动订阅更改并在 uiThread 上显式更新,因为这违背了 Reactui 的目的,并且使得将所有逻辑封装在 PCL 中变得更加困难。
下面我提供了一个使用 Xamaring 和 Reactiveui 的非常简单的(Android)项目,用于执行以下操作:
我遇到的问题如下:
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;
}
}
}
这已经在here和there进行了讨论,简短的答案是“出于性能原因而设计”。
我个人不太相信后者(在设计 API 时,性能通常是一个糟糕的驱动因素),但我会尝试解释为什么我认为这个设计是正确的:
将对象绑定到视图时,您通常期望视图出现并在对象属性处达到峰值(读取),并且它是从 UI 线程执行此操作的。
一旦您承认这一点,修改此对象(从 UI 线程达到峰值)的唯一明智的(如线程安全且保证工作)方法就是从 UI 线程执行此操作。
来自其他线程的修改可能会起作用,但仅在特定条件下有效,开发人员通常不关心(直到他们获得 UI 工件,在这种情况下他们......执行刷新......)。
例如,如果您使用INPC,and您的属性值是不可变的(例如字符串),and您的视图在收到通知之前观察到值更改不会感到难过(简单的控件)可能可以,具有过滤/排序功能的网格可能不行,除非它们完全深度复制其源)。
您在设计 ViewModel 时应该考虑到它存在于 UI 上下文中这一事实。
对于 Rx,这意味着在 ViewModel 修改代码之前有
.ObserveOn(RxApp.MainThreadScheduler)
。