Introduction
Some said: "Silverlight is dead". Others added: "Silverlight + Desktop = WinRT". I started working on my Silverlight project long before Microsoft announced its plans on Silverlight. I think that having experience in using Silverlight can be very useful, and I'm sure it will come in handy in Microsoft technologies to come.
However, while working on my Silverlight application, I came across some issues. I spent a lot of time searching the Internet for solutions. In order to save you some time, I decided to share these solutions.
I'm not inventing the wheel here. Most of the code that you will see in my article was found online. I just put it all together. I'll do my best to provide appropriate links to the original sources of the code. If I miss something, let me know and I'll update my article accordingly. I apologize in advance for any inconvenience related to the missed links.
I'm going to use the Problem-Design-Solution approach in my article. I think that it’s a great fit for this article, and I hope that it will make for an easier reading.
One more thing: English is not my native language, so I apologize for any grammatical and/or stylistic quirks in advance.
So let's get started.
Prerequisites
In the ContactLOB project I use the following:
- Visual Studio 2010
- Silverlight 5
- Prism 4.0
- WCF RIA Services
I'd like to give you a little tip on Prism 4.0 that was released before Silverlight 5 did - recompile the Prism 4.0 libraries using Silverlight 5 references.
Problem
One of the problems I came across is the way the user is going to log in to your application. There are a lot of ways to do it.
One of the scenarios is to show a login page on an application startup which will navigate the user to a main page upon receiving their credentials. So if the users run your application, they will see a login page like the one in Figure 1.
Figure 1.
After a successful login you'll navigate the user to a main page like the one in Figure 2.
Figure 2.
If the user gets to the main page, they can't get back to the login page using the browser’s back button - the back button is disabled.
In Silverlight you must provide one and only one main page as an entry point of the application:
private void Application_Startup(object sender, StartupEventArgs e)
{
this.RootVisual = new LoginPage();
}
So you need to replace the login page with the main page somehow:
private void Application_Startup(object sender, StartupEventArgs e)
{
this.RootVisual = new MainPage();
}
The problem is that you can only assign a page to the RootVisual once. If you try to assign to the RootVisual more than once, nothing will happen. For further reference let's call this problem the Navigation Problem.
The next problem is that you might want to use the Form Authentication like this one, to authenticate the user but you don't know how to authenticate the user on the web server side using your own database. Let's call this problem the Authentication Problem.
Another problem is that you might want to encrypt a user password so it won't be sent over the Internet as plain text. One of the best ways to encrypt a user password is to use the MD5 hash. Unfortunately Microsoft does not provide libraries to get the MD5 hash on the Silverlight client side. Let's call this problem the Encryption Problem.
Design
Let's address the problems described above one by one.
To solve the Navigation Problem, I use the Prism regions.
In the Prism documentation it's called View-Based Navigation.
To solve the Authentication Problem, I use a class inherited from the AuthenticationBase
class. The class has a couple of overridden functions that can make a difference.
To solve the Encryption Problem, I use the MD5 hash. The procedure here is very simple. You encrypt the user password and send the MD5 hash to the web server. You don't need to decrypt the user password on the server side. You just need to compare encrypted password with the known MD5 hash. If there is a match, you can authorize the user. The trick is getting the MD5 hash.
Solution
I'll walk you through the process of creating a Silverlight application and applying the design described above.
- Start Visual Studio 2010 and click New Project...
- In the New Project form select the Silverlight Application project template and type in ContactLOB as a project name:
Click the OK button.
- In the New Silverlight Application form, check the Enable WCF RIA Services checkbox and click the OK button:
- Launch the project to build it. You'll have fewer problems later.
- Add the Base, ViewModels and Views folders to the ContactDB project.
- Add the Prism library references:
- Go to the Mark Harris's post and download the code that provides the MD5 hash.
Add the file to the project.
Note: You don’t need to download the source code for the ContactLOB project attached to the article - it's already there.
- Add the ViewModelBase.cs file to the Base folder.
- Insert the code into the file:
using System.ComponentModel;
namespace ContactLOB.Base
{
public class ViewModelBase : INotifyPropertyChanged
{
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if (null != PropertyChanged)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
}
The MVVM design pattern is not the goal of this article. So the implementation of the INotifyPropertyChanged
interface is very simple.
- Add the LoginViewModel.cs file to the ViewModels project folder and insert
the code:
using System.ComponentModel;
using System.ServiceModel.DomainServices.Client.ApplicationServices;
using System.Text;
using System.Windows.Input;
using ContactLOB.Base;
using FlowGroup.Crypto;
using Microsoft.Practices.Prism.Commands;
using Microsoft.Practices.Prism.Regions;
using Microsoft.Practices.ServiceLocation;
using Microsoft.Practices.Unity;
namespace ContactLOB.ViewModels
{
public class LoginViewModel : ViewModelBase
{
private AuthenticationService authService;
public LoginViewModel()
{
LoginCommand = new DelegateCommand(ClickLogin);
if (!DesignerProperties.IsInDesignTool)
{
authService = WebContext.Current.Authentication;
}
}
public ICommand LoginCommand { get; private set; }
private string userName;
public string UserName
{
get
{
return userName;
}
set
{
userName = value;
OnPropertyChanged("UserName");
}
}
private string password;
public string Password
{
get
{
return password;
}
set
{
password = value;
OnPropertyChanged("Password");
}
}
internal void ClickLogin()
{
if (authService == null) return;
LoginParameters loginParams = new LoginParameters(UserName,
GetPasswordHash(Password));
var loginOperation = authService.Login(loginParams,
(loginOp) =>
{
if (loginOp.LoginSuccess)
{
GoToMainPage();
}
else if (loginOp.HasError)
{
loginOp.MarkErrorAsHandled();
}
},
null);
}
private string GetPasswordHash(string password)
{
UTF8Encoding encoder = new UTF8Encoding();
byte[] arr = encoder.GetBytes(password);
MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider();
byte[] md5arr = md5.ComputeHash(arr);
return BytesToHexString(md5arr);
}
private string BytesToHexString(byte[] value)
{
StringBuilder sb = new StringBuilder(value.Length * 2);
foreach (byte b in value)
{
sb.AppendFormat("{0:x2}", b);
}
return sb.ToString();
}
private void GoToMainPage()
{
IRegionManager regionManager =
ServiceLocator.Current.GetInstance<IRegionManager>();
if (regionManager == null) return;
IUnityContainer container =
ServiceLocator.Current.GetInstance<iunitycontainer>();
if (container == null) return;
IRegion mainRegion = regionManager.Regions["MainRegion"];
if (mainRegion == null) return;
MainPage view = mainRegion.GetView("MainPage") as MainPage;
if (view == null)
{
view = container.Resolve<mainpage>();
mainRegion.Add(view, "MainPage");
mainRegion.Activate(view);
}
else
{
mainRegion.Activate(view);
}
}
}
}
Rebuild the project just in case.
The main part of the code above is the GoToMainPage
function. The logic is pretty straightforward. Using the Region Manager we check if the MainPage
view
is already created, and if not, create one and activate it. When we activate the MainPage
view, the user will be navigated to the MainPage
.
- Add the
LoginView
user control to the Views folder and insert the following code:
<UserControl
x:Class="ContactLOB.Views.LoginView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:ContactLOB.ViewModels"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">
<UserControl.DataContext>
<vm:LoginViewModel />
</UserControl.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto"/>
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="Auto"/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<StackPanel Grid.Row="1" Grid.Column="1">
<Border BorderThickness="2" CornerRadius="4" BorderBrush="Black">
<Grid Margin="5">
<Grid.RowDefinitions>
<RowDefinition Height="10" />
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="10"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock Text="User Name" Grid.Row="1" VerticalAlignment="Center" />
<TextBox FontSize="12" Margin="8" Grid.Column="1" Width="150" Grid.Row="1"
Text="{Binding Path=UserName, Mode=TwoWay, NotifyOnValidationError=True,
TargetNullValue=''}"
VerticalAlignment="Center"/>
<TextBlock Text="Password" Grid.Row="2" VerticalAlignment="Center" />
<PasswordBox FontSize="12" Margin="8" Grid.Column="1" Width="150" Grid.Row="2"
Password="{Binding Path=Password, Mode=TwoWay, NotifyOnValidationError=True, TargetNullValue=''}"
VerticalAlignment="Center"/>
<Button Content="Login" Grid.Column="1" Grid.Row="4" HorizontalAlignment="Left" Margin="8"
Command="{Binding Path=LoginCommand}" Width="80" />
</Grid>
</Border>
</StackPanel>
</Grid>
</UserControl>
- Add the
ShellView
user control to the Views folder and insert the following code:
<usercontrol
x:class="ContactLOB.Views.ShellView"
d:designwidth="400"
d:designheight="300"
mc:ignorable="d"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:prism="http://www.codeplex.com/prism">
<contentcontrol horizontalcontentalignment="Stretch"
verticalcontentalignment="Stretch"
prism:regionmanager.regionname="MainRegion" x:name="MainRegion">
</contentcontrol>
</usercontrol>
- Add the following code to the MainPage.xaml so you can display the user info when the user gets there:
<grid>
<textblock x:name="txtWelcome" text="This is the Main Page">
</textblock>
</grid>
- Add the code to the MainPage.xaml.cs to display the user info:
using System.Windows.Controls;
using ContactLOB.Web.Services;
namespace ContactLOB
{
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
this.Loaded += (s, e) =>
{
WebUser usr = WebContext.Current.User;
this.txtWelcome.Text = string.Format("Welcome {0} {1}",
usr.FirstName, usr.LastName);
};
}
}
}
- Add the Bootstrapper.cs file to the ContactLOB project and insert the
code:
using System.Windows;
using ContactLOB.Views;
using Microsoft.Practices.Prism.Regions;
using Microsoft.Practices.Prism.UnityExtensions;
using Microsoft.Practices.Unity;
namespace ContactLOB
{
public class Bootstrapper : UnityBootstrapper
{
protected override DependencyObject CreateShell()
{
ShellView view = this.Container.TryResolve<ShellView>();
Application.Current.RootVisual = view;
return view;
}
protected override void InitializeShell()
{
base.InitializeShell();
IRegionManager regionManager = RegionManager.GetRegionManager(Shell);
if (regionManager == null) return;
var view = this.Container.Resolve<loginview>();
if (view == null) return;
regionManager.Regions["MainRegion"].Add(view, "LoginView");
}
}
}
- Open the App.xaml.cs file and add the code:
public App()
{
this.Startup += this.Application_Startup;
this.Exit += this.Application_Exit;
this.UnhandledException += this.Application_UnhandledException;
InitializeComponent();
WebContext webContext = new WebContext();
webContext.Authentication = new FormsAuthentication()
{ DomainContext = new AuthenticationDomainContext() };
this.ApplicationLifetimeObjects.Add(webContext);
}
private void Application_Startup(object sender, StartupEventArgs e)
{
new Bootstrapper().Run();
}
Don't worry about the code line with AuthenticationDomainContext
.
It will be fixed when we're done with the server part of the application.
- Now go to the ContactLOB.Web project and add the following references:
- System.ServiceModel.DomainServices.Server
- System.ServiceModel.DomainServices.Hosting
- System.Security.
- Add the Services folder to the ContactLOB.Web project and add the
AuthenticationDomainService.cs file to the folder. Add this code to the file:
using System.Security.Principal;
using System.ServiceModel.DomainServices.Hosting;
using System.ServiceModel.DomainServices.Server.ApplicationServices;
namespace ContactLOB.Web.Services
{
[EnableClientAccess]
public class AuthenticationDomainService : AuthenticationBase<webuser>
{
protected override WebUser GetAuthenticatedUser(IPrincipal principal)
{
WebUser user = new WebUser();
user.Name = principal.Identity.Name;
user.FirstName = "Bill";
user.LastName = "Gates";
return user;
}
protected override bool ValidateUser(string userName, string password)
{
string usrName = "demo";
string pswHash = "fe01ce2a7fbac8fafaed7c982a04e229";
return (usrName.Equals(userName) && pswHash.Equals(password));
}
}
public class WebUser : UserBase
{
public string UserId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public bool IsAdmin { get; set; }
}
}
You can find the more comprehensive sample of the AuthenticationBase
usage
here.
I've included the WebUser
class to demonstrate how to pass some
of the user info from the server to the client. User info provided by the
WebUser
class is used in the Main Page (see item 14).
- 19. To make all this work you have to modify the web.config file:
="1.0"
-->
<configuration>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true">
<add name="DomainServiceModule" preCondition="managedHandler"
type="System.ServiceModel.DomainServices.Hosting.DomainServiceHttpModule,
System.ServiceModel.DomainServices.Hosting, Version=4.0.0.0,
Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
</modules>
<validation validateIntegratedModeConfiguration="false"/>
</system.webServer>
<system.web>
<compilation debug="true" targetFramework="4.0" />
<httpModules>
<add name="DomainServiceModule"
type="System.ServiceModel.DomainServices.Hosting.DomainServiceHttpModule,
System.ServiceModel.DomainServices.Hosting, Version=4.0.0.0,
Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
</httpModules>
<authentication mode="Forms"> <forms name=".ContactLOB_ASPXAUTH" /> </authentication>
</system.web>
<system.serviceModel>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true"/>
</system.serviceModel>
</configuration>
Notice the authentication tag in the web.config
. It indicates
that we're using the Form Authentication.
- Rebuild the ContactLOB.Web project and then the ContactLOB project - in
this order. The order is important here. Launch the application and enter the
user name demo and the password demo. If you put it in properly, you'll be
redirected to the MainPage.
Summary
You now know how to provide the user login in the line-of-business
applications, use the MD5 hash to encrypt the user password, use the Prism
Region Manager to navigate from the LoginPage to the MainPage and also how to
use the Form Authentication to authenticate the user. The Problem is solved.