3
\$\begingroup\$

I develop Windows Phone App with Prism framework (MVVM).

I use data caching. To get data I use proxy service. Proxy service creates two tasks:

  1. the first task is receiving data from a web service and update data in local database (SqLite).
  2. the second task is receiving data from the local database.

The ViewModel launches tasks and displays data on UI.

It works, but I think it can be done much better. Any ideas?

ViewModel

public class UserProfilePageViewModel : ViewModel, IUserProfilePageViewModel
{
 private readonly IUserServiceProxy _userServiceProxy;
 private IUserItemViewModel _userProfile;
 public IUserItemViewModel UserProfile
 {
 get { return _userProfile; }
 set { SetProperty(ref _userProfile, value); }
 }
 public UserProfilePageViewModel(IUserServiceProxy userServiceProxy)
 {
 if (userServiceProxy == null) { throw new ArgumentNullException("The userServiceProxy can't be null"); } 
 _userServiceProxy = userServiceProxy; 
 }
 public async override void OnNavigatedTo(object navigationParameter, Windows.UI.Xaml.Navigation.NavigationMode navigationMode, Dictionary<string, object> viewModelState)
 { 
 _userId = navigationParameter as string;
 await Load(navigationParameter as string);
 } 
 public async Task Load(string id)
 { 
 var taskList = _userServiceProxy.GetUserTaskList(id);
 while (taskList.Count > 0)
 { 
 var t = await Task.WhenAny(taskList);
 taskList.Remove(t);
 try
 {
 var result = await t;
 //update UI 
 UserProfile.Model = result.Result;
 }
 catch (OperationCanceledException) { }
 catch (Exception) { }
 }
 await Task<User>.WhenAll(taskList);
 }
}

ServiceProxy

public class UserServiceProxy : IUserServiceProxy
{
 private readonly IUserApi _userApi;
 private readonly IUserRepository _userRepository;
 private readonly IUserSettings _userSettings;
 private readonly IAuthService _authService;
 public UserServiceProxy(IUserApi userApi, IUserRepository userRepository, IUserSettings userSettings, IAuthService authService)
 {
 if (userApi == null) { throw new ArgumentNullException("The userApi can't be null"); }
 if (userRepository == null) { throw new ArgumentNullException("The userRepository can't be null"); }
 if (userSettings == null) { throw new ArgumentNullException("The settings can't be null"); }
 if (authService == null) { throw new ArgumentNullException("The authService can't be null"); }
 _userApi = userApi;
 _userRepository = userRepository;
 _userSettings = userSettings;
 _authService = authService;
 }
 public List<Task<ResponseWrapper<User>>> GetUserTaskList(string id, CancellationToken cancellationTokenSource = new CancellationToken())
 {
 Func<Task<ResponseWrapper<User>>> getFromWeb = async () =>
 {
 var poco = await _userApi.GetAsync(id, cancellationTokenSource);
 await _userRepository.MergeUser(LocalSQLite.Models.User.FromPoco(poco.Result));
 return poco;
 };
 Task<Task<ResponseWrapper<User>>> outerTaskWeb = Task.Factory.StartNew(getFromWeb);
 Task<ResponseWrapper<User>> innerTaskWeb = outerTaskWeb.Unwrap();
 Func<Task<ResponseWrapper<User>>> getFromLocal = async () =>
 {
 var localUser = await _userRepository.LoadByUserIdAsync(id);
 if (localUser == null) localUser = new LocalSQLite.Models.User();
 return Models.WebApi.ResponseWrapper<Models.User.User>.Wrap(localUser.ToPoco());
 };
 Task<Task<ResponseWrapper<User>>> outerTaskLocal = Task.Factory.StartNew(getFromLocal);
 Task<ResponseWrapper<User>> innerTaskLocal = outerTaskLocal.Unwrap();
 List<Task<ResponseWrapper<User>>> tasks = new List<Task<ResponseWrapper<User>>>()
 {
 innerTaskWeb,
 innerTaskLocal
 };
 return tasks;
 }
}
asked Jan 31, 2015 at 18:12
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

Let's start with the ServiceProxy.

  1. You're misusing ArgumentNullException, if you pass only one parameter it should contain the name of the parameter which value is null.
  2. You don't need Task.Factory.StartNew at all, as you just kill all the benefits of task-oriented approach with it. All you need to do is just capture the task, without awaiting on it. As a result you don't need all those .Unwrap() calls as well.
  3. Since you're just outputting the collection of different tasks that eventually return the ResponseWrapper<User> object - I would prefer to return the IEnumerable<T> instead of List<T>.
  4. And finally - split different approaches of getting the result record into different methods (if not classes) so that the code remains clean. If you use an IoC container you may even want to split the implementation into different classes implementing the same interface, but that would be too much for this example.

As a result we should get something like this:

UserServiceProxy

public class UserServiceProxy : IUserServiceProxy
{
 private readonly IUserApi _userApi;
 private readonly IUserRepository _userRepository;
 private readonly IUserSettings _userSettings;
 private readonly IAuthService _authService;
 public UserServiceProxy(IUserApi userApi, IUserRepository userRepository, IUserSettings userSettings, IAuthService authService)
 {
 if (userApi == null) { throw new ArgumentNullException("userApi", "The userApi can't be null"); }
 if (userRepository == null) { throw new ArgumentNullException("userRepository", "The userRepository can't be null"); }
 if (userSettings == null) { throw new ArgumentNullException("userSettings", "The settings can't be null"); }
 if (authService == null) { throw new ArgumentNullException("authService", "The authService can't be null"); }
 _userApi = userApi;
 _userRepository = userRepository;
 _userSettings = userSettings;
 _authService = authService;
 }
 public IEnumerable<Task<ResponseWrapper<User>>> GetUserTasks(string id, CancellationToken cancellationToken = new CancellationToken())
 {
 yield return GetUserFromWebAsync(id, cancellationToken);
 yield return GetUserFromLocalAsync(id);
 }
 private async Task<ResponseWrapper<User>> GetUserFromLocalAsync(string id)
 {
 var localUser = await _userRepository.LoadByUserIdAsync(id) ?? new LocalSQLite.Models.User();
 return Models.WebApi.ResponseWrapper<Models.User.User>.Wrap(localUser.ToPoco());
 }
 private async Task<ResponseWrapper<User>> GetUserFromWebAsync(string id, CancellationToken cancellationToken)
 {
 var poco = await _userApi.GetAsync(id, cancellationToken);
 await _userRepository.MergeUser(LocalSQLite.Models.User.FromPoco(poco.Result));
 return poco;
 }
}

Now let's look at the Load method. The main issue there is the processing of each task as it finishes. It can easily be done like this (note that both methods are no longer async, as they do not await on tasks):

 private void UpdateUi(ResponseWrapper<User> user)
 {
 //update UI 
 UserProfile.Model = result.Result;
 }
 public Task Load(string id)
 {
 var uiUpdateTasks = _userServiceProxy.GetUserTasks(id)
 .Select(async userTask => UpdateUi(await userTask));
 return Task.WhenAll(uiUpdateTasks);
 }
answered Feb 4, 2015 at 17:02
\$\endgroup\$
3
  • \$\begingroup\$ Great! Thank you very much! I have little experience in Task programming, your code is very useful for me. \$\endgroup\$ Commented Feb 4, 2015 at 19:17
  • \$\begingroup\$ If I need to perform some action after all tasks (eg to set a flag allDataLoaded), I should to use await keyword before Task.WhenAll? \$\endgroup\$ Commented Feb 5, 2015 at 8:43
  • \$\begingroup\$ yes (and mark this method as async) \$\endgroup\$ Commented Feb 5, 2015 at 11:16

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.