Skip to content

9. 案例研究

9.1. Introduction

我们将介绍一个案例研究,该研究此前已发表于URL [http://tahe.developpez.com/dotnet/pam-aspnet/]上的文章中。 在该文章中,案例研究使用的是经典的ASP.NET以及ORM和NHibernate。 在此,我们将使用 ASP.NETMVC 和 ORM 实体框架(Entity Framework)进行演示。 与现有文章一样,本案例研究以大学课程的形式呈现,因此主要面向学生群体。对于所有问题,文中均附有对前文详细阐述的章节的引用,以便读者查阅相关资料。

9.2. 待解决的问题

我们希望编写一个Web应用程序,允许用户模拟计算某市镇“幼儿之家”协会中保育员的工资。我们将同样关注应用程序代码的结构及其代码本身。

该应用程序将采用 APU [Application à Page Unique] 类型,并仅使用 Ajax 调用与服务器通信。它将向用户展示以下视图:

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

Image

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

Image

  • 视图 [VueSimulations],显示客户执行的模拟列表

Image

  • 视图 [VueSimulationsVides] 表示客户没有或已无模拟:

Image

  • 视图 [VueErreurs] 显示存在一个或多个错误(此处 SGBD 和 MySQL 已被停止):

Image

9.3. 应用程序架构

应用程序架构如下:

[EF5] 层指代 ORM Entity Framework 5。将使用的 SGBD 为 MySQL。

我们将首先使用一个模拟的 [métier] 层来构建此应用程序:

这将使我们能够完全专注于 [web] 层。模拟的 [métier] 层将遵循实际 [métier] 层的接口。 当 [web] 层投入运行后,我们将构建 [métier]、[DAO] 和 [EF5] 层。

9.4. 数据库

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

Image

EMPLOYES(INDEMNITE_ID) 列与 INDEMNITES(ID) 列之间存在外键关系。 该数据库的结构由其与EF5的关联决定。在构建应用程序的底层时,我们将再次讨论这一点。

EMPLOYES:汇总了关于各家庭保育员的信息

结构

ID
由 SGBD 自动递增的主键
SS
员工的社会保险号——唯一
NOM
员工姓名
PRENOM
其名字
ADRESSE
他的地址
VILLE
他的城市
CP
他的邮政编码
VERSIONING
一个整数,每次修改记录时自动递增
INDEMNITE_ID
表[INDEMNITES]中字段[ID]的外键

其内容可能如下:

Image

COTISATIONS:汇总从工资中扣除的社会保险费率

结构

ID
主键由SGBD自动递增
CSGRDS
百分比:普遍社会保险费 + 社会债务偿还费
CSGD
百分比:可抵扣的通用社会保险费
SECU
百分比:社会保险
RETRAITE
百分比:补充养老金 + 失业保险
VERSIONING
一个整数,每次修改记录时自动递增

其内容可能如下:

Image

社会保险费率与员工无关。前面的表只有一行。

INDEMNITES:汇总了与员工指数相关的各类津贴
ID
主键由SGBD自动递增
INDICE
薪级——唯一
BASE_HEURE
每小时看护服务的欧元净价
ENTRETIEN_JOUR
每小时看护的欧元生活津贴
REPAS_JOUR
每看护日餐费补贴(欧元)
INDEMNITES_CP
带薪休假补贴。该金额为基本工资的百分比。
VERSIONING
一个整数,每次修改记录时自动递增

其内容可能如下:

Image

9.5. 中保育员工资的计算方式

下面介绍保育员月薪的计算方法。我们以玛丽·朱维纳尔女士为例,她在当月工作了20天,共计150小时。

计算时考虑以下要素:
[TOTALHEURES]:当月总工作小时数
[TOTALJOURS]:当月工作天数
[TOTALHEURES]=150
[TOTALJOURS]= 20
保育员的基本工资按以下公式计算:
[SALAIREBASE]=([TOTALHEURES]*[BASEHEURE])*(1+[INDEMNITESCP]/100)
[SALAIREBASE]=(150*[2.1])*(1+0.15)= 362.25
需从该基本工资中扣除若干社会保险费:
综合社会保险费及社会债务偿还费:[SALAIREBASE]*[CSGRDS/100]
可抵扣的通用社会保险费:[SALAIREBASE]*[CSGD/100]
社会保险、遗属保险、养老保险:[SALAIREBASE]*[SECU/100]
补充养老金 + AGPF + 失业保险:[SALAIREBASE]*[RETRAITE/100]
CSGRDS:12.64
CSGD:22.28
社会保险:34.02
养老金:28.55
社会保险费总额:
[COTISATIONSSOCIALES]=[SALAIREBASE]*(CSGRDS+CSGD+SECU+RETRAITE)/100
[COTISATIONSSOCIALES]=97.48
此外,家庭保育员每工作一天,有权获得生活津贴和餐费补贴。为此,她将获得以下津贴:
[Indemnités]=[TOTALJOURS]*(ENTRETIENJOUR+REPASJOUR)
[INDEMNITES]=104
最终,应支付给保育员的净工资如下:
[SALAIREBASE] - [COTISATIONSSOCIALES] + [INDEMNITÉS]
[salaire NET]=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] 中,包含 JQuery 系列中用于应用程序国际化、客户端验证和 AJAX 化的脚本 JS;
  • 在 [8] 中,[myScripts.js] 是包含我们自定义脚本 JS 的文件;
  • 在 [9] 中,包含应用程序的视图:
    • [Index]:首页,
    • [Formulaire]:员工及其工作时长和工作日期的录入表单,
    • [Simulation]:显示模拟结果的视图,
    • [Simulations]:显示已执行模拟列表的视图,
    • [Erreurs]:显示潜在错误列表的视图,
    • [InitFailed]:应用程序初始化失败时显示错误消息的视图;
  • 在 [10] 中,应用程序的主页面 [_Layout];
  • 在 [11] 中,用于配置应用程序的文件 [Web.config]、[Global.asax]。

9.7. 步骤 1 – 建立模拟的 [métier] 层

从现在开始,我们将描述完成本案例研究所需的步骤。如有必要,我们会标注章节编号,以便您在完成指定任务时查阅。 项目中的部分元素已放在文件夹 [aspnetmvc-support.zip] 中,该文件夹可在本文档的网站上找到。其中包含文件夹 [étudedecas-support],内容如下:

  

此外,该项目还包含前几章介绍的内容。您只需通过复制/粘贴的方式,将这些内容从 PDF 文件夹复制到 Visual Studio 中即可。

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

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

  • 一个用于模拟 [métier] 层的项目;
  • 一个用于 Web 层的项目 MVC。

我们将使用两个工具:

  • Visual Studio Express 2012 for Desktop,用于构建 [métier] 层;
  • Visual Studio Express 2012 for Web,用于构建 [web] 层。

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

  • 在 [1] 中,选择 C# 应用程序;
  • 在 [2] 中,选择 [Application console];
  • 在 [3] 中,为解决方案命名;
  • 在 [4] 中,为该解决方案生成一个目录;
  • 在 [5] 中,为 [métier] 层命名;
  • 在 [6] 中,显示生成的解决方案。

9.7.2. [métier] 层的接口

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

[métier]层应向[web]层提供何种接口?这两个层之间可能存在哪些交互?让我们回顾一下将向用户展示的Web界面:

  1. 在表单初始显示时,[1]中应包含员工列表。简化的列表即可(姓、名、SS)。 编号 SS 用于访问所选员工的补充信息(信息 6 至 11)。
  2. 第12至15项信息为各项缴费率。
  3. 信息16至19为员工的津贴
  4. 信息 20 至 24 是根据用户输入的 1 至 3 项计算得出的工资组成部分。

由[métier]层提供给[web]层的[IPamMetier]接口必须满足上述要求。可能的接口方案有很多。我们建议采用以下方案:


using Pam.Metier.Entites;
namespace Pam.Metier.Service
{
  public interface IPamMetier
  {
    // 显示所有员工身份信息列表 
    Employe[] GetAllIdentitesEmployes();

    // ------- 工资计算 
    FeuilleSalaire GetSalaire(string ss, double heuresTravaillées, int joursTravaillés);
  }
}
  • 第 7 行:用于填充下拉列表 [1] 的方法
  • 第10行:用于获取第6至24项信息的处理方法。这些信息已被整合到一个类型为[FeuilleSalaire]的对象中,我们将在后续进行说明。

我们将此接口放入文件夹 [metier/service] 中:

9.7.3. [métier] 层的实体

前面的接口使用了两个类 [Employe] 和 [FeuilleSalaire],我们需要定义它们:

  • [Employe] 是数据库表 [employes] 中某行数据的图像;
  • [FeuilleSalaire] 代表一名员工的工资单。

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

在最终架构中,[métier]层将处理数据库中的图像实体:

Image

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

类 [Employe]

该类表示表 [employes] 中的一个行。其代码如下:


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; }

    // 签名
    public override string ToString()
    {
      return string.Format("Employé[{0},{1},{2},{3},{4},{5}]", SS, Nom, Prenom, Adresse, Ville, CodePostal);
    }
  }
}

类 [Indemnites]

它代表表 [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; }
    // 签名
    public override string ToString()
    {
      return string.Format("Indemnités[{0},{1},{2},{3},{4}]", Indice, BaseHeure, EntretienJour, RepasJour, IndemnitesCp);
    }
  }
}

类 [Cotisations]

它代表表 [cotisations] 中的一个记录。其代码如下:


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; }
    // 签名
    public override string ToString()
    {
      return string.Format("Cotisations[{0},{1},{2},{3}]", CsgRds, Csgd, Secu, Retraite);
    }
  }
}

需要注意的是,这些类并未包含表中的 [ID] 和 [VERSIONING] 列。 这些列在使用 ORM 和 EF5 时很有用,但在模拟的 [métier] 层中则无用。

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


namespace Pam.Metier.Entites
{
  public class FeuilleSalaire
  {

