VSTO format word-footer to be left-bound - c#

im need to implement a way to one-click add a footer to a word document consisting of one line.
The first part needs to be the absolute Path to the document and it has to be left-bound. In addition to this, there has to be the actual page number aligned to the right.
This wasn't a problem on Excel; there I could use LeftFooter, CenterFooter, RightFooter.
On Word however there are no such properties to access.
edit: I found a semi-working solution which has some bugs in it and isn't properly designed because I could not find a proper way yet.
Word.Document doc = Globals.ThisAddIn.Application.ActiveDocument;
foreach (Word.Section wordSection in doc.Sections)
{
Word.Range PageNumberRange = wordSection.Range;
PageNumberRange.Fields.Add(PageNumberRange, Word.WdFieldType.wdFieldEmpty ,"PAGE Arabic ", true);
Word.Range footer = wordSection.Footers[Word.WdHeaderFooterIndex.wdHeaderFooterPrimary].Range;
footer.ParagraphFormat.Alignment = Word.WdParagraphAlignment.wdAlignParagraphCenter;
footer.Tables.Add(footer, 1, 3);
Word.Table tbl = footer.Tables[1];
tbl.Cell(1, 1).Range.Text = doc.FullName;
tbl.Cell(1, 3).Range.Text = PageNumberRange.Text;
/**/
footer.Font.ColorIndex = Word.WdColorIndex.wdBlack;
footer.Font.Size = 6;
PageNumberRange.Text = "";
The problems with this one are: It never overwrites the exisiting footer. If it writes "document1 ... 1" and you click on it again, because you saved your document, it doenst change the footer. Furthermore: If you have multiple pages, every page except page 1 gets deleted.
I never imagined it could be so hard, to implement such an easy task.

An alternative approach using styles
Document doc = this.application.ActiveDocument;
Section wordSection = doc.Sections[1];
Range footer = wordSection.Footers[WdHeaderFooterIndex.wdHeaderFooterPrimary].Range;
footer.Fields.Add(footer, WdFieldType.wdFieldEmpty, #"PAGE \* ARABIC", true);
footer.Collapse(WdCollapseDirection.wdCollapseStart);
footer.InsertBefore("\t \t");
footer.InsertBefore(doc.FullName);
footer.Font.Name = "Arial";

Related

C# Word Document Replace PlainText with mergefield

I've got a word document template and a CSV i would like to mailmerge it with.
In the word document i have text surrounded with <<>> if i want to use it to mailmerge, this matches the headers in my csv. For example i have <<Salutation>> in my word document and the field name Salutation in my csv.
Is there an easy way to replace the text surrounded by <<>> with a mailmerge field corresponding to its header in the CSV?
The code i have so far for reading the data in is:
Microsoft.Office.Interop.Word.Application _wordApp = new Microsoft.Office.Interop.Word.Application();
Microsoft.Office.Interop.Word.Document oDoc = _wordApp.Documents.Add(#"C:\Eyre\Template.docx");
_wordApp.Visible = true;
oDoc.MailMerge.MainDocumentType = Microsoft.Office.Interop.Word.WdMailMergeMainDocType.wdFormLetters;
oDoc.MailMerge.OpenDataSource(#"C:\Eyre\CSV.csv", false, false, true);
oDoc.MailMerge.Destination = Microsoft.Office.Interop.Word.WdMailMergeDestination.wdSendToNewDocument;
oDoc.MailMerge.Execute(false);
Microsoft.Office.Interop.Word.Document oLetters = _wordApp.ActiveDocument;
oLetters.SaveAs2(#"C:\Eyre\letters.docx",
Microsoft.Office.Interop.Word.WdSaveFormat.wdFormatDocumentDefault);
Any help would be much appreciated
---EDIT---
This seems to be confusing some people. I have a word template with plain text such as Salutation and need a C# program that will replace this plain text with a merge field from a csv.
Here's a C# version of code to replace "placeholders" in a Word document with merge fields. (For readers looking for a VB-version, see https://stackoverflow.com/a/50159375/3077495.)
My code uses an already running instance of Word, so the part that interests you starts at foreach (Word.MailMergeDataField...
The Find/Replace actions are in their own procedure ReplaceTextWithMergeField, to which the name of the data source field (as Word sees it!), and the target Range for the search are passed.
Note how the angled bracket pairs are appended to the data field name in this procedure.
The Find/Replace actions are standard, re-setting the Range object of continuing the search for a data field name is a bit different because it's necessary to get the position outside the merge field - after inserting the field the Range is inside the field code. If this isn't done, Find could end up in the same field "infinitely". (Note: Not in this case, with the double angled brackets. But if anyone were to use the code without them, then the problem would occur.)
EDIT: In order to find and replace in Shape objects, those objects must be looped separately. Anything formatted with text wrapping is in a different layer of the document and is not part of Document.Content. I've adapted the find procedure in a third procedure to search through the document's ShapeRange, testing for Text Boxes, specifically.
private void btnDataToMergeFields_Click(object sender, EventArgs e)
{
getWordInstance();
if (wdApp != null)
{
if (wdApp.Documents.Count > 0)
{
Word.Document doc = wdApp.ActiveDocument;
Word.Range rng = doc.Content;
Word.ShapeRange rngShapes = rng.ShapeRange;
if (doc.MailMerge.MainDocumentType != Word.WdMailMergeMainDocType.wdNotAMergeDocument)
foreach (Word.MailMergeDataField mmDataField in doc.MailMerge.DataSource.DataFields)
{
System.Diagnostics.Debug.Print(ReplaceTextWithMergeField(mmDataField.Name, ref rng).ToString()
+ " merge fields inserted for " + mmDataField.Name);
rng = doc.Content;
System.Diagnostics.Debug.Print(ReplaceTextWithMergeFieldInShapes(mmDataField.Name, ref rngShapes)
+ " mergefields inserted for " + mmDataField.Name);
}
}
}
}
//returns the number of times the merge field was inserted
public int ReplaceTextWithMergeField(string sFieldName, ref Word.Range oRng)
{
int iFieldCounter = 0;
Word.Field fldMerge;
bool bFound;
oRng.Find.ClearFormatting();
oRng.Find.Forward = true;
oRng.Find.Wrap = Word.WdFindWrap.wdFindStop;
oRng.Find.Format = false;
oRng.Find.MatchCase = false;
oRng.Find.MatchWholeWord = false;
oRng.Find.MatchWildcards = false;
oRng.Find.MatchSoundsLike = false;
oRng.Find.MatchAllWordForms = false;
oRng.Find.Text = "<<" + sFieldName + ">>";
bFound = oRng.Find.Execute();
while (bFound)
{
iFieldCounter = iFieldCounter + 1;
fldMerge = oRng.Fields.Add(oRng, Word.WdFieldType.wdFieldMergeField, sFieldName, false);
oRng = fldMerge.Result;
oRng.Collapse(Word.WdCollapseDirection.wdCollapseEnd);
oRng.MoveStart(Word.WdUnits.wdCharacter, 2);
oRng.End = oRng.Document.Content.End;
oRng.Find.Text = "<<" + sFieldName + ">>";
bFound = oRng.Find.Execute();
}
return iFieldCounter;
}
public int ReplaceTextWithMergeFieldInShapes(string sFieldName,
ref Word.ShapeRange oRng)
{
int iFieldCounter = 0;
Word.Field fldMerge;
bool bFound;
foreach (Word.Shape shp in oRng)
{
if (shp.Type == Office.MsoShapeType.msoTextBox)
{
Word.Range rngText = shp.TextFrame.TextRange;
rngText.Find.ClearFormatting();
rngText.Find.Forward = true;
rngText.Find.Wrap = Word.WdFindWrap.wdFindStop;
rngText.Find.Format = false;
rngText.Find.MatchCase = false;
rngText.Find.MatchWholeWord = false;
rngText.Find.MatchWildcards = false;
rngText.Find.MatchSoundsLike = false;
rngText.Find.MatchAllWordForms = false;
rngText.Find.Text = "<<" + sFieldName + ">>";
bFound = rngText.Find.Execute();
while (bFound)
{
iFieldCounter = iFieldCounter + 1;
fldMerge = rngText.Fields.Add(rngText, Word.WdFieldType.wdFieldMergeField, sFieldName, false);
rngText = fldMerge.Result;
rngText.Collapse(Word.WdCollapseDirection.wdCollapseEnd);
rngText.MoveStart(Word.WdUnits.wdCharacter, 2);
rngText.End = shp.TextFrame.TextRange.End;
rngText.Find.Text = sFieldName;
bFound = rngText.Find.Execute();
}
}
}
return iFieldCounter;
}
There are a number of approaches depending on your broader requirements. If you will run the tool as needed for simple / small tasks on your Windows machine, then the VBA/macro approach is probably best since you already have the things in place you need.
Another approach requires more coding and understanding of DOCX, but you could potentially scale it and run it on machines without MS Office libraries. Since DocX is open and text based, you can unzip it process the XML contents and re-zip. There are some gotchas because the XML is not trivial. If you were doing this, using Word Merge Fields is better (for the programmer) than plain text since finding fields is simpler. Plain text is better for the person working with the Document/Template since they don't have to deal with merge fields, but the downside is the XML processing can become much more complicated. The text in the template <<Salutation>> might not be easy to find the XML - it could be split into pieces.
Another solution is to use something like Docmosis (a commercial product - please note I work for Docmosis). The upsides are that Docmosis can do the replacement and more complex requirements (conditional and looping structures, PDF conversion for example). The downside is you have to learn the API and install the software (or call out to the cloud) and also get your data into a format to pass to the engine.
I hope that helps.

MigraDoc not adding page breaks automatically

I am tasked with refactoring an old MigraDoc project written by a dev that is no longer with my company, and am having problems with the following bit of code..
var Split = new String[1];
Split[0] = "||";
if (invoiceObject.Note != null)
{
var Lines = invoiceObject.Note.Split(Split, StringSplitOptions.RemoveEmptyEntries);
for (var i = 0; i < Lines.Count(); i++)
{
if (i > 0)
lineItemParagraph.AddLineBreak();
lineItemParagraph.AddText("" + Lines[i].Replace(" ", " ").Replace("|", ""));
}
}
This is working and it's taking a double pipe delimited notes fields and breaking it out into new lines as expected. The issue is that for very large note fields, the rendered PDF only has 1 page and the text just runs off the page. (The item I am testing with has enough data in the notes field for 20+ pages in the rendered PDF).
Edit
The code is inside of a text frame defined like this.
TextFrame lineItemFrame;
this.lineItemFrame = section.AddTextFrame();
this.lineItemFrame.Height = "3.0cm";
this.lineItemFrame.Width = "8.0cm";
this.lineItemFrame.Left = "0cm";
this.lineItemFrame.RelativeHorizontal = RelativeHorizontal.Margin;
this.lineItemFrame.Top = "9.0cm";
this.lineItemFrame.RelativeVertical = RelativeVertical.Page;
The Text frame is inside of a section that is defined like this. Looking through the code, it appears this is the only section on the PDF. Do I perhaps need more sections?
section = this.document.AddSection();
section.PageSetup.StartingNumber = 1;
I can't figure out how to make MigraDoc add the page breaks for me automatically.
Am I missing something painfully obvious?
MigraDoc adds page breaks automatically - with two exceptions: TextFrames do no break, table rows do not break. Tables break between rows only.

Add hyperlink to paragraph using word Interop

I'm trying to add an hyperlink to some XML being inserted in a field of a Word Document (using Microsoft.Office.Interop.Word).
The XML being inserted contains multiple paragraphs, each containing some text that should be converted to a hyperlink. The text that contains the hyperlink is extracted from the end of the paragraph after the "Available at " substring is found.
The following code is able to create the hyperlink but the first hyperlink is always applied to all paragraphs. I was expecting the code to create an hyperlink for each of the paragraphs being iterated.
My guess is that the paragraph.Range object is pointing to text that is in fact the whole XML inserted as opposed to the text contained within the paragraph. I've also confirmed that the paragraph.Range.Text property returns the correct text for each paragraph so I am completely confused as to what should be expected for the Range property.
Any ideas? Thanks in advance.
if (!string.IsNullOrWhiteSpace(bibliography))
{
const string linkToken = "Available at ";
field.Result.InsertXML(bibliography);
foreach (Paragraph paragraph in field.Result.Paragraphs)
{
var paragraphText = paragraph.Range.Text;
var indexOfLink = paragraphText.IndexOf(linkToken, StringComparison.OrdinalIgnoreCase);
if (indexOfLink >= 0)
{
var linkStart = indexOfLink + linkToken.Length;
var linkPart = paragraphText.Substring(linkStart);
Uri uriFound;
if (Uri.TryCreate(linkPart, UriKind.Absolute, out uriFound))
{
object linkAddress = uriFound.ToString();
paragraph.Range.Hyperlinks.Add(paragraph.Range, ref linkAddress);
}
}
}
}

Insert text to Word document

I would like to insert text to Word document, in a specified format, using Interop.Word:
Something like this :
wordDoc.InsertText("Text \n", "Arial");
or
wordDoc.InsertText("Text \n", "Bold");
Is it possible?
There is no direct method like this AFAIK. However, you can write a wrapper over Word Interop and do it. Internally inside your InsertText() method you will have to do the following.
1: Use the Text property of a Range object to insert text in a document.
object start = 0;
object end = 12;
Word.Range rng = this.Range(ref start, ref end);
rng.Text = "New Text";
rng.Select();
2: Format text using a document-level customization.
// Set the Range to the first paragraph.
Word.Range rng = this.Paragraphs[1].Range;
// Change the formatting.
rng.Font.Size = 14;
rng.Font.Name = "Arial";
rng.ParagraphFormat.Alignment = Word.WdParagraphAlignment.wdAlignParagraphCenter;
rng.Select();
For further details please see this and this which I have used sometime back with good results.
Hope this helps.

How to insert blank page at the begining of word 2003 document

In fact, I can insert a new blank page at the beginning of a Word 2003 document and some text.
However the problem is the new blank page will get the format or style from the next page.
For example:
The original document with bullet point:
a
b
c
d
 
After editing with the C# code below:
Hello world
-----
a
b
c
d
I want to insert a new blank page without any styles or formatting.
How can I do this in C#?
Here is the existing code that I'mu sing to try and insert a blank page with some text:
var app = new Application();
app.Visible = false;
app.DisplayAlerts = WdAlertLevel.wdAlertsNone;
var doc = app.Documents.Open(#"C:\test.doc");
object what = WdGoToItem.wdGoToLine;
object which = WdGoToDirection.wdGoToFirst;
app.Selection.GoTo(ref gotoPage, ref gotoNext, ref gotoCount, ref gotoName);
//Insert a blank page
app.Selection.InsertNewPage();
object start = 0;
object end = 0;
var range = doc.Range(ref start, ref end);
range.Font.Size = 10;
range.Text = "Hello\n World";
doc.Save();
doc.Close();
app.Quit();
Thanks very much.
I would also appreciate if you could suggest how to insert text with new line sign as "\n" doesn't work.
I am not quit sure whether this will work for the Word 2003. I've tried this for Word 2010. The idea is to reset style of the Paragraph. Considering your code:
//Insert a blank
app.Selection.InsertNewPage();
Word.Paragraph par = doc.Paragraphs[1];
par.Reset();
Take a look on this page. Hopefully this will help you.
Considering line break, I would suggest to try '\r' or '\r\n' symbols. Or as Pat has proposed, you can try Environment.NewLine. In Word 2010 your code works also fine, I mean with \n.
I overcome this problem with merge two file one for blank and one is original.
This is helpful article to solve this problem.
How To: Merge Multiple Microsoft Word Documents in C#
Try to insert the Page Break by inserting symbols "\f\r". And then set the Style of the first paragraph like "Normal".
For example:
Range docRange = doc.Range();
docRange.Text = docRange.Text.Insert(0, "\f\r");
docRange.Paragraphs.First.set_Style(WdBuiltinStyle.wdStyleNormal);

Categories