How to add a column to a list

In this article I illustrate a technique to dynamically add a column to the list of a B panel in M3 using a Personalized Script for Lawson Smart Office.

For example, suppose we want to add the column Country (CSCD) in CRS610/B1. None of the available Sorting orders (QTTP) displays the Country. How do we do?

This is the desired result:

1. Add a column by changing the View in CRS020

The first technique would be to add a column by simple configuration of the View in CRS020. But sometimes the specified M3 program is not configured for it. For instance, CRS610 is not configurable, whereas MMS200 is configurable as seen in this screenshot:

2. M3 modification

The second technique would be to add a column with a modification to the M3 Java source code and to the View Definition with MAK. But modifications may not be an option in certain M3 implementations.

3. Add a column programmatically with a script

The third technique would be to add a column dynamically with a Personalized Script for Lawson Smart Office.

Append the column

First, we get a reference to the list’s controls:

var listControl = controller.RenderEngine.ListControl; // MForms.ListControl
var listView = controller.RenderEngine.ListViewControl; // System.Windows.Controls.ListView
var columns = listView.View.Columns; // System.Windows.Controls.GridViewColumnCollection

Second, we append a new GridViewColumn to the ListView:

var gvch = new GridViewColumnHeader();
gvch.Content = "New Column";
var gvc = new GridViewColumn();
gvc.Header = gvch;
gvc.CellTemplateSelector = new ListCellTemplateSelector(columns.Count, listControl.Columns);
columns.Add(gvc);

Third, we increase each row’s array by one additional element:

var rows = listView.Items;
for (var i = 0; i < rows.Count; i++) {
	var row = rows[i];
	var oldArray = row.Items;
	var newArray = new String[columns.Count];
	oldArray.CopyTo(newArray, 0);
	row.Items = newArray;
	rows.RemoveAt(i);
	rows.Insert(i, row);
}

Finally, we can set our values in the new column:

listView.Items[0].Items[columns.Count - 1] = "Hello world 0";
listView.Items[1].Items[columns.Count - 1] = "Hello world 1";
listView.Items[2].Items[columns.Count - 1] = "Hello world 2";
listView.Items[3].Items[columns.Count - 1] = "Hello world 3";
listView.Items[4].Items[columns.Count - 1] = "Hello world 4";

The result looks like this, with the Personalizations like Hyperlinks and Conditional Styles preserved:

The complete script looks like this:

Populate with data

Now we have to populate the column with actual data from M3. For that we can call an M3 API, execute SQL, or consume a Lawson Web Service. We can even use the API MDBREADMI to read an M3 table instead of using SQL.

In this article, I will just hard-code “Hello World” and I will let the reader choose the technique that best suits its needs because getting the data off M3 is not the point of this post.

Load more data on scroll view

Finally, we have to load data in increments as the user scrolls the view. Indeed, as the user scrolls down the list Smart Office loads the data in increments, without re-rendering the whole panel, for efficiency.

For that, we need the VisualTreeHelper to get a reference to the ScrollViewer:

var border = VisualTreeHelper.GetChild(listView, 0);
var grid = VisualTreeHelper.GetChild(border, 0);
var scrollViewer: ScrollViewer = VisualTreeHelper.GetChild(grid, 3); // System.Windows.Controls.ScrollViewer

Then, we have to attach to the ScrollViewer’s OnScrollChanged event:

scrollViewer.add_ScrollChanged(OnScrollChanged);
function OnScrollChanged(sender: Object, e: ScrollChangedEventArgs) {}

That event is fired either once, either consecutively twice depending on if new rows were added to the list or not. Only when e.VerticalChange==0 it means that new rows were added.

var oldCount, newCount;
function OnScrollChanged(sender: Object, e: ScrollChangedEventArgs) {
     if (e.VerticalChange != 0) {
         oldCount = listView.Items.Count;
     } else {
         newCount = listView.Items.Count;
         var diff = newCount - oldCount; // that many rows were just added to the list 
     }
}

