Reputation: 21
I have a question about Reactive ui, its bindings, and how it handles ui updates. I always assumed that using ReactiveUi would take care of all ui updates on the ui thread. But I recently found out this isn't always the case.
In short the question is: How can I use reactiveui to two-way-model-bind a viewmodel and a view, and assure that updating the ViewModel doesn't crash when run on a different thread than the ui-thread? Without having to manually subscribe to changes and update explicitely on the uiThread, as that defeats the purpose of reactiveui, as well as making it harder to encapsulate all logic in the PCL.
Below I've provided a very simple (Android) project using Xamaring and Reactiveui, to do the following:
The issue I have is the following:
button.Click += delegate
{
this.ViewModel.Text += "a"; // does not crash
Task.Run(() => { this.ViewModel.Text += "a"; }); // crash
};
Directly appending 'a' is not an issue. However, adding 'a' on a different thread results in the well-known Java exception: Exception: Only the original thread that created a view hierarchy can touch its views.
I understand the exception and where it's coming from. In fact, if I were to append the 'a' on a different thread, I already had it working with simply not binding the text. But rather by subscribing to changes, and using the RunOnUiThread-method to make changes to the ui. But this scenario kind of defeats the purpose of using ReactiveUi. I really like the clean coding way of the simple statement 'this.Bind(ViewModel, x => x.Text, x => x.button.Text);', but if this has to run on the uiThread, I can't see how to make it work.
And naturally this is the bare mininum to show the problem. The actual problem as to why I bring this up is because I want to use the 'GetAndFetchLatest'-method from akavache. It gets data asynchroniously and caches it, and executes a function (being updating the ViewModel). If the data is already in the cache, it will execute the ViewModel-update with the cached result AND do the computationlogic in a different thread, and then call the same function again once it's done (resulting in the crash, because that's on a different thread, updates the ViewModel, which results in the crash).
Note that even though explicitely using RunOnUiThread works, I really don't want (can't even) to call this within the ViewModel. Because I have a more complex piece of code in which a button simply tells the ViewModel to go fetch data and update itself. If I were required to do this on the uiThread (i.e. after I got data back, I update the ViewModel), then I can't bind iOS to the same ViewModel anymore.
And lastly, here's the entire code to make it crash. I've seen the Task.Run-part sometimes work, but if you add some more tasks and keep updating the ViewModel in them, it's bound to crash eventually on the UI-thread.
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;
}
}
}
Upvotes: 2
Views: 1331
Reputation: 2962
This has been already discussed here and there, and the short answer is "as designed, for performance reasons".
I'm personally not really convinced by the later (performance is usually a bad driver when designing an API), but I'll try to explain why I think this design is correct anyway:
When binding an object to a view, you usually expect the view to come and peak (read) at your object properties, and it's doing so from the UI thread.
Once you acknowledge that, the only sane (as in thread-safe and guaranteed to work) way to modify this object (which is being peaked into from the UI thread) is to do so also from the UI thread.
Modifications from other threads may work, but only within specific conditions, that devs usually don't care about (up until they get UI artifacts, in which case they ... perform a refresh...).
For instance if you're using INPC, and your property values are immutable (e.g. string), and your view won't feel bad about observing a value change before it receives the notification of it (simple controls probably are ok with it, grids with filtering/sorting capabilities are probably not ok, unless they completely deep-copy their source).
You should design your ViewModel with the fact that it lives in the UI context in mind.
With Rx, that means having .ObserveOn(RxApp.MainThreadScheduler)
right before ViewModel modification code.
Upvotes: 1