Compartir a través de


Proporcionar la compatibilidad con entradas de formularios de datos CRUD (crear, leer, actualizar y eliminar)

de Microsoft

Descargar PDF

Este es el paso 5 de un tutorial de la aplicación "NerdDinner" gratuito que le guía durante el proceso de creación de una aplicación web pequeña, pero completa, con ASP.NET MVC 1.

El paso 5 muestra cómo seguir nuestra clase DinnersController habilitando la compatibilidad para editar, crear y eliminar cenas con ella también.

Si utiliza ASP.NET MVC 3, le recomendamos que siga los tutoriales Introducción a MVC 3 o Tienda de música de MVC.

Paso 5 de NerdDinner: Creación, actualización, eliminación de escenarios de formulario

Hemos introducido controladores y vistas, y hemos tratado cómo usarlos para implementar una experiencia de descripción y detalles para cenas en el sitio. Nuestro siguiente paso será seguir con nuestra clase DinnersController y habilitar la compatibilidad para editar, crear y eliminar cenas con ella también.

Direcciones URL controladas por DinnersController

Anteriormente agregamos métodos de acción a DinnersController que implementaban compatibilidad con dos direcciones URL: /Dinners y /Dinners/Details/[id].

URL VERBO Propósito
/Dinners/ OBTENER Muestra una lista HTML de las próximas cenas.
/Dinners/Details/[id] OBTENER Muestra los detalles de un cena específica.

Ahora agregaremos métodos de acción para implementar tres direcciones URL adicionales: /Dinners/Edit/[id], /Dinners/Create y /Dinners/Delete/[id]. Estas direcciones URL permitirán la edición de cenas existentes, la creación de nuevas cenas y la eliminación de cenas.

Se admitirán las interacciones de los verbos HTTP GET y HTTP POST con estas nuevas direcciones URL. Las solicitudes HTTP GET a estas direcciones URL mostrarán la vista HTML inicial de los datos (un formulario rellenado con los datos de las cenas en el caso de "editar", un formulario en blanco en el caso de "crear" y una pantalla de confirmación de eliminación en caso de "eliminar"). Las solicitudes HTTP POST a estas direcciones URL guardarán, actualizarán o eliminarán los datos de DinnerRepository (y de allí a la base de datos).

URL VERBO Propósito
/Dinners/Edit/[id] OBTENER Muestra un formulario HTML editable rellenado con los datos de las cenas.
PUBLICAR Guarde los cambios del formulario para una cena determinada en la base de datos.
/Dinners/Create OBTENER Muestra un formulario HTML vacío que permite a los usuarios definir nuevas cenas.
PUBLICAR Cree una nueva cena y guárdela en la base de datos.
/Dinners/Delete/[id] OBTENER Muestra la pantalla de confirmación de eliminación.
PUBLICAR Elimina la cena especificada de la base de datos.

Compatibilidad con Edit

Comencemos implementando el escenario de "edición".

Método de acción Edit de HTTP-GET

Comenzaremos implementando el comportamiento HTTP "GET" del método de acción Edit. Este método se invocará cuando se solicite la dirección URL /Dinners/Edit/[id]. Nuestra implementación tendrá el siguiente aspecto:

//
// GET: /Dinners/Edit/2

public ActionResult Edit(int id) {

    Dinner dinner = dinnerRepository.GetDinner(id);
    
    return View(dinner);
}

El código anterior usa DinnerRepository para recuperar un objeto Dinner. A continuación, representa una plantilla de vista mediante el objeto Dinner. Dado que no hemos pasado explícitamente un nombre de plantilla al método de asistente View(), usará la ruta de acceso predeterminada basada en convención para resolver la plantilla de vista: /Views/Dinners/Edit.aspx.

Ahora vamos a crear esta plantilla de vista. Para ello, haga clic con el botón derecho en el método Edit y seleccione el comando contextual "Agregar vista":

Screenshot of creating a view template to add view in Visual Studio.

En el cuadro de diálogo "Agregar vista", indicaremos que pasamos un objeto Dinner a nuestra plantilla de vista como modelo y elegiremos aplicar scaffolding automático a una plantilla "Edit":

Screenshot of Add view to auto-scaffold an Edit template.

Al hacer clic en el botón "Agregar", Visual Studio nos creará un nuevo archivo de plantilla de vista "Index.aspx" dentro de nuestro directorio "\Views\Dinners". También abrirá la nueva plantilla de vista "Edit.aspx" en el editor de código, rellenada con una implementación de scaffolding "Edit" inicial, como se muestra a continuación:

Screenshot of new Edit view template within the code-editor.

Vamos a realizar algunos cambios en la scaffolding "Edit" predeterminada generada y actualizar la plantilla de vista de edición para que tenga el contenido siguiente (que quita algunas de las propiedades que no queremos exponer):

<asp:Content ID="Title" ContentPlaceHolderID="TitleContent" runat="server">
    Edit: <%=Html.Encode(Model.Title)%>
</asp:Content>

<asp:Content ID="Main" ContentPlaceHolderID="MainContent" runat="server">

    <h2>Edit Dinner</h2>

    <%=Html.ValidationSummary("Please correct the errors and try again.") %>  
    
    <% using (Html.BeginForm()) { %>

        <fieldset>
            <p>
                <label for="Title">Dinner Title:</label>
                <%=Html.TextBox("Title") %>
                <%=Html.ValidationMessage("Title", "*") %>
            </p>
            <p>
                <label for="EventDate">EventDate:</label>
                <%=Html.TextBox("EventDate", String.Format("{0:g}", Model.EventDate))%>
                <%=Html.ValidationMessage("EventDate", "*") %>
            </p>
            <p>
                <label for="Description">Description:</label>
                <%=Html.TextArea("Description") %>
                <%=Html.ValidationMessage("Description", "*")%>
            </p>
            <p>
                <label for="Address">Address:</label>
                <%=Html.TextBox("Address") %>
                <%=Html.ValidationMessage("Address", "*") %>
            </p>
            <p>
                <label for="Country">Country:</label>
                <%=Html.TextBox("Country") %>               
                <%=Html.ValidationMessage("Country", "*") %>
            </p>
            <p>
                <label for="ContactPhone">ContactPhone #:</label>
                <%=Html.TextBox("ContactPhone") %>
                <%=Html.ValidationMessage("ContactPhone", "*") %>
            </p>
            <p>
                <label for="Latitude">Latitude:</label>
                <%=Html.TextBox("Latitude") %>
                <%=Html.ValidationMessage("Latitude", "*") %>
            </p>
            <p>
                <label for="Longitude">Longitude:</label>
                <%=Html.TextBox("Longitude") %>
                <%=Html.ValidationMessage("Longitude", "*") %>
            </p>
            <p>
                <input type="submit" value="Save"/>
            </p>
        </fieldset>
        
    <% } %>
    
