9. Caso de estudio
9.1. Introduction
Vamos a presentar un caso de estudio ya publicado en un artículo disponible en el URL [http://tahe.developpez.com/dotnet/pam-aspnet/]. En este artículo, el caso de estudio se lleva a cabo con ASP.NET clásico y el ORM NHibernate. Aquí lo realizaremos con ASP.NET, MVC y ORM de Entity Framework. Al igual que en el artículo existente, el caso de estudio se presenta como un proyecto universitario. Por lo tanto, está dirigido a estudiantes. Para todas las preguntas, se incluyen referencias a los capítulos que acabamos de detallar para indicar lecturas útiles.
9.2. El problema a resolver
Queremos desarrollar una aplicación web que permita a un usuario realizar simulaciones del cálculo de la nómina de las cuidadoras infantiles de la asociación «Maison de la petite enfance» de un municipio. Nos centraremos tanto en la organización del código DotNet de la aplicación como en el código en sí.
La aplicación será del tipo APU [Application à Page Unique] y utilizará exclusivamente llamadas Ajax para comunicarse con el servidor. Presentará al usuario las siguientes vistas:
- la vista [VueSaisies], que muestra el formulario de simulación

- la vista [VueSimulation], utilizada para mostrar el resultado detallado de la simulación:

- la vista [VueSimulations], que muestra la lista de simulaciones realizadas por el cliente

- la vista [VueSimulationsVides], que indica que el cliente no tiene simulaciones o ya no las tiene:

- la vista [VueErreurs], que indica uno o varios errores (en este caso, se detuvieron las simulaciones SGBD y MySQL):

9.3. Arquitectura de la aplicación
La arquitectura de la aplicación será la siguiente:
![]() |
La capa [EF5] hace referencia al Entity Framework 5 de ORM. El SGBD que se utilizará será MySQL.
Construiremos esta aplicación primero con una capa [métier] simulada:
![]() |
Esto nos permitirá enfocarnos únicamente en la capa [web]. La capa simulada [métier] cumplirá con la interfaz de la capa real [métier]. Cuando la capa [web] esté operativa, se crearán las capas [métier], [DAO] y [EF5].
9.4. La base de datos
Los datos estáticos necesarios para generar la nómina se almacenan en una base de datos MySQL denominada [dbpam_ef5] (pam = Nómina de cuidadoras infantiles). Esta base de datos tiene un administrador llamado root sin contraseña. Cuenta con tres tablas:

Existe una relación de clave externa entre la columna EMPLOYES (INDEMNITE_ID) y la columna INDEMNITES (ID). La estructura de esta base de datos viene determinada por su uso con EF5. Volveremos a este tema cuando construyamos las capas inferiores de la aplicación.
Estructura:
![]() |
|
Su contenido podría ser el siguiente:
![]()
Estructura:
![]() |
|
Su contenido podría ser el siguiente:
![]()
Las tasas de las cotizaciones sociales son independientes del empleado. La tabla anterior solo tiene una fila.
![]() |
|
Su contenido podría ser el siguiente:
![]()
9.5. Método de cálculo del salario de una cuidadora infantil
A continuación, presentamos el método de cálculo del salario mensual de una cuidadora infantil. Tomamos como ejemplo el salario de la Sra. Marie Jouveinal, quien trabajó 150 horas durante 20 días en el mes a pagar.
Se toman en cuenta los siguientes elementos: | [TOTALHEURES]: total de horas trabajadas en el mes [TOTALJOURS]: total de días trabajados en el mes | [TOTALHEURES]=150 [TOTALJOURS] = 20 |
El salario base de la niñera se calcula mediante la siguiente fórmula: | [SALAIREBASE]=([TOTALHEURES]*[BASEHEURE])*(1+[INDEMNITESCP]/100) | [SALAIREBASE] = (150 * [2.1]) * (1 + 0,15) = 362,25 |
De este salario base se deben deducir una serie de contribuciones sociales: | Contribución social generalizada y contribución para el pago de la deuda social: [SALAIREBASE]*[CSGRDS/100] Contribución social general deducible: [SALAIREBASE]*[CSGD/100] Seguridad social, viudedad, vejez: [SALAIREBASE]*[SECU/100] Pensión complementaria + AGPF + Seguro de desempleo: [SALAIREBASE]*[RETRAITE/100] | CSGRDS: 12,64 CSGD: 22,28 Seguridad social: 34,02 Pensión: 28,55 |
Total de contribuciones sociales: | [COTISATIONSSOCIALES] = [SALAIREBASE] *(CSGRDS + CSGD + SECU + RETRAITE)/100 | [COTISATIONSSOCIALES]=97,48 |
Además, la cuidadora infantil tiene derecho, por cada día trabajado, a una indemnización por gastos de manutención y a una indemnización por comida. Por este concepto, recibe las siguientes indemnizaciones: | [Indemnités]=[TOTALJOURS]*(ENTRETIENJOUR+REPASJOUR) | [INDEMNITES]=104 |
En definitiva, el salario neto que se le debe pagar a la niñera es el siguiente: | [SALAIREBASE] - [COTISATIONSSOCIALES] + [INDEMNITÉS] | [salaire NET]=368,77 |
9.6. El proyecto de Visual Studio de la capa [web]
El proyecto de Visual Web Developer de la aplicación será el siguiente:
![]() |
- en [1], la estructura general del proyecto [pam-web-01];
- en [2], la carpeta [Content] es la carpeta donde se colocan los recursos estáticos del proyecto:
- [indicator.gif]: la imagen animada que se muestra mientras se espera a que finalice una solicitud Ajax,
- [standard.jpg]: la imagen de fondo de las diferentes vistas,
- [Site.css]: la hoja de estilo de la aplicación;
- en [3], el único controlador de la aplicación [PamController];
- en [4], las clases necesarias para la aplicación pero que no pueden clasificarse como elementos de MVC:
- [ApplicationModelBinder]: la clase que permite incluir los datos del ámbito [Application] en el modelo de acciones,
- [SessionModelBinder]: la clase que permite incluir los datos del ámbito [Session] en el modelo de acciones,
- [Static]: una clase de apoyo con métodos estáticos;
- en [5], las plantillas de la aplicación, ya sean plantillas de acciones o de vistas:
- [ApplicationModel]: modelo que contiene los datos del ámbito [Application],
- [SessionModel]: modelo que contiene los datos del ámbito [Session],
- [Simulation]: clase que encapsula los elementos de una simulación de cálculo de salario,
- [IndexModel]: modelo de la primera vista [Index] que muestra la aplicación;
- en [6], los scripts JS necesarios para la globalización de la aplicación;
- en [7], los scripts JS de la familia JQuery necesarios para la internacionalización, la validación del lado del cliente y la implementación de AJAX en la aplicación;
- en [8], [myScripts.js] es el archivo que contiene nuestros propios scripts JS;
- en [9], las vistas de la aplicación:
- [Index]: la página de inicio;
- [Formulaire]: formulario para ingresar los datos del empleado y sus horas y días trabajados,
- [Simulation]: la vista que muestra una simulación,
- [Simulations]: la vista que muestra la lista de simulaciones realizadas,
- [Erreurs]: la vista que muestra la lista de posibles errores,
- [InitFailed]: la vista que muestra mensajes de error si falla la inicialización de la aplicación;
- en [10], la página principal de la aplicación [_Layout];
- en [11], los archivos [Web.config] y [Global.asax] utilizados para configurar la aplicación.
9.7. Paso 1: configuración de la capa simulada [métier]
A partir de ahora, describimos los pasos a seguir para realizar el caso de estudio. Cuando sea necesario, indicaremos el número del capítulo que se debe volver a leer para realizar el trabajo solicitado. Algunos elementos del proyecto se proporcionan en una carpeta [aspnetmvc-support.zip] que se encuentra en el sitio web de este documento. En ella se encuentra la carpeta [étudedecas-support] con el siguiente contenido:
![]() |
Además, el proyecto retoma elementos presentados en los capítulos anteriores. Por lo tanto, basta con recuperarlos mediante copiar y pegar entre este archivo PDF y Visual Studio.
9.7.1. La solución de Visual Studio de la aplicación completa
En primer lugar, crearemos una solución de Visual Studio en la que crearemos dos proyectos:
- un proyecto para la capa simulada [métier];
- un proyecto para la capa web MVC.
![]() |
Utilizaremos dos herramientas:
- Visual Studio Express 2012 para escritorio, que servirá para desarrollar la capa [métier];
- Visual Studio Express 2012 para la web, que se utilizará para desarrollar la capa [web].
Con Visual Studio Express para escritorio, creamos una solución [pam-td]:
![]() |
- en [1], selecciona una aplicación C#;
- en [2], selecciona [Application console];
- en [3], asigne un nombre a la solución;
- en [4], genera un directorio para esta solución;
- en [5], asigna un nombre a la capa [métier];
- en [6], la solución generada.
9.7.2. La interfaz de la capa [métier]
En una arquitectura por capas, es una buena práctica que la comunicación entre capas se realice a través de interfaces:
![]() |
¿Qué interfaz debe presentar la capa [métier] a la capa [web]? ¿Cuáles son las interacciones posibles entre estas dos capas? Recordemos la interfaz web que se le presentará al usuario:
![]() |
- Al mostrar el formulario por primera vez, debe aparecer en [1] la lista de empleados. Basta con una lista simplificada (Apellido, Nombre, SS). El n.º SS es necesario para acceder a la información adicional sobre el empleado seleccionado (información del 6 al 11).
- Los datos del 12 al 15 corresponden a las diferentes tasas de cotización.
- Los datos del 16 al 19 corresponden a las prestaciones del empleado
- Los datos del 20 al 24 son los componentes del salario calculados a partir de los datos ingresados por el usuario en los campos 1 a 3.
La interfaz [IPamMetier] que la capa [métier] ofrece a la capa [web] debe cumplir con los requisitos mencionados anteriormente. Existen numerosas interfaces posibles. Proponemos la siguiente:
using Pam.Metier.Entites;
namespace Pam.Metier.Service
{
public interface IPamMetier
{
// lista de todas las identidades de los empleados
Employe[] GetAllIdentitesEmployes();
// ------- cálculo del salario
FeuilleSalaire GetSalaire(string ss, double heuresTravaillées, int joursTravaillés);
}
}
- línea 7: el método que permitirá completar el combo [1]
- línea 10: el método que permitirá obtener la información de la 6 a la 24. Esta información se ha reunido en un objeto de tipo [FeuilleSalaire] que describiremos a continuación.
Colocaremos esta interfaz en una carpeta [metier/service]:
![]() |
9.7.3. Las entidades de la capa [métier]
La interfaz anterior utiliza dos clases, [Employe] y [FeuilleSalaire], que debemos definir:
- [Employe] es la imagen de una fila de la tabla [employes] de la base de datos;
- [FeuilleSalaire] es la nómina de un empleado.
Las entidades se colocarán en una carpeta [metier / entites] del proyecto:
![]() |
En la arquitectura final, la capa [métier] manejará las entidades de imagen de la base de datos:

Utilizaremos las siguientes clases para representar las filas de las tres tablas de la base de datos. Consulte el párrafo 9.4 para conocer el significado de los distintos campos.
Clase [Employe]
Representa una fila de la tabla [employes]. Su código es el siguiente:
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; }
// firma
public override string ToString()
{
return string.Format("Employé[{0},{1},{2},{3},{4},{5}]", SS, Nom, Prenom, Adresse, Ville, CodePostal);
}
}
}
Clase [Indemnites]
Representa una fila de la tabla [indemnites]. Su código es el siguiente:
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; }
// firma
public override string ToString()
{
return string.Format("Indemnités[{0},{1},{2},{3},{4}]", Indice, BaseHeure, EntretienJour, RepasJour, IndemnitesCp);
}
}
}
Clase [Cotisations]
Representa una fila de la tabla [cotisations]. Su código es el siguiente:
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; }
// firma
public override string ToString()
{
return string.Format("Cotisations[{0},{1},{2},{3}]", CsgRds, Csgd, Secu, Retraite);
}
}
}
Cabe señalar que las clases no incluyen las columnas [ID] y [VERSIONING] de las tablas. Estas columnas, que son útiles cuando se utilizan las tablas ORM y EF5, no lo son en el contexto de la capa simulada [métier].
La clase [FeuilleSalaire] encapsula la información de los campos 6 a 24 del formulario ya presentado:
namespace Pam.Metier.Entites
{
public class FeuilleSalaire
{
// propiedades automáticas
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);
}
}
}
- línea 7: la información del 6 al 11 sobre el empleado cuyo salario se calcula y la información del 16 al 19 sobre sus prestaciones. No hay que olvidar aquí que un objeto [Employe] encapsula un objeto [Indemnites] que representa sus prestaciones;
- línea 8: la información del 12 al 15;
- línea 9: la información del 20 al 24;
- líneas 12-14: el método [ToString].
La clase [ElementsSalaire] encapsula la información del 20 al 24 del formulario:
namespace Pam.Metier.Entites
{
public class ElementsSalaire
{
// propiedades automáticas
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);
}
}
}
- líneas 6-10: los componentes del salario tal como se explican en las reglas de negocio descritas anteriormente;
- línea 6: el salario base del empleado, en función del número de horas trabajadas;
- línea 7: las cotizaciones deducidas de este salario base;
- líneas 8 y 9: las indemnizaciones que se suman al salario base, según el índice del empleado y el número de días trabajados;
- línea 10: el salario neto a pagar;
- líneas 14-17: el método [ToString] de la clase.
9.7.4. La clase [PamException]
Creamos un tipo de excepciones específico para nuestra aplicación. Se trata del siguiente tipo [PamException]:
using System;
namespace Pam.Metier.Entites
{
// clase de excepción
public class PamException : Exception
{
// el código de error
public int Code { get; set; }
// constructores
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;
}
}
}
- línea 6: la clase deriva de la clase [Exception];
- línea 10: tiene una propiedad pública [Code] que es un código de error;
- en nuestra aplicación utilizaremos dos tipos de constructores:
- el de las líneas 23-27, que se puede utilizar como se muestra a continuación:
- (continuación)
- o el de las líneas 29-33, destinado a reportar una excepción que haya ocurrido encapsulándola en una excepción de tipo [PamException]:
try{
....
}catch (IOException ex){
// encapsulamos la excepción ex
throw new PamException("Problème d'accès aux données",ex,10);
}
Este segundo método tiene la ventaja de no perder la información que pueda contener la primera excepción.
9.7.5. Implementación de la capa [métier]
La interfaz [IPamMetier] se implementará mediante la siguiente clase [PamMetier]:
using System;
using Pam.Metier.Entites;
using System.Collections.Generic;
namespace Pam.Metier.Service
{
public class PamMetier : IPamMetier
{
// lista de empleados en caché
public Employe[] Employes { get; set; }
// empleados indexados por su número SS
private IDictionary<string, Employe> dicEmployes = new Dictionary<string, Employe>();
// lista de empleados
public Employe[] GetAllIdentitesEmployes()
{
...
// se genera la lista de empleados
return Employes;
}
// cálculo del salario
public FeuilleSalaire GetSalaire(string ss, double heuresTravaillées, int joursTravaillés)
{
...
}
}
- línea 7: la clase [PamMetier] implementa la interfaz [IPamMetier];
- línea 10: la clase [PamMetier] mantiene la lista de empleados en caché;
- línea 12: un diccionario que asocia a un empleado con su número de seguro social;
- líneas 15-20: el método que devuelve la lista de empleados;
- líneas 23-26: el método que calcula el salario de un empleado.
El método [GetAllIdentitesEmploye] es el siguiente:
// lista de empleados
public Employe[] GetAllIdentitesEmployes()
{
if (Employes == null)
{
// se crea una tabla con tres empleados
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]);
// un empleado ficticio que no se incluirá en el diccionario
// para simular un empleado inexistente
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 }
};
}
// se muestra la lista de empleados
return Employes;
}
- línea 4: se verifica si la lista de empleados ya se ha generado;
- línea 7: si no es así, se crea una tabla con tres empleados;
- líneas 8-17: el primer empleado;
- línea 18: se agrega al diccionario;
- líneas 19-28: el segundo empleado;
- línea 29: se agrega al diccionario;
- líneas 32-42: el tercer empleado. Este no se agrega al diccionario por una razón que explicaremos.
El método [GetSalaire] será el siguiente:
// cálculo del salario
public FeuilleSalaire GetSalaire(string ss, double heuresTravaillées, int joursTravaillés)
{
// se recupera el empleado con el n.º 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);
}
// se genera una nómina ficticia
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 }
};
}
- línea 2: el método recibe el n.º SS del empleado cuyo salario queremos calcular, junto con su número de horas trabajadas y su número de días trabajados;
- línea 5: se busca al empleado en el diccionario. Recordemos que uno de ellos no se encuentra allí;
- líneas 7-10: si no se encuentra al empleado, se lanza una excepción [PamException];
- líneas 12-17: se devuelve una nómina ficticia.
9.7.6. La prueba de consola de la capa [métier]
El proyecto de la capa [métier] es actualmente el siguiente:
![]() |
La clase [Program] mencionada anteriormente probará los métodos de la interfaz [IPamMetier]. Un ejemplo básico podría ser el siguiente:
using Pam.Metier.Entites;
using Pam.Metier.Service;
using System;
namespace Pam.Metier.Tests
{
class Program
{
public static void Main()
{
// instanciación de la capa [métier]
IPamMetier pamMetier = new PamMetier();
// lista de empleados
Employe[] employes = pamMetier.GetAllIdentitesEmployes();
Console.WriteLine("Liste des employés--------------------");
foreach (Employe e in employes)
{
Console.WriteLine(e);
}
// cálculos de las hojas de pago
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));
}
}
}
}
- línea 12: instanciación de la capa [métier];
- líneas 14-19: prueba del método [GetAllIdentitesEmploye] de la interfaz [IPamMetier];
- líneas 21-31: prueba del método [GetSalaire] de la interfaz [IPamMetier].
Al ejecutar este programa de consola se obtienen los siguientes resultados:
Se invita al lector a establecer la relación entre estos resultados y el código ejecutado.
Para poder utilizar este proyecto en el proyecto web que vamos a desarrollar, lo convertimos en una biblioteca de clases:
![]() |
- en [1], en las propiedades del archivo [Program.cs];
- en [2], indicamos que el archivo no formará parte del ensamblado generado;
- en [3, 4], en las propiedades del proyecto [pam-metier-simule], en la opción [Application] [3], se indica en [4] que la generación debe proporcionar una biblioteca de clases (en forma de un DLL).
![]() |
- en [5], se solicita un ensamblado de tipo [Release]. El otro tipo es [Debug]. El ensamblado contiene entonces información que facilita la depuración;
- en [6], se genera el proyecto [pam-metier-simule];
![]() |
- en [7], se muestran todos los archivos de la solución;
- en [8], dentro de la carpeta [bin / Release], se encuentra el archivo DLL de nuestro proyecto.
9.8. Paso 2: configuración de la aplicación web
En la solución anterior de Visual Studio, vamos a crear el proyecto para la capa web MVC.
![]() |
Con Visual Studio Express para la web, abrimos la solución [pam-td] creada anteriormente con Visual Studio Express para escritorio.
![]() |
- En [1], la solución [pam-td] se ha cargado en Visual Studio Express para la web;
- en [2], la solución y el proyecto para la capa simulada [métier] que acabamos de crear.
En este nuevo paso, vamos a crear el esqueleto de la aplicación web.
![]() |
- en [1], agregamos un nuevo proyecto a la solución [pam-td];
![]() |
- en [2], seleccionamos un proyecto ASP.NET MVC 4;
- llamado [pam-web-01] [3];
- en [4], se elige la plantilla básica ASP.NET MVC;
- en [5], el proyecto creado;
![]() |
- en [6], se crea un nuevo proyecto, el proyecto de inicio de la solución, el que se ejecutará al ejecutar [Ctrl-F5];
- en [7], el nombre del nuevo proyecto aparece en negrita, lo que indica que es el proyecto de inicio de la solución.
Ahora, con el Explorador de Windows, reemplazamos la carpeta [Content] del proyecto por la carpeta [étudedecas-support / web / Content]. Una vez hecho esto, hay que incluir los nuevos archivos en el proyecto [pam-web-01]. Procederemos de la siguiente manera:
![]() |
- en [1], actualizamos la solución;
- en [2], se muestran todos los archivos de la solución;
- en [3], aparece una carpeta [Images];
- que se incluye en el proyecto en [4].
En la carpeta [Scripts], agregue los scripts JQuery y [1] de globalización necesarios para la validación del lado del cliente.
![]() |
La página maestra [_Layout.cshtml] [2] tendrá el siguiente contenido:
<!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>
Nota: en la línea 8, adapta la versión de jQuery a la de tu versión de Visual Studio.
- línea 7: referencia a la hoja de estilo de la aplicación;
- líneas 8-10: referencias a los scripts necesarios para la validación del lado del cliente;
- líneas 11-12: referencias sobre los scripts necesarios para ingresar números reales franceses con coma;
- línea 13: referencia a los scripts necesarios para el modo Ajax;
- línea 14: los scripts propios de la aplicación;
- línea 24: la imagen de espera al finalizar las llamadas Ajax;
- líneas 26-39: seis enlaces de JavaScript;
- línea 43: la sección donde se mostrarán las diferentes vistas de la aplicación;
- línea 44: el cuerpo de las diferentes vistas de la aplicación.
A continuación, modificaremos la ruta predeterminada de la aplicación:
![]() |
El archivo [RouteConfig] tendrá el siguiente contenido:
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" }
);
}
}
}
- línea 14: los URL tendrán el formato [{controller}/{action}];
- línea 15: si no hay ninguna acción, se utilizará la acción [Index]. Si no hay ningún controlador, se utilizará el controlador [Pam].
De esta configuración se desprende que URL [/] es equivalente a URL [/Pam/Index]. Dado que nuestra aplicación es del tipo APU, URL y [/] serán los únicos URL de esta.
Crea el controlador [Pam]:
Modifique el controlador [PamController] de la siguiente manera:
using System.Web.Mvc;
namespace Pam.Web.Controllers
{
public class PamController : Controller
{
[HttpGet]
public ViewResult Index()
{
return View();
}
}
}
- línea 3: colocamos el controlador en el espacio de nombres [Pam.Web.Controllers];
- línea 7: la acción [Index] procesará únicamente el comando HTTP GET;
- línea 8: se devuelve un tipo [ViewResult] en lugar de un tipo [ActionResult].
Ahora cree la vista [Index.cshtml] que muestra la acción [Index] anterior:
![]() |
Modifique [Index.cshtml] de la siguiente manera:
@{
ViewBag.Title = "Pam";
}
<h2>Formulaire</h2>
Ejecute la aplicación mediante [Ctrl-F5]. Debería obtener la siguiente página:
![]() |
Tarea: Explica qué sucedió.
La aplicación utiliza una hoja de estilo a la que se hace referencia en la página maestra [_Layout.cshtml]:
<link rel="stylesheet" href="~/Content/Site.css" />
La hoja de estilo [/Content/Site.css] define una imagen de fondo para las páginas de la aplicación:
body {
background-image: url("/Content/Images/standard.jpg");
}
![]() |
9.9. Paso 3: configuración de la plantilla APU
Queremos desarrollar una aplicación siguiendo la plantilla APU (Aplicación de una sola página) descrita en el párrafo 7.5, así como en el párrafo 7.6. La página única es aquella que carga el navegador al iniciar la aplicación:
![]() |
- la parte [1] anterior es la parte fija de la página única. Hemos visto que la proporciona la página maestra [_Layout.cshtml];
- la parte [2] es la parte variable de la página única. Se inserta en la región de id [content] de la página maestra [_Layout.cshtml]:
<!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>
Los distintos fragmentos de página de la aplicación se mostrarán en la región con el identificador [content] de la línea 13. Se mostrarán mediante llamadas Ajax. Los scripts de JavaScript que ejecutan estas llamadas se encuentran en el archivo [myScripts.js], al que se hace referencia en la línea 6. Crea este archivo, que vamos a necesitar:
![]() |
Ahora seguimos la plantilla APU descrita en el párrafo 7.6. Vuelve a leer ese párrafo si lo has olvidado. A continuación, vamos a implementar los distintos fragmentos de página que muestra la aplicación.
9.9.1. Las herramientas de desarrollo de JavaScript
Recordamos que, con el navegador Chrome, tienes a tu disposición una variedad de herramientas para depurar el JavaScript de tus páginas (HTML, CSS). Estas herramientas se presentaron parcialmente en el párrafo 7.2. En el modelo APU, los navegadores almacenan en caché los scripts de JavaScript a los que hace referencia la primera página de la aplicación. Por lo tanto, es importante vaciar esta caché cuando modifiques tus scripts; de lo contrario, es posible que los cambios no se reflejen. A continuación te explicamos cómo hacerlo con Chrome:
- ejecuta [Ctrl-Maj-I] para mostrar el entorno de desarrollo
![]() |
- haz clic en el ícono [1] en la esquina inferior derecha de la ventana de desarrollo;
- luego marca la opción [2] que desactiva la caché en modo de desarrollo.
9.9.2. Uso de una vista parcial para mostrar el formulario
El formulario de entrada es uno de los fragmentos que muestra la aplicación. Por el momento, este formulario se muestra mediante la vista [Index.cshtml], que es una vista completa:
@{
ViewBag.Title = "Pam";
}
<h2>Formulaire</h2>
Esta vista es mostrada por la acción [Index]:
[HttpGet]
public ViewResult Index()
{
return View();
}
En la línea 4 anterior, lo que se muestra es efectivamente una vista [View] y no una vista parcial [PartialView]. Necesitamos una vista parcial para el formulario, que será un fragmento de página. Modificamos la vista [Index.cshtml] de la siguiente manera:
@{
ViewBag.Title = "Pam";
}
@Html.Partial("Formulaire")
En la línea 4, el formulario ya no forma parte de la página [Index.cshtml]. Ahora se encuentra en una vista parcial [Formulaire.cshtml]:
![]() |
El código de [Formulaire.cshtml] es simplemente el siguiente:
<h2>Formulaire</h2>
Realice estos cambios y verifique que siga apareciendo la siguiente vista al iniciar la aplicación:
![]() |
9.9.3. La llamada Ajax [faireSimulation]
Nos interesa el fragmento que se muestra cuando el usuario hace clic en el enlace [Faire la simulation]:
![]() |
- en [1], el usuario hace clic en el enlace [Faire la simulation];
- en [2], la simulación aparece debajo del formulario.
Modificamos de la siguiente manera la vista parcial [Formulaire.cshtml] que muestra el formulario:
<h2>Formulaire</h2>
<div id="simulation" />
En la línea 3, creamos una región con el ID [simulation] para alojar el fragmento de la simulación.
Creamos la vista parcial [Simulation.cshtml] de la siguiente manera:
![]() |
El contenido de la vista [Simulation.cshtml] es el siguiente:
<hr />
<h2>Simulation</h2>
Ahora debemos escribir el código de JavaScript que maneja el clic en el enlace [Faire la simulation]. Seguiremos el procedimiento descrito en el párrafo 7.6.5. En primer lugar, veamos el código HTML del enlace en [_Layout.cshtml]:
<a id="lnkFaireSimulation" href="javascript:faireSimulation()">| Faire la simulation<br />
</a>
Se observa que al hacer clic en el enlace [Faire la simulation] se iniciará la ejecución de la función JS [faireSimulation]. Esta función se escribirá en el archivo [myScripts.js], junto con las demás funciones JS necesarias para la aplicación:
// variables globales
var loading;
var content;
function faireSimulation() {
// se realiza una llamada Ajax manualmente
...
}
function effacerSimulation() {
// se borran los datos ingresados en el formulario
...
}
function enregistrerSimulation() {
// se realiza una llamada Ajax manualmente
...
}
function voirSimulations() {
// se realiza una llamada Ajax manualmente
...
}
function retourFormulaire() {
// se realiza una llamada Ajax manualmente
...
}
function terminerSession() {
...
}
// al cargar el documento
$(document).ready(function () {
// se recuperan las referencias de los distintos componentes de la página
loading = $("#loading");
content = $("#content");
});
- líneas 35-39: la función JQuery se ejecuta al iniciar la aplicación;
- líneas 37-38: se inicializan las variables globales de las líneas 2 y 3.
Cabe recordar que los elementos de identificación [loading] y [content] se definen en la página maestra [_Layout.cshtml] (líneas 14 y 21 a continuación):
<!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>
Tarea: siguiendo el procedimiento expuesto en el párrafo 7.6.5, escriba la función JS [faireSimulation]. Esta función realizará una llamada Ajax de tipo POST a la acción [/Pam/FaireSimulation]. Por el momento, no se enviarán datos. Laacción [/Pam/FaireSimulation] devolverá la vista parcial [Simulation.cshtml] a la función JS [faireSimulation], la cual colocará entonces este flujo HTML en la región con el ID [simulation] del formulario.
Prueba el enlace [Faire la simulation] de tu aplicación.
9.9.4. La llamada Ajax [enregistrerSimulation]
El enlace [Enregistrer la simulation] se define de la siguiente manera en [_Layout.cshtml]:
<a id="lnkEnregistrerSimulation" href="javascript:enregistrerSimulation()">| Enregistrer la simulation<br />
</a>
Tarea: siguiendo el procedimiento anterior, escriba la función JS [enregistrerSimulation]. Esta función realizará una llamada Ajax de tipo POST a la acción [/Pam/EnregistrerSimulation]. Por el momento, no se enviarán datos. Laacción [/Pam/EnregistrerSimulation] devolverá la vista parcial [Simulations.cshtml] a la función JS [enregistrerSimulation], la cual colocará este flujo HTML en la región con el ID [content] de la página maestra.
La vista [Simulations.cshtml] es la siguiente:
![]() |
Su contenido es el siguiente:
<h2>Simulations</h2>
A continuación se muestra un ejemplo de ejecución:
![]() | ![]() |
9.9.5. La llamada Ajax [voirSimulations]
El enlace [Voir les simulations] se define de la siguiente manera en [_Layout.cshtml]:
<a id="lnkVoirSimulations" href="javascript:voirSimulations()">| Voir les simulations<br />
</a>
Tarea: siguiendo el procedimiento anterior, escriba la función JS [voirSimulations]. Esta función realizará una llamada Ajax de tipo POST a la acción [/Pam/VoirSimulations]. Por el momento, no se enviarán datos. Laacción [/Pam/VoirSimulations] devolverá la vista parcial [Simulations.cshtml] a la función JS [voirSimulations], la cual colocará este flujo HTML en la región con el ID [content] de la página maestra.
La vista [Simulations.cshtml] es la misma que ya se utilizó en la pregunta anterior.
A continuación se muestra un ejemplo de ejecución:
![]() |
9.9.6. La llamada Ajax [retourFormulaire]
El enlace [Retour au formulaire de simulation] se define de la siguiente manera en [_Layout.cshtml]:
<a id="lnkRetourFormulaire" href="javascript:retourFormulaire()">| Retour au formulaire de simulation<br />
</a>
Tarea: siguiendo el procedimiento anterior, escriba la función JS [retourFormulaire]. Esta función realizará una llamada Ajax de tipo POST a la acción [/Pam/Formulaire]. Por el momento, no se enviarán datos. Laacción [/Pam/Formulaire] devolverá la vista parcial [Formulaire.cshtml] a la función JS [retourFormulaire], la cual colocará este flujo HTML en la región con el ID [content] de la página maestra.
La vista [Formulaire .cshtml] ya se ha definido. A continuación se muestra un ejemplo de ejecución:
![]() |
9.9.7. La llamada Ajax [terminerSession]
El enlace [Terminer la session] se define de la siguiente manera en [_Layout.cshtml]:
<a id="lnkTerminerSession" href="javascript:terminerSession()">| Terminer la session<br />
</a>
Tarea: siguiendo el procedimiento anterior, escribe la función JS [terminerSession]. Esta función realizará una llamada Ajax de tipo POST a la acción [/Pam/TerminerSession]. Por el momento no se enviarán datos. Laacción [/Pam/TerminerSession] devolverá la vista parcial [Formulaire.cshtml] a la función JS [terminerSession], la cual colocará este flujo HTML en la región con el ID [content] de la página maestra.
A continuación se muestra un ejemplo de ejecución:
![]() |
9.9.8. La función JS [effacerSimulation]
El enlace [Effacer la simulation] se define de la siguiente manera en [_Layout.cshtml]:
<a id="lnkEffacerSimulation" href="javascript:effacerSimulation()">| Effacer la simulation<br />
</a>
La función JS [effacerSimulation] tiene como objetivo:
- ocultar el fragmento [Simulation], si existe;
- restablecer los campos de entrada del formulario al estado en que se encontraban al cargar la aplicación por primera vez (cuando haya campos de entrada; por el momento no hay ninguno).
Tarea: escribe la función JS [effacerSimulation]. Aquí no hay ninguna llamada Ajax. Lo que ocurre es interno al navegador y no involucra al servidor.
A continuación, un ejemplo de ejecución:
![]() |
9.9.9. Manejo de la navegación entre pantallas
Por el momento, los enlaces siempre se muestran. Ahora vamos a controlar su visualización con una función de JavaScript. Recordemos primero el código de los seis enlaces de JavaScript en [_Layout.cshtml]:
<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>
Todos los enlaces tienen un atributo [id] que nos permitirá gestionarlos en JavaScript. Modificamos el método JS que se ejecuta al cargar la página de la siguiente manera:
// variables globales
var loading;
var content;
var lnkFaireSimulation;
var lnkEffacerSimulation
var lnkEnregistrerSimulation;
var lnkTerminerSession;
var lnkVoirSimulations;
var lnkRetourFormulaire;
var options;
...
// al cargar el documento
$(document).ready(function () {
// se recuperan las referencias de los distintos componentes de la página
loading = $("#loading");
content = $("#content");
// los enlaces del menú
lnkFaireSimulation = $("#lnkFaireSimulation");
lnkEffacerSimulation = $("#lnkEffacerSimulation");
lnkEnregistrerSimulation = $("#lnkEnregistrerSimulation");
lnkVoirSimulations = $("#lnkVoirSimulations");
lnkTerminerSession = $("#lnkTerminerSession");
lnkRetourFormulaire = $("#lnkRetourFormulaire");
// se colocan en una tabla
options = [lnkFaireSimulation, lnkEffacerSimulation, lnkEnregistrerSimulation, lnkVoirSimulations, lnkTerminerSession, lnkRetourFormulaire];
// se ocultan ciertos elementos de la página
loading.hide();
// se fija el menú
setMenu([lnkFaireSimulation, lnkVoirSimulations, lnkTerminerSession]);
});
- líneas 19-24: recuperamos las referencias de los seis enlaces. Estas referencias se definen como variables globales en las líneas 4-9;
- línea 26: se inicializa el array [options] con las seis referencias. Este array se define como variable global en la línea 10;
- línea 28: se oculta la imagen animada que indica que se está esperando a que finalicen las llamadas Ajax;
- línea 30: se muestran los enlaces [lnkFaireSimulation, lnkVoirSimulations, lnkTerminerSession]. Los demás se ocultarán.
La función JS [setMenu] es la siguiente:
function setMenu(show) {
// se muestran los enlaces de la tabla [show]
...
}
Tarea: escribe la función JS [setMenu].
Si T es una matriz de enlaces:
- T.length es el número de enlaces;
- T[i] es el enlace n.º i;
- T[i].show() muestra el enlace n.º i;
- T[i].hide() oculta el enlace n.º i.
Con estas nuevas funciones JS, la página que se muestra al inicio es la siguiente:
![]() |
Adapta las funciones JS y [faireSimulation, effacerSimulation, enregistrerSimulation, voirSimulations, retourFormulaire, terminerSession] para obtener las siguientes pantallas:
![]() | ![]() |
![]() | ![]() |
![]() | ![]() |
![]() | ![]() |
![]() | ![]() |
![]() | ![]() |
Ahora que la plantilla APU y los enlaces de navegación están listos, podemos pasar a escribir las acciones y las vistas del lado del servidor. A medida que avances por los pasos, descubrirás que algunos de los enlaces Ajax que ahora funcionan ya no lo harán, debido a que modificarás las vistas parciales enviadas al cliente. A medida que vayas creando las diferentes acciones y vistas del lado del servidor, los enlaces Ajax del lado del cliente volverán a funcionar como los habías configurado.
9.10. Paso 4: desarrollo de la acción del servidor [Index]
Actualmente, al iniciar la aplicación, aparece la siguiente pantalla:
![]() |
En lugar de esta pantalla, nos gustaría tener la siguiente:
![]() |
Es la acción [Index] la que debe generar esta página. Hagamos algunas observaciones:
- la página presenta un formulario con tres campos de entrada:
- el empleado cuyo salario se calcula,
- su número de horas trabajadas,
- su número de días trabajados;
- el formulario se envía mediante el enlace [Faire la simulation];
- se debe verificar la validez de los campos de entrada [Heures travaillées] y [Jours travaillés];
- la lista de empleados proviene de la capa [métier] que creamos anteriormente.
Recordemos el código actual de la acción [Index]:
[HttpGet]
public ViewResult Index()
{
return View();
}
el de la vista [Index.cshtml] que muestra esta acción:
@{
ViewBag.Title = "Pam";
}
@Html.Partial("Formulaire")
y el de la vista parcial [Formulaire.cshtml]:
<h2>Formulaire</h2>
Se realizarán modificaciones en estos tres lugares.
9.10.1. La plantilla del formulario
Volvamos a la cadena de procesamiento de URL [/Pam/Index]:
![]() |
- la solicitud HTTP del cliente llega como [1];
- en [2], la información contenida en la solicitud se transformará en el modelo de acción [3], que servirá como entrada para la acción [4];
- en [4], la acción, a partir de este modelo, generará una respuesta. Esta tendrá dos componentes: una vista V [6] y el modelo M de esta vista [5];
- la vista V [6] utilizará su modelo M [5] para generar la respuesta HTTP destinada al cliente.
La acción que nos interesa es la acción [Index], que por el momento es la siguiente:
[HttpGet]
public ViewResult Index()
{
return View();
}
La acción [Index] no pasa ningún modelo a la vista [Index.cshtml]. Por lo tanto, esta no podrá mostrar la lista de empleados. Dicha lista se puede solicitar a la capa [métier]. Para ello, es necesario que el proyecto [pam-web-01] tenga una referencia al proyecto [pam-metier-simule]. Creamos esta referencia ahora:
![]() |
- en [1], haz clic con el botón derecho en [References] del proyecto [pam-web-01] y luego en [Ajouter une référence];
- en [2], selecciona la opción [Solution] y luego el proyecto [pam-metier-simule] en [3];
- en [4], el proyecto [pam-metier-simule] se ha agregado a las referencias del proyecto [pam-web-01].
9.10.2. El modelo de la aplicación
En el párrafo 4.10, página 78, presentamos los conceptos importantes de modelo de aplicación y modelo de sesión. Ahora los utilizaremos. Recordemos que en el modelo se incluyen:
- en el modelo de aplicación, datos de solo lectura para todos los usuarios. Este modelo constituye una memoria compartida por todas las consultas de todos los usuarios;
- en el modelo de sesión, datos de lectura y escritura para un usuario específico. Este modelo constituye una memoria compartida por todas las consultas de ese usuario.
¿Qué vamos a incluir en el modelo de aplicación? Volvamos a su arquitectura:
![]() |
La capa [web] contiene una referencia a la capa [métier]. Esta última puede ser compartida por todos los usuarios. Por lo tanto, podemos incluirla en el modelo de la aplicación. Además, vamos a suponer que la lista de empleados no cambia. Por lo tanto, puede leerse una sola vez y luego compartirse entre todos los usuarios. Proponemos entonces el siguiente modelo de aplicación:
![]() |
El código de la clase [ApplicationModel] podría ser el siguiente:
using Pam.Metier.Entites;
using Pam.Metier.Service;
namespace PamWeb.Models
{
public class ApplicationModel
{
// --- datos del ámbito de la aplicación ---
public Employe[] Employes { get; set; }
public IPamMetier PamMetier { get; set; }
}
}
Para mostrar una lista desplegable en una vista, se escribe algo como lo siguiente:
<!-- la lista desplegable -->
<tr>
<td>Liste déroulante</td>
<td>@Html.DropDownListFor(m => m.DropDownListField,
new SelectList(@Model.DropDownListFieldItems, "Value", "Label"))
</td>
</tr>
El método [DropDownListFor] espera como segundo parámetro un tipo SelectListItem[], que se había proporcionado anteriormente mediante un tipo [SelectList]. Necesitamos construir un array de este tipo con la lista de empleados. Dado que los empleados no cambian, este array también puede colocarse en el modelo de la aplicación. Actualizamos este de la siguiente manera:
using Pam.Metier.Entites;
using Pam.Metier.Service;
using System.Web.Mvc;
namespace Pam.Web.Models
{
public class ApplicationModel
{
// --- datos del ámbito de la aplicación ---
public Employe[] Employes { get; set; }
public IPamMetier PamMetier { get; set; }
public SelectListItem[] EmployesItems { get; set; }
}
}
¿En qué momento se debe crear este modelo? Lo hemos mostrado en el párrafo 4.10. Es durante la ejecución del método [Application_Start] del archivo [Global.asax]:
![]() |
El método [Application_Start] es, por el momento, el siguiente:
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);
}
}
}
Lo estamos modificando de la siguiente manera:
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()
{
// ----------Generado automáticamente
AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
// -------------------------------------------------------------------
// ---------- configuración específica
// -------------------------------------------------------------------
// datos de ámbito de la aplicación
ApplicationModel application = new ApplicationModel();
Application["data"] = application;
// instanciación de la capa [métier]
application.PamMetier = ...
// tabla de empleados
application.Employes = ...
// elementos del menú desplegable de empleados
application.EmployesItems = ...
// model binder para [ApplicationModel]
...
}
}
}
Tarea: completar el código del método [Application_Start]. Todo lo que necesitas se encuentra en el párrafo 4.10. Tómate el tiempo para releer este párrafo, que es largo pero importante.
La línea 33, en realidad, consta de varias líneas. Para crear un objeto de tipo [SelectListItem], puedes utilizar el siguiente método:
new SelectListItem() { Text = unTexte, Value = uneValeur };
Este [SelectListItem] servirá para generar la siguiente etiqueta HTML <option>:
de la lista desplegable. Nos aseguraremos de que:
- unTexte sea el nombre seguido del apellido del empleado;
- uneValeur sea el número SS del empleado.
En la línea 35, arriba, necesitará la clase [ApplicationModelBinder] descrita en el párrafo 4.10, página 82:
![]() |
9.10.3. El código de la acción [Index]
Ahora que hemos definido un modelo para la aplicación, podemos modificar el código de la acción [Index] de la siguiente manera:
[HttpGet]
public ViewResult Index(ApplicationModel application)
{
return View();
}
- línea 4: el modelo de la aplicación es ahora un parámetro de la acción [Index]. En el párrafo 4.10 explicamos cómo el marco de trabajo inicializa este parámetro.
9.10.4. El modelo de la vista [Index.cshtml]
Ahora, la acción [Index] tiene acceso a los empleados que están registrados en el modelo de la aplicación. Ahora debe pasarlos a la vista [Index.cshtml] que va a mostrar. Se podría pasar un tipo [ApplicationModel] como modelo de la vista [Index.cshtml], pero pronto veremos que esta vista necesita otra información que no se encuentra en [ApplicationModel]. Vamos a utilizar la plantilla de vista [IndexModel] siguiente:
![]() |
namespace Pam.Web.Models
{
public class IndexModel
{
// datos de ámbito de la aplicación
public ApplicationModel Application { get; set; }
}
}
- línea 6: [IndexModel] incorpora la plantilla de la aplicación.
La acción [Index] queda así:
[HttpGet]
public ViewResult Index(ApplicationModel application)
{
return View(new IndexModel() { Application = application });
}
- En la línea 4, se muestra la vista predeterminada [Index.cshtml] con un tipo de modelo [IndexModel] inicializado con los datos del modelo de la aplicación.
Sabemos que la vista [Index.cshtml] debe mostrar un formulario:

Volvamos a la cadena de procesamiento de una solicitud:
![]() |
Para la solicitud [GET /Pam/Index]:
- la acción es [Index];
- el modelo de esta acción es [ApplicationModel];
- la vista es [Index.cshtml];
- el modelo de esta vista es [IndexModel].
Cuando se envíe el formulario, tendremos una cadena de procesamiento similar:
- la acción es la que procesa el POST;
- su modelo recopila los valores enviados, en este caso:
- el n.º SS del empleado seleccionado;
- el número de horas trabajadas;
- el número de días trabajados;
Se podría crear una plantilla de acción que reúna estos tres valores. También es común reutilizar la plantilla que se utilizó para mostrar el formulario. Eso es lo que haremos aquí. La clase [IndexModel] se modifica de la siguiente manera:
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
namespace Pam.Web.Models
{
[Bind(Exclude = "Application")]
public class IndexModel
{
// datos del ámbito de la aplicación
public ApplicationModel Application { get; set; }
// valores registrados
[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; }
}
}
- líneas 13, 16, 18: los tres valores registrados. Cabe señalar que [joursTravaillés] se ha declarado como de tipo [double], cuando en realidad se espera un entero. Se introdujo el tipo [double] para facilitar la validación de este campo del lado del cliente, ya que la validación de un tipo [int] había presentado problemas;
- líneas 12, 14, 17: descripciones para los métodos [Html.LabelFor] de la vista asociada al modelo;
- línea 15: una anotación para que el campo [HeuresTravaillées] se muestre con dos decimales;
- línea 5: se indica que la propiedad denominada [Application] no forma parte de los valores enviados.
9.10.5. Las vistas [Index.cshtml] y [Formulaire.cshtml]
La vista [Index.cshtml] se muestra mediante la siguiente acción [Index]:
[HttpGet]
public ViewResult Index(ApplicationModel application)
{
return View(new IndexModel() { Application = application });
}
Curiosamente, la vista [Index.cshtml] permanece sin cambios:
@{
ViewBag.Title = "Pam";
}
@Html.Partial("Formulaire")
- la vista no declara ningún modelo;
- línea 4: incorpora la vista parcial [Formulaire.cshtml], nuevamente sin pasarle ningún modelo. Durante las pruebas, se observó que el modelo [IndexModel] pasado a la vista [Index.cshtml] se propagaba implícitamente a la vista parcial [Formulaire.cshtml]. Esta última vista ahora podría tener la siguiente forma:
@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" />
- línea 1: la vista recibe un modelo de tipo [IndexModel];
- línea 3: el formulario;
- líneas 6-10: los encabezados de la tabla de entradas;
- líneas 12-14: la línea de datos;
- líneas 15-17: los posibles mensajes de error.
Tarea: completar el código de la vista [Formulaire.cshtml]. Se utilizarán los métodos [DropDownListFor, EditorFor, LabelFor, ValidationMessageFor] descritos en el párrafo 5.7.
9.10.6. Prueba de la acción [Index]
Hemos escrito todos los elementos de la cadena de procesamiento de URL y [/Pam/Index]:
![]() |
Probamos la aplicación con [Ctrl-F5]:
![]() | ![]() |
Debe verificar que su lista desplegable se haya llenado correctamente con la lista de empleados que definimos en la capa simulada [métier].
9.11. Paso 5: configuración de la validación de los datos ingresados
9.11.1. El problema
Aunque no hayamos hecho nada al respecto, ya se están aplicando validaciones del lado del cliente:
![]() |
![]() |
La validación del lado del cliente está activa de forma predeterminada debido a la línea 3 que se muestra a continuación en el archivo [Web.config] de la aplicación.
<appSettings>
...
<add key="ClientValidationEnabled" value="true" />
</appSettings>
Sin embargo, debido a que en [IndexModel] se ha declarado el campo [JoursTravaillés] como de tipo [double]:
public double JoursTravaillés { get; set; }
se puede ingresar un número real en este campo:
![]() |
Además, se pueden ingresar valores arbitrarios en ambos campos:
![]() |
El modelo [IndexModel] del formulario es actualmente el siguiente:
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
namespace Pam.Web.Models
{
[Bind(Exclude = "Application")]
public class IndexModel
{
// datos de ámbito de la aplicación
public ApplicationModel Application { get; set; }
// valores registrados
[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; }
}
}
Tarea: mejora esta plantilla para:
- tener mensajes de error personalizados;
- aceptar únicamente valores reales dentro del intervalo [0,400] para el campo [HeuresTravaillées];
- aceptar únicamente valores enteros dentro del intervalo [0,31] para el campo [JoursTravaillées];
Se puede utilizar el ejemplo del párrafo 7.6.2 como referencia. Para verificar que el número de días trabajados sea un número entero, se puede utilizar una expresión regular (véanse los ejemplos del párrafo 5.9.1).
A continuación se muestran ejemplos de lo que se espera:
![]() |
![]() |
![]() |
9.11.2. Ingreso de números reales en formato francés
En la versión actual de la aplicación, el número de horas trabajadas debe ser un número decimal en formato anglosajón (con punto decimal). No se acepta el formato francés con coma:
![]() |
Este problema se identificó y se abordó en el párrafo 6.1.
Tarea: siguiendo el procedimiento del párrafo mencionado anteriormente, realiza las modificaciones necesarias para que se puedan ingresar los números reales con el formato decimal francés. Prueba tu aplicación.
Ahora, la pantalla anterior queda así:
![]() |
9.11.3. Validación del formulario mediante el enlace de JavaScript [Faire la simulation]
Actualmente, se pueden enviar valores no válidos, como lo muestra la siguiente secuencia:
![]() |
![]() |
La presencia de la simulación en [1] y el cambio de menú en [2] demuestran que al hacer clic en el enlace [Faire la simulation] se envió el formulario a pesar de que los valores ingresados eran inválidos. Este problema se identificó y se abordó en el párrafo 7.6.5.
Tarea: siguiendo el procedimiento descrito en el párrafo mencionado anteriormente, asegúrese de que el POST del enlace [Faire la simulation] no se pueda ejecutar si los valores ingresados son inválidos. Recuerde borrar la caché del navegador antes de probar sus modificaciones.
Recordamos que la vista parcial [Formulaire.cshtml] genera un formulario HTML con el identificador [formulaire] (línea 1 a continuación):
@using (Html.BeginForm("FaireSimulation", "Pam", FormMethod.Post, new { id = "formulaire" }))
{
...
}
Esto se puede verificar al mostrar el código fuente del formulario en el navegador:
<div id="content">
<form action="/Pam/FaireSimulation" id="formulaire" method="post">
...
</form>
<div id="simulation" />
</div>
9.12. Paso 6: realizar una simulación
9.12.1. El problema
Cuando realizamos una simulación, queremos obtener el siguiente resultado:
![]() |
La vista parcial [Simulation.cshtml] ahora muestra la nómina de un empleado.
9.12.2. Creación de la vista [Simulation.cshtml]
La vista [Simulation.cshtml] cambia de la siguiente manera:
@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>
- línea 1: la vista [Simulation.cshtml] tiene como modelo el tipo [FeuilleSalaire] definido en el párrafo 9.7.3;
- la vista utiliza las clases [libellé, info, valeur] definidas en la hoja de estilo de la aplicación [Content / Site.css]:
.libellé {
background-color: azure;
margin: 5px;
padding: 5px;
}
.info {
background-color: antiquewhite;
margin: 5px;
padding: 5px;
}
.valeur {
background-color: beige;
padding: 5px;
margin: 5px;
}
Además, también en [Site.css], se establece la altura de las filas de las diferentes tablas HTML de la región con id [simulation], precisamente donde se muestra la hoja de salario:
#simulación de la tabla tr {
height: 30px;
}
Tarea: complete la vista [Simulation.cshtml].
Para mostrar los euros de una suma de dinero, se utilizará el método [string.Format]:
La instrucción anterior muestra [somme] como valor monetario [C] (Moneda) con dos decimales [C2].
Para probar esta vista, se le debe proporcionar una nómina. Esta debe ser proporcionada por la acción [/Pam/FaireSimulation], que es el destino de la llamada Ajax del enlace [Faire la simulation]. Actualmente, esta acción es la siguiente:
[HttpGet]
public ViewResult Index(ApplicationModel application)
{
return View(new IndexModel() { Application = application });
}
// realizar una simulación
[HttpPost]
public PartialViewResult FaireSimulation()
{
return PartialView("Simulation");
}
En el ejemplo anterior, la acción [FaireSimulation] no pasa ninguna plantilla a la vista [Simulation.cshtml]. Debe pasarle una hoja de salario. Sabemos que es la capa [métier] la que realiza el cálculo de las hojas de pago. Se puede acceder a esta capa [métier] a través del modelo de la aplicación [ApplicationModel] que definimos en el párrafo 9.10.2:
public class ApplicationModel
{
// --- datos de ámbito de la aplicación ---
public Employe[] Employes { get; set; }
public IPamMetier PamMetier { get; set; }
public SelectListItem[] EmployesItems { get; set; }
}
Se puede acceder a la capa [métier] a través de la propiedad de la línea 5 anterior. Para que la acción [FaireSimulation] tenga acceso a la capa [métier], le pasaremos el modelo de la aplicación tal como lo hicimos para la acción [Index]. El código queda entonces de la siguiente manera:
// realizar una simulación
[HttpPost]
public PartialViewResult FaireSimulation(ApplicationModel application)
{
return PartialView("Simulation");
}
Ahora, dentro de la acción, podemos calcular una nómina ficticia. El código queda así:
// realizar una simulación
[HttpPost]
public PartialViewResult FaireSimulation(ApplicationModel application)
{
FeuilleSalaire feuilleSalaire = application.PamMetier.GetSalaire("254104940426058", 150, 20);
return PartialView("Simulation", feuilleSalaire);
}
- En la línea 5, se calcula un salario ficticio. El primer parámetro es un número SS existente. Se definió en la clase [métier] simulada en el párrafo 9.7.5. El segundo parámetro es el número de horas trabajadas y el tercero, el número de días trabajados;
- línea 6: esta hoja de salario se pasa como modelo a la vista [Simulation.cshtml].
Ahora estamos listos para probar la vista [Simulation.cshtml]:
![]() |
No se ingresa ningún dato y se solicita la simulación. Se obtiene entonces el siguiente resultado:
![]() |
9.12.3. Cálculo del salario real
Nuestra acción actual [FaireSimulation] siempre calcula la misma hoja de salario:
// realizar una simulación
[HttpPost]
public PartialViewResult FaireSimulation(ApplicationModel application)
{
FeuilleSalaire feuilleSalaire = application.PamMetier.GetSalaire("254104940426058", 150, 20);
return PartialView("Simulation", feuilleSalaire);
}
No toma en cuenta la información ingresada:
- el empleado cuyo salario se calcula;
- su número de horas trabajadas;
- su número de días trabajados.
Los valores ingresados llegan a la acción [FaireSimulation] de la siguiente manera:
- el usuario hace clic en el enlace [Faire la simulation]. Esto activa la ejecución de la función JS [faireSimulation] que ya hemos escrito;
- La función JS [faireSimulation] realiza luego una llamada Ajax a la acción del servidor [/Pam/FaireSimulation], en la que estamos trabajando actualmente. Por el momento, la función JS [faireSimulation] no envía ninguna información a la acción del servidor. Deberá enviarle los valores ingresados por el usuario;
- la acción del servidor [/Pam/FaireSimulation] recuperará los valores ingresados de entre los valores enviados por la función JS y [faireSimulation].
Comencemos por el punto 2: la función JS [faireSimulation] debe enviar los valores ingresados por el usuario a la acción de servidor [/Pam/FaireSimulation].
Tarea: completa la función JS [faireSimulation] para que envíe los valores ingresados por el usuario. Puedes usar como referencia el ejemplo del párrafo 7.6.5, donde se abordó este problema.
Ahora abordemos el punto 3 anterior. La acción de servidor [/Pam/FaireSimulation] debe recuperar los valores enviados por la función JS [faireSimulation].
Tarea: complete el método de servidor [FaireSimulation] para que calcule el salario con los valores enviados por las funciones JS y [faireSimulation]. Podemos volver a recurrir al ejemplo del párrafo 7.6.5, donde se abordó este problema. Por el momento, supondremos que el modelo derivado de los valores enviados sigue siendo válido.
Sugerencia: la acción de servidor [FaireSimulation] se desarrolla de la siguiente manera:
// realizar una simulación
[HttpPost]
public PartialViewResult FaireSimulation(ApplicationModel application, FormCollection data)
{
// creación del modelo de la acción
...
// intentamos recuperar los valores registrados en este modelo
...
// se calcula el salario
FeuilleSalaire feuilleSalaire = ...
// se muestra la nómina
return PartialView("Simulation", feuilleSalaire);
}
A continuación se muestra un ejemplo de ejecución:
![]() |
Elegimos [Justine Laverti]. Entonces obtenemos el siguiente resultado:
![]() |
Efectivamente, se obtuvo la nómina ficticia de [Justine Laverti]. Anteriormente, la única nómina que se calculaba era la de [Marie Jouveinal]. Por lo tanto, se ha utilizado el valor registrado para la selección del empleado. En cuanto al número de horas y al número de días, no podemos decir nada, ya que nuestra capa simulada [métier] no los toma en cuenta.
9.12.4. Manejo de errores
Veamos el siguiente ejemplo:
![]() |
- en [1], se elige un empleado que no existe (véase la definición de la capa [métier] simulada en el párrafo 9.7.5;
- en [2], se realiza la simulación;
- en [3] a continuación, aparece una página de error.
![]() |
¿Qué pasó?
Se ejecutó la función JS [faireSimulation]. Su código se ve así:
function faireSimulation() {
...
// se realiza una llamada Ajax manualmente
$.ajax({
url: '/Pam/FaireSimulation',
...
beforeSend: function () {
// se enciende la luz de espera
loading.show();
},
success: function (data) {
...
},
error: function (jqXHR) {
// se muestra un error
simulation.html(jqXHR.responseText);
simulation.show();
},
complete: function () {
// se apaga la señal de espera
loading.hide();
}
});
// menú
setMenu([lnkEffacerSimulation, lnkEnregistrerSimulation, lnkTerminerSession, lnkVoirSimulations]);
}
La llamada Ajax falló y se ejecutó la función de las líneas 14 a 18. Se mostró la página de error [jqXHR.responseText] devuelta por el servidor. Esta es bastante precisa. La capa simulada [métier] generó una excepción porque el número SS que se le proporcionó no corresponde a un empleado existente (véase el código de la capa simulada [métier] en el párrafo 9.7.5). Debemos manejar este caso de manera adecuada.
Crearemos una vista parcial [Erreurs.chtml] que se devolverá al cliente JS cada vez que se detecte un error del lado del servidor:
![]() |
El código de la vista parcial [Erreurs.chtml] es el siguiente:
@model IEnumerable<string>
<hr />
<h2>Les erreurs suivantes se sont produites</h2>
<ul>
@foreach (string msg in Model)
{
<li>@msg</li>
}
</ul>
- línea 1: la vista recibe como modelo una lista de mensajes de error;
- líneas 5-10: que se muestran en una lista HTML;
Ahora, modifiquemos el código de la acción del servidor [FaireSimulation] de la siguiente manera:
// realizar una simulación
[HttpPost]
public PartialViewResult FaireSimulation(ApplicationModel application, FormCollection data)
{
...
// se calcula el salario
FeuilleSalaire feuilleSalaire = null;
Exception exception=null;
try
{
// cálculo del salario
feuilleSalaire = ...
}
catch (Exception ex)
{
exception = ex;
}
// ¿Error?
if (exception == null)
{
// se muestra la nómina
return PartialView("Simulation", feuilleSalaire);
}
else
{
// se muestra la página de errores
return PartialView("Erreurs", Static.GetErreursForException(exception));
}
}
- líneas 9-17: el cálculo del salario ahora se realiza dentro de un try / catch;
- línea 27: si se produjo un error, se muestra la vista parcial [Erreurs.cshtml] utilizando como plantilla la lista de mensajes de error proporcionada por el método estático [Static.GetErreursForException(exception)].
En la clase [Static] se agrupan dos funciones de utilidad estáticas [1]:
![]() |
using System;
using System.Collections.Generic;
using System.Web.Mvc;
namespace PamWeb.Infrastructure
{
public class Static
{
// lista de mensajes de error de una excepción
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;
}
// lista de mensajes de error relacionados con un modelo no válido
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;
}
// el mensaje de error relacionado con un elemento del modelo de la acción
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;
}
}
}
- líneas 10-19: la función estática [GetErreursForException] devuelve la lista de errores de una pila de excepciones;
- líneas 22-36: la función estática [GetErreursForModel] devuelve la lista de errores de un modelo de acción inválido. El código de esta función, así como el del método privado [getErrorMessageFor] (líneas 39-54), ya se ha visto anteriormente.
Una vez hecho esto, podemos volver a probar el caso de error:
![]() |
- en [1], seleccionamos al empleado que no existe;
- en [2], se realiza la simulación;
- en [3], se muestra la nueva página de errores.
Volvamos a la acción del servidor [FaireSimulation]:
// realizar una simulación
[HttpPost]
public PartialViewResult FaireSimulation(ApplicationModel application, FormCollection data)
{
// creación del modelo de la acción
IndexModel modèle = new IndexModel() { Application = application};
// Se intenta recuperar los valores publicados en el modelo
TryUpdateModel(modèle, data);
// se calcula el salario
...
}
En la línea 8, actualizamos el modelo de la línea 6 con los valores enviados por la llamada Ajax. No verificamos la validez del modelo. Es necesario hacerlo porque no podemos saber de dónde provienen los valores enviados. Alguien podría haber manipulado un POST y enviarnos datos inválidos.
Tarea: siguiendo el modelo que desarrollamos para el caso de la excepción, modifica la acción del servidor [FaireSimulation] para que envíe una página de error cuando los datos enviados no sean válidos. Para ello, utilizaremos el método estático [GetErreursForModel] de la clase [Static].
¿Cómo probar esta modificación? En el párrafo 9.11.3, se aseguró de que la función JS [faireSimulation] no procesara los valores ingresados mediante POST si estos no eran válidos. Comente las líneas que realizan esto y luego realice la siguiente prueba:
![]() |
- en [1], se realiza la simulación con valores inválidos;
- en [2], se recupera correctamente la página de errores que acabamos de crear, lo que demuestra que los validadores del lado del servidor han funcionado correctamente.
A continuación, recuerda descomentar las líneas que acabas de comentar en la función JS [faireSimulation].
9.13. Paso 7: configuración de una sesión de usuario
La aplicación [Simulateur de calcul de paie] permite al usuario realizar diversas simulaciones de nómina con el enlace [Faire la simulation], guardarlas con el enlace [Enregistrer la simulation], visualizarlas mediante el enlace [Voir les simulations] y eliminarlas mediante el enlace [Retirer la simulation]. Sabemos que entre dos solicitudes sucesivas del usuario no hay memoria, a menos que se cree una mediante el mecanismo de la sesión (véase el párrafo 4.10). Queda bastante claro aquí que debemos conservar en la sesión la lista de simulaciones registradas a lo largo del tiempo por el usuario. Hay otros datos que deben memorizarse: cuando el usuario realiza una simulación, esta solo se registra en la lista de simulaciones si el usuario lo solicita mediante el enlace [Enregistrer la simulation]. Cuando lo hace, debemos poder recuperar la simulación calculada en la solicitud anterior. Para ello, esta también se guardará en la sesión. Por último, vamos a numerar las simulaciones a partir del 1. Para numerar correctamente una nueva simulación, es necesario haber conservado el número de la simulación anterior, nuevamente en la sesión.
En el párrafo 4.10, introdujimos el concepto de modelo de sesión como parámetro de entrada de una acción para que esta tenga acceso a la sesión. Retomaremos este concepto. Te invitamos a releer el párrafo correspondiente si este concepto te resulta confuso.
Creamos la siguiente clase [SessionModel]:
![]() |
Su código es el siguiente:
using Pam.Web.Models;
using System.Collections.Generic;
namespace Pam.Web.Models
{
public class SessionModel
{
// la lista de simulaciones
public List<Simulation> Simulations { get; set; }
// N.º de la próxima simulación
public int NumNextSimulation { get; set; }
// la última simulación
public Simulation Simulation { get; set; }
// creador
public SessionModel()
{
// lista de simulaciones vacía
Simulations = new List<Simulation>();
// N.º de la próxima simulación
NumNextSimulation = 1;
}
}
}
La clase [Simulation], en las líneas 9 y 13, registrará información sobre una simulación. ¿Qué necesitamos registrar? El enlace [Faire la simulation] calcula una nómina de tipo [FeuilleSalaire]. Parece lógico incluirla en la simulación. Además, debemos guardar la información que dio lugar a esta hoja de salario:
- el empleado seleccionado. Este se encuentra en el campo [FeuilleSalaire.Employe]. Por lo tanto, no es necesario guardarlo por segunda vez;
- el número de horas y días trabajados. Esta información no se encuentra en el tipo [FeuilleSalaire]. Por lo tanto, debemos memorizarla.
Por último, cada simulación se identifica con un número. Por lo tanto, podríamos partir de la siguiente clase [Simulation]:
using Pam.Metier.Entites;
namespace Pam.Web.Models
{
public class Simulation
{
// N.º de la simulación
public int Num { get; set; }
// el número de horas trabajadas
public double HeuresTravaillées { get; set; }
// número de días trabajados
public int JoursTravaillés { get; set; }
// la nómina
public FeuilleSalaire FeuilleSalaire { get; set; }
}
}
La acción de servidor [FaireSimulation] debe, además de calcular una nómina, crear una simulación e incluirla en la sesión. Para ello, recibirá como parámetro el modelo de la sesión:
// realizar una simulación
[HttpPost]
public PartialViewResult FaireSimulation(ApplicationModel application, SessionModel session, FormCollection data)
{
// creación del modelo de la acción
IndexModel modèle = new IndexModel() { Application = application };
// intentamos recuperar los valores registrados en el modelo
TryUpdateModel(modèle, data);
// ¿Plantilla válida?
if (!ModelState.IsValid)
{
// se muestra la página de errores
return PartialView("Erreurs", Static.GetErreursForModel(ModelState));
}
// se calcula el salario
FeuilleSalaire feuilleSalaire = null;
Exception exception = null;
try
{
// cálculo del salario
feuilleSalaire = application.PamMetier.GetSalaire(modèle.SS, modèle.HeuresTravaillées, (int)modèle.JoursTravaillés);
}
catch (Exception ex)
{
exception = ex;
}
// ¿Error?
if (exception != null)
{
// se muestra la página de errores
return PartialView("Erreurs", Static.GetErreursForException(exception));
}
// se crea una simulación y se incluye en la sesión
session.Simulation = ...
// se muestra la nómina
return PartialView("Simulation", feuilleSalaire);
}
- línea 3: la acción recibe como parámetro el modelo de la sesión;
Tarea 1: completa el código de la acción, línea 34
Tarea 2: siguiendo el procedimiento del párrafo 4.10, haz lo necesario para que el parámetro [SessionModel session] de la acción sea inicializado correctamente por el marco de trabajo. Si no se hace nada, se tendrá un puntero null para este parámetro.
9.14. Paso 8: guardar una simulación
9.14.1. El problema
Cuando hemos realizado una simulación, podemos guardarla:
![]() |

