C# Script para PBIR

Este contenido fue traducido mediante IA y no ha sido revisado por un editor humano. Las imágenes y los gráficos permanecen en su idioma original.

Puntos clave

  • Gracias al formato Enhanced Report de Power BI (PBIR), la capa del Report de Power BI ya no está fuera del alcance de los scripts. Ahora puedes usar scripts de C# (C# Script) en Tabular Editor para manipular y agilizar cambios en Reports y objetos Visual.
  • Esto sigue siendo un área emergente. Aunque existe Sempy Labs para realizar algunas operaciones con Reports publicados, es muy limitado. Al hacer scripting sobre archivos PBIR, en la práctica empezamos desde cero.
  • Los cambios no se ven automáticamente en Power BI Desktop (todavía). Hay muchos casos de uso potenciales, pero tenemos que cerrar y volver a abrir el Report para ver las diferencias, lo que reduce el atractivo de este tipo de scripts.

Este resumen lo ha producido el autor y no una IA.


Introducción

La funcionalidad de C# Script de Tabular Editor lleva mucho tiempo siendo una de sus principales ventajas. Te permite manipular un modelo de forma programática, ayudándote a agilizar o automatizar muchas tareas con total comodidad. Incluso puedes usarlo para llamar a APIs, como una para dar formato a Power Query o usar LLMs para traducir tus medidas. Aunque no sepas C#, puedes usar LLMs para que te ayuden a escribir los scripts.

Si buscas en Internet, encontrarás muchos scripts que te ayudarán desde crear medidas SUM para las columnas seleccionadas hasta casos de uso más complejos, como crear medidas para identificar violaciones de integridad referencial en el modelo, o añadir grupos de cálculo y UDF de DAX dependientes del modelo. Pero ¿sabías que ahora también puedes hacer scripting en la capa del Report? He estado experimentando con esto y en Tabular Editor creemos que mucha más gente podría beneficiarse.

Aquí puedes ver un ejemplo que copia el formato condicional de una columna al resto:

El código de este ejemplo en concreto se puede encontrar aquí. En este artículo hablaremos, de forma más general, sobre cómo crear este tipo de scripts, profundizando en un caso de uso distinto.

El potencial del scripting en la capa del Report

La mayoría de los desarrolladores de Power BI estarán de acuerdo en que crear un Report atractivo suele implicar hacer un montón de clics en el panel de formato. Incluso cuando tienes un tema personalizado bien cuidado (y deberías), hay operaciones que implican muchos clics, como configurar el mismo formato condicional para todos los campos de una tabla ancha; de ahí el ejemplo que vimos arriba.

Hay casos de uso que requieren modificar tanto el modelo como la capa de Report. Por ejemplo, después de crear esas medidas de integridad referencial mencionadas anteriormente, sería ideal que el script continuara configurando una página para identificar exactamente los valores infractores. Por suerte, estos dos scripts (y algunos otros) ya existen, pero hay muchos más casos de uso. Pero ¿cómo funcionan estos scripts? Pues modifican los archivos de metadatos del Report que usan el nuevo formato PBIR. Veámoslo en detalle.

Por comodidad, este vídeo te guía por todo el proceso, de principio a fin. Muestra un ejemplo de cómo puedes hacer scripting del Report y del modelo a la vez:

\n

 

Todo lo que aparece en el vídeo se explica en el artículo siguiente.

¿Qué es PBIR y cómo puedes modificarlo?

PBIR es un nuevo formato que se puede usar para Reports de Power BI. Entrar en detalle sobre el formato PBIR queda fuera del alcance de este artículo, pero basta con saber que divide todos los elementos del Report en archivos JSON más pequeños. La mayor parte de las definiciones del Report se almacenan en archivos visual.json, uno por cada objeto Visual. Aunque hay algunos archivos más, cualquier configuración que hagas en tu Visual lo más probable es que quede reflejada en el archivo visual.json correspondiente. Puedes modificar los archivos manualmente, pero lleva tiempo y no es fácil. C# Script puede ayudarte a hacer estas modificaciones mucho más rápido. Pero hay algunos retos que superar.

¿Dónde están los archivos del Report?

Poder hacer scripting en la capa de Report es genial, pero hacer scripting sobre el formato PBIR es complejo. Para empezar, Tabular Editor trabaja conectado a un modelo, pero incluso cuando quieres modificar el Report asociado a ese modelo dentro del mismo proyecto PBIP, no hay forma de encontrarlo automáticamente en el equipo: tienes que indicárselo al script. Como PBIR siempre sigue la misma estructura, una posible implementación es apuntar al archivo “.pbir” (un archivo muy pequeño en la raíz de la carpeta de definición del Report) y, a partir de ahí, el script puede encontrar el resto de archivos relevantes. Ahora bien, estar apuntando cada vez al mismo archivo mientras desarrollas un script es un fastidio; por eso existe una lista de archivos recientes como la que viste en el vídeo anterior de este artículo. En la siguiente sección, explicaremos con más detalle cómo aprovechar esto y otras funcionalidades genéricas en tus scripts.

¿Cómo modificas archivos JSON con C#?

El siguiente problema al que te enfrentarás es cómo trabajar con los archivos JSON. En C# puedes definir objetos y, lo más importante, ya existe funcionalidad nativa que convierte un archivo JSON en un objeto y viceversa. Convertir un JSON en un objeto de C# se llama deserialización, y convertir un objeto de C# en un archivo JSON se llama serialización. Así, la estrategia es primero deserializar todos los archivos JSON en un objeto que te resulte cómodo, hacer las modificaciones necesarias y volver a serializar todos los archivos que hayas modificado.

B001 Figure 2 - Diagram showing three-step C# workflow for working with JSON files: deserialize JSON to C# objects, modify the objects in C#, then serialize back to JSON

Por supuesto, la forma en que definas ese objeto debe coincidir con lo que viene en el archivo JSON. Y aquí es donde la cosa se complica. Aunque el esquema de estos archivos es técnicamente público, es tan complejo (y está repartido por tantos sitios) que resulta muy difícil crear una clase que contemple todas las definiciones posibles de objetos Visual. Para este caso de uso, aprovecharemos un repositorio público que puede servir como punto de partida.

Una vez que hayas convertido el archivo JSON en un objeto de C#, puedes trabajar con él igual que con el modelo.

NOTA

La enorme complejidad de los objetos hace que la versión de C# que se usa en Tabular Editor 2 se quede corta. Por suerte, es posible configurar un compilador distinto y usar expresiones más avanzadas en el código. En Tabular Editor 3 no hace falta configuración adicional.

¿Guardar o no guardar?

Con Power BI Desktop abierto, cuando cambias los archivos visual.json del formato PBIR del Report, el Report que ves no cambia de inmediato. Para evitar confusiones sobre cuándo guardar (o no) al cerrar Power BI Desktop, la forma más segura de hacerlo es:

  1. Guarda tu trabajo y cierra Power BI Desktop antes de ejecutar el script.
  2. Abre el modelo en TE directamente desde model.bim o TMDL y ejecuta el script.
  3. Cuando el script termine, puedes abrir Power BI Desktop para ver los cambios en el Report.

Y por último, como ya estamos usando el formato PBIP, asegúrate de inicializar el repositorio Git local y hacer commit de cualquier cambio antes de ejecutar scripts. Es la mejor forma de verificar que los cambios son los que querías, y la manera más sencilla de volver atrás si pasa algo inesperado. Esto se aplica a cualquier script, pero aún más a los scripts que modifican la capa del Report. Aunque las modificaciones del modelo se pueden deshacer con Ctrl+Z, eso no ocurre con las modificaciones en los archivos PBIR.

CONSEJO

