Guides · Version control

Import VBA Modules and UserForms Without Breaking References

Published · Tested with Microsoft 365 Excel 16.0.20430.20092 (64-bit), PowerShell 7.6.6, Windows 11 Pro

Exporting a workbook's VBA to text is the easy half. Putting it back into another workbook is where things break: Import renames a module that already exists, a UserForm without its .frx refuses to load, the code of ThisWorkbook arrives as a new class, and code that ran yesterday stops with “Can't find project or library”. This guide reproduces each of those in Excel and builds one PowerShell script that imports into a copy of the workbook, restores the references and checks the result by exporting it again.

Before you start

Every output below was copied from test scripts written for this guide and run three times each. The source workbook, orders.xlsm, is invented: a standard module Orders, a class OrderLine, code in ThisWorkbook and Sheet1, a UserForm frmOrder with a label, a text box, a button and an image, and a reference to Microsoft Scripting Runtime for a Scripting.Dictionary.

What goes wrong when you import

VBComponents.Import “adds a component to a project from a file; returns the newly added component” (Microsoft Learn). The page says nothing about what happens when the name is taken or the file is not a plain module. Into a new, empty workbook:

1. A form without its .frx. The .frm holds the form's code and a pointer, OleObjectBlob = "frmOrder.frx":0000; the controls themselves are in the binary .frx. Leave it behind (an email attachment filter, a .gitignore, a copy of “just the code”) and the import fails:

Import failed: Errors during load. Refer to '.\frmOrder.log' for details (0x800AEA9D)
components after: ThisWorkbook(100), Sheet1(100)

# frmOrder.log, written next to the .frm
Line 8: Property OleObjectBlob in frmOrder had an invalid file reference.

No half-loaded form stays behind; you only get an error and a log file. The .frx is not optional decoration: the same form exported to a 2,584-byte .frx without its image control and 9,752 bytes with it.

2. A name that already exists. The target workbook already had an older Orders module, an OrderLine class and a frmOrder form (numbers are component types: 1 module, 2 class, 3 form, 100 sheet or workbook):

before: ThisWorkbook(100), Sheet1(100), Orders(1), frmOrder(3), OrderLine(2)
Import over existing: Orders.bas -> Orders1, OrderLine.cls -> OrderLine1
frmOrder.frm over existing: Errors during load. Refer to '.\frmOrder.log' for details
after: ThisWorkbook(100), Sheet1(100), Orders(1), frmOrder(3), OrderLine(2), Orders1(1), OrderLine1(2)

# frmOrder.log
Line 2: The Form or MDIForm name frmOrder is already in use; cannot load this form.

The module and the class came in next to the old ones, as Orders1 and OrderLine1, with no error; any code that calls Orders.CountCustomers still runs the old version. The form failed outright. Removing the old component by its exact name first, then importing, gave the right names in all three runs:

Remove then Import: Orders.bas -> Orders, OrderLine.cls -> OrderLine, frmOrder.frm -> frmOrder (4 controls)

3. ThisWorkbook and sheet code. Their files are exported as .cls, and importing them does what it does for any class:

ThisWorkbook.cls -> ThisWorkbook1 (type 2); Sheet1.cls -> Sheet11 (type 2)
ThisWorkbook module lines: 0

Two new class modules, and the real ThisWorkbook still empty: event handlers such as Workbook_Open or Worksheet_Change would never fire from a class. Nothing in the exported header names the sheet either; the only sign is a pair of attributes an ordinary class does not have:

VERSION 1.0 CLASS
BEGIN
  MultiUse = -1  'True
END
Attribute VB_Name = "ThisWorkbook"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = True
Option Explicit

The fix is to leave the document module where it is and replace its code: CodeModule.DeleteLines, then CodeModule.AddFromString with the file minus that header.

4. Every import adds a blank line to a form. We imported frmOrder.frm into a new workbook, exported it, imported that export, three times in a row. The file grew by one line each time (20, 21, 22, 23 lines), all of them empty lines between the header and Option Explicit. Harmless to run, but every round trip shows up as a change in git, and the import check below would fail on it. The script removes the added lines after each form import.

5. The .frx changes on every export. Four exports of the same, unchanged orders.xlsm:

