Is it possible to enable "Sharing" on excel documents through OpenXML or ClosedXML? Or any other library if it can help... I believe this is usually performed when you save the document (at least that's how it works in VBA), but I can't find how to specify saving arguments in C#.
I'd like to avoid using InterOp since I might batch this process on multiple files through a network.
EDIT: According to some old pages from 2009, there are limitations where OpenXML cannot operate protected files. However, would that apply to sharing too?
Sharing Excel documents using OpenXML SDK is not well documented.
I did some tests and found that it is possible to enable sharing on Excel documents
using OpenXML SDK. The following steps are necessary to enable sharing:
Add a WorkbookUserDataPart to your Excel document. Add an empty Users collection
to the part. In this collection Excel stores all users who currently have
this shared workbook open.
Add a WorkbookRevisionHeaderPart to your Excel document. Add a Headers collection
to the part. In this collection Excel will store references to history, version and revision
information. Add a first element (Header) to the collection which contains the
SheetIdMap (used for tracking revision records). In the code sample below
I've added all worksheets included in the document.
Furthermore add a WorkbookRevisionLogPart to the workbook's revision header part.
In the log part a list of revision made to the document is stored.
The code sample below shows how to enable sharing on an Excel document.
The code also checks whether sharing is already enabled on a document.
Before you enable sharing you should create a backup of your original documents.
using (SpreadsheetDocument sd = SpreadsheetDocument.Open("c:\\temp\\enable_sharing.xlsx", true))
{
WorkbookPart workbookPart = sd.WorkbookPart;
if (workbookPart.GetPartsCountOfType<WorkbookRevisionHeaderPart>() != 0)
{
Console.Out.WriteLine("Excel document already shared!");
return;
}
// Create user data part if it does not exist.
if (workbookPart.GetPartsCountOfType<WorkbookUserDataPart>() == 0)
{
Console.Out.WriteLine("Adding user data part");
WorkbookUserDataPart workbookUserDataPart = workbookPart.AddNewPart<WorkbookUserDataPart>();
Users users = new Users() { Count = (UInt32Value)0U };
users.AddNamespaceDeclaration("r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships");
workbookUserDataPart.Users = users;
}
// Create revision header part and revision log part.
WorkbookRevisionHeaderPart workbookRevisonHeaderPart = workbookPart.AddNewPart<WorkbookRevisionHeaderPart>();
WorkbookRevisionLogPart workbookRevisionLogPart = workbookRevisonHeaderPart.AddNewPart<WorkbookRevisionLogPart>();
// Create empty collection of revisions.
Revisions revisions = new Revisions();
revisions.AddNamespaceDeclaration("r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships");
workbookRevisionLogPart.Revisions = revisions;
string lastSetOfRevisionsGuid = Guid.NewGuid().ToString("B");
// Create headers collection (references to history, revisions)
Headers headers = new Headers() { Guid = lastSetOfRevisionsGuid };
headers.AddNamespaceDeclaration("r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships");
int worksheetPartsCount = workbookPart.GetPartsCountOfType<WorksheetPart>();
// Create first element in headers collection
// which contains the SheetIdMap.
Header header = new Header() { Guid = lastSetOfRevisionsGuid, DateTime = DateTime.Now,
MaxSheetId = (UInt32Value)(uint)worksheetPartsCount+1, UserName = "hans", Id = "rId1" };
// Create the list of sheet IDs that are used for tracking
// revision records. For every worksheet in the document
// create one SheetId.
SheetIdMap sheetIdMap = new SheetIdMap() { Count = (UInt32Value)(uint)worksheetPartsCount };
for (uint i = 1; i <= worksheetPartsCount; i++)
{
SheetId sheetId = new SheetId() { Val = (UInt32Value)i };
sheetIdMap.Append(sheetId);
}
header.Append(sheetIdMap);
headers.Append(header);
workbookRevisonHeaderPart.Headers = headers;
}
Related
I'm moving some legacy code using Office Interop libraries to epplus, one thing I can't figure out is how to set a Workbook to ask the user on open the file to open it read only. Like if the user clicks on File -> Info -> Protect Workbook -> Always Open Read-Only
I've tried to set the DocSecurity property stated here (https://sno.phy.queensu.ca/~phil/exiftool/TagNames/OOXML.html), but to no success. excelWorkBook.Properties.SetExtendedPropertyValue("DocSecurity", "2");
I also tried to add the following node to the workbookxml <fileSharing readOnlyRecommended="1"/>
I even tried to compare the unzipped excel files protected, non protected, but there were too many changes.
It can be done but it is not really straightforward. Setting DocSecurity can be done by generating the Workbook.Properties object. But that is only half of it. You also need to set the flag inside the Workbook itself which can only be done via XML manipulation.
[TestMethod]
public void DocSecurity_Test()
{
//https://stackoverflow.com/questions/58335624/is-there-a-programmatically-way-for-excels-protect-workbook
var fi = new FileInfo(#"c:\temp\DocSecurity_Test.xlsx");
if (fi.Exists)
fi.Delete();
using (var package = new ExcelPackage(fi))
{
//Create a simple workbook
var workbook = package.Workbook;
var worksheet = workbook.Worksheets.Add("Sheet1");
worksheet.Cells["A1"].Value = "Test";
//Extended properties is a singleton so reference it to generate the app.xml file
//needed and add the security setting
var extProps = workbook.Properties;
extProps.SetExtendedPropertyValue("DocSecurity", "2");
//Also need to tell the workbook itself but can only do it via XML
var xml = workbook.WorkbookXml;
var att = xml.CreateAttribute("readOnlyRecommended");
att.Value = "1";
const string mainNs = #"http://schemas.openxmlformats.org/spreadsheetml/2006/main";
var fileSharing = xml.CreateElement("fileSharing", mainNs);
fileSharing.Attributes.Append(att);
//Element must be at the beginning of the tree
xml.DocumentElement.PrependChild(fileSharing);
package.Save();
}
}
Which will look like this:
I am creating an offer generator for my company. The generated offer must contains the General Conditions of Sale in the second worksheet (the offer details are displayed in the first worksheet). The General Conditions of Sale are contained into a word document, so I have to inject them into the worksheet. Last constraint, I have to respect the word's two column layout.
I already have the logic (it can be improved but that's not the point of my question) to extract and insert them into the worksheet. I am using a predefined excel file (with header, footer, column sizes, etc, already defined) and I simply insert the terms in the right merged cells, using ClosedXml.
My issue concerns the printing of that offer. When I open the generated offer, directly click on Print and select print the entire workbook, I can then scrolldown in the preview to check if everything is good. When I arrive to the second worksheet, the font and/or the font size are not the ones I specified in the code.
If I open the second worksheet at least once, and then do the former procedure, everything is back to normal and I can print my offer with the right font and font size. I encounter the same issue when I try to export the offer to pdf.
Here is the preview when I do not open the second worksheet, with wrong font size. And Here is the preview if I open the second worksheet at least one before printing, with good font and font size.
Here is my code (notice in the PrintTerms function the specification of the font and the size):
class Program
{
const int nbRowByPage = 67;
const int nbCharByPage = 3650;
const string generalTermsFileName = "ConditionsGeneralSales_R5.docx";
const string baseTemplateFileName = "TemplateOffer.xlsx";
const string generatedExcelFileName = "Offer.xlsx";
static void Main(string[] args)
{
// Deletes any existing excel file
var fi = new FileInfo(generatedExcelFileName);
if (fi.Exists)
{
fi.Delete();
}
using (var workbookDocument = new XLWorkbook(baseTemplateFileName))
{
// Fills first worksheet
// [..]
// Gets the general terms from the word and injects them into the second worksheet
using (WordprocessingDocument wordprocessingDocument = WordprocessingDocument.Open(generalTermsFileName, true))
{
// Extracts the paragraphs
var paragraphs = wordprocessingDocument.MainDocumentPart.Document.Body
.Descendants<Paragraph>()
.Where(p => !String.IsNullOrWhiteSpace(p.InnerText))
.ToList();
// Gets the second worksheet
var conditionsWS = workbookDocument.Worksheet(2);
// Injects the paragraphs in the worksheets
PrintTerms(conditionsWS, paragraphs);
}
// Saves the excel
workbookDocument.SaveAs(generatedExcelFileName, true);
}
// Opens the generated excel with MS Excel
Process.Start(generatedExcelFileName);
}
/// <summary>
/// Adds the specified paragraph into the specified worksheet according this methodology:
/// - Splits the paragraphs into two columns
/// - Fills the excel columns alternatively in order to looks like a two columns word text
/// - Manage the break page
/// </summary>
/// <param name="ws"></param>
/// <param name="generalTerms"></param>
static void PrintTerms(IXLWorksheet ws, List<Paragraph> generalTerms)
{
var currentParagraphIndex = 0;
var index = 1;
while (currentParagraphIndex < generalTerms.Count)
{
var nbChar = 0;
var currentParagraphs = generalTerms.Skip(currentParagraphIndex).TakeWhile(p => (nbChar += p.InnerText.Length) < nbCharByPage).ToList();
var row = ((int)Math.Ceiling(index / 2d) - 1) * nbRowByPage + 1;
var column = index % 2 == 0 ? 3 : 1;
var currentCell = ws.Cell(row, column);
currentCell.Style.Alignment.SetHorizontal(XLAlignmentHorizontalValues.Justify);
currentCell.Style.Alignment.SetVertical(XLAlignmentVerticalValues.Top);
currentCell.Style.Font.SetFontName("Arial");
currentCell.Style.Font.SetFontSize(8);
foreach (var paragraph in currentParagraphs)
{
foreach (var run in paragraph.Descendants<Run>())
{
var text = currentCell.RichText.AddText(run.InnerText);
if (run.RunProperties.Bold != null)
{
text.SetBold();
}
if (run.RunProperties.Italic != null)
{
text.SetItalic();
}
if (run.RunProperties.Underline != null)
{
text.SetUnderline();
}
}
// Break line
currentCell.RichText.AddNewLine();
currentCell.RichText.AddNewLine();
}
ws.Range(row, column, row + nbRowByPage - 1, column).Merge();
index++;
currentParagraphIndex += currentParagraphs.Count();
}
}
}
To run this code, you will need to put at the root of the console application the two following files: terms and template (check copy if newer in the files properties).
I also tried a lot of combination like:
Setting the workbook global style:
workbookDocument.Style
.Font.SetFontName("Arial")
.Font.SetFontSize(8);
Setting the worksheet style:
offerWS.Style
.Font.SetFontName("Arial")
.Font.SetFontSize(8);
Setting the cell's RichText style:
generalTermsCell.RichText
.SetFontName("Arial")
.SetFontSize(8);
All of this, at the begining or at the ending of the file modification. Nothing works.
I don't think it is a bug into ClosedXML. Excel does not ask me for saving the changes when leaving the app if I had opened the second worksheet. So the generated files do not seem corrupted. It looks like an Excel bug, but may be there is a way in ClosedXML to workaround that?
I am exporting protected excel sheet using EPPlus in my winform(C#) project. Now I want functionality to allow user to edit ranges in that protected excel sheet using same plugin.
It would be great if you provide Code snippet.
Thanks in advance.
var fileName = "sample.xlsx";
var fileInfo = new FileInfo(fileName);
using (var excel = new ExcelPackage(fileInfo))
{
var ws = excel.Workbook.Worksheets.Add("sheet1");
ws.Protection.IsProtected = true;
ws.ProtectedRanges.Add("editable", new ExcelAddress("C:N"));
excel.Save();
}
I know its very late to reply but may help others. I had a similar issue and was struggling to get sorting and auto-filtering in protected worksheet.
After protecting the worksheet, I have added below two settings that allow sorting and auto-filtering.
ws.Protection.IsProtected = True
ws.Protection.AllowSort = True
ws.Protection.AllowAutoFilter = True
In my case however the next requirement was to unlock some columns to allow editing, I achieved that using:
ws.Column(12).Style.Locked = False
If you have a range however you can try something like this:
For Each cell In ws.Cells("B1:C8")
cell.Style.Locked = False
Next
I built an application in C# that copies documents from a source NSF to a destination NSF. The destination NSF is an empty shell, retaining all design elements, based on the source NSF. I am using Lotus Notes 8.5.3 and am not connected to a Domino Server.
I use this application to split the source NSF into smaller chunks. The goal is to create destination NSFs that can be handled effectively by our automated (eDiscovery) systems. I need to ensure that as much metadata as possible are preserved.
My existing code meets these goals, except that that
(1) I lose foldering information. After copying documents, all folders are empty.
(2) All documents are marked as Read, even if they were unread in the source.
Code C#
//Establish session
NotesSession ns = new Domino.NotesSessionClass();
ns.Initialize("");
//Open source NSF
NotesDatabase nd = ns.GetDatabase("", "test.nsf", false);
//Open destination NSF.
//Assume that all design elements of nd2 are identical to those of nd
NotesDatabase nd2 = ns.GetDatabase("", "test2.nsf", false);
//Create view that returns all documents.
NotesView nView2 = nd.GetView("$All");
nd.CreateView("All-DR", "SELECT #ALL", nView2, false);
NotesView nView = NotesConnectionDatabase.GetView("All-DR");
//Loop through entries in the new view
NotesViewEntry nvec = nView.AllEntries;
nve = nvec.GetFirstEntry();
for (int j = 1; j <= intEntryCount; j++)
{
if (j == 1)
{
nve = nvec.GetFirstEntry();
}
else
{
nve = nvec.GetNextEntry(nve);
}
//Copy document to second database.
NotesDocument ndoc = nd.GetDocumentByUNID(nve.UniversalID);
ndoc.CopyToDatabase(nd2);
}
//End loop.
//All documents are copied.
The result is that I end up with a destination NSF that has all the documents copied over. Assume that all the folders are also there. However, none of the documents are in the folders. Every document is marked as read.
How can I fix the folders and unread issue?
There is a FolderReferences property in the NotesDocument class in the back-end classes. I'm not 100% sure if that property is exposed in the COM classes and interop for C#, but if it is, you can use that along with the PutInFolder() method to solve part of your problem.
As far as read/unread marks are concerned, the critical question is whether you are concerned only about the read/unread status for yourself, or whether you are trying to preserve it for all users of the database. If you only care about unread marks for yourself, then you might be able to use the getAllUnreadDocuments() method of the NotesDatabase class -- but this requires Notes/Domino 8 or above (on the machine where your code is running), and again I'm not sure if this method (or the NotesNoteCollection class that it returns) is exposed via the COM/interop interface for C#. If it is available, then you can iterate through the collection and use the MarkUnread() method. If you care about unread marks for all users, then I'm not sure if there is a way to do it at all -- but if there is, it's going to require using calls from the Notes C API.
Another thought on moving to folders, especially if the database isn't set up for FolderReferences to work:
You can iterate over the array of NotesView objects obtained from the NotesDatabase object's Views property. Each NotesView has a property that tells you if it is a folder.
Once you know about all the folders, you can iterate within each folder and collect a list of NotesDocuments that are contained within. Then by storing this information in a dictionary you could use this as a lookup while you process each document to decide what folder(s) it needs to be placed in.
Something like this (not tested):
object oViews = nd.Views;
object[] oarrViews = (object[])oViews;
Dictionary<string, List<string>> folderDict = new Dictionary<string, List<string>>();
for (int x=0; x < oarrViews.Length - 1; x++)
{
NotesView view = viewArray[x];
if (view.IsFolder)
{
NotesDocument doc = view.GetFirstDocument();
while (doc != null)
{
// Populate folderDict Dictionary by setting
// document's UNID as Key, and adding folder name to List
}
}
}
Then in your loop:
//Copy document to second database.
NotesDocument ndoc = nd.GetDocumentByUNID(nve.UniversalID);
NotesDocument newDoc = ndoc.CopyToDatabase(nd2);
if (folderDict.ContainsKey(nve.UniversalID)) {
foreach (var folderName in folderDict[nve.UniversalID]) {
newDoc.PutInFolder(folderName, true);
}
}
I was able to access a bookmark in my word document using this code:
var res = from bm in mainPart.Document.Body.Descendants<BookmarkStart>()
where bm.Name == "BookmarkName"
select bm;
Now I want to insert a paragraph and a table after this bookmark. How do I do that? (example code would be appreciated)
Code
Once you have the bookmark you can access its parent element and add the other items after it.
using (WordprocessingDocument document = WordprocessingDocument.Open(#"C:\Path\filename.docx", true))
{
var mainPart = document.MainDocumentPart;
var res = from bm in mainPart.Document.Body.Descendants<BookmarkStart>()
where bm.Name == "BookmarkName"
select bm;
var bookmark = res.SingleOrDefault();
if (bookmark != null)
{
var parent = bookmark.Parent; // bookmark's parent element
// simple paragraph in one declaration
//Paragraph newParagraph = new Paragraph(new Run(new Text("Hello, World!")));
// build paragraph piece by piece
Text text = new Text("Hello, World!");
Run run = new Run(new RunProperties(new Bold()));
run.Append(text);
Paragraph newParagraph = new Paragraph(run);
// insert after bookmark parent
parent.InsertAfterSelf(newParagraph);
var table = new Table(
new TableProperties(
new TableStyle() { Val = "TableGrid" },
new TableWidth() { Width = 0, Type = TableWidthUnitValues.Auto }
),
new TableGrid(
new GridColumn() { Width = (UInt32Value)1018U },
new GridColumn() { Width = (UInt32Value)3544U }),
new TableRow(
new TableCell(
new TableCellProperties(
new TableCellWidth() { Width = 0, Type = TableWidthUnitValues.Auto }),
new Paragraph(
new Run(
new Text("Category Name"))
)),
new TableCell(
new TableCellProperties(
new TableCellWidth() { Width = 4788, Type = TableWidthUnitValues.Dxa }),
new Paragraph(
new Run(
new Text("Value"))
))
),
new TableRow(
new TableCell(
new TableCellProperties(
new TableCellWidth() { Width = 0, Type = TableWidthUnitValues.Auto }),
new Paragraph(
new Run(
new Text("C1"))
)),
new TableCell(
new TableCellProperties(
new TableCellWidth() { Width = 0, Type = TableWidthUnitValues.Auto }),
new Paragraph(
new Run(
new Text("V1"))
))
));
// insert after new paragraph
newParagraph.InsertAfterSelf(table);
}
// close saves all parts and closes the document
document.Close();
}
The above code should do it. However, I'll explain some special circumstances.
Be aware that it will attempt the insertions after the parent element of the bookmark. What behavior do you expect if your bookmark happens to be part of a paragraph inside a table? Should it append the new paragraph and table right after it, within that table? Or should it do it after that table?
You might be wondering why the above questions matter. It all depends on where the insertion will occur. If the bookmark's parent is in a table, currently the above code would attempt to place a table within a table. That's fine, however an error might occur due to an invalid OpenXml structure. The reason is that if the inserted table was the last element in the original table's TableCell, there needs to be a Paragraph element added after the closing TableCell tag. You would promptly discover this issue if it occurred once you attempted to open the document in MS Word.
The solution is to determine whether you are indeed performing the insertion within a table.
To do so, we can add to the above code (after the parent var):
var parent = bookmark.Parent; // bookmark's parent element
// loop till we get the containing element in case bookmark is inside a table etc.
// keep checking the element's parent and update it till we reach the Body
var tempParent = bookmark.Parent;
bool isInTable = false;
while (tempParent.Parent != mainPart.Document.Body)
{
tempParent = tempParent.Parent;
if (tempParent is Table && !isInTable)
isInTable = true;
}
// ...
newParagraph.InsertAfterSelf(table); // from above sample
// if bookmark is in a table, add a paragraph after table
if (isInTable)
table.InsertAfterSelf(new Paragraph());
That should prevent the error from occurring and give you valid OpenXml. The while loop idea can be used if you answered "yes" to my earlier question and wanted to perform the insertion after the parent table rather than inside the table as the above code would do. If that's the case, the above issue would no longer be a concern and you can replace that loop and boolean with the following:
var parent = bookmark.Parent; // bookmark's parent element
while (parent.Parent != mainPart.Document.Body)
{
parent = parent.Parent;
}
This keeps re-assigning the parent till it's the main containing element at the Body level. So if the bookmark was in a paragraph that was in a table, it would go from Paragraph to TableCell to TableRow to Table and stop there since the Table's parent is the Body. At that point parent = Table element and we can insert after it.
That should cover some different approaches, depending on your original intent. Let me know if you need any clarification after trying it out.
Document Reflector
You might be wondering how I determined the GridColumn.Width values. I made a table and used the Document Reflector tool to get it. When you installed the Open Xml SDK, the productivity tools (if you installed them) would be located in C:\Program Files\Open XML Format SDK\V2.0\tools (or similar).
The best way to learn how the *.docx format works (or any Open Xml formatted doc) is to open an existing file with the Document Reflector tool. Navigate the document part, and locate the items you want to replicate. The tool shows you the actual code used to generate the entire document. This is code you can copy/paste into your application to generate similar results. You can ignore all the reference IDs usually; you'll have to take a look and try it out to get a feel for it.
As I mentioned, the above Table code was adapted from a sample document. I added a simple table to a docx, then opened it in the tool, and copied the code generated by the tool (I removed some extras to clean it up). That gave me a working sample to add a table.
It is especially helpful when you want to know how to write code that generates something, such as formatted tables and paragraphs with styles etc.
Take a look at this link for screenshots and info on the other tools included in the SDK: An introduction to Open XML SDK 2.0.
Code Snippets
You might also be interested in the code snippets for Open Xml. For a list of snippets check this blog post. You can download them from here: 2007 Office System Sample: Open XML Format SDK 2.0 Code Snippets for Visual Studio 2008.
Once installed you would add them from Tools | Code Snippet Manager menu. Select C# for the language, click the Add button, and navigate to PersonalFolder\Visual Studio 2008\Code Snippets\Visual C#\Open XML SDK 2.0 for Microsoft Office to add them. From your code you would right-click and select "Insert Snippet" and select the one you want.