Gestor de gastos: Aplicación web con .Net Core MVC y AJAX
Aprende a hacer una aplicación web sencilla de la mejor manera posible: haciendo cosas y dejando de postergarlas 😉

En esta ocasión les traigo un taller: haremos una aplicación web para la gestión de nuestros gastos. Este es el corazón de este blog pragmático, aquí plasmaremos la teóría en una aplicación web totalmente funcional, y aunque es una aplicación sencilla, te permitirá consolidar lo aprendido, debes saber que esta es la mejor manera de aprender crack y la razón de ser de este blog, así que comencemos primero repasando los temas que aprenderemos:

  • Programación asíncrona
  • Patrón MVC
  • .Net Core C#
  • Entity Framework, la técnica code first
  • AJAX
  • Razor
  • Bootstrap
  • Formulario modal
  • Filtros
  • Notify JS

Configuración inicial

Configurando el proyecto

Creamos un nuevo proyecto con Visual Studio, en mi caso la versión 2019, el proyecto será del tipo:

Aplicación web de ASP.NET Core (Modelo-Vista-Controlador)

o si lo tienes en inglés:

ASP.NET Core Web App (Model-View-Controller)

Yo llamaré a mi proyecto: MisGastos

En cuanto a la versión de .Net Core puedes seleccionar la 3.1 o la 5.0 como prefieras, este proyecto en ambas lo probé y es compatible.

Visual Studio nos crea el proyecto base, verás algo similar a esto:

Instalando librerías desde Nuget

Como me gusta ir en orden de arranque vamos a instalar las dependencias que utilizaremos para este proyecto así que instala estas librerías desde Nuget (clic derecho al proyecto y administrar paquetes de Nuget):

  • Microsoft.EntityFrameworkCore (en mi caso la versión 5.0.15)
  • Microsoft.EntityFrameworkCore.SqlServer (en mi caso la versión 5.0.15)
  • Microsoft.EntityFrameworkCore.Tools (en mi caso la versión 5.0.15)

Una vez instalados, tendremos esto:

Cadena de conexión

Es momento de crear nuestra cadena de conexión: la cadena va en el archivo appsettings.Development.json, así que la añadimos, yo la llamaré defaultConnection:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "connectionStrings": {
    "defaultConnection": "server=.;initial catalog=MisGastos0204;integrated security=true"
  }
}

Mi cadena tiene esa apariencia, tu deberías poner tus respectivos datos, pero usualmente te va a funcionar si la dejas tal cual está. Para estas alturas ya deberías saber la estructura y sintaxis de una cadena de conexión pero si no, no te alarmes, es muy sencillo y tienes a stackoverflow siempre 😉

Podríamos haber creado nuestra cadena de conexión en appsettings.json directamente, pero como estamos en un ambiente de desarrollo es buena práctica hacerlo en appsettings.Development.json

Creando el modelo

Con modelo me refiero a las entidades, o en este caso, la entidad, ya que habrá solo una en esta aplicación: Gasto.

Entonces en la carpeta Models añadimos la clase Gasto:

    public class Gasto
    {
        [Key]
        public int Id { get; set; }

        [Column(TypeName = "varchar(70)")]
        [MaxLength(70)]
        [Required(ErrorMessage = "La descripción es obligatoria")]
        [DisplayName("Descripción")]
        public string Descripcion { get; set; }

        [Column(TypeName = "decimal(10,2)")]
        [Required(ErrorMessage = "El monto es obligatorio")]
        [DisplayName("Monto")]        
        public decimal Monto { get; set; }

        [Column(TypeName = "datetime")]
        [Required(ErrorMessage = "Ingrese la fecha")]
        [DisplayFormat(DataFormatString = "{0:dd/MM/yyyy}", ApplyFormatInEditMode = true)]
        [Display(Name = "Fecha del gasto")]        
        public DateTime Fecha { get; set; }
    }

Estamos haciendo uso de los data annotations para especificarle a Entity Framework y al frontend algunas instrucciones, como por ejemplo con que label se mostrará el control en el frontend, o qué tido de dato va a tener en la base de datos.

Creando el Contexto y configurándolo

En el directorio raiz del proyecto creamos una clase llamada ApplicationDbContext, este será nuestro contexto, algo imprescindible cuando se trabaja con Entity Framework

    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext(DbContextOptions options) : base(options)
        {

        }

        public DbSet<Gasto> Gastos { get; set; } //Esta se convertirá en una tabla en la BD cuando migremos
    }

Ahora es momento de configurar nuestro contexto en Startup -> ConfigureServices

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();

            //Configurando nuesto contexto
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(Configuration.GetConnectionString("defaultConnection")));
        }

Esto se hace para hacer uso de la inyección de dependencias posteriormente en los controladores, y de esta forma interactuar con la base de datos.

Hasta el momento el explorador de soluciones del proyecto tiene esta apariencia:

Aplicando migración

Una migración es cuando EF convierte nuestro modelo hecho en código C# en un modelo de base de datos en SQL Server, esto es porque estamos usando EF bajo la técnica conocida como Code First.

Veamos como hacer nuestra primera migración:

Abrir la consola del administrador de paquetes o Package manager console (PMC):

Si no la ves, está en el menú Herramientas > Administrador de paquetes Nuget

Ejecutar los siguientes comandos, uno después del otro:

add-migration initial

update-database

Si todo está bien entonces tendrás esto y no se mostrarán errores:

Ahora revisemos si creó el modelo de datos en la Base de datos, a través de SQL Server Management Studio:

Genial vemos que todo bien! 😎

Disfruta mientras aprendes, ríete de tus bugs cuando estás en desarrollo para que no te suceda cuando estás en producción 😉

Desarrollando funcionalidades

Configurando font Awesome

Como vamos a usar esta fuente para íconos, entonces la configuramos.

Abrimos la carpeta Views > Shared

Actualizamos el archivo _Layout.cshtml añadiendo esta línea

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - MisGastos</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
    <link rel="stylesheet" href="~/css/site.css" />
    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" rel="stylesheet" />
</head>

Las cdn de distintas versiones las puedes obtener en https://cdnjs.com/libraries/font-awesome/5.15.4

Controlador GastosController

Creamos el controlador GastosController en la carpeta Controllers

   public class GastosController : Controller
    {
        private readonly ApplicationDbContext context;

        public GastosController(ApplicationDbContext context)
        {
            this.context = context;
        }

        [HttpGet]
        public async Task<IActionResult> Index()
        {
            var gastos = await context.Gastos.ToListAsync();
            return View(gastos);
        }

        [HttpGet]
        public async Task<ActionResult> AddOrEdit(int id = 0)
        {
            if (id == 0)
                return View(new Gasto() { Fecha = DateTime.Now });

            var gasto = await context.Gastos.FirstOrDefaultAsync(x => x.Id == id);
            if (gasto == null)
                return NotFound();

            return View(gasto);
        }

        [HttpPost]
        public async Task<ActionResult> AddOrEdit(int id, [FromForm] Gasto gasto)
        {
            if (ModelState.IsValid)
            {
                //insert
                if (id == 0)
                {
                    context.Gastos.Add(gasto);
                    await context.SaveChangesAsync();
                }
                //update
                else
                {
                    context.Gastos.Update(gasto);
                    await context.SaveChangesAsync();
                }
            }
            return View(gasto);
        }

        [HttpDelete]
        public async Task<ActionResult> Delete(int id)
        {
            var gasto = await context.Gastos.FirstOrDefaultAsync(x => x.Id == id);
            if (gasto == null)
                return NotFound();

            context.Gastos.Remove(gasto);
            await context.SaveChangesAsync();
            return View("Index", await context.Gastos.ToListAsync());
        }
    }

Configurando controlador de arranque

Para indicarle que arranque la aplicación con el controlador de Gastos vamos a la clase Startup y en el método Configure, actualizamos:

        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.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Gastos}/{action=Index}/{id?}");
            });
        }

Creamos la vista Index

En el controlador GastosController clic derecho y agregar vista (vacía) y la llamaremos Index.cshtml

Y tendrá de momento una estructura html como sigue:

@model IEnumerable<Gasto>
<div class="page">

    <h1 class="page-header text-center">Listado de Gastos <i class="fas fa-money-bill text-primary"></i> </h1>

    <div class="breadcrumb">
        <a onclick="showModal('@Url.Action("AddOrEdit","Gastos",new { Id = 0 },Context.Request.Scheme)', 'Nuevo Gasto')" class="btn btn-primary text-white">Agregar nuevo</a>
    </div>

    <table class="table table-hover">
        <thead>
            <tr>
                <th>@Html.DisplayNameFor(x => x.Descripcion)</th>
                <th>@Html.DisplayNameFor(x => x.Fecha)</th>
                <th>@Html.DisplayNameFor(x => x.Monto)</th>
            </tr>
        </thead>
        <tbody>
            @if (Model != null)
            {
                @foreach (var item in Model)
                {
                    <tr>
                        <td>@item.Descripcion</td>
                        <td>@item.Fecha</td>
                        <td>@item.Monto</td>
                        <td>Opciones...</td>
                    </tr>
                }
            }
        </tbody>
    </table>
    @section Scripts{
        @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
    }
</div>

Si compilamos y corremos el proyecto para ir viendo nuestro avance tendremos:

Vamos bien hasta ahora!

La tabla aparece vacía porque no tenemos datos, pero vamos a solucionar esto.

Creamos la vista AddOrEdit

Del mismo modo que antes, agregamos la vista para el método AddOrEdit del controlador GastosController

@model Gasto

@{
    Layout = null;
}

<div class="row">
    <div class="col-md-12">
        <form asp-action="AddOrEdit" asp-route-id="@Model.Id" onsubmit="return xxx" autocomplete="off">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>

            <input type="hidden" asp-for="Id" />            

            <div class="form-group">
                <label asp-for="Descripcion" class="control-label"></label>
                <input asp-for="Descripcion" class="form-control" />
                <span asp-validation-for="Descripcion" class="text-danger"></span>
            </div>

            <div class="row">
                <div class="col-md-6">
                    <div class="form-group">
                        <label asp-for="Monto" class="control-label"></label>
                        <div class="input-group">
                            <div class="input-group-prepend">
                                <div class="input-group-text"><i class="fas fa-dollar-sign"></i></div>
                            </div>
                            <input asp-for="Monto" class="form-control" />
                        </div>
                        <span asp-validation-for="Monto" class="text-danger"></span>
                    </div>
                </div>
                <div class="col-md-6">
                    <div class="form-group">
                        <label asp-for="Fecha" class="control-label"></label>                        
                        @Html.TextBoxFor(x => x.Fecha, "{0:dd/MM/yyyy}", new { @class = "form-control datepicker" })
                        <span asp-validation-for="Fecha" class="text-danger"></span>
                    </div>
                </div>
            </div>
                        

            <div class="row">
                <div class="col-md-12">
                    <input type="submit" value="Enviar" class="btn btn-primary btn-block" />
                </div>
            </div>

        </form>
    </div>
</div>

Modales en Bootstrap

Bootstrap nos ofrece modales, puedes checar la documentación aquí: https://getbootstrap.com/docs/4.4/components/modal/

Añadir el trozo de código para un modal en el archivo _Layout.cshtml dentro de la carpeta Views > Shared. Este trozo de código lo pones antes del footer.

    @*MODAL*@
    <div class="modal" tabindex="-1" role="dialog" id="form-modal">
        <div class="modal-dialog" role="document">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title"></h5>
                    <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                        <span aria-hidden="true">&times;</span>
                    </button>
                </div>
                <div class="modal-body">

                </div>
            </div>
        </div>
    </div>

Ahora añadiremos diferentes funciones en site.js dentro de la carpeta wwwroot > js

El archivo de javascript site.js sirve para que crees tu código JS personalizado y en nuestro caso contendrá varias funciones para el procesamiento de los modales y las peticiones AJAX, Visual Studio lo crea en el proyecto, pero en caso que no lo tengas, entonces deberías crearlo y añadir su referencia en _Layout.cshtml

En site.js definimos la siguiente función:

function showModal(url, title) {
    $.ajax(
        {
            type: "GET",
            url: url,
            success: function (response) {
                $("#form-modal .modal-body").html(response);
                $("#form-modal .modal-title").html(title);
                $("#form-modal").modal('show');
            }
        }
    )
}

Probamos el botón Agregar nuevo y vemos que el modal está funcionando 😉

Que bonita pinta va teniendo nuestro formulario, se ve profesional

Partial views

Para usar AJAX será necesario usar vistas parciales.

En este caso haremos que la tabla que está en la vista Index sea una vista parcial y la llamaremos _VerTodos.cshtml

@model IEnumerable<Gasto>

<table class="table table-striped">
    <thead>
        <tr>
            <th>@Html.DisplayNameFor(x => x.Descripcion)</th>
            <th>@Html.DisplayNameFor(x => x.Monto)</th>
            <th>@Html.DisplayNameFor(x => x.Fecha)</th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in @Model)
        {
            <tr>
                <td>@item.Descripcion</td>
                <td>@item.Monto</td>
                <td>@item.Fecha.ToShortDateString()</td>
                <td>...</td>
            </tr>
        }
    </tbody>
</table>

Entonces ahora debemos actualizar la vista Index.cshtml para sacar la tabla y en su lugar hacer la referencia a la vista parcial.

@model IEnumerable<Gasto>
<div class="page">

    <h1 class="page-header text-center">Listado de Gastos <i class="fas fa-money-bill text-primary"></i> </h1>

    <div class="breadcrumb">
        <a onclick="showModal('@Url.Action("AddOrEdit","Gastos",new { Id = 0 },Context.Request.Scheme)', 'Nuevo Gasto')" class="btn btn-primary text-white">Agregar nuevo</a>
    </div>

    <div id="view-all">
        @await Html.PartialAsync("_VerTodos", Model)
    </div>

    @section Scripts{
        @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
    }
</div>

Uso de Helpers

Crear una carpeta llamada Helpers y dentro haremos una clase helper llamada RenderRazor

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

namespace MisGastos.Helpers
{
    public class RenderRazor
    {
        public static string RenderRazorViewToString(Controller controller, string viewName, object model = null)
        {
            controller.ViewData.Model = model;
            using (var sw = new StringWriter())
            {
                IViewEngine viewEngine = controller.HttpContext.RequestServices.GetService(typeof(ICompositeViewEngine)) as ICompositeViewEngine;
                ViewEngineResult viewResult = viewEngine.FindView(controller.ControllerContext, viewName, false);

                ViewContext viewContext = new ViewContext(
                    controller.ControllerContext,
                    viewResult.View,
                    controller.ViewData,
                    controller.TempData,
                    sw,
                    new HtmlHelperOptions()
                );
                viewResult.View.RenderAsync(viewContext);
                return sw.GetStringBuilder().ToString();
            }
        }

        [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
        public class NoDirectAccessAttribute : ActionFilterAttribute
        {
            public override void OnActionExecuting(ActionExecutingContext filterContext)
            {
                if (filterContext.HttpContext.Request.GetTypedHeaders().Referer == null ||
         filterContext.HttpContext.Request.GetTypedHeaders().Host.Host.ToString() != filterContext.HttpContext.Request.GetTypedHeaders().Referer.Host.ToString())
                {
                    filterContext.HttpContext.Response.Redirect("/");
                }
            }
        }
    }
}

Actualizar el método AddOrEdit [HttpPost] de GastosController

        [HttpPost]
        public async Task<ActionResult> AddOrEdit(int id, [FromForm] Gasto gasto)
        {
            if (ModelState.IsValid)
            {
                //insert
                if (id == 0)
                {                    
                    context.Gastos.Add(gasto);
                    await context.SaveChangesAsync();
                }
                //update
                else
                {
                    context.Gastos.Update(gasto);
                    await context.SaveChangesAsync();
                }
                return Json(new { isValid = true, html = RenderRazor.RenderRazorViewToString(this, "_VerTodos", context.Gastos.ToList()) });
            }
            return Json(new { isValid = false, html = RenderRazor.RenderRazorViewToString(this, "AddOrEdit", gasto) });
        }

Añadimos una función más al archivo site.js para manejar el post de GastosController

function jQueryAjaxPost(form) {
    try {
        $.ajax({
            type: 'POST',
            url: form.action,
            data: new FormData(form),
            contentType: false,
            processData: false,
            success: function (response) {
                if (response.isValid) {
                    $("#view-all").html(response.html);
                    $("#form-modal .modal-body").html('');
                    $("#form-modal .modal-title").html('');
                    $("#form-modal").modal('hide');
                }
                else {
                    $("#form-modal .modal-body").html(response.html);
                }
            },
            error: function (error) {
                console.log(error)
            }
        })
    }
    catch (e) {
        console.log(e);
    }
    return false;
}

Actualizar ahora la vista AddOrEdit.cshtml:

<div class="row">
    <div class="col-md-12">
        <form asp-action="AddOrEdit" asp-route-id="@Model.Id" onsubmit="return jQueryAjaxPost(this)" autocomplete="off">
 ...

Probamos y tenemos esto, ya funciona el añadir un registro y también las validaciones en el modal 😎

Actualizar la vista parcial _VerTodos.cshtml para añadir los botones de editar y borrar

@model IEnumerable<Gasto>

<table class="table table-striped">
    <thead>
        <tr>
            <th>@Html.DisplayNameFor(x => x.Descripcion)</th>
            <th>@Html.DisplayNameFor(x => x.Monto)</th>
            <th>@Html.DisplayNameFor(x => x.Fecha)</th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in @Model)
        {
            <tr>
                <td>@item.Descripcion</td>
                <td>@item.Monto</td>
                <td>@item.Fecha.ToShortDateString()</td>
                <td>
                    <div class="container text-center">
                        <div class="row">
                            <a onclick="showModal('@Url.Action("AddOrEdit","Gastos",new { Id = item.Id }, Context.Request.Scheme)', 'Editar gasto')" class="btn btn-primary text-white mr-1"><i class="fas fa-pencil-alt"></i> Editar</a>

                            <form asp-action="Delete" asp-route-id="@item.Id" onsubmit="return jQueryAjaxDelete(this)">
                                <button type="submit" name="name" class="btn btn-danger"><i class="fas fa-trash"></i> Borrar</button>
                            </form>
                        </div>
                    </div>
                </td>
            </tr>
        }
    </tbody>
</table>

Los botones lucen así, y el editar ya es funcional!

Añadir una función en site.js para manejar el borrado con AJAX

function jQueryAjaxDelete(form) {
    if (confirm('¿Está seguro de eliminar el registro?')) {
        try {
            $.ajax({
                type: 'DELETE',
                url: form.action,
                data: new FormData(form),
                contentType: false,
                processData: false,
                success: function (response) {
                    $("#view-all").html(response.html);                    
                },
                error: function (error) {
                    console.log(error)
                }
            })            
        }
        catch (ex) {
            console.log(ex);
        }
        return false;
    }
		return false;
}

Actualizar el método Delete de GastosController:

        [HttpDelete]
        public async Task<ActionResult> Delete(int id)
        {
            var gasto = await context.Gastos.FirstOrDefaultAsync(x => x.Id == id);
            if (gasto == null)
                return NotFound();

            context.Gastos.Remove(gasto);
            await context.SaveChangesAsync();
            return Json(new { html = RenderRazor.RenderRazorViewToString(this, "_VerTodos", context.Gastos.ToList()) });
        }

Ahora ya está funcional tanto el editar como el borrar, genial crack!

Features Adicionales

Sé de los que dan la milla extra al trabajar en ti. Photo by Clique Images on Unsplash

Notificaciones y Spin

Añadiremos un par de cosas 😉

Vamos a usar una librería llamada NotifyJS de https://notifyjs.jpillora.com/ la cual nos permitirá tener unas notificaciones agradables luego de hacer una inserción, actualización o borrado de datos.

También utilizaremos un spin, es decir una ruedita que gira mientras nuestra aplicación web se encuentra procesando alguna petición.

Descargar notify.min.js y añadirlo en la carpeta wwwroot > js

Actualizar _Layout.cshtml para añadir tanto la referencia del script como también el Spin.

    @*SPIN*@
    <div class="loaderbody" id="loaderbody">
        <div class="loader"></div>
    </div>

    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2022 - MisGastos - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
        </div>
    </footer>
    <script src="~/lib/jquery/dist/jquery.min.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>
    <script src="~/js/notify.min.js"></script>
    @await RenderSectionAsync("Scripts", required: false)

Actualizar site.css de la carpeta wwwroot > css y añadir las siguientes líneas:

/* CUSTOM CSS
---------------------------------------------------*/
a.btn:hover {
    cursor: pointer !important;
}

/*loader*/
.loaderbody {
    width: 100%;
    height: 100%;
    left: 0px;
    top: 0px;
    position: absolute;
    background-color: rgba(128,128,128,0.2);
    z-index: 2147483647;
}

.loader {
    border: 16px solid #f3f3f3; /* Light grey */
    border-top: 16px solid #3498db; /* Blue */
    border-radius: 50%;
    width: 80px;
    height: 80px;
    animation: spin 2s linear infinite;
    position: fixed;
    top: 40%;
    left: 47%;
}

@keyframes spin {
    0% {
        transform: rotate(0deg);
    }

    100% {
        transform: rotate(360deg);
    }
}

.hide {
    display: none;
}

Añadir una función en site.js y actualizar las funciones jQueryAjaxPost y jQueryAjaxDelete con las notificaciones:

$(function () {
    $("#loaderbody").addClass('hide');

    $(document).bind('ajaxStart', function () {
        $("#loaderbody").removeClass('hide');
    }).bind('ajaxStop', function () {
        $("#loaderbody").addClass('hide');
    });
});

function showModal(url, title) {
    $.ajax(
        {
            type: "GET",
            url: url,
            success: function (response) {
                $("#form-modal .modal-body").html(response);
                $("#form-modal .modal-title").html(title);
                $("#form-modal").modal('show');
            }
        }
    )
}

function jQueryAjaxPost(form) {
    try {
        $.ajax({
            type: 'POST',
            url: form.action,
            data: new FormData(form),
            contentType: false,
            processData: false,
            success: function (response) {
                if (response.isValid) {
                    $("#view-all").html(response.html);
                    $("#form-modal .modal-body").html('');
                    $("#form-modal .modal-title").html('');
                    $("#form-modal").modal('hide');

                    //notify notification
                    $.notify('Datos registrados', { globalPosition: 'top-center', className: 'success' });
                }
                else {
                    $("#form-modal .modal-body").html(response.html);
                }
            },
            error: function (error) {
                console.log(error)
            }
        })
    }
    catch (e) {
        console.log(e);
    }
    return false;
}

function jQueryAjaxDelete(form) {
    if (confirm('¿Está seguro de eliminar el registro?')) {
        try {
            $.ajax({
                type: 'DELETE',
                url: form.action,
                data: new FormData(form),
                contentType: false,
                processData: false,
                success: function (response) {
                    $("#view-all").html(response.html);
                    //notify notification
                    $.notify('Registro eliminado', { globalPosition: 'top-center', className: 'success' });
                },
                error: function (error) {
                    console.log(error)
                }
            })
        }
        catch (ex) {
            console.log(ex);
        }
        return false;
    }
    return false;
}

Ahora toca probar cómo va quedando la aplicación, para esto es recomendable que hagas una compilacion y además refresques el navegador con CTRL + F5 ya que debido al caché algunos cambios a veces suelen no verse reflejados y el developer se vuelve loco sin saber por qué 😁 me ha pasado...

Probamos añadiendo un registro y vemos que funcionan las notificaciones, buena crack!

Y al eliminar un registro también

Seguridad mediante Filtros

Si navegamos a la dirección /Gastos/AddOrEdit notamos cómo sí nos permite ingresar a esta vista, que se supone sólo debería cargarse desde un modal, vamos a solucionar esto.

Es una mala práctica de seguridad que nuestras rutas queden expuestas de esa manera, ya que se supone que el usuario no puede ingresar a esa dirección directamente sin modal y por fuera de la vista Index.

Esto se soluciona con Filtros.

Creamos la clase NoDirectAccessAttribute dentro de la carpeta Helpers

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Filters;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace MisGastos.Helpers
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
    public class NoDirectAccessAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            if (filterContext.HttpContext.Request.GetTypedHeaders().Referer == null ||
                filterContext.HttpContext.Request.GetTypedHeaders().Host.Host.ToString() !=
                    filterContext.HttpContext.Request.GetTypedHeaders().Referer.Host.ToString())
            {
                filterContext.HttpContext.Response.Redirect("/");
            }
        }
    }
}

Finalmente añadimos el filtro a modo de anotación en el método AddOrEdit de GastosController:

        [NoDirectAccess]
        [HttpGet]
        public async Task<ActionResult> AddOrEdit(int id = 0)
        {
            if (id == 0)
                return View(new Gasto() { Fecha = DateTime.Now });

            var gasto = await context.Gastos.FirstOrDefaultAsync(x => x.Id == id);
            if (gasto == null)
                return NotFound();

            return View(gasto);
        }

Ahora ya no deberías poder navegar directamente a la ruta /Gastos/AddOrEdit sino que serás redirigido al inicio.

Al culminar todo tu explorador de soluciones tiene esta estructura:

Pues eso es todo crack, ya lo tienes! Una aplicación web totalmente funcional con CRUD y una buena pinta, pero lo más importante son todos los conceptos que repasamos, practícala y añádele más cosas 😊

Usa todas las herramientas que tienes a tu alcance, Photo by Yohan Cho on Unsplash

En conclusión

Ahora ya podrás poner en práctica lo aprendido en la teoría 🥳

Esta es la mejor forma de aprender, haciendo cosas, no hay más ciencia.

La segunda mejor forma de aprender es enseñando a otros, así que enséñalo a otros y comparte esta entrada crack!

Todo el proyecto está disponible en mi Github, en el siguiente enlace https://github.com/GeaSmart/MisGastos

Hey crack! Si esta entrada te ha gustado tanto como a mi, compártela! 😉

Deja una respuesta

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