We are developing an Outlook VSTO add-in.
Right now I am trying to append some information to a meeting invite the user is in the process of composing. I want the content to appear in the body like what clicking the Teams-meeting button would do, where formatted text and links are appended to the end of the body.
Since the content is HTML and the Outlook Object Model does not expose an HTMLBody property for AppointmentItems, I try to set it via Redemption:
// Dispose logic left out for clarity but everything except outlookApplication and outlookAppointment is disposed after use
Application outlookApplication = ...;
AppointmentItem outlookAppointment = ...; // taken from the open inspector
NameSpace outlookSession = outlookApplication.Session;
RDOSession redemptionSession = RedemptionLoader.new_RDOSession();
redemptionSession.MAPIOBJECT = outlookSession.MAPIOBJECT;
var rdoAppointment = (RDOAppointmentItem)redemptionSession.GetRDOObjectFromOutlookObject(outlookAppointment);
string newBody = transform(rdoAppointment.HTMLBody); // appends content right before HTML </body> tag
rdoAppointment.BodyFormat = (int)OlBodyFormat.olFormatHTML;
rdoAppointment.HTMLBody = newBody;
Problem
The Outlook inspector window is not updating with the appended content. If I try to run the code again, I can see the appended content in the debugger, but not in Outlook.
Things I have tried:
Saving the RDOAppointmentItem
Also adding the content to Body property
Using SafeAppointmentItem instead of RDOAppointmentItem; didn't work because HTMLBody is a read-only property there
Setting PR_HTML via RDOAppointment.Fields
Paste the HTML via WordEditor (see below)
Attempt to use WordEditor
Per suggestion I also attempted to insert the HTML via WordEditor:
// Dispose logic left out for clarity but everything except inspector is disposed after use
string htmlSnippet = ...;
Clipboard.SetText(htmlSnippet, TextDataFormat.Html);
Inspector inspector = ...;
Document wordDoc = inspector.WordEditor;
Range range = wordDoc.Content;
range.Collapse(WdCollapseDirection.wdCollapseEnd);
object placement = WdOLEPlacement.wdInLine;
object dataType = WdPasteDataType.wdPasteHTML;
range.PasteSpecial(Placement: ref placement, DataType: ref dataType);
... but I simply receive the error System.Runtime.InteropServices.COMException (0x800A1066): Kommandoen lykkedes ikke. (= "Command failed").
Instead of PasteSpecial I also tried using PasteAndFormat:
range.PasteAndFormat(WdRecoveryType.wdFormatOriginalFormatting);
... but that also gave System.Runtime.InteropServices.COMException (0x800A1066): Kommandoen lykkedes ikke..
What am I doing wrong here?
EDIT: If I use Clipboard.SetText(htmlSnippet, TextDataFormat.Text); and then use plain range.Paste();, the HTML is inserted at the end of the document as intended (but with the HTML elements inserted literally, so not useful). So the general approach seems to be okay, I just can't seem to get Outlook / Word to translate the HTML.
Version info
Outlook 365 MSO 32-bit
Redemption 5.26
Since the appointment is being displayed, work with the Word Object Model - Inspector.WordEditor returns the Document Word object.
Per Dmitrys suggestion, here is a working solution that:
Shows the inserted content in the inspector window.
Handles HTML content correctly with regards to both links and formatting (as long as you stay within the limited capabilities of Words HTML engine).
using System;
using System.IO;
using System.Text;
using Outlook = Microsoft.Office.Interop.Outlook;
using Word = Microsoft.Office.Interop.Word;
namespace VSTO.AppendHtmlExample
{
public class MyExample
{
public void AppendAsHTMLViaFile(string content)
{
// TODO: Remember to release COM objects range and wordDoc and delete output file in a finally clause
Outlook.Inspector inspector = ...;
string outputFolderPath = ...;
string outputFilePath = Path.Combine(outputFolderPath, "append.html");
Word.Document wordDoc = inspector.WordEditor;
File.WriteAllText(outputFilePath, $"<html><head><meta charset='utf-8'/></head><body>{content}</body></html>", Encoding.UTF8);
Word.Range range = wordDoc.Content;
range.Collapse(Word.WdCollapseDirection.wdCollapseEnd);
object confirmConversions = false;
object link = false;
object attachment = false;
range.InsertFile(fileName,
ConfirmConversions: ref confirmConversions,
Link: ref link,
Attachment: ref attachment);
}
}
}
Related
I'm trying to automatically add an attachment to a PDF file through VBA.
From what I can tell it is possible to handle this through FileAttachment annotations.
Dim testAnno As Object
Dim props As Object
Set testAnno = jso.AddAnnot
Set props = testAnno.getProps
props.Type = "FileAttachment"
' Code to attach file to PDF as attachment ......
testAnno.setProps props
I've found the following example in the Adobe documentation so essentially looking for a VBA equivalent to this:
var annot = this.addAnnot({
page: 0
type: "FileAttachment",
point: [400,500],
author: "A. C. Robat",
contents: "Call Smith to get help on this paragraph.",
cAttachmentPath: "/d/a.pdf" ,
});
I want to add a "resend" context menu in my Outlook 2016 add-in, to resend an email. The original email should be re-displayed to the user for him to make any modifications if necessary, and then press the 'send' button. It seems that I need to create a copy of the email, as calling Display() on the original message (or a copy created with MailItem.Copy()) just views the message, as opposed to showing it editable with a send button.
I got this so far - pretty straight forward:
Outlook.MailItem clone = Globals.ThisAddIn.Application.CreateItem(Outlook.OlItemType.olMailItem) as Outlook.MailItem;
clone.SendUsingAccount = email.SendUsingAccount;
clone.To = email.To;
clone.CC = email.CC;
clone.BCC = email.BCC;
clone.Subject = email.Subject;
clone.Body = email.Body;
clone.HTMLBody = email.HTMLBody;
for (int i = 1; i <= email.Attachments.Count; ++i)
clone.Attachments.Add(email.Attachments[i], email.Attachments[i].Type, email.Attachments[i].Position, email.Attachments[i].DisplayName);
However, I am getting a DISP_E_MEMBERNOTFOUND error when trying to copy the attachments. What am I doing wrong?
Attachments.Add only allows to pass a string pointing to a fully qualified path to a file or an Outlook item (such as MailItem). Also note that you code only copies the recipient display names, which may or may not be successfully resolved.
Outlook Object Model exposes MailItem.Copy method, but it creates a copy in the same sent/unsent state as the original.
If using Redemption (I am its author) is an option, you can use RDOMail.CopyTo() method - it will copy all the properties and sub-objects (such as recipients and attachments) but it will leave the sent state intact (since in MAPI it can only be set before the message is saved for the very first time).
Off the top of my head:
using Redemption;
...
RDOSession session = new RDOSession();
session.MAPIOBJECT = Globals.ThisAddIn.Application.Session.MAPIOBJECT;
RDOMail clone = session.GetDefaultFolder(rdoDefaultFolders.olFolderDrafts).Items.Add();
RDOMail original = (RDOMail)session.GetRDOObjectFromOutlookObject(email);
original.CopyTo(clone);
clone.Save();
MailItem OutlookClone = Globals.ThisAddIn.Application.Session.GetItemFromID(clone.EntryID);
OutlookClone.Display()
I know that when adding text/content/DataContext in XAML you refer to resource dictionary or inline mark up for styling around text or in template.
Q:
However I'm having trouble trying to find a way to do the following:
Data is coming from a View Model/Model that is pulled from a database.
(string value)
I am a <Bold>smart</Bold> man.
to show in a flow document like this:
I am a smart man.
Q end
Either by binding to a converter, behavior, or would saving the paragraph/document that I put in the flow document to a .rtf file in memory stream be a better option?
I've tried to utilize the option for behavior listed > here < but that is for text block and unable to redirect for type text instead of text block.
Trying to make it streamlined.
Tried to use data binding and apply the converter but even though I have the resource for the behavior / converter, it work due to the type conversion.
One clever solution is presented by Rockford Lhotka in post Set rich text into RichTextBlock control. His idea is to create a custom control which then creates the RichTextBlock using XamlReader.Load.
This allows you to use code like the following:
<local:RichTextDisplay Xaml="{Binding Hello}" HorizontalAlignment="Center"
VerticalAlignment="Center"/>
Where Hello is:
public string Hello { get; set; } = "I am a <Bold>smart</Bold> man.";
With a result:
If you use UWP/Win 8.1 XAML, you can use the original code from the blog post with the following small change (Paragraphs added):
<UserControl
xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml""
xmlns:mc=""http://schemas.openxmlformats.org/markup-compatibility/2006"">
<Grid>
<RichTextBlock><Paragraph>");
xaml.Append(ctl.Xaml);
xaml.Append(#"
</Paragraph></RichTextBlock>
</Grid>
</UserControl>
");
To answer my own question:
My case was creating a Document style display for user to update and save as a PDF, but I didn't want to rely on Office being on our application Server.
So I resolved this in my case by using a full "doc.RTF" document and importing that as a memory stream/string and apply my needed updates for values to that.
i.e. VB.net snippet example
Using uStream = Assembly.GetExecutingAssembly.GetManifestResourceStream("Resourcefilepath.rtf")
Using mStream As system.IO.MemoeryStream = New MemoryStream()
uStream.CopyTo(mStream)
rtfstring = Encoding.UTF8.GetSTring(mStream.toArray())
'--Do the updates to the needed string as needed:
rtfstring.Replace("Value","UpdatedValue")
'--Load Property Memory String this method is returnind
RTFDataProperty = New MemoryStream(Encoding.UTF8.GetBytes(rtfstring))
End Using
End Using
Then I loaded my XAML Rich Text Box with that memory stream as DataFormats.Rtf.
RichTextBox1.SelectAll()
RichTextBox1.Selection.Load(ClassName.RTFDataProperty, DataFormats.Rtf)
This gave me a template for formatting and layout of that document. (More of a case scenario and not a normal practice)
I also wanted to apply a starting selection so here is what I did there:
'--Get my RichTextBox Text
rtbtext As String = New TextRange(RichTextBox1.Document.contentStart, RichTextbox1.Document.ContentEnd).Text
Dim strStartSelection As String = "Comments..."
Dim startTP As TextPointer
Dim endTP As TextPointer
'--Loop through the paragraphs of the richtextbox for my needed selection starting point:
For Each para As Paragraph In RichTextBox1.Document.Blocks
Dim paraText As String = New TextRange(para.ContentStart, para.ContentEnd).Text
If paraText = "" Then
Dim pos As TextPointer = para.ContentStart
startTP = pos
endTP = startTP.GetPositionAtOffset("".Length + 3) '--my string had ... on the end so had to add for avoiding the escape of that on length
RichTextBox1.Selection.Select(startTP, endTP)
RichTextBox1.Focus()
Exit For
End If
Next
This is the simple VB.net code layout, but you can simplify and adjust from there if you find it useful.
Thanks
I have this script that finds a paragraph style, puts an item from a library at the very end and applies object style:
myDoc = app.documents[0];
myLib = app.libraries[0];
myObjectStyle = myDoc.objectStyles.item ("marker");
app.findTextPreferences = app.changeTextPreferences = null;
app.findTextPreferences.appliedParagraphStyle = "Custom"
var myFound = app.activeDocument.findText(true);
alert (myFound.length);
try {
for (i = 0; i < myFound.length; i++) {
myIcon = myLib.assets.itemByName("winieta_tr").placeAsset (myFound[i].insertionPoints[-2])[0];
myIcon.appliedObjectStyle = myObjectStyle;
// myFound[i].remove ();
}
}
catch (e) {alert (e.message)}
I don't know how to alter it, so the items are obtained not from library but form pasteboard - any help would be appreciated.
Is it possible to find elements that are in the document by name, as it is with library elements?
Yes, you can find an object by name (you would assign that name in the layers panel) simply by using
myDoc.pageItems.itemByName("myItemName");
If you are looking for the same thing on a specific spread (for example if several items on several spreads have the same name), you can use
myDoc.spreads[0].pageItems.itemByName("myItemName");
Or if you just want to use the currently active spread
app.activeWindow.activeSpread.pageItems.itemByName("myItemName");
Just make sure not to use the page to address a page item on the pasteboard as the pasteboard does not belong to any page.
Is it possible to find elements that are in the document by name, as it is with library elements?
You can apply a script labels to the frame on the pasteboard to give it a name.
So I've coded a VSTO addin using vb.net to add a header to a document in Word however from historical methods we have lots of templates with field codes. My addin does not account for these and simply strips the header to add xxxxx value you choose from the pop up.
I need my code to be smart enough to 'spot' the field code and append or if it does not exist e.g. a blank document then continue running as expected. I can append this field code using the below code:
wordDocument.Variables("fieldname").Value = "xxxx"
wordDocument.Fields.Update
However my tool then adds the header as normal and strips most the content from the template. So effectively my question is how would I code a check for this before proceeding. So in plain English I would need my addin to go from this:
Load pop up
Set xxxx value in header
Close
To this:
Load pop up
Check Document for existing "fieldname"
If "fieldname" exists then
wordDocument.Variables("fieldname").Value = "xxxx" (from pop up selection)
wordDocument.Fields.Update
However if "fieldname" doesn't exist then continue as normal....
Sorry if this is a little complex and/or long winded.
Thanks in advance.
Here is my code in C#, hope this might help you to code in VB.Net
foreach (Section sec in doc.Sections)
{
doc.ActiveWindow.View.set_SeekView(WdSeekView.wdSeekCurrentPageHeader);
foreach (HeaderFooter headerFooter in sec.GetHeadersFooters())
{
doc.ActiveWindow.View.set_SeekView(headerFooter.IsHeader ? WdSeekView.wdSeekCurrentPageHeader : WdSeekView.wdSeekCurrentPageFooter);
if (headerFooter.Range.Fields.Count > 0)
{
//Append to existing fields
UpdateFields(headerFooter.Range.Fields);
}
else
{
//Add field code
AddFieldCode(headerFooter.Range);
}
}
doc.ActiveWindow.View.set_SeekView(WdSeekView.wdSeekMainDocument);
}
Extension method to iterate through the header types
public static IEnumerable<HeaderFooter> GetHeadersFooters(this Section section)
{
List<HeaderFooter> headerFooterlist = new List<HeaderFooter>
{
section.Headers[WdHeaderFooterIndex.wdHeaderFooterPrimary],
section.Headers[WdHeaderFooterIndex.wdHeaderFooterFirstPage],
section.Headers[WdHeaderFooterIndex.wdHeaderFooterEvenPages],
section.Footers[WdHeaderFooterIndex.wdHeaderFooterPrimary],
section.Footers[WdHeaderFooterIndex.wdHeaderFooterFirstPage],
section.Footers[WdHeaderFooterIndex.wdHeaderFooterEvenPages]
};
return headerFooterlist;
}