</asp:Content>

Cuando ejecutemos la aplicación y solicitemos la dirección URL "/Dinners/Edit/1", veremos la página siguiente:

Screenshot of My M V C Application page.

El marcado HTML generado por nuestra vista es similar al siguiente. Es HTML estándar: con un elemento <form> que realiza una solicitud HTTP POST a la dirección URL /Dinners/Edit/1 cuando se inserta el botón "Save" <input type="submit"/>. Se ha generado un elemento <input type="text"/> para cada propiedad editable:

Screenshot of the generated H T M L markup.

Métodos del asistente Html.BeginForm() y Html.TextBox()

Nuestra plantilla de vista "Edit.aspx" usa varios métodos del "asistente HTML": Html.ValidationSummary(), Html.BeginForm(), Html.TextBox() y Html.ValidationMessage(). Además de generar marcado HTML para nosotros, estos métodos del asistente proporcionan compatibilidad integrada con el control de errores y la validación.

Método del asistente Html.BeginForm()

El método del asistente Html.BeginForm() es lo que genera el elemento <form> HTML en nuestro marcado. En nuestra plantilla de vista Edit.aspx observará que estamos aplicando una instrucción "using" de C# al usar este método. La llave de apertura indica el principio del contenido de <form> y la llave de cierre es lo que indica el final del elemento </form>:

<% using (Html.BeginForm()) { %>

   <fieldset>
   
      <!-- Fields Omitted for Brevity -->
   
      <p>
         <input type="submit" value="Save"/>
      </p>
   </fieldset>
   
<% } %>

Como alternativa, si encuentra el enfoque de instrucción "using" no natural para un escenario como este, puede usar una combinación Html.BeginForm() y Html.EndForm() (que hace lo mismo):

<% Html.BeginForm();  %>

   <fieldset>
   
      <!-- Fields Omitted for Brevity -->
   
      <p>
          <input type="submit" value="Save"/>
      </p>
   </fieldset>
   
<% Html.EndForm(); %>

Llamar a Html.BeginForm() sin parámetros hará que genere un elemento form que realice una solicitud HTTP-POST a la dirección URL de la solicitud actual. Por eso nuestra vista de edición genera un elemento <form action="/Dinners/Edit/1" method="post">. También podríamos haber pasado parámetros explícitos a Html.BeginForm() si hubiéramos querido publicar en una dirección URL diferente.

Método de asistente Html.TextBox()

Nuestra vista de Edit.aspx usa el método de asistente Html.TextBox() para generar elementos <input type="text"/>:

<%= Html.TextBox("Title") %>

El método Html.TextBox() anterior toma un único parámetro, que se usa para especificar los atributos id/name del elemento <input type="text"/> que se va a generar, así como la propiedad del modelo para rellenar el valor del cuadro de texto. Por ejemplo, el objeto Dinner que pasamos a la vista Edit tenía un valor de propiedad "Title" de ".NET Futures" y, por tanto, nuestra salida de llamada de método Html.TextBox("Title"): <input id="Title" name="Title" type="text" value=".NET Futures" />.

Como alternativa, podemos usar el primer parámetro Html.TextBox() para especificar el identificador o nombre del elemento y, a continuación, pasar explícitamente el valor para usarlo como segundo parámetro:

<%= Html.TextBox("Title", Model.Title)%>

A menudo, queremos realizar el formato personalizado en el valor que es la salida. El método estático String.Format() integrado en .NET es útil para estos escenarios. Nuestra plantilla de vista de Edit.aspx usa esto para dar formato al valor EventDate (que es de tipo DateTime) para que no muestre segundos para la hora:

<%= Html.TextBox("EventDate", String.Format("{0:g}", Model.EventDate)) %>

Opcionalmente, se puede usar un tercer parámetro para Html.TextBox() para generar atributos HTML adicionales. El fragmento de código siguiente muestra cómo representar un atributo size="30" adicional y un atributo class="mycssclass" en el elemento <input type="text"/>. Tenga en cuenta cómo se escapa el nombre del atributo de clase mediante un carácter "@" porque "class" es una palabra clave reservada en C#:

<%= Html.TextBox("Title", Model.Title, new { size=30, @class="myclass" } )%>

Implementación del método de acción Edit HTTP-POST

Ahora tenemos la versión HTTP-GET del método de acción Edit implementado. Cuando un usuario solicita la dirección URL de /Dinners/Edit/1, recibe una página HTML como la siguiente:

Screenshot of H T M L output when user requests an Edit Dinner.

Al presionar el botón "Save" se produce una publicación de formulario en la dirección URL /Dinners/Edit/1 y se envían los valores del formulario <input> HTML mediante el verbo HTTP POST. Ahora vamos a implementar el comportamiento HTTP POST de nuestro método de acción Edit, que controlará el guardado de la cena.

Comenzaremos agregando un método de acción "Edit" sobrecargado a nuestro DinnersController que tiene un atributo "AcceptVerbs" en él que indica que controla los escenarios HTTP POST:

//
// POST: /Dinners/Edit/2

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {
   ...
}

