En esta entrada vamos a hacer algo pr谩ctico: una aplicaci贸n web con el patr贸n MVC. Programaremos unos formularios con estilo asistente Wizard de los que contienen siguiente, siguiente, finalizar.
Las funcionalidades de nuestra aplicacion ser谩n:
- La aplicaci贸n permitir谩 guardar los datos ingresados en cada formulario, si retrocede a煤n podr谩 ver los datos ingresados
- La aplicaci贸n tendr谩 validaciones en los campos de los formularios
- Al finalizar el asistente o Wizard guardar谩 los datos en la base de datos
- Permitir谩 cancelar el proceso en cualquiera de las ventanas, siendo redirigidos a la pantalla principal
- Nuestro Wizard tendr谩 2 pantallas, es decir constar谩 de s贸lo dos pasos, en la primera un siguiente y en la segunda un finalizar.
Las tecnolog铆as y caracter铆sticas t茅cnicas usadas ser谩n:
- Usaremos el patr贸n MVC
- Utilizaremos Entity Framework Core Code First para la persistencia de la informaci贸n
- Utilizaremos .NET Core 3.1
- Para el frontend UI utilizaremos Razor y Bootstrap
- Se usar谩 una vista por cada pantalla
- Haremos uso de los ViewModels para mapear cada pantalla del Wizard
Vamos a ser pragm谩ticos, as铆 que iremos al grano, comencemos cracks! 馃挭馃敟
Configurando el proyecto
El caso de estudio ser谩: Nos pidieron que hagamos una serie de formularios para guardar la informaci贸n de n贸mina de personal que tenga dos ventanas en la primera pedir谩 informaci贸n personal y en la segunda ventana informaci贸n laboral y finalmente finalizaremos el registro.
Creamos un proyecto nuevo, en mi caso usar茅 Visual Studio 2019
Creamos un proyecto tipo ASP.NET Core Web App (Model-View-Controller)
Lo llamar茅 SmartWizard
Tenemos un proyecto nuevo y limpio:

Instalando Paquetes desde Nuget
Para el presente proyecto necesitaremos instalar los siguientes packages:
- Microsoft.EntityFrameworkCore
- Microsoft.EntityFrameworkCore.SqlServer
- Microsoft.EntityFrameworkCore.Tools
Creando las entidades
Haremos este proyecto simple, con una sola entidad, tu puedes luego hacerlo con varias tablas inclusive con relaciones entra tablas y una inserci贸n en cascada donde desde un asistente guarde a diversas tablas en la base de datos manejando todo de forma transaccional, esto ser谩 motivo para un post futuro. Pero aqu铆 lo mantendremos simple y pr谩ctico:
Creamos la carpeta Entities y dentro la clase empleado:
public class Empleado
{
[Key]
public int Id { get; set; }
[Column(TypeName = "varchar(75)")]
public string Nombres { get; set; }
[Column(TypeName = "varchar(75)")]
public string Apellidos { get; set; }
[Column(TypeName = "varchar(100)")]
public string Domicilio { get; set; }
[Column(TypeName = "varchar(30)")]
public string Departamento { get; set; }
public DateTime FechaIngreso { get; set; }
public decimal Salario { get; set; }
}
Configurando los datos
Una vez que ya tenemos la entidad, ahora toca crear la cadena de conexi贸n, esto lo hacemos desde el archivo appsettings.json, agregamos lo siguiente:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"default": "server=.;initial catalog=test0810;integrated security=true"
}
}
En la cadena de conexi贸n configurar seg煤n corresponda en nuestro caso
Ahora crearemos la clase para usar como contexto en el Entity Framework dentro de la carpeta Models, a mi me gusta llamarla ApplicationDBContext y tendr谩 el siguiente contenido:
public class ApplicationDBContext : DbContext
{
public ApplicationDBContext(DbContextOptions options) : base(options)
{
}
public DbSet<Empleado> Empleados { get; set; }
}
Inyecci贸n de dependencias
La inyecci贸n de dependencias la usamos para que el framework inyecte a nuestro contexto en todos los controladores, agregamos el siguiente c贸digo en el m茅todo ConfigureServices de la clase Startup
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddDbContext<ApplicationDBContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("default"))
);
}
Migraciones
En la consola Nuget o tambi茅n llamado Package Manager Console ejecutar el comando
add-migration initial
Eso har谩 que se generen las clases para la migraci贸n del c贸digo de nuestras entidades hacia la base de datos
Si no hay errores entonces ejecutamos
update-database
Para que esa migraci贸n se vea reflejada en la base de datos
Ahora si ves en tu SQL Server Management Studio podr谩s ver la tabla creada

