Mostrando las entradas con la etiqueta ASP.NET MVC. Mostrar todas las entradas
Mostrando las entradas con la etiqueta ASP.NET MVC. Mostrar todas las entradas

Mvc4 y jquerymobile

En este post vamos a hablar sobre la integración de asp.net mvc v4 con jquery.mobile. Para quienes necesiten una breve introducción a jquery.mobile pueden leer el post sobre jquery.mobile

Cuando desarrollamos una aplicación que se tiene que ver tanto en un browser de PC como en un dispositivo móvil, la primera pregunta que debemos hacernos es: "¿es la misma aplicación o estamos hablando de dos aplicaciones diferentes?", y no me refiero a nivel técnico, sino a nivel funcional, muchas veces es diferente el uso que se le da a una aplicación desde un dispositivo móvil y desde una PC, en este post vamos a desarrollar la opción de que es la misma aplicación.

Vistas

Como sería de esperar, es en las vistas donde deberíamos tener las mayores diferencias para dispositivos móbiles y de escritorio.

Una facilidad que nos da mvc4 es la de "sobreescribir" páginas para ser utilizadas desde dispositivos móviles simplemente creando una página con el mismo nombre y sufijo .mobile, por ejemplo: Index.cshtml e Index.mobile.cshtml, la primera página se va a utilizar cuando accedamos desde una PC mientras que la segunda se utilizará cuando se acceda desde un dispositivo móvil. Lo mismo aplica tanto para partial views como para layouts.

Hay 3 casos de vistas (que a su vez aplican sobre view, partial view y layouts)
  1. Vistas que son totalmente agnósticas del dispositivo con que se accede. Es el caso de About.cshtml.
  2. Vistas muy diferentes en cuanto a estructura, por lo que amerita tener dos archivos diferentes. Es el caso de Films.cshtml y Films.mobile.cshtml.
  3. Vistas híbridas, donde las diferencias no causan problemas ya sea porque utilizan atributos que son ignorados por la otra modalidad y/o determinados class que, en el peor de los casos, no los encontrará. Es el caso de Disponibility.cshtml que solo agrega los atributos data-role y data-min que jquery.mobile interpretará.
Para este ejemplo las únicas vistas que son totalmente diferentes son: Layout, Film y DateTime. En el resto no hay diferencia estructural que lo ameriten.

Controllers

Los controllers son los mismos ya sea que se navegue de un dispositivo móvil o desde un desktop, quizás lo único interesante para mencionar en este post es el que nos permite cambiar intencionalmente entre vistas (mobile vs desktop):

Un caso particular se da cuando queremos redireccionar a otra acción con RedirectToAction, si bien la redirección parece funcionar a primera vista, cuando queremos accionar sobre la nueva página esta tiene como url la anterior provocando así un extraño comportamiento. La solución a esto es indicar manualmente el atributo data-url del div de la página. En el ejemplo que les comparto hubo un solo caso y está puesto directamente en el controller:

mientras que en _Layout.mobile.cshtml está:

Reconociendo dispositivos

Por default, la versión beta con la que estoy trabajando de mvc4, no reconoce muchos dispositivos mobile, como es el caso de mi Android, por este motivo es que hay que agregarlo.

Y en Global.asax.cs, debemos agregar al Application_Start la siguiente línea:


Algunas consideraciones

asp.net mvc4 y jquery.mobile son dos framework independientes y tienen algunas "fricciones", por ejemplo:
  • jquery.mobile maneja por default un esquema de navegación con request ajax y agregando nuevas páginas como div en función de los response obtenidos. Este funcionamiento tiene algunos roses con el RedirecToAction por ejemplo, la solución a este caso debe ir por el data-url y no por desactivar ajax ($.mobile.ajaxEnabled = false;).
  • Existen casos particulares donde no queremos utilizar la navegación default de jquery.mobile que utiliza ajax como explicábamos en el punto anterior, en estos casos tenemos dos alternativas rel="external" y data-ajax="false". Documentación sobre Navegación en jquery.mobile
  • Al utilizar jquery.mobile tenemos que considerar que tiene una estructura de eventos complementaria a jquery por el tema ya mencionado de la navegación. Por ejemplo debemos utilizar los eventos pageCreate o pageInit en lugar de ready. Documentación sobre Eventos en jquery.mobile

