用例
我有一个 Windows 服务,它利用 OpenXML sdk 每天将表格定价数据写入 Excel 工作表。此报告可通过文件共享获得,也可作为附件发送。工作簿中还有其他几张表,其中包含引用通过 OpenXML 写入的数据以进行可视化等的公式。
如果未设置 ForceFullCalculation 属性,则公式不会在打开时更新,直到对公式依赖链中最靠下的单元格进行编辑。
document.WorkbookPart.Workbook.CalculationProperties.ForceFullCalculation = true;
此行解决了所有计算问题,因为所有公式在打开时都会重新计算。如果在 Excel 桌面客户端中打开。
问题
在移动 Office 应用程序、移动 Excel 应用程序、Excel Online 和电子邮件预览应用程序上,在对触发重新计算的单元格进行编辑之前,不会计算公式。全部显示为 N/A。
努力解决
对于移动/网络/附件查看器,尚未解决此问题的事情:
- ForceFullCalculation = true
- CalculationOnSave = true
- CalculationMode = CalculateModeValues.Auto(Excel 自动计算模式)
- 移除
CalculaitonChainPart
元素。 - 解压缩 xlsx 并从 CalcChain 文件中删除/编辑/删除单元格数据。
- 通过非桌面 Excel 版本手动单击“计算工作簿”/“计算单元格”/“计算表”按钮
- 在打开之前将文件发布到 sharepoint online / office 365
什么“解决”了这个问题
- 在分发工作表之前在桌面版 Excel 上打开文件。这会起作用,但需要手动干预,这对于基于服务的应用程序是不切实际的。
- 在非桌面版本的 Excel 上打开文件并在“公式树”中编辑单元格值。
目标
我希望有人可以对这个问题提供额外的见解,并解决这是否可以使用 OpenXML sdk 解决。如果 excel 的非桌面版本不尊重该ForceFullCalculation
属性,是否可以通过 XML 进行任何操作以使 excel 将单元格视为脏并运行重新计算?
理想情景
我的服务创建一个新的 xlsx 文件并通过 OpenXML sdk 写入数据。然后将该文件写入文件共享并作为电子邮件附件发送。收件人可以在桌面、移动设备、Web 和不太重要的附件查看器上打开工作簿,并查看公式的结果而不是 N/A,直到找到要编辑的特定单元格以诱使 Excel 更新所有值。
补充说明
我痛苦地意识到这是对 Excel/OpenXML 的误用——像 tableau/power BI 这样的报告解决方案更适合我的用例,但这是最初由业务用户创建的工作表,并且一直是手动工作量一个人每天更新多年。自动化工作表的日常数据输入和分发允许我们的用户继续使用引用此工作表的其他工作表,并且不需要花费大量精力来重新定义整个过程,因为基本上可以归结为人们每天早上查看的报告。
最终,我们会有更好的解决方案,但这是一项缓慢而持续的努力。我希望有一种前进的方式可以满足现在的需要。
感谢您的阅读和任何帮助:)
示例代码
using System;
using System.IO;
using System.Threading.Tasks;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Spreadsheet;
namespace SampleProject.Services {
public interface IExportPriceDataService
{
public Task CreateDailyWorkbookAsync();
}
public class ExportPriceDataService : IExportPriceDataService
{
private readonly IDistributionService _distributionService;
public ExportPriceDataService(IDistributionService distributionService)
{
_ditributionService = distributionService;
}
public async Task CreateDailyWorkbookAsync()
{
// Template file - contains constant data (headers, formulas, etc.) but no data.
var templateFile = "C:\\DailyExportTemplate.xlsx";
var filename = "C:\\DailyExport.xlsx";
// Load price data from the database - assume already shaped into the PriceData data structure (classes below).
List<PriceData> priceData = await fetchTabularPriceDataAsync(DateTime.Now.Year, DateTime.Now.Month);
// Delete previous day exports.
if (File.Exists(filename))
{
File.Delete(filename);
}
// Create today's copy.
File.Copy(templateFile, filename);
// Populate today's copy with data.
using (SpreadsheetDocument document = SpreadsheetDocument.Open(filename))
{
if (document == null) return;
var workbookPart = document.WorkbookPart;
// Extension method to get the worksheet part by the sheet name.
var worksheetPart = workbookPart.GetWorksheetPartByName("PriceDataSheet");
writePriceData(worksheetPart, priceData);
document.WorkbookPart.Workbook.CalculationProperties.CalculationMode = CalculateModeValues.Auto;
document.WorkbookPart.Workbook.CalculationProperties.CalculationOnSave = true;
document.WorkbookPart.Workbook.CalculationProperties.ForceFullCalculation = true;
document.Save();
}
_distributionService.PublishToSharepointOnline(filename);
_distributionService.SendAsEmailAttachment(filename);
}
private void writePriceData(WorksheetPart worksheetPart, List<PriceData> priceData)
{
foreach (var item in priceData)
{
// Match ItemName with constant column headers in the sheet.
var col = GetColumnForItem();
// Header row is always row 1.
var row = 1;
// Custom extension methods for creating and writing to cells.
// Internal code from: https://docs.microsoft.com/en-us/office/open-xml/how-to-insert-text-into-a-cell-in-a-spreadsheet
worksheetPart.InsertCellInWorksheet(col, row)
.SetCellType(CellValues.String)
.SetCellValue(new CellValue(item.ItemName));
foreach (var price in Item.Prices)
{
// Row # matches up with the day plus offset of 1 for the header row.
row = Date.day + 1;
worksheetPart.InsertCellInWorksheet(col, row)
.SetCellType(CellValues.Number)
.SetCellValue(new CellValue(price.Price));
}
}
}
}
public class PriceData
{
public string ItemName {get; set;}
public List<PriceDate> Prices { get; set; }
}
public class PriceDate
{
public DateTime Date {get; set;}
public decimal Price {get; set;}
}
}