Hace unas semanas, en el post Cambiar la ubicación de las vistas en ASP.NET MVC estuvimos viendo cómo era posible aprovechar la flexibilidad del framework para saltarse las convenciones respecto a la ubicación de los archivos de vistas (.aspx, .ascx y .master).
En esta ocasión vamos a seguir profundizando en los puntos de extensión del framework, centrándonos en la facilidad con la que podemos modificar el ControllerFactory
, que es el componente encargado de localizar y crear las instancias de los controladores que deben procesar las peticiones.
Y vamos a ilustrarlo con un ejemplo, en el que desmontaremos otra de las convenciones del framework MVC: el nombre de los controladores. Como sabemos, cuando se recibe una petición que según su ruta debe ser procesada por un controlador llamado XYZ, el motor por defecto intentará instanciar una clase denominada XYZController
. Lo que vamos a hacer es retocar este comportamiento para que nuestros controladores puedan denominarse “ControladorDeXYZ
”, un nombre mucho más acertado en nuestro idioma.
En este post veremos cómo podemos modificar la forma en que este componente localiza las clases de controladores y adaptarla a nuestras preferencias.
Pero antes, recuerda:
Saltarse las convenciones = malo
Hazlo sólo cuando realmente esté justificado... o por diversión, como es el caso ;-)
Lo que vamos a hacer en el post no es una implementación eficiente, y por supuesto no es válida para un entorno de producción; se trata simplemente de un experimento que demuestra la facilidad para sustituir ciertos componentes del framework, y la flexibilidad que nos aporta. Los conceptos que vamos a tratar pueden ser utilizados con otros fines, como facilitar la inyección de dependencias en controladores, que probablemente veamos en un futuro post.
Y ahora, al tema…
1. El proceso básico de la petición
Antes de empezar la faena, es conveniente estudiar un poco el funcionamiento del framework; así podremos entender mejor qué estamos tocando.
Cuando se recibe una petición, su tratamiento pasa a través de una serie de componentes que se encargan de procesarla a distintos niveles siguiendo el ciclo de vida definido para el framework. Así, una vez que el sistema de enrutamiento ha terminado su tarea detectando la información presente en la URL y determinando, por tanto, su destino, utilizará un manejador MvcHandler
para continuar procesando la petición desde el método ProcessRequest()
.
La porción de código de ASP.NET MVC framework donde se define este método es la siguiente (en el archivo MvcHandler.cs
, recordad que el código fuente está disponible en Codeplex):
1: protected internal virtual void ProcessRequest(HttpContextBase httpContext)
2: {
3: [...]
4:
5: // Get the controller type
6: string controllerName =
7: RequestContext.RouteData.GetRequiredString("controller");
8:
9: // Instantiate the controller and call Execute
10: IControllerFactory factory =
11: ControllerBuilder.GetControllerFactory();
12:
13: IController controller =
14: factory.CreateController(RequestContext, controllerName);
15:
16: if (controller == null)
17: {
18: throw new InvalidOperationException(...); // Simplificado
19: }
20: try
21: {
22: controller.Execute(RequestContext);
23: }
24: finally
25: {
26: factory.ReleaseController(controller);
27: }
28: }
Resumidamente, podríamos decir que el sistema procesa las peticiones siguiendo estos pasos:
- en primer lugar, obtiene el nombre del controlador desde el contexto de la petición (normalmente desde la ruta). En el código anterior, se guarda en la variable
controllerName
. - a continuación obtiene una instancia de la factoría de controladores desde la clase
ControllerBuilder
. una vez obtenida la factoría según el paso anterior, se le pide mediante una llamada a su método
CreateController()
que cree el controlador que procesará la petición, enviándole como parámetro el nombre obtenido anteriormente.- y por último, se invoca al método
Execute
del controlador para que realice las acciones oportunas, liberando al final los recursos utilizados mediante una llamada aReleaseController
.
Como en otras muchas partes del framework, se puede observar en el código anterior la utilización de interfaces en lugar de clases para los elementos que intervienen en el proceso, lo que da pistas sobre la flexibilidad de ASP.NET MVC: casi cualquier elemento puede ser sustituido por otro siempre que se cumpla el contrato definido.
2. La factoría de controladores, a corazón abierto
Visto el procedimiento anteriormente descrito, está claro que la lógica de localización e instanciación del controlador es responsabilidad de las factorías de controladores, y más exactamente del método
que éstas deben implementar.CreateController()
ASP.NET MVC utiliza como factoría la clase DefaultControllerFactory
de forma predeterminada, sin embargo, nada en el código anterior obliga a que esto sea así. Si logramos crear una nueva factoría que implemente el interfaz IControllerFactory
e indicamos al framework que la utilice, ya casi tendremos el trabajo hecho.
En el código de la clase DefaultControllerFactory
(la factoría predeterminada), se puede observar que el proceso de creación está compuesto por dos pasos; en el primero de ellos se obtiene el tipo de controlador mediante una llamada a GetControllerType()
, mientras que en el segundo se obtiene una instancia de dicho tipo llamando a GetControllerInstance()
, que es la que retorna el método:
1: public virtual IController
2: CreateController(RequestContext requestContext, string controllerName)
3: {
4: [...] // Simplificado
5: Type controllerType = GetControllerType(controllerName);
6: IController controller = GetControllerInstance(controllerType);
7: return controller;
8: }
Dado que lo único que queremos sustituir es el mecanismo de localización, lo más sencillo es crear nuestra nueva factoría, a la que llamaremos AcmeControllerFactory
, heredando de DefaultControllerFactory
y sobrescribir el método GetControllerType
, que afortunadamente es virtual.
El código completo para esta nueva clase es el siguiente:
1: public class AcmeControllerFactory: DefaultControllerFactory
2: {
3: protected override Type GetControllerType(string controllerName)
4: {
5: Type r = base.GetControllerType(controllerName);
6: if (r == null)
7: {
8: Assembly current = Assembly.GetExecutingAssembly();
9: r = (from t in current.GetTypes()
10: where
11: (
12: t.Name.Equals("ControladorDe" + controllerName,
13: StringComparison.InvariantCultureIgnoreCase)
14: && t.IsPublic
15: && !t.IsAbstract
16: && typeof(IController).IsAssignableFrom(t)
17: )
18: select t).FirstOrDefault();
19: }
20: return r;
21: }
22: }
Como podemos observar, lo primero que hacemos es intentar localizar el controlador con el nombre que nos llega en el parámetro, pero utilizando el método propuesto por la factoría por defecto, es decir, llamando al método GetControllerType()
original. Es decir, vamos a ser al menos algo respetuosos con las convenciones de nombrado existentes ;-) … primero intentaremos localizar el controlador ciñéndonos a ellas, y sólo si no tenemos éxito aplicaremos nuestro propio criterio de nombrado.
Así, ante una petición rutada hacia el controlador Cliente,
primero buscaremos la clase ClienteController
(según la convención), y si no existe es cuando intentaremos localizar un tipo llamado ControladorDeCliente
. Obviamente, podríamos hacerlo al revés, es decir, buscar primero según nuestro criterio de nombrado y si no tenemos éxito buscar utilizando el estándar, o bien ni siquiera intentar esta última opción.
Por tanto, si el método por defecto no es capaz de encontrar un controlador correspondiente a dicha denominación es cuando ejecutamos nuestra lógica adicional, que consiste en buscar en el ensamblado actual, mediante una consulta LINQ, aquellas clases cuyo nombre corresponda con el patrón que pretendemos emplear (“ControladorDe”+controllerName
), y que sean públicas, instanciables, e implementen el interfaz IController
.
No se han implementado optimizaciones, como la inclusión de un caché, que permitan mejorar el rendimiento en esta consulta. Cada petición deberá recorrer, en el peor de los casos, todo el ensamblado en busca de un controlador apropiado.
Tampoco se ha implementado ningún tipo de resolución de conflictos; si existe más de un controlador con el mismo nombre en distintos namespaces, se retornará el primero que se encuentre.
Eso os lo dejo de deberes ;-)
3. Sustituir la factoría de controladores
Ya tenemos creada la clase AcmeControllerFactory,
nuestra propia factoría de controladores, y está lista para entrar en acción. Pero, ¿cómo le decimos al framework que queremos utilizar una factoría de controladores distinta a la predeterminada?
Volvamos a estudiar el código de procesado de las peticiones que vimos anteriormente. Como se puede observar, el IControllerFactory
a utilizar es obtenido desde la clase ControllerBuilder
:
1: protected internal virtual void ProcessRequest(HttpContextBase httpContext)
2: {
3: [...]
4: // Instantiate the controller and call Execute
5: IControllerFactory factory =
6: ControllerBuilder.GetControllerFactory();
7:
8: IController controller =
9: factory.CreateController(RequestContext, controllerName);
10: [...]
11: }
Y aquí de nuevo entra en juego la flexibilidad del diseño de ASP.NET MVC: ControllerBuilder
viene preparada para que podamos sustituir muy fácilmente la factoría a instanciar, por lo que podemos indicarle el tipo concreto que queremos que utilice.
En el siguiente código estamos indicando al ControllerBuilder
que nuestra factoría por defecto, es decir, la clase encargada de buscar e instanciar los controladores, será AcmeControllerFactory.
Como se define en la inicialización de la aplicación (Application_Start
del global.asax
), todas las peticiones utilizarán esta factoría para crear los controladores:
1: protected void Application_Start()
2: {
3: RegisterRoutes(RouteTable.Routes);
4: ControllerBuilder
5: .Current
6: .SetControllerFactory(typeof(AcmeControllerFactory));
7: }
4. Probar que realmente funciona
Podemos comprobar el funcionamiento de todo este invento de forma muy sencilla. Una vez seguidos los pasos anteriores, basta con crear un nuevo controlador que ya atienda a nuestra propia convención de nombrado, como el siguiente:
1: [HandleError]
2: public class ControladorDeCliente : Controller
3: {
4: public ActionResult Listar()
5: {
6: return View();
7: }
8:
9: public ActionResult Editar()
10: {
11: return View();
12: }
13: }
Creamos, asimismo, una vista “Listar.aspx” y otra “Editar.aspx” en la carpeta “Views\Cliente” para que el motor pueda localizarlas y podamos ver algún resultado en pantalla. A partir de este momento, ejecutando el proyecto, podremos comprobar cómo si introducimos la URL http://localhost:tupuerto/Cliente/Listar
el flujo de ejecución pasará por el método Listar()
visto anteriormente. También podemos invocar a la acción Editar()
, y el resultado será el esperado.
Y para los perezosos, ahí va el código fuente del proyecto (requiere Visual Studio 2008 o Web Developer Express con SP1 y ASP.NET MVC 1.0):
Publicado en: Variable not found.
Publicado por José M. Aguilar a las 9:33 p. m.
Etiquetas: .net, asp.net, aspnetmvc, desarrollo
Lo sé. Estoy desactualizado... pero me sigue gustando Office 2003, como supongo que ocurre a mucha gente todavía. Y de hecho, continúo creando mis documentos con Microsoft Word 2003.
Sin embargo, a pesar del largo camino que hemos recorrido juntos, aún no he sido capaz de verle sentido al modo diseño de lectura, esa extraña forma de presentar los documentos por defecto al abrirlos, o que podemos activar voluntariamente desde el menú “edición”. ¿A quién se le ocurrió esa idea? ¿Alguien la ha visto útil alguna vez? ¿Tiene, quizás, alguna utilidad oculta que no he sido capaz de intuir en todos estos años?
Bueno, en cualquier caso, podemos conseguir muy fácilmente que no utilice el diseño de lectura en el momento de abrir los documentos, accediendo al cuadro de diálogo de configuración (Herramientas >> Opciones) y desmarcando el check “Permitir el inicio en diseño Lectura”:
Sencillo, ¿eh? Desde luego, no tengo excusa para haber aguantado este odioso comportamiento durante más de un lustro…
Publicado en: Variable not found.
He recibido algunos mensajes de lectores de Variable not found que quieren iniciarse en ASP.NET MVC, pero no saben por dónde empezar, y me sugieren que escriba algunos posts que expliquen desde cero este nuevo framework. Posiblemente lo haga algún día, pero mientras tanto, les he recordado que existen recursos gratuitos que pueden ser de mucha utilidad para dar los primeros pasos (e incluso profundizar un poco) en esta tecnología.
Por ejemplo, recientemente han aparecido una gran cantidad de libros sobre ASP.NET MVC, y de la mayoría de ellos se pueden descargar capítulos gratuitos que, además de ayudarnos a decidir cuál de ellos puede ser de nuestro interés, tienen valor por la información que nos pueden aportar. Como siempre, el problema es la dispersión de esta información, por lo que he decidido hacer esta recopilación para tenerlos todos a mano. Conforme vaya descubriendo nuevos ejemplares iré ampliando la lista; y por supuesto, si conocéis alguno que no esté aquí, me lo comentáis y lo añadimos.
Eso sí, todos en inglés, aunque al ser muy técnicos creo que son fáciles de comprender.
Pro ASP.NET MVC Framework Autor: Steve Sanderson Post de presentación: Now Published Pro ASP.NET MVC Framework (Apress) « Steve Sanderson’s blog Capítulo gratuito: Chapter 2: Your First ASP.NET MVC Application (23 páginas)En este capítulo, el autor muestra muy detalladamente, prácticamente paso a paso, cómo crear nuestra primera aplicación simple con ASP.NET MVC. En primer lugar, crea un proyecto muy básico mediante el cual explica los fundamentos básicos de la programación siguiendo este modelo, y a continuación entra en la creación de una mini-aplicación con entrada de datos, validaciones y lógica simple. | |
Professional ASP.NET MVC 1.0 Autores: Scott Hanselman, Rob Conery, Phil Haack y Scott Guthrie Capítulo gratuito: Chapter 1: Nerdinner (192 páginas) Este extenso capítulo describe la creación de un sitio web medianamente complejo, www.nerddinner.com, utilizando gran cantidad de tecnologías relacionadas con el framework MVC como filtros, Ajax, LinqToSql, pruebas unitarias, etc. Muy recomendable, imprescindible. | |
ASP.NET MVC 1.0 Quickly Autor: Maarten Balliauw Post de presentación: Announcing my book- ASP.NET MVC 1.0 Quickly Capítulo gratuito: Chapter 2: Your first ASP.NET MVC application (20 páginas) | ASP.NET MVC in Action Autores: Jeffrey Palermo, Ben Scheirman, Jimmy Bogard Post de presentación: Announcing ASP.NET MVC in Action (from Manning) - Jeffrey Palermo (.com) - CodeBetter.Com Recurso gratuito: Getting Started with the ASP.NET MVC Framework (Green Paper – PDF) (18 páginas) (El enlace a este documento te lo envían por correo electrónico tras facilitar tu dirección). Se trata de otra introducción al framework, el sistema de rutas, controladores y vistas partiendo desde cero. Capítulo gratuito: Chapter 9: AJAX in ASP.NET MVC (21 páginas) |
ASP.NET MVC Framework Unleashed Autor: Stephen Walther Capítulos gratuitos: el autor ha publicado varios capítulos en su blog, y los retirará en cuanto el libro esté disponible en Amazon, así que daos prisa que los contenidos son excelentes. | |
Beginning ASP.NET MVC 1.0 Autores: Simone Chiaretta, Keyvan Nayyeri Capítulo gratuito: Chapter 9: Testing ASP.NET MVC Applications (38 páginas) |
Publicado por José M. Aguilar a las 12:00 a. m.
Etiquetas: asp.net, aspnetmvc, desarrollo, libros, servicios on-line
El control de errores en aplicaciones web es fundamental si queremos ofrecer un interfaz robusto y amigable para los usuarios en cualquier situación. No hay nada más frustrante para un usuario que una pantalla de error con contenidos indescifrables y que no le aportan alternativas de salida.
El framework ASP.NET MVC nos ofrece mecanismos de control de errores muy potentes basada en la utilización del atributo HandleError,
el cual definirá la vista que será mostrada al usuario cuando se produzca alguna excepción no controlada en el código de los controladores, siempre que en el web.config
se haya activado el uso de errores personalizados mediante la propiedad CustomErrors
.
En este post vamos a profundizar en el uso del atributo HandleError,
comentando cómo se implementa en el controlador, su ámbito de actuación, los parámetros que ofrece y la forma de crear las vistas para mostrar los errores de forma amigable.
El controlador
HandleError
puede ser declarado tanto a nivel de clase (controlador) como a nivel de acción. En el primer caso, se establecerá el comportamiento general para todas las acciones del controlador, mientras que en el segundo será aplicable sólo a la acción a la que se asocie el atributo:
// Control de errores a nivel de clase de controlador,
// que será aplicado a todas las acciones del mismo.
[HandleError()]
public class HomeController : Controller
{
...
}
// Control de errores a nivel de acción concreta...
[HandleError(ExceptionType=typeof(ArgumentException), View="ArgumentError")]
public ActionResult Calculate(int a, int b)
{
ViewData["results"] = calculateSomething();
return View();
}
En realidad, el comportamiento definido en el atributo HandleError
a nivel de clase también se aplicará a los errores generados por las vistas u otros resultados (ActionResult
) retornados por los controladores. Es decir, sobre el segundo de los ejemplos anteriores, la vista “ArgumentError” (que existirá en un archivo llamado ArgumentError.aspx) será invocada cuando la excepción ArgumentException
sea lanzada bien por el propio controlador, o bien por la vista “Calculate” que retorna por defecto.
Un último detalle sobre esto: el atributo HandleError
puede ser especificado tantas veces como necesitemos sobre la misma acción o controlador, indicando comportamientos para distintos tipos de excepción. El atributo que se tendrá en cuenta cuando se produzca un error será el primero que se encuentre cuyo tipo de excepción (parámetro ExceptionType) sea compatible con la excepción lanzada.
El manejador de errores que se empleará en una acción será el primero que corresponda al tipo de excepción producida, teniendo en cuenta tanto los atributos que adornan la acción como los que acompañan a su controlador, y siempre según un orden preestablecido.
Veamos con más detenimiento los parámetros que admite la declaración del atributo.
Parámetros de HandleError
[HandleError(
ExceptionType=typeof(DivisionByZero),
View="ErrorPersonalizado",
Master="MaestraErrores",
Order = 0)
]
public ActionResult Index()
...
- ExceptionType permite indicar el tipo de excepción que se pretende controlar. Por defecto, el sistema entenderá que la regla se refiere al tipo base
Exception
, por lo que se aplicará a todos los errores que se generen, pues todas las excepciones heredan de esta clase. - View, el nombre de la vista que será mostrada al usuario. Por defecto se tomará el valor “Error”, por eso en la plantilla de proyectos ASP.NET MVC ya existe una vista con este nombre.
- Master, la página maestra con la que será renderizada la vista, independientemente de lo que tenga declarado ésta.
- Order, un valor numérico (por defecto –1, el más prioritario) que indica la prioridad de aplicación de esta regla cuando el sistema encuentre varios atributos
HandleError
aplicables al mismo elemento y que puedan presentar conflictos. Los valores más pequeños, Por ejemplo, si no se indicara este parámetro en el siguiente caso, el resultado dependería del orden de declaración, lo cual no es demasiado recomendable:
[HandleError(Order=10, View="ErrorGenerico")]
public class HomeController : Controller
{
...
[HandleError (ExceptionType=typeof(DivisionByZero),
View="OperacionIncorrecta", Order=1)]
public ActionResult Calculate()
...
Como puede intuirse, esto hará que en caso de producirse una división por cero, se muestre la vista “OperacionIncorrecta” y no la “ErrorGenerico”.
Cada vez que utilicemos HandleError
es conveniente tener muy en cuenta la prioridad (definida en la propiedad Order
), el alcance (las excepciones a tratar, definidas en la propiedad ExceptionType
), así como los valores por defecto en cada caso. Esto evitará comportamientos misteriosos del sistema una vez se produzcan errores en tiempo de ejecución.
Acceso desde la vista a la información del error
Ya hemos comentado anteriormente que la vista que será mostrada a los usuarios cuando se produzca un error será la indicada en el parámetro View
del atributo HandleError
, o la vista "Error", si este parámetro no es informado. Sea cual sea, el archivo nombredevista.aspx deberá estar localizable por el motor de vistas en el momento de su lanzamiento (por cierto, si no te gustan las ubicaciones por defecto, puedes ver cómo modificar la forma en la que se buscan las vistas en este post).
La vista de un error es una página .aspx normal, como una vista más de la web, pero con la particularidad de que puede recibir información sobre el error que ha provocado su presentación. De hecho, se trata de una vista tipada que hereda de ViewPage<System.Web.Mvc.HandleErrorInfo>
, de forma que la propiedad Model
será del tipo HandleErrorInfo
, clase que nos ofrece completa información sobre el origen del problema:
<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage<System.Web.Mvc.HandleErrorInfo>" %>
<asp:Content ID="errorTitle" ContentPlaceHolderID="TitleContent" runat="server">
Error en el sistema
</asp:Content>
<asp:Content ID="errorContent" ContentPlaceHolderID="MainContent" runat="server">
<h2>
Ups, se ha producido un ligero inconveniente...
</h2>
<p>
La acción <%= Model.ActionName %>
del controlador <%= Model.ControllerName %>
ha lanzado la excepción <%= Model.Exception.GetType().Name %> con
el mensaje "<%= Model.Exception.Message %>".
</p>
</asp:Content>
A pesar de no ser un buen ejemplo como pantalla del error amigable para el usuario ;-), el código anterior ilustra cómo es posible acceder desde la vista a los datos del contexto del error proporcionados por el entorno, y cómo la clase HandleErrorInfo
nos ofrece una información sobre la acción en la que se ha lanzado la excepción, el controlador en el que se encuentra la misma, y lo más interesante, nos ofrece en la propiedad Exception
la excepción producida, por lo que tendremos acceso a su tipo, descripción e incluso al estado de la pila de llamadas en el momento de producirse el error.
Publicado en: www.variablenotfound.com
Publicado por José M. Aguilar a las 11:59 p. m.
Etiquetas: asp.net, aspnetmvc, c#, desarrollo, programación, web
<code>
y </code>
el texto que tengamos seleccionado en el momento de su utilización, pero evita tener que entrar al modo de edición HTML para hacer estos ajustes. Basándome en el código fuente de CodeTag (que podéis descargar al final de este post), voy a describir, paso a paso, cómo podemos crear complementos para Writer que nos faciliten un poco las tareas cotidianas de edición.
0. Antes de empezar…
Para poder desarrollar el plugin necesitáis básicamente dos cosas:- en primer lugar, el propio software Live Writer, más que nada porque necesitamos referenciar algunos de sus ensamblados, y por ir probando el complemento durante el desarrollo.
- en segundo lugar, Visual Studio 2005 o 2008, aunque sea una edición express. Este desarrollo vamos a realizarlo en C#, pero la traducción a Visual Basic .NET sería trivial.
1. Preparación del proyecto
Los plugins para Live Writer son archivos .dll, es decir, ensamblados .NET, que se colocan en el directorio "plugins" de la carpeta de instalación del programa. En mi caso, están en la carpetaC:\Archivos de programa\Windows Live\Writer\Plugins,
y si no habéis cambiado las rutas de instalación por defecto, será allí donde los podéis encontrar también. Durante el proceso de arranque, Writer examinará dicha carpeta y cargará los complementos que encuentre en ella. Lo que vamos a hacer es crear desde Visual Studio un proyecto de librería de clases. Cada vez que compilemos, copiaremos el ensamblado resultante en dicho directorio (veremos cómo podemos automatizar esta tarea) y podremos lanzar Live Writer para comprobar el funcionamiento.
Por tanto, en primer lugar creamos un proyecto de librería de clases como siempre, al que le damos el nombre CodeTag. Obviamente, podéis dar al proyecto el nombre que estiméis conveniente.
Una vez que el IDE crea la estructura, debemos añadir dos referencias al proyecto, que serán necesarias para poder continuar:
- La primera referencia es al ensamblado que contiene el API de Live Writer, que podéis encontrar en el directorio de instalación de la herramienta. El archivo a incluir es
WindowsLive.Writer.Api.dll
. - La segunda es a
System.Windows.Forms
. Hay que tener en cuenta que Live Writer es una herramienta de escritorio, y esta referencia es importante para poder interactuar con el mismo.
Otro detalle más que nos va a facilitar las cosas: vamos a indicar a Visual Studio que el resultado de la compilación lo copie en el directorio de plugins de Live Writer. Para ello, lo único que tenemos que hacer es acudir a las propiedades del proyecto, pestaña “eventos de generación” e incluir la siguiente orden en el cuadro “línea de comandos del evento posterior a la generación” (obviamente, modificando la ruta si es necesario):
XCOPY /D /Y /R "$(TargetPath)" "C:\Archivos de Programa\Windows Live\Writer\Plugins\"
Ojo, es importante tener en cuenta algo más cuando vayamos a compilar: si Live Writer está abierto, no podremos sobrescribir nuestro plugin con una nueva versión, pues éste se encontrará en uso. En estos casos debemos cerrar el programa antes de copiar el archivo, o antes de compilar.
Con estas operaciones ya tenemos el proyecto listo para empezar a codificar el plugin.
2. Escribimos la clase principal
Un plugin sencillo como este no requiere demasiada programación, pero sí hay que cumplir una serie de requisitos para que se integre correctamente en Live Writer.En primer lugar, creamos una clase, a la que llamaremos
CodeTag
, e insertamos el siguiente código: public class CodeTag: ContentSource
{
public override DialogResult CreateContent(IWin32Window dialogOwner,
ref string content)
{
if (string.IsNullOrEmpty(content))
content = "YourCodeHere";
content = "<code>" + content + "</code> ";
return DialogResult.OK;
}
}
Como se puede observar, la clase hereda de
ContentSource,
un tipo definido en WindowsLive.Writer.Api
, que sirve como base para la creación de complementos sencillos como el que nos ocupa. Para casos más complejos, que permitieran por ejemplo edición de contenidos bidireccional entre el contenido de la página y un editor personalizado podríamos heredar de SmartContentSource
, pero no profundizaremos en ello ahora. La codificación de la lógica del plugin puede realizarse en varios puntos, dependiendo de la forma en que Writer lo active; en nuestro caso, el complemento se lanzará cuando el usuario presione un botón en la barra lateral o seleccione la opción correspondiente en el menú "insertar", por lo que simplemente deberemos sobrescribir el método
CreateContent
. Dicho método recibe dos parámetros. El primero de ellos hace referencia a la ventana activa, que podemos utilizar como "padre" si quisiéramos crear un cuadro de diálogo desde nuestro código. El segundo parámetro contendrá una referencia al texto seleccionado en el momento de lanzar el plugin, pudiendo darse los dos casos que se contemplan en la codificación:
- Si la variable
content
viene vacía o con valor nulo, es que el complemento ha sido lanzado en modo inserción, es decir, no existía ningún texto seleccionado y por lo tanto lo que se pretende es añadir las etiquetas de código para introducir posteriormente contenido. Como puede observarse en el código, lo que se hace en este caso es insertar las etiquetas<code>
con un texto arbitrario para que el usuario lo modifique a su antojo más adelante. - En caso contrario, si la variable
content
trae algún contenido, lo que se hace es rodear éste por la apertura y cierre de la etiqueta<code>
.
En ambos casos se retorna un valor
DialogResult.OK
, que indica a Live Writer que debe insertar el texto contenido en content en la ubicación donde se encuentre el cursor, o bien sustituir el texto seleccionado por el nuevo valor. 3. Añadimos metadatos
Heredar de la claseContentSource
no es el único requisito para que esta sea considerada como un componente de Writer, es necesario adornarla con un conjunto de atributos como los mostrados en el siguiente código: [WriterPlugin("98497c2b-bbfd-4bd1-b343-226f3c9e766b", "Code Tag",
Description = "Crea etiquetas <code></code> en el contenido",
PublisherUrl = "http://www.variablenotfound.com")]
[InsertableContentSource("Etiqueta <code>")]
public class CodeTag: ContentSource
{
[...]
}
El atributo [
WriterPlugin]
es el que realmente identifica la clase como plugin de Live Writer. Los parámetros que se le están enviando son los siguientes: - El primer parámetro es un GUID, es decir, un identificador único que debemos generar para el plugin, utilizando este servicio on-line u otras herramientas como la incluida en Visual Studio.
- El segundo parámetro es el nombre del plugin. Es la identificación que aparece en el cuadro de diálogo de configuración de complementos de Live Writer.
Description
permite añadir una descripción textual ampliada del plugin.PublisherUrl
es una dirección web de referencia al autor. Pura propaganda ;-)
El atributo
[InsertableContentsource]
indica que el complemento debe aparecer en el menú "insertar" de las barra de tareas de Writer y en el menú principal. El parámetro que le estamos enviando es simplemente el texto que aparecerá en estos menús. 4. Compilamos el proyecto
Con lo hecho hasta el momento ya podemos compilar e intentar probar nuestro complemento. Este procedimiento lo repetiremos varias veces durante el desarrollo, por lo que es posible que nos encontremos a menudo con un error como el siguiente:Esto simplemente quiere decir que Live Writer está usando CodeTag.dll y no puede ser sobrescrito, por lo que ¡cerrad Live Writer antes de compilar!
Una vez superado este leve impedimento, si aparece, ya podremos comenzar a disfrutar de nuestro plugin. Veremos que aparece en el menú "insertar", con la descripción apropiada, y funcionando correctamente.
Pero vaya, es cierto que en el menú aparece, pero destaca sobre el resto de complementos porque es el único que no tiene un icono, así que habrá que mejorarlo un poco…
5. Añadirle un icono al plugin
Incluir un icono a nuestro complemento le dará sin duda un aspecto más profesional, vamos a ello.Lo primero que necesitamos es una imagen de 16x16 píxeles, por ejemplo en formato .bmp, que actuará como icono. Como el diseño gráfico decididamente no es lo mío, he creado una imagen muy simple pero creo que bastante ilustrativa de la tarea que realiza el complemento: la propia etiqueta
<code>
que pretendemos crear. El archivo debemos incluirlo, para no tener problemas, en el directorio raíz del proyecto, y a continuación, hay que indicar a Visual Studio que dicha imagen será un recurso incrustado (embedded resource) en el ensamblado. Este paso es importante, pues si no se hace correctamente, la imagen no será incluida en la DLL.
A continuación es necesario indicar a Live Writer la imagen a utilizar como icono, lo que se consigue añadiendo un parámetro más (
ImagePath
) en la definición del atributo [WriterPlugin]
con la ruta hacia el fichero que hemos incrustado. Eso sí, no me refiero a la ruta física del archivo .bmp, sino a la representación como espacio de nombres de la misma (por ejemplo, si la imagen se llama logo.bmp
y está en (raíz)\recursos
, la ruta hacia ella será “recursos.logo.bmp”
). Como en este caso hemos depositado la imagen en el directorio raíz, la declaración de atributos quedará como sigue:
[WriterPlugin("98497c2b-bbfd-4bd1-b343-226f3c9e766b", "Code Tag",
Description = "Crea etiquetas <code></code> en el contenido",
ImagePath = "CodeTag.bmp",
PublisherUrl = "http://www.variablenotfound.com")]
[InsertableContentSource("Etiqueta <code>")]
public class CodeTag : ContentSource
{
[...]
}
Un último apunte relativo a este tema: si al iniciar Live Writer éste es incapaz de localizar el recurso indicado en el parámetro
ImagePath
, el plugin funcionará, pero aparecerá el siguiente mensaje en el arranque de la aplicación: 6. O, por si no quieres teclear…
… he colgado en SkyDrive el código fuente del proyecto, para el que quiera utilizarlo como base de creación de sus propios complementos, o simplemente para trastearlo un poco.Requiere, como mínimo, Visual C# 2008 Express, con su correspondiente Service Pack 1.
Publicado en: Variable not found.
Publicado por José M. Aguilar a las 11:52 p. m.
Etiquetas: blogging, c#, desarrollo, software, utilidades
El framework ASP.NET MVC utiliza en varios puntos la filosofía que suele denominarse convención sobre configuración, ahorrando tiempo y esfuerzos al desarrollador que decida asumir una serie de normas preestablecidas, a la vez que facilita la homogeneidad y coherencia en las soluciones que las implementen.
Por ejemplo, todos sabemos que las vistas de una aplicación ASP.NET MVC deben encontrarse en la carpeta Views
. Si se trata de una vista compartida como puede ser una página maestra, debemos colocarlas en la subcarpeta Shared
; en caso contrario, deberá existir una subcarpeta con el nombre del controlador en la que se encontrarán las vistas para las distintas acciones, nombradas igual que éstas. Esto, a la postre, evita que sea el desarrollador el que tenga que decidir dónde ubicar dichos archivos y configurar el sistema para que sea capaz de localizarlos cuando sean necesarios: simplemente deberá seguir la convención preestablecida.
Pero, ¿qué ocurre cuando estas convenciones no encajan con nuestras necesidades o preferencias?
En este post voy a describir cómo es posible modificar la ubicación de las vistas en una aplicación ASP.NET MVC 1.0, saltándonos las convenciones establecidas por el framework. Pero sobre todo, ojo:
Saltarse las convenciones = malo
Hazlo sólo cuando realmente esté justificado... o por diversión, como es el caso ;-)
1. Cómo localiza ASP.NET MVC las vistas
Cada vez que el framework necesita localizar una vista, por ejemplo después de ejecutar una acción cuyo resultado indica que debe mostrarse al usuario una página, recorre los directorios donde, por convención, se supone que debe estar. Esta información se encuentra definida a nivel de código en el constructor de la clase WebFormViewEngine
, es decir, en el motor de vistas Webforms que se utiliza por defecto en ASP.NET MVC. Su definición es la siguiente (recordad que está disponible el código fuente del framework):
public class WebFormViewEngine : VirtualPathProviderViewEngine
{
[...]
public WebFormViewEngine()
{
base.MasterLocationFormats = new string[]
{
"~/Views/{1}/{0}.master",
"~/Views/Shared/{0}.master"
};
base.ViewLocationFormats = new string[]
{
"~/Views/{1}/{0}.aspx",
"~/Views/{1}/{0}.ascx",
"~/Views/Shared/{0}.aspx",
"~/Views/Shared/{0}.ascx"
};
base.PartialViewLocationFormats = base.ViewLocationFormats;
}
[...]
}
Como se puede observar, existen tres propiedades en el motor del vistas, heredadas de su antecesor, que indican los directorios donde se encuentran los archivos con la definición del interfaz:
- MasterLocationFormats es un array con las rutas en las que se buscarán sucesivamente las páginas maestras. En el código puede verse que por defecto el sistema intentará localizar las masters en
~/Views
y~/Views/Shared
; los parámetros {0} y {1} corresponden al nombre de la master y del controlador que están siendo buscados, respectivamente.Es importante tener en cuenta que el valor de esta propiedad sólo se aplica cuando se establece desde el código la página maestra con la que debe ser mostrada una vista.
- ViewLocationFormats contiene las rutas donde se intentarán localizar las vistas. En la implementación por defecto, puede verse cómo el framework intenta localizar primero en
~/Views/{1}
(recordad que este parámetro se sustituirá por el nombre del controlador) y luego en~/Views/Shared
una página .aspx o .ascx con el nombre de la vista (parámetro {0}). - Por último, PartialViewLocationFormats indica las carpetas donde se deberán buscar las vistas parciales. Por defecto, se buscarán en los mismos directorios que las vistas normales.
2. Lo que pretendemos
Una vez visto en qué se basa el framework para localizar las vistas, vamos al ataque. Nuestra intención es saltarnos las convenciones creando una nueva estructura de carpetas para las mismas, como la que puede observarse en la imagen lateral. Existirá un directorio raíz, llamado "Interfaz", en el que podemos encontrar dos subdirectorios: "Vistas" y "Masters". En ellos almacenaremos, respectivamente, las vistas y las páginas maestras de nuestra aplicación.
La estructura de carpetas del proyecto será, por tanto, diferente a la propuesta por las plantillas de Visual Studio. Además, no tiene demasiado sentido, desde el punto de vista práctico, pero nos valdrá como ejemplo.
En los siguientes apartados vamos a ver dos formas distintas de conseguirlo.
3. Cómo lograrlo
La forma más sencilla de conseguir reemplazar las rutas de búsqueda de las vistas es, sin duda, alterar el contenido de las propiedades del motor descritas en el primer punto (MasterLocationFormats
, ViewLocationFormats
y PartialViewLocationFormats
).
Esto puede conseguirse introduciendo el siguiente código en la inicialización de la aplicación, en el archivo global.asax:
public class MvcApplication : System.Web.HttpApplication
{
[...]
protected void Application_Start()
{
ReplacePaths(); // Reemplaza las rutas en el View Engine actual
RegisterRoutes(RouteTable.Routes);
}
private void ReplacePaths()
{
WebFormViewEngine eng = ViewEngines.Engines[0] as WebFormViewEngine;
eng.MasterLocationFormats =
new string[] {
"~/Interfaz/Masters/{0}.master"
};
eng.ViewLocationFormats =
new string[] {
"~/Interfaz/Vistas/{0}.aspx",
"~/Interfaz/Vistas/{0}.ascx"
};
eng.PartialViewLocationFormats = eng.ViewLocationFormats;
}
[...]
}
Como se observa en el código anterior, en el método ReplacePaths()
en primer lugar se obtiene una referencia al motor de vistas por defecto, que sabemos que es del tipo WebFormViewEngine
. Después, modificamos las propiedades para introducir las rutas que nos interesan. Tan simple como eso.
Es obvio que de esta forma estamos realizando una actualización destructiva de las propiedades. Si quisiéramos mantener las localizaciones por defecto del WebFormViewEngine
pero añadir nuevas ubicaciones de búsqueda deberíamos copiar el contenido original del array, añadirle las rutas adicionales, y establecerlo en las propiedades.
Veamos otra forma de conseguir lo mismo.
4. Cómo lograrlo, toma 2
ASP.NET MVC framework ha sido diseñado desde sus orígenes con la extensibilidad en mente. Prácticamente cualquiera de sus comportamientos puede ser personalizado sustituyendo componentes como si se trataran de piezas de un mecano.
Una de las posibilidades de extensión incluidas en la plataforma es la sustitución del motor de vistas que utiliza para componer los interfaces de usuario. Esto, llevado al extremo, permite la utilización de motores distintos al proporcionado por defecto (WebForms) y utilizar alternativas como NHaml, Spark, Brail, o NVelocity, entre otros.
Pero nosotros no iremos tan lejos. Para cumplir con nuestros objetivos simplemente necesitamos crear y registrar en el sistema un nuevo motor de vistas que herede de WebFormViewEngine
y reemplace la inicialización de las propiedades que definen la localización de las vistas.
El nuevo motor, al que llamaremos AcmeViewEngine, será algo como lo siguiente:
public class AcmeViewEngine : WebFormViewEngine
{
public AcmeViewEngine()
{
MasterLocationFormats =
new string[] {
"~/Interfaz/Masters/{0}.master"
};
ViewLocationFormats =
new string[] {
"~/Interfaz/Vistas/{0}.aspx",
"~/Interfaz/Vistas/{0}.ascx",
};
PartialViewLocationFormats = base.ViewLocationFormats;
}
}
Y ahora sólo nos faltaría registrar el motor de vistas en ASP.NET MVC framework, tarea que podemos realizar en el global.asax, así:
protected void Application_Start()
{
ReplaceViewEngine(); // Registra el nuevo View Engine
RegisterRoutes(RouteTable.Routes);
}
private void ReplaceViewEngine()
{
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new AcmeViewEngine());
}
Observad que antes de añadir el nuevo motor de vistas a la colección estamos vaciándola (llamando a su método Clear()
), lo cual asegurará que nuestro AcmeViewEngine
será el utilizado por la plataforma.
5. Conclusión
Como ya he comentado al principio, saltarnos las convenciones no es buena idea. Por ejemplo, un daño colateral de romper con la convención propuesta por el framework MVC para la localización de las vistas es que Visual Studio no será capaz de proporcionarnos la función que nos permite abrir la vista asociada con una acción seleccionando la opción “Go to View” del menú contextual, o intentará insistentemente crear las nuevas vistas (con la opción “Add View”) en las carpetas en las que deberían encontrarse según la convención.
Pero en cualquier caso, es un ejercicio interesante para conocer las interioridades de este framework y comprobar de primera mano la flexibilidad con la que ha sido implementado.
Si lo deseas, puedes descargar el código fuente de los ejemplos (requiere Visual Studio 2008 o Web Developer Express con SP1 y ASP.NET MVC 1.0):
Publicado en: www.variablenotfound.com
Publicado por José M. Aguilar a las 11:58 p. m.
Etiquetas: asp.net, aspnetmvc, desarrollo, programación, trucos