Content-aware re-sizing of M3 programs, and text interpolation

Today I’ll illustrate two usability ideas for Infor M3 – content-aware re-sizing of M3 programs, and text interpolation – to make M3 more usable on high resolution screens.

Synopsis

The M3 user interface was built over 20 years ago for AS/400 and 5250 screens of 73 up to 98 columns and 23 rows, that’s about 0.0022 Mega-characters. The M3 user interface has evolved over the years and has stabilized in Smart Office around the same number of columns and rows as 20 years ago with each cell measuring 10 x 22 pixels at 100% zoom level, that’s about 0.5 Megapixels. It’s about the same for Infor H5 Client.

Meanwhile, screen resolutions and pixel density have continued to increase at an incredible pace. Mobile phones are being produced in China with a horizontal resolution of 3,000 pixels (3K). Televisions are being produced in Japan with a horizontal resolution of 8,000 pixels (8K). And the trend will continue. I even started working on a laptop that has a 3K screen, 3200 x 1800, that’s 5.8 Megapixels or a seven-fold increase in available pixels compared to 1024 x 768. The number of pixels on the surface grows to the square of the linear resolution, for example if we multiply by two the screen resolution on each axis from 1600 x 900 to 3200 x 1800 that’s a four-fold increase in surface.

Despite those technological advances, the M3 user interface hasn’t adapted. Here is a screenshot of Smart Office on my laptop with a screen resolution of 3200 x 1800 pixels, showing CRS610/E sized to 1024 x 768 pixels at the top left corner; the result evidently illustrates how the majority of the space is unused:
3200x1800

There are numerous technical challenges to increasing the number of rows and columns of M3 programs, leading to problems in the database and in legacy source code, but there are several steps we can take in that direction.

Two years ago I illustrated the usability idea how to tile windows in Smart Office. Today, I will illustrate two new ideas: content-aware re-sizing of M3 programs, and text interpolation.

Content-aware re-sizing of M3 programs

The first usability idea is to implement content-aware re-sizing of M3 programs with inspiration from seam carving as introduced by Shai Avidan and Ariel Shamir in 2007. You can watch a demonstration of seam carving in the authors video here. The idea of seam carving is to calculate the energy function of neighboring pixels and remove paths of lowest energy in a way that respects the overall composition of the image. We can manipulate the energy values to keep certain pixels and discard others.

To try seam carving, you can use the Content Aware Scaling feature in Adobe Photoshop, or the Liquid Rescale plugin for Gimp, or the online tool RSIZR.

Here is an example of CRS610/E in RSIZR; I used the preserve and remove brushes to mark in green the important portions of CRS610/E to keep and in red the portions to discard:
mask

Previous known techniques to re-size windows include re-scaling but it causes cropping and scrollbar hell, and re-sizing but it causes the image to be squooshed and minimized. The ideal solution is re-targeting with content-aware re-sizing.

Here is a video of the result using a screenshot of CRS610/E in RSIZR:

It’s not possible to simply implement seam carving to M3 because the rendering of the M3 user interface is based on text and not on pixels. Yet, we can approximate an implementation by choosing which parts of the user interface we want to keep and which parts we can discard. We can do this with a hierarchy of importance. And we can do this programmatically. The new M3 user interface does something similar with the adaptable panel sequence ribbon.

Text interpolation

The second usability idea is text interpolation in M3 programs for text like column headers and labels.

M3 is translated in multiple languages by professional translators. You can read more about it in the two blog posts: Translate M3 with Google Translate API, and Write a Mashup in multiple languages.

The language constants are chosen in different widths to fit the varying space of the target medium. The size ids are: 03, 05, 10, 15, AA, C0, C1, C2, CA, and CF.

The text is retrieved at run-time based on the user’s language settings in MNS150.

The Java source code of each M3 program maps the fields and message identifiers. For example CRS610DSP.java:

static final String[] fieldNames={
"X0RCID","PXFKEY","WWCLIN","WWCPOS","WBOPT2","S0SFH","WWCOLN","WWPSEQ","WOPAVR","WOUPVR",
"WWQTTP","WWNFTR","WWAGGR","WXSLCT","WFSLCT","WTSLCT","WXSLC2","WFSLC2","WTSLC2","WXSLC3",
"WFSLC3","WTSLC3","WXTXT1","WXTXT2","WXTXT3","WXTXT4","WXTXT5","WXTXT6","WXTXT7","WXTXT8",
...
"WRGEOC","WRTECN","WRTEEC","WRTAXC","WTTAXC","WRAGBP","WRAGPY","WRACLB","WRAACB","WRAGCP",
"WRAGAC","WRAGPN","WRAGBG","WRAGPG","WRAGTD","OKAGTN","WRAGCA","WTRTEP","WRRPLT","WRRDIS",
"WRRTEP","WRRSMC","WRAOTP","WTRPLT","WTRDIS","WTRSMC","WTAOTP","PXPDSE","WWSPIC","WWDSEQ",
"WWCFEN","WWTPIC","WWTXVR","WWLNCD"};

static final String[] fieldMessageIds={
null,null,null,null,null,null,null,null,null,null,
null,null,null,null,null,null,null,null,null,null,
null,null,null,null,null,null,null,null,null,null,
...
"WGE10","WTE26",null,"WTA39","WDE03","WAGB0","WAG69","WAC95","WAACB","WAG78",
"WAG70","WAG73","WAG77","WAG76",null,"WAG75","WAG72","WDE03","WRPLT","WRDIS",
"WRTEP","WRSMC","WAT90",null,null,"WDE03",null,null,null,null,
null,null,null,null};

For example, here are the identifiers of some common fields in CRS610:

  • Customer name – CUNM: WNA01
  • Customer number – CUNO: WCU02
  • Telephone number 1 – PHNO: WPH01

