Here is a real example of how to add a column to a list in M3 using a Script for Smart Office. It’s an illustration of my previous post on the same subject, an implementation of what was discussed in the comments of the first post, and a continuation of Karin’s post.
In this example I will add three columns to the list of Stock Location – MMS010/B1 to display the Geographic codes X, Y, and Z, which correspond to the longitude, latitude, and altitude of a Stock Location in a Warehouse. The Geo codes are stored in MMS010/F.
Why it matters
The benefit of this kind of solutions is to avoid an M3 Java modification.
From a technical point of view, this example illustrates how to dynamically add content to an existing M3 panel, how to access the ListView’s new internals in Smart Office 10.x, how to call an M3 API, how to use a background thread, how to indicate activity in the UI thread while the background thread works, and how to use the ScrollViewer to load data at each page scroll.
Desired result
This is the desired result:
Source code
Here is the complete source code:
import System; import System.Collections; import System.Windows.Controls; import System.Windows.Input; import System.Windows.Media; import Lawson.M3.MI; import Mango.UI.Services.Lists; import MForms; /* Displays the Geo codes XYZ in MMS010/B1 in three new columns loaded by calling MMS010MI.ListLocations. Thibaud Lopez Schneider, Infor, 2012-09-27 */ package MForms.JScript { class ShowGeoCodes { /* PENDING - Horizontal align the columns contents to the right - Vertical align the columns headers to top - Auto width the columns */ var controller: Object, content: Object, debug: Object; var listView; // System.Windows.Controls.ListView var rows: System.Windows.Controls.ItemCollection; var columns: System.Windows.Controls.GridViewColumnCollection; var scrollViewer: System.Windows.Controls.ScrollViewer; var oldCount: int = 0, newCount: int = 0; var GeoCodes; // System.Collections.Generic.IList[Lawson.M3.MI.MIRecord] public function Init(element: Object, args: Object, controller : Object, debug : Object) { try { // global variables this.controller = controller; this.content = controller.RenderEngine.Content; this.debug = debug; this.listView = controller.RenderEngine.ListControl.ListView; // == controller.RenderEngine.ListViewControl this.rows = listView.Items; this.columns = listView.View.Columns; // append three new columns to the ListView var newColumns = ['Geo code X', 'Geo code Y', 'Geo code Z']; for (var i in newColumns) { var gvch = new GridViewColumnHeader(); gvch.Content = newColumns[i]; var gvc = new GridViewColumn(); gvc.Header = gvch; gvc.CellTemplateSelector = new ListCellTemplateSelector(columns.Count, controller.RenderEngine.ListControl.Columns); columns.Add(gvc); } // register the ScrollChanged event of the ListView oldCount = newCount = rows.Count; var border = VisualTreeHelper.GetChild(listView, 0); var grid = VisualTreeHelper.GetChild(border, 0); this.scrollViewer = VisualTreeHelper.GetChild(grid, 3); this.scrollViewer.add_ScrollChanged(OnScrollChanged); // load the Geo codes XYZ by calling MMS010MI var CONO = UserContext.CurrentCompany; var WHLO = ScriptUtil.FindChild(content, 'WWWHLO').Text; BeginLoadGeoCodes(CONO, WHLO); // attach event to cleanup controller.add_RequestCompleted(OnRequestCompleted); } catch (ex: Exception) { debug.WriteLine(ex); } } /* Loads the Geo codes XYZ by calling MMS010MI.ListLocations. */ function BeginLoadGeoCodes(CONO: int, WHLO: String) { controller.RenderEngine.ShowMessage('loading Geo codes...'); content.Cursor = Cursors.Wait; var record = new MIRecord(); record['CONO'] = CONO; record['WHLO'] = WHLO; var parameters = new MIParameters(); parameters.OutputFields = ['WHSL', 'GEOX', 'GEOY', 'GEOZ']; parameters.MaxReturnedRecords = 0; MIWorker.Run('MMS010MI', 'ListLocations', record, EndLoadGeoCodes, parameters); } /* Handles the response from MMS010MI.ListLocations. */ function EndLoadGeoCodes(response: MIResponse) { try { controller.RenderEngine.ClearMessage(); content.Cursor = Cursors.Arrow; if (response.HasError) { controller.RenderEngine.ShowMessage(response.ErrorMessage); } else { this.GeoCodes = response.Items; ShowGeoCodesXYZ(0, rows.Count-1); } } catch(ex: Exception) { debug.WriteLine(ex); } } /* Loads more rows on ScrollViewer. */ function OnScrollChanged(sender: Object, e: ScrollChangedEventArgs) { try { if (e.VerticalChange != 0) { oldCount = listView.Items.Count; } else { var newCount = listView.Items.Count; var diff: int = newCount - oldCount; var fromRow = oldCount; var toRow = listView.Items.Count - 1; if (diff > 0) { ShowGeoCodesXYZ(fromRow, toRow); } } } catch (ex: Exception) { debug.WriteLine(ex); } } /* Shows the Geo codes XYZ for the specified rows. */ function ShowGeoCodesXYZ(fromRow: int, toRow: int) { var rows = IList(listView.ItemsSource); for (var i = fromRow; i <= toRow; i++) { var WHSL = rows[i].Item[0]; var codes = GetGeoCode(WHSL); // replace this row by a new row that's incremented by three new columns var row = rows[i]; var oldArray = row.Items; var newArray = new String[oldArray.length + 3]; oldArray.CopyTo(newArray, 0); newArray[newArray.length-3] = codes.GEOX; newArray[newArray.length-2] = codes.GEOY; newArray[newArray.length-1] = codes.GEOZ; row.Items = newArray; rows.RemoveAt(i); rows.Insert(i, row); } } /* Returns the Geo codes XYZ for the specified WHSL. */ function GetGeoCode(WHSL: String) { var i = 0; while (i < this.GeoCodes.Count && this.GeoCodes[i]['WHSL'] != WHSL) i++; // search array if (i < this.GeoCodes.Count) return { 'GEOX': this.GeoCodes[i]['GEOX'], 'GEOY': this.GeoCodes[i]['GEOY'], 'GEOZ': this.GeoCodes[i]['GEOZ'] }; // found WHSL else return { 'GEOX': '', 'GEOY': '', 'GEOZ': '' }; // no hit } function OnRequestCompleted(sender: Object, e: RequestEventArgs) { try { var controller: MForms.InstanceController = sender; if (controller.RenderEngine == null) { // program is closing, cleanup scrollViewer.remove_ScrollChanged(OnScrollChanged); controller.remove_RequestCompleted(OnRequestCompleted); } } catch (ex: Exception) { debug.WriteLine(ex); } } } }
Result
This is the final result:
Future work
This solution loads all records from the API call with MaxReturnedRecords=0. This could be a problem when there are more than several hundred records, or when the server/network response time is bad. I yet have to find a solution to improve this.
Also, the user could scroll the page while the response of the API call hasn’t arrived yet. I yet have to improve the code for that.
Finally, in my next article I will illustrate how to achieve the same result without programming, by using Custom Lists and Mashups.
UPDATE 2012-09-27
I updated the script to detach the event handlers on program close, i.e. cleanup.
Related articles:
- Custom Lists & Mashups, how to add columns to a list with a Custom List and Mashups
- How to add a column to a list, older post on how to add columns to a list with a script
- How to add a column to a list — #Comments
- Adding a new column in a M3 List, by Karin
- BackgroundWorkers in Smart Office Scripts – Part 2 – How to disable/enable the user interface, how to indicate activity, and how to show progress
- Geocoding of Stock Locations in MMS010
Hi Thibaud,
In all of the examples I have seen, columns are added to the list view but how feasible is to actually insert the new column in between existing ones? I need to insert a new column in OIS101 and this list view in CRS020 is already with the needed columns so I can’t just replace any of them. Could you please provide an example or feedback on this topic?
Thank you,
Gaston
LikeLike
Hi Gaston. Yes, in .NET you can insert an element at a specified position of the Array using the Insert method: http://msdn.microsoft.com/en-us/library/system.collections.arraylist.insert(v=vs.110).aspx . It goes after the oldArray.CopyTo(newArray, 0); Hope it helps. /Thibaud
LikeLike
While trying to copy a B Panel list view
(var newItems = new String[row.Items.length];
row.Items.CopyTo(newItems, 0) ) I got an error because the B Panel I was copying had editable cells. How can I add editable cells as my custom column?
LikeLike
Hi Jean, the object tree is different for editable cells. I could dig some source code from my boxes and I could help you as part of a project. /Thibaud
LikeLike
I have the same Problem with editalbe cells an row.Items.CopyTo(newItems,0); Please help me …
LikeLike
Hej Jörg, The object tree is different for editable cells. You can use a tool like Microsoft Inspect (see my post on Tools for Scripts) and find the hierarchy of objects for an editable cell. I searched my archive for past examples but I don’t have any for copying editable cells. I could help you through an Infor project and we would find the answer. Or try asking karinpb on the Smart Office blog. Hope it helps. Mvh, /Thibaud
LikeLike
Thanks very much for the valuable information. I follow the logic to work on PPS170 to tell users which proposal is for customer order and which one is for safety stock.
The listView needs to be refreshed by calling listView.Items.Refresh() to have updated items displayed.
I add a button for users to manually update it when they browse to next page instead of using ScrollChanged, which does not work. I also give users a feedback using MessageBox to tell them how many rows browsed and how many updated.
LikeLike
Thanks for sharing your solution Warren.
LikeLike
Hi Warren,
I have the same requirement. I have to update an editable cell when a user clicks a button and the value of that cell is blank. In the debug line I see the value being updated. However, in M3 even if I add the refresh the new values still doesn’t reflect. Please see my code below for the onClick method.
P.S. I’ve also tried to move the refresh after the assign and out of the for loop. Still no luck.
LikeLike
I got it to work. Apparently listView.Items[i].Items[7] = defWHSL; is what’s causing the refresh not to work I had to change it to row.Item[7].Text = defWHSL
LikeLike
Glad you found the solution. Thank you for sharing.
LikeLike
UPDATE: The script works correctly from the Script Tool, but I had to dispatch a Delegate in order for the script to run when it’s deployed on the server. The delta is:
…
import System.Windows.Threading;
import Mango.Core.Util;
package MForms.JScript {
class ShowGeoCodes_V2 {
…
public function Init(element : Object, args : Object, controller : Object, debug : Object) {
try {
// global variables
this.controller = controller;
this.content = controller.RenderEngine.Content;
this.debug = debug;
// dispatch delegate
var StartDelegate : VoidDelegate = Start;
content.Dispatcher.BeginInvoke(DispatcherPriority.Background, StartDelegate);
// move the rest of the Init code to the Start function
} catch (ex : Exception) {
…
}
}
function Start() {
// move to here
}
…
}
}
LikeLike
I want to know if there’s a way to get and set the color of text in the browse list through a J Script instead of using personalize.
LikeLike
Bonjour Jean, it’s probably possible but the browse list is difficult to access; Smart Office doesn’t have a public API for it. I think you have to hack into the hierarchy of ancestor windows, and descend to find the popup. I can probably do it after a day or two of investigation. Email me at Ciber for that. Or ask Karin if she knows. /Thibaud
LikeLike
Thanks thibaudatwork ,, for that useful Post ,,, is there a way to remove an existing column or Hide it ? ,,, for example if i want to Hide the first Column ,,i used that Code in Script DLL
IList columns = (IList)listControl.Columns;
columns.Remove(columns[0]);
—–
actually the number of Columns is decreased ,, but on the View the Column STill exists ,,, i found using Google the Property AutoGenerateColumnsProperty ,, but i don’t know where i can find it . i think i need to set it to false before removing the required column.
or i need to refresh the Grid with some way
regards
LikeLike
Hi Zaher, yes you can remove a column. You have to remove it from the view and from the model. It’s been a couple of years since I worked with this, I think the view is ListView and the model is ListControl. Otherwise, the easiest is to simply remove it from the M3 View (PAVR), press F4 twice in the dropdown list for the View. /Thibaud
LikeLike
ok ,,, thanks alot ,, but is there a way to Remove a specific Action ,,, in the Actions Menu … i could successfully remove Options From the Related Option and in the Basic Options (Change- Create ,…) .. but for the Actions menu ( Refresh, Cancel, Setings, Close) i could not edit it 😦
LikeLike
hi i’m getting an error on the following line, with 13.2 this no longer works:
scrollViewer = VisualTreeHelper.GetChild(grid, 3);
do you have a solution for it?
LikeLike
Hi Shiraz, that technique is deprecated. Use ScrollChangedEvent as illustrated by norpe at http://smartofficeblog.com/2012/10/12/tooltips-with-mi-data-in-m3-lists/
LikeLike
Thanks a lot 🙂
LikeLike
Dear thibaudatwork,
I used your code in MMS010. data is not coming in grid as you shown in the final result. grid header is coming but data is not coming. what should i do?
LikeLike
Hi jaju, that code is deprecated with the various updates from the comments and Karin’s and Peter’s blog posts. It’s just the API for the data model that has a simple change. You need a .NET developer that can assist you. Refer to http://smartofficeblog.com/2012/10/12/tooltips-with-mi-data-in-m3-lists/ and http://smartofficeblog.com/2012/02/13/adding-a-new-column-in-a-m3-list/
LikeLike
Iam getting error like this
System.NullReferenceException: Object reference not set to an instance of an object.
at MForms.JScript.ShowGeoCodes.ShowGeoCodesXYZ(Int32 fromRow, Int32 toRow)
at MForms.JScript.ShowGeoCodes.EndLoadGeoCodes(MIResponse response)
LikeLike
Jaju, you’ll have to debug your code line by line (comment all the lines and uncomment one by one, or use a lot of debug.WriteLine) to identify the variable that’s null.
LikeLike
Also, try this:
var rows: IList;
if (listView.ItemsSource != null) {
rows = IList(listView.ItemsSource);
} else {
rows = listView.Items;
}
LikeLike
Hi, all is clear but the part
var border = VisualTreeHelper.GetChild(listView, 0);
var grid = VisualTreeHelper.GetChild(border, 0);
this.scrollViewer = VisualTreeHelper.GetChild(grid, 3);
The numbers 0, 0, 3 and the structure is magic for me. I tried to do simillar script, in script tool is everything working, in the script put to personalization this part does not work, is there a way, how to find the tree structure of page and how to get to the scrollViewer for which I need to attach event handler?
LikeLike
Hello Jan,
Yes, the constant numbers will break if the visual tree changes in Smart Office.
The best tools to see the visual tree are: the Windows SDK Inspect tool, and Snoop: https://m3ideas.org/2011/09/29/tools-to-develop-smart-office-scripts/
Otherwise visit the tree in code: https://m3ideas.org/2014/03/18/hacking-customer-lifecycle-management-clm/
As for your problem that the code works in Script Tool but does not work when deployed, you may need a StartDelegate:
Hope it helps,
–Thibaud
LikeLike
UPDATE: Carlos Roda says the line:
this.scrollViewer = VisualTreeHelper.getChild(grid, 3)
changed to border:
VisualTreeHelper.getChild(border, 0)
LikeLike
Hi! Did you ever find a way to align the column content (not the header) to the right? Haven’t been able to find a way to do this myself.
LikeLike