Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 8 de diciembre de 2009

Controladores en ASP.NET MVCCuando desarrollamos sobre el framework MVC, estamos acostumbrados a crear nuestros controladores partiendo de la clase base Controller, que nos proporciona métodos, propiedades y mecanismos que nos ahorran mucho trabajo en su implementación. Por ejemplo, toda la lógica de localización e invocación de las acciones está definida en esta clase, así como métodos de creación de los tipos más utilizados de ActionResult, las llamadas al model binder, o propiedades de acceso rápido a datos del contexto.

Sin embargo, y es una prueba más de la flexibilidad de diseño del framework, esto no es ni mucho menos obligatorio. La factoría de controladores por defecto no es tan exigente a la hora de localizar estas clases como primer paso durante el proceso de una petición.

Si observamos el código del marco de trabajo, concretamente la clase ControllerTypeCache, nos encontramos con este método estático, responsable de determinar si un tipo dado cumple las condiciones necesarias para ser considerado un controlador válido:

internal static bool IsControllerType(Type t)
{
    return ((((t != null)
             && t.IsPublic) 
             && (t.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) 
             && !t.IsAbstract)) 
             && typeof(IController).IsAssignableFrom(t));
}

Como podemos observar, en primer lugar se comprueba que la clase sea pública; es lógico, pues el framework debe instanciarla. A continuación, comprueba que su nombre atienda a la convención de acabar en “Controller” (sí, la convención que aprendimos a desmontar hace algún tiempo ;-)). La siguiente es que no se trate de una clase abstracta, algo básico si pretendemos instanciarla. Por último, se exige que implemente el interfaz IController cuya definición es la siguiente:

public interface IController
{
    void Execute(RequestContext requestContext);
}

Es decir, en ningún caso aparece la clase Controller, ni siquiera la clase ControllerBase, antecesora de la primera. Basta con implementar el método Execute() definido en el interfaz.

Por tanto, cumpliendo estos requisitos podríamos escribir un controlador tan reducido como el siguiente:

using System.Web.Routing;
using System.Web.Mvc;
 
namespace MasterViewModel.Controllers
{
    public class PersonaController : IController
    {
        public void Execute(RequestContext contexto)
        {
            contexto.HttpContext.Response.Write(
                    "Ejecutando la acción " + contexto.RouteData.Values["action"] +
                    " con parámetro " + contexto.RouteData.Values["id"]
            );
        }
    }
}

Incluyendo dicho controlador en un proyecto en el que mantengamos las rutas por defecto, si realizamos una petición del tipo GET /Persona/Beber/Cerveza, obtendremos en el navegador un mensaje como el siguiente:

Resultado de ejecución

Sin embargo, aunque el desarrollo de controladores así de ligeros pueda resultar a priori una idea atractiva  pensando sobre todo en optimizar aún más la velocidad de respuesta, en la práctica no tiene demasiado sentido hacerlo. Sería prácticamente igual de eficiente sobrescribir el método ExecuteCore de la clase Controller, y nos beneficiaríamos de los métodos y propiedades implementados en la misma.

Publicado en: Variable not found.

martes, 1 de diciembre de 2009

Redirección basada en retornar un HTTP 302ASP.NET MVC ofrece “de serie” mecanismos para transferir el control desde una acción a otra utilizando para ello redirecciones HTTP. Esto significa que cuando un cliente realiza una petición y el método de acción desea que ésta sea procesada desde otra acción, el navegador será informado de la URL a la que debe dirigirse mediante una respuesta de tipo 302 para que, tras recibirla, realice una nueva solicitud que finalmente ejecutará la lógica apropiada.

En el diagrama de la derecha se muestra el proceso completo de transferencia desde una petición a /home/mail hacia /mail/index utilizando la ruta por defecto, y muestro una posible implementación en ASP.NET MVC a continuación:

public class HomeController : Controller
{
    [...]
    // GET /Home/Mail
    public ActionResult Mail()
    {
        return RedirectToAction("index", "mail");
    }
 
}
 
