En la empresa en la que trabajo actualmente han surgido una serie de nuevos proyecto en la que parte de la solución pasa por la creación de Servicios Windows que ejecuten tareas de forma automática y sistemática (sin la intervención de un usuario). Debido a esto me han pedido que cree una proyecto plantilla que sirva como punto de inicio de cada proyecto especifico.

Las premisas de la plantilla eran las siguientes:

  1. Que se pueda depurar haciendo F5 en Visual Studio
  2. Que sea fácil de instalar
  3. Que tenga incorporado un logger para “trazar” la aplicación.
Aunque todas estas premisas son tareas comunes cuando desarrollas un servicio el tema de implementarlas en un servicio base se debe a la necesidad de que todos los servicios que se creen partiendo de esta "Plantilla" tengan esta funcionalidad implementada de la misma forma.

Que se pueda depurar haciendo F5 en Visual Studio

Aunque hay varias formas de depurar un servicio de Windows (tal y como se describe en MSDN) seguro que todo desarrollador agradecería que con un simple F5 se pueda entrar en modo depuración del servicio.

Para resolver este punto, y entendiendo que el código que se quiere depurar es el que se encuentra dentro del método OnStart() del servicio. Podríamos modificar el método Main que te crea la plantilla por defecto que viene con VisualStudio ( al hacer File –> New Project –> Windows Service ) para que acepte un arreglo de string como parámetro y en función del valor del parámetro crear una instancia de la clase del servicio y llamar al metodo OnStart().

using System.ServiceProcess;
using System.Threading;

namespace WindowsService1
{
    static class Program
    {
        /// 
        /// The main entry point for the application.
        /// 
        static void Main(string[] args)
        {
            if (args[0] == "D" || args[0] == "-D")
            {
                var service = new Service1();
                service.OnStart(null);

                do
                {
                    Thread.Sleep(1000);
                } while (true);
            }
            else
            {
                ServiceBase[] ServicesToRun;
                ServicesToRun = new ServiceBase[]
                                    {
                                        new Service1()
                                    };
                ServiceBase.Run(ServicesToRun);
            }
        }
    }
}

En este punto nos encontramos con un problema y es que el método OnStart() es un método protegido por lo que solo se puede llamar desde la clase en la que esta definido o en las que deriven de esta. Así que una solución podría ser pasar el punto de entrada de la aplicación (Método MAIN) a nuestra case Service1 y eliminar el archivo Program.cs. A continuación se muestra como va quedando la clase.

using System.ServiceProcess;
using System.Threading;

namespace WindowsService1
{
    public partial class Service1 : ServiceBase
    {
        public Service1()
        {
            InitializeComponent();
        }

        protected override void OnStart(string[] args)
        {
        }

        protected override void OnStop()
        {
        }

        static void Main(string[] args)
        {
            if (args[0] == "D" || args[0] == "-D")
            {
                var service = new Service1();
                service.OnStart(null);

                do
                {
                    Thread.Sleep(1000);
                } while (true);
            }
            else
            {
                ServiceBase[] ServicesToRun;
                ServicesToRun = new ServiceBase[]
                                    {
                                        new Service1()
                                    };
                ServiceBase.Run(ServicesToRun);
            }
        }
    }
}

En este punto solo nos queda pasarle este parámetro a método Main a presionar la tecla F5 estando en VisualStudio. Esto es realmente sencillo, para ello haremos click derecho –> Propiedades sobre el proyecto de Servicio. y dentro del Tab “Debug” nos vamos a la opción “Start Options” y dentro de esta buscamos la caja de texto “Command line arguments” y escribimos –D

Properties_Debug

y con esto ya podemos depurar pulsando la tecla F5 desde Visual Studio.

Que sea facil de instalar

Para instalar un servicio podemos hacerlo utilizando las herramientas habituales: Proyecto de Instalación de VisualeStudio, herramienta de instalación InstallUtil.exe, InstallShield etc. Pero en esta ocasión y siendo consecuentes con la solución anterior queremos que el servicio se instale y desinstale pasándole parámetros (-I, -U)

Para lograr esto hemos utilizado la plataforma P/Invoke y las APIs para win32.

Una herramienta de mucha utilidad siempre se trabaje con win32 y pinvoke es la wiki desarrollada por redgate (pinvoke.net).

