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
, noUnload
: 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.