public class MailController : Controller
{
    [...]
    // GET /Mail
    public ActionResult Index()
    {
        return View();
    }
 
}

Aunque es la forma habitual de realizar redirecciones en ASP.NET MVC, hace unos días estaba pensando que hay ocasiones en las tanta petición puede resultar pesada y vendría bien disponer de algo parecido al Server.Transfer, presente desde que peleábamos con ASP clásico, que era capaz de transferir la ejecución a otro punto sin necesidad de que el cliente replanteara la petición.

Y no es que me parezca una práctica especialmente recomendable, puesto que hay muchas cuestiones de ese tipo que pueden resolverse en MVC a nivel configuración de rutas o simplemente replanteando la estructura de direcciones del sitio, pero la verdad es que cuando te pica el gusanillo…

Intento erróneo #1: invocar directamente una acción desde otra

Bueno, en realidad este intento no llegué a hacerlo, pero se trata de un error bastante frecuente cuando se empieza con ASP.NET MVC framework, y me ha parecido interesante reflejarlo aquí como aviso a navegantes:

public class HomeController : Controller
{
    // GET /Home/Mail
    public ActionResult Mail()
    {
        return new MailController().Index(); // <- Mal
    }
 
}

Aunque a primera vista parece tener sentido, y de hecho funcionará muchas veces, se trata de una mala práctica y puede causarnos muchos problemas, y sobre todo, como en el ejemplo anterior, si se trata de llamadas de acciones de distintos controladores. Hay que tener en cuenta que en el contexto de petición estará la información de la petición inicial (/home/mail), no el que podría esperarse desde el método de acción new MailController().Index().

También es peligroso porque al invocarlo directamente estaríamos saltándonos todos los filtros que hubiéramos podido establecer mediante atributos en la acción final, MailController.Index(). Imaginad, por ejemplo, que la acción está protegida por un [Authorize]… :-O

Intento erróneo #2: invocar a Server.Transfer en el controlador

El segundo intento fue, pues eso, invocar directamente a Server.Transfer() desde el cuerpo de una acción. Pensaba que en cualquier caso no era una buena idea puesto que el método de acción que lo invocase sería difícilmente comprobable usando pruebas unitarias, pero bueno, estaba dispuesto a sacrificar esta ventaja:

public class HomeController : Controller
{
    // GET /Home/Mail
    public ActionResult Mail()
    {
        Server.Transfer(Url.Action("Index", "Mail")); // Mal
        return new EmptyResult();                     // En realidad podría devolver
    }                                                 // un nulo, o cualquier otra cosa
}

Al ejecutar el proyecto, el resultado obtenido es una bonita excepción “Error al ejecutar la solicitud secundaria” al intentar procesar la transferencia hacia la dirección “/Mail” (generada por el helper Url.Action()).

Primer acercamiento a la solución correcta: default.aspx

Aparentemente, la solución para transferir el control hacia otra acción pasa por recorrer el ciclo de vida completo de la petición, pero sin necesidad de que el navegador vuelva a hacerla. Ahora la cuestión es cómo conseguir esto armando el menor estropicio posible.

Si os fijáis, al crear el proyecto ASP.NET MVC, en la plantilla se habrá incluido un archivo en la carpeta raíz del mismo, llamado default.aspx. Su objetivo es transferir la ejecución al framework MVC si un usuario realiza una petición al raíz del sitio, siendo esta página el documento por defecto.

Observemos el código por defecto del método Page_Load que encontramos en su code-behind:

public void Page_Load(object sender, System.EventArgs e)
{
    // Change the current path so that the Routing handler can correctly interpret
    // the request, then restore the original path so that the OutputCache module
    // can correctly process the response (if caching is enabled).
 
    string originalPath = Request.Path;
    HttpContext.Current.RewritePath(Request.ApplicationPath, false);
    IHttpHandler httpHandler = new MvcHttpHandler();
    httpHandler.ProcessRequest(HttpContext.Current);
    HttpContext.Current.RewritePath(originalPath, false);
}

