Client und Layout¶
Coworkee.Client.Blazor ist eine vollständige WebAssembly-Shell: Layout, Navigation, Benutzermenü, Benachrichtigungen, Admin-Seiten für Benutzer, Gruppen, Rollen, Einstellungen, Dienste, Themes, Mail-Vorlagen und Audit-Log. Die App ergänzt ihre Seiten und Menüeinträge.
var builder = WebAssemblyHostBuilder.CreateDefault(args);
var baseAddress = new Uri(builder.HostEnvironment.BaseAddress);
builder.Services.AddCoworkeeClient(baseAddress, options =>
{
options.AppTitle = "MyApp";
options.AppDescription = "Clean Architecture starter for Blazor, built on Coworkee modules.";
options.AboutLinks.Add(new AboutLink("Documentation", "https://fgilde.github.io/CoworkeeLib/"));
});
builder.Services.AddHttpClient<ICatalogApi, CatalogApi>(client => client.BaseAddress = baseAddress);
builder.Services.AddSingleton<INavigationContributor, MyAppNavigation>();
builder.Services.Configure<NavigationMenuOptions>(MyAppNavigation.Order);
var host = builder.Build();
await host.Services.InitializeCoworkeeClientAsync();
await host.RunAsync();
<Router AppAssembly="typeof(Program).Assembly" AdditionalAssemblies="new[] { typeof(CoworkeeClientOptions).Assembly }">
<Found Context="routeData">
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(CoworkeeLayout)" />
</Found>
</Router>

Navigation¶
internal sealed class MyAppNavigation : INavigationContributor
{
private const string CatalogManagement = "Catalog Management";
public static void Order(NavigationMenuOptions menu) => menu
.OrderGroup("Personal", 0)
.OrderGroup(NavigationGroups.Administration, 2)
.OrderGroup(CatalogManagement, 3);
public IEnumerable<CoworkeeNavItem> Items =>
[
new("Dashboard", "/dashboard", Icons.Material.Outlined.Dashboard, CatalogPermissions.Dashboards.View, Group: "Personal"),
new("Products", "/catalog/products", Icons.Material.Outlined.ViewCarousel, CatalogPermissions.Products.View, Group: CatalogManagement),
new("Brands", "/catalog/brands", Icons.Material.Outlined.Sell, CatalogPermissions.Brands.View, Group: CatalogManagement, Order: 1),
];
}
NavigationMenuOptions |
|
|---|---|
OrderGroup(name, order) |
Position einer Gruppe; unbekannte Gruppen kommen zuletzt, nach Namen |
Hide(hrefs) |
entfernt Einträge, auch die eingebauten Admin-Seiten |
ShowHome, HomeTitle |
der Startseiten-Link oben |
IconForGroup(name, icon) |
das Symbol eines Gruppenknotens |
Place(href, group, title?, order?) |
verschiebt einen Link der Bibliothek in eine andere Gruppe; ein Gruppenpfad wie Administration/Localization verschachtelt |
Die Admin-Seiten der Bibliothek liegen in Gruppen unter Administration: NavigationGroups.Identity (Benutzer, Gruppen, Rollen, Anwendungen, Scopes), NavigationGroups.System (Einstellungen, Mail-Einstellungen, Dienste, Mandanten, Editionen, Themes, Sicherungen), NavigationGroups.Communication (Mail-Vorlagen, Mail-Log), NavigationGroups.Localization (Sprachen, Übersetzungen) und NavigationGroups.Monitoring (Audit-Log, KI-Werkzeugaufrufe). Verschachtelte Gruppen sortieren sich innerhalb ihrer obersten Gruppe, die ihren Platz aus OrderGroup behält.
Das Menü ist ein MudExTreeView: Gruppen sind Knoten, das Filterfeld hebt Treffer hervor und öffnet ihre Gruppen, und die Ansicht wechselt zwischen Baum, Liste und flacher Liste. Angeheftet bleibt die Leiste als Symbolstreifen unter der App-Leiste, der Menüknopf verbreitert sie; nicht angeheftet öffnet sie sich als Überlagerung und verschwindet wieder. Ob eine oder mehrere Gruppen offen bleiben, ist ein zweiter Schalter. Beides speichert der Browser.
Einträge mit HostOnly: true dienen der ganzen Installation und erscheinen nur Benutzern der Systemorganisation; die Bibliothek markiert so Mandanten, Editionen, Anwendungen, Scopes, Dienste und Sicherungen, und die Einstellungsseite zeigt ihre typisierten Reiter und den Reiter System nur dort. Die Organisation kennt der Client aus /bff/user (SystemTenant, aus dem Claim system_tenant des Auth-Servers); Anmeldungen von vor diesem Claim zeigen alles wie bisher.
Für reine Oberflächenfälle entscheidet ein Eintrag selbst: Visible: sp => sp.GetRequiredService<MyTips>().HasAnyAsync() blendet ihn aus, wenn der Callback false liefert. Menü und Befehlspalette fragen ihn bei jedem Aufbau (nach der Anmeldung, nach Rechte- und Feature-Änderungen), halte ihn also billig und cache, was er lädt. Er blendet nur aus; muss der Server zustimmen, nimm eine berechnete Berechtigung.

