martes, 1 de marzo de 2016

Binding de listas en ASP.NET MVC

El binding de listas en ASP.NET MVC puede ser algo bastante sencillo y automático (en el caso de tipos simples), o por el contrario algo lioso y poco intuitivo (en el caso de tipos complejos).

Para bindear tipos simples tan sólo hay que seguir la norma HTTP de repetir el nombre del parámetro. Por ejemplo para recibir un IEnumerable foo, podríamos llamarlo con ?foo=Bar&foo=Baz y todo funcionará a la perfección.

Para el caso de tipos complejos, igualmente todo funcionará siempre y cuando el nombre de los parámetros sea el adecuado y en función del ValueProvider que recoja los datos.

Por ejemplo, si esperamos recibir un objeto Person:

    
public class Person
    {
        public string Name { get; set; }
        public IEnumerable<Address> Addresses { get; set; }
    }

    public class Address
    {
        public string City { get; set; }
        public string Country { get; set; }
    }

La url que deberíamos enviar por GET sería:

name=John
&addresses[0].city=Madrid
&addresses[0].country=Spain
&addresses[1].city=New York
&addresses[1].country=USA

Fíjate que la norma es propiedad[índice].propiedad, por ejemplo addresses[0].country

Lo más sencillo es utilizar los helpers de Html en la vista para que los atributos name se generen acorde a esta norma.

Sin embargo, ¿Cómo enviamos un triste objeto Javascript cumpliendo esta norma?

En mi caso usaré jQuery.

Si es por POST no hay ningún problema. Cualquier de las siguientes opciones es válida (la primera funciona por la existencia de JQueryFormValueProvider que sabe interpretar cómo serializa jQuery el payload, y la segunda por JsonValueProvider).

            var data = {
                name: "John",
                addresses: [
                    { city: "Madrid", country: "Spain" },
                    { city: "New York", country: "USA" }
                ]
            };
            $.ajax({
                type: "POST",
                url: url,
                data: data
            });
            data = JSON.stringify(john);
            $.ajax({
                type: "POST",
                url: url,
                data: data,
                contentType: "application/json"
            });

Sin embargo, si queremos enviar ese mismo objeto por GET no funcionará.

Si probamos con el método $.param de jQuery (el que nos recomiendan para obtener un objeto serializado y enviarlo por querystring) el resultado no es el esperado:

$.param({
                name: "John",
                addresses: [
                    { city: "Madrid", country: "Spain" },
                    { city: "New York", country: "USA" }
                ]
            });
name=John
&addresses[0][city]=Madrid
&addresses[0][country]=Spain
&addresses[1][city]=New York
&addresses[1][country]=USA

Se parece bastante a lo espera recibir ASP.NET MVC, pero no es lo mismo. Eso lo entiende JQueryFormValueProvider, pero para GET ese proveedor no actúa.

Para solucionar esto (y este es el propósito del post) podemos usar la siguiente función (donde aumentar el objeto String con format no debería ir ahí, pero para el ejemplo y si no vas a usar la función format en ningún otro sitio, pues podría valer).

        var serialize = (function () {

            if (typeof String.format != 'function') {
                String.format = function () {
                    var format = arguments[0];
                    for (var i = 0; i < arguments.length - 1; i++) {
                        var reg = new RegExp('\\{' + i + '\\}', 'gm');
                        format = format.replace(reg, arguments[i + 1]);
                    }
                    return format;
                };
            }

            function serialize(obj) {
                var s = _serialize(obj);
                if (s !== "") {
                    s = s.substring(1, s.length);
                }
                return s;
            }

            function _serialize(obj, prefix) {
                prefix = prefix || "";
                var s = "";
                if (typeof obj !== 'object') {
                    return String.format('&{0}={1}', encodeURIComponent(prefix), encodeURIComponent(obj));
                }
                for (var prop in obj) {
                    if (!obj[prop]) {
                        continue;
                    }
                    if (Array.isArray(obj[prop])) {
                        for (var index = 0; index < obj[prop].length; index++) {
                            s += _serialize(obj[prop][index], prefix + (prefix ? '.' : '') + prop + '[' + index + ']');
                        }
                    } else if (typeof obj[prop] === "object") {
                        s += _serialize(obj[prop], (prefix ? '.' : "") + prop);
                    } else {
                        s += String.format('&{0}={1}', encodeURIComponent((prefix ? prefix + '.' : '') + prop), encodeURIComponent(obj[prop]));
                    }
                }
                return s;
            }

            return serialize;
        })();

Ahora una llamada con:

serialize({
                name: "John",
                addresses: [
                    { city: "Madrid", country: "Spain" },
                    { city: "New York", country: "USA" }
                ]
            });

Devolverá una query string que al enviar como parámetro en GET será conforme a la norma que establece ASP.NET MVC para el binding de listas de tipos complejos.

name=John
&addresses[0].city=Madrid
&addresses[0].country=Spain
&addresses[1].city=New York
&addresses[1].country=USA

Un saludo!