We have written an add-on for Outlook that files emails into our CRM system. Int he process of this, it saves the Outlook Message ID as a UserField on the Message itself.
eg.
currentUserProperty = Constants.APPLICATION_NAME + "EntryID";
mailItem.UserProperties.Add(currentUserProperty,
Microsoft.Office.Interop.Outlook.OlUserPropertyType.olText,
Missing.Value,
Missing.Value).Value = entryId;
Unfortunately, this is a HUUUGGEE number, much like:
"00000000D502D779150E2F4580B1AADDF04ECDA6070097EF5A1237597748A4B4F9BFF540020800000006E9E4000068BB5B6DFC36924FAEC709A17D056583000002DE0E350000"
The problem is that when the user prints the message off, Outlook insists on including this field (beneath the From/To) and because it has no spaces, cannot wrap the ID and compresses the A4 page until it can fit on horizontally. This produces teeny-tiny email printouts.
Is there any way I can correct this? I had thought of overwriting the field OriginalEntryID (which is the one causing the problem) with one delimited by spaces but I get an exception from the COM layer. My next stop is to try and suppress the output of this and other user-defined fields on Outlook stationary.
Does anyone know how this can be achieved?
You must use .NET Reflection to fix this (as recommended by Microsoft Support). Hopefully this will be fixed in future versions of the VSTO SDK.
Suppress Outlook User Field Printing
static void SuppressUserPropertyPrinting(Outlook.MailItem message)
{
try
{ // Late Binding in .NET: https://support.microsoft.com/en-us/kb/302902
Type userPropertyType;
long dispidMember = 107;
long ulPropPrintable = 0x4; // removes PDO_PRINT_SAVEAS
string dispMemberName = String.Format("[DispID={0}]", dispidMember);
object[] dispParams;
if (message.UserProperties.Count == 0) return; // no props found (exit)
// marks all user properties as suppressed
foreach (Outlook.UserProperty userProperty in message.UserProperties.Cast<Outlook.UserProperty>())
{
if (userProperty == null) continue; // no prop found (go to next)
userPropertyType = userProperty.GetType(); // user property type
// Call IDispatch::Invoke to get the current flags
object flags = userPropertyType.InvokeMember(dispMemberName, BindingFlags.GetProperty, null, userProperty, null);
long lFlags = long.Parse(flags.ToString()); // default is 45 - PDO_IS_CUSTOM|PDO_PRINT_SAVEAS|PDO_PRINT_SAVEAS_DEF (ref: http://msdn.microsoft.com/en-us/library/ee415114.aspx)
// Remove the hidden property Printable flag
lFlags &= ~ulPropPrintable; // change to 41 - // PDO_IS_CUSTOM|PDO_PRINT_SAVEAS_DEF (ref: http://msdn.microsoft.com/en-us/library/ee415114.aspx)
// Place the new flags property into an argument array
dispParams = new object[] { lFlags };
// Call IDispatch::Invoke to set the current flags
userPropertyType.InvokeMember(dispMemberName, BindingFlags.SetProperty, null, userProperty, dispParams);
}
}
catch { } // safely ignore if property suppression doesn't work
}
I think this might be able to help me:
http://www.add-in-express.com/forum/read.php?FID=5&TID=5071#postform
Related
I'm writing a visual studio extension based on the Concord Samples Hello World project. The goal is to let the user filter out stack frames by setting a list of search strings. If any of the search strings are in a stack frame, it is omitted.
I've got the filter working for a hardcoded list. That needs to be in a non-package-based dll project in order for the debugger to pick it up. And I have a vsix project that references that dll with an OptionPageGrid to accept the list of strings. But I can't for the life of me find a way to connect them.
On the debugger side, my code looks something like this:
DkmStackWalkFrame[] IDkmCallStackFilter.FilterNextFrame(DkmStackContext stackContext, DkmStackWalkFrame input)
{
if (input == null) // null input frame indicates the end of the call stack. This sample does nothing on end-of-stack.
return null;
if (input.InstructionAddress == null) // error case
return new[] { input };
DkmWorkList workList = DkmWorkList.Create(null);
DkmLanguage language = input.Process.EngineSettings.GetLanguage(new DkmCompilerId());
DkmInspectionContext inspection = DkmInspectionContext.Create(stackContext.InspectionSession, input.RuntimeInstance, input.Thread, 1000,
DkmEvaluationFlags.None, DkmFuncEvalFlags.None, 10, language, null);
string frameName = "";
inspection.GetFrameName(workList, input, DkmVariableInfoFlags.None, result => GotFrameName(result, out frameName));
workList.Execute();
CallstackCollapserDataItem dataItem = CallstackCollapserDataItem.GetInstance(stackContext);
bool omitFrame = false;
foreach (string filterString in dataItem.FilterStrings)
{
if (frameName.Contains(filterString))
{
omitFrame = true;
}
}
The CallstackCollapserDataItem is where I theoretically need to retrieve the strings from user settings. But I don't have access to any services/packages in order to e.g. ask for WritableSettingsStore, like in You've Been Haacked's Example. Nor can I get my OptionPageGrid, like in the MSDN Options Example.
The other thing I tried was based on this StackOverflow question. I overrode the LoadSettingsFromStorage function of my OptionPageGrid and attempted to set a static variable on a public class in the dll project. But if that code existed in the LoadSettingsFromStorage function at all, the settings failed to load without even entering the function. Which felt like voodoo to me. Comment out the line that sets the variable, the breakpoint hits normally, the settings load normally. Restore it, and the function isn't even entered.
I'm at a loss. I really just want to pass a string into my Concord extension, and I really don't care how.
Ok, apparently all I needed to do was post the question here for me to figure out the last little pieces. In my CallstackCollapserDataItem : DkmDataItem class, I added the following code:
private CallstackCollapserDataItem()
{
string registryRoot = DkmGlobalSettings.RegistryRoot;
string propertyPath = "vsix\\CallstackCollapserOptionPageGrid";
string fullKey = "HKEY_CURRENT_USER\\" + registryRoot + "\\ApplicationPrivateSettings\\" + propertyPath;
string savedStringSetting = (string)Registry.GetValue(fullKey, "SearchStrings", "");
string semicolonSeparatedStrings = "";
// The setting resembles "1*System String*Foo;Bar"
if (savedStringSetting != null && savedStringSetting.Length > 0 && savedStringSetting.Split('*').Length == 3)
{
semicolonSeparatedStrings = savedStringSetting.Split('*')[2];
}
}
vsix is the assembly in which CallstackCollapserOptionPageGrid is a DialogPage, and SearchStrings is its public property that's saved out of the options menu.
I'm referring this thread to refresh the windows explorer, I want to refresh some windows only, which means I want to filter the opened windows according to their title or path. Let me copy the code from that thread for more clarification:
Guid CLSID_ShellApplication = new Guid("13709620-C279-11CE-A49E-444553540000");
Type shellApplicationType = Type.GetTypeFromCLSID(CLSID_ShellApplication, true);
object shellApplication = Activator.CreateInstance(shellApplicationType);
object windows = shellApplicationType.InvokeMember("Windows", System.Reflection.BindingFlags.InvokeMethod, null, shellApplication, new object[] { });
Type windowsType = windows.GetType();
object count = windowsType.InvokeMember("Count", System.Reflection.BindingFlags.GetProperty, null, windows, null);
for (int i = 0; i < (int)count; i++)
{
object item = windowsType.InvokeMember("Item", System.Reflection.BindingFlags.InvokeMethod, null, windows, new object[] { i });
Type itemType = item.GetType();
string itemName = (string)itemType.InvokeMember("Name", System.Reflection.BindingFlags.GetProperty, null, item, null);
if (itemName == "Windows Explorer")
{
// Here I want to check whether this window need to be refreshed
// based on the opened path in that window
// or with the title of that window
// How do I check that here
itemType.InvokeMember("Refresh", System.Reflection.BindingFlags.InvokeMethod, null, item, null);
}
}
What I understood from the above code is: By using this line windowsType.InvokeMember("Item", System.Reflection.BindingFlags.InvokeMethod, null, windows, new object[] { i }); we will get the current window object, and then we are using .InvokeMember("Name".. to get the name of that object, like wise what should I pass to InvokeMember method to get the path of that object or the title of that window? or can anyone tell me the possible alternative values for "Name" in the above statement?
What I'm expecting is some code like the following:
string itemPath = (string)itemType.InvokeMember("Something here", System.Reflection.BindingFlags.GetProperty, null, item, null);
OR
string itemTitle = (string)itemType.InvokeMember("Something here", System.Reflection.BindingFlags.GetProperty, null, item, null);
I can give you more information if you need, expecting expert's suggestion to solve this issue,
Thanks in advance
This is the way you had to write late-bound COM client code in the Bad Old Days. Considerable pain and suffering to get it going, what is in the snippet is not close yet. I'll first propose a very different way to do this, there just isn't any point in doing it late-bound since these COM objects are available on any Windows version and are never going to change anymore. The "Embed Interop Types" feature supported since VS2010 removes any good reason to avoid it.
Project > Add Reference > COM tab. Tick "Microsoft Internet Controls" and "Microsoft Shell Controls and Automation". Now you can write it early-bound, nice and compact with all the benefits of IntelliSense to help you find the correct members and avoid typos:
var shl = new Shell32.Shell();
foreach (SHDocVw.InternetExplorer win in shl.Windows()) {
var path = win.LocationURL;
if (!path.StartsWith("file:///")) continue;
path = System.IO.Path.GetFullPath(path.Substring(8));
if (path.StartsWith("C")) win.Refresh();
}
A slightly silly example, it refreshes any Explorer window who's displayed path is located on the C drive. Note how the Path property is not useful to discover what is displayed, LocationURL is needed. You might have to find the distinction between Internet Explorer and Windows Explorer windows (aka "File Explorer"), albeit that IE can also display directory content so I think this is the most correct version.
If you really want to do this late-bound then use the dynamic keyword to minimize the suffering. In this case almost identical:
dynamic shl = Activator.CreateInstance(Type.GetTypeFromProgID("Shell.Application"));
foreach (var win in shl.Windows()) {
string path = win.LocationURL;
if (!path.StartsWith("file:///")) continue;
path = System.IO.Path.GetFullPath(path.Substring(8));
if (path.StartsWith("C")) win.Refresh();
}
Answering your question explicitly, use "LocationURL".
Using VSTO and C#, I'm trying to get Outlook to highlight specific words in an e-mail body. So far I've been able to get this done using the code below:
Outlook.MailItem mailItem = this.inspector.CurrentItem as Outlook.MailItem;
if (inspector.IsWordMail())
{
var outlookWordDocument = inspector.WordEditor as Word.Document;
if (outlookWordDocument == null || outlookWordDocument.Application.Selection == null)
{ return; }
var wordRange = outlookWordDocument.Application.Selection.Range;
Word.Find find_highlight = wordRange.Find;
find_highlight.HitHighlight("apples", Word.WdColor.wdColorDarkRed);
find_highlight.ClearHitHighlight(); // trying to clear for testing purposes, but does nothing
}
My problem is that the ClearHitHighlight() function isn't clearing anything. The only way I can clear is if I perform another search right after. See comments below:
find_highlight.HitHighlight("apples"); //highlights "apples"
find_highlight.HitHighlight("oranges"); //highlights "oranges" too
find_highlight.ClearHitHighlight(); //does nothing
find_highlight.HitHighlight("pears"); //clears previous highlights, adds pears
As an alternative I could highlight the text by formatting the actual body of the e-mail, but this HitHighlight function seems to be more appropriate - if only I could figure out how to clear the markings when finished!
Any help would be appreciated.
Instead of calling find on the Word Range, call find on the document's content variable. Not sure exactly why, but this change causes the correct behaviour.
Change this:
var wordRange = outlookWordDocument.Application.Selection.Range;
Word.Find find_highlight = wordRange.Find;
To this:
Word.Find find_highlight = outlookWordDocument.Content.Find;
In Revit 2013 I have tool that I'm making that copies dimensions from one drafting view to another. I've got it to properly create a new version of a dimension including the Curve, DimensionType, and References but I'm having trouble with the properties Above, Below, Prefix, and Suffix. They copy just fine if at least one of them has a value. However, if none of them have a value then it will throw an AccessViolationException when I try to access them. I have tried to catch that exception but it bubbles up and it crashes Revit (I'm assuming it's caused by native code that fails).
How can I check to see if these properties have any value when I do my copying without triggering this AccessViolationException?
Autodesk Discussion Group Question
The DimensionData class is my own used for storing the dimension information so that it can be used to create the dimension in a separate document.
private IEnumerable<DimensionData> GetDimensionDataSet(Document document,
View view)
{
if (document == null)
throw new ArgumentNullException("document");
if (view == null)
throw new ArgumentNullException("view");
List<DimensionData> dimensionDataSet = new List<DimensionData>();
FilteredElementCollector dimensionCollector =
new FilteredElementCollector(document, view.Id);
dimensionCollector.OfClass(typeof(Dimension));
foreach (Dimension oldDimension in dimensionCollector)
{
Line oldDimensionLine = (Line)oldDimension.Curve;
string dimensionTypeName = oldDimension.DimensionType.Name;
List<ElementId> oldReferences = new List<ElementId>();
foreach (Reference oldReference in oldDimension.References)
oldReferences.Add(oldReference.ElementId);
DimensionData dimensionData;
try
{
string prefix = oldDimension.Prefix;
dimensionData = new DimensionData(oldDimensionLine,
oldReferences,
dimensionTypeName,
prefix,
oldDimension.Suffix,
oldDimension.Above,
oldDimension.Below);
}
catch (AccessViolationException)
{
dimensionData = new DimensionData(oldDimensionLine,
oldReferences, dimensionTypeName);
}
dimensionDataSet.Add(dimensionData);
}
return dimensionDataSet;
}
Regarding transactions: As far as I'm aware, you are only required to be inside a transaction when you are making any sort of CHANGE (modifications, deletions, additions). If all you are doing is collecting dimension information, you would not need a transaction, but when you use that information to create new dimensions in another document, that code would have to be inside a transaction. I have had a number of programs under development which did not yet modify the document but simply collected parameter settings and posted them to a TaskDialog.Show(). These programs worked fine, and I don't see anything in your code that actually modifies your model, so that doesn't seem to be your issue.
It seems like I bug.
Can you post the issue to the ADN Support?
The solution I can suggest is to use Parameters of the Dimension element instead of Dimension class properties.
For example, you can get Suffix and Prefix by following code
var suffixParameter =
oldDimension.get_Parameter(BuiltInParameter.SPOT_SLOPE_SUFFIX);
string suffix = null;
if (suffixParameter != null)
{
suffix = suffixParameter.AsString();
}
var prefixParameter =
oldDimension.get_Parameter(BuiltInParameter.SPOT_SLOPE_PREFIX);
string prefix = null;
if (prefixParameter != null)
{
prefix = prefixParameter.AsString();
}
Unfortunatelly, I don't tell you how to get Above and Below Properties via parameters, because I don't have a project to test. But you can easily determine parameters using BuiltInParameter Checker
Hope it helps.
This is a 2 parter.
First I am having a heck of a time getting the paste part of a copy and paste operation to work.
I have a method that copies information to the Clipboard, works perfectly.
private void CopyData(string format, object data, string text)
{
bool addedData = false;
DataObject copyData = new DataObject();
if (!string.IsNullOrEmpty(text))
{
copyData.SetData(DataFormats.Text, text);
addedData = true;
}
if (!string.IsNullOrEmpty(format) && data != null)
{
copyData.SetData(format, false, data);
addedData = true;
//this is only for testing
object obj = null;
if (copyData.GetDataPresent(format))
obj = (object)copyData.GetData(format);
}
if (addedData)
Clipboard.SetDataObject(copyData, true);
}
When I check that the data was added the object (obj) is not null.
However when I then go to paste the data from a different method using the same format key I get null, every time.
private void PasteFromClipboard()
{
object obj = null;
IDataObject paste = null;
if (Clipboard.GetDataObject().GetDataPresent("mydatatype"))
obj = (object)Clipboard.GetDataObject().GetData("mydatatype");
else
return;
if (obj == null)
throw new NullReferenceException("Could not gather information from the
}
I have tried everything that I can think of and it just doesn't make sense. I created an array of strings to capture all of the format keys the DataObject was holding and "mydatatype" was the first one. I have tried casting, not casting, using (Clipboard.GetDataObject().GetData("mydatatype") as object) and I just cannot figure it out. I know that there is data there because I can go to NotePad and paste the text that I copied along with the object.
Any thoughts as to why I would be able to get the data in one method, but not another?
Secondly I was wondering how I would go about making a Cut & Paste operation work between two of my windows. I am thinking about something like Excel, where if only the text is pasted the data will remain, however if the objects are pasted then the source will be deleted.
Thanks
Patrick.
Try pulling the data out as Text (instead of "mydatatype") - at least to confirm you can read from the clipboard. This is most likely what Notepad is reading. Also, does it matter that you're copying with "format" but pasting with "mydatatype"?
Could it be that text parameter always has a value and gets set. Then possibly the second if the one that would set the object does not get executed. Or if it does since the data was set in the first if statement the second set fails to set it correctly.
My recommendation would be to walk the code in the debugger during the copy operation.
Before the paste use GetDataObject().GetFormats() to enumerate the list of formatting codes.
Perhaps your using the wrong one.. just an idea
Try using reflection like this:
private static T TryGetClipboardData<T>(IDataObject clipboardData, string dataFormat)
{
System.Reflection.FieldInfo fieldInfo = clipboardData.GetType().GetField("innerData", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
var outerData = fieldInfo.GetValue(clipboardData);
if (outerData == null)
{
return default(T);
}
fieldInfo = outerData.GetType().GetField("innerData", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
var innerData = fieldInfo.GetValue(outerData);
if (innerData is System.Runtime.InteropServices.ComTypes.IDataObject)
{
// It is (probably) necessary to wrap COM IDataObject to Windows.Forms.IDataObject
System.Windows.Forms.DataObject wrappedDataObject = new System.Windows.Forms.DataObject(innerData);
var data = wrappedDataObject.GetData(dataFormat);
if (data is T)
{
return (T)data;
}
}
return default(T);
}
I suspect the COM object of your data in the Clipboard had a difficult time converting itself to the format you specified. I'm also playing safe with the input format string so that it is registered as a proper clipboard format.
HTH