diff --git a/src/EPPlus/Core/Worksheet/ExcelTableCopyEventArgs.cs b/src/EPPlus/Core/Worksheet/ExcelTableCopyEventArgs.cs new file mode 100644 index 0000000000..c546dca825 --- /dev/null +++ b/src/EPPlus/Core/Worksheet/ExcelTableCopyEventArgs.cs @@ -0,0 +1,43 @@ +/************************************************************************************************* + Required Notice: Copyright (C) EPPlus Software AB. + This software is licensed under PolyForm Noncommercial License 1.0.0 + and may only be used for noncommercial purposes + https://polyformproject.org/licenses/noncommercial/1.0.0/ + + A commercial license to use this software can be purchased at https://epplussoftware.com +************************************************************************************************* + Date Author Change +************************************************************************************************* + 08/05/2026 EPPlus Software AB Added +*************************************************************************************************/ +namespace OfficeOpenXml.Core.Worksheet +{ + /// + /// Provides context for a table that is being copied to a new worksheet, and allows + /// a custom name to be assigned to the copied table. + /// + public class ExcelTableCopyEventArgs + { + /// + /// The name of the table on the source worksheet. + /// + public string SourceTableName { get; internal set; } + + /// + /// The name that was assigned to the copied table by default, before this handler + /// runs. When the worksheet is copied within the same workbook, this is a generated + /// name (Table1, Table2, ...). When copied to another workbook, the original name is + /// kept when it is still available, in which case this equals ; + /// if a table with that name already exists in the target workbook, a generated name + /// is used instead. + /// + public string DefaultName { get; internal set; } + + /// + /// The name to assign to the copied table. Leave as null to keep . + /// Setting this to an existing table name will cause the same validation exception + /// as a normal table name assignment. + /// + public string NewName { get; set; } + } +} \ No newline at end of file diff --git a/src/EPPlus/Core/Worksheet/ExcelWorksheetCopyOptions.cs b/src/EPPlus/Core/Worksheet/ExcelWorksheetCopyOptions.cs new file mode 100644 index 0000000000..4848995483 --- /dev/null +++ b/src/EPPlus/Core/Worksheet/ExcelWorksheetCopyOptions.cs @@ -0,0 +1,35 @@ +/************************************************************************************************* + Required Notice: Copyright (C) EPPlus Software AB. + This software is licensed under PolyForm Noncommercial License 1.0.0 + and may only be used for noncommercial purposes + https://polyformproject.org/licenses/noncommercial/1.0.0/ + + A commercial license to use this software can be purchased at https://epplussoftware.com +************************************************************************************************* + Date Author Change +************************************************************************************************* + 08/05/2026 EPPlus Software AB Added +*************************************************************************************************/ +using System; + +namespace OfficeOpenXml.Core.Worksheet +{ + /// + /// Used to specify options when copying a worksheet. + /// + public class ExcelWorksheetCopyOptions + { + internal static ExcelWorksheetCopyOptions Default => new ExcelWorksheetCopyOptions(); + + /// + /// A handler that is invoked for each table that is copied to the new worksheet. + /// Use this to assign a custom name to the copied table. When a worksheet is copied + /// within the same workbook, copied tables are otherwise given a generated name + /// (Table1, Table2, ...). Set on the + /// argument to rename the copied table. The rename is applied through the same path + /// as a normal assignment, so + /// formula references are updated and name uniqueness is validated. + /// + public Action TableCopyHandler { get; set; } + } +} diff --git a/src/EPPlus/Core/Worksheet/WorksheetCopyHelper.cs b/src/EPPlus/Core/Worksheet/WorksheetCopyHelper.cs index 45f5d1e932..05cdf6a85e 100644 --- a/src/EPPlus/Core/Worksheet/WorksheetCopyHelper.cs +++ b/src/EPPlus/Core/Worksheet/WorksheetCopyHelper.cs @@ -45,7 +45,7 @@ namespace OfficeOpenXml.Core.Worksheet { internal static class WorksheetCopyHelper { - internal static ExcelWorksheet Copy(ExcelWorksheets targetWorksheets, string name, ExcelWorksheet sourceWorksheet) + internal static ExcelWorksheet Copy(ExcelWorksheets targetWorksheets, string name, ExcelWorksheet sourceWorksheet, ExcelWorksheetCopyOptions options) { int sheetID; Uri uriWorksheet; @@ -111,9 +111,10 @@ internal static ExcelWorksheet Copy(ExcelWorksheets targetWorksheets, string nam CopySlicers(sourceWorksheet, targetWorksheet); CopyDrawing(sourceWorksheet, targetWorksheet); } + List> copiedTableNames = null; if (sourceWorksheet.Tables.Count > 0) { - CopyTable(sourceWorksheet, targetWorksheet); + copiedTableNames = CopyTable(sourceWorksheet, targetWorksheet); } if (sourceWorksheet.PivotTables.Count > 0) @@ -182,10 +183,50 @@ internal static ExcelWorksheet Copy(ExcelWorksheets targetWorksheets, string nam pageSetup.Attributes.Remove(attr); } } + //Apply any caller-supplied table names last, once the copy is fully + //materialized. Renaming earlier would break internal lookups in + //CopyDxfStyles and the slicer copy, which resolve the copied tables + //by their default name. + ApplyTableCopyOptions(targetWorksheet, options, copiedTableNames); return targetWorksheet; } + private static void ApplyTableCopyOptions(ExcelWorksheet added, ExcelWorksheetCopyOptions options, List> copiedTableNames) + { + if (options == null || options.TableCopyHandler == null || copiedTableNames == null) + { + return; + } + + foreach (var pair in copiedTableNames) + { + var sourceTableName = pair.Key; + var defaultName = pair.Value; + var copiedTable = added.Tables[defaultName]; + if (copiedTable == null) + { + continue; + } + + var args = new ExcelTableCopyEventArgs + { + SourceTableName = sourceTableName, + DefaultName = defaultName + }; + options.TableCopyHandler.Invoke(args); + + if (!string.IsNullOrEmpty(args.NewName) && args.NewName != defaultName) + { + //The copied worksheet is now internally consistent (table and formulas both + //use the default name), so routing through the ExcelTable.Name setter updates + //the table and its references token based, and validates name uniqueness, + //exactly as for a normal rename. + copiedTable.Name = args.NewName; + } + } + } + private static void SetTableFunction(ExcelWorksheet added) { foreach (var t in added.Tables) @@ -1024,26 +1065,27 @@ private static bool HasExternalReference(string formula) return false; } - private static void CopyTable(ExcelWorksheet Copy, ExcelWorksheet added) + private static List> CopyTable(ExcelWorksheet sourceWs, ExcelWorksheet destWs) { + var copiedTableNames = new List>(); string prevName = ""; //First copy the table XML - foreach (var tbl in Copy.Tables) + foreach (var tbl in sourceWs.Tables) { string xml = tbl.TableXml.OuterXml; string name; - if (Copy.Workbook == added.Workbook || added.Workbook.ExistsTableName(tbl.Name)) + if (sourceWs.Workbook == destWs.Workbook || destWs.Workbook.ExistsTableName(tbl.Name)) { if (prevName == "") { - name = Copy.Tables.GetNewTableName(); + name = sourceWs.Tables.GetNewTableName(); } else { int ix = int.Parse(prevName.Substring(5)) + 1; name = string.Format("Table{0}", ix); - while (added._package.Workbook.ExistsPivotTableName(name)) + while (destWs._package.Workbook.ExistsPivotTableName(name)) { name = string.Format("Table{0}", ++ix); } @@ -1055,10 +1097,12 @@ private static void CopyTable(ExcelWorksheet Copy, ExcelWorksheet added) } //ensure the _nextTableID value has been initialized - Pull request by WillR - added.Workbook.ReadAllTables(); + destWs.Workbook.ReadAllTables(); - int Id = added.Workbook._nextTableID++; + int Id = destWs.Workbook._nextTableID++; prevName = name; + copiedTableNames.Add(new KeyValuePair(tbl.Name, name)); + XmlDocument xmlDoc = new XmlDocument(); xmlDoc.LoadXml(xml); xmlDoc.SelectSingleNode("//d:table/@id", tbl.NameSpaceManager).Value = Id.ToString(); @@ -1067,34 +1111,34 @@ private static void CopyTable(ExcelWorksheet Copy, ExcelWorksheet added) xml = xmlDoc.OuterXml; //var uriTbl = new Uri(string.Format("/xl/tables/table{0}.xml", Id), UriKind.Relative); - var uriTbl = XmlHelper.GetNewUri(added._package.ZipPackage, "/xl/tables/table{0}.xml", ref Id); - if (added.Workbook._nextTableID < Id) added.Workbook._nextTableID = Id; + var uriTbl = XmlHelper.GetNewUri(destWs._package.ZipPackage, "/xl/tables/table{0}.xml", ref Id); + if (destWs.Workbook._nextTableID < Id) destWs.Workbook._nextTableID = Id; - var part = added._package.ZipPackage.CreatePart(uriTbl, "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml", added._package.Compression); + var part = destWs._package.ZipPackage.CreatePart(uriTbl, "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml", destWs._package.Compression); StreamWriter streamTbl = new StreamWriter(part.GetStream(FileMode.Create, FileAccess.Write)); streamTbl.Write(xml); //streamTbl.Close(); streamTbl.Flush(); //create the relationship and add the ID to the worksheet xml. - var rel = added.Part.CreateRelationship(UriHelper.GetRelativeUri(added.WorksheetUri, uriTbl), Packaging.TargetMode.Internal, ExcelPackage.schemaRelationships + "/table"); + var rel = destWs.Part.CreateRelationship(UriHelper.GetRelativeUri(destWs.WorksheetUri, uriTbl), Packaging.TargetMode.Internal, ExcelPackage.schemaRelationships + "/table"); if (tbl.RelationshipID == null) { - var topNode = added.WorksheetXml.SelectSingleNode("//d:tableParts", tbl.NameSpaceManager); + var topNode = destWs.WorksheetXml.SelectSingleNode("//d:tableParts", tbl.NameSpaceManager); if (topNode == null) { - added.CreateNode("d:tableParts"); - topNode = added.WorksheetXml.SelectSingleNode("//d:tableParts", tbl.NameSpaceManager); + destWs.CreateNode("d:tableParts"); + topNode = destWs.WorksheetXml.SelectSingleNode("//d:tableParts", tbl.NameSpaceManager); } - XmlElement elem = added.WorksheetXml.CreateElement("tablePart", ExcelPackage.schemaMain); + XmlElement elem = destWs.WorksheetXml.CreateElement("tablePart", ExcelPackage.schemaMain); topNode.AppendChild(elem); elem.SetAttribute("id", ExcelPackage.schemaRelationships, rel.Id); } else { XmlAttribute relAtt; - relAtt = added.WorksheetXml.SelectSingleNode(string.Format("//d:tableParts/d:tablePart/@r:id[.='{0}']", tbl.RelationshipID), tbl.NameSpaceManager) as XmlAttribute; + relAtt = destWs.WorksheetXml.SelectSingleNode(string.Format("//d:tableParts/d:tablePart/@r:id[.='{0}']", tbl.RelationshipID), tbl.NameSpaceManager) as XmlAttribute; relAtt.Value = rel.Id; } @@ -1103,8 +1147,8 @@ private static void CopyTable(ExcelWorksheet Copy, ExcelWorksheet added) { if (col.Slicer != null) { - var newCol = added.Tables[name].Columns[col.Position]; - foreach (var d in added.Drawings) + var newCol = destWs.Tables[name].Columns[col.Position]; + foreach (var d in destWs.Drawings) { if (d is ExcelTableSlicer slicer) { @@ -1119,30 +1163,51 @@ private static void CopyTable(ExcelWorksheet Copy, ExcelWorksheet added) } } } + + //The copied worksheet's formula cells were cloned verbatim and still reference + //the source table names. Because copied tables are written to XML directly (to + //avoid materializing them, for performance), the ExcelTable.Name setter never + //runs and the copied formulas are not adjusted. Adjust them here, once every + //table part has been written, scoped to the copied worksheet only so the source + //worksheet's references to its own tables are left untouched. Deferring until + //after the loop also ensures added.Tables is first materialized in its complete + //state. + foreach (var pair in copiedTableNames) + { + var sourceName = pair.Key; + var copiedName = pair.Value; + if (sourceName != copiedName) + { + var ta = new TableAdjustFormula(destWs.Tables[copiedName]); + ta.AdjustFormulas(sourceName, copiedName, destWs); + } + } + + return copiedTableNames; } - private static void CopyPivotTable(ExcelWorksheet Copy, ExcelWorksheet added) + private static void CopyPivotTable(ExcelWorksheet sourceWs, ExcelWorksheet destWs) { - Copy._package.Workbook.ReadAllPivotTables(); + sourceWs._package.Workbook.ReadAllPivotTables(); string prevName = ""; var worksheetMap = new Dictionary(); var nameMap = new Dictionary(); - var wbAdded = added.Workbook; - var isPackageInternal = Copy.Workbook == wbAdded; - foreach (var tbl in Copy.PivotTables) + var wbAdded = destWs.Workbook; + var isPackageInternal = sourceWs.Workbook == wbAdded; + foreach (var tbl in sourceWs.PivotTables) { string xml = tbl.PivotTableXml.OuterXml; string name; - if (isPackageInternal || added.PivotTables._pivotTableNames.ContainsKey(tbl.Name)) + if (isPackageInternal || destWs.PivotTables._pivotTableNames.ContainsKey(tbl.Name)) { if (prevName == "") { - name = added.PivotTables.GetNewTableName(); + name = destWs.PivotTables.GetNewTableName(); } else { int ix = int.Parse(prevName.Substring(10)) + 1; name = string.Format("PivotTable{0}", ix); - while (added.Workbook.ExistsPivotTableName(name)) + while (destWs.Workbook.ExistsPivotTableName(name)) { name = string.Format("PivotTable{0}", ++ix); } @@ -1160,34 +1225,34 @@ private static void CopyPivotTable(ExcelWorksheet Copy, ExcelWorksheet added) xmlDoc.LoadXml(xml); xmlDoc.SelectSingleNode("//d:pivotTableDefinition/@name", tbl.NameSpaceManager).Value = name; - int Id = added.Workbook._nextPivotTableID++; - var uriTbl = XmlHelper.GetNewUri(added._package.ZipPackage, "/xl/pivotTables/pivotTable{0}.xml", ref Id); - if (added.Workbook._nextPivotTableID < Id) added.Workbook._nextPivotTableID = Id; + int Id = destWs.Workbook._nextPivotTableID++; + var uriTbl = XmlHelper.GetNewUri(destWs._package.ZipPackage, "/xl/pivotTables/pivotTable{0}.xml", ref Id); + if (destWs.Workbook._nextPivotTableID < Id) destWs.Workbook._nextPivotTableID = Id; xml = xmlDoc.OuterXml; - var partTbl = added._package.ZipPackage.CreatePart(uriTbl, ContentTypes.contentTypePivotTable, added._package.Compression); + var partTbl = destWs._package.ZipPackage.CreatePart(uriTbl, ContentTypes.contentTypePivotTable, destWs._package.Compression); StreamWriter streamTbl = new StreamWriter(partTbl.GetStream(FileMode.Create, FileAccess.Write)); streamTbl.Write(xml); streamTbl.Flush(); //create the relationship and add the ID to the worksheet xml. - added.Part.CreateRelationship(UriHelper.ResolvePartUri(added.WorksheetUri, uriTbl), Packaging.TargetMode.Internal, ExcelPackage.schemaRelationships + "/pivotTable"); + destWs.Part.CreateRelationship(UriHelper.ResolvePartUri(destWs.WorksheetUri, uriTbl), Packaging.TargetMode.Internal, ExcelPackage.schemaRelationships + "/pivotTable"); if (isPackageInternal) { partTbl.CreateRelationship(tbl.CacheDefinition.CacheDefinitionUri, tbl.CacheDefinition.Relationship.TargetMode, tbl.CacheDefinition.Relationship.RelationshipType); } else { - CreateCacheInNewPackage(added, tbl, partTbl); + CreateCacheInNewPackage(destWs, tbl, partTbl); } } - added._pivotTables = null; //Reset collection so it's reloaded when accessing the collection next time. + destWs._pivotTables = null; //Reset collection so it's reloaded when accessing the collection next time. //Refresh all items in the copied table. - foreach (var copiedTbl in added.PivotTables) + foreach (var copiedTbl in destWs.PivotTables) { if (!copiedTbl.CacheDefinition._cacheReference._pivotTables.Contains(copiedTbl)) { @@ -1201,19 +1266,19 @@ private static void CopyPivotTable(ExcelWorksheet Copy, ExcelWorksheet added) if (copiedTbl.CacheDefinition.IsExternalReferernce) continue; - ChangeToWsLocalPivotTable(added, nameMap); + ChangeToWsLocalPivotTable(destWs, nameMap); foreach (var fld in copiedTbl.Fields) { fld.Cache.Refresh(); } } //Can't have a cell selected when "group editing" avoids pop-up by not selecting sheet. - added.View.SetTabSelected(false); + destWs.View.SetTabSelected(false); } - private static void CreateCacheInNewPackage(ExcelWorksheet added, ExcelPivotTable tbl, ZipPackagePart partTbl) + private static void CreateCacheInNewPackage(ExcelWorksheet sourceWs, ExcelPivotTable tbl, ZipPackagePart partTbl) { - var wbAdded = added.Workbook; + var wbAdded = sourceWs.Workbook; PivotTableCacheInternal newCache; var cacheAddress = tbl.CacheDefinition._cacheReference.GetSourceAddress(); if (wbAdded._pivotTableCaches.TryGetValue(cacheAddress, out ExcelWorkbook.PivotTableCacheRangeInfo rangeInfo)) @@ -1226,10 +1291,10 @@ private static void CreateCacheInNewPackage(ExcelWorksheet added, ExcelPivotTabl rangeInfo = new ExcelWorkbook.PivotTableCacheRangeInfo(); string xmlCache = tbl.CacheDefinition.CacheDefinitionXml.OuterXml; var cacheId = wbAdded._nextPivotCacheId; - var uriCache = XmlHelper.GetNewUri(added._package.ZipPackage, "/xl/pivotCache/pivotCacheDefinition{0}.xml", ref cacheId); + var uriCache = XmlHelper.GetNewUri(sourceWs._package.ZipPackage, "/xl/pivotCache/pivotCacheDefinition{0}.xml", ref cacheId); if (wbAdded._nextPivotCacheId < cacheId) wbAdded._nextPivotCacheId = cacheId; - var partCache = added._package.ZipPackage.CreatePart(uriCache, ContentTypes.contentTypePivotCacheDefinition, added._package.Compression); + var partCache = sourceWs._package.ZipPackage.CreatePart(uriCache, ContentTypes.contentTypePivotCacheDefinition, sourceWs._package.Compression); StreamWriter streamCache = new StreamWriter(partCache.GetStream(FileMode.Create, FileAccess.Write)); streamCache.Write(xmlCache); streamCache.Flush(); @@ -1242,8 +1307,8 @@ private static void CreateCacheInNewPackage(ExcelWorksheet added, ExcelPivotTabl { if (tbl.CacheDefinition.SourceRange.Worksheet != null && tbl.CacheDefinition.SourceRange.Worksheet.Name == tbl.WorkSheet.Name) { - rangeInfo.Address = ExcelCellBase.GetQuotedWorksheetName(added.Name) + "!" + tbl.CacheDefinition.SourceRange.LocalAddress; - newCache.SetXmlNodeString(PivotTableCacheInternal._sourceWorksheetPath, added.Name); + rangeInfo.Address = ExcelCellBase.GetQuotedWorksheetName(sourceWs.Name) + "!" + tbl.CacheDefinition.SourceRange.LocalAddress; + newCache.SetXmlNodeString(PivotTableCacheInternal._sourceWorksheetPath, sourceWs.Name); } else { @@ -1256,15 +1321,15 @@ private static void CreateCacheInNewPackage(ExcelWorksheet added, ExcelPivotTabl var rId = partCache.CreateRelationship(rel.TargetUri, rel.TargetMode, rel.RelationshipType); newCache.SourceRId = rId.Id; } - added.Workbook.AddPivotTableCache(newCache, true); + sourceWs.Workbook.AddPivotTableCache(newCache, true); newCache.AddRecordsXml(); } } - private static void ChangeToWsLocalPivotTable(ExcelWorksheet added, Dictionary nameMap) + private static void ChangeToWsLocalPivotTable(ExcelWorksheet sourceWs, Dictionary nameMap) { - foreach (var d in added.Drawings) + foreach (var d in sourceWs.Drawings) { if (d is ExcelPivotTableSlicer s) { @@ -1273,38 +1338,38 @@ private static void ChangeToWsLocalPivotTable(ExcelWorksheet added, Dictionary(); - CopyDxfStylesTables(copy, added); - CopyDxfStylesPivotTables(copy, added, dxfStyleCashe); - CopyDxfStylesConditionalFormatting(copy, added, dxfStyleCashe); + CopyDxfStylesTables(sourceWs, destWs); + CopyDxfStylesPivotTables(sourceWs, destWs, dxfStyleCashe); + CopyDxfStylesConditionalFormatting(sourceWs, destWs, dxfStyleCashe); } - private static void CopyDxfStylesTables(ExcelWorksheet copy, ExcelWorksheet added) + private static void CopyDxfStylesTables(ExcelWorksheet sourceWs, ExcelWorksheet destWs) { //Table formats - for (int i = 0; i < copy.Tables.Count; i++) + for (int i = 0; i < sourceWs.Tables.Count; i++) { - var tblFrom = copy.Tables[i]; - var tblTo = added.Tables[tblFrom.Name]; //Use Name, as id can differ if the worksheets are in different workbooks. + var tblFrom = sourceWs.Tables[i]; + var tblTo = destWs.Tables[i]; //Use Name, as id can differ if the worksheets are in different workbooks. DxfStyleHandler.CopyDxfStylesTable(tblFrom, tblTo); } } - private static void CopyDxfStylesPivotTables(ExcelWorksheet copy, ExcelWorksheet added, Dictionary dxfStyleCache) + private static void CopyDxfStylesPivotTables(ExcelWorksheet sourceWs, ExcelWorksheet destWs, Dictionary dxfStyleCache) { //Table formats - foreach (var pt in copy.PivotTables) + foreach (var pt in sourceWs.PivotTables) { var ix = 0; - var newPt = added.PivotTables[pt.Name]; + var newPt = destWs.PivotTables[pt.Name]; foreach (var a in pt.Styles._list) { var addedStyle = newPt.Styles[ix++]; @@ -1313,17 +1378,17 @@ private static void CopyDxfStylesPivotTables(ExcelWorksheet copy, ExcelWorksheet } } } - private static void CopyDxfStylesConditionalFormatting(ExcelWorksheet copy, ExcelWorksheet added, Dictionary dxfStyleCache) + private static void CopyDxfStylesConditionalFormatting(ExcelWorksheet sourceWs, ExcelWorksheet destWs, Dictionary dxfStyleCache) { //Conditional Formatting - for (var i = 0; i < copy.ConditionalFormatting.Count; i++) + for (var i = 0; i < sourceWs.ConditionalFormatting.Count; i++) { - var cfSource = copy.ConditionalFormatting[i]; + var cfSource = sourceWs.ConditionalFormatting[i]; var dxfId = cfSource.DxfId; if (dxfId != -1) { - AppendDxf(copy.Workbook.Styles, added.Workbook.Styles, dxfStyleCache, dxfId); - added.ConditionalFormatting[i].DxfId = dxfStyleCache[dxfId]; + AppendDxf(sourceWs.Workbook.Styles, destWs.Workbook.Styles, dxfStyleCache, dxfId); + destWs.ConditionalFormatting[i].DxfId = dxfStyleCache[dxfId]; } } } @@ -1338,75 +1403,75 @@ private static void AppendDxf(ExcelStyles stylesFrom, ExcelStyles stylesTo, Dict } } - private static int CopyValues(ExcelWorksheet Copy, ExcelWorksheet added, int row, int col, bool hasMetadata, bool sameWorkbook) + private static int CopyValues(ExcelWorksheet sourceWs, ExcelWorksheet destWs, int row, int col, bool hasMetadata, bool sameWorkbook) { - var valueCore = Copy.GetCoreValueInner(row, col); - added.SetValueStyleIdInner(row, col, valueCore._value, valueCore._styleId); + var valueCore = sourceWs.GetCoreValueInner(row, col); + destWs.SetValueStyleIdInner(row, col, valueCore._value, valueCore._styleId); byte fl = 0; - if (Copy._flags.Exists(row, col, ref fl)) + if (sourceWs._flags.Exists(row, col, ref fl)) { - added._flags.SetValue(row, col, fl); + destWs._flags.SetValue(row, col, fl); } if (hasMetadata) { ExcelWorksheet.MetaDataReference md = new ExcelWorksheet.MetaDataReference(); - if (Copy._metadataStore.Exists(row, col, ref md)) + if (sourceWs._metadataStore.Exists(row, col, ref md)) { if (sameWorkbook) { - added._metadataStore.SetValue(row, col, md); + destWs._metadataStore.SetValue(row, col, md); } else { - RichDataCopyHelper.CopyMetadata(Copy, added, Copy.Workbook.RichData, Copy.Cells[row, col]); + RichDataCopyHelper.CopyMetadata(sourceWs, destWs, sourceWs.Workbook.RichData, sourceWs.Cells[row, col]); } } } - var v = Copy._formulas.GetValue(row, col); + var v = sourceWs._formulas.GetValue(row, col); if (v != null) { - added.SetFormula(row, col, v); + destWs.SetFormula(row, col, v); } - var hyperLink = Copy._hyperLinks.GetValue(row, col); + var hyperLink = sourceWs._hyperLinks.GetValue(row, col); if (hyperLink != null) { - added._hyperLinks.SetValue(row, col, hyperLink); + destWs._hyperLinks.SetValue(row, col, hyperLink); } return valueCore._styleId; } - private static void CopyThreadedComments(ExcelWorksheet copy, ExcelWorksheet added) + private static void CopyThreadedComments(ExcelWorksheet sourceWs, ExcelWorksheet destWs) { //Copy the underlaying legacy comments. - CopyComment(copy, added); + CopyComment(sourceWs, destWs); //First copy the drawing XML - string xml = copy.ThreadedComments.ThreadedCommentsXml.InnerXml; - var ix = added.SheetId; - var tcUri = UriHelper.ResolvePartUri(added.WorksheetUri, XmlHelper.GetNewUri(added._package.ZipPackage, "/xl/threadedComments/threadedComment{0}.xml", ref ix)); + string xml = sourceWs.ThreadedComments.ThreadedCommentsXml.InnerXml; + var ix = destWs.SheetId; + var tcUri = UriHelper.ResolvePartUri(destWs.WorksheetUri, XmlHelper.GetNewUri(destWs._package.ZipPackage, "/xl/threadedComments/threadedComment{0}.xml", ref ix)); - var part = added._package.ZipPackage.CreatePart(tcUri, "application/vnd.ms-excel.threadedcomments+xml", added._package.Compression); + var part = destWs._package.ZipPackage.CreatePart(tcUri, "application/vnd.ms-excel.threadedcomments+xml", destWs._package.Compression); StreamWriter streamDrawing = new StreamWriter(part.GetStream(FileMode.Create, FileAccess.Write)); streamDrawing.Write(xml); streamDrawing.Flush(); //Add the relationship ID to the worksheet xml. - added.Part.CreateRelationship(tcUri, Packaging.TargetMode.Internal, ExcelPackage.schemaThreadedComment); + destWs.Part.CreateRelationship(tcUri, Packaging.TargetMode.Internal, ExcelPackage.schemaThreadedComment); - added.LoadThreadedComments(); - foreach (var t in added.ThreadedComments) + destWs.LoadThreadedComments(); + foreach (var t in destWs.ThreadedComments) { for (int i = 0; i < t.Comments.Count; i++) { t.Comments[i].Id = ExcelThreadedComment.NewId(); if (i == 0) { - added.Comments[t.CellAddress].Author = "tc=" + t.Comments[i].Id; + destWs.Comments[t.CellAddress].Author = "tc=" + t.Comments[i].Id; } else { @@ -1415,39 +1480,39 @@ private static void CopyThreadedComments(ExcelWorksheet copy, ExcelWorksheet add } } - if (copy.Workbook != added.Workbook) //Different package. Copy all persons from source package. + if (sourceWs.Workbook != destWs.Workbook) //Different package. Copy all persons from source package. { - var wbDest = added.Workbook; - foreach (var p in copy.Workbook.ThreadedCommentPersons) + var wbDest = destWs.Workbook; + foreach (var p in sourceWs.Workbook.ThreadedCommentPersons) { wbDest.ThreadedCommentPersons.Add(p.DisplayName, p.UserId, p.ProviderId, p.Id); } } } - private static void CopyHeaderFooterPictures(ExcelWorksheet Copy, ExcelWorksheet added) + private static void CopyHeaderFooterPictures(ExcelWorksheet sourceWs, ExcelWorksheet destWs) { - if (Copy.TopNode != null && Copy.GetNode("d:headerFooter") == null) return; + if (sourceWs.TopNode != null && sourceWs.GetNode("d:headerFooter") == null) return; //Copy any images first, so the pictures exist on the target before the //header/footer text is parsed. The text may contain the image code (&G), //and parsing it reads added.HeaderFooter.Pictures. - if (Copy.HeaderFooter.Pictures.Count > 0) + if (sourceWs.HeaderFooter.Pictures.Count > 0) { - Uri source = Copy.HeaderFooter.Pictures.Uri; - Uri dest = XmlHelper.GetNewUri(added._package.ZipPackage, @"/xl/drawings/vmlDrawing{0}.vml"); - added.DeleteNode("d:legacyDrawingHF"); + Uri source = sourceWs.HeaderFooter.Pictures.Uri; + Uri dest = XmlHelper.GetNewUri(destWs._package.ZipPackage, @"/xl/drawings/vmlDrawing{0}.vml"); + destWs.DeleteNode("d:legacyDrawingHF"); - foreach (ExcelVmlDrawingPicture pic in Copy.HeaderFooter.Pictures) + foreach (ExcelVmlDrawingPicture pic in sourceWs.HeaderFooter.Pictures) { ExcelVmlDrawingPicture item; - if (Copy._package != added._package) + if (sourceWs._package != destWs._package) { - var ii = added.Workbook._package.PictureStore.AddImage(pic.Image.ImageBytes, null, pic.Image.Type); - item = added.HeaderFooter.Pictures.Add(pic.Id, ii.Uri, pic.Title, pic.Width, pic.Height); + var ii = destWs.Workbook._package.PictureStore.AddImage(pic.Image.ImageBytes, null, pic.Image.Type); + item = destWs.HeaderFooter.Pictures.Add(pic.Id, ii.Uri, pic.Title, pic.Width, pic.Height); } else { - item = added.HeaderFooter.Pictures.Add(pic.Id, ((IPictureContainer)pic).UriPic, pic.Title, pic.Width, pic.Height); + item = destWs.HeaderFooter.Pictures.Add(pic.Id, ((IPictureContainer)pic).UriPic, pic.Title, pic.Width, pic.Height); } foreach (XmlAttribute att in pic.TopNode.Attributes) { @@ -1465,12 +1530,12 @@ private static void CopyHeaderFooterPictures(ExcelWorksheet Copy, ExcelWorksheet } //Copy the texts - if (Copy.HeaderFooter._oddHeader != null) CopyText(Copy.HeaderFooter._oddHeader, added.HeaderFooter.OddHeader); - if (Copy.HeaderFooter._oddFooter != null) CopyText(Copy.HeaderFooter._oddFooter, added.HeaderFooter.OddFooter); - if (Copy.HeaderFooter._evenHeader != null) CopyText(Copy.HeaderFooter._evenHeader, added.HeaderFooter.EvenHeader); - if (Copy.HeaderFooter._evenFooter != null) CopyText(Copy.HeaderFooter._evenFooter, added.HeaderFooter.EvenFooter); - if (Copy.HeaderFooter._firstHeader != null) CopyText(Copy.HeaderFooter._firstHeader, added.HeaderFooter.FirstHeader); - if (Copy.HeaderFooter._firstFooter != null) CopyText(Copy.HeaderFooter._firstFooter, added.HeaderFooter.FirstFooter); + if (sourceWs.HeaderFooter._oddHeader != null) CopyText(sourceWs.HeaderFooter._oddHeader, destWs.HeaderFooter.OddHeader); + if (sourceWs.HeaderFooter._oddFooter != null) CopyText(sourceWs.HeaderFooter._oddFooter, destWs.HeaderFooter.OddFooter); + if (sourceWs.HeaderFooter._evenHeader != null) CopyText(sourceWs.HeaderFooter._evenHeader, destWs.HeaderFooter.EvenHeader); + if (sourceWs.HeaderFooter._evenFooter != null) CopyText(sourceWs.HeaderFooter._evenFooter, destWs.HeaderFooter.EvenFooter); + if (sourceWs.HeaderFooter._firstHeader != null) CopyText(sourceWs.HeaderFooter._firstHeader, destWs.HeaderFooter.FirstHeader); + if (sourceWs.HeaderFooter._firstFooter != null) CopyText(sourceWs.HeaderFooter._firstFooter, destWs.HeaderFooter.FirstFooter); } private static void CopyText(ExcelHeaderFooterText from, ExcelHeaderFooterText to) { @@ -1479,21 +1544,21 @@ private static void CopyText(ExcelHeaderFooterText from, ExcelHeaderFooterText t to.RightAlignedText = from.RightAlignedText; } - private static void CopySlicers(ExcelWorksheet source, ExcelWorksheet target) + private static void CopySlicers(ExcelWorksheet sourceWs, ExcelWorksheet destWs) { - foreach (var slicer in source.SlicerXmlSources._list) + foreach (var slicer in sourceWs.SlicerXmlSources._list) { - var id = target.SheetId; - var uri = XmlHelper.GetNewUri(target.Part.Package, "/xl/slicers/slicer{0}.xml", ref id); - var part = target.Part.Package.CreatePart(uri, "application/vnd.ms-excel.slicer+xml", target.Part.Package.Compression); - var rel = target.Part.CreateRelationship(uri, Packaging.TargetMode.Internal, ExcelPackage.schemaRelationshipsSlicer); + var id = destWs.SheetId; + var uri = XmlHelper.GetNewUri(destWs.Part.Package, "/xl/slicers/slicer{0}.xml", ref id); + var part = destWs.Part.Package.CreatePart(uri, "application/vnd.ms-excel.slicer+xml", destWs.Part.Package.Compression); + var rel = destWs.Part.CreateRelationship(uri, Packaging.TargetMode.Internal, ExcelPackage.schemaRelationshipsSlicer); var xml = new XmlDocument(); xml.LoadXml(slicer.XmlDocument.OuterXml); var stream = new StreamWriter(part.GetStream(FileMode.Create, FileAccess.Write)); xml.Save(stream); //Now create the new relationship between the worksheet and the slicer. - var relNode = (XmlElement)(target.WorksheetXml.DocumentElement.SelectSingleNode($"d:extLst/d:ext/x14:slicerList/x14:slicer[@r:id='{slicer.Rel.Id}']", target.NameSpaceManager)); + var relNode = (XmlElement)(destWs.WorksheetXml.DocumentElement.SelectSingleNode($"d:extLst/d:ext/x14:slicerList/x14:slicer[@r:id='{slicer.Rel.Id}']", destWs.NameSpaceManager)); relNode.Attributes["r:id"].Value = rel.Id; } } diff --git a/src/EPPlus/ExcelWorksheets.cs b/src/EPPlus/ExcelWorksheets.cs index 9878e1c686..3f696065e4 100644 --- a/src/EPPlus/ExcelWorksheets.cs +++ b/src/EPPlus/ExcelWorksheets.cs @@ -173,16 +173,46 @@ private ExcelWorksheet AddSheet(string Name, bool isChart, eChartType? chartType return worksheet; } } + /// /// Adds a copy of a worksheet /// - /// The name of the workbook + /// The name of the worksheet /// The worksheet to be copied + /// public ExcelWorksheet Add(string Name, ExcelWorksheet Copy) + { + return Add(Name, Copy, ExcelWorksheetCopyOptions.Default); + } + + /// + /// Adds a copy of a worksheet + /// + /// The name of the worksheet + /// The worksheet to be copied + /// An action used to configure options for the copy operation + /// The new copy added to the end of the worksheets collection + public ExcelWorksheet Add(string Name, ExcelWorksheet Copy, Action optionsHandler) + { + var options = ExcelWorksheetCopyOptions.Default; + if (optionsHandler != null) + { + optionsHandler.Invoke(options); + } + return Add(Name, Copy, options); + } + + /// + /// Adds a copy of a worksheet + /// + /// The name of the worksheet + /// The worksheet to be copied + /// Options for copying the worksheet + internal ExcelWorksheet Add(string Name, ExcelWorksheet Copy, ExcelWorksheetCopyOptions options) { lock (_worksheets) { - return WorksheetCopyHelper.Copy(this, Name, Copy); + return WorksheetCopyHelper.Copy(this, Name, Copy, options); } } /// @@ -645,6 +675,7 @@ public ExcelWorksheet this[string Name] return GetByName(Name); } } + /// /// Copies the named worksheet and creates a new worksheet in the same workbook /// @@ -652,12 +683,24 @@ public ExcelWorksheet this[string Name] /// The name of the new worksheet to create /// The new copy added to the end of the worksheets collection public ExcelWorksheet Copy(string Name, string NewName) + { + return Copy(Name, NewName, null); + } + + /// + /// Copies the named worksheet and creates a new worksheet in the same workbook + /// + /// The name of the existing worksheet + /// The name of the new worksheet to create + /// An action used to configure options for the copy operation + /// The new copy added to the end of the worksheets collection + public ExcelWorksheet Copy(string Name, string NewName, Action optionsHandler) { ExcelWorksheet Copy = this[Name]; if (Copy == null) throw new ArgumentException(string.Format("Copy worksheet error: Could not find worksheet to copy '{0}'", Name)); - ExcelWorksheet added = Add(NewName, Copy); + ExcelWorksheet added = Add(NewName, Copy, optionsHandler); return added; } #endregion diff --git a/src/EPPlus/Table/TableAdjustFormula.cs b/src/EPPlus/Table/TableAdjustFormula.cs index aa134aa6f2..3befa22130 100644 --- a/src/EPPlus/Table/TableAdjustFormula.cs +++ b/src/EPPlus/Table/TableAdjustFormula.cs @@ -12,45 +12,50 @@ public TableAdjustFormula(ExcelTable tbl) _tbl = tbl; } - internal void AdjustFormulas(string prevName, string name) + internal void AdjustFormulas(string prevName, string name, ExcelWorksheet ws) { - foreach (var ws in _tbl.WorkSheet.Workbook.Worksheets) + foreach (var tbl in ws.Tables) { - foreach (var tbl in ws.Tables) + foreach (var c in tbl.Columns) { - foreach (var c in tbl.Columns) + if (!string.IsNullOrEmpty(c.CalculatedColumnFormula)) { - if (!string.IsNullOrEmpty(c.CalculatedColumnFormula)) - { - c.CalculatedColumnFormula = ReplaceTableName(c.CalculatedColumnFormula, prevName, name); - } + c.CalculatedColumnFormula = ReplaceTableName(c.CalculatedColumnFormula, prevName, name); } } + } - var cse = new CellStoreEnumerator(ws._formulas); - while (cse.Next()) + var wsCse = new CellStoreEnumerator(ws._formulas); + while (wsCse.Next()) + { + if (wsCse.Value is string f) { - if (cse.Value is string f) + if (f.IndexOf(prevName, StringComparison.InvariantCultureIgnoreCase) > -1) { - if (f.IndexOf(prevName, StringComparison.InvariantCultureIgnoreCase) > -1) - { - ws._formulas.SetValue(cse.Row, cse.Column, ReplaceTableName(f, prevName, name)); - } + ws._formulas.SetValue(wsCse.Row, wsCse.Column, ReplaceTableName(f, prevName, name)); } } + } - foreach (var sf in ws._sharedFormulas.Values) + foreach (var sf in ws._sharedFormulas.Values) + { + if (sf.Formula.IndexOf(prevName, StringComparison.InvariantCultureIgnoreCase) > -1) { - if (sf.Formula.IndexOf(prevName, StringComparison.InvariantCultureIgnoreCase) > -1) - { - sf.Formula = ReplaceTableName(sf.Formula, prevName, name); - } + sf.Formula = ReplaceTableName(sf.Formula, prevName, name); } + } - foreach (var n in ws.Names) - { - AdjustName(n, prevName, name); - } + foreach (var n in ws.Names) + { + AdjustName(n, prevName, name); + } + } + + internal void AdjustFormulas(string prevName, string name) + { + foreach (var ws in _tbl.WorkSheet.Workbook.Worksheets) + { + AdjustFormulas(prevName, name, ws); } foreach (var n in _tbl.WorkSheet.Workbook.Names) @@ -96,5 +101,4 @@ private string ReplaceTableName(string formula, string prevName, string name) return f; } } -} - +} \ No newline at end of file diff --git a/src/EPPlusTest/Core/Worksheet/CopyWorksheetTests.cs b/src/EPPlusTest/Core/Worksheet/CopyWorksheetTests.cs new file mode 100644 index 0000000000..2780e91ced --- /dev/null +++ b/src/EPPlusTest/Core/Worksheet/CopyWorksheetTests.cs @@ -0,0 +1,377 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OfficeOpenXml; +using OfficeOpenXml.Table; +using System; +using System.Collections.Generic; + +namespace EPPlusTest.Core.Worksheet +{ + [TestClass] + public class CopyWorksheetTests : TestBase + { + private static ExcelPackage CreatePackageWithTable(out ExcelWorksheet source) + { + var package = new ExcelPackage(); + source = package.Workbook.Worksheets.Add("Template"); + source.Cells["A1"].Value = "Header"; + source.Cells["A2"].Value = 1; + source.Cells["A3"].Value = 2; + source.Tables.Add(source.Cells["A1:A3"], "BoxOffice"); + return package; + } + + [TestMethod] + public void Copy_WithTableCopyHandler_RenamesCopiedTable() + { + using (var package = CreatePackageWithTable(out var source)) + { + var copy = package.Workbook.Worksheets.Copy(source.Name, "BaltimoreMD", options => + { + options.TableCopyHandler = args => + { + args.NewName = "BaltimoreMD_" + args.SourceTableName; + }; + }); + + Assert.AreEqual(1, copy.Tables.Count); + Assert.IsNotNull(copy.Tables["BaltimoreMD_BoxOffice"]); + } + } + + [TestMethod] + public void Copy_WithTableCopyHandler_ProvidesSourceAndDefaultName() + { + using (var package = CreatePackageWithTable(out var source)) + { + string capturedSourceName = null; + string capturedDefaultName = null; + + package.Workbook.Worksheets.Copy(source.Name, "Copy", options => + { + options.TableCopyHandler = args => + { + capturedSourceName = args.SourceTableName; + capturedDefaultName = args.DefaultName; + }; + }); + + Assert.AreEqual("BoxOffice", capturedSourceName); + // Same workbook, so the copy is given a generated name. + Assert.IsTrue(capturedDefaultName.StartsWith("Table")); + } + } + + [TestMethod] + public void Copy_TableCopyHandler_UpdatesFormulaReferencesToRenamedTable() + { + using (var package = CreatePackageWithTable(out var source)) + { + // A cell on the source that references the table by name. + source.Cells["C1"].Formula = "SUM(BoxOffice[Header])"; + + var copy = package.Workbook.Worksheets.Copy(source.Name, "Renamed", options => + { + options.TableCopyHandler = args => + { + args.NewName = "NewBoxOffice"; + }; + }); + + // The copied formula should now reference the renamed table, + // updated token based through the ExcelTable.Name setter. + Assert.AreEqual("SUM(NewBoxOffice[Header])", copy.Cells["C1"].Formula); + } + } + + [TestMethod] + public void Copy_TableCopyHandler_NullNewName_KeepsDefaultName() + { + using (var package = CreatePackageWithTable(out var source)) + { + string defaultName = null; + + var copy = package.Workbook.Worksheets.Copy(source.Name, "Copy", options => + { + options.TableCopyHandler = args => + { + defaultName = args.DefaultName; + // NewName left null. + }; + }); + + Assert.IsNotNull(copy.Tables[defaultName]); + } + } + + [TestMethod] + public void Copy_WithoutOptions_KeepsExistingBehavior() + { + using (var package = CreatePackageWithTable(out var source)) + { + var copy = package.Workbook.Worksheets.Copy(source.Name, "Copy"); + + Assert.AreEqual(1, copy.Tables.Count); + // Same workbook copy generates a Table{n} name. + Assert.IsTrue(copy.Tables[0].Name.StartsWith("Table")); + } + } + + [TestMethod] + public void Copy_TableCopyHandler_RenameToExistingName_Throws() + { + using (var package = CreatePackageWithTable(out var source)) + { + // A second table whose name we will collide with. + source.Cells["E1"].Value = "H"; + source.Cells["E2"].Value = 1; + source.Tables.Add(source.Cells["E1:E2"], "Ancillary"); + + Assert.ThrowsExactly(() => + { + package.Workbook.Worksheets.Copy(source.Name, "Copy", options => + { + options.TableCopyHandler = args => + { + // Force both copied tables to the same name. + args.NewName = "Duplicate"; + }; + }); + }); + } + } + + [TestMethod] + public void Copy_MultipleTables_RenamesOnlySelectedSubset() + { + using (var package = CreatePackageWithTable(out var source)) + { + source.Cells["E1"].Value = "H"; + source.Cells["E2"].Value = 1; + source.Cells["E3"].Value = 2; + source.Tables.Add(source.Cells["E1:E3"], "Ancillary"); + + var copy = package.Workbook.Worksheets.Copy(source.Name, "Copy", options => + { + options.TableCopyHandler = args => + { + if (args.SourceTableName == "BoxOffice") + { + args.NewName = "Copied_BoxOffice"; + } + // Ancillary left with its default name. + }; + }); + + Assert.IsNotNull(copy.Tables["Copied_BoxOffice"]); + Assert.AreEqual(2, copy.Tables.Count); + } + } + + [TestMethod] + public void Copy_TableCopyHandler_DoesNotAffectSourceWorksheetFormulas() + { + using (var package = CreatePackageWithTable(out var source)) + { + source.Cells["C1"].Formula = "SUM(BoxOffice[Header])"; + + package.Workbook.Worksheets.Copy(source.Name, "Renamed", options => + { + options.TableCopyHandler = args => + { + args.NewName = "NewBoxOffice"; + }; + }); + + //The source worksheet still has its own BoxOffice table; its formula must be untouched. + Assert.AreEqual("SUM(BoxOffice[Header])", source.Cells["C1"].Formula); + } + } + + [TestMethod] + public void Copy_WithoutHandler_AssignsGeneratedTableName() + { + using (var package = CreatePackageWithTable(out var source)) + { + var copy = package.Workbook.Worksheets.Copy(source.Name, "Copy"); + + Assert.AreEqual(1, copy.Tables.Count); + //Same workbook copy renames the copied table to a generated Table{n} name. + Assert.AreNotEqual("BoxOffice", copy.Tables[0].Name); + Assert.IsTrue(copy.Tables[0].Name.StartsWith("Table")); + } + } + + [TestMethod] + public void Copy_WithoutHandler_GeneratedNameIsUniqueInWorkbook() + { + using (var package = CreatePackageWithTable(out var source)) + { + var copy = package.Workbook.Worksheets.Copy(source.Name, "Copy"); + + //Source keeps its original name, the copy gets a distinct generated name. + Assert.AreEqual("BoxOffice", source.Tables[0].Name); + Assert.AreNotEqual(source.Tables[0].Name, copy.Tables[0].Name); + Assert.IsFalse(source.Tables[0].Name.Equals(copy.Tables[0].Name, StringComparison.OrdinalIgnoreCase)); + } + } + + [TestMethod] + public void Copy_WithoutHandler_AdjustsCopiedFormulaToGeneratedName() + { + using (var package = CreatePackageWithTable(out var source)) + { + source.Cells["C1"].Formula = "SUM(BoxOffice[Header])"; + + var copy = package.Workbook.Worksheets.Copy(source.Name, "Copy"); + + //The copied formula must reference the copied table's generated name, + //not the source table name. + var expected = "SUM(" + copy.Tables[0].Name + "[Header])"; + Assert.AreEqual(expected, copy.Cells["C1"].Formula); + } + } + + [TestMethod] + public void Copy_WithoutHandler_DoesNotAffectSourceFormula() + { + using (var package = CreatePackageWithTable(out var source)) + { + source.Cells["C1"].Formula = "SUM(BoxOffice[Header])"; + + package.Workbook.Worksheets.Copy(source.Name, "Copy"); + + //Source formula still references the source table, untouched by the copy. + Assert.AreEqual("SUM(BoxOffice[Header])", source.Cells["C1"].Formula); + } + } + + [TestMethod] + public void Copy_WithoutHandler_MultipleTables_AllGetGeneratedNames() + { + using (var package = CreatePackageWithTable(out var source)) + { + source.Cells["E1"].Value = "H"; + source.Cells["E2"].Value = 1; + source.Cells["E3"].Value = 2; + source.Tables.Add(source.Cells["E1:E3"], "Ancillary"); + + var copy = package.Workbook.Worksheets.Copy(source.Name, "Copy"); + + Assert.AreEqual(2, copy.Tables.Count); + foreach (var t in copy.Tables) + { + Assert.IsTrue(t.Name.StartsWith("Table")); + } + //The two copied tables get distinct names. + Assert.AreNotEqual(copy.Tables[0].Name, copy.Tables[1].Name); + } + } + + [TestMethod] + public void Copy_WithoutHandler_MultipleTables_AdjustsEachFormulaToItsOwnCopy() + { + using (var package = CreatePackageWithTable(out var source)) + { + source.Cells["E1"].Value = "H"; + source.Cells["E2"].Value = 1; + source.Cells["E3"].Value = 2; + source.Tables.Add(source.Cells["E1:E3"], "Ancillary"); + + source.Cells["G1"].Formula = "SUM(BoxOffice[Header])"; + source.Cells["G2"].Formula = "SUM(Ancillary[H])"; + + var copy = package.Workbook.Worksheets.Copy(source.Name, "Copy"); + + //Resolve which copied table owns which column by header, since names are generated. + string boxOfficeCopyName = null; + string ancillaryCopyName = null; + foreach (var t in copy.Tables) + { + if (t.Columns[0].Name == "Header") boxOfficeCopyName = t.Name; + if (t.Columns[0].Name == "H") ancillaryCopyName = t.Name; + } + + Assert.AreEqual("SUM(" + boxOfficeCopyName + "[Header])", copy.Cells["G1"].Formula); + Assert.AreEqual("SUM(" + ancillaryCopyName + "[H])", copy.Cells["G2"].Formula); + } + } + + [TestMethod] + public void Copy_ToDifferentPackage_PreservesTableName_WhenNameIsFree() + { + using (var sourcePackage = CreatePackageWithTable(out var source)) + using (var targetPackage = new ExcelPackage()) + { + var copy = targetPackage.Workbook.Worksheets.Add("Copy", source); + + //Different package, name not taken in the target: the original name is kept, + //not renamed to a generated Table{n} name. + Assert.AreEqual(1, copy.Tables.Count); + Assert.IsNotNull(copy.Tables["BoxOffice"]); + Assert.AreEqual("BoxOffice", copy.Tables[0].Name); + } + } + + [TestMethod] + public void Copy_ToDifferentPackage_PreservedName_KeepsFormulaReference() + { + using (var sourcePackage = CreatePackageWithTable(out var source)) + using (var targetPackage = new ExcelPackage()) + { + source.Cells["C1"].Formula = "SUM(BoxOffice[Header])"; + + var copy = targetPackage.Workbook.Worksheets.Add("Copy", source); + + //Name preserved, so the copied formula still references it unchanged. + Assert.AreEqual("SUM(BoxOffice[Header])", copy.Cells["C1"].Formula); + } + } + + [TestMethod] + public void Copy_ToDifferentPackage_RenamesTable_WhenNameAlreadyExists() + { + using (var sourcePackage = CreatePackageWithTable(out var source)) + using (var targetPackage = new ExcelPackage()) + { + //Pre-existing table with the same name in the target workbook. + var existing = targetPackage.Workbook.Worksheets.Add("Existing"); + existing.Cells["A1"].Value = "Header"; + existing.Cells["A2"].Value = 1; + existing.Tables.Add(existing.Cells["A1:A2"], "BoxOffice"); + + var copy = targetPackage.Workbook.Worksheets.Add("Copy", source); + + //Name collision in the target: the copied table must be renamed, and the + //original must remain untouched. + Assert.AreEqual("BoxOffice", existing.Tables[0].Name); + Assert.AreNotEqual("BoxOffice", copy.Tables[0].Name); + Assert.IsTrue(copy.Tables[0].Name.StartsWith("Table")); + } + } + + [TestMethod] + public void Copy_TableCopyHandler_NewNameCollidesWithExistingTable_Throws() + { + using (var package = CreatePackageWithTable(out var source)) + { + //A second, differently named table already in the workbook. + var other = package.Workbook.Worksheets.Add("Other"); + other.Cells["A1"].Value = "Header"; + other.Cells["A2"].Value = 1; + other.Tables.Add(other.Cells["A1:A2"], "ExistingTable"); + + Assert.ThrowsExactly(() => + { + package.Workbook.Worksheets.Copy(source.Name, "Copy", options => + { + options.TableCopyHandler = args => + { + //Collide with a table that already exists in the workbook. + args.NewName = "ExistingTable"; + }; + }); + }); + } + } + } +} \ No newline at end of file diff --git a/src/EPPlusTest/Issues/WorksheetIssues.cs b/src/EPPlusTest/Issues/WorksheetIssues.cs index a500392eb7..96038fff17 100644 --- a/src/EPPlusTest/Issues/WorksheetIssues.cs +++ b/src/EPPlusTest/Issues/WorksheetIssues.cs @@ -1200,7 +1200,6 @@ public void Issue2445() var worksheet = package.Workbook.Worksheets.Add("Table1"); worksheet.Cells["2:5"].EntireRow.Group(); worksheet.Cells["C:E"].EntireColumn.Group(); - Assert.AreEqual(true, worksheet.OutLineSummaryBelow); Assert.AreEqual(true, worksheet.OutLineSummaryRight); }