C A PI TO LO 7 Automazione di un'applicazione tramite Visual Basic Perché non sono utilizzate le macro . . . . . . . . . . . . . . . . 335 Assistenza per l'immissione dei dati . . . . . . . . . . . . . . . . 337 Convalida di dati complessi . . . . . . . . . . . . . . . . . . . . . . . 356 Controllo dell'ordine di tabulazione su una maschera a più pagine . . . . . . . . . . . . . . . . . . . . . . . . . . . . 365 Collegamento ai dati correlati in un'altra maschera o report . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 382 Automazione di attività complesse . . . . . . . . . . . . . . . . . 390 Automazione dei report . . . . . . . . . . . . . . . . . . . . . . . . . . 399 Richiamo di macro per i dati con nome . . . . . . . . . . . . . 408 Automazione della selezione dei dati . . . . . . . . . . . . . . . 367 O ra che hai appreso i fondamenti dell'utilizzo di Microsoft Visual Basic, è tempo di mettere in pratica queste conoscenze. In questo capitolo imparerai a creare il codice Visual Basic necessario per automatizzare molte attività comuni. Nei database Conrad Systems Contacts, Housing Reservations e Wedding List puoi trovare decine di esempi di automazione. Ogni volta che, esplorando i database, vedi qualcosa d'interessante, apri la maschera o il report in visualizzazione Struttura, e osserva il codice Visual Basic della maschera o del report. Questo capitolo spiega in modo approfondito alcuni degli esempi più interessanti di questi database. Nota Puoi trovare il codice discusso in questo capitolo nelle applicazioni Conrad Systems Contacts (Contacts.accdb), Housing Reservations (Housing.accdb) e Wedding List (WeddingList.accdb). Perché non sono utilizzate le macro Anche se puoi utilizzarle per automatizzare le applicazioni, le macro hanno dei limiti. Come avrai notato esaminando l'elenco degli eventi disponibili nel capitolo 1, "Elaborazione degli eventi", molti eventi richiedono o restituiscono parametri che possono essere passati o letti da una routine Visual Basic, ma non da una macro. Come hai appreso nei capitoli 2, "Automazione di un'applicazione client tramite macro", e 3, "Automazione di un'applicazione Web tramite macro", le funzioni di debug per le macro non sono affidabili quanto quelle di Visual Basic. 335 336 Capitolo 7 Automazione di un'applicazione tramite Visual Basic Quando utilizzare le macro Utilizza le macro nell'applicazione in ognuna delle seguenti circostanze: Capitolo 7 ● Per gestire un database Web. ● L'applicazione è composta solo da poche maschere e da pochi report. ● Devi creare una semplice applicazione automatizzata utilizzando solo azioni macro attendibili, in modo che possa essere eseguita in un ambiente non attendibile. ● L'applicazione potrebbe essere utilizzata da utenti che non hanno familiarità con Visual Basic, che desiderano comprendere com'è costruita l'applicazione e possibilmente modificarla o arricchirla. ● Stai sviluppando un prototipo di applicazione e desideri automatizzare velocemente alcune funzionalità per far vedere la struttura del progetto. Una volta che conosci Visual Basic, automatizzare un'applicazione dimostrativa è molto semplice utilizzando le routine evento. ● Non hai bisogno di valutare o impostare i parametri passati da alcuni eventi, quali DopoConfermaEliminazione, Applicazionefiltro, PrimaDiConfermaEliminazione, Errore, Filtro, TastoGiù, TastoSu, PulsanteMouseGiù, MouseSpostato, PulsanteMouseSu, NonInElenco e Aggiornato. ● Non hai bisogno di aprire recordset o altri oggetti o di lavorare con essi. Quando utilizzare Visual Basic Anche se le macro dell'interfaccia utente possono essere utili quando lavori con le maschere Web, numerose attività non possono essere eseguite con le macro e altre possono essere implementate meglio per mezzo di una routine Visual Basic. Utilizza una routine Visual Basic invece di una macro in ognuna delle seguenti circostanze: ● Serve una gestione degli errori complessa nell'applicazione. ● Desideri definire una nuova funzione. ● Devi gestire alcuni eventi che passano parametri o accettano valori di ritorno (a parte l'evento di annullamento). ● Devi creare nuovi oggetti (tabelle, query, maschere o report) nel database, dal codice dell'applicazione. ● L'applicazione deve interagire con un altro programma basato su Windows tramite automazione ActiveX. 337 ● Desideri essere in grado di chiamare direttamente le funzioni Windows API (Application Programming Interface). ● Desideri definire codice applicativo comune a più applicazioni, in una libreria. ● Desideri essere in grado di aprire un recordset e di lavorare con i suoi dati, record per record. ● Devi utilizzare alcuni programmi di utilità nativi del sistema di gestione del database relazionale che gestisce le tue tabelle collegate (quali le routine Microsoft SQL Server o le funzionalità di definizione dei dati). ● Desideri le massime prestazioni nell'applicazione. Dato che sono compilati, durante l'esecuzione i moduli risultano leggermente più veloci delle macro. Probabilmente noterai la differenza solo sui processori più lenti. ● Stai creando un'applicazione complicata, della quale sarà difficile eseguire il debug. Assistenza per l'immissione dei dati Per garantire che l'utente dell'applicazione immetta dati corretti, puoi utilizzare macro per i dati e definire valori predefiniti, maschere di input e regole di convalida. Ma cosa puoi fare se i valori predefiniti provengono da una tabella correlata? Come puoi assistere un utente che deve immettere un valore che non si trova nell'origine riga di una casella combinata? Come puoi rendere più leggibile il testo visualizzato? Esiste un modo per semplificare all'utente l'acquisizione delle date e degli orari? E come puoi aiutare l'utente a modificare file immagine collegati? Puoi trovare le risposte a queste domande nelle sezioni seguenti. Immissione di dati correlati La tabella tblContactProducts nel database Conrad Systems Contacts (Contacts.accdb) contiene un campo SoldPrice che riporta il prezzo effettivo del prodotto al momento della vendita. La tabella tblProducts contiene un campo UnitPrice in cui è presente il prezzo di vendita normale del prodotto. Quando l'utente lavora nella maschera Contacts (frmContacts) e desidera vendere un nuovo prodotto, non vuoi che debba ricercare il prezzo corrente del prodotto prima di immetterlo nel record. Come probabilmente sai, puoi creare una maschera con sottomaschere nidificate a due livelli per modificare i contatti, la società predefinita per ciascun contatto e i prodotti venduti a quella società e registrati per il contatto corrente. Tuttavia, se apri frmContacts nel database di esempio Contacts.accdb e fai clic sulla scheda Products come mostrato nella figura 7-1, puoi notare che questa apparentemente non mostra alcun dato aziendale di collegamento tra contatti e prodotti venduti. La sottomaschera usata per visualizzare i prodotti di contatto non è nidificata all'interno di un'altra sottomaschera che mostra le società per il contatto attuale. Di nuovo, l'utente non dovrebbe cercare l'ID della società Capitolo 7 Assistenza per l'immissione dei dati 338 Capitolo 7 Automazione di un'applicazione tramite Visual Basic predefinita per il contatto corrente, prima di vendere un prodotto. Nota che nella figura 7-1 siamo passati al quarto record di contatto. Capitolo 7 Figura 7-1 La vendita di un prodotto a un contatto comporta l'inserimento del prezzo e della società predefinita. Una casella combinata nella sottomaschera (fsubContactProducts) aiuta l'utente a scegliere il prodotto da vendere. Parte del segreto per impostare automaticamente il prezzo (il campo SoldPrice nella tabella tblContactProducts) si trova nella query dell'origine riga per la casella combinata qlkpProductsForContacts, come mostrato nella figura 7-2. Figura 7-2 La query qlkpProductsForContacts è l'origine riga della casella combinata Product in fsubContactProducts. 339 Certamente ti serve il campo ProductID per il nuovo record nella tabella tblContactProducts. Visualizzare il campo ProductName nella casella combinata è più significativo che mostrare il valore di ProductID e, come puoi vedere nella figura 7-1, l'elenco della casella combinata mostra anche il campo CategoryDescription e l'indicazione se il prodotto è una versione di prova. Ma perché abbiamo incluso le colonne UnitPrice, TrialExpire e PreRequisite nella griglia di struttura della query? Come puoi intuire, per recuperare uno di questi campi dalla riga corrente della casella combinata, puoi fare riferimento alla proprietà Column della casella combinata. Più avanti in questo capitolo, nella sezione "Convalida di dati complessi" a pagina 356, vedrai che l'altro codice presente nella maschera utilizza i campi aggiuntivi per garantire che il contatto disponga già di eventuali prodotti indicati come prerequisiti. Puoi vedere la semplice riga di codice che copia il campo UnitPrice aprendo il modulo Visual Basic nella maschera fsubContactProducts. Passa al riquadro di spostamento, seleziona la maschera fsubContactProducts, fai clic con il pulsante destro del mouse sulla maschera, fai clic su Visualizzazione Struttura nel menu e infine fai clic sul pulsante Visualizza codice nel gruppo Strumenti della scheda Struttura. Nella finestra Codice di Visual Basic Editor (VBE) scorri finché non trovi la routine cmbProductID_AfterUpdate. Il codice è il seguente: Private Sub cmbProductID_AfterUpdate() ‘ Grab the default price from the hidden 5th column Me.SoldPrice = Me.cmbProductID.Column(4) End Sub Per prelevare la colonna desiderata è utilizzato un numero d'indice. L'indice parte da zero; puoi fare riferimento alla quinta colonna nella query (UnitPrice), chiedendo la proprietà Column(4) della casella combinata. Il codice utilizza l'oggetto Me per fare riferimento all'oggetto maschera dove il codice è in esecuzione. Pertanto, ogni volta che prelevi un prodotto differente si verifica l'evento DopoAggiornamento per la casella combinata ProductID, e questo codice assegna automaticamente un valore al prezzo correlato. Chiudi la maschera fSubContactProducts prima di procedere con la sezione successiva. Se apri la maschera frmContacts in visualizzazione Struttura, selezioni la maschera fsubContactProducts sulla scheda Products, ed esamini le proprietà Collega campi secondari e Collega campi master, puoi notare che le due maschere sono collegate sul campo ContactID. La tabella tblContactProducts tuttavia richiede anche il campo CompanyID nella sua chiave primaria. Il codice nel modulo per la maschera fsubContactProducts gestisce il prelevamento dell'ID della società predefinita per il contatto corrente, e quindi non occorre una sottomaschera intermediaria, che confonderebbe la struttura della maschera. Se hai ancora il modulo per la maschera fsubContactProducts aperto nella finestra VBE, puoi trovare il codice nella routine Form_BeforeInsert: Il codice è il seguente: Capitolo 7 Assistenza per l'immissione dei dati 340 Capitolo 7 Automazione di un'applicazione tramite Visual Basic Capitolo 7 Private Sub Form_BeforeInsert(Cancel As Integer) Dim varCompanyID As Variant ‘ First, disallow insert if nothing in outer form If IsNothing(Me.Parent.ContactID) Then MsgBox “You must define the contact information on a new row before “attempting to sell a product”, vbCritical, gstrAppTitle Cancel = True Exit Sub End If ‘ Try to lookup this contact’s Company ID varCompanyID = DLookup(“CompanyID”, “qryContactDefaultCompany”, _ “(ContactID = “ & Me.Parent.ContactID.Value & “)”) If IsNothing(varCompanyID) Then ‘ If not found, then disallow product sale MsgBox “You cannot sell a product to a Contact that does not have a “related Company that is marked as the default for this Contact.” “ Press Esc to clear your edits and click on the Companies tab “ “to define the default Company for this Contact.”, vbCritical, _ gstrAppTitle Cancel = True Else ‘ Assign the company ID behind the scenes Me.CompanyID = varCompanyID End If End Sub “ & _ “ & _ & _ & _ Questa routine viene eseguita quando l'utente imposta un valore su una nuova riga nella sottomaschera. Per prima cosa, la routine verifica che la maschera esterna contenga un ContactID valido. Fatto ciò, il codice utilizza la funzione di dominio DLookup per tentare di recuperare l'identificativo predefinito della società per il contatto attuale. La query include un filtro per ritornare solo le righe della tabella tblCompanyContacts per le quali il campo DefaultForContact è True. Se la funzione ritorna un valore valido, il codice imposta automaticamente il campo CompanyID richiesto. Se non riesce a trovare un CompanyID, il codice utilizza l'istruzione MsgBox per avvisare l'utente dell'errore. Nota La funzione IsNothing, che vedi utilizzata nel codice di tutte le applicazioni di esempio, non è una funzione Visual Basic predefinita. Questa funzione verifica se il valore che le viene passato contiene una stringa di lunghezza nulla o un valore Null o zero. Questa funzione è disponibile nel modulo modUtility standard di tutti i database di esempio. Assistenza per l'immissione dei dati L'utilità delle funzioni di dominio Abbastanza frequentemente nel codice, in una query oppure nell'origine controllo di una maschera o di un report, potresti dover cercare un singolo valore da una delle tabelle o delle query del database. Anche se è possibile definire e aprire un recordset nel codice, Access fornisce un insieme di funzioni, chiamate funzioni di dominio, che possono fornire il valore che ti serve con una singola chiamata di funzione. Le funzioni disponibili sono le seguenti: Nome di funzione Descrizione DFirst, DLast Ritorna un valore casuale dal dominio specificato (la tabel la o la query che è l'origine record) DLookup Cerca un valore nel dominio specificato DMax Ritorna il valore massimo (Max) nel dominio specificato DMin Ritorna il valore minimo (Min) nel dominio specificato DStDev, DstDevP Ritorna la deviazione standard di un campione della popo lazione o della popolazione del dominio specificato DSum Ritorna la somma di un'espressione da un dominio DVar, DVarP Ritorna la varianza di un campione della popolazione o della popolazione del dominio specificato La sintassi per chiamare una funzione di dominio è la seguente: <function name>(<field expression>, <domain name> > [, <criteria> > ]) dove <nome funzione> è il nome di una delle funzioni elencate in precedenza <espressione campo> è un letterale o il nome di una variabile di tipo stringa contenente il nome di un campo o un'espressione che utilizza i campi del dominio specificato <nome dominio> è un letterale o il nome di una variabile di tipo stringa contenente il nome di una tabella o di una query nel database <criteri> è un letterale o il nome di una variabile di tipo stringa contenente un'espressione di confronto booleana usata per filtrare i record nel dominio Quando una funzione di dominio non trova alcun record, il valore ritornato è Null; dovresti dunque assegnare sempre il risultato a una variabile di tipo Variant. Quando costruisci un'espressione per i criteri, devi racchiudere i letterali di tipo stringa tra virgolette e i letterali di tipo data/ora tra i caratteri #. Se utilizzi le doppie virgolette per delimitare la stringa dei criteri, usa le virgolette semplici per i letterali all'interno della stringa e viceversa. Per trovare il valore minimo tra i codici postali, per i contatti il cui tipo è customer e la data di nascita è anteriore al primo gennaio 1970, digita DMin("WorkPostalCode", "tblContacts", "[ContactType] = 'customer' And Format([BirthDate], 'mm/dd/yyyy') < #01/01/1970#") Capitolo 7 APPROFONDIMENTO AP PPROFONDIMENTO 341 342 Capitolo 7 Automazione di un'applicazione tramite Visual Basic Gestione dell'evento NonInElenco Capitolo 7 In quasi tutte le maschere di immissione dati, devi offrire all'utente un modo per impostare la chiave esterna del record modificato sul lato molti di una relazione, per puntare al record corretto del lato uno, ad esempio per impostare il campo ProductID della tabella tblContactProducts, quando viene venduto un prodotto nella scheda Products della maschera frmContacts. Ma cosa succede se l'utente deve creare un nuovo prodotto? L'utente deve prima aprire la maschera per la modifica dei prodotti, per creare il nuovo prodotto, prima di venderlo? La risposta è assolutamente no; devi comporre il codice nell'evento NonInElenco della casella combinata, per gestire i nuovi valori e fornire un modo per creare nuove righe nella tabella tblProducts. La figura 7-3 mostra cosa accade quando l'utente cerca di digitare il nome di un prodotto che non è già presente nella tabella tblProducts. In questo caso il cliente desidera acquistare un prodotto con supporto di due anni invece del prodotto con supporto di un anno, già disponibile. Puoi vedere che qualcosa ha intercettato il nuovo nome di prodotto, per confermare l'aggiunta del nuovo prodotto da parte dell'utente. Figura 7-3 Quando immetti un prodotto che non è definito nel database, l'applicazione chiede se desideri aggiungere il nuovo prodotto. Per iniziare, la casella combinata è stata definita con la proprietà Solo in elenco impostata su Sì. Quindi è stata definita una routine evento per gestire l'evento NotInList della casella combinata; è il codice di questo evento che chiede all'utente se desidera aggiungere un prodotto. Se l'utente fa clic su Sì per confermare l'aggiunta del prodotto, la routine evento apre la maschera frmProductAdd in modalità Dialog, per consentire all'utente di immettere i nuovi dati, come mostrato nella figura 7-4. L'apertura di una maschera in modalità Dialog forza l'utente a rispondere prima che l'applicazione riprenda l'esecuzione. Il codice che apre questa maschera passa il nome del prodotto immesso e il tipo del prodotto selezionato 343 dall'utente, prima di immettere un nuovo nome di prodotto. L'utente può compilare il prezzo e altri dettagli. Se l'utente fa clic su Cancel il record non viene salvato e la maschera viene chiusa. Se l'utente fa clic su Save, il record del nuovo prodotto viene salvato e la maschera viene chiusa per consentire al codice della routine evento NonInElenco di proseguire. Figura 7-4 La maschera frmProductAdd ti permette di definire i dettagli del nuovo prodotto. Per vedere come funziona, apri la maschera fsubContactProducts nella visualizzazione Struttura, fai clic sulla casella combinata cmbProductID, trova la proprietà evento NonInElenco nella finestra delle proprietà e fai clic sul pulsante Genera per aprire il codice. Ti abbiamo fatto selezionare la casella combinata dalla finestra delle proprietà perché il controllo casella di testo ProductName si sovrappone al controllo casella combinata cmbProductID sulla griglia di progettazione della maschera. Il codice per la routine è il seguente. Private Sub cmbProductID_NotInList(NewData As String, Response As Integer) Dim strType As String, strWhere As String ‘ User has typed in a product name that doesn’t exist strType = NewData ‘ Set up the test predicate strWhere = “[ProductName] = “”” & strType & “””” ‘ Ask if they want to add this product If vbYes = MsgBox(“Product “ & NewData & “ is not defined. “ & _ “Do you want to add this Product?”, vbYesNo + vbQuestion + _ vbDefaultButton2, gstrAppTitle) Then ‘ Yup. Open the product add form and pass it the new name ‘ - and the pre-selected Category DoCmd.OpenForm “frmProductAdd”, DataMode:=acFormAdd, _ WindowMode:=acDialog, _ OpenArgs:=strType & “;” & Me.cmbCategoryDescription ‘ Verify that the product really got added If IsNull(DLookup(“ProductID”, “tblProducts”, strWhere)) Then ‘ Nope. MsgBox “You failed to add a Product that matched what you entered.” & _ “ Please try again.”, vbInformation, gstrAppTitle ‘ Tell Access to continue - we trapped the error Response = acDataErrContinue Else ‘ Product added OK - tell Access so that combo gets requeried Response = acDataErrAdded Capitolo 7 Assistenza per l'immissione dei dati 344 Capitolo 7 Automazione di un'applicazione tramite Visual Basic Capitolo 7 End If Else ‘ Don’t want to add - let Access display normal error Response = acDataErrDisplay End If End Sub Access passa due parametri alla routine evento NotInList. Il primo parametro, NewData, contiene la stringa che hai immesso nella casella combinata. Puoi impostare il valore del secondo parametro, Response, prima di uscire dalla subroutine per indicare ad Access ciò che desideri fare. Poiché non è possibile accedere a questi parametri in una macro, è evidente che per poter gestire correttamente l'evento è necessaria una routine Visual Basic. La routine crea la stringa dei criteri, usata per verificare che l'utente abbia salvato il prodotto, quindi utilizza la funzione MsgBox per chiedere se l'utente desidera aggiungere al database il prodotto visualizzato nella figura 7-3. Se hai mai letto l'argomento della Guida in linea relativo alla funzione MsgBox, sai che il secondo parametro è un numero che è la somma di tutte le opzioni desiderate. Fortunatamente Visual Basic fornisce costanti con nome per queste opzioni, e quindi non devi ricordare i codici numerici. In questo caso la routine richiede la visualizzazione di un'icona punto interrogativo (vbQuestion) e dei pulsanti Sì e No (vbYesNo); essa specifica inoltre che il pulsante predefinito è il secondo pulsante (vbDefaultButton2), il pulsante No, nel caso in cui l'utente prema velocemente Invio dopo aver visto il messaggio. Se l'utente fa clic su Sì nella finestra messaggio, la routine utilizza DoCmd.OpenForm per aprire la maschera frmProductAdd in modalità Dialog, e passa ad essa il nome del prodotto immesso e il tipo del prodotto selezionato, impostando la proprietà OpenArgs della maschera. Nota l'uso della sintassi con parametri con nome nella chiamata a DoCmd. OpenForm per semplificare l'impostazione dei parametri desiderati. Devi aprire la maschera in modalità Dialog. In caso contrario, il codice continua a essere eseguito durante l'apertura della maschera. Ogni volta che viene aperta una finestra di dialogo, l'esecuzione del codice Visual Basic si interrompe fino alla chiusura della finestra di dialogo, che in questo caso è critica poiché il record deve essere salvato o annullato prima di poter proseguire con altri test. Dopo la chiusura della maschera frmProductAdd, l'istruzione successiva chiama la funzione DLookup per verificare che il prodotto sia stato realmente aggiunto al database. Se il codice non riesce a trovare un nuovo nome di prodotto corrispondente (l'utente ha cambiato il nome del prodotto nella maschera di aggiunta oppure ha fatto clic su Cancel), utilizza l'istruzione MsgBox per informare l'utente del problema e imposta un valore di ritorno nel parametro Response, per indicare ad Access che il valore non è stato aggiunto, ma che Access può proseguire senza visualizzare il proprio messaggio di errore (acDataErrContinue). Se il nome del nuovo prodotto esiste (il che significa che l'utente ha fatto clic su Save nella maschera frmProductAdd), il codice indica ad Access che il nuovo prodotto ora esiste (acDataErrAdded). 345 Access interroga nuovamente la casella combinata e cerca una nuova corrispondenza. Infine, se l'utente fa clic su No nella finestra messaggio della figura 7-3, la routine imposta Response su acDataErrDisplay, per indicare ad Access di visualizzare il normale messaggio di errore. L'altra parte di codice critica è nell'evento Load per la maschera frmProductAdd: Il codice è il seguente: Private Sub Form_Load() Dim intI As Integer If Not IsNothing(Me.OpenArgs) Then ‘ If called from “not in list”, Openargs should have ‘ Product Name; Category Description ‘ Look for the semi-colon separating the two intI = InStr(Me.OpenArgs, “;”) ‘ If not found, then all we have is a product name If intI = 0 Then Me.ProductName = Me.OpenArgs Else ‘ If called from fsubContactProducts, ‘ .. have category only If intI > 1 Then ‘ Have a product name - grab it Me.ProductName = Left(Me.OpenArgs, intI - 1) End If Me.CategoryDescription = Mid(Me.OpenArgs, intI + 1) ‘ lock the category Me.CategoryDescription.Locked = True Me.CategoryDescription.Enabled = False ‘ .. and clear the tool tip Me.CategoryDescription.ControlTipText = “” End If End If End Sub Come ricorderai, la routine evento NotInList della casella combinata cmbProductID passa al metodo OpenForm la stringa originale immessa dall'utente e il tipo di prodotto selezionato (il campo CategoryDescription) come parametro OpenArgs. Viene così impostata la proprietà OpenArgs della maschera che viene aperta; la proprietà OpenArgs dovrebbe contenere il nome del nuovo prodotto, un punto e virgola e il tipo del prodotto selezionato. La routine Form_Load analizza il nome e il tipo del prodotto per mezzo della funzione InStr, per cercare il punto e virgola (la funzione InStr ritorna l'offset nella stringa specificata nel primo parametro, corrispondente al punto nel quale si trova la stringa specificata nel secondo parametro; ritorna 0, se non trova la stringa cercata); il codice utilizza poi i due valori trovati per impostare i campi ProductName e CategoryDescription. Quando il codice trova una descrizione di categoria, blocca quella casella combinata, in modo che l'utente non possa cambiarla in qualcosa di diverso da ciò che era selezionato sulla riga del nuovo prodotto nella maschera originale. Capitolo 7 Assistenza per l'immissione dei dati 346 Capitolo 7 Automazione di un'applicazione tramite Visual Basic Controllo automatico di un indirizzo di posta elettronica Capitolo 7 Uno dei modi più facili per immettere un collegamento ipertestuale consiste nell'utilizzare la funzionalità Inserisci collegamento ipertestuale. Tuttavia puoi anche digitare l'indirizzo del collegamento ipertestuale direttamente nel campo in un foglio dati o in una maschera. Ricorda che un campo collegamento ipertestuale può contenere fino a quattro parti: testo visualizzato, indirizzo del collegamento ipertestuale, segnalibro e testo della descrizione comando. Se un utente si limita a immettere un indirizzo di posta elettronica in un campo collegamento ipertestuale, Access 2010 riconosce il formato, aggiunge il protocollo mailto: e utilizza ciò che l'utente digita come testo visualizzato. Se per esempio l'utente immette [email protected] Access memorizza nel campo collegamento ipertestuale [email protected]#mailto:[email protected]# Invece di ripetere l'indirizzo di posta elettronica come testo visualizzato, il risultato potrebbe avere un aspetto migliore se il testo mostrato fosse il nome della persona. Una delle maschere che contengono un indirizzo di posta elettronica è la maschera frmContacts nell'applicazione Conrad Systems Contacts. Puoi trovare il codice che esamina e cerca di correggere l'indirizzo nella routine evento AfterUpdate (dell'evento DopoAggiornamento) per la casella di testo EmailName. Se l'utente immette un protocollo valido diverso da http:// o mailto:, questo codice non lo altera. Il codice è il seguente: Private Sub EmailName_AfterUpdate() ‘ If you just type in an email name: [email protected] ‘ Access changes it to: [email protected]#mailto:[email protected]# ‘ This code replaces the display field with the user name Dim intI As Integer ‘ Don’t do anything if email is empty If IsNothing(Me.EmailName) Then Exit Sub ‘ Fix up http:// if it’s there ‘ This was an old bug in 2003 and earlier, but fixed in Access 2007 Me.EmailName = Replace(Me.EmailName, “http://”, “mailto:”) ‘ Now look for the first “#” that delimits the hyperlink display name intI = InStr(Me.EmailName, “#”) ‘ And put the person name there instead if found If intI > 0 Then Me.EmailName = (Me.FirstName + “ “) & Me.LastName & _ Mid(Me.EmailName, intI) End If End Sub Se l'utente deseleziona la casella di testo EmailName, il codice non esegue alcuna operazione. Se invece la casella di testo contiene una stringa, il codice utilizza la funzione Replace per cercare un indirizzo http:// non corretto e sostituirlo con l'identificatore di protocollo mailto: corretto. Come sai, un campo collegamento ipertestuale può contenere testo che viene visualizzato al posto del collegamento ipertestuale, un delimitatore di carattere # e l'effettivo indirizzo del collegamento ipertestuale. Il codice utilizza la funzione 347 InStr per verificare la presenza del delimitatore. La funzione InStr restituisce l'offset nella stringa specificata nel primo parametro, corrispondente al punto in cui si trova la stringa specificata nel secondo parametro. Se trova il delimitatore, il codice sostituisce il contenuto del campo con il nome e il cognome della persona come testo da visualizzare, seguito dal testo che comincia con il delimitatore # . La funzione Mid chiamata senza specificare la lunghezza (il terzo parametro facoltativo) restituisce tutti i caratteri che cominciano all'offset specificato. Nota In Access 2003 e versioni precedenti, quando si digitava un indirizzo di posta elettronica senza il prefisso del protocollo mailto: in un campo collegamento ipertestuale, Access memorizzava erroneamente il collegamento ipertestuale con il prefisso di protocollo http://. / Questo problema è stato corretto in Access 2007, ma il codice precedente corregge il problema se lo utilizzi nelle versioni precedenti. Utilizzo di un calendario grafico Puoi fornire una maschera di input per aiutare l'utente a immettere correttamente le date e le ore, tuttavia una maschera di input può essere inadeguata, per esempio se richiede che l'utente digiti sempre il mese con due cifre. Una maschera di input può anche essere in conflitto con qualsiasi valore predefinito che potresti voler assegnare. È perciò preferibile che l'utente possa scegliere la data da un calendario grafico. Access 2010 fornisce la nuova proprietà Visualizza selezione data per le caselle di testo. Puoi impostare questa proprietà a Per le date per indurre Access a visualizzare un'icona calendario accanto al controllo quando contiene un valore di data/ora e ha lo stato attivo. L'utente può fare clic sul pulsante per far apparire un calendario grafico in cui selezionare un valore di data. Visualizza selezione data non è disponibile per i controlli diversi dalle caselle di testo e il selettore data consente all'utente di immettere soltanto la data e non la data e l'ora. Poiché nel selettore data sei anche limitato a spostarti di un mese alla volta usando i tasti freccia sinistro e destro, per immettere una data che dista di molti mesi dalla data corrente, ad esempio una data di nascita, devi fare clic molte volte sui tasti freccia per raggiungere il mese e l'anno corretti. Entrambe le applicazioni di esempio Conrad Systems Contacts e Housing Reservations offrono maschere e codice calendario di esempio che puoi utilizzare per impostare un valore di data/ora in qualsiasi controllo. Queste due applicazioni contengono una maschera calendario chiamata frmCalendar che impiega codice Visual Basic per trascinare il calendario in una maschera usando un gruppo opzioni e controlli pulsante interruttore. La maschera calendario fornisce un'opzione che permette di immettere l'ora e selezionare la data. Capitolo 7 Assistenza per l'immissione dei dati 348 Capitolo 7 Automazione di un'applicazione tramite Visual Basic RISOLUZIONE DEI PROBLEMI Perché Access non imposta il mio valore definito per un campo di data/ora? Capitolo 7 Hai anche definito la proprietà Maschera di input? Se è così, il problema è questo. Un campo Data/ora è effettivamente un numero in virgola mobile ma Access converte e visualizza sempre il valore carattere nei fogli dati delle tabelle e delle query e nelle maschere e report. Quando definisci la proprietà Maschera di input, qualsiasi impostazione Valore predefinito deve corrispondere alle restrizioni imposte dalla maschera di input. Se il valore viola le restrizioni, Access non utilizzerà il valore predefinito. Quando assegni un valore predefinito a un campo Data/ora, utilizzi tipicamente una delle funzioni incorporate Date e Now. Queste funzioni restituiscono un valore in virgola mobile di data/ora valido che probabilmente non corrisponderà alle restrizioni della maschera di input. Per ottenere che Access utilizzi il valore predefinito, devi formattarlo in modo che corrisponda alla maschera di input. Ad esempio, se la tua maschera di input è 00/90/0000\ 00:00, devi impostare la proprietà Valore predefinito del campo o controllo su =Format(Now(), "gg/mm/aaaa hh:nn"). Questo induce Access a restituire un valore stringa come predefinito che corrisponde alla maschera di input. Questa funzionalità grafica è disponibile nelle applicazioni di esempio ogni qualvolta vedi un piccolo pulsante di comando vicino a una campo di tipo data o data/ora su una maschera. Fai clic sul pulsante per aprire il calendario e impostare il valore. Un controllo che utilizza una maschera calendario personalizzata è il controllo ContactDateTime della scheda Events della maschera frmContacts. Puoi vedere il calendario aperto nella figura 7-5. Figura 7-5 Fai clic sul pulsante di comando del calendario accanto al controllo ContactDateTime nella scheda Events della maschera frmContacts per aprire una maschera grafica in cui selezionare la data e immettere l'ora. Il codice nell'evento Clic di questo pulsante di comando chiama una funzione pubblica per aprire la maschera e passare ad essa il controllo correlato che dovrebbe ricevere il valore della data risultante. Questo codice è disponibile nel modulo della maschera fsubContactEvents. Private Sub cmdContactTimeCal_Click() Dim varReturn As Variant ‘ Clicked the calendar icon asking for graphical help ‘ Put the focus on the control to be updated Me.ContactDateTime.SetFocus ‘ Call the get a date/time function varReturn = GetDate(Me.ContactDateTime, False) End Sub APPROFONDIMENTO AP PPROFONDIMENTO Utilizzare i tasti di scelta rapida per passare a routine e funzioni Se evidenzi la chiamata di funzione (GetDate nell'esempio precedente) e premi Maiusc+F2, Access passa direttamente a quella funzione nel modulo modCalendar. Quando l'utente fa clic sul pulsante di comando, Access lo attiva. Il codice attiva nuovamente il campo data e chiama la funzione pubblica dove avviene l'azione effettiva. Puoi trovare il codice per la funzione GetDate nel modulo modCalendar. Option Compare Database Option Explicit Public Function GetDate(ctl As control, _ Optional intDateOnly As Integer = 0) As Integer ‘----------------------------------------------------------‘ Inputs: A Control object containing a date/time value ‘ Optional “date only” (no time value) flag ‘ Outputs: Sets the Control to the value returned by frmCalendar ‘ Created By: JLV 09/05/01 ‘ Last Revised: JLV 09/05/01 ‘----------------------------------------------------------Dim varDateTime As Variant Dim strDateTime As String Dim frm As Form ‘ Error trap just in case On Error GoTo Error_Date ‘ First, validate the kind of control passed Select Case ctl.ControlType ‘ Text box, combo box, and list box are OK Case acTextBox, acListBox, acComboBox Case Else GetDate = False Exit Function End Select ‘ If the control has no value If IsNothing(ctl.Value) Then 349 Capitolo 7 Assistenza per l'immissione dei dati 350 Capitolo 7 Automazione di un'applicazione tramite Visual Basic Capitolo 7 If intDateOnly Then ‘ Set default date varDateTime = Date Else ‘ .. or default date and time varDateTime = Now End If Else ‘ Otherwise, pick up the current value varDateTime = ctl.Value ‘ Make sure it’s a date/time If vbDate <> varType(varDateTime) Then GetDate = False Exit Function End If End If ‘ Turn the date and time into a string ‘ to pass to the form strDateTime = Format(varDateTime, “mm/dd/yyyy hh:nn”) ‘ Make sure we don’t have an old copy of ‘ frmCalendar hanging around If IsFormLoaded(“frmCalendar”) Then DoCmd.Close acForm, “frmCalendar” End If ‘ Open the calendar as a dialog so this code waits, ‘ and pass the date/time value DoCmd.OpenForm “frmCalendar”, WindowMode:=acDialog, _ OpenArgs:=strDateTime & “,” & intDateOnly ‘ If the form is gone, user canceled the update If Not IsFormLoaded(“frmCalendar”) Then Exit Function ‘ Get a pointer to the now-hidden form Set frm = Forms!frmCalendar ‘ Grab the date part off the hidden text box strDateTime = Format(frm.ctlCalendar.Value, “dd-mmm-yyyy”) If Not intDateOnly Then ‘ If looking for date and time, ‘ also grab the hour and minute strDateTime = strDateTime & “ “ & frm.txtHour & _ “:” & frm.txtMinute End If ‘ Stuff the returned value back in the caller’s control ctl.Value = DateValue(strDateTime) + TimeValue(strDateTime) ‘ Close the calendar form to clean up DoCmd.Close acForm, “frmCalendar” GetDate = True Exit_Date: Exit Function Error_Date: ‘ This code is pretty simple and does check for ‘ a usable control type, ‘ .. so this should never happen. ‘ But if it does, log it... ErrorLog “GetDate”, Err, Error GetDate = False Resume Exit_Date End Function 351 La funzione imposta innanzitutto la gestione degli errori che provoca l'esecuzione del codice all'etichetta Error_Date se qualcosa va male. La funzione accetta gli argomenti ctlToUpdate e intDateOnly. Access utilizza l'argomento ctlToUpdate per esaminare il tipo di controllo e controllare il valore passato alla funzione. Access utilizza l'argomento facoltativo intDateOnly per indicare se il controllo richiede data e ora o solo la data. La funzione verifica se il controllo passato è una casella di testo, una casella combinata o una casella di riepilogo. Se il controllo coincide con uno di questi tipi, la funzione procede, altrimenti si interrompe. La funzione controlla quindi il valore del controllo passato. Se nel controllo non è presente alcun valore, Access assegna alla variabile varDateTime la data corrente o la data e l'ora correnti, a seconda del valore presente nell'argomento intDateOnly. Se il controllo ha un valore, la funzione assegna tale valore attuale alla variabile e verifica anche che il valore corrisponda a una data e un'ora valide. Fatto ciò, la funzione converte il valore di tipo Variant in una stringa e verifica se la maschera frmCalendar è aperta. Se trova la maschera calendario aperta, la funzione la chiude e la riapre in modalità Dialog. Nella chiamata OpenForm a frmCalendar, la funzione passa il valore stringa di data e ora e il valore intDateOnly come parametri di OpenArgs. Una volta che l'utente ha fatto clic sul pulsanti Save o Cancel nella maschera frmCalendar, questa funzione continua a rimanere in esecuzione. Se l'utente annulla l'aggiornamento facendo clic sul pulsante di comando cmdCancel nella maschera frmCalendar, la funzione si interrompe. Se l'utente fa clic sul pulsante di comando cmdSave nella maschera frmCalendar, la funzione prende le informazioni di data e ora selezionate dalla maschera, imposta il valore del controllo chiamante con data e ora, chiude la maschera frmCalendar e si interrompe. Gli ultimi segmenti di codice che fanno funzionare l'esempio si trovano nel modulo della maschera frmCalendar. Il codice nell'evento Load della maschera è elencato di seguito: Private Sub Form_Load() ‘ Establish an initial value for the date If IsNothing(Me.OpenArgs) Then varDate = Date Else ‘ Should have date, time, and “DateOnly” ‘ indicator in OpenArgs: ‘ mm/dd/yyyy hh:mm,-1 varDate = Left(Me.OpenArgs, 10) Me.txtHour = Mid(Me.OpenArgs, 12, 2) Me.txtMinute = Mid(Me.OpenArgs, 15, 2) ‘ If “date only” If Right(Me.OpenArgs, 2) = “-1” Then ‘ Hide some stuff Me.txtHour.Visible = False Me.txtMinute.Visible = False Me.lblColon.Visible = False Me.lblTimeInstruct.Visible = False Me.SetFocus ‘ .. and resize my window DoCmd.MoveSize , , , 4295 End If Capitolo 7 Assistenza per l'immissione dei dati 352 Capitolo 7 Automazione di un'applicazione tramite Visual Basic Capitolo 7 End If ‘ Initialize the month selector Me.cmbMonth = Month(varDate) ‘ Initialize the year selector Me.cmbYear = Year(varDate) ‘ Call the common calendar draw routine SetDays ‘ Place the date/time value in a hidden control ‘ The calling routine fetches it from here Me.ctlCalendar = varDate ‘ Highlight the correct day box in the calendar Me.optCalendar = Day(varDate) End Sub Per prima cosa, il codice controlla alla maschera vengono passati parametri OpenArgs. In caso contrario, il codice assegna la data corrente a una variabile chiamata varDate. Se i parametri OpenArgs vengono passati, il codice esamina gli elementi OpenArgs alla ricerca della possibile parte relativa a data e ora. Se la variabile facoltativa intDateOnly è vera (il controllo richiede solo un valore di tipo data, non una data e un'ora), la maschera si restringe per nascondere quelle caselle di testo. Poiché il campo data/ora dell'evento richiede un valore di ora, il parametro è False, perciò dovresti essere in grado di vedere le caselle di testo dell'ora e dei minuti. La parte finale del codice imposta il calendario in base a elementi di controllo, in modo da farlo corrispondere al valore già presente nel controllo o alla data e all'ora di sistema. Come puoi notare, il codice richiama una routine SetDays inclusa nel modulo classe della maschera per impostarne i vari controlli. Quando il codice di impostazione è terminato, la maschera aspetta fino a quando l'utente non immette un valore e fa clic su Save, oppure decide di non modificare il valore e fa clic su Cancel. Il codice delle due routine che rispondono ai pulsanti di comando è il seguente: Public Sub cmdCancel_Click() ‘ Closing doesn’t pass the value back DoCmd.Close acForm, Me.Name End Sub Private Sub cmdSave_Click() ‘ Hiding this dialog lets the calling code in GetDate continue Me.Visible = False End Sub Se l'utente fa clic sul pulsante Cancel (cmdCancel_Click) la maschera viene chiusa senza modificare nessun valore nel controllo passato ad essa. Il codice che salva il valore selezionato dall'utente in un calendario grafico si trova nel modulo GetDate. Per salvare il valore, l'evento clic del pulsante di comando cmdSave deve semplicemente nascondere la maschera frmCalendar. Assistenza per l'immissione dei dati 353 RISOLUZIONE DEI PROBLEMI In Access 2010, Microsoft ha rimosso il controllo Calendario di ActiveX fornito con molte delle precedenti versioni di Access. Per visualizzare un calendario grafico agli utenti che devono selezionare una data, devi utilizzare il nuovo controllo incorporato Selezione data fornito con Access, oppure creare una maschera calendario personalizzata usando Visual Basic, come abbiamo fatto nelle applicazioni di esempio Conrad Systems Contacts e Housing Reservations. Lavorare con foto collegate Benché in un'applicazione Access sia possibile memorizzare e visualizzare fotografie in modo sicuro usando il tipo di dati Oggetto OLE, se l'applicazione ha bisogno di gestire centinaia o migliaia di foto puoi facilmente eccedere il limite di dimensione di 2 gigabyte dei file .accdb In alternativa puoi utilizzare il tipo di dati Allegato per archiviare le tue fotografie più efficientemente, ma anche in questo caso è possibile incorrere nelle limitazioni relative alle dimensioni dei file. Il metodo alternativo è memorizzare le immagini come file e salvare i percorsi d'immagine come campi di testo nelle tabelle. La buona notizia è che il controllo Immagine di Access 2010 ti permette di specificare la proprietà Origine controllo. Quando questa proprietà punta a un campo contenente una posizione di cartella e file come stringa di testo, il controllo Immagine carica direttamente la foto da quella posizione. Tu devi comunque fornire caratteristiche nelle maschere per aiutare l'utente a modificare facilmente le informazioni sulla posizione del file. Il database Housing Reservations (Housing.accdb) è progettato in modo da utilizzare questa funzionalità. Apri il database di esempio Housing.accdb, quindi apri la maschera frmEmployeesPlain, come mostrato nella figura 7-6. L'immagine del dipendente che vedi nelle maschere frmEmployees e frmEmployeesPlain viene recuperata dal controllo Immagine attraverso il percorso memorizzato nel campo Photo della tabella. Capitolo 7 Perché in Access 2010 non è presente il Controllo Calendario di ActiveX? 354 Capitolo 7 Automazione di un'applicazione tramite Visual Basic Capitolo 7 Figura 7-6 Il controllo Immagine carica la foto nella maschera Employees da un percorso. Nota che l'utente non può vedere il contenuto del campo Photo che include le informazioni sul percorso d'immagine. Abbiamo comunque fornito due pulsanti di comando per facilitare all'utente la modifica e l'eliminazione delle informazioni sul percorso d'immagine. Eliminazione e aggiornamento di un percorso d'immagine Cancellare un nome di file salvato nel record è la parte semplice, perciò vediamola per prima. Dietro il pulsante Delete che puoi vedere sulla maschera frmEmployeesPlain è presente il codice che segue: Private Sub cmdDelete_Click() ‘ User asked to remove the picture ‘ Clear photo Me.txtPhoto = Null ‘ Set the message Me.lblMsg.Caption = “Click Add to create a photo for this employee.” ‘ Put focus in a safe place Me.FirstName.SetFocus End Sub Quando l'utente fa clic sul pulsante di comando che chiede di eliminare la foto, il codice imposta il percorso della foto a Null e mostra l'etichetta informativa. Impostando il campo Photo su Null si induce il controllo Immagine a eliminare l'immagine. Poiché lo sfondo del controllo Immagine è trasparente, lascia vedere il controllo Etichetta nascosto dietro che mostra un messaggio informativo. La parte difficile è fornire all'utente un modo per immettere un percorso d'immagine affinché possa aggiungere o aggiornare un'immagine in un record. Nonostante sia possibile utilizzare la funzione InputBox per chiedere il percorso all'utente, è molto più professionale richiamare la finestra di dialogo Apri file di Windows per fare in modo che l'utente possa accedere all'immagine desiderata utilizzando strumenti familiari. La cattiva 355 notizia è che chiamare qualsiasi routine in Windows è complicato e di solito richiede di impostare strutture di parametri e una speciale dichiarazione di funzione esterna. La buona notizia è che Microsoft Office 2010 include lo speciale oggetto FileDialog che semplifica notevolmente la procedura. Per facilitare l'uso di questo oggetto, devi aggiungere un riferimento alla libreria Microsoft Office: dalla finestra Visual Basic Editor, seleziona Riferimenti nel menu Strumenti e accertati che sia selezionata l'opzione Microsoft Office 14.0 Object Library. Dopo averlo fatto, puoi includere codice utilizzando l'oggetto FileDialog per caricare un percorso d'immagine. Puoi trovare il codice che segue dietro l'evento Click del pulsante Add (cmdAdd) della maschera frmEmployeesPlain: Private Sub cmdAdd_Click() ‘ User asked to add a new photo Dim strPath As String ‘ Grab a copy of the Office file dialog With Application.FileDialog(msoFileDialogFilePicker) ‘ Select only one file .AllowMultiSelect = False ‘ Set the dialog title .Title = “Locate the Employee picture file” ‘ Set the button caption .ButtonName = “Choose” ‘ Make sure the filter list is clear .Filters.Clear ‘ Add two filters .Filters.Add “JPEGs”, “*.jpg” .Filters.Add “Bitmaps”, “*.bmp” ‘ Set the filter index to 2 .FilterIndex = 2 ‘ Set the initial path name .InitialFileName = CurrentProject.Path & “\Pictures” ‘ Show files as thumbnails .InitialView = msoFileDialogViewThumbnail ‘ Show the dialog and test the return If .Show = 0 Then ‘ Didn’t pick a file - bail Exit Sub End If ‘ Should be only one filename - grab it strPath = Trim(.SelectedItems(1)) ‘ Set an error trap On Error Resume Next ‘ Set the image Me.txtPhoto = strPath ‘ Set the message in case Image control couldn’t find it Me.lblMsg.Caption = “Failed to load the picture you selected.” & _ “ Click Add to try again.” End With ‘ Put focus in a safe place Me.FirstName.SetFocus End Sub Capitolo 7 Assistenza per l'immissione dei dati 356 Capitolo 7 Automazione di un'applicazione tramite Visual Basic Capitolo 7 Il codice definisce un puntatore all'oggetto FileDialog per mezzo di un'istruzione With, imposta le varie proprietà dell'oggetto, comprese le estensioni di file consentite e il percorso iniziale, quindi utilizza il metodo Show per visualizzare la finestra di dialogo Apri file. Impostando il campo Photo si induce il controllo Immagine a caricare la nuova immagine, ma il codice imposta anche il messaggio nascosto dietro il controllo Immagine nel caso il controllo Immagine incontri un problema caricando il file. Convalida di dati complessi Anche se puoi certamente trarre vantaggio dalla proprietà Input Mask e dalle proprietà Validation Rule di campo e di tabella, la tua applicazione spesso possiede regole applicative aggiuntive che puoi far rispettare solo aggiungendo codice alle maschere fornite per modificare i dati. Gli esempi seguenti mostrano come molte delle regole aziendali nelle applicazioni Conrad Systems Contacts e Housing Reservations sono fatte rispettare con codice Visual Basic. Verifica di possibili nomi duplicati Quando progetti una tabella, in generale devi cercare di identificare alcune combinazioni di campi che siano uniche nell'insieme di tutti i record da utilizzare come chiave primaria. Quando tuttavia crei una tabella per memorizzare alcune informazioni sulle persone, solitamente crei un codice artificiale univoco come chiave primaria della tabella, poiché dovresti combinare molti campi per assicurare un valore univoco. Infatti anche costruendo una chiave primaria dal nome, dal cognome, dall'indirizzo, dal codice postale e dal numero telefonico, non si può essere certi dell'univocità della chiave all'interno di tutte le righe. L'utilizzo di una chiave primaria artificiale non significa rinunciare a identificare le righe potenzialmente duplicate. Il codice nella maschera frmContacts dell'applicazione Conrad Systems Contacts verifica il cognome immesso dall'utente per un nuovo record, e visualizza un messaggio di avvertimento se trova nomi simili. Se ad esempio l'utente crea un nuovo record e immette un cognome tipo "Viescas" (presumendo che il record di John sia ancora presente nella tabella), il codice dell'evento BeforeUpdate nella maschera scopre il nome simile e visualizza il messaggio d'avvertimento della figura 7-7. 357 Capitolo 7 Convalida di dati complessi Figura 7-7 L'applicazione segnala un nome potenzialmente duplicato nell'applicazione Conrad Systems Contacts. Il codice cerca potenziali duplicati confrontando i codici Soundex dei cognomi. La formula per generare un codice Soundex per un nome è stata creata da U.S. National Archives and Records Administration (NARA). Vengono esaminate le lettere in base al suono e viene prodotto un codice di quattro caratteri; quando i codici dei due nomi corrispondono, è probabile che i nomi siano molto simili e producano lo stesso suono. Pertanto, grazie all'impiego di Soundex, il codice di verifica degli errori non solo trova contatti esistenti con esattamente lo stesso cognome, ma anche altri contatti il cui nome potrebbe essere lo stesso pur non essendo stato scritto correttamente. Access 2010 fornisce una funzione Soundex predefinita (SQL Server sì), ma è facile creare una semplice routine Visual Basic per generare il codice per un nome. Puoi trovare una funzione Soundex nel modulo modUtility, in entrambi i database Conrad Systems Contacts e Housing Reservations. Puoi trovare il codice che controlla un nome potenzialmente duplicato nella routine evento BeforeUpdate della maschera frmContacts: Il codice è il seguente: Private Sub Form_BeforeUpdate(Cancel As Integer) Dim rst As DAO.Recordset, strNames As String ‘ If on a new row, If (Me.NewRecord = True) Then ‘ Check for similar name If Not IsNothing(Me.LastName) Then ‘ Open a recordset to look for similar names Set rst = CurrentDb.OpenRecordset(“SELECT LastName, FirstName FROM “ & _ “tblContacts WHERE Soundex([LastName]) = ‘” & _ Soundex(Me.LastName) & “’”) 358 Capitolo 7 Automazione di un'applicazione tramite Visual Basic Capitolo 7 ‘ If got some similar names, collect them for the message Do Until rst.EOF strNames = strNames & rst!LastName & “, “ & rst!FirstName & vbCrLf rst.MoveNext Loop ‘ Done with the recordset rst.Close Set rst = Nothing ‘ See if we got some similar names If Len(strNames) > 0 Then ‘ Yup, issue warning If vbNo = MsgBox(“CSD Contacts found contacts with similar “ & _ “last names already saved in the database: “ & vbCrLf & vbCrLf & _ strNames & vbCrLf & _ “Are you sure this contact is not a duplicate?”, _ vbQuestion + vbYesNo + vbDefaultButton2, gstrAppTitle) Then ‘ Cancel the save Cancel = True End If End If End If End If End Sub Il codice esegue la verifica solo quando l'utente sta per salvare una nuova riga. Apre un recordset per prelevare tutti gli altri record dei contatti dove il codice Soundex del cognome corrisponde al cognome che sta per essere salvato; include tutti i nomi che trova nel messaggio di avvertimento, in modo che l'utente possa verificare se il nuovo contatto è un duplicato. Se l'utente decide di non salvare il record, il codice imposta il parametro Cancel a True, per indicare ad Access di non salvare il nuovo contatto. Verifica dell'esistenza di record correlati in fase di eliminazione di un record Dovresti definire alcune relazioni tra le tabelle, e chiedere ad Access di far rispettare l'integrità referenziale, per impedire il salvataggio di record non correlati oppure l'eliminazione di un record che ha ancora record correlati in altre tabelle. Nella maggior parte dei casi, non desideri attivare la funzionalità di eliminazione a cascata, per eliminare automaticamente i record correlati. Ogni volta che l'utente cerca di eliminare un record che possiede record dipendenti in altre tabelle, Access visualizza un messaggio con l'avviso che il record non può essere eliminato o modificato, in quanto 'tblXYZ' contiene record correlati. Puoi eseguire le tue verifiche nel codice delle maschere, nell'evento Eliminazione (Delete), e puoi inviare all'utente un messaggio che identifica più chiaramente il problema. Ecco per esempio il codice della routine evento Delete della maschera frmContacts nell'applicazione Conrad Systems Contacts: 359 Private Sub Form_Delete(Cancel As Integer) Dim db As DAO.Database, qd As DAO.QueryDef, rst As DAO.Recordset Dim varRelate As Variant ‘ Check for related child rows ‘ Get a pointer to this database Set db = CurrentDb ‘ Open the test query Set qd = db.QueryDefs(“qryCheckRelateContact”) ‘ Set the contact parameter qd!ContactNo = Me.ContactID ‘ Open a recordset on the related rows Set rst = qd.OpenRecordset() ‘ If we got rows, then can’t delete If Not rst.EOF Then varRelate = Null ‘ Loop to build the informative error message rst.MoveFirst Do Until rst.EOF ‘ Grab all the table names varRelate = (varRelate + “, “) & rst!TableName rst.MoveNext Loop MsgBox “You cannot delete this Contact because you have “ & _ “related rows in “ & _ varRelate & _ “. Delete these records first, and then delete the Contact.”, _ vbOKOnly + vbCritical, gstrAppTitle ‘ close all objects rst.Close qd.Close Set rst = Nothing Set qd = Nothing Set db = Nothing ‘ Cancel the delete Cancel = True Exit Sub End If ‘ No related rows - clean up objects rst.Close qd.Close Set rst = Nothing Set qd = Nothing Set db = Nothing ‘ No related rows, so OK to ask if they want to delete! If vbNo = MsgBox(“Are you sure you want to delete Contact “ & _ Me.txtFullName & “?”, _ vbQuestion + vbYesNo + vbDefaultButton2, gstrAppTitle) Then Cancel = True End If End Sub Il codice utilizza una speciale query di unione con parametri, qryCheckRelateContact, che cerca di prelevare le righe correlate da tblCompanyContacts, tblCompanies (il campo ReferredBy), tblContactEvents e tblContactProducts, e ritorna i nomi delle tabelle che Capitolo 7 Convalida di dati complessi 360 Capitolo 7 Automazione di un'applicazione tramite Visual Basic Capitolo 7 hanno righe correlate. Quando il codice trova delle righe ritornate dalla query, formatta un messaggio contenente i nomi più significativi per l'utente, e include tutte le tabelle che l'utente deve deselezionare per essere in grado di eliminare il contatto. Il messaggio di errore standard di Access elenca solo la prima tabella correlata trovata da Access. Anche quando il controllo di record correlati è negativo, il codice offre all'utente la possibilità di decidere di non eliminare il contatto. Verifica dei prerequisiti In alcune applicazioni ha senso salvare un certo tipo di record, solo se esistono alcuni record prerequisiti. In un'applicazione per l'iscrizione scolastica per esempio, l'utente potrebbe verificare che la persona che si sta iscrivendo ha completato con successo i corsi prerequisiti. Nell'applicazione Conrad Systems Contacts non ha senso vendere il supporto per un prodotto che il contatto non possiede. Non è possibile chiedere ad Access di eseguire questo tipo di verifica in una regola di convalida, pertanto devi scrivere il codice o creare una macro che facciano rispettare questa regola aziendale. La figura 7-8 mostra il messaggio che l'utente vede quando cerca di vendere il supporto per un prodotto che il contatto non possiede. Questo messaggio appare anche se l'utente tenta di vendere lo speciale upgrade al prodotto Multi-user a un contatto che non possiede il prodotto Single-user indicato come prerequisito. Figura 7-8 Lo speciale codice della regola aziendale non ti consente di vendere un prodotto con un prerequisito mancante. Il codice che fa rispettare questa regola aziendale è nella routine evento BeforeUpdate della maschera fsubContactProducts: Il codice è il seguente: 361 Private Sub Form_BeforeUpdate(Cancel As Integer) Dim lngPreReq As Long, strPreReqName As String ‘ Check for prerequisite If Not IsNothing(Me.cmbProductID.Column(6)) Then ‘ Try to lookup the prerequisite for the contact lngPreReq = CLng(Me.cmbProductID.Column(6)) If IsNull(DLookup(“ProductID”, “tblContactProducts”, _ “ProductID = “ & lngPreReq & “ And ContactID = “ & _ Me.Parent.ContactID)) Then ‘ Get the name of the prerequisite strPreReqName = DLookup(“ProductName”, “tblProducts”, _ “ProductID = “ & lngPreReq) ‘ Display error MsgBox “This contact must own prerequisite product “ & strPreReqName & _ “ before you can sell this product.”, vbCritical, gstrAppTitle ‘ Cancel the edit Cancel = True End If End If End Sub La query che fornisce l'origine riga per la casella combinata cmbProductID include ogni ProductID prerequisito nella sua settima colonna, come mostrato nella figura 7-2. Quando il codice trova un prerequisito, utilizza la funzione DLookup per verificare che il contatto corrente possieda già il prodotto richiesto. In caso contrario il codice cerca il nome del prodotto, lo include in un messaggio di errore visualizzato all'utente, e non consente il salvataggio del prodotto impostando il parametro Cancel a True. In questo modo viene fatta rispettare la regola aziendale e viene indicata chiaramente all'utente l'azione correttiva necessaria. Mantenimento di uno speciale valore univoco Quando due soggetti sono legati da una relazione molti a molti nel database, devi definire una tabella di collegamento per creare la relazione. Per ulteriori informazioni sulla progettazione di tabelle a supporto di una relazione molti a molti, consulta l'articolo 1, "Progettazione di un'applicazione di database". Spesso aggiungerai alcuni campi nella tabella di collegamento, per rendere più chiara la relazione tra una riga in una della tabelle correlate e la riga corrispondente nell'altra. La figura 7-9 mostra la tabella che definisce il collegamento tra società e contatti nell'applicazione Conrad Systems Contacts. Capitolo 7 Convalida di dati complessi 362 Capitolo 7 Automazione di un'applicazione tramite Visual Basic Capitolo 7 Figura 7-9 La tabella tblCompanyContacts definisce la relazione molti a molti tra società e contatti. Due speciali campi sì/no in questa tabella identificano quale società è la predefinita per un contatto e quale contatto è il predefinito per una società. Un contatto non può avere due o più società predefinite; analogamente, non ha senso per una società avere più di un contatto predefinito. Per verificare questo tipo di vincolo speciale di valore univoco, devi aggiungere alcune regole aziendali nel codice delle maschere che fornisci agli utenti per modificare questi dati. Puoi trovare il codice che assicura che vi sia solo una società predefinita per ciascun contatto nel codice della maschera fsubContactCompanies dell'applicazione Conrad Systems Contacts (Contacts.accdb). Il codice è nella routine evento BeforeUpdate per il controllo DefaultForContact della maschera: Il codice è il seguente: Private Sub DefaultForContact_BeforeUpdate(Cancel As Integer) ‘ Disallow update if there’s no Company ID yet If IsNothing(Me.CompanyID) Then MsgBox “You must select a Company / Organization before” & _ “ you can set Default.”, _ vbCritical, gstrAppTitle Cancel = True Exit Sub End If ‘ Make sure there’s only one default ‘ Check only if setting Default = True If (Me.DefaultForContact = True) Then ‘ Try to lookup another contact set Default If Not IsNothing(DLookup(“ContactID”, “tblCompanyContacts”, _ “ContactID = “ & Me.Parent.ContactID & _ “ AND CompanyID <> “ & Me.CompanyID & _ “ AND DefaultForContact = True”)) Then ‘ ooops... MsgBox “You have designated another Company as the” & _ 363 “ Default for this Contact.” & _ “ You must remove that designation before you” & _ “ can mark this Company as the Default.”, _ vbCritical, gstrAppTitle Cancel = True End If End If End Sub Per prima cosa, il codice verifica che l'utente abbia scelto una società per questo record. Le proprietà Collega campi secondari e Collega campi master del controllo sottomaschera forniscono il ContactID. Se l'utente sta cercando di contrassegnare questa società come la società predefinita per il contatto, il codice utilizza la funzione DLookup per vedere se esiste un altro record (nella tabella tblCompanyContacts per il contatto corrente) contrassegnato come predefinito. Se trova un tale record duplicato, avvisa l'utente e imposta il parametro Cancel a True per impedire il salvataggio della modifica al controllo. Troverai codice simile nella maschera fsubCompanyContactst, che assicura che solo un contatto sia la chiave primaria per una società. Verifica dell'esistenza di dati sovrapposti Quando crei un'applicazione che mantiene traccia della programmazione di eventi o prenotazioni che possono estendersi per un periodo di tempo, devi molto probabilmente assicurare che un nuovo evento o prenotazione non si sovrapponga a uno esistente. Questa operazione può essere complicata, specialmente nel caso in cui i record contengono date e/o ore, iniziali e finali. L'applicazione Housing Reservations (Housing.accdb) deve assicurare che un impiegato non immetta una richiesta di prenotazione che si sovrappone a un'altra. Per vedere come funziona, apri il database e poi apri la maschera frmSplash per avviare l'applicazione. Scegli il nome di un dipendente qualunque dalla casella combinata nella finestra di dialogo Sign-on (ad esempio Jack Richins), digita password come password e fai clic sul pulsante Sign On. Nel pannello comandi principale, fai clic sul pulsante Reservation Requests. Se viene visualizzata la finestra di dialogo Edit Reservation Requests (poiché sei connesso come manager), fai clic sul pulsante Edit All. La maschera Reservation Requests non ti consente di immettere una data di prenotazione relativa al passato. Fai clic nella nuova riga vuota nell'elenco delle richieste di prenotazione, immetti una richiesta di prenotazione per la settimana prossima della durata di alcuni giorni e salva la riga. Ricorda che, per scegliere le date desiderate, puoi fare clic sui pulsanti Calendar visualizzati accanto ai campi data quando è attivo il campo. Immetti un'altra richiesta che si sovrappone alla prenotazione appena creata e cerca di salvare la riga; dovresti vedere un messaggio di avvertimento simile a quello della figura 7-10. Capitolo 7 Convalida di dati complessi 364 Capitolo 7 Automazione di un'applicazione tramite Visual Basic Capitolo 7 Figura 7-10 L'applicazione Housing Reservations visualizza un avvertimento quando cerchi di salvare una richiesta di prenotazione che si sovrappone a una esistente. Se fai clic su No, il codice annulla il salvataggio e puoi correggere il record. Come puoi notare, l'applicazione consente anche di fare clic su Sì per salvare il duplicato, poiché il dipendente potrebbe volere prenotare intenzionalmente due o più stanze in date che si sovrappongono. Il codice che esegue questo controllo nell'evento BeforeUpdate della maschera fsubReservationRequests è il seguente (nota che questo codice si trova vicino alla fine del codice di evento BeforeUpdate): Dim varNum As Variant ‘ Check for overlap with existing request ‘ Try to grab RequestID - will be Null on unsaved row varNum = Me.RequestID If IsNull(varNum) Then varNum = 0 ‘ Set dummy value If Not IsNull(DLookup(“RequestID”, “tblReservationRequests”, _ “(EmployeeNumber = “ & _ Me.Parent.EmployeeNumber & “) AND (CheckInDate < #” & _ Format(Me.CheckOutDate, “mm/dd/yyyy”) & _ “#) AND (CheckOutDate > #” & _ Format(Me.CheckInDate, “mm/dd/yyyy”) & “#) AND (RequestID <> “ & _ varNum & “)”)) Then If vbNo = MsgBox(“You already have a room request “ & _ “that overlaps the dates you have “ & _ “requested. Are you sure you want to make this request?”, _ vbQuestion + vbYesNo + vbDefaultButton2, gstrAppTitle) Then Cancel = True Exit Sub End If End If Il codice utilizza la funzione DLookup per vedere se esiste un'altra prenotazione (ma con un differente RequestID) per lo stesso impiegato, con date che si sovrappongono. I criteri richiedono che ogni record abbia una data di check-in anteriore alla data di check-out 365 richiesta (un dipendente può legittimamente lasciare una stanza e quindi registrarsi di nuovo nella stessa data) e una data di check-out successiva alla data di check-in richiesta. Potresti essere tentato di creare criteri più complessi, che verificano tutte le combinazioni di prenotazioni che si sovrappongono con l'inizio o con la fine del periodo richiesto, che si estendono sull'intero periodo richiesto oppure che sono contenute interamente all'interno del periodo richiesto, ma i due semplici test sono sufficienti. Controllo dell'ordine di tabulazione su una maschera a più pagine Puoi creare una maschera a più pagine come metodo per gestire la visualizzazione di dati che non possono essere visualizzati tutti insieme sullo schermo del computer. Puoi inoltre controllare l'ordine di tabulazione sulla maschera, impostando la proprietà Sequenza della maschera a Pagina corrente. Uno svantaggio di questo approccio è l'impossibilità di utilizzare i tasti Tab o Maiusc+Tab per passare ad altre pagine o record. A tal fine, devi infatti utilizzare i tasti PagSu e PagGiù o i selettori di record. Per ripristinare questa funzionalità, puoi impostare la proprietà Sequenza su Tutti i record, ma alcuni strani fatti possono verificarsi se non aggiungi il codice per gestire l'allineamento della pagina. Per vedere cosa accade, apri la maschera frmXmplContactsPages del database Conrad Systems Contacts (Contacts.accdb) nel riquadro di spostamento. Premi PagGiù per spostarti sul campo Home Address dell'ultimo contatto; fatto ciò, premi una volta Maiusc+Tab. Lo schermo dovrebbe essere simile a quello della figura 7-11. Figura 7-11 Quando premi Maiusc+Tab nel campo Home Address in frmXmplContactsPages, la pagina della maschera non viene allineata correttamente. Se lasci la proprietà Sequenza impostata a Tutti i record o a Record corrente, la tabulazione oltre i limiti della pagina causa un cattivo allineamento, a meno che non aggiungi codice specifico per gestire l'allineamento stesso. Ciò che accade è che Access scorre la visualizzazione della maschera solo per mostrare il controllo sul quale sei posizionato dopo Capitolo 7 Controllo dell'ordine di tabulazione su una maschera a più pagine 366 Capitolo 7 Automazione di un'applicazione tramite Visual Basic Capitolo 7 aver premuto il tasto Tab (in questo esempio ti sei posizionato sulla casella di testo Notes). Apri la maschera frmContactsPages che contiene il codice per risolvere questo problema e prova lo stesso esercizio. Dovresti scoprire che Maiusc+Tab ti posiziona sul campo Notes, ma la maschera scorre per mostrare l'intera prima pagina. Per consentire la tabulazione oltre il limite di una pagina, mantenendo un corretto allineamento della pagina, sono necessarie alcune routine nell'evento Invio (Enter) per il primo e l'ultimo controllo che può ricevere l'attivazione, su ciascuna pagina. Se esamini il codice della maschera frmContactsPages, troverai queste quattro routine: Private Sub ContactID_Enter() ‘ If tabbing forward into this field from previous record ‘ align page 1 Me.GoToPage 1 End Sub Private Sub HomeAddress_Enter() ‘ If tabbing forward into this field, align page 2 Me.GoToPage 2 End Sub Private Sub Notes_Enter() On Error Resume Next ‘ If tabbing backward into the last control on page 1, align it Me.GoToPage 1 End Sub Private Sub Photo_Enter() On Error Resume Next ‘ If tabbing backward into the last control on page 2, align it Me.GoToPage 2 End Sub Si potrebbe obiettare che questo è uno dei segmenti di codice più semplice nei database di esempio, ma questa attenzione per il dettaglio renderà gli utenti della tua applicazione molto felici. Nota Il codice viene eseguito anche quando premi Maiusc+Tab nei controlli ContactID e HomeAddress, quando passi ai controlli Notes o Photo, oppure quando fai clic in qualunque controllo. Access si rende conto che la maschera è già sulla pagina richiesta in ciascun caso, e quindi non fa niente. Automazione della selezione dei dati 367 Una delle attività più comuni da automatizzare in un'applicazione di database è il filtro dei dati. Quando un database contiene migliaia di record, gli utenti in genere devono lavorare con pochi record alla volta; se le maschere di modifica visualizzano sempre tutti i record, le prestazioni possono soffrirne enormemente. È buona norma consentire all'utente di specificare facilmente un sottoinsieme dei record; in questa sezione sono esaminati quattro metodi per farlo. Utilizzo di una casella di riepilogo a scelta multipla In Windows e in Access lavori sempre con le caselle di riepilogo. L'elenco di file in Esplora risorse e il riquadro di spostamento di Access 2010 sono caselle di riepilogo, come lo è anche l'elenco delle proprietà in qualsiasi scheda della finestra delle proprietà. Nell'elenco della finestra delle proprietà, puoi selezionare una sola proprietà alla volta. se fai clic su un'altra proprietà, la precedente viene deselezionata. Questa è una casella di riepilogo semplice. In Esplora risorse puoi selezionare un file, puoi selezionare più file non contigui, tenendo premuto il tasto Ctrl mentre fai clic, oppure puoi selezionare un intervallo di file, tenendo premuto il tasto Maiusc mentre fai clic; questa è una casella di riepilogo a scelta multipla. Supponi di utilizzare l'applicazione Conrad Systems Contacts (Contacts.accdb) e di essere interessato a esaminare i dettagli di molti contatti, ma di voler molto raramente esaminare l'intero elenco. Avvia l'applicazione aprendo la maschera frmSplash, seleziona John Viescas come User Name, fai clic su Sign On (non è necessaria alcuna password) Fai clic sul pulsante Contacts nella maschera del pannello comandi principale; fatto ciò, l'applicazione apre la maschera Select Contacts (frmContactList). Come mostrato nella figura 7-12, la maschera frmContactList contiene una casella di riepilogo a scelta multipla. Nota Se l'opzione Don't Show Contact List è selezionata nel profilo utente di John, non vedrai la finestra di dialogo Select Contacts. Se la maschera Contacts viene aperta quando fai clic sul pulsante Contacts nel pannello comandi principale, chiudila e fai clic sul pulsante Users. Deselezona l'opzione Clear the Don't Show Contact List nel profilo di John, salva il record, e chiudi la maschera. Dovresti ora vedere la finestra di dialogo Select Contacts, quando fai clic sul pulsante Contacts sul pannello comandi principale. Capitolo 7 Automazione della selezione dei dati 368 Capitolo 7 Automazione di un'applicazione tramite Visual Basic Capitolo 7 Figura 7-12 Puoi selezionare più record di contatti da modificare nella maschera frmContactList. In questa casella di riepilogo i contatti sono mostrati in ordine alfabetico per cognome, e l'elenco è associato al campo ContactID della tabella sottostante. Puoi modificare ogni singolo contatto semplicemente facendo doppio clic sul nome della persona. Puoi spostare l'evidenziazione in su o in giù, per mezzo dei tasti freccia. Puoi anche digitare la prima lettera del cognome di un contatto per saltare al contatto successivo il cui cognome comincia con quella lettera. Puoi tenere premuto il tasto Maiusc e utilizzare i tasti freccia per estendere la selezione a più nomi. Puoi infine tenere premuto il tasto Maiusc, o il tasto Ctrl, e utilizzare il mouse per selezionare più nomi. La figura 7-12 mostra tre contatti selezionati per mezzo del tasto Ctrl e del mouse. Quando fai clic su Edit, viene aperta la maschera frmContacts con solo i record che hai selezionato. La didascalia a destra della casella del numero di record indica che sono disponibili tre record e che il recordset è filtrato, come mostrato nella figura 7-13. 369 Capitolo 7 Automazione della selezione dei dati Figura 7-13 Dopo aver selezionato i record che desideri modificare nella maschera frmContactList, l'applicazione apre la maschera frmContacts e visualizza solo quei record. Per capire come funziona, devi esaminare la struttura della maschera frmContactList. Fai clic su Exit sul pannello comandi principale, per tornare al riquadro di spostamento. Se l'applicazione si offre di creare un backup, fai clic su Sì nella finestra messaggio "Are you sure you want to exit?", quindi fai clic su No. Seleziona frmContactList e apri la maschera nella visualizzazione Struttura, come mostrato nella figura 7-14. Fai clic sul controllo casella di riepilogo e apri la Finestra delle proprietà per vedere come è definita la casella di riepilogo. La casella di riepilogo utilizza due colonne dalla query qlkpContacts, nasconde ContactID (la chiave primaria che consente una ricerca veloce) nella prima colonna e visualizza il nome del contatto nella seconda colonna. L'aspetto più importante di questa casella di riepilogo è che la sua proprietà Selezione multipla è impostata a Estesa. L'impostazione estesa offre le funzionalità complete Ctrl+Clic o Maiusc+Clic che vedi nella maggior parte delle caselle di riepilogo in Windows. Il valore predefinito per questa proprietà è Nessuna, che consente di selezionare solo un valore alla volta. Puoi impostarla su Semplice se desideri selezionare o deselezionare più valori per mezzo del mouse o della barra spaziatrice. 370 Capitolo 7 Automazione di un'applicazione tramite Visual Basic Capitolo 7 Figura 7-14 La casella di riepilogo a scelta multipla nella maschera frmContactList ha la proprietà Selezione multipla impostata su Estesa. Se scorri le proprietà sulla scheda Evento, troverai una routine evento definita per l'evento Su doppio clic. Il codice per questa routine evento, chiamata quando fai doppio clic su un elemento nella casella di riepilogo, esegue solo la routine cmdSome_Click. Fai clic destro sul pulsante di comando cmdSome (quello la cui didascalia dice Edit), e seleziona Genera evento nel menu di scelta rapida, per accedere alla routine cmdSome_Click che esegue tutto il lavoro. Private Sub cmdSome_Click() Dim strWhere As String, varItem As Variant ‘ Request to edit items selected in the list box ‘ If no items selected, then nothing to do If Me!lstCName.ItemsSelected.Count = 0 Then Exit Sub ‘ Loop through the items selected collection For Each varItem In Me!lstCName.ItemsSelected ‘ Grab the ContactID column for each selected item strWhere = strWhere & Me!lstCName.Column(0, varItem) & “,” Next varItem ‘ Throw away the extra comma on the “IN” string strWhere = Left$(strWhere, Len(strWhere) - 1) ‘ Open the contacts form filtered on the selected contacts strWhere = “[ContactID] IN (“ & strWhere & “) And (Inactive = False)” DoCmd.OpenForm FormName:=”frmContacts”, WhereCondition:=strWhere DoCmd.Close acForm, Me.Name End Sub Quando imposti la proprietà Selezione multipla di una casella di riepilogo a qualcosa di diverso da Nessuna, puoi esaminare l'insieme ItemsSelected del controllo, per determinare cosa è stato selezionato. Per prima cosa, il codice Visual Basic della routine cmdSome_Click controlla la proprietà Count della raccolta ItemsSelected del controllo per stabilire se è stato selezionato qualcosa. Se il valore di Count è 0, non ci sono operazioni di eseguire e la routine si interrompe. 371 L'insieme ItemsSelected è composto da valori di tipo variant, ciascuno dei quali fornisce un indice a un elemento evidenziato nella casella di riepilogo. Il ciclo For Each chiede a Visual Basic di ciclare su tutti i valori variant disponibili nell'insieme, uno alla volta. All'interno del ciclo il codice utilizza il valore del tipo variant per recuperare il ContactID dall'elenco. Le caselle di riepilogo hanno anche una proprietà Column; puoi fare riferimento a tutti i valori nell'elenco utilizzando un'istruzione quale Me.ListBoxName.Column(ColumnNum, RowNum) dove NomeCasellaRiepilogo è il nome del controllo casella di riepilogo, NumColonna è il numero di colonna relativo (la prima colonna è 0, la seconda è 1, e così via) e NumRiga è il numero di riga relativo (anch'esso comincia da 0). I valori di tipo variant nell'insieme ItemsSelected ritornano il numero di riga relativo. Questo codice Visual Basic utilizza la colonna 0 e i valori nell'insieme ItemsSelected per aggiungere tutti i ContactID selezionati a una variabile di tipo stringa, separati da virgole. Un elenco di valori separati dal punto e virgola è ideale per una clausola IN. Dopo aver recuperato tutti i ContactID, l'istruzione successiva rimuove la virgola finale dalla stringa. La clausola Where finale include un ulteriore criterio, per visualizzare solo i contatti attivi. Il comando DoCmd.OpenForm utilizza la stringa risultante, per creare una clausola filtro all'apertura della maschera. Infine, la macro chiude la maschera CityInformation. (Me. Name è il nome della maschera attuale.) Utilizzo di una query in base a maschera personalizzata Supponi di voler eseguire una ricerca più complessa sulla maschera frmContacts, utilizzando criteri quali il tipo di contatto, la società o i prodotti disponibili, invece che semplicemente il nome del contatto. Potresti insegnare ai tuoi utenti come utilizzare la funzionalità Filtro in base a maschera, per creare la ricerca, oppure potresti utilizzare Filtro in base a maschera per costruire facilmente criteri OR multipli su semplici test. Ma se desideri per esempio trovare tutti i contatti che possiedono l'edizione Single User oppure che hai contattato entro certe date, non vi è modo di costruire questa richiesta utilizzando le funzionalità di filtro standard. Infatti, quando definisci un filtro per una sottomaschera, come ad esempio la sottomaschera Events della maschera frmContacts tramite la funzionalità Filtro in base a maschera, filtri solo le righe della sottomaschera. Così facendo non puoi trovare i contatti che hanno solo una riga di sottomaschera corrispondente. L'unica soluzione è dunque quella di fornire una query in base a maschera personalizzata, che fornisce le opzioni per eseguire una ricerca su tutti i campi importanti, e poi creare la clausola Where per risolvere il problema della ricerca utilizzando il codice Visual Basic. Apri l'applicazione Conrad Systems Contacts Se sei tornato al riquadro di spostamento, puoi avviare l'applicazione aprendo frmSplash. connettiti, fai clic sul pulsante Contacts sul pannello comandi principale e poi fai clic sul pulsante Search, nella finestra di dialogo Select Contacts. A questo punto dovresti vedere la maschera fdlgContactSearch, come mostrato nella figura 7-15. Capitolo 7 Automazione della selezione dei dati 372 Capitolo 7 Automazione di un'applicazione tramite Visual Basic Capitolo 7 Figura 7-15 Per eseguire una ricerca complessa, puoi progettare una query in base a maschera personalizzata. Prova a selezionare i contatti il cui cognome comincia con la lettera M e che hai contattato tra il 1° settembre 2010 e il 15 dicembre 2010 e che possiedono il prodotto BO$$ Single User (dall'elenco a discesa Owns Product). Facendo clic sul pulsante Search, dovresti vedere la maschera frmContacts aperta con due contatti visualizzati. Per capire come funziona, devi esplorare la struttura della maschera fdlgContactSearch. Passa al riquadro di spostamento (premendo F11) e apri la maschera nella visualizzazione Struttura. Dovresti vedere una finestra simile a quella mostrata nella figura 7-16. Nota che la maschera non è collegata ad alcuna origine record. i controlli non devono essere associati in modo da poter accettare qualunque criterio immesso dall'utente. Figura 7-16 Quando osservi la maschera fdlgContactSearch nella visualizzazione Struttura, puoi vedere che non possiede un'origine record. Automazione della selezione dei dati 373 Private Sub cmdSearch_Click() Dim varWhere As Variant, varDateSearch As Variant Dim rst As DAO.Recordset ‘ Initialize to Null varWhere = Null varDateSearch = Null ‘ First, validate the dates ‘ If there’s something in Contact Date From If Not IsNothing(Me.txtContactFrom) Then ‘ First, make sure it’s a valid date If Not IsDate(Format(Me.txtContactFrom, “mm/dd/yyyy”)) Then ‘ Nope, warn them and bail MsgBox “The value in Contact From is not a valid date.”, _ vbCritical, gstrAppTitle Exit Sub End If ‘ Now see if they specified a “to” date If Not IsNothing(Me.txtContactTo) Then ‘ First, make sure it’s a valid date If Not IsDate(Format(Me.txtContactTo, “mm/dd/yyyy”)) Then ‘ Nope, warn them and bail MsgBox “The value in Contact To is not a valid date.”, _ vbCritical, gstrAppTitle Exit Sub End If ‘ Got two dates, now make sure “to” is >= “from” If Format(Me.txtContactTo, “mm/dd/yyyy”) < _ Format(Me.txtContactFrom, “mm/dd/yyyy”) Then MsgBox “Contact To date must be greater than “ & _ “or equal to Contact From date.”, _ vbCritical, gstrAppTitle Exit Sub End If End If Else ‘ No “from” but did they specify a “to”? If Not IsNothing(Me.txtContactTo) Then ‘ Make sure it’s a valid date If Not IsDate(Format(Me.txtContactTo, “mm/dd/yyyy”)) Then ‘ Nope, warn them and bail MsgBox “The value in Contact To is not a valid date.”, _ vbCritical, gstrAppTitle Exit Sub End If End If End If ‘ If there’s something in Follow-up Date From If Not IsNothing(Me.txtFollowUpFrom) Then ‘ First, make sure it’s a valid date If Not IsDate(Format(Me.txtFollowUpFrom, “mm/dd/yyyy”)) Then ‘ Nope, warn them and bail MsgBox “The value in Follow-up From is not a valid date.”, _ Capitolo 7 Il grosso del lavoro avviene quando fai clic sul pulsante Search. Il codice della routine evento per l'evento Clic del pulsante Search è il seguente. 374 Capitolo 7 Automazione di un'applicazione tramite Visual Basic Capitolo 7 vbCritical, gstrAppTitle Exit Sub End If ‘ Now see if they specified a “to” date If Not IsNothing(Me.txtFollowUpTo) Then ‘ First, make sure it’s a valid date If Not IsDate(Format(Me.txtFollowUpTo, “mm/dd/yyyy”)) Then ‘ Nope, warn them and bail MsgBox “The value in Follow-up To is not a valid date.”, _ vbCritical, gstrAppTitle Exit Sub End If ‘ Got two dates, now make sure “to” is >= “from” If Format(Me.txtFollowUpTo, “mm/dd/yyyy”) < _ Format(Me.txtFollowUpFrom, “mm/dd/yyyy”) Then MsgBox “Follow-up To date must be greater than “ & _ “or equal to Follow-up From date.”, _ vbCritical, gstrAppTitle Exit Sub End If End If Else ‘ No “from” but did they specify a “to”? If Not IsNothing(Me.txtFollowUpTo) Then ‘ Make sure it’s a valid date If Not IsDate(Format(Me.txtFollowUpTo, “mm/dd/yyyy”)) Then ‘ Nope, warn them and bail MsgBox “The value in Follow-up To is not a valid date.”, _ vbCritical, gstrAppTitle Exit Sub End If End If End If ‘ OK, start building the filter ‘ If specified a contact type value If Not IsNothing(Me.cmbContactType) Then ‘ .. build the predicate varWhere = “(ContactType.Value = ‘” & Me.cmbContactType & “’)” End If ‘ Do Last Name next If Not IsNothing(Me.txtLastName) Then ‘ .. build the predicate ‘ Note: taking advantage of Null propagation ‘ so we don’t have to test for any previous predicate varWhere = (varWhere + “ AND “) & “([LastName] LIKE ‘” & _ Me.txtLastName & “*’)” End If ‘ Do First Name next If Not IsNothing(Me.txtFirstName) Then ‘ .. build the predicate varWhere = (varWhere + “ AND “) & “([FirstName] LIKE ‘” & _ Me.txtFirstName & “*’)” End If ‘ Do Company next If Not IsNothing(Me.cmbCompanyID) Then ‘ .. build the predicate ‘ Must use a subquery here because the value is in a linking table... varWhere = (varWhere + “ AND “) & _ “([ContactID] IN (SELECT ContactID FROM tblCompanyContacts “ & _ “WHERE tblCompanyContacts.CompanyID = “ & Me.cmbCompanyID & “))” End If ‘ Do City next If Not IsNothing(Me.txtCity) Then ‘ .. build the predicate ‘ Test for both Work and Home city varWhere = (varWhere + “ AND “) & “(([WorkCity] LIKE ‘” & _ Me.txtCity & “*’)” & _ “ OR ([HomeCity] LIKE ‘” & Me.txtCity & “*’))” End If ‘ Do State next If Not IsNothing(Me.txtState) Then ‘ .. build the predicate ‘ Test for both Work and Home state varWhere = (varWhere + “ AND “) & “(([WorkStateOrProvince] LIKE ‘” & _ Me.txtState & “*’)” & _ “ OR ([HomeStateOrProvince] LIKE ‘” & Me.txtState & “*’))” End If ‘ Do Contact date(s) next -- this is a toughie ‘ because we want to end up with one filter on the subquery table ‘ for both Contact Date range and FollowUp Date range ‘ Check Contact From first If Not IsNothing(Me.txtContactFrom) Then ‘ .. build the predicate varDateSearch = “tblContactEvents.ContactDateTime >= #” & _ Format(Me.txtContactFrom, “mm/dd/yyyy”) & “#” End If ‘ Now do Contact To If Not IsNothing(Me.txtContactTo) Then ‘ .. add to the predicate, but add one because ContactDateTime includes ‘ a date AND a time varDateSearch = (varDateSearch + “ AND “) & _ “tblContactEvents.ContactDateTime < #” & _ CDate(Format(Me.txtContactTo, “mm/dd/yyyy”)) + 1 & “#” End If ‘ Now do Follow-up From If Not IsNothing(Me.txtFollowUpFrom) Then ‘ .. add to the predicate varDateSearch = (varDateSearch + “ AND “) & _ “tblContactEvents.ContactFollowUpDate >= #” & _ Format(Me.txtFollowUpFrom, “mm/dd/yyyy”) & “#” End If ‘ Finally, do Follow-up To If Not IsNothing(Me.txtFollowUpTo) Then ‘ .. add to the predicate varDateSearch = (varDateSearch + “ AND “) & _ “tblContactEvents.ContactFollowUpDate <= #” & _ Format(Me.txtFollowUpTo, “mm/dd/yyyy”) & “#” End If ‘ Did we build any date filter? If Not IsNothing(varDateSearch) Then 375 Capitolo 7 Automazione della selezione dei dati 376 Capitolo 7 Automazione di un'applicazione tramite Visual Basic Capitolo 7 ‘ OK, add to the overall filter ‘ Must use a subquery here because the value is in a linking table... varWhere = (varWhere + “ AND “) & _ “([ContactID] IN (SELECT ContactID FROM tblContactEvents “ & _ “WHERE “ & varDateSearch & “))” End If ‘ Do Product If Not IsNothing(Me.cmbProductID) Then ‘ .. build the predicate ‘ Must use a subquery here because the value is in a linking table... varWhere = (varWhere + “ AND “) & _ “([ContactID] IN (SELECT ContactID FROM tblContactProducts “ & _ “WHERE tblContactProducts.ProductID = “ & Me.cmbProductID & “))” End If ‘ Finally, do the Inactive check box If (Me.chkInactive = False) Then ‘ Build a filter to exclude inactive contacts varWhere = (varWhere + “ AND “) & “(Inactive = False)” End If ‘ Check to see that we built a filter If IsNothing(varWhere) Then MsgBox “You must enter at least one search criteria.”, _ vbInformation, gstrAppTitle Exit Sub End If ‘ Open a recordset to see if any rows returned with this filter Set rst = CurrentDb.OpenRecordset(“SELECT * FROM tblContacts “ & _ “WHERE “ & varWhere) ‘ See if found none If rst.RecordCount = 0 Then MsgBox “No Contacts meet your criteria.”, vbInformation, gstrAppTitle ‘ Clean up recordset rst.Close Set rst = Nothing Exit Sub End If ‘ Hide me to fix later focus problems Me.Visible = False ‘ Move to last to find out how many rst.MoveLast ‘ If 5 or less or frmContacts already open, If (rst.RecordCount < 6) Or IsFormLoaded(“frmContacts”) Then ‘ Open Contacts filtered ‘ Note: if form already open, this just applies the filter DoCmd.OpenForm “frmContacts”, WhereCondition:=varWhere ‘ Make sure focus is on contacts Forms!frmContacts.SetFocus Else ‘ Ask if they want to see a summary list first If vbYes = MsgBox(“Your search found “ & rst.RecordCount & _ “ contacts. “ & _ “Do you want to see a summary list first?”, _ vbQuestion + vbYesNo, gstrAppTitle) Then ‘ Show the summary DoCmd.OpenForm “frmContactSummary”, WhereCondition:=varWhere 377 ‘ Make sure focus is on contact summary Forms!frmContactSummary.SetFocus Else ‘ Show the full contacts info filtered DoCmd.OpenForm “frmContacts”, WhereCondition:=varWhere ‘ Make sure focus is on contacts Forms!frmContacts.SetFocus End If End If ‘ Done DoCmd.Close acForm, Me.Name ‘ Clean up recordset rst.Close Set rst = Nothing End Sub La prima parte della routine convalida i valori From e To delle date di contatto e follow-up. Se vi sono date non valide, oppure se una data From è maggiore della corrispondente data To, il codice visualizza un appropriato messaggio di avvertimento e si interrompe. I successivi segmenti di codice creano una stringa WHERE, esaminando i controlli non associati, uno alla volta. Se il campo corrispondente è una stringa, il codice crea un test utilizzando il predicato LIKE, in modo che qualunque cosa l'utente immetta possa corrispondere a qualunque parte del campo nella tabella sottostante; non tutti i campi sono però stringhe. Quando la funzione aggiunge una clausola durante la creazione della stringa WHERE, inserisce la parola chiave AND tra le clausole, se esistono già altre clausole. Poiché la variabile contenente la clausola WHERE è di tipo Variant inizializzata a Null, il codice può utilizzare una concatenazione con l'operatore + per aggiungere facoltativamente la parola chiave AND. Nota che poiché il campo ContactType è un campo a valori multipli, il codice cerca in modo specifico la proprietà Valore del campo. L'origine record per la maschera frmContacts non include direttamente le informazioni sugli eventi o sui prodotti del contatto, e quindi la routine deve creare un predicato utilizzando una sottoquery, se chiedi una ricerca basata sulla data Contact, sulla data Follow-up oppure sul prodotto. Nel caso della data Contact o Follow-up, il codice crea una stringa di filtro (varDateSearch), poiché entrambi i campi sono nella stessa tabella (tblContactEvents). Se richiedi la verifica di un intervallo di date, il codice crea i criteri utilizzando una sottoquery, che trova il ContactID dai record della tabella tblContactEvents che rientrano nell'intervallo. Nel caso di una ricerca per prodotto, il codice crea i criteri utilizzando una sottoquery, che trova il ContactID dai record della tabella tblContactProducts, che corrispondono al prodotto che hai selezionato. Se non selezioni la casella di controllo Include Inactive Contacts, il codice aggiunge un test per includere solo i record attivi. Dopo aver esaminato tutti i possibili valori del filtro che l'utente potrebbe aver immesso, il codice verifica se c'è qualcosa nella stringa del filtro (varWhere). Non serve a niente aprire la maschera senza un filtro, e quindi il codice visualizza un messaggio ed esce, lasciando la maschera aperta per consentire all'utente di riprovare. Capitolo 7 Automazione della selezione dei dati 378 Capitolo 7 Automazione di un'applicazione tramite Visual Basic Capitolo 7 La parte finale della routine crea un semplice recordset sulla tabella tblContacts, utilizzato nelle maschere frmContacts e frmContactSummary, applicando la clausola WHERE creata dal codice nella prima parte della routine. Se non trova nessun record, utilizza la funzione MsgBox per informare l'utente, e poi lascia all'utente la possibilità di riprovare. Quando apri la prima volta un oggetto Recordset nel codice, la sua proprietà RecordCount è 0, se il recordset è vuoto, ed è maggiore di 0, se il recordset contiene record. La proprietà RecordCount di un oggetto Recordset indica il numero di righe visitate e non il numero di righe nel recordset; pertanto, se trova delle righe, la routine passa all'ultima riga del recordset temporaneo per ottenere un conteggio accurato. Quando il conteggio di record è maggiore di 5 e la maschera frmContacts non è già aperta, la routine utilizza la funzione MsgBox per offrire all'utente la possibilità di vedere un riepilogo dei record che si trovano nella maschera frmContactSummary, oppure di visualizzare i record che si trovano direttamente nella maschera frmContacts (come notato in precedenza, entrambe le maschere utilizzano la stessa origine record, e dunque il codice può applicare il filtro da esso creato, all'apertura di entrambe le maschere). Esamineremo il funzionamento della maschera frmContactSummary nella prossima sezione. Selezione da un elenco di riepilogo Se più di cinque righe soddisfano i criteri immessi, l'utente deve fare una scelta, come hai visto nella routine cmdSearch_Click nella sezione precedente. Per esaminare questa caratteristica in maggior dettaglio, assicurati che la maschera frmContacts non sia aperta, e ricerca i contatti con Contact Type uguale a Customer, nella maschera fdlgContactSearch. Il risultato dovrebbe essere simile a quello della figura 7-17, in cui vi sono 30 contatti di tipo Customer. Figura 7-17 Questa finestra messaggio appare quando la routine cmdSearch_Click restituisce più di cinque righe. Se fai clic su Sì, la routine cmdSearch_Click apre la maschera Contact Search Summary (frmContactSummary), come mostrato nella figura 7-18. Puoi posizionarti su qualunque riga e attivarla (assicurati che l'indicatore di selettore di riga stia puntando a quella riga), quindi puoi fare clic sul pulsante View Details per aprire la maschera frmContacts e visualizzare i dettagli per il contatto selezionato. Questo è un modo molto efficiente per aiutare l'utente a restringere una ricerca a un particolare contatto. 379 Capitolo 7 Automazione della selezione dei dati Figura 7-18 Puoi selezionare uno specifico contatto dalla maschera di riepilogo di una ricerca. Puoi inoltre fare doppio clic sul campo Contact ID o sul campo Name, per vedere i dettagli relativi a quel contatto. Poiché questo elenco è già filtrato in base ai criteri specificati nella maschera fdlgContactSearch, il codice che risponde alla tua richiesta crea un semplice filtro su Contact ID per rendere più efficiente l'apertura della maschera frmContacts. Il codice di questa maschera, che risponde alla tua richiesta nell'evento Click del pulsante di comando Details, è il seguente: Private Sub Details_Click() Dim strFilter As String ‘ They asked for details (or double-clicked one of the controls) ‘ Set up the filter strFilter = “(ContactID = “ & Me.ContactID & “)” ‘ Open contacts filtered on the current row DoCmd.OpenForm FormName:=”frmContacts”, WhereCondition:=strFilter ‘ Close me DoCmd.Close acForm, Me.Name ‘ Put focus on contacts Forms!frmContacts.SetFocus End Sub Filtro di un elenco con un altro elenco Come puoi aver notato durante le modifiche ai prodotti nella scheda Products della maschera frmContacts (figura 7-1), puoi prima scegliere un tipo di prodotto per restringere l'elenco dei prodotti, per poi scegliere il prodotto desiderato. Nell'applicazione di esempio ci sono solo undici prodotti, e quindi restringere la scelta dei prodotti non è molto utile, ma puoi immaginare come una funzionalità come questa sarebbe assolutamente necessaria in un'applicazione con migliaia di prodotti disponibili per la vendita. Il segreto è dato dal fatto che l'origine riga per la casella combinata Product è una query con parametri, che filtra i prodotti in base al tipo di prodotto scelto. Quando utilizzi questa 380 Capitolo 7 Automazione di un'applicazione tramite Visual Basic Capitolo 7 tecnica in una maschera in visualizzazione Maschera singola, tutto ciò che devi fare è rieseguire la query sulla casella combinata filtrata (in questo caso la casella combinata Product), quando l'utente passa a un nuovo record (nell'evento Corrente della maschera), e rieseguire la query, quando l'utente sceglie un differente valore nella casella combinata che fornisce il valore del filtro (nell'evento DopoAggiornamento della casella combinata che fornisce il valore del filtro). L'utilizzo di questa tecnica per una maschera in visualizzazione Maschera continua è tuttavia notevolmente più complesso. Anche se puoi vedere più righe nella visualizzazione Maschera continua, sulla maschera c'è in realtà una sola copia di ciascun controllo. Se ripeti la query sulla casella combinata Product ogni volta che ti sposti in una nuova riga, il nome del prodotto visualizzato nelle altre righe che hanno un differente tipo di prodotto appare vuoto. Quando il valore in una riga non corrisponde a un valore nell'elenco, ottieni un risultato vuoto, non il valore reale del campo. Per risolvere questo problema devi includere il nome da visualizzare nel recordset per la maschera, quindi sovrapporre attentamente ciascuna casella combinata con una casella di testo che visualizzi sempre il valore corretto indipendentemente dal filtro. Per vedere come abbiamo fatto, puoi aprire la maschera fsubContactProducts in visualizzazione Struttura. La figura 7-19 mostra la maschera con le due caselle di testo che si sovrappongono (CategoryDescription e ProductName) estratte dalle caselle combinate sottostanti (Unbound e ProductID). Figura 7-19 Puoi risolvere un problema di visualizzazione di una casella combinata filtrata mediante la sovrapposizione di caselle di testo. L'origine controllo della casella combinata Product è in realtà il campo ProductID, ma la casella combinata visualizza il campo ProductName. La casella combinata Product Type inoltre non è associata a nessun campo (non esiste un campo CategoryDescription nella tabella tblContactProducts), ma visualizza il campo CategoryDescription dalla tabella di ricerca. Per far funzionare tutto devi includere i campi ProductName e CategoryDescription nell'origine record per questa maschera. Queste caselle di testo devono visualizzare i valori sovrapposti senza tuttavia essere aggiornate dall'utente. Esse hanno la proprietà Bloccato impostata su Sì per impedire l'aggiornamento, e la proprietà Seleziona con tabulazione impostata su No per fare in modo che l'utente possa premere il tasto Tab per posizionarsi sulle caselle combinate sottostanti e non su queste caselle di testo. La figura 7-20 mostra la query qryContactProducts, che è l'origine per questa maschera. 381 Capitolo 7 Automazione della selezione dei dati Figura 7-20 La query qryContactProducts fornisce i campi ProductName e CategoryDescription necessari da una tabella correlata, in modo da permettere di visualizzare i valori. Perché tutto funzioni, molte routine evento assicurano che vengano attivati i campi necessari e che venga ripetuta la query sulla casella combinata filtrata Product. Il codice della maschera fsubContactProducts che svolge questo compito è il seguente: Private Sub CategoryDescription_GotFocus() ‘ We have some tricky “overlay” text boxes here that ‘ shouldn’t get the focus. Move focus to the underlying ‘ combo box if that happens. Me.cmbCategoryDescription.SetFocus End Sub Private Sub cmbCategoryDescription_AfterUpdate() ‘ If they pick a new Category, then requery the ‘ product list that’s filtered on category Me.cmbProductID.Requery ‘ Set the Product to the first row in the new list Me.cmbProductID = Me.cmbProductID.ItemData(0) ‘ .. and signal Product after update. cmbProductID_AfterUpdate End Sub Private Sub Form_Current() ‘ If we have a valid Category Description on this row... If Not IsNothing(Me.CategoryDescription) Then ‘ Then make sure the unbound combo is in sync. Me.cmbCategoryDescription = Me.CategoryDescription End If ‘ Requery the product list to match the current category Me.cmbProductID.Requery If (Me.Invoiced = True) Then Me.cmbProductID.Locked = True Me.cmbCategoryDescription.Locked = True Me.DateSold.Locked = True Me.SoldPrice.Locked = True Me.RegistrationCode.Locked = True 382 Capitolo 7 Automazione di un'applicazione tramite Visual Basic Else Capitolo 7 Me.cmbProductID.Locked = False Me.cmbCategoryDescription.Locked = False Me.DateSold.Locked = False Me.SoldPrice.Locked = False Me.RegistrationCode.Locked = False End If End Sub Private Sub ProductName_GotFocus() ‘ We have some tricky “overlay” text boxes here that ‘ shouldn’t get the focus. Move focus to the underlying ‘ combo box if that happens. Me.cmbProductID.SetFocus End Sub Come previsto, il codice ripete le query sulla casella combinata Product ogni volta che selezioni una nuova categoria (cmbCategoryDescription_AfterUpdate) o ti sposti su una nuova riga (Form_Current). Il codice mantiene inoltre sincronizzata la casella combinata non associata quando passi da una riga all'altra, a condizione che il record sottostante abbia una categoria valida. Ricorda che un nuovo record non ha una CategoryDescription correlata fino a quando non scegli un ProductID, pertanto il codice non aggiorna la casella combinata non associata su un nuovo record. Se infine cerchi di fare clic su CategoryDescription o ProductName, il codice GotFocus ti sposta sulla casella combinata sottostante. Perché non abbiamo semplicemente impostato la proprietà Abilitato a Sì, per CategoryDescription e ProductName? Se lo facessimo, non potresti mai fare clic sulle caselle combinate della categoria o del prodotto, poiché la casella di testo disabilitata, sovrapposta, ti bloccherebbe. Nota Per vedere come appare la casella combinata filtrata senza la sovrapposizione della casella di testo, crea una copia di backup di Contacts.accdb, apri la maschera fsubContactProducts nella visualizzazione Struttura, sposta le caselle di testo Category Description e Product Name in basso come mostrato nella figura 7-19 e infine salva la maschera. Fatto ciò, apri la maschera frmContacts e fai clic sulla scheda Products. Collegamento ai dati correlati in un'altra maschera o report Ora che sai come creare un filtro, per limitare i dati visti dall'utente, puoi probabilmente supporre che l'utilizzo di un filtro rappresenti un buon metodo per aprire un'altra maschera, o un altro report, che visualizza informazioni correlate al record corrente o all'insieme di record filtrati nella maschera corrente. Questa sezione mostra come farlo per le maschere e per i report; più avanti in questa sezione imparerai a utilizzare gli eventi nei moduli di classe, per creare collegamenti sofisticati. Collegamento ai dati correlati in un'altra maschera o report 383 La maschera frmContactSummary (vista nella figura 7-18) utilizza un semplice filtro per eseguire un collegamento tra il record selezionato in quella maschera e i dettagli completi nella maschera frmContacts. Puoi trovare codice simile nella maschera fsubCompanyContacts, utilizzata come sottomaschera della maschera frmCompanies. La figura 7-21 mostra la maschera frmCompanies e i pulsanti Edit This su quella sottomaschera. Figura 7-21 Puoi fornire un collegamento dalla maschera Companies/Organizations ai dettagli riguardanti un particolare contatto. Per vedere i dettagli per un particolare contatto, l'utente fa clic sul pulsante Edit This, sul record del contatto scelto, e il codice apre la maschera frmContacts con quel contatto visualizzato. Il codice di questo pulsante è il seguente: Private Sub cmdEdit_Click() ‘ Open Contacts on the related record DoCmd.OpenForm “frmContacts”, WhereCondition:=”ContactID = “ & Me.ContactID End Sub Il codice nell'evento Corrente della maschera impedisce all'utente di fare clic sul pulsante quando si trova su un nuovo record che non ha un ContactID: Private Sub Form_Current() ‘ Disable “edit this” if on a new row Me.cmdEdit.Enabled = Not (Me.NewRecord) End Sub Se la proprietà Abilitato del pulsante è impostata a False il pulsante viene visualizzato oscurato, e l'utente non può fare clic su esso. Capitolo 7 Collegamento delle maschere per mezzo di un filtro 384 Capitolo 7 Automazione di un'applicazione tramite Visual Basic Collegamento a un report per mezzo di un filtro Capitolo 7 Esaminiamo ora l'utilizzo della tecnica del filtro per collegare informazioni correlate in un report. Apri la maschera frmInvoices nell'applicazione Conrad Systems Contacts (Contacts. accdb) e passa a una fattura che sembra interessante, ad esempio la numero 50. Fai clic sul pulsante Print per aprire la maschera Print Invoices (fdlgInvoicePrintOptions), la quale ti offre la possibilità di vedere la fattura corrente formattata in un report, visualizzare tutte le fatture non stampate in un report, visualizzare solo le fatture non stampate per il cliente corrente o stampare tutte le fatture al momento visualizzate nella maschera frmInvoices. Se desideri filtrare le fatture da visualizzare, puoi utilizzare Search. Seleziona l'opzione Current Invoice Only e fai nuovamente clic su Print per vedere la fattura in un report, come mostrato nella figura 7-22. La figura mostra la sequenza che vedi dopo aver fatto clic sul pulsante Print della maschera frmInvoices. La finestra di dialogo Print Invoices viene chiusa, dopo l'apertura del report. Figura 7-22 Puoi chiedere di stampare solo la fattura corrente nel database Conrad Systems Contacts. Il codice dell'evento Clic del pulsante Print nella maschera fdlgInvoicePrintOptions è il seguente: Private Sub cmdPrint_Click() Dim strFilter As String, frm As Form ‘ Set an error trap On Error GoTo cmdPrint_Error ‘ Get a pointer to the Invoices form Set frm = Forms!frmInvoices Select Case Me.optFilterType.Value ‘ Current Invoice Case 1 ‘ Set filter to open Invoice report for current invoice only strFilter = “[InvoiceID] = “ & frm!InvoiceID ‘ All unprinted invoices Case 2 ‘ Set filter to open all unprinted invoices strFilter = “[InvoicePrinted] = 0” ‘ Unprinted invoices for current company Case 3 ‘ Set filter to open unprinted invoices for current company strFilter = “[CompanyID] = “ & frm!cmbCompanyID & _ “ AND [InvoicePrinted] = 0” ‘ Displayed invoices (if filter set on form) Case 4 ‘ Check for a filter on the form If IsNothing(frm.Filter) Then ‘ Make sure they want to print all! If vbNo = MsgBox(“Your selection will print all “ & _ “Invoices currently in the “ & _ “database. Are you sure you want to do this?”, _ vbQuestion + vbYesNo + vbDefaultButton2, _ gstrAppTitle) Then Exit Sub End If ‘ Set “do them all” filter strFilter = “1 = 1” Else strFilter = frm.Filter End If End Select ‘ Hide me Me.Visible = False ‘ Have a filter now. Open the report on that filter DoCmd.OpenReport “rptInvoices”, acViewPreview, , strFilter ‘ Update the Print flag for selected invoices CurrentDb.Execute “UPDATE tblInvoices SET InvoicePrinted = -1 WHERE “ & _ strFilter ‘ Refresh the form to show updated Printed status frm.Refresh ‘ Execute the Current event on the form to make sure it is locked correctly frm.Form_Current cmdPrint_Exit: ‘ Clear the form object Set frm = Nothing ‘ Done DoCmd.Close acForm, Me.Name Exit Sub cmdPrint_Error: ‘ Got an error 385 Capitolo 7 Collegamento ai dati correlati in un'altra maschera o report 386 Capitolo 7 Automazione di un'applicazione tramite Visual Basic Capitolo 7 ‘ If Cancel, that means the filter produced no Invoices If Err = errCancel Then ‘ Exit - report will display “no records” message Resume cmdPrint_Exit End If ‘ Got unknown error - display and log MsgBox “Unexpected error while printing and updating print flags: “ & _ Err & “, “ & _ Error, vbCritical, gstrAppTitle ErrorLog Me.Name & “_Print”, Err, Error Resume cmdPrint_Exit End Sub Questa prima parte di questa routine imposta un riferimento alla maschera frmInvoices, perché sia più facile utilizzare InvoiceID o CompanyID e fare riferimento alle proprietà e ai metodi dell'oggetto della maschera. L'istruzione Select Case trova il pulsante di opzione selezionato dall'utente nella maschera fdlgInvoicePrintOption e crea l'appropriato filtro per il report. Nota che, se l'utente richiede la stampa di tutte le fatture attualmente visualizzate nella maschera, il codice verifica innanzitutto l'esistenza di un filtro applicato dall'utente sulla maschera frmInvoices e, se non trova nessun filtro, chiede se l'utente desidera stampare tutte le fatture. Il codice utilizza il filtro da esso creato (o il filtro corrente nella maschera frmInvoices) per aprire il report rptInvoices in Anteprima di stampa, quindi esegue un'istruzione SQL UPDATE per contrassegnare tutte le fatture stampate dall'utente. Se esamini il codice dell'evento Corrente della maschera frmInvoices, puoi notare che tutti i controlli sono bloccati, in modo che l'utente non possa aggiornare una fattura che è stata stampata. Sincronizzazione di due maschere per mezzo di un evento di classe Talvolta è utile dare all'utente la possibilità di aprire una maschera popup, che mostra altri dettagli relativi ad alcune informazioni visualizzate su un'altra maschera. Quando ti sposti da una riga all'altra nella maschera principale, sarebbe bello se la maschera che visualizza le informazioni aggiuntive rimanesse sincronizzata. L'evento Corrente di una maschera naturalmente ti consente di sapere quando ti sposti su una nuova riga. Nel database Wedding List creato con le macro (WeddingListMC.accdb), le macro eseguono un'elaborata operazione di filtro, per mantenere la maschera popup che visualizza le informazioni aggiuntive sulla città dell'invitato sincronizzata con la maschera principale. Farlo con le macro rappresenta tuttavia il metodo più difficile! L'applicazione Wedding List principale si trova nel database WeddingList.accdb, e utilizza Visual Basic per fornire tutta l'automazione. Con Visual Basic siamo stati in grado di dichiarare e di utilizzare un evento personalizzato nella maschera WeddingList, per segnalare alla maschera CityInformation se è aperta e se risponde agli eventi. Nell'evento Corrente della maschera WeddingList non dobbiamo preoccuparci se la maschera associata è aperta; il codice segnala semplicemente l'evento e lascia che la maschera City Information Collegamento ai dati correlati in un'altra maschera o report 387 Capitolo 7 si preoccupi di mantenersi sincronizzata con la maschera principale (l'utente può aprire la maschera City Information in ogni istante, facendo clic sul pulsante City Info sulla maschera Wedding List). Puoi vedere queste due maschere in azione nella figura 7-23. Figura 7-23 La maschera CityInformation si apre sopra la maschera principale WeddingList per visualizzare informazioni aggiuntive sulla città dell'invitato. Ecco il codice del modulo di classe WeddingList che rende disponibile un evento per inviare segnalazioni alla maschera CityInformation: Option Compare Database Option Explicit ‘ Event to signal we’ve moved to a new city Public Event NewCity(varCityName As Variant) ‘ End of Declarations Section Private Sub Form_Current() On Error GoTo Form_Current_Err ‘ Signal the city form to move to this city ‘ and pass the city name to the event RaiseEvent NewCity(Me!City) Form_Current_Exit: Exit Sub Form_Current_Err: MsgBox Error$ Resume Form_Current_Exit End Sub Private Sub cmdCity_Click() On Error GoTo cmdCity_Click_Err ‘ If the city form is not open, open it If Not IsFormLoaded(“CityInformation”) Then 388 Capitolo 7 Automazione di un'applicazione tramite Visual Basic Capitolo 7 DoCmd.OpenForm “CityInformation”, acNormal, , , acFormReadOnly, acHidden ‘ Give the other form a chance to “hook” our event DoEvents End If ‘ Signal the form we just opened RaiseEvent NewCity(Me!City) cmdCity_Click_Exit: Exit Sub cmdCity_Click_Err: MsgBox Error$ Resume cmdCity_Click_Exit End Sub Nella sezione Dichiarazioni del modulo abbiamo dichiarato una variabile di evento e ho indicato che abbiamo intenzione di passare un parametro (il nome della città) nell'evento. Nella routine evento Form_Current il codice utilizza RaiseEvent per passare il nome della città corrente a ogni altro modulo in ascolto. Il codice non deve preoccuparsi se altri moduli sono interessati a questo evento, ma segnala semplicemente l'evento quando appropriato, e poi termina Questo modo di procedere è simile a quello di Access; quando una maschera passa a un nuovo record, Access segnala l'evento Form_Current, ma non accade nulla se non hai inserito il codice di risposta all'evento. La variabile passata è dichiarata come tipo Variant, per gestire il caso in cui l'utente si sposta alla fine della nuova riga (in questo caso il controllo City sarà Null). Un pulsante di comando (cmdCity) sulla maschera WeddingList consente all'utente di aprire la maschera CityInformation. L'evento Clic di quel pulsante apre la maschera nascosta e utilizza la funzione DoEvents per dare alla maschera CityInformation la possibilità di essere aperta e di indicare che desidera ascoltare l'evento NewCity sulla maschera WeddingList. Dopo aver aspettato che la maschera CityInformation termini l'elaborazione, il codice genera l'evento per indicare a quella maschera la città nella riga corrente. La maschera CityInformation svolge tutto il lavoro (quando è aperta) per rispondere all'evento segnalato dalla maschera WeddingList e si sposta sulla riga corretta. Il codice è il seguente. Option Compare Database Option Explicit Dim WithEvents frmWedding As Form_WeddingList ‘ End of the Declarations Section Private Sub Form_Load() On Error GoTo Form_Load_Err ‘ If the wedding list form is open If IsLoaded(“WeddingList”) Then ‘ Then set to respond to the NewCity event Set frmWedding = Forms!WeddingList End If Form_Load_Exit: Exit Sub Form_Load_Err: MsgBox Error$ Resume Form_Load_Exit Collegamento ai dati correlati in un'altra maschera o report 389 Private Sub frmWedding_NewCity(varCityName As Variant) ‘ The Wedding List form has asked us to move to a ‘ new city via the NewCity event On Error Resume Next If IsNothing(varCityName) Then ‘ Hide me if city name is empty Me.Visible = False Else ‘ Reveal me if there’s a city name, and go ‘ find it Me.Visible = True Me.Recordset.FindFirst “[CityName] = “”” & _ varCityName & “””” End If End Sub Nella sezione Dichiarazioni puoi trovare una variabile oggetto chiamata frmWedding, che ha un tipo di dati uguale al nome del modulo di classe della maschera WeddingList. La parola chiave WithEvents indica che il codice in questo modulo di classe risponde agli eventi segnalati da qualunque oggetto assegnato a questa variabile. Quando la maschera viene aperta, la routine Form_Load controlla se la maschera WeddingList è aperta (nel caso tu abbia aperto questa maschera dal riquadro di spostamento). Se la maschera WeddingList è aperta, "aggancia" l'evento NewCity in quella maschera, assegnandolo alla variabile frmWedding. La routine frmWedding_NewCity risponde all'evento NewCity dell'oggetto frmWedding. Una volta che il codice dell'evento Load definisce frmWedding come un puntatore alla maschera WeddingList, questa routine viene eseguita ogni volta che il codice nel modulo di classe per quella maschera segnala l'evento NewCity con RaiseEvent. Il codice nella routine evento è molto semplice. Se il parametro CityName passato dall'evento contiene un valore Null o una stringa di lunghezza zero, la routine nasconde la maschera poiché non trova nulla da visualizzare. Se l'evento passa un nome di città valido, la routine utilizza il metodo FindFirst dell'oggetto Recordset di questa maschera per spostarsi alla città corretta. Nota In Access 2010, la proprietà Recordset di una maschera di un database Access (file .accdb) restituisce un recordset DAO (Data Access Objects). Per questo motivo, per individuare le righe nel recordset di una maschera dovresti utilizzare un metodo DAO FindFirst, invece di un metodo ADO Find. Capitolo 7 End Sub 390 Capitolo 7 Automazione di un'applicazione tramite Visual Basic Automazione di attività complesse Capitolo 7 Il codice Visual Basic più complesso esaminato finora in questo capitolo è la routine per la creazione di una clausola di ricerca a partire dai dati immessi nella maschera fdlgContactSearch. Tuttavia abbiamo solo cominciato a scalfire la superficie. Attivazione di un'attività da una maschera correlata Uno dei blocchi di codice più complessi nel database Conrad Systems Contacts è attivato dalla maschera fsubContactEvents, che fa parte della maschera frmContacts. Dopo essersi connesso correttamente, l'utente può aprire la maschera frmContacts, fare clic sulla scheda Events e aggiungere un evento che indica la vendita di un prodotto. Non appena l'utente salva il record, il codice della sottomaschera aggiunge automaticamente il prodotto al contatto, come mostrato nella figura 7-24. Figura 7-24 La registrazione dell'evento della vendita di un prodotto nella scheda Events comporta la vendita automatica del prodotto al contatto. Se esamini la struttura della maschera fsubContactEvents, puoi trovare alcune routine evento che individuano la creazione di un evento di vendita da parte dell'utente e che eseguono un comando SQL INSERT per creare la riga del prodotto correlato. Il codice è il seguente: Option Compare Database Option Explicit ‘ Flag to indicate auto-add of a product if new event requires it Dim intProductAdd As Integer ‘ Place to store Company Name on a product add Dim varCoName As Variant ‘ End of the Declarations Section Private Sub ContactEventTypeID_BeforeUpdate(Cancel As Integer) ‘ Did they pick an event that involves a software sale? ‘ NOTE: All columns in a combo box are TEXT If Me.ContactEventTypeID.Column(4) = “-1” Then ‘ Try to lookup this contact’s Company Name varCoName = DLookup(“CompanyName”, “qryContactDefaultCompany”, _ “ContactID = “ & Me.Parent.ContactID.Value) ‘ If not found, then disallow product sale If IsNothing(varCoName) Then MsgBox “You cannot sell a product to a Contact “ & _ “that does not have a “ & _ “related Company that is marked as the default for this Contact.” & _ “ Press Esc to clear your edits and click on the Companies tab “ & _ “to define the default Company for this Contact.”, _ vbCritical, gstrAppTitle Cancel = True End If End If End Sub Private Sub Form_BeforeUpdate(Cancel As Integer) ‘ Did they pick an event that involves a software sale? ‘ NOTE: All columns in a combo box are TEXT If Me.ContactEventTypeID.Column(4) = “-1” Then ‘ Do this only if on a new record or they changed the EventID value If (Me.NewRecord) Or (Me.ContactEventTypeID <> _ Me.ContactEventTypeID.OldValue) Then ‘ Set the add product flag ‘- product added by AfterUpdate code for safety intProductAdd = True End If End If End Sub Private Sub Form_AfterUpdate() Dim strSQL As String, curPrice As Currency, Dim lngProduct As Long, varCoID As Variant Dim rst As DAO.Recordset, strPreReqName As String ‘ See if we need to auto-add a product If (intProductAdd = True) Then ‘ Reset so we only do this once intProductAdd = False ‘ Set an error trap On Error GoTo Insert_Err ‘ Save the Product ID lngProduct = Me.ContactEventTypeID.Column(5) ‘ Fetch the product record Set rst = CurrentDb.OpenRecordset(“SELECT * FROM tblProducts “ & _ “WHERE ProductID = “ & lngProduct) ‘ Make sure we got a record If rst.EOF Then MsgBox “Could not find the product record for this sales event.” & _ “ Auto-create of “ & _ “product record for this contact has failed.”, _ vbCritical, gstrAppTitle 391 Capitolo 7 Automazione di attività complesse 392 Capitolo 7 Automazione di un'applicazione tramite Visual Basic Capitolo 7 rst.Close Set rst = Nothing GoTo Insert_Exit End If ‘ Check for prerequisite product If Not IsNull(rst!PreRequisite) Then ‘ Make sure contact owns the prerequisite product If IsNull(DLookup(“ProductID”, “tblContactProducts”, _ “ProductID = “ & rst!PreRequisite & “ And ContactID = “ & _ Me.Parent.ContactID)) Then ‘ Get the name of the prerequisite strPreReqName = DLookup(“ProductName”, “tblProducts”, _ “ProductID = “ & rst!PreRequisite) ‘ Display error MsgBox “This contact must own prerequisite product “ & _ strPreReqName & “ before you can sell this product.” & _ vbCrLf & vbCrLf & _ “Auto-create of product record for this contact has failed”, _ vbCritical, gstrAppTitle ‘ Bail rst.Close Set rst = Nothing GoTo Insert_Exit End If End If ‘ Save the price curPrice = rst!UnitPrice ‘ Done with the record - close it rst.Close Set rst = Nothing ‘ Now, find the default company for this contact varCoID = DLookup(“CompanyID”, “qryContactDefaultCompany”, _ “ContactID = “ & Me.Parent.ContactID.Value) ‘ If not found, then disallow product sale If IsNothing(varCoID) Then MsgBox “You cannot sell a product to a Contact who does not have a “ & _ “related Company that is marked as the default for this Contact.”, _ vbCritical, gstrAppTitle GoTo Insert_Exit End If ‘ Set up the INSERT command strSQL = “INSERT INTO tblContactProducts “ & _ “(CompanyID, ContactID, ProductID, DateSold, SoldPrice) “ & _ “VALUES(“ & varCoID & “, “ & Me.Parent.ContactID & “, “ & _ lngProduct & “, #” & _ DateValue(Format(Me.ContactDateTime, “mm/dd/yyyy”)) & “#, “ & _ curPrice & “)” ‘ Attempt to insert the Product row CurrentDb.Execute strSQL, dbFailOnError ‘ Got a good add - inform the user MsgBox “The product you sold with this event “ & _ “has been automatically added “ & _ “to the product list for this user. “ & _ “Click the Products tab to verify the price.”, _ vbInformation, gstrAppTitle 393 ‘ Requery the other subform to get the new row there Me.Parent.fsubContactProducts.Requery End If Insert_Exit: Exit Sub Insert_Err: ‘ Was error a duplicate row? If Err = errDuplicate Then MsgBox “CSD Contacts attempted to auto-add “ & _ “the product that you just indicated “ & _ “that you sold, but the Contact appears “ & _ “to already own this product. Be sure “ & _ “to verify that you haven’t tried to sell the same product twice.”, _ vbCritical, gstrAppTitle Else MsgBox “There was an error attempting to auto-add “ & _ “the product you just sold: “ & _ Err & “, “ & Error, vbCritical, gstrAppTitle ‘ Log the error ErrorLog Me.Name & “_FormAfterUpdate”, Err, Error End If Resume Insert_Exit End Sub Nella sezione Dichiarazioni del modulo, puoi trovare due variabili utilizzate dalle routine evento per passare le informazioni tra gli eventi. Se le dichiari all'interno di una delle routine, le variabili possono essere utilizzate solamente dalla routine in questione. La routine evento BeforeUpdate (dell'evento PrimaDiAggiornare) per il tipo di evento contatto controlla se l'evento è la vendita di un prodotto (esaminando una delle colonne nascoste nell'origine riga della casella combinata). Se l'utente sta cercando di registrare la vendita di un prodotto e questo particolare contatto non ha una società predefinita, il codice visualizza un messaggio di errore e non consente all'utente di salvare quel tipo di evento. Ricorda che un record nella tabella tblContactProducts deve avere un CompanyID, come pure un ContactID. Quando l'utente cerca di salvare il record di un evento, nuovo o modificato, Access esegue la routine evento BeforeUpdate della maschera; questo codice controlla di nuovo se il record che sta per essere salvato è relativo alla vendita di un prodotto. Se tuttavia questo non è un nuovo record oppure se l'utente sta salvando il vecchio record di un evento, ma non ha modificato il tipo di evento, il codice esce, per non aggiungere due volte il record di un prodotto (se questo è un record esistente e il tipo di evento non è cambiato, questo codice probabilmente ha creato il record del prodotto del contatto, la prima volta che l'utente ha salvato il record). Il codice potrebbe a questo punto inserire il record nella tabella tblContactProducts, ma il record viene effettivamente salvato solo al termine dell'evento PrimaDiAggiornare. Questo codice imposta quindi la variabile modulo per indicare alla routine evento AfterUpdate (dell'evento DopoAggiornamento) della maschera di eseguire quell'attività dopo che Access ha salvato il record modificato. Capitolo 7 Automazione di attività complesse 394 Capitolo 7 Automazione di un'applicazione tramite Visual Basic Capitolo 7 Dopo aver salvato il record di un evento, nuovo o modificato, Access esegue la routine evento AfterUpdate della maschera. Se il codice in BeforeUpdate ha impostato la variabile modulo intProductAdd a True, indicando che è necessario inserire un prodotto, questo codice aggiunge il nuovo record; apre un recordset sulla tabella tblProducts per il prodotto che è stato appena venduto, in modo da poter ottenere il prezzo del prodotto e verificare l'esistenza di qualsiasi prodotto prerequisito; se il prodotto ha un prerequisito, ma questo contatto non possiede il prodotto prerequisito, il codice visualizza un messaggio di errore ed esce. Anche se il codice precedente ha verificato l'esistenza di un CompanyID predefinito per questo contatto, questo codice esegue nuovamente questo controllo, ed esce se non riesce a trovare un CompanyID predefinito. Quando il codice ha completato tutti i controlli, e possiede le informazioni necessarie relative al prezzo e al CompanyID, inserisce il nuovo record nella tabella tblContactProducts mediante SQL. Nella parte inferiore della routine puoi trovare il codice di gestione degli errori che controlla se l'inserimento ha causato un errore di record duplicato. Collegamento a un'attività correlata Passa all'applicazione Housing Reservations (Housing.accdb) ed esamina il processo di conferma di una camera per una richiesta di prenotazione. Avvia l'applicazione aprendo la maschera frmSplash, quindi connettiti come amministratore (Conrad, Jeff, Richins, Jack S., Schare, Gary o John L. Viescas) digitando password come password. Nel pannello comandi principale fai clic su Reservation Requests, e poi fai clic su View Unbooked nella finestra di dialogo Edit Reservation Requests. Fatto ciò, vedrai la maschera Unbooked Requests (frmUnbookedRequests), come mostrato nella figura 7-25. Nota La query che fornisce i record visualizzati nella maschera frmUnbookedRequests comprende alcuni criteri che permettono di escludere tutte le richieste che hanno una data di check-in anteriore alla data odierna. Come è facile intuire, non ha senso confermare una richiesta di prenotazione per una data già trascorsa. L'ultima data di check-in richiesta nel database originale è il venerdì 11 marzo 2011, perciò probabilmente vedi un messaggio di errore quando cerchi di esaminare le richieste non registrate. Puoi utilizzare la maschera zfrmLoadData per caricare nuove prenotazioni e richieste più recenti nella query qryUnbookedRequests, in modo da non eliminare le vecchie richieste ed essere in grado di vedere come funziona la maschera frmUnbookedRequests. 395 Capitolo 7 Automazione di attività complesse Figura 7-25 La maschera Unbooked Requests consente di vedere le richieste pendenti e di avviare il processo di prenotazione. Nella sezione "Collegamento ai dati correlati in un'altra maschera o report" a pagina 382, hai appreso una tecnica che permette di utilizzare un pulsante di comando per un collegamento a un'attività correlata. L'attività principale nell'applicazione Housing Reservations consiste nell'assegnare una camera e nel registrare una prenotazione per le richieste pendenti. Quando fai clic su uno dei pulsanti Book sulla maschera Unbooked Requests, il codice della maschera apre una maschera per mostrare le camere che corrispondono alla richiesta e che non sono registrate per l'intervallo di tempo richiesto. Se fai clic sulla richiesta da parte di Kirk DeGrasse per una camera con letto king-size dal 1° dicembre 2010 al 19 dicembre 2010, vedrai l'elenco delle camere disponibili nella maschera fdlgAvailableRooms, come mostrato nella figura 7-26. 396 Capitolo 7 Automazione di un'applicazione tramite Visual Basic Capitolo 7 Figura 7-26 La maschera fdlgAvailableRooms elenca le camere disponibili in base alla richiesta di prenotazione selezionata. Il codice del pulsante Book della maschera frmUnbookedRequests è il seguente: Private Sub cmdBook_Click() ‘ Make sure no changes are pending If Me.Dirty Then Me.Dirty = False ‘ Open the available rooms form - hidden, dialog ‘ and check if any available DoCmd.OpenForm “fdlgAvailableRooms”, _ WhereCondition:=”Smoking = “ & Me.Smoking, _ WindowMode:=acHidden If Forms!fdlgAvailableRooms.RecordsetClone.RecordCount = 0 Then MsgBox “There are no available rooms of this “ & _ “type for the dates requested.” & _ vbCrLf & vbCrLf & _ “You can change the Room Type or dates and try again.”, _ vbInformation, gstrAppTitle DoCmd.Close acForm, “fdlgAvailableRooms” Exit Sub End If ‘ Show the available rooms ‘ - form will call our public sub to create the res. Forms!fdlgAvailableRooms.Visible = True End Sub L'origine record della maschera fdlgAvailableRooms è una query con parametri, che filtra le camere già registrate per le date specificate, e include le rimanenti camere che corrispondono al tipo di camera richiesto. Il codice del pulsante Book aggiunge un filtro per la richiesta di camere per fumatori o non fumatori, poiché questa informazione non Automazione di attività complesse 397 Private Sub cmdPick_Click() Dim intReturn As Integer ‘ Call the build a reservation proc in the calling form intReturn = Form_frmUnbookedRequests.Bookit(Me.FacilityID, Me.RoomNumber, _ Me.DailyRate, Me.WeeklyRate) If (intReturn = True) Then MsgBox “Booked!”, vbExclamation, gstrAppTitle Else MsgBox “Room booking failed. Please try again.”, _ vbCritical, gstrAppTitle End If DoCmd.Close acForm, Me.Name End Sub Riesci a immaginare cosa avviene? In frmUnbookedRequests c'è una funzione pubblica di nome Bookit richiamata da questo codice come metodo di quella maschera. Questa funzione passa i campi FacilityID, RoomNumber, DailyRate e WeeklyRate per completare la prenotazione. Il codice della funzione pubblica Bookit è il seguente: Public Function Bookit(lngFacility As Long, lngRoom As Long, _ curDaily As Currency, curWeekly As Currency) As Integer ‘ Sub called as a method by fdlgAvailableRooms to book the selected room ‘ Caller passes in selected Facility, Room number, and rates Dim db As DAO.Database, rstRes As DAO.Recordset Dim varResNum As Variant, strSQL As String, intTrans As Integer ‘ Set error trap On Error GoTo BookIt_Err ‘ Get a pointer to this database Set db = CurrentDb ‘ Open the reservations table for insert Set rstRes = db.OpenRecordset(“tblReservations”, _ dbOpenDynaset, dbAppendOnly) ‘ Start a transaction BeginTrans intTrans = True ‘ Get the next available reservation number varResNum = DMax(“ReservationID”, “tblReservations”) If IsNull(varResNum) Then varResNum = 0 varResNum = varResNum + 1 ‘ Update the current row strSQL = “UPDATE tblReservationRequests SET ReservationID = “ & _ varResNum & “ WHERE RequestID = “ & Me.RequestID db.Execute strSQL, dbFailOnError ‘ Book it! rstRes.AddNew ‘ Copy reservation ID rstRes!ReservationID = varResNum ‘ Copy employee number rstRes!EmployeeNumber = Me.EmployeeNumber ‘ Copy facility ID from the room we picked rstRes!FacilityID = lngFacility ‘ .. and room number Capitolo 7 è inclusa nel tipo di camera, ma è indicata per ciascuna camera disponibile. Il codice del pulsante Pick This della maschera fdlgAvailableRooms è il seguente: 398 Capitolo 7 Automazione di un'applicazione tramite Visual Basic Capitolo 7 rstRes!RoomNumber = lngRoom ‘ Set reservation date = today rstRes!ReservationDate = Date ‘ Copy check-in, check-out, and notes rstRes!CheckInDate = Me.CheckInDate rstRes!CheckOutDate = Me.CheckOutDate rstRes!Notes = Me.Notes ‘ Copy daily and weekly rates rstRes!DailyRate = curDaily rstRes!WeeklyRate = curWeekly ‘ Calculate the total charge rstRes!TotalCharge = ((Int(Me.CheckOutDate - Me.CheckInDate) \ 7) * _ curWeekly) + _ ((Int(Me.CheckOutDate - Me.CheckInDate) Mod 7) * _ curDaily) ‘ Save the Reservation Row rstRes.Update ‘ Commit the transaction CommitTrans intTrans = False ‘ Clean up rstRes.Close Set rstRes = Nothing Set db = Nothing ‘ Requery this form to remove the booked row Me.Requery ‘ Return success Bookit = True BookIt_Exit: Exit Function BookIt_Err: MsgBox “Unexpected Error: “ & Err & “, “ & Error, vbCritical, gstrAppTitle ErrorLog Me.Name & “_Bookit”, Err, Error Bookit = False If (intTrans = True) Then Rollback Resume BookIt_Exit End Function Ha senso avere il codice di registrazione nella maschera frmUnbookedRequests, poiché la riga che il codice deve inserire nella tabella tblReservations richiede molti campi del record di richiesta corrente (EmployeeNumber, CheckInDate, CheckOutDate e Notes). Il codice avvia una transazione, poiché deve simultaneamente immettere un ReservationID in entrambe le tabelle tblReservationRequests e tblReservations. Se fallisce, il codice di gestione degli errori ripristina entrambi gli aggiornamenti. Il codice apre la tabella tblReservations in modalità solo aggiunta, per rendere l'inserimento della nuova prenotazione più efficiente. Calcolo di un valore memorizzato Se applichi le regole per la corretta progettazione delle tabelle, sai che memorizzare un valore calcolato in una tabella in genere non è una buona idea, poiché è necessario scrivere il codice che deve gestire tale valore. Talvolta tuttavia, in un database molto grosso, 399 è necessario calcolare e salvare un valore, per migliorare le prestazioni per le ricerche e i report; l'applicazione Housing Reservations non è così grossa, ma potrebbe esserlo nella vita reale. Abbiamo scelto di memorizzare il conto totale calcolato, per ciascuna prenotazione, per mostrarti alcune delle azioni che devi eseguire per gestire un valore come questo. Gli utenti possono creare e modificare le richieste di prenotazione, ma la creazione dei record delle prenotazioni, che contengono il valore calcolato, è controllata interamente dal codice; la gestione del valore calcolato TotalCharge in questa applicazione è quindi molto semplice. Hai già visto dove viene creato il record di una nuova prenotazione (nella funzione pubblica Bookit, che si trova nella maschera frmUnbookedRequests). Il segmento di codice che calcola il valore è il seguente: ‘ Calculate the total charge rstRes!TotalCharge = ((Int(Me.CheckOutDate - Me.CheckInDate) \ 7) * _ curWeekly) + _ ((Int(Me.CheckOutDate - Me.CheckInDate) Mod 7) * _ curDaily) In molte applicazioni tuttavia non riuscirai a controllare così bene la modifica di un valore calcolato. Devi considerare attentamente le conseguenze del salvataggio di un valore calcolato nella tua tabella, e devi forse comporre il codice che un amministratore può eseguire periodicamente, per verificare che ogni valore calcolato salvato sia sincronizzato con gli altri campi utilizzati per eseguire il calcolo. Nota Access 2010 comprende il nuovo tipo di dati calcolato che puoi usare per memorizzare i valori calcolati, ma in questa sezione vogliamo mostrarti come ottenere queste funzionalità tramite Visual Basic. Devi tuttavia ricordare che, utilizzando Visual Basic per memorizzare un valore calcolato, non puoi forzare l'esecuzione del calcolo quando un utente modifica i dati direttamente in un foglio dati di una tabella o query. Se utilizzi un tipo di dati calcolato, Access controlla il valore calcolato a livello di motore. Automazione dei report In un'applicazione tipica, probabilmente circa l'80 o il 90 percento del lavoro di scrittura del codice riguarda le routine evento per le maschere. Ciò non significa che non esistano numerose attività che possono essere automatizzare nei report. Questa sezione mostra alcune delle possibilità. Capitolo 7 Automazione dei report 400 Capitolo 7 Automazione di un'applicazione tramite Visual Basic Gestione delle etichette postali Capitolo 7 Hai mai desiderato creare un report di etichette postali, ed escogitare un metodo per stampare le etichette selezionate su una pagina già parzialmente utilizzata? Puoi trovare la risposta nell'applicazione Conrad Systems Contacts (Contacts.accdb). Supponi di volere inviare un messaggio promozionale a tutti i contatti che possiedono il prodotto Single User offrendo loro un upgrade a Multi-User. Apri il pannello comandi principale (frmMain), fai clic su Contacts e poi fai clic su Search nella finestra popup Select Contacts. Esegui una ricerca di tutti i contatti che possiedono il prodotto BO$$ Single User: nel database originale dovresti trovarne otto. Fai clic su No quando l'applicazione ti chiede se desideri vedere prima un elenco di riepilogo. Fai clic sul pulsante Print nella maschera frmContacts, seleziona Avery 5163 (2"× 4"), richiedi che il report includa i contatti visualizzati (Displayed Contacts) e indica che 3 etichette sono già state utilizzate sulla prima pagina. A questo punto, il tuo schermo dovrebbe apparire simile alla figura 7-27. Figura 7-27 Puoi richiedere etichette postali e specificare che alcune etichette sono già state utilizzate sulla prima pagina. Fai clic sul pulsante Print nella finestra di dialogo; dovresti vedere le etichette stampate, ma con tre spazi vuoti in corrispondenza di quelle utilizzate, come mostrato nella figura 7-28. 401 Capitolo 7 Automazione dei report Figura 7-28 Vengono stampate solo le etichette non utilizzate. Puoi trovare del codice interessante nella routine evento AfterUpdate del gruppo di opzioni per scegliere il tipo di report, nella maschera fdlgContactPrintOptions: Il codice è il seguente: Private Sub optReportType_AfterUpdate() ‘ Figure out whether to show the “used labels” combo Select Case Me.optReportType Case 1 ‘ Show the used labels combo Me.cmbUsedLabels.Visible = True ‘ Hide the number of days option group Me.optDisplay.Visible = False ‘ up to 29 used labels on 5160 Me.cmbUsedLabels.RowSource = “0;1;2;3;4;5;6;7;8;9;10;11;12;13;” & _ “14;15;16;17;18;19;20;21;22;23;24;25;26;27;28;29” Case 2 ‘ Show the used labels combo Me.cmbUsedLabels.Visible = True ‘ Hide the number of days option group Me.optDisplay.Visible = False ‘ up to 9 used labels on 5163 Me.cmbUsedLabels.RowSource = “0;1;2;3;4;5;6;7;8;9” Case 3, 4 ‘ Don’t need the combo for Envelopes and contact list Me.cmbUsedLabels.Visible = False ‘ .. or the number of days filter Me.optDisplay.Visible = False Case 5, 6 ‘ Don’t need the used labels combo for contact events or products Me.cmbUsedLabels.Visible = False ‘ Do need the day filter Me.optDisplay.Visible = True End Select End Sub 402 Capitolo 7 Automazione di un'applicazione tramite Visual Basic Capitolo 7 Puoi avere fino a 29 etichette usate quando stampi su un foglio Avery 5160 (1" x 2.625"). Puoi avere fino a 9 etichette usate quando stampi su un foglio Avery 5163 (2" x 4"). La casella combinata che puoi utilizzare per indicare il numero di etichette usate ha un Value List come tipo di origine riga, e quindi il codice imposta l'appropriato elenco in base al tipo di etichetta scelto. Tuttavia, il vero trucco che permette di lasciare spazi vuoti all'interno del report si trova nella query qryRptContactLabels che rappresenta l'origine record del report rptContactLabels5163. Nel database di esempio puoi trovare una tabella chiamata ztblLabelSpace che contiene 30 record, ognuno dei quali ha un campo contenente i valori da 1 a 30. Il codice SQL per la query qryRptContactLabels è il seguente: PARAMETERS [Forms]![fdlgContactPrintOptions]![cmbUsedLabels] Long; SELECT “” As Contact, “” As CompanyName, “” As Address, “” As CSZ, Null As ContactID, “” As Zip, “” As LastName, “” As FirstName, “” As ContactType, “” As WorkCity, “” As WorkStateOrProvince, “” As HomeCity, “” As HomeStateOrProvince, 0 As Inactive FROM ztblLabelSpace WHERE ID <= [Forms]![fdlgContactPrintOptions]![cmbUsedLabels] UNION ALL SELECT ([tblContacts].[Title]+” “) & [tblContacts].[FirstName] & “ “ & ([tblContacts].[MiddleInit]+”. “) & [tblContacts].[LastName] & (“, “+[tblContacts].[Suffix]) AS Contact, Choose([tblContacts].[DefaultAddress], qryContactDefaultCompany.CompanyName, Null) As CompanyName, Choose([tblContacts].[DefaultAddress],[tblContacts].[WorkAddress], [tblContacts].[HomeAddress]) AS Address, Choose([tblContacts].[DefaultAddress],[tblContacts].[WorkCity] & “, “ & [tblContacts].[WorkStateOrProvince] & “ “ & [tblContacts].[WorkPostalCode], [tblContacts].[HomeCity] & “, “ & [tblContacts].[HomeStateOrProvince] & “ “ & [tblContacts].[HomePostalCode]) AS CSZ, tblContacts.ContactID, Choose([tblContacts].[DefaultAddress],[tblContacts].[WorkPostalCode], [tblContacts].[HomePostalCode]) AS Zip, tblContacts.LastName, tblContacts.FirstName, tblContacts.ContactType, tblContacts.WorkCity, tblContacts.WorkStateOrProvince, tblContacts.HomeCity, tblContacts.HomeStateOrProvince, tblContacts.Inactive FROM tblContacts LEFT JOIN qryContactDefaultCompany ON tblContacts.ContactID = qryContactDefaultCompany.ContactID; La prima istruzione SELECT (fino a UNION ALL) crea colonne vuote fittizie per i campi utilizzati dal report e impiega la tabella ztblLabelSpace e un filtro sulla casella combinata nella maschera fdlgContactPrintOptions (figura 7-27) per restituire il numero corretto di righe vuote. La query utilizza un'istruzione UNION con la query attuale che ritorna i dati del contatto, per visualizzare le informazioni sul report. Poiché questo report stampa un logo, la parte finale del codice evita che queste informazioni appaiano sulle etichette vuote nel report rptContactLabels5163. Il codice è il seguente: 403 Private Sub Detail_Format(Cancel As Integer, FormatCount As Integer) ‘ Don’t print the return logo if this is a “spacer” record If IsNull(Me.ContactID) Then Me.imgCSD.Visible = False Else Me.imgCSD.Visible = True End If End Sub L'evento Formattazione (Format) della sezione Dettagli dipende dal fatto che ContactID nelle righe vuote sia Null. Quando stampi una riga vuota per motivi di spaziatura, il codice nasconde il logo. Disegno su un report Quando desideri disegnare un bordo intorno all'area di stampa di un report, talvolta devi comporre il codice per chiedere ad Access di disegnare le linee o un bordo, dopo aver posto i dati sulla pagina. Questo è vero specialmente se uno o più controlli sul report possono espandersi, per accogliere una grossa quantità di dati. Abbiamo utilizzato la Creazione guidata Report per creare il report rptContacts che utilizza il formato Giustificato. Abbiamo inoltre personalizzato il report al termine della creazione guidata. La creazione guidata ha creato un layout abbastanza decente, con un bordo intorno a tutti i campi, ma la casella di testo per visualizzare le note non è abbastanza grande da visualizzare il testo per tutti i contatti. La figura 7-29 mostra il report che visualizza il record di John dal database. Come puoi vedere, le note su John sono troncate in basso. Figura 7-29 Questo report utilizza un bordo intorno ai dati, ma una delle caselle di testo non è abbastanza larga per visualizzare tutto il testo. Capitolo 7 Automazione dei report 404 Capitolo 7 Automazione di un'applicazione tramite Visual Basic Capitolo 7 È abbastanza semplice cambiare la proprietà Espandibile della casella di testo a Sì, per consentire la sua espansione, ma il controllo rettangolo utilizzato per disegnare il bordo intorno a tutto il testo non ha una proprietà Espandibile. La soluzione consiste nel rimuovere il rettangolo e nell'utilizzare il metodo Line dell'oggetto report nell'evento Formattazione del report della sezione Dettaglio. Il codice che puoi trovare in questa routine evento nel report rptContactsExpandNotes è il seguente: Private Sub Detail_Print(Cancel As Integer, PrintCount As Integer) Dim sngX1 As Single, sngY1 As Single Dim sngX2 As Single, sngY2 As Single, lngColor As Long ‘ Set coordinates sngX1 = 120 sngY1 = 120 sngX2 = 9120 ‘ Adjust the height if Notes has expanded sngY2 = 7680 + (Me.Notes.Height - 2565) ‘ Draw the big box around the data ‘ Set width of the line to 8 pixels Me.DrawWidth = 8 ‘ Draw the rectangle around the expanded fields Me.Line Step(sngX1, sngY1)-Step(sngX2, sngY2), RGB(0, 0, 197), B End Sub Il metodo Line accetta tre parametri: 1. Gli angoli superiore sinistro e inferiore destro della linea o del riquadro che desideri disegnare, espressi in twip. Ci sono 1440 twip per pollice. Includi la parola chiave Step per indicare che le coordinate sono relative alla posizione corrente del disegno, che comincia sempre alla posizione 0,0. Quando utilizzi Step per la seconda coordinata, fornisci i valori relativi a quelli che hai specificato nel primo insieme di coordinate. 2. Il colore che desideri utilizzare per disegnare la linea o il riquadro, espresso come valore RGB (red-green-blue). Per questo parametro è utile la funzione RGB. 3. Un indicatore per richiedere una linea, un rettangolo o un rettangolo pieno. Se non è specificato alcun indicatore, viene disegnata una linea. Includi la lettera B per richiedere un rettangolo; aggiungi la lettera F per richiedere un rettangolo pieno. Prima di chiamare il metodo Line, puoi impostare la proprietà DrawWidth del report, per impostare la larghezza della linea (la larghezza predefinita è in pixel). L'unica complicazione sono le coordinate. Sul report originale il rettangolo comincia a 0,0833 pollici da sinistra e dal margine superiore; moltiplicando dunque quel valore per 1440 twip per pollice, abbiamo ottenuto i valori di partenza di 120 dal margine superiore e di 120 da sinistra. La larghezza del rettangolo deve essere di circa 6,3333 pollici (16 cm), e quindi la coordinata relativa per l'angolo superiore destro è 6,3333 x 1440, oppure circa 9.120 twip. L'altezza del rettangolo deve essere almeno 5,3333 pollici (13,5 cm), o circa 7680 twip, e l'altezza deve essere adattata alla dimensione della casella di testo Notes, che può espandersi. La casella di testo Notes è progettata per essere alta come minimo 1,7813 Automazione dei report 405 Se apri il report rptContactsExpandNotes e ti sposti sul record di John alla pagina 32, vedrai che il rettangolo ora si espande correttamente, per adattarsi intorno alla casella di testo Notes che si è ingrandita per visualizzare tutto il testo nel record di John . La figura 7-30 mostra il report con un rettangolo disegnato dal codice del report. Figura 7-30 Il codice del report rptContactsExpandNotes disegna un rettangolo personalizzato attorno al testo espanso. APPROFONDIMENTO AP PPROFONDIMENTO Esaminare altri rapporti che usano il metodo Line Per vedere esempi più interessanti di impiego del metodo Line per disegnare linee dinamiche in un rapporto, puoi anche esaminare i rapporti client rptLaborPlan, rptLaborPlanIndividual e rptWeeklyPostedSchedule nel database Web Back Office Software System. Spesso, la comprensione di questi rapporti risulta più facile se utilizzi il database BOSSDataCopy.accdb, poiché il database principale BOSS.accdb usa barre multifunzione personalizzate. Capitolo 7 pollici (4,5 cm), o 2.565 twip, e quindi sottraendo 2.565 dall'altezza attuale della casella di testo Notes quando è formattata (anche la proprietà Altezza è espressa in twip) ottieni la quantità che devi aggiungere all'altezza originale del rettangolo. 406 Capitolo 7 Automazione di un'applicazione tramite Visual Basic Filtro dinamico di un report in fase di apertura Capitolo 7 Per aprire un report filtrato e stampare specifici record, puoi utilizzare uno dei due seguenti metodi: ● Utilizza il parametro WhereCondition con il metodo DoCmd.OpenReport (in genere nel codice di una routine evento di una maschera), per specificare un filtro. ● Basa il report su una query con parametri che chieda all'utente i valori del filtro, oppure che faccia riferimento ai valori in una maschera aperta. In alcuni casi potresti progettare un report che intendi aprire da molti punti dell'applicazione, e non puoi garantire che la maschera per fornire i valori del filtro sia sempre aperta. In alternativa, puoi avere più report che richiedono gli stessi criteri di filtro e non desideri dover progettare una maschera di filtro distinta per ciascun report. Per risolvere questi problemi, puoi aggiungere al report il codice per aprire la sua finestra di dialogo di filtro dalla routine evento Open del report. Torna all'applicazione Housing Reservations (Housing.accdb), per esaminare un report che utilizza questa tecnica. Nell'applicazione Housing Reservations, i report rptFacilityOccupancy e rptFacilityRevenueChart dipendono da una singola maschera, fdlgReportDateRange, per fornire una data iniziale e finale per il report. Per vedere il report rptFacilityOccupancy, avvia l'applicazione aprendo la maschera frmSplash, connettiti come amministratore (Conrad, Jeff, Richins, Jack S., Schare, Gary o John L. Viescas), fai clic sul pulsante Reports nel pannello comandi principale, quindi sul pulsante Reservations nella categoria Facilities nel pannello comandi Reports. Puoi anche aprire direttamente il report dal riquadro di spostamento. Quando apri il report, puoi vedere una finestra di dialogo che richiede le date desiderate, come mostrato nella figura 7-31. Figura 7-31 Dal report che hai chiesto di visualizzare viene aperta una finestra di dialogo con parametri. 407 A meno che tu non abbia ricaricato i dati di esempio, il database contiene prenotazioni dal 24 agosto 2010 al 22 marzo 2011, pertanto puoi stampare un report per ottobre, novembre e dicembre. Immetti le date desiderate e fai clic su Go per vedere il report, come mostrato nella figura 7-32. Figura 7-32 Il report Facility Occupancy utilizza una finestra di dialogo di filtro condivisa per consentirti di specificare un intervallo di date. Il report ha una query con parametri nella sua origine record, e i parametri puntano alle date From e To nella maschera fdlgReportDateRange mostrata nella figura 7-31. Il codice del pulsante Reservations sul pannello comandi Reports apre tuttavia il report non filtrato; è il codice nella routine evento Open del report che apre la finestra di dialogo, in modo che la query nell'origine record possa trovare i parametri necessari. Il codice è il seguente: Private Sub Report_Open(Cancel As Integer) ‘ Open the date range dialog ‘ .. report record source is filtered on this! DoCmd.OpenForm “fdlgReportDateRange”, WindowMode:=acDialog End Sub Ciò funziona perché un oggetto Report non cerca di aprire l'origine record per il report fino al termine dell'esecuzione del codice dell'evento Open. Puoi quindi inserire del codice nell'evento Open per cambiare dinamicamente l'origine record del report o, come in questo esempio, aprire una maschera in modalità Dialog per attendere fino a quando tale maschera non viene chiusa o nascosta. Il codice della maschera fdlgReportDateRange è il seguente: Capitolo 7 Automazione dei report 408 Capitolo 7 Automazione di un'applicazione tramite Visual Basic Capitolo 7 Private Sub Form_Load() ‘ If passed a parameter, reset defaults to last quarter If Not IsNothing(Me.OpenArgs) Then ‘ Set the start default to first day of previous quarter Me.txtFromDate.DefaultValue = “#” & _ DateSerial(Year(Date), ((Month(Date) - 1) \ 3) * 3 - 2, 1) & “#” ‘ Set the end default to last day of previous quarter Me.txtToDate.DefaultValue = “#” & _ DateSerial(Year(Date), ((Month(Date) - 1) \ 3) * 3 + 1, 1) & “#” End If End Sub Private Sub cmdGo_Click() ‘ Hide me so report can continue Me.Visible = False End Sub Il codice nell'evento Caricamento (Load) della maschera controlla se il report che sta aprendo la maschera ha passato un parametro nella proprietà OpenArgs. In caso affermativo, il codice reimposta i valori predefiniti per le due caselle di testo delle date sulle date iniziale e finale del trimestre precedente. Se esamini il codice del report rptFacilityRevenueChart, puoi notare che questo report richiede valori predefiniti differenti, ma è il codice nell'evento Clic del pulsante di comando Go che fa funzionare tutto. Il codice della maschera risponde al tuo clic sul pulsante Go, per nascondere se stessa in modo che il report possa proseguire; non può chiudere se stessa, poiché l'origine record del report dipende dalle due date. Come notato in precedenza, nascondere questa maschera, aperta in modalità Dialog, consente al codice nell'evento Apertura del report di terminare, in modo tale che il report possa infine caricare la propria origine record. Come potresti sospettare, nell'evento Chiusura (Close) del report si trova il codice per chiudere la maschera dei parametri, quando chiudi il report o quando il report termina la stampa. Richiamo di macro per i dati con nome Access 2010 comprende ora macro per i dati che puoi utilizzare per collegare la logica al livello dei dati. Quando colleghi le macro per i dati agli eventi tabella, Access esegue queste macro solo in risposta agli eventi dati, come ad esempio l'inserimento di un nuovo record, l'aggiornamento di valori in un record esistente o l'eliminazione di uno o più record. Puoi anche creare una macro per i dati con nome collegata alla tabella stessa. Per molti aspetti, una macro per i dati con nome è simile a una routine o a una funzione pubblica in un modulo di codice standard: puoi richiamare una macro per i dati con nome da altre macro e codice Visual Basic per eseguire le azioni definite nella macro per i dati e se desideri restituire il risultato al richiedente. Nel database Web Back Office Software System, abbiamo definito molte macro per i dati con nome collegate alle tabelle Web, in modo tale da aiutare ad automatizzare l'applicazione. Nella tabella tblErrorLog inclusa in questo database Web registriamo gli errori dell'applicazione che si verificano quando gli utenti lavorano con il database nel 409 client Access. Collegata a questa tabella c'è una macro per i dati con nome chiamata LogError. Questa macro per i dati con nome include sei parametri usati per rilevare i dati da registrare in merito all'errore nella tabella. Il blocco di dati CreateRecord viene usato per creare un nuovo record nella tabella tblErrorLog, quindi l'azione SetField permette di passare i dati nei parametri ai campi appropriati. Apri il database Web BOSSDataCopy.accdb per vedere come richiamare queste macro per i dati con nome da Visual Basic. Se apri una maschera o un rapporto client in questo database Web, puoi notare che ogni routine o funzione Visual Basic contiene un'etichetta del gestore errori. Quando la routine o funzione rileva un errore, viene richiamata una routine pubblica in un modulo standard per registrarne i dettagli. L'esempio di codice che segue mostra la chiamata a livello di maschera e di report: ErrorHandler "frmDailyLaborPlans", "cmdClearList_Click", Err.Number, Err.Description Come puoi vedere, viene richiamata una routine pubblica chiamata ErrorHandler che include quattro parametri: il nome dell'oggetto in cui si è verificato l'errore, la specifica routine che ha generato l'errore, il numero di errore restituito da Access e la descrizione dell'errore dell'applicazione. Per esaminare questa routine, apri il modulo modErrorTrap nella visualizzazione Struttura dal riquadro di spostamento. Il codice nella routine pubblica ErrorHandler in modErrorTrap è il seguente: Public Sub ErrorHandler(strModule As String, _ strProcedure As String, _ lngErrorNumber As Long, _ strErrorString As String) ‘ For this procedure, continue on if any errors occur On Error Resume Next Dim strMessage As String Dim datCurrentTime As Date Dim strUser As String ‘ Current date and time datCurrentTime = Now() ‘ If we can’t determine the current web user who ‘ triggered the error, then use the legacy CurrentUser ‘ function to log user name If IsNull(CurrentWebUser(acWebUserName)) Then strUser = CurrentUser() Else strUser = CurrentWebUser(acWebUserName) End If ‘ The second action argument of DoCmd.SetParameter is an expression. ‘ If the second argument is a string, ‘ you need to escape the string in double quotes. DoCmd.SetParameter “ParamModule”, “’” & strModule & “’” DoCmd.SetParameter “ParamProcedure”, “’” & strProcedure & “’” DoCmd.SetParameter “ParamErrorNumber”, lngErrorNumber DoCmd.SetParameter “ParamErrorString”, “’” & strErrorString & “’” DoCmd.SetParameter “ParamUser”, “’” & strUser & “’” DoCmd.SetParameter “ParamTime”, “#” & datCurrentTime & “#” ‘ Run the named data macro to log this error event to the error table DoCmd.RunDataMacro “tblErrorLog.LogError” Capitolo 7 Richiamo di macro per i dati con nome 410 Capitolo 7 Automazione di un'applicazione tramite Visual Basic Capitolo 7 ‘ Build up a custom message to display to the user strMessage = “There was an unexpected error in your application.” & vbCr & vbCr strMessage = strMessage & “Error Details:” & vbCr strMessage = strMessage & “Module : “ & strModule & vbCr strMessage = strMessage & “Procedure : “ & strProcedure & vbCr strMessage = strMessage & “Error : “ & lngErrorNumber & vbCr & vbCr strMessage = strMessage & “Please notify Technical Support of this error.” _ & vbCr & vbCr strMessage = strMessage & “Would you like to view this report in order to print?” ‘ View report if requested by user If MsgBox(strMessage, vbCritical + vbYesNo + vbDefaultButton1, _ “BOSS - Unexpected Error”) = vbYes Then DoCmd.OpenReport “rptErrorLog”, acViewPreview, , _ “ErrorTime=#” & datCurrentTime & “#” End If End Sub Nella prima parte della routine, il codice recupera la data e l'ora correnti di sistema e il nome dell'utente attualmente connesso da registrare nella tabella tblErrorLog. Fatto ciò, il codice deve eseguire un metodo DoCmd.SetParameter per ogni parametro che abbiamo definito nella macro per i dati. Il primo argomento dell'azione SetParameter è il nome del parametro definito nella macro per i dati con nome, mentre il secondo argomento dell'azione sono i dati da passare al parametro. Come puoi vedere nel codice, vengono elencati i nomi di parametro e la variabile contenente i dati da registrare. Nota che, se i dati che vengono passati sono una stringa, questa deve essere racchiusa tra virgolette. Una volta che il codice ha impostato tutti i parametri, è possibile procedere a richiamare la macro per i dati con nome usando il metodo DoCmd.RunDataMacro. Nell'argomento del metodo RunDataMacro, devi inserire il nome della tabella, un punto e il nome della macro per i dati con nome collegata alla tabella. Devi racchiudere l'argomento tra virgolette anche se i nomi della tabella e della macro per i dati con nome non contengono spazi. L'ultima parte di questo codice crea un messaggio personalizzato da mostrare all'utente e chiede se desidera stampare il report degli errori. La routine di codice completata registra qualsiasi errore dell'applicazione rilevato nel client Access richiamando la nostra macro per i dati con nome. Quando richiami una macro per i dati con nome che restituisce dati al richiedente usando ReturnVar, puoi usare il valore nella variabile ReturnVar nel codice Visual Basic. Ad esempio, se desideri utilizzare Visual Basic per ottenere il numero della versione attuale dell'applicazione BOSS e mostrare tale valore in una finestra di messaggio, puoi usare la routine di esempio TestReturnVar nel modulo modErrorTrap: Public Sub TestReturnVar() ‘ Set the parameter of ParamValue to Version DoCmd.SetParameter “ParamValue”, “””Version””” ‘ Run the named data macro to get the current version of BOSS DoCmd.RunDataMacro “tblSettings.GetCurrentValue” ‘ Display the current version of BOSS in a message box ‘ Use the ReturnVar called RVVersion in the message box MsgBox “The current version number of BOSS is: “ & _ ReturnVars!RVVersion, vbInformation, “Version Number” End Sub 411 Quando esegui questa routine di esempio, il codice esegue la macro per i dati con nome GetCurrentValue collegata alla tabella Web tblSettings, passa il valore del parametro appropriato da usare e visualizza quindi il valore restituito dalla macro per i dati nella finestra di messaggio usando la raccolta ReturnVars. Nota che, a differenza della raccolta TempVars, la raccolta ReturnVars non supporta alcun metodo Remove o RemoveAll per eliminare o cancellare i valori memorizzati nella raccolta ReturnVars. Non è possibile nemmeno impostare una variabile di ritorno usando Visual Basic; per fare ciò, devi infatti utilizzare sempre l'azione sui dati SetReturnVar in una macro per i dati con nome collegata a una tabella. Se imposti un ReturnVar in una macro per i dati con nome, Access conserva tale valore nella raccolta ReturnVars fino a quando non esegui un'altra macro per i dati con nome che imposti un altro ReturnVar, oppure fino alla chiusura del database. Nella raccolta ReturnVars possono essere impostate fino a 255 variabili di ritorno, ma queste dovrebbero quindi essere impostate in una chiamata a una macro per i dati con nome. Come abbiamo visto in questo capitolo, Visual Basic è un linguaggio incredibilmente potente, e le attività che puoi eseguire con esso sono limitate solo dalla tua immaginazione. Nel capitolo 8, "Ultimi ritocchi", apprenderai come impostare le proprietà di avvio e creare barre multifunzione personalizzate. Capitolo 7 Richiamo di macro per i dati con nome