|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionWhen seeing the article Line Counter - Writing a Visual Studio 2005 Add-In [^] written by Jon Rista, I wanted to show you how to write that add-in for SharpDevelop. In this article, I will show you how to create an add-in, but I will not discuss the details of SharpDevelop's add-in architecture here - you can read more about that in my article Building Applications with the SharpDevelop Core [^]. The line counter code is taken from the VS2005 add-in written by Jon Rista; the counting algorithm itself is from Oz Solomon. I will discuss the changes I did to the code in this article - after all, there aren't so many changes required. Creating a new add-inOur add-in will be a menu entry in the "Tools" menu that opens a document window displaying the line counter UI. While it is possible to develop SharpDevelop add-ins in Visual Studio, you can get started a lot faster if you use SharpDevelop because it already comes with a project template for an add-in extending the "Tools" menu: "Tools menu entry".
We create a SharpDevelop add-in with the name "LineCounter". The "SharpDevelop AddIn" template could also be used, but it starts with an add-in defining a pad (a docked tool window like "Projects", "Properties"); the "Tool menu entry" add-in starts with a menu command. Now let's look at what SharpDevelop has generated for us. We have a project with a few files:
First look at the .addin file. The template already defines a menu item in the Tools menu, we'll just modify the label. And we'll add a new section to the .addin file: the <Manifest> section. Our add-in needs a unique identity - use something like a namespace name. Because we don't want lots of add-ins with the identity "AddIn1" around, the template does not contain the manifest section by default. The identity is not strictly required, but it allows us to use SharpDevelop's AddIn Manager for our add-in - and it is also required when other add-ins want to reference your add-in. Because the API between SharpDevelop 2.0.x.y and SharpDevelop 2.1.a.b will change a bit, we use the dependency to ensure that our add-in can only be installed into SharpDevelop 2.0.*.*. Since our LineCounter will only work when a solution is opened, we put the menu item inside a <AddIn name = "LineCounter"
author = "Daniel Grunwald"
url = "http://www.codeproject.com/useritems/LineCounterSDAddIn.asp"
description = "Advanced line counter AddIn">
<Manifest>
<Identity>Grunwald.LineCounter</Identity>
<Dependency addin="SharpDevelop" version="2.0"/>
</Manifest>
<Runtime>
<Import assembly = "LineCounter.dll"/>
</Runtime>
<Path name = "/Workspace/Tools">
<!-- disable our menu item if condition "SolutionOpen" is not met -->
<Condition name="SolutionOpen" action = "disable">
<MenuItem id = "LineCounterCommand1"
label = "Show Line Counter"
class = "Grunwald.LineCounter.ToolCommand1"/>
</Condition>
</Path>
</AddIn>
Our menu item uses the " TestingHowever, first we want to get our add-in running. For testing, we'll display a
You should now be able to successfully compile the project. If you look into the bin/Debug directory, you will see a copy of the LineCounter add-in, the compiled LineCounter.dll, and the debug symbols. If you want to test your add-in, you need to register it with SharpDevelop. The best way for testing is to copy these files to the AddIns directory of your SharpDevelop installation. Restart SharpDevelop to load the add-in. You should see the "Line Counter" command in the Tools menu, disabled until you open a solution. If you click the menu item, the message box should show up. If you want to update your add-in, you'll need to shutdown SharpDevelop, copy the files, and restart it. If you do this a few times, you'll probably want to use a separate installation of SharpDevelop to develop your add-in, so you can leave your development environment opened. Alternatively, you could use Visual Studio to write your add-in... Creating the line counterNow to the real work: implementing the line counter. We want to display the line counter user interface as a document, like the start page. Document views in SharpDevelop are called ViewContents. There are two types of view contents: primary and secondary. A primary view content is capable of displaying something on its own; a secondary view content extends another view content by displaying multiple tabs in the document - SharpDevelop's Windows.Forms designer is a secondary view content. We want to display a simple document view, so we'll use a normal (primary) view content. Showing such a view content is easy: public override void Run()
{
WorkbenchSingleton.Workbench.ShowView(new LineCounterViewContent());
}
Now create the new class named public class LineCounterViewContent : AbstractViewContent
{
LineCounterBrowser browser = new LineCounterBrowser();
public override Control Control {
get { return browser; }
}
public LineCounterViewContent() {
this.TitleName = "Line Counter";
}
}
This control is a UserControl copied from the Visual Studio Line Counter article [^] — copy LineCounterBrowser.* into the "Src" directory and use "Add existing file" to add LineCounterBrowser.cs to the project. SharpDevelop should add LineCounterBrowser.Designer.cs and LineCounterBrowser.resx automatically. Now, we still have to change the parts where the line counter references Visual Studio's Accessing the project in SharpDevelopFirst, fix the using ICSharpCode.SharpDevelop.Project;
Then remove the variable and property referencing the Solution solution = ProjectService.OpenSolution;
if (solution != null) // OpenSolution is null when no solution is opened
{
FileInfo fiSolution = new FileInfo(solution.FileName);
SharpDevelop doesn't need "plural" classes like "Projects", but uses the standard .NET collection classes. Unlike Visual Studio, List<IProject> projects = new List<IProject>(solution.Projects);
tsprgTotal.Maximum = projects.Count;
tsprgTask.Value = 0;
foreach (IProject fiProject in projects) {
As you might guess, foreach (IProject fiProject in projects) {
tsprgTotal.PerformStep();
string projName, lang;
if (fiProject.FileName.IndexOf("://") != -1)
{
projName = fiProject.FileName; // this is a web project
lang = "{00000001-0000-0000-0000-000000000000}";
} else {
projName = fiProject.Name;
lang = fiProject.TypeGuid;
}
int iconIndex;
// default icon 0
m_projIconMappings.TryGetValue(lang, out iconIndex);
summary = new LineCountSummary(projName, iconIndex);
m_summaryList.Add(summary);
tsprgTask.Maximum = 0;
tsprgTotal.Value = 0;
ScanProjectItems(fiProject.Items, summary);
}
private void ScanProjectItems(List<ProjectItem> projectItems,
LineCountSummary summary)
{
tsprgTask.Maximum += projectItems.Count;
foreach (ProjectItem projectItem in projectItems)
{
tsprgTask.PerformStep();
if (!(projectItem is FileProjectItem)) {
// Skip references and other special MSBuild things
continue;
}
string projectFile = projectItem.FileName;
if (!Directory.Exists(projectFile))
{
int iconIndex = 0;
m_fileIconMappings.TryGetValue(Path.GetExtension(projectFile),
out iconIndex);
summary.FileLineCountInfo.Add(new LineCountInfo(projectFile,
iconIndex, summary));
}
}
}
Now try to compile again. The only thing missing is " // Map project types to icons for use in the projects list
m_projIconMappings = new Dictionary<string, int>();
m_projIconMappings.Add("{00000000-0000-0000-0000-000000000000}", 0);
m_projIconMappings.Add("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}", 1); // C#
m_projIconMappings.Add("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}", 2); // VB
m_projIconMappings.Add("{00000001-0000-0000-0000-000000000000}", 5);
This fixes all compile errors. And if you test it, you'll find that it even runs correctly! :-) Although the extension process is very different in SharpDevelop and Visual Studio, the API is quite similar - after all, both are modelling MSBuild solutions and a similar feature set. I hope this shows you that porting add-ins from Visual Studio to SharpDevelop isn't very hard, and we would like to see more SharpDevelop add-ins in the future. Here an image showing the add-in counting itself:
Possible improvementsWhile this is a basic port of the add-in to SharpDevelop, there are lots of possibilities for improvement. Getting icons from SharpDevelopFirst, we to replace some bad code in Before we can continue, we need to be aware how SharpDevelop deals with icons: menu items are defined in XML, and can be shown before the add-in library is loaded. Because of that, resource files can be registered in XML, which makes the icons inside it available to the The line counter code uses image lists with a few predefined images. Because I do not want to change it, I will just introduce a new class public class ImageListHelper
{
ImageList imageList;
Dictionary<string, int> dict = new Dictionary<string, int>();
public ImageListHelper(ImageList imageList)
{
if (imageList == null)
throw new ArgumentNullException("imageList");
this.imageList = imageList;
}
public int GetIndex(string imageName)
{
int index;
if (!dict.TryGetValue(imageName, out index)) {
index = imageList.Images.Count;
imageList.Images.Add(IconService.GetBitmap(imageName));
dict[imageName] = index;
}
return index;
}
}
We will use two instances of this class to control the two image lists, iconIndex = projectImageListHelper.GetIndex(
IconService.GetImageForProjectType(fiProject.Language));
Do the same for the file icons in iconIndex = fileImageListHelper.GetIndex(
IconService.GetImageForFile(projectFile));
Now the add-in uses the icons shipped with SharpDevelop, and will automatically use the icons of other languages added to SharpDevelop. Adding new counting algorithmsThe great thing about the SharpDevelop add-in infrastructure is that it allows an add-in to extend another. We will modify the Line Counter in such a way that it is possible for other add-ins to introduce new counting algorithms. We want to lazy-load our counting algorithms only when a file of the specified type is encountered. This leads to the doozer-descriptor-implementation pattern you will often see for extendable SharpDevelop features. In the public interface ICountingAlgorithm
{
void CountLines(LineCountInfo info);
}
public class CountingAlgorithmGeneric : ICountingAlgorithm {
public void CountLines(LineCountInfo info) {
LineCounterBrowser.CountLinesGeneric(info);
}
}
public class CountingAlgorithmCStyle : ICountingAlgorithm {
public void CountLines(LineCountInfo info) {
LineCounterBrowser.CountLinesCStyle(info);
}
}
public class CountingAlgorithmVBStyle : ICountingAlgorithm {
public void CountLines(LineCountInfo info) {
LineCounterBrowser.CountLinesVBStyle(info);
}
}
public class CountingAlgorithmXmlStyle : ICountingAlgorithm {
public void CountLines(LineCountInfo info) {
LineCounterBrowser.CountLinesXMLStyle(info);
}
}
The <Path name = "/AddIns/LineCounter/CountingAlgorithms">
<LineCountingAlgorithm
id = "Generic"
extensions = ".txt;.res;.sql;.cd"
class = "LineCounterAddin.CountingAlgorithmGeneric" />
<LineCountingAlgorithm
id = "CStyle"
extensions = ".cs;.vj;.js;.cpp;.cc;.cxx;.
c;.hpp;.hh;.hxx;.h;.idl;.odl;.css"
class = "LineCounterAddin.CountingAlgorithmCStyle" />
<LineCountingAlgorithm
id = "VBStyle"
extensions = ".vb;.vbs"
class = "LineCounterAddin.CountingAlgorithmVBStyle" />
<LineCountingAlgorithm
id = "XmlStyle"
extensions = ".xml;.xsl;.xslt;.xsd;.config;.resx;.
aspx;.ascx;.ashx;.asmx;.asax;.html;.html"
class = "LineCounterAddin.CountingAlgorithmXmlStyle" />
</Path>
Because we are using custom attributes, we are using a new codon name " <Import assembly = "LineCounter.dll">
<Doozer name="LineCountingAlgorithm"
class="LineCounterAddin.CountingAlgorithmDoozer"/>
</Import>
The public class CountingAlgorithmDoozer : IDoozer
{
public bool HandleConditions {
get {
// our doozer cannot handle conditions, let SharpDevelop
// do that for us
return false;
}
}
public object BuildItem(object caller, Codon codon,
System.Collections.ArrayList subItems)
{
return new CountingAlgorithmDescriptor(codon.AddIn,
codon.Properties["extensions"],
codon.Properties["class"]);
}
}
This means our doozer will always build objects of the type public class CountingAlgorithmDescriptor
{
AddIn addIn;
string[] extensions;
string className;
public CountingAlgorithmDescriptor(AddIn addIn,
string extensions, string className)
{
this.addIn = addIn;
this.extensions = extensions.ToLowerInvariant().Split(';');
this.className = className;
}
public bool CanCountLines(LineCountInfo info)
{
return (Array.IndexOf(extensions,
info.FileType.ToLowerInvariant()) >= 0);
}
ICountingAlgorithm cachedAlgorithm;
public ICountingAlgorithm GetAlgorithm()
{
if (cachedAlgorithm == null) {
cachedAlgorithm = (ICountingAlgorithm)addIn.CreateObject(className);
}
return cachedAlgorithm;
}
}
Now the List<CountingAlgorithmDescriptor> countingAlgorithms;
Initializing this list in the constructor is easy: countingAlgorithms = AddInTree.BuildItems<CountingAlgorithmDescriptor>
("/AddIns/LineCounter/CountingAlgorithms", this);
// Iterate through algorithms to fill list of known countable types
foreach (CountingAlgorithmDescriptor desc in countingAlgorithms) {
m_countableTypes.AddRange(desc.extensions);
}
And finally, replace the usage of foreach (CountingAlgorithmDescriptor desc in countingAlgorithms) {
if (desc.CanCountLines(info)) {
desc.GetAlgorithm().CountLines(info);
break;
}
}
So, let's reiterate how this extension model works: When SharpDevelop starts, SharpDevelop only loads your .addin file, but this bit of XML parsing is quite fast. Your .dll is not loaded, but SharpDevelop remembers where it has to look when the Only when counting the lines in a file of any matching extension, the descriptor creates an instance of the class. The "AddIn" stored is the context in which the class name occurred - it knows which runtime assemblies were specified in that XML file. If there were additional algorithms in other assemblies (for example, a Boo line counter), those assemblies would be loaded only if you count files in those languages. This means unused SharpDevelop add-ins use very little memory and startup time (if their extension points are coded properly) - it only takes parsing the XML, and storing the resulting codons in a compact object tree where they are mixed with the other addins' codons. Creating an add-in installation packageNow to move to a completely different and much easier topic at the end: we want to have an easy way for users to install, update, and uninstall our add-in. Creating a SharpDevelop add-in installation package (.sdaddin file) is easy. Just create a Zip file containing LineCounter.addin and LineCounter.dll, and rename the Zip file to "LineCounter.sdaddin". That really was everything you had to do - double-clicking this .sdaddin file will open SharpDevelop's AddIn Manager where you can install the add-in with just one click. Add-ins installed this way will be extracted to %Application Data%\.ICSharpCode\SharpDevelop2\AddIns.
More add-in writing help
LicenseSharpDevelop 2.0 is released under the terms of the GNU Lesser General Public License. In plain English, that means you can use any license you want for your own add-ns and do not have to open-source them. You only have to release modifications to the SharpDevelop libraries. SummaryThis article shows you how to start writing SharpDevelop add-ins. It is a complete walkthrough from creating a new project to creating the installation package. History
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||