A continuación el código de la clase que utilizamos para instalar y desinstalar el servicio.

using System;
using System.Runtime.InteropServices;

namespace WindowsService
{
    class ServiceInstaller
    {
        [DllImport("advapi32.dll")]
        public static extern IntPtr OpenSCManager(string lpMachineName,string lpScdb, int scParameter);

        [DllImport("Advapi32.dll")]
        public static extern IntPtr CreateService(IntPtr scHandle,string lpSvcName,string lpDisplayName, int dwDesiredAccess,int dwServiceType,int dwStartType,int dwErrorControl,string lpPathName, string lpLoadOrderGroup,int lpdwTagId,string lpDependencies,string lpServiceStartName,string lpPassword);

        [DllImport("advapi32.dll")]
        public static extern IntPtr CloseServiceHandle(IntPtr schandle);

        [DllImport("advapi32.dll")]
        public static extern IntPtr StartService(IntPtr svhandle, int dwNumServiceArgs, string lpServiceArgVectors);
  
        [DllImport("advapi32.dll",SetLastError=true)]
        public static extern IntPtr OpenService(IntPtr schandle,string lpSvcName,int dwNumServiceArgs);

        [DllImport("advapi32.dll")]
        public static extern IntPtr DeleteService(IntPtr svhandle);
 
        [DllImport("kernel32.dll")]
        public static extern int GetLastError();
  
        public void Install(string servicePath, string serviceName, string serviceDisplayName)
        {
            int SC_MANAGER_CREATE_SERVICE = 0x0002;
            int SERVICE_WIN32_OWN_PROCESS = 0x00000010;
            //int SERVICE_AUTO_START                = 0x00000002;
            int SERVICE_DEMAND_START = 0x00000003;
            int SERVICE_ERROR_NORMAL = 0x00000001;

            int STANDARD_RIGHTS_REQUIRED = 0xF0000;
            int SERVICE_QUERY_CONFIG = 0x0001;
            int SERVICE_CHANGE_CONFIG = 0x0002;
            int SERVICE_QUERY_STATUS = 0x0004;
            int SERVICE_ENUMERATE_DEPENDENTS = 0x0008;
            int SERVICE_START = 0x0010;
            int SERVICE_STOP = 0x0020;
            int SERVICE_PAUSE_CONTINUE = 0x0040;
            int SERVICE_INTERROGATE = 0x0080;
            int SERVICE_USER_DEFINED_CONTROL = 0x0100;

            int SERVICE_ALL_ACCESS = (STANDARD_RIGHTS_REQUIRED |
                SERVICE_QUERY_CONFIG |
                SERVICE_CHANGE_CONFIG |
                SERVICE_QUERY_STATUS |
                SERVICE_ENUMERATE_DEPENDENTS |
                SERVICE_START |
                SERVICE_STOP |
                SERVICE_PAUSE_CONTINUE |
                SERVICE_INTERROGATE |
                SERVICE_USER_DEFINED_CONTROL);

            IntPtr iptSCMHandle = OpenSCManager(null, null, SC_MANAGER_CREATE_SERVICE);

            if (iptSCMHandle == IntPtr.Zero)
            {
                throw new CustomServiceException(GetLastError());
            }

            IntPtr iptSVCHandle = CreateService(iptSCMHandle,
                                                serviceName,
                                                serviceDisplayName,
                                                SERVICE_ALL_ACCESS,
                                                SERVICE_WIN32_OWN_PROCESS,
                                                SERVICE_DEMAND_START,
                                                SERVICE_ERROR_NORMAL,
                                                servicePath,
                                                null, 0, null,
                                                null, null);

            if (iptSVCHandle == IntPtr.Zero)
            {
                throw new CustomServiceException(GetLastError());
            }

            if (CloseServiceHandle(iptSVCHandle) == IntPtr.Zero)
            {
                throw new CustomServiceException(GetLastError());
            }
        }

        public void Uninstall(string serviceName)
        {
            int GENERIC_WRITE = 0x40000000;
            int DELETE = 0x10000;
            IntPtr iptSCMHandle;
            IntPtr iptSVCHandle;

            iptSCMHandle = OpenSCManager(null, null, GENERIC_WRITE);

            if (iptSCMHandle == IntPtr.Zero)
            {
                throw new CustomServiceException(GetLastError());
            }

            iptSVCHandle = OpenService(iptSCMHandle, serviceName, DELETE);
            if (iptSVCHandle == IntPtr.Zero)
            {
                throw new CustomServiceException(GetLastError());
            }

            if (DeleteService(iptSVCHandle) == IntPtr.Zero)
            {
                throw new CustomServiceException(GetLastError());
            }

            if (CloseServiceHandle(iptSCMHandle) == IntPtr.Zero)
            {
                throw new CustomServiceException(GetLastError());
            }
        }
        
    }
}

Con esto logramos instalar y desinstalar el servicio por líneas de comando.

Command Prompt

Que tenga incorporado un logger para “trazar” la aplicación.

Debido a la amplia variedad de productos que existen para implementar trazas en una aplicación. A continuación enumero algunas de las que he utilizado y creo son las mas populares.

  1. Log4net
  2. NLog
  3. Enterprise Libraries

y debido al hecho de que este es un servicio plantilla. La implementación del sistema de trazas no debería estar acoplado al servicio. Para esto hemos creado un contrato (interface ILogger) la cual provee al servicio de una capa de abstracción de la implementación real, con el objetivo de poder cambiarla cada vez que se desee. En este caso en particular y debido al amplio uso de las Enterprise Libraries  dentro de mi empresa, la implementación concreta del logger esta basada en este producto.

Aquí dejo el código fuente completo del Servicio .


