How to extend the Project Model
From MonoDevelop
| Table of contents |
Introduction
MonoDevelop has an extensible project model, designed to make it possible for add-ins to easily implement and integrate new features into the model.
There are many ways of extending the model, and every add-in should find the way that better fits its needs. Here are some examples of things an add-in can do:
- Add support for a .NET language (for example, the Boo add-in).
- Add support for a non .NET language (a C add-in).
- Create a new solution item (for example, a database project).
- Add new properties to existing types (for example, add GTK# design information to projects).
This article explain how can you do all this.
Overview of the Project Model
The following UML diagram is a simplified view of the project model:
Here is some info about the classes in the diagram:
- CombineEntry is the base class of anything that can be added to a solution.
- Combine is a group of CombineEntry (a solution).
- Project is a set of files that are usualy compiled together to generate a binary output.
- DotNetProject is type of project specialized in building and executing .NET assemblies.
- IProjectBinding is an interface that needs to be implemented to support new types of projects.
- DotNetProjectConfiguration is a class that keeps common configuration properties for .NET projects.
- ILanguageBinding is an interface that needs to be implemented to support code completion and refactoring services in new languages.
- IDotNetLanguageBinding is an interface that needs to be implemented to support compilation of new .NET languages.
Adding support non .NET languages
MonoDevelop is not limited to .NET languages, it can support any type of language. This is what you need to do to support a new language:
Create a Project subclass
You need to create a Project subclass and implement several virtual methods it provides, such as: Build(), DoExecute(), or GetOutputFileName().
Implement IProjectBinding
You also need to implement the IProjectBinding interface. The Name property identifies the new project type, and it is the name used in project templates to refer to this project type.
If you want to support single file compilation (that is, be able to compile and run a source code file without having to create a project), you can implement CreateSingleFileProject and CanCreateSingleFileProject. Just return null if you don't want to support it.
The project binding must be registered in an extension point, for example:
<Extension path = "/SharpDevelop/Workbench/ProjectBindings"> <ProjectBinding id = "MyProject" class = "MyAddin.MyProjectType" /> </Extension>
Optional: Implement ILanguageBinding
ILanguageBinding can be implemented to support additional language features such as code completion or refactoring. New language bindig types must be registered like this:
<Extension path = "/SharpDevelop/Workbench/LanguageBindings"> <LanguageBinding id = "MyLanguage" supportedextensions = ".ml" class = "MyAddin.MyLanguageBinding" /> </Extension>
Create a project template
Projects are created by users using the project templates available in the New Project dialog. So in general add-ins that implement new project types should also provide project templates for those.
When creating a project template, the project type (that is, the Name of the project binding, not the name of the class) can be specified by adding the type attribute to the Project element. For example:
<Template>
<TemplateConfiguration>
<_Name>Custom Project</_Name>
<Category>Custom</Category>
<Icon>res:MyProjectIcon</Icon>
<_Description>Creates a new custom project.</_Description>
</TemplateConfiguration>
<!-- Actions -->
<Actions>
...
</Actions>
<!-- Template Content -->
<Combine name = "${ProjectName}" directory = ".">
<Options>
<StartupProject>${ProjectName}</StartupProject>
</Options>
<Project name = "${ProjectName}" directory = "." type="MyProject">
<Options/>
<Files>
...
</Files>
</Project>
</Combine>
</Template>
Adding support for new .NET languages
To support a new .NET language it should be enough to provide an implementation of IDotNetLanguageBinding. .NET languages have in common that they generate assemblies, and that all those assemlies can be executed in the same way. The class DotNetProject handles all this. What's specific is how the assemblies are generated, and that's what IDotNetLanguageBinding provides. So this is what you need to do:
Implement IDotNetLanguageBinding
The Compile() method should compile the source code and generate an assembly. If there are some compiler specific options you want to make available, implement the CreateCompilationParameters method.
IDotNetLanguageBinding inherits from ILanguageBinding, so you can also provide support for code completion or refactoring by implementing the Parser and Refactorer properties. See "Implement ILanguageBinding" in the previous chapter for more info.
IDotNetLanguageBinding implementations must be registered as regular language bindings, that is:
<Extension path = "/SharpDevelop/Workbench/LanguageBindings"> <LanguageBinding id = "MyLanguage" supportedextensions = ".ml" class = "MyAddin.MyLanguageBinding" /> </Extension>
Create a project template
Project templates can be created and registered like the non .NET language templates (see previous chapter), with some particularities:
- The project type don't need to be specified, since it is DotNet by default.
- The language name can be specified in a <Language> element in the <TemplateConfiguration> element of the template. The language can also be specified per project by setting the language attribute of the <Options> element of the project.
Creating new types of solution elements
A solution can contain objects which are not solutions or projects, but arbitrary sets of information. For example, the NUnit add-in defines a new type of CombineEntry which is a set of assemblies to be tested. This is what you need to do:
Create a subclass of CombineEntry
There are several abstract methods you need to implement: Clean, Build, Execute and NeedsBuilding. Other virtual methods already provide a fully working implementation and their reimplementation is optional.
Create a project template
The <CombineEntry> element can be used to declare an arbitrary solution element. Use the type attribute to specify the fully qualified name of the class. For example:
<Template>
<!-- Template Header -->
<TemplateConfiguration>
<_Name>NUnit assembly test collection</_Name>
<Category>NUnit</Category>
<_Description>Create an NUnit assembly test collection</_Description>
</TemplateConfiguration>
<!-- Template Content -->
<Combine name = "${ProjectName}" directory = ".">
<Options>
<StartupProject>${ProjectName}</StartupProject>
</Options>
<CombineEntry name = "${ProjectName}" directory = "." type = "MonoDevelop.NUnit.NUnitAssemblyGroupProject, MonoDevelop.NUnit">
</CombineEntry>
</Combine>
</Template>
Adding properties to existing project or solution types
Any CombineEntry subclass (including Project and Combine) can be extended with new properties. To add a new property to a type do the following:
Register the property
New properties are registered in the add-in xml file like this:
<Extension path = "/SharpDevelop/Workbench/Serialization/ExtendedProperties"> <ItemProperty class = "MonoDevelop.Projects.Project" name = "MyAddin.MyPropertyName" type = "System.String" /> </Extension>
It is recomended to use the add-in namespace as prefix of the name, to avoid name colisions. You are not limited to using primitive types in new properties. In fact, if an add-in is going to need many additional properties, it's better to create a class with all needed info and declare just one property using that class as type.
Set and get property values
Use the IExtendedDataItem interface to get and set custom property values. Here is an example:
Project project = ...; IExtendedDataItem item = (IExtendedDataItem) project; item.ExtendedProperties ["MyAddin.MyPropertyName"] = "hello!"; ... string storedValue = (string) item.ExtendedProperties ["MyAddin.MyPropertyName"];
Project service extensions
Using Project Service Extensions add-ins can hook on the project service and change the behavior of some common project an solution operations. A project service extension can be created by implementing a subclass of ProjectServiceExtension. This class is defined in the MonoDevelop.Projects namespace and looks like this:
public class ProjectServiceExtension
{
// Called when a combine entry is saved
public virtual void Save (IProgressMonitor monitor, CombineEntry entry) {...}
// Called to check if a file is a combine entry which can be loaded by the Load method
public virtual bool IsCombineEntryFile (string fileName) {...}
// Called to load a combine entry
public virtual CombineEntry Load (IProgressMonitor monitor, string fileName) {...}
// Called to clean the combine entry
public virtual void Clean (IProgressMonitor monitor, CombineEntry entry) {...}
// Called to build the combine entry
public virtual ICompilerResult Build (IProgressMonitor monitor, CombineEntry entry) {...}
// Called to execute the combine entry
public virtual void Execute (IProgressMonitor monitor, CombineEntry entry, ExecutionContext context) {...}
// Called to check if the combine entry needs to be built (if it depends files that have been modified)
public virtual bool GetNeedsBuilding (CombineEntry entry) {...}
// Called to set the build flag
public virtual void SetNeedsBuilding (CombineEntry entry, bool val) {...}
// Called to get all files that should be copied when exporting the combine entry to a new location
public virtual StringCollection GetExportFiles (CombineEntry entry) {...}
}
An add-in can override any of the ProjectServiceExtension methods and provide a custom behavior for it. For example:
public class MyExtension: ProjectServiceExtension
{
public override ICompilerResult Build (IProgressMonitor monitor, CombineEntry entry)
{
Console.WriteLine ("Do something before the build");
base.Build (monitor, entry);
Console.WriteLine ("Do something after the build");
}
}
In this example the extension is calling base.Build() because it doesn't want to completely replace the Build method, it only wants to run some custom code before and after the build.
Project service extensions must be registered in the "/SharpDevelop/Workbench/ProjectServiceExtensions" extension point. For example:
<Extension path = "/SharpDevelop/Workbench/ProjectServiceExtensions"> <Class class="MyAddin.MyExtension"/> </Extension>
Custom File Formats
MonoDevelop has a pluggable file format system which allows defining multiple file formats for projects and solutions. A file format is an implementation of the IFileFormat interface, which has methods for identifying, loading and saving a combine entry. The project and combine classes are decoupled from the read/write code. Everything is done in IFileFormat implementations (the "native" MD file format is just another IFileFormat implementation).
IFileFormat is defined in the MonoDevelop.Projects namespace and looks like this:
public interface IFileFormat
{
// Display name of the file format (e.g. Visual Studio 2005)
string Name { get; }
// Returns a valid file name for the provided object and file (e.g. it might change
// the extension to .csproj for the VS2005 format)
string GetValidFormatName (object obj, string fileName);
// Returns true if this file format can read the provided file
bool CanReadFile (string file);
// Returns true if this file format can write the provided object
bool CanWriteFile (object obj);
// Writes an object
void WriteFile (string file, object obj, IProgressMonitor monitor);
// Reads an object
object ReadFile (string file, IProgressMonitor monitor);
// Returns the list of files where the object is stored
StringCollection GetExportFiles (object obj);
}
MonoDevelop supports multiple file formats for the same kind of combine entry (project, solution or whatever). You only need to register it in the "/SharpDevelop/Workbench/ProjectFileFormats" extension point. For example:
<Extension path = "/SharpDevelop/Workbench/ProjectFileFormats"> <FileFormat id="MSBuildFileFormat" class="MonoDevelop.Prj2Make.MSBuildFileFormat"/> </Extension>
The default file format
MonoDevelop has a default IFileFormat implementation that will be used when none of the registered file formats can handle a given object type. This default file format uses the serialization engine to serialize the object to a file, and will use ".mdse" as file extension.
It means that if you are creating a new subclass of CombineEntry you are not forced also to implement a IFileFormat class. By default, the .mdse file format will be used.
Adding serialization support
MonoDevelop has a serialization engine which is used to save and load solutions and projects from files. Like in the system XML serializer, the process of serialization can be controlled by applying some attributes to fields and properties.
Serialization attributes
Here is a description of the attributes you can use:
- ItemPropertyAttribute: This attribute has to be applied to the fields or properties that have to be serialized. Notice that only fields or properties with this attribute are serialized (this is different from XmlSerializer, where all public members are serialized by default). ItemPropertyAttribute has several properties (all of them optional) that can be set to change the default serialization behavior:
- Name: Name of the property in the serialized model. It's the member name by default. You can specify a nested element name. For example, the name "Data/Name" will create the property as a child of the Data element, and it will be named "Name".
- DefaultValue: Default value of the member. A property won't be serialized if the assigned value is the default value.
- ReadOnly: true if the member will be read but never written.
- WriteOnly: true if the member will be written but never read.
- Scope: When added to a collection or array member, it specifies the nesting level to which the ItemPropertyAttribute applies. 0 is the member itself, 1 is the first level of collection elements, and so on.
- ValueType: Specifies the type to use for serialization, if the one declared in the member is too generic.
- DataItemAttribute: This attribute can be used to set the name of the type in the serialized model. By default it's the name of the class. This attribute is optional.
- DataIncludeAttribute: All types involved in an object tree must be know in advance by the serializer, or it won't be able to serialize or deserialize the tree. This attribute can be applied to classes or members to include types in the serialization model. This is only needed for types not explicitely declared in members.
- ExpandedCollectionAttribute: When applied to an array or collection attribute, the collection contents will be serialized as children of the container object, that is, there won't be a root collection element.
Here are some examples:
// This class will be serialized using "SomeTest" as root element.
[DataItem ("SomeTest")]
public class SerializationTest
{
// This field won't be serialized
int someInt;
// This field will be serialized
[ItemProperty]
string someString;
// This field will be serialized, but only if it's not empty.
[ItemProperty (DefaultValue="")]
string someDefaultString;
// This field will be serialized as a child element of "Data"
[ItemProperty ("Data/Info")]
string someInfo;
// This property will be serialized as an array of strings.
// The element name will be "Names" (instead of "names"),
// and there will be a "Name" element for each item of the list.
[ItemProperty ("Names")]
[ItemProperty ("Name", Scope=1, ValueType=typeof(string))]
ArrayList names;
// This property will be serialized as a set of Value elements
// There won't be a "values" root element.
[ItemProperty ("Value")]
[ExpandedCollection]
string[] values;
}
Here is some sample XML which could have been generated from the previous class:
<SomeTest>
<someString>Hi</someString>
<Data>
<Info>some info</Info>
</Data>
<Names>
<Name>One</Name>
<Name>Two</Name>
<Name>Three</Name>
</Names>
<Value>First</Value>
<Value>Second</Value>
</SomeTest>
Custom serialization
Types with complex serialization needs which can't be specified using attributes can implement ICustomDataItem to provide a custom serialization behavior. ICustomDataItem defines two methods:
- Serialize: This method has to serialize the contents of the object into a DataCollection. It provides as input parameter an ITypeSerializer instance which you can use to run the default serializer.
- Deserialize: It has to deserialize the provided DataCollection and fill the object with data. An ITypeSerializer is also provided.
Here is an example:
public class WindowData: ICustomDataItem
{
[ItemProperty]
string id;
[ItemProperty]
string title;
int width;
int height;
DataCollection ICustomDataItem.Serialize (ITypeSerializer serializer)
{
// Use the provided serializer to run the default serialization.
// This is optional, but it's useful if you just want to
// tweak the default behavior, not completely reimplement it.
DataCollection data = serialize.Serialize (this);
// Now add some custom data
data.Add (new DataValue ("size", width + " " + height));
return data;
}
void ICustomDataItem.Deserialize (ITypeSerializer serializer, DataCollection data)
{
// Extract the custom value we added in the Serialize method
DataValue size = data.Extract ("size") as DataValue;
string[] sizes = size.Value.Split (' ');
width = int.Parse (sizes[0]);
height = int.Parse (sizes[1]);
// Deserialize the other fields.
serializer.Deserialize (this, data);
}
}
Creating configuration dialog panels
After extending a project or solution (either by subclassing or adding properties), you'll need to provide configuration panels so the user can browse and set the new information. This is what you have to do:
Create an AbstractOptionPanel subclass
You'll need to implement, at least:
- LoadPanelContents(): Called to load the information into the panel.
- StorePanelContents (): Called when the user clicks on OK.
You can get the information to load into the panel from the CustomizationObject property. CustomizationObject returns an IProperties object, whose contents depend on the object being edited:
- When editing options for a Project: The IProperties object contains a property named "Project" which returns the project instance.
- When editing options for a Project configuration: In addition to the "Project" property, it contains a "Config" property which returns the project configuration instance.
- When editing options for a combine: The IProperties object contains a property named "Combine" which returns the combine instance.
- When editing options for a Project configuration: In addition to the "Combine" property, it contains a "Config" property which returns the combine configuration instance.
Here is a simple example:
public class SimplePanel: AbstractOptionPanel
{
Gtk.Entry entry;
Project project;
public SimplePanel ()
{
entry = new Gtk.Entry ();
Add (entry);
ShowAll ();
}
public override void LoadPanelContents()
{
IProperties props = (IProperties) CustomizationObject;
project = (Project) props.GetProperty ("Project");
entry.Text = project.Name;
}
public override bool StorePanelContents()
{
project.Name = entry.Text;
return true;
}
}
Register the new panel
New configuration panels are registered using the <DialogPanel> element. For example:
<DialogPanel id = "SteticOptionsPanel"
_label = "Widget Export"
class = "MonoDevelop.GtkCore.Dialogs.WidgetBuilderOptionPanel"/>
The extension point where to add the new element depends on which information the panel edits. It can be one of the following:
- For common project options: /SharpDevelop/Workbench/ProjectOptions/GeneralOptions/Common.
- For project configuration options: /SharpDevelop/Workbench/ProjectOptions/ConfigurationProperties.
- For common combine options: /SharpDevelop/Workbench/CombineOptions/GeneralOptions/Common.
- For combine configuration options: /SharpDevelop/Workbench/CombineOptions/ConfigurationProperties
For example, to show an options panel in the project dialog for every configuration in the project, the panel should be registered like this:
<Extension path = "/SharpDevelop/Workbench/ProjectOptions/ConfigurationProperties"> <Conditional activelanguage="C#"> <DialogPanel id = "CSharpCodeGenerationPanel" _label = "Code Generation" class = "CSharpBinding.CodeGenerationPanel"/> </Conditional> </Extension>
The <Conditional> element is used in this case to make the panel visible only for C# projects.


Powered by MediaWiki