Introduction
There are several posts about how to implement the master page feature in Silverlight. So the question is do we really need this master page feature in Silverlight. If there is an advantage to use 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 Requirement
Design Requirement
Just like traditional web pages, the login page is in start up as Fig 1. The login control consists of 2 text boxes, 1 combo box and 2 button controls. The text boxes collect 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 Fig 2.
Fig 2. MainPage
The MainPage has two main sections: master page section and sub page section as Fig 3. The master page section has command buttons bar across the top and the 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 content area
-
11 command buttons: to do action in sub page
Fig 4. Details of master page and sub page
11 command buttons include the following:
-
Search: to trigger Search state
-
Execute: to extract server data back to client and also trigger Modify state
-
Edit: to make editable field control enable
-
Delete: to delete current record
-
Save: to save update change
-
First Record: go to first record
-
Previous Record: go to previous record
-
Next Record: go to next record
-
Last Record: go to last record
-
Excel: to export data to excel
-
Exit: to exit and close browser
Fig 5. 11 command buttons description
INITIAL : Search button is enabled as Fig 6.
Fig 6. Initial State
SEARCH: Search and Execute is enabled as Fig 7.
Fig 7. Search State
MODIFY: All buttons are enabled except the execute button as Fig 8.
Fig 8. Modify State
CUSTOM: You can decide which buttons to enable/disable, for example, you can enable all buttons as Fig 9.
Fig 9. Custom State
Tree view can be expanded or reduced as Fig 10.
Fig 10. Expanding tree menu
Brief of Each Project
There are four projects:
-
DataObjectCollection : Data structures that are used by the service to communicate with client
-
CommandInMasterDaoWcf: The service to pass data from server side to the client.
-
CommandInMasterDemo : The Silverlight Application project.
There are 3 main Silverlight control need to address:
-
LeftTreeViewMain - Tree view menu
-
Login Control – Login page
-
TopToolBar - Control contains 11 command buttons
Fig 11. Projects
Using the Code
Before we start to look into sample, there are few methods I need to address first.
App.xaml.cs
To classify 4 states, Initial, Search, Modify and Custom
public enum WorkState { INT,//Initial State SEA,//Search State MOD,//Modify State CUS,//Custom State }
|
Get or Set tree view menu control
public static System.Collections.ObjectModel.Collection<MenuDataContext> MenuList { get; set; }
|
Get or Set Current Form ID
public static string CurrentFormID { get; set;}
|
In the Application_Startup, add login control in the RootVisual. It will make 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 user control. For example, the MainPage has namespace CommandInMasterDemo and class name MainPage, therefore I can use reflection to create the CommandInMasterDemo.MainPage object then convert it into UserControl type. If the user control is not null, I set the CurrentFormID to strName then add user control into 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)); } }
|
The 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 its own sub group folder , such as CHM, FCM. Therefore we need to add a sub group name in the namespace. For example FCM201 user control has the namespace CommandInMasterDemo.FCM and the class name FCM201, 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); }
|
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"; return; } 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"); } }
|
Lets have look into proxy_GetUserInfoCompleted, I am storing 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 the proxy_GetFunctionMenuCompleted, after I got menu list I store the menu list to the App MenuList property, then use App.Navigate to go MainPage
System.Collections.ObjectModel.Collection<MenuDataContext> list = e.Result; if(list.Count > 0) { App CurrentApp = (App)Application.Current; App.MenuList = list; App.Navigate("MainPage"); }
|
TopToolBar.xaml.cs
I created a delegate MenuEventHandler(object sender, RouteEventArgs e) for all 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 3 properties:
CurrentState : Get/Set current states
public WorkState CurrentState { get { return curretState; } set { curretState = value; SetButtonState(); } }
|
BindGrid : Get/Set DataGrid control, so it can interact with First, Previous, Next and Last buttons.
public DataGrid BindGrid { get; set; }
|
TotalRowCount : Get/Set total data record count
public int TotalRowCount { get; set;}
|
The SetButtonState is to hide/show command buttons in different states.
private void SetButtonState() { switch (CurrentState) { case WorkState.INT: txtStatus.Text = "Initial"; //Search
btnSearch.IsEnabled = true; imgbtnSearchOn.Visibility = Visibility.Visible; ............... break;
case WorkState.SEA:
txtStatus.Text = "Search";
//Search
btnSearch.IsEnabled = true; imgbtnSearchOn.Visibility = Visibility.Visible;
............... break;
case WorkState.MOD:
txtStatus.Text = "Modify";
//Search
btnSearch.IsEnabled = true; imgbtnSearchOn.Visibility = Visibility.Visible;
............... break; case
WorkState.CUS: txtStatus.Text = "Custom"; break; default: txtStatus.Text = "Search"; //Search btnSearch.IsEnabled = true; imgbtnSearchOn.Visibility = Visibility.Visible; ............... break;
}
}
|
There are two ways to trigger 4 record movement buttons (First, Previous, Next and Last). One is 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 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 found the MainPage control, I can use FindName method to get TopToolBar control.
public TopToolBar GetTopToolBar(UserControl currentPage, bool blSub) { TopToolBar ttb = GetMainPage(currentPage, blSub).FindName("topToolBar") as TopToolBar; return ttb; }
|
ExportExcel.ashx.cs
The Silverlight doesn't offer ability to save a file on local disk, therefore I use handler to create 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 3 samples I am going to go through:
FCM201 HARDWARE
This sample is showing how to use button to trigger different state.
private void Button_Click(object sender, RoutedEventArgs e) { Button b = (Button)sender; switch (b.Tag.ToString()) { case "INT": topToolBar.CurrentState = WorkState.INT; break; case "SEA": topToolBar.CurrentState = WorkState.SEA; break; case "MOD": topToolBar.CurrentState = WorkState.MOD; break; case "CUS": topToolBar.CurrentState = WorkState.CUS; topToolBar.SearchEnable = true; topToolBar.ExecuteEnable = true; topToolBar.EditEnable = true; topToolBar.DeleteEnable = true; topToolBar.SaveEnable = true; topToolBar.RecordMoveEnable = true; topToolBar.ExcelEnable = true; break; } }
|
Initial button: to trigger Initial state as Fig 12
Fig 12. Initial State
Search button: to trigger Search state
Fig 13. Search State
Modify button: to trigger Modify State
Fig 14. Modify Stat
Custom button to trigger Custom state
Fig 15. Custom State
FCM202 SOFTWARE
This is the default flow in a general case. The flow goes Initial State -->Search State --> Modify State.
In the initial state, there is only Search button is enable.
Fig 16. Initial State
After click Search button, the state will change to Search and Execute button will become visible.
Fig 17. Search State
After clicking the Execute button, all command buttons became visible except Execute button. Now you should see the data displayed in the content page.
Fig 18. Modify State
Clicking the Delete Button, it will come up warning message. The deletion is not functional in this sample.
Fig 19. Delete Button
Clicking 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 data, you can click Save button to update server data.
Fig 21. Save Button
In FCM202, I trigger record navigation button in the sub page control.
void topToolBar_FirstClick(object sender, RoutedEventArgs e)
{ iCurrent = 0; SetCountStatus(iCurrent); }
void topToolBar_LastClick(object sender, RoutedEventArgs e) { iCurrent = list.Count - 1; SetCountStatus(iCurrent); }
void topToolBar_NextClick(object sender, RoutedEventArgs e) { if (iCurrent != list.Count - 1) { iCurrent = iCurrent + 1; SetCountStatus(iCurrent);
} }
void topToolBar_PreviousClick(object sender, RoutedEventArgs e) { if (iCurrent != 0) { iCurrent = iCurrent - 1; SetCountStatus(iCurrent); } }
|
Fig 22. Record Navigation Button
Pass information to handler to generate excel file
void topToolBar_ExcelClick(object 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 customize flow. The flow goes Initial State --> Search State --> Custom State.
In order to active custom state, you need to set topToolBar.CurrentState = WorkState.CUS
topToolBar.CurrentState = WorkState.CUS; topToolBar.SearchEnable = true; topToolBar.ExecuteEnable = false; topToolBar.EditEnable = false; topToolBar.DeleteEnable = false; topToolBar.SaveEnable = false; topToolBar.RecordMoveEnable = true; topToolBar.ExcelEnable = true; topToolBar.ExitEnable = true; txtAccountYear.IsEnabled = false;
|
Fig 24. Custom State
In FCM203, I trigger record navigation button in the TopToolBar by set 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 restriction. It doesn't have the best pattern design or coding style. Anyone 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.