using the example here How to Copy a Worksheet within a Workbook
I have successfully been able to clone/copy sheets in my excel file, however when I open the excel the 2nd sheet is the active(visible) sheet. I haven't been able to locate a property that could do thins.....Is there any way to specify what sheet is active?
I've tried to force it by opening and editing the first sheet in the file thinking it was the last edited sheet that was active but that didn't work either.
any help would be great. TIA
update: looking at the workbook.xml created when renaming the .xlsx to .zip I came accross the 'activeTab' property. made a quick change to my code and seems to work just fine
public void SetFirstSheetInFocus(String xlsxFile)
{
using (SpreadsheetDocument spreadSheet = SpreadsheetDocument.Open(xlsxFile, true))
{
//Get a reference to access the main Workbook part, which contains all references
WorkbookPart _workbookPart = spreadSheet.WorkbookPart;
if (_workbookPart != null)
{
WorkbookView _workbookView = spreadSheet.WorkbookPart.Workbook.BookViews.ChildElements.First<WorkbookView>();
if (_workbookView != null)
{
_workbookView.ActiveTab = 0; // 0 for first or whatever tab you want to use
}
// Save the workbook.
_workbookPart.Workbook.Save();
}
}
}
If the name of your sheet is in the variable
sheetName
you can set the sheet with that name active like this:
using (var spreadsheetDoc = SpreadsheetDocument.Open(emptyHIPTemplatePath, true /* isEditable */, new OpenSettings { AutoSave = false }))
{
var workbookPart = spreadsheetDoc.WorkbookPart;
var workBook = spreadsheetDoc.WorkbookPart.Workbook;
var sheet = workBook.Descendants<Sheet>().FirstOrDefault(s => s.Name == sheetName);
var sheetIndex = workBook.Descendants<Sheet>().ToList().IndexOf(sheet);
var workBookView = workBook.Descendants<WorkbookView>().First();
workBookView.ActiveTab = Convert.ToUInt32(sheetIndex);
...
workBook.Save();
}
From Vincent Tan's book:
The SheetId property doesn't determine the order. The order of
appending the Sheet classes to the Sheets class, does.
When you add a sheet, it gets the next index, but a single sheet does not have an index. OpenXML gives it an index when you are done adding sheets. Again, from Vincent Tan's book:
Let's say you have 3 worksheets named Sheet1, Sheet2 and Sheet3.
However, when you appended the corresponding Sheet classes, you did it
as Sheet2, Sheet3 and Sheet1, in that order.
Related
I am trying to embed object into .xlsx document and copy sheets with embedded objects.
1. Copying sheets
This looks like straight forward issue. I have created method to copy the sheets:
static void CopySheetInsideWorkbook(string filename, string sheetName, string clonedSheetName)
{
using (SpreadsheetDocument spreadsheetDocument = SpreadsheetDocument.Open(filename, true))
{
WorkbookPart workbookPart = spreadsheetDocument.WorkbookPart;
WorksheetPart sourceSheetPart = GetWorksheetPartByName(spreadsheetDocument, sheetName);
SpreadsheetDocument tempSheet =
SpreadsheetDocument.Create(new MemoryStream(), spreadsheetDocument.DocumentType);
WorkbookPart tempWorkbookPart = tempSheet.AddWorkbookPart();
WorksheetPart tempWorksheetPart = tempWorkbookPart.AddPart<WorksheetPart>(sourceSheetPart);
WorksheetPart clonedSheet = workbookPart.AddPart<WorksheetPart>(tempWorksheetPart);
Sheets sheets = workbookPart.Workbook.GetFirstChild<Sheets>();
Sheet copiedSheet = new Sheet
{
Name = clonedSheetName,
Id = workbookPart.GetIdOfPart(clonedSheet),
SheetId = (uint) sheets.ChildElements.Count + 1
};
sheets.Append(copiedSheet);
workbookPart.Workbook.Save();
}
}
The ouput is as expected but the embedded files are copied as "Picture" rather than "Object". I unzipped .xlsx file and all looks legit ie. similar to the sheet I copied. Yet still the file cannot be opened on the copied sheet. All images, strings are displayed in correct way.
2. Embedding the object
What I understand I need to do is:
Convert object into oleObject - this will be separate fun.
Add DrawingsPart - It looks like it's read-only and I can only add ImagePart.
Embed Object
Connect both drawing and embedded object part toghether and allocate to some range in spreadsheet.
static void EmbedFileXlsx(string path, string embeddedFilePath, string placeholderImagePath)
{
using (SpreadsheetDocument spreadsheetDocument = SpreadsheetDocument.Open(path, true))
{
WorksheetPart sourceSheetPart = GetWorksheetPartByName(spreadsheetDocument, "Test");
var imagePart = sourceSheetPart.AddImagePart(ImagePartType.Emf, "rId1");
imagePart.FeedData(File.Open(placeholderImagePath, FileMode.Open));
var embeddedObject =
sourceSheetPart.AddEmbeddedObjectPart(#"application/vnd.openxmlformats-officedocument.oleObject");
embeddedObject.FeedData(File.Open(embeddedFilePath, FileMode.Open));
spreadsheetDocument.Save();
}
}
This code just adds embedded objects into the file but does not create any type of relationship between them. This means that file is not visible on the spreadsheet.
I tried copying sheets using ClosedXML as well but unfortunately this is not supported nor the embedding.
I also managed to understand how I can copy sheet into new document with all embedded objects using .xml files inside spreadsheet but I do not think this would be much productive and I would like to achieve this using all the methods inside OpenXML. It looks everything is there but something is amiss.
I am no expert in this, but could this help you on your way?
spreadsheetDocument.CreateRelationshipToPart(SOME ID);
I need to merge the worksheets of some workbooks into one new workbook. What I tried is this, but I am getting "Cannot insert the OpenXmlElement "newChild" because it is part of a tree.".
using (var document = SpreadsheetDocument.Create(destinationFileName, SpreadsheetDocumentType.Workbook))
{
// Add a WorkbookPart to the document
var workbookPart = document.AddWorkbookPart();
workbookPart.Workbook = new Workbook();
// Add a WorksheetPart to the WorkbookPart
var worksheetPart = workbookPart.AddNewPart<WorksheetPart>();
worksheetPart.Worksheet = new Worksheet(new SheetData());
foreach (var sourceFileName in sourceFileNames)
{
using (var sourceDocument = SpreadsheetDocument.Open(sourceFileName, false))
{
var sheet = (Sheet)sourceDocument.WorkbookPart.Workbook.Sheets.FirstChild;
workbookPart.Workbook.AppendChild(new Worksheet(sheet));
}
}
}
The error message means that you are trying to add an OpenXmlElement (a Sheet in this case) that already has a parent OpenXmlCompositeElement (a Sheets object in this case) as a new child to another OpenXmlCompositeElement (a Worksheet in this case).
In the following line item, you are grabbing a reference to a Sheet object that already has a parent Sheets object.
var sheet = (Sheet)sourceDocument.WorkbookPart.Workbook.Sheets.FirstChild;
With new Worksheet(sheet), you are trying to add that same sheet to another parent, i.e., the Worksheet instance that you are creating. That does not work.
Assuming for a moment that it makes sense to add a Sheet to a Worksheet, your second line would have to be rewritten as follows:
workbookPart.Workbook.AppendChild(new Worksheet(sheet.CloneNode(true)));
In the above line of code, sheet is replaced with sheet.CloneNode(true), which makes a deep copy of your original Sheet object that does not have a parent. Thus, the clone can be added as a new child to another parent.
While the above solution is an answer to your immediate question (because it avoids the error message), your code does not make sense, because it does not create valid Open XML markup. Worksheet instances should not be added to Workbook instances, and Sheet instances should not be added to Worksheet instances. This is not how you would copy multiple worksheets, which is a very complicated task that requires you to create multiple worksheet parts, link those worksheet parts to your workbook part, and consider shared strings, styles, and other things.
I'm trying to read a fairly simple Excel spreadheet (saved as an xlsx) using the OpenXML nuget package.
I'm able to locate the specific Sheet I'm interested in, but calling sheet.GetFirstChild<SheetData>() on it, in order to access the cell values, always returns null. I've looked at several examples online, and they all seem to agree that this is the correct way to access the data.
The xlsx file I'm trying to read can be downloaded here.
Here is the code I'm using to read the file. The line var data = sheet.GetFirstChild<SheetData>(); is where the null is returned.
void Main()
{
using (var s = System.IO.File.OpenRead("c:\\ImportTemplate.xlsx"))
using (var document = SpreadsheetDocument.Open(s, false))
{
foreach (var worksheetPart in document.WorkbookPart.WorksheetParts)
{
Sheet sheet = GetSheetFromWorkSheet(document.WorkbookPart, worksheetPart);
var name = sheet.Name.Value;
if (name == "ImportData")
{
var data = sheet.GetFirstChild<SheetData>(); // <-- data is set to null
foreach (var row in data.Descendants<Row>()){
// [...]
}
}
}
}
}
public static Sheet GetSheetFromWorkSheet(WorkbookPart workbookPart, WorksheetPart worksheetPart)
{
string relationshipId = workbookPart.GetIdOfPart(worksheetPart);
IEnumerable<Sheet> sheets = workbookPart.Workbook.Sheets.Elements<Sheet>();
return sheets.FirstOrDefault(s => s.Id.HasValue && s.Id.Value == relationshipId);
}
The above code as an easily runnable LINQPad file can be downloaded from here.
What am I doing wrong?
As I'm writing quite large xlsx files, I'm using OpenXmlReader and OpenXmlWriter as recommended on this page:
https://blogs.msdn.microsoft.com/brian_jones/2010/06/22/writing-large-excel-files-with-the-open-xml-sdk/
What I only do is change formulas inside existing cells and making sure that their value is discarded so that it is recalculated when Excel opens the file.
Here is the function that I'm using:
public void Save(Stream Input, Stream Output)
{
Input.Position = 0;
if (Input != Output)
Input.CopyTo(Output);
using (SpreadsheetDocument document = SpreadsheetDocument.Open(Output, true))
{
WorkbookPart wbPart = document.WorkbookPart;
// force recalculation as we change formulas
wbPart.Workbook.CalculationProperties.ForceFullCalculation = true;
wbPart.Workbook.CalculationProperties.FullCalculationOnLoad = true;
// store the worksheet parts in a separate list because the loop below
// adds and removes elements inside wbPart.WorksheetParts
List<WorksheetPart> originalWsParts = new List<WorksheetPart>();
foreach (WorksheetPart inputWsPart in wbPart.WorksheetParts)
originalWsParts.Add(inputWsPart);
// process all worksheets in the workbook
foreach (WorksheetPart inputWsPart in originalWsParts)
{
string origninalSheetId = wbPart.GetIdOfPart(inputWsPart);
WorksheetPart replacementWsPart = wbPart.AddNewPart<WorksheetPart>();
string replacementWsPartId = wbPart.GetIdOfPart(replacementWsPart);
OpenXmlReader reader = OpenXmlReader.Create(inputWsPart);
OpenXmlWriter writer = OpenXmlWriter.Create(replacementWsPart);
while (reader.Read())
{
logger.Debug(reader.ElementType.Name);
if (reader.ElementType == typeof(Cell) && reader.IsStartElement)
{
writer.WriteStartElement(reader);
// write the cell content, changing the formula and skipping the value
while (reader.Read() && !(reader.ElementType == typeof(Cell) && reader.IsEndElement))
{
if (reader.IsStartElement)
{
if (reader.ElementType == typeof(CellFormula))
{
CellFormula element = reader.LoadCurrentElement() as CellFormula;
element.Text = "SUM(1,2)";
element.CalculateCell = true;
writer.WriteElement(element);
}
else if (reader.ElementType != typeof(CellValue))
{
writer.WriteStartElement(reader);
string elementText = reader.GetText();
if (!String.IsNullOrEmpty(elementText))
writer.WriteString(elementText);
}
}
else if (reader.IsEndElement)
{
if (reader.ElementType != typeof(CellValue))
writer.WriteEndElement();
}
}
writer.WriteEndElement();
}
else
{
if (reader.IsStartElement)
{
writer.WriteStartElement(reader);
string elementText = reader.GetText();
if (!String.IsNullOrEmpty(elementText))
writer.WriteString(elementText);
}
else if (reader.IsEndElement)
{
writer.WriteEndElement();
}
}
}
reader.Close();
writer.Close();
Sheet sheet = wbPart.Workbook.Descendants<Sheet>()
.Where(s => s.Id.Value.Equals(origninalSheetId)).First();
sheet.Id.Value = replacementWsPartId;
wbPart.DeletePart(inputWsPart);
}
}
}
It works quite well on the simplest workbooks, but it creates unreadable files when there are drawings inside sheets in the file.
For instance, if I have a drawings on Sheet1, when Excel opens the saved file, it complains that the file has unreadable parts and shows me the drawings in the list of things it has deleted.
I unzipped the xlsx file and compared the sheetX.xml files, and apart from the added x: prefix, they are the same.
Obviously, I'm missing something but reading the various docs I could find, nothing came to me. I believe there is a reference to the original worksheet part that has not been updated but I don't see any drawings descendant in the workbook.
Any help is most welcome.
Update
I looked more closely at the files content and there are two folders missing inside the xl folder: charts and drawings
So clearly, I'm missing code that would add those into the final archive, but I can't (yet) figure out what code this is.
The above code creates a new WorksheetPart inside the loop, when the intention was always to clone the input part.
I thus went looking for a way to clone a worksheet, but there is no ClonePart() method on WorkbookPart.
Fortunately for me, someone else already had this issue and Brian Jones did a part on his blog about this:
https://blogs.msdn.microsoft.com/brian_jones/2009/02/19/how-to-copy-a-worksheet-within-a-workbook/
So, I changed the start of the using statement to this:
using (SpreadsheetDocument document = SpreadsheetDocument.Open(Output, true), tmpDocument = SpreadsheetDocument.Create(new MemoryStream(), document.DocumentType))
{
WorkbookPart wbPart = document.WorkbookPart;
WorkbookPart tmpWbPart = tmpDocument.AddWorkbookPart();
Then instead of calling AddNewPart<WorksheetPart>, I now have the following code:
// can't directly clone, so add to the temporary workbook part and then back
// into the working workbook part
WorksheetPart tmpWsPart = tmpWbPart.AddPart(inputWsPart);
WorksheetPart replacementWsPart = wbPart.AddPart(tmpWsPart);
tmpWbPart.DeletePart(tmpWsPart);
tmpWsPart = null;
With those changes, I now have my Drawing part in the worksheet, the associated folders in the xlsx file.
As a result, Excel no longer complains when opening the file and the graphs are all there and updated.
I am trying to programmatically group (shift select) sheets in Excel using Open XML API. I know how to do it in Interop way like:
String[] sheetsToBeSelected = {"Sheet1","Sheet2","Sheet3"};
excel.Workbook workbook = ExcelApp.ActiveWorkbook;
excel.Sheets worksheets = workbook.Worksheets;
((excel.Sheets)worksheets.get_Item(sheetsToBeSelected)).Select();
Tried hard to translate it to Open XML API, but no luck. Please help.
Thanks.
Try setting the TabSelected property of SheetView. Here is the sample code
foreach(Sheet sht in myWorkBook.WorkBook.Descendants<Sheet>())
{
WorkSheetPart wrkShtPart = (WorkSheetPart)myWorkBook.GetPartById(sht.Id);
SheetViews shtViews = wrkShtPart.WorkSheet.GetFirstChild<SheetViews>();
SheetView shtView = shtViews.GetFirstChild<SheetView>();
shtView.TabSelected = null;
}