Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

BuildIncrement2012 Add-in for C++ Projects

0.00/5 (No votes)
16 Jul 2013 1  
BuildIncrement2012 add-in for C++ projects.

 

Introduction

The true and ultimate purpose of this article is to get feedback on this Add-in so that I can fix whatever errors it might contain. You see, I have never done C# development. In fact, I have never worked with VS2012. To this day, I still work with VC6 due to employment requirements. But I've just gotten my hands on VS2012 and this Add-in will have to do as my "hello world" project... err, solution(?).

I am writing the article as I write the demo project. This is my attempt to provide the painfully detailed tutorial I wish I had found ... with one significant caveat: I can explain nothing. My understanding of Automation/Extensibility and C# is exactly nil.

Disclaimers and Limitations - Read This!

All horrors apply including, by not limited to, sure-as-shooting corruption of any projects you use this Add-in on.

The add-in has only been tested on C++ solutions that contain a single project. It tries to implement How to increment version information after each build in Visual C++ and does so with clumsy yet tenacious incompetence. For instance, the Add-in will increment before each build, rather than after, because I dare to dream.

You've been warned, time and again. I can't be sure it will even execute once before your computer reaches for a razor and does itself in as a form of preemptive surrender.

Please also note that, in order to function properly, the Add-in requires the delicate experimental conditions of my own computer (not available to the public at large). Once in the wild, the Add-in will wreck everything in its path while popping up tens of confrontational error messages. Misdirection is key here. It will confuse, it will destroy.

Background

Geez. I dearly hope you know more than me. So, yeah, a background in Automation/Extensibility and C# would be useful.

Step-by-Step Tutorial

Act I. Project Kick-off

Fire up VS2012 and click on New Project -> Templates -> Other Project Types -> Extensibility -> Visual Studio Add-in. Enter a Name, choose a Location, and click on OK.

The Add-in Wizard should pop up (knock on wood). Choose Create and Add-in using Visual C#. Click on Next twice. Fill in a What is the name ... and a What is the description ... and click on Next.

Check Yes, create a 'Tools' menu ... and I would like my ... Click on Next twice and then on Finish.

The Connect.cs file opens automatically and you feel your very soul drop to the floor. Code (allegedly) fills the screen but it's a completely undecipherable, utterly incomprehensible mess. Scroll down to the OnConnection method (is that what they are called in C#? Methods?) and put a breakpoint on the first line. This is the sanity check. Click on Start Debugging.

If execution does not stop at the breakpoint, you are on your own.

If it does, we are in business. Stop debugging now. You can let go.

It's time to design the GUI. It took me a while (feel no shame yourself) to figure out how to add a Windows Form. I will not describe the hours spent crafting the GUI in the screenshot above nor naming controls with tortuous labels. Have your own fun.

Add using System.Windows.Forms to the top of Connect.cs and modify the Exec method as follows. Compile and debug. I cannot find another way to test the GUI and, anyway, it is good practice to run the project often.

...
using System.Windows.Forms;
...
public void Exec( string commandName, vsCommandExecOption executeOption, 
       ref object varIn, ref object varOut, ref bool handled )
{
    ...
    handled = true;

    BIForm biForm = new BIForm();

    biForm.ShowDialog();
    ...
}
...

Once the second instance of VS starts, you will find that the first entry under the Tools menu is a smiley. No kidding. This is the default icon for Add-ins. Click on it. You should see our dialog pop up. Take a moment to verify that pressing TAB moves from control to control in the order expected (go to View -> Tab Order to modify it).

If you want to change the default icon (I so do), see How to: Change the Default Icon for an Add-in (insane stuff). I chose an icon from the Farm-Fresh collection.

The screenshot speaks for itself. It will display the project's name (why not), and three version numbers that the user can modify at will. The fourth is the build number that will be increased automatically before every build. Below the version textboxes is the 1st defines' window and is proof of how programmer-lazy I am. It will make it easy to insert the header (_version.h) that contains the version defines (BI2012_V0, BI2012_V1, BI2012_V2, BI2012_BUILD,...) in our code later on.

Last, and below the 1st defines' window, is the 2nd defines' window. This one is for configuration user defines. What's that? Well, I often have defines that switch configuration settings and that I need to comment in/out before building a project. As I said, I'm lazy. I'm putting them here so that I don't forget them or have to search for them every time.

