Visual Studio Extension – Part 2

Visual Studio Extension – Part 2

Last time I went over a Visual Studio template item extension that I created to generate simple CRUD repository classes, for my projects.

This time I’ll be covering the internals of the extension itself. I’ll assume that anyone reading this article knows at least something about how Visual Studio extensions work. If that’s not true, for you, then I suggest you stop here and do some basic research on the subject before coming back to this article.

HERE is a decent place to get started. Also HERE is a great page from Mads Kristensen, who is the Mad King of Visual Studio extensions.



The CG.Ruby source code is available HERE

The compiled (but unsigned) VSIX project is available HERE

Let’s start with what you’ll need to do, in order to even open a project of this type, in Visual Studio.

Start by open the Visual Studio Installer and scrolling all the way to the bottom of the Workloads list until you see the “Visual Studio extensions development” workload. Make sure that is checked. Check it, if it’s not.

Once you have Visual Studio configured to work with extension projects, let’s start by opening the CG.Ruby solution. The overall project structure should look like this:

As one might expect, the VSIX project generates a VSIX file. VSIX is the mechanism of choice for adding extensions to Visual Studio. A VSIX file is actually just a ZIP file with everything needed to make an extension work. In our case, our VSIX file contains our CG.Ruby.UI project and our ItemTemplate1 project – plus any required support files, of course.

A VSIX file can be run just like an executable. Here is what my VSIX looks like when I run it:

So, if you run my VSIX, and press the Install button, it loads my CG.Ruby Visual Studio extension into Visual Studio for your use and enjoyment. Don’t be put off by the lack of a digital signature. That just means I’m too lazy to get a signing certificate and figure out the command line syntax required to properly sign my VSIX.

The ItemTemplate1 project contains the C# templates for generating classes in Visual Studio. Here is what that project looks like:

There really isn’t much to say about this part of the solution. The irony is, the actual template project is the least interesting part of a Visual Studio template project. The C# projects we see here are actually template files with a .cs extension. Inside, the files contain an odd mix of C# and what can only be described as gobbledegook. Here is a sample:

namespace $newnamespace$
{
	/// <summary>
	/// This class is a default implementation of the <see cref="$newifacename$"/>
	/// interface.
	/// </summary>
	internal class $newclassname$ : $newifacename$
	{
		// Methods redacted, for clarity.
	}
}

See those words with $ symbols before and after? Those are replacements tokens. At runtime, after the user has run our extension, Visual Studio takes these templates and replaces the words inside the $ symbols with legal C# tokens. The end result, when saved to disk, is a C# file that actually compiles and works. All the templates in the ItemTemplate1 project look like this.

The CG.Ruby.UI project is where I put everything that pops up on the screen, when my extension is run. By default, a Visual Studio extension doesn’t actually show anything. But, I wanted mine to collect some information, at runtime, and that required me to write a simple WPF wizard form.

The project itself looks like this:

We can see it has an Assets folder for my wonderful logo image. It has an Infrastructure folder for some WPF specific code. Is has a ViewModel folder for the form’s view-model. And finally, it has the WizardWindow form itself, which is the thing we see on the screen when we run the extension in Visual Studio.

I’ll cover the CG.Ruby.UI project in more detail in the next article. For now, let’s focus on the CG.Ruby.VSIXProject. Here is what that project looks like:

Let’s start with the VSWizard class, which looks like this:

#region Local using statements
using CG.Ruby.UI;
using CG.Ruby.UI.ViewModels;
using EnvDTE;
using Microsoft.Internal.VisualStudio.PlatformUI;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.TemplateWizard;
using System;
using System.Collections.Generic;
using System.IO;
using System.Windows;
using System.Windows.Interop;
#endregion

namespace CG.Ruby.VSIXProject
{
    /// <summary>
    /// This class is a default implementation of the <see cref="IWizard"/>
    /// interface.
    /// </summary>
    public class VSWizard : IWizard
    {
        // *******************************************************************
        // Fields.
        // *******************************************************************