frmOrder.frm : 75A1DA7C 75A1DA7C 75A1DA7C 75A1DA7C
frmOrder.frx : C3A7139A E6DFEEF1 FE0DE9ED D9090197
Orders.bas : 8CDB3F9B 8CDB3F9B 8CDB3F9B 8CDB3F9B
ThisWorkbook.cls : BD73C173 BD73C173 BD73C173 BD73C173

(The first 8 characters of each SHA256.) Eight bytes at offset 1156 of this .frx are a Windows timestamp equal to the time of the export, and between 5 and 12 other bytes changed from one export to the next. So a byte-for-byte comparison of a .frx never passes, and in git the .frx changes whenever you export, even if nobody touched the form.

Why the references break

References (Tools › References in the VBA editor) belong to the project, not to the modules, so no exported file carries them. Import Orders.bas into a workbook that lacks Microsoft Scripting Runtime and run CountCustomers, which declares Dim seen As Scripting.Dictionary. The VBA editor opens with this message (read from the dialog by our test script, three runs out of three):

Compile error:

User-defined type not defined

The other classic is a reference that exists but points nowhere. We made the target reference a small library workbook, pricing-lib.xlsm, saved it, and moved the library away. The reference list then shows it as broken, with the path as its name (paths shortened):

  Scripting {420B2830-E718-11CF-893D-00A0C9054228} 1.0 broken=False path=C:\Windows\System32\scrrun.dll
  ...\lib\pricing-lib.xlsm  0.0 broken=True path=...\lib\pricing-lib.xlsm

Then we ran Stamp, a function that uses only VBA's own Left$, Format$ and Date, nothing from the library:

Compile error:

Can't find project or library

That is the confusing part: the error points at a built-in function, not at the reference. Microsoft's page on this error is blunt about it: “You can't run your code until all missing references are resolved”, and “unresolved references are prefixed with MISSING in the References dialog box” (Microsoft Learn). From code, the same state is Reference.IsBroken, which tells “whether the Reference object points to a valid reference in the registry” (Microsoft Learn).

Adding a reference back is References.AddFromGuid(guid, major, minor), which “searches the registry to find the reference that you want to add” (Microsoft Learn). What it did on this PC, three runs each:

AddFromGuid with a GUID not on this PC : Object library not registered (0x8002801D)
AddFromGuid Scripting, version 9.0      : Object library not registered (0x8002801D)
AddFromGuid Scripting, version 0.0      : added, as version 1.0
AddFromGuid Scripting, already there   : Name conflicts with existing module, project, or object library (0x800A802D)

So the import needs the GUID and version of every reference the source had, and has to skip the ones already there. One more detail from the export: adding a UserForm to a workbook adds a reference to Microsoft Forms 2.0 on its own, and the list should carry it too.

Step 1: export the references too

Add these lines to export-vba.ps1 from the git guide, inside the try block, right after the foreach loop that exports the components:

    # References the code needs, for the import step (VBA and Excel itself are built in).
    $references = @(foreach ($ref in $book.VBProject.References) {
            if ($ref.BuiltIn) { continue }
            [ordered]@{ name = $ref.Name; guid = $ref.GUID; major = $ref.Major; minor = $ref.Minor
                        fullPath = $(try { $ref.FullPath } catch { '' }); isBroken = $ref.IsBroken }
        })
    ConvertTo-Json -InputObject $references -Depth 3 |
        Set-Content -LiteralPath (Join-Path $staging 'vba-references.json') -Encoding utf8

The file is written before the manifest, so the manifest lists it and the manifest check covers it. VBA and Excel's own library are left out: every workbook has them and they cannot be removed. For orders.xlsm:

[
  {
    "name": "stdole",
    "guid": "{00020430-0000-0000-C000-000000000046}",
    "major": 2,
    "minor": 0,
    "fullPath": "C:\\Windows\\System32\\stdole2.tlb",
    "isBroken": false
  },
  {
    "name": "Office",
    "guid": "{2DF8D04C-5BFA-101B-BDE5-00AA0044DE52}",
    "major": 2,
    "minor": 8,
    "fullPath": "C:\\Program Files\\Common Files\\Microsoft Shared\\OFFICE16\\MSO.DLL",
    "isBroken": false
  },
  {
    "name": "Scripting",
    "guid": "{420B2830-E718-11CF-893D-00A0C9054228}",
    "major": 1,
    "minor": 0,
    "fullPath": "C:\\Windows\\System32\\scrrun.dll",
    "isBroken": false
  },
  {
    "name": "MSForms",
    "guid": "{0D452EE1-E08F-101A-852E-02608C4D0BB4}",
    "major": 2,
    "minor": 0,
    "fullPath": "C:\\WINDOWS\\system32\\FM20.DLL",
    "isBroken": false
  }
]

