使用 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** 会有所帮助。您可能还需要使用 **行为** 和 **值转换器**。我发现即使在我最复杂的应用程序中,也只需要这些。


