excel vba user-interface userform

excel - ¿Existen desventajas al poner código en Userforms en lugar de módulos?



vba user-interface (1)

Descargo de responsabilidad Escribí el rubberduckvba.wordpress.com/2017/10/25/userform1-show Victor K se unió . Soy dueño de ese blog y administro el proyecto de complemento VBIDE de código abierto para el que es.

Ninguna de sus alternativas es ideal. Volver a lo básico.

Para seleccionar diferentes filtros, tengo diferentes formas de usuario (sic).

Sus especificaciones exigen que el usuario necesite poder seleccionar diferentes filtros, y usted elige implementar una interfaz de usuario para ello mediante un UserForm . Hasta ahora, todo bien ... y todo es cuesta abajo desde allí.

Hacer que el formulario sea responsable de cualquier otra cosa que no sean problemas de presentación es un error común, y tiene un nombre: es el patrón Smart UI [anti], y el problema es que no se escala . Es excelente para la creación de prototipos (es decir, hacer una cosa rápida que "funcione" - tenga en cuenta las citas de miedo), no tanto para cualquier cosa que deba mantenerse durante años.

Probablemente haya visto estos formularios, con 160 controles, 217 controladores de eventos y 3 procedimientos privados que se cierran en 2000 líneas de código cada uno: así de mal se escala la interfaz de usuario inteligente , y es el único resultado posible en ese camino.

Verá, un UserForm es un módulo de clase: define el plano de un objeto . Los objetos generalmente quieren ser instanciados , pero alguien tuvo la genial idea de otorgar a todas las instancias de MSForms.UserForm una ID predeclarada , que en términos COM significa que básicamente obtienes un objeto global de forma gratuita.

¡Excelente! ¿No? No.

UserForm1.Show decisionInput1 = UserForm1.decision If decisionInput1 Then UserForm2.Show Else UserForm3.Show End If

¿Qué sucede si UserForm1 es "X''d-out"? ¿O si UserForm1 es Unload ed? Si el formulario no maneja su evento QueryClose , el objeto se destruye, pero debido a que esa es la instancia predeterminada , VBA crea automáticamente / silenciosamente uno nuevo para usted, justo antes de que su código lea UserForm1.decision , como resultado obtiene lo que sea El estado global inicial es para UserForm1.decision .

Si no se trataba de una instancia predeterminada , y QueryClose no se manejaba, acceder al miembro .decision de un objeto destruido le daría el clásico error 91 en tiempo de ejecución para acceder a una referencia de objeto nulo.

UserForm2.Show y UserForm3.Show hacen lo mismo: disparar y olvidar: pase lo que pase, y para averiguar exactamente en qué consiste, debe desenterrarlo en el código subyacente respectivo de los formularios.

En otras palabras, los formularios están ejecutando el programa . Son responsables de recopilar los datos, presentarlos, recopilar las aportaciones de los usuarios y hacer cualquier trabajo que se necesite hacer con ellos . Es por eso que se llama "Smart UI": la IU lo sabe todo.

Hay una mejor manera MSForms es el ancestro COM del marco de UI WinForms de .NET, y lo que el ancestro tiene en común con su sucesor .NET es que funciona particularmente bien con el famoso patrón Model-View-Presenter (MVP).

El modelo

Esa es tu información . Esencialmente, es lo que la lógica de su aplicación necesita saber fuera del formulario.

  • UserForm1.decision vamos con eso.

Agregue una nueva clase, FilterModel , por ejemplo, FilterModel . Debería ser una clase muy simple:

Option Explicit Private Type TModel SelectedFilter As String End Type Private this As TModel Public Property Get SelectedFilter() As String SelectedFilter = this.SelectedFilter End Property Public Property Let SelectedFilter(ByVal value As String) this.SelectedFilter = value End Property Public Function IsValid() As Boolean IsValid = this.SelectedFilter <> vbNullString End Function

Eso es realmente todo lo que necesitamos: una clase para encapsular los datos del formulario. La clase puede ser responsable de alguna lógica de validación, o lo que sea, pero no recopila los datos, no los presenta al usuario y tampoco los consume . Son los datos.

Aquí solo hay 1 propiedad, pero podría tener muchas más: piense en un campo en el formulario => una propiedad.

El modelo también es lo que el formulario necesita saber de la lógica de la aplicación. Por ejemplo, si el formulario necesita un menú desplegable que muestre varias selecciones posibles, el modelo sería el objeto que las exponga.

La vista

Esa es tu forma. Es responsable de conocer los controles, escribir y leer del modelo , y ... eso es todo. Estamos viendo un diálogo aquí: lo presentamos, el usuario lo llena, lo cierra y el programa actúa sobre él: el formulario en sí no hace nada con los datos que recopila. El modelo podría validarlo, el formulario podría decidir deshabilitar su botón Aceptar hasta que el modelo diga que sus datos son válidos y que están listos, pero bajo ninguna circunstancia un UserForm lee o escribe desde una hoja de trabajo, una base de datos, un archivo, una URL, o algo.

El código subyacente del formulario es muy simple: conecta la interfaz de usuario con la instancia del modelo y activa / desactiva sus botones según sea necesario.

Las cosas importantes para recordar:

  • Hide , no Unload : la vista es un objeto y los objetos no se autodestruyen.
  • NUNCA consulte la instancia predeterminada del formulario.
  • Siempre maneje QueryClose , nuevamente, para evitar un objeto autodestructivo ("X-out" del formulario destruiría la instancia).

En este caso, el código subyacente podría verse así:

Option Explicit Private Type TView Model As FilterModel IsCancelled As Boolean End Type Private this As TView Public Property Get Model() As FilterModel Set Model = this.Model End Property Public Property Set Model(ByVal value As FilterModel) Set this.Model = value Validate End Property Public Property Get IsCancelled() As Boolean IsCancelled = this.IsCancelled End Property Private Sub TextBox1_Change() this.Model.SelectedFilter = TextBox1.Text Validate End Sub Private Sub OkButton_Click() Me.Hide End Sub Private Sub Validate() OkButton.Enabled = this.Model.IsValid End Sub Private Sub CancelButton_Click() OnCancel End Sub Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer) If CloseMode = VbQueryClose.vbFormControlMenu Then Cancel = True OnCancel End If End Sub Private Sub OnCancel() this.IsCancelled = True Me.Hide End Sub

Eso es literalmente todo lo que hace la forma. No es responsable de saber de dónde provienen los datos o qué hacer con ellos .

El presentador

Ese es el objeto "pegamento" que conecta los puntos.

Option Explicit Public Sub DoSomething() Dim m As FilterModel Set m = New FilterModel With New FilterForm Set .Model = m ''set the model .Show ''display the dialog If Not .IsCancelled Then ''how was it closed? ''consume the data Debug.Print m.SelectedFilter End If End With End Sub

Si los datos en el modelo debían provenir de una base de datos, o alguna hoja de trabajo, utiliza una instancia de clase (sí, ¡ otro objeto!) Que es responsable de hacer exactamente eso.

El código de llamada podría ser el controlador de clics de su botón ActiveX, activar el presentador y llamar a su método DoSomething .

Esto no es todo lo que hay que saber sobre OOP en VBA (ni siquiera mencioné interfaces, polimorfismo, trozos de prueba y pruebas de unidad), pero si desea un código objetivamente escalable, querrá ir por el agujero del conejo MVP y explore las posibilidades que el código verdaderamente orientado a objetos trae a VBA.

TL; DR:

El código ("lógica empresarial") simplemente no pertenece al código subyacente de los formularios, en ninguna base de código que signifique escalar y mantenerse durante varios años.

En la "variante 1", el código es difícil de seguir porque está saltando entre los módulos y las preocupaciones de la presentación se mezclan con la lógica de la aplicación: no es el trabajo del formulario saber qué otro formulario mostrar se presionó el botón A o el botón B. En su lugar, debe permitirle al presentador saber qué quiere hacer el usuario y actuar en consecuencia.

En la "variante 2", el código es difícil de seguir porque todo está oculto en el código subyacente de las formas de usuario: no sabemos cuál es la lógica de la aplicación a menos que profundicemos en ese código, que ahora mezcla intencionalmente las preocupaciones de presentación y lógica empresarial. Eso es exactamente lo que hace el antipatrón "Smart UI".

En otras palabras, la variante 1 es ligeramente mejor que la variante 2, porque al menos la lógica no está en el código subyacente, pero sigue siendo una "IU inteligente" porque está ejecutando el programa en lugar de decirle a la persona que llama lo que está sucediendo .

En ambos casos, la codificación contra las instancias predeterminadas de los formularios es perjudicial, ya que pone el estado en un alcance global (cualquiera puede acceder a las instancias predeterminadas y hacer cualquier cosa en su estado, desde cualquier parte del código).

Trata las formas como los objetos que son: ¡ejemplifícalas!

En ambos casos, debido a que el código del formulario está estrechamente relacionado con la lógica de la aplicación y entrelazado con las preocupaciones de presentación, es completamente imposible escribir una sola prueba unitaria que cubra incluso un solo aspecto de lo que está sucediendo. Con el patrón MVP, puede desacoplar completamente los componentes, abstraerlos detrás de las interfaces, aislar responsabilidades y escribir docenas de pruebas unitarias automatizadas que cubren cada pieza de funcionalidad y documentan exactamente cuáles son las especificaciones, sin escribir un solo bit de documentación: El código se convierte en su propia documentación .

¿Hay desventajas en poner código en un formulario de usuario de VBA en lugar de en un módulo "normal"?

Esta podría ser una pregunta simple, pero no he encontrado una respuesta concluyente al buscar en la web y stackoverflow.

Antecedentes: estoy desarrollando una aplicación front-end de una base de datos en Excel-VBA. Para seleccionar diferentes filtros tengo diferentes formas de usuario. Pregunto qué diseño de programa general es mejor: (1) poner la estructura de control en un módulo separado O (2) poner el código para la siguiente forma de usuario o acción en la forma de usuario .

Hagamos un ejemplo. Tengo un botón Active-X que activa mis filtros y mis formularios.

Variante1: Módulos

En el CommandButton:

Private Sub CommandButton1_Click() call UserInterfaceControlModule End Sub

En el modulo:

Sub UserInterfaceControllModule() Dim decisionInput1 As Boolean Dim decisionInput2 As Boolean UserForm1.Show decisionInput1 = UserForm1.decision If decisionInput1 Then UserForm2.Show Else UserForm3.Show End If End Sub

En la variante 1, la estructura de control está en un módulo normal. Y las decisiones sobre qué formulario de usuario mostrar a continuación se separan del formulario de usuario. Cualquier información necesaria para decidir qué formulario de usuario mostrar a continuación debe extraerse del formulario de usuario.

Variante2: Forma de usuario

En el CommadButton:

Private Sub CommandButton1_Click() UserForm1.Show End Sub

En Userform1:

Private Sub ToUserform2_Click() UserForm2.Show UserForm1.Hide End Sub Private Sub UserForm_Click() UserForm2.Show UserForm1.Hide End Sub

En la Variante 2, la estructura de control está directamente en los formularios de usuario y cada formulario de usuario tiene las instrucciones sobre lo que viene después.

He comenzado el desarrollo utilizando el método 2. Si esto fue un error y hay algunos inconvenientes serios para este método, quiero saberlo más pronto que tarde.