This commit is contained in:
Wei Lin 2024-03-06 15:24:08 +08:00
commit 4a480727c2
18 changed files with 752 additions and 232 deletions

View File

@ -20,11 +20,6 @@
---
## 01/13 希望能更換掉民進黨
---
### Introduction
MiniExcel is simple and efficient to avoid OOM's .NET processing Excel tool.
@ -1126,7 +1121,42 @@ Since V1.26.0, we can set the attributes of Column dynamically
```
![image](https://user-images.githubusercontent.com/12729184/164510353-5aecbc4e-c3ce-41e8-b6cf-afd55eb23b68.png)
#### 8. DynamicSheetAttribute
Since V1.31.4 we can set the attributes of Sheet dynamically. We can set sheet name and state (visibility).
```csharp
var configuration = new OpenXmlConfiguration
{
DynamicSheets = new DynamicExcelSheet[] {
new DynamicExcelSheet("usersSheet") { Name = "Users", State = SheetState.Visible },
new DynamicExcelSheet("departmentSheet") { Name = "Departments", State = SheetState.Hidden }
}
};
var users = new[] { new { Name = "Jack", Age = 25 }, new { Name = "Mike", Age = 44 } };
var department = new[] { new { ID = "01", Name = "HR" }, new { ID = "02", Name = "IT" } };
var sheets = new Dictionary<string, object>
{
["usersSheet"] = users,
["departmentSheet"] = department
};
var path = PathHelper.GetTempPath();
MiniExcel.SaveAs(path, sheets, configuration: configuration);
```
We can also use new attribute ExcelSheetAttribute:
```C#
[ExcelSheet(Name = "Departments", State = SheetState.Hidden)]
private class DepartmentDto
{
[ExcelColumn(Name = "ID",Index = 0)]
public string ID { get; set; }
[ExcelColumn(Name = "Name",Index = 1)]
public string Name { get; set; }
}
```
### Add, Delete, Update
@ -1688,6 +1718,21 @@ foreach (var sheet in sheets)
![image](https://user-images.githubusercontent.com/12729184/116199841-2a1f5300-a76a-11eb-90a3-6710561cf6db.png)
#### Q. How to query or export information about sheet visibility?
A. `GetSheetInformations` method.
```csharp
var sheets = MiniExcel.GetSheetInformations(path);
foreach (var sheetInfo in sheets)
{
Console.WriteLine($"sheet index : {sheetInfo.Index} "); // next sheet index - numbered from 0
Console.WriteLine($"sheet name : {sheetInfo.Name} "); // sheet name
Console.WriteLine($"sheet state : {sheetInfo.State} "); // sheet visibility state - visible / hidden
}
```
#### Q. Whether to use Count will load all data into the memory?
@ -1859,7 +1904,8 @@ Thanks for providing a free All product IDE for this project ([License](https://
### Benefit
Link https://github.com/mini-software/MiniExcel/issues/560#issue-2080619180
### Contributors

View File

@ -1810,6 +1810,10 @@ public static DataTable QueryAsDataTableWithoutEmptyRow(Stream stream, bool useH
### 收益流水
目前收益 https://github.com/mini-software/MiniExcel/issues/560#issue-2080619180
### Contributors
![](https://contrib.rocks/image?repo=shps951023/MiniExcel)

View File

@ -1788,6 +1788,9 @@ public static DataTable QueryAsDataTableWithoutEmptyRow(Stream stream, bool useH
感謝提供免費IDE支持此專案 ([License](https://user-images.githubusercontent.com/12729184/123988233-6ab17c00-d9fa-11eb-8739-2a08c6a4a263.png))
### 收益流水
目前收益 https://github.com/mini-software/MiniExcel/issues/560#issue-2080619180
### Contributors

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,21 @@
using MiniExcelLibs.OpenXml;
using System;
namespace MiniExcelLibs.Attributes
{
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class ExcelSheetAttribute : Attribute
{
public string Name { get; set; }
public SheetState State { get; set; } = SheetState.Visible;
}
public class DynamicExcelSheet : ExcelSheetAttribute
{
public string Key { get; set; }
public DynamicExcelSheet(string key)
{
Key = key;
}
}
}

View File

@ -1,8 +1,6 @@
namespace MiniExcelLibs
{
using OpenXml;
using Utils;
using Zip;
using System;
using System.Collections;
using System.Collections.Generic;
@ -10,6 +8,8 @@
using System.Dynamic;
using System.IO;
using System.Linq;
using Utils;
using Zip;
public static partial class MiniExcel
{
@ -147,7 +147,7 @@
{
ExcelTemplateFactory.GetProvider(stream, configuration, excelType).MergeSameCells(filePath);
}
#endregion
/// <summary>
@ -217,6 +217,20 @@
return new ExcelOpenXmlSheetReader(stream, config).GetWorkbookRels(archive.entries).Select(s => s.Name).ToList();
}
public static List<SheetInfo> GetSheetInformations(string path, OpenXmlConfiguration config = null)
{
using (var stream = FileHelper.OpenSharedRead(path))
return GetSheetInformations(stream, config);
}
public static List<SheetInfo> GetSheetInformations(this Stream stream, OpenXmlConfiguration config = null)
{
config = config ?? OpenXmlConfiguration.DefaultConfig;
var archive = new ExcelOpenXmlZip(stream);
return new ExcelOpenXmlSheetReader(stream, config).GetWorkbookRels(archive.entries).Select((s, i) => s.ToSheetInfo((uint)i)).ToList();
}
public static ICollection<string> GetColumns(string path, bool useHeaderRow = false, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null)
{
using (var stream = FileHelper.OpenSharedRead(path))

View File

@ -8,7 +8,6 @@ using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
@ -25,7 +24,7 @@ namespace MiniExcelLibs.OpenXml
private MergeCells _mergeCells;
private ExcelOpenXmlStyles _style;
private readonly ExcelOpenXmlZip _archive;
private OpenXmlConfiguration _config;
private readonly OpenXmlConfiguration _config;
private static readonly XmlReaderSettings _xmlSettings = new XmlReaderSettings
{
@ -56,10 +55,19 @@ namespace MiniExcelLibs.OpenXml
if (sheetName != null)
{
SetWorkbookRels(_archive.entries);
var s = _sheetRecords.SingleOrDefault(_ => _.Name == sheetName);
if (s == null)
var sheetRecord = _sheetRecords.SingleOrDefault(_ => _.Name == sheetName);
if (sheetRecord == null && _config.DynamicSheets != null)
{
var sheetConfig = _config.DynamicSheets.FirstOrDefault(ds => ds.Key == sheetName);
if (sheetConfig != null)
{
sheetRecord = _sheetRecords.SingleOrDefault(_ => _.Name == sheetConfig.Name);
}
}
if (sheetRecord == null)
throw new InvalidOperationException("Please check sheetName/Index is correct");
sheetEntry = sheets.Single(w => w.FullName == $"xl/{s.Path}" || w.FullName == $"/xl/{s.Path}" || w.FullName == s.Path || s.Path == $"/{w.FullName}");
sheetEntry = sheets.Single(w => w.FullName == $"xl/{sheetRecord.Path}" || w.FullName == $"/xl/{sheetRecord.Path}" || w.FullName == sheetRecord.Path || sheetRecord.Path == $"/{w.FullName}");
}
else if (sheets.Count() > 1)
{
@ -402,6 +410,14 @@ namespace MiniExcelLibs.OpenXml
public IEnumerable<T> Query<T>(string sheetName, string startCell) where T : class, new()
{
if (sheetName == null)
{
var sheetInfo = CustomPropertyHelper.GetExcellSheetInfo(typeof(T), this._config);
if (sheetInfo != null)
{
sheetName = sheetInfo.ExcelSheetName;
}
}
return ExcelOpenXmlSheetReader.QueryImpl<T>(Query(false, sheetName, startCell), startCell, this._config);
}
@ -562,6 +578,7 @@ namespace MiniExcelLibs.OpenXml
{
yield return new SheetRecord(
reader.GetAttribute("name"),
reader.GetAttribute("state"),
uint.Parse(reader.GetAttribute("sheetId")),
XmlReaderHelper.GetAttribute(reader, "id", _relationshiopNs)
);

View File

@ -8,7 +8,6 @@ using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@ -34,6 +33,8 @@ namespace MiniExcelLibs.OpenXml
public string Name { get; set; }
public int SheetIdx { get; set; }
public string Path { get { return $"xl/worksheets/sheet{SheetIdx}.xml"; } }
public string State { get; set; }
}
internal class DrawingDto
{
@ -63,7 +64,8 @@ namespace MiniExcelLibs.OpenXml
this._archive = new MiniExcelZipArchive(_stream, ZipArchiveMode.Create, true, _utf8WithBom);
this._printHeader = printHeader;
this._value = value;
_sheets.Add(new SheetDto { Name = sheetName, SheetIdx = 1 }); //TODO:remove
var defaultSheetInfo = GetSheetInfos(sheetName);
_sheets.Add(defaultSheetInfo.ToDto(1)); //TODO:remove
}
public ExcelOpenXmlSheetWriter()
@ -82,12 +84,13 @@ namespace MiniExcelLibs.OpenXml
foreach (var sheet in sheets)
{
sheetId++;
var s = new SheetDto { Name = sheet.Key, SheetIdx = sheetId };
_sheets.Add(s); //TODO:remove
var sheetInfos = GetSheetInfos(sheet.Key);
var sheetDto = sheetInfos.ToDto(sheetId);
_sheets.Add(sheetDto); //TODO:remove
currentSheetIndex = sheetId;
CreateSheetXml(sheet.Value, s.Path);
CreateSheetXml(sheet.Value, sheetDto.Path);
}
}
else if (_value is DataSet)
@ -98,13 +101,13 @@ namespace MiniExcelLibs.OpenXml
foreach (DataTable dt in sheets.Tables)
{
sheetId++;
var s = new SheetDto { Name = dt.TableName, SheetIdx = sheetId };
_sheets.Add(s); //TODO:remove
var sheetPath = s.Path;
var sheetInfos = GetSheetInfos(dt.TableName);
var sheetDto = sheetInfos.ToDto(sheetId);
_sheets.Add(sheetDto); //TODO:remove
currentSheetIndex = sheetId;
CreateSheetXml(dt, sheetPath);
CreateSheetXml(dt, sheetDto.Path);
}
}
else
@ -119,6 +122,155 @@ namespace MiniExcelLibs.OpenXml
_archive.Dispose();
}
private void GenerateSheetByEnumerable(MiniExcelStreamWriter writer, IEnumerable values)
{
var maxColumnIndex = 0;
var maxRowIndex = 0;
List<ExcelColumnInfo> props = null;
string mode = null;
int? rowCount = null;
var collection = values as ICollection;
if (collection != null)
{
rowCount = collection.Count;
}
else if (!_configuration.FastMode)
{
// The row count is only required up front when not in fastmode
collection = new List<object>(values.Cast<object>());
rowCount = collection.Count;
}
// Get the enumerator once to ensure deferred linq execution
var enumerator = (collection ?? values).GetEnumerator();
// Move to the first item in order to inspect the value type and determine whether it is empty
var empty = !enumerator.MoveNext();
if (empty)
{
// only when empty IEnumerable need to check this issue #133 https://github.com/shps951023/MiniExcel/issues/133
var genericType = TypeHelper.GetGenericIEnumerables(values).FirstOrDefault();
if (genericType == null || genericType == typeof(object) // sometime generic type will be object, e.g: https://user-images.githubusercontent.com/12729184/132812859-52984314-44d1-4ee8-9487-2d1da159f1f0.png
|| typeof(IDictionary<string, object>).IsAssignableFrom(genericType)
|| typeof(IDictionary).IsAssignableFrom(genericType))
{
WriteEmptySheet(writer);
return;
}
else
{
SetGenericTypePropertiesMode(genericType, ref mode, out maxColumnIndex, out props);
}
}
else
{
var firstItem = enumerator.Current;
if (firstItem is IDictionary<string, object> genericDic)
{
mode = "IDictionary<string, object>";
props = GetDictionaryColumnInfo(genericDic, null);
maxColumnIndex = props.Count;
}
else if (firstItem is IDictionary dic)
{
mode = "IDictionary";
props = GetDictionaryColumnInfo(null, dic);
//maxColumnIndex = dic.Keys.Count;
maxColumnIndex = props.Count; // why not using keys, because ignore attribute ![image](https://user-images.githubusercontent.com/12729184/163686902-286abb70-877b-4e84-bd3b-001ad339a84a.png)
}
else
{
SetGenericTypePropertiesMode(firstItem.GetType(), ref mode, out maxColumnIndex, out props);
}
}
writer.Write($@"<?xml version=""1.0"" encoding=""utf-8""?><x:worksheet xmlns:r=""http://schemas.openxmlformats.org/officeDocument/2006/relationships"" xmlns:x=""http://schemas.openxmlformats.org/spreadsheetml/2006/main"" >");
long dimensionWritePosition = 0;
// We can write the dimensions directly if the row count is known
if (_configuration.FastMode && rowCount == null)
{
// Write a placeholder for the table dimensions and save thee position for later
dimensionWritePosition = writer.WriteAndFlush("<x:dimension ref=\"");
writer.Write(" />");
}
else
{
maxRowIndex = rowCount.Value + (_printHeader && rowCount > 0 ? 1 : 0);
writer.Write($@"<x:dimension ref=""{GetDimensionRef(maxRowIndex, maxColumnIndex)}""/>");
}
//cols:width
WriteColumnsWidths(writer, props);
//header
writer.Write($@"<x:sheetData>");
var yIndex = 1;
var xIndex = 1;
if (_printHeader)
{
PrintHeader(writer, props);
yIndex++;
}
if (!empty)
{
// body
if (mode == "IDictionary<string, object>") //Dapper Row
maxRowIndex = GenerateSheetByColumnInfo<IDictionary<string, object>>(writer, enumerator, props, xIndex, yIndex);
else if (mode == "IDictionary") //IDictionary
maxRowIndex = GenerateSheetByColumnInfo<IDictionary>(writer, enumerator, props, xIndex, yIndex);
else if (mode == "Properties")
maxRowIndex = GenerateSheetByColumnInfo<object>(writer, enumerator, props, xIndex, yIndex);
else
throw new NotImplementedException($"Type {values.GetType().FullName} is not implemented. Please open an issue.");
}
writer.Write("</x:sheetData>");
if (_configuration.AutoFilter)
writer.Write($"<x:autoFilter ref=\"{GetDimensionRef(maxRowIndex, maxColumnIndex)}\" />");
// The dimension has already been written if row count is defined
if (_configuration.FastMode && rowCount == null)
{
// Flush and save position so that we can get back again.
var pos = writer.Flush();
// Seek back and write the dimensions of the table
writer.SetPosition(dimensionWritePosition);
writer.WriteAndFlush($@"{GetDimensionRef(maxRowIndex, maxColumnIndex)}""");
writer.SetPosition(pos);
}
writer.Write("<x:drawing r:id=\"drawing" + currentSheetIndex + "\" /></x:worksheet>");
}
private static void PrintHeader(MiniExcelStreamWriter writer, List<ExcelColumnInfo> props)
{
var xIndex = 1;
var yIndex = 1;
writer.Write($"<x:row r=\"{yIndex}\">");
foreach (var p in props)
{
if (p == null)
{
xIndex++; //reason : https://github.com/shps951023/MiniExcel/issues/142
continue;
}
var r = ExcelOpenXmlUtils.ConvertXyToCell(xIndex, yIndex);
WriteC(writer, r, columnName: p.ExcelColumnName);
xIndex++;
}
writer.Write("</x:row>");
}
private void CreateSheetXml(object value, string sheetPath)
{
ZipArchiveEntry entry = _archive.CreateEntry(sheetPath, CompressionLevel.Fastest);
@ -131,10 +283,6 @@ namespace MiniExcelLibs.OpenXml
goto End; //for re-using code
}
var type = value.GetType();
Type genericType = null;
//DapperRow
if (value is IDataReader)
@ -143,137 +291,7 @@ namespace MiniExcelLibs.OpenXml
}
else if (value is IEnumerable)
{
var values = value as IEnumerable;
// try to get type from reflection
// genericType = null
var rowCount = 0;
var maxColumnIndex = 0;
//List<object> keys = new List<object>();
List<ExcelColumnInfo> props = null;
string mode = null;
// reason : https://stackoverflow.com/questions/66797421/how-replace-top-format-mark-after-MiniExcelStreamWriter-writing
// check mode & get maxRowCount & maxColumnIndex
{
foreach (var item in values) //TODO: need to optimize
{
rowCount = checked(rowCount + 1);
//TODO: if item is null but it's collection<T>, it can get T type from reflection
if (item != null && mode == null)
{
if (item is IDictionary<string, object>)
{
mode = "IDictionary<string, object>";
var dic = item as IDictionary<string, object>;
props = GetDictionaryColumnInfo(dic, null);
maxColumnIndex = props.Count;
}
else if (item is IDictionary)
{
var dic = item as IDictionary;
mode = "IDictionary";
props = GetDictionaryColumnInfo(null, dic);
//maxColumnIndex = dic.Keys.Count;
maxColumnIndex = props.Count; // why not using keys, because ignore attribute ![image](https://user-images.githubusercontent.com/12729184/163686902-286abb70-877b-4e84-bd3b-001ad339a84a.png)
}
else
{
var _t = item.GetType();
if (_t != genericType)
genericType = item.GetType();
genericType = item.GetType();
SetGenericTypePropertiesMode(genericType, ref mode, out maxColumnIndex, out props);
}
var collection = value as ICollection;
if (collection != null)
{
rowCount = checked((value as ICollection).Count);
break;
}
continue;
}
}
}
if (rowCount == 0)
{
// only when empty IEnumerable need to check this issue #133 https://github.com/shps951023/MiniExcel/issues/133
genericType = TypeHelper.GetGenericIEnumerables(values).FirstOrDefault();
if (genericType == null || genericType == typeof(object) // sometime generic type will be object, e.g: https://user-images.githubusercontent.com/12729184/132812859-52984314-44d1-4ee8-9487-2d1da159f1f0.png
|| typeof(IDictionary<string, object>).IsAssignableFrom(genericType)
|| typeof(IDictionary).IsAssignableFrom(genericType))
{
WriteEmptySheet(writer);
goto End; //for re-using code
}
else
{
SetGenericTypePropertiesMode(genericType, ref mode, out maxColumnIndex, out props);
}
}
writer.Write($@"<?xml version=""1.0"" encoding=""utf-8""?><x:worksheet xmlns:r=""http://schemas.openxmlformats.org/officeDocument/2006/relationships"" xmlns:x=""http://schemas.openxmlformats.org/spreadsheetml/2006/main"" >");
// dimension
var maxRowIndex = rowCount + (_printHeader && rowCount > 0 ? 1 : 0); //TODO:it can optimize
writer.Write($@"<x:dimension ref=""{GetDimensionRef(maxRowIndex, maxColumnIndex)}""/>");
//cols:width
var ecwProp = props?.Where(x => x?.ExcelColumnWidth != null).ToList();
if (ecwProp != null && ecwProp.Count > 0)
{
writer.Write($@"<x:cols>");
foreach (var p in ecwProp)
{
writer.Write($@"<x:col min=""{p.ExcelColumnIndex + 1}"" max=""{p.ExcelColumnIndex + 1}"" width=""{p.ExcelColumnWidth}"" customWidth=""1"" />");
}
writer.Write($@"</x:cols>");
}
//header
writer.Write($@"<x:sheetData>");
var yIndex = 1;
var xIndex = 1;
if (_printHeader)
{
var cellIndex = xIndex;
writer.Write($"<x:row r=\"{yIndex}\">");
foreach (var p in props)
{
if (p == null)
{
cellIndex++; //reason : https://github.com/shps951023/MiniExcel/issues/142
continue;
}
var r = ExcelOpenXmlUtils.ConvertXyToCell(cellIndex, yIndex);
WriteC(writer, r, columnName: p.ExcelColumnName);
cellIndex++;
}
writer.Write($"</x:row>");
yIndex++;
}
// body
if (mode == "IDictionary<string, object>") //Dapper Row
GenerateSheetByColumnInfo<IDictionary<string, object>>(writer, value as IEnumerable, props, xIndex, yIndex);
else if (mode == "IDictionary") //IDictionary
GenerateSheetByColumnInfo<IDictionary>(writer, value as IEnumerable, props, xIndex, yIndex);
else if (mode == "Properties")
GenerateSheetByColumnInfo<object>(writer, value as IEnumerable, props, xIndex, yIndex);
else
throw new NotImplementedException($"Type {type.Name} & genericType {genericType.Name} not Implemented. please issue for me.");
writer.Write("</x:sheetData>");
if (_configuration.AutoFilter)
writer.Write($"<x:autoFilter ref=\"A1:{ExcelOpenXmlUtils.ConvertXyToCell(maxColumnIndex, maxRowIndex == 0 ? 1 : maxRowIndex)}\" />");
writer.Write("<x:drawing r:id=\"drawing" + currentSheetIndex + "\" /></x:worksheet>");
GenerateSheetByEnumerable(writer, value as IEnumerable);
}
else if (value is DataTable)
{
@ -281,7 +299,7 @@ namespace MiniExcelLibs.OpenXml
}
else
{
throw new NotImplementedException($"Type {type.Name} & genericType {genericType.Name} not Implemented. please issue for me.");
throw new NotImplementedException($"Type {value.GetType().FullName} is not implemented. Please open an issue.");
}
}
End: //for re-using code
@ -353,12 +371,15 @@ namespace MiniExcelLibs.OpenXml
{
writer.Write($@"<?xml version=""1.0"" encoding=""utf-8""?><x:worksheet xmlns:x=""http://schemas.openxmlformats.org/spreadsheetml/2006/main""><x:dimension ref=""A1""/><x:sheetData></x:sheetData></x:worksheet>");
}
private void GenerateSheetByColumnInfo<T>(MiniExcelStreamWriter writer, IEnumerable value, List<ExcelColumnInfo> props, int xIndex = 1, int yIndex = 1)
private int GenerateSheetByColumnInfo<T>(MiniExcelStreamWriter writer, IEnumerator value, List<ExcelColumnInfo> props, int xIndex = 1, int yIndex = 1)
{
var isDic = typeof(T) == typeof(IDictionary);
var isDapperRow = typeof(T) == typeof(IDictionary<string, object>);
foreach (T v in value)
do
{
// The enumerator has already moved to the first item
T v = (T)value.Current;
writer.Write($"<x:row r=\"{yIndex}\">");
var cellIndex = xIndex;
foreach (var p in props)
@ -390,7 +411,9 @@ namespace MiniExcelLibs.OpenXml
}
writer.Write($"</x:row>");
yIndex++;
}
} while (value.MoveNext());
return yIndex - 1;
}
private void WriteCell(MiniExcelStreamWriter writer, int rowIndex, int cellIndex, object value, ExcelColumnInfo p)
@ -560,18 +583,30 @@ namespace MiniExcelLibs.OpenXml
// dimension
var maxRowIndex = value.Rows.Count + (_printHeader && value.Rows.Count > 0 ? 1 : 0);
var maxColumnIndex = value.Columns.Count;
writer.Write($@"<x:dimension ref=""{GetDimensionRef(maxRowIndex, maxColumnIndex)}""/><x:sheetData>");
writer.Write($@"<x:dimension ref=""{GetDimensionRef(maxRowIndex, maxColumnIndex)}""/>");
var props = new List<ExcelColumnInfo>();
for (var i = 0; i < value.Columns.Count; i++)
{
var columnName = value.Columns[i].Caption ?? value.Columns[i].ColumnName;
var prop = GetColumnInfosFromDynamicConfiguration(columnName);
props.Add(prop);
}
WriteColumnsWidths(writer, props);
writer.Write("<x:sheetData>");
if (_printHeader)
{
writer.Write($"<x:row r=\"{yIndex}\">");
var xIndex = xy.Item1;
foreach (DataColumn c in value.Columns)
foreach (var p in props)
{
var r = ExcelOpenXmlUtils.ConvertXyToCell(xIndex, yIndex);
WriteC(writer, r, columnName: c.Caption ?? c.ColumnName);
WriteC(writer, r, columnName: p.ExcelColumnName);
xIndex++;
}
writer.Write($"</x:row>");
yIndex++;
}
@ -596,11 +631,12 @@ namespace MiniExcelLibs.OpenXml
private void GenerateSheetByIDataReader(MiniExcelStreamWriter writer, IDataReader reader)
{
var xy = ExcelOpenXmlUtils.ConvertCellToXY("A1"); /*TODO:code smell*/
long dimensionWritePosition = 0;
writer.Write($@"<?xml version=""1.0"" encoding=""utf-8""?><x:worksheet xmlns:x=""http://schemas.openxmlformats.org/spreadsheetml/2006/main"">");
var yIndex = xy.Item2;
var xIndex = 0;
var xIndex = 1;
var yIndex = 1;
var maxColumnIndex = 0;
var maxRowIndex = 0;
{
if (_configuration.FastMode)
@ -613,9 +649,10 @@ namespace MiniExcelLibs.OpenXml
for (var i = 0; i < reader.FieldCount; i++)
{
var columnName = reader.GetName(i);
var prop = GetColumnInfosForIDataReader(columnName);
var prop = GetColumnInfosFromDynamicConfiguration(columnName);
props.Add(prop);
}
maxColumnIndex = props.Count;
WriteColumnsWidths(writer, props);
@ -623,23 +660,14 @@ namespace MiniExcelLibs.OpenXml
int fieldCount = reader.FieldCount;
if (_printHeader)
{
writer.Write($"<x:row r=\"{yIndex}\">");
xIndex = xy.Item1;
foreach (var p in props)
{
var r = ExcelOpenXmlUtils.ConvertXyToCell(xIndex, yIndex);
WriteC(writer, r, columnName: p.ExcelColumnName);
xIndex++;
}
writer.Write($"</x:row>");
PrintHeader(writer, props);
yIndex++;
}
while (reader.Read())
{
writer.Write($"<x:row r=\"{yIndex}\">");
xIndex = xy.Item1;
xIndex = 1;
for (int i = 0; i < fieldCount; i++)
{
var cellValue = reader.GetValue(i);
@ -649,20 +677,23 @@ namespace MiniExcelLibs.OpenXml
writer.Write($"</x:row>");
yIndex++;
}
// Subtract 1 because cell indexing starts with 1
maxRowIndex = yIndex - 1;
}
writer.Write("</x:sheetData>");
if (_configuration.AutoFilter)
writer.Write($"<x:autoFilter ref=\"A1:{ExcelOpenXmlUtils.ConvertXyToCell((xIndex - 1)/*TODO:code smell*/, yIndex - 1)}\" />");
writer.Write($"<x:autoFilter ref=\"{GetDimensionRef(maxRowIndex, maxColumnIndex)}\" />");
writer.WriteAndFlush("</x:worksheet>");
if (_configuration.FastMode)
{
writer.SetPosition(dimensionWritePosition);
writer.WriteAndFlush($@"A1:{ExcelOpenXmlUtils.ConvertXyToCell((xIndex - 1)/*TODO:code smell*/, yIndex - 1)}""");
writer.WriteAndFlush($@"{GetDimensionRef(maxRowIndex, maxColumnIndex)}""");
}
}
private ExcelColumnInfo GetColumnInfosForIDataReader(string columnName)
private ExcelColumnInfo GetColumnInfosFromDynamicConfiguration(string columnName)
{
var prop = new ExcelColumnInfo
{
@ -695,6 +726,31 @@ namespace MiniExcelLibs.OpenXml
return prop;
}
private ExcellSheetInfo GetSheetInfos(string sheetName)
{
var info = new ExcellSheetInfo
{
ExcelSheetName = sheetName,
Key = sheetName,
ExcelSheetState = SheetState.Visible
};
if (_configuration.DynamicSheets == null || _configuration.DynamicSheets.Length <= 0)
return info;
var dynamicSheet = _configuration.DynamicSheets.SingleOrDefault(_ => _.Key == sheetName);
if (dynamicSheet == null)
{
return info;
}
if (dynamicSheet.Name != null)
info.ExcelSheetName = dynamicSheet.Name;
info.ExcelSheetState = dynamicSheet.State;
return info;
}
private static void WriteColumnsWidths(MiniExcelStreamWriter writer, IEnumerable<ExcelColumnInfo> props)
{
var ecwProps = props.Where(x => x?.ExcelColumnWidth != null).ToList();
@ -812,7 +868,14 @@ namespace MiniExcelLibs.OpenXml
foreach (var s in _sheets)
{
sheetId++;
workbookXml.AppendLine($@"<x:sheet name=""{s.Name}"" sheetId=""{sheetId}"" r:id=""{s.ID}"" />");
if (string.IsNullOrEmpty(s.State))
{
workbookXml.AppendLine($@"<x:sheet name=""{s.Name}"" sheetId=""{sheetId}"" r:id=""{s.ID}"" />");
}
else
{
workbookXml.AppendLine($@"<x:sheet name=""{s.Name}"" sheetId=""{sheetId}"" state=""{s.State}"" r:id=""{s.ID}"" />");
}
workbookRelsXml.AppendLine($@"<Relationship Type=""http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"" Target=""/{s.Path}"" Id=""{s.ID}"" />");
//TODO: support multiple drawing

View File

@ -8,7 +8,7 @@ namespace MiniExcelLibs.OpenXml
{
private readonly Stream _stream;
private readonly Encoding _encoding;
internal readonly StreamWriter _streamWriter;
private readonly StreamWriter _streamWriter;
private bool disposedValue;
public MiniExcelStreamWriter(Stream stream,Encoding encoding, int bufferSize)
{
@ -30,6 +30,12 @@ namespace MiniExcelLibs.OpenXml
return this._streamWriter.BaseStream.Position;
}
public long Flush()
{
this._streamWriter.Flush();
return this._streamWriter.BaseStream.Position;
}
public void SetPosition(long position)
{
this._streamWriter.BaseStream.Position = position;

View File

@ -1,10 +1,4 @@

using MiniExcelLibs.Utils;
using System.Collections.Generic;
using System;
using System.ComponentModel;
using MiniExcelLibs.Attributes;
using MiniExcelLibs.Attributes;
namespace MiniExcelLibs.OpenXml
{
@ -19,5 +13,6 @@ namespace MiniExcelLibs.OpenXml
public bool EnableWriteNullValueCell { get; set; } = true;
public bool EnableSharedStringCache { get; set; } = true;
public long SharedStringCacheSize { get; set; } = 5 * 1024 * 1024;
public DynamicExcelSheet[] DynamicSheets { get; set; }
}
}

View File

@ -0,0 +1,32 @@
namespace MiniExcelLibs.OpenXml
{
public class SheetInfo
{
public SheetInfo(uint id, uint index, string name, SheetState sheetState)
{
Id = id;
Index = index;
Name = name;
State = sheetState;
}
/// <summary>
/// Internal sheet id - depends on the order in which the sheet is added
/// </summary>
public uint Id { get; }
/// <summary>
/// Next sheet index - numbered from 0
/// </summary>
public uint Index { get; }
/// <summary>
/// Sheet name
/// </summary>
public string Name { get; }
/// <summary>
/// Sheet visibility state
/// </summary>
public SheetState State { get; }
}
public enum SheetState { Visible, Hidden, VeryHidden }
}

View File

@ -1,20 +1,38 @@
namespace MiniExcelLibs.OpenXml
using System;
namespace MiniExcelLibs.OpenXml
{
internal sealed class SheetRecord
{
public SheetRecord(string name, uint id, string rid)
public SheetRecord(string name, string state, uint id, string rid)
{
Name = name;
State = state;
Id = id;
Rid = rid;
}
public string Name { get; }
public string State { get; set; }
public uint Id { get; }
public string Rid { get; set; }
public string Path { get; set; }
public SheetInfo ToSheetInfo(uint index)
{
if (string.IsNullOrEmpty(State))
{
return new SheetInfo(Id, index, Name, SheetState.Visible);
}
if (Enum.TryParse(State, true, out SheetState stateEnum))
{
return new SheetInfo(Id, index, Name, stateEnum);
}
throw new ArgumentException($"Unable to parse sheet state. Sheet name: {Name}");
}
}
}

View File

@ -1,10 +1,10 @@
namespace MiniExcelLibs.Utils
{
using MiniExcelLibs.Attributes;
using MiniExcelLibs.OpenXml;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Dynamic;
using System.Linq;
using System.Reflection;
@ -23,6 +23,26 @@
public bool ExcelIgnore { get; internal set; }
}
internal class ExcellSheetInfo
{
public object Key { get; set; }
public string ExcelSheetName { get; set; }
public SheetState ExcelSheetState { get; set; }
private string ExcelSheetStateAsString
{
get
{
return ExcelSheetState.ToString().ToLower();
}
}
public SheetDto ToDto(int sheetIndex)
{
return new SheetDto { Name = ExcelSheetName, SheetIdx = sheetIndex, State = ExcelSheetStateAsString };
}
}
internal static partial class CustomPropertyHelper
{
internal static IDictionary<string, object> GetEmptyExpandoObject(int maxColumnIndex, int startCellIndex)
@ -142,9 +162,9 @@
DescriptionAttribute[] attributes = (DescriptionAttribute[])fi.GetCustomAttributes(
typeof(DescriptionAttribute), false);
if (attributes != null && attributes.Length > 0)
if (attributes != null && attributes.Length > 0)
return attributes[0].Description;
else
else
return source.ToString();
}
@ -185,7 +205,7 @@
ExcelColumnWidth = p.GetAttribute<ExcelColumnWidthAttribute>()?.ExcelColumnWidth ?? excelColumn?.Width,
ExcelFormat = excelFormat ?? excelColumn?.Format,
};
}).Where(_=>_!=null);
}).Where(_ => _ != null);
}
private static IEnumerable<ExcelColumnInfo> GetExcelPropertyInfo(Type type, BindingFlags bindingFlags, Configuration configuration)
@ -194,6 +214,38 @@
return ConvertToExcelCustomPropertyInfo(type.GetProperties(bindingFlags), configuration);
}
}
internal static ExcellSheetInfo GetExcellSheetInfo(Type type, Configuration configuration)
{
// default options
var sheetInfo = new ExcellSheetInfo()
{
Key = type.Name,
ExcelSheetName = null, // will be generated automatically as Sheet<Index>
ExcelSheetState = SheetState.Visible
};
// options from ExcelSheetAttribute
ExcelSheetAttribute excelSheetAttribute = type.GetCustomAttribute(typeof(ExcelSheetAttribute)) as ExcelSheetAttribute;
if (excelSheetAttribute != null)
{
sheetInfo.ExcelSheetName = excelSheetAttribute.Name ?? type.Name;
sheetInfo.ExcelSheetState = excelSheetAttribute.State;
}
// options from DynamicSheets configuration
OpenXmlConfiguration openXmlCOnfiguration = configuration as OpenXmlConfiguration;
if (openXmlCOnfiguration != null && openXmlCOnfiguration.DynamicSheets != null && openXmlCOnfiguration.DynamicSheets.Length > 0)
{
var dynamicSheet = openXmlCOnfiguration.DynamicSheets.SingleOrDefault(_ => _.Key == type.Name);
if (dynamicSheet != null)
{
sheetInfo.ExcelSheetName = dynamicSheet.Name;
sheetInfo.ExcelSheetState = dynamicSheet.State;
}
}
return sheetInfo;
}
}
}

View File

@ -140,13 +140,13 @@
{
newValue = XmlEncoder.DecodeString(itemValue?.ToString());
}
else if (pInfo.Property.Info.PropertyType.IsEnum)
else if (pInfo.ExcludeNullableType.IsEnum)
{
var fieldInfo = pInfo.Property.Info.PropertyType.GetFields().FirstOrDefault(e => e.GetCustomAttribute<DescriptionAttribute>(false)?.Description == itemValue?.ToString());
var fieldInfo = pInfo.ExcludeNullableType.GetFields().FirstOrDefault(e => e.GetCustomAttribute<DescriptionAttribute>(false)?.Description == itemValue?.ToString());
if (fieldInfo != null)
newValue = Enum.Parse(pInfo.Property.Info.PropertyType, fieldInfo.Name, true);
newValue = Enum.Parse(pInfo.ExcludeNullableType, fieldInfo.Name, true);
else
newValue = Enum.Parse(pInfo.Property.Info.PropertyType, itemValue?.ToString(), true);
newValue = Enum.Parse(pInfo.ExcludeNullableType, itemValue?.ToString(), true);
}
else
{

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,10 @@
using Xunit;
using System.Linq;
using MiniExcelLibs.Attributes;
using MiniExcelLibs.OpenXml;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Xunit;
namespace MiniExcelLibs.Tests
{
@ -84,7 +87,7 @@ namespace MiniExcelLibs.Tests
{
var rows = MiniExcel.Query(path, sheetName: sheetName);
}
Assert.Equal(new[] { "Sheet2", "Sheet1", "Sheet3" }, sheetNames);
}
@ -100,5 +103,186 @@ namespace MiniExcelLibs.Tests
}
}
}
[ExcelSheet(Name = "Users")]
private class UserDto
{
public string Name { get; set; }
public int Age { get; set; }
}
[ExcelSheet(Name = "Departments", State = SheetState.Hidden)]
private class DepartmentDto
{
public string ID { get; set; }
public string Name { get; set; }
}
[Fact]
public void ExcelSheetAttributeIsUsedWhenReadExcel()
{
var path = @"../../../../../samples/xlsx/TestDynamicSheet.xlsx";
using (var stream = File.OpenRead(path))
{
var users = stream.Query<UserDto>().ToList();
Assert.Equal(2, users.Count());
Assert.Equal("Jack", users[0].Name);
var departments = stream.Query<DepartmentDto>().ToList();
Assert.Equal(2, departments.Count());
Assert.Equal("HR", departments[0].Name);
}
{
var users = MiniExcel.Query<UserDto>(path).ToList();
Assert.Equal(2, users.Count());
Assert.Equal("Jack", users[0].Name);
var departments = MiniExcel.Query<DepartmentDto>(path).ToList();
Assert.Equal(2, departments.Count());
Assert.Equal("HR", departments[0].Name);
}
}
[Fact]
public void DynamicSheetConfigurationIsUsedWhenReadExcel()
{
var configuration = new OpenXmlConfiguration
{
DynamicSheets = new[]
{
new DynamicExcelSheet("usersSheet") { Name = "Users" },
new DynamicExcelSheet("departmentSheet") { Name = "Departments" }
}
};
var path = @"../../../../../samples/xlsx/TestDynamicSheet.xlsx";
using (var stream = File.OpenRead(path))
{
// take first sheet as default
var users = stream.Query(configuration: configuration, useHeaderRow: true).ToList();
Assert.Equal(2, users.Count());
Assert.Equal("Jack", users[0].Name);
// take second sheet by sheet name
var departments = stream.Query(sheetName: "Departments", configuration: configuration, useHeaderRow: true).ToList();
Assert.Equal(2, departments.Count());
Assert.Equal("HR", departments[0].Name);
// take second sheet by sheet key
departments = stream.Query(sheetName: "departmentSheet", configuration: configuration, useHeaderRow: true).ToList();
Assert.Equal(2, departments.Count());
Assert.Equal("HR", departments[0].Name);
}
{
// take first sheet as default
var users = MiniExcel.Query(path, configuration: configuration, useHeaderRow: true).ToList();
Assert.Equal(2, users.Count());
Assert.Equal("Jack", users[0].Name);
// take second sheet by sheet name
var departments = MiniExcel.Query(path, sheetName: "Departments", configuration: configuration, useHeaderRow: true).ToList();
Assert.Equal(2, departments.Count());
Assert.Equal("HR", departments[0].Name);
// take second sheet by sheet key
departments = MiniExcel.Query(path, sheetName: "departmentSheet", configuration: configuration, useHeaderRow: true).ToList();
Assert.Equal(2, departments.Count());
Assert.Equal("HR", departments[0].Name);
}
}
[Fact]
public void ReadSheetVisibilityStateTest()
{
var path = @"../../../../../samples/xlsx/TestMultiSheetWithHiddenSheet.xlsx";
{
var sheetInfos = MiniExcel.GetSheetInformations(path).ToList();
Assert.Collection(sheetInfos,
i =>
{
Assert.Equal(0u, i.Index);
Assert.Equal(2u, i.Id);
Assert.Equal(SheetState.Visible, i.State);
Assert.Equal("Sheet2", i.Name);
},
i =>
{
Assert.Equal(1u, i.Index);
Assert.Equal(1u, i.Id);
Assert.Equal(SheetState.Visible, i.State);
Assert.Equal("Sheet1", i.Name);
},
i =>
{
Assert.Equal(2u, i.Index);
Assert.Equal(3u, i.Id);
Assert.Equal(SheetState.Visible, i.State);
Assert.Equal("Sheet3", i.Name);
},
i =>
{
Assert.Equal(3u, i.Index);
Assert.Equal(5u, i.Id);
Assert.Equal(SheetState.Hidden, i.State);
Assert.Equal("HiddenSheet4", i.Name);
});
}
}
[Fact]
public void WriteHiddenSheetTest()
{
var configuration = new OpenXmlConfiguration
{
DynamicSheets = new[]
{
new DynamicExcelSheet("usersSheet")
{
Name = "Users",
State = SheetState.Visible
},
new DynamicExcelSheet("departmentSheet")
{
Name = "Departments",
State = SheetState.Hidden
}
}
};
var users = new[] { new { Name = "Jack", Age = 25 }, new { Name = "Mike", Age = 44 } };
var department = new[] { new { ID = "01", Name = "HR" }, new { ID = "02", Name = "IT" } };
var sheets = new Dictionary<string, object>
{
["usersSheet"] = users,
["departmentSheet"] = department
};
string path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.xlsx");
MiniExcel.SaveAs(path, sheets, configuration: configuration);
var sheetInfos = MiniExcel.GetSheetInformations(path).ToList();
Assert.Collection(sheetInfos,
i =>
{
Assert.Equal(0u, i.Index);
Assert.Equal(1u, i.Id);
Assert.Equal(SheetState.Visible, i.State);
Assert.Equal("Users", i.Name);
},
i =>
{
Assert.Equal(1u, i.Index);
Assert.Equal(2u, i.Id);
Assert.Equal(SheetState.Hidden, i.State);
Assert.Equal("Departments", i.Name);
});
foreach (var sheetName in sheetInfos.Select(s => s.Name))
{
var rows = MiniExcel.Query(path, sheetName: sheetName);
}
}
}
}

View File

@ -1242,7 +1242,60 @@ namespace MiniExcelLibs.Tests
Assert.Contains("Its value", rows[0]);
Assert.Contains("Name of something", rows[1]);
Assert.Contains("Its value", rows[1]);
Assert.Equal("MiniExcel", rows[0]["Name of something"]);
Assert.Equal(1D, rows[0]["Its value"]);
Assert.Equal("Github", rows[1]["Name of something"]);
Assert.Equal(2D, rows[1]["Its value"]);
}
}
[Fact]
public void DynamicColumnsConfigurationIsUsedWhenCreatingExcelUsingDataTable()
{
var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.xlsx");
var table = new DataTable();
{
table.Columns.Add("Column1", typeof(string));
table.Columns.Add("Column2", typeof(int));
table.Rows.Add("MiniExcel", 1);
table.Rows.Add("Github", 2);
}
var configuration = new OpenXmlConfiguration
{
DynamicColumns = new[]
{
new DynamicExcelColumn("Column1")
{
Name = "Name of something",
Index = 0,
Width = 150
},
new DynamicExcelColumn("Column2")
{
Name = "Its value",
Index = 1,
Width = 150
}
}
};
MiniExcel.SaveAs(path, table, configuration: configuration);
using (var stream = File.OpenRead(path))
{
var rows = stream.Query(useHeaderRow: true)
.Select(x => (IDictionary<string, object>)x)
.Select(x => (IDictionary<string, object>)x)
.ToList();
Assert.Contains("Name of something", rows[0]);
Assert.Contains("Its value", rows[0]);
Assert.Contains("Name of something", rows[1]);
Assert.Contains("Its value", rows[1]);
Assert.Equal("MiniExcel", rows[0]["Name of something"]);
Assert.Equal(1D, rows[0]["Its value"]);
Assert.Equal("Github", rows[1]["Name of something"]);