[ASP.NET MVC] Discriminar acciones según el nombre de los parámetros

El caso que voy a exponer hoy está “basado en hechos reales”, como las películas de Antena 3. Me encontré con la necesidad de ofrecer dos rutas como:

http://mi.com/Form/Show?url=http://geeks.ms

http://mi.com/Form/Show?name=Geeks

Es decir, para quien usa la página, nuestra acción Show puede recibir una URL o bien el nombre de una página que ya tengamos previamente guardada. Son las dos formas de acceder al servicio. Por poner algo más de contexto, puede imaginar el lector un servicio como Google Mobilizer (transformar una página para facilitar su lectura en dispositivos móviles) mezclado con un gestor de marcadores (bookmarks). El uso de la misma ruta en ambos casos es una decisión de diseño que no depende de nosotros.

Este diseño admite dos posibles planteamientos en MVC:

  • Una única acción con dos parámetros opcionales, url y name.
    • Según el parámetro dado, elegir un comportamiento u otro.
    • Decidir qué hacer si nos pasan los dos parámetros (por ejemplo, dar un error).
  • Dos acciones separadas, una con un parámetro url y otra con name.
    • Pro: podemos organizar mejor nuestro código (en cada acción lo suyo).
    • Pro: la decisión la hace MVC, no necesitamos discriminar según los parámetros recibidos. Queda más natural.
    • Contra: no es soportado de forma nativa en MVC 3, como veremos.

Como el código de ambas acciones no tiene apenas nada en común, me gustaba más la segunda opción, por lo que vamos a ver si podemos ir salvando los obstáculos que nos encontremos en el desarrollo del ejemplo. Para comenzar:

  1. Creamos un nuevo proyecto ASP.NET MVC 3 de nombre SelectingActionByArgumentsName.
  2. Usamos la plantilla Vacía (Empty) y motor de vistas Razor (View engine).
  3. Añadimos un nuevo controlador: clic derecho en la carpeta Controllers, y elegimos Agregar (Add) > Controller.
  4. Le llamamos FormController, y lo creamos vacío (Empty controller).

Ya tenemos el proyecto listo para incluir nuestras dos acciones. Bajo la acción Index que se genera siempre, añadimos las dos nuestras, devolviendo sólo un contenido (para simplificar):

public ActionResult Show(string url)
{
    return Content("Loaded from url");
}

public ActionResult Show(string name)
{
    return Content("Loaded from name");
}

Y encontramos el primer obstáculo:

C# no admite dos métodos con la misma firma en una clase.

Evidentemente, aunque el nombre de los parámetros sea distinto, son del mismo tipo, por lo que C# (no ASP.NET MVC, sino el compilador) nos dará un error. Contra esto, poco podemos hacer, hay que cambiar algo. Pero existe una solución sencilla: añadir un parámetro opcional. Esto hará que los métodos sean diferentes. Por ejemplo, vamos a añadir un parámetro opcional save a la acción que recibe una url:

http://mi.com/Form/Show?url=http://geeks.ms&saveAs=Geeks

Este parámetro indica que queremos que se guarde la dirección con ese nombre. De hecho, hemos aprovechado para añadir funcionalidad, pero en la práctica podríamos haber añadido un parámetro sin utilidad y no usarlo nunca (su única utilidad sería permitir que el compilador acepte nuestros dos métodos de forma válida). Y lo haremos opcional, por lo que la URL inicial seguirá funcionando. Así que cambiamos la primera acción por:

public ActionResult Show(string url, string saveAs = "")

Y ya tenemos el primer obstáculo salvado: el compilador no se queja. Ya podemos ejecutar. Pero si navegamos a (en xxx irá el puerto de vuestro servidor web):

http://localhost:xxx/Form/Show?name=Geeks

Recibimos un error ahora de ASP.NET MVC (segundo obstáculo):

The current request for action ‘Show’ on controller type ‘FormController’ is ambiguous between the following action methods:
System.Web.Mvc.ActionResult Show(System.String, System.String) on type SelectingActionByArgumentsName.Controllers.FormController
System.Web.Mvc.ActionResult Show(System.String) on type SelectingActionByArgumentsName.Controllers.FormController

ASP.NET MVC no sabe elegir qué acción utilizar. ¿Pero no hemos puesto name en nuestra URL? Pues usa la acción que tiene un argumento name, so tonto (no sé si vosotros también soléis insultar a la pantalla). Pero no: ASP.NET MVC no usa los nombres de los argumentos para elegir qué acción llamar durante el proceso de enrutado (por defecto, que como casi todo en MVC 3, esto puede cambiarse). Ojo, sí usa los nombres después para asignar los parámetros, pero eso es cuando ya ha elegido la acción; es para elegirla cuando no los considera. Sólo sus tipos. Y usando sólo los tipos, se encuentra con esa ambigüedad.

Pero como hemos dicho, casi todo en MVC 3 puede cambiarse, y además de diferentes formas. En este caso, vamos a usar una solución vista en StackOverflow, aunque modificada para hacerla más genérica, dirigida a cambiar el mecanismo de selección de acciones (sí, ese que hemos dicho que se basa en los tipos) para que considere también los nombres de los argumentos. Y además vamos a hacerlo mediante un atributo para que sólo afecte a las acciones donde lo necesitemos. Para esto, sólo debemos crear una clase que extienda de ActionMethodSelectorAttribute y sobreescribir el método IsValidForRequest, que es quien realizará la comprobación.

public class SelectByArgumentNamesAttribute : ActionMethodSelectorAttribute {
    public override bool IsValidForRequest(ControllerContext controllerContext, 
                                           MethodInfo methodInfo) {
        return methodInfo.GetParameters()
                .All(pi => pi.IsOptional 
                        || controllerContext.HttpContext.Request[pi.Name] != null);
    }
}

Un detalle aclaratorio: no tenemos que comprobar el nombre del método, ya que esto lo hace ASP.NET MVC antes de llamar a esta validación. Sólo tenemos que comprobar si, según los datos de la petición (Request), este método (methodInfo) es el apropiado o no. Por lo que comprobamos que todos los argumentos del método vienen en la Request (con su nombre), salvo que sean opcionales.

Ahora sólo tenemos que usar este atributo en las acciones conflictivas (en ambas). Por ejemplo:

[SelectByArgumentNames]
public ActionResult Show(string url, string saveAs = "") {
…

Tras usar el atributo en las dos acciones, ya podemos probar a navegar a las URL:

http://localhost:xxx/Form/Show?url=http://geeks.ms

http://localhost:xxx/Form/Show?name=Geeks

¿Pero qué sucede? Que todavía nos queda por salvar el tercer obstáculo, aunque no está relacionado con lo anterior. Si comprobáis, veréis que funciona correctamente con la primera dirección (con o sin el parámetro opcional) pero falla con la segunda, la que usa el parámetro name.

¿Por qué? Bueno, pues este es uno de esos problemas que nos invitan a creer en la magia y abandonar la programación para dedicarnos a echar las cartas, hasta que descubrimos de qué se trata: la culpa es del nombre elegido para el parámetro url, dado que siempre que preguntemos por Request[“url”] obtendremos un valor, nunca es null, aunque no hayamos pasado ningún parámetro con ese nombre en la URL, porque el valor obtenido es ¡el de la propia URL de la petición!

Por suerte, este obstáculo es el de más fácil solución: en lugar de buscar en Request, profundizamos un poco más hasta Request.QueryString, donde sólo están los parámetros que vienen en la QueryString, con lo que sólo habrá un url si se lo pasamos, y si no devolverá null. La solución pasa por cambiar:

controllerContext.HttpContext.Request[pi.Name] != null

por

controllerContext.HttpContext.Request.QueryString[pi.Name] != null

Y resuelto. He querido incluir este último obstáculo por 1. ser fiel a la realidad (recordad que el artículo está basado en hechos reales) y 2. por la moraleja, parafraseando: “el más mínimo desconocimiento de la herramienta puede hacerse pasar por magia”.

En conclusión, hemos visto cómo se puede cambiar de forma muy sencilla la forma en que ASP.NET MVC selecciona la acción a partir de la URL. Recordad que, igualmente, muchos otros comportamientos generales se pueden modificar y adaptar a los casos concretos, o incluso esto podíamos haberlo conseguido de otras formas. Y sobre todo, lo más importante: hay que conocer bien la herramienta con la que trabajamos para adaptarla, entenderla… y distinguirla de la magia.

Un placer.

Anuncios
| 3 comentarios

Recibiendo un parámetro de tipo Array en un Controller de ASP.NET MVC

Implementando una acción en un controlador (Controller) de ASP.NET MVC he necesitado que uno de los parámetros fuera una lista o array de enteros, en concreto los Id a considerar para un pequeño informe. Por ponerlo negro sobre blanco, o mejor, pixel sobre pantalla:

public class InformeController : Controller
{
    //
    // GET: /Informe/Ventas?desde=6/1/2011&hasta=6/30/2011&centros=1,2,3

    public ActionResult Ventas(DateTime desde, DateTime hasta, int[] centros)
    {
        return Content("Centros: " + string.Join(" / ", centros));
    }
}

Para mi informe necesito un rango de fechas definido por desde y hasta, y una lista de centros definidos por sus Id. El paso de parámetros en la URL, mediante HTTP GET, tiene para mí muchas ventajas, entre ellas la facilidad para realizar pruebas.

A modo de test devuelvo una cadena con los centros separados por barras, para que se vea que no hay truco. En el comentario del propio código puede verse cómo sería una llamada, el interés está en el parámetro centros que recibe una lista separada por comas de los Id (dejando de lado que las fechas deben ir en inglés, pero esa es otra cuestión). La elección de la coma como separador es mía, aunque sigue una convención bastante extendida.

La cuestión es que esta acción tan sencilla e intuitiva no funciona puesto que en el argumento centros no se recibe el valor deseado en todos los casos.

  • En el ejemplo del comentario, con centros=1,2,3 en la QueryString, recibiremos null en centros. Mal.
  • En cambio, si pasamos centros=1 recibiremos un array con un entero de valor 1. Bien.

¿A qué se debe esto? Bien, tenemos que saber que estamos confiando en el ModelBinder de ASP.NET MVC para convertir los valores de la QueryString (nuestra URL) en los parámetros de nuestras acciones. Este ModelBinder tiene una funcionalidad muy completa con los tipos sencillos, pero poco más. Si necesitamos algo extra, como es nuestro caso, no nos ayuda.

Pero ASP.NET MVC tiene múltiples puntos de extensión, entre ellos la posibilidad de definir nuestros propios ModelBinder para cubrir los tipos que necesitemos. Así que vamos a definir uno para permitir la recepción de un array de enteros en un formato separado por comas. Sólo tenemos que extender de IModelBinder:

public class ArrayOfIntModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        return controllerContext.HttpContext.Request[bindingContext.ModelName]
            .Split(',')
            .Select(int.Parse)
            .ToArray();
    }
}

Esta interfaz sólo nos exige implementar el método BindModel que tiene la responsabilidad de transformar el trozo de la QueryString donde va nuestro parámetro (y que es un string, no lo olvidemos) en el valor destino, en nuestro caso un array de enteros. Recibe dos argumentos:

  • El contexto del controlador, que incluye la información de la petición (Request). De aquí obtenemos el valor del parámetro en la QueryString.
  • El contexto del modelo, en nuestro caso el argumento destino de la conversión. De aquí obtenemos el nombre del argumento buscado, que en nuestro ejemplo anterior sería “centros” pero que si usamos bindingContext.ModelName tenemos un Binder genérico que nos sirve para cualquier otro uso de array de enteros en otra acción (o incluso como otro parámetro en la misma).

El resto del método es sencillo: dividimos la lista de enteros usando la coma como separador, los convertimos a enteros con int.Parse y convertimos el IEnumerable resultante en un array. Pido perdón por mi concisión en el código, parte de la culpa la tiene Linq, otra buena parte Resharper, y otra yo mismo, qué pasa, si tenemos un lenguaje potente y hermoso como C# es para aprovecharlo, ¿no?

