![](http://nesteruk.org/pix/0/0944bf8b-45ef-4449-a91b-1ba9fcb07454.jpg)
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 == null) return;<br/>
var fileSystemPath = FileSystemPath.TryParse(path);<br/>
if (fileSystemPath == null) return;<br/>
var assemblyFile = provider.Solution.AddAssembly(fileSystemPath);<br/>
if (assemblyFile == null) return;<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.) стало намного проще. ■