Guida dello sviluppatore
Come si crea un'applicazione sulla shell Promisoft Dashboard, che cosa porta la shell, dove sono le cuciture da riempire, come si installano i due moduli disponibili, come si estende un'installazione già consegnata con un pacchetto di componenti e come si fa generare una pagina CRUD dall'agente AI.
1. Com'è fatta
La shell è un pacchetto NuGet, non un template da copiare: l'applicazione la referenzia e prende gli aggiornamenti alzando un numero di versione. Tutto quello che sta nel progetto generato è tuo - non esiste più una zona da non toccare.
| Pezzo | Che cos'è |
|---|---|
Promisoft.Dashboard.Shell |
La shell come Razor Class Library: layout e chrome, pagine di amministrazione e
impostazioni, servizi, DashboardDbContext con le sue migrazioni,
risorse resx, asset statici (icone, CSS, JS). È il pacchetto che installi. |
Promisoft.Dashboard.Components |
I contratti contro cui si compila un pacchetto di componenti (§9): gli attributi che rendono un componente collocabile, il contesto di pagina, i servizi che un componente può chiedere. Referenzia solo il framework: niente shell, niente DevExpress, e quindi nessuna licenza per chi lo compila. Linea di versione propria (1.x). |
Promisoft.Dashboard.Templates |
Due template: dotnet new promisoft-dashboard (la sola zona
applicativa, con shell e moduli presi dal feed) e
dotnet new promisoft-components (un pacchetto di componenti con
la sua applicazione di anteprima e la skill per l'agente). |
Promisoft.Dashboard.Modules.MultiTab |
Modulo aggiuntivo: layout multi-scheda (sopra la libreria
Promisoft.Blazor.MultiTab, indipendente dalla shell). |
Promisoft.Dashboard.Modules.Tasks |
Modulo aggiuntivo a pagamento: gestione attività - richieste di lavoro, progetti, Kanban, Gantt, scheda ore, tariffe e report. |
Promisoft.Dashboard.Host |
L'applicazione dimostrativa (il banco di sviluppo della shell): Identity, le
pagine /Account, i provider demo, le pagine CRUD su Northwind.
Non viene mai distribuita: è il modello da cui copiare. |
Due database, nessuna chiave esterna incrociata
Identity resta dell'applicazione (schema standard, connessione
DefaultConnection); tutto quello che la shell persiste vive in
DashboardDbContext (connessione DashboardConnection). Le righe
della shell riferiscono gli account Identity logicamente, per
UserId: nessuna FK attraversa i due database, e la pulizia alla cancellazione
di un account è esplicita.
La regola di registrazione
I servizi della shell si registrano dentro la shell
(AddPromisoftDashboardShell), mai nel Program.cs
dell'applicazione. L'applicazione registra solo le proprie cuciture:
IUserDirectory (che nomina il suo tipo utente), i provider di navigazione e
dati, i descrittori dei moduli, i descrittori delle sorgenti di notifica.
2. Prerequisiti
- .NET 10 SDK
- SQL Server - qualsiasi edizione; per lo sviluppo va bene LocalDB
- DevExpress Blazor 26.1 - serve un feed NuGet licenziato per ogni sviluppatore che compila. La shell dichiara DevExpress come dipendenza, non la include mai
- Il feed Promisoft - dove vivono i pacchetti di shell, moduli e
template: GitHub Packages dell'organizzazione
Promisoft-rc,https://nuget.pkg.github.com/Promisoft-rc/index.json, configurato dalnuget.configche ogni progetto porta con sé. È privato e non consente lettura anonima: ogni sviluppatore registra la sorgente una volta per macchina
La registrazione della sorgente, con un token personale GitHub che abbia il solo scope
read:packages (da PowerShell, che lascia intatti i %...%):
setx GITHUB_PACKAGES_TOKEN <token>
dotnet nuget add source https://nuget.pkg.github.com/Promisoft-rc/index.json -n promisoft -u <username> -p %GITHUB_PACKAGES_TOKEN% --store-password-in-clear-text
Il %GITHUB_PACKAGES_TOKEN% va lasciato scritto così: nel
NuGet.Config finisce il segnaposto e non il token, e NuGet lo risolve al
momento del download. Dopo setx serve un terminale nuovo, altrimenti il primo
restore risponde 401.
3. Installare da template
È la strada consigliata: il progetto nasce già cucito, con lo scheletro delle risorse, i
provider, le pagine /Account e la skill dell'agente AI dentro.
Una volta per macchina
dotnet new install Promisoft.Dashboard.Templates
Nessuna versione da indicare: viene installata la più recente pubblicata sul feed
(anche se prerelease). Le credenziali sono quelle registrate al punto 2, valide per
qualsiasi cartella. Più avanti, dotnet new update porta il template
all'ultima versione.
Una volta per applicazione
dotnet new promisoft-dashboard -n easy.web -o D:\Projects\easy.web
Il valore di -n diventa il namespace radice (lettere, cifre e punti; nessuno
spazio). Il progetto generato è la sola zona applicativa: un'app Blazor
Server sottile che referenzia Promisoft.Dashboard.Shell e i moduli dal feed.
Database e primo avvio
dotnet restore
dotnet tool install dotnet-ef --create-manifest-if-needed
dotnet dotnet-ef database update --context ApplicationDbContext
dotnet dotnet-ef database update --context DashboardDbContext
dotnet run
Il dotnet restore non è di cortesia: i comandi dotnet-ef
leggono i metadati del progetto da obj/project.assets.json, che nasce col
ripristino. Senza, rispondono
NETSDK1004: il file di risorse ... non è stato trovato.
Lo schema del dashboard arriva dalle migrazioni dentro il pacchetto della shell:
non c'è niente da copiare. Le stringhe di connessione stanno in
appsettings.json (DefaultConnection per Identity,
DashboardConnection per la shell).
Il primo accesso
In sviluppo si entra direttamente con Bootstrap:AdminEmail /
Bootstrap:AdminPassword (admin@example.com /
Promisoft1! come spedito, in appsettings.Development.json): all'avvio
l'account viene creato con il ruolo Admin se manca.
Bootstrap:AdminPassword in
produzione. Senza la password il flusso è: registra l'account
AdminEmail, riavvia una volta - all'avvio successivo quell'account entra nel
ruolo Admin.
4. Aggiungere la shell a un'app esistente
Se l'applicazione esiste già (Blazor Server con render mode
InteractiveServer), la shell si installa come pacchetto. Questi sono i punti
da toccare - il template li contiene tutti già fatti, e resta il riferimento migliore.
-
Riferimenti nel
.csproj(più ilnuget.configcon il feed Promisoft):<PackageReference Include="Promisoft.Dashboard.Shell" Version="2.0.0-preview.25" /> <PackageReference Include="DevExpress.Blazor" Version="26.1.*" /> -
Stringhe di connessione:
DashboardConnection(la shell la pretende all'avvio) accanto a quella di Identity. -
Program.cs: una sola chiamata porta dentro DevExpress, localizzazione, aspetto, moduli, notifiche, messaggi, preferiti, ricerca, job e repository. Il callback dichiara le famiglie di stringhe dell'applicazione.builder.Services.AddPromisoftDashboardShell(builder.Configuration, localization => localization .AddScope<NavigationStrings>("navigation", "Navigation") .AddScope<ProductsStrings>("products", "Products") .AddSeedAssembly(typeof(Program).Assembly)); -
Le cuciture dell'applicazione: la shell vede solo interfacce.
// L'autenticazione: account locali, che sono un pacchetto. Toglila, insieme al // riferimento, per autenticarsi altrove e non spedire nessuna pagina /Account. builder.Services.AddIdentityAccounts(builder.Configuration) .AddEntityFrameworkStores<ApplicationDbContext>(); // Il rail di navigazione, la ricerca, i dati della home. builder.Services.AddScoped<INavigationProvider, AppNavigationProvider>(); builder.Services.AddScoped<ISearchProvider, CustomerSearchProvider>(); builder.Services.AddScoped<IDashboardDataProvider, AppDashboardDataProvider>(); -
Router (
Routes.razor): le pagine della shell (/admin/*,/settings/*,/search,/tasks,/messages) vivono nel suo assembly, quelle dei moduli nei loro;ShellPageAssembliesle elenca tutte, e il layout predefinito è quello della shell.@inject ShellPageAssemblies Pages <Router AppAssembly="typeof(Program).Assembly" AdditionalAssemblies="Pages.Additional"> <Found Context="routeData"> <AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)" /> </Found> </Router> -
Endpoint (
Program.cs, dopoUseRequestLocalization()):app.MapRazorComponents<App>() .AddInteractiveServerRenderMode() .AddShellPageAssemblies(app.Services) // shell + moduli installati .AllowAnonymous(); app.MapPromisoftDashboardShell(); // cookie della cultura, logo, export traduzioni -
App.razor: il tema DevExpress si registra per richiesta (niente lampo di tema sbagliato) e lo script della shell chiude il body.@DxResourceManager.RegisterTheme(ActiveTheme) @DxResourceManager.RegisterScripts() ... <script src="@Assets["_content/Promisoft.Dashboard.Shell/js/shell.js"]"></script>ActiveThemeviene daAppThemes.Resolve(cookie, Appearance.Current.Accent): il modo chiaro o scuro dal cookie del browser, l'accento dalle impostazioni dell'installazione. -
_Imports.razor: aggiungiPromisoft.Dashboard.Shell.Components.Layout,...Shell.Resources,...Shell.ServiceseDevExpress.Blazor.
System.Net.Http.Json) vanno scritti.
5. Aggiornare la shell
<PackageReference Include="Promisoft.Dashboard.Shell" Version="2.0.0-preview.25" />
Alza la versione, ripristina, esegui di nuovo il comando di migrazione del
DashboardDbContext (le tabelle nuove della shell arrivano come migrazioni del
pacchetto e sono sempre additive), riavvia. Il contenuto del database - marchio,
attivazioni dei moduli, traduzioni, notifiche, preferiti - sopravvive a ogni aggiornamento:
i seed riempiono solo i buchi e non sovrascrivono mai una modifica fatta da un
amministratore.
dotnet dotnet-ef database update --context DashboardDbContext
Le versioni 2.0.0-preview.N possono ancora cambiare API tra un numero e
l'altro; con la 2.0.0 parte il versionamento semantico vero (rotture solo nei
major, annotate nel CHANGELOG.md).
6. Le funzionalità della shell
Quello che segue è ciò che l'applicazione ha senza scrivere una riga: pagine, servizi e cuciture. Per il punto di vista dell'utente finale, con le stesse schermate spiegate come si usano, vedi la guida dell'utente.
6.1 Chrome, rail, temi
- Rail di navigazione - popolato da
INavigationProvider: sezioni, voci, icone, sigle, voci "pinned" fuori sezione. Lo stato aperto/compresso è una classe CSS su<html>scritta dallo script della shell e persistita nel cookieshell.drawer, cheApp.razorrilegge lato server: nessun parametro da infilare nei link, e il comportamento è identico sulle pagine statiche. - Preferiti - la stella nella barra superiore aggiunge la pagina
corrente alla sezione "Preferiti" del rail, per utente
(
IFavoritesService). - Tema chiaro/scuro - cookie
shell.themepiù un reload: le visualizzazioni DevExpress cuociono la palette nel loro SVG al momento del render, e un cambio a caldo lascerebbe assi bianchi su fondo bianco. PageHeadereCommandBar- titolo, sottotitolo, breadcrumb e barra comandi che le pagine dichiarano dove vogliono e che atterrano in cima all'area di contenuto tramiteSectionOutlet.- Icone - SVG usati come
mask-image(20x20, tratto 1.5) registrati inicons.csse usati comeIconCssClass="icon icon-<nome>": seguono il colore del tema per costruzione.
6.2 Ricerca globale
IGlobalSearch interroga ogni ISearchProvider registrato. La
shell ne porta uno suo (ogni pagina del rail trovabile per nome); l'applicazione e i moduli
ne registrano uno per dominio ricercabile. La casella in barra superiore porta a
/search, che raggruppa i risultati per categoria.
/search: i risultati arrivano dai provider registrati - qui gli
ordini demo.6.3 Notifiche
Si emette attraverso INotificationPublisher: per utente, per ruolo o per unità
organizzativa. Ogni emettitore dichiara un
NotificationSourceDescriptor; il publisher, prima di consegnare, consulta le
scelte del destinatario da /settings/notifications. I sottoscrittori
ricaricano al segnale del dispatcher - il segnale non porta payload, quindi non ci sono
dati stantii da conciliare.
services.AddSingleton(new NotificationSourceDescriptor(
"fatture-scadute", "Fatture scadute", "Un avviso quando una fattura supera la scadenza."));
await publisher.PublishToUserAsync(userId, new Notification(
"fatture-scadute", "Fattura 2026/1187 scaduta", "Logistica Adriatica - 4 giorni"));
/admin/notifications: il banco di prova manda una notifica
vera attraverso la pipeline vera.
6.4 Messaggi
Il gemello deliberato delle notifiche, per il contenuto invece che per le
interruzioni: IMessagePublisher deposita copie per destinatario con una
ThreadKey, la pagina /messages le mostra come conversazioni.
Differenza di principio: le preferenze di notifica non filtrano mai un
messaggio - silenziare una sorgente spegne la campanella, non il post.
La casella cresce solo, quindi ha un contrappeso: MessageRetentionJob con due
finestre (Messages:RetentionDays per i letti,
Messages:HardRetentionDays per tutto; zero disattiva la regola).
/messages: conversazioni a sinistra, messaggi a destra. Aprire una
conversazione la legge.6.5 Attività personali
/tasks è una lista di scadenze, non un calendario di appuntamenti.
PersonalTask appartiene alla login (OwnerUserId): la
scadenza è una DateOnly? con un TimeOnly? facoltativo, la
ricorrenza (giornaliera, settimanale, mensile, annuale, con intervallo) materializza la
riga successiva al completamento, e l'invariante
CompletedUtc valorizzato ⇔ ProgressPercent == 100 la impone il servizio, mai
la UI.
La pagina aggrega ogni ITaskListSource registrato - quella
personale è l'unica modificabile: il modulo di gestione attività aggiunge le proprie
sorgenti senza toccare la pagina.
/tasks: righe raggruppate per finestra di scadenza, colonna
"Origine" con la sorgente che le ha portate.6.6 Processi in background
Le implementazioni di IShellJob si registrano in DI (lo stesso schema additivo
delle sorgenti di notifica) e le esegue un solo ShellJobRunner: tick di 30
secondi, isolamento degli errori per job, nessuna sovrapposizione, stato
(JobRun) persistito perché un riavvio non rispari tutto.
Deliberatamente non Hangfire: la shell è un pacchetto e non impone un
framework di scheduling.
/admin/jobs: pianificazione, ultima e prossima esecuzione, esito,
errore, ed "Esegui ora".6.7 Aspetto e marchio
/settings/appearance è per installazione, condivisa da tutti: logotipo (solo
testo, solo immagine, immagine e testo; una variante per fondi chiari e una per fondi
scuri, PNG/WebP/SVG fino a 512 KB), colore del marchio (da cui DevExpress deriva l'intera
scala primaria, che la shell rilegge per la propria interfaccia) e colori di superficie per
rail e pannelli.
E un CSS dell'installazione, in AppearanceSettings.CustomCss,
emesso da <InstallationStyles /> come ultimo <style>
della pagina - dopo i fogli della shell, quelli dei moduli e gli override delle superfici.
Essere ultimo significa che basta pari specificità per vincere.
In linea e non come file servito, ed è tutto il senso della cosa: gli asset statici sono
fingerprintati al build (MapStaticAssets), quindi un .css
modificato in una cartella pubblicata non corrisponde più all'hash che il manifesto
dichiara. Questo vive nel database, così cambia su un'applicazione che nessuno può
ricompilare - il caso di un'installazione consegnata senza i sorgenti o senza una licenza
DevExpress. Non costa nulla per richiesta: le impostazioni sono in cache in memoria e un
documento Blazor Server si scarica una volta per sessione.
Due protezioni, perché il valore raggiunge la pagina attraverso un
MarkupString: </ è rifiutato senza appello (chiuderebbe
l'elemento in anticipo e il resto diventerebbe markup) e la lunghezza è limitata a 64 KB.
Entrambe verificate al salvataggio e prima di emettere, perché lo store è un
database che un'installazione può raggiungere anche con un query tool.
<head>. DevExpress vi rende
<dxbl-resource-script>, e un elemento sconosciuto nell'head fa chiudere
l'head al parser HTML: tutto ciò che segue - site.css e i fogli dei moduli
compresi, da sempre - finisce nel body. La risposta del server è corretta; il DOM non è
dove controllare.
6.8 Impostazioni per installazione
IShellSettings conserva un blob JSON sotto una chiave scelta dal chiamante
("proprietario.impostazione") e lo restituisce tipizzato: lo store ignora il
contenuto, la classe delle impostazioni vive con chi la possiede. È in cache come aspetto e
moduli, quindi un chiamante consultato a ogni render può chiedere liberamente; niente di
salvato si legge come i default dichiarati dalla classe. Un salvataggio alza
Changed, ed è ciò che fa ridisegnare il rail quando un'impostazione letta da un
provider di navigazione cambia.
sealed class FatturazioneSettings { public bool MostraBozze { get; set; } = true; }
var settings = shellSettings.Get<FatturazioneSettings>("fatturazione.opzioni");
await shellSettings.SaveAsync("fatturazione.opzioni", settings);
/settings/demo nell'host dimostrativo: l'esempio da copiare per una
pagina di impostazioni tutta tua sopra IShellSettings.6.9 Repository e pagine dati
Tre registrazioni open-generic coprono le pagine dati, senza registrazioni per entità:
| Interfaccia | Quando |
|---|---|
IReadOnlyRepository<T, TKey, TContext> | Sola lettura: combo su tabelle di riferimento, griglie non modificabili. |
IRepository<T, TKey, TContext> | CRUD completo quando lettura e modifica sono la stessa tabella. |
IRepositoryDual<TRead, TWrite, TKey, TContext> | La griglia legge una vista denormalizzata, la modifica lavora sulla tabella. È il modello canonico. |
Il modello di riferimento per una pagina dati è ProductsList.razor dell'host:
CommandBar con i comandi che agiscono sulla riga focalizzata,
DxGrid dentro una Card, modifica ed eliminazione in
DxPopup posseduti dalla pagina, toast per gli esiti.
7. Autenticazione, ruoli e persone
Chi possiede cosa
L'autenticazione è un pacchetto, e la shell non ne nomina nulla. La shell
contiene IUserDirectory e nessuna implementazione: non menziona ASP.NET Core
Identity in nessun file. Legge righe DirectoryUser e chiede modifiche
attraverso l'interfaccia.
Gli account locali sono Promisoft.Dashboard.Accounts.Identity:
DashboardUser, IdentityUserDirectory, e l'intera famiglia
/Account - login, registrazione, recupero password, gestione profilo, 2FA,
codici di recupero, dati personali - come pagine compilate e già instradate.
Non sono più sorgente dell'applicazione: sono due righe.
builder.Services.AddIdentityAccounts(builder.Configuration)
.AddEntityFrameworkStores<ApplicationDbContext>();
È il default, non l'unica scelta. Un'applicazione che si autentica altrove
(LDAP, un identity provider esterno, un archivio utenti che esiste già) toglie il
riferimento al pacchetto e queste due righe, registra la propria implementazione di
IUserDirectory e non spedisce nessuna pagina /Account. Tiene
intatte tutte le pagine di amministrazione della shell.
Due cose restano all'applicazione di proposito. Il DbContext di Identity e le sue
migrazioni - è uno schema standard che l'applicazione possiede, ed è anche il
motivo per cui AddIdentityAccounts restituisce l'IdentityBuilder:
un'installazione che esiste già conserva la sua storia delle migrazioni. E il
tipo utente non si estende: quella riga è un login, la persona è
UserProfile nella rubrica della shell. Un campo su una persona va lì, o in una
tabella tua con chiave UserId.
Deliberatamente non è un IShellModule: i moduli si accendono e
si spengono da /admin/moduli, e un sistema di login non è qualcosa che
un'installazione commuta mentre gira. È un pacchetto fratello scelto in fase di build, come
il provider PostgreSQL.
/Account sono in SSR statico: l'attributo
[ExcludeFromInteractiveRouting] nel loro _Imports.razor è
portante - senza, il primo click ucciderebbe il circuito.
Ruoli
I ruoli restano roba di Identity. La shell ne conosce uno per nome,
ShellRoles.Admin: è quello che apre /admin/*. All'avvio
l'applicazione crea il ruolo se manca e ci mette dentro l'account di bootstrap - altrimenti
non ci sarebbe modo di entrare nelle pagine di amministrazione, perché solo un Admin può
concedere ruoli. Le pagine tue si proteggono come sempre:
[Authorize(Roles = "Admin")].
/admin/roles crea ed elimina ruoli e mostra il numero di membri;
l'assegnazione si fa dalla riga dell'utente in /admin/users. Due garanzie
vivono nel servizio, non nella UI: l'ultimo amministratore non può essere
rimosso dal ruolo (né disabilitato, né cancellato), e ogni cambio di ruolo
ruota il security stamp, così le sessioni aperte perdono i vecchi claim.
/admin/roles: ruoli e conteggio membri.
/admin/users: persone, stato derivato, ruoli, e la barra comandi
che agisce sulla riga focalizzata.Anagrafica people-first
Il record primario è la persona (UserProfile: nome, cognome,
email); la login Identity è facoltativa e viene predisposta dal flusso di invito. È questo
che permette di mettere qualcuno nell'organigramma prima che esista un account.
L'email è immutabile dopo la creazione: è la login.
Gli stati sono derivati, non un campo:
| Stato | Come si ottiene |
|---|---|
| In attesa | Creata (magari invitata) ma mai entrata: la login può ancora non esistere. |
| Attivo | ActivatedUtc valorizzato: la persona ha impostato la password seguendo il link di invito. |
| Disabilitato | DisabledUtc valorizzato: resta in anagrafica e nell'organigramma, la login è bloccata e le sessioni aperte muoiono. |
Il flusso di invito
- Nuovo utente crea la persona: nessuna login, stato "In attesa". Assegnabile subito a un'unità organizzativa.
- Invia o reinvia l'invito predispone la login (email come username, nessuna password) ed emette un token di reset password. Richiamarlo emette semplicemente un token nuovo.
- Il link punta a
/Account/AcceptInvitation. Quando la persona imposta la password, la shell conferma l'email (aver seguito il link prova la casella) e timbraActivatedUtc.
Nessuna password viaggia per email, mai: viaggia solo un token a scadenza.
L'invio esiste (IShellMailer su SendGrid via HttpClient) e si
configura in due modi: dall'amministratore in /admin/email - chiave API,
mittente verificato in SendGrid, nome mittente, salvati nel database della shell con la
chiave cifrata, più un pulsante che spedisce un messaggio di prova - oppure nel file di
configurazione (Email:SendGrid:ApiKey, Email:From,
Email:FromName). Quello che è salvato nella pagina prevale, campo per campo.
Finché la chiave manca, o se il provider rifiuta, /admin/users mostra il
link in un popup da copiare e consegnare a mano. È uno stato supportato di proposito, non
un errore. Non è ancora stato collaudato con un account SendGrid reale.
Restano da fare, quando si vorrà chiudere il capitolo:
- configurare e collaudare davvero SendGrid (chiave, mittente verificato, recapito);
- nessuna alternativa SMTP: oggi c'è solo SendGrid;
- i testi dell'email sono nel codice della pagina utenti, non modificabili da amministratore né in template;
- nessuna coda né ripetizione: un rifiuto del provider si vede come messaggio d'errore e finisce lì;
- le email di Identity (conferma indirizzo, reset password) passano dallo stesso
mailer solo se la chiave è configurata - altrimenti restano silenziose
(
IdentityNoOpEmailSender), quindi "Password dimenticata?" non recapita nulla.
Registrazione pubblica
Chiusa dietro Users:AllowSelfRegistration (false come spedito):
con il flag a false il collegamento "Crea un account" non appare nella pagina di accesso, e
le persone entrano solo per invito.
Organigramma
OrgUnit / OrgUnitMember riferiscono il profilo, non la
login, così le persone entrano nei reparti prima di avere un account. L'indirizzamento
colpisce il nodo esatto; la portata sul sottoalbero è un problema di chi legge
(IOrgDirectory.GetSubtreeMemberIdsAsync). Le eliminazioni rifiutano le unità
non vuote, gli spostamenti rifiutano i cicli.
/admin/organization: gerarchia libera, azioni per riga
(rinomina, membri, sotto-unità, elimina).
Estendere il profilo
Il tipo utente non è il posto giusto: quella riga è un login, e con gli
account locali la possiede il pacchetto. La persona è UserProfile, nella
rubrica della shell, che raggiunge il login logicamente per UserId.
Quindi, in ordine di preferenza: un campo che descrive una persona e serve a tutti va su
UserProfile; un campo che serve a una sola installazione va in una
tabella tua con chiave UserId, che è il pattern usato in tutta
la shell (nessuna chiave esterna fra database, riferimento logico per stringa) e non
richiede di toccare né la shell né il pacchetto di autenticazione.
Se invece il valore deve viaggiare nel cookie di autenticazione ed essere leggibile senza
una query, la via è un UserClaimsPrincipalFactory personalizzato
nell'applicazione: la shell legge solo i claim dell'utente autenticato, e
il claim può venire da qualunque tabella, compresa quella qui sopra.
8. Moduli aggiuntivi
8.1 Come funziona il sistema
Un modulo è un pacchetto NuGet, e basta. L'applicazione lo referenzia e lo
accende in /admin/modules: non c'è una riga da scrivere nel suo
Program.cs, né pagine da aggiungere al menu, né fogli di stile da collegare, né
dotnet ef da lanciare. Per installazione si conserva solo lo stato di
attivazione (più la chiave di licenza e le impostazioni del modulo).
È il modulo a dichiarare sé stesso, con un attributo di assembly e un
IShellModule. La shell li cerca fra i pacchetti installati e registra quello
che ciascuno dichiara.
[assembly: ShellModule(typeof(FatturazioneModule))]
public sealed class FatturazioneModule : IShellModule
{
public ShellModuleDescriptor Descriptor =>
new("fatturazione", "Fatturazione", "...", Version: "1.0", RequiresLicense: true)
{
SettingsEditorComponent = typeof(MioEditor), // dentro la card di /admin/modules
Localization = new("fatture", "Fatturazione", typeof(MieStringhe)),
Stylesheets = ["fatturazione.css"], // nel <head>, via <ModuleStyles />
DatabaseContext = typeof(FattureDbContext), // migrazioni applicate all'attivazione
Navigation = [new("work", "Nav.Fatture", "fatture")]
};
public void ConfigureServices(ShellModuleContext context) { /* i servizi del modulo */ }
}
LayoutHostComponent- avvolge il corpo della pagina mentre il modulo è attivo (contratto: un parametroRenderFragmentchiamatoBody). Al massimo un modulo attivo può dichiararlo.SettingsEditorComponent- viene reso dentro la card del modulo in/admin/modules; legge e salva le proprie impostazioni conIModuleService.Localization- la famiglia di stringhe del modulo: registra lo scope, i file di seed, e risolveName/Descriptiondella card e i testi delle voci di menu.Navigation- le voci che il modulo porta nel rail, con la chiave della sezione in cui entrano; compaiono e spariscono con l'interruttore.Stylesheets,PageAssembly,DatabaseContext- fogli di stile, pagine instradabili e tabelle proprie del modulo.
La scoperta legge il manifesto delle dipendenze (deps.json):
sono candidate le librerie che dipendono dalla shell, e fra queste i moduli sono quelle che
portano l'attributo. Non i riferimenti dell'assembly dell'applicazione: il compilatore non
emette il riferimento a un assembly che il codice non usa mai - cioè esattamente il caso di
un'applicazione che non nomina i suoi moduli da nessuna parte.
IModuleService.IsEnabled(chiave) è l'unica domanda che layout e pagine fanno:
è vera quando il modulo è acceso per questa installazione e, se richiede licenza, la chiave
salvata valida. Il validatore è una cucitura: ILicenseValidator. Quello che un
modulo porta nelle cuciture additive della shell (IShellJob,
ITaskListSource, ISearchProvider, sorgenti di notifica) è invece
filtrato dalla shell stessa: un modulo spento è muto per costruzione, e torna a parlare
appena lo si accende, senza riavvii.
DemoLicenseValidator: accetta qualunque chiave
non vuota. La verifica vera (firma, scadenza, vincolo all'installazione) va
innestata lì, senza toccare il resto della shell.
/admin/modules: una card per modulo, interruttore di attivazione,
versione, chiave di licenza e - se il modulo lo porta - il suo editor di
impostazioni.8.2 Modulo: layout multi-scheda
Promisoft.Dashboard.Modules.MultiTab apre le pagine in schede che restano
vive: lo stato dei componenti è conservato quando si cambia scheda, le
schede si riordinano trascinandole, si chiudono da menu contestuale e si ripristinano dopo
un aggiornamento della pagina (persistite in localStorage). Sotto c'è la
libreria Promisoft.Blazor.MultiTab, che non sa niente della shell.
Installazione
dotnet add package Promisoft.Dashboard.Modules.MultiTab --prerelease
Non serve altro: il modulo si registra da sé, e con lui la libreria sottostante.
Il --prerelease serve finché siamo alla 2.0.0-preview: senza
versioni stabili pubblicate, dotnet add package si rifiuta di prenderne una
e lo dice solo dopo aver scritto «Aggiunta dell'oggetto PackageReference», così
sembra riuscito mentre il csproj è rimasto intatto. In alternativa si nomina la versione
(-v 2.0.0-preview.6), che non ha bisogno del flag.
ShellPageAssemblies: shell, applicazione e ogni modulo
installato), lo stesso che alimenta il Router e
MapRazorComponents. Non c'è più niente da ricordare.
Impostazioni (dalla card in /admin/modules)
- Numero massimo di schede aperte - al limite la scheda più vecchia, non attiva e chiudibile viene chiusa (comportamento LRU).
- Quando le schede superano la finestra - vanno a capo su una nuova riga, oppure scorrono.
<PageTitle> della
pagina.- Le schede non attive restano vive nel DOM: un
querySelectorglobale può prendere la toolbar o il popup della pagina sbagliata. Vale per il codice JS e per i test end-to-end. - Una pagina tenuta viva non vede i dati creati altrove finché non si premono i suoi comandi di ricarica.
- Lo stato di pagina va letto da rotta e query string, non da
NavigationManager.Uria ogni render: l'URL può essere quello di un'altra scheda. - Il layout host si applica solo ai circuiti interattivi e autenticati: in SSR
statico (le pagine
/Account) la shell rende il corpo classico.
8.3 Modulo: gestione attività
Promisoft.Dashboard.Modules.Tasks è il modulo a pagamento del lavoro:
richieste tra colleghi e reparti, progetti con aree ad
albero, bacheca Kanban, Gantt, scheda ore
con tariffe e report mensile.
Installazione
dotnet add package Promisoft.Dashboard.Modules.Tasks --prerelease
Il --prerelease è obbligatorio finché non esce una versione stabile: vedi la
nota al modulo multi-scheda, poco sopra.
Poi si accende in /admin/modules con la chiave di licenza. Non c'è altro: le
sue pagine (/requests, /projects, /kanban,
/gantt, /timesheet e le due amministrative) si instradano da sole,
le sue voci compaiono nel rail tradotte, il suo foglio di stile entra nel <head>
e le sue tabelle vengono create nel momento dell'attivazione.
Database
Il modulo ha il proprio DbContext sullo stesso database della
shell (DashboardConnection) ma con una tabella di storia migrazioni separata,
__TasksModuleMigrationsHistory. I riferimenti alle tabelle della shell
(UserProfile, OrgUnit) e a Identity sono logici: le FK
esistono solo tra tabelle del modulo. Le migrazioni le applica la shell
quando un amministratore attiva il modulo; la card mostra quante ne restano in sospeso dopo
un aggiornamento del pacchetto, con il pulsante per applicarle. Se il database non risponde
il modulo resta spento, con il motivo scritto sulla card.
Attivazione
Il modulo richiede licenza: dopo l'installazione va acceso da
/admin/modules inserendo una chiave (con il validatore demo qualunque testo non
vuoto passa). Finché è spento è come se non ci fosse: niente voci nel menu, niente
promemoria, niente richieste nelle attività personali. Le sue pagine restano raggiungibili
per collegamento diretto, e spiegano che la licenza non è attiva.
Che cosa porta, in dettaglio
Richieste di lavoro (/requests) - il ciclo è
Aperta → Presa in carico → Evasa, più Rifiutata e Annullata. L'autore è una login
(CreatedByUserId); il destinatario è people-first: una persona
oppure un'unità organizzativa, esattamente uno dei due. Scadenza come le
attività personali (data più ora facoltativa), priorità, progetto e area facoltativi,
conversazione in coda (i commenti non si cancellano). I permessi vivono nel servizio: il
destinatario prende in carico, evade, rifiuta; l'autore modifica e annulla; commentano
entrambi.
/requests: "Ricevute" e "Inviate", con la barra comandi che
rispecchia i permessi sulla riga focalizzata.Progetti e aree (/projects) - un progetto è ciò a cui si
intestano attività e richieste. Visibilità: creatore o membro (filtrata in SQL); il
creatore è membro per costruzione e l'unico che rinomina, archivia e gestisce le persone. I
progetti non si cancellano, si archiviano: i riferimenti logici da altri
contesti non devono restare appesi. Le aree sono un albero a profondità libera
dentro il progetto (la forma dell'organigramma, riusandone la logica anti-ciclo); l'obbligo di
scegliere un'area scatta se il progetto ha aree, e la loro esistenza è
l'interruttore.
/projects: creatore, numero di membri, stato.Kanban (/kanban) - la bacheca di un progetto, sul componente
Kanban ufficiale DevExpress. Le colonne sono una lettura dell'avanzamento, non
uno stato proprio: Da fare = 0%, In corso = 1-99%, Fatto = completata. Trascinare una card muta
ProgressPercent passando dal servizio della shell, quindi bacheca e
/tasks non possono mai essere in disaccordo. Le card sono le attività di
tutti i membri (intestare a un progetto significa condividere); ognuno trascina solo le
proprie, le altrui hanno il lucchetto - che il creatore del progetto può aprire.
/kanban: colonne riordinabili trascinando le intestazioni, iniziali
del proprietario sulla card, percentuale su quelle in corso.Gantt (/gantt) - il progetto su una linea del tempo, HTML e CSS
puri su DxTreeList (nessuna dipendenza DevExtreme). Anche questo è una
lettura: si modifica col popup delle attività, con gli stessi permessi della bacheca.
Regole di onestà del disegno: barra solo con entrambe le date, una sola data = rombo di
milestone, nessuna data = riga elencata senza barra; la data d'inizio non viene mai inferita.
Le date di un'area sono il minimo e il massimo di tutto il suo sottoalbero, la percentuale è la
media pesata sulla durata delle sole foglie - e senza denominatore si scrive "—", mai
0%.
/gantt: colonne di sinistra congelate, scala mesi/settimane,
weekend ombreggiati, linea di oggi.Scheda ore, tariffe, report - /timesheet è la scheda personale
mese per mese: ore a quarti, massimo 24 al giorno, su un'attività di progetto
oppure su una descrizione libera. /admin/rates tiene le tariffe
sulla persona con storia per data di validità: un aumento è una riga nuova, i mesi già
pagati non cambiano; oraria o giornaliera con ore/giornata per il prorata.
/admin/timesheet-report valorizza il mese per persona e destinazione applicando la
tariffa vigente nel giorno della riga, con arrotondamento per riga così il totale è la
somma di ciò che si legge. Senza tariffa il costo resta vuoto con un avviso, mai zero
silenzioso.
Come si innesta sulle cuciture della shell
ITaskListSource- le richieste prese in carico compaiono in/tasks.ITaskCompletionObserver- prendere in carico una richiesta con progetto crea l'attività che la esegue; completare quell'attività evade la richiesta. Unidirezionale di proposito: riaprire l'attività non riapre la richiesta.IShellJob- promemoria di scadenza delle richieste, visibili in/admin/jobs, riusando le regole pure della shell.NotificationSourceDescriptor- quattro sorgenti (richieste ricevute, avanzamento, commenti, scadenze), silenziabili per utente.IProjectCatalog- il modulo sostituisce (services.Replace) il catalogo vuoto della shell: senza modulo il campo progetto non esiste, con modulo disattivato il catalogo risponde vuoto.IOrgDirectory,IUserDirectory- indirizzamento a unità e selettore di persone.IMessagePublisher- un commento a una richiesta arriva nella posta di chi è coinvolto.
IProjectCatalog) rompe i moduli già impacchettati con un errore di
caricamento tipi. La shell è un NuGet: i membri nuovi vanno aggiunti come default
interface member.
9. Pacchetti di componenti e pagine composte
Un'applicazione consegnata come binario chiuso - il cliente la esegue, non
la ricompila - cresce lo stesso: con un pacchetto di componenti (pack),
una Razor Class Library che chiunque può compilare contro il solo pacchetto dei contratti,
copiata nella cartella component-packs/ dell'installazione e letta all'avvio.
Un amministratore compone poi pagine con quei componenti da
/admin/pages, senza che nessuno compili nulla: la shell le serve a
/p/<slug> e le mette nel rail. La forma lunga, con le trappole incontrate,
è docs/component-packs.md nel repository.
9.1 Perché esistono
La shell referenzia DevExpress Blazor, e DevExpress compila un analizzatore dentro
DevExpress.Data che gira in ogni progetto che ha quel pacchetto nel
grafo - anche transitivamente attraverso la shell, anche senza nominare un solo tipo
DevExpress - e scrive un attributo di licenza nell'assembly compilato. A runtime viene letto
l'attributo dell'assembly d'ingresso: una shell licenziata non copre l'applicazione
che la ospita. Quindi chi compila un progetto che referenzia la shell ha bisogno di
una licenza DevExpress propria. Cancellare le pagine demo non cambia nulla.
Il pack gira intorno al muro: referenzia Promisoft.Dashboard.Components, che
referenzia il framework condiviso e nient'altro. Il suo build non ha vendor dentro -
verificato: nessun DevExpress nel deps.json, nessun
DXLicenseInformationAttribute nella DLL. Chi scrive un pack non vede nemmeno
quale libreria di componenti sta dentro la shell, e sostituirla domani non lo tocca.
/admin/modules. Un pack arriva dopo la compilazione, da una cartella,
non ha attivazione né DbContext, e i suoi componenti non sono pagine ma pezzi che un
amministratore mette su una pagina. Se stai scrivendo qualcosa che Promisoft rilascia a
tutte le installazioni, è un modulo; se stai aggiungendo un riquadro a questa
installazione senza ricompilarla, è un pack.
9.2 Il contratto
| Tipo | A che serve |
|---|---|
[assembly: ComponentPack(typeof(...))], IComponentPack, ComponentPackDescriptor |
Che cosa rende l'assembly un pack: chiave, nome, versione, i fogli di stile del suo
wwwroot. ConfigureServices è facoltativo - quasi nessun
pack registra qualcosa. |
[DashboardComponent(key, displayName)] |
Sul componente Razor: lo rende collocabile. Chiave stabile in
minuscolo con trattini, unica nel pack; le pagine salvano la chiave, quindi
rinominare la classe è gratis e rinominare la chiave orfana le pagine.
Category raggruppa nel selettore. |
[ComponentParameter(Label, Help, Hidden)] |
Sui [Parameter]: che cosa legge l'amministratore nel form e la frase
che evita la domanda. |
DashboardPageContext |
Cascading parameter, nullo fuori da una pagina composta: slug e titolo della pagina, posizione della regione. |
ICurrentUser |
La persona autenticata: id, nome visualizzato, ruoli. Il primo dei servizi della shell che un componente può iniettare; gli altri entrano nel pacchetto quando un componente reale li chiede. |
Ogni tipo del contratto cresce per proprietà, mai per parametri di costruttore: un pack compilato contro una versione precedente deve continuare a caricarsi.
9.3 Scrivere un pack
dotnet new install Promisoft.Dashboard.Templates
dotnet new promisoft-components -n Acme.Widgets
Nascono due progetti: Acme.Widgets, il pack con due componenti d'esempio e un
wwwroot/sample.css, e Acme.Widgets.Preview
(§9.6). Un componente è così:
@attribute [DashboardComponent("kpi-tile", "KPI tile",
Description = "Una cifra con didascalia.", Category = "Cifre")]
@inject ICurrentUser User
<section class="acme-kpi">
<span class="acme-kpi-caption">@Caption</span>
<strong>@Value.ToString(Format)</strong>
</section>
@code {
[Parameter, ComponentParameter(Label = "Didascalia")]
public string Caption { get; set; } = "Ordini aperti";
[Parameter, ComponentParameter(Label = "Valore")]
public decimal Value { get; set; }
[Parameter, ComponentParameter(Label = "Formato", Help = "Formato .NET: n0, n2, c.")]
public string Format { get; set; } = "n0";
[CascadingParameter] public DashboardPageContext? Page { get; set; }
}
Le regole, ciascuna un difetto già visto o certo:
- Solo HTML, CSS e C#, più i contratti.
DxGrid,DxButton,Card,PageHeaderqui non esistono: il pack non referenzia la shell, ed è questo il punto. - Colori dai token della shell -
var(--shell-surface),var(--shell-content),var(--shell-border),var(--shell-accent), sempre con un fallback - così il componente segue il tema dell'installazione e il modo scuro per costruzione. Un colore letterale rompe il tema scuro. CSS inwwwroot/, dichiarato inStylesheets, classi con il prefisso del pack. - Ogni parametro ha un default che rende qualcosa di sensato: galleria e
form partono dai default. E ogni parametro è di un tipo che fa il giro del JSON:
stringhe, numeri, booleani, enum,
DateOnly, classi semplici di questi. Niente delegati néRenderFragment: un amministratore non li può digitare. - Null è un valore su ogni parametro di riferimento: il form può svuotare un campo.
- Niente inizializzatori JS (
<Assembly>.lib.module.jsnon gira per un pack caricato a runtime): il JavaScript si importa conIJSRuntime. - Lavoro costoso in
OnInitializedAsync, protetto: un componente che lancia è una regione rotta con un avviso, un componente che si blocca blocca la pagina.
La skill dashboard-component, che il template porta in
.claude/skills/, guida l'agente attraverso queste regole: parola chiave
"Nuovo componente", domande prima, checklist alla fine.
9.4 Come viene caricato
La cartella è Components:Folder nella configurazione, per default
component-packs/ sotto la radice dei contenuti - accanto ad
appsettings.json in sviluppo, accanto al binario in produzione. È un
percorso di deploy dell'amministratore di sistema: una DLL lì dentro gira con
i privilegi dell'applicazione, e niente nell'interfaccia vi scrive. Un pack è una
sotto-cartella con l'output di build (<Nome>.dll,
<Nome>.deps.json, cartelle satellite) più il suo wwwroot/; il
target PackDeployFolder del csproj generato fa la copia a ogni build.
- Un
AssemblyLoadContextper pack, con regola "prima l'host": tutto ciò che il contesto predefinito sa risolvere - framework, Blazor, i contratti, la shell - viene da lì, e la copia del pack non viene mai aperta. È ciò che fa coincidere i tipi: i contratti esistono una volta sola nel processo. Solo ciò che l'host non ha arriva dalla cartella del pack, viadeps.json. - Non scaricabile: un pack si aggiorna riavviando l'applicazione. I pack si leggono all'avvio, prima che il container esista.
- Un pack rotto non ferma l'avvio: DLL mancante, attributo assente,
costruttore che lancia, due pack con la stessa chiave - ogni caso è una riga in
/admin/diagnostics(controllo "Component packs"), che è il primo posto dove guardare quando "il mio componente non c'è". Un pack che porta una copia della shell o di DevExpress viene nominato lì: vuol dire che è stato compilato contro la shell. - Asset statici: il manifest dell'host è stato scritto alla compilazione e
non conosce il pack, quindi la shell serve da sé il
wwwrootdel pack sotto_content/<Assembly>/e<ModuleStyles />collega i fogli dichiarati.
components
Su Windows è la cartella Components/ dell'applicazione Blazor, e il primo pack di
prova si è copiato fra App.razor e Routes.razor. Da qui
component-packs/.
9.5 Le pagine composte
Una pagina è una riga della tabella DynamicPage della shell: slug,
titolo, posizione nel rail (la chiave di una sezione, oppure null per stare in cima
accanto alla Home con un'icona e una posizione), ruoli, e le regioni come JSON
([{ "component": "acme/kpi-tile", "parameters": { ... } }]). La sola pagina
compilata è /p/{slug}: trova la definizione, verifica i ruoli (nessun ruolo =
ogni utente autenticato) e rende una regione dopo l'altra con DynamicComponent,
convertendo il JSON salvato nei tipi dei parametri. Due tolleranze che un form richiede: un
parametro stringa accetta qualunque scalare, un booleano scritto come testo si legge come
booleano. Ciò che non combacia viene saltato e segnalato - solo agli amministratori, sotto la
regione. Un componente il cui pack è sparito è un avviso; un componente che lancia è un
ErrorBoundary, non una pagina rotta.
Il rail: una pagina nomina una sezione. Una chiave esistente (le sezioni della shell, quelle dell'applicazione) accoglie la voce; una chiave nuova crea la sezione con il titolo e l'icona della pagina, dopo Utilità e prima di Amministrazione. La rail, l'elenco delle pagine e una copia aperta della pagina si ridisegnano al salvataggio, senza riavvio.
/admin/components è la galleria: ogni componente del catalogo
reso con i suoi default, ciascuno dentro il suo ErrorBoundary. È la sandbox
dentro la shell vera: un pack appena copiato si vede lì prima di comporre una pagina.
/admin/pages è l'editor, descritto nella
guida dell'utente.
9.6 Anteprima e template
Il progetto <Nome>.Preview generato dal template è una piccola
applicazione Blazor Server che ospita il pack dentro un'imitazione della shell:
barra superiore, rail, area contenuto, interruttore chiaro/scuro sui token veri della shell
(wwwroot/shell-tokens.css, generato dal foglio di stile della shell e da non
modificare a mano), un utente finto configurabile in appsettings.json. La
galleria rende ogni componente con i default; /c/<pack>/<chiave> lascia
digitare il JSON dei parametri e applicarlo, segnalando gli errori come farebbe la shell.
F5, hot reload, debugger: il pack è un riferimento a progetto.
È un'imitazione dichiarata: la shell vera non è referenziabile senza portarsi DevExpress.
Il controllo finale resta il dashboard: pack copiato nella sua cartella, riavvio,
/admin/diagnostics, /admin/components, una pagina da
/admin/pages.
9.7 Componenti nativi: griglia, form, grafico
Un pack è HTML e CSS; la pagina che ogni installazione chiede per prima - una griglia, un
form, un grafico sui propri dati - ha bisogno del vendor, e il vendor resta dentro la shell.
Per questo la shell porta tre componenti nativi, nel catalogo sotto la
sorgente shell, il cui comportamento è dato che l'amministratore digita
nell'editor:
| Componente | Che cosa fa |
|---|---|
Data grid shell/data-grid |
Le righe di una vista o tabella: ordinamento, filtri, raggruppamento, paginazione,
ricerca, totali, export xlsx/csv, selettore colonne. Colonne, intestazioni,
larghezze, formati e totali scelti nell'editor; vuoto = tutte le colonne. In memoria
fino al limite di righe, oppure in modalità server
(ServerMode): la griglia è legata a una GridCustomDataSource
e il database ordina, filtra, pagina, totalizza e raggruppa (un livello). I criteri
DevExpress sono tradotti una volta in un modello di interrogazione neutro
(DataQuery, DataFilter) e da lì in SQL parametrizzato; ciò
che non si traduce viene rifiutato con un messaggio, mai approssimato. Niente export
in modalità server. |
Data form shell/data-form |
Griglia su una vista o tabella e finestra di modifica generata dalle colonne della
tabella che scrive (testo, memo, numero, casella, data; obbligatorio dalla nullabilità;
chiave bloccata). Nuovo, modifica, elimina con conferma. Il pattern duale di
RepositoryDual come configurazione: la griglia legge la vista
denormalizzata, la finestra legge e scrive la tabella (WriteObject),
agganciate dalla chiave che la vista espone (WriteKeyField se i nomi
differiscono); stessa sorgente per entrambe. Le chiavi esterne della tabella
verso tabelle esposte diventano tendine (Lookups: campo, tabella,
colonna chiave, colonna testo), proposte dall'editor leggendo le relazioni dal
catalogo del database. Concorrenza: ultimo che scrive vince, detto chiaramente. |
Data chart shell/data-chart |
Barre, linee, aree o torta su una vista: una colonna argomento, una o più colonne
valore piegate da un'aggregazione (somma, conteggio, media, minimo, massimo) in una
GROUP BY. |
Data filter shell/data-filter |
I valori distinti di una colonna di una vista o tabella, pubblicati alla pagina con
un nome (PublishAs). I componenti di una pagina comunicano attraverso
IPageSignals, un'interfaccia dei contratti (1.1) cascata dalla pagina
composta, una per pagina aperta: griglia e form pubblicano una colonna della riga
selezionata (PublishField, PublishAs), griglia, form e
grafico ascoltano (Listen: nome pubblicato, colonna) e il valore
diventa un'uguaglianza nel filtro neutro, sommata al WHERE in modalità
server. Anche un componente di un pack può ascoltare o pubblicare. |
Le sorgenti dati sono chiuse. Un amministratore non digita mai il nome di una
tabella su una connessione a sua scelta. Components:DataSources nella
configurazione elenca le sorgenti - un nome, una connessione (o il nome di una sotto
ConnectionStrings) e le viste e tabelle di essa che si possono collocare;
"*" apre ogni vista e nessuna tabella:
"Components": {
"DataSources": [
{
"Name": "demo",
"DisplayName": "Demo (Northwind)",
"ConnectionStringName": "DemoConnection",
"Objects": [ "dbo.ProductsList", "dbo.Products", "dbo.Customers" ]
}
]
}
- Chi amministra il server decide che cosa è esponibile; chi amministra l'applicazione compone dentro quel perimetro. Ogni nome di oggetto e di colonna che arriva da una pagina viene confrontato con lo schema letto dal database prima di entrare in una query, i valori viaggiano come parametri, e nessun punto del codice accetta SQL da una pagina.
- Le connessioni si aprono con il provider dell'installazione: un'installazione SQL Server legge sorgenti SQL Server, una PostgreSQL legge PostgreSQL, senza driver aggiunti alla shell. Una sorgente su un motore diverso non è supportata.
- Le letture tornano in memoria, con un tetto di righe (5000 per default) e un avviso quando il tetto ha tagliato; la griglia ordina, filtra, pagina ed esporta entro quelle.
- Il controllo "Data sources" in
/admin/diagnosticsdice se ogni sorgente risponde e se la sua lista nomina cose che esistono.
Ogni componente nativo ha un editor proprio nella finestra della pagina: scelta
della sorgente e dell'oggetto dagli elenchi letti dal database, colonne con spunta e
intestazione, chiave, serie del grafico. Lo dichiara con l'attributo [ComponentEditor],
che è della shell e non dei contratti: un pack non può portare un editor che la shell sappia
rendere, e il contratto resta piccolo.
10. L'agente AI per le pagine CRUD
Le pagine di gestione dati non si scrivono a mano: le genera un agente AI (Claude Code)
guidato da una skill che viaggia dentro il progetto, in
.claude/skills/crud-scaffold/. Il template la porta già, quindi
un'applicazione appena creata ce l'ha.
Come si avvia
Nella sessione dell'agente, dentro la cartella del progetto, si scrive la parola chiave - facoltativamente seguita dal nome dell'entità o della vista:
Automazione dxGrid Fatture
L'agente apre la checklist e la annuncia; da lì il flusso ha quattro fasi e non genera codice finché la specifica non è confermata.
Fase 1 - Le domande che decidono tutto
La prima domanda è la fonte dati, e sceglie il repository che la pagina inietterà:
- Sola lettura -
IReadOnlyRepository: griglia senza editing su una vista o tabella. - Lettura e modifica (singola) -
IRepository: griglia e form sulla stessa tabella. - Lettura e modifica (doppia) -
IRepositoryDual: griglia su una vista denormalizzata, form sulla tabella. È il modello canonico (la pagina Prodotti).
Poi l'agente scandaglia la soluzione per i progetti dati (*.db/Models/*Context.cs)
e chiede, con le opzioni scoperte nel codice: quale DbContext, quali sorgenti
(vista di lettura e/o tabella di scrittura), Queryable o
InMemory (tabelle di lookup sotto ~2000 righe), e l'ampiezza dell'editing
(CRUD completo o solo aggiornamento).
Fase 2 - Introspezione e specifica
L'agente legge le classi del modello e l'OnModelCreating per quell'entità, poi
scrive una bozza di specifica in docs/crud/<entità>.md
già precompilata: colonne (spuntate quelle che portano identità, larghezze e formati
suggeriti per id, date e importi), campi del form (obbligatori dai NOT NULL, lunghezze
massime dagli HasMaxLength, un sottoinsieme rappresentativo se la tabella ha
duecento colonne), combo per le chiavi esterne, totali candidati, filtri, concorrenza
(rowversion presente o "ultimo che scrive vince") e le avvertenze di dominio.
La bozza si legge, si corregge e si approva - le specifiche di
docs/crud/products.md e customers.md sono gli esempi compilati.
Fase 3 - Generazione
Le convenzioni non si ridiscutono, l'agente le applica:
- cartella per funzionalità:
Components/Pages/<Entità>/con<Entità>List.razore<Entità>EditPopup.razor; - griglia su un contesto posseduto dal componente
(
IDbContextFactory) dentro unGridDevExtremeDataSource<TRead>; - barra comandi con Nuovo / Apri / Aggiorna / Esporta (xlsx e csv) / Colonne (il selettore
nativo della griglia), verbi condivisi da
CommonStrings; - modifica in popup con
EditFormeDataAnnotationsValidator; le validazioni stanno in una classe parzialeIValidatableObjectnel progetto dati - il solo file scritto a mano lì, così sopravvive a un nuovo scaffolding EF; - eliminazione con popup di conferma della pagina (mai
ShowDataItemDeleteConfirmation, che su un'istanza stantia della riga focalizzata non fa nulla in silenzio); - nessuna stringa scritta a mano nel markup: inglese neutro nel resx
della pagina, tutto attraverso i localizzatori, marker aggiunto al catalogo così le
chiavi appaiono in
/admin/localisation; - voce nel rail attraverso
INavigationProvider, come chiave diNavigationStrings; - errori a toast, compreso il conflitto di concorrenza; successo a toast più ricarica della griglia.
Fase 4 - Verifica
dotnet build, poi la pagina viene esercitata davvero nel browser: caricamento,
filtri (i totali devono ricalcolare), apertura, salvataggio con errori di validazione e
salvataggio pulito. Se emerge un comportamento nuovo di un repository, nasce un test
accanto a RepositoryDualTests.cs.
.csproj,
AddDbContextFactory<TContext> nel Program.cs con la stringa
di connessione <Database>Connection, versioni dei pacchetti EF allineate
tra i progetti (altrimenti NU1605).
11. Localizzazione
L'inglese neutro vive nei resx (in Shell/Resources per la
shell, in Resources per l'applicazione); ogni altra lingua vive nel
database e si modifica da /admin/localisation. Le lingue spedite
arrivano come JSON di seed (Data/Localization/it.seed.json, chiave di sezione =
nome completo del tipo di risorsa) applicato a ogni avvio chiave per chiave: una
chiave aggiunta a un seed atterra anche su installazioni che hanno già quella lingua, e un
valore modificato da un amministratore non viene mai sovrascritto.
Il meccanismo è la sostituzione della IStringLocalizerFactory: lo store si
stratifica sopra il resx senza che i componenti se ne accorgano. Anche le stringhe interne
dei componenti DevExpress (pager, menu filtro, selettore colonne) passano da lì, sotto la
pseudo-risorsa DevExpress.
Aggiungere una stringa di interfaccia significa toccare due file: il resx e il seed.
/admin/localisation: lingue abilitate, filtro per risorsa e
sezione, "solo mancanti", export del seed di una cultura.- La traduzione automatica è una cucitura non riempita:
IMachineTranslatorha una sola implementazione che risponde "non configurato", e il pulsante nella pagina resta spento. - Le pagine
/Account/*non sono localizzate: sono ancora i testi inglesi dello scaffolding di Identity. - Alcune stringhe che arrivano dai descrittori - nomi e descrizioni delle
sorgenti di notifica, nomi dei job in
/admin/jobs, nomi delle sorgenti di attività nella colonna "Origine" - sono in inglese: non passano dai resx, e i descrittori non hanno un marker di localizzazione come i moduli.
12. Chiavi di configurazione
| Chiave | A che serve |
|---|---|
ConnectionStrings:DefaultConnection | Il database Identity dell'applicazione. |
ConnectionStrings:DashboardConnection | Il database della shell (e dei moduli). Obbligatoria: senza, l'avvio si ferma. |
Bootstrap:AdminEmail | L'account che all'avvio entra nel ruolo Admin. |
Bootstrap:AdminPassword | Solo in sviluppo: crea quell'account se manca. Mai in produzione. |
Users:AllowSelfRegistration | Apre o chiude la registrazione pubblica (false come spedito). |
Email:SendGrid:ApiKey | Chiave SendGrid. Assente = nessun invio, link di invito da consegnare a mano. Quello che l'amministratore salva in /admin/email prevale. |
Email:From / Email:FromName | Mittente (indirizzo verificato in SendGrid) e nome visualizzato. Anche questi si impostano in /admin/email. |
Accounts:PasswordMinimumLength | Lunghezza minima della password (10). Restano attive le regole di Identity su cifre, maiuscole e simboli. |
Accounts:LockoutMaxFailedAttempts / Accounts:LockoutMinutes | Tentativi falliti prima del blocco dell'account (5) e durata del blocco (15 minuti). |
Accounts:SessionDays | Durata della sessione senza attività, scorrevole (7 giorni). |
Accounts:ThrottleMaxAttempts / Accounts:ThrottleWindowMinutes | Tentativi che un chiamante può fare contro login, reset e invito nella finestra (20 in 10 minuti). |
Tasks:ReminderTime | L'ora in cui il job spedisce i promemoria (08:00 locali del server per default). |
Messages:RetentionDays | Dopo quanti giorni si cancellano i messaggi letti (90; zero disattiva). |
Messages:HardRetentionDays | Dopo quanti giorni si cancella qualunque messaggio (365; zero disattiva). |
Components:Folder | La cartella dei pacchetti di componenti (§9). Relativa alla radice dei contenuti; component-packs per default. Letta all'avvio. |
Components:DataSources | Le sorgenti dati dei componenti nativi (§9.7): nome, connessione, viste e tabelle esponibili. Assente = i componenti nativi non hanno nulla da leggere. |
Docs:Url | Dove punta il collegamento "Documentazione" del rail. Vuoto = questa guida, servita dal pacchetto. |
Demo:NotificationFeed | Solo nell'host dimostrativo: il flusso di notifiche di esempio (attivo in Development). |
13. Parti da completare
Onestà sullo stato: questa è la 2.0.0-preview. Quello che segue non è rotto -
è dichiaratamente non finito.
/admin/email
(chiave, mittente, nome mittente, con un pulsante di prova), ma non ancora
collaudato in un'installazione reale. Finché non è configurato, il link di invito
si consegna a mano dal popup di /admin/users. Mancano l'alternativa SMTP, i
template modificabili e la coda con ripetizione dei tentativi.
Vedi §7.
licensing.promisoft.it all'accensione del modulo e ricontrollate ogni giorno;
un servizio irraggiungibile non ferma nulla, un rifiuto esplicito (limite raggiunto, licenza
revocata) spegne il modulo con la frase del servizio sulla card. Il servizio è scritto e
testato ma non ancora pubblicato: finché non lo è, ogni installazione
risulta "servizio non raggiungibile" e continua a funzionare.
/Account in inglese; nomi e descrizioni di sorgenti di notifica, job e
sorgenti di attività in inglese; traduzione automatica non configurata.
ICurrentUser e IPageSignals:
impostazioni, notifiche e il resto entrano quando un componente reale li chiede. Un pack non
può ancora dichiarare una famiglia di stringhe per /admin/localisation. I
collegamenti fra regioni sono uguaglianze su una colonna (niente intervalli né "contiene");
una regione occupa una frazione della riga e fino a tre righe; nessuna sorgente su un motore
diverso da quello dell'installazione; un pack nuovo o aggiornato si vede dopo un riavvio.
Promisoft-rc), ma la
release resta manuale: build/release.ps1 packa e pubblica, il tag si mette a
mano, non c'è una pipeline che lo faccia al posto tuo. Il feed è privato e richiede un
token anche in lettura: per i clienti finali la base gratuita andrà su nuget.org, con i
moduli a pagamento su un feed autenticato.
Vedi anche la guida dell'utente per il punto di vista di chi la usa
ogni giorno, e CHANGELOG.md nel repository per la storia delle versioni.



