Analisi dei file di soluzione di Visual Studio

Come posso analizzare i file della soluzione Visual Studio (SLN) in .NET? Mi piacerebbe scrivere un’app che unisca più soluzioni in una mentre si salva l’ordine di costruzione relativo.

La versione .NET 4.0 dell’assembly Microsoft.Build contiene una class SolutionParser nello spazio dei nomi Microsoft.Build.Construction che analizza i file di soluzione di Visual Studio.

Sfortunatamente questa class è interna, ma ho racchiuso parte di quella funzionalità in una class che utilizza la riflessione per ottenere alcune proprietà comuni che potrebbero essere utili.

public class Solution { //internal class SolutionParser //Name: Microsoft.Build.Construction.SolutionParser //Assembly: Microsoft.Build, Version=4.0.0.0 static readonly Type s_SolutionParser; static readonly PropertyInfo s_SolutionParser_solutionReader; static readonly MethodInfo s_SolutionParser_parseSolution; static readonly PropertyInfo s_SolutionParser_projects; static Solution() { s_SolutionParser = Type.GetType("Microsoft.Build.Construction.SolutionParser, Microsoft.Build, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", false, false); if (s_SolutionParser != null) { s_SolutionParser_solutionReader = s_SolutionParser.GetProperty("SolutionReader", BindingFlags.NonPublic | BindingFlags.Instance); s_SolutionParser_projects = s_SolutionParser.GetProperty("Projects", BindingFlags.NonPublic | BindingFlags.Instance); s_SolutionParser_parseSolution = s_SolutionParser.GetMethod("ParseSolution", BindingFlags.NonPublic | BindingFlags.Instance); } } public List Projects { get; private set; } public Solution(string solutionFileName) { if (s_SolutionParser == null) { throw new InvalidOperationException("Can not find type 'Microsoft.Build.Construction.SolutionParser' are you missing a assembly reference to 'Microsoft.Build.dll'?"); } var solutionParser = s_SolutionParser.GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic).First().Invoke(null); using (var streamReader = new StreamReader(solutionFileName)) { s_SolutionParser_solutionReader.SetValue(solutionParser, streamReader, null); s_SolutionParser_parseSolution.Invoke(solutionParser, null); } var projects = new List(); var array = (Array)s_SolutionParser_projects.GetValue(solutionParser, null); for (int i = 0; i < array.Length; i++) { projects.Add(new SolutionProject(array.GetValue(i))); } this.Projects = projects; } } [DebuggerDisplay("{ProjectName}, {RelativePath}, {ProjectGuid}")] public class SolutionProject { static readonly Type s_ProjectInSolution; static readonly PropertyInfo s_ProjectInSolution_ProjectName; static readonly PropertyInfo s_ProjectInSolution_RelativePath; static readonly PropertyInfo s_ProjectInSolution_ProjectGuid; static readonly PropertyInfo s_ProjectInSolution_ProjectType; static SolutionProject() { s_ProjectInSolution = Type.GetType("Microsoft.Build.Construction.ProjectInSolution, Microsoft.Build, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", false, false); if (s_ProjectInSolution != null) { s_ProjectInSolution_ProjectName = s_ProjectInSolution.GetProperty("ProjectName", BindingFlags.NonPublic | BindingFlags.Instance); s_ProjectInSolution_RelativePath = s_ProjectInSolution.GetProperty("RelativePath", BindingFlags.NonPublic | BindingFlags.Instance); s_ProjectInSolution_ProjectGuid = s_ProjectInSolution.GetProperty("ProjectGuid", BindingFlags.NonPublic | BindingFlags.Instance); s_ProjectInSolution_ProjectType = s_ProjectInSolution.GetProperty("ProjectType", BindingFlags.NonPublic | BindingFlags.Instance); } } public string ProjectName { get; private set; } public string RelativePath { get; private set; } public string ProjectGuid { get; private set; } public string ProjectType { get; private set; } public SolutionProject(object solutionProject) { this.ProjectName = s_ProjectInSolution_ProjectName.GetValue(solutionProject, null) as string; this.RelativePath = s_ProjectInSolution_RelativePath.GetValue(solutionProject, null) as string; this.ProjectGuid = s_ProjectInSolution_ProjectGuid.GetValue(solutionProject, null) as string; this.ProjectType = s_ProjectInSolution_ProjectType.GetValue(solutionProject, null).ToString(); } } 

Si noti che è necessario modificare il framework di destinazione in ".NET Framework 4" (non nel profilo client) per poter aggiungere il riferimento Microsoft.Build al progetto.

Con Visual Studio 2015 è ora disponibile una class SolutionFile accessibile al pubblico che può essere utilizzata per analizzare i file di soluzione:

 using Microsoft.Build.Construction; var _solutionFile = SolutionFile.Parse(path); 

Questa class si trova nell’assembly Microsoft.Build.dll 14.0.0.0 . Nel mio caso si trovava a:

 C:\Program Files (x86)\Reference Assemblies\Microsoft\MSBuild\v14.0\Microsoft.Build.dll 

Grazie a Phil per averlo fatto notare !

Non so se qualcuno è ancora alla ricerca di soluzioni a questo problema, ma mi sono imbattuto in un progetto che sembra fare solo ciò che è necessario. https://slntools.codeplex.com/ Una delle funzioni di questo strumento è quella di unire più soluzioni insieme.

I JetBrain (i creatori di Resharper) hanno capacità di analisi del pubblico nelle loro assemblee (non è necessaria alcuna riflessione). Probabilmente è più robusto delle soluzioni open source esistenti qui suggerite (per non parlare degli hack di ReGex). Tutto quello che devi fare è:

  • Scarica gli strumenti della riga di comando di ReSharper (gratuito).
  • Aggiungi quanto segue come riferimento al tuo progetto
    • JetBrains.Platform.ProjectModel
    • JetBrains.Platform.Util
    • JetBrains.Platform.Interop.WinApi

La libreria non è documentata, ma Reflector (o in effetti, dotPeek) è tuo amico. Per esempio:

 public static void PrintProjects(string solutionPath) { var slnFile = SolutionFileParser.ParseFile(FileSystemPath.Parse(solutionPath)); foreach (var project in slnFile.Projects) { Console.WriteLine(project.ProjectName); Console.WriteLine(project.ProjectGuid); Console.WriteLine(project.ProjectTypeGuid); foreach (var kvp in project.ProjectSections) { Console.WriteLine(kvp.Key); foreach (var projectSection in kvp.Value) { Console.WriteLine(projectSection.SectionName); Console.WriteLine(projectSection.SectionValue); foreach (var kvpp in projectSection.Properties) { Console.WriteLine(kvpp.Key); Console.WriteLine(string.Join(",", kvpp.Value)); } } } } } 

Non posso davvero offrirti una biblioteca e la mia ipotesi è che non ce n’è una che esiste là fuori. Ma ho trascorso un po ‘di tempo a fare casino con i file .sln in scenari di editing batch e ho trovato che Powershell è uno strumento molto utile per questa attività. Il formato .SLN è piuttosto semplice e può essere analizzato quasi completamente con alcune espressioni veloci e sporche. Per esempio

File di progetto inclusi.

 gc ConsoleApplication30.sln | ? { $_ -match "^Project" } | %{ $_ -match ".*=(.*)$" | out-null ; $matches[1] } | %{ $_.Split(",")[1].Trim().Trim('"') } 

Non è sempre bello, ma è un modo efficace per eseguire l’elaborazione in batch.

abbiamo risolto un problema simile di unione automatica delle soluzioni scrivendo un plug-in di Visual Studio che creava una nuova soluzione, quindi cercava il file * .sln e lo importava nel nuovo usando:

 dte2.Solution.AddFromFile(solutionPath, false); 

Il nostro problema era leggermente diverso in quanto volevamo che VS decidesse di ordinare l’ordine di costruzione per noi, così abbiamo poi convertito tutti i riferimenti alle DLL nei riferimenti ai progetti laddove ansible.

Quindi l’abbiamo automatizzato in un processo di build eseguendo VS tramite l’automazione COM.

Questa soluzione era un po ‘Heath Robinson, ma aveva il vantaggio che VS stava facendo il assembly in modo che il nostro codice non dipendesse dal formato del file sln. Che è stato utile quando ci siamo trasferiti dal VS 2005 al 2008 e di nuovo al 2010.

Tutto è grandioso, ma volevo anche ottenere la capacità di generazione sln – nell’istantanea del codice sopra stai solo analizzando i file .sln – Volevo fare qualcosa di simile, tranne essere in grado di rigenerare sln con lievi modifiche al file .sln . Tali casi potrebbero essere, ad esempio, il porting dello stesso progetto per diverse piattaforms .NET. Per ora è solo una nuova generazione, ma in seguito la espanderò anche ai progetti.

Suppongo che volessi anche dimostrare la potenza delle espressioni regolari e delle interfacce native. (Più piccola quantità di codice con più funzionalità)

Aggiornamento 4.1.2017 Ho creato un repository svn separato per l’analisi della soluzione .sln: https://sourceforge.net/p/syncproj/code/HEAD/tree/

Di seguito è riportato il mio frammento di esempio di codice (predecessore). Sei libero di usare qualcuno di loro.

È ansible che in futuro il codice di analisi della soluzione basato su svn sarà aggiornato anche con funzionalità di generazione.

Aggiornamento 4.2.2017 Il codice sorgente in SVN supporta anche la generazione .sln.

