Solicita dinámicamente un comando IBM i / AS400 CL con Blazor y NTi

Descubra en este artículo cómo generar dinámicamente interfaces de entrada para cualquier comando IBM i / AS400 CL utilizando Toolbox Extension, NTi y Blazor. Un enfoque .NET moderno, genérico y nativo para complementar las pantallas 5250.

Imagen ilustrativa del artículo

Cuando se trata de ejecutar comandos CL(CMD) en IBM i, la mayoría de los usuarios siguen utilizando las tradicionales pantallas verdes (5250). Estas interfaces, aunque eficaces para los usuarios internos, suelen ser complejas y poco intuitivas para los usuarios con poca o ninguna experiencia en IBM i. Ahora existen soluciones para modernizar, simplificar y mejorar la experiencia del usuario.

Con NTi y su extensión Toolbox, disponible como paquete NuGet, puede consultar directamente la API del sistema QCDRCMDD, suministrada por IBM, para recuperar automáticamente la descripción completa de un comando CL.

El método RetrieveCommandDefinition() encapsula completamente esta operación, realizándola automáticamente:

  • 1️⃣ La llamada a la API.
  • 2️⃣ Recuperación del flujo XML de la orden (CDML).
  • 3️⃣ La deserialización de este flujo en un objeto C# fuertemente tipado que puede ser utilizado directamente en sus aplicaciones .NET.

En función de sus necesidades, puede :

  • Recupere el XML sin procesar directamente mediante el método RetrieveCommandDefinitionXml() para un uso personalizado, en particular a través de API o herramientas de análisis externas.

Flujo XML del comando CL CRTLIB devuelto por QCDRCMDD

  • Utilice el objeto C# estructurado devuelto por RetrieveCommandDefinition(), para generar automática y dinámicamente interfaces de usuario modernas, en particular con Blazor como se ilustra en este tutorial.

Objeto CmdDefinition deserializado en C#

Así, a partir de una única instrucción

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

Obtendrá una descripción completa que podrá utilizar inmediatamente en su código, en particular:

  • Una lista exhaustiva de parámetros (obligatorios y opcionales)
  • Su tipo preciso (texto, numérico, cualificado, lista, etc.)
  • Valores por defecto, especiales y únicos
  • Todas las dependencias condicionales (Control Prompt) entre parámetros
  • Subelementos detallados de cada parámetro complejo

Esta capacidad de generar dinámicamente la estructura de los comandos CL abre el camino a nuevos usos, como la creación de formularios interactivos con prellenado automático, la automatización avanzada de validaciones de parámetros y la generación automática de comandos CL listos para usar con el fin de simplificar su ejecución y reutilización.

💡 El código presentado en este artículo se facilita únicamente a título informativo. No es una solución definitiva ni está optimizado para todos los contextos de uso. Simplemente ilustra lo que se puede conseguir utilizando los métodos NTi y Blazor de Toolbox Extensions.

El escenario: crear una interfaz dinámica para cualquier comando CL

Para ilustrar estas posibilidades en términos concretos, en este tutorial vamos a crear una Blazor Web App (.NET 8) basada en NTi y la extensión Toolbox. El objetivo será construir una interfaz dinámica y genérica que permita:

  • Cargue la estructura detallada de cualquier orden CL (estándar o personalizada).
  • Genere automáticamente los campos de entrada adecuados (texto, listas desplegables, casillas de verificación, etc.).
  • Construye dinámicamente la sintaxis CL completa lista para su uso.
  • Ejecute este comando validado directamente en el lado IBM i.

Todo ello presentado en una interfaz moderna, con el diseño Carbon de IBM, radicalmente diferente del enfoque tradicional de IBM i.

Formulario Blazor generado para CL CRTLIB

Paso 1: Cargar dinámicamente una estructura de pedido

El primer paso consiste en cargar dinámicamente la estructura completa de un comando CL introducido por el usuario. A continuación, esta estructura se utilizará para generar automáticamente los campos del formulario de entrada.

En primer lugar, es necesario establecer una conexión tradicional con el IBM i a través de NTi:

  • Instalador le paquete NuGet Aumerial.Data.Nti (NTi Data Provider).
  • A continuación, añada el paquete NuGet Aumerial.Toolbox para acceder a los métodos CL.
  • Configure una NTiConnection que apunte a su IBM i, y haga referencia a los 2 paquetes en su proyecto.

Una vez realizada esta configuración, la aplicación puede consultar el sistema para recuperar la descripción completa de un pedido.

En concreto, en cuanto el usuario introduce el nombre de un comando (por ejemplo CRTLIB), la aplicación Blazor interroga a la API de IBM i a través de NTi y recupera toda la información necesaria para construir el formulario de forma dinámica.

