How to get the text panel in a Mashup

I often forget how to add a TextPanel, for example CRS610/T, into a Mashup for Infor Smart Office, so here is the solution so I can remember next time, and maybe it will help you too.

First, we need to understand how to enter text in M3 using the T panel. Then, we need to understand how the text is stored in M3, in the tables for text headers and text lines, and the TXID. Then, we need to understand how to get that text using a series of three transactions of the M3 API CRS980MI. Finally, we build the Mashup around it using the MI controls and XAML.

How to enter text

To enter text, for example for M3 Customer. Open – CRS610:

  1. Go to Smart Office.
  2. Open CRS610.
  3. Add Panel T to the Panel Sequence.
  4. Select a record, select Options > 2-Change, and click Next until you reach the Panel T; the M3 Text popup will open.
  5. Enter some text, for example Hello World lorem ipsum, on multiple lines.
  6. Click Next. The popup will close.
  7. To enter a second Text block, go back to the popup and click Text block.
  8. Once you enter two text blocks or more, when you go back to the popup, it will first show the M3 Text blocks (text headers), select one, and then it will show the M3 Text (text lines).
  9. You can create text blocks for different languages.

Here’s an example of CRS610/B with Panel Sequence T:
1

Here’s an example of the text headers popup:
2

Here’s an example of the text lines popup:
3

Where is the text stored?

The text is stored in the M3 database in a pair of tables: there’s a table for the text headers and a table for the text lines. The pair of tables depends on the originating M3 Program, for example for CRS610 the tables are OSYTXH and OSYTXL. It’s all tied together by a Text Identity field TXID, for example the Customer table OCUSMA has a TXID column, so do OSYTXH and OSYTXL. The text headers are identified by the fields CONO, DIVI, TXID, TXVR, and LNCD (language). And the text lines are identified by the foreign keys of their text header, and by LINO (line number).

I never rememeber which M3 Program stores text in which pair of tables, for example it took me a while to remember that Customer text is stored in OSTYXL. There’s probably a short way to remember: perhaps somewhere in the M3 Companion, perhaps reading the cryptic M3 Java source code. I usually go to M3 MetaData Publisher (MDP), I search for tables with the word “text”, and then I go fishing for my text with SQL, searching the contents of the tables one by one. Yeah I know…there’s got to be a better way.

Here’s me searching in MDP (I had tried all the pairs of table Text head and Text line, one by one, starting at the top letter A, until I found OSYTXL way below and took this screenshot):
0

Here’s me fishing in SQuirreL (bingo! I finally found my text):
0__

It seems every four years I go thru this learning process all over again, as for the first time, each time making detailed notes and screenshots and telling myself this time it would be for good, and then four years later I forget it all again. Yikes! If someone has a better way to find the tables please let me know.

How to get the text using M3 APIs?

Several years ago, the M3 Product Development team finally introduced an M3 API CRS980MI to get the text. Before that we had to use good ol’ SQL. There are three transactions (methods) to call. First, we need to get the TXID based on the originating table and key, in my case table OCUSMA and key Company CONO and Customer number CUNO. Then, we need to get the text headers for that TXID. Then, we need to get the text lines for a selected text header.

Step 1 – Get the TXID

To get the TXID:

  • M3 Program: CRS980MI
  • Transaction: GetTextID
  • Input fields:
    • FILE, in my case OCUSMA00
    • KV01, in my case the CONO, for example 735
    • KV02, in my case the CUNO, for example ACME
  • Output field:
    • TXID, for example 544

In the Mashup, we’ll call that using a hidden MIPanel control.

Step 2 – Get the text headers

To get the text headers:

  • M3 Program: CRS980MI
  • Transaction: LstTxtBlocks
  • Input fields:
    • CONO, for example 735
    • DIVI, for example AAA
    • TXID, for example 544
    • TFIL, for example OSYTXH
  • Output fields, a list of:
    • TXVR, for example THIBAUD
    • LNCD, for example GB
    • TX40
    • TXEI

In the Mashup, we’ll call that using a MIListPanel control.

Step 3 – Get the text lines

To get the text lines:

  • M3 Program: CRS980MI
  • Transaction: SltTxtBlock
  • Input fields:
    • CONO, for example 735
    • DIVI, for exapmle AAA
    • TXID, for example 544
    • TXVR, for example THIBAUD
    • LNCD, for example GB
    • TFIL, in my case OSYTXH
  • Output fields, a list of:
    • LINO
    • TX60

In the Mashup, we’ll call that using a MIListPanel control.

Build the Mashup

Here’s the XAML source code (I forgot to un-hard-code the CONO):

<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:ui="clr-namespace:Mango.UI.Controls;assembly=Mango.UI" xmlns:mashup="clr-namespace:Mango.UI.Services.Mashup;assembly=Mango.UI" xmlns:m3="clr-namespace:MForms.Mashup;assembly=MForms">
    <Grid.Resources></Grid.Resources>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="1*" />
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="1*" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="1*" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="1*" />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>

    <m3:ListPanel Name="CustomerList" Header="{m3:Constant Key=CR61001,File=MVXCON}" IsListHeaderVisible="True" Grid.Row="0">
        <m3:ListPanel.Events>
            <mashup:Events>
                <mashup:Event SourceEventName="Startup">
                    <mashup:Parameter TargetKey="OKCONO" />
                    <mashup:Parameter TargetKey="OKCUNO" />
                </mashup:Event>
            </mashup:Events>
        </m3:ListPanel.Events>
        <m3:ListPanel.Bookmark>
            <m3:Bookmark Program="CRS610" Table="OCUSMA" KeyNames="OKCONO,OKCUNO" IncludeStartPanel="True" SortingOrder="1" View="STD01-01" />
        </m3:ListPanel.Bookmark>
    </m3:ListPanel>

    <m3:MIPanel Name="MIGetTextID">
        <m3:MIPanel.Events>
            <mashup:Events>
                <mashup:Event SourceEventName="CurrentItemChanged" SourceName="CustomerList">
                    <mashup:Parameter TargetKey="FILE" Value="OCUSMA00" />
                    <mashup:Parameter TargetKey="KV01" SourceKey="CONO" />
                    <mashup:Parameter TargetKey="KV02" SourceKey="CUNO" />
                </mashup:Event>
            </mashup:Events>
        </m3:MIPanel.Events>
        <m3:MIPanel.DataSource>
            <m3:MIDataSource Program="CRS980MI" Transaction="GetTextID" Type="Get" InputFields="FILE,KV01,KV02" OutputFields="TXID" />
        </m3:MIPanel.DataSource>
    </m3:MIPanel>

    <Label Grid.Row="1" Content="Text headers" Style="{DynamicResource styleGroupBoxHeaderMashup}" />
    <m3:MIListPanel Name="MILstTxtBlocks" Grid.Row="2">
        <m3:MIListPanel.Events>
            <mashup:Events>
                <mashup:Event SourceName="CustomerList" SourceEventName="CurrentItemChanged" TargetEventName="Clear" />
                <mashup:Event SourceName="MIGetTextID" SourceEventName="Running">
                    <mashup:Parameter TargetKey="CONO" Value="735" />
                    <mashup:Parameter TargetKey="DIVI" />
                    <mashup:Parameter SourceKey="TXID" TargetKey="TXID" />
                    <mashup:Parameter TargetKey="TFIL" Value="OSYTXH" />
                </mashup:Event>
            </mashup:Events>
        </m3:MIListPanel.Events>
        <m3:MIListPanel.DataSource>
            <m3:MIDataSource Program="CRS980MI" Transaction="LstTxtBlocks" Type="List" InputFields="CONO,DIVI,TXID,TFIL" OutputFields="TXVR,LNCD,TX40,TXEI" />
        </m3:MIListPanel.DataSource>
        <ListView ItemsSource="{Binding Items}" Style="{DynamicResource styleListView}" ItemContainerStyle="{DynamicResource styleListViewItem}">
            <ListView.View>
                <GridView ColumnHeaderContainerStyle="{DynamicResource styleGridViewColumnHeader}">
                    <GridView.Columns>
                        <GridViewColumn Header="Text block" DisplayMemberBinding="{Binding [TXVR]}" />
                        <GridViewColumn Header="Language" DisplayMemberBinding="{Binding [LNCD]}" />
                        <GridViewColumn Header="Description" DisplayMemberBinding="{Binding [TX40]}" />
                        <GridViewColumn Header="External/internal text" DisplayMemberBinding="{Binding [TXEI]}" />
                    </GridView.Columns>
                </GridView>
            </ListView.View>
        </ListView>
    </m3:MIListPanel>

    <Label Grid.Row="3" Content="Text lines" Style="{DynamicResource styleGroupBoxHeaderMashup}" />
    <m3:MIListPanel Name="MISltTxtBlock" Grid.Row="4">
        <m3:MIListPanel.Events>
            <mashup:Events>
                <mashup:Event SourceName="CustomerList" SourceEventName="CurrentItemChanged" TargetEventName="Clear" />
                <mashup:Event SourceName="MILstTxtBlocks" SourceEventName="CurrentItemChanged">
                    <mashup:Parameter TargetKey="CONO" Value="735" />
                    <mashup:Parameter TargetKey="DIVI" />
                    <mashup:Parameter TargetKey="TXID" Value="{Binding [TXID], ElementName=MIGetTextID}" />
                    <mashup:Parameter SourceKey="TXVR" TargetKey="TXVR" />
                    <mashup:Parameter SourceKey="LNCD" TargetKey="LNCD" />
                    <mashup:Parameter TargetKey="TFIL" Value="OSYTXH" />
                </mashup:Event>
            </mashup:Events>
        </m3:MIListPanel.Events>
        <m3:MIListPanel.DataSource>
            <m3:MIDataSource Program="CRS980MI" Transaction="SltTxtBlock" Type="List" InputFields="CONO,DIVI,TXID,TXVR,LNCD,TFIL" OutputFields="TX60,LINO" />
        </m3:MIListPanel.DataSource>
        <ListView ItemsSource="{Binding Items}" Style="{DynamicResource styleListView}" ItemContainerStyle="{DynamicResource styleListViewItem}">
            <ListView.View>
                <GridView ColumnHeaderContainerStyle="{DynamicResource styleGridViewColumnHeader}">
                    <GridView.Columns>
                        <GridViewColumn Header="Line number" DisplayMemberBinding="{Binding [LINO]}" />
                        <GridViewColumn Header="Text" DisplayMemberBinding="{Binding [TX60]}" />
                    </GridView.Columns>
                </GridView>
            </ListView.View>
        </ListView>
    </m3:MIListPanel>

    <ui:StatusBar Name="StatusBar" Grid.Row="5" />
</Grid>

Here’s a screenshot of the result, with the list of customers at the top, the list of text headers for that customer in the middle, and the text lines for that text header at the bottom:
4

That’s it!

Send us your comments below, subscribe to this blog with the Follow button below, be an author and write your own ideas, share with colleagues, and enjoy.

Event Analytics for Infor Process Automation (IPA)

Today I will illustrate how to setup Event Analytics for Infor Process Automation (IPA). Event Analytics is an application that subscribes to Event Hub, that filters events based on conditions, and that takes actions. My goal is to single out specific Infor M3 events to trigger IPA flows with accuracy, for example to trigger a HelloWrrrld flow only when an M3 Item number ABC123 has changed from Status 10 to 20, specifically. This post is intended for readers already familiar with IPA and Event Hub, yet not too familiar with Event Analytics. For an introduction on Event Hub for IPA, I invite you to read my previous article.

About Event Analytics

Event Analytics is an application for the Infor Grid that subscribes to Event Hub. It uses a rules engine with business rules to single out specific events out of the million of events produced by M3, i.e. it will find the needle in the haystack, and it will carry out actions. It’ s fast and scalable and doesn’t affect M3 performance. It’s used for example to pass Business Object Documents (BODs) to Infor ION.

It uses the Production Rule System JBoss Drools, a “Business Logic integration Platform which provides a unified and integrated platform for Rules, Workflow and Event Processing”,  and it uses the Drools Rule language, a declarative domain-specific language that looks like when <condition> then <action> . Drools Rule files have the .drl extension. The Smart Rules Editor is an optional plugin for Eclipse based on Drools Expert to help produce Drools Rules for Event Analytics. For further reading on JBoss Drools and Drools Rules, I recommend the Rules Programming tutorial by Srinath Perera.

Documentation

The Infor LifeCycle Manager (LCM) InfoCenter has detailed documentation about Event Analytics: facts, subscriptions, administration, example rules, etc. For that, go to your LCM InfoCenter at http://lcmserver:4062/ and then navigate to Documentation > Infor Smart Office Infocenter > Installation Guides > Infor ION Grid Extensions Installation and Administration Guide > Event Hub and Event Analytics Grid Extensions:
doc

Event Analytics or Event Hub?

Why should we use Event Analytics to trigger IPA flows when we can use Event Hub alone? Well, if we used Event Hub alone to directly trigger IPA flows we could potentially get too many false positives. For instance, in my example above I want to trigger a flow only when the Item number ABC123 has changed from Status 10 to 20; I don’t want events for other Item numbers nor Statuses. Unwanted events would create too many unnecessary WorkUnits in IPA, and that would clog the server with noise in the database even if we used an if-then-else Branch activity node at the start of the flow to eventually cancel the execution downstream. The solution is to filter events upstream with Event Analytics.

Dual subscriber/publisher

Once a condition is met in a Drools Rule, a typical action for Event Analytics is to post a new event to Event Hub. Then, subscribers like IPA can subscribe to those events with Publisher:EventAnalytics instead of the traditional Publisher:M3. Thus, Event Analytics is dual subscriber and publisher. It took me a while to figure out the gymnastics in my head, eventually it became clear.

Here is an illustration:

EventAnalytics

The HelloWrrrld scenario

For illustration purposes in this article, the simple scenario will be to trigger a HelloWrrrld flow when an M3 Item number ABC123 has changed from Status 10 to 20. The baby steps will be:

First, I will create a Drools Rule that will subscribe to events where Publisher:M3, Document:MITMAS, and Operation:U, and with the conditions ITNO=ABC123, old STAT=10, and new STAT=20. If that condition is met, the Rule will carry out the action to post a new event MITMAS_ABC123_20.

Then, with Infor Process Designer (IPD), I will create and deploy a simple HelloWrrrld flow. The flow will receive as input variables all the data from the event. So I will add a simple activity node that will show the M3 fields <!CONO>, <!ITNO>, <!ITDS>, and <!STAT>.

Then, in IPA Rich Client Admin, I will create a new Event Hub Receiver with a subscription to EventAnalytics:MITMAS_ABC123_20 that will trigger the HelloWrrrld flow.

Then, I will do a test. I will go to MMS001 in Smart Office, I will prepare an Item ABC123 with Status 10, I will save it, and then I will change it to Status 20. I will also update other Items to produce additional events (noise). M3 will send all those events to Event Hub. Event Hub will pass those events to Event Analytics. Event Analytics will single out the event that matches the condition ITNO=ABC123, old STAT=10, new STAT=20, and it will post a new Event MITMAS_ABC123_20. Then, the Event Hub Receiver will receive that event and will trigger my HelloWrrrld flow with the data.

Finally, the resulting WorkUnit will contain all the variables of the event, the M3 fields, the old values, and the new values.

OK let’s do it.

Create a Drools Rule

