Validating M3 panels using JScript and MI programs before a request

The scenario for this post is to be able to verify something on a panel in a M3 program before a request is submitted to the M3 BE server. The validation in the example will be made using a MI program that is called asynchronously. The example is not a realistic case but it is easy to test and can be modified and generalized for real business cases. Note that the script is just an example and not intended for production use.

Script overview
The script is intended to be used on the the E-panel in the MMS001 program. When the Requesting event is fired for the current panel the script will only allow some keys to be pressed, including F3,F4,F5 and F12. If any other key is pressed the script will cancel the Requesting event to prevent the request from running and then start the validation. The validation is done by loading user data for the item responsible from the MNS150MI program. The request will be allowed to execute if the facility for the item responsible matches one of the allowed facilities that are defined in the script. In case the facility is not allowed a warning dialog will be displayed for the user.

The functions in the script will be described in more detail below followed by some instructions for how to test the script. The script code can be found in the end of the post.

Init function
The Init function tries to find the item responsible (MMRESP) field and if it cannot be found the script will exit. Event handlers are attached to the Requesting event to be able to cancel requests and to the Requested event so that the event handlers can be disconnected later.

OnRequesting event handler
The script checks if a key has been pressed and if it is one of the allowed keys defined in the validKeys variable (F3,F4,F5,F12). If it is one of the allowed keys the request is allowed. In all other cases the request is cancelled by the setting the Cancel property to true on the CancelRequestEventArgs parameter. The validation is then started unless a validation is already running. The key that was pressed is also stored so that it can be pressed programatically if the validation succeeds.

OnRequested event handler
This function removes the event handlers and also sets the isUnloaded flag to indicate that the script is no longer in a valid state.

StartValidation function
The StartValidation function gets the responsible from the panel, constructs a MIRecord and then uses the MIWorker to call MNS150MI asynchronously. The mouse cursor is also set to busy.

OnResponse event handler
This event handler is always called when the MI program has completed, successfully or not. If there is an error or the script has been unloaded the script will exit.

The facility value is retrieved from the MIRecord and then then checked against the valid facilities in the validFacilities variable. If the facility value is valid the script sets the isValidated flag to indicate this and then uses the PressKey method on the controller to programatically press the key that was stored in the OnRequesting event handler. If the facility was not valid a warning dialog is shown to the user.

SetWaitCursor function
This is a helper function for setting the wait and default cursor for the program window.

Testing the script
1. Start MMS001 and navigate to the E-panel.
2. Open the Script Tool, load the script code or paste it from the web page.
3. Run the script and then click the Next button or the Enter key on the E-panel in MMS001.
4. Unless your user has the facility A01, B01 or C01 defined in MNS150 you should get the warning dialog and the next panel should not be loaded.
5. You can change the values of the validFacilities values in the script to test with different facilities. You can also change your facility in the E-panel in MNS150.

Warning dialog

Script code

import System;
import System.Windows;
import System.Windows.Controls;
import System.Windows.Input;
import Lawson.M3.MI;
import Mango.UI;
import MForms;

