Tuesday, March 29, 2011

Extending a F# Project Template with a Template Wizard

A little background:

One of the issues that I experienced early on when building the various F# templates that are available on Visual Studio Gallery, was that of invalid references between some of the projects that made up the multi-project templates. This is because the act of creating a new project from an installed template always causes new project GUIDs to be created. Since the project references are tied to the GUIDs of the projects that existed when the multi-project template was developed, the references were being marked as suspect. This didn't really cause errors; however, it did cause an unsightly warning icon:

It was quickly discovered that the best approach for accomplishing the association of projects within the multi-project template was to create a custom template wizard. This post will show how to create one of these custom template wizards using an example similar to the code which was developed for the F# and C# ASP.NET MVC 3 template.

Follow these simple steps:

1. Create a new F# project and add a class that implement IWizard and uses the standard DTE commands to add each project as a reference. An example is shown below:

namespace FSharpMVC3TemplateWizard

open System
open System.Collections.Generic
open System.Collections
open EnvDTE
open Microsoft.VisualStudio.TemplateWizard
open VSLangProj

[<AutoOpen>]
module TemplateWizardMod =
    let AddProjectReference (target:Option<Project>) (projToReference:Option<Project>) =
        if ((Option.isSome target) && (Option.isSome projToReference)) then
            let vsControllerProject = target.Value.Object :?> VSProject
            let existingProjectReference = 
                vsControllerProject.References.Find(projToReference.Value.Name) 
            if (existingProjectReference <> null) then existingProjectReference.Remove() 
            vsControllerProject.References.AddProject(projToReference.Value) |> ignore

    let BuildProjectMap (projectEnumerator:IEnumerator) =
        let rec buildProjects (projectMap:Map<string,Project>) = 
            match projectEnumerator.MoveNext() with
            | true -> let project = projectEnumerator.Current :?> Project
                      projectMap 
                      |> Map.add project.Name project
                      |> buildProjects 
            | _ -> projectMap
        buildProjects Map.empty

type TemplateWizard() =
    let projectRefs = [("Controllers", "Models"); ("Web", "Core")
                       ("Web", "Models"); ("Web", "Controllers")]
    [<DefaultValue>] val mutable Dte : DTE
    interface IWizard with
        member x.RunStarted (automationObject:Object, 
                             replacementsDictionary:Dictionary<string,string>, 
                             runKind:WizardRunKind, customParams:Object[]) =
            x.Dte <- automationObject :?> DTE
        member x.ProjectFinishedGenerating (project:Project) =
            try
                let projects = BuildProjectMap (x.Dte.Solution.Projects.GetEnumerator())
                projectRefs 
                |> Seq.iter (fun (target,source) ->
                             do AddProjectReference (projects.TryFind target) 
                                                    (projects.TryFind source))
            with 
            | _ -> "Do Nothing" |> ignore
        member x.ProjectItemFinishedGenerating projectItem = "Do Nothing" |> ignore
        member x.ShouldAddProjectItem filePath = true
        member x.BeforeOpeningFile projectItem = "Do Nothing" |> ignore
        member x.RunFinished() = "Do Nothing" |> ignore

2. Add references to EnvDTE.dll, Microsoft.VisualStudio.TemplateWizardInterface.dll, and VSLangProj.dll.

3. This assembly needs to be signed with a strong name, so add the "keyfile" switch to the "Other flags" field on the build properties for your F# template project and specify the location of your strongly named key file as shown below:

4. In the source.extension.vsixmanifest file of the VSIX project, add a new Content reference to the TemplateWizard project with a content type of Template Wizard as show here:


4. Add a WizardExtension section to your project collection .vstemplate file (named FSMVC3.vstemplate in the F# and C# ASP.NET MVC 3 Template)
 <WizardExtension>
   <Assembly>FSharpMVC3TemplateWizard, Version=0.0.0.0, Culture=neutral, PublicKeyToken=ba79043a32149735</Assembly>
   <FullClassName>FSharpMVC3TemplateWizard.TemplateWizard</FullClassName>
 </WizardExtension> 

5. Repackage your multi-project template as described in this post.

6. Build it, test it, and call it a day.

Where to go from here:

This simple edition to our template has solved the little problem that was previously experienced; however, we have only seen the tip of the iceberg when it comes to the power that is available to us through these template wizards. In a future post, I plan to explore these possibilities in greater details.

No comments:

Post a Comment