viernes, 2 de julio de 2010

ASP.NET MVC Q&A: ¿Como reciben parámetros los controladores?

Este es el segundo post de la serie que nace a raíz de las preguntas que se me realizaron en el Webcast que di sobre ASP.NET MVC.

Estaba explicando la tabla de rutas por “defecto” de ASP.NET MVC, indicando que el primer valor era el controlador, el segundo la acción y el tercero un parámetro llamado id:

routes.MapRoute(
"Default", // Nombre de la ruta
"{controller}/{action}/{id}", // Formato de url /Controlador/Accion/Id
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Valores x defecto
);





Comenté que dada esa tabla de rutas una url /Home/Index/100 se mapeaba a l’acción Index del controlador Home con un parámetro id a valor 100 (parámetro que era opcional).



En el Webcast no comenté nada más… voy a ampliar el tema ahora, porque es realmente interesante.



1. Acciones y tabla de rutas



Vamos a aclarar algunos puntos que son fundamentales relativos a las acciones y la tabla de rutas:




  1. Las acciones se distinguen por su nombre, sin tener en cuenta sus parámetros. Eso significa que un controlador no puede definir dos veces la misma acción con parámetros distintos, salvo que se responda a verbos http distintos (p.ej. una acción responda a GET y otra a POST).


  2. La tabla de rutas mapea una URL a una única acción de un controlador.


  3. La tabla de rutas tiene una o más rutas, que se evalúan en orden. Cuando una URL hace matching con una ruta, se termina la evaluación.



Así el siguiente código es erróneo:




public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
public ActionResult Index(int id)
{
return View();
}
}





Uno podría pensar que la tabla de rutas enrutaría /Home/Index al método Index() sin parámetros y /Home/Index/100 al método Index() que acepta un entero, pero no. La tabla de rutas enruta tanto /Home/Index/ como /Home/Index/100 a la acción Index. Luego el framework cuando va a invocarla se encuentra con dos métodos que la implementan y da un error: The current request for action 'Index' on controller type 'HomeController' is ambiguous between the following action methods: System.Web.Mvc.ActionResult Index() on type MvcControllerParams.Controllers.HomeController System.Web.Mvc.ActionResult Index(Int32) on type MvcControllerParams.Controllers.HomeController



2. Parámetros opcionales en las rutas



¿Y que ocurriría con el siguiente código?




public class HomeController : Controller
{
public ActionResult Index(int id)
{
return View();
}
}





La tabla de rutas enruta /Home/Index/100 a dicha acción y el parámetro id vale 100… pero que ocurre si entramos /Home/Index?



Pues que da error: El parámetro id está declarado como opcional en la ruta, eso significa que puede no aparecer en la url. Así pues la url /Home/Index es válida y se enruta a la acción Index. Y cuando el framework se encuentra que quiere pasarle un valor al parámetro id, no puede porque no hay parámetro id en la url y int no es nullable. El mensaje de error que da es:



The parameters dictionary contains a null entry for parameter 'id' of non-nullable type 'System.Int32' for method 'System.Web.Mvc.ActionResult Index(Int32)' in 'MvcControllerParams.Controllers.HomeController'. An optional parameter must be a reference type, a nullable type, or be declared as an optional parameter.



La solución? Pues declarar que el parámetro id sea nullable:




public class HomeController : Controller
{
public ActionResult Index(int? id)
{
return View();
}
}





Fíjate en el uso de int? (que es la sintaxis que da C# para usar Nullable<int>).



Otra posible opción seria modificar la tabla de rutas para que en lugar de declarar el parámetro simplemente como opcional, asignarle un valor por defecto:




routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = 0 } // Parameter defaults
);





Fíjate que donde antes ponía id = UrlParameter.Optional ahora pone id = 0. La diferencia es sutil pero importante: antes si id no aparecía el framework asumía que no tenía valor, ahora si el parámetro no aparece el framework le asigna un valor por defecto de 0. Así, ahora en el controlador podemos volver a usar int en lugar de Nullable<int> (int? en c#).



El “pero” de esta opción es que no puedes distinguir entre /Home/Index y /Home/Index/0. Ambas URLs invocan la acción Index con el valor id=0.



Bien… ¿y que ocurrirá si entro la url /Home/Index/eiximenis? Pues que la tabla de rutas mapeará esta url a la acción Index y cuando el framework vaya a invocarla intentará convertir “eiximenis” a int y dicha conversión devolverá null al no ser posible. Así:




  • Si teníamos Index(int id) el framework dará error, indicando que no puede asignar “null” a int.


  • Si teníamos Index (int? id) recibiremos el valor null en el parámetro id. Efectivamente en este caso es lo mismo /Home/Index que /Home/Index/eiximenis



Si en lugar de declarar la acción con un parámetro int la declaro que recibe una cadena:




public ActionResult Index(string id)
{
return View();
}





Todo es mucho más sencillo:




  • /Home/Index –> Invoca Index() con id = null


  • /Home/Index/100 –> Invoca Index() con id = “100”


  • /Home/Index/eiximenis –> Invoca Index() con id=”eiximenis”



3. Acciones sin parámetros opcionales



Vamos a cambiar de nuevo la tabla de rutas para que quede:




routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index" } // Parameter defaults
);





