Creando instaladores para sitios Web

martes, 14 de julio de 2009

Desde hace ya varias versiones, Visual Studio trae un tipo de proyecto que nos facilita las tareas de despliegue de soluciones, y ya en las últimas versiones ha venido ofreciendo una herramienta realmente confiable y de fácil implementación.

Los proyectos de Setup & Deployment se diferencian del resto por que no se debe escribir ni una sola línea de código para que funcionen, es mas si se utiliza el asistente para incrustar uno de estos proyecto en nuestra solución no necesitamos mas para obtener como resultado un instalador (básico) para un despliegue indoloro (depende del grado de complicación al que hayamos llegado).

Como soy un WebDeveloper el presente post trata (como lo dice el título) sobre instaladores para sitios web, si bien un sitio web no se lo instala en todas partes como una aplicación de escritorio o mobile, en el caso de trabajar con clientes externos a nuestro entorno es necesario (lo dicen las buenas prácticas) presentar un producto bien terminado, empaquetado y listo para la distribución. Comencemos entonces.

New Project

Voy a suponer que ya contamos con una solución web lista y funcionando, vamos a agregar un proyecto mas de tipo setup & deployment y como veran tenemos varias opciones, para hacer el post mas didáctico vamos a ir por el Setup Wizard, este proyecto nos tomara como proyecto principal a aquel proyecto que tengamos como start up de la solución, en el caso de proyectos web solo podemos incluir el Content de este, aquí vienen todos los assemblies que neesita para funciona asi que no es necesario incluir el resto de proyectos (librerías) al instalador.

Project Output

Una vez ubicado el proyecto en el directorio correspondiente llegamos a la vista del file system del instalador, veamos sus partes.

File System

En lugar de tener un espacio de edición de código, tenemos una especie de explorador, a la izquierda se ven las carpetas o folders que serán desplegados en la máquina destino (target) y a la derecha la lista de archivos, assemblies, project outputs y demas recursos que necesite el instalador para cumplir su misión, para poder agregar algo al file system solo necesitamos hacer click con el boton derecho sobre la zona de archivos y seleccionar la opción que se adecue a nuestras necesidades. (entre los recursos que normalmente se adjuntan estan los documentos de licencias, imagenes de splash, banners y assemblies que no van dentro de la solución pero que son necesarias).

Contando solo con esto ya tenemos un instalador de nuestro proyecto, pero, como todos sabemos una web que no se conecte a una base de datos no es web (punto de vista personal) entonces lo que faltaría aquí es establecer la conexión a la base de datos. ¿Y qué pasa si la base de datos no existe? bueno hay que crearla, desde la instalación el usuario final debería ser capaz de realizar todas estas tareas (y muchas otras más) sin complicarse la vida, es mas un usuario final con 0(cero) conocimiento de desarrollo de software debería ser capaz de instalar nuestro producto así como instala un juego o una aplicación de escritorio.

Obviamente una aplicación web no se parece mucho a una de escritorio, depende de la existencia de IIS y de los permisos necesarios para acceder a este. Es bueno saber que un setup de Windows Installer es capaz de detectar aquellos recursos que sean necesarios para el producto que se esta instalando, en este caso IIS, si este servicio no esta presente el instalador se encarga de levantar una excepción informandole al usuario final la situación.

Volviendo al Demo, necesitamos crear la base de datos a la que nuestra web se conectara y ademas tenemos que modificar el archivo web.config para indicar que la cadena de conexión apunta al servidor del cliente con el usuario que este ha definido. Para lograr esto hay que escribir código. :)

Existe una clase que permite que sobrecarguemos los procesos que se llevan a cabo cuando una aplicación (en este caso web) se está instalando, esta es la clase installer (System.Configuration.Install.Installer)

Como aprovechar la clase Installer

La idea de esta clase es la de proporcionar un vínculo entre el proceso de instalación y los eventos que esta genera con las modificaciones y/o configuraciones que necesita nuestra solución para estar correctamente instalada, esta clase está disponible sin importar el tipo de proyecto que se este trabajando (web o escritorio) pero para el caso del demo vamos a trabajar con Web.

Para comenzar, la clase Installer debe ser heredada, y esta clase derivada deberá encontrarse en un componente que este incluido en el proyecto de instalacion, las cosas deberán verse de esta forma:

SolutionExplorer

Como veran el proyecto CustomInstaller solo tiene una clase (CustomInstaller.cs) que es derivada de la clase Installer, este proyecto es de tipo ClassLibrary y el output del mismo debera estar incluido en el proyecto de instalación.

El namespace System.Configuration.Install debe estar referenciado en el proyecto CustomInstaller para poder tener dicponible la clase Installer.

Existen procesos que pueden ser sobrecargados al heredar esta clase, Install, Commit, Rollback y Unistall, para cada uno de ellos podemos escribir el código que necesitemos, en el caso de este demo solo vamos a usar el proceso Install. El código debería verse mas o menos asi:

using System;
using System.Configuration;
using System.Configuration.Install;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Web.Configuration;
using System.DirectoryServices;
using System.Data;
using System.Data.SqlClient;
using System.Text;