Cuando el atributo [AcceptVerbs] se aplica a los métodos de acción sobrecargados, ASP.NET MVC controla automáticamente la distribución de solicitudes al método de acción adecuado en función del verbo HTTP entrante. Las solicitudes HTTP POST a las direcciones URL /Dinners/Edit/[id] irán al método Edit anterior, mientras que todas las demás solicitudes de verbo HTTP a las direcciones URL /Dinners/Edit/[id] irán al primer método Edit que hemos implementado (que no tenía un atributo [AcceptVerbs]).

Tema secundario: ¿Por qué diferenciar mediante verbos HTTP?
Podría preguntar: ¿por qué usamos una sola dirección URL y diferenciamos su comportamiento mediante el verbo HTTP? ¿Por qué no solo tiene dos direcciones URL independientes para controlar la carga y guardar los cambios de edición? Por ejemplo: /Dinners/Edit/[id] para mostrar el formulario inicial y /Dinners/Save/[id] para controlar la publicación del formulario para guardarla. La desventaja de publicar dos direcciones URL independientes es que, en los casos en los que publicamos en /Dinners/Save/2 y, a continuación, necesita volver a reproducir el formulario HTML debido a un error de entrada, el usuario final terminará teniendo la dirección URL /Dinners/Save/2 en la barra de direcciones del explorador (ya que esa era la dirección URL a la que se publicó el formulario). Si el usuario final marca esta página que se ha vuelto a mostrar en su lista de favoritos del explorador, o copia o pega la dirección URL y la envía por correo electrónico a un amigo, terminará guardando una dirección URL que no funcionará en el futuro (ya que esa dirección URL depende de los valores de publicación). Al exponer una sola dirección URL (por ejemplo: /Dinners/Edit/[id]) y diferenciar el procesamiento de él por verbo HTTP, es seguro que los usuarios finales marquen la página de edición o envíen la dirección URL a otros usuarios.

Recuperación de valores de publicación de formularios

Hay varias maneras de acceder a los parámetros de formulario publicados en nuestro método "Edit" de HTTP POST. Un enfoque sencillo consiste simplemente en usar la propiedad Request de la clase base Controller para acceder a la colección de formularios y recuperar los valores publicados directamente:

//
// POST: /Dinners/Edit/2

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {

    // Retrieve existing dinner
    Dinner dinner = dinnerRepository.GetDinner(id);

    // Update dinner with form posted values
    dinner.Title = Request.Form["Title"];
    dinner.Description = Request.Form["Description"];
    dinner.EventDate = DateTime.Parse(Request.Form["EventDate"]);
    dinner.Address = Request.Form["Address"];
    dinner.Country = Request.Form["Country"];
    dinner.ContactPhone = Request.Form["ContactPhone"];

    // Persist changes back to database
    dinnerRepository.Save();

    // Perform HTTP redirect to details page for the saved Dinner
    return RedirectToAction("Details", new { id = dinner.DinnerID });
}

Sin embargo, el enfoque anterior es un poco detallado, especialmente una vez que agregamos lógica de control de errores.

Un mejor enfoque para este escenario es aprovechar el método de asistente UpdateModel() integrado en la clase base Controller. Admite la actualización de las propiedades de un objeto que se pasa mediante los parámetros de formulario entrantes. Usa la reflexión para determinar los nombres de propiedad en el objeto y, a continuación, convierte y asigna automáticamente valores a ellos en función de los valores de entrada enviados por el cliente.

Podríamos usar el método UpdateModel() para simplificar nuestra acción de edición HTTP-POST mediante este código:

//
// POST: /Dinners/Edit/2

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    UpdateModel(dinner);

    dinnerRepository.Save();

    return RedirectToAction("Details", new { id = dinner.DinnerID });
}

Ahora podemos visitar la dirección URL /Dinners/Edit/1 y cambiar el título de nuestra cena:

Screenshot of the Edit Dinner page.

Al hacer clic en el botón "Save", realizaremos una publicación de formulario en nuestra acción de edición y los valores actualizados se conservarán en la base de datos. A continuación, se le redirigirá a la dirección URL de detalles de la cena (que mostrará los valores recién guardados):

Screenshot of the details URL for the Dinner.

Control de errores de edición

Nuestra implementación HTTP-POST actual funciona bien, excepto cuando hay errores.

Cuando un usuario comete un error al editar un formulario, es necesario asegurarnos de que el formulario se vuelve a reproducir con un mensaje de error informativo que le guía para corregirlo. Esto incluye casos en los que un usuario final publica una entrada incorrecta (por ejemplo, una cadena de fecha con formato incorrecto), así como los casos en los que el formato de entrada es válido, pero hay una infracción de regla de negocios. Cuando se producen errores, el formulario debe conservar los datos de entrada que el usuario escribió originalmente para que no tenga que rellenar los cambios manualmente. Este proceso debe repetirse tantas veces como sea necesario hasta que el formulario se complete correctamente.

ASP.NET MVC incluye algunas características integradas agradables que facilitan el control de errores y la reproducción de formularios. Para ver estas características en acción, vamos a actualizar nuestro método de acción Edit con el código siguiente:

//
// POST: /Dinners/Edit/2

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    try {

        UpdateModel(dinner);

        dinnerRepository.Save();

        return RedirectToAction("Details", new { id=dinner.DinnerID });
    }
    catch {

        foreach (var issue in dinner.GetRuleViolations()) {
            ModelState.AddModelError(issue.PropertyName, issue.ErrorMessage);
        }

        return View(dinner);
    }
}

El código anterior es similar a la implementación anterior, salvo que ahora encapsulamos un bloque de control de errores try/catch alrededor de nuestro trabajo. Si se produce una excepción al llamar a UpdateModel() o cuando intentamos guardar DinnerRepository (lo que generará una excepción si el objeto Dinner que intentamos guardar no es válido debido a una infracción de regla dentro de nuestro modelo), se ejecutará el bloque de control de errores catch. Dentro de ella se recorren en bucle las infracciones de reglas que existen en el objeto Dinner y las agregamos a un objeto ModelState (que analizaremos en breve). A continuación, volvemos a mostrar la vista.

