Auto DictionariesはUmbraco 17にアップグレード冒険の中で、動的アイテム付きツリーを作った。
でもこの新しいツリーの実装方法は、本当に複雑だと思う。
実装していた時、いろんなブログ記事やUmbracoのドキュメントなどを探したけど、見つけた方法はどれも違っていた。
僕がしたい実装に近い例は、あまり見つからなかった。
だから、ツリーの実装には時間がかかりすぎた。
そのため、僕の方法を説明したいために、このブログ記事を書くことにした。
この方法は Umbraco 17.0.1 で実装した。
すべてのステップを説明する。
そして、最終的な結果はこれだ。

動的アイテム付きツリーを作る。そのあと、workspaceでアイテムを表示する。
この結果を作るために、いくつかのものを実装しなければいけない。
sidebarに新しいsectionを追加する新しい用語やアブストラクトな概念が多いと思う。
だから、僕はこんらんしていたから、この図を作った!

僕はこの図でこう考えている
始める前に、プロジェクトの構造を話したほうがいい。
「(第2回)Auto Dictionaries:Umbraco 13 から 17 への冒険 — プロジェクト構造と TypeScript への移行準備」というブログ記事で詳しく説明した。
でも今は少しまとめる。
1つのmanifests.tsのファイルを使っている。
そのファイルの中でextensionRegistry.registerMany(manifests);を使っている。
だから、毎回manifests.tsのファイルを話した時、同じファイルを話す!
重要!
僕のTypeScriptはまだ上手じゃないから、ほかの方法があると思う。
全部のコードを見たい時、Auto Dictionraiesのリポジトリで読める。
では行きましょう!
まずはバックエンドのコードのコントローラーを作ったほうがいい。
このコントローラーはすべてのツリーアイテムを返す。
public async Task<ActionResult<PagedViewModel<AutoDictionariesModel>>> GetChildren()
{
return Ok(new PagedViewModel<AutoDictionariesModel>
{
Items = await GetAllViews(),
Total = 100
});
}PagedViewModelの戻り値が必要だよ!
書くバックエンドのコードはそれだけ!
次に、TypeScriptでHey APIのnpmのパッケージを使うから、型付きAPIを作る。
そして、「Section Sidebar App」を実装する。
図でこれは1だ。
「Section Sidebar App」はsidebarに新しいsectionを作れる。
manifest.tsにはsectionSidebarAppを追加する。
const menuSidebarApp: UmbExtensionManifest = {
type: "sectionSidebarApp",
kind: "menu",
alias: "autoDictionaries.sidebarapp",
name: "Auto Dictionarie sidebar menu",
weight: 100,
meta: {
label: "Auto Dictionary",
menu: menu.alias, //<-- 次に作る
},
conditions: [
{
alias: "Umb.Condition.SectionAlias",
match: "Umb.Section.Translation",
},
],
};kindがmenuと menuWithEntityActionsがある.
kindを追加しない時、自分のツリーアイテムのUIを実装しなければいけない。
例えば、
import { LitElement, html } from "lit";
import { customElement } from "lit/decorators.js";
@customElement("auto-dictionaries-sidebar")
export class AutoDictionariesSidebarElement extends LitElement {
protected createRenderRoot() {
return this; // allow Umbraco global styling
}
render() {
return html`
<umb-tree alias="autoDictionaries.tree"></umb-tree>
`;
}
}sectionSidebarAppを追加した時、これはけっかだ。

