.NET 9 varianta knihovny XmlStackBuilder.Core, která umožňuje:
XmlBuilderFromClass.BuildFromObject()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.
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
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.
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");
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.
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:
PushObjectpřevede všechny public properties na XML atributy.PushCollectionvytvoří 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).
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);
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);
{
"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"
}]
}
// 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);
// 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();
}
}
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}"
}
]
}
// 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}");
}
}
| 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) |
| 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 |
Fáze 1 — koexistence (doporučeno na začátek):
XmlStackReportService vedle stávajícího ReportingServiceFáze 2 — migrace approval reportu:
SchvaleniPozadavku.jsonMakeApprovalReportAsync (viz příklad 5.2)ApprovalPdfDocument.csFáze 3 — odstranění starých závislostí (volitelné):
QuestPDF, PuppeteerSharp, RazorEngineCorePuppeteerService.cs, RazorRenderer.cs, HtmlReportGenerator.csReporting/QuestDocuments/, Reporting/HtmlTemplates/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);
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");
JSON šablony lze editovat s IntelliSense. Schémata jsou v XmlStackBuilder/schemas/:
table-report.schema.json — tabulkové sestavydocument.schema.json — dokumentylabel.schema.json — štítkyVe 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"
}
]
}
Všechny třídy jsou v namespace XmlStackBuilder.Core.
// === 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);
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:
decimal, double, float → InvariantCulture (tečka jako oddělovač)DateTime → yyyy-MM-dd (bez času) nebo yyyy-MM-ddTHH:mm:ss (s časem)bool → true / falsenull → přeskočeno (atribut se nevytvoří)string → automaticky trimovánovar 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.
.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:
Pokud by se SluzbyPM nasazoval na Linux (ne Windows), System.Drawing.Common nefunguje. Řešení:
libgdiplus nebo přejít na SixLabors.ImageSharpPdfSharp 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();
Pro sestavy s 10 000+ řádky zvažte:
pageLayout: "paged" v JSON (stránkování)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.
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:
/dev/reports/{name}/xml)idPozadavky, predmet, stav...) a prohlížeč s live preview 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)
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á)
{
// 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(vizSluzbyPM.Api/Properties/launchSettings.json). Pokud se změní, je třeba aktualizovatapiUrl.
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:
"PrehledPozadavku" v xsb-reports.jsonGET http://localhost:5144/dev/reports/PrehledPozadavku/xml?idStavy=3ReportXmlService.BuildXmlAsync("PrehledPozadavku", {"idStavy":"3"}, ct)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.
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:
dynamic-schema.json (30 KB) — obsahuje reálné názvy polí z XMLeditor.code-workspace s vazbou na dynamické schémapreview.html z aktuální JSON šablony + stažených datVýsledek ve VS Code:
idPozadavky, refNo, predmet, kategorie, stredisko, stav, urgentni, vlozeno, vlozil, resitel, planCena, schvCenaKrok 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.
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
.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:
PrehledPozadavku.json → dynamic-schema.json (IntelliSense)xsb.exe preview → překreslí preview.htmlemeraldwalk.runonsave (live preview)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:
PushObject("parametr", new { ... })— anonymní objekt → element s atributyPushCollection("pozadavky", rows)— kolekce → kontejner +<row>elementy s atributy- Čísla, datumy, bool se formátují automaticky (InvariantCulture, ISO datum)
nullhodnoty se přeskočí (nevytvoří se prázdný atribut)- Element
parametr: metadata sestavy (nadpis, filtr) — zobrazí se v záhlaví
Build metody v ReportXmlService slouží dvakrát:
/dev/reports/{name}/xml) — CLI stáhne XML pro editaci šablony// 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();
BuildNovySestava() v ReportXmlService.csswitch v BuildXmlAsync()xsb-reports.jsondotnet run --project SluzbyPM.Api)xsb.exe edit NovaSestava --new → vytvořit a doladit JSON šablonugit add JSON šablonu do Reporting/Templates/