[How To] NodeJS, Sinatra, NGINX y Azure Worker Role

UPDATE: Ya esta en este repositorio el código de este ejemplo. El orden ha cambiado y el código de los scripts es más fácil de entender ahora.

En el post anterior vimos como correr NGINX dentro de un Azure Node Worker Role, aunque de NodeJS no vimos nada, pues la configuración la dejamos pendiente para una siguiente entrada. Así pues, veamos como correr NodeJS dentro de nginx en nuestro Azure Worker Role.

1- Crear archivo de configuración para nginx

1.1- Nginx usa un archivo de configuración localizado en la ruta /nginx/conf/nginx.conf Este archivo trae una configuración de forma predeterminada, configuración que debemos sobrescribir para poder usar el proxy inverso y llamar, por ejemplo, a nuestro nodejs.

La mejor forma de hacerlo, para no complicarnos mucho, es crear una copia del nginx.conf en la raíz del WorkerRole1 y agregar la siguiente entrada:

image

1.2- Ahora necesitamos que al finalizar la descarga y el unzip del nginx sea reemplazada la configuración predeterminada por nuestro nuevo .conf. Para esto agregamos esta línea Copy-Item "$current\nginx.conf" "$current\nginx-1.6.0\conf\" -force a el archivo download_nginx.ps1 (archivo que vimos en el articulo anterior). El script queda así:

$current = [string] (Get-Location -PSProvider FileSystem)
$webclient = New-Object System.Net.WebClient
$url = "http://nginx.org/download/nginx-1.6.0.zip"
$file = "$current\nginx.zip"
$webclient.DownloadFile($url,$file)

$shell_app=new-object -com shell.application
$zip_file = $shell_app.namespace($file)
$destination = $shell_app.namespace($current)
$destination.Copyhere($zip_file.items())
Remove-Item nginx.zip

Copy-Item "$current\nginx.conf" "$current\nginx-1.6.0\conf\" -force

$firewallFile = "$current\nginx-1.6.0\nginx.exe"
netsh advfirewall firewall add rule name="nginx" dir=in action=allow program="$firewallFile" enable=yes
cd "nginx-1.6.0"
& ".\nginx.exe"

1.3- Si ejecutamos el Emulador de Azure y navegamos al http://localhost/ veremos algo como:

image

Y si apuntamos a /nodeapp veremos algo como:

image

Y en las herramientas de captura web encontraremos:

image

Hasta aquí este post sería muy corto, así que mostraré como correr Sinatra, a la par, dentro del NGINX.

2- Descargar Ruby e instalar Sinatra

2.1- Agregaremos un archivo llamado download_ruby.ps1 a la raíz del WorkerRole1. El código de este archivo será el siguiente:

$current = [string] (Get-Location -PSProvider FileSystem)
$webclient = New-Object System.Net.WebClient
$url = "http://dl.bintray.com/oneclick/rubyinstaller/rubyinstaller-2.0.0-p481.exe"
$file = "$current\ruby.exe"
$webclient.DownloadFile($url,$file)

Invoke-Expression "$file /silent"
Remove-Item ruby.exe
& "$current\install_sinatra.cmd"

El código anterior descargará la versión 2 de Ruby que encontramos en Ruby Installer. Ejecutará el instalador de forma silenciosa, removerá el .exe y finalmente llamará a otro .cmd que será el encargado de instalar Sinatra.

2.2- Agregamos un archivo llamado install_sinatra.cmd en la raíz del WorkerRole1. con el siguiente código:

 
D:\Ruby200\bin\gem.bat install sinatra --no-ri --no-rdoc

NOTA: En mi maquina de desarrollo (como en muchas) los programas se instalan de forma predeterminada en la partición C del disco duro. En las maquinas virtuales de Azure lo hacen en D:\. No olvides cambiar el cmd o agregar alguna validación para que funcione en ambos entornos.

2.3- Agregaremos ahora un archivo main.rb a la raíz del WorkerRole1, este archivo tendrá alguna configuración de Sinatra y será el encargado de responder las solicitudes que hagamos a la ruta “/”. El código de este archivo es:

require 'sinatra'
set :environment, :development
set :port, 1338
get '/' do
  "Hello, my name is Sinatra"
end

