Интерфейс, контролы и API WebPart
Руководство для разработчиков: как строить UI с контролами, обрабатывать действия пользователя и работать с данными Portal — на сервере (C#) и в браузере (JavaScript).
См. также: стили PortalUI, манифест (CSS/JS в пакете), SDK (контракт IWebPart), API каталога.
Архитектура: гибридный SSR
Заголовок раздела «Архитектура: гибридный SSR»Portal WebPart работает по модели гибридного серверного рендеринга:
Редактор страницы → properties в zones_content ↓csharpWebPartShell.js → POST /api/v1/webparts/invoke/{key}/render ↓C# IWebPart.RenderAsync → HTML ↓Браузер вставляет HTML + подключает CSS/JS из пакета ↓Пользователь кликает → POST /invoke/{key}/action (опционально) ↓C# IWebPart.HandleActionAsync → новый HTML ↓bind(container) из dist/main.js (опционально)| Слой | Где выполняется | Для чего |
|---|---|---|
| HTML | Сервер (C#) | Разметка, данные, формы, кнопки |
| CSS | Пакет .portalpart | Оформление, сетки, модалки |
| JS (bind) | Пакет .portalpart | Локальный UI без round-trip (поиск, модалки просмотра) |
| Actions | Сервер (C#) | Запись в списки, смена периода, валидация |
| Property Pane | Платформа | Настройки WebPart (listId, pageSize и т.д.) |
Важно: любые операции с данными, требующие проверки прав (чтение списков, создание элементов), должны выполняться на сервере через IWebPartContext. JavaScript в пакете имеет доступ к REST API браузера, но не должен быть единственным местом защиты.
1. Добавление интерфейса с контролами
Заголовок раздела «1. Добавление интерфейса с контролами»1.1. Базовая разметка в RenderAsync
Заголовок раздела «1.1. Базовая разметка в RenderAsync»HTML формируется в C# как строка и возвращается в WebPartResult. Корневой контейнер — с уникальным префиксом классов:
public async Task<WebPartResult> RenderAsync(IWebPartContext ctx, CancellationToken ct){ var title = ctx.Properties.GetString("title", "Мой виджет"); var html = new StringBuilder(); html.Append("""<div class="webpart-my-widget">"""); html.Append($"""<h3 class="webpart-my-widget__title">{WpHtml.Escape(title)}</h3>"""); html.Append(""" <div class="webpart-my-widget__toolbar"> <button type="button" class="btn btn--settings btn--sm" data-wp-action="refresh"> <i class="fa-solid fa-rotate" aria-hidden="true"></i> <span class="btn__text">Обновить</span> </button> </div> <p class="webpart-my-widget__body">Содержимое виджета</p> """); html.Append("</div>"); return new WebPartResult(html.ToString());}Вспомогательный класс для экранирования (обязателен для защиты от XSS):
internal static class WpHtml{ public static string Escape(string? text) => System.Net.WebUtility.HtmlEncode(text ?? ""); public static string EscapeAttr(string? text) => Escape(text).Replace("\"", """, StringComparison.Ordinal); public static string Hint(string text) => $"""<p class="webpart-hint">{Escape(text)}</p>"""; public static string Error(string text) => $"""<p class="form-error">{Escape(text)}</p>""";}1.2. Стили портала и контролы
Заголовок раздела «1.2. Стили портала и контролы»Кратко — самые частые классы:
| Класс | Назначение |
|---|---|
btn btn--settings btn--sm | Кнопка тулбара |
btn btn--primary btn--sm | Главное действие |
btn btn--ghost btn--sm | Второстепенное / отмена |
tbx__control | Поле ввода, <select>, <textarea> |
form-error / form-success | Ошибка / успех |
webpart-hint / webpart-empty | Подсказка / пусто |
loading-text | Индикатор загрузки |
Подробный справочник с примерами, токенами темы, msg / badge / blk / tbl и антипаттернами: Стили и контролы PortalUI.
Свой layout — в assets/main.css с префиксом .webpart-…. Подключение: manifest.md.
Пикеры person / lookup: готовые контролы PortalPersonPicker и PortalLookupPicker (data-portal-person-picker / data-portal-lookup-picker) — chip+search, single/multi, автоинициализация после render, значение в hidden. Подробно: Кастомная форма §6.
1.3. Типичные контролы
Заголовок раздела «1.3. Типичные контролы»Кнопки навигации (предыдущий/следующий период):
<button type="button" class="btn btn--settings btn--sm" data-wp-action="changePeriod" data-wp-delta="prev" data-wp-date="2026-07-06" data-wp-mode="week"> ‹</button>Выбор даты — <input type="date"> с data-wp-action (срабатывает на change):
<input type="date" class="tbx__control" data-wp-action="changePeriod" data-wp-mode="week" data-wp-date="2026-07-06" value="2026-07-06">Переключатель режима (неделя/месяц):
<button type="button" data-wp-action="changeMode" data-wp-mode="month" data-wp-date="2026-07-06"> Месяц</button>Форма в модальном окне — поля без data-wp-action, кнопка отправки с action и начальными data-wp-*:
<form id="my-form"> <label>Название <input type="text" class="tbx__control" name="title" required> </label> <label>Статус <select class="tbx__control" name="status" id="my-status"> <option value="Новая" selected>Новая</option> <option value="В работе">В работе</option> </select> </label></form><button type="button" id="my-submit" data-wp-action="createItem" data-wp-date="2026-07-06"> Создать</button>Таблица / таймлайн — ячейки-кнопки для выбора слота:
<button type="button" class="webpart-my-widget__slot" data-wp-action="selectSlot" data-wp-room-id="{roomId}" data-wp-start="2026-07-06T10:00:00" data-wp-end="2026-07-06T10:30:00"></button>Полные примеры разметки: TasksWebPart.cs, MeetingRoomBookingWebPart.cs.
1.4. HTML-шаблоны (Embedded Resource)
Заголовок раздела «1.4. HTML-шаблоны (Embedded Resource)»Для статической разметки (каркас формы, модалки, toolbar, карточки, карусель) удобнее вынести HTML в Templates/*.html и подключить как Embedded Resource в .csproj. Платформа принимает только итоговую строку WebPartResult.Html — способ сборки на ваше усмотрение.
Рекомендуемый гибридный подход:
| Часть | Где держать | Как собирать |
|---|---|---|
| Каркас формы, модалки, toolbar | Templates/*.html | WebPartTemplate.Load + {{key}} |
<option>, строки таблицы, циклы | C# | StringBuilder или string.Join |
| Ошибки, условные блоки | C# | if + WpHtml.Error(...) |
Структура исходников пакета:
my-widget/├── Templates/│ ├── widget.html ← embedded в DLL│ └── create-form.html├── MyWidgetWebPart.cs├── MyWidget.csproj ← <EmbeddedResource Include="Templates\*.html" />├── manifest.json└── dist/ ├── main.css ← assets для браузера └── main.jsПример в C# — SDK-хелпер WebPartTemplate из Portal.WebPart.Sdk:
private const string CreateFormTemplate = "Templates.create-form.html";
private static string RenderCreateFormModal(...){ var statusOptions = new StringBuilder(); foreach (var status in StatusChoices) { statusOptions.Append($"""<option value="{WpHtml.EscapeAttr(status)}">{WpHtml.Escape(status)}</option>"""); }
return WebPartTemplate.LoadAndApply( typeof(MyWebPart).Assembly, CreateFormTemplate, new Dictionary<string, string> { ["formError"] = string.IsNullOrEmpty(formError) ? "" : WpHtml.Error(formError), ["statusOptions"] = statusOptions.ToString(), });}Имя ресурса — manifest name из сборки (обычно {RootNamespace}.Templates.create-form.html) или суффикс Templates.create-form.html, если он уникален.
Важно: значения плейсхолдеров экранируйте вручную через WpHtml.Escape / EscapeAttr до подстановки. HTML-файлы из manifest.json (styles, scripts) отдаются браузеру и не доступны C# при RenderAsync.
Шаблоны в примерах (packages/webpart-examples/, не входят в поставку):
| Пакет | Файлы в Templates/ |
|---|---|
tasks | create-form, toolbar, legend, detail-modal |
meeting-room-booking | booking-form, toolbar, legend, detail-modal |
address-book | shell, employee-card |
birthdays | hero-header, empty-state, person-card |
birthdays-widget | header, carousel-shell, slide |
banners | carousel-shell, slide, empty-state |
hello-widget | widget |
current-user-widget | guest, profile |
Эталонный пример с формами и таймлайном: tasks.
2. Взаимодействие с контролами
Заголовок раздела «2. Взаимодействие с контролами»В Portal есть три паттерна взаимодействия. Их можно комбинировать в одном WebPart.
2.1. Серверные действия: data-wp-action
Заголовок раздела «2.1. Серверные действия: data-wp-action»Платформа (csharpWebPartShell.js) автоматически привязывает обработчики ко всем элементам с атрибутом data-wp-action:
- Пользователь кликает (или меняет
input[type="date"]/selectсdata-wp-action). - Браузер собирает все
data-wp-*атрибуты элемента в объектdata. - Отправляется
POST /api/v1/webparts/invoke/{manifestKey}/actionс телом:
{ "action": "changePeriod", "data": { "wpDelta": "prev", "wpDate": "2026-07-06", "wpMode": "week" }, "properties": { "listId": "..." }, "nodeId": "uuid-узла-страницы"}- Сервер вызывает
HandleActionAsyncи возвращает новый HTML целиком. - Контейнер WebPart заменяется; снова вызываются
_bindActions()иbind(container).
Правила именования атрибутов:
| В HTML | В data на сервере | Как читать в C# |
|---|---|---|
data-wp-action="book" | action = "book" | параметр action |
data-wp-room-id | wpRoomId | data.AsActionData().GetString("roomId") |
data-wp-start | wpStart | form.GetString("start") |
name="assignee_json" | wpAssigneeJson | form.GetJson("assigneeJson") |
data-wp-delta | wpDelta | form.GetString("delta") |
Префикс wp / snake_case / camelCase нормализует SDK — свой GetDataString копировать не нужно:
using Portal.WebPart.Sdk;
var form = data.AsActionData();var roomId = form.GetGuid("roomId");var start = form.GetString("start");var isActive = form.GetBoolean("isActive");var assignee = form.GetJson("assigneeJson"); // object/array или JSON-строка из hiddenМетоды: GetString, GetBoolean, GetGuid, GetInt32, GetNumber, GetJson, TryGet.
Эквивалент: data.GetActionString("title"), data.GetActionJson("assigneeJson").
Обработчик действий на сервере:
public async Task<WebPartResult> HandleActionAsync( string action, JsonElement data, IWebPartContext ctx, CancellationToken ct){ return action switch { "changePeriod" => await HandleChangePeriodAsync(ctx, data, ct), "changeMode" => await HandleChangeModeAsync(ctx, data, ct), "selectSlot" => await HandleSelectSlotAsync(ctx, data, ct), "createItem" => await HandleCreateItemAsync(ctx, data, ct), "cancelSelect" => await RenderMainViewAsync(ctx, ct), _ => new WebPartResult("", Error: "Неизвестное действие"), };}Состояние UI (выбранная дата, открытая форма, режим просмотра) хранится в серверном рендере: при каждом action C# заново строит HTML с нужными модалками, значениями полей и сообщениями об ошибках. Отдельного клиентского state store нет.
Особенности input[type="date"]: при change платформа кладёт выбранное значение в data.wpTarget (в дополнение к остальным data-wp-*).
2.2. Клиентская логика без сервера: хук bind
Заголовок раздела «2.2. Клиентская логика без сервера: хук bind»Для чисто клиентского поведения (фильтрация, раскрытие дерева, модалка «только просмотр») используйте scripts в манифесте и регистрацию в PortalWebPartClients:
(function () { const MANIFEST_KEY = 'demo.address-book'; // = id в manifest.json
function bind(root) { if (!root || root.dataset.uiBound === '1') return; root.dataset.uiBound = '1';
const searchInput = root.querySelector('.webpart-address-book__search-input'); searchInput?.addEventListener('input', () => applyFilters(root));
root.querySelectorAll('.webpart-address-book__tree-btn').forEach((btn) => { btn.addEventListener('click', () => { // переключение активного подразделения, фильтрация карточек applyFilters(root); }); }); }
window.PortalWebPartClients = window.PortalWebPartClients || {}; window.PortalWebPartClients[MANIFEST_KEY] = { bind(container) { const roots = container.classList?.contains('webpart-address-book') ? [container] : Array.from(container.querySelectorAll('.webpart-address-book')); roots.forEach(bind); }, };})();bind вызывается после каждого SSR-рендера и action. Защита от повторной привязки: root.dataset.uiBound = '1'.
Пример: address-book/dist/main.js — поиск и дерево подразделений без запросов к серверу. Данные для фильтрации сервер встраивает в JSON:
<script type="application/json" class="webpart-address-book__data" data-address-book-employees> [{"id":"...","fullName":"..."}]</script>2.3. Автосбор полей формы
Заголовок раздела «2.3. Автосбор полей формы»Платформа (csharpWebPartShell.js) при клике по data-wp-action сама собирает контролы с атрибутом name из ближайшей <form> или [data-wp-collect] и кладёт их в payload action (как wpTitle для name="title"). На сервере: data.AsActionData().GetString("title") / GetJson("assigneeJson").
- Явные
data-wp-*на кнопке не перезаписываются полями формы. - Опт-аут:
data-wp-no-collectна кнопке или контейнере. - Для create/edit элементов предпочтителен
PortalListItem.
Ручное копирование в dataset из bind (как в старых примерах meeting-room-booking) больше не обязательно для обычных полей.
2.4. Сравнение паттернов
Заголовок раздела «2.4. Сравнение паттернов»| Задача | Паттерн | Пример |
|---|---|---|
| Смена даты, пагинация | data-wp-action | meeting-room-booking, tasks |
| Создание / правка элемента списка | data-wp-action + автосбор формы + PortalListItem | custom-form-fields |
| Поиск по уже загруженным карточкам | bind + JSON в HTML | address-book |
| Модалка просмотра (без записи) | bind, без action | meeting-room-booking |
3. API на сервере (C# / IWebPartContext)
Заголовок раздела «3. API на сервере (C# / IWebPartContext)»Серверный контекст создаётся для каждого вызова render / action. Права текущего пользователя проверяются при каждом обращении к Lists/Libraries/Nodes.
3.1. Свойства и пользователь
Заголовок раздела «3.1. Свойства и пользователь»Объявление схемы, сценарии (в т.ч. список из другого узла) и все типы: Параметры WebPart.
// Property Pane (manifest.json → properties)var listId = ctx.Properties.GetGuid("listId");var pageSize = ctx.Properties.GetInt32("pageSize", 10);var showTitle = ctx.Properties.GetBoolean("showTitle", true);var title = ctx.Properties.GetString("title", "Заголовок");// Список из другого узла: nodePicker (scope: subtree) + listPicker dependsOn — см. properties.md §3.2
// Текущий пользователь и узел страницыvar userId = ctx.User?.Id;var displayName = ctx.User?.DisplayName ?? ctx.User?.Login;var nodeId = ctx.NodeId; // Guid? — узел страницы/формы (= ctx.Host.NodeId)
// Контекст формы списка (без Property Pane itemId)if (ctx.Host.IsListForm){ var item = await ctx.Host.GetOrNewListItemAsync(ct); // ctx.Host.ListId, ItemRef, FormMode — см. SDK § IWebPartHost}Расширения: WebPartPropertiesExtensions в IWebPart.cs.
Host: SDK § IWebPartHost, кастомная форма.
3.2. Списки (ctx.Lists)
Заголовок раздела «3.2. Списки (ctx.Lists)»Рекомендуемый путь — PortalListItem по internal_name:
using Portal.Contracts.Lists;
// Метаданные спискаvar list = await ctx.Lists.GetAsync(listId, ct);var listTitle = list.GetProperty("title").GetString();
// Элементы (с опциональным viewId и limit)var viewId = ctx.Properties.GetGuid("viewId");Guid? view = viewId == Guid.Empty ? null : viewId;var items = await ctx.Lists.GetListItemsAsync(listId, view, limit: 50, ct);
// Фильтрованная выборка (предпочтительно для больших списков)var filtered = await ctx.Lists.QueryListItemsAsync(listId, ListItemQueryBuilder.ForList(listId) .WhereField("title", f => f.Eq("GlobalSettings")) .Take(1) .Build(), ct);
foreach (var row in items){ var itemTitle = row.GetString("title"); var status = row.GetString("status");}
// Один элемент (itemRef — номер или guid)var item = await ctx.Lists.GetListItemAsync(listId, "12", ct);
// Создание (проверка права add на узел)var created = await ctx.Lists.NewListItemAsync(listId, ct);created["title"] = "Новая задача";created["status"] = "Новая";created["start_at"] = "2026-07-06";created["assigned_to"] = new Dictionary<string, object?>{ ["principalType"] = "user", ["id"] = userId.ToString(), ["display"] = displayName,};created["room"] = new Dictionary<string, object?>{ ["itemId"] = roomItemId.ToString(), ["display"] = "Переговорная 301",};await created.CreateAsync(ct);
// Обновить (merge — только изменённые поля)item["status"] = "Готово";await item.UpdateAsync(ct);Низкоуровнево: GetFieldsAsync + GetItemAsync / CreateItemAsync с UUID-ключами в field_values. Форматы значений — API списков, значения полей.
3.3. Библиотеки (ctx.Libraries)
Заголовок раздела «3.3. Библиотеки (ctx.Libraries)»var library = await ctx.Libraries.GetAsync(libraryId, ct);var files = await ctx.Libraries.GetFilesAsync(libraryId, limit: 20, ct: ct);
foreach (var file in files){ var fileId = Guid.Parse(file.GetProperty("id").GetString()!); var fileName = file.GetProperty("name").GetString(); var downloadHref = ctx.Libraries.DownloadUrl(fileId); // downloadHref = "/api/v1/files/{fileId}/download"}3.4. Узлы и маршруты
Заголовок раздела «3.4. Узлы и маршруты»if (ctx.NodeId is Guid nodeId){ var node = await ctx.Nodes.GetAsync(nodeId, ct); var children = await ctx.Nodes.GetChildrenAsync(nodeId, ct);
foreach (var child in children) { var childId = Guid.Parse(child.GetProperty("id").GetString()!); var href = await ctx.Routes.NodeHrefAsync(childId, ct); // href — человеко-читаемый путь, например "/otdel/it" }
var itemHref = await ctx.Routes.ListItemHrefAsync(nodeId, listId, itemId, ct);}3.5. Форматирование полей
Заголовок раздела «3.5. Форматирование полей»var fields = await ctx.Lists.GetFieldsAsync(listId, ct);var field = fields.First(f => f.GetProperty("internal_name").GetString() == "due_date");// Сырое значение для FormatValue — из схемы/UUID или из PortalListItem:var rawValue = item["due_date"]; // PortalListItemvar display = ctx.Fields.FormatValue(field, rawValue); // "06.07.2026 15:30" для datetimeПример таблицы: встроенный ListViewWebPart.
3.6. Типичный поток «загрузка → рендер → действие»
Заголовок раздела «3.6. Типичный поток «загрузка → рендер → действие»»public async Task<WebPartResult> RenderAsync(IWebPartContext ctx, CancellationToken ct){ var config = await LoadConfigAsync(ctx, ct); if (config.Error is not null) return new WebPartResult(config.Error);
var items = await ctx.Lists.QueryListItemsAsync(config.ListId, ListItemQueryBuilder.ForList(config.ListId) .View(ctx.Properties.GetGuid("viewId") is var v && v != Guid.Empty ? v : null) .Take(100) .Build(), ct); return new WebPartResult(BuildHtml(items, pending: null, error: null));}
private async Task<WebPartConfig> LoadConfigAsync(IWebPartContext ctx, CancellationToken ct){ var listId = ctx.Properties.GetGuid("listId"); if (listId == Guid.Empty) return new() { Error = WpHtml.Hint("Выберите список в настройках WebPart") };
try { await ctx.Lists.GetAsync(listId, ct); // проверка доступа return new() { ListId = listId }; } catch (Exception ex) { return new() { Error = WpHtml.Error(ex.Message) }; }}4. API в JavaScript (браузер)
Заголовок раздела «4. API в JavaScript (браузер)»4.1. Эндпоинты самого WebPart
Заголовок раздела «4.1. Эндпоинты самого WebPart»Из JS пакета или консоли браузера доступны вызовы через PortalApi (обёртка над /api/v1):
// Повторный рендер (обычно делает платформа сама)const res = await PortalApi.post('/webparts/invoke/contoso.my-widget/render', { properties: { listId: '...', pageSize: 10 }, nodeId: 'uuid-узла',});const html = res.data.html;const error = res.data.error;
// Действие (аналог клика по data-wp-action)const actionRes = await PortalApi.post('/webparts/invoke/contoso.my-widget/action', { action: 'refresh', data: { wpDate: '2026-07-06' }, properties: { listId: '...' }, nodeId: 'uuid-узла',});Контекст страницы для picker-ов в редакторе: GET /api/v1/webparts/context?nodeId={uuid}.
Ассеты пакета: GET /api/v1/webparts/assets/{manifestKey}/dist/main.v1.0.0.js.
4.2. PortalWebPartContext — удобная обёртка
Заголовок раздела «4.2. PortalWebPartContext — удобная обёртка»Модуль portalContext.js предоставляет клиентский аналог серверного API:
const ctx = PortalWebPartContext.create({ nodeId: 'uuid-узла-страницы', user: window.currentUser, // опционально});
// Узлыconst node = await ctx.nodes.get(nodeId);const children = await ctx.nodes.getChildren(nodeId);const resolved = await ctx.routes.resolvePath('/otdel/it');
// Спискиconst list = await ctx.lists.get(listId);const fields = await ctx.lists.getFields(listId);const items = await ctx.lists.getItems(listId, { viewId, top: 50, filter: { logic: 'and', conditions: [{ column: 'title', operator: 'eq', value: 'A' }] }, q: 'поиск',});const item = await ctx.lists.getItem(listId, itemId);const views = await ctx.lists.getViews(listId);
// Библиотекиconst library = await ctx.libraries.get(libraryId);const files = await ctx.libraries.getFiles(libraryId, { limit: 20 });const downloadUrl = ctx.libraries.downloadUrl(fileId);
// Права (node, list, library, page — без listId/libraryId)const canEdit = await ctx.permissions.check('list', listId, 'edit');// list_item / file: PortalApi.get('/permissions/check?...&listId=...')
// Полные примеры проверки и назначения — см. API: права доступа (раздел «Примеры из кода»)
// Форматированиеconst text = ctx.fields.formatValue(field, value);Прямой доступ к REST (создание, обновление, удаление):
// Создать элемент (нужно право add)await PortalApi.post(`/lists/${listId}/items`, { fieldValues: { 'field-uuid-title': 'Заголовок', 'field-uuid-status': 'Новая', },});
// Обновить элемент (право edit)await PortalApi.patch(`/lists/${listId}/items/${itemId}`, { fieldValues: { 'field-uuid-status': 'Готово' },});
// Удалить (право delete)await PortalApi.delete(`/lists/${listId}/items/${itemId}`);Полный перечень методов — в API списков, значения полей, библиотек, узлов.
4.3. Что разумно делать в JS
Заголовок раздела «4.3. Что разумно делать в JS»| Допустимо в JS | Лучше на сервере (C#) |
|---|---|
| Фильтрация/сортировка уже отрендеренных данных | Первичная загрузка данных с проверкой прав |
| Модалки просмотра, анимации, drag-and-drop UI | Создание записей с бизнес-валидацией |
| Дополнительные GET для динамической подгрузки | Скрытие данных, недоступных пользователю |
Сбор полей формы перед data-wp-action | Конфликты бронирования, проверка дубликатов |
PortalApi для SPA-подобных сценариев внутри bind | Любая логика, которую нельзя доверять клиенту |
Рекомендация: запись в списки (бронирование, задачи) — через HandleActionAsync + PortalListItem (NewListItemAsync / GetListItemAsync → CreateAsync / UpdateAsync). JS — для UX вокруг этого. См. Значения полей списков.
4.4. Пример: динамическая подгрузка в bind
Заголовок раздела «4.4. Пример: динамическая подгрузка в bind»async function loadRecentComments(root, ctx, listId) { const container = root.querySelector('[data-recent-comments]'); if (!container || container.dataset.loaded === '1') return; container.dataset.loaded = '1'; container.textContent = 'Загрузка…';
try { const items = await ctx.lists.getItems(listId, { top: 5 }); container.innerHTML = items.map((i) => `<li>${escapeHtml(i.field_values?.[titleFieldId] ?? '—')}</li>` ).join(''); } catch (e) { container.textContent = 'Не удалось загрузить'; }}
function bind(root) { // ... const ctx = PortalWebPartContext.create({ nodeId: root.dataset.nodeId }); loadRecentComments(root, ctx, root.dataset.listId);}nodeId и listId можно передать в HTML как data-node-id / data-list-id при серверном рендере.
5. HTTP API WebPart (справочник)
Заголовок раздела «5. HTTP API WebPart (справочник)»| Метод | Путь | Описание |
|---|---|---|
| POST | /api/v1/webparts/invoke/{key}/render | SSR: { properties, nodeId } → { html, error } |
| POST | /api/v1/webparts/invoke/{key}/action | Action: { action, data, properties, nodeId } → { html, error } |
| GET | /api/v1/webparts/context?nodeId= | Списки/библиотеки узла для Property Pane |
| GET | /api/v1/webparts/definitions | Каталог WebPart |
| GET | /api/v1/webparts/assets/{key}/{path} | CSS/JS из пакета |
Подробнее: api/webparts.md.
6. Сквозной пример: кнопка «Обновить» с перезагрузкой списка
Заголовок раздела «6. Сквозной пример: кнопка «Обновить» с перезагрузкой списка»C# — рендер:
public async Task<WebPartResult> RenderAsync(IWebPartContext ctx, CancellationToken ct){ var listId = ctx.Properties.GetGuid("listId"); if (listId == Guid.Empty) return new WebPartResult(WpHtml.Hint("Выберите список"));
var items = await ctx.Lists.GetItemsAsync(listId, limit: 5, ct: ct); var sb = new StringBuilder("""<div class="webpart-simple-list">"""); sb.Append("""<button type="button" class="btn btn--settings btn--sm" data-wp-action="refresh">Обновить</button><ul>"""); foreach (var item in items) sb.Append($"""<li>{WpHtml.Escape(item.GetProperty("id").GetString())}</li>"""); sb.Append("</ul></div>"); return new WebPartResult(sb.ToString());}
public Task<WebPartResult> HandleActionAsync(string action, JsonElement data, IWebPartContext ctx, CancellationToken ct) => action == "refresh" ? RenderAsync(ctx, ct) : Task.FromResult(new WebPartResult("", Error: "Unknown"));Никакого JS не требуется — платформа сама обрабатывает data-wp-action="refresh".
7. Примеры в репозитории
Заголовок раздела «7. Примеры в репозитории»| Пакет | UI | Actions | JS | API |
|---|---|---|---|---|
hello-widget | Статичный блок | — | — | Properties |
current-user-widget | Имя пользователя | — | — | ctx.User |
meeting-room-booking | Таймлайн, форма | changeDate, book, … | Модалки, форма | Lists CRUD |
tasks | Таймлайн задач | createTask, changePeriod, … | — (только shell) | Lists |
address-book | Дерево + карточки | — | Поиск, фильтр | Lists (SSR) |
8. Чеклист разработчика
Заголовок раздела «8. Чеклист разработчика»- manifest.json —
id,entry,entryType,styles, при интерактиве —scripts. - Корневой класс — уникальный префикс (
.webpart-…). - Экранирование — все пользовательские строки через
HtmlEncode. - Кнопки/навигация —
data-wp-action+data-wp-*для параметров. - Формы — серверный рендер полей; submit через action; при необходимости JS собирает значения в
dataset. - Данные —
ctx.Lists/Libraries/Nodesв C#; JS — только для UX или доп. GET. - bind —
PortalWebPartClients[manifestKey]; защита от двойной привязки. - Сборка —
scripts/pack-portalpart.sh/.ps1из SDK, загрузка.portalpart, увеличениеversionпри обновлении (cli.md).