Click here to Skip to main content
Click here to Skip to main content

CodeDOM Classes for Solution and Project Files (Part 5)

By , 30 Nov 2012
Rate this:
Please Sign up or sign in to vote.

Introduction

This article is about creating Solution and Project codeDOM objects that model the functionality of “.sln” and “.csproj” files, and can be used to parse, modify, and save such files. Sources are included. This is Part 5 of a series on codeDOMs, but it may be useful for anyone who wishes to programmatically manipulate VS solution and project files. In the previous parts, I’ve discussed “CodeDOMs”, and provided a C# codeDOM along with a WPF UI and IDE, and a C# parser

What Do We Need?

The main purpose here is to create Solution and Project classes that model the functionality of these concepts in a generic fashion – a solution as a container of projects, and a project as a collection of code files that is built into a single output assembly and has references to other projects and external assemblies. A secondary goal is to be able to load, modify, and save existing VS “.sln” and “.csproj” files, so this will require dealing with proprietary formats and data – we want to “hide” implementation details where possible, to insulate users from them. We won’t be adding full support for non-C# projects at this time, but we still need to be able to handle them in a limited way (when we get to resolving, we’ll want to load metadata from output assemblies of projects for other languages).

Modeling a Solution

A Solution is primarily a collection of Projects, so I gave it a ChildList of Projects. Of course, it’s derived from CodeObject like all codeDOM objects, and it implements the INamedCodeObject interface, along with a new IFile interface since it’s usually mapped to a file. I determined that I wanted the solution to hold a collection of annotations (messages and “listed” comments, such as “TODO”) for the entire solution – but I found a need to record the Project and/or CodeUnit that each Annotation is associated with, so I created a CodeAnnotation wrapper class and gave the solution an ObservableCollection of this type. A solution also has an ActiveConfiguration and ActivePlatform (both strings). There’s a LoadOptions enum to control parsing, logging, etc while loading, and a LoadStatus enum used to broadcast progress events to any associated UI. These enums and the CodeAnnotation class are at global scope, but I chose to contain them inside the “Solution.cs” file for now since they’re small.

Implementation details include dealing with the content of the “.sln” file, which is a text file with a proprietary format. It has an entry for each project which in turn contains “project section” entries, and there is a global area which contains “global section” entries for things such as source control, configuration information, project nesting, etc. So, I created ProjectEntry, ProjectSection, and GlobalSection nested classes to model this information. The solution parses all of this, and it’s available for manipulation if necessary, but it’s automatically managed and can be ignored in most situations. I haven’t needed to manipulate source control information yet, so I haven’t modeled it separately for convenience, but it can be accessed through the appropriate global section if necessary.

Here’s an example of loading a solution (taken from the Nova.Examples project):

// Load a solution, specifying the configuration and platform - these are optional,
// but can affect conditionally compiled code.  Also, turn on logging of messages.
Solution solution = Solution.Load("Nova.Examples.sln", "Debug", "x86",
                                  LoadOptions.Complete | LoadOptions.LogMessages);
if (solution != null)
    Log.WriteLine("Solution '" + solution.Name + "' successfully loaded, parsed.");

Modeling a Project

For now, I’m creating just a single Project class for all types of projects – this can be split into a hierarchy of project subtypes if/when support is added for other languages. It has a ChildList of CodeUnits, is derived from CodeObject, and implements the INamedCodeObject and IFile interfaces. A project has a number of global settings, such as ProductVersion, ProjectGuid, AssemblyName, and many more. It also has multiple configurations, each of which has a name (such as “Debug”), platform (such as “x86”), and various configuration-specific settings, such as OutputPath, DefineConstants (conditional compilation symbols), WarningLevel, etc. A nested Configuration class models these settings, and a project has a ChildList of these. The CurrentConfiguration property represents the currently selected configuration. A project also has a ChildList of Reference objects, which is subclassed into AssemblyReference, ProjectReference, and COMReference – for now, we’re not doing much with these other than keeping track of them (we’ll be using them in the next article to load metadata).

Implementation details include dealing with the content of the “.csproj” file, which is in XML with many proprietary tags. Settings are stored under PropertyGroup tags, in global and configuration-specific groups and are read directly into global fields or Configuration objects. References and files are stored under ItemGroup tags, with a Reference tag for references, a Compile tag for source files, and various other file tags (Content, Page, etc). References are read into a ChildList of appropriate Reference object subclasses, while files are read into a ChildList of FileItem instances (a nested type) while also creating a CodeUnit for each source file. This makes it possible to manipulate non-source files in the project if necessary, while normally just dealing with the CodeUnit collection. The project file is parsed using an XmlReader, but is stored in fields and objects as discussed above. This presents a problem: New XML tags might be added that are not yet supported by the code. This is handled using lists of a nested UnhandledData type to store any unrecognized data in the project file, so that it can be inspected and also written back to the file if it’s saved.

There is also support for parsing the “Web.config” files of “website projects”. And, each project has a list of project type Guid objects, many of which are predefined, such as Project.CSProjectType being the guid for C# projects. Projects are complicated, so there are yet more details handled in the attached code, but we’ve covered the more important things.

Projects are generally loaded automatically when you load their parent solution, but you can also load them directly, as shown in the example below (taken from the Nova.Examples project):

// Load a project, specifying the configuration and platform - these are optional, but
// the configuration can affect conditionally compiled code.
Project project = Project.Load("Nova.Examples.csproj", "Debug", "x86");
if (project != null)
    Log.WriteLine("Project '" + project.Name + "' successfully loaded, parsed.");

Creating Solutions and Projects

It’s also possible to create new solutions/projects and save them, like this (from Nova.Examples):

// Create a new solution and project
const string path = @"Generated\";
Solution solution = new Solution(path + "Solution");  // extension will default to '.sln'
Project project = solution.CreateProject(path + "Project");  // will default to '.csproj'
project.OutputType = Project.OutputTypes.Exe;  // Output type will default to Library

// Add a file to the project, and put some code in it
CodeUnit codeUnit = project.CreateCodeUnit(path + "Program");  // will default to '.cs'
codeUnit.Add(
    new UsingDirective(project.ParseName("System")),
    new UsingDirective(project.ParseName("System.Text"))
    );
NamespaceDecl namespaceDecl = new NamespaceDecl(project.ParseName("Generated"));
codeUnit.Add(namespaceDecl);
ClassDecl classDecl = new ClassDecl("Program");
namespaceDecl.Add(classDecl);
MethodDecl methodDecl = new MethodDecl("Main", typeof(void), Modifiers.Static,
                                       new ParameterDecl("args", typeof(string[])));
methodDecl.Add(new Comment("Add code here"));
classDecl.Add(methodDecl);

// Save the entire solution, including all projects and files
solution.SaveAll();

Nova Studio Changes

My “IDE” can now load “.sln” and “.csproj” files. Any messages generated during loading (CodeAnnotation instances “bubbled up” to the Solution) are displayed in the Messages window at the bottom, and the pane on the right is now a full “solution tree” which includes tooltips (projects show their configuration and target platform). The active configuration and platform are shown on the toolbar, and clicking on them shows a window which allows them to be changed. Changes are persisted in an “.nuo” user settings file, and making them also forces a re-parse since it might affect how conditional compiler directives are parsed. Here’s a screenshot with the “Nova.sln” solution loaded:

When loading larger solutions, you’ll notice the solution tree populating dynamically by way of status callbacks as it loads. I think it’s rather fast – less than 6 seconds to load a solution I created with almost 2,000 test projects from the Mono compiler. To compare, VS takes about 1 second per project, so probably about 30 minutes (I say “probably” because the last time I actually tried to wait that long it seemed to get stuck and never finish). To be fair, I’m not loading referenced assemblies yet – I’ll do a more detailed comparison later in this series.

Using the Attached Source Code

The new code added in this installment is located in a new CodeDOM/Projects folder in the Nova.CodeDOM project. In addition to the new Solution and Project classes and References folder, the existing CodeUnit class and Namespaces folder have been moved to this folder. There is a new IFile interface in CodeDOM/Base/Interfaces which is implemented by Solution, Project, and CodeUnit. There are new examples in Nova.Examples for loading solutions and projects, generating a solution manually, and there are now LINQ query examples for working with solutions and projects. Nova Studio can now load (parse) and save or diff solution and project files! Try loading “Nova.sln”, or try some of your own solutions or projects.  As usual, a separate ZIP file containing binaries is provided so that you can run them without having to build them first.

Summary 

My codeDOM now models solutions and projects, and can parse and save “.sln” and “.csproj” files. Nova Studio is starting to look more like a real IDE, but there’s still a lot of work to be done. How about all of those red unresolved symbolic references in the code? We really need to implement the ability to resolve those. However, a lot of those references are to things declared in external assemblies, such as the .NET BCL. So… in my next article, I’ll add loading of type metadata from .NET assemblies, in preparation for resolving symbolic references within a codeDOM.

License

This article, along with any associated source code and files, is licensed under The Common Development and Distribution License (CDDL)

About the Author

KenBeckett
Software Developer (Senior)
United States United States
I've been writing software since the late 70's, currently focusing mainly on C#.NET. I also like to travel around the world, and I own a Chocolate Factory (sadly, none of my employees are oompa loompas).

Comments and Discussions

 
GeneralNice article PinmvpEspen Harlinn27-Nov-12 3:49 
GeneralRe: Nice article PinmemberKenBeckett28-Nov-12 8:15 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140415.2 | Last Updated 1 Dec 2012
Article Copyright 2012 by KenBeckett
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid