Entre otras cosas, comentábamos que SignalR crea una capa de abstracciones sobre una conexión virtual permanente entre cliente y servidor, sobre la que podemos trabajar de diferentes formas:
- mediante conexiones persistentes, la opción de menor nivel, que proporciona mecanismos de notificación de conexión y desconexión de clientes, así como para recibir y enviar mensajes asíncronos a clientes conectados, tanto de forma individual como colectiva.
- mediante el uso de “hubs”, que ofrece una interfaz de desarrollo mucho más sencilla, con una integración entre cliente y servidor que parece pura magia, y que seguro será la opción más utilizada por su potencia y facilidad de uso.
Publicado por José M. Aguilar a las 9:14 a. m.
Etiquetas: asp.net, aspnetmvc, desarrollo, signalr
Por ejemplo, en Google Docs, si estamos editando un documento online y otro usuario accede al mismo, podemos ver sobre la marcha que ha entrado, e incluso las modificaciones que va realizando sobre el documento. O algo más cotidiano, en un simple chat vía web van apareciendo los mensajes tecleados por nuestros compañeros de sala como por arte de magia. Ambos sistemas utilizan el mismo tipo de solución: el envío asíncrono de datos entre servidor y clientes en tiempo real.
En esta serie de artículos veremos cómo podemos implementar sorprendentes funcionalidades de este tipo utilizando SignalR, un framework open source desarrollado por gente del equipo de ASP.NET, que nos facilitará bastante la tarea.
Publicado por José M. Aguilar a las 8:58 p. m.
Etiquetas: ajax, asp.net, aspnetmvc, desarrollo, signalr
.Net
- Mono in 2011
Miguel de Icaza
Asp.net
- How to combine a Worker Role with a MVC4 Web Role into a single instance
Liam Cavanagh - ASP.NET MVC + Selenium + IISExpress
Stephen Walther - Model Binding with Dropdown List in ASP.NET 4.5
Abhijit Jana - Knockout 2.0.0 released
Steve Sanderson - Free eBook: OWASP Top 10 for .NET developers
Troy Hunt - Solving Duplicate Content with Distinct URLs Issue in ASP.NET MVC
Imran Baloch's - New Request validation features in ASP.Net 4.5
K. G. Sreeju Nair - Using CORS to access ASP.NET services across domains
Dave Ward - HttpRequestBase vs HttpRequest
Eduard Tomás - Debugging Application_Start and Module Initialization with IIS and Visual Studio
Rick Strahl - Rotate an ASP.NET Image Control using HTML 5 Canvas
Suprotim Agarwal - Retoques a MVC y Razor para proyectos grandes–Una recomendación
Lucas Ontivero - High-Performance ASP.NET Caching
Peter Vogel - Microsoft blogging platform gains 33% performance boost after adopting RequestReduce
Matt Wrocks
Azure / Cloud
- Azure Monitor for Windows Phone 7 is out!
Ibon Landa - Libro gratuito sobre Windows Azure (Parte IV): Diseño y herramientas
CampusMVP - SQL Azure Q4 2011 Service Release
Ibon Landa
Data access
- MongoDB Best Practices
Inés Sombra - Inserciones masivas en SQL Server CE vs SQL Server vs MongoDb (y II)
Josué Yeray - Raven DB VI, La hora de las consultas…
Unai Zorrilla
Html/Css/Javascript
- JavaScript as a First Language
John Resig - The output element in HTML5
Richard Clark - Excssive, herramienta online para comprimir archivos CSS
Zach Will (Vía desarrolloweb.com) - Meet the CSS4 selectors
Catalin Rosu - JavaScript for C# developers: calling functions and the ‘this’ variable
Julian M. Bucknall - Top 10 “Must Follow” JavaScript Best Practices
Salman Siddiqui - Online Javascript Compression Tool
BrightBit - The rise and rise of JavaScript
Dan North - Using the JavaScript Prototype Property to Structure and Extend Code
Dan Wahlin's - Multiple Attribute Values
Chris Coyier - The Seven Deadly Sins Of JavaScript Implementation
Christian Heilmann (vía @alegrebandolero)
Visual Studio/Complementos
- C# + ReSharper = Awesome: Tip #5 – Replace Constructor with Factory Method
Alvin Ashcraft - ReSharper 6.1, dotCover 1.2 and dotTrace 4.5.2 Performance Released
Hadi Hariri - C# + ReSharper = Awesome: Tip #4 – Convert Abstract Class to Interface
Alvin Ashcraft - C# + ReSharper = Awesome: Tip #3 – Convert Into LINQ Expression
Alvin Ashcraft
Publicado en Variable not found
Sin embargo, no es este el único mecanismo de extensión del framework a este respecto: también podemos crear fácilmente nuevos atributos que aporten información extra de metadatos a las clases y propiedades del Modelo. Y esta es la razón de ser del interfaz
IMetadataAware
.Aprovecho además para pediros opinión sobre un nuevo formato de presentación de los enlaces, usando categorizaciones. De esta forma podréis acceder directamente a aquellos cuya temática os interese, en lugar de tener que leerlos todos para ver si hay alguno que al que valga la pena echar el vistazo. ¿Qué os parece? ¿Mejor así?
.Net
- Formatting Enumeration Constants
BlackWasp - Install-Package Roslyn
Kirill Osenkov - Return Multiple Values from Methods with Tuples
Peter Vogel - C# Fundamentals: Returning Zero or One Item As IEnumerable<T>
James Michael Hare - .Net Reflector y ILSpy, ¿podrían inferir mejor el código a partir del IL?
Lucas Ontivero
Asp.net
- [Video] ASP.NET vNext - Filtrado de datos - Value Provider Attributes
Luis Ruiz - How to use SignalR and Knockout in an ASP.NET MVC 3 web application to handle real-time UX updates
Justin Schwartzenberger - MVC and HTML5 Web Workers
Dean hume - Introducing SignalR.EventStream
Ben Dornis - Update jqGrid Html helper for ASP.NET MVC
Robin van der Knaap - Implement secure ASP.NET MVC applications
Jovan Popovic - Which is the Fastest Webserver? Apache, IIS, Nginx, LightHttpd or G-Wan?
WebPerformance (vía @campusmvp) - OWASP Top 10 for .NET developers part 10: Unvalidated Redirects and Forwards
Troy Hunt - Espresso Tip: IHttpHandler and IsReusable
David Neal - Introducción a less
Sergio León - Automatically trim html controls in a asp.net mvc project
Richard Wilde - On deploying ASP.NET MVC site as a desktop application
Andrei Marukovich - Todo sobre AutoEventWireUp en páginas ASP.NET Web Forms
José Manuel Alarcón - Using QUnit with Razor Layouts
Phil Haack - ASP.NET MVC ViewModel usage and pick your best pattern
Kazi Manzur Rashid - MVC Route/URL Generation Unit Tester
Codeplex - Easy URL rewriting in ASP.NET 4.0 web forms
Jalpesh Vadgama - Looking into Web performance in ASP.Net applications
Nikolaos Kantzelis - [Vídeo] ASP.NET vNext - Selección de datos (SelectMethod)
Luis Ruiz - Using SignalR to broadcast a slide deck
Marteen Balliauw
Azure / Cloud
- Utilizar Windows Azure AppFabric Caching como session provider
Gisela Torres - VMware Cloud Foundry cloud platform now supports .Net
Mary-Joe Foley - Now Available: SQL Azure Q4 2011 Service Release
Azure Team (vía @ibonilm) - The SQL Azure Team Unveils a New Server Management UI
Roger Jennings - Differences Between the Storage Emulator and Windows Azure Storage Services
MSDN
Conceptos
- Algorithms Course Materials
Jeff Erickson - Hash Functions
Bret Mulvey
Data access
- RavenDB (IV) La identidad de los documentos
Unai Zorrilla - Data Access Performance Comparison in .NET
Luis Rocha
Html/Css/Javascript
- 10 Best jQuery Form Plugins
jQuery4u - Your jQuery: Now With 67% Less Suck
Scott Kosman - JavaScript – Add Commas To Number
Shai Raiten - Frontend SPOF
Steve Souders - The 30 Most Effective jQuery Plugins
Awcore - Animations in HTML5
Florian Rappl - HTML 5 Input Types - How useful is this really going to be?
Rick Strahl - The CSS profilers are coming!
Lennart Schoors - A Key Code Checker for DOM Keyboard Events
Rick Strahl
Visual Studio/Complementos
- [HowTo] Agregar Ficheros a un Proyecto Existente
Javier Torrecilla - C# + ReSharper = Awesome: Tip #2 – Create Field
Alvin Ashcraft - NuGet 1.6 Release Notes
Nuget Team - [How To] Agregar un elemento de menú a Visual Studio.
Javier Torrecilla - Go To Definition for JavaScript Functions in Visual Studio 2011
Abhijit Jana - Introducing CSSCop - FxCop for stylesheets
Mads Kristensen
Otros
- La fruta más alta siempre sabe mejor
José Manuel Alarcón
Publicado en Variable not found
Y uno de estos casos es un detallito muy simple pero útil: establecer el foco de edición en un control concreto al cargar una página. En Webforms era suficiente con asignar al atributo
defaultFocus
del tag <form>
el nombre del control que nos interesara, y ya lo teníamos solucionado; ASP.NET MVC no trae ninguna solución “de serie” para conseguirlo, aunque, como veremos en este post, no es mucho más complicado que la alternativa Webforms una vez hemos preparado la infraestructura necesaria.Sin embargo, los atributos en el propio código de la clase no son la única vía para especificar metadatos en el framework. En este post veremos cómo extender el framework para crear nuevas vías para especificar esta información.
Required
o StringLength
no las definíamos a nivel de código mediante atributos, sino en el archivo de configuración, lo que podía aportar interesantes ventajas vistas a flexibilizar nuestras soluciones.En este post vamos a ver un nuevo ejemplo de cómo utilizar el mismo mecanismo de proveedores, pero esta vez para conseguir de forma muy sencilla simplificar el código de clases del modelo en las que todas sus propiedades sean por defecto obligatorias, salvo aquellas en las que indiquemos expresamente lo contrario.
Este mecanismo hace posible la creación de bundles, o paquetes de uno o varios archivos de scripts o estilos, que son optimizados en tamaño por el servidor y cargados desde las páginas en una única petición. El proceso de compactación se realizaría en servidor, pero sólo la primera vez, quedando almacenado en caché para las peticiones posteriores.
Publicado por José M. Aguilar a las 10:14 a. m.
Etiquetas: asp.net, aspnetmvc, desarrollo, optimización
Pero bueno, centrándonos en ASP.NET MVC 4, se trata de un adelanto de lo que ya se nos avanzó en el Roadmap del producto, y que se ha distribuido en dos paquetes distintos: uno para Visual Studio 2010, y otro para el flamante Visual Studio 11 Developer Preview, también lanzado durante el Build. Ambos pueden descargarse a través de Web Platform Installer, o desde los enlaces proporcionados en esta página.
Este post vamos a dedicarlo a comentar algunas de las cosas que me han ido llamando la atención durante los primeros paseos por el producto. Y ojo, que se trata de una versión muy preliminar: puede tener fallos, cambiar conforme vayamos aproximándonos a la versión final, o incluso hay características que podrían desaparecer.
Por ejemplo, en el caso de la distribución para Visual Studio 2010, no debería interferir con otras versiones del producto instaladas, por lo que deberíais poder probarla en vuestra máquina habitual sin miedo a que os rompa nada. Sin embargo, sí que me ha dado algún problemilla al compilar proyectos MVC 3 existentes, puesto que se introducían dependencias a la versión 2 de
System.Web.Webpages
(distribuida con MVC 4), pero vaya, nada que no pueda solucionarse en un par de minutos retocando las referencias del proyecto.Publicado por José M. Aguilar a las 10:17 a. m.
Etiquetas: asp.net, aspnetmvc, desarrollo, novedades
A raíz de dicho post, el amigo Héctor S. (¡gracias!) me envió una interesante pregunta: ¿por qué no utilizar la anotación
System.ComponentModel.DataAnnotations.
EnumDataType
para ello? Y la respuesta corta es: pues también se podría. Con sus particularidades, pero sería posible controlar los valores de entrada a un
enum
usando esta anotación.Empecemos por el principio.
EnumDataTypeAttribute
es una anotación introducida en .NET 4 que nos permite indicar que una propiedad (no necesariamente de tipo enum
) admite exclusivamente valores definidos en un enumerado. Mejor lo vemos con algo de código: public enum Sexo
{
Hombre = 1,
Mujer = 2
}
[EnumDataType(typeof(Sexo))]
public int Sexo { get; set; }
En el ejemplo anterior observamos una propiedad
Sexo
de tipo int
, lo que le permite virtualmente alojar cualquier valor entero posible. Sin embargo, el hecho de decorarla con el atributo EnumDataTypeAttribute
y asociarla a la enumeración Sexo
hará que en la validación de la entidad su valor sea considerado incorrecto si éste no pertenece a los permitidos en ella, en este caso, 1 y 2.Su uso es interesante, por ejemplo, como método para acercar representaciones de la información. Por ejemplo, si la propiedad
Sexo
está mapeada hacia un campo de base de datos, es bastante habitual que éste sea de tipo entero, por lo que el hecho de declararla en la entidad como int
facilita el movimiento de datos entre uno y otro. Al decorarla con EnumDataType
lo que hacemos simplemente es asegurar que contendrá un valor correcto conforme al enumerado, pero ojo, siempre que la entidad sea validada considerando sus anotaciones (como ocurre durante el binding).Y volviendo al tema del post, entonces, ¿por qué no utilizamos este método para asegurar la validez de los datos en el post al que hacía referencia, en lugar de montar un binder personalizado?
Pues el motivo principal sería la pereza. Realmente, me parece demasiado trabajoso el tener que decorar cada propiedad
enum
del modelo con una declaración tan redundante como la siguiente: public class Persona
{
public string Nombre { get; set; }
[EnumDataType(typeof(Sexo))]
public Sexo Sexo { get; set; }
...
}
A ver, si estoy declarando una propiedad de tipo
Sexo
, me resulta pesado tener que decirle algo más arriba que ésta es un tipo de datos enumerado de tipo Sexo
.Pero bueno, salvando este incómodo detalle,
EnumDataType
es una buena fórmula para controlar la corrección de los valores de entrada, y sobre todo muy cómoda si la propiedad parámetro a actualizar es de un tipo no enum
, como un int
:. [EnumDataType(typeof(Sexo))]
public int Sexo { get; set; }
Su uso aporta, además, otras posibilidades interesantes. Por ejemplo, sería posible crear editores personalizados sobre el tipo
int
que, basándose en este atributo, mostraran a nivel de vista desplegables u otros controles de selección de forma automática.Es importante tener en cuenta que, a diferencia de mi enfoque, la comprobación del atributo se realiza durante el proceso de validación, que es posterior al binding en sí. La ventaja de esto es que se integra de forma natural en dicho proceso (por ejemplo, ModelState.IsValid retornará falso si hemos intentado introducir un "3" en Sexo), pero también pueden producirse errores previos generados por el propio binder si no es capaz de realizar la transformación de forma automática (por ejemplo, al intentar un valor textual no presente en la enumeración, como 'Gato'), que de la otra forma sí sería posible controlar.
Publicado en: Variable not found.
En este post vamos a ver un ejemplo ómo podemos utilizar este nuevo componente para montar en pocos segundos el sistema de membresía, roles y perfiles de una aplicación web utilizando SQL Compact Edition, algo que por otra parte no es una idea nada descabellada para sitios de pequeño calibre. Recordad que esta edición de SQL Server no requiere un servicio funcionando en el servidor (se ejecuta en el mismo proceso que la aplicación web), por lo que puede resultar muy apropiada para su uso en pequeños sitios donde no se justifica la implantación de sus hermanas mayores (express o superiores).
1. Instalamos el tooling en Visual Studio 2010 SP1
Lo primero que vamos a hacer es instalar las herramientas de SQL Compact Edition para el Visual Studio. Éstas las podéis descargar desde este enlace, aunque antes debéis aseguraros de tener instalado el Service Pack 1 de Visual Studio.Esto no es estrictamente necesario si no pensamos crear o acceder a la base de datos desde Visual Studio, aunque en cualquier caso es recomendable para poder echar un vistazo a la estructura y datos más adelante.
2. Creamos una aplicación web
Creamos ahora una aplicación Web desde Visual Studio. No importa si es ASP.NET MVC o WebForms: en la pila de tecnologías estamos un nivel por abajo, directamente jugando con características de ASP.NET, que, como sabéis, son comunes a ambos frameworks.Para ilustrar nuestro ejemplo crearemos una aplicación Webforms seleccionando la plantilla “Aplicación Web ASP.NET” en el cuadro de diálogo de nuevo proyecto del IDE, pero podríamos perfectamente elegir la “Aplicación web de ASP.NET MVC” y todo sería exactamente igual.
3. Descargamos de Nuget los componentes que necesitamos
Para lograr nuestros objetivos sólo necesitaremos dos paquetes:- el proveedor de datos SQL CE, que nos permitirá conectarnos a esta base de datos y acceder a su información ya sea utilizando directamente las clases de ADO.NET como Entity Framework,
- el paquete de Proveedores Universales para ASP.NET, que creará la abstracción sobre el origen de datos concreto y nos permitirá trabajar virtualmente con cualquier motor relacional (aunque, como comentaba en el post anterior, por las pruebas que he hecho de momento no sea así).
PM> Install-Package System.Web.Providers [...] Successfully installed 'System.Web.Providers 1.0.1'. PM> Install-Package SqlServerCompact [...] Successfully installed 'SqlServerCompact 4.0.8482.1'.
Si en cambio sois más de utilizar el GUI, podéis buscar en la galería los paquetes “ASP.NET Universal Providers” y “SqlServerCompact”:
4. Modificamos la cadena de conexión
El proceso de instalación del paquete de proveedores habrá modificado nuestro web.config, dejándolo listo para su utilización con SQL Express. Como no es esto lo que queremos, acudimos a este archivo y modificamos la cadena de conexión usada por los proveedores:<connectionStrings> <add name="DefaultConnection" connectionString="Data Source=|DataDirectory|\Datos.sdf;" providerName="System.Data.SqlServerCe.4.0" /> </connectionStrings>
Observad que estamos indicando que el proveedor de datos es SqlServerCe (lo hemos descargado previamente usando Nuget), y que la base de datos se llamará datos.sdf y estará ubicada en la carpeta App_Data del proyecto.
De forma opcional, podemos aprovechar el momento para limpiar un poco el web.config, eliminando líneas que vienen en la plantilla de proyectos por defecto, pero que ya no vamos a necesitar.
5. ¡Y esto es todo!
Pues sí, eso es todo lo que necesitamos hacer. En este momento ya tenemos configurados los proveedores de membresía, roles y perfiles. De hecho, dado que las plantillas de proyecto tanto para MVC como para Webforms ya incluyen un sistema de autenticación y registro básico, podemos directamente ejecutarla y utilizar sus funcionalidades para registrarnos y autenticarnos en el sistema:Asimismo, dado que se están utilizando mecanismos estándar en el framework, podemos utilizar la propia herramienta de gestión de ASP.NET para crear los usuarios, asignarles funciones o definir reglas de acceso por carpetas. Como ya sabéis, esta utilidad viene incluida de serie en el framework, y podemos acceder a ella a través del menú “Proyecto > Configuración de ASP.NET” de Visual Studio o pulsando sobre un icono que aparece en el explorador de soluciones.
Eso sí, para comenzar a definir roles y asignarlos a usuarios es necesario activar previamente el proveedor de roles (
DefaultRoleProvider
), ya sea desde la herramienta de administración, bien añadiendo en el nodo <RoleProvider>
del web.config el atributo enabled="true"
.De la misma forma, podemos activar la persistencia del estado de sesión sobre la misma base de datos estableciendo el atributo
mode="Custom"
en el nodo <sessionState>
(por defecto está configurado como "InProc"
).En cualquier caso, una vez accedamos a alguno de estos proveedores, podremos observar que se ha creado de forma automática la base de datos llamada Datos.sdf en la carpeta App_Data. Por supuesto, también podríamos haber indicado el nombre de un base de datos existente (creada por ejemplo desde el mismo VS) y el sistema habría creado únicamente las tablas e índices usados por los proveedores sin afectar al resto de objetos.
Recapitulando: si ya tenemos el tooling y el proveedor de datos para SQLCE en un proyecto, lo único que tenemos que hacer es, en primer lugar, descargar a través de Nuget el paquete de Proveedores Universales de ASP.NET, y a continuación ajustar la cadena de conexión en el web.config. Más sencillo, imposible.
He dejado en Skydrive una solución de ejemplo con un proyecto Webforms y otro MVC para que podáis probarlo de forma sencilla. En la base de datos hay un usuario creado (jmaguilar con clave jmaguilar, aunque podéis registraros desde las propias aplicaciones); para que podáis ver cómo se comporta el sistema de autenticación y membresía, no se permite el acceso a las páginas “Acerca de” a usuarios no identificados previamente.
Publicado en: Variable not found.
Lo interesante que tiene el ensamblado
System.Web.Providers
, que así es como se distribuye el componente, es que podemos modificar el motor de persistencia sobre el que trabajan simplemente tocando la cadena de conexión en el web.config, evitándonos el tener que crear proveedores en cuanto nos salíamos de los provistos por defecto en la plataforma (SQL Server). O en otras palabras, tenemos aquí un proveedor para gobernarlos a todos ;-) Como no podría ser de otra forma, la distribución del paquete de proveedores se realiza a través de Nuget, por lo que su instalación y puesta en marcha es sencillísima.
Una vez descargado e instalado el paquete “ASP.NET Universal Providers”, además de añadirse el ensamblado
System.Web.Providers
y sus correspondientes referencias al proyecto, veremos que en el web.config se ha incluido una cadena de conexión, llamada “DefaultConnection
”, que es la que será utilizada por estos nuevos proveedores. Por defecto podremos comprobar que esta conexión está configurada para trabajar sobre una instancia de usuario de SQL Express, pero simplemente modificándola podremos hacer que funcione sobre ediciones superiores de SQL Server, SQL Azure o SQL Compact.Un detalle importante es que, para evitar daños colaterales, el proceso de instalación no elimina la cadena de conexión previa (esa que vemos con la denominación “
ApplicationServices
”), ni la configuración de proveedores previa: simplemente añade el nuevo proveedor y lo establece por defecto, por lo que podemos eliminar tranquilamente esas configuraciones que ya no utilizamos.Una vez configurada correctamente la cadena de conexión (“
DefaultConnection
”) ya no será necesario tocar nada más; el sistema se encargará de crear las tablas necesarias para almacenar la información de usuarios, roles, perfiles, e incluso para hacer persistente el estado de sesión (si utilizamos el proveedor incluido en este componente); además, si no existe la base de datos, será capaz de crearla automáticamente si tenemos permisos para ello. Como nota negativa, seguro que habéis pensado que ya que es posible modificar el proveedor de datos a nivel de archivo de configuración, parece lógico que podamos utilizar cualquier origen de datos disponible, ¿verdad? Pues no necesariamente. He probado con el conector MySQL y no ha habido forma de echar a andar estos proveedores debido a un problema de incompatibilidad del tipo “image” en los metadatos. No sé si se debe a limitaciones del Universal Provider, o bien se trata de algún problema en la implementación del conector MySQL.
Sin duda, es un avance interesante que puede ahorrarnos bastante tiempo en esa tarea tan molesta que es montar la infraestructura de membresía y seguridad de nuestras aplicaciones. Y además, como comentaba Hanselman en su momento, estos proveedores es posible que formen parte de la próxima versión del framework, por lo que habrá que estar atentos a ellos.
En un próximo post os mostraré un ejemplo práctico, cómo utilizar este componente para poner en marcha el sistema de membership, roles y perfiles sobre una base de datos SQLCE.
Publicado en: Variable not found.
Publicado por José M. Aguilar a las 9:43 a. m.
Etiquetas: asp.net, aspnetmvc, desarrollo, novedades, webforms
enum
en C#), a pesar de su comodidad e idoneidad en multitud de ocasiones, suele siempre una tarea pesada debido a la falta de soporte directo existente en algunas tecnologías.Por ejemplo, hasta la fecha, Entity Framework los ha ignorado por completo (aunque que esto va a cambiar y la próxima versión de EF sí soportará enums :-)), lo cual resulta muy incómodo en el momento de crear componentes de acceso a datos, viéndonos obligados a realizar demasiadas conversiones, o incluso a veces el plantearnos otras soluciones, como el uso de constantes numéricas en su lugar para simplificar las operaciones.
public enum Color
{
Rojo = 1,
Verde = 2,
Azul = 3
}
Ya centrados en ASP.NET MVC, un aspecto muy interesante del mecanismo de binding es su capacidad para trabajar de forma muy natural con estos tipos de datos, aunque esto pueda acarrear algunos problemillas. Veámoslo con un ejemplo partiendo del siguiente controlador:public class EnumController : Controller
{
public ActionResult Test(Color color)
{
return Content("Color: " + color);
}
}
Suponiendo que utilizamos la ruta por defecto, una petición dirigida a la dirección URL /enum/test?color=1
retornará por pantalla el texto “Color: Rojo”. Durante el binding, el framework ha recuperado el parámetro “color” presente en la query string (un “1”) y lo ha transformado en el elemento de la enumeración correspondiente con este valor.Y no sólo eso, también podemos hacer referencia al identificador asignado en la enumeración; así, una petición como
/enum/test?color=Verde
será capaz de interpretarlo de forma correcta, asignando al parámetro color
el valor apropiado.Pero en realidad no se trata de un mérito del binder, ni tan siquiera del framework MVC. Internamente, el proceso de conversión de los tipos enumerados se delega a la clase
EnumConverter
(presente en System.ComponentModel
), que a su vez llamará a Enum.Parse()
para obtener el valor apropiado desde el string
obtenido por el value provider desde la query string. Este método examina la cadena y en función de su contenido buscará de una u otra forma el miembro de la enumeración a retornar:- Si la cadena comienza por un dígito o los signos “-“ o “+”, interpreta que el contenido del
string
es el valor asignado al elemento, por lo que intentará obtenerlo partiendo de éste. Por eso el ejemplo anterior, cuando en la petición asignábamos acolor
el valor “1” funcionaba correctamente. - En caso contrario, se asume que la cadena contiene directamente el identificador (“Rojo”, “Verde”…), y se intenta localizar en la enumeración el elemento que coincida con el suministrado. Por esta razón el segundo ejemplo, cuando en la petición asignábamos a
color
el valor “verde” también funcionaba correctamente.
/enum/test/?color=Rojo,Verde
será también interpretada de forma correcta, introduciendo en el parámetro color la combinación (un "or" binario) de ambos valores, de la misma forma que lo sería /enum/test/?color=1,2
.En resumen, esta flexibilidad a la hora de interpretar los valores y obtener el elemento correspondiente del enumerado es realmente potente y da mucho juego a la hora de crear en nuestras aplicaciones acciones muy expresivas y respetuosas con el protocolo HTTP sobre el que trabajamos. Sin embargo, esta potencia tiene también unas contraindicaciones que debemos conocer.
¿Y qué ocurre cuando los valores que se envían a una acción no son correctos?
Pues aquí llegan los problemas, claro ;-). Si a la acción anterior enviamos una petición incorrecta introduciendo en el navegador una URL como/enum/test?color=Amarillo
veremos que se produce una excepción:Observad que, a diferencia de lo que podríamos esperar, la excepción no hace referencia al hecho de que el miembro
Amarillo
no pertenece al enum
, sino a que el parámetro color
no puede contener un nulo. Es decir, el framework utiliza los componentes descritos anteriormente para intentar la conversión, pero éstos retornan un nulo y, dada la firma de la acción, éste es un valor que no puede aceptarse.Para confirmarlo, sólo tenemos que utilizar un tipo anulable en el parámetro de la acción, así:
public ActionResult Test(Color? color)
...
Simplemente con esa línea ya aseguramos que la aplicación no se parará en ejecución cuando llegue un nombre incorrecto del miembro de la enumeración, simplemente deberemos controlar lo que queremos hacer en caso de que color sea nulo.
Pero un problema aún más grave tenemos cuando el valor incorrecto viene expresado en forma numérica, indicando un valor inexistente en el tipo enumerado, por ejemplo en
/enum/test?color=24
. Fijaos que el valor está fuera de los permitidos, sería equivalente a hacer lo siguiente desde código: Color color = (Color) 24;
Seguro que ya sabéis lo que ocurre en este caso: nada. A la hora de asignar valores a una variable de tipo enum
no se realiza de forma automática ningún tipo de comprobación que permita detectar que el valor no forma parte de los permitidos, por lo que la variable color
contendrá un bonito 24
que en el contexto de nuestro sistema no significa absolutamente nada :-(Y precisamente por eso decía el problema es más grave que antes, que al menos la excepción o el valor nulo en el parámetro de entrada indicaba que el valor recibido no es correcto. Si lo que nos llega es un número fuera de rango no nos enteraremos y “se colará” silenciosamente en nuestra lógica, lo que podría liar un desaguisado tremendo.
A continuación se muestra cómo podríamos gestionar en la acción la entrada de un dato incorrecto, bien sea por tratarse de un identificador inválido (que entraría un nulo) o bien un valor numérico fuera de los permitidos en la enumeración. Observad que utilizamos el método
IsDefined()
de la clase Enum
para comprobar si el valor suministrado es válido: public ActionResult Test(Color? color)
{
if (!color.HasValue || !Enum.IsDefined(typeof(Color), color))
throw new ArgumentOutOfRangeException("color");
return Content("Color: " + color);
}
Supongo que estaréis pensando, y con razón, que sería demasiado trabajo repetirlo estas comprobaciones en todas las acciones que acepten parámetros de tipo enum
, ¿verdad?Controlando los valores de entrada mediante un model binder personalizado
Sin duda, sería mejor idea aprovechar las posibilidades de extensión del framework y crear rápidamente un binder específico que sea capaz de liberar a nuestro controlador de realizar las tareas de comprobación de los parámetros de entrada de tipoenum
, como el siguiente:public class EnumBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value = base.BindModel(controllerContext, bindingContext);
var type = bindingContext.ModelType;
if (value!=null && Enum.IsDefined(type, value))
return value;
throw new ArgumentOutOfRangeException(bindingContext.ModelName);
}
}
Como se observa en el código, se comprueba que el valor no sea nulo y que se encuentre entre los definidos en la enumeración, lanzando una excepción en caso contrario. Os dejo como deberes la implementación del binder que acepte enumeraciones de tipo flag, y parámetros de tipo anulable ;-)A continuación tenemos que indicar al framework que debe utilizar este model binder para las enumeraciones. Una posibilidad sería registrar en la colección
ModelBinders
una asociación para cada tipo de enumeración, algo así: ModelBinders.Binders.Add(typeof(Color), new EnumBinder());
Desafortunadamente, esta fórmula de registro de binders no funciona para jerarquías de clases; es decir, no podemos registrar un binder para la clase Enum
y que se aplique automáticamente para todas sus descendientes, por lo que si usamos muchas enumeraciones sería bastante tedioso registrar uno por uno los binders.Otra posibilidad sería utilizar los nuevos
ModelBinderProviders
, una característica introducida en ASP.NET MVC 3, que permiten introducir lógica en el momento de obtención de los binders y, por tanto, decidir qué binder queremos asignar a cada clase. El siguiente código podría ser un proveedor simple que conseguiría el efecto deseado:class MyModelBinderProvider: IModelBinderProvider
{
public IModelBinder GetBinder(Type modelType)
{
if (modelType.IsEnum)
return new EnumBinder();
return null;
}
}
Y su registro en el global.asax.cs sería así:
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
ModelBinderProviders.BinderProviders.Add(new MyModelBinderProvider());
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
En cualquier caso, sea cual sea la vía utilizada, una vez asociado el binder en la inicialización de la aplicación, ya controlará por nosotros la entrada de valores incorrectos para la enumeración:
public ActionResult Test(Color color)
{
// Podemos asegurar que "color" es un color válido
return Content("Color: " + color);
}
Generación de rutas hacia acciones en cuyos parámetros hay enums
Ya hemos visto cómo funciona el mecanismo de binding en acciones entre cuyos parámetros se encuentran enums, pero, ¿cómo tenemos en cuenta esta particularidad a la hora de generar enlaces o rutas hacia estas acciones?Pues la respuesta es bien simple: como si se tratara de cualquier otro tipo de datos. El siguiente código muestra la generación de enlaces hacia la acción vista anteriormente utilizando el helper
Html.ActionLink()
, primero de forma directa y a continuación utilizando T4MVC: @Html.ActionLink("Enlace a azul", "Test", "Enum", new {color = Color.Azul}, null)
@Html.ActionLink("Enlace a verde", MVC.Enum.Test(Color.Verde))
Como se puede observar, no hay que realizar conversiones ni ningún otro tipo de malabarismos, simplemente podemos utilizar elementos de la enumeración de forma directa cuando sean necesarios :-)Publicado en: Variable not found.
ActionResult
que podemos utilizar como retorno de nuestras acciones (FileResult
, ContentResult
, ViewResult
, RedirectResult
, etc…) y que cubren la mayoría de escenarios de uso frecuente al desarrollar aplicaciones para este framework.Pero sin duda, lo mejor de todo es lo fácilmente que podemos extender este conjunto para lograr resultados muy potentes, reutilizables y respetuosos con el patrón.
En este post vamos a ver cómo crear en unos minutos un nuevo tipo de resultado para nuestras acciones llamado
ZipResult
, que nos permitirá generar al vuelo archivos en formato comprimido .ZIP, en cuyo interior podremos añadir los ficheros que queramos.Así, nuestro
ZipResult
recibirá una serie nombres de archivo, los comprimirá, y los retornará al usuario empaquetados en un único fichero .ZIP. A nivel de código, su uso será así de simple:public ActionResult DescargarArchivos()
{
return new ZipResult("c:\\archivo1.dat", "c:\\archivo2.dat");
}
¡Vamos allá!1. Creación de zips desde .NET
Desde la llegada de Nuget, nada ha vuelto a ser lo mismo. En unos segundos, sólo abriendo la herramienta de gestión de paquetes, seleccionando la opción “online” y haciendo una búsqueda sobre el término “zip” tenemos acceso a la oferta de paquetes relacionados con el mismo:Como podemos ver, existen muchas opciones para tratar con archivos .zip que tenemos al alcance de un clic. En este caso vamos a usar DotNetZip, una potente biblioteca open source que nos ofrece todo lo que necesitamos en este proyecto y mucho más ;-), pero a diferencia de otras, es bastante más sencilla y cómoda de utilizar.
Y como muestra el siguiente método, que recibe una lista de rutas de archivo y los comprime sobre un fichero .zip:
public void Comprime(IEnumerable<string> files)
{
using (ZipFile zf = new ZipFile())
{
zf.AddFiles(_files, false, "");
zf.Save(@"d:\prueba.zip");
}
}
El código es bastante conciso y fácil de comprender; creamos un nuevo archivo zip, representado por la instacia del tipo ZipFile
, llamamos a su método AddFiles()
suministrándole la colección de rutas de los ficheros a comprimir, y salvamos el resultado al disco. El segundo parámetro de AddFiles()
se usa para indicar si se respetan las rutas originales de los archivos y el tercero especifica el nombre de carpeta donde se almacenarán dentro del fichero .zip.2. Creación de ActionResults personalizados
Aunque estrictamente hablando no tendría por qué ser así, la práctica totalidad de las acciones en ASP.NET MVC retornan un objeto de tipoActionResult
, como en el siguiente ejemplo:public ActionResult About()
{
return View();
}
La clase abstracta ActionResult
se define en el espacio de nombres System.Web.Mvc
de la siguiente forma:public abstract class ActionResult
{
public abstract void ExecuteResult(ControllerContext context);
}
Cuando una acción retorna un subtipo de ActionResult
, el framework se encarga de invocar a su método ExecuteResult()
para que envíe el resultado al cliente. Por ejemplo, en el caso de un ViewResult
(el retorno generado por el método View()
del controlador), su método ExecuteResult()
es el responsable de ponerse en contacto con el motor de vistas para generar el HTML, y retornar el marcado al cliente; un RedirectResult
, en cambio, sólo se encargará de retornar una redirección (temporal o permanente).Por tanto, lo único que necesitamos para crear nuestro tipo de resultado personalizado es crear una clase que herede de
ActionResult
e implementar en ella el método ExecuteResult()
. En la práctica, normalmente encontraremos en este método la lógica de generación del resultado, establecimiento de encabezados de la respuesta (content-type, content-disposition, status code, etc.), y el envío a través del canal de salida de la información deseada.3. ZipResult, el ActionResult que retorna archivos .zip
A continuación se muestra el código de la claseZipResult
, que se encarga de retornar al cliente un archivo comprimido en formato .zip en cuyo interior se encontrarán todos los archivos indicados en el momento de su instanciación.Lo que vale la pena leer está prácticamente al final de la porción de código, el método
ExecuteResult()
, que es el que realmente realiza el trabajo de comprimir y enviar al cliente el archivo resultante:public class ZipResult : ActionResult
{
private IEnumerable<string> _files;
private string _fileName;
public string FileName
{
get
{
return _fileName ?? "archivo.zip";
}
set { _fileName = value; }
}
public ZipResult(params string[] files)
{
this._files = files;
}
public ZipResult(IEnumerable<string> files)
{
this._files = files;
}
public override void ExecuteResult(ControllerContext context)
{
using (ZipFile zf = new ZipFile())
{
zf.AddFiles(_files, false, "");
context.HttpContext
.Response.ContentType = "application/zip";
context.HttpContext
.Response.AppendHeader("content-disposition", "attachment; filename=" + FileName);
zf.Save(context.HttpContext.Response.OutputStream);
}
}
}
Observad que el método ExecuteResult()
es prácticamente idéntico al Comprime()
que mostrábamos más arriba para ver lo fácil que resultaba comprimir archivos con DotNetZip. Sólo le estamos añadiendo los encabezados para la respuesta HTTP, y estamos haciendo que el archivo sea salvado directamente sobre el stream de salida en lugar de hacerlo en disco.4. Uso desde el controlador
Y para utilizar nuestro flamanteActionResult
, lo único que debemos hacer es instanciarlo desde la acción, suministrarle las rutas hacia los archivos que deseamos comprimir y retornarlo como resultado:public ActionResult Descargar()
{
return new ZipResult(
Server.MapPath("~/Archivos/fich1.txt"),
Server.MapPath("~/Archivos/fich2.txt"),
Server.MapPath("~/Archivos/fich3.txt")
);
}
Para que podáis verlo en vivo y en directo, he colgado en SkyDrive una demo algo más completita en la que es posible elegir los archivos de una carpeta, que son comprimidos y retornados por ZipResult
.Descargar proyecto de demostración.
Espero que os resulte interesante.
Publicado en: Variable not found.
Aunque ya aquí hemos hablado varias veces sobre la compilación de vistas, el enfoque de este nuevo proyecto es bastante diferente, pues permite generar clases en C# partiendo de las vistas, lo que permite, por ejemplo:
- distribuir vistas compiladas en una DLL, facilitando así el despliegue,
- evitar la distribución de los archivos .cshtml y, por tanto, la posibilidad de que sean modificados fácilmente,
- al disponer de una clase que genera la vista, podemos realizar pruebas unitarias que comprueben su contenido de forma muy sencilla (puedes ver un ejemplo aquí),
- reducir drásticamente el tiempo de arranque de la aplicación ASP.NET MVC en producción, dado que no es necesario compilar las vistas en ese momento,
- … y, por supuesto, comprobamos su corrección sintáctica en tiempo de compilación.
Una vez descargada esta extensión, si deseamos generar la clase asociada a una vista simplemente debemos acudir a las propiedades del archivo, y establecer a “RazorGenerator” su herramienta personalizada, como puede observarse en la captura de pantalla adjunta.
A partir de ese momento, cada vez que modifiquemos la vista (el archivo .cshtml), se generará de forma automática el fichero de código .cs con la clase correspondiente, de forma que al compilar el proyecto ya éstas se estarán incluyendo en el ensamblado resultante.
La siguiente parte del proyecto de David es un ViewEngine especialmente diseñado para la ocasión, que en lugar de utilizar las vistas disponibles en el sistema de archivos del servidor, intenta localizar las clases compiladas correspondientes.
Para facilitar la tarea e instalar de forma correcta este ViewEngine, simplemente hemos de utilizar Nuget para montar el paquete “PrecompiledMvcViewEngine”:
PM> Install-Package PrecompiledMvcViewEngine Attempting to resolve dependency 'WebActivator (≥ 1.4)'. Successfully installed 'WebActivator 1.4.1'. Successfully installed 'PrecompiledMvcViewEngine 1.0'. ...
Bien, pues lo curioso del tema es que este paquete podemos instalarlo directamente sobre un proyecto de biblioteca de clases e introducir en él todas las vistas de nuestra aplicación. Estableciendo la herramienta personalizada de todas ellas a “RazorGenerator”, tendremos las vistas compiladas y para utilizarlas únicamente será necesario referenciar esta biblioteca desde el proyecto MVC principal. Y obviamente, ya no tendremos que distribuir las vistas de /Views, puesto que se estarán utilizando las versiones compiladas :-)
Puedes ver un completo paso a paso sobre cómo precompilar las vistas en el blog de David Ebbo.
Aunque todavía es pronto y quizás no sea buena idea utilizar estos componentes el producción, la precompilación de vistas con este enfoque aporta un gran número de ventajas directas ya comentadas, y lo que es mejor, deja entrever interesantes utilidades como la posibilidad de conseguir plugins o áreas fácilmente reutilizables de una aplicación a otra simplemente copiando los ensamblados al proyecto. A ver si un día de estos tengo un rato y hago alguna pruebilla al respecto y os comento mis conclusiones.
Publicado en: Variable not found.
<input type="text" … />
con el atributo Maxlength
establecido, con objeto de evitar la introducción de textos más extensos de lo indicado en las restricciones StringLength
del Modelo.En este post vamos a implementar la misma funcionalidad, pero sobre áreas de texto (tag
<textarea>
), de forma que podamos también limitar el número de caracteres introducidos en este tipo de controles. Como ya sabemos en este caso es algo más complejo, puesto que tenemos que solventar mediante scripts la ausencia del atributo maxlength
que sí teníamos disponible en los cuadros de texto convencionales.Por cierto, no sé qué razones llevarían a no incluir al
textarea
el atributo maxlength
en las versiones anteriores de (X)HTML, pero parece ser que HTML5 va a enmendar esta situación permitiéndolo como atributo válido. De hecho, a día de hoy ya hay algunos navegadores que lo soportan, como las últimas versiones de Chrome y Firefox; IE9 y Opera, por ejemplo, todavía no lo han incorporado… en fin, un poco lo de siempre :-(1. Situación de partida
En la actualidad, podemos decorar una propiedad del Modelo con el atributoStringLength
para limitar el número máximo de caracteres que acepta, y utilizar el atributo DataType
para indicar que su edición por defecto debe realizarse mediante un control multilínea, es decir, un área de texto. Hasta ahí, todo es correcto y muy cómodo de implementar.Sin embargo, debido a la ausencia del atributo
maxlength
en los <textarea>
, el usuario puede introducir en tiempo de edición todo el texto que desee. Más adelante, al salir del campo o al aceptar el formulario, el mecanismo de validación mostrará el error de que se han sobrepasado el número de caracteres admitidos y no lo dejará continuar, lo cual, como podréis entender, es una auténtica aberración.Desde el punto de vista del usuario, el número de caracteres no es un dato conocido para él mientras teclea, la limitación no es indicada en ningún punto, y además no obtiene feedback alguno en el momento que ésta ha sido superada, por lo que lo único que puede hacer cuando aparece el error es comenzar a eliminar texto e ir probando periódicamente hasta que alcanza el tamaño deseado. Usabilidad cero, o menos :-(
2. ¿Qué pretendemos conseguir?
Pues básicamente añadir a nuestras aplicaciones ASP.NET MVC la posibilidad de mostrar áreas de edición (<textarea>
en HTML) con una capacidad limitada de texto, que será la especificada en las propiedades del Modelo mediante el uso del atributo StringLength
.Como una imagen vale más que todo lo que yo os pueda describir en varios párrafos, a la derecha tenéis una captura de pantalla del resultado que vamos a conseguir.
Crearemos un “control” reutilizable que nos permitirá, de forma muy sencilla y casi totalmente automática, generar editores en áreas de texto limitadas que informarán al usuario en todo momento del número de caracteres que puede introducir, y que no permitirá sobrepasar la longitud especificada en el atributo
StringLength
.3. Limitando los <textarea> mediante scripts
Está claro que las funcionalidades que vamos a añadir a las áreas de texto necesitan ser implementadas mediante scripting: es necesario contar los caracteres introducidos por el usuario, eliminar lo que exceda del tope especificado, mostrar información… Afortunadamente, podemos encontrar en la red muchos scripts que realizan estas tareas y que podemos utilizar de forma directa.El plugin Limit para jQuery es un buen ejemplo: simple, sin grandes ambiciones, pero funciona bien y es muy ligero, así que le delegaremos el trabajo sucio. El siguiente código muestra cómo podemos limitar la longitud para un
textarea
concreto, y mostrar el contador de caracteres que faltan para completarlo:<textarea id="myTextarea"></textarea> <div>Quedan <span id="charsLeft"></span> caracteres</div> <script type="text/javascript" src="/scripts/jquery.limit.js"></script> <script type="text/javascript"> $('#myTextarea').limit('140', '#charsLeft'); </script>
Si conseguimos generalizar este código e inyectar el tamaño indicado con el atributo
StringLength
en la propiedad a editar, tendremos el trabajo hecho :-)El primer paso que vamos a dar, de momento, es descargar el plugin y copiarlo a la carpeta /Scripts del proyecto, bajo el nombre jquery.limit.js.
4. El enfoque
Vamos a implementar este componente basándonos en las plantillas de edición de ASP.NET MVC, un mecanismo del framework que nos facilita la creación de potentes interfaces de introducción de datos, muy reutilizables y fáciles de construir.Cuando desde una vista utilizamos el helper
Html.EditorFor()
para generar el editor de una propiedad concreta, el framework acude a la carpeta /Views/Shared/EditorTemplates en busca de una plantilla (=vista parcial), que genere el interfaz de edición de dicha propiedad. Normalmente la plantilla que se intenta localizar depende del tipo de dato de la propiedad a editar. Así, por ejemplo, si la propiedad es
DateTime
se intenta localizar en la citada carpeta una vista parcial llamada DateTime.cshtml (o .aspx si trabajamos con el motor Webforms); si es un int
se buscará la plantilla int.cshtml, y así sucesivamente. Puedes ver un ejemplo en otro post, donde explicaba paso a paso cómo implementar un editor de fechas bastante apañado.Aquí, sin embargo, vamos a utilizar otro enfoque. Existen distintas fórmulas para indicar al framework la plantilla de edición exacta que queremos utilizar para editar una propiedad:
- en la propia llamada a
Html.EditorFor()
, que dispone de una sobrecarga en la que podemos indicar el nombre de la plantilla a utilizar para la edición del dato, - o bien, sobre la propiedad del modelo usando el atributo
[UIHint]
, suministrándole como parámetro el nombre de la plantilla.
[DisplayName("Descripción ampliada")]
[StringLength(140), Required]
[UIHint("LimitedTextArea")]
public string Descripcion { get; set; }
Y de esta forma, al generar el editor para la propiedad
Descripcion
desde cualquier vista con Html.EditorFor()
, renderizaremos el contenido de la plantilla LimitedTextArea.cshtml disponible en /Views/Shared/EditorTemplates. Más sencillo imposible.5. LimitedTextArea.cshtml
El código de esta plantilla de edición no es demasiado extenso, creo que se puede entender de un vistazo:@model string
@{
// Obtenemos el valor de la anotación MaxLength del modelo...
int maxLength = 0;
var stringLengthAdapter = ViewContext.ViewData.ModelMetadata
.GetValidators(ViewContext.Controller.ControllerContext)
.OfType<StringLengthAttributeAdapter>()
.FirstOrDefault();
if (stringLengthAdapter != null)
{
var parms = stringLengthAdapter
.GetClientValidationRules()
.First()
.ValidationParameters;
if (parms.ContainsKey("max"))
{
maxLength = (int)parms["max"];
}
}
// Obtenemos ahora el id y name del <textarea> a editar...
var id = ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldId("");
var name = ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName("");
}
@Html.TextAreaFor(model => model, new { cols=50, rows=5 })
@if (maxLength > 0)
{
<div class="textarea-chars" id="msg-textarea-@(id)">
Máximo @maxLength caracteres
</div>
<script type="text/javascript"> $(function () {
var limit = $('#@id').limit;
if (limit) {
$("#msg-textarea-@id")
.html("Quedan <span id='left@(id)'></span> caracteres");
$('#@id').limit('@maxLength', '#left@(id)');
}
});
</script>
}
En la primera parte, el bloque de código de servidor, se extrae el número máximo de caracteres permitidos desde los metadatos de la propiedad que estamos editando, obteniendo para ello la información indicada en el atributo
StringLength
. A continuación simplemente se obtienen también los id
y name
que serán asignados al <textarea>
para poder referenciarlo más adelante.Seguidamente, mediante una llamada al helper
Html.TextAreaFor()
se genera el área de texto de la forma habitual.Por último, siempre que
maxlength > 0
, es decir, que se haya indicado un límite de caracteres concreto, se inserta un <div>
para mostrar los mensajes, y se inicializa el plugin jQuery Limit, que se encargará del resto.En vista del código, seguro que entendéis las tres posibilidades que se pueden dar a la hora de renderizarse este editor:
- Si la propiedad decorada con el atributo
UIHint("LimitedTextArea")
no tiene establecido unStringLength
, aparecerá el área de edición como siempre, no se realiza ninguna operación adicional. - En caso contrario, es decir, si se ha especificado un tamaño máximo, se comprueba la disponibilidad del plugin Limit:
- si no ha podido detectarse, por ejemplo porque hemos olvidado incluir la biblioteca jQuery.limit.js en la página, simplemente aparecerá un mensaje indicando al usuario el número máximo de caracteres, aunque no se activará ninguna de las funcionalidades ni automatismos previstos,
- si el plugin ha sido cargado, se activa en el
<textarea>
que hemos creado.
¡Y esto es todo! Simplemente creando este archivo LimitedTextArea.cshtml en la carpeta /Views/Shared/EditorTemplates, decorando con el atributo
UIHint("LimitedTextArea")
las propiedades que queremos editar en el editor multilínea controlado, e incluyendo en la vista la biblioteca jQuery.Limit.js
, lo tendremos funcionando.… aunque esto último todavía podríamos mejorarlo un poco… ;-)
6. Punto extra: ¡Incluye el script por mí, por favor!
Sobre la marcha se me ocurre una mejora que creo que podría resultar interesante: ya que detectamos cuándo el plugin jQuery Limit no está disponible, podríamos insertar un código de tratamiento para este caso e intentar incluirlo de forma automática, asumiendo que se encuentra en la carpeta/Scripts
. El tema consistiría únicamente en sustituir el bloque <script> del código anterior por el siguiente:
<script type="text/javascript">
$(function () {
var limit = $('#@id').limit;
if (!limit) {
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = "@Url.Content("~/scripts/jquery.limit.js")";
$("head").append(script);
limit = $('#@id').limit;
}
if (limit) {
$("#msg-textarea-@id")
.html("Quedan <span id='left@(id)'></span> caracteres");
$('#@id').limit('@maxLength', '#left@(id)');
}
});
</script>
Fijaos que el único cambio es la inserción del primer
if
, en el que se detecta la inexistencia del plugin, y se añade un tag <script>
al DOM para cargarlo. De esta forma, no tendremos que acordarnos de hacer la inclusión de la biblioteca en cada vista donde lo utilicemos :-)Podéis descargar el código completo del editor y una demo para ASP.NET MVC 3 desde Skydrive, y veréis lo bien que queda y lo fácil que es de utilizar.
Publicado en: Variable not found.