Bueno, nos queda la parte más importante, porque como ya imagináis esta clase por sí misma no hace nada si no le decimos a ASP.NET MVC que la utilice al procesar las peticiones. Para esto, basta con incluir en el Global.asax.cs una línea como:

ModelBinders.Binders[typeof(int[])] = new ArrayOfIntModelBinder();

Podemos ponerla en Application_Start(), y su mandato es sencillo de definir con palabras: “cuando encuentres un parámetro de tipo array de entero, lo procesas con esta instancia y no con el ModelBinder genérico.” Dicho y hecho, si todavía tenéis la URL anterior, volved a ejecutar el proyecto y refrescar esa página, podéis comprobar como ahora el parámetro centros devuelve todos los enteros que hayamos tenido a bien suministrarle en la URL.

Un placer.

| 2 comentarios

Nuevos videos en SilverDiagram

Desde hace unas semanas formo parte del equipo de SilverDiagram, una librería para la edición de diagramas en Silverlight. Estamos dándola a conocer, implementando nueva funcionalidad y elaborando documentación, ejemplos y videos. Para agrupar toda este nuevo material en un único sitio hemos creado una nueva sección en la web, Learn SilverDiagram.

En esta página podemos encontrar:

Seguid atentos a los nuevos videos y ejemplos. Esperamos que todo este material sea de ayuda a quienes queráis conocer más sobre SilverDiagram.

Publicado en SilverDiagram | Etiquetado , , , , , | Deja un comentario

Integrando MVC 3 en una aplicación de Dynamic Data

Basándome en los buenos consejos de Eduard Tomás en su post ¿WebForms y ASP.NET MVC juntos? vamos a  intentar aplicar esas mismas ideas para migrar una aplicación ASP.NET Dynamic Data a MVC 3. Una aplicación Dynamic Data es realmente una aplicación Web Forms con algunos enrutamientos particulares ya definidos.

  1. Convertir el proyecto a .net 4.0 (necesario)
    Basta con cambiarlo en propiedades del proyecto, pero no os olvidéis de confirmar con vuestro hosting que soporta el framework 4 (yo no tengo problemas porque los chicos de Domitienda-Businet están siempre a la última).
    NOTA: Cuidado porque al cambiar el framework se pierde la sección <configSections> del web.config, hay que volver a introducirla, como se indica aquí. Puedes recuperarla comparando diferencias con la versión anterior del control de código fuente que uses (y si no lo usas, sigue cavando tu tumba).
  2. Añadir assemblies en Web.config / compilation. Algunas de las librerías que indica Eduard ya estarán, puesto que Dynamic Data ya las incluye. En mi caso sólo he tenido que añadir:
    <add assembly="System.Web.Helpers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
    <add assembly="System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
    <add assembly="System.Web.WebPages, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
  3. Incluir espacios de nombres de MVC para las páginas, en Web.config / system.web / configuration / pages

    <pages>
      <namespaces>
        <add namespace="System.Web.Helpers" />
        <add namespace="System.Web.Mvc" />
        <add namespace="System.Web.Mvc.Ajax" />
        <add namespace="System.Web.Mvc.Html" />
        <add namespace="System.Web.Routing" />
        <add namespace="System.Web.WebPages"/>
      </namespaces>
    </pages>


  4. Agregar referencias a System.Web.Mvc.dll (normalmente desde C:\Program Files\Microsoft ASP.NET\ASP.NET MVC 3\Assemblies). Si vas a usar Razor (¡cómo no!) también System.Web.WebPages.dll (desde C:\Program Files\Microsoft ASP.NET\ASP.NET Web Pages\v1.0\Assemblies).
  5. Inicializar MVC en el Global.asax.cs
    Uso el mismo código de Eduard, pero ojo porque Dynamic Data ya genera un método RegisterRoutes, así que renombramos el nuestro por RegisterRoutesMvc (también su uso en MvcInit).

    private void MvcInit()
    {
        AreaRegistration.RegisterAllAreas();
        RegisterGlobalFilters(GlobalFilters.Filters);
        RegisterRoutesMvc(RouteTable.Routes);
    }
    
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new HandleErrorAttribute());
    }
    
    public static void RegisterRoutesMvc(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
        routes.MapRoute("Default", "{controller}/{action}/{id}",
            new {action = "index", id = UrlParameter.Optional});
    }
  6. Cambiar el tipo de proyecto (para que Visual Studio nos ofrezca las herramientas de MVC, como Add controller, Go to view y demás comandos).
    Hay que añadir {E53F8FEA-EAE0-44A6-8774-FFD645390401} en <ProjectTypeGuids> del .csproj (ya sabes, Unload project y después Edit .csproj). Ojo que ese Guid debe ser el primero, los dos que ya estaban deben seguir pero detrás de este (así viene en un nuevo proyecto de MVC, puedes consultarlo si tienes dudas).
  7. Añadir manualmente la estructura de carpetas de MVC: Controllers, Views y Shared con las plantillas _ViewStart.cshtml, Layout.cshtml y el Web.config de Razor (sí, fíjate que en los proyectos de MVC tienes un Web.config dentro de Views; pues cópialo en tu carpeta Views y listo). Y bueno, ya estás en MVC, el resto ya es cosa tuya, ¿no? Si quieres echa un vistazo a cómo puedes empezar.

