OdeToCode IC Logo

Sorting the Visual Studio "Add New Item" Dialog with PowerShell

Saturday, February 10, 2007

The items in the "Add New Item" dialog box of Visual Studio appear in an arbitrary order. After a bit of sleuthing, I put together a brute force Powershell script to sort my items alphabetically. Now "Code File" appears near "Class", and I can always find "XML File" near the bottom of the dialog.

sorted items in vs2005

SortOrder, and my sanity, is restored.

What follows is the script. Download sort-vsItems.ps1. Let me caveat this script by saying it has only been tested on two machines. If you have any problems, do let me know.

# sort-vsItems
# scott@OdeToCode.com
# Modified by AlexanderGross at gmx dot de
#
# Use at your own risk!
# Script will make a backup of each template just in case.

# vjslib for .zip support.
[System.Reflection.Assembly]::Load("vjslib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a") | out-null

# Get list of VS installed templates.
$installDir = [System.IO.Path]::Combine((gp HKLM:Software\Microsoft\VisualStudio\8.0).InstallDir, "ItemTemplates")
$templateFiles = gci -recurse $installDir | ? {$_.extension -eq ".zip"}

# Append list of custom templates.
$installDir = [System.IO.Path]::Combine((gp "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders").Personal, "Visual Studio 2005\Templates\ItemTemplates")
$templateFiles += gci -recurse $installDir | ? {$_.extension -eq ".zip"}

# Sort all templates by filename.
$templateFiles = $templateFiles | sort name

$i = 1
$count = 0
$tmpDir = new-item ([System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), [System.IO.Path]::GetRandomFileName())) -type directory
$buffer = new-object System.SByte[] (8192)

# Iterate through all template files.
foreach($templateFile in $templateFiles)
{
write-host "Processing" $templateFile.FullName

# Extract all files (no methods available to modify zip in place).
$zip = new-object java.util.zip.ZipFile($templateFile.FullName)
$entries = $zip.entries()
while($entries.hasMoreElements())
{
$zipEntry = $entries.nextElement()

# Ensure output directory exists.
$filename = [System.IO.Path]::Combine($tmpDir.FullName, $zipEntry.getName())

# $zipEntry.isDirectory() does not work for Microsoft zips (e.g. Web\CSharp\WinFxServiceItemTemplate.zip).
$directory = [System.IO.Path]::GetDirectoryName($filename)
if ([System.IO.Directory]::Exists($directory) -ne $true)
{
mkdir $directory | out-null

# In case the zip tells us it's a directory entry, skip the entry to prevent exceptions.
if ($zipEntry.isDirectory())
{
continue
}
}

$in = $zip.getInputStream($zipEntry)
$out = new-object java.io.FileOutputStream($filename)

while(($count = $in.read($buffer, 0, $buffer.Count)) -gt 0)
{
$out.write($buffer, 0, $count)
}

$out.Close()
$in.Close()
}
$zip.Close()

# Tweak the sort order element.
$vst = gci -recurse $tmpDir | ? {$_.extension -eq ".vstemplate"}
if ($vst -eq $null)
{
# The zip file does not contain a vstemplate.

# Clean temporary directory for the next template file.
del $tmpDir\* -force -recurse
continue
}

$xmlDoc = new-object System.Xml.XmlDocument
$xmlDoc.Load($vst.FullName)
if ($xmlDoc.VSTemplate.TemplateData.SortOrder -ne $null)
{
# Sort by zip name. Sort order must be a multiple of 10.
$xmlDoc.VSTemplate.TemplateData.SortOrder = ($i++ * 10).ToString()

# Sort by item name in Visual Studio.
# Uncomment this line if you want to let Visual Studio sort by item name.
# $xmlDoc.VSTemplate.TemplateData.SortOrder = "10"

$xmlDoc.Save($vst.FullName)
}

# Backup existing zip file.
$backupName = $templateFile.FullName + ".bak"
if(test-path $backupName)
{
# Remove previous backups.
remove-item $backupName
}
move-item $templateFile.FullName $backupName

# Zip up modified version.
$zip = new-object java.util.zip.ZipOutputStream(new-object java.io.FileOutputStream($templateFile.FullName))
$files = gci -recurse $tmpDir
foreach($file in $files)
{
if ($file.Attributes -contains "Directory")
{
# Subfolders are created automatically with files residing in subfolders.
continue
}

# Create a file entry.
# Replacing the last backslash from $tmpDir.FullName is crucial, the zips would work with any other
# zip editor but Visual Studio doesn't like files with a leading backslash (though one doesn't see
# it in WinZip).
$zipEntry = new-object java.util.zip.ZipEntry($file.FullName.Replace($tmpDir.FullName + "\", ""))
$zip.putNextEntry($zipEntry)
$in = new-object java.io.FileInputStream($file.FullName)
while(($count = $in.read($buffer, 0, $buffer.Count)) -gt 0)
{
$zip.write($buffer, 0, $count)
}
$in.close()
$zip.closeEntry()

}
$zip.close()

# Clean temporary directory for the next template file.
del $tmpDir\* -force -recurse
}

del $tmpDir -force -recurse

write-host "Running Visual Studio to refresh templates"
$vstudio = (gp HKLM:Software\Microsoft\VisualStudio\8.0).InstallDir
& $vstudio\devenv /setup

Powershell is beauty!