    // 自动属性 
    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 项信息。这里需要注意的是,一个 [Employe] 对象封装了一个代表其津贴的 [Indemnites] 对象;
  • 第8行:第12至15项信息;
  • 第 9 行:信息 20 至 24;
  • 第12-14行:方法[ToString]。

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


namespace Pam.Metier.Entites
{
  public class ElementsSalaire
  {
    // 自动属性 
    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
{
  // 异常类
  public class PamException : Exception
  {

    // 错误代码 
    public int Code { get; set; }

    // 构造函数 
    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){
     // 封装异常     
    throw new PamException("Problème d'accès aux données",ex,10);
}

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

9.7.5. [métier] 层的实现

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


using System;
using Pam.Metier.Entites;
using System.Collections.Generic;

namespace Pam.Metier.Service
{
  public class PamMetier : IPamMetier
  {
    // 缓存的员工列表
    public Employe[] Employes { get; set; }
    // 按编号索引的员工SS
    private IDictionary<string, Employe> dicEmployes = new Dictionary<string, Employe>();

    // 员工列表
    public Employe[] GetAllIdentitesEmployes()
    {
...
      // 生成员工名单
      return Employes;
    }

    // 计算工资
    public FeuilleSalaire GetSalaire(string ss, double heuresTravaillées, int joursTravaillés)
    {
...
  }
}
  • 第 7 行:类 [PamMetier] 实现了接口 [IPamMetier];
  • 第 10 行:类 [PamMetier] 将员工列表保存在缓存中;
  • 第 12 行:一个将员工与其社会保障号关联起来的字典;
  • 第 15-20 行:返回员工列表的方法;
  • 第 23-26 行:计算员工工资的方法。

方法 [GetAllIdentitesEmploye] 如下:


// 员工名单
    public Employe[] GetAllIdentitesEmployes()
    {
      if (Employes == null)
      {
        // 创建包含三名员工的表格
        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]);
        // 一名虚构员工,不会被添加到字典中
        // 用于模拟一名不存在的员工
        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 }
        };
      }
      // 输出员工列表
      return Employes;
    }
  • 第 4 行:检查员工列表是否已生成;
  • 第7行:如果不是这种情况,则创建一个包含三名员工的数组;
  • 第8-17行:第一个员工;
  • 第18行:将其放入字典中;
  • 第19-28行:第二个员工;
  • 第29行:将其放入字典中;
  • 第32-42行:第三位员工。出于稍后将解释的原因,该员工未被放入字典中。

方法 [GetSalaire] 将如下所示:


    // 计算工资
    public FeuilleSalaire GetSalaire(string ss, double heuresTravaillées, int joursTravaillés)
    {
      // 检索编号为 SS 的员工
      Employe e = dicEmployes.ContainsKey(ss) ? dicEmployes[ss] : null;
      // 是否存在?
      if (e == null)
      {
        throw new PamException(string.Format("L'employé de n° SS [{0}] n'existe pas", ss), 10);
      }
      // 生成一份虚拟工资单
      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行:该方法接收需要计算工资的员工的编号SS、其工作小时数及工作日数;
  • 第5行:在字典中查找该员工。需注意其中一名员工不在字典中;
  • 第7-10行:若未找到该员工,则抛出异常[PamException];
  • 第12-17行:返回一份虚拟的工资单。

9.7.6. [métier] 层的控制台测试

[métier] 层的项目目前如下:

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


using Pam.Metier.Entites;
using Pam.Metier.Service;
using System;

namespace Pam.Metier.Tests
{
  class Program
  {
    public static void Main()
    {
      // 实例化层 [métier]
      IPamMetier pamMetier = new PamMetier();
      // 员工列表
      Employe[] employes = pamMetier.GetAllIdentitesEmployes();
      Console.WriteLine("Liste des employés--------------------");
      foreach (Employe e in employes)
      {
        Console.WriteLine(e);
      }
      // 工资单计算 
      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 行:实例化 [métier] 层;
  • 第 14-19 行:测试接口 [IPamMetier] 的方法 [GetAllIdentitesEmploye];
  • 第 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项目中使用该项目,我们将它封装为类库:

  • 在文件 [Program.cs] 的属性中,将其设置为 [1];
  • 在 [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 解决方案中,我们将创建 Web 层项目 MVC。

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

  • 在 [1] 中,解决方案 [pam-td] 已加载到 Visual Studio Express for Web 中;
  • 在 [2] 中,我们刚刚创建的模拟 [métier] 层的解决方案和项目。

在此新步骤中,我们将创建 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 行,请将 jQuery 的版本号调整为您所使用的 Visual Studio 版本。

  • 第 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] 等效。 由于我们的应用程序类型为 APU,因此 URL [/] 将是该应用程序唯一的 URL。

创建控制器 [Pam]:

  

按以下方式修改控制器 [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] 运行应用程序。您应看到以下页面:

 

任务:解释发生了什么。


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


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

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


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

9.9. 步骤 3:配置模板 APU

我们将根据第 7.5 节和第 7.6 节中描述的 APU 模板(单页应用程序)编写一个应用程序。单页即应用程序启动时由浏览器加载的页面:

  • 上文中的 [1] 部分是单页的固定部分。我们已经看到,它由主页面 [_Layout.cshtml] 提供;
  • [2]部分是该单一页面的可变部分。它位于主页面[_Layout.cshtml]的id为[content]的区域内:

<!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-Maj-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]。它现在位于部分视图 [Formulaire.cshtml] 中:

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


<h2>Formulaire</h2>

进行这些修改后,请确认在应用程序启动时仍能看到以下界面:

 

9.9.3. Ajax调用 [faireSimulation]

我们关注用户点击链接 [Faire la simulation] 时显示的片段:

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

我们将显示表单的子视图 [Formulaire.cshtml] 进行如下调整:


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

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

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

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


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

现在我们需要编写处理 [Faire la simulation] 链接点击事件的 JavaScript 代码。我们将遵循第 7.6.5 节中介绍的方法。首先,让我们查看 [_Layout.cshtml] 中该链接的代码 HTML:


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

可以看到,点击链接 [Faire la simulation] 将触发函数 JS [faireSimulation] 的执行。 该函数将与应用程序所需的其他函数 JS 一起写入文件 [myScripts.js] 中:


// 全局变量
var loading;
var content;

function faireSimulation() {
  // 手动发起 Ajax 调用
...
}

function effacerSimulation() {
  // 清除表单输入
...
}

function enregistrerSimulation() {
  // 手动发起 Ajax 请求
  ...
}

function voirSimulations() {
  // 手动发起 Ajax 请求
  ...
}

function retourFormulaire() {
  // 手动发起Ajax请求
...
}

function terminerSession() {
...
}

// 文档加载时
$(document).ready(function () {
  // 获取页面中各个组件的引用
  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] 操作将部分视图 [Simulation.cshtml] 返回给函数 JS [faireSimulation],该函数随后将此数据流 HTML 放入表单的 。


请测试您应用程序中的链接 [Faire la simulation]。

9.9.4. Ajax 调用 [enregistrerSimulation]

链接 [Enregistrer la simulation] 在 [_Layout.cshtml] 中定义如下:


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

任务:按照上述步骤,编写函数 JS [enregistrerSimulation]。该函数将向操作 [/Pam/EnregistrerSimulation] 发出 POST 类型的 Ajax 调用。 目前不会提交任何数据。[/Pam/EnregistrerSimulation] 操作将部分视图 [Simulations.cshtml] 返回给函数 JS [enregistrerSimulation],该函数随后将此数据流 HTML 放置在 。


视图 [Simulations.cshtml] 如下所示:

其内容如下:


<h2>Simulations</h2>

以下是一个执行示例:

9.9.5. Ajax调用 [voirSimulations]

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


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

任务:按照上述步骤,编写函数 JS [voirSimulations]。该函数将向操作 [/Pam/VoirSimulations] 发出类型为 POST 的 Ajax 调用。 目前不会提交任何数据。[/Pam/VoirSimulations] 操作将部分视图 [Simulations.cshtml] 返回给函数 JS [voirSimulations],该函数随后将此数据流 HTML 放入 。


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

以下是一个执行示例:

9.9.6. Ajax调用 [retourFormulaire]

链接 [Retour au formulaire de simulation] 在 [_Layout.cshtml] 中定义如下:


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

任务:按照上述步骤,编写函数 JS [retourFormulaire]。该函数将向操作 [/Pam/Formulaire] 发出类型为 POST 的 Ajax 调用。 目前不会提交任何数据。[/Pam/Formulaire] 操作将部分视图 [Formulaire.cshtml] 返回给函数 JS [retourFormulaire],该函数随后将此数据流 HTML 放入 。


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

9.9.7. Ajax调用 [terminerSession]

链接 [Terminer la session] 在 [_Layout.cshtml] 中定义如下:


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

作业:按照之前的步骤,编写函数 JS [terminerSession]。该函数将向操作 [/Pam/TerminerSession] 发出类型为 POST 的 Ajax 调用。 目前不会提交任何数据。[/Pam/TerminerSession] 操作将部分视图 [Formulaire.cshtml] 返回给函数 JS [terminerSession],该函数随后将此数据流 HTML 放入 。


以下是一个执行示例:

9.9.8. 函数 JS [effacerSimulation]

在 [_Layout.cshtml] 中,链接 [Effacer la simulation] 的定义如下:


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

函数 JS [effacerSimulation] 的目的是:

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

任务:编写函数 JS [effacerSimulation]。此处不涉及 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 进行管理。我们将页面加载时执行的 JS 方法修改如下:


// 全局变量
var loading;
var content;
var lnkFaireSimulation;
var lnkEffacerSimulation
var lnkEnregistrerSimulation;
var lnkTerminerSession;
var lnkVoirSimulations;
var lnkRetourFormulaire;
var options;

...
// 在文档加载时
$(document).ready(function () {
  // 获取页面中各个组件的引用
  loading = $("#loading");
  content = $("#content");
  // 菜单链接
  lnkFaireSimulation = $("#lnkFaireSimulation");
  lnkEffacerSimulation = $("#lnkEffacerSimulation");
  lnkEnregistrerSimulation = $("#lnkEnregistrerSimulation");
  lnkVoirSimulations = $("#lnkVoirSimulations");
  lnkTerminerSession = $("#lnkTerminerSession");
  lnkRetourFormulaire = $("#lnkRetourFormulaire");
  // 将其放入数组中
  options = [lnkFaireSimulation, lnkEffacerSimulation, lnkEnregistrerSimulation, lnkVoirSimulations, lnkTerminerSession, lnkRetourFormulaire];
  // 隐藏页面中的某些元素
  loading.hide();
  // 固定菜单
  setMenu([lnkFaireSimulation, lnkVoirSimulations, lnkTerminerSession]);
});

  • 第19-24行:获取六个链接的引用。这些引用在第4-9行被定义为全局变量;
  • 第26行:使用这六个引用初始化数组[options]。该数组在第10行被定义为全局变量;
  • 第28行:隐藏等待Ajax请求完成的动画图片;
  • 第30行:显示链接[lnkFaireSimulation, lnkVoirSimulations, lnkTerminerSession]。其余链接将被隐藏。

函数 JS [setMenu] 如下:


function setMenu(show) {
  // 显示表格中的链接 [show]
...
}

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


T 是一个链接数组:

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

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

 

请调整函数 JS 和 [faireSimulation, effacerSimulation, enregistrerSimulation, voirSimulations, retourFormulaire, terminerSession],以显示以下界面:

现在,APU 模板和导航链接已就位,我们可以开始编写服务器端的操作和视图了。随着步骤的推进,你会发现某些原本能正常工作的 Ajax 链接不再起作用,这是因为你将修改发送给客户端的部分视图。 随着您逐步构建各种服务器端操作和视图,客户端的 Ajax 链接将恢复您最初设定的功能。

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

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

 

我们希望将此界面替换为以下界面:

 

应由 [Index] 操作生成此页面。请注意以下几点:

  • 该页面包含一个带有三个输入字段的表单:
    • 需要计算工资的员工,
    • 其工作小时数,
    • 其工作天数;
  • 表单通过链接 [Faire la simulation] 提交;
  • 需验证输入字段 [Heures travaillées] 和 [Jours travaillés] 的有效性;
  • 员工列表来自我们之前构建的 [métier] 层。

回顾当前操作 [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] 传递任何模板。因此,该视图将无法显示员工列表。该列表可向 [métier] 层请求。 为此,项目 [pam-web-01] 必须引用项目 [pam-metier-simule]。现在我们创建此引用:

  • 在 [1] 中,右键单击项目 [pam-web-01] 中的 [References],然后选择 [Ajouter une référence];
  • 在 [2] 中,选择 [Solution] 选项,然后将项目 [pam-metier-simule] 添加到 [3] 中;
  • 在 [4] 中,项目 [pam-metier-simule] 已添加到项目 [pam-web-01] 的引用中。

9.10.2. 应用程序模型

我们在第 78 页的 4.10 节中介绍了应用程序模型和会话模型这两个重要概念。现在我们将运用它们。需要提醒的是,模型中包含:

  • 应用程序模型:包含所有用户的只读数据。该模型构成所有用户所有请求共享的内存;
  • 会话模型:为特定用户提供可读写的数据。该模型构成该用户所有请求共享的内存。

我们将在应用程序模型中放入什么?让我们回顾一下其架构:

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

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


using Pam.Metier.Entites;
using Pam.Metier.Service;
namespace PamWeb.Models
{
  public class ApplicationModel
  {
    // --- 应用程序作用域数据 ---
    public Employe[] Employes { get; set; }
    public IPamMetier PamMetier { get; set; }
  }
}

要在视图中显示下拉列表,代码大致如下:


        <!-- 下拉列表 -->
        <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
  {
    // --- 应用程序范围数据 ---
    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()
    {
      // ----------自动生成
      AreaRegistration.RegisterAllAreas();
      WebApiConfig.Register(GlobalConfiguration.Configuration);
      FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
      RouteConfig.RegisterRoutes(RouteTable.Routes);
      BundleConfig.RegisterBundles(BundleTable.Bundles);
      // -------------------------------------------------------------------
      // ---------- 特定配置
      // -------------------------------------------------------------------
      // 应用程序作用域数据
      ApplicationModel application = new ApplicationModel();
      Application["data"] = application;
        // [métier] 层实例化
        application.PamMetier = ...
        // 员工表 
        application.Employes = ...
        // 员工下拉列表项
        application.EmployesItems = ...
      // [ApplicationModel] 的模型绑定器
      ...
    }
  }
}

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


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


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

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

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

来自下拉列表。我们将确保:

  • unTexte 表示员工的姓名(名在前,在后);
  • uneValeur 为员工的编号 SS

在上述第 35 行,您需要使用第 82 页第 4.10 节中描述的类 [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
  {
    // 应用程序范围数据
    public ApplicationModel Application { get; set; }
  }
}
  • 第 6 行:[IndexModel] 加载应用程序模板。

操作 [Index] 变为:


    [HttpGet]
    public ViewResult Index(ApplicationModel application)
    {
      return View(new IndexModel() { Application = application });
}
  • 第 4 行,显示默认视图 [Index.cshtml],其模板为类型 [IndexModel],该模板已使用应用程序模型的数据进行初始化。

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

Image

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

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

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

当表单提交时,处理流程如下:

  • 处理 POST 的正是该操作;
  • 其模板汇总了提交的值,此处为:
    • 所选员工的编号 SS;
    • 工作小时数;
    • 工作天数;

我们可以创建一个集合这三个值的操作模板。通常也会复用之前用于显示表单的模板。我们这里就采用这种做法。[IndexModel]类的变更如下:


using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
namespace Pam.Web.Models
{
  [Bind(Exclude = "Application")]
  public class IndexModel
  {
    // 应用程序作用域数据
    public ApplicationModel Application { get; set; }

    // 已发布值
    [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 行:三个提交的值。请注意,[joursTravaillés] 被声明为 [double] 类型,而实际上需要的是一个整数。 引入类型 [double] 是为了便于在客户端验证该字段,因为验证类型 [int] 时曾出现问题;
  • 第 12、14、17 行:为模型关联视图中的 [Html.LabelFor] 方法添加了标签;
  • 第 15 行:添加注释,使字段 [HeuresTravaillées] 的显示保留两位小数;
  • 第 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 行:它嵌入了子视图 [Formulaire.cshtml],同样未通过该视图传递任何模板。 在测试过程中发现,传递给视图 [Index.cshtml] 的模板 [IndexModel] 会隐式传播到子视图 [Formulaire.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] 测试该应用程序:

您需要确认下拉列表是否已正确填充了我们在模拟层 [métier] 中定义的员工列表。

9.11. 步骤 5:设置数据输入验证

9.11.1. 问题

尽管我们尚未进行任何操作,客户端验证功能已开始生效:

 
 

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


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

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


    public double JoursTravaillés { get; set; }

因此可以在该字段中输入实数:

 

此外,这两个字段均可输入任意值:

 

该表单的模板 [IndexModel] 目前如下所示:


using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
namespace Pam.Web.Models
{
  [Bind(Exclude = "Application")]
  public class IndexModel
  {
    // 应用程序作用域数据
    public ApplicationModel Application { get; set; }

    // 已发布值
    [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; }
  }
}

任务:改进此模板,以:

  • 支持自定义错误信息;
  • 仅允许字段 [HeuresTravaillées] 输入 [0,400] 区间内的实数;
  • 仅接受 [0,31] 区间内的整数值作为 [JoursTravaillées] 字段的值;

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

以下是预期结果的示例:

 
 
 

9.11.2. 以法语格式输入实数

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

 

该问题已在第6.1节中识别并处理。


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


现在,前面的屏幕显示为:

 

9.11.3. 通过JavaScript链接验证表单 [Faire la simulation]

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

 

[1]中的模拟操作以及[2]中的菜单切换表明,点击[Faire la simulation]链接时,即使输入的值无效,表单仍被提交。 该问题已在第7.6.5节中识别并处理。


任务:按照上述段落所述的方法,确保当输入值无效时,无法通过链接 [Faire la simulation] 生成 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 节中定义的类型 [FeuilleSalaire];
  • 该视图使用在应用程序样式表 [Content / Site.css] 中定义的类 [libellé, info, valeur]:

.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)的行高,该区域正是显示工资单的位置:


#表 tr 模拟 {
  height: 30px;
}

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


要显示金额中的欧元数,将使用方法 [string.Format]:

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

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

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


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

    // 进行模拟
    [HttpPost]
    public PartialViewResult FaireSimulation()
    {
      return PartialView("Simulation");
}

如上所示,操作 [FaireSimulation] 并未向视图 [Simulation.cshtml] 传递任何模板。它必须向该视图传递一份工资单。 我们知道,是 [métier] 层负责计算工资单。可以通过我们在第 9.10.2 节中定义的应用程序模板 [ApplicationModel] 访问该 [métier] 层:


  public class ApplicationModel
  {
    // --- 应用范围数据 ---
    public Employe[] Employes { get; set; }
    public IPamMetier PamMetier { get; set; }
    public SelectListItem[] EmployesItems { get; set; }
}

可以通过上述第 5 行中的属性访问 [métier] 层。 为了使操作 [FaireSimulation] 能够访问层 [métier],我们将像对操作 [Index] 所做的那样,向其传递应用程序模型。代码随之演变为如下形式:


    // 进行模拟
    [HttpPost]
    public PartialViewResult FaireSimulation(ApplicationModel application)
    {
      return PartialView("Simulation");
}

现在,我们可以在该操作内部计算一份模拟工资单。代码如下所示:


// 进行模拟
    [HttpPost]
    public PartialViewResult FaireSimulation(ApplicationModel application)
    {
      FeuilleSalaire feuilleSalaire = application.PamMetier.GetSalaire("254104940426058", 150, 20);
      return PartialView("Simulation", feuilleSalaire);
    }
  • 第 5 行,计算一个虚拟工资。 第一个参数是一个现有的 SS 编号。该编号已在第 9.7.5 节中模拟的 [métier] 类中定义。第二个参数是工作小时数,第三个参数是工作日数;
  • 第6行:将此工资单作为模板传递给视图[Simulation.cshtml]。

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

 

无需输入任何数据,直接进行模拟。结果如下:

 

9.12.3. 实际工资计算

我们当前的 [FaireSimulation] 操作始终计算同一张工资单:


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

该操作未考虑已输入的信息:

  • 待计算工资的员工;
  • 其工作小时数;
  • 其工作天数。

输入的值以如下方式传递至 [FaireSimulation] 操作:

  1. 用户点击链接 [Faire la simulation]。这将触发我们已编写的函数 JS [faireSimulation] 的执行;
  2. 随后,函数 JS [faireSimulation] 会向我们当前正在处理的服务器操作 [/Pam/FaireSimulation] 发起一次 Ajax 调用。 目前,函数 JS [faireSimulation] 尚未向服务器操作传递任何信息。它需要向该操作传递用户输入的值;
  3. 服务器操作 [/Pam/FaireSimulation] 将从 JS 和 [faireSimulation] 函数发送的 POST 数据中获取这些值。

首先处理第2点:函数 JS [faireSimulation] 需将用户输入的值提交至服务器操作 [/Pam/FaireSimulation]。


任务:完善函数 JS [faireSimulation],使其能够提交用户输入的值。可参考第 7.6.5 节中的示例,该问题已在其中进行过处理。


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


任务:完善服务器方法 [FaireSimulation],使其能够利用函数 JS 和 [faireSimulation] 提交的值来计算工资。 我们可以再次参考第7.6.5节中的示例,该问题已在其中进行过处理。目前,我们假设由已发布值生成的模型仍然有效。


提示:服务器操作 [FaireSimulation] 的流程如下:


// 进行模拟
    [HttpPost]
    public PartialViewResult FaireSimulation(ApplicationModel application, FormCollection data)
    {
      // 创建动作模型
      ...
      // 尝试检索该模板中已发布的值
      ...
      // 计算工资
      FeuilleSalaire feuilleSalaire = ...
      // 显示工资单
      return PartialView("Simulation", feuilleSalaire);
    }

以下是一个执行示例:

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

我们确实获得了 [Justine Laverti] 的模拟工资单。此前,唯一被计算的工资单是 [Marie Jouveinal]。 因此,员工选择项的已过账值已被利用。至于工时数和天数,我们无法做出判断,因为我们模拟的[métier]层未包含这些数据。

9.12.4. 错误处理

让我们来看以下示例:

  • 在 [1] 中,选择了一名不存在的员工(参见第 9.7.5 节中模拟的 [métier] 层定义);
  • 在 [2] 中,进行模拟;
  • 在下方的 [3] 中,会返回一个错误页面。

发生了什么?

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


function faireSimulation() {
...
  // 手动发起 Ajax 调用
  $.ajax({
    url: '/Pam/FaireSimulation',
...
    beforeSend: function () {
      // 等待信号亮起
      loading.show();
    },
    success: function (data) {
...
    },
    error: function (jqXHR) {
      // 显示错误
      simulation.html(jqXHR.responseText);
      simulation.show();
    },
    complete: function () {
      // 关闭等待信号
      loading.hide();
    }
  });
  // 菜单
  setMenu([lnkEffacerSimulation, lnkEnregistrerSimulation, lnkTerminerSession, lnkVoirSimulations]);
}

Ajax调用失败,导致第14-18行的函数被执行。服务器返回的错误页面[jqXHR.responseText]已被显示。该页面信息相当详细。 模拟的 [métier] 层抛出了异常,因为提供的编号 SS 并非现有员工的编号(参见第 9.7.5 节中模拟的 [métier] 层的代码)。 我们需要妥善处理这种情况。

我们将创建一个子视图 [Erreurs.chtml],每当服务器端检测到错误时,该视图就会返回给客户端 JS:

部分视图 [Erreurs.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] 的代码修改如下:


    // 进行模拟
    [HttpPost]
    public PartialViewResult FaireSimulation(ApplicationModel application, FormCollection data)
    {
    ...
      // 正在计算工资
      FeuilleSalaire feuilleSalaire = null;
      Exception exception=null;
      try
      {
        // 工资计算
        feuilleSalaire = ...
      }
      catch (Exception ex)
      {
        exception = ex;
      }
      // 错误?
      if (exception == null)
      {
        // 显示工资单
        return PartialView("Simulation", feuilleSalaire);
      }
      else
      {
        // 显示错误页面
        return PartialView("Erreurs", Static.GetErreursForException(exception));
      }
}
  • 第 9-17 行:工资计算现在在 try/catch 块中进行;
  • 第 27 行:若发生错误,则显示部分视图 [Erreurs.cshtml],其模板基于静态方法 [Static.GetErreursForException(exception)] 提供的错误消息列表。

在类 [Static] 中,将两个静态辅助函数 [1] 合并:


using System;
using System.Collections.Generic;
using System.Web.Mvc;

namespace PamWeb.Infrastructure
{
  public class Static
  {
    // 异常的错误消息列表
    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;
    }

    // 与无效模板相关的错误消息列表
    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;
    }

    // 与操作模型元素相关的错误消息
    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 行:静态函数 [GetErreursForException] 返回异常堆栈中的错误列表;
  • 第 22-36 行:静态函数 [GetErreursForModel] 返回无效操作模型的错误列表。该函数的代码以及私有方法 [getErrorMessageFor](第 39-54 行)的代码此前已出现过。

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

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

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


    // 进行模拟
    [HttpPost]
    public PartialViewResult FaireSimulation(ApplicationModel application, FormCollection data)
    {
      // 创建操作模型
      IndexModel modèle = new IndexModel() { Application = application};
      // 尝试从模型中检索提交的值
      TryUpdateModel(modèle, data);
      // 计算工资
...
}

在第 8 行,我们使用 Ajax 调用提交的值更新第 6 行的表单。我们并未验证表单的有效性。必须进行验证,因为我们无法确定提交的值来自何处。有人可能篡改了 POST 并向我们发送了无效数据。


任务:参照我们为异常情况开发的模型,修改服务器操作 [FaireSimulation],以便在提交的数据无效时返回错误页面。 为此,我们将使用 [Static] 类中的静态方法 [GetErreursForModel]。


如何测试此修改?在第 9.11.3 节中,您已确保当输入值无效时,函数 JS [faireSimulation] 不会对这些值执行 POST 操作。 将实现该功能的代码行注释掉,然后执行以下测试:

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

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

9.13. 步骤 7:建立用户会话

应用程序 [Simulateur de calcul de paie] 允许用户通过链接 [Faire la simulation] 进行各种薪资模拟,通过链接 [Enregistrer la simulation] 保存这些模拟, 通过链接 [Voir les simulations] 查看,以及通过链接 [Retirer la simulation] 删除。我们知道,在用户两次连续请求之间,系统不会保留任何数据,除非通过会话机制创建存储(参见第 4.10 节)。 这里很明显,我们需要在会话中保存用户随时间累积的已保存模拟列表。还有其他数据需要保存:当用户进行模拟时,只有当用户通过链接 [Enregistrer la simulation] 明确要求时,该模拟才会被记录到模拟列表中。 当用户执行该操作时,系统必须能够检索到前一次请求中计算的模拟结果。为此,该结果也将被存入会话中。最后,我们将从1开始对模拟进行编号。为了正确为新模拟编号,必须在会话中保留前一次模拟的编号。

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

我们创建以下类 [SessionModel]:

其代码如下:


using Pam.Web.Models;
using System.Collections.Generic;

namespace Pam.Web.Models
{
  public class SessionModel
  {
    // 模拟列表
    public List<Simulation> Simulations { get; set; }
    // 下一次模拟的编号
    public int NumNextSimulation { get; set; }
    // 最后一次模拟
    public Simulation Simulation { get; set; }

    // 生成器
    public SessionModel()
    {
      // 模拟列表为空
      Simulations = new List<Simulation>();
      // 下一次模拟编号
      NumNextSimulation = 1;
    }
  }
}

第 9 行和第 13 行中的类 [Simulation] 将记录有关模拟的信息。我们需要记录什么?链接 [Faire la simulation] 计算出类型为 [FeuilleSalaire] 的工资单。 将该工资单纳入模拟似乎是理所当然的。此外,我们需要记录生成此工资单的相关信息:

  • 所选员工。该信息可在字段 [FeuilleSalaire.Employe] 中找到,因此无需再次记录;
  • 工作小时数和天数。这些信息不在[FeuilleSalaire]类型中,因此我们需要将其保存下来。

最后,每次模拟都由一个编号标识。因此我们可以基于以下类 [Simulation] 进行操作:


using Pam.Metier.Entites;

namespace Pam.Web.Models
{
  public class Simulation
  {
    // 模拟编号
    public int Num { get; set; }
    // 工作小时数
    public double HeuresTravaillées { get; set; }
    // 工作日数
    public int JoursTravaillés { get; set; }
    // 工资单
    public FeuilleSalaire FeuilleSalaire { get; set; }
  }
}

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


// 进行模拟
    [HttpPost]
    public PartialViewResult FaireSimulation(ApplicationModel application, SessionModel session, FormCollection data)
    {
      // 创建操作模板
      IndexModel modèle = new IndexModel() { Application = application };
      // 尝试从模板中提取已录入的值
      TryUpdateModel(modèle, data);
      // 模型有效吗?
      if (!ModelState.IsValid)
      {
        // 显示错误页面
        return PartialView("Erreurs", Static.GetErreursForModel(ModelState));
      }
      // 计算工资
      FeuilleSalaire feuilleSalaire = null;
      Exception exception = null;
      try
      {
        // 计算工资
        feuilleSalaire = application.PamMetier.GetSalaire(modèle.SS, modèle.HeuresTravaillées, (int)modèle.JoursTravaillés);
      }
      catch (Exception ex)
      {
        exception = ex;
      }
      // 错误?
      if (exception != null)
      {
        // 显示错误页面
        return PartialView("Erreurs", Static.GetErreursForException(exception));
      }
      // 创建模拟并将其放入会话
      session.Simulation = ...
      // 显示工资单
      return PartialView("Simulation", feuilleSalaire);
    }
  • 第3行:该操作接收会话模板作为参数;

任务 1:完善该操作的代码,第 34 行



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


9.14. 步骤 8:保存模拟

9.14.1. 问题

完成模拟后,我们可以将其保存:

Image

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

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

Ajax 链接 [Enregistrer la simulation] 调用服务器操作 [EnregistrerSimulation],其代码此前如下:


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

现已更新为:


    // 保存模拟
    [HttpPost]
    public PartialViewResult EnregistrerSimulation(SessionModel session)
    {
      // 将最后一次模拟保存到会话的模拟列表中
      ...
      // 在会话中递增下一次模拟的编号
      ...
      // 显示模拟列表
      ...
}
  • 第 1 行:操作 [EnregistrerSimulation] 需要访问会话。因此,其参数为会话模板。

任务:完善服务器操作 [EnregistrerSimulation]。


9.14.3. 编写子视图 [Simulations.cshtml]

前一个操作 [EnregistrerSimulation] 会显示部分视图 [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中每项模拟对应的链接[retirer]将是一个形式如下所示的JavaScript链接:

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

其中 N 代表模拟编号。


任务 2:通过进行模拟来测试您的应用程序。 要进行模拟,请重复执行以下步骤:1) 通过 [F5] 加载应用程序页面,2) 进行一次模拟,3) 保存模拟结果。模拟数据将累积在当前会话中,并在视图 [Simulations.cshtml] 中体现出来。



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


Image

将使表 HTML 中的 <tr> 行, 在样式表 [/Content/Site.css] 中定义的类 CSS、[pair] 和 [impair]:


.impair {
  background-color: beige;
}

.pair {
  background-color: lightsteelblue;
}

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

9.15.1. 问题

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

Image

Image

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

Ajax 链接 [Retour au formulaire de simulation] 调用服务器操作 [Formulaire],其代码此前如下:


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

其显示的片段视图 [Formulaire] 需要一个 [IndexModel] 模型(见下文第 1 行):


@model Pam.Web.Models.IndexModel

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

这就是为什么链接 [Retour au formulaire de simulation] 无法正常工作。


工作内容:编写服务器操作 [Formulaire] 的新版本(需重写 2 行代码),然后进行测试。


9.15.3. 修改JavaScript函数[retourFormulaire]

根据之前的修改,现在可以返回表单,但此时会出现一个异常:

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

该异常已在第7.6.7节中识别并处理。


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


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

9.16.1. 问题

在使用模拟表单时,可以查看已执行的模拟列表:

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

Ajax链接 [Voir les simulations] 调用了服务器操作 [VoirSimulations],其代码此前如下:


    // 查看模拟
    [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>
...
}

这就是为什么链接 [Voir les simulations] 无法正常工作。


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


9.17. 步骤 11:结束会话

9.17.1. 问题

可随时通过链接 [Ajax] [Terminer la session] 结束用户会话。这将导致放弃当前会话并开始新会话。此外,系统将返回表单视图:

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

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

Ajax链接 [Terminer la session] 调用服务器操作 [TerminerSession],其代码此前如下:


    // 结束会话
    [HttpPost]
    public PartialViewResult TerminerSession()
    {
      return PartialView("Formulaire");
}

其显示的片段视图 [Formulaire] 需要一个 [IndexModel] 模型(见下文第 1 行):


@model Pam.Web.Models.IndexModel

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

这就是为什么链接 [Terminer la session] 无法正常工作。


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


:要在操作中注销会话,应编写:

Session.Abandon() ;

9.17.3. 修改 JavaScript 函数 [terminerSession]

根据之前的修改,现在可以返回表单了,但此时会出现一个异常,即第9.15.3节中描述的那个异常。


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


9.18. 步骤 12:清除模拟

9.18.1. 问题

完成模拟后,可通过 JavaScript 链接 [Effacer la simulation] 将其清除:

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

JavaScript 函数 [effacerSimulation] 目前的代码如下:


function effacerSimulation() {
  // 清除表单中的输入
  // ...
  // 若存在模拟,则将其隐藏
  $("#simulation").hide();
  // 菜单
  setMenu([lnkFaireSimulation, lnkTerminerSession, lnkVoirSimulations]);
}

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


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

9.19.1. 问题

在模拟页面中,可以通过 JavaScript 链接 [retirer] 删除某些模拟:

Image

Image

9.19.2. 客户操作记录 [retirerSimulation]

[retirer] 链接的格式如下:

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

其中 N 是模拟编号。


任务:按照第 9.9.3 节的步骤编写函数 JS [retirerSimulation]。 该函数将向操作 [/Pam/RetirerSimulation] 发出类型为 POST 的 Ajax 调用。它将以 num=N 的形式提交数据 N。


:函数 JS [retirerSimulation] 与您编写的其他向服务器发起 Ajax 调用的 JS 函数类似。 这里唯一的新变化是 POST 处理的值不在表单中。我们知道,提交的值会被组合成一个字符串,格式为:

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

因此,函数 JS [retirerSimulation] 的形式如下:


function retirerSimulation(N) {
  // 手动发起 Ajax 请求
  $.ajax({
    url: '/Pam/RetirerSimulation',
...
    data:"num="+N,
...
  });
  // 菜单
  setMenu([lnkRetourFormulaire, lnkTerminerSession]);
}
  • 第 6 行:Ajax 调用 JQuery 的属性 [data] 代表发送到服务器的字符串。

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

服务器操作 [RetirerSimulation]:

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

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


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

我们的 Web 应用程序已开发完成。通过模拟类 [métier],该应用程序已可正常运行。回顾我们开发的架构:

在进入 [métier] 层的实际实现之前,还有一些细节需要处理,这些处理发生在应用程序的初始化方法中:即 [Global.asax] 中的 [Application_Start] 方法:

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


// 应用程序
    protected void Application_Start()
    {
      // ----------自动生成
      AreaRegistration.RegisterAllAreas();
      WebApiConfig.Register(GlobalConfiguration.Configuration);
      FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
      RouteConfig.RegisterRoutes(RouteTable.Routes);
      BundleConfig.RegisterBundles(BundleTable.Bundles);
      // -------------------------------------------------------------------
      // ---------- 特定配置
      // -------------------------------------------------------------------
      // 应用程序作用域数据
      ApplicationModel application = new ApplicationModel();
      Application["data"] = application;
      // [métier] 层实例化
      application.PamMetier = new PamMetier();
...
      // 模型绑定器
...
}

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


  public class ApplicationModel
  {
    // --- 应用程序作用域数据 ---
    public Employe[] Employes { get; set; }
    public IPamMetier PamMetier { get; set; }
    public SelectListItem[] EmployesItems { get; set; }
}

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

此处我们将使用依赖注入容器 [Spring.net]。还有其他 .NET 框架可以实现相同功能,或许效果更好且更简单。

项目架构演变如下:

  • 在 [A] 中,[ASP.NET MVC] 层的初始化方法将向 [Spring.net] 请求模拟的 [métier] 层的引用;
  • 在 [B] 中,[Spring.net] 将利用其配置文件确定应实例化哪个类,从而创建模拟的 [métier] 层;
  • 在 [C] 中,[Spring.net] 将把模拟层 [métier] 的引用传递给 [ASP.NET MVC] 层。

需要注意的是,默认情况下,由 [Spring.net] 管理的对象是单例:它们仅存在一个实例。 因此,如果在本例后续代码中,再次向 [Spring.net] 请求模拟层 [métier] 的引用,[Spring.net] 仅会返回最初创建的对象的引用。

9.20.1. 将 [Spring] 引用添加到 Web 项目中

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

在 [1] 中,右键单击项目中的 [References] 分支,然后选择 [Gérer les packages NuGet] 选项。 此操作需要网络连接。随后,按照之前处理库 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 配置 -->
  <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行:定义位于DLL和[Spring.Core]中的[Spring.Context.Support.DefaultSectionHandler]类,该类能够处理第14至16行中的[objects]部分;
  • 第 6 行:定义位于 DLL 和 [Spring.Core] 中的类 [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] 是我们模拟的 [métier] 层所属的类。需返回其定义以了解其完整名称:

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

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

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

9.20.3. 修改 [Application_Start]

[Application_Start]方法的变更如下:


using Spring.Context.Support;

// 应用程序
    protected void Application_Start()
    {
      // ----------自动生成
      AreaRegistration.RegisterAllAreas();
      WebApiConfig.Register(GlobalConfiguration.Configuration);
      FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
      RouteConfig.RegisterRoutes(RouteTable.Routes);
      BundleConfig.RegisterBundles(BundleTable.Bundles);
      // -------------------------------------------------------------------
      // ---------- 特定配置
      // -------------------------------------------------------------------
      // 应用程序作用域数据
      ApplicationModel application = new ApplicationModel();
      Application["data"] = application;
      // [métier] 层实例化
      application.PamMetier = ContextRegistry.GetContext().GetObject("pammetier") as IPamMetier;
...
      // 模型绑定器
...
}
  • 第 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;

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

  • 最明显的原因是实例化对象的名称有误;
  • 另一种情况是 [métier] 层的实例化出现问题。对于我们模拟的 [métier] 层,这种情况不会发生,但对于将连接到数据库的实际 [métier] 层,则可能出现这种情况。 SGBD 可能未被调用,待处理数据库的信息可能不正确,等等……

我们将通过 try/catch 块处理可能出现的异常。代码修改如下:


// 应用程序
    protected void Application_Start()
    {
      // ----------自动生成
...
      // -------------------------------------------------------------------
      // ---------- 特定配置
      // -------------------------------------------------------------------
      // 应用程序作用域数据
      ApplicationModel application = new ApplicationModel();
      Application["data"] = application;
      application.InitException = null;
      try
      {
        // [métier] 层实例化
        application.PamMetier = ContextRegistry.GetContext().GetObject("pammetier") as IPamMetier;
      }
      catch (Exception ex)
      {
        application.InitException = ex;
      }
      //若无错误
      if (application.InitException == null)
      {
....
      }
      // 模型绑定器
...
    }
  • 第 12 行,我们在应用程序模型中引入了一个名为 [InitException] 的新属性:

  public class ApplicationModel
  {
    // --- 应用程序作用域数据 ---
    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 行:[métier] 层的实例化现在在 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)
    {
      // 初始化错误?
      if (application.InitException != null)
      {
        // 无菜单的错误页面
        return View("InitFailed",Static.GetErreursForException(application.InitException));
      }
      // 无错误
      return View(new IndexModel() { Application = application });
}

第 8 行,若初始化出现错误,我们将显示视图 [InitFailed.cshtml],其模板为初始化过程中发生的异常所包含的错误消息列表。 方法 [Static.GetErreursForException] 已在第 9.12.4 节中介绍并说明。视图 [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] 中将 [métier] 层的实例化修改如下:


      try
      {
        // [métier] 层实例化
        application.PamMetier = ContextRegistry.GetContext().GetObject("xx") as IPamMetier;
      }
      catch (Exception ex)
      {
        application.InitException = ex;
}

第 4 行中,系统正在查找一个在 Spring 对象中不存在的对象。

当保存这些修改并启动应用程序时,将显示以下页面:

 

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

9.21. 目前进展如何?

现在,我们已拥有一个可运行的Web应用程序,该应用程序与模拟的业务层协同工作。其架构如下:

[ASP.NET MVC] 层通过 [IPamMetier] 接口与模拟业务层进行交互。如果我们将该模拟业务层替换为符合该接口的真实业务层,则无需修改 Web 层的代码。 借助 [Spring.net],我们只需在 [web.config] 中更改 [IPamMetier] 接口的实现类。我们将采取这一方案。

新架构如下:

我们将依次说明:

  • 连接至 SGBD 的 [EF5] 层。该层将通过 Entity Framework 5(EF5)实现;
  • [DAO] 层,该层通过 [EF5] 层管理数据访问。 这使其能够忽略 SGBD 的存在。该层仅负责操作 [Employe, Cotisations, Indemnites] 应用程序中的实体;
  • [métier] 层,负责实现薪资计算。

新架构即本文开头第1.1节中介绍的架构,现再次列出如下:

  • [Web]层是与Web应用程序用户交互的层。用户通过浏览器显示的网页与Web应用程序进行交互。 ASP.NET MVC 位于该层,且仅位于该层;
  • [métier] 层实现应用程序的管理规则,例如工资或发票的计算。 该层通过 [Web] 层使用来自用户的数据,并通过 [DAO] 层使用来自 SGBD 的数据;
  • [DAO] 层(数据访问对象)、[ORM] 层(对象关系映射器)以及 ADO.NET 连接器负责管理对 SGBD 层数据的访问。 [ORM] 层在 [DAO] 层处理的对象与关系型数据库数据的行和列之间架起了一座桥梁。 全球范围内,通常使用两个 ORM:NET、 NHibernatehttp://sourceforge.net/projects/nhibernate/)和 Entity Frameworkhttp://msdn.microsoft.com/en-us/data/ef.aspx);
  • 各层的集成可通过依赖注入容器(Dependency Injection Container)实现,例如 Spring(http://www.springframework.net/);

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

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

[EF5] 层的创建更多是配置工作,而非编码工作。为了理解该层的编写,请阅读 [Introduction à Entity Framework 5 Code First] 文档,该文档可在 URL [http://tahe.developpez.com/dotnet/ef5cf-02/] 中获取。 这是一份篇幅较长的文档。基础知识主要集中在前四章。具体需要重点阅读的段落将在后续说明中明确。在引用该文档时,我们将使用 [refEF5] 这一标记。

此外,我们有时需要用到C#的相关概念。届时将引用课程[Introduction au langage C#],该课程可在URL [http://tahe.developpez.com/dotnet/csharp/]中找到,我们将使用[refC#]这一标记进行引用。

9.22.1. 数据库

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

回顾一下数据库的结构。它包含三个表:

Image

EMPLOYES(INDEMNITE_ID)列与 INDEMNITES(ID)列之间存在外键关系。 该数据库的部分结构由其与 EF5 的配合使用所决定。

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


-- phpMyAdmin SQL 转储
-- 版本 3.5.1
-- http://www.phpmyadmin.net
--
-- 客户端:localhost
-- 生成时间:2013年11月4日 星期一 09:34
-- 服务器版本:5.5.24-log
-- 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 */;

--
-- 数据库:`dbpam_ef5`
--

-- --------------------------------------------------------

--
-- `cotisations` 表的结构
--

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 ;

--
-- `cotisations` 表的内容
--

INSERT INTO `cotisations` (`ID`, `SECU`, `RETRAITE`, `CSGD`, `CSGRDS`, `VERSIONING`) VALUES
(11, 9.39, 7.88, 6.15, 3.49, 1);

--
-- `cotisations` 触发器
--
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 ;

-- --------------------------------------------------------

--
-- `employes` 表的结构
--

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 ;

--
-- `employes` 表的内容
--

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);

--
-- `employes` 表的触发器
--
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 ;

-- --------------------------------------------------------

--
-- `indemnites` 表的结构
--

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 ;

--
-- `indemnites` 表的内容
--

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);

--
-- `indemnites` 表的触发器
--
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 ;

--
-- 导出表的约束
--

--
-- `employes` 表的约束
--
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行:编号SS具有唯一性约束;
  • 第 130 行:员工编号具有唯一性约束;
  • 第 168-169 行:表 [employes] 指向表 [indemnites] 的外键;
  • 第 49 行:触发器 [Trigger] 是一个由 SGBD 嵌入的脚本 SQL,并在特定时刻执行;
  • 第51-54行:在表[cotisations]的任何行被修改之前,触发器[INCR_VERSIONING_COTISATIONS]会被触发。此时,它会将列[VERSIONING]的值递增1;
  • 第59-62行:在向表[cotisations]插入新行之前,触发器[START_VERSIONING_COTISATIONS]会被触发。此时,它将列[VERSIONING]初始化为1;
  • 最终,当在表 [cotisations] 中创建一行时,列 [VERSIONING] 的值为 1,并且每当对该行进行修改时,该值就会增加 1。 该机制使 EF5 能够通过以下方式管理对表 [cotisations] 中某行数据的并发访问:
    • 进程 P1 在时间点 T1 读取表 [cotisations] 中的行 L。该行的 [VERSIONING] 列值为 V1
    • 进程 P2 在时间点 T2 读取了表 [cotisations] 中的同一行 L。 该行包含列 [VERSIONING] V1,因为进程 P1 尚未提交其修改;
    • 进程 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] 中,该项目属于 [console] 类型,名称为 [4] [pam-ef5];
  • 在 [5] 中,创建了该项目。其名称未加粗,因此这不是解决方案的启动项目;
  • 在 [6] 和 [7] 中,将新项目设为启动项目。

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

让我们来整体了解该项目:

我们的项目需要以下几个 DLL:

  • Entity Framework 5 的 DLL;
  • SGBD MySQL 的连接器 ADO.NET 所需的 DLL。

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

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

  • 在 [3] 中,浏览文件系统至 [lib] 文件夹;
  • 在 [4] 中,选中三个 DLL 文件,然后点击两次确认;
  • 在 [5] 中,三个 DLL 已添加到项目引用中。

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

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

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

9.22.4. Entity Framework 实体

Entity Framework 实体是用于封装数据库中不同表的行数据的类。回顾一下这些表:

Image

在 [web] 层中,我们曾使用过 [Employe, Cotisations, Indemnités] 实体(参见第 9.7.3 节,第 219 页)。这些实体并非对表的精确映射。因此,[ID, VERSIONING] 中的列被忽略了。 在此情况下则不同,因为它们被 ORM 和 EF5 所使用。因此,我们将为它们添加缺失的属性。我们在项目中的 [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; }

    // 签名
    public override string ToString()
    {
      return string.Format("Cotisations[{0},{1},{2},{3}, {4}, {5}]", Id, Versioning, CsgRds, Csgd, Secu, Retraite);
    }
  }
}
  • 第 3 行:命名空间已调整为新项目;
  • 第 7 行和第 12 行的属性已添加,以反映表 [cotisations] 的结构;
  • 第 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; }

    // 签名
    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] 现在显示这两个新字段。

