本文档定义星栈的模块开发、清单、UI、生命周期、目录和打包规范。当前实现基于 .NET 10、Avalonia 12.1 和独立 AssemblyLoadContext。
- 容器外壳只负责窗口、应用发现、导航、安装和加载。
- 应用必须独立成 .NET 类库,不能引用
AsterDock.Host。 - 应用通过
AsterDock.Contracts与容器交互;内置应用可额外引用AsterDock.UI复用统一设计系统,但不能引用AsterDock.Host。 - 应用主界面必须是 Avalonia
Control,推荐使用UserControl。 - 应用必须释放自身创建的资源、后台任务和事件订阅。
- 应用应同时兼容 Windows 和 macOS;平台专用功能必须显式保护。
MyApplication.Module/
├─ MyApplication.Module.csproj
├─ app.json
├─ MyApplicationModule.cs
├─ Models/
├─ Services/
├─ ViewModels/
├─ Views/
│ ├─ MyApplicationView.axaml
│ └─ MyApplicationView.axaml.cs
└─ Assets/
应用逻辑较多时,可以另外建立不依赖 Avalonia 的 Core 项目:
MyApplication.Core/ 业务逻辑、数据处理
MyApplication.Module/ Avalonia UI 和容器入口
应用模块目标框架必须为 net10.0。建议开启 CopyLocalLockFileAssemblies,确保第三方依赖和本机运行库能够进入应用目录。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>MyApplication.Module</RootNamespace>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\AsterDock.Contracts\AsterDock.Contracts.csproj" />
<ProjectReference Include="..\AsterDock.UI\AsterDock.UI.csproj" Private="false" />
<PackageReference Include="Avalonia" Version="12.1.2" />
</ItemGroup>
<ItemGroup>
<None Update="app.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>要求:
- 不得引用
AsterDock.Host。 - Avalonia 版本必须与容器保持一致。
- 引用
AsterDock.UI时使用Private="false",由宿主提供共享程序集和 Semi/Ursa 主题。 - 使用原生库时,必须把对应平台文件放入
runtimes/<RID>/native/。 - 不要把其他版本的
AsterDock.Contracts.dll当作私有 API 使用。
入口类必须:
- 实现
IApplicationModule。 - 需要容器服务时同时实现
IApplicationContextAware。 - 提供公开的无参数构造函数。
- 通过
CreateView()返回应用主视图。 - 在
Dispose()中释放资源。
using AsterDock.Contracts;
using Avalonia.Controls;
using MyApplication.Module.Views;
namespace MyApplication.Module;
public sealed class MyApplicationModule : IApplicationModule, IApplicationContextAware
{
private MyApplicationView? _view;
private IApplicationContext? _context;
public void Initialize(IApplicationContext context)
=> _context = context;
public Control CreateView()
=> _view ??= new MyApplicationView(_context!);
public void Dispose()
{
_view?.DisposeResources();
_view = null;
_context = null;
}
}容器会先调用 Initialize(),再调用 CreateView(),并缓存返回的控件。应用不应在每次显示时重复创建昂贵资源。旧应用可以只实现 IApplicationModule,因此新增上下文接口不会破坏现有应用。
应用根目录必须包含 app.json:
{
"id": "my-application",
"name": "我的应用",
"description": "应用功能说明",
"version": "1.0.0",
"entryAssembly": "MyApplication.Module.dll",
"entryType": "MyApplication.Module.MyApplicationModule",
"icon": "app",
"category": "工具",
"order": 20
}字段说明:
| 字段 | 必填 | 说明 |
|---|---|---|
id |
是 | 全局唯一应用标识,只使用字母、数字、.、-、_ |
name |
是 | 应用显示名称 |
description |
否 | 设置页和应用选择器中的功能说明 |
version |
是 | 建议使用语义化版本,例如 1.2.0 |
entryAssembly |
是 | 入口程序集文件名,必须位于应用根目录内 |
entryType |
是 | 实现 IApplicationModule 的完整类型名 |
icon |
否 | 图标类型;当前支持 home、printer、monitor、network、serial,其他值使用默认应用图标 |
category |
否 | 悬浮应用选择器中的分类,默认是“工具” |
order |
否 | 显示顺序,数字越小越靠前 |
同一 id 存在多个应用时,容器选择版本号更高的一个。
构建后的应用目录必须自包含运行所需的托管依赖和本机依赖:
MyApplication/
├─ app.json
├─ MyApplication.Module.dll
├─ MyApplication.Module.deps.json
├─ AsterDock.Contracts.dll
├─ ThirdParty.Dependency.dll
├─ Assets/
└─ runtimes/
├─ win-x64/native/
├─ osx-x64/native/
└─ osx-arm64/native/
app.json 和 entryAssembly 指定的 DLL 必须位于应用根目录。容器不会搜索 bin/Debug 等开发目录。
- 主视图使用
UserControl,不要替换容器的主窗口。 - 应用需要打开普通窗口或模态窗口时,优先使用
IApplicationContext.Windows,不要自行修改ApplicationLifetime.MainWindow。 - 优先使用
AsterDock.UI的共享控件和ad-*语义样式,避免在页面内重复定义卡片、按钮、输入框和状态样式。 - 使用
DynamicResource引用App*设计令牌,以自动适配亮色与暗色主题。 - 页面内样式只处理业务专属控件,例如终端日志、波形或设备预览。
常用设计系统入口:
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:ui="using:AsterDock.UI.Controls">
<Grid RowDefinitions="64,*">
<ui:PageHeader Title="应用名称" Subtitle="页面说明" />
<Border Grid.Row="1" Classes="ad-card">
<StackPanel>
<Border Classes="ad-segmented">
<StackPanel Orientation="Horizontal">
<Button Classes="ad-segment-item selected" Content="选项一" />
<Button Classes="ad-segment-item" Content="选项二" />
</StackPanel>
</Border>
<Button Classes="ad-primary" Content="主要操作" />
</StackPanel>
</Border>
</Grid>
</UserControl>当前公共样式包括 ad-card、ad-workspace-card、ad-section-card、ad-icon、ad-primary、ad-accent、ad-segmented、ad-segment-item、ad-section-header、ad-compact、ad-status-badge、ad-status-text、ad-muted、ad-heading、ad-page-title、ad-nav、ad-chip、ad-flat、ad-scrim 和 ad-on-scrim。预览遮罩可配合 AppScrimBrush 与 AppOnScrimBrush。
容器通过 IWindowService 统一管理应用创建的窗口。被管理的窗口会在模块卸载、重新加载或容器退出时自动关闭。
打开普通的所属窗口:
var window = new MyToolWindow();
_context.Windows.Show(window);打开不隶属于容器主窗口的独立窗口:
_context.Windows.Show(new MyIndependentWindow(), owned: false);打开模态窗口并取得返回值:
var accepted = await _context.Windows.ShowDialogAsync<bool>(
new MyConfirmDialog());避免重复打开同一种窗口:
_context.Windows.ShowOrActivate(
"main-editor",
() => new MyEditorWindow());同一个 key 已存在时,容器会激活原窗口;窗口关闭后,该 key 可以再次创建窗口。
IWindowService 提供:
| 方法 | 用途 |
|---|---|
Show(window, owned) |
打开并跟踪普通窗口 |
ShowOrActivate(key, factory, owned) |
打开或激活单例窗口 |
ShowDialogAsync<TResult>(window) |
打开模态窗口并返回结果 |
CloseAll() |
关闭当前应用通过服务打开的全部窗口 |
提示、确认和失败不要再自建提示窗口,也不要只写在容易被忽略的状态文字里。通过 IApplicationContext.Feedback 使用容器内的对话框和通知,详见 对话框与通知。
容器通过 IApplicationContext.Shell 向应用提供受控导航。应用不得通过查找主窗口控件或反射 Host 类型来切换页面。
| 成员 | 用途 |
|---|---|
Applications |
当前已加载应用的只读摘要 |
RecentApplications |
当前容器会话中的最近使用记录 |
StateChanged |
应用列表或最近使用发生变化 |
OpenApplication(id) |
按应用 ID 打开应用 |
ShowSettings() |
打开容器设置工作区 |
ShowApplicationSwitcher() |
显示悬浮应用选择器 |
TryExecuteApplicationAction(appId, actionId) |
执行其他应用公开的快捷动作 |
_context.Shell.OpenApplication("invoice-printer");
_context.Shell.ShowSettings();
_context.Shell.ShowApplicationSwitcher();订阅 StateChanged 的应用必须在 Dispose() 中退订。不要长期保存 ApplicationSummary;应用重新加载后应从 Applications 重新读取。
CPU、GPU、内存、磁盘和网络指标必须使用 IApplicationContext.SystemMetrics。应用不得自行创建硬件采集器或重复启动轮询定时器。
private IDisposable? _metricsSubscription;
public void Activate()
{
_metricsSubscription ??= _context.SystemMetrics.Subscribe(snapshot =>
{
Dispatcher.UIThread.Post(() => CpuUsage = snapshot.CpuUsage);
});
}
public void Deactivate()
{
Interlocked.Exchange(ref _metricsSubscription, null)?.Dispose();
}容器只运行一个底层采集器:
- 第一个订阅者出现时按需启动,每秒生成一份共享快照。
- 新订阅者可以先收到最近一次缓存快照。
- 最后一个订阅者退订后等待 8 秒;期间没有新订阅者才停止采集并释放系统计数器。
- 页面离开可视树、悬浮窗关闭或模块释放时必须退订。
- 订阅回调来自后台线程;更新 Avalonia 控件或视图模型时必须切换到 UI 线程。
- 设备静态信息通过
GetDeviceDetailsAsync()获取,由容器缓存,不应反复查询系统。 - 应用只能依赖 Contracts 中的
ISystemMetricsService,不得引用 Host 的具体实现。
应用需要把常用功能发布到容器托盘时,可以选择实现 IApplicationQuickActionProvider。容器仍然只依赖 Contracts,不会引用具体应用程序集。
public sealed class MyApplicationModule :
IApplicationModule,
IApplicationContextAware,
IApplicationQuickActionProvider
{
public IReadOnlyList<ApplicationQuickAction> GetQuickActions() =>
[
new ApplicationQuickAction(
"toggle-widget",
"显示/隐藏悬浮窗",
ToggleWidget)
];
}要求:
-
动作
id在当前应用内必须唯一且保持稳定。 -
动作显示名称应简短明确。
-
动作由 Avalonia UI 线程调用,不得执行阻塞操作。
-
动作创建的窗口仍必须通过
IApplicationContext.Windows打开。 -
应用卸载时必须关闭动作创建的窗口并释放委托引用。
-
容器可以选择展示哪些动作;实现接口不代表动作一定出现在所有平台的托盘菜单中。
-
文件和目录选择必须使用 Avalonia
StorageProvider:
var storage = TopLevel.GetTopLevel(this)?.StorageProvider;- 优先使用 Avalonia FluentTheme 已有组件,例如
Button、MenuFlyout、ListBox、TabControl、ToggleSwitch、NumericUpDown和ProgressBar。 - 应用主视图必须适应容器尺寸变化,避免依赖固定窗口位置。
- 耗时工作必须异步执行,不得阻塞 Avalonia UI 线程。
容器提供以下动态资源:
| 资源 | 用途 |
|---|---|
AppPageBrush |
页面背景 |
AppSurfaceBrush |
卡片和主要表面 |
AppChromeBrush |
标题和工具区域 |
AppBorderBrush |
边框和分隔线 |
AppMutedBrush |
次要文字 |
AppPreviewBrush |
预览区域背景 |
使用示例:
<Border Background="{DynamicResource AppSurfaceBrush}"
BorderBrush="{DynamicResource AppBorderBrush}"
BorderThickness="1"/>微应用存在详情页、编辑页等内部页面时,可以选择实现 IApplicationNavigationProvider。宿主会在右上角子胶囊中自动显示返回按钮:
public bool CanGoBack => _navigationStack.Count > 1;
public event EventHandler? NavigationStateChanged;
public void GoBack()
{
if (!CanGoBack) return;
_navigationStack.Pop();
NavigationStateChanged?.Invoke(this, EventArgs.Empty);
}要求:
- 内部页面层级发生变化后触发
NavigationStateChanged。 GoBack()只处理微应用内部导航,不得切换宿主应用。- 无内部页面时
CanGoBack返回false。 - 模块释放后不得继续触发导航事件。
应用被替换、重新加载或容器关闭时会调用 Dispose()。容器会在模块释放时关闭通过 IWindowService 创建的窗口。应用仍必须释放:
Bitmap、文件流和数据库连接。CancellationTokenSource和后台任务。- 定时器、文件监听器和操作系统句柄。
- 对容器、窗口或静态对象的事件订阅。
- 应用创建但仍保持引用的临时文件。
避免使用无法卸载的全局静态状态。否则即使模块使用可回收 AssemblyLoadContext,程序集仍可能无法卸载。
不要把用户数据或配置写入应用安装目录。实现 IApplicationContextAware 后,使用容器分配的数据目录:
var dataRoot = _context.DataDirectory;容器按应用 id 创建数据目录:AsterDock/AppData/<application-id>。
临时输出使用 Path.GetTempPath(),并在不再需要时清理。
共享逻辑应保持平台无关。平台专用代码必须显式判断:
if (OperatingSystem.IsWindows())
{
// Windows 实现
}
else if (OperatingSystem.IsMacOS())
{
// macOS 实现
}要求:
- 不使用 WPF、WinForms 或仅 Windows 可用的 UI 类型。
- Windows P/Invoke 必须位于 Windows 分支之后。
- macOS 外部命令使用参数列表传参,不拼接未转义的用户输入。
- 发布前至少验证
win-x64、osx-x64和osx-arm64。
容器会扫描两类位置。
内置应用:
<容器目录>/Apps/
用户应用:
- Windows:
%LOCALAPPDATA%\AsterDock\Apps - macOS:
~/Library/Application Support/AsterDock/Apps
用户可以在设置页加载应用目录或 .appbundle 文件。
.appbundle 是 ZIP 格式的应用包。压缩包根目录必须直接包含 app.json,不能再嵌套一层应用文件夹。
官方发布的轻应用采用通用包:同一个 .appbundle 可由所有受支持的 AsterDock 宿主加载。应用如包含按系统或 CPU 架构区分的原生依赖,应把这些文件一并放入包内,并在运行时选择当前宿主对应的文件;不要为每个设备分别发布应用包。
正确:
MyApplication.appbundle
├─ app.json
├─ MyApplication.Module.dll
└─ runtimes/...
错误:
MyApplication.appbundle
└─ MyApplication/
├─ app.json
└─ MyApplication.Module.dll
当前安全限制:
- 解压后总大小不超过 512 MB。
- 文件条目不超过 2000 个。
- 禁止路径穿越。
- 应用包解压到用户缓存后再加载。
可以参考项目脚本生成发票应用包:
powershell -ExecutionPolicy Bypass -File scripts/package-invoice-app.ps1 -RuntimeIdentifier win-x64bash scripts/package-invoice-app.sh Release 1.0.0 osx-arm64dotnet build src/MyApplication.Module/MyApplication.Module.csproj检查输出目录至少包含:
app.json- 入口 DLL
.deps.json- 业务专属的第三方托管依赖
.appbundle 由官方脚本创建时会移除由容器统一提供的 AsterDock.Contracts、Avalonia、SkiaSharp、HarfBuzzSharp、主题库,以及这些库在应用根目录和 runtimes 下的本机文件(libSkiaSharp、libHarfBuzzSharp、libAvaloniaNative、av_libglesv2)和所有 .pdb 调试符号。这些本机文件按文件名删除,runtimes 目录和其中的业务本机库会保留,只有清空后的目录会被去掉。应用包只能由版本兼容的星栈容器加载;PDFium 等业务专属本机库必须保留在 runtimes/<RID>/native/。
然后通过星栈设置页加载目录或 .appbundle,验证:
- 应用出现在“已加载的应用”列表。
- 描述、版本、分类和图标正确。
- 点击应用 Logo 可以打开主视图。
Space + A应用选择器中可以找到并打开应用。- 重新加载或退出后没有文件锁和后台任务残留。
- 目标框架是
net10.0 - 仅引用
AsterDock.Contracts和可选的AsterDock.UI,未引用 Host - Avalonia 版本与容器一致
- 入口类实现
IApplicationModule - 需要窗口或数据目录时实现
IApplicationContextAware - 入口类具有公开无参数构造函数
-
app.json位于应用根目录 -
id唯一且格式合法 -
version已更新 - 描述、分类和显示顺序已填写
- 入口 DLL 和所有依赖已复制
- Windows、macOS 和 Linux 本机运行库完整(例如发票应用的 PDFium)
- UI 不阻塞主线程
- 新窗口通过
IWindowService打开并由容器跟踪 - 容器导航仅通过
IApplicationContext.Shell完成 - 硬件指标仅通过
IApplicationContext.SystemMetrics订阅,并在不可见或释放时退订 - 已在
Dispose()中退订Shell.StateChanged - 托盘快捷动作 ID 唯一且执行过程不阻塞 UI
-
Dispose()能释放所有资源 -
.appbundle根目录结构正确 - 已完成 Windows 和 macOS 验证
当前标准参考应用:
src/AsterDock.UI(共享主题、设计令牌和公共控件)src/SerialDebugger.Module(统一设计系统与多卡片工作区示例)src/InvoicePrinter.Modulesrc/InvoicePrinter.Module/app.jsonsrc/InvoicePrinter.Module/InvoicePrinterApplicationModule.cssrc/DeviceInformation.Core(跨平台硬件采集和平台能力降级)src/DeviceInformation.Module(主视图和独立透明悬浮窗)src/Home.Module(默认主页、容器导航和设备概览)
容器契约与加载器:
src/AsterDock.Contractssrc/AsterDock.Contracts/IApplicationContext.cssrc/AsterDock.Host/Modules/ModuleCatalog.cssrc/AsterDock.Host/Modules/AppModuleLoadContext.cssrc/AsterDock.Host/Modules/AppPackageExtractor.cssrc/AsterDock.Host/Services/ApplicationWindowService.cs