65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (18投票s)

2010年8月31日

Ms-PL

6分钟阅读

viewsIcon

66892

downloadIcon

2566

该模块允许您轻松地从 DotNetNuke 门户中的用户那里收集和处理费用报告。

现场演示: 链接 (需要注册和登录)

注意: 在安装模块之前,请确保您已安装并运行 LinqPrep。有关安装 DotNetNuke 模块的说明,请参阅 此链接 中的示例。

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 辅助类

我写了以下博客来解释 DataGrid 上的删除按钮是如何工作的。

验证

我写了以下 **CodeProject** 文章来解释服务器端验证是如何工作的。

文件上传

文件上传的代码包含在这篇 **CodeProject** 文章中。

属性、集合和 ICommands(可能还有行为和值转换器)

视图模型/MVVM 模式有许多变体,但在开始时,有时限制自己只使用 **属性**、**集合** 和 **ICommands** 会有所帮助。您可能还需要使用 **行为** 和 **值转换器**。我发现即使在我最复杂的应用程序中,也只需要这些。

© . All rights reserved.