Abrir y cerrar formularios con Visual Basic .net
Por Enrique Martínez Montejo
Última revisión: 15/05/2008
 

A simple vista, parece que abrir y cerrar un formulario o un cuadro de diálogo no debería de presentar ningún tipo de problema, y generalmente así es, pero debe saber también que a la hora de cerrar el formulario, existen ciertas diferencias sutiles que están en función con la manera en que aquel fue abierto.

Como probablemente conocerá, existen dos maneras de mostrar un formulario en una aplicación Windows Forms desarrollada con Visual Basic .net: de manera modal y no modal. Cada una de ellas dispone de su correspondiente método para llamar al formulario: el método ShowDialog (para mostrar un formulario de manera modal), y el método Show (para abrir los formularios no modales), cuestión esta que no sucede en Visual Basic 6.0, el cual utiliza el método Show con un valor adecuado para mostrar ambos tipos de formularios.

Los formularios modales se utilizan como cuadros de diálogo que se le presentan al usuario para que éste introduzca los datos que necesita la aplicación, o para mostrarle cierta información, y hay que cerrarlos expresamente para que el usuario pueda continuar utilizando la aplicación con total normalidad. Ejemplos de formularios modales sería el típico cuadro de diálogo Acerca de, el cuadro de mensaje utilizado por la clase MessageBox, los utilizados para seleccionar archivos, colores, fuentes, y generalmente aquellos que ofrezcan información importante al usuario.

A diferencia de los anteriores, los formularios no modales no necesitan que sean cerrados, por lo que el usuario puede alternar el foco de entrada entre el formulario principal y el formulario no modal. Ejemplos de formularios no modales serían los utilizados como formularios secundarios en aplicaciones MDI (Interfaz de Múltiples Documentos), los utilizados como cuadros de herramientas, o el clásico cuadro de diálogo Buscar existente en muchas aplicaciones.

Esa diferencia entre cerrar o no expresamente un formulario o un cuadro de diálogo para que el usuario pueda continuar con su trabajo, es lo que hace que el marco de trabajo de .net utilice dos métodos distintos para mostrar uno u otro tipo de formulario, métodos que también afectan al cierre y destrucción del formulario, porque mientras que los formularios no modales mostrados mediante el método Show se destruyen cuando son cerrados, no sucede lo mismo con los formularios modales llamados con el método ShowDialog, los cuales se ocultarán haciéndonos creer que se han cerrado y destruido.

Y de esto último es de lo que pretendo hablar en el presente artículo, para mostrarle al lector mediante unos escenarios específicos que podrá llevarlos a cabo paso a paso, la manera que tiene de actuar el marco de trabajo de .net con cada uno de los dos tipos de formularios que podemos mostrar en una aplicación Windows Forms. Para ello, primero comenzaré con los formularios no modales.

Escenario 1. Utilizar una variable objeto a nivel de clase para mostrar un formulario de manera no modal.

1. En el formulario de inicio, efectúe la siguiente declaración:

Private m_frm As Form2 = New Form2()

Con dicha declaración estamos creando una nueva instancia de la clase Form2, que referenciará al formulario de igual nombre existente en nuestro proyecto.

2. En el evento Click de un control Button, inserte las siguientes líneas de código:

m_frm.Show()
MessageBox.Show("Se ha mostrado el formulario.")

Cuando pulse el control Button, se mostrará el segundo formulario de manera no modal, lo que significa que el código seguirá su ejecución en las líneas siguientes a la llamada del método Show, de ahí que se muestre el cuadro de diálogo con el texto del mensaje especificado.

No es necesario llamar al método Show para mostrar un formulario no modal, porque idéntico resultado obtendría si estableciera el valor True a la propiedad Visible del formulario:

m_frm.Visible = True
MessageBox.Show("Se ha mostrado el formulario.")

Ello es posible por temas de herencia, ya que la clase Form hereda el método Show de la clase Control, y éste método es el encargado de establecer a True el valor de su propiedad Visible.

3. Cierre el segundo formulario y pulse de nuevo el control Button existente para mostrar nuevamente Form2.

Si no lo esperaba, lo mismo se estará preguntando por el motivo de haber obtenido una excepción del tipo ObjectDisposedExcepction, a pesar de que la instancia del formulario no tiene el valor Nothing. El motivo de ello, se encuentra en que al cerrarse el formulario no modal, el marco de trabajo de .NET ha llamado a su método Close, el cual, a su vez, se encarga de eliminar el identificador asociado al formulario. En realidad, desde el método Close lo que se hace es efectuar una llamada a la función SendMessage de la API de Windows, para ejecutar el mensaje WM_CLOSE, constante cuyo valor es &H10. Si desea usted mismo probarlo, ejecute el siguiente código fuente para cerrar el formulario actual:

Imports System.Runtime.InteropServices

<DllImport("user32.dll", CharSet:=CharSet.Auto)> _
Public Shared Function SendMessage(ByVal hWnd As HandleRef, _
                                   ByVal msg As Int32, _
                                   ByVal wParam As Int32, _
                                   ByVal lParam As Int32) As IntPtr
End Function

' Obtenemos el identificador del formulario actual.
'

Dim h As HandleRef = New HandleRef(Me, Me.Handle)

' Cerramos el formulario actual
'

Const WM_CLOSE As Int32 = &H10
SendMessage(h, WM_CLOSE, 0, 0)

Si desea cerrar un formulario no modal desde el formulario llamador utilizando la función SendMessage, ejecutaría lo siguiente:

' Obtenemos el identificador del formulario.
'

Dim h As HandleRef = New HandleRef(Me, m_frm.Handle)

' Cerramos el formulario no modal mostrado.
'

Const WM_CLOSE As Int32 = &H10
SendMessage(h, WM_CLOSE, 0, 0)

Al cerrarse el formulario no modal, el valor de la propiedad IsHandleCreated (que Form hereda de la clase Control) pasa a ser False cuando se ejecuta el evento Disposed del formulario, o cuando explícitamente se llame al método Dispose del propio formulario, por lo que éste ya no tendrá un identificador asociado.

Para no obtener la excepción ObjectDisposedExcepction cuando desee nuevamente mostrar el formulario, tendrá que verificar el valor de la propiedad IsHandleCreated, y si su valor es False, necesitará crear una nueva instancia del formulario:

If (m_frm Is Nothing) Then
    m_frm = New Form2()
ElseIf (Not m_frm.IsHandleCreated) Then
    m_frm = New Form2()
End If

Este código también se podría abreviar de la siguiente manera:

If ((m_frm Is Nothing) OrElse (Not m_frm.IsHandleCreated)) Then
    m_frm = New Form2()
End If

Pero siempre deberá verificar primero si el valor de la instancia es Nothing. Si no lo hace así, se está arriesgando a obtener una excepción del tipo NullReferenceExcepction con el clásico mensaje Referencia a objeto no establecida como instancia de un objeto.

En el ejemplo que estamos utilizando, es difícil que obtenga la citada excepción, ya que en la propia declaración de la variable objeto, se está creando una nueva instancia de Form2, pero puede que no suceda así en el código fuente de su proyecto, por lo que es una buena práctica de programación verificar si el valor de cualquier variable objeto es Nothing antes de utilizarla.

El orden en el que se desencadenan los eventos al cerrar el formulario no modal, o al llamar al método Close del formulario, que viene a ser lo mismo, sería el siguiente:

  1. FormClosing. Antes de que se cierre el formulario.
  2. FormClosed. Después de haberse cerrado el formulario.
  3. Disposed. Cuando el formulario se ha eliminado mediante una llamada a su método Dispose.

Si en cualquiera de los eventos mencionados hace una llamada a expresa al método Dispose del formulario, automáticamente dejará de tener un identificador asociado, es decir, el valor de la propiedad IsHandleCreated pasará a ser False. En este caso, la llamada al método Dispose hace que se desencadene inmediatamente el evento Disposed, volviéndose nuevamente a desencadenar tras el evento FormClosed.

Por ejemplo, si en el evento FormClosing ejecuta

Me.Dispose()

el orden de los eventos sería el siguiente:

  1. FormClosing
  2. Disposed
  3. FormClosed
  4. Disposed

Como puede observar, el evento Disposed se desencadena dos veces: la primera por la llamada expresa al método Dispose y la segunda por el orden natural de desencadenamiento de los eventos del formulario mostrado como no modal.

Pero si decide cerrar el formulario llamando a su método Dispose desde cualquier procedimiento o función existente en el mismo, el único suceso que se produciría sería el evento Disposed; no se ejecutaría ningún otro evento relacionado con el cierre del formulario.

Escenario 2. Utilizar una variable objeto a nivel de clase para mostrar un formulario de manera modal.

1. En el segundo formulario de su proyecto, inserte un control DataGridView y configúrelo adecuadamente para mostrar una serie de datos en el mismo cuando se haga Click en un control Button.

2. En el formulario de inicio, efectúe la siguiente declaración:

Private m_frm As Form2 = New Form2()

3. En el evento Click de un control Button, inserte las siguientes líneas de código:

m_frm.ShowDialog()
MessageBox.Show("Se ha cerrado el formulario.")

Cuando efectúe la llamada al método ShowDialog, el formulario se mostrará de manera modal, lo que significa que el código existente tras la llamada al método ShowDialog, no se ejecutará hasta que se cierre u oculte el formulario llamado.

4. Una vez que haya comprobado que el control DataGridView se ha llenado de datos, cierre el formulario y vuelva de nuevo a abrirlo.

Ahora, lo mismo se estará preguntando cual puede ser el motivo para que los datos continúen existiendo en el control DataGridView, y ello se debe a que el formulario no ha sido cerrado, ya que el marco de trabajo de .net no ha llamado a su método Close (recuerde que es aquel método que envía el mensaje WM_CLOSE con la función SendMessage para cerrar el formulario), por tanto, el formulario sigue teniendo un identificador asociado, ya que su propiedad IsHandleCreated tendrá el valor True hasta que se destruya.

A diferencia de lo que sucede cuando se muestra el formulario de manera no modal con el método Show, cuando se cierra un formulario modal, más que cerrar el formulario lo que en realidad se está haciendo es ocultarlo, de igual manera que si estableciéramos el valor False a su propiedad Visible, aunque si hiciera esto último, no se desencadenaría ningún evento de cierre. Como el formulario está oculto, puede mostrarlo nuevamente sin necesidad de crear una nueva instancia del mismo. Recuerde que esto último no se puede hacer si el formulario se muestra de manera no modal, porque obtendría la excepción ObjectDisposedExcepction.

Los únicos eventos que se desencadena cuando se cierra un formulario modal, bien porque se cierre desde el menú del sistema del formulario, se pulse el famoso control X existente en la esquina superior derecha, se especifique un valor adecuado a la propiedad DialogResult, o simplemente se llame a su método Close, son los siguientes:

  1. FormClosing. Antes de que se cierre el formulario.
  2. FormClosed. Después de haberse cerrado el formulario.

Observe que no se ejecuta el evento Disposed, y eso se debe a que no se ha llamado expresamente a su método Dispose, porque como he comentado anteriormente, el formulario se encuentra oculto, de ahí que podamos leer los valores de las propiedades de Form2 desde el formulario llamador, aún después de cerrar supuestamente el formulario modal. En este caso, al no encontrarse completamente cerrado el formulario, se necesita hacer una llamada al método Dispose para destruir el formulario una vez que no tengamos intención de utilizarlo más:

' Comprobamos si existe un identificador asociado.
'

If ((m_frm Is Nothing) OrElse (Not m_frm.IsHandleCreated)) Then
    m_frm = New Form2()
End If

' Llamamos al formulario de manera modal.
'
m_frm.ShowDialog()

MessageBox.Show("Se ha cerrado el formulario.")

' Destruimos el formulario.
'
m_frm.Dispose()

Tras llamar al método Dispose es cuando se desencadenará el evento Disposed, con lo cual se desencadenarían los mismos tres eventos de cierre que se desencadenan cuando cerramos un formulario no modal: FormClosing, FormClosed y Disposed. Si actúa de ésta manera, podrá comprobar que en sucesivas llamadas al formulario, ya no se visualizarán los datos en el control DataGridView; tendrá que pulsar de nuevo el control Button para rellenarlo.

Estos tres eventos se desencadenarán, y en el orden indicado, si cierra el formulario modal mediante una llamada a la función API SendMessage, tal y como se indicó anteriormente, con lo que no sería necesario efectuar una llamada expresa al método Dispose del formulario modal.

Escenario 3. Controlar desde un mismo formulario el cierre de otros formularios existentes en nuestro proyecto.

Una situación que se produce con bastante frecuencia, es llamar a distintos formularios modales y no modales desde un mismo formulario, sobre todo si estamos trabajando con formularios MDI secundarios. En estos casos sería aceptable controlar desde un mismo formulario (que sería el formulario llamador), aquellos eventos que creamos oportunos, en lugar de hacerlo en los módulos de clase de los respectivos formularios que vayamos a utilizar.

1. En el formulario de inicio, efectúe la siguiente declaración a nivel del módulo de clase del formulario, para que pueda acceder a la variable objeto declarada desde otros procedimientos existentes en el formulario:

Private m_frm As Form = Nothing

2. Cree los siguientes procedimientos en el formulario de inicio, los cuales se ejecutarán posteriormente en sustitución de los eventos Dispose, FormClosed y FormClosing. Por supuesto, deberá de escribir en ellos el código que desee ejecutar en cada caso:

Private Sub FrmDisposed(ByVal sender As Object, ByVal e As EventArgs)

    ' Obtenemos la referencia al objeto o formulario
    ' que ha provocado el evento.
    '

    Dim frm As Form = DirectCast(sender, Form)

    ' Se produce cuando el componente se elimina
    ' mediante una llamada al método Dispose. (Se hereda de Component)
    '

    MessageBox.Show("Evento Disposed")

End Sub

Private Sub FrmClosed(ByVal sender As Object, ByVal e As FormClosedEventArgs)

    ' Obtenemos la referencia al objeto o formulario
    ' que ha provocado el evento.
    '

    Dim frm As Form = DirectCast(sender, Form)

    ' Se produce después de haberse cerrado el formulario.
    '

    MessageBox.Show("Evento FormClosed")

    ' Si no se trata de un formulario MdiChild,
    ' llamamos a su método Dispose.
    '

    If ((frm IsNot Nothing) AndAlso _
        (Not frm.IsMdiChild)) Then frm.Dispose()

    ' Eliminamos la referencia al objeto
    '
    ' frm = Nothing


End Sub

Private Sub FrmClosing(ByVal sender As Object, ByVal e As FormClosingEventArgs)

    ' Obtenemos la referencia al objeto o formulario
    ' que ha provocado el evento.
    '

    Dim frm As Form = DirectCast(sender, Form)

    ' Se produce antes de cerrar el formulario.
    '

    MessageBox.Show("Evento FormClosing")

End Sub

Observe que en el procedimiento FormClosed se ejecutará la llamada al método Dispose de la instancia actual del formulario, siempre y cuando no se trate de un formulario MDI secundario, ya que éstos necesariamente se mostrarán de manera modal, de ésta manera, ya no es necesario llamar a dicho método tras cerrar (u ocultar) el formulario modal.

En cuanto a establecer o no el valor Nothing en el evento FormClosed, dicho valor no tendrá efecto alguno sobre la instancia del objeto referenciada, por lo que una vez ejecutado el evento, se puede seguir utilizando los métodos y propiedades de la instancia actual del objeto.

3. Cuando desee llamar de manera modal a un formulario concreto, ejecute lo siguiente:

' Creamos la instancia del formulario.
'

m_frm = New Form2()

' Añadimos los correspondientes procedimientos de evento
' que deseamos controlar.
'

AddHandler m_frm.Disposed, AddressOf FrmDisposed
AddHandler m_frm.FormClosed, AddressOf FrmClosed
AddHandler m_frm.FormClosing, AddressOf FrmClosing

' Lo mostramos de manera modal.
'

m_frm.ShowDialog(Me)

' Si lo creemos oportuno, leemos ciertas propiedades
' Public o Friend existentes en el formulario llamado.
'
If (m_frm.DialogResult = Windows.Forms.DialogResult.OK) Then
    MessageBox.Show(m_frm.Text, "Disposed=" & m_frm.IsDisposed.ToString)
End If

' Por último, destruimos el formulario modal.
'
m_frm.Dispose()

Si prefiere llamar a un formulario MDI secundario, efectúe la llamada de la siguiente manera:

' Mostramos de manera no modal un formulario MDI secundario
'

With m_frm
    .MdiParent = Me
    .Show()
End With

Como habrá tenido oportunidad de comprobarlo, hemos utilizado una variable objeto de la clase Form, para crear instancias específicas de un formulario concreto existente en nuestro proyecto. Asimismo, mediante la instrucción AddHandler hemos asociado tres eventos de la clase Form a sus controladores de evento correspondientes, de ésta manera, desde un único formulario podemos controlar el cierre de los formularios llamados, con lo cual estaremos a la misma vez reutilizando nuestro código fuente.

En fin, espero que tras la lectura y puesta en práctica de los ejemplos de este artículo, tenga un poco más claro lo que sucede cuando se abre y cierra un formulario de cualquier aplicación Windows Forms.

 

Otros enlaces de interés:

Índice de la colección de ejemplos de las clases del marco de trabajo de .NET


Enrique Martínez Montejo - 2008

NOTA: La información contenida en este artículo, así como el código fuente incluido en el mismo, se proporciona COMO ESTÁ, sin garantías de ninguna clase, y no otorga derecho alguno. Usted asume cualquier riesgo al poner en práctica, utilizar o ejecutar lo explicado, recomendado o sugerido en el presente artículo.

NOTE: The information contained in this article and source code included therein, is provided AS IS without warranty of any kind, and confers no rights. You assume any risk to implement, use or run it explained, recommended or suggested in this article.