Pull to refresh

Быстрое добавление ссылок или «прощай Add Reference»

Reading time10 min
Views4.7K
Недавно я допилил одну проблему, которая меня уже очень давно достает. Суть ее в том, что диалог Add Reference в Visual Studio не нужен, если вы берете сборку из одного из тех мест, где их ищет студия. Не нужен он потому, что студия вполне могла бы сама проиндексировать все пространства имен в этих сборках и при написании using Biztalk дать мне возможность добавить ссылку автоматически. Поскольку студия это делать не умеет, пришлось ей помочь.


Идея сама по себе простая, и состоит из 2х частей, а именно:

  • Нужно найти все важные сборки и проиндексировать все их пространства имен.
  • При наведении курсора на using, нужно сделать поиск всех возможных сборок и показать меню.

Индексирование


База для пространств имен и путям к файлам сборок делается за секунды. Единственный трюк – это использование Cecil вместо извращений вроде Assembly.ReflectionOnlyLoad(), которые пытаются подгрузить зависимости и ещё невесть что. Быстренько находим все типы, записываем их простнанства имен в HashSet, и засовываем все это в базу. Как? Об этом сейчас и поговорим.

Во-первых, пути к файлам которые использует Add Reference находятся как минимум в 2х местах – в реестре, и в папке PublicAssemblies. Чтобы найти те папки, которые указаны в реестре, я написал следующий код:



public static IEnumerable<string> GetAssemblyFolders()<br/>
{<br/>
  string[] valueNames = new[] { string.Empty, "All Assemblies In" };<br/>
  string[] keyNames = new[]<br/>
  {<br/>
    @"SOFTWARE\Microsoft\.NETFramework\AssemblyFolders",<br/>
    @"SOFTWARE\Wow6432Node\Microsoft\.NETFramework\AssemblyFolders"<br/>
  };<br/>
  var result = new HashSet<string>();<br/>
  foreach (var keyName in keyNames)<br/>
  {<br/>
    using (var key = Registry.LocalMachine.OpenSubKey(keyName))<br/>
    {<br/>
      if (key != null)<br/>
        foreach (string subkeyName in key.GetSubKeyNames())<br/>
        {<br/>
          using (var subkey = key.OpenSubKey(subkeyName))<br/>
          {<br/>
            if (subkey != null)<br/>
            {<br/>
              foreach (string valueName in valueNames)<br/>
              {<br/>
                string value = (subkey.GetValue(valueName) as string ?? string.Empty).Trim();<br/>
                if (!string.IsNullOrEmpty(value))<br/>
                  result.Add(value);<br/>
              }<br/>
            }<br/>
          }<br/>
        }<br/>
    }<br/>
  }<br/>
  return result;<br/>
}<br/>

Изначально у меня мало что работало, т.к. ключи на 32-битной и 64-битной системе разные. В очередной раз замечаю что с переходом на 64-битную систему начал писать более качественный код :)

Чтобы найти папку PublicAssemblies, нужно сначала найти где установлена Visual Studio:

public static string GetVS9InstallDirectory()<br/>
{<br/>
  var keyNames = new string[]<br/>
   {<br/>
     @"SOFTWARE\Wow6432Node\Microsoft\VisualStudio\9.0\Setup\VS",<br/>
     @"SOFTWARE\Microsoft\VisualStudio\9.0\Setup\VS"<br/>
   };<br/>
  foreach (var keyName in keyNames)<br/>
  {<br/>
    using (var key = Registry.LocalMachine.OpenSubKey(keyName))<br/>
    {<br/>
      if (key != null)<br/>
        return key.GetValue("ProductDir").ToString();<br/>
    }<br/>
  }<br/>
  return null;<br/>
}<br/>

Имея список папок, можно в каждой искать все DLL-файлы и индексировать их. Помимо тех папок что всегда фигурируют в диалоге Add Reference, можно добавлять свои папки, что бывает удобно.

using (var dc = new StatsDataContext())<br/>
{<br/>
  var dirs = new HashSet<string>();<br/>
  dirs.Add(@"C:\Program Files (x86)\JetBrains\ReSharper\v4.5\Bin");<br/>
  foreach (var dir in GetAssemblyFolders()) dirs.Add(dir);<br/>
  dirs.Add(Path.Combine(GetVS9InstallDirectory(), @"Common7\IDE\PublicAssemblies"));<br/>
  foreach (string dir in dirs.Where(Directory.Exists))<br/>
  {<br/>
    string[] files = Directory.GetFiles(dir, "*.dll");<br/>
    var entries = new HashSet<Namespace>();<br/>
    foreach (string file in files)<br/>
    {<br/>
      var ns = AddNamespacesFromFile(file);<br/>
      foreach (var n in ns)<br/>
        entries.Add(n);<br/>
    }<br/>
    dc.Namespaces.InsertAllOnSubmit(entries);<br/>
  }<br/>
  dc.SubmitChanges();<br/>
}<br/>

Добавление происходит с помощью метода AddNamespacesFromFile() который, как я уже писал, использует Mono.Cecil.