これから、TreeRootを実装するまで、あまりUIがない。
「Menu」はツリーを登録するコンテナーだ。
図でこれは2だ。
manifest.tsにはmenuで追加する。
const menu: UmbExtensionManifest = {
type: "menu",
alias: "autoDictionaries.menu",
name: "Auto dictionaries menu"
};次に、バックエンドのコントローラーからデータを取得する方法が必要だ。
だから、「Tree Repository」を作ったほうがいい。
repositoryを実装するために、 UmbTreeRepositoryBaseとUmbTreeServerDataSourceBaseも実装しなければいけない。
UmbTreeRepositoryBase - 取得したデータを ツリーから使えるようにするUmbTreeServerDataSourceBase - API からデータを取るまずはUmbTreeServerDataSourceBaseを実装する。
新しいファイルを作ったほうがいい。
そのファイル(data-source.ts)でUmbTreeServerDataSourceBase<any, any>を継承した時、4つの関数を実装しなければいけない。
getRootItemsgetAncestorsOfgetChildrenOfmapperこのツリーの方法は1つのアイテムがあるから、getAncestorsOfとgetChildrenOfの戻り値は少なくできる。
const getAncestorsOf = async (): Promise<UmbDataSourceResponse<any>> => {
return { data: { items: [] } };
};
const getChildrenOf = async (): Promise<UmbDataSourceResponse<any>> => {
return { data: { items: [] } };
};そして、getRootItemsにはツリーアイテムを返したい。
そのアイテムはバックエンドのコントローラーから取得するため、TypeScriptの型付きAPIを使う。
const getRootItems = async (): Promise<UmbDataSourceResponse<any>> => {
try {
const { data, error } = await AutoDictionariesService.getChildren();
if (error || !data) {
return { error: error ? new Error(JSON.stringify(error)) : new Error('Unknown error') };
}
return {
data: {
items: data.items ?? [],
total: data.total ?? 0
}
};
} catch (err) {
return { error: err instanceof Error ? err : new Error('Failed to fetch root items') };
}
};最後の関数mapperは、ここでバックエンドのツリーアイテムモデルをUmbTreeItemModelにマッピングする
const mapper = (item: AutoDictionariesModel): UmbTreeItemModel => {
return {
unique: item?.key ?? null,
parent: { unique: null, entityType: "auto-dictionaries-root" },
name: item.name ?? "unknown",
entityType: "auto-dictionaries-item",
hasChildren: false,
isFolder: false,
icon: "icon-book"
};
};次に、UmbTreeRepositoryBaseを実装する。
新しいファイル(repository.tree.ts)を作ったほうがいい。
そのファイルにはUmbTreeRepositoryBase<UmbTreeItemModel, UmbTreeRootModel> 継承しなければいけない。
import type { UmbControllerHost } from "@umbraco-cms/backoffice/controller-api";
import type { UmbApi } from "@umbraco-cms/backoffice/extension-api";
import { UmbTreeRepositoryBase, type UmbTreeItemModel, type UmbTreeRootModel } from "@umbraco-cms/backoffice/tree";
import { autoDictionariesTreeDataSource } from "./auto-dictionaries.data-source";
export class autoDictionariesTreeRepository extends UmbTreeRepositoryBase<UmbTreeItemModel, UmbTreeRootModel>
implements UmbApi {
constructor(host: UmbControllerHost) {
super(host, autoDictionariesTreeDataSource);
}
async requestTreeRoot() {
const data: UmbTreeRootModel = {
unique: null,
entityType: "auto-dictionaries-root",
name: "Auto Dictionaries",
icon: "icon-book",
hasChildren: false,
isFolder: true,
};
return { data };
}
}
export { autoDictionariesTreeRepository as api };最後、manifest.tsにはrepositoryを追加する。
const repository: UmbExtensionManifest = {
type: "repository",
alias: "autoDictionaries.tree.repository",
name: "AutoDictionaries Repository Settings",
api: () => import("./auto-dictionaries.repository.tree.js"),
};ツリーの構造を定義する。
図でこれは3だ。
これを実装するために、manifest.tsにtypeがtreeを追加する。
const tree: UmbExtensionManifest = {
type: "tree",
kind: "default",
alias: "autoDictionaries.tree",
name: "Auto Dictionaries Tree Settings",
meta: {
repositoryAlias: repository.alias,
},
};重要!
kindの値はdefaultにする必要がある。さもないと動作しない。
「Tree Root」はツリーのルートノード。
図でこれは4だ。
これを実装するために、manifest.tsにtypeがmenuItemを追加する。
const menuItem: UmbExtensionManifest = {
type: 'menuItem',
//kind: "tree",
alias: 'autoDictionaries.tree.menu.item',
name: 'Auto Dictionarie Tree Item',
meta: {
label: 'Auto dictionaries',
icon: 'icon-book',
entityType: "auto-dictionaries-root",
menus: [
menu.alias
],
treeAlias: tree.alias,
}
};kind: "tree"を使ったほうがいいけど、僕は使わない。
使わない時、この違うことに気がついた。
manifest.tsにはkind: "tree"を使わない。
repository.tree.tsにはhasChildren: trueと子ノードがあるのに、下の矢印が見られない。
kind: "tree"を使わない時、URLはここになる。
/umbraco/section/translation/workspace/auto-dictionaries-rootでも、kind: "tree"を使う時、URLはここになる
/umbraco/section/translation/workspace/auto-dictionaries-root/edit/nullツリー終わった!

