Implement Master Pages in Silverlight

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.


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.
Login Page

Fig 1. Login Page


After clicking the login button, the MainPage will display as Fig 2.
Main Page

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.

Master Page section and Sub page section

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

Details of master page and 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

11 command buttons description
Fig 5. 11 command buttons description

INITIAL : Search button is enabled as Fig 6.

Initial State

Fig 6. Initial State


SEARCH: Search and Execute is enabled as Fig 7.
Search State
Fig 7. Search State

MODIFY: All buttons are enabled except the execute button as Fig 8.

Modify State

Fig 8. Modify State


CUSTOM: You can decide which buttons to enable/disable, for example, you can enable all buttons as Fig 9.


Custom State

Fig 9. Custom State



Tree view can be expanded or reduced as Fig 10.

Expanding tree menu

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
  • CommandInMasterDemo.Web: This is created as you create thr Silverlight application. It will host the Silverlight controls to run in the browser.

Projects
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

Initial State

Fig 12. Initial State



Search button: to trigger Search state

Search State

Fig 13. Search State



Modify button: to trigger Modify State

Modify State

Fig 14. Modify Stat


Custom button to trigger Custom state

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.

Initial State

Fig 16. Initial State



After click Search button, the state will change to Search and Execute button will become visible.

Search State

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.

Modify State

Fig 18. Modify State


Clicking the Delete Button, it will come up warning message. The deletion is not functional in this sample.

Delete Button

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.

Edit Button

Fig 20. Edit Button


After you modify data, you can click Save button to update server data.

Save Button

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);

}

}


Record Navigation Button

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));

}

Excel Button
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;


Custom State

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;


Record Navigation Button

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.