Cuando ejecutes scripts, y especialmente cuando ejecutes scripts que modifican la capa de Report, asegúrate de revisar los cambios en Git.

Caso de uso: almacenar y aplicar nombres para mostrar personalizados

Para demostrar lo descrito en las secciones anteriores, vamos a meternos con el siguiente caso de uso: a veces, en el Report queremos usar nombres para mostrar que son distintos de los nombres reales de medidas y columnas. Esto puede deberse a varios motivos: por ejemplo, queremos mostrar medidas distintas con el mismo nombre, o en el modelo necesitamos un nombre largo para dejar claro qué es, pero no necesitamos tanta información en el objeto Visual.

Lo habitual sería configurar manualmente los nombres que queremos en un objeto Visual y luego darnos cuenta de que tenemos que replicarlos en otras partes del Report. Sería ideal poder guardar los nombres para mostrar en algún sitio dentro de las propiedades de la columna o medida, y luego aplicarlos allí donde sea necesario. Pues ahora podemos. Vamos a ello. Crearemos un script para guardar los nombres para mostrar de uno o varios Visuales y otro para aplicarlos a uno o varios Visuales.

NOTA

Para este caso de uso, utilizaremos la configuración de autoría de código presentada en esta entrada del blog. Esta solución contiene clases con funciones generales, la deserialización del Report y su manipulación. La configuración incluye una macro para copiar cualquiera de los scripts definidos en Visual Studio o VS Code a Tabular Editor. La macro añadirá las clases personalizadas necesarias para ejecutar el script y otras pequeñas modificaciones.

Almacenar los nombres para mostrar

Para este caso de uso, tendremos que trabajar tanto con el Report como con el modelo. El script se ejecutará mientras estés conectado al modelo, así que en ese sentido no hay nada que hacer. Para el Report, en cambio, tendremos que seleccionarlo para que el script pueda deserializar archivos JSON en un objeto de C# más manejable.

Vamos a utilizar los métodos de la clase GeneralFunctions y los de ReportFunctions. Para marcar esta dependencia al escribir el código en Visual Studio o VS Code, tenemos que incluir estos dos comentarios al principio del método:

//using GeneralFunctions; 
//using Report.DTO;

Estos dos comentarios serán eliminados por la macro que copia el código para pegarlo en Tabular Editor, y esto es específico de la configuración de autoría que estamos usando. Son la marca que hará que la macro recupere el código de las clases personalizadas y lo añada al final del script.

La inicialización del Report es siempre la misma, y podemos aprovechar un método ya definido en la clase ReportFunctions. Este método analiza pages.json, todos los archivos page.json y todos los archivos visual.json, y organiza todos los objetos en un único objeto Report de gran tamaño. También incluye todos los cuadros de diálogo para seleccionar un Report reciente o seleccionar uno nuevo.

// Step 1: Initialize report
ReportExtended report = Rx.InitReport();
if (report == null) return;

A continuación, podemos permitir al usuario seleccionar uno o varios Visuales del Report que se usarán como origen para importar cualquier nombre para mostrar personalizado configurado en ellos.

// Step 2: Let user select visuals
IList<VisualExtended> selectedVisuals = Rx.SelectVisuals(report);
if (selectedVisuals == null || selectedVisuals.Count == 0)
{
    Info("No visuals selected.");
    return;
}
CONSEJO

Se considera una buena práctica evitar excepciones no controladas. Siempre que haya interacción del usuario, ten en cuenta que puede cerrar el cuadro de diálogo sin hacer una selección válida. Es buena idea comprobarlo siempre y, si el objeto no se ha inicializado, informar al usuario y cancelar la ejecución. Rx.InitReport ya tiene los mensajes en el código, así que solo es necesario cancelar la ejecución. Si la ejecución continúa, significa que todas las variables se han inicializado correctamente.

Una vez llegados a este punto, es momento de iterar los Visuales seleccionados y ver si los campos tienen definido un nombre personalizado. Primero, tenemos que mirar el archivo JSON real para entender dónde se encuentra esta información.

{
  "$schema": "https://developer...",
  "name": "5715...",
  "position": {...},
  "visual": {
    "visualType": "clusteredColumnChart",
    "query": {
      "queryState": {
        "Category": {
          "projections": [...]
        },
        "Y": {
          "projections": [...]
        }
      }
    },
    "objects": {...},
    "drillFilterOtherVisuals": true
  }
}

Los campos que componen un Visual están dentro de estos distintos elementos “queryState”, “Category”, “Y” (hay otros tipos en otros Visuales). Todos tienen contenidos similares. Pueden ser “Projections” o “Field Parameters”. Para nuestro caso de uso, no nos importan los Field Parameters. Así que, para iterar todas las proyecciones posibles, podemos escribir el siguiente código:

foreach (var visual in selectedVisuals)
{
    var queryState = visual.Content?.Visual?.Query?.QueryState;
    if (queryState == null) continue;
 
    // Create list of all projection sets to iterate
    var projectionSets = new List<VisualDto.ProjectionsSet>
    {
        queryState.Values,
        queryState.Y,
        queryState.Y2,
        queryState.Category,
        queryState.Series,
        queryState.Data,
        queryState.Rows
    };
 
    // Iterate through each projection set
    foreach (var projectionSet in projectionSets)
    {
foreach (var projection in projectionSet.Projections)
{ ... }
    }
}
NOTA

Los signos de interrogación en las expresiones de acceso a miembros no son posibles con la versión predeterminada de C# usada por Tabular Editor 2, y la sintaxis alternativa es extremadamente verbosa. Para ejecutar scripts que contengan este tipo de expresiones, tendrás que configurar el compilador Roslyn tal y como se explicó antes en el artículo, o ejecutar el script en Tabular Editor 3.

Las proyecciones pueden ser un “Field” o un “Visual Calc”. Solo nos interesan los campos, porque son los que existen en el modelo y se pueden reutilizar en distintos Visuales. Para cada campo, queremos comprobar si tiene configurado un “displayName” y, si es así, usaremos “Entity” y “Property” (es decir, el nombre de tabla y el nombre de campo) para ir al modelo y guardar ahí el nombre para mostrar.

{
  "field": {
    "Measure": {
      "Expression": {
        "SourceRef": {
          "Entity": "Sales"  <-------- Table Name
        }
      },
      "Property": "Sales Amount" <---- Measure Name
    }
  },
  "queryRef": "Sales.Sales Amount",
  "nativeQueryRef": "Sales",
  "displayName": "Sales"  <---------- Display Name
}

En este punto, el código se complica un poco más. El script primero crea una estructura que almacena todos los nombres para mostrar posibles encontrados para cada campo, porque el código permite seleccionar más de un Visual para el análisis. Luego, si se ha encontrado más de un nombre para mostrar distinto para el mismo campo, la macro exige al usuario que elija el nombre para mostrar que se almacenará.

Una vez que tenemos una lista limpia de pares clave-valor (campo – nombre para mostrar), el script por fin guarda el nombre para mostrar elegido en el modelo, como una anotación:

foreach (var kvp in measureDisplayNames)
{
    var measure = Model.AllMeasures.FirstOrDefault(
        m => m.Table.DaxObjectFullName + m.DaxObjectFullName == kvp.Key);
    if (measure != null)
    {
        if (measure.GetAnnotation("DisplayName") == kvp.Value) continue;
        
        measure.SetAnnotation("DisplayName", kvp.Value);
        measuresUpdated++;
    }
}

Las anotaciones son un gran sitio para almacenar información adicional sobre cualquier elemento del modelo, como el nombre para mostrar en este script. Las anotaciones no tienen ningún efecto funcional sobre el comportamiento del modelo; son, más bien, un lugar práctico para metadatos.

