Skip to content

Commit 357b9de

Browse files
Add deferred cell style updates (#1013)
* Add deferred cell style updates * Stream worksheet updates in OpenXml editor * Renamed the MiniExcel class to MiniExcelV2 Applied for rebasing on top of #1010 * Refactored the new OpenXmlEditor utility - Separated the API from the implementation details by moving the latters from `OpenXmlEditor` to `OpenXmlEditorInternals` - Moved the builder pattern to the intermediate class `OpenXmlEditingPipeline` to facilitate handling the resources and adding new features - Added proper synchronous implementation via the `SyncMethodGenerator` - Ajusted tests to reflect the changes * Separated synchronous from asynchronous tests and moved them to the Editor folder * Added documentation for the API nethods * Minor adjustments Fixed small stream disposal and task cancellation issues in `OpenXmlEditor.SaveAsync` and created new "Excel Editor" readme section * Added method overloads to save pipeline modifications to different paths or streams * Moved AlterSheet API from OpenXmlExporter to OpenXmlEditor The `AlterSheet` API methods have been moved from the `OpenXmlExporter` to the now more appropriate `OpenXmlEditor`, and renamed to `AlterSheetInfo`. The implementation details have also been moved from `OpenXmlWriter` to `OpenXmlEditorInternals` , and the tests have been tweaked to reflect the changes. The older API signatures have been obsoleted for eventual removal. --------- Co-authored-by: Michele Bastione <michele.bastione@gmail.com>
1 parent 8fd3e38 commit 357b9de

17 files changed

Lines changed: 1082 additions & 116 deletions

‎README_V2.md‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ The exporters also fully support asynchronous operations:
171171
await exporter.ExportAsync(outputPath, values);
172172
```
173173

174+
174175
### Release Notes
175176

176177
If you're migrating from a `1.x` version, please check the [upgrade notes](V2-Upgrade-Notes.md).
@@ -201,6 +202,7 @@ You can find the benchmarks' results for the latest release [here](benchmarks/re
201202
- [Query/Import](#docs-import)
202203
- [Create/Export](#docs-export)
203204
- [Excel Template](#docs-template)
205+
- [Excel Editor](#docs-editing)
204206
- [Attributes and configuration](#docs-attributes)
205207
- [CSV specifics](#docs-csv)
206208
- [Other functionalities](#docs-other)
@@ -1177,6 +1179,21 @@ Result:
11771179
<img width="890" height="999" alt="image" src="https://github.com/user-attachments/assets/fae209ec-b3e2-4f2e-94e4-3b52a37dc364" />
11781180

11791181

1182+
### Editing existing workbooks <a name="docs-editing" />
1183+
1184+
> Warning: this feature is a work in progress and currently very limited!
1185+
1186+
Cell style updates are queued and applied in worksheet and cell order when `Save` is called. If the same cell is updated more than once, the last update wins.
1187+
1188+
```csharp
1189+
var editor = MiniExcelV2.Editors.GetOpenXmlEditor();
1190+
editor.StartEditingPipeline(path)
1191+
.UpdateCellStyle("A1", style => style.FontColor = Color.Red)
1192+
.UpdateCellStyle("X100", style => style.FontColor = Color.Blue)
1193+
.SaveChanges();
1194+
```
1195+
1196+
11801197
### Attributes and configuration <a name="docs-attributes" />
11811198

11821199
#### 1. Specify the column name, column index, or ignore the column entirely.
@@ -1611,12 +1628,12 @@ exporter.Export(path, value, configuration: config);
16111628
#### Read empty string as null
16121629

16131630
By default, empty values are mapped to `string.Empty`.
1614-
You can modify this behavior and map them to `null` using the `CsvConfiguration.ReadEmptyStringAsNull` property:
1631+
You can modify this behavior and map them to `null` using the `CsvConfiguration.ReadEmptyFieldsAsDefault` property:
16151632

16161633
```csharp
16171634
var config = new CsvConfiguration
16181635
{
1619-
ReadEmptyStringAsNull = true
1636+
ReadEmptyFieldsAsDefault = true
16201637
};
16211638
```
16221639

‎src/MiniExcel.Core/MiniExcel.cs‎

Whitespace-only changes.

‎src/MiniExcel.Core/MiniExcelProviders.cs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,8 @@ public sealed class MiniExcelTemplaterProvider
1414
{
1515
internal MiniExcelTemplaterProvider() { }
1616
}
17+
18+
public sealed class MiniExcelEditorProvider
19+
{
20+
internal MiniExcelEditorProvider() { }
21+
}

‎src/MiniExcel.Core/MiniExcelV2.cs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public static class MiniExcelV2
88
public static readonly MiniExcelExporterProvider Exporters = new();
99
public static readonly MiniExcelImporterProvider Importers = new();
1010
public static readonly MiniExcelTemplaterProvider Templaters = new();
11+
public static readonly MiniExcelEditorProvider Editors = new();
1112
}
1213

1314
[Obsolete("This class will be removed in the full release, use MiniExcelV2 instead.", true)]
@@ -16,4 +17,5 @@ public static class MiniExcel
1617
public static readonly MiniExcelExporterProvider Exporters = new();
1718
public static readonly MiniExcelImporterProvider Importers = new();
1819
public static readonly MiniExcelTemplaterProvider Templaters = new();
20+
public static readonly MiniExcelEditorProvider Editors = new();
1921
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
using MiniExcelLib.OpenXml.Editor;
2+
3+
// ReSharper disable once CheckNamespace
4+
namespace MiniExcelLib.OpenXml;
5+
6+
public sealed partial class OpenXmlEditor
7+
{
8+
internal OpenXmlEditor() { }
9+
10+
11+
/// <summary>
12+
/// Creates a new editing pipeline for the provided Excel document.
13+
/// </summary>
14+
/// <param name="path">The file path to the Excel document to edit.</param>
15+
/// <returns>
16+
/// An <see cref="OpenXmlEditingPipeline"/> instance that can be used to apply modifications to the document.
17+
/// </returns>
18+
/// <remarks>
19+
/// This method opens the file for exclusive read-write access. The file is locked until
20+
/// SaveChanges or SaveChangesAsync is called.
21+
/// </remarks>
22+
public OpenXmlEditingPipeline StartEditingPipeline(string path)
23+
{
24+
if (string.IsNullOrWhiteSpace(path))
25+
throw new ArgumentException("The path cannot be null or whitespace.", nameof(path));
26+
27+
var stream = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.Read);
28+
return new OpenXmlEditingPipeline(stream, leaveOpen: false);
29+
}
30+
31+
/// <summary>
32+
/// Creates a new editing pipeline for the provided Excel document.
33+
/// </summary>
34+
/// <param name="stream">The stream containing the Excel file data.</param>
35+
/// <param name="leaveOpen">
36+
/// If true the stream remains open after changes are saved and must be disposed by the caller,
37+
/// if false it is automatically closed when changes are saved. Default is false.
38+
/// </param>
39+
/// <returns>
40+
/// An <see cref="OpenXmlEditingPipeline"/> instance that can be used to apply modifications to the file.
41+
/// </returns>
42+
/// <remarks>
43+
/// Even with parameter <c>leaveOpen: false</c>, the underlying stream will not be disposed until
44+
/// SaveChanges or SaveChangesAsync are called.
45+
/// </remarks>
46+
public OpenXmlEditingPipeline StartEditingPipeline(Stream stream, bool leaveOpen = false)
47+
{
48+
if (stream is null)
49+
throw new ArgumentNullException(nameof(stream));
50+
51+
return new OpenXmlEditingPipeline(stream, leaveOpen);
52+
}
53+
54+
/// <summary>
55+
/// Modify the properties of a worksheet in the specified document.
56+
/// </summary>
57+
/// <param name="path">The path to the OpenXml document.</param>
58+
/// <param name="sheetName">The name of the worksheet to modify.</param>
59+
/// <param name="newSheetName">The new name to assign to the worksheet, or <c>null</c> to leave as is.</param>
60+
/// <param name="newSheetIndex">The position in the workbook to assign to the worksheet, or <c>null</c> to leave as is.</param>
61+
/// <param name="newSheetState">The visibility state to assign to the worksheet, or <c>null</c> to leave as is.</param>
62+
/// <param name="cancellationToken">The token to monitor for cancellation requests</param>
63+
[CreateSyncVersion]
64+
public async Task AlterSheetInfoAsync(string path, string sheetName, string? newSheetName = null, int? newSheetIndex = null, SheetState? newSheetState = null, CancellationToken cancellationToken = default)
65+
{
66+
var stream = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.Read);
67+
await using var disposableStream = stream.ConfigureAwait(false);
68+
69+
await AlterSheetInfoAsync(stream, sheetName, newSheetName, newSheetIndex, newSheetState, cancellationToken).ConfigureAwait(false);
70+
}
71+
72+
/// <summary>
73+
/// Modify the properties of a worksheet in the specified document.
74+
/// </summary>
75+
/// <param name="stream">The stream to the OpenXml document.</param>
76+
/// <param name="sheetName">The name of the worksheet to modify.</param>
77+
/// <param name="newSheetName">The new name to assign to the worksheet, or <c>null</c> to leave as is.</param>
78+
/// <param name="newSheetIndex">The position in the workbook to assign to the worksheet, or <c>null</c> to leave as is.</param>
79+
/// <param name="newSheetState">The visibility state to assign to the worksheet, or <c>null</c> to leave as is.</param>
80+
/// <param name="cancellationToken">The token to monitor for cancellation requests</param>
81+
[CreateSyncVersion]
82+
public async Task AlterSheetInfoAsync(Stream stream, string sheetName, string? newSheetName = null, int? newSheetIndex = null, SheetState? newSheetState = null, CancellationToken cancellationToken = default)
83+
{
84+
var internals = new OpenXmlEditorInternals(stream, true);
85+
await internals.AlterWorksheetAsync(sheetName, newSheetName, newSheetIndex, newSheetState, cancellationToken).ConfigureAwait(false);
86+
}
87+
}

‎src/MiniExcel.OpenXml/Api/OpenXmlExporter.cs‎

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -200,12 +200,10 @@ public async Task<int[]> ExportAsync(Stream stream, object value, bool printHead
200200
/// <param name="newSheetState">The visibility state to assign to the worksheet, or <c>null</c> to leave as is.</param>
201201
/// <param name="cancellationToken">The token to monitor for cancellation requests</param>
202202
[CreateSyncVersion]
203+
[Obsolete("This method will be removed in the full release, please use MiniExcelV2.Editors.GetOpenXmlEditor().AlterSheetInfo instead.")]
203204
public async Task AlterSheetAsync(string path, string sheetName, string? newSheetName = null, int? newSheetIndex = null, SheetState? newSheetState = null, CancellationToken cancellationToken = default)
204205
{
205-
var stream = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.Read);
206-
await using var disposableStream = stream.ConfigureAwait(false);
207-
208-
await AlterSheetAsync(stream, sheetName, newSheetName, newSheetIndex, newSheetState, cancellationToken).ConfigureAwait(false);
206+
await new OpenXmlEditor().AlterSheetInfoAsync(path, sheetName, newSheetName, newSheetIndex, newSheetState, cancellationToken).ConfigureAwait(false);
209207
}
210208

211209
/// <summary>
@@ -218,12 +216,9 @@ public async Task AlterSheetAsync(string path, string sheetName, string? newShee
218216
/// <param name="newSheetState">The visibility state to assign to the worksheet, or <c>null</c> to leave as is.</param>
219217
/// <param name="cancellationToken">The token to monitor for cancellation requests</param>
220218
[CreateSyncVersion]
219+
[Obsolete("This method will be removed in the full release, please use MiniExcelV2.Editors.GetOpenXmlEditor().AlterSheetInfo instead.")]
221220
public async Task AlterSheetAsync(Stream stream, string sheetName, string? newSheetName = null, int? newSheetIndex = null, SheetState? newSheetState = null, CancellationToken cancellationToken = default)
222221
{
223-
var writer = await OpenXmlWriter
224-
.CreateAsync(stream, null, sheetName, false, new OpenXmlConfiguration { FastMode = true }, cancellationToken)
225-
.ConfigureAwait(false);
226-
227-
await writer.AlterWorksheetAsync(sheetName, newSheetName, newSheetIndex, newSheetState, cancellationToken).ConfigureAwait(false);
222+
await new OpenXmlEditor().AlterSheetInfoAsync(stream, sheetName, newSheetName, newSheetIndex, newSheetState, cancellationToken).ConfigureAwait(false);
228223
}
229224
}

‎src/MiniExcel.OpenXml/Api/ProviderExtensions.cs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ public static class ProviderExtensions
66
public static OpenXmlExporter GetOpenXmlExporter(this MiniExcelExporterProvider exporterProvider) => new();
77
public static OpenXmlImporter GetOpenXmlImporter(this MiniExcelImporterProvider importerProvider) => new();
88
public static OpenXmlTemplater GetOpenXmlTemplater(this MiniExcelTemplaterProvider templaterProvider) => new();
9-
}
9+
public static OpenXmlEditor GetOpenXmlEditor(this MiniExcelEditorProvider editorProvider) => new();
10+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
using MiniExcelLib.OpenXml.Styles;
2+
3+
namespace MiniExcelLib.OpenXml.Editor;
4+
5+
/// <summary>
6+
/// Represents a pipeline for editing Excel files with a fluent API.
7+
/// </summary>
8+
public sealed partial class OpenXmlEditingPipeline
9+
{
10+
private readonly OpenXmlEditorInternals _internals;
11+
12+
internal OpenXmlEditingPipeline(Stream stream, bool leaveOpen)
13+
{
14+
_internals = new OpenXmlEditorInternals(stream, leaveOpen);
15+
}
16+
17+
/// <summary>
18+
/// Updates the style of a cell using a callback function that modifies the provided <see cref="OpenXmlCellStyle"/> object.
19+
/// </summary>
20+
/// <param name="cellReference">The cell reference in standard Excel format (e.g., "A1", "B5").</param>
21+
/// <param name="updateCellCallback">A callback function that receives an <see cref="OpenXmlCellStyle"/> object and applies the desired style changes.</param>
22+
/// <param name="sheetName">The name of the worksheet to update. If null or not specified, the first sheet in the workbook is used.</param>
23+
/// <returns>
24+
/// Returns this <see cref="OpenXmlEditingPipeline"/> instance to enable method chaining.
25+
/// </returns>
26+
/// <remarks>
27+
/// Modifications are queued in the pipeline and not written to the file until SaveChanges or SaveChangesAsync is called.
28+
/// </remarks>
29+
public OpenXmlEditingPipeline UpdateCellStyle(string cellReference, Action<OpenXmlCellStyle> updateCellCallback, string? sheetName = null)
30+
{
31+
if (updateCellCallback is null)
32+
throw new ArgumentNullException(nameof(updateCellCallback));
33+
34+
var style = new OpenXmlCellStyle();
35+
updateCellCallback(style);
36+
37+
return UpdateCellStyle(cellReference, style, sheetName);
38+
}
39+
40+
/// <summary>
41+
/// Updates the style of a cell using a pre-configured <see cref="OpenXmlCellStyle"/> object.
42+
/// </summary>
43+
/// <param name="cellReference">The cell reference in standard Excel format (e.g., "A1", "B5").</param>
44+
/// <param name="cellStyle">The <see cref="OpenXmlCellStyle"/> object containing the style properties to apply to the cell.</param>
45+
/// <param name="sheetName"> The name of the worksheet to update. If null or not specified, the first sheet in the workbook is used.</param>
46+
/// <returns>
47+
/// Returns this <see cref="OpenXmlEditingPipeline"/> instance to enable method chaining.
48+
/// </returns>
49+
/// <remarks>
50+
/// Modifications are queued in the pipeline and not written to the file until SaveChanges or SaveChangesAsync is called.
51+
/// </remarks>
52+
public OpenXmlEditingPipeline UpdateCellStyle(string cellReference, OpenXmlCellStyle cellStyle, string? sheetName = null)
53+
{
54+
_internals.UpdateCellStyle(cellReference, cellStyle, sheetName);
55+
return this;
56+
}
57+
58+
/// <summary>
59+
/// Applies all queued modifications to the Excel document and saves it to the original stream or file.
60+
/// The pipeline cannot be reused afterwards.
61+
/// </summary>
62+
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
63+
/// <remarks>
64+
/// If the pipeline was created from a stream with <c>leaveOpen: true</c>, the caller is responsible for its disposal.
65+
/// </remarks>
66+
[CreateSyncVersion]
67+
public async Task SaveChangesAsync(CancellationToken cancellationToken = default)
68+
{
69+
await _internals.SaveAsync(cancellationToken).ConfigureAwait(false);
70+
}
71+
72+
/// <summary>
73+
/// Applies all queued modifications to the Excel document and saves it to the provided path.
74+
/// The pipeline cannot be reused afterwards.
75+
/// </summary>
76+
/// <param name="outputPath">The path to save the modified Excel document to.</param>
77+
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
78+
/// <remarks>
79+
/// If the pipeline was created from a stream with <c>leaveOpen: true</c>, the caller is responsible for its disposal.
80+
/// </remarks>
81+
[CreateSyncVersion]
82+
public async Task SaveChangesAsync(string outputPath, CancellationToken cancellationToken = default)
83+
{
84+
var stream = File.OpenWrite(outputPath);
85+
await using var disposableStream = stream.ConfigureAwait(false);
86+
87+
await SaveChangesAsync(stream, cancellationToken).ConfigureAwait(false);
88+
}
89+
90+
/// <summary>
91+
/// Applies all queued modifications to the Excel document and saves it to the provided stream.
92+
/// The pipeline cannot be reused afterwards.
93+
/// </summary>
94+
/// <param name="outputStream">The stream to save the modified Excel document to.</param>
95+
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
96+
/// <remarks>
97+
/// If the pipeline was created from a stream with <c>leaveOpen: true</c>, the caller is responsible for its disposal.
98+
/// The caller is always responsible for disposing the output stream.
99+
/// </remarks>
100+
[CreateSyncVersion]
101+
public async Task SaveChangesAsync(Stream outputStream, CancellationToken cancellationToken = default)
102+
{
103+
await _internals.SaveAsync(outputStream, cancellationToken).ConfigureAwait(false);
104+
}
105+
}

0 commit comments

Comments
 (0)