Exporting a UserForm from the VBE... and working around API bugs Rubberduck has been using an extension method to export code files, notably for source control purposes.

While testing source control, I ran into a weird bug in the VBIDE API, which caused UserForm modules to perpetually be treated as "modified", because every time we exported them, the VBIDE API would add an empty line at the beginning of the code-behind.

So I had to modify our ExportAsSourceFile extension method to work around that glitch:


Export the form to a temporary file
Read the contents of the file
Locate the last line of the file heading (attributes that are now showing in the VBE)
Determine if we have an extraneous empty line, and remove it
Write the modified content into the intended export folder


Modifying the file directly in the export folder wasn't an option, because then Git would have considered it as modified anyway.

For context/reference, here's what a UserForm code file looks like:

VERSION 5.00
Begin {C62A69F0-16DC-11CE-9E98-00AA00574A4F} UserForm1 
   Caption         =   "UserForm1"
   ClientHeight    =   3165
   ClientLeft      =   45
   ClientTop       =   390
   ClientWidth     =   4710
   OleObjectBlob   =   "UserForm1.frx":0000
   StartUpPosition =   1  'CenterOwner
End
Attribute VB_Name = "UserForm1"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Option Explicit 'first line of actual user code visible in the VBE


Notice the VB_PredeclaredId attribute is always the last one specified: my fix relies on that detail.

Here's the extension method in question:

/// &lt;summary&gt;
/// Exports the component to the directoryPath. The file is name matches the component name and file extension is based on the component's type.
/// &lt;/summary&gt;
/// &lt;param name="component"&gt;The component to be exported to the file system.&lt;/param&gt;
/// &lt;param name="directoryPath"&gt;Destination Path for the resulting source file.&lt;/param&gt;
public static string ExportAsSourceFile(this VBComponent component, string directoryPath)
{
    var path = Path.Combine(directoryPath, component.Name + component.Type.FileExtension());
    if (component.Type == vbext_ComponentType.vbext_ct_Document)
    {
        var lineCount = component.CodeModule.CountOfLines;
        if (lineCount &gt; 0)
        {
            var text = component.CodeModule.get_Lines(1, lineCount);
            File.WriteAllText(path, text);
        }
    }
    else if(component.Type == vbext_ComponentType.vbext_ct_MSForm)
    {
        // VBIDE API *sometimes* inserts an extra newline when exporting a UserForm module.
        // this issue causes forms to always be treated as "modified" in source control, which causes conflicts.
        // we need to remove the extra newline before the file gets written to its output location.
        var tempFile = component.ExportToTempFile();
        var contents = File.ReadAllLines(tempFile);
        var vbExposedAttributeLine = 0;
        for (var i = 0; i &lt; contents.Length; i++)
        {
            if (contents[i].StartsWith("Attribute VB_Exposed = "))
            {
                vbExposedAttributeLine = i;
                break;
            }
        }
        if (contents.Length &gt; vbExposedAttributeLine &amp;&amp; contents[vbExposedAttributeLine + 1].Replace(Environment.NewLine, string.Empty).Trim() == string.Empty)
        {
            // first line in module is empty - assume it wasn't in the VBE
            var edited = contents.Take(vbExposedAttributeLine).Union(contents.Skip(vbExposedAttributeLine + 1)).ToArray();
            File.WriteAllLines(path, edited);
        }
    }
    else
    {
        // other module types can just be exported directly
        component.Export(path);
    }

    return path;
}

public static string ExportToTempFile(this VBComponent component)
{
    var path = Path.Combine(Path.GetTempPath(), component.Name + component.Type.FileExtension());
    component.Export(path);
    return path;
}


Is there a better way to do this? It works.. but is there anything that could go wrong with it?
Well, here's a form saved from Excel 2013.

Notice the VB_Exposed, VB_HelpID and VB_Description attributes. I set the Description and Help attributes using the Object Browser Context Menu ("Properties"), but a user might have manually edited the attributes in a previous export/import, and so they might be in a different order

VERSION 5.00
Begin {C62A69F0-16DC-11CE-9E98-00AA00574A4F} UserForm1 
   Caption         =   "UserForm1"
   ClientHeight    =   3165
   ClientLeft      =   45
   ClientTop       =   390
   ClientWidth     =   4710
   OleObjectBlob   =   "UserForm1.frx":0000
   StartUpPosition =   1  'CenterOwner
End
Attribute VB_Name = "UserForm1"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = False
Attribute VB_HelpID = 55
Attribute VB_Description = "This is a description"
Private Sub UserForm_Click()
  Debug.Print "Test"
End Sub


There is a trailing blank line, but CR MarkDown isn't showing it.

Edit:
With Excel working again, I imported the file, and I do get the extra header line, and if it is exported with a header line, and reimported, there are then 2 header lines, so I'm getting the same behaviour as you.

You won't be able to rely on the position/order of VB_Attributes, so you'll have to parse the file.

You're also assuming a user won't have intentionally placed a blank line at the top of the module. And what happens if the user has more than one blank line at the top of the module?

The safest approach would probably be to count the blank lines at the top of the code module, before the export, and then adjust the number of blank lines found after the last VB_Attribute but before the actual code begins.

That would potentially deal with any fix that might come to VBE.