Skip to content

9. 案例研究

9.1. 引言

我们将介绍一篇此前发表在 [http://tahe.developpez.com/dotnet/pam-aspnet/] 网址上的文章中的案例研究。 在该文章中,该案例研究采用经典的 ASP.NETNHibernate ORM 实现。在此,我们将使用 ASP.NET MVCEntity Framework ORM 进行实现。与现有文章一样,本案例研究以大学实验室作业的形式呈现,因此主要面向学生。针对所有问题,我们均提供了对前文所述章节的引用,以指明有用的阅读资料。

9.2. :待解决的问题

我们希望编写一个 Web 应用程序,允许用户模拟当地市政“Maison de la petite enfance”协会中儿童保育人员的薪资计算。我们将同样重视应用程序 .NET 代码的架构设计与代码本身。

该应用程序将采用单页应用(SPA)架构,仅通过Ajax调用与服务器进行通信。它将向用户呈现以下视图:

  • [VueSaisies] 视图,用于显示模拟表单

Image

  • [VueSimulation] 视图,用于显示模拟的详细结果:

Image

  • [SimulationsView] 视图,用于列出客户端执行的模拟

Image

  • [VueSimulationsVides] 视图,用于提示客户端没有模拟或已无更多模拟:

Image

  • [VueErreurs] 视图,用于指示一个或多个错误(此处,MySQL 数据库管理系统已关闭):

Image

9.3. 应用程序架构

应用程序架构如下:

[EF5] 层指的是 Entity Framework 5 ORM。将使用的数据库管理系统(DBMS)为 MySQL。

我们将首先构建一个带有模拟[业务]层的应用程序:

这样我们就能完全专注于 [web] 层。模拟的 [business] 层将遵循实际 [business] 层的接口。一旦 [web] 层投入运行,我们将随后构建 [business]、[DAO] 和 [EF5] 层。

9.4. 数据库

构建工资单所需的静态数据存储在名为 [dbpam_ef5] 的 MySQL 数据库中(pam = Paie Assistante Maternelle)。该数据库有一个名为 root 的管理员,且无密码。它包含三个表:

Image

EMPLOYEES表的INDEMNITE_ID列与INDEMNITIES表的ID列之间存在外键关系。该数据库的结构由其与EF5的配合使用所决定。在构建应用程序的底层时,我们将再次探讨这一点。

EMPLOYEES 表:包含各类托儿服务提供者的信息

结构

ID:由数据库管理系统自动递增的主键SSN:员工的社保号 - 唯一字段NAME:员工的姓氏FIRST NAME:名字ADDRESS:地址CITY:所在城市ZIPh:邮政编码VERSIONING:整数字段,每次修改记录时自动递增INDEMNITY_ID:外键,关联[INDEMNITES]表的[ID]字段

Image

其内容可能如下所示:

Image

COTISATIONS 表:包含从工资中扣除的社会保险缴费率

结构

ID主键,由数据库管理系统自动递增CSGRDS百分比:一般社会缴费 + 偿还社会债务的缴费CSGD百分比:可扣除的一般社会缴费SECU百分比:社会保障PENSION百分比:补充养老金 + 失业保险VERSIONING一个整数,每次修改记录时自动递增

Image

其内容可能如下:

Image

社会保险缴费率与员工无关。前表仅有一行。

INDEMNITIES 表:列出基于员工指数的各类津贴

ID主键,由数据库管理系统自动递增INDEX工资索引 - 唯一BASE_HOUR每小时待命工作的净价(欧元)DAILY_MAINTENANCE每日护理津贴(欧元)MEAL_DAY每日餐费津贴(欧元)PAID_LEAVE_ALLOWANCE带薪休假津贴。 这是一个应用于基本工资的百分比。VERSIONING 一个整数,每次修改记录时自动递增

Image

其内容可能如下:

Image

9.5. 保育员薪资计算方法

接下来我们将说明保育员月薪的计算方法。以玛丽·朱维纳尔女士为例,她在该薪资周期内工作了20天,共计150小时。

以下因素将被纳入考量:
[TOTALHOURS]:当月总工作小时数
[TOTALDAYS]:当月总工作日数
[TOTALHOURS]=150
[TOTALDAYS] = 20
保育员的基本工资按以下公式计算:
[BASESALARY] = ([TOTALHOURS] * [HOURLYRATE]) * (1 + [CPALLOWANCE] / 100)
[BASESALARY]=(150*[2.1])*(1+0.15)= 362.25
需从该基本工资中扣除若干社会保险费:
一般社会保险费及偿还社会债务的缴费:[BASESALARY]*[CSGRDS/100]
应扣缴的一般社会保险费:[BASESALARY]*[CSGD/100]
社会保障、遗属及养老福利:[BASESALARY]*[SECU/100]
补充养老金 + AGPF + 失业保险:[基本工资]*[PENSION/100]
CSGRDS:12.64
CSGD:22.28
社会保障:34.02
养老金:28.55
社会保障缴费总额:
[SOCIALCONTRIBUTIONS] = [BASESALARY] * (CSGRDS + CSGD + SECU + RETIREMENT) / 100
[SOCIALCONTRIBUTIONS]=97.48
此外,保育员有权获得每日生活津贴和每日餐费津贴。因此,她可获得以下津贴:
[津贴]=[总工作日数]*(每日生活津贴+每日餐费)
[津贴]=104
最终,支付给保育员的净薪资为 ,计算如下:
[基本工资] - [社会保险缴费] + [津贴]
[净薪]=368.77

9.6. [Web]层的Visual Studio项目

该应用程序的 Visual Web Developer 项目将如下所示:

  • 在 [1] 中,[pam-web-01] 项目的总体结构;
  • 在 [2] 中,[Content] 文件夹用于存储项目的静态资源:
    • [indicator.gif]:显示等待 Ajax 请求完成的动画图像,
    • [standard.jpg]:各视图的背景图片,
    • [Site.css]:应用程序的样式表;
  • 在 [3] 中,应用程序的单个控制器 [PamController];
  • 在 [4] 中,应用程序所需的但不能归类为 MVC 组件的类:
    • [ApplicationModelBinder]:允许将 [Application] 作用域中的数据纳入操作模型的类,
    • [SessionModelBinder]:允许将 [Session] 作用域中的数据纳入操作模型的类,
    • [Static]:包含静态方法的辅助类;
  • 在 [5] 中,应用程序模型,无论是操作模型还是视图模型:
    • [ApplicationModel]:包含 [Application] 作用域数据的模型,
    • [SessionModel]:包含 [Session] 作用域数据的模型,
    • [Simulation]:封装薪资计算模拟元素的类,
    • [IndexModel]:应用程序显示的首个 [Index] 视图的模型;
  • 在 [6] 中,应用程序国际化所需的 JS 脚本;
  • 在 [7] 中,应用程序国际化、客户端验证和 AJAX 功能所需的 JQuery 系列 JS 脚本;
  • 在 [8] 中,[myScripts.js] 是包含我们自有 JS 脚本的文件;
  • 在 [9] 中,应用程序视图:
    • [Index]:首页,
    • [Form]:用于输入员工信息及其工作时长和工作日数的表单,
    • [Simulation]:显示模拟结果的视图,
    • [Simulations]:显示已执行模拟列表的视图,
    • [Errors]:显示错误列表的视图,
    • [初始化失败]:应用程序初始化失败时显示错误消息的视图;
  • 在 [10] 中,应用程序主页面 [_Layout];
  • 在 [11] 中,用于配置应用程序的 [Web.config] 和 [Global.asax] 文件。

9.7. 步骤 1 – 设置模拟的 [业务] 层

从这里开始,我们将描述完成本案例研究所需的步骤。在相关处,我们会提供章节编号,以便您在需要时查阅以完成任务。部分项目元素已放在 [aspnetmvc-support.zip] 文件夹中,该文件夹可在本文档的网站上获取。其中包含 [case-study-support] 文件夹,内含以下内容:

Image

该项目还包含前几章介绍的组件。您只需在本 PDF 文档与 Visual Studio 之间进行复制粘贴,即可轻松获取这些组件。

9.7.1. 完整应用程序的 Visual Studio 解决方案

首先,我们将创建一个 Visual Studio 解决方案,并在其中创建两个项目:

  • 一个用于模拟 [业务] 层的项目;
  • 一个用于 MVC Web 层的项目。

我们将使用两个工具:

  • Visual Studio Express 2012 for Desktop,用于构建 [业务] 层;
  • Visual Studio Express 2012 for Web,用于构建 [Web] 层。

使用 Visual Studio Express for Desktop,我们创建一个 [pam-td] 解决方案:

  • 在 [1] 中,选择 C# 应用程序;
  • 在 [2] 中,选择 [控制台应用程序];
  • 在 [3] 中,为解决方案命名;
  • 在 [4] 中,为该解决方案创建一个目录;
  • 在 [5] 中,为 [业务] 层命名;
  • 在 [6] 中,生成解决方案。

9.7.2. [业务]层的接口

在分层架构中,层与层之间的通信应通过接口进行,这是良好的编程实践:

[业务]层应向[Web]层提供何种接口?这两个层之间可能进行哪些交互?让我们回顾一下将呈现给用户的Web界面:

  1. 表单初次显示时,员工列表应出现在[1]处。简化的列表即可(姓、名、社会安全号)。社会安全号是访问所选员工详细信息(第6至11字段)的必填项。
  2. 字段12至15为各项缴费率。
  3. 第16至19项为员工的津贴
  4. 第20至24项信息是根据用户输入的第1至3项计算得出的薪资组成部分。

由[业务]层提供给[Web]层的[IPamMetier]接口必须满足上述要求。可能的接口形式多种多样。我们建议采用以下方案:


using Pam.Metier.Entites;
namespace Pam.Metier.Service
{
  public interface IPamMetier
  {
    // list of all employee identities 
    Employe[] GetAllIdentitesEmployes();
 
    // ------- salary calculation 
    FeuilleSalaire GetSalaire(string ss, double heuresTravaillées, int joursTravaillés);
  }
}
  • 第 7 行:用于填充下拉列表 [1] 的方法
  • 第 10 行:用于检索第 6 至 24 项信息的方法。这些信息已收集在一个类型为 [PayrollSheet] 的对象中,我们稍后将对此进行说明。

我们将把此接口放置在 [business/department] 文件夹中:

9.7.3. [business]层中的实体

前一个接口使用了两个类,即 [Employee] 和 [PayStub],我们需要定义它们:

  • [Employee] 代表数据库中 [employees] 表的一行;
  • [PayStub] 代表一名员工的工资单。

这些实体将放置在项目内的 [business/entities] 文件夹中:

在最终架构中,[business] 层将操作数据库实体的表示形式:

Image

我们将使用以下类来表示这三个数据库表的行。各字段的含义请参见第 9.4 节。

[Employee] 类

它表示 [employees] 表中的一行。其代码如下:


using System;
 
namespace Pam.Metier.Entites
{
 
  public class Employe
  {
    public string SS { get; set; }
    public string Nom { get; set; }
    public string Prenom { get; set; }
    public string Adresse { get; set; }
    public string Ville { get; set; }
    public string CodePostal { get; set; }
    public Indemnites Indemnites { get; set; }
 
    // signature
    public override string ToString()
    {
      return string.Format("Employé[{0},{1},{2},{3},{4},{5}]", SS, Nom, Prenom, Adresse, Ville, CodePostal);
    }
  }
}

类 [Allowances]

它代表 [indemnites] 表中的一行。其代码如下:


using System;
 
namespace Pam.Metier.Entites
{
  public class Indemnites
  {
    public int Indice { get; set; }
    public double BaseHeure { get; set; }
    public double EntretienJour { get; set; }
    public double RepasJour { get; set; }
    public double IndemnitesCp { get; set; }
    // signature
    public override string ToString()
    {
      return string.Format("Indemnités[{0},{1},{2},{3},{4}]", Indice, BaseHeure, EntretienJour, RepasJour, IndemnitesCp);
    }
  }
}

类 [Contributions]

它表示 [contributions] 表中的一行。其代码如下:


using System;
 
namespace Pam.Metier.Entites
{
 
  public class Cotisations
  {
    public double CsgRds { get; set; }
    public double Csgd { get; set; }
    public double Secu { get; set; }
    public double Retraite { get; set; }
    // signature
    public override string ToString()
    {
      return string.Format("Cotisations[{0},{1},{2},{3}]", CsgRds, Csgd, Secu, Retraite);
    }
  }
}

请注意,这些类中未包含表中的 [ID] 和 [VERSIONING] 列。这些列在使用 EF5 ORM 时非常有用,但在模拟的 [业务] 层中并不需要。

[FeuilleS al] 类封装了先前展示的表单中的第 6 至 24 项信息:


namespace Pam.Metier.Entites
{
  public class FeuilleSalaire
  {
 
    // automatic properties 
    public Employe Employe { get; set; }
    public Cotisations Cotisations { get; set; }
    public ElementsSalaire ElementsSalaire { get; set; }
 
    // ToString 
    public override string ToString()
    {
      return string.Format("[{0},{1},{2}]", Employe, Cotisations, ElementsSalaire);
    }
  }
}
  • 第 7 行:用于计算薪资的员工的第 6 至 11 字段,以及其津贴的第 16 至 19 字段。请注意,[Employee] 对象封装了一个代表其津贴的 [Allowances] 对象;
  • 第 8 行:字段 12 至 15;
  • 第 9 行:第 20 至 24 项信息;
  • 第 12–14 行:[ToString] 方法。

[Elements Salaire] 类封装了表单中的第 20 至 24 项信息:


namespace Pam.Metier.Entites
{
  public class ElementsSalaire
  {
    // automatic properties 
    public double SalaireBase { get; set; }
    public double CotisationsSociales { get; set; }
    public double IndemnitesEntretien { get; set; }
    public double IndemnitesRepas { get; set; }
    public double SalaireNet { get; set; }
 
 
    // ToString 
    public override string ToString()
    {
      return string.Format("[{0} : {1} : {2} : {3} : {4} ]", SalaireBase, CotisationsSociales, IndemnitesEntretien, IndemnitesRepas, SalaireNet);
    }
  }
}
  • 第6–10行:薪资构成,如上文所述的业务规则所解释;
  • 第6行:根据工作小时数计算的员工基本工资;
  • 第7行:从该基本工资中扣除的各项扣款;
  • 第8和第9行:根据员工职级和实际工作日数计入基本工资的津贴;
  • 第 10 行:应支付的净薪资;
  • 第 14–17 行:该类的 [ToString] 方法。

9.7.4. [PamException] 类

我们为应用程序创建了一个特定的异常类型。即以下 [PamException] 类型:


using System;
 
namespace Pam.Metier.Entites
{
  // exceptional class
  public class PamException : Exception
  {
 
    // the error code 
    public int Code { get; set; }
 
    // manufacturers 
    public PamException()
    {
    }
 
    public PamException(int Code)
      : base()
    {
      this.Code = Code;
    }
 
    public PamException(string message, int Code)
      : base(message)
    {
      this.Code = Code;
    }
 
    public PamException(string message, Exception ex, int Code)
      : base(message, ex)
    {
      this.Code = Code;
    }
  }
}
  • 第 6 行:该类继承自 [Exception] 类;
  • 第 10 行:它有一个公共属性 [Code],该属性是一个错误代码;
  • 在我们的应用程序中,我们将使用两种类型的构造函数:
  • 第23–27行中的构造函数,其用法如下所示:
throw new PamException("Problème d'accès aux données",5);
  • (待续)
    • 或者第 29–33 行中的构造函数,它旨在通过将已发生的异常包装在 [PamException] 异常中来传播该异常:
try{
....
}catch (IOException ex){
    // on encapsule l'exception ex    
    throw new PamException("Problème d'accès aux données",ex,10);
}

这种第二种方法的优势在于不会丢失第一个异常可能包含的信息。

9.7.5. [业务]层的实现

[IPamMetier] 接口将由以下 [PamMetier] 类实现:


using System;
using Pam.Metier.Entites;
using System.Collections.Generic;
 
namespace Pam.Metier.Service
{
  public class PamMetier : IPamMetier
  {
    // list of cached employees
    public Employe[] Employes { get; set; }
    // employees indexed by their SS number
    private IDictionary<string, Employe> dicEmployes = new Dictionary<string, Employe>();
 
    // list of employees
    public Employe[] GetAllIdentitesEmployes()
    {
...
      // we return the list of employees
      return Employes;
    }
 
    // salary calculation
    public FeuilleSalaire GetSalaire(string ss, double heuresTravaillées, int joursTravaillés)
    {
...
  }
}
  • 第 7 行:[PamMetier] 类实现了 [IPamMetier] 接口;
  • 第 10 行:[PamMetier] 类将员工列表保存在缓存中;
  • 第 12 行:一个将员工与其社会保险号关联起来的字典;
  • 第 15–20 行:返回员工列表的方法;
  • 第 23–26 行:计算员工薪资的方法。

[GetAllIdentitesEmploye] 方法如下:


// list of employees
    public Employe[] GetAllIdentitesEmployes()
    {
      if (Employes == null)
      {
        // we create a table of three employees
        Employes = new Employe[3];
        Employes[0] = new Employe()
        {
          SS = "254104940426058",
          Nom = "Jouveinal",
          Prenom = "Marie",
          Adresse = "5 rue des oiseaux",
          Ville = "St Corentin",
          CodePostal = "49203",
          Indemnites = new Indemnites() { Indice = 2, BaseHeure = 2.1, EntretienJour = 2.1, RepasJour = 3.1, IndemnitesCp = 15 }
        };
        dicEmployes.Add(Employes[0].SS, Employes[0]);
        Employes[1] = new Employe()
        {
          SS = "260124402111742",
          Nom = "Laverti",
          Prenom = "Justine",
          Adresse = "La brûlerie",
          Ville = "St Marcel",
          CodePostal = "49014",
          Indemnites = new Indemnites() { Indice = 1, BaseHeure = 1.93, EntretienJour = 2, RepasJour = 3, IndemnitesCp = 12 }
        };
        dicEmployes.Add(Employes[1].SS, Employes[1]);
        // a fictitious employee who will not be included in the dictionary
        // to simulate a non-existent employee
        Employes[2] = new Employe()
        {
          SS = "XX",
          Nom = "X",
          Prenom = "X",
          Adresse = "X",
          Ville = "X",
          CodePostal = "X",
          Indemnites = new Indemnites() { Indice = 0, BaseHeure = 0, EntretienJour = 0, RepasJour = 0, IndemnitesCp = 0 }
        };
      }
      // we return the list of employees
      return Employes;
    }
  • 第 4 行:检查员工列表是否尚未创建;
  • 第 7 行:若未创建,则创建一个包含 3 名员工的数组;
  • 第8–17行:第一个员工;
  • 第18行:将其添加到字典中;
  • 第19–28行:第二位员工;
  • 第29行:将其添加到字典中;
  • 第 32–42 行:第三位员工。出于稍后将解释的原因,此员工未被添加到字典中。

[GetSalary] 方法将如下所示:


    // salary calculation
    public FeuilleSalaire GetSalaire(string ss, double heuresTravaillées, int joursTravaillés)
    {
      // we retrieve employee n° SS
      Employe e = dicEmployes.ContainsKey(ss) ? dicEmployes[ss] : null;
      // exists?
      if (e == null)
      {
        throw new PamException(string.Format("L'employé de n° SS [{0}] n'existe pas", ss), 10);
      }
      // a fictitious payslip is returned
      return new FeuilleSalaire()
      {
        Employe = e,
        Cotisations = new Cotisations() { CsgRds = 3.49, Csgd = 6.15, Secu = 9.38, Retraite = 7.88 },
        ElementsSalaire = new ElementsSalaire() { CotisationsSociales = 100, IndemnitesEntretien = 100, IndemnitesRepas = 100, SalaireBase = 100, SalaireNet = 100 }
      };
}
  • 第 2 行:该方法接收我们要计算工资的员工的社会保险号、工作小时数和工作天数;
  • 第 5 行:我们在字典中查找该员工。请注意其中一位员工并不存在;
  • 第 7–10 行:如果未找到该员工,则抛出 [PamException] 异常;
  • 第12–17行:返回一个示例工资单。

9.7.6. [业务]层的控制台测试

当前 [business] 层项目结构如下:

上文中的 [Program] 类将测试 [IPamMetier] 接口的方法。一个基本示例如下:


using Pam.Metier.Entites;
using Pam.Metier.Service;
using System;
 
namespace Pam.Metier.Tests
{
  class Program
  {
    public static void Main()
    {
      // instantiation layer [business]
      IPamMetier pamMetier = new PamMetier();
      // list of employees
      Employe[] employes = pamMetier.GetAllIdentitesEmployes();
      Console.WriteLine("Liste des employés--------------------");
      foreach (Employe e in employes)
      {
        Console.WriteLine(e);
      }
      // payslip calculations 
      Console.WriteLine("Calculs de feuilles de salaire-----------------");
      Console.WriteLine(pamMetier.GetSalaire(employes[0].SS, 30, 5));
      Console.WriteLine(pamMetier.GetSalaire(employes[1].SS, 150, 20));
      try
      {
        Console.WriteLine(pamMetier.GetSalaire(employes[2].SS, 150, 20));
      }
      catch (PamException ex)
      {
        Console.WriteLine(string.Format("PamException : {0}", ex.Message));
      }
    }
  }
}

  • 第 12 行:实例化 [business] 层;
  • 第 14–19 行:测试 [IPamMetier] 接口的 [GetAllEmployeeIDs] 方法;
  • 第 21–31 行:测试 [IPamMetier] 接口的 [GetSalaire] 方法。

运行此控制台程序将产生以下结果:

Liste des employés--------------------
Employé[254104940426058,Jouveinal,Marie,5 rue des oiseaux,St Corentin,49203]
Employé[260124402111742,Laverti,Justine,La brûlerie,St Marcel,49014]
Employé[XX,X,X,X,X,X]
Calculs de feuilles de salaire-----------------
[Employé[254104940426058,Jouveinal,Marie,5 rue des oiseaux,St Corentin,49203],Co
tisations[3,49,6,15,9,38,7,88],[100 : 100 : 100 : 100 : 100]]
[Employé[260124402111742,Laverti,Justine,La brûlerie,St Marcel,49014],Cotisation
s[3,49,6,15,9,38,7,88],[100 : 100 : 100 : 100 : 100]]
PamException : L'employé de n° SS [XX] n'existe pas

请读者将这些结果与已执行的代码联系起来。

为了在即将构建的 Web 项目中使用此项目,我们将它转换为类库:

  • 在 [1] 中,于 [Program.cs] 文件的属性中;
  • 在 [2] 中,我们指定该文件不属于生成的程序集;
  • 在 [3, 4] 中,进入 [pam-metier-simule] 项目的属性,在 [Application] 选项 [3] 下,我们指定 [4] 构建必须生成类库(以 DLL 形式)。
  • 在 [5] 中,我们指定了一个 [Release] 程序集。另一种类型是 [Debug]。该程序集随后包含有助于调试的信息;
  • 在 [6] 中,生成 [pam-metier-simule] 项目;
  • 在 [7] 中,显示解决方案中的所有文件;
  • 在 [8] 中,位于 [bin/Release] 文件夹内,是本项目的 DLL 文件。

9.8. 步骤 2:配置 Web 应用程序

在之前的 Visual Studio 解决方案中,我们将创建 MVC Web 层的项目。

使用 Visual Studio Express for the Web,我们打开之前使用 Visual Studio Express for the Desktop 创建的 [pam-td] 解决方案。

  • 在[1]中,[pam-td]解决方案已加载到Visual Studio Express for the Web中;
  • 在 [2] 中,显示了该解决方案以及我们刚刚创建的模拟 [business] 层项目。

在此新步骤中,我们将创建 Web 应用程序的骨架。

  • 在 [1] 中,我们将一个新项目添加到 [pam-td] 解决方案中;
  • 在[2]中,我们选择了一个ASP.NET MVC 4项目;
  • 命名为 [pam-web-01] [3];
  • 在 [4] 中,选择“基本 ASP.NET MVC”模板;
  • 在 [5] 中,项目已创建;
  • 在 [6] 中,我们将新项目设为解决方案的启动项目,即按下 [Ctrl-F5] 时运行的项目;
  • 在 [7] 中,新项目的名称显示为粗体,表明它是解决方案的启动项目。

现在,使用 Windows 资源管理器,将该项目的 [Content] 文件夹替换为 [étudedecas-support / web / Content] 文件夹。完成此操作后,必须将新文件添加到 [pam-web-01] 项目中。操作步骤如下:

  • 在 [1] 中,刷新解决方案;
  • 在 [2] 中,显示解决方案中的所有文件;
  • 在 [3] 中,会出现一个 [Images] 文件夹;
  • 将其添加到 [4] 中的项目中。

在 [Scripts] 文件夹中,添加客户端验证所需的 JQuery Globalization [1] 脚本。

母版页 [_Layout.cshtml] [2] 将包含以下内容:


<!DOCTYPE html>
<html>
<head>
  <title>@ViewBag.Title</title>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width" />
  <link rel="stylesheet" href="~/Content/Site.css" />
  <script type="text/javascript" src="~/Scripts/jquery-1.8.2.min.js"></script>
  <script type="text/javascript" src="~/Scripts/jquery.validate.min.js"></script>
  <script type="text/javascript" src="~/Scripts/jquery.validate.unobtrusive.min.js"></script>
  <script type="text/javascript" src="~/Scripts/globalize/globalize.js"></script>
  <script type="text/javascript" src="~/Scripts/globalize/cultures/globalize.culture.fr-FR.js"></script>
  <script type="text/javascript" src="~/Scripts/jquery.unobtrusive-ajax.js"></script>
  <script type="text/javascript" src="~/Scripts/myScripts.js"></script>
</head>
<body>
  <table>
    <tbody>
      <tr>
        <td>
          <h2>Simulateur de calcul de paie</h2>
        </td>
        <td style="width: 20px">
          <img id="loading" style="display: none" src="~/Content/images/indicator.gif" />
        </td>
        <td>
          <a id="lnkFaireSimulation" href="javascript:faireSimulation()">| Faire la simulation<br />
          </a>
          <a id="lnkEffacerSimulation" href="javascript:effacerSimulation()">| Effacer la simulation<br />
          </a>
          <a id="lnkVoirSimulations" href="javascript:voirSimulations()">| Voir les simulations<br />
          </a>
          <a id="lnkRetourFormulaire" href="javascript:retourFormulaire()">| Retour au formulaire de simulation<br />
          </a>
          <a id="lnkEnregistrerSimulation" href="javascript:enregistrerSimulation()">| Enregistrer la simulation<br />
          </a>
          <a id="lnkTerminerSession" href="javascript:terminerSession()">| Terminer la session<br />
          </a>
        </td>
    </tbody>
  </table>
  <hr />
  <div id="content">
    @RenderBody()
  </div>
</body>
</html>

注意:第 8 行,请根据您使用的 Visual Studio 版本调整 jQuery 版本。

  • 第 7 行:引用应用程序的样式表;
  • 第 8–10 行:引用客户端验证所需的脚本;
  • 第 11–12 行:引用用于输入带逗号的法语小数所需的脚本;
  • 第 13 行:引用 Ajax 模式所需的脚本;
  • 第 14 行:应用程序专用的脚本;
  • 第 24 行:Ajax 调用的加载图片;
  • 第 26–39 行:六个 JavaScript 链接;
  • 第 43 行:应用程序各种视图将在此处显示的区域;
  • 第 44 行:应用程序各种视图的主体内容。

接下来,我们将修改应用程序的默认路由:

[RouteConfig] 文件将包含以下内容:


using System.Web.Mvc;
using System.Web.Routing;
 
namespace pam_web_01
{
  public class RouteConfig
  {
    public static void RegisterRoutes(RouteCollection routes)
    {
      routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
      routes.MapRoute(
          name: "Default",
          url: "{controller}/{action}",
          defaults: new { controller = "Pam", action = "Index" }
      );
    }
  }
}

  • 第 14 行:URL 将采用 [{controller}/{action}] 的形式;
  • 第 15 行:如果未指定操作,将使用 [Index] 操作。如果未指定控制器,将使用 [Pam] 控制器。

根据此配置,URL [/] 等同于 URL [/Pam/Index]。由于我们的应用程序是一个 API,URL [/] 将是其唯一的 URL。

创建 [Pam] 控制器:

Image

按以下方式修改 [PamController]:


using System.Web.Mvc;
 
namespace Pam.Web.Controllers
{
    public class PamController : Controller
    {
        [HttpGet]
        public ViewResult Index()
        {
            return View();
        }
 
    }
}

  • 第 3 行:我们将控制器放置在 [Pam.Web.Controllers] 命名空间中;
  • 第 7 行:[Index] 操作仅处理 HTTP GET 请求;
  • 第 8 行:我们返回 [ViewResult] 类型而非 [ActionResult] 类型。

现在创建由上述 [Index] 操作调用的 [Index.cshtml] 视图:

按以下方式修改 [Index.cshtml]:


@{
  ViewBag.Title = "Pam";
}
<h2>Formulaire</h2>

按 [Ctrl-F5] 运行应用程序。您应该会看到以下页面:

Image


任务:解释发生了什么。


该应用程序使用了主页面 [_Layout.cshtml] 中引用的样式表:


  <link rel="stylesheet" href="~/Content/Site.css" />

样式表 [/Content/Site.css] 为应用程序的页面定义了背景图像:


body {
  background-image: url("/Content/Images/standard.jpg");
}

9.9. 步骤 3:实现 SPU 模型

我们将编写一个遵循第 7.5 节和第 7.6 节所述 SPU(单页应用程序)模型的应用程序。该单页即应用程序启动时由浏览器加载的页面:

  • 上文中的 [1] 部分是单页的固定部分。我们已经看到,它由母版页 [_Layout.cshtml] 提供;
  • 部分 [2] 是单页的动态部分。它位于母版页 [_Layout.cshtml] 的 [content] ID 区域内:


<!DOCTYPE html>
<html>
<head>
  <title>@ViewBag.Title</title>
  ...
  <script type="text/javascript" src="~/Scripts/myScripts.js"></script>
</head>
<body>
  <table>
...
  </table>
  <hr />
  <div id="content">
    @RenderBody()
  </div>
</body>
</html>

应用程序的各个页面片段将显示在第 13 行 ID 为 [content] 区域中。它们将通过 Ajax 调用显示。执行这些调用的 JavaScript 脚本位于第 6 行引用的文件 [myScripts.js] 中。请创建此文件,我们将需要它:

我们现在遵循第 7.6 节中描述的 APU 模型。如果您已忘记相关内容,请重新阅读该节。接下来我们将设置应用程序显示的各个页面片段。

9.9.1. JavaScript 开发者工具

请记住,Chrome 浏览器提供了一系列工具,用于调试页面的 HTML、CSS 和 JavaScript。这些工具在第 7.2 节中已部分介绍过。在 APU 模型中,浏览器会缓存应用程序首页引用的 JavaScript 脚本。因此,修改脚本时必须记得清除该缓存,否则更改可能无法生效。以下是在 Chrome 中操作的方法:

- 按 [Ctrl-Shift-I] 打开开发者工具

  • 点击开发者控制台右下角的 [1] 图标;
  • 然后勾选选项 [2] 以在开发者模式下禁用缓存。

9.9.2. 使用部分视图显示表单

输入表单是应用程序显示的片段之一。目前,该表单由 [Index.cshtml] 视图显示,这是一个完整视图:


@{
  ViewBag.Title = "Pam";
}
<h2>Formulaire</h2>

此视图由 [Index] 操作显示:


    [HttpGet]
    public ViewResult Index()
    {
      return View();
}

上文第 4 行显示的是 [View],而不是 [PartialView]。我们需要一个用于窗体的部分视图,它将作为页面片段。我们将 [Index.cshtml] 视图修改如下:


@{
  ViewBag.Title = "Pam";
}
@Html.Partial("Formulaire")

第 4 行:表单不再是 [Index.cshtml] 页面的一部分。它现在位于一个部分视图 [Form.cshtml] 中:

[Form.cshtml] 的代码如下所示:


<h2>Formulaire</h2>

进行这些更改后,请确认应用程序启动时仍会显示以下视图:

Image

9.9.3. Ajax 调用 [runSimulation]

我们需要关注用户点击 [运行模拟] 链接时显示的片段:

  • 在[1]中,用户点击[运行模拟]链接;
  • 在[2]中,模拟结果会显示在表单下方。

我们将显示表单的局部视图 [Formulaire.cshtml] 更新如下:


<h2>Formulaire</h2>
<div id="simulation" />

第 3 行:我们创建一个 ID 为 [simulation] 的区域,用于容纳模拟片段。

我们创建以下部分视图 [Simulation.cshtml]:

[Simulation.cshtml] 视图的内容如下:


<hr />
<h2>Simulation</h2>

现在我们需要编写处理 [Run Simulation] 链接点击事件的 JavaScript 代码。我们将遵循第 7.6.5 节中概述的步骤。首先,让我们看看 [_Layout.cshtml] 中该链接的 HTML 代码:


<a id="lnkFaireSimulation" href="javascript:faireSimulation()">| Faire la simulation<br />
</a>

我们可以看到,点击 [运行模拟] 链接将触发 JS 函数 [runSimulation] 的执行。该函数将与其他应用程序所需的 JS 函数一起,编写在 [myScripts.js] 文件中:


// global variables
var loading;
var content;
 
function faireSimulation() {
  // make a manual Ajax call
...
}
 
function effacerSimulation() {
  // delete form entries
...
}
 
function enregistrerSimulation() {
  // make a manual Ajax call
  ...
}
 
function voirSimulations() {
  // make a manual Ajax call
  ...
}
 
function retourFormulaire() {
  // make a manual Ajax call
...
}
 
function terminerSession() {
...
}
 
// document loading
$(document).ready(function () {
  // retrieve the references of the page's various components
  loading = $("#loading");
  content = $("#content");
});

  • 第 35–39 行:应用程序启动时执行的 jQuery 函数;
  • 第 37–38 行:初始化第 2 行和第 3 行中的全局变量。

请注意,ID 为 [loading] 和 [content] 的元素是在母版页 [_Layout.cshtml] 中定义的(如下文第 14 行和第 21 行):


<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
  <table>
    <tbody>
      <tr>
        <td>
          <h2>Simulateur de calcul de paie</h2>
        </td>
        <td style="width: 20px">
          <img id="loading" style="display: none" src="~/Content/images/indicator.gif" />
        </td>
...
        </td>
    </tbody>
  </table>
  <hr />
  <div id="content">
    @RenderBody()
  </div>
</body>
</html>


作业:按照第 7.6.5 节所述的步骤,编写 JS 函数 [faireSimulation]。该函数将向操作 [/Pam/FaireSimulation] 发送一个 POST 类型的 Ajax 请求。 此时不会提交任何数据。操作 [/Pam/FaireSimulation] 将向 JS 函数 [faireSimulation] 返回部分视图 [Simulation.cshtml],该函数随后会将此 HTML 输出放置在表单中 ID 为 [simulation] 区域


在您的应用程序中测试 [Run Simulation] 链接。

9.9.4. Ajax 调用 [enregistrerSimulation]

在 [_Layout.cshtml] 中,[Save Simulation] 链接的定义如下:


<a id="lnkEnregistrerSimulation" href="javascript:enregistrerSimulation()">| Enregistrer la simulation<br />
</a>


任务:按照前面的步骤,编写 JS 函数 [enregistrerSimulation]。该函数将向操作 [/Pam/EnregistrerSimulation] 发送一个 POST 类型的 Ajax 调用。 此时不会提交任何数据。[ /Pam/EnregistrerSimulation ] 操作将向 JS 函数 [enregistrerSimulation] 返回部分视图 [Simulations.cshtml],该函数随后会将此 HTML 流放置在母版页中 ID 为 [content] 区域内。


[Simulations.cshtml]视图内容如下:

其内容如下:


<h2>Simulations</h2>

以下是一个执行示例:

Image

Image

9.9.5. Ajax 调用 [viewSimulations]

链接 [View simulations] 在 [_Layout.cshtml] 中定义如下:


<a id="lnkVoirSimulations" href="javascript:voirSimulations()">| Voir les simulations<br />
</a>


任务:按照之前的步骤,编写 JS 函数 [viewSimulations]。该函数将向操作 [/Pam/ViewSimulations] 发送一个 POST 类型的 Ajax 请求。 目前不会提交任何数据。操作 [/Pam/VoirSimulations] 将向 JS 函数 [voirSimulations] 返回部分视图 [Simulations.cshtml],该函数随后会将此 HTML 输出放置在母版页中 ID 为 [content] 区域内。


视图 [Simulations.cshtml] 即前一题中已使用的视图。

以下是一个执行示例:

9.9.6. Ajax 调用 [returnToForm]

链接 [返回模拟表单] 在 [_Layout.cshtml] 中定义如下:


<a id="lnkRetourFormulaire" href="javascript:retourFormulaire()">| Retour au formulaire de simulation<br />
</a>


任务:按照前面的步骤,编写 JS 函数 [returnForm]。该函数将向操作 [/Pam/Form] 发送一个 POST 类型的 Ajax 请求。此时不会提交任何数据。 [/Pam/Formulaire] 操作将向 JS 函数 [retourFormulaire] 返回部分视图 [Formulaire.cshtml],该函数随后会将此 HTML 流放置在母版页中 ID 为 [content] 区域内。


[Formulaire.cshtml]视图已定义完毕。以下是一个执行示例:

9.9.7. Ajax 调用 [endSession]

[End Session] 链接在 [_Layout.cshtml] 中定义如下:


<a id="lnkTerminerSession" href="javascript:terminerSession()">| Terminer la session<br />
</a>


任务:按照之前的步骤,编写 JS 函数 [terminerSession]。该函数将向操作 [/Pam/TerminerSession] 发送一个 POST 类型的 Ajax 调用。目前不会提交任何数据。 [/Pam/TerminerSession] 操作将向 JS 函数 [terminerSession] 返回部分视图 [Formulaire.cshtml],该函数随后会将此 HTML 流放置在母版页中 ID 为 [content] 区域内。


以下是一个执行示例:

9.9.8. JS 函数 [clearSimulation]

在 [_Layout.cshtml] 中,[Clear Simulation] 链接的定义如下:


<a id="lnkEffacerSimulation" href="javascript:effacerSimulation()">| Effacer la simulation<br />
</a>

JS 函数 [clearSimulation] 的目的是:

  • 若存在 [Simulation] 片段,则将其隐藏;
  • 将表单输入字段重置为应用程序初始加载时的状态(当存在输入字段时——目前尚无任何输入字段)。


任务:编写 JS 函数 [clearSimulation]。此处不涉及 Ajax 调用。该过程在浏览器内部进行,不涉及服务器。


以下是执行示例:

9.9.9. 管理屏幕间的导航

目前,这些链接始终处于显示状态。接下来,我们将通过一个 JavaScript 函数来管理它们的显示。首先,让我们回顾一下 [_Layout.cshtml] 中六个 JavaScript 链接的代码:


<a id="lnkFaireSimulation" href="javascript:faireSimulation()">| Faire la simulation<br />
</a>
<a id="lnkEffacerSimulation" href="javascript:effacerSimulation()">| Effacer la simulation<br />
</a>
<a id="lnkVoirSimulations" href="javascript:voirSimulations()">| Voir les simulations<br />
</a>
<a id="lnkRetourFormulaire" href="javascript:retourFormulaire()">| Retour au formulaire de simulation<br />
</a>
<a id="lnkEnregistrerSimulation" href="javascript:enregistrerSimulation()">| Enregistrer la simulation<br />
</a>
<a id="lnkTerminerSession" href="javascript:terminerSession()">| Terminer la session<br />
</a>

所有链接都带有 [id] 属性,这将使我们能够通过 JavaScript 进行管理。我们将页面加载时执行的 JavaScript 方法修改如下:


// variables globales
var loading;
var content;
var lnkFaireSimulation;
var lnkEffacerSimulation
var lnkEnregistrerSimulation;
var lnkTerminerSession;
var lnkVoirSimulations;
var lnkRetourFormulaire;
var options;
 
...
// au chargement du document
$(document).ready(function () {
  // on récupère les références des différents composants de la page
  loading = $("#loading");
  content = $("#content");
  // les liens du menu
  lnkFaireSimulation = $("#lnkFaireSimulation");
  lnkEffacerSimulation = $("#lnkEffacerSimulation");
  lnkEnregistrerSimulation = $("#lnkEnregistrerSimulation");
  lnkVoirSimulations = $("#lnkVoirSimulations");
  lnkTerminerSession = $("#lnkTerminerSession");
  lnkRetourFormulaire = $("#lnkRetourFormulaire");
  // on les met dans un tableau
  options = [lnkFaireSimulation, lnkEffacerSimulation, lnkEnregistrerSimulation, lnkVoirSimulations, lnkTerminerSession, lnkRetourFormulaire];
  // on cache certains éléments de la page
  loading.hide();
  // on fixe le menu
  setMenu([lnkFaireSimulation, lnkVoirSimulations, lnkTerminerSession]);
});
 

  • 第 19–24 行:获取六个链接的引用。这些引用在第 4–9 行被定义为全局变量;
  • 第 26 行:使用这六个引用初始化 [options] 数组。该数组在第 10 行被定义为全局变量;
  • 第 28 行:隐藏表示 Ajax 调用正在处理中的动画图像;
  • 第 30 行:显示链接 [lnkFaireSimulation, lnkVoirSimulations, lnkTerminerSession]。其余链接将被隐藏。

JS 函数 [setMenu] 如下:


function setMenu(show) {
  // display table links [show]
...
}


任务:编写 JS 函数 [setMenu]。


T 是一个链接数组:

  • T.length 表示链接的数量;
  • T[i] 表示第 i 个链接;
  • T[i].show() 显示第 i 个链接;
  • T[i].hide() 隐藏第 i 个链接。

借助这些新的 JS 函数,启动时显示的页面如下:

Image

请修改 JS 函数 [runSimulation, clearSimulation, saveSimulation, viewSimulations, returnToForm, endSession],以显示以下界面:

Image

Image

Image

Image

Image

Image

Image

Image

Image

Image

Image

Image

既然 APU 模型和导航链接已经就位,我们可以继续编写服务器端的操作和视图。随着步骤的推进,你会发现目前部分可用的 Ajax 链接将不再起作用,因为你将修改发送给客户端的部分视图。随着你构建各种服务器端操作和视图,客户端的 Ajax 链接将恢复正常工作。

9.10. 步骤 4:编写 [Index] 服务器操作

目前,当应用程序启动时,我们会看到以下界面:

Image

我们希望看到的不是这个界面,而是以下界面:

Image

必须由 [Index] 操作来生成此页面。让我们进行一些观察:

  • 该页面显示了一个包含三个输入字段的表单:
    • 正在计算薪资的员工,
    • 工作小时数,
    • 工作天数;
  • 表单通过 [运行模拟] 链接提交;
  • 必须验证[工作小时数]和[工作日数]输入字段的有效性;
  • 员工列表来自我们之前构建的[业务]层。

让我们回顾一下当前 [Index] 操作的代码:


    [HttpGet]
    public ViewResult Index()
    {
      return View();
}

此操作所显示的 [Index.cshtml] 视图中的代码:


@{
  ViewBag.Title = "Pam";
}
@Html.Partial("Formulaire")

以及部分视图 [Formulaire.cshtml]:


<h2>Formulaire</h2>

将在以下三个位置进行修改。

9.10.1. 表单模板

让我们回到 URL 处理流程 [/Pam/Index]:

  • 客户端的 HTTP 请求到达 [1];
  • 在 [2],请求中包含的信息被转换为一个操作模型 [3],该模型作为操作 [4] 的输入;
  • 在 [4],操作基于该模型生成响应。该响应包含两个组成部分:视图 V [6] 及其对应的模型 M [5];
  • 视图 V [6] 将利用其模型 M [5] 为客户端生成 HTTP 响应。

我们关注的操作是 [Index] 操作,其当前实现如下:


    [HttpGet]
    public ViewResult Index()
    {
      return View();
}

[Index] 操作未向 [Index.cshtml] 视图传递任何模型。因此,它将无法显示员工列表。该列表可从 [business] 层获取。为此,[pam-web-01] 项目必须引用 [pam-metier-simule] 项目。我们将现在创建此引用:

  • 在 [1] 中,右键单击 [pam-web-01] 项目中的 [引用],然后选择 [添加引用];
  • 在 [2] 中,选择 [解决方案] 选项,然后在 [3] 中选择 [pam-metier-simule] 项目;
  • 在 [4] 中,[pam-metier-simule] 项目已添加到 [pam-web-01] 项目的引用中。

9.10.2. 应用程序模型

我们在第 4.10 节(第 70 页)中介绍了应用程序模型和会话模型的重要概念。现在我们将运用这些概念。回顾一下,我们在模型中放置了:

  • 一个包含所有用户只读数据的应用程序模型。该模型构成所有用户请求的共享内存;
  • 针对特定用户的可读写会话数据。该模型构成了该用户所有请求的共享内存。

我们将把什么内容纳入应用程序模型?让我们重新审视其架构:

[Web]层持有对[业务]层的引用。该引用可由所有用户共享。因此,我们可以将其置于应用程序模型中。此外,我们假设员工列表不会发生变化。因此,该列表可以被读取一次,然后在所有用户之间共享。因此,我们提出以下应用程序模型:

[ApplicationModel]类的代码可能如下所示:


using Pam.Metier.Entites;
using Pam.Metier.Service;
namespace PamWeb.Models
{
  public class ApplicationModel
  {
    // --- application scope data ---
    public Employe[] Employes { get; set; }
    public IPamMetier PamMetier { get; set; }
  }
}

要在视图中显示下拉列表,请编写类似以下代码:


        <!-- the drop-down list -->
        <tr>
          <td>Liste déroulante</td>
          <td>@Html.DropDownListFor(m => m.DropDownListField,
           new SelectList(@Model.DropDownListFieldItems, "Value", "Label"))
          </td>
</tr>

[DropDownListFor] 方法期望其第二个参数为 SelectListItem[] 类型,而上述 [SelectList] 类型已提供了该参数。我们需要根据员工列表构建这样一个数组。由于员工名单不会发生变化,因此该数组也可以放在应用程序模型中。我们按以下方式对其进行更新:


using Pam.Metier.Entites;
using Pam.Metier.Service;
using System.Web.Mvc;
 
namespace Pam.Web.Models
{
  public class ApplicationModel
  {
    // --- application scope data ---
    public Employe[] Employes { get; set; }
    public IPamMetier PamMetier { get; set; }
    public SelectListItem[] EmployesItems { get; set; }
  }
}

何时应构建此模型?我们在第 4.10 节中对此进行了演示。当 [Global.asax] 文件中的 [Application_Start] 方法被执行时,该模型便会构建:

当前 [Application_Start] 方法如下:


using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
 
namespace pam_web_01
{
  public class MvcApplication : System.Web.HttpApplication
  {
    protected void Application_Start()
    {
      AreaRegistration.RegisterAllAreas();
 
      WebApiConfig.Register(GlobalConfiguration.Configuration);
      FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
      RouteConfig.RegisterRoutes(RouteTable.Routes);
      BundleConfig.RegisterBundles(BundleTable.Bundles);
    }
  }
}

我们将其扩展如下:


using Pam.Metier.Entites;
using Pam.Metier.Service;
using PamWeb.Infrastructure;
using PamWeb.Models;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
 
namespace pam_web_01
{
  public class MvcApplication : System.Web.HttpApplication
  {
    protected void Application_Start()
    {
      // ----------Auto-generated
      AreaRegistration.RegisterAllAreas();
      WebApiConfig.Register(GlobalConfiguration.Configuration);
      FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
      RouteConfig.RegisterRoutes(RouteTable.Routes);
      BundleConfig.RegisterBundles(BundleTable.Bundles);
      // -------------------------------------------------------------------
      // ---------- specific configuration
      // -------------------------------------------------------------------
      // application scope data
      ApplicationModel application = new ApplicationModel();
      Application["data"] = application;
        // instantiation layer [business]
        application.PamMetier = ...
        // employee roster 
        application.Employes = ...
        // employee combo items
        application.EmployesItems = ...
      // model binder for [ApplicationModel]
      ...
    }
  }
}


任务:完成 [Application_Start] 方法的代码。所需内容均在第 4.10 节中。请花时间重新阅读这一篇幅较长但非常重要的章节。


第 33 行实际上由多行代码组成。要创建 [SelectListItem] 类型的对象,可以使用以下方法:


new SelectListItem() { Text = unTexte, Value = uneValeur };

该 [SelectListItem] 将用于生成以下 HTML 标签:<option>

<option value='uneValeur'>unTexte</option>

在下拉列表中。我们将确保:

  • text 由员工的姓和名组成;
  • aValue 是该员工的社会保险号。

在上面的第 35 行,您需要使用第 4.10 节(第 74 页)中描述的 [ApplicationModelBinder] 类:

9.10.3. [Index] 操作的代码

既然我们已经为应用程序定义了模型,就可以按以下方式更新 [Index] 操作的代码:


    [HttpGet]
    public ViewResult Index(ApplicationModel application)
    {
      return View();
}

  • 第 4 行:应用程序模型现在是 [Index] 操作的参数。我们在第 4.10 节中解释了框架如何初始化此参数。

9.10.4. [Index.cshtml] 视图模板

现在,[Index] 操作可以访问存储在应用程序模型中的员工数据。接下来,它必须将这些数据传递给 [Index.cshtml] 视图,以便进行显示。虽然我们可以将 [ApplicationModel] 类型作为视图模型传递给 [Index.cshtml] 视图,但很快我们会发现,该视图还需要 [ApplicationModel] 中未包含的额外信息。 我们将使用以下 [IndexModel] 视图模型:


namespace Pam.Web.Models
{
  public class IndexModel
  {
    // application scope data
    public ApplicationModel Application { get; set; }
  }
}

  • 第 6 行:[IndexModel] 包含应用程序模型。

[Index] 操作变为如下形式:


    [HttpGet]
    public ViewResult Index(ApplicationModel application)
    {
      return View(new IndexModel() { Application = application });
}

  • 第 4 行,使用 [IndexModel] 类型作为模型(该模型通过应用程序模型中的数据进行初始化)来显示默认视图 [Index.cshtml]。

我们知道 [Index.cshtml] 视图必须显示一个表单:

Image

让我们回到请求处理流程:

对于 [GET /Pam/Index] 请求:

  • 操作为 [Index];
  • 该操作的模型是 [ApplicationModel];
  • 视图是 [Index.cshtml];
  • 该视图的模型是 [IndexModel]。

当表单提交时,处理流程将类似于:

  • 该操作负责处理 POST 请求;
  • 该模型会收集已发布的值,在本例中为:
    • 所选员工的社会保障号码;
    • 工作小时数;
    • 工作天数;

我们可以创建一个将这三个值结合起来的操作模型。通常也会复用用于显示表单的模型。这就是我们接下来要做的。[IndexModel] 类的演变如下:


using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
namespace Pam.Web.Models
{
  [Bind(Exclude = "Application")]
  public class IndexModel
  {
    // application scope data
    public ApplicationModel Application { get; set; }
 
    // posted values
    [Display(Name = "Employé")]
    public string SS { get; set; }
    [Display(Name = "Heures travaillées")]
    [UIHint("Decimal")]
    public double HeuresTravaillées { get; set; }
    [Display(Name = "Jours travaillés")]
    public double JoursTravaillés { get; set; }
  }
}

  • 第 13、16、18 行:三个提交的值。请注意,尽管实际期望的是整数,但 [daysWorked] 已被声明为 [double] 类型。引入 [double] 类型是为了便于对该字段进行客户端验证,因为验证 [int] 类型曾导致过问题;
  • 第 12、14、17 行:与该模型关联的视图中 [Html.LabelFor] 方法的标签;
  • 第 15 行:一个注解,用于将 [HoursWorked] 字段显示为小数点后两位;
  • 第 5 行:指定名为 [Application] 的属性不包含在提交的值中。

9.10.5. [Index.cshtml] 和 [Formulaire.cshtml] 视图

[Index.cshtml] 视图由以下 [Index] 操作显示:


    [HttpGet]
    public ViewResult Index(ApplicationModel application)
    {
      return View(new IndexModel() { Application = application });
}

有趣的是,[Index.cshtml] 视图保持不变:


@{
  ViewBag.Title = "Pam";
}
@Html.Partial("Formulaire")

  • 该视图未声明任何模型;
  • 第 4 行:它包含部分视图 [Form.cshtml],同样未向其传递模型。在测试过程中,观察到传递给 [Index.cshtml] 视图的 [IndexModel] 模型被隐式传播到了 [Form.cshtml] 部分视图。后者现在可以采用以下形式:


@model Pam.Web.Models.IndexModel
 
@using (Html.BeginForm("FaireSimulation", "Pam", FormMethod.Post, new { id = "formulaire" }))
{
  <table>
    <thead>
      <tr>
...
      </tr>
    </thead>
    <tbody>
      <tr>
...
      </tr>
      <tr>
...
      </tr>
    </tbody>
  </table>
}
<div id="simulation" />

  • 第 1 行:视图接收一个类型为 [IndexModel] 的模型;
  • 第 3 行:表单;
  • 第 6–10 行:输入表的表头;
  • 第 12–14 行:输入行;
  • 第 15–17 行:任何错误消息。


任务:完成 [Formulaire.cshtml] 视图的代码。使用第 5.7 节中描述的 [DropDownListFor、EditorFor、LabelFor、ValidationMessageFor] 方法。


9.10.6. 测试 [Index] 操作

我们已经编写了 URL 处理链 [/Pam/Index] 的所有元素:

我们使用 [Ctrl-F5] 测试应用程序:

Image

Image

您必须确认下拉列表中已填充了我们在模拟的 [业务] 层中定义的员工列表。

9.11. 步骤 5:实现输入验证

9.11.1. 问题

尽管我们尚未进行任何配置,但客户端验证功能已生效:

Image

Image

由于应用程序 [Web.config] 文件中第 3 行代码的设置,客户端验证默认处于启用状态


  <appSettings>
    ...
    <add key="ClientValidationEnabled" value="true" />
</appSettings>

然而,由于在 [IndexModel] 中,我们将 [JoursTravaillés] 字段声明为 [double] 类型:


    public double JoursTravaillés { get; set; }

因此,我们可以在该字段中输入一个实数:

Image

此外,我们可以在这两个字段中输入任意值:

Image

该表单的 [IndexModel] 当前如下所示:


using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
namespace Pam.Web.Models
{
  [Bind(Exclude = "Application")]
  public class IndexModel
  {
    // application scope data
    public ApplicationModel Application { get; set; }
 
    // posted values
    [Display(Name = "Employé")]
    public string SS { get; set; }
    [Display(Name = "Heures travaillées")]
    [UIHint("Decimal")]
    public double HeuresTravaillées { get; set; }
    [Display(Name = "Jours travaillés")]
    public double JoursTravaillés { get; set; }
  }
}


任务:改进此模型,使其能够:


  • 显示自定义错误消息;
  • 仅接受 [0,400] 范围内的实数作为 [HoursWorked] 字段的值;
  • 仅接受 [DaysWorked] 字段中 [0,31] 范围内的整数值;



您可以参考第 7.6.2 节中的示例。要验证工作日数是否为整数,可以使用正则表达式(参见第 5.9.1 节中的示例)。

以下是一些符合要求的示例:

Image

Image

Image

9.11.2. 以法语格式输入实数

在应用程序的当前版本中,工作小时数必须采用英式格式(带小数点)的十进制数。不接受使用逗号的法国格式:

Image

该问题已在第 6.1 节中识别并予以解决。


任务:按照上述章节中的步骤,进行必要的修改,以便能够使用法语小数格式输入实数。测试您的应用程序。


现在,之前的屏幕变为:

Image

9.11.3. 通过 JavaScript 链接 [运行模拟] 进行表单验证

目前,可以提交无效值,如下所示:

Image

[1] 中的模拟界面出现以及 [2] 中的菜单变化表明,尽管输入的值无效,但点击 [运行模拟] 链接仍提交了表单。该问题已在第 7.6.5 节中识别并得到解决。


任务:按照前述章节所述的步骤,确保当输入的值无效时,无法执行 [运行模拟] 链接的 POST 操作。在测试更改之前,请记得清除浏览器缓存。


请注意,部分视图 [Formulaire.cshtml] 生成了一个 ID 为 [formulaire] 的 HTML 表单(如下第 1 行):


@using (Html.BeginForm("FaireSimulation", "Pam", FormMethod.Post, new { id = "formulaire" }))
{
...
}

可以通过在浏览器中查看表单的源代码来验证这一点:


<div id="content">
 
    <form action="/Pam/FaireSimulation" id="formulaire" method="post">
    ...
    </form>
    <div id="simulation" />
</div>

9.12. 第 6 步:运行模拟

9.12.1. 问题

当我们运行模拟时,希望得到以下结果:

部分视图 [Simulation.cshtml] 现在显示员工的工资单。

9.12.2. 编写 [Simulation.cshtml] 视图

[Simulation.cshtml] 视图的更改如下:


@model Pam.Metier.Entites.FeuilleSalaire
<hr />
<p><span class="info">Informations Employé</span></p>
<table>
  <tbody>
    <tr>
      <td><span class="libellé">Nom</span>
      </td>
      <td><span class="libellé">Prénom</span>
      </td>
      <td><span class="libellé">Adresse</span>
      </td>
    </tr>
    <tr>
      <td>
        <span class="valeur">@Model.Employe.Nom</span>
      </td>
...
    </tr>
    <tr>
      <td><span class="libellé">Ville</span>
      </td>
      <td><span class="libellé">Code Postal</span>
      </td>
      <td><span class="libellé">Indice</span>
      </td>
    </tr>
    <tr>
...
    </tr>
  </tbody>
</table>
<br />
<p><span class="info">Informations Cotisations</span></p>
<table>
...
  </tbody>
</table>
<br />
<p><span class="info">Informations Indemnités</span></p>
<table>
...
</table>
<br />
<p><span class="info">Informations Salaire</span></p>
<table>
...
</table>
<br />
<table>
...
</table>

  • 第 1 行:[Simulation.cshtml] 视图基于第 9.7.3 节中定义的 [PayrollSheet] 类型;
  • 该视图使用了应用程序样式表 [Content / Site.css] 中定义的 [label, info, value] 类:


.libellé {
  background-color: azure;
  margin: 5px;
  padding: 5px;
}
 
.info {
  background-color: antiquewhite;
  margin: 5px;
  padding: 5px;
}
 
.valeur {
  background-color: beige;
  padding: 5px;
  margin: 5px;
}

此外,仍在 [Site.css] 中,我们设置了 ID 为 [simulation] 区域内各个 HTML 表格的行高,特别是显示工资单的位置:


#simulation table tr {
  height: 30px;
}


任务:完成 [Simulation.cshtml] 视图。


为了显示一笔款项的欧元金额,我们将使用 [string.Format] 方法:

string.Format("{0:C2}",somme)

上述语句将 [somme] 显示为货币值 [C](货币),小数点后保留两位 [C2]。

要测试此视图,必须向其提供一份工资单。该工资单必须由 [/Pam/FaireSimulation] 操作提供,该操作是 [Run Simulation] 链接发起的 Ajax 调用的目标。目前,该操作如下所示:


    [HttpGet]
    public ViewResult Index(ApplicationModel application)
    {
      return View(new IndexModel() { Application = application });
    }
 
    // make a simulation
    [HttpPost]
    public PartialViewResult FaireSimulation()
    {
      return PartialView("Simulation");
}

在上面的代码中,[FaireSimulation] 操作未向 [Simulation.cshtml] 视图传递任何模型。它需要向该视图传递一份工资单。我们知道,工资单是由 [business] 层计算的。可以通过我们在第 9.10.2 节中定义的应用程序模型 [ApplicationModel] 访问该 [business] 层:


  public class ApplicationModel
  {
     // --- application scope data ---
    public Employe[] Employes { get; set; }
    public IPamMetier PamMetier { get; set; }
    public SelectListItem[] EmployesItems { get; set; }
}

可以通过上文第 5 行中的属性访问 [business] 层。为了让 [RunSimulation] 操作能够访问 [business] 层,我们将像对待 [Index] 操作那样,向其传递应用程序模型。代码随后演变为如下形式:


    // make a simulation
    [HttpPost]
    public PartialViewResult FaireSimulation(ApplicationModel application)
    {
      return PartialView("Simulation");
}

现在,在该操作方法中,我们可以计算出一份虚拟的工资单。代码演变如下:


// make a simulation
    [HttpPost]
    public PartialViewResult FaireSimulation(ApplicationModel application)
    {
      FeuilleSalaire feuilleSalaire = application.PamMetier.GetSalaire("254104940426058", 150, 20);
      return PartialView("Simulation", feuilleSalaire);
    }

  • 在第 5 行,计算了一个虚构的工资。第一个参数是一个现有的社会保障号码。它在第 9.7.5 节中模拟的 [business] 类中进行了定义。第二个参数是工作小时数,第三个是工作日数;
  • 第 6 行:将此工资单作为模板传递给 [Simulation.cshtml] 视图。

现在我们可以测试 [Simulation.cshtml] 视图了:

Image

我们不输入任何数据并请求模拟。随后得到以下结果:

Image

9.12.3. 实际工资的计算

当前的 [RunSimulation] 操作始终计算出相同的工资单:


    // make a simulation
    [HttpPost]
    public PartialViewResult FaireSimulation(ApplicationModel application)
    {
      FeuilleSalaire feuilleSalaire = application.PamMetier.GetSalaire("254104940426058", 150, 20);
      return PartialView("Simulation", feuilleSalaire);
}

它未考虑已输入的信息:

  • 正在计算薪资的员工;
  • 工作小时数;
  • 工作天数。

输入的值将按以下方式传递给 [RunSimulation] 操作:

  1. 用户点击 [Run Simulation] 链接。这将触发我们已编写的 JS 函数 [runSimulation] 的执行;
  2. 随后,JS 函数 [faireSimulation] 会向我们当前正在开发的服务器操作 [/Pam/FaireSimulation] 发起一次 Ajax 调用。目前,JS 函数 [faireSimulation] 尚未向服务器操作发送任何信息。它需要将用户输入的值发送给服务器操作;
  3. 服务器操作 [/Pam/FaireSimulation] 将从 JS 函数 [faireSimulation] 提交的数据中提取用户输入的值。

让我们从第 2 点开始:JS 函数 [faireSimulation] 必须将用户输入的值提交至服务器操作 [/Pam/FaireSimulation]。


任务:完善 JS 函数 [faireSimulation],使其能够提交用户输入的值。您可以参考第 7.6.5 节中的示例,该示例已解决了此问题。


现在让我们处理上述第 3 点。服务器操作 [/Pam/FaireSimulation] 必须获取由 JS 函数 [faireSimulation] 提交的值。


任务:完善服务器方法 [FaireSimulation],使其能够利用 JS 函数 [faireSimulation] 提交的值来计算薪资。您可再次参考第 7.6.5 节中的示例,该问题已在其中得到解决。目前,我们将假设根据提交的值推导出的模型始终有效。


提示:服务器操作 [FaireSimulation] 的演变过程如下:


// make a simulation
    [HttpPost]
    public PartialViewResult FaireSimulation(ApplicationModel application, FormCollection data)
    {
      // action model creation
      ...
      // we try to retrieve the values posted in this model
      ...
      // salary calculation
      FeuilleSalaire feuilleSalaire = ...
      // the salary sheet is displayed
      return PartialView("Simulation", feuilleSalaire);
    }

以下是一个执行示例:

我们选择 [Justine Laverti]。随后得到以下结果:

我们确实获得了[Justine Laverti]的模拟工资单。此前,唯一被计算的工资单是[Marie Jouveinal]的。因此,系统采用了员工选择项中填写的数值。至于工时和天数,我们无法给出具体说明,因为我们的模拟[业务]层并未将这些因素纳入考量。

9.12.4. 错误处理

让我们来看以下示例:

  • 在 [1] 中,我们选择了一名不存在的员工(参见第 9.7.5 节中对模拟 [business] 层的定义);
  • 在 [2] 中,我们运行模拟;
  • 在下文的[3]中,返回了一个错误页面。

发生了什么?

JS 函数 [doSimulation] 已被执行。其代码如下:


function faireSimulation() {
...
  // make a manual Ajax call
  $.ajax({
    url: '/Pam/FaireSimulation',
...
    beforeSend: function () {
      // wait signal on
      loading.show();
    },
    success: function (data) {
...
    },
    error: function (jqXHR) {
      // error display
      simulation.html(jqXHR.responseText);
      simulation.show();
    },
    complete: function () {
      // wait signal off
      loading.hide();
    }
  });
  // menu
  setMenu([lnkEffacerSimulation, lnkEnregistrerSimulation, lnkTerminerSession, lnkVoirSimulations]);
}

Ajax 调用失败,并执行了第 14–18 行中的函数。显示了服务器返回的错误页面 [jqXHR.responseText]。这非常具体。模拟的 [business] 层抛出了异常,因为提供给它的 SSN 不属于现有员工(参见第 9.7.5 节中模拟的 [business] 层的代码)。我们需要妥善处理这种情况。

我们将创建一个片段视图 [Errors.chtml],每当服务器端检测到错误时,该视图将返回给 JavaScript 客户端:

部分视图 [Errors.chtml] 的代码如下:


@model IEnumerable<string>
 
<hr />
<h2>Les erreurs suivantes se sont produites</h2>
<ul>
  @foreach (string msg in Model)
  {
    <li>@msg</li>
  }
</ul>

  • 第 1 行:视图接收一个错误消息列表作为模型;
  • 第 5–10 行:这些错误消息以 HTML 列表的形式显示;

现在,让我们按以下方式修改服务器操作 [FaireSimulation] 的代码:


    // make a simulation
    [HttpPost]
    public PartialViewResult FaireSimulation(ApplicationModel application, FormCollection data)
    {
    ...
      // salary calculation
      FeuilleSalaire feuilleSalaire = null;
      Exception exception=null;
      try
      {
        // salary calculation
        feuilleSalaire = ...
      }
      catch (Exception ex)
      {
        exception = ex;
      }
      // mistake?
      if (exception == null)
      {
        // the salary sheet is displayed
        return PartialView("Simulation", feuilleSalaire);
      }
      else
      {
        // the error page is displayed
        return PartialView("Erreurs", Static.GetErreursForException(exception));
      }
}

  • 第 9–17 行:薪资计算现在在 try/catch 块内进行;
  • 第 27 行:如果发生错误,将显示部分视图 [Errors.cshtml],并使用静态方法 [Static.GetErrorsForException(exception)] 提供的错误消息列表作为模板。

我们在 [Static] 类中将两个静态实用函数 [1] 归为一组:


using System;
using System.Collections.Generic;
using System.Web.Mvc;
 
namespace PamWeb.Infrastructure
{
  public class Static
  {
    // list of exception error messages
    public static List<string> GetErreursForException(Exception ex)
    {
      List<string> erreurs = new List<string>();
      while (ex != null)
      {
        erreurs.Add(ex.Message);
        ex = ex.InnerException;
      }
      return erreurs;
    }
 
    // list of error messages linked to an invalid model
    public static List<string> GetErreursForModel(ModelStateDictionary état)
    {
      List<string> erreurs = new List<string>();
      if (!état.IsValid)
      {
        foreach (ModelState modelState in état.Values)
        {
          foreach (ModelError error in modelState.Errors)
          {
            erreurs.Add(getErrorMessageFor(error));
          }
        }
      }
      return erreurs;
    }
 
    // the error message linked to an element of the action model
    static private string getErrorMessageFor(ModelError error)
    {
      if (error.ErrorMessage != null && error.ErrorMessage.Trim() != string.Empty)
      {
        return error.ErrorMessage;
      }
      if (error.Exception != null && error.Exception.InnerException == null && error.Exception.Message != string.Empty)
      {
        return error.Exception.Message;
      }
      if (error.Exception != null && error.Exception.InnerException != null && error.Exception.InnerException.Message != string.Empty)
      {
        return error.Exception.InnerException.Message;
      }
      return string.Empty;
    }
 
  }
}

  • 第 10–19 行:静态函数 [GetErrorsForException] 返回异常堆栈中的错误列表;
  • 第 22–36 行:静态函数 [GetErrorsForException] 返回异常堆栈中的错误列表;第 22–36 行:静态函数 [GetErrorsForModel] 返回无效操作模型的错误列表。该函数的代码以及私有方法 [getErrorMessageFor](第 39–54 行)的代码,此前已出现过。

完成上述工作后,我们可以再次测试错误情况:

  • 在 [1] 中,我们选择不存在的员工;
  • 在 [2] 中,我们运行模拟;
  • 在 [3] 中,我们获取新的错误页面。

让我们回到服务器操作 [RunSimulation]:


    // make a simulation
    [HttpPost]
    public PartialViewResult FaireSimulation(ApplicationModel application, FormCollection data)
    {
      // action model creation
      IndexModel modèle = new IndexModel() { Application = application};
      // we try to retrieve the values posted in the model
      TryUpdateModel(modèle, data);
      // salary calculation
...
}

在第 8 行,我们使用 Ajax 调用提交的值更新第 6 行的模型。我们不会对模型进行验证。之所以这样做,是因为我们无法确定提交的值来自何处。有人可能会篡改 POST 请求并向我们发送无效数据。


任务:遵循我们为异常情况制定的模式,修改服务器操作 [FaireSimulation],使其在提交的数据无效时返回错误页面。为此,我们将使用 [Static] 类中的静态方法 [GetErreursForModel]。


如何测试此更改?在第 9.11.3 节中,您已确保 JS 函数 [faireSimulation] 不会在输入值无效时发送 POST 请求。请注释掉执行此操作的代码行,然后执行以下测试:

  • 在 [1] 中,使用无效值运行模拟;
  • 在 [2] 中,我们成功调用了刚刚构建的错误页面,这证明了服务器端的验证器工作正常。

接下来,请记得取消注释 JS 函数 [faireSimulation] 中刚刚注释掉的行。

9.13. 步骤 7:设置用户会话

[薪资计算器] 应用程序允许用户通过 [运行模拟] 链接执行各种薪资模拟,通过 [保存模拟] 链接保存模拟结果,通过 [查看模拟] 链接查看模拟结果,并通过 [删除模拟] 链接删除模拟。 我们知道,在两个连续的用户请求之间,除非通过会话机制(参见第 4.10 节)创建状态,否则不存在状态。在此处很明显,我们必须将会话中存储用户随时间保存的模拟列表。还有其他数据需要存储:当用户执行模拟时,只有当用户通过 [保存模拟] 链接请求时,该模拟才会保存在模拟列表中。 当用户这样做时,我们必须能够检索到上一次请求中计算出的模拟结果。为此,该结果也将存储在会话中。最后,我们将从 1 开始对模拟进行编号。为了正确编号新的模拟,我们必须保留上一次模拟的编号,同样存储在会话中。

在第 4.10 节中,我们曾介绍过会话模型的概念,将其作为操作的输入参数,以便操作能够访问会话。我们将重新探讨这一概念。若您对该概念尚不明确,建议重新阅读相关章节。

我们创建以下 [SessionModel] 类:

其代码如下:


using Pam.Web.Models;
using System.Collections.Generic;
 
namespace Pam.Web.Models
{
  public class SessionModel
  {
    // list of simulations
    public List<Simulation> Simulations { get; set; }
    // n° of next simulation
    public int NumNextSimulation { get; set; }
    // the last simulation
    public Simulation Simulation { get; set; }
 
    // manufacturer
    public SessionModel()
    {
      // empty simulation list
      Simulations = new List<Simulation>();
      // next simulation no
      NumNextSimulation = 1;
    }
  }
}

第 9 行和第 13 行中的 [Simulation] 类将存储有关模拟的信息。我们需要存储哪些内容?[Run Simulation] 链接会计算一份 [Payroll] 类型的工资单。将此内容纳入模拟似乎是理所当然的。此外,我们还需要存储导致生成此工资单的信息:

  • 所选员工。该信息可在 [PayrollSheet.Employee] 字段中找到。因此,无需重复存储;
  • 工作小时数和天数。这些信息未包含在 [Payroll] 类中,因此我们需要将其存储。

最后,每个模拟都由一个编号标识。因此,我们可以从以下 [Simulation] 类开始:


using Pam.Metier.Entites;
 
namespace Pam.Web.Models
{
  public class Simulation
  {
    // simulation no
    public int Num { get; set; }
    // number of hours worked
    public double HeuresTravaillées { get; set; }
    // number of days worked
    public int JoursTravaillés { get; set; }
    // payslip
    public FeuilleSalaire FeuilleSalaire { get; set; }
  }
}

服务器操作 [RunSimulation] 除了计算工资单外,还必须创建一个模拟并将其放入会话中。为此,它将接收会话模型作为参数:


// make a simulation
    [HttpPost]
    public PartialViewResult FaireSimulation(ApplicationModel application, SessionModel session, FormCollection data)
    {
      // action model creation
      IndexModel modèle = new IndexModel() { Application = application };
      // we try to retrieve the values posted in the model
      TryUpdateModel(modèle, data);
      // valid model?
      if (!ModelState.IsValid)
      {
        // the error page is displayed
        return PartialView("Erreurs", Static.GetErreursForModel(ModelState));
      }
      // salary calculation
      FeuilleSalaire feuilleSalaire = null;
      Exception exception = null;
      try
      {
        // salary calculation
        feuilleSalaire = application.PamMetier.GetSalaire(modèle.SS, modèle.HeuresTravaillées, (int)modèle.JoursTravaillés);
      }
      catch (Exception ex)
      {
        exception = ex;
      }
      // mistake?
      if (exception != null)
      {
        // the error page is displayed
        return PartialView("Erreurs", Static.GetErreursForException(exception));
      }
      // create a simulation and place it in the session
      session.Simulation = ...
      // the salary sheet is displayed
      return PartialView("Simulation", feuilleSalaire);
    }

  • 第 3 行:该操作将会话模型作为参数接收;


任务 1:完成操作代码,第 34 行



任务 2:按照第 4.10 节中的步骤,采取必要措施确保该操作的 [SessionModel session] 参数由框架正确初始化。如果不进行任何操作,该参数将指向指针。


9.14. 步骤 8:保存模拟

9.14.1. 问题

执行完一次仿真后,我们可以将其保存:

Image

部分视图 [Simulations.cshtml] 现在显示了用户已执行的模拟列表。请注意,计算出的工资单是虚构的。

9.14.2. 编写服务器操作 [SaveSimulation]

Ajax 链接 [Save Simulation] 会调用服务器操作 [SaveSimulation],其代码此前如下:


    [HttpPost]
    public PartialViewResult EnregistrerSimulation()
    {
      return PartialView("Simulations");
}

其演变过程如下:


    // save a simulation
    [HttpPost]
    public PartialViewResult EnregistrerSimulation(SessionModel session)
    {
      // save the last simulation run in the session's simulation list
      ...
      // increment the number of the next simulation in the session
      ...
      // the list of simulations is displayed
      ...
}

  • 第 1 行:[SaveSimulation] 操作需要访问会话。因此,它将会话模型作为参数。


任务:完成服务器操作 [SaveSimulation]。


9.14.3. 编写部分视图 [Simulations.cshtml]

前面的 [SaveSimulation] 操作会显示部分视图 [Simulations.cshtml],并将其作为用户已执行的模拟列表作为模型。其代码如下:


@model IEnumerable<Simulation>
 
@using Pam.Web.Models
 
@if (Model.Count() == 0)
{
  <h2>Votre liste de simulations est vide</h2>
}
@if (Model.Count() != 0)
{
  <h2>Liste des simulations</h2>
...
}


作业 1:完成部分视图 [Simulations.cshtml] 的代码。使用 HTML 表格来显示模拟结果。可参考第 5.4 节中的示例。


注意:HTML 表格中每个模拟的 [remove] 链接将是一个 JavaScript 链接,格式如下:

<a href="javascript:retirerSimulation(N)">retirer</a>

其中 N 代表模拟编号。


任务 2:通过运行模拟来测试您的应用程序。为此,请重复以下步骤:1) 按 [F5] 重新加载应用程序页面,2) 运行一次模拟,3) 保存模拟。模拟结果将累积在当前会话中,并在 [Simulations.cshtml] 视图中显示。



任务 3:改进部分视图 [Simulations.cshtml],使 HTML 表格中各行的颜色交替显示。


Image

将样式表 [/Content/Site.css] 中定义的 CSS 类 [even] 和 [odd] 交替应用于 HTML 表格的 <tr> 行:


.impair {
  background-color: beige;
}
 
.pair {
  background-color: lightsteelblue;
}

9.15. 步骤 9:返回输入表单

9.15.1. 问题

获取模拟列表后,我们可以返回输入表单,这是我们之前一段时间无法做到的:

Image

Image

9.15.2. 编写服务器操作 [Form]

Ajax链接 [返回模拟表单] 会调用服务器操作 [Form],其代码此前如下:


    [HttpPost]
    public PartialViewResult Formulaire()
    {
      return PartialView("Formulaire");
}

它所显示的 [Form] 部分视图需要一个 [IndexModel](见下文第 1 行):


@model Pam.Web.Models.IndexModel
 
@using (Html.BeginForm("FaireSimulation", "Pam", FormMethod.Post, new { id = "formulaire" }))
{
...
}
<div id="simulation" />

这就是为什么 [返回模拟表单] 链接不再起作用的原因。


任务:编写 [Form] 服务器操作的新版本(只需重写 2 行代码),然后运行测试。


9.15.3. 修改 JavaScript 函数 [returnToForm]

根据之前的修改,我们现在可以返回表单了,但随后出现了一个问题:

  • 在 [1] 中,我们返回输入表单;
  • 在[2]中,我们使用错误的输入数据进行模拟。随后发现客户端验证器不再起作用。此时,由于第9.12.4节中完成的工作,服务器被调用并返回了错误页面。

该问题已在第7.6.7节中识别并得到解决。


任务:按照第 7.6.7 节中的步骤,修正 JavaScript 函数 [returnToForm],然后运行测试以验证客户端验证器是否已恢复正常工作。


9.16. 步骤 10:查看模拟列表

9.16.1. 问题

在操作模拟表单时,您可以查看已执行的模拟列表:

Image

Image

9.16.2. 编写服务器操作 [ViewSimulations]

Ajax 链接 [View Simulations] 会调用服务器操作 [ViewSimulations],其代码此前如下:


    // see simulations
    [HttpPost]
    public PartialViewResult VoirSimulations()
    {
      return PartialView("Simulations");
}

它所显示的 [Simulations] 部分视图期望一个 [IEnumerable<Simulation>] 模型(见下文第 1 行):


@model IEnumerable<Simulation>
 
@using Pam.Web.Models
 
@if (Model.Count() == 0)
{
  <h2>Votre liste de simulations est vide</h2>
}
@if (Model.Count() != 0)
{
  <h2>Liste des simulations</h2>
...
}

这就是为什么 [查看模拟] 链接不再起作用的原因。


任务:编写服务器操作 [ViewSimulations] 的新版本(需重写 2 行代码),然后运行测试。


9.17. 步骤 11:结束会话

9.17.1. 问题

您可以随时通过 [Ajax] [结束会话] 链接结束用户的会话。这将结束当前会话并启动一个新的会话。此外,您将返回表单视图:

  • 在 [1] 中,我们运行了两次模拟,然后结束了会话;
  • 在 [2] 中,我们返回了输入表单。我们希望查看这些模拟;
  • 在 [3] 中,由于会话变更,模拟列表现已清空。

9.17.2. 编写 [EndSession] 服务器操作

Ajax 链接 [End Session] 调用服务器操作 [EndSession],其代码此前如下:


    // end session
    [HttpPost]
    public PartialViewResult TerminerSession()
    {
      return PartialView("Formulaire");
}

该部分视图 [Form] 的显示需要一个 [IndexModel](见下文第 1 行):


@model Pam.Web.Models.IndexModel
 
@using (Html.BeginForm("FaireSimulation", "Pam", FormMethod.Post, new { id = "formulaire" }))
{
...
}
<div id="simulation" />

这就是为什么 [结束会话] 链接不再起作用的原因。


任务:编写服务器操作 [EndSession] 的新版本(需重写 2 行代码),然后运行测试。


注意:要在操作中终止会话,请编写:

Session.Abandon() ;

9.17.3. 修改 JavaScript 函数 [terminerSession]

完成之前的修改后,我们现在可以返回表单,但随后会出现一个异常——即第 9.15.3 节中描述的那个异常。


任务:按照第 9.15.3 节中的步骤,修正 JavaScript 函数 [terminerSession],然后运行测试以验证客户端验证器是否已恢复正常工作。


9.18. 步骤 12:清除模拟

9.18.1. 问题

创建模拟后,可通过 JavaScript 链接 [Clear Simulation] 清除该模拟:

Image

Image

9.18.2. 编写客户端操作 [clearSimulation]

JavaScript 函数 [clearSimulation] 当前包含以下代码:


function effacerSimulation() {
  // delete form entries
  // ...
  // hide the simulation if it exists
  $("#simulation").hide();
  // menu
  setMenu([lnkFaireSimulation, lnkTerminerSession, lnkVoirSimulations]);
}


任务:完成此代码。可参考第 7.6.6 节中的示例


9.19. 步骤 13:删除一个模拟

9.19.1. 问题

在“模拟”页面上,你可以使用 JavaScript 链接 [remove] 删除某些模拟:

Image

Image

9.19.2. 编写客户端操作 [removeSimulation]

[remove] 链接采用以下 HTML 格式:

<a href="javascript:retirerSimulation(N)">retirer</a>

其中 N 代表模拟编号。


任务:按照第 9.9.3 节中的步骤,编写 JS 函数 [removeSimulation]。该函数将向操作 [/Pam/RemoveSimulation] 发送一个 POST 类型的 Ajax 请求。它将以 num=N 的形式提交数据 N。


:JS 函数 [removeSimulation] 与您之前编写的其他向服务器发起 Ajax 调用的 JS 函数类似。唯一的区别在于,这里提交的值并非来自表单。我们知道,提交的值会被组合成一个字符串,格式如下:

param1=val1&param2=val2&....

因此,JS 函数 [removeSimulation] 将采用以下形式:


function retirerSimulation(N) {
  // make a manual Ajax call
  $.ajax({
    url: '/Pam/RetirerSimulation',
...
    data:"num="+N,
...
  });
  // menu
  setMenu([lnkRetourFormulaire, lnkTerminerSession]);
}

  • 第 6 行:jQuery Ajax 调用的 [data] 属性表示发送到服务器的字符串。

9.19.3. 编写服务器操作 [RemoveSimulation]

服务器操作 [RemoveSimulation]:

  • 接收一个名为 [num] 的提交参数,该参数是模拟的编号;
  • 必须从会话中存储的模拟列表中移除该编号的模拟;
  • 随后必须显示新的模拟列表。


任务:编写服务器操作 [RemoveSimulation]。请参阅第 4.1 节,了解如何获取名为 [num] 的提交参数。


9.20. 步骤 14:改进应用程序初始化方法

我们的 Web 应用程序已开发完成。它在使用模拟的 [business] 类时能够正常运行。让我们回顾一下我们所开发的架构:

在进入 [business] 层的实际实现之前,还有几个细节需要处理,这些工作将在应用程序初始化方法中完成:即 [Global.asax] 中的 [Application_Start] 方法:

[Global.asax] 中的 [Application_Start] 方法仅在应用程序启动时执行一次。这里可以利用 [Web.config] 配置文件。目前,我们的 [Application_Start] 方法如下所示:


// application
    protected void Application_Start()
    {
      // ----------Auto-generated
      AreaRegistration.RegisterAllAreas();
      WebApiConfig.Register(GlobalConfiguration.Configuration);
      FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
      RouteConfig.RegisterRoutes(RouteTable.Routes);
      BundleConfig.RegisterBundles(BundleTable.Bundles);
      // -------------------------------------------------------------------
      // ---------- specific configuration
      // -------------------------------------------------------------------
      // application scope data
      ApplicationModel application = new ApplicationModel();
      Application["data"] = application;
      // instantiation layer [business]
      application.PamMetier = new PamMetier();
...
      // model binders
...
}

在第 17 行,使用 new 运算符实例化了业务层。此外,应用程序模型定义如下:


  public class ApplicationModel
  {
     // --- application scope data ---
    public Employe[] Employes { get; set; }
    public IPamMetier PamMetier { get; set; }
    public SelectListItem[] EmployesItems { get; set; }
}

在上文第 5 行中,我们可以看到 [PamMetier] 属性的类型是 [IPamMetier] 接口的类型。这意味着该属性可以由任何实现此接口的对象进行初始化。然而,在 [Application_Start] 的第 17 行中,我们硬编码了一个实现 [IPamMetier] 的类名。 因此,如果要使用一个新的、实现 [IPamMetier] 接口的类来实现 [business] 层,就必须修改这一行代码。这虽然不是什么大问题,但可以避免。我们可以将实现 [IPamMetier] 接口的类定义移至配置文件中。要更改实现,只需修改该配置文件的内容即可,而 .NET 代码无需更改。

在此,我们将使用 [Spring.net] 依赖注入容器。还有其他 .NET 框架也能实现相同功能,甚至可能做得更好、更简单。

项目架构演变如下:

  • 在 [A] 中,[ASP.NET MVC] 层的初始化方法将向 [Spring.net] 请求对模拟 [业务] 层的引用;
  • 在 [B] 中,[Spring.net] 将通过其配置文件确定应实例化哪个类,从而创建模拟的 [业务] 层;
  • 在 [C] 中,[Spring.net] 将模拟的 [业务] 层的引用返回给 [ASP.NET MVC] 层。

请注意,默认情况下,由 [Spring.net] 管理的对象均为单例:每个对象仅有一个实例。因此,如果在本示例的后续代码中再次向 [Spring.net] 请求模拟 [业务] 层的引用,[Spring.net] 只会返回最初创建的对象的引用。

9.20.1. 向 Web 项目添加 [Spring] 引用

我们将使用 [Spring.net]。该框架以 DLL 形式提供,必须将其添加到项目的引用中。具体操作如下:

在 [1] 中,右键单击项目的 [References] 分支,然后选择 [Manage NuGet Packages] 选项。 此操作需要互联网连接。随后按照之前为 JQuery [Globalize] 库所做的步骤进行。搜索关键词 [Spring.core] 并安装该包。安装包含两个 DLL:[Spring.core] [2] 和 [Common.Logging] [3]。在下面的示例中,使用的是 Spring 1.3.2 版本。

注意:如果您没有互联网连接,可以在本案例研究资料的 [lib] 文件夹中找到这些 DLL 文件。

9.20.2. 配置 [web.config]

[IPamMetier] 接口的实现类定义位于 [web.config] 文件中。


<configuration>
  <configSections>
...
    <sectionGroup name="spring">
      <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
      <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
    </sectionGroup>
  </configSections>
  <!-- spring configuration -->
  <spring>
    <context>
      <resource uri="config://spring/objects" />
    </context>
    <objects xmlns="http://www.springframework.net">
      <object id="pammetier" type="Pam.Metier.Service.PamMetier, pam-metier-simule"/>
    </objects>
  </spring>
...
  • 第 2–8 行:在文件中找到 <configSections> 标签,并将第 4–7 行插入其中;
  • 第 4 行:[name="spring"] 属性提供了关于第 10–17 行中 [spring] 部分的信息;
  • 第 5 行:定义位于 [Spring.Core] DLL 中的类 [Spring.Context.Support.DefaultSectionHandler],使其能够处理第 14–16 行中的 [objects] 部分;
  • 第 6 行:将位于 [Spring.Core] DLL 中的 [Spring.Context.Support.ContextHandler] 类定义为能够处理第 11–13 行中的 [context] 部分的类;
  • 第 11–13 行:该部分提供了信息 [<resource uri="config://spring/objects" />],表明 Spring 对象位于配置文件中 [/spring/objects] 部分内,即第 14–16 行;
  • 第 14–16 行:[objects] 标签引入了 Spring 对象;
  • 第 15 行:定义了一个由 [id="pammetier"] 标识的对象,该对象是位于 DLL [pam-metier-simule] 中的类 [Pam.Metier.Service.PamMetier] 的实例。 请务必在此处避免出错。对于 [id] 属性,您可以使用任意名称。您将在 [Global.asax] 中使用此标识符。类 [Pam.Metier.Service.PamMetier] 即为我们的模拟 [业务] 层。您需要返回其定义以查找其完整名称:

namespace Pam.Metier.Service
{
  public class PamMetier : IPamMetier
  {
    ...

对于 [pam-metier-simule] DLL,您需要检查 [pam-metier-simule] C# 项目的属性:

您必须使用 [1] 中指定的名称。

9.20.3. 修改 [Application_Start]

[Application_Start] 方法的修改如下:


using Spring.Context.Support;
 
// application
    protected void Application_Start()
    {
      // ----------Auto-generated
      AreaRegistration.RegisterAllAreas();
      WebApiConfig.Register(GlobalConfiguration.Configuration);
      FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
      RouteConfig.RegisterRoutes(RouteTable.Routes);
      BundleConfig.RegisterBundles(BundleTable.Bundles);
      // -------------------------------------------------------------------
      // ---------- specific configuration
      // -------------------------------------------------------------------
      // application scope data
      ApplicationModel application = new ApplicationModel();
      Application["data"] = application;
      // instantiation layer [business]
      application.PamMetier = ContextRegistry.GetContext().GetObject("pammetier") as IPamMetier;
...
      // model binders
...
}
  • 第 19 行:我们使用 Spring 类 [ContextRegistry],该类能够处理 [web.config] 文件。为此,我们需要从第 1 行导入该命名空间。静态方法 [GetContext] 用于获取 [context] 标签的内容,这些标签指明了 Spring 对象的位置。随后,静态方法 [GetObject] 允许我们根据其 id 属性检索特定的对象。 请注意,实现 [IPamMetier] 接口的类名不再硬编码在代码中,而是位于 [web.config] 文件中。

完成所有这些更改后,请测试您的应用程序。它应该可以正常运行。

9.20.4. 处理应用程序初始化错误

在 [Application_Start] 方法中,我们写道:


application.PamMetier = ContextRegistry.GetContext().GetObject("pammetier") as IPamMetier;

等号右侧的语句可能会失败。这可能有多种原因:

  • 最明显的原因是我们写错了待实例化对象的名称;
  • 另一种可能是 [业务] 层的实例化失败。对于我们模拟的 [业务] 层,这种情况不会发生,但在连接数据库的真实 [业务] 层中则可能出现。例如,数据库管理系统 (DBMS) 可能未运行,或待管理的数据库信息可能有误等……

我们将通过 try/catch 代码块处理所有异常。代码演变如下:


// application
    protected void Application_Start()
    {
      // ----------Auto-generated
...
      // -------------------------------------------------------------------
      // ---------- specific configuration
      // -------------------------------------------------------------------
      // application scope data
      ApplicationModel application = new ApplicationModel();
      Application["data"] = application;
      application.InitException = null;
      try
      {
        // instantiation layer [business]
        application.PamMetier = ContextRegistry.GetContext().GetObject("pammetier") as IPamMetier;
      }
      catch (Exception ex)
      {
        application.InitException = ex;
      }
      //if no error
      if (application.InitException == null)
      {
....
      }
      // model binders
...
    }

  • 在第 12 行,我们在应用程序模型中引入了一个名为 [InitException] 的新属性:

  public class ApplicationModel
  {
     // --- application scope data ---
    public Employe[] Employes { get; set; }
    public IPamMetier PamMetier { get; set; }
    public SelectListItem[] EmployesItems { get; set; }
    public Exception InitException { get; set; }
}
  • 上文第 7 行,应用程序初始化过程中可能发生的异常;
  • [Application_Start] 的第 13–21 行:[业务] 层的实例化现在在 try/catch 块内进行;
  • 第 20 行:捕获该异常;
  • 第 23–26 行:若未发生错误,则执行之前的代码;
  • 第 28 行:无论是否发生错误,都会创建 [ModelBinders]。这一点很重要。我们需要确保应用程序模型 [ApplicationModel] 能被框架正确绑定。

我们知道,当应用程序启动时,会执行 [Index] 服务器操作。目前,其实现如下:


    [HttpGet]
    public ViewResult Index(ApplicationModel application)
    {
      return View(new IndexModel() { Application = application });
}

第 2 行:[Index] 操作接收应用程序模型。因此,它可以判断初始化是否成功,并在初始化出现任何问题时显示错误页面。我们将代码修改如下:


    [HttpGet]
    public ViewResult Index(ApplicationModel application)
    {
      // initialization error?
      if (application.InitException != null)
      {
        // error page without menu
        return View("InitFailed",Static.GetErreursForException(application.InitException));
      }
      // no error
      return View(new IndexModel() { Application = application });
}

第 8 行:如果发生初始化错误,我们将显示 [InitFailed.cshtml] 视图,并使用初始化过程中发生的异常中的错误消息列表作为模型。第 9.12.4 节介绍了 [Static.GetErrorsForException] 方法并对其进行了说明。[InitFailed.cshtml] 视图如下所示:

其代码如下:


@model IEnumerable<string>
@{
  Layout = null;
}
<!DOCTYPE html>
<html>
<head>
  <title>@ViewBag.Title</title>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width" />
  <link rel="stylesheet" href="~/Content/Site.css" />
</head>
<body>
  <table>
    <tbody>
      <tr>
        <td>
          <h2>Simulateur de calcul de paie</h2>
        </td>
    </tbody>
  </table>
  <hr />
  <h2>Les erreurs suivantes se sont produites à l'initialisation de l'application : </h2>
  <ul>
    @foreach (string msg in Model)
    {
      <li>@msg</li>
    }
  </ul>
</body>
</html>
  • 第 1 行:视图模板是一组错误消息。这些消息在第 24–29 行以 HTML 列表的形式显示;
  • 第 3 行:此视图不使用母版页 [_Layout.cshtml]。这是因为我们不需要该文档提供的菜单。因此,我们构建了一个完整的 HTML 页面(第 5–23 行)。

要测试此功能,只需按以下方式修改 [Application_Start] 中 [business] 层的实例化代码:


      try
      {
        // instantiation layer [business]
        application.PamMetier = ContextRegistry.GetContext().GetObject("xx") as IPamMetier;
      }
      catch (Exception ex)
      {
        application.InitException = ex;
}

第 4 行:我们正在查找一个在 Spring 对象中不存在的对象。

保存这些更改并运行应用程序后,我们会看到以下页面:

Image

我们得到一个没有菜单的错误页面。用户除了确认错误外别无他法。这正是我们想要的效果。

9.21. 目前进展如何?

现在,我们已拥有一个基于模拟业务层的可运行 Web 应用程序。其架构如下:

[ASP.NET MVC] 层通过 [IPamMetier] 接口与模拟的业务层进行交互。如果我们将这个模拟的业务层替换为一个实现该接口的真实业务层,则无需修改 Web 层的代码。 得益于 [Spring.net],我们只需在 [web.config] 中更改 [IPamMetier] 接口的实现类即可。我们将采用这种方法继续推进。

新架构将如下所示:

我们将依次说明以下内容:

  • 连接至数据库管理系统(DBMS)的 [EF5] 层。该层将采用 Entity Framework 5(EF5)实现;
  • [DAO] 层,通过 [EF5] 层管理数据访问。这使其无需关注具体的数据库管理系统。该层仅操作应用程序实体 [Employee, Contributions, Allowances];
  • 实现薪资计算的 [业务] 层。

新架构即本文档开头第1.1节中介绍的架构,现将其总结如下:

  • [Web]层是与Web应用程序用户直接交互的层。用户通过浏览器中显示的网页与Web应用程序进行交互。ASP.NET MVC仅位于此层,且仅在此层运行;
  • [业务]层实现应用程序的业务规则,例如计算工资或生成发票。该层通过[Web]层获取用户数据,并通过[DAO]层获取来自DBMS的数据;
  • [DAO](数据访问对象)层、[ORM](对象关系映射器)层以及 ADO.NET 连接器负责管理对 DBMS 数据的访问。[ORM] 层充当 [DAO] 层处理的对象与关系型数据库中数据行和列之间的桥梁。 .NET 领域中常用的 ORM 有两种:NHibernate (http://sourceforge.net/projects/nhibernate/) 和 Entity Framework (http://msdn.microsoft.com/en-us/data/ef.aspx);
  • 可以通过依赖注入容器(如 Spring http://www.springframework.net/)来实现各层的集成;

[业务]、[DAO] 和 [EF5] 层将通过 C# 项目实现。从现在起,我们将使用 Visual Studio Express 2012 for Desktop 进行开发。

9.22. 步骤 15:配置 Entity Framework 5 层

创建 [EF5] 层主要侧重于配置,而非编码。要了解如何编写该层,请阅读文档 [Entity Framework 5 Code First 入门],该文档可通过 URL [http://tahe.developpez.com/dotnet/ef5cf-02/] 获取。这是一份篇幅较长的文档,前四章涵盖了基础知识。后续将标明需要阅读的具体章节。 在引用该文档时,我们将使用 [refEF5] 这一标记。

此外,我们有时还需要用到 C# 的相关概念。届时我们将通过 [refC#] 这一标记,参考网址 [http://tahe.developpez.com/dotnet/csharp/] 上的课程 [C# 语言入门]。

9.22.1. 数据库

本应用程序的数据库已在第 9.4 节中介绍。这是一个名为 [dbpam_ef5](pam = Paie Assistante Maternelle)的 MySQL 数据库。该数据库有一个名为 root 的管理员,且无密码。

让我们回顾一下数据库架构。它包含三个表:

Image

EMPLOYEES表的INDEMNITY_ID列与INDEMNITIES表的ID列之间存在外键关系。该数据库结构的部分设计是由其与EF5的集成需求所决定的。

创建数据库的 SQL 脚本如下:


-- phpMyAdmin SQL Dump
-- version 3.5.1
-- http://www.phpmyadmin.net
--
-- Customer: localhost
-- Generated on: Mon November 04, 2013 at 09:34 am
-- Server version: 5.5.24-log
-- Version of PHP: 5.4.3
 
SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";
SET time_zone = "+00:00";
 
 
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
 
--
-- Database: `dbpam_ef5`
--
 
-- --------------------------------------------------------
 
--
-- Structure of the `contributions` table
--
 
CREATE TABLE IF NOT EXISTS `cotisations` (
  `ID` bigint(20) NOT NULL AUTO_INCREMENT,
  `SECU` double NOT NULL,
  `RETRAITE` double NOT NULL,
  `CSGD` double NOT NULL,
  `CSGRDS` double NOT NULL,
  `VERSIONING` int(11) NOT NULL,
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 AUTO_INCREMENT=12 ;
 
--
-- Contents of the `contributions` table
--
 
INSERT INTO `cotisations` (`ID`, `SECU`, `RETRAITE`, `CSGD`, `CSGRDS`, `VERSIONING`) VALUES
(11, 9.39, 7.88, 6.15, 3.49, 1);
 
--
-- Contribution triggers
--
DROP TRIGGER IF EXISTS `INCR_VERSIONING_COTISATIONS`;
DELIMITER //
CREATE TRIGGER `INCR_VERSIONING_COTISATIONS` BEFORE UPDATE ON `cotisations`
 FOR EACH ROW BEGIN
  SET NEW.VERSIONING:=OLD.VERSIONING+1;
END
//
DELIMITER ;
DROP TRIGGER IF EXISTS `START_VERSIONING_COTISATIONS`;
DELIMITER //
CREATE TRIGGER `START_VERSIONING_COTISATIONS` BEFORE INSERT ON `cotisations`
 FOR EACH ROW BEGIN
  SET NEW.VERSIONING:=1;
END
//
DELIMITER ;
 
-- --------------------------------------------------------
 
--
-- Structure of the `employees` table
--
 
CREATE TABLE IF NOT EXISTS `employes` (
  `ID` bigint(20) NOT NULL AUTO_INCREMENT,
  `PRENOM` varchar(20) CHARACTER SET latin1 NOT NULL,
  `SS` varchar(15) CHARACTER SET latin1 NOT NULL,
  `ADRESSE` varchar(50) CHARACTER SET latin1 NOT NULL,
  `CP` varchar(5) CHARACTER SET latin1 NOT NULL,
  `VILLE` varchar(30) CHARACTER SET latin1 NOT NULL,
  `NOM` varchar(30) CHARACTER SET latin1 NOT NULL,
  `VERSIONING` int(11) NOT NULL,
  `INDEMNITE_ID` bigint(20) NOT NULL,
  PRIMARY KEY (`ID`),
  UNIQUE KEY `SS` (`SS`),
  KEY `FK_EMPLOYES_INDEMNITE_ID` (`INDEMNITE_ID`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 AUTO_INCREMENT=26 ;
 
--
-- Contents of the `employees` table
--
 
INSERT INTO `employes` (`ID`, `PRENOM`, `SS`, `ADRESSE`, `CP`, `VILLE`, `NOM`, `VERSIONING`, `INDEMNITE_ID`) VALUES
(24, 'Marie', '254104940426058', '5 rue des oiseaux', '49203', 'St Corentin', 'Jouveinal', 1, 93),
(25, 'Justine', '260124402111742', 'La Brûlerie', '49014', 'St Marcel', 'Laverti', 1, 94);
 
--
-- Used' triggers
--
DROP TRIGGER IF EXISTS `INCR_VERSIONING_EMPLOYES`;
DELIMITER //
CREATE TRIGGER `INCR_VERSIONING_EMPLOYES` BEFORE UPDATE ON `employes`
 FOR EACH ROW BEGIN
  SET NEW.VERSIONING:=OLD.VERSIONING+1;
END
//
DELIMITER ;
DROP TRIGGER IF EXISTS `START_VERSIONING_EMPLOYES`;
DELIMITER //
CREATE TRIGGER `START_VERSIONING_EMPLOYES` BEFORE INSERT ON `employes`
 FOR EACH ROW BEGIN
  SET NEW.VERSIONING:=1;
END
//
DELIMITER ;
 
-- --------------------------------------------------------
 
--
-- Structure of the `indemnities` table
--
 
CREATE TABLE IF NOT EXISTS `indemnites` (
  `ID` bigint(20) NOT NULL AUTO_INCREMENT,
  `ENTRETIEN_JOUR` double NOT NULL,
  `REPAS_JOUR` double NOT NULL,
  `INDICE` int(11) NOT NULL,
  `INDEMNITES_CP` double NOT NULL,
  `BASE_HEURE` double NOT NULL,
  `VERSIONING` int(11) NOT NULL,
  PRIMARY KEY (`ID`),
  UNIQUE KEY `INDICE` (`INDICE`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 AUTO_INCREMENT=95 ;
 
--
-- Contents of the `indemnities` table
--
 
INSERT INTO `indemnites` (`ID`, `ENTRETIEN_JOUR`, `REPAS_JOUR`, `INDICE`, `INDEMNITES_CP`, `BASE_HEURE`, `VERSIONING`) VALUES
(93, 2.1, 3.1, 2, 15, 2.1, 1),
(94, 2, 3, 1, 12, 1.93, 1);
 
--
-- Compensation triggers
--
DROP TRIGGER IF EXISTS `INCR_VERSIONING_INDEMNITES`;
DELIMITER //
CREATE TRIGGER `INCR_VERSIONING_INDEMNITES` BEFORE UPDATE ON `indemnites`
 FOR EACH ROW BEGIN
  SET NEW.VERSIONING:=OLD.VERSIONING+1;
END
//
DELIMITER ;
DROP TRIGGER IF EXISTS `START_VERSIONING_INDEMNITES`;
DELIMITER //
CREATE TRIGGER `START_VERSIONING_INDEMNITES` BEFORE INSERT ON `indemnites`
 FOR EACH ROW BEGIN
  SET NEW.VERSIONING:=1;
END
//
DELIMITER ;
 
--
-- Constraints for exported tables
--
 
--
-- Constraints for the `employees` table
--
ALTER TABLE `employes`
  ADD CONSTRAINT `FK_EMPLOYES_INDEMNITE_ID` FOREIGN KEY (`INDEMNITE_ID`) REFERENCES `indemnites` (`ID`);
 
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

请注意以下几点:

  • 第 30、73、122 行:表的主键处于 [AUTO_INCREMENT] 模式。这些由 MySQL 管理,而非 EF5;
  • 第 83 行:社会安全号码(SSN)具有唯一性约束;
  • 第 130 行:员工 ID 具有唯一性约束;
  • 第 168–169 行:[employees] 表指向 [benefits] 表的外键;
  • 第 49 行:触发器是数据库管理系统(DBMS)嵌入的 SQL 脚本,会在特定时刻执行;
  • 第 51–54 行:触发器 [INCR_VERSIONING_COTISATIONS] 在 [cotisations] 表中的任何行被修改之前触发。随后它将 [VERSIONING] 列的值递增 1;
  • 第 59–62 行:[START_VERSIONING_COTISATIONS] 触发器在向 [cotisations] 表插入任何新行之前触发。随后,它将 [VERSIONING] 列初始化为 1;
  • 最终,当在 [contributions] 表中创建一行时,[VERSIONING] 列会被设置为 1,并且每次对该行进行修改时,该列都会递增 1。此机制使 EF5 能够按以下方式管理对 [contributions] 表中某行的并发访问:

    • 进程 P1 在时间 T1 从 [contributions] 表中读取一行 L。该行的 [VERSIONING] 列值为 V1
    • 进程 P2 在时间 T2 从 [contributions] 表中读取同一行 L。由于进程 P1 尚未提交其修改,该行的 [VERSIONING] 列值为 V1
    • 进程 P1 修改行 L 并提交其变更。随后,由于 [INCR_VERSIONING_COTISATIONS] 触发器的作用,行 L 的 [VERSIONING] 列值变为 V1+1;
    • 随后进程 P2 也执行了相同的操作。此时 EF5 抛出异常,因为进程 P2 拥有一行,其 [VERSIONING] 列的值为 V1,这与数据库中查找到的值 V1+1 不一致。只有当行中的 [VERSIONING] 值与数据库中的值相同时,该行才能被修改。

这被称为乐观并发控制。在 EF5 中,承担此角色的字段必须带有 [ConcurrencyCheck] 注解。

  • 针对 [employes] 表(第 98–113 行)和 [indemnites] 表(第 144–159 行)也创建了类似的机制。


任务:使用前面的 SQL 脚本创建 MySQL 数据库 [dbpam_ef5]。由于脚本不会自动创建该数据库,因此必须预先创建 [dbpam_ef5] 数据库。随后,我们将在此数据库上运行该 SQL 脚本。


9.22.2. Visual Studio 项目

使用 Visual Studio Express 2012 for Desktop,我们加载构建 [web] 层时使用的 [pam-td] 解决方案:

  • 在 [1] 中,VS 2012 Express for Desktop 无法加载 Web 项目 [pam-web-01]。这是正常现象,无需担心;
  • 在 [2] 中,我们将一个新项目添加到 [pam-td] 解决方案中;
  • 在 [3] 中,该项目类型为 [控制台],命名为 [4] [pam-ef5];
  • 在 [5] 中,项目已创建。其名称未加粗,因此并非该解决方案的启动项目;
  • 在 [6] 和 [7] 中,我们将新项目设置为启动项目。

9.22.3. 向项目添加必要的引用

让我们来看看整个项目:

我们的项目需要若干个 DLL:

  • Entity Framework 5 DLL;
  • 用于 MySQL 数据库管理系统 (DBMS) 的 ADO.NET 连接器 DLL。

[refEF5] 的第 4.2 节说明了如何使用 [NuGet] 工具安装这些 DLL。目前(2013 年 11 月),Entity Framework 的可用版本是 6 版(EF6)。 遗憾的是,截至2013年11月,通过[NuGet]获取的MySQL ADO.NET连接器似乎与EF6不兼容。因此,我们将EF5 DLL以及[pam-ef5]项目所需的其他DLL文件放置在[lib]文件夹中[1]

我们已将其他 DLL 文件放置在 [lib] 文件夹中。稍后我们将使用它们。在 [2] 中,我们将这些新 DLL 添加到项目中。

  • 在[3]中,通过文件系统导航至[lib]文件夹;
  • 在 [4] 中,选中这三个 DLL 文件,然后点击两次“确定”;
  • 在[5]中,这三个DLL文件已添加到项目引用中。

我们还需要另一个 DLL。该 DLL 可在计算机的 .NET 框架中找到。

  • 在 [1] 中,向项目添加一个新的引用;
  • 在 [2] 中,选择 [程序集];
  • 在 [3] 中,输入 [system.component];
  • 在 [4] 中,选择 [System.ComponentModel.DataAnnotations] 程序集;
  • 在 [5] 中,引用已添加。

现在我们可以开始编写代码并进行配置了。

9.22.4. Entity Framework 实体

Entity Framework 实体是封装各种数据库表中数据行的类。让我们来回顾一下它们:

Image

在 [web] 层中,我们使用了 [Employee、Contributions、Benefits] 实体(参见第 9.7.3 节,第 211 页)。它们并非对表的精确映射。因此,[ID、VERSIONING] 列被忽略了。而在这里情况将有所不同,因为 EF5 ORM 会使用这些列。因此,我们将向它们添加缺失的属性。 我们在项目内的 [Models] 文件夹中创建这些实体:

它们的新代码如下:

类 [Cotisations]


using System;
 
namespace Pam.EF5.Entites
{
  public class Cotisations
  {
    public int Id { get; set; }
    public double CsgRds { get; set; }
    public double Csgd { get; set; }
    public double Secu { get; set; }
    public double Retraite { get; set; }
    public int Versioning { get; set; }
 
    // signature
    public override string ToString()
    {
      return string.Format("Cotisations[{0},{1},{2},{3}, {4}, {5}]", Id, Versioning, CsgRds, Csgd, Secu, Retraite);
    }
  }
}
  • 第 3 行:命名空间已根据新项目进行了调整;
  • 第 7 行和第 12 行的属性已添加,以反映 [contributions] 表的结构;
  • 第 17 行:[ToString] 方法现在会显示这两个新字段。

类 [Indemnites]


using System;
 
namespace Pam.EF5.Entites
{
  public class Indemnites
  {
    public int Id { get; set; }
    public int Indice { get; set; }
    public double BaseHeure { get; set; }
    public double EntretienJour { get; set; }
    public double RepasJour { get; set; }
    public double IndemnitesCp { get; set; }
    public int Versioning { get; set; }
 
    // signature
    public override string ToString()
    {
      return string.Format("Indemnités[{0},{1},{2},{3},{4}, {5}, {6}]", Id, Versioning, Indice, BaseHeure, EntretienJour, RepasJour, IndemnitesCp);
    }
  }
}
  • 第3行:命名空间已调整为新项目;
  • 第 7 行和第 13 行的属性已添加,以反映 [indemnites] 表的结构;
  • 第 18 行:[ToString] 方法现在会显示这两个新字段。

类 [Employee]


using System;
 
namespace Pam.EF5.Entites
{
 
  public class Employe
  {
    public int Id { get; set; }
    public string SS { get; set; }
    public string Nom { get; set; }
    public string Prenom { get; set; }
    public string Adresse { get; set; }
    public string Ville { get; set; }
    public string CodePostal { get; set; }
    public Indemnites Indemnites { get; set; }
    public int Versioning { get; set; }
 
    // signature
    public override string ToString()
    {
      return string.Format("Employé[{0},{1},{2},{3},{4},{5}, {6}, {7}]", Id, Versioning, SS, Nom, Prenom, Adresse, Ville, CodePostal);
    }
  }
}
  • 第 3 行:命名空间已根据新项目进行了调整;
  • 第 8 行和第 16 行的属性已添加,以反映 [employees] 表的结构;
  • 第 21 行:[ToString] 方法现在会显示这两个新字段。

为了使这些类能够被 EF5 ORM 使用,必须为其属性添加注解。


任务:参照 [refEF5] 的第 3.4 节 [基于实体创建数据库],为 [Employee、Contributions、Benefits] 实体添加 EF5 所需的注解。


提示

  • 你只需创建注解。请勿遵循所引文献中关于[数据库创建]的部分;
  • 对于 [Table] 注解,请参考 [refEF5] 第 4.2 节中的 MySQL 示例;
  • 对于 [Versioning] 属性上的 [ConcurrencyCheck] 注解,请参考 [refEF5] 第 5.2 节中的 Oracle 示例;
  • 对于 [employes] 表对 [indemnités] 表的外键,请参考 [refEF5] 中的示例 3.4.2。因此,您将向 [Employe] 实体添加一个新属性:

    public int IndemniteId { get; set; }

其值将与 [employes] 表中的 [INDEMNITES_ID] 列的值一致。您需要为 [Employe] 实体的 [IndemniteId] 和 [Indemnites] 属性应用外键注解。为此,请参考 [refEF5] 中的示例 3.4.2;

  • 您无需管理这些外键的反向关系;
  • 此任务需要阅读 [refEF5] 中的相关内容。

9.22.5. 配置 EF5 ORM

让我们将项目置于上下文中:

[EF5] 层将通过 [ADO.NET] 连接器访问 MySQL 数据库管理系统(DBMS)。访问该数据库需要某些信息,这些信息分布在项目的各个部分中。

首先,我们必须创建数据库上下文。该上下文是一个从系统类 [System.Data.Entity.DbContext] 派生的类,用于定义数据库表的对象表示形式。我们将把该类与 EF5 实体一同放置在项目的 [Models] 文件夹中:

[DbPamContext] 类将如下所示:


using Pam.EF5.Entites;
using System.Data.Entity;
 
namespace Pam.Models
{
  public class DbPamContext : DbContext
  {
    public DbSet<Employe> Employes { get; set; }
    public DbSet<Cotisations> Cotisations { get; set; }
    public DbSet<Indemnites> Indemnites { get; set; }
  }
}
  • 第 6 行:[DbPamContext] 类继承自系统类 [DbContext];
  • 第 8–10 行:三个数据库表的对象表示。它们的类型为 [DbSet<Entity>],其中 [Entity] 是我们刚刚定义的 Entity Framework 实体之一。[DbSet] 类型可视为实体的集合。它可以使用 LINQ(语言集成查询)进行查询。 不熟悉 LINQ 的读者建议阅读 [refEF5] 中的第 3.5.4 节 [使用 LINQPad 学习 LINQ]。

此后,我们将把 [DbPamContext] 类称为 [dbpam_ef5] 数据库的持久化上下文。这是 ORM(对象关系映射器)中的标准术语。该持久化上下文是数据库的面向对象表示。 我们还指代持久化上下文与数据库之间的同步:对持久化上下文所做的修改、添加和删除操作都会反映到数据库中。这种同步发生在特定时刻:当持久化上下文关闭时、事务结束时,或者在对数据库执行 SQL SELECT 查询之前。

关于数据库管理系统(DBMS)和数据库的信息存储在 [App.config] 中。

[app.config] 文件中的必要配置在 [refEF5] 的以下章节中进行了说明:

  • 3.4 节针对 SQL Server 数据库管理系统。该节阐述了 EF5 配置的主要原则;
  • 4.2 章节针对 MySQL 数据库管理系统。

我们将遵循最后这一节,并按如下方式配置 [app.config] 文件:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
    <!-- configuration EF5 -->
    <!-- database connection string [dbam_ef5] -->
    <connectionStrings>
        <add name="DbPamContext"
         connectionString="Server=localhost;Database=dbpam_ef5;Uid=root;Pwd=;"
         providerName="MySql.Data.MySqlClient" />
    </connectionStrings>
    <!-- the MySQL factory provider -->
    <system.data>
        <DbProviderFactories>
            <remove invariant="MySql.Data.MySqlClient"/>
            <add name="MySQL Data Provider" invariant="MySql.Data.MySqlClient" description=".Net Framework Data Provider for MySQL"
          type="MySql.Data.MySqlClient.MySqlClientFactory, MySql.Data, Version=6.5.4.0, Culture=neutral, PublicKeyToken=C5687FC88969C44D"
        />
        </DbProviderFactories>
    </system.data>
</configuration>
  • 已添加第 6–21 行。这些内容必须插入在第 2 行和第 22 行的 <configuration> 标签内;
  • 第 8–12 行:定义数据库连接字符串,这是 ADO.NET 的概念(参见 [refC#] 中的 7.3.5 节);
  • 第 9–11 行:定义连接到 MySQL 数据库 [dbpam_ef5] 的连接字符串;
  • 第 9 行:连接字符串的名称。此处不能随意输入。默认情况下,必须输入实现数据库上下文的类名:

  public class DbPamContext : DbContext
  {
    public DbSet<Employe> Employes { get; set; }
    public DbSet<Cotisations> Cotisations { get; set; }
    public DbSet<Indemnites> Indemnites { get; set; }
}

该类名为 [DbPamContext]。在 [app.config] 的第 9 行,必须设置 [name="DbPamContext"];

  • 第 10 行:针对 MySQL 数据库管理系统(DBMS)的连接字符串:
    • [Server=localhost]:托管数据库管理系统的主机IP地址。此处为本地主机 [localhost];
    • [Database=dbpam_ef5;]:数据库名称,
    • [Uid=root;]:用于连接数据库的用户名,
    • [Pwd=;]:此登录的密码。此处未设置密码;
  • 第 10 行:[providerName="MySql.Data.MySqlClient"] 是要使用的 ADO.NET 提供程序的名称。该名称对应于第 17 行中的 [invariant] 属性。只要遵循前面的规则,且尚未注册过具有相同 invariant 的提供程序,您可以使用任何名称;
  • 第 15–20 行:定义一个 ADO.NET 提供程序工厂。对我来说,[DbProviderFactory] 是一个有些模糊的概念。从其名称来看,它似乎是一个能够生成 ADO.NET 提供程序的类,该提供程序用于访问数据库管理系统(DBMS),在本例中即 MySQL 5。这些代码行通常是复制粘贴的,但它们是必不可少的。 请注意第 16 行中的 [Version=6.5.4.0] 属性。此版本号必须与您添加到项目引用中的 [MySql.Data] DLL 的版本号一致:
  • 第 16 行非常重要。由于无法安装两个同名的提供程序,请先删除任何可能与第 17 行要安装的提供程序同名的现有提供程序;

就这样。初次操作时可能会觉得复杂且令人困惑,但随着时间推移,它会变得简单,因为这始终是一个重复的过程。

9.22.6. 测试 [EF5] 层

现在我们可以测试我们的 [EF5] 层了。我们将使用现有的 [Program.cs] 程序来完成此操作:

我们将显示数据库中的内容。如果成功,这将初步表明我们的配置是正确的。[refEF5] 的第 3.5.3 节中提供了一个代码示例。[Program.cs] 的代码如下:


using Pam.EF5.Entites;
using Pam.Models;
using System;
 
namespace Pam
{
  class Program
  {
    static void Main(string[] args)
    {
      try
      {
        using (var context = new DbPamContext())
        {
          // display table contents
          Console.WriteLine("Liste des employés ----------------------------------------");
          foreach (Employe employe in context.Employes)
          {
            Console.WriteLine(employe);
          }
          Console.WriteLine("Liste des indemnités --------------------------------------");
          foreach (Indemnites indemnite in context.Indemnites)
          {
            Console.WriteLine(indemnite);
          }
          Console.WriteLine("Liste des cotisations -------------------------------------");
          foreach (Cotisations cotisations in context.Cotisations)
          {
            Console.WriteLine(cotisations);
          }
        }
      }
      catch (Exception e)
      {
        Console.WriteLine(e);
        return;
      }
    }
  }
}

  • 第 13 行:所有数据库操作均通过此数据库上下文执行。我们使用 [DbPamContext] 类实现了该上下文。我们也将其称为数据库持久化上下文
  • 第 13、31 行:对持久化上下文的操作在 [using] 代码块内进行。持久化上下文在 [using] 代码块开始时打开,并在代码块结束时自动关闭。这意味着在 [using] 代码块内对持久化上下文所做的任何更改,都将在代码块结束时反映到数据库中。 随后,一系列 SQL 语句会在事务内发送至数据库。这意味着如果某条 SQL 语句失败,之前执行的所有 SQL 语句都会被回滚。此时 EF5 会抛出异常;
  • 第 17 行:表达式 [context.Employees] 指代 [employees] 表的对象模型。请注意,[Employees] 是持久化上下文 [DbPamContext] 的一个属性:

  public class DbPamContext : DbContext
  {
    public DbSet<Employe> Employes { get; set; }
    public DbSet<Cotisations> Cotisations { get; set; }
    public DbSet<Indemnites> Indemnites { get; set; }
}
  • 第 17 行:由于 [foreach] 循环遍历 [context.Employees] 集合,因此会将数据库中的所有员工数据加载到持久化上下文中。EF5 将会据此执行一条 SQL SELECT 语句;
  • 第 17–20 行:我们遍历员工集合,并在第 19 行使用 [Employee] 类的 [ToString] 方法将员工信息显示在控制台上;
  • 第 21–25 行:福利集合的操作与之相同;
  • 第 27–30 行:福利集合的操作方式相同。

让我们重新审视 [Employee] 实体的定义:


using System;
 
namespace Pam.EF5.Entites
{
 
  public class Employe
  {
    public int Id { get; set; }
    public string SS { get; set; }
    public string Nom { get; set; }
    public string Prenom { get; set; }
    public string Adresse { get; set; }
    public string Ville { get; set; }
    public string CodePostal { get; set; }
    public Indemnites Indemnites { get; set; }
    public int Versioning { get; set; }
 
    // signature
    public override string ToString()
    {
      return string.Format("Employé[{0},{1},{2},{3},{4},{5}, {6}, {7}]", Id, Versioning, SS, Nom, Prenom, Adresse, Ville, CodePostal);
    }
  }
}
  • 第 15 行:员工与一项福利相关联。

当员工被引入持久化上下文时,其津贴是否也会被引入?默认答案是否定的。这就是[延迟加载]的概念。被另一个实体引用的实体不会随该实体一起被引入持久化上下文。它们仅在打开的持久化上下文中被代码请求时才会被引入。如果持久化上下文已关闭,则会抛出异常。

因此,如果 [ToString] 方法像下面这样引用了 [Indemnites] 属性:


    // signature
    public override string ToString()
    {
      return string.Format("Employé[{0},{1},{2},{3},{4},{5},{6},{7},{8}]", Id, Versioning, SS, Nom, Prenom, Adresse, Ville, CodePostal, Indemnites);
}

[Program.cs] 中的以下操作:


          foreach (Employe employe in context.Employes)
          {
            Console.WriteLine(employe);
}

这不仅会将员工数据,还会将他们的福利数据返回给持久化上下文,因为在第 3 行,调用了 [Employee.ToString] 方法,而该方法引用了 [Benefits] 实体。

执行 [Program.cs] 文件将产生以下结果:

1
2
3
4
5
6
7
8
Liste des employés -----------------------------------------
Employé[24,1,254104940426058,Jouveinal,Marie,5 rue des oiseaux,St Corentin,49203]
Employé[25,1,260124402111742,Laverti,Justine,La Brûlerie,St Marcel,49014]
Liste des indemnités -----------------------------------------
Indemnités[93,1,2,2,1,2,1,3,1,15]
Indemnités[94,1,1,1,93,2,3,12]
Liste des cotisations -----------------------------------------
Cotisations[11,1,3,49,6,15,9,39,7,88]

如果无法正常工作该怎么办?您遇到麻烦了……可能的错误来源有很多:

  • 检查 EF5 配置(第 9.22.5 节);
  • 检查您的 Entity Framework 实体(第 9.22.4 节)。

9.22.7. [EF5] 层 DLL

我们将项目转换为类库,以便在生成时生成 .dll 程序集而非 .exe 文件。如第 9.7.6 节中针对模拟业务层所示,此操作在项目属性中进行。


任务:将项目类型 [pam-ef5] 更改为类库,然后重新生成项目。


9.23. 步骤 16:实现 [DAO] 层

9.23.1. [DAO] 层接口

与模拟的 [business] 层一样,[DAO] 层也将通过一个接口进行访问。这个接口会是什么?

让我们看看我们构建的模拟 [业务] 层中的 [IPamMetier] 接口:


    public interface IPamMetier {
        // list of all employee identities 
        Employe[] GetAllIdentitesEmployes();
 
        // ------- salary calculation 
        FeuilleSalaire GetSalaire(string ss, double heuresTravaillées, int joursTravaillés);
}

第 3 行:使用 [GetAllEmployeeIDs] 方法来填充主页上的下拉列表:

这些员工必须从数据库中检索出来。

第 6 行,[GetSalary] 方法用于计算已知社会安全号码(SSN)的员工的工资单。回顾 [PayStub] 类型的定义:


  public class FeuilleSalaire
  {
 
    // automatic properties 
    public Employe Employe { get; set; }
    public Cotisations Cotisations { get; set; }
    public ElementsSalaire ElementsSalaire { get; set; }
}

第 5 行和第 6 行中的信息将来自数据库。请记住,员工有一个 [Allowances] 属性。该信息也必须从数据库中检索。

因此,我们可以先定义以下用于 [DAO] 层的接口:


    public interface IPamDao {
        // list of all employee identities 
        Employe[] GetAllIdentitesEmployes();
        // an individual employee with benefits 
        Employe GetEmploye(string ss);
        // list of all contributions 
        Cotisations GetCotisations();
}

9.23.2. Visual Studio 项目


任务:在 [pam-td] 解决方案中添加一个名为 [pam-dao] 的新 [控制台] 项目。将其设置为解决方案的启动项目。


Image

9.23.3. 向项目添加必要的引用

让我们整体看看这个项目:

[pam-dao] 项目需要若干 DLL:

  • [pam-ef5] 项目引用的所有 DLL;
  • 以及 [pam-ef5] 项目本身提供的那个。

此外,我们将使用 [Spring.net] 来实例化 [DAO] 层。为此,我们需要 [Spring.core] 和 [Common.Logging] 动态链接库。这些动态链接库位于案例研究材料的 [lib] 文件夹中。


任务:将这些引用添加到 [pam-dao] 项目中。


9.23.4. [DAO]层的实现

上文中的 [PamException] 类即第 9.7.4 节中定义的类。我们只需更改其命名空间(如下第 1 行):


namespace Pam.Dao.Entites
{
  // exceptional class
  public class PamException : Exception
  {
....
  }
}

[IPamDao] 接口就是我们在第 9.23.1 节中刚刚定义的那个:


using Pam.EF5.Entites;
 
namespace Pam.Dao.Service
{
  public interface IPamDao
  {
    // list of all employee identities 
    Employe[] GetAllIdentitesEmployes();
    // an individual employee with benefits 
    Employe GetEmploye(string ss);
    // list of all contributions 
    Cotisations GetCotisations();
  }
}

[PamDaoEF5] 类使用 EF5 ORM 实现了该接口。其代码如下:


using Pam.Dao.Entites;
using Pam.EF5.Entites;
using Pam.Models;
using System;
using System.Linq;
 
namespace Pam.Dao.Service
{
 
  public class PamDaoEF5 : IPamDao
  {
    // private fields 
    private Cotisations cotisations;
    private Employe[] employes;
 
    // Manufacturer
    public PamDaoEF5()
    {
      // contribution
      try
      {
....
      }
      catch (Exception e)
      {
        throw new PamException("Erreur système lors de la construction de la couche [DAO]", e, 1);
      }
    }
 
    // GetCotisations
    public Cotisations GetCotisations()
    {
      return cotisations;
    }
 
    // GetAllIdentitesEmploye
    public Employe[] GetAllIdentitesEmployes()
    {
      return employes;
    }
 
    // GetEmploye
    public Employe GetEmploye(string SS)
    {
      try
      {
....
      catch (Exception e)
      {
        throw new PamException(string.Format("Erreur système lors de la recherche de l'employé [{0}]", SS), e, 2);
      }
    }
  }
}

注:

  • 第 10 行:类 [PamDaoEF5] 实现了接口 [IPamDao];
  • 表 [contributions] 和 [employees] 被缓存于第 13–14 行的属性中。员工数据不包含其津贴
  • 第17–28行:构造函数初始化第13–14行;
  • 第 43–52 行:[GetEmployee] 方法返回一名员工及其津贴。该方法以员工的社会保险号作为参数。如果数据库中不存在该员工,该方法将返回一个指针。


任务:完成 [PamDaoEF5] 类的代码。


对于构造函数,请参考第 9.22.6 节中 [EF5] 层的测试代码。对于 [GetEmploye] 方法,请参考 [refEF5] 第 3.5.7 节 [即时加载与延迟加载] 中的示例。

9.23.5. 配置 [DAO] 层

与第 9.22.5 节中的操作一样,我们需要在项目的 [App.config] 文件中配置 EF5:


任务 1:在 [App.config] 中配置 EF5。只需复制 [EF5] 层 [App.config] 文件中的配置即可。


我们的测试程序将使用 [Spring.net] 来获取 [DAO] 层的引用。


任务 2:利用第 9.20.2 节中的信息,修改 [pam-dao] 项目中的 [app.config] 配置文件,使其定义一个名为 [pamdao] 的 Spring 对象,该对象与我们刚刚创建的 [PamDaoEF5] 类相关联。[app.config] 和 [web.config] 文件具有相同的结构。 请确保 <configSections> 标签是紧跟在根 <configuration> 标签之后出现的第一个标签。


9.23.6. 测试 [DAO] 层

现在我们可以测试 [DAO] 层了。我们将使用现有的 [Program.cs] 程序进行测试:

我们将测试 [DAO] 层接口的各项功能。[Program.cs] 的代码如下:


using Pam.Dao.Service;
using Pam.EF5.Entites;
using Spring.Context.Support;
using System;
 
namespace Pam.Dao.Tests
{
  public class Program
  {
    public static void Main()
    {
      try
      {
        // layer instantiation [dao]
        IPamDao pamDao = (IPamDao)ContextRegistry.GetContext().GetObject("pamdao");
        // list of employee identities 
        foreach (Employe Employe in pamDao.GetAllIdentitesEmployes())
        {
          Console.WriteLine(Employe.ToString());
        }
            // an employee with benefits 
            Console.WriteLine("------------------------------------");
            Employe e = pamDao.GetEmploye("254104940426058");
            Console.WriteLine("employé= {0}, indemnités={1}", e, e.Indemnites);
            Console.WriteLine("------------------------------------");
        // an employee who doesn't exist 
        Employe employe = pamDao.GetEmploye("xx");
        Console.WriteLine("Employé n° xx");
        Console.WriteLine((employe == null ? "null" : employe.ToString()));
        Console.WriteLine("------------------------------------");
        // list of contributions 
        Cotisations cotisations = pamDao.GetCotisations();
        Console.WriteLine(cotisations.ToString());
      }
      catch (Exception ex)
      {
        // exception display 
        Console.WriteLine(ex.ToString());
      }
      //break 
      Console.ReadLine();
    }
  }
}

  • 第 15 行:我们使用 [Spring.net] 获取 [DAO] 层的引用。

运行此程序的结果如下:

1
2
3
4
5
6
7
8
9
Employé[22,1,254104940426058,Jouveinal,Marie,5 rue des oiseaux,St Corentin,49203]
Employé[23,1,260124402111742,Laverti,Justine,La Brûlerie,St Marcel,49014]
------------------------------------
employé= Employé[22,1,254104940426058,Jouveinal,Marie,5 rue des oiseaux,St Corentin,49203], indemnités=Indemnités[91,1,2,2,1,2,1,3,1,15]
------------------------------------
Employé n° xx
null
------------------------------------
Cotisations[10,1,3,49,6,15,9,39,7,88]

9.23.7. 图层 DLL [DAO]


任务:将项目类型 [pam-dao] 转换为类库,然后重新生成项目(重复第 9.22.7 节中的步骤)。


9.24. 步骤 17:设置 [business] 层

9.24.1. [业务] 层接口

[business] 层的接口将是我们在第 9.7.2 节中构建的模拟 [business] 层的 [IPamMetier] 接口。


    public interface IPamMetier {
        // list of all employee identities 
        Employe[] GetAllIdentitesEmployes();
 
        // ------- salary calculation 
        FeuilleSalaire GetSalaire(string ss, double heuresTravaillées, int joursTravaillés);
}

9.24.2. Visual Studio 项目


任务:在 [pam-td] 解决方案中添加一个名为 [pam-metier] 的新 [控制台] 项目。将其设置为解决方案的启动项目。


Image

9.24.3. 向项目添加必要的引用

让我们整体看看这个项目:

[pam-metier] 项目需要若干 DLL:

  • 所有由 [pam-dao] 和 [pam-ef5] 项目引用的 DLL;
  • [pam-dao] 和 [pam-ef5] 项目本身的 DLL 文件。

任务:将这些引用添加到 [pam-metier] 项目中。


Image

9.24.4. [业务]层的实现

在上文中,我们发现模拟的 [business] 层中已使用了四个元素(参见第 9.7 节)。这些不同类所导入的命名空间可能会发生变化。请处理这些变化。[PamMetier] 类如下所示实现了 [IPamMetier] 接口:


using Pam.Dao.Service;
using Pam.EF5.Entites;
using Pam.Metier.Entites;
using System;
 
namespace Pam.Metier.Service
{
 
  public class PamMetier : IPamMetier
  {
 
    // reference to layer [DAO] initialized by Spring
    public IPamDao PamDao { get; set; }
 
    // list of all employee identities 
    public Employe[] GetAllIdentitesEmployes()
    {
      ...
    }
 
    // an individual employee with benefits 
    public Employe GetEmploye(string ss)
    {
      ...
    }
 
    // contributions 
    public Cotisations GetCotisations()
    {
      ...
    }
 
    // wage calculation 
    public FeuilleSalaire GetSalaire(string ss, double heuresTravaillées, int joursTravaillés)
    {
      // SS : employee's SS number 
      // HeuresTravaillées: number of hours worked 
      // Days worked: number of days worked 
...
  }
}

  • 第 13 行:这里引用了 [DAO] 层。当 [PamMetier] 类被实例化时,Spring 会对其进行初始化。因此,在执行各种方法时,第 13 行已经初始化完毕。


任务:完成 [PamMetier] 类的代码。如果在 [GetSalaire] 中发现该社保号对应的员工不存在,则抛出 [PamException]。薪资计算方法详见第 9.5 节。请确保将所有中间计算结果四舍五入至小数点后两位。


9.24.5. 配置 [business] 层

如第 9.22.5 节所示,我们需要在项目的 [app.config] 文件中配置 EF5:


任务 1:在 [app.config] 中配置 EF5。只需复制 [EF5] 层 [app.config] 文件中的配置即可。


我们的测试程序将使用 [Spring.net] 来获取 [业务] 层的引用。


任务 2:利用您在第 9.23.5 节中完成的工作,修改 [pam-metier] 项目的 [app.config] 配置文件,使其定义一个名为 [pammetier] 的 Spring 对象,并与我们刚刚创建的 [PamMetier] 类相关联。 最简单的方法是从 [pam-dao] 项目中复制 [app.config] 文件,并补充缺失的内容。


这里有一个挑战。你不仅需要使用 [PamMetier] 类实例化 [business] 层,还必须初始化其 [PamDao] 属性:


    // référence sur la couche [DAO] initialisée par Spring
    public IPamDao PamDao { get; set; }

随后,[app.config] 中的 Spring 配置如下:


  <spring>
    <context>
      <resource uri="config://spring/objects" />
    </context>
    <objects xmlns="http://www.springframework.net">
      <object id="pamdao" type=" Pam.Dao.Service.PamDaoEF5, pam-dao"/>
      <object id="pammetier" type="Pam.Metier.Service.PamMetier, pam-metier">
        <property name="PamDao" ref="pamdao" />
      </object>
    </objects>
</spring>

  • 第 6 行:定义与 [PamDaoEF5] 类关联的 [pamdao] 对象;
  • 第 7 行:定义与 [PamMetier] 类关联的 [pammetier] 对象;
  • 第 8 行:使用 [property] 标签初始化 [PamMetier] 类的公共属性。 [name="PamDao"] 属性对应 [PamMetier] 类中待初始化的属性名称。属性 [ref="pamdao"] 表示该属性通过引用进行初始化,即引用第 6 行中的 [pamdao] 对象,从而引用来自 [DAO] 层的对象。这正是我们想要的效果。

9.24.6. 测试 [business] 层

现在我们可以测试 [business] 层了。我们将使用现有的 [Program.cs] 程序进行测试:

我们将测试[业务]层接口的各项功能。[Program.cs]的代码如下:


using System;
using Pam.Dao.Entites;
using Pam.Metier.Service;
using Spring.Context.Support;
using Pam.EF5.Entites;
 
namespace Pam.Metier.Tests
{
  public class Program
  {
    public static void Main()
    {
      try
      {
        // instantiation layer [business]
        IPamMetier pamMetier = ContextRegistry.GetContext().GetObject("pammetier") as IPamMetier;
        // list of employee identities
        Console.WriteLine("Employés -----------------------------");
        foreach (Employe Employe in pamMetier.GetAllIdentitesEmployes())
        {
          Console.WriteLine(Employe);
        }
 
        // payslip calculations 
        Console.WriteLine("salaires -----------------------------");
        Console.WriteLine(pamMetier.GetSalaire("260124402111742", 30, 5));
        Console.WriteLine(pamMetier.GetSalaire("254104940426058", 150, 20));
        try
        {
          Console.WriteLine(pamMetier.GetSalaire("xx", 150, 20));
        }
        catch (PamException ex)
        {
          Console.WriteLine(string.Format("PamException : {0}", ex.Message));
        }
      }
      catch (Exception ex)
      {
        Console.WriteLine(string.Format("Exception : {0}, Exception interne : {1}", ex.Message, ex.InnerException == null ? "" : ex.InnerException.Message));
      }
      // break 
      Console.ReadLine();
    }
  }
}

  • 第 16 行:我们使用 [Spring.net] 获取 [业务] 层的引用。

运行此程序的结果如下:

1
2
3
4
5
6
7
Employés -----------------------------
Employé[24,1,254104940426058,Jouveinal,Marie,5 rue des oiseaux,St Corentin,49203]
Employé[25,1,260124402111742,Laverti,Justine,La Brûlerie,St Marcel,49014]
salaires -----------------------------
[Employé[25,1,260124402111742,Laverti,Justine,La Brûlerie,St Marcel,49014],Cotisations[11,1,3,49,6,15,9,39,7,88],[64,85 : 17,45 : 10 : 15 : 72,4]]
[Employé[24,1,254104940426058,Jouveinal,Marie,5 rue des oiseaux,St Corentin,49203],Cotisations[11,1,3,49,6,15,9,39,7,88],[362,25 : 97,48 : 42 : 62 : 368,77]]
PamException : L'employé de n° [xx] n'existe pas

9.24.7. [业务]层的DLL


任务:将 [pam-business] 项目类型转换为类库,然后重新生成项目(重复第 9.22.7 节中的步骤)。


9.25. 步骤 18:实现 [web] 层

我们已到达架构的最后一个层,即 [web] 层:

我们将复用之前基于模拟的 [business] 层所开发的 [web] 层。

9.25.1. Visual Studio 项目

我们将回到 Visual Studio Express 2012 for the Web,将 Web 层与刚刚开发的 [业务逻辑、DAO、EF5] 层连接起来。这主要涉及一些配置和少量命名空间的更改。

在 Visual Studio Express 2012 for Web 中,加载 [pam-td] 解决方案:

  • 在 [1] 中,即 Visual Studio Express for Web 中的 [pam-td] 解决方案。Web 项目 [pam-web-01] 再次可见。我们在 Visual Studio Express for Desktop 中曾丢失过该项目。
  • 需要修改 [pam-web-01] Web 项目的配置。为了避免直接修改正在运行的项目,我们将对该项目的副本进行修改。首先,在 [2] 中,我们将该项目从解决方案中移除(这不会从文件系统中删除任何内容)。
  • 在 [3] 中,使用 Windows 资源管理器,将 [pam-web-01] 文件夹复制为 [pam-web-02];
  • 在 [4] 中,将 [pam-web-02] 项目添加到 [pam-td] 解决方案中。此时它显示为 [pam-web-01];
  • 在 [5] 中,将此名称更改为 [pam-web-02],并将其设为启动项目;
  • 在 [6] 中,加载旧项目 [pam-web-01]。现在您已拥有所有项目。请务必使用 [pam-web-02] 进行操作。

9.25.2. 向项目添加必要的引用

让我们整体查看一下该项目:

[pam-web-02] 项目需要若干 DLL:

  • [pam-metier]、[pam-dao] 和 [pam-ef5] 项目所引用的所有 DLL;
  • 以及 [pam-metier]、[pam-dao] 和 [pam-ef5] 项目自身包含的 DLL。

任务:将这些引用添加到 [pam-web-02] 项目中。必须移除对 [pam-metier-simule] 项目的引用。我们将切换到 [business] 层。引用列表中已有部分 DLL。请先移除它们,然后再进行添加。


Image

9.25.3. [web] 层的实现

构建 [pam-web-02] 项目。此时会出现如下错误:

Image

[ApplicationModel] 类使用了 [Employee] 类型。在模拟 [business] 层时,该类型定义在 [Pam.Business.Entities] 命名空间中。现在它位于 [Pam.EF5.Entities] 命名空间中。请按照上文所述修正这些错误。

9.25.4. 配置 [web] 层

如第 9.24.5 节所述,我们需要在项目的 [web.config] 文件中配置 EF5:


任务 1:将 [web.config] 文件中的当前全部内容替换为 [pam-metier] 项目中的 [app.config] 文件内容。


我们 Web 应用程序的 [Global.asax] 文件使用 [Spring.net] 来获取 [business] 层的引用:


      try
      {
        // instantiation layer [business]
        application.PamMetier = ContextRegistry.GetContext().GetObject("pammetier") as IPamMetier;
      }
      catch (Exception ex)
      {
        application.InitException = ex;
}

第 4 行:我们请求名为 [pammetier] 的 Spring 对象的引用。这确实是 [business] 层的名称(请在您的 [web.config] 文件中确认)。

9.25.5. 测试 [web] 层

现在我们可以测试 [web] 层了。首先,我们将更改其工作端口。默认情况下,[pam-web-02] 与 [pam-web-01] 具有相同的配置,因此运行在同一端口上。经验表明这会引发问题:IIS 会继续使用 [pam-web-01] 项目中的代码。请按以下步骤操作:

在 [4] 中,修改端口号,例如通过更改末位数字。

按 [Ctrl-F5] 运行 [pam-web-02] 项目。随后您将看到以下主页:

在 [1] 中,我们从 [dbpam_ef5] 数据库中检索员工信息。请注意,此前出现在模拟 [business] 层中的员工 [X X] 已不复存在。让我们进行一次模拟:

在[2]中,我们看到的是实际薪资,而非虚构的薪资。现在让我们停止MySQL5数据库管理系统,并运行另一项模拟:

在[3]中,我们获得了一个可读的错误页面,尽管其中部分信息是英文的。现在让我们再次停止MySQL,并使用[Ctrl-F5]在VS中重新运行应用程序:

Image

我们得到了第 9.20.4 节中创建的 [initFailed.cshtml] 视图。它显示了来自异常堆栈的错误消息。欢迎读者进行进一步的测试。

9.26. 步骤 19:使 ASP.NET 应用程序可通过互联网访问

使用 Visual Studio 开发 ASP.NET 应用程序时,默认配置确保该应用程序仅可通过 [localhost] 地址访问。Visual Studio 的嵌入式服务器会拒绝任何其他地址的访问请求,并返回 [400 Bad Request] 错误。

可通过以下方式进行验证:

  • 在 DOS 窗口中,记下您的开发机器的 IP 地址:

Microsoft Windows [version 6.3.9600]
(c) 2013 Microsoft Corporation. Tous droits réservés.
 
dos>ipconfig
 
Configuration IP de Windows
 
 
 
Carte Ethernet Connexion au réseau local :
 
   Suffixe DNS propre à la connexion. . . : ad.univ-angers.fr
   Adresse IPv6 de liaison locale. . . . .: fe80::698b:455a:925:6b13%4
   Adresse IPv4. . . . . . . . . . . . . .: 172.19.81.34
   Masque de sous-réseau. . . . . . . . . : 255.255.0.0
   Passerelle par défaut. . . . . . . . . : 172.19.0.254
 
Carte réseau sans fil Wi-Fi :
 
   Statut du média. . . . . . . . . . . . : Média déconnecté
   Suffixe DNS propre à la connexion. . . :

IP 地址显示在第 14 行。如果您已连接 Wi-Fi,设备的 Wi-Fi 地址将显示在第 20 行及之后。

  • 检查项目属性 [右键单击项目 / 属性 / Web 选项卡]:

Image

该应用程序将在 [localhost] 机器的 [65010] 端口上运行。

  • 按 [Ctrl-F5] 运行项目

Image

  • 将 [localhost] 替换为计算机的 IP 地址:

Image

服务器返回了 [400 Bad Request] 响应。Visual Studio 使用的 IIS Express 服务器仅接受 [localhost] 这一名称。

若要使开发的应用程序可通过 [http://adresseIP/contexte/...] 这样的 URL 访问,必须使用 IIS Express 以外的服务器,例如 IIS 服务器(非 Express 版)。要检查该服务器是否可用(通常在 Windows 专业版中可用),请转至“控制面板”[控制面板\系统和安全\管理工具]:

Image

此选项并非总是可见。若未显示,请转至 [控制面板 \ 程序] 并安装 Web 管理工具。

一旦 [Internet Information Services (IIS) 管理器] 选项可用,请将其启用:

启动默认网站。为此,必须先确保 [万维网发布服务] 正在运行:

完成上述操作后,在浏览器中输入 URL [http://localhost]。首先,请确认没有其他 Web 服务器正在使用端口 80。如果有,请将其停止。

IIS 服务器已响应。现在将 [localhost] 替换为您的计算机 IP 地址:

成功了。现在让我们回到 Visual Studio:

  • 首先,你需要以 [管理员] 模式启动 Visual Studio

完成后,你需要修改要部署的 Web 项目的配置 [右键单击项目 / 属性 / Web 选项卡]:

必须选择本地 IIS 服务器作为部署服务器。Visual Studio 会自动设置应用程序的 URL,您也可以自行修改。按 [Ctrl-F5] 运行项目:

现在将 [localhost] 替换为您计算机的 IP 地址:

如果您没有 IIS 服务器,可以使用免费的 ASP.NET 服务器,例如 [Ultidev Web Server Pro],其下载地址为 [http://ultidev.com/Download/]。安装完成后,有两种方法可以通过此服务器启动 Web 应用程序:

快速方法

打开 Windows 资源管理器,选择包含您要部署的 ASP.NET 应用程序的文件夹:

随后 Web 服务器将启动,Web 应用程序将在浏览器中显示:

  • 在 [3] 中,您可以停止或启动 Web 服务器;
  • 在 [4] 中,您可以更改 Web 应用程序的服务端口;

在启动服务器之前,下方的 [UWS HiPriv Services] 服务必须正在运行:

服务器运行后,界面如下所示:

点击链接 [6] 将显示应用程序的首页:

随后,您可以将 [localhost] 替换为机器的 IP 地址:

因此,此处同样仅接受名称 [localhost]。

绕远路

启动 Ultidev Web Explorer 应用程序

并按照以下步骤操作:

  • 在 [8] 中,指定要部署的 Web 应用程序的文件夹;
  • 在 [10-11] 中,必须通过 URL [http://localhost:81/] 访问该 Web 应用程序;
  • 使用 [14] 启动 Web 服务器;
  • 访问 URL [19];
  • 在 [20] 中,我们通过使用机器的本地 IP 地址代替 [localhost] 成功访问了目标页面。这正是我们想要的结果;

Ultidev 服务器以 Windows 服务的形式安装,并会自动启动。您可以按照以下步骤禁用 Ultidev 服务器的自动启动:

  • 进入 [控制面板\系统和安全\管理工具];
  • [1, 2]:选择 [Ultidev Web Server Pro] 服务的属性;
  • [3]:将其启动类型设置为“手动”。

要手动启动服务器,请使用 [Ultidev Web Explorer] 应用程序,例如:

9.27. 步骤 20:生成原生 Android 应用

当您拥有单页应用程序(SPA)时,可以使用 [PhoneGap] 工具 [http://phonegap.com/] 生成移动端可执行文件(Android、iOS、Windows 8 等)。此外还有其他方法,特别是使用开源产品 Apache Cordova [https://cordova.apache.org/]。 PhoneGap 网站 [http://build.phonegap.com/apps] 提供的在线工具会“上传”待转换网站的 ZIP 文件。主页必须命名为 [index.html],且必须是静态页面,即非由 Web 框架(如 ASP.NET、JEE、PHP 等)生成的页面。我们将从构建此页面开始。

9.27.1. 应用程序架构

这里需要特别注意的是,我们的目标是创建一个 Android 应用。此类应用通常具有以下架构:

  • 在 [1] 中,用户使用一台 Android 平板电脑,该设备与一个或多个 Web 服务 [2] 进行通信;

让我们回到 APU 模型:

  • 浏览器加载初始页面(上图未说明该页面来自何处);
  • 后续视图通过Ajax调用获取[1-4]。浏览器不会加载新的页面;

初始视图可能由同一台服务器提供,也可能由其他服务器提供。如果初始视图并非由同一台服务器提供,则初始页面上的 JavaScript 必须知道将提供其他视图的 Web 服务器的 URL。我们即将构建的 Android 应用程序就是这种情况:

  • 静态页面 [index.html] 将封装在一个具备浏览器功能的原生 Android 应用程序 [1] 中,因此该应用程序能够执行 [index.html] 页面中嵌入的 JavaScript;
  • 该页面将通过向服务器 [2] 发起 Ajax 调用来获取其他视图。为此,它需要知道 Web 服务器的 URL;

我们将重构 [pam-web-02] 应用程序,使其以这种模式运行。因此,第一页将如下所示:

  • 在 [1] 中,即应用程序初始页面的 URL。该 URL 将由第 9.26 节中讨论的 Ultidev 服务器提供;
  • 在 [2] 中,用户必须输入薪资模拟器的 URL。虽然我们可以将其硬编码到初始页面的 JavaScript 中,但这会增加测试的复杂性:一旦我们更改了模拟器的 IP 地址(或端口),就必须在 JavaScript 代码中进行相应修改;
  • 在 [3] 中,[登录] 链接将加载以下视图:
  • 请注意,在[4]中,浏览器的URL并未改变。它仍然是初始页面的URL,并且在应用程序的整个生命周期内都将保持不变。

该视图加载完成后,一切运行如常:通过 Ajax 调用加载不同的视图。我们将看到,几乎无需修改任何代码。

9.27.2. 重构 [pam-web-02] 项目

在 [pam-web-02] 项目的 [Content] 文件夹内,我们创建以下 [bootstrap] 文件夹(名称不限):

我们已包含静态页面 [index.html] 及其所需的所有资源(CSS 和 JS 文件)。[index.html] 页面采用了 Visual Studio 项目主页面 [_Layout.cshtml] 中的代码,并删除了所有非静态内容。最终生成的代码如下:


<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Simulateur de paie</title>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <link rel="stylesheet" href="Site.css" />
    <script type="text/javascript" src="jquery-1.8.2.min.js"></script>
    <script type="text/javascript" src="jquery.validate.min.js"></script>
    <script type="text/javascript" src="jquery.validate.unobtrusive.min.js"></script>
    <script type="text/javascript" src="globalize.js"></script>
    <script type="text/javascript" src="globalize.culture.fr-FR.js"></script>
    <script type="text/javascript" src="jquery.unobtrusive-ajax.min.js"></script>
    <script type="text/javascript" src="myScripts.js"></script>
</head>
<body>
    <table>
        <tbody>
            <tr>
                <td>
                    <h2>Simulateur de calcul de paie</h2>
                </td>
                <td style="width: 20px">
                    <img id="loading" style="display: none" src="indicator.gif" />
                </td>
                <td>
                    <a id="lnkConnexion" href="javascript:connexion()">
                        | Connexion<br />
                    </a>
                    <a id="lnkFaireSimulation" href="javascript:faireSimulation()">
                        | Faire la simulation<br />
                    </a>
                    <a id="lnkEffacerSimulation" href="javascript:effacerSimulation()">
                        | Effacer la simulation<br />
                    </a>
                    <a id="lnkVoirSimulations" href="javascript:voirSimulations()">
                        | Voir les simulations<br />
                    </a>
                    <a id="lnkRetourFormulaire" href="javascript:retourFormulaire()">
                        | Retour au formulaire de simulation<br />
                    </a>
                    <a id="lnkEnregistrerSimulation" href="javascript:enregistrerSimulation()">
                        | Enregistrer la simulation<br />
                    </a>
                    <a id="lnkTerminerSession" href="javascript:terminerSession()">
                        | Terminer la session<br />
                    </a>
                </td>
        </tbody>
    </table>
    <hr />
    <div id="content">
        <table>
            <tr>
                <td>URL du simulateur</td>
                <td><input type="text" id="urlServiceWeb" name="urlServiceWeb" size="80"></td>
            </tr>
        </table>
        <div id="erreur">
            <h3>Réponse du serveur :</h3>
            <div id="erreur1"></div>
            <div id="erreur2"></div>
        </div>
    </div>
</body>
</html>

我们添加了以下内容:

  • 第 27-29 行:我们添加了 [登录] 菜单选项,以便连接到模拟服务;
  • 第55-56行:模拟器URL输入字段;
  • 第 59-63 行:连接失败时的错误提示;

代码重构仅在上述第 14 行的 [myScripts.js] 代码中进行。其余部分保持不变。代码演变如下:


// au chargement du document
$(document).ready(function () {
    // on récupère les références des différents composants de la page
    loading = $("#loading");
    content = $("#content");
    erreur = $("#erreur");
    erreur1 = $("#erreur1");
    erreur2 = $("#erreur2");
    // les liens du menu
    lnkConnexion = $("#lnkConnexion");
    lnkFaireSimulation = $("#lnkFaireSimulation");
    lnkEffacerSimulation = $("#lnkEffacerSimulation");
    lnkEnregistrerSimulation = $("#lnkEnregistrerSimulation");
    lnkVoirSimulations = $("#lnkVoirSimulations");
    lnkTerminerSession = $("#lnkTerminerSession");
    lnkRetourFormulaire = $("#lnkRetourFormulaire");
    // on les met dans un tableau
    options = [lnkConnexion, lnkFaireSimulation, lnkEffacerSimulation, lnkEnregistrerSimulation, lnkVoirSimulations, lnkTerminerSession, lnkRetourFormulaire];
    // on cache certains éléments de la page
    loading.hide();
    erreur.hide();
    // on fixe le menu
    setMenu([lnkConnexion]);
});
  • 第 6-8 行:[index.html] 页面上显示连接错误区域的 ID;
  • 第10行:连接模拟器的新链接;
  • 第 21 行:错误区域初始状态为隐藏;
  • 第 23 行:仅显示连接链接;

在 [index.html] 页面中,连接链接的定义如下:


<a id="lnkConnexion" href="javascript:connexion()">
| Connexion<br />
</a>

JS 函数 [connexion](第 1 行)如下:


var urlServiceWeb;
var erreur, erreur1, erreur2;
 
 
function connexion() {
    // retrieve the urlServiceWeb from the web service
    urlServiceWeb = $("#urlServiceWeb").val();
    // retrieve the input form
    $.ajax({
        url: urlServiceWeb + '/Pam/Formulaire',
        type: 'POST',
        dataType: 'html',
        beforeSend: function () {
            // wait signal on
            loading.show();
        },
        success: function (data) {
            // displaying results
            content.html(data);
            // menu
            setMenu([lnkFaireSimulation]);
        },
        error: function (jqXHR) {
            erreur2.html(jqXHR.responseText);
            erreur1.html(jqXHR.getAllResponseHeaders().replace(/\r\n/g, "<br/>").replace(/\r/g, "<br/>").replace(/\n/g, "<br/>"));
            erreur.show();
        },
        complete: function () {
            // wait signal off
            loading.hide();
        }
    });
}
  • 第 7 行:我们获取用户输入的 URL。该 URL 存储在第 1 行定义的全局变量中。这样,该 URL 即可在该文件的其他函数中使用;
  • 第 10 行:我们向模拟器的 URL [/Pam/Form] 发起 Ajax 请求。该 URL 渲染用于输入模拟数据(员工、工作小时数、工作日数)的局部视图。 在 [pam-web-02] 的初始版本中,仅此 URL 即可满足需求。系统会自动在其前缀添加调用初始页面的 URL。现在,我们假设初始页面可能由除模拟器托管服务器以外的其他服务器提供。 因此,URL [/Pam/Formulaire] 必须加上第 1 行中的 [urlServiceWeb] 变量作为前缀,该变量即为模拟器的 URL(例如 http://172.19.81.34/pam-web-02)。文件中的所有 Ajax 调用都必须这样做
  • 第 17–22 行:若连接成功,则显示部分视图 [Formulaire.cshtml],并显示一个仅包含 [Run Simulation] 链接的菜单(第 21 行);
  • 第 23–27 行:如果连接失败:
    • 第 24 行,显示 Web 服务器发送的 HTML 响应(如有);
    • 第 25 行,显示 Web 服务器发送的 HTTP 头(如果服务器已响应);

就是这样。如果成功,将显示以下页面:

现在我们回到了之前的状态,视图现在是通过 Ajax 调用获取的。因此,如上所示,点击 [Run Simulation] 链接将由 [myScripts.js] 文件中的以下代码执行:


function faireSimulation() {
    // on récupère des références
    var simulation = $("#simulation");
    var formulaire = $("#formulaire");
    // formulaire valide ?
    var formValid = formulaire.validate().form();
    if (!formValid) return;
    // on fait un appel Ajax à la main
    $.ajax({
        url: urlServiceWeb + '/Pam/FaireSimulation',
        type: 'POST',
        data: formulaire.serialize(),
        dataType: 'html',
        ...
    });
    // menu
    setMenu([lnkEffacerSimulation, lnkEnregistrerSimulation, lnkTerminerSession, lnkVoirSimulations]);
}
  • 仅对第 10 行进行了修改,将原 URL 前缀替换为模拟器的 URL;

9.27.3. 测试重构后的项目

在第 9.26 节中,我们演示了如何在 Ultidev 服务器上安装 [pam-web-02] 应用程序。我们将以此为起点:

  • 在 [6] 中,我们请求显示页面 [bootstrap/index.html]。我们得到以下视图:

让我们输入一个错误的 URL:

  • 在 [10] 中,服务器响应的 HTTP 头部;
  • 在 [11] 中,服务器响应中的 HTML 文档;

如果您输入正确的 URL:

我们将获得以下响应:

9.27.4. 创建 Android 二进制文件

我们将基于刚刚创建并测试过的静态网站生成 Android 二进制文件[1]:

Image

Image

我们在[2]中添加了一个[config.xml]文件,该文件将用于配置[Phonegap]插件,该插件将生成Android二进制文件。其代码如下:


<?xml version='1.0' encoding='utf-8'?>
<widget id="android.exemples.pam" version="0.0.1" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
    <name>Pam</name>
    <description>
        IstiA - Université d'Angers
    </description>
    <author email="serge.tahe@univ-angers.fr">
      Serge Tahé
    </author>
    <content src="index.html" />
    <access origin="*" />
    <allow-navigation href="*" />
    <allow-intent href="*" />
    <plugin name="cordova-plugin-whitelist" />
</widget>
  • 第 7-9 行:在此处输入您的联系信息;
  • 第 11-13 行:这些行允许 Web 应用程序中嵌入的 JavaScript(将在 Android 设备上运行)请求设备外部的 URL;

我们将 [Content/bootstrap] 文件夹中的内容打包为压缩文件:

Image

接下来,访问 PhoneGap 网站 [http://build.phonegap.com/apps]:

  • 在 [1] 之前,您可能需要创建一个账户;
  • 在 [1] 处,我们开始操作;
  • 在 [2] 处,选择仅允许创建一个 PhoneGap 应用的免费套餐;
  • 在 [3],下载压缩后的应用 [4];
  • 在 [5] 处,输入应用名称;
  • 点击链接 [6] 生成适用于 iOS、Android 和 Windows 的二进制文件。此过程可能需要几秒钟;
  • 在 [7-9] 处,下载 Android 二进制文件;

启动一个用于 Android 平板电脑的 [GenyMotion] 模拟器(参见第 11.1 节):

Image

上文中,我们启动了一个基于 Android API 21 的平板电脑模拟器。模拟器运行后,

  • 请将锁定图标(如有)向侧边拖动并松开以解锁;
  • 使用鼠标,将您下载的 [Pam-debug.apk] 文件拖拽并放到模拟器上。随后该文件将被安装并运行;

按照第 9.27.3 节的说明输入 [1] 模拟器的 URL。完成后,使用链接 [2] 连接到模拟器:

Image

在模拟器上测试应用程序。它应该可以正常运行。