First, let’s create the new Drools Rule in Event Analytics:

  1. Go to Infor LifeCycle Manager (LCM).
  2. Find EventAnalytics in your Grid (expand the tree or use the filter).
  3. Right-click > Manage Application.
  4. There will be one or more Sessions. We’ll use Session Default for now. Click Rules.
  5. There will be zero or more Drools Rule Language Files, active or not. Click Create.
  6. Enter a Resource Name, for example MITMAS_ABC123_20_Rule.
  7. The editor will generate a sample Drools Rule with subscription M3:MITMAS:U, and condition elementValues[“STAT”]=”20″. Good. We’ll keep that.
  8. Rename the rule MITMAS_20_Demo to MITMAS_ABC123_20_Demo.
  9. Add the condition elementValues[“ITNO”]=”ABC123″ .
  10. Add the condition elementOldValues[“STAT”]=”10″ .
  11. In the actions, rename the postEvent to MITMAS_ABC123_20.
  12. Delete the rules Start_Demo, Time_Demo, and Stop_Demo.
  13. Click Save.
  14. The result will look like this:
    ea8
  15. Close the editor.
  16. Back in the list of Drools Rule Language File, select the checkbox next to your Rule to activate it.
  17. Click Reload to load your Rule.
  18. Verify in the list of Rules that your Rule is now there.

Create a HelloWrrrld flow

Then, with Infor Process Designer (IPD), let’s create and deploy the simple HelloWrrrld flow that will show the M3 fields <!CONO>, <!ITNO>, <!ITDS>, and <!STAT>.

It will look like this:
flow2

Create an Event Hub Receiver

Then, let’s create the new Event Hub Receiver in IPA Rich Client Admin:

  1. Go to IPA Rich Client Admin.
  2. Switch to the desired data area (development, test, production, etc.)
  3. Open Channels Administrator.
  4. Create a new Event Hub Receiver.
  5. Set the Subscription to EventAnalytics:MITMAS_ABC123_20.
  6. Select Process HelloWrrrld.
  7. Select Startup Type Automatic.
  8. Click Save.
  9. Select Actions > Activate.
  10. The status bar will show “Activate Completed”.

The result will look like this:
richclient3

Test in Smart Office

Then, let’s do a test in Smart Office.

  1. Go to MMS001 in Smart Office.
  2. Create an Item ABC123 with Status 10, and save it.
  3. Change the Item to Status 20:
    iso3
  4. Optionally, update other Items to produce additional events (noise).

Resulting WorkUnit

Finally, open the resulting WorkUnit in Rich Client Admin, switch to the Variables tab. It will show all the input variables of the event, the M3 fields, the old values, and the new values:
z_

That’s it!

If you liked this post, please subscribe to this blog by clicking the Follow button below. And leave your comments in the section below. That will support and grow the community. Also, spread the word to your colleagues, customers, and partners. And if you have something to share, let me know and I will send you an author invite. And even better, create your own blog to grow the community even more.

 

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

Event Hub for Infor Process Automation (IPA)

Today I will illustrate how to setup Event Hub for Infor Process Automation (IPA). The goal is to receive M3 events in IPA in order to trigger approval flows, for example to trigger a User approval flow when a new User is created in M3, or to trigger a Purchase Order flow when a new Purchase Order is created in M3. This technique has been around for several years and replaces pretty much all the previous techniques to trigger flows.

This post is intended for readers already familiar with IPA, yet not too familiar with Event Hub.

About Event Hub

Event Hub is a publish-subscribe distributed messaging system for M3. It seems to use JBoss HornetQ, the “open source project to build a multi-protocol, embeddable, very high performance, clustered, asynchronous messaging system”, it seems to use JGroups, “a toolkit for reliable messaging [that] can be used to create clusters whose nodes can send messages to each other”, it seems to use Netty, “an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients”, and it seems to use Disruptor, a “High Performance Inter-Thread Messaging Library”, based on unzipping the Grid Archive file Infor\LifeCycle Manager\LCM-Server\products\eventhub-installer\components\eventhub-gar-2.0.16.gar.

M3 produces millions of events as part of its daily operation. For example: users open programs, they create new records and update existing ones, the database gets updated, etc. M3 is the Producer of events. The Documents can be M3 Programs (such as M3 Customer – CRS610) and M3 Tables (such as OCUSMA). The Operations on M3 Programs can be: Start, eXit, Fail, reQuest, and Response. The Operations on M3 Tables can be Create, Update, and Delete. When an event happens in M3, M3 sends that event to Event Hub. Then Event Hub distributes that event to any subscriber that is interested in that event, for example to M3 Enterprise-Collaborator (MeC) whom will exchange BODs and initiate EDI transactions, to Infor Enterprise Search (IES) whom will update the search engine’s indexes, to Infor Process Automation (IPA) whom will trigger flows, to Infor Customer Lifecycle Management (CLM) whom will synchronize M3 and CLM customer records, etc.

Here’s an illustration:
hub

I like Event Hub because it’s a beautiful piece of engineering, and it’s based on modern, well documented, open source software.

Documentation

The Infor LifeCycle Manager (LCM) InfoCenter has detailed documentation about Event Hub: overview, events, documents, operations, subscriptions, administration, etc. For that, go to your LCM InfoCenter at http://lcmserver:4062/ and then navigate to Documentation > Infor Smart Office Infocenter > Installation Guides > Infor ION Grid Extensions Installation and Administration Guide > Event Hub and Event Analytics Grid Extensions:
1

