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

SolutionZipper: VS 2005 Add-in Cleans and Zips a Solution in One Step

0.00/5 (No votes)
3 Aug 2007 6  
A convenient tool for quick solution back-ups.

Introduction

Updated to v1.3 on 31-July-2007: See updated source, notes in article and Revision history for details. There are already a couple of good Visual Studio solution and project zipping tools available:

  • ProjectZip 1.6: Zip up the source code for your latest CodeProject article
  • ZipStudio: A versatile Visual Studio add-in to zip up Visual Studio solutions and projects

The code described in this article provides a simplified version of these add-ins. Specifically, SolutionZipper has a single-click-does-everything interface, which is what automation is all about: saving time. There are no dialogs that present file lists, file names to be chosen or user options to select. SolutionZipper does the following in a single step:

  1. Saves all unsaved project files
  2. Cleans all available solution configurations (Debug, Release, etc.)
  3. Cleans all Deployment Projects
  4. Zips all files in the entire solution:
    • The ZIP file is uniquely named SolutionName_YYYYMMDD-HHMMSS.zip
    • The path for all files are solution directory relative
    • The ZIP file is placed at the same level as the solution directory

When these operations are completed, a confirmation dialog is displayed:

Screenshot - SolutionZipper-confirm.JPG

The ZIP file can be used to share projects and is in the proper form for CodeProject submissions (of course). If you are not using a source code control system, SolutionZipper is useful for quickly backing up a snapshot of a solution at major development milestones.

Add-in implementation

The standard VS Add-in Wizard was used to create the project. See How to: Create an Add-in for details. In order to get a custom icon on the menu item,

Screenshot - SolutionZipper-menu.JPG

follow the instructions in How to: Display a Custom Icon on the Add-in Button carefully. Also note that the icon must be exactly 16x16 pixels to be displayed. ICSharpCode.SharpZipLib.dll is used for the ZIP implementation and can be downloaded here.

Automation

The automation actions are implemented in the IDTExtensibility2.Exec function.

