CodeStash source
Codestash Codeplex site :
http://codestash.codeplex.com/
CodeStash Article Listings
Part 1 : CodeStash web site overview / High Level Architecture / How To
Install CodeStash
Part 2 : CodeStash web site Low Level Architecture
Part
3 : (This article) CodeStash addin
I beg your indulgence
This article was originally intended to be launched alongside Sacha's two articles. Unfortunately, due to circumstances beyond his control, Sacha is unable to post his articles tonight. Saying that, I did promise that there would be a posting of CodeStash, so I am uploading this part for those who are interested in how I developed the extension. Once Sacha has uploaded his articles, I will come back and update the section above to link to those articles.
I promised that there would be an announcement, and I will not disappoint. When Sacha and I developed CodeStash, which consists of a website and a Visual Studio extension, there was always the question hanging around about how we were going to host the website. We decided that we would initially offer CodeStash as a self-hosting site in order to gauge how useful people found the idea, and to get feedback on the type of features they would like to see.
Obviously, this is not an ideal situation on something which is intended to be a central repository for code snippets. Well, who should step into the fray to help us and to offer to host CodeStash for us but our very own Chris Maunder, who has kindly offered to host the site for us. To this end, we will be working with the Code Project team to turn CodeStash into a Code Project packaged snippet manager. And we need your help. We need you, the Code Project community, to try out CodeStash. We need your requests and your feedback. We need your enthusiasm and your boundless creativity.
A brief history
Around about a year ago, Code Project wunderkind
Sacha Barber mentioned that he had a project he wanted to kick off, and he
would need somebody who knew how to write Visual Studio addins to help with it.
Well this was too good an opportunity to pass up; anybody who has read any of
his articles knows what a superstar he is, and it's a great chance to work with
one of the superstars of the development world.
Anyway, time went on, but no project appeared - but this was simply down to
the massive number of articles that Sacha had on the go over this period.
Finally, around about last September/October, Sacha sent me a spec for this
project called CodeStash and asked if I was still interested - this was not
something I was going to turn down. So, towards the back end of November, we
actually started working on CodeStash.
This project has been a labour of love. Working with Sacha is an absolute
joy, he's been so patient waiting for me to deliver my side, even when I
completely changed my mind about what MVVM framework I was going to use
internally, ripped out the core and effectively started again (obviously the
fact that I decided to use Cinch might
have helped sway the matter a bit).
Well, here we are, Sacha's vision has come to fruition and it's good. My
humble contribution just builds onto the fantastic underpinnings that Sacha put
in place, which have made this a complete joy to work on and a huge amount of
fun. So, from the bottom of
my heart, I thank you Sacha. You are, without a doubt, the man.
For those who have been patiently been waiting for the next in my
Windows Phone series, I apologise most humbly. I haven't forgotten you, and I
haven't lost interest in the series. CodeStash has been pretty much my all
consuming passion for the last few months, and I hope you feel that this makes
the delay worth the wait.
So, what is CodeStash all about? As this started as the brainchild of
Sacha, I think it's only fair that I use his words to describe CodeStash.
Well CodeStash, it
is a productivity tool for a single developer or team of developers (where they
could be part of the same team).
The tool itself can be thought of as a web based centralized snippet
repository for the single developer (or team of developers), where the
develop(s) may manage useful code snippets that they wish to use to carry out
their day to day tasks.
The web site will provide the following functionality
• OpenId authorization which will work in conjunction with standard ASP .NET
forms authentication
• Tag cloud for most common snippet types, to allow
quick search lookup for these types of snippet
• The ability to search for
existing code snippets (ones that have been stored), by keyword tag, group,
language, code content
• The ability to create/delete/edit existing code
snippets
• The ability to group snippets into certain groups such that when
searching for a single code snippet all related snippets will be shown. For
example if I search for INPC I would get a C# snippet for declaring a INPC based
model/ViewModel but would possibly get a XAML snippet that would also come back
as part of the search
The web site is largely concerned with CRUD (Create/Retrieve/Update/Delete)
of code snippets, which in its self is not that tricky, or even revolutionary
(although none of the existing solutions out there deal with the concept of
grouping at all).
There is however another angle to this project, in that it is intended to
have seamless integration with Visual Studio (2010 and above), in that it will
come with a managed VS addin, that will integrate with VS by way of certain
content menus and property pages, that will allow the VS user to upload code to
CodeStash, and also
allow the VS user to search
CodeStash by the use
of a set of standard ASP .NET MVC controller actions that will expose/accept
RESTful data to the Bespoke UI that will be launched from inside of the VS.
When a user searches for a code snippets that has previously been stored
within CodeStash they
will have the option to insert the matched
CodeStash snippet
into the VS editing pane (providing the
CodeStash snippet
type matches the current VS editor file type).
Existing Solutions
Like I say the web site itself is not such a novel thing, there are a number
of existing solutions out there that do a similar job to the web site aspect of CodeStash, after all
it really just comes down to CRUD operations, but what great idea doesn't really
boil down to that in the end, even Facebook is
just CRUD with some marketing veener.
Thing was once you looked at all these existing solutions they all seemed to
have certain areas where the functionality was lacking, for example some of the
existing solutions suffered in these areas:
- Searching was very limited, if offered at all, which made it very hard
to find a snippet once you uploaded it.
- No concept of grouping, which is pretty bad I feel. These days code is
made up of many elements, you may have a HTML file a CSS file a JavaScript
file, all of which could form a logic snippet. There may of course be a
single file, but where there are multiple related files the ability to bring
these back within the same search is extremely important and aids
productivity.
- No Visual Studio integration
For completeness here is a list of some of the existing solutions out there
that we examined before embarking on the mission to create
CodeStash
That is obviously not an exhaustive list, but they were the best of the bunch
we found while doing our research into what was out there. You know there was no
point inventing a wheel if someone had already come out with a Ferrari.
So in a nutshell that's what
CodeStash is all
about. Now if any of you have bothered to click the link you will notice that is
just takes you to a codeplex web site, with not much there apart from source
code. Before we go any further with this article, it's important to realise that
the addin is just one part of this project, so I would highly recommend that you
go and read Sacha's article, and learn how to set the website up - you're going
to need to do that before the addin will work.
The "We couldn't have done this without" section
First and foremost, I have to thank Sacha. Working with him has been a complete blast and I think that this project is huge.
I would also like to thank Chris and the team at Code Project for giving us hope for the future. Honestly, their involvement has been a huge boost that kept our flagging spirits up as we were writing the articles.
What we will cover
In this article, we are going to cover some of the thought processes behind
writing a reasonably complex multi-layered Visual Studio extension. We will find
out how the commands were put together and how to interact with a well defined
REST interface from inside an addin. We will see how we can use MEF to ease the
pain of working with shared code, how to interact with internal Visual Studio
services and to provide our own property pages.
This article does not teach writing XAML code, or how to use MVVM. We will
not be covering how to use Cinch (I would heartily recommend reading Sacha's
series on the Cinch framework to get this background). It also does not aim to
cover MEF development in great depth, as all these concepts are tangential to
the aim of understanding what the CodeStash addin is designed to achieve. CodeStash
also makes use of
Daniel Grunwald's excellent AvalonEdit control to display snippets, the
sheer depth of this control means that I have to recommend reading the
original article he wrote to support it; right now I'd just like to say a
big thanks to Dan for writing this control and making it freely available.
Prerequisites
In order to write Visual Studio 2010 extensions you're going to need Visual
Studio 2010 Professional (or above), Visual Studio 2010
Service Pack 1 and the Visual Studio 2010
SP1
SDK.
A brief note in advance
In order to make this article a little less dry, and a little bit chattier, I
have taken deliberate liberties with the way sentences are formatted, along with
moving backwards and forwards in terms of pronouns. I apologise to the English
purists out there, but I wanted to take what could have been a very dry and
technical article and make it less formal, and hopefully easier to read. This is
why I slip backwards and forwards with such things as the use of Visual Studio
and VS to represent Visual Studio. In order to bridge the gap between developers
who are used to developing addins in previous versions of Visual Studio, I use
the terms addin and extensions interchangeably, but they are both referring to
an extension that can be installed via the extension manager.
The addin is, effectively three parts. The first part is the menus used to
show our CodeStash functionality. The second part are the views that comprise
saving and inserting snippets, and the third part is the settings that the user
needs to actually use CodeStash. In this article, we are going to take a look at
how these all come together, picking up some tips and tricks for doing some cool
Visual Studio addin stuff, and how to interact with the CodeStash website. This
is not going to be a line by line breakdown of every part of the extension -
frankly, there's so much there that this article would end up being one vast
code dump with your will to live draining away. The article is, also, not going
to touch on every cool little trick we have put into the code base; hopefully,
once you see the code in action, you'll want to explore the code for yourself.
This article should give you enough information to continue the voyage of Visual
Studio Package discovery all by yourself.
The extension in action
When text is selected in the editor window, the Save snippet option is
enabled to let our intrepid users save their snippets.
Saving a snippet is as easy as filling in a simple dialog and clicking Save.
This dialog shows snippet retrieval in action.
It's easy to see what code is present for a snippet in the snippet view.
Here we can see that CodeStash is listed in the About Visual Studio dialog.
Running CodeStash
A brief note on debugging addins. Starting the project up in Debug mode can
take a lot of time when you debug a Visual Studio extension. What I like to
do is to start the project using "Start without Debugging", and then attach
the debugger to the Visual Studio that's running the extension. Try it
yourself, you'll find that it saves you a load of time.
The config file has the following settings.
- EncryptionEnabled. This must be set to the same value as is set in the
CodeStash website web.config file.
- RestAddress. This is the location that the RESTful services are located.
By default, this is set to
http://localhost:8300/Rest/. Change the
localhost:8300 to the address of the website you deploy the application to.
- CodeSnippetAddress. This is the location of the code snippet address
RESTful services. By default, this is set to
http://localhost:8300/CodeSnippet/..
Change the localhost:8300 to the address of the website you deploy the
application to.
The CodeStash website must be running before this extension can communicate
with it, so I'd recommend starting that before you attempt to run the addin.
When you run the extension from within the editor, an experimental instance of
Visual Studio will open up - you may need to adjust where it's fired from in the
Debug tab (in the CodeStash.Addin properties window), depending on whether you
are using Visual Studio on 32 bit or 64 bit version of Windows.
When you run CodeStash, your first real interaction with it is probably going
to be via the context menu commands, so let's start by seeing how they actually
work.
Wiring up the commands
One of the biggest issues we faced when creating the addin was that it has to
work in multiple different editor windows, with more than one menu item grouped
into a sub-menu. With the best will in the world to Microsoft, the documentation
surrounding how to actually do this is scattered in lots of different locations
and is downright arcane in places.
In order to get the plumbing in place to hook the commands up in the way we
wanted, we had to battle with the innermost workings of something known as the
Visual Studio Command Table (VSCT). When you create a Visual Studio addin,
you'll see a file that ends with the extension .vsct; this is
the file that supports this feature. When you open it up, you'll see that it's
an XML file, and it simply describes the layout and appearance of the command
items in a VSPackage. Well, I say simply, but getting your head around it is
anything other than straightforward. Here, I'm going to lay out what the file
looks like, and then I'll deconstruct it so that the other parts of the command
structure make more sense (hopefully).
="1.0" ="utf-8"
<CommandTable xmlns="http://schemas.microsoft.com/VisualStudio/2005-10-18/CommandTable" xmlns:xs="http://www.w3.org/2001/XMLSchema">
-->
<Extern href="vsshlids.h"/>
<Commands package="guidCodeStash_AddinPkg">
<Groups>
<Group guid="CodeStashGrouping" id="CodeStashGroupedMenus" priority="0x0600">
<Parent guid="guidSHLMainMenu" id="IDM_VS_CTXT_CODEWIN"/>
</Group>
<Group guid="CodeStashGrouping" id="SubMenuGroup" priority="0x0602">
<Parent guid="CodeStashGrouping" id="SubMenu" />
</Group>
</Groups>
<Menus>
-->
<Menu guid="CodeStashGrouping" id="SubMenu" priority="0x0200" type="Menu">
<Parent guid="CodeStashGrouping" id="CodeStashGroupedMenus" />
<Strings>
<ButtonText>Code Stash</ButtonText>
<CommandName>CodeStash</CommandName>
</Strings>
</Menu>
</Menus>
<Buttons>
-->
<Button guid="CodeStashGrouping" id="SaveSnippetId" priority="0x0100" type="Button">
<Parent guid="CodeStashGrouping" id="SubMenuGroup" />
<CommandFlag>DefaultDisabled</CommandFlag>
<Strings>
<CommandName>SaveSnippetId</CommandName>
<ButtonText>Save snippet</ButtonText>
</Strings>
</Button>
<Button guid = "CodeStashGrouping" id="InsertSnippetId" priority="0x101" type="Button">
<Parent guid="CodeStashGrouping" id="SubMenuGroup"/>
<CommandFlag>DynamicVisibility</CommandFlag>
<Strings>
<CommandName>InsertSnippetId</CommandName>
<ButtonText>Insert snippet</ButtonText>
<ToolTipText>Insert the snippet from CodeStash into the editor window.</ToolTipText>
</Strings>
</Button>
-->
</Buttons>
</Commands>
<CommandPlacements>
<CommandPlacement guid="CodeStashGrouping" id="CodeStashGroupedMenus" priority="0x0600">
<Parent guid="HtmlEditorWindows" id="IDMX_HTM_SOURCE_BASIC"/>
</CommandPlacement>
<CommandPlacement guid="CodeStashGrouping" id="CodeStashGroupedMenus" priority="0x0600">
<Parent guid="HtmlEditorWindows" id="IDMX_HTM_SOURCE_HTML"/>
</CommandPlacement>
<CommandPlacement guid="CodeStashGrouping" id="CodeStashGroupedMenus" priority="0x0600">
<Parent guid="HtmlEditorWindows" id="IDMX_HTM_SOURCE_SCRIPT"/>
</CommandPlacement>
-->
<CommandPlacement guid="CodeStashGrouping" id="CodeStashGroupedMenus" priority="0x0600">
<Parent guid="CssEditorWindows" id="IDMX_HTM_SOURCE_CSS"/>
</CommandPlacement>
<CommandPlacement guid="CodeStashGrouping" id="CodeStashGroupedMenus" priority="0x0600">
<Parent guid="XamlEditorWindows" id="IDMX_XAML_SOURCE_BASIC"/>
</CommandPlacement>
</CommandPlacements>
<Symbols>
-->
<GuidSymbol name="guidCodeStash_AddinPkg" value="{857c13ce-c509-4244-9216-59b112462c5f}" />
-->
<GuidSymbol name="CodeStashGrouping" value="{4f6378f6-4249-474b-bd22-d5ecf4996156}">
<IDSymbol name="SubMenu" value="0x1001"/>
<IDSymbol name="SubMenuGroup" value="0x1000"/>
<IDSymbol name="InsertSnippetId" value="0x0101"/>
<IDSymbol name="CodeStashGroupedMenus" value="0x1020" />
<IDSymbol name="SaveSnippetId" value="0x0100" />
</GuidSymbol>
-->
<GuidSymbol name="HtmlEditorWindows" value="{d7e8c5e1-bdb8-11d0-9c88-0000f8040a53}">
<IDSymbol name="IDMX_HTM_SOURCE_BASIC" value="0x32" />
<IDSymbol name="IDMX_HTM_SOURCE_HTML" value="0x33" />
<IDSymbol name="IDMX_HTM_SOURCE_SCRIPT" value="0x34" />
-->
<IDSymbol name="IDMX_HTM_SOURCE_ASMX_CODE_VB" value="0x39" />
</GuidSymbol>
<GuidSymbol name="CssEditorWindows" value="{A764E896-518D-11D2-9A89-00C04F79EFC3}">
<IDSymbol name="IDMX_HTM_SOURCE_CSS" value="0x0102"/>
</GuidSymbol>
<GuidSymbol name="XamlEditorWindows" value="{4C87B692-1202-46AA-B64C-EF01FAEC53DA}">
<IDSymbol name="IDMX_XAML_SOURCE_BASIC" value="0x0103"/>
</GuidSymbol>
</Symbols>
</CommandTable>
I appreciate that there's a lot going on there,
and that it looks quite daunting but, hopefully, by the time you reach the end
of this section it will make a lot more sense.
Near the top of this file, the addin links to
an external resource. It does this in the line:
<Extern href="vsshlids.h"/>
As we can see, this line looks like it's
importing a C/C++ header file. Well, surprise surprise that, in effect, is
exactly what it's doing. In the same way that we use header files in C/C++ to
provide definitions, you do the same for Visual Studio extensions. This betrays
the fact that you used to have to write extensions in C or C++, so it's a
convenient way to get at the standard Visual Studio shell ids (that's what
vsshlids stands for). You don't have to use this file - we could do this
entirely in code, as we'll see later on, but as it's there and it's convenient,
we'll use it.
If you look in the project, you won't actually see this
file anywhere; it's stored in a predefined location that the compiler knows
to retrieve the include files from (path to program files directory\Microsoft Visual Studio 2010
SDK\VisualStudioIntegration\Common\Inc).
What we are interested in, in this file, are
the following:
// Guid for Shell's group and menu ids
DEFINE_ Guid (guidSHLMainMenu,
0xd309f791,0xd309f791, 0x903f, 0x11d0, 0x9e, 0xfc, 0x00, 0xa0, 0xc9, 0x11, 0x00, 0x4f);
#define IDM_VS_CTXT_CODEWIN 0x040D
These two parts represent the Guid and command
ids that are needed to hook up to the context menu in the code editor window.
This is an important part of getting to know how to add commands to windows in
Visual Studio - each window is represented by a Guid, and each command is
represented by an Id. Every command that you add to an addin
will be represented by a Guid and an Id. As I said before, you could get away without importing
this command file - all you have to do is drop the Extern
element, and add the
following to the Symbols
section at the bottom:
<GuidSymbol name="guidSHLMainMenu" value="{D309f791-903f-11d0-9efc-00a0c911004f}">
<IDSymbol name="IDM_VS_CTXT_CODEWIN" value="0x040D" />
</GuidSymbol>
I'm going to jump around the vsct file a bit
now because we need to take a look at the Buttons
and see how they come into
play. This will help to make the other sections of this file more apparent.
CodeStash will add two menu options to the
context menus in the editor windows (I'm going to be coming back to this one in
a bit as this isn't as easy as you'd hope). In order to add these menus, we have
to create Button
elements. Let's take another look at this section:
<Buttons>
-->
<Button guid="CodeStashGrouping" id="SaveSnippetId" priority="0x0100" type="Button">
<Parent guid="CodeStashGrouping" id="SubMenuGroup" />
<CommandFlag>DefaultDisabled</CommandFlag>
<Strings>
<CommandName>SaveSnippetId</CommandName>
<ButtonText>Save snippet</ButtonText>
</Strings>
</Button>
<Button guid = "CodeStashGrouping" id="InsertSnippetId" priority="0x101" type="Button">
<Parent guid="CodeStashGrouping" id="SubMenuGroup"/>
<CommandFlag>DynamicVisibility</CommandFlag>
<Strings>
<CommandName>InsertSnippetId</CommandName>
<ButtonText>Insert snippet</ButtonText>
<ToolTipText>Insert the snippet from CodeStash into the editor window.</ToolTipText>
</Strings>
</Button>
-->
</Buttons>
Each item that is going to appear in our
CodeStash menu is represented as a Button
element. Again, you can see that we have identified the button uniquely with a
Guid and an Id, but where do they come from? The answer lies in the
Symbols
section in the vsct file. In this section, we have added several
GuidSymbol
elements. The GuidSymbol
is created with a symbolic name and a Guid as the value field. The symbolic name
is what we use elsewhere when we want to refer to the Guid; effectively you can
think of it as an alias, so when you see CodeStashGrouping
in any
other element, what it actually means is use the Guid {4f6378f6-4249-474b-bd22-d5ecf4996156}
.
(When we look at the way we actually hook the buttons up to code, we'll see how
this Guid becomes relevant).
The id
refers to an
IDSymbol
that is contained inside the
GuidSymbol
group. So, in the case of our
buttons, we look at the
IDSymbol
that exists inside the CodeStashGrouping
element. Again, this is an alias, so when you see id="..."
, what is really being
used is the value referred to by that id. Note that when you use the Guid
from a GuidSymbol
the id that you use must be listed inside that particular GuidSymbol
element. The following diagram should make this a little bit clearer.
Each Button
has a Parent. We're
going to come back to this one a bit later, as this helps us to determine the
hierarchy of how the buttons are laid out.
The Strings
section is the part we're actually going to set up the text that
appears in the menu (ButtonText
), and any optional Tooltip text
(ToolTipText
) that we want to appear.
At this point, I think we need to take a quick
break from the vsct file. We need to see how this file actually fits into the
bigger picture, after all there's no real code in there - all we have is a
description of what commands we have available to play with.
Core command files
When you create a Visual Studio addin, the
addin template creates a lot of files. There are three files that we are going
to concentrate on right now.
"Gosh Pete, they even have a file called Guids in there. They must be
obsessed with them" I hear you say. Well, yes, I suppose you're right. The
Guid.cs file represents the same Guids that we've had to create in the
Symbols
section in
the vsct file. See, I told you we'd come back to this. It looks like this:
using System;
namespace CodeStash.Addin
{
static class GuidList
{
public const string guidCodeStash_AddinPkgString = "857c13ce-c509-4244-9216-59b112462c5f";
public const string CodeStashGroupingString = "4f6378f6-4249-474b-bd22-d5ecf4996156";
public static readonly Guid CodeStashGrouping = new Guid(CodeStashGroupingString);
public const string PropertyPageGuid = "D6B8D576-8A4F-402A-8D20-B1FD98322EEC";
}
As this is such a handy place to store Guids, we have sneaked an extra one in
here that has nothing to do with the commands. The Guid at the bottom maps to a
property page that we have added into the settings dialog in Visual Studio. The
ability to add your own property pages is just one of the many cool things that
you can do with a Visual Studio addin.
Something that may surprise you is how few Guids we actually have in this
file. After all, we added a lot more GuidSymbol
elements in the vsct file. The reason for this is that not every GuidSymbol
entry relates to our CodeStash commands - in fact, we only need two of the
Guids. The other Guids relate to internal Visual Studio elements which we will
use later on when we are wiring our commands up into the different editor
windows.
At this stage, you've leaped ahead of me and realised that the PkgCmdID.cs
file actually maps to the command ids laid out in the
IDSymbol
elements.
namespace CodeStash.Addin
{
static class PkgCmdIDList
{
public const uint SaveSnippetId = 0x100;
public const uint InsertSnippetId = 0x101;
};
}
Not much to worry about there. Again, it doesn't matter that we have far
fewer ids in this list than we have
IDSymbol
elements, by now you'll have figured
out that these other ids have something to do with the ways that the commands
are placed inside Visual Studio.
One thing you will have noticed about these two files is that they merely
define constants. This would indicate that other code is going to use them, and
in the case of this package, the file we are interested in is
CodeStash.AddinPackage.cs. Rather than listing the whole file out, I'm going to
concentrate o how we went
about creating the CodeStash menus, and actually getting them to appear on the
screen.
If you take a look at this class, you'll see that it inherits from Package
.
You can think of this as the entry point into the addin, so it stands to reason
that this is the file we are going to use to actually hook code into the command
table. Fortunately, there is an initialisation routine that we can use to do
exactly this:
protected override void Initialize()
{
base.Initialize();
GetDTE();
OleMenuCommandService mcs = GetService(typeof(IMenuCommandService)) as OleMenuCommandService;
if (null != mcs)
{
CommandID menuCommandID = new CommandID(GuidList.CodeStashGrouping, PkgCmdIDList.SaveSnippetId);
CommandID searchSnippetCommandID = new CommandID(GuidList.CodeStashGrouping, PkgCmdIDList.InsertSnippetId);
saveSnippetMenu = new OleMenuCommand(AddSnippetMenuItemCallback, menuCommandID);
saveSnippetMenu.BeforeQueryStatus += OnQuerySaveSnippet;
mcs.AddCommand(saveSnippetMenu);
MenuCommand searchMenuItem = new MenuCommand(SearchSnippetsMenuItemCallback, searchSnippetCommandID);
mcs.AddCommand(searchMenuItem);
}
}
We will come back the the call to GetDTE
in a little while. Before then, we
will discuss the actual menu creation code.
The Package
class provides the ability to get access to any of
the services that Visual Studio makes available through a call to GetService
.
Simply pass in the type of service that is needed, and it's available for us to
use. So, in the case of adding menus, we need to get an instance of
IMenuCommandService
(GetService
returns an object, so we have to cast it to an
appropriate type).
Now we can see that we create two CommandID
instances, and at this point the
parts that we've discussed before should start to become apparent. Each
CommandID
is instantiated with the Guid from Guids.cs and the id from
PkgCmdIDList.cs which, as we've already discussed, match the elements defined in
the vsct file. See, I told you that this would all start to come together.
Anyway, creating CommandID
instances isn't enough on its own. We actually
need to do something with them, and what we do is use these to create the actual
MenuCommand
and OleMenuCommand
instances (each of which have a parameter that
accepts a callback routine which we use to actually perform the operation that
we want from the menu command).
Now, you may be wondering why we have two different types of menu here. The
reason is actually pretty simple; if we don't need to have Visual Studio
automatically enable and disable the menu for in response to things happening,
then you should use a
MenuCommand
. If, however, we do need this facility then we have to
use OleMenuCommand
(we use the OleMenuCommand.BeforeQueryStatus
event to allow us to hook our enable/disable logic up). Once we have created our
menu commands, it's a simple matter to add the commands to the menu command
service using AddCommand
.
If we ran the code based on what I've just described, we would get two menu
items, but they would be in the main editor window context menu, but that's not
what we want. What we really want to do is have a CodeStash submenu off the
context menu, it's a great way to make your commands stand out in a myriad of
other commands. Now, the documentation on how to do this is an absolute
nightmare to find and get your head around - I'm sorry Microsoft, but if you
want people to be able to understand how this all hangs together, you have to
get in the habit of providing decent samples with decent naming conventions
(this is why we don't use guid..... for our menus in CodeStash).
So, how do we actually go about creating the submenu? Well, it's back to the
vsct file I'm afraid. Don't worry, there's not much more to cover there. By now
you have enough information for me to be able to introduce some more "advanced"
concepts without causing headaches. I'll break this down a little bit at a time,
starting with the way we hook the menu to the editor window context menu.
<Group guid="CodeStashGrouping" id="CodeStashGroupedMenus" priority="0x0600">
<Parent guid="guidSHLMainMenu" id="IDM_VS_CTXT_CODEWIN"/>
</Group>
Right, there we've created a group that we are going to make a child of the
editor context window. It doesn't have a physical representation, it merely acts
as a way to hook the menu in.
<Menu guid="CodeStashGrouping" id="SubMenu" priority="0x0200" type="Menu">
<Parent guid="CodeStashGrouping" id="CodeStashGroupedMenus" />
<Strings>
<ButtonText>CodeStash</ButtonText>
</Strings>
</Menu>
As you can see, the Menu
element is linked to the Group
we listed above via
the Parent
element. OK, so we have a Menu
in the command table, but how do we
actually get our menus hooked into it? It should come as no surprise that we
need to add another Group
that has this Menu
as the Parent
.
<Group guid="CodeStashGrouping" id="SubMenuGroup" priority="0x0602">
<Parent guid="CodeStashGrouping" id="SubMenu" />
</Group>
Finally, our Button
s are parented to
this Group
. When you need to add submenus, this mechanism of
linking Group
s and Menu
s via a child/parent
relationship is the way to do it.
OK, so if we run the extension now it will show our CodeStash submenu and our
buttons under it, right? Well, that really depends which editor you open up
inside Visual Studio. There isn't one standard editor and they all have
different context menus, so our menu will only display in the standard text
editor window, such as the C# editor window. Well, this isn't much use for an
addin that is meant to work with just about any language you can host in Visual
Studio. There must be a way that we can add our menus to the other editor
windows. Well, fortunately for us, there is.
If you look at the
Symbols
section, you can see that we've created other GuidSymbol
elements. Each editor window has a Guid and command id associated with it, so we
have added these into this section. With this information, we can place our menu
against other editor windows through another vsct feature called a
CommandPlacement
.
<CommandPlacement guid="CodeStashGrouping" id="CodeStashGroupedMenus" priority="0x0600">
<Parent guid="HtmlEditorWindows" id="IDMX_HTM_SOURCE_BASIC"/>
</CommandPlacement>
As you can see here, we have effectively said
"place the Menu that has the CodeStashGrouping Guid and CodeStashGroupedMenus in
the context menu for the window that has the Guid identified by
HtmlEditorWindows and the id is IDMC_HTM_SOURCE_BASIC". I know that's a little
wordy, but that is what is happening internally. Do this with all the different
editor windows and ids that we can find, and our menus should appear.
Again, Microsoft, could you please make your
documentation on this clearer? Finding this information should be a lot more
straightforward, and should not involve having to sacrifice a cucumber on a cold
December night, wearing nothing but a loin cloth and a scarf made of pocket
lint.
The Visual Studio experience
While it's handy to have the ability to create Visual Studio addins, if there
was no way to actually interact with Visual Studio itself, addins would be of
little value. Fortunately for us, we can get access to Visual Studio
functionality through the Package class, using GetService
to retrieve the
services that provide this functionality. If you recall, in the previous
section, I said that CodeStash has a method called GetDTE
; this is the point at
which we choose the functionality we are interested in. In our case, we want to
get access to the Design Time Environment (DTE) which lets us get to the
documents in the editor, and the status bar for providing status information.
private void GetDTE()
{
EnvDTE.DTE provider = (EnvDTE.DTE)GetService(typeof(EnvDTE.DTE));
MEFPartsResolver.Instance.Resolve<IDocumentService>().SetDTE(provider);
IVsStatusbar statusBar = (IVsStatusbar)GetService(typeof(SVsStatusbar));
MEFPartsResolver.Instance.Resolve<IStatusBarService>().SetStatusBar(statusBar);
}
The addin makes a fair use of interfaces, so we use MEF to resolve
to the physical implementation of these interfaces. We will cover more on this
shortly.
One of the nicer features provided to addin developers is the ability to add
your own settings pages to the standard Visual Studio settings dialog. With this
ability, it's straightforward to provide a seamless designer experience, making
it look like the addin belongs inside VS. We use this in CodeStash to store the
login information for the current user.
Adding the page consists of three
parts, creating the UI for the property page, adding a DialogPage
class that
shows the page and registering it with the package. I won't cover the creation
of the UI as that's simply a user control that gets displayed. The interesting
parts are the DialogPage
and the registration part. The DialogPage
is
implemented in CodeStash.Addin.CodeStashSettingsPropertyPage (look in the
PropertyPage folder in CodeStash.Addin).
[Guid(GuidList.PropertyPageGuid)]
public class CodeStashSettingsPropertyPage : DialogPage
{
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
protected override System.Windows.Forms.IWin32Window Window
{
get
{
CodeStashSettingsView settings = new CodeStashSettingsView();
settings.Location = new System.Drawing.Point(0, 0);
return settings;
}
}
}
It probably comes as no surprise to you now to see that the property page has
a Guid associated with it. Yup, Visual Studio uses that Guid as an id when it
has to look this page up, and when we register the page. This Guid is one that
we create, and I have chosen to place it in GuidList.cs.
"Ah hah Pete. That's all very straightforward, but how does Visual Studio
know about our settings page?" I hear you ask (or that could just be the voices
in my head). Well, registering settings with an addin is as simple as decorating
the package class with a simple attribute.
[ProvideOptionPageAttribute(typeof(CodeStashSettingsPropertyPage), "CodeStash", "Settings", 101, 1000, true)]
If you run CodeStash, open up the VS settings dialog using Tools >
Options... You will see that the CodeStash settings are in there as
CodeStash > Settings (the names the settings pages appear as,
and the key they appear under are specified in the attribute).
Obviously, if the user hasn't entered their credentials and they try to run
CodeStash, they aren't going to get very far with saving or retrieving snippets,
so we need a way to guide them to enter their credentials. We can't rely on them
reading documentation, and we don't want them to only find out they can't get to
CodeStash by opening only after they've opened up one of the snippets windows.
This means that we need to guide them to the settings page so that they can
enter the details. Fortunately, getting access to our settings page
programatically couldn't be easier. All we need to do is get a reference to the
DTE, and open up the Options window, passing in the Guid of our
property page.
private void ShowCodeStashSettings()
{
EnvDTE.DTE dte = (EnvDTE.DTE)GetGlobalService(typeof(SDTE));
dte.ExecuteCommand("Tools.Options", GuidList.PropertyPageGuid);
}}
That's it, if you know the Guid of a settings page, you can access it using
that simple command (although it's not well documented that you can do this,
again a lot of digging around was needed to find this).
So, where exactly are we storing this login information? Well, surprise
surprise, we are using isolated storage to store this data. There are two classes
that are of interest to us here. The first class, LoginDetails
, is
the "model" containing the login information. The second,
CredentialManager
, is responsible for saving the settings to isolated
storage, and loading them back in again from isolated storage. For security
purposes, the code uses DESCryptoProvider
to encode and decode the
file. (To find these files, look in the Login directory in
CodeStash.Addin.Core.dll).
Saving the file involves serializing the LoginDetails
then
converting this to a byte array, which is then saved to disk.
public static void Save(LoginDetails login)
{
login.Validate();
XmlSerializer serializer = new XmlSerializer(typeof(LoginDetails));
using (StringWriter sw = new StringWriter())
{
serializer.Serialize(sw, login);
SaveFile(ASCIIEncoding.ASCII.GetBytes(sw.ToString()));
}
}
private static void SaveFile(byte[] fileData)
{
using (IsolatedStorageFile store = GetStore())
{
using (IsolatedStorageFileStream ifs = GetStream(store, FileMode.OpenOrCreate))
{
using (DESCryptoServiceProvider des = GetCryptoProvider())
{
using (CryptoStream crypt = new CryptoStream(ifs, des.CreateEncryptor(), CryptoStreamMode.Write))
{
crypt.Write(fileData, 0, fileData.Length);
crypt.Close();
}
}
}
}
}
Loading the file is simply a matter of reading the file back in, decrypting
the file stream, and deserializing the data back to an instance of LoginDetails
.
If the details haven't previously been saved, a new instance of LoginDetails
is created.
public static LoginDetails Load()
{
XmlSerializer serializer = new XmlSerializer(typeof(LoginDetails));
string fileContents = ReadFile();
if (!string.IsNullOrWhiteSpace(fileContents))
{
using (StringReader sr = new StringReader(fileContents))
{
return (LoginDetails)serializer.Deserialize(sr);
}
}
return new LoginDetails();
}
private static string ReadFile()
{
using (IsolatedStorageFile store = GetStore())
{
if (!store.FileExists(FileName))
{
return string.Empty;
}
using (IsolatedStorageFileStream ifs = GetStream(store, FileMode.Open))
{
using (DESCryptoServiceProvider des = GetCryptoProvider())
{
using (CryptoStream crypt = new CryptoStream(ifs, des.CreateDecryptor(), CryptoStreamMode.Read))
{
using (StreamReader reader = new StreamReader(crypt))
{
string retVal = reader.ReadToEnd();
crypt.Close();
return retVal;
}
}
}
}
}
}
CodeStash - bringing two worlds together
This would seem to be a fairly opportune point to talk about credential
management, and how the addin works with the actual CodeStash services. If you
have read Sacha's article on the CodeStash website (and if you haven't, why not
- go read it, I'll wait for you to come back) you'll know that CodeStash uses
REST to do pretty much all of the heavy lifting with regards to the CodeStash
functionality. Well, the addin makes extensive use of these same REST services -
which means that it needs to use exactly the same credentials as you would use
on the website, and it abides by the same credential encryption rules as
specified in the website.
OK, that's pretty wordy. What exactly does it mean? Well, now that you've
read Sacha's article, you know that there's a setting that says whether or not
the credential information is encrypted. This setting needs to be the same in
both places, and our route into it is managed by the EncryptionHelper
class in
the CodeStash.Common assembly reading the EncryptionEnabled key in the
configuration file. This setting determines whether or not the user credentials
are encrypted, and should be set in the configuration file in the addin as well
as web.config.
If you take a look in the constructor for the EncryptionHelper
class, you will see that it looks like this:
static EncryptionHelper()
{
encryptionEnabled = CodeStash.Common.Helpers.ConfigurationSettings.EncryptionEnabled;
}
Seems innocent enough, doesn't it? Well, that one line requires some gnarly
work going on in the background to ensure that the Addin and the website can
both use config files to retrieve the value from.
As you're aware, you can associate a config file with a .NET executable, and
as long as it's copied into a location such as the output path, you can access
the values in it. I'm not going to rehash stuff that you know inside out here,
other than to say that the key is that the config file is associated with the
executable, not a DLL. Now, this could present us with a bit of a problem
because we can't assign a config file to Visual Studio and start working with
it. But, as you can see, we are just using a single property to get the
configuration value that we need. There must be something else going on in
there, mustn't there?
Well, yes there is. In order to associate the config file with the DLL, we
name it CodeStash.Addin.Dll.Config, mimicking the naming
convention of exe config files. We then do a little bit of trickery to get the
value out of the config file in the file
CodeStash.Common.Helpers.ConfigurationSettings.
private static Configuration configuration;
private static string RetrieveSetting(string setting)
{
string value = ConfigurationManager.AppSettings[setting];
if (string.IsNullOrWhiteSpace(value))
{
GetConfiguration();
KeyValueConfigurationElement ret = configuration.AppSettings.Settings[setting];
if (ret != null)
{
value = ret.Value;
}
}
return value;
}
private static void GetConfiguration()
{
if (configuration != null) return;
string location = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "CodeStash.Addin.Core.dll");
configuration = ConfigurationManager.OpenExeConfiguration(location);
}
OK, so what our configuration settings properties do is call RetrieveSetting
.
If the setting they are after has a value, it means we are pulling the value
from the web.config file. If it's empty, we need to delve a bit deeper, so
the extension calls GetConfiguration
, which uses the standard .NET
ConfigurationManager.OpenExeConfiguration
to open our addin config file. Once we
have opened the configuration file, it's a simple matter to retrieve the setting
and return it. In order to avoid having to get the config file every time, we
assign it as we are opening it.
MEF, Sacha and the Big Lebowski
I've already said that one of the big holdups with delivering the addin was
down to changing my mind about the MVVM framework that underpinned it.
Initially, I hadn't intended to use a third party framework - and I wrote a lot
of functionality to support this. There was always that nagging doubt at the
back of my mind, though, that it wasn't right doing this so, after some soul
searching, navel gazing and toe wrestling, I decided to use an existing
framework. I looked at using my own Goldlight framework but decided, in the end,
that this would be a good time to use Sacha's Cinch framework.
One of the big things about Cinch is that it works with MEF, and works nicely
with a project I had some small part in developing,
MEFedMVVM. Visual
Studio makes heavy use of MEF, so it seemed that hooking it together would be a
simple matter and progress would shoot ahead. Hah! I say. Hah! No matter what I
tried, I couldn't get MEFedMVVM to play nicely inside Visual Studio - Sacha
offered to look at it for me, and I gladly accepted his offer. Yup, you guessed
it, Sacha ran into exactly the same problems that I did, and we came to the
conclusion that our MEF loader was conflicting with the fact that Visual Studio
is heavily MEF based, so it has its own MEF load infrastructure. Well, never say never
is Sacha's motto (it is now that I've assigned it to him).
Eventually, Sacha decided to take matters into his own hands and he wrote a
new MEF export provider for CodeStash, which would work alongside Cinch. You can
find out how he implemented this by looking at the files in the MEF
folder in CodeStash.Addin. From the CodeStash point of view,
the really interesting point can be seen in the file MEFPartsResolver
.
In here, the MEFfed up services that we want to use inside CodeStash are
resolved and hooked up. This class is then called by other parts of the code
base, whenever they need access to one of the services.
Time for a REST
Well, it's all very well doing all this plumbing work with the addin, but we
have to actually be able to communicate with the website, and Sacha has provided
some very handy RESTful services for the addin to hook into. Fortunately for us,
actually hooking into these services is incredibly easy. At the heart of this,
the addin uses a single class, CodeStashRestBase
, which provides
the core implementation that we will use. Let's take a look at one call that the
addin makes, and see how it actually calls the REST code.
protected byte[] Search(string searchText, SearchType searchType, CodeSnippetVisibility visibility, string[] tags, int pageSize, int pageNumber)
{
JSONSearchInput input = new JSONSearchInput(
openId, emailAddress, password, searchType,
searchText,
pageNumber,
pageSize,
visibility,
tags
);
return CallService(input, CodeStash.Common.Helpers.ConfigurationSettings.RestAddress, "Search");
}
private byte[] CallService<T>(T input, string address, string methodToCall)
{
values = new NameValueCollection();
Utilities.AddValue(values, "input", input);
WebClient client = new WebClient();
return client.UploadValues(string.Format("{0}{1}", address, methodToCall), values);
}
When we attempt a search from the addin, we build up a json type (the json
types are defined in the CodeStash.Common assembly) which we pass into a very
handy little method which actually calls the service, getting the address of the
service from the config file. All of the CodeStash json types take the open id,
email address and password as the first three parameters.
When the code does the search, we get a byte array back which we need to
convert back into a type we can use in the addin, after all, a byte array isn't
that easy to read. To get the type, we use a handy little conversion routine, so
in the search case, our code is converted out like this:
return Utilities.GetValue<JSONPagesSearchResultCodeSnippet>(base.Search(searchText, searchType, visibility, tags, PageSize, pageNumber));
And this method is as simple as:
internal static T GetValue<T>(Byte[] results) where T : class
{
using (MemoryStream ms = new MemoryStream(results))
{
jss = new DataContractJsonSerializer(typeof(T));
return (T)jss.ReadObject(ms);
}
}
As this plainly demonstrats, all we are doing is creating a MemoryStream
encapsulating the stream that has come back from the REST service. We then use
this to reconstruct a proper json type from the stream. The code is fairly
straightforward, and should be pretty familiar to any reasonably experienced
.NET developer.
Note: The REST services are designed to be accessed via MEF, so when we
look at the ViewModel code you'll see it being accessed via an interface. All
interaction with the REST services are implemented in the interface.
public interface IRestService
{
int PageSize { get; set; }
JSONGroupingResult RetrieveGroups();
JSONLanguagesResult RetrieveLanguages();
JSONCodeSnippetAddSingleResult AddCodeSnippet(
string actualCode,
string categoryName,
int languageId,
string language,
string tags,
string description,
string title,
int? groupId,
string groupName,
CodeSnippetVisibility visibility);
JSONPagesSearchResultCodeSnippet Search(
string searchText,
SearchType searchType,
CodeSnippetVisibility visibility,
string[] tags,
int pageNumber
);
}
As you can see from this, there's not a lot to the interface. The addin
doesn't actually have to do that much to talk to the website, so the interface
is pretty simple.
"Enough with the waffle Pete, show me the screens. If you don't, I will rend
thee in the gobberwarts with my blurglecruncheon". OK, I appreciate that we've
had to cover a lot of background functionality here, and it can seem a little
bit disjointed. How does this relate to showing some screens, interacting with
the REST services, and actually doing stuff with the windows in Visual Studio?
Sit back, relax and enjoy the ride. We're about to start pulling this together.
You might be wondering where all the WPF stuff in this is. After all, Sacha
and I are big fans of WPF, and I've already said that we are using a MVVM
framework here, yet I haven't talked about it at all. Let's take a look at the
functionality we use for actually saving the snippets into the CodeStash database.
Saving snippets
When the user selects some text in an editor window in Visual Studio, the
Save snippet menu becomes enabled via the
BeforeQueryStatus
event. Clicking Save snippet creates a model window using the
command:
new AddSnippetView().ShowModal();
If you open up the view windows though, you'll see that we haven't created a
straightforward WPF dialog. As we want our application to behave consistently
with Visual Studio, we use a different base for our window based on
DialogWindow
.
<ui:DialogWindow x:Name="Window"
x:Class="CodeStash.Addin.Views.AddSnippetView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:enum="clr-namespace:CodeStash.Common.Enums;assembly=CodeStash.Common"
xmlns:extension="clr-namespace:CodeStash.Addin.Extensions"
xmlns:local="clr-namespace:CodeStash.Addin"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="clr-namespace:Microsoft.VisualStudio.PlatformUI;assembly=Microsoft.VisualStudio.Shell.10.0"
xmlns:vsfx="clr-namespace:Microsoft.VisualStudio.Shell;assembly=Microsoft.VisualStudio.Shell.10.0"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:core="clr-namespace:CodeStash.Addin.Core;assembly=CodeStash.Addin.Core"
xmlns:PresentationOptions="http://schemas.microsoft.com/winfx/2006/xaml/presentation/options"
SizeToContent="Height"
Width="500"
Background="{DynamicResource {x:Static vsfx:VsBrushes.ToolWindowBackgroundKey}}"
Foreground="{DynamicResource {x:Static vsfx:VsBrushes.ToolWindowTextKey}}"
ShowInTaskbar="False"
Title="Save Snippet"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
...
</ui:DialogWindow>
Please don't be put off if you're not comfortable with XAML, I'll give a
quick run through of what this code means, and then I'll cover the
Background
and Foreground
lines. If you're an expert with
XAML, please feel free to take a comfort break at this point while we skim
through this code.
Effectively, this code tells the compiler to create a class that inherits
from DialogWindow
. The class it creates is called
AddSnippetView
and then some XML namespaces (xmlns
)are
added, in much the same way we would add a using
statement to a
standard .cs file. The lines starting with SizeToContent
and Width
set up how big the dialog is going to
be - WPF provides the ability to size the window to the size of the child
elements or you can explicitly set the size. I'm going to skip the next two
lines because I want to come back to them for the benefit of the XAML experts
who might have chosen to skip over this paragraph.
The dialog is then set up so that it doesn't appear in the windows taskbar,
"Save Snippet" appears in the title bar, and the window starts up in the center
of the screen. Phew, that's a lot taken care of, and it's not as scary as it
looks at first.
XAML experts, you can come back into the room now.
Now, for the lines that I emboldened above. What, exactly, do they do? Well,
they set the background and foreground colours to predefined Visual Studio
colours. By using DynamicResource
, we let our dialog respond to
changes to the underlying VS themes, so if the user changes their VS themes, our
dialog will use the updated themes.
I'm not going to cover the rest of the XAML now - it's fairly
straightforward, and it isn't really doing anything clever. It's purely there to
present the UI. The interesting parts are all in the ViewModel that goes behind
the view, and the code we have in the extension to hook into the RESTful
services. Let's start with the constructor.
[ImportingConstructor]
public AddSnippetViewModel(
IViewAwareStatus viewAwareStatusService,
IMessageBoxService messageBoxService,
IUIVisualizerService uiVisualizer,
IDocumentService documentService,
IRestService restService,
IStatusBarService statusBarService)
{
chooseGroup = true;
SnippetVisibility = CodeSnippetVisibility.JustMe;
this.viewAwareStatusService = viewAwareStatusService;
this.messageBoxService = messageBoxService;
this.uiVisualizer = uiVisualizer;
this.documentService = documentService;
this.restService = restService;
this.statusBarService = statusBarService;
RetrieveLanguages();
RetrieveGroups();
SaveSnippetCommand = new SimpleCommand<object, object>(x => isValid, ExecuteSaveSnippet);
Validator = new AddSnippetViewModelValidator(this);
statusBarService.SetText(Messages.Ready);
}
The ImportingConstructor
attribute is used by MEF to allow us to
hook into the services that match the parameters in the constructor argument
list. The first three parameters are provided by Cinch, while the last three are
specific to our extension. Note: You won't find any code in the code base that
calls this ViewModel with this parameter list; this is handled for us by MEF, so
we don't have to worry about it.
The internals of the constructor are straightforward enough, with the code
specifiying some default values and hooking up members to the argument list. The
interesting line in here is the line
statusBarService.SetText(Messages.Ready);
which clearly demonstrates
that, even though we have passed in an interface to this constructor and we
haven't added any code to actually hook up to the status bar service
implementation, the MEF resolution means that we are working with the resolved
instance here.
In the constructor, we call two methods which use the REST service; one to
retrieve the list of languages, the other to retrieve the groups to display in
the group selection drop down. Let's see what the code for retrieving the
languages looks like.
private void RetrieveLanguages()
{
statusBarService.SetText(Messages.GetLanguage);
JSONLanguagesResult languages = restService.RetrieveLanguages();
Languages = languages.Languages;
statusBarService.Clear();
}
In this method, the status bar service writes that CodeStash is retrieving
the languages list to the Visual Studio status bar (it's the ability to give
little bits of feedback like this to the user that VS provides that helps to
give your extensions that little bit of professional polish). The REST service
is used to retrieve the languages list, and the languages returned are stored in
an ObservableCollection
. Finally, the method clears the status bar
text.
Quick Pete Detour
We have mentioned the status bar service a couple of times here, and we
haven't actually seen much code that interacts with Visual Studio itself. Now
seems to be a really opportune moment to show just how easy it is to hook into
these Visual Studio services.
There are two parts to working with the status bar service in CodeStash. The
first part revolves around getting a hook into the status bar service itself.
This is handled in CodeStash_AddinPackag
by the following code:
IVsStatusbar statusBar = (IVsStatusbar)GetService(typeof(SVsStatusbar));
MEFPartsResolver.Instance.Resolve<IStatusBarService>().SetStatusBar(statusBar);
There we simply get a reference to the Visual Studio status bar service, and
then set this reference inside our concrete status bar service. This leads to
the second part, which is the actual implementation of the status bar service.
[Export(typeof(IStatusBarService))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class StatusBarService : IStatusBarService
{
private IVsStatusbar statusBar;
public void SetStatusBar<T>(T statusBar)
{
this.statusBar = statusBar as IVsStatusbar;
}
public void SetText(string text)
{
if (!IsFrozen)
{
statusBar.SetText(text);
}
}
public void Clear()
{
if (!IsFrozen)
{
statusBar.Clear();
}
}
private bool IsFrozen
{
get
{
int frozen;
statusBar.IsFrozen(out frozen);
return frozen != 0;
}
}
}
The attributes are used by MEF to identify this class as an implementation of
a MEFfable "contract", and to state that the same instance of this service will
be used whenever it's encountered in the addin. Most of the functionality is
pretty self explanatory, possibly the only area that needs discussion being the
property IsFrozen
. This property is used to determine whether or
not we can interact with the status bar, or if some other service has frozen it
for their usage. If you are using the VS status bar in your own extensions, I
would urge you to make sure that you are doing the same - it's important that
extensions cooperate with each other, and don't cause any unexpected side
effects in other peoples extensions.
Back to the show
As we are using Cinch as the underlying framework, the code makes heavy use
of the features and concepts provided there. One of the more interesting
features from our perspective is the provision of validation - it's common to
see this implemented internally in the ViewModel. Rather than merging the code
together like this, CodeStash moves validation out to separate classes which can
be consumed from the View. This means that the validation code is simple to
test, and exists in isolation from the ViewModel.
When the user has successfully filled in all the information she needs to
save the snippet and they have clicked Save, the application passes this
information over to the REST service. One little wrinkle is that we must HTML
encode any text that we pass across so that it can be passed over. When the text
is encoded, characters that would normally choke the REST service are converted
so that the service won't fall over because, for instance, we've tried to use a
<
character (in this case, the character would be encoded to
<
). What this means is that we have to decode the text when we receive
the code back from the service so that what we insert is the same text as we
originally selected, and not the HTML encoded version; in order to display the
text in the search results dialog, we use a converter to decode the encoded
data.
The code to save the snippet looks like this:
private void ExecuteSaveSnippet(object parameter)
{
try
{
statusBarService.SetText(Messages.Save);
int? groupId = null;
string groupName = Encode(NewGroup);
if (SelectedGroup != null)
{
groupId = SelectedGroup.GroupId;
groupName = Encode(SelectedGroup.Description);
}
restService.AddCodeSnippet(
Encode(this.documentService.SelectedText),
Encode(Category),
SelectedLanguage.LanguageId,
SelectedLanguage.Language,
Encode(Tag),
Encode(Description),
Encode(Title),
groupId,
groupName,
SnippetVisibility);
((DialogWindow)this.viewAwareStatusService.View).Close();
statusBarService.SetText(Messages.Ready);
messageBoxService.ShowInformation(Messages.SnippetSaved);
}
catch (Exception ex)
{
messageBoxService.ShowError(Messages.SnippetFailed);
}
}
private string Encode(string text)
{
return WebUtility.HtmlEncode(text);
}
The actual snippet text is retrieved from the document service. This simple
little service just hooks into the Visual Studio DTE and picks up the selected
text like so:
public string SelectedText
{
get
{
TextSelection selection = GetSelectedText();
if (selection == null) return string.Empty;
return selection.Text;
}
}
private TextSelection GetSelectedText()
{
if (provider == null || provider.ActiveDocument == null) return null;
return (TextSelection)provider.ActiveDocument.Selection;
}
Right, that's covered some of the interesting parts of saving the snippet to
the database and that's all well and good, but what about retrieving the
snippets? Well, this is where things get slightly more complicated. We're going
to start by looking at how the search actually takes place, and then move onto
what happens when we want to display the results and insert a snippet into the
code editor. Here's the code to actually do the search:
private void DoSearch(int pageNumber, bool fetchingDueToPaging)
{
AsyncState = AsyncType.Busy;
WaitText = "Searching for snippets";
ApplicationHelper.DoEvents();
CancellationToken cancellationToken = cancellationTokenSource.Token;
Task<bool> cancellationDelayTask = TaskHelper.CreateDelayTask(searchTimeOutMilliSeconds);
cancellationDelayTask.ContinueWith(dt =>
{
cancellationTokenSource.Cancel();
}, TaskContinuationOptions.OnlyOnRanToCompletion);
try
{
Task<JSONPagesSearchResultCodeSnippet> searchTask = Task.Factory.StartNew<JSONPagesSearchResultCodeSnippet>(() =>
{
string searchText = GetSearchText();
string[] tagsArray = !String.IsNullOrEmpty(this.Tags) && this.SelectedSearchType == SearchType.ByTag
? this.Tags.Split(new string[] { ";", ",", ":" }, StringSplitOptions.RemoveEmptyEntries)
: new string[] { };
if ((!string.IsNullOrEmpty(searchText) && this.SelectedSearchType != SearchType.ByTag) ||
(tagsArray.Any() && this.SelectedSearchType == SearchType.ByTag))
{
JSONPagesSearchResultCodeSnippet results = restService.Search(
searchText,
this.SelectedSearchType,
this.SelectedVisibility,
tagsArray,
pageNumber);
return results;
}
else
{
return null;
}
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
searchTask.ContinueWith(ant =>
{
if (!fetchingDueToPaging)
{
Mediator.Instance.NotifyColleagues<JSONPagesSearchResultCodeSnippet>("DisplaySearchResults", ant.Result);
}
else
{
Mediator.Instance.NotifyColleagues<JSONPagesSearchResultCodeSnippet>("DisplayPagedSearchResults", ant.Result);
}
AsyncState = AsyncType.Content;
ApplicationHelper.DoEvents();
}, cancellationToken, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.FromCurrentSynchronizationContext());
searchTask.ContinueWith(ant =>
{
AsyncState = AsyncType.Error;
ErrorMessage = "Search failed to run to completion";
}, cancellationToken, TaskContinuationOptions.NotOnRanToCompletion, TaskScheduler.FromCurrentSynchronizationContext());
}
catch (AggregateException aggEx)
{
ErrorMessage = "Search failed to run correctly";
AsyncState = AsyncType.Error;
ApplicationHelper.DoEvents();
}
}
Phew. There's a lot going on in there, and it looks quite scary doesn't it?
The first thing to be aware of is that the search results are paged, so this
method will only retrieve the number of items needed to be displayed on a single
page, and the heavy lifting for the actual search is perfomed via the REST
service. By this stage, we're comfortable with this concept, so we can take that
out of the complexity stakes. The next thing to think about is that the search
is performed asynchronously with the Task Parallel Library (TPL). For an
excellent series on using TPL, I would heartily recommend reading Sacha's series
starting with
this article.
So, why have we stopped by this method if it's all so straightforward? Well,
I want to cover a really cool little trick that Sacha has put in here. We don't
want the search to run indefinitely waiting for the server to time out, and we
don't want to introduce blocking in the user interface, so Sacha has implemented
this really nifty cancellable task that doesn't block the UI. The code in this
method that it affects is this:
Task<bool> cancellationDelayTask = TaskHelper.CreateDelayTask(searchTimeOutMilliSeconds);
cancellationDelayTask.ContinueWith(dt =>
{
cancellationTokenSource.Cancel();
}, TaskContinuationOptions.OnlyOnRanToCompletion);
The code that sits behind this is neat and simple in its elegance:
public static Task<bool> CreateDelayTask(int milliSecondsTimeout)
{
TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
new Timer(self =>
{
((IDisposable)self).Dispose();
tcs.TrySetResult(true);
}).Change(milliSecondsTimeout, -1);
return tcs.Task;
}
Anyway, once you've done your search, you obviously want to display your
results and let the user select and display an individual snippet. Well, the UI
uses a data grid and simple binding to actually display the data, but do you
remember that I mentioned that there was a wrinkle here? Earlier on I said that
the text elements had to be decoded as they were stored using HTML encoding.
There are two ways that we could achieve this effect. The first way would be to
loop through all the returned data and decode the text before we display it;
doable but inefficient. The second way to achieve this, and funnily enough the
way I chose, is to use a converter to perform the decode for us on the fly. This
is a simple piece of code, but I hope it helps to demonstrate that XAML
applications often just need simple little solutions to what seem to be complex
problems:
public class DecodeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value == null) return string.Empty;
return WebUtility.HtmlDecode(value.ToString());
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
Inserting the snippet is a nice and simple operation (well simplish), so
let's cover that now. The document service provides the following method to
actually insert text into the editor:
public void InsertText(string textToInsert)
{
if (provider == null || provider.ActiveDocument == null) return;
if (provider.UndoContext.IsOpen)
provider.UndoContext.Close();
provider.UndoContext.Open(Messages.UndoContext);
try
{
TextSelection sel = (TextSelection)provider.ActiveDocument.Selection;
EditPoint startPoint = sel.TopPoint.CreateEditPoint();
EditPoint endPoint = sel.BottomPoint.CreateEditPoint();
if (sel.Text.Length == 0)
{
endPoint.Insert(textToInsert);
}
else
{
endPoint.ReplaceText(startPoint, textToInsert, (int)EnvDTE.vsEPReplaceTextOptions.vsEPReplaceTextAutoformat);
}
Autoformat(startPoint, endPoint);
}
finally
{
provider.UndoContext.Close();
}
}
This method starts off by ensuring that we have an open document in the
editor to insert in to, and it then closes any open undo context before opening
a new one specific to CodeStash (don't worry about what an UndoContext is right
now, we'll be covering this one shortly). Next, we create a couple of EditPoint
elements. These represent points in the code editor that are the current
position of the editor. Next we determine whether or not we have any text
selected; if we do, we know that we are going to have to replace it with the
snippet, otherwise we will be inserting the snippet. Once the snippet is
inserted, the document is automatically formatted so that the snippets are neat,
and finally the undo context is closed.
So, what is the undo context? Well, it represents an atomic action that can
be undone or redone by Visual Studio. This allows us to group many internal
operations together into a single item that Visual Studio can undo or redo, just
like this:
The undo context in action.
So there we go, snippets retrieved from the service and added into Visual
Studio - all managed in a handy "undoable" operation. As I said
before, I haven't covered the
snippet viewer; this is based on
Daniel Grunwald's excellent
AvalonEdit control, and you really need to read the article that goes with
the control to get an understanding of how it works.
I have a question Pete. Why is the addin split into two projects?
I'm glad that you asked this. When we look at the code, we see that we could
easily have moved all of the code into one project, and it seems strange that
the interfaces work in the way that they do. The reason for this is down to a
decision I took early on in the development to put the infrastructure in place
to support future functionality. While this version of CodeStash is code
complete, it is a long way away from finished - in fact, I doubt it will ever be
finished. Sacha and I always envisaged that the first release would be to gauge
the reaction to CodeStash, and to see if it was something that would interest
people. Development does not stop here though. We want people to feed back ideas
about what they would like to see in place, and we have some great plans for
enhancing CodeStash in some really cool and exciting ways. If we had pushed
ahead with all the features that we wanted, these articles would have been
delivered a year later, so we would rather get a version 1 that has all the
basic functionality in place, and that provides the underpinnings to push
forward with new functionality.
So, to answer the question, the addin is split into two projects to provide
the base that we need for the next versions of CodeStash, such as client side
caching of groups, snippet autocompletion and other great features.
What have we really learned by now?
Well, hopefully you've come away from this article thinking that CodeStash is
a cool extension and one that you will want to use on a daily basis. You've also
learned that Pete O'Hanlon plays fast and loose with the rules of the English
language when it suits him. You should have an understanding of how to add
commands to a Visual Studio package, and how to associate the same command with
multiple windows. You know how to utilise Visual Studio services, and how to
hook into external REST services without having to import service contracts.
Finally, you know a quick way to associate config files with DLLs.
A point to ponder. This article spent so long covering how the command tables
work inside Visual Studio because the documentation for it is so disjointed, and
lacking in some parts that I wanted to take this opportunity to put what I
learned down so that you don't have to go through the pain points that I did. I
hope you find it worth your while.
Please, download the code and read it. Let us know what features you would
like to see in future iterations of CodeStash. The only way it will grow is with
feedback.