Fíjate que hemos eliminado la parte donde ponía id = UrlParameter.Optional. Así ahora el parámetro id es obligatorio en la ruta.



La url /Home/Index/eiximenis nos invoca la acción Index() con id=”eiximenis” pero que hace la url /Home/Index? Pues devuelve un 404. ¿Por que? Bien, cuando una URL no satisface ninguna de las entradas de la tabla de rutas, el framework devuelve un 404 para indicar que la URL no se corresponde a ningún recurso. En nuestro caso la tabla de rutas tiene una sola entrada que obliga que las urls tengan la forma /controlador/accion/id. Y dado que id no se ha declarado opcional ni se le ha dado ningún valor por defecto, debe aparecer explícitamente en la URL. Como en nuestro caso no aparece, la URL /Home/Index no cumple esta entrada en la tabla de rutas y por eso recibimos el 404.



4. Rutas con más de un parámetro



La pregunta explícita que me hicieron en el Webcast fue “Y se puede pasar más de un parámetro”? Mi respuesta en aquel momento fue simplemente que sí, añadiendolos a la tabla de rutas:




routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}/{desc}", // URL with parameters
new { controller = "Home", action = "Index",
id = UrlParameter.Optional, desc = UrlParameter.Optional } // Parameter defaults
);





Ahora la ruta tiene dos parámetros (ambos opcionales) id y desc.



En el controlador definimos la acción con los dos parámetros:




public ActionResult Index(string id, string desc)
{
return View();
}





Tal y como lo tenemos:




  • /Home/Index –> Invoca a Index con id=null y desc=null


  • /Home/Index/eiximenis –> Invoca a Index con id=”eiximenis” y desc=null


  • /Home/Index/eiximenis/edu –> Invoca a Index con id=”eiximenis” y desc=”edu”



De esta manera podemos pasar más de un parámetro a los controladores.



5. Otros parámetros (querystrings,…)



Bueno… vamos a liarla un poco más! Tal y como lo tenemos que ocurre con la url /Home/Index/eiximenis?desc=edu ?



Pues veamos: La tabla de rutas no usa la querystring, así que para la tabla de rutas es como si hubiesemos entrado /Home/Index/eiximenis, lo que se mapea a la acción Index. Luego cuando el framework va a invocar dicha acción:




  • Pone el valor de id a “eiximenis” (primer valor de la url)


  • El valor de la ruta para desc és null (recordad: la tabla de rutas no entiende de querystring). Entonces se analiza la querystring y el framework ve que el controlador acepta otro parámetro llamado desc y que en la querystring aparece dicho parámetro, así que le asigna valor.



Así por lo tanto, la url /Home/Index/eiximenis?desc=edu llama a l’acción Index con el valor de id a “eiximenis” y el valor de desc a “edu”.



Fijaos pues que las urls /Home/Index/eiximenis/edu y /Home/Index/eiximenis?desc=edu son equivalentes, puesto que ambas terminan llamando a la misma acción con los dos parámetros… pero que sean equivalentes no quiere decir que sean indistinguibles. Desde el controlador podemos saber si los parámetros nos vienen via ruta o bien via querystring:




public ActionResult Index(string id, string desc)
{
var routedesc = this.RouteData.Values["desc"];
return View();
}





El valor de routdesc será “edu” si venimos por la url /Home/Index/eiximenis/edu y null si venimos por la url /Home/Index/eiximenis?desc=edu



Nota: Todo lo que hemos dicho sobre la querystring aplica también a parámetros POST.



6. Bindings



Y vamos ya con la última… Imagina que tenemos una clase así:




public class FooModel
{
public string id { get; set; }
public string desc { get; set; }
}





Y declaramos nuestra acción para que reciba, no dos strings sinó un objeto FooModel:




public ActionResult Index(FooModel m)
{
return View();
}





Ahora en este caso la url:




  • /Home/Index –> Invoca a Index() con un objeto FooModel con id=null y desc=null


  • /Home/Index/eiximenis –> Invoca a Index() con un objeto FooModel con id=”eiximenis” y desc=null


  • /Home/Index/eiximenis/edu –> Invoca a Index() con un objeto FooModel con id=”eiximenis” y desc=”edu”


  • /Home/Index/eiximenis?desc=edu –> Invoca a Index() con un objeto FooModel con id=”eiximenis” y desc=”edu”



Que te parece? El framework es capaz de convertir los parámetros (vengan de la ruta o del querystring) en el objeto que espera la acción, instanciando sus propiedades!



Y bueno… vamos a dejarlo aquí por el momento :) Espero que ahora tengáis un poco más claro como funciona el “paso de parámetros” a los controladores.



Un saludo a todos! ;-)



Una postdata para quien quiera profundizar



Si quieres profundizar en este tema, antes que nada ten presente los siguientes puntos:




  • A través de la tabla de rutas el framework decide que acción implementa la URL


  • Antes de invocar una acción, los Value Providers entran en acción, procesando los valores de la request. Los valores de la request pueden estar en:

    • RouteData (puestos por la tabla de rutas)


    • QueryString


    • POST


    • Cualquier otro sitio (p.ej. cookies). Se pueden crear value providers propios.




  • Antes de invocar la acción, el ModelBinder entra en acción: coge los valores que han procesado los Value Providers y los intenta mapear a los parámetros de la acción.


  • Se invoca la acción del controlador.



Ahora algunos links:





PD: Este post es un crosspost desde mi blog de geeks.ms!

miércoles, 30 de junio de 2010

ASP.NET MVC Q&A: Cómo usar la sesión?

Hola a todos! Este es el primer post de la serie que “nace” a raíz de las preguntas que se me realizaron durante el Webcast de ASP.NET MVC que realizé el pasado 28 de Junio.

Una de las preguntas fue precisamente si se podia usar la sesión. La respuesta corta que di en el Webcast fue “sí: la sesión funciona exactamente igual que en Webforms y en mi opinión el sitio donde usarla es en los controladores”. Ahora viene la respuesta larga… :)

Acceder a la sesión desde un controlador es trivial:

public class HomeController : Controller
{
public ActionResult Index()
{
Session["data"] = "Datos en sesión";
return View();
}
}





Como se puede ver el controlador tiene acceso directo a la sesión a través de la propiedad Session declarada en la clase Controller.



Desde las vistas también tenemos acceso a la sesión:




<div>
Datos en sesión: <%: this.Session["data"] %>
</div>





1. NO accedas a la sesión desde las vistas



ASP.NET MVC expone un modelo extremadamente sencillo y potente: toda petición es enrutada a un controlador y el controlador hace lo que tenga que hacer para terminar pasándole un conjunto de datos a la vista (usando lo que se llama un ViewModel que no es nada más que una clase propia que encapsula los datos que la vista requiere). La vista accede a su ViewModel a través de la propiedad Model.



La vista debe ser agnóstica sobre donde proceden los datos: le basta saber que los tiene disponibles en su ViewModel. Deja que sea el controlador si es preciso quien acceda a la sesión, cree un ViewModel con los datos y se los pasa a la vista.



Otro sitio donde uno podría usar Session es en el Modelo, es decir dejar la lógica de si un dato debe almacenarse en sesión o no en el propio Modelo. De esta aproximación lo que mi no me convence es que termina atando nuestro modelo al objeto HttpSessionStateBase. Yo personalmente prefiero que todo acceso a sesión esté en los controladores, dados que es más natural que estos estén atados a “objetos típicos de http”.



2. ¿Para qué quieres la sesión?



Asegúrate que usas la sesión para lo que pertoca. En la sesión se guardan datos relativos a un usuario para un periodo de tiempo relativamente largo, generalmente hasta que el usuario se desconecta de nuestra aplicación. No hagas nunca esto:




public class HomeController : Controller
{
public ActionResult Index()
{
Session["data"] = "Datos en sesión";
return RedirectToAction("OtraAccion", "OtroControlador");
}
}
public class OtroControladorController : Controller
{
public ActionResult OtraAccion()
{
var datos = Session["data"] as string;
// hacemos algo con los datos
Session.Remove("data");
return View();
}
}





En este código el controlador Home pone datos en la sesión y luego redirige a la acción OtraAccion del controlador OtroControlador que recoge los datos de la sesión, hace algo con ellos (p.ej. crear un ViewModel), los elimina de la sesión y llama a su vista.



Aquí se está usando la sesión para compartir datos temporales entre controladores. Dado que este es un escenario relativamente habitual ASP.NET MVC proporciona un mecanismo propio para ello: el TempData.



El código anterior quedaría mejor con:




public class HomeController : Controller
{
public ActionResult Index()
{
TempData["data"] = "Datos temporales";
return RedirectToAction("OtraAccion", "OtroControlador");
}
}
public class OtroControladorController : Controller
{
public ActionResult OtraAccion()
{
var datos = TempData["data"] as string;
// hacemos algo con los datos
return View();
}
}





TempData funciona de un método algo raro: Cuando una propiedad de TempData es leída se marca para eliminación y dicha eliminación sucede al fin de la request. Eso significa que a efectos prácticos los datos de TempData son datos para leer una sola vez. Una buena solución pues para pasar datos temporales entre controladores. Y sí: un dato almacenado en TempData que nunca es consultado permanece entre requests. Esta capacidad permite dar soporte a escenarios con varias redirecciones, pero debemos andarnos con cuidado: si ponemos un valor en TempData que jamás es consultado, ahí se queda ocupando espacio… ¿Y espacio de donde? Pues si esto de que el dato permanece entre requests te suena mucho a “sesión”, no vas desencaminado: internamente TempData se guarda dentro de Session. Para más información sobre TempData léete este brutal post de José M. Aguilar donde lo cuenta mejor que yo.



3. Considera no usar sesión



Otro tema importante a tener en cuenta es evitar el uso de la sesión. La sesión lleva consiguo ciertas implicaciones. Tenemos tres modos de sesión en ASP.NET (y por lo tanto en ASP.NET MVC):




  • InProc: La sesión se guarda en el propio servidor web. No se comparte entre servidores web de una misma web farm, por lo que si tenemos una web farm debemos usar sticky sessions lo que limita la esacabilidad.


  • StateServer: La sesión se guarda en un único servidor. Se comparte entre diversos los servidores de la web farm, pero añade un único punto de fallo: si se cae el servidor de sesión, se cae toda nuestra aplicación.


  • SQLServer: La sesión se guarda en base de datos. Se comparte entre los servidores de la web farm, a costa de perder rendimiento y de perder parte de escalabilidad (siempre es más difícil escalar un SQL Server que añadir un servidor web adicional).



Así pues, ten presente el precio que pagas por usar la sesión. Con eso no digo que la sesión esté prohibida (ni mucho menos), simplemente que consideres si la necesitas de verdad.



4. El punto (3) te echa para atrás pero necesitas usar sesión?



Quieres usar la sesión, pero no estás satisfecho con ninguna de las tres alternativas que te proporciona por defecto ASP.NET? Ningún problema, puedes crearte tu propio proveedor de sesión que use algún mecanismo que satisfaga tus necesidades… Ya, no es tarea fácil verdad? Por suerte (o por desgracia) Microsoft no para de sacar productos, APIs y cosas nuevas y hay una muy interesante: Velocity.



Velocity (incluída dentro de lo que llama Windows Server AppFabric) es una cache distribuída. Lo bueno es que viene con un proveedor de sesión para ASP.NET. Por lo tanto, puedes utilizar Velocity para almacenar tu sesión! Esto implica que la sesión, al estar en la cache distribuída, se comparte entre los servidores web del webfarm sin ser un único punto de fallo (como StateServer) ni penalizar tanto el rendimiento (como SQLServer). Así pues, es una muy buena opción para usar como proveedor de sesión (no solo para ASP.NET MVC, también para ASP.NET tradicional).



En este post Stephen Walther tienes más información sobre como usar Velocity en ASP.NET MVC.



5. Y sobre unit testing?



Una de las ventajas de MVC respecto Webforms es la capacidad de probar nuestras aplicaciones usando unit testing. Generalmente lo que más nos interesa pobar son los controladores y el modelo, dado que las vistas no contienen ninguna lógica (tan solo presentación).



Si nuestros controladores usan la sesión, necesitan un HttpContext para funcionar, pero durante tus pruebas untarias no vas a tenerlo. Imagina que tengo este controlador:




public class HomeController : Controller
{
public ActionResult Index()
{
Session["data"] = "eiximenis";
var data = Session["data"] as string;
return View();
}
}





Y un test unitario para probar algo sobre dicha acción:




[TestMethod]
public void TestMethod1()
{
HomeController hc = new HomeController();
var result = hc.Index() as ViewResult;
Assert.AreEqual(string.Empty, result.ViewName);
}





Al ejecutar este UnitTest… patapaaaam! Nos va a lanzar una NullReferenceException, porque como no estamos ejecutando los tests bajo el motor de ASP.NET no existe Session ni nada parecido (por lo que el método Index de HomeController se encuentra un null cuando accede a la propiedad Session). ¿Y entonces? Pues debemos, de alguna manera u otra conseguir crear Mocks de dichos objetos.



Hay varias maneras de hacer esto, pero una de las más sencillas es usar el TestHelper de MvcContrib. Veamos como podemos testear este método usando MvcContrib. Para ello nos descargamos MvcContrib (para la versión de ASP.NET MVC que estés usando) y:




  1. Agregamos una referencia a MvcContrib.TestHelper.dll


  2. Agregamos otra referencia a Rhino.Mocks.dll. Nota: Técnicamente esta referencia no es necesaria que la agreguemos en nuestro proyecto. El requisito a cumplir es que Rhino.Mocks.dll esté disponible en tiempo de ejecución del test (MvcContrib.TestHelper.dll depende de ella). P. ej. otra forma de conseguir esto es agregar Rhino.Mocls.dll a la solución y usar [DeploymentItem].



Ahora el código del test queda así:




[TestMethod]
public void TestMethod1()
{
TestControllerBuilder tb = new TestControllerBuilder();
HomeController hc = tb.CreateController<HomeController>();
var result = hc.Index() as ViewResult;
Assert.AreEqual(string.Empty, result.ViewName);
}





Usamos la clase TestControllerBuilder que es capaz de crear controladores con todos los “objetos http” inicializados (con Mocks).



Los Mocks de MvcContrib no solo hacen que el código del controlador no de un error, también simulan la funcionalidad de la clase real. Por lo tanto podemos probar que el método Index guarda algo en la sesión:




[TestMethod]
public void TestMethod1()
{
TestControllerBuilder tb = new TestControllerBuilder();
HomeController hc = tb.CreateController<HomeController>();
hc.Index();
Assert.AreEqual("pepe", hc.HttpContext.Session["data"]);
}





¿Que os parece? MvcContrib es una manera elegante y fácil de poder probar nuestros controladores que tienen dependencias contra “objetos http” como la sesión.



Un saludo a todos!!! ;-)



PD: Como siempre esto es un crosspost desde mi blog de geeks.ms. Pásate por allí mejor que somos más!

martes, 29 de junio de 2010

[WebCast] ASP.NET MVC UoC DotNetClub – Material

Hola a todos!

Ayer 28 de Junio a las 20:00 di un WebCast de introducción a ASP.NET MVC gracias a la gente del UoC DotNetClub. La verdad es que yo me lo pasé fenomenal, a pesar de no ser muy ducho con el LiveMeeting que digamos, pero bueno, jejejeee… :)

Visto en retrospectiva, que siempre es fácil, intenté dar un vistazo (muy) rápido a todo ASP.NET MVC, explicando cual es la necesidad de un nuevo framework para el desarrollo de aplicaciones web en ASP.NET y luego comentando cada uno de los aspectos propios. No puede entrar en tanto detalle en algunos puntos en los que quería hacer especial incapié (sobretodo la factoría de controladores) y me gustaría haber dado más ejemplos de ViewResults y ActionFilters pero la verdad es que iba mirando el reloj, porque había muchas cosas que contar… y muy poco tiempo. Honestamente ahora quizá un par de puntos que intenté dar no los hubiera mencionado porque creo que no pude explicarlos como se merecen.

Después en el turno de preguntas me realizaron varias y aunqué las contesté muy rápidamente, creo que muchas son muy interesantes, así que dedicaré un post a cada una de ellas. Cuando vaya escribiendo los posts iré actualizando este a modo de índice. Las preguntas que voy intentar resolver son las siguientes:

  1. ¿Como pasar parámetros a un controlador?
  2. ¿Como podemos crear vistas “complejas”?
  3. ¿Como usar la sesión desde ASP.NET MVC?
  4. ¿Como usar cookies desde ASP.NET MVC?
  5. ¿Como simular el control Repeater desde ASP.NET MVC?
  6. ¿Tenemos grids tan potentes en ASP.NET MVC?

Lo que no aseguro es que las responda precisamente en este orden, ya que para algunos temas necesito más preparación que para otros y no se en que orden voy a abordarlos. También si me llega alguna pregunta más por mail o por twitter y que considere que es de interés general la añadiré.

Sólo me queda dejaros el material: el powerpoint que usé (tenéis plena libertad para usarlo como y para lo que queráis) y los ejemplos que enseñé. Algunos de los ejemplos no son exactamente los mismos, puesto que los iba creando desde cero en el webcast, pero se parecen bastante. Aquí tenéis el enlace a la carpeta en skydrive donde estan el powerpoint y un zip con las demos:

Finalmente sólo dar las gracias a la gente del UoC DotNetClub y por supuesto a todos los que asististeis al webcast. Fue un auténtico placer.

Un saludo!

PD: Este es un crosspost desde mi blog en geeks.ms. Mejor pásate por allí que somos más!

PD2: El post que iré actualizando a modo de índice no es este, sinó el que hay en geeks.ms!

viernes, 18 de junio de 2010

Usa las interfaces… que para eso están!

Ole! Vaya título tan imperativo me ha salido, eh??? Un post cortito para comentar un problemilla que hemos tenido en casa de un cliente, y que al final era debido por no usar interfaces (en nuestro caso interfaces COM).

El problemilla…

En todos los ordenadores de desarrollo el sistema funcionaba perfectamente (como siempre… en mi máquina funciona!).

En los ordenadores de prueba el sistema daba error, concretamente se quejaba que no encontraba el ensamblado mshtml.dll. Nosotros usábamos una referencia contra el ensamblado PIA que está en “%PROGRAMFILES%\Microsoft.NET\Primary Interop Assemblies” y que se llama Microsoft.mshtml.dll.

Vimos que efectivamente dicho ensamblado no se distribuía junto con nuestro proyecto y que no estaba en la GAC de los ordenadores, así que cambiamos la referencia para que fuese con Copy Local=true. Ahora sí que aparecia el ensamblado: Microsoft.mshtml.dll.

Desplegando esta versión de Microsoft.mshtml.dll el sistema… seguía sin funcionar. Pero ahora ya no daba errores de carga de assemblies ni de validaciones falladas de strong name, sino que daba una excepción:

Unable to cast COM object of type 'System.__ComObject' to class type 'mshtml.HTMLDocumentClass'. COM components that enter the CLR and do not support IProvideClassInfo or that do not have any interop assembly registered will be wrapped in the __ComObject type. Instances of this type cannot be cast to any other class; however they can be cast to interfaces as long as the underlying COM component supports QueryInterface calls for the IID of the interface.

El código que daba el error era el siguiente:

var mainDocument = (mshtml.HTMLDocumentClass)webBrowserControl.Document.DomDocument;





La solución



Bueno… Lo primero que pensamos era que alguna diferencia de versión de internet explorer podía causar eso, pero descartamos el tema: aunque es cierto que había distintas versiones (aunque todos los ordenadores tenían la 6(*), no todos tenían las mismas actualizaciones) había un ordenador de desarrollo que tenía la 8… Si compilábamos en dicho ordenador podíamos ejecutar el programa en otros ordenadores de desarrollo que tuvieran la 6. Estaba claro que interet explorer no era el problema.



Entonces? Bueno… si habéis leído el título del post ya sabréis la causa: estábamos utilizando directamente las clases COM (P.ej. HTMLDocumentClass) en lugar de usar la interfaz (p.ej. IHTMLDocument2). Cambiando el código por el siguiente:




var mainDocument = (mshtml.IHTMLDocument2)webBrowserControl.Document.DomDocument;





Todo funcionó a la perfección!



¿Conclusion? Pues eso… usad las interfaces (COM) que para eso están! :)



Un saludo!!!!



(*) Sí, sí, sí… Internet Explorer 6 ya no está suportado por MS, es más viejo que matusalén, tienes más agujeros de seguridad que un queso gruyere pero… en muchos sitios sigue formando parte de la plataforma corporativa.



PD: Esto es un crosspost de mi blog en geeks.ms. Pásate mejor por allí, hombre!!! ;-)

miércoles, 16 de junio de 2010

ASP.NET MVC: Create tus propias validaciones

Una de las noverdades de ASP.NET MVC 2 es que lleva integrado el uso de Data Annotations para permitirnos validar los modelos. En ASP.NET MVC 1 también era posible pero no era un proceso tan integrado como con la nueva versión.

Mediante Data Annotations podemos indicar un conjunto de reglas que deben cumplir las propiedades de nuestros modelos. Así para indicar que el campo Login es obligatorio basta con decorar la propiedad correspondiente:

public class UserData
{
[Required(ErrorMessage="El nombre de usuario NO puede estar vacío")]
public string Login { get; set; }
}





Y luego dejar que el sistema de ASP.NET MVC 2 haga “la magia”: cuando el model binder deba reconstruir el objeto UserData a partir de los datos de la request, si no existe el dato para la propiedad Login, automáticamente nos pondrá el ModelState a inválido:




[HttpPost()]
public ActionResult Index(UserData data)
{
if (!ModelState.IsValid) {
// Hay errores en el modelo (data.Login está vacío)
return View(data);
}

// El modelo es correcto... realizar tareas
}





Pero bueno… lo normal es que los atributos que vienen de serie se te queden “cortos” y que tarde o temprano necesites crear tus propias validaciones… y este es el motivo de este post.



Vamos a realizar un ejemplo sencillo: crearemos una validación que indique si una propiedad string contiene una representación válida de un número entero. Vamos a soportar notación decimal, hexadecimal (prefijada por 0x), octal (prefijada por 0) y binaria (prefijada por 0b). Así:




  • 1234567890 es una cadena válida (decimal)


  • 0xaa112d es una cadena válida (hexadecimal prefijada por 0x)


  • 01239 es una cadena inválida (prefijo 0 indica octal y 9 no es carácter octal).


  • 0b00112101 es una cadena inválida (prefijo 0b indica binario y el carácter 2 no es binario).



Vamos a ello?



1. Creación de la validación en el servidor



Para crear una validación en el servidor debemos crear un nuevo atributo que herede de ValidationAttribute y que contenga él código de la validación sobrecargando el método IsValid:




public class DeveloperIntegerAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
bool valid = false;
if (value is string && value != null)
{
string sval = value as string;
if (sval.StartsWith("0x") || sval.StartsWith("0X"))
{
valid = CheckChars(sval.Skip(2), "0123456789abcdefABCDEF");
}
else if (sval.StartsWith("0b") || sval.StartsWith("0B"))
{
valid = CheckChars(sval.Skip(2), "01");
}
else if (sval.StartsWith("0"))
{
valid = CheckChars(sval.Skip(1), "01234567");
}
else
{
valid = CheckChars(sval, "0123456789");
}
}
return valid ? ValidationResult.Success : new ValidationResult(ErrorMessage);
}

private bool CheckChars(IEnumerable<char> str, string validchars)
{
return str.All(x => validchars.Contains(x));
}
}





Como podéis ver el código es realmente sencillo: debemos comprobar que el parámetro “value” satisface nuestras condiciones y en este caso devolver ValidationResult.Success y en caso contrario devolver un ValidationResult con el mensaje de error asociado.



Ahora ya podemos aplicar nuestro nuevo flamante atributo [DeveloperInteger] a nuestras clases de modelo:




public class FooModel
{
[Required]
[DeveloperInteger(ErrorMessage="Numero no correcto (se aceptan prefijos 0 - octal, 0x - hexa, 0b - binario y ninguno para decimal")]
public string DeveloperNumber { get; set; }

[Required]
public string OtraCadena {get; set;}
}





Y listos! Con esto la validación ya está integrada en el sistema de ASP.NET MVC. Podemos comprobarlo si creamos una vista para crear datos de tipo FooModel:




<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<CustomValidations.Models.FooModel>" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Index</title>
</head>
<body>
<% using (Html.BeginForm()) {%>
<%: Html.ValidationSummary(true) %>
<fieldset>
<legend>Fields</legend>
<div class="editor-label">
<%: Html.LabelFor(model => model.DeveloperNumber) %>
</div>
<div class="editor-field">
<%: Html.TextBoxFor(model => model.DeveloperNumber) %>
<%: Html.ValidationMessageFor(model => model.DeveloperNumber) %>
</div>
<div class="editor-label">
<%: Html.LabelFor(model => model.OtraCadena) %>
</div>
<div class="editor-field">
<%: Html.TextBoxFor(model => model.OtraCadena) %>
<%: Html.ValidationMessageFor(model => model.OtraCadena) %>
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
<% } %>
<div>
<%: Html.ActionLink("Back to List", "Index") %>
</div>
</body>
</html>





Este código es el código estándard que genera Visual Studio si añadimos una vista tipada para Crear objetos FooModel. Fijaos en el uso de ValidationMessageFor para mostrar (en caso de que el modelo no esté correcto) los mensajes de error correspondientes.



Y finalmente como siempre en el controlador, el par de métodos para mostrar la vista para entrar datos y para recoger los datos y mirar si el modelo es válido:




public ActionResult Index()
{
return View();
}

[HttpPost()]
public ActionResult Index(FooModel data)
{
if (!ModelState.IsValid)
{
return View(data);
}
else
{
// modelo es correcto
return RedirectToAction("Hola");
}
}





Más fácil imposible, no??? :)



2. Validación en Javascript



Crear validaciones en servidor es muy fácil, pero ahora lo que se lleva es validar los datos en el cliente. Esto hace nuestra aplicación más amigable ya que le evitamos esperas al usuario (no enviamos datos incorrectos al servidor).




Nota: Se ha repetido innumerables veces pero no está de más decirlo de nuevo: Las validaciones en cliente no pueden sustituir nunca a las validaciones en servidor. El objetivo de validar en cliente no es garantizar la seguridad ni la consistencia de los datos, és únicamente proporcionar mejor experiencia de usuario. Siempre debe validarse en servidor, siempre!




Para validar en cliente debemos realizar tres pasos: Habilitar la validación de cliente en la vista, activarla en servidor y crear el código javascript. Veamos cada uno de esos pasos.



El primero es el más fácil, basta con añadir la llamada al método EnableClientValidation() que está en el HtmlHelper. Este método es de la Microsoft Ajax Library así que debéis referenciarla con tags <script>:




<head runat="server">
<script src="../../Scripts/MicrosoftAjax.js" type="text/javascript"></script>
<script type="text/javascript" src="../../Scripts/MicrosoftMvcValidation.js"></script>
<title>Index</title>
</head>
<body>
<% Html.EnableClientValidation(); %>





De esta manera veréis p.ej. que automáticamente ya nos valida si los campos de texto están vacíos (porque usábamos [Required]). Obviamente nuestra validación propia, no la realiza en javascript… veamos como podemos hacerlo.



Primero debemos activar la validación en servidor, y eso se consigue creando una clase derivada de DataAnnotationsModelValidator<TAttr> donde TAttr es el tipo del atributo que tiene la validación, en nuestro caso DeveloperIntegerAttribute. En esta clase debemos redefinir el método GetClientValidationRules para devolver la lista de validaciones en cliente a ejecutar (métodos javascript a llamar).




public class DeveloperIntegerValidator : DataAnnotationsModelValidator<DeveloperIntegerAttribute>
{
private string message;
public DeveloperIntegerValidator(ModelMetadata metadata, ControllerContext cc, DeveloperIntegerAttribute attr)
: base(metadata, cc, attr)
{
message = attr.ErrorMessage;
}
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
var rule = new ModelClientValidationRule()
{
ValidationType = "devinteger",
ErrorMessage = message
};
// Aquí podríamos añadir parámetros a la función javascript:
// rule.ValidationParameters.Add("parametro", valor);
return new[] { rule };
}
}





Fijaos que devolvemos una colección de objetos ModelClientValidationRule que representan los métodos javascript a llamar para realizar nuestra validación (podemos asociar más de uno).



Un paso adicional que debemos realizar es indicar que la clase DevelolperIntegerValidator gestiona las validaciones en cliente para DeveloperIntegerAttribute y para ello añadimos la siguiente línea en global.asax (p.ej. en Application_Start):




DataAnnotationsModelValidatorProvider.RegisterAdapter(
typeof(DeveloperIntegerAttribute), typeof(DeveloperIntegerValidator));





Ahora ya sólo nos queda crear nuestro método javascript. Realmente no tenemos una función llamada devinteger sino que accedemos a Sys.Mvc.ValidatorRegistry.validators y establecemos una entrada llamada devinteger cuyo valor es un método que recoge los parámetros de validación (si hubiese, en nuestro caso no hay) y devuelve otra función que es la que realiza la validación…



Sí, parece complicado pero tampoco lo es tanto:




Sys.Mvc.ValidatorRegistry.validators["devinteger"] = function (rule) {
// Si tuvieramos un parametro llamado parametro lo recogeríamos aqui:
// var parameter = rule.ValidationParameters["parametro"];
// Debemos devolver la función que realiza la validación
return function (value, context) {
var svalue = "" + value;
var chars = "0123456789";
var radix = 10;
var start = 0;
if (svalue.substr(0, 2) == "0x" || svalue.substr(0, 2) == "0X") { chars = "01234567890abcdefABCDEF", start = 2; }
else if (svalue.substr(0, 2) == "0b" || svalue.substr(0, 2) == "0B") { chars = "01"; start = 2; }
else if (svalue.search(0, 1) == "0") { chars = "01234567"; start = 1; }

return (function (str, chars) {
var ok = true;
for (var i = 0; i < str.length; i++) {
var char = str.charAt(i);
ok = chars.indexOf(char) != -1;
if (!ok) break;
}
return ok;
})(svalue.substring(start), chars) ? true : rule.ErrorMessage;
};
};





Fijaos en el return function (value, context). Aquí devolvemos la función anónima que realmente realiza la validación. Dicha función recibe dos parámetros: value, que es el valor a evaluar y context que es un contexto de validación. El código dentro de esta función anónima debe devolver true si el value valida satisfactoriamente y la cadena de error en caso contrario.



Y listos! Ya tenemos la validación por javascript en cliente! Si alguien sabe alguna manera mejor de realizar dicha validación en javascript que me lo diga (sí, javascript no es mi fuerte). Yo intenté usar parseInt, pero dado que parseInt valida los carácteres válidos hasta que encuentra el primer inválido no me servia (para mi 0x1j es inválido y no el número 1).



Y listos! Ya tenemos la validación en cliente para nuestro validador propio… Que os parece? Fácil, no???? :)



Un saludo!



PD: Como siempre… esto es un crosspost desde mi blog en geeks.ms. Pásate mejor por allí!!! :)

viernes, 11 de junio de 2010

Sobre ir recorriendo enumerables…

El otro día, Oren Eini (aka Ayende) escribió en su blog un post, en respuesta a otro post escrito por Phil Haack (aka Haacked). En su post Phil mostraba un método extensor para comprobar si un IEnumerable<T> era null o estaba vacío (y sí, Phil usa Any() en lugar de Count() para comprobar si la enumeración está vacía):

public static bool IsNullOrEmpty<T>(this IEnumerable<T> items) {
return items == null || !items.Any();
}





Aquí tenéis el post de Phil: Checking For Empty Enumerations



Y este es el de Oren: Checking For Empty Enumerations



Oren plantea una cuestión muy interesante al respecto de los enumerables y es la posibilidad de que haya enumeraciones que sólo se puedan recorrer una sóla vez. En este caso el método de Phil fallaría, puesto que al llamar a .Any() para validar si hay un elemento este elemento sería leído (y por lo tanto se perdería) por lo que cuando después recorriesemos el enumerable no obtendríamos el primer elemento.



Pero el tema es… ¿pueden existir enumerables de un sólo recorrido? Pues poder, pueden pero mi opinión personal es que no son enumerables válidos.



Vayamos por partes… Que es un enumerable? Pues algo tan simple como esto:




public interface IEnumerable<out T> : IEnumerable
{
IEnumerator<T> GetEnumerator();
}






Nota: Si no os suena eso de “out” es una novedad de C# 4 que nos permite especificar covarianza en el tipo genérico. No afecta a lo que estamos discutiendo en este post. Tenéis más info en este clarificador post de Matt Hiddinger.




Bueno… resumiendo lo único que podemos hacer con un enumerable es… obtener un enumerador. Y que es un enumerador? Pues eso:




public interface IEnumerator<out T> : IDisposable, IEnumerator
{
T Current { get; }
// Esto se hereda de IEnumerator
object Current { get; }
bool MoveNext();
void Reset();
// Esto se hereda de IDisposable
void Dispose();
}





Un enumerador es lo que se recorre: Tenemos una propiedad (Current) que nos permite obtener el elemento actual así como un método MoveNext() que debe moverse al siguiente elemento (devolviendo true si este movimiento ha sido válido). Hay otro método adicional Reset() que debe posicionar el enumerador antes del primer elemento, aunque en la propia MSDN se indica que no es un método de obligada implementación. Así pues, ciertamente no podemos asumir que un IEnumerator se pueda recorrer más de una vez. Así que no lo hagáis: asumid que los IEnumerator sólo pueden recorrerse una vez.



Pero que un IEnumerator sólo pueda recorrerse una vez no implica que no pueda obtener dos, tres o los que quiera IEnumerator a partir del mismo IEnumerable: puedo llamar a GetEnumerator() tantas veces como quiera.



Y, ahí está el quid de la cuestión del post de Oren: el método Any() crea un IEnumerator y luego el foreach crea otro IEnumerator. Así pues en este código:




void foo(IEnumerable<T> els)
{
if (els.Any()) {
foreach (var el in els) { ... }
}
}





Se crean dos IEnumerator: uno cuando se llama a Any() y otro cuando se usa el foreach. Y ambos IEnumerators nos permiten recorrer el IEnumerable des del principio: por eso no perdemos ningún elemento (al contrario de lo que afirma Oren en su post).



Conclusión



Antes he dicho que pueden existir IEnumerables que solo se puedan recorrer una sola vez, pero que en mi opinión no son correctos. Cuando digo que pueden existir me refiero a que se pueden crear, cuando digo que (en mi opinión) no son correctos me refiero a que según la msdn (http://msdn.microsoft.com/en-us/library/system.collections.ienumerable.getenumerator.aspx) la llamada a GetEnumerator debe:




  • Devolver un enumerador (IEnumerator) que itere sobre la colección (Returns an enumerator that iterates through a collection).


  • Inicialmente el enumerador debe estar posicionado antes del primer elemento (Initially, the enumerator is positioned before the first element in the collection).



Por lo tanto de aquí yo interpreto que cada vez que llame a GetEnumerator obtendré un enumerador posicionado antes del primer elemento, y dado que en ningún momento se me avisa que un IEnumerable pueda admitir una SOLA llamada a GetEnumerator(), entiendo que puedo obtener tantos enumeradores como quiera y que cada llamada me devolverá un IEnumerator posicionado antes del primer elemento.



Así que podéis usar el método de Phil para comprobar si un IEnumerable está vacío sin miedo a perder nunca el primer elemento!



Un saludo!



PD: Esto es un crosspost desde mi blog de geeks.ms. Pásate por allí que somos más!! ;-)