namespace CustomInstaller
{
[RunInstaller(true)]
public class CustomInstaller : Installer
{
const string DATABASE_CONNECTION_KEY = "ConnectionStringDemo";
public override void Commit(System.Collections.IDictionary savedState)
{
base.Commit(savedState);
}
public override void Install(System.Collections.IDictionary stateSaver)
{
base.Install(stateSaver);
string targetSite = string.Empty;
string targetVDir = string.Empty;
string targetDirectory = string.Empty;
string Server = string.Empty;
string Database = string.Empty;
string Username = string.Empty;
string Password = string.Empty;

try
{
//obtiene la informacion pasada por el usuario
targetSite = Context.Parameters["targetsite"];
targetVDir = Context.Parameters["targetvdir"];
targetDirectory = Context.Parameters["targetdir"];
//'Conexion a DB
Server = Context.Parameters["Server"];
Database = Context.Parameters["Catalogo"];
Username = Context.Parameters["Username"];
Password = Context.Parameters["Password"];
}
catch(Exception ex)
{
throw new Exception("Error al acceder a los valores de contexto :: " , ex);
}
InstallDatabase(targetSite, targetVDir, Server, Database, Username, Password);
}
public override void Rollback(System.Collections.IDictionary savedState)
{
base.Rollback(savedState);
}
public override void Uninstall(System.Collections.IDictionary savedState)
{
base.Uninstall(savedState);
}
private void InstallDatabase(string targetSite, string targetVDir, string server, string database, string Username, string Password)
{
string con = string.Empty;
SqlCommand cmd = new SqlCommand();
try
{
//Creando la base de datos con el nombre proporcionado por el usuario
con = string.Format("server={0};database=master;UID={1};PWD={2}", server, Username, Password);
cmd.CommandText = string.Format("create database {0}",database);
cmd.CommandType = CommandType.Text;
cmd.Connection = new SqlConnection(con);
cmd.Connection.Open();
cmd.ExecuteNonQuery();
cmd.Connection.Close();
}
catch (Exception ex)
{
throw new Exception(string.Format("La base de datos {0} no pudo ser creada ",database), ex);
}
try
{
//Creando la base de datos (contenido)
con = string.Format("server={0};database={3};UID={1};PWD={2}", server, Username, Password, database);
StreamReader script = new StreamReader(@"C:\inetpub\wwwroot\" + targetVDir + @"\DatabaseScript.sql");
cmd.CommandText = script.ReadToEnd();
cmd.CommandType = CommandType.Text;
cmd.Connection = new SqlConnection(con);
cmd.Connection.Open();
cmd.ExecuteNonQuery();
}
catch (Exception ex)
{
throw new Exception("
Los datos de la base de datos no fueron creados :: " , ex);
}
finally
{
if (cmd.Connection.State == ConnectionState.Open)
{ cmd.Connection.Close(); }
}

try
{
//Abriendo el archivo web.config del sitio web
Configuration config = WebConfigurationManager.OpenWebConfiguration("
/" + targetVDir);

//Creando una nueva llave en la seccion de connectionStrings para nuestra base de datos
ConnectionStringSettings appDatabase = new ConnectionStringSettings();
appDatabase.Name = DATABASE_CONNECTION_KEY;
appDatabase.ConnectionString = con;
appDatabase.ProviderName = "
System.Data.SqlClient";

config.ConnectionStrings.ConnectionStrings.Clear();
config.ConnectionStrings.ConnectionStrings.Add(appDatabase);

//grabando las modificaciones
config.Save();
}
catch (Exception ex)
{
throw new Exception("
El archivo web.config no pudo ser modificado :: " , ex);
}
}
}
}
 

Nota

El atributo [RunInstaller(true)] debe estar presente, de lo contrario la clase no se instancia y por ende el código no se ejecuta.

Interfaz de usuario del asistente

Como se puede ver, no es nada complejo, ahora lo que necesitamos es pasarle los parametros de contexto desde la interfaz (UI) del asistente de instalación, para eso vamos a adicionar pasos al instalador.

Primero vamos a familiarizarnos con el IDE y como agregamos estos pasos al asistente, para ver el UI del mismo tenemos 2 opciones, hacer click con el boton derecho sobre el proyecto de instalación en la opción ver (view), seleccionar Interfaz de usuario (User Interface)

image

Otra opción es usando los botones en la ventana del explorador de la solución al seleccionar el proyecto de instalación. (el tercero de derecha a izquierda)

image

La IDE que se desplegará debiera verse mas o menos asi:

image

En la imagen se puede ver que ya tengo agregado un paso con el nombre Texboxes (A) y que en la ventana de propiedades se ven todas las propiedades que se tienen disponibles. Como verán (o mejor dicho como no verán) no hay forma de ver la interfaz de forma gráfica como un formulario windows, debemos trabajar todo a traves de estas propiedades; todas las propiedades que terminen con "label" son las etiquetas que mostraran una descripción del campo que se solicita al usuario, las que terminan con el termino "property" son el nombre con el que seran administradas las propiedades desde la clase Installer (Context) (eso se puede ver en el código de la clase derivada de Installer)

El nombre de las propiedades sera siempre en mayúsculas (automaticamente) y debe ser exactamente el mismo cuando accedamos a ellas desde la información de contexto.

Deben preguntarse ¿Por qué hay dos grupos, Install y Administrative Install? (bueno si no se lo preguntaban antes ahora sí) la razón es sencilla, el grupo Install es el que se despliega al usar el instalador como usuario final, es decir, que el usuario inicie el proceso de instalacion al ejecutar el setup.exe, el otro grupo (Administrative Install) se ejecuta cuando un administrador de red ejecuta el instalador desde un distribuidor de aplicaciones (SMS, MOS, etc.) el demo no va a tocar ese tema (aun) asi que lo dejamos como se ve e la imagen.

En este caso solo he agregado 1 paso más al instalador, se pueden agregar cuantos pasos sean necesarios para obtener toda la información requerida por el instalador para que todo salga bien, en el caso de este demo solo se solicita la información necesaria para la creación de la base de datos a la que nuestra aplicación web se va a conectar (servidor, base de datos, usuario y clave).

Con todos estos pasos (que no son muchos) es posible crear un instalador que permitirá no solo instalar sino que reparar y desinstalar nuestra aplicación en el cliente sin complicarle la vida.

0 comments:

Publicar un comentario