SharpDevelop Community

Get your problems solved!
Welcome to SharpDevelop Community Sign in | Join | Help
in Search

Daniel Grunwald

Architecture changes in SharpDevelop 5.0 Beta 1

Last Sunday, we released SharpDevelop 5.0 Beta 1. This is a major milestone for us: SharpDevelop 5.0 Beta 1 ships architecture changes that we have been working on since 2010. These changes enable us to provide a better experience when editing C# code: semantic highlighting, background detection of syntax errors and other issues, and tons of refactorings.

However, there is also a downside to these major architecture changes: they required a rewrite of most of our language bindings, and languages other than C# have lost features in the transition. Most dramatically, even essential features such as code completion are no longer available for Visual Basic. And the Boo language is no longer supported at all.

In this post, I will try to give an overview of the most important API changes in SharpDevelop 5.0. I hope this information will be useful to developers of third-party AddIns, and other users of the SharpDevelop codebase.
Most difficulty in porting an AddIn from SD 4.x to 5.0 is with the redesigned ICSharpCode.NRefactory and ICSharpCode.SharpDevelop.Dom libraries. These were rewritten from scratch, and there have been some major conceptual changes (e.g. much more pervasive immutability; and the split of the type system into resolved+unresolved variables), which might require significant changes to to your AddIns.

If your AddIn is not a language binding and does not provide refactorings, porting it will not be as difficult; although there are still plenty of other API changes in SharpDevelop 5.0 Beta 1.
Many of these other changes are trivial, like types being moved to different namespaces/libraries, or methods being moved to different classes.

An incomplete list of classes that were renamed/moved can be found in the SharpDevelop wiki. Note that several important types were moved to the ICSharpCode.NRefactory library. You might have to add a reference to ICSharpCode.NRefactory If your AddIn doesn't already have that reference.

NRefactory was rewritten from scratch, and SharpDevelop.Dom was replaced by NRefactory 5

In SharpDevelop 4.x, 'NRefactory' was the C# (and VB) parser library. Semantic analysis and code completion belonged to the 'SharpDevelop.Dom' library. This changes with SharpDevelop 5.0: NRefactory now takes over the responsibilities of the old SD.Dom, and thus has much more central position in the SharpDevelop infrastructure.

While SharpDevelop 4.x had a single type system (in the old SD.Dom), SharpDevelop 5.0 has no less than three different type system APIs:

  • Unresolved type system (NR.TypeSystem.IUnresolved*)
  • Resolved type system (NR.TypeSystem)
  • Mutable, observable type system model (new SD.Dom)

In comparison, the old type system in SD 4.x was part immutable, part mutable, but not observable. When porting code from SD 4.x, be careful which of the new type systems you use.

The unresolved type system should be used if you are providing a type system to the SD infrastructure (IParser.Parse implementation in your language binding). Try to avoid using the unresolved type system for anything else.

The resolved type system is the most powerful of the three options, and should be used in most cases. However, you should not hold onto references to the resolved system for an extended amount of time: doing so will hold a snapshot of the whole solution in memory!
Instead, if you need to hold on to a reference to the type system, use the new SD.Dom. This mutable model tracks changes to the type system, and thus doesn't keep old versions of the project in memory.
You can use the .GetModel() extension method to convert from resolved type system to SD.Dom, and the .Resolve() method to convert back.

For an introduction to NRefactory, take a look at the article "Using NRefactory for analyzing C# code".

Static services were replaced with interfaces

To make SharpDevelop AddIn code easier to test, we have replaced many of the static service classes in SharpDevelop with interfaces. The new static class "SD" has static properties for retrieving references to the services, so the call "ResourceService.GetString()" becomes "SD.ResourceService.GetString()". To make porting code easier, some commonly used static service classes still exist (e.g. MessageService and LoggingService), but they just forward calls to the interface (so are still testable). However, not all services have been converted to interfaces yet.

In unit tests, Rhino.Mocks (or another mocking framework of your choice) can be used to easily create mocks/stubs of the services:

SD.InitializeForUnitTests(); // initialize container and replace services from previous test cases
SD.Services.AddService(typeof(IParserService), MockRepository.GenerateStrictMock<IParserService>());
SD
.ParserService.Stub(p => p.GetCachedParseInformation(textEditor.FileName)).Return(parseInfo);
SD
.ParserService.Stub(p => p.GetCompilationForFile(textEditor.FileName)).Return(compilation);

It is possible to define a service interface in ICSharpCode.SharpDevelop.dll and have the implementation somewhere else (SharpDevelop will find the implementation through the addin tree).
This allows for AddIns to consume each other's functionality (e.g. debugger accessing the decompiler service) without having to define a custom extension point in each case.
We are also trying to separate the public API from the implementation details. For example, instead of a public class NewFileDialog, we simply provide a 'ShowFileDialog()' API. This reduction of the API surface will allow us to make major changes (for example, reimplement the dialog using WPF instead of WinForms) without breaking consumers of the API.

Namespaces in ICSharpCode.SharpDevelop reorganized

Along with the API surface reduction, we are moving around types in ICSharpCode.SharpDevelop.dll, reorganizing the namespace structure.
AddIns code will need to update plenty of using directives to follow along.

As for the reasoning behind this change: the old namespace structure was mostly based on a separation of 'Gui' and 'Services'.
However, if UI code is properly separated from the underlying logic, this means that essentially every feature ends up having both GUI and service components. With the new namespace structure, we re-group the types into feature areas instead.

AddInTree paths reorganized

Plenty of AddIn tree paths have been changed to better match the new namespace structure.
       
I used a global replace operation for renaming paths; so AddIns that are in the SharpDevelop repository but not in the SharpDevelop solution (e.g. the sample AddIns) should have been adjusted as well.

However, 3rd party AddIns will have to be manually adjusted. Our wiki has a list with some of the changes (second table, at the end of the page).

SD.MainThread

The new best way to invoke a call on the main thread is:
  SD.MainThread.InvokeAsyncAndForget(delegate { ... });

Note that InvokeAsync returns a Task (like all .NET 4.5 *Async APIs). If any exceptions occur while executing the delegate, they will get stored in the task object. This can cause the exception to get silently ignored if the task object isn't used later. The "InvokeAsyncAndForget()" method works like the old "BeginInvoke()" method and does not try to catch any exceptions.

It is also often possible to avoid explicit thread switching alltogether by using the C# 5 async/await feature.

ICSharpCode.Core.ICommand was replaced with WPF ICommand

SharpDevelop 4.x was still using our own ICommand interface, which we introduced back in the .NET 1.0 days. Now that System.Windows.Input.ICommand is independent from WPF (it was moved to System.dll so that it can be used by Win8 apps), we decided to remove our own ICommand interface and just use the equivalent interface from the .NET framework.

There are some minor API differences, so we kept the old base class "AbstractMenuCommand" around, so that existing commands do not require any modifications. For new commands, you should derive from "SimpleCommand" instead of "AbstractMenuCommand".

SD.PropertyService

The Get()/Set() methods no longer support nested Properties objects or lists of elements -- you will need to use the new dedicated GetList()/SetList()/NestedProperties() methods for that.

The Get() method no longer causes the default value to be stored in the container; and GetList() results in a read-only list - an explicit SetList() call is required to store any modifications. However, a nested properties container still is connected with its parent, and any changes done to the nested container will get saved without having to call the SetNestedProperties() method.

The property service now uses XAML serialization instead of XML serialization. This might require some changes to your classes to ensure they get serialized correctly:

  • Make sure the class being serialized is public so that it can be constructed from XAML code
  • Do not expose public fields; use properties instead.

We decided on XAML serialization because it works OK to preserve settings when upgrading to another SharpDevelop version that adds additional properties to the deserialized class (and has a different full assembly name due to the version number). It also has a much faster startup time than the XmlSerializer.

SD.ParserService

The result of a parser run (ParseInformation) now may contain a fully parsed AST.
The ParserService may cache such full ASTs, but may also drop them from memory at any time. Currently, this is implemented by keeping the last 5 accessed files in the cache.
Every parse information also contains an IUnresolvedFile instance with the type system information. This IUnresolvedFile is stored permanently (both in the ParserService and in the IProjectContents).

Solution model

The class 'Solution' has been replaced with the interface 'ISolution'.
The static events that report changes to the solution (e.g. project added) no longer exist on IProjectService; instead the ISolution.Projects collection itself has a changed event.

Text editor and document services

In SharpDevelop 4.x, it was possible to use IDocument.GetService(typeof(ITextEditor)) to find the editor that presents the document.
This is no longer possible in SharpDevelop 5, as the same IDocument may be used by multiple editors (once issue #303 is implemented).

ITextEditor and IDocument now use separate service containers.
ITextEditor.GetService() will also return document services, but not the other way around.

The attributes [DocumentService] and [TextEditorService] are used to mark the service interfaces that are available in the document and in the editor respectively. The attributes exist purely for documentation purposes.

View content services

Instead of casting a view content to an interface "var x = viewContent as IEditable;",
SD5 must use "var x = viewContent.GetService<IEditable>()".

This allows the view content implementation to be flexible where the interface is implemented (it no longer is necessary to implement everything in the same class).

Interfaces that are supposed to be used as view content services are documented using the [ViewContentService] attribute. In the case of the AvalonEditViewContent, all text editor and document services are also available via IViewContent.GetService().

OpenedFile models

The SD-1234 refactoring still makes life difficult for IViewContent implementations. We have concrete plans for fixing this in the 5.0 release, but this didn't make it into Beta 1. All view contents will likely need to be adjusted once this change lands.

 

Published Jan 26 2014, 05:17 PM by DanielGrunwald
Filed under: ,

Comments

No Comments
Powered by Community Server (Commercial Edition), by Telligent Systems
Don't contact us via this (fleischfalle@alphasierrapapa.com) email address.