An ADR Visual Studio Extension – Listing and Reading the files

pcmichaels profile image Paul Michaels Updated on ・3 min read

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, 
                    && pi.ProjectItems != null)
                    await ScanProjectItems(pi.ProjectItems, projectData);

                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)

            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();

                TextDocument textDocument;
                if (!projectItem.IsOpen)
                    var doc = projectItem.Open(EnvDTE.Constants.vsViewKindCode);
                    textDocument = (TextDocument)doc.Document.Object("TextDocument");
                    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.




Posted on by:

pcmichaels profile

Paul Michaels


I'm a Lead Developer. I've been programming professionally since 1997. I'm interested in finding neat solutions to difficult problems.


Editor guide