Most of the Rx Compelling ExamplesTM load and store data locally, which I don't find that Compelling.
I spent some time coming up with how to best perform the following actions but want to make sure I am not Doing It WrongTM.
Here are the cases I am trying to solve:
- Load data from cache and remote server when view model is instantiated
- Save data to server when UI interaction happens (some button press)
- Handle error cases (network down)
Some items I am trying to avoid:
- Errors being swallowed (async in ctor for example)
- Incorrect use of Rx patterns (is there a more 'correct' way)
- Loss of data either through out of sync lists, not using cache correct, other.
public class CommonViewModel : ReactiveObject, IRoutableViewModel
{
private readonly RemoteHttpService _remoteClient;
private ReactiveList<Item> _items;
public ReactiveList<Item> ItemsFromServer
{
get { return _items; }
set { this.RaiseAndSetIfChanged(ref _items, value); }
}
public ReactiveCommand<List<Item>> LoadItems{ get; protected set; }
public ReactiveCommand<object> SendItemsToServerCommand { get; protected set; }
public string UrlPathSegment
{
get { return "CommonViewModel View"; }
}
public IScreen HostScreen { get; private set; }
public CommonViewModel(IScreen screen)
{
HostScreen = screen;
ItemsFromServer = new ReactiveList<Item>();
_remoteClient = new RemoteHttpService(); //using refit and polly behind the scenes.
SendItemsToServerCommand = ReactiveCommand.CreateAsyncTask((_, ctx) => InternalSendItemsToServer());
SendItemsToServerCommand.ThrownExceptions.Subscribe(ex => UserError.Throw("Could not send data to server", ex));
LoadItems = ReactiveCommand.CreateAsyncObservable(_ => this.InternalGetItemsFromCacheAndServer());
LoadItems.Subscribe(list =>
{
_items.Clear();
foreach (var item in list){
//using AddRange throw Presentation Exception
_items.Add(item);
}
});
LoadItems.ThrownExceptions.Subscribe(ex => UserError.Throw("Could not get data from server"));
LoadItems.ExecuteAsyncTask();
}
private IObservable<List<Item>> InternalGetItemsFromCacheAndServer()
{
return BlobCache.LocalMachine.GetAndFetchLatest("items",
InternalGetRemoteItems,
dt => true,
RxApp.MainThreadScheduler.Now + TimeSpan.FromMinutes(5));
}
private async Task<List<Item>> InternalGetRemoteItems()
{
var conferences = await _remoteClient.GetItems().ConfigureAwait(false);
return conferences;
}
private async Task<object> InternalSendItemsToServer()
{
var itemsToSendToServer = ItemsFromServer.SomeLinq().Maybe();
return await _remoteClient.SendDataToServer(itemsToSendToServer);
}
}
-
\$\begingroup\$ I added the language tag to your question. Please correct it if I got it wrong. \$\endgroup\$RubberDuck– RubberDuck2014年12月23日 16:37:21 +00:00Commented Dec 23, 2014 at 16:37
1 Answer 1
Sorry to disappoint, but you did everything right! This is a quite good example of using RxUI, Akavache, and Refit together.
The only thing I would change, is to not immediately call LoadItems.ExecuteAsyncTask
in the ViewModel constructor. Invoking this in the VM constructor means that your VM class becomes more difficult to test, because you always have to mock out the effects of calling LoadItems
, even if the thing you are testing is unrelated.
Instead, I always call these commands in the View constructor, something like:
this.WhenAnyValue(x => x.ViewModel.LoadItems)
.SelectMany(x => x.ExecuteAsync())
.Subscribe();
This means that any time we get a new ViewModel, we execute LoadItems
, which is what we want when the app is running, but not what we want in every unit test.
-
1\$\begingroup\$ Is there any pros/cons to putting the 'WhenAnyValue' code in the view? \$\endgroup\$detroitpro– detroitpro2014年12月29日 19:07:48 +00:00Commented Dec 29, 2014 at 19:07
-
1\$\begingroup\$ Yes, on WPF you need to use the WhenActivated method to avoid leaking memory \$\endgroup\$Ana Betts– Ana Betts2014年12月29日 19:23:39 +00:00Commented Dec 29, 2014 at 19:23
-
\$\begingroup\$ I would really try to return IObservable<Item> from your load items method in order to use subscription to add one item to the _items list as they arrive (or use Buffer to get chunks). Your current implementation will not block the UI but will wait until you retrieve all items from the server before it shows something. \$\endgroup\$Alexey Zimarev– Alexey Zimarev2015年03月16日 20:48:43 +00:00Commented Mar 16, 2015 at 20:48
-
1\$\begingroup\$ I disagree with Alexey - unless your API itself is streaming items on an ongoing basis, your UI will not be any more usable if dozens of items per second are flying into a list. Showing a great "in progress" visual as well as a cancel button is the way to go here. \$\endgroup\$Ana Betts– Ana Betts2015年03月17日 01:33:24 +00:00Commented Mar 17, 2015 at 1:33
Explore related questions
See similar questions with these tags.