2016-11-22 33 views
4

我寫了一個版本控制模塊。只要我或其他維護人員登錄,AutoExec宏就會啓動它。它會查找自上次更新後創建或修改的數據庫對象,然後向Versions表中添加一個條目,然後打開該表(已過濾爲最後一條記錄),因此我可以鍵入我執行的更改的摘要。如何檢查VBA模塊何時被修改?

它非常適用於表格,查詢,表單,宏等,但我無法讓它正確地爲模塊工作。

我發現提出一個最後修改日期兩種不同的屬性...

CurrentDB.Containers("Modules").Documents("MyModule").Properties("LastUpdated").Value 
CurrentProject.AllModules("MyModule").DateModified 

第一個(CurrentDB)始終顯示爲創建日期,除非你修改的說明「LASTUPDATED」該模塊或界面中的某些東西。這告訴我,這個屬性純粹是爲容器對象 - 而不是它的內容。

第二個工程更好。它準確地顯示了我修改和編譯/保存模塊的日期。唯一的問題是,當您保存或編譯模塊時,它會再次保存/編譯所有模塊,因此將DateModified字段設置爲全局相同的日期。它有點違背了在單個模塊上使用DateModified屬性的目的嗎?

所以我的下一步行動將會更加劇烈。我想我需要維護所有模塊的列表,並使用VBA Extensions對每個模塊中的代碼行進行計數。然後,如果代碼行與列表中記錄的不同 - 那麼我知道模塊已被修改 - 除了「自上次檢查以來」我不知道什麼時候「

有沒有人更好的方法?我寧願不採取我的下一步行動,因爲我可以看到它顯着影響數據庫性能(以糟糕的方式)

+1

將模塊導出爲文本文件非常簡單,那麼您可以比較全文。我不會建議只檢查行數,因爲有人可以在一行中更改代碼,並且不會被檢測到。幾年前,我喜歡'WinDiff.exe',它會顯示兩個文件的差異(我發現它仍然可以下載)。我建立了一個具有所有對象,控件,屬性和值的'數據字典'數據庫,然後可以比較舊的和新的來顯示更改。此外,如果您使用SourceSafe(或現代版本),可以跟蹤更改。 –

+0