En un post anterior detallé los pasos básicos necesarios para la implementación de un ActiveX con .NET (C#). Pero faltó por describir aquellos necesarios para la interacción de la página HTML con el ActiveX y de este con la página.

Interacción de la página con el ActiveX

La interacción de la página con el ActiveX consiste en poder llamar a métodos del ActiveX o establecer y/o recuperar valores de propiedades públicas del mismo.

Para establecer el valor de una propiedad haremos lo siguiente:

  1. Marcar la propiedad, a la que queremos acceder desde la página, como visible para COM.
  2. [ComVisible(true)]
    public string Property1
    {
        get; set;
    }
  3. Establecer el valor de la propiedad pasándolo como parámetro del ActiveX
  4. <object id="MyActiveX" width="306px" height="167px" classid="clsid:D857B4F5-8684-453e-82C8-7F493CBE5592"
                viewastext style="border-style:dashed">
        <param name="Property1" value="desde Javascript" />
        <p>
            Pon el texto que quieras mostrar si se abre la página con un browser no compatible
            o si el ActiveX no está instalado correctamente.             
        </p>
    </object>
  5. Recuperar el valor de la propiedad del ActiveX
  6. <script type="text/javascript">
        function Method1() {
            alert(MyActiveX.Property1);
        }
    </script>

Para llamar a un método del ActiveX haremos lo siguiente:

  1. Marcar el método que queremos llamar como visible para COM.
  2. [ComVisible(true)]
    public void Method1()
    {
        MessageBox.Show("Hola Mundo desde Javascript");
    }
  3. Llamar el método desde la página.
  4. <script type="text/javascript">
        function Method2() {
            MyActiveX.Method1();
        }
    </script>
    y si abrimos la página en Internet Explorer y hacemos clic en el nuevo botón veremos lo siguiente
    LlamadaDesdeJS

Interaccion del ActiveX con la página

La interacción del ActiveX con la página consiste en poder ejecutar código javascript desde el ActiveX. Esta funcionalidad esta muy bien descrita en este artículo de microsoft y en esencia se trata de crear eventos en .NET y suscribirse a estos desde javascript.

Para ello implementamos la siguiente interface.

using System.Runtime.InteropServices;

namespace MyActiveX
{
    public delegate void ControlEventHandler(string eventArgs);

    [Guid("0422D916-C11A-474e-947D-45A107038D12")]
    [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
    [ComVisible(true)]
    public interface IControlEvents
    {
        [DispId(0x60020000)]
        void Execute(string handler);
    }
}

Marcamos el control tal como sigue

[ClassInterface(ClassInterfaceType.AutoDispatch), ComSourceInterfaces(typeof(IControlEvents))]
[ComVisible(true), Guid("D857B4F5-8684-453e-82C8-7F493CBE5592")]
public partial class MyUserControl : UserControl
{
    ...
}

Luego creamos un evento y el método que lo dispara

public event ControlEventHandler Execute;

private void OnExecute(string handler)
{
    if (Execute != null)
        Execute(handler);
}

Una vez hecho esto en la página HTML nos suscribimos al evento de la siguiente forma

<script type="text/javascript">        
    function MyActiveX::Execute(param)
    {
        alert(param)
    }
</script>

y ya solo los queda llamarlo cuando haga falta. Para este ejemplo la llamada la haremos al hacer click en un botón del control.

mapping_events

y esto ha sido todo. Creo que solo quedaría lo relacionado con marcar el ActiveX como seguro para scripting pero eso es algo que podemos retomar en otro post. Mientras tanto aquí les dejo el código fuente.


Uno de los proyectos en los que trabajo actualmente trata de crear un componente común a varias aplicaciones de una empresa. Dichas aplicaciones son todas web y el componente trata de automatizar un proceso que solo puede ocurrir del lado del cliente: capturar la firma digital hecha por un usuario de cualquiera de estas aplicaciones a través de un tableta conectada a un ordenador por USB. Así que como solución se optó por crear un ActiveX ya que como política de la empresa todos eran ordenadores Windows que usaban Microsoft Internet Explorer como navegador.

Yo nunca antes había hecho un ActiveX así que comencé a buscar información en la web. Aunque existen muchos sitios en los cuales se puede encontrar información acerca de este tema. La solución final fue el resultado de lo encontrado en diversos lugares. Este artículo resume todos los pasos que necesité para completar la construcción del ActiveX, y espero que me sirva como referencia en el futuro.

1.) Creando el control ActiveX

Lo primero que haremos es crear un proyecto en VisualStudio del tipo Librería de Clases (Class Library). En la que añadiremos un UserControl en el pondremos un botón y en el evento clic de este mostraremos un mensaje.

SolucionInicial La clase “MyUserControl” deberemos marcarla con los siguientes atributos:

  1. Guid –> Identificador de la clase cuando esta sea expuesta en COM
  2. ComVisible –> Expone la clase para que sea visible desde COM
  3. ClassInterfaceAttribute –> Es el tipo de Interface COM que encapsulará a la clase (mas información en MSDN)

Tip: Si tu ensamblado va a tener más de una clase asegúrate de que esté marcado como NO visible para COM y solo marca las clases realmente quieras exponer.

[assembly: ComVisible(false)]
Al final tenemos algo como en código que sigue a continuación.
using System;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace MyActiveX
{
    [ClassInterface(ClassInterfaceType.AutoDispatch)]
    [ComVisible(true), Guid("D857B4F5-8684-453e-82C8-7F493CBE5592")]
    public partial class MyUserControl : UserControl
    {
        public MyUserControl()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            MessageBox.Show("Hola Mundo");
        }
    }
}

2.) Incrustando el control en la página HTML

Para mostrar el control en una página web primero hay que instalarlo, ese será el siguiente paso, y luego solo hay que utilizar el tag object como se muestra a continuación. Ten en cuenta que el atributo classid debes poner el GUID con que has decorado la clase en C#



    

Este texto se mostrará si se abre la página con un browser no compatible o si el ActiveX no está instalado correctamente.

3.) Instalando el AcativeX

Para instalar el ActiveX una de las opciones que tenemos es la de crear un instalador que además de dar la opción de ubicar los archivos necesarios para que la aplicación funcione, registre el ensamblado (DLL) que contiene la clase que queremos exponer en COM. Solo hará falta añadir el proyecto principal al proyecto de instalación de VisualStudio.

PrimaryOutput

Seleccionando el “Primary output from MyActiveX” y visualizando sus propiedades, podremos marcar la DLL para que se registre en COM durante la instalación.

InstaladorUna vez hecho esto generamos el instalador lo instalamos en el ordenador y al abrir Internet Explorer y navegar a la página de demostración, vemos el control que hemos creado previamente.

html

y si hacemos clic en el botón vemos que funciona tal y como esperábamos.

holamundo

En un próximo post trataré el tema de la interacción entre el ActiveX y el Navegador.

 

Puedes descargarte el código fuente desde aquí


 |