Now that we have our model layer implemented, we can move on to write the ViewModel layer. The ViewModel will be responsible for presenting each operation to the UI and offering properties to be filled out by the View layer. Other common responsibilities of this layer are input validation and simple logic to display busy indicators.
At this point, it would be a good idea to include the ServiceContainer
class from the previous chapter in our XamChat.Core
project, as we will be using it through our ViewModels to interact with the Model layer. We will be using it as a simple option to support dependency injection and Inversion of Control; however, you may use another library of your preference for this.
Normally, we start off by writing a base class for all the ViewModel layers within our project. This class always has the functionality shared by all the classes. It's a good place to put some parts of the code that are used by all the methods in the classes; for example, notification changes, methods, or similar instances.
Place the following code snippet in a new ViewModels
folder within your project:
public class BaseViewModel { protected readonly IWebService service = ServiceContainer.Resolve<IWebService>(); protected readonly ISettings settings = ServiceContainer.Resolve<ISettings>(); public event EventHandler IsBusyChanged = delegate { }; private bool isBusy = false; public bool IsBusy { get { return isBusy; } set { isBusy = value; IsBusyChanged(this, EventArgs.Empty); } } }
The BaseViewModel
class is a great place to insert any common functionality that you plan on reusing throughout your application. For this app, we only need to implement some functionality to indicate if the ViewModel layer is busy. We provided a property and an event that the UI will be able to subscribe to and display a wait indicator on the screen. We also added some fields for the services that will be needed. Another common feature that could be added would be validation for user inputs; however, we don't really need it for this application.
Now that we have a base class for all of the ViewModel layers, we can implement ViewModel for the first screen in our application, the Login screen.
Now let's implement a LoginViewModel
class as follows:
public class LoginViewModel : BaseViewModel { public string Username { get; set; } public string Password { get; set; } public async Task Login() { if (string.IsNullOrEmpty(Username)) throw new Exception("Username is blank."); if (string.IsNullOrEmpty(Password)) throw new Exception("Password is blank."); IsBusy = true; try { settings.User = await service.Login(Username, Password); settings.Save(); } finally { IsBusy = false; } } }
In this class, we implemented the following:
BaseViewModel
to get access to IsBusy
and the fields containing common servicesUsername
and Password
properties to be set by the View layerUser
property to be set when the log in process is completedLogin
method to be called from View, with validation on Username
and Password
propertiesIsBusy
during the call to the Login
method on IWebService
User
property by awaiting the result from Login
on the web serviceBasically, this is the pattern that we'll follow for the rest of the ViewModels in the application. We provide properties for the View layer to be set by the user's input, and methods to call for various operations. If it is a method that could take some time, such as a web request, you should always return Task
and use the async
and await
keywords.
Since we have finished writing our ViewModel
class to log in, we will now need to create one for the user's registration.
Let's implement another ViewModel to register a new user:
public class RegisterViewModel : BaseViewModel { public string Username { get; set; } public string Password { get; set; } public string ConfirmPassword { get; set; } }
These properties will handle inputs from the user. Next, we need to add a Register
method as follows:
public async Task Register() { if (string.IsNullOrEmpty(Username)) throw new Exception("Username is blank."); if (string.IsNullOrEmpty(Password)) throw new Exception("Password is blank."); if (Password != ConfirmPassword) throw new Exception("Passwords don't match."); IsBusy = true; try { settings.User = await service.Register(new User { Username = Username, Password = Password, }); settings.Save(); } finally { IsBusy = false; } }
The RegisterViewModel
class is very similar to the LoginViewModel
class, but has an additional ConfirmPassword
property for the UI to set. A good rule to follow for when to split up the ViewModel layer's functionality is to always create a new class when the UI has a new screen. This helps to keep your code clean and somewhat follow the single responsibility principle for your classes. This concept states that a class should only have a single purpose or responsibility. We'll try to follow this concept to keep our classes small and organized, which can be more important than usual when sharing code across platforms.
Next on the list is a ViewModel layer to work with a user's friend list. We will need a method to load a user's friend list and add a new friend.
Now let's implement the FriendViewModel
as follows:
public class FriendViewModel : BaseViewModel { public User[] Friends { get; private set; } public string Username { get; set; } }
Now we'll need a method to load friends. This method is as follows:
public async Task GetFriends() { if (settings.User == null) throw new Exception("Not logged in."); IsBusy = true; try { Friends = await service.GetFriends(settings.User.Id); } finally { IsBusy = false; } }
Finally, we'll need a method to add a new friend, and then update the list of friends contained locally:
public async Task AddFriend() { if (settings.User == null) throw new Exception("Not logged in."); if (string.IsNullOrEmpty(Username)) throw new Exception("Username is blank."); IsBusy = true; try { var friend = await service.AddFriend(settings.user.Id, Username); //Update our local list of friends var friends = new List<User>(); if (Friends != null) friends.AddRange(Friends); friends.Add(friend); Friends = friends.OrderBy(f => f.Username).ToArray(); } finally { IsBusy = false; } }
Again, this class is fairly straightforward. The only thing new here is that we added some logic to update the list of friends and sort them within our client application and not the server. You could also choose to reload the complete list of friends if you have a good reason to do so.
Our final required ViewModel layer will be handling messages and conversations. We need to create a way to load conversations and messages, and send a new message.
Let's start implementing our MessageViewModel
class as follows:
public class MessageViewModel : BaseViewModel { public Conversation[] Conversations { get; private set; } public Conversation Conversation { get; set; } public Message[] Messages { get; private set; } public string Text { get; set; } }
Next, let's implement a method to retrieve a list of conversations as follows:
public async Task GetConversations() { if (settings.User == null) throw new Exception("Not logged in."); IsBusy = true; try { Conversations = await service.GetConversations(settings.User.Id); } finally { IsBusy = false; } }
Similarly, we need to retrieve a list of messages within a conversation. We will need to pass the conversation ID to the service as follows:
public async Task GetMessages() { if (Conversation == null) throw new Exception("No conversation."); IsBusy = true; try { Messages = await service.GetMessages(Conversation.Id); } finally { IsBusy = false; } }
Finally, we need to write some code to send a message and update the local list of messages as follows:
public async Task SendMessage() { if (settings.User == null) throw new Exception("Not logged in."); if (Conversation == null) throw new Exception("No conversation."); if (string.IsNullOrEmpty (Text)) throw new Exception("Message is blank."); IsBusy = true; try { var message = await service.SendMessage( new Message{ UserId = settings.User.Id, ConversationId = Conversation.Id, Text = Text, }); //Update our local list of messages var messages = new List<Message>(); if (Messages != null) messages.AddRange(Messages); messages.Add(message); Messages = messages.ToArray(); } finally {IsBusy = false; } }
This concludes the ViewModel layer of our application and the entirety of the shared code used on iOS and Android. For the MessageViewModel
class, you could have also chosen to put GetConversations
and Conversations
properties in their own class, since they could be considered as a separate responsibility, but it is not really necessary.
Here is the final class diagram of our ViewModel layer:
3.147.65.65