2015-10-03 2 views
1

У меня есть вопрос о 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; 
     } 
    } 
} 

ответ

0

Это уже обсуждалось here и there, и короткий ответ «как задумано, по соображениям производительности».

Я лично не очень убежден позже (производительность, как правило, плохой водитель при разработке API), но я попытаюсь объяснить, почему я думаю, что этот проект является правильным в любом случае:

При связывании объект для представления, вы обычно ожидаете, что представление появится и достигнет пика (прочитанного) в ваших свойствах объекта, и это происходит из потока пользовательского интерфейса.

После того, как вы признаете, что единственный способ использования этого объекта (как в потокобезопасном и гарантированном для работы) изменить этот объект (который впишется в поток пользовательского интерфейса) - это сделать также из потока пользовательского интерфейса.

Модификации из других потоков могут работать, но только в определенных условиях, которые обычно не заботятся о разработчиках (вплоть до получения артефактов UI, и в этом случае они ... выполняют обновление ...).

Например, если вы используете INPC, и ваши значения свойств являются неизменяемыми (например, строка), и ваш взгляд не будет чувствовать себя плохо о наблюдении изменения значения, прежде чем она получает уведомление об этом (простой управления, вероятно, в порядке с ним, сетки с возможностями фильтрации/сортировки, вероятно, не в порядке, если только они полностью не копируют исходный код).

Вы должны создать свой ViewModel с тем фактом, что он живет в контексте пользовательского интерфейса.

С Rx это означает, что .OnserverOn(/* ui sheduler */) прямо перед модификацией модели ViewModel.