У меня есть вопрос о Reactive ui, его привязках и о том, как он обрабатывает обновления ui. Я всегда предполагал, что использование ReactiveUi позаботится обо всех обновлениях ui в потоке ui. Но я недавно узнал, что это не всегда так.Reactive-ui binding with async viewmodelupdate crashes
Вкратце вопрос: как я могу использовать реактивную модель с привязкой к двухсторонней модели viewmodel и представлению и гарантировать, что обновление ViewModel не сбой при работе в другом потоке, чем в ui-thread ? Без необходимости вручную подписываться на изменения и явно обновлять на uiThread, поскольку это побеждает цель реактивной игры, а также затрудняет инкапсуляцию всей логики в PCL.
Ниже я обеспечил очень простой проект (Android) с помощью Xamaring и Reactiveui, чтобы сделать следующее:
- кнопку с текстом «Hello World»
- Кликнув по ней присоединяет «а» к тексту кнопки.
- Я разрешаю Activity реализовать IViewFor, и я использую ViewModel, полученный из ReactiveObject, содержащий текст, который я хочу изменить.
- Я привязываю кнопку Activity.Text к ViewModel.Text, чтобы позволить реактивной игре со всеми изменениями и обновлениями ui.
- Наконец, я добавляю функцию к кнопке onclick для добавления 'a' в ViewModel.
Проблема у меня следующая:
button.Click += delegate
{
this.ViewModel.Text += "a"; // does not crash
Task.Run(() => { this.ViewModel.Text += "a"; }); // crash
};
Непосредственно добавлением 'а' не является проблемой. Однако добавление «a» в другой поток приводит к хорошо известному исключению Java: Исключение: только исходный поток, создавший иерархию представлений, может коснуться его представлений.
Я понимаю исключение и откуда оно исходит. На самом деле, если бы я добавил «a» в другой поток, я уже работал, просто не привязывая текст. Но вместо того, чтобы подписаться на изменения и использовать RunOnUiThread-метод для внесения изменений в ui. Но этот сценарий отчасти поражает цель использования ReactiveUi. Мне очень нравится чистый способ кодирования простого выражения this.indew (ViewModel, x => x.Text, x => x.button.Text); ', но если этот имеет для запуска на uiThread, я не вижу, как заставить его работать.
И, естественно, это голый минимум, чтобы показать проблему. Фактическая проблема в том, почему я это объясняю, заключается в том, что я хочу использовать метод GetAndFetchLatest из akavache. Он получает данные асинхронно и кэширует его и выполняет функцию (обновляя ViewModel). Если данные уже находятся в кеше, он выполнит обновление ViewModel с кешированным результатом И сделает computationlogic в другом потоке, а затем снова вызовет ту же функцию после ее завершения (что приведет к сбою, потому что это на другой поток, обновляет ViewModel, что приводит к сбою).
Обратите внимание, что хотя явное использование RunOnUiThread работает, я действительно не хочу (даже не могу) назвать это в ViewModel. Потому что у меня есть более сложный фрагмент кода, в котором кнопка просто сообщает ViewModel, чтобы он извлекал данные и сам обновлял их. Если бы мне потребовалось сделать это на uiThread (т. Е. После того, как я получил данные назад, я обновляю ViewModel), то я больше не могу привязывать iOS к той же ViewModel.
И, наконец, вот весь код, чтобы он разбился. Я видел, как Task.Run-part иногда работает, но если вы добавите еще несколько задач и продолжаете обновлять 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;
}
}
}