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);
        }

jueves, 17 de noviembre de 2016

Implementar multiidioma en una aplicación ASP.NET con Angular translate.

Cuando nuestros pacientes aprendían idiomas antiguamente a veces se veían apurados al cambiar de uno a otro y se notaba que tenían que refrescar el conocimiento.

Ahora se les exige que sean capaces de cambiar de un idioma a otro sin que se note.

Este post requiere conocimientos de Angular al menos a nivel básico, para adquirirlos basta con leer unos cuantos capítulos de esta web:

Primeros pasos con Angular

Al menos hay que leer lo suficiente como para tener claro lo que son los controladores, los módulos, la aplicación angular, los bindings a etiquetas, las directivas y los scopes, son conceptos realmente sencillos de comprender y que llevan mucho menos tiempo del que parece si ya se domina JavaScript, cosa que se da por sentada, por tanto se anima al lector a empaparse de ello para comprender mejor lo que se explica a continuación.

El otro modo, es el de toda la vida, el de copy/paste como un loro, pero bueno, allá cada cual.

Lo que se presenta en este artículo es una solución que quizás necesita retoques por supuesto, pero que funciona, que es lo importante.

En nuestro caso, el multiidioma lo implementamos en la master de la aplicación ASP.NET para que tenga efecto sobre cualquier página que pongamos incrustada.

Lo primero es insertar las referencias a angular y al js en el cual estará el código del programador (translateApp.js en este caso), es recomendable subir las referencias a una carpeta local del proyecto para evitar, si nuestras referencias son remotas, que pudieran afectar las posibles actualizaciones sin previo aviso:

    <script src="Scripts/angular.min.js"></script>
    <script src="Scripts/angular-translate.min.js"></script>
    <script src="Scripts/translateApp.js"></script>

Dentro de un div en la master que englobe a los asp:Contents se colocará un control de selección para el idioma, si se requiere imágenes u otra cosa, se hará lo equivalente para otro control:

       <div class="container body-content" ng-app="at" ng-controller="Ctrl">
            <div class="ng-scope">
                 <select ng-model="selectedLanguage" ng-change="selectLanguage()" ng-options="x for (x, y) in languages">
                 </select>
            </div>
            <asp:ContentPlaceHolder ID="MainContent" runat="server">
            </asp:ContentPlaceHolder>
        </div>

Explicando rápidamente el tag select de arriba, el selector tiene unas etiquetas de Angular para controlar su enlazado a datos ng-model, el evento al cambiar su valor, ng-change, que va a una función llamada selectLanguage() y una expresión para llenar sus opciones en ng-options.

Por supuesto, el div que lo engloba es el que contiene los tags de Angular para referenciar a la app de Angular y al controlador que contendrá el código para manejar todo el contenido dentro de ese div.

Ahora exponemos el contenido del archivo translateApp.js, donde se hace uso de la api translate de Angular:


var app = angular.module('at', ['pascalprecht.translate']);

app.config(function ($translateProvider) {
    $translateProvider.translations('English', {
        HelloLiteral: 'Hello',
        FirstParagraphLiteral: 'This is a paragraph.'
    });
    $translateProvider.translations('German', {
        HelloLiteral: 'Hallo',
        FirstParagraphLiteral: 'Dies ist ein Paragraph.'
    });
    $translateProvider.preferredLanguage('English');
});

app.controller('Ctrl', function ($scope, $translate) {
    $scope.languages = {
        English: 'English',
        German: 'German'
    };
    $scope.selectedLanguage = $scope.languages.English;
    $scope.selectLanguage = function () {
        $translate.use($scope.selectedLanguage);
    }
});

Tanto la configuración como el controlador de la app se puede encontrar en su estado básico en la web de angular translate, de donde se ha sacado el código, lo único reseñable en este caso es haber adaptado el código original, el cual venía preparado para un botón, a un control select.

Además se ha añadido la inicialización del select al preferredLanguage, en este caso mediante código lo cual no es lo más deseable, pero con algunas modificaciones puede detectarse automáticamente.

Dentro de cada función translations se incluirá un idioma donde se abarcan todas las etiquetas de los controles que hay en la interfaz para cada texto que llevará en ese idioma.

Por simple organización y comodidad, quizás sería deseable poner las etiquetas y los literales para cada idioma en archivos json separados por idioma, esto a gusto del consumidor.