View models
En la Carpeta Models crearemos dos ViewModels, uno por cada pantalla de nuestro wizard
public class EmpleadoPersonalViewModel
{
[Required (ErrorMessage = "El campo nombres es obligatorio")]
public string Nombres { get; set; }
[Required(ErrorMessage = "El campo apellidos es obligatorio")]
public string Apellidos { get; set; }
[Required(ErrorMessage = "El campo domicilio es obligatorio")]
public string Domicilio { get; set; }
}
Y para la segunda pantalla donde ir谩n los datos laborales:
public class EmpleadoLaboralViewModel
{
[Required(ErrorMessage = "El departamento es obligatorio")]
public string Departamento { get; set; }
[Required(ErrorMessage = "La fecha de ingreso es obligatoria")]
public DateTime FechaIngreso { get; set; }
[Required(ErrorMessage = "El salario es obligatorio")]
public Decimal Salario { get; set; }
}
Estamos haciendo uso de los data annotations para efectos de validaci贸n de campos en el front end.
Variables de sesi贸n
Para que cuando hayamos pasado al paso 2 y regresemos al formulario anterior en caso demos click a previous y se muestre lo que hab铆amos rellenado en los controles debemos guardar los datos de los formularios temporalmente y para esto usaremos variables de sesi贸n, en .NET Framework era muy sencillo pero en Net core requiere de m谩s pasos, veamos:
Primero creamos una carpeta Utils y dentro una clase est谩tica llamada SessionExtensions:
public static class SessionExtensions
{
public static void SetObject(this ISession session, string key, object value)
{
session.SetString(key, JsonConvert.SerializeObject(value));
}
public static T GetObject<T>(this ISession session, string key)
{
var value = session.GetString(key);
return value == null ? default(T) : JsonConvert.DeserializeObject<T>(value);
}
}
Las variables de sesi贸n en Net core a diferencia de Net framework tenemos que configurarlas en la clase Startup en los m茅todos ConfigureServices y Configure, a帽adir lo siguiente:
En el m茅todo ConfigureServices:
public void ConfigureServices(IServiceCollection services)
{
services.AddDistributedMemoryCache();
services.AddSession(options => {
options.IdleTimeout = TimeSpan.FromMinutes(10);//Puede cambiar el tiempo
});
services.AddControllersWithViews();
services.AddDbContext<ApplicationDBContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("default"))
);
}
Ojo crack, estas lineas tienen que estar antes de services.AddControllersWithViews()
En el m茅todo Configure:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSession();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
Controladores
Crear el Controlador EmpleadosController y codificar:
public class EmpleadosController : Controller
{
private readonly ApplicationDBContext context;
public EmpleadosController(ApplicationDBContext context)
{
this.context = context;
}
public ActionResult Index()
{
return View("PersonalInfo");
}
private Empleado GetEmpleado()
{
if (HttpContext.Session.GetObject<Empleado>("DataObject") == null)
{
HttpContext.Session.SetObject("DataObject", new Empleado());
}
return (Empleado)HttpContext.Session.GetObject<Empleado>("DataObject");
}
private void RemoveEmpleado()
{
HttpContext.Session.SetObject("DataObject", null);
}
[HttpPost]
public ActionResult PersonalInfo(EmpleadoPersonalViewModel personal, string BtnPrevious, string BtnNext)
{
if (BtnNext != null)
{
if (ModelState.IsValid)
{
Empleado empleado = GetEmpleado();
empleado.Nombres = personal.Nombres;
empleado.Apellidos = personal.Apellidos;
empleado.Domicilio = personal.Domicilio;
HttpContext.Session.SetObject("DataObject", empleado);
return View("LaboralInfo");
}
}
return View();
}
[HttpPost]
public ActionResult LaboralInfo(EmpleadoLaboralViewModel laboral, string BtnPrevious, string BtnNext, string BtnCancel)
{
Empleado empleado = GetEmpleado();
if (BtnPrevious != null)
{
EmpleadoPersonalViewModel info = new EmpleadoPersonalViewModel();
info.Nombres = empleado.Nombres;
info.Apellidos = empleado.Apellidos;
info.Domicilio = empleado.Domicilio;
return View("PersonalInfo", info);
}
if (BtnNext != null)
{
if (ModelState.IsValid)
{
empleado.Departamento = laboral.Departamento;
empleado.FechaIngreso = laboral.FechaIngreso;
empleado.Salario = laboral.Salario;
context.Empleados.Add(empleado);
context.SaveChanges();
RemoveEmpleado();
return View("Completado");
}
}
if (BtnCancel != null)
RemoveEmpleado();
return View();
}
}
Personalizando las vistas
A帽adir el link a la p谩gina inicial del proyecto en la carpeta Shared y el archivo _Layout.cshtml
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container">
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">SmartWizard</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Empleados" asp-action="Index">Empleados</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
Ahora creamos las tres vistas que usaremos:
La vista PersonalInfo.cshtml
@model SmartWizard.Models.EmpleadoPersonalViewModel
@{
ViewBag.Title = "Personal Info";
}
<h2>Personal info</h2>
@using (Html.BeginForm("PersonalInfo", "Empleados", FormMethod.Post))
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>PASO 1: Infomaci贸n personal del empleado</h4>
<hr />
@Html.ValidationSummary(true)
<div class="form-group">
@Html.LabelFor(model => model.Nombres, new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.TextBoxFor(model => model.Nombres)
@Html.ValidationMessageFor(model => model.Nombres)
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Apellidos, new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.TextBoxFor(model => model.Apellidos)
@Html.ValidationMessageFor(model => model.Apellidos)
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Domicilio, new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.TextBoxFor(model => model.Domicilio)
@Html.ValidationMessageFor(model => model.Domicilio)
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" name="BtnPrevious" value="Previous" class="btn btn-default" />
<input type="submit" name="BtnNext" value="Next" class="btn btn-default" />
</div>
</div>
</div>
}
La vista LaboralInfo.cshtml
@model SmartWizard.Models.EmpleadoLaboralViewModel
@{
ViewBag.Title = "Laboral Info";
}
<h2>Laboral Info</h2>
@using (Html.BeginForm("LaboralInfo", "Empleados", FormMethod.Post))
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>PASO 2: Informaci贸n laboral del empleado</h4>
<hr />
@Html.ValidationSummary(true)
<div class="form-group">
@Html.LabelFor(model => model.Departamento, new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.TextBoxFor(model => model.Departamento)
@Html.ValidationMessageFor(model => model.Departamento)
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.FechaIngreso, new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.TextBoxFor(model => model.FechaIngreso, "{0:yyyy-MM-dd}", new { type = "date" })
@Html.ValidationMessageFor(model => model.FechaIngreso)
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Salario, new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.TextBoxFor(model => model.Salario)
@Html.ValidationMessageFor(model => model.Salario)
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" name="BtnPrevious" value="Previous" class="btn btn-default" />
<input type="submit" name="BtnNext" value="Finish" class="btn btn-default" />
<input type="submit" name="BtnCancel" value="Cancel" class="btn btn-danger" />
</div>
</div>
</div>
}
Finalmente Completado.cshtml
@{
ViewBag.Title = "Completado";
}
<h3>El empleado fue agregado!</h3>
<div>
@Html.ActionLink("A帽adir otro", "Index", "Empleados")
</div>
Entonces ya tenemos todo listo, y el proyecto luce as铆:

Tests
Lleg贸 la hora de las pruebas, corremos el proyecto:

Chequeamos que las validaciones est茅n funcionando

Completamos los datos e intentamos guardar

Genial, se nos muestra la vista Completado! 馃敟

Ahora vamos a la BD a ver si es cierta tanta belleza

Somos unos cracks! 馃挭馃コ
Listo, ahi lo tienes, disfr煤talo y practica como un orate crack, hasta la pr贸xima.
Si deseas el c贸digo, est谩 disponible en mi Github https://github.com/GeaSmart/WizardMVC
Desde esta entrada del buen colega Nimit Joshi de c-sharpcorner.com puedes hacer algo similar, pero en su caso usando .Net Framework y EF database first con el asistente, sin duda un gran aporte tambi茅n.

Si esta entrada te ha gustado considera compartirla pues, eso ayuda bastante genios 馃殌