Skip to content

Commit e338b05

Browse files
Improve Excel preflight and report evidence
1 parent c8ac579 commit e338b05

13 files changed

Lines changed: 823 additions & 3 deletions

Directory.Build.targets

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
1212
</PropertyGroup>
1313

14-
<!-- Examples are executable documentation. Keep samples focused on API usage rather than nullable ceremony. -->
14+
<!-- Examples are executable documentation. Keep nullable enabled so annotated samples do not emit CS8632 noise. -->
1515
<PropertyGroup Condition="'$(MSBuildProjectName)' == 'OfficeIMO.Examples'">
1616
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
17-
<Nullable>disable</Nullable>
17+
<Nullable>enable</Nullable>
1818
</PropertyGroup>
1919
</Project>

Docs/officeimo.excel.large-workbook-guidance.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,20 @@ formulas.EnsureAllHaveCachedResults();
4343

4444
Use `EnsureNoAdvancedFeatures()` only for workflows that must avoid preserve-only package content such as custom XML, macros, slicers, timelines, embedded packages, or external workbook relationships.
4545

46+
For workflow routing, prefer the capability preflight API over ad hoc feature-name checks:
47+
48+
```csharp
49+
ExcelFeatureReport features = document.InspectFeatures();
50+
51+
features.EnsureCan(ExcelPreflightCapability.EditCellValues);
52+
53+
if (!features.Can(ExcelPreflightCapability.ExportPdfReport)) {
54+
File.WriteAllText("excel-preflight.md", features.ToMarkdown());
55+
}
56+
```
57+
58+
`ExcelPreflightCapability` covers readback, cell-value edits, structure-changing edits, cached formula reads, OfficeIMO formula calculation, template binding, and first-party PDF report export. This is intentionally separate from benchmark guidance and does not require benchmark runs in CI.
59+
4660
## Measuring A Change
4761

4862
Use the benchmark harness for repeatable local evidence:
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using System;
2+
using System.IO;
3+
using OfficeIMO.Excel;
4+
5+
namespace OfficeIMO.Examples.Excel {
6+
/// <summary>
7+
/// Demonstrates workflow-level feature preflight before read, edit, template, or PDF operations.
8+
/// </summary>
9+
public static class FeaturePreflight {
10+
public static void Example(string folderPath, bool openExcel) {
11+
Console.WriteLine("[*] Excel - Feature preflight");
12+
string filePath = Path.Combine(folderPath, "FeaturePreflight.xlsx");
13+
14+
using (ExcelDocument document = ExcelDocument.Create(filePath)) {
15+
ExcelSheet sheet = document.AddWorkSheet("Report");
16+
sheet.CellValue(1, 1, "Name");
17+
sheet.CellValue(1, 2, "Score");
18+
sheet.CellValue(2, 1, "Alpha");
19+
sheet.CellValue(2, 2, 10d);
20+
sheet.CellValue(3, 1, "Beta");
21+
sheet.CellValue(3, 2, 20d);
22+
sheet.CellFormula(4, 2, "SUM(B2:B3)");
23+
document.Calculate();
24+
document.Save(openExcel);
25+
}
26+
27+
using (ExcelDocument document = ExcelDocument.Load(filePath, readOnly: true)) {
28+
ExcelFeatureReport report = document.InspectFeatures();
29+
30+
foreach (ExcelPreflightCapability capability in Enum.GetValues(typeof(ExcelPreflightCapability))) {
31+
Console.WriteLine($"{capability}: {(report.Can(capability) ? "ready" : "blocked")}");
32+
foreach (string diagnostic in report.GetCapabilityDiagnostics(capability)) {
33+
Console.WriteLine(" - " + diagnostic);
34+
}
35+
}
36+
}
37+
}
38+
}
39+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using DocumentFormat.OpenXml.Spreadsheet;
2+
using OfficeIMO.Excel;
3+
using OfficeIMO.Excel.Pdf;
4+
using PdfCore = OfficeIMO.Pdf;
5+
6+
namespace OfficeIMO.Examples.Excel {
7+
/// <summary>
8+
/// Demonstrates a template-to-report workflow with formulas, charts, pivots, preflight, and PDF export.
9+
/// </summary>
10+
public static class ReportWorkflow {
11+
public static void Example(string folderPath, bool openExcel) {
12+
Console.WriteLine("[*] Excel - Report workflow");
13+
string workbookPath = Path.Combine(folderPath, "ExcelReportWorkflow.xlsx");
14+
string pdfPath = Path.Combine(folderPath, "ExcelReportWorkflow.pdf");
15+
16+
using (ExcelDocument document = ExcelDocument.Create(workbookPath, "Report")) {
17+
ExcelSheet sheet = document.Sheets[0];
18+
sheet.Cell(1, 1, "{{ReportTitle}}");
19+
sheet.Cell(2, 1, "Region");
20+
sheet.Cell(2, 2, "Revenue");
21+
sheet.Cell(2, 3, "Cost");
22+
sheet.Cell(2, 4, "Margin");
23+
sheet.Cell(3, 1, "East");
24+
sheet.Cell(3, 2, 120);
25+
sheet.Cell(3, 3, 50);
26+
sheet.CellFormula(3, 4, "B3-C3");
27+
sheet.Cell(4, 1, "West");
28+
sheet.Cell(4, 2, 90);
29+
sheet.Cell(4, 3, 40);
30+
sheet.CellFormula(4, 4, "B4-C4");
31+
32+
document.ApplyTemplate(new {
33+
ReportTitle = "Executive revenue report"
34+
});
35+
document.Calculate();
36+
37+
sheet.AddTable("A2:D4", hasHeader: true, name: "RevenueData", style: OfficeIMO.Excel.TableStyle.TableStyleMedium4);
38+
sheet.AddChartFromRange("A2:D4", row: 6, column: 1, widthPixels: 420, heightPixels: 240,
39+
type: ExcelChartType.ColumnClustered, title: "Revenue and Margin");
40+
sheet.AddPivotTable(
41+
sourceRange: "A2:D4",
42+
destinationCell: "F2",
43+
name: "RevenuePivot",
44+
rowFields: new[] { "Region" },
45+
dataFields: new[] { new ExcelPivotDataField("Revenue", DataConsolidateFunctionValues.Sum, "Total Revenue") },
46+
pivotStyleName: "PivotStyleMedium9");
47+
48+
ExcelFeatureReport report = document.InspectFeatures();
49+
if (!report.Can(ExcelPreflightCapability.ExportPdfReport)) {
50+
Console.WriteLine("[!] Excel-to-PDF export is blocked:");
51+
foreach (string diagnostic in report.GetCapabilityDiagnostics(ExcelPreflightCapability.ExportPdfReport)) {
52+
Console.WriteLine(" - " + diagnostic);
53+
}
54+
return;
55+
}
56+
57+
document.Save(false);
58+
document.SaveAsPdf(pdfPath, new ExcelPdfSaveOptions {
59+
IncludeSheetHeadings = false,
60+
HeaderRowCount = 1,
61+
PageSize = new PdfCore.PageSize(560, 520),
62+
Margins = PdfCore.PageMargins.Uniform(24)
63+
});
64+
65+
if (openExcel) {
66+
document.Open(workbookPath, true);
67+
}
68+
}
69+
}
70+
}
71+
}

OfficeIMO.Examples/OfficeIMO.Examples.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
<ItemGroup>
1313
<ProjectReference Include="..\\OfficeIMO.Excel\\OfficeIMO.Excel.csproj" />
14+
<ProjectReference Include="..\\OfficeIMO.Excel.Pdf\\OfficeIMO.Excel.Pdf.csproj" />
1415
<ProjectReference Include="..\\OfficeIMO.Excel.GoogleSheets\\OfficeIMO.Excel.GoogleSheets.csproj" />
1516
<ProjectReference Include="..\\OfficeIMO.GoogleWorkspace\\OfficeIMO.GoogleWorkspace.csproj" />
1617
<ProjectReference Include="..\\OfficeIMO.Pdf\\OfficeIMO.Pdf.csproj" />

OfficeIMO.Examples/Program.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,10 @@ static void Main(string[] args) {
319319
// Excel.ReadPresetsAndHelpers.Example(folderPath, false);
320320
// // Excel/Read for PowerShell consumption (emits JSON rows)
321321
// Excel.ReadForPowerShell.Example(folderPath, false);
322+
// // Excel/Feature preflight for read/edit/template/PDF workflow routing
323+
// Excel.FeaturePreflight.Example(folderPath, false);
324+
// // Excel/Report workflow with template, formulas, chart, pivot, preflight, and PDF
325+
// Excel.ReportWorkflow.Example(folderPath, false);
322326
// // Excel/PowerShell-style round trip: write → read → modify → write → JSON
323327
// Excel.PowerShellRoundTrip.Example(folderPath, false);
324328
// // Excel/Headers + Footers + Properties
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
namespace OfficeIMO.Excel {
2+
/// <summary>
3+
/// Workflow-level OfficeIMO.Excel operation categories covered by feature preflight checks.
4+
/// </summary>
5+
public enum ExcelPreflightCapability {
6+
/// <summary>Read worksheet data, tables, and typed rows from the workbook.</summary>
7+
ReadWorkbookData,
8+
9+
/// <summary>Edit existing cell values while preserving package parts OfficeIMO does not fully author yet.</summary>
10+
EditCellValues,
11+
12+
/// <summary>Perform structure-changing edits such as adding/removing sheets, tables, drawings, pivots, or relationships.</summary>
13+
EditWorkbookStructure,
14+
15+
/// <summary>Use cached formula values for reads, export, or downstream reporting.</summary>
16+
UseCachedFormulaValues,
17+
18+
/// <summary>Calculate workbook formulas through OfficeIMO's lightweight evaluator.</summary>
19+
CalculateFormulas,
20+
21+
/// <summary>Bind data into an Excel template and save the generated workbook.</summary>
22+
BindTemplate,
23+
24+
/// <summary>Export a report workbook through the first-party OfficeIMO Excel-to-PDF path.</summary>
25+
ExportPdfReport
26+
}
27+
28+
public sealed partial class ExcelFeatureReport {
29+
/// <summary>
30+
/// True when OfficeIMO can attempt read-oriented workbook data operations.
31+
/// </summary>
32+
public bool CanReadWorkbookData => UnsupportedFeatures.Count == 0;
33+
34+
/// <summary>
35+
/// True when OfficeIMO can attempt cell-value edits without known unsupported package features.
36+
/// </summary>
37+
public bool CanEditCellValues => UnsupportedFeatures.Count == 0;
38+
39+
/// <summary>
40+
/// True when OfficeIMO can attempt structure-changing workbook edits without preserve-only or unsupported feature blockers.
41+
/// </summary>
42+
public bool CanEditWorkbookStructure => !HasAdvancedFeatures;
43+
44+
/// <summary>
45+
/// True when cached formula values can be trusted for read/export workflows.
46+
/// </summary>
47+
public bool CanUseCachedFormulaValues =>
48+
FindFeatureCount("Missing formula caches") == 0 &&
49+
FindFeatureCount("Formula dependency issues") == 0;
50+
51+
/// <summary>
52+
/// True when OfficeIMO's lightweight evaluator can calculate all discovered formulas without known dependency issues.
53+
/// </summary>
54+
public bool CanCalculateFormulas =>
55+
FindFeatureCount("Unsupported formulas") == 0 &&
56+
FindFeatureCount("Formula dependency issues") == 0;
57+
58+
/// <summary>
59+
/// True when template binding can be attempted without preserve-only or unsupported advanced package features.
60+
/// </summary>
61+
public bool CanBindTemplate => !HasAdvancedFeatures;
62+
63+
/// <summary>
64+
/// True when the workbook is suitable for the first-party report-grade Excel-to-PDF export path.
65+
/// </summary>
66+
public bool CanExportPdfReport =>
67+
!HasAdvancedFeatures &&
68+
CanUseCachedFormulaValues;
69+
70+
/// <summary>
71+
/// Returns true when the requested workflow-level capability can be attempted for this workbook.
72+
/// </summary>
73+
/// <param name="capability">The OfficeIMO.Excel workflow capability to check.</param>
74+
public bool Can(ExcelPreflightCapability capability) {
75+
switch (capability) {
76+
case ExcelPreflightCapability.ReadWorkbookData:
77+
return CanReadWorkbookData;
78+
case ExcelPreflightCapability.EditCellValues:
79+
return CanEditCellValues;
80+
case ExcelPreflightCapability.EditWorkbookStructure:
81+
return CanEditWorkbookStructure;
82+
case ExcelPreflightCapability.UseCachedFormulaValues:
83+
return CanUseCachedFormulaValues;
84+
case ExcelPreflightCapability.CalculateFormulas:
85+
return CanCalculateFormulas;
86+
case ExcelPreflightCapability.BindTemplate:
87+
return CanBindTemplate;
88+
case ExcelPreflightCapability.ExportPdfReport:
89+
return CanExportPdfReport;
90+
default:
91+
throw new ArgumentOutOfRangeException(nameof(capability), capability, "Unsupported Excel preflight capability.");
92+
}
93+
}
94+
95+
/// <summary>
96+
/// Throws with workflow-specific diagnostics when the requested capability cannot be attempted for this workbook.
97+
/// </summary>
98+
/// <param name="capability">The OfficeIMO.Excel workflow capability that must be available.</param>
99+
/// <returns>The current feature report for fluent guard usage.</returns>
100+
public ExcelFeatureReport EnsureCan(ExcelPreflightCapability capability) {
101+
if (Can(capability)) {
102+
return this;
103+
}
104+
105+
IReadOnlyList<string> diagnostics = GetCapabilityDiagnostics(capability);
106+
string detail = diagnostics.Count == 0
107+
? "No additional diagnostics were reported."
108+
: string.Join("; ", diagnostics);
109+
throw new InvalidOperationException($"Excel preflight capability '{capability}' is not available: {detail}");
110+
}
111+
112+
/// <summary>
113+
/// Returns operation-specific diagnostics explaining why a workflow-level capability is blocked, or an empty list when it can be attempted.
114+
/// </summary>
115+
/// <param name="capability">The OfficeIMO.Excel workflow capability to explain.</param>
116+
public IReadOnlyList<string> GetCapabilityDiagnostics(ExcelPreflightCapability capability) {
117+
if (Can(capability)) {
118+
return Array.Empty<string>();
119+
}
120+
121+
var messages = new List<string>();
122+
switch (capability) {
123+
case ExcelPreflightCapability.ReadWorkbookData:
124+
AddUnsupportedDiagnostics(messages, "Workbook data reads are blocked by unsupported workbook features.");
125+
break;
126+
case ExcelPreflightCapability.EditCellValues:
127+
AddUnsupportedDiagnostics(messages, "Cell-value edits are blocked by unsupported workbook features.");
128+
break;
129+
case ExcelPreflightCapability.EditWorkbookStructure:
130+
AddUnsupportedDiagnostics(messages, "Structure-changing edits are blocked by unsupported workbook features.");
131+
AddPreservedDiagnostics(messages, "Structure-changing edits should not be attempted while preserve-only workbook features are present.");
132+
break;
133+
case ExcelPreflightCapability.UseCachedFormulaValues:
134+
AddFormulaDiagnostics(messages, requireCachedValues: true, requireSupportedFormulas: false);
135+
break;
136+
case ExcelPreflightCapability.CalculateFormulas:
137+
AddFormulaDiagnostics(messages, requireCachedValues: false, requireSupportedFormulas: true);
138+
break;
139+
case ExcelPreflightCapability.BindTemplate:
140+
AddUnsupportedDiagnostics(messages, "Template binding is blocked by unsupported workbook features.");
141+
AddPreservedDiagnostics(messages, "Template binding should not be attempted while preserve-only workbook features are present unless the workflow has explicit preservation coverage for those parts.");
142+
break;
143+
case ExcelPreflightCapability.ExportPdfReport:
144+
AddUnsupportedDiagnostics(messages, "Excel-to-PDF report export is blocked by unsupported workbook features.");
145+
AddPreservedDiagnostics(messages, "Excel-to-PDF report export does not render preserve-only workbook features.");
146+
AddFormulaDiagnostics(messages, requireCachedValues: true, requireSupportedFormulas: false);
147+
break;
148+
default:
149+
throw new ArgumentOutOfRangeException(nameof(capability), capability, "Unsupported Excel preflight capability.");
150+
}
151+
152+
if (messages.Count == 0) {
153+
AddDistinct(messages, "The requested Excel workflow is not available for this workbook.");
154+
}
155+
156+
return messages.AsReadOnly();
157+
}
158+
159+
private void AddUnsupportedDiagnostics(List<string> messages, string fallbackMessage) {
160+
if (UnsupportedFeatures.Count == 0) {
161+
return;
162+
}
163+
164+
AddDistinct(messages, fallbackMessage);
165+
AddFeatureDiagnostics(messages, UnsupportedFeatures);
166+
}
167+
168+
private void AddPreservedDiagnostics(List<string> messages, string fallbackMessage) {
169+
if (PreservedFeatures.Count == 0) {
170+
return;
171+
}
172+
173+
AddDistinct(messages, fallbackMessage);
174+
AddFeatureDiagnostics(messages, PreservedFeatures);
175+
}
176+
177+
private void AddFormulaDiagnostics(List<string> messages, bool requireCachedValues, bool requireSupportedFormulas) {
178+
if (requireSupportedFormulas) {
179+
AddFeatureDiagnostics(messages, FindFeatures("Unsupported formulas"));
180+
}
181+
182+
if (requireCachedValues) {
183+
AddFeatureDiagnostics(messages, FindFeatures("Missing formula caches"));
184+
}
185+
186+
AddFeatureDiagnostics(messages, FindFeatures("Formula dependency issues"));
187+
}
188+
189+
private static void AddFeatureDiagnostics(List<string> messages, IEnumerable<ExcelFeatureFinding> findings) {
190+
foreach (ExcelFeatureFinding finding in findings) {
191+
AddDistinct(messages, FormatCapabilityFinding(finding));
192+
}
193+
}
194+
195+
private static string FormatCapabilityFinding(ExcelFeatureFinding finding) {
196+
string message = $"{finding.Name} ({finding.Count}, {finding.SupportLevel}): {finding.Note}";
197+
if (finding.Details.Count == 0) {
198+
return message;
199+
}
200+
201+
const int maxDetails = 3;
202+
string details = string.Join("; ", finding.Details.Take(maxDetails));
203+
if (finding.Details.Count > maxDetails) {
204+
details += $"; +{finding.Details.Count - maxDetails} more";
205+
}
206+
207+
return message + " [" + details + "]";
208+
}
209+
210+
private int FindFeatureCount(string featureName) {
211+
return FindFeatures(featureName).Sum(feature => feature.Count);
212+
}
213+
214+
private static void AddDistinct(List<string> messages, string message) {
215+
if (string.IsNullOrWhiteSpace(message)) {
216+
return;
217+
}
218+
219+
for (int i = 0; i < messages.Count; i++) {
220+
if (string.Equals(messages[i], message, StringComparison.Ordinal)) {
221+
return;
222+
}
223+
}
224+
225+
messages.Add(message);
226+
}
227+
}
228+
}

0 commit comments

Comments
 (0)