// Updated: v1.3
public void Exec(string commandName, 
    vsCommandExecOption executeOption, 
    ref object varIn, ref object varOut, ref bool handled)
{
    handled = false;
    if(executeOption == 
            vsCommandExecOption.vsCommandExecOptionDoDefault)
    {
        if(commandName == 
            "SolutionZipper.Connect.SolutionZipper")
        {
            const string ADDIN_NAME = "SolutionZipper";
            _Solution sol = _applicationObject.Solution;
            handled = true;
            if (string.IsNullOrEmpty(sol.FullName)) return; 
            // no solution loaded
            string solpath = Path.GetDirectoryName(sol.FullName); 
            // solution path
            // If there are external projects, they will be copied here to
            // be included in the zip file and then deleted.
            string extDir = null;
            // Status bar
            EnvDTE.StatusBar statusbar = _applicationObject.StatusBar;

            // Do a Save All
            //
            statusbar.Text = ADDIN_NAME + ": Save All Files...";
            _applicationObject.ExecuteCommand("File.SaveAll", "");

            // Clean all configurations
            //
            statusbar.Text = ADDIN_NAME + ": Cleaning all configurations...";
            SolutionBuild2 sb = (SolutionBuild2)sol.SolutionBuild;
            // Remember current configuration
            SolutionConfiguration curr_conf = sb.ActiveConfiguration;
            
            foreach (SolutionConfiguration s in sb.SolutionConfigurations)
            {
                s.Activate();
                sb.Clean(true); // Clean the solution.
            }
            // Restore previously active config
            curr_conf.Activate();
 
            // Clean deployment projects.
            //
            statusbar.Text = ADDIN_NAME + ": Handle deployment projects...";
            foreach (Project p in sol.Projects)
            {
                try
                {
                    if (p.UniqueName.IndexOf(".vdproj", 
                        0, StringComparison.CurrentCultureIgnoreCase) != -1)
                    {
                        string vddir = Path.GetDirectoryName(p.FullName);
                        // Remove contents of the Release and 
                        // Debug directories
                        DeleteDirContents(Path.Combine(vddir, "Release"));
                        DeleteDirContents(Path.Combine(vddir, "Debug"));
                    }
                }
                catch (NullReferenceException)
                {
                    // A Web Deployment project will throw this 
                    // exception when UniqueName is accessed.
                    // Name is the only valid property: See if the 
                    // project is in the solution dir.
                    // Delete product if it is.
                    string wdprojfile = 
                        Path.Combine(Path.Combine(solpath, p.Name), 
                        p.Name + ".wdproj");
                    if (File.Exists(wdprojfile))
                    {
                        string wddir = Path.Combine(solpath, p.Name);
                        // Remove contents of the Release and 
                        // Debug directories
                        DeleteDirContents(Path.Combine(wddir, "Release"));
                        DeleteDirContents(Path.Combine(wddir, "Debug"));
                    }
                }
                catch (Exception ex)
                {
                    // Just in case some other type of exception occurs!!
                    MessageBox.Show(string.Format(
                        "An Error has occurred:\n{0}\n\nZip " + 
                        "file not created.",
                        ex.Message), ADDIN_NAME, 
                        MessageBoxButtons.OK, MessageBoxIcon.Error);
                    statusbar.Clear();
                    return;
                }
            }

            // Copy external projects to solution dir
            //
            ExtProjectMap fmap = null;
            statusbar.Text = ADDIN_NAME + ": Handle external projects...";
            foreach (Project p in sol.Projects)
            {
                // Ignore Web deployment projects!
                try 
                { 
                    string uname = p.UniqueName; 
                } 
                catch (NullReferenceException) 
                { 
                    continue; 
                }

                if (!string.IsNullOrEmpty(p.FileName) 
                    && Path.IsPathRooted(p.FileName) && 
                    p.FileName.IndexOf(solpath) == -1)
                {
                    // This project is not in the solution directory
                    if (extDir == null) 
                    {
                        // Make the external projects dir
                        extDir = Path.Combine(solpath, "szExternalProjects");
                        if (!Directory.Exists(extDir))
                        {
                            try
                            {
                                Directory.CreateDirectory(extDir);
                            }
                            catch (Exception ex)
                            {
                                MessageBox.Show(string.Format(
                                    "Failed to create temporary " + 
                                    "directory:\n{0}\nError: " + 
                                    "{1}\n\nZip file not created.",
                                    extDir, ex.Message), ADDIN_NAME, 
                                    MessageBoxButtons.OK, 
                                    MessageBoxIcon.Error);
                                statusbar.Clear();
                                return;
                            }
                        }
                        fmap = new ExtProjectMap(extDir);
                    }
                    // Copy this project to solution temporary directory
                    string srcPath = Path.GetDirectoryName(p.FullName);
                    string dstPath = 
                        Path.Combine(extDir, Path.GetFileName(srcPath));
                    try
                    {
                        xDirectory.Copy(srcPath, dstPath);
                    }
                    catch (Exception ex)
                    {
                        MessageBox.Show(string.Format(
                            "Failed to copy:\n\nSource: " + 
                            "{0}\nDestination: {1}\n\nError: {2}\n\nZip " + 
                            "file not created.",
                            srcPath, dstPath, ex.Message), 
                            ADDIN_NAME, MessageBoxButtons.OK, 
                            MessageBoxIcon.Error);
                        DeleteDirContents(extDir);
                        statusbar.Clear();
                        return;
                    }
                    fmap.Add(Path.GetFileName(srcPath) + 
                         "\\" + Path.GetFileName(p.UniqueName),srcPath);
                }
            }
                  
            // Create zip file paths and name
            //
            statusbar.Text = ADDIN_NAME + ": Creating Zip file...";
            DateTime dt = DateTime.Now;
            string sname = Path.GetFileName(sol.FileName);
            // SolutionName_YYYYMMDD-HHMMSS.zip
            string zipname = 
                string.Format("{0}_{1:D2}{2:D2}{3:D2}-{4:D2}{5:D2}{6:D2}.zip",
                sname.Substring(0, sname.Length - 4), 
                dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second);
            // Save zip file in the parent directory, not 
            // in the solution directory
            string zippath = 
                Path.Combine(Path.GetDirectoryName(solpath),zipname);
            ZipDirectoryHierarchy zs = new ZipDirectoryHierarchy();
            zs.ZipIt(solpath, zippath);

            // Clean up for projects that were copied to the temporary dir
            //
            if (extDir != null)
            {
                statusbar.Text = ADDIN_NAME + ": Project clean-up...";
                DeleteDirContents(extDir);
            }
            statusbar.Clear();
            MessageBox.Show(string.Format(
                "Successfully created:\n\n{0}\n\n{1}",
                zippath,zs.StatusString),
                ADDIN_NAME, MessageBoxButtons.OK, 
                MessageBoxIcon.Information);
            return;
        }
    }
}

This code is pretty self-explanatory.

Notes

  • In addition to its Text property, EnvDTE.StatusBar also has a Progress method that allows control of the VS progress bar
  • The _DTE.ExecuteCommand method allows you to execute any VS command or macro. The command name can be found in the Tools>Options>Environment>Keyboard dialog, which has a nice search feature:

    Screenshot - SolutionZipper-options-kb.JPG

  • All available solution configurations are cleaned. Unfortunately, VS does not provide a way to clean Deployment Projects. This is handled by searching through all projects and finding those that have ".vdproj" in the UniqueName property. The contents of the Release and Debug directories are then deleted.
  • v1.1 update notes:
    • Projects that do not reside in the solution directory are cleaned and then copied to the current solution in a temporary directory, szExternalProjects. After the ZIP file is created, the temporary directory is removed. See Revision history for more details.
    • Note that the DeleteDirContents method now removes the entire directory tree recursively, including the directory itself.
  • v1.2 update notes:
    • Added improved error handling. See Revision history for more details.
    • Added support for Web Deployment projects. See Revision history for more details. Other than cleaning these projects when they exist in the solution directory, the changes mostly protect SolutionZipper from crashing due to the incomplete Project instance they create.
    • The ExtProjectMap class creates a ExternalProjectsMap.txt file and adds an entry for each external project added to the temporary directory. This documentation allows for easier reconstruction of the solution from the extracted ZIP. Here is an example:
      SolutionZipper: External projects in this directory.
      
      Project                     : Original Path
      ----------------------      : -------------
      extProject\extProject.csproj: C:\Projects\SolutionZipper\
                                        extProject\extProject
      extSetup\extSetup.vdproj    : C:\Projects\SolutionZipper\
                                        extProject\extSetup
    • The IDTExtensibility2.QueryStatus function was modified so that the SolutionZipper Tools menu item would only be available when a saved solution is loaded into the IDE.
      public void QueryStatus(string commandName, 
          vsCommandStatusTextWanted neededText, 
          ref vsCommandStatus status, ref object commandText)
      {
          if(neededText == 
             vsCommandStatusTextWanted.vsCommandStatusTextWantedNone)
          {
              if(commandName == "SolutionZipper.Connect.SolutionZipper")
              {
                  status = (vsCommandStatus
                      )vsCommandStatus.vsCommandStatusSupported;
                  if (!string.IsNullOrEmpty(
                      _applicationObject.Solution.FullName)) 
                      status |= vsCommandStatus.vsCommandStatusEnabled;
                  return;
              }
          }
      }

ZipDirectoryHierarchy

Since there are usually other non-project files somewhere in the solution, it is preferable to simply zip up the entire solution directory hierarchy. The ZipDirectoryHierarchy class uses the SharpZipLib to do this. The path entries in the ZIP file will be relative, i.e. not begin with \, and will start at the solution directory.

internal class ZipDirectoryHierarchy
{
    // Keep track of dir/file/byte counts
    private int DirCount = 0;
    private int FileCount = 0;
    private long ByteCount = 0;

    // Add a single file to the zip output
    private void ZipOne(ZipOutputStream s, 
        Crc32 crc, string basedir, string file)
    {
        if (Path.GetExtension(file) == ".ncb") 
            return; // ignore VC++ Intellisense database files
        FileInfo fi = new FileInfo(file);
        if ((fi.Attributes & FileAttributes.Hidden) != 0) 
            return; // ignore hidden files
        FileStream fs = File.OpenRead(file);
        byte[] buffer = new byte[fs.Length];
        fs.Read(buffer, 0, buffer.Length);
        String relpath = file.Substring(basedir.Length + 1); 
            // make path relative 
        ZipEntry entry = new ZipEntry(relpath);
        entry.DateTime = DateTime.Now;
        entry.Size = fs.Length;
        this.FileCount++;
        this.ByteCount += entry.Size;
        fs.Close();
        crc.Reset();
        crc.Update(buffer);
        entry.Crc = crc.Value;
        s.PutNextEntry(entry);
        s.Write(buffer, 0, buffer.Length);
    }
    // Recursive directory zipping
    private void ZipDirectory(ZipOutputStream s, 
        Crc32 crc, string basedir, string dirpath)
    {
        // All of the files
        string[] filenames = Directory.GetFiles(dirpath);
        if (filenames.Length > 0)
        {
            this.DirCount++; 
            // only count directories with files in them
            foreach (string file in filenames)
            {
                ZipOne(s, crc, basedir, file);
            }
        }
        // All of the directories
        string[] dirs = Directory.GetDirectories(dirpath);
        foreach (string dir in dirs)
        {
            DirectoryInfo di = new DirectoryInfo(dir);
            if ((di.Attributes & FileAttributes.Hidden) == 0) 
                // ignore hidden directories
                ZipDirectory(s, crc, basedir, dir);
        }
    }
    //------------------------------------------------------------
    // Public methods

