Article Series
This article is part one of a series on developing a Silverlight business application using MEF, MVVM Light, and WCF RIA Services.
Contents
Introduction
This sample application is the result of my initiative to learn Silverlight and WCF RIA Services. With my background of using WPF and MVVM for the past
several years, I found that there is a lack of sample LOB applications that can combine the latest Silverlight enhancements with MVVM. This three part article
series is my effort at creating such a sample. The choice of an issue tracking application comes from David Poll's PDC09 talk,
and the design architecture is from Shawn Wildermuth's blog posts.
The main features of this issue tracking application are:
- Login screen provides custom authentication and password reset based on security question and answer.
- My Profile screen is for updating user information, password, security questions and answers.
- User Maintenance screen is only available to Admin users, and lets the Admin user add/delete/update users.
- New Issue screen is for creating new issues (bugs, work items, spec defects, etc.).
- My Issues screen is for tracking all active and resolved issues assigned to a user.
- All Issues screen is for tracking all issues (Open, Active, Pending, or Resolved).
- Bug Report screen provides a summary of bug trend, bug count, and the functionality to print the summary.
- Four different Themes are available and can be applied dynamically at any time.
Requirements
In order to build the sample application, you need:
- Microsoft Visual Studio 2010 SP1
- Silverlight 4 Toolkit April 2010 (included in the sample solution)
- MVVM Light Toolkit V3 SP1 (included in the sample solution)
Installation
After downloading the setup package to a location on your local disk, we need to complete the following steps:
1. Installing the IssueVision Sample Database
To install the sample database, please run SqlServer_IssueVision_Schema.sql and SqlServer_IssueVision_InitialDataLoad.sql included in the setup package
zip file. SqlServer_IssueVision_Schema.sql creates the database schema and database user IVUser
;
SqlServer_IssueVision_InitialDataLoad.sql loads all the data needed to run this application, including the initial application user ID user1
and Admin user ID admin1, with passwords all set as P@ssword1234.
2. Installing the Web Setup Package
After the database setup, run setup.exe also included in the setup package zip file. This will install the IssueVision for Silverlight website.
When done installing the website, we can access the Silverlight application as follows:
Architecture
1. Solution Structure
Inside the sample solution file, projects are further organized into either the Client folder or the Server folder. The Client
folder includes all the projects that will be compiled into the file IssueVision.Client.xap, and the Server folder consists of all
the projects that will eventually run inside a web server environment.
For the projects inside the Server folder:
- IssueVision.Web project is the main startup project. It includes the startup page Default.aspx and the Silverlight application package
IssueVision.Client.xap.
- IssueVision.Data.Web project is the server-side data access layer. It receives requests from clients, accesses the database through the
database user IVUser, and returns the results back. The major components of this project include the
IssueVision
Entity data
model and all related DomainService
classes.
For the projects inside the Client folder:
- IssueVision.Data project has a WCF RIA Services link to
IssueVision.Data.Web
, and therefore hosts the generated
client-side proxy code and shared source code. This project also includes all the client-side only partial classes that do not need to be duplicated on the server side.
- IssueVision.Common project, as the name suggests, includes all the common interface classes and helper classes shared among other client projects.
- IssueVision.Model project defines the Model of MVVM, and it has the following three model classes:
AuthenticationModel
PasswordResetModel
IssueVisionModel
- IssueVision.ViewModel project is the ViewModel part of MVVM, and includes all the nine ViewModel classes.
- IssueVison.Client is the main client-side project, and is also the View of MVVM that hosts all the UI logic.
From the solution structure above, we should notice that MVVM provides good separation of concerns between the UI and the business logic in order to make
those UIs easier to maintain by developers and designers. Next, let's visit the Model
, ViewModel
, and View
classes in more detail.
2. IssueVisionModel Class
We will discuss the classes AuthenticationModel
and PasswordResetModel
in part 3. For now, let's focus on the class
IssueVisionModel
, the main Model (of MVVM) class for this application. IssueVisionModel
is based on the following interface,
IIssueVisionModel
:
public interface IIssueVisionModel : INotifyPropertyChanged
{
void GetIssueTypesAsync();
event EventHandler<EntityResultsArgs<IssueType>> GetIssueTypesComplete;
void GetPlatformsAsync();
event EventHandler<EntityResultsArgs<Platform>> GetPlatformsComplete;
void GetResolutionsAsync();
event EventHandler<EntityResultsArgs<Resolution>> GetResolutionsComplete;
void GetStatusesAsync();
event EventHandler<EntityResultsArgs<Status>> GetStatusesComplete;
void GetSubStatusesAsync();
event EventHandler<EntityResultsArgs<SubStatus>> GetSubStatusesComplete;
void GetUsersAsync();
event EventHandler<EntityResultsArgs<User>> GetUsersComplete;
void GetCurrentUserAsync();
event EventHandler<EntityResultsArgs<User>> GetCurrentUserComplete;
void GetSecurityQuestionsAsync();
event EventHandler<EntityResultsArgs<SecurityQuestion>>
GetSecurityQuestionsComplete;
void GetMyIssuesAsync();
event EventHandler<EntityResultsArgs<Issue>> GetMyIssuesComplete;
void GetAllIssuesAsync();
event EventHandler<EntityResultsArgs<Issue>> GetAllIssuesComplete;
void GetAllUnresolvedIssuesAsync();
event EventHandler<EntityResultsArgs<Issue>> GetAllUnresolvedIssuesComplete;
void GetActiveBugCountByMonthAsync(Int32 numberOfMonth);
event EventHandler<InvokeOperationEventArgs> GetActiveBugCountByMonthComplete;
void GetResolvedBugCountByMonthAsync(Int32 numberOfMonth);
event EventHandler<InvokeOperationEventArgs> GetResolvedBugCountByMonthComplete;
void GetActiveBugCountByPriorityAsync();
event EventHandler<InvokeOperationEventArgs> GetActiveBugCountByPriorityComplete;
Issue AddNewIssue();
void RemoveAttribute(IssueVision.Data.Web.Attribute attribute);
void RemoveFile(IssueVision.Data.Web.File file);
User AddNewUser();
void RemoveUser(IssueVision.Data.Web.User user);
void SaveChangesAsync();
event EventHandler<SubmitOperationEventArgs> SaveChangesComplete;
void RejectChanges();
Boolean HasChanges { get; }
Boolean IsBusy { get; }
}
We define a separate Model
class and do not use the data context class itself as the Model
because the Model
is best
expressed as a set of properties and operations that retrieve, add, delete, and update data. This makes the Model
easier to maintain and test.
Additionally, as Shawn mentioned in his blog, "creating a custom Model
allows us to isolate what transport layer we're using so we can change
it or even have several data providers specifying data for our Model
".
Next, let's look at how a retrieve method in the IssueVisionModel
class is actually implemented:
public void GetIssueTypesAsync()
{
PerformQuery(Context.GetIssueTypesQuery(), GetIssueTypesComplete);
}
GetIssueTypeAsync()
calls the private
method PerformQuery()
and passes in an EntityQuery GetIssueTypesQuery()
and an event GetIssueTypesComplete
. When the retrieve call is done, the event GetIssueTypesComplete
will fire and pass back the result set, or any error message if something goes wrong. In fact, almost all retrieve methods are as simple as calling the
PerformQuery()
method defined below:
private void PerformQuery<T>(EntityQuery<T> qry,
EventHandler<EntityResultsArgs<T>> evt) where T : Entity
{
Context.Load(qry, LoadBehavior.RefreshCurrent, r =>
{
if (evt != null)
{
try
{
if (r.HasError)
{
evt(this, new EntityResultsArgs<T>(r.Error));
r.MarkErrorAsHandled();
}
else
{
evt(this, new EntityResultsArgs<T>(r.Entities));
}
}
catch (Exception ex)
{
evt(this, new EntityResultsArgs<T>(ex));
}
}
}, null);
}
Also, the Model
class exports itself to the ViewModel
classes by using the MEF Export
attribute on the class as follows:
[Export(typeof(IIssueVisionModel))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class IssueVisionModel : IIssueVisionModel
3. The ViewModel Classes
Most of the ViewModel
classes include six regions: Private Data Members region, Constructor region, Public Properties region,
Public Commands region, ICleanup Interface region, and Private Methods region. The Public Properties
and Public Commands regions expose all the necessary properties and commands to its View
class. And, the constructor
sets up event handling, sets the initial values for any private
data, and registers the AppMessages
needed inside
the ViewModel
class. Here is an example:
[ImportingConstructor]
public IssueEditorViewModel(IIssueVisionModel issueVisionModel)
{
_issueVisionModel = issueVisionModel;
_issueVisionModel.GetIssueTypesComplete += _issueVisionModel_GetIssueTypesComplete;
_issueVisionModel.GetPlatformsComplete += _issueVisionModel_GetPlatformsComplete;
_issueVisionModel.GetResolutionsComplete += _issueVisionModel_GetResolutionsComplete;
_issueVisionModel.GetStatusesComplete += _issueVisionModel_GetStatusesComplete;
_issueVisionModel.GetSubStatusesComplete += _issueVisionModel_GetSubStatusesComplete;
_issueVisionModel.GetUsersComplete += _issueVisionModel_GetUsersComplete;
_currentIssueCache = null;
IssueTypeEntries = null;
_issueVisionModel.GetIssueTypesAsync();
PlatformEntries = null;
_issueVisionModel.GetPlatformsAsync();
ResolutionEntriesWithNull = null;
_issueVisionModel.GetResolutionsAsync();
StatusEntries = null;
_issueVisionModel.GetStatusesAsync();
SubstatusEntriesWithNull = null;
_issueVisionModel.GetSubStatusesAsync();
UserEntries = null;
UserEntriesWithNull = null;
_issueVisionModel.GetUsersAsync();
AppMessages.EditIssueMessage.Register(this, OnEditIssueMessage);
}
We can see from the code above that the ViewModel
class gets an object that implements the IIssueVisionModel
interface by using the
ImportingConstructor
attribute which tells MEF to supply the discovered model class into the ViewModel
. In turn, all the
ViewModel
classes export themselves like the following:
[Export(ViewModelTypes.IssueEditorViewModel, typeof(ViewModelBase))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class IssueEditorViewModel : ViewModelBase
4. The View Classes and the Code-behind Files
Before we discuss any View
class, let us first take a look at how a global CompositionContainer
object is defined inside the file App.xaml.cs.
public partial class App : Application
{
public static CompositionContainer Container;
public App()
{
Startup += Application_Startup;
Exit += Application_Exit;
UnhandledException += Application_UnhandledException;
InitializeComponent();
}
private void Application_Startup(object sender, StartupEventArgs e)
{
Container = new CompositionContainer(new DeploymentCatalog());
CompositionHost.Initialize(Container);
CompositionInitializer.SatisfyImports(this);
RootVisual = new MainPage();
}
......
}
With access to the static Container
object, we can easily request a new ViewModel
object as follows:
_viewModelExport = App.Container.GetExport<ViewModelBase>(
ViewModelTypes.AllIssuesViewModel);
if (_viewModelExport != null) DataContext = _viewModelExport.Value;
And, we can release a ViewModel
object with the following three lines of code:
DataContext = null;
App.Container.ReleaseExport(_viewModelExport);
_viewModelExport = null;
Each View
class finds its ViewModel
object through a function call to _viewModelExport = App.Container.GetExport()
, followed
by DataContext = _viewModelExport.Value
in the constructor of each View
class. This function instructs MEF at runtime to fulfill
a chain of dependencies, which in turn creates all the Model
and ViewModel
objects required. The beauty of using MEF is that we can
keep these projects loosely coupled. In fact, the projects IssueVision.Model, IssueVision.ViewModel, and
IssueVison.Client do not need a reference to the other two projects to compile successfully. The project IssueVison.Client has a reference
to the other two projects only because we need to add them into the output IssueVision.Client.xap file.
In the same constructor, we also register AppMessage
s the View class will handle. The IssueEditor
class below is a good sample:
public partial class IssueEditor : UserControl, ICleanup
{
#region "Private Data Members"
private Lazy<ViewModelBase> _viewModelExport;
#endregion "Private Data Members"
#region "Constructor"
public IssueEditor()
{
InitializeComponent();
AppMessages.ReadOnlyIssueMessage.Register(this, OnReadOnlyIssueMessage);
AppMessages.OpenFileMessage.Register(this, OnOpenFileMessage);
AppMessages.SaveFileMessage.Register(this, OnSaveFileMessage);
if (!ViewModelBase.IsInDesignModeStatic)
{
_viewModelExport = App.Container.GetExport<ViewModelBase>(
ViewModelTypes.IssueEditorViewModel);
if (_viewModelExport != null) DataContext = _viewModelExport.Value;
}
}
#endregion "Constructor"
.........
}
Within the code-behind files, we define all the UI-related logic like event handlers to dynamically enable/disable a button or AppMessage
s that
display an error message when something goes wrong. As long as the code is related to the UI logic, it is perfectly OK to add them into the code-behind
file, like the following:
private void userNameTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
if (!string.IsNullOrWhiteSpace(loginScreenErrorMessageTextBox.Text))
loginScreenErrorMessageTextBox.Text = string.Empty;
loginButton.IsEnabled =
!(string.IsNullOrWhiteSpace(userNameTextBox.Text) ||
string.IsNullOrWhiteSpace(passwordPasswordBox.Password));
}
private void passwordPasswordBox_PasswordChanged(object sender, RoutedEventArgs e)
{
if (!string.IsNullOrWhiteSpace(loginScreenErrorMessageTextBox.Text))
loginScreenErrorMessageTextBox.Text = string.Empty;
loginButton.IsEnabled =
!(string.IsNullOrWhiteSpace(userNameTextBox.Text) ||
string.IsNullOrWhiteSpace(passwordPasswordBox.Password));
}
Custom Controls for Layout
The custom controls FlipControl
and MainPageControl
defined in the project IssueVision.Common are used as the basis for screen layout.
FlipControl
is used in LoginForm.xaml, which hosts both the login screen and the password reset screen. Switching the Dependency
Property IsFlipped
will toggle between these two screens, with animations defined inside the VisualStateManager
.
Similarly, MainPageControl
is used in MainPage.xaml, and divides the whole screen into title content, login/logout menu contents,
login page content, and main page content. The Dependency Property IsLoggedIn
switches between the login page and the main page.
Using this layout, custom controls could be considered as another application of separation of concerns. The screen layout styles along with animations
defined in VisualStateManager
is encapsulated by itself. As long as they provide the same functionality, we can easily change them, let's say
creating a new animation, without affecting any View
classes defined in the project IssueVision.Client.
Dynamic Theming
There are four different themes defined in this application, and they are BureauBlue
, ExpressionLight
, ShinyBlue
,
and TwilightBlue
. Each theme is included in the project IssueVision.Client as a ResourceDictionary
, which defines
all the styles for built-in controls as well as styles for custom controls built specifically for this sample. They are in the Assets folder shown below:
When we want to dynamically change themes, ChangeThemmeCommand
will get called, and following is the source code:
private RelayCommand<string> _changeThemeCommand = null;
public RelayCommand<string> ChangeThemeCommand
{
get
{
if (_changeThemeCommand == null)
{
_changeThemeCommand = new RelayCommand<string>(
OnChangeThemeCommand,
g =>
{
var themeResource = Application.GetResourceStream
(new Uri("/IssueVision.Client;component/Assets/" +
g, UriKind.Relative));
return themeResource != null;
});
}
return _changeThemeCommand;
}
}
private void OnChangeThemeCommand(String g)
{
try
{
if (g == "BureauBlue.xaml" || g == "ExpressionLight.xaml" ||
g == "ShinyBlue.xaml" || g == "TwilightBlue.xaml")
{
Application.Current.Resources.MergedDictionaries.RemoveAt
(Application.Current.Resources.MergedDictionaries.Count - 1);
var themeResource = Application.GetResourceStream(new Uri
("/IssueVision.Client;component/Assets/" +
g, UriKind.Relative));
var rd = (ResourceDictionary)(XamlReader.Load
(new StreamReader(themeResource.Stream).ReadToEnd()));
Application.Current.Resources.MergedDictionaries.Add(rd);
if (g == "BureauBlue.xaml")
{
IsBureauBlueTheme = true;
IsExpressionLightTheme = false;
IsShinyBlueTheme = false;
IsTwilightBlueTheme = false;
}
else if (g == "ExpressionLight.xaml")
{
IsBureauBlueTheme = false;
IsExpressionLightTheme = true;
IsShinyBlueTheme = false;
IsTwilightBlueTheme = false;
}
else if (g == "ShinyBlue.xaml")
{
IsBureauBlueTheme = false;
IsExpressionLightTheme = false;
IsShinyBlueTheme = true;
IsTwilightBlueTheme = false;
}
else if (g == "TwilightBlue.xaml")
{
IsBureauBlueTheme = false;
IsExpressionLightTheme = false;
IsShinyBlueTheme = false;
IsTwilightBlueTheme = true;
}
}
}
catch (Exception ex)
{
AppMessages.RaiseErrorMessage.Send(ex);
}
}
I like the flexibility of using ResourceDictionary
directly for dynamic theming because we can easily modify them any time there is a bug found
or any enhancements are needed. Also, we have the option to define our own styles for any custom controls, as follows:
<ResourceDictionary>
.........
-->
<LinearGradientBrush x:Key="IssueVisionBackgroundBrush" EndPoint="1,0.5"
StartPoint="0,0.5">
<GradientStop Color="#FFBFDBFF" Offset="0"/>
<GradientStop Color="#FFA6C2E5" Offset="1"/>
</LinearGradientBrush>
-->
<Style TargetType="common:MainPageControl">
<Setter Property="Background"
Value="{StaticResource IssueVisionBackgroundBrush}"/>
</Style>
-->
<Style TargetType="common:FlipControl">
<Setter Property="Background"
Value="{StaticResource IssueVisionBackgroundBrush}"/>
<Setter Property="BorderBrush" Value="DarkBlue"/>
<Setter Property="BorderThickness" Value="3"/>
<Setter Property="CornerRadius" Value="4"/>
</Style>
</ResourceDictionary>
Next Steps
In this article, we visited how the application is installed, as well as the design architecture, layout custom controls, and dynamic theming. In part 2, we
will go through the topics of how the MVVM Light Toolkit is used: namely, RelayCommand
, Messenger
, EventToCommand
, and ICleanup
.
I hope you find this article useful, and please rate and/or leave feedback below. Thank you!
History
- May 2010 - Initial release
- July 2010 - Minor update based on feedback
- November 2010 - Update to support VS2010 Express Edition
- February 2011 - Update to fix multiple bugs including memory leak issues
- March 2011 - Built with Visual Studio 2010 SP1
- June 2011 - Update based on feedback
- July 2011 - Update to fix multiple bugs