private static IEnumerable<Namespace> AddNamespacesFromFile(string file)<br/>
{<br/>
  HashSet<Namespace> result = new HashSet<Namespace>();<br/>
  try<br/>
  {<br/>
    var ad = AssemblyFactory.GetAssembly(file);<br/>
    foreach (ModuleDefinition m in ad.Modules)<br/>
    {<br/>
      foreach (TypeDefinition type in m.Types)<br/>
      {<br/>
        if (type.IsPublic && !string.IsNullOrEmpty(type.Namespace))<br/>
        {<br/>
          result.Add(new Namespace<br/>
          {<br/>
            AssemblyName = ad.Name.Name,<br/>
            AssemblyVersion = ad.Name.Version.ToString(),<br/>
            NamespaceName = type.Namespace,<br/>
            PhysicalPath = file<br/>
          });<br/>
        }<br/>
      }<br/>
    }<br/>
  }<br/>
  catch<br/>
  {<br/>
    // it's okay, probably a non-.Net DLL
  }<br/>
  return result;<br/>
}<br/>

С наполнением базы на этом все. Далее можно пользоваться результатами, хотя я помимо этого сделал фоновую утилиту которая позволяет освежать данные и добавлять новые пути.

Использование


Не имея лучших вариантов, я реализовал добавление ссылок как context action для ReSharper. Идея простая – пользователь наводит курсор на слово Biztalk в строке using Biztalk; и видит магическое меню, при выборе элементов которого автоматически добавляется ссылка в проект.

Сам СА наследует от полезного класса CSharpContextActionBase, внутри которого кроме проверки а «применимость» ничего умного не происходит. Поиск по базе ведется с помощью простой выборки в стиле SELECT * from Namespaces where NamespaceName LIKE '%BizTalk%'. Для базы в которой у вас будет пара тысяч элементов (ну, может 10 тысяч, если постараетесь), такой подход адекватен.

protected override bool IsAvailableInternal()<br/>
{<br/>
  items = EmptyArray<IBulbItem>.Instance;<br/>
  var element = GetSelectedElement<IElement>(false);<br/>
  if (element == null)<br/>
    return false;<br/>
  var parent = element.ToTreeNode().Parent;<br/>
  if (parent == null || parent.GetType().Name != "ReferenceName" || parent.Parent == null<br/>
    || string.IsNullOrEmpty(parent.Parent.GetText()))<br/>
    return false;<br/>
  string s = parent.Parent.GetText();<br/>
  if (string.IsNullOrEmpty(s))<br/>
    return false;<br/>
  var bulbItems = new HashSet<RefBulbItem>();<br/>
  using (var conn = new SqlConnection(<br/>
    "Data Source=(local);Initial Catalog=Stats;Integrated Security=True"))<br/>
  {<br/>
    conn.Open();<br/>
    var cmd = new SqlCommand(<br/>
      "select * from Namespaces where NamespaceName like '%" + s + "%'", conn);<br/>
    using (var r = cmd.ExecuteReader())<br/>
    {<br/>
      int count = 0;<br/>
      while (r.Read())<br/>
      {<br/>
        bulbItems.Add(new RefBulbItem(<br/>
          provider,<br/>
          r.GetString(2).Trim(),<br/>
          r.GetString(3).Trim(),<br/>
          r.GetString(4).Trim()));<br/>
        count++;<br/>
      }<br/>
      if (count > 0)<br/>
      {<br/>
        items = System.Linq.Enumerable.ToArray(<br/>
          System.Linq.Enumerable.ThenBy(<br/>
            System.Linq.Enumerable.OrderBy(<br/>
              bulbItems,<br/>
              i => i.AssemblyName),<br/>
            i => i.AssemblyVersion));<br/>
        return true;<br/>
      }<br/>
    }<br/>
  }<br/>
  return false;<br/>
}<br/>

Все интересное происходит в BulbItemах, то есть желтых лампочках которые появляются в процессе вызова контекстного меню. Сама лампочка – это некий POCO который умеет в нужный момент добавить ссылку на определенную сборку.

protected override void ExecuteBeforeTransaction(ISolution solution,<br/>
  JetBrains.TextControl.ITextControl textControl, IProgressIndicator progress)<br/>
{<br/>
  var project = provider.Project;<br/>
  if (project == nullreturn;<br/>
  var fileSystemPath = FileSystemPath.TryParse(path);<br/>
  if (fileSystemPath == nullreturn;<br/>
  var assemblyFile = provider.Solution.AddAssembly(fileSystemPath);<br/>
  if (assemblyFile == nullreturn;<br/>
  var cookie = project.GetSolution().EnsureProjectWritable(project, out project, SimpleTaskExecutor.Instance);<br/>
  QuickFixUtil.ExecuteUnderModification(textControl,<br/>
    () => project.AddModuleReference(assemblyFile.Assembly),<br/>
    cookie);<br/>
}<br/>

Код выше удалось написать только с помощью члена команды JetBrains (planerist – спасибо!), т.к. у меня у самого не хватило усидчивости чтобы найти правильный способ.

Заключение


Не знаю сколько я сэкономил времени путем реализации этого функционала, но головной боли в стиле «сидим ждем Add Reference» точно стало меньше. И компоновать проекты с моим любимым набором сборок (DI, Mocks, validation, utilities, etc.) стало намного проще. ■
Tags:
Hubs:
+16
Comments23

Articles

Change theme settings