The HelloWorld scenario

The simple Hello World scenario I will illustrate in this post is the following: when the user closes M3 Program EUS001 I will trigger a HelloWorld flow. It’s as simple as that. Why not. It doesn’t have any value from a functional point of view, but it’s a great illustration from a technical point of view. Here are the baby steps:

First, I will go to the Event Hub Configuration in my Grid and determine what the host and port number are to subscribe to Event Hub.

Then, I will enter the host and port number in IPA Rich Client Admin to setup IPA as a subscriber of Event Hub. (Conversely, IPA can also be a publisher and publish events to Event Hub but I won’t cover that scenario here.)

Then, I will create a HelloWorld flow, and I will setup a subscription M3:EUS001:X that will trigger the flow. The flow will be a simple Start -> End flow with no intermediate activity nodes. No need for fluff.

Finally, I will do a complete test, I will: start Infor Smart Office, open and close EUS001, and analyze the resulting WorkUnit in IPA.

Event Hub Configuration

Let’s go to the Event Hub Configuration in the Grid and determine what the host and port number are to subscribe to Event Hub:

  1. Open Infor LifeCycle Manager (LCM)
  2. Expand the Grid (for example, Development, Test, Production)
  3. Expand the Products until you find EventHub (you can expand the tree, or use the filter)
  4. Right-click > Configure Application
  5. In the Bindings section, write down the Host:
    lcm1
  6. Select Edit Properties
  7. Expand Server
  8. Write down the Port number:
    lcm2

Setup IPA as a subscriber of Event Hub

Then, let’s enter the host and port number in IPA Rich Client Admin to setup IPA as a subscriber of Event Hub:

  1. Start the IPA Rich Client Admin
  2. Switch the data area (for example, dev, test, prod, etc.)
  3. Start Channels Administrator (either search for channel, either go to Start > Applications > Process Server Administrator > Administration)
  4. Create an Event Hub Channel (Actions > Create):
    channel1
  5. Enter a Name, for example EventHub
  6. Enter a Description, for example EventHub
  7. Check the box External
  8. Enter the Host
  9. Enter the Port Number:
    channel2
  10. Click Save, the status bar will say “Channel Created”
  11. Select Actions > Activate, the status bar will say “Activate Completed”

Create a flow, the subscription, and the trigger

Then, let’s create a HelloWorld flow and setup a subscription M3:EUS001:X that will trigger the flow.

  1. Create a new HelloWorld flow in Infor Process Designer (IPD), a simple Start -> End flow with no intermediate activity nodes will suffice (you can add other nodes if you want), and deploy it on the server with Process > Upload process:
    ipd
  2. Back in the Event Hub Channel in IPA Rich Client Admin, switch to the tab Event Hub Channel Receivers and create a new Receiver:
    channel3
  3. Enter a Receiver and Description, for example HelloWorld.
  4. Enter Subscription M3:EUS001:X.
  5. Select Process HelloWorld (the flow).
  6. Select Startup type Automatic.
  7. Click Save, the status bar will say “Pfi Receiver Created”:
    channel4
  8. Select Actions > Activate, the status bar will “Activate Completed”.
  9. Close the Event Hub Receiver window.
  10. Close the Event Hub Channel Receivers window.
  11. You will be back in the Channels Administrator window. Make sure the Event Hub Channel EventHub is Active and the Event Hub Receiver HelloWorld is Active.
  12. You can close the Channels Administrator window.

Test

Finally, let’s do a complete test: start Infor Smart Office, open and close EUS001, and analyze the resulting WorkUnit in IPA.

  1. Go to Infor Smart Office.
  2. Open EUS001 (mforms://eus001), and close it (F3):
    eus001
  3. That will cause the event M3:EUS001:X to happen in M3, Event Hub will forward the event to IPA, and IPA will trigger the HelloWorld flow.
  4. Back in IPA Rich Client Admin, open the Work Units (Start > Applications > Process Server Administrator > Administration > Work Units)
  5. Find the latest WorkUnit for the HelloWorld Process:
    WorkUnit1
  6. Open it, and switch to the WorkUnit Variables, it will show all the keys/values for the event:
    WorkUnit3

That’s it! We setup IPA as a subscriber of Event Hub, we created a receiver to subscribe to a specific event and trigger our flow, we did a test and caused the event to happen, and we analyzed the resulting WorkUnit. That illustrates how to setup Event Hub for IPA to receive M3 events in order to trigger a flow.

Next time I will show you how to setup Event Analytics for IPA and how to setup Drools Rules to filter events with conditions.

If you liked this, please subscribe to this blog by clicking the Follow button, and let us know your comments in the section below. You can also become an author and share your ideas (let me know and I’ll send you an author invite).

How to call an M3-API with Angular JS and from a web page

I’m new to JAvascript, and Angular JS has given me a challenge but a great tool for manipulating web page content. It’s a MVC framework that’s recommended by a few colleagues.

The details below outline how to create a webpage to:

a) Get a list of customers ordered by Customer Number from M3
b) display on a web page the customer number, customer name and customer phone number.