Como indican los comentarios, lo que se hace es cambiar la ruta original de la petición, instanciar un nuevo manejador MVC (MvcHttpHandler) y hacer que éste se encargue de procesarla… o en otras palabras, estamos transfiriendo la ejecución, justo lo que queríamos conseguir. Después, para evitar daños colaterales, se vuelve a dejar la ruta de la petición como estaba.

Por tanto, podríamos utilizar este mismo código y crear un método de extensión sobre la clase Controller:

public static class ControllerExtensions
{
    public static void Transfer(this Controller controller, string url)
    {
        string originalPath = controller.Request.Path;
        HttpContext.Current.RewritePath(url, false);
        IHttpHandler httpHandler = new MvcHttpHandler();
        httpHandler.ProcessRequest(HttpContext.Current);
        HttpContext.Current.RewritePath(originalPath, false);
    }
}

Así, podríamos utilizarlo desde nuestras acciones de la siguiente forma:

public class HomeController : Controller
{
    [...]
 
    // GET /Home/Mail
    public ActionResult Mail()
    {
        this.Transfer(Url.Action("index", "mail"));
        return null;
    }
}

Con esto ya tenemos una primera solución a nuestro problema. Sin embargo, aunque puede parecer muy limpio, y de hecho yo estaba bastante conforme con esta solución, este código tiene varios problemas.

En primer lugar, realizar pruebas unitarias sobre la acción Mail sería realmente complicado. Además, la invocación a Transfer() ejecutaría la acción asociada a la ruta de la URL indicada, pero al finalizar continuaría ejecutándose el método Mail(), desde donde la hemos invocado. Esto puede provocar efectos curiosos, si por ejemplo retornásemos un ViewResult o cualquier otro tipo de contenido desde la misma (serían enviados de forma consecutiva el cliente).

Solución final: crear un ActionResult personalizado

Aunque el enfoque anterior es correcto y podría ser válido a pesar de sus inconvenientes, encuentro una solución más fina en una pregunta de StackOverflow, How to simulate Server.Transfer in ASP.NET MVC: crear un resultado de acción (ActionResult) personalizado.

/// <summary>
/// Transfers execution to the supplied url.
/// </summary>
public class TransferResult : RedirectResult
{    
    public TransferResult(string url): base(url)    {    }
   
    public override void ExecuteResult(ControllerContext context)    
    {        
        var httpContext = HttpContext.Current;        
        httpContext.RewritePath(Url, false);        
        IHttpHandler httpHandler = new MvcHttpHandler();        
        httpHandler.ProcessRequest(HttpContext.Current);    
    }
}

De esta forma, evitamos los dos problemas de la anterior aproximación. El método de acción seguiría siendo fácilmente testeable, y evitaríamos resultados indeseados al tratarse de un tipo de respuesta, que evita la posibilidad de introducir código adicional.

Publicado en: Variable not found.

domingo, 29 de noviembre de 2009

Los controladores ASP.NET MVC que heredan de la clase Controller permiten procesar muy fácilmente las peticiones realizadas a acciones no definidas. Para ello, lo único que hay que hacer es sobrescribir el método HandleUnknowAction() e implementar la lógica que queremos que se ejecute en estos casos.

En el siguiente código, las peticiones realizadas a /Home/Index y /Home/About serán procesadas normalmente, pero /Home/BeberCerveza  será procesada por HandleUnknowAction, cuya  implementación mostrará la vista “Index” con un mensaje personalizado:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        ViewData["Message"] = "Welcome to ASP.NET MVC!";
        return View();
    }
 
    public ActionResult About()
    {
        return View();
    }
 
    protected override void HandleUnknownAction(string actionName)
    {
        ViewData["Message"] = "¿Estás intentando " + actionName + "?";
        View("Index").ExecuteResult(this.ControllerContext);
    }
}

image

Publicado en: Variable not found.

martes, 24 de noviembre de 2009

En un post anterior dedicado a jqGrid y ASP.NET MVC vimos lo sencillo que resultaba implementar un potente grid para mostrar datos tabulares, permitiendo paginación, ordenación y redimensionado de columnas.

Pero, como ya comenté entonces, jqGrid es mucho más que eso. En este artículo estudiaremos la implementación de la funcionalidad de borrado de filas integrada en el propio componente, utilizando intercambio de datos Ajax con el lado servidor para actualizar el modelo.

Partiremos del ejemplo que desarrollamos anteriormente, y nos centraremos únicamente en introducir los cambios necesarios para introducir esta nueva capacidad. También, como en el caso anterior, encontraréis al final un enlace al proyecto de demostración, para Visual Web Developer Express 2008 SP1 y ASP.NET MVC 1.0.

1. Preparamos la infraestructura

Selección de módulos en jqGridAntes de nada, y es algo que es importante recordar cuando trabajemos con jqGrid, debemos pensar qué módulos de este componente necesitamos en nuestro proyecto.

En el post anterior descargamos exclusivamente los necesarios para implementar la visualización del grid; ahora, dado que vamos a utilizar más funcionalidades del componente, debemos seleccionar en la herramienta de descarga aquellos que nos harán falta, el base y los relativos a la edición de datos.

En teoría deberíamos seleccionar los módulos estrictamente necesarios para nuestros fines, pero en la práctica no es fácil adivinar cuál de ellos implementa justamente lo que estamos buscando. De hecho, es bastante frecuente encontrarse con errores de script cuando no acertamos con el módulo exacto, al que siguen bastantes iteraciones prueba-error hasta que conseguimos averiguar cuál debemos indicar en la descarga.

Por eso en este caso, seleccionaremos todos los módulos relativos a la edición; eso nos permitirá, además, seguir implementando funcionalidades como el alta o la modificación sin tener que volver a descargar componentes.

Aparte de los módulos a incluir en la descarga, el resto de los pasos de preparación de la infraestructura son idénticos a los descritos en los puntos 1 al 3 del post inicial:

  • Copiar los archivos de script (jqGrid y el archivo de localización) en el proyecto.
  • Descargar el tema visual de jQuery UI y añadirlo al proyecto.
  • referenciar las librerías de scripts y estilos en la master (o vistas donde vayamos a usarlo).

2. El modelo

Vamos a ampliar la clase GestorDeAmigos anterior para que sea capaz de emular el almacenamiento en una base de datos, pero utilizando como soporte una colección en memoria que haremos persistir en una variable de aplicación. Además, aprovecharemos para añadirle un método Delete() que nos permita eliminar del almacén la persona cuyo “Id” le pasemos como parámetro.

public bool Delete(int id)
{
    Amigo amigo = DatosAmigos.FirstOrDefault(a => a.Id == id);
    if (amigo == null)
        return false;
 
    DatosAmigos.Remove(amigo);
    return true;
}
 
No profundizaré más en el modelo, pues el código es de lo más convencional. El que tenga curiosidad por ver cómo se implementa el almacén en una variable de aplicación, que acuda al fuente del proyecto de demostración.

3. La vista

En la vista debemos hacer muy pocos ajustes para permitir la eliminación de los datos. Básicamente, tendremos que habilitar un panel de botones en el grid, indicar que deseamos que aparezca el botón de eliminación, y configurar el comportamiento de éste, para lo cual invocaremos al método navGrid() después de la llamada de inicialización del grid que ya vimos el otro día:

<script type="text/javascript">
    jQuery(document).ready(function() {
    
        jQuery("#list").jqGrid({
            [...] // OMITIDO. Idéntico al del post anterior.
        });
 
        $("#list").navGrid(
                null,
                { refresh: true, add: false, edit: false, del: true, search: false },
                null, // parámetros para el alta
                null, // parámetros para la edición
                {     // parámetros para la eliminación
                    url: '<%= Url.Action("Eliminar") %>',
                    width: 500,
                    afterSubmit: function(r, d) {
                        return [r.responseText=="", r.responseText];
                    }
                }
        );
});

