# Gemini: a WPF shell framework for building IDE-like desktop applications

> Gemini is a WPF framework for applications with the shape of an IDE, built on AvalonDock for docking and Caliburn Micro for MVVM, with an extensible module and tool model. It suits a Windows developer who needs a document-and-pane shell rather than a single window, and its release history and its package line are both things to check before adopting it.

**tgjones/gemini** — Gemini is an IDE framework similar in concept to the Visual Studio Shell. It uses AvalonDock and has an MVVM architecture based on Caliburn Micro.

- Repository: https://github.com/tgjones/gemini
- Stars: 1,150 · Forks: 294
- Language: C#
- License: NOASSERTION
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/tgjones-gemini

## What a Gemini application is made of

Gemini is a WPF framework designed specifically for building IDE-like applications, and it allows you to build your WPF application by composing separate modules, which provides a nice way of separating out the code for each part of your application.

A module is a class with a couple of attributes and a base class, and the example is short enough to read as the shape of the whole thing:

```csharp
[Export(typeof(IModule))]
public class Module : ModuleBase
{
	[Import]
	private IPropertyGrid _propertyGrid;

    public override IEnumerable<Type> DefaultTools
    {
        get { yield return typeof(IInspectorTool); }
    }

	public override void Initialize()
	{
		var homeViewModel = IoC.Get<HomeViewModel>();
		Shell.OpenDocument(homeViewModel);

		_propertyGrid.SelectedObject = homeViewModel;
	}
}
```

Three things are happening. The `[Export]` attribute registers the module with the container, so the shell discovers it without you listing it. The `[Import]` on the property grid field means the container supplies the inspector, so the module does not have to know how it is built. And `Initialize` is where the module does its work, opening a document into the shell and selecting an object in the inspector.

The `DefaultTools` property is the docking side of it, and it returns types rather than instances, which is what lets the framework decide where to place them. Tools are usually docked to the sides of the window, and the `IInspectorTool` in the example is what the inspector panel hosts.

So the model is a shell with a document area, a set of dockable tool panels, and a module system that composes them. That is a specific shape, and it is the shape of every IDE, every modelling tool and every log analyser ever built.

## Documents, and the boilerplate the base classes remove

Documents are usually displayed in the main area in the middle of the window, and to create a new document type you simply inherit from the `Document` class.

```csharp
public class SceneViewModel : Document
{
	public override string DisplayName
	{
		get { return "3D Scene"; }
	}

	private Vector3 _position;
	public Vector3 Position
	{
        get { return _position; }
        set
        {
            _position = value;
            NotifyOfPropertyChange(() => Position);
        }
    }
}
```

The `DisplayName` override is what appears in the document tab, and the `NotifyOfPropertyChange` call is the Caliburn Micro convention that tells the view to refresh, without you implementing `INotifyPropertyChanged` by hand.

Opening one is a single call, `Shell.OpenDocument(new SceneViewModel());`, and the `Shell` is defined in `ModuleBase`, or you can retrieve it from the IoC container with `IoC.Get<IShell>()`. The view is found by convention rather than by you wiring it up, so you then create a `SceneView` view and Caliburn Micro uses a convention-based lookup to find the correct view.

The bigger convenience is `PersistedDocument`, for a document that needs to be loaded from and saved to a file, which removes a lot of the boilerplate code you would usually have to write. You only need to implement the `DoNew`, `DoLoad` and `DoSave` methods, and the framework handles the dirty tracking, the file dialogs and the file extension handling:

```csharp
public class EditorViewModel : PersistedDocument
{
	private EditorView _view;
	private string _originalText;

	protected override Task DoNew()
	{
		_originalText = string.Empty;
		ApplyOriginalText();
		return TaskUtility.Completed;
	}

	protected override Task DoLoad(string filePath)
	{
		_originalText = File.ReadAllText(filePath);
		ApplyOriginalText();
		return TaskUtility.Completed;
	}

	protected override Task DoSave(string filePath)
	{
		var newText = _view.textBox.Text;
		File.WriteAllText(filePath, newText);
		_originalText = newText;
		return TaskUtility.Completed;
	}
```

