jueves, 26 de julio de 2012

Introducción a Knockout (VII) - La propiedad $index

El otro día estaba revisando la documentación de binding foreach de knockout cuando recordé que una de las novedades que había traído la versión 2.1.0 era la inclusión de la propiedad $index dentro del contexto foreach. Pensando que utilidad podría tener dentro de una aplicación real y que no fuera la de simplemente mostrar un índice ya que eso lo podemos tener usando <ol>.

Pensando un poco se me ocurrió un ejemplo donde nos podría ser útil esta nueva propiedad, así que supongamos que tenemos una colección de elementos de este tipo
public class Item
{
 public int IdItem { get; set; }
 public string Description { get; set; }
 public int Quantity { get; set; }
}
Esta colección de elementos se la tenemos que presentar al usuario y este debe escribir las cantidades que quiere para cada Item, y enviarla de nuevo al servidor. Se podría ver como un versión muy reducida de un carrito de la compra, donde tenemos los elementos que queremos comprar y podemos modificar la cantidad de elementos. Bien, para mostrar esta información y para simplificar el ejemplo he creado la siguiente modelo y acciones

Modelo
public class Model
{
 public List<Item> Items { get; set; }
}
Acciones
public ActionResult Index()
{
 Model model = new Model();
 FillModel(model);

 return View(model);
}

[HttpPost()]
public ActionResult Index(Model model)
{
 return RedirectToAction("Index");
}

private void FillModel(Model model)
{
 model.Items = new List<Item>();
 for (int i = 1; i <= 10; i++)
  model.Items.Add(new Item() { IdItem = i, Description = string.Format("Item{0}", i), Quantity = 0 });
}
Vista
<h2>Items</h2>
@using (Html.BeginForm())
{ 
 for(int i = 0; i < Model.Items.Count; i++)
 {
  <div>
   @Html.HiddenFor(m => Model.Items[i].IdItem)
   @Html.HiddenFor(m => Model.Items[i].Description)
   @Model.Items[i].Description
   @Html.TextBoxFor(m => Model.Items[i].Quantity)
  </div>
 }
 <input type="submit" value="Enviar" />
}
El aspecto de este formulario sería el siguiente

Vista

Bien, si modificamos alguna de las cantidades y le damos a enviar, obtendremos esto en la controladora

Post

Como vemos el valor introducido por el usuario (10)  ha sido "bindeado" en nuestro modelo sin problemas. Bien, ahora supongamos que queremos hacer lo mismo pero usando knockout. Para eso, debemos modificar ligeramente la vista de la siguiente manera.
@using (Html.BeginForm())
{ 
 <!-- ko foreach: items -->
 <div>
  <input type="hidden" data-bind="value: IdItem"  />
  <input type="hidden" data-bind="value: Description"  />
  <span data-bind="text: Description"></span>
  <input type="text" data-bind="value: Quantity"  />
 </div>
 <!-- /ko -->
 <input type="submit" value="Enviar" />
}

<script type="text/javascript">
 $(document).ready(function () {
  function ViewModel(model) {
   var self = this;

   self.items = ko.observableArray(model.Items);
  }
  var viewModel = new ViewModel(@Html.Raw(Json.Encode(Model)));
  ko.applyBindings(viewModel);
 });
</script>
Con esto veremos que nuestra vista es igual a la obtenida anteriormente, pero al enviar el form nos daremos cuenta que nuestro modelo no se "bindea" correctamente.


Si sabemos como funciona el bind de ASP.NET MVC veremos que este error es lógico ya que los elementos que estamos poniendo en nuestra vista no tienen el atributo name definido, pero además hay que tener en cuenta que estamos intentando "bindear" una colección por lo que tendremos que especificar el índice que ocupa el elemento en la colección. Para esto es para lo que viene de maravilla el uso de la propiedad $index dentro del contexto foreach (no es que antes no se pudiera hacer, sino que era más complicado). Con esto la vista quedaría de la siguiente manera
<!-- ko foreach: items -->
<div>
 <input type="hidden" data-bind="value: IdItem, attr: {name: 'Items['+$index()+'].IdItem'}"  />
 <input type="hidden" data-bind="value: Description, attr: {name: 'Items['+$index()+'].Description'}"  />
 <span data-bind="text: Description"></span>
 <input type="text" data-bind="value: Quantity, attr: {name: 'Items['+$index()+'].Quantity'}"  />
</div>
<!-- /ko -->
Esto generaría el siguiente código html que vemos que cumple con lo dicho anteriormente

Vista HTML

Y si hacemos el post veremos que el modelo se ha "bindeado" de nuevo correctamente



NOTA
Hay otra forma de generar la vista de una manera más "elegante" y evitando así tener que usar un bucle y teniendo a nuestra alcance el Intellisense es la siguiente. Creamos una página parcial (Partial Page) llamada Item.cshtml  (mismo nombre que la clase que contiene la colección) en el directorio View/Home/RenderPartials o Shared/RenderPartials y en ella pondremos lo siguiente
@model test.web.mvc.cs.Models.Item

<div>
 @Html.HiddenFor(m => m.IdItem)
 @Html.HiddenFor(m => m.Description)
 @Model.Description
 @Html.TextBoxFor(m => m.Quantity)
</div>
Luego en la vista tan solo deberemos poner lo siguiente
@using (Html.BeginForm())
{ 
 @Html.EditorFor(x => x.Items)
 <input type="submit" value="Enviar" />
}

Happy coding!

No hay comentarios:

Publicar un comentario