        #region Fields

        /// <summary>
        /// This field indicats whether or not the wizard was cancelled.
        /// </summary>
        private bool _weCancelled;

        /// <summary>
        /// This field indicates what type of repository to generate.
        /// </summary>
        private RepositoryTypes _repositoryType;

        /// <summary>
        /// This field indicates whether the repository type exists in the solution.
        /// </summary>
        private bool _repositoryTypeFound;

        /// <summary>
        /// This field indicates whether the repository will use a factory for data-contexts.
        /// </summary>
        private bool _useDataContext;

        #endregion

        // *******************************************************************
        // Public methods.
        // *******************************************************************

        #region Public methods

        /// <inheritdoc/>
        public virtual void RunStarted(
            object automationObject, 
            Dictionary<string, string> replacementsDictionary, 
            WizardRunKind runKind, 
            object[] customParams
            )
        {
            try
            {
                // Make the compiler happy.
                ThreadHelper.ThrowIfNotOnUIThread();

                // Recover the VS object.
                var dte = automationObject as DTE;

                // Walk the entire Visual Studio solution tree looking for
                // potential model classes.
                var table = new Dictionary<string, Dictionary<string, List<string>>>();
                dte.FindClassesInSolution(table);

                // Look for the RepositoryException type in the solution.
                _repositoryTypeFound = table.RepositoryExceptionFound();

                // Create the wizard UI.
                var form = new WizardWindow();

                // Initialize the wizard form's view-model.
                var viewModel = (form.DataContext as WizardViewModel);
                if (null != viewModel)
                {
                    // Select the default(s) for the wizard UI.
                    viewModel.Table = table;
                    
                    viewModel.NameSpace = replacementsDictionary["$rootnamespace$"];
                    viewModel.ClassName = replacementsDictionary["$safeitemname$"];
                    viewModel.IFaceName = "I" + replacementsDictionary["$safeitemname$"];

                    viewModel.UseDataContextFactory = true;

                    viewModel.AddAnyAsync = true;
                    viewModel.AddCreateAsync = true;
                    viewModel.AddDeleteAsync = true;
                    viewModel.AddUpdateAsync = true;
                    viewModel.AddFindAsync = true;
                    viewModel.AddFindSingleAsync = true;
                }

                // Show the wizard UI to the caller.
                var result = form.ShowDialog();

                // Did the caller cancel?
                if (!result.HasValue || !result.Value)
                {
                    // Remember that we cancelled.
                    _weCancelled = true;
                    return; // Nothing left to do!
                }

                // Update the wizard state based on the UI.
                if (null != viewModel)
                {
                    _repositoryType = (RepositoryTypes)Enum.Parse(
                        typeof(RepositoryTypes),
                        viewModel.SelectedRepoType
                        );

                    _useDataContext = viewModel.UseDataContextFactory;

                    replacementsDictionary["$newnamespace$"] =
                        viewModel.NameSpace.TrimEnd('.').TrimStart('.');
                    replacementsDictionary["$newclassname$"] =
                        viewModel.ClassName.TrimEnd('.').TrimStart('.');
                    replacementsDictionary["$newifacename$"] = 
                        viewModel.IFaceName.TrimEnd('.').TrimStart('.');

                    replacementsDictionary["$addanyasync$"] =
                        viewModel.AddAnyAsync ? "true" : "false";
                    replacementsDictionary["$addcreateasync$"] =
                        viewModel.AddCreateAsync ? "true" : "false";
                    replacementsDictionary["$adddeleteasync$"] =
                        viewModel.AddDeleteAsync ? "true" : "false";
                    replacementsDictionary["$addfindasync$"] =
                        viewModel.AddFindAsync ? "true" : "false";
                    replacementsDictionary["$addfindsingleasync$"] =
                        viewModel.AddFindSingleAsync ? "true" : "false";
                    replacementsDictionary["$addupdateasync$"] =
                        viewModel.AddUpdateAsync ? "true" : "false";

                    replacementsDictionary["$modelproject$"] =
                        viewModel.SelectedModelProject;
                    replacementsDictionary["$modelnamespace$"] =
                        viewModel.SelectedModelNamespace;
                    replacementsDictionary["$modelclass$"] =
                        viewModel.SelectedModelClass;

                    if (viewModel.EFCoreTypeChosen)
                    {
                        replacementsDictionary["$efcorecontextproject$"] =
                            viewModel.SelectedEfCoreContextProject;
                        replacementsDictionary["$efcorecontextnamespace$"] =
                            viewModel.SelectedEfCoreContextNamespace;
                        replacementsDictionary["$efcorecontextclass$"] =
                            viewModel.SelectedEfCoreContextClass;

                        replacementsDictionary["$usedatacontextfactory$"] =
                            viewModel.UseDataContextFactory ? "true" : "false";
                    }
                }
            }
            catch (Exception ex)
            {
                // Don't add items if we crashed.
                _weCancelled = true;

                // Show the world what happened.
                MessageBox.Show(ex.ToString());
            }
        }

