Well that was an interesting surprise.
As I mentioned in a previous post, we are in the process of migrating from MoveX Apps 5.2 to M3 Apps 10.1 – though I may have made a point of dragging out the versions a little 😉
Credit where credit is due, Smart Office is worlds apart from MoveX Explorer and Workplace – truly a work of art. But, that’s not what this post is about…
In my world
In my world all sorts of odd things happen, code works, people can describe their queries in a sensible and accurate fashion, and APIs remain static.
Sometimes my world and everyone elses worlds are, well…worlds apart.
So, a number of years ago we had the opportunity to take a look at the APIs that were exposed by MoveX, quite a few and many providing useful features. Intentia even provided an API toolkit which included a very helpfuly program called MiTest and another quaint wee thing that generates code that you can use to wrap the APIs.
It is this later mentioned program (StructBuilder I think) which I thought was pretty nifty, sure the code it spat out was VB6, but it was pretty straight forward to migrate in to C# or VB.Net. So, out I go and write some software which makes calls to the APIs, and I take advantage of this tool.
Now for the uninitiated, the Intentia crew did something really quite smart when it came down to the APIs, your application would create a plain text string and send it to the MoveX Business Engine via the MvxSock.dll, and the MoveX Business engine would be kind enough to send back a plain string which you then needed to break down in to it’s appropriate fields.
So, an example is in MRS001MI, you send the string “LstTransactionsOIS100MI” which will go out and list all of the transactions that are available in the OIS100MI interface. MoveX returns the string “REP OIS100MI AddBatchAddressAdd batch order address 71 0 13.145V0T0S200009170948202008072354 12071 ” which then needs to be split in to specific fields – in this instance the “Add batch order address” is a specific field, the description of the Transaction which starts at 40 characters in to the returned string and is 60 characters in length. Each field starts at a specific location and has a specific length, information which is stored in the table CMIFLD or in MRS001.
The beauty in this method is that it is really quick and efficient to process and relatively easy to troubleshoot visually.
The Problem
In my world of bliss with the little wee application that Intentia provided which generated wrappers for the API calls all was great – until I tried a new version of MoveX. Lawson had decided to increase the length of several fields that OIS100MI used, that’s all well and good – until you realise that the code generated that wrapped the API calls is set to look at static locations. So, when we are looking for the Line Number (PONR) returned by AddBatchLine it is at position 16 and is now 5 characters in length rather than 3. This of-course means that the Line Suffix which follows the Line Number (POSX) is now 2 characters out – oh dear, how sad, never mind…
Basically it means that my code needs to be adjusted and then recompiled and then deployed, not so fun.
Confounded by my own Laziness
Now because of the existence of a program which did most of the heavy lifting I never really dug too deeply in to the Library/Program GENERAL or more importantly the Library/Program MRS001MI. It would appear that the Intentia developers had thought recognised that there would be a need to change field lengths and to this end they had an interface specifically designed to provide information about the API field positions (and even a table!).
So really, if you want to be correct and want your code to work in future versions you should at the very least call MRS001MI.LstFields to get the In and Out field positions for the API that you want to use. I guess if you really want to be sure then you should also check that the API (program and transaction) still exists and is at a usable status – again with transactions in MRS001MI.
My Solution
In order to get things moving again, I have written a series of classes for each of the Transactions, they inherit much functionality from a base class which stores a List of field definitions for that specific transaction. The class also allows us to set and retrieve values against a specific field name. The list of fields and their positions get populated on the fly from MRS001MI.
My code has been adjusted so it will use the Field name that is present in the data returned by MRS001MI.ListFields so in theory, as long as mandatory fields don’t change then my code will stand a very good chance of just working; users will be able to flick backwards and forwards between versions when testing without needing separately compiled programs.
Because I am a big fan of LINQ and .Net3.5, the code that I have created is well, LINQ and .Net3.5 based – but if you aren’t using .Net then you should be able to adapt it to suit your purposes. It’s in VB though I will be looking at doing it in C# in the near future and clean it all up too!
To Start with my class mvxAPIField will store information about the field returned by MRS001MI, it will also have an additional field which we can use to store a value.
Public Class mvxAPIField ''' Is this In (I) or Out (O) Public Property Direction As String ''' the value that we will be submitting Public Property Value As String ''' the name of the field this is associated with Public Property FieldName As String ''' the from position Public Property From As String ''' length of the field Public Property Length As String ''' The version that this pertains to Public Property Version As String ''' this is the constructor – to make things easy we can populate the base information from here ''' astrDirection is the direction (I = In, O = Out - ie. In is us submitting to MoveX, O is the result from MoveX) ''' astrFieldName is the name of the field, eg. PONR ''' astrFrom is the position that the field starts at ''' astrLength is the length of the field ''' astrVersion is the version that we are using Sub New(ByVal astrDirection As String, ByVal astrFieldName As String, ByVal astrFrom As String, ByVal astrLength As String, ByVal astrVersion As String) Direction = astrDirection FieldName = astrFieldName From = astrFrom Length = astrLength Version = astrVersion End Sub ''' return the length as a int Public Function LengthAsInt() As Int32 Dim result As Int32 = -1 If Length <> Nothing Then If IsNumeric(Length) = True Then result = CInt(Length) End If End If LengthAsInt = result End Function ''' return the From as an int Public Function FromAsInt() As Int32 Dim result As Int32 = -1 If From <> Nothing Then If IsNumeric(From) = True Then result = CInt(From) End If End If FromAsInt = result End Function End Class
Then we have our actual helper class – it groups all of fields together and allows us to generate the string to submit to the API and functions that allow us to decode the results.
Public Class BaseAPIMI
' this is where we will store the fields themselves
Dim lsFields As List(Of mvxAPIField)
' the program name eg. OIS100MI
Public Property ProgramName As String
' the name of the transaction eg. AddBatchHead
Public Property TransactionName As String
' the version that we are playing with
Public Property Version As String = Nothing
''' This is the largest position that we have found (determines the size of the Space() we do)
Dim giMaxPosition As Int32 = 0
''' Create a new Input String to send to MoveX based on the values stored within the lsFields list
Public Function CreateInString(ByVal astrVersion As String) As String
CreateInString = Nothing
' create a big string filled with spaces
Dim strPopulatedString = Space(giMaxPosition)
' set the transaction name
Mid(strPopulatedString, 1) = TransactionName
' retrieve all of the fields for this version that are In fields (ie. fields that we will send to MoveX)
Dim vFields = From field In lsFields Where field.Version = astrVersion And field.Direction = "I" Select field
If Not vFields Is Nothing Then
If Enumerable.Count(vFields) >= 1 Then
For Each apifCurrentField As mvxAPIField In vFields
If Not apifCurrentField Is Nothing Then
Dim iFrom As Int32 = apifCurrentField.FromAsInt()
Dim iLength As Int32 = apifCurrentField.LengthAsInt()
' retrieve the value of the field at a specific location and for a specific length
Mid(strPopulatedString, iFrom, iLength) = apifCurrentField.Value
End If
Next
End If
End If
CreateInString = strPopulatedString
End Function
''' Get a value from a result for a specific fieldname
Public Function GetOutValue(ByVal astrFieldName As String, ByVal astrVersion As String) As String
GetOutValue = Nothing
Dim apifField As mvxAPIField = GetOutField(astrFieldName, astrVersion)
If Not apifField Is Nothing Then
GetOutValue = apifField.Value
End If
End Function
' this function will look for a specific fieldname and retrieve the mvxAPIField associated with it
Public Function GetOutField(ByVal astrFieldName As String, ByVal astrVersion As String) As mvxAPIField
GetOutField = Nothing
If astrFieldName <> Nothing Then
If Not lsFields Is Nothing Then
Dim vField = From field In lsFields Where field.FieldName = astrFieldName And field.Version = astrVersion Select field
If Not vField Is Nothing Then
If Enumerable.Count(vField) >= 1 Then
GetOutField = vField(0)
End If
End If
End If
End If
End Function
''' Set the value for a specific INPUT field
' when we want to create a string we should use this sub to
' set the values - it will look through our fields looking for
' the appropriate field to set
Public Sub SetInputValue(ByVal astrFieldName As String, ByVal astrValue As String)
If astrFieldName <> Nothing Then
If Not lsFields Is Nothing Then
Dim vField = From field In lsFields Where field.FieldName = astrFieldName Select field
If Not vField Is Nothing Then
If Enumerable.Count(vField) >= 1 Then
For Each apifCurrentField As mvxAPIField In vField
If Not apifCurrentField Is Nothing Then
apifCurrentField.Value = astrValue
End If
Next
End If
End If
End If
End If
End Sub
''' interpret the result string
' basically loop through the fields that we have and
' extract the data at each field position from the astrResult
' string and store it in the Value
Public Sub SetResult(ByVal astrResult As String)
If Not lsFields Is Nothing Then
If astrResult <> Nothing Then
For Each apifCurrentField As mvxAPIField In lsFields
If Not apifCurrentField Is Nothing Then
If astrResult.Length >= (apifCurrentField.FromAsInt() + apifCurrentField.LengthAsInt()) Then
Dim strTemporary As String = astrResult.Substring(apifCurrentField.FromAsInt() - 1, apifCurrentField.LengthAsInt())
If strTemporary <> Nothing Then
apifCurrentField.Value = strTemporary.Trim()
End If
End If
End If
Next
End If
End If
End Sub
' the constructors requires a list of the fields that were retrieved from MRS001MI
' and then populated in to a list of mvxAPIField objects
' it probably would have been easier to just extract the full results from MRS001MI
' directly in to mvxAPIField but I was pressed for time
Public Sub New(ByVal alsAPIPositions As List(Of mvxAPIField), ByVal astrVersion As String, Optional ByVal astrResult As String = Nothing)
Version = astrVersion
lsFields = alsAPIPositions
' we want to do a count up to determine the maximum size of our string
If Not lsFields Is Nothing Then
For Each apifCurrentField As mvxAPIField In lsFields
If Not apifCurrentField Is Nothing Then
Dim iFrom As Int32 = apifCurrentField.FromAsInt()
Dim iLength As Int32 = apifCurrentField.LengthAsInt()
If iFrom <> -1 Then
If iLength <> -1 Then
If (iFrom + iLength) > giMaxPosition Then
giMaxPosition = iFrom + iLength
End If
End If
End If
End If
Next
End If
If astrResult <> Nothing Then
SetResult(astrResult)
End If
End Sub
End Class
And finally, overtop of that we have the class which stores the field names – making the code a little bit more readable and clear.
Public Class OIS100_Confirm
Inherits BaseAPIMI
Dim lsFields As List(Of mvxAPIField)
Public Sub New(ByVal alsAPIPositions As List(Of mvxAPIField), ByVal astrVersion As String, Optional ByVal astrResult As String = Nothing)
MyBase.New(alsAPIPositions, astrVersion, astrResult)
ProgramName = "OIS100MI"
TransactionName = "Confirm"
End Sub
' I have done this to help make my code more readable (CONO just
' doesn't mean that much to people outside of MoveX)
Public Sub SetCompany(ByVal astrCompany As String)
SetInputValue("CONO", astrCompany)
End Sub
' if we are sending this to MoveX, then we should try to
' set the Order Number
Public Sub SetOrderNumber(ByVal astrOrderNumber As String)
SetInputValue("ORNO", astrOrderNumber)
End Sub
' if we are initialised with a result then this will go out and try to extract
' the Order Number (ORNO)
Public Function GetOrderNumber() As String
GetOrderNumber = GetOutValue("ORNO", Version)
End Function
End Class
And this is the function that will do the setting of the variables and submitting it to MoveX, it’s adapted from the Intentia supplied programs code
' Confirm an order that has been submitted via AddBatchHead and is in
' OIS275
' alsConfirmInfFields should be a list of mvxAPIFields that have been populated from MRS001MI
Private Function Confirm(byval alsConfirmInFields as List(Of mvxAPIField), byval astrAPIVersion as string, byval astrTemporaryOrderNumber as string, byval astrCompanyNumber)
as string) As String
Confirm = Nothing
Dim transStr As String = Nothing
Dim Result As String = Nothing
Dim rc As Long
' create our handler class and set the fields and version
Dim conConfirm As New OIS100_Confirm(alsConfirmInFields, astrAPIVersion )
If Not conConfirm Is Nothing Then
' set the company
conConfirm.SetCompany(astrCompanyNumber)
' set the order number we want to confirm
conConfirm.SetOrderNumber(astrTemporaryOrderNumber)
' create the string that we will send to MoveX
transStr = conConfirm.CreateInString(astrAPIVersion)
End If
' submit to MoveX
rc = MoveXSocket.MvxSockTrans(transStr, Result)
If rc <> 0 Then
MoveXSocket.MvxSockShowLastError("")
Exit Function
End If
If Left(Result, 5) = "REP " Then
Do
'Handle the returned string here
rc = MoveXSocket.MvxSockReceive(Result)
If Left(Result, 5) = "NOK " Then
Exit Function
End If
Loop Until Left(Result, 5) <> "REP "
End If
' return our result
Confirm = Result
End Function
And this is basically the code which will do the actual confirm.
' where ConfirmInFields is a List of mvxAPIFields that we have populated based on the In values for OIS100MI.Confirm from
' MRS001MI
' Version is the version that we have extracted from GENERAL.GetMIBuild.
' astrTemporaryOrderNumber was generated from our AddBatchHead command
' astrCompanyNumber was the company number of the order is for
Dim strConfirmResult As String = Confirm(ConfirmInFields, Version, astrTemporaryOrderNumber, astrCompanyNumber)
If strConfirmResult <> Nothing Then
' if the string starts with OK then we should be all good
If strConfirmResult.StartsWith("OK") = True Then
' create a new OIS100_Confirm object and populate it with the OutFields we have populated from
' MRS001MI
' set the version
' and supply the result string we got when we submitted the confirm to MoveX
Dim conConfirmResult As New OIS100_Confirm(ConfirmOutFields, Version, strConfirmResult)
If Not conConfirmResult Is Nothing Then
' and finally we extract the order number
Dim strOrderNumber As String = conConfirmResult.GetOrderNumber()
End If
End If
End If
End If
The code to extract the fields from MRS001MI are missing, but if you have had any experience with the APIs it should be fairly trivial for you to create – unfortunately due to the structure of my existing application there is a lot of interdependencies on other code making it difficult to extract for the purposes of illustration. Though in saying that I do plan on restructuring my code in the near future so may include it in a future post.
Anyways, I hope that this gives others a bit of a heads up or that the code is of some use to reduce compatibility problems in the future…
How do you use the MvxSock.dll in .Net? DllImport? I mostly use WebServices but I’d like to use the APIs to gain some speed and also I’d like to use it from .Net.
You’ll need to add a reference to MvxSockX_SVR.dll, I have had issues if the API toolkit isn’t installed first.
If you’re using Visual Studio, right click on References and then Add References, then browse to MvxSockX_SVR.dll
Yes, tried that, but it won’t add in VS2010… Tried both in a C# and VB project.
Interesting, most of my projects were migrated from Visual Studio 2008 which seemed to work ok. If I try with VS2010 it complains but still creates the interop assembly. That may work just like that.
I read your comment again. The problem was that I didn’t install the API toolkit correctly. I guess the dll wasn’t registered correctly on my computer. After installing I found the MvxSockX_SVR.dll as a COM (ActiveX) in VS2010.
Thanks!
Good to hear you got it sorted 🙂
Just found this post. Cross-referencing this other post just in case: http://m3ideas.org/2012/02/23/how-to-call-m3-api-from-net/