We can get the values in a Smart Office script with:

function ... {
    var list: ArrayList = new ArrayList();
    list.Add(new TranslationItem(messageId + width, "MVXCON"));
    TranslationService.Current.Translate(list, OnTranslation, "GB");
}
function OnTranslation(items: IEnumerable) {
    for (var item: TranslationItem in items) {
        if (!String.IsNullOrEmpty(item.Text)) {
            if (item.Text != item.Key) {
                debug.WriteLine('Translated ' + item.Key + ' to ' + item.Text);
            } else {
                debug.WriteLine('Key ' + item.Key + ' not found in file ' + item.File);
            }
        } else {
            debug.WriteLine('item.Text is null or empty');
        }
    }
}

That calls the Net Extension command TRANSLATE.

The result would be for example:

PHNO[WPH0103]=Tl1
PHNO[WPH0105]=Tel 1
PHNO[WPH0110]=Tel no 1
PHNO[WPH0115]=Telephone no 1
PHNO[WPH01AA]=telephone number 1

We can then interpolate the resulting Strings, for example my simple algorithm returns:


Tl1
Tel1
Tel 1
Tel n1
Tel nb1
Tel nb 1
Tele nb 1
Telep nb 1
Teleph nb 1
Telepho nb 1
Telephon nb 1
Telephone nb 1
Telephone nub 1
Telephone numb 1
Telephone numbe 1
Telephone number 1

Here’s the simple algorithm I used for that:

/*
    Returns the specified headers interpolated one character at a time, first ocurrence of any different character, from left-to-right.
*/
function interpolate(headers) {
    var interpolated = new ArrayList();
    var str1;
    var str2;
    for (var i = 0; i < headers.length - 1; i++) {
        str1 = headers[i];
        str2 = headers[i + 1];
        interpolated.Add(str1);
        for (var j = 0; j < (str2.length - 1) && str1 != str2; j++) {
            if (j < str1.length) {
                if (str1[j] != str2[j]) {
                    // the character at this position of str2 doesn't exist in str1, add it to str1
                    str1 = str1.Insert(j, str2[j]);
                    interpolated.Add(str1);
                }
            } else {
                // we reached the tail of str1, add the tail of str2 to str1
                str1 = str1.Insert(j, str2[j]);
                interpolated.Add(str1);
            }
        }
    }
    interpolated.Add(str2);
    return interpolated;
}

It turns out the algorithm is not that simple to implement as not all the language constants in M3 can be automatically interpolated, for example the abbreviation no (letter O) would have to be specially interpolated to number (no letter O).

Here is a resulting video of column header interpolation in CRS610/B; I manually sanitized the interpolated Strings and added the field name in parenthesis:

Summary

In this post I illustrated two usability ideas – content-aware re-sizing of M3 programs, and text interpolation – to benefit from the technological advances in ever higher resolution screens and to improve the M3 user interface. This would be specially ideal for large Mashups.

 

That’s it! If you liked this post, please click the Follow button to subscribe to this blog, leave your comments in the section below, click Like, and share with your peers.

 

Third part of international phone number parsing, validation and formatting for Smart Office

I just completed implementing international phone number parsing, validation and formatting for Accounts in Infor Customer Lifecycle Management (CLM) in Infor Smart Office. This complements my previous work on international phone number parsing, validation and formatting for M3 MForms like CRS610/E.