2.4- Ahora que tenemos la aplicación de Ruby lista necesitamos iniciarla, para esto agregaremos u nuevo archivo llamado start_ruby.cmd con una sola línea de código:

D:\Ruby200\bin\ruby.exe main.rb

2.5- Con esto solo nos queda invocar los .cmd correspondientes desde el Worker Role. Para esto agregaremos dos Task al Service Definition, una llamando al .cmd encargado de descargar e instalar Ruby y otra para iniciar nuestra app:

      <Task commandLine="setup_ruby.cmd" executionContext="elevated" taskType="background" />
      <Task commandLine="start_ruby.cmd" executionContext="elevated" taskType="background" />

Las entradas en el Service Definition quedan así :

image

3- Editar el nginx.conf

3.1- Para decirle a nginx que corra Sinatra editaremos la entrada location / del nginx.conf que agregamos, queda así:

        location / {            
            proxy_pass   http://127.0.0.1:1338;
            #root   html;
            #index  index.html index.htm;
        }   

3.2- Si ejecutamos ahora el emulador y navegamos a al localhost veremos algo como:

sinatranodeng

Known Issues

1- Se hace necesario agregar enviaroment variables a las tasks que ejecutan la instalación de Ruby y Sinatra. Esto para poder alternar, por ejemplo, donde se busca el ruby.exe en los .cmd.

2- He notado que en ocasiones(a veces si, a veces no) no se ejecutan todos los Scripts al desplegar en la nube. Aún desconozco la razón de este comportamiento. Por ello se hace necesario habilitar la conexión por escritorio remoto al Azure Role para poder terminar la instalación.

3- Si se cambia la entrada del location de Sinatra en el nginx.conf. Sinatra responde con un 404.

Notas finales

1- Cabe anotar que nada de lo mostrado aquí ha sido probado en entornos de producción.

2- Cualquier sugerencia por favor no dude en comentarlo.

3- En los próximos días publicaré el repo en mi GitHub. Revisar el update

Espero les sea de utilidad.

Hasta el próximo post.

[How To] NodeJS, Sinatra, NGINX y Azure Worker Role

[How to] Correr NGINX en un NodeJS Azure Worker Role

No hace mucho empecé a jugar con nginx para correr django y nodejs dentro del mismo server, haciendo uso de su propiedad de proxy inverso. Ésta es solo una de sus características, pero hablar de nginx daría para muchos artículos, y además, hay documentación oficial del producto.

Estas pruebas las venía realizando dentro de mi maquina de desarrollo, nunca he probado o trabajado con nginx en un entorno productivo, así que, para tratar de llevar un poco más allá estas pruebas me decidí a desplegar el nginx dentro de un worker role que me respondiera las solicitudes al nodejs y demás.

Vale aclarar que existen más formas de hacerlo, como haciendo deploy de una imagen de maquina virtual que ya tenga el nginx o creando una maquina virtual de Ubuntu e instalando y configurando el servidor.

1. Crear el NodeJS Azure Cloud Service

Sobre este tema ya se escribió un tutorial completo en el sitio de azure, pero los pongo aquí para que se entienda.

1.1-  Abrimos el Azure Power Shell con privilegios de administrador, ubicamos el directorio en que vamos a trabajar y ejecutamos el comando New-AzureServiceProject testnginx

image

1.2- Ahora, dentro de la carpeta que creó el comando anterior agregamos un Azure Worker Role con el comando Add-azurenodeworkerrole

image

1.3- Ya con esto podemos probar que está funcionando, haciendo uso del comando start-azureemulator y navegando en el browser a la dirección indicada.

2. Descargar e iniciar nginx

2.1- Lo primero que haremos será revisar que es lo que han hecho estos comandos que ejecutamos en el paso uno. Para ello usaré Sublime Text (aunque no hace falta usar editores complejos, con el File explorer y notepad es suficiente)

image

De entrada vemos la carpeta WorkerRole1, ésta contiene un par de .cmd, .js y un .ps1. Para ver como interactúan estos archivos solo hace falta revisar el ServiceDefinition.csdef. En este ubicaremos el Startup y cada una de sus Tasks. Estas tareas llaman a cada uno de los .cmd que ya habíamos visto antes. Y estos .cmd ejecutarán las tareas que tengan programadas, como por ejemplo, descargar nodejs.exe. Viendo esto podemos darnos cuenta por donde es el camino y empezaremos a realizar todas las tareas de scripting que necesitemos.

2.2- Agregaremos una Task al Startup del Role, esta tarea llamará a un archivo setup_nginx.cmd:

image

De hecho, esta será la única modificación que haremos sobre el ServiceDefinition. Queda así:

<?xml version="1.0" encoding="utf-8"?>
<ServiceDefinition xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" name="testnginx" xmlns="http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceDefinition">
  <WorkerRole name="WorkerRole1">
    <Startup>
      <Task commandLine="setup_worker.cmd &gt; log.txt" executionContext="elevated">
        <Environment>
          <Variable name="EMULATED">
            <RoleInstanceValue xpath="/RoleEnvironment/Deployment/@emulated" />
          </Variable>
          <Variable name="RUNTIMEID" value="node" />
          <Variable name="RUNTIMEURL" value="" />
        </Environment>
      </Task>
      <Task commandLine="node.cmd .\startup.js" executionContext="elevated" />
      <Task commandLine="setup_nginx.cmd" executionContext="elevated" taskType="background" />
    </Startup>
    <Endpoints>
      <InputEndpoint name="HttpIn" protocol="tcp" port="80" localPort="80"/>
    </Endpoints>
    <Runtime>
      <Environment>
        <Variable name="PORT">
          <RoleInstanceValue xpath="/RoleEnvironment/CurrentInstance/Endpoints/Endpoint[@name='HttpIn']/@port" />
        </Variable>
        <Variable name="EMULATED">
          <RoleInstanceValue xpath="/RoleEnvironment/Deployment/@emulated" />
        </Variable>
      </Environment>
      <EntryPoint>
        <ProgramEntryPoint commandLine="node.cmd .\server.js" setReadyOnProcessStart="true" />
      </EntryPoint>
    </Runtime>
  </WorkerRole>
</ServiceDefinition>

2.3- En la carpeta WorkerRole1 agregaremos el archivo setup_nginx.cmd con el siguiente código:

@echo off
powershell -c "Set-ExecutionPolicy Unrestricted"
powershell .\download_nginx.ps1

Este código lo único que haremos es llamar al script downloadnginx.ps1

2.4- En la carpeta WorkerRole1 agregamos un archivo llamado download_nginx.ps1 con el siguiente código:

$current = [string] (Get-Location -PSProvider FileSystem)
$webclient = New-Object System.Net.WebClient
$url = "http://nginx.org/download/nginx-1.6.0.zip"
$file = "$current\nginx.zip"
$webclient.DownloadFile($url,$file)

$shell_app=new-object -com shell.application
$zip_file = $shell_app.namespace($file)
$destination = $shell_app.namespace($current)
$destination.Copyhere($zip_file.items())
Remove-Item nginx.zip

$firewallFile = "$current\nginx-1.6.0\nginx.exe"
netsh advfirewall firewall add rule name="nginx" dir=in action=allow program="$firewallFile" enable=yes
cd "nginx-1.6.0"
& ".\nginx.exe"

Este código se divide en tres secciones. La primera crea un webclient apuntando a la dirección de descarga de la ultima versión estable de nginx, la descarga y la deja en la carpeta “current”. La segunda sección hace un unzip de los archivos y borra el .zip. La tercera y ultima sección del script agrega una regla en el firewall y ejecuta el nginx.exe

2.5- Por ahora (aún no vamos a correr nodejs dentro del nginx) cambiaremos el puerto del nodejs al 1337. Esto en el archivo server.js:

image

2.6- Si ejecutamos el comando start-azureemulator y navegamos al localhost veremos algo como:

image

3- Desplegar en la nube

Sobre esto también se ha escrito documentación en el sitio oficial pero la transcribiré aquí para no perder el hilo de la historia.

3.1- Lo primero será agregar el Azure Account con el comando Add-Azureaccount

3.2- Descargamos el publication settings con el comando Get-AzurePublishSettingsFile

3.3- Importamos el archivo que habíamos descargado con el comando Import-AzurePublishSettingsFile "path donde esta el archivo"

3.4- publicamos con el comando Publish-AzureServiceProject -ServiceName nombre_del_servicio_en_la_nube -Location "East US" -Launch

Espero les sea de utilidad.

Hasta el próximo post.

[How to] Correr NGINX en un NodeJS Azure Worker Role

