Ci sono degli svantaggi nel mettere il codice in Userforms invece dei moduli?

Ci sono degli svantaggi nel mettere il codice in un Userform VBA invece che in un modulo “normale”?

Questa potrebbe essere una semplice domanda, ma non ho trovato una risposta definitiva durante la ricerca sul web e sullo stackoverflow.

Background: sto sviluppando un’applicazione Front-End di una base dati in Excel-VBA. Per selezionare diversi filtri ho diverse forms utente. Chiedo quale sia la migliore progettazione del programma generale: (1) ponendo la struttura di controllo in un modulo separato OPPURE (2) inserendo il codice per il prossimo userform o azione nel modulo utente .

Facciamo un esempio. Ho un pulsante Active-X che triggers i miei filtri e le mie forms.

Variante 1: moduli

Nel controllo CommandButton:

Private Sub CommandButton1_Click() call UserInterfaceControlModule End Sub 

Nel 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 

In Variante 1 la struttura di controllo si trova in un modulo normale. E le decisioni che userform mostrare dopo sono separate dal userform. Qualsiasi informazione necessaria per decidere su quale userform mostrare successivamente deve essere estratta dal modulo utente.

Variant2: Userform

Nel CommadButton:

 Private Sub CommandButton1_Click() UserForm1.Show End Sub 

In Userform1:

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

In Variant 2 la struttura di controllo è direttamente nelle Userform e l’userform ha le istruzioni che seguono.

Ho iniziato lo sviluppo usando il metodo 2. Se questo è stato un errore e ci sono alcuni inconvenienti gravi a questo metodo, voglio saperlo piuttosto prima che dopo.

Disclaimer Ho scritto l’ articolo a cui è stato collegato Victor K. Possiedo quel blog e gestisco il progetto di componente aggiuntivo VBIDE open-source per il quale è destinato.

Nessuna delle tue alternative è l’ideale. Ritorno alle basi.


Per selezionare diversi filtri ho differenti userforms (sic).

Le tue specifiche richiedono che l’utente debba essere in grado di selezionare diversi filtri e tu hai scelto di implementare un’interfaccia utente per esso utilizzando un UserForm . Fin qui, tutto bene … ed è tutto in discesa da lì.

Rendere la forma responsabile per qualcosa di diverso dai problemi di presentazione è un errore comune, e ha un nome: è il modello [anti-] Smart UI , e il problema è che non si scala . È fantastico per la prototipazione (ad esempio, crea una cosa rapida che “funziona”, osserva le virgolette), non tanto per tutto ciò che deve essere mantenuto per anni.

Probabilmente hai visto queste forms, con 160 controlli, 217 gestori di eventi e 3 procedure private che si chiudono su 2000 righe di codice ciascuna: è così che Smart UI scala, ed è l’unico risultato ansible su questa strada.

Vedete, un UserForm è un modulo di class: definisce il progetto di un object . Gli oggetti di solito vogliono essere istanziati , ma poi qualcuno ha avuto l’idea geniale di concedere tutte le istanze di MSForms.UserForm un ID predeterminato , che in termini COM significa sostanzialmente ottenere gratuitamente un object globale.

Grande! No? No.

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

Cosa succede se UserForm1 è “X’d-out”? O se UserForm1 è Unload ed? Se il modulo non sta gestendo il suo evento QueryClose , l’object viene distrutto, ma poiché questa è l’ istanza predefinita , VBA crea automaticamente / automaticamente una nuova per te, appena prima che il codice legga UserForm1.decision – di conseguenza ottieni qualunque lo stato globale iniziale è per UserForm1.decision .

Se non si trattava di un’istanza predefinita e QueryClose non veniva gestito, l’accesso al membro .decision di un object distrutto .decision l’errore 91 di runtime classico per accedere a un riferimento a un object nullo.

UserForm2.Show e UserForm3.Show fanno entrambi la stessa cosa: fire-and-forget – qualsiasi cosa accada, e per scoprire esattamente cosa consiste in questo, devi cercarlo nel rispettivo code-behind dei moduli.

In altre parole, le forms eseguono lo spettacolo . Sono responsabili della raccolta dei dati, della presentazione di tali dati, della raccolta di input da parte dell’utente e del lavoro svolto . Ecco perché si chiama “Smart UI”: l’interfaccia utente sa tutto.

C’è un modo migliore. MSForms è l’antenato COM del framework UI di WinForms di .NET e ciò che l’antenato ha in comune con il suo successore .NET, è che funziona particolarmente bene con il famoso pattern Model-View-Presenter (MVP).


Il modello

Questi sono i tuoi dati . Essenzialmente, è ciò che la logica dell’applicazione deve conoscere dal modulo.

  • UserForm1.decision andiamo con quello.

Aggiungi una nuova class, chiamala, per esempio, FilterModel . Dovrebbe essere una class molto semplice:

 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 

Questo è tutto ciò di cui abbiamo bisogno: una class per incapsulare i dati del modulo. La class può essere responsabile di alcune logiche di validazione, o qualsiasi altra cosa – ma non raccoglie i dati, non li presenta all’utente e non li consuma neanche. Sono i dati.

Qui c’è solo 1 proprietà, ma potresti averne molte di più: pensa a un campo sul form => una proprietà.

Il modello è anche ciò che il modulo deve conoscere dalla logica dell’applicazione. Ad esempio se il modulo ha bisogno di un menu a discesa che visualizza un numero di selezioni possibili, il modello sarebbe l’object che le espone.


La vista

Questa è la tua forma. È responsabile della conoscenza dei comandi, della scrittura e della lettura dal modello , e … tutto qui. Stiamo esaminando una finestra di dialogo qui: la raccogliamo, l’utente lo riempie, lo chiude e il programma agisce su di esso – il modulo stesso non fa nulla con i dati che raccoglie. Il modello potrebbe validarlo, il modulo potrebbe decidere di disabilitare il pulsante Ok fino a quando il modello dice che i suoi dati sono validi e validi, ma in nessun caso un UserForm legge o scrive da un foglio di lavoro, un database, un file, un URL, o niente.

Il code-behind del modulo è estremamente semplice: collega l’interfaccia utente con l’istanza del modello e triggers / distriggers i pulsanti in base alle necessità.

Le cose importanti da ricordare:

  • Hide , non Unload : la vista è un object e gli oggetti non si autodistruggono.
  • Non fare MAI riferimento all’istanza predefinita del modulo.
  • QueryClose sempre QueryClose , ancora una volta, per evitare un object autodistruggente (“X-out” del modulo altrimenti distruggerebbe l’istanza).

In questo caso il code-behind potrebbe apparire come questo:

 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 

Questo è letteralmente tutto ciò che fa la forma. Non è responsabile per sapere da dove provengono i dati o cosa fare con esso .


Il presentatore

Questo è l’object “colla” che collega i punti.

 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 

Se i dati nel modello dovevano provenire da un database o da un foglio di lavoro, utilizza un’istanza di class (sì, un altro object!) Che è responsabile di fare proprio questo.

Il codice chiamante potrebbe essere il gestore di clic del pulsante ActiveX, New up del presenter e il suo metodo DoSomething .


Questo non è tutto ciò che c’è da sapere su OOP in VBA (non ho nemmeno menzionato interfacce, polimorfismo, test di stub e test di unità), ma se vuoi codice oggettivamente scalabile, ti consigliamo di andare giù nel rabbitmq MVP ed esplorare le possibilità che il codice veramente orientato agli oggetti porta a VBA.


TL; DR:

Il codice (“logica aziendale”) semplicemente non appartiene al code-code delle forms, in qualsiasi base di codice che significa scalare e mantenere diversi anni.

Nella “variante 1” il codice è difficile da seguire perché stai saltando tra i moduli e le preoccupazioni di presentazione sono mescolate con la logica dell’applicazione: non è compito del modulo sapere quale altra forma mostrare il pulsante A o B premuto. Invece dovrebbe lasciare che il relatore sappia cosa l’utente intenda fare e agire di conseguenza.

Nella “variante 2” il codice è difficile da seguire perché tutto è nascosto nel code-behind degli userform: non sappiamo quale sia la logica dell’applicazione a meno che non scegliamo in quel codice, che ora mescola di proposito le preoccupazioni relative alla presentazione e alla logica aziendale. Questo è esattamente ciò che fa l’anti-pattern “Smart UI”.

In altre parole, la variante 1 è leggermente migliore della variante 2, perché almeno la logica non è nel code-behind, ma è ancora una “Smart UI” perché sta eseguendo lo show invece di dire al suo interlocutore cosa sta succedendo .

In entrambi i casi, la codifica contro le istanze predefinite dei moduli è dannosa, perché mette lo stato in ambito globale (chiunque può accedere alle istanze predefinite e fare qualsiasi cosa al suo stato, da qualsiasi parte nel codice).

Tratta le forms come gli oggetti che sono: istanziali!

In entrambi i casi, poiché il codice del modulo è strettamente associato alla logica dell’applicazione e intrecciato con i problemi di presentazione, è assolutamente imansible scrivere un test di una singola unità che copra anche un singolo aspetto di ciò che sta accadendo. Con il pattern MVP, è ansible disaccoppiare completamente i componenti, astrarli dietro interfacce, isolare le responsabilità e scrivere decine di test unitari automatizzati che coprono ogni singolo elemento di funzionalità e documentano esattamente quali sono le specifiche, senza scrivere un singolo frammento di documentazione: il codice diventa la propria documentazione .