Tools to develop Smart Office Scripts

I often get the question about which tool do I use to develop Scripts for Lawson Smart Office. Here’s my list of tools.

Smart Office Script Tool

The Smart Office Script Tool comes with all installations of Smart Office and can be run with mforms://jscript. It is basically the only tool you need to develop Scripts for Smart Office. It creates a simple JScript.NET source code template (File > New), it shows the list of current Elements with their Name, Control Type, position, and value, it has syntax coloring, it can hook to an External Editor, you can play with the Arguments, it has a debug console, and most importantly it has the Compile and Run buttons.

Here is a screenshot of a template script compiled and run:

Notepad++

I use Notepad++ for most everything text editing; it’s my vi. It has syntax coloring for JavaScript (which is close enough to JScript.NET) and C#. I hook it to the Script Tool (Script Tool > Script > Use external editor). Also, I use Notepad++ Plugin Manager (Plugins menu) to add the TextFX, Compare, XML/HTML, and HEX plugins.

Here is a screenshot of a simple script in Notepad++:

Sublime Text

I also started using Sublime Text 2 as an alternate text editor. I like the more readable syntax coloring, and the Minimap on the right.

Here is a screenshot of a simple script in Sublime Text 2:

Smart Office SDK

The Smart Office SDK is the premier source of information. We can see the official public interface, descriptive comments, and we can search for classes and members. Right now it’s only available to Infor employees and select partners.

.NET Reflector

Red Gate Software .NET Reflector is a MUST for any .NET development. I strongly recommend it. You can search for a Type, get the Class hierarchy, Analyze a Class, and the best is that you can disassemble the binary.

The Standard $35 version is enough for my needs. You will get the most value if you load the DLLs from Smart Office. The folder that contains the DLLs can be found with Smart Office > Help > About Lawson Smart Office > View log file. Then you have to go up and down the folder structure to find all the DLLs, or do a Search. The folder names have an ID that’s different on each Smart Office installation. On my laptop the paths are:

C:\Users\12229\AppData\Local\Apps\2.0\Data\79O176HR.9E2\N43AOMBD.0HB\http..tion_689b45b5b21234e1_0009.0001_46f64bcdec4dff2a\Data\
C:\Users\12229\AppData\Local\Apps\2.0\3YDTAV5W.D33\Q01ZYJYT.RW0\http..tion_689b45b5b21234e1_0009.0001_46f64bcdec4dff2a\

Import the Smart Office DLLs into .NET Reflector (File > Open Assembly) and start introspecting Smart Office. You can start with Mango.Core.dll, Mango.UI.dll, and MForms.dll. You can even make separate lists (File > Open Assembly List) of the different DLLs from the different versions of Smart Office.

Here is a screenshot of the Class for a B panel ListRow:

Microsoft .NET Framework Class Library

I refer extensively to Microsoft’s MSDN .NET Framework Class Library. When I need the API reference for a control (for example a TextBox) I just Google the following:

C# TextBox Class System.Windows.Controls site:microsoft.com

You will get the Inheritance Hierarchy, Syntax, Constructors, Properties, Methods, Events, Fields, and Examples.

Here is a screenshot of the TextBox Class:

Inspect

The Microsoft Windows SDK provides the great Inspect tool to dynamically introspect a program. You can start if from Windows > Start > Programs > Microsoft Windows SDK > Tools > Inspect Object. You can point and click at any element of Smart Office and the Inspect tool will show you the control’s Name, Type, Value, place in the Object tree, siblings, etc.

Here is a screenshot of the Inspect Tool highlighting in a yellow box a ListRow of a B panel:

UISpy

I have also used Microsoft UISpy but I changed laptops and I haven’t re-installed it yet.

Accessible Event Watcher

I have also used Microsoft Windows SDK Accessible Event Watcher (AccEvent). You can start it from Windows > Start > Programs > Microsoft Windows SDK > Tools > Accessible Event Watcher.