1️⃣ Declaración de estructuras de datos internasDeclaración de estructuras de datos internas

En el código del componente Blazor, declaramos varias variables que se utilizarán para gestionar el estado del formulario y almacenar los valores introducidos por el usuario:

// Objeto principal que contiene la definición completa del comando
private CommandDefinition? Command;
// El comando seleccionado actualmente
private Command? SelectedCommand;
// Almacenamiento de valores simples asociados a cada palabra clave (Kwd) en los parámetros del comando
private Dictionary CommandValues = new();
// Gestión de parámetros cualificados (por ejemplo, File(NAME/LIB))
private Dictionary> QualifierValues = new();
// Permite al usuario introducir un valor personalizado (anulación)
private Dictionary> QualifierCustom = new();
// Gestión de los subelementos de un parámetro complejo de tipo ELEM
private Dictionary> ElementValues = new();
// Gestión de selecciones múltiples mediante casillas de verificación
private Dictionary> CheckedValues = new();

Estos diccionarios permiten gestionar de forma independiente y aislada cada tipo de dato asociado a los distintos parámetros, lo que garantiza una gestión clara.

2️⃣ Recuperación e inicialización dinámica de datos

Cuando un usuario introduce un comando y lo valida, se llama al método RetrieveCommandDefinition():

try
{
    // Llamada al método RetrieveCommandDefinition() desde NTiConnection a través de Toolbox
    Command = await Task.Run(() => DBService.Conn.RetrieveCommandDefinition(CommandName.ToUpper(), "QSYS", true));
    // Comprobar que se ha devuelto un pedido válido
    if (Command != null && Command.CmdDefinition.Any())
    {
        // Selección del primer comando devuelto
        SelectedCommand = Command.CmdDefinition.FirstOrDefault();
        if (SelectedCommand != null)
        {
            // Restablecer los diccionarios antes de usarlos
            CommandValues.Clear();
            QualifierValues.Clear();
            QualifierCustom.Clear();
            CheckedValues.Clear();
            ElementValues.Clear();
            // Búsqueda de parámetros para la inicialización individual
            foreach (var param in SelectedCommand.Parameters)
            {
                // cAlmacena el valor por defecto o vacío si no está definido
                CommandValues[param.Kwd] = param.Dft ?? string.Empty;
                // Inicialización de casillas de verificación para parámetros de selección múltiple
                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;  // ninguna selección marcada por defecto
                    }
                }
                // Inicialización de parámetros cualificados (tipo QUAL, por ejemplo, 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++}";
                    
                    // Si el parámetro es de tipo QUAL, inicialice los valores a vacíos por defecto
                    QualifierValues[param.Kwd][key] = param.Type == "QUAL" ? "" : (qual.Dft ?? "");
                    QualifierCustom[param.Kwd][key] = ""; // siempre vacío inicialmente para posibles entradas del usuario
                }

                // Inicialización de subelementos para parámetros complejos (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 vez recuperada la estructura completa del comando, validamos que esté correctamente definido e inicializamos los diccionarios internos para almacenar por separado cada valor vinculado a este comando. En concreto, repasamos cada parámetro devuelto por la API para determinarlo:

  • Valores por defecto que se mostrarán en los campos simples,
  • Múltiples opciones (casillas de verificación) y su estado inicial (sin marcar por defecto),
  • Cualquier valor cualificado (nombre/biblioteca) que se presente al usuario,
  • Les sous-éléments des paramètres complexes à afficher distinctement.

Interfaz Blazor dinámica con campos dependientes

Este enfoque garantiza que toda la información necesaria, claramente estructurada y aislada en nuestros diccionarios, esté inmediatamente disponible para construir una interfaz dinámica adaptada a cada pedido de CL, sea cual sea su complejidad o particularidades, con la certeza de que todos los campos están correctamente precumplimentados, que las listas desplegables, casillas de verificación y zonas condicionales funcionan a la perfección, y que el pedido se reconstruirá dinámicamente sin ninguna pérdida de información.

👉 Para más detalles sobre la estructura devuelta, consulte la documentación del método RetrieveCommandDefinition.

Paso 2: Generar campos de entrada dinámicamente

Una vez cargada la estructura completa del comando CL e inicializados nuestros diccionarios de estados (ver paso 1), podemos generar dinámicamente los campos de entrada adaptados a cada parámetro en nuestra interfaz Blazor y que reflejen fielmente los parámetros devueltos por el IBM i a través de NTi.

En concreto, recorremos cada parámetro de SelectedCommand.Parameters, y aplicamos lógica condicional para mostrar dinámicamente el componente adecuado según el tipo y las restricciones definidas en el lado IBM i: tipo simple, valores especiales, parámetros cualificados, elementos anidados, selección múltiple, etc.

Una lógica basada en metadatos

Cada parámetro se procesa en un bucle @foreach y, en función de sus propiedades (Type, SpecialValues, Elements, etc.), se genera un renderizado específico. Esta lógica se basa en bloques if/else que interpretan dinámicamente los metadatos proporcionados por la API para cada 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" />
    }
}

Generación dinámica de campos de entrada CL

La estructura del renderizado no sólo es dinámica, sino que también se adapta al comportamiento esperado en el lado IBM i, gracias a los metadatos devueltos por la API a través de NTi.

  • La longitud máxima autorizada (param.Len)
  • El texto de la sugerencia (param.Prompt)
  • Valores por defecto (param.Dft)
  • Valores permitidos (SpecialValues)
  • Grupos de entrada (elementos cualificados y anidados)

Si un parámetro de tipo CHAR tiene una longitud de 10 caracteres con un valor predeterminado, éste se rellenará automáticamente y la longitud se limitará a 10 mediante el atributo maxlength. Si el parámetro espera un valor cualificado (por ejemplo, archivo + biblioteca), se muestran varios campos y se vinculan a sus subpartes. Cada campo se vincula directamente a los diccionarios de estado inicializados anteriormente (CommandValues, QualifierValues, ElementValues, etc.), de modo que cualquier valor introducido o seleccionado pueda capturarse inmediatamente sin necesidad de procesamiento adicional.

Gestión de la dependencia

Algunos comandos también incorporan dependencias condicionales entre parámetros, conocidas como Control Prompt y expuestas a través de SelectedCommand.Dependencies. Estas reglas pueden utilizarse en el componente para desactivar, rellenar previamente o hacer obligatorios determinados campos en función de los valores introducidos en otros. Por ejemplo: si el parámetro TYPE es *USR, entonces OWNER se convierte en obligatorio.

Este tutorial no entra en detalles sobre cómo gestionarlos en la interfaz, pero todos los elementos necesarios son accesibles a través del método RetrieveCommandDefinition(), de modo que pueden integrarse fácilmente en el componente Bazor.

Paso 3: Construir dinámicamente el comando CL a ejecutar

En esta fase, se han rellenado todos los campos de la interfaz y sus valores se han almacenado en los diccionarios internos (CommandValues, QualifierValues, ElementValues, etc.). El objetivo ahora es reconstruir dinámicamente la sintaxis completa del comando CL a partir de los datos recogidos, en forma de cadena lista para ser ejecutada en el lado IBM i.

Esta lógica de implementación está centralizada en un método específico: 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();

            // Elementos integrados (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)})");
                }
            }

            // Parámetros cualificados (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)
                {
                    // No generar nada si falta uno de los elementos
                }
                else if (values.Any())
                {
                    var combined = string.Join("/", values.AsEnumerable().Reverse());
                    segment.Add($"{param.Kwd.ToUpperInvariant()}({combined.ToUpperInvariant().Trim()})");
                }
            }

            // Selecciones múltiples (casilla de verificación)
            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();
}

El comando final se inicializa comenzando por el nombre principal (DLTLIB, CRTLIB, etc.), seguido de una serie de parámetros formateados dinámicamente según su tipo. Para cada parámetro se manejan varios casos.

  • Si se introduce un único valor como CommandValues, se añade directamente, con un tratamiento especial para el parámetro TEXT (rodeado de comillas simples).
  • Si no se rellena ningún campo simple, se analizan las estructuras secundarias
  • Para los parámetros anidados de tipo ELEM, ensamblamos los subvalores separándolos con un espacio.
  • Para los parámetros QUAL, combinamos los subvalores (por ejemplo, archivo/biblioteca), teniendo en cuenta cualquier entrada manual (anulación). Evitamos añadir el parámetro si falta parte de él (sobre todo en el caso de pares críticos como FILE).
  • Para múltiples valores de casilla de verificación, todas las opciones seleccionadas se concatenan con una coma, como en OPTION(*SRC, *MBR).

Cada segmento se convierte a mayúsculas, se limpia y luego se concatena con el comando principal. El resultado final es un comando CL completo, limpio y compatible que puede ser ejecutado en el lado IBM i - como si hubiera sido tecleado en un terminal 5250, pero aquí generado dinámicamente desde una interfaz moderna.

Comando CL generado dinámicamente en Blazor

Paso 4: Ejecute el comando en el lado IBM i

Una vez construido el comando (véase el paso 3), sólo queda ejecutarlo en el IBM i. Para ello hay dos etapas: una comprobación preliminar de la sintaxis, seguida de la ejecución propiamente dicha del comando. Todo sucede en un método SubmitCommand() llamado cuando el formulario Blazor es enviado:

private async Task SubmitCommand()
{
    messageSubmit = "";
    string commandCL = BuildCommandCL();
    try
    {
        DBService.Conn.CheckCLCommand(commandCL); // Comprueba la sintaxis sin ejecutar
        DBService.Conn.ExecuteClCommand(commandCL); // Ejecuta el comando
        messageSubmit = "Commande envoyée avec succès.";

        await Task.Delay(4000); // Pausa antes del reinicio
        ResetForm();            // Vaciar el formulario
        ResetGenerateCommand(); // Borra el comando visualizado
    }
    catch (Exception ex)
    {
        messageSubmit = $"{ex.Message}"; 
    }
}

1️⃣ Comprobación previa con CheckCLCommand()

Antes de que el pedido pueda ser enviado, debe ser validado. Esto se comprueba mediante el método CheckCLCommand(), que también está disponible a través de la extensión NTi Toolbox. Interroga al IBM i para analizar la validez sintáctica completa del comando, sin ejecutarlo: palabras clave desconocidas, valor mal formateado, parámetro obligatorio ausente. Esta comprobación anticipada evita los errores en caliente, reproduciendo exactamente el mismo comportamiento que el sistema en un entorno 5250.

Verificación de sintaxis con CheckCLCommand

2️⃣ Ejecución con ExecuteClCommand().

Una vez validado, el comando se transmite a través de ExecuteClCommand(), un método expuesto por la clase NTiConnection de NTi DataProvider. Se ejecuta en la sesión que ya está abierta, con su entorno completo: usuario, trabajo activo, biblioteca actual, autorizaciones. Cualquier mensaje de respuesta o error del sistema se devuelve como si el comando se hubiera ejecutado directamente desde un terminal IBM i.

Ejecución del CL con ExecuteClCommand

Qué se controla en el lado Blazor/.NET

Todo el proceso se gestiona dentro de la aplicación .NET. Se orquesta:

  • Generación dinámica del comando CL mediante BuildCommandCL(), a partir de los valores introducidos en la interfaz.
  • Validación sintáctica mediante CheckCLCommand(), que simula el comportamiento del sistema sin ejecutar realmente el comando.
  • La ejecución real con ExecuteClCommand(), en el contexto de sesión de IBM i (trabajo, usuario, biblioteca actual).
  • Gestión de errores y mensajes mediante un bloque try/catch, para proporcionar al usuario información del sistema IBM i.

El comando se ejecutó correctamente, y vemos el mensaje de éxito

Mensaje de éxito tras la ejecución del CL

Y la biblioteca se ha creado en nuestro IBM i:

Biblioteca MYLIB creada en IBM i

Conclusión

Con RetrieveCommandDefinition() y RetrieveCommandDefinitionXml(), ofrecidos por ToolboxExtension, puede generar dinámicamente interfaces de entrada para cualquier comando CL, simplemente confiando en los metadatos devueltos por la API QCDRCMDD.

RetrieveCommandDefinition()` encapsula este proceso por completo en .NET: consulta la API de IBM i, recupera el flujo XML en formato CDML, lo descodifica y lo deserializa en un objeto C# fuertemente tipado. No es necesario analizar ni manipular manualmente el XML. Toda la estructura del comando (parámetros, tipos, valores, dependencias, etc.) está inmediatamente disponible y puede utilizarse en el código.

Esta abstracción reduce considerablemente la carga de trabajo en la parte de desarrollo, lo que le permite concentrarse únicamente en la UX y la lógica empresarial, sin tener que codificar manualmente la estructura de cada pedido.

Pero, sobre todo, por fin sales de los rígidos confines de un terminal 5250. Su interfaz se convierte en portátil, accesible desde un navegador, una aplicación empresarial, una estación de trabajo cliente o una plataforma en contenedores. En otras palabras, este cambio de enfoque abre el camino a usos realmente nuevos:

  • Formularios simplificados para equipos no especializados en IBM i
  • Centralización de los pedidos en un portal de administración
  • Integración en flujos de trabajo DevOps o herramientas de apoyo
  • Automatización de sistemas o tareas técnicas, sin sesión verde

No se abandona IBM i: se amplía. A la web, a .NET, al mañana.

Los métodos de Toolbox no pretenden sustituir sus conocimientos de IBM i, sino liberarle de la complejidad técnica y ampliarlos a un nuevo ecosistema más ágil y accesible. Ahorre tiempo de desarrollo y aumente claramente el valor de sus aplicaciones.


Quentin Destrade