Step 2: import into a copy

import-vba.ps1 never touches the workbook you give it. It copies it, imports into the copy, and keeps the copy only if the check at the end passes:

# import-vba.ps1: imports a folder made by export-vba.ps1 into a COPY of a workbook, restores
# its references, and checks the result by exporting it again. Needs "Trust access to the VBA
# project object model" (File > Options > Trust Center), which this script never changes.
# Exit codes: 0 imported and checked; 1 the check failed (the copy is deleted);
#             2 bad input (nothing written); 3 the VBA project cannot be read (copy deleted).
param(
    [Parameter(Mandatory)] [string] $Workbook,        # imported into a copy; never changed
    [Parameter(Mandatory)] [string] $VbaDir,          # .bas/.cls/.frm/.frx + vba-references.json
    [Parameter(Mandatory)] [string] $OutputWorkbook   # the copy to create; must not exist
)
$ErrorActionPreference = 'Stop'
$extensions = @{ 1 = '.bas'; 2 = '.cls'; 3 = '.frm'; 100 = '.cls' }  # module, class, form, sheet/workbook

Add-Type -Namespace Win32Tools -Name Native -MemberDefinition @'
[DllImport("user32.dll")]
public static extern uint GetWindowThreadProcessId(System.IntPtr hWnd, out uint processId);
[DllImport("kernel32.dll")]
public static extern uint GetACP();
'@
# The VBA editor reads and writes these files in the Windows ANSI code page, not UTF-8.
$ansi = [Text.Encoding]::GetEncoding([int] [Win32Tools.Native]::GetACP())

function Get-ReferencePath($Reference) {
    # A MISSING type library can throw on FullPath: an empty path then, not a failed run.
    try { $Reference.FullPath } catch { '' }
}
function Get-ComponentName([string[]] $Lines) {
    foreach ($line in $Lines) { if ($line -match '^Attribute VB_Name = "(.+)"$') { return $Matches[1] } }
}
function Get-CodeBody([string[]] $Lines) {
    # Skips the header the editor writes (VERSION, BEGIN...END, Attribute VB_*): only the code
    # goes into a document module.
    $i = 0
    if ($Lines.Count -gt 0 -and $Lines[0] -like 'VERSION *') { $i++ }
    if ($i -lt $Lines.Count -and $Lines[$i] -cmatch '^(BEGIN|Begin)\b') {
        while ($i -lt $Lines.Count -and $Lines[$i] -cnotmatch '^(END|End)$') { $i++ }
        $i++
    }
    while ($i -lt $Lines.Count -and $Lines[$i] -like 'Attribute VB_*') { $i++ }
    if ($i -ge $Lines.Count) { return '' }
    $Lines[$i..($Lines.Count - 1)] -join "`r`n"
}
function Remove-AddedBlankLines($CodeModule, [string] $Body) {
    # Each import of a form adds an empty line at the top of its code: keep only the original ones.
    $wanted = 0
    while ($Body.Substring([Math]::Min($wanted * 2, $Body.Length)).StartsWith("`r`n")) { $wanted++ }
    $blank = 0
    while ($blank -lt $CodeModule.CountOfLines -and $CodeModule.Lines($blank + 1, 1) -eq '') { $blank++ }
    if ($blank -gt $wanted) { $CodeModule.DeleteLines(1, $blank - $wanted) }
}
function Compare-Export([string] $Name, [switch] $SizeOnly) {
    $expected = Join-Path $dir $Name
    $actual = Join-Path $checkDir $Name
    if (-not (Test-Path -LiteralPath $actual)) { $problems.Add("${Name}: not exported again"); return }
    $same = if ($SizeOnly) { (Get-Item -LiteralPath $expected).Length -eq (Get-Item -LiteralPath $actual).Length }
            else { (Get-FileHash -LiteralPath $expected).Hash -eq (Get-FileHash -LiteralPath $actual).Hash }
    if (-not $same) { $problems.Add("${Name}: differs from the folder after the import") }
}
function Test-DocumentHeader([string[]] $Lines) {
    # ThisWorkbook and sheet modules export as classes with both of these set.
    ($Lines -contains 'Attribute VB_PredeclaredId = True') -and ($Lines -contains 'Attribute VB_Exposed = True')
}

# --- Checks before anything is written (exit 2) ---
$source = Resolve-Path -LiteralPath $Workbook -ErrorAction SilentlyContinue
$dir = Resolve-Path -LiteralPath $VbaDir -ErrorAction SilentlyContinue
$output = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputWorkbook)
$inputErrors = [Collections.Generic.List[string]]::new()
if (-not $source) { $inputErrors.Add("workbook not found: $Workbook") }
if (-not $dir) { $inputErrors.Add("folder not found: $VbaDir") }
if (Test-Path -LiteralPath $output) { $inputErrors.Add("output already exists: $OutputWorkbook") }
if ($source -and [IO.Path]::GetExtension($source) -notin '.xlsm', '.xlsb', '.xls') {
    $inputErrors.Add("$Workbook cannot keep macros: use a .xlsm, .xlsb or .xls workbook")
}
if ($source -and [IO.Path]::GetExtension($output) -ne [IO.Path]::GetExtension($source)) {
    $inputErrors.Add("the copy must keep the extension of $Workbook")
}
if (-not (Test-Path -LiteralPath (Split-Path -Parent $output) -PathType Container)) {
    $inputErrors.Add("folder of the output not found: $OutputWorkbook")
}
if ($dir) {
    $files = @(Get-ChildItem -LiteralPath $dir -File | Where-Object Extension -in '.bas', '.cls', '.frm' | Sort-Object Name)
    if ($files.Count -eq 0) { $inputErrors.Add("no .bas, .cls or .frm files in $VbaDir") }
    foreach ($form in $files | Where-Object Extension -eq '.frm') {
        $frx = [IO.Path]::ChangeExtension($form.FullName, '.frx')
        if (-not (Test-Path -LiteralPath $frx)) { $inputErrors.Add("$($form.Name): $(Split-Path -Leaf $frx) is missing next to it") }
    }
    $referencesFile = Join-Path $dir 'vba-references.json'
    if (-not (Test-Path -LiteralPath $referencesFile)) { $inputErrors.Add('vba-references.json is missing: export again') }
    else {
        try { $wantedReferences = @(Get-Content -LiteralPath $referencesFile -Raw | ConvertFrom-Json) }
        catch { $inputErrors.Add("vba-references.json is not valid JSON: $($_.Exception.Message)") }
    }
}
if ($inputErrors.Count -gt 0) {
    Write-Output "ERROR: nothing imported"; $inputErrors | ForEach-Object { Write-Output "  - $_" }; exit 2
}

# --- Import into the copy ---
Copy-Item -LiteralPath $source -Destination $output
$excel = $excelProcess = $null
$checkDir = "$output.check-" + [guid]::NewGuid().ToString('N')
$problems = [Collections.Generic.List[string]]::new()
$notes = [Collections.Generic.List[string]]::new()
$checked = [Collections.Generic.List[string]]::new()   # files imported, to compare after re-export
$exitCode = 0
try {
    $excel = New-Object -ComObject Excel.Application
    $excelPid = [uint32] 0
    [void] [Win32Tools.Native]::GetWindowThreadProcessId([IntPtr] $excel.Hwnd, [ref] $excelPid)
    $excelProcess = Get-Process -Id $excelPid
    $excel.DisplayAlerts = $false
    $excel.AutomationSecurity = 3   # msoAutomationSecurityForceDisable: no macro runs
    $book = $excel.Workbooks.Open($output, 0)
    try { $project = $book.VBProject; [void] $project.VBComponents.Count }
    catch { $exitCode = 3; throw 'Cannot read the VBA project: "Trust access to the VBA project object model" is off.' }

    # References first, so the imported code compiles against them.
    foreach ($ref in $wantedReferences) {
        $present = $false
        foreach ($have in $project.References) {
            if (($ref.guid -and $have.GUID -eq $ref.guid) -or ($ref.fullPath -and (Get-ReferencePath $have) -eq $ref.fullPath)) { $present = $true }
        }
        if ($present) { continue }
        try {
            if ($ref.guid) { [void] $project.References.AddFromGuid($ref.guid, $ref.major, $ref.minor) }
            else { [void] $project.References.AddFromFile($ref.fullPath) }   # a reference to another workbook
            $notes.Add("reference added: $($ref.name) $($ref.major).$($ref.minor)")
        }
        catch { $problems.Add("reference $($ref.name) $($ref.major).$($ref.minor) not added: $($_.Exception.Message)") }
    }
    foreach ($ref in $project.References) {
        if ($ref.IsBroken) { $problems.Add("reference broken (MISSING): $($ref.Name) $(Get-ReferencePath $ref)") }
    }

    foreach ($file in $files) {
        $lines = [IO.File]::ReadAllLines($file.FullName, $ansi)
        $name = Get-ComponentName $lines
        if ($name -ne $file.BaseName) { $problems.Add("$($file.Name): its Attribute VB_Name is '$name', not '$($file.BaseName)'"); continue }
        $existing = $null
        foreach ($component in $project.VBComponents) { if ($component.Name -eq $name) { $existing = $component } }
        if ($existing -and $existing.Type -eq 100) {
            # ThisWorkbook or a sheet: Import would add a class named ThisWorkbook1 instead.
            $code = $existing.CodeModule
            if ($code.CountOfLines -gt 0) { $code.DeleteLines(1, $code.CountOfLines) }
            $body = Get-CodeBody $lines
            if ($body) { $code.AddFromString($body) }
            $checked.Add($file.Name)
            continue
        }
        if ($file.Extension -eq '.cls' -and (Test-DocumentHeader $lines)) {
            $problems.Add("$($file.Name): code of a sheet or ThisWorkbook, but this workbook has no module named $name"); continue
        }
        # Same name: Import would add Orders as Orders1 (and fail for a form). Remove it first.
        if ($existing) { $project.VBComponents.Remove($existing) }
        $imported = $project.VBComponents.Import($file.FullName)
        if ($imported.Name -ne $name) { $problems.Add("$($file.Name): imported as $($imported.Name), not $name"); continue }
        if ($file.Extension -eq '.frm') { Remove-AddedBlankLines $imported.CodeModule (Get-CodeBody $lines) }
        $checked.Add($file.Name)
    }

    # Check: export the copy again and compare with the folder it came from. Code files must be
    # identical; a .frx only the same size (Excel writes the export time and other bytes into it).
    New-Item -ItemType Directory -Path $checkDir | Out-Null
    foreach ($component in $project.VBComponents) {
        $fileName = $component.Name + $extensions[[int] $component.Type]
        if ($fileName -in $checked) { $component.Export((Join-Path $checkDir $fileName)); continue }
        $hasCode = $component.Type -ne 100 -or $component.CodeModule.CountOfLines -gt 0
        if ($hasCode -and $fileName -notin $files.Name) { $notes.Add("kept, not in the folder: $fileName") }
    }
    foreach ($name in $checked) {
        Compare-Export $name
        if ($name -like '*.frm') { Compare-Export ([IO.Path]::ChangeExtension($name, '.frx')) -SizeOnly }
    }
    if ($problems.Count -eq 0) { $book.Save() }
    $book.Close($false)
}
catch {
    if ($exitCode -eq 0) { $exitCode = 1 }
    $problems.Add($_.Exception.Message)
}
finally {
    # Close only this Excel: release COM objects, Quit, wait, and end this process if it hangs.
    $book = $project = $code = $existing = $imported = $component = $ref = $have = $null
    $Error.Clear()   # error records can hold COM objects too
    [GC]::Collect(); [GC]::WaitForPendingFinalizers(); [GC]::Collect(); [GC]::WaitForPendingFinalizers()
    if ($excel) {
        try { $excel.Quit() } catch { Write-Warning "Quit: $($_.Exception.Message)" }
        [void] [Runtime.InteropServices.Marshal]::FinalReleaseComObject($excel)
    }
    if ($excelProcess) {
        $deadline = [DateTime]::UtcNow.AddSeconds(10)
        while (-not $excelProcess.HasExited -and [DateTime]::UtcNow -lt $deadline) { Start-Sleep -Milliseconds 250 }
        if (-not $excelProcess.HasExited) {
            Write-Warning "Excel $($excelProcess.Id) still running 10 s after Quit(); ending it."
            try { $excelProcess.Kill(); [void] $excelProcess.WaitForExit(5000) } catch { }   # it may exit on its own meanwhile
        }
    }
    Remove-Item -LiteralPath $checkDir -Recurse -Force -ErrorAction SilentlyContinue
}

$notes | ForEach-Object { Write-Output "  $_" }
if ($problems.Count -gt 0) {
    Remove-Item -LiteralPath $output -Force -ErrorAction SilentlyContinue   # never leave a half-imported copy
    $kept = if (Test-Path -LiteralPath $output) { 'could NOT be deleted: do not use it' } else { 'not kept' }
    Write-Output "FAIL: $($problems.Count) problem(s); $OutputWorkbook $kept"
    $problems | ForEach-Object { Write-Output "  - $_" }
    if ($exitCode -eq 0) { $exitCode = 1 }
    exit $exitCode
}
Write-Output "OK: $($files.Count) file(s) imported into $OutputWorkbook; the re-export matches the folder"
exit 0

What each part answers, from the tests above:

Results

The target, target.xlsm, has an older Orders module, a module of its own (Helpers) and no Scripting Runtime reference. Output of the third run; the first two printed the same lines, apart from the process ids and which runs needed the fallback (see Limits):

PS> ./import-vba.ps1 -Workbook target.xlsm -VbaDir vba -OutputWorkbook target-v2.xlsm
WARNING: Excel 40752 still running 10 s after Quit(); ending it.
  reference added: Scripting 1.0
  reference added: MSForms 2.0
  kept, not in the folder: Helpers.bas
OK: 5 file(s) imported into target-v2.xlsm; the re-export matches the folder
(exit code 0)

PS> ./import-vba.ps1 -Workbook target-v2.xlsm -VbaDir vba -OutputWorkbook target-v2b.xlsm
  kept, not in the folder: Helpers.bas
OK: 5 file(s) imported into target-v2b.xlsm; the re-export matches the folder
(exit code 0)

The second import, into a copy of the first copy, is the round trip that used to add blank lines to the form; it passes the same check. Opened with macros enabled, the second copy runs the new code, keeps its own module, and has its form and references back:

Orders.CountCustomers = 2
Helpers.Twice(21) = 42
ThisWorkbook code: Option Explicit |  | Public Function BuiltFor() As String |     BuiltFor = "orders demo" | End Function
frmOrder controls: lblSku, txtSku, cmdOK, imgLogo; imgLogo.Picture.Width (from VBA): 847
frmOrder first code line: [Option Explicit]
references: VBA, Excel, stdole, Office, Scripting, MSForms

And the failures, each ending without a copy (the original target.xlsm had the same SHA256 after all runs):

# the .frx deleted from the folder
ERROR: nothing imported
  - frmOrder.frm: frmOrder.frx is missing next to it
(exit code 2)

# a reference added to vba-references.json with a GUID that is not on this PC
WARNING: Excel 46756 still running 10 s after Quit(); ending it.
  reference added: Scripting 1.0
  reference added: MSForms 2.0
  kept, not in the folder: Helpers.bas
FAIL: 1 problem(s); target-v4.xlsm not kept
  - reference LabelPrinter 1.0 not added: Object library not registered
(exit code 1)

# a macro-free copy of the target
ERROR: nothing imported
  - target-nomacros.xlsx cannot keep macros: use a .xlsm, .xlsb or .xls workbook
(exit code 2)

# a target whose first sheet has the code name Data instead of Sheet1
WARNING: Excel 32744 still running 10 s after Quit(); ending it.
  reference added: Scripting 1.0
  reference added: MSForms 2.0
  kept, not in the folder: Helpers.bas
FAIL: 1 problem(s); target-v5.xlsm not kept
  - Sheet1.cls: code of a sheet or ThisWorkbook, but this workbook has no module named Sheet1
(exit code 1)

Limits

Just want the scripts? The Scripts Pack · €12 has the code of this guide and the other guides ready to run, with examples and tests. The code on this page stays free.

The code on this page was written for this guide and tested on the versions above. The workbooks and their data are invented.