La vista parcial [Simulations.cshtml] ahora muestra la lista de simulaciones realizadas por el usuario. Recordamos que la nómina calculada es ficticia.
9.14.2. Escritura de la acción de servidor [EnregistrerSimulation]
El enlace Ajax [Enregistrer la simulation] llama a la acción de servidor [EnregistrerSimulation], cuyo código era hasta ahora el siguiente:
[HttpPost]
public PartialViewResult EnregistrerSimulation()
{
return PartialView("Simulations");
}
Ahora cambia de la siguiente manera:
// guardar una simulación
[HttpPost]
public PartialViewResult EnregistrerSimulation(SessionModel session)
{
// se guarda la última simulación realizada en la lista de simulaciones de la sesión
...
// se incrementa en la sesión el número de la próxima simulación
...
// se muestra la lista de simulaciones
...
}
- línea 1: la acción [EnregistrerSimulation] necesita tener acceso a la sesión. Por eso tiene como parámetro el modelo de la sesión.
Tarea: completar la acción de servidor [EnregistrerSimulation].
9.14.3. Escritura de la vista parcial [Simulations.cshtml]
La acción anterior [EnregistrerSimulation] muestra la vista parcial [Simulations.cshtml] con la lista de simulaciones realizadas por el usuario como modelo. Su código es el siguiente:
@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>
...
}
Tarea 1: completar el código de la vista parcial [Simulations.cshtml]. Se utilizará una tabla HTML para mostrar las simulaciones. Se pueden consultar los ejemplos del párrafo 5.4.
Nota: el enlace [retirer] de cada simulación de la tabla HTML será un enlace de JavaScript con el siguiente formato:
donde N es el número de la simulación.
Tarea 2: prueba tu aplicación realizando simulaciones. Para realizarlas, repita la siguiente secuencia: 1) cargue la página de la aplicación mediante [F5], 2) realice una simulación, 3) guárdela. Las simulaciones se acumularán en la sesión, lo cual debería reflejarse en la vista [Simulations.cshtml].
Tarea 3: mejora la vista parcial [Simulations.cshtml] de tal manera que los colores de las filas de la tabla HTML se alternen.

Se asignarán de manera alterna a las filas <tr> de la tabla HTML, las clases CSS, [pair] y [impair] definidas en la hoja de estilo [/Content/Site.css]:
.impair {
background-color: beige;
}
.pair {
background-color: lightsteelblue;
}
9.15. Paso 9: regresar al formulario de entrada
9.15.1. El problema
Una vez que obtuvimos la lista de simulaciones, podemos regresar al formulario de entrada, algo que no podíamos hacer desde hacía un tiempo:


9.15.2. Escritura de la acción del servidor [Formulaire]
El enlace Ajax [Retour au formulaire de simulation] llama a la acción de servidor [Formulaire], cuyo código era hasta ahora el siguiente:
[HttpPost]
public PartialViewResult Formulaire()
{
return PartialView("Formulaire");
}
La vista parcial [Formulaire] que muestra espera un modelo [IndexModel] (línea 1 a continuación):
@model Pam.Web.Models.IndexModel
@using (Html.BeginForm("FaireSimulation", "Pam", FormMethod.Post, new { id = "formulaire" }))
{
...
}
<div id="simulation" />
Por esta razón, el enlace [Retour au formulaire de simulation] ya no funcionaba.
Tarea: escribir la nueva versión de la acción de servidor [Formulaire] (2 líneas que reescribir) y luego realizar las pruebas.
9.15.3. Modificación de la función de JavaScript [retourFormulaire]
Con la modificación realizada anteriormente, ahora se puede regresar al formulario, pero entonces aparece una anomalía:
![]() |
- en [1], se regresa al formulario de entrada;
- en [2], se realiza una simulación con datos erróneos. Entonces se descubre que los validadores del lado del cliente ya no funcionan. En este caso, se ha llamado al servidor y este ha devuelto una página de errores gracias al trabajo realizado en el párrafo 9.12.4.
Esta anomalía se identificó y se resolvió en el apartado 7.6.7.
Tarea: siguiendo el procedimiento del párrafo 7.6.7, corrija la función de JavaScript [retourFormulaire] y luego realice pruebas para verificar que los validadores del lado del cliente vuelvan a funcionar.
9.16. Paso 10: ver la lista de simulaciones
9.16.1. El problema
Al trabajar con el formulario de simulación, se puede visualizar la lista de simulaciones que se han realizado:
![]() | ![]() |
9.16.2. Codificación de la acción del servidor [VoirSimulations]
El enlace Ajax [Voir les simulations] llama a la acción de servidor [VoirSimulations], cuyo código era hasta ahora el siguiente:
// ver las simulaciones
[HttpPost]
public PartialViewResult VoirSimulations()
{
return PartialView("Simulations");
}
La vista parcial [Simulations] que muestra espera un modelo [IEnumerable<Simulation>] (línea 1 a continuación):
@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>
...
}
Por esta razón, el enlace [Voir les simulations] ya no funcionaba.
Tarea: escribir la nueva versión de la acción de servidor [VoirSimulations] (2 líneas que reescribir) y luego realizar las pruebas.
9.17. Paso 11: finalizar la sesión
9.17.1. El problema
En cualquier momento se puede finalizar la sesión del usuario con el enlace [Ajax] [Terminer la session]. Esto tiene como efecto abandonar la sesión actual para iniciar una nueva. Además, se regresa a la vista del formulario:
![]() |
![]() |
- en [1], se realizaron dos simulaciones y luego se cerró la sesión;
- en [2], se regresó al formulario de entrada de datos. Se desea ver las simulaciones;
- en [3], debido al cambio de sesión, la lista de simulaciones ahora está vacía.
9.17.2. Codificación de la acción del servidor [TerminerSession]
El enlace Ajax [Terminer la session] llama a la acción del servidor [TerminerSession], cuyo código era hasta ahora el siguiente:
// finalizar la sesión
[HttpPost]
public PartialViewResult TerminerSession()
{
return PartialView("Formulaire");
}
La vista parcial [Formulaire] que muestra espera un modelo [IndexModel] (línea 1 a continuación):
@model Pam.Web.Models.IndexModel
@using (Html.BeginForm("FaireSimulation", "Pam", FormMethod.Post, new { id = "formulaire" }))
{
...
}
<div id="simulation" />
Por esta razón, el enlace [Terminer la session] ya no funcionaba.
Tarea: escribir la nueva versión de la acción de servidor [TerminerSession] (2 líneas que reescribir) y luego realizar las pruebas.
Nota: para cerrar la sesión en la acción, se escribe:
9.17.3. Modificación de la función de JavaScript [terminerSession]
Con la modificación realizada anteriormente, ahora se puede regresar al formulario, pero entonces aparece una anomalía, la que se describió anteriormente en el párrafo 9.15.3.
Tarea: siguiendo el procedimiento que seguiste en el párrafo 9.15.3, corrige la función de JavaScript [terminerSession] y luego realiza pruebas para verificar que los validadores del lado del cliente vuelvan a funcionar.
9.18. Paso 12: borrar la simulación
9.18.1. El problema
Cuando se ha realizado una simulación, se puede borrar con el enlace de JavaScript [Effacer la simulation]:
![]() | ![]() |
9.18.2. Registro de la acción del cliente [effacerSimulation]
La función de JavaScript [effacerSimulation] tiene, por el momento, el siguiente código:
function effacerSimulation() {
// se borran los datos ingresados en el formulario
// ...
// se oculta la simulación, si existe
$("#simulation").hide();
// menú
setMenu([lnkFaireSimulation, lnkTerminerSession, lnkVoirSimulations]);
}
Tarea: completa este código. Puedes inspirarte en el ejemplo del párrafo 7.6.6
9.19. Paso 13: eliminar una simulación
9.19.1. El problema
Cuando se está en la página de simulaciones, se pueden eliminar algunas de ellas con el enlace de JavaScript [retirer]:


9.19.2. Registro de la acción del cliente [retirerSimulation]
Los enlaces [retirer] tienen el siguiente formato: HTML:
donde N es el número de la simulación.
Tarea: siguiendo el procedimiento descrito en los párrafos 9.9.3, escriba la función JS [retirerSimulation]. Esta función realizará una llamada Ajax de tipo POST a la acción [/Pam/RetirerSimulation]. Enviará el dato N en el formato num=N.
Nota: la función JS [retirerSimulation] es similar a las otras funciones JS que has escrito y que realizan una llamada Ajax al servidor. La única novedad aquí es el envío de un valor que no se encuentra en un formulario. Sabemos que los valores enviados se agrupan en una cadena de caracteres con el formato:
por lo que la función JS [retirerSimulation] tendrá la siguiente forma:
function retirerSimulation(N) {
// se realiza una llamada Ajax manualmente
$.ajax({
url: '/Pam/RetirerSimulation',
...
data:"num="+N,
...
});
// menú
setMenu([lnkRetourFormulaire, lnkTerminerSession]);
}
- línea 6: la propiedad [data] de una llamada Ajax JQuery representa la cadena enviada al servidor.
9.19.3. Escritura de la acción del servidor [RetirerSimulation]
La acción del servidor [RetirerSimulation]:
- recibe un parámetro enviado llamado [num], que es el número de una simulación;
- debe eliminar de la lista de simulaciones registradas en la sesión la simulación que tiene ese número;
- luego debe mostrar la nueva lista de simulaciones.
Tarea: escribe la acción de servidor [RetirerSimulation]. Revisa el párrafo 4.1 para saber cómo recuperar el parámetro enviado por POST llamado [num].
9.20. Paso 14: mejora del método de inicialización de la aplicación
Nuestra aplicación web está terminada. Funciona con una clase [métier] simulada. Recordemos la arquitectura que hemos desarrollado:
![]() |
Quedan algunos detalles por resolver antes de pasar a la implementación real de la capa [métier], y esto se lleva a cabo en el método de inicialización de la aplicación: el método [Application_Start] en [Global.asax]:
![]() |
El método [Application_Start] en [Global.asax] se ejecuta una sola vez al iniciar la aplicación. Es ahí donde se puede aprovechar el archivo de configuración [Web.config]. Por el momento, nuestro método [Application_Start] se ve así:
// aplicación
protected void Application_Start()
{
// ----------Generado automáticamente
AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
// -------------------------------------------------------------------
// ---------- configuración específica
// -------------------------------------------------------------------
// datos de ámbito de la aplicación
ApplicationModel application = new ApplicationModel();
Application["data"] = application;
// instanciación de capa [métier]
application.PamMetier = new PamMetier();
...
// enlazadores de modelos
...
}
En la línea 17, la capa de negocio se instancia mediante el operador new. Por otra parte, el modelo de la aplicación se define de la siguiente manera:
public class ApplicationModel
{
// --- datos de ámbito de la aplicación ---
public Employe[] Employes { get; set; }
public IPamMetier PamMetier { get; set; }
public SelectListItem[] EmployesItems { get; set; }
}
En la línea 5 anterior, vemos que el tipo de la propiedad [PamMetier] es el de la interfaz [IPamMetier]. Esto significa que esta propiedad puede ser inicializada por cualquier objeto que implemente dicha interfaz. Sin embargo, en la línea 17 de [Application_Start], hemos escrito de forma estática el nombre de una clase que implementa [IPamMetier]. Por lo tanto, si la capa [métier] llegara a implementarse con una nueva clase que implemente [IPamMetier], habría que modificar esta línea. No es algo muy importante, pero se puede evitar. La definición de la clase de implementación de la interfaz [IPamMetier] se puede trasladar a un archivo de configuración. Para cambiar la implementación, simplemente se modifica el contenido de ese archivo de configuración. No es necesario modificar el código .NET.
Aquí utilizaremos el contenedor de inyección de dependencias [Spring.net]. Existen otros marcos de trabajo .NET para hacer lo mismo, tal vez de mejor manera y con mayor simplicidad.
La arquitectura del proyecto evoluciona de la siguiente manera:
![]() |
- en [A], el método de inicialización de la capa [ASP.NET MVC] solicitará a [Spring.net] una referencia sobre la capa [métier] simulada;
- en [B], [Spring.net] creará la capa simulada [métier] utilizando su archivo de configuración para determinar qué clase debe instanciar;
- en [C], [Spring.net] devolverá la referencia de la capa simulada [métier] a la capa [ASP.NET MVC].
Cabe señalar que, por defecto, los objetos administrados por [Spring.net] son singletons: solo existe un único ejemplar de cada uno. Así, si más adelante en nuestro ejemplo, el código vuelve a solicitar a [Spring.net] una referencia a la capa [métier] simulada, [Spring.net] simplemente devuelve la referencia al objeto creado inicialmente.
9.20.1. Incorporación de las referencias [Spring] al proyecto web
Vamos a utilizar [Spring.net]. Este marco de trabajo viene en forma de DLL, que hay que agregar a las referencias del proyecto. Se puede proceder de la siguiente manera:
![]() |
En [1], haz clic con el botón derecho en la rama [References] del proyecto y selecciona la opción [Gérer les packages NuGet]. Se requiere una conexión a Internet. A continuación, se procederá de la misma manera que se hizo anteriormente con la biblioteca JQuery [Globalize]. Se buscará la palabra clave [Spring.core] y se instalará este paquete. La instalación incluye dos DLL: [Spring.core], [2] y [Common.Logging], [3]. En los siguientes ejemplos, se utilizó la versión 1.3.2 de Spring.
Nota: si no cuenta con conexión a Internet, encontrará estos archivos DLL en una carpeta [lib] del material de apoyo de este caso de estudio.
9.20.2. Configuración de [web.config]
La definición de la clase de implementación de la interfaz [IPamMetier] se encuentra en el archivo [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>
<!-- Configuración de 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>
...
- líneas 2-8: localice la etiqueta <configSections> del archivo e inserte en ella las líneas 4-7;
- línea 4: el atributo [name="spring"] proporciona información sobre la sección [spring] de las líneas 10 a 17;
- línea 5: define la clase [Spring.Context.Support.DefaultSectionHandler], ubicada en DLL [Spring.Core], como la capaz de procesar la sección [objects] de las líneas 14 a 16;
- línea 6: define la clase [Spring.Context.Support.ContextHandler], ubicada en DLL y [Spring.Core], como la capaz de procesar la sección [context] de las líneas 11 a 13;
- líneas 11-13: esta sección proporciona la información [<resource uri="config://spring/objects" />], que indica que los objetos Spring se encuentran en el archivo de configuración, en la sección [/spring/objects], es decir, en las líneas 14-16;
- líneas 14-16: la etiqueta [objects] introduce los objetos Spring;
- línea 15: define un objeto identificado por [id="pammetier"], que es una instancia de la clase [Pam.Metier.Service.PamMetier] ubicada en DLL [pam-metier-simule]. Aquí no hay que equivocarse. Para el atributo [id], puedes poner lo que quieras. Vas a utilizar este identificador en [Global.asax]. La clase [Pam.Metier.Service.PamMetier] es la de nuestra capa simulada [métier]. Hay que volver a su definición para conocer su nombre completo:
namespace Pam.Metier.Service
{
public class PamMetier : IPamMetier
{
...
Para el DLL [pam-metier-simule], hay que revisar las propiedades del proyecto C# [pam-metier-simule]:
![]() |
Se debe utilizar el nombre indicado en [1].
9.20.3. Modificación de [Application_Start]
El método [Application_Start] cambia de la siguiente manera:
using Spring.Context.Support;
// aplicación
protected void Application_Start()
{
// ----------Generado automáticamente
AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
// -------------------------------------------------------------------
// ---------- configuración específica
// -------------------------------------------------------------------
// datos de ámbito de la aplicación
ApplicationModel application = new ApplicationModel();
Application["data"] = application;
// instanciación de la capa [métier]
application.PamMetier = ContextRegistry.GetContext().GetObject("pammetier") as IPamMetier;
...
// enlazadores de modelos
...
}
- línea 19: se utiliza la clase Spring [ContextRegistry], que es una clase capaz de procesar el archivo [web.config]. Para ello, es necesario importar el espacio de nombres de la línea 1. El método estático [GetContext] permite obtener el contenido de las etiquetas [context], que indican dónde se encuentran los objetos de Spring. El método estático [GetObject] permite, a continuación, obtener un objeto específico identificado por su atributo id. Cabe señalar que ahora el nombre de la clase que implementa la interfaz [IPamMetier] ya no está escrito de forma fija en el código. Ahora se encuentra en el archivo [web.config].
Después de realizar todos estos cambios, prueba tu aplicación. Debería funcionar.
9.20.4. Manejo de un error de inicialización de la aplicación
En el método [Application_Start] escribimos:
application.PamMetier = ContextRegistry.GetContext().GetObject("pammetier") as IPamMetier;
La instrucción a la derecha del signo = puede fallar. Hay varias razones para ello:
- la más obvia es que nos equivocamos en el nombre del objeto que se va a instanciar;
- otra es que la instanciación de la capa [métier] no se realice correctamente. Esto no puede ser el caso para nuestra capa simulada [métier], pero sí podría serlo para nuestra capa real [métier], que estará conectada a una base de datos. Es posible que la capa SGBD no se ejecute, que la información sobre la base de datos que se debe gestionar sea incorrecta, etc.
Vamos a manejar una posible excepción en un try / catch. El código queda así:
// aplicación
protected void Application_Start()
{
// ----------Generado automáticamente
...
// -------------------------------------------------------------------
// ---------- configuración específica
// -------------------------------------------------------------------
// datos de ámbito de la aplicación
ApplicationModel application = new ApplicationModel();
Application["data"] = application;
application.InitException = null;
try
{
// instanciación de la capa [métier]
application.PamMetier = ContextRegistry.GetContext().GetObject("pammetier") as IPamMetier;
}
catch (Exception ex)
{
application.InitException = ex;
}
//si no hay error
if (application.InitException == null)
{
....
}
// enlazadores de modelos
...
}
- En la línea 12, introducimos una nueva propiedad llamada [InitException] en el modelo de la aplicación:
public class ApplicationModel
{
// --- datos de ámbito de la aplicación ---
public Employe[] Employes { get; set; }
public IPamMetier PamMetier { get; set; }
public SelectListItem[] EmployesItems { get; set; }
public Exception InitException { get; set; }
}
- línea 7 anterior, la excepción que podría ocurrir durante la inicialización de la aplicación;
- líneas 13-21 de [Application_Start]: la instanciación de la capa [métier] ahora se realiza dentro de un try / catch;
- línea 20: se almacena la excepción;
- líneas 23-26: si no hubo ningún error, se ejecuta el código que teníamos anteriormente;
- línea 28: se crean los [ModelBinders] independientemente de si hubo un error o no. Esto es importante. Queremos asegurarnos de que el marco de trabajo vincule correctamente el modelo de la aplicación [ApplicationModel].
Sabemos que, al iniciar la aplicación, se ejecuta la acción de servidor [Index]. Por el momento, es la siguiente:
[HttpGet]
public ViewResult Index(ApplicationModel application)
{
return View(new IndexModel() { Application = application });
}
En la línea 2, la acción [Index] recibe el modelo de la aplicación. De esta manera, puede determinar si la inicialización se realizó correctamente o no, y mostrar una página de errores si la inicialización falló de alguna manera. Modificamos el código de la siguiente manera:
[HttpGet]
public ViewResult Index(ApplicationModel application)
{
// ¿Error de inicialización?
if (application.InitException != null)
{
// página de errores sin menú
return View("InitFailed",Static.GetErreursForException(application.InitException));
}
// sin error
return View(new IndexModel() { Application = application });
}
Línea 8: en caso de error de inicialización, mostramos la vista [InitFailed.cshtml] con la lista de mensajes de error de la excepción que se produjo durante la inicialización como modelo. El método [Static.GetErreursForException] se presentó y explicó en el párrafo 9.12.4. La vista [InitFailed.cshtml] será la siguiente:
![]() |
Su código es el siguiente:
@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>
- línea 1: la plantilla de la vista es una lista de mensajes de error. Estos se muestran en una lista HTML en las líneas 24 a 29;
- línea 3: esta vista no utiliza la página maestra [_Layout.cshtml]. De hecho, no se desea el menú que incluye ese documento. Por lo tanto, se crea una página HTML completa (líneas 5 a 23).
Para probarlo, basta con modificar en [Application_Start] la instanciación de la capa [métier] de la siguiente manera:
try
{
// instanciación de la capa [métier]
application.PamMetier = ContextRegistry.GetContext().GetObject("xx") as IPamMetier;
}
catch (Exception ex)
{
application.InitException = ex;
}
En la línea 4, se busca un objeto que no existe entre los objetos de Spring.
Al validar estos cambios y ejecutar la aplicación, se obtiene la siguiente página:
![]() |
Aparece una página de errores sin menú. El usuario no puede hacer nada más que constatar el error. Esto es lo que se buscaba.
9.21. ¿En qué punto estamos?
Ahora contamos con una aplicación web operativa que funciona con una capa de negocio simulada. Su arquitectura es la siguiente:
![]() |
La capa [ASP.NET MVC] interactúa con la capa de negocio simulada a través de la interfaz [IPamMetier]. Si reemplazamos esta capa de negocio simulada por una capa de negocio real que cumpla con esta interfaz, no tendremos que modificar el código de la capa web. Gracias a [Spring.net], solo tendremos que cambiar en [web.config] la clase de implementación de la interfaz [IPamMetier]. Seguiremos por este camino.
La nueva arquitectura será la siguiente:
![]() |
Describiremos sucesivamente:
- la capa [EF5] conectada a SGBD. Se implementará con Entity Framework 5 (EF5);
- la capa [DAO], que administra el acceso a los datos a través de la capa [EF5]. Esto le permite ignorar la existencia de SGBD. Esta capa se limita a manipular las entidades de la aplicación [Employe, Cotisations, Indemnites];
- la capa [métier], que implementa el cálculo del salario.
La nueva arquitectura es la que se presentó al principio de este documento en el párrafo 1.1, y que ahora recordamos:
![]() |
- la capa [Web] es la capa que interactúa con el usuario de la aplicación web. Este interactúa con la aplicación web a través de páginas web visualizadas por un navegador. Es en esta capa donde se encuentran ASP.NET y MVC, y únicamente en esta capa;
- la capa [métier] implementa las reglas de gestión de la aplicación, tales como el cálculo de un salario o de una factura. Esta capa utiliza datos provenientes del usuario a través de la capa [Web] y de SGBD a través de la capa [DAO];
- la capa [DAO] (Objetos de Acceso a Datos), la capa [ORM] (Mapeador Objeto-Relacional) y el conector ADO.NET gestionan el acceso a los datos de la capa SGBD. La capa [ORM] actúa como puente entre los objetos manipulados por la capa [DAO] y las filas y columnas de los datos de una base de datos relacional. Actualmente se utilizan dos ORM en todo el mundo: NET, NHibernate (http://sourceforge.net/projects/nhibernate/) y Entity Framework (http://msdn.microsoft.com/en-us/data/ef.aspx);
- la integración de las capas puede realizarse mediante un contenedor de inyección de dependencias (Dependency Injection Container) como Spring (http://www.springframework.net/);
Las capas [métier], [DAO] y [EF5] se implementarán mediante proyectos en C#. A partir de ahora, trabajaremos con Visual Studio Express 2012 para escritorio.
9.22. Paso 15: implementación de la capa Entity Framework 5
![]() |
La creación de la capa [EF5] tiene menos que ver con la programación que con la configuración. Para comprender cómo escribir esta capa, se debe leer el documento [Introduction à Entity Framework 5 Code First], disponible en URL [http://tahe.developpez.com/dotnet/ef5cf-02/]. Es un documento bastante extenso. Los conceptos fundamentales se encuentran en los primeros cuatro capítulos. Se indicarán los párrafos que deben leerse con mayor atención. Cuando hagamos referencia a este documento, utilizaremos la notación [refEF5].
Además, en ocasiones necesitaremos conceptos de C#. En ese caso, haremos referencia al curso [Introduction au langage C#], disponible en URL [http://tahe.developpez.com/dotnet/csharp/], con la notación [refC#].
9.22.1. La base de datos
La base de datos de la aplicación se presentó en el párrafo 9.4. Se trata de una base de datos MySQL denominada [dbpam_ef5] (pam = Pago de Cuidadoras de Niños). Esta base de datos tiene un administrador llamado root sin contraseña.
Recordemos el esquema de la base de datos. Cuenta con tres tablas:

Existe una relación de clave externa entre la columna EMPLOYES (INDEMNITE_ID) y la columna INDEMNITES (ID). Parte de la estructura de esta base de datos viene determinada por su uso con EF5.
El script SQL para la creación de la base de datos es el siguiente:
-- phpMyAdmin SQL Volcado
-- versión 3.5.1
-- http://www.phpmyadmin.net
--
-- Cliente: localhost
-- Generado el: lunes 4 de noviembre de 2013 a las 09:34
-- Versión del servidor: 5.5.24-log
-- Versión de 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 */;
--
-- Base de datos: `dbpam_ef5`
--
-- --------------------------------------------------------
--
-- Estructura de la tabla `cotizaciones`
--
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 ;
--
-- Contenido de la tabla `cotizaciones`
--
INSERT INTO `cotisations` (`ID`, `SECU`, `RETRAITE`, `CSGD`, `CSGRDS`, `VERSIONING`) VALUES
(11, 9.39, 7.88, 6.15, 3.49, 1);
--
-- Desencadenadores de `cotizaciones`
--
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 ;
-- --------------------------------------------------------
--
-- Estructura de la tabla `empleados`
--
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 ;
--
-- Contenido de la tabla `empleados`
--
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);
--
-- Desencadenantes de `empleados`
--
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 ;
-- --------------------------------------------------------
--
-- Estructura de la tabla `indemnizaciones`
--
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 ;
--
-- Contenido de la tabla `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);
--
-- Desencadenadores de `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 ;
--
-- Restricciones para las tablas exportadas
--
--
-- Restricciones para la tabla `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 */;
Cabe destacar los siguientes puntos:
- líneas 30, 73, 122: las claves primarias de las tablas están en modo [AUTO_INCREMENT]. Es MySQL quien las administra y no EF5;
- línea 83: el n.º SS tiene una restricción de unicidad;
- línea 130: el índice del empleado tiene una restricción de unicidad;
- líneas 168-169: la clave externa de la tabla [employes] hacia la tabla [indemnites];
- línea 49: un disparador, [Trigger], es un script SQL integrado en SGBD y que se ejecuta en determinados momentos;
- líneas 51-54: el disparador [INCR_VERSIONING_COTISATIONS] se activa antes de cualquier modificación de una fila de la tabla [cotisations]. A continuación, incrementa en una unidad la columna [VERSIONING];
- líneas 59-62: el disparador [START_VERSIONING_COTISATIONS] se activa antes de cualquier inserción de una nueva fila en la tabla [cotisations]. A continuación, inicializa la columna [VERSIONING] a 1;
- al final, la columna [VERSIONING] toma el valor 1 cuando se crea una fila en la tabla [cotisations] y luego se incrementa en 1 con cada modificación que se realiza en esa fila. Este mecanismo permite que EF5 gestione la concurrencia en el acceso a una fila de la tabla [cotisations] de la siguiente manera:
- un proceso P1 lee una fila L de la tabla [cotisations] en el momento T1. La fila tiene en la columna [VERSIONING] el valor V1;
- un proceso P2 lee la misma fila L de la tabla [cotisations] en el momento T2. La fila tiene las columnas [VERSIONING] y V1 porque el proceso P1 aún no ha validado su modificación;
- El proceso P1 modifica la línea L y valida su modificación. La columna [VERSIONING] de la línea L pasa entonces a V1+1 debido al disparador [INCR_VERSIONING_COTISATIONS];
- luego, el proceso P2 hace lo mismo. EF5 lanza entonces una excepción porque el proceso P2 tiene una línea con una columna [VERSIONING] cuyo valor V1 es diferente al que se encuentra en la base de datos, que es V1+1. Solo se puede modificar una fila si se tiene el mismo valor de [VERSIONING] que en la base de datos.
A esto se le llama gestión optimista de accesos concurrentes. Con EF5, un campo que desempeñe esta función debe tener la anotación [ConcurrencyCheck].
- Se crea un mecanismo similar para la tabla [employes] (líneas 98-113) y la tabla [indemnites] (líneas 144-159).
Tarea: Crea la base de datos MySQL y [dbpam_ef5] utilizando el script SQL anterior. La base [dbpam_ef5] debe crearse previamente, ya que el script no la crea. A continuación, se ejecutará el script SQL en esta base.
9.22.2. El proyecto de Visual Studio
Con Visual Studio Express 2012 para escritorio, cargamos la solución [pam-td] utilizada durante la construcción de la capa [web]:
![]() |
- En [1], VS, Visual Studio Express 2012 para escritorio no logra cargar el proyecto web [pam-web-01]. Esto es normal y no supone ningún inconveniente;
- en [2], se agrega un nuevo proyecto a la solución [pam-td];
![]() |
- en [3], el proyecto es del tipo [console] y se llama [4] [pam-ef5];
- en [5], el proyecto creado. Su nombre no aparece en negrita, por lo que no es el proyecto de inicio de la solución;
![]() |
- en [6] y [7], se define el nuevo proyecto como proyecto de inicio.
9.22.3. Agregar las referencias necesarias al proyecto
Veamos el proyecto en su conjunto:
![]() |
Nuestro proyecto necesita varios DLL:
- el DLL de Entity Framework 5;
- el DLL del conector ADO.NET del SGBD MySQL.
El párrafo 4.2 de [refEF5] explica cómo instalar estos DLL utilizando la herramienta [NuGet]. Actualmente (noviembre de 2013), la versión disponible de Entity Framework es la versión 6 (EF6). Lamentablemente, parece que el conector ADO.NET de los archivos SGBD y MySQL disponibles (noviembre de 2013) a través de [NuGet] no sea compatible con EF6. Por ello, se ha colocado en una carpeta [lib] [1] la DLL deEF5, así como los demás DLL necesarios para el proyecto [pam-ef5]
![]() |
Hemos colocado otros DLL en la carpeta [lib]. Los utilizaremos más adelante. En [2], agregamos estos nuevos DLL al proyecto.
![]() |
- en [3], navegamos por el sistema de archivos hasta la carpeta [lib];
- en [4], se seleccionan los tres DLL y luego se confirma dos veces;
- en [5], los tres DLL se han agregado a las referencias del proyecto.
Necesitamos otro DLL. Este se encontrará entre los del marco .NET de la máquina.
![]() |
- en [1], agrega una nueva referencia al proyecto;
![]() |
- en [2], seleccione [Assemblys];
- en [3], escriba [system.component];
- en [4], seleccione el ensamblado [System.ComponentModel.DataAnnotations];
- en [5], se ha agregado la referencia.
Ahora estamos listos para programar y configurar.
9.22.4. Las entidades de Entity Framework
Las entidades de Entity Framework son clases en las que se encapsulan las filas de las diferentes tablas de la base de datos. Recordemos cuáles son:

En la capa [web], habíamos utilizado las entidades [Employe, Cotisations, Indemnités] (véase el párrafo 9.7.3, página 219). Estas no eran representaciones fieles de las tablas. Por lo tanto, las columnas [ID, VERSIONING] se habían ignorado. En este caso, no será así, ya que son utilizadas por las entidades ORM y EF5. Por lo tanto, les agregaremos las propiedades que faltan. Creamos estas entidades en una carpeta [Models] del proyecto:
![]() |
Su nuevo código es ahora el siguiente:
Clase [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; }
// firma
public override string ToString()
{
return string.Format("Cotisations[{0},{1},{2},{3}, {4}, {5}]", Id, Versioning, CsgRds, Csgd, Secu, Retraite);
}
}
}
- línea 3: el espacio de nombres se ha adaptado al nuevo proyecto;
- se han agregado las propiedades de las líneas 7 y 12 para reflejar la estructura de la tabla [cotisations];
- línea 17: el método [ToString] ahora muestra los dos campos nuevos.
Clase [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; }
// firma
public override string ToString()
{
return string.Format("Indemnités[{0},{1},{2},{3},{4}, {5}, {6}]", Id, Versioning, Indice, BaseHeure, EntretienJour, RepasJour, IndemnitesCp);
}
}
}
- línea 3: se ha adaptado el espacio de nombres al nuevo proyecto;
- se han agregado las propiedades de las líneas 7 y 13 para reflejar la estructura de la tabla [indemnites];
- línea 18: el método [ToString] ahora muestra los dos campos nuevos.
Clase [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; }
// firma
public override string ToString()
{
return string.Format("Employé[{0},{1},{2},{3},{4},{5}, {6}, {7}]", Id, Versioning, SS, Nom, Prenom, Adresse, Ville, CodePostal);
}
}
}
- línea 3: se ha adaptado el espacio de nombres al nuevo proyecto;
- se han agregado las propiedades de las líneas 8 y 16 para reflejar la estructura de la tabla [employes];
- línea 21: el método [ToString] ahora muestra los dos campos nuevos.
Para que ORM y EF5 puedan utilizarlas, las propiedades de estas clases deben estar decoradas con anotaciones.
Tarea: con la ayuda del párrafo 3.4 de [Création de la base à partir des entités] de [refEF5], agrega a las entidades [Employe, Cotisations, Indemnites] las anotaciones necesarias para EF5.
Consejos:
- solo se trata de crear anotaciones. No sigas la parte [création de base] del párrafo mencionado;
- para la anotación [Table], siga el ejemplo MySQL del párrafo 4.2 de [refEF5];
- para la anotación [ConcurrencyCheck] sobre la propiedad [Versioning], siga el ejemplo de Oracle del párrafo 5.2 de [refEF5];
- para la clave externa que tiene la tabla [employes] sobre la tabla [indemnités], siga el ejemplo 3.4.2 de [refEF5]. De esta manera, agregará una nueva propiedad a la entidad [Employe]:
public int IndemniteId { get; set; }
cuyo valor será el de la columna [INDEMNITES_ID] de la tabla [employes]. Debe asignar las anotaciones de clave externa a las propiedades [IndemniteId] y [Indemnites] de la entidad [Employe]. Para ello, siga el ejemplo 3.4.2 de [refEF5];
- no gestionarás las relaciones inversas de las claves externas;
- este trabajo requiere leer un poco de [refEF5].
9.22.5. Configuración de ORM y EF5
Pongamos el proyecto en contexto:
![]() |
La capa [EF5] accederá a la base de datos a través del conector [ADO.NET] del SGBD MySQL. Necesita cierta información para acceder a esta base de datos. Dicha información se encuentra en distintos lugares del proyecto.
En primer lugar, debemos crear el contexto de la base de datos. Este contexto es una clase derivada de la clase del sistema [System.Data.Entity.DbContext]. Sirve para definir las imágenes de los objetos de las tablas de la base de datos. Colocaremos esta clase en la carpeta [Models] del proyecto junto con las entidades EF5:
![]() |
La clase [DbPamContext] será la siguiente:
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; }
}
}
- línea 6: la clase [DbPamContext] deriva de la clase del sistema [DbContext];
- líneas 8-10: los objetos de imagen de las tres tablas de la base de datos. Su tipo es [DbSet<Entity>], donde [Entity] es una de las entidades de Entity Framework que acabamos de definir. El tipo [DbSet] puede considerarse como una colección de entidades. Se puede consultar mediante LINQ (Language INtegrated Query). Si el lector no conoce LINQ, se le invita a leer el párrafo 3.5.4 [Apprentissage de LINQ avec LINQPad] de [refEF5].
En adelante, nos referiremos a la clase [DbPamContext] como el contexto de persistencia de la base de datos [dbpam_ef5]. Esta es una terminología habitual en los ORM (mapeadores objeto-relacionales). Este contexto de persistencia es una representación objetiva de la base de datos. También se habla de sincronización del contexto de persistencia con la base de datos: las modificaciones, adiciones y eliminaciones realizadas en el contexto de persistencia se reflejan en la base de datos. Esta sincronización se lleva a cabo en momentos específicos: al cerrar el contexto de persistencia, al finalizar una transacción o antes de una consulta SQL SELECT en la base de datos.
La información sobre SGBD y la base de datos se almacena en [App.config].
![]() |
La configuración necesaria en [app.config] se explica en los siguientes párrafos de [refEF5]:
- 3.4 para el servidor SGBD SQL. Ahí es donde se establecen los principios fundamentales de la configuración de EF5;
- 4.2 para el servidor SGBD MySQL.
Seguimos este último párrafo y configuramos el archivo [app.config] de la siguiente manera:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
<!-- configuración EF5 -->
<!-- cadena de conexión a la base de datos [dbam_ef5] -->
<connectionStrings>
<add name="DbPamContext"
connectionString="Server=localhost;Database=dbpam_ef5;Uid=root;Pwd=;"
providerName="MySql.Data.MySqlClient" />
</connectionStrings>
<!--, el proveedor de fábrica de 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>
- se han agregado las líneas 6 a 21. Deben insertarse dentro de la etiqueta <configuration> de las líneas 2 y 22;
- líneas 8-12: definen cadenas de conexión a bases de datos, un concepto de ADO.NET (véase el párrafo 7.3.5 en [refC#]);
- líneas 9-11: definen la cadena de conexión a la base de datos MySQL [dbpam_ef5];
- línea 9: el nombre de la cadena de conexión. Aquí no se puede poner cualquier cosa. Por defecto, hay que poner el nombre de la clase que implementa el contexto de la base de datos:
public class DbPamContext : DbContext
{
public DbSet<Employe> Employes { get; set; }
public DbSet<Cotisations> Cotisations { get; set; }
public DbSet<Indemnites> Indemnites { get; set; }
}
La clase se llama [DbPamContext]. En la línea 9 de [app.config], hay que poner entonces [name="DbPamContext"];
- línea 10: una cadena de conexión específica para SGBD MySQL:
- [Server=localhost]: dirección IP de la máquina que aloja el SGBD. En este caso, es la máquina local [localhost];
- [Database=dbpam_ef5;]: nombre de la base de datos,
- [Uid=root;]: nombre de usuario con el que nos conectaremos a la base de datos,
- [Pwd=;]: contraseña de ese nombre de usuario. Aquí no hay contraseña;
- línea 10: [providerName="MySql.Data.MySqlClient"] es el nombre del conector ADO.NET que se debe utilizar. Este nombre es el del atributo [invariant] de la línea 17. Se puede poner cualquier cosa siempre y cuando se respete la regla anterior y no se haya registrado ya un proveedor con el mismo invariante;
- líneas 15-20: definen una fábrica (factory) de conectores (provider) ADO.NET. El [DbProviderFactory] es un concepto un poco confuso para mí. A juzgar por su nombre, se trataría de una clase capaz de generar el conector ADO.NET que da acceso al SGBD, en este caso MySQL5. Por lo general, estas líneas se copian y pegan. Son necesarias. Presta atención al atributo [Version=6.5.4.0] de la línea 16. Este número de versión debe coincidir con el número de versión de DLL [MySql.Data] que agregaste a las referencias del proyecto:
![]() |
- La línea 16 es importante. Como no se pueden instalar dos proveedores con el mismo nombre, primero hay que eliminar cualquier proveedor ya instalado que tenga el mismo nombre que el que vamos a instalar en la línea 17;
Eso es todo. Es complicado y confuso la primera vez que lo haces, pero con el tiempo se vuelve sencillo porque siempre es lo mismo que se repite.
9.22.6. Prueba de la capa [EF5]
Estamos listos para probar nuestra capa [EF5]. Lo hacemos con ayuda del programa [Program.cs] que ya está presente:
![]() |
Vamos a mostrar el contenido de la base de datos. Si lo logramos, será un primer indicio de que nuestra configuración es correcta. En el párrafo 3.5.3 de [refEF5] hay un ejemplo de código disponible. El código de [Program.cs] será el siguiente:
using Pam.EF5.Entites;
using Pam.Models;
using System;
namespace Pam
{
class Program
{
static void Main(string[] args)
{
try
{
using (var context = new DbPamContext())
{
// se muestra el contenido de las tablas
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;
}
}
}
}
- línea 13: toda operación sobre BD se realiza a través del contexto de esta base de datos. Hemos implementado este contexto con la clase [DbPamContext]. También lo hemos denominado contexto de persistencia de la base de datos;
- líneas 13 y 31: las operaciones sobre el contexto de persistencia se realizan dentro de una cláusula [using]. El contexto de persistencia se abre al inicio de la cláusula [using] y se cierra automáticamente al salir de dicha cláusula. Esto implica que cualquier modificación realizada en el contexto de persistencia dentro de la cláusula [using] se reflejará en la base de datos al salir de la cláusula. A continuación, se envía una serie de órdenes SQL a la orden BD dentro de una transacción. Esto significa que, si una orden SQL falla, todas las órdenes SQL emitidas previamente se cancelan. A continuación, EF5 lanza una excepción;
- línea 17: la expresión [context.Employes] hace referencia a la imagen objeto de la tabla [employes]. Cabe recordar que [Employes] es una propiedad del contexto de persistencia [DbPamContext]:
public class DbPamContext : DbContext
{
public DbSet<Employe> Employes { get; set; }
public DbSet<Cotisations> Cotisations { get; set; }
public DbSet<Indemnites> Indemnites { get; set; }
}
- línea 17: el hecho de que [foreach] recorra la colección [context.Employes] traerá a todos los empleados de la base de datos al contexto de persistencia. Por lo tanto, EF5 emitirá una orden SQL SELECT;
- líneas 17-20: se recorre la colección de empleados y, en la línea 19, se utiliza el método [ToString] de la clase [Employe] para mostrar a los empleados en la consola;
- líneas 21-25: lo mismo para la colección de indemnizaciones;
- líneas 27-30: lo mismo para la colección de cotizaciones.
Volvamos a la definición de la entidad [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; }
// firma
public override string ToString()
{
return string.Format("Employé[{0},{1},{2},{3},{4},{5}, {6}, {7}]", Id, Versioning, SS, Nom, Prenom, Adresse, Ville, CodePostal);
}
}
}
- línea 15: un empleado tiene una referencia a una indemnización.
Cuando se recupera a un empleado en el contexto de persistencia, ¿se recupera también su indemnización? La respuesta es no, por defecto. Este es el concepto de [Lazy Loading]. Las entidades a las que se hace referencia dentro de otra entidad no se incorporan al contexto de persistencia junto con esa otra entidad. Solo se incorporan cuando el código las solicita dentro de un contexto de persistencia abierto. Si el contexto de persistencia está cerrado, se lanza una excepción.
Por lo tanto, si el método [ToString] hubiera hecho referencia a la propiedad [Indemnites] de la siguiente manera:
// firma
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);
}
la siguiente operación en [Program.cs]:
foreach (Employe employe in context.Employes)
{
Console.WriteLine(employe);
}
habría devuelto al contexto de persistencia no solo a los empleados, sino también sus prestaciones, ya que en la línea 3 se invoca el método [Employe.ToString] y este hace referencia a la entidad [Indemnites].
La ejecución de [Program.cs] arroja los siguientes resultados:
¿Qué hacer si no funciona? Estás en problemas... Hay muchas posibles causas de error:
- verifica la configuración de EF5 (párrafo 9.22.5);
- verifica tus entidades de Entity Framework (párrafo 9.22.4).
9.22.7. DLL de la capa [EF5]
Convertimos nuestro proyecto en una biblioteca de clases para que, al generarlo, se cree un ensamblado .dll en lugar de un .exe. Esto se realiza en las propiedades del proyecto, tal como se vio en el párrafo 9.7.6, para la capa de negocio simulada.
Tarea: cambie el tipo del proyecto [pam-ef5] a biblioteca de clases y luego vuelva a generar el proyecto.
9.23. Paso 16: configuración de la capa [DAO]
9.23.1. La interfaz de la capa [DAO]
![]() |
Al igual que hicimos con la capa simulada [métier], se podrá acceder a la capa [DAO] a través de una interfaz. ¿Cuál será?
Veamos la interfaz [IPamMetier] de la capa simulada [métier] que hemos construido:
public interface IPamMetier {
// lista de todas las identidades de los empleados
Employe[] GetAllIdentitesEmployes();
// ------- el cálculo del salario
FeuilleSalaire GetSalaire(string ss, double heuresTravaillées, int joursTravaillés);
}
En la línea 3, el método [GetAllIdentitesEmployes] sirve para alimentar la lista desplegable de la página de inicio:
![]() |
Estos empleados deberán buscarse en la base de datos.
Línea 6: el método [GetSalaire] permite calcular la nómina de un empleado cuyo número es SS. Recordemos la definición del tipo [FeuilleSalaire]:
public class FeuilleSalaire
{
// propiedades automáticas
public Employe Employe { get; set; }
public Cotisations Cotisations { get; set; }
public ElementsSalaire ElementsSalaire { get; set; }
}
La información de las líneas 5 y 6 provendrá de la base de datos. Recordemos que un empleado tiene una propiedad [Indemnites]. Esta información también deberá recuperarse.
Por lo tanto, podríamos comenzar con la siguiente interfaz para la capa [DAO]:
public interface IPamDao {
// lista de todas las identidades de los empleados
Employe[] GetAllIdentitesEmployes();
// un empleado específico con sus prestaciones
Employe GetEmploye(string ss);
// lista de todas las cotizaciones
Cotisations GetCotisations();
}
9.23.2. El proyecto de Visual Studio
Tarea: agrega a la solución [pam-td] un nuevo proyecto de tipo [console] llamado [pam-dao]. Establece este proyecto como el proyecto de inicio de la solución.
![]() |
9.23.3. Agregar las referencias necesarias al proyecto
Veamos el proyecto en su conjunto:
![]() |
El proyecto [pam-dao] necesita una cierta cantidad de DLL:
- todos los que están referenciados por el proyecto [pam-ef5];
- el del propio proyecto [pam-ef5].
Además, vamos a utilizar [Spring.net] para instanciar la capa [DAO]. Para ello, necesitamos los archivos DLL, [Spring.core] y [Common.Logging]. Estos DLL se encuentran en la carpeta [lib] del material de apoyo del caso de estudio.
Tarea: agrega estas referencias al proyecto [pam-dao].
![]() |
9.23.4. Implementación de la capa [DAO]
![]() |
En lo anterior, la clase [PamException] es la que se definió en el párrafo 9.7.4. Simplemente se cambia su espacio de nombres (línea 1 a continuación):
namespace Pam.Dao.Entites
{
// clase de excepción
public class PamException : Exception
{
....
}
}
La interfaz [IPamDao] es la que acabamos de definir en el párrafo 9.23.1:
using Pam.EF5.Entites;
namespace Pam.Dao.Service
{
public interface IPamDao
{
// lista de todas las identidades de los empleados
Employe[] GetAllIdentitesEmployes();
// un empleado específico con sus prestaciones
Employe GetEmploye(string ss);
// lista de todas las cotizaciones
Cotisations GetCotisations();
}
}
La clase [PamDaoEF5] implementa esta interfaz mediante ORM y EF5. Su código es el siguiente:
using Pam.Dao.Entites;
using Pam.EF5.Entites;
using Pam.Models;
using System;
using System.Linq;
namespace Pam.Dao.Service
{
public class PamDaoEF5 : IPamDao
{
// campos privados
private Cotisations cotisations;
private Employe[] employes;
// Fabricante
public PamDaoEF5()
{
// cuota
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);
}
}
}
}
Nota:
- línea 10: la clase [PamDaoEF5] implementa la interfaz [IPamDao];
- las tablas [cotisations] y [employes] se almacenan en caché en las propiedades de las líneas 13-14. Los empleados no cuentan con sus prestaciones;
- líneas 17-28: es el constructor el que inicializa las líneas 13-14;
- líneas 43-52: el método [GetEmploye] devuelve un empleado con sus prestaciones. Recibe como parámetro el número de seguro social de dicho empleado. Si el empleado no existe en la base de datos, el método devolverá el puntero como nulo.
Tarea: completar el código de la clase [PamDaoEF5].
Para el constructor, nos basaremos en el código de prueba de la capa [EF5] presentado en el párrafo 9.22.6. Para el método [GetEmploye], se tomará como referencia el ejemplo del párrafo 3.5.7, [Eager and Lazy loading] de [refEF5].
9.23.5. Configuración de la capa [DAO]
Al igual que se hizo en el párrafo 9.22.5, debemos configurar EF5 en el archivo [App.config] del proyecto:
![]() |
Tarea 1: configura EF5 en [App.config]. Solo tienes que repetir lo que se hizo en el archivo [App.config] de la capa [EF5].
Nuestro programa de prueba utilizará [Spring.net] para obtener una referencia en la capa [DAO].
Tarea 2: con la ayuda de lo que se hizo en el párrafo 9.20.2, modifica el archivo de configuración [app.config] del proyecto [pam-dao] para que defina un objeto Spring llamado [pamdao] asociado a la clase [PamDaoEF5] que acabamos de crear. Los archivos [app.config] y [web.config] tienen la misma estructura. Hay que asegurarse de que la etiqueta <configSections> sea la primera etiqueta que aparezca después de la etiqueta raíz <configuration>.
9.23.6. Prueba de la capa [DAO]
Estamos listos para probar nuestra capa [DAO]. Lo hacemos con ayuda del programa [Program.cs] que ya está presente:
![]() |
Vamos a probar las diferentes funcionalidades de la interfaz de la capa [DAO]. El código de [Program.cs] será el siguiente:
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
{
// instanciación de la capa [dao]
IPamDao pamDao = (IPamDao)ContextRegistry.GetContext().GetObject("pamdao");
// lista de identidades de los empleados
foreach (Employe Employe in pamDao.GetAllIdentitesEmployes())
{
Console.WriteLine(Employe.ToString());
}
// un empleado con sus prestaciones
Console.WriteLine("------------------------------------");
Employe e = pamDao.GetEmploye("254104940426058");
Console.WriteLine("employé= {0}, indemnités={1}", e, e.Indemnites);
Console.WriteLine("------------------------------------");
// un empleado que no existe
Employe employe = pamDao.GetEmploye("xx");
Console.WriteLine("Employé n° xx");
Console.WriteLine((employe == null ? "null" : employe.ToString()));
Console.WriteLine("------------------------------------");
// lista de cotizaciones
Cotisations cotisations = pamDao.GetCotisations();
Console.WriteLine(cotisations.ToString());
}
catch (Exception ex)
{
// visualización de excepción
Console.WriteLine(ex.ToString());
}
//pausa
Console.ReadLine();
}
}
}
- línea 15: se obtiene una referencia a la capa [DAO] gracias a [Spring.net].
Los resultados de la ejecución de este programa son los siguientes:
9.23.7. DLL de la capa [DAO]
Tarea: cambie el tipo del proyecto [pam-dao] a biblioteca de clases y luego vuelva a generar el proyecto (repita lo que se hizo en el párrafo 9.22.7).
9.24. Paso 17: configuración de la capa [métier]
9.24.1. La interfaz de la capa [métier]
![]() |
La interfaz de la capa [métier] será la interfaz [IPamMetier] de la capa [métier] simulada que construimos en el párrafo 9.7.2.
public interface IPamMetier {
// lista de todas las identidades de los empleados
Employe[] GetAllIdentitesEmployes();
// ------- el cálculo del salario
FeuilleSalaire GetSalaire(string ss, double heuresTravaillées, int joursTravaillés);
}
9.24.2. El proyecto de Visual Studio
Tarea: agrega a la solución [pam-td] un nuevo proyecto de tipo [console] llamado [pam-metier]. Establece este proyecto como el proyecto de inicio de la solución.
![]() |
9.24.3. Agregar las referencias necesarias al proyecto
Veamos el proyecto en su conjunto:
![]() |
El proyecto [pam-metier] necesita una cierta cantidad de DLL:
- todos los que están referenciados por los proyectos [pam-dao] y [pam-ef5];
- las de los proyectos [pam-dao] y [pam-ef5] mismos.
Tarea: agrega estas diferentes referencias al proyecto [pam-metier].
![]() |
9.24.4. Implementación de la capa [métier]
![]() |
En lo anterior, se encuentran cuatro elementos que ya se utilizaron en la capa simulada [métier] (véase el párrafo 9.7). Puede haber cambios en los espacios de nombres importados por estas diferentes clases. Adáptalos. La clase [PamMetier] implementa la interfaz [IPamMetier] de la siguiente manera:
using Pam.Dao.Service;
using Pam.EF5.Entites;
using Pam.Metier.Entites;
using System;
namespace Pam.Metier.Service
{
public class PamMetier : IPamMetier
{
// referencia a la capa [DAO] inicializada por Spring
public IPamDao PamDao { get; set; }
// lista de todas las identidades de los empleados
public Employe[] GetAllIdentitesEmployes()
{
...
}
// un empleado específico con sus prestaciones
public Employe GetEmploye(string ss)
{
...
}
// las cotizaciones
public Cotisations GetCotisations()
{
...
}
//: cálculo del salario
public FeuilleSalaire GetSalaire(string ss, double heuresTravaillées, int joursTravaillés)
{
// SS: número de empleado SS
// HeuresTravaillées: número de horas trabajadas
// Días trabajados: número de días trabajados
...
}
}
- línea 13: hay una referencia a la capa [DAO]. Spring la inicializará al instanciar la clase [PamMetier]. Por lo tanto, cuando se ejecutan los distintos métodos, la línea 13 ya ha sido inicializada.
Tarea: completar el código de la clase [PamMetier]. Si en [GetSalaire] se detecta que el empleado con número de seguro social no existe, se ejecutará una [PamException]. El método de cálculo del salario se explica en el párrafo 9.5. Se debe tener cuidado de redondear todos los cálculos intermedios a dos decimales.
9.24.5. Configuración de la capa [métier]
Al igual que se hizo en el párrafo 9.22.5, debemos configurar EF5 en el archivo [app.config] del proyecto:
![]() |
Tarea 1: configura EF5 en [app.config]. Solo tienes que repetir lo que se hizo en el archivo [app.config] de la capa [EF5].
Nuestro programa de prueba utilizará [Spring.net] para obtener una referencia en la capa [métier].
Tarea 2: con la ayuda de lo que hiciste anteriormente en el párrafo 9.23.5, modifica el archivo de configuración [app.config] del proyecto [pam-metier] para que defina un objeto Spring llamado [pammetier] asociado a la clase [PamMetier] que acabamos de crear. Lo más sencillo es copiar el archivo [app.config] del proyecto [pam-dao] y agregar lo que falte.
Aquí surge una dificultad. No solo hay que instanciar la capa [métier] con la clase [PamMetier], sino que también hay que inicializar su propiedad [PamDao]:
// referencia a la capa [DAO] inicializada por Spring
public IPamDao PamDao { get; set; }
La configuración de Spring en [app.config] es entonces la siguiente:
<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>
- línea 6: define el objeto [pamdao] asociado a la clase [PamDaoEF5];
- línea 7: define el objeto [pammetier] asociado a la clase [PamMetier];
- línea 8: la etiqueta [property] sirve para inicializar una propiedad pública de la clase [PamMetier]. El atributo [name="PamDao"] corresponde al nombre de la propiedad que se va a inicializar en la clase [PamMetier]. El atributo [ref="pamdao"] indica que la propiedad se inicializa con una referencia, la del objeto [pamdao] de la línea 6, es decir, con la referencia de la capa [DAO]. Eso es lo que queríamos.
9.24.6. Prueba de la capa [métier]
Estamos listos para probar nuestra capa [métier]. Lo hacemos con el programa [Program.cs] que ya está presente:
![]() |
Vamos a probar las diferentes funcionalidades de la interfaz de la capa [métier]. El código de [Program.cs] será el siguiente:
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
{
// instanciación de la capa [métier]
IPamMetier pamMetier = ContextRegistry.GetContext().GetObject("pammetier") as IPamMetier;
// lista de identidades de los empleados
Console.WriteLine("Employés -----------------------------");
foreach (Employe Employe in pamMetier.GetAllIdentitesEmployes())
{
Console.WriteLine(Employe);
}
// cálculos de las nóminas
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));
}
// pausa
Console.ReadLine();
}
}
}
- línea 16: se obtiene una referencia a la capa [métier] gracias a [Spring.net].
Los resultados de la ejecución de este programa son los siguientes:
9.24.7. DLL de la capa [métier]
Tarea: transforma el tipo del proyecto [pam-metier] en una biblioteca de clases y luego regenera el proyecto (repite lo que se hizo en el párrafo 9.22.7).
9.25. Paso 18: implementación de la capa [web]
Llegamos a la última capa de nuestra arquitectura, la capa [web]:
![]() |
Vamos a reutilizar la capa [web] que habíamos desarrollado con la ayuda de una capa [métier] simulada.
9.25.1. El proyecto de Visual Studio
Regresamos a Visual Studio Express 2012 para la web con el fin de conectar nuestra capa web a las capas [métier, DAO, EF5] que acabamos de desarrollar. Principalmente hay que realizar algunas configuraciones y algunos cambios en los espacios de nombres.
Con Visual Studio Express 2012 para la web, carga la solución [pam-td]:
![]() |
- en [1], y la solución [pam-td] en VS Studio para la web. El proyecto web [pam-web-01] vuelve a estar visible. Lo habíamos perdido en VS Studio para escritorio.
- Habrá que modificar la configuración del proyecto web [pam-web-01]. En lugar de modificar un proyecto que funciona, realizaremos los cambios en una copia de ese proyecto. En primer lugar, en [2], eliminamos el proyecto de la solución (esto no elimina nada del sistema de archivos).
![]() |
- en [3], con el Explorador de Windows, duplicamos la carpeta [pam-web-01] en [pam-web-02];
- en [4], se carga el proyecto [pam-web-02] en la solución [pam-td]. Aparece con el nombre [pam-web-01];
- En [5], cambia este nombre a [pam-web-02] y establece este proyecto como el proyecto de inicio;
![]() |
- en [6], carga el proyecto anterior [pam-web-01]. Ahora ya tienes todos tus proyectos. Asegúrate de trabajar con [pam-web-02].
9.25.2. Agregar las referencias necesarias al proyecto
Veamos el proyecto en su conjunto:
![]() |
El proyecto [pam-web-02] necesita una cierta cantidad de DLL:
- todos los que hacen referencia los proyectos [pam-metier], [pam-dao] y [pam-ef5];
- las de los proyectos [pam-metier], [pam-dao] y [pam-ef5] mismos.
Tarea: agrega estas diferentes referencias al proyecto [pam-web-02]. Se debe eliminar la referencia del proyecto [pam-metier-simule]. Cambiamos a la capa [métier]. Algunas referencias DLL ya están presentes en las referencias. Elimínalas y luego realiza tus adiciones.
![]() |
9.25.3. Implementación de la capa [web]
Genere el proyecto [pam-web-02]. Aparecerán errores como el siguiente:
![]() |
La clase [ApplicationModel] utiliza el tipo [Employe]. Con la capa [métier] simulada, este tipo estaba definido en el espacio de nombres [Pam.Metier.Entites]. Ahora se encuentra en el espacio de nombres [Pam.EF5.Entites]. Corrija estos errores como se muestra arriba.
9.25.4. Configuración de la capa [web]
Tal como se hizo en el párrafo 9.24.5, debemos configurar EF5 en el archivo [web.config] del proyecto:
![]() |
Tarea 1: reemplace todo el contenido actual de [web.config] por el del archivo [app.config] del proyecto [pam-metier].
El archivo [Global.asax] de nuestra aplicación web utiliza [Spring.net] para obtener una referencia en la capa [métier]:
try
{
// instanciación de la capa [métier]
application.PamMetier = ContextRegistry.GetContext().GetObject("pammetier") as IPamMetier;
}
catch (Exception ex)
{
application.InitException = ex;
}
En la línea 4, se solicita una referencia al objeto Spring denominado [pammetier]. Este es, efectivamente, el nombre que se le ha asignado a la capa [métier] (verifíquelo en su archivo [web.config]).
9.25.5. Prueba de la capa [web]
Estamos listos para probar nuestra capa [web]. Primero cambiaremos su puerto de trabajo. Por defecto, [pam-web-02] tiene la configuración de [pam-web-01] y, por lo tanto, opera en el mismo puerto. La experiencia demuestra que esto genera un problema: IIS sigue utilizando los códigos del proyecto [pam-web-01]. Proceda de la siguiente manera:
![]() |
![]() |
En [4], cambie el número de puerto, por ejemplo, modificando la cifra de las unidades.
Ejecutamos el proyecto [pam-web-02] mediante [Ctrl-F5]. A continuación, obtenemos la siguiente página de inicio:
![]() |
En [1], se obtienen los empleados de la base de datos [dbpam_ef5]. Cabe señalar que ya no aparece el empleado [X X] que teníamos con la capa simulada [métier]. Hagamos una simulación:
![]() |
En [2], se obtiene efectivamente el salario real y ya no un salario ficticio. Ahora detengamos SGBD y MySQL5 y hagamos otra simulación:
![]() |
En [3], se obtuvo una página de errores legible, aunque algunos mensajes estén en inglés. Ahora detengamos nuevamente MySQL y volvamos a ejecutar la aplicación en VS a través de [Ctrl-F5]:
![]() |
Se obtiene la vista [initFailed.cshtml] creada en el párrafo 9.20.4. Muestra los mensajes de error de la pila de excepciones. Se invita al lector a realizar otras pruebas.
9.26. Paso 19: hacer accesible en Internet una aplicación ASP.NET
Al desarrollar una aplicación ASP.NET con Visual Studio, la configuración predeterminada hace que la aplicación desarrollada solo sea accesible en la dirección [localhost]. Cualquier otra dirección es rechazada por el servidor integrado de Visual Studio, el cual devuelve el error [400 Bad Request].
Esto se puede observar de la siguiente manera:
- en una ventana DOS, anote la dirección IP de su máquina de desarrollo:
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. . . :
La dirección IP aparece aquí en la línea 14. Si tienes una conexión wifi, la dirección wifi de la máquina aparecerá en las líneas 20 y siguientes.
- Verifica las propiedades del proyecto [clic droit sur projet / propriétés / onglet web]:
![]() |
La aplicación se ejecutará en el puerto [65010] de la máquina [localhost].
- Ejecute su proyecto mediante [Ctrl-F5]

- Reemplace [localhost] por la dirección IP de la máquina:

El servidor devolvió una respuesta [400 Bad Request]. El servidor IIS Express utilizado por Visual Studio solo acepta el nombre [localhost].
Para que la aplicación desarrollada sea accesible desde un servidor del tipo [http://adresseIP/contexte/...], es necesario utilizar un servidor distinto al IIS Express; por ejemplo, un servidor IIS (no Express). Para verificar si este está presente (normalmente en las versiones Pro de Windows), hay que ir al Panel de configuración [Panneau de configuration\Système et sécurité\Outils d’administration]:

Esta opción no siempre está presente. En ese caso, hay que ir a [ Panneau de configuration \ Programmes] e instalar las Herramientas de administración web.
![]() |
Una vez que la opción [Gestionnaire des services internet (IIS)] esté disponible, se activa:
![]() |
Se inicia el sitio web predeterminado. Para ello, es necesario que el servicio [Service de publication World Wide Web] ya esté en ejecución:
![]() |
Una vez hecho esto, acceda a URL [http://localhost] con un navegador. Verifique antes de que otro servidor web ya no esté ocupando el puerto 80. Si es así, deténgalo.
![]() |
El servidor IIS nos respondió. Ahora reemplaza [localhost] por la dirección IP de tu equipo:
![]() |
Funciona. Ahora regresemos a Visual Studio:
- en primer lugar, hay que iniciar Visual Studio en modo [administrateur]
![]() |
Una vez hecho esto, hay que cambiar la configuración del proyecto web que queremos implementar [clic droit sur projet / propriétés / onglet web]:
![]() |
Hay que seleccionar el servidor local IIS como servidor de implementación. Visual Studio establece la ruta de la aplicación URL. Se puede modificar. Ejecute el proyecto mediante [Ctrl-F5]:
![]() |
Ahora reemplaza [localhost] por la dirección IP de tu equipo:
![]() |
Si no se cuenta con el servidor IIS, se puede utilizar un servidor ASP.NET gratuito, como el [Ultidev Web Server Pro], disponible en URL [http://ultidev.com/Download/ ]. Una vez instalado, hay dos formas de ejecutar una aplicación web con este servidor:
La forma rápida
Abre un explorador de Windows y selecciona la carpeta de la aplicación ASP.NET que deseas implementar:
![]() |
A continuación, se inicia el servidor web y la aplicación web se muestra en un navegador:
![]() |
- En [3], se puede detener o iniciar el servidor web;
- en [4], se puede cambiar el puerto de servicio de la aplicación web;
Antes de iniciar el servidor, es necesario que el servicio [UWS HiPriv Services] que se muestra a continuación esté en ejecución:
![]() |
Una vez iniciado el servidor, la interfaz se presenta de la siguiente manera:
![]() |
Al hacer clic en el enlace [6], se muestra la primera página de la aplicación:
![]() |
Entonces se puede sustituir [localhost] por la dirección IP de la máquina:
![]() |
Así que, en este caso también, solo se acepta el nombre [localhost].
El método extenso
Inicie la aplicación Ultidev Web Explorer
![]() |
y luego sigue estos pasos:
![]() |
![]() |
![]() |
- en [8], seleccione la carpeta de la aplicación web que desea implementar;
![]() |
- debido a [10-11], la aplicación web deberá solicitarse con URL y [http://localhost:81/];
![]() |
![]() |
- inicie el servidor web con [14];
![]() |
- solicita el URL [19] ;
![]() |
- en [20], se obtuvo la página deseada utilizando la dirección local IP de la máquina en lugar del nombre [localhost]. Esto es lo que buscábamos;
El servidor Ultidev se instaló como un servicio de Windows que se inicia automáticamente. Puedes desactivar el inicio automático del servidor Ultidev de la siguiente manera:
- selecciona la opción [Panneau de configuration\Système et sécurité\Outils d’administration];
![]() |
- [1, 2]: selecciona las propiedades del servicio [Ultidev Web Server Pro];
- [3]: configúrelo para que se inicie manualmente.
Para iniciar el servidor manualmente, utilice, por ejemplo, la aplicación [Ultidev Web Explorer]:
![]() |
9.27. Paso 20: generación de una aplicación nativa para Android
Cuando se tiene una aplicación web del tipo APU (aplicación de página única), es posible generar un ejecutable para dispositivos móviles (Android, IoS, Windows 8, ...) con la herramienta [Phonegap] [http://phonegap.com/]. Existen otras formas de hacerlo, en particular con el producto de código abierto Apache Cordova [https://cordova.apache.org/]. La herramienta disponible en línea en el sitio web de PhoneGap [http://build.phonegap.com/apps] «sube» el archivo zip del sitio que se va a convertir. La página de inicio debe llamarse [index.html] y debe ser una página estática, es decir, no debe estar generada por un marco de trabajo web (ASP.NET, JEE, PHP, ...). Comenzaremos por crear esta página.
9.27.1. La arquitectura de la aplicación
Hay que recordar aquí que queremos crear una aplicación para Android. Este tipo de aplicación suele tener la siguiente arquitectura:
![]() |
- en [1], el usuario utiliza una tableta Android que se comunica con uno o varios servicios web [2];
Volvamos al modelo APU:
![]() |
- se carga una página inicial en el navegador (el diagrama anterior no indica de dónde proviene);
- las siguientes vistas se obtienen mediante llamadas Ajax [1-4]. El navegador no cargará ninguna página nueva;
La vista inicial puede ser proporcionada o no por el mismo servidor que las otras vistas obtenidas mediante llamadas Ajax. Si no es proporcionada por el mismo servidor, el JavaScript de la página inicial debe conocer el URL del servidor web que entregará las otras vistas. Este será el caso en la aplicación para Android que vamos a desarrollar:
![]() |
- la página estática [index.html] se encapsulará en una aplicación nativa de Android [1] que cuenta con las capacidades de un navegador, por lo que es capaz de ejecutar el JavaScript integrado en la página [index.html];
- esta página obtendrá las demás vistas mediante llamadas Ajax al servidor [2]. Para ello, necesita conocer el URL del servidor web;
Vamos a refactorizar la aplicación [pam-web-02] para que funcione de esta manera. Así, la primera página será la siguiente:
![]() |
- en [1], el URL de la página inicial de la aplicación. Nos lo proporcionará el servidor Ultidev analizado en el párrafo 9.26;
- en [2], el usuario deberá ingresar el URL del simulador de nómina. Se podría introducir de forma estática en el código JavaScript de la página inicial, pero eso complicaría las pruebas: tan pronto como se cambiara la dirección del simulador IP (o el puerto), habría que cambiarla también en el código JavaScript;
- por [3], el enlace [Connexion] que mostrará la siguiente vista:
![]() |
- Cabe señalar que, en [4], el URL del navegador no ha cambiado. Sigue siendo el de la página inicial y permanecerá así durante toda la vida útil de la aplicación.
Una vez obtenida esta vista, todo funciona como antes: las diferentes vistas se obtienen mediante llamadas Ajax. Veremos que hay que modificar muy poco código.
9.27.2. Reestructuración del proyecto [pam-web-02]
Dentro de la carpeta [Content] del proyecto [pam-web-02], creamos la siguiente carpeta [bootstrap] (el nombre no importa):
![]() |
En ella hemos incluido la página estática [index.html] y todos los recursos que necesita (archivos CSS y JS). La página [index.html] toma el código de la página maestra [_Layout.cshtml] del proyecto de Visual Studio y elimina todo lo que no sea estático. Esto da como resultado el siguiente código:
<!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>
Hemos agregado los siguientes elementos:
- líneas 27-29: se ha agregado la opción de menú [Connexion] para permitir la conexión al servicio de simulación;
- líneas 55-56: la entrada del URL del simulador;
- líneas 59-63: un mensaje de error si falla la conexión;
La refactorización del código se realiza únicamente en el código [myScripts.js] de la línea 14 anterior. No cambia nada más. El código evoluciona de la siguiente manera:
// al cargar el documento
$(document).ready(function () {
// se recuperan las referencias de los distintos componentes de la página
loading = $("#loading");
content = $("#content");
erreur = $("#erreur");
erreur1 = $("#erreur1");
erreur2 = $("#erreur2");
// los enlaces del menú
lnkConnexion = $("#lnkConnexion");
lnkFaireSimulation = $("#lnkFaireSimulation");
lnkEffacerSimulation = $("#lnkEffacerSimulation");
lnkEnregistrerSimulation = $("#lnkEnregistrerSimulation");
lnkVoirSimulations = $("#lnkVoirSimulations");
lnkTerminerSession = $("#lnkTerminerSession");
lnkRetourFormulaire = $("#lnkRetourFormulaire");
// se colocan en una tabla
options = [lnkConnexion, lnkFaireSimulation, lnkEffacerSimulation, lnkEnregistrerSimulation, lnkVoirSimulations, lnkTerminerSession, lnkRetourFormulaire];
// se ocultan ciertos elementos de la página
loading.hide();
erreur.hide();
// se fija el menú
setMenu([lnkConnexion]);
});
- líneas 6-8: los identificadores del área que muestra los errores de conexión en la página [index.html];
- línea 10: el nuevo enlace para conectarse al simulador;
- línea 21: el área de errores está oculta inicialmente;
- línea 23: solo se muestra el enlace de conexión;
En la página [index.html], el enlace de conexión se define de la siguiente manera:
<a id="lnkConnexion" href="javascript:connexion()">
| Connexion<br />
</a>
La función JS [connexion] (línea 1) es la siguiente:
var urlServiceWeb;
var erreur, erreur1, erreur2;
function connexion() {
// se recupera el urlServiceWeb del servicio web
urlServiceWeb = $("#urlServiceWeb").val();
// se recupera el formulario de entrada
$.ajax({
url: urlServiceWeb + '/Pam/Formulaire',
type: 'POST',
dataType: 'html',
beforeSend: function () {
// se enciende la señal de espera
loading.show();
},
success: function (data) {
// visualización de resultados
content.html(data);
// menú
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 () {
// se apaga la señal de espera
loading.hide();
}
});
}
- línea 7: se recupera el valor de URL ingresado por el usuario. Se asigna a la variable global de la línea 1. De esta manera, estará disponible en las demás funciones del archivo;
- línea 10: se realiza una llamada Ajax al URL [/Pam/Formulaire] del simulador. Este URL muestra la vista parcial de la entrada de información de la simulación (empleados, horas trabajadas, días trabajados). En la versión inicial de [pam-web-02], este URL era suficiente. Se le añadía automáticamente el prefijo del URL, que había cargado la página inicial. Ahora, se parte de la hipótesis de que la página inicial puede ser proporcionada por un servidor distinto al que aloja el simulador. Entonces hay que anteponer a URL y [/Pam/Formulaire] la variable [urlServiceWeb] de la línea 1, que es el URL del simulador (por ejemplo, http://172.19.81.34/pam-web-02). Esto deberá hacerse para todas las llamadas Ajax del archivo;
- líneas 17-22: si la conexión se realiza con éxito, se muestra la vista parcial [Formulaire.cshtml] y se muestra un menú con el único enlace [Faire la simulation] (línea 21);
- líneas 23-27: si la conexión falla:
- en la línea 24, se muestra la respuesta HTML enviada por el servidor web (si la hay);
- en la línea 25, se muestran los encabezados HTTP enviados por el servidor web (si ha respondido);
Eso es todo. Si la conexión se realiza con éxito, se obtiene la siguiente página:
![]() |
Ahora nos encontramos en la situación anterior, en la que las vistas se obtienen mediante llamadas Ajax. Así, como se muestra arriba, al hacer clic en el enlace [Faire la simulation] se ejecutará el siguiente código del archivo [myScripts.js]:
function faireSimulation() {
// se recuperan las referencias
var simulation = $("#simulation");
var formulaire = $("#formulaire");
// ¿Formulario válido?
var formValid = formulaire.validate().form();
if (!formValid) return;
// se realiza una llamada Ajax manualmente
$.ajax({
url: urlServiceWeb + '/Pam/FaireSimulation',
type: 'POST',
data: formulaire.serialize(),
dataType: 'html',
...
});
// menú
setMenu([lnkEffacerSimulation, lnkEnregistrerSimulation, lnkTerminerSession, lnkVoirSimulations]);
}
- Se ha realizado una única modificación, en la línea 10, donde el código anterior URL ahora lleva como prefijo el del simulador;
9.27.3. Prueba del proyecto refactorizado
En el párrafo 9.26, mostramos cómo instalar la aplicación [pam-web-02] en el servidor Ultidev. Partiremos de ahí:
![]() |
- en [6], solicitamos que se muestre la página [bootstrap/index.html]. Obtenemos la siguiente vista:
![]() |
Intentemos una URL errónea:
![]() |
- en [10], los encabezados HTTP de la respuesta del servidor;
- en [11], el documento HTML de la respuesta del servidor;
Si se ingresa el URL correcto:
![]() |
se obtiene la siguiente respuesta:
![]() |
9.27.4. Creación del binario de Android
Vamos a crear el binario de Android a partir del sitio estático que acabamos de crear y probar [1]:
![]() | ![]() |
Agregamos en [2] un archivo [config.xml] que servirá para configurar el complemento [Phonegap], el cual generará el binario de Android. Su código es el siguiente:
<?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>
- líneas 7-9: ingresa aquí tus datos de contacto;
- líneas 11-13: estas líneas permiten que el JavaScript integrado en la aplicación web, que se ejecutará dentro del dispositivo Android, solicite archivos URL externos a dicho dispositivo;
Comprimimos el contenido de la carpeta [Content/bootstrap]:
![]() |
Luego vamos al sitio web de PhoneGap [http://build.phonegap.com/apps]:
![]() |
- Antes de [1], es posible que tengas que crear una cuenta;
- en [1], comenzamos;
- en [2], eliges un plan gratuito que solo permite una aplicación PhoneGap;
- en [3], descargamos la aplicación comprimida [4];
![]() |
![]() |
- en [5], el nombre de la aplicación;
- haga clic en el enlace [6] para compilar los binarios de OS y IoS, para Android y Windows. Esto puede tardar unos segundos;
![]() |
- en [7-9], descarga el binario para Android;
![]() |
Inicie un emulador [GenyMotion] para una tableta Android (consulte el párrafo 11.1):
![]() |
En la imagen anterior, se inicia un emulador de tableta con la versión 21 de Android (API). Una vez iniciado el emulador,
- desbloquéalo deslizando el pestillo (si lo hay) hacia un lado y soltándolo;
- con el ratón, arrastre el archivo [Pam-debug.apk] que descargó y suéltelo en el emulador. Se instalará y ejecutará de inmediato;
![]() |
Configure [1] con el URL del simulador tal como se describió en el párrafo 9.27.3. Una vez hecho esto, conéctese al simulador mediante el enlace [2]:
![]() |
Pruebe la aplicación en el emulador. Debería funcionar.








































































































































































