To implement the solution I used libphonenumber (C#), the known C# port of “Google’s phone number handling library, powering Android and more”, I used the Infor Smart Office SDK, and I did a lot of hacking CLM.

Here is the result for an invalid phone number:
1

Here is a phone number entered in various non-normalized formats before saving the Account:
2

Here are the resulting normalized phone numbers after having saved the Account:
3

Here is a video of the result (watch in full-screen HD for better viewing):

This solution works for saving new accounts and for updating existing accounts. From a development point of view, I had to respectively handle the event OnEntityBeforeSave for a modal dialog box and the event OnEntityBeforeUpdate for a dashboard task.

Note this solution will only apply to the CLM user interface in Smart Office and not to other CLM user interfaces like CLM Web client, it will not apply retro-actively to phone numbers previously stored in the database, and it will not apply to phone numbers eventually synchronized in the background server-to-server from other sources like CRS610; those scenarios would need to be handled separately.

 

That’s it! If you like it, click the Follow button below to subscribe to this blog, let me know what you think in the comments below, and spread the word.

Second part of international phone number parsing, validation and formatting for Smart Office

I just implemented international phone number parsing, validation and formatting for all MForms in Smart Office as an implementation of my previous post for a customer that needed to enforce this validation rule, and I will share my findings here with you.

Overview

I used libphonenumber-csharp, the known C# port of “Google’s phone number handling library, powering Android and more”.

I integrated it with Smart Office SDK as an MForms extension with Global scope so it applies to all MForms programs that have phone number fields, for instance CRS610, CRS620, and OIS002. For that I followed Peter’s post: Introduction to MForms extensions.

Then, if the phone number is not valid, I show an error message in MForms, I set focus in the corresponding input field, and I cancel the user request. For that I followed Peter’s other post: Validating M3 panels using JScript and MI programs before a request.

And if the phone number is valid, I re-format it in E.164 phone number format.

Then, I deployed it globally to all users via Infor LifeCycle Manager (LCM).

Now users have to enter valid phone numbers in MForms or they will get an error message that will prevent them from moving forward.

Hunt for the phone number fields

In order to make it work for all MForms, I had to determine what is the set of M3 programs, panels, and phone number fields.

A quick scan in the XML View Definitions gives:

  • 4,260 programs (E:\M3BE\MVX\15.1\base\viewdefs>dir *.xml /s)
  • 72,212 panels (findstr /c:”\<Panel” /s *.xml)
  • 144,377 fields (findstr /c:”\<EntryField” /s *.xml)

That’s too big of a space to search exhaustively.

Then, I did a quick search in MetaData Publisher for the strings: phone, facsimile, fax, and mobile. Here is a screenshot:
1.1_

There were 173 results for phone including telephone, the search is case insensitive, 72 results for facsimile, no results for mobile, and results for fax with type checkbox and text that are unrelated to our problem at hand. That narrows down the search space to only 245 fields.

I merged both result sets, I removed the field name prefixes to keep only the radices, and I eliminated duplicates, and that further narrowed down the search space to only nine fields: APHN, CAPH, CPHN, GPNO, PHN1, PHN2, PHNO, SPHN, and TFNO. Here is a screenshot:
5.1

Then, I did the reverse search in MDP to verify that every field is a phone number, to prove the space is bijective, and I realized there are three fields that end in TFNO – they are CPTFNO, PPTFNO, and SPTFNO – that are not phone number fields. We can eliminate those fields by looking up the type and length of the field: it must be String of length 16.

Thus, the resulting set of all phone number fields across all of M3 is the following:

  • APHN
  • CAPH
  • CPHN
  • GPNO
  • PHN1
  • PHN2
  • PHNO
  • SPHN
  • TFNO String[16]

A quick verification by scanning the View Definitions for those fields shows the following M3 Programs: APS095, APS200, ARS025, ARS115, ARS175, ARS200, ARS360, ARS390, CBS020, COS105, CRS435, CRS530, CRS538, CRS605, CRS609, CRS610, CRS620, CRS623, CRS690, CRS691, CRS739, CRS949, CSS204, CSS205, DRS013, GMS090, GMS200, LTS100, LTS101, MHS813, MHS850, MHS890, MMS005, MMS453, MNS100, MNS150, MNS205, MNS212, MOS156, MOS272, MOS295, MSS225, MTS201, MWS098, MWS099, MWS212, OIS002, OIS054, OIS055, OIS056, OIS102, OIS269, POS010, PPS171, PPS200, PPS360, PPS370, PPS390, QQS001, QUS095, QUS100, QUS112, RMS421, RSS103, RSS303, SAS002, SOS100, SOS101, SOS102, SOS105, SOS106, SOS110, SOS165, SOS375, SOS378, SOS435, SOS485, SOS520, SOS650, SOS972, SPS200, STS050, STS100, STS101, STS201, TXS100, TXS130, TXS140, TXS510. I recognize CRS610, CRS620, and OIS002 so I’m confident.

My approach is heuristic and is not guaranteed to be exact.

Source code

Here is the source code in C#:

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using libphonenumber;
using MForms;
using MForms.Extension;

namespace PhoneNumberValidation
{
    public class PhoneNumberValidationExtension : IPanelExtension
    {
        private static String validKeys = "F3,F4,F5,F12";
        private static String phoneNumberFields = "APHN,CAPH,CPHN,GPNO,PHN1,PHN2,PHNO,SPHN,TFNO";
        private static readonly log4net.ILog Logger = Mango.Core.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        public void Load(PanelExtensionEventArgs e)
        {
            try
            {
                InstanceController controller = (InstanceController)e.Controller;
                controller.Requesting += OnRequesting;
                controller.Requested += OnRequested;
            }
            catch (Exception ex)
            {
                Logger.Error("Failed to handle extension event", ex);
                throw new ApplicationException();
            }
        }

        void OnRequesting(Object sender, CancelRequestEventArgs e)
        {
            try
            {
                if (e.CommandType == "KEY" && validKeys.Contains(e.CommandValue))
                {
                    // Request allowed
                    return;
                }
                // validate all phone numbers in this panel
                InstanceController controller = (InstanceController)sender;
                Grid content = controller.RenderEngine.Content;
                IList<FrameworkElement> controls = controller.RenderEngine.Controls;
                foreach (FrameworkElement control in controls)
                {
                    if (control is System.Windows.Controls.TextBox)
                    {
                        TextBox txtbox = (TextBox)control;
                        String baseName = txtbox.Name.Substring(2);
                        bool IsPhoneNumberField = phoneNumberFields.Contains(baseName) && txtbox.MaxLength == 16;
                        if (IsPhoneNumberField)
                        {
                            if (txtbox.Text != "")
                            {
                                try
                                {
                                    PhoneNumber number = PhoneNumberUtil.Instance.Parse(txtbox.Text, RegionInfo.CurrentRegion.Name);
                                    if (number.IsValidNumber)
                                    {
										txtbox.Text = number.Format(PhoneNumberUtil.PhoneNumberFormat.E164);
                                    }
                                    else
                                    {
                                        controller.RenderEngine.ShowMessage("The phone number is not valid.");
                                        txtbox.Focus();
                                        e.Cancel = true;
                                    }
                                }
                                catch (com.google.i18n.phonenumbers.NumberParseException ex)
                                {
                                    Logger.Debug(ex.Message);
                                    controller.RenderEngine.ShowMessage(ex.Message);
                                    txtbox.Focus();
                                    e.Cancel = true;
                                }
                            }
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Logger.Debug(ex.ToString());
            }
        }
        void OnRequested(Object sender, RequestEventArgs e)
        {
            try
            {
                InstanceController controller = (InstanceController)sender;
				// clean-up
                controller.Requesting -= OnRequesting;
                controller.Requested -= OnRequested;
            }
            catch (Exception ex)
            {
                Logger.Debug(ex.ToString());
            }
        }
    }
}

Result

Here is the result.

Here is a sample user input with phone numbers formatted incorrectly:
b1

Here is the result after parsing, validation, and formatting:
b2

And here is an invalid input and the error message in the status bar:
b3

Future work

As future work, I would like to use ValidationRule which Infor already uses for Infor Document Archive.

 

That’s it! Check out my series on telephony. And as usual, subscribe, comment, share, and enjoy.

 

AsYouTypeFormatter for Smart Office

In this post I will introduce a proof-of-concept of AsYouTypeFormatter for Smart Office. AsYouTypeFormatter is used to “format phone numbers on-the-fly when users enter each digit.” It’s part of the open source library libphonenumber, “Google’s phone number handling library, powering Android and more.” This post complements my previous post where I discussed International phone number parsing, validation and formatting for Smart Office.

AsYouTypeFormatter

In addition to parsing, validation, and formatting, libphonenumber has a nice AsYouTypeFormatter that formats the phone number as the user types it.

You can test it with the Phone Number Parser Demo. Here’s a screenshot:
AsYouTypeFormatter

Standard Smart Office without AsYouTypeFormatter

Here is a demo of entering a phone number in the field WRPHNO in M3. Customer Open – CRS610/E in standard Smart Office (without AsYouTypeFormatter); the phone number is not validated nor formatted:

Smart Office with AsYouTypeFormatter

And here’s the same demo with AsYouTypeFormatter that’s formatting the phone number on-the-fly as I enter each digit (I typed only the digits):

Here is the complete source code for that demo:

import System;
import System.Collections;
import System.Windows.Controls;
import libphonenumber;
import MForms;

package MForms.JScript {
	class Test {
		var debug;
		var formatter: AsYouTypeFormatter = PhoneNumberUtil.Instance.GetAsYouTypeFormatter("US");
		var textboxes: ArrayList = new ArrayList();
		var isTextChanging: boolean = false; // to avoid infinite loop in OnTextChanged
		public function Init(element : Object, args : Object, controller : Object, debug : Object) {
			try {
				// save global variables
				this.debug = debug;
				// attach to the phone fields
				var content = controller.RenderEngine.Content;
				var supportedPhoneFields: String[] = ["WRPHNO", "WRPHN2", "WRTFNO"];
				for (var i: int in supportedPhoneFields) {
					var fieldName: String = supportedPhoneFields[i];
					var textbox: TextBox = ScriptUtil.FindChild(content, fieldName);
					if (textbox != null) {
						textboxes.Add(textbox);
						textbox.add_TextChanged(OnTextChanged);
					}
				}
				controller.add_Requested(OnRequested);
			} catch (ex: Exception) {
				debug.WriteLine(ex);
			}
		}
		/* User is typing */
		function OnTextChanged(sender: Object, e: TextChangedEventArgs) {
			try {
				if (!isTextChanging) {
					var textbox: TextBox = sender;
					if (textbox.Text.Length > 0) {
						// format the phone number as the user is typing it
						var newChar: char = textbox.Text.Substring(textbox.Text.Length - 1);
						var newText: String = formatter.InputDigit(newChar);
						isTextChanging = true;
						textbox.Text = newText;
						textbox.CaretIndex = textbox.Text.Length;
						isTextChanging = false;
					}
				}
			} catch (ex : Exception) {
				debug.WriteLine(ex);
			}
		}
		/* Clean-up */
		function OnRequested(sender: Object, e: RequestEventArgs) {
			try {
				if (sender != null) {
					sender.remove_Requested(OnRequested);
				}
				for (var textbox: TextBox in textboxes) {
					if (textbox != null) {
						textbox.remove_TextChanged(OnTextChanged);
					}
				}
			} catch (ex : Exception) {
				debug.WriteLine(ex);
			}
		}
	}
}

Limitations and future work

But according to this thread, AsYouTypeFormatter doesn’t support the backspace key, nor emptying the field, nor replacing a selection, nor inserting text somewhere in the middle. The solution is to handle all the cases ourselves in code. All these are already implemented in android.telephony.PhoneNumberFormattingTextWatcher. There is a partial port of Android to C# in XobotOS, “a Xamarin research project that explored porting Android 4.0 from Java/Dalvik to C#”. So to properly implement AsYouTypeFormatter in Smart Office we would need to combine libphonenumber-csharp and XobotOS.

 

That’s it! That was my proof-of-concept demo of AsYouTypeFormatter for Smart Office to format phone numbers in M3 Programs as the user is typing the digits.

Like, share, comment, enjoy.

/Thibaud

 

International phone number parsing, validation and formatting for Smart Office

Today I will introduce a simple solution to do international phone number parsing, validation and formatting in Infor Smart Office. The goal is to validate phone numbers entered by users in Infor M3 Programs such as M3 Customer – CRS610/E, or in Infor Customer Lifecycle Management (CLM) Account Details, against international phone number specifications, and to get the resulting phone number in any of the desired output formats: E.164, international, national, and RFC3966. For that, I will use libphonenumber, “Google’s phone number handling library, powering Android and more”, and more specifically I will use libphonenumber-csharp, the known port for C#.

Examples

Here are some examples of parsing, validation and formatting of a US phone number:

  • Valid phone number: 415 535 5452
  • Invalid phone number: 415 535 545222
  • Country code: 1
  • Phone Number region: US
  • Number type: FIXED_LINE_OR_MOBILE
  • E.164 format: +14155355452
  • International format: +1 415-535-5452
  • National format: (415) 535-5452
  • RFC3966 format: tel:+1-415-535-5452
  • Format for out-of-country calling from France: 00 1 415-535-5452

Why does it matter?

Phone number parsing, validation and formatting may be important in some scenarios.

For instance, for one of my customers, I’m integrating Cisco IP Communicator and Cisco Agent Desktop with CLM in Smart Office such that when customer service representatives receive incoming phone calls from their customers, Smart Office automatically searches for that incoming phone number in CLM and displays a list of possible matches. Then, the user can select the desired match and open the corresponding CLM Account Details. It saves precious time during the call. I wrote a previous post about it with some preliminary findings.

Conversely, users can click a phone number in CLM to make that outgoing phone call.

To implement that programmatically, how do we match the phone number of the incoming call with the phone numbers entered by users in CLM? Cisco Agent Desktop returns the ANI of incoming phone numbers as format 4155355452. What if the user entered the phone number in CLM as format (415) 535-5452? What if another user entered a duplicate record in CLM as format 415-535-5452? What if a user entered the phone number in CRS610 as format +14155355452? Also, for outgoing calls Cisco Agent Desktop will accept phone numbers as format 14155355452. That’s five different formats for the same phone number, and it requires record linkage.

That’s why it’s crucial to normalize the phone numbers so we can compare them.

Also, M3 and CLM synchronize their records with each other via Event Hub. So we have to validate entries on both sides or they would risk polluting each other.

The solution is to do phone number validation and formatting at user input so the user has a chance to enter the correct phone number. But doing it at the user interface level alone is not sufficient. We would also need to cover the other entry points such as M3 API, M3 Web Services of type M3 Display Program (MDP), and REST/SOAP Web Services. Also, as a reminder, we never do direct data entry in the database with SQL CREATE/UPDATE/DELETE as that could potentially break the integrity of the system, so we don’t need to cover that side.

Insufficient solutions

A naive solution to search a record by phone number is to select all records that match the input without normalization.

For example, searching CLM with SQL could be: SELECT AccountID FROM LCLM.Account WHERE Phone=’4155355452′. But that will fail to find the alternate valid numbers (415) 535-5452 and 415-535-5452. And we would have to protect it against SQL injection attacks.

Another naive solution is to strip all non-digit characters and count the resulting number of characters. For example, phone numbers in the United States have 10 digits, so the valid phone number (415) 535-5452 would be correctly validated, but the alternate valid phone number +1 (415) 535-5452 would incorrectly be rejected whereas it’s valid. We could improve the solution and say we now accept 11 digits, but then the valid French phone number +33 6 15 62 07 51 would incorrectly be validated as a US phone number, which is not true.

We could go further and restrict the space of phone numbers to only a specific country, say United States and use the North American Numbering Plan (NANP). But that will just temporarily buy time as there will likely be a need to support international phone numbers at a later point in the future.

Going further, we could use regular expressions. For example, according to this Microsoft pattern & practices document, the regular expression ^[01]?[- .]?(\([2-9]\d{2}\)|[2-9]\d{2})[- .]?\d{3}[- .]?\d{4}$ “Validates a U.S. phone number. It must consist of 3 numeric characters, optionally enclosed in parentheses, followed by a set of 3 numeric characters and then a set of 4 numeric characters.” Unfortunately, that will not validate a valid phone number such as +14155355452; we could improve the regular expression to validate the international prefix +1. Also, it will only validate NANP phone numbers, not international numbers; we could add more regular expressions for other countries.

Also, what about legitimate phone numbers with extensions like (415) 535-5452#1738 ?

We could iteratively improve the solutions, adding more tests and fixes, but it will unfortunately prove to be insufficient unless we spend a tremendous amount of effort. It’s like trying to implement ones own library of time and time zones, or trying to implement ones own cryptographic library.

Advantages of using a known library

libphonenumber is “Google’s common Java, C++ and Javascript library for parsing, formatting, storing and validating international phone numbers. The Java version is optimized for running on smartphones, and is used by the Android framework since 4.0 (Ice Cream Sandwich).”

Using proven robust open source libraries is always a good choice. This library is used by the billion Android phones on the market, so that tells something about its robustness and correctness. And it supports Java and JavaScript so we could use it in M3 Business Engine (Java) and H5 Client (JavaScript). And there is a port for C# so we can also use it in Smart Office, which I will.

Examples for Smart Office

I will use libphonenumber-csharp in a Smart Office script.

For that, I need to create a new feature in Smart Office SDK to add a reference to libphonenumber.dll and to deploy it with ClickOnce. I had originally tried simply using System.Reflection.Assembly.LoadFrom(String), but I couldn’t get it to work.

Once I have a reference to the assembly, I validate the phone number with:

import libphonenumber;
...
var number: PhoneNumber = PhoneNumberUtil.Instance.Parse(phoneNumberStr, "US");
if (number.IsValidNumber) {
    // valid
} else {
    // invalid
}

Then, I format the phone number with:

number.Format(PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL)
number.Format(PhoneNumberUtil.PhoneNumberFormat.NATIONAL)
number.Format(PhoneNumberUtil.PhoneNumberFormat.E164)
number.Format(PhoneNumberUtil.PhoneNumberFormat.RFC3966)
number.FormatOutOfCountryCallingNumber("US")
number.FormatOutOfCountryCallingNumber("FR")

Example for M3 Programs

The sample source code to validate and format phones numbers in M3 Programs is the following:

import System;
import libphonenumber;

package MForms.JScript {
    class Test {
        public function Init(element : Object, args : Object, controller : Object, debug : Object) {
            var number: PhoneNumber = PhoneNumberUtil.Instance.Parse(element.Text, "US");
            if (number.IsValidNumber) {
                debug.WriteLine(number.Format(PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL));
                debug.WriteLine(number.Format(PhoneNumberUtil.PhoneNumberFormat.NATIONAL));
                debug.WriteLine(number.Format(PhoneNumberUtil.PhoneNumberFormat.E164));
                debug.WriteLine(number.Format(PhoneNumberUtil.PhoneNumberFormat.RFC3966));
                debug.WriteLine(number.FormatOutOfCountryCallingNumber("US"));
                debug.WriteLine(number.FormatOutOfCountryCallingNumber("FR"));
            }
        }
    }
}

Here is a sample screenshot of the validation and formatting of the field Telephone no 1 (WRPHNO) in M3 Customer. Open – CRS610/E:

2

Example for CLM

My sample source code to validate and format phones numbers in CLM Accounts is the following:

import System;
import System.Windows;
import lclmControls.Classes.UI;
import lclmControls.Common;
import lclmControls.Custom;
import Mango.Services;
import Mango.UI.Core;
import Mango.UI.Services;
import libphonenumber;

package MForms.JScript {
	class Test {
		public function Init(element : Object, args : Object, controller : Object, debug : Object) {
			try {
				var runners: RunnerCollection = DashboardTaskService.Manager.ExecutingTasks();
				var runner: IRunner = runners[4]; // I'm Feeling Lucky
				var task: ITask = runner.Task;
				var view: TabularDetailsView = task.Parameter;
				var detailsView: DetailsView = view.DetailsView;
				var baseDialog: BaseDialog = detailsView.BaseDialog;
				var groups: DataGroup[] = baseDialog.DataGroups;
				var group: DataGroup = groups[0]; // I'm Feeling Lucky
				var sections: DataSection[] = group.GetSections();
				var section: DataSection = sections[0]; // I'm Feeling Lucky
				var dataField: DataField = section.GetField("Phone");
				var dataEditor: FrameworkElement = dataField.DataEditor;
				var txtbox: SingleLineTextBox = dataEditor;
				var number: PhoneNumber = PhoneNumberUtil.Instance.Parse(txtbox.Text, "US");
				if (number.IsValidNumber) {
					debug.WriteLine(number.Format(PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL));
					debug.WriteLine(number.Format(PhoneNumberUtil.PhoneNumberFormat.NATIONAL));
					debug.WriteLine(number.Format(PhoneNumberUtil.PhoneNumberFormat.E164));
					debug.WriteLine(number.Format(PhoneNumberUtil.PhoneNumberFormat.RFC3966));
					debug.WriteLine(number.FormatOutOfCountryCallingNumber("US"));
					debug.WriteLine(number.FormatOutOfCountryCallingNumber("FR"));
				}
			} catch (ex : Exception) {
				debug.WriteLine(ex);
			}
		}
	}
}

Here is a sample screenshot of the validation and formatting of the field Phone in a CLM Account Details View:
4

This is a proof-of-concept source code for demonstration purposes with “I’m feeling lucky” about the array indexes and not checking if object references are null. I will let the reader write the proper, and more lengthy code.

Completeness

For completeness, a robust solution would need to cover all entry points:

For M3 Programs:

  • MForms in Smart Office
  • MForms in H5 Client
  • M3 API (MvxAPI protocol, REST, and SOAP)
  • M3 Web Services (MWS) of type M3 Display Program (MDP) (REST and SOAP)

For the M3 UI, the solution would involve a combination of Smart Office Scripts in .NET, and H5 Client Web Parts in JavaScript. And for the backend, it would be M3 Java modifications with MAK; using Event Hub would be too late.

For CLM:

  • CLM in Smart Office
  • CLM-Web
  • CLM REST Web Services
  • CLM SOAP Web Services

For the CLM UI, the solution would involve the same combination of Smart Office Scripts in .NET, and H5 Client Web Parts in JavaScript. And for the backend, I’m not a CLM expert but I heard database triggers would do it.

Also, we would need to do retro- validation and formatting of phone numbers that were already entered in the M3 Programs and CLM.

That’s a lot of work.

My wish

My wish is that Infor Product Development implements phone number validation standard into Smart Office. Same for address validation in M3.

Conclusion

In this article I introduced my simple solution to do phone number parsing, validation and formatting for M3 Programs and CLM Accounts in Smart Office using the proven open source library libphonenumber and its known port for C#. I also explained why parsing, validation and formatting of phone numbers matters in some cases. I implemented a demo for M3 Programs and one for CLM Account Details. I also presented my thoughts on insufficient solutions. Then, I discussed what a complete solution would look like.

In my next article, I will present a proof-of-concept of AsYouTypeFormatter to format a phone number as the user is typing the digits.

That’s it!

If you liked this, please Follow the blog by clicking the button below, and let us know your comments in the section below. And share this with your colleagues, customers and partners to grow the community. Or become an author and share your ideas here. Or be a ninja and create your own blog!

/Thibaud

Open source release of Address Validation for M3

I’m please to announce I finally released the first free and open source version of the Address Validation for M3, and I published the code on GitHub at https://github.com/M3OpenSource/AddressValidation . It’s a combination of a Mashup and a Script for Infor Smart Office, so it’s easy for you to modify, deploy, and run. Currently, it only supports the Google Geocoding API with more address providers to be added later.

I had implemented the first proprietary version of the script for Lawson Software in 2009. Then I had implemented several variants for various customers while working at Lawson Software and Infor. I’ve always wanted it to become available to more customers, so I decided to make it free and open source. But in order to not infringe any intellectual property and copyrights over the previous source code, I had to re-write everything from scratch. The opportunity came up when I quit Infor, and before I joined Ciber, in between the two, so there was no question on the ownership of the source code, and I had made an announcement. It took me a while but here it is. And I made some improvements over to the previous proprietary code.

Self-configuration

The script is self-configurable for the following M3 Panels, i.e. just add the script to one of the supported M3 Panels and execute, without any modifications nor arguments:

  • Customer. Open – CRS610/E
  • Customer. Connect Addresses – OIS002/E
  • Supplier. Connect Address – CRS622/E
  • Customer Order. Connect Address – OIS102/E
  • Internal Address. Open – CRS235/E1
  • Company. Connect Division – MNS100/E
  • Ship-Via Address. Open – CRS300/E
  • Service Order. Connect Delivery Address – SOS005/E
  • Shop. Open – OPS500/I
  • Bank. Open – CRS690/E
  • Bank. Connect Bank Branch Office – CRS691/E
  • Equipment Address. Open – MOS272/E

Deploy locally and test

To start using the script, download the Mashup’s Manifest and XAML and the Script from the GitHub repository. Save the three files somewhere temporary on your computer. Then, install the Mashup locally using the Mashup Designer at designer://mashup (watch the video below), and run the Script with the Script Tool at mforms://jscript (watch the video below). Then, enter an address in M3, click the validate button (the little globe icon), you can also use the TAB key from Address line 1 to focus the button and press SPACE to click it, then the Mashup will pop-up with a list of possible matches, and select an address by pressing ENTER or double-clicking the address.

Screenshots

Here are some screenshots:

1 2 3 4 5

Videos

Here are some videos (watch in full-screen and high-definition for better view):

  • How to deploy the Mashup locally
  • How to test the Script
  • Sample address searches:

Then, you can deploy the Mashup globally with LifeCycle Manager, and set the script to everybody with the Smart Office Personalization Manager in the Navigator widget > Administration tools.

Future work

There’s still more work to do. For instance, it appears the Google Geocoding API doesn’t follow the same format for all addresses, they’re local to each country, so right now we have to manually change the address layout based on the country, and I would like to improve that.

Also, I want to add a WebBrowser control to show the addresses in Google Maps.

Also, this first release only supports the Google Geocoding API. I want to add support for other address providers, like Experian QAS, FedEx, Microsoft Bing Maps, UPS, United States Postal Service, and local address providers like Pages Jaunes in France, and Eniro in Sweden.

If you like it, join the project and become a contributor!

Thibaud Lopez Schneider

Progress indicator adorner

Last week in Stockholm norpe showed me how to add a progress indicator Adorner to a ListView when I call an M3 API in a background thread in a Personalized Script for Lawson Smart Office. When I execute a time consuming operation I like to maintain good usability by indicating activity to the user. For that, I used to display a message to the user in a Label like “Loading please wait…”, but that doesn’t catch the eye very well, and/or I used to changed the mouse cursor to Cursor.Wait. but I think that’s a shared resource. So I prefer this new technique.

It’s only two lines of code from Mango.UI.Controls:

ProgressIndicatorAdorner.AddAdorner(element, useOpacity, scaleFactor);
ProgressIndicatorAdorner.RemoveAdorner(element);

Here’s the method’s signature:
Reflector

Here’s the result:
1_ 2_

That’s it.

How to tile windows in Smart Office

Here is a solution to tile M3 programs in Smart Office. It is a Tiling window manager for Smart Office with automatic scaling, placement, and arrangement of windows, for example to organize M3 programs horizontally across the screen.

This solution is useful for example to put side by side two programs that a user might often use, for example Customer Order. Open Toolbox – OIS300 to see all the orders in M3, and Batch Customer Order. Open – OIS275 to see problems with those orders. A user might want to put the two windows side by side to monitor the orders. If a user does that every day, she might want a solution to tile the programs automatically. This solution will enhance the user experience and will contribute to increase user productivity.

First, we get a reference to the window with:

var window = controller.Host.Implementation; // Mango.UI.Services.EmbeddedHostWindow

Then, we de-iconify the window with:

window.ActivateWindow(true);

Then, we scale the window in pixels, for example:

window.Width = 1280;
window.Height = 800;

Then, we scale the window relative to the main Smart Office window – which is given by controller.ParentWindow – for example to half the width and full height of the screen:

window.Width = controller.ParentWindow.Width / 2;
window.Height = controller.ParentWindow.Height;

Then, we position the window horizontally and vertically in pixels on the screen by using Mango.UI.Services.DashboardService, for example:

DashboardService.Window.SetPosition(new Point(100, 20), window); // x, y

Then, we get a list of the M3 programs that are currently running – we use MainController for that – and we get a reference to each window:

var instances = MainController.Current.GetInstances(); // System.Collections.Generic.IList<IInstanceController>
for (var i: int = 0; i < instances.Count; i++) {
    var controller_: Object = instances[i]; // MForms.IInstanceController
    var window = controller_.Host.Implementation;
}

If we want to tile the windows horizontally, we scale each window’s width respective to the total number of windows. For example, if there are three windows on the screen, each window will occupy a third of the screen:

window.Width = controller_.ParentWindow.Width / instances.Count;
DashboardService.Window.SetPosition(new Point(window.Width * i, 0), window);

If we want to tile two specific M3 programs, we can find them by their name, and tile them accordingly. For example, here I position OIS275 to the left, and OIS300 to the right:

var name = controller_.RenderEngine.PanelHeader;
if (name.Equals('OIS275/B1')) {
    DashboardService.Window.SetPosition(new Point(0, 0), window); // leftelse if (name.Equals('OIS300/B')) {
    DashboardService.Window.SetPosition(new Point(controller_.ParentWindow.Width - window.Width, 0), window); // right
}

Here is my full source code to automatically tile all the windows horizontally:

import System.Windows;
import Mango.UI.Services;
import MForms;

package MForms.JScript {
	class TileHorizontally {
		public function Init(element: Object, args: Object, controller : Object, debug : Object) {
			var instances = MainController.Current.GetInstances(); // System.Collections.Generic.IList
			for (var i: int = 0; i < instances.Count; i++) {
				var controller_: Object = instances[i]; // MForms.IInstanceController
				var name = controller_.RenderEngine.PanelHeader; // M3 program name
				var window = controller_.Host.Implementation; // Mango.UI.Services.EmbeddedHostWindow
				window.ActivateWindow(true); // de-iconify
				window.Width = controller_.ParentWindow.Width / instances.Count; // set width to a respective fraction of the screen
				window.Height = controller_.ParentWindow.Height; // set to full height
				DashboardService.Window.SetPosition(new Point(window.Width * i, 0), window); // position
			}

		}
	}
}

Here is a screenshot of the result that shows three windows tiled horizontally. It’s just for illustration purposes as the windows look crowded with my low resolution screen; in a real scenario two windows or a bigger screen would look better.

Voilà!

If you liked this solution, I invite you to subscribe to this blog.

Also, read the follow-up to this post with Stand-alone scripts for Smart Office where I convert this Tiling Window Manager into a widget-like script.

Special thanks to Karinpb for the help.

Translate M3 with Google Translate API

Here is a solution to automatically translate M3 and user-generated content in 52 languages.

For that, I will use the Google Translate API and a Personalized Script for Lawson Smart Office.

Business advantage

This solution is interesting to translate content that is generated by users, such as:

  • Bill of Materials
  • Work Orders
  • Service Orders
  • Customer Order Notes
  • etc.

Such content is entered in the user’s language and by design is not translated by Lawson Smart Office.

Also, this solution is interesting to translate M3 itself beyond the number of languages that Lawson makes available.

Lawson Smart Office

Lawson Smart Office supports 18 languages: Czech, Danish, German, Greek, English, Spanish, Finnish, French, Hungarian, Italian, Japanese, Dutch, Norwegian, Polish, Portuguese, Russian, Swedish, and Chinese:

It’s a high number of languages given that text is manually translated by professional translators which are probably paid by the word.

The quality is near perfect.

But by design, the user-generated content is not translated.

Google Translate

Google Translate supports 52 languages: Afrikaans, Albanian, Arabic, Belarusian, Bulgarian, Catalan, Chinese Simplified, Chinese Traditional, Croatian, Czech, Danish, Dutch, English, Estonian, Filipino, Finnish, French, Galician, German, Greek, Hebrew, Hindi, Hungarian, Icelandic, Indonesian, Irish, Italian, Japanese, Korean, Latvian, Lithuanian, Macedonian, Malay, Maltese, Norwegian, Persian, Polish, Portuguese, Romanian, Russian, Serbian, Slovak, Slovenian, Spanish, Swahili, Swedish, Thai, Turkish, Ukrainian, Vietnamese, Welsh, and Yiddish.

It’s a very high number of languages because it uses machine learning and statistical analysis for automatic machine translation of millions of web pages and of official translations done by governments and by international organizations.

It is one of the best machine translations available, considered state of the art, and the quality is improving constantly. [1] [2] [3].

Google is even working on recognizing handwritten text, and text in images.

But even though the quality is good it’s not yet accurate.

It may not be accurate enough in a professional context to translate user-generated content in M3 with the Google Translate API.

But it still gives the user a general idea of the meaning of the text.

And as a pedagogical tool, it serves the purpose of illustrating how to write scripts for Smart Office, and how to integrate M3 to external systems.

Hello World!

To use the Google Translate API you need to register and obtain a key. It is a paid service that will translate one million characters of text for $20.

Once you obtain your key, you need to construct a URL with your API key, the text to translate, and the source and target languages.

Here is a sample URL that translates the text Hello World! from English (en) to French (fr):

https://www.googleapis.com/language/translate/v2?key=YOUR_API_KEY&q=Hello%20World!&source=en&target=fr

The result is a JSON object like this:

{
 "data": {
  "translations": [
   {
    "translatedText": "Bonjour tout le monde!"
   }
  ]
 }
}

First script

Then write a Personalized Script for Lawson Smart Office using the Script Tool.

The script will submit the HTTP GET Request to the Google Translate API over HTTPS and will parse the JSON response.

function translate(text: String, source, target) {
     var url = 'https://www.googleapis.com/language/translate/v2?key=YOUR_API_KEY&source=' + source + '&target=' + target + '&q=' + HttpUtility.UrlEncode(text);
     var request = HttpWebRequest(WebRequest.Create(url));
     var response = HttpWebResponse(request.GetResponse());
     var jsonText = (new StreamReader(response.GetResponseStream())).ReadToEnd();
     var o = eval('(' + jsonText + ')''unsafe');
     return o.data.translations[0].translatedText;
}

We can now use this function to translate any piece of user-generated content, for example the Customer Name in CRS610/E (WRCUNM):

var WRCUNM = ScriptUtil.FindChild(controller.RenderEngine.Content, 'WRCUNM');
WRCUNM.Text = translate(WRCUNM.Text, 'en''fr');

Also, we can translate several pieces of text at once by appending as many q parameters to the URL as pieces of text.

Beyond

With this technique, we can translate all the Controls of our Panel, including the user-generated content: Label, TextBox, Button, ListView, GridViewColumnHeader, ListRow, etc. That will cover Panels A, B, E, F, etc.

Also, we will need to submit the HTTP Request in a background thread to avoid blocking the user interface.

Complete Script

Here is the complete source code of my script that translates all the content of any M3 program, any panel.

Installation

Replace the constant YOUR_API_KEY of the source code with your own Google Translate API key.

The script has a limit GOOGLE_MAX_TEXT_SEGMENTS which was applicable when I wrote the script back in March 2010, but Google has since removed the limit so you can remove it from the script as well.

Then deploy the script on each program and each panel that you’d like to translate. The deployment can probably be automated with some custom XML and XSLT.

Result

Here is an animation of the M3 program Work Order – MOS100/B1 with buttons for seven languages. Click on the image to see the animation. Note how the user-generated content in the rightmost column of the list is also being translated.

Future Work

A future implementation should also translate menus, drop down lists, and text panels (T). I still haven’t been able to execute scripts in a T panel.

That’s it!

 

Updates

UPDATE 2012-08-02: Just fixed the line breaks at line 280 which the copy/paste had corrupted + fixed GetType().ToString() + fixed Exception handling in BackgroundWorker.

UPDATE 2012-08-03, Martin Trydal Torp & Thibaud: Adapted listView for newer LSO (new: listView.ItemsSource; old: listView.Items) + change sourceLanguage dynamically