This will be a custom webpage that can be called within Infor’s H5 client. As there are no customisations available yet, Javascript + Angular JS is a good open source alternative.

Continue reading How to call an M3-API with Angular JS and from a web page

Add a Profile fragment with Smart Office SDK

Continuing my previous and previous posts on Infor Smart Office SDK, I will now post on how to add a profile fragment in Smart Office Developer mode. For that, I will follow the instructions from the Developer’s Guide and from Karin’s comments.

We use the <Profile> section of the feature Manifest to merge our settings into the system profile. It gets merged only when the feature is deployed to the server with LifeCycle Manager (LCM). It doesn’t get merged when Smart Office is running in Developer mode. The workaround is to use a local profile.xml and reference it in our Windows Registry. First we need to get the server’s profile.xml.

Find the server’s profile.xml

In old versions of Smart Office, the file profile.xml used to be an actual file on the server. Now with the Infor Grid the file has been moved into a distributed database. Supposing you have such version of Smart Office:

  1. Go to your H2 Web Console following Scott’s instructions, login to the MangoServer application, click on the CATEGORYFILES table, it will display the contents of the table, and copy the CONTENT of profile.xml:
    1anon
  2. HEX-decode the contents, for example with Notepad++ or Ostermiller’s decoder:
    2anon
  3. Save the decoded contents to a file profile.xml somewhere on your computer, and ensure it’s valid XML:
    3anon
  4. That’s the server’s profile.xml.

Add your fragment

Now you can add your settings:

  1. Open the file profile.xml with a text editor like Notepad, add your application group (or extend an existing one), and add your settings, for example:
    <applicationgroup name="Thibaud">
        <A>
            <A1>Hello World!</A1>
            <A2>123</A2>
        </A>
        <B>
            <B1>true</B1>
            <B2>["abc", "def"]</B2>
            <B3>{ "msg": "I'm a value!" }</B3>
        </B>
    </applicationgroup>
    
  2. Ensure the file is still valid XML:
    4
  3. Optionally, you can add Description attributes at each element, and enabled=”true” on the applicationgroup.

Reference the file in Windows Registry

Now tell Smart Office Developer mode to use that profile.xml:

  1. Start your Windows Registry Editor (C:\Windows\regedit.exe).
  2. Go to the Smart Office SDK Window Registry Key HKEY_CURRENT_USER\Software\Infor\MangoDev
  3. Add a String value ProfilePath, and set the value to the path of your profile.xml:
    5

Test in Smart Office

Now test it in Smart Office Developer mode:

  1. Now re-start Smart Office in Developer mode (from your Visual Studio solution).
  2. Start the Profile Editor from the Navigator widget > Administration tools.
  3. Select your application group, Thibaud in my example.
  4. Select Advanced and your settings are there:
    6

That’s it!

If you liked this, click the Follow link below to subscribe to this blog. And join the authors and share your own M3 ideas with us.

Related articles

 

Add a feature to Smart Office SDK

After my previous post Hello World of Infor Smart Office SDK, I will now illustrate how to add a feature to Infor Smart Office SDK, for example the Infor Customer Lifecycle Management (CLM) feature. For that, we’ll need to get the feature files and add them to our Visual Studio Solution.

Get the feature files

To get the feature files, we need to find the Manifest and Application extension (DLL) files of our feature.

From the Smart Office server

You can find the feature files in the Infor LifeCycle Manager (LCM) server > products > components folder, and unzip the feature:

From Infor Xtreme

You can also find the feature files by downloading and unzipping the feature from Infor Xtreme. In my case I couldn’t find the same version of CLM to download as my server, but I put screenshots anyway to illustrate the point:

7.2__

From the local deployment

You can also find the feature files locally in the current ClickOnce application deployment folder of your computer:

  1. Start Smart Office from the server (not Smart Office Developer from Visual Studio).
  2. Select Show > Settings > Infor Smart Office, switch the Log level to Debug, and click Save:
    6.2
  3. Logoff Smart Office and logon again.
  4. Select the Help menu (question mark icon at the top right) > About Infor Smart Office > View log file:
    6.2
  5. Filter Origin with RegisterGroups, filter Message with the Manifest of the feature you want to add, for instance LCLM.manifest, and notice the path to the Manifest, for example C:\Users\…\AppData\Local\Apps\2.0\Data\…\…\http…\Data\F\<feature>\ :
    6.4_
  6. And open that path in Windows Explorer:
    3.2___

Reference the files in Visual Studio

Once you found the Manifest and Application extension (DLL) files:

  1. Copy/paste them to the Bin directory of your SDK root directory:
    3.2__
  2. Open your Visual Studio solution, select the MangoClient Project, and select PROJECT > Add Reference:
    3.1_
  3. Browse to the Bin directory, select the DLL files you just pasted, and click OK:
    6.7
  4. Now Rebuild your solution, Start it, and the feature will be there in your Smart Office Developer mode:
    3.9

That’s it!

Note: Instead of copy/pasting the files to the Bin directory, I also tried adding a reference to the files of the ClickOnce directory, i.e. no need to copy/paste, and it worked fine. But the Developer’s Guide states “The feature assemblies and the feature manifest must be copied to the bin directory so that the framework can load the feature when the client starts.” Maybe there’s a reason I don’t know about, so I followed their instructions.

If you liked this post, subscribe to this blog and we’ll send you an email notification when we write a new post. Also, become a contributor and post your M3 ideas.

Hello World of Infor Smart Office SDK

Today I will write a post on how to start with the Infor Smart Office SDK as a continuation of Karin’s getting started post and Scott’s one to eight part series. We use the Smart Office SDK to develop applications in C#/WPF that run inside Smart Office with the benefits of look & feel, session, user information, M3 context, integration to M3 APIs and M3 Web Services, etc. I’m developing an application for a customer to integrate Cisco IP phones with Infor Customer Lifecycle Management (CLM). This is a great opportunity for me to share with you interesting bits and pieces as I progress, and it’s a way for me to anchor what I learn. I will put screenshots galore.

Here are the steps:

  1. Download and install Microsoft Visual Studio for C# and WPF development. I opted for the free Visual Studio Express 2013 for Windows Desktop:
    1.1
  2. Launch it from the Windows Start menu > All Programs > Visual Studio 2013 > VS Express 2013 for Desktop:
    1.2
  3. Download and install the Infor Smart Office SDK from Infor Xtreme > Downloads > ProductsUser Productivity PlatformSmart Office SDK – M3; choose the same version as the target Smart Office server you will develop for, version 10.1 in my case:
    2.1
  4. Unzip to a temporary folder in your computer and move the contents to the SDK root directory, for example C:\InforSmartOfficeSDK\ ; I moved to a folder with the full version number as I might have to switch back and forth between different versions for customer projects:
    2.2
  5. For more information , read the installation instructions in the Infor Smart Office Developers Guide at path Documentation\DevelopersGuide.pdf:
    0
  6. Set an environment variable LSOSDKBin in My Computer > Properties > Advanced system settings > Environment Variables, to the path of the Bin sub-folder, in my case it’s C:\InforSmartOfficeSDK_10.1.1.1_20130920\Bin :
    2.4
  7. Edit the contents of the file RegisterServer.reg in a text editor like Notepad, and change the value of Server to point to the scheme://domain:port of your Smart Office server URL. Then execute the file to merge the new MangoDev Key into your Windows Registry (C:\Windows\regedit.exe); in my case I later manually added String values for Username and Password so I don’t get authentication prompts anymore (beware of this security risk):
    2.5_
  8. Open the Sample Solution Samples\Samples.sln in Visual Studio:
    2.7
  9. Select BUILD > Build Solution F7:
    2.8
  10. Select DEBUG > Start Debugging F5:
    2.9
  11. That will start Smart Office in Developer mode. Select Help (question mark icon at the top right) > About Infor Smart OfficeView features, that will show you the Framework Examples and the Sample Feature:
    2.10_
  12. Start the Hello World application in the Sample menu of the Navigator widget:
    2.12
  13. And try the other samples and widgets:
    2.11

That’s it!

If you like this, subscribe to this blog to receive an email notification when we write posts, and become a contributor and write your M3 ideas.

Hacking Customer Lifecycle Management (CLM)

Today I will show you how I made simple modifications to Infor Customer LifeCycle Management (CLM), the CRM product for Infor M3. With CLM standard out-of-the-box we only have the ability to show/hide fields, for example choosing whether or not we want the name, address, and phone number columns in the list or the fields in the details view. CLM is a great product, and by design it is intended to be simple to use. In my case I wanted something more: I needed to add a call button next to the phone number. That is not officially possible by default so I had to do some hacking.

I’m working on a project for a customer to integrate Cisco IP phones with CLM, such that when a customer service representative on the phone receives an incoming phone call we automatically pop-up the corresponding customer data on the screen, and conversely, such that they can click a phone number in CLM and make that outgoing phone call. I had already done some work in the past integrating Skype with Smart Office, and integrating ShoreTel phones with Smart Office. This time it’s Cisco IP phones. I cannot show you the entire source code as it’s propriety of the customer and my employer, but I will show you interesting bits and pieces, and the writing helps me clean-up my code too.

I will show you:

  • How to get the list of open CLM windows
  • How to find the phone number field
  • How to add a button to CLM and use the Design System Icons
  • How to make the outgoing phone call

About CLM

To tell if you have CLM, go to Smart Office > Help > About Infor Smart Office > View features, and you will see CLM Application:
1

Then, go to the Navigator widget, you will see the menu Customer Lifecycle Management, expand it and launch My Accounts > All:
2

It will open the list of accounts:
3_

Double-click one of the rows to open the account details:
4_

How to get the list of open CLM windows

Now we have two CLM windows open: the list of accounts, and the details of an account. To programmatically get that list, I use the DashboardTaskService.FindRunningTaskByUri method, to discriminate by Uri lclm://
0


var list /*System.Collections.Generic.List<Mango.UI.Services.FindTaskResult>*/ = DashboardTaskService.Manager.FindRunningTaskByUri("lclm://", TaskMatch.StartsWith);
for (var result : FindTaskResult in list) {
    var runner : IRunner = result.Runner;
    var task : ITask = runner.Task;
    debug.WriteLine(task.Uri);
}

That will return two tasks:
lclm://filter/?ActionType=View&MainTableID=…&FilterGroupID=…&SubFilterID=…
lclm://details/?ActionType=View&MainTableID=…&PrimaryKey=…

Now we need to tell apart the Accounts windows from the other potential CLM windows such as Activities or Contacts:

var host : IInstanceHost = runner.Host;
if (host.HostTitle.StartsWith("Account")) {
    // ...
}

This code will only work for English. Ideally we would use an official CLM API that returns the correct Tasks, but I haven’t found one. Let me know if you find one.

How to find the phone number field

Now that we have the correct window, we can get its contents and find the phone number field. First, I use WPF Inspector to visually introspect the window and find the phone number field in the visual tree:
6

The fields are layed out in one of the ancestor Grids:
5

More specifically the phone number field is itself a Grid of one row and three columns:
6_

That’s where I’ll inject my button. To get there programmatically, I use the VisualTreeHelper, and I do a pre-order depth first search:

function ... {
    //...
    var content : FrameworkElement = host.HostContent;
    if (content.GetType().ToString() == "lclmControls.Custom.TabularDetailsView") {
        var o: DependencyObject = FindPhoneTextBox(content);
        if (o != null) {
            var txtbox: SingleLineTextBox = o;
        }
    }
}
function FindPhoneTextBox(o : DependencyObject): DependencyObject {
    // visit node
    if (o != null) {
        if (o.GetType().ToString().EndsWith("SingleLineTextBox")) {
            var txtbox: SingleLineTextBox = o;
            if (txtbox.Name == "Phone") {
                return o;
            }
        }
    }
    // visit children
    for (var i = 0; i < VisualTreeHelper.GetChildrenCount(o); i++) {
        var child = VisualTreeHelper.GetChild(o, i);
        var result: DependencyObject = FindPhoneTextBox(child);
        if (result != null) {
            return result;
        }
    }
    // not found
    return null;
}

How to add a button to CLM and use the Design System Icons

Now that we found the phone number field and the Grid, we can add a button. I will use the IconButtons of the Design System as illustrated by norpe:

var btn: IconButton = new IconButton();
btn.IconName = "Phone";
btn.ToolTip = "Call this phone number."
btn.HorizontalAlignment = HorizontalAlignment.Left;
btn.Margin = new Thickness(5, 0, 0, 0);
btn.Tag = txtbox; // remember the textbox
Grid.SetRow(btn, 0);
Grid.SetColumn(btn, 2);
Grid.SetColumnSpan(btn, 2);
var grid: Grid = VisualTreeHelper.GetParent(txtbox);
grid.Children.Add(btn);
btn.add_Click(OnCall);

This is the result, with the IconButton hover and ToolTip:
7_

Note: to get the Grid I used the textboxe’s parent. This assumption is true in CLM version 1.0.0.99, but could be false in a future version of CLM in which case this code would break. Ideally we would have an official CLM API for this.

How to make the outgoing phone call

Now you can do whatever with the phone number, for example use the default operating system’s URI handler for the tel scheme which is Skype in my case.

function OnCall(sender: Object, e: RoutedEventArgs) {
    try {
        var btn: Button = sender;
        var txtbox: SingleLineTextBox = btn.Tag;
        var phoneNumber: String = txtbox.Text;
        var uri: Uri = new Uri("tel:" + phoneNumber); // RFC 3966
        ScriptUtil.Launch(uri);
    } catch (ex : Exception) {
        debug.WriteLine(ex);
    }
}

And here’s the result:
8_

The method ScriptUtil.Launch will instruct the operating system to execute the specified command. That’s the equivalent of typing start command at the DOS prompt. In our case it’s:

start tel:+14156247033

9

That means any special characters of the command must be escaped, such as white spaces and ampersands. To escape white spaces in DOS that means enclosing the entire string in double-quotes. I tried enclosing the URI in double quotes, and it didn’t work. I also tried other escaping and encoding techniques like using backslash, plus sign, and %20, and they didn’t work either. So let’s simply strip it out:

phoneNumber = phoneNumber.Replace(' ', '');

Also, in my example I used a phone number that’s already correctly formatted in international E.123 notation which Skype understands. To validate the phone number, we can use a regular expression. A simple one is to strip all characters and keep only the plus sign and the digits, but that’s probably not fully compliant with the E.123 specification so we need to work more on this in the future:

var regex = /[^\+^\d]/g;
phoneNumber =  phoneNumber.replace(regex, "");

Future work

Future work includes:

  • Use an event handler to listen for new CLM windows to automatically add the Call button as the user opens the Account views. I couldn’t find an event, and Karin confirmed it’s not currently supported. I tried MForms.MainController, DashboardTaskService, DashboardTaskBar, lclmControls.EventNotifier, WindowManager, etc. I found an event handler for M3 Forms, an event handler for the Quick Start CTRL+R, and private event handlers that would have worked had they been public. Nothing I could use. I ended up using a worker that’s polling Tasks every second in a background thread (yikes).
  • Remember we added the button so we don’t add it again next time.
  • Add the Call button on all phone number fields: fax, mobile phone, home phone, etc.
  • Validate the phone number with a regular expression that complies with the specifications.
  • Make the outgoing call thru the Cisco IP phone instead of using Skype.
  • Listen for incoming phone calls.
  • Move the script to a widget using the Smart Office SDK.

That’s it! If you like this post, subscribe to this blog. And if you rock, become an author to share your ideas.