package MForms.JScript {
	class ValidateFieldWithMI {
		var controller;
		var debug;
		var pressedKey;
		var textBoxResponsible;
		var isValidating = false;
		var isValidated = false;
		var isUnloaded = false;
		var validKeys = "F3,F4,F5,F12";
		var validFacilities = "A01,B01,C01";

		public function Init(element : Object, args : Object, controller : Object, debug : Object) {
			var content : Object = controller.RenderEngine.Content;
			textBoxResponsible = ScriptUtil.FindChild(content, "MMRESP");
			if (textBoxResponsible == null) {
				debug.WriteLine("Responsible field not found");
				return;
			}

			this.controller = controller;
			this.debug = debug;
			controller.add_Requesting(OnRequesting);
			controller.add_Requested(OnRequested);
			debug.WriteLine("Script initialized");
		}

		public function OnRequesting(sender : Object, e : CancelRequestEventArgs) {
			if (isValidated || (e.CommandType == "KEY" && validKeys.Contains(e.CommandValue))) {
				debug.WriteLine("Request allowed");
				return;
			}
			if (!isValidating) {
				pressedKey = e.CommandValue;
				StartValidation();
			}
			e.Cancel = true;
			debug.WriteLine("Request cancelled");
		}

		public function OnRequested(sender : Object, e : RequestEventArgs) {
			isUnloaded = true;
			controller.remove_Requesting(OnRequesting);
			controller.remove_Requested(OnRequested);
			debug.WriteLine("Script unloaded");
		}

		public function StartValidation() {
			try {
				var responsible = textBoxResponsible.Text;
				debug.WriteLine("Starting validation for responsible " + responsible);
				isValidating = true;
				var record = new MIRecord();
				record["USID"] = responsible;
				MIWorker.Run("MNS150MI", "GetUserData", record, OnResponse);
				SetWaitCursor(true);
			} catch (ex) {
				debug.WriteLine(ex);
			}
		}

		public function OnResponse(response : MIResponse) {
			SetWaitCursor(false);
			if (response.HasError) {
				debug.WriteLine("MIAccess error. " + response.Error.ToString());
				return;
			}

			if (isUnloaded) {
				return;
			}

			var record = response.Item;
			var facility = record["FACI"];
			if (validFacilities.Contains(facility)) {
				debug.WriteLine("The facility " + facility + " is valid, sending " + pressedKey + " key.");
				isValidated = true;
				controller.PressKey(pressedKey);
			} else {
				var header = "The facility " + facility + " is valid not valid";
				var message = "The item responsible " + textBoxResponsible.Text + " must belong to one of the following facilities: " + validFacilities;
				debug.WriteLine(header);
				ConfirmDialog.ShowWarningDialog(header, message, null);
			}
			isValidating = false;
		}

		public function SetWaitCursor(wait) {
			var element = controller.Host.Implementation;
			element.Cursor = wait ? Cursors.Wait : Cursors.Arrow;
			element.ForceCursor = true;
		}
	}
}

21 thoughts on “Validating M3 panels using JScript and MI programs before a request

  1. Pingback: Calling M3 APIs | Developing for Infor Smart Office

  2. Gaston

    Peter,
    This is perhaps what I am looking for. I have a script on OIS101 that shows a text field where users key in a decimal value in order to recalculate the %Discount value but people in Europe can not use it because they use a comma decimal format instead of a period. I was thinkig to call MNS150MI from OIS100 to determine the user’s decimal format in order to replace the periods and commas accordingly. Could you please advice if there is an easier or native way for a script to manage the decimal formats same as LSO?

    textBoxProductDiscount.Text = roundNumber(100 – Double.Parse(textBoxRatio.Text), 2);

    Thank you,
    Gaston

    Reply
  3. Nexhib

    Hello
    I find this description really good. A request, that I have often from Customers is the validating Data (items) in OIS101/B1 before they are entered as positions.
    E.g. I want to check a value from the MITMAS (MMSTCN) befor i Enter my position. after entered my Item and the quantity > ‘Enter’ > check should start now. if the defined value in MMSTCN is not as described in the script, a dialog should pop up. but this validation i would like to restrict only to a few items (only some items should be checked with this script).

    Can someone help me please?

    Reply
  4. Pingback: Second part of international phone number parsing, validation and formatting for Smart Office | M3 ideas

  5. Maxric

    Hi,

    Does it mean that the controller.PressKey(pressedKey) command will launch again the OnRequesting() subroutine (which will do nothing more as isValidated = true) ? What happen if the startValidation() routine is so fast that it has finished before “e.Cancel = true” ?

    Reply
    1. karinpb

      Hi,
      It is not possible for the startValidation() routine to finish before “e.Cancel=true”. startValidation will call a background thread and the UI thread will complete that method before switching to another method on the UIThread and OnResponse from the MIAccess component is on the UIThread. Think of it as the OnResponse is on the UIThread queue waiting for the previous call chain to complete.

      Reply
      1. Maxric

        Thanks for the reply.

        startValidation() is called in a background thread because of the MIWorker (which is asynchronous). If the startValidation() routine was doing nothing more than a debug.WriteLine(“Hello”), you would see “Hello” in the log before the e.Cancel = true i guess.

    2. karinpb

      I can’t answer in your thread below but you wrote:
      “startValidation() is called in a background thread because of the MIWorker (which is asynchronous). If the startValidation() routine was doing nothing more than a debug.WriteLine(“Hello”), you would see “Hello” in the log before the e.Cancel = true i guess.”

      startValidation is not called on a background thread. Notice how it sets the wait cursor – that would result in an exception if startValidation was on a worker thread. The MIWorker.Run will launch call the API on a worker thread and then dispatch back to OnResponse wich will be called on the UIThread.

      Reply
  6. ZaherElShamy

    i have a question ,, how can i close the current program after calling another one Once it Starts,,, i put this line in the INIT method but it doesn’t work ,,,
    controller.PressKey(“F3”);
    ////
    //this code doesn’t close the Current one ,, it only opens my Target Program
    // DLL Script
    String url = “mforms://….
    Uri uri = new Uri(url);
    DashboardTaskService.Manager.LaunchTask(new Task(uri));
    this.controller.PressKey(“F3”);

    thanks

    Reply
    1. norpe Post author

      This will work if you delay closing the current program instance using the dispatcher queue, see the example below.

      import System;
      import System.Windows;
      import System.Windows.Controls;
      import System.Windows.Threading;
      import Mango.UI.Core;
      import Mango.UI.Services;
      import MForms;
      
      package MForms.JScript {
      	class LaunchAndCloseExample {
      		private var controller;
      		public function Init(element : Object, args : Object, controller : Object, debug : Object) {
      			this.controller = controller;
      
      			var uri = new Uri("mforms://CRS610");
      			DashboardTaskService.Manager.LaunchTask(new Task(uri));
      
      			var action : Action = CloseProgram;
      			Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Input, action);
      		}
      
      		private function CloseProgram() {
      			if (this.controller) {
      				this.controller.PressKey("F3");
      				this.controller = null;
      			}
      		}
      	}
      }
      
      
      Reply
  7. tom

    Hi,

    I would like to use event

    controller.add_Requesting(OnRequesting); in CRS610/E

    If I add a new customer in CRS610/E “controller.add_Requesting(OnRequesting);” works.

    But if I would like modify an existing customer in CRS610/E event “controller.add_Requesting(OnRequesting);” will not be fired, only controller.add_Requested(OnRequested) wenn leaving the panel with enter.

    How can I validate in this case the panel, if someone changes the content of a field?

    thanks

    Reply
    1. karinpb

      Hi,
      There must be some other issue with your script. Requesting and Requested are always fired in sequence. If you modify an existing customer Requesting will be fired. You also have an option to check TextChanged on a checkbox as long as you remove all event handlers correctly.

      Reply
  8. Jonas

    Hi,

    I’m trying to tune up a script we have in a M3 program.

    The sequence is:
    User updates pallet dock location for warehouse goods in text boxes in a list.
    User press F18
    F18 triggers an API call to print a pallet label via OnRequesting function and BackroundWorker..

    However, if the user by mistake enter an invalid location in the program and hit F18 the API call is still triggered, but the action for F18 fails in M3 since the value entered is not valid.
    The pallet label is printed containing the wrong information since script was triggered by F18.

    I tried using checks for panelState.MessageId to validate if i got any of the known error messages, but the message is not printed until after OnRequesting and OnRequested are already handled.

    Do i need to validate each value entered via the script, or is there some errorlevel handling for identifying failed requests or even triggering events after the M3 request has been carried out?

    Best regards

    Reply
    1. Karl Geiger

      If you’re using MIAccess for your API call like below in the OnDoWork event code of the BackgroundWorker you can set a response object and then read from it after execution outside of the OnDoWork event.

      MIResponse miResponse = new MIResponse();

      private void OnDoWork(Object sender, DoWorkEventArgs e)
      {
      MIRequest miRequest = new MIRequest();
      MIParameters miParameters = new MIParameters();
      MIRecord miRecord = new MIRecord();
      String[] miResponseFields = new String[] {“MyMiResponseParameter1”};
      miParameters.OutputFields = miResponseFields;
      miRequest.Program = “MYMIPROGRAM”;
      miRequest.Transaction = “MYMITRANSACTION”;
      miRecord.Set(“MyMiReqestParameter1”, “ABCD”);
      miRequest.MIParameters = miParameters;
      miRequest.Record = miRecord;
      this.SetResponse(MIAccess.Execute(miRequest));
      }

      private void SetResponse(MIResponse miResponse)
      {
      this.miResponse = miResponse;
      }

      private MIResponse GetResponse()
      {
      return this.miResponse;
      }

      private void OnRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
      {
      MIResponse miResponse = GetResponse();
      ConfirmDialog.ShowInformationDialogNeverHidden(“API Has Error?”, miResponse.HasError.ToString());
      ConfirmDialog.ShowInformationDialogNeverHidden(“Record Count”, miResponse.Items.Count.ToString());
      ConfirmDialog.ShowInformationDialogNeverHidden(“MyMiResponseParameter1 value”, miResponse.Item.GetString(“MyMiResponseParameter1”));
      }

      Reply
      1. Jonas

        Hi Karl,
        Thanks for that!
        Although your example here shows the MI response.. But I got to your blog and found the post about handling BE messages which seem to be the thing I was searching for.
        It is interesting that you can capture the response xml in that state, I will give it a try.

        In the end I made another solution, I added a flag in an instance cache for the script to check if the expected request was carried out (since the panel reloaded the script after user presses F18) then I validate any panel state message ID for known errors and abort the MI transaction if there would be any errors.

        A glimpse of this:
        If the user press F18, the sessionValue is set to PRINT, when the script loads again it check if there is anything stored in the cache and validates this before triggering the API Call..

        ......
        
        if(InstanceCache.ContainsKey(controller, sessionValueKey))
        				{
        					sessionValue = InstanceCache.Get(controller, sessionValueKey);
        				}
        			Log("SessionValue: " + sessionValue);
        			Log("panelState Messageid: "+ panelState.MessageId);
        			if(sessionValue =="PRINT" && panelState.MessageId != "WTW0402" && panelState.MessageId != "WWS0103"){
        			      Log("Printing Transport Label");
        				  var worker = new BackgroundWorker();  
        			 	  worker.add_DoWork(OnDoWork);  
        				  worker.add_RunWorkerCompleted(OnRunWorkerCompleted);  
        				  worker.RunWorkerAsync();
        				  
        			
        			}
        			sessionValue ="0";
        ......
        
        
  9. Jessika

    Hi,
    I noticed you use the SetWaitCursor. I use that one when exposing CUGEX data on standard panels, while waiting for the MI calls to finish retrieving the data.

    However, I’ve noticed an issue:
    The cursor isn’t returned to its full functionality. After the cursor has returned from spinning wheel to arrow it no longer changes to the hand when hoovering over Expand toolbox (on the right hand side of the panel) or to the double arrows at the edge of the program window (when adjusting the size of the window on full screen canvas). The functionality is still there, but the animation doesn’t change.

    Any ideas on how to solve this?

    Thanks

    Reply
    1. karinpb

      Hi, Sorry for the late reply. I was hoping to get some time to investigate this. I have not seen this behavior and it seems strange that if this happens when we internally change the cursor then we should have seen this error internally as well. I think that the issue is because the initial state isn’t kept. Before changing the cursor you can save the cursor state to that you set it to the initial state instead of to arrow when done.

      previousCursor is a field in the class.

      var previousCursor = null;
      ....
      public function SetWaitCursor(wait) {
      			var element = controller.Host.Implementation;
      			if(wait){
      				previousCursor = element.Cursor;
      				element.Cursor = Cursors.Wait;
      			}else{
      				element.Cursor = previousCursor;
      			}
      			element.ForceCursor = true;
      		}
      

      Then you set the previousCursor back when you are done with the Wait cursor.

      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