blob: c58131946b16fcabfa56cf3fa44f8a1b57a9bd53 [file] [log] [blame]
/*******************************************************************************
* You may amend and distribute as you like, but don't remove this header!
*
* EPPlus provides server-side generation of Excel 2007/2010 spreadsheets.
* See http://www.codeplex.com/EPPlus for details.
*
* Copyright (C) 2011 Jan Källman
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* The GNU Lesser General Public License can be viewed at http://www.opensource.org/licenses/lgpl-license.php
* If you unfamiliar with this license or have questions about it, here is an http://www.gnu.org/licenses/gpl-faq.html
*
* All code and executables are provided "as is" with no warranty either express or implied.
* The author accepts no liability for any damage or loss of business that this product may cause.
*
* Code change notes:
*
* Author Change Date
* ******************************************************************************
* Jan Källman Initial Release 2011-01-01
* Jan Källman License changed GPL-->LGPL 2011-12-27
* Richard Tallent Fix escaping of quotes 2012-10-31
*******************************************************************************/
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.IO;
using System.Text;
using System.Xml;
using OfficeOpenXml.FormulaParsing;
using OfficeOpenXml.FormulaParsing.LexicalAnalysis;
using OfficeOpenXml.Packaging;
using OfficeOpenXml.Utils;
namespace OfficeOpenXml;
/// <summary>
/// How the application should calculate formulas in the workbook
/// </summary>
public enum ExcelCalcMode {
/// <summary>
/// Indicates that calculations in the workbook are performed automatically when cell values change.
/// The application recalculates those cells that are dependent on other cells that contain changed values.
/// This mode of calculation helps to avoid unnecessary calculations.
/// </summary>
Automatic,
/// <summary>
/// Indicates tables be excluded during automatic calculation
/// </summary>
AutomaticNoTable,
/// <summary>
/// Indicates that calculations in the workbook be triggered manually by the user.
/// </summary>
Manual,
}
/// <summary>
/// Represents the Excel workbook and provides access to all the
/// document properties and worksheets within the workbook.
/// </summary>
public sealed class ExcelWorkbook : XmlHelper {
internal class SharedStringItem {
internal int pos;
internal string Text;
internal bool isRichText;
}
private readonly ExcelPackage _package;
private ExcelWorksheets _worksheets;
private OfficeProperties _properties;
private ExcelStyles _styles;
internal static ImmutableArray<string> WorkbookSchemaNodeOrder = [
"fileVersion",
"fileSharing",
"workbookPr",
"workbookProtection",
"bookViews",
"sheets",
"functionGroups",
"functionPrototypes",
"externalReferences",
"definedNames",
"calcPr",
"oleSize",
"customWorkbookViews",
"pivotCaches",
"smartTagPr",
"smartTagTypes",
"webPublishing",
"fileRecoveryPr",
];
protected override ImmutableArray<string> SchemaNodeOrder => WorkbookSchemaNodeOrder;
internal ExcelWorkbook(ExcelPackage package, XmlNamespaceManager namespaceManager)
: base(namespaceManager) {
_package = package;
_names = new(this);
_namespaceManager = namespaceManager;
WorkbookXml = package.GetOrCreateXmlDocument(
WorkbookUri,
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml",
ExcelPackage._schemaRelationships + "/officeDocument",
() => CreateEmptyWorkbookXml(namespaceManager));
_stylesXml = package.GetOrCreateXmlDocument(
StylesUri,
"application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml",
ExcelPackage._schemaRelationships + "/styles",
CreateEmptyStylesXml);
TopNode = WorkbookXml.DocumentElement;
FullCalcOnLoad = true; //Full calculation on load by default, for both new workbooks and templates.
GetSharedStrings();
GetExternalReferences();
GetDefinedNames();
}
private static XmlDocument CreateEmptyWorkbookXml(XmlNamespaceManager namespaceManager) {
var result = new XmlDocument(namespaceManager.NameTable);
var wbElem = result.CreateElement("workbook", ExcelPackage._schemaMain);
// Add the relationships namespace
wbElem.SetAttribute("xmlns:r", ExcelPackage._schemaRelationships);
result.AppendChild(wbElem);
// Create the bookViews and workbooks element
var bookViews = result.CreateElement("bookViews", ExcelPackage._schemaMain);
wbElem.AppendChild(bookViews);
var workbookView = result.CreateElement("workbookView", ExcelPackage._schemaMain);
bookViews.AppendChild(workbookView);
return result;
}
private static XmlDocument CreateEmptyStylesXml() {
StringBuilder xml = new StringBuilder(
"<styleSheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\">");
xml.Append("<numFmts />");
xml.Append("<fonts count=\"1\"><font><sz val=\"11\" /><name val=\"Calibri\" /></font></fonts>");
xml.Append(
"<fills><fill><patternFill patternType=\"none\" /></fill><fill><patternFill patternType=\"gray125\" /></fill></fills>");
xml.Append(
"<borders><border><left /><right /><top /><bottom /><diagonal /></border></borders>");
xml.Append("<cellStyleXfs count=\"1\"><xf numFmtId=\"0\" fontId=\"0\" /></cellStyleXfs>");
xml.Append("<cellXfs count=\"1\"><xf numFmtId=\"0\" fontId=\"0\" xfId=\"0\" /></cellXfs>");
xml.Append("<cellStyles><cellStyle name=\"Normal\" xfId=\"0\" builtinId=\"0\" /></cellStyles>");
xml.Append("<dxfs count=\"0\" />");
xml.Append("</styleSheet>");
var result = new XmlDocument();
result.LoadXml(xml.ToString());
return result;
}
internal readonly Dictionary<string, SharedStringItem> _sharedStrings = new(); //Used when reading cells.
internal List<SharedStringItem> _sharedStringsList = new(); //Used when reading cells.
internal ExcelNamedRangeCollection _names;
internal int _nextTableID = int.MinValue;
internal int _nextPivotTableID = int.MinValue;
private readonly XmlNamespaceManager _namespaceManager;
private FormulaParser _formulaParser;
private FormulaParserManager _parserManager;
internal CellStore<List<Token>> _formulaTokens;
/// <summary>
/// Read shared strings to list
/// </summary>
private void GetSharedStrings() {
if (_package.Package.PartExists(SharedStringsUri)) {
var xml = _package.GetXmlDocument(SharedStringsUri);
XmlNodeList nl = xml.SelectNodes("//d:sst/d:si", NameSpaceManager);
_sharedStringsList = new();
if (nl != null) {
foreach (XmlNode node in nl) {
XmlNode n = node.SelectSingleNode("d:t", NameSpaceManager);
if (n != null) {
_sharedStringsList.Add(
new() {
Text = ConvertUtil.ExcelDecodeString(n.InnerText),
});
} else {
_sharedStringsList.Add(
new() {
Text = node.InnerXml,
isRichText = true,
});
}
}
}
//Delete the shared string part, it will be recreated when the package is saved.
foreach (var rel in Part.GetRelationships()) {
if (rel.TargetUri.OriginalString.EndsWith(
"sharedstrings.xml",
StringComparison.InvariantCultureIgnoreCase)) {
Part.DeleteRelationship(rel.Id);
break;
}
}
_package.Package.DeletePart(SharedStringsUri); //Remove the part, it is recreated when saved.
}
}
internal void GetDefinedNames() {
XmlNodeList nl = WorkbookXml.SelectNodes("//d:definedNames/d:definedName", NameSpaceManager);
if (nl != null) {
foreach (XmlElement elem in nl) {
string fullAddress = elem.InnerText;
ExcelWorksheet nameWorksheet;
if (!int.TryParse(elem.GetAttribute("localSheetId"), out var localSheetId)) {
localSheetId = -1;
nameWorksheet = null;
} else {
nameWorksheet = Worksheets[localSheetId + 1];
}
var addressType = ExcelAddressBase.IsValid(fullAddress);
ExcelRangeBase range;
ExcelNamedRange namedRange;
if (fullAddress.IndexOf("[") == 0) {
int start = fullAddress.IndexOf("[");
int end = fullAddress.IndexOf("]", start);
if (start >= 0 && end >= 0) {
string externalIndex = fullAddress.Substring(start + 1, end - start - 1);
if (int.TryParse(externalIndex, out var index)) {
if (index > 0 && index <= _externalReferences.Count) {
fullAddress =
fullAddress.Substring(0, start)
+ "["
+ _externalReferences[index - 1]
+ "]"
+ fullAddress.Substring(end + 1);
}
}
}
}
if (addressType == ExcelAddressBase.AddressType.Invalid
|| addressType == ExcelAddressBase.AddressType.InternalName
|| addressType == ExcelAddressBase.AddressType.ExternalName
|| addressType == ExcelAddressBase.AddressType.Formula
|| addressType
== ExcelAddressBase
.AddressType
.ExternalAddress) //A value or a formula
{
range = new(this, nameWorksheet, elem.GetAttribute("name"), true);
if (nameWorksheet == null) {
namedRange = _names.Add(elem.GetAttribute("name"), range);
} else {
namedRange = nameWorksheet.Names.Add(elem.GetAttribute("name"), range);
}
if (fullAddress.StartsWith(
"\"")) //String value
{
namedRange.NameValue = fullAddress.Substring(1, fullAddress.Length - 2);
} else if (double.TryParse(
fullAddress,
NumberStyles.Any,
CultureInfo.InvariantCulture,
out var value)) {
namedRange.NameValue = value;
} else {
//if (addressType == ExcelAddressBase.AddressType.ExternalAddress || addressType == ExcelAddressBase.AddressType.ExternalName)
//{
// var r = new ExcelAddress(fullAddress);
// namedRange.NameFormula = '\'[' + r._wb
//}
//else
//{
namedRange.NameFormula = fullAddress;
//}
}
} else {
ExcelAddress addr = new ExcelAddress(fullAddress, this, null);
if (localSheetId > -1) {
if (string.IsNullOrEmpty(addr._ws)) {
namedRange = Worksheets[localSheetId + 1].Names.Add(
elem.GetAttribute("name"),
new(this, Worksheets[localSheetId + 1], fullAddress, false));
} else {
namedRange = Worksheets[localSheetId + 1].Names.Add(
elem.GetAttribute("name"),
new(this, Worksheets[addr._ws], fullAddress, false));
}
} else {
var ws = Worksheets[addr._ws];
namedRange = _names.Add(elem.GetAttribute("name"), new(this, ws, fullAddress, false));
}
}
if (elem.GetAttribute("hidden") == "1" && namedRange != null) {
namedRange.IsNameHidden = true;
}
if (!string.IsNullOrEmpty(elem.GetAttribute("comment"))) {
namedRange.NameComment = elem.GetAttribute("comment");
}
}
}
}
/// <summary>
/// Provides access to all the worksheets in the workbook.
/// </summary>
public ExcelWorksheets Worksheets {
get {
if (_worksheets == null) {
var sheetsNode = WorkbookXml.DocumentElement.SelectSingleNode(
"d:sheets",
_namespaceManager);
if (sheetsNode == null) {
sheetsNode = CreateNode("d:sheets");
}
_worksheets = new(_package, this, _namespaceManager, sheetsNode);
}
return (_worksheets);
}
}
/// <summary>
/// Provides access to named ranges
/// </summary>
public ExcelNamedRangeCollection Names => _names;
internal FormulaParser FormulaParser {
get {
if (_formulaParser == null) {
_formulaParser = new(new EpplusExcelDataProvider(this));
}
return _formulaParser;
}
}
public FormulaParserManager FormulaParserManager {
get {
if (_parserManager == null) {
_parserManager = new(FormulaParser);
}
return _parserManager;
}
}
private ExcelProtection _protection;
/// <summary>
/// Access properties to protect or unprotect a workbook
/// </summary>
public ExcelProtection Protection => _protection ??= new(NameSpaceManager, TopNode);
private ExcelWorkbookView _view;
/// <summary>
/// Access to workbook view properties
/// </summary>
public ExcelWorkbookView View {
get {
if (_view == null) {
_view = new(NameSpaceManager, TopNode, this);
}
return _view;
}
}
/// <summary>
/// URI to the workbook inside the package
/// </summary>
internal static Uri WorkbookUri { get; } = new("/xl/workbook.xml", UriKind.Relative);
/// <summary>
/// URI to the styles inside the package
/// </summary>
private static Uri StylesUri { get; } = new("/xl/styles.xml", UriKind.Relative);
/// <summary>
/// URI to the shared strings inside the package
/// </summary>
private static Uri SharedStringsUri { get; } = new("/xl/sharedStrings.xml", UriKind.Relative);
/// <summary>
/// Returns a reference to the workbook's part within the package
/// </summary>
internal ZipPackagePart Part => (_package.Package.GetPart(WorkbookUri));
/// <summary>
/// Provides access to the XML data representing the workbook in the package.
/// </summary>
internal XmlDocument WorkbookXml { get; }
private const string _codeModuleNamePath = "d:workbookPr/@codeName";
internal string CodeModuleName {
get => GetXmlNodeString(_codeModuleNamePath);
set => SetXmlNodeString(_codeModuleNamePath, value);
}
internal void CodeNameChange(string value) {
CodeModuleName = value;
}
private const string _date1904Path = "d:workbookPr/@date1904";
internal const double _date1904Offset = 365.5 * 4; // offset to fix 1900 and 1904 differences, 4 OLE years
/// <summary>
/// The date systems used by Microsoft Excel can be based on one of two different dates. By default, a serial number of 1 in Microsoft Excel represents January 1, 1900.
/// The default for the serial number 1 can be changed to represent January 2, 1904.
/// This option was included in Microsoft Excel for Windows to make it compatible with Excel for the Macintosh, which defaults to January 2, 1904.
/// </summary>
public bool Date1904 {
get => GetXmlNodeBool(_date1904Path, false);
set {
if (Date1904 != value) {
// Like Excel when the option it's changed update it all cells with Date format
foreach (var item in Worksheets) {
item.UpdateCellsWithDate1904Setting();
}
}
SetXmlNodeBool(_date1904Path, value, false);
}
}
private readonly XmlDocument _stylesXml;
/// <summary>
/// Package styles collection. Used internally to access style data.
/// </summary>
public ExcelStyles Styles {
get {
if (_styles == null) {
_styles = new(NameSpaceManager, _stylesXml, this);
}
return _styles;
}
}
/// <summary>
/// The office document properties
/// </summary>
public OfficeProperties Properties {
get {
if (_properties == null) {
// Create a NamespaceManager to handle the default namespace,
// and create a prefix for the default namespace:
_properties = new(_package, NameSpaceManager);
}
return _properties;
}
}
private readonly string _calcModePath = "d:calcPr/@calcMode";
/// <summary>
/// Calculation mode for the workbook.
/// </summary>
public ExcelCalcMode CalcMode {
get {
string calcMode = GetXmlNodeString(_calcModePath);
switch (calcMode) {
case "autoNoTable":
return ExcelCalcMode.AutomaticNoTable;
case "manual":
return ExcelCalcMode.Manual;
default:
return ExcelCalcMode.Automatic;
}
}
set {
switch (value) {
case ExcelCalcMode.AutomaticNoTable:
SetXmlNodeString(_calcModePath, "autoNoTable");
break;
case ExcelCalcMode.Manual:
SetXmlNodeString(_calcModePath, "manual");
break;
default:
SetXmlNodeString(_calcModePath, "auto");
break;
}
}
}
private const string _fullCalcOnLoadPath = "d:calcPr/@fullCalcOnLoad";
/// <summary>
/// Should Excel do a full calculation after the workbook has been loaded?
/// <remarks>This property is always true for both new workbooks and loaded templates(on load). If this is not the wanted behavior set this property to false.</remarks>
/// </summary>
public bool FullCalcOnLoad {
get => GetXmlNodeBool(_fullCalcOnLoadPath);
set => SetXmlNodeBool(_fullCalcOnLoadPath, value);
}
internal void Save() {
if (Worksheets.Count == 0) {
throw new InvalidOperationException("The workbook must contain at least one worksheet");
}
DeleteCalcChain();
UpdateDefinedNamesXml();
// save the style sheet
Styles.UpdateXml();
// save all the open worksheets
var isProtected = Protection.LockWindows || Protection.LockStructure;
foreach (ExcelWorksheet worksheet in Worksheets) {
if (isProtected && Protection.LockWindows) {
worksheet.View.WindowProtection = true;
}
worksheet.Save();
}
_package.Package.CreatePart(
SharedStringsUri,
ExcelPackage._contentTypeSharedString,
SaveSharedStringHandler);
Part.CreateRelationship(
UriHelper.GetRelativeUri(WorkbookUri, SharedStringsUri),
TargetMode.Internal,
ExcelPackage._schemaRelationships + "/sharedStrings");
// Data validation
ValidateDataValidations();
}
private void DeleteCalcChain() {
//Remove the calc chain if it exists.
Uri uriCalcChain = new Uri("/xl/calcChain.xml", UriKind.Relative);
if (_package.Package.PartExists(uriCalcChain)) {
Uri calcChain = new Uri("calcChain.xml", UriKind.Relative);
foreach (var relationship in Part.GetRelationships()) {
if (relationship.TargetUri == calcChain) {
Part.DeleteRelationship(relationship.Id);
break;
}
}
// delete the calcChain part
_package.Package.DeletePart(uriCalcChain);
}
}
private void ValidateDataValidations() {
foreach (var sheet in Worksheets) {
if (!(sheet is ExcelChartsheet)) {
sheet.DataValidations.ValidateAll();
}
}
}
private void SaveSharedStringHandler(StreamWriter sw) {
var cache = new StringBuilder();
cache.AppendFormat(
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\" ?><sst xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" count=\"{0}\" uniqueCount=\"{0}\">",
_sharedStrings.Count);
foreach (string t in _sharedStrings.Keys) {
SharedStringItem ssi = _sharedStrings[t];
if (ssi.isRichText) {
cache.Append("<si>");
ConvertUtil.ExcelEncodeString(cache, t);
cache.Append("</si>");
} else {
if (t.Length > 0
&& (t[0] == ' '
|| t[t.Length - 1] == ' '
|| t.Contains(" ")
|| t.Contains("\t")
|| t.Contains("\n")
|| t.Contains(
"\n"))) //Fixes issue 14849
{
cache.Append("<si><t xml:space=\"preserve\">");
} else {
cache.Append("<si><t>");
}
ConvertUtil.ExcelEncodeString(cache, ConvertUtil.ExcelEscapeString(t));
cache.Append("</t></si>");
}
if (cache.Length > 0x600000) {
sw.Write(cache.ToString());
cache = new();
}
}
cache.Append("</sst>");
sw.Write(cache.ToString());
sw.Flush();
Part.CreateRelationship(
UriHelper.GetRelativeUri(WorkbookUri, SharedStringsUri),
TargetMode.Internal,
ExcelPackage._schemaRelationships + "/sharedStrings");
}
private void UpdateDefinedNamesXml() {
try {
XmlNode top = WorkbookXml.SelectSingleNode("//d:definedNames", NameSpaceManager);
if (!ExistsNames()) {
if (top != null) {
TopNode.RemoveChild(top);
}
return;
}
if (top == null) {
CreateNode("d:definedNames");
top = WorkbookXml.SelectSingleNode("//d:definedNames", NameSpaceManager);
} else {
top.RemoveAll();
}
foreach (ExcelNamedRange name in _names) {
XmlElement elem = WorkbookXml.CreateElement("definedName", ExcelPackage._schemaMain);
top.AppendChild(elem);
elem.SetAttribute("name", name.Name);
if (name.IsNameHidden) {
elem.SetAttribute("hidden", "1");
}
if (!string.IsNullOrEmpty(name.NameComment)) {
elem.SetAttribute("comment", name.NameComment);
}
SetNameElement(name, elem);
}
foreach (ExcelWorksheet ws in _worksheets) {
if (!(ws is ExcelChartsheet)) {
foreach (ExcelNamedRange name in ws.Names) {
XmlElement elem = WorkbookXml.CreateElement("definedName", ExcelPackage._schemaMain);
top.AppendChild(elem);
elem.SetAttribute("name", name.Name);
elem.SetAttribute("localSheetId", name.LocalSheetId.ToString());
if (name.IsNameHidden) {
elem.SetAttribute("hidden", "1");
}
if (!string.IsNullOrEmpty(name.NameComment)) {
elem.SetAttribute("comment", name.NameComment);
}
SetNameElement(name, elem);
}
}
}
} catch (Exception ex) {
throw new("Internal error updating named ranges ", ex);
}
}
private void SetNameElement(ExcelNamedRange name, XmlElement elem) {
if (name.IsName) {
if (string.IsNullOrEmpty(name.NameFormula)) {
if ((name.NameValue.GetType().IsPrimitive
|| name.NameValue is double
|| name.NameValue is decimal)) {
elem.InnerText = Convert
.ToDouble(name.NameValue, CultureInfo.InvariantCulture)
.ToString("R15", CultureInfo.InvariantCulture);
} else if (name.NameValue is DateTime time) {
elem.InnerText = time.ToOADate().ToString(CultureInfo.InvariantCulture);
} else {
elem.InnerText = "\"" + name.NameValue + "\"";
}
} else {
elem.InnerText = name.NameFormula;
}
} else {
elem.InnerText = name.FullAddressAbsolute;
}
}
/// <summary>
/// Is their any names in the workbook or in the sheets.
/// </summary>
/// <returns>?</returns>
private bool ExistsNames() {
if (_names.Count == 0) {
foreach (ExcelWorksheet ws in Worksheets) {
if (ws is ExcelChartsheet) {
continue;
}
if (ws.Names.Count > 0) {
return true;
}
}
} else {
return true;
}
return false;
}
internal bool ExistsTableName(string name) {
foreach (var ws in Worksheets) {
if (ws.Tables._tableNames.ContainsKey(name)) {
return true;
}
}
return false;
}
internal bool ExistsPivotTableName(string name) {
foreach (var ws in Worksheets) {
if (ws.PivotTables._pivotTableNames.ContainsKey(name)) {
return true;
}
}
return false;
}
internal void AddPivotTable(string cacheId, Uri defUri) {
CreateNode("d:pivotCaches");
XmlElement item = WorkbookXml.CreateElement("pivotCache", ExcelPackage._schemaMain);
item.SetAttribute("cacheId", cacheId);
var rel = Part.CreateRelationship(
UriHelper.ResolvePartUri(WorkbookUri, defUri),
TargetMode.Internal,
ExcelPackage._schemaRelationships + "/pivotCacheDefinition");
item.SetAttribute("id", ExcelPackage._schemaRelationships, rel.Id);
var pivotCaches = WorkbookXml.SelectSingleNode("//d:pivotCaches", NameSpaceManager);
pivotCaches.AppendChild(item);
}
internal List<string> _externalReferences = new();
//internal bool _isCalculated=false;
internal void GetExternalReferences() {
XmlNodeList nl = WorkbookXml.SelectNodes(
"//d:externalReferences/d:externalReference",
NameSpaceManager);
if (nl != null) {
foreach (XmlElement elem in nl) {
string rId = elem.GetAttribute("r:id");
var rel = Part.GetRelationship(rId);
var part = _package.Package.GetPart(UriHelper.ResolvePartUri(rel.SourceUri, rel.TargetUri));
XmlDocument xmlExtRef = new XmlDocument();
LoadXmlSafe(xmlExtRef, part.GetStream());
XmlElement book =
xmlExtRef.SelectSingleNode("//d:externalBook", NameSpaceManager) as XmlElement;
if (book != null) {
string rIdExtRef = book.GetAttribute("r:id");
var relExtRef = part.GetRelationship(rIdExtRef);
if (relExtRef != null) {
_externalReferences.Add(relExtRef.TargetUri.OriginalString);
}
}
}
}
}
internal void ReadAllTables() {
if (_nextTableID > 0) {
return;
}
_nextTableID = 1;
_nextPivotTableID = 1;
foreach (var ws in Worksheets) {
if (!(ws
is ExcelChartsheet)) //Fixes 15273. Chartsheets should be ignored.
{
foreach (var tbl in ws.Tables) {
if (tbl.Id >= _nextTableID) {
_nextTableID = tbl.Id + 1;
}
}
foreach (var pt in ws.PivotTables) {
if (pt.CacheID >= _nextPivotTableID) {
_nextPivotTableID = pt.CacheID + 1;
}
}
}
}
}
}