AppAlling
Welcome to AppAlling โ a pluggable, extensible WinForms desktop shell built on modern .NET 9 practices.
AppAlling is intentionally "boring by design."
The host application contains very little logic of its own; its main job is to:
- Wire up dependency injection (via
Microsoft.Extensions.DependencyInjection
) - Provide a centralized state store (Redux-like, using
IStore<T>
) - Load plugins from a designated plugins folder
- Compose menus, commands, and tool windows dynamically at runtime
- Host the main application shell (
MainForm
)
Everything else โ commands, menus, view models, tool windows, and services โ comes from plugins.
โจ Features
- Reactive / Observable state: based on
System.Reactive
for one-way data flow and change subscriptions. - Command bus with named
CommandDescriptor
objects and async execution support. - Menu composer that merges plugin-provided menu models into the main menu strip.
- Tool window factories that allow plugins to register their own forms and open them on demand.
- DI-first architecture using
IServiceCollection
andIServiceProvider
. - Pluggable: Drop new assemblies into the
Plugins
folder to extend functionality without touching host code.
๐ Getting Started
1. Clone and Build
git clone https://github.com/yourorg/AppAlling.git
cd AppAlling
dotnet build
2. Run the Host
dotnet run --project src/AppAlling.UI.WinForms
The app will start, load any plugins found in the Plugins
directory, and build the menu dynamically.
๐งฉ Creating a Plugin
A plugin implements one or more of the following contribution interfaces:
ICommandContribution
โ declares commands (IDs, titles, shortcuts)IMenuModelContribution
โ provides a menu hierarchy (MenuItemDescriptor
)IToolWindowContribution
โ declares tool windows (title + command ID)IAppallingPlugin
โ exposesPluginMetadata
and registers services viaConfigureServices
Minimal Example
public sealed class HelloWorldPlugin :
IAppallingPlugin,
ICommandContribution,
IMenuModelContribution
{
public PluginMetadata Metadata => new("hello.world", "Hello World", "1.0.0");
public void ConfigureServices(IServiceCollection services, IPluginContext ctx)
{
services.AddSingleton<ICommandContribution>(this);
services.AddSingleton<IMenuModelContribution>(this);
services.AddSingleton<ICommandExec>(new Exec("tools.sayHello", _ =>
{
MessageBox.Show("Hello from plugin!");
return Task.CompletedTask;
}));
}
public IEnumerable<CommandDescriptor> DescribeCommands()
=> [new("tools.sayHello", "Say Hello")];
public IEnumerable<MenuItemDescriptor> BuildMenuModel()
=> [new("&Tools", Children: [new MenuItemDescriptor("Say &Hello", "tools.sayHello")])];
}
Drop the resulting DLL in Plugins
and run the host โ your command and menu item appear instantly.
๐งช Testing
AppAlling uses TinyBDD for behavior-driven tests. Example scenario from the test suite:
[Scenario("Given MainForm with demo contributions; When I click Tools โ Say Hello; Then the bus executes 'demo.hello'")]
[StaFact]
public Task Clicking_menu_executes_command()
=> Given("MainForm with demo contributions", Given_mainform_with_demo_contributions)
.When("I click Tools โ Say Hello", ctx => When_I_click_menu_item(ctx.Form, "Tools", "Say Hello"))
.Then("the command bus executed 'demo.hello'", ctx => Then_bus_executed(ctx.Bus, "demo.hello"))
.AssertPassed();
๐ Project Structure
src/
AppAlling.Abstractions/ # Core contracts (ICommandContribution, IToolWindowContribution, etc.)
AppAlling.Application/ # State store, reducers, command bus
AppAlling.UI.WinForms/ # MainForm, MenuComposer, DI setup
AppAlling.PluginHost/ # PluginLoader, context creation
AppAlling.Plugins.HelloWorld/ # Sample plugin
tests/
AppAlling.Tests/ # Behavior-driven acceptance tests
AppAlling.Plugins.HelloWorld.Tests/ # Sample plugin tests
๐ค Contributing
- Fork the repo & create a feature branch
- Add your plugin / fix / feature
- Add TinyBDD scenarios for new behavior
- Submit a PR โ we love good tests โค๏ธ