App-Leiste¶
Die App-Leiste zeigt den Suchknopf der Befehlspalette (Strg+K), Theme-Menü, Rechts-nach-links-Schalter, Dunkelmodus, Benachrichtigungen, Benutzermenü und den Info-Dialog; auf dem Smartphone teilen sich die Nebeneinträge ein Menü, siehe Smartphones, installierbare App und Push. Eigene Komponenten kommen so dazu:
internal sealed class LanguageBar : IAppBarContributor
{
public IEnumerable<AppBarItem> Items => [new AppBarItem(typeof(LanguageSwitch), Order: 10)];
}

Die eigene API aufrufen¶
Typisierte Clients erben von ApiClientBase. Sie senden den CSRF-Header, den der BFF verlangt, und machen aus Problem Details eine ApiException:
internal sealed class CatalogApi(HttpClient http) : ApiClientBase(http), ICatalogApi
{
public Task<DashboardDto> GetDashboardAsync(CancellationToken cancellationToken = default) => GetAsync<DashboardDto>("api/v1/dashboard", cancellationToken);
public Task DeleteBrandsAsync(IReadOnlyList<Guid> ids, CancellationToken cancellationToken = default) =>
SendAsync(HttpMethod.Post, "api/v1/brands/delete", new IdsRequest(ids), cancellationToken);
}
Snackbar.RunAsync(() => api.SaveAsync(...), "Saved") führt einen Aufruf aus und zeigt Erfolg oder die Validierungsmeldungen eines Fehlers.
Aktueller Benutzer¶
ClientUser.From(user) liest den angemeldeten Benutzer aus einem ClaimsPrincipal (IsAuthenticated, UserId, TenantId, Email, DisplayName); mit dem kaskadierenden Authentifizierungszustand braucht eine Seite nicht mehr. Dienste und Seiten ohne ihn injizieren das scoped CurrentUserInfo (registriert von AddCoworkeeClient):
var me = await currentUser.GetAsync();
if (me.UserId is { } userId) { ... }
currentUser.Changed += user => ...; // Anmeldung, Abmeldung, neu ausgestellte Rechte
Den Claim sub nicht selbst parsen.
Personen¶
UserAvatar zeigt das Bild eines Benutzers oder seine Initialen, UserCard Bild und Name mit optionaler zweiter Zeile. Beide laden gebündelt und aktualisieren sich für alle, sobald jemand Name oder Bild auf der Profilseite ändert (Realtime-Topic tenant:users).

<UserCard UserId="entry.ActorId">@entry.CreatedAt.ToString("g")</UserCard>
<UserAvatar UserId="message.FromUserId" Size="Size.Small" ShowTooltip="true" />
Kontoseite¶
/profile enthält die Tabs Profil (Bild, Name, Telefon, Adresse), Sicherheit, Benachrichtigungen und Einstellungen; /profile/{key} öffnet einen direkt. Mit einem ProfileTab fügst du eigene hinzu:
builder.Services.AddSingleton(new ProfileTab("Documents", "documents", typeof(MyDocuments), DocumentPermissions.Documents.View));
Bausteine¶
| Komponente | Zweck |
|---|---|
PageHeader |
Titel, Beschreibung, Aktionen rechts; setzt den Browsertitel. Übergib Textschlüssel (Title="Brands"), es übersetzt sie selbst; ebenso CreateText von CoworkeeDataTable |
StickyMergeTabs |
umschließt ein <MudTabs>: dessen Tab-Leiste klebt unter der App-Leiste und schrumpft auf die Breite der Tabs schrumpft, sodass eine klebende MudExObjectEditForm-Toolbar im Panel daneben sichtbar wird; nicht auf schmalen Bildschirmen |
PermissionGate, PermissionView |
rendern Inhalt nur mit einer Berechtigung |
RealtimeSubscription |
ruft zurück, wenn der Server auf einem Thema veröffentlicht, z. B. type:Brand |
EntityWatch |
hält eine Detailseite mit ihrem Eintrag gleich, geht zurück, wenn er gelöscht wird |
NotAllowed |
was der Router statt einer verbotenen Seite zeigt, mit Wechsel zu einer Rolle, die sie erlaubt |
ActiveRoleMenu, ChatNotifier |
im Benutzermenü: die aktive Rolle und Chat-Snackbars |
CoworkeeDataTable<T> |
Datentabellen |
ShowEditAsync |
aus einem Modell erzeugter Bearbeitungsdialog |
AuditTimeline, VersionHistory |
Änderungshistorie einer Entity |
ResourcePermissionsPanel |
Berechtigungen auf einer einzelnen Ressource |