Ejemplo completo

Luego de descargar el ejemplo desde aquí y abrir la solución, es necesario ejecutar los siguiente comandos desde Packages Manager Console:

Los mismos son para instalar todas las librerías desde NuGet (asp.net mvc4, nunit, etc) utilizadas en el ejemplo la primera vez que compiles.

Otra opción es hacer clic derecho sobre la solución y tildar la opción "Enabled NuGet Package Restore" como dice el siguiente artículo: Using NuGet without committing packages to source control.

ASP.NET MVC - ModelMetadata basada en convenciones y recursos

Cuando trabajamos con mvc, posiblemente tengamos nuestros models plagados de strings para definir nombres, mensajes de validación, nombres de recursos y posiblemente estos se repitan en varios models. Este ejemplo intenta mostrar una forma de resolverlo mediante convenciones y recursos.

Metadata basada en DataAnnotacion básica

Partiendo del siguiente ejemplo podemos observar varios strings hardcode

public class RegisterModel {
    [Required(ErrorMessage = "The User name is required")]
    [Display(Name = "User name")]
    public string UserName { get; set; }

    [Required(ErrorMessage = "The Email address is required")]
    [DataType(DataType.EmailAddress)]
    [Display(Name = "Email address")]
    public string Email { get; set; }

    [Required(ErrorMessage = "The Password is required")]
    [ValidatePasswordLength]
    [DataType(DataType.Password)]
    [Display(Name = "Password")]
    public string Password { get; set; }

    [DataType(DataType.Password)]
    [Display(Name = "Confirm password")]
    [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
    public string ConfirmPassword { get; set; }
}

uno de los problemas que plantea esto es la internacionalización

Metadata basada en recursos

Para solucionar la internacionalización es bien conocido el uso de recursos, para esto indicamos los textos como recursos:
Screenshot
y nuestro model debería ser algo parecido a:

public class RegisterModel
{
    [Required(ErrorMessageResourceType = typeof(Resources), ErrorMessageResourceName = "UserNameRequired")]
    [Display(ResourceType = typeof(Resources), Name = "UserName")]
    public string UserName { get; set; }

    [Required(ErrorMessageResourceType = typeof(Resources), ErrorMessageResourceName = "EMailRequired")]
    [DataType(DataType.EmailAddress)]
    [Display(ResourceType = typeof(Resources), Name = "EMail")]
    public string Email { get; set; }

    [Required(ErrorMessageResourceType = typeof(Resources), ErrorMessageResourceName = "PasswordRequired")]
    [ValidatePasswordLength]
    [DataType(DataType.Password)]
    [Display(ResourceType = typeof(Resources), Name = "Password")]
    public string Password { get; set; }

    [DataType(DataType.Password)]
    [Display(ResourceType = typeof(Resources), Name = "ConfirmPassword")]
    [Compare("Password", ErrorMessageResourceType = typeof(Resources), ErrorMessageResourceName = "ConfirmPasswordRequired")]
    public string ConfirmPassword { get; set; }
}

con esto resolvimos la internacionalización, ahora tenemos los mensajes en un archivo de recursos, pero seguimos teniendo muchos strings hardcode que ahora hasta parecen seguir un determinado patrón. Además, si tuviésemos otro model con la propiedad Email o UserName tendríamos que volver a definir todos los recursos.

Metadata basada en convenciones

La propuesta de “Metadata basada en convenciones” intenta buscar el nuestro model se parezca a algo como lo siguiente:

public class RegisterModel
{
    [Required]
    public string UserName { get; set; }

    [Required]
    [DataType(DataType.EmailAddress)]
    public string Email { get; set; }

    [Required]
    [ValidatePasswordLength]
    [DataType(DataType.Password)]
    public string Password { get; set; }