I'm now going to fix my wife's bike and take a shower. Feel free to meander.

Act II. We lose our way

Replacing handlebar grips is trickier than it seems. I broke a fingernail. So please keep in mind that, while I proceed heroically, I proceed wounded.

OK. Let's do some C# coding ... what?! No header files? And how does one go about organizing a C# project? What goes where?

I am improvising here and hoping for the best... (twenty minutes later) ... OK, somewhere, in deep cover, is something called the Solution Explorer. Right-click on the project, select Add -> New Item, and then select Class. Again in the Solution Explorer, right-click on the class created (e.g. Class1.cs) and rename it to, say, BIManager.cs for personal comfort purposes.

...
namespace BuildIncrement2012
{
    public class BIManager
    {
    }
}
...

Let us hook this up with the Connect class.

Oops. As soon as I take a closer look at Connect.cs, things go horribly wrong. From what I have unearthed on the subject, I am supposed to add an event handler so that I am notified when the solution is opened. This makes sense to me. It turns out, however, that this will be pointless because the Exec method seems to always be called after a fresh instatiation of the class Connect.

What this means is that I cannot count on keeping information in an object of Connect because the lifepan of these objects is mysterious to me. I'm sure I have to be wrong about this because it is a loony, bunglesome way to do things.

However, wrong or not, this is what I have. Consequently, I am only implementing OnBuildBeginEventHandler and creating a new instance of the Add-in logic every single time a compile is made... as well as every single time the user invokes the Add-in through the Tools menu. I don't like it but, at this point, it is either this or nothing.

But there is more. Event handlers are said to disappear down the garbage-collection chute. Great. Let's add a member to the class to record the BuildEvents. The monstrosity looks like this:

...
public class Connect : IDTExtensibility2, IDTCommandTarget
{
    ...
    public void OnConnection( object application, ext_ConnectMode connectMode, 
                              object addInInst, ref Array custom )
    {
    ...
        if( connectMode == ext_ConnectMode.ext_cm_Startup )
        {
            m_BuildEvents = _applicationObject.Events.BuildEvents;

            m_BuildEvents.OnBuildBegin += 
              new _dispBuildEvents_OnBuildBeginEventHandler( BuildEvents_OnBuildBegin );
        }
    }
    
    public void OnDisconnection( ext_DisconnectMode disconnectMode, ref Array custom )
    {
        m_BuildEvents.OnBuildBegin -= 
          new _dispBuildEvents_OnBuildBeginEventHandler( BuildEvents_OnBuildBegin );
    }
    ...
    BuildEvents m_BuildEvents;
}
...

I don't know what the naming conventions are in C# so I'll prefix class members with m_ as I was raised to do in polite company.

Note that we inspect connectMode before assigning the event handler. This is because OnConnection might be called several times. Without this check, we could have duplicate event handlers and unexpected behaviour.

The implementation of BuildEvents_OnBuildBegin is as follows:

...
private void BuildEvents_OnBuildBegin( vsBuildScope Scope, vsBuildAction Action )
{
    System.Array arrProjects = ( System.Array )_applicationObject.ActiveSolutionProjects;
    EnvDTE.Project pjProject = ( EnvDTE.Project )arrProjects.GetValue( 0 );
    
    // Check for the GUID string of C++ projects
    if( pjProject.Kind == "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}" )
    {
        BIManager biManager = new BIManager( pjProject );
    
        biManager.IncrementBuildNumber();
    }
}
...

Let's flip to BIManager.cs to set up the class with a few members to hold the Add-in information for the active project.

The constructor will be the place to figure out the project's name and paths. The method IncrementBuildNumber is still non-functional and is only partially implemented so that the Add-in builds cleanly (and, consequently, it is possible to confirm the behavior of events when debugging).

class BIManager
{
    Project m_Project;
    String m_strFnRC;
    String m_strFnRC2;
    String m_strFnVersion;
    bool m_bBIEnabled;
    int m_iV0;
    int m_iV1;
    int m_iV2;
    int m_iBUILD;
    