Here’s an illustration:

Now we know exactly which rows are new:

var fromRowIndex = oldCount;
var toRowIndex = newCount - 1;
for (var i = fromRowIndex; i <= toRowIndex; i++) {
     listView.Items[i] // new row
}

Now we can load and show some data, like Hello + customer number:

var lastColumnIndex = columns.Count - 1;
for (var i = fromRowIndex; i <= toRowIndex; i++) {
     var row = listView.Items[i];
     var data = "Hello " + row.Item[0]; // Hello CUNO
     row.Items[lastColumnIndex] = data;
}

Here is a screenshot of the final result:

Here’s the complete source code:

 import System;
 import System.Windows;
 import System.Windows.Controls;
 import System.Windows.Media;
 import Mango.UI.Services.Lists;

 package MForms.JScript {
     class Test {

         var listView, oldCount;
         /*
             Main entry point.
         */
         public function Init(element: Object, args: Object, controller : Object, debug : Object) {
             try {
                 var listControl = controller.RenderEngine.ListControl; // MForms.ListControl
                 this.listView = controller.RenderEngine.ListViewControl; // System.Windows.Controls.ListView
                 var columns = listView.View.Columns; // System.Windows.Controls.GridViewColumnCollection

                 // append a new GridViewColumn to the ListView
                 var gvch = new GridViewColumnHeader();
                 gvch.Content = "New Column";
                 var gvc = new GridViewColumn();
                 gvc.Header = gvch;
                 gvc.CellTemplateSelector = new ListCellTemplateSelector(columns.Count, listControl.Columns);
                 columns.Add(gvc);

                 var fromRow = 0;
                 var toRow = listView.Items.Count - 1;
                 var newNbColumns = columns.Count;
                 var lastColumnIndex = columns.Count - 1;

                 // increase each row's array by one additional element
                 increaseRowsArray(fromRow, toRow, newNbColumns);

                 // load data in the new column of each row
                 loadData(fromRow, toRow, lastColumnIndex);

                 // find the ScrollViewer
                 var border = VisualTreeHelper.GetChild(listView, 0);
                 var grid = VisualTreeHelper.GetChild(border, 0);
                 var scrollViewer: ScrollViewer = VisualTreeHelper.GetChild(grid, 3); // System.Windows.Controls.ScrollViewer

                 // attach to the OnScrollChanged event
                 scrollViewer.add_ScrollChanged(OnScrollChanged);
                 scrollViewer.add_Unloaded(OnUnloaded);            

             } catch (ex: Exception) {
                 debug.WriteLine(ex);
             }
         }

         /*
             That event is fired either once, either consecutively twice depending on if new rows were added to the list or not.
             Only when e.VerticalChange==0 it means that new rows were added.
         */
         function OnScrollChanged(sender: Object, e: ScrollChangedEventArgs) {
             try {
                 if (e.VerticalChange != 0) {
                     oldCount = listView.Items.Count;
                 } else {
                     var fromRow = oldCount;
                     var toRow = listView.Items.Count - 1;
                     var newNbColumns = listView.View.Columns.Count;
                     var lastColumnIndex = listView.View.Columns.Count - 1;
                     increaseRowsArray(fromRow, toRow, newNbColumns);
                     loadData(fromRow, toRow, lastColumnIndex);
                 }
             } catch (ex: Exception) {
                 MessageBox.Show(ex);
             }
         }

         /*
             Increase each row's array by one additional element.
         */
         function increaseRowsArray(fromRow, toRow, newNbColumns) {
             var rows = listView.Items;
             for (var i = fromRow; i <= toRow; i++) {
                 var row = rows[i];
                 var oldArray = row.Items;
                 var newArray = new String[newNbColumns];
                 oldArray.CopyTo(newArray, 0);
                 row.Items = newArray;
                 rows.RemoveAt(i);
                 rows.Insert(i, row);
             }
         }

         /*
             Loads data in the list, from the specified row's index, to the specified row's index, at the specified column index.
         */
         function loadData(fromRow, toRow, columnIndex) {
             for (var i = fromRow; i <= toRow; i++) {
                 var data = "Hello " + listView.Items[i].Item[0]; // Hello CUNO
                 listView.Items[i].Items[columnIndex] = data;
             }
         }

         /*
             Cleanup
         */
         function OnUnloaded(sender: Object, e: RoutedEventArgs) {
             sender.remove_Unloaded(OnUnloaded);
             sender.remove_ScrollChanged(OnScrollChanged);
         }

     }
 }

That’s it! Special thanks to Peder W for the original solution.

UPDATE 2012-09-27

This script is deprecated. Refer to the latest script here.

Related articles

Published by

thibaudatwork

ex- M3 Technical Consultant

24 thoughts on “How to add a column to a list”

  1. Nice post 🙂
    I have a few but important comments.

    1. You should only use the unloaded event to unregister events in Lawson Smart Client, version 1.0.3 and earlier. For Lawson Smart Office, version 9.0 and later, and with Lawson Smart Client, version 1.0.4 and later you should use the Requested event to unload the event handlers and nothing else. This is due to the control pooling that LSO uses. You might not get issues with this code when you run a demo but it is not safe and you should follow the guidelines in the Lawson Smart Office Developers Guide for M3.

    2. loadData has to make an async data call. This example is a showcase of what you can do but any real example communicating with a server has to involve a background thread, for example by using a BackgroundWorker. This introduces more complexity as the list might not be available once the data is returned.

    3. Always think twice about any call that you need to do per line. If there is one call per panel that is ok but one call per line is expensive. If you choose to implement it you should have the option of cancelling all pending request ( as the user might navigate to another panel before your data is loaded) making the solution even more complex.

    For Lawson Smart Office 9 and later versions of LSC i would not use the OnScrollChanged event at all but instead use the RequestCompleted event. If the command type is a PAGE then I know that a page down has taken place :-). The OnScrollChanged event will probably work just as good but the other is cleaner since it is anly called after a runtrip to M3.

    I did like the fact that you counted the lines. Never assume that there are 33 lines, also a page can be 32 or 64 lines depending on client screen resolution.

    Like

    1. Karin, in response to your great answers:

      1. Thank you for the heads up on the incorrect Unloaded event. I hadn’t noticed the difference and I will start using the Requested event from now on.
      2. You are completely correct. We have to use a background thread whenever we execute potentially time consuming code. There is indeed added complexity by the mutually exclusive constraints: perceived performance, usability, stability, code readability, etc. Usability alone includes: indicate activity, show progress, allow cancellation, allow the user to exit, scroll, or refresh the panel, etc. I know something about asynchronous calls (c.f. http://asynchronous.me/) so I should be able to find a good solution. Maybe the ideal solution is to have the M3 BE team allow all Views of all M3 programs to be configurable in CRS020, or at least the most used Views, but that is not easy to solve in bulk either.
      3. Indeed, making one call per row would be very counter efficient for the server and for the clients. Instead, we have to make one call per page view, as you remind us. There is also some added complexity to implementing that. There are only so many options: calling an M3 API, calling a Lawson Web Service of type MDP, executing SQL, or using some custom technique. If we call an M3 API, there is the problem of wasted results: API don’t accept as an input a list of identifiers that we would want to have exclusively queried and returned. For example if I had records A, B, and C loaded in the list at that point in time, I would like to call an API for those records only, not for records D, E, F which are not yet loaded in the list, and which would be wasted if they were returned by the API at that point in time, unless I implement a buffer for later use but that is even more complexity. If we call a Lawson Web Service of type MDP, it will only return the results of one record which is what we are trying to avoid. If we execute SQL we would be able to specify exactly which records we want returned, thereby maximizing efficiency, but there is the problem of querying the database directly from a client: network and firewall configuration, client-side driver installation, user/password transmitted in clear text, etc. The last option is to make some custom code on the server-side, more specifically in the same middleware as MNE which the clients can already connect to. For example we could write a JSP page since the application server is already in Java, that would verify authentication by the application server (something similar to IBrix SSO), that would accept a list of identifiers, that would execute SQL, that we would call from the client with HTTP, and that would return the result in any format like XML or JSON. This solution is becoming complex given the simplicity of the original question. That’s why I had originally avoided this topic in the post. But it’s actually good that you brought it up. Maybe it’s an opportunity for LPD to come up with a better solution.
      4. Thank you for the tip about the RequestCompleted event. I hadn’t thought about that. That’s more elegant indeed.

      Like

      1. Hey great work in here…I am a newbie to LSO scripting…but been functional consultant for m3 many yrs . have you thought about CRS990MI which is custom browse API that you could create in MNS185 for issue with #3

        Like

  2. Fantastic post, I’ve had a look at this particular problem and got thoroughly messed up trying to figure out what was going on under the hood.

    Very helpful indeed!

    Like

  3. Hi Thibaud,

    Have you looked at this with the November release of LSO? For MMS080 at least, the RemoveAt and Insert logic no longer works. When calling these functions a .Net error message is generated “Operation is not valid while ItemsSource is in use. Access and modify elements with ItemsControl.ItemsSource instead”.

    Any ideas how to refactor this code address this?

    Like

  4. UPDATE 2013-12-27: To add the columns to the middle of the list (instead of appending them to the end of the list), at a specified index i, use columns.Insert(i, gvc) instead of columns.Add(gvc).

    Like

  5. Hi Thibaud ,, thanks for that great Post ,,, but it does not work for me on Infor Smart Office … the problem is that i can not get the ScrollViewer Object ,,, the Object is always Grid. i don’t know what i should add for that code to work for me.
    Regards

    Like

    1. i have that Exception also

      System.InvalidOperationException: Operation is not valid while ItemsSource is in use. Access and modify elements with ItemsControl.ItemsSource instead.
      at System.Windows.Controls.ItemCollection.CheckIsUsingInnerView()
      at System.Windows.Controls.ItemCollection.RemoveAt(Int32 removeIndex)
      at invoker14.Invoke(Object , Object[] )
      at Microsoft.JScript.JSMethodInfo.Invoke(Object obj, BindingFlags options, Binder binder, Object[] parameters, CultureInfo culture)
      at Microsoft.JScript.LateBinding.CallOneOfTheMembers(MemberInfo[] members, Object[] arguments, Boolean construct, Object thisob, Binder binder, CultureInfo culture, String[] namedParameters, VsaEngine engine, Boolean& memberCalled)
      at Microsoft.JScript.LateBinding.Call(Binder binder, Object[] arguments, ParameterModifier[] modifiers, CultureInfo culture, String[] namedParameters, Boolean construct, Boolean brackets, VsaEngine engine)
      at Microsoft.JScript.LateBinding.Call(Object[] arguments, Boolean construct, Boolean brackets, VsaEngine engine)
      at MForms.JScript.TestColumn.increaseRowsArray(Object fromRow, Object toRow, Object newNbColumns)
      at MForms.JScript.TestColumn.Init(Object element, Object args, Object controller, Object debug)

      Like

      1. thanks alot Thibaud … it was very helpful for me … sorry for replying too late,,, i did not check that i have a reply ,, i thought i would have a notification on my mail when i have a new reply

        Like

  6. When you’re rebuilding the array, is it possible to control the display properties of the fields? i.e. pretend the telephone field was editable, how would you go about setting it as read only?
    I’m trying to make the price fields in PPS220/G (confirm order line) as read only…

    Like

  7. Hi! Do you know if it’s possible to sort on the added column somehow? Maybe with a few lines of code inside the script?

    Thanks in advance!

    Like

Leave a comment