    [DataType(DataType.Password)]
    [Compare("Password")]
    public string ConfirmPassword { get; set; }
}
y esto se logra implementando un custom ModelMetadataProvider como el siguiente:
public class DataAnnotationAndResourcesModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
    private List<Attribute> _attributeList;

    protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
    {
        _attributeList = new List<Attribute>(attributes);

        var modelMetadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);

        if (propertyName != null)
        {
            if (string.IsNullOrWhiteSpace(modelMetadata.DisplayName))
                modelMetadata.DisplayName = SearchResource(propertyName);

            if (string.IsNullOrWhiteSpace(modelMetadata.Description))
                modelMetadata.Description = SearchResource(propertyName + "Description");

            var validators = _attributeList.OfType<ValidationAttribute>();
            foreach (var validator in validators)
            {
                if (string.IsNullOrWhiteSpace(validator.ErrorMessage) &&
                    string.IsNullOrWhiteSpace(validator.ErrorMessageResourceName))
                {
                    var resourceName = propertyName + validator.GetType().Name;
                    if (resourceName.EndsWith("Attribute"))
                        resourceName = resourceName.Substring(0, resourceName.Length - 9);
                    var resourceType = validator.ErrorMessageResourceType ?? typeof(Resources);
                    var prop = resourceType.GetProperty(resourceName);
                    if (prop != null)
                    {
                        validator.ErrorMessageResourceType = resourceType;
                        validator.ErrorMessageResourceName = resourceName;
                    }
                }
            }
        }

        return modelMetadata;
    }

    private static string SearchResource(string resourceName)
    {
        string displayName = null;
        var resourceType = typeof(Resources);
        var prop = resourceType.GetProperty(resourceName);
        if (prop != null)
        {
            var value = prop.GetValue(resourceType, null);
            displayName = value != null ? value.ToString() : resourceName;
        }
        return displayName;
    }
}
Para que mvc utilice este provider, debemos indicárselo en ModelMetadataProviders.Current, por ejemplo en el Application_Start de Global.asax:
ModelMetadataProviders.Current = new DataAnnotationAndResourcesModelMetadataProvider();
Este ModelMetadataProvider primero crea la metadata de la forma tradicionar (basándose en DataAnnotation), si esta no indica los nombres, mensajes, etc. se basa en convenciones de nombres para buscar en los recursos:
  • Si no está indicado el DisplayName, busca en los Resources uno que tenga el mismo nombre de la propiedad.
  • Si no está indicada la Description, busca en los Resources uno que tenga el mismo nombre de la propiedad y que termine en “Description”.
  • Para cada ValidationAttribute busca entre los Resources uno que comience con el nombre de la propiedad y termine con el nombre del attribute, por ejemplo: UserNameRequired para la propiedad UserName que tiene indicada la validación Required.

Y si tenemos otros models con las propiedades UserName, EMail, etc. Los recursos serán reutilizados cuando sea necesario si necesidad de definirlo.

Nota: con esto resolvemos nombres, mensajes, etc. Es decir los textos de comunicación con el usuario, no definimos comportamiento, para esto último seguimos utilizando DataAnnotations.
Una implementación real (y mas completa) de esto se puede ver en el proyecto que estamos desarrollando en la comunidad alt.net hispano para la gestión administrativa de eventos haciendo clic aquí.

Login y Logout explícitos en un sitio web con ADFS

Este post es parte de una serie de artículos complementarios a la VAN sobre Identity Providers.

Escenario

Hay veces que podemos querer que manejar el login y logout del usuario independientemente de la página que esté navegando, es decir que una misma página se comporta de una manera si el usuario está autenticado y de otra cuando no lo está, pero la página es la misma.

El ejemplo que vamos a ver es en una aplicación MVC, por lo que a continuación veremos las acciones de Login y Logout, pero fácilmente se podría adaptar a otros escenarios con asp.net.

Actions