無恥的插件,但[Rubberduck加載項](https://github.com/rubberduck-vba/Rubberduck)具有源代碼管理功能。 – Comintern

+0

@Comintern當然,但它不處理任何Access特定的組件。 –

回答

2

這裏有一個簡單的建議:

  1. 計算MD5哈希每個模塊。
  2. 將其存儲在版本表中。
  3. 在AutoExec中爲每個模塊重新計算它並將其與Versions表中的值進行比較。如果不同,你可以認爲它已經改變了(雖然MD5對安全性不好,但對於完整性來說它仍然是可靠的)。

要獲得使用VBE擴展模塊的文字,你可以做

Dim oMod As CodeModule 
Dim strMod As String 
Set oMod = VBE.ActiveVBProject.VBComponents(1).CodeModule 
strMod = oMod.Lines(1, oMod.CountOfLines) 

然後你就可以從this answer如下使用以下修改MD5哈希函數,你可以把每個哈希模塊來存儲它,然後在你的AutoExec中進行比較。

Public Function StringToMD5Hex(s As String) As String 
    Dim enc 
    Dim bytes() As Byte 
    Dim outstr As String 
    Dim pos As Integer 
    Set enc = CreateObject("System.Security.Cryptography.MD5CryptoServiceProvider") 
    'Convert the string to a byte array and hash it 
    bytes = StrConv(s, vbFromUnicode) 
    bytes = enc.ComputeHash_2((bytes)) 
    'Convert the byte array to a hex string 
    For pos = 0 To UBound(bytes) 
     outstr = outstr & LCase(Right("0" & Hex(bytes(pos)), 2)) 
    Next 
    StringToMD5Hex = outstr 
    Set enc = Nothing 
End Function 
+0

比什麼更簡單?這完全是我給出的完全相同的答案,除非您將數據存儲在表中,並且您沒有提及在模塊被重命名或刪除時無效的陳舊記錄的警告。研究一種延遲綁定到.net散列函數的方式的榮譽,並沒有意識到你可以做到這一點。 +1。 –

+0

@ Mat'sMug哈哈!對不起:PI意味着比OP的段落更簡單「因此,我的下一步行動將變得更加激烈,我想我需要維護所有模塊的列表,並且使用VBA統計每個模塊中的代碼行數然後,如果代碼行與列表記錄不同 - 那麼我知道該模塊已被修改 - 除了「自上次檢查以來」,我只是不知道什麼時候「 – Blackhawk

+0

我也太喜歡瞭如何遲到.NET功能 - 一直想知道如何做到這一點。去看看。實際上我曾想過使用MD5或我自己的校驗和算法(它不是火箭科學),但認爲它會比它的價值更麻煩。至於在哪裏存儲MD5哈希,我正在考慮將它作爲一個屬性存儲在每個模塊上。還沒有嘗試過 - 但它會避免使用表格並解決重命名問題。 – DHW

4

當模塊被修改時,您不可能知道。 VBIDE API甚至不告訴你是否模塊被修改,所以你必須自己弄清楚。


VBIDE API令人難以忍受 - 正如您已經注意到的那樣。

Rubberduck尚未處理特定於主機的組件(例如表,查詢等),但其解析器在自上一次解析後判斷模塊是否被修改方面做了很好的工作。

「自上次檢查後修改」實際上是您所需要知道的。你不能靠行雖然統計,因爲這個:

Option Explicit 

Sub DoSomething 
    'todo: implement 
End Sub 

將與此相同:

Option Explicit 

Sub DoSomething 
    DoSomethingElse 42 
End Sub 

而且很明顯,你會希望這種改變被拾起和跟蹤。比較每一行代碼中的每個字符都可以工作,但速度要快得多。

總體思路是獲取CodeModule的內容,對其進行散列,然後與之前的內容散列進行比較 - 如果有任何修改,我們正在查看「髒」模塊。它是C#,我不知道是否有一個COM庫可以容易地從VBA中散列字符串,但最糟糕的情況是,您可以在.NET中編譯一個小型實用程序DLL,公開一個需要String並返回的COM可見函數一個散列,不應該太複雜。

下面是Rubberduck.VBEditor.SafeComWrappers.VBA.CodeModule相關的代碼,如果它的任何幫助:

private string _previousContentHash; 
public string ContentHash() 
{ 
    using (var hash = new SHA256Managed()) 
    using (var stream = Content().ToStream()) 
    { 
     return _previousContentHash = new string(Encoding.Unicode.GetChars(hash.ComputeHash(stream))); 
    } 
} 

public string Content() 
{ 
    return Target.CountOfLines == 0 ? string.Empty : GetLines(1, CountOfLines); 
} 

public string GetLines(Selection selection) 
{ 
    return GetLines(selection.StartLine, selection.LineCount); 
} 

public string GetLines(int startLine, int count) 
{ 
    return Target.get_Lines(startLine, count); 
} 

這裏TargetMicrosoft.Vbe.Interop.CodeModule對象 - 如果你在VBA的土地是那麼這只是一個CodeModule,從VBA擴展性庫;這樣的事情:

Public Function IsModified(ByVal target As CodeModule, ByVal previousHash As String) As Boolean 

    Dim content As String 
    If target.CountOfLines = 0 Then 
     content = vbNullString 
    Else 
     content = target.GetLines(1, target.CountOfLines) 
    End If 

    Dim hash As String 
    hash = MyHashingLibrary.MyHashingFunction(content) 

    IsModified = (hash <> previousHash) 

End Function 

所以是的,你的「激烈」的解決方案几乎是唯一可靠的方法去實現它。幾件事情要記住:

  • 「保持所有模塊的列表」,將工作,但如果你只儲存模塊名稱和模塊改名,緩存是陳舊的,你需要一種方法來使它無效。
  • 如果你存儲每個模塊對象的ObjPtr而不是他們的名字,我不確定它在VBA中是否可靠,但我可以告訴你,通過COM互操作,COM對象的哈希碼不會始終一致在調用之間 - 所以你會有一個陳舊的緩存和一種方法來使它失效,也是如此。儘管如此,可能不是100%VBA解決方案的問題。

我會去Dictionary存儲模塊的對象指針作爲一個關鍵,他們的內容哈希作爲一個值。


這就是說作爲Rubberduck項目的管理員,我寧願看到你加入我們,幫助我們整合全功能的源代碼控制(即特定主機的功能)直接進入VBE =)

