viernes, 24 de marzo de 2017

ASP.NET MVC. Propiedades del objeto a null en el parámetro de la acción.

Nos llegó un paciente que se encontraba confuso, no recordaba quién era, no sabía dónde estaba ni qué hacía allí, intentamos ver el porqué de su amnesia...

En ocasiones tendremos que en un formulario no necesitamos rellenar algunas propiedades del modelo, y veremos que dichas propiedades del objeto vienen a null al utilizar una acción a la que le pasamos por parámetro ese objeto del modelo desde la vista, para crear o editar por ejemplo.

Esto sucede porque no teníamos los campos en la vista, ya que al no necesitar editarlos pues no les hemos puesto controles.

Cuando tengamos un campo que no necesitemos editar, pero sí necesitemos conservar su valor después del postback tenemos las siguientes opciones:

1.       Poner dichos campos en hiddens dentro del form, de modo que viajen con el postback, la más recomendable

Vista:
@Html.HiddenFor(model => model.MiCampo)
@Html.EditorFor(model => model.OtroCampo)


Controlador:
Sin olvidarnos de incluirlo en el binding del parámetro de la acción.

public ActionResult Edit([Bind(Include = "MiCampo","OtroCampo")] MiEntidad entidad)
{
                               //hacer cosas
}

2.       Inicializar dichos campos dentro de la acción. Nos obligará a repetir el código en todas las acciones.

Vista:
@Html.EditorFor(model => model.OtroCampo)

Controlador:
    public ActionResult Edit([Bind(Include = "OtroCampo")] MiEntidad entidad)
         {
            entidad.MiCampo = "valor";
            //guardar. Otro campo sí desponía de un control editable en la vista
         }

3.       Guardar el id del objeto en un hidden, buscar el objeto desde la base de datos con un Find e inicializar las propiedades del objeto buscado a los valores de las propiedades editables del objeto que viene por parámetro. Este método es recomendable si tenemos un formulario de display, pero que tiene unos poquitos campos que sí serían editables. Imaginemos una vista que muestra en sólo lectura diez valores y hay un campo que podemos rellenar y un botón para enviar.

Vista:
@Html.HiddenFor(model => model.IdEntidad)
@*otros campos dentro de DisplayFor*@
@Html.EditorFor(model => model.OtroCampo)

     Controlador:
public ActionResult Edit([Bind(Include = "IdEntidad", "OtroCampo")] MiEntidad entidad)
         {
            Entidades bd = new Entidades();
            MiEntidad guardable = bd.Find(IdEntidad);

            //campo a rellenar, el resto estaban en sólo lectura y los acabamos de sacar de BD
            //porque en el parámetro vienen a null
            guardable.OtroCampo = entidad.OtroCampo;
            //guardar

         }

miércoles, 22 de marzo de 2017

¿Asignar CurrentCulture al ModelState de MVC? ¿Está seguro doctor? El caso de las fechas invalidables.

Nuestro paciente en ocasiones no digería bien las fechas presentando graves reacciones alérgicas además, presentaba una grave desorientación espacial sin ser capaz de asociar a qué país viajaba.

Notábamos que si metíamos una fecha a un controller, usualmente mediante un datepicker de jquery o bootstrap observábamos que para unos formatos culturales ModelState validaba bien y para otros no, a pesar de que el string de la fecha está perfecto.

Cuando examinábamos ModelState a ver qué tenía, veíamos que la cultura actual del sistema no correspondía con la de ModelState, causando el problema.

Cultura actual: "es-ES", y sin embargo: 



Explicamos cómo se trata en estos casos al paciente.

Cuando sucede esto es común preguntar en google sobre cómo hacer que ModelState tenga siempre la cultura actual, llegando a la conclusión de que esto no es posible de llevar a cabo pues MVC siempre utilizará InvariantCulture para pasar valores desde una vista a un controlador, parece raro pero si nos paramos a pensarlo es la manera más óptima.

Para que las fechas encajen con la región cultural actual es necesario utilizar InvariantCulture, o siempre tendremos problemas.

Además, conviene que el datepicker que utilicemos forme bien los strings de las fechas.

De modo que cuando pongamos un input para fechas en nuestro html haremos lo siguiente en el cshtml:

<div class="form-group">
                    <div class="control-label col-md-2">
                        <b>@NSVWeb.Resources.Literals.FieldFechaTasacion:</b>
                    </div>
                    <div class="col-md-10 input-group">
                        <label for="date-picker-1" class="input-group-addon btn">
                            <span class="glyphicon glyphicon-calendar" id="date-picker-btn-1"></span>
                        </label>
                        @Html.TextBoxFor(model => model.FechaTasacion,
    new
    {
        @Value = Model.FechaTasacion.ToString("d", System.Globalization.CultureInfo.InvariantCulture),
        @Class = "date-picker form-control"
    })
                        @Html.ValidationMessageFor(model => model.FechaTasacion, "", new { @class = "text-danger" })
                    </div>
                </div>


Para conseguir que el datepicker tome el formato de fecha de la cultura actual, simplemente se la pasaremos por un input hidden:

<input type="hidden" id="currentDateTimeFormat" value="@System.Globalization.CultureInfo.InvariantCulture.DateTimeFormat.ShortDatePattern.ToLower()" />

Y la tomaremos para el datepicker en el .js:

$(".date-picker").datepicker({
    format: $('#currentDateTimeFormat').val(),
    showOtherMonths: true,
    selectOtherMonths: true,
    autoclose: true,
    changeMonth: true,
    changeYear: true,
});


viernes, 10 de marzo de 2017

Ventana popup en ASP.NET MVC. Formularios con envío AJAX a Controller Web Api y validación en servidor.

Introducción:

¡Cuánto tiempo sin postear un caso!

Y es que el servicio de urgencias últimamente está más apacible desde que nos hemos trasladado a los nuevos proyectos, que son más de desarrollo y menos de técnica de sistemas.

Por eso hoy, más que arreglar síntomas de enfermos, nos dedicaremos a dar a luz un formulario en popup para una aplicación ASP.NET MVC que es abierto con jquery y envía los datos a un controlador web api con AJAX, para después mostrar una validación que viene del servidor si hubo problemas o mostrar otro popup indicando que se ha guardado todo con éxito si fue todo bien.

Aunque lo explicado aquí es muy ad hoc, siempre podrá el lector entresacar algo, o al menos alguna idea útil, aunque suponemos que este ejemplo es perfectamente aplicable a cualquier desarrollo que se haga con esta tecnología.

Nos disculparemos desde Doctor SharePoint diciendo que aquí somos así, ponemos casos que nos suceden, no somos un blog de divulgación ni de pruebas piloto, por ni mencionar que este caso ni siquiera es de SharePoint :-o.

Materia:

Comencemos por JavaScript, son dos archivos, uno para inicializar el popup o los popups que necesitemos mostrar desde una vista y otro que contiene el código que muestra el popup (nuestra caja negra)

Son necesarios los siguientes elementos en la vista origen o hay que tener en cuenta los siguientes aspectos:

1. Un div para la caja de diálogo con un id.
2. Un input de tipo button con un id. Podría valer con otro control, pero no está garantizado que funcione.
3. Un anchor con un id, debe llevar un href apuntando a la url de la vista de formulario que quermos mostrar.
4. Un div para mostrar una caja de resultado con un Id, el mensaje de éxito debe ir ya dentro.
5. Hay que inicializar las variables de configuración y llamar al método crearPopUp como se indica más abajo en el código JavaScript con los Id's comentados antes.
6. En un array hay que poner los nombres de los campos que se pretende pasar al servidor, sus nombres han de ser iguales a los del modelo de la vista popup.
7. En caso de querer poner más de un popup en una vista, todos los elementos deben ser únicos para cada popup.

Ejemplo de código que hay que poner en la vista desde la que se abrirá el popup, explicado antes:

<div id="dialog" style="overflow:hidden"></div>
        <input type="button" id="btnPopUp" value="Nueva entidad" class="btn btn-default" />
        <a id="urlPopUp" href="@Url.Action("CreatePopUp", "Entidades", null)" style="display:none"></a>
        <div id="dialogAlert" style="display:none">
            <p>
                Se ha enviado con éxito
            </p>
        </div>

Necesitamos que sea así pues nos obliga la estructura de multiidioma con recursos literales, que aquí no se muestran, si no necesitamos utilizar recursos de servidor aún podríamos minimizarlo más, pero no es el caso.

Inicialización de las variables en el código js para cada formulario:

//Id del div para encajar popup.
var dialogNameId = "dialog";

//Id del div para mostrar un diálogo de confirmación.
var dialogAlertId = "dialogAlert";

//Id del anchor que contiene la url a la vista que queremos mostrar.
var viewUrlAnchorId = "urlPopUp";

//Id del botón que generará la acción de mostrar el popup.
var popUpActionTriggerId = "btnPopUp"

//Url relativa hacia el controlador web api que ejecutará la validación y las inserciones o lecturas cuando pulsemos en el submit dentro del popup.
var controllerUrl = "/api/Controlador/CreatePopUp";

//Array con los nombres de los campos, deben ser iguales al modelo de la vista que se muestra en el //popup. 
//Estas columnas serán montadas como un string de JSON para enviar al controlador web api
var modelColumns = ["UserName", "NIF", "Email", "CodigoEntidad"];

crearPopUp(dialogNameId, dialogAlertId, viewUrlAnchorId, popUpActionTriggerId, controllerUrl, modelColumns);

Segundo archivo, el que muestra el popup, este código no debe tocarse, tan sólo hay que inicializar lo anterior como se ha especificado y llamar a esta función:

function crearPopUp(dialogNameId, dialogAlertId, viewUrlAnchorId, popUpActionTriggerId, controllerUrl, modelColumns, dialogTitle) {
    var dialog = $("#" + dialogNameId);
    var dialogAlert = $('#' + dialogAlert);
    var url;
//evento del botón que acciona el popup
    $("#" + popUpActionTriggerId).click(function (e) {
        url = $('#' + viewUrlAnchorId).attr('href');
        e.preventDefault();
        dialog.dialog('open');
    });
    dialog.dialog({
        autoOpen: false,
        width: 700,
        resizable: false,
        title: $("#" + popUpActionTriggerId).val(),
        modal: true,
        dialogClass: 'ui-dialog-osx',
        open: function (event, ui) {
            $(this).load(url);
            $('button[type="button"]').each(function () {
                $(this).addClass('btn btn-default');
            });
        },
        close: function (event, ui) {
            //$(this).find('form')[0].reset();
        },
        buttons: {
            OK: function () {
//llamada con ajax para controlar que el controlador no nos redirija a otra vista.
                $.ajax({
                    type: "POST",
                    url: document.location.origin + controllerUrl,
                    data: initializeModel(),// JSON.stringify(model),
                    contentType: "application/json",
                    success: function (result) {
//cerramos el popup y mostramos un popup de envío exitoso
                        dialog.dialog("close");
                        dialogAlert.dialog('open');
                    },
                    error: function (result) {
//recojemos el ModelState con los errores de validación
                        for (var name in result.responseJSON.ModelState) {
                            var value = result.responseJSON.ModelState[name];
                            $('#' + name).removeClass('form-control text-box single-line');
                            $('#' + name).addClass('input-validation-error form-control text-box single-line');
                            $('span[data-valmsg-for="' + name + '"]').removeClass('field-validation-valid text-danger');
                            $('span[data-valmsg-for="' + name + '"]').addClass('field-validation-error text-danger');
                            $('span[data-valmsg-for="' + name + '"]').text(value);
                        }
                    }
                }).done(function (res) {
                    console.log('res', res);
                });
            },
            Cancel: function () {
                dialog.dialog("close");
            }
        }
    });
    dialogAlert.dialog({
        modal: true,
        autoOpen: false,
        open: function () {
            $(".ui-dialog-title").html("Alert"); //título de la ventana de popup éxito
        },
        buttons: {
            Ok: function () {
                dialog.dialog("close");
            }
        }
    });
}

//montaje del JSON a partir del array de columnas
function initializeModel() {
    var str = "{";
    for (i = 0; i < modelColumns.length; i++) {
        str += '"' + modelColumns[i] + '":"' + $('#' + modelColumns[i]).val() + '",';
    }
    str = str.substring(0, str.length - 1);
    str += "}";
    return str;
}

Vista del popup, NÓTESE que no lleva botón submit, el js ya se encarga de poner los botones:

@model Entity
 using (Html.BeginForm())
    {
        @Html.AntiForgeryToken()
        <div class="form-horizontal">
            <hr />
            @Html.ValidationSummary(true, "", new { @class = "text-danger" })
            <div class="form-group">
                <div class="control-label col-md-2">
                    <b>Nombre</b>
                </div>
                <div class="col-md-10">
                    @Html.EditorFor(model => model.UserName, new { htmlAttributes = new { @class = "form-control" } })
                    @Html.ValidationMessage("UserName", new { @class = "text-danger" })
                </div>
            </div>
            <div class="form-group">
                <div class="control-label col-md-2">
                    <b>NIF</b>
                </div>
                <div class="col-md-10">
                    @Html.EditorFor(model => model.NIF, new { htmlAttributes = new { @class = "form-control" } })
                    @Html.ValidationMessage("NIF", new { @class = "text-danger" })
                </div>
            </div>
            <div class="form-group">
                <div class="control-label col-md-2">
                    <b>Email</b>
                </div>
                <div class="col-md-10">
                    @Html.EditorFor(model => model.Email, new { htmlAttributes = new { @class = "form-control" } })
                    @Html.ValidationMessage("Email", new { @class = "text-danger" })
                </div>
            </div>
            <div class="form-group">
                <div class="control-label col-md-2">
                    <b>Código Entidad</b>
                </div>
                <div class="col-md-10">
                    @Html.EditorFor(model => model.CodigoEntidad, new { htmlAttributes = new { @class = "form-control" } })
                    @Html.ValidationMessage("CodigoEntidad", new { @class = "text-danger" })
                </div>
            </div>
        </div>
    }


Vista del controlador Web Api EntidadesController:

 [HttpPost] //es un post
 [ValidateModel] //validación de servidor, uso de ModelState
public HttpResponseMessage CreatePopUp([FromBody] Entity entity)
        {
            ValidarMandatario(entity); //código para validar
            if (ModelState.IsValid)
            {
                //guardar datos o hacer otras cosas y devolver response http OK
                        return new HttpResponseMessage(HttpStatusCode.OK);
            }
//devolver http rseponse error con el ModelState, que será parseado a JSON
            return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
        }