Quick add links or goodbye Add Reference

    I recently completed one problem that has been bothering me for a very long time. Its essence is that the Add Reference dialog in Visual Studio is not needed if you take the assembly from one of those places where the studio is looking for them. It is not needed because the studio itself could well index all the namespaces in these assemblies and, when writing, using Biztalkgive me the opportunity to add the link automatically. Since the studio does not know how to do this, I had to help her.


    The idea itself is simple, and consists of 2 x parts, namely:

    • You need to find all the important assemblies and index all their namespaces.
    • When you hover over using, you need to search for all possible assemblies and show the menu.

    Indexing


    The base for namespaces and assembly file paths is done in seconds. The only trick is the use of Cecil instead of perversions like Assembly.ReflectionOnlyLoad(), which try to load dependencies and even that. We quickly find all types, write their namespaces in HashSet, and put it all into the database. How? We’ll talk about this now.

    First, the path to the file that uses the Add Reference are at least 2 x locations - in the registry, and PublicAssemblies folder. To find the folders that are listed in the registry, I wrote the following code:



    public static IEnumerable GetAssemblyFolders()

    {

      string[] valueNames = new[] { string.Empty, "All Assemblies In" };

      string[] keyNames = new[]

      {

        @"SOFTWARE\Microsoft\.NETFramework\AssemblyFolders",

        @"SOFTWARE\Wow6432Node\Microsoft\.NETFramework\AssemblyFolders"

      };

      var result = new HashSet();

      foreach (var keyName in keyNames)

      {

        using (var key = Registry.LocalMachine.OpenSubKey(keyName))

        {

          if (key != null)

            foreach (string subkeyName in key.GetSubKeyNames())

            {

              using (var subkey = key.OpenSubKey(subkeyName))

              {

                if (subkey != null)

                {

                  foreach (string valueName in valueNames)

                  {

                    string value = (subkey.GetValue(valueName) as string ?? string.Empty).Trim();

                    if (!string.IsNullOrEmpty(value))

                      result.Add(value);

                  }

                }

              }

            }

        }

      }

      return result;

    }


    Initially, little worked for me, because keys on a 32-bit and 64-bit system are different. Once again, I notice that with the transition to a 64-bit system, I started writing better code :)

    To find the PublicAssemblies folder, you must first find where Visual Studio is installed:

    public static string GetVS9InstallDirectory()

    {

      var keyNames = new string[]

       {

         @"SOFTWARE\Wow6432Node\Microsoft\VisualStudio\9.0\Setup\VS",

         @"SOFTWARE\Microsoft\VisualStudio\9.0\Setup\VS"

       };

      foreach (var keyName in keyNames)

      {

        using (var key = Registry.LocalMachine.OpenSubKey(keyName))

        {

          if (key != null)

            return key.GetValue("ProductDir").ToString();

        }

      }

      return null;

    }


    Having a list of folders, you can search all the DLL files in each and index them. In addition to the folders that always appear in the Add Reference dialog, you can add your own folders, which is convenient.

    using (var dc = new StatsDataContext())

    {

      var dirs = new HashSet();

      dirs.Add(@"C:\Program Files (x86)\JetBrains\ReSharper\v4.5\Bin");

      foreach (var dir in GetAssemblyFolders()) dirs.Add(dir);

      dirs.Add(Path.Combine(GetVS9InstallDirectory(), @"Common7\IDE\PublicAssemblies"));

      foreach (string dir in dirs.Where(Directory.Exists))

      {

        string[] files = Directory.GetFiles(dir, "*.dll");

        var entries = new HashSet();

        foreach (string file in files)

        {

          var ns = AddNamespacesFromFile(file);

          foreach (var n in ns)

            entries.Add(n);

        }

        dc.Namespaces.InsertAllOnSubmit(entries);

      }

      dc.SubmitChanges();

    }


    Adding occurs using a method AddNamespacesFromFile()that, as I wrote, uses Mono.Cecil.

    private static IEnumerable AddNamespacesFromFile(string file)

    {

      HashSet result = new HashSet();

      try

      {

        var ad = AssemblyFactory.GetAssembly(file);

        foreach (ModuleDefinition m in ad.Modules)

        {

          foreach (TypeDefinition type in m.Types)

          {

            if (type.IsPublic && !string.IsNullOrEmpty(type.Namespace))

            {

              result.Add(new Namespace

              {

                AssemblyName = ad.Name.Name,

                AssemblyVersion = ad.Name.Version.ToString(),

                NamespaceName = type.Namespace,

                PhysicalPath = file

              });

            }

          }

        }

      }

      catch

      {

        // it's okay, probably a non-.Net DLL
      }

      return result;

    }


    With filling the base, that’s all. Further, you can use the results, although in addition to this I made a background utility that allows you to refresh data and add new paths.

    Using


    Having no better options, I implemented adding links as context action for ReSharper. The idea is simple - the user hovers over a word Biztalkin a line using Biztalk;and sees a magic menu, when you select the elements of which a link is automatically added to the project.

    The CA itself inherits from a useful class CSharpContextActionBase, inside of which, apart from checking and "applicability", nothing smart happens. The database is searched using a simple style selection SELECT * from Namespaces where NamespaceName LIKE '%BizTalk%'. For a base in which you will have a couple of thousand elements (well, maybe 10 thousand if you try), this approach is adequate.

    protected override bool IsAvailableInternal()

    {

      items = EmptyArray.Instance;

      var element = GetSelectedElement(false);

      if (element == null)

        return false;

      var parent = element.ToTreeNode().Parent;

      if (parent == null || parent.GetType().Name != "ReferenceName" || parent.Parent == null

        || string.IsNullOrEmpty(parent.Parent.GetText()))

        return false;

      string s = parent.Parent.GetText();

      if (string.IsNullOrEmpty(s))

        return false;

      var bulbItems = new HashSet();

      using (var conn = new SqlConnection(

        "Data Source=(local);Initial Catalog=Stats;Integrated Security=True"))

      {

        conn.Open();

        var cmd = new SqlCommand(

          "select * from Namespaces where NamespaceName like '%" + s + "%'", conn);

        using (var r = cmd.ExecuteReader())

        {

          int count = 0;

          while (r.Read())

          {

            bulbItems.Add(new RefBulbItem(

              provider,

              r.GetString(2).Trim(),

              r.GetString(3).Trim(),

              r.GetString(4).Trim()));

            count++;

          }

          if (count > 0)

          {

            items = System.Linq.Enumerable.ToArray(

              System.Linq.Enumerable.ThenBy(

                System.Linq.Enumerable.OrderBy(

                  bulbItems,

                  i => i.AssemblyName),

                i => i.AssemblyVersion));

            return true;

          }

        }

      }

      return false;

    }


    All interesting things happen in BulbItemah, that is, yellow bulbs that appear during the call of the context menu. The bulb itself is a certain POCO that can add a link to a specific assembly at the right time.

    protected override void ExecuteBeforeTransaction(ISolution solution,

      JetBrains.TextControl.ITextControl textControl, IProgressIndicator progress)

    {

      var project = provider.Project;

      if (project == null) return;

      var fileSystemPath = FileSystemPath.TryParse(path);

      if (fileSystemPath == null) return;

      var assemblyFile = provider.Solution.AddAssembly(fileSystemPath);

      if (assemblyFile == null) return;

      var cookie = project.GetSolution().EnsureProjectWritable(project, out project, SimpleTaskExecutor.Instance);

      QuickFixUtil.ExecuteUnderModification(textControl,

        () => project.AddModuleReference(assemblyFile.Assembly),

        cookie);

    }


    The code above was written only with the help of a member of the JetBrains team ( planerist - thanks!), Because I myself did not have the perseverance to find the right way.

    Conclusion


    I don’t know how much time I saved by implementing this functionality, but the headache in the style of “sitting, waiting for Add Reference” definitely became less. And compiling projects with my favorite set of assemblies (DI, Mocks, validation, utilities, etc.) has become much easier. ■

    Also popular now: