In Part 1 of this series I talked about and described the Model-View-ViewModel (MVVM) pattern.
In this post I will describe the application I wish to build to demonstrate this pattern to you, and at the same time demonstrate usage of the Silverstone framework.
A "social networking" application
In picking my sample app I wanted to choose something fairly simple. I decided on a small social networking app, which would basically allow the following use cases:
- Register - A user can enter their email address and choose a password. The user must confirm their password. Password must be between 4 and 10 characters. After registering, the user proceeds to use case 3
- Login - An existing user can enter their email address and password to login. If they enter the wrong details the system should inform them that their details were incorrect. After logging in, the user proceeds to use case 3
- View Friends - Displays a list of friends for the currently logged in user.
And that's it! Despite being fairly minimal (and not that useful without being able to add friends or contact them!) that's all I am going to build for now I think.
Creating a solution
We will name this application "Silversocial" (not very imaginative I know, but it's only a demo app!)
You will require Visual Studio 2008, the Silverlight Tools for Visual Studio, and a copy of
Silverstone.dll.
Start off by creating a new project of type Silverlight Application named
Silversocial.Client.Views. Choose to create a new solution and call it
SilversocialChoose to add a new Web to the solution for hosting the Silverlight content, and call it
Silversocial_WebHost (I choose a web application because I prefer them). The solution will be created containing a Silverlight project and a Web application project.
Out of interest, if you right click on the Web Application project properties, you will see a new tab called "Silverlight links" and you will see that the Silverlight application you just created has been added for you automatically. All this really means is that when you build the Silverlight application, the compiled .xap file will be copied into the Web application's /ClientBin folder.
The shell
The shell in a Silverstone application is the central view component which is responsible for managing the other views in the application. Hence, the IShell interface contains just one method:
void SetView(IView view)Typically an application will implement the shell as the main UI component containing any navigation and common elements for every page (much like ASP.NET's master page). The application will create its own derived IShell interface and expose any elements required by the individual pages through that interface.
So, given the requirements specified earlier, our application's shell only requires an extra property - the currently logged in User. This would allow the shell's view to display the user and allow the other pages to retrieve it when necessary.
To begin, we will create another Silverlight class project called Silversocial.Client.Modules to house the rest of our application's interfaces and concrete implementations. Obviously in a larger application you would consider splitting your code into multiple assemblies grouped by role.
First, the User class which stores the username and password:
using System.ComponentModel;
namespace Silversocial.Client.Modules
{
public class User : INotifyPropertyChanged
{
private string username;
private string password;
public event PropertyChangedEventHandler PropertyChanged = (s,p) => {};
public string Username
{
get { return this.username; }
set
{
this.username = value;
this.PropertyChanged(this, new PropertyChangedEventArgs("Username"));
}
}
public string Password
{
get { return this.password; }
set
{
this.password = value;
this.PropertyChanged(this, new PropertyChangedEventArgs("Password"));
}
}
}
}
And now the shell interface itself:
using Silverstone;
namespace Silversocial.Client.Modules
{
public interface IAppShell : IShell
{
User User { get; set; }
}
}
Creating the first ViewModel
For now, we will not worry about building a concrete View. Instead, we will start with our ViewModel. The page we will build is the Login page, and we will create a new class called LoginViewModel for it.
ILoginView will be the interface which our View will have to implement, and has a single method as follows:
using Silverstone;
namespace Silversocial.Client.ViewModels
{
public interface ILoginView : IView
{
void LoginUnsuccessful();
}
}
The contract states that the LoginUnsuccessful() method will be called by the ViewModel if the login is unsuccessful.
On the other side of the equation, our ViewModel will require some way of actually validating the user. In the real world application we will expose a service for doing this, but for now, let's stub it out with the following interface:
using System;
namespace Silversocial.Client.Modules
{
public class ValidateUserCompletedEventArgs : EventArgs
{
public ValidateUserCompletedEventArgs(bool successful)
{
this.Successful = successful;
}
public bool Successful { get; set; }
}
public interface ILoginDataProvider
{
event EventHandler<ValidateUserCompletedEventArgs> ValidateUserCompleted;
void ValidateUser(User user);
}
}
(Note that this interface is implementing an asynchronous pattern - raising the ValidateUserCompleted event when the method has completed. This ties in well with the fact that Ajax requests are asynchronous, and that Silverlight's WCF ChannelFactory implementation only supports asynchronous requests.)
The View Model is shown below. The main features are:
- All dependencies passed through as interfaces to the constructor. This includes other views, the data provider, and the shell.
- Property exposed for the User, which the View can bind to.
- Property exposed for whether we are in the middle of a login attempt, which the View can bind to.
- Exposes a Login command which will attempt to use the data provider to validate the user. If successful the callback will set the user on the shell and change to the FriendListView. If unsuccessful the callback will call the LoginUnsuccessful() method on the view.
- Exposes a Register command which will simply change views to the RegisterView.
using Silverstone;
namespace Silversocial.Client.Modules
{
/// <summary>
/// ViewModel for the login page
/// </summary>
public class LoginViewModel : ViewModelBase<ILoginView>
{
// Dependencies
private readonly IAppShell shell;
private readonly ILoginDataProvider dataProvider;
private readonly IFriendListView friendListView; // This is just an empty view interface for now
private readonly IRegisterView registerView; // This is just an empty view interface for now
// Commands
private readonly ICommand login;
private readonly ICommand register;
// Data Fields
private readonly User user = new User();
private bool isLoggingIn;
public LoginViewModel(IAppShell shell, ILoginDataProvider dataProvider, IFriendListView gameChooserView, IRegisterView registerView)
{
this.shell = shell;
this.registerView = registerView;
this.friendListView = gameChooserView;
this.dataProvider = dataProvider;
this.login = new LoginCommand(this);
this.register = new RegisterCommand(this);
}
/// <summary>
/// Gets the user being logged in
/// </summary>
public User User
{
get { return this.user; }
}
/// <summary>
/// Gets or sets whether the user is currently being logged in
/// </summary>
public bool IsLoggingIn
{
get { return isLoggingIn; }
set
{
isLoggingIn = value;
this.RaisePropertyChanged("IsLoggingIn");
}
}
/// <summary>
/// Gets the command used to login the user
/// </summary>
public ICommand Login
{
get { return this.login; }
}
/// <summary>
/// Gets the command used to register a new user
/// </summary>
public ICommand Register
{
get { return register; }
}
private class LoginCommand : CommandBase<LoginViewModel>
{
public LoginCommand(LoginViewModel viewModel) : base(viewModel)
{
this.ViewModel.dataProvider.ValidateUserCompleted += this.HandleValidateUserCompleted;
}
public override bool CanExecute(object parameter)
{
// Used to inform the command framework that we can only login once the user
// has entered a username and password and we are not already in the middle of a login
return !string.IsNullOrEmpty(this.ViewModel.User.Username)
&& !string.IsNullOrEmpty(this.ViewModel.User.Password)
&& !this.ViewModel.IsLoggingIn;
}
public override void Execute(object parameter)
{
// Call the Async method on the data provider to validate the user. The ViewModel will
// handle the response to this method
this.ViewModel.dataProvider.ValidateUser(this.ViewModel.User);
// Record that we are currently in the middle of a login attempt
this.ViewModel.IsLoggingIn = true;
}
private void HandleValidateUserCompleted(object sender, ValidateUserCompletedEventArgs e)
{
// Record that we have finished attempting to log in
this.ViewModel.IsLoggingIn = false;
if (e.Successful)
{
this.ViewModel.shell.User = this.ViewModel.User;
this.ViewModel.shell.SetView(this.ViewModel.friendListView);
}
else
{
this.ViewModel.View.LoginUnsuccessful();
}
// Raise the "CanExecuteChanged" method
this.RaiseCanExecuteChanged();
}
}
private class RegisterCommand : CommandBase<LoginViewModel>
{
public RegisterCommand(LoginViewModel viewModel) : base(viewModel)
{
}
public override void Execute(object parameter)
{
// Simply use the shell to navigate to the register view when this command is executed
this.ViewModel.shell.SetView(this.ViewModel.registerView);
}
}
}
}
The next post
In the next post I will talk about unit testing the view model, and will start to wire things together so our designer can build a View for all this.