Implementación de operaciones de adición y edición de tablas principales y secundarias en ASP.NET MVC con Entity Framework

La gestión de formularios que involucran tablas principales y secundarias simultáneamente en ASP.NET MVC es un requisito común en el desarrollo de sistemas empresariales. A pesar de su frecuencia, las soluciones completas y eficientes en línea son escasas. Este artículo explora una implementación para manejar estas operaciones y busca fomentar la discusión sobre enfoques más optimizados.

Ejemplo de Implementación

La estructura de las entidades involucra una entidad principal (Orden) y una entidad secundaria (Detalle de Orden). La interfaz de usuario presenta un formulario principal para la cabecera de la orden y una tabla dinámica para los detalles. La edición de los detalles se realiza a través de ventanas modales emergentes utilizando Bootstrap.

Capa de Vista (View)

Las vistas como Index, Create y Edit manejan la presentación y la interacción del usuario. La vista parcial EditForm se reutiliza en las vistas de creación y edición. El formulario de detalles de la orden (_OrderDetailForm) se presenta en una ventana modal.

La recopilación de datos del encabezado y los detalles de la orden se realiza mediante jQuery, serializando la información en un objeto JSON para enviarlo al controlador vía AJAX POST.


var $orderDetailTable = {}; // Tabla de detalles de la orden
var objectState = "Added"; // Estado del objeto (Añadido, Modificado, Eliminado)

$(document).ready(function () {
   $('#orderForm').submit(function (event) {
       event.preventDefault(); // Prevenir el envío tradicional del formulario

       var actionUrl = $(this).attr('action');
       var orderDetails = $orderDetailTable.bootstrapTable('getData'); // Obtener datos de la tabla de detalles

       var orderData = {
           Id: $('#OrderId').val() || 0, // Obtener ID de la orden, o 0 si es nuevo
           Customer: $('#Customer').val(),
           ShippingAddress: $('#ShippingAddress').val(),
           OrderDate: $('#OrderDate').val(),
           ObjectState: objectState, // Estado de la orden principal
           OrderDetails: orderDetails.map(function(detail) {
               // Asegurarse de que cada detalle tenga su estado correcto
               return {
                   Id: detail.Id || 0,
                   ProductId: detail.ProductId,
                   Quantity: detail.Quantity,
                   UnitPrice: detail.UnitPrice,
                   ObjectState: detail.ObjectState || "Added" // Estado del detalle
               };
           })
       };

       console.log(orderData); // Log para depuración

       $.ajax({
           url: actionUrl,
           type: "POST",
           dataType: "json",
           contentType: "application/json; charset=utf-8",
           data: JSON.stringify(orderData),
           success: function (result) {
               console.log("Éxito:", result);
               // Redirigir o mostrar mensaje de éxito
               window.location.href = "/Orders/Index";
           },
           error: function (xhr, status, error) {
               console.error("Error:", status, error);
               alert("La operación falló. Consulte la consola para más detalles.");
           }
       });
   });

   // Inicialización de la tabla de detalles (ejemplo básico)
   $orderDetailTable = $('#orderDetailTable').bootstrapTable({
       // Opciones de configuración de Bootstrap Table
       columns: [
           { field: 'Id', title: 'ID', visible: false },
           { field: 'ProductId', title: 'Product ID', visible: false },
           { field: 'ProductName', title: 'Producto' },
           { field: 'Quantity', title: 'Cantidad' },
           { field: 'UnitPrice', title: 'Precio Unitario' },
           {
               field: 'operate',
               title: 'Acciones',
               align: 'center',
               formatter: operateFormatter,
               events: operateEvents
           }
       ]
   });
});

// Función para el formato de botones de acción en la tabla
function operateFormatter(value, row, index) {
   return [
       '<a class="edit" href="javascript:void(0)" title="Editar">',
       '<i class="glyphicon glyphicon-edit"></i>',
       '</a>  ',
       '<a class="remove" href="javascript:void(0)" title="Eliminar">',
       '<i class="glyphicon glyphicon-remove"></i>',
       '</a>'
   ].join('');
}

// Eventos para los botones de acción
window.operateEvents = {
   'click .edit': function (e, value, row, index) {
       console.log("Editar fila:", row, "Índice:", index);
       // Lógica para abrir modal de edición con datos de la fila 'row'
       editOrderDetail(row, index);
   },
   'click .remove': function (e, value, row, index) {
       console.log("Eliminar fila:", row, "Índice:", index);
       // Lógica para eliminar la fila de la tabla temporalmente
       // y marcarla para eliminación en el backend.
       deleteOrderDetail(index);
   }
};

// Función para añadir nuevo detalle de orden
$('#newOrderDetailButton').on('click', function (e) {
   e.preventDefault();
   if ($("#orderForm").valid()) { // Validar el formulario principal antes de añadir
       var url = "/Orders/CreateOrderDetailPartial"; // Acción que devuelve el Partial View del formulario de detalle
       $.get(url, function (data) {
           $('#orderDetailModalBody').html(data);
           $('#orderDetailModal').modal('show');
           // Inicializar campos del modal si es necesario
           $('#orderDetailRowIndex').val(-1); // -1 indica nueva fila
           $('#orderDetailId').val(0); // ID 0 para nueva fila
       });
   }
});

// Función para editar un detalle de orden existente
function editOrderDetail(rowData, rowIndex) {
    var url = "/Orders/EditOrderDetailPartial/" + rowData.Id; // Acción que devuelve el Partial View para editar
    $.get(url, function (data) {
        $('#orderDetailModalBody').html(data);
        $('#orderDetailModal').modal('show');
        $('#orderDetailRowIndex').val(rowIndex); // Guardar el índice de la fila a editar
        $('#orderDetailId').val(rowData.Id); // Guardar el ID de la fila
    });
}

// Función para marcar una fila para eliminación
function deleteOrderDetail(rowIndex) {
   var data = $orderDetailTable.bootstrapTable('getData');
   if (rowIndex >= 0 && rowIndex < data.length) {
       var row = data[rowIndex];
       // Marcar el objeto para eliminación si ya existe en la base de datos
       if (row.Id && row.Id > 0) {
           row.ObjectState = "Deleted";
           // Opcionalmente, ocultar visualmente la fila
           $('#orderDetailTable').bootstrapTable('updateRow', {
               index: rowIndex,
               row: { ...row, // Copiar propiedades existentes
                      // Añadir una clase o estilo para indicar eliminación
                      css: { 'text-decoration': 'line-through', 'color': 'grey' }
                    }
           });
            // También puedes eliminarla de los datos si la lógica del backend
            // maneja los estados y no necesita la fila presente en el POST
            // $orderDetailTable.bootstrapTable('removeByUniqueId', row.Id); // Si tienes un ID único configurado
            // O simplemente actualizar el array de datos si no usas un ID único para bootstrap table
            data.splice(rowIndex, 1);
            $orderDetailTable.bootstrapTable('load', data); // Recargar tabla con datos actualizados
       } else {
           // Si es una fila recién añadida y no guardada, simplemente la removemos
           $orderDetailTable.bootstrapTable('removeRow', { index: rowIndex });
       }
   }
}

// Función para guardar el detalle de orden desde el modal
function saveOrderDetail() {
   var form = $('#orderDetailForm');
   if (form.valid()) {
       var detailData = form.serializeObject(); // Asumiendo que tienes una función serializeObject para jQuery
       var rowIndex = parseInt($('#orderDetailRowIndex').val());
       var existingId = parseInt($('#orderDetailId').val());
       var objectState = (existingId > 0) ? "Modified" : "Added";

       detailData.ObjectState = objectState;
       if (existingId > 0) {
           detailData.Id = existingId;
       }

       if (rowIndex === -1) {
           // Añadir nueva fila a la tabla
           $orderDetailTable.bootstrapTable('append', detailData);
       } else {
           // Actualizar fila existente
           $orderDetailTable.bootstrapTable('updateRow', {
               index: rowIndex,
               row: detailData
           });
       }
       $('#orderDetailModal').modal('hide');
   }
}

// Adaptador simple para serializeObject si no está disponible
$.fn.serializeObject = function() {
   var o = {};
   var a = this.serializeArray();
   $.each(a, function() {
       if (o[this.name] !== undefined) {
           if (!o[this.name].push) {
               o[this.name] = [o[this.name]];
           }
           o[this.name].push(this.value || '');
       } else {
           o[this.name] = this.value || '';
       }
   });
   return o;
};

 

Nota sobre la serialización JSON: La dificultad para serializar el campo ObjectState del tipo enumerado desde el cliente al servidor es un desafío común. Esto a menudo requiere lógica adicional en el controlador para reestablecer el estado de la entidad antes de la persistencia.

Capa de Controlador (Controller)

El método Create (POST) en el controlador es responsable de procesar los datos enviados. Dado que el estado ObjectState no se serializa correctamente desde el cliente, se implementa un bucle foreach para establecer manualmente el estado de la orden principal y sus detalles antes de llamar al servicio de inserción/actualización.


using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using YourProject.Models; // Asegúrate de incluir tus namespaces
using YourProject.Services; // Namespace para tus servicios
using YourProject.Infrastructure; // Namespace para UnitOfWork

public class OrdersController : Controller
{
   private readonly IOrderService _orderService;
   private readonly IUnitOfWork _unitOfWork;

   // Constructor para inyección de dependencias
   public OrdersController(IOrderService orderService, IUnitOfWork unitOfWork)
   {
       _orderService = orderService;
       _unitOfWork = unitOfWork;
   }

   // GET: Orders/Create
   public ActionResult Create()
   {
       // Inicializar ViewBag para listas desplegables si son necesarias
       var orderRepository = _unitOfWork.Repository<Order>();
       ViewBag.OrderId = new SelectList(orderRepository.Queryable(), "Id", "Customer");

       var productRepository = _unitOfWork.Repository<Product>();
       ViewBag.ProductId = new SelectList(productRepository.Queryable(), "Id", "Name");

       return View();
   }

   // POST: Orders/Create
   [HttpPost]
   // [ValidateAntiForgeryToken] // Descomentar si usas ValidateAntiForgeryToken
   public ActionResult Create(Order order) // El modelo ahora puede incluir OrderDetails directamente
   {
       if (ModelState.IsValid)
       {
           // Lógica para manejar el estado de los objetos
           order.ObjectState = ObjectState.Added; // Estado para la orden principal

           if (order.OrderDetails != null)
           {
               foreach (var detail in order.OrderDetails)
               {
                   // Asegurar que los detalles también tengan su estado correcto
                   detail.ObjectState = ObjectState.Added;
                   // Si el producto está incluido y necesita ser marcado como Desvinculado
                   if (detail.Product != null)
                   {
                       detail.Product.ObjectState = ObjectState.Detached;
                   }
               }
           }

           // Llamar al servicio para insertar o actualizar el gráfico de entidades
           _orderService.InsertOrUpdateGraph(order);
           _unitOfWork.SaveChanges(); // Guardar todos los cambios en la base de datos

           // Podrías querer retornar un JSON de éxito o redirigir
           // return RedirectToAction("Index");
            return Json(new { Status = "Success", Message = "Orden creada exitosamente." }, JsonRequestBehavior.AllowGet);
       }

       // Si el modelo no es válido, devolver la vista con errores
       // Es posible que necesites volver a poblar los ViewBag si los usas
       var orderRepo = _unitOfWork.Repository<Order>();
       ViewBag.OrderId = new SelectList(orderRepo.Queryable(), "Id", "Customer", order.Id);
       var productRepo = _unitOfWork.Repository<Product>();
       ViewBag.ProductId = new SelectList(productRepo.Queryable(), "Id", "Name");

       return View(order);
   }

    // Método para obtener el PartialView de creación de detalle de orden
   public ActionResult CreateOrderDetailPartial()
   {
       // Devolver una vista parcial vacía o con datos iniciales
       // Asegúrate de que este método devuelva el ViewResult correcto para tu Partial View
       var model = new OrderDetail { Id = 0, ProductId = 0, Quantity = 1, UnitPrice = 0 };
       // Cargar listas desplegables si son necesarias para el detalle
       var productRepository = _unitOfWork.Repository<Product>();
       ViewBag.ProductId = new SelectList(productRepository.Queryable(), "Id", "Name");
       return PartialView("_OrderDetailForm", model);
   }

   // Método para obtener el PartialView de edición de detalle de orden
   public ActionResult EditOrderDetailPartial(int id)
   {
       var detailRepository = _unitOfWork.Repository<OrderDetail>();
       var orderDetail = detailRepository.Find(id);
       if (orderDetail == null)
       {
           return HttpNotFound();
       }
       // Cargar listas desplegables si son necesarias para el detalle
       var productRepository = _unitOfWork.Repository<Product>();
       ViewBag.ProductId = new SelectList(productRepository.Queryable(), "Id", "Name", orderDetail.ProductId);
       return PartialView("_OrderDetailForm", orderDetail);
   }

