I wrote an addin a while back for Outlook that adds/removes an optional tagline below the signature in an outlook message. This add-in works with no issues.
I'm writing a second add-in that needs to potentially add information below that (whether or not the optional signature is there) and am again referencing the _MailAutoSig bookmark from the Word editor. The issue I'm running into is that this bookmark no longer seems to appear, nor does the bookmark from my other add-in.
One difference in the two pieces of code below is that the first one has the MailItem being converted from an object passed by ItemSend, whereas the second is processed BEFORE the ItemSend event.
Here is the code from what I am currently writing:
Word.Document toMsg = msg.GetInspector.WordEditor as Word.Document;
foreach (Word.Bookmark b in toMsg.Bookmarks)
Debug.Print(b.ToString());
Word.Range r_toMsg;
try
{
string oBookmark = "_MailAutoSig"; // Outlook internal bookmark for location of the e-mail signature`
object oBookmarkObj = oBookmark;
if (toMsg.Bookmarks.Exists(oBookmark) == true)
Debug.Print("sigbookmark");
r_toMsg = toMsg.Bookmarks.get_Item(ref oBookmarkObj).Range;
}
catch
{
string oOffsiteBookmark = "OffsiteBookmark";
object oOffsiteBookmarkObj = oOffsiteBookmark;
if (toMsg.Bookmarks.Exists(oOffsiteBookmark) == true) // if the custom bookmark exists, remove it
Debug.Print("offsite bookmark");
}
finally
{
r_toMsg = toMsg.Range(missing,missing);
}
and here is code from my working add-in:
void InsertOffsiteSig(Outlook.MailItem oMsg)
{
object oBookmarkName = "_MailAutoSig"; // Outlook internal bookmark for location of the e-mail signature
string oOffsiteBookmark = "OffsiteBookmark"; // bookmark to be created in Outlook for the Offsite tagline
object oOffsiteBookmarkObj = oOffsiteBookmark;
Word.Document SigDoc = oMsg.GetInspector.WordEditor as Word.Document; // edit the message using Word
string bf = oMsg.BodyFormat.ToString(); // determine the message body format (text, html, rtf)
// Go to the e-mail signature bookmark, then set the cursor to the very end of the range.
// This is where we will insert/remove our tagline, and the start of the new range of text
Word.Range r = SigDoc.Bookmarks.get_Item(ref oBookmarkName).Range;
object collapseEnd = Word.WdCollapseDirection.wdCollapseEnd;
r.Collapse(ref collapseEnd);
string[] taglines = GetRssItem(); // Get tagline information from the RSS XML file and place into an array
// Loop through the array and insert each line of text separated by a newline
foreach (string taglineText in taglines)
r.InsertAfter(taglineText + "\n");
r.InsertAfter("\n");
// Add formatting to HTML/RTF messages
if (bf != "olFormatPlain" && bf != "olFormatUnspecified")
{
SigDoc.Hyperlinks.Add(r, taglines[2]); // turn the link text into a hyperlink
r.Font.Underline = 0; // remove the hyperlink underline
r.Font.Color = Word.WdColor.wdColorGray45; // change all text to Gray45
r.Font.Size = 8; // Change the font size to 8 point
r.Font.Name = "Arial"; // Change the font to Arial
}
r.NoProofing = -1; // turn off spelling/grammar check for this range of text
object range1 = r;
SigDoc.Bookmarks.Add(oOffsiteBookmark, ref range1); // define this range as our custom bookmark
if (bf != "olFormatPlain" && bf != "olFormatUnspecified")
{
// Make the first line BOLD only for HTML/RTF messages
Word.Find f = r.Find;
f.Text = taglines[0];
f.MatchWholeWord = true;
f.Execute();
while (f.Found)
{
r.Font.Bold = -1;
f.Execute();
}
}
else
{
// otherwise turn the plain text hyperlink into an active hyperlink
// this is done here instead of above due to the extra formatting needed for HTML/RTF text
Word.Find f = r.Find;
f.Text = taglines[2];
f.MatchWholeWord = true;
f.Execute();
SigDoc.Hyperlinks.Add(r, taglines[2]);
}
r.NoProofing = -1; // disable spelling/grammar checking on the updated range
r.Collapse(collapseEnd);
}
The problem is that Microsoft converts the "Office HTML" (I'm not sure the proper term) to normal HTML before it fires the ItemSend() event, which causes the _MailAutoSig bookmark to disappear.
The only way to get the _MailAutoSig bookmark back is to first CANCEL the ItemSend() event, then fire off a timer to run a function which will in turn access the email object and manipulate it how you want, add a user property to mark the email has been processed, and then send the email again.
For example:
Dim modItem As Object 'need to hold the item somewhere so the timer can access it
Sub object_ItemSend(ByVal Item As Object, Cancel As Boolean)
If Item.UserProperties.Item("isModded") Is Nothing Then
'User has composed a mail and hit "Send", we need to make our modifications to the signature though
modItem = item
Cancel = True 'cancel the Send so we can make the modifications
mytimer.Enabled = True 'fire off a timer to make the modifications
Exit Sub
Else
Item.UserProperties.Item("isModded").Delete 'this flag will keep the email from ping-ponging between ItemSend and the timer
End If
End Sub
'10 millisecond timer? I think the cancel is almost instant, but experiment
Sub mytimer_Timer()
mytimer.Enabled = False
If Not modItem Is Nothing Then
modItem.HtmlBody = ...... the signature bookmark will be intact again, so make your modifications ......
modItem.UserProperties.Add("isModded", olText) 'no ping-pong
modItem.Send 'send it again
modItem = Nothing
End If
End Sub
I had to do something similar for a project where some Outlook fields were not set until I was in the ItemSend() event, so I was forcing the email to send, get my information, then cancel the send. It worked great.
Now, this was written off the top of my head, so I'm sure the code above will not be perfect, but it should give you the idea of what needs to be done.
Related
I am not finding a way to set the ContentControl.Range.Text from where the C# is executing from (inside the content control). Perhaps I should be looking at it from a completely different perspective.
Currently I have a content control that produces a set of text with some text between [] square brackets and I want to select text and format the colour by setting the start and end of the range of characters between the []. I am stuck on trying to set the initial range to the contentcontrol I am currently using.
Most of what I have managed/found/patched together below.
object word;
Microsoft.Office.Interop.Word.Document _PWdDoc;
try
{
word = System.Runtime.InteropServices.Marshal.GetActiveObject("Word.Application");
//If there is a running Word instance, it gets saved into the word variable
}
catch (Exception ex)
{
//If there is no running instance, it creates a new one
Type type = Type.GetTypeFromProgID("Word.Application");
word = System.Activator.CreateInstance(type);
}
Microsoft.Office.Interop.Word.Application oWord = (Microsoft.Office.Interop.Word.Application) word;
_PWdDoc = oWord.ActiveDocument;
System.Collections.IEnumerator ContentX = _PWdDoc.ContentControls.GetEnumerator();
//Microsoft.Office.Interop.Word.ContentControl ContentX = Microsoft.Office.Interop.Word.ContentControls.Item[];
//Microsoft.Office.Interop.Word.Range rng = Microsoft.Office.Interop.Word.ContentControl.Range.Duplicate(ref ContentX);
//var rngX = Microsoft.Office.Interop.Word.ContentControl.Range(ContentX);
//Microsoft.Office.Interop.Word.ContentControl cc1 = ContentX;
Excuse the coding mess but it's all I can come up with with the minimal experience I have with this.
Now I have gotten the IEnumerator fo the Content Control(I think) I have no idea how to use it besides from what I have read, they say to iterate through the IEnumerables accessing each of them. That's not what I want to do. I want 1 content control. The current one that I am working in. I want to find it's range and assign it to a value. Then in that range's "text" I want to do some [fancy] highlighting.
Determining whether the current selection or a specific Range is in a content control and doing something with that content control is not a trivial matter. Most other Word objects will return something that they're "in"; content controls do not.
So the approach I use is to
create a Range that reaches from the current selection (or a specific Range) back to the beginning of the document
count the number of content controls in that range
then check whether the current selection is in the same range as the last content control of the extended range.
if it is, then I know the selection is within a content control and I can access the content control.
Here's some sample code. The snippet that calls the function I use to return the information:
Word.Range rng = null;
//Substitute a specific Range object if working with a Range, rather than a Selection
Word.ContentControl cc = IsSelectionInCC(wdApp.Selection.Range);
if ( cc != null)
{
rng = cc.Range;
rng.HighlightColorIndex = Word.WdColorIndex.wdYellow;
}
The function:
private Word.ContentControl IsSelectionInCC(Word.Range sel)
{
Word.Range rng = sel.Range;
Word.Document doc = (Word.Document) rng.Parent;
rng.Start = doc.Content.Start;
int nrCC = rng.ContentControls.Count;
Word.ContentControl cc = null;
bool InCC = false;
rng.Start = doc.Content.Start;
if (nrCC > 0)
{
if (sel.InRange(doc.ContentControls[nrCC].Range))
{
InCC = true; //Debug.Print ("Sel in cc")
cc = doc.ContentControls[nrCC];
}
else
{
sel.MoveEnd(Word.WdUnits.wdCharacter, 1);
if (sel.Text == null)
{
//Debug.Print ("Sel at end of cc")
InCC = true;
cc = doc.ContentControls[nrCC];
}
}
}
return cc;
}
Assuming you mean that the insertion point is inside a Content Control, and your Word Application object is called oWord, then you can get the range of that content control using e.g.
Microsoft.Office.Interop.Word.Range r = oWord.Selection.Range.ParentContentControl.Range
If you have nested controls You can verify that the insertion point is in a Content Control (Word 2013 and later, I think) by checking the value of inCC as follows:
Boolean inCC = (Boolean)oWord.Selection.Information[Microsoft.Office.Interop.Word.WdInformation.wdInContentControl]
However, when dealing with content controls, be aware that selecting a content control in the UI is different from selecting the "range of the content control". Programmatically, it's obvious how to select the Range - not so obvious how to select the control. If you select the Range, the ParentContentControl should be the control whose range you've selected. If you (or the user) selected the control, OTTOMH I am not so sure.
I hope this easy but I can't seem to find it. I'm working with a word document object in an Outlook VSTO project to modify values that are located between hidden key values.
IE [key_start] text [key_end]
When they make selections in the drop down from the addin I change the text in the body of the email for them.
To make this work
1) I un-hide all my keys
Word.Document doc = Inspector.WordEditor as Word.Document;
doc.Content.Font.Hidden = 0;
2) Then find my keys and generate a range between them
int start_pos = -1;
int end_pos = -1;
//SelectTextRange is a custom function to find range based on text
ValueRange = Custom.WordDocument.SelectTextRange(doc, key_value_start);
if (ValueRange != null) {
start_pos = ValueRange.End;
}
ValueRange = Custom.WordDocument.SelectTextRange(doc, key_value_end);
if (ValueRange != null) {
end_pos = ValueRange.Start;
}
3) Then update the text.
if (start_pos > -1 && end_pos > -1) {
ValueRange = doc.Range(start_pos, end_pos);
ValueRange.Text = " new text goes here ";
}
4) Then hide my keys again.
Everything works great but it looks a little tacky when the changes cascade through the doc as it updates. It looks as though the document updates on every command and doesn't wait until all my commands are finished. Is there a way to prevent the document from committing changes so I can make it do this all in one shot and not have the user see flickers of hidden text when this process occurs?
YowE3K nailed it. Thank you again.
Adding this to the beginning of my changes
doc.Application.ScreenUpdating = False
And then adding this to the end
doc.Application.ScreenUpdating = True
Did exactly what I needed. All my changes occured without the document updating and then once I set doc.Application.ScreenUpdating = True it draws the updated document with all my changes.
I'm trying to make an application in C#. When pressing a radio button, I'd like to open a Microsoft Word document (an invoice) and replace some text with text from my Form. The Word documents also contains some textboxes with text.
I've tried to implement the code written in this link Word Automation Find and Replace not including Text Boxes but when I press the radio button, a window appears asking for "the encoding that makes the document readable" and then the Word document opens and it's full of black triangles and other things instead of my initial template for the invoice.
How my invoice looks after:
Here is what I've tried:
string documentLocation = #"C:\\Documents\\Visual Studio 2015\\Project\\Invoice.doc";
private void yes_radioBtn_CheckedChanged(object sender, EventArgs e)
{
FindReplace(documentLocation, "HotelName", "MyHotelName");
Process process = new Process();
process.StartInfo.FileName = documentLocation;
process.Start();
}
private void FindReplace(string documentLocation, string findText, string replaceText)
{
var app = new Microsoft.Office.Interop.Word.Application();
var doc = app.Documents.Open(documentLocation);
var range = doc.Range();
range.Find.Execute(FindText: findText, Replace: WdReplace.wdReplaceAll, ReplaceWith: replaceText);
var shapes = doc.Shapes;
foreach (Shape shape in shapes)
{
var initialText = shape.TextFrame.TextRange.Text;
var resultingText = initialText.Replace(findText, replaceText);
shape.TextFrame.TextRange.Text = resultingText;
}
doc.Save();
doc.Close();
Marshal.ReleaseComObject(app);
}
So if your word template is the same each time you essentially
Copy The Template
Work On The Template
Save In Desired Format
Delete Template Copy
Each of the sections that you are replacing within your word document you have to insert a bookmark for that location (easiest way to input text in an area).
I always create a function to accomplish this, and I end up passing in the path - as well as all of the text to replace my in-document bookmarks. The function call can get long sometimes, but it works for me.
Application app = new Application();
Document doc = app.Documents.Open("sDocumentCopyPath.docx");
if (doc.Bookmarks.Exists("bookmark_1"))
{
object oBookMark = "bookmark_1";
doc.Bookmarks.get_Item(ref oBookMark).Range.Text = My Text To Replace bookmark_1;
}
if (doc.Bookmarks.Exists("bookmark_2"))
{
object oBookMark = "bookmark_2";
doc.Bookmarks.get_Item(ref oBookMark).Range.Text = My Text To Replace bookmark_2;
}
doc.ExportAsFixedFormat("myNewPdf.pdf", WdExportFormat.wdExportFormatPDF);
((_Document)doc).Close();
((_Application)app).Quit();
This code should get you up and running unless you want to pass in all the values into a function.
EDIT: If you need more examples I'm working on a blog post as well, so I have a lot more detail if this wasn't clear enough for your use case.
I'm creating a Microsoft Office 2013 Add-in that does some behaviour similar to the Address book in an Active Mail item:
I'm working with Microsoft.Office.Interop.Outlook.MailItem and using the Recipients property, and the Recipients.ResolveAll() method to load and remove addresses to it, everything seems to work fine until I click OK on my loaded from so I send control back to the active mail Item. At this point Outlook goes crazy and doesn't place the addresses properly in the To or the CC, or some of them go missing.
I'm thinking that one solution to the problem could be to trigger the Check Names when the user is ready to click OK and send all the addresses from my form to the Active mail item.
How can I trigger this action / button?
I couldn't find anything that could do that in the MailItem class. I would like something similar to this (but to use from within my poped-out windows form in Outloook):
UPDATE:
This are some bits of my code to add more context:
I use the method AddRecipientToActiveItem for each item that I have. It will verify if it already exists (it was already added), if not it will resolve it and if it is correct add it.
private void AddRecipientToActiveItem(string recipientAddress, Recipients recipientList, OlMailRecipientType recipientType)
{
Recipient recipientObject = default(Recipient);
if (!string.IsNullOrWhiteSpace(recipientAddress) && !EmailRecipientAlreadyExists(recipientAddress, recipientType))
{
recipientObject = recipientList.Add(recipientAddress);
recipientObject.Resolve();
if (recipientObject.Resolved)
{
recipientObject.Type = (int)recipientType;
recipientList.ResolveAll();
}
else
{
recipientObject.Delete();
}
}
}
for this, I have to iterate through each element in the list of recipients and compare by address + type (From,To,CC,BCC) pair:
private bool EmailRecipientAlreadyExists(string fullEmailAddress, OlMailRecipientType recipientType)
{
foreach (Microsoft.Office.Interop.Outlook.Recipient recipientObject in ActiveMailItem.Recipients)
{
if (GetRecipientEmailAddress(recipientObject) != null)
{
if (GetRecipientEmailAddress(recipientObject).Equals(fullEmailAddress) && recipientObject.Type == (int)recipientType)
return true;
}
}
return false;
}
But the user can also add addresses to their email item manually, and some of them exist in the exchange server, but others are simple smtp address, so when comparing I have to handle both scenarios:
private string GetRecipientEmailAddress(Microsoft.Office.Interop.Outlook.Recipient recipientObject)
{
Outlook.ExchangeUser objExchangeUser = null;
if (recipientObject.Address != null && recipientObject.AddressEntry != null)
objExchangeUser = recipientObject.AddressEntry.GetExchangeUser();
if (recipientObject.Address == null && objExchangeUser == null)
return recipientObject.Name;
if (objExchangeUser == null)
return recipientObject.Address;
return objExchangeUser.PrimarySmtpAddress;
}
}
It seems like this what I've done is not enough to keep consistent states of the email addresses when I read them from the mail item and put them in a text box of my form, then add more addresses and send them back to the mail item and close the form, and repeat the process.
The Namespace class provides the GetSelectNamesDialog method which obtains a SelectNamesDialog object for the current session. It displays the Select Names dialog box for the user to select entries from one or more address lists, and returns the selected entries in the collection object specified by the property SelectNamesDialog.Recipients.
Sub SelectRecipients()
Dim oMsg As MailItem
Set oMsg = Application.CreateItem(olMailItem)
Dim oDialog As SelectNamesDialog
Set oDialog = Application.Session.GetSelectNamesDialog
With oDialog
.InitialAddressList = _
Application.Session.GetGlobalAddressList
.Recipients = oMsg.Recipients
If .Display Then
'Recipients Resolved
oMsg.Subject = "Hello"
oMsg.Send
End If
End With
End Sub
The dialog box displayed by SelectNamesDialog.Display is similar to the Select Names dialog box in the Outlook user interface. It observes the size and position settings of the built-in Select Names dialog box. However, its default state does not show Message Recipients above the To, Cc, and Bcc edit boxes. For more information on using the SelectNamesDialog object to display the Select Names dialog box, see Display Names from the Address Book.
Im trying to translate this VBA code from an Outlook AddIn to C#
Private Sub objInspector_Activate() Handles objInspector.Activate
Dim wdDoc As Microsoft.Office.Interop.Word.Document = objInspector.WordEditor
wdDoc.Windows(1).Panes(1).View.Zoom.Percentage = lngZoom
End Sub
But I can't get access to the Panes.View.Zoom.Percentage property
The main idea is that when the user opens an email, he will get a custom zoom level.
What I got at the moment is:
void Inspector_Activate()
{
// this bool is true
// bool iswordMail = objInspector.IsWordMail();
//I get the word document
Document word = objInspector.WordEditor as Microsoft.Office.Interop.Word.Document;
word.Application.ActiveDocument.ActiveWindow.View.Zoom.Percentage = 150;
// at this point i'm getting an exception
// I've also tried with
// word.ActiveWindow.ActivePane.View.Zoom.Percentage = 150; getting the same exception
}
The exception is :
An exception of type 'System.Runtime.InteropServices.COMException'
occurred in OutlookAddInTest.dll but was not handled in user code
Additional information: This object model command is not available in
e-mail.
I'm quite new in C# and Office addins, any advise?
Use word.Windows.Item(1).View.Zoom.Percentage = 150 (where word comes from Inspector.WordEditor)
word.Application.ActiveDocument.ActiveWindow.View.Zoom.Percentage = 150;
What property exactly fires the exception?
Anyway, there is no need to call the Application and ActiveDocument properties in the code. The WordEditor property of the Inspector class returns an instance of the Document class (not Word Application instance).
Thanks to Eugene Astafiev for his help.
The square brackets did the trick
VBA
Private Sub objInspector_Activate() Handles objInspector.Activate
Dim wdDoc As Microsoft.Office.Interop.Word.Document = objInspector.WordEditor
wdDoc.Windows(1).Panes(1).View.Zoom.Percentage = 150
End Sub
C#
private void Inspector_Activate()
{
Document wdDoc = objInspector.WordEditor;
wdDoc.Windows[1].Panes[1].View.Zoom.Percentage = 150;
}
I've been wanting this forever, and then I stumbled on a nice project in the MSDN Gallery Outlook 2010: Developing an Inspector Wrapper. It has a set of wrappers for all the Outlook objects, so you get a true event for every item of interest. Not sure if it's the most efficient thing ever, but it seems to work.
I have trouble with my eyesight so want black everything, and zoom everything. I seem to be able to do that by overriding the Activate() method. It's all pretty new so we'll see if it survives long term.
protected virtual void Activate() {
var activeDocument = Inspector.WordEditor as Document;
if (activeDocument == null)
return;
var mailZoom = GetSetting("MailZoom", 125);
if (mailZoom != 0)
activeDocument.Windows[1].View.Zoom.Percentage = mailZoom;
if (GetSetting("MailBlack", true)) {
activeDocument.Background.Fill.ForeColor.RGB = 0;
activeDocument.Background.Fill.Visible = msoTrue;
activeDocument.Saved = true;
}
}
In this example, GetSetting is just a function that returns a setting from an INI file. you can use constants or some other storage method.
There might be a better way to get the white on black text, but this seems pretty good.