I have a C# application which will open a word document and then replace some of the predefined bookmarks with the data which i have like Name,Class etc..
It is all are just string values .Now I want to render a Table with dynamic number of rows to the word document.I want the table in a particular place in the document.
Can i use bookmark for this.If so how ??? IS there any other method?
Yes, you can use bookmarks and also use fields to replace it with table with n no. of rows and n no. of columns.
You can loop through fields and get its range and using range you can add table at field location:
//CREATING OBJECTS OF WORD AND DOCUMENT
Word.Application oWord = new Word.Application();
Word.Document oWordDoc = new Word.Document();
foreach (Word.Field myMergeField in oWordDoc.Fields)
{
iTotalFields++;
Word.Range rngFieldCode = myMergeField.Code;
String fieldText = rngFieldCode.Text;
// ONLY GETTING THE MAILMERGE FIELDS
if (fieldText.StartsWith("tablename"))
{
myMergeField.Select();
oWordDoc.table.add(rngFieldCode,4//for rows,4// for colioulns,ref omising....);
}
}
Related
Using OpenXML in C#, we need to:
Find a specific string of text on a Word document (this text will always exist in a table cell)
Get the formatting of the text and the table that the text exists in.
Create a new table with the same text and table formatting while pulling in text values for the cell from a nested List
This is the code that I currently have and the places I am not sure how do:
using (WordprocessingDocument wordDoc = WordprocessingDocument.Open(fileWordFile, true))
{
MainDocumentPart mainPart = wordDoc.MainDocumentPart;
Body body = mainPart.Document.Body;
IEnumerable paragraphs = body.Elements<Paragraph>();
Paragraph targetParagraph = null;
//Comment 1: Loop through paragraphs and search for a specific string of text in word document
foreach (Paragraph paragraph in paragraphs) {
if(paragraph.Elements<Run>().Any()) {
Run run = paragraph.Elements<Run>().First();
if(run.Elements<Text>().Any()) {
Text text = run.Elements<Text>().First();
if (text.Text.Equals("MY SEARCH STRING")) {
targetParagraph = paragraph;
// Comment 2: How can I get the formatting of the table that contains this text??
}
}
}
}
//Comment 3: Create table with same formatting as where the text was found
Table table1 = new Table();
TableProperties tableProperties1 = new TableProperties();
//Comment 4: How can I set these properties to be the same as the one found at "Comment 2"??
wordDoc.Close();
wordDoc.Dispose();
}
If you're looking for text elements that are inside a table cell, you can use a LINQ query to get there quickly without needing to use a heap of nested loops.
// Find the first text element matching the search string
// where the text is inside a table cell.
var textElement = body.Descendants<Text>()
.FirstOrDefault(t => t.Text == searchString &&
t.Ancestors<TableCell>().Any());
Once you have your match, the easiest way to duplicate the containing table with all its formatting and contents is simply to clone it.
if (textElement != null)
{
// get the table containing the matched text element and clone it
Table table = textElement.Ancestors<Table>().First();
Table tableCopy = (Table)table.CloneNode(deep: true);
// do stuff with copied table (see below)
}
After that, you can add things to the corresponding cell of the copied table. It's not entirely clear what you meant by "pulling in text values for the cell from a nested List" (what list? nested where?), so I'll just show a contrived example. (This code would replace the "do stuff" comment in the code above.)
// find the table cell containing the search string in the copied table
var targetCell = tableCopy.Descendants<Text>()
.First(t => t.InnerText == searchString)
.Ancestors<TableCell>()
.First();
// get the properties from the first paragraph in the target cell (so we can copy them)
var paraProps = targetCell.Descendants<ParagraphProperties>().First();
// now add new stuff to the target cell
List<string> stuffToAdd = new List<string> { "foo", "bar", "baz", "quux" };
foreach (string item in stuffToAdd)
{
// for each item, clone the paragraph properties, then add a new paragraph
var propsCopy = (ParagraphProperties)paraProps.CloneNode(deep: true);
targetCell.AppendChild(new Paragraph(propsCopy, new Run(new Text(item))));
}
Lastly, you need to add the copied table to the document somewhere or you won't see it. You don't say in your question where you would want this to appear, so I'll just put it at the end of the document. You can use methods like InsertAfter, InsertAt, InsertBefore, etc. to insert the table relative to other elements.
body.AppendChild(tableCopy);
Hope this helps.
I am attempting to add consecutive tables to a word document from C# (VS 2013, .NET 4.5, Word 2013) and the code below nests the second table within the first table. I am attempting to create distinct tables that are added between two bookmarks (I have tested this with a range defined as a single bookmark as well).
I own the code and source/destination document format, so i can make any changes as needed, however the content is all dynamic, so i can't just create a pre-defined bookmark for each table.
A subset of the code that is representative of my issue is below:
Word.Application oWord = new Word.Application();
Word.Document oTargetWordDoc = oWord.Documents.Open(WordTemplateFile);
object oBookmarkBegin = "bookmark_begin";
object oBookmarkEnd = "bookmark_end";
object oBookmarkBeginRange = oTargetWordDoc.Bookmarks.get_Item(ref oBookmarkBegin).Range.Start;
object oBookmarkEndRange = oTargetWordDoc.Bookmarks.get_Item(ref oBookmarkEnd).Range.Start;
Word.Range oRange = oTargetWordDoc.Range(ref oBookmarkBeginRange, ref oBookmarkEndRange);
Word.Table wtTable1 = oRange.Tables.Add(oRange, 1, 2);
wtTable1.Cell(1,1).Range.Text = "t1/r1/c1";
wtTable1.Cell(1,2).Range.Text = "t1/r1/c2";
Word.Table wtTable2 = oRange.Tables.Add(oRange, 1, 2);
wtTable2.Cell(1,1).Range.Text = "t2/r1/c1";
wtTable2.Cell(1,2).Range.Text = "t2/r1/c2";
// other goo/close/save/etc...
The output looks like this:
t1/r1/c1|t1/r1/c2
t2/r1/c1|t2/r1/c2| <-- nested within column 1 in the first table (notice trailing | marking beginning of first table's second column)
Clearly, I would like two distinct tables, without nesting, resulting in:
t1/r1/c1|t1/r1/c2
t2/r1/c1|t2/r1/c2
Any assistance in this matter would be greatly appreciated!
I am processing an .xlsm file and need to know how to use a list on another sheet for data validation using openXML and C#.
To start, I have a .xlsm file with two empty sheets and macros in it. In my program I open the file, Create the column header on Sheet1 then create the validation list on sheet2. So, after I run my program Sheet1 "A1" contains the text "Color" and Sheet2 "A1:A4" contains "Blue","Green","Red","Yellow". I get this far just fine.
I would like to make it so there is a dropdown list in all cells of column "A" on sheet1 that contains each of the 4 colors and enforces them as the only input. In Microsoft Excel this is done by going to the "Data" tab, selecting "Data Validation" selecting "List" and highlighting the cells you want to use. I need to make this association programmatically.
The (Desired) XML that Microsoft Excel creates if I do it manually is this:
<extLst>
<ext uri="{CCE6A557-97BC-4b89-ADB6-D9C93CAAB3DF}" xmlns:x14="http://schemas.microsoft.com/office/spreadsheetml/2009/9/main">
<x14:dataValidations count="1" xmlns:xm="http://schemas.microsoft.com/office/excel/2006/main">
<x14:dataValidation type="list" allowBlank="1" showInputMessage="1" showErrorMessage="1">
<x14:formula1>
<xm:f>'Validation Data'!$A$1:$A$4</xm:f>
</x14:formula1>
<xm:sqref>A1:A1048576</xm:sqref>
</x14:dataValidation>
</x14:dataValidations>
</ext>
</extLst>
The following method and results is something I tried. It may give a better Idea of what I'm trying to do.
Here, I pass in "'Sheet2'!$A$1:$A$4" as the "validationListCells" parameter. This represents the cells in "Sheet2" that, in this example, would contain the color names "Red", "Green"...etc.
I pass in "A2:A1048576" as the "cellsToValidate" parameter. This represents all cells of Sheet1 column "A", on which I want to enforce validation.
I pass "Sheet1" as the worksheetName parameter.
private void InsertValidation(String worksheetName, String validationListCells, String cellsToValidate)
{
DataValidations dataValidations1 = new DataValidations() { Count = (UInt32Value)1U };
DataValidation dataValidation1 = new DataValidation()
{
Formula1 = new Formula1(validationListCells),
Type = DataValidationValues.List,
ShowInputMessage = true,
ShowErrorMessage = true,
SequenceOfReferences = new ListValue<StringValue>() { InnerText = cellsToValidate }
};
dataValidations1.Append(dataValidation1);
using (SpreadsheetDocument spreadSheet = SpreadsheetDocument.Open(_documentPath, true))
{
WorksheetPart worksheetPart = GetWorksheetPartByName(spreadSheet, worksheetName);
worksheetPart.Worksheet.Append(dataValidations1);
worksheetPart.Worksheet.Save();
}
}
It results in this XML in Sheet1.xml. Which causes an error in Excel.
<x:dataValidations count="1">
<x:dataValidation type="list" showInputMessage="1" showErrorMessage="1" sqref="A2: A1048576">
<x:formula1>'Sheet2'!$A$1:$A$5</x:formula1>
</x:dataValidation>
</x:dataValidations>
It looks like I may be on the right track since it is beginning to resemble the xml created by Excel, but I'm completely new to openXML and I'm finding little about this topic on the net.
Thanks in advance!
For anyone else in need of this..the code below worked for me.
I put in there user3251089's variable names.
In general, when I try to programmatically create an excel "feature" I manually make a really basic excel that has in it that feature (delete extra sheets too). Then I reflect the code and try to make it prettier.
hope it serves to someone!
using Excel = DocumentFormat.OpenXml.Office.Excel;
using X14 = DocumentFormat.OpenXml.Office2010.Excel;
.....
Worksheet worksheet = worksheetPart.Worksheet;
WorksheetExtensionList worksheetExtensionList = new WorksheetExtensionList();
WorksheetExtension worksheetExtension = new WorksheetExtension() { Uri = "{CCE6A557-97BC-4b89-ADB6-D9C93CAAB3DF}" };
worksheetExtension.AddNamespaceDeclaration("x14", "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main");
X14.DataValidations dataValidations = new X14.DataValidations() { Count = (UInt32Value)3U };
dataValidations.AddNamespaceDeclaration("xm", "http://schemas.microsoft.com/office/excel/2006/main");
//sites validation
dataValidations.Append(new X14.DataValidation()
{
Type = DataValidationValues.List,
AllowBlank = true,
ShowInputMessage = true,
ShowErrorMessage = true,
DataValidationForumla1 = new X14.DataValidationForumla1() { Formula = new Excel.Formula(validationListCells) },
ReferenceSequence = new Excel.ReferenceSequence(cellsToValidate)
});
worksheetExtension.Append(dataValidations);
worksheetExtensionList.Append(worksheetExtension);
worksheet.Append(worksheetExtensionList);
worksheet.Save();
I have a template word file composed by 2 pages, each page has a bookmark, the first page bookmark name is A4 and the second page bookmark name is A3, but when I read all bookmarks from the word document I get them in alphabetical order, I want them in page order, how can i do this?
foreach (Bookmark bookMark in MergeResultDoc.Bookmarks)
{//IMPORTANTE:IL NOME DEL SEGNALIBRO DEVE ESSERE IL TIPO DI CARTA
pagInizio = Convert.ToInt32(pagNum);
pagNum = bookMark.Range.Information[WdInformation.wdActiveEndPageNumber].ToString();
addData( pagInizio, pagNum, bookMark.Name);
iteration++;
}
You can read the bookMark.Start value.
This returns the start position of the Bookmark in the document.
So you can run over all Bookmarks and sort them by their start position.
Here is a code to do that:
// List to store all bookmarks sorted by position.
List<Bookmark> bmList = new List<Bookmark>();
// Iterate over all the Bookmarks and add them to the list (unordered).
foreach (Bookmark curBookmark in MergeResultDoc.Bookmarks)
{
bmList.Add(curBookmark);
}
// Sort the List by the Start member of each Bookmark.
// After this line the bmList will be ordered.
bmList.Sort(delegate(Bookmark bm1, Bookmark bm2)
{
return bm1.Start.CompareTo(bm2.Start);
});
Use LINQ OrderBy:
var orderedResults = MergeResultDoc.Bookmarks.OrderBy(d => d.Start).ToList();
Document.Boomarks should return the bookmarks in alpha sequence.
Document.Content.Bookmarks should return the bookmarks in the sequence they appear in the document. But VBA collection documentation does not typically guarantee a particular sequence for anything, it's safer to read the Start (as suggested by etaiso) and sort using that.
Is it possible to add several rows with the help of Bookmarks and openXML to a word document?
We have a worddocument that serves as a report template.
In that template we need to add several transaction rows.
The problem is that the number of rows aren't static. It could be 0, 1 or 42 for example.
In the current template (which we can change) we have added 3 bookmarks
TransactionPart, TransactionPart2 and TransactionPart3.
The tree transactionparts forms a singel row with three different datacontent (ID, Description, Amount)
If we have just one transaction row we have no problem adding the data to those bookmarks, but what do we do when we should add row two? There are no bookmarks for more rows.
Is there a smart way of doing this?
Or should we change the worddocument so that the rows end up in a table? Would that solve the problem in a better way?
I would put a single bookmark lets call it "transactions" inside a 3 coloumn table.
Like this
When you know the design of the table, but not the number of rows you'll be needing the simplest way is to add a row for each line of data you have.
You could accomplish that with a code like this
//make some data.
List<String[]> data = new List<string[]>();
for (int i = 0; i < 10; i++)
data.Add(new String[] {"this","is","sparta" });
using (WordprocessingDocument wordDoc = WordprocessingDocument.Open("yourDocument.docx", true))
{
var mainPart = wordDoc.MainDocumentPart;
var bookmarks = mainPart.Document.Body.Descendants<BookmarkStart>();
var bookmark =
from n in bookmarks
where n.Name == "transactions"
select n;
OpenXmlElement elem = bookmark.First().Parent;
//isolate tabel
while (!(elem is DocumentFormat.OpenXml.Wordprocessing.Table))
elem = elem.Parent;
var table = elem; //found
//save the row you wanna copy in each time you have data.
var oldRow = elem.Elements<TableRow>().Last();
DocumentFormat.OpenXml.Wordprocessing.TableRow row = (TableRow)oldRow.Clone();
//remove old row
elem.RemoveChild<TableRow>(oldRow);
foreach (String[] s in data)
{
DocumentFormat.OpenXml.Wordprocessing.TableRow newrow = (TableRow)row.Clone();
var cells = newrow.Elements<DocumentFormat.OpenXml.Wordprocessing.TableCell>();
//we know we have 3 cells
for(int i = 0; i < cells.Count(); i++)
{
var c = cells.ElementAt(i);
var run = c.Elements<Paragraph>().First().Elements<Run>().First();
var text = run.Elements<Text>().First();
text.Text = s[i];
}
table.AppendChild(newrow);
}
}
You end up with this
I've tested this code on a pretty basic document and know it works.
Good luck and let me know if I can clarify further.