Dynamic Data es una herramienta potente para elaborar prototipos, poner en marcha rápidamente proyectos, y para ofrecer un mantenimiento sencillo de tablas (CRUD) a usuarios finales. Pero cuando la aplicación comienza a crecer, toda la ayuda que ha prestado en la fase inicial se convierte en obstáculos, por lo que es una buena idea ir abandonándolo, al menos para las páginas que tengan cierta lógica de negocio. Y es esa la oportunidad para ir migrando a MVC, ya que se va a llevar muy bien con Dynamic Data, se permite incluso tener rutas parcialmente comunes entre ambos: por ejemplo, yo tengo Acciones/List.aspx de Dynamic Data y Acciones/Notificar atendida por un Controller de MVC.

Por cierto, comentar que hace un tiempo oí que el equipo de Dynamic Data estaba planteando su migración a MVC (todavía su base es el ASP.net clásico, ya que Dynamic Data es anterior a MVC). En ese caso la integración con MVC resultará mucho más natural. Pero con las novedades de MVC que han llegado y que se esperan, quizá dicha evolución no llegue nunca a ver la luz, ya que MVC ofrece una gran facilidad para el desarrollo rápido de aplicaciones y prototipos.

| Deja un comentario

Cómo iniciarse con ASP.net MVC

Un buen amigo me pide enlaces e información para iniciarse con ASP.net MVC, y cuando el correo que le estaba escribiendo ha alcanzado un cierto tamaño, me he dado cuenta de que era mejor ponerlo en una entrada del blog.

