CodeDOM Classes for Solution and Project Files (Part 5)





5.00/5 (11 votes)
CodeDOM objects for VS Solution and Project files.
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 Project
s,
so I gave it a ChildList
of Project
s. 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 CodeUnit
s,
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.