类 [Employe]


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; }

    // 签名
    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 行的属性,以反映 [employes] 表的结构;
  • 第21行:方法[ToString]现在会显示这两个新字段。

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


任务:参考 [refEF5] 文档中的第 3.4 节,为 [Employe, Cotisations, Indemnites] 实体添加 EF5 所需的注解。


提示

  • 此处仅需创建注释。请勿遵循所引用段落中的 [création de base] 部分;
  • 对于注释 [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. ORM 的配置 EF5

让我们重新梳理整个项目:

[EF5]层将通过SGBD MySQL中的[ADO.NET]连接器访问数据库。 该层需要若干信息才能访问该数据库。这些信息分布在项目的不同位置。

首先,我们需要创建数据库上下文。该上下文是系统类 [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(语言 INtegrated 查询)对其进行查询。 若读者不熟悉 LINQ,请参阅 [refEF5] 文档中的 3.5.4 [Apprentissage de LINQ avec LINQPad] 章节。

下文我们将类 [DbPamContext] 称为数据库 [dbpam_ef5] 的持久化上下文。这是 ORM(对象关系映射器)中常用的术语。 该持久化上下文是数据库的对象映射。这也被称为持久化上下文与数据库的同步:对持久化上下文所做的修改、添加或删除操作都会反映到数据库中。 这种同步在特定时刻进行:关闭持久化上下文时、事务结束时,或向数据库发出查询之前。

关于 SGBD 和数据库的信息被放置在 [App.config] 中。

[app.config]中所需的配置在[refEF5]的以下段落中进行了说明:

  • 3.4 针对 SGBD SQL 服务器。此处阐述了 EF5 配置的基本原则;
  • 4.2 针对 SGBD 和 MySQL。

我们遵循最后一段的说明,并按以下方式配置 [app.config] 文件:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
    <!-- 配置EF5 -->
    <!-- 数据库连接字符串 [dbam_ef5] -->
    <connectionStrings>
        <add name="DbPamContext"
         connectionString="Server=localhost;Database=dbpam_ef5;Uid=root;Pwd=;"
         providerName="MySql.Data.MySqlClient" />
    </connectionStrings>
    <!-- MySQL 的工厂提供程序 -->
    <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 行:SGBD 专用的连接字符串 MySQL:
    • [Server=localhost]:托管 SGBD 的服务器的地址 IP。此处为本地服务器 [localhost];
    • [Database=dbpam_ef5;]:数据库名称,
    • [Uid=root;]:用于连接数据库的用户名,
    • [Pwd=;]:该登录名的密码。此处未设置密码;
  • 第 10 行:[providerName="MySql.Data.MySqlClient"] 是将要使用的连接器 ADO.NET 的名称。 该名称即第17行中[invariant]属性的值。只要符合前述规则且尚未注册过具有相同不变量的提供程序,名称可任意设定;
  • 第15-20行:定义了一个名为ADO.NET的连接器(provider)工厂(factory)。[DbProviderFactory]这个概念对我来说有些模糊。 如果仅从名称推测,这应该是一个能够生成 ADO.NET 连接器的类,该连接器可访问 SGBD(此处为 MySQL5)。通常我们会直接复制粘贴这些代码行,它们是必不可少的。 请注意第16行中的[Version=6.5.4.0]属性。该版本号必须与您添加到项目引用中的DLL和[MySql.Data]的版本号一致:
  • 第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())
        {
          // 显示表中的内容
          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 行:对 BD 的所有操作均通过该数据库的上下文进行。我们使用类 [DbPamContext] 实现了该上下文,并将其称为数据库的持久化上下文
  • 第 13、31 行:对持久化上下文的操作在 [using] 子句中进行。 持久化上下文在 [using] 子句开始时打开,并在该子句结束时自动关闭。这意味着在 [using] 子句中对持久化上下文所做的任何修改,都将在该子句结束时同步到数据库中。 随后,在事务内向 BD 发送了一系列 SQL 命令。 这意味着,如果 SQL 命令失败,则之前发出的所有 SQL 命令都会被取消。此时,EF5 会抛出一个异常;
  • 第 17 行:表达式 [context.Employes] 指代表 [employes] 的对象映像。需要指出的是,[Employes] 是持久化上下文 [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.Employes],因此将把数据库中的所有员工数据导入持久化上下文。 因此,EF5 将发出 SQL SELECT 命令;
  • 第17-20行:遍历员工集合,并在第19行使用类[Employe]中的方法[ToString]将员工信息显示在控制台上;
  • 第21-25行:对津贴集合执行相同操作;
  • 第27-30行:对缴费集合的操作同上。

让我们回顾一下实体 [Employe] 的定义:


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; }

    // 签名
    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 行:一名员工与一项津贴相关联。

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

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


    // 签名
    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 行调用了方法 [Employe.ToString],而该方法引用了实体 [Indemnites]。

执行 [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. DLL 来自 [EF5] 层

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


任务:将项目 [pam-ef5] 的类型转换为类库,然后重新生成该项目。


9.23. 步骤 16:配置 [DAO] 层

9.23.1. [DAO]

正如我们对模拟的 [métier] 层所做的那样,[DAO] 层也将通过一个接口进行访问。这个接口会是什么呢?

让我们看看我们构建的模拟层 [métier] 的接口 [IPamMetier]:


    public interface IPamMetier {
        // 所有员工身份信息列表 
        Employe[] GetAllIdentitesEmployes();

        // ------- 工资计算 
        FeuilleSalaire GetSalaire(string ss, double heuresTravaillées, int joursTravaillés);
}

第 3 行,方法 [GetAllIdentitesEmployes] 用于为首页的下拉列表提供数据:

这些员工的信息需从数据库中检索。

第 6 行,方法 [GetSalaire] 用于计算已知编号为 SS 的员工的工资单。回顾类型 [FeuilleSalaire] 的定义:


  public class FeuilleSalaire
  {

    // 自动属性 
    public Employe Employe { get; set; }
    public Cotisations Cotisations { get; set; }
    public ElementsSalaire ElementsSalaire { get; set; }
}

第5行和第6行的信息将来自数据库。需要说明的是,每位员工都拥有一个[Indemnites]属性。该信息也需一并获取。

因此,我们可以采用以下接口作为 [DAO] 层的起点:


    public interface IPamDao {
        // 所有员工身份列表 
        Employe[] GetAllIdentitesEmployes();
        // 某位特定员工及其津贴 
        Employe GetEmploye(string ss);
        // 所有缴费明细列表 
        Cotisations GetCotisations();
}

9.23.2. Visual Studio 项目


任务:在 [pam-td] 解决方案中添加一个名为 [pam-dao] 的新项目,类型为 [console]。将其设为解决方案的启动项目。


 

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

让我们来了解整个项目的结构:

项目[pam-dao]需要一定数量的DLL:

  • 所有由项目 [pam-ef5] 引用的;
  • 以及 [pam-ef5] 项目本身所包含的那个。

此外,我们将使用 [Spring.net] 来实例化 [DAO] 层。 为此,我们需要 DLL、[Spring.core] 和 [Common.Logging]。 这些 DLL 位于案例研究支持文件中的 [lib] 文件夹内。


任务:将这些不同的参考资料添加到项目 [pam-dao] 中。


9.23.4. [DAO] 层的实现

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


namespace Pam.Dao.Entites
{
  // 异常类
  public class PamException : Exception
  {
....
  }
}

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


using Pam.EF5.Entites;

namespace Pam.Dao.Service
{
  public interface IPamDao
  {
    // 所有员工身份信息列表 
    Employe[] GetAllIdentitesEmployes();
    // 某特定员工及其津贴 
    Employe GetEmploye(string ss);
    // 所有缴费清单 
    Cotisations GetCotisations();
  }
}

类 [PamDaoEF5] 通过 ORM 和 EF5 实现了该接口。其代码如下:


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 Cotisations cotisations;
    private Employe[] employes;

    // 构建器
    public PamDaoEF5()
    {
      // 缴费
      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];
  • 表 [cotisations] 和 [employes] 被缓存于第 13-14 行的属性中。员工未获得其津贴
  • 第17-28行:由构造函数初始化第13-14行;
  • 第43-52行:方法[GetEmploye]返回包含津贴的员工记录。该方法接收该员工的社保号作为参数。如果数据库中不存在该员工,方法将返回指针。

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


构造函数的实现可参考第 9.22.6 节中 [EF5] 层的测试代码。 对于方法 [GetEmploye],应参考第 3.5.7 节中 [Eager and Lazy loading] 与 [refEF5] 的示例。

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
      {
        // [dao] 层实例化
        IPamDao pamDao = (IPamDao)ContextRegistry.GetContext().GetObject("pamdao");
        // 员工身份列表 
        foreach (Employe Employe in pamDao.GetAllIdentitesEmployes())
        {
          Console.WriteLine(Employe.ToString());
        }
            // 一名员工及其津贴 
            Console.WriteLine("------------------------------------");
            Employe e = pamDao.GetEmploye("254104940426058");
            Console.WriteLine("employé= {0}, indemnités={1}", e, e.Indemnites);
            Console.WriteLine("------------------------------------");
        // 一名不存在的员工 
        Employe employe = pamDao.GetEmploye("xx");
        Console.WriteLine("Employé n° xx");
        Console.WriteLine((employe == null ? "null" : employe.ToString()));
        Console.WriteLine("------------------------------------");
        // 缴费清单 
        Cotisations cotisations = pamDao.GetCotisations();
        Console.WriteLine(cotisations.ToString());
      }
      catch (Exception ex)
      {
        // 异常显示 
        Console.WriteLine(ex.ToString());
      }
      //暂停 
      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:设置 [métier] 层

9.24.1. [métier] 层的接口

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


    public interface IPamMetier {
        // 所有员工身份列表 
        Employe[] GetAllIdentitesEmployes();

        // ------- 工资计算 
        FeuilleSalaire GetSalaire(string ss, double heuresTravaillées, int joursTravaillés);
}

9.24.2. Visual Studio 项目


任务:在 [pam-td] 解决方案中添加一个名为 [pam-metier] 的新项目(类型为 [console])。将其设为该解决方案的启动项目。


 

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

让我们来了解整个项目的结构:

项目 [pam-metier] 需要一定数量的 DLL:

  • 所有由项目 [pam-dao] 和 [pam-ef5] 引用的;
  • 以及项目 [pam-dao] 和 [pam-ef5] 本身所包含的。

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


 

9.24.4. [métier] 层的实现

上文提到的四个元素已在模拟的 [métier] 层中使用过(参见第 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
  {

    // 关于由 Spring 初始化的 [DAO] 层的参考
    public IPamDao PamDao { get; set; }

    // 所有员工身份列表 
    public Employe[] GetAllIdentitesEmployes()
    {
      ...
    }

    // 某位特定员工及其津贴 
    public Employe GetEmploye(string ss)
    {
      ...
    }

    // 保险费 
    public Cotisations GetCotisations()
    {
      ...
    }

    // 工资计算 
    public FeuilleSalaire GetSalaire(string ss, double heuresTravaillées, int joursTravaillés)
    {
      // SS:员工的SS编号 
      // HeuresTravaillées:工作小时数 
      // 工作日:工作日数 
...
  }
}
  • 第 13 行:这里引用了 [DAO] 层。该引用将在 [PamMetier] 类实例化时由 Spring 进行初始化。因此,当各种方法执行时,第 13 行已初始化完毕。

任务:完善类 [PamMetier] 的代码。如果在 [GetSalaire] 中发现员工编号为 ss 的员工不存在,则调用 [PamException]。 薪资计算方法详见第9.5节。请注意将所有中间计算结果四舍五入至小数点后两位。


9.24.5. [métier] 层的配置

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


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


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


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


这里存在一个难点。不仅需要使用类 [PamMetier] 来实例化 [métier] 层,还必须初始化其属性 [PamDao]:


    // Spring 初始化的 [DAO] 层引用
    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. 测试 [métier] 层

现在我们可以测试 [métier] 层了。我们将使用已存在的 [Program.cs] 程序进行测试:

我们将测试 [métier] 层界面的各项功能。[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
      {
        // [métier] 层的实例化
        IPamMetier pamMetier = ContextRegistry.GetContext().GetObject("pammetier") as IPamMetier;
        // 员工身份信息列表
        Console.WriteLine("Employés -----------------------------");
        foreach (Employe Employe in pamMetier.GetAllIdentitesEmployes())
        {
          Console.WriteLine(Employe);
        }

        // 工资单计算 
        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));
      }
      // 暂停 
      Console.ReadLine();
    }
  }
}
  • 第16行:通过[Spring.net]获取了[métier]层的引用。

