Search code examples
c#asp.net-coreasp.net-core-mvc

How can I create a controller that will handle a view model in Razor?


To give you some context I am currently learning how to create an ASP.NET Core MVC app, and I am having trouble figuring out how controllers actually work, and how EF handles relationships.

Right now I have these two tables:

public class Persona
{
    public int Id { get; set; }
    public string Nombre { get; set; }
    public string ApellidoPaterno { get; set; }
    public string ApellidoMaterno { get; set; }
    public string Email { get; set; }
    public string Telefono { get; set; }

    // Relationship
    public int? UsuarioId { get; set; }
    public Usuario? Usuario { get; set; }  
}

public class Usuario
{
    public int Id { get; set; }
    public string Username { get; set; }
    public string Password { get; set; }
    
    // Relacion
    public int? PersonaId { get; set; }
    [ForeignKey("PersonaId")]
    public Persona? Persona { get; set; }
}

These two tables have a one to one relationship to which I have created a view model that will be used in order to display and edit both of them at the same time.

This is the view model I have created:

public class PersonaUsuarioVista
{
        // Persona properties
        public string Nombre { get; set; }
        public string ApellidoPaterno { get; set; }
        public string ApellidoMaterno { get; set; }
        public string Email { get; set; }
        public string Telefono { get; set; }

        // Usuario properties
        public string Username { get; set; }
        public string Password { get; set; }
}

This is the view model I have used to correctly display and create new Personas and Usuarios.

But when It comes to the Edit. I am not sure why it isn't working.

The Get method works correctly, meaning it does display the data inside the input fields.

But once a change is made and saved... Nothing I am not sure why this is the case.

This is the Post method for the edit:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, UsuarioPersonaVista.PersonaUsuarioVista model)
{
    if (ModelState.IsValid)
    {
        var personaToUpdate = await _context.Personas.FindAsync(id);

        if (personaToUpdate == null)
        {
            return NotFound();
        }

        var usuarioToUpdate = await _context.Usuarios.FirstOrDefaultAsync(u => u.PersonaId == id);

        if (usuarioToUpdate == null)
        {
            return NotFound();
        }

        // Update Persona properties
        personaToUpdate.Nombre = model.Nombre;
        personaToUpdate.ApellidoPaterno = model.ApellidoPaterno;
        personaToUpdate.ApellidoMaterno = model.ApellidoMaterno;
        personaToUpdate.Email = model.Email;
        personaToUpdate.Telefono = model.Telefono;

        // Update Usuario properties
        usuarioToUpdate.Username = model.Username;

        // Save changes
        _context.Usuarios.Update(usuarioToUpdate);
        _context.Personas.Update(personaToUpdate);
        await _context.SaveChangesAsync();

        return RedirectToAction(nameof(Index));
    }

    return View(model); // Return the view with the model if the ModelState is invalid
}

And this is the Razor page itself:

<form asp-controller="Persona" asp-action="Edit" method="post">
    <div>
        <label for="Nombre">Nombre</label>
        <input type="text" asp-for="Nombre" />
        <span asp-validation-for="Nombre"></span>
    </div>
    <div>
        <label for="ApellidoPaterno">Apellido Paterno</label>
        <input type="text" asp-for="ApellidoPaterno" />
        <span asp-validation-for="ApellidoPaterno"></span>
    </div>
    <div>
        <label for="ApellidoMaterno">Apellido Materno</label>
        <input type="text" asp-for="ApellidoMaterno" />
        <span asp-validation-for="ApellidoMaterno"></span>
    </div>
    <div>
        <label for="Email">Email</label>
        <input type="text" asp-for="Email" />
        <span asp-validation-for="Email"></span>
    </div>
    <div>
        <label for="Telefono">Telefono</label>
        <input type="text" asp-for="Telefono" />
        <span asp-validation-for="Telefono"></span>
    </div>
    <div>
        <label for="Username">Username</label>
        <input type="text" asp-for="Username" />
        <span asp-validation-for="Username"></span>
    </div>
    <input type="submit" value="Editar">
</form>

Any help or guidance towards solving this problem or just understanding better what I did wrong is highly appreciated. Thank you very much!


