Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Implement Master Pages in Silverlight

0.00/5 (No votes)
19 Aug 2009 1  
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. 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.

Login Page

Fig. 1. Login page

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

Main Page

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.

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

Button Description

Fig. 5. The 11 command buttons description

There are four kinds of states:

INITIAL: Search button is enabled, as in Fig. 6.

Initial State

Fig. 6. Initial state

SEARCH: Search and Execute is enabled, as in Fig. 7.

Search State

Fig. 7. Search state

MODIFY: All buttons are enabled except the Execute button, as in 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 in Fig. 9.

Custom State

Fig. 9. Custom state

The tree view can be expanded or reduced, as in Fig. 10.

Treeview Expand

Fig. 10. Expanding the tree menu

Brief of Each Project

There are four projects:

  • DataObjectCollection: Data structures that are used by the service to communicate with the client.
  • CommandInMasterDaoWcf: The service to pass data from the server side to the client.
  • CommandInMasterDemo: The Silverlight application project.
  • There are three main Silverlight controls that needs to be addressed:

    • LeftTreeViewMain - Tree view menu
    • Login control – Login page
    • TopToolBar - Control contains 11 command buttons
  • CommandInMasterDemo.Web: This is created as you create the 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 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,//Initial State
    SEA,//Search State
    MOD,//Modify State 
    CUS,//Custom State
}

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

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";
        //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 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.

Initial State

Fig. 16. Initial state

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

Search State

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.

Modify State

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.

Delete Button

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.

Edit Button

Fig. 20. Edit button

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

Save Button

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

Record Navigation Button

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

Excel Button

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;

Record Navigation Button

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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here