Tooltips with MI data in M3 lists

A script requirement we recently heard of was to be able to show tooltips on cells in a M3 list and that the tooltip value should be retrieved using a MI-program. The specific requirement was to get the user name for list columns that contains M3 user IDs. The idea is to use this in M3 lists where the user ID is shown in a column but the user name is not part of the list. The best and most efficient solution would be to solve this in BE using views or modifications. The script solution should only be used if there are no other options.

At first the script implementation seems kind of straightforward but there are a couple of things that must be considered that make the script quite complicated.

The basic idea would be to:

  • Add a dummy tooltip to the ContentPresenter for the list cell.
  • Add an event handler for the ToolTipOpening event on the ContentPresenter.
  • When the event is fired call MNS150MI asynchronously to get the user name.
  • Update the tooltip with the user name.

What complicates things are:

  • The WPF ListView is virtualized so the UI elements in the list are not created until they are visible.
  • When the list is scrolled up and down the virtualized items are thrown away and new ones must be initialized with tooltips.
  • The list will load new list rows from the server when scrolled to the end and these items must be updated.
  • The MI-calls are expensive so the user names that have been loaded should be cached and reused.
  • The list columns might be reordered and the script should still work if so.

The example script tries to solve the problems above but it requires quite a lot of code. The updating of the list rows is kind of brute force and occurrs each time the scrolling is changed. It is possible to optimize the script but when doing this it is also easy to break the functionality of the script. When using the script you should test the performance in the UI to make sure that it doesn’t slow things down too much. I did not find any noticeable difference in scroll performance but it might be different on other machines.

Testing the script

The script should work by default in a list that contains a USID column. To simplify testing I have just used the MNS150 program. This does not make sense outside testing since the MNS150 list already shows the user name. The good thing is that it is very easy to see if the script works as expected when tested in MNS150.

The script tested in MNS150

Extending the script

The script example is currently not generic but it would be very easy to make it generic using arguments. The target column is an obvious choice for a script argument. The MI program, transaction and input/output field names could also be arguments to make the script even more generic. I’ll leave these changes as an exercise for the reader.

Script overview

Some of the concepts and functions in the example script are describe below. See the script code in the end of the post for more information.

Logging

The Log function is a simple wrapper that works with both the new logging methods and the old ones. Most of the Log calls have been commented out but you can uncomment them to get a better sense of how the script works.

Caching

The user names that have been loaded are cached in a Hastable that is stored in the SessionCache. The cached user names will be available during the Smart Office session and can be shared by all scripts that use the same cache key.

Init function

The script adds a handler for the ScrollChanged event and starts updating the tooltips. The tooltip update is delayed using the dispatcher queue to make sure that the list is rendered.

UpdateToolTipsAsync function

This starts the tooltip updates. It is called from a couple of different places such as Init, OnRequestCompleted and OnScrollChangedList.

AddToolTips function

This function adds a dummy tooltip and a ToolTipOpening event handler on all list cells in the target column that has been created. List cells that have not been created due to virtualization are simply skipped. Note that the original column index is used when getting the ContentPresenter.

OnToolTipOpening function

The event handler that is called when the dummy tooltip is opened. If the user name is available in the cache the tooltip is set directly. If the tooltip is not available it will be loaded from the MI-program.

LoadUserName function

This function uses the MIWorker to call the MI program.

OnResponse function

This function handles the response from the MI program, stores the value in the cache and updates the tooltip. If the user name cannot be loaded the tooltip is set to the user ID.

Example script code

