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.
- 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.
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.
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.
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" />
}
}
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ámetroTEXT
(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.
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.
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.
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
Y la biblioteca se ha creado en nuestro 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