Introduction
There are several posts about how to implement the master page feature in Silverlight. The question is do we really need this master page feature in Silverlight. If there is an advantage to using the master page features in ASP.NET, then I can’t see a reason why Silverlight can’t take advantage of this as well. This article will demonstrate how to build a traditional master page style application in Silverlight.
System Requirements
Design Requirements
Just like traditional web pages, the login page is in start up as Fig 1. The login control consists of two text boxes, a combo box, and two button controls. The text boxes collect the username and password information from the user, the combo box is used to determine which environment to login. The Cancel button will remove the data in the text boxes and the Login button is used to submit the information for authentication.
Fig. 1. Login page
After clicking the login button, the MainPage will display as in Fig. 2.
Fig. 2. MainPage
The MainPage has two main sections: master page section and sub page section, as in Fig. 3. The master page section has a command buttons bar across the top and a tree view menu on the left side. The sub page section has a content area on the right.
Fig. 3. Master Page section and Sub Page section
The master page consists of:
- Form Title Label: to display sub form ID
- User ID Label: to display the current user
- System Label: to display system environment name
- Date Label: to display current date
- Count Label: to display data record count
- Status Label: to display current state
- Tree view: to dynamically change the content in the content area
- 11 command buttons: to do action in sub page
Fig. 4. Details of master page and sub page
The 11 command buttons include the following:
- Search: to trigger the Search state
- Execute: to extract the server data back to the client and also trigger the Modify state
- Edit: to enable editable field controls
- Delete: to delete the current record
- Save: to update changes
- First Record: go to the first record
- Previous Record: go to the previous record
- Next Record: go to the next record
- Last Record: go to the last record
- Excel: to export data to Excel
- Exit: to exit and close the browser
Fig. 5. The 11 command buttons description
There are four kinds of states:
INITIAL: Search button is enabled, as in Fig. 6.
Fig. 6. Initial state
SEARCH: Search and Execute is enabled, as in Fig. 7.
Fig. 7. Search state
MODIFY: All buttons are enabled except the Execute button, as in Fig. 8.
Fig. 8. Modify state
CUSTOM: You can decide which buttons to enable/disable, for example, you can enable all buttons, as in Fig. 9.
Fig. 9. Custom state
The tree view can be expanded or reduced, as in Fig. 10.
Fig. 10. Expanding the tree menu
Brief of Each Project
There are four projects:
Fig. 11. Projects
Using the Code
Before we start to look into the sample, there are a few methods I need to address first.
App.xaml.cs
To classify four states, Initial
, Search
, Modify
, and Custom
.
public enum WorkState
{
INT, SEA, MOD, CUS,}
Get or set the tree view menu control:
public static System.Collections.ObjectModel.Collection<MenuDataContext>
MenuList { get; set; }
Get or set the current Form ID:
public static string CurrentFormID
{
get;
set;
}
In Application_Startup
, add login control in the RootVisual
. It will put login control in the start up.
private void Application_Startup(object sender, StartupEventArgs e)
{
this.RootVisual = rootVisual;
rootVisual.Children.Add(lg);
}
I am using Reflection to create an instance of the user control. For example, the MainPage
has the namespace CommandInMasterDemo
and the class name MainPage
, therefore I can use Reflection to create the CommandInMasterDemo.MainPage
object and then convert it into the UserControl
type. If the user control is not null, I set the CurrentFormID
to strName
then add the user control into the current Application.RootVisual
.
public static void Navigate(string strName)
{
App CurrentApp = (App)Application.Current;
Type type = CurrentApp.GetType();
Assembly assembly = type.Assembly;
UserControl uc = (UserControl)assembly.CreateInstance(
type.Namespace + "." + strName);
if (uc != null)
{
CurrentFormID = strName;
CurrentApp.rootVisual.Children.Clear();
CurrentApp.rootVisual.Children.Add((UserControl)assembly.CreateInstance(
type.Namespace + "." + strName));
}
}
GetUserControl
also uses Reflection to create a user control instance. The only difference between Navigate
and GetUserControl
is preparing the namespace. All sub pages have their own sub group folder, such as CHM, FCM. Therefore we need to add a sub group name in the namespace. For example, the FCM201
user control has the namespace CommandInMasterDemo.FCM
and the class name FCM
201, therefore we use type.Namespace + "." + strName.Substring(0, 3) + "." + strName
to create its instance.
public static UserControl GetUserControl(string strName)
{
CurrentFormID = strName;
App CurrentApp = (App)Application.Current;
Type type = CurrentApp.GetType();
Assembly assembly = type.Assembly;
return (UserControl)assembly.CreateInstance(type.Namespace + "." +
strName.Substring(0, 3) + "." + strName);
}
LoginControl.xaml.cs
The login button will trigger proxy_GetUserInfoCompleted
, then proxy_GetUserInfoCompleted
will trigger proxy_GetFunctionMenuCompleted
.
private void btnLogin_Click(object sender, RoutedEventArgs e)
{
if (string.IsNullOrEmpty(txtAccount.Text) ||
string.IsNullOrEmpty(txtPassword.Password))
{
txtErrorInformation.Text = "Account and Password must enter";
return;
}
else
{
this.Cursor = Cursors.Wait;
try
{
DaoWcfClient daoWcf = new DaoWcfClient();
daoWcf.GetUserInfoCompleted +=
new EventHandler<GetUserInfoCompletedEventArgs>(proxy_GetUserInfoCompleted);
daoWcf.GetUserInfoAsync(txtAccount.Text, txtPassword.Password);
}
catch (Exception ex)
{
MessageBox.Show("btnLogin_Click Exception: " + ex.Message);
}
finally
{
this.Cursor = Cursors.Arrow;
}
}
}
void proxy_GetUserInfoCompleted(object sender, GetUserInfoCompletedEventArgs e)
{
try
{
strCurrentUser = e.Result;
if (string.IsNullOrEmpty(strCurrentUser))
{
txtErrorInformation.Text = "Account or Password is incorrect";
}
else
{
Resources.Remove("CurrentUser");
Resources.Add("CurrentUser", txtAccount.Text);
Resources.Remove("CurrentDatabase");
Resources.Add("CurrentDatabase", cbDb.SelectionBoxItem.ToString());
DaoWcfClient daoWcf = new DaoWcfClient();
daoWcf.GetFunctionMenuCompleted += new
EventHandler<GetFunctionMenuCompletedEventArgs>(
proxy_GetFunctionMenuCompleted);
daoWcf.GetFunctionMenuAsync(txtAccount.Text);
}
}
catch (Exception ex)
{
MessageBox.Show("proxy_FindUserInfoCompleted Exception: " + ex.Message);
}
}
void proxy_GetFunctionMenuCompleted(object sender, GetFunctionMenuCompletedEventArgs e)
{
System.Collections.ObjectModel.Collection<MenuDataContext> list = e.Result;
if (list.Count > 0)
{
App CurrentApp = (App)Application.Current;
App.MenuList = list;
App.Navigate("MainPage");
}
}
Let us have look into proxy_GetUserInfoCompleted
; I am storing the User ID and system name by using Resources
.
Resources.Remove("CurrentUser");
Resources.Add("CurrentUser", txtAccount.Text);
Resources.Remove("CurrentDatabase");
Resources.Add("CurrentDatabase", cbDb.SelectionBoxItem.ToString());
In proxy_GetFunctionMenuCompleted
, after I get the menu list, I store the menu list to the App
MenuList
property, then use App.Navigate
to go to MainPage
.
System.Collections.ObjectModel.Collection<MenuDataContext> list = e.Result;
if (list.Count > 0)
{
App CurrentApp = (App)Application.Current;
App.MenuList = list;
App.Navigate("MainPage");
}
I created a delegate MenuEventHandler(object sender, RouteEventArgs e)
for all the command buttons.
public delegate void MenuEventHandler(object sender, RoutedEventArgs e);
Each command button has its own event:
public event MenuEventHandler SearchClick;
public event MenuEventHandler ExecuteClick;
public event MenuEventHandler EditClick;
public event MenuEventHandler DeleteClick;
public event MenuEventHandler SaveClick;
public event MenuEventHandler LastClick;
public event MenuEventHandler FirstClick;
public event MenuEventHandler PreviousClick;
public event MenuEventHandler NextClick;
public event MenuEventHandler ExcelClick;
There are three properties:
CurrentState
: Get/set the current states
public WorkState CurrentState
{
get
{
return curretState;
}
set
{
curretState = value;
SetButtonState();
}
}
BindGrid
: Get/set the DataGrid
control, so it can interact with the First, Previous, Next, and Last buttons.
public DataGrid BindGrid { get; set; }
TotalRowCount
: Get/set the total data record count
public int TotalRowCount { get; set; }
SetButtonState
is to hide/show the command buttons in the different states.
private void SetButtonState()
{
switch (CurrentState)
{
case WorkState.INT:
txtStatus.Text = "Initial";
btnSearch.IsEnabled = true;
imgbtnSearchOn.Visibility = Visibility.Visible;
...............
break;
case WorkState.SEA:
txtStatus.Text = "Search";
btnSearch.IsEnabled = true;
imgbtnSearchOn.Visibility = Visibility.Visible;
...............
break;
case WorkState.MOD:
txtStatus.Text = "Modify";
btnSearch.IsEnabled = true;
imgbtnSearchOn.Visibility = Visibility.Visible;
...............
break;
case WorkState.CUS:
txtStatus.Text = "Custom";
break;
default:
txtStatus.Text = "Search";
btnSearch.IsEnabled = true;
imgbtnSearchOn.Visibility = Visibility.Visible;
...............
break;
}
}
There are two ways to trigger the four record movement buttons (First, Previous, Next, and Last). One is to interact with the DataGrid
control, the other is to trigger it in the sub page.
private void btnLast_Click(object sender, RoutedEventArgs e)
{
if (BindGrid != null)
{
BindGrid.SelectedIndex = TotalRowCount - 1;
}
else
{
LastClick(this, e);
}
}
private void btnNext_Click(object sender, RoutedEventArgs e)
{
if (BindGrid != null)
{
if (BindGrid.SelectedIndex != TotalRowCount - 1)
{
BindGrid.SelectedIndex = BindGrid.SelectedIndex + 1;
}
}
else
{
NextClick(this, e);
}
}
private void btnPrevious_Click(object sender, RoutedEventArgs e)
{
if (BindGrid != null)
{
if (BindGrid.SelectedIndex != 0)
{
BindGrid.SelectedIndex = BindGrid.SelectedIndex - 1;
}
}
else
{
PreviousClick(this, e);
}
}
private void btnFirst_Click(object sender, RoutedEventArgs e)
{
if (BindGrid != null)
{
BindGrid.SelectedIndex = 0;
}
else
{
FirstClick(this, e);
}
}
CommonUtility.cs
In order to get the TopToolBar
control, I need to find the MainPage
control first. That’s because the MainPage
contains TopToolBar
.
public MainPage GetMainPage(UserControl currentPage, bool blSub)
{
MainPage mainPage = blSub ? (MainPage)((Grid)((Grid)((Grid)
currentPage.Parent).Parent).Parent).Parent : (MainPage)(
(Grid)((Grid)currentPage.Parent).Parent).Parent;
return mainPage;
}
After I find the MainPage
control, I can use the FindName
method to get the TopToolBar
control.
public GetTopToolBar(UserControl currentPage, bool blSub)
{
ttb = GetMainPage(currentPage, blSub).FindName("topToolBar") as ;
return ttb;
}
ExportExcel.ashx.cs
Silverlight doesn’t offer the ability to save a file on local disk, therefore I use a handler to create a CSV/Excel file.
public void ProcessRequest(HttpContext context)
{
string strContext = context.Request.QueryString["Context"] != null ?
HttpUtility.UrlDecode(context.Request.QueryString["Context"]) :
DateTime.Now.ToString("yyyyMMdd_HHmmss");
string[] strSplit =
strContext.Replace("[", "").Replace("]", "").Split(char.Parse(";"));
string strFileName = strSplit[0];
string strQueryCase = strSplit[1];
DataGrid dg = new DataGrid();
switch (strQueryCase)
{
case "FindMTAccntScopeByYear":
List<AccountDataContext> list = new ().GetAccountByYear(strSplit[2]);
dg.DataSource = list;
break;
case "FindAllyCompAccountByOwnerId":
List<AllyCompAcctDataContext> listAlly =
new DaoWcf().GetAllyCompAccountByOwnerId(strSplit[2]);
dg.DataSource = listAlly;
break;
}
dg.DataBind();
context.Response.Buffer = true;
context.Response.ClearContent();
context.Response.ClearHeaders();
context.Response.ContentType = "application/vnd.ms-excel";
context.Response.AddHeader("content-disposition",
"attachment;filename=" + strFileName + ".xls");
dg.HeaderStyle.ForeColor = Color.Blue;
dg.HeaderStyle.BackColor = Color.White;
dg.ItemStyle.BackColor = Color.White;
System.IO.StringWriter tw = new StringWriter();
System.Web.UI.HtmlTextWriter hw = new HtmlTextWriter(tw);
dg.RenderControl(hw);
context.Response.Write(tw.ToString());
context.Response.Flush();
context.Response.Close();
context.Response.End();
}
Demonstration
There are three samples I am going to go through:
FCM201 HARDWARE
This sample shows how to use a button to trigger different states.
private void Button_Click(object sender, RoutedEventArgs e)
{
Button b = (Button)sender;
switch (b.Tag.ToString())
{
case "INT":
WorkState.INT;
break;
case "SEA":
WorkState.SEA;
break;
case "MOD":
WorkState.MOD;
break;
case "CUS":
WorkState.CUS;
true;
topToolBar.ExecuteEnable = true;
topToolBar.EditEnable = true;
topToolBar.DeleteEnable = true;
topToolBar.SaveEnable = true;
topToolBar.RecordMoveEnable = true;
topToolBar.ExcelEnable = true;
break;
}
}
Initial button: To trigger the Initial state, as in Fig. 12.
Fig. 12. Initial state
Search button: To trigger the Search state.
Fig 13. Search state
Modify button: To trigger the Modify state.
Fig 14. Modify State
Custom button to trigger the Custom state:
Fig. 15. Custom state
FCM202 SOFTWARE
This is the default flow in a general case. The flow goes like: Initial State --> Search State --> Modify State.
In the initial state, there is only the Search button that is enabled.
Fig. 16. Initial state
After clicking the Search button, the state will change to Search, and the Execute button will become visible.
Fig. 17. Search state
After clicking the Execute button, all the command buttons become visible except the Execute button. Now you should see the data displayed in the content page.
Fig. 18. Modify state
On clicking the Delete button, it will come up with a warning message. The deletion is not functional in this sample.
Fig. 19. Deletion button
On clicking the Edit button, it will change the editable fields to allow modifications. The date type file will display a calendar control. The multi-selection field will display a combo box.
Fig. 20. Edit button
After you modify the data, you can click the Save button to update the server data.
Fig. 21. Save button
In FCM202, I trigger the record navigation button in the sub page control.
voidobject sender, RoutedEventArgs e)
{
iCurrent = 0;
SetCountStatus(iCurrent);
}
voidobject sender, RoutedEventArgs e)
{
iCurrent = list.Count - 1;
SetCountStatus(iCurrent);
}
voidobject sender, RoutedEventArgs e)
{
if (iCurrent != list.Count - 1)
{
iCurrent = iCurrent + 1;
SetCountStatus(iCurrent);
}
}
voidobject sender, RoutedEventArgs e)
{
if (iCurrent != 0)
{
iCurrent = iCurrent - 1;
SetCountStatus(iCurrent);
}
}
Fig. 22. Record navigation button
We pass the information to the handler to generate the Excel file.
voidobject sender, RoutedEventArgs e)
{
string strOwnerId = txtOwnerId.Text;
string strEncodeUrl = System.Windows.Browser.HttpUtility.UrlEncode(
"[AllyCompAcct;FindAllyCompAccountByOwnerId;" + strOwnerId + "]");
string strUri = "http://localhost/CommandInMasterDaoWcf/ExportExcel.ashx?Context=" +
strEncodeUrl;
HtmlPage.Window.Navigate(new Uri(strUri, UriKind.Absolute));
}
Fig. 23. Export data to Excel
FCM203 LOCAL
This is a customized flow. The flow goes like: Initial State --> Search State --> Custom State. In order to active custom state, you need to set topToolBar.CurrentState = WorkState.CUS
.
Fig. 24. Custom state
In FCM203, I trigger the record navigation button in the TopToolBar
by setting the DataGrid
to TopToolBar
’s BindGrid
property.
topToolBar.BindGrid = this.dgAccountYear;
topToolBar.TotalRowCount = list.Count;
Fig. 25. Record navigation button
Moving Forward
I am placing this code into the public domain without restrictions. It doesn't have the best pattern in design or coding style. You can use it for any purpose, including in commercial products. If you can improve the code or even make it more clear, please let me know. I will update the code to make it more useful. Thank you all.