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! 馃槑
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">×</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 馃槈
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
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">
© 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 馃槉
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! 馃槈