Para ver este trabajo, vamos a volver a ejecutar la aplicación, editar una cena y cambiarla para que tenga un título vacío, un EventDate de "BOGUS" y use un número de teléfono del Reino Unido con un valor de país o región de EE. UU. Al presionar el botón "Save" nuestro método de edición de HTTP POST no podrá guardar la cena (porque hay errores) y volverá a reproducir el formulario:

Screenshot of the form redisplay due to errors using the H T T P S P O S T Edit method.

Nuestra aplicación tiene una experiencia de error decente. Los elementos de texto con la entrada no válida se resaltan en rojo y los mensajes de error de validación se muestran al usuario final sobre ellos. El formulario también conserva los datos de entrada que el usuario escribió originalmente, de modo que no tenga que rellenar nada.

¿Cómo, podría preguntarse, ha ocurrido esto? ¿Cómo se resaltan los cuadros de texto Title, EventDate y ContactPhone en rojo y saben generar los valores de usuario especificados originalmente? ¿Y cómo se muestran los mensajes de error en la lista en la parte superior? La buena noticia es que esto no ha ocurrido por magia, sino porque usamos algunas de las características integradas ASP.NET MVC que facilitan la validación de entrada y los escenarios de control de errores.

Descripción de ModelState y los métodos de asistente HTML de validación

Las clases de controlador tienen una colección de propiedades "ModelState", que proporcionan una manera de indicar que existen errores con un objeto de modelo que se pasa a una vista. Las entradas de error de la colección ModelState identifican el nombre de la propiedad del modelo con el problema (por ejemplo: "Title", "EventDate" o "ContactPhone") y permiten especificar un mensaje de error descriptivo para el usuario (por ejemplo: "Title is required").

El método de asistente UpdateModel() rellena automáticamente la colección ModelState cuando encuentra errores al intentar asignar valores de formulario a las propiedades del objeto de modelo. Por ejemplo, la propiedad EventDate del objeto Dinner es de tipo DateTime. Cuando el método UpdateModel() no pudo asignarle el valor de cadena "BOGUS" en el escenario anterior, el método UpdateModel() agregó una entrada a la colección ModelState que indica que se había producido un error de asignación con esa propiedad.

Los desarrolladores también pueden escribir código para agregar explícitamente entradas de error a la colección ModelState, como estamos haciendo a continuación en nuestro bloque de control de errores "catch", que rellena la colección ModelState con entradas basadas en las infracciones de reglas activas en el objeto Dinner:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    try {
    
        UpdateModel(dinner);

        dinnerRepository.Save();

        return RedirectToAction("Details", new { id=dinner.DinnerID });
    }
    catch {
    
        foreach (var issue in dinner.GetRuleViolations()) {
            ModelState.AddModelError(issue.PropertyName, issue.ErrorMessage);
        }

        return View(dinner);
    }
}

Integración del asistente HTML con ModelState

Métodos de asistente HTML, como Html.TextBox(): compruebe la colección ModelState al representar la salida. Si existe un error para el elemento, representan el valor especificado por el usuario y una clase de error CSS.

Por ejemplo, en nuestra vista "Edit" usamos el método de asistente Html.TextBox() para representar EventDate de nuestro objeto Dinner:

<%= Html.TextBox("EventDate", String.Format("{0:g}", Model.EventDate)) %>

Cuando la vista se representa en el escenario de error, el método Html.TextBox() comprueba la colección ModelState para ver si hay algún error asociado a la propiedad "EventDate" del objeto Dinner. Cuando determinó que había un error que representaba la entrada de usuario enviada ("BOGUS") como valor y agregaba una clase de error CSS al marcado <input type="textbox"/> generado:

<input class="input-validation-error"id="EventDate" name="EventDate" type="text" value="BOGUS"/>

Puede personalizar la apariencia de la clase de error css para que tenga el aspecto deseado. La clase de error CSS predeterminada, "input-validation-error", se define en la hoja de estilos \content\site.css y tiene el siguiente aspecto:

.input-validation-error
{
    border: 1px solid #ff0000;
    background-color: #ffeeee;
}

Esta regla CSS es lo que provocó que los elementos de entrada no válidos se resaltaran de la siguiente manera:

Screenshot of the highlighted invalid input elements.

Método de asistente Html.ValidationMessage()

El método de asistente Html.ValidationMessage() se puede usar para generar el mensaje de error ModelState asociado a una propiedad de modelo determinada:

<%= Html.ValidationMessage("EventDate")%>

El código anterior genera: <span class="field-validation-error">. El valor 'BOGUS' no es válido </span>

El método de asistente Html.ValidationMessage() también admite un segundo parámetro que permite a los desarrolladores invalidar el mensaje de texto de error que se muestra:

<%= Html.ValidationMessage("EventDate","*") %>

El código anterior genera: <span class="field-validation-error">*</span> en lugar del texto de error predeterminado cuando existe un error para la propiedad EventDate.

Método de asistente Html.ValidationSummary()

El método de asistente Html.ValidationSummary() se puede usar para representar un mensaje de error de resumen, acompañado de una lista <ul><li/></ul> de todos los mensajes de error detallados de la colección ModelState:

Screenshot of the list of all detailed error messages in the ModelState collection.

El método de asistente Html.ValidationSummary() toma un parámetro de cadena opcional, que define un mensaje de error de resumen que se muestra encima de la lista de errores detallados:

<%= Html.ValidationSummary("Please correct the errors and try again.") %>

Opcionalmente, puede usar CSS para invalidar el aspecto de la lista de errores.

Uso de un método de asistente AddRuleViolations

Nuestra implementación de edición HTTP-POST inicial usó una instrucción foreach dentro de su bloque catch para recorrer en bucle las infracciones de reglas del objeto Dinner y agregarlas a la colección ModelState del controlador:

catch {
        foreach (var issue in dinner.GetRuleViolations()) {
            ModelState.AddModelError(issue.PropertyName, issue.ErrorMessage);
        }

        return View(dinner);
    }