Rubberduck's Source Control panel

+0

墊子,感謝您花時間在您的答案中提供如此多的細節。我不喜歡使用第三方API。根據我的經驗,它們變得更加麻煩「我們必須重新安裝訪問並且數據庫不能正常工作」。理想情況下,我想完全保留在覈心參考內,但如果我需要使用VBE,我可以彎曲,因爲它非常標準。足夠容易做一個後期綁定和避免參考問題。 – DHW

+0

@DHW無論你是遲綁定還是早綁定第三方庫都沒有區別,如果庫失敗了,它會失敗。如果庫沒有出現在它正在運行的機器上,那麼只有在運行時纔會知道它是否與遲綁定,編譯時與早期綁定。對於早期綁定庫的恐懼似乎是不合理的,特別是當你談論VBIDE API時,任何運行VBA的計算機(甚至是腳本運行時 - 這些庫都是任何Windows盒子上的標準)都是標準的。 –

+2

除非在更高版本的Access版本中進行了更改:Early Binding將您與特定版本的DLL聯繫在一起。後期綁定只會將您與任何具有您要查找的對象的dll綁定。因此,以Microsoft Excel庫爲例。如果我早期與版本15綁定 - 並且機器較舊,並且只有版本14,那麼將會缺少引用問題。但是,如果我延遲綁定 - 它會看到我正在查找「Application.Excel」對象並將其解析爲任何存在的庫。 – DHW

2

我想我要補充,我想出了一個哈希/校驗生成模塊的最終代碼,因爲這真的是件我失蹤。感謝@BlackHawk回答,通過表明您可以延遲.NET類的綁定來填補這個空白 - 現在這將爲我開創很多可能性。

我已完成編寫我的版本檢查器。我遇到的幾個警告使得很難依賴LastUpdated日期。

  1. 調整表或查詢中的列的大小更改了LastUpdated日期。
  2. 編譯編譯所有模塊的任何模塊,從而更新了所有模塊的LASTUPDATED日期(如已經指出的)
  3. 添加過濾器,以查看模式形式使形式的過濾器字段中被更新,這又更新LASTUPDATED日期。
  4. 當窗體或報表上使用SaveAsText,改變打印機或顯示器驅動程序會影響PrtDevMode編碼,所以需要計算校驗

對於表之前帶他們出去我建了一個字符串,它是表名稱,所有字段名稱及其大小和數據類型的拼接。然後我計算了這些散列。

對於查詢,我簡單地計算了SQL上的散列值。

對於模塊,宏,表單和報告我使用Application.SaveAsText將其保存到臨時文件。然後我把這個文件讀入一個字符串並計算出一個散列值。對於表單和報告,我沒有開始添加到字符串,直到「開始」行通過。

似乎現在工作,我還沒有遇到過任何情況下,當事情沒有真正改變時,它會提示版本修訂。

爲了計算校驗和或散列,我構建了一個名爲CryptoHash的類模塊。以下是完整的源代碼。我將字節數組優化爲十六進制字符串轉換更快。

Option Compare Database 
Option Explicit 

Private objProvider As Object   ' Late Bound object variable for MD5 Provider 
Private objEncoder As Object   ' Late Bound object variable for Text Encoder 
Private strArrHex(255) As String  ' Hexadecimal lookup table array 

Public Enum hashServiceProviders 
    MD5 
    SHA1 
    SHA256 
    SHA384 
    SHA512 
End Enum 

Private Sub Class_Initialize() 
    Const C_HEX = "ABCDEF" 
    Dim intIdx As Integer    ' Our Array Index Iteration variable 

    ' Instantiate our two .NET class objects 
    Set objEncoder = CreateObject("System.Text.UTF8Encoding") 
    Set objProvider = CreateObject("System.Security.Cryptography.MD5CryptoServiceProvider") 

    ' Initialize our Lookup Table (array) 
    For intIdx = 0 To 255 
    ' A byte is represented within two hexadecimal digits. 
    ' When divided by 16, the whole number is the first hex character 
    '      the remainder is the second hex character 
    ' Populate our Lookup table (array) 
    strArrHex(intIdx) = Mid(C_HEX, (intIdx \ 16) + 1, 1) & Mid(C_HEX, (intIdx Mod 16) + 1, 1) 
    Next 