Copy the code to your favorite text editor for better viewing. A tip is to use Notepad++ with the JSMin plugin for code formatting (http://jsminnpp.sourceforge.net) and the “Use External editor” mode in the Script Tool.

import System;
import System.Collections;
import System.Windows;
import System.Windows.Controls;
import System.Windows.Threading;
import Lawson.M3.MI;
import Mango.UI.Utils;
import MForms;

package MForms.JScript {
   class UserNameToolTipList {

      var targetColumn = "USID";
      var program = "MNS150MI";
      var transaction = "GetUserData";
      var inputFieldName = "USID";
      var outputFieldName = "NAME";
      var cacheKey = "UserNameToolTipList.Cache";
      var loadingText = "Loading user name...";

      var typeGridViewRowPresenter = Type.GetType("System.Windows.Controls.GridViewRowPresenter, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");
      var typeContentPresenter = Type.GetType("System.Windows.Controls.ContentPresenter, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");

      var debug;
      var controller;
      var listControl;
      var listView;
      var currentUserId;

      public function Init(element : Object, args : Object, controller : Object, debug : Object) {

         controller.add_Requested(OnRequested);
         controller.add_RequestCompleted(OnRequestCompleted);

         this.debug = debug;
         this.controller = controller;
         this.listControl = controller.RenderEngine.ListControl;
         if (listControl == null) {
            Log("No list found");
            return;
         }
         this.listView = listControl.ListView;

         var handler : ScrollChangedEventHandler = OnScrollChangedList;
         listView.AddHandler(ScrollViewer.ScrollChangedEvent, handler);

         UpdateToolTipsAsync();

         Log("UserNameToolTipList script initialized");
      }

      public function OnRequested(sender : Object, e : RequestEventArgs) {
         try {
            if (e.CommandType == MNEProtocol.CommandTypePage) {
               return;
            }
            if (controller != null) {
               controller.remove_Requested(OnRequested);
               controller.remove_RequestCompleted(OnRequestCompleted);
            }
            if (listView != null) {
               var handler : ScrollChangedEventHandler = OnScrollChangedList;
               listView.RemoveHandler(ScrollViewer.ScrollChangedEvent, handler);
            }
         } catch (ex) {
            Log(ex);
         }
      }

      public function OnRequestCompleted(sender : Object, e : RequestEventArgs) {
         try {
            // Log("OnRequestCompleted");
            if (e.CommandType == MNEProtocol.CommandTypePage) {
               // Log("OnRequestCompleted: Updating tooltips after page down");
               UpdateToolTipsAsync();
            }
         } catch (ex) {
            Log(ex);
         }
      }

      private function Log(value) {
         if (debug.Debug) {
            if ("object" == typeof(value)) {
               debug.Error("Exception thrown", value);
            } else {
               debug.Debug(value);
            }
         } else {
            debug.WriteLine(value);
         }
      }

      public function LoadUserName(userId, contentPresenter) {
         try {
            currentUserId = userId;
            var request = new MIRequest();
            request.Program = program;
            request.Transaction = transaction;
            request.Tag = contentPresenter;
            var record = new MIRecord();
            record[inputFieldName] = userId;
            request.Record = record;
            MIWorker.Run(request, OnResponse);
         } catch (ex) {
            Log(ex);
         }
      }

      public function OnResponse(response : MIResponse) {
         try {
            var userName;

            if (!response.HasError) {
               userName = response.Item[outputFieldName];
               // Log("OnResponse: Loaded user name " + currentUserId + " = " + userName);
            } else {
               userName = currentUserId;
               // Log("OnResponse: " + response.ErrorMessage);
            }

            if (userName != null) {
               var cache = GetCache();
               cache[currentUserId] = userName;

               var presenter = response.Tag;
               if (presenter.IsVisible) {
                  presenter.ToolTip = userName;
               }
            }
         } catch (ex) {
            Log(ex);
         }
      }

      public function UpdateToolTipsAsync() {
         var action : Action = UpdateToolTips;
         Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background, action);
      }

      public function UpdateToolTips() {
         try {
            AddToolTips(listView);
         } catch (ex) {
            Log(ex);
         }
      }

      public function OnScrollChangedList(sender : Object, e : ScrollChangedEventArgs) {
         try {
            UpdateToolTipsAsync();
         } catch (ex) {
            debug.WriteLine(ex);
         }
      }

      public function GetCache() {
         var cache = SessionCache.Get(cacheKey);
         if (cache == null) {
            cache = new Hashtable();
            SessionCache.Add(cacheKey, cache);
         }
         return cache;
      }

      public function GetCachedName(userId) {
         var cache = GetCache();
         if (cache.ContainsKey(userId)) {
            return cache[userId];
         }
      }

      public function OnToolTipOpening(sender : Object, e : ToolTipEventArgs) {
         try {
            // Log("OnToolTipOpening");

            var presenter = e.Source;
            presenter.remove_ToolTipOpening(OnToolTipOpening);

            var userId = e.Source.Tag;
            if (userId == null) {
               return;
            }

            var userName = GetCachedName(userId);
            if (userName != null) {
               presenter.ToolTip = userName;
               // Log("OnToolTipOpening: Using cached user name for " + userId);
            } else {
               presenter.ToolTip = loadingText;
               LoadUserName(userId, e.Source);
               // Log("OnToolTipOpening: Loading user name for " + userId);
            }
         } catch (ex) {
            Log(ex);
         }
      }

      private function AddToolTips(listView) {
         try {
            var cellIndex = listControl.GetColumnIndexByName(targetColumn);
            cellIndex = GetOriginalIndex(listView, cellIndex);
            if (cellIndex < 0) {
               // Log("Could not find column " + targetColumn);
               return;
            }

            var foundFirstVisible = false;
            for (var i = 0; i < listView.Items.Count; i++) {
               var presenter = GetCellPresenter(listView, i, cellIndex);
               if (presenter == null) {
                  continue;
               }

               foundFirstVisible = true;
               if (presenter.ToolTip == null) {
                  var item = listView.Items[i];
                  var value = listControl.GetColumnValue(targetColumn, item);
                  if (value != null) {
                     presenter.Tag = value;
                     presenter.ToolTip = "";
                     presenter.add_ToolTipOpening(OnToolTipOpening);
                     // Log("Added tooltip for " + value + " in column index " + cellIndex);
                  } else {
                     // Log("No value found for column " + targetColumn);
                  }
               }
            }
         } catch (ex) {
            Log(ex);
         }
      }

      private function GetOriginalIndex(listView, index) {
         var listColumn = listView.View.Columns[index].Header.Tag;
         return listColumn.Index;
      }

      private function GetCellPresenter(listView, rowIndex, cellIndex) {
         if (rowIndex >= listView.Items.Count) {
            // Log("The rowIndex is to large " + rowIndex);
            return null;
         }

         var container = listView.ItemContainerGenerator.ContainerFromIndex(rowIndex);
         if (container == null) {
            return null;
         }

         var rowPresenter = Helpers.FindElementOfType(container, typeGridViewRowPresenter);
         if (rowPresenter == null) {
            // Log("No GridViewRowPresenter found for rowIndex " + rowIndex);
            return null;
         }

         var presenters = Helpers.FindElementsOfType(rowPresenter, typeContentPresenter);
         if (presenters != null && cellIndex < presenters.Count) {
            return presenters[cellIndex];
         }

         // Log("No ContentPresenter found for cellIndex " + cellIndex);
      }
   }
}

