Ir al contenido principal

Creación de una aplicación web sencilla

Después de conocer los fundamentos teóricos de ASP.NET Core, es momento de poner en práctica estos conocimientos creando tu primera aplicación web funcional. En este artículo desarrollaremos paso a paso una aplicación web sencilla que incluirá las funcionalidades básicas que encontrarás en la mayoría de proyectos web: páginas dinámicas, formularios, navegación entre páginas y manejo de datos. Esta experiencia práctica te permitirá comprender cómo se conectan todos los conceptos de ASP.NET Core en un proyecto real.

Crearemos una aplicación de gestión de tareas personales que permitirá a los usuarios añadir, ver, editar y eliminar tareas. A través de este proyecto práctico, aprenderás a estructurar un proyecto ASP.NET Core, crear controladores y vistas, manejar formularios, trabajar con modelos de datos en memoria, y implementar navegación entre páginas. Al finalizar, tendrás una aplicación web completamente funcional que podrás ejecutar en tu navegador.

Configuración del proyecto

Creación del proyecto

Para comenzar, crearemos un nuevo proyecto ASP.NET Core desde la línea de comandos o desde Visual Studio:

# Crear el proyecto usando la CLI de .NET
dotnet new mvc -n GestorTareas
cd GestorTareas

# Restaurar paquetes y ejecutar
dotnet restore
dotnet run

Estructura del proyecto

Un proyecto MVC de ASP.NET Core tiene una estructura predefinida que organiza el código de manera lógica:

Carpeta/Archivo Propósito
Controllers/ Contiene los controladores que manejan las peticiones
Views/ Contiene las vistas (páginas HTML con Razor)
Models/ Contiene las clases que representan los datos
wwwroot/ Archivos estáticos (CSS, JavaScript, imágenes)
Program.cs Punto de entrada de la aplicación
appsettings.json Archivo de configuración

Creación del modelo de datos

Comenzaremos definiendo el modelo de datos para nuestras tareas:

// Models/Tarea.cs
using System.ComponentModel.DataAnnotations;

namespace GestorTareas.Models
{
    public class Tarea
    {
        public int Id { get; set; }
        
        [Required(ErrorMessage = "El título es obligatorio")]
        [StringLength(100, ErrorMessage = "El título no puede exceder 100 caracteres")]
        public string Titulo { get; set; }
        
        [StringLength(500, ErrorMessage = "La descripción no puede exceder 500 caracteres")]
        public string Descripcion { get; set; }
        
        [Display(Name = "Fecha de creación")]
        public DateTime FechaCreacion { get; set; }
        
        [Display(Name = "Fecha límite")]
        public DateTime? FechaLimite { get; set; }
        
        [Display(Name = "Completada")]
        public bool EstaCompletada { get; set; }
        
        [Display(Name = "Prioridad")]
        public PrioridadTarea Prioridad { get; set; }
    }
    
    public enum PrioridadTarea
    {
        Baja = 1,
        Media = 2,
        Alta = 3
    }
}

Servicio para manejo de datos

Crearemos un servicio que simule una base de datos en memoria para gestionar nuestras tareas:

// Services/ITareaService.cs
namespace GestorTareas.Services
{
    public interface ITareaService
    {
        Task<List<Tarea>> ObtenerTodasAsync();
        Task<Tarea> ObtenerPorIdAsync(int id);
        Task<Tarea> CrearAsync(Tarea tarea);
        Task<bool> ActualizarAsync(Tarea tarea);
        Task<bool> EliminarAsync(int id);
        Task<int> ContarPendientesAsync();
    }
}

// Services/TareaService.cs
namespace GestorTareas.Services
{
    public class TareaService : ITareaService
    {
        private static List<Tarea> _tareas = new List<Tarea>();
        private static int _siguienteId = 1;
        
        public TareaService()
        {
            // Datos de ejemplo si la lista está vacía
            if (!_tareas.Any())
            {
                InicializarDatosEjemplo();
            }
        }
        
        public Task<List<Tarea>> ObtenerTodasAsync()
        {
            return Task.FromResult(_tareas.OrderBy(t => t.EstaCompletada)
                                          .ThenByDescending(t => t.Prioridad)
                                          .ThenBy(t => t.FechaLimite)
                                          .ToList());
        }
        
        public Task<Tarea> ObtenerPorIdAsync(int id)
        {
            var tarea = _tareas.FirstOrDefault(t => t.Id == id);
            return Task.FromResult(tarea);
        }
        
        public Task<Tarea> CrearAsync(Tarea tarea)
        {
            tarea.Id = _siguienteId++;
            tarea.FechaCreacion = DateTime.Now;
            _tareas.Add(tarea);
            return Task.FromResult(tarea);
        }
        
        public Task<bool> ActualizarAsync(Tarea tarea)
        {
            var tareaExistente = _tareas.FirstOrDefault(t => t.Id == tarea.Id);
            if (tareaExistente == null)
                return Task.FromResult(false);
                
            tareaExistente.Titulo = tarea.Titulo;
            tareaExistente.Descripcion = tarea.Descripcion;
            tareaExistente.FechaLimite = tarea.FechaLimite;
            tareaExistente.EstaCompletada = tarea.EstaCompletada;
            tareaExistente.Prioridad = tarea.Prioridad;
            
            return Task.FromResult(true);
        }
        
        public Task<bool> EliminarAsync(int id)
        {
            var tarea = _tareas.FirstOrDefault(t => t.Id == id);
            if (tarea == null)
                return Task.FromResult(false);
                
            _tareas.Remove(tarea);
            return Task.FromResult(true);
        }
        
        public Task<int> ContarPendientesAsync()
        {
            var pendientes = _tareas.Count(t => !t.EstaCompletada);
            return Task.FromResult(pendientes);
        }
        
        private void InicializarDatosEjemplo()
        {
            _tareas.AddRange(new[]
            {
                new Tarea
                {
                    Id = _siguienteId++,
                    Titulo = "Completar tutorial de ASP.NET Core",
                    Descripcion = "Finalizar todos los ejercicios del tutorial",
                    FechaCreacion = DateTime.Now.AddDays(-2),
                    FechaLimite = DateTime.Now.AddDays(3),
                    Prioridad = PrioridadTarea.Alta,
                    EstaCompletada = false
                },
                new Tarea
                {
                    Id = _siguienteId++,
                    Titulo = "Revisar emails",
                    Descripcion = "Responder emails pendientes de trabajo",
                    FechaCreacion = DateTime.Now.AddDays(-1),
                    Prioridad = PrioridadTarea.Media,
                    EstaCompletada = true
                }
            });
        }
    }
}

Controlador de tareas

El controlador manejará todas las operaciones CRUD (Crear, Leer, Actualizar, Eliminar) de nuestras tareas:

// Controllers/TareasController.cs
using Microsoft.AspNetCore.Mvc;
using GestorTareas.Models;
using GestorTareas.Services;

namespace GestorTareas.Controllers
{
    public class TareasController : Controller
    {
        private readonly ITareaService _tareaService;
        
        public TareasController(ITareaService tareaService)
        {
            _tareaService = tareaService;
        }
        
        // GET: Tareas
        public async Task<IActionResult> Index()
        {
            var tareas = await _tareaService.ObtenerTodasAsync();
            ViewBag.TareasPendientes = await _tareaService.ContarPendientesAsync();
            return View(tareas);
        }
        
        // GET: Tareas/Detalle/5
        public async Task<IActionResult> Detalle(int id)
        {
            var tarea = await _tareaService.ObtenerPorIdAsync(id);
            if (tarea == null)
            {
                return NotFound();
            }
            
            return View(tarea);
        }
        
        // GET: Tareas/Crear
        public IActionResult Crear()
        {
            var tarea = new Tarea
            {
                FechaLimite = DateTime.Now.AddDays(7) // Fecha por defecto
            };
            
            return View(tarea);
        }
        
        // POST: Tareas/Crear
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Crear(Tarea tarea)
        {
            if (ModelState.IsValid)
            {
                await _tareaService.CrearAsync(tarea);
                TempData["Mensaje"] = "Tarea creada exitosamente";
                return RedirectToAction(nameof(Index));
            }
            
            return View(tarea);
        }
        
        // GET: Tareas/Editar/5
        public async Task<IActionResult> Editar(int id)
        {
            var tarea = await _tareaService.ObtenerPorIdAsync(id);
            if (tarea == null)
            {
                return NotFound();
            }
            
            return View(tarea);
        }
        
        // POST: Tareas/Editar/5
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Editar(int id, Tarea tarea)
        {
            if (id != tarea.Id)
            {
                return BadRequest();
            }
            
            if (ModelState.IsValid)
            {
                var actualizado = await _tareaService.ActualizarAsync(tarea);
                if (actualizado)
                {
                    TempData["Mensaje"] = "Tarea actualizada exitosamente";
                    return RedirectToAction(nameof(Index));
                }
                
                return NotFound();
            }
            
            return View(tarea);
        }
        
        // POST: Tareas/Eliminar/5
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Eliminar(int id)
        {
            var eliminado = await _tareaService.EliminarAsync(id);
            if (eliminado)
            {
                TempData["Mensaje"] = "Tarea eliminada exitosamente";
            }
            else
            {
                TempData["Error"] = "No se pudo eliminar la tarea";
            }
            
            return RedirectToAction(nameof(Index));
        }
        
        // POST: Tareas/CambiarEstado/5
        [HttpPost]
        public async Task<IActionResult> CambiarEstado(int id)
        {
            var tarea = await _tareaService.ObtenerPorIdAsync(id);
            if (tarea != null)
            {
                tarea.EstaCompletada = !tarea.EstaCompletada;
                await _tareaService.ActualizarAsync(tarea);
            }
            
            return RedirectToAction(nameof(Index));
        }
    }
}

Creación de las vistas

Vista principal (Index)

@* Views/Tareas/Index.cshtml *@
@model List<Tarea>

@{
    ViewData["Title"] = "Mis Tareas";
}

<div class="container mt-4">
    <div class="row">
        <div class="col-md-8">
            <h1>@ViewData["Title"]</h1>
        </div>
        <div class="col-md-4 text-end">
            <a asp-action="Crear" class="btn btn-primary">
                <i class="fas fa-plus"></i> Nueva Tarea
            </a>
        </div>
    </div>

    @if (ViewBag.TareasPendientes > 0)
    {
        <div class="alert alert-info">
            <i class="fas fa-info-circle"></i>
            Tienes <strong>@ViewBag.TareasPendientes</strong> tareas pendientes.
        </div>
    }

    @if (TempData["Mensaje"] != null)
    {
        <div class="alert alert-success alert-dismissible fade show">
            @TempData["Mensaje"]
            <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
        </div>
    }

    @if (TempData["Error"] != null)
    {
        <div class="alert alert-danger alert-dismissible fade show">
            @TempData["Error"]
            <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
        </div>
    }

    <div class="row">
        @if (Model.Any())
        {
            @foreach (var tarea in Model)
            {
                <div class="col-md-6 col-lg-4 mb-3">
                    <div class="card h-100 @(tarea.EstaCompletada ? "border-success" : "")">
                        <div class="card-header d-flex justify-content-between align-items-center">
                            <span class="badge bg-@(tarea.Prioridad == PrioridadTarea.Alta ? "danger" : 
                                                   tarea.Prioridad == PrioridadTarea.Media ? "warning" : "secondary")">
                                @tarea.Prioridad
                            </span>
                            
                            @if (tarea.EstaCompletada)
                            {
                                <span class="badge bg-success">
                                    <i class="fas fa-check"></i> Completada
                                </span>
                            }
                        </div>
                        
                        <div class="card-body">
                            <h5 class="card-title @(tarea.EstaCompletada ? "text-decoration-line-through text-muted" : "")">
                                @tarea.Titulo
                            </h5>
                            
                            @if (!string.IsNullOrEmpty(tarea.Descripcion))
                            {
                                <p class="card-text text-muted">
                                    @(tarea.Descripcion.Length > 100 ? 
                                      tarea.Descripcion.Substring(0, 100) + "..." : 
                                      tarea.Descripcion)
                                </p>
                            }
                            
                            @if (tarea.FechaLimite.HasValue)
                            {
                                var claseColor = tarea.FechaLimite.Value < DateTime.Now && !tarea.EstaCompletada ? "text-danger" : "text-muted";
                                <small class="@claseColor">
                                    <i class="fas fa-calendar"></i>
                                    Vence: @tarea.FechaLimite.Value.ToString("dd/MM/yyyy")
                                </small>
                            }
                        </div>
                        
                        <div class="card-footer">
                            <div class="btn-group w-100" role="group">
                                <a asp-action="Detalle" asp-route-id="@tarea.Id" 
                                   class="btn btn-outline-primary btn-sm">
                                    <i class="fas fa-eye"></i>
                                </a>
                                
                                <a asp-action="Editar" asp-route-id="@tarea.Id" 
                                   class="btn btn-outline-warning btn-sm">
                                    <i class="fas fa-edit"></i>
                                </a>
                                
                                <form asp-action="CambiarEstado" asp-route-id="@tarea.Id" 
                                      method="post" class="d-inline">
                                    <button type="submit" class="btn btn-outline-@(tarea.EstaCompletada ? "secondary" : "success") btn-sm">
                                        <i class="fas fa-@(tarea.EstaCompletada ? "undo" : "check")"></i>
                                    </button>
                                </form>
                                
                                <form asp-action="Eliminar" asp-route-id="@tarea.Id" 
                                      method="post" class="d-inline"
                                      onsubmit="return confirm('¿Estás seguro de que quieres eliminar esta tarea?')">
                                    <button type="submit" class="btn btn-outline-danger btn-sm">
                                        <i class="fas fa-trash"></i>
                                    </button>
                                </form>
                            </div>
                        </div>
                    </div>
                </div>
            }
        }
        else
        {
            <div class="col-12">
                <div class="text-center py-5">
                    <i class="fas fa-tasks fa-3x text-muted mb-3"></i>
                    <h3 class="text-muted">No hay tareas</h3>
                    <p class="text-muted">¡Crea tu primera tarea para comenzar!</p>
                    <a asp-action="Crear" class="btn btn-primary">
                        <i class="fas fa-plus"></i> Crear Primera Tarea
                    </a>
                </div>
            </div>
        }
    </div>