Here is a screenshot of AccEvent listening to my Smart Office clicks:

Microsoft C# Express

Sometimes I use Microsoft Visual C# 2010 Express . It doesn’t have support for JScript.NET but I can create compiled C# Scripts, for its IntelliSense autocompletion, and for the step by step debug options.

Here is a screenshot of a compiled Smart Office Script in C#:

Microsoft JScript.NET compiler

Sometimes I use jsc.exe to compile Scripts from the command line, outside of Smart Office. You need to reference various .NET paths. I have these ones in my laptop:

C:\WINDOWS\Microsoft.NET\Framework\v1.0.3705\
C:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\
C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\
C:\WINDOWS\Microsoft.NET\Framework\v3.0\
C:\WINDOWS\Microsoft.NET\Framework\v3.5\
C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319\
C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\
C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.5\

Also, you can compile Smart Office Scripts from the command line if you reference the Smart Office DLL paths (see the .NET Reflector chapter above). They won’t always execute correctly as they will be missing the dynamic context of Smart Office.

Also, you can even reference a specific DLL like this:

jsc.exe /reference:Interop.SKYPE4COMLib.dll Test.js

Here is a screenshot that shows Hello World! from a Smart Office Script in JScript.NET compiled at the command line (note the extra print statement at the bottom):

Fiddler

Microsoft’s Fiddler tool is your HTTP best friend. It will capture HTTP traffic between Smart Office and M3. It will even capture and decrypt Smart Office’s encrypted traffic. For example, press F5 in CRS610/B to refresh the panel and you will see the HTTP Request to the MvxMCSvt servlet with all the values of the panel. Or for example, create a Script that makes an M3 API call and you will see the HTTP Request to MIAccess with all the Input and Output parameters. It’s fantastic for debugging!

Here is a screenshot that shows CRS610/B displaying a record:

Snoop

Snoop is another great WPF spying utility like the Inspect tool of the Microsoft Windows SDK.

Snoop also shows the 3D breakdown of how the controls are visually rendered hierarchically on the screen:

JetBrains

dotPeek is a free-of-charge .NET decompiler from JetBrains that can be used like Red Gate Reflector.

Disclaimer

The opinions expressed in this blog are my personal opinions only, and not those of Lawson nor Infor. By using recommendations from this blog your code might break with upgrades of Smart Office. To ensure your code won’t break, use only the Smart Office API which you can find in the Smart Office Developer’s Guide.

UPDATE 2012-04-24: Added paragraphs for Sublime Text 2, and Smart Office SDK.

UPDATE 2012-07-12: Added paragraphs for JetBrains .NET decompiler, and Snoop the WPF spy utility, following Karinpb‘s recommendation.

Date picker in Mashup

Here’s the XAML code to add a date picker in a Mashup, using DatePicker:

<DatePicker Name="dp" SelectedDateFormat="Short" />

The result looks like this:

If you want to get the value of the date picker use this:

{Binding ElementName=dp, Path=Text}

Here’s a simple Mashup that shows the selected date in a Label:

<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:wf="clr-namespace: System.Windows.Forms;assembly=System.Windows.Forms" xmlns:wfi="clr-namespace: System.Windows.Forms.Integration;assembly=WindowsFormsIntegration">
    <Grid.Resources>
    </Grid.Resources>

    <Grid.ColumnDefinitions>
       <ColumnDefinition Width="Auto" />
       <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
       <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>

    <DatePicker Name="dp" SelectedDateFormat="Short" Grid.Column="0" />
    <Label Name="Date" Content="{Binding ElementName=dp, Path=Text}" Grid.Column="1" />

 </Grid>

That’s it!

How to add a column to a list

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

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

This is the desired result:

1. Add a column by changing the View in CRS020

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

2. M3 modification

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

3. Add a column programmatically with a script

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

Append the column

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

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

Second, we append a new GridViewColumn to the ListView:

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

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

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

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

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

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

The complete script looks like this:

Populate with data

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

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

Load more data on scroll view

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

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

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

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

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

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

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

Here’s an illustration:

Now we know exactly which rows are new:

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

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

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

Here is a screenshot of the final result:

Here’s the complete source code:

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

 package MForms.JScript {
     class Test {

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

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

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

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

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

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

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

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

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

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

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

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

     }
 }

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

UPDATE 2012-09-27

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

Related articles

How to consume a Lawson Web Service from a Personalized Script in Smart Office

Calling Lawson Web Service (LWS) from a Personalized Script in Lawson Smart Office is very useful as LWS has three adapters: M3 API, SQL, and M3 Display Program (MDP).

As of today, there are three known solutions to call LWS from a script: 1) the “Big string”, 2) the XML writer, and 3) the C# proxy written with Microsoft Visual Studio. Each of these solutions has its advantages and disadvantages.

In the paper How to call LWS from a JScript I illustrate a new solution which complements the other three known solutions. This new solution is interesting as it minimizes code source surface while still ensuring SOAP validation. And the solution does not involve any C# coding, nor does it require Microsoft Visual Studio. For this new solution, we will use the free Microsoft Web Services Description Language Tool (wsdl.exe) to generate a proxy class in C# that we’ll use from JScript.NET.

As an addendum to the paper, I write two additions. First, it’s possible to make the C# proxy re-usable for multiple environments (ex: DEV, PRD, TST) by setting the variable Url of the proxy instance. Second, I have successfully tested this technique with all three LWS adapters: M3 API, SQL, and MDP.

Related articles

How to pass settings to a script

In this post I discuss several techniques to pass settings to a Personalized Script for Lawson Smart Office.

Suppose we have three settings: fieldX, and Y that we want to pass to a script with the respective values WRPHNO, 33, and 17. The question is how do we pass those settings and their values to the script? I can think of five techniques: 1) hard-code the settings in the source code, 2) use comma separated values and String.Split, 3) use JSON, 4) use XML, or 5) use Java Properties or .NET Resource files.

1. Hard-code the settings

The first technique is to hard-code the settings as Fields in the class declaration of the script:

var field = "WRPHNO";
var X = 33;
var Y = 17;

Here is an example:

Or we can use an Array:

var settings = ["WRPHNO", 33, 17];
settings[0] // field
settings[1] // X
settings[2] // Y

Here is an example with the Array:

2. Use comma separated values and String.Split

The second technique is to pass a comma separated list of values like WRPHNO,33,17 as the argument of the script, and to access the values as an array after a String.Split:

var settings = args.Split(",");
settings[0] // field
settings[1] // X
settings[2] // Y

Here is an example:

We can also read the values from a file:

var settings = System.IO.File.ReadAllText("C:\\path\\settings.txt").Split(",");

3. Use JSON

The third technique is to use JSON. With JSON we write our settings in object literal notation like this:

{ "field": "WRPHNO",
 "X": 33,
 "Y": 17 }

We set the JSON text as the argument of the script, surrounded by parenthesis, and we call eval(JSON) from the script. The settings become a JScript object that we can access with settings.field, settings.X, and settings.Y. Here is an example:

We can also read the JSON text from a file:

var JSON = System.IO.File.ReadAllText("C:\\path\\settings.txt");
var settings = eval("("+JSON+")");

4. Use XML

The fourth technique is to pass XML as the argument of the script, and to use XPath in SelectSingleNode to access the values:

var doc = new XmlDocument();
doc.LoadXml(args);
doc.SelectSingleNode("/settings/field").InnerText
doc.SelectSingleNode("/settings/X").InnerText
doc.SelectSingleNode("/settings/Y").InnerText

Here is an example:

We can also load the XML from a file:

doc.Load("file://hostname/settings.xml");

5. Use Properties or Resource files

The fifth technique is to use Java Properties files or .NET Resource files, but I couldn’t find a concise solution that fits in only a couple of lines of code; it seems to require many lines of code.

UPDATE 2012-12-14: I would copy/paste my properties file to the Smart Office installation point folder on the web server, and refer to it from the script as http://smartoffice/LSO/thibaud.properties using standard .NET classes to read files. If you have an example let me know and I can post it here.

Discussion

The hard-coded values are simple to implement for the developer, but it makes the script non reusable by nature, and it invites a risk of corruption when the administrator has to manually edit the source code to change the settings.

The comma separated list of values and String.Split is also simple to implement for the developer, but maintenance is inversely proportional to scalability: the more settings we add the harder it becomes to identify which value is located where in the string. Also, if by mistake a comma goes missing the whole settings are compromised.

Using JSON is great because of the possibilities to validate the JSON object with JSlint and JSONLint. Also, it’s scalable: we can store a large number of settings in complex structures like in sub-settings or in a tree of settings and still maintain readability.

Using XML is great for all the advantages of XML, like editing tools and semantic validation.

Settings page

I encourage implementing a settings page as an HTML form for the administrators to easily configure the script and save the settings in the XML Customization file in Smart Office.

In our example, and with JSON, we would have the following HTML form:

<input id="field"/>

We create a JSON string in JavaScript like this:

var settings = {};
settings.field = document.getElementById("field").value;
settings.X = document.getElementById("X").value;
settings.Y = document.getElementById("Y").value;
var JSONtext = JSON.stringify(settings);

The JSON object is provided as a JS file by Douglas Crockford here, and it is also natively supported in modern web browsers like Microsoft Internet Explorer 8 and in Google Chrome.

Our settings page would look like this:

The source code for that simple settings page is:

// <![CDATA[
javascript" src="https://raw.github.com/douglascrockford/JSON-js/master/json2.js">
// ]]>// Field  X  Y  

Here are two sample web pages that use JSON for passing plenty of settings to a script to be stored in the XML Customization file:

Sample 1: http://ibrix.info/AddressValidation/settings/

Sample 2: http://ibrix.info/skype/settings/

Customization file

Once the settings are generated, either as a comma separated list of values, or as JSON text, or as XML, we can save the result in the argument attribute of the Customization file in Smart Office. The value must be XML escaped with for example this online tool.

Here is an example of an XML Customization file for a script AddressValidationM3 for CRS610/E with plenty of XML-escaped JSON as the argument:

For more information on XML Customization files in Smart Office, refer to the Chapter Managing M3 MForms Personalizations of the Lawson Smart Office Administration Guide:

How to add filters to a ListView in a Mashup

What is the XAML code needed to add filters to a ListView in a Mashup in Lawson Smart Office? The solution is to put a StackPanel with a Label and a TextBox inside the GridViewColumn:

<GridViewColumn>
    <StackPanel>
        <Label Content="Location" Foreground="White" />
        <TextBox Name="WHSL" />
    </StackPanel>
</GridViewColumn>

Here is a screenshot of a desired M3 program (for example: MWS060/B) with filters in the list (in this case: Receiving number, Location, and Lot number); we want to reproduce that list and its filters as a Mashup:

And here is the resulting Mashup with the filters in the list; the result is surprisingly similar to the actual M3 panel:

The particular Mashup shown in the screenshot above calls a REST Web Service made with JSP and which is not provided here. The Mashup passes the values of the filters as parameters of the Web Service in the URL. The Web Service executes SQL against M3 using JDBC and returns the result set as XML. Finally, the Mashup displays the resulting XML in the ListView as data in columns and rows. Here’s the full XAML code of that Mashup:

<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" Margin="10">
	<Grid.Resources>
	</Grid.Resources>

	<Grid.ColumnDefinitions>
		<ColumnDefinition Width="*" />
	</Grid.ColumnDefinitions>
	<Grid.RowDefinitions>
		<RowDefinition Height="Auto" />
		<RowDefinition Height="*" />
	</Grid.RowDefinitions>

	<StackPanel Orientation="Horizontal" Grid.Row="0">
		<Label Content="Warehouse" VerticalAlignment="Center" Margin="3" />
		<TextBox Name="WHLO" VerticalAlignment="Center" Margin="3" />
		<Label Content="Item number" VerticalAlignment="Center" Margin="3" />
		<TextBox Name="ITNO" VerticalAlignment="Center" Margin="3" />
		<Label Content="Container" VerticalAlignment="Center" Margin="3" />
		<TextBox Name="CAMU" VerticalAlignment="Center" Margin="3" />

		<Button Content="Apply" IsDefault="True" VerticalAlignment="Center" Margin="3" Width="55">
			<Button.CommandParameter>
				<mashup:Event TargetName="WebService" TargetEventName="List">
					<mashup:Parameter SourceKey="WHLO" Value="{Binding ElementName=WHLO, Path=Text}" />
					<mashup:Parameter SourceKey="ITNO" Value="{Binding ElementName=ITNO, Path=Text}" />
					<mashup:Parameter SourceKey="CAMU" Value="{Binding ElementName=CAMU, Path=Text}" />
					<mashup:Parameter SourceKey="REPN" Value="{Binding ElementName=REPN, Path=Text}" />
					<mashup:Parameter SourceKey="WHSL" Value="{Binding ElementName=WHSL, Path=Text}" />
					<mashup:Parameter SourceKey="BANO" Value="{Binding ElementName=BANO, Path=Text}" />
				</mashup:Event>
			</Button.CommandParameter>
		</Button>
	</StackPanel>

	<mashup:DataListPanel Name="WebService" Grid.Row="1" Margin="3">
		<mashup:DataListPanel.Events>
			<mashup:Events>
				<mashup:Event SourceEventName="Startup" />
			</mashup:Events>
		</mashup:DataListPanel.Events>
		<mashup:DataListPanel.DataService>
			<mashup:DataService Type="REST">
				<mashup:DataService.Operations>
					<mashup:DataOperation Name="List">
						<mashup:DataParameter Key="REST.BaseAddress" Value="http://hostname/ItemSearchMashup.jsp?WHLO={WHLO}&amp;ITNO={ITNO}&amp;CAMU={CAMU}&amp;REPN={REPN}&amp;WHSL={WHSL}&amp;BANO={BANO}" />
						<mashup:DataParameter Key="REST.RemoveNamespace" Value="True" />
						<mashup:DataParameter Key="REST.XPath" Value="root/r" />
					</mashup:DataOperation>
				</mashup:DataService.Operations>
			</mashup:DataService>
		</mashup:DataListPanel.DataService>
		<ListView Name="Customers" ItemsSource="{Binding Items}" Style="{DynamicResource styleListView}" ItemContainerStyle="{DynamicResource styleListViewItem}">
			<ListView.View>
				<GridView ColumnHeaderContainerStyle="{DynamicResource styleGridViewColumnHeader}">
					<GridView.Columns>
						<GridViewColumn Header="Whs" DisplayMemberBinding="{Binding XPath=MLWHLO}" />
						<GridViewColumn DisplayMemberBinding="{Binding XPath=MLREPN}">
							<StackPanel>
								<Label Content="Recvng no" Margin="0" Foreground="White" />
								<TextBox Name="REPN" Margin="0" />
							</StackPanel>
						</GridViewColumn>

						<GridViewColumn Header="Item number" DisplayMemberBinding="{Binding XPath=MLITNO}" />

						<GridViewColumn Header="Name" DisplayMemberBinding="{Binding XPath=MMITDS}" />
						<GridViewColumn Header="Supplier" DisplayMemberBinding="{Binding XPath=LMSUNO}" />
						<GridViewColumn DisplayMemberBinding="{Binding XPath=MLWHSL}">
							<StackPanel>
								<Label Content="Location" Margin="0" Foreground="White" />
								<TextBox Name="WHSL" Margin="0" />
							</StackPanel>
						</GridViewColumn>

						<GridViewColumn DisplayMemberBinding="{Binding XPath=MLBANO}">
							<StackPanel>
								<Label Content="Lot number" Margin="0" Foreground="White" />
								<TextBox Name="BANO" Margin="0" />
							</StackPanel>
						</GridViewColumn>

						<GridViewColumn Header="On-hand" DisplayMemberBinding="{Binding XPath=MLSTQT}" />
						<GridViewColumn Header="Allocatble" DisplayMemberBinding="{Binding XPath=Allocatble}" />
						<GridViewColumn Header="Sts" DisplayMemberBinding="{Binding XPath=MLSTAS}" />
						<GridViewColumn Header="M dt" DisplayMemberBinding="{Binding XPath=LMMFDT}" />
						<GridViewColumn Header="Exp dt" DisplayMemberBinding="{Binding XPath=LMEXPI}" />
						<GridViewColumn Header="Sls dt" DisplayMemberBinding="{Binding XPath=LMSEDT}" />
					</GridView.Columns>
				</GridView>
			</ListView.View>
		</ListView>
	</mashup:DataListPanel>

</Grid>

That’s it!

Web Service pretty print

Here is a technique that uses XSLT to pretty print the XML metadata of a Lawson Web Service. The output shows each operation’s details (name, input/output parameters, type, length, constraint, SQL statement, etc.) in a human readable HTML format. This technique is useful for creating template spreadsheets in Excel where we input data without having to manually enter the headers in the spreadsheet which is error prone. The result is similar to the template spreadsheets generated by Smart Data Tool.

  1. Suppose we have the following Thibaudweb service with three operations, one of each type, API, MDP, and SQL:

  2. Save the XML metadata into a file somewhere in your computer. For that, go to the Lawson Web Service server view (for example: http://hostname/LWS_DEV/), select List Services, expand your web service (in my case Thibaud), right-click the Meta Data link, select Save Target As, and save the XML file somewhere in your computer:
  3. Then, open the XML file in a text editor, insert the following processing instruction at the top of the file, and save the file:
    <?xml-stylesheet type="text/xsl" href="WebServicePrettyPrint.xslt"?>
  4. Then, save a copy of the following XSLT file to somewhere in your computer, in the same folder as the XML file: http://ibrix.info/lws/WebServicePrettyPrint.xslt
  5. Then, open the XML file in Microsoft Internet Explorer. The XSLT processor of Internet Explorer’s MSXML will convert the XML metadata into HTML using the XSLT file above. The HTML output shows each operation’s details (name, input/output parameters, type, length, constraint, SQL statement, etc.) in a human readable HTML format. The result looks like this:
  6. Internet Explorer will show a security warning asking if you want to run the script. Click Allow blocked content. The blocked content is a small piece of JavaScript code that transposes the HTML tables.
  7. The little button at the top right transposes the HTML tables. Click the button. Copy/paste the transposed table in an Excel spreadsheet. That will serve as the header. Now just enter the data. That’s useful to create the template spreadsheets in Excel similar to Smart Data Tool.

That’s it!

 

UPDATE 2012-08-14: Added support for MDP Output fields and Related Programs.

Print M3 programs locally

Did you ever want to quickly print a screen capture of the current M3 program using any of your local Windows printers and printer preferences?

Print Screen

One known technique is to press the Print Screen button on the keyboard, and to paste the resulting bitmap image into Microsoft Paint or Microsoft Word, and to print from there. The result is a bitmap print. The advantages are: the truthfulness of the result which is a replica of what the user sees on the screen (WYSIWYG), the ability to print locally using the locally configured Windows printers, as well as the last minute control of the printer preferences. The disadvantage is the poor non vectorial quality, and the waste of printer ink used in printing background colors.

MOM, StreamServe

The other known technique is to print using the standard M3 functionality via MOM and streamfiles to StreamServe. The result is a vectorial print. The advantages are: the high quality of the vectorial print, the ability to customize the resulting documents via StreamServe Design Center, and the pre-configuration of the printer preferences in MOM. Also, StreamServe can print more complex content like barcodes. The disadvantage is that pretty much each M3 program needs to be configured via MOM and StreamServe, which requires custom implementation, as well as the inability to print locally using the locally configured Windows printers, and the inability to have last minute control of the printer preferences.

Script

It’s actually possible to programmatically print a screen capture of the current M3 program with a simple Personalized Script for Lawson Smart Office in JScript.NET. The result is similar to using the Print Screen button, while avoiding the extra steps of pasting the bitmap image into Microsoft Paint or Microsoft Word. One click print, locally.

For that, the script must get a reference to the current M3 window, convert the content from vectorial to bitmap using RenderTargetBitmap and BmpBitmapEncoder, and to print it using PrintDocument.

import System;
import System.IO;
import System.Drawing;
import System.Drawing.Printing;
import System.Windows.Media;
import System.Windows.Media.Imaging;

package MForms.JScript {
	class PrintMe {
		var img;
		public function Init(element: Object, args: Object, controller : Object, debug : Object) {
			try {

				// get the current window
				var h = controller.RenderEngine.Host;
				var e = h.VisualElement;

				// convert vectorial to bitmap
				var RTbmap: RenderTargetBitmap = new RenderTargetBitmap(h.Width, h.Height, 96, 96, PixelFormats.Default);
				RTbmap.Render(e);
				var encoder = new BmpBitmapEncoder();
				encoder.Frames.Add(BitmapFrame.Create(RTbmap));
				var stream = new MemoryStream();
				encoder.Save(stream);
				var gdiBitmap = new Bitmap(stream);
				stream.Close();
				stream.Dispose();
				this.img = gdiBitmap;

				// print
				var pd: PrintDocument = new PrintDocument();
				pd.add_PrintPage(OnPrintPage);
				pd.Print();

			} catch (ex: Exception) {
				debug.WriteLine(ex);
			}
		}
		function OnPrintPage(sender: Object, e: PrintPageEventArgs) {
			e.Graphics.DrawImage(this.img, 0, 0);
		}
	}
}

There are also options to preview the document and to open the printer preferences but I haven’t yet succeeded in using them correctly:

(new PrintPreviewDialog()).ShowDialog();
(new PrintDialog()).ShowDialog();

Suppose you have an M3 program like this:

The result of the print would look like this (using printer PDF995):

The next step is to find a solution to add a margin, polish the print, and show the print preview and printer preferences.

The last step would be to place the script in a new Print button in the M3 panel, or to inject a new Print option in the File menu, to deploy the script on the server, and to attach it to the desired M3 programs with the XML Customization files. With that, the user would be able to quickly and locally print the M3 programs.

SQL to XML in a Script

Here’s an example of how to read data from M3’s database using SQL, and how to convert the result into XML, in a Personalized Script for Lawson Smart Office. This example is for Microsoft SQL Server.

import System.Data;
import System.Data.SqlClient;

package MForms.JScript {
     class Test {
         public function Init(element: Object, args: Object, controller : Object, debug : Object) {
             var connection = new SqlConnection('server=sqlserver;database=M3EDBTST;uid=userid;pwd=password');
             connection.Open();
             var cmd: SqlCommand = new SqlCommand('SELECT DISTINCT OKCONO, OKCUNO FROM MVXJDTA.OCUSMA', connection);
             var da: SqlDataAdapter = new SqlDataAdapter(cmd);
             var ds: DataSet = new DataSet('result');
             da.Fill(ds);
             debug.WriteLine(ds.GetXml());
         }
     }
}

It produces the following XML:

<result>
  <Table>
    <OKCONO>1</OKCONO>
    <OKCUNO>Y60000    </OKCUNO>
  </Table>
  <Table>
    <OKCONO>1</OKCONO>
    <OKCUNO>Y60001    </OKCUNO>
  </Table>
  ...
</result>

The result looks like: