Короче говоря, вопрос таков: как я могу использовать reactiveui для двусторонней привязки модели представления и представление и убедиться, что обновление ViewModel не приводит к сбою при запуске в потоке, отличном от ui-потока? Без необходимости вручную подписываться на изменения и явно обновлять uiThread, поскольку это противоречит цели reactiveui, а также усложняет инкапсуляцию всей логики в PCL.
Ниже я представил очень простой (Android) проект с использованием Xamaring и Reactiveui, позволяющий сделать следующее:
- Кнопка с текстом ' Привет, мир!
- При нажатии на нее к тексту кнопки добавляется буква «a».
- Я позволяю Activity реализовать IViewFor и использую производную ViewModel из ReactiveObject, содержащего текст, который я хочу изменить.
- Я привязываю кнопку Activity button.Text к ViewModel.Text, чтобы позволить reactiveui обрабатывать все изменения и обновления пользовательского интерфейса.
Наконец, я добавляю функцию при нажатии кнопки, чтобы добавить букву «a» в ViewModel.
button.Click += delegate
{
this.ViewModel.Text += "a"; // does not crash
Task.Run(() => { this.ViewModel.Text += "a"; }); // crash
};
Непосредственное добавление «a» не является проблемой. Однако добавление 'a' в другой поток приводит к известному исключению Java: Исключение: только исходный поток, создавший иерархию представлений, может касаться своих представлений.
Я понимаю исключение и откуда оно взялось. Фактически, если бы я добавил букву «а» в другой поток, у меня уже все работало бы, просто не привязывая текст. А лучше подписаться на изменения и использовать метод RunOnUiThread для внесения изменений в пользовательский интерфейс. Но этот сценарий противоречит цели использования ReactiveUi. Мне очень нравится чистый способ кодирования простого оператора «this.Bind(ViewModel, x => x.Text, x => x.button.Text);», но если это нужно запустить в uiThread я не понимаю, как заставить его работать.
И, естественно, это минимум, чтобы показать проблему. Настоящая проблема, почему я поднимаю этот вопрос, заключается в том, что я хочу использовать метод GetAndFetchLatest из akavache. Он асинхронно получает данные, кэширует их и выполняет функцию (обновляя ViewModel). Если данные уже находятся в кеше, он выполнит обновление ViewModel с кэшированным результатом И выполнит логику вычислений в другом потоке, а затем снова вызовет ту же функцию, как только она будет выполнена (что приведет к сбою, поскольку это происходит в другом потоке). поток обновляет ViewModel, что приводит к сбою).
Обратите внимание, что хотя явное использование RunOnUiThread работает, я действительно не хочу (даже не могу) вызовите это в ViewModel. Потому что у меня есть более сложный фрагмент кода, в котором кнопка просто сообщает ViewModel, что нужно получить данные и обновиться. Если бы мне пришлось сделать это в uiThread (т. е. после того, как я получил данные обратно, я обновил ViewModel), то я больше не могу привязать iOS к той же ViewModel.
И наконец, вот весь код, вызывающий сбой. Я видел, как часть Task.Run иногда работает, но если вы добавите еще несколько задач и продолжите обновлять в них ViewModel, в конечном итоге произойдет сбой в потоке пользовательского интерфейса.
public class MainActivity : Activity, IViewFor
{
public RandomViewModel ViewModel { get; set; }
private Button button;
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
SetContentView(Resource.Layout.Main);
this.button = FindViewById(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;
}
}
}
Подробнее здесь: https://stackoverflow.com/questions/329 ... te-crashes