※この「…」は、表示されていないアイテムがまだあることを示している
今、その ツリーアイテムにナビゲーションするために、「Workspace」を作る。
「Workspace」を作るために、workspaceとworkspaceContextとworkspaceViewの実装をしなければいけない。
Workspace
└ Context (状態とロジック)
└ Views (UI)僕は「Workspace」を作る時、たいてい2つ作っている。
僕はその「Workspace」の名前を使っているけど、何でも使わない。
この構造を使っている。
# Workspace
# Overview
- manifest.ts
- workspace.context.ts
- workspace.element.ts
- overview.element.ts
# Item
- manifest.ts
- workspace.context.ts
- workspace.element.ts
- item.element.tsまずはoverviewのworkspaceを追加する。
バックエンドのツリーアイテムを取得するために、「Workspace Context」を使っている。
workspace.context.ts
export class autoDictionariesWorkspaceContext extends UmbControllerBase implements UmbWorkspaceContext {
public readonly workspaceAlias: string = "autoDictionaries.workspace";
#repository: AutoDictionariesRepository;
#views = new UmbObjectState<AutoDictionariesModel[] | undefined>(undefined);
public readonly views = this.#views.asObservable();
constructor(host: UmbControllerHostElement) {
super(host);
this.provideContext(UMB_WORKSPACE_CONTEXT, this);
this.#repository = new AutoDictionariesRepository(this);
}
async load() {
const data = await this.#repository.getAllViews();
if (data) {
this.#views.setValue(data);
}
}
getRepository(): AutoDictionariesRepository {
return this.#repository;
}
getEntityType(): string {
return "auto-dictionaries-root";
}
}
export default autoDictionariesWorkspaceContext;そして、overviewのmanifest.tsを作ると、これを追加する。
const context: UmbExtensionManifest = {
type: "workspaceContext",
alias: "autoDictionaries.workspace.context",
name: "Auto dictionaries workspace context",
js: () => import("./workspace.context.js"),
conditions: [
{
alias: "Umb.Condition.WorkspaceAlias",
match: "autoDictionaries.workspace",
},
],
};これはworkspaceのコンテナーだ
overviewのworkspace.element.ts
@customElement("auto-dictionaries-root")
export class autoDictionariesWorkspaceRootElement extends UmbElementMixin(LitElement) {
_workspaceContext: autoDictionariesWorkspaceContext;
constructor() {
super();
this._workspaceContext = new autoDictionariesWorkspaceContext(this);
}
render() {
return html`
<umb-workspace-editor headline="Auto Dictionaries" alias="autoDictionaries.workspace" .enforceNoFooter=${true}></umb-workspace-editor>`;
}
}
export default autoDictionariesWorkspaceRootElement;overviewのmanifest.tsにはこれを追加する
const workspace: UmbExtensionManifest = {
type: "workspace",
alias: "autoDictionaries.workspace",
name: "Auto dictionaries workspace",
js: () => import('./workspace.element.js'),
meta: {
entityType: "auto-dictionaries-root",
}
};ここで、すべてのUIを追加したほうがいい。
overview.element.ts
@customElement("auto-dictionaries-overview")
export class autoDictionariesOverviewViewElement extends UmbElementMixin(LitElement) {
#workspaceContext?: autoDictionariesWorkspaceContext;
@state()
private _views?: AutoDictionariesModel[] = [];
constructor() {
super();
this.consumeContext(UMB_WORKSPACE_CONTEXT, (context) => {
this.#workspaceContext = context as autoDictionariesWorkspaceContext;
this.#observeContext();
});
}
#observeContext() {
if (!this.#workspaceContext) return;
this.observe(this.#workspaceContext.views, (view) => {
this._views = view;
});
}
connectedCallback() {
super.connectedCallback();
if (this.#workspaceContext) {
this.#workspaceContext.load();
}
}
private _getUrl(view: AutoDictionariesModel) {
if (!view?.id) return;
return `/umbraco/section/translation/workspace/auto-dictionaries-item/edit/${view.id}`;
};
private _renderView(view: AutoDictionariesModel) {
if (!view) return;
return html`<a href=${this._getUrl(view)}>view.name</a>`;
}
render() {
return html`${repeat(this.views?? [], (view) => view.id, (view) => this._renderView(view))}`;
}
}
export default autoDictionariesOverviewViewElement;overviewのmanifest.tsにはこれを追加する。
const workspaceViews: Array<UmbExtensionManifest> = {
type: "workspaceView",
alias: "autoDictionaries.workspaceView.overview",
name: "Auto dictionaries workspace overview view",
js: () => import("./views/overview/overview.element.js"),
weight: 20,
meta: {
label: "#autoDictionaries_overview",
pathname: "overview",
icon:"icon-book"
},
conditions: [
{
alias: "Umb.Condition.WorkspaceAlias",
match: "autoDictionaries.workspace"
}
]
};overviewのworkspaceを終わった。

