IFL has been running 13.4 for some months now, and given what’s not being said about Smart Office in a couple of the conferences earlier this year, I felt it was time to investigate migrating scripts to H5.
Previously, I had been reluctant to pursue this because H5 really lacked maturity and for the majority of our users, Smart Office is just easier to use. The last time I did some serious work with it, it felt like whack-a-mole with bugs and functionality. But we’re now more than a year further down the track and it’s clear that there has been a lot of focus put on H5.
It also helps that the H5 SDK document (Infor M3 H5 Development Guide) in the Knowledge Base talks about debugging the Javascripts with Visual Studio and has some templates – something that piqued my interest 🙂
Though I’ve played around with the H5 scripts in the past, and the updated PDF is more than a copy/paste/tweak from the Smart Office SDK – it discusses new methods to work with the UI so I figured I’d start with a Smart Office script that was pretty simple but extracted data from the ListView, added controls, updated controls and had event subscriptions. Also the H5 Development Guide recommends using TypeScript which I’ve never worked with so I wanted to get a bit of a feel for it without dealing with too much change.
The script in question is on MWS060/B. It totals the on hand balance, allocated quantity and provides a count of the selected records in the ListView.
Pretty basic sort of stuff.
We’re going to be adding 6 controls, 3 labels, 3 TextBoxes. We are going to subscribe to the event on the ListView which fires when the selection changes, and we are going to extract data from the selected records on the listview. This example covers most of the fundamentals in my scripts.
First things first. I grabbed the Visual Studio template from KB 1909067 (it’s an attachment). Then I added my new Typescript file, MWS060_TotalSelectedRows to the project.
Note: The .ts file automatically generates a Javascript .js file which is what we need to upload and reference in H5.
I added a few lines of code so I could test the script and breakpoints, but this is where I encountered my first issue.
Before we go in to that, we probably need to do some explaining first – please refer to the documentation for more information as I am only going to touch on some key points.
In the H5ScriptsProjectTemplate we have a start URL, this points to H5 and we provide some additional arguments. We also have a location for a local webserver where our script will actually be loaded from.
And where the script is loaded from is
http://localhost:50167/
When we run our project, IE will be launched and we log in to the H5 client, we then navigate to the program that has our script (MWS060_TotalSelectedRows.js) attached. The page will attempt to load the script from http://localhost:50167/ however, we are mixing https with http – IE, depending on your security settings will dutifully not load the http content.
What I had encountered was an issue with a mix of content secured from M3 and content that wasn’t secured off my machine. To allow this to work, you need to change a setting on your internet settings (or set the local IIS Express instance to use https – but I couldn’t quickly get that up and running).
I added M3/H5 server to my trusted sites zone, and I set the display mixed content to prompt. Yes, you could change this to enable, but even with trusted sites I’d prefer to know that we have potential issues.
Now I get this:
Click on Show all content -> Leave this page
You’ll be dropped back to the main H5 page, but I can just launch MWS060 again and my breakpoint gets triggered
The next thing to do was to subscribe to the SelectedRowsChanged event, the H5Developers Guide has an example of doing this,
private run(): void { this.AddControls(); const list = this.gController.GetGrid(); const handler = (e, args) => { this.onSelectionChanged(e, args); }; list.onSelectedRowsChanged.subscribe(handler); } private onSelectionChanged(e: any, args: any): void { let grid = args.grid; let selected = grid.getSelectedRows(); }
Interestingly on my MUA build, the event doesn’t trigger on the first click, you have to select a record and then select a second record after the fact before the event triggers. I’m waiting on a new build to be applied to the MUA before I log an issue.
We need to add our controls to the panel – nothing special there.
Next came the part that I was expecting to be straight forward, the actual updating of my controls with the values. With H5 Infor provide a number of common methods for you to extract and update UI data but this is where what was available departed from what the documentation said. They provide an example of using ScriptUtil to update values, but the arguments in the documentation is very different from what I could use. Eventually I noticed that the controller has an option to SetValue so I can do something like this:
this.gController.SetValue(“tbSelectedOhB”, totalOhB);
Ultimately we end up with this
It’s a little disappointing that I’m still hitting bugs with H5 that impact basic functionality but it’s likely something that I can work around by subscribing to a different event. Despite that, it is feeling a lot more mature and we have the GetMode() method which tells us whether we got to the panel through a create/change/copy/delete/display action – something I have been wanting for years.
There is definitely room for me to refine my development process which I’ll do as I spend more time with it.
Anyway, I’ve attached the Smart Office Jscript and the H5 TypeScript below.
Original JScript:
/* ** Name: MWS060_TotalSelectedRows ** Program: MWS060/B ** Description: Totals the allocated/on hand balance and number of rows selected ** ** History ** 20150727 - added on-hand balance ** 20161110 - quantities display two decimals now */ import System; import System.Windows; import System.Windows.Controls; import MForms; import Mango.UI; package MForms.JScript { class MWS060_TotalSelectedRows { var gDebug = null; var gController = null; var gContent = null; var giAllocatableColumn : int = -1; var gstrAllocateableColumnName : String = "AVAL"; var giOnHandBalanceColumn : int = -1; var gstrOnHandBalanceColumnName : String = "STQT"; var glvListView = null; var gtbSelectedAllocated : TextBox = new TextBox(); var glblSelectedAllocated : Label = new Label(); var gtbSelectedPallets : TextBox = new TextBox(); var glblSelectedPallets : Label = new Label(); var gtbSelectedOhB : TextBox = new TextBox(); var glblSelectedOhB : Label = new Label(); var giStartRow : int = 5; var giColumn : int = 50; var gQuantityFormat : String = "d2"; public function Init(element: Object, args: Object, controller : Object, debug : Object) { gContent = controller.RenderEngine.Content; gController = controller; gDebug = debug; var lcListControl : MForms.ListControl = gController.RenderEngine.ListControl; glvListView = lcListControl.ListView; giAllocatableColumn = lcListControl.GetColumnIndexByName(gstrAllocateableColumnName); giOnHandBalanceColumn = lcListControl.GetColumnIndexByName(gstrOnHandBalanceColumnName); if(-1 != giAllocatableColumn) { glblSelectedAllocated.Content = "Sel Alloc. Qty:"; Grid.SetRow(glblSelectedAllocated, giStartRow); Grid.SetColumn(glblSelectedAllocated, giColumn); Grid.SetColumnSpan(glblSelectedAllocated, 10); Grid.SetRow(gtbSelectedAllocated, giStartRow); Grid.SetColumn(gtbSelectedAllocated, giColumn + 10); Grid.SetColumnSpan(gtbSelectedAllocated, 10); gtbSelectedAllocated.Height = Configuration.ControlHeight; gtbSelectedAllocated.Width = 100; gtbSelectedAllocated.TextAlignment = TextAlignment.Right; gContent.Children.Add(gtbSelectedAllocated); gContent.Children.Add(glblSelectedAllocated); } glblSelectedPallets.Content = "Sel Pallet. Qty:"; Grid.SetRow(glblSelectedPallets, giStartRow+1); Grid.SetColumn(glblSelectedPallets, giColumn); Grid.SetColumnSpan(glblSelectedPallets, 10); Grid.SetRow(gtbSelectedPallets, giStartRow+1); Grid.SetColumn(gtbSelectedPallets, giColumn + 10); Grid.SetColumnSpan(gtbSelectedPallets, 10); gtbSelectedPallets.Height = Configuration.ControlHeight; gtbSelectedPallets.Width = 100; gtbSelectedPallets.TextAlignment = TextAlignment.Right; if(-1 != giOnHandBalanceColumn) { glblSelectedOhB.Content = "Sel On hand Bal:"; Grid.SetRow(glblSelectedOhB, giStartRow-1); Grid.SetColumn(glblSelectedOhB, giColumn); Grid.SetColumnSpan(glblSelectedOhB, 10); Grid.SetRow(gtbSelectedOhB, giStartRow-1); Grid.SetColumn(gtbSelectedOhB, giColumn + 10); Grid.SetColumnSpan(gtbSelectedOhB, 10); gtbSelectedOhB.Height = Configuration.ControlHeight; gtbSelectedOhB.Width = 100; gtbSelectedOhB.TextAlignment = TextAlignment.Right; gContent.Children.Add(glblSelectedOhB); gContent.Children.Add(gtbSelectedOhB); } gContent.Children.Add(glblSelectedPallets); gContent.Children.Add(gtbSelectedPallets); glvListView.add_SelectionChanged(onSelectionChanged); gController.add_Requested(OnRequested); } public function onSelectionChanged(sender : Object, args : SelectionChangedEventArgs) { if(-1 != giAllocatableColumn) { gtbSelectedAllocated.Text = ""; } if(-1 != giOnHandBalanceColumn) { gtbSelectedOhB.Text = ""; } gtbSelectedPallets.Text = ""; if(null != glvListView.SelectedItem && glvListView.SelectedItems.Count > 0) { var iAllocatedCount : decimal = 0; var iOnHandBalanceCount : decimal = 0; for(var row in glvListView.SelectedItems) { if(-1 != giAllocatableColumn) { iAllocatedCount += decimal(row[giAllocatableColumn]); } if(-1 != giOnHandBalanceColumn) { iOnHandBalanceCount += decimal(row[giOnHandBalanceColumn]); } } if(-1 != giAllocatableColumn) { gtbSelectedAllocated.Text = iAllocatedCount.ToString(gQuantityFormat); } if(-1 != giOnHandBalanceColumn) { gtbSelectedOhB.Text = iOnHandBalanceCount.ToString(gQuantityFormat); } gtbSelectedPallets.Text = glvListView.SelectedItems.Count.ToString(); } } public function OnRequested(sender: Object, e: RequestEventArgs) { if(MNEProtocol.CommandTypePage != e.CommandType) { // remove our event subscriptions gController.remove_Requested(OnRequested); glvListView.add_SelectionChanged(onSelectionChanged); } } } }
TypeScript (note: this will not work if uploaded directly to M3, it needs to be converted to javascript)
/* ** Name: MWS060_TotalSelectedRows ** Panel: MWS060/B ** ** Description: ** Totals the selected records On hand Balance, Allocatable Balance and counts the records selected ** ** Written By: ** scott.campbell@indfish.co.nz ** ** History: ** 20170823 SAC * Ported to H5 ** */ class MWS060_TotalSelectedRows { gDebug: IScriptLog = null; gController: IInstanceController = null; gContent: IContentElement = null; gAllocateableColumnName: String = "AVAL"; gOnHandBalanceColumnName: String = "STQT"; gListView = null; /* ** New controls that we will add to the panel */ tbSelectedAllocated: any = new TextBoxElement(); lbSelectedAllocated: any = new LabelElement(); tbSelectedPallets: any = new TextBoxElement(); lbSelectedPallets: any = new LabelElement(); tbSelectedOhB: any = new TextBoxElement(); lbSelectedOhB: any = new LabelElement(); gStartRow = 5; gColumn = 50; gQuantityFormat = "d2"; constructor(scriptArgs: IScriptArgs) { this.gDebug = scriptArgs.log; this.gController = scriptArgs.controller; this.gContent = scriptArgs.controller.GetContentElement(); this.gListView = ListControl.ListView; } private run(): void { this.AddControls(); const list = this.gController.GetGrid(); const handler = (e, args) => { this.onSelectionChanged(e, args); }; list.onSelectedRowsChanged.subscribe(handler); } private onSelectionChanged(e: any, args: any): void { let grid = args.grid; let selected = grid.getSelectedRows(); if (selected.length > 0) { let totalOhB = 0; let totalAllocated = 0; let totalPallets = 0; let OhB = ListControl.ListView.GetValueByColumnName(this.gOnHandBalanceColumnName); let allocated = ListControl.ListView.GetValueByColumnName(this.gAllocateableColumnName); if (null != OhB) { for (let currentOhB of OhB) { totalOhB += +currentOhB; } for (let currentAllocated of allocated) { totalAllocated += +currentAllocated; } this.gController.SetValue("tbSelectedPallets", allocated.length); this.gController.SetValue("tbSelectedOhB", totalOhB); this.gController.SetValue("tbSelectedAllocated", totalAllocated); } } } private AddControls() { if (null != this.gListView.GetValueByColumnName(this.gAllocateableColumnName)) { this.lbSelectedAllocated.Position = new PositionElement(); this.lbSelectedAllocated.Position.Top = this.gStartRow; this.lbSelectedAllocated.Position.Left = this.gColumn; this.lbSelectedAllocated.Value = "Sel Alloc. Qty:"; this.gContent.AddElement(this.lbSelectedAllocated); this.tbSelectedAllocated.Position = new PositionElement(); this.tbSelectedAllocated.Position.Top = this.gStartRow; this.tbSelectedAllocated.Position.Left = (this.gColumn + 10); this.tbSelectedAllocated.Position.Width = 10; this.tbSelectedAllocated.IsRightJustified = true; this.tbSelectedAllocated.Name = "tbSelectedAllocated"; this.gContent.AddElement(this.tbSelectedAllocated); } this.lbSelectedPallets.Position = new PositionElement(); this.lbSelectedPallets.Position.Top = this.gStartRow - 1; this.lbSelectedPallets.Position.Left = this.gColumn; this.lbSelectedPallets.Value = "Sel Pallet. Qty:"; this.gContent.AddElement(this.lbSelectedPallets); this.tbSelectedPallets.Position = new PositionElement(); this.tbSelectedPallets.Position.Top = this.gStartRow - 1; this.tbSelectedPallets.Position.Left = (this.gColumn + 10); this.tbSelectedPallets.Position.Width = 10; this.tbSelectedPallets.IsRightJustified = true; this.tbSelectedPallets.Name = "tbSelectedPallets"; this.gContent.AddElement(this.tbSelectedPallets); if (null != this.gListView.GetValueByColumnName(this.gOnHandBalanceColumnName)) { this.lbSelectedOhB.Position = new PositionElement(); this.lbSelectedOhB.Position.Top = this.gStartRow + 1; this.lbSelectedOhB.Position.Left = this.gColumn; this.lbSelectedOhB.Value = "Sel Pallet. Qty:"; this.gContent.AddElement(this.lbSelectedOhB); this.tbSelectedOhB.Position = new PositionElement(); this.tbSelectedOhB.Position.Top = this.gStartRow + 1; this.tbSelectedOhB.Position.Left = (this.gColumn + 10); this.tbSelectedOhB.Position.Width = 10; this.tbSelectedOhB.Name = "tbSelectedOhB"; this.tbSelectedOhB.IsRightJustified = true; this.gContent.AddElement(this.tbSelectedOhB); } } public static Init(args: IScriptArgs): void { new MWS060_TotalSelectedRows(args).run(); } }
Hello,
How do you feel about the Focus on the H5 client? Is the functionality of a java script the same as the functionality in a C# script?
What about the SDK. We have implemented a few SDK apps already are we going to be able to migrate those to the H5 client? Same question for the mashups?
If the rumours are true there will not be a Smart office client after the 13.4 release.
Kind regards,
Lode Vlaeminck
Infor are focusing heavily on the H5/Ming.le client – I spoke to one of the PMs in November and it sounds like they are really focused on improving performance and usability.
I’m comfortable with the level of H5 Javascript functionality now – there are a few holes like how we interface with MWS which concern me. InforAPIs are obviously the way it will go but there needs to be a clean transparent authentication method for interfacing calls from scripts. I don’t think that Infor have really thought this one through enough and may be working on the philosophy of Mongoose being the path in those instances – hopefully not. In saying that, there are many instances where we can ditch MWS and SQL queries in favour of CMS100.
SDK apps written in C# will need to be rewritten – how much work involved depends on how many .Net calls you make in your code and how you’ve structured it – so in some instances it may be a pretty significant rewrite.
Mashups – you can convert mashups to H5 mashups – I haven’t really tried it myself. I suspect that if you have added a lot of extra custom XAML to make things work you may have some issues. And given how AppBuilder is being positioned, I don’t think Mashups will be getting much more love.
Smart Office – the last I heard, Smart Office will not be offered with the multi-tenanted solution. It sounds like we are going to be finally transition to Grid 2, and there has been talk about optimising the communications between H5 and the M3BE my suspicion would be that Smart Office will not have this functionality added. Much of this should become clearer later in the year but I just can’t see Smart Office being an option for customers after 13.4.
Cheers,
Scott
Hi Scott,
Thanks for the reply! I did not receive an email that you replied, I probably did not fill out the checkboxes…
I don’t really like the idea of going to Javascript/typescript being a C# developer. If we ever make a move to an infor cloud solution i guess I will convert the sdk application to WPF applications, that seems to be the smallest step (knowing that some applications use EF, NLOG, Castle windsor, … ).
Again thanks for you point of view on the matter.
Keep up de good work. I like reading your blog/ infor research.
Kind regards
Lode Vlaeminck
Hi Scott,
Hope you are doing well. Do you know if I can write H5 script to load data from an Excel Spreadsheet to an M3 program as we do not have the API for it? I wrote the Jscript but it no longer works for H5.
Any guidance from you is highly appreciated.
Thanks,
Trung
Hi Trung,
yes, you will be able to however you’ll need to rewrite the JScript as Javascript – and you’ll probably need to look at some of the Javascript modules that are around on the web that can parse .xlsx files.
Cheers,
Scott
Hello Team,
Can we add Hyperlink,Dragdrop and treeview in H5?
Can anyone please help me?