   // --- Métodos para manejar la eliminación ---
   // La eliminación de registros hijos es un desafío.
   // Una estrategia común es marcar los registros para eliminación en lugar de eliminarlos físicamente de inmediato.
   // O manejar la eliminación directamente en el POST del formulario principal.
   // Aquí se asume que el cliente enviará los detalles y su estado (Added, Modified, Deleted).
   // El método InsertOrUpdateGraph debería manejar la lógica de eliminación basada en el ObjectState.

}

// Definición de enumeración para el estado del objeto (ejemplo)
public enum ObjectState
{
   Unchanged,
   Added,
   Modified,
   Deleted,
   Detached // Para entidades que no deben ser rastreadas por el contexto
}

// Clase base de entidad (ejemplo)
public abstract class Entity
{
   public int Id { get; set; }
   public ObjectState ObjectState { get; set; } = ObjectState.Unchanged;
}

// Entidades de ejemplo
public class Order : Entity
{
   public string Customer { get; set; }
   public string ShippingAddress { get; set; }
   public DateTime OrderDate { get; set; }
   public virtual ICollection<OrderDetail> OrderDetails { get; set; } = new List<OrderDetail>();
}

public class OrderDetail : Entity
{
   public int OrderId { get; set; }
   public int ProductId { get; set; }
   public int Quantity { get; set; }
   public decimal UnitPrice { get; set; }

   public virtual Order Order { get; set; }
   public virtual Product Product { get; set; }
}

public class Product : Entity
{
   public string Name { get; set; }
   // Otras propiedades...
}

// Interfaz y Clase de ejemplo para UnitOfWork y Repository
public interface IRepository<T> where T : class
{
   IQueryable<T> Queryable();
   T Find(int id);
   void Add(T entity);
   void Update(T entity);
   void Delete(T entity);
   // ... otros métodos
}

public interface IUnitOfWork : System.IDisposable
{
   IRepository<T> Repository<T>() where T : class;
   int SaveChanges();
}

// Implementación de servicio de ejemplo (necesitaría una implementación real)
public interface IOrderService
{
   void InsertOrUpdateGraph(Order order);
}

// Ejemplo de implementación de InsertOrUpdateGraph (simplificado)
public class OrderService : IOrderService
{
   private readonly IUnitOfWork _unitOfWork;

   public OrderService(IUnitOfWork unitOfWork)
   {
       _unitOfWork = unitOfWork;
   }

   public void InsertOrUpdateGraph(Order order)
   {
       var orderRepo = _unitOfWork.Repository<Order>();
       var detailRepo = _unitOfWork.Repository<OrderDetail>();

       if (order.ObjectState == ObjectState.Added)
       {
           orderRepo.Add(order);
       }
       else if (order.ObjectState == ObjectState.Modified)
       {
           orderRepo.Update(order); // Asumiendo que Update maneja la actualización de propiedades
       }
       // Si la orden principal se marca como eliminada, se debería manejar aquí o en SaveChanges

       if (order.OrderDetails != null)
       {
           foreach (var detail in order.OrderDetails)
           {
               switch (detail.ObjectState)
               {
                   case ObjectState.Added:
                       detailRepo.Add(detail);
                       break;
                   case ObjectState.Modified:
                       detailRepo.Update(detail);
                       break;
                   case ObjectState.Deleted:
                       detailRepo.Delete(detail);
                       break;
               }
           }
       }
   }
}
 

Desafío de la Eliminación: La eliminación de detalles de la orden en estado de edición presenta una dificultad particular. Una solución podría ser marcar los detalles para su eliminación (ObjectState.Deleted) en lugar de eliminarlos enmediatamente del DOM. El backend luego procesaría estas marcas. Sin embargo, esto requiere lógica adicional para filtrar o mostrar correctamente las filas marcadas.

Se está buscando activamente una solución más elegante para la gestión de la eliminación y la serialización del estado del objeto que minimice el código repetitivo en el controlador.

Edición de Detalles en Modal

El botón "Añadir nuevo detalle" (#neworderdetailbutton) y la lógica asociada en el controlador (CreateOrderDetailPartial) permiten abrir una ventana modal para ingresar nuevos detalles. De manera similar, la edición de detalles existentes se maneja a través de una acción del controlador que devuelve un PartialView específico para la edición.

La implementación del manejo de la eliminación, especialmente al editar registros existentes, sigue siendo un área de mejora activa, buscando una solución que sea eficiente y fácil de mantener.

Etiquetas: ASP.NET MVC Entity Framework C# Master-Detail CRUD

Publicado el 7-28 17:26