Skip to content

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

4.1. 简介

我们希望编写一个 .NET 应用程序,允许用户模拟某市“Maison de la petite enfance”协会中儿童保育人员的薪资计算。

该 ASP.NET 薪资计算表单 的外观如下:

Image

该 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] 事件后,接下来处理 [Salary] 按钮的 [Click] 事件。该事件需要 [Global] 中未缓存的数据。因此,事件处理程序从数据库中检索数据。这就是上文中的操作 3。随后计算薪资,操作 4 将结果发送给用户。

4.2. Visual Web Developer 2008

  • 在 [1] 中,创建一个新项目
  • 在 [2] 中,选择类型为 [Web / ASP.NET Web Application]
  • 在 [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]


//------------------------------------------------------------------------------
// <auto-generated>
//      This code was generated by a tool.
//      Runtime version :2.0.50727.3603
//
//      Changes made to this file may result in incorrect behavior and will be lost if
//      the code is regenerated.
// </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
下拉列表
员工下拉列表
包含员工姓名列表
2
文本框
TextBoxHours
实际工作小时数
3
文本框
TextBoxDays
工作天数 – 整数
4
按钮
ButtonSalary
计算薪资
5
文本框
错误文本框
用户信息提示
ReadOnly=true, TextMode=MultiLine
6
标签
标签名称
在 (1) 中选定的员工姓名
7
标签
LabelFirstName
在(1)中选定的员工的名字
8
标签
LabelAddress
地址
9
标签
标签城市
城市
10
标签
邮政编码
邮政编码
11
标签
标签索引
索引
12
标签
标签CSGRDS
CSGRDS 贡献率
13
标签
CSGD 标签
CSGD 贡献率
14
标签
退休标签
退休供款率
15
标签
标签SS
社会保障缴费率
16
标签
LabelSH
(11)中所示指数的基准时薪
17
标签
标签EJ
第(11)项所示指数的每日生活津贴
18
标签
LabelRJ
第(11)项所示指数的每日伙食津贴
19
标签
LabelVacation
适用于基本工资的带薪休假津贴率
20
标签
标签SB
基本工资金额
21
标签
标签CS
应缴纳的社会保险费金额
22
标签
标签IE
育儿津贴金额
23
标签
标签IR
受托儿童的伙食津贴金额
24
标签
标签SN
应支付给员工的净工资

该表单还包含两个 [Panel] 容器:

PanelErrors
包含 (5) 个 TextBoxError 组件
PanelSalary
包含组件 (6) 至 (24)

可以通过其布尔属性 [Panel].Visible 通过编程方式将 [Panel] 组件设为可见或隐藏。

4.2.2. 输入验证

要计算薪资,用户需:

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

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

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

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

如果客户端浏览器本身不验证数据的有效性,则数据仅在浏览器将表单条目提交至服务器时才会被验证。此时,处理浏览器请求的服务器端代码将负责验证数据的有效性。 请注意,即使客户端浏览器中显示的页面包含执行相同验证的 JavaScript 代码,也必须始终执行此验证。这是因为服务器无法确定其收到的 POST 请求是否确实来自该页面,因此也无法确定数据是否已经过验证。

验证者名单如下:

编号
类型
名称
角色
25
必填字段验证器
必填字段验证器(小时)
检查 [2] [TextBoxHeures] 字段是否为空
26
范围验证器
RangeValidatorHours
检查字段 [2] [TextBoxHours] 是否为 [0, 200] 范围内的实数
27
必填字段验证器
必填字段验证器(天数)
检查字段 [3] [TextBoxDays] 是否不为空
28
范围验证器
RangeValidatorDays
检查 [3] [TextBoxDays] 字段是否为 [0,31] 范围内的整数

任务:构建 [Default.aspx] 页面。首先,放置两个容器 [PanelErrors] 和 [PanelSalary],以便随后放置它们应包含的组件。


4.2.3. 应用程序实体

读取后,来自 [contributions]、[employees] 和 [allowances] 表的行将存储在类型为 [Contributions]、[Employee] 和 [Allowances] 的对象中,这些对象定义如下:


namespace Pam.Entites
{
  public class Cotisations
  {
    // automatic properties
    public double CsgRds { get; set; }
    public double Csgd { get; set; }
    public double Secu { get; set; }
    public double Retraite { get; set; }
 
    // manufacturers
    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
  {
    // automatic properties
    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; }
 
    // manufacturers
    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
  {
 
    // automatic properties
    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; }
 
    // manufacturers
    public Indemnites()
    {
 
    }
 
    public Indemnites(int indice, double baseHeure, double entretienJour, double repasJour, double indemnitesCP)
    {
      Indice = indice;
      BaseHeure = baseHeure;
      EntretienJour = entretienJour;
      RepasJour = repasJour;
      IndemnitesCP = indemnitesCP;
    }
 
    // identity
    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 Server 数据库的连接字符串
  • 第 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
  {
    // --- static application data ---
    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;
 
    // application startup
    public void Application_Start(object sender, EventArgs e)
    {
...
      try
      {
        // connection
        ConnectionString = ...
        using (SqlConnection connexion = new SqlConnection(ConnectionString))
        {
          connexion.Open();
          // retrieve the list of employees and place it in the static array [Employees]
...
          // contribution rates are retrieved from the static variable [Contributions]
...
          // retrieve allowances from the static dictionary [Allowances]
...
          // we succeeded
          Msg = "Base chargée...";
        }
      }
      catch (Exception ex)
      {
        // we note the error
        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 行:类的 public 和 static 字段。static 字段由类的所有实例共享。因此,如果创建了多个 [Global] 类的实例,它们都将共享同一个 static 字段 [Employees],可通过引用 [Global.Employees] 访问,即 [ClassName].StaticField。 [Global] 是类名,因此它是一种数据类型。该名称可任意设定。该类始终继承自 [System.Web.HttpApplication]。

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

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


            Application.Add("employes",Employes);

默认情况下,在任何 ASP.NET 应用程序中,[Application] 都是对 [Global.asax.cs] 中定义的类的实例的引用。Application 是一个能够存储任何类型对象的容器。随后,可以在 Web 应用程序中任何页面的代码中按如下方式检索 Employees 数组:


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

由于 Application 容器存储任何类型的对象,因此我们获取到的对象类型为 Object,必须进行类型转换。虽然这种方法始终可用,但通过 [Global] 对象的类型化静态字段共享数据可以避免类型转换,并允许编译器执行类型检查以辅助开发人员。本文将采用这种方法。

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

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

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


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

4.3.1. [P age_Load] 过程

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

  • 下拉列表 [1] 已填充完毕
  • 文本框 [5] 表明数据库已正确读取

如果应用程序启动时发生初始化错误,文本框 [5] 将显示以下内容:

Image


问题:为 [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] 过程的代码。