23 thoughts on “Tooltips with MI data in M3 lists

  1. Juan V.

    Thanks Peter, useful post.
    For that purpose I´ve created a jscript that only load the tooltip when the user selects the row. The jscript is simpler and performance is improved only to request the API or LWS on the selected rows.
    The worst part in that you need to focus out and focus in again to see it but it´s a good choice to load extra information when the user doesn´t want a MAK modification.

    Reply
    1. norpe Post author

      The script in this post will only call the MI for non-cached values when the user hovers over a list cell. It will not call the MI for all list row so in that regard the performance should be similar to your implementation.

      Reply
      1. Juan V.

        Here you got me testing on Saturday 🙂
        After have a deeper look and test, it really rocks and performance is really great. I´ve been testing at two LSO installations in France and in US.
        Result = My jscript have been deleted. It really sucks after see this.
        Thanks Peter.

  2. Olivier

    hi,
    I would assume that this is possible for detail panels as well. There are some panels in M3 that miss description fields, or I could even imagine that in some cases this would be great to create some customer specific logic showing “any” related information in the tooltip. Just the first example that comes to my mind is get item information (MMS200MI – GetItm*) in the tooltip for an item number in a detail panel.
    As I am not very skilled when it comes to scripting (I’m a BC, sorry 😉 ), is there anyone who could give me a little push in the right direction?

    tx & regards, Olivier

    Reply
    1. norpe Post author

      The techniques used in this post can be applied to many different scenarios, including detail panels. A developer with some MForms scripting experience should be able to create variations of this script for detail panels and other cases.

      Reply
  3. Ahmed Taha

    while tying to call any API it gives me this error message
    C:\Users\ahtaha\AppData\Local\Temp\rjjeqxnm.0.js(97,31) : error JS1135: Variable ‘MIRequest’ has not been declared
    C:\Users\ahtaha\AppData\Local\Temp\rjjeqxnm.0.js(101,30) : error JS1135: Variable ‘MIRecord’ has not been declared
    C:\Users\ahtaha\AppData\Local\Temp\rjjeqxnm.0.js(104,13) : error JS1135: Variable ‘MIWorker’ has not been declared
    C:\Users\ahtaha\AppData\Local\Temp\rjjeqxnm.0.js(110,45) : error JS1135: Variable ‘MIResponse’ has not been declared

    Reply
    1. karinpb

      Hi,
      Have you modified the script?
      Are you on LSO version 9.1.3 or above?
      The MIRequest etc were added in that version and this script will only work on 9.1.3 or later.

      Reply
  4. Mbenmoussati

    Hi,
    Tanks a lot for this site. I’ve tried your script on the CRS207 but i get an error in some cases. When the matrix isn’t complete, the tooltip isn’t opened in the good cell. I can’t explain why.
    Do you have an Idea???
    Thanks a lot for all what you done!

    Reply
    1. karinpb

      Hi,
      I don’t know how to get to CRS207. What you can do is to log more in the script to try and see what is going on or you can use a tool like Fiddler2, to check that the requests on the network really are. Or tell me how to get to CRS207…

      Reply
      1. Mohamed

        Thank you for your reply. I use fiddler and logs but i can’t find the issue… If you give me your mail, i can send you screenshots of the troubles and how to acces to the crs207. This screen is just a matrix. I m consumming a ws, and the result is usee to populate the tooltip.
        Regards,
        Mohamed

      2. karinpb

        I’m sorry if it works sometimes it can be related to the indata. I don’t have time to support specific scripts, I can just point you in the right direction. If you have access to the SDK you can try and call your code from the script so that you can debug. If you can’t do that then it is up to logging and using Fiddler.

    1. karinpb

      Hi, what is the exact exception? Nu such member means that the method or property does not exist in your version of Smart Office, probably because it was added in an newer version. Check that it’s not a typo and check the Api documentation if you have that – or decompile the mforms.dll.

      Reply
      1. karinpb

        I think 9.1.3.7 has MIAccess and all those classes – but what member is it complaining on? Change log level to debug and share part of the log that has the error.

  5. Doms Molina

    Hi,

    I have a requirement that updates an editable cell when a button is clicked and if the value is blank. On top of that I incorporated the OnScrollChangedList functionality to update the newly added rows automatically when the button has been already clicked & only load if button is not clicked. I am wondering why in MMS001 when for scrolling I’m having no problem however when I transfer to PPS300 wherein the editable cell to be updated is present the code suddenly enters into an endless loop with OnScrollChangedList. Please note that I have already tested the updating of editable cell and OnScrollChangedList separately(PPS300 & MMS001 respectively). I wanted to paste the ode here however it is quite long.

    Reply
    1. karinpb

      Hi,
      I’m not aware of any issues related to this. It’s impossible for me to give advice without more information. Perhaps you can include a public accessible link to the script (as .txt)? Does the Smart Office log show anything?

      Reply
      1. karinpb

        Hi,

        I have checked the code. There are some issues to it.

        1. There is nothing that will prevent the OnScrollChangedList to be called multiple times if for example the LoadData methods in turn triggers the scrolling event.

        2. Old Count is hard coded to 33 but the number of lines can be 66 so you would need to get the initial lines in OnRequestCompleted for example.

        3. OnRequestCompleted. You are removing the scroll down here – new rows will not be affected by the script. Shouldn’t you use the same check on commandtypepage as in OnRequested?

        4. A loops should be terminated when value is found. This is not an error but getting the column number for WHSL should be a method that returns. Eg getWarehouseColumn().

        I think listView.Items.Refresh(); might cause scrolling events.

        What is the goal with the script? You are currently only updating the data object and not anything in the visual tree. As long as you are not doing anything visual you should only focus on when the button is clicked and the onRequestCompleted event and update the data then.

        Try without listView.Items.Refresh(); as well since it might not be necessary. You should test if you really need it.

        If you really need to change visual stuff you need to make sure that you bail out from onScrollChangedList if it is triggered from a LoadData. But that scrollChangedList might be dispatched – but if you are lucky it isn’t and then it is enough to set a flag that you are entering the loadData and then reset it once you leave the method.

      2. Doms Molina

        Hi Karin,

        1. Yes you are right. I created a message box logs(since debug.writeline won’t work anymore due to endless loop) and noticed that OnScrollChangedList is being triggered during the update of row.Item[WHSLCOL].Text. I already removed listView.Items.Refresh(); entirely from the code and I’m not sure why the event is being called almost immediately after the first update.

        2. Yes, this is because I was expecting that OnScrollChangedList will only be triggered when another 33 rows is added by m3(after scrolling all the way down of the initial set of rows loaded).

        3. I tried to log the e.CommandValue & e.CommandValue and I got inconsistent results. Sometimes it would return PAGE, DOWN and sometimes it will be PAGE,PAGE,PAGE,DOWN. I modified the code the remove the handler if either PAGE or DOWN is returned in OnRequested.

        4. Yes, I will remove this so that no unnecessary processing is done once we find WHSL.

        I’ve already removed listView.Items.Refresh(); but still the scrolling event is still being called.

        The goal of the script is if the Location(WHSL) in PPS300 is empty we should update it to GR-PUTAWAY. This functionality works fine when scrolling is not present. However, I have to account for the scenarios listed in OnScrollChangedList. 1 is when the user clicks the button and scrolls down adding another 33 rows(I have to also update these new rows to GR-PUTAWAY). 2 is when the user continues to scroll down and loads another 33 rows(total of 99 by now). 3 is to count the rows without doing the update(user did not click the button yet) and 4 is when the user scrolls further.

        My problem is why loadData still calls the onScrollChangedList even without the listView.Items.Refresh();

        I have updated the code with the link below. Below the code you can also see the logs produced by running it. I will still have to update the code as there are new errors that have surfaced such as the computation of the updRowsAt. Will inform you once I have figured out what’s causing the code to call onScrollChangedList over and over again. Thanks.

        http://pastebin.com/embed_js/XC2SazjH

  6. Doms Molina

    Hi Karin,

    Client informed me that they’ll have to scroll down before pressing the button so the onscroll is not required anymore. I will try to make this work during my free time. Thanks for your help.

    Reply

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s