XmlStackBuilder.Core — Integrace do .NET 9 / SluzbyPM.WebApi

Obsah

  1. Co je XmlStackBuilder.Core.Net
  2. Zprovoznění — reference z projektu
  3. Registrace služeb (DI)
  4. Základní použití — tři kroky
  5. Praktické příklady pro SluzbyPM
  6. Nahrazení stávajícího reporting stacku
  7. JSON šablony — kde je vzít
  8. API reference — klíčové třídy
  9. Omezení oproti .NET 4.8 verzi
  10. Troubleshooting
  11. xsb CLI — editace JSON sablon s live preview

1. Co je XmlStackBuilder.Core.Net

.NET 9 varianta knihovny XmlStackBuilder.Core, která umožňuje:

Tři typy rendererů:

Renderer Použití Výstupní formáty
table Přehledy, výkazy, saldokonta, inventury HTML, PDF, XLSX
document Faktury, dodací listy, objednávky, dopisy HTML, PDF
label Štítky, obálky, čárové kódy HTML, PDF

Klíčová výhoda: JSON šablony jsou deklarativní a uživatelsky editovatelné — lze měnit layout sestav bez rekompilace.


2. Zprovoznění — reference z projektu

2.1 Přidat ProjectReference

V SluzbyPM.Application.csproj:

<ProjectReference Include="..\..\XmlStackBuilder\XmlStackBuilder.Core.Net\XmlStackBuilder.Core.Net.csproj" />

Pozn.: Relativní cesta závisí na umístění repozitářů. Výše uvedená odpovídá struktuře:

C:\Users\Krejčí\source\repos\
  ├── XmlStackBuilder\
  │   ├── XmlStackBuilder.Core.Net\    ← .NET 9 projekt
  │   └── XmlStackBuilder.Core\        ← .NET 4.8 (sdílený zdrojový kód)
  └── SluzbyPM.WebApi\
      └── SluzbyPM.Application\        ← sem přidáváme referenci

2.2 Ověřit build

dotnet build SluzbyPM.sln

Žádné další NuGet balíčky ani DLL nejsou potřeba — všechny závislosti (PDFsharp, ClosedXML, ZXing.Net, Markdig) se natáhnou tranzitivně přes ProjectReference.


3. Registrace služeb (DI)

XmlStackBuilder je stateless — nepoužívá DI kontejner. Všechny renderery jsou statické třídy:

// Žádná registrace do DI není potřeba!
// Renderery se volají přímo:
string html = HtmlTableReportRenderer.RenderWithMetadata(xml, json);
PdfTableReportRenderer.RenderToPdf(xml, json, outputPath, out error);
XlsxTableReportRenderer.RenderToXlsx(xml, json, outputPath, out error);

Pokud chcete wrapper service (doporučeno pro SluzbyPM):

// SluzbyPM.Application/Reporting/Infrastructure/XmlStackReportService.cs

using XmlStackBuilder.Core;

namespace SluzbyPM.Application.Reporting.Infrastructure;

public class XmlStackReportService
{
    private readonly string _templateDir =
        Path.Combine(AppContext.BaseDirectory, "Reporting", "Templates");

    /// <summary>
    /// Vytvoří nový builder s kořenovým elementem.
    /// </summary>
    public XmlStackBuilder.Core.XmlStackBuilder CreateBuilder()
    {
        var builder = new XmlStackBuilder.Core.XmlStackBuilder();
        builder.CreateRoot("root");
        return builder;
    }

    /// <summary>
    /// Renderuje do byte[] — pro web API (PDF, XLSX).
    /// Temp soubor se vytvoří a ihned smaže.
    /// </summary>
    public byte[] RenderToBytes(XmlStackBuilder.Core.XmlStackBuilder builder,
        string templateName, string format = "pdf")
    {
        return builder.RenderToBytes(format, Path.Combine(_templateDir, templateName));
    }

    /// <summary>
    /// Renderuje HTML — vrací string.
    /// </summary>
    public string RenderToHtml(XmlStackBuilder.Core.XmlStackBuilder builder, string templateName)
    {
        return System.Text.Encoding.UTF8.GetString(
            builder.RenderToBytes("html", Path.Combine(_templateDir, templateName)));
    }
}

Registrace:

// V ApplicationServiceCollectionExtension.cs, metoda AddReporting():
services.AddScoped<XmlStackReportService>();

Použití:

// Ve service nebo endpointu
var builder = _reportService.CreateBuilder();
builder.Push("header");
builder.AddAttribute("nadpis", "Přehled");
builder.Pop();
// ... naplnit data ...
byte[] pdf = _reportService.RenderToBytes(builder, "PrehledPozadavku.json");
// nebo HTML:
string html = _reportService.RenderToHtml(builder, "PrehledPozadavku.json");

4. Základní použití

Doporučený přístup — instance XmlStackBuilder + Render()

Stejný vzor jako ve FoxPro — XML se staví uvnitř builderu a renderuje přímo, bez nutnosti získávat XML string:

using XmlStackBuilder.Core;

var builder = new XmlStackBuilder.Core.XmlStackBuilder();
builder.CreateRoot("root");