    public BIManager( Project pjProject )
    {
        m_Project = pjProject;
        
        String strProjectPath = m_Project.FullName;
        
        strProjectPath = strProjectPath.Remove(1 + strProjectPath.LastIndexOf("\\"));
        
        m_strFnRC = strProjectPath + m_Project.Name + ".rc";
        m_strFnRC2 = strProjectPath + "res\\" + m_Project.Name + ".rc2";
        m_strFnVersion = strProjectPath + "res\\_version.h";
        m_bBIEnabled = false;
    }
    
    public int IncrementBuildNumber()
    {
        m_iBUILD++;
        
        return m_iBUILD;
    }
}

I want to be done with the Connect.cs file. It gives me a headache just to look at it.

The Exec method is called when the user brings up the Add-in through the Tools menu (or a toolbar if an icon has been manually added). As I mentioned earlier, Exec behaves unlike other events. I can't say why and will appreciate a head's up. Anyhow. What this means is that we have to implement a fresh instance of the class BIManager for the form.

...
handled = true;

if( _applicationObject.ActiveDocument != null )
{
    System.Array arrProjects = ( System.Array )_applicationObject.ActiveSolutionProjects;
    EnvDTE.Project pjProject = ( EnvDTE.Project )arrProjects.GetValue( 0 );

    // Check for the GUID string of C++ projects
    if( pjProject.Kind != "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}" )
    {
        MessageBox.Show( "The BuildIncrement2012 Add-in only works with C++ projects" );
    }
    else
    {
        BIForm biForm = new BIForm( pjProject );

        biForm.ShowDialog();
    }
}

return;
...

Flip to BIForm.cs to address these changes. The code looks like this:

...
using EnvDTE;
...
BIManager m_biManager;
bool m_bBlockEvents;
...
public BIForm( Project pjProject )
{
    InitializeComponent();
    
    m_biManager = new BIManager( pjProject );
    
    PopulateControls();    
}

private void PopulateControls()
{
    m_bBlockEvents = true;
    
    cbEnableBI.Checked = m_biManager.IsBIEnabled();
    
    nudV0.Value = m_biManager.GetV0();
    nudV1.Value = m_biManager.GetV1();
    nudV2.Value = m_biManager.GetV2();
    tbBuild.Text = m_biManager.GetBUILD().ToString();
    
    lbBIDefines.Items.Clear();
    lbBIDefines.Items.Add( "#include \"_version.h\"" );
    lbBIDefines.Items.Add( "BI2012_V0" );
    lbBIDefines.Items.Add( "BI2012_V1" );
    lbBIDefines.Items.Add( "BI2012_V2" );
    lbBIDefines.Items.Add( "BI2012_BUILD" );
    lbBIDefines.Items.Add( "BI2012_BUILD_DATE" );
    lbBIDefines.Items.Add( "BI2012_BUILD_TIME" );
    lbBIDefines.Items.Add( "BI2012_STR_V0" );
    lbBIDefines.Items.Add( "BI2012_STR_V1" );
    lbBIDefines.Items.Add( "BI2012_STR_V2" );
    lbBIDefines.Items.Add( "BI2012_STR_BUILD" );
    lbBIDefines.Items.Add( "BI2012_STR_VERSION" );
    
    m_bBlockEvents = false;
}
...

You should also be able to tell from the code what I named each control. Welcome to my world.

The m_bBlockEvents is a hack needed to prevent GUI events from firing and it will be necessary later. If someone knows a more elegant workaround, please let me know.

The rest of the code is about gathering information from the BIManager class and displaying it. The Add-in defines can be added now because they do not change. The code to fill the user defines' checkedlistbox will be added later.

Now we have a few getter(?) methods in the BIManager class that need to be implemented. Simple stuff.

...
public bool IsBIEnabled()
{
    return m_bBIEnabled;
}

public int GetV0()
{
    return m_iV0;
}

public int GetV1()
{
    return m_iV1;
}

public int GetV2()
{
    return m_iV2;
}

public int GetBUILD()
{
    return m_iBUILD;
}
...

Build and debug. If you have set breakpoints munificently, you will be rewarded with a better understanding of how everything fits together. It is also a good time to create a test C++ project to mess with. When a project is loaded, the Add-in dialog can be accessed through the Tools menu. It does nothing but it's there.

