Cómo hacer un Wizard (formulario estilo next-next-finish) con .NET Core MVC
Aprende a hacer una serie de formularios web al estilo de un asistente Wizard esos de next next finish! 🥳

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

Perfecto 🥳

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 🚀

2 comentarios en «Cómo hacer un Wizard (formulario estilo next-next-finish) con .NET Core MVC»

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *