Part 3: Syntax And Setting Issues
Let's Talk About Strings
If you've worked with the classic API extensively you'll be aware that it makes extensive use of fixed length strings... which do not exist, as such, in .Net.
There are a few ways around this. One is apparently to use character arrays. I haven't tried this and would be a bit hesitant to do it given that the TM1 API is hardly bulletproof when it comes to error handling. (As noted earlier, the API's preferred method of handling invalid arguments in an Excel TM1 API application is to send Excel plummeting into a flaming pile of wreckage onto your desktop.)
In the earlier days of my work with the API in .Net I went with another option, one which I consider is most likely the safest one. That's to make use of the VB6 Compatibility library (the full name of which is
Microsoft.VisualBasic.Compatibility). This library has a
VB6.FixedLengthString method in one of its classes. To add the library to your project either:
- Double click on the My Project node in Solution Explorer which will open the project's properties. Go to the References tab (from the tabs on the left hand side of the dialog) and scroll down if necessary until you see the [Add...] button. Click that to bring up the Add Reference form, then and go to the .Net tab, sort the libraries in name order and find Microsoft.VisualBasic.Compatibility. Double click on it to add it to the project. OR
- Right click on the project name in Solution Explorer and choose "Add Reference..." which will take you straight to the Add Reference dialog that I referred to above.
There is of course no such thing as a free lunch, and the cost of this one is this; the Compatibility library, unlike the standard Visual Basic library, is not (I understand) considered to be one of the core libraries of the .Net Framework. A number of experienced and respected VB.Net gurus and authors discourage its use on the basis that it was primarily intended to support code which was generated by the VB6 upgrade wizard and that the library may be pulled by MS in the future, just as they pulled the wizard itself in Visual Studio 2010. And that is true enough. However given my experience of the TM1 API being damn fussy, I (initially at least) preferred to take the risk of that than try to make (say) character arrays work, much less attempting to check that they will work under
all circumstances. Aside from which MS is certainly not going to retrospectively pull the library from the .Net 3.5 (or 4.0) Framework, even if the time comes when they don't include it in (say) the version 6 or version 7 Framework; something which is pure speculation anyway. If I revisit this topic in 5 years I may have a different opinion but for now I'd say that if you want the safest way you should probably use what is known to work, and what is closest to the legacy code that the API was designed for.
Let's look at these strings in action.
I have a number of standard modules that I use for .Net API projects. These typically contain functions which wrap around common tasks, such as obtaining an object's name from a handle to it. The specific function that I'll discuss below is normally used when I iterate through an array of objects (cubes, dims, etc) using the array's index, and obtain a handle to the object from
TM1ObjectListHandleByIndexGet. That handle is passed to my function as one of its arguments along with the corresponding user handle and pool handle.
We need some variables to begin with. The first will store a handle to the object's name property:
Stuff to note about this declaration:
- Microsoft may have dumped Hungarian notation, but I still use a version of it because I prefer to be able to tell at a glance what a variable is supposed to contain. If you happen to feel that this is some kind of archaic, reactionary, behind the curve, atavistic policy you are again invited to bite me. Most of my Hungarian style tags are followed by an underscore; s_ for string, i_ for integer, is_ for short integer and so on. I don't use the underscores on TM1 API variables simply because again that tells me at a glance that it is indeed a TM1 API-related variable. h is used as a handle to an object or a user handle. vh is used for a handle to a value. s, i, is etc are used without an underscore for the raw values that are returned from an API function, and which may need additional processing. (The other advantage of using h and vh is that a lot of the code in the manual uses those prefixes, which minimises the amount of modification needed when I copy and paste from the documentation.)
- Remember that the change of long integer length in .Net from 32 bit (in VB6) to 64 bit requires that the value handle be declared as an Integer rather than a Long.
- Note that the variable is initialised with a value of 0 at the same time as it's declared. The ability to do this is a vast improvement over classic VB6 but there is a HUGE gotcha that you have to be aware of here. One concept that you need to wrap your head around in .Net (rapidly) is that variables are also objects. And objects can be Null. In VB6 a declaration of such a variable would cause it to be created with the default value of 0. But in VB.Net, it's Null until you assign it a value. Consequently if you don't initialise the variable as I've done here then you can, if things don't work out properly, run smack bang into a Null exception runtime error. Consequently you should always initialise when you declare. (At least for "normal" variables like strings and integers.)
Next, we need a variable to hold the name when it's returned from TM1ValStringGet_VB. This of course is supposed to be a fixed length string so as per what I've said above I declare it as:
Code: Select all
Dim sObjectName As New VB6.FixedLengthString(gISC_TM1STRING_SIZE_DEFT)
Things to note about this one:
- Remember that you'll get a syntax error if you haven't added a reference to the Compatibility library. You should also have
Code: Select all
Imports Microsoft.VisualBasic.Compatibility
At the head of your file or have specified it as a globally imported namespace in the lower part of the References tab of your project's properties;
- gISC_TM1STRING_SIZE_DEFT is a short integer constant that I have declared earlier in the module. It's defined as 255 because it's pointless fiddling around trying to anticipate what length to use. Really, who cares? Set it for the maximum size that an object name can be and you never have to worry about it. (Yes, technically the manual states that the maximum length is only 128 characters. The manual has lied to me so often, and is so incomplete and frequently flat out wrong that I'd rather trust my instincts and allow the extra space.)
The last variable that I use is:
From the underscore you can see that this is one of my standard string variables. I use it to store a trimmed version of the fixed width string above.
Note again that I've initialised it with an empty string to prevent null errors.
Next, we need to pack the VB6 Fixed Length string with null characters, just as we had to (or generally did) in VBA/VB6. Note that the fixed length string is an object but it doesn't have a default property the way some VB6/VBA objects had. We must therefore initialise the correct
property of the object, which is the .Value property:
Code: Select all
sObjectName.Value = New String(Chr(0), gISC_TM1STRING_SIZE_DEFT)
Hard core VB6/VBA-ers may recall that the Chr() function was less desirable than the Chr$() function since the latter returned a string data type, and the former returned a Variant in string format which involved a couple of extra microseconds of conversion work under the hood. Micro-optimisers would therefore always go for the chr$() type, but it doesn't exist in VB.Net. (Nor do Variants, of course.) Chr() does and it's safe to use indefinitely since it's in the Microsoft.VisualBasic.Strings library, not the Compatibility one. The statement above obviously creates a new string consisting of 255 null characters and assigns it to the Value property of my VB6 Fixed length String object.
The next line of code obtains the handle to the name property; there is nothing new here, this is just vanilla Tm1 API code. hPool is a handle to a value pool that was passed to the function as one of its arguments. Mine is stored as a property of a custom TM1 Connection object and that property is passed to my function, yours may be a Public variable; it really doesn't matter, as long as it's valid. hObjectHandle is a handle to the object that I'm obtaining the name for, which has also been passed to my procedure as an argument:
Code: Select all
vhObjectName = TM1ObjectPropertyGet(hPool, hObjectHandle, TM1ObjectName())
Next, I call a function which checks the returned value handle and makes sure that it contains a string and not an error. I'll omit that line here since you obviously won't have that function to call, but I do recommend that you write your own code to make the same check.
After that, we extract the string from the handle to the ObjectName property using the standard function:
Code: Select all
TM1ValStringGet_VB(hUser, vhObjectName, sObjectName.Value, gISC_TM1STRING_SIZE_DEFT)
The first argument is a user handle which has been passed to the function. The second is the value capsule handle that we obtained from the TM1ObjectPropertyGet function. The third is our VB6 fixed length string's .Value property (which will receive the extracted value), and the last argument is my custom constant giving the value of the size of that string, which is of course 255.
Finally, I call another custom function to get rid of the excess null characters:
Code: Select all
s_ObjectName = TrimNull(sObjectName.Value)
TrimNull is another function that you won't have since it's a custom one in one of my classes. However all it does is look for the first null character in the fixed length string, and return any characters to the left of that. (Or an empty string if there aren't any.) I assign the return value from that function that to my standard string variable.
Next, that value is returned to the calling procedure. In VB6/VBA this would be done through assigning the value to the function name and while that still does work in VB.Net (you'll see an example of where I still use it later) the more conventional .Net way is to use the Return statement:
Yes, I could omit the use of the s_ObjectName variable and return directly from the TrimNull function but depending on how edgy I'm feeling at the time I don't usually like returning a value directly from another function. On the off chance that anything goes wrong in the function call I'd rather that the error in the function call be separate and distinct from the value assignment. Again, if you think that's being then over-cautious then let the biting begin.
Regardless of what I have said above, there is also a third option that I experimented with and which seems to work and which, to be honest, I've been using more commonly in my more recent code.
Another of my standard functions (the one which checks the data type of a returned value handle, actually) uses the TM1ValErrorString_VB function to extract any error string that may be embedded in the value capsule. As with TM1ValStringGet_VB, the API documentation claims that the argument that receives the value needs to be a fixed length string. Mind you, it looks like Iboglix just copied and pasted from the TM1ValStringGet_VBdocumentation into the TM1ValErrorString_VB documentation, as a result of which they nominate the wrong function. However the principle still applies:
Returns the string Str padded with blanks. It must be declared with a fixed maximum length. The length should match the value of Max. For example:
Code: Select all
Dim Str as String * 75
TM1ValStringGet_VB( hUser, vValue, Str, 75)
In my function I decided to use a standard string variable and simply pre-populate it with the specified number of null characters, like so:
Code: Select all
Dim sTM1ValErrorString_VBReturned As String = New String(Chr(0), gISC_TM1STRING_SIZE_DEFT)
Thus far I've had no issues with this variable being used directly in the TM1ValErrorString_VB function:
Code: Select all
TM1ValErrorString_VB(UserHandle, CapsuleHandle, sTM1ValErrorString_VBReturned, gISC_TM1STRING_SIZE_DEFT)
Or, indeed, with the TM1ValStringGet_VB function either. But again, if you want to do that... test, test, and test again because you're still using a data type that is different to the one that is specified by the documentation, so there is some risk involved.
One last trap that is dangerously easy to fall into; remember that while TM1 is usually case insensitive, VB.Net (like VB6 before it for that matter) is not. Thus the expression:
Code: Select all
? "Chores And Processes"="Chores and Processes"
will return FALSE. (Note the lower case "a" in the second "and".) If you want a case insensitive comparison, convert both expressions to upper or lower case first.
Error Handling
This isn't intended to be a tutorial on VB.Net so I won't be covering this in any great detail, but I'll mention it in passing.
Those who have read some of my other posts will know that IMHO anyone who does not use error handling needs a good shootin'. An unhandled, environment-crashing, cryptic error blasting out into some poor hapless end user's face is the epitome of bad programming.
Traditional VB6 error handling still works in .Net, which is to say that you can:
- Put a label in your procedure (or in your top level procedure at least), just below an Exit Sub or Exit Function statement;
- Put a block of error handling code into the procedure below that label;
- Put an On Error Goto statement at the top of the procedure which will redirect the code to the error handling block in the event of a runtime error.
You can raise a custom error through the .Raise method of the Err object.
In .Net an alternative method called Try / Catch blocks is used. I didn't take to this method initially but have started to warm to it. These blocks take the form:
Code: Select all
Try
Block of code
Catch
Error Handling Code
Finally
Optional block of (typically clean up) code to execute regardless of whether the block of code errors or not
End Try
They can get more sophisticated than this; for example you can nest Try / Catch blocks.
You can use On Error Goto code in some procedures and Try / Catch ones in others within the one project or module, but you can't mix and match within one procedure.
The other big difference is that rather than the error codes / descriptions that we had in VB6/VBA and which could be generated using the .Raise method of the rather basic Err object, .Net uses Exceptions. This isn't just a change in terminology; Exceptions are fully formed objects, capable of having distinctive properties which go beyond mere description text. (This includes embedding other exceptions within an exception to allow a trail of errors to be built up, though that's seldom necessary.) There is a hierarchy of exception objects, and you can also build your own if you want. You can also have multiple Catch statements, each intended to catch a specific type of exception, as long as you go from the more specific ones to the more general ones.
As with VB6/VBA legacy errors, if you don't have a Catch block within a procedure then the exception will flow back up the call stack. To ensure that the user doesn't get an unhandled error slamming into their face you therefore only really need a Try Catch block in the procedure at the top of the stack.
To give an example, let's look at a block of code from my procedure which checks the data type of a Value Capsule. The user handle (hUser) is passed as an argument to the function.
gSC_TM1APIERR_HUSER_INVALID_30001 was a string constant which contained a description of the error:
Code: Select all
If hUser = 0 Then
'Old Style (Commented Out)
'Err.Raise(vbObjectError + 30001, , gSC_TM1APIERR_HUSER_INVALID_30001)
'New Style
Throw New ArgumentException("The user handle that has been passed is invalid. " &
"This is needed to check the type of a value capsule. " &
"Please ensure that you are connected to the TM1 server and that your Windows Path environment variable (Computer -> Properties -> Advanced System Settings) includes the path to the lib folder of your TM1 client software.")
End If
Things to note about this:
- Although this point belongs in the previous section it's worth noting that by putting the ampersand (&) at the end of each line you no longer need to use the VB line continuation character (that is, the underscore). The underscore will still work, you just don't need it. I always preferred to put the ampersands at the front of a string continuation line to emphasise the fact that it's a continued line, but unfortunately you don't have that option unless you want to stick with having underscores at the end of the line.
- Blessed be VB.Net, for we finally have arguments that you can pass when you create a new object. In VB6/VBA you have the Class_Initialize() procedure but you can't pass any arguments to it which led to all manner of ridiculous and unsafe workarounds to populate some of a class object's properties when a new object was created. Classes in VB.Net have real constructor procedures which allow you to pass arguments to them.
- The ArgumentException object (and indeed other Exception family objects) is no different. The constructor function is overloaded meaning that you have a range of different arguments that you can pass, but for this specific Exception class it comes down to three arguments in varying combinations:
- Message, a description of the error;
- ParamName, which allows you to pass back the parameter that had the invalid value; and
- InnerException, which allows you to pass back any existing Exception object as a property of your own one.
In my case I've used the description only. Other Exception objects may have different sets of arguments, appropriate to the errors that they're describing.
Option Explicit And Option Strict
All VB6 / VBA coders should already be familiar with
Option Explicit. It's the declaration which is made at the head of a module requiring you to declare any variables before using them so that you don't mistype the variable name
MyStr as
MySr and then have to spend two hours figuring out why your code gives unexpected results. Anyone who writes code for me without using Option Explicit also needs a good shootin', ideally simultaneously with anyone who doesn't use error handling. The only difference in VB.Net is that it's declared outside of the module block.
Option Strict is new to VB.Net. In theory it will generate compile errors if you try to assign a value in a way which results in a narrowing of the value; for example, if you assign a long integer value to a standard integer variable. Even if (at run time) the Long contained a value which is small enough to be stored in an Integer, you'd still get a compile error since obviously there is no way of the compiler validating what the Long might or might not contain when the program is run 6 months or a year from now.
In VB6 Option Explicit had to be stated at the head of the module though there was a VBE option which allowed the statement to be automatically added to any new modules. (Again, work for me, don't use it, shootin' time.)
In .Net You can still add Option Explicit and now Option Strict to the head of your files (before any Module or Class declaration) but there is also another way; if you double click on the
My Project node to open the project properties and select the
Compile tab you can set either or both to apply to your entire project. These settings won't add the keywords to the head of your files, but the whole project will behave as if they were there.
My recommendation is that you have Option Explicit on but think a little bit about what you want to do with Option Strict. The reason is that some TM1 API functions are inconsistent in the value types returned, and Option Strict can rapidly become a pain in your backside.
To give an example (and this one is down to inconsistency on Iboglix's part), the
TM1ValType() function returns a Short (formerly Integer). However the functions that return the data type values, and which you have to compare TM1ValType()'s return value to (like
TM1ValTypeIndex) return an Integer (formerly a Long).
In the procedure that I use to check a value capsule's type (called, poetically enough,
CheckValueCapsuleType) I extract the value type by assigning the return value from TM1ValType() to a variable named
is_ValTypeReturned. I then check it against the various API functions to determine what type it is. I also have a Public enumeration called
TM1API_ValTypes. (In .Net you access each specific enumeration value via the dot operator, as you'll see in a moment.) I compare the value that is stored in is_ValTypeReturned against each TM1 API data type function using a Select Case block. Now if I do this:
Code: Select all
Select Case is_ValTypeReturned
Case TM1ValTypeReal()
CheckValueCapsuleType = TM1API_ValTypes.TM1VTReal
Case TM1ValTypeString()
CheckValueCapsuleType = TM1API_ValTypes.TM1VTString
Case TM1ValTypeIndex()
CheckValueCapsuleType = TM1API_ValTypes.TM1VTIndex
Case TM1ValTypeBool()
CheckValueCapsuleType = TM1API_ValTypes.TM1VTBool
{Etcetera}
then as soon as I turn on Option Strict,
boom! I get a lengthy list of "
Option Strict disallows implicit conversions from Integer to Short" messages in the errors display. No, I'm not
assigning the Int value to a Short, and I have no idea why the compiler can't just treat the Short as an Int for the purposes of comparison since it clearly can't contain a value larger than the ones that it's being compared to.
This type of pedantry on the part of Option Strict can cause your program to transform itself from a sleek, sweetly compiling piece of code to a wizened, wrinkled, black clad, foul-breathed shrew which keeps waving a metaphorical arthritic finger in your face and squawking "
Uh-uh, not allowed, uh-uh!!!" The options that you have to deal with this are as follows:
- Punch the monitor and yell "**** OFF!" at it. While this option is gratifying, surprisingly the other options are slightly more efficacious.
- Turn off Option Strict. You can do it for just in the file(s) which contain your TM1 API module(s) if you wish to (by setting it to Off in the heading of the file), and leave the global setting on for the rest of the project. This is perhaps a bit of a cheat but not a wholly invalid one because this isn't the only instance where Option Strict will throw up errors that really are complete bullsh..., er, which have no real world validity. Another random example is Registry.GetValue. This reads a value from the Registry into a variable. You can specify a default value so that if there is no such registry value, a value will still be returned. However with Option Strict on you still have to explicitly convert the return value of that function to the variable's data type otherwise "Option Strict On disallows implicit conversion from Object to {whatever}. The worst example of all is si_Idx += 1 Yes, I tell you truthfully that this will generate a compile error because the compiler will not recognise that the constant value 1 is just as valid as a short integer as it is as a standard integer. As far as it is concerned 1 can only be only a 32 bit integer and must therefore be explicitly converted to a 16 bit integer before you can use it in this increment statement. This kinda kills the mood of having that nice increment shortcut that we lacked in VB6. "Uh-uh, not allowed, uh-uh!!!" (As a digression there are two other code elements that you're going to become real close buddies with if you use Option Strict. The first is the .ToString method of most variable types, which allow you to convert (say) a numeric value to a string before displaying it. The other is the TryCast() function, which allows you to try to convert one type of object (as opposed to a simple variable) to another.) It's up to you whether you think that the safety net provided by Option Strict outweighs the irritating nitpicking that it sometimes (OK, often) pulls on you.
- The third is to convert the values returned by the various TM1ValType functions to Shorts using either the CShort method, or the more .Netish Convert.ToInt16() method from the System.Object library. However this can involve a substantial amount of typing. OR
- Finally, the shortest and easiest option if you elect to leave Option Strict on is...
Code: Select all
Select Case Convert.ToInt32(is_ValTypeReturned)
Case 0
CheckValueCapsuleType = TM1API_ValTypes.TM1VTUnknown
Case TM1ValTypeReal()
{Etcetera}
Upon doing any of these all of my compile errors immediately evaporated and I had my sweetly functioning code back.
XML Comments
Before we move on to connecting to the server, I want to mention something about commenting in VB.Net. You can of course still create conventional header blocks in your procedures, but you can also create XML comments.
I would have to say that I'm not generally a fan of XML. In a lot of cases it's just clouds of metadata bloat encasing a small amount of content which could more easily, and far more readably, be contained in plain text format. However it does have one big advantage here.
If you type three single quotes on the line immediately above a Module block, a procedure, or indeed a number of other code elements, an XML template will appear. (The nodes are the default ones, but you can customise them by adding additional ones.)

- 010_XML1.jpg (141.44 KiB) Viewed 26091 times
In the case of a procedure, you get nodes for a summary of the procedure, for each of the parameters / arguments that you pass to it, for the return value, and for any remarks that you have. When you fill in the nodes, then when you call the procedure you will have a rich Intellisense prompt that gives you the details of the procedure and its arguments, the summary of it, and as you move from one argument to another the description that you've added of what the argument is for right at the bottom.

- 011_XML1.jpg (92.37 KiB) Viewed 26091 times
Obviously this feature becomes exponentially more useful as your project becomes larger and the number of people who work on it increases.
It's as useless as chicken dung on a pump handle for producing human-friendly documentation in any kind of help file format, though, at least without some kind of external utility to lend it a hand.