Los parámetros que estamos pasando al método navGrid() son los siguientes:

  • el primer parámetro debería ser jQuery('#pager'), que es la referencia hacia el control de paginación que estamos utilizando. En este caso es un nulo porque esta referencia se incluyó en la inicialización de jqGrid.
  • A continuación, creamos un objeto anónimo en el que establecemos a true las propiedades del y refresh, que indica que queremos mostrar los botones de eliminación y recarga de datos. El resto de propiedades predefinidas, add, edit, y search, equivalentes a los botones de añadir, editar y buscar registros, respectivamente, las establecemos a false con objeto de que no aparezcan botones para invocarlas; ya las activaremos en otros posts ;-)
  • el siguiente parámetro se trata de un objeto donde configuramos el comportamiento del botón de alta de registros. Dado que no vamos a implementarlo ahora, lo establecemos a nulo.
  • el cuarto parámetro es lo mismo que el anterior, pero para configurar la edición, por lo que también se encuentra establecido a null.
  • a continuación, y por último, el objeto en cuyas propiedades definimos el comportamiento del botón de eliminación:

    • La URL a la acción que se invocará en servidor, que la obtenemos utilizando el UrlHelper de MVC. En este caso, invocaremos a una acción llamada “Eliminar”, a la que el sistema enviará el Id del registro activo.
    • El ancho del cuadro de diálogo de confirmación.
    • En afterSubmit implementamos una función callback que jqGrid llamará cuando haya recibido el resultado de la petición Ajax. El primer parámetro que nos envía es el objeto XMLHttpRequest donde encontraremos la respuesta obtenida desde el servidor; el segundo parámetro contiene los datos que han sido enviados a la petición.

      El retorno de la función callback debe ser siempre un array con dos elementos. El primero es un booleano indicando si la eliminación ha tenido éxito, y el segundo es el mensaje de error, en caso de que se haya producido:

      return [ exito , "mensaje de error" ];

      Es importante ahora resaltar una cosa: salvo los parámetros de entrada y el tipo de retorno descritos anteriormente, jqGrid no nos impone ninguna particularidad más respecto a cómo debemos implementar este método, el tipo de información que recibiremos desde el controlador o cómo la procesaremos al recibirla. Somos totalmente libres de elegir la forma en la que haremos las cosas.

      En nuestro caso, vamos a hacer una implementación muy simple en base a la siguiente convención: el controlador retornará un string con una descripción del error en caso de que se produzca algún problema borrando el registro, y retornará un nulo cuando todo vaya bien. Esto nos permite implementar el callback utilizando la siguiente expresión:
      return [r.responseText=="", r.responseText];

      Si observáis, estamos llenando el array de retorno de tal forma que el primer parámetro será cierto si la respuesta obtenida está vacía (o sea, no hay error), y en el segundo parámetro introducimos la respuesta tal cual la hemos obtenido del servidor.

4. El controlador

Dado que la lógica la tenemos implementada en el modelo, y que la vista ya está preparada para ponerse en contacto con el controlador vía Ajax y para recibir el feedback sobre el éxito de la operación, sólo nos queda implementar muy rápidamente el método de acción:

public ActionResult Eliminar(int id)
{
    if (!gestorDeAmigos.Delete(id))
        return Content("Error eliminando el registro");
 
    return null;
}

Podréis observar que si se produce un error, retornamos un ContentResult describiendo el problema; en otros casos, devolvemos un nulo.

Para probar el funcionamiento de una eliminación errónea podéis, por ejemplo, abrir dos navegadores contra la aplicación, borrar un registro desde uno de ellos e intentar borrarlo también desde el otro.

Y… ¡Voilá!

Una vez habiendo implementado el modelo, la vista y el controlador, sólo nos queda probar la aplicación, que veremos que funciona perfectamente ;-P

jqGrid+ASP.NET MVC en acción

Resumiendo, en este post hemos seguido profundizando en las capacidades de jqGrid, implementando paso a paso la funcionalidad de eliminación de registros. Hemos podido observar también el escaso código (¡a pesar de la longitud del post!) que hay que añadir para disponer de esta funcionalidad, y de lo sencillo y reutilizable que resulta su implementación.

Descargar proyecto de demostración:

Publicado en: Variable not found.