We’re in the process of going from MoveX V12 Java, SP16 fix pack6, Foundation 7.1, Apps 5.2 to M3 V14, or 10.1 and 10.1 offers us a lot of opportunities through Smart Office and WebServices.
One of the issues that caused a lot of grief has been the APIs in MoveX V12 are very weak, they often get 60% of the way to achieving a goal but then don’t have the other core 40% of functionality there so you end up having to take a hybrid approach of direct SQL reads and APIs and using MoveX Explorer. Or worse still, you have a situation where the API functionality doesn’t honour all of the rules that the client does so you end up with inconsistent behaviour.
As most of you have no-doubt experienced, ERP solutions though great, often leave these little holes or gaps that your organisation needs to fill – this can’t be solved with MoveX alone. One of these gaps for us was EFT processing, in short creating a file of payments to suppliers that we upload to our bank for electronic payment.
The Lawson solution was to use MeC, and it is a horrible, horrible solution. MeC is over complicated and can get so severely screwed up with timing issues when it comes to payments that you have to adjust your process to work around the shortcomings – increasing workload and increasing the chances that your Accounts Payable staff can make a mistake, that’s before we get in to the arcane setup you need to go through and the consulting in-experience in this end of the world. Don’t get me wrong, I’m sure MeC has its place, but for us, it was like using an industrial power drill to hammer in a tac.
So for us I wrote a little .Net application that did away with the MeC side of the equation, it would use the APIs CRS692MI and APS130MI along with several SQL statements to extract a payment proposal, the payment proposals items and the bank account details and finally handle the deletion of the payment proposal.
In the upgrade it looks like something has changed around the functionality in APS130MI and it proposal deletion function which prevents it from working correctly (takes the proposal to status 4 indefinitely, an issue we are logging with Lawson). Which prompted me to consider, instead of using an external application, why not just create a jscript within Smart Office – the user will just highlight the proposal and click on the “Generate EFT” button and it will spit out a file which can then be uploaded – no more nonsense with APIs, additional user logins, SQL requirements and additional programs.
A jscript for generating the EFTs? Conceptually, very straight forward, the EFT program really consists of several loops and a function that spits out a file in the right format, can’t really get much easier. However, it requires usage of classes, properties, may use a window to display information and need to capture some of the events from the new window; things that I haven’t had any experience with in a jscript world.
So, I decided that I would clean up my earlier code around reflection, have it so it is displayed in a ListView (so I can use binding to make life easier) on a newly opened Window and set it up so you could select a property and then open a new window and show the selected objects info to give me a better idea of design for the EFTs.
Defining Classes
Well, this part is pretty simple, when we use the mforms://jscript editor and go New it creates the package and the class for us, so we have a framework to work with. But can we just define another class within the one package? Can we include a class within a class, what happens when we do that? Yes, yes and not a lot J
To start with I created a very basic class, it has a couple of variables and a constructor. It is the class that we will be using to store the information about each piece of information we find when we do a discovery on an object.
public class extMemberInfo { var Name : String; var MemberType : String; var DeclaringType : String; var Module : String; var ReflectedType : String; var Value : String; // this is the constructor for the object, here we set all the values function extMemberInfo(aName, aMemberType, aDeclaringType, aModule, aReflectedType, aValue) { this.Name = aName; this.MemberType = aMemberType; this.DeclaringType = aDeclaringType; this.Module = aModule; this.ReflectedType = aReflectedType; this.Value = aValue; } }
As you can see, not a lot to it, for the sake of experimentation and I do like type safety, I’ve set the data type of the variables, I defined a constructor so we could easily create and initialise the object in a single hit. After spending a lot of time in a type safe world and my unconditional love of properties I wondered if properties were supported within jscript, this little project was going to need properties that I could bind the ListView columns too.
public class extMemberInfo { private var pri_Name : String; private var pri_MemberType : String; private var pri_DeclaringType : String; private var pri_Module : String; private var pri_ReflectedType : String; private var pri_Value : String; // this effectively presents itself as a Property // Binding will only work on properties // retrieve the MemberType function get MemberType() : String { return this.pri_MemberType; } // retrieve the DeclaringType function get DeclaringType() : String { return this.pri_DeclaringType; } // retrieve the Module function get Module() : String { return this.pri_Module; } // retrieve the ReflectedType function get ReflectedType() : String { return this.pri_ReflectedType; } // retrieve the Value function get Value() : String { return this.pri_Value; } // retrieve the Name function get Name() : String { return this.pri_Name; } // this is the constructor for the object, here we set all the values function extMemberInfo(aName, aMemberType, aDeclaringType, aModule, aReflectedType, aValue) { this.pri_Name = aName; this.pri_MemberType = aMemberType; this.pri_DeclaringType = aDeclaringType; this.pri_Module = aModule; this.pri_ReflectedType = aReflectedType; this.pri_Value = aValue; } }
Here we set our variables to private, so no functions outside of this class should be able to change their values. The only way for the values to change is when you construct the object, and this will suit our purposes quite nicely.
The new parts that we have are is:
function get Name() : String
Quite simply, it is defining a function which is presented as a property, using the keyword get or set allows you to get or set a value as if it were a property. Name() is the name of the property, and : String says that this is going to return a String.
return this.pri_Name;
Will then return the value stored in pri_Name
Given we are effectively replicating many of the values in the MemberInfo class that we use to populate this object, we could have probably looked at inheritance, but that’s for another day to look in to.
Ok, so now we have a class which will store our values and supports properties so we can bind our columns to it.
Assemblies
In order to use reflection and some of the methods that I use we need to include other assemblies.
import System; // Environment class, allows us to use Environment.NewLine import System.ComponentModel; import System.Windows.Input; // we need System.Reflection so we can inspect the objects import System.Reflection; import System.Windows; import System.Windows.Controls; import MForms; import Mango.UI.Core; import Mango.UI.Core.Util; import Mango.UI.Services;
Most of these are already defined when you create a new jscript within the Smart Office editor.
Incidently, if you want to take a look at the assembles that Lawson provide, then you can use the Visual Studio Object Browser. The filename of the assemblies typically is the name of the Assembly with a .dll appended to the end (eg. Mango.UI.Core.dll), these are stored in the directory that the ClickOnce deployment dropped Smart Office in to, usually <user profile>\AppData\Local\2.0\<random name>\<random name>\<crytic GUID>\, basically I do a search in the AppData directory for Mango and it will show up the assemblies, in my case:
“C:\Users\Scott\AppData\Local\Apps\2.0\LQK5EE90.NZQ\9P7DMX0R.92Q\http..tion_3eefdc12643b7dbb_0009.0001_0654d9605bc969c8\Mango.Core.dll”
“C:\Users\Scott\AppData\Local\Apps\2.0\LQK5EE90.NZQ\9P7DMX0R.92Q\http..tion_3eefdc12643b7dbb_0009.0001_0654d9605bc969c8\Mango.Skin.dll”
“C:\Users\Scott\AppData\Local\Apps\2.0\LQK5EE90.NZQ\9P7DMX0R.92Q\http..tion_3eefdc12643b7dbb_0009.0001_0654d9605bc969c8\Mango.UI.dll”
“C:\Users\Scott\AppData\Local\Apps\2.0\LQK5EE90.NZQ\9P7DMX0R.92Q\http..tion_3eefdc12643b7dbb_0009.0001_0654d9605bc969c8\MangoChart.Core.dll”
“C:\Users\Scott\AppData\Local\Apps\2.0\LQK5EE90.NZQ\9P7DMX0R.92Q\mang..core_9759cb56e4efc0a2_0009.0001_none_1ff9594031846a9b\MangoChart.Core.dll”
Events
Now events caused me a lot of problems within this little project, you look at the Lawson provided examples and it makes event handling look so much more simpler than in the big boy programming languages J
Events cannot be created within jscript L, however you can subscribe to existing events. You need to find out what the event is called for an object and then call <object>.add_<eventname>(<event handling function>) and a call will need to be made later to unsubscribe from the event, this follows a similar format <object>.remove_<eventname>(<event handling function>)
Turns out that they can be very challenging, especially within Smart Office. I found that timing can be everything between hoses your Smart Office instance and it working flawlessly, that you have be pay very special attention to cleaning up resources and getting handlers of events arguments 100% correct – even then, I couldn’t for the life of my get the MouseDoubleClick event to fire in my ListView so resorted to SelectionChanged events.
In my situation I ended up adding event handlers that would listen for the closing of our window and the loaded event. Within the loaded event I would then add a handler to listen for SelectionChanged events against the ListView. Because this was such a problematic area I ended up leaving the event registration within the loaded event.
public function OnReflectionWindowLoaded(sender: Object, e: RoutedEventArgs) { try { // ensure that we have a ListView object to attach to if(null != wndReflectionWindow.lvListView) { // do the actual attach wndReflectionWindow.lvListView.add_SelectionChanged(OnSelectionChange); // if no exception was generated, then set a // flag so we know we did infact attach and will // need to eventually dispose of the attachment wndReflectionWindow.bListViewMouseDoubleClickAdded = true; } } catch(ex) { MessageBox.Show("Exception adding MouseDoubleClick to ListView: " + ex.Message + Environment.NewLine + ex.StackTrace); } } public function OnSelectionChange(sender: Object, e: SelectionChangedEventArgs) { // do something really fancy }
So when our Windows Loaded event is fired (basically once everything has been initialised), we attach a handler to our ListView looking for any SelectionChanged events, if there is one then the OnSelectionChange function will be called. It is absolutely critical to get the type correct for the EventArgs, odd things happen within Smart Office ranging from cryptic compile errors to bizzare crashes if you don’t.
It helps to refer to the library at http://msdn.microsoft.com has a lot of information on events and the args types, unfortunately the jscript examples are almost non-existant.
The Code
The moment you’ve been waiting for, the code. I was testing this against BUS100, so if you want to test this directly in your TEST environment, in Smart Office run BUS100 and then load the jscript editor and paste in the code. I’ve added a lot of comments in here to explain what is happening. In my example I use the Lawson ListView as the object I will examine.
There is a lot of ways that this could be expanded. I’ve created a ReflectionWindow class to hold some common values and in the knowledge that one day I will add code to allow us to open multiple windows at a time and recurse through objects.
If you want to get information on property from the first opened ListView, then hold down the Alt key and then select the property you want to inspect. A new window will open (oddly UNDER the main listview window).
Also NOTE that when-ever I selected any of the Lawson objects the script would get in to a mess, not sure why but it basically meant a restart of Smart Office to get things running again.
import System; // Environment class import System.ComponentModel; import System.Windows.Input; // we need System.Reflection so we can inspect the objects import System.Reflection; import System.Windows; import System.Windows.Controls; import MForms; import Mango.UI.Core; import Mango.UI.Core.Util; import Mango.UI.Services; package MForms.JScript { class bus100s { var debug, button, content, lv, listControl; var wndReflectionWindow : ReflectionWindow; public function Init(element: Object, args: Object,controller : Object, debug : Object) { content = controller.RenderEngine.Content; listControl = controller.RenderEngine.ListControl; lv = listControl.ListView; button = new Button(); button.Content = "Inspect"; Grid.SetColumnSpan(button, 10); Grid.SetColumn(button, 0); Grid.SetRow(button, 1); content.Children.Add(button); button.add_Click(OnClick); button.add_Unloaded(OnUnloaded); wndReflectionWindow = null; } public function OnClick(sender: Object, e: RoutedEventArgs) { try { if(null != wndReflectionWindow) { MessageBox.Show("wndReflectionWindow not null"); } // we are just going to inspect the listview // you can point this at any object to take a look addWindow(lv, false); } catch(ex) { MessageBox.Show("Exception: " + ex.Message + Environment.NewLine + ex.StackTrace); } } public function OnUnloaded(sender: Object, e: RoutedEventArgs) { // the window has been unloaded, time to clean up try { // if we have the wndReflectionWindow // look at closing it so we are totally // cleaned up if(null != wndReflectionWindow) { if(null != wndReflectionWindow.wndWindow) { wndReflectionWindow.wndWindow.Close(); } } // remove the event handlers that we added button.remove_Click(OnClick); button.remove_Unloaded(OnUnloaded); } catch(ex) { MessageBox.Show("Exception: " + ex.Message + Environment.NewLine + ex.StackTrace); } } public function OnReflectionWindowLoaded(sender: Object, e: RoutedEventArgs) { try { if(null != wndReflectionWindow.lvListView) { // subscribe to the SelectionChanged event, call OnSelectionChange() wndReflectionWindow.lvListView.add_SelectionChanged(OnSelectionChange); wndReflectionWindow.bListViewMouseDoubleClickAdded = true; } } catch(ex) { MessageBox.Show("Exception adding MouseDoubleClick to ListView: " + ex.Message + Environment.NewLine + ex.StackTrace); } } public function OnReflectionWindowClose(sender: Object, e: CancelEventArgs) { try { if(null != wndReflectionWindow) { if(null != wndReflectionWindow.lvListView) { if(true == wndReflectionWindow.bListViewMouseDoubleClickAdded) { // remove our subscription to the SelectionChanged // event wndReflectionWindow.lvListView.remove_SelectionChanged(OnSelectionChange); } } if(null != wndReflectionWindow.wndWindow) { // remove other subscribed events wndReflectionWindow.wndWindow.remove_Loaded(OnReflectionWindowLoaded); wndReflectionWindow.wndWindow.remove_Closing(OnReflectionWindowClose); } } } catch(ex) { MessageBox.Show("Exception: " + ex.Message + Environment.NewLine + ex.StackTrace); } // set to null so by rights the garbage collector // will see there are no more objects connected and clean up wndReflectionWindow = null; } public function OnSelectionChange(sender: Object, e: SelectionChangedEventArgs) { try { // if the user holds down the Alt key when the change their // selection on a property we will open a new window displaying // that objects properties if(Keyboard.Modifiers == ModifierKeys.Alt) { // only look at the first selected object if(e.AddedItems.Count >= 1) { var emiExtendedMemberInfo = e.AddedItems[0]; if(null != emiExtendedMemberInfo) { // we can only examine properties if(String.Compare("Property", emiExtendedMemberInfo.MemberType) == 0) { var piPropertyInfo = wndReflectionWindow.wndWindow.Tag.GetType().GetProperty(emiExtendedMemberInfo.Name); if(null != piPropertyInfo) { var objPropertyValue = piPropertyInfo.GetValue(wndReflectionWindow.wndWindow.Tag, null); if(null != objPropertyValue) { // call a new window with the value of the selected // object addWindow(objPropertyValue, true); } } } } } } } catch(ex) { MessageBox.Show("Exception: " + ex.Message + Environment.NewLine + ex.StackTrace); } } // addWindow - this function will use reflect to look at // an object and maybe give us some clues about that the // object does. It will open a new window with information // about the various properties, methods etc that are // exposed // objCurr = the object that we want to inspect private function addWindow(objCurr: Object, bNoEvents: boolean) { // wrap the function in a try...catch(), let us handle // the errors rather than having Smart Office get upset try { // create a new window which we will show to the user var wndWindow = new Window(); if(null != wndWindow) { if(false == bNoEvents) { wndReflectionWindow = new ReflectionWindow(); wndReflectionWindow.wndWindow = wndWindow; wndReflectionWindow.wndWindow.add_Loaded(OnReflectionWindowLoaded); wndReflectionWindow.wndWindow.add_Closing(OnReflectionWindowClose); } // store the object that we are looking at so we can do further analysis if the user desires wndWindow.Tag = objCurr; // set the Window Height to 600 wndWindow.Height = 600; // create a StackPanel, this makes it a little easier // with control placement in the window var spStackPanel = new StackPanel(); if(null != spStackPanel) { // StackPanel should be vertical! spStackPanel.Orientation = System.Windows.Controls.Orientation.Vertical; // Create a Textbox that we will use to display // the type of the object that we are inspecting var tbTextBox = new TextBox(); if(null != tbTextBox) { // it is set to ReadOnly tbTextBox.IsReadOnly = true; // add TextBox to the StackPanel spStackPanel.Children.Add(tbTextBox); // set the type of the object // we are inspecting to the TextBox tbTextBox.Text = objCurr.GetType().ToString(); // finally go out and create the ListView // that will display everything var lvListView : ListView = createListView(); if(null != lvListView) { if(false == bNoEvents) { wndReflectionWindow.lvListView = lvListView; } var miMemberInfo; var mtType; mtType = objCurr.GetType(); // retrieve the members from the object type // it is this list that we will iterate through // and display miMemberInfo = mtType.GetMembers(); // for some reason the for(var miCurrentMember in miMemberInfo) didn't work, resorted to a more traditional loop for(var i = 0; i < miMemberInfo.Length-1;i++) { var miCurrentMember = miMemberInfo[i]; var emiNewMember; var strPropertyValue : String = "N/A"; // only Properties will have values, everything else we will display as "N/A" var piPropertyInfo : PropertyInfo; // if we are a property then we should look at getting the value if(MemberTypes.Property == miCurrentMember.MemberType) { piPropertyInfo = objCurr.GetType().GetProperty(miCurrentMember.Name); if(null != piPropertyInfo) { strPropertyValue = piPropertyInfo.GetValue(objCurr, null); } } // create a new extMemberInfo object, this object effectively stores the pertinent information we want to look at emiNewMember = new extMemberInfo(miCurrentMember.Name, miCurrentMember.MemberType, miCurrentMember.DeclaringType, miCurrentMember.Module, miCurrentMember.ReflectedType, strPropertyValue); if(null != emiNewMember) { // add our entry to the ListView lvListView.Items.Add(emiNewMember); } } // add the ListView to the StackPanel spStackPanel.Children.Add(lvListView); } } // add the StackPanel to the Window wndWindow.Content = spStackPanel; // and last but not least, show the Window wndWindow.Show(); } } } catch(ex) { MessageBox.Show("Exception: " + ex.Message + " " + ex.StackTrace); } } // create our ListView with specific columns private function createListView() : ListView { var result : ListView = null; try { var lvListView = new ListView(); if(null != lvListView) { var gvGridView = new GridView(); lvListView.Height = 500; if(null != gvGridView) { lvListView.View = gvGridView; // create a new column and bind it to // the Name property var gvcName = new GridViewColumn(); gvcName.DisplayMemberBinding = new System.Windows.Data.Binding("Name"); gvcName.Header = "Name"; gvcName.Width = 100; gvGridView.Columns.Add(gvcName); // create a new column and bind it to // the MemberType property var gvcMemberType = new GridViewColumn(); gvcMemberType.DisplayMemberBinding = new System.Windows.Data.Binding("MemberType"); gvcMemberType.Header = "MemberType"; gvcMemberType.Width = 100; gvGridView.Columns.Add(gvcMemberType); var gvcDeclaringType = new GridViewColumn(); gvcDeclaringType.DisplayMemberBinding = new System.Windows.Data.Binding("DeclaringType"); gvcDeclaringType.Header = "DeclaringType"; gvcDeclaringType.Width = 100; gvGridView.Columns.Add(gvcDeclaringType); var gvcModule = new GridViewColumn(); gvcModule.DisplayMemberBinding = new System.Windows.Data.Binding("Module"); gvcModule.Header = "Module"; gvcModule.Width = 100; gvGridView.Columns.Add(gvcModule); var gvcReflectedType = new GridViewColumn(); gvcReflectedType.DisplayMemberBinding = new System.Windows.Data.Binding("ReflectedType"); gvcReflectedType.Header = "ReflectedType"; gvcReflectedType.Width = 100; gvGridView.Columns.Add(gvcReflectedType); var gvcValue = new GridViewColumn(); gvcValue.DisplayMemberBinding = new System.Windows.Data.Binding("Value"); gvcValue.Header = "Value"; gvcValue.Width = 100; gvGridView.Columns.Add(gvcValue); result = lvListView; } } } catch(ex) { MessageBox.Show("Exception: " + ex.Message + Environment.NewLine + ex.StackTrace); } return result; } } // this is a new class that we will store information that will be displayed by the // ListView // this could probably be done through inheritance, but // this is for the purposes of exploring what is supported // by the Lawson implementation of jscript // - there were a number of issues around this, effectively managing // to get things presented as properties public class extMemberInfo { private var pri_Name : String; private var pri_MemberType : String; private var pri_DeclaringType : String; private var pri_Module : String; private var pri_ReflectedType : String; private var pri_Value : String; // this effectively presents itself as a Property // Binding will only work on properties // retrieve the MemberType function get MemberType() : String { return this.pri_MemberType; } // retrieve the DeclaringType function get DeclaringType() : String { return this.pri_DeclaringType; } // retrieve the Module function get Module() : String { return this.pri_Module; } // retrieve the ReflectedType function get ReflectedType() : String { return this.pri_ReflectedType; } // retrieve the Value function get Value() : String { return this.pri_Value; } // retrieve the Name function get Name() : String { return this.pri_Name; } // this is the constructor for the object, here we set all the values function extMemberInfo(aName, aMemberType, aDeclaringType, aModule, aReflectedType, aValue) { this.pri_Name = aName; this.pri_MemberType = aMemberType; this.pri_DeclaringType = aDeclaringType; this.pri_Module = aModule; this.pri_ReflectedType = aReflectedType; this.pri_Value = aValue; } } // this class is a common place holder // we can look at supporting deeper recursion // and multiple window support in the futre public class ReflectionWindow { var wndWindow; var lvListView; var bListViewMouseDoubleClickAdded = false; } }
And that’s it. Given the code above runs through almost all of the concepts I need with the EFTs (except for writing to files and web-services), then I hope to convert my EFT code in to jscript over the next couple of weekends and post it here.
Update 20101122 As it turns out, the priority of the source code has been reduced as Lawson provided a fix before we went live on 10.1, see fix JT-185102
hi,
excellent site, and excellent post. i am initiate in JScript and Ibrix development and this is a excellent help.
Hope that we can change knowledgement about these issues in future
Thank you. I’m all for the sharing of knowledge.
Smart Client / Office are truly great products (which is part of the reason why I’ve been tinkering with reflection) and I think that the information needs to be there for people to take advantage of – if even to know that something is possible – or they aren’t the only ones to stumble across something that just don’t quite seem right 🙂
Your site and ideas are very good. I have also a question: Is it still possible to start an API directly in a Jscript ?
Hi Patrick, thank you for the comment 🙂
I believe that you can directly call the APIs from the jscript, that will of-course mean that you have to distribute the mvxAPI toolset to each computer that is going to run the jscript in question. IIRC the mvxsock*.dll is a COM object and you can use something similar to what I did with getting Excel access in my other blog postings – so something like:
var mvxSocket = new ActiveXObject();
I’d tend to recommend that you wrap the API in a webservice and call the webservice. While we’re on the subject, be aware that the API field positions can move between versions of M3, check out my posting “APIs – don’t forget to read the fine print!”
Cheers,
Scott
Thanks for your suggestion, but habe you such an example of a Jscript calling a WebService ?
🙂
Ah, not at the moment. I’ve created webservices in M3 but haven’t tried calling one from jscript. It is on my list of things to do and post about soon 🙂