domingo, 14 de febrero de 2010

C# - [WinForms] Instancia Única de Formulario

 

Introducción


La manipulación de las instancias de un formulario en algunas circunstancias puede no ser tan simple como aparenta.

Note en algunas oportunidades la necesidad que solo tener una única instancia, además de poder controlar las acciones o eventos que se ejecuten sobre este formulario, pero desde quien  realiza la apertura.

 

Ejemplo sobre Formularios  Simples


Para comenzar a ver este tema lo haremos en un primer ejemplo simple con formularios comunes.

Para esto tendremos un formulario que podrá denominarse Principal, el cual tiene dos botones, uno hará uso de instancias únicas, mientras que el segundo creara muchas instancias de ese formulario

 

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private Form2 form = null;

    private Form2 FormInstance
    {
        get
        {
            if (form == null)
            {
                form = new Form2();
                form.Disposed += new EventHandler(form_Disposed);
            }

            return form;
        }
    }

    void form_Disposed(object sender, EventArgs e)
    {
        form = null;
    }


    private void btnAbrirUnicaInstanciaForm_Click(object sender, EventArgs e)
    {
        Form2 frm = this.FormInstance;
         
        frm.Text = "Unica Instancia";
        frm.Show();

        // si el Formulario estaba abierto seguramente este en segundo plano
        // con esta linea hacemos que pase adelante
        frm.BringToFront();
    }

    private void btnAbrirInstanciaMultiplesForm_Click(object sender, EventArgs e)
    {
        Form2 frm = new Form2();

        frm.Text = "Instancia Multiple";
        frm.Show();
    }
}

El punto importante en este código y que marca la diferencia es el uso de la propiedad de nombre “FormInstance” esta propiedad permitirá aplicar la lógica que valide si ya existe una instancia creada para el formulario.

Si se analiza el código de la propiedad se notara que hace uso de una propiedad privada global al formulario, la idea es siempre crear y acceder al formulario a través de la propiedad y nunca hacer uso de la variable privada.

Es la propiedad la que provee del encapsulamiento de la lógica para determinar la reutilización de la instancia del formulario.

Hay un detalle adicional que por ahí noten como extraño, dentro de la propiedad al crear la instancia se adiciona el handler de un evento “Disposed”, seguramente se preguntaran cual es el objetivo de este evento.

Cuando se crea la instancia del formulario la variable privada contiene dicha instancia, pero al cerrarse el formulario la variable seguirá manteniendo el puntero cuando ya no sea una referencia valida, pues el formulario ha sido destruido, el tema es que nunca se reflejo esto en la variable privada. El uso del evento informa al formulario que realiza la apertura de la destrucción al cierre del formulario para que pueda quitar la referencia de la variable, en este caso simplemente igualándola a null.

Es importante también notar la diferencia en como se crea una instancia que es única, y cuando es múltiple, cuando se permiten muchas instancias solo basta con hacer uso del operador new cada vez que se quiere una nueva.

 

Formularios MDI


Como segundo ejemplo veremos como hacer esto mismo pero dentro de un formularios MDI, al cual le hemos agregado algunas otras tareas.

Básicamente la técnica es la misma, una propiedad en el formulario que valida la existencia de una instancia del formulario, pero en este caso en particular se han agregado algunos eventos adicionales que se querían trabajar desde el formulario que realiza la apertura.

Como se ver se hace uso de los eventos Load y FormClosed para poder informar en un StatusTrip de estas acciones.

 

public partial class frmPrincipalMDI : Form
{
    public frmPrincipalMDI()
    {
        InitializeComponent();
    }

    private frmSecundario form = null;

    private frmSecundario FormInstance
    {
        get
        {
            if (form == null)
            {
                form = new frmSecundario();
                form.MdiParent = this;

                form.Disposed += new EventHandler(form_Disposed);
                form.FormClosed += new FormClosedEventHandler(form_FormClosed);
                form.Load += new EventHandler(form_Load); 
                
            }

            return form;
        }
    }

    void form_Load(object sender, EventArgs e)
    {
        tslMensaje.Text = "Formulario abierto";
    }

    void form_FormClosed(object sender, FormClosedEventArgs e)
    {
        tslMensaje.Text = "Se ha cerrado el Formulario";
    }

    void form_Disposed(object sender, EventArgs e)
    {
        form = null;
    }


