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.
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.
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…