在 Silverlight 中实现主从页面
本文将演示如何在 Silverlight 中构建传统的母版页式应用程序。
引言
有几篇关于如何在 Silverlight 中实现母版页功能的文章。问题是我们真的需要 Silverlight 中的母版页功能吗?如果 ASP.NET 的母版页功能有优势,那么我看不出 Silverlight 不能利用这一点的理由。本文将演示如何在 Silverlight 中构建传统的母版页式应用程序。
系统要求
- 此应用程序要求用户拥有 Visual Studio 2008。
- Microsoft® Silverlight™ 3 Tools for Visual Studio 2008 SP1
设计要求
与传统的网页一样,登录页面是启动页面,如图 1 所示。登录控件包括两个文本框、一个组合框和两个按钮控件。文本框用于收集用户的用户名和密码信息,组合框用于确定登录哪个环境。取消按钮将清除文本框中的数据,登录按钮用于提交信息进行身份验证。
点击登录按钮后,将显示 MainPage,如图 2 所示。
MainPage 分为两个主要部分:主页部分和子页面部分,如图 3 所示。主页部分顶部有一个命令按钮栏,左侧有一个树状视图菜单。子页面部分右侧有一个内容区域。
主页包括
- 窗体标题标签:显示子窗体 ID
- 用户 ID 标签:显示当前用户
- 系统标签:显示系统环境名称
- 日期标签:显示当前日期
- 计数标签:显示数据记录数
- 状态标签:显示当前状态
- 树状视图:动态更改内容区域中的内容
- 11 个命令按钮:在子页面中执行操作
这 11 个命令按钮包括以下内容:
- 搜索:触发搜索状态
- 执行:将服务器数据提取回客户端,并触发修改状态
- 编辑:启用可编辑字段控件
- 删除:删除当前记录
- 保存:更新更改
- 第一条记录:转到第一条记录
- 上一条记录:转到上一条记录
- 下一条记录:转到下一条记录
- 最后一条记录:转到最后一条记录
- Excel:将数据导出到 Excel
- 退出:退出并关闭浏览器
有四种状态:
初始:启用搜索按钮,如图 6 所示。
搜索:启用搜索和执行按钮,如图 7 所示。
修改:除执行按钮外,所有按钮都启用,如图 8 所示。
自定义:您可以决定启用/禁用哪些按钮,例如,可以启用所有按钮,如图 9 所示。
树状视图可以展开或折叠,如图 10 所示。
项目简介
有四个项目:
- DataObjectCollection:服务用于与客户端通信的数据结构。
- CommandInMasterDaoWcf:用于将数据从服务器端传递到客户端的服务。
- CommandInMasterDemo:Silverlight 应用程序项目。
- LeftTreeViewMain - 树状视图菜单
- Login 控件 – 登录页面
- TopToolBar - 包含 11 个命令按钮的控件
- CommandInMasterDemo.Web:创建 Silverlight 应用程序时会自动创建。它将托管 Silverlight 控件在浏览器中运行。
有三个主要的 Silverlight 控件需要处理:
Using the Code
在开始查看示例之前,有几个方法我需要先介绍一下。
App.xaml.cs
将四种状态分类:Initial
、Search
、Modify
和 Custom
。
public enum WorkState
{
INT,//Initial State
SEA,//Search State
MOD,//Modify State
CUS,//Custom State
}
获取或设置树状视图菜单控件
public static System.Collections.ObjectModel.Collection<MenuDataContext>
MenuList { get; set; }
获取或设置当前窗体 ID
public static string CurrentFormID
{
get;
set;
}
在 Application_Startup
中,将登录控件添加到 RootVisual
。这将把登录控件放在启动位置。
private void Application_Startup(object sender, StartupEventArgs e)
{
this.RootVisual = rootVisual;
rootVisual.Children.Add(lg);
}
我使用反射来创建用户控件的实例。例如,MainPage
的命名空间是 CommandInMasterDemo
,类名是 MainPage
,因此我可以使用反射来创建 CommandInMasterDemo.MainPage
对象,然后将其转换为 UserControl
类型。如果用户控件不为空,则将 CurrentFormID
设置为 strName
,然后将用户控件添加到当前的 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
也使用反射来创建用户控件实例。Navigate
和 GetUserControl
之间的唯一区别是准备命名空间。所有子页面都有自己的子组文件夹,例如 CHM、FCM。因此,我们需要在命名空间中添加子组名称。例如,FCM201
用户控件的命名空间是 CommandInMasterDemo.FCM
,类名是 FCM
201,因此我们使用 type.Namespace + "." + strName.Substring(0, 3) + "." + strName
来创建其实例。
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
登录按钮将触发 proxy_GetUserInfoCompleted
,然后 proxy_GetUserInfoCompleted
将触发 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");
}
}
我们来看看 proxy_GetUserInfoCompleted
;我使用 Resources
来存储用户 ID 和系统名称。
Resources.Remove("CurrentUser");
Resources.Add("CurrentUser", txtAccount.Text);
Resources.Remove("CurrentDatabase");
Resources.Add("CurrentDatabase", cbDb.SelectionBoxItem.ToString());
在 proxy_GetFunctionMenuCompleted
中,在我获取菜单列表后,我将菜单列表存储到 App
的 MenuList
属性中,然后使用 App.Navigate
转到 MainPage
。
System.Collections.ObjectModel.Collection<MenuDataContext> list = e.Result;
if (list.Count > 0)
{
App CurrentApp = (App)Application.Current;
App.MenuList = list;
App.Navigate("MainPage");
}
我为所有命令按钮创建了一个委托 MenuEventHandler(object sender, RouteEventArgs e)
。
public delegate void MenuEventHandler(object sender, RoutedEventArgs e);
每个命令按钮都有自己的事件。
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;
有三个属性:
CurrentState
:获取/设置当前状态。
public WorkState CurrentState
{
get
{
return curretState;
}
set
{
curretState = value;
SetButtonState();
}
}
BindGrid
:获取/设置 DataGrid
控件,以便它可以与第一条、上一条、下一条和最后一条按钮进行交互。public DataGrid BindGrid { get; set; }
TotalRowCount
:获取/设置总数据记录数。public int TotalRowCount { get; set; }
SetButtonState
用于在不同状态下隐藏/显示命令按钮。
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;
}
}
有两种方式可以触发四个记录移动按钮(第一条、上一条、下一条和最后一条)。一种是与 DataGrid
控件交互,另一种是在子页面中触发它。
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
为了获取 TopToolBar
控件,我首先需要找到 MainPage
控件。这是因为 MainPage
包含 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;
}
在我找到 MainPage
控件后,就可以使用 FindName
方法获取 TopToolBar
控件。
public GetTopToolBar(UserControl currentPage, bool blSub)
{
ttb = GetMainPage(currentPage, blSub).FindName("topToolBar") as ;
return ttb;
}
ExportExcel.ashx.cs
Silverlight 不提供在本地磁盘上保存文件的能力,因此我使用一个处理程序来创建 CSV/Excel 文件。
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();
}
演示
我将介绍三个示例:
FCM201 硬件
此示例演示如何使用按钮触发不同的状态。
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;
}
}
初始按钮:触发初始状态,如图 12 所示。
搜索按钮:触发搜索状态。
修改按钮:触发修改状态。
自定义按钮触发自定义状态。
FCM202 软件
这是通用情况下的默认流程。流程为:初始状态 --> 搜索状态 --> 修改状态。
在初始状态下,只有搜索按钮可用。
点击搜索按钮后,状态将变为搜索,执行按钮将显示出来。
点击执行按钮后,除执行按钮外,所有命令按钮都将显示出来。现在您应该在内容页面中看到显示的数据。
点击删除按钮时,会弹出警告消息。删除在此示例中不起作用。
点击编辑按钮时,可编辑字段将变为可修改。日期类型字段将显示一个日历控件。多选字段将显示一个组合框。
修改数据后,您可以点击保存按钮来更新服务器数据。
在 FCM202 中,我在子页面控件中触发了记录导航按钮。
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);
}
}
我们将信息传递给处理程序来生成 Excel 文件。
voidobject sender, RoutedEventArgs e)
{
string strOwnerId = txtOwnerId.Text;
string strEncodeUrl = System.Windows.Browser.HttpUtility.UrlEncode(
"[AllyCompAcct;FindAllyCompAccountByOwnerId;" + strOwnerId + "]");
string strUri = "https:///CommandInMasterDaoWcf/ExportExcel.ashx?Context=" +
strEncodeUrl;
HtmlPage.Window.Navigate(new Uri(strUri, UriKind.Absolute));
}
FCM203 本地
这是一个自定义流程。流程为:初始状态 --> 搜索状态 --> 自定义状态。要激活自定义状态,您需要设置 topToolBar.CurrentState = WorkState.CUS
。
在 FCM203 中,我通过将 DataGrid
设置为 TopToolBar
的 BindGrid
属性来触发 TopToolBar
中的记录导航按钮。
topToolBar.BindGrid = this.dgAccountYear;
topToolBar.TotalRowCount = list.Count;
展望未来
我将此代码放入公共领域,不设任何限制。它没有最好的设计模式或编码风格。您可以将其用于任何目的,包括商业产品。如果您可以改进代码,甚至使其更清晰,请告诉我。我将更新代码以使其更有用。谢谢大家。