</div>

Formulario para crear/editar tareas

@* Views/Tareas/Crear.cshtml *@
@model Tarea

@{
    ViewData["Title"] = "Nueva Tarea";
}

<div class="container mt-4">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">
                    <h2><i class="fas fa-plus"></i> @ViewData["Title"]</h2>
                </div>
                
                <div class="card-body">
                    <form asp-action="Crear" method="post">
                        <div asp-validation-summary="ModelOnly" class="alert alert-danger"></div>
                        
                        <div class="mb-3">
                            <label asp-for="Titulo" class="form-label"></label>
                            <input asp-for="Titulo" class="form-control" placeholder="Ej: Completar informe mensual">
                            <span asp-validation-for="Titulo" class="text-danger"></span>
                        </div>
                        
                        <div class="mb-3">
                            <label asp-for="Descripcion" class="form-label"></label>
                            <textarea asp-for="Descripcion" class="form-control" rows="3" 
                                      placeholder="Describe los detalles de la tarea..."></textarea>
                            <span asp-validation-for="Descripcion" class="text-danger"></span>
                        </div>
                        
                        <div class="row">
                            <div class="col-md-6">
                                <div class="mb-3">
                                    <label asp-for="FechaLimite" class="form-label"></label>
                                    <input asp-for="FechaLimite" type="date" class="form-control">
                                    <span asp-validation-for="FechaLimite" class="text-danger"></span>
                                </div>
                            </div>
                            
                            <div class="col-md-6">
                                <div class="mb-3">
                                    <label asp-for="Prioridad" class="form-label"></label>
                                    <select asp-for="Prioridad" class="form-select" asp-items="Html.GetEnumSelectList<PrioridadTarea>()">
                                        <option value="">Selecciona la prioridad</option>
                                    </select>
                                    <span asp-validation-for="Prioridad" class="text-danger"></span>
                                </div>
                            </div>
                        </div>
                        
                        <div class="mb-3 form-check">
                            <input asp-for="EstaCompletada" type="checkbox" class="form-check-input">
                            <label asp-for="EstaCompletada" class="form-check-label"></label>
                        </div>
                        
                        <div class="d-grid gap-2 d-md-flex justify-content-md-end">
                            <a asp-action="Index" class="btn btn-secondary">
                                <i class="fas fa-arrow-left"></i> Cancelar
                            </a>
                            <button type="submit" class="btn btn-primary">
                                <i class="fas fa-save"></i> Guardar Tarea
                            </button>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Configuración de la aplicación

Finalmente, debemos configurar la aplicación en el archivo Program.cs:

// Program.cs
using GestorTareas.Services;

var builder = WebApplication.CreateBuilder(args);

// Agregar servicios
builder.Services.AddControllersWithViews();
builder.Services.AddScoped<ITareaService, TareaService>();

var app = builder.Build();

// Configurar pipeline
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();

// Configurar rutas
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Tareas}/{action=Index}/{id?}");

app.Run();

Ejecución de la aplicación

Para ejecutar la aplicación, utiliza el siguiente comando:

dotnet run

La aplicación estará disponible en https://localhost:5001 o http://localhost:5000.

Resumen

Has creado exitosamente tu primera aplicación web completa con ASP.NET Core. Esta aplicación de gestión de tareas incluye todas las funcionalidades básicas de una aplicación web moderna: creación, lectura, actualización y eliminación de datos (CRUD), formularios con validación, navegación entre páginas, y una interfaz de usuario responsiva. A través de este proyecto práctico, has aprendido a estructurar un proyecto MVC, crear modelos con validaciones, implementar servicios para el manejo de datos, desarrollar controladores que manejen las diferentes operaciones, y crear vistas dinámicas con Razor. Esta experiencia te proporciona una base sólida para desarrollar aplicaciones web más complejas y te prepara para integrar bases de datos reales, autenticación de usuarios, y otras funcionalidades avanzadas en futuros proyectos.