9. Caso práctico
9.1. Introduction
Vamos a presentar un caso práctico ya publicado en un artículo disponible en el URL [http://tahe.developpez.com/dotnet/pam-aspnet/]. En este artículo, el caso práctico se lleva a cabo con ASP.NET clásico y el ORM NHibernate. Aquí lo llevaremos a cabo con ASP.NET, MVC y ORM de Entity Framework. Al igual que en el artículo existente, el caso práctico se presenta como un TD universitario. Por lo tanto, está dirigido a estudiantes. Para todas las cuestiones, se incluyen referencias a los capítulos que acabamos de detallar con el fin de 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í mismo.
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 han detenido 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 ORM. El SGBD que se utilizará será MySQL.
Construiremos esta aplicación primero con una capa [métier] simulada:
![]() |
Esto nos permitirá centrarnos únicamente en la capa [web]. La capa simulada [métier] respetará 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 elaborar 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. Consta de 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 sobre 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:
![]()
Los tipos 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, que ha trabajado 150 horas repartidas en 20 días durante el mes a pagar.
Se tienen 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 cuidadora infantil 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 deben deducirse una serie de cotizaciones sociales: | Contribución social general y contribución al reembolso de la deuda social: [SALAIREBASE]*[CSGRDS/100] Contribución social general deducible: [SALAIREBASE]*[CSGD/100] Seguridad Social, viudedad y 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 cotizaciones sociales: | [COTISATIONSSOCIALES] = [SALAIREBASE] *(CSGRDS + CSGD + SECU + RETRAITE) / 100 | [COTISATIONSSOCIALES]=97,48 |
Por otra parte, la cuidadora tiene derecho, por cada día trabajado, a una indemnización por gastos de manutención y a una indemnización por comida. En este concepto, percibe las siguientes indemnizaciones: | [Indemnités]=[TOTALJOURS]*(ENTRETIENJOUR+REPASJOUR) | [INDEMNITES]=104 |
En definitiva, el salario neto que se debe pagar a la cuidadora infantil 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 guardan los recursos estáticos del proyecto:
- [indicator.gif]: la imagen animada que muestra la espera hasta que finaliza 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 auxiliar con métodos estáticos;
- en [5], los modelos de la aplicación, ya sean modelos 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 salarial,
- [IndexModel]: modelo de la primera vista [Index] mostrada por 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 introducir 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 práctico. Cuando sea necesario, indicaremos el número del capítulo que conviene volver a leer para realizar el trabajo solicitado. Algunos elementos del proyecto se proporcionan en una carpeta [aspnetmvc-support.zip] que se encuentra en la página 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. Basta, por tanto, con recuperarlos mediante copiar y pegar entre este PDF y Visual Studio.
9.7.1. La solución de Visual Studio de la aplicación completa
En primer lugar, vamos a crear 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 crear 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], asignar un nombre a la solución;
- en [4], crear 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 mostrará al usuario:
![]() |
- Al mostrar el formulario por primera vez, debe aparecer en [1] la lista de empleados. Basta con una lista simplificada (Apellidos, Nombre, SS). El n.º SS es necesario para acceder a la información adicional sobre el empleado seleccionado (datos del 6 al 11).
- Los datos del 12 al 15 corresponden a los distintos tipos 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 introducidos por el usuario en los campos 1 a 3.
La interfaz [IPamMetier] que la capa [métier] ofrece a la capa [web] debe cumplir los requisitos anteriores. 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á rellenar el cuadro combinado [1]
- línea 10: el método que permitirá obtener la información de las posiciones 6 a 24. Esta información se ha reunido en un objeto de tipo [FeuilleSalaire] que describiremos en breve.
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] gestionará 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 apartado 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 línea 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, útiles cuando se utilicen 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 las posiciones 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: los datos del 6 al 11 sobre el empleado cuyo salario se calcula y los datos del 16 al 19 sobre sus complementos. No hay que olvidar aquí que un objeto [Employe] encapsula un objeto [Indemnites] que representa sus complementos;
- línea 8: los datos del 12 al 15;
- línea 9: los datos 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 y 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 añaden al salario base, en función del índice del empleado y del 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 notificar una excepción que se haya producido 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.º 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 cada empleado con su número de la Seguridad 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 comprueba 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 añade al diccionario;
- líneas 19-28: el segundo empleado;
- línea 29: se añade al diccionario;
- líneas 32-42: el tercer empleado. Este no se añade al diccionario por una razón que explicaremos más adelante.
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 figura en él;
- 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] anterior 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 nóminas
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], se ha cargado la solución [pam-td] 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], añadimos un nuevo proyecto a la solución [pam-td];
![]() |
- en [2], seleccionamos un proyecto ASP.NET MVC 4;
- denominado [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, 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, mediante el Explorador de Windows, sustituimos 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]. Para ello, procederemos de la siguiente manera:
![]() |
- en [1], se actualiza 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], añada los scripts JQuery Globalization y [1] 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 a los scripts necesarios para introducir 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 por defecto 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 deduce 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 la misma.
Crea el controlador [Pam]:
Modifica 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] solo procesará el comando HTTP GET;
- línea 8: se devuelve un tipo [ViewResult] en lugar de un tipo [ActionResult].
Crea ahora la vista [Index.cshtml] que muestra la acción [Index] anterior:
![]() |
Modifica [Index.cshtml] de la siguiente manera:
@{
ViewBag.Title = "Pam";
}
<h2>Formulaire</h2>
Ejecuta la aplicación mediante [Ctrl-F5]. Deberías obtener la siguiente página:
![]() |
Tarea: Explica qué ha ocurrido.
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 escribir una aplicación siguiendo la plantilla APU (Aplicación de página única) descrita en el apartado 7.5, así como en el apartado 7.6. La página única es la 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 inscribe 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 ID [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 apartado 7.6. Vuelve a leer ese apartado 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, dispones de una serie de herramientas para depurar el código JavaScript de tus páginas (HTML, CSS). Estas herramientas se han presentado parcialmente en el apartado 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 tengan en cuenta. A continuación te explicamos cómo hacerlo con Chrome:
- ejecuta [Ctrl-Maj-I] para mostrar el entorno de desarrollo
![]() |
- haz clic en el icono [1] situado en la parte inferior derecha de la ventana de desarrollo;
- y, a continuación, 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 introducción de datos 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 se muestra mediante 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>
Realiza estos cambios y comprueba que sigues viendo la siguiente pantalla 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 tenemos que escribir el código JavaScript que gestiona el clic en el enlace [Faire la simulation]. Seguiremos el procedimiento descrito en el apartado 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>
Vemos 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 introducidos 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 con ID [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 apartado 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], que a su vez colocará 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, escribe 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], que a su vez 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, escribe 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], que a su vez 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, escribe 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], que a su vez 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>
Ejercicio: siguiendo los pasos anteriores, 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], que a su vez 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 el que se encontraban al cargar inicialmente la aplicación (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 implica al servidor.
He aquí un ejemplo de ejecución:
![]() |
9.9.9. Gestión de la navegación entre pantallas
Por el momento, los enlaces siguen mostrándose. Ahora vamos a gestionar su visualización mediante 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 cargarse 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 algunos 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 iniciar 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 configurados, 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 dejarán de hacerlo, ya que vas a modificar 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 tal y como los habías configurado.
9.10. Paso 4: creación 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,
- el número de días trabajados;
- el formulario se envía mediante el enlace [Faire la simulation];
- se debe comprobar la validez de los campos de entrada [Heures travaillées] y [Jours travaillés];
- la lista de empleados procede de la capa [métier] que hemos creado 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 van a realizar 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 la plantilla de acción [3], que servirá de 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 dicha 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 ninguna plantilla 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 ahora esta referencia:
![]() |
- en [1], hacer clic con el botón derecho del ratón en [References] del proyecto [pam-web-01] y, a continuación, en [Ajouter une référence];
- en [2], selecciona la opción [Solution] y, a continuación, el proyecto [pam-metier-simule] en [3];
- en [4], el proyecto [pam-metier-simule] se ha añadido a las referencias del proyecto [pam-web-01].
9.10.2. El modelo de la aplicación
En el apartado 4.10, página 78, hemos introducido los conceptos importantes de modelo de aplicación y modelo de sesión. Ahora vamos a utilizarlos. 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 concreto. Este modelo constituye una memoria compartida por todas las consultas de dicho 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. Por otra parte, vamos a partir de la hipótesis de que la lista de empleados no cambia. Por lo tanto, puede leerse una sola vez y luego compartirse entre todos los usuarios. Proponemos, pues, 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:
<!-- el menú 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]. Tenemos que crear un array de este tipo con la lista de empleados. Dado que los empleados no cambian, este array también se puede incluir en el modelo de la aplicación. Modificamos este último 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 debe crearse esta plantilla? Lo hemos mostrado en el apartado 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 vamos a modificar 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 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 apartado 4.10. Tómate tu tiempo para releer este apartado, que es largo pero importante.
La línea 33 consta, en realidad, 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, más arriba, necesitará la clase [ApplicationModelBinder] descrita en el apartado 4.10, página 82:
![]() |
9.10.3. El código de la acción [Index]
Ahora que hemos definido una plantilla 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: la plantilla de la aplicación es ahora un parámetro de la acción [Index]. En el apartado 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 reuniera estos tres valores. También es habitual reutilizar la plantilla que se ha utilizado para mostrar el formulario. Eso es lo que vamos a hacer aquí. La clase [IndexModel] evoluciona de la siguiente manera:
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 contabilizados
[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 y 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 ha introducido el tipo [double] para facilitar la validación de este campo en el lado del cliente, ya que la validación de un tipo [int] había planteado problemas;
- líneas 12, 14 y 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], de nuevo sin pasar a ella desde ninguna plantilla. 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 podría tener ahora 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 una plantilla 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 introducidos;
- 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 apartado 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 comprobar que su lista desplegable se ha rellenado correctamente con la lista de empleados que habíamos definido en la capa simulada [métier].
9.11. Paso 5: configuración de la validación de los datos introducidos
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 se aplica 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, dado que en [IndexModel] se ha declarado el campo [JoursTravaillés] como de tipo [double]:
public double JoursTravaillés { get; set; }
se puede introducir un número real en este campo:
![]() |
Por otra parte, se pueden introducir valores arbitrarios en ambos campos:
![]() |
La plantilla [IndexModel] del formulario es actualmente la 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 contabilizados
[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:
- disponer de 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];
Podemos guiarnos por el ejemplo del apartado 7.6.2. Para comprobar que el número de días trabajados es un número entero, podemos utilizar una expresión regular (véanse los ejemplos del apartado 5.9.1).
A continuación se muestran algunos ejemplos de lo que se espera:
![]() |
![]() |
![]() |
9.11.2. Introducción 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 ha identificado y solucionado en el apartado 6.1.
Tarea: siguiendo el procedimiento del apartado mencionado anteriormente, realiza los cambios necesarios para que se puedan introducir 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 JavaScript [Faire la simulation]
Actualmente, se pueden enviar valores no válidos, como muestra la siguiente secuencia:
![]() |
![]() |
La presencia de la simulación en [1] y el cambio de menú en [2] muestran que al hacer clic en el enlace [Faire la simulation] se envió el formulario a pesar de que los valores introducidos eran inválidos. Este problema se ha identificado y solucionado en el apartado 7.6.5.
Tarea: siguiendo el procedimiento expuesto en el apartado mencionado anteriormente, asegúrese de que el POST del enlace [Faire la simulation] no se pueda ejecutar si los valores introducidos son inválidos. No olvide vaciar la caché del navegador antes de probar sus modificaciones.
Recordamos que la vista parcial [Formulaire.cshtml] genera un formulario HTML con el ID [formulaire] (línea 1 a continuación):
@using (Html.BeginForm("FaireSimulation", "Pam", FormMethod.Post, new { id = "formulaire" }))
{
...
}
Esto se puede comprobar mostrando 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] muestra ahora 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 apartado 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 nómina:
#simulación de la tabla tr {
height: 30px;
}
Tarea: completa la vista [Simulation.cshtml].
Para mostrar los euros de una cantidad de dinero, se utilizará el método [string.Format]:
La instrucción anterior muestra [somme] como valor monetario [C] (Currency) con dos decimales [C2].
Para probar esta vista, hay que proporcionarle una nómina. Esta debe ser facilitada 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]. Es necesario que le pase una nómina. Sabemos que es la capa [métier] la que realiza el cálculo de las nóminas. Se puede acceder a esta capa [métier] a través del modelo de la aplicación [ApplicationModel] que hemos definido en el apartado 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 y como hicimos con 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 ya existente. Se definió en la clase [métier] simulada en el apartado 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 nómina se pasa como modelo a la vista [Simulation.cshtml].
Ahora ya estamos listos para probar la vista [Simulation.cshtml]:
![]() |
No introducimos ningún dato y solicitamos la simulación. Obtenemos entonces el siguiente resultado:
![]() |
9.12.3. Cálculo del salario real
Nuestra acción actual [FaireSimulation] sigue calculando siempre la misma nómina:
// realizar una simulación
[HttpPost]
public PartialViewResult FaireSimulation(ApplicationModel application)
{
FeuilleSalaire feuilleSalaire = application.PamMetier.GetSalaire("254104940426058", 150, 20);
return PartialView("Simulation", feuilleSalaire);
}
No tiene en cuenta la información introducida:
- el empleado cuyo salario se calcula;
- su número de horas trabajadas;
- el número de días trabajados.
Los valores introducidos 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;
- A continuación, la función JS [faireSimulation] realiza 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 introducidos por el usuario;
- la acción del servidor [/Pam/FaireSimulation] recuperará los valores introducidos de entre los valores enviados por la función JS y [faireSimulation].
Empecemos por el punto 2: la función JS [faireSimulation] debe enviar los valores introducidos por el usuario a la acción de servidor [/Pam/FaireSimulation].
Tarea: completa la función JS [faireSimulation] para que envíe los valores introducidos por el usuario. Puedes ayudarte del ejemplo del apartado 7.6.5, donde se ha tratado este problema.
Pasemos ahora al 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 apartado 7.6.5, donde se ha tratado 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
...
// se intenta 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]. Obtenemos entonces el siguiente resultado:
![]() |
Efectivamente, hemos obtenido 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 tiene en cuenta.
9.12.4. Gestión de errores
Veamos el siguiente ejemplo:
![]() |
- en [1], se selecciona un empleado que no existe (véase la definición de la capa [métier] simulada en el apartado 9.7.5;
- en [2], se realiza la simulación;
- en [3], que se muestra a continuación, aparece una página de error.
![]() |
¿Qué ha pasado?
Se ha ejecutado la función JS [faireSimulation]. Su código tiene este aspecto:
function faireSimulation() {
...
// se realiza una llamada Ajax manualmente
$.ajax({
url: '/Pam/FaireSimulation',
...
beforeSend: function () {
// se enciende la señal 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 ha fallado y se ha ejecutado la función de las líneas 14-18. Se ha mostrado la página de error [jqXHR.responseText] devuelta por el servidor. Esta es bastante precisa. La capa simulada [métier] ha lanzado una excepción porque el número SS que se le ha proporcionado no corresponde a ningún empleado existente (véase el código de la capa simulada [métier] en el apartado 9.7.5). Tenemos que gestionar este caso de forma adecuada.
Vamos a crear una vista parcial [Erreurs.chtml] que se devolverá al cliente JS cada vez que se detecte un error en el 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 se realiza ahora en un try / catch;
- línea 27: si se ha producido 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 no vá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], realizamos la simulación;
- en [3], se recupera 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 introducidos 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 mediante la llamada Ajax. No verificamos la validez del modelo. Es necesario hacerlo, ya que no podemos saber de dónde proceden los valores enviados. Alguien podría haber manipulado un POST y enviarnos datos no válidos.
Tarea: siguiendo el modelo que hemos desarrollado 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 se prueba esta modificación? En el apartado 9.11.3, se aseguró de que la función JS [faireSimulation] no realizara el POST de los valores introducidos si estos no eran válidos. Comente las líneas que realizan esta acción y, a continuación, realice la siguiente prueba:
![]() |
- en [1], realizamos la simulación con valores no válidos;
- en [2], se muestra 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, no olvides descomentar las líneas que acabas de comentar en las funciones JS y [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 mediante el enlace [Faire la simulation], guardarlas mediante 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 apartado 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 hay que memorizar: 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 ser capaces de recuperar la simulación calculada en la solicitud anterior. Para ello, esta también se almacenará 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, también en la sesión.
En el apartado 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. Vamos a retomar este concepto. Le invitamos a releer el apartado en cuestión si este concepto le 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], de 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. Por otra parte, debemos guardar la información que ha dado lugar a esta nómina:
- 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 figura 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 de la plantilla de la acción
IndexModel modèle = new IndexModel() { Application = application };
// intentamos recuperar los valores registrados en la plantilla
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 apartado 4.10, haz lo necesario para que el parámetro [SessionModel session] de la acción sea inicializado correctamente por el framework. Si no se hace nada, se obtendrá 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 grabarla:
![]() |

La vista parcial [Simulations.cshtml] muestra ahora 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 siguiente 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 apartado 5.4.
Nota: el enlace [retirer] de cada simulación de la tabla HTML será un enlace JavaScript con el siguiente formato:
donde N es el número de la simulación.
Tarea 2: prueba tu aplicación realizando simulaciones. Para ello, 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 que debería reflejarse en la vista [Simulations.cshtml].
Tarea 3: mejora la vista parcial [Simulations.cshtml] de tal forma que los colores de las filas de la tabla HTML se alternen.

Se asignarán de forma 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: volver al formulario de introducción de datos
9.15.1. El problema
Una vez obtenida la lista de simulaciones, podemos volver al formulario de introducción de datos, algo que llevábamos un tiempo sin poder hacer:


9.15.2. Creación de la acción de servidor [Formulaire]
El enlace Ajax [Retour au formulaire de simulation] llama a la acción del 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 este motivo, el enlace [Retour au formulaire de simulation] ya no funcionaba.
Tarea: escribir la nueva versión de la acción de servidor [Formulaire] (hay que reescribir 2 líneas) y, a continuación, realizar las pruebas.
9.15.3. Modificación de la función JavaScript [retourFormulaire]
Con la modificación realizada anteriormente, ahora se puede volver al formulario, pero entonces aparece un error:
![]() |
- en [1], se vuelve al formulario de introducción de datos;
- en [2], se realiza una simulación con datos introducidos erróneamente. 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 apartado 9.12.4.
Esta anomalía se identificó y se solucionó en el apartado 7.6.7.
Tarea: siguiendo el procedimiento del apartado 7.6.7, corrige la función JavaScript [retourFormulaire] y, a continuación, realiza pruebas para comprobar que los validadores del lado del cliente vuelven 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 realizadas:
![]() | ![]() |
9.16.2. Codificación de la acción del servidor [VoirSimulations]
El enlace Ajax [Voir les simulations] llama a la acción del 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 una plantilla [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 este motivo, el enlace [Voir les simulations] ya no funcionaba.
Tarea: escribir la nueva versión de la acción de servidor [VoirSimulations] (hay que reescribir 2 líneas) y, a continuación, realizar las pruebas.
9.17. Paso 11: finalizar la sesión
9.17.1. El problema
Se puede finalizar la sesión del usuario en cualquier momento mediante el enlace [Ajax] [Terminer la session]. Esto tiene como efecto abandonar la sesión actual para iniciar una nueva. Además, se vuelve a la vista del formulario:
![]() |
![]() |
- en [1], se han realizado dos simulaciones y, a continuación, se cierra la sesión;
- en [2], se ha vuelto al formulario de introducción de datos. Queremos ver las simulaciones;
- en [3], debido al cambio de sesión, la lista de simulaciones está ahora 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 este motivo, el enlace [Terminer la session] ya no funcionaba.
Tarea: escribir la nueva versión de la acción de servidor [TerminerSession] (hay que reescribir 2 líneas) y, a continuación, realizar las pruebas.
Nota: para cerrar la sesión en la acción, se escribe:
9.17.3. Modificación de la función JavaScript [terminerSession]
Con la modificación realizada anteriormente, ahora se puede volver al formulario, pero entonces aparece una anomalía, la que se ha descrito anteriormente en el apartado 9.15.3.
Tarea: siguiendo el procedimiento que ha seguido en el apartado 9.15.3, corrija la función JavaScript [terminerSession] y, a continuación, realice pruebas para comprobar que los validadores del lado del cliente vuelven 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 mediante el enlace de JavaScript [Effacer la simulation]:
![]() | ![]() |
9.18.2. Creación de la acción de cliente [effacerSimulation]
La función JavaScript [effacerSimulation] tiene, por el momento, el siguiente código:
function effacerSimulation() {
// se borran los datos introducidos 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 apartado 7.6.6
9.19. Paso 13: eliminar una simulación
9.19.1. El problema
Cuando se accede a la página de simulaciones, se pueden eliminar algunas de ellas mediante el enlace de JavaScript [retirer]:


9.19.2. Registro de la acción de cliente [retirerSimulation]
Los enlaces [retirer] tienen el siguiente formato: HTML:
donde N es el número de la simulación.
Tarea: siguiendo los pasos descritos en los apartados 9.9.3, escribe 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 demás 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 mediante POST denominado [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;
- a continuación, debe mostrar la nueva lista de simulaciones.
Tarea: escribe la acción de servidor [RetirerSimulation]. Revisa el apartado 4.1 para saber cómo recuperar el parámetro enviado mediante POST denominado [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 tiene lugar 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] tiene este aspecto:
// 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 del ámbito de la aplicación
ApplicationModel application = new ApplicationModel();
Application["data"] = application;
// instanciación de la 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 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 especificado de forma estática el nombre de una clase que implementa [IPamMetier]. Por lo tanto, si la capa [métier] se implementara con una nueva clase que implementara [IPamMetier], habría que cambiar esta línea. No es muy importante, pero se puede evitar. La definición de la clase de implementación de la interfaz [IPamMetier] puede trasladarse a un archivo de configuración. Para cambiar de implementación, basta con modificar el contenido de dicho 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 frameworks .NET para hacer lo mismo, quizá mejor y de forma más sencilla.
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 a 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 gestionados 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] se limita a devolver 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 se presenta en forma de DLL, que hay que añadir a las referencias del proyecto. Se puede proceder de la siguiente manera:
![]() |
En [1], haz clic con el botón derecho del ratón en la rama [References] del proyecto y selecciona la opción [Gérer les packages NuGet]. Se necesita conexión a Internet. A continuación, se procederá igual 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 paquetes DLL: [Spring.core], [2] y [Common.Logging], [3]. En los ejemplos siguientes se ha utilizado la versión 1.3.2 de Spring.
Nota: si no dispone de conexión a Internet, encontrará estos archivos DLL en una carpeta [lib] del material de este caso práctico.
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], situada 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], situada en DLL y [Spring.Core], como la capaz de procesar la sección [context] de las líneas 11-13;
- líneas 11-13: esta sección aporta 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] situada en DLL [pam-metier-simule]. Aquí no hay que equivocarse. En cuanto al atributo [id], puede introducir lo que desee. 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 DLL y [pam-metier-simule], hay que consultar las propiedades del proyecto C# [pam-metier-simule]:
![]() |
Hay que 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. A continuación, el método estático [GetObject] permite obtener un objeto concreto identificado por su atributo id. Cabe señalar que, ahora, el nombre de la clase de implementación de la interfaz [IPamMetier] ya no está escrito de forma fija en el código. Ahora se encuentra en el archivo [web.config].
Una vez realizadas todas estas modificaciones, prueba tu aplicación. Debería funcionar.
9.20.4. Gestionar un error de inicialización de la aplicación
En el método [Application_Start] hemos escrito:
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 evidente es que nos hayamos equivocado en el nombre del objeto que se va a instanciar;
- otra es que la instanciación de la capa [métier] falle. Esto no puede ocurrir en el caso de nuestra capa simulada [métier], pero sí podría ocurrir en el caso de 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 gestionar 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 errores
if (application.InitException == null)
{
....
}
// enlazadores de modelos
...
}
- En la línea 12, introducimos una nueva propiedad denominada [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 puede producirse durante la inicialización de la aplicación;
- líneas 13-21 de [Application_Start]: la instanciación de la capa [métier] se realiza ahora en un try / catch;
- línea 20: se almacena la excepción;
- líneas 23-26: si no se ha producido ningún error, se ejecuta el código que teníamos anteriormente;
- línea 28: se crean los [ModelBinders] independientemente de si ha habido 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 este modo, puede saber si la inicialización se ha realizado correctamente o no y mostrar una página de errores si la inicialización ha fallado 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 errores
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 ha presentado y explicado en el apartado 9.12.4. La vista [InitFailed.cshtml] tendrá el siguiente aspecto:
![]() |
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-29;
- línea 3: esta vista no utiliza la página maestra [_Layout.cshtml]. De hecho, no se desea el menú que aporta este documento. Por lo tanto, se crea una página HTML completa (líneas 5-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 pretendía.
9.21. ¿En qué punto nos encontramos?
Ahora tenemos 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 sustituimos esta capa de negocio simulada por una capa de negocio real que respete dicha 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]. Vamos a seguir por este camino.
La nueva arquitectura será la siguiente:
![]() |
Vamos a describir sucesivamente:
- la capa [EF5] conectada a SGBD. Se implementará con Entity Framework 5 (EF5);
- la capa [DAO], que gestiona 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 presenta al principio de este documento, en el apartado 1.1, y que ahora recordamos:
![]() |
- la capa [Web] es la capa que está en contacto 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, como el cálculo de un salario o de una factura. Esta capa utiliza datos procedentes 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: el 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 familiarizarse con la escritura de esta capa, se recomendará leer el documento [Introduction à Entity Framework 5 Code First], disponible en URL [http://tahe.developpez.com/dotnet/ef5cf-02/]. Se trata de un documento bastante extenso. Los conceptos fundamentales se recogen en los cuatro primeros capítulos. Se indicarán los párrafos que conviene leer con especial atención. Cuando hagamos referencia a este documento, utilizaremos la notación [refEF5].
Por otra parte, en ocasiones necesitaremos conceptos de C#. En esos casos, 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 apartado 9.4. Se trata de 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.
Recordemos el esquema de la base de datos. Consta de 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);
--
-- Desencadenantes 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);
--
-- Desencadenantes 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 y 122: las claves primarias de las tablas están en modo [AUTO_INCREMENT]. Es MySQL quien las gestiona, 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 incrustado en el 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 línea 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 línea en la tabla [cotisations]. A continuación, inicializa a 1 la columna [VERSIONING];
- al final, la columna [VERSIONING] toma el valor 1 cuando se crea una fila en la tabla [cotisations] y, a continuación, se incrementa en 1 cada vez que se realiza una modificación en dicha fila. Este mecanismo permite a EF5 gestionar la concurrencia de acceso a una fila de la tabla [cotisations] de la siguiente manera:
- un proceso P1 lee una línea L de la tabla [cotisations] en el momento T1. La línea tiene en la columna [VERSIONING] el valor V1;
- un proceso P2 lee la misma línea 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 dicha modificación. La columna [VERSIONING] de la línea L pasa entonces a V1+1 debido al desencadenador [INCR_VERSIONING_COTISATIONS];
- a continuación, 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 difiere del 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 denomina gestión optimista de los accesos concurrentes. Con EF5, un campo que desempeñe esta función debe tener la anotación [ConcurrencyCheck].
- Se crea un mecanismo análogo 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 de datos [dbpam_ef5] debe crearse previamente, ya que el script no la crea. A continuación, se ejecutará el script SQL en esta base de datos.
9.22.2. El proyecto de Visual Studio
Con Visual Studio Express 2012 para escritorio, cargamos la solución [pam-td] utilizada durante la creación de la capa [web]:
![]() |
- En [1], VS, Visual Studio Express 2012 para escritorio no consigue cargar el proyecto web [pam-web-01]. Esto es normal y no supone ningún problema;
- en [2], se añade 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. Añadir las referencias necesarias al proyecto
Situemos el proyecto en su contexto general:
![]() |
Nuestro proyecto necesita una serie de DLL:
- el DLL de Entity Framework 5;
- el DLL del conector ADO.NET del SGBD MySQL.
El apartado 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 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], añadimos 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 se confirma dos veces;
- en [5], los tres DLL se han añadido a las referencias del proyecto.
Necesitamos otro DLL. Este se encontrará entre los del marco .NET del equipo.
![]() |
- en [1], añade una nueva referencia al proyecto;
![]() |
- en [2], seleccione [Assemblys];
- en [3], escribe [system.component];
- en [4], seleccione el ensamblado [System.ComponentModel.DataAnnotations];
- en [5], se ha añadido la referencia.
Ya 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 apartado 9.7.3, página 219). No eran representaciones fieles de las tablas. Por ello, se habían ignorado las columnas [ID, VERSIONING]. En este caso, no va a ser así, ya que las utilizan las entidades ORM y EF5. Por lo tanto, vamos a añadirles 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 añadido las propiedades de las líneas 7 y 12 para reflejar la estructura de la tabla [cotisations];
- línea 17: el método [ToString] muestra ahora los dos nuevos campos.
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 añadido las propiedades de las líneas 7 y 13 para reflejar la estructura de la tabla [indemnites];
- línea 18: el método [ToString] muestra ahora los dos nuevos campos.
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 añadido las propiedades de las líneas 8 y 16 para reflejar la estructura de la tabla [employes];
- Línea 21: el método [ToString] muestra ahora los dos nuevos campos.
Para que puedan ser utilizadas por ORM y EF5, las propiedades de estas clases deben estar decoradas con anotaciones.
Tarea: con la ayuda del apartado 3.4 de [Création de la base à partir des entités] de [refEF5], añade 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 apartado mencionado;
- para la anotación [Table], siga el ejemplo MySQL del apartado 4.2 de [refEF5];
- para la anotación [ConcurrencyCheck] sobre la propiedad [Versioning], siga el ejemplo de Oracle del apartado 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 este modo, añadirá 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]. Asigne 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 el documento [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 una serie de datos para acceder a esta base de datos. Estos se encuentran 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] tendrá el siguiente aspecto:
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 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 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 apartado 3.5.4 [Apprentissage de LINQ avec LINQPad] de [refEF5].
En lo sucesivo, denominaremos a la clase [DbPamContext] «contexto de persistencia» de la base de datos [dbpam_ef5]. Se trata de 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 concretos: 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 apartados de [refEF5]:
- 3.4 para el servidor SGBD SQL. Aquí se establecen los principios fundamentales de la configuración de EF5;
- 4.2 para el servidor SGBD MySQL.
Siguiendo este último párrafo, 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 predeterminado 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 añadido las líneas 6-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 apartado 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 introducir cualquier cosa. Por defecto, hay que introducir 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 introducir entonces [name="DbPamContext"];
- línea 10: una cadena de conexión específica para SGBD MySQL:
- [Server=localhost]: dirección IP del equipo que aloja el SGBD. En este caso, se trata del equipo 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 este nombre de usuario. En este caso, no hay contraseña;
- línea 10: [providerName="MySql.Data.MySqlClient"] es el nombre del conector ADO.NET que se va a utilizar. Este nombre es el del atributo [invariant] de la línea 17. Se puede introducir cualquier valor siempre que se respete la regla anterior y que 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. Hay que prestar 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 has añadido a las referencias del proyecto:
![]() |
- La línea 16 es importante. Como no se pueden instalar dos proveedores con el mismo nombre, lo primero que hay que hacer es eliminar cualquier proveedor ya instalado que tenga el mismo nombre que el que vamos a instalar en la línea 17;
Eso es todo. Resulta complicado y confuso la primera vez que se hace, pero con el tiempo se vuelve sencillo, ya que siempre se repite lo mismo.
9.22.6. Prueba de la capa [EF5]
Ya estamos listos para probar nuestra capa [EF5]. Lo hacemos con ayuda del programa [Program.cs], que ya está instalado:
![]() |
Vamos a mostrar el contenido de la base de datos. Si lo conseguimos, será un primer indicio de que nuestra configuración es correcta. En el apartado 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: cualquier 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 en 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 anteriormente se anulan. 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 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 transfieren al contexto de persistencia junto con dicha entidad. Solo se transfieren 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.
Así, si el método [ToString] hubiera hecho referencia a la propiedad [Indemnites] como se muestra a continuación:
// 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 indemnizaciones, 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] ofrece los siguientes resultados:
¿Qué hacer si no funciona? Lo has hecho mal... Hay muchas posibles fuentes de error:
- comprueba la configuración de EF5 (apartado 9.22.5);
- comprueba tus entidades de Entity Framework (apartado 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 hace en las propiedades del proyecto, tal y como se ha visto en el apartado 9.7.6, para la capa de negocio simulada.
Tarea: transforma el tipo del proyecto [pam-ef5] en una biblioteca de clases y, a continuación, vuelve 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 creado:
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);
}
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.
En la 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 procederá 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 partir de la siguiente interfaz para la capa [DAO]:
public interface IPamDao {
// lista de todas las identidades de los empleados
Employe[] GetAllIdentitesEmployes();
// un empleado concreto con sus complementos
Employe GetEmploye(string ss);
// lista de todas las cotizaciones
Cotisations GetCotisations();
}
9.23.2. El proyecto de Visual Studio
Tarea: añadir a la solución [pam-td] un nuevo proyecto de tipo [console] llamado [pam-dao]. Establecerlo como proyecto de inicio de la solución.
![]() |
9.23.3. Añadir las referencias necesarias al proyecto
Veamos el proyecto en su conjunto:
![]() |
El proyecto [pam-dao] necesita un determinado número de DLL:
- todas las que aparecen referenciadas en 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 DLL, [Spring.core] y [Common.Logging]. Estos DLL se encuentran en la carpeta [lib] del material del caso práctico.
Tarea: añade estas referencias al proyecto [pam-dao].
![]() |
9.23.4. Implementación de la capa [DAO]
![]() |
La clase [PamException] mencionada anteriormente es la que se definió en el apartado 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 apartado 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 concreto 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()
{
// cotización
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 tienen sus complementos;
- 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 la Seguridad 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 apartado 9.22.6. Para el método [GetEmploye], se tomará como referencia el ejemplo del apartado 3.5.7, [Eager and Lazy loading], de [refEF5].
9.23.5. Configuración de la capa [DAO]
Al igual que se ha hecho en el apartado 9.22.5, debemos configurar EF5 en el archivo [App.config] del proyecto:
![]() |
Tarea 1: configura EF5 en [App.config]. Basta con repetir lo que se ha hecho 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: basándote en lo que se ha hecho en el apartado 9.20.2, modifica el archivo de configuración [app.config] del proyecto [pam-dao] para que defina un objeto Spring denominado [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]
Ya estamos listos para probar nuestra capa [DAO]. Lo hacemos con ayuda del programa [Program.cs], que ya está instalado:
![]() |
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 una 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: transforma el tipo del proyecto [pam-dao] en una biblioteca de clases y, a continuación, vuelve a generar el proyecto (repite lo que se hizo en el apartado 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 hemos creado en el apartado 9.7.2.
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);
}
9.24.2. El proyecto de Visual Studio
Tarea: añade a la solución [pam-td] un nuevo proyecto de tipo [console] llamado [pam-metier]. Establece este proyecto como proyecto de inicio de la solución.
![]() |
9.24.3. Añadir las referencias necesarias al proyecto
Veamos el proyecto en su conjunto:
![]() |
El proyecto [pam-metier] necesita un determinado número de DLL:
- todas las que aparecen referenciadas en los proyectos [pam-dao] y [pam-ef5];
- las de los propios proyectos [pam-dao] y [pam-ef5].
Tarea: añade estas diferentes referencias al proyecto [pam-metier].
![]() |
9.24.4. Implementación de la capa [métier]
![]() |
En lo anterior se muestran cuatro elementos ya utilizados en la capa simulada [métier] (véase el apartado 9.7). Puede haber cambios en los espacios de nombres importados por estas diferentes clases. Gestiónalos. 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 concreto con sus complementos
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.º SS del empleado
// 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 se habrá inicializado.
Tarea: completar el código de la clase [PamMetier]. Si en [GetSalaire] se detecta que el empleado con el n.º ss no existe, se lanzará un [PamException]. El método de cálculo del salario se explica en el apartado 9.5. Se prestará atención a 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 apartado 9.22.5, debemos configurar EF5 en el archivo [app.config] del proyecto:
![]() |
Tarea 1: configura EF5 en [app.config]. Basta con repetir lo que se ha hecho 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: basándote en lo que has hecho anteriormente en el apartado 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 añadir 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, por tanto, 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 justo lo que queríamos.
9.24.6. Prueba de la capa [métier]
Ya estamos listos para probar nuestra capa [métier]. Lo hacemos mediante 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, a continuación, vuelve a generar el proyecto (repite lo que se hizo en el apartado 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
Volvemos 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. Se trata principalmente de realizar algunas configuraciones y unos cuantos 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 ordenador.
- 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 dicho 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 por [pam-web-02] y establece este proyecto como 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. Añadir las referencias necesarias al proyecto
Veamos el proyecto en su conjunto:
![]() |
El proyecto [pam-web-02] necesita un determinado número de DLL:
- todos los que hacen referencia los proyectos [pam-metier], [pam-dao] y [pam-ef5];
- las de los propios proyectos [pam-metier], [pam-dao] y [pam-ef5].
Tarea: añada estas diferentes referencias al proyecto [pam-web-02]. Se debe eliminar la referencia del proyecto [pam-metier-simule]. Cambiamos de capa a [métier]. Algunas referencias DLL ya están presentes en las referencias. Elimínalas y, a continuación, realiza las adiciones.
![]() |
9.25.3. Implementación de la capa [web]
Genera 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 tal y como se indica más arriba.
9.25.4. Configuración de la capa [web]
Tal y como se hizo en el apartado 9.24.5, debemos configurar EF5 en el archivo [web.config] del proyecto:
![]() |
Tarea 1: sustituya 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 recuperar 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] (compruébalo en tu archivo [web.config]).
9.25.5. Prueba de la capa [web]
Ya estamos listos para probar nuestra capa [web]. Primero vamos a cambiar su puerto de trabajo. Por defecto, [pam-web-02] tiene la configuración de [pam-web-01] y, por lo tanto, funciona en el mismo puerto. La experiencia demuestra que esto plantea 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 destacar 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 ha obtenido una página de errores legible, aunque algunos mensajes estén en inglés. Ahora volvamos a detener 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 apartado 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]. El servidor integrado de Visual Studio rechaza cualquier otra dirección y devuelve el error [400 Bad Request].
Esto se puede comprobar de la siguiente manera:
- en una ventana DOS, anota la dirección IP de tu 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 dispone de conexión wifi, la dirección wifi del equipo aparecerá en las líneas 20 y siguientes.
- Comprueba las propiedades del proyecto [clic droit sur projet / propriétés / onglet web]:
![]() |
La aplicación se ejecutará en el puerto [65010] del equipo [localhost].
- Ejecute su proyecto mediante [Ctrl-F5]

- Sustituya [localhost] por la dirección IP del equipo:

El servidor ha devuelto 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 URL del tipo [http://adresseIP/contexte/...], es necesario utilizar un servidor distinto de IIS Express, por ejemplo, un servidor IIS (no Express). Para comprobar si está presente (normalmente en las versiones Pro de Windows), hay que ir al panel de control [Panneau de configuration\Système et sécurité\Outils d’administration]:

Esta opción no siempre está disponible. En ese caso, hay que ir a [ Panneau de configuration \ Programmes] e instalar las Herramientas de administración web.
![]() |
Una vez que aparezca la opción [Gestionnaire des services internet (IIS)], hay que activarla:
![]() |
Se inicia el sitio web por defecto. Para ello, es necesario que el servicio [Service de publication World Wide Web] se haya iniciado previamente:
![]() |
Una vez hecho esto, acceda a URL [http://localhost] con un navegador. Compruebe antes de que ningún otro servidor web esté ocupando ya el puerto 80. Si es así, deténgalo.
![]() |
El servidor IIS nos ha respondido. Ahora sustituye [localhost] por la dirección IP de tu ordenador:
![]() |
Funciona. Volvamos ahora 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 acceso URL de la aplicación. Se puede modificar. Ejecuta el proyecto mediante [Ctrl-F5]:
![]() |
Ahora sustituya [localhost] por la dirección IP de su equipo:
![]() |
Si no dispone del servidor IIS, puede utilizar un servidor ASP.NET gratuito, como el [Ultidev Web Server Pro], disponible en el URL [http://ultidev.com/Download/ ]. Una vez instalado, hay dos formas de ejecutar una aplicación web con este servidor:
El método rápido
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 indica a continuación esté en marcha:
![]() |
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:
![]() |
A continuación, se puede sustituir [localhost] por la dirección IP del equipo:
![]() |
Así que, en este caso también, solo se acepta el nombre [localhost].
El método largo
Inicie la aplicación Ultidev Web Explorer
![]() |
y, a continuación, sigue estos pasos:
![]() |
![]() |
![]() |
- en [8], indique 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/];
![]() |
![]() |
- inicia el servidor web con [14];
![]() |
- solicita el URL [19] ;
![]() |
- en [20], se ha obtenido la página deseada utilizando la dirección local IP del equipo en lugar del nombre [localhost]. Esto es lo que buscábamos;
El servidor Ultidev se ha instalado como un servicio de Windows que se inicia automáticamente. Puedes desactivar el inicio automático del servidor Ultidev de la siguiente manera:
- seleccionar 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 en «Inicio manual».
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 dispone de 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 la página web de PhoneGap [http://build.phonegap.com/apps] «sube» el archivo zip del sitio web que se desea 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 framework web (ASP.NET, JEE, PHP, ...). Empezaremos 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 aplicaciones 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 esquema anterior no indica de dónde procede);
- las vistas siguientes 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 demás vistas obtenidas mediante llamadas Ajax. Si no la proporciona el mismo servidor, el JavaScript de la página inicial debe conocer el URL del servidor web que va a entregar las demás 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 tiene 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 apartado 9.26;
- en [2], el usuario deberá introducir el URL del simulador de nóminas. Se podría introducir de forma fija en el código JavaScript de la página inicial, pero eso complicaría las pruebas: en cuanto 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. Refactorizació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 (los 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. El resultado es 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 añadido los siguientes elementos:
- líneas 27-29: se ha añadido la opción de menú [Connexion] para permitir la conexión al servicio de simulación;
- líneas 55-56: la introducción 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 introducen en una tabla
options = [lnkConnexion, lnkFaireSimulation, lnkEffacerSimulation, lnkEnregistrerSimulation, lnkVoirSimulations, lnkTerminerSession, lnkRetourFormulaire];
// se ocultan algunos 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 introducción de datos
$.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 URL introducido por el usuario. Se asigna a la variable global de la línea 1. De este modo, 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 introducción de datos de la simulación (empleados, horas trabajadas, días trabajados). En la versión inicial de [pam-web-02], esta URL era suficiente. Se le añadía automáticamente el prefijo de la 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. En ese caso, 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 establece correctamente, 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 establece correctamente, se obtiene la siguiente página:
![]() |
Nos encontramos entonces en la situación anterior, en la que ahora las vistas se obtienen mediante llamadas Ajax. Así, en el ejemplo anterior, 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 un único cambio, en la línea 10, donde el anterior URL ahora va precedido del prefijo del simulador;
9.27.3. Prueba del proyecto refactorizado
En el apartado 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:
![]() |
Introducimos 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 introduce el código correcto URL:
![]() |
se obtiene la siguiente respuesta:
![]() |
9.27.4. Creación del binario para Android
Vamos a crear el binario de Android a partir del sitio web estático que acabamos de crear y probar [1]:
![]() | ![]() |
Añadimos en [2] un archivo [config.xml] que servirá para configurar el plugin [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: introduce 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á en el dispositivo Android, solicite archivos URL externos a dicho dispositivo;
Comprimimos el contenido de la carpeta [Content/bootstrap]:
![]() |
A continuación, accedemos a la página web de Phonegap [http://build.phonegap.com/apps]:
![]() |
- Antes de [1], es posible que tengas que crear una cuenta;
- en [1], empezamos;
- en [2], se elige un plan gratuito que solo permite una aplicación Phonegap;
- en [3], se descarga la aplicación comprimida [4];
![]() |
![]() |
- en [5], el nombre de la aplicación;
- haz 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 (véase el apartado 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, arrastra el archivo [Pam-debug.apk] que has descargado y suéltalo en el emulador. Se instalará y se ejecutará;
![]() |
Introduce en [1] el URL del simulador tal y como se ha descrito en el apartado 9.27.3. Una vez hecho esto, conéctate al simulador mediante el enlace [2]:
![]() |
Pruebe la aplicación en el emulador. Debería funcionar.








































































































































































































