Live example at this: Link (registration and login required)
Note: Make sure you install, and run LinqPrep before installing the module. For directions on installing DotNetNuke modules, see the example at this link.
Silverlight View Model Series:
A Silverlight Expense Report Module
This module allows you to easily gather, and process Expense Reports, from users in your DotNetNuke portal.
Let's first look at the application:
When portal users access the module, they will enter a name and a description for each of their Expense Reports.
They will enter the report details in the bottom section.
Because this is a Silverlight application, they can attach a scan of their receipt, that can be any size.
They simply click, Insert to add the detail item.
- The line items show up in a sortable Grid.
- Items can be deleted by clicking the "X" next to each line (a confirmation box will show).
- The scans can be viewed by clicking the paper clip icon.
- A summary and total is displayed on the right side.
The Expense Report can then be Printed.
The Expense Report, is printed, using a template that can easily be altered using Microsoft Expression Blend.
The default template provides a line to sign, and date the report.
For some organizations, it is convenient to have the users scan in their signed Expense Report.
When an Administrator logs in, they will see a dropdown that shows the users that have submitted an Expense Report. They can select the user to see all their reports.
The Administrator will usually Lock the report while they are processing it.
The original user, will still be able to see the report, but they will not be able to make any changes unless the Administrator unlocks it.
The Administrator will usually mark it Approved after reviewing all the scanned receipts. This allows the accounting personnel to know when they should reimburse the user.
After accounting has reimbursed the user, the Expense Report is then marked Completed.
Advantages of Using Silverlight
- It is faster - When viewing the Expense Reports for a single user, there are no post-backs. The application moves considerably faster than a normal web application.
- It will not time-out - Normal web applications require you to input something every 20 minutes, or you will be timed-out, and will lose any un-saved information.
- Large file uploads - Users can upload scans of any size.
- It can be re-designed with no code - This allows a designer, to completely redesign this application using Microsoft Expression Blend, with no code changes. Simply open the source up and make changes. When you compile the changes, it will create a "ExpenseReports.xap" file. Simply replace the file in the "DesktopModules\ExpenseReports\ClientBin" directory and you're done!
View Model / MVVM
This application is designed using View Model (MVVM) structure.
The biggest benefit, is that it allows a Developer to create an application with no User Interface (UI). A Designer is then able to create the entire UI using Microsoft Expression Blend 4+, without writing a single line of code.
If you are new to View Model Style it is suggested that you read Silverlight View Model Style : An (Overly) Simplified Explanation for an introduction.
Above, is an overview of the Database, and the Web Service.
Above, is the overview of the classes for the View Model and the Model.
Above, shows the Website, and Silverlight project files.
Walk-Thru Of The Application Starting
To give you an idea of the flow of the application, we will look at how the application starts.
<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
id="silverlightControl">
<param name="source" value="<%=SilverlightSourceParams %>" />
<param name="InitParams" value="<%=SilverlightInitParams %>" />
<param name="onError" value="onSilverlightError" />
<param name="background" value="white" />
<param name="minRuntimeVersion" value="4.0.41108.0" />
<param name="autoUpgrade" value="true" />
<a href="http://go.microsoft.com/fwlink/?LinkID=149156&v=4.0.41108.0" style="text-decoration: none">
<img src="http://go.microsoft.com/fwlink/?LinkId=161376" alt="Get Microsoft Silverlight"
style="border-style: none" />
</a>
</object>
The first thing to load is the Silverlight Control code. The markup is coded to allow SilverlightSourceParams, and SilverlightInitParams, to be set at run-time.
SilverlightSourceParams = this.TemplateSourceDirectory + "/ClientBin/ExpenseReports.xap";
SilverlightInitParams = string.Format("UserId={0},RIAKey={1},PortalId={2},IPAddress={3},IsAdmin={4}," +
"AuthUserId={5},CurrentUser={6}",
UserIdToAdminister.ToString(), strRIAKey, PortalId.ToString(), this.Request.UserHostAddress,
boolIsAdmin.ToString(), UserId.ToString(), UserToAdminister);
The values are set using the code above. A RIAKey is created for the current user and placed in the ExpenseReports_RIAUser table. This RIAKey will be used on all subsequent requests from the Silverlight application.
The App.xaml.cs file, in the Silverlight application, receives all the parameters and stores them in the Resources of the application, under a key called "RIAAuthenticationHeader":
private void Application_Startup(object sender, StartupEventArgs e)
{
RIAAuthenticationHeader RIAAH = new RIAAuthenticationHeader();
RIAAH.UserID = Convert.ToInt32(e.InitParams["UserId"]);
RIAAH.Password = Convert.ToString(e.InitParams["RIAKey"]);
RIAAH.PortalID = Convert.ToInt32(e.InitParams["PortalId"]);
RIAAH.IPAddress = Convert.ToString(e.InitParams["IPAddress"]);
RIAAH.IsAdmin = Convert.ToBoolean(e.InitParams["IsAdmin"]);
RIAAH.AuthUserID = Convert.ToInt32(e.InitParams["AuthUserId"]);
RIAAH.Username = Convert.ToString(e.InitParams["CurrentUser"]);
Application.Current.Resources.Add("RIAAuthenticationHeader", RIAAH);
this.RootVisual = new MainPage();
}
Next, the MainPage.xaml file (the View), contains the code, that specifies that the MainPageModel class is its View Model:
<UserControl.DataContext>
<local:MainPageModel/>
</UserControl.DataContext>
the MainPageModel class calls GetReportsFromModel in it's constructor:
#region GetReportsFromModel
private void GetReportsFromModel()
{
colReports.Clear();
Model.GetReports((sender, EventArgs) =>
{
if (EventArgs.Error == null)
{
foreach (var Report in EventArgs.Result)
{
colReports.Add(Report);
}
if (colReports.Count > 0)
{
SelectedReportIndex = 0;
GetReportFromModel(EventArgs.Result[0].ID);
}
else
{
SetToNewReport();
}
#region Process Any Errors
colErrors.Clear();
foreach (var Report in EventArgs.Result)
{
if (Report.Errors.Count > 0)
{
foreach (var item in Report.Errors)
{
colErrors.Add(item);
}
}
}
ErrorsVisibility = (colErrors.Count > 0) ? Visibility.Visible : Visibility.Collapsed;
#endregion
}
});
}
#endregion
It calls the GetReports method in the Model, and processes the result, and fills the colReports collection, that is displayed in the drop down at the top of the page in the View.
The GetReports method, like all the methods in the Model is very simple. Note that it calls the GetAuth method, that gets the contents of the RIAAuthenticationHeader class and passes it's parameters to the web service:
#region GetReports
public static void GetReports(EventHandler<GetReportsCompletedEventArgs> eh)
{
WebServiceSoapClient WS = new WebServiceSoapClient();
WS.Endpoint.Address = new EndpointAddress(GetBaseAddress());
WS.GetReportsCompleted += eh;
WS.GetReportsAsync(GetAuth());
}
#endregion
It calls a slightly more complicated web method, that is complicated mostly because it has to perform security:
#region GetReports
[WebMethod]
public List<ExpenseReports_Report> GetReports(RIAAuthenticationHeader Auth)
{
// Make a empty collection so that something will always be returned
List<ExpenseReports_Report> colExpenseReports_Report = new List<ExpenseReports_Report>();
// See if the user is authorized
RIAAuthentication RIAAuth = new RIAAuthentication(Auth);
if (RIAAuth.IsUserValid())
{
try
{
ExpenseReportsDBDataContext db = new ExpenseReportsDBDataContext();
// Unless a User is an Administrator, set the UserID to AuthUserID
// to prevent a non Administrator from getting somone elses Reports
if (!RIAAuth.IsAdmin())
{
Auth.UserID = Auth.AuthUserID;
}
// Get The Reports
var reports = from ExpenseReports_Reports in db.ExpenseReports_Reports
where ExpenseReports_Reports.UserID == Auth.UserID
orderby ExpenseReports_Reports.InsertDate descending
select ExpenseReports_Reports;
foreach (var item in reports)
{
// Set the ExpenseReports_Details to null
// To prevent it from being returned (because it is not needed)
// Also to prevent the web service call from failing when trying
// To return a complex type
item.ExpenseReports_Details = null;
colExpenseReports_Report.Add(item);
}
}
catch (System.Exception ex)
{
// If there are errors, add them to the response
ExpenseReports_Report ER = new ExpenseReports_Report();
ER.ID = -1;
ER.Errors.Add(ex.Message);
colExpenseReports_Report.Add(ER);
}
}
// Return the response
return colExpenseReports_Report;
}
#endregion
The colReports collection (in the View Model), is bound to the drop down (in the View), and it automatically fills when the web service returns its response.
Pointers To Others Parts Of The Code
I have written a number of blogs and tutorials over the past few weeks that cover many of the techniques and code used.
Behaviors To Eliminate Code Behind
The only reason we want to eliminate code behind (and put all code in the View Model and supporting classes), is that our Designers may not be programmers. If we have any code behind, and the Designer accidently deletes something in the UI (the View), they will need a programmer to put things back together!
Usually in my projects, after the initial version, I do not touch any file with a .xaml extensions, and the Designers do not touch anything with a .cs extension. This works if I do not use any code behind.
As Ian Lackey told me once, 99% of the time you think you need code behind, you can use a Behavior. This project uses the following Behaviors:
DataGrid Helper Class
I wrote the following Blog explaining how the delete button works on the DataGrid:
Validation
I wrote the following CodeProject article to explain how the server side validation works:
File Upload
The code for the File Upload is covered in this CodeProject article:
Properties, Collections, And ICommands (and maybe Behaviors and Value Converters)
Thee are many variations to the View Model / MVVM pattern, but, when starting out, it is sometimes helpful to try to limit yourself to using just Properties, Collections, And ICommands. You may also need to use Behaviors and Value Converters. I have found that this is all that is needed, even in my most complex applications.