Podemos hacer que este código sea un poco más limpio agregando una clase "ControllerHelpers" al proyecto NerdDinner e implementar un método de extensión "AddRuleViolations" dentro de él que agrega un método de asistente a la clase ModelStateDictionary de ASP.NET MVC. Este método de extensión puede encapsular la lógica necesaria para rellenar ModelStateDictionary con una lista de errores RuleViolation:

public static class ControllerHelpers {

   public static void AddRuleViolations(this ModelStateDictionary modelState, IEnumerable<RuleViolation> errors) {
   
       foreach (RuleViolation issue in errors) {
           modelState.AddModelError(issue.PropertyName, issue.ErrorMessage);
       }
   }
}

A continuación, podemos actualizar nuestro método de acción Edit de HTTP-POST para usar este método de extensión para rellenar la colección ModelState con nuestras infracciones de reglas de cena.

Completar las implementaciones del método de acción Edit

El código siguiente implementa toda la lógica del controlador necesaria para nuestro escenario de edición:

//
// GET: /Dinners/Edit/2

public ActionResult Edit(int id) {

    Dinner dinner = dinnerRepository.GetDinner(id);
    
    return View(dinner);
}

//
// POST: /Dinners/Edit/2

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    try {
    
        UpdateModel(dinner);

        dinnerRepository.Save();

        return RedirectToAction("Details", new { id=dinner.DinnerID });
    }
    catch {
    
        ModelState.AddRuleViolations(dinner.GetRuleViolations());

        return View(dinner);
    }
}

Lo bueno de nuestra implementación de edición es que ni nuestra clase Controller ni nuestra plantilla View tienen que saber nada sobre la validación específica o las reglas de negocio que aplica nuestro modelo Dinner. Podemos agregar reglas adicionales a nuestro modelo en el futuro y no es necesario realizar ningún cambio de código en nuestro controlador o vista para que se admitan. Esto nos proporciona la flexibilidad para evolucionar fácilmente nuestros requisitos de aplicación en el futuro con un mínimo de cambios de código.

Compatibilidad con Create

Hemos terminado de implementar el comportamiento "Edit" de nuestra clase DinnersController. Ahora vamos a implementar la compatibilidad con "Create" en ella, lo que permitirá a los usuarios agregar nuevas cenas.

Método de acción Create de HTTP-GET

Comenzaremos implementando el comportamiento HTTP "GET" de nuestro método de acción Create. Se llamará a este método cuando alguien visite la dirección URL /Dinners/Create. Nuestra implementación tiene el siguiente aspecto:

//
// GET: /Dinners/Create

public ActionResult Create() {

    Dinner dinner = new Dinner() {
        EventDate = DateTime.Now.AddDays(7)
    };

    return View(dinner);
}

El código anterior crea un nuevo objeto Dinner y asigna su propiedad EventDate para que sea una semana en el futuro. A continuación, representa una vista basada en el nuevo objeto Dinner. Dado que no hemos pasado explícitamente un nombre al método de asistente View(), usará la ruta de acceso predeterminada basada en convención para resolver la plantilla de vista: /Views/Dinners/Create.aspx.

Ahora vamos a crear esta plantilla de vista. Para ello, haga clic con el botón derecho en el método de acción de creación y seleccione el comando contextual "Agregar vista". En el cuadro de diálogo "Agregar vista", indicaremos que pasamos un objeto Dinner a la plantilla de vista y elegiremos aplicar scaffolding automático a una plantilla "Create":

Screenshot of Add view to create a view template.

Al hacer clic en el botón "Agregar", Visual Studio guardará una nueva vista "Create.aspx" basada en scaffolding en el directorio "\Views\Dinners" y la abrirá en el IDE:

Screenshot of the I D E to edit the code.

Vamos a realizar algunos cambios en el archivo de scaffolding "create" predeterminado que se generó para nosotros y modificarlo para que tenga un aspecto similar al siguiente:

<asp:Content ID="Title" ContentPlaceHolderID="TitleContent" runat="server">
     Host a Dinner
</asp:Content>

<asp:Content ID="Main" ContentPlaceHolderID="MainContent" runat="server">

    <h2>Host a Dinner</h2>

    <%=Html.ValidationSummary("Please correct the errors and try again.") %>
 
    <% using (Html.BeginForm()) {%>
  
        <fieldset>
            <p>
                <label for="Title">Title:</label>
                <%= Html.TextBox("Title") %>
                <%= Html.ValidationMessage("Title", "*") %>
            </p>
            <p>
                <label for="EventDate">EventDate:</label>
                <%=Html.TextBox("EventDate") %>
                <%=Html.ValidationMessage("EventDate", "*") %>
            </p>
            <p>
                <label for="Description">Description:</label>
                <%=Html.TextArea("Description") %>
                <%=Html.ValidationMessage("Description", "*") %>
            </p>
            <p>
                <label for="Address">Address:</label>
                <%=Html.TextBox("Address") %>
                <%=Html.ValidationMessage("Address", "*") %>
            </p>
            <p>
                <label for="Country">Country:</label>
                <%=Html.TextBox("Country") %>
                <%=Html.ValidationMessage("Country", "*") %>
            </p>
            <p>
                <label for="ContactPhone">ContactPhone:</label>
                <%=Html.TextBox("ContactPhone") %>
                <%=Html.ValidationMessage("ContactPhone", "*") %>
            </p>            
            <p>
                <label for="Latitude">Latitude:</label>
                <%=Html.TextBox("Latitude") %>
                <%=Html.ValidationMessage("Latitude", "*") %>
            </p>
            <p>
                <label for="Longitude">Longitude:</label>
                <%=Html.TextBox("Longitude") %>
                <%=Html.ValidationMessage("Longitude", "*") %>
            </p>
            <p>
                <input type="submit" value="Save"/>
            </p>
        </fieldset>
    <% } 
%>
</asp:Content>

Y ahora, cuando ejecutamos nuestra aplicación y accedemos a la dirección URL "/Dinners/Create" dentro del explorador, representará la interfaz de usuario como la siguiente desde nuestra implementación de la acción de creación:

Screenshot of Create action implementation when we run our application and access the Dinners U R L.

Implementación del método de acción de creación HTTP-POST

Tenemos la versión HTTP-GET del método de acción Create implementado. Cuando un usuario hace clic en el botón "Save", realiza una publicación de formulario en la dirección URL /Dinners/Create y envía los valores del formulario <input> HTML mediante el verbo HTTP POST.

Ahora vamos a implementar el comportamiento HTTP POST de nuestro método de acción Create. Comenzaremos agregando un método de acción "Create" sobrecargado a nuestro DinnersController que tiene un atributo "AcceptVerbs" en él que indica que controla escenarios HTTP POST:

//
// POST: /Dinners/Create

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create() {
    ...
}

Hay una variedad de formas de acceder a los parámetros de formulario publicados dentro de nuestro método "Create" habilitado para HTTP-POST.

Un enfoque consiste en crear un nuevo objeto Dinner y, a continuación, usar el método de asistente UpdateModel() (como hicimos con la acción de edición) para rellenarlo con los valores de formulario publicados. A continuación, podemos agregarlo a nuestra DinnerRepository, conservarlo en la base de datos y redirigir al usuario a nuestra acción Details para mostrar la cena recién creada con el código siguiente:

//
// POST: /Dinners/Create

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create() {

    Dinner dinner = new Dinner();

    try {
    
        UpdateModel(dinner);

        dinnerRepository.Add(dinner);
        dinnerRepository.Save();

        return RedirectToAction("Details", new {id=dinner.DinnerID});
    }
    catch {
    
        ModelState.AddRuleViolations(dinner.GetRuleViolations());

        return View(dinner);
    }
}

Como alternativa, podemos usar un enfoque en el que tenemos nuestro método de acción Create() para realizar un objeto Dinner como parámetro del método. ASP.NET MVC creará automáticamente una instancia de un nuevo objeto Dinner para nosotros, rellenará sus propiedades mediante las entradas del formulario y lo pasará al método de acción:

//
//
// POST: /Dinners/Create

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(Dinner dinner) {

    if (ModelState.IsValid) {

        try {
            dinner.HostedBy = "SomeUser";

            dinnerRepository.Add(dinner);
            dinnerRepository.Save();

            return RedirectToAction("Details", new {id = dinner.DinnerID });
        }
        catch {        
            ModelState.AddRuleViolations(dinner.GetRuleViolations());
        }
    }
    
    return View(dinner);
}

Nuestro método de acción anterior comprueba que el objeto Dinner se ha rellenado correctamente con los valores de publicación del formulario comprobando la propiedad ModelState.IsValid. Esto devolverá false si hay problemas de conversión de entrada (por ejemplo, una cadena de "BOGUS" para la propiedad EventDate) y, si hay algún problema, el método de acción vuelve a mostrar el formulario.

Si los valores de entrada son válidos, el método de acción intenta agregar y guardar la nueva cena en DinnerRepository. Encapsula este trabajo dentro de un bloque try/catch y vuelve a reproducir el formulario si hay infracciones de reglas de negocio (lo que provocaría que el método dinnerRepository.Save() genere una excepción).

Para ver este comportamiento de control de errores en acción, podemos solicitar la dirección URL /Dinners/Create y rellenar los detalles sobre una nueva cena. La entrada o los valores incorrectos harán que el formulario de creación se vuelva a reproducir con los errores resaltados, como se indica a continuación:

Screenshot of the form redisplayed with errors highlighted.

Observe cómo nuestro formulario Create respeta exactamente las mismas reglas de validación y negocio que nuestro formulario Edit. Esto se debe a que nuestras reglas de negocio y validación se definieron en el modelo y no se insertaron dentro de la interfaz de usuario o el controlador de la aplicación. Esto significa que más adelante podemos cambiar o evolucionar nuestras reglas de validación o de negocio en un solo lugar y hacer que se apliquen a lo largo de nuestra aplicación. No tendremos que cambiar ningún código dentro de nuestros métodos de acción Edit o Create para respetar automáticamente las nuevas reglas o modificaciones en las existentes.

Al corregir los valores de entrada y hacer clic de nuevo en el botón "Save", nuestra adición a DinnerRepository se realizará correctamente y se agregará una nueva cena a la base de datos. A continuación, se le redirigirá a la dirección URL de /Dinners/Details/[id], donde se mostrarán detalles sobre la cena recién creada:

Screenshot of the newly created Dinner.

Compatibilidad con Delete

Ahora vamos a agregar compatibilidad con "Delete" aDinnersController.

Método de acción Delete de HTTP-GET

Comenzaremos implementando el comportamiento HTTP GET del método de acción Delete. Se llamará a este método cuando alguien visite la dirección URL /Dinners/Delete/[id]. A continuación se muestra la implementación:

//
// HTTP GET: /Dinners/Delete/1

public ActionResult Delete(int id) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    if (dinner == null)
         return View("NotFound");
    else
        return View(dinner);
}

El método de acción intenta recuperar la cena que se va a eliminar. Si la cena existe, representa una vista basada en el objeto Dinner. Si el objeto no existe (o ya se ha eliminado), devuelve una vista que representa la plantilla de vista "NotFound" que creamos anteriormente para el método de acción "Details".

Podemos crear la plantilla de vista "Delete" haciendo clic con el botón derecho en el método de acción Delete y seleccionando el comando contextual "Agregar vista". En el cuadro de diálogo "Agregar vista", indicaremos que pasamos un objeto Dinner a nuestra plantilla de vista como modelo y elegiremos crear una plantilla vacía:

Screenshot of creating the Delete view template as an an empty template.

Al hacer clic en el botón "Agregar", Visual Studio nos creará un nuevo archivo de plantilla de vista "Details.aspx" en nuestro directorio "\Views\Dinners": Agregaremos código y HTML a la plantilla para implementar una pantalla de confirmación de eliminación como la siguiente:

<asp:Content ID="Title" ContentPlaceHolderID="TitleContent" runat="server">
    Delete Confirmation:  <%=Html.Encode(Model.Title) %>
</asp:Content>

<asp:Content ID="Main" ContentPlaceHolderID="MainContent" runat="server">

    <h2>
        Delete Confirmation
    </h2>

    <div>
        <p>Please confirm you want to cancel the dinner titled: 
           <i> <%=Html.Encode(Model.Title) %>? </i> 
        </p>
    </div>
    
    <% using (Html.BeginForm()) {  %>
        <input name="confirmButton" type="submit" value="Delete" />        
    <% } %>
     
</asp:Content>

El código anterior muestra el título de la cena que se va a eliminar y genera un elemento <form> que realiza una publicación en la dirección URL /Dinners/Delete/[id] si el usuario final hace clic en el botón "Delete" dentro de él.

Cuando ejecutamos nuestra aplicación y accedemos a la dirección URL "/Dinners/Delete/[id]" de un objeto Dinner válido, representa la interfaz de usuario como se indica a continuación:

Screenshot of the Dinner delete confirmation U I in the H T T P G E T Delete action method.

Tema secundario: ¿Por qué estamos haciendo un POST?
Podría preguntar: ¿por qué hemos recorrido el esfuerzo de crear un <form> en nuestra pantalla de confirmación Delete? ¿Por qué no usar un hipervínculo estándar para vincular a un método de acción que realiza la operación de eliminación real? El motivo es que queremos tener cuidado de protegerse contra los rastreadores web y los motores de búsqueda que detectan nuestras direcciones URL y provocan que los datos se eliminen involuntariamente cuando siguen los vínculos. Las direcciones URL basadas en HTTP-GET se consideran "seguras" para que tengan acceso o rastreo, y se supone que no siguen a las HTTP-POST. Una buena regla es asegurarse de que siempre coloca operaciones destructivas o de modificación de datos detrás de solicitudes HTTP-POST.

Implementación del método de acción Delete de HTTP-POST

Ahora tenemos la versión HTTP-GET del método de acción Delete implementado que muestra una pantalla de confirmación de eliminación. Cuando un usuario final hace clic en el botón "Delete", realizará una publicación de formulario en la dirección URL /Dinners/Dinner/[id].

Ahora vamos a implementar el comportamiento HTTP "POST" del método de acción Delete mediante el código siguiente:

// 
// HTTP POST: /Dinners/Delete/1

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Delete(int id, string confirmButton) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    if (dinner == null)
        return View("NotFound");

    dinnerRepository.Delete(dinner);
    dinnerRepository.Save();

    return View("Deleted");
}

La versión HTTP-POST del método de acción Delete intenta recuperar el objeto Dinner que se va a eliminar. Si no lo encuentra (porque ya se ha eliminado), representa nuestra plantilla "NotFound". Si encuentra la cena, la elimina de DinnerRepository. A continuación, representa una plantilla "Deleted".

Para implementar la plantilla "Deleted", haremos clic con el botón derecho en el método de acción y elegiremos el menú contextual "Agregar vista". Asignaremos un nombre a la vista "Deleted" y tendrá que ser una plantilla vacía (y no tomaremos un objeto de modelo fuertemente tipado). A continuación, agregaremos contenido HTML:

<asp:Content ID="Title" ContentPlaceHolderID="TitleContent" runat="server">
    Dinner Deleted
</asp:Content>

<asp:Content ID="Main" ContentPlaceHolderID="MainContent" runat="server">

    <h2>Dinner Deleted</h2>

    <div>
        <p>Your dinner was successfully deleted.</p>
    </div>
    
    <div>
        <p><a href="/dinners">Click for Upcoming Dinners</a></p>
    </div>
    
</asp:Content>

Y ahora, cuando ejecutamos nuestra aplicación y accedemos a la dirección URL "/Dinners/Delete/[id]" para un objeto Dinner válido, se representará nuestra pantalla de confirmación de eliminación de cena como la siguiente:

Screenshot of the Dinner delete confirmation screen in the H T T P P O S T Delete action method.

Al hacer clic en el botón "Delete", se realizará una solicitud HTTP-POST en la dirección URL /Dinners/Delete/[id], que eliminará la cena de nuestra base de datos y mostrará la plantilla de vista "Deleted":

Screenshot of the Deleted view template.

Seguridad de enlace de modelos

Hemos analizado dos maneras diferentes de usar las características integradas de enlace de modelos de ASP.NET MVC. El primero que usa el método UpdateModel() para actualizar las propiedades de un objeto de modelo existente y el segundo mediante la compatibilidad de ASP.NET MVC para pasar objetos de modelo como parámetros de método de acción. Ambas técnicas son muy eficaces y extremadamente útiles.

Este poder también conlleva la responsabilidad. Es importante ser siempre muy precavido con la seguridad cuando se acepta cualquier entrada de usuario, y esto también es cierto cuando se vinculan objetos a la entrada de formularios. Debe tener cuidado de codificar siempre en HTML cualquier valor introducido por el usuario para evitar ataques de inyección HTML y JavaScript, y tener cuidado con los ataques por inyección de código SQL (nota: estamos utilizando LINQ to SQL para nuestra aplicación, que codifica automáticamente los parámetros para evitar este tipo de ataques). Nunca debe confiar solo en la validación del lado cliente y emplear siempre la validación del lado servidor para protegerse contra los hackers que intentan enviar valores falsos.

Un elemento de seguridad adicional para asegurarse de que piensa al usar las características de enlace de ASP.NET MVC es el ámbito de los objetos que está enlazando. En concreto, quiere asegurarse de que comprende las implicaciones de seguridad de las propiedades que permite enlazar y asegúrese de que solo permita que se actualicen esas propiedades que realmente deberían poder ser actualizadas por el usuario final.

De forma predeterminada, el método UpdateModel() intentará actualizar todas las propiedades del objeto de modelo que coincidan con los valores de parámetro de formulario entrantes. Del mismo modo, los objetos pasados como parámetros de método de acción también de forma predeterminada pueden tener todas sus propiedades establecidas a través de parámetros de formulario.

Bloqueo del enlace por uso

Puede bloquear la directiva de enlace por uso proporcionando una "lista de inclusión" explícita de propiedades que se pueden actualizar. Para ello, pase un parámetro de matriz de cadena adicional al método UpdateModel() como se indica a continuación:

string[] allowedProperties = new[]{ "Title","Description", 
                                    "ContactPhone", "Address",
                                    "EventDate", "Latitude", 
                                    "Longitude"};
                                    
UpdateModel(dinner, allowedProperties);

Los objetos pasados como parámetros de método de acción también admiten un atributo [Bind] que permite especificar una "lista de inclusión" de propiedades permitidas, como se indica a continuación:

//
// POST: /Dinners/Create

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create( [Bind(Include="Title,Address")] Dinner dinner ) {
    ...
}

Bloqueo del enlace por tipo

También puede bloquear las reglas de enlace por tipo. Esto le permite especificar las reglas de enlace una vez y, a continuación, aplicarlas en todos los escenarios (incluidos los escenarios de parámetros del método UpdateModel y de acción) en todos los controladores y métodos de acción.

Puede personalizar las reglas de enlace por tipo agregando un atributo [Bind] a un tipo o registrándolo en el archivo Global.asax de la aplicación (útil para escenarios en los que no posee el tipo). A continuación, puede usar las propiedades Include y Exclude del atributo Bind para controlar qué propiedades se pueden enlazar para la clase o interfaz determinada.

Usaremos esta técnica para la clase Dinner en nuestra aplicación NerdDinner y agregaremos un atributo [Bind] a ella que restrinja la lista de propiedades enlazables a lo siguiente:

[Bind(Include="Title,Description,EventDate,Address,Country,ContactPhone,Latitude,Longitude")]
public partial class Dinner {
   ...
}

Observe que no estamos permitiendo que la colección de RSVP se manipule a través del enlace, ni estamos permitiendo que las propiedades DinnerID o HostedBy se establezcan a través del enlace. Por motivos de seguridad, solo manipularemos estas propiedades concretas mediante código explícito dentro de nuestros métodos de acción.

Encapsulado CRUD

ASP.NET MVC incluye una serie de características integradas que ayudan a implementar escenarios de publicación de formularios. Usamos una variedad de estas características para proporcionar compatibilidad con la interfaz de usuario CRUD sobre nuestro DinnerRepository.

Estamos usando un enfoque centrado en el modelo para implementar nuestra aplicación. Esto significa que toda la lógica de reglas de negocios y validación se define dentro de nuestra capa de modelo, y no dentro de nuestros controladores o vistas. Ni nuestra clase Controller ni nuestras plantillas View saben nada sobre las reglas de negocio específicas que aplica nuestra clase de modelo Dinner.

Esto mantendrá la arquitectura de la aplicación limpia y facilita la prueba. Podemos agregar reglas de negocio adicionales a nuestra capa de modelo en el futuro y no tener que realizar ningún cambio de código en nuestro controlador o vista para que sean compatibles. Esto nos proporcionará una gran agilidad para evolucionar y cambiar nuestra aplicación en el futuro.

Nuestro DinnersController ahora habilita las listas o detalles de la cena, así como la compatibilidad con Create, Edit y Delete. El código completo de la clase se puede encontrar a continuación:

public class DinnersController : Controller {

    DinnerRepository dinnerRepository = new DinnerRepository();

    //
    // GET: /Dinners/

    public ActionResult Index() {

        var dinners = dinnerRepository.FindUpcomingDinners().ToList();
        return View(dinners);
    }

    //
    // GET: /Dinners/Details/2

    public ActionResult Details(int id) {

        Dinner dinner = dinnerRepository.GetDinner(id);

        if (dinner == null)
            return View("NotFound");
        else
            return View(dinner);
    }

    //
    // GET: /Dinners/Edit/2

    public ActionResult Edit(int id) {

        Dinner dinner = dinnerRepository.GetDinner(id);
        return View(dinner);
    }

    //
    // POST: /Dinners/Edit/2

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Edit(int id, FormCollection formValues) {

        Dinner dinner = dinnerRepository.GetDinner(id);

        try {
            UpdateModel(dinner);

            dinnerRepository.Save();

            return RedirectToAction("Details", new { id= dinner.DinnerID });
        }
        catch {
            ModelState.AddRuleViolations(dinner.GetRuleViolations());

            return View(dinner);
        }
    }

    //
    // GET: /Dinners/Create

    public ActionResult Create() {

        Dinner dinner = new Dinner() {
            EventDate = DateTime.Now.AddDays(7)
        };
        return View(dinner);
    }

    //
    // POST: /Dinners/Create

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Create(Dinner dinner) {

        if (ModelState.IsValid) {

            try {
                dinner.HostedBy = "SomeUser";

                dinnerRepository.Add(dinner);
                dinnerRepository.Save();

                return RedirectToAction("Details", new{id=dinner.DinnerID});
            }
            catch {
                ModelState.AddRuleViolations(dinner.GetRuleViolations());
            }
        }

        return View(dinner);
    }

    //
    // HTTP GET: /Dinners/Delete/1

    public ActionResult Delete(int id) {

        Dinner dinner = dinnerRepository.GetDinner(id);

        if (dinner == null)
            return View("NotFound");
        else
            return View(dinner);
    }

    // 
    // HTTP POST: /Dinners/Delete/1

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Delete(int id, string confirmButton) {

        Dinner dinner = dinnerRepository.GetDinner(id);

        if (dinner == null)
            return View("NotFound");

        dinnerRepository.Delete(dinner);
        dinnerRepository.Save();

        return View("Deleted");
    }
}

siguiente paso

Ahora tenemos compatibilidad básica con CRUD (crear, leer, actualizar y eliminar) en nuestra clase DinnersController.

Ahora veamos cómo podemos usar las clases ViewData y ViewModel para habilitar una interfaz de usuario aún más completa en nuestros formularios.