Curso visto en NetCore 3 y hecho en NetCore6
Curso: Desarrollo Web en ASP.NET CORE 5 (2021)
Sistema de Control de Ingresos y Gastos - Full-Stack Web. Tiene conexión a la BD manual/por archivo.
Se pueden ver, crea datos de tipo Ingreso o Gasto por medio de una lista desplegable con codigo quemado. Se reemplaza texto para mostrar otro texto en la vista. IN = Ingreso, GA = Gasto. Se creo filtro para buscar por año y mes. Se usan plantillas Boostrap. Se agrega un ForeignKey entre dos tablas.
- ASP.NET Core: 6.0
- Web Application (MVC)
- Bootstrap: v5.1.0
- SQL Server: 2019
- Entity Framework Core: 7.0.2
Aplicacion web de ASP.NET MVC: C#/Windows/Web, desde codigo se agrega el StringConnection a la BD y se crea la BD para el proyecto.
- falta: no se pudo alimentar el dropdownlist desde una tabla
- Interacciones Front-End y Back-End
- Data Model con Entity Framework Core
- Traer y Desplegar Datos
- Forms y Validación de Datos
- Ajuste de Código y Servicios de Datos
- Back-End y Front-End Debugging
- AspNet Core
Archivos estaticos en: wwwroot/
El primer controlador y vista que se ejecutan son: Version de Boostrap: ...\IngresosGastos\wwwroot\lib\bootstrap\dist\css\bootstrap.css
public class Program
{
...
app.MapControllerRoute()
...
}
- Microsoft.EntityFrameworkCore
- Microsoft.EntityFrameworkCore.SqlServer
- Microsoft.EntityFrameworkCore.Design
- Microsoft.EntityFrameworkCore.Tools (convierte modelos a tablas)
Models/Categoria.cs
public class Categoria
{
// estas anotaciones sirven para cuando se cree la BD y tabla Categoria se usen como campos de la tabla
[Key]
public int Id { get; set; }
[Required]
[MaxLength(120)]
[Display(Name ="Nombre Categoria")]
public string NombreCategoria { get; set; }
[Required]
[MaxLength(2)]
[Display(Name = "Tipo")]
public string Tipo { get; set; } // IN: Ingreso, GA: Gasto
[Required]
public bool Estado { get; set; }
}
Models/NombreCategoria.cs
public class NombreCategoria
{
// para mostrar el DropDownList: Ingreso, Gasto
[Key]
public int Id { get; set; }
[Required]
[MaxLength(10)]
[Display(Name = "Tipo")]
public string Tipo { get; set; } // Ingreso o Gasto
}
Conexión BD: appsettings.json
Se creara una DB llamada: IngresoGastosDB Se creara la tabla: Categorias Se creara la tabla: NombreCategoria
Datos BD:
Servidor: 192.168.0.20\SERVER\SQLEXPRESS,1433
DB: IngresoGastosDB
user id: bduserX
password: admin1234
Plantilla standar security:
Server=myServerName\myInstanceName;Database=myDataBase;User Id=myUsername;Password=myPassword;
Plantilla trusted connection:
Server=myServerAddress;Database=myDataBase;Trusted_Connection=True;
ConnectionStrings usado:
"ConnectionStrings": {
"ConexionBD": "Server=192.168.0.20\\SERVER\\SQLEXPRESS,1433;Database=IngresoGastosDB;User Id=bduserX;Password=admin1234;Trust Server Certificate=true;"
},
Agregar carpeta Data/ y clase: Data/AppDBContext.cs Esto solo se hace en MVC 6 porque MVC 3 tiene el StartUp.cs
public class AppDBContext: DbContext
{
public AppDBContext(DbContextOptions<AppDBContext> options): base(options)
{
}
// Para inyeccion de dependencias
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.Build();
var connectionString = configuration.GetConnectionString("ConexionBD");
optionsBuilder.UseSqlServer(connectionString);
}
public DbSet<Categoria> Categorias { get; set; }
public DbSet<NombreCategoria> NombreCategoria { get; }
}
Program.cs En la ultima linea de la seccion // Add services to the container. Esto solo se hace en MVC 6 porque MVC 3 tiene el StartUp.cs. Agregar:
var connectionString = builder.Configuration.GetConnectionString("ConexionBD");
builder.Services.AddDbContext<AppDBContext>(x => x.UseSqlServer(connectionString));
Hacer migracion de datos models a bd:
En visual > Herramientas > Administrador de paquetes Nuget > Consola:
add-migration MigracionCategoria1
update-database
Se creará la BD especificada y las tablas Categorias, NombreCategoria
Agregar datos a la tabla:
INSERT INTO NombreCategoria (Nombre) VALUES ('Ingreso');
INSERT INTO NombreCategoria (Nombre) VALUES ('Gasto');
Crear Models/CategoriasController.cs Controlador MVC con vistas Entity Framework
- Clase: Categoria (de controllers/models)
- Categoria: AppDBContext (de data/in)
- Generar vistas, referencias, usar pagina de diseño
- diseño: views/shared/_Layout.cshtml
- Nombre: CategoriasController
- Vista index creada: Views/Categorias/Index.cshtml
Agregar en CategoriasController Views/Shared/_Layout vista de CategoriasController
...
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
<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="Categorias" asp-action="Index">Categorias</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</li>
</ul>
</div>
Para relacionar el ingreso / gasto, Agregar Fecha y valor a los Ingresos/Gastos ya creados. Models/IngresoGasto
public class IngresoGasto
{
[Key]
public int Id { get; set; }
[Required]
public int CategoriaId { get; set; }
[ForeignKey("CategoriaId")]
public Categoria Categoria { get; set; } // Categoria de la clase/tabla Categoria
[Required]
[Display(Name ="Fecha")]
public DateTime Fecha { get; set; } //cuando se ingreso el ingreso o gasto
[Required]
[Range(1,100000)] // para que siempre sea mayor que 1
[DisplayFormat(DataFormatString ="{0:C}")]
[Display(Name ="Valor")]
public double Valor { get; set; }
}
Ahora se agregar al contexto en: Data/AppDBContext
public DbSet<IngresoGasto> IngresoGasto { get; set; }
Hacer migracion de datos models a bd con nombre MigracionCategoria2:
En visual > Herramientas > Administrador de paquetes Nuget > Consola:
add-migration MigracionCategoria2
update-database
Agregar Controlador: Controllers/IngresoGastos
Crear Models/IngresoGastos.cs Controlador MVC con vistas Entity Framework
- Clase: IngresoGasto (de controllers/models)
- Categoria: AppDBContext (de data/in)
- Generar vistas, referencias, usar pagina de diseño
- diseño: views/shared/_Layout.cshtml
- Nombre: IngresoGastosController
- Vista index creada: Views/IngresoGastos/Index.cshtml
Agregar seccion en la vista: views/shared/_Layout
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="IngresoGastos" asp-action="Index">Ingreso-Gastos</a>
</li>
Views/Categorias/Index
Reemplaza texto IN, GA por Ingreso, Gasto. Seccion de @foreach
<td>
@* @Html.DisplayFor(modelItem => item.Tipo) *@
@if (item.Tipo=="IN")
{
<p class="btn btn-outline-success">Ingreso</p>
}
else
{
<p class="btn btn-outline-danger">Gasto</p>
}
</td>
Views/Categorias/Index
Reemplaza casilla true/false Estado por Activo, Inactivo. Seccion de @foreach
<td>
@* @Html.DisplayFor(modelItem => item.Estado) *@
@if (item.Estado)
{
<p class="btn btn-outline-primary">Activo</p>
}
else
{
<p class="btn btn-outline-info">Inactivo</p>
}
</td>
Filtrar Categorias en IngresoGasto para mostrar solo Categorias Activas:
//GET de Create() del controller, agregar .Where(var => var.Estado==true)
en la conexion a la BD de Categorias.
// GET: IngresoGastos/Create
public IActionResult Create()
{
//filtrar solo por categorias Activas
ViewData["CategoriaId"] = new SelectList(_context.Categorias.Where(var => var.Estado==true), "Id", "NombreCategoria");
//ViewData["CategoriaId"] = new SelectList(_context.Categorias, "Id", "NombreCategoria");
return View();
}
Views/Categorias/Create
Reemplaza texto IN, GA por una lista desplegable con Ingreso, Gasto con codigo quemado. dentro de <form asp-action="Create">
de Tipo y Estado. Quitar <input>
y agregar <select>
<div class="form-group">
<label asp-for="Tipo" class="control-label"></label>
@*<input asp-for="Tipo" class="form-control" />*@
<select asp-for="Tipo" class="form-control">
<option value="IN">Ingreso</option>
<option value="GA">Gasto</option>
</select>
<span asp-validation-for="Tipo" class="text-danger"></span>
</div>
Views/Categorias/Create
Quitar form-check
. En <label>
reemplazar form-check-label
por control-label
, reemplazar todo el <input>
por Estado, luego del final del </label>
agregar <select>
<div class="form-group">
<label class="control-label">Estado</label>
<select asp-for="Estado" class="form-control">
<option value=true>Activo</option>
<option value=false>Inactivo</option>
</select>
</div>
Views/Categorias/Edit
Reemplaza texto IN, GA por una lista desplegable con Ingreso, Gasto con codigo quemado. dentro de <form asp-action="Create">
de Tipo y Estado. Quitar <input>
y agregar <select>
<div class="form-group">
<label asp-for="Tipo" class="control-label"></label>
@*<input asp-for="Tipo" class="form-control" />*@
<select asp-for="Tipo" class="form-control">
<option value="IN">Ingreso</option>
<option value="GA">Gasto</option>
</select>
<span asp-validation-for="Tipo" class="text-danger"></span>
</div>
Views/Categorias/Edit
Quitar form-check
en <label>
reemplazar form-check-label
por control-label
, reemplazar todo el <input>
por Estado, luego del </label>
agregar <select>
<div class="form-group">
<label class="control-label">Estado</label>
<select asp-for="Estado" class="form-control">
<option value=true>Activo</option>
<option value=false>Inactivo</option>
</select>
</div>
Views/Categorias/Details
Reemplaza texto IN, GA por Ingreso, Gasto. Seccion de @foreach
<dd class = "col-sm-10">
@* @Html.DisplayFor(model => model.Tipo)*@
@if (Model.Tipo == "IN")
{
<p class="btn btn-outline-success">Ingreso</p>
}
else
{
<p class="btn btn-outline-danger">Gasto</p>
}
</dd>
Views/Categorias/Details
Reemplaza casilla true/false Estado por Activo, Inactivo. Seccion de @foreach
<dd class = "col-sm-10">
@*@Html.DisplayFor(model => model.Estado)*@
@if (Model.Estado)
{
<p class="btn btn-outline-primary">Activo</p>
}
else
{
<p class="btn btn-outline-info">Inactivo</p>
}
</dd>
En el controlador se modifica el metodo de la vista, en este caso Index()
y se agregan parametros (opcionales porque pueden ser null) que enviará la vista Index(int? mesV, int? anioV)
.
Se agregan los valores de los parametros a variables locales en back:
ViewData["mesV"] = mesV;
ViewData["anioV"] = anioV;
Nombre atributos de la vista:
name=mesV
name=anioV
NOTA: Ambos pares de variables deben llamarse igual en Back y en front
Luego se agrega la logica, cuando alguno de los datos es null retorna appDBContext que viene por defecto, en caso contrario se agrega el WHERE fecha=anioV AND mes=mesV
y se retorna appDBContext.
Controllers/IngresoGastos Mostrar resultados del Index por defecto o por mes y año segun lo seleccionado en la vista, si alguno de los valores son null restorna la vista por defecto (muestra los datos sin filtrar).
public async Task<IActionResult> Index(int? mesV, int? anioV)
{
ViewData["mesV"] = mesV;
ViewData["anioV"] = anioV;
if (mesV == null || anioV==null)
{
var appDBContextDefault = _context.IngresoGasto.Include(i => i.Categoria);
return View(await appDBContextDefault.ToListAsync());
}
else
{
var appDBContextBusqueda = _context.IngresoGasto.Include(i => i.Categoria).Where(i => i.Fecha.Month == mesV && i.Fecha.Year == anioV);
return View(await appDBContextBusqueda.ToListAsync());
}
}
Views/IngresoGastos/Index
Se agregan los input para mes y año. Se guarda el valor en name="anioV"
, mes="mesV"
y se envia su valor al back. El valor de las variables se cambia segun envie value="@ViewBag.mesV"
y value="@ViewBag.anioV"
.
<form class="row g-3" asp-action="index">
<div class="col-auto">
<label>Mes</label>
</div>
<div class="col-auto">
<input type="number" class="form-control text-right" name="mesV" min="1" max="12" value="@ViewBag.mesV" />
</div>
<div class="col-auto">
<label>Año</label>
</div>
<div class="col-auto">
<input type="number" class="form-control text-right" name="anioV" min="1" max="3000" value="@ViewBag.anioV" />
</div>
<div class="col-auto">
<input type="submit" value="Consultar" class="btn btn-primary" />
</div>
</form>
En Views/Categorias/Index agregar clases de Boostrap
Evitar que se autocompleten los campos input agregando autocomplete="off"
Agregar diseño en los inputos con etiquetas botones.
Agregar diseño de tabla:
- Encabezado Tablas:
- Cuerpo tablas:
- Texto: class="btn btn-primary"
- la tabla de la bd se define en AppDBContext y el controller lo usa como _context.
- los query de las tablas son: _context.IngresoGasto.{query}
- Select -> .Include(i=>i.categoria)
- Where -> .Where(i=>i.categoria.Estado==true)
- Enviar datos de back a front
- En el controller usar ViewData["NombreVar"] = .... y en la vista usarlo como "asp-for="CategoriaId" ... asp-items="ViewBag.NombreVar"
- Se pueden cambiar los atributes de un texto que se mnuestra en html colocandolo dentro de una etqiqueta
- Recrear el proyecto, eliminar Migration y ejecutar:
add-migration MigracionCategoria2
yupdate-database
Cuando agregaba un IngresoGasto la pagina no realizaba ninguna acción. Revisé el método de Create()
del controlador para revisar si habian fallos. Por alguna razón ModelState.IsValid
siempre era false.
Usar el modo Depuración y mirar el mensaje del ModelState. agregar:
var errors = ModelState.Values.SelectMany(v => v.Errors);
Quedaría así:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Id,CategoriaId,Fecha,Valor")] IngresoGasto ingresoGasto)
{
if (ModelState.IsValid)
{
_context.Add(ingresoGasto);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
var errors = ModelState.Values.SelectMany(v => v.Errors);
ViewData["CategoriaId"] = new SelectList(_context.Categorias, "Id", "NombreCategoria", ingresoGasto.CategoriaId);
return View(ingresoGasto);
}
Ver mensaje en:
errors>vista de resultados>[0]>ErrorMessage
Resultado:
ErrorMessage: "The Categoria field is required."
Solución Como el problema era con Categoria en el controlador de IngresoGasto coloque el parametro como opcional.
[ForeignKey("CategoriaId")]
public Categoria? Categoria { get; set; }
Basado en el curso: Desarrollo Web en ASP.NET CORE 5 (2021) Boostrap 5.1 Componentes