今、overviewのworkspaceがあるけど、アイテムの詳細を見たいからitemのworkspaceも作った。
今回もworkspaceとworkspaceContextとworkspaceViewの実装をしなければいけない。
workspace.context.ts
export class AutoDictionariesItemWorkspaceContext extends UmbControllerBase implements UmbWorkspaceContext {
public readonly workspaceAlias: string = "autoDictionaries.item.workspace";
#repository: AutoDictionariesRepository;
#unique = new UmbObjectState<string | undefined>(undefined);
public readonly unique = this.#unique.asObservable();
#currentItem = new UmbObjectState<AutoDictionariesModel | undefined>(undefined);
public readonly currentItem = this.#currentItem.asObservable();
constructor(host: UmbControllerHostElement) {
super(host);
this.provideContext(UMB_WORKSPACE_CONTEXT, this);
this.#repository = new AutoDictionariesRepository(this);
}
setUnique(unique: string | undefined) {
this.#unique.setValue(unique);
if (unique) {
this.load(unique);
}
}
getUnique(): string | undefined {
return this.#unique.getValue();
}
async load(id: string) {
const data = await this.#repository.getViewById(id);
if (data) {
this.#currentItem.setValue(data);
}
}
getCurrentItem(): AutoDictionariesModel | undefined {
return this.#currentItem.getValue();
}
getRepository(): AutoDictionariesRepository {
return this.#repository;
}
getEntityType(): string {
return "auto-dictionaries-item";
}
}
export default AutoDictionariesItemWorkspaceContext;itemのmanifest.ts
const itemContext: UmbExtensionManifest = {
type: "workspaceContext",
alias: "autoDictionaries.item.workspace.context",
name: "Auto dictionaries item workspace context",
js: () => import("./workspace.context.js"),
conditions: [
{
alias: "Umb.Condition.WorkspaceAlias",
match: "autoDictionaries.item.workspace",
},
],
};itemのworkspaceはちょっと違う。
Auto Dictionaries:Umbraco 13 から 17 への冒険の中で、どうやってitemのIDをworkspaceに取得したほうがいいか。
答えを長らく探したのに、 あまり答えを見つけなかった。
でも!やっと答えを見つけた!
答えはUmbRoute[]だよ!
workspace.context.ts
@customElement('auto-dictionaries-item-workspace')
export class AutoDictionariesItemWorkspaceElement extends UmbElementMixin(LitElement) {
#workspaceContext: AutoDictionariesItemWorkspaceContext;
#routes: UmbRoute[] = [
{
path: 'edit/:id',
component: () => import('./item.element'),
setup: (_component, info) => {
const id = info.match.params.id;
if (id) {
this.#workspaceContext.setUnique(id);
}
},
},
{
path: '',
redirectTo: 'edit/',
}
];
constructor() {
super();
this.#workspaceContext = new AutoDictionariesItemWorkspaceContext(this);
}
#handleBack() {
window.history.pushState({}, '', "/umbraco/section/translation/workspace/auto-dictionaries-root/");
}
render() {
return html`
<umb-workspace-editor .enforceNoFooter=${true}>
<uui-button id="back-button" slot="header" compact @click=${this.#handleBack} label="Back">
<uui-icon name="icon-arrow-left"></uui-icon>
</uui-button>
<div slot="header">Auto dictionaries</div>
<umb-router-slot id="router-slot" .routes=${this.#routes}></umb-router-slot>
</umb-workspace-editor>`;
}
}
export default AutoDictionariesItemWorkspaceElement;
declare global {
interface HTMLElementTagNameMap {
'auto-dictionaries-item-workspace': AutoDictionariesItemWorkspaceElement;
}
}itemのmanifest.tsにはこれを追加する
const itemWorkspace: UmbExtensionManifest = {
type: "workspace",
alias: "autoDictionaries.item.workspace",
name: "Auto dictionaries item workspace",
js: () => import('./workspace.element.js'),
meta: {
entityType: "auto-dictionaries-item",
},
};item.element.ts
@customElement("auto-dictionaries-item-edit")
export class autoDictionariesItemViewElement extends UmbElementMixin(LitElement) {
private _routeBuilder?: UmbModalRouteBuilder;
#workspaceContext?: AutoDictionariesItemWorkspaceContext;
#observerCleanup?: () => void;
@state()
private _item?: AutoDictionariesModel;
constructor() {
super();
new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL)
.addAdditionalPath('general/:entityType')
.onSetup((params) => {
return { data: { entityType: params.entityType, preset: {} } };
})
.observeRouteBuilder((routeBuilder) => {
this._routeBuilder = routeBuilder;
});
this.consumeContext(UMB_WORKSPACE_CONTEXT, (context) => {
this.#workspaceContext = context as AutoDictionariesItemWorkspaceContext;
this.#observeContext();
});
}
#observeContext() {
if (!this.#workspaceContext) return;
this.observe(this.#workspaceContext.currentItem, (item) => {
this._item = item;
});
}
render() {
return html`
<umb-body-layout header-transparent>
<div id="autoDictionaries-layout">
<div id="autoDictionaries-main">{{this._item.name}}</div>
</div>
</umb-body-layout>`;
}
}
export default autoDictionariesItemViewElement;itemのworkspaceを終わった。

Umbraco 13の動的アイテム付きツリーはもっと簡単だと思う!
Auto Dictionariesのアップグレードの冒険の間に、あまりドキュメントがないから、よく他のUmbraco 17のパッケージやブログやUmbraco 17のソースコードなどを見た。
そのアップグレードした時、ツリーの実装はずっと一番難しいと思う。
今は、Umbraco のドキュメントも増えたのに、Umbracoの例は簡単すきて、自分のプロジェクトをした時、あまり手伝わない。
このブログ記事を書いたから、将来の僕を手伝うといい。
でも、みなさんにも手伝うといい!

ヨハネス・ランツ
ウェブ開発者・経験8年