Сброс изменений, внесенных в VBProject.VBComponents в Excel с использованием VBA

Я испытываю некоторые странные причуды в Excel, в то время как программно удаляет модули, а затем реимпортирует их из файлов. В принципе, у меня есть модуль с именем VersionControl, который должен экспортировать мои файлы в предопределенную папку и reimport их по требованию. Это код для реимпортации (проблема с ним описана ниже):

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

После выполнения этого я заметил, что некоторые модули больше не отображаются, а некоторые - дубликаты (например, mymodule и mymodule1). При переходе через код стало очевидно, что некоторые модули все еще задерживаются после вызова Remove, и они становятся реимпортированными, пока они все еще находятся в проекте. Иногда это приводило к тому, что модуль имел суффикс 1, но иногда у меня были как оригинал, так и копия.

Есть ли способ сбросить вызовы на Remove и Import, чтобы они применялись? Я думаю, чтобы вызвать функцию Save после каждого, если она есть в объекте Application, хотя это может привести к потерям, если во время импорта все будет не так.

Идеи?

Изменить: изменил тег synchronization на version-control.

Ответ 1

Это живой массив, который вы добавляете и удаляете элементы во время итерации, тем самым изменяя номера индексов. Попробуйте обработать массив назад. Вот мое решение без обработки ошибок:

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

OP здесь... Мне удалось обойти эту странную проблему, но я не нашел правильного решения. Вот что я сделал.

  • Моя первая попытка после публикации вопроса была в этом (спойлер: он почти сработал):

    Продолжайте удаление отдельно от импорта, но в той же процедуре. Это означает, что у меня было 3 цикла: один для хранения списка имен модулей (в виде простых строк), другой для удаления модулей, а другой для импорта модулей из файлов (на основе имен, которые были сохранены в вышеупомянутом списке),

    Проблема: некоторые модули все еще были в проекте, когда цикл удаления закончился. Зачем? Я не могу объяснить. Я отмечу это как глупую проблему нет. 1. Затем я попытался разместить вызов Remove для каждого модуля внутри цикла, который пытался удалить этот единственный модуль, пока он не смог найти его в проекте. Это застряло в бесконечном цикле для определенного модуля - я не могу сказать, что так особенного в этом конкретном.

    В конце концов я понял, что модули были действительно удалены после того, как Excel находит некоторое время, чтобы очистить свои мысли. Это не с Application.Wait(). Текущий код VBA на самом деле нужен, чтобы это произошло. Weird.

  • Вторая попытка совместной работы (спойлер: опять же, она почти сработала):

    Чтобы предоставить Excel необходимое время для дыхания после удаления, я поместил цикл удаления внутри обработчика нажатия кнопки (без цикла "Удалить до конца" ) и цикла импорта в обработчике кликов другой кнопки. Конечно, мне нужен список имен модулей, поэтому я сделал его глобальным массивом строк. Он был создан в обработчике кликов, перед циклом удаления, и он должен был получить доступ к циклу импорта. Должно было работать, правильно?

    Проблема: вышеупомянутый строковый массив был пуст при запуске цикла импорта (внутри другого обработчика кликов). Это было точно, когда цикл удаления закончился - я напечатал его с помощью Debug.Print. Я предполагаю, что он был удален из-за удаления (??). Это была бы глупая проблема. 2. Без строкового массива, содержащего имена модулей, цикл импорта ничего не сделал, поэтому этот обход не удался.

  • Окончательный, функциональный обход. Это работает.

    Я взял Work-around номер 2 и вместо хранения имен модулей в массиве строк я сохранил их в строке вспомогательного листа (я назвал этот лист "Devel" ).

Вот оно. Если кто-нибудь может объяснить глупую проблему нет. 1 и глупой проблемой нет. 2, прошу вас, сделайте это. Вероятно, они не такие глупые - я все еще начинаю с VBA, но у меня есть твердые знания программирования на других (нормальных и современных) языках.

Я мог бы добавить код для иллюстрации глупой проблемы нет. 2, но этот ответ уже длинный. Если то, что я сделал, было неясно, я поместил его здесь.

Ответ 3

Чтобы избежать дублирования при импорте, я изменил script на следующую стратегию:

  • Переименовать существующий модуль
  • Модуль импорта
  • Удалить переименованный модуль

У меня больше нет дубликатов во время импорта.


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

Код для вставки в новый модуль "VersionControl"

Ответ 4

Я борюсь с этой проблемой в течение нескольких дней. Я построил грубую систему управления версиями, подобную этой, хотя и не используя массивы. Модуль управления версиями импортируется в Workbook_Open, а затем запускается процедура запуска для импорта всех модулей, перечисленных в модуле управления версиями. Все отлично работает, за исключением того, что Excel начал создавать дублирующие модули управления версиями, поскольку он импортировал новый модуль до удаления существующего. Я работал над этим, добавив Delete в предыдущий модуль. Проблема тогда заключалась в том, что все еще были две процедуры с тем же именем. У Чипа Пирсона есть код для удаления программы программно, поэтому я удалил код запуска из более старого модуля управления версиями. Тем не менее, у меня возникла проблема, когда процедура не была удалена к моменту запуска процедуры запуска. Я, наконец, нашел решение в другом потоке, которое так просто, что заставляет меня хотеть просунуть голову через стену. Все, что мне нужно было сделать, это изменить способ, которым я вызываю свою процедуру запуска, используя

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

Все работает отлично. Хотя, я, вероятно, вернусь и удалю теперь избыточное переименование модуля и удалю вторую процедуру и посмотрю, решит ли это мою первоначальную проблему. Вот еще один поток с решением...

Управление версиями модулей кода Excel VBA

Ответ 5

Переименование, импорт и удаление обходного пути не было выполнено в моем случае. Кажется (но это чисто предположение), что Excel может сохранить скомпилированные объекты в своем файле .XLMS, и когда этот файл будет снова открыт, эти объекты будут перезагружены в памяти до того, как произойдет функция ThisWorkbook_open. И это приводит к переименованию (или удалению) определенных модулей с ошибкой или задержке (даже при попытке принудительно вызвать вызов DoEvents). Единственным обходным решением, которое я нашел, является использование бинарного формата .XLS. По какой-то неясной причине (я подозреваю, что скомпилированные объекты не связаны в файле), это работает для меня.

Вы должны знать, что вы не сможете повторно импортировать какой-либо модуль, который был использован или был указан в момент запуска вашего кода импорта (переименование не будет выполнено с ошибкой 32813/удаление модуля будет отложено до тех пор, пока после того, как вы попытаетесь импортировать, добавив раздражающий "1 в конце ваших имен модулей" ). Но для любого другого модуля он должен работать.

Если вам нужно управлять всем исходным кодом, лучшим решением будет "построить" вашу книгу с нуля с помощью некоторого script или инструмента или переключиться на более подходящий язык программирования (т.е. тот, который не жить внутри программного пакета Office;) Я не пробовал, но вы могли бы посмотреть здесь: Управление версиями модулей кода Excel VBA.