# Sunday, 26 August 2007

I downloaded the very excellent Resource Refactoring Tool (http://www.codeplex.com/ResourceRefactoring) the other day.  It works great in code files, but I found that it didn't work at all for ASPX/HTML pages.  Seeing this as a challenge, I decided to hack something up in VBA to automatically extract the selected text, create a resx name for the text, add it to the resource file, and insert the ASP.NET literal control.

How to use it

This is split up into two different macro modules, one named Utilities, and the other named what ever you want.  Then add them to your Visual Studio tool bar so you have something convenient you can click on (Right click the tool bar, click Customize, click Commands, then Macros under categories, find what you named the macro and then drag it up to your tool bar.  You can either drag it to an existing tool strip, or create your own.  Finally right click on the item while still in customize mode and add an image).



Imports System Imports EnvDTE Imports EnvDTE80 Imports System.Diagnostics Public Module MoreMacros Public Sub AspTextLiteral() Dim resx as ProjectItem = DTE.ActiveWindow.Object.GetItem("solution\project\folder\ResourceFile.resx") Utilities.AspTextLiteral(resx) End Sub Public Sub InQuoteLiteral() DTE.Windows.Item(Constants.vsWindowKindSolutionExplorer).Activate() Dim resx as ProjectItem = DTE.ActiveWindow.Object.GetItem("solution\project\folder\ResourceFile.resx") Utilities.InQuoteLiteral(resx) End Sub End Module


Imports System Imports EnvDTE Imports EnvDTE80 Imports System.Diagnostics Imports System.Xml Public Module Utilities Public Sub AspTextLiteral(ByRef resourceFile As ProjectItem) Const literalFormat = "<asp:Literal runat=""server"" Text=""<%$ Resources:{0},{1}%>"" />" Const inQuotesFormat = "<%$ Resources:{0},{1}%>" LiteralReplacer(resourceFile, literalFormat, 4) End Sub Public Sub InQuoteLiteral(ByRef resourceFile As ProjectItem) Const inQuotesFormat = "<%$ Resources:{0},{1}%>" LiteralReplacer(resourceFile, inQuotesFormat, 3) End Sub Private Sub LiteralReplacer(ByRef resourceFile As ProjectItem, ByVal template As String, ByVal delete As Int16) Dim ts As TextSelection = DTE.ActiveDocument.Selection Dim name As String = StripNonAllowedChars(ts.Text).Trim.ToLower Dim value As String = ts.Text Dim path As String = resourceFile.Properties.Item("LocalPath").Value Dim resName As String = System.IO.Path.GetFileNameWithoutExtension(resourceFile.Name) name = AddResourceEntry(path, name, value) ts.Text = String.Format(template, resName, name) DTE.ActiveDocument.Selection.Delete(delete) End Sub 'Returns the value of the name used Public Function AddResourceEntry(ByRef resourceFilePath As String, ByVal name As String, ByVal value As String) As String Dim doc As XmlDocument = New XmlDocument doc.Load(resourceFilePath) 'See if it exists first Dim root As XmlElement = doc.DocumentElement Dim e As XmlElement = root.SelectSingleNode(String.Format("data[@name=""{0}""]", name)) If e Is Nothing Then name = CreateUniqueName(root, name) Dim elem As XmlElement = CreateResourceFileElement(doc, name, value) root.AppendChild(elem) Else If e.SelectSingleNode("value").InnerText.ToLower = value.ToLower Then Return name Else name = CreateUniqueName(root, name) Dim elem As XmlElement = CreateResourceFileElement(doc, name, value) root.AppendChild(elem) End If End If doc.Save(resourceFilePath) Return name End Function Private Function CreateUniqueName(ByRef root As XmlElement, ByVal name As String) As String If IsNumeric(name.Substring(0, 1)) Then name = "n" & name End If 'Name doesn't exist, use it If Not CheckNameExists(root, name) Then Return name End If Dim result As String 'Build a unique name For I As Int16 = 1 To 100 result = name & I If Not CheckNameExists(root, result) Then Return result End If Next Return name & Guid.NewGuid.ToString End Function Private Function CheckNameExists(ByRef root As XmlElement, ByVal name As String) As Boolean Dim e As XmlElement = root.SelectSingleNode(String.Format("data[@name=""{0}""]", name)) Return Not (e Is Nothing) End Function Private Function CreateResourceFileElement(ByRef doc As XmlDocument, ByVal name As String, ByVal value As String) As XmlElement Dim result As XmlElement = doc.CreateElement("data") SetAttribute(result, "name", name) SetAttribute(result, "xml:space", "preserve") Dim valueElem As XmlElement = doc.CreateElement("value") valueElem.InnerText = value result.AppendChild(valueElem) Return result End Function Private Function SetAttribute(ByRef element As XmlElement, ByVal attr As String, ByVal value As String) As XmlElement If element.Attributes.GetNamedItem(attr) Is Nothing Then Dim attrib As XmlAttribute = element.OwnerDocument.CreateAttribute(attr) attrib.Value = value element.Attributes.Append(attrib) Else element.Attributes.Item(attr).Value = value End If Return element End Function Private Function StripNonAllowedChars(ByVal text As String) As String Const non_allowed = """';:,<.>/?\|`~!@#$%^&*()-=+[{]} " For I As Integer = 0 To non_allowed.Length - 1 text = text.Replace(non_allowed(I).ToString, "") Next Return text End Function End Module


del.icio.us Tags: , , ,
Sunday, 26 August 2007 20:56:04 (Alaskan Daylight Time, UTC-08:00)
# Sunday, 20 May 2007

The Visual Studio solution file for our software contains 36 projects (and growing).  If you've ever tried to find a particular file or project in a 36 project solution when many projects and folders are expanded, then you know how frustrating it can be.

The Solution

After putting up with it for over a year, I finally asked a co-worker of mine if he knew of a way to quickly jump to a particular project in Visual Studio.  He reminded me that Visual Studio has excellent macro support.  A few minutes later using Visual Studios Macro Recorder feature I had something to jump to a project I'm in a lot.  All told, I jump between around 4 projects pretty regularly - our business logic layer, domain model, smart client and UI, and having these macros have been a huge time saver!

The Visual Studio Macro

Sub TemporaryMacro() DTE.ActiveWindow.Object.GetItem("MySolution\MyProject").UIHierarchyItems.Expanded = True DTE.ActiveWindow.Object.GetItem("MySolution\MyProject").Select(vsUISelectionType.vsUISelectionTypeSelect) End Sub

Using this macro, you can straight to the "MyProject" project in your solution.  This works, but its not very generic.  The solution name is hard coded, which is fine if the particular project only lives in one solution.  But in our case, we have 3 or 4 different solutions created.  The monster with all 36 solutions, a client only solution with 10 of the projects, and a core only solution.

I don't feel like creating different macros to jump between the same 5 projects depending upon which solution is open.  So lets refactor this a little bit. 

1 Private Function GetSolution() As UIHierarchyItem 2 Dim win As Window = DTE.Windows.Item(Constants.vsWindowKindSolutionExplorer) 3 Dim uih As UIHierarchy = win.Object 4 GetSolution = uih.UIHierarchyItems.Item(1) 5 End Function 6 7 Private Function GetProject(ByVal ProjectName As String) As UIHierarchyItem 8 GetProject = GetSolution().UIHierarchyItems.Item(ProjectName) 9 End Function 10 11 Private Sub GotoProject(ByVal ProjectName As String) 12 DTE.Windows.Item(Constants.vsWindowKindSolutionExplorer).Activate() 13 14 Try 15 GetProject(ProjectName).Select(vsUISelectionType.vsUISelectionTypeSelect) 16 GetProject(ProjectName).UIHierarchyItems.Expanded = True 17 Catch ex As Exception 18 End Try 19 End Sub 20 21 Public Sub MyProject() 22 GotoProject("MyProject") 23 End Sub

Lets break this down. The GetSolution method returns a UIHierarchyItem (an item in the solution explorer tree you can click on). The first thing we do is tell the DTE (What does DTE stand for btw?) we want the solution explorer window (line 2), once we have that we can get the hierarchy (line 3), then we can get the items and get the first item (line 4) which is the solution item in the hierarchy.

NOTE: The macro environment is based on COM,  and in COM collections and arrays start at 1, and not 0.

In the GetProject method, you should see code which looks familiar.  The interesting things come in the GotoProject method.  First we active the the solution explorer window in the IDE (line 12), then get the project hierarchy item and select it so it has focus (line 15), and finally we expand the item in the hierarchy so you can see all the files under the project (line 16).

And finally on line 21 we have the actual callable macro which will warp us to the project we want in the solution.

A Good First Step

So thats a good first step, but what else can we do?  We can collapse all the items in the solution explorer, open up a particular file, or even "fix" items in your solution.

Collapsing the Solution Explorer

One thing I don't like about Visual Studio is over time, with a large number of projects,  a lot of projects, folders and compound items (winforms items with a designer and resource file) get expanded and it makes it especially hard to pick things out of the visual clutter.  I always find it to be very tedious to close all the items in the solution to clean up the clutter.  Lets write a macro to fix this mess for us.

1 Public Sub CollapseTopLevel() 2 DTE.Windows.Item(Constants.vsWindowKindSolutionExplorer).Activate() 3 4 Dim solutionWindow As EnvDTE.Window = DTE.ActiveWindow 5 solutionWindow.Visible = False 6 Dim solution As UIHierarchyItem = GetSolution() 7 8 CollapseHierarchy(solution.UIHierarchyItems, True, True) 9 solutionWindow.Visible = True 10 DTE.StatusBar.Clear() 11 DTE.StatusBar.Progress(False) 12 End Sub 13 Public Sub CollapseAll() 14 DTE.Windows.Item(Constants.vsWindowKindSolutionExplorer).Activate() 15 16 Dim solutionWindow As EnvDTE.Window = DTE.ActiveWindow 17 solutionWindow.Visible = False 18 Dim solution As UIHierarchyItem = GetSolution() 19 20 CollapseHierarchy(solution.UIHierarchyItems, True, False) 21 solutionWindow.Visible = True 22 DTE.StatusBar.Clear() 23 DTE.StatusBar.Progress(False) 24 End Sub 25 Private Sub CollapseHierarchy(ByRef items As UIHierarchyItems, ByVal IsRoot As Boolean, ByVal OnlyCollapseRootLevel As Boolean) 26 For i As Int32 = 1 To items.Count 27 If IsRoot Then DTE.StatusBar.Progress(True, "Collapsing", i, items.Count) 28 If (items.Item(i).UIHierarchyItems.Count > 0 And Not OnlyCollapseRootLevel) Then 29 DTE.StatusBar.Text = "Collapsing " & items.Item(i).Name 30 CollapseHierarchy(items.Item(i).UIHierarchyItems, False, False) 31 End If 32 items.Item(i).UIHierarchyItems.Expanded = False 33 Next 34 End Sub

There are two different methods here to collapse the solution, CollapseTopLevel and CollapseAll.  CollapseTopLevel only collapses project items in the UI, while CollapseAll will drill down and collapse every item.  The first is fast, takes less than a second to collapse 36 items, while the later takes about 15 seconds.  If you notice lines 4, 5 we grab a reference to the solution window, and then hide it.  If the solution window is visible while the projects are collapsed, the whole process takes much longer while the UI repaints.

Opening a Particular File

When working on our data access layer (DAL), I do a lot of editing of our SQL upgrade script.  I don't really like to try and find this file in the solution explorer.  More than that, when I'm editing this file, I'm always adding to the bottom of this file.  Can we do all that with a macro?  Sure enough!

1 Public Sub DbScripts() 2 DTE.Windows.Item(Constants.vsWindowKindSolutionExplorer).Activate() 3 Dim project As UIHierarchyItem = GetSolution().UIHierarchyItems.Item("SchemaUpgradeManagerProject") 4 Dim scripts As ProjectItem = project.UIHierarchyItems.Item("Scripts").UIHierarchyItems.Item("DbScripts.sql").Object 5 scripts.Open().Activate() 6 DTE.ActiveDocument.Selection.EndOfDocument() 7 End Sub

Fixing Things in the Solution

One of the frustrating things about creating NHibernate mapping files is remembering to set the build action to Embedded Resource.  Or remembering to set the build action of images, sounds and movie clips and other files to Embedded Resource.  Why not let a macro fix this mess for you too?

Public Sub FixThings() Try Dim project As Project Dim projectItem As UIHierarchyItem Dim folder As UIHierarchyItem projectItem = GetProject("DataAccessProject") folder = projectItem.UIHierarchyItems.Item("Mappings") EmbeddResources(folder, "hbm.xml") project = projectItem.Object project.Save() projectItem = GetProject("UserInterfaceProject") folder = projectItem.UIHierarchyItems.Item("Icons") EmbeddResources(folder, "png") project = projectItem.Object project.Save() DTE.StatusBar.Clear() DTE.StatusBar.Progress(False) Catch ex As Exception End Try End Sub Private Sub EmbeddResources(ByRef folder As UIHierarchyItem, ByVal extension As String) Dim file As ProjectItem For i As Int32 = 1 To folder.UIHierarchyItems.Count file = folder.UIHierarchyItems.Item(i).Object DTE.StatusBar.Progress(True, "Setting BuildAction in " & folder.Name, i, folder.UIHierarchyItems.Count) If file.Name.EndsWith(extension) Then file.Properties.Item("BuildAction").Value = 3 ' Embedded Resource End If Next End Sub

There you have it, some simple Visual Studio macros to make your development much easier!  Questions, comments, concerns, leave me a comment.

Your Macros, Now Only a Click Away!

Now that we have all of these macros, we need a way to quickly and easily access them.  Custom Visual Studio toolbars to the rescue!  To create a custom toolbar, click on Tools -> Customize -> Toolbars.  From there click on the "New" button and give it a name.  I choose the very original name of "My Macros" :). Dock your toolbar if you'd like.

With that done, click over to "Commands" tab and click on "Macros" in the left-side. All of your macros, as well as the sample macros will show up in the right-side.  Drag the macros you want to the toolbar you just created.  You'll notice that your macro has a really long name like "Macros.MyMacros.SomeName.CollapseTopLevel."  Long names like that will quickly eat up your valuable screen real estate.

Thankfully we can fix that.  While still in Customize mode, you can right-click on your macro in the toolbar (and indeed any item in any toolbar) and the third option down is "Name."  Set this to what ever you want.  Since screen real-estate is precious to me, I named my "Macros.MyMacros.ProjectName.CollapseTopLevel" macro to "ClpseTopLvl".


In my haste to publish this article at around 1 AM I failed to mention how to create a custom tool bar to give you quick access to all of your macros.

11-Feb-2009 - Johnny Idol has referenced this blog post, and gone into detail about how to actually create the macros and bind keyboard shortcuts to them.

Sunday, 20 May 2007 01:12:32 (Alaskan Daylight Time, UTC-08:00)