It's there.

Act III. My London London Bridge want to go down like

What will happen if I double-click on a control on the form? Let's find out by clicking everywhere. You first.

Below is what I get when I click on the first few controls and how I implement them.

...
private void cbEnableBI_CheckedChanged( object sender, EventArgs e )
{
    if( m_bBlockEvents )
    {
        return;
    }
    
    if( cbEnableBI.Checked && m_biManager.LoadVersionInfo() == false )
    {
        m_biManager.ModifyResourceFiles();
        
        m_biManager.CreateVersionFile();
        
        m_biManager.ModifyRC2();
    }

    m_biManager.EnableBI( cbEnableBI.Checked );
    
    PopulateControls();
}

private void nudV0_ValueChanged( object sender, EventArgs e )
{
    if( m_bBlockEvents )
    {
    return;
    }
    
    m_biManager.VersionNumberChange( (int)nudV0.Value );
}

private void nudV1_ValueChanged( object sender, EventArgs e )
{
    if( m_bBlockEvents )
    {
    return;
    }
    
    m_biManager.VersionNumberChange( -1, (int)nudV1.Value );
}

private void nudV2_ValueChanged( object sender, EventArgs e )
{
    if( m_bBlockEvents )
    {
    return;
    }
    
    m_biManager.VersionNumberChange( -1, -1, (int)nudV2.Value );
}
...

The code above hooks up the GUI with the BIManager. The only point of interest is in cbEnableBI_CheckedChanged where, if BuildIncrement2012 is enabled, we modify the project's resource files and create the project's version file. Of course, if these modifications were already done, we simply load the information and move on.

OK. We now have to implement the logic outlined in these methods. Flip to BIManager.cs.

We start with LoadVersionInfo. First, we call HasTheProjectBeenSetup to check if the project has previously been setup by the Add-in. If not, quit. If so, read the version file. The code will look awkward to anyone that knows C#. I, however, am in love.

After looking around for a Map class, I came across Dictionary. Who knew? It will hold the user defines and whether (or not) they are commented out. We will modify PopulateControls later so that these defines are displayed along with everything else.

...
using System.IO;
...
Dictionary<String, bool> m_dUserDefines = new Dictionary<String, bool>();
...
public BIManager( Project pjProject )
{
    m_Project = pjProject;
    
    String strProjectPath = m_Project.FullName;
    
    strProjectPath = strProjectPath.Remove( 1 + strProjectPath.LastIndexOf( "\\" ) );
    
    m_strFnRC = strProjectPath + m_Project.Name + ".rc";
    m_strFnRC2 = strProjectPath + "res\\" + m_Project.Name + ".rc2";
    m_strFnVersion = strProjectPath + "res\\_version.h";
    m_bBIEnabled = false;
    
    LoadVersionInfo();
}
...
private bool HasTheProjectBeenSetup()
{
    bool bIsRC2_Modified = false;
    StreamReader sr = new StreamReader( m_strFnRC2, true );
    String strLine = sr.ReadLine();
    
    while( strLine != null && !bIsRC2_Modified )
    {
        bIsRC2_Modified = ( strLine.IndexOf( "VS_VERSION_INFO" ) != -1 );
        
        strLine = sr.ReadLine();
    }
    
    sr.Close();
    
    return File.Exists( m_strFnVersion ) && bIsRC2_Modified;
}

public bool LoadVersionInfo()
{
    bool bSuccess = HasTheProjectBeenSetup();

    m_dUserDefines.Clear();
    
    if( bSuccess )
    {
        StreamReader sr = new StreamReader( m_strFnVersion, true );
        String strLine = sr.ReadLine();
        bool bUserDefines = false;
        
        while( strLine != null )
        {
            if( bUserDefines == false && 
                strLine.IndexOf( "// User defines from here on" ) == 0 )
            {
                bUserDefines = true;
            }
            else if( bUserDefines )
            {
                bool bEnabled = ( strLine.IndexOf( "//" ) != 0 );

                m_dUserDefines.Add( bEnabled ? 
                  strLine.Substring( 8 ) : strLine.Substring( 10 ), bEnabled );
            }
            else if( strLine.IndexOf( "#define BI2012_ENABLED" ) == 0 )
            {
                m_bBIEnabled = Convert.ToBoolean( strLine.Substring( 40 ) );
            }
            else if( strLine.IndexOf( "#define BI2012_V0" ) == 0 )
            {
                m_iV0 = Convert.ToInt32( strLine.Substring( 40 ) );
            }
            else if( strLine.IndexOf( "#define BI2012_V1" ) == 0 )
            {
                m_iV1 = Convert.ToInt32( strLine.Substring( 40 ) );
            }
            else if( strLine.IndexOf( "#define BI2012_V2" ) == 0 )
            {
                m_iV2 = Convert.ToInt32( strLine.Substring( 40 ) );
            }
            else if( strLine.IndexOf( "#define BI2012_BUILD " ) == 0 )
            {
                m_iBUILD = Convert.ToInt32( strLine.Substring( 40 ) );
            }
            
            strLine = sr.ReadLine();
        }
        
        sr.Close();
    }
    
    return bSuccess;
}
...

Note the modification to the constructor to include a call to LoadVersionInfo. Also, and as you have seen, there is no exception handling or error checking of any kind. This is not by accident. It is a pattern that will confirm, in case you needed any more evidence, that I have no idea what I am doing.

It's time to implement the methods that modify the resource files of the project so that the Add-in can do its magic. You will be thankful for a test project right now. The code looks as follows.

...
public void ModifyResourceFiles()
{
    String strFnTemp = m_strFnRC + ".tmp";
    StreamReader srRC = new StreamReader(m_strFnRC, true);
    TextWriter twTemp = new StreamWriter(strFnTemp, false, Encoding.Unicode);
    TextWriter twRC2 = new StreamWriter(m_strFnRC2, true, Encoding.Unicode);
    String strLine = srRC.ReadLine();
    bool bFound = false;
    
    twRC2.WriteLine();
    
    while (strLine != null)
    {
        bFound |= (strLine.IndexOf("VS_VERSION_INFO") == 0);
        
        if(bFound)
        {
            twRC2.WriteLine(strLine);
            
            if (strLine.IndexOf("END") == 0)
            {
                twRC2.WriteLine();
                
                bFound = false;
            }
        }
        else
        {
            twTemp.WriteLine(strLine);
        }
        
        strLine = srRC.ReadLine();
    }
    
    srRC.Close();
    twTemp.Close();
    twRC2.Close();
    
    File.Delete(m_strFnRC);
    File.Move(strFnTemp, m_strFnRC);
}

public void CreateVersionFile()
{
    m_bBIEnabled = true;
    m_iV0 = 1;
    m_iV1 = 0;
    m_iV2 = 0;
    m_iBUILD = 0;

    TextWriter tw = new StreamWriter( m_strFnVersion, false, Encoding.Unicode );

    tw.WriteLine();
    tw.WriteLine( "#define BI2012_ENABLED                  True" );
    tw.WriteLine( "#define BI2012_V0                       1" );
    tw.WriteLine( "#define BI2012_V1                       0" );
    tw.WriteLine( "#define BI2012_V2                       0" );
    tw.WriteLine( "#define BI2012_BUILD                    0" );
    tw.WriteLine( "#define BI2012_BUILD_DATE               L\"00/00/00\"" );
    tw.WriteLine( "#define BI2012_BUILD_TIME               L\"00:00:00\"" );
    tw.WriteLine();
    tw.WriteLine( "#define BI2012_STR_EXPAND(tok) #tok" );
    tw.WriteLine( "#define BI2012_STR(tok) BI2012_STR_EXPAND(tok)" );
    tw.WriteLine();
    tw.WriteLine( "#define BI2012_STR_V0 _T(BI2012_STR(BI2012_V0))" );
    tw.WriteLine( "#define BI2012_STR_V1 _T(BI2012_STR(BI2012_V1))" );
    tw.WriteLine( "#define BI2012_STR_V2 _T(BI2012_STR(BI2012_V2))" );
    tw.WriteLine( "#define BI2012_STR_BUILD _T(BI2012_STR(BI2012_BUILD))" );
    tw.WriteLine( "#define BI2012_STR_VERSION BI2012_STR" 
      "(BI2012_V0) \".\" BI2012_STR(BI2012_V1) \".\" BI2012_STR(" 
      "BI2012_V2) \".\" BI2012_STR(BI2012_BUILD)" );
    tw.WriteLine();
    tw.WriteLine( "// User defines from here on" );

    tw.Close();

    m_Project.ProjectItems.AddFromFile( m_strFnVersion );
}

public void ModifyRC2()
{
    String strFnTemp = m_strFnRC2 + ".tmp";
    TextWriter tw = new StreamWriter( strFnTemp, false, Encoding.Unicode );
    StreamReader sr = new StreamReader( m_strFnRC2, true );
    String strLine = sr.ReadLine();

    while( strLine != null )
    {
        if( strLine.IndexOf( "VS_VERSION_INFO" ) != -1 )
        {
            tw.WriteLine( "#include \"_version.h\"" );
            tw.WriteLine();
            tw.WriteLine( strLine );
        }
        else if( strLine.IndexOf( "FILEVERSION" ) != -1 )
        {
            tw.WriteLine( "FILEVERSION BI2012_V0,BI2012_V1,BI2012_V2,BI2012_BUILD" );
        }
        else if( strLine.IndexOf( "PRODUCTVERSION" ) != -1 )
        {
            tw.WriteLine( "PRODUCTVERSION BI2012_V0,BI2012_V1,BI2012_V2,BI2012_BUILD" );
        }
        else if( strLine.IndexOf( "VALUE \"FileVersion\"" ) != -1 )
        {
            tw.WriteLine( "VALUE \"FileVersion\", BI2012_STR_VERSION" );
        }
        else if( strLine.IndexOf( "VALUE \"ProductVersion\"" ) != -1 )
        {
            tw.WriteLine( "VALUE \"ProductVersion\", BI2012_STR_VERSION" );
        }
        else
        {
            tw.WriteLine( strLine );
        }

        strLine = sr.ReadLine();
    }

    sr.Close();
    tw.Close();

    File.Delete( m_strFnRC2 );
    File.Move( strFnTemp, m_strFnRC2 );
}
...

Writing this code was like composing alexandrine poetry using Ancient Egyptian hieroglyphs. However, the code is simple from an algorithmic perspective. Construct temp files for the modified contents of rc and rc2 and then replace the originals. Creating the _version.h file is completely straightforward.

There is an issue, however, that I want to bring attention to in case someone knows the answer (Google was not my friend). I added m_Project.ProjectItems.AddFromFile( m_strFnVersion ) because I want the file _version.h to be included automatically in the project. As the code stands, the file is included by default in the Header files node/filter/folder. Does anyone know how to include it in another folder, say the Resource files node/filter/folder?

I am commenting out the methods that still need to be implemented so that I can build and step through the code in the debugger. Let us verify that _version.h is created and rc/rc2 are modified correctly for the test project ...

Yep. It all works for me. If it does for you too, we are in business.

Act IV. Drunk with power, Napoleon beelines for Paris

It is now time to go for broke. If we can update _version.h appropriately, we are done. The simplest way to do this is by implementing the method EnableBI in BIManager. In turn, this requires implementing UpdateVersionFile, since we want _version.h to record whether or not the Add-in is enabled. The code looks as follows:

...
public void EnableBI( bool bEnableBI )
{
    m_bBIEnabled = bEnableBI;

    UpdateVersionFile( false );
}

