Category Archives: Visual Studio Extensions

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 2 – Refactoring

A short while ago, I wrote an article about how to create a new extension for Visual Studio. The end target of this is to have a tool that will allow easy management of ADR Records.

In this post, I’m going to clean up some of the code in that initial sample. There’s nothing new here, just some basic WPF good practices. If you’re interested in downloading, or just seeing the code, it’s here.

What’s wrong with what was there?

The extension worked (at least as far as it went), but it used code behind to execute the functionality. This means that the logic and the UI are tightly coupled. My guess is that soon (maybe as part of .Net 5) the extensions will move over to another front end tech (i.e. not WPF), which means that people that have written extensions may need to re-write them. This is a guess – I don’t know any more than you do.

Onto the refactoring… Starting with MVVM basics

Let’s start with a simple View Model; previously, the code was in the code behind, so we’ll move that all over to a view model:

    public class AdrControlViewModel : INotifyPropertyChanged
    {
        public AdrControlViewModel()
        {
            Scan = new RelayCommandAsync<object>(ScanCommand);
        }

        private string _summary;
        public string Summary 
        { 
            get => _summary; 
            set => UpdateField(ref _summary, value); 
        }

        public RelayCommandAsync<object> Scan { get; set; }

        private async Task ScanCommand(object arg)
        {
            var solutionAnalyser = new SolutionAnalyser();
            Summary = await solutionAnalyser.ScanSolution();            
        }
    }

You’ll also need the following INotifyPropertyChanged boilerplate code:

        #region INotifyPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged([CallerMemberName]string fieldName = null) =>        
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(fieldName));

        private void UpdateField<T>(ref T field, T value, [CallerMemberName]string fieldName = null)
        {
            field = value;
            OnPropertyChanged(fieldName);
        }
        #endregion

One day, this can go into a base class, if we ever create a second View Model. We’ll come back to SolutionAnalyser in a while. I shamelessly pilfered the RelayCommand code from here. Finally, I did a bit of shuffling around:

Finally, the code behind needs to be changed as follows:

    public partial class AdrControl : UserControl
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="AdrControl"/> class.
        /// </summary>
        public AdrControl()
        {
            this.InitializeComponent();
            DataContext = new AdrControlViewModel();
        }   
    }

SolutionAnalyser

This is, essentially, the only real code that actually does anything. It’s likely to be severely refactored in a later incarnation, but for now, it’s just in its own class:

    public class SolutionAnalyser
    {
        internal async Task<string> ScanSolution()
        {
            try
            {
                await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
                var dte = (DTE)Package.GetGlobalService(typeof(DTE));

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

                foreach (Project p in dte.Solution.Projects)
                {
                    summaryText += $"{Environment.NewLine} {p.Name} {p.ProjectItems.Count}";
                }
                return summaryText;
            }
            catch
            {
                return "Solution is not ready yet.";
            }            
        }
    }

What’s next?

The next stage is to introduce a search and create facility. I’m going to start creating some issues in the GitHub repo when I get some time.