Por último, ponemos los controles allá donde se desee en las páginas de la aplicación que hereden de la master, si no heredan de la master, se deberá declarar otro controlador para esa página.

En este caso no ha sido necesario, a continuación se presenta un ejemplo de controles en una página que hereda de la master:

<div>
    <h2 translate="HelloLiteral">Hello</h2>
    <p translate="FirstParagraphLiteral">This is a paragraph.</p>
</div>

La etiqueta translate que Angular translate define, servirá para enlazar cada control con su literal.

El resultado final:



miércoles, 16 de noviembre de 2016

Devolver Json desde tus Controllers en una aplicación ASP.NET.

Una buena forma de conseguir que los métodos de tus controllers devuelvan en Json en vez de xml por defecto es poner la siguiente instrucción de formateo para la app.

Ponla en el Global.asax si tu app no es MVC o en el WebApiConfig si es MVC.

GlobalConfiguration.Configuration.Formatters.JsonFormatter.MediaTypeMappings
.Add(new RequestHeaderMapping("Accept",
                              "text/html",
                              StringComparison.InvariantCultureIgnoreCase,
                              true,
                              "application/json"));


viernes, 14 de octubre de 2016

El servicio de Sincronización de Perfiles se queda pillado en "iniciando"

Esto viene a raíz de querer hacer una sincronización de perfiles de usuarios pero encontrarnos con que el servicio de sincronización no arrancaba, se quedaba pillado en "iniciando" o "starting".

Hagamos una traducción al inglés:

User Profile Service Stuck on Starting.

Cuando en la Central de Administración vamos a "servicios en el servidor" o "services on server" y vemos que el servicio de sincronización no termina nunca de arrancar, podemos pasar este script para desaprovisionarlo:

Add-PSSnapin Microsoft.SharePoint.Powershell
$TypeName = “User Profile Synchronization Service”
$ServerName = “SERVERNAME” #Replace with your server name where the service is stuck on Starting
$Serviceinstance = Get-SPServiceInstance | where-object {$_.TypeName -eq $TypeName -and $_.Server.Address -eq $ServerName}
$Serviceinstance.Unprovision()



Después, volvemos a servicios en el servidor y lo inicialmos de nuevo para que aprovisione bien.

Se han creado alertas correctamente, pero estos usuarios no recibirán notificaciones por correo electrónico hasta que se proporcionen direcciones válidas.


A nosotros el paciente nos alertó en inglés, algo de que no escuchaba bien, venía con serios problemas en el tímpano:

Alerts have been created successfully but these users will not receive notifications until valid e-mail or mobile addresses have been provided.

Pero para el caso es lo mismo, al intentar crear una alerta a un usuario concreto en un elemento de lista o biblioteca, aparece el siguiente mensaje:



Solucionamos el problema yendo al servicio de perfiles de usuario a realizar una sincronización, pero vimos que la sincronización de perfiles de usuario no estaba aprovisionada, para resolver esto vamos a servicios en el servidor e iniciamos el servicio de sincronización de perfiles.



Ahora, vamos al servicio de perfiles de usuario y una vez haya terminado el aprovisionamiento, haremos una sincronización COMPLETA o full sync, en la imagen a continuación, el aprovisionamiento aún no ha terminado.
Una vez terminada la sincronización, podemos observar si la cuenta tiene o no correo electrónico, hacemos clic en Manage User Profiles o Administrar Perfiles de Usuario y buscamos, como podemos observar, no tenemos correo, en otro caso, podríamos dar por resuelto el problema:


Esto se debe a que la cuenta no tiene en Directorio Activo un email asignado o bien a que la cuenta es de Office 365 y no cuenta con sincronización on-premise, no hay problema, el servicio de perfiles permite que nosotros asignemos un correo electrónico a la cuenta, para ello hacemos clic en la cuenta en la columna Account name (en la imagen de arriba) y elegimos en el combo la opción editar, después asignamos una dirección de correo electrónico y guardamos y cerramos (botón arriba y abajo de la página):



Por último, volvemos a hacer una sincronización completa, por alguna extraña razón que no nos hemos parado a destripar (aún), no sirve aparentemente las sincronizaciones incrementales.

Si todo va bien, podremos recibir alertas en esta cuenta sin problemas.