2012-01-13 5 views
11

Ho riscontrato alcuni strani capricci in Excel durante la rimozione programmatica dei moduli, quindi la reimportazione da file. Fondamentalmente, ho un modulo chiamato VersionControl che dovrebbe esportare i miei file in una cartella predefinita e reimportarli su richiesta. Questo è il codice per la reimportazione (il problema con esso è descritto di seguito):Flushing delle modifiche apportate a VBProject.VBComponents in Excel tramite VBA

Dim i As Integer 
Dim ModuleName As String 
Application.EnableEvents = False 
With ThisWorkbook.VBProject 
    For i = 1 To .VBComponents.Count 
     If .VBComponents(i).CodeModule.CountOfLines > 0 Then 
      ModuleName = .VBComponents(i).CodeModule.Name 
      If ModuleName <> "VersionControl" Then 
       If PathExists(VersionControlPath & "\" & ModuleName & ".bas") Then 
        Call .VBComponents.Remove(.VBComponents(ModuleName)) 
        Call .VBComponents.Import(VersionControlPath & "\" & ModuleName & ".bas") 
       Else 
        MsgBox VersionControlPath & "\" & ModuleName & ".bas" & " cannot be found. No operation will be attempted for that module." 
       End If 
      End If 
     End If 
    Next i 
End With 

Dopo l'esecuzione di questo, ho notato che alcuni moduli non appaiono più, mentre alcuni hanno duplicati (es mymodule e myModule1) . Mentre si passava attraverso il codice, è diventato ovvio che alcuni moduli si attardano ancora dopo la chiamata Remove e possono essere reimportati mentre si è ancora nel progetto. A volte, questo ha avuto come risultato solo il suffisso del modulo con 1, ma a volte avevo sia l'originale che la copia.

C'è un modo per scaricare le chiamate su Remove e Import in modo che si applichino? Sto pensando di chiamare una funzione Save dopo ciascuna, se ce n'è una nell'oggetto Application, anche se questo può causare perdite se le cose vanno male durante l'importazione.

Idee?

Modifica: tag modificato synchronization a version-control.

+0

+1 Clever Little modo per fare un po 'in casa il controllo di versione. Dovrei fare qualcosa di simile anch'io. –

+0

È stato ispirato da [questa domanda] (http://stackoverflow.com/questions/131605/best-way-to-do-version-control-for-ms-excel) qui su StackOverflow - la mia versione è solo una modesta remake. – CamilB

+1

Non ho creato nulla di simile, ma le cose che proverei sarebbero: chiamare da un'altra cartella di lavoro/addin; eseguire il backup della cartella di lavoro prima, rimuovendo tutte le operazioni contemporaneamente, salvando, importando tutto in una volta. Potresti anche scherzare con la versione COM di Code Cleaner di Rob Bovey. È possibile impostare un riferimento ad esso e accedere alle funzioni di importazione, esportazione e altre funzioni. Sarò interessato a vedere cosa scopri. –

risposta

12

Questo è un array live, si stanno aggiungendo e rimuovendo gli articoli durante l'iterazione cambiando quindi i numeri indice. Prova a processare l'array al contrario. Ecco la mia soluzione senza alcun gestione degli errori:

Private Const DIR_VERSIONING As String = "\\VERSION_CONTROL" 
Private Const PROJ_NAME As String = "PROJECT_NAME" 

Sub EnsureProjectFolder() 
    ' Does this project directory exist 
    If Len(Dir(DIR_VERSIONING & PROJ_NAME, vbDirectory)) = 0 Then 
     ' Create it 
     MkDir DIR_VERSIONING & PROJ_NAME 
    End If 
End Sub 

Function ProjectFolder() As String 
    ' Ensure the folder exists whenever we try to access it (can be deleted mid execution) 
    EnsureProjectFolder 
    ' Create the required full path 
    ProjectFolder = DIR_VERSIONING & PROJ_NAME & "\" 
End Function 

Sub SaveCodeModules() 

    'This code Exports all VBA modules 
    Dim i%, sName$ 

    With ThisWorkbook.VBProject 
     ' Iterate all code files and export accordingly 
     For i% = 1 To .VBComponents.count 
      ' Extract this component name 
      sName$ = .VBComponents(i%).CodeModule.Name 
      If .VBComponents(i%).Type = 1 Then 
       ' Standard Module 
       .VBComponents(i%).Export ProjectFolder & sName$ & ".bas" 
      ElseIf .VBComponents(i%).Type = 2 Then 
       ' Class 
       .VBComponents(i%).Export ProjectFolder & sName$ & ".cls" 
      ElseIf .VBComponents(i%).Type = 3 Then 
       ' Form 
       .VBComponents(i%).Export ProjectFolder & sName$ & ".frm" 
      ElseIf .VBComponents(i%).Type = 100 Then 
       ' Document 
       .VBComponents(i%).Export ProjectFolder & sName$ & ".bas" 
      Else 
       ' UNHANDLED/UNKNOWN COMPONENT TYPE 
      End If 
     Next i 
    End With 

End Sub 

Sub ImportCodeModules() 
    Dim i%, sName$ 

    With ThisWorkbook.VBProject 
     ' Iterate all components and attempt to import their source from the network share 
     ' Process backwords as we are working through a live array while removing/adding items 
     For i% = .VBComponents.count To 1 Step -1 
      ' Extract this component name 
      sName$ = .VBComponents(i%).CodeModule.Name 
      ' Do not change the source of this module which is currently running 
      If sName$ <> "VersionControl" Then 
       ' Import relevant source file if it exists 
       If .VBComponents(i%).Type = 1 Then 
        ' Standard Module 
        .VBComponents.Remove .VBComponents(sName$) 
        .VBComponents.Import fileName:=ProjectFolder & sName$ & ".bas" 
       ElseIf .VBComponents(i%).Type = 2 Then 
        ' Class 
        .VBComponents.Remove .VBComponents(sName$) 
        .VBComponents.Import fileName:=ProjectFolder & sName$ & ".cls" 
       ElseIf .VBComponents(i%).Type = 3 Then 
        ' Form 
        .VBComponents.Remove .VBComponents(sName$) 
        .VBComponents.Import fileName:=ProjectFolder & sName$ & ".frm" 
       ElseIf .VBComponents(i%).Type = 100 Then 
        ' Document 
        Dim TempVbComponent, FileContents$ 
        ' Import the document. This will come in as a class with an increment suffix (1) 
        Set TempVbComponent = .VBComponents.Import(ProjectFolder & sName$ & ".bas") 

        ' Delete any lines of data in the document 
        If .VBComponents(i%).CodeModule.CountOfLines > 0 Then .VBComponents(i%).CodeModule.DeleteLines 1, .VBComponents(i%).CodeModule.CountOfLines 

        ' Does this file contain any source data? 
        If TempVbComponent.CodeModule.CountOfLines > 0 Then 
         ' Pull the lines into a string 
         FileContents$ = TempVbComponent.CodeModule.Lines(1, TempVbComponent.CodeModule.CountOfLines) 
         ' And copy them to the correct document 
         .VBComponents(i%).CodeModule.InsertLines 1, FileContents$ 
        End If 

        ' Remove the temporary document class 
        .VBComponents.Remove TempVbComponent 
        Set TempVbComponent = Nothing 

       Else 
        ' UNHANDLED/UNKNOWN COMPONENT TYPE 
       End If 
      End If 
      Next i 
     End With 

End Sub 
+2

Darei +2 se possibile. In primo luogo, hai rivelato la vera causa dell'errore: modificare l'array live; è stato un errore molto stupido, non ero molto sicuro di come funzionassero quegli array.Si scopre che in realtà sono oggetti 'Collection'. In secondo luogo, il tuo codice gestisce correttamente _differenti tipi di moduli_ e li salva con l'estensione di file corretta. Ho modificato il mio codice per farlo anche un po 'di tempo fa, è piuttosto importante; +1 per mostrare il tuo codice che lo fa, così la gente lo saprà. – CamilB

1

OP qui ... Sono riuscito a risolvere questo strano problema, ma non ho trovato una vera soluzione. Ecco cosa ho fatto.

  1. Il mio primo tentativo dopo la pubblicazione la domanda era questa (spoiler: si quasi lavorato):

    Mantenere la rimozione separata di importare, ma nello stesso procedimento. Ciò significa che ho avuto 3 loop: uno per memorizzare un elenco dei nomi dei moduli (come semplici stringhe), un altro per rimuovere i moduli e un altro per importare i moduli dai file (in base ai nomi che sono stati memorizzati nell'elenco di cui sopra) .

    Il problema: alcuni moduli erano ancora nel progetto al termine del ciclo di rimozione. Perché? Non so spiegare. Lo contrassegnerò come problema stupido no. 1. Ho quindi provato a effettuare la chiamata Remove per ogni modulo all'interno di un loop che continuava a provare a rimuovere quel singolo modulo fino a quando non è stato possibile trovarlo nel progetto. Questo è rimasto bloccato in un ciclo infinito per un certo modulo - Non posso dire cosa c'è di così speciale in quel particolare.

    Alla fine ho capito che i moduli sono stati rimossi solo dopo che Excel ha trovato del tempo per cancellare i suoi pensieri. Questo non ha funzionato con Application.Wait(). Il codice VBA attualmente in esecuzione in realtà doveva terminare perché ciò accadesse. Strano.

  2. secondo work-around tentativo (spoiler: ancora una volta, quasi lavorato):

    Per dare Excel il tempo necessario per respirare dopo traslochi, ho messo l'anello di rimozione all'interno di un gestore di scatto del tasto (senza il "chiama Rimuovi fino a quando non è andato" ciclo), e il ciclo di importazione nel gestore dei clic di un altro pulsante. Certo, avevo bisogno dell'elenco dei nomi dei moduli, quindi ho creato una serie globale di stringhe. È stato creato nel gestore dei clic, prima del ciclo di rimozione, e doveva essere accessibile dal ciclo di importazione. Dovrebbe aver funzionato, giusto?

    Il problema: il suddetto array di stringhe era vuoto all'avvio del ciclo di importazione (all'interno dell'altro gestore di clic). È stato sicuramente lì quando è terminato il ciclo di rimozione: l'ho stampato con Debug.Print. Immagino che sia stato disallocato dalle rimozioni (??). Questo sarebbe il problema stupido no. 2. Senza l'array di stringhe contenente i nomi dei moduli, il ciclo di importazione non ha fatto nulla, quindi questa soluzione non ha funzionato.

  3. Soluzione finale e funzionale. Questo funziona.

    Ho eseguito il numero di work-around 2 e, invece di memorizzare i nomi dei moduli in un array di stringhe, li ho memorizzati in una riga di un foglio ausiliario (ho chiamato questo foglio "Devel").

Questo era. Se qualcuno può spiegare il problema stupido no. 1 e problema stupido no. 2, ti prego, fallo. Probabilmente non sono così stupidi - Sono ancora all'inizio con VBA, ma ho una solida conoscenza della programmazione in altre lingue (sane e moderne).

Potrei aggiungere il codice per illustrare il problema stupido no. 2, ma questa risposta è già lunga. Se quello che ho fatto non è stato chiaro, lo posizionerò qui.

+1

Speculazione pura: suppongo che il motore VBA di Excel non possa gestire la rimozione di moduli nel mezzo di una funzione VBA presumibilmente compilata. (Cosa c'è da fare, ricompilare il VBA e sostituirlo per finire il lavoro? Troppo difficile.) Aspettare finché non sei fuori dalla funzione VBA ha senso per me. –

+0

@ todda.speot.is: Sarebbe logico, ma alcuni moduli sono stati rimossi durante il ciclo (uno che conosco, almeno, potrebbe essersi comportato in modo strano con altri, prima che iniziassi a capire cosa stava succedendo sopra). VBA si è bloccato solo su un modulo specifico e si è rifiutato di procedere in seguito. – CamilB

+1

Altre speculazioni: alcuni dei moduli avevano variabili globali e altri no. Quelli senza potrebbero essere rimossi bene, perché non c'è bisogno di ricompilare gli altri moduli. Quelli con variabili globali (o forse riferiti dal modulo su cui la tua funzione stava girando) non potevano essere rimossi fino alla fine dell'esecuzione. –

1

Per evitare duplicati durante l'importazione, ho modificato lo script con seguente strategia:

  • Rinomina
  • modulo di importazione
  • Cancella modulo rinominato modulo esistente

Non ho più duplicato durante l'importazione.


Sub SaveCodeModules() 

'This code Exports all VBA modules 
Dim i As Integer, name As String 

With ThisWorkbook.VBProject 
For i = .VBComponents.Count To 1 Step -1 

    name = .VBComponents(i).CodeModule.name 

    If .VBComponents(i).Type = 1 Then 
     ' Standard Module 
     .VBComponents(i).Export Application.ThisWorkbook.Path & "\trunk\" & name & ".module" 
    ElseIf .VBComponents(i).Type = 2 Then 
     ' Class 
     .VBComponents(i).Export Application.ThisWorkbook.Path & "\trunk\" & name & ".classe" 
    ElseIf .VBComponents(i).Type = 3 Then 
     ' Form 
     .VBComponents(i).Export Application.ThisWorkbook.Path & "\trunk\" & name & ".form" 
    Else 
     ' DO NOTHING 
    End If 
Next i 
End With 

End Sub 

Sub ImportCodeModules() 

Dim i As Integer 
Dim delname As String 
Dim modulename As String 

With ThisWorkbook.VBProject 
For i = .VBComponents.Count To 1 Step -1 

    modulename = .VBComponents(i).CodeModule.name 

    If modulename <> "VersionControl" Then 

     delname = modulename & "_to_delete" 

     If .VBComponents(i).Type = 1 Then 
      ' Standard Module 
      .VBComponents(modulename).name = delname 
      .VBComponents.Import Application.ThisWorkbook.Path & "\trunk\" & modulename & ".module" 
      .VBComponents.Remove .VBComponents(delname) 

     ElseIf .VBComponents(i).Type = 2 Then 
      ' Class 
      .VBComponents(modulename).name = delname 
      .VBComponents.Import Application.ThisWorkbook.Path & "\trunk\" & modulename & ".classe" 
      .VBComponents.Remove .VBComponents(delname) 

     ElseIf .VBComponents(i).Type = 3 Then 
      ' Form 
      .VBComponents.Remove .VBComponents(modulename) 
      .VBComponents.Import Application.ThisWorkbook.Path & "\trunk\" & modulename & ".form" 
     Else 
      ' DO NOTHING 
     End If 

    End If 
Next i 

End With 

End Sub 

codice da incollare in un nuovo modulo "VersionControl"

0

La ridenominazione, importazione ed eliminare soluzione non ha funzionato nel mio caso. Sembra (ma questa è pura speculazione) che Excel possa salvare gli oggetti compilati nel suo file .XLMS e, quando questo file viene riaperto, questi oggetti vengono ricaricati in memoria prima che si verifichi la funzione ThisWorkbook_open. E questo porta alla ridenominazione (o alla rimozione) di alcuni moduli per fallire o essere ritardata (anche quando si tenta di forzarla con la chiamata DoEvents). L'unica soluzione che ho trovato è utilizzare il formato binario .XLS. Per qualche oscuro motivo (ho il sospetto che gli oggetti compilati non siano raggruppati nel file), funziona per me.

È necessario sapere che non sarà possibile reimportare qualsiasi modulo che è/che è stato utilizzato o referenziato nel momento in cui viene eseguito il codice di importazione (la rinomina avrà esito negativo con errore 32813/la rimozione del modulo verrà ritardata fino a quando non tenterai di importare aggiungendo fastidiosi "1 alla fine dei nomi dei moduli".Ma per qualsiasi altro modulo, dovrebbe funzionare.

Se tutto il codice sorgente deve essere gestito, una soluzione migliore sarebbe quella di "costruire" la cartella di lavoro da zero utilizzando alcuni script o uno strumento, o passare a un linguaggio di programmazione più adatto (cioè uno che non vive all'interno di un software della suite Office;) Non l'ho provato ma si potrebbe guardare qui: Source control of Excel VBA code modules.

1

Sono stato alle prese con questo problema per giorni. Ho creato un sistema di controllo delle versioni rozzo simile a questo, sebbene non utilizzassi gli array. Il modulo di controllo versione viene importato su Workbook_Open e quindi viene richiamata la procedura di avvio per importare tutti i moduli elencati nel modulo di controllo versione. Tutto funziona alla grande, eccetto che Excel ha iniziato a creare moduli di controllo delle versioni duplicati perché importerebbe il nuovo modulo prima che la cancellazione di quella esistente fosse completata. Ci ho lavorato aggiungendo Delete al modulo precedente. Il problema è che c'erano ancora due procedure con lo stesso nome. Chip Pearson ha un codice per cancellare una procedura a livello di codice, quindi ho cancellato il codice di avvio dal modulo di controllo della versione precedente. Ancora, mi sono imbattuto in un problema in cui la procedura non era stata eliminata dal momento in cui è stata chiamata la procedura di avvio. Ho finalmente trovato una soluzione su un altro thread di overflow dello stack che è così semplice da farmi desiderare di mettere la mia testa attraverso un muro. Tutto quello che dovevo fare era cambiare il modo in cui io chiamo la mia procedura di avvio utilizzando

Application.OnTime Now + TimeValue("00:00:01"), "StartUp"  

Tutto funziona perfettamente ora. Tuttavia, probabilmente tornerò indietro e cancellerò la ridenominazione del modulo ora in uso e cancellerò la seconda procedura e vedrò se questo risolve da solo il mio problema originale. Ecco l'altro thread con la soluzione ...

Source control of Excel VBA code modules