Home .NET Quick add links or “goodbye Add Reference”

Quick add links or “goodbye Add Reference”

by admin

I recently finished a problem that hasbeenbugging me for a very long time. The gist of it is that the Add Reference dialog inVisual Studio is not needed ifyou take an assembly from one of those places where the studio looks for them. You don’t need it because the studio itself could index all the namespaces inthose assemblies and when you write usingBiztalk allow me to add the reference automatically. Since the studio doesn’t know how to do this, I had to help it.
The idea itself is simple, and consists of 2 x parts, viz:

  • We need to find all the important assemblies and index all their namespaces.
  • When hovering the cursor over using , you need to search for all possible assemblies and show a menu.

Indexing

The database for namespaces and assembly filepaths is done inseconds. The only trick is to use Cecil instead of perversions like Assembly.ReflectionOnlyLoad() which tryto load dependencies or whatever else. Quickly, we find all types, write their names inthe HashSet and then we dump it all into the database. How? That’s what we’re going to talk about now.
First, the file paths that Add Reference uses are at least 2 x places – inthe registry, and inthe PublicAssemblies folder. To find those folders listed inthe registry, I wrote the following code :

public static IEnumerable< string > GetAssemblyFolders()<br/>
{<br/>
string [] valueNames = new [] { string Empty, "All Assemblies In" };<br/>
string [] keyNames = new []<br/>
{<br/>
@"SOFTWAREMicrosoft.NETFrameworkAssemblyFolders" , <br/>
@"SOFTWAREWow6432NodeMicrosoft.NETFrameworkAssemblyFolders" <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/>

Initially it did not work much for me as the keys are different on 32-bit and 64-bit systems. I notice once again that I began to write much better code with my move to a 64-bit system 🙂
To find the PublicAssemblies folder, you must first find where Visual Studio is installed:

public static string GetVS9InstallDirectory()<br/>
{<br/>
var keyNames = new string []<br/>
{<br/>
@"SOFTWAREWow6432NodeMicrosoftVisualStudio9.0SetupVS" , <br/>
@"SOFTWAREMicrosoftVisualStudio9.0SetupVS" <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/>

By having a list of folders, you can search all the DLL files in each folder and index them. In addition to those folders that always appear in the Add Reference dialog, you can add your own folders, which comes in handy.

using ( var dc = new StatsDataContext())<br/>
{<br/>
var dirs = new HashSet< string > ();<br/>
dirs.Add( @"C:Program Files (x86)JetBrainsReSharperv4.5Bin" );<br/>
foreach ( var dir in GetAssemblyFolders()) dirs.Add(dir);<br/>
dirs.Add(Path.Combine(GetVS9InstallDirectory(), @"Common7IDEPublicAssemblies" ));<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/>

This is added using the method AddNamespacesFromFile() which, as I wrote before, uses 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/>

That’s it for filling the base. Then you can use the results, although I also made a background utility that allows you to refresh the data and add new paths.

Using

Not having better options, I implemented adding links as a context action for ReSharper. The idea is simple – the user hovering over the word Biztalk on the line usingBiztalk; and sees a magic menu, which automatically adds a link to the project when you select items.
CA itself inherits from a useful class CSharpContextActionBase which does nothing clever inside except checking for "applicability". The database is searched with a simple sampling in the style of SELECT * from Namespaces where NamespaceName LIKE '%BizTalk%' For a database in which you will have a couple thousand elements (well, maybe 10 thousand ifyou try), this approach is adequate.

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/>

All the interesting stuff happens in BulbItem ah, i.e. the yellow bulbs that appear when the context menu is called. The bulb itself is a kind of POCO which is able to add a link to a certain assembly at the right moment.

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/>

I was only able to write the code above with the help of a member of the JetBrains team ( planerist – thanks!), as I didn’t have the perseverance to find the right way myself.

Conclusion

I don’t know how much time I saved by implementing this feature, but it definitely gave me less headaches like "waiting for Add Reference". And linking projects with my favorite set of builds (DI, Mocks, validation, utilities, etc.) became much easier. ■

You may also like