Architecture Redesign
From MonoDevelop
| Table of contents |
Introduction
Looks like everybody hates the internal architecture of MD and everybody has ideas about how to improve it. This document is a collection of existing problems and proposed solutions. It is probably not complete, and I expect people to add more issues and suggestions.
GUI Thread
One of the problems I find when writing code for MonoDevelop is that the user interface can only be modified from the GUI thread. This means that, for example, if I have a background thread that needs to add something in the task pane, I need to do it through the DispatchService.
However, the fact that TaskService.AddTask (and many other methods) needs to run in the gui thread is common knowledge not documented anywhere, and even if it was documented, this information wouldn't be reliable. For example, the TaskService itself does not modify the user interface, but an OpenTaskView object is subscribed to the TaskAdded event, and that one changes de UI. This makes the development of addins really hard.
Proposed solution
In order to simplify the development and to make MonoDevelop more stable we need to follow a policy like the following:
- Methods from services or from objects accessed through services can be safely called from any thread. It is the responsibility of the service to marshal the call into the GUI thread if needed.
- Events from services or from objects within services can be raised in any thread. It is the resposibility of the subscriber to marshal the event call into the correct thread if needed.
- Calls and events from GUI objects need, of course, to be executed in the GUI thread.
As an example, the method Project.AddFile() can be safely called from any thread (a Project object is accessed through the ProjectService). However, if the ProjectBrowserView is subscribed to the FileAddedToProject event, it needs to marshal the call into the GUI thread, since the Project object may raise the event in any thread.
To simplify even more the development we'll implement two helper classes:
- A base class for classes that need safe access to the GUI. All method calls to objects of that class would be automatically marshaled and executed in the GUI thread. This can be implemented using ContextBoundObject and the interception sinks it provides. This would be really useful for example to make the status bar service thread safe by only changing the base class.
- A class for generating wrapper delegates that would automatically marshal calls into the gui thread, so we could do something like:
taskService.TasksChanged += (EventHandler) DispatchService.CreateGuiDispatch (new EventHandler (ShowResults));
This would generate a wrapper delegate that would queue the call into the gui thread.
IDE Logic
We need to draw a line between classes that implement the IDE user interface and the classes that are independent from the GUI (that implement the internal logic of the IDE).
Right now, this separation is not clear, and the result is that some code is repeated in several classes, there are several ways of doing the same things, and it is difficult to reuse code.
As a sanity check, I should be able to write a console application that, for example, loads a project, adds a file to the project and compiles the project, without needing to link with GTK#. This is impossible right now.
Proposed solution
The first and most important step is to move all code that is really independent of the GUI into GUI-free classes. And notice that Commands are not GUI-free classes. Combine and Project must be GUI-free classes. A second step would be to move the GUI-free classes into an independent assembly.
Combine/Project class model
The current project class model is not extensible. Language addins can create new types of projects with custom information, but it is not possible to create new types of files, or groups of files. There are only 8 types of files, which are hardcoded in the MonoDevelop.Internal.Project.Subtype enum: Code, Directory, WinForm, WebForm, XmlForm, WebService, WebReferences and Dataset. An addin should be able to create new types of project items, and to extend the existing ones.
Proposed solution
Implement a new class model for projects based on a tree of nodes. In this model, everything are nodes: combines, projects, files or whatever. The root node is a combine, which contains projects nodes, which contains file nodes, and so on. Addins can create new types of nodes and freely integrate them in the main node tree. Nodes don't need to be bound to physical files, we could have for example a database table node.
Nodes will share several resources and mechanisms:
- Serialization: A project file will be generated by serializing the tree of nodes into XML. MD will have its own serializer, the existing XmlSerializer wouldn't be powerful enough for this. Nodes created by addins will be serialized just like any other node.
- Event dispatching. We need an event model that is more flexible than the traditional .NET way of implementing events. That event model would allow subscribers to subscribe to events at any node level. For example, an addin may need to be notified when a specific project file is modified (so it would subscribe to the Modified event of that specific project file instance). On the other hand, another addin may need to be notified when any file inside a project is modified (without needing to subscribe an event for every file instance). Events will flow from the leafs to the root of the node tree, so addins can subscribe at the level of choice. Addins will also be able to define its own events and reuse this event dispatching infrastructure.
This new model should also allow us to get rid of methods like this one from IProjectService:
/// <remarks> /// Only to be called by AbstractProject /// </remarks> void OnRenameProject(ProjectRenameEventArgs e);
which is clearly a design flaw.
- Custom properties: Addins will be able to add custom properties to existing nodes. For example, a SVN addin may need to add SVN status information to project nodes and project file nodes.
Services
The existing Service system is really annoying to use. Calls like this one:
IProjectService projectService = (IProjectService) ServiceManager.GetService (typeof(IProjectService)); projectService.SaveCombine();
are repeated over and over, add burden to the code and are probably slow. Calls like this one:
FileUtilityService fileUtilityService; fileUtilityService = (FileUtilityService)ServiceManager.GetService(typeof(FileUtilityService)); return fileUtilityService.AbsoluteToRelativePath (path1, path2);
are just ridiculous.
Proposed solution
Access services through static properties, something like this:
Application.ProjectService.SaveCombine ();
I don't care if Application.ProjectService internally makes a ServiceManager.GetService() call, I just don't want to know.
Commands
I don't like the current way of implementing and dispatching commands, because:
- Commands should not implement IDE logic. It should only include GUI operations (such as showing a dialog) and calls to the IDE logic layer.
- A single command can't have a different behavior depending on the window that has the focus. For example, the same "Delete" command can't be used to delete chars when the focus is in the text editor and to delete files when the project browser has the focus. We currently need two commands for doing this (OK, we can handle both cases in the same command class, but this is not extensible so it won't work for addins that want to reuse commands).
Proposed solution
Add a subscription mechanism to the command system. That is, classes could subscribe to commands, and handle commands like if they where normal events. In the command handler, a class could do several things:
- Do nothing. In this case, the command would follow the default handling path and execute the default handler if no other handler is found.
- Modify the command. For example, the project tree pane could intercept the Add File command, and assign to it the currently selected project. After that the command would execute the default handler that would add the file to that project.
- Handle the command. For example, an UML addin could intercept the Delete command and remove the selected class in a class diagram.
- Cancel the command.
BTW, why do we need to model startup operations as commands? It doesn't make sense.

Powered by MediaWiki