        // *******************************************************************

        /// <inheritdoc/>
        public virtual bool ShouldAddProjectItem(
            string filePath
            )
        {
            // Don't add anything if we were cancelled.
            if (_weCancelled)
            {
                return false;
            }

            // Strip out just the file name.
            var fileName = Path.GetFileNameWithoutExtension(filePath);

            // Always allow these files.
            if ("IRepository" == fileName)
            {
                return true;
            }

            // Sometimes allow these files.
            if ("RepositoryException" == fileName)
            {
                // Add it only if it doesn't already exist.
                return !_repositoryTypeFound;
            }

            // Only allow these files if the user wants them.
            var retValue = true;
            switch(_repositoryType)
            {
                case RepositoryTypes.EfCore:
                    retValue = _useDataContext 
                        ? "EfCoreRepository" == fileName
                        : "EfCoreRepository2" == fileName;
                    break;
                case RepositoryTypes.Default:
                    retValue = "DefaultRepository" == fileName;
                    break;
            }

            // Return the results.
            return retValue;
        }

        // *******************************************************************

        /// <inheritdoc/>
        public virtual void ProjectItemFinishedGenerating(
            ProjectItem projectItem
            )
        {
            // Deliberately empty.
        }

        // *******************************************************************

        /// <inheritdoc/>
        public virtual void ProjectFinishedGenerating(
            Project project
            )
        {
            // Deliberately empty.
        }

        // *******************************************************************

        /// <inheritdoc/>
        public virtual void BeforeOpeningFile(
            ProjectItem projectItem
            )
        {
            // Deliberately empty.
        }
                
        // *******************************************************************

        /// <inheritdoc/>
        public virtual void RunFinished()
        {
            // Deliberately empty.
        }

        #endregion
    }
}

This class, VSWizard, is the interface that Visual Studio uses to talk to our extension. It does that through the IWizard interface, which we implement in this class. Most of the methods in IWizard aren’t used, thankfully, but two methods are, so we’ll cover those now.

The first is the RunStarted method. We can see that this method accepts a few arguments from Visual Studio. The two we really care about are the automationObject parameter, and the replacementsDictionary parameter.

The automationObject parameter is actually something called a DTE object. DTE is a dispatch interface, defined by Visual Studio, that exposes the inner workings of Visual Studio to our extension. We’ll be using that object soon. By the way, when I say “dispatch interface”, I’m talking old school OLE Automation. Hey, it’s the tool we have to work with, if we want to talk to Visual Studio …

The replacementsDictionary parameter is just a dictionary that we’ll use to add, or modify replacement tokens in our template library (The ItemTemplate1 project we first covered). Visual Studio will use the values in that dictionary to generate classes from our template files.

The RunStarted method itself starts by calling ThreadHelper.ThrowIfNotOnUIThread, which is a helper method that ensures we are running this code on the UI thread. Why is that important? Well, two reasons, really, first because the DTE object is a Component Object Model (COM) object, and second because we’re going to be popping up windows and such on the UI. If we messed up and did any of that on a background thread, Windows would not be happy with us.

After that we cast the automationObject parameter to a DTE reference. Then we create a dictionary, called table, and populate it using an extension I wrote called FindClassesInSolution. I’ll cover the extension methods shortly, for now, just know that this method uses the DTE object to walk through the Visual Studio solution looking for classes, interfaces, and such. We’ll need that information to populate the dropdown lists, in our UI.

Once the table is filled with namespaces and class names, we then search for an existing RepositoryException type in the caller’s solution. That way, we don’t accidentally break their project by adding another RepositoryException class to the project. We need the RepositoryException type because that’s the type we throw in our repository methods. If the type doesn’t exist in the callers project, we add it. If it does exists, we don’t try to add it again.

Next we create an instance of our WizardWindow class, which is the WPF wizard form, from our CG.Ruby.UI project. Once we have the form reference, we then get a reference to the associated view-model. Assuming that all happens without an error, we then setup some reasonable defaults in our table Dictionary. Those defaults are then reflected in the opening state of our Visual Studio extension.

After we’ve setup the form’s view-model, we show the form by calling the ShowDialog method. If the caller cancels the form, we set a flag so we’ll know the wizard was cancelled, and we exit. Nothing left to do at that point.

On the other hand, if the caller didn’t cancel the form we use the state of the viewModel view-model to update the replacementsDictionary dictionary.

At that point, our involvement in the extension is done and Visual Studio takes over to generate the actual C# files. Luckily, we don’t have to generate those files ourselves.

The next method we’ll cover. in the VSWizard class, is the ShouldAddProjectItem method. That method accepts a filePath string, from Visual Studio. This method is called by Visual Studio after the RunStarted method is finished, to decide which files in our ItemTemplate1 project should be added to the caller’s project.

The method starts by checking the _weCancelled flag. If the caller cancelled our popup Wizard form, this flag is true. Otherwise, it’s value is false. If it is true, we always return false – to tell Visual Studio not to add any files to the caller’s project.

Assuming the caller didn’t cancel, we then move on to get the filename from the filePath path. That path, is one of the files in our ItemTemplate1 project. For our part, we only care about the file name for this method.

The IRepository file (from ItemTemplate1) is always generated, no matter what, so we always return true for that file.

The RepositoryException file is only added if it wasn’t already a part of the caller’s project. So, we return the value of the _repositoryTypeFound flag.

For the other three files, we only want to return true for whatever repository type has been selected by the caller. So, for instance, if the caller chooses the ‘Default’ repository type, we return true for the “DefaultRepository” file. Otherwise, we return true for the “EfCoreRepository” or “EfCoreRepository2” file, depending on the state of the “Use Data-Context Factory” checkbox.

And that’s about it for the Visual Studio extension itself. By default, Visual Studio extensions are not typically very complicated. What get’s complicated is when you want to do anything out of the ordinary in an extension. For instance, we wanted to walk through the caller’s solution, looking for namespaces and classes, and such. That’s a simple requirement but not a simple solution. In order to keep that code out of our VSWizard class, I put all that other code in extension classes. Let’s go look at those now.

Let’s first cover the code we used to discover if the RepositoryException type was already part of the caller’s project. That method is called RepositoryExceptionFound and it looks like this:

#region Local using statements
using System;
#endregion

namespace System.Collections.Generic
{ 
    /// <summary>
    /// This class contains extension methods related to the <see cref="Dictionary{Key, Value}"/>
    /// type.
    /// </summary>
    public static partial class DictionaryExtensions
    {
        // *******************************************************************
        // Public methods.
        // *******************************************************************

        #region Public methods

        /// <summary>
        /// This method walks the dictionary looking for the RepositoryException
        /// class.
        /// </summary>
        /// <param name="table">The table to search.</param>
        /// <returns>True if the class exists in the table; false otherwise.</returns>
        public static bool RepositoryExceptionFound(
            this Dictionary<string, Dictionary<string, List<string>>> table
            )
        {
            // Loop through the project keys.
            foreach (var projKey in table.Keys)
            {
                // Loop through the namespace keys.
                foreach (var nsKey in table[projKey].Keys)
                {
                    // Loop through the classes.
                    foreach (var className in table[projKey][nsKey])
                    {
                        // Did we find a match?
                        if ("RepositoryException" == className)
                        {
                            return true;
                        }
                    }
                }
            }

            // Not found.
            return false;
        }

        #endregion
    }
}

We can see that this code uses the Dictionary we previously populated from the caller’s solution. That dictionary is passed into this method and it is then searched by project, then by namespace, then finally by class name(s). If we find a class anywhere in the solution named RepositoryException, we return true. Otherwise, we return false.

I put the code that walks through the caller’s Visual Studio solution into another extension method called FindClassesInSolution. Let’s look at that code now:

#region Local using statements
using Microsoft.VisualStudio.Shell;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
#endregion

namespace EnvDTE
{
    /// <summary>
    /// This class contains extension methods related to the <see cref="DTE"/>
    /// type.
    /// </summary>
    public static partial class DTEExtensions
    {
        // *******************************************************************
        // Public methods.
        // *******************************************************************

        #region Public methods

        /// <summary>
        /// This method performs a recursive walkthrough of the current Visual
        /// Studio solution, building up a list of project names, namespaces,
        /// and class names.
        /// </summary>
        /// <param name="dte">The Visual Studio automation object to use for
        /// the operation.</param>
        /// <param name="table">The results of the operation, a table of project
        /// names, that contains a table of namesspaces, that contains a list 
        /// of class names.</param>
        /// <returns>the value of the <paramref name="dte"/> parameter, for 
        /// chaining method calls together, Fluent style.</returns>
        /// <exception cref="COMException">This exception is thrown whenever
        /// the Visual Studio automation object decides to barf all over the 
        /// place, for whatever reason.</exception>
        public static DTE FindClassesInSolution(
            this DTE dte,
            Dictionary<string, Dictionary<string, List<string>>> table
            )
        {
            // Make the compiler happy.
            ThreadHelper.ThrowIfNotOnUIThread();

            // Loop through the projects in the solution.
            var projCount = dte.Solution.Projects.Count;
            for (var x = 1; x <= projCount; x++)
            {
                // Get the next project in the solution.
                var project = dte.Solution.Projects.Item(x);

                // Add a record to the table for this project.
                table[project.Name] = new Dictionary<string, List<string>>();

                // Walk down through the tree.
                dte.FindClassesInProject(project, table);
            }

            // Return the DTE.
            return dte;
        }

        // *******************************************************************

        /// <summary>
        /// This method performs a recursive walkthrough of the current Visual
        /// Studio project, building up a list of namespaces and class names.
        /// </summary>
        /// <param name="dte">The Visual Studio automation object to use for
        /// the operation.</param>
        /// <param name="project">The project to recursively walkthrough.</param>
        /// <param name="table">The results of the operation, a table of project
        /// names, that contains a table of namesspaces, that contains a list 
        /// of class names.</param>
        /// <returns>the value of the <paramref name="dte"/> parameter, for 
        /// chaining method calls together, Fluent style.</returns>
        /// <exception cref="COMException">This exception is thrown whenever
        /// the Visual Studio automation object decides to barf all over the 
        /// place, for whatever reason.</exception>
        public static DTE FindClassesInProject(
            this DTE dte, 
            Project project,
            Dictionary<string, Dictionary<string, List<string>>> table
            )
        {
            // Make the compiler happy.
            ThreadHelper.ThrowIfNotOnUIThread();

            // Loop through any sub-items in this project.
            var itemCount = project.ProjectItems.Count;
            for (var x = 1; x <= itemCount; x++)
            {
                // Walk down through the tree.
                var projItem = project.ProjectItems.Item(x);
                dte.FindClassesInProjectItem(projItem, table);
            }

            // Return the DTE.
            return dte;
        }

        // *******************************************************************

        /// <summary>
        /// This method performs a recursive walkthrough of the current Visual
        /// Studio project item, building up a list of class names.
        /// </summary>
        /// <param name="dte">The Visual Studio automation object to use for
        /// the operation.</param>
        /// <param name="projItem">The project item to recursively walkthrough.</param>
        /// <param name="table">The results of the operation, a table of project
        /// names, that contains a table of namesspaces, that contains a list 
        /// of class names.</param>
        /// <returns>the value of the <paramref name="dte"/> parameter, for 
        /// chaining method calls together, Fluent style.</returns>
        /// <exception cref="COMException">This exception is thrown whenever
        /// the Visual Studio automation object decides to barf all over the 
        /// place, for whatever reason.</exception>
        public static DTE FindClassesInProjectItem(
            this DTE dte, 
            ProjectItem projItem,
            Dictionary<string, Dictionary<string, List<string>>> table
            )
        {
            // Make the compiler happy.
            ThreadHelper.ThrowIfNotOnUIThread();

            // Loop through any sub-items in this project item.
            var subItemCount = projItem.ProjectItems.Count;
            for (var x = 1; x <= subItemCount; x++)
            {
                // Walk down through the tree.
                var subItem = projItem.ProjectItems.Item(x);
                dte.FindClassesInProjectItem(subItem, table);
            }

            // Look for a namespace in the code-elements, for this project item.
            var codeNamespace = projItem.FileCodeModel?.CodeElements?.Cast<CodeElement>()
                    .OfType<CodeNamespace>().FirstOrDefault();

            // Did we find one?
            if (null != codeNamespace)
            {
                // Add a namespace to the table, for this project.
                //table[projItem.ContainingProject.Name][projItem.Name] = new List<string>();

                // Look for a code-type in the members, for this namespace.
                var codeType = codeNamespace.Members?.Cast<CodeElement>()
                    .OfType<CodeType>().FirstOrDefault();

                // Did we find one?
                if (null != codeType)
                {
                    // Get property(s) we care about.
                    var className = codeType.Name;
                    var classNamespace = codeType.Namespace.Name;

                    // Get the name of the parent project.
                    var projName = projItem.ContainingProject.Name;

                    // Get the table for the parent project.
                    var projTable = table[projName];

                    // Should we add the namespace?
                    if (!projTable.ContainsKey(classNamespace))
                    {
                        // Add a blank list for the namespace.
                        projTable[classNamespace] = new List<string>();
                    }

                    // Add the class to the list, for this namespace.
                    projTable[classNamespace].Add(className);
                }
            }

            // Return the DTE.
            return dte;
        }

        #endregion
    }
}

There’s a bunch of recursion going on here. That’s because a Visual Studio solution is actually a tree of objects that are themselves, potentially, trees of objects. Since I have no control over the state of the caller’s solution tree, I have no choice but to visit each node in the tree. The most efficient way to traverse any tree is with recursion, so here we go!

From the code listing above, we see that the FindClassesInSolution method begins by calling the now familiar ThreadHelper.ThrowIfNotOnUIThread method. I’ll stop pointing this call out, since I have to do this anytime I interact with the DTE object.

Next, the method loops through all the objects in the DTE.Solution.Projects collection. For each item, we then add an entry to the table Dictionary, and call the FindClassesInProject to walk down each project tree.

The FindClassesInProject method iterates over the item in the project’s ProjItems collection. For each project item found, it then calls the FindClassesInProjectItem method for that item.

The FindClassesInProjectItem method has a bit more going on that the other methods in this class. It starts by iterating through the project items in the current project item. For each item found, it then calls the FindClassesInProjectItem again, to walk down that tree.

Once I’ve walked down each tree I can then process the nodes. I start by looking for a CodeNamespace object in the item’s collection of CodeElement objects. Assuming I find one, I then get the associated class name and namespace. Once I have that information, I can then add an entry to the table Dictionary for this C# class (or interface, or enum, or whatever it actually is).

The end result, after all that processing, is that I’ve found all the classes in the caller’s solution, shaped by project and namespace. That way, we can use the dropdowns in the UI to allow the caller to drill down to the appropriate data-context, or model.

There’s more to this VSIX project, to be sure. Like the source.extensions.vsixmanifest file, which is used to decide what gets added to the generated VSIX file. Opening that file, in Visual Studio, looks like this:

I won’t go through all the fields and tabs. Suffice to say, this is how Visual Studio determines what should be in the final VSIX file.

I’ll also point out that the ItemsTemplate1 project also has an ItemTemplate1.vstemplate file, which looks like this:

<?xml version="1.0" encoding="utf-8"?>
<VSTemplate Version="3.0.0" Type="Item" xmlns="http://schemas.microsoft.com/developer/vstemplate/2005" xmlns:sdk="http://schemas.microsoft.com/developer/vstemplate-sdkextension/2010">
  <TemplateData>
    <Name>CodeGator Repository</Name>
    <Description>A C# generated CRUD repository.</Description>
    <Icon>ItemTemplate1.ico</Icon>
    <TemplateID>9ce87ff9-d10e-4f1c-b379-ff7f93bae804</TemplateID>
    <ProjectType>CSharp</ProjectType>
	<ProjectSubType>Data</ProjectSubType>
    <RequiredFrameworkVersion>2.0</RequiredFrameworkVersion>
    <NumberOfParentCategoriesToRollUp>1</NumberOfParentCategoriesToRollUp>
	<DefaultName>Repository.cs</DefaultName>
  </TemplateData>
  <TemplateContent>
    <References>
      <Reference>
        <Assembly>System</Assembly>
      </Reference>
    </References>
	  <ProjectItem ReplaceParameters="true" TargetFileName="$newclassname$.cs">DefaultRepository.cs</ProjectItem>
	  <ProjectItem ReplaceParameters="true" TargetFileName="$newclassname$.cs">EfCoreRepository.cs</ProjectItem>
	  <ProjectItem ReplaceParameters="true" TargetFileName="$newclassname$.cs">EfCoreRepository2.cs</ProjectItem>
	  <ProjectItem ReplaceParameters="true" TargetFileName="$newifacename$.cs">IRepository.cs</ProjectItem>      
	  <ProjectItem ReplaceParameters="true" TargetFileName="RepositoryException.cs">RepositoryException.cs</ProjectItem>
  </TemplateContent>
  <WizardExtension>
    <Assembly>CG.Ruby.VSIXProject, Version=1.0.0.0, Culture=neutral, PublicKeyToken=ae7b24e69016d0dd</Assembly>
	<FullClassName>CG.Ruby.VSIXProject.VSWizard</FullClassName>
  </WizardExtension>
</VSTemplate>

I’m pointing this file out because this is how Visual Studio knows how to show our custom WPF wizard form, at runtime. Not because of anything in the CG.Ruby.VSIXProject project, which is what any reasonable person might believe. No, its here, in this XML file. Look at the WizardExtension section, at the bottom of the file. See the two sub sections there? Assembly tells Visual Studio where to find the assembly that contains the UI for this extension. FullClassName tells Visual Studio what the name of our “wizard” is. See the name? VSWizard. Look familiar? Yeah, this is how Visual Studio knows about that class.

Next time I’ll cover the WPF wizard and the other code in the CG.Ruby.UI project.

Photo by Patrick Tomasso on Unsplash