    // Zip method
    public void ZipIt(string dirpath, string zipname)
    {
        Crc32 crc = new Crc32();
        ZipOutputStream s = new ZipOutputStream(File.Create(zipname));
        s.SetLevel(6); // 0 - store only to 9 - means best compression
        // Get the base path, which is the parent of the directory being
        // zipped (dirpath).
        // All zip file paths will be relative to this.
        string basepath = Path.GetDirectoryName(dirpath);
        ZipDirectory(s, crc, basepath , dirpath);
        s.Finish();
        s.Close();
    }

    // Get the status string
    public string StatusString
    {
        get
        {
            return string.Format(
                "{0,-6}\tKB " + 
                "(uncompressed)\n{1,-6}\tFiles\n{2,-6}\tDirectories",
                ByteCount / 1024, FileCount, DirCount);
        }
    }
}

Compatibility

I have only been able to test SolutionZipper on a limited number of environments and project types.

  • VS 2005: C# and Deployment projects are OK. It has not been tested on any other project types -- but it should work.
  • VS 2003: In addition to the Add-in installation differences between VS2003 and VS2005, SolutionZipper.dll is a .NET 2.0 assembly, so it will not work with older IDEs. It shouldn't be difficult to port Connect.cs back to VS 2003 though.
  • VS C# Express Edition: Does not work. Express Editions do not support Add-ins.

Conclusion

Please let me know if you have problems with SolutionZipper. I will provide bug fix updates as necessary and will consider including suggested enhancements in the future. Enjoy!

Revision history

  • v1.3: 31-July-2007
    • Fixed a bug that was causing SZ to fail during the "Handle external projects" phase.
    • Ignore VC++ Intellisense Database files, i.e. *.ncb.
    • Ignore hidden files and folders.
  • v1.2: 17-Sep-2006
    • Fixed a bug in DeleteDirContents that was causing SolutionZipper to fail if the directory did not exist.
    • Added error messages for when failures occur. One typical situation is when a project contains a file that is currently in use by another process. The inability to access that file will cause an error. For example, the following error will occur when trying to zip a web project when the ASP.NET development server is running:

      Screenshot - SolutionZipper-error.JPG

      This is solved by stopping the development server and running SolutionZipper again.
    • Added an external project map file, ExternalProjectsMap.txt, to the szExternalProjects directory. This file shows the original full directory path for each project in that directory. See details here.
    • Added limited support for web deployment projects. "Limited" means that the web deployment project must reside within the solution directory. This is because these project types only provide the Name property in the Project instance, so it is impossible to know the actual project directory. What's interesting is that other Project properties, like UniqueName, contain instances of an Exception. I've used this to keep SolutionZipper from failing when this project type is encountered.
  • v1.1: 09-Sep-2006
    • SolutionZipper v1.0 did not take into account that Visual Studio allows solutions to contain projects that reside outside of the solution directory. There were two possible solutions for this:
      1. Include the external projects in the ZIP file with "project" level relative directories at the same level as the solution directory.
      2. Copy the external projects into a temporary solution directory and remove the temporary directory after the ZIP file is created. All projects are maintained under the single solution directory.
      I decided to take the second approach because the external projects in #1 would likely be extracted to the wrong location. In either case then, the external projects would have to be manually copied back to their original location. However, solution #2 avoids possible confusion. External C# and deployment projects were added to the SolutionZipper solution source for testing.
    • The DeleteDirContents method now removes the entire directory tree recursively, including the directory itself.
    • Recursive directory copy code is from xDirectory.Copy() - Copy Folder, SubFolders and Files (unmodified).
  • v1.0: 05-Sep-2006
    • Initial release.

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