Skip to content

4. 应用程序 [SimuPaie] – 版本 1 – ASP.NET

4.1. Introduction

我们希望编写一个名为 .NET 的应用程序,让用户能够模拟计算某市镇“幼儿之家”协会的保育员工资。

工资计算表单 (ASP.NET)将呈现如下样式:

应用程序 ASP.NET 的架构如下:

  • 在首次向应用程序发起请求时,将实例化一个从类型 [System.Web.HttpApplication] 派生而来的 [Global] 类型对象。 该对象将读取 Web 应用程序的配置文件 [web.config],并将数据库中的某些数据缓存起来(即上文所述的操作 0)。
  • 当首次向应用程序的唯一页面 [Default.aspx] 发出请求(操作 1)时,将处理事件 [Load]。该事件负责处理员工下拉列表的填充。 下拉列表所需的数据从已将其缓存的对象 [Global] 中获取。即上文中的操作 2。操作 4 将初始化好的页面发送给用户。
  • 当向页面 [Default.aspx] 发起薪资计算请求(操作 1)时,将再次处理事件 [Load]。 此时不会执行任何操作,因为这是 POST 类型的请求,而处理程序 [Pam_Load] 被设计为在此情况下不执行任何操作(通过布尔值 IsPostBack 实现)。 处理完事件 [Load] 后,接下来处理的是按钮 [Salaire] 上的事件 [Click]。 该事件需要一些未在 [Global] 中缓存的数据。因此,事件处理程序会向数据库请求这些数据。这就是上文中的操作 3。随后进行薪资计算,操作 4 将结果发送给用户。

4.2. Visual Web Developer 2008 项目

  • 在 [1] 中,创建了一个新项目
  • 在 [2] 中,类型为 [Web / Application Web ASP.NET]
  • 在 [3] 中,为项目命名
  • 在 [4] 中,指定其名称,并在 [5] 中指定其位置。将为该项目创建一个名为 [c:\temp\pam-aspnet\pam-v1-adonet] 的文件夹。
  • 在 [5] 中,Visual Web Developer 项目
  • 在 [6] 中,项目属性(右键单击项目 / 属性 / 应用程序)。
  • 在 [7] 中,即项目生成时将生成的程序集名称
  • 在 [8] 中,我们希望为项目类设置的默认命名空间。 在文件 [Default.aspx.cs] 和 [Default.aspx.designer.cs] 中定义的类 [_Default] 则创建在从项目名称派生的命名空间 [pam_v1_adonet] 中。 我们可以直接在上述两个文件的代码中修改该命名空间:

[Default.aspx.cs]


using System;
....

namespace pam_v1
{
  public partial class _Default : System.Web.UI.Page
  {

[Default.aspx.designer.cs]


//------------------------------------------------------------------------------
// <自动生成>
//      此代码由工具生成。
//      运行时版本:2.0.50727.3603
//
//      对该文件所做的修改可能会导致行为异常,并且如果
//      代码重新生成时,这些修改将被丢失。
// </auto-generated>
//------------------------------------------------------------------------------

namespace pam_v1 {
    
    
    public partial class _Default {
        
.........

文件 [Default.aspx] 的标记也需进行修改:

  

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="pam_v1._Default" %>
...

上文中的 Inherits 属性指代在 [Default.aspx.cs] 和 [Default.aspx.designer.cs] 文件中定义的类。其中使用了 pam_v1 命名空间。

4.2.1. 表单 [Default.aspx]

表单 [Default.aspx] 的外观如下:

组件如下:

编号
类型
名称
作用
1
DropDownList
ComboBoxEmployes
包含员工姓名列表
2
TextBox
TextBoxHeures
实际工作小时数
3
TextBox
TextBoxJours
工作天数 – 整数
4
按钮
ButtonSalaire
请求计算工资
5
TextBox
TextBoxErreur
致用户的通知
ReadOnly=true, TextMode=MultiLine
6
标签
LabelNom
在 (1) 中选定的员工姓名
7
标签
LabelPrénom
在(1)中选定的员工名字
8
标签
LabelAdresse
地址
9
标签
LabelVille
城市
10
标签
LabelCP
邮政编码
11
标签
LabelIndice
索引
12
标签
LabelCSGRDS
缴费率CSGRDS
13
标签
LabelCSGD
缴费率 CSGD
14
标签
LabelRetraite
养老保险缴费率
15
标签
LabelSS
社会保险缴费率
16
标签
LabelSH
(11)中所示指数的基本时薪
17
标签
LabelEJ
(11)中所示指数的每日生活津贴
18
标签
LabelRJ
针对(11)中所示指数的每日餐费补贴
19
标签
LabelCongés
适用于基本工资的带薪休假补贴率
20
标签
LabelSB
基本工资金额
21
标签
LabelCS
应缴社会保险费金额
22
标签
LabelIE
受托儿童抚养费金额
23
标签
LabelIR
受托儿童的餐费金额
24
标签
LabelSN
应付给员工的净工资

该表格还包含两个类型为 [Panel] 的容器:

PanelErreurs
包含组件 (5) TextBoxErreur
PanelSalaire
包含组件 (6) 至 (24)

类型为 [Panel] 的组件可通过其布尔属性 [Panel].Visible 进行编程控制,使其可见或不可见。

4.2.2. 输入验证

要计算工资,用户:

  • 在 [1] 中选择一名员工
  • 在 [2] 中输入工作小时数。该数值可以是小数,例如 2.5 表示 2 小时 30 分钟。
  • 在 [3] 中输入工作日数。该数值为整数。
  • 点击按钮 [4] 查询工资

当用户在 [2] 和 [3] 中输入错误数据时,系统会在用户切换输入字段的瞬间进行验证。 因此,下图的屏幕截图是在用户点击按钮 [Salaire] 之前捕获的:

要实现上述行为,需要满足两个条件:

  • 验证组件的 EnableClientScript 属性必须设置为 true:
  
  • 显示该页面的浏览器必须能够执行页面中嵌入的 JavaScript 代码 HTML。

如果客户端浏览器未自行验证数据的有效性,则数据仅在浏览器将表单输入提交至服务器时才会被验证。届时,位于服务器端并处理浏览器请求的代码将负责验证数据的有效性。 需要提醒的是,即使客户端浏览器中显示的页面嵌入了执行相同验证的 JavaScript 代码,也必须始终进行此项验证。因为服务器无法确保发给它的 POST 请求确实来自该页面,因此无法保证数据验证已经完成。

验证器列表如下:

编号
类型
名称
角色
25
RequiredFieldValidator
RequiredFieldValidatorHeures
检查字段 [2] [TextBoxHeures] 是否为空
26
RangeValidator
RangeValidatorHeures
检查字段 [2] [TextBoxHeures] 是否为 [0, 200] 区间内的实数
27
RequiredFieldValidator
RequiredFieldValidatorJours
验证字段 [3] [TextBoxJours] 是否不为空
28
RangeValidator
RangeValidatorJours
检查字段 [3] [TextBoxJours] 是否为 [0,31] 区间内的整数

待完成的工作:构建页面 [Default.aspx]。首先放置两个容器 [PanelErreurs] 和 [PanelSalaire],以便随后将它们应包含的组件放置其中。


4.2.3. 应用程序的实体

读取后,表 [cotisations]、[employes]、[indemnites] 中的行将被放入类型为 [Cotisations] 的对象中, [Employe] 和 [Indemnites] 类型的对象中,这些对象的定义如下:


namespace Pam.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 Cotisations()
    {
    }

    public Cotisations(double csgRds, double csgd, double secu, double retraite)
    {
      CsgRds = csgRds;
      Csgd = csgd;
      Secu = secu;
      Retraite = retraite;
    }

    // ToString
    public override string ToString()
    {
      return string.Format("[{0},{1},{2},{3}]", CsgRds, Csgd, Secu, Retraite);
    }
  }
}

namespace Pam.Entites
{
  public class Employe
  {
    // 自动属性
    public string SS { get; set; }
    public string Nom { get; set; }
    public string Prenom { get; set; }
    public string Adresse { get; set; }
    private string Ville { get; set; }
    private string CodePostal { get; set; }
    private int Indice { get; set; }

    // 制造商
    public Employe()
    {

    }

    public Employe(string ss, string nom, string prenom, string adresse, string codePostal, string ville, int indice)
    {
      SS = ss;
      Nom = nom;
      Prenom = prenom;
      Adresse = adresse;
      CodePostal = codePostal;
      Ville = ville;
      Indice = indice;
    }

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

namespace Pam.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 Indemnites()
    {

    }

    public Indemnites(int indice, double baseHeure, double entretienJour, double repasJour, double indemnitesCP)
    {
      Indice = indice;
      BaseHeure = baseHeure;
      EntretienJour = entretienJour;
      RepasJour = repasJour;
      IndemnitesCP = indemnitesCP;
    }

    // 标识
    public override string ToString()
    {
      return string.Format("[{0}, {1}, {2}, {3}, {4}]", Indice, BaseHeure, EntretienJour, RepasJour, IndemnitesCP);
    }

  }
}

4.2.4. 应用程序配置

用于配置应用程序的文件 [Web.config] 内容如下:


<?xml version="1.0" encoding="utf-8"?>

<configuration>
    <configSections>
...
    </configSections>

  <connectionStrings>
    <add name="dbpamSqlServer2005" connectionString="Data Source=.\SQLEXPRESS;AttachDbFilename=C:\data\...\dbpam.mdf;User Id=sa;Password=msde;Connect Timeout=30;" providerName="System.Data.SqlClient"/>
  </connectionStrings>

  <appSettings>
    <add key="selectEmploye" value="select NOM,PRENOM,ADRESSE,VILLE,CODEPOSTAL,INDICE from EMPLOYES where SS=@SS"/>
    <add key="selectEmployes" value="select PRENOM, NOM, SS from EMPLOYES"/>
    <add key="selectCotisations" value="select CSGRDS,CSGD,SECU,RETRAITE from COTISATIONS"/>
    <add key="SelectIndemnites" value="select INDICE,BASEHEURE,ENTRETIENJOUR,REPASJOUR,INDEMNITESCP from INDEMNITES"/>
  </appSettings>

  <system.web>
        <!-- 
            Définissez compilation debug="true" pour insérer des symboles 
            de débogage dans la page compilée. Comme ceci 
            affecte les performances, définissez cette valeur à true uniquement 
            lors du développement.
        -->
        <compilation debug="false">
...
</configuration>
  • 第 9 行:定义连接 SQL 服务器的连接字符串
  • 第 13-16 行:定义应用程序使用的 SQL 查询,以避免将其硬编码在代码中。
  • 第 13 行:配置了 SQL 查询。该参数的表示法是 SQL Server 特有的。

待完成工作:将这些参数引入文件 [Web.config]。第 9 行将根据 [dbpam.mdf] 数据库的实际路径进行调整。


4.2.5. 应用程序初始化

ASP.NET 应用程序的初始化由文件 [Global.asax.cs] 完成。该文件的结构如下:

  • 在 [1] 中,向项目添加一个新元素
  • 在 [2] 中,添加全局应用程序类,其默认名称为 [Global.asax] [3]
  • 在 [4] 中,生成文件 [Global.asax] 及其关联类 [Global.asax.cs]
  • 在 [5] 中,显示 [Global.asax] 的标记

<%@ Application Codebehind="Global.asax.cs" Inherits="pam_v1.Global" Language="C#" %>

类 [pam_v1.Global] 在文件 [Global.asax.cs] 中定义。针对我们的问题,其定义如下:


using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data.SqlClient;
using Pam.Entites;

namespace pam_v1
{
  public class Global : System.Web.HttpApplication
  {
    // --- 应用程序静态数据 ---
    public static Employe[] Employes;
    public static Cotisations Cotisations;
    public static Dictionary<int, Indemnites> Indemnites = new Dictionary<int, Indemnites>();
    public static string Msg = string.Empty;
    public static bool Erreur = false;
    public static string ConnectionString = null;

    // 应用程序启动
    public void Application_Start(object sender, EventArgs e)
    {
...
      try
      {
        // 登录
        ConnectionString = ...
        using (SqlConnection connexion = new SqlConnection(ConnectionString))
        {
          connexion.Open();
          // 获取员工列表并将其放入静态数组 [Employes]
...
          // 从静态变量中获取缴费率 [Cotisations]
...
          // 从静态字典中提取津贴,存储在静态字典 [Indemnites] 中
...
          // 操作成功
          Msg = "Base chargée...";
        }
      }
      catch (Exception ex)
      {
        // 记录错误
        Msg = string.Format("L'erreur suivante s'est produite lors de l'accès à la base de données : {0}", ex);
        Erreur = true;
      }
    }
  }
}
  • 第 20 行:Web 应用程序启动时将执行方法 [Application_Start]。该方法仅执行一次。
  • 第 12-17 行:类的公共字段和静态字段。静态字段由该类的所有实例共享。 因此,如果创建了多个 [Global] 类的实例,它们都将共享同一个静态字段 [Employes],可通过引用 [Global.Employes]、c.a.d 访问。 [NomDeClasse].ChampStatique. [Global] 是一个类名。因此它是一种数据类型。该名称是任意的。该类始终继承自 [System.Web.HttpApplication]。

让我们回到 [Global] 类。有人可能会问,是否有必要将其字段声明为静态。事实上,在某些情况下,[Global] 类可能会有多个实例,因此将所有这些实例需要共享的字段设为静态是合理的。

还有另一种方法可以在 Web 应用程序的不同页面之间共享“应用程序”作用域的数据。因此,员工数组 Employes 可以按以下方式存储在过程 Application_Start 中:


            Application.Add("employes",Employes);

在所有 [Application] 应用程序中,ASP.NET 默认是 [Global.asax.cs] 定义的类的一个实例的引用。Application 是一个可以存储任何类型对象的容器。 随后,可以在 Web 应用程序的任意页面代码中通过以下方式获取数组 Employes


            Employe[] employes=Application.Get["employes"] as Employe[];

由于容器 Application 存储了各种类型的对象,因此获取到的类型为 Object,随后需要对其进行类型转换。 虽然这种方法依然可行,但通过 [Global] 对象的静态类型字段共享数据,可以避免类型转换,并让编译器进行类型检查,从而为开发人员提供帮助。本文将采用这种方法。

所有用户共享的数据如下:

  • 第12行:类型为[Employe]的对象数组,用于存储所有员工的简化列表(SS、NOM、PRENOM)
  • 第13行:类型为[Cotisations]的对象,用于存储缴费率
  • 第14行:字典,用于存储与员工各项指数相关的津贴。该字典将按员工指数进行索引,其值类型为[Indemnites]
  • 第15行:一条消息,指示初始化结果(成功或出错)
  • 第16行:一个布尔值,用于指示初始化是否以错误结束
  • 第17行:连接数据库的字符串。

问题:完善类 [Application_Start] 的代码。


4.3. 表单 [Default.aspx] 的事件

4.3.1. 过程 [Page_Load]

当表单 [Default.aspx] 加载时,它会从数组 [Global.Employes](第 12 行)中检索员工姓名,并将它们放入下拉列表 [1] 中:

  • 下拉列表 [1] 已填充
  • TextBox [5] 表示数据库已正确读取

如果应用程序启动时发生初始化错误,TextBox [5] 会显示:


问题:编写网页 [Default.aspx] 中的 [Page_Load] 过程,该过程在应用程序启动时执行,以确保上述功能正常运行。


4.3.2. 工资计算

点击按钮 [4] 将触发以下管理程序的执行:


    protected void ButtonSalaire_Click(object sender, System.EventArgs e)

该管理程序首先会验证在 [2] 和 [3] 中输入的数据是否有效。如果其中任何一项有误,系统将如前所述发出错误提示。 一旦验证了 [2] 和 [3] 中的输入并确认有效,应用程序应显示在 [1] 中选定用户的补充数据及其工资(参见第 4.1 节的屏幕截图)。


问题:编写过程 [ButtonSalaire_Click] 的代码。