Url.Action()
o Html.ActionLink()
, éstas son generadas usando mayúsculas y minúsculas según hubiéramos indicado en los parámetros de las llamadas:Helper | URL generada |
---|---|
@Url.Action("index", "home") | /home/index |
@Url.Action("List","Products", new{ Category="PC" }) | /Products/List?Category=PC |
@Url.Action("VIEWALL", "PRODUCTS") | /PRODUCTS/VIEWALL |
Como podemos ver, la URL resultante queda a criterio del desarrollador o, lo que es peor, al puro azar. A veces incluso no es algo que podamos elegir fácilmente, puesto que son generadas por otros componentes como T4MVC. Y no sé si desde el punto de vista del SEO tendrá su impacto, pero desde luego el ofrecer estas direcciones sin un aspecto unificado no da buena impresión.
En versiones anteriores a ASP.NET 4.5, esto podíamos solucionarlo creando helpers, o rutas personalizadas que, heredando de
Route
, realizaran esta conversión a minúsculas. Sin embargo, ahora disponemos de un mecanismo más cómodo para forzar que las URLs generadas sean normalizadas a minúsculas, así:public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.LowercaseUrls = true; routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); [...] } }
Un detalle, sin duda interesante, para tener en cuenta en nuestros desarrollos.
Publicado en: Variable not found.
Publicado por José M. Aguilar a las 9:30 a. m.
Etiquetas: asp.net, aspnetmvc, desarrollo, trucos
Asíncronía = bueno
A grandes rasgos, la explicación es la siguiente: IIS tiene disponible un número limitado de hilos (threads) destinados a procesar las peticiones. Cuando llega una petición, uno de estos hilos es asignado en exclusiva a ella y permanecerá ocupado hasta que haya sido totalmente procesada. Si llegan peticiones cuando todos los hilos están ocupados, se introducen en una cola, también limitada. Cuando el tamaño máximo de esta cola ha sido superado, ya al servidor no le queda más remedio que responder con un error HTTP 503 al usuario indicándole que está seriamente ocupado.
DefaultDisplayMode
proporcionada por el framework, con la que podíamos cubrir la mayoría de necesidades comunes.Así, veíamos cómo el siguiente código era suficiente para registrar un nuevo Display Mode llamado “iPhone”, que sería activado cuando en el identificador del agente de usuario (encabezado user-agent de la petición) incluyera el texto “iPhone”:
DisplayModeProvider.Instance.Modes.Insert(0, new DefaultDisplayMode("iPhone") { ContextCondition = (context => context.Request.UserAgent.IndexOf ("iPhone", StringComparison.OrdinalIgnoreCase) >= 0) });Hecho esto, ya podíamos definir vistas alternativas a las habituales específicas para este dispositivo, cuyos nombres de archivo acabarían siempre en “.iphone.cshtml”. Observad que estamos usando la clase
DefaultDisplayMode
, a la que estamos facilitando la condición que debe cumplirse para que se active este Display mode.Sin embargo, si pensamos crear muchas vistas específicas para dispositivos, podríamos encontrarnos con un maremagnum de archivos como el que veis en la captura de pantalla adjunta.
Obviamente, no es una situación fácilmente manejable, así que ¿por qué no cambiar la forma de nombrar los archivos dependiendo del Display Mode actual? Pues dicho y hecho, vamos a conseguir que cada dispositivo disponga de una carpeta específica para guardar sus vistas.
Heredando de DefaultDisplayMode
Si analizamos el código fuente de la clase DefaultDisplayMode
, veremos que hay varios miembros virtuales que podemos sobrescribir para tomar el control, y uno de ellos es el método TransformPath()
, encargado de transformar la ruta hacia el archivo físico donde está definida la vista teniendo en cuenta el nombre del Display Mode actual.El código por defecto del método es el siguiente:
protected virtual string TransformPath(string virtualPath, string suffix)
{
if (string.IsNullOrEmpty(suffix))
return virtualPath;
string extension = Path.GetExtension(virtualPath);
return Path.ChangeExtension(virtualPath, suffix + extension);
}
O sea, que se reemplaza la extensión del archivo, normalmente “.cshtml” por el resultado de concatenar el sufijo suministrado (el nombre del Display Mode) a dicha extensión. Por esta razón, el comportamiento por defecto del framework es utilizar construcciones como “nombrevista.iphone.cshtml”.Si, como es el caso, queremos cambiar la ruta donde van a intentar localizarse las vistas, lo único que tenemos que hacer es crear una clase descendiente de
DefaultDisplayMode
, sobrescribir la forma de “montar” la ruta hacia la vista, y utilizar esta nueva clase para registrar los modos de visualización que nos interesen. Una posible implementación podría ser la siguiente: public class OrganizedDisplayMode: DefaultDisplayMode
{
public OrganizedDisplayMode(string displayModeId): base(displayModeId)
{
}
protected override string TransformPath(string virtualPath, string suffix)
{
if (string.IsNullOrEmpty(suffix))
return virtualPath;
// Transforms /index.cshtml --> /suffix/index.cshtml
int lastSeparator = virtualPath.LastIndexOf('/');
virtualPath = virtualPath.Substring(0, lastSeparator) +
"/" + suffix +
virtualPath.Substring(lastSeparator);
return virtualPath;
}
}
Y en la inicialización de la aplicación ya podríamos registrar este Display Mode y asociarlo a la condición que esperamos que cumpla la petición: DisplayModeProvider.Instance.Modes.Insert(0,
new OrganizedDisplayMode("iPhone")
{
ContextCondition =
context => context.Request.UserAgent.Contains("iPhone")
});
De esta forma, ya podemos organizar las vistas como podéis observar en la captura de pantalla de la derecha: cada dispositivo (o Display Mode registrado) dispondría de una carpeta en cuyo interior se encontrarían las vistas específicas para el mismo.Y observad que su funcionamiento no se limita a las vistas asociadas a controladores concretos, la solución también sería válida en vistas compartidas (Shared) y con aquellas incluidas en áreas :-)
Publicado en Variable not found.
Publicado por José M. Aguilar a las 9:18 a. m.
Etiquetas: asp.net, aspnetmvc, desarrollo, novedades, trucos
Lo más habitual es utilizarlas para crear editores o visualizadores para tipos de datos concretos, en cuyo caso las vistas disponibles en las carpetas citadas anteriormente se llaman igual que el tipo de dato a editar o visualizar.
Por ejemplo, si tenemos una vista llamada int.cshtml en la carpeta /Views/Shared/EditorTemplates , será la utilizada como editor de propiedades de tipo entero. Algo parecido utilizamos por aquí, hace ya bastante tiempo, para integrar jQuery datepicker en nuestras aplicaciones.
También es habitual encontrar en estas carpetas plantillas vinculadas a propiedades u objetos de forma directa mediante el atributo
[UIHint]
(como en este post), mediante el cual se indica explícitamente qué plantilla debe utilizarse: [DisplayName("Long description")]
[StringLength(140), Required]
[UIHint("LimitedTextArea")] // Use the template "LimitedTextArea.cshtml"
public string Description { get; set; }
Y otra alternativa, aunque quizás algo menos utilizada, es la posibilidad de indicar la plantilla a utilizar en la misma llamada al helper
EditorFor()
o DisplayFor()
que provoca su renderizado: @Html.EditorFor(m=>m.Description, "LimitedTextArea")
Sin embargo, hay una posibilidad adicional, que curiosamente no es demasiado conocida, consistente en utilizar el atributo DataTypeAttribute
para indicar la plantilla de edición o visualización a emplear para la propiedad a la que se aplica.El funcionamiento es trivial: decoramos las propiedades con
[DataType]
suministrándole como parámetro el tipo de información que va a contener, y el sistema intentará localizar una plantilla con ese nombre. Veamos un ejemplo: [DataType(DataType.EmailAddress)]
public string Email { get; set; }
[DataType(DataType.Time)]
public DateTime StartDate { get; set; }
[DataType(DataType.Html)]
public string LongDescription { get; set; }
En el momento de renderizar un editor (o un display) template para la primera propiedad, el sistema utilizará, si existe, el archivo EmailAddress.cshtml; en caso de no existir, intentará localizar un string.cshtml, y si tampoco lo encuentra generará un editor por defecto. En el segundo caso se realizará la misma operación con Time.cshtml, y el último de ellos usará la plantilla Html.cshtml como primera opción.Podemos utilizar como nombre de plantilla todos los valores que encontramos en la enumeración
DataType
:DateTime
Date
Time
Duration
PhoneNumber
Currency
Text
Html
MultilineText
EmailAddress
Password
Url
ImageUrl
Y como viene siendo costumbre, vamos a dar un repaso a todo lo que encontramos en esta nueva entrega, que presumiblemente será la última (bueno, o penúltima, nunca se sabe) antes de la versión definitiva. Eso sí, para no hacer el post demasiado largo nos centraremos exclusivamente en los cambios más destacables y visibles que se han introducido respecto a la versión beta.
Esto tiene mucha utilidad directa, por ejemplo, a la hora de construir aplicaciones web que adapten su interfaz en función de si el cliente es un dispositivo móvil o desktop, pero como casi siembre ocurre en el framework, se trata de un mecanismo muy extensible y fácil de adaptar a otras necesidades.
- www.acme.org, que sería el sitio principal.
- crm.acme.org, con el sistema CRM de la empresa.
- erp.acme.org, con un sistema de gestión empresarial.
- administration.acme.org con las herramientas de administración del sistema.
- etc.
No es una tarea complicada en ASP.NET, aunque hay que hacer algunos ajustillos para que todo funcione correctamente. Veámoslos.
Y las veinte más votadas, auténticas joyas algunas de ellas, fueron las siguientes...
Publicado por José M. Aguilar a las 9:45 a. m.
Etiquetas: desarrollo, javascript, knockout, patrones
En este post ascenderemos a un nivel de abstracción mucho mayor que el proporcionado por las conexiones persistentes y veremos cómo utilizar los Hubs, otro mecanismo proporcionado por SignalR que nos permitirá lograr una integración increíble entre el código cliente y de servidor, y hará aún más sencilla la implementación de este tipo de servicios.
Por tanto, esta tercera entrega de la serie vamos a dedicarla (otra vez ;-)) a las conexiones persistentes, veremos qué cosas han cambiado con la llegada de la revisión 0.4, y desarrollaremos un nuevo ejemplo que ilustre las novedades que podemos encontrar a la hora de trabajar a bajo nivel con SignalR.
Url.Action()
y Html.ActionLink()
son capaces de generar URLs hacia acciones partiendo de la información disponible en la tabla de rutas y de los parámetros que les suministramos. Por ejemplo, el siguiente código genera un enlace hacia la acción
Edit
del controlador Friends
, suministrándole el valor 3 al parámetro de ruta id
:@Html.ActionLink("Edit friend", "Edit", "Friends", new {id = 3}, null)
Sin embargo, estos helpers no nos ayudan demasiado cuando desconocemos a priori el valor de los parámetros. De hecho, si en lugar del 3 que hemos suministrado quisiéramos enviar un valor obtenido a través de scripting tendríamos un problema, puesto que el bloque de código anterior se ejecuta en servidor y el enlace sería generado antes incluso de ejecutarse el script para obtener el valor a enviar.Esta es una pregunta que suelo ver muy a menudo en comunidades y foros (como el de ASP.NET MVC en MSDN) y me han realizado más de una vez en el curso de ASP.NET MVC 3 que tutorizo en CampusMVP, así que vamos a ver un par de enfoques para enfrentarnos a este escenario, bastante frecuente al trabajar en sistemas web y, sobre todo, en aquellos muy basados en scripting y Ajax.
Publicado por José M. Aguilar a las 10:06 a. m.
Etiquetas: asp.net, aspnetmvc, consultas, desarrollo, trucos
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.
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
La solución propuesta consistía en introducir en la vista código de script para desplazar el foco hasta el control indicado mediante una llamada al helper
Html.SetFocusTo()
, que implementábamos en el mismo post, aunque hay otras formas para conseguirlo.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
.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.
file
(el que usamos para hacer los uploads) de un formulario, por ejemplo, para evitar que el usuario envíe un archivo que por cualquier motivo no deba ser subido al servidor.O dicho de otra forma, imaginemos la siguiente porción de un formulario en pantalla, que podría ser generada con el código que podéis ver justo a continuación:
<label for="archivo">Archivo a enviar:</label> <input type="file" id="archivo" name="archivo" /> <input type="button" onclick="limpiarInputFile('archivo');" value="Limpiar" />
Y la pregunta en este momento sería, ¿qué código deberíamos implementar en la función
limpiarInputFile()
que estamos utilizando en el evento onclick
si quisiéramos limpiar o inicializar el contenido del campo archivo
?Aunque de forma intuitiva podría parecer que basta con establecer el valor del campo a una cadena vacía, una prueba rápida nos demostrará que esto no es posible. Desde hace ya tiempo, por motivos de seguridad, los navegadores no permiten el acceso de escritura a la propiedad value en los campos de envío de archivos, por lo que nos encontramos una vía sin salida. Esto lo vemos con el siguiente código, utilizando jQuery:
function limpiarInputfile(id) {
var input = $('#' + id);
var nuevoValor = "c:\\windows\\system32\\mspaint.exe";
alert(input.val()); // Muestra "C:\Datos.dat"
input.val(nuevoValor); // Establecemos un nuevo valor
alert(input.val()); // ¡¡Muestra "C:\Datos.dat"!!
}
Como vemos, somos vilmente ignorados cuando intentamos establecerle un valor.
Pues bien, una posible solución consiste en eliminar del DOM el elemento
<input type="file">
y volver a crearlo justo después en el mismo lugar. He visto por ahí varias implementaciones que obligaban a introducir este elemento dentro de un contenedor, pero he creado otra que creo que es más sencilla e igual de efectiva: function limpiarInputfile(id) {
var input = $('#' + id);
var clon = input.clone(); // Creamos un clon del elemento original
input.replaceWith(clon); // Y sustituimos el original por el clon
}
Y eso es todo :-). Observad que lo único que hacemos es crean un clon del elemento original cuyo
value
por supuesto estará en blanco (recordad que esta propiedad no se puede establecer), y justo a continuación eliminamos el elemento original sustituyéndolo por este clon.Si queremos generalizar este código e implementar esta funcionalidad de forma no intrusiva podríamos hacer lo siguiente:
$(function () {
$("input[type=file]").after(
"<input type='button' class='limpiar-inputfile' value='Limpiar'>"
);
$(".limpiar-inputfile").click(function () {
var input = $(this).prev("input[type=file]");
var clon = input.clone();
input.replaceWith(clon);
return false;
});
});
Este código añade automáticamente un botón “Limpiar” a continuación de todos los
<input type=file>
de la página, implementando en el manejador del evento click
la lógica de inicialización del componente que hemos visto antes. De esta forma, sólo se introducirá en la página el botón de limpiado cuando estén activados los scripts, que es en el único momento en que su ejecución tendrá sentido con la solución propuesta.Espero que os sea de utilidad.
Publicado en: Variable not found.
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.