Outlook Redemption : GetNamesFromIDs - vb.net

I'm trying to get all property names / values from an Outlook item. I have custom properties in addition to the default outlook item properties. I'm using redemption to get around the Outlook warnings but I'm having some problems with the GetNamesFromIDs method on a Redemption.RDOMail Item....
I'm using my redemption session to get the message and trying to use the message to get the names of all the properties.
Dim rMessage as Redemption.RDOMail = _RDOSession.GetMessageFromID(EntryID, getPublicStoreID())
Dim propertyList As Redemption.PropList = someMessage.GetPropList(Nothing)
For i As Integer = 1 To propertyList.Count + 1
Console.WriteLine(propertyList(i).ToString())
Console.WriteLine(someMessage.GetNamesFromIDs(________, propertyList(i)))
Next
I'm not totally sure what to pass in as the first parameter to getNamesFromIDs. The definition of GetNamesFromIDs is as follows:
GetNamesFromIDs(MAPIProp as Object, PropTag as Integer) As Redemption.NamedProperty
I'm not totally sure what should be passed in as the MAPIProp object. I don't see this property referenced in the documentation. http://www.dimastr.com/redemption/rdo/MAPIProp.htm#properties
Any help or insight would be greatly appreciated.

I think I figured it out. I have used VBA only, so you need to "think around" it's limitations, it will follow the same scheme in VB.NET.
The function signature is this:
Function GetNamesFromIDs(MAPIProp As Unknown, PropTag As Long) As NamedProperty
As the first parameter it requires an object which supports the IUnknown interface. Looking at the Redemption docs it became clear that there is an interface named _MAPIProp, from which many other RDO objects are derived (IRDOMail is among them). So this must be the very RDOMail you are trying to get data out of.
Knowing that, it needed only one other subtle hint from the docs to get it working:
Given a prop tag (>= 0x80000000),
returns the GUID and id of the named
property.
So property tag must be >= 0x80000000, this means it wont work for all properties, but just for the custom ones (I guess that is the distinction in this case, correct me if I'm wrong.) Passing in property tags not fulfilling this condition raises an error message (0x8000ffff "unexpected results").
Here is my code. It is VBA, so forgive me the Hex() blunder, as VBAs long integer overflows for numbers that big. I am sure you will get the picture.
Sub GetNamesFromIds()
Dim rSession As New Redemption.RDOSession
Dim rMessage As Redemption.RDOMail
Dim PropertyList As Redemption.PropList
Dim PropTag As Long
Dim EntryId As String
Dim i As Integer
rSession.MAPIOBJECT = Application.Session.MAPIOBJECT
' retrieve first random mail for this example '
EntryId = ActiveExplorer.CurrentFolder.Items(1).EntryId
Set rMessage = rSession.GetMessageFromID(EntryId)
Set PropertyList = rMessage.GetPropList(0)
For i = 1 To PropertyList.Count
PropTag = PropertyList(i)
If "0x" & Right("00000000" & Hex(PropTag), 8) > "0x80000000" Then
Debug.Print
If IsArray(rMessage.Fields(PropTag)) Then
Debug.Print Hex(PropTag), "(Array:", UBound(rMessage.Fields(PropTag)), "items)"
Else
Debug.Print Hex(PropTag), "(", rMessage.Fields(PropTag), ")"
End If
Debug.Print " GUID:", rMessage.GetNamesFromIds(rMessage, PropTag).GUID
Debug.Print " ID:", rMessage.GetNamesFromIds(rMessage, PropTag).ID
End If
Next
End Sub
First snippet from the output:
8041001E ( urn:content-classes:message )
GUID: {00020386-0000-0000-C000-000000000046}
ID: content-class

Well, for background info, the author suggests using something like OutlookSpy to see how Outlook stores the properties.
Looking at this exchange (make sure to read through all the follow-up responses), there isn't much more (in fact, I think at one point the Outlook MVP types GetNamesFromIDs when he means GetIDsFromNames).
What you might try is using GetIDsFromNames to see what that returns, and then use that to pass to GetNamesFromIDs.
I've used Redemption before, but not in this particular manner, so that's all I've got for you ...

Related

How to handle object declaration in VBA (Error 91)

I'm stuck in VBA and I couldn't find a good answer in the other questions related to error 91. I want to create an object and store variables and arrays inside that object. I tried an approach like I would do in js:
Dim block As Object
...
Set block = Nothing
block.Name = "Unbekannter Prüfblock"
block.Awf = "Unbekannter Anwendungsfall"
block.Conditions = Array()
block.Checks = Array()
I use the "Set block = Nothing" because I will use it multiple times in a loop.
But all I get is error 91 - Object variable not set
How can I set the object?
Do I really have to declare everything in vba?
Isn't there a "stop annoying me with declaration notices" toggle? ;-)
Update
Thank you all so much for the detailed answers!
As suggested I created a class for "block" and also a class for "condition" and "check". Block for example:
Option Explicit
Public name As String
Public awf As String
Public conditions As Collection
Public checks As Collection
Then I use it inside my code like this:
Dim bl As Block
Dim co As Condition
Dim ce As Check
Set bl = New Block
bl.name = ws.Range("B" & i).value
bl.awf = ws.Range("B" & i).value
Set co = New Condition
co.attr = ws.Range("B" & i).value
co.value = ws.Range("C" & i).value
bl.conditions.Add co
VBA isn't Javascript; objects and their members cannot be created inline, they need a class definition.
When you make a member call against an object, you refer to it by name, and whenever that name refers to a null reference (Nothing) you'll get error 91.
To fix it, you need to ensure every member call is made against a valid object reference. Using the Set keyword you can assign such a reference, and to create a new instance of an object you can use the New keyword followed by the name of the class that defines the type you want a new instance of:
Dim Block As Object
Block.Something = 42 ' Error 91
Set Block = New SomeClass ' set reference
Block.Something = 42 ' OK
Note that because the object is declared As Object, every member call is late-bound (resolved at run-time); if the member doesn't exist (or if there's a typo), you'll get error 438 at run-time.
You can move this error to compile-time with early binding by using a more specific data type for the declaration:
Dim Block As SomeClass
Because the members of SomeClass are known at compile-time, the IDE will now provide you with a member completion list when you type up a member call, and typos will no longer be valid at compile-time: strive to remain in the early-bound realm whenever possible! Note: As Variant (explicit or not) is also similarly late-bound.
So we add a new class module and call it SomeClass and we add a couple of public fields:
Option Explicit
Public Name As String
Public Case As String
Public Condition As Variant
Public Check As Variant
And now you can create and consume a new instance of that class, and add instances of it to a collection to process later (note: you can't do that with a UDT/Type).
The VBIDE settings have an annoying option ("automatic syntax check", IIRC) that immediately pops a message box whenever there's a compilation error on the current line; uncheck it (invalid lines will appear in red, without a message box), but do have the "require variable declaration" setting checked: it will add Option Explicit to every module, and that will spare you from a number of easily avoidable run-time errors, moving them to compile-time.
In JS, you can add properties (together with values) on the fly to an object. That's not possible in VBA (and most other languages).
Your declaration Dim block As Object is defining a variable that is supposed to point to an Object. But it isn't pointing to anything yet, per default it is initialized with Nothing, which is, literally, nothing, and has neither properties nor methods, it's just a placeholder to signal "I don't point to anything yet". Furthermore, Object cannot be instantiated.
in VBA, you assign something to an object variable with Set (this is different to most other languages). If you want to create a new object, you use the keyword New.
However, before you do that, you need to know what kind of object (which class) you need. This can be an existing class, eg a Worksheet or a Range in Excel, or it can be an own defined class (by creating a new class module in your code). In any case, you need to define the properties and the methods of that class. Considering the most simple class module Class1 (of course you should think about a more usefull name):
Option Explicit
Public name as String
Public case as String
(I will not start to talk about private members and getter and setter).
You then write
Dim block As Class1
Set block = New Class1
block.name = "Unbekannter Prüfblock"
(But block.data = "Hello world" will not be possible as data is not a member of your class.)
Big advantage of this attempt is that the compiler can show you when you for example mistyped a property name before you even start your code (Debug->Compile). In JS, you will get either a runtime error or a new property on the fly - hard to find nasty bugs.
If you later need a new (empty) object, just create a new object using New. If the old object is not referenced anywhere else, the VBA runtime will take care that the memory is freed (so no need to write Set block = Nothing).
In case you really don't know the properties in advance (eg when you read XML or JSON files or a key-value list from an Excel sheet...), you can consider to use a Collection or a Dictionary. Plenty of examples on SO and elsewhere.
One remark to block.Condition = Array(). Arrays in VBA are not really dynamic, you cannot add or remove entries easily during runtime. In VBA, you have static and dynamic arrays:
Dim a(1 to 10) as String ' Static array with 10 members (at compile time)
Dim b() as String ' Dynamic array.
However, for dynamic members you need to define how many members you need before you write something into it, you use the ReDim statement for that. Useful if you can calculate the number of members in advance:
Redim b(1 to maxNames) ' Dynamic array, now with maxNames members (whatever maxNames is)
You can change the array size of a dynamic array with Redim Preserve, but that should be an exception (bad programming style, inperformant). Without Preserve, you will get a new array, but the former data is lost.
Redim Preserve b(1 to maxNames+10) ' Now with 10 more members.
If you really don't know the number of members and it can change often during runtime, again a Collection or a Dictionary can be the better alternative. Note that for example a Dictionary can itself a Dictionary as value, which allows to define Tree structures.
Regarding your issue adding to the collection:
You need to add this code to your class module "Block" - only then you can add objects to the collections
Private Sub Class_Initialize()
Set conditions = New Collection
set checks = new Collection
End Sub

VBA How to programmatically set objects name?

In FactoryTalk View SE I’m trying to set an objects name in VBA based on another value.
This works:
Dim PumpSP As Object
Set PumpSP = ThisDisplay.PumpSetpoint1
PumpSP.Vaule = 10
This doesn’t work:
Dim PumpSP As Object
Set PumpSP = "ThisDisplay.PumpSetpoint" & "1"
PumpSP.Vaule = 10
How do I get it to take a concatenated string?
Thanks.
How do I get it to take a concatenated string?
You don't, and you can't. PumpSP is an Object, not a String. The only thing you can ever Set it to, is an object reference.
This is very similar to, say, how you would access the controls on a UserForm:
Set box = Me.TextBox1
The reason you can get that object with a string, is because a form has a Controls collection that lets you provide a string, works out which control has that name, and returns the Control object for it:
Set box = Me.Controls("TextBox" & i)
So in your case to go from this:
Set PumpSP = ThisDisplay.PumpSetpoint1
I don't know what ThisDisplay is, but maybe it has a similar collection:
Set PumpSP = ThisDisplay.Controls("PimpSetpoint" & i)
If there's no collection that lets you retrieve items by name with a string literal, you're out of luck.
That is the hard part as we have paid support with Rockwell but they will not help with any VBA questions. Ok fine but there VBA commands and class are not like 90% of the Excel or MS Office VBA you find online. Anyway I was able to find the correct class member.
So this work for me:
Set PumpSP = ThisDisplay.FindElement("PumpSetpoint" & “1”) Thanks for all the help.

GetType does not exist a Reflection puzzle in VB

This is a real basic schoolboy level error but I've not figured it out yet despite having used a fair amount of reflection in my generic updater class.
The puzzle is as follows: I want to get the type of a value that I've extracted in the code sample. Using that type I wish to compare it with another extracted value. To compare it correctly with Option Strict On I must directCast the extracted value to the type that the PropertyInfo says it is.
The code below simply illustrates my using it in Directcast or Ctype versions.
For Each p As PropertyInfo In _validProperties
Dim NewValue = p.GetValue(UpdateItem)
Dim x = CType(NewValue, p.PropertyType) 'Error p.PropertyType does not exist --How?
'Elided
Next
'Alternative approach
For Each p As PropertyInfo In _validProperties
Dim NewValue = p.GetValue(UpdateItem)
Dim x = DirectCast(NewValue, p.PropertyType) 'Error p.PropertyType does not exist --How?
'Elided
Next
I've tried explicitly putting the newly discovered type in it's own variable and it does exist.
This is an odd little trap I've had a few times and usually solved but this one is not budging. Therefore I'm fundementally misunderstanding how I need to use both the instance x.Gettype (OR GetType(Class) syntaxes).
Whatever I do, p.PropertyType does not exist.
Can anyone clear up my foggy mind on this please?
Edit: For now, Option Strict is off merely to allow this to work.
For Each p In _validProperties
Dim NewValue = p.GetValue(UpdateItem), OriginalValue = p.GetValue(originalItem)
If (p.PropertyType.Name.Contains("Nullable") AndAlso Not Nullable.Equals(NewValue, OriginalValue)) _
OrElse NewValue <> OriginalValue Then p.SetValue(originalItem, NewValue)
Next
All my tests pass so I'll live with it for now. I simply don't want the update to fire if there is no difference between the values on the properties in question.

VB6 map string to integer for headers

I'm trying to parse a CSV File into a VB6 application in order to update multiple records on a table on SQL with existing single record updating code already in the form. The CSV Files will have a header row whixh can be used to validate the information going into the correct place in the ADODB recordset. In C++ you can use a map to say like
map<String s, int x> column
column<"First Name", -1>
column<"Last Name",-1>
Then create a counter across the comma delimited values where if the third value is Last Name then the code could be written to change
column<"Last Name",-1> to column<"Last Name",3> and if x != -1 in any of the maps the file is valid for use, I would then loop through the remaining records and parse into a container using something similar to
strLastName = Array<column[3]>
to assign the record values to the correct variables. I am still very new to VB6, how can I accomplish something similar in VB6 and what containers should be used? So far I have
Public Sub GetImportValues()
On Error GoTo GetImportValues_Error:
Dim intFileNum As Integer
Open Path For Input As #intFileNum
Do Until EOF(intFileNum)
Line Input #intFileNum, vbCrLf
FunctionThatSavesInformationToSQL
Loop
Close #intFileNum
GetImportValues_Exit:
Exit Sub
GetImportValues_Error:
Err.Source = "frmMemberAdd.GetImportValues" & " | " & Err.Source
Err.Raise Err.Number, Err.Source, Err.Description
End Sub
with a dialog box returning the path as a string using App.path in a separate Function
*****************************************************Slight change to answer
The collection was on track for what I had asked but I did have to change it to dictionary because you cannot return items on a collection which kept me from comparing the items and changing the keys but dictionary can. Make sure if you use dictionary you switch the item and key.
If I understand your question correctly, you're trying to create a map (Dictionary<string, int> in C#). In VB6, you can use Collection for this purpose - it's roughly equivalent to C#'s Dictionary<string, object>. It uses String keys and stores all values as Variant. For example:
Dim oColl As Collection
Set oColl = New Collection
oColl.Add -1, "ColumnName"
Dim nColumnIndex As Long
'Get column index for column name.
nColumnIndex = oColl.Item("ColumnName")
If nColumnIndex = -1 Then
nColumnIndex = ...
'When you want to update a column index in the collection, you
'first have to remove the item and then add it back with the right
'index.
oColl.Remove "ColumnName"
oColl.Add nColumnIndex, "ColumnName"
End If
Edit 1:
One word of warning regarding VB6: you'll see many samples doing this:
Dim oObj As New SomeClass
It's ok to do this in VB.Net but don't ever do this in VB6. Declare and instantiate the object on separate statements because the single-statement form generates code where oObj is checked for Nothing and set to an instance before each use. This slows down your code (unnecessary checks) and creates hard-to-find bugs if you're using an instance that's supposed to be gone.
Always do this instead:
Dim oObj As SomeClass
Set oObj = New SomeClass
...
'Clean up the object when you're done with it. Remember, there's
'no garbage collection in COM / VB6, you have to manage object
'lifetimes.
Set oObj = Nothing
Also, use Long instead of Integer as much as you can - Long is a 32-bit integer, while Integer is only 16-bits. VB6 type names can be misleading frequently. Here's an old answer of mine with a bit more detail (not strictly related to your question but useful).
Alternatively, you can create a simplified wrapper around the .NET Dictionary class and expose it as a COM object: this would allow you to call it from VB6. This would likely be (somewhat) slower than Collection and it'd require the .NET Framework for your VB6 project to run.
Edit 2:
As #CMaster commented, Dictionary is available from the Microsoft Scripting Runtime library - you need to add a reference to it to use it (this is why I prefer Collection - it has no dependency). This answer has details about how to use it.

Reading and changing fields in SAP with RFC via VB .NET

I'm currently trying to figure out the basics of remote function calls via vb .NET. I feel pretty helpless however, because their just isn't any useful documentation for the most simple of tasks.
What I'm trying to do atm is starting the transaction CO13, write the confirmation number in the appropriate field and cancel the order. Even this simple tasks turned out to be a pain in the ass. I'm still not sure how to access and modify the contents of a specific field. There are some examples with tables for excel in the net, but hat's about it. What I have so far is this (login is working and in another function):
Public Function stornieren() As Boolean
Dim ordernr As String
Dim confirmationnr
Dim confirmation As Object
Dim R3 As Object
Dim CO13 As Object
Dim result
R3 = CreateObject("SAP.Functions")
ordernr = TextBox3.Text
confirmationnr = TextBox4.Text
CO13 = R3.Add("RFC_CALL_TRANSACTION_USING")
CO13.exports("TCODE") = "CO13"
CO13.exports("MODE") = "S"
confirmation = CO13.exports("RUECK")
confirmation.value = confirmationnr
result = CO13.call
End Function
RUECK is the Field Name. I want to write the value of "confirmationnr" into the field RUECK. "confirmation.value = confirmationnr" throws the error message "the object variable could not be determined" and "NullReferenceException" was not handled. Sounds to me like the object is empty.
Thanks in advance.
EDIT: Now trying via BAPIs and particularly BAPI_PRODORDCONF_CANCEL. I have no idea about the syntax though. Any help would be appreciated.