该程序的执行结果如下:

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 来自 [métier] 层


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


9.25. 步骤 18:建立 [web] 层

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

我们将复用之前借助模拟的 [métier] 层开发的 [web] 层。

9.25.1. Visual Studio 项目

我们回到 Visual Studio Express 2012 for Web,将我们的 Web 层与刚刚开发的 [métier, DAO, EF5] 层连接起来。主要需要进行一些配置,并更改几个命名空间。

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

  • 在 [1] 中,将 [pam-td] 解决方案导入 VS Web Studio 中。 Web 项目 [pam-web-01] 再次可见。此前该项目在 VS 桌面版 Studio 中已丢失。
  • 需要修改 Web 项目 [pam-web-01] 的配置。为了避免修改正在运行的项目,我们将对该项目的副本进行修改。首先在 [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] 引用的;
  • 以及项目 [pam-metier]、[pam-dao] 和 [pam-ef5] 本身所引用的。

任务:将这些不同的引用添加到项目 [pam-web-02] 中。项目 [pam-metier-simule] 上的引用应予删除。 切换到 [métier] 层。引用中已存在部分 DLL 条目。请先删除它们,然后进行添加。


 

9.25.3. 实现 [web] 层

生成项目 [pam-web-02]。将出现如下错误:

类 [ApplicationModel] 使用了类型 [Employe]。 在模拟的 [métier] 层中,该类型定义在 [Pam.Metier.Entites] 命名空间中。现在它位于 [Pam.EF5.Entites] 命名空间中。请按照上述说明修正这些错误。