The `ApplyOriginalText` helper in the example is the interesting part, because it is where the dirty flag comes from. It sets the text and then subscribes to `TextChanged`, comparing the current text against the original and setting `IsDirty` accordingly. That is a small piece of logic that every text editor has to get right, and the base class gives you the flag and the plumbing around it.

The `OnViewLoaded` override is where the view is captured, which is how the model reaches the control it needs to read from.

## The two libraries it stands on

Gemini builds on some excellent libraries, and knowing which is which tells you what you would have to assemble yourself without it.

The first is AvalonDock, and the note is specific. It uses Dirkster99's fork, because it supports .NET Core. That detail is more informative than it looks. AvalonDock is a docking library with a long history in the WPF world, and the fork matters because the .NET Framework original could not be brought forward to .NET Core, and a framework that only runs on .NET Framework is a framework with a shorter life ahead of it.

The second is Caliburn Micro, which is the MVVM layer. The repository description says Gemini has an MVVM architecture based on Caliburn Micro, and the code confirms it in the shape of the APIs. `ModuleBase` gives you `Shell`, the container is available through `IoC.Get<T>()`, views are found by convention, and property change notification is the `NotifyOfPropertyChange(() => Property)` call rather than an explicit interface implementation. Those are all Caliburn Micro idioms, and Gemini has shaped its own API around them rather than wrapping them awkwardly.

So the division of labour is sensible. Gemini provides the shell, the docking, the module system and the document model, and Caliburn Micro provides the MVVM plumbing and the IoC container, and AvalonDock provides the layout.

The themes follow the same division. Gemini ships with two themes, a Light theme and a Blue theme, and there is also an in-development Dark theme. The screenshots for the light and blue themes are in the repository under `doc/`.

The repository layout is a conventional .NET one with a `src/` directory, a `doc/` directory, an `eng/` directory, a `global.json` pinning the SDK, a `version.json`, a `CHANGELOG.md` and a `LICENCE.txt`.

And the `eng/` directory turns out to be the most interesting part of the layout, because the continuous build section says the project files and `eng/package-contract.json` are the authoritative dependency and asset inventory, and CI rejects packages that drift from that contract.

## The package contract, and why the CI rejects drift

The continuous builds section is short and it describes a much more disciplined release process than the version history would suggest.

GitHub Actions builds every push and pull request for the `net48` and `net10.0-windows` targets. Successful runs provide the validated NuGet and symbol packages as a downloadable workflow artifact. The project files and `eng/package-contract.json` are the authoritative dependency and asset inventory, and CI rejects packages that drift from that contract. Net6/net7 remain available from the legacy package line. Publication remains restricted to approved upstream releases. And the CI workflow at `.github/workflows/ci.yml` holds the exact build and release contract.

Three things stand out.

The two build targets are .NET Framework 4.8 and .NET 10 on Windows. Building both on every push means the framework still works on the .NET Framework, which matters for a library whose users are often on locked-down enterprise desktops, and it means the modern path is exercised continuously rather than occasionally.

The package contract is the unusual bit and the most useful thing here. A package that ships a set of NuGet modules has a dependency graph that is easy to break by accident, because adding a `PackageReference` to one project changes what the others resolve to. Declaring the inventory in a JSON file and failing the build when the packages drift from it turns an invisible packaging regression into a failed check.

And the symbol packages are published alongside the NuGet packages, which is a small thing that makes stack traces from a consuming application point at real source lines.

The migration guidance is explicit about who needs to read it. Applications and extensions moving from the previous package line or the earlier .NET Framework target should read the Caliburn Micro 5 migration notes in `doc/caliburn-5-migration.md`.

That is a significant dependency, incidentally. Moving to Caliburn Micro 5 is a framework-level change for anything built on Caliburn, not a version bump, and pointing migration notes at the exact document is the kind of thing that saves a week.

## The version history, and the gap that a reader should notice

The recent releases are v0.7.0 on 2018-10-12 and v0.6.2 on 2015-09-20, and the last push was on 2026-10-14.

So the last tagged release is from 2018, while the repository is being worked on in 2026. That gap is the first thing to reconcile when you are evaluating the project, and the continuous build section is where the reconciliation starts, because it describes a current build matrix, a current package contract and a current workflow.

The most likely reading is that the NuGet line and the tagged releases and the repository's build have diverged, which is common for a library whose maintainer moved to a continuous release model. The `GeminiWpf` package identifier and the new `Gemini.Modules.*` packages are the new line, and the previous package line and the earlier .NET Framework target are what the migration note is for.

There is one more detail in the setup that is a small trap. The base package ID is `GeminiWpf`, to distinguish it from another NuGet package with the same name. So `Gemini` in the installation instructions means the `GeminiWpf` package, and getting that wrong means installing somebody else's package.

The module packages are named by function, and the list is the clearest statement of what a Gemini application can be built from: `Gemini.Modules.CodeCompiler`, `Gemini.Modules.CodeEditor`, `Gemini.Modules.ErrorList`, `Gemini.Modules.GraphEditor`, `Gemini.Modules.Inspector`, `Gemini.Modules.Output` and `Gemini.Modules.PropertyGrid`.

Some modules have dependencies on other modules, and the documentation says that is taken care of by the NuGet package dependency system, so you install the one you want and the rest arrive.

The licence field for the repository is one that GitHub cannot classify, and the file is `LICENCE.txt` with the British spelling, which is the older convention in the .NET world and matches the era of the project.

## Conclusion

Gemini fits a Windows team building a tool that needs docked panels, a document area, an inspector and an error list, because all of that shell work is already done and an application can start from a bootstrapper and a module. Skip it if your application is a single window, since the framework is heavy for that, and skip it if you are starting a new project, because the last tagged release is v0.7.0 from 2018 and the continuous build work since then is a separate effort on a new package line. Before you start, read the Caliburn Micro 5 migration notes, since applications and extensions moving from the previous package line or the earlier .NET Framework target are pointed there, and check the current NuGet package identifiers, because the base package is `GeminiWpf` rather than `Gemini` to avoid a name collision.

## FAQ

### What is the Gemini framework for WPF?

It is a WPF framework designed specifically for building IDE-like applications, allowing you to build your application by composing separate modules, and it builds on AvalonDock for docking and Caliburn Micro for MVVM.

### How do I start a new WPF application with Gemini?

Install the Gemini NuGet package, delete MainWindow.xaml, remove the StartupUri attribute from App.xaml, add the Gemini XML namespace, and add the AppBootstrapper to a ResourceDictionary inside Application.Resources.

### What does a Gemini module look like?

A class marked with `[Export(typeof(IModule))]` that inherits from ModuleBase, with an Initialize method that can open documents through the shell and select objects in imported tool panels such as the property grid.

### Which Gemini module packages are available on NuGet?

Gemini.Modules.CodeCompiler, Gemini.Modules.CodeEditor, Gemini.Modules.ErrorList, Gemini.Modules.GraphEditor, Gemini.Modules.Inspector, Gemini.Modules.Output and Gemini.Modules.PropertyGrid, on top of the base package whose ID is GeminiWpf.

### What are the build and release arrangements for Gemini?

GitHub Actions builds every push and pull request for the net48 and net10.0-windows targets, the project files and eng/package-contract.json are the authoritative dependency inventory that CI rejects drift from, and publication is restricted to approved upstream releases. The last tagged release was v0.7.0 in 2018 and the last push was on 2026-10-14.

## Sources

- [Issues](https://github.com/tgjones/gemini/issues)
- [README](https://github.com/tgjones/gemini/blob/master/README.md)
- [Releases](https://github.com/tgjones/gemini/releases)
- [tgjones/gemini on GitHub](https://github.com/tgjones/gemini)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/tgjones-gemini
