Table of Contents

English | 日本語 | 简体中文 | 繁體中文 | 한국어 |

模組開發

這篇文件說明如何撰寫符合 ZYC.Framework Host 執行階段模型的模組:以依賴注入為入口,使用 URI 驅動 Tab 導覽,按需接入主選單,並清楚分離公開契約與 WPF 實作。

什麼時候建立模組

當一個功能需要由 ZYC.Framework Host 發現並載入,而不是直接編譯進 Shell 時,就應該做成模組。模組可以提供 View、Tab factory、主選單項目、設定、狀態、背景服務與命令列選項。

如果功能契約需要被其他模組重用,請建立 *.Abstractions 專案。公開 DTO、常數、服務介面、選單 Provider 介面放在這裡。WPF View、TabItem 實作與執行階段註冊邏輯放在實作專案。

建議專案結構

區域 常見檔案 職責
Abstractions *ModuleConstants.cs, I*MainMenuItemsProvider.cs, service interfaces 其他模組可引用的公開契約,不依賴 WPF 實作細節。
Implementation Module.cs 模組生命週期入口。註冊 factory、選單項目、Provider 與服務。
Navigation *TabItemFactory.cs, *TabItem.cs 匹配 zyc:// 或 app URI,並建立 Tab 實例。
Menu *MainMenuItem.cs, optional *MainMenuItemsProvider.cs 加入使用者可見命令,用於導覽到模組 Tab 或執行模組動作。
UI UI/*View.xaml, UI/*View.xaml.cs TabItem 使用的 WPF View。

Abstractions 專案不要暴露 WPF 控制項型別。如果需要命令契約,可以在 Abstractions 中使用 System.Windows.Input.ICommand

生命週期

方法 時機 用途
RegisterAsync(ContainerBuilder builder) Autofac 容器建構之前。 註冊必須在整個容器中可用的服務。
LoadAsync(ILifetimeScope lifetimeScope) 容器建構之後,且模組處於啟用狀態時。 註冊 Tab factory、主選單項目、狀態列項目與執行階段 Hook。
AfterLoadedAsync(ILifetimeScope lifetimeScope) 所有啟用模組載入完成之後。 需要依賴其他模組已完成註冊的跨模組初始化。

大多數 UI 模組只需要實作 LoadAsync

最小 View 模組

對於單一 View 模組,可以在 LoadAsync 中註冊 simple tab factory:

using Autofac;
using ZYC.CoreToolkit.Extensions.Autofac;
using ZYC.Framework.Abstractions;
using ZYC.Framework.Abstractions.Tab;
using ZYC.Framework.Core;
using MyCompany.Tools.UI;

namespace MyCompany.Tools;

internal class Module : ModuleBase
{
    public override Task LoadAsync(ILifetimeScope lifetimeScope)
    {
        lifetimeScope.RegisterSimpleTabItemFactory(
            new SimpleTabItemFactoryInfo(typeof(ToolsView)));

        return Task.CompletedTask;
    }
}

這是 minimal 專案範本使用的模式。當模組只需要把一個 WPF UserControl 暴露為 Tab 時,這種方式就足夠。

帶路由的 Tab 模組

如果模組需要穩定 URI、路由匹配、參數、客製單例行為或多個 Tab,請使用 TabItemFactoryBase

using Autofac;
using ZYC.CoreToolkit.Extensions.Autofac.Attributes;
using ZYC.Framework.Abstractions.Tab;
using ZYC.Framework.Core;
using ZYC.Framework.Modules.Reports.Abstractions;

namespace ZYC.Framework.Modules.Reports;

[RegisterSingleInstance]
[TabItemRoute(Host = ReportsModuleConstants.Host)]
internal class ReportsTabItemFactory : TabItemFactoryBase
{
    public override async Task<ITabItemInstance> CreateTabItemInstanceAsync(
        TabItemCreationContext context)
    {
        await Task.CompletedTask;
        return context.Resolve<ReportsTabItem>(
            new TypedParameter(
                typeof(TabReference),
                new TabReference(context.Uri)));
    }
}

TabItemRouteAttribute 可以依 SchemeHostPathPathMatch 匹配 URI。如果多個 factory 匹配同一個 URI,Priority 較高的 factory 會優先。TabItemFactoryBase 預設 IsSingle = true;如果模組允許開啟多個實例,請覆寫它。

TabItem 與 View

帶路由的 Tab 通常用 TabItemInstanceBase<TView> 包裝 WPF View:

using Autofac;
using ZYC.CoreToolkit.Extensions.Autofac.Attributes;
using ZYC.Framework.Abstractions.Tab;
using ZYC.Framework.Core.Tab;
using ZYC.Framework.Modules.Reports.Abstractions;
using ZYC.Framework.Modules.Reports.UI;

namespace ZYC.Framework.Modules.Reports;

[Register]
[ConstantsSource(typeof(ReportsModuleConstants))]
internal class ReportsTabItem : TabItemInstanceBase<ReportsView>
{
    public ReportsTabItem(
        ILifetimeScope lifetimeScope,
        TabReference tabReference) : base(lifetimeScope, tabReference)
    {
    }
}

Tab 生命週期行為放在 TabItem 中,視覺行為放在 View 中。這樣可以把路由、Tab 識別與 UI 組合拆開。

主選單入口

選單項目通常導覽到模組 URI:

using Autofac;
using ZYC.CoreToolkit.Extensions.Autofac.Attributes;
using ZYC.Framework.Abstractions.MainMenu;
using ZYC.Framework.Core;
using ZYC.Framework.Modules.Reports.Abstractions;

namespace ZYC.Framework.Modules.Reports;

[RegisterSingleInstance]
internal class ReportsMainMenuItem : MainMenuItem
{
    public ReportsMainMenuItem(ILifetimeScope lifetimeScope)
    {
        Info = new MenuItemInfo
        {
            Title = ReportsModuleConstants.Title,
            Icon = ReportsModuleConstants.Icon
        };

        Command = lifetimeScope.CreateNavigateCommand(ReportsModuleConstants.Uri);
    }
}

然後從模組註冊:

public override Task LoadAsync(ILifetimeScope lifetimeScope)
{
    lifetimeScope.RegisterTabItemFactory<ReportsTabItemFactory>();
    lifetimeScope.RegisterExtensionsMainMenuItem<ReportsMainMenuItem>();

    return Task.CompletedTask;
}

優先使用符合功能位置的既有選單 Provider,例如 File、View、Tools、Extensions 或 About。只有當功能擁有多個子命令時,才增加模組自己的 Provider。

設定與狀態

實作 IConfigIState 的型別會在模組註冊期間從 settings 目錄讀取,並自動註冊到 Autofac。使用者可編輯設定使用 config;執行階段持久化內容,例如選中路徑、待處理操作、視窗狀態,使用 state。

不要把大型業務資料放進 config/state 類別。它們應保持小型、可序列化,並且能承受版本演進。

模組載入與依賴

Host 會發現命名類似 ZYC.Framework.Modules*.dll 的標準模組組件,然後追加 ModuleConfig.AdditionalAssemblyNames 中列出的組件。ModuleConfig.DisabledAssemblyNames 中的組件會被發現,但不會作為啟用模組載入。

依賴關係會透過對其他模組 *.Abstractions.dll 的引用推斷。如果模組 A 引用了 ZYC.Framework.Modules.B.Abstractions.dll,執行階段就可以報告 A 依賴 B,而不需要 A 直接引用 B 的 WPF 實作。

檢查清單

  • 公開常數與契約放在 *.Abstractions
  • WPF View 與 TabItem 放在實作專案。
  • 只有必須在容器建構前存在的服務才放進 RegisterAsync
  • Tab factory 與選單項目在 LoadAsync 中註冊。
  • 穩定 URI 路由使用 TabItemRouteAttribute
  • 新增選單 Provider 前,優先使用既有主選單 Provider。
  • config/state 類別保持小型且可序列化。