 using System; using System.Linq; using System.Collections.Generic; using System.IO; using System.Diagnostics; using System.Text.RegularExpressions; using System.Text; public class Program { [DebuggerDisplay("{ProjectName}, {RelativePath}, {ProjectGuid}")] public class SolutionProject { public string ParentProjectGuid; public string ProjectName; public string RelativePath; public string ProjectGuid; public string AsSlnString() { return "Project(\"" + ParentProjectGuid + "\") = \"" + ProjectName + "\", \"" + RelativePath + "\", \"" + ProjectGuid + "\""; } } ///  /// .sln loaded into class. ///  public class Solution { public List slnLines; // List of either String (line format is not intresting to us), or SolutionProject. ///  /// Loads visual studio .sln solution ///  ///  /// The file specified in path was not found. public Solution( string solutionFileName ) { slnLines = new List(); String slnTxt = File.ReadAllText(solutionFileName); string[] lines = slnTxt.Split('\n'); //Match string like: Project("{66666666-7777-8888-9999-AAAAAAAAAAAA}") = "ProjectName", "projectpath.csproj", "{11111111-2222-3333-4444-555555555555}" Regex projMatcher = new Regex("Project\\(\"(?{[A-F0-9-]+})\"\\) = \"(?.*?)\", \"(?.*?)\", \"(?{[A-F0-9-]+})"); Regex.Replace(slnTxt, "^(.*?)[\n\r]*$", new MatchEvaluator(m => { String line = m.Groups[1].Value; Match m2 = projMatcher.Match(line); if (m2.Groups.Count < 2) { slnLines.Add(line); return ""; } SolutionProject s = new SolutionProject(); foreach (String g in projMatcher.GetGroupNames().Where(x => x != "0")) /* "0" - RegEx special kind of group */ s.GetType().GetField(g).SetValue(s, m2.Groups[g].ToString()); slnLines.Add(s); return ""; }), RegexOptions.Multiline ); } ///  /// Gets list of sub-projects in solution. ///  /// true if get also sub-folders. public List GetProjects( bool bGetAlsoFolders = false ) { var q = slnLines.Where( x => x is SolutionProject ).Select( i => i as SolutionProject ); if( !bGetAlsoFolders ) // Filter away folder names in solution. q = q.Where( x => x.RelativePath != x.ProjectName ); return q.ToList(); } ///  /// Saves solution as file. ///  public void SaveAs( String asFilename ) { StringBuilder s = new StringBuilder(); for( int i = 0; i < slnLines.Count; i++ ) { if( slnLines[i] is String ) s.Append(slnLines[i]); else s.Append((slnLines[i] as SolutionProject).AsSlnString() ); if( i != slnLines.Count ) s.AppendLine(); } File.WriteAllText(asFilename, s.ToString()); } } static void Main() { String projectFile = @"yourown.sln"; try { String outProjectFile = Path.Combine(Path.GetDirectoryName(projectFile), Path.GetFileNameWithoutExtension(projectFile) + "_2.sln"); Solution s = new Solution(projectFile); foreach( var proj in s.GetProjects() ) { Console.WriteLine( proj.RelativePath ); } SolutionProject p = s.GetProjects().Where( x => x.ProjectName.Contains("Plugin") ).First(); p.RelativePath = Path.Combine( Path.GetDirectoryName(p.RelativePath) , Path.GetFileNameWithoutExtension(p.RelativePath) + "_Variation" + ".csproj"); s.SaveAs(outProjectFile); } catch (Exception ex) { Console.WriteLine("Error: " + ex.Message); } } } 

Ho esposto, determinato che le classi MSBuild possono essere utilizzate per manipolare le strutture sottostanti. Avrò ulteriore codice sul mio sito web più tardi.

 // VSSolution using System; using System.Reflection; using System.Collections.Generic; using System.Linq; using System.Diagnostics; using System.IO; using AbstractX.Contracts; namespace VSProvider { public class VSSolution : IVSSolution { //internal class SolutionParser //Name: Microsoft.Build.Construction.SolutionParser //Assembly: Microsoft.Build, Version=4.0.0.0 static readonly Type s_SolutionParser; static readonly PropertyInfo s_SolutionParser_solutionReader; static readonly MethodInfo s_SolutionParser_parseSolution; static readonly PropertyInfo s_SolutionParser_projects; private string solutionFileName; private List projects; public string Name { get { return Path.GetFileNameWithoutExtension(solutionFileName); } } public IEnumerable Projects { get { return projects; } } static VSSolution() { s_SolutionParser = Type.GetType("Microsoft.Build.Construction.SolutionParser, Microsoft.Build, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", false, false); s_SolutionParser_solutionReader = s_SolutionParser.GetProperty("SolutionReader", BindingFlags.NonPublic | BindingFlags.Instance); s_SolutionParser_projects = s_SolutionParser.GetProperty("Projects", BindingFlags.NonPublic | BindingFlags.Instance); s_SolutionParser_parseSolution = s_SolutionParser.GetMethod("ParseSolution", BindingFlags.NonPublic | BindingFlags.Instance); } public string SolutionPath { get { var file = new FileInfo(solutionFileName); return file.DirectoryName; } } public VSSolution(string solutionFileName) { if (s_SolutionParser == null) { throw new InvalidOperationException("Can not find type 'Microsoft.Build.Construction.SolutionParser' are you missing a assembly reference to 'Microsoft.Build.dll'?"); } var solutionParser = s_SolutionParser.GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic).First().Invoke(null); using (var streamReader = new StreamReader(solutionFileName)) { s_SolutionParser_solutionReader.SetValue(solutionParser, streamReader, null); s_SolutionParser_parseSolution.Invoke(solutionParser, null); } this.solutionFileName = solutionFileName; projects = new List(); var array = (Array)s_SolutionParser_projects.GetValue(solutionParser, null); for (int i = 0; i < array.Length; i++) { projects.Add(new VSProject(this, array.GetValue(i))); } } public void Dispose() { } } } // VSProject using System; using System.Reflection; using System.Collections.Generic; using System.Linq; using System.Diagnostics; using System.IO; using System.Xml; using AbstractX.Contracts; using System.Collections; namespace VSProvider { [DebuggerDisplay("{ProjectName}, {RelativePath}, {ProjectGuid}")] public class VSProject : IVSProject { static readonly Type s_ProjectInSolution; static readonly Type s_RootElement; static readonly Type s_ProjectRootElement; static readonly Type s_ProjectRootElementCache; static readonly PropertyInfo s_ProjectInSolution_ProjectName; static readonly PropertyInfo s_ProjectInSolution_ProjectType; static readonly PropertyInfo s_ProjectInSolution_RelativePath; static readonly PropertyInfo s_ProjectInSolution_ProjectGuid; static readonly PropertyInfo s_ProjectRootElement_Items; private VSSolution solution; private string projectFileName; private object internalSolutionProject; private List items; public string Name { get; private set; } public string ProjectType { get; private set; } public string RelativePath { get; private set; } public string ProjectGuid { get; private set; } public string FileName { get { return projectFileName; } } static VSProject() { s_ProjectInSolution = Type.GetType("Microsoft.Build.Construction.ProjectInSolution, Microsoft.Build, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", false, false); s_ProjectInSolution_ProjectName = s_ProjectInSolution.GetProperty("ProjectName", BindingFlags.NonPublic | BindingFlags.Instance); s_ProjectInSolution_ProjectType = s_ProjectInSolution.GetProperty("ProjectType", BindingFlags.NonPublic | BindingFlags.Instance); s_ProjectInSolution_RelativePath = s_ProjectInSolution.GetProperty("RelativePath", BindingFlags.NonPublic | BindingFlags.Instance); s_ProjectInSolution_ProjectGuid = s_ProjectInSolution.GetProperty("ProjectGuid", BindingFlags.NonPublic | BindingFlags.Instance); s_ProjectRootElement = Type.GetType("Microsoft.Build.Construction.ProjectRootElement, Microsoft.Build, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", false, false); s_ProjectRootElementCache = Type.GetType("Microsoft.Build.Evaluation.ProjectRootElementCache, Microsoft.Build, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", false, false); s_ProjectRootElement_Items = s_ProjectRootElement.GetProperty("Items", BindingFlags.Public | BindingFlags.Instance); } public IEnumerable Items { get { return items; } } public VSProject(VSSolution solution, object internalSolutionProject) { this.Name = s_ProjectInSolution_ProjectName.GetValue(internalSolutionProject, null) as string; this.ProjectType = s_ProjectInSolution_ProjectType.GetValue(internalSolutionProject, null).ToString(); this.RelativePath = s_ProjectInSolution_RelativePath.GetValue(internalSolutionProject, null) as string; this.ProjectGuid = s_ProjectInSolution_ProjectGuid.GetValue(internalSolutionProject, null) as string; this.solution = solution; this.internalSolutionProject = internalSolutionProject; this.projectFileName = Path.Combine(solution.SolutionPath, this.RelativePath); items = new List(); if (this.ProjectType == "KnownToBeMSBuildFormat") { this.Parse(); } } private void Parse() { var stream = File.OpenRead(projectFileName); var reader = XmlReader.Create(stream); var cache = s_ProjectRootElementCache.GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic).First().Invoke(new object[] { true }); var rootElement = s_ProjectRootElement.GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic).First().Invoke(new object[] { reader, cache }); stream.Close(); var collection = (ICollection)s_ProjectRootElement_Items.GetValue(rootElement, null); foreach (var item in collection) { items.Add(new VSProjectItem(this, item)); } } public IEnumerable EDMXModels { get { return this.items.Where(i => i.ItemType == "EntityDeploy"); } } public void Dispose() { } } } // VSProjectItem using System; using System.Reflection; using System.Collections.Generic; using System.Linq; using System.Diagnostics; using System.IO; using System.Xml; using AbstractX.Contracts; namespace VSProvider { [DebuggerDisplay("{ProjectName}, {RelativePath}, {ProjectGuid}")] public class VSProjectItem : IVSProjectItem { static readonly Type s_ProjectItemElement; static readonly PropertyInfo s_ProjectItemElement_ItemType; static readonly PropertyInfo s_ProjectItemElement_Include; private VSProject project; private object internalProjectItem; private string fileName; static VSProjectItem() { s_ProjectItemElement = Type.GetType("Microsoft.Build.Construction.ProjectItemElement, Microsoft.Build, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", false, false); s_ProjectItemElement_ItemType = s_ProjectItemElement.GetProperty("ItemType", BindingFlags.Public | BindingFlags.Instance); s_ProjectItemElement_Include = s_ProjectItemElement.GetProperty("Include", BindingFlags.Public | BindingFlags.Instance); } public string ItemType { get; private set; } public string Include { get; private set; } public VSProjectItem(VSProject project, object internalProjectItem) { this.ItemType = s_ProjectItemElement_ItemType.GetValue(internalProjectItem, null) as string; this.Include = s_ProjectItemElement_Include.GetValue(internalProjectItem, null) as string; this.project = project; this.internalProjectItem = internalProjectItem; // todo - expand this if (this.ItemType == "Compile" || this.ItemType == "EntityDeploy") { var file = new FileInfo(project.FileName); fileName = Path.Combine(file.DirectoryName, this.Include); } } public byte[] FileContents { get { return File.ReadAllBytes(fileName); } } public string Name { get { if (fileName != null) { var file = new FileInfo(fileName); return file.Name; } else { return this.Include; } } } } } 

La risposta di @ john-leidegren è fantastica. Per pre-VS2015, questo è di grande utilità. Ma c’è stato un errore minore, in quanto mancava il codice per recuperare le configurazioni. Quindi volevo aggiungerlo, nel caso qualcuno stia lottando per usare questo codice.
Il miglioramento è molto semplice:

  public class Solution { //internal class SolutionParser //Name: Microsoft.Build.Construction.SolutionParser //Assembly: Microsoft.Build, Version=4.0.0.0 static readonly Type s_SolutionParser; static readonly PropertyInfo s_SolutionParser_solutionReader; static readonly MethodInfo s_SolutionParser_parseSolution; static readonly PropertyInfo s_SolutionParser_projects; static readonly PropertyInfo s_SolutionParser_configurations;//this was missing in john's answer static Solution() { s_SolutionParser = Type.GetType("Microsoft.Build.Construction.SolutionParser, Microsoft.Build, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", false, false); if ( s_SolutionParser != null ) { s_SolutionParser_solutionReader = s_SolutionParser.GetProperty("SolutionReader", BindingFlags.NonPublic | BindingFlags.Instance); s_SolutionParser_projects = s_SolutionParser.GetProperty("Projects", BindingFlags.NonPublic | BindingFlags.Instance); s_SolutionParser_parseSolution = s_SolutionParser.GetMethod("ParseSolution", BindingFlags.NonPublic | BindingFlags.Instance); s_SolutionParser_configurations = s_SolutionParser.GetProperty("SolutionConfigurations", BindingFlags.NonPublic | BindingFlags.Instance); //this was missing in john's answer // additional info: var PropNameLst = GenHlp_PropBrowser.PropNamesOfType(s_SolutionParser); // the above call would yield something like this: // [ 0] "SolutionParserWarnings" string // [ 1] "SolutionParserComments" string // [ 2] "SolutionParserErrorCodes" string // [ 3] "Version" string // [ 4] "ContainsWebProjects" string // [ 5] "ContainsWebDeploymentProjects" string // [ 6] "ProjectsInOrder" string // [ 7] "ProjectsByGuid" string // [ 8] "SolutionFile" string // [ 9] "SolutionFileDirectory" string // [10] "SolutionReader" string // [11] "Projects" string // [12] "SolutionConfigurations" string } } public List Projects { get; private set; } public List Configurations { get; private set; } //... //... //... no change in the rest of the code } 

Come ulteriore aiuto, fornendo un codice semplice per sfogliare le proprietà di un System.Type come suggerito da @oasten.

 public class GenHlp_PropBrowser { public static List PropNamesOfClass(object anObj) { return anObj == null ? null : PropNamesOfType(anObj.GetType()); } public static List PropNamesOfType(System.Type aTyp) { List retLst = new List(); foreach ( var p in aTyp.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance) ) { retLst.Add(p.Name); } return retLst; } } 

Grazie a @John Leidegren offre un modo efficace. Scrivo una class hlper perché non posso usare il suo codice che non riesce a trovare s_SolutionParser_configurations e i progetti senza FullName.

Il codice è in github che può ottenere i progetti con FullName.

E il codice non può ottenere SolutionConfiguration.

Ma quando si dev un vsx il vs dirà cant trovare Microsoft.Build.dll , quindi si può provare a usare dte per ottenere tutti i progetti.

Il codice che usa dte per ottenere tutti i progetti è in github

Vedi: http://www.wwwlicious.com/2011/03/29/envdte-getting-all-projects-html/