PBIX Dashboard-Patcher
Bearbeiten Sie eine Power BI Desktop .pbix-Datei direkt: Seiten hinzufügen, Visuals hinzufügen, Layout ändern, Felder neu binden – und packen Sie sie dann so neu, dass Power BI Desktop sie ohne Korruptionsfehler öffnet.
Wann verwenden
- Der Benutzer hat eine gespeicherte
.pbix-Datei und möchte Visuals hinzufügen / ein Dashboard generieren. - Der Benutzer möchte Power BI-Berichtsänderungen automatisieren (CI, Massenvorlagen, datengesteuertes Layout).
- Ein früherer Bearbeitungsversuch hat
MashupValidationError,Invalid SecurityBindingsoder "korrupte Datei" beim Öffnen verursacht.
Das Kernproblem (lesen Sie dies zuerst)
Moderne .pbix-Dateien verwenden das PBIR Enhanced Report Format: Das Berichtslayout wird als JSON-Dateien pro Visual unter Report/definition/pages/<pageId>/visuals/<visualId>/visual.json gespeichert, nicht als das alte monolithische Report/Layout.
Eine PBIX ist ein ZIP-Archiv, das (unter anderem) Folgendes enthält:
DataModel– das Analysis Services-Modell (darf NICHT neu komprimiert werden; unverändert speichern).Report/definition/...– PBIR-JSON-Struktur pro Visual.SecurityBindings– ein DPAPI-verschlüsseltes Manifest, das das Dateilayout signiert.[Content_Types].xml– listet jede Datei im ZIP-Archiv auf.
Fallstricke, die die Datei beschädigen (aus Erfahrung gelernt):
- Python
zipfilesetzt externe Dateiattribute, die sich von denen von .NETsZipArchiveunterscheiden. Power BI validiert diese. Verwenden Sie .NETZipArchiveüber PowerShell, mitExternalAttributes = 0für jeden Eintrag. - Erneutes Komprimieren von
DataModelzerbricht es. Es muss mit derselben Komprimierungsmethode wie die Quelle gespeichert werden (normalerweiseDeflate, aber manchmalStored); am sichersten istNoCompression, wenn Sie es neu geschrieben haben, ansonsten bewahren Sie die ursprünglichen Bytes unverändert. - Das Hinzufügen von Dateien zerbricht
SecurityBindings. Es ist ein signiertes Manifest dessen, was im ZIP-Archiv ist. Wenn SieReport/definition/pages/<new>/visuals/.../visual.jsonhinzufügen, stimmt die Signatur nicht mehr überein und PBI weigert sich zu öffnen. - Eintragsnamen dürfen KEIN führendes
/haben.MissingBIPackagePart: /Versionbeim Öffnen bedeutet, dass IhrVersion-Eintrag als/Version(mit führendem Schrägstrich) gezippt wurde. Die naive PowerShellSubstring($src.Length + 1)hinterlässt einen führenden Backslash, der im ZIP zu/wird -> PBI lehnt ab. Berechnen Sie das Präfix aus dem aufgelösten langen Pfad undTrimStart('/')für jeden Eintragsnamen. - UTF-8-BOM in
Report/definition/**/*.jsonist fatal. PBI-Fehler:Only text with UTF8 encoding without BOM (byte order marks) is supported. Detected BOM: 'UTF-8'.Set-Content -Encoding UTF8undFile.WriteAllTextohne explizite Kodierung schreiben BEIDE eine BOM in Windows PowerShell 5.1 / .NET Framework. Schreiben Sie JSON immer mitNew-Object System.Text.UTF8Encoding($false), das anWriteAllTextoderStreamWriterübergeben wird.
Die Lösung für SecurityBindings: Entfernen Sie es aus dem ZIP und entfernen Sie seinen Eintrag aus [Content_Types].xml. Power BI Desktop regeneriert SecurityBindings beim nächsten Speichern. Die Datei öffnet sich sauber.
Vorgehensweise
1. Datei inspizieren
# In ein Arbeitsverzeichnis extrahieren, .NET verwenden, damit Attribute sauber zurückkommen
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::ExtractToDirectory("Test.pbix", "pbix_src")
Prüfen, ob es sich um PBIR (neu) oder Legacy handelt:
- PBIR: hat
Report\definition\pages\<pageId>\visuals\<visualId>\visual.json. - Legacy: hat nur
Report\Layout(binäre JSON-Zeichenfolge). Diese Fähigkeit zielt auf PBIR ab.
2. Das visual.json-Schema verstehen (Version 2.12.0)
Minimales cardVisual (KPI-Kachel):
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/2.12.0/schema.json",
"name": "<20-hex-char id>",
"position": {"x": 20, "y": 20, "z": 0, "height": 160, "width": 440, "tabOrder": 0},
"visual": {
"visualType": "cardVisual",
"query": {
"queryState": {
"Data": {
"projections": [{
"field": {
"Aggregation": {
"Expression": {"Column": {"Expression": {"SourceRef": {"Entity": "financials"}}, "Property": "Sales"}},
"Function": 0
}
},
"queryRef": "Sum(financials.Sales)",
"nativeQueryRef": "Sum of Sales",
"active": true
}]
}
},
"sortDefinition": {
"sort": [{"field": {"Aggregation": {"Expression": {"Column": {"Expression": {"SourceRef": {"Entity": "financials"}}, "Property": "Sales"}}, "Function": 0}}, "direction": "Descending"}],
"isDefaultSort": true
}
},
"drillFilterOtherVisuals": true
}
}
Wichtige Fakten:
nameist eine 20-stellige Hex-ID; muss innerhalb der Seite eindeutig sein.visualTypeBeispiele:cardVisual,clusteredBarChart,clusteredColumnChart,lineChart,donutChart,pieChart,tableEx,pivotTable.cardVisualverwendet die RolleData. Balken-/Säulen-/Liniendiagramme verwendenCategory+Y(+ optionalSeries). Donut-/Tortendiagramme verwendenCategory+Y.Aggregation.Function: 0=Summe, 1=Mittelwert, 2=Anzahl, 3=Min, 4=Max, 5=AnzahlNichtNull, 6=Median, 7=StdAbw, 8=Varianz.drillFilterOtherVisuals: trueist erforderlich; das Weglassen beschädigt die Datei.
3. Seite registrieren
Report/definition/pages/pages.json:
{"$schema":"...pagesMetadata/1.1.0/schema.json","pageOrder":["<pageId1>","<pageId2>"],"activePageName":"<pageIdX>"}
Jeder Seitenordner hat eine page.json:
{"$schema":"...page/2.1.0/schema.json","name":"<pageId>","displayName":"Dashboard","displayOption":"FitToPage","height":1080,"width":1920}
4. JSON-Dateien schreiben – UTF-8 OHNE BOM
Jedes Mal, wenn Sie visual.json, page.json, report.json oder eine andere Datei unter Report/definition/ bearbeiten, schreiben Sie sie ohne BOM. Windows PowerShell 5.1 fügt standardmäßig eine BOM hinzu.
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($path, $json, $utf8NoBom)
Um eine BOM zu entfernen, die durch einen früheren Fehler geschrieben wurde:
$b = [System.IO.File]::ReadAllBytes($path)
if ($b.Length -ge 3 -and $b[0] -eq 0xEF -and $b[1] -eq 0xBB -and $b[2] -eq 0xBF) {
$text = [System.Text.Encoding]::UTF8.GetString($b, 3, $b.Length - 3)
[System.IO.File]::WriteAllText($path, $text, (New-Object System.Text.UTF8Encoding($false)))
}
5. Neu packen – die EINZIG zuverlässige Methode
Verwenden Sie NICHT Python zipfile. Verwenden Sie PowerShell mit .NET ZipArchive. Wichtige Details:
- Eintragsnamen verwenden Schrägstriche und haben KEIN führendes
/. - Berechnen Sie die Länge des Quellpräfixes aus
(Get-Item $src).FullName.TrimEnd('\').Length + 1(gleiche Groß-/Kleinschreibung/Langpfadform wieFullNamebei Get-ChildItem-Elementen).TrimStart('/')für jeden Eintragsnamen als Sicherheitsmaßnahme. - Jeder Texteintrag (einschließlich
[Content_Types].xml) wird mit einem UTF-8-no-BOMStreamWritergeschrieben.
Add-Type -AssemblyName System.IO.Compression
$src = "pbix_src"
$dst = "Test_patched.pbix"
if (Test-Path $dst) { Remove-Item $dst }
$srcFull = (Get-Item $src).FullName.TrimEnd('\')
$prefixLen = $srcFull.Length + 1
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
$fs = [System.IO.File]::Open($dst, 'Create')
$zip = New-Object System.IO.Compression.ZipArchive($fs, [System.IO.Compression.ZipArchiveMode]::Create)
Get-ChildItem $src -Recurse -File | ForEach-Object {
$full = $_.FullName
$rel = $full.Substring($prefixLen).Replace('\','/').TrimStart('/')
# SecurityBindings ENTFERNEN – PBI regeneriert sie beim Speichern
if ($rel -eq 'SecurityBindings') { return }
# Content_Types.xml bearbeiten, um den SecurityBindings-Verweis zu entfernen
if ($rel -eq '[Content_Types].xml') {
$xml = [System.IO.File]::ReadAllText($full) -replace '<Override[^/]*PartName="/SecurityBindings"[^/]*/>',''
$entry = $zip.CreateEntry($rel, [System.IO.Compression.CompressionLevel]::Optimal)
$entry.ExternalAttributes = 0
$sw = New-Object System.IO.StreamWriter($entry.Open(), $utf8NoBom)
$sw.Write($xml); $sw.Close()
return
}
# DataModel darf NICHT neu komprimiert werden
$level = if ($rel -eq 'DataModel') {
[System.IO.Compression.CompressionLevel]::NoCompression
} else {
[System.IO.Compression.CompressionLevel]::Optimal
}
$entry = $zip.CreateEntry($rel, $level)
$entry.ExternalAttributes = 0 # kritisch – Python setzt dies auf einen Wert ungleich Null, den PBI ablehnt
$es = $entry.Open()
$sfs = [System.IO.File]::OpenRead($full)
$sfs.CopyTo($es)
$sfs.Close(); $es.Close()
}
$zip.Dispose(); $fs.Close()
6. Vor dem Öffnen validieren
Vier Prüfungen, in dieser Reihenfolge:
Add-Type -AssemblyName System.IO.Compression.FileSystem
$z = [System.IO.Compression.ZipFile]::OpenRead("Test_patched.pbix")
# a) Erforderliche Teile vorhanden, SecurityBindings abwesend
$z.Entries | Where-Object { $_.FullName -match '^(Version|DataModel|Metadata|Settings|SecurityBindings|\[Content_Types\]\.xml)$' } |
Select-Object FullName, Length
# b) Kein Eintrag hat ein führendes '/'
$z.Entries | Where-Object { $_.FullName.StartsWith('/') } | ForEach-Object { "SCHLECHT: $($_.FullName)" }
# c) Keine BOM für Text-Metadaten oder Report/definition JSON
foreach ($e in $z.Entries) {
if ($e.FullName -match '\.(json|xml)$|^Version$|^Metadata$|^Settings$') {
$s = $e.Open(); $ms = New-Object System.IO.MemoryStream; $s.CopyTo($ms); $s.Close()
$b = $ms.ToArray()
if ($b.Length -ge 3 -and $b[0] -eq 0xEF -and $b[1] -eq 0xBB -and $b[2] -eq 0xBF) {
Write-Host "BOM gefunden: $($e.FullName)"
}
}
}
# d) Jedes Report/definition JSON wird geparst
foreach ($e in $z.Entries) {
if ($e.FullName -like 'Report/definition/*.json' -or $e.FullName -like 'Report/definition/**/*.json') {
$s = $e.Open(); $sr = New-Object System.IO.StreamReader($s)
$txt = $sr.ReadToEnd(); $sr.Close()
try { $null = $txt | ConvertFrom-Json } catch { "UNGÜLTIGES JSON: $($e.FullName) – $($_.Exception.Message)" }
}
}
$z.Dispose()
Erwartet: DataModel, Version, Metadata, Settings, [Content_Types].xml vorhanden; SecurityBindings abwesend; keine führenden Schrägstriche; keine BOMs; alle JSONs parsen.
Verbindung zu einer laufenden Power BI Desktop-Sitzung (optional – zur Datenermittlung)
Wenn PBI Desktop bereits ausgeführt wird, stellt es einen Analysis Services-Endpunkt auf localhost bereit:
# Den Port ermitteln
Get-Process msmdsrv | ForEach-Object {
$port = Get-NetTCPConnection -OwningProcess $_.Id -State Listen |
Where-Object { $_.LocalAddress -eq '127.0.0.1' } |
Select-Object -First 1 -ExpandProperty LocalPort
Write-Host "Analysis Services-Port: $port"
}
Dann über ADOMD verbinden (Power BI Desktop installieren; die DLL wird damit ausgeliefert):
# pip install pyadomd pythonnet
import clr
clr.AddReference(r"C:\Program Files\Microsoft Power BI Desktop\bin\Microsoft.PowerBI.AdomdClient.dll")
from Microsoft.AnalysisServices.AdomdClient import AdomdConnection # echter Namespace, nicht der Dateiname
conn = AdomdConnection(f"Data Source=localhost:{port}")
conn.Open()
# DAX ausführen: EVALUATE SUMMARIZECOLUMNS(financials[Segment], "Sales", SUM(financials[Sales]))
Verwenden Sie dies nur, um Schema und Vorschauzahlen zu lesen; endgültige Visuals werden als JSON-Dateien geschrieben und oben neu verpackt.
Visuals gestalten – die Eigenschaftsnamen, die tatsächlich funktionieren
Die Bezeichnungen im Formatierungsbereich von Power BI Desktop stimmen NICHT mit den JSON-Eigenschaftsnamen überein. Wenn Sie raten, verwirft PBI die falschen Eigenschaften stillschweigend beim nächsten Speichern – die Datei wird zwar geöffnet, aber Ihr Stil verschwindet. Erstellen Sie echte Änderungen immer einmal in PBI, speichern Sie, extrahieren Sie und kopieren Sie die Eigenschaftsnamen aus dem gespeicherten JSON.
Wo die Gestaltung in visual.json lebt
visual.visualContainerObjects– der äußere Rahmen der Visual-Kachel (Hintergrund, Rahmen, Schlagschatten, Titelleiste). Gilt für jeden Visual-Typ.visual.objects– das Innere des Visuals (Plotbereich, Achsen, Legende, Datenpunkte, Kartenwert, Karteninnenfüllung). Eigenschaftsnamen hier sind pro Visual-Typ und müssen mit dem Schema des DesignsvisualStyles[visualType]übereinstimmen.
Containerrahmen (funktioniert für jedes Visual)
"visualContainerObjects": {
"background": [{"properties":{
"color":{"solid":{"color":{"expr":{"Literal":{"Value":"'#EEF2FF'"}}}}}},
"transparency":{"expr":{"Literal":{"Value":"0D"}}}
}}],
"border": [{"properties":{
"show":{"expr":{"Literal":{"Value":"true"}}},
"color":{"solid":{"color":{"expr":{"Literal":{"Value":"'#E2E8F0'"}}}}}},
"radius":{"expr":{"Literal":{"Value":"12D"}}}
}}],
"dropShadow": [{"properties":{
"show":{"expr":{"Literal":{"Value":"true"}}},
"color":{"solid":{"color":{"expr":{"Literal":{"Value":"'#0F172A'"}}}}}},
"transparency":{"expr":{"Literal":{"Value":"85D"}}},
"position":{"expr":{"Literal":{"Value":"'Outer'"}}},
"preset":{"expr":{"Literal":{"Value":"'BottomRight'"}}}
}}]
}
cardVisual (KPI-Kachel) – Fallstricke
Die große Zahl ist NICHT callout.color und NICHT value.color. PBI verwirft beide stillschweigend.
- Schriftfarbe der großen Zahl:
value.fontColor(nichtcolor). - Schriftart/-größe/-stärke der großen Zahl:
value.fontFamily,value.fontSize,value.bold. - Kategoriebezeichnung unter der Zahl:
label.fontColor,label.fontFamily,label.fontSize. - Getönter Hintergrund hinter der Zahl:
cardCalloutArea.backgroundFillColor, mit einemselector, der die Metadaten der Measureprojektion benennt – z. B."selector":{"metadata":"Sum(financials.Profit)"}. Lesen Sie diese Zeichenfolge ausvisual.query.queryState.Data.projections[0].queryRef.
"objects": {
"value": [{"properties":{
"fontColor":{"solid":{"color":{"expr":{"Literal":{"Value":"'#4F46E5'"}}}}}},
"fontFamily":{"expr":{"Literal":{"Value":"'Segoe UI Semibold'"}}},
"fontSize":{"expr":{"Literal":{"Value":"40D"}}},
"bold":{"expr":{"Literal":{"Value":"true"}}}
}}],
"label": [{"properties":{
"fontColor":{"solid":{"color":{"expr":{"Literal":{"Value":"'#64748B'"}}}}}}
}}],
"cardCalloutArea": [{
"properties": {
"show":{"expr":{"Literal":{"Value":"true"}}},
"backgroundFillColor":{"solid":{"color":{"expr":{"Literal":{"Value":"'#E0E7FF'"}}}}}}
},
"selector":{"metadata":"Sum(financials.Sales)"}
}]
}
Balken / Säule / Linie / Donut – Datenfarben
dataPoint.defaultColor– die Fallback-Farbe.dataPoint.fill– die Füllfarbe.dataPoint.strokeColor– die Rahmenfarbe.