9.25.4. 配置 [web] 层

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


任务 1:将 [web.config] 中的所有现有内容替换为项目 [pam-metier] 中 [app.config] 文件的内容。


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


      try
      {
        // [métier] 层实例化
        application.PamMetier = ContextRegistry.GetContext().GetObject("pammetier") as IPamMetier;
      }
      catch (Exception ex)
      {
        application.InitException = ex;
}

第 4 行,请求获取名为 [pammetier] 的 Spring 对象的引用。这正是 [métier] 层被命名的名称(请在您的 [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] 中的员工信息。 值得注意的是,此前在模拟的 [métier] 层中出现的员工 [X X] 已不复存在。让我们进行一次模拟:

在 [2] 中,我们确实获得了实际工资,而不是虚构的工资。现在停止 SGBD 和 MySQL5,并进行另一项模拟:

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

 

此时将显示第 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. . . :

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

  • 请检查项目 [clic droit sur projet / propriétés / onglet web] 的属性:
 

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

  • 通过 [Ctrl-F5] 运行您的项目

Image

  • 将 [localhost] 替换为该节点的地址 IP:

Image

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

若要使所开发的应用程序可供 URL 类型的 [http://adresseIP/contexte/...] 访问,必须使用 IIS Express 以外的其他服务器,例如 IIS 服务器(非 Express 版)。 要检查该服务器是否存在(通常在 Windows Pro 版本中),请进入 [Panneau de configuration\Système et sécurité\Outils d’administration] 控制面板:

Image

该选项并非总是存在。此时需进入 [ Panneau de configuration \ Programmes] 并安装 Web 管理工具。

一旦出现 [Gestionnaire des services internet (IIS)] 选项,请将其启用:

启动默认网站。为此,必须先启动 [Service de publication World Wide Web] 服务:

完成上述操作后,请使用浏览器访问 URL [http://localhost]。请先确认端口 80 是否已被其他 Web 服务器占用。若已被占用,请将其停止。

服务器 IIS 已回复。现在请将 [localhost] 替换为您设备的地址 IP:

成功了。现在回到 Visual Studio:

  • 首先,需以 [administrateur] 模式启动 Visual Studio

完成上述操作后,需要修改要部署的 Web 项目的配置 [clic droit sur projet / propriétés / onglet web]:

需选择本地服务器作为部署服务器。Visual Studio 会自动设置应用程序的路径,该路径可进行修改。通过以下命令运行项目:

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

如果没有 IIS 服务器,可以使用 URL 提供的免费服务器(如 [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 服务器的自动启动:

  • 选择选项 [Panneau de configuration\Système et sécurité\Outils d’administration];
  • [1, 2]:选择服务 [Ultidev Web Server Pro] 的属性;
  • [3]:将其设置为手动启动。

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

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

当拥有 APU 类型的 Web 应用程序(单页应用程序)时,可以使用工具 [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 平板电脑,该平板电脑与一个或多个 [2] 网络服务进行通信;

让我们回到 APU 模型:

  • 浏览器中加载了一个初始页面(上图未说明其来源);
  • 后续视图通过 Ajax 调用 [1-4] 获取。浏览器不会加载任何新页面;

初始视图可能由与通过 Ajax 调用获取的其他视图相同的服务器提供,也可能不是。如果不是由同一服务器提供,则初始页面的 JavaScript 必须知道将提供其他视图的 Web 服务器的 URL。在我们即将构建的 Android 应用中就是这种情况:

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

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

  • 在 [1] 中,应用程序初始页面的 URL。该值将由第 9.26 节中讨论的 Ultidev 服务器提供;
  • 在 [2] 中,用户需输入薪资模拟器的 URL。 虽然可以将其硬编码到首页的 JavaScript 代码中,但这会增加测试的复杂性:一旦更改了地址模拟器 IP(或端口),就需要在 JavaScript 代码中相应地进行修改;
  • 改为 [3],该链接 [Connexion] 将加载下一视图:
  • 需要注意的是,在 [4] 时,浏览器的 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 行:添加了菜单选项 [Connexion],用于连接模拟服务;
  • 第 55-56 行:添加了模拟器 URL 的输入字段;
  • 第59-63行:若连接失败,则显示错误提示;

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


// 加载文档时
$(document).ready(function () {
    // 获取页面中各个组件的引用
    loading = $("#loading");
    content = $("#content");
    erreur = $("#erreur");
    erreur1 = $("#erreur1");
    erreur2 = $("#erreur2");
    // 菜单链接
    lnkConnexion = $("#lnkConnexion");
    lnkFaireSimulation = $("#lnkFaireSimulation");
    lnkEffacerSimulation = $("#lnkEffacerSimulation");
    lnkEnregistrerSimulation = $("#lnkEnregistrerSimulation");
    lnkVoirSimulations = $("#lnkVoirSimulations");
    lnkTerminerSession = $("#lnkTerminerSession");
    lnkRetourFormulaire = $("#lnkRetourFormulaire");
    // 将其放入数组中
    options = [lnkConnexion, lnkFaireSimulation, lnkEffacerSimulation, lnkEnregistrerSimulation, lnkVoirSimulations, lnkTerminerSession, lnkRetourFormulaire];
    // 隐藏页面中的某些元素
    loading.hide();
    erreur.hide();
    // 固定菜单
    setMenu([lnkConnexion]);
});
  • 第 6-8 行:用于在页面 [index.html] 中显示连接错误的区域标识符;
  • 第 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() {
    // 获取Web服务的urlServiceWeb
    urlServiceWeb = $("#urlServiceWeb").val();
    // 获取输入表单
    $.ajax({
        url: urlServiceWeb + '/Pam/Formulaire',
        type: 'POST',
        dataType: 'html',
        beforeSend: function () {
            // 等待信号亮起
            loading.show();
        },
        success: function (data) {
            // 显示结果
            content.html(data);
            // 菜单
            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 () {
            // 等待信号熄灭
            loading.hide();
        }
    });
}
  • 第7行:获取用户输入的URL。将其赋值给第1行的全局变量。这样,该值在文件中的其他函数中即可被识别;
  • 第10行:通过Ajax调用模拟器的URL [/Pam/Formulaire]。该URL返回模拟信息录入的局部视图(员工、工作小时数、工作日数)。 在 [pam-web-02] 的初始版本中,仅此 URL 便已足够。它会自动由 URL 作为前缀,而 URL 此前已加载了初始页面。 现在,我们假设初始页面可能由支持模拟器的服务器以外的其他服务器提供。 因此,需要将第1行中的变量[urlServiceWeb](即模拟器的URL)作为前缀添加到URL和[/Pam/Formulaire]前 (例如,http://172.19.81.34/pam-web-02)。该文件中的所有 Ajax 调用都必须进行此操作
  • 第 17-22 行:若连接成功,则显示部分视图 [Formulaire.cshtml],并显示一个仅包含链接 [Faire la simulation] 的菜单(第 21 行);
  • 第23-27行:若连接失败:
    • 第 24 行,显示 Web 服务器发送的响应 HTML(如有);
    • 第25行,显示Web服务器发送的HTTP头部信息(如果服务器已响应);

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

此时我们处于前文所述的情形,视图现通过 Ajax 调用获取。因此如上所述,点击链接 [Faire la simulation] 将由文件 [myScripts.js] 中的以下代码执行:


function faireSimulation() {
    // 正在检索参考信息
    var simulation = $("#simulation");
    var formulaire = $("#formulaire");
    // 表单有效吗?
    var formValid = formulaire.validate().form();
    if (!formValid) return;
    // 手动发起 Ajax 请求
    $.ajax({
        url: urlServiceWeb + '/Pam/FaireSimulation',
        type: 'POST',
        data: formulaire.serialize(),
        dataType: 'html',
        ...
    });
    // 菜单
    setMenu([lnkEffacerSimulation, lnkEnregistrerSimulation, lnkTerminerSession, lnkVoirSimulations]);
}
  • 仅进行了一处修改,即第10行中,原先的URL现在被模拟器的前缀所取代;

9.27.3. 重构项目的测试

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

  • 在 [6] 中,我们请求显示页面 [bootstrap/index.html]。结果显示如下:

输入一个错误的 URL:

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

如果输入正确的 URL:

将获得以下响应:

9.27.4. 创建 Android 二进制文件

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

我们添加一个名为[2]的文件,该文件将用于配置[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] 中的内容打包为压缩文件:

 

随后访问 Phonegap 网站 [http://build.phonegap.com/apps]:

  • 在访问 [1] 之前,您可能需要创建一个账户;
  • 在 [1] 页面,开始操作;
  • 在 [2] 步骤中,选择仅支持一个 Phonegap 应用的免费套餐;
  • 在 [3] 步骤中,下载压缩包 [4];
  • 在 [5] 中,输入应用程序名称;
  • 点击链接 [6] 生成 OS、IoS、Android 和 Windows 版本的二进制文件。此过程可能需要几秒钟;
  • 在 [7-9] 中,下载 Android 二进制文件;

启动适用于 Android 平板电脑的模拟器(参见第 11.1 节):

 

上图展示了使用 Android 21 版本的 API 启动平板电脑模拟器。模拟器启动后,

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

按照第 9.27.3 节所述,将模拟器的 URL 设置为 [1]。完成此操作后,通过链接 [2] 登录模拟器:

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