Una vez ejecutado el script, se recomienda guardar los cambios en el modelo para que estos nombres para mostrar estén disponibles en el futuro y se puedan aplicar a cualquier Report vinculado a él.

NOTA

El script se ejecutará tanto si el Report está conectado al modelo como si no, pero usar un Report no conectado al modelo puede provocar resultados inesperados.

Aquí puedes encontrar el script. Veámoslo en acción:

Aplicar los nombres para mostrar

Veamos cómo podemos construir el segundo script. Al igual que en el anterior, tendremos que apuntar al Report que hay que modificar. Si es el mismo Report, aparecerá en la parte superior de la lista de los últimos Reports en el selector de archivos, así que puedes hacer clic en OK.

El código es muy similar hasta que llegamos a la iteración de cada una de las proyecciones. Aquí es donde comprueba si el campo, tanto en el modelo como en el Report, tiene un nombre para mostrar configurado. Si encuentra una anotación de nombre para mostrar en el modelo y no hay un nombre para mostrar personalizado definido en el Visual, o hay uno distinto, aplicará al objeto Visual el nombre para mostrar almacenado en el modelo.

if (projection?.Field == null) continue;
 
string displayNameFromModel = null;
 
// Check if it's a medida
if (projection.Field.Measure != null)
{
    var measureExpr = projection.Field.Measure;
    if (measureExpr.Expression?.SourceRef?.Entity != null 
        && measureExpr.Property != null)
    {
        string fullName = String.Format(
            "'{0}'[{1}]", 
            measureExpr.Expression.SourceRef.Entity, 
            measureExpr.Property);
        
        var measure = Model.AllMeasures.FirstOrDefault(
            m => m.Table.DaxObjectFullName 
                + m.DaxObjectFullName == fullName);
 
        if (measure != null)
        {
            displayNameFromModel = 
                measure.GetAnnotation("DisplayName");
        }
    }
}
// Check if it's a column
else if (projection.Field.Column != null)
{  ... }
 
// Apply display name if found in model
if (!string.IsNullOrEmpty(displayNameFromModel))
{
    // Check if projection already has a display name
    if (string.IsNullOrEmpty(projection.DisplayName) 
        || projection.DisplayName != displayNameFromModel)
    {
        projection.DisplayName = displayNameFromModel;
        updatedCount++;
        visualModified = true;
    }
}

Este es el código que se ejecuta mientras se itera por cada tipo de proyección de un único Visual. Aquí es importante marcar si se ha hecho algún cambio en el Visual. Esto se consigue con la instrucción visualModified = true para indicar que el Visual se ha modificado. Después, tras iterar todas las proyecciones de un único Visual, se ejecuta este código:

// Save visual if it was modified
if (visualModified)
{
    Rx.SaveVisual(visual);
    visualsUpdated++;
}

De nuevo, aquí aprovechamos un método definido en la clase ReportFunctions que contiene el código necesario para volver a serializar el contenido del objeto de C# en el archivo visual.json. Aquí podemos llevar la cuenta de cuántos objetos Visual se modificaron para informar al usuario al final.

Consulta el script completo aquí. Y veámoslo en acción:

Este es solo un ejemplo sencillo de cómo leer y usar información del PBIR en un script de C#. Hay mucho más que puedes hacer, como buscar y reemplazar campos, automatizar cambios de formato y más.

En conclusión

Ahora que PBIR está disponible, los scripts de C# de Tabular Editor ya no están limitados a modificaciones del modelo. Al mismo tiempo, las modificaciones del Report no están exentas de riesgos y siempre deben hacerse prestando atención a los cambios que muestra Git.

Crear scripts para la capa del Report es complejo y, por ese motivo, se recomienda usar un entorno de desarrollo como el utilizado en el caso de uso, para aprovechar las ventajas de un IDE y poder reutilizar código entre scripts con facilidad.

Related articles