Lo que sigue no es un tutorial sino una recopilación de enlaces (pocos), consejos y opiniones para comenzar con ASP.net MVC. Y está escrito de forma informal porque con mi amigo tengo mucha confianza 😉

  1. Ya está la versión 3.0, olvídate de las anteriores (y ten cuidado al seguir artículos o tutoriales, ha habido una evolución importante desde las anteriores: ojo a la fecha de los artículos).
  2. Además de instalar VS2010 y su SP1, debes instalar las MVC 3 Tools Update.
    Pero para no tener que hacer todo esto, en http://www.asp.net/mvc hay un botón Install Visual Studio Express que te lo hace todo. Y con la versión Express tienes suficiente para empezar… y para terminar la aplicación.
  3. Apúntate a AUGES (http://www.auges.org/), un grupo online centrado en ASP.NET.
  4. Su primer evento fue Un paseo por ASP.NET MVC, impartido por Eduard Tomàs, de Raona. Puedes descargar los materiales y volver a ver el evento online.
  5. De paso, apúntate también a http://secondnug.com/, un grupo online con muchísima actividad, tanto de desarrollo como de sistemas (aunque yo de sistemas, ya sabes…) Tienen muchísimo material, sobre todo vídeos de charlas.
  6. Consejo personal para MVC: En las primeras versiones usaban la sintaxis clásica para las vistas, lo del <% %>, pero ahora tienen una sintaxis nueva llamada Razor. Los archivos en vez de .aspx son .cshtml (o .vbhtml). Aunque puedes escoger entre las dos, te recomiendo entrar en Razor directamente.
  7. Y sobre el acceso a datos, no sé si has probado EF, ya va por la versión EF 4.1 (aka CodeFirst). También muy recomendable para iniciarse en MVC, además muchos de los ejemplos combinan EF y MVC, así que matas dos cerdos de un pajarazo.
  8. Hay un ejemplo “oficial” con un tutorial muy muy extenso: http://mvcmusicstore.codeplex.com/ Puedes descargarlo para bichearlo, o tratar de hacerlo tú mismo con el tutorial. Se aprende mucho.

Bueno, hay muchos más enlaces pero para eso Google, o mejor, sigue la pista a los blogs de los miembros de Auges, que son todos unos cracks.

Suerte y para cualquier duda aquí me tienes.

| 2 comentarios

[WiX] Incluyendo ventanas en un instalador

En las entradas anteriores hemos generado un instalador sencillo que permite actualizar instalaciones anteriores, pero su interfaz de usuario es cuasi nulo: sin preguntar nada, sale la ventana de Windows Installer y procede a realizar la instalación.

Instalación básica

Al terminar, se cierra sin mostrar confirmación alguna. Aunque esto puede ser suficiente en algunos casos, vamos a ver como incluir en la instalación las típicas ventanas de Bienvenida, Seleccionar carpeta, Proceso y Fin de la instalación.

En primer lugar, WiX ofrece por separado las interfaces gráficas en la librería WixUIExtension.dll, que debemos incluir en la generación del instalador en la herramienta light con los siguientes parámetros:

light PicaSetup.wixobj -cultures:es-ES -ext WixUIExtension.dll

Con esto tendremos disponibles las ventanas en nuestro instalador, y además en nuestro idioma. Pero para que se usen tenemos que activarlas dentro de la etiqueta Project:

<Property Id="WIXUI_INSTALLDIR" Value="AppDir"/>
<UIRef Id="WixUI_InstallDir" />

Empecemos por el final: la etiqueta UIRef es la que incorpora las ventanas al instalador. Su valor es la plantilla de instalador que queremos usar, en este caso WixUI_InstallDir identifica la típica instalación con Bienvenida, Licencia, Carpeta, Progreso y Fin. Hay otras plantillas

Por otro lado, la etiqueta Property tiene una utilidad auxiliar pero muy importante: asociar el resultado de la ventana donde se selecciona la carpeta destino con el valor usado en el elemento Directory para instalar el producto. De no indicarla, el directorio seleccionado en esa ventana no se usaría después. Debe contener como Value el Id de la etiqueta Directory final donde vamos a instalar. En nuestro ejemplo anterior era AppDir, ya que se corresponde con la carpeta última donde se instalan los componentes.

Con esto ya podemos generar (usando los parámetros de light indicados antes para considerar la librería de UI) y tendremos un instalador totalmente funcional. Pero podemos personalizar su apariencia mediante variables de WiX.

Variables de WiX

Mediante las variables de WiX podemos pasar parámetros para la generación del instalador. Son parámetros para su construcción, no para el momento de la instalación. El valor de estas variables puede establecerse de dos formas:

  • En el archivo wxs, mediante un elemento WixVariable dentro de Wix:
    <WixVariable Id=”var” Value=”valor”/>
  • Como argumento de línea de comandos de la herramienta light:
    light PicaSetup.wxsobj -dvar=valor

Estas variables pueden ser consumidas también por las extensiones de WiX, y en nuestro caso WixUIExtension.dll acepta unas cuantas de ellas, como WixUIBannerBmp o WixUIDialogBmp.

Cambiando el texto de licencia

Una aplicación práctica, y quizá la más prioritaria en un instalador comercial, se refiere a la ventana de licencia, que por defecto muestra la licencia CPLv2 en inglés:

Licencia CPL por defecto en WIX

Esto desentona tanto por el idioma como porque quizá esta no sea nuestra licencia elegida. Para cambiarlo, podemos establecer la variable WixUILicenseRtf con la ruta donde encontrar un archivo RTF con la licencia. Esta variable puede declararse en el archivo wxs:

<WixVariable Id="WixUIDialogBmp" Value="licencia.rtf"/>

o bien como parámetro de la herramienta light:

light PicaSetup.wxsobj -dWixUIDialogBmp=licencia.rtf

En resumen, las posibilidades de configuración del interfaz gráfica del instalador son enormes en WiX, sirva este artículo como sencillo ejemplo de cómo conseguir un instalador decente y muy estándar para nuestras aplicaciones.

Publicado en Setup | Etiquetado , , , , , | 1 Comentario

[WiX] Actualizando una aplicación existente

Vamos a ver cómo preparar un instalador de WiX para soportar actualizaciones mayores. Aclaremos que actualizaciones mayores son actualizaciones completas, que no requieren de una versión anterior; mientras que las actualizaciones menores son parches que incluyen sólo parte de la funcionalidad, por lo que deben aplicarse sobre una instalación existente.

Aunque en mi introducción a WiX usé el atributo Product.UpgradeCode, este por sí mismo no tiene funcionalidad. Pero vamos a aclarar primero cómo usarlo y después veremos como hacer para que se tenga en cuenta. Según ya comentamos, su objetivo es identificar versiones anteriores del mismo producto, por lo que su valor debe conservarse en futuros instaladores. En cambio el Product.Id sí debe ser distinto en cada generación, de ahí que usemos ‘*’ para usar un nuevo Guid en cada caso. Pero si sólo con esto generamos dos instalaciones distintas y las instalamos en secuencia en un mismo puesto, veremos como en Agregar o quitar programas tenemos 2 productos con el mismo nombre.

Para evitarlo, debemos añadir dos nuevas etiquetas a nuestro elemento Product:

    <Upgrade Id="11111111-2222-3333-4444-555555555555">
      <UpgradeVersion Minimum="0.0.0" IncludeMinimum="yes"
                      Maximum="1.2.3" IncludeMaximum="no"
                      Property="PREVIOUSFOUND" OnlyDetect="no" />
    </Upgrade>
    <InstallExecuteSequence>
      <RemoveExistingProducts After="InstallInitialize"/>
    </InstallExecuteSequence>

La etiqueta Upgrade hace que se busquen versiones anteriores por su número de versión. Lo más sencillo es poner como Maximum la misma versión actual y excluirla con IncludeMaximum=”no”. Hay que destacar que es el atributo OnlyDetect=”no” el que da la orden de desinstalar las versiones anteriores, pero para ello requiere de la existencia de la etiqueta RemoveExistingProducts que vemos más abajo.

[Act 13:54] Y una cosa más que se me olvidaba y me parece importante: al comprobar versiones anteriores, sólo se comprueban las 3 primeras partes de la versión. Es decir, que si instalamos 1.0.0.0 y luego intentamos actualizar con 1.0.0.1, como las 3 primeras partes son iguales creerá que se trata de la misma versión. Esto he podido constatarlo personalmente y lo he corroborado con otras opiniones, pero no he encontrado documentación oficial al respecto en WiX ni en Windows Installer. Pero como el número de revisión (4º número de la versión) sólo suele diferenciar versiones del mismo día, su repercusión práctica significa que si queremos actualizar una versión por otra lanzada después el mismo día, tendremos que desinstalar la primera manualmente. Pienso que es difícil que tenga repercusión en nuestros clientes, o en todo caso de forma muy puntual.

Para profundizar en la actualización de versiones anteriores y descubrir más opciones, el manual de WiX: How to implement a major upgrade in your installer.

Publicado en Setup | Etiquetado , , , , , | 1 Comentario