How to add a column to a list (continued)

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:

Published by

thibaudatwork

ex- M3 Technical Consultant

33 thoughts on “How to add a column to a list (continued)”

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

      Like

  1. 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?

    Like

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

      Like

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

    Like

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

          function OnClick(sender: Object, e: RoutedEventArgs) {
              var listView = gController.RenderEngine.ListControl.ListView;   
              var rows = listView.ItemsSource;
              for(var i =0; i<rows.Count; i++) {
                  var row = rows[i];
                  if (row.Item[7].Text == ''){
                      listView.Items[i].Items[7] = defWHSL;
                  }
                  gDebug.WriteLine(listView.Items[i].Items);
                  listView.Items.Refresh();
              }
          }
      

      P.S. I’ve also tried to move the refresh after the assign and out of the for loop. Still no luck.

      Like

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

        Like

  3. 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
    }

    }
    }

    Like

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

    Like

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

      Like

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

    Like

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

      Like

      1. 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 😦

        Like

  6. 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?

    Like

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

    Like

  8. 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)

    Like

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

      Like

  9. 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?

    Like

    1. 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:

      import System.Windows.Threading;
      import Mango.Core.Util;
      ...
      var controller : Object;
      var debug : Object;
      public function Init(element : Object, args : Object, controller : Object, debug : Object) {
          this.controller = controller;
          this.debug = debug;
          var StartDelegate : VoidDelegate = Start;
          // wait for UI to render
          controller.RenderEngine.Content.Dispatcher.BeginInvoke(DispatcherPriority.Background, StartDelegate);
      }
      function Start() {
          // ready
      }
      

      Hope it helps,

      –Thibaud

      Like

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

    Like

Leave a comment