public void Logout()
{
    WSFederationAuthenticationModule authModule = FederatedAuthentication.WSFederationAuthenticationModule;
    string signoutUrl =
        (WSFederationAuthenticationModule.GetFederationPassiveSignOutUrl(authModule.Issuer, authModule.Realm, null));

    WSFederationAuthenticationModule.FederatedSignOut(new Uri(signoutUrl),
                                                      new Uri(authModule.Realm));
}

public ActionResult Login()
{
    WSFederationAuthenticationModule authModule = FederatedAuthentication.WSFederationAuthenticationModule;
    var signinUrl = authModule.CreateSignInRequest("passive", authModule.Realm, false);
    return Redirect(signinUrl.RequestUrl);
}

Breve explicación

Para hacer en forma explícita el login y logout tenemos que redireccionar a otro sitio web (con ciertos parámetros en la url), justamente al sitio de ADFS y una buena forma de determinar la url completa a la que redireccionar es usas los servicios que nos provee WIF para tal fin, estos servicios armarán la url según la configuración en la sección microsoft.IdentityModel.

Un detalle aquí es que debemos permitir el acceso a usuarios no autenticados a nuestro sitio, es decir, debemos modificar la configuración por defecto que crea Federation Utility respecto a:

<authorization>
    <allow users="*"/>
</authorization>

De esta forma, el login y logout del sitio no queda atado a las páginas sino a la intención manifiesta del usuario.

Seguir leyendo otros artículos de la serie

Drag and drop entre listas con asp.net mvc y jQuery

View

Para hacer la funcionalidad de drag and drop entre listas con jquery basta con una simple instrucción:

$("#left #center #right").sortable({
                connectWith: '.drag-and-drop-connectedSortable'
            }).disableSelection();

Aquí le estamos diciendo que los elementos left, center y right (en mi caso son todas <ul>) son listas conectadas con los elementos que tengan como class a “drag-and-drop-connectedSortable” (que en este caso son las mismas listas), lo que permite tomar elementos de una lista y ponerlos en otra.

Luego agregamos algunos detalles estéticos y logramos algo como:

ui

los nombres en este ejemplo son en agradecimiento a los que participar en la discusión del foro sobre algunos temas relacionados (hasta el momento de la publicación de este post). hilo en altnet-hispano.

Controller

La acción del controller que deseo invocar es la siguiente:

[AcceptVerbs(HttpVerbs.Post)]
public JsonResult SendList(IList<string> left, IList<string> center, IList<string> right)
{
    return Json(new
                    {
                        Right = from i in right select i.ToLower(),
                        Center = from i in center select i.ToLower(),
                        Left = from i in left select i.ToLower()
                    });
}

pero al momento de invocar dicha acción para informarle sobre los elementos seleccionados en cada lista me encontré con algunos inconvenientes que conversamos en altnet-hispano (hilo):

  • Pasar una lista (<ul> <li>) al controller como parámetros de una acción.
  • Armar los parámetros del post para que sean correctamente interpretados por asp.net mvc.
Pasar una lista (<ul> <li>) al controller como parámetros de una acción

Aquí vimos que no era posible que asp.net mvc interpretara la lista como valores que el usuario puede modificar ya que como dijo Fernando solo los <input…> son tenidos en cuenta. Con esto ya decidimos hacer la invocación del POST con jquery.

Armar los parámetros del post para que sean correctamente interpretados por asp.net mvc.

Para esto tuvimos agregar la línea: jQuery.ajaxSettings.traditional = true; ya que sino asp.net mvc no puede interpretar correctamente los parámetros de tipo array.

El código

$(document).ready(function() {
    jQuery.ajaxSettings.traditional = true;

    $("#form").submit(function() {
        var leftvalues = [];
        $("#left li").each(function() { leftvalues.push($(this).text()) });
        var centervalues = [];
        $("#center li").each(function() { centervalues.push($(this).text()) });
        var rightvalues = [];
        $("#right li").each(function() { rightvalues.push($(this).text()) });

        var postData = { left: leftvalues, center: centervalues, right: rightvalues };

        $.post('<%= Url.Action("SendList", "DragAndDrop") %>',
            postData,
            function(data) {
                alert("left: "+ data.Left + "\n"+ "center: "+ data.Center + "\n"+ "right: "+ data.Right);
            },
            'json');

        return false;
    });

Cómo comentaba mas arriba, la primera línea es debido a que estamos usando jQuery 1.4.x por lo que la serealización de parámetros de tipo array es diferente a como la espera asp.net mvc, por eso seteamos el modo tradicional. les dejo un link al respecto: http://forum.jquery.com/topic/jquery-1-4-breaks-asp-net-mvc-parameter-posting

Luego, simplemente estamos tomando los valores de las listas, pasándolos a un array y armando el post.

Código completo

http://dl.dropbox.com/u/7345566/MvcAndjQuery.7z

Proceso asincrónico en asp.net mvc con jquery

Lo que vamos a hacer en esta oportunidad es una aplicación asp.net mvc con un proceso de duración considerable como para querer tener una barra de progreso usando jquery y jquery.ui

Básicamente, esto se logra haciendo llamadas asincrónicas (e independientes) desde la view a las acciones del controller.

mvc

Controller

En el controller vamos a tener dos acciones, una que se va a invocar mediante un POST (el proceso principal) y otra que se va a invocar mediante otro POST y va a devolver un JSON (consulta del estado del proceso).

Nota: Vamos a hacer uso de los items de HttpApplication para guardar el estado del mismo y para poder ejecutar varios procesos en forma simultanea es que los mismos tienen un processId. Otra opción sería, por ejemplo, persistirlo en una base de datos.

Proceso principal
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult LongProcess(int loops, string processId)
{
    HttpContext.Application["Status_" + processId] = 0;
    HttpContext.Application["Loops_" + processId] = loops;

    for (int i = 0; i < loops; i++)
    {
        Thread.Sleep(1000); //Action

        HttpContext.Application["Status_" + processId] = i + 1; //Update status
    }
    return View("Result", new ResultModel {Loops = loops});
}

donde Thread.Sleep(1000) está representando la duración de una etapa del proceso.

Consulta del estado del proceso

aquí tomamos los valores del estado del proceso, calculamos el porcentaje y armamos una respuesta JSON.

[AcceptVerbs(HttpVerbs.Post)]
public JsonResult GetStatus(string processId)
{
    int status = HttpContext.Application["Status_" + processId] != null
                    ? (int)HttpContext.Application["Status_" + processId]
                    : 0;
    int loops = HttpContext.Application["Loops_" + processId] != null
                    ? (int)HttpContext.Application["Loops_" + processId]
                    : 0;

    int value = 0;
    if (status != 0 && loops != 0)
        value = status * 100 / loops;

    return Json(new { Result = value });
}

View

La vista propiamente dicha contiene un div con los controles que realizan la invocación:

  • un formulario: fromprocess
  • un hidden para guardar el Id de proceso: processId
  • otro hidden para guardar el Id del intervalo que consulta el estado periódicamente: intervalId
  • una caja de texto para ingresar la cantidad de ciclos del loop que estamos simulando: loops.
  • un botón de tipo submit para invocar al post del formulario.

y otro div para mostrar el progreso, el estado y el resultado.

<h2><%= Html.Encode(ViewData["Message"]) %></h2>
<% using (Html.BeginForm("LongProcess", "Home", FormMethod.Post, new {id = "fromprocess"}))
   {%>
    <div id="invoke">
        <fieldset>
            <%=Html.Hidden("processId", Model.ProcessId, new { id = "processId" })%>
            <input id="intervalId" type="hidden" />
            <%= Html.TextBox("loops", 10, new {id = "loops"}) %>
            <input type="submit" value="Run Long process" />
        </fieldset>
    </div>
    <div id="progress">
        <div id="progressbar"></div>
        <div id="status"></div>
        <div id="result"></div>
    </div>
<% } %>

La función javascript para consultar el estado

Esta función consulta el estado del proceso haciendo un POST e interpretando el JSON de respuesta. En url indicamos el link a consultar, en data los parámetros de la acción (en el controller) y cuando la consulta se completa tomamos el resultado (que ya viene parseado a JSON), asignamos la propiedad Result al progressbar y en texto al div status.

function updateProgress() {
    $.ajax({
        url: '<%=Url.Action("GetStatus", "Home") %>',
        data: { processId: $("#processId").val() },
        success: function(data) {
            if (data != null) {
                $("#progressbar").progressbar("option", "value", data.Result);
                $("#status").html("Progress: " + data.Result + "%");
            }
        },
        error: function(msg) {
            $("#status").html("Error: " + msg);
        }
    });
}
La función javascript para hacer el POST asincrónico

cuando la página se carga estamos inicializando algunas propiedades…

  • en la primera línea estamos configurando el div progressbar como una progressbar de jquery.
  • en la segunda estamos ocultando el div progress (será visible cuando se inicie el proceso).
  • la tercer línea es muy importante para que funcione correctamente en IE, sino IE hace un cache de las llamadas ajax y nunca vuelve a consultar el estado al servidor sino que se queda con la primer respuesta. (para FF y Chrome no es necesaria).
  • en la cuarta línea estamos agregando una función al submit del formulario de nuestra página que va a realizar la misma invocación que haría el formulario pero en forma asincrónica.
$(document).ready(function() {
    $("#progressbar").progressbar({ value: 0 });
    $("#progress").hide();

    $.ajaxSetup({ cache: false }); // this line is needed to ajax on IE

    $("#fromprocess").submit(function() {
        $("#invoke").hide(1000);

        $("#progressbar").progressbar("option", "value", 0);
        $("#status").html("loading...");
        $("#progress").show(1000);

        $.ajax({
            type: "POST",
            url: $("#fromprocess").attr("action"),
            data: $("#fromprocess").serialize(),
            success: function(data) {
                clearInterval($("#intervalId").val());
                $("#progressbar").progressbar("option", "value", 100);
                $("#status").html("complete");
                $("#result").html(data);
            },
            error: function(msg) {
                clearInterval($("#intervalId").val());
                $("#status").html("error");
                $("#result").html(msg);
            }
        });

        var intervalId = setInterval("updateProgress()", 100);
        $("#intervalId").val(intervalId);

        return false;
    });
});

la función que acabamos de poner en el submit hace una invocación ajax de tipo POST a url que tiene configurada como action el form fromprocess, en data envía el formulario serializado, esto hace que no nos quedemos esperando el resultado del post sino que indicamos una función js que se va a invocar cuando el proceso se complete.

Luego seteamos un intervalo de ejecución para la función updateProgress y guardamos el Id del intervalo para, que cuando se complete el proceso, detenerlo.

Cuando se completa el proceso:

  • detenemos la consulta de estado
  • seteamos el progressbar en 100%
  • ponemos el status en complete
  • mostramos el resultado en el div result.

Código completo

para descargar el código completo, click aquí

ASP.NET MVC en IIS 6

Desde hace unos días estuve intentando hacer andar ASP.NET MVC sobre IIS 6, hay un problema documentado por el cual nos encontramos con el error “The Page cannot be found” cuando queremos acceder a una determinada View

step0 - pagenotfound

Luego de recorrer y probar varias soluciones de lo mas diversas que iban desde modificar el código fuente de mi aplicación a configurar el IIS me quedé con esta que les cuento a continuación.

Pasos para hacer funcionar ASP.NET MVC sobre IIS 6

1. En las propiedades del sitio (o el directorio virtual) vamos a la sección “Application settings” y entramos en “Configuration”

step1

2. en “Wildcard application maps” vamos a “Insert” y agregamos “c:\windows\microsoft.net\framework\v2.0.50727\aspnet_isapi.dll” (o “Edit” en caso de que exista)

step2

3. y ATENCIÓN a que NO esté tildada la opción “Verify that file exists”

step3

Finalmente aceptamos todas las ventanas que fuimos abriendo y el sitio debería estar funcionando.

referencias: http://www.google.com.ar/search?q=asp.net+mvc+on+iis+6