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:

Pozn.: tento dokument je psaný pro SluzbyPM (.NET 9); XmlStackBuilder.Core.Net je dnes multi-target net9 + net10 (net10 je primární pro JUW2, net9 zůstává podporovaný floor).

Renderery:

Renderer Použití Výstupní formáty
table Přehledy, výkazy, saldokonta, inventury HTML, PDF, XLSX, DOCX
document Faktury, dodací listy, objednávky, dopisy HTML, PDF, DOCX
label Štítky, obálky, čárové kódy HTML, PDF
Markdown (.md) Opakované textové dokumenty (přílohy, dopisy) HTML, PDF, DOCX, čistý MD

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)