Tag Archives: Visual Studio Extensions

An ADR Visual Studio Tool – Part 5 – Sub Projects

Here, I started writing about my efforts to create an extension for Visual Studio that would allow a user to see all of the ADR records in their solution.

If you wish to see the code for this project, then it can be found here.

Sub Projects

In this post, I wanted to cover the concept of Sub Projects. Essentially, when you have a solution folder, scrolling through the solution projects will return top level solution folders as “Project Items”. Being folders, these don’t contain “Project Items” of their own – rather they contain Sub Projects. Let’s see how we could change our code to look at these:

        private async Task ScanProjectItems(
            ProjectItems projectItems, ProjectData projectData, string solutionDirectory)
        {
            await Microsoft.VisualStudio.Shell.ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();

            foreach (EnvDTE.ProjectItem pi in projectItems)
            {
                if (pi.IsKind(ProjectItemTypes.SOLUTION_FOLDER, 
                              ProjectItemTypes.PROJECT_FOLDER,
                              ProjectItemTypes.SOLUTION_ITEM))
                {
                    if (pi.ProjectItems != null)
                    {
                        await ScanProjectItems(pi.ProjectItems, projectData, solutionDirectory);
                        continue;
                    }
                    else if (pi.SubProject != null)
                    {
                        await ScanProjectItems(pi.SubProject.ProjectItems, projectData, solutionDirectory);
                        continue;
                    }                    
                }

                if (!_rulesAnalyser.IsProjectItemNameValid(pi.Name))
                {
                    continue;
                }

                string text = await pi.GetDocumentText(solutionDirectory);
                if (string.IsNullOrWhiteSpace(text)) continue;

                projectData.Items.Add(new Models.ProjectItem()
                {
                    Name = pi.Name,
                    Data = text
                });
            }
        }

Previously, we were only calling recursively where we had project items, but now we’re checking for SubProjects, and using the project items inside the sub project to recursively call the method.

Validation

The other issue that we have is that, for the solution items, we can’t get the path to the specific item. For normal projects, we would do it like this:

        private async static Task<string> GetFullPath(Properties properties)
        {
            try
            {
                await Microsoft.VisualStudio.Shell.ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
                return properties?.Item("FullPath")?.Value?.ToString();
            }
            catch
            {
                return string.Empty;
            }
        }

So, what we need to do is check if we can get the text; then, if it’s blank, check if we can get it another way; then, if it’s blank… etc.. It looks like this:

            string path = await GetFullPath(projectItem.Properties);
            if (string.IsNullOrWhiteSpace(path))
            {
                path = await GetFullPath(projectItem.ContainingProject?.Properties);

                if (string.IsNullOrWhiteSpace(path))
                {
                    path = Path.Combine(solutionDirectory, projectItem.Name);
                }
                else
                {
                    path = Path.Combine(path, projectItem.Name);
                }
            }

Not very pretty, I’ll grant!

References

https://stackoverflow.com/questions/38740773/how-to-get-project-inside-of-solution-folder-in-vsix-project

https://stackoverflow.com/questions/2336818/how-do-you-get-the-current-solution-directory-from-a-vspackage

An ADR Visual Studio Tool – Part 4 – Dependency Injection

Continuing with my little series on creating a visual studio extension, in this post, I’ll talk about how to add dependency injection to your project.

If you’d like to see the whole solution for this, it can be found here.

Unity

In this post on Azure Functions, I talked about using Unity as an IoC container, in a place where an IoC container might not necessarily fit; whilst this is no longer true for Azure functions, it does appear to be for extensions – I presume because they don’t expect you to have one big enough to warrant IoC; also, even with DI, testing is very difficult, because most of what you’re doing, you’re doing to Visual Studio.

Let’s start by installing Unity in our project:

Install-Package Unity

Rules Analyser

In our case, we were analysing the project and extracting files; however, we were extracting all files; as a result, a check needed to be made to extract only markdown files. Consequently, I created a RulesAnalyser class:

    public class RulesAnalyser : IRulesAnalyser
    {
        public bool IsProjectItemNameValid(string projectItemName) =>
            projectItemName.EndsWith("md");        
    }

We could (and I did initially) instantiate that directly in the ViewModel, but that feels quite dirty.

AdrPackage

The *Package file for the extension seems to be the entry point, so we can add the unity container to here:

    public sealed class AdrPackage : AsyncPackage
    {        
        public static Lazy<IUnityContainer> UnityContainer =
         new Lazy<IUnityContainer>(() =>
         {
             IUnityContainer container = InitialiseUnityContainer();
             return container;
         });

        private static IUnityContainer InitialiseUnityContainer()
        {
            UnityContainer container = new UnityContainer();
            container.RegisterType<IRulesAnalyser, RulesAnalyser>();
            container.RegisterType<ISolutionAnalyser, SolutionAnalyser>();
            return container;
        }

        . . .

View Model

The next thing we need to do is to inject our dependencies.

        public AdrControlViewModel() 
            : this(AdrPackage.UnityContainer.Value.Resolve<IRulesAnalyser>(),
                  AdrPackage.UnityContainer.Value.Resolve<ISolutionAnalyser>())
        {}

        public AdrControlViewModel(IRulesAnalyser rulesAnalyser, ISolutionAnalyser solutionAnalyser)
        {            
            _rulesAnalyser = rulesAnalyser;
            _solutionAnalyser = solutionAnalyser;

            Scan = new RelayCommandAsync<object>(ScanCommand);
        }

And that’s it, we now have a working DI model in our project.

References

https://stackoverflow.com/questions/2875429/iunitycontainer-resolvet-throws-error-claiming-it-cannot-be-used-with-type-par

https://www.pmichaels.net/2018/02/04/using-unity-azure-functions/

An ADR Visual Studio Tool – Part 3 – Listing and Reading the files

In this post, I refactored a VS Extension Plug-in, that I originally started here.

We’ll get the plug-in to list the items found in the projects, and read the contents of the files. The source code for this can be found here. I won’t be listing all the source code in this article (most of it is just simple WPF and View Model binding).

To look through all the projects and folders in the solution, we’ll need to recursively scan all the files, and then read them; let’s have a look at what such a method might look like:

        private async Task ScanProjectItems(ProjectItems projectItems, ProjectData projectData)
        {
            await Microsoft.VisualStudio.Shell.ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();

            foreach (EnvDTE.ProjectItem pi in projectItems)
            {
                if (pi.IsKind(ProjectItemTypes.SOLUTION_FOLDER, 
                              ProjectItemTypes.PROJECT_FOLDER,
                              ProjectItemTypes.SOLUTION_ITEM)
                    && pi.ProjectItems != null)
                {                    
                    await ScanProjectItems(pi.ProjectItems, projectData);
                    return;
                }

                string text = await GetDocumentText(pi);
                if (string.IsNullOrWhiteSpace(text)) continue;

                projectData.Items.Add(new Models.ProjectItem()
                {
                    Name = pi.Name,
                    Data = text
                });
            }
        }

I wanted to look specifically into two aspects of this method: IsKind() and GetDocumentText(). None of the rest of this is particularly exciting.

Kind of File

In a VS Extension, you can read ProjectItems – they represent pretty much anything in the solution, and so it’s necessary to be able to find out exactly what the type is. As you can see above, I have an extension method, which was taken from here. Let’s have a quick look at the file that defines the ProjectItemTypes:

    public static class ProjectItemTypes
    {
        public const string MISC = "{66A2671D-8FB5-11D2-AA7E-00C04F688DDE}";
        public const string SOLUTION_FOLDER = "{66A26720-8FB5-11D2-AA7E-00C04F688DDE}";
        public const string SOLUTION_ITEM = "{66A26722-8FB5-11D2-AA7E-00C04F688DDE}";                                            
        public const string PROJECT_FOLDER = "{6BB5F8EF-4483-11D3-8BCF-00C04F8EC28C}";        
    }

I’m sure there’s a better way, but after I realised what Mads was doing in the above linked project, I just stuck a breakpoint in the code, and copied the “Kind” guid from there! The IsKind method is taken from the same codebase:

        public static bool IsKind(this ProjectItem projectItem, params string[] kindGuids)
        {
            Microsoft.VisualStudio.Shell.ThreadHelper.ThrowIfNotOnUIThread();

            foreach (var guid in kindGuids)
            {
                if (projectItem.Kind.Equals(guid, StringComparison.OrdinalIgnoreCase))
                    return true;
            }

            return false;
        }

As you can see, it’s almost not worth mentioning – except that the extensions are very particular about running in the UI thread, so you’ll find ThrowIfNotOnUIThread scattered around your code like confetti!

Reading File Contents

If you need to access the file contents in an extension, one way is to convert the project item document to a TextDocument, and then use Edit Points:

        public static async Task<string> GetDocumentText(this ProjectItem projectItem)
        {
            if (projectItem == null) return string.Empty;
            await Microsoft.VisualStudio.Shell.ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();

            try
            {
                TextDocument textDocument;
                if (!projectItem.IsOpen)
                {
                    var doc = projectItem.Open(EnvDTE.Constants.vsViewKindCode);
                    textDocument = (TextDocument)doc.Document.Object("TextDocument");
                }
                else
                {
                    textDocument = (TextDocument)projectItem.Document.Object("TextDocument");
                }
                EditPoint editPoint = textDocument.StartPoint.CreateEditPoint();
                return editPoint.GetText(textDocument.EndPoint);
            }
            catch (Exception)
            {
                return string.Empty;
            }
        }

Edit Point are much more powerful that this, they allow you to change the text in a document; for example, imagine your extension needed to change every local pascal cased variable into one with an underscore (myVariable to _myVariable), you may choose to use edit points there.

References

https://www.csharpcodi.com/csharp-examples/EnvDTE.Document.Object(string)/

https://github.com/madskristensen/MarkdownEditor/

An ADR Visual Studio Tool – Part 1 – Creating a Visual Studio Extension to Scrape the Solution and List all Items, Including Solution Items and Solution Folders

A while back, a colleague of mine brought the concept of ADRs to my attention. The idea being that, when you make a decision on a project, you write it down, but you do so inside the code base, and check it into the source control system.

Even in the days when people believed writing long functional specifications was a good idea, having documentation that married up to the code it documented was a distant dream. Typically, you’d spend about a week writing a spec, and the minute you wrote the first line of code, the document was, essentially, considered dead (and only ever referred back to where the customer disputed what had been delivered).

Since I’ve never written a Visual Studio Extension, but always thought it would be a cool idea, I had an idea to start with this. My thought was that I could build something that would extract the ADRs from the main codebase. This isn’t one of those posts where I have a completed solution, and I’m just documenting it… it’s more of an ongoing journey… which may result in the conclusion that this either doesn’t make sense, isn’t feasible, or has already been done.

I’m going to upload the progress so far to this GitHub repo.

In this first post, we’ll create an extension capable of viewing the project it’s in.

Step 1 – Install the SDK

To do any extension development, you need to install the SDK – you can do this through the Visual Studio Installer:

Step 2 – Create a new (VSIX) project

VS Extensions are referred to as VSIX, because that’s the extension of the deployable product.

Step 3 – Add a new Tool Window and Test

Add a new Item (right click project -> add new item), and select the Tool Window:

There is no need to do any plumbing here – any eligible extension types in the solution will be compiled and used – try pressing F5 now. You should get a version of Visual Studio to debug:

As you can see, I’ve been here before. For the purposes of testing, I’ve set-up a convoluted project:

The reason for this will become clear shortly, for now, just launch the tool window that you created (View -> Other Windows -> Tool Window 1 (or whatever you called it):

Step 4 – Add some code to the Tool Window

For the purpose of this first stage, we’ll just analyse the project structure. When it’s finished, I’d like it to be able to identify the ADR docs based on a configurable location but, for now, let’s just show how many projects and files we have. For now, we won’t change anything, let’s just hook into the button click of the subtle button in the screenshot above:

        private async void button1_Click(object sender, RoutedEventArgs e)
        {
            await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
            var dte = (DTE)Package.GetGlobalService(typeof(DTE));            

            var sln = Microsoft.Build.Construction.SolutionFile.Parse(dte.Solution.FullName);
            projectsText.Text = $"{sln.ProjectsInOrder.Count.ToString()} projects";

            foreach (Project p in dte.Solution.Projects)
            {
                projectsText.Text += $"{Environment.NewLine} {p.Name} {p.ProjectItems.Count}";
            }
        }

SwitchToMainThreadAsync is because any interaction with the solution needs to be on the main thread. After that, we parse the solution file and output the name and items in each “project”:

As you can see, it classes each top level folder as a solution project, which will be ideal for us.

Summary

In this post, we’ve seen how to create a Visual Studio Extension, and how to trawl the current solution and projects. In the next post, we’ll try to extract some ADR specific stuff.

References

https://docs.microsoft.com/en-us/visualstudio/extensibility/installing-the-visual-studio-sdk?view=vs-2017

https://msdn.microsoft.com/en-us/library/ms973240.aspx?f=255&MSPPError=-2147217396

https://docs.microsoft.com/en-us/visualstudio/extensibility/starting-to-develop-visual-studio-extensions?view=vs-2017

http://www.visualstudioextensibility.com/articles/packages/