End Sub 

Private Sub Class_Terminate() 
    ' Explicity remove the references to our objects so Access can free memory 
    Set objProvider = Nothing 
    Set objEncoder = Nothing 
End Sub 

Public Property Let Provider(NewProvider As hashServiceProviders) 

    ' Switch our Cryptographic hash provider 
    Select Case NewProvider 
    Case MD5: 
     Set objProvider = CreateObject("System.Security.Cryptography.MD5CryptoServiceProvider") 
    Case SHA1: 
     Set objProvider = CreateObject("System.Security.Cryptography.SHA1CryptoServiceProvider") 
    Case SHA256: 
     Set objProvider = CreateObject("System.Security.Cryptography.SHA256Managed") 
    Case SHA384: 
     Set objProvider = CreateObject("System.Security.Cryptography.SHA384Managed") 
    Case SHA512: 
     Set objProvider = CreateObject("System.Security.Cryptography.SHA512Managed") 
    Case Else: 
     Err.Raise vbObjectError + 2029, "CryptoHash::Provider", "Invalid Provider Specified" 
    End Select 

End Property 

' Converts an array of bytes into a hexadecimal string 
Private Function Hash_BytesToHex(bytArr() As Byte) As String 
    Dim lngArrayUBound As Long   ' The Upper Bound limit of our byte array 
    Dim intIdx As Long     ' Our Array Index Iteration variable 

    ' Not sure if VBA re-evaluates the loop terminator with every iteration or not 
    ' When speed matters, I usually put it in its own variable just to be safe 
    lngArrayUBound = UBound(bytArr) 

    ' For each element in our byte array, add a character to the return value 
    For intIdx = 0 To lngArrayUBound 
    Hash_BytesToHex = Hash_BytesToHex & strArrHex(bytArr(intIdx)) 
    Next 
End Function 

' Computes a Hash on the supplied string 
Public Function Compute(SourceString As String) As String 
    Dim BytArrData() As Byte   ' Byte Array produced from our SourceString 
    Dim BytArrHash() As Byte   ' Byte Array returned from our MD5 Provider 

    ' Note: 
    ' Because some languages (including VBA) do not support method overloading, 
    ' the COM system uses "name mangling" in order to allow the proper method 
    ' to be called. This name mangling appends a number at the end of the function. 
    ' You can check the MSDN documentation to see how many overloaded variations exist 

    ' Convert our Source String into an array of bytes. 
    BytArrData = objEncoder.GetBytes_4(SourceString) 

    ' Compute the MD5 hash and store in an array of bytes 
    BytArrHash = objProvider.ComputeHash_2(BytArrData) 

    ' Convert our Bytes into a hexadecimal representation 
    Compute = Hash_BytesToHex(BytArrHash) 

    ' Free up our dynamic array memory 
    Erase BytArrData 
    Erase BytArrHash 

End Function 
+0

我不知道這是否適合在Code Review網站上詢問,但我想知道如果您願意分享您的解決方案的完整源代碼,請在此處描述。如果不是這個答案,那麼也許作爲一個外部鏈接。我非常有興趣使用Git爲Access VBA項目實施源代碼控制,這將是一個巨大的幫助。我們沒有管理員權限來嘗試使用RubberDuck,所以我仍然試圖構建自己的解決方案。 – BarrettNashville

+1

我不介意分享。我沒有分享版本系統的完整代碼,因爲它與問題無關。我已將它放到我的Google雲端硬盤上。該代碼完全評論,所以它應該是自我解釋。 https://drive.google.com/file/d/0B_uoYoBD3vqMMVpuM0tyWEFmMEk如果您最終使用它,只需在信用到期時給予貸記。 Thx爲upvotes。 – DHW

+0

太棒了!謝謝。奇蹟般有效。代碼很好的評論和AutoExec宏是不言自明的。很酷!我很好奇你爲什麼在用戶打開數據庫時將版本檢查放在AutoExec中,而不是保存數據庫或關閉時?我確信有一個正當的理由,我只是習慣於在做了一堆更改之後,在退出之前做代碼簽入,而不是當我第一次進入代碼時。 – BarrettNashville