Friday, March 23, 2007

Building a Visual Studio Custom Project Type

Recently I've been looking at Visual Studio Extensibility and specifically how to create my own custom project types. There are three fundamental ways of extending VS:

  • Macros. This is the easiest way to automate VS and can be as simple as recording a few key strokes or as complex as coding (in VB ugh!) entire extensibility applications against the VS automation model (known as EnvDTE).
  • AddIns. These allow deeper integration with VS and allow you to code in C# and ship your automation code as a compiled assembly.
  • Packages. This is the deepest level of integration and is how Microsoft creates features in VS, including all the languages.

Now there's a common misconception among .net developers (myself included, before I started looking into this) that Visual Studio is itself writen in .net, it's not, it's a evolution of the earlier C++ IDE and is fundamentally a COM framework with plugable COM components called 'Packages'. Apparently many of the VS packages are now implemented in .net, but they still have to deal with the VS COM framework via interop. The only way to do what I want and create a new project type is to use VS Packages. Unfortunately this means dealing with VS in the raw and coding against the gnarly old VS COM framework. Microsoft provide interop classes and a managed framework, the Managed Package Framework (MPF) to help with this, but it's not very well documented and hardly a walk in the park. OK, so after much fustration and effort I've managed to create the most basic non-functional custom project type. It doesn't do anything, but you can select it in the New Project dialogue box and create an instance of it that you can see in solution explorer. The steps below show how I did it. I'm not sure if it's the right way, but it works. First you'll need to download the Visual Studio SDK, currently at version 4. Once that's installed, you can use the new package wizard to create a basic package: 1. Open Visual Studio 2. Select File->New->Project, the new project dialog appears 3. Select Other Project Types->Extensibility->Visual Studio Integration Package 4. Give your package a name 5. Click OK, The Visual Studio Integration Package Wizard Appears, click Next 6. Select C# and Generate a new key file to sign the assembly, click Next 7. Enter some information about your package, this will appear in the VS about dialog, click Next 8. Leave all the following checkboxes blank, click Finish. The wizard does it's stuff and now you've got a working Visual Studio package (not project that comes later!), you can build it and run it by hitting F5. A new instance of VS appears and if you click on Help->About Microsoft Visual Studio, you can see that your package has been registered and appears under 'installed products' with the information you entered in the Wizard. The instance of VS that runs when you hit F5 uses what's known as the 'Experimental Hive' which is simply a copy of the VS registry settings that's created for developing and debugging extensions. You can reset it at any time by using the VsRegEx.exe tool. The part of the MPF that deals with creating new project types isn't provided as part of the MPF assemblies, but as source code, called 'ProjectBase', that you are supposed to include in your project. On my machine, the VS SDK installed it here:

C:\Program Files\Visual Studio 2005 SDK\2007.02\VisualStudioIntegration\Common\Source\CSharp\Project

To include it you have to edit your package's csproj file to include the following nodes:

<!-- This imports the files which makes up the project base classes -->
<PropertyGroup>
<ProjectBasePath>C:\Program Files\Visual Studio 2005 SDK\2007.02\VisualStudioIntegration\Common\Source\CSharp\Project</ProjectBasePath>
</PropertyGroup>
<Import Project="$(ProjectBasePath)\ProjectBase.Files" />

Once you reload the project you'll see that a folder called project base has been added. If you now try and build the project, you'll get a whole load of errors. To fix them, add the following references: EnvDTE Microsoft.VisualStudio.Designer.Interfaces and add the following lines to your AssemblyInfo.cs file:

using System;
...
[assembly: CLSCompliant(false)]

Now you should be able to build the project with the ProjectBase files included. In order for Visual Studio to create an instance of your project you have to create a 'ProjectFactory', you do this by extending the ProjectFactory class in ProjectBase (Microsoft.VisualStudio.Package). Here's my ProjectFactory. Note it must be given a fixed Guid to allow the class to be identified by COM:

using System;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio.Package;
using Microsoft.VisualStudio.Shell;
using IOleServiceProvider = Microsoft.VisualStudio.OLE.Interop.IServiceProvider;

namespace Company.VsPackage1
{
    [Guid("AC959CD1-5A2C-4306-BE72-259804B01F08")]
    public class MyProjectFactory : ProjectFactory
    {
        public MyProjectFactory(Package package)
            : base(package)
        {
        }

        protected override ProjectNode CreateProject()
        {
            // TODO: create project here
            return null;
        }
    }
}

MyProjectFactory now has to be registered by the package. The MPF uses attributes on the package class to create the required registry entries so you need to add the following attribute to the package class created by the wizard. In my case it's called 'VsPackage1' in the file 'VsPkg.cs'. Here you should define the name of your project type and your project file's file extension:

[ProvideProjectFactory(typeof(MyProjectFactory),
"My Project",
"MyProject Files (*.myproj);*.myproj",
"myproj",
"myproj",
@"..\..\Templates\Projects")]

You also have to get the Package to register the project factory type when it initialises by calling RegisterProjectFactory and passing in a new instance of MyProjectFactory that takes a pointer to the package in its constructor:

protected override void Initialize()
{
    Trace.WriteLine (string.Format(CultureInfo.CurrentCulture, "Entering Initialize() of: {0}", this.ToString()));
    this.RegisterProjectFactory(new MyProjectFactory(this));
    base.Initialize();

}

Also at this stage we should change our Pakage to inherit from ProjectPackage (in Microsoft.VisualStudio.Package) rather than Package:

public sealed class VsPackage1 : ProjectPackage

If you hit F5 now to run your package you see that when you go to File->New->Project you'll see 'My Project' as one of the project types available. If you click on 'My Project' there are no projects to select. All the available project types are defined as templates, so the next step is to define a template file. In the ProvideProjectFactory attribute above the last parameter is the path to where VS should look to find your project templates, so create a new folder under your project called 'Templates' and under that another folder called 'Projects'. In that folder create a file called 'My Project.myproj'. Note that the extension should match the extension defined above in the ProvideProjectFactory attribute. Paste this minimal project template into the file:

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <Name>"My Project"</Name>
    <SchemaVersion>2.0</SchemaVersion>
    <ProjectGuid>{AC959CD1-5A2C-4306-BE72-259804B01F08}</ProjectGuid>
  </PropertyGroup>
</Project>

Note that the ProjectGuid property should be the same as the MyProjectFactory class' Guid. Now when you hit F5 and then File->New->Project and navigate to 'My Project' you will see the 'My Project' template. If you select it and hit 'OK', you'll get a nasty error and VS will crash out. The next task is do define the project class and get the project factory to create it. Create a class called 'MyProject' and paste in the following code:

using System;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio.Package;

namespace Company.VsPackage1
{
    [CLSCompliant(false)]
    [ComVisible(true)]
    [Guid("7B61EA22-333F-4b4e-A000-FB847CDD4A99")]
    public class MyProject : ProjectNode
    {
        public override Guid ProjectGuid
        {
            get { return typeof(MyProjectFactory).GUID; }
        }

        public override string ProjectType
        {
            get { return this.GetType().Name; }
        }
    }
}

And return a new instance of the project class from the project factory's CreateProject method. We also have to set the site of the project by providing the package's service provider (see my post on service providers for more on this):

protected override ProjectNode CreateProject()
{
    MyProject project = new MyProject();
    project.SetSite((IOleServiceProvider)
        (((IServiceProvider)this.Package).GetService(typeof(IOleServiceProvider)))
        );
    return project;
}

Now when you hit F5 and create a new 'My Project' project it works! You can see a new instance of your project in Solution Explorer under a new solution. You can right click on the solution select Add->New Project and add another instance of your project type to the solution. It took me ages (several days) to work out these few steps to creating my own custom project type. All the walkthroughs I could find only covered building simple single menu command, tool window or editor packages and none covered using the ProjectBase classes. In the end I had to work it out from the samples (mainly the NestedProject and IronPython samples) and a few hints and tips from blogs and the Visual Studio Extensibility forum. I'm expecting a similarly difficult and steep learning curve as I try to add functionality to my project. Getting to grips with buiding packages is also confused by having lots of samples and instructions based on the COM interfaces. I guess you've got to understand that architecture in order to effectively build VS solutions, but when I came to use the MPF I didn't really know how to translate what I'd learnt about the VS architecture into how to code against the MPF. I guess it's good that the VS extensibility team have taken an agile approach and are releasing stuff as they put it together without waiting for it to be fully documented. I'd much rather have the MPF without much documentation than have to code against the raw interop classes and the release cycle is pretty short so I imagine we'll see version 5 of the MPF pretty soon. Just to wrap up I have to mention Carlos Quintero's MZ-Tools site. It's a goldmine of information especially the resources page. Watching the Videos, Webcasts and Screeencasts he lists there is probably the easiest way to get up to speed with VS Packages. I've also recently read Inside Visual Studio .net by Johnson, Skibo and Young which, although it doesn't cover VS Packages is a goldmine of information on the DTE automation API and stuff to do with AddIns. The current version is also avaiable as a free PDF download when you register Visual Studio.

16 comments:

olegt said...

awesome work, man! You saved me ages figuring this out.

Paul said...

Many thanks for this. I've implemented custom project types in the past for vs.net 2003 but everything seems to have changed with 2005. I felt like I was running in circles trying to follow the docs and samples.

You've saved me many hours of head scratching and painful trawling through docs - thanks!

Anonymous said...

Hi,
Thanks for this article, it saved me alot of time. One question though. Once I have created my custom project type, 'References' is present when I create a project of my new type. How can I edit my custom project so references isnt part of the project?

elopio said...

thanks, it has been really usefull. I start looking the Iron Pyton solution but it has so many things I didn't know where to start.

Mike Hadlow said...

Thanks for the comments guys. anonymous asked 'How can I edit my custom project so references isnt part of the project?'. You have to step through the MPF code and work out the place where it adds that node and comment it out. Sorry I can't remember off the top of my head exactly where that is.

elopio said...

Hello again,

I'm trying to enable the "add existing item as link" function, on my custom project. I haven't received any answer on msdn forums, do you know what's required?

thanks.

elopio said...

Here is the answer of anonymous' question ;)

Anonymous said...

Thanks that removed the references perfectly.

Anonymous said...

Is there a way to "clone" the C++ project and modify the project type's icon (in the solution explorer). I am creating an add-in to create unit testing project and I want to clearly identify these projects so I would like a different icon. Then I created my own project using your method. So I would like to have the same behavior as a C++ project but using my own icon. Thank you.

Mike Hadlow said...

Hi anonymous. I'm not aware of a way to do what you ask (cloning the C++ project and then changing its icon). It might be worth posting your question on the VS Extensibility forum (http://forums.microsoft.com/MSDN/ShowForum.aspx?ForumID=57&SiteID=1)

Mike

goRide said...

hi,
great article.
However I still got a question.
I already have a template file with some extra logic for wizard (WizardExtension tag).
How can I integrate this to your solution ?
I only have a project file and in the template file you define what project to use...
so this is what confuse me...
Thanks,
Tidhar

Fluxtah said...

Cheers, also saved me lots of time.

Anonymous said...

Hi All,

Ithought i would share this as there is NO documentation anywhere on the web to highlight this.

the example above shows you how to create a totally new project type in Visual studio, whereas i wanted to create a project type based on a web application project.

so in the guids list i added the GUIDID for a CSharp web aplication (and not the made up one in the example), and then told my project to use a .csproj file for configuraion (renamed it from .myproj [in the example] to .csproj [the expected type for a web application]) and it worked.

the Guid for a CSharp Web Application is {349C5851-65DF-11DA-9384-00065B846F21} - Web Application Project Factory

and the guids list now reads


using System;

namespace [your namespace]
{
static class GuidList
{
public const string guidIMTProjectPkgString ="96bf4c26-d94e-43bf-a56a-f8500b52bfad";

public const string guidIMTProjectFactoryString ="349C5851-65DF-11DA-9384-00065B846F21";

public static readonly Guid guidIMTProjectFactory = new Guid(guidIMTProjectFactoryString);
};

}

so now i can create a new c# web application project type with a default template/master page, built in login control; references to other assemblies and projects, embedded resources etc... and this is available at the click of a mouse as my new project type.

Ace.

- A. L. Hopkins

L S Srinivas said...

HI, This is a great work-around for VS 2005, but is not working for VS 2008, plz suggest a way for me.

Kastor said...

Awesome ! Does this VISP will work for 2008 too ?

granadaCoder said...

SUPER SWEET! Our CruiseControl.NET build machine cannot build the VS2010 new 'database project' types of projects, so I've looking to create a "custom" project type that mimics the VS2008 database project.

Here is a link to the VS2010 database project issue on a build server (a machine that does NOT have Visual Studio installed).

http://stackoverflow.com/questions/3988879/getting-msbuild-and-cruisecontrol-net-to-build-and-deploy-vs2010-database-proje

Thanks Microsoft!!!