[How To][Windows Azure] Agregar Un Web Service a un Proyecto Azure

Últimamente he visto en varios foros y grupos de usuarios, personas con la necesidad de subir a la nube de Microsoft servicios web del tipo .asmx, la verdad que no tiene mucho misterio pero debo decir que no lo he probado en ningún proyecto en producción porque no se me ha presentado un caso donde necesite explícitamente un Web Service por sobre un WCF por ejemplo. Igual tampoco me gustan los WCF pero eso no hace parte de esta entrada 😛

Empezamos con un proyecto de tipo Cloud:

En este caso no me interesa agregar ningún tipo de Role, así que pulsamos ok sin agregar ninguno, así:

Una vez realizado esto, podemos entonces agregar un nuevo proyecto de tipo Web Service, pero ojo en este punto! Y creo, es por lo que a la gente le genera tanta confusión, en Framework 4.0 no se pueden crear proyectos de extensión .asmx, para esta versión solo se dispone de las aplicaciones de WCF con extensión .svc. No soy un experto en estas dos tecnologías de servicios web, y no tengo muy claro el por qué de este “impedimento”. Asi que debemos seleccionar el framework 3.5 para poder ubicar en la sección de proyectos web la plantilla de ASP.NET Web Service Application, así:

Una vez hecho esto, solo debemos agregar un nuevo Web Role a nuestro proyecto de Cloud, haciendo click derecho en la carpeta Roles opción Add opción Web Role in Project Solution, así:

Una vez hecho esto, puedes probar que tu aplicación este corriendo localmente pulsando F5.

Una vez publicado en la nube puedes probar su funcionamiento con un proyecto cliente por ejemplo o simplemente apuntando a la Url del servicio desde tu navegador.

Espero les sea de utilidad.

Hasta el próximo post.

[How To][Windows Azure] Agregar Un Web Service a un Proyecto Azure

[How To] Ventanas Modales en WPF

Hola, en los últimos días volví a tocar uno de los lenguajes de marcado que mas me ha gustado, si estoy hablando de XAML, esta vez con Windows Presentation Foundation, allí tuve que implementar ventanas modales, y creí que seria tan fácil como crear un elemento ChildWindow (Silverlight) y llamarlo. Pero bueno, manos a la obra, igual si algún lector se sabe una mejor… por favor hágamelo saber 😀

– Implementando una ventana Modal:

En un proyecto con dos ventanas de WPF normales:

Para lanzar una ventana como modal, basta con hacer lo siguiente.

Sobre el evento del control que debe llamar la ventana modal:

private void LlamarModalClick(object sender, RoutedEventArgs e)

{

     Modal modal = new Modal();

     modal.Owner = this;

     modal.ShowDialog();

}

Como podemos ver, la ventana ha salido en una posición “aleatoria” y parece todo menos una ventana modal, para cambiar esto lo que haremos será modificar algunas propiedades en esta.

El XAML de la ventana modal quedaría de la forma:

<Window x:Class=”ModalWPF.Modal”

        xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation&#8221;

        xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml&#8221;

        Title=”Modal”

        Height=”300″

        Width=”300″

        WindowStyle=”None”

        ShowInTaskbar=”False”

        WindowStartupLocation=”CenterOwner”>

    <Grid>

        <Label Content=”Hola soy la
ventana modal 😀 “></
Label>

    </Grid>

</Window>

 

Ahora nuestra ventana modal tiene un mejor aspecto, aun se puede mejorar, pero para allá vamos, lo que si notamos es que no se ve bonita la ventana de fondo, es decir, debería tener un efecto como de desenfoque ¿no?

– Aplicando efecto de fondo con una ventana Modal:

Para aplicar un efecto de desenfoque emplearemos el espacio de nombres System.Windows.Media.Effects, el código nos quedaría de la forma:


private void LlamarModalClick(object sender, RoutedEventArgs e)

{

  Modal modal = new Modal();

modal.Owner = this;
  AplicarEfecto(this);
modal.ShowDialog();

}

private void AplicarEfecto(Window win)

{

  var objBlur = new System.Windows.Media.Effects.BlurEffect();

objBlur.Radius = 5;

win.Effect = objBlur;

} 

Y el resultado se vería de la forma:

Ahora bien, si quisiéramos recibir la respuesta de la ventana y una vez hecho esto, volver la Main Window a su estado original, ¿Cómo se haria?

– Obtener el resultado de la ventana y volver al estado original:

Sobre la ventana modal agregamos un botón “VOLVER” y sobre el evento click del mismo, implementamos el siguiente código:

private void VolverButtonClick(object sender, RoutedEventArgs e)

{
DialogResult = false;
Close();
}

Y sobre la ventana principal, implementamos el siguiente código:


private void AceptarButtonClick(object sender, RoutedEventArgs e)

{

            ConfirmacionVotoModal modal = new ConfirmacionVotoModal();

            modal.Owner = this;

            AplicarEfecto(this);

            modal.ShowDialog();

            bool?
resultado = modal.DialogResult;

            if (resultado != null)
{

                if (resultado == false)

                {

                    QuitarEfecto(this);

                }
}

}

private void AplicarEfecto(Window win)

{

    var objBlur = new System.Windows.Media.Effects.BlurEffect();

objBlur.Radius = 5;

win.Effect = objBlur;

}

private void QuitarEfecto(Window win)

{

    win.Effect = null;

}

Y listo, ya podremos recuperar el estado original de nuestra ventana principal.

Como bonus les dejo el XAML (me lo encontré en la WEB) para mejorar la apariencia de la ventana modal:

<Window x:Class=”ModalWPF.Modal” xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation&#8221;

        xmlns:x=http://schemas.microsoft.com/winfx/2006/xaml

        Title=”Modal”

        Height=”300″

        Width=”300″

        WindowStyle=”None”       

        ShowInTaskbar=”False”

        WindowStartupLocation=”CenterOwner”

        AllowsTransparency=”True”

        Background=”Transparent”>       

    <Border BorderBrush=”Gray”
 BorderThickness=”0,0,2,2″

 CornerRadius=”10″

 Background=”AliceBlue”>   

    <Border BorderBrush=”Transparent”
 BorderThickness=”5″

 CornerRadius=”10″>  

      <Border BorderBrush=”Black”
 BorderThickness=”1.5″

 CornerRadius=”10″>

        <Grid>

          <Label Content=”Hola soy la ventana modal 😀 “></Label>

          <Button Click=”VolverButtonClick”

                  Content=”Volver”

                  Height=”60″

                  VerticalAlignment=”Bottom”

                  Margin=”5″></Button>

        </Grid>

      </Border>

    </Border>

  </Border>

</Window>

El resultado se vería como el siguiente:

Bien, con esto llegamos al final.

Espero les sea de utilidad.

Descarga el ejemplo.

Hasta el próximo post.

[How To] Ventanas Modales en WPF

[How To] Implementar el corrector ortográfico de Word en nuestra aplicación

Hola, revisando un desarrollo que tenia de hace unos meses recordé que en este había implementado una herramienta de revisión de ortografía, que hacía uso del componente de Microsoft Office Word, y estoy seguro que a mas de uno le podría llegar a ser útil así que aquí les comparto este tutorial.

Lo primero que haremos será crear un proyecto de Windows Forms, sobre este agregamos un RichTextbox y un botón, así:

Una vez tenemos la interfaz grafica 😀 vamos a agregar una referencia a microsoft.Office.interop.Word:

Aparte agregamos un Using a System.Reflection e implementamos el siguiente método:

        private static string RevisarOrtografia(string texto)

        {

            var app = new Microsoft.Office.Interop.Word.Application();

            string corregido = string.Empty;

            if (!String.IsNullOrEmpty(texto))

            {

                app.Visible = false;

                object template = Missing.Value;

                object newTemplate = Missing.Value;

                object documentType = Missing.Value;

                object visible = false;

                Microsoft.Office.Interop.Word._Document doc1 = app.Documents.Add(ref template, ref newTemplate,

                                                                                 ref documentType, ref visible);

                doc1.Words.First.InsertBefore(texto);

                object optional = Missing.Value;

                doc1.CheckSpelling(

                    ref optional, ref optional, ref optional, ref optional, ref optional, ref optional,

                    ref optional, ref optional, ref optional, ref optional, ref optional, ref optional);

                object first = 0;

                object last = doc1.Characters.Count – 1;

                corregido = doc1.Range(ref first, ref last).Text;

            }

            object saveChanges = false;

            object originalFormat = Missing.Value;

            object routeDocument = Missing.Value;

            app.Application.Quit(ref saveChanges, ref originalFormat, ref routeDocument);

            return corregido;

        }

Y lo podemos consumir desde el evento Click del Botón de la siguiente forma:

        private void CorregirClick(object sender, EventArgs e) 
        { 
            richTextBox1.Text = RevisarOrtografia(richTextBox1.Text); 
        } 

Y obtendremos el siguiente resultado:

Descarga el ejemplo

Espero les sea de utilidad.

Hasta el próximo post

[How To] Implementar el corrector ortográfico de Word en nuestra aplicación

[How To][Windows Forms] Acceder a elementos de una página web

Hola, desde hace ya bastante tiempo había prometido una entrada a manera de tutorial de cómo realizar operaciones sobre una página Web con el control Web Browser.

En este tutorial vamos a insertar texto en los textbox de credenciales de facebook y hacer click en el botón “Entrar”, para esto creamos una nueva aplicación de Windows Forms y agregamos un control de Web Browser, en su propiedad Url asignamos http://www.facebook.com/, agregamos un botón y en el evento click de este empezamos a trabajar.

Nuestro formulario luciría así:

Luego de esto con ayuda con algún inspector de HTML como las Herramientas de desarrollo de IE9, inspeccionamos los elementos necesarios para cumplir nuestro objetivo. *Para desplegar esta herramienta basta con pulsar F12 cuando este abierto.

Se vería de la siguiente forma:

Cuando ya conocemos los Id’s de los elementos HTML que usaremos, podemos trabajar nuestro código:

 HtmlElement txtUsuario = webBrowser1.Document.GetElementById("email");
 HtmlElement txtPassword = webBrowser1.Document.GetElementById("pass");
 HtmlElement boton = webBrowser1.Document.GetElementById("loginbutton");
 txtUsuario.SetAttribute("value", "NombreDelUsuario");
 txtPassword.SetAttribute("value", "clave");
 boton.InvokeMember("click");

Cosas a notar en este código, el uso de la propiedad Document que se encarga de obtener un objeto del tipo HtmlDocument. El uso del método GetElementById(string) que nos retorna un objeto del tipo HtmlElement
a partir del ID que pasemos como parámetro, si existen dos elementos con el mismo ID el método retorna el primero que encuentre. El uso de los métodos SetAttribute(string, string) que nos permite asignar un valor a una propiedad, y el uso de InvokeMember() que
nos permite ejecutar un método no expuesto en el elemento DOM.

Consideraciones:

Una consideración a tener en cuenta es el uso de frames de algunas páginas, nunca me había sucedido hasta que me hicieron una pregunta de este control y donde al ver la estructura HTML de la pagina, pues debía cambiar el código un poco, el objetivo era el mismo, localizar un textbox, un botón y hacer click sobre el mismo, solo que en esta ocasión la pagina usaba frames, mi solución fue la siguiente:

HtmlWindow htmlWindow = webBrowser1.Document.Window; 
HtmlElement txt = null; 
HtmlElement boton = null; 
foreach (HtmlWindow frameWindow in htmlWindow.Frames) 
{ 
      control = frameWindow.Document.GetElementById("textBox"); 
      boton = frameWindow.Document.GetElementById("boton"); 
      if (txt != null && boton != null) 
          break; 
} 
control.SetAttribute("value""valor"); 
boton.InvokeMember("click"); 

Bien, espero les sea de utilidad.

Hasta el próximo post.

[How To][Windows Forms] Acceder a elementos de una página web

Forms en paneles, adiós a los formularios MDI

ACTUALIZACIÓN IMPORTANTE!: En uno de los comentarios un lector me hizo caer en cuenta de un grave error, el código, como estaba, generaba siempre una nueva instancia del formulario y solo removíamos la referencia del contenedor… lo que significaba un uso excesivo de memoria. El articulo fue actualizado explicando está ultima implementación. Cualquier sugerencia adicional es siempre bienvenida.

Cuando necesitamos trabajar múltiples “vistas” dentro de nuestros formularios por lo general recurrimos al control TabControl, pero cuando necesitamos ya una funcionalidad es muy compleja o simplemente queremos separar estas funcionalidades en interfaces diferentes e integrarlas en un solo formulario, recurrimos a los formularios MDI, en estos podemos trabajar con formularios padres (contenedor) y sus formularios hijos (contenidos), personalmente no tengo nada en contra de estos, pero me parecen un tanto tediosos de trabajar y algo estrictos en su personalización.

Es por eso que en una ocasión, cuando necesitaba hacer un proyecto de ese estilo, me decidí por integrar otro método para acoplar formularios hijos en uno padre SIN QUE ESTE FUERA UN MDI CONTAINER, ni trabajar con los TabControls. Al ver la sección de contenedores en el ToolBox de Visual Studio vi el Panel, un control tan simple pero tan potente que a la final sería la mejor solución a mi problema, aquí les dejo como lo hice.

Una vez aclarados estos antecedentes, manos al teclado, lo primero que vamos a hacer es crear un proyecto de tipo Windows Forms, a este le vamos a agregar tres Forms (Padre, Hijo1, Hijo2):

Form Padre e hijos

Al formulario Padre, le vamos a agregar dos botones (btMostrarHijo1, btMostrarHijo2) y un panel (panelContenedor), así:

así luce el form padre

Y en los Forms hijos agregamos etiquetas grandes y distintivas (o lo que quieras agregar), así:

Así lucen los forms hijos

Ahora, en el lado que nos gusta (el lado del código) veamos cómo implementar estos formularios en el panel, en el evento click del botón btMostrarHijo1 agregamos el siguiente código:

private void btMostrarHijo1_Click(object sender, EventArgs e)
{
            if (this.panelContenedor.Controls.Count &amp;gt; 0)
                this.panelContenedor.Controls.RemoveAt(0);
            Hijo1 form = Application.OpenForms.OfType<Hijo1>().FirstOrDefault();
            Hijo1 hijo1 = form ?? new Hijo1();
            hijo1.FormBorderStyle = FormBorderStyle.None;
            hijo1.Dock = DockStyle.Fill;
            this.panelContenedor.Controls.Add(hijo1);
            this.panelContenedor.Tag = hijo1;
            hijo1.Show();
}

La explicación de cómo funciona este código es muy simple, primero preguntamos si existe algún control en el interior del panel, de ser verdadero lo eliminamos. Luego creamos una nueva instancia del formulario a agregar Luego preguntamos si existe algún formulario del tipo Hijo1 que ya este abierto, si existe usamos esa misma instancia, de lo contrario creamos una nueva y sobre este objeto reescribimos algunas de sus propiedades, TopLevel establece si el formulario debe mostrarse como ventana nivel superior, FormBorderStyle define el estilo de los bordes de nuestro formulario (en nuestro caso no queremos mostrarlos), Dock establece como se acoplara el control a su contenedor principal (en nuestro caso queremos que rellene todo el panel), Por ultimo lo agregamos al panel, establecemos la instancia como contenedor de datos de nuestro panel y lo mostramos, el resultado se verá así:

Y si tengo 50 botones en mi formulario padre, ¿tengo que escribir todo este código 50 veces? La respuesta es NO, por fortuna para nosotros podemos escribir un método que nos ahorre a un más el trabajo, en nuestro caso crearemos un método void (sin retorno de datos) que recibirá como parámetro la instancia del  Formulario a mostrar, así:

private void AddFormInPanel(Form fh)
{
            if (this.panelContenedor.Controls.Count &amp;gt; 0)
                this.panelContenedor.Controls.RemoveAt(0);            
            fh.TopLevel = false;
            fh.FormBorderStyle = FormBorderStyle.None;
            fh.Dock = DockStyle.Fill;
            this.panelContenedor.Controls.Add(fh);
            this.panelContenedor.Tag = fh;
            fh.Show();
} 

Y para acudir a este, desde el evento click del botón lo hacemos así:

private void btMostrarHijo2_Click(object sender, EventArgs e)
{            
        var form = Application.OpenForms.OfType<Hijo2>().FirstOrDefault();
        Hijo2 hijo = form ?? new Hijo2();
        AddFormInPanel(hijo);
}

De esta forma es mucho más sencillo y agradable el trabajar con formularios contenidos dentro de otros.

Descarga el ejemplo

Bien espero les sea de utilidad en sus proyectos.

Hasta el próximo post.

Forms en paneles, adiós a los formularios MDI