public void UpdateVersionFile( bool bBuild )
{
    String strBuildDate = "L\"" + 
      DateTime.Now.ToString( "MM/dd/yy" ) + "\"";
    String strBuildTime = "L\"" + 
      DateTime.Now.ToString( "hh:mm:ss" ) + "\"";

    if( bBuild == false )
    {
        // Fetch old values before overwriting the file
        StreamReader sr = new StreamReader( m_strFnVersion, true );
        String strLine = sr.ReadLine();

        while( strLine != null )
        {
            if( strLine.IndexOf( "#define BI2012_BUILD_DATE" ) == 0 )
            {
                strBuildDate = strLine.Substring( 40 );
                strLine = sr.ReadLine();
                strBuildTime = strLine.Substring( 40 );
                break;
            }

            strLine = sr.ReadLine();
        }

        sr.Close();
    }

    TextWriter tw = new StreamWriter( m_strFnVersion, false, Encoding.Unicode );

    tw.WriteLine();
    tw.WriteLine( "#define BI2012_ENABLED                  " + m_bBIEnabled.ToString() );
    tw.WriteLine( "#define BI2012_V0                       " + m_iV0.ToString() );
    tw.WriteLine( "#define BI2012_V1                       " + m_iV1.ToString() );
    tw.WriteLine( "#define BI2012_V2                       " + m_iV2.ToString() );
    tw.WriteLine( "#define BI2012_BUILD                    " + m_iBUILD.ToString() );
    tw.WriteLine( "#define BI2012_BUILD_DATE               " + strBuildDate );
    tw.WriteLine( "#define BI2012_BUILD_TIME               " + strBuildTime );
    tw.WriteLine();
    tw.WriteLine( "#define BI2012_STR_EXPAND(tok) #tok" );
    tw.WriteLine( "#define BI2012_STR(tok) BI2012_STR_EXPAND(tok)" );
    tw.WriteLine();
    tw.WriteLine( "#define BI2012_STR_V0 _T(BI2012_STR(BI2012_V0))" );
    tw.WriteLine( "#define BI2012_STR_V1 _T(BI2012_STR(BI2012_V1))" );
    tw.WriteLine( "#define BI2012_STR_V2 _T(BI2012_STR(BI2012_V2))" );
    tw.WriteLine( "#define BI2012_STR_BUILD _T(BI2012_STR(BI2012_BUILD))" );
    tw.WriteLine( "#define BI2012_STR_VERSION BI2012_STR(" 
      "BI2012_V0) \".\" BI2012_STR(BI2012_V1) \".\" BI2012_STR(" 
      "BI2012_V2) \".\" BI2012_STR(BI2012_BUILD)" );
    tw.WriteLine();
    tw.WriteLine( "// User defines from here on" );

    foreach( var pair in m_dUserDefines )
    {
        tw.WriteLine( ( pair.Value == false ? "//" : "" ) + "#define " + pair.Key );
    }

    tw.Close();
}
...

The EnableBI method is trivial. It records the change of value and then calls UpdateVersionFile.

The UpdateVersionFile method, however, is a visionary among its peers. It looks forward to a time when it will be called upon to record a build date and time. But not yet. At this point, being called from EnableBI, it takes the old date/time values since, after all, we are just recording that the Add-in has been enabled or disabled.

Build and debug. Go mad clicking on cbEnableBI while you have _version.h opened in a text editor smart enough to reload the file on external modification. Don't be shy. Let it rip. You'll see BI2012_ENABLED go from True to False and back again as the world smiles with you.

We are on a roll. Let us now implement VersionNumberChange, the last method we commented out earlier so that we could build and debug this monster.

...
public void VersionNumberChange( int iV0 = -1, int iV1 = -1, int iV2 = -1, int iBuild = -1 )
{
    if( iV0 != -1 )
    {
        m_iV0 = iV0;
    }

    if( iV1 != -1 )
    {
        m_iV1 = iV1;
    }

    if( iV2 != -1 )
    {
        m_iV2 = iV2;
    }

    bool bBuild = ( iBuild != -1 );

    if( bBuild )
    {
        m_iBUILD = iBuild;
    }

    if( iV0 != -1 || iV1 != -1 || iV2 != -1 || bBuild )
    {
        UpdateVersionFile( bBuild );
    }
}
...

There. The method VersionNumberChange is called when the user manually changes the version number in the dialog and, automatically, before each build per the code in Connect.cs.

OK, then. What's left to do? Ah, yes. Populate the user defines' checkedlistbox and write logic for the Add, Delete, and Copy to clipboard buttons.

...
private void PopulateControls()
{
...
    clbUserDefines.Items.Clear();
    Dictionary<String, bool> dUserDefines = 
                 m_biManager.GetUserDefinesDictionary();

    foreach( var pair in dUserDefines )
    {
        clbUserDefines.Items.Add( pair.Key, pair.Value );
    }
...
}

