diff --git a/README.md b/README.md index f39e4471..e289ea1c 100644 --- a/README.md +++ b/README.md @@ -1121,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 + { + ["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 @@ -1683,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? diff --git a/samples/xlsx/TestDynamicSheet.xlsx b/samples/xlsx/TestDynamicSheet.xlsx new file mode 100644 index 00000000..97512abd Binary files /dev/null and b/samples/xlsx/TestDynamicSheet.xlsx differ diff --git a/samples/xlsx/TestMultiSheetWithHiddenSheet.xlsx b/samples/xlsx/TestMultiSheetWithHiddenSheet.xlsx new file mode 100644 index 00000000..4f720b62 Binary files /dev/null and b/samples/xlsx/TestMultiSheetWithHiddenSheet.xlsx differ diff --git a/src/MiniExcel/Attributes/ExcelSheetAttribute.cs b/src/MiniExcel/Attributes/ExcelSheetAttribute.cs new file mode 100644 index 00000000..b3f1b7d6 --- /dev/null +++ b/src/MiniExcel/Attributes/ExcelSheetAttribute.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/src/MiniExcel/MiniExcel.cs b/src/MiniExcel/MiniExcel.cs index 6751f03f..abf56496 100644 --- a/src/MiniExcel/MiniExcel.cs +++ b/src/MiniExcel/MiniExcel.cs @@ -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 @@ public static void MergeSameCells(this Stream stream, byte[] filePath, ExcelType { ExcelTemplateFactory.GetProvider(stream, configuration, excelType).MergeSameCells(filePath); } - + #endregion /// @@ -217,6 +217,20 @@ public static List GetSheetNames(this Stream stream, OpenXmlConfiguratio return new ExcelOpenXmlSheetReader(stream, config).GetWorkbookRels(archive.entries).Select(s => s.Name).ToList(); } + public static List GetSheetInformations(string path, OpenXmlConfiguration config = null) + { + using (var stream = FileHelper.OpenSharedRead(path)) + return GetSheetInformations(stream, config); + } + + public static List 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 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)) diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetReader.cs b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetReader.cs index 579a9bb7..73ace9e7 100644 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetReader.cs +++ b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetReader.cs @@ -8,7 +8,6 @@ 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 @@ internal class ExcelOpenXmlSheetReader : IExcelReader 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 @@ public IEnumerable> Query(bool useHeaderRow, string 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 @@ private void SetCellsValueAndHeaders(object cellValue, bool useHeaderRow, ref Di public IEnumerable Query(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(Query(false, sheetName, startCell), startCell, this._config); } @@ -562,6 +578,7 @@ internal IEnumerable ReadWorkbook(ReadOnlyCollection _.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 props) { var ecwProps = props.Where(x => x?.ExcelColumnWidth != null).ToList(); @@ -839,7 +868,14 @@ private void GenerateEndXml() foreach (var s in _sheets) { sheetId++; - workbookXml.AppendLine($@""); + if (string.IsNullOrEmpty(s.State)) + { + workbookXml.AppendLine($@""); + } + else + { + workbookXml.AppendLine($@""); + } workbookRelsXml.AppendLine($@""); //TODO: support multiple drawing diff --git a/src/MiniExcel/OpenXml/OpenXmlConfiguration.cs b/src/MiniExcel/OpenXml/OpenXmlConfiguration.cs index 1c1a538a..d4d320ac 100644 --- a/src/MiniExcel/OpenXml/OpenXmlConfiguration.cs +++ b/src/MiniExcel/OpenXml/OpenXmlConfiguration.cs @@ -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 @@ public class OpenXmlConfiguration : Configuration 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; } } } \ No newline at end of file diff --git a/src/MiniExcel/OpenXml/SheetInfo.cs b/src/MiniExcel/OpenXml/SheetInfo.cs new file mode 100644 index 00000000..b0fddb32 --- /dev/null +++ b/src/MiniExcel/OpenXml/SheetInfo.cs @@ -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; + } + + /// + /// Internal sheet id - depends on the order in which the sheet is added + /// + public uint Id { get; } + /// + /// Next sheet index - numbered from 0 + /// + public uint Index { get; } + /// + /// Sheet name + /// + public string Name { get; } + /// + /// Sheet visibility state + /// + public SheetState State { get; } + } + + public enum SheetState { Visible, Hidden, VeryHidden } +} diff --git a/src/MiniExcel/OpenXml/SheetRecord.cs b/src/MiniExcel/OpenXml/SheetRecord.cs index c452a68b..a33b4ae7 100644 --- a/src/MiniExcel/OpenXml/SheetRecord.cs +++ b/src/MiniExcel/OpenXml/SheetRecord.cs @@ -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}"); + } } } diff --git a/src/MiniExcel/Utils/CustomPropertyHelper.cs b/src/MiniExcel/Utils/CustomPropertyHelper.cs index 1bf6e0b3..97f2b683 100644 --- a/src/MiniExcel/Utils/CustomPropertyHelper.cs +++ b/src/MiniExcel/Utils/CustomPropertyHelper.cs @@ -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 @@ internal class ExcelColumnInfo 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 GetEmptyExpandoObject(int maxColumnIndex, int startCellIndex) @@ -142,9 +162,9 @@ internal static string DescriptionAttr(Type type, object source) 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 @@ private static IEnumerable ConvertToExcelCustomPropertyInfo(Pro ExcelColumnWidth = p.GetAttribute()?.ExcelColumnWidth ?? excelColumn?.Width, ExcelFormat = excelFormat ?? excelColumn?.Format, }; - }).Where(_=>_!=null); + }).Where(_ => _ != null); } private static IEnumerable GetExcelPropertyInfo(Type type, BindingFlags bindingFlags, Configuration configuration) @@ -194,6 +214,38 @@ private static IEnumerable GetExcelPropertyInfo(Type type, Bind 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 + 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; + } + } } diff --git a/tests/MiniExcelTests/MiniExcelOpenXmlMultipleSheetTests.cs b/tests/MiniExcelTests/MiniExcelOpenXmlMultipleSheetTests.cs index 8a390d71..57c8c003 100644 --- a/tests/MiniExcelTests/MiniExcelOpenXmlMultipleSheetTests.cs +++ b/tests/MiniExcelTests/MiniExcelOpenXmlMultipleSheetTests.cs @@ -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 @@ public void MultiSheetsQueryTest() { var rows = MiniExcel.Query(path, sheetName: sheetName); } - + Assert.Equal(new[] { "Sheet2", "Sheet1", "Sheet3" }, sheetNames); } @@ -100,5 +103,186 @@ public void MultiSheetsQueryTest() } } } + + [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().ToList(); + Assert.Equal(2, users.Count()); + Assert.Equal("Jack", users[0].Name); + + var departments = stream.Query().ToList(); + Assert.Equal(2, departments.Count()); + Assert.Equal("HR", departments[0].Name); + } + + { + var users = MiniExcel.Query(path).ToList(); + Assert.Equal(2, users.Count()); + Assert.Equal("Jack", users[0].Name); + + var departments = MiniExcel.Query(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 + { + ["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); + } + } } } \ No newline at end of file