    private void instanciaUnicaToolStripMenuItem_Click(object sender, EventArgs e)
    {
        frmSecundario frm = this.FormInstance;

        // se varifica si el formulario no esta minimizado, en caso de estarlo
        // se lo cambia a un estado normal
        if (frm.WindowState == FormWindowState.Minimized)
            frm.WindowState = FormWindowState.Normal; 

        frm.Show(); 
    }

    private void instanciaMultipleToolStripMenuItem_Click(object sender, EventArgs e)
    {
        frmSecundario frm = new frmSecundario();
        frm.MdiParent = this;

        frm.Show();
    }
}

[C#]
[VB.NET]

26 comentarios:

  1. Leandro estoy haciendo una aplicacion MDI en la cual ya tengo varios forms en C#. Estuve viendo el ejemplo y esta muy bueno pero por lo que veo funciona solo para un formulario... Mo me doy cuenta como tendria que hacer para manejar los N formularios que pueda tener en mi aplicacion. Es decir para cada vez que hago clik en el menu de cada formulario me compruebe si esta abierto o no.
    Si me podrias orientar te lo agradeceria mucho. Desde ya Gracias

    ResponderEliminar
  2. Antes que nada te felicito por el aporte, es muy necesario y no hay mucha documentación útil al respecto.

    Lo que me gustaría es avanzar un pasito más y poder crear la propiedad para cualquier form y no para uno solo en particular. Lo intenté pero no sé como hacer en la línea "form = new Form1()" ya que no sé si se puede pasar como parámetro la clase del formulario que me interesa instanciar.

    Te agradecería si le podés pegar un vistazo a mi problema.

    ResponderEliminar
  3. hola Federico

    Venia con la idea desde hace esta operacion de instancias unicas del formulario algo mas generica

    quizas algo como muestra la segunda parte de este articulo:

    A Generic Singleton Form Provider for C#

    Pero mi idea no era usando algo externo, si con generics, pero para que heredaran los formularios directamente y usaran la funcionalidad

    Igualmente lo propuesto por el articulo esta muy bueno.

    saludos

    ResponderEliminar
  4. Muchas gracias Leandro, muy útil.

    ResponderEliminar
  5. Hola Leandro,

    me ha surgido el problema de que necesito abrir un único formulario pero con distintos parámetros. Una vez que esté abierto ya no se podrá abrir con otros parámetros, tendrá que cerrar y volver a abrir. ¿Alguna idea?.

    Saludos.

    ResponderEliminar
  6. Yo lo he solucionado así:

    // Recorremos los formularios para saber si ya está abierto.
    foreach (Form f in Application.OpenForms)
    {
    if (f is frmDietas)
    {
    miFrmDietas.Show();
    if (miFrmDietas.WindowState == FormWindowState.Minimized)
    miFrmDietas.WindowState = FormWindowState.Normal;
    miFrmDietas.BringToFront();
    return;
    }
    }

    pero me parece más efectivo y elegante como en tu ejemplo con una propiedad.

    ResponderEliminar
  7. Hola es la primera ves que subo algo a un blog/foro, siempre los uso para sacarme dudas y este seria un pequeño aporte.
    Me sirvió mucho este blog y quería agradecerles.
    Este Blog es viejo pero tal vez gente como yo está con este problema ahora o luego lo tendrán.
    tuve que hacer unos cambio para resolver lo mismo pasando como parámetro el formulario y así usarlo para todos los de mi aplicación.
    les paso el código.
    Esta función "ActivarForm" deja activo el formulario si es que ya se había abierto antes. y abajo dejo la función y la llamada desde un menu del mdi.
    private Boolean ActivarForm(Form Formulario)
    {

    foreach (Control control in this.Controls)
    {
    if (control.HasChildren)//porque el mdi es un contenedor.
    {
    foreach (Control controlChild in control.Controls)
    {
    if (controlChild.GetType() == Formulario.GetType())
    {
    if (((Form)controlChild).WindowState == FormWindowState.Minimized)
    {
    ((Form)controlChild).WindowState = FormWindowState.Normal;


    }
    ((Form)controlChild).BringToFront();
    return true;

    }
    }
    }

    }
    return false;
    }

    private void toolStripMenuItem3_Click(object sender, EventArgs e)
    {
    FrmMovimientosCaja childForm = new FrmMovimientosCaja();

    if (ActivarForm(childForm) == true)
    {
    childForm = null;
    }
    else
    {
    childForm.MdiParent = this;
    childForm.WindowState = FormWindowState.Maximized;
    childForm.Show();
    }

    }
    Espero que les sirva, saludos.

    ResponderEliminar
  8. leandro por favor si pusieras actualizar tu skydrive o volver a subir el codigo por favor gracias

    ResponderEliminar
  9. hola Johanbert

    hoy veo de actualizar los links de descarga

    saludos

    ResponderEliminar
  10. Hola Leandro,
    Estoy con una gran duda respecto al Evento Disposed para quitar el puntero cuando el formulario sea cerrado:
    Cuando se crea la instancia del formulario la variable privada contiene dicha instancia, pero al cerrarse el formulario la variable seguirá manteniendo el puntero cuando ya no sea una referencia valida, pues el formulario ha sido destruido, el tema es que nunca se reflejo esto en la variable privada.
    Lo que si no mal interpreto es liberar la memoria utilizada de la variable al crear el nuevo formulario y cuando esté sea cerrado poder liberarlo, verdad? para ello igualas a nothing en el delegado.
    Sin embargo he estado leyendo este artículo:
    http://msmvps.com/blogs/pmackay/archive/2007/03/28/liberacion.aspx
    Dónde indica el autor del blog que al referencia un objeto a nothing o null esté sera destruido o liberado cuando el GC(Garbac Colector) lo vea necesario. En este caso lo que se plantea para liberar memoria, es con el patron Dispose que se encargada de eliminar la memoria no utilizada de forma directa.
    No sé si estoy entendiendo bien los conceptos, o si estoy confundiendo lo que tu planteas en tu ejemplo. Espero puedas corregirme.
    Muchas gracias.

    ResponderEliminar
  11. hola Miguel

    el Dispos ese utiliza cuando necesitas detectar la destruccion de un objeto para liberar quizas algun otro que este este usando internamente

    imaginate creas una clase que dentro tiene la funcionalidad de comunicarse mediante puerto serie con algun dispositivo, entonces cuando este objeto se destruye tambien quieres cerrar la conexion serial y terminar ese objeto que usas internamente, bueno es alli donde usas el Dispose

    si es verdad que aunque iguales a null una variable esta no se destruye de forma automatica, sino que es el GC quien lo hace en realidad, pero eso quizas es para analizar muy puntualmente
    siempre puedes forzar la releccion usando el

    GC.Collect (Método)

    saludos

    ResponderEliminar
  12. Hola Leandro,
    Gracias por la respuesta, y gracias por aclararme el tema... y perdón la demora en contestar.
    Pero mira he estado leyendo en los foros de msdn y he visto en varios hilos en lo que tu y otros expertos hacen referencia a que solo se usa el Dispose() cuando se usan objetos que no son del .NetFramework, osea objetos no manejados, sin embargo, mi duda es, cuales exactamente son?... Puesto que me gustaría poder desarrollar un programa que no tenga problemas de quedarse colgado por no liberar la memoria, debido que en mi trabajo hay un sistema que se ha comprado hace ya un buen tiempo a una "Empresa", sin embargo el sistema a veces se cuelga, y aveces es muy lento al momento de realizar transacciones ABM, es por ello que asumo que el problema del programa que se adquirio es por la memoria no liberada.Por eso es que comenze a averiguar sobre el tema. Aunque claro talvez no sea solo sea problema de la memoria no liberada por la cual un programa puede quedarse colgado, en ese caso ¿Cuáles serían las causas por las que un software se cuelgue(hago referencia con la palabra cuelgue que el programa no responda y tenga que finalizarse con el Administrador de tareas y abrirlo nuevamente)?.
    Te estaré agradecido si podrías resolver estas dudas...
    Muchas gracias.

    ResponderEliminar
  13. hola Miguel

    pueden ser objeto no manejados o tambien pueden serlo

    podria en el disponse cerrar la conexion a una base de datos, o podrias destruir el objeto que se conecta a excel mediante las librerias COM de office
    estas librerias de office son no menejadas porque no son objetos .net

    las causas pueden ser muchas y no necesariamente por un problema de liberacion de memoria, quizas se quede en un ciclo recursivo, o quizas el timeout de conexion a la db es muy grande, etc las causas pueden ser variada
    si se tiene el codigo se podria analizar con alguna tool como ser:
    http://www.red-gate.com/products/dotnet-development/ants-performance-profiler/

    para ver que esta pasando cuando se congela en que funcionalidad sucede esto
    pero es algo que hay que analizar, no tiene porque pasar siempre

    saludos

    ResponderEliminar
  14. Muy buen aporte Leandro...
    solo una consulta... el ejemplo siver cuando se muestra un formulario hijo, que sucede cuando en el formulario hijo tengo redireccionar o mostrar otro formulario hijo?
    Ejemplo, el 1er formulario hijo es el listado de ciertos items, y si quiero ver en detalle un item, tengo que recurrir a un segundo formulario hijo..
    hay algun modo de hacer eso???
    espero puedas o puedan orientarme...
    De antemano Gracias...

    ResponderEliminar
  15. hola Hugo

    pero no entiendo necesitas tener algun comunicacion entre el primer form y un tercero que muestra el detalle de un item ?

    porque podrias simplemente hacer que el form que lo abre lo manupule para que necesitas tantos saltos entre formularios si no vas a operar o realizar alguna operacion entre estos

    o al menos no mencionas que se realice nada entre estos 3 formularios

    saludos

    ResponderEliminar
  16. Hola Leandro,
    estoy realizando una aplicacion MDI y necesito pasar un parametro del formulario padre al hijo con la instancia unica, como podria realizar algo asi?
    te agradeceria alguna ayuda.

    ResponderEliminar
  17. Hola Leandro, gracias por la aclaración, perdón por no contestar antes.
    Aprovechando este post, me preguntaba porque habrás dejado de publicar nuevos post, seria una pena que dejarás de hacerlo, ya que estoy seguro que tus tutoriales han ayudado a más que uno(me incluyo obviamente) y estoy muy agradecido contigo.
    Sin más espero que puedas seguir aportando con sus post, cuidese mucho.
    Un abrazo.

    ResponderEliminar
  18. hola Cristhian

    si en el form padre tienes la instancia podrias invocar alguna propiedad o metodo publico que definas en el form hijo

    [WinForms] – Pasaje de información formulario hijo

    saludos

    ResponderEliminar
  19. para los que no nos sale dispose(), podrias comentar como se implementa esta funcionalidad con GC.

    ResponderEliminar
  20. hola k999

    te refieres al Disposed como evento ?
    no entiendo porque le pones () en el articulo es un evento no un metodo

    saludo

    ResponderEliminar
  21. Leandro

    Antes que nada gracias por todos tus aportes, son de mucha utilidad, necesito algo de ayuda, como podría utilizar tu rutina para considerar N formularios.

    Saludos..

    ResponderEliminar
  22. hola Ed Garza

    podrias aplicar esta otra tecnica

    [WinForms] Verificar si el form esta abierto (instancia única)

    si es una validacion dinamica con linq seria mas simple de aplicar cuando lo necesites

    saludos

    ResponderEliminar
  23. hola Leandro tengo una consulta ocupo editar una fila de un datagridview en que cuando selecciono una fila le doy el boton editar me llama un formulario y me jala los datos pero el problema esta en que no me jala los datos en que van guardados en un combobox.
    Esto es lo que tengo


    Friend Sub pasarInformacion(dgv As DataGridView, pedido As Pedido)


    Me.dgv = dgv

    txtID.Enabled = False
    cmbNombrePerfume.Enabled = False
    cmbNombreVendedor.Enabled = False

    Dim perfume As New Perfume
    Dim vendedor As New Vendedor

    txtID.Text = pedido.Id
    cmbNombrePerfume.SelectedItem = perfume.Nombre
    ' cmbNombreVendedor.SelectedItem = vendedor.Nombre
    ' lblIdVendedor.Text = pedido.IdVendedor
    'lblIdPerfume.Text = pedido.IdPerfume
    txtCantidad.Text = pedido.Cantidad


    ' cmbNombreVendedor.SelectedValue = lblIdVendedor.Text
    ' cmbNombrePerfume.SelectedValue = lblIdPerfume.Text

    End Sub

    lo que esta comentado es lo que e intentado pero no me da

    ResponderEliminar
  24. hola Leandro!!!
    tengo un datagridview en el cual cargo algunos datos y al dar dobleclik sobre uno de ellos abre un nuevo FORM con sus datos, y al dar click en otro registro del datagrid abre el mismo FORM con datos y un nombre de ventana distintos, la duda es en como puedo hacer para que pueda abrir solo un FORM por cada registro que tenga en mi datagrid. Alguna idea de como pueda hacerlo??

    ResponderEliminar
  25. alguien me puede ayudar necesito sus ayuda porfavor

    ResponderEliminar
  26. Excelente aporte. Muchas gracias, me solucionaste ese pequeño problema de multiples Form abiertos.

    ResponderEliminar