I have a C#/WPF application that allows the user to export information into a Word document.
At the moment it works - and creates the document as expected - however the UI locks and the moment I try to thread this method I get varying errors.
The document creation takes in a list of custom items, then builds sections in the Word document based on each item. It creates a table for each image, and in those tables I insert an image placeholder. Once this is done I traverse the document and replace the placeholders with their associated image.
I believe the threading issue is due to the way images are inserted into the document - utilising Clipboard.Clear() and Clipboard.SetDataObject(img).
Is there a cleaner way for me to insert JPG's from disk into the document, or is there a nice way to thread such a method? Here is the offending method:
private static void InsertImagesTables(string document, List<Record> allRecords)
{
Document oDoc = oWord.Documents.Open(document);
Object oMissing = Missing.Value;
object NormalStyle = "Normal";
oWord.Visible = false;
foreach (Record record in allRecords)
{
foreach (RecordImage rImage in record.Images)
{
//insert over placeholder
var range = oDoc.Content;
if (range.Find.Execute("[[" + record.Title + rImage.ImagePath + "]]"))
{
try
{
//insert the image
var prevRange = range.Previous(WdUnits.wdCharacter);
Table imageTable;
imageTable = oDoc.Tables.Add(range, 1, 1, ref oMissing, ref oMissing);
imageTable.Borders.InsideLineStyle = WdLineStyle.wdLineStyleNone;
imageTable.Borders.OutsideLineStyle = WdLineStyle.wdLineStyleNone;
Image img = Image.FromFile(rImage.ImagePath + ".jpg");
Clipboard.Clear();
Clipboard.SetDataObject(img);
imageTable.Cell(1, 1).Range.Paste();
imageTable.Cell(1, 1).Range.set_Style(ref NormalStyle);
imageTable.Cell(1, 1).Range.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphCenter;
InlineShape inlineShape = imageTable.Cell(1, 1).Range.InlineShapes[1];
imageTable.Rows.Alignment = WdRowAlignment.wdAlignRowCenter;
string caption = rImage.Caption;
inlineShape.Range.InsertCaption(Label: "Figure", Title: " - " + caption, Position: WdCaptionPosition.wdCaptionPositionBelow);
range.Expand(WdUnits.wdParagraph);
}
catch // no image for record - do nothing
{ }
}
}
}
oDoc.Close(true);
}
I've tried BackgroundWorkers, Dispatchers, async Tasks and Threads (with and without ApartmentState.STA) with varying outcomes. Most just raise an error, but a few run and complete, without placing every image in the document - such as the STA approach.
Any help here is much appreciated,
Mike
Fixed. Thanks vernou for taking the time. I solved it by removing the Clipboard interaction and instead, properly utilising Word interop with a backgroundworker:
Range cellRange = imageTable.Cell(1, 1).Range;
cellRange.InlineShapes.AddPicture(rImage.ImagePath + ".jpg", ref oMissing, ref oMissing, ref oMissing);
instead of:
Clipboard.Clear();
Clipboard.SetDataObject(img);
imageTable.Cell(1, 1).Range.Paste();
Related
I am currently creating a c# program which uses a template.dotx containing pre-defined building blocks (with bookmarks) to insert pages of content into a new document, all working as follows
Word._Application oWord;
Word._Document oDoc;
object oMissing = System.Reflection.Missing.Value;
object bAddress = "bAddress";
oWord = new Word.Application();
object oTemplate = _template;
oDoc = oWord.Documents.Add(ref oTemplate, ref oMissing,
ref oMissing, ref oMissing);
Word.Template objTmpl = (Word.Template)oDoc.get_AttachedTemplate();
Word.BuildingBlock objBB = objTmpl.BuildingBlockEntries.Item("PageBB");
Range where = oDoc.Range(oDoc.Content.End - 1, oDoc.Content.End - 1);
var orng = objBB.Insert(where, true);
orng.Bookmarks[bAddress].Range.Text = "Address";
I would like to do this in reverse, to open created file at a later date, and read through each bookmark to get the value.
The problem is when I open the resulting DocXCreatedFromTemplate.docx, the bookmarks have disappeared. Programmatically, I receive the bookmark not in collection error, but also by allowing word to open and checking manually the bookmarks are replaced with text but the bookmark reference has gone.
Is there any way to get round this?
I have worked this out, the following text replaces and deletes the bookmark
orng.Bookmarks[bAddress].Range.Text = "Address";
The solution was to re-add the bookmark afterwards using a method as follows
private void UpdateBookmarkWithoutDeleting(object bookmark, string text, _Document document)
{
var bookmarkRange = document.Bookmarks[bookmark].Range;
bookmarkRange.Text = text;
document.Bookmarks.Add(bookmark.ToString(), bookmarkRange);
}
I found the information at the following link (in VB):
https://wordmvp.com/FAQs/MacrosVBA/InsertingTextAtBookmark.htm
I have found many examples online about converting word (.docx) file to .pdf file.
It works fine when I run from UI thread.
But the moment I execute it on background process. then it fails.
Is there some way to convert Word to PDF via background process execution ?
Below are the IronPython and c# code that works on UI thread.
IronPython:
import sys
import clr
import System
from System.Text import StringBuilder
from System.IO import DirectoryInfo, File, FileInfo, Path, StreamWriter
clr.AddReference("Microsoft.Office.Interop.Word")
import Microsoft.Office.Interop.Word as Word
def ConvertWordToPDF(wordDocFullPath):
"""
<Script>
<Author>SNI</Author>
<Description>Converts Word document to PDF.</Description>
<Parameters>
<Parameter name="wordDocFullPath" type="string">Word document full path.</Parameter>
</Parameters>
<ReturnValue type="string">PDF full path</ReturnValue>
</Script>
"""
wordDocInfo = FileInfo(wordDocFullPath)
pdfFullPath = Path.Combine(wordDocInfo.DirectoryName, wordDocInfo.Name.TrimEnd(wordDocInfo.Extension.ToCharArray()) + '.pdf')
word_application = Word.ApplicationClass()
word_application.visible = False
wordDocument = word_application.Documents.Open(wordDocFullPath);
if wordDocument:
wordDocument.ExportAsFixedFormat(pdfFullPath, Word.WdExportFormat.wdExportFormatPDF);
wordDocument.Close()
word_application.Quit()
word_application = None
else:
print 'failed to initialize word document'
print pdfFullPath
return pdfFullPath`
c# code:
public string Convert(string wordDocPath)
{
string pdfPath = string.Empty;
try
{
Microsoft.Office.Interop.Word.Application word = null;
Document doc = null;
// C# doesn't have optional arguments so we'll need a dummy value
object oMissing = System.Reflection.Missing.Value;
object saveChanges = WdSaveOptions.wdDoNotSaveChanges;
try
{
word = new Microsoft.Office.Interop.Word.Application();
Thread.Sleep(5000);
word.Visible = false;
word.ScreenUpdating = false;
doc = word.Documents.Open(wordDocPath);
pdfPath = wordDocPath.Replace(".docx", ".pdf");
doc.ExportAsFixedFormat(
pdfPath,
Microsoft.Office.Interop.Word.WdExportFormat.wdExportFormatPDF,
OptimizeFor: Microsoft.Office.Interop.Word.WdExportOptimizeFor.wdExportOptimizeForOnScreen,
BitmapMissingFonts: true, DocStructureTags: false);
((Microsoft.Office.Interop.Word._Document)doc).Close();
((_Application)word).Quit(ref oMissing, ref oMissing, ref oMissing);
}
catch
{
if (doc != null)
{
((_Document)doc).Close(ref saveChanges, ref oMissing, ref oMissing);
}
if (word != null)
{
((_Application)word).Quit(ref oMissing, ref oMissing, ref oMissing);
}
throw;
}
}
catch (Exception ex)
{
var st = new StackTrace(ex, true);
// Get the top stack frame
var frame = st.GetFrame(0);
// Get the line number from the stack frame
var line = frame.GetFileLineNumber();
throw new Exception("Line Number: " + line.ToString() + " " + ex.ToString());
}
return pdfPath;
}
Got to know below answer from my colleague Anders.
Seconds answer here Open and modify Word Document says Microsoft does not support unattended autmation this way.
It also suggests using http://freeword.codeplex.com/
I have just verified that this script works - also from a job / background process:
Although this is not a free software, but it works on background process.
It can be easily ported to c# as well, as it is a .net dll.
import clr
clr.AddReferenceToFileAndPath(r"C:\Program Files (x86)\e-iceblue\Spire.Doc-FE\Bin\NET4.0\Spire.Doc.dll")
from Spire.Doc import *
from Spire.Doc.Documents import *
from System.IO import DirectoryInfo, File, FileInfo, Path
def ConvertWordToPDFUsingSPIRE(wordDocFullPath):
"""
<Script>
<Author>SNI</Author>
<Description>Converts Word document to PDF.</Description>
<Parameters>
<Parameter name="wordDocFullPath" type="string">Word document full path.</Parameter>
</Parameters>
<ReturnValue type="string">PDF full path</ReturnValue>
</Script>
"""
wordDocInfo = FileInfo(wordDocFullPath)
pdfFullPath = Path.Combine(wordDocInfo.DirectoryName, wordDocInfo.Name.TrimEnd(wordDocInfo.Extension.ToCharArray()) + '.pdf')
document = Document();
document.LoadFromFile(wordDocFullPath);
# Convert Word to PDF
document.SaveToFile(pdfFullPath, FileFormat.PDF);
I am new to C#. I am attempting to simply use Microsoft.Office.Interop.Word
All is working except that the Merge Fields in the footer are not updating. I continue to get the mergecode text only («pname»).
Here is the important part of my code
private void getDoc()
{
String cdir = Directory.GetCurrentDirectory();
btnGetPoa.Visible = false;
var application = new Word.Application();
Object dir = #"../../templates/";
Directory.SetCurrentDirectory(dir.ToString());
var doc = new Word.Document();
var dirf = Directory.GetCurrentDirectory() + "\\poas.docx";
doc = application.Documents.Add(Template: dirf);
foreach (Word.Field fld in doc.Fields)
{
if(fld.Code.Text.Contains("pname"))
{
fld.Select();
application.Selection.TypeText(txtpName.Text.ToString());
}
}
object what = Word.WdGoToItem.wdGoToPage;
object which = Word.WdGoToDirection.wdGoToFirst;
object count = 3;
object missing = Missing.Value;
application.Selection.GoTo(ref what, ref which, ref count, ref missing);
application.Visible = true;
}
Any ideas why the Fields in the Footer are not merging?
Thank you.
The Document object addresses only the main document "story". So querying fields in that object doesn't "see" any fields in headers, footers, Shapes, etc. You need to specifically address these other objects. For a general method on how to address all StoryRange objects, look at the VBA Language Reference example for StoryRange and related. You'll also find discussions and code on this site.
For your specific request, I'm assuming that the document has only the one section and the standard footer - no first page footer or even page footer. (A Word document can have multiple sections and different types of headers and footers, but a "plain vanilla" document has only one of each.)
Word.Range rngFooter = doc.Sections[1].Footers[Word.WdHeaderFooterIndex.wdHeaderFooterPrimary].Range;
foreach (Word.Field fld in rngFooter.Fields)
{
}
I have put together a simple C# Winforms Application that queries AS400 data based on user criteria and returns the results in a ListView Control. On Button_Click() I then store the Headers and Data in a .txt file. Below I am trying to use that .txt file as the DataSource for a Mail Merge word document.
When the code reaches oWrdDoc = oWord.Documents.Open(oTemplatePath); the program seems to just freeze up. Nothing occurs and I cannot step through to the next line. Anyone have ideas for what I am doing wrong?
public void Print(string docLoc, string docSource)
{
try
{
Word.Application oWord = new Word.Application();
Word.Document oWrdDoc = new Word.Document();
oWord.Visible = true;
Object oTemplatePath = "C:\\Users\NAME\\Desktop\\B-AIAddChgDual10-06-NEW.doc";
oWrdDoc = oWord.Documents.Open(oTemplatePath);
Object oMissing = System.Reflection.Missing.Value;
oWrdDoc.MailMerge.OpenDataSource("C:\\Users\\NAME\\Desktop\\Test2.txt", oMissing, oMissing, oMissing,
oMissing, oMissing, oMissing, oMissing, oMissing, oMissing, oMissing, oMissing, oMissing, oMissing, oMissing, oMissing);
oWrdDoc.MailMerge.Execute();
}
catch (Exception ex)
{
MessageBox.Show("Source:\t" + ex.Source + "\nMessage: \t" + ex.Message + "\nData:\t" + ex.Data);
}
finally
{
//
}
}
EDIT: Turns out I did not have the new Word instance set to Visible = true so when the instance brought up a dialog saying the file was locked for editing (by myself???) it was prompting me to open a Read-Only version, which previously I could not see, making it look like everything was frozen in processing. I've modified my code above to reflect the changes.
Any ideas for why I'm locked out of my own file, and how to prevent this?
These are the dialogs I receive after accepting open a Read-Only document (in order):
After selecting how to replace the fields in the above:
Original Mail Merge Fields:
After Personal Selections:
How do I tell the Word Application to use the '!' character as the Field Delimiter in my C# code?
Also, how do I proceed with the dialogs? I'm assuming I receive each one due to my datasource not containing fields matching those listed as Mail Merge Fields?
Here are my Mail Merge Fields:
-fuldate
-sys
-memno
-name
-address1
-address2
-address3
-sal
And here are my Delimited fields from my .txt DataSource file:
memno!name!addr1!addr2!city!state!zip!old_addr1!old_addr2!old_city!old_state!old_zip
You can set OpenAndRepair Mode to True and then Open the Document
Replace this:
oWrdDoc = oWord.Documents.Open(oTemplatePath);
With following:
oWrdDoc = word.Documents.Open(oTemplatePath, OpenAndRepair: true);
How can I convert an RTF file to a PDF one? I have the adobe PDF printer, should I use it? If so, how can I programmatically access it?
You can use a PDF printer, but then you still have a few problems to solve.
In order to handle text that spans multiple pages, you need this article to create a descendant of RichTextbox that handles the EM_FORMATRANGE Message.
There are a lot of (free) PDF printer out there, but I found that only BioPdf will let you control the filename of the output. They also have reasonable rates for licensed versions.
I have used this to create complex reports (combinations of multiple RTF segments and custom graphics) as attachments for emailing.
You could use the virtual print Driver doPdf http://www.dopdf.com/ if this is permitted on the production machine. This will convert more or less any file type to a pdf format not just rtf. It just appears as another printer within Print Manager once installed.
To use it in say winforms code I adapted the code found on the msdn printing example http://msdn.microsoft.com/en-us/library/system.drawing.printing.printdocument.aspx
private void button1_Click(object sender, EventArgs e)
{
try
{
streamToPrint = new System.IO.StreamReader
(#"F:\temp\labTest.txt");
try
{
printFont = new Font("Arial", 10);
PrintDocument pd = new PrintDocument();
pd.PrinterSettings.PrinterName = "doPDF v6";//<-------added
pd.PrintPage += new PrintPageEventHandler
(this.pd_PrintPage);
pd.Print();
}
finally
{
streamToPrint.Close();
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
The only part of the code I needed to add was that marked above e.g. pd.PrinterSettings.PrinterName = "doPDF v6";
There may be a printer enumeration method which would be more elegant and robust and against this one could test to see if the print driver existed perhaps against a config file setting.
Update:
Handling multiple pages is taken care of in this method : this.pd_PrintPage as per the msdn sample.
PrintDocument supports from and to page printing.
DoPdf will pops up a fileSaveAsDialog box automatically so the files can be saved as a pdf document.
What about rtf though?
A Microsoft format not supported very well so it would seem. This article http://msdn.microsoft.com/en-us/library/ms996492.aspx with demo code uses the RichTextBox as a starting point and by using P/Invoke leverages the power of Win32 to print RTF as WYSIWG. The control defines it's own page length method replacing the one used above in the code snippet and still uses PrintDocument so it should be easy to use. You can assign any rtf using Rtb.rtf method.
An RTF document has to be read and interpreted by some app that can understand that format. You would need to programmatically launch that app, load your RTF file, and send it to the PDF printer. Word would be good for that, since it has a nice .NET interface. An overview of the steps would be:
ApplicationClass word = new ApplicationClass();
Document doc = word.Documents.Open(ref filename, ...);
doc.PrintOut(...);
You will need to use the Microsoft.Office.Interop.Word namespace and add a reference to the Microsoft.Office.Interop.Word.dll assembly.
Actually, none of these are terribly reliable or do what I want. The solution is simple, install Adobe Acrobat and just have it open the RTF file using the Process class.
I also found a more reasonable approach. I save the file as an RTF, the open it in word, and save it as PDF (Word's Print As PDF plugin must be installed)
SaveFileDialog sfd = new SaveFileDialog();
sfd.Filter = "Personal Document File (*.pdf)|*.pdf";
if (sfd.ShowDialog() == DialogResult.OK) {
String filename = Path.GetTempFileName() + ".rtf";
using (StreamWriter sw = new StreamWriter(filename)) {
sw.Write(previous);
}
Object oMissing = System.Reflection.Missing.Value; //null for VB
Object oTrue = true;
Object oFalse = false;
Microsoft.Office.Interop.Word.Application oWord = new Microsoft.Office.Interop.Word.Application();
Microsoft.Office.Interop.Word.Document oWordDoc = new Microsoft.Office.Interop.Word.Document();
oWord.Visible = false;
Object rtfFile = filename;
Object saveLoc = sfd.FileName;
Object wdFormatPDF = 17; //WdSaveFormat Enumeration
oWordDoc = oWord.Documents.Add(ref rtfFile, ref oMissing, ref oMissing, ref oMissing);
oWordDoc.SaveAs(ref saveLoc, ref wdFormatPDF, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing,
ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing);
oWordDoc.Close(ref oFalse, ref oMissing, ref oMissing);
oWord.Quit(ref oFalse, ref oMissing, ref oMissing);
//Get the MD5 hash and save it with it
FileStream file = new FileStream(sfd.FileName, FileMode.Open);
MD5 md5 = new MD5CryptoServiceProvider();
byte[] retVal = md5.ComputeHash(file);
file.Close();
using (StreamWriter sw = new StreamWriter(sfd.FileName + ".md5")) {
sw.WriteLine(sfd.FileName + " - " + DateTime.Now.ToLongDateString() + " " + DateTime.Now.ToShortTimeString() + " md5: " + BinaryToHexConverter.To64CharChunks(retVal)[0]);
}
}