使用 View Model (MVVM) 的 Silverlight 费用报表模块






4.82/5 (18投票s)
该模块允许您轻松地从 DotNetNuke 门户中的用户那里收集和处理费用报告。
现场演示: 链接 (需要注册和登录)
注意: 在安装模块之前,请确保您已安装并运行 LinqPrep。有关安装 DotNetNuke 模块的说明,请参阅 此链接 中的示例。
Silverlight 视图模型系列
- Silverlight 视图模型样式:一种(过于)简化的解释
- RIATasks: 一个简单的 Silverlight CRUD 示例
- Silverlight RIA 任务 2:
动态视图模型 - 中心 Silverlight 业务规则验证
一个 Silverlight 费用报告模块
该模块允许您轻松地从您的 **DotNetNuke** 门户中的用户那里收集和处理 **费用报告**。
让我们先看一下应用程序
当门户用户访问该模块时,他们将为他们的每个 **费用报告** 输入一个名称和描述。
他们将在底部输入报告的详细信息。
因为这是一个 Silverlight 应用程序,他们可以附加收据扫描件,收据可以是任何大小。
他们只需单击 **插入** 即可添加明细项。
- 明细项显示在一个可排序的网格中。
- 可以通过单击每行旁边的“**X**”来删除项目(将显示确认框)。
- 可以通过单击回形针图标来查看扫描件。
- 摘要和总计显示在右侧。
**费用报告** 然后可以 **打印**。
**费用报告** 是使用一个模板打印的,该模板可以使用 **Microsoft Expression Blend** 轻松修改。
默认模板提供一行用于在报告上签名和注明日期。
对于某些组织来说,方便用户扫描已签名的 **费用报告**。
当管理员登录时,他们会看到一个下拉列表,显示已提交 **费用报告** 的用户。他们可以选择用户以查看其所有报告。
管理员通常在处理报告时 **锁定** 该报告。
原始用户仍然可以看到报告,但除非管理员解锁,否则他们无法进行任何更改。
管理员在审查所有扫描件收据后通常会将其标记为 **已批准**。这可以让会计人员知道何时应该报销用户。
在会计人员报销用户后,**费用报告** 将被标记为 **已完成**。
使用 Silverlight 的优点
- **速度更快** - 在查看单个用户的费用报告时,没有回发。该应用程序的运行速度比普通 Web 应用程序快得多。
- **不会超时** - 普通 Web 应用程序要求您每 20 分钟输入一次内容,否则您将超时,并丢失任何未保存的信息。
- **大文件上传** - 用户可以上传任意大小的扫描件。
- **无需代码即可重新设计** - 这允许设计师使用 **Microsoft Expression Blend** 完全重新设计此应用程序,而无需更改代码。只需打开源文件进行更改。编译更改后,它将生成一个“ExpenseReports.xap”文件。只需替换“DesktopModules\ExpenseReports\ClientBin”目录中的文件即可!
视图模型 / MVVM
此应用程序使用 **视图模型**(**MVVM**)结构设计。
最大的好处是,它允许 **开发人员** 创建一个没有用户界面 (UI) 的应用程序。然后 **设计师** 可以使用 **Microsoft Expression Blend 4+** 创建整个 UI,而无需编写任何代码。
如果您是 **视图模型样式** 的新手,建议您阅读 Silverlight 视图模型样式:一种(过于)简化的解释 以获得介绍。
上面概述了 **数据库** 和 **Web 服务**。
上面概述了 **视图模型** 和 **模型** 的类。
上面显示了 **网站** 和 **Silverlight** 项目文件。
应用程序启动过程演练
为了让您了解应用程序的流程,我们将看看应用程序是如何启动的。
<object data="data:application/x-silverlight-2," type="application/x-silverlight-2" id="silverlightControl"> <param name="source" value="<%=SilverlightSourceParams %>" /> <param name="InitParams" value="<%=SilverlightInitParams %>" /> <param name="onError" value="onSilverlightError" /> <param name="background" value="white" /> <param name="minRuntimeVersion" value="4.0.41108.0" /> <param name="autoUpgrade" value="true" /> <a href="http://go.microsoft.com/fwlink/?LinkID=149156&v=4.0.41108.0" style="text-decoration: none"> <img src="http://go.microsoft.com/fwlink/?LinkId=161376" alt="Get Microsoft Silverlight" style="border-style: none" /> </a> </object>
首先加载的是 Silverlight 控件代码。标记被编码为允许在运行时设置 **SilverlightSourceParams** 和 **SilverlightInitParams**。
SilverlightSourceParams = this.TemplateSourceDirectory + "/ClientBin/ExpenseReports.xap"; SilverlightInitParams = string.Format("UserId={0},RIAKey={1},PortalId={2},IPAddress={3},IsAdmin={4}," + "AuthUserId={5},CurrentUser={6}", UserIdToAdminister.ToString(), strRIAKey, PortalId.ToString(), this.Request.UserHostAddress, boolIsAdmin.ToString(), UserId.ToString(), UserToAdminister);
使用上面的代码设置值。为当前用户创建一个 *RIAKey* 并将其放入 **ExpenseReports_RIAUser** 表中。此 *RIAKey* 将用于 Silverlight 应用程序的后续所有请求。
Silverlight 应用程序中的 **App.xaml.cs** 文件接收所有参数,并将它们存储在应用程序的 **Resources** 中,键为“**RIAAuthenticationHeader**”。
private void Application_Startup(object sender, StartupEventArgs e) { // Create RIAAuthenticationHeader RIAAuthenticationHeader RIAAH = new RIAAuthenticationHeader(); RIAAH.UserID = Convert.ToInt32(e.InitParams["UserId"]); RIAAH.Password = Convert.ToString(e.InitParams["RIAKey"]); RIAAH.PortalID = Convert.ToInt32(e.InitParams["PortalId"]); RIAAH.IPAddress = Convert.ToString(e.InitParams["IPAddress"]); RIAAH.IsAdmin = Convert.ToBoolean(e.InitParams["IsAdmin"]); RIAAH.AuthUserID = Convert.ToInt32(e.InitParams["AuthUserId"]); RIAAH.Username = Convert.ToString(e.InitParams["CurrentUser"]); // Add RIAAuthenticationHeader to Application Resources Application.Current.Resources.Add("RIAAuthenticationHeader", RIAAH); this.RootVisual = new MainPage(); }
接下来,**MainPage.xaml** 文件(**视图**)包含指定 **MainPageModel** 类是其 **视图模型** 的代码:
<UserControl.DataContext> <local:MainPageModel/> </UserControl.DataContext>
**MainPageModel** 类在其构造函数中调用 **GetReportsFromModel**。
#region GetReportsFromModel private void GetReportsFromModel() { // Clear the Report Collection colReports.Clear(); // Call the Model to get the Reports Model.GetReports((sender, EventArgs) => { if (EventArgs.Error == null) { // loop thru each item foreach (var Report in EventArgs.Result) { // Add to the Reports collection colReports.Add(Report); } // If we have any Reports... if (colReports.Count > 0) { //set the selected item value to the first one SelectedReportIndex = 0; // Set the Current Report GetReportFromModel(EventArgs.Result[0].ID); } else { // There are no Reports SetToNewReport(); } #region Process Any Errors // Clear any Errors colErrors.Clear(); // Show any errors foreach (var Report in EventArgs.Result) { if (Report.Errors.Count > 0) { foreach (var item in Report.Errors) { colErrors.Add(item); } } } // Set the visibility of the Message ListBox ErrorsVisibility = (colErrors.Count > 0) ? Visibility.Visible : Visibility.Collapsed; #endregion } }); } #endregion
它调用 **模型** 中的 **GetReports** 方法,处理结果,并填充 **colReports** 集合,该集合显示在 **视图** 中页面顶部的下拉列表中。
**GetReports** 方法,像模型中的所有方法一样,非常简单。请注意,它调用 **GetAuth** 方法,该方法获取 **RIAAuthenticationHeader** 类的内容,并将其参数传递给 web 服务。
#region GetReports public static void GetReports(EventHandler<GetReportsCompletedEventArgs> eh) { // Set up web service call WebServiceSoapClient WS = new WebServiceSoapClient(); // Set the EndpointAddress WS.Endpoint.Address = new EndpointAddress(GetBaseAddress()); WS.GetReportsCompleted += eh; WS.GetReportsAsync(GetAuth()); } #endregion
它调用一个稍微复杂一些的 web 方法,该方法之所以复杂,主要是因为它必须执行安全检查。
#region GetReports [WebMethod] public List<ExpenseReports_Report> GetReports(RIAAuthenticationHeader Auth) { // Make a empty collection so that something will always be returned List<ExpenseReports_Report> colExpenseReports_Report = new List<ExpenseReports_Report>(); // See if the user is authorized RIAAuthentication RIAAuth = new RIAAuthentication(Auth); if (RIAAuth.IsUserValid()) { try { ExpenseReportsDBDataContext db = new ExpenseReportsDBDataContext(); // Unless a User is an Administrator, set the UserID to AuthUserID // to prevent a non Administrator from getting somone elses Reports if (!RIAAuth.IsAdmin()) { Auth.UserID = Auth.AuthUserID; } // Get The Reports var reports = from ExpenseReports_Reports in db.ExpenseReports_Reports where ExpenseReports_Reports.UserID == Auth.UserID orderby ExpenseReports_Reports.InsertDate descending select ExpenseReports_Reports; foreach (var item in reports) { // Set the ExpenseReports_Details to null // To prevent it from being returned (because it is not needed) // Also to prevent the web service call from failing when trying // To return a complex type item.ExpenseReports_Details = null; colExpenseReports_Report.Add(item); } } catch (System.Exception ex) { // If there are errors, add them to the response ExpenseReports_Report ER = new ExpenseReports_Report(); ER.ID = -1; ER.Errors.Add(ex.Message); colExpenseReports_Report.Add(ER); } } // Return the response return colExpenseReports_Report; } #endregion
**视图模型** 中的 **colReports** 集合(**视图模型**)绑定到 **视图** 中的下拉列表,并在 web 服务返回其响应时自动填充。
代码其他部分的指针
过去几周,我写了许多博客和教程,涵盖了所使用的许多技术和代码。
用于消除代码隐藏的行为
我们希望消除代码隐藏(并将所有代码放在 **视图模型** 和支持类中)的唯一原因是我们设计的可能不是程序员。如果存在任何代码隐藏,并且设计人员意外删除了 UI(**视图**)中的内容,他们将需要程序员来重新组合!
通常在我的项目中,在初始版本之后,我不会触碰任何带有 **.xaml** 扩展名的文件,而设计人员也不会触碰任何带有 **.cs** 扩展名的文件。如果我不使用任何代码隐藏,这种情况就可以实现。
正如 Ian Lackey 曾经告诉我的那样,99% 的时候,当你认为你需要代码隐藏时,你就可以使用行为。本项目使用了以下行为:
- 在 DataGrid 中使用“Hisowa Simple PopUp Behavior”
http://openlightgroup.net/Blog/tabid/58/EntryId/116/Using-the-ldquo-Hisowa-Simple-PopUp-Behavior-rdquo-in-a-DataGrid.aspx - Silverlight Open File Dialog Behavior (MVVM)
http://openlightgroup.net/Blog/tabid/58/EntryId/119/Open-File-Dialog-Using-ldquo-View-Model-Style-rdquo-it-rsquo-s-like-MVVM.aspx
DataGrid 辅助类
我写了以下博客来解释 DataGrid 上的删除按钮是如何工作的。
- 使用按钮删除 Silverlight DataGrid 中的一行
http://openlightgroup.net/Blog/tabid/58/EntryId/115/Deleting-A-Silverlight-DataGrid-Row-With-A-Button-On-The-Row.aspx
验证
我写了以下 **CodeProject** 文章来解释服务器端验证是如何工作的。
文件上传
文件上传的代码包含在这篇 **CodeProject** 文章中。
属性、集合和 ICommands(可能还有行为和值转换器)
视图模型/MVVM 模式有许多变体,但在开始时,有时限制自己只使用 **属性**、**集合** 和 **ICommands** 会有所帮助。您可能还需要使用 **行为** 和 **值转换器**。我发现即使在我最复杂的应用程序中,也只需要这些。