Introduction
This article does not aim to introduce any complexity in writing addins. It merely uses simple approaches to create an addin in VS.NET IDE that can be useful for code writing. I believe that most addins for VS.NET are very useful but they are even better if they can speed up the code writing process.
Getting Started
I was recently working in a project that involved a series of independent classes that needed to be stored in strongly typed collections in different parts of the program. From the wise words of Phil Wright (the creator of dotnetmagic library) Although creating a collection class is not difficult it can be time consuming. Indeed! I decided to create a VS.NET Addin to speed up that part of development.
The Addin consisted in adding a Menu Item when the user right clicks a class in the "Class View Window" in VS.NET, exposing an option to create a Strongly Typed Collection based on the selected class.
How to insert a Menu Item in the VS.NET Context Menu?
Firstly we need to work out how to add a Menu Item in the Context Menu but only in the "Class View Window". In order to get this done, we need to understand how the context Menus in VS.NET are correlated with the addins themselves. All context menus and Menus in VS.NET are office CommandBars. So we can find out the indexes by looping throughout the collection (this can be done on the OnConnnection
method from a dummy addin). Here is part of the dump I wrote to the disk (the full listing is included with the source files :-p).
...
Text Editor
XML Data
XML Schema
Device
Debug Location
Debug
Layout
Dialog Editor
Image Editor
Source Control
Analyzer
Analyzer Windows
TestContextMenu
ResultContextMenu
Project
...
Taking in consideration that we can add the menu item anywhere from the above list as a test I tried to add one on the "Code Window" which describes the context menu that pops up when we right click on the Text Editor. I am sure someone can come up with good ideas for addins using this, such as adding quick code snippets or else :-)
After trying out (and having fun) placing different Menu Items around VS.NET, I came to realise that in the class view window there are a few different Context menus, but the one I wanted is called "Class View Item" which is the one that pops up when we select the Item in the Class view tree. This is how we add a menu item in that specific Context menu:
Command command = commands.AddNamedCommand(addInInstance,
"CollectionCommand", "Create Collection",
"Adds a collection based on a type", true, 137,
ref contextGUIDS,
(int)vsCommandStatus.vsCommandStatusSupported +
(int)vsCommandStatus.vsCommandStatusEnabled);
CommandBar commandBar = (CommandBar)commandBars["Class View Item"];
_commandBarControl = command.AddControl(commandBar, 1);
Notice that I have (for simplicity) kept the same code produced by the Wizard. Yes, what you need to do is to replace the menubar name given by the wizard with any of the indexes from the above list, automatically you will have that very same menu item in the belonging command bar (whatever it may be either a menu bar or context menu).
How to check if we are selecting a Class and not other elements?
Worst case scenario I did not want to create a strongly typed collection based on a namespace name or class member. The addin works on the premises that the user right clicks on the top of the class that he/she wishes to create a collection from. *The answer for this is to use the QueryStatus
which is called every time the context menu pop up, notice that in VS.NET 2002 was a bug and this behaviour was not consistent however it's been fixed for VS.NET 2003 and it works perfectly!
So, when we the Class View Item pops up the QueryStatus
fires and we can verify if the selected item is a class by trying to cast it to a CodeClass
type, if yes we enable the adding making it visible (showing the menu item) otherwise we hide it.
...
object _object = this.VSNET.SelectedItems.SelectionContainer.Item(1);
if(_object is CodeClass)
{
status = (vsCommandStatus) vsCommandStatus.vsCommandStatusSupported |
vsCommandStatus.vsCommandStatusEnabled;
}
else
{
status = (vsCommandStatus)vsCommandStatus.vsCommandStatusInvisible;
}
...
Well, this is the presentation of the addin now we need to work on the guts of it!
Generating a Strongly Typed Collection - Design decisions
Once we have the reference from the ProjectItem instance where we want to insert the code into, we need to decide how we are going to generate the code. There are a few option available:
- Create a template and replace tagged strings
- Create the Class using the FileCodeModel exposed by the DTE object
- Use Microsoft's CodeDom
For simplicity, I've decided to use a template based solution. The other options are have your advantages (as well as disadvantages) however they also would introduce more complexity to the code. Thus, from the Magic articles written by Phil Wright I have introduced his Strongly Typed Collections with events as a template. Here is the template:
using System.Collections;
namespace #namespace#
{
public class CollectionWithEvents : CollectionBase
{
...
public delegate void CollectionClear();
public delegate void CollectionChange(int index, object value);
public event CollectionClear Clearing;
public event CollectionClear Cleared;
public event CollectionChange Inserting;
public event CollectionChange Inserted;
public event CollectionChange Removing;
public event CollectionChange Removed;
...
}
}
Then we have the template that will inherit from the CollectionWithEvents
(as per dotnetmagic tips and tricks documents):
using System.Collections;
namespace #namespace#
{
public class #myTypeCollection# : CollectionWithEvents
{
public int Add(#myType# value)
{
return base.List.Add(value as object);
}
public void Remove(#myType# value)
{
base.List.Remove(value as object);
}
public void Insert(int index, #myType# value)
{
base.List.Insert(index, value as object);
}
public bool Contains(#myType# value)
{
return base.List.Contains(value as object);
}
public #myType# this[int index]
{
get { return (base.List[index] as #myType#); }
}
}
}
Note that we have already introduced some stubs such as #namespace#
and #myType#
that will be replaced by whatever string value that the selected class uses as Name identifier. Furthermore we store these templates in the C:\Program Files\Microsoft Visual Studio .NET 2003\VC#\CSharpProjectItems; the setup already deploys these templates there :-)
In a nutshell we created a method called AddCollection() which is called when clicking on the addin menu item. The method firstly gets the reference of the current loaded project:
...
CodeClass codeClass =
(CodeClass)this.VSNET.SelectedItems.SelectionContainer.Item(1);
Project prj =
(Project)((Array)this.VSNET.ActiveSolutionProjects).GetValue(0);
...
Then, it stores the template(s) full filename path into a string:
...
string typeCollectionTemplate
= applicationObject.Solution.ProjectItemsTemplatePath(
VSLangProj.PrjKind.prjKindCSharpProject )
+ "\\MyTypeCollection.cs";
...
Finally, we do some work. Note that we use the ReplaceText three times for the custom type so we can change the namespace, class name and item type that is stored in the collection:
...
try
{
itemCollection = prj.ProjectItems.AddFromFileCopy(typeCollectionTemplate);
itemCollection.Properties.Item("Filename").Value = codeClass.Name +
"Collection.cs";
string s = itemCollection.Properties.Item("Filename").Value.ToString();
itemCollection.Open(Constants.vsViewKindCode);
itemCollection.Save("");
itemCollection.Document.ReplaceText("#namespace#",
codeClass.Namespace.Name + ".Collections",0);
itemCollection.Document.ReplaceText("#myTypeCollection#",codeClass.Name +
"Collection",0);
itemCollection.Document.ReplaceText("#myType#",codeClass.FullName,0);
}
catch(Exception e)
{
itemCollection.Delete();
System.Windows.Forms.MessageBox.Show(e.Message);
}
...
Important to notice that this addin will create the collection in a Collections relative namespace i.e.: in a project such as TestProject, the collection will be created in the TestProject.Collections
, this can be further customized for your own need. The most important thing is that there is no need to code a custom collection from scratch :-)
Using the code
After installing the addin and writting a class that needs be represented by a collection of its on, you can merely right click on the class and press "Create Collection". Here is an example:
public class AccountRecord
{
private decimal _credit = decimal.Zero;
private decimal _debit = decimal.Zero;
private string _description = string.Empty;
public AccountRecord(string description, decimal credit, decimal debit)
{
_credit = credit;
_debit = debit;
_description = description;
}
public decimal Credit
{
get{return _credit;}
}
public decimal Debit
{
get{return _debit;}
}
public string Description
{
get{return _description;}
}
}
After creating the collection we are going to be able to instantiate a AccountRecordCollection
(Generated by the addin) and procede:
AccountRecordCollection recordCollection = new AccountRecordCollection();
for(int iCount = 0;iCount < 100 ; iCount++)
recordCollection.Add(new AccountRecord("test" + iCount.ToString(),
new Decimal(iCount),new Decimal(iCount)));
foreach(AccountRecord record in recordCollection)
{
Console.WriteLine(record.Description);
Console.WriteLine(record.Credit.ToString("C"));
Console.WriteLine(record.Debit.ToString("C"));
}
...
Looking forward
As we can see it isnt very hard to create a addin to speed up code writting and no new custom class were introduced here. As I understand this is a simple addin that has helped me quite a bit, however we could make some improvements such as:
- Add a VB.NET templates support (just detect the language and add templates in the VB.NET project path)
- Customize namespace target (instead of
Root.Collections
)
- Add other templates or Smart code building using dynamic code generation (CodeDom or FileCodeModel). This can be very interesting!