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
  • 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 馃殌

Deja una respuesta

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