viernes, enero 27, 2012

Guardados parciales de formularios web mediante Ajax

La forma de guardar un formulario web sin hacer un submit y por lo tanto se recargue la página, es mediante ajax. Esto puede ser útil si queremos que lo que está haciendo el usuario no se pierda si por accidente sale de la página. Por último tendremos que decidir cuándo hacer estos guardados parciales: cada cierto tiempo, cuando cambie cierto valor...

Código javascript:


Untitled Page
// Variable donde se guardan los valores de la última grabación automática.
var _savedFormValues;
 
// Carga del formulario (jQuery)
$(document).ready(function () {
 
    // Guardado automático (lanzado por el sistema). Mientras no se diga lo contrario (=0) los guardados son de tipo automático (=-1)
    $('#GuardadoAutomatico').value = -1;
 
    // Inicializar campos _savedFormValues
    _savedFormValues = $(document.forms[0]).serialize();
}
 
// Guardado asíncrono (no se espera al retorno)
function guardarAutomatico() {
 
    // Serializar los valores actuales de la pantalla
    currentFormValues = $(document.forms[0]).serialize();
 
    // Comparar los valores actuales con los últimos guardados
    if (currentFormValues != _savedFormValues) {
 
        // Lanzar POST al controller, mediante ajax (asíncrono y transparente para el usuario)
        $.ajax({
            type: 'POST',
            url: $(this).attr('action'),
            cache: false,
            data: currentFormValues
            //,success: function (data) { 
             //$('#result').html(data); // result es un campo del formulario donde escribir el retorno del POST; en este caso no se utiliza.
            //}
        });
 
        // Actualizar los últimos valores guardados
        _savedFormValues = currentFormValues;
    }
}
 

 
NOTAS: Obsérvese...
1) Cómo se recogen todos los valores del formulario y se serializan para pasarlos al post: $(document.forms[0]).serialize()
2) Si no hay cambios, no se hace el post.
3) El campo GuardadoAutomatico, con el que controlaremos si el guardado es automático/parcial o no. Sólo en el guardado final deberemos establecerlo a 0.


 
 
El controller sería algo así:


 
[HttpPost]
public ActionResult Edit(MiViewModel viewModel)
{ 
    ...

    if (viewModel.GuardadoAutomatico == -1) 
    {
        // Guardar parcial automático 
        ...

        // No redirigir
        return null;
    }
    else 
    {
        // Guardar normal lanzado por el usuario 
        ...
 
        // Redirigir a donde toque...
        return RedirectToAction("Index""PáginaX");
    }
} 
 

Gestión de cookies en javascript


Funciones javascript para leer, crear o borrar cookies (Copiado de http://www.quirksmode.org/js/cookies.html):

function createCookie(name, value, days) {
    var expires = "";
    if (days) {
        var date = new Date();
        date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
        expires = "; expires=" + date.toGMTString();
    }
    document.cookie = name + "=" + value + expires + "; path=/";
}
 
function readCookie(name) {
    var nameEQ = name + "=";
    var ca = document.cookie.split(';');
    for (var i = 0; i < ca.length; i++) {
        var c = ca[i];
        while (c.charAt(0) == ' ') c = c.substring(1, c.length);
        if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
    }
    return null;
}
 
function eraseCookie(name) {
    createCookie(name, "", -1);
}

jueves, enero 12, 2012

Combos en cascada en ASP.Net MVC Razor mediante Ajax y Json utilizando Ajax.BeginForm

Untitled Page
@model IEnumerable<Dream.Models.Articulo>
 
@{
    ViewBag.Title = "Referencias";
    Layout = "~/Views/Shared/_Layout.cshtml";
}
 
<script src="@Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript"></script>
 
<script type="text/javascript">
    $.ajaxSetup({
        cache: false
    });
 
    // Inicialización
    $(document).ready(function () {
        _$("comboGruposDeProductos").options[0].disabled = true;
        _$("comboProductos").disabled = true;
    });
    
    // Carga el desplegable de productos en función
    // del grupo que le llega como parámetro.
    function actualizaComboProductos(grupoDeProductos) {
 
        // Eliminar el elemento "0" = "<Selecciona grupo>" si es que aún existe
        if (_$("comboGruposDeProductos").options[0].value == "0")
            _$("comboGruposDeProductos").options[0] = null;
 
        // Limpiar combo de Productos y poner "Espere..." mientras se carga.
        var dd = _$("comboProductos");
        dd.options.length = 0;
        dd.options[0] = new Option("Espere...");
        dd.selectedIndex = 0;
        dd.disabled = true;
 
        // Control de errores
        $("#comboProductos").ajaxError(function (event, request, settings) {
            dd.options[0] = new Option("Grupo incorrecto");
        });
 
        // Obtenemos los datos...
        var selectedProductIndex = 0;
        $.getJSON(
            '@Url.Action("GetJsonProductSelectList")',  // URL a la acción
            {grupo: grupoDeProductos },                // Objeto JSON con parámetros
            function (data) {                           // Función de retorno exitoso
                dd.options[0] = new Option("<Selecciona producto>""0");
                dd.options[0].disabled = true;
                $.each(data, function (i, item) {
                    dd.options[i + 1] = new Option(item.Text, item.Value);
                });
                dd.selectedIndex = selectedProductIndex;
                dd.disabled = false;
            }
        );
    }
 
    //Recargar lista de artículos: se utiliza submit para que MVC haga una llamada ajax
    function actualizaListaDeArticulos() {
        $('form').submit();
 
        // Eliminar el elemento "0" = "<Selecciona producto>" si es que aún existe
        if (_$("comboProductos").options[0].value == "0")
            _$("comboProductos").options[0] = null;
    }
    
    function _$(id) {
        return document.getElementById(id);
    }
</script>
 
<h3>@Request.Params["vehiculo"]</h3>
 
@using (Ajax.BeginForm("ItemListGrid", 
    new { idCar = int.Parse(Request["idCar"]) },
    new AjaxOptions { 
        UpdateTargetId = "divReferencias", 
        LoadingElementId = "divCargando", 
        HttpMethod = "GET", 
        InsertionMode = InsertionMode.Replace,
        OnBegin = "_$(\"divReferencias\").style.display = \"none\";",
        OnComplete = "_$(\"divReferencias\").style.display = \"block\";"
    },
    new { 
        name = "formReferencias",
        style = "display:fixed"
    }))
{
    @Html.DropDownList("comboGruposDeProductos", (SelectList)ViewData["ProductGroupSelectList"], new
    {
        onchange = "actualizaComboProductos(this.options[this.selectedIndex].value)",
        style = "width: 300px; display:fixed"
    })
 
    var productSelectList = new List<SelectListItem>(); // De entrada no necesito nada en el 2º combo...
    @Html.DropDownList("comboProductos", productSelectList, new
    {
        onchange = "actualizaListaDeArticulos()",
        style = "width: 400px"
    })
}
 
<div id="divReferencias" class="tabletGrid">
    @Html.Partial("ItemListGrid", Model)
</div>
                            
<!--div id="divCargando" class="divCargando"><p>Cargando...</p><img src=@Url.Content("~/Images/cargando2.gif") alt="Cargando..." title="Cargando..." /></div!!!-->
 
 
Otras entradas del blog relacinadas:

Carga de combos en cascada en página web mediante Ajax y Json

Actualización de datos en página web mediante AJAX y JSON

 
 

Ejemplo de uso de WebGrid en Razor

Untitled Page

Definición del HTML que contiene un grid, utilizando sintaxis Razor

@model IEnumerable<Dream.Models.Articulo>
 
@{
    if (Model != null)
    {
        var grid = new WebGrid(
            source: Model,
            canPage: false,
            canSort: false
            );
 
            @grid.GetHtml(
                //rowStyle: @MyHelpers.Utils.GetArticleLineStyleClass(p => p.codProdAE, 5),
                //"font-size:1.3em",
                columns: new[] {
                    grid.Column("marca""Fabricante",
                    format: p => @MyHelpers.HtmlHelpers.GetArticleLineStyleClass(p.codProdAE, p.stock, p.marca),
                    style: "leftAlign"
                    ),
                    grid.Column("articulo""Ref. Fabricante",
                    format: p => @MyHelpers.HtmlHelpers.GetArticleLineStyleClass(p.codProdAE, p.stock, p.articulo),
                    style: "leftAlign"
                    ),
                    grid.Column("codProdAE""Ref. Autoequip",
                    format: p => @MyHelpers.HtmlHelpers.GetArticleLineStyleClass(p.codProdAE, p.stock, p.codProdAE, false),
                    style: "centerAlign200"
                    ),
                    grid.Column("stock""Stock", 
                    format: p => @MyHelpers.HtmlHelpers.GetArticleLineStyleClass(p.codProdAE, p.stock, p.stock.ToString(), false),
                    //format: p => (p.codProdAE == "") ? "" : p.stock.ToString() + "  ",
                    style: "centerAlign50"
                    ) /*,
                    grid.Column("precioConIVA", "P.V.P.",
                    format: p => @MyHelpers.Utils.GetArticleLineStyleClass(p.codProdAE, p.stock, p.precioConIVA.ToString() + " €", false),
                    style: "rightAlign"
                    )*/
            }
        )
    }
}

Helper que retorna un <p> con diferentes class en función de los parámetros que se le pasen:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
 
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
namespace MyHelpers
{
 public static class HtmlHelpers
 {
  public static MvcHtmlString GetArticleLineStyleClass(string codProdAE, decimal stock, string text, bool showIfThereIsNoCodProd = true)
  {
   var builder_p = new TagBuilder("p") { InnerHtml = text };
 
   if (codProdAE == null)
    if (!showIfThereIsNoCodProd)
     builder_p.InnerHtml = "";
    else
     builder_p.Attributes.Add("class""gridRowWithoutCodProdAutoequip");
   else if (stock == 0)
    builder_p.Attributes.Add("class""gridRowWithoutStock");
   else
    builder_p.Attributes.Add("class""gridRowWithStock");
 
   return MvcHtmlString.Create(builder_p.ToString(TagRenderMode.Normal));
  }
        }
}

    
 

Funciones javascript para formatear campos númericos con decimales

Untitled Page
 // Formatear campos numéricos
 function formatNumber(input, maxNumber) {
  var num = input.value.replace(/\./g, '');
  num = num.replace(/\D/g, '')
  if (!isNaN(num)) {
   if (num > maxNumber)
   {
    num = num.toString().substring(0, maxNumber.toString().length);
    if (num > maxNumber)
     num = num.toString().substring(0, maxNumber.toString().length - 1);
   }
   num = num.toString().split('').reverse().join('').replace(/(?=\d*\.?)(\d{3})/g, '$1.');
   num = num.split('').reverse().join('').replace(/^[\.]/, '');
   input.value = num;
  }
 }
 
 // Formatear campos numéricos con decimales
 function formatNumber2(input, numberOfDigitsBeforeComma, numberOfDigitsAfterComma, completeDecimalsWithZeros) {
  var num = input.value.replace(/\./g, ',');
  var numparts = num.split(',');
  numparts[0] = numparts[0].replace(/\D/g, ''); //Elimina cualquier cosa que no sea un carácter numérico
  if (numparts[0].length > 1) //Elimina 0 delante de otro carácter numérico
   if (numparts[0].substr(0, 1) == 0)
    numparts[0] = numparts[0].substr(1, numparts[0].length - 1);
  
  if (numparts[0].length > numberOfDigitsBeforeComma) {
   //Un bug de Android provoca que al picar 1234 se ponga 12,43!!!
   if (numparts.length == 1) { // Poner la coma automáticamente
    numparts = (numparts[0].substr(0,numberOfDigitsBeforeComma) + ',' + numparts[0].substr(numberOfDigitsBeforeComma,1)).split(',');
   }
   else {
    numparts[0] = numparts[0].substr(0,numberOfDigitsBeforeComma);
   }
  }
  if (numparts.length >= 2) {
   numparts[1] = numparts[1].replace(/\D/g, '');
   if (numparts[1].length > numberOfDigitsAfterComma)
    numparts[1] = numparts[1].substr(0,numberOfDigitsAfterComma);
  }
  if (completeDecimalsWithZeros == true) {
   if (numparts.length >= 2) {
    if (numparts[0].length == 0)
     numparts[0] = '0';
    numparts[1] = (numparts[1] + '00').substring(0,2);
    input.value = numparts[0] + ',' + numparts[1];
   }
   else {
    if (numparts[0].length > 0)
     input.value = numparts[0] + ',00';
    else
     input.value = '0,00';
   }
  }
  else {
   if (numparts.length >= 2)
    input.value = numparts[0] + ',' + numparts[1];
   else
    input.value = numparts[0];
  }
 }
 
 // Formatear campos numéricos con decimales
 function formatNumber3(input, numberOfDigitsBeforeComma, numberOfDigitsAfterComma, deleteCommaIfNotDecimals) {
  var num = input.value.replace(/\./g, ',');
  var numparts = num.split(',');
  numparts[0] = numparts[0].replace(/\D/g, '');
  if (numparts[0].length > numberOfDigitsBeforeComma)
   numparts[0] = numparts[0].substring(0,numberOfDigitsBeforeComma);
  if (numparts.length > 1) {
   numparts[1] = numparts[1].replace(/\D/g, '');
   if (numparts[1].length > numberOfDigitsAfterComma)
    numparts[1] = numparts[1].substring(0,numberOfDigitsAfterComma);
   if (numparts[1].length == 0 && deleteCommaIfNotDecimals == true) {
    num = numparts[0];
   }
   else {
    num = numparts[0] + ',' + numparts[1];
   }
  }
  else {
   num = numparts[0];
  }
  input.value = num;
 }

Carga de combos en cascada en página web mediante Ajax y Json

Untitled Page

cuestión a resolver

Imaginemos que queremos cargar 3 combos en cascada, que contienen respectivamente continentes, países y ciudades: Cuando el usuario cambie el valor del primer combo (continente) hay que cambiar el contenido del combo de países, y cuando cambie el de países, hay que cambiar el de las ciudades.

Esta otra entrada de mi blog hace referencia a algunos de los conceptos que aquí se utilizan: Actualización de datos en página web mediante AJAX y JSON

 

parte cliente

Definición de los combos EN Html

 

<select name="cmbContinentes" id="continentes" class="combosLugar"></select>
<select name="cmbPaises" id="paises" class="combosLugar "></select>
<select name="cmbCiudades" id="ciudades" class="combosLugar"></select>

        

eL CÓDIGO Javascript

1.       En la carga de la pantalla (con jQuery) añadir la carga del primer combo que a su vez desencadena la carga de los siguientes, y el evento .change para los elementos con class="comboDeLugar" (es decir, nuestros combos).

$(document).ready(function () {

 

    // Cargar 1er combo (que a su vez desencadena la carga de los demás)
    cargaCombo("cmbContinentes"'../../Lugar/Continentes'null);
 
    // Definición evento OnChange para los combos de medida de neumáticos
    $(".combosLugar").change(function (e) {
        cargaSiguienteCombo(this.id)
    });
 
    // Inicialización (opcional). Sólo el primer combo a modo de ejemplo.
    $("#comboContinentes")[0].value = "cmbContinentes"
  

}

 

2.       Definición de la función que carga un combo mediante Ajax y Json

    // Cargar combo de forma asíncrona, utilizando ajax y formato json.
    function cargaCombo(cmb, url, params) {
        $.ajax({
            url: url,
            data: params,
            dataType: 'json',
            success: (function (data) {
                $("#" + cmb).html('');
                $.each(data, function (i, item) {
                    $("#" + cmb).append("<option value='" +
                        item.Value + "'>" + item.Text + "</option>");
                });
                cargaSiguienteCombo(cmb);
            })
        });
    }

 

3.       Definición de la función que carga el siguiente combo.

    // Cargar el siguiente combo
    function cargaSiguienteCombo(changedCombo) {
 
        if (changedCombo == "") {
            return cargaCombo(
                "cmbContinentes",
                '../../Lugar/Continentes',
                null
            );
        }
 
        if (changedCombo == "cmbContinentes") {
            return cargaCombo(
                "cmbPaises",
                '../../Lugar/Paises',
                "continente=" + $("#cmbContinentes")[0].value
            );
        }
 
        if (changedCombo == "cmbPaises") {
            return cargaCombo(
                "cmbCiudades",
                '../../Lugar/Ciudades', 
                "pais=" + $("#comboPaises")[0].value
            );
        }
    }

 

parte servidor: la clase que recibe las peticiones ajax

Partimos de la base de que utilizamos ASP.Net MVC 3.

La definición de las entidades Continente, Pais y Ciudad no se describe en este documento, porque son muy simples. Sólo indicaré que están ligadas a sendas tablas cuyos campos son:

·         Continentes: Nombre

·         Paises: Nombre, Continente

·         Ciudades: Nombre, Pais

La definición de la clase que recibe las peticiones Ajax desde el código javascript de la página web, es la siguiente:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Dream.Models;
 
namespace MiProyecto.Controllers
{
    public class LugarController : Controller
    {
        private MisEntities db = new MisEntities();
         
        /// <summary>
        /// Retorna los continentes, hardcoded (no desde BDD)
        /// </summary>
        /// <returns></returns>
        public JsonResult Continentes()
        {
 
            var list = new List<object>() {
                new { Text = "Europa", Value = "Europa" },
                new { Text = "América", Value = "América" },
                new { Text = "Asia", Value = "Asia" },
                new { Text = "África", Value = "África" },
                new { Text = "Oceanía", Value = "Oceanía" },
            };
 
            return this.Json(list, JsonRequestBehavior.AllowGet);
        }
 
        /// <summary>
        /// Retorna los países de cierto continente
        /// </summary>
        /// <param name="continente">Nombre de continente</param>
        /// <returns></returns>
        public JsonResult Paises(string continente)
        {
            var list = from pais in db.Paises
                where pais.Continente == continente
                select new { Text = pais.Nombre, Value = pais.Nombre };
 
            return this.Json(list.ToList(), JsonRequestBehavior.AllowGet);
        }
 
        /// <summary>
        /// Retorna las ciudades de cierto pais
        /// </summary>
        /// <param name="pais">Nombre de pais</param>
        /// <returns></returns>
        public JsonResult Ciudades(string pais)
        {
            var list = from ciudad in db.Ciudades
                where ciudad.Pais == pais
                select new { Text = ciudad.Nombre, Value = ciudad.Nombre };
 
            return this.Json(list.ToList(), JsonRequestBehavior.AllowGet);
        }
    }