Moving Code Blocks Among Code Regions using VS Extensions






4.99/5 (38 votes)
Extend VS by invoking menu item to the context menu that enables moving code blocks among code regions
Introduction
Are you familiar with the keyword #region
? If yes, you can skip this introduction.
Well, let's introduce what #region
is used for and how.
If you worked in a class that is big enough, say 100s of lines and 10s of methods and properties. Imagine if these methods and properties are arranged in some sort of logical groups (e.g., read only properties, read/write properties, static
methods, private
methods, public
methods, ...) and these groups are collapsible. This way will make it easier and faster to find a specific piece of code to maintain. And it will also be easier to other developers who use, maintain, or review your code.
The keyword #region
is there for this purpose. And there are 3 ways to insert and use region
in your code.
- Type
#region
followed byregion
name, and in the next line, type#endregion
, then write or paste your code in between. - An easier way is to right click, select
Insert Snippet
,Visual C#/VB.NET
then#region
. - An additional option is to select a block of code, right click, and select
Surround with
then#region
.
Background
To format an existing code by grouping code blocks in logical regions using currently available Visual Studio features, you need to follow these steps:
- Create the regions
- For each block of code, do the following:
- Select that block
- Cut the selected code
- Navigate to the destination region, or search for it by name
- Paste your code inside it
As I frequently need to do this operation, and may be many developers I think, I created MoveToRegionVSX
extension to Visual Studio to make it easier and faster. With this tool, the process of formatting an existing code will be as follows:
- Create the regions.
- Right click the editor and select Move to Region, this will bring a tool window displaying a list of all existing regions.
- For each block of code, do the following:
- Select that block.
- Double click the destination region from the tool window just opened.
Prerequisites
- Visual Studio 2010 or higher
- Visual Studio 2010 SDK or higher
Preparing the Project
Creating an extension to Visual Studio is some sort of fun and challenge as well. The first step is to select the correct project template. For this tool, I used Visual Studio Package with C#. In the next few lines, I will explain step by step how to recreate this tool yourself.
- Open Visual Studio and select new project
- Select your language (C# was my option)
- Select Extensibility from the template categories
- Select Visual Studio Package from templates
- Click next in the first 2 screens in the wizard (the first is a welcome, and the second is to select the language which is predefined)
- In the company name field, I recommend to enter your name as this will be used as the namespace name. Also, provide a descriptive name for the package (mine is
MoveToRegionVSX
) - The next step is very important. In the additional functionality screen, select both Menu Command and Tool Window. Menu Command is needed to invoke a menu command to the context menu, and the Tool Window is to enable displaying a panel that will contain the
regions
list. - Enter the command name "Move to Region", and its command id
- Enter the window name "Select Region", and its command id
- In the final screen of the wizard, just uncheck the test projects.
Believe it or not, you have created an extension to Visual Studio 2010. Press F5 to believe me. It will launch a new instance of Visual Studio. Click the Tools menu, you will find a new command named "Move To Region", when clicked, it will show a message box. This is the result of the extension you just created.
Now, we are ready for the real work. But first, let me break down the mission into smaller tasks.
- Move the command from Tools menu to Context Menu
- Create the Tool Window
- Add required Assemblies and Namespaces
- Load Regions
- Move Selected Code to Selected Region
- Handle Menu Item Click
Move the Command from Tools Menu to Context Menu
As the formatting process is related to the editor, I think the logical place for our command is the context menu rather than the Tools menu. But as you saw, the default menu used is the Tools menu. In the solution explorer, open the file MoveToRegionVSX.vsct and search for the following block.
<!-- In this section you can define new menu groups. A menu group is a
container for other menus or buttons (commands); from a visual point of view
you can see the group as the part of a menu contained between two lines. The
parent of a group must be a menu. -->
<Groups>
<Group guid="guidMoveToRegionVSXCmdSet" id="MyMenuGroup" priority="0x0600">
<Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_TOOLS"/>
</Group>
</Groups>
In this portion of that file, the parent menu for our command is defined from the Parent
id.
IDM_VS_MENU_TOOLS
refers to Tools menuIDM_VS_CTXT_CODEWIN
refers to Context menu
So, just replace the existing parent
id with that one of the Context menu and it is done.
Create the Tool Window
From the solution explorer, open the file MyControl.xaml, you will find something like this image:
- Delete the button and label
- Add a list box and name it
lstbxRegions
, this will hold the regions list. (Note that this is a WPF control, so, there is no ID, rather set the name property.) - Adjust the dimensions of the list box to be the same as the user control. Say
width=200
andheight=300
.
Now, let's integrate what we have so far. In other words, let's show the tool window when we click on the "Move to Region" command.
To do so, follow these steps:
- From the solution explorer, open the file "MoveToRegionVSXPackage.cs" and find the
MenuItemCallback
event handler. As the name states, this is the event handler that is called when you click o "Move To Region" command. - Replace the body of the event handler with a simple call to the method
ShowToolWindow
:private void MenuItemCallback(object sender, EventArgs e) { //Show the tool window ShowToolWindow(sender, e); }
- Run it now to see the result.
Note that the first time you run it, you may find the tool window little bigger than the list box, adjust it once and it will reserve its dimensions for the next runs.
Add Required Assemblies and Namespaces
Add references to the following assemblies:
Microsoft.VisualStudio.CoreUtility
Microsoft.VisualStudio.Editor
Microsoft.VisualStudio.Text.Data
Microsoft.VisualStudio.Text.Logic
Microsoft.VisualStudio.Text.UI
Microsoft.VisualStudio.Text.UI.Wpf
In the file "MoveToRegionVSXPackage.cs", replace the existing using
statements with the following ones:
using System;
using System.Collections.Generic;
using System.ComponentModel.Design;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio.Editor;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.TextManager.Interop;
Load Regions
The idea is very simple. We need to get access to the current active code viewer, then parse its text searching for all the occurrences of the keyword #region
. For each found region
, we will read its name and add it to a string
list, then populate the list box of the tool window with that string
list.
So, first, we will declare the following members in the MoveToRegionVSXPackage
class:
public sealed class MoveToRegionVSXPackage : Package
{
//<gouda>
//The current active editor's view info
private IVsTextView currentTextView;
private IVsUserData userData;
private IWpfTextViewHost viewHost;
private string allText;
private const string keyword = "#region ";
//</gouda>
Now, we can implement the method GetRegions()
:
/// <summary>
/// Parse all the text in the active editor's view and get all regions
/// </summary>
/// <returns> list of strings containing the names of the existing regions </returns>
internal List<string> GetRegions()
{
List<string> regionsList = new List<string>();
userData = currentTextView as IVsUserData;
if (userData == null)// no text view
{
Console.WriteLine("No text view is currently open");
return regionsList;
}
// In the next 4 statements, I am trying to get access to the editor's view
object holder;
Guid guidViewHost = DefGuidList.guidIWpfTextViewHost;
userData.GetData(ref guidViewHost, out holder);
viewHost = (IWpfTextViewHost)holder;
//Now, I will load all the text of the editor to detect the key word "#region"
allText = viewHost.TextView.TextSnapshot.GetText();
string[] regionDelimitedCode = System.Text.RegularExpressions.Regex.Split(allText,
"\\s#region\\s", //'\s' means any white space character e.g. \t, space, \n, \r, etc
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
for (int index = 1/*skip first block*/; index < regionDelimitedCode.Length; index++)
{
regionsList.Add(regionDelimitedCode[index].Split('\r')[0]);
}
return regionsList;
}
Now, assume we have the list of regions
on hand, so we can populate the list box laid on the tool window. For this purpose, I added a simple method to MyControl
class and named it PopulateList
.
/// <summary>
/// Populates the list box with the list of regions found in the current active view
/// </summary>
/// <param name="regionsList"> list of strings holding the names of regions </param>
/// <param name="activeView"> reference to my package (MoveToRegion Package)</param>
public void PopulateList(List<string> regionsList, MoveToRegionVSXPackage myPkg)
{
//Here is the best place to initialise the packageRef
//I need that reference to enable calling the method MoveToRegion later on double click
//Unless it is logically to do this initialization in the constructor,
//this cannot be done
//because we do not create instance of that class directly
if(packageRef == null)
packageRef = myPkg;
lstbxRegions.Items.Clear();
foreach (string s in regionsList)
lstbxRegions.Items.Add(s);
}
You see, we can retrieve all regions using GetRegions
which is a member of MoveToReionVSXPackage
class, and we fill in the list box using PopulateList
which is a member of MyControl
class. So, how to pass the regions list returned from the former method as a parameter to the later method?
The place for this is the ShowToolWindow
method. To justify, we need to understand how this method works.
The method body is generated by the wizard as follows:
/// <summary>
/// This function is called when the user clicks the menu item that shows the
/// tool window. See the Initialize method to see how the menu item is associated to
/// this function using the OleMenuCommandService service and the MenuCommand class.
/// </summary>
private void ShowToolWindow(object sender, EventArgs e)
{
// Get the instance number 0 of this tool window. This window is single
// instance so this instance
// is actually the only one.
// The last flag is set to true so that if the tool window does not exists
// it will be created.
ToolWindowPane window = this.FindToolWindow(typeof(MyToolWindow), 0, true);
if ((null == window) || (null == window.Frame))
{
throw new NotSupportedException(Resources.CanNotCreateWindow);
}
IVsWindowFrame windowFrame = (IVsWindowFrame)window.Frame;
Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(windowFrame.Show());
}
The window
object is the result of the call to the method FindToolWindow
which takes the type of MyToolWindow
as its first parameter. This call in its turn calls the parameter-less constructor of the class MyToolWindow
at the first call only. If you paid a visit to that class, you will find nothing other than that constructor. And the constructor in turn assigns a new instance of MyControl
class to the property Content
. Here is the relation between the list box and the menu command.
So, after getting the window, I will cast its Content
property to MyControl
to invoke the method PopulateList
.
ToolWindowPane window = this.FindToolWindow(typeof(MyToolWindow), 0, true);
//<gouda>
List<string> regionsList = this.GetRegions();
//call the method PopulateList which is member in MyControl class
//So, cast the Content property of window to MyControl
((MyControl)window.Content).PopulateList(regionsList, this);
//</gouda>
Move Selected Code to Selected Region
The idea here is to detect selection which is so easy once we initialize the viewHost
(I will tell you later when and how to initialize it).
After initializing it, we can get access to its TextView
property which provides a list of rich properties and methods.
Rather, I will let you see the code with few lines of comments.
/// <summary>
/// Moves the selected text to the given regionName
/// If no selection, does nothing
/// </summary>
/// <param name="regionName"> The name of the destination region </param>
internal void MoveToRegion(string regionName)
{
if (viewHost.TextView.Selection.IsEmpty)
return;
//Get the selected text
string selectedText = viewHost.TextView.Selection.StreamSelectionSpan.GetText();
//get the selected span to delete its contents
Span deleteSpan = viewHost.TextView.Selection.SelectedSpans[0];
//now, delete the span as its text is saved in the selectedText
viewHost.TextView.TextBuffer.Delete(deleteSpan);
//Now, I will load all the text of the editor again, because it is subject to change
allText = viewHost.TextView.TextSnapshot.GetText();
//get the position at which region exists
string fullRegionName = keyword + regionName;
int regPos = allText.IndexOf(fullRegionName) + fullRegionName.Length;
//insert the selected text at the specified position
viewHost.TextView.TextBuffer.Insert(regPos, "\r" + selectedText);
}
Handle Menu Item Click
Here, we initialize the currentTextView
from which we can later initialize the viewHost
. Then, we show the tool window.
/// <summary>
/// This function is the callback used to execute a command when the a menu item
/// is clicked.
/// See the Initialize method to see how the menu item is associated to this
/// function using
/// the OleMenuCommandService service and the MenuCommand class.
/// </summary>
private void MenuItemCallback(object sender, EventArgs e)
{
IVsTextManager txtMgr = (IVsTextManager)GetService(typeof(SVsTextManager));
int mustHaveFocus = 1;//means true
//initialize the currentTextView
txtMgr.GetActiveView(mustHaveFocus, null, out currentTextView);
//Show the tool window
ShowToolWindow(sender, e);
}
How to Download
You can download the plug in from the installer link at the top of this article.
Or you can find it on VisualStudio Gallery or VisualStudio Marketplace.
Supported Visual Studio Versions
Currently, I updated the code and plug in to support Visual Studio 2015.
This extension was originally developed using Visual Studio 2010.
If you need to install it on newer versions, just do the following:
- Open the source code using your own version
- Upgrade the project to that version
- Update the project references to use VS SDK of your target version
No need to change any piece of code.
It was tested on both Visual Studio 2012 and 2013.
Unfortunately, it failed on versions earlier than 2010. But as the code is available, feel free to trace and update it to be compatible with your own version if needed.
Special Thanks
Special thanks to Matt U and Ron Nicholson for their support through this useful discussion.
References
Unfortunately, the assemblies used for this purpose were not fully commented. So, I had to try many classes, interfaces, methods, and properties to get it done.
However, I learned more from these 2 links:
- http://msdn.microsoft.com/en-us/library/dd884850(VS.100).aspx
- http://www.devx.com/VS_2010/Article/44073
History
- 30th March 2010: Initial version submitted as my involvement in Visual Studio 2010 Extension Contest
- 1st April 2010: Added the "How to Download" section, besides some text changes
- 28th April 2014: Added "Supported Visual Studio Version" and "Special Thanks" section
- 13th Jan 2017: Updated source code and plug in to support Visual Studio 2015