Prompt dinamico di un comando CL su IBM i con Blazor e NTi

In questo articolo scoprirete come generare dinamicamente interfacce di input per qualsiasi comando IBM i / AS400 CL utilizzando Toolbox Extension, NTi e Blazor. Un approccio .NET moderno, generico e nativo per integrare le schermate 5250.

immagine illustrativa dell'articolo “Prompt dinamico di un comando CL su IBM i con Blazor e NTi”

Quando si tratta di eseguire comandi CL(CMD) su IBM i, la maggior parte degli utenti utilizza ancora le tradizionali schermate verdi (5250). Queste interfacce, pur essendo efficaci per i principianti, sono spesso complesse e poco intuitive per gli utenti con scarse o nulle competenze di IBM i. Oggi esistono soluzioni per modernizzare, semplificare e migliorare l'esperienza dell'utente.

Con NTi e la sua estensione Toolbox, disponibile come pacchetto NuGet, è possibile interrogare direttamente l'API del sistema QCDRCMDD, fornito da IBM, per recuperare automaticamente la descrizione completa di un comando CL.

Il metodo RetrieveCommandDefinition() incapsula completamente questa operazione, eseguendola automaticamente:

  • 1️⃣ Chiamare l'API.
  • 2️⃣ Recupero del feed XML (CDML) per il comando CL.
  • 3️⃣ La deserializzazione di questo flusso in un oggetto C# fortemente tipizzato che può essere utilizzato direttamente nelle applicazioni .NET.

A seconda delle vostre esigenze, potete :

  • Recuperare l'XML grezzo direttamente con il metodo RetrieveCommandDefinitionXml() per un uso personalizzato, in particolare tramite API o strumenti di analisi esterni.

Flusso XML del comando CL CRTLIB restituito da QCDRCMDD

  • Utilizzare l'oggetto strutturato C# restituito da RetrieveCommandDefinition(), per generare automaticamente e dinamicamente interfacce utente moderne, in particolare con Blazor, come illustrato in questo tutorial.

Oggetto CmdDefinition deserializzato in C#

Quindi, da una singola istruzione

CommandDefinition commandDef = Conn.RetrieveCommandDefinition("CRTLIB", "QSYS", false);

Si ottiene una descrizione completa che può essere utilizzata immediatamente nel codice, in particolare:

  • Un elenco esaustivo di parametri (obbligatori e facoltativi)
  • Il loro tipo preciso (testo, numerico, qualificato, elenco, ecc.)
  • Valori predefiniti, speciali e unici
  • Tutte le dipendenze condizionali (prompt di controllo) tra i parametri
  • Sottoelementi dettagliati di ciascun parametro complesso

Questa capacità di generare dinamicamente la struttura dei comandi CL apre la strada a nuovi utilizzi, come la creazione di moduli interattivi con precompilazione automatica, l'automazione avanzata della validazione dei parametri e la generazione automatica di comandi CL pronti all'uso per semplificarne l'esecuzione e il riutilizzo.

💡 Il codice presentato in questo articolo è fornito solo a scopo informativo. Non rappresenta una soluzione definitiva, né è ottimizzato per tutti i contesti di utilizzo. Illustra semplicemente ciò che si può ottenere utilizzando i metodi delle estensioni del Toolbox NTi e Blazor.

Lo scenario: creare un'interfaccia dinamica per qualsiasi comando CL

Per illustrare concretamente queste possibilità, in questo tutorial creeremo un'applicazione web Blazor (.NET 8) basata su NTi e sull'estensione Toolbox. L'obiettivo sarà quello di costruire un'interfaccia dinamica e generica che permetta:

  • Caricare la struttura dettagliata di qualsiasi commando CL (standard o personalizzato).
  • Generare automaticamente i campi di input appropriati (testo, elenchi a discesa, caselle di selezione, ecc.).
  • Costruire dinamicamente la sintassi completa del CL pronta per l'uso.
  • Eseguire questo comando convalidato direttamente sul lato IBM i.

Il tutto presentato in un'interfaccia moderna, con il design Carbon di IBM, radicalmente diversa dall'approccio tradizionale di IBM i.

Modulo Blazor generato per CL CRTLIB

Passo 1: caricare dinamicamente la struttura di un ordine

Il primo passo consiste nel caricare dinamicamente la struttura completa di un comando CL inserito dall'utente. Questa struttura verrà poi utilizzata per generare automaticamente i campi del modulo di input.

Innanzitutto, è necessario stabilire una connessione tradizionale all'IBM i tramite NTi:

  • Installatore del pacchetto NuGet Aumerial.Data.Nti (NTi Data Provider).
  • Aggiungere quindi il pacchetto NuGet Aumerial.Toolbox per accedere ai metodi CL.
  • Impostate una NTiConnection che punti al vostro IBM i e fate riferimento ai due pacchetti nel vostro progetto.

Una volta effettuata questa configurazione, l'applicazione può interrogare il sistema per recuperare la descrizione completa di un ordine.

In concreto, non appena l'utente inserisce il nome di un comando (ad esempio CRTLIB), l'applicazione Blazor interroga l'API IBM i tramite NTi e recupera tutte le informazioni necessarie per costruire il modulo in modo dinamico.

1️⃣ Dichiarazione delle strutture dati interne

Nel codice del componente Blazor, dichiariamo diverse variabili che saranno utilizzate per gestire lo stato del modulo e memorizzare i valori inseriti dall'utente:

// Oggetto principale contenente la definizione completa del comando
private CommandDefinition? Command;
// Il comando attualmente selezionato
private Command? SelectedCommand;
// Memorizzazione dei valori semplici associati a ciascuna parola chiave (Kwd) nei parametri del comando
private Dictionary CommandValues = new();
// Gestione di parametri qualificati (ad es. File(NAME/LIB))
private Dictionary> QualifierValues = new();
// Permette all'utente di inserire un valore personalizzato (override)
private Dictionary> QualifierCustom = new();
// Gestione dei sottoelementi di un parametro complesso di tipo ELEM
private Dictionary> ElementValues = new();
// Gestione di selezioni multiple tramite caselle di controllo
private Dictionary> CheckedValues = new();

Questi dizionari consentono di gestire ogni tipo di dato associato ai diversi parametri in modo indipendente e isolato, garantendo una gestione chiara.

2️⃣ Recupero e inizializzazione dinamica dei dati

Quando un utente inserisce un comando e lo convalida, viene richiamato il metodo RetrieveCommandDefinition():

try
{
    // Chiamata del metodo RetrieveCommandDefinition() da NTiConnection tramite Toolbox
    Command = await Task.Run(() => DBService.Conn.RetrieveCommandDefinition(CommandName.ToUpper(), "QSYS", true));
    // Verificare che sia stato restituito un ordine valido
    if (Command != null && Command.CmdDefinition.Any())
    {
        // Selezione del primo comando restituito
        SelectedCommand = Command.CmdDefinition.FirstOrDefault();
        if (SelectedCommand != null)
        {
            // Azzeramento dei dizionari prima dell'uso
            CommandValues.Clear();
            QualifierValues.Clear();
            QualifierCustom.Clear();
            CheckedValues.Clear();
            ElementValues.Clear();
            // Navigazione dei parametri per l'inizializzazione individuale
            foreach (var param in SelectedCommand.Parameters)
            {
                // Memorizza il valore predefinito o vuoto se non definito
                CommandValues[param.Kwd] = param.Dft ?? string.Empty;
                // Inizializzazione delle caselle di controllo per i parametri a selezione multipla
                if (param.SpecialValues.Any() && int.TryParse(param.Max, out int max) && max > 1)
                {
                    CheckedValues[param.Kwd] = new Dictionary();
                    foreach (var spcVal in param.SpecialValues.SelectMany(s => s.SpecialValues))
                    {
                        CheckedValues[param.Kwd][spcVal.Value] = false;  // nessuna selezione controllata per impostazione predefinita
                    }
                }
                // Inizializzazione di parametri qualificati (tipo QUAL, ad esempio FILE(NOM/LIB))
                QualifierValues[param.Kwd] = new Dictionary();
                QualifierCustom[param.Kwd] = new Dictionary();

                int qualIndex = 0;
                foreach (var qual in param.Qualifiers)
                {
                    var key = !string.IsNullOrEmpty(qual.Prompt) ? qual.Prompt : $"{qual.Type}_{qualIndex++}";
                    
                    // Se il parametro è di tipo QUAL, inizializzare i valori a vuoto per impostazione predefinita
                    QualifierValues[param.Kwd][key] = param.Type == "QUAL" ? "" : (qual.Dft ?? "");
                    QualifierCustom[param.Kwd][key] = ""; // toujours vide initialement pour une éventuelle saisie utilisateur
                }

                // Inizializzazione di sottoelementi per parametri complessi (tipo ELEM)
                ElementValues[param.Kwd] = new Dictionary();
                foreach (var elem in param.Elements)
                {
                    ElementValues[param.Kwd][elem.Prompt] = elem.Dft ?? string.Empty;
                }
            }
        }
    }
}
catch (Exception ex)
{
    errorMessage = ex.Message;
}

Una volta recuperata la struttura completa del comando, ne convalidiamo la corretta definizione e inizializziamo i dizionari interni per memorizzare separatamente ogni valore collegato a questo comando. In concreto, si esamina ogni parametro restituito dall'API per determinare:

  • Valori predefiniti da visualizzare nei campi semplici,
  • Opzioni multiple (caselle di controllo) e il loro stato iniziale (deselezionato per impostazione predefinita),
  • Eventuali valori qualificati (nome/libreria) da presentare all'utente,
  • I sottoelementi dei parametri complessi devono essere visualizzati separatamente.

Interfaccia Blazor dinamica con campi dipendenti

Questo approccio garantisce che tutte le informazioni necessarie, chiaramente strutturate e isolate nei nostri dizionari, siano immediatamente disponibili per costruire un'interfaccia dinamica adatta a ogni ordine di CL, qualunque sia la sua complessità o le sue particolarità, con la certezza che tutti i campi siano correttamente precompilati, che gli elenchi a discesa, le caselle di controllo e le zone condizionali funzionino perfettamente e che l'ordine venga ricostruito dinamicamente senza alcuna perdita di informazioni.

👉 Per maggiori dettagli sulla struttura restituita, vedere la documentazione del metodo RetrieveCommandDefinition.

Passo 2: generare dinamicamente i campi di input

Una volta caricata la struttura completa del comando CL e inizializzati i nostri dizionari di stato (vedi passo 1), possiamo generare dinamicamente i campi di input adattati a ciascun parametro della nostra interfaccia Blazor e che riflettono fedelmente i parametri restituiti dall'IBM i tramite NTi.

In concreto, si esamina ogni parametro di SelectedCommand.Parameters e si applica la logica condizionale per visualizzare dinamicamente il componente giusto in base al tipo e ai vincoli definiti sul lato IBM i: tipo semplice, valori speciali, parametri qualificati, elementi annidati, selezione multipla, ecc.

Una logica guidata dai metadati

Ogni parametro viene elaborato in un ciclo @foreach e, a seconda delle sue proprietà (Tipo, Valori speciali, Elementi, ecc.), viene generato un rendering specifico. Questa logica si basa su blocchi if/else che interpretano dinamicamente i metadati forniti dall'API per ogni campo.

@foreach (var param in SelectedCommand.Parameters)
{
    if (param.Type == "QUAL")
    {
        foreach (var key in QualifierValues[param.Kwd].Keys)
        {
            <input class="bx--text-input" @bind="QualifierValues[param.Kwd][key]" placeholder="@key" />
        }
    }
    else if (param.SpecialValues.Any())
    {
        <select class="bx--select-input" @bind="CommandValues[param.Kwd]">
            <option value="">Choisir</option>
            @foreach (var val in param.SpecialValues.SelectMany(s => s.SpecialValues))
            {
                <option value="@val.Value">@val.Value</option>
            }
        </select>
    }
    else if (param.Elements.Any())
    {
        <fieldset>
            @foreach (var elem in param.Elements)
            {
                <input class="bx--text-input" @bind="ElementValues[param.Kwd][elem.Prompt]" placeholder="@elem.Prompt" />
            }
        </fieldset>
    }
    else if (param.Type == "CHAR")
    {
        <input class="bx--text-input" @bind="CommandValues[param.Kwd]" placeholder="@param.Prompt" maxlength="@param.Len" />
    }
}

Generazione dinamica dei campi di input CL

Non solo la struttura del rendering è dinamica, ma si adatta anche al comportamento previsto sul lato IBM i, grazie ai metadati restituiti dall'API tramite NTi.

  • La lunghezza massima autorizzata (param.Len)
  • Il testo del suggerimento (param.Prompt)
  • Valori predefiniti (param.Dft)
  • Valori ammessi (SpecialValues)
  • Gruppi di ingresso (elementi qualificati e annidati)

Se un parametro di tipo CHAR ha una lunghezza di 10 caratteri con un valore predefinito, questo verrà automaticamente precompilato e la lunghezza limitata a 10 utilizzando l'attributo maxlength. Se il parametro prevede un valore qualificato (ad esempio, file + libreria), vengono visualizzati diversi campi e collegati alle loro sottoparti. Ogni campo è legato direttamente ai dizionari di stato inizializzati in precedenza (CommandValues, QualifierValues, ElementValues, ecc.), in modo che qualsiasi valore inserito o selezionato possa essere catturato immediatamente senza ulteriori elaborazioni.

Gestione delle dipendenze

Alcuni comandi incorporano anche dipendenze condizionali tra i parametri, note come Prompt di controllo ed esposte tramite SelectedCommand.Dependencies. Queste regole possono essere utilizzate nel componente per disattivare, precompilare o rendere obbligatori alcuni campi in base ai valori inseriti in altri. Ad esempio: se il parametro TYPE è *USR, allora OWNER diventa obbligatorio.

Questo tutorial non approfondisce come gestirli nell'interfaccia, ma tutti gli elementi necessari sono accessibili tramite il metodo RetrieveCommandDefinition(), in modo da poterli integrare facilmente nel componente Bazor.

Passo 3: costruire dinamicamente il comando CL da eseguire

A questo punto, tutti i campi dell'interfaccia sono stati compilati e i loro valori memorizzati nei dizionari interni (CommandValues, QualifierValues, ElementValues, ecc.). L'obiettivo è ora quello di ricostruire dinamicamente la sintassi completa del comando CL a partire dai dati raccolti, sotto forma di una stringa pronta per essere eseguita sul lato IBM i.

Questa logica di implementazione è centralizzata in un metodo dedicato: BuildCommandCL():

string BuildCommandCL()
{
    var commandCl = SelectedCommand.CommandName.ToUpperInvariant().Trim() + " ";
    foreach (var param in SelectedCommand.Parameters)
    {
        if (!string.IsNullOrWhiteSpace(CommandValues[param.Kwd]))
        {
            string value = CommandValues[param.Kwd].ToUpperInvariant().Trim();
            if (param.Kwd.Equals("TEXT", StringComparison.OrdinalIgnoreCase))
            {
                value = $"'{value}'"; // Le champ TEXT est entre quotes
            }
            commandCl += $"{param.Kwd.ToUpperInvariant()}({value}) ";
        }
        else
        {
            var segment = new List();

            // Elementi incorporati (ELEM)
            if (ElementValues.TryGetValue(param.Kwd, out var elements))
            {
                var values = elements.Values.Where(v => !string.IsNullOrWhiteSpace(v))
                                            .Select(v => v.ToUpperInvariant().Trim()).ToList();
                if (values.Any())
                {
                    segment.Add($"{param.Kwd.ToUpperInvariant()}({string.Join(" ", values)})");
                }
            }

            // Parametri qualificati (QUAL)
            if (QualifierValues.TryGetValue(param.Kwd, out var qualifiers) &&
                QualifierCustom.TryGetValue(param.Kwd, out var customQualifiers))
            {
                var values = qualifiers.Keys.Select(key => !string.IsNullOrWhiteSpace(customQualifiers[key])
                                                           ? customQualifiers[key]
                                                           : qualifiers[key])
                                             .Where(v => !string.IsNullOrWhiteSpace(v)).ToList();

                if ((param.Kwd == "FILE" || param.Kwd == "SRCFILE") && values.Count < 2)
                {
                    // Non generare nulla se uno degli elementi è mancante
                }
                else if (values.Any())
                {
                    var combined = string.Join("/", values.AsEnumerable().Reverse());
                    segment.Add($"{param.Kwd.ToUpperInvariant()}({combined.ToUpperInvariant().Trim()})");
                }
            }

            // Selezioni multiple (casella di controllo)
            if (CheckedValues.TryGetValue(param.Kwd, out var checkDict))
            {
                var checkedValues = checkDict.Where(x => x.Value).Select(x => x.Key.ToUpperInvariant().Trim());
                if (checkedValues.Any())
                {
                    segment.Add($"{param.Kwd.ToUpperInvariant()}({string.Join(",", checkedValues)})");
                }
            }

            if (segment.Any())
            {
                commandCl += string.Join(" ", segment) + " ";
            }
        }
    }
    return commandCl.Trim();
}

Il comando finale viene inizializzato iniziando con il nome principale (DLTLIB, CRTLIB, ecc.), seguito da una serie di parametri formattati dinamicamente in base al loro tipo. Per ogni parametro vengono gestiti diversi casi.

  • Se un singolo valore viene inserito come CommandValues, viene aggiunto direttamente, con un trattamento speciale per il parametro TEXT (circondato da apici singoli).
  • Se non viene compilato alcun campo semplice, vengono analizzate le strutture secondarie.
  • Per i parametri annidati di tipo ELEM, si assemblano i sottovalori separandoli con uno spazio.
  • Per i parametri QUAL, si combinano i sottovalori (ad esempio, file/libreria), tenendo conto di eventuali inserimenti manuali (override). Si evita di aggiungere il parametro se una parte di esso manca (in particolare per coppie critiche come FILE).
  • Per i valori di più caselle di controllo, tutte le opzioni selezionate vengono concatenate con una virgola, come in OPTION(*SRC, *MBR).

Ogni segmento viene convertito in maiuscolo, ripulito e quindi concatenato con il comando principale. Il risultato finale è un comando CL completo, pulito e conforme che può essere eseguito sul lato IBM i, proprio come se fosse stato inserito in un terminale 5250, ma qui generato dinamicamente da un'interfaccia moderna.

Comando CL generato dinamicamente in Blazor

Passo 4: Eseguire il comando sul lato IBM i

Una volta costruito il comando (vedi passo 3), non resta che eseguirlo sull'IBM i. Questa fase si svolge in due tempi: un controllo preliminare della sintassi, seguito dall'esecuzione vera e propria del comando. Tutto avviene in un metodo SubmitCommand() chiamato quando il modulo Blazor viene inviato:

private async Task SubmitCommand()
{
    messageSubmit = "";
    string commandCL = BuildCommandCL();
    try
    {
        DBService.Conn.CheckCLCommand(commandCL); // Controlla la sintassi senza eseguire
        DBService.Conn.ExecuteClCommand(commandCL); // Esegue il comando
        messageSubmit = "Commande envoyée avec succès.";

        await Task.Delay(4000); // Pausa prima del reset
        ResetForm();            // Svuotare il modulo
        ResetGenerateCommand(); // Cancella il comando visualizzato
    }
    catch (Exception ex)
    {
        messageSubmit = $"{ex.Message}"; 
    }
}

1️⃣ Pre-verifica con CheckCLCommand().

Prima di essere inviato, l'ordine deve essere convalidato. Questo viene verificato con il metodo CheckCLCommand(), disponibile anche tramite l'estensione NTi Toolbox. Interroga l'IBM i per analizzare la completa validità sintattica del comando, senza eseguirlo: parole chiave sconosciute, valore mal formattato, parametro obbligatorio mancante. Questo controllo anticipato evita gli errori a caldo, riproducendo esattamente lo stesso comportamento del sistema in un ambiente 5250.

Controllo sintassi con CheckCLCommand

2️⃣ Esecuzione con EseguireClComando()

Una volta convalidato, il comando viene trasmesso tramite ExecuteClCommand(), un metodo esposto dalla classe NTiConnection di NTi DataProvider. Viene eseguito nella sessione già aperta, con il suo ambiente completo: utente, lavoro attivo, biblioteca corrente, autorizzazioni. Eventuali messaggi di feedback o errori di sistema sono restituiti come se il comando fosse stato eseguito direttamente da un terminale IBM i.

Esecuzione del CL con ExecuteClCommand

Cosa viene controllato sul lato Blazor/.NET

L'intero processo è gestito all'interno dell'applicazione .NET. Il processo è orchestrato:

  • Generazione dinamica del comando CL tramite BuildCommandCL(), in base ai valori inseriti nell'interfaccia.
  • Convalida della sintassi tramite CheckCLCommand(), che simula il comportamento del sistema senza eseguire effettivamente il comando.
  • L'esecuzione effettiva con ExecuteClCommand(), nel contesto della sessione IBM i (lavoro, utente, libreria corrente).
  • Gestione degli errori e dei messaggi tramite un blocco try/catch, per fornire all'utente un feedback da parte del sistema IBM i.

Il comando è stato eseguito correttamente e viene visualizzato il messaggio di successo

Messaggio di successo dopo l’invio del CL

E la libreria è stata creata sul nostro IBM i:

Libreria MYLIB creata su IBM i

Conclusione

Con RetrieveCommandDefinition() e RetrieveCommandDefinitionXml(), offerte da ToolboxExtension, è possibile generare dinamicamente interfacce di input per qualsiasi comando CL, semplicemente basandosi sui metadati restituiti dall'API QCDRCMDD.

RetrieveCommandDefinition()` incapsula questo processo interamente sul lato .NET: interroga l'API IBM i, recupera il flusso XML in formato CDML, lo decodifica e lo deserializza in un oggetto C# fortemente tipizzato. Non è necessario alcun parsing manuale o manipolazione dell'XML. L'intera struttura del comando (parametri, tipi, valori, dipendenze, ecc.) è immediatamente disponibile e può essere utilizzata nel codice.

Questa astrazione riduce notevolmente il carico di lavoro sul lato dello sviluppo, consentendovi di concentrarvi esclusivamente sulla UX e sulla logica di business, senza dover mai codificare manualmente la struttura di ogni ordine.

Ma soprattutto, si esce finalmente dai rigidi confini di un terminale 5250. L'interfaccia diventa portatile, accessibile da un browser, da un'applicazione aziendale, da una workstation client o da una piattaforma containerizzata. In altre parole, questo cambiamento di approccio apre la strada a usi veramente nuovi:

  • Moduli semplificati per i team IBM i non specializzati
  • Centralizzazione degli commando in un portale amministrativo
  • Integrazione nei flussi di lavoro DevOps o negli strumenti di supporto
  • Automazione di sistemi o attività tecniche, senza una sessione verde

Non si abbandona IBM i, ma lo si amplia. Al web, a .NET, a domani.

I metodi Toolbox non intendono sostituire le vostre competenze su IBM i, ma liberarvi dalla complessità tecnica ed estenderle in un nuovo ecosistema più agile e accessibile. Risparmiando tempo sul lato dello sviluppo e aumentando chiaramente il valore delle vostre applicazioni.


Quentin Destrade