Solution

  • Let's break this down and see how we can resolve the issue with your Edit method in your MVC .NET Core application.

    Understanding the Controllers and EF Core Relationships First, let's make sure we understand how Entity Framework (EF) handles relationships and how controllers work in this context.

    Entity Framework Relationships In EF Core, relationships between entities are defined using navigation properties and foreign keys. In your case, the Persona and Usuario classes have a one-to-one relationship:

    Persona has a foreign key to Usuario (UsuarioId).

    Usuario has a foreign key to Persona (PersonaId).

    View Model The PersonaUsuarioVista view model combines properties from both Persona and Usuario for display and editing purposes.

    Issue in Edit Method You mentioned that while the Get method works correctly, the Post method does not update the records as expected. Let's break down your Post method:

    csharp
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Edit(int id, UsuarioPersonaVista.PersonaUsuarioVista model)
    {
        if (ModelState.IsValid)
        {
            var personaToUpdate = await _context.Personas.FindAsync(id);
            if (personaToUpdate == null)
            {
                return NotFound();
            }
    
            var usuarioToUpdate = await _context.Usuarios.FirstOrDefaultAsync(u => u.PersonaId == id);
            if (usuarioToUpdate == null)
            {
                return NotFound();
            }
    
            // Update Persona properties
            personaToUpdate.Nombre = model.Nombre;
            personaToUpdate.ApellidoPaterno = model.ApellidoPaterno;
            personaToUpdate.ApellidoMaterno = model.ApellidoMaterno;
            personaToUpdate.Email = model.Email;
            personaToUpdate.Telefono = model.Telefono;
    
            // Update Usuario properties
            usuarioToUpdate.Username = model.Username;
    
            // Save changes
            _context.Usuarios.Update(usuarioToUpdate);
            _context.Personas.Update(personaToUpdate);
            await _context.SaveChangesAsync();
    
            return RedirectToAction(nameof(Index));
        }
    
        return View(model);
    }
    

    Potential Issues Foreign Key Relationship:

    Ensure that the relationships between Persona and Usuario are properly configured.

    When updating related entities, make sure that the foreign key values are correctly set.

    Tracking Changes:

    EF Core tracks changes to entities. Ensure that the entities retrieved (personaToUpdate and usuarioToUpdate) are the same instances being updated and saved.

    Password Update:

    If you're not updating the Password, ensure it is not cleared out during the update.

    Suggested Changes Include Password in the ViewModel (if needed):

    csharp
    public class PersonaUsuarioVista
    {
        // Persona properties
        public string Nombre { get; set; }
        public string ApellidoPaterno { get; set; }
        public string ApellidoMaterno { get; set; }
        public string Email { get; set; }
        public string Telefono { get; set; }
    
        // Usuario properties
        public string Username { get; set; }
        public string Password { get; set; } // Include this if the password needs updating
    }
    Ensure Changes are Tracked Properly:
    
    csharp
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Edit(int id, UsuarioPersonaVista.PersonaUsuarioVista model)
    {
        if (ModelState.IsValid)
        {
            var personaToUpdate = await _context.Personas.FindAsync(id);
            if (personaToUpdate == null)
            {
                return NotFound();
            }
    
            var usuarioToUpdate = await _context.Usuarios.FirstOrDefaultAsync(u => u.PersonaId == id);
            if (usuarioToUpdate == null)
            {
                return NotFound();
            }
    
            // Update Persona properties
            personaToUpdate.Nombre = model.Nombre;
            personaToUpdate.ApellidoPaterno = model.ApellidoPaterno;
            personaToUpdate.ApellidoMaterno = model.ApellidoMaterno;
            personaToUpdate.Email = model.Email;
            personaToUpdate.Telefono = model.Telefono;
    
            // Update Usuario properties
            usuarioToUpdate.Username = model.Username;
            if (!string.IsNullOrWhiteSpace(model.Password))
            {
                usuarioToUpdate.Password = model.Password; // Only update if Password is provided
            }
    
            // Save changes
            _context.Entry(personaToUpdate).State = EntityState.Modified;
            _context.Entry(usuarioToUpdate).State = EntityState.Modified;
            await _context.SaveChangesAsync();
    
            return RedirectToAction(nameof(Index));
        }
    
        return View(model);
    }
    

    Razor Page Adjustment Ensure the Razor page includes the hidden field for the Id:

    html
    <form asp-controller="Persona" asp-action="Edit" method="post">
        <input type="hidden" asp-for="Id" />
        <div>
            <label for="Nombre">Nombre</label>
            <input type="text" asp-for="Nombre" />
            <span asp-validation-for="Nombre"></span>
        </div>
        <div>
            <label for="ApellidoPaterno">Apellido Paterno</label>
            <input type="text" asp-for="ApellidoPaterno" />
            <span asp-validation-for="ApellidoPaterno"></span>
        </div>
        <div>
            <label for="ApellidoMaterno">Apellido Materno</label>
            <input type="text" asp-for="ApellidoMaterno" />
            <span asp-validation-for="ApellidoMaterno"></span>
        </div>
        <div>
            <label for="Email">Email</label>
            <input type="text" asp-for="Email" />
            <span asp-validation-for="Email"></span>
        </div>
        <div>
            <label for="Telefono">Telefono</label>
            <input type="text" asp-for="Telefono" />
            <span asp-validation-for="Telefono"></span>
        </div>
        <div>
            <label for="Username">Username</label>
            <input type="text" asp-for="Username" />
            <span asp-validation-for="Username"></span>
        </div>
        <div>
            <label for="Password">Password</label>
            <input type="password" asp-for="Password" />
            <span asp-validation-for="Password"></span>
        </div>
        <input type="submit" value="Editar">
    </form>
    Summary
    

    In summary, the main points are:

    Make sure to set the state of the entities to EntityState.Modified.

    Ensure that the Password field is correctly handled.

    Include a hidden field for the Id in your form.

    Great, let's build on what we've discussed and create a controller that will handle the PersonaUsuarioVista ViewModel in Razor.

    Step-by-Step Guide

    1. Create a Controller First, let's create a controller. In your project, you can add a new controller by right-clicking on the Controllers folder and selecting "Add" -> "New Item..." -> "Controller". Name it PersonaController.

    Here's an example of how your PersonaController might look:

    csharp
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.EntityFrameworkCore;
    using YourNamespace.Models;
    using YourNamespace.ViewModels;
    
    public class PersonaController : Controller
    {
        private readonly YourDbContext _context;
    
        public PersonaController(YourDbContext context)
        {
            _context = context;
        }
    
        // GET: Persona/Edit/5
        public async Task<IActionResult> Edit(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }
    
            var persona = await _context.Personas
                .Include(p => p.Usuario)
                .FirstOrDefaultAsync(m => m.Id == id);
    
            if (persona == null)
            {
                return NotFound();
            }
    
            var viewModel = new PersonaUsuarioVista
            {
                Nombre = persona.Nombre,
                ApellidoPaterno = persona.ApellidoPaterno,
                ApellidoMaterno = persona.ApellidoMaterno,
                Email = persona.Email,
                Telefono = persona.Telefono,
                Username = persona.Usuario?.Username
                // Password is not included here for security reasons, handle password updates separately
            };
    
            return View(viewModel);
        }
    
        // POST: Persona/Edit/5
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Edit(int id, PersonaUsuarioVista model)
        {
            if (id != model.Id)
            {
                return NotFound();
            }
    
            if (ModelState.IsValid)
            {
                var personaToUpdate = await _context.Personas.FindAsync(id);
                if (personaToUpdate == null)
                {
                    return NotFound();
                }
    
                var usuarioToUpdate = await _context.Usuarios.FirstOrDefaultAsync(u => u.PersonaId == id);
                if (usuarioToUpdate == null)
                {
                    return NotFound();
                }
    
                // Update Persona properties
                personaToUpdate.Nombre = model.Nombre;
                personaToUpdate.ApellidoPaterno = model.ApellidoPaterno;
                personaToUpdate.ApellidoMaterno = model.ApellidoMaterno;
                personaToUpdate.Email = model.Email;
                personaToUpdate.Telefono = model.Telefono;
    
                // Update Usuario properties
                usuarioToUpdate.Username = model.Username;
                if (!string.IsNullOrWhiteSpace(model.Password))
                {
                    usuarioToUpdate.Password = model.Password; // Ensure password update is secure
                }
    
                try
                {
                    _context.Update(personaToUpdate);
                    _context.Update(usuarioToUpdate);
                    await _context.SaveChangesAsync();
                }
                catch (DbUpdateConcurrencyException)
                {
                    if (!PersonaExists(personaToUpdate.Id))
                    {
                        return NotFound();
                    }
                    else
                    {
                        throw;
                    }
                }
    
                return RedirectToAction(nameof(Index));
            }
    
            return View(model);
        }
    
        private bool PersonaExists(int id)
        {
            return _context.Personas.Any(e => e.Id == id);
        }
    }
    
    1. Create the View Next, create the Razor view for the Edit action. Right-click on the Views folder, then add a new folder named Persona. Inside this folder, add a new Razor view named Edit.cshtml.

    Here is how your Edit.cshtml might look:

    html
    @model YourNamespace.ViewModels.PersonaUsuarioVista
    
    <h2>Edit Persona and Usuario</h2>
    
    <form asp-action="Edit">
        <input type="hidden" asp-for="Id" />
        <div>
            <label asp-for="Nombre"></label>
            <input asp-for="Nombre" class="form-control" />
            <span asp-validation-for="Nombre" class="text-danger"></span>
        </div>
        <div>
            <label asp-for="ApellidoPaterno"></label>
            <input asp-for="ApellidoPaterno" class="form-control" />
            <span asp-validation-for="ApellidoPaterno" class="text-danger"></span>
        </div>
        <div>
            <label asp-for="ApellidoMaterno"></label>
            <input asp-for="ApellidoMaterno" class="form-control" />
            <span asp-validation-for="ApellidoMaterno" class="text-danger"></span>
        </div>
        <div>
            <label asp-for="Email"></label>
            <input asp-for="Email" class="form-control" />
            <span asp-validation-for="Email" class="text-danger"></span>
        </div>
        <div>
            <label asp-for="Telefono"></label>
            <input asp-for="Telefono" class="form-control" />
            <span asp-validation-for="Telefono" class="text-danger"></span>
        </div>
        <div>
            <label asp-for="Username"></label>
            <input asp-for="Username" class="form-control" />
            <span asp-validation-for="Username" class="text-danger"></span>
        </div>
        <div>
            <label asp-for="Password"></label>
            <input asp-for="Password" class="form-control" />
            <span asp-validation-for="Password" class="text-danger"></span>
        </div>
        <div>
            <input type="submit" value="Save" class="btn btn-primary" />
        </div>
    </form>
    
    <div>
        <a asp-action="Index">Back to List</a>
    </div>
    Summary
    

    Controller: We created the PersonaController with Edit actions to handle both GET and POST requests.

    View: The Razor view Edit.cshtml uses the PersonaUsuarioVista ViewModel to display and edit both Persona and Usuario properties.