private void btCopytoClipboardBIDefines_Click( object sender, EventArgs e )
{
    String strToClip = "";

    foreach( Object selecteditem in lbBIDefines.SelectedItems )
    {
        String strItem = selecteditem as String;

        strToClip += strItem;

        if( strItem == "#include \"_version.h\"" )
        {
            strToClip += "\n";
        }
        else
        {
            strToClip += ", ";
        }
    }

    if( strToClip != "" )
    {
        char[] charsToTrim = { ' ', ',' };

        Clipboard.SetText( strToClip.TrimEnd( charsToTrim ) );
    }
}

private void btCopyToClipboardUserDefines_Click( object sender, EventArgs e )
{
    Object oSelection = clbUserDefines.SelectedItem;

    if( oSelection != null )
    {
        Clipboard.SetText( oSelection.ToString() );
    }
}

private void btDeleteUserDefine_Click( object sender, EventArgs e )
{
    Object oSelection = clbUserDefines.SelectedItem;

    if( oSelection != null )
    {
        Dictionary<String, bool> dUserDefines = m_biManager.GetUserDefinesDictionary();

        dUserDefines.Remove( oSelection.ToString() );

        clbUserDefines.Items.Remove( oSelection );

        m_biManager.UpdateVersionFile( false );
    }
}

private void btAddUserDefine_Click( object sender, EventArgs e )
{
    AddNewUserDefine();
}

private void tbNewUserDefine_KeyDown( object sender, KeyEventArgs e )
{
if( e.KeyCode == Keys.Enter )
{
    AddNewUserDefine();
}
}

private void AddNewUserDefine()
{
    String strNewUserDefine = tbNewUserDefine.Text;

    if( strNewUserDefine != "" )
    {
        Dictionary<String, bool> dUserDefines = m_biManager.GetUserDefinesDictionary();

        if( dUserDefines.ContainsKey( strNewUserDefine ) == true )
        {
            MessageBox.Show( "This define already exists" );
        }
        else
        {
            m_bBlockEvents = true;
            clbUserDefines.Items.Add( strNewUserDefine, true );
            m_bBlockEvents = false;

            dUserDefines.Add( strNewUserDefine, true );

            m_biManager.UpdateVersionFile( false );
        }
    }
}
...

It seems like a lot of code but it is easy to follow. Let us not forget to add a trivial getter hack in the BIManager class to fetch the Dictionary of user defines.

...
public Dictionary<String, bool> GetUserDefinesDictionary()
{
    return m_dUserDefines;
}
...

Also, what does it mean for the Add-in to be disabled? We could gray out all controls in BIForm but I am simply going to stop the Add-in from updating the build count automatically. All else will remain functional. The method IncrementBuildNumber is to be modified as follows.

...
public int IncrementBuildNumber()
{
    if( m_bBIEnabled )
    {
        m_iBUILD++;

        VersionNumberChange( -1, -1, -1, m_iBUILD );
    }

    return m_iBUILD;
}
...

Done? Nope. We got to make sure _version.h is updated when a user define is (un)checked. No problem. Find ItemCheck under Behavior in Properties -> Events for the user defines' checkedlistbox.

...
private void clbUserDefines_ItemChecked( object sender, ItemCheckEventArgs e )
{
    if( m_bBlockEvents )
    {
        return;
    }

    Dictionary<String, bool> dUserDefines = m_biManager.GetUserDefinesDictionary();
    String strKey = clbUserDefines.Items[ e.Index ].ToString();

    if( dUserDefines.ContainsKey( strKey ) == true )
    {
        dUserDefines[ strKey ] = ( e.NewValue == CheckState.Checked );

        m_biManager.UpdateVersionFile( false );
    }
}
...

I think we have actually finished the entire Add-in. Strangely anticlimactic.

Test it, test it, and then test it again. Remember that this Add-in will be messing with the resource files of your precious projects. Don't let it be said that I did not warn you enough.

Act V. I see dead code

It is your code. The project you loved so much has now been destroyed by this Add-in. You paid no heed to my warnings and now ... well, you have to start all over.

Addendum 

I hope the tutorial is helpful to someone. I can only apologize for my ignorance on the subject. This is the best I could do for you guys over the span of a few hours.

And, please, if you do know more than me (a sure bet), let me know what can be improved on and what is deadly wrong. I'll update the tutorial and give you credit. The comment thread is all yours.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here