// Hlavička — anonymní objekt → atributy
builder.PushObject("parametr", new { firma = "SluzbyPM s.r.o.", ico = "12345678" });

// Data — LINQ výsledek → elementy s atributy
builder.PushCollection("polozky", items.Select(x => new {
    x.Nazev, x.Cena, x.Pocet
}));

// Renderovat přímo — formát z přípony, typ šablony auto-detekce z JSON
builder.Render("output.pdf", "sablona.json");   // PDF
builder.Render("output.html", "sablona.json");  // HTML
builder.Render("output.xlsx", "sablona.json");  // XLSX

Poznámka: Render() auto-detekuje typ šablony (table/document/label) z obsahu JSON a výstupní formát z přípony souboru. JSON šablona může být cesta k souboru nebo JSON string.

Naplnění dat z EF Core

var builder = new XmlStackBuilder.Core.XmlStackBuilder();
builder.CreateRoot("root");

// Hlavička — anonymní objekt, všechny properties → XML atributy
builder.PushObject("header", new {
    nadpis = "Přehled požadavků",
    filtr = $"Stav: {stavNazev}"
});

// Data z DB — LINQ Select → anonymní objekty → XML řádky
var pozadavky = await _db.Pozadavky
    .Include(p => p.StavEntity)
    .Where(p => p.IdStavy >= 3)
    .OrderBy(p => p.IdPozadavky)
    .Select(p => new {
        id = p.IdPozadavky,
        nazev = p.Nazev ?? "",
        stav = p.StavEntity != null ? p.StavEntity.Nazev : "",
        planCena = p.PlanCena ?? 0m,
        datum = p.Vlozeno
    })
    .ToListAsync();

builder.PushCollection("pozadavky", pozadavky);

// Renderovat
builder.Render(tempPdfPath, "PrehledPozadavku.json");

Tip: PushObject převede všechny public properties na XML atributy. PushCollection vytvoří kontejnerový element a pro každý objekt v kolekci element <row> s atributy. Čísla, datumy, bool se formátují automaticky (InvariantCulture, ISO datum, true/false). Null hodnoty se přeskočí (nevytvoří se prázdný atribut).

Alternativa — statické metody (pokud XML máte z jiného zdroje)

Pokud XML již máte jako string (např. z externího systému), lze volat renderery přímo:

// HTML
string html = HtmlTableReportRenderer.RenderWithMetadata(xmlString, jsonTemplate);

// PDF
PdfTableReportRenderer.RenderToPdf(xmlString, jsonTemplate, "output.pdf", out string error);

// XLSX
XlsxTableReportRenderer.RenderToXlsx(xmlString, jsonTemplate, "output.xlsx", out string error);

Alternativa — automatická serializace .NET objektu

Pro jednorázové reporty bez složité XML struktury:

var data = new
{
    parametr = new { firma = "SluzbyPM s.r.o.", ico = "12345678" },
    polozky = new[]
    {
        new { nazev = "Položka 1", cena = 100.50m, pocet = 3 },
        new { nazev = "Položka 2", cena = 250.00m, pocet = 1 },
    }
};

string xml = XmlBuilderFromClass.BuildFromObject(data).ToString();
HtmlTableReportRenderer.RenderWithMetadata(xml, jsonTemplate);

JSON šablona

{
  "renderer": "table",
  "report": {
    "title": "Přehled požadavků",
    "culture": "cs-CZ",
    "dataFontPt": 8,
    "pageLayout": "paged",
    "orientation": "landscape"
  },
  "header": {
    "company": "SluzbyPM s.r.o."
  },
  "pageHeader": {
    "left": "{company}",
    "center": "{title}",
    "right": "Strana {page} z {pages}"
  },
  "sections": [{
    "element": "pozadavky",
    "columns": {
      "id":       { "label": "#",         "align": "right",  "width": "10mm" },
      "refNo":    { "label": "Ref. číslo", "width": "35mm" },
      "nazev":    { "label": "Název",      "width": "60mm" },
      "stav":     { "label": "Stav",       "width": "20mm" },
      "zadal":    { "label": "Zadal",      "width": "30mm" },
      "planCena": { "label": "Plán. cena", "align": "right", "format": "N2", "width": "25mm" },
      "schvCena": { "label": "Schv. cena", "align": "right", "format": "N2", "width": "25mm" },
      "datum":    { "label": "Datum",      "align": "center", "format": "dd.MM.yyyy", "width": "22mm" }
    },
    "sumExpressions": [
      { "field": "planCena", "expr": "SUM(planCena)", "format": "N2" },
      { "field": "schvCena", "expr": "SUM(schvCena)", "format": "N2" }
    ],
    "sumLabel": "Celkem"
  }]
}

Krok 3: Renderovat

// HTML
string html = HtmlTableReportRenderer.RenderWithMetadata(xml, jsonTemplate);

// PDF
PdfTableReportRenderer.RenderToPdf(xml, jsonTemplate, "output.pdf", out string error);

// XLSX
XlsxTableReportRenderer.RenderToXlsx(xml, jsonTemplate, "output.xlsx", out string error);

5. Praktické příklady pro SluzbyPM

5.1 Přehled požadavků jako PDF endpoint

// SluzbyPM.Api/Endpoints/Pozadavky/GetPozadavkyReportEndpoint.cs

public class GetPozadavkyReportEndpoint : IEndpoint
{
    public void MapEndpoint(IEndpointRouteBuilder app)
    {
        app.MapGet("/api/pozadavky/report/{format}",
            async (string format, [FromQuery] int? idStavy,
                   MainDbContext db, CancellationToken ct) =>
        {
            // 1. Načíst data
            var query = db.Pozadavky
                .Include(p => p.StavEntity)
                .Include(p => p.VlozilEntity)
                .AsQueryable();

            if (idStavy.HasValue)
                query = query.Where(p => p.IdStavy == idStavy.Value);

            var rows = await query
                .OrderByDescending(p => p.IdPozadavky)
                .Select(p => new
                {
                    id = p.IdPozadavky,
                    refNo = p.refNo ?? "",
                    nazev = p.Nazev ?? "",
                    stav = p.StavEntity != null ? p.StavEntity.Nazev : "",
                    zadal = p.VlozilEntity != null ? p.VlozilEntity.DisplayName : "",
                    planCena = p.PlanCena,
                    schvCena = p.SchvCena,
                    datum = p.Vlozeno.ToString("yyyy-MM-dd")
                })
                .ToListAsync(ct);

            // 2. Připravit XML přes builder
            var builder = new XmlStackBuilder.Core.XmlStackBuilder();
            builder.CreateRoot("root");
            builder.PushCollection("pozadavky", rows);

            // 3. Renderovat — RenderToBytes vrací byte[], žádné temp soubory
            string templatePath = Path.Combine(
                AppContext.BaseDirectory, "Reporting", "Templates", "PrehledPozadavku.json");

            return format.ToLower() switch
            {
                "pdf" => Results.File(
                    builder.RenderToBytes("pdf", templatePath),
                    "application/pdf", "pozadavky.pdf"),
                "xlsx" => Results.File(
                    builder.RenderToBytes("xlsx", templatePath),
                    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
                    "pozadavky.xlsx"),
                "html" => Results.Content(
                    System.Text.Encoding.UTF8.GetString(builder.RenderToBytes("html", templatePath)),
                    "text/html; charset=utf-8"),
                _ => Results.BadRequest($"Nepodporovaný formát: {format}")
            };
        })
        .RequireAuthorization();
    }
}

5.2 Schvalovací protokol jako PDF (náhrada QuestPDF)

Stávající ApprovalPdfDocument.cs (QuestPDF, ~150 řádků C#) lze nahradit JSON šablonou:

// Stávající kód v PozadavkyReportingService:
//   var doc = new ApprovalPdfDocument(pozadavek);
//   await _reportingService.CreatePdfUsingQuestPdf(doc, fileName, ...);

// Nový kód:
public async Task<TaskResult<Foto>> MakeApprovalReportAsync(
    Pozadavky pozadavek, string userId, CancellationToken ct)
{
    // 1. Připravit XML data
    var builder = new XmlStackBuilder.Core.XmlStackBuilder();
    builder.CreateRoot("root");

    builder.PushObject("parametr", new { firma = "SluzbyPM s.r.o." });

    builder.PushObject("header", new {
        id = pozadavek.IdPozadavky,
        refNo = pozadavek.refNo ?? "",
        nazev = pozadavek.Nazev ?? "",
        stav = pozadavek.StavEntity?.Nazev ?? "",
        barva = pozadavek.StavEntity?.Barva ?? "#999",
        popis = pozadavek.Popis ?? "",
        planCena = pozadavek.PlanCena ?? 0m,
        schvCena = pozadavek.SchvCena ?? 0m,
        zadal = pozadavek.VlozilEntity?.DisplayName ?? "",
        resi = pozadavek.ResiEntity?.DisplayName ?? "",
        schvalil = pozadavek.RozhodlEntity?.DisplayName ?? "",
        datumVlozeni = pozadavek.Vlozeno,
        datumSchvaleni = pozadavek.DatumRozhodnuti
    });

    // 2. Renderovat PDF do byte[]
    string templatePath = Path.Combine(AppContext.BaseDirectory, "Reporting", "Templates", "SchvaleniPozadavku.json");
    byte[] pdfBytes = builder.RenderToBytes("pdf", templatePath);

    // 3. Upload (stávající logika)
    string fileName = $"Potvrzeni_o_schvaleni_{pozadavek.IdPozadavky}";
    Foto foto = Foto.Create("POZADAVKY_PDF", pozadavek.IdPozadavky,
                            fileName, "pdf", "application/pdf", userId);
    await _db.Foto.AddAsync(foto, ct);
    await _db.SaveChangesAsync(ct);

    string fotoFileName = foto.IdFoto.ToString("D10") + ".pdf";
    bool uploaded = _docApiClient.Upload(pdfBytes, fotoFileName, "");
    if (!uploaded)
    {
        _db.Foto.Remove(foto);
        await _db.SaveChangesAsync(ct);
        return TaskResult<Foto>.Failure("Upload selhal");
    }

    return TaskResult<Foto>.Success(foto);
}

A odpovídající JSON šablona SchvaleniPozadavku.json:

{
  "renderer": "document",
  "document": {
    "title": "Schválení požadavku #{header.id}",
    "pageMarginMm": 20,
    "fontSizePt": 10
  },
  "blocks": [
    {
      "type": "text-block",
      "text": "Schválení požadavku #{header.id}",
      "style": "title"
    },
    {
      "type": "text-block",
      "template": "Ref. číslo: {header.refNo}",
      "style": "bold"
    },
    { "type": "separator", "style": "thick" },
    {
      "type": "info-grid",
      "element": "header",
      "columns": 2,
      "items": [
        { "label": "Název",           "field": "nazev" },
        { "label": "Stav",            "field": "stav", "style": "bold" },
        { "label": "Plánovaná cena",  "field": "planCena", "format": "N2" },
        { "label": "Schválená cena",  "field": "schvCena", "format": "N2" },
        { "label": "Zadal",           "field": "zadal" },
        { "label": "Řeší",            "field": "resi" },
        { "label": "Datum zadání",    "field": "datumVlozeni" },
        { "label": "Schválil",        "field": "schvalil", "style": "bold" },
        { "label": "Datum schválení", "field": "datumSchvaleni", "style": "bold" }
      ]
    },
    { "type": "separator" },
    {
      "type": "text-block",
      "element": "header",
      "field": "popis",
      "label": "Popis"
    },
    { "type": "spacer", "heightMm": 20 },
    {
      "type": "signature",
      "signatureLeft": "Schválil: {header.schvalil}",
      "signatureRight": "Datum: {header.datumSchvaleni}"
    }
  ]
}

5.3 Integrace s Hangfire (background job)

// ApprovalReportJob.cs — stávající pattern zůstává
public class ApprovalReportJob
{
    private readonly IPozadavkyReportingService _reportingService;

    public ApprovalReportJob(IPozadavkyReportingService reportingService)
    {
        _reportingService = reportingService;
    }

    public async Task ExecuteAsync(int pozadavekId, string userId, CancellationToken ct)
    {
        // Stávající logika — jen volání nové implementace MakeApprovalReportAsync
        var result = await _reportingService.MakeApprovalReportAsync(pozadavekId, userId, ct);
        if (!result.IsSuccess)
            throw new Exception($"Report generation failed: {result.ErrorMessage}");
    }
}

6. Nahrazení stávajícího reporting stacku

Stávající stack v SluzbyPM

Komponenta NuGet Účel
QuestPDF 2025.7.4 Generování PDF fluent API (ApprovalPdfDocument)
PuppeteerSharp 19.0.0 HTML → PDF přes headless Chrome
RazorEngineCore 2024.4.1 Kompilace Razor šablon (.cshtml)

Co XmlStackBuilder nahrazuje

Stávající XmlStackBuilder Poznámka
ApprovalPdfDocument.cs (QuestPDF, ~150 řádků C#) SchvaleniPozadavku.json (~50 řádků JSON) JSON místo C# — editovatelné bez rekompilace
Approval.cshtml (Razor + CSS) nepotřeba HTML renderer generuje kompletní HTML
PuppeteerService.cs (headless Chrome) PdfDocumentRenderer / PdfTableReportRenderer Nativní PDF přes PdfSharp — bez externího procesu
RazorRenderer.cs nepotřeba Šablony v JSON, ne Razor

Doporučená strategie migrace

  1. Fáze 1 — koexistence (doporučeno na začátek):

  2. Fáze 2 — migrace approval reportu:

  3. Fáze 3 — odstranění starých závislostí (volitelné):

Co XmlStackBuilder nenahrazuje


7. JSON šablony — kde je vzít

7.1 Uložení šablon v projektu

Doporučená struktura:

SluzbyPM.Application/
  Reporting/
    Templates/
      PrehledPozadavku.json        ← tabulková sestava
      SchvaleniPozadavku.json      ← dokumentový report
      PrehledZarizeni.json

V .csproj zajistit kopírování do výstupní složky:

<ItemGroup>
  <Content Include="Reporting\Templates\*.json">
    <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
  </Content>
</ItemGroup>

Načítání:

string templateDir = Path.Combine(AppContext.BaseDirectory, "Reporting", "Templates");
string json = File.ReadAllText(Path.Combine(templateDir, "PrehledPozadavku.json"), Encoding.UTF8);

7.2 Automatické generování šablony z dat

XmlStackBuilder umí vygenerovat kostru JSON šablony z XML dat:

// Z cursor schema (definice sloupců)
string json = CursorSchemaGenerator.GenerateColumnsFromSchema(
    "Id I, RefNo C(20), Nazev C(100), Stav C(30), PlanCena N(12,2), Datum D",
    "dataFontPt:8,element:pozadavky,title:Přehled požadavků");

// Z XML dat (pro dokumentové šablony)
string docJson = DocumentTemplateGenerator.GenerateFromXml(xml, "type:faktura,title:Faktura");

7.3 Editace šablon ve VS Code

JSON šablony lze editovat s IntelliSense. Schémata jsou v XmlStackBuilder/schemas/:

Ve VS Code nastavit v settings.json:

{
  "json.schemas": [
    {
      "fileMatch": ["**/Templates/*Report*.json"],
      "url": "file:///C:/Users/Krejčí/source/repos/XmlStackBuilder/schemas/table-report.schema.json"
    }
  ]
}

8. API reference — klíčové třídy

Všechny třídy jsou v namespace XmlStackBuilder.Core.

Renderery (statické třídy)

// === TABULKOVÉ SESTAVY ===

// HTML
string html = HtmlTableReportRenderer.RenderWithMetadata(string xmlData, string jsonMetadata);

// PDF
bool ok = PdfTableReportRenderer.RenderToPdf(
    string xmlData, string jsonMetadata, string outputPath, out string errorMessage);

// XLSX
bool ok = XlsxTableReportRenderer.RenderToXlsx(
    string xmlData, string jsonMetadata, string outputPath, out string errorMessage);

// === DOKUMENTY (faktury, dopisy) ===

// HTML
string html = HtmlDocumentRenderer.Render(string xmlData, string jsonMetadata);

// PDF
bool ok = PdfDocumentRenderer.RenderToPdf(
    string xmlData, string jsonMetadata, string outputPath, out string errorMessage);

// === ŠTÍTKY ===

// PDF
bool ok = PdfLabelRenderer.RenderToPdf(
    string xmlData, string jsonMetadata, string outputPath, out string errorMessage);

// HTML
string html = HtmlLabelRenderer.Render(string xmlData, string jsonMetadata);

XML stavba — doporučené metody

var builder = new XmlStackBuilder();
builder.CreateRoot("root");

// --- PushObject: anonymní objekt nebo entita → element s atributy ---
builder.PushObject("header", new { nadpis = "Přehled", datum = DateTime.Now });
// Výsledek: <header nadpis="Přehled" datum="2026-04-02" />

builder.PushObject("header", pozadavekEntity);
// Výsledek: <header IdPozadavky="1" Nazev="..." PlanCena="15000" ... />
// Navigační properties → child elementy (rekurzivně)

// --- PushCollection: LINQ výsledek → kontejner + řádky ---
var data = await db.Pozadavky
    .Select(p => new { p.IdPozadavky, p.Nazev, stav = p.StavEntity.Nazev })
    .ToListAsync();
builder.PushCollection("pozadavky", data);
// Výsledek: <pozadavky><row IdPozadavky="1" Nazev="..." stav="Nový" />...</pozadavky>

builder.PushCollection("adresy", adresy, "adresa");  // vlastní název child elementu
// Výsledek: <adresy><adresa ... /><adresa ... /></adresy>

// --- Ruční stavba (pro speciální případy) ---
builder.Push("element");
builder.AddAttribute("field", "value");
builder.Pop();

Pravidla formátování v PushObject/PushCollection:

Sjednocené API (instance-based — doporučené)

var builder = new XmlStackBuilder();
builder.CreateRoot("root");
builder.PushObject("parametr", new { firma = "SluzbyPM s.r.o." });
builder.PushCollection("polozky", linqResult);

// Auto-detekce typu šablony + výstupního formátu z přípony
builder.Render("output.pdf", jsonOrFile);    // jsonOrFile = cesta k souboru nebo JSON string
builder.Render("output.html", jsonOrFile);
builder.Render("output.xlsx", jsonOrFile);

// Pokud XML pochází z externího zdroje (string nebo soubor)
builder.RenderFromFiles("output.pdf", "sablona.json", "data.xml");

Toto je primární API — funguje identicky jako ve FoxPro. XML se staví uvnitř builderu, není třeba ho extrahovat.


9. Omezení oproti .NET 4.8 verzi

.NET 9 varianta nepodporuje (kvůli absenci WinForms/GDI):

Funkce Důvod Alternativa v .NET 9
Tisk na tiskárnu (PRINTER:) System.Drawing.Printing Generovat PDF, tisknout na klientovi
Progress dialog (ShowProgress) WinForms Nepotřeba na serveru
HTML → PDF přes WebView2 (PdfFromHtml) Westwind.WebView2 PdfDocumentRenderer (nativní)
Dávkový tisk s dialogem WinForms dialog Nepotřeba na serveru

Vše ostatní funguje identicky:


10. Troubleshooting

System.Drawing.Common na Linuxu

Pokud by se SluzbyPM nasazoval na Linux (ne Windows), System.Drawing.Common nefunguje. Řešení:

PdfSharp font resolver

PdfSharp 6.x na .NET 9 potřebuje přístup k systémovým fontům. Na Windows to funguje automaticky. Pokud by PDF renderování padalo na chybějící font:

// Nastavit před prvním renderováním (např. v Program.cs)
PdfSharp.Fonts.GlobalFontSettings.FontResolver = new PdfSharp.Fonts.PlatformFontResolver();

Velké sestavy — paměť

Pro sestavy s 10 000+ řádky zvažte:

Encoding

JSON šablony musí být v UTF-8. Pokud se české znaky zobrazují špatně, ověřte kódování souboru. XML data z XmlBuilderFromClass.BuildFromObject() a builder.ToXmlString() jsou vždy UTF-8.


11. xsb CLI — editace JSON sablon s live preview

11.1 O co jde

Ve FoxPro prostředí stačí zavolat OpenReportEditorWithSchemas(jsonPath, xmlPath) — VS Code se otevře s IntelliSense a live preview. V SluzbyPM (.NET 9 web API) ale XML data vznikají z EF Core databáze, ne ze souboru.

CLI nástroj xsb.exe řeší tento problém:

  1. Stáhne XML data z běžícího API (endpoint /dev/reports/{name}/xml)
  2. Z XML vygeneruje dynamické JSON schéma s reálnými názvy polí z databáze
  3. Vytvoří VS Code workspace s vazbou na schéma
  4. Otevře VS Code s IntelliSense (nabízí idPozadavky, predmet, stav...) a prohlížeč s live preview

11.2 Architektura

  Programátor                     xsb.exe                        SluzbyPM API (Development)
  ──────────                     ────────                        ──────────────────────────
  xsb edit PrehledPozadavku  →   HTTP GET                   →   /dev/reports/PrehledPozadavku/xml
                                  ← XML data                ←   ReportXmlService.BuildXmlAsync()
                                 Vygeneruje:
                                   - dynamic-schema.json (IntelliSense)
                                   - editor.code-workspace
                                   - preview.html
                                 Otevře VS Code + prohlížeč
  Edituje JSON, uloží        →   RunOnSave: xsb preview     →   Re-render HTML
                                 Prohlížeč se obnoví (meta refresh 2s)

11.3 Soubory v repozitáři SluzbyPM

Dva konfigurační soubory leží vedle SluzbyPM.sln:

C:\Users\Krejčí\source\repos\SluzbyPM.WebApi\
    SluzbyPM.sln
    xsb-config.json                    ← konfigurace CLI
    xsb-reports.json                   ← registrace reportů
    .xsb\                             ← dočasné soubory (v .gitignore)
    SluzbyPM.Application\
        Reporting\
            Services\
                ReportXmlService.cs    ← C# Build metody pro XML data
            Templates\
                PrehledPozadavku.json  ← JSON šablona tabulkové sestavy
                SchvaleniPozadavku.json← JSON šablona dokumentu
    SluzbyPM.Api\
        Endpoints\
            Testing\
                GetReportXmlEndpoint.cs← Dev endpoint (jen v Development)

CLI exe je v druhém repozitáři:

C:\Users\Krejčí\source\repos\XmlStackBuilder\
    XmlStackBuilder.DevCli\
        bin\Debug\net9.0\xsb.exe       ← CLI nástroj
    schemas\                            ← JSON schémata (bázová)

11.4 xsb-config.json — konfigurace CLI

{
    // URL API v development režimu
    "apiUrl": "http://localhost:5144",

    // Složka s JSON šablonami (relativní k tomuto souboru)
    "templatesDir": "SluzbyPM.Application/Reporting/Templates",

    // Složka se základními JSON schématy (nepovinné — schémata jsou embedded v xsb.exe)
    "schemasDir": "../XmlStackBuilder/schemas",

    // Složka pro dočasné soubory (přidat do .gitignore)
    "devDir": ".xsb",

    // Cesta k CLI exe pro RunOnSave (null = použije samo sebe)
    "cliPath": null
}
Klíč Hodnota v SluzbyPM Popis
apiUrl http://localhost:5144 Port z launchSettings.json (profil http)
templatesDir SluzbyPM.Application/Reporting/Templates Kde leží JSON šablony (Git-tracked)
schemasDir ../XmlStackBuilder/schemas Bázová schémata ve vedlejším repozitáři
devDir .xsb Dočasný adresář — sample.xml, dynamic-schema.json, preview.html
cliPath null CLI použije svou vlastní cestu pro RunOnSave příkaz

Poznámka k portu: SluzbyPM API běží na portu 5144 (viz SluzbyPM.Api/Properties/launchSettings.json). Pokud se změní, je třeba aktualizovat apiUrl.

11.5 xsb-reports.json — registrace reportů

Každý report, který chceme editovat přes CLI, musí být zaregistrován:

{
    "PrehledPozadavku": {
        "template": "PrehledPozadavku.json",
        "renderer": "table",
        "description": "Přehled požadavků s filtrem podle stavu",
        "params": {
            "idStavy": { "type": "int", "optional": true, "description": "ID stavu" },
            "datumOd": { "type": "date", "optional": true, "description": "Datum od" },
            "datumDo": { "type": "date", "optional": true, "description": "Datum do" }
        }
    },
    "SchvaleniPozadavku": {
        "template": "SchvaleniPozadavku.json",
        "renderer": "document",
        "description": "Protokol o schválení požadavku",
        "params": {
            "idPozadavky": { "type": "int", "optional": false, "description": "ID požadavku" }
        }
    }
}
Vlastnost Popis
klíč ("PrehledPozadavku") Název reportu — musí odpovídat case v ReportXmlService.BuildXmlAsync()
template Název JSON šablony v templatesDir
renderer "table" nebo "document" — určuje bázové schéma pro IntelliSense
params Parametry pro API endpoint (předávají se jako query string)
params.*.optional true = parametr lze vynechat, false = povinný

Propojení: Když zavoláte xsb edit PrehledPozadavku idStavy=3, CLI:

  1. Najde záznam "PrehledPozadavku" v xsb-reports.json
  2. Zavolá GET http://localhost:5144/dev/reports/PrehledPozadavku/xml?idStavy=3
  3. API dispatchne do ReportXmlService.BuildXmlAsync("PrehledPozadavku", {"idStavy":"3"}, ct)
  4. Build metoda vrátí XML s daty filtrovanými na stav 3
  5. CLI z XML vygeneruje dynamické schéma → VS Code nabízí reálná pole

11.6 Co je potřeba na straně API

Dvě věci (již implementováno):

1. ReportXmlService — centrální dispatch + Build metody:

// SluzbyPM.Application/Reporting/Services/ReportXmlService.cs

public class ReportXmlService
{
    private readonly MainDbContext _db;

    public ReportXmlService(MainDbContext db) { _db = db; }

    public async Task<string> BuildXmlAsync(
        string reportName, Dictionary<string, string> parameters, CancellationToken ct)
    {
        return reportName switch
        {
            "PrehledPozadavku" => await BuildPrehledPozadavku(parameters, ct),
            "SchvaleniPozadavku" => await BuildSchvaleniPozadavku(parameters, ct),
            _ => throw new ArgumentException(
                $"Neznamy report: '{reportName}'. " +
                "Zaregistrujte ho v xsb-reports.json a pridejte Build metodu.")
        };
    }
}

Registrace v DI (ApplicationServiceCollectionExtension.cs):

services.AddScoped<ReportXmlService>();

2. Dev endpoint — generický, čte název reportu z URL:

// SluzbyPM.Api/Endpoints/Testing/GetReportXmlEndpoint.cs

app.MapGet("dev/reports/{name}/xml", async (
    string name, HttpContext ctx,
    ReportXmlService reportXmlService, CancellationToken ct) =>
{
    var parameters = ctx.Request.Query
        .ToDictionary(q => q.Key, q => q.Value.ToString());
    string xml = await reportXmlService.BuildXmlAsync(name, parameters, ct);
    return Results.Content(xml, "application/xml");
});

Registrován přes MapTestingEndpointsIfDevelopment() — dostupný jen v Development režimu, bez autentizace.

11.7 Konkrétní příkazy — editace existujícího reportu

Potřebujete dva okna cmd.exe:

Okno 1 — spustit API:

cd C:\Users\Krejčí\source\repos\SluzbyPM.WebApi
dotnet run --project SluzbyPM.Api

Počkat až vypíše Now listening on: http://localhost:5144. Toto okno nechat běžet.

Okno 2 — spustit xsb edit:

cd C:\Users\Krejčí\source\repos\SluzbyPM.WebApi
C:\Users\Krejčí\source\repos\XmlStackBuilder\XmlStackBuilder.DevCli\bin\Debug\net9.0\xsb.exe edit PrehledPozadavku

S parametry (filtr na konkrétní stav):

C:\Users\Krejčí\source\repos\XmlStackBuilder\XmlStackBuilder.DevCli\bin\Debug\net9.0\xsb.exe edit PrehledPozadavku idStavy=1

Výstup vypadá takto:

Stahování XML dat z http://localhost:5144 ...
  XML uloženo: C:\Users\Krejčí\source\repos\SluzbyPM.WebApi\.xsb\PrehledPozadavku\sample.xml (7 569 znaků)
Generování JSON schématu s IntelliSense ...
  Schéma: C:\Users\Krejčí\source\repos\SluzbyPM.WebApi\.xsb\PrehledPozadavku\dynamic-schema.json
  Workspace: C:\Users\Krejčí\source\repos\SluzbyPM.WebApi\.xsb\PrehledPozadavku\editor.code-workspace
Renderování preview ...
  Preview: C:\Users\Krejčí\source\repos\SluzbyPM.WebApi\.xsb\PrehledPozadavku\preview.html
Otevírání VS Code ...

Hotovo. Edituj JSON šablonu ve VS Code, ukládej a sleduj preview v prohlížeči.

Co se stalo:

  1. CLI stáhlo XML z API (7 569 znaků reálných dat z DB)
  2. Vygenerovalo dynamic-schema.json (30 KB) — obsahuje reálné názvy polí z XML
  3. Vytvořilo editor.code-workspace s vazbou na dynamické schéma
  4. Renderovalo preview.html z aktuální JSON šablony + stažených dat
  5. Otevřelo VS Code (workspace) a prohlížeč (preview.html)

Výsledek ve VS Code:

11.8 Konkrétní příkazy — vytvoření nového reportu

Krok 1 — napsat Build metodu v ReportXmlService.cs a přidat do switch:

"PrehledZarizeni" => await BuildPrehledZarizeni(parameters, ct),

Krok 2 — přidat do xsb-reports.json:

"PrehledZarizeni": {
    "template": "PrehledZarizeni.json",
    "renderer": "table",
    "description": "Prehled zarizeni",
    "params": {}
}

Krok 3 — restartovat API (Ctrl+C v okně 1, pak dotnet run --project SluzbyPM.Api) a spustit:

C:\Users\Krejčí\source\repos\XmlStackBuilder\XmlStackBuilder.DevCli\bin\Debug\net9.0\xsb.exe edit PrehledZarizeni --new

Přepínač --new vygeneruje starter JSON šablonu ze stažených XML dat a otevře editor.

11.9 Další příkazy

Refresh dat (nové XML ze změněné DB nebo Build metody, bez otevírání VS Code):

C:\Users\Krejčí\source\repos\XmlStackBuilder\XmlStackBuilder.DevCli\bin\Debug\net9.0\xsb.exe edit PrehledPozadavku --refresh

Výpis registrovaných reportů:

C:\Users\Krejčí\source\repos\XmlStackBuilder\XmlStackBuilder.DevCli\bin\Debug\net9.0\xsb.exe list

11.10 Co CLI vygeneruje do .xsb/

.xsb\PrehledPozadavku\
    sample.xml              ← XML stažené z API (reálná data z DB)
    dynamic-schema.json     ← JSON schéma s názvy polí pro IntelliSense
    editor.code-workspace   ← VS Code workspace (schema + RunOnSave)
    preview.html            ← HTML preview (meta refresh 2s)

Složka .xsb/ je v .gitignore — necommituje se. JSON šablony v Reporting/Templates/ se commitují.

Vygenerovaný editor.code-workspace obsahuje:

11.11 Jak napsat Build metodu

Vzor — tabulková sestava s filtry:

private async Task<string> BuildPrehledPozadavku(
    Dictionary<string, string> p, CancellationToken ct)
{
    var builder = new XmlStackBuilder.Core.XmlStackBuilder();
    builder.CreateRoot("root");

    // 1. Element "parametr" — metadata pro záhlaví sestavy
    var filterParts = new List<string>();
    int? idStavy = null;
    if (p.TryGetValue("idStavy", out var sStavy) && int.TryParse(sStavy, out var parsedStavy))
    {
        idStavy = parsedStavy;
        var stav = await _db.Stavy.FindAsync(new object[] { parsedStavy }, ct);
        if (stav != null) filterParts.Add($"Stav: {stav.Nazev}");
    }

    builder.PushObject("parametr", new {
        nadpis = "Prehled pozadavku",
        filtr = filterParts.Count > 0 ? string.Join(", ", filterParts) : "Vsechny pozadavky"
    });

    // 2. Data z DB — LINQ Select + PushCollection
    var query = _db.Pozadavky
        .Include(x => x.StavEntity)
        .Include(x => x.VlozilEntity)
        .AsQueryable();

    if (idStavy.HasValue)
        query = query.Where(x => x.IdStavy == idStavy.Value);

    var rows = await query
        .OrderByDescending(x => x.IdPozadavky)
        .Select(x => new {
            idPozadavky = x.IdPozadavky,
            predmet = x.Predmet ?? "",
            stav = x.StavEntity!.Nazev ?? "",
            vlozeno = x.Vlozeno,
            planCena = x.PlanCena
        })
        .ToListAsync(ct);

    builder.PushCollection("pozadavky", rows);

    return builder.ToXmlString();
}

Jak to funguje:

11.12 Produkční použití stejných Build metod

Build metody v ReportXmlService slouží dvakrát:

  1. Dev endpoint (/dev/reports/{name}/xml) — CLI stáhne XML pro editaci šablony
  2. Produkční endpoint — ten samý XML + JSON šablona → PDF/HTML/XLSX
// Produkční endpoint — stejná BuildXmlAsync metoda
app.MapGet("/api/pozadavky/report/{format}", async (
    string format,
    [FromQuery] int? idStavy,
    ReportXmlService reportXmlService,
    CancellationToken ct) =>
{
    var parameters = new Dictionary<string, string>();
    if (idStavy.HasValue) parameters["idStavy"] = idStavy.Value.ToString();

    // Stejná Build metoda jako pro dev endpoint
    string xml = await reportXmlService.BuildXmlAsync("PrehledPozadavku", parameters, ct);

    // XML + JSON šablona → RenderToBytesFromXml → Results.File (žádné temp soubory)
    var builder = new XmlStackBuilder.Core.XmlStackBuilder();
    string jsonPath = Path.Combine(AppContext.BaseDirectory, "Reporting", "Templates", "PrehledPozadavku.json");

    return format.ToLower() switch
    {
        "pdf" => Results.File(
            builder.RenderToBytesFromXml("pdf", jsonPath, xml),
            "application/pdf", "prehled.pdf"),
        "xlsx" => Results.File(
            builder.RenderToBytesFromXml("xlsx", jsonPath, xml),
            "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
            "prehled.xlsx"),
        "html" => Results.Content(
            System.Text.Encoding.UTF8.GetString(
                builder.RenderToBytesFromXml("html", jsonPath, xml)),
            "text/html; charset=utf-8"),
        _ => Results.BadRequest($"Nepodporovaný formát: {format}")
    };
})
.RequireAuthorization();

11.13 Checklist pro přidání nového reportu

  1. Napsat BuildNovySestava() v ReportXmlService.cs
  2. Přidat do switch v BuildXmlAsync()
  3. Přidat záznam do xsb-reports.json
  4. Restartovat API (dotnet run --project SluzbyPM.Api)
  5. xsb.exe edit NovaSestava --new → vytvořit a doladit JSON šablonu
  6. git add JSON šablonu do Reporting/Templates/
  7. Přidat produkční endpoint (pokud potřeba)