Introduction to MForms extensions

When extending the functionality of M3 panels in MForms you have three different options and these are JScript files, Script assemblies and MForms extensions. MForms extensions allow you to do similar things as with a regular JScript files but extensions are Smart Office features, written in a .NET language such as C#. The way extensions are deployed, configured and registered is also somewhat different compared to how you usually do with JScript.

This post is a brief introduction of how to create and deploy an MForms extension using the Smart Office SDK. Note that if you don’t have access to the SDK you will not be able follow along in the example on your own. I will also assume that you are familiar with the Smart Office SDK and will mainly focus on what is specific for MForms extensions.

When to use extensions

Before we get into the details I just want to clarify a couple of things. MForms extensions is not a replacement for Jscript or Script assemblies and you should only use them in scenarios where they make sense.

The main scenario for using extensions is when you want to add some generic functionality in many programs and panels. Adding generic functionality in many programs and panels with JScript is a lot of work since the scripts must be configured on each panel. In such cases extensions could be an option.

You could also use extensions for complex or large solutions. It is easier to create those in C# with Visual Studio and it is possible to debug but you basically get the same benefits by using Script assemblies.

You should not use extensions for simple things that could just as well be done in a JScript file. It is harder to deploy and modify an extension compared to a script and the added complexity is not worth it for small solutions.

How it works

In the MForms assembly there is an ExtensionService that can be used to register MForms extensions. A feature would typically register an extension when its application is initialized. You can wait until MForms is available before registering but that is not strictly required. One thing that is required though is to check if extensions are allowed using the IsEnabled property on the ExtensionService. There is a setting in MForms called EnableExtensions that is used to enable/disable the extension functionality.

When registering an extension you provide a PanelExtensionRegistration instance that describes your extension. An extension can have different scopes such as panel, program and global. The scope determines when the extension will be called. A global extension will be called on every panel in every program. When the panel scope is used the extension will only be called on a specific panel.

When the program is running the extensions that are registered will be called in way similar to how JScripts are called if the scope in their registrations match the current panel.

The Smart Office SDK documentation contains more information about all the classes in the MForms.Extension namespace.

A word of warning

MForms extensions can be very powerful if used correctly but you can also get into trouble. You should always be very careful to not execute server requests or other long running operations on the UI thread but this is even more important with extensions. If you create an extension with a global scope and that extension takes too long time to execute the Load method the entire M3 system will feel slow since every panel is affected.

Register extensions with a narrow scope if possible and only use global extensions when it’s really necessary. When creating a global extension be extra careful and make sure the Load method returns as quickly as possible.

If your extension throws exceptions from the Load method the extension will be unregistered and will not be loaded again until the client is restarted. If this is the behavior you want it is OK to throw exceptions from the Load method but make sure that you handle exceptions everywhere else and never ever throw exceptions on background threads or from event handlers.

Creating feature projects

To create and test an extension you need to create two projects using the SDK, one feature project and on client project. This is documented in the SDK documentation so I won’t cover that here.

For my example I created a feature called ExtensionDemo. You can use the M3 application group for your application in the manifest so that your feature will not be loaded if the M3 application group is not enabled. The exception to this is if you want to have a profile fragment or a settings file defined in the manifest. In this case you need to have your own application group.

Add a reference to MForms.dll in both the feature project and the client project, this will be required in the following steps. You also need to make sure that the manifest for the MForms feature (Infor.M3.manifest) is in the bin\Debug (or bin\Release) directory for the client project. You can copy the manifest there or add a link to the manifest in the client project (Add Existing item, add as link) and set Copy to output directory to Copy if newer.

The solution in Visual Studio should now look something like this.

M3EXT_Projects

Creating extension classes

For this example I will create two extensions classes, one global called GlobalDemoExtension and one panel specific called PanelDemoExtension. Both classes must implement the IPanelExtension interface that contains a single method called Load. I added a logger instance and a try/catch statement in the Load method. In the catch block the exception is logged and then a new ApplicationException is thrown. This will cause the extension to be unregistered. You will have to decide which exceptions that are fatal for your extension and which exceptions that can be handled. If any exception is thrown from the Load method the extension will be unregistered.

The source for the GlobalDemoExtension class can be seen below. Before adding more code I will show how to register the extensions.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MForms.Extension;

namespace ExtensionDemo
{
    public class GlobalDemoExtension : IPanelExtension
    {
        private static readonly log4net.ILog Logger = Mango.Core.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        public void Load(PanelExtensionEventArgs e)
        {
            try
            {

            }
            catch (Exception ex)
            {
                Logger.Error("Failed to handle extension event", ex);
                throw new ApplicationException();
            }
        }
    }
}

Registering extensions

To register the extensions I created a RegisterExtensions method in the application class for my feature (ExtensionDemoApplication). The method is then called from the Create method.

Each extension is registered by creating a PanelExtensionRegistration instance, setting up some properties and calling the RegisterPanelExtension method on the ExtensionService. The registration should have a unique ID and I have chosen to use the fully qualified name of the extension classes.

The scope is set by creating a new ExtensionScope instance. For the global extension the IsGlobal property is set to true. For the panel extension I set the Panel to MMA001E0 which means that the extension will only be called on the E-panel in the MMS001 program. Some kind of visible name must also be set.

The final step is to decide how you want the extension to be created. You can create the extension instance yourself by setting the ExtensionSingleton property or let the framework create the extension instance by setting the ExtensionType property. In my example I have used both options. If you choose the singleton option you must make sure that your extension class is stateless and can be called multiple times from different panels and programs. If you choose the type option a new instance of your extension class will be created each time the extension is called. This is similar to how JScript works when new instance is created each time.

The RegisterExtension method could look something like this.

private void RegisterExtensions()
{
    if (!ExtensionService.Current.IsEnabled)
    {
        return;
    }

    var registration = new PanelExtensionRegistration
    {
        Id = typeof(GlobalDemoExtension).FullName,
        Scope = new ExtensionScope { IsGlobal = true },
        ExtensionSingleton = new GlobalDemoExtension(),
        VisibleName = "Global extension demo"
    };
    ExtensionService.Current.RegisterPanelExtension(registration);

    registration = new PanelExtensionRegistration
    {
        Id = typeof(PanelDemoExtension).FullName,
        Scope = new ExtensionScope { Panel = "MMA001E0" },
        ExtensionType = typeof(PanelDemoExtension),
        VisibleName = "Panel extension demo"
    };
    ExtensionService.Current.RegisterPanelExtension(registration);
}

Implementing the extensions

Without any real scenarios to show I will just implement two simple demo examples. I am sure that you can come up with some nice real world scenarios where extensions could be useful.

For the global extension I will just add a big red text in the middle of the panel that shows the program name and the panel name. As the extension is global this text should show up on every panel in every M3 program. The PanelExtensionEventArgs parameter to the load method contains a Controller and PanelState property and I used the PanelState to get the program and panel names.

The panel extension will first check that it is actually running on the MMA001E0 panel. Depending on the scope of the extension and what the extension supports you will have to check the ProgramName and PanelName properties on the PanelState in the Load method. You should do these checks as early as possible to avoid running unnecessary code. Since the extension knows that it is running on a specific panel we can find a known field on that panel. Note that the field name might be different depending on the BE version. If the item number field is found I show a message that will be displayed in the status bar or in a dialog depending on the user settings.

The source code for both the extensions can be seen below.

using MForms;
using MForms.Extension;
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace ExtensionDemo
{
    public class GlobalDemoExtension : IPanelExtension
    {
        private static readonly log4net.ILog Logger = Mango.Core.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        public void Load(PanelExtensionEventArgs e)
        {
            try
            {
                var panelState = e.PanelState;
                var message = panelState.ProgramName + " / " + panelState.PanelName;
                var textBlock = new TextBlock
                {
                    Text = message,
                    FontSize = 30,
                    Foreground = Brushes.Red,
                    HorizontalAlignment = HorizontalAlignment.Center,
                    VerticalAlignment = VerticalAlignment.Center
                };

                Grid.SetColumnSpan(textBlock, Configuration.GridColumnsMax);
                Grid.SetRowSpan(textBlock, Configuration.GridRows);
                panelState.Content.Children.Add(textBlock);
            }
            catch (Exception ex)
            {
                Logger.Error("Failed to handle extension event", ex);
                throw new ApplicationException();
            }
        }
    }
}
using MForms;
using MForms.Extension;
using System;
using System.Windows.Controls;

namespace ExtensionDemo
{
    public class PanelDemoExtension : IPanelExtension
    {
        private static readonly log4net.ILog Logger = Mango.Core.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        public void Load(PanelExtensionEventArgs e)
        {
            try
            {
                var panelState = e.PanelState;
                if (panelState.PanelName != "MMA001E0")
                {
                    return;
                }

                const string fieldName = "WEITNO";
                var textBox = ScriptUtil.FindChild(panelState.Content, fieldName) as TextBox;
                if (textBox != null)
                {
                    var itemNumber = textBox.Text;
                    e.Controller.RenderEngine.ShowMessage(string.Format("The item number is: {0}", itemNumber));
                }
                else
                {
                    Logger.WarnFormat("Field {0} not found on panel {1}", fieldName, panelState.PanelName);
                }
            }
            catch (Exception ex)
            {
                Logger.Error("Failed to handle extension event", ex);
                throw new ApplicationException();
            }
        }
    }
}

Testing the extensions

Assuming that the feature and client projects are setup correctly you should be able to set breakpoints in your code, press F5 in Visual Studio and when the extensions are called the breakpoints should be hit. It is always a good idea to start with a breakpoint in the Create method of your application to make sure that the application is created and that your extension registration code is executed. You should also set breakpoints in the Load methods for the extensions to make sure that they are called when expected.

If you have issues with MForms not loading make sure that both MForms.dll and the MForms manifest (Infor.M3.manifest) can be found in the bin\Debug (or bin\Release) directory of your client project.

With my example code I get the result in the image below for the E-panel in MMS001. The red text is from the GlobalDemoExtension class and the message in the status bar is from the PanelDemoExtension class.

M3EXT_MMA001E0

Conclusion

This was just a very quick overview of MForms extensions and unless you have experience with both JScript and the Smart Office SDK it might be a bit confusing. Either way I hope that you got some ideas of how it is possible to extend the functionality of M3 using MForms extensions. Just remember that “With great power comes great responsibility”.

If you have questions or suggestions for future posts in this area post a comment below.

9 thoughts on “Introduction to MForms extensions

  1. Mario

    Hi, nice post, and useful.
    I am starting in M3 Jscript world (I use LSO 9.1), and I would like to ask your if does exists a MForms classes list, or guide, where classes were detailed for its use in scripting.
    regards

    1. norpe Post author

      In the Smart Office SDK there is a document called M3DevelopersGuide.pdf that contains information about JScript. The SDK also contains a help file named InforSmartOfficeSDK.chm with all the public APIs that are allowed to be used for JScript and C# feature development.

  2. perlun

    Very nice functionality indeed. I think this is far superior than JScripting in many cases, especially when you want to build up a more complex UI and so forth + the benefit of IntelliSense etc. The Microsoft JScript.NET runtime also has some critical bugs (for example when using eval() from multiple threads at the same time) which makes .NET/C# much more “stable” under such circumstances.

  3. Priyantha

    solution is grate !!! , i have small question , think i want to add new ListView . then i can write folling code .

    System.Windows.Controls.ListView lv = new System.Windows.Controls.ListView();
    lv.SetValue(Grid.RowProperty, 4);
    //grid.Children.Add(lv);

    GridView gv = new GridView();
    GridViewColumn gvc1 = new GridViewColumn();

    gvc1.DisplayMemberBinding = new Binding(“Company”);
    gvc1.Header = “Company”;
    gv.Columns.Add(gvc1);

    GridViewColumn gvc2 = new GridViewColumn();
    gvc2.DisplayMemberBinding = new Binding(“Facili”);
    gvc2.Header = “Facili”;
    gv.Columns.Add(gvc2);

    GridViewColumn gvc3 = new GridViewColumn();
    gvc3.DisplayMemberBinding = new Binding(“Name”);
    gvc3.Header = “Last Name”;
    gv.Columns.Add(gvc3);

    lv.ItemsSource = (IEnumerable)facilities;
    lv.View = gv;

    then how can i apply default style that in Lawson smart office ListView. please help me

    1. karinpb

      Hi,
      You need to add:
      lv.ItemContainerStyle = Mango.UI.Core.StyleManager.StyleListViewItem;
      gv.ColumnHeaderContainerStyle = Mango.UI.Core.StyleManager.StyleGridViewColumnHeader;

      You don’t need to set the style for the list itself since that is set by the global style that we have. But in case you need it you should set this:
      lv.Style = Mango.UI.Core.StyleManager.StyleListView;

      The StyleManager has a large set of styles that you can use. If you have the Smart Office SDK it will be included in the documentation. If you don’t have that I recommend you to have a look in Mango.UI.dll with dotPeek.

  4. Jimmy Ehrnström

    I really appreciated this guide. One of the few that was well written and worked at first attempt.

    I have tried to expand on your example by adding an Image control with support for dropping externa files onto. I get the event handlers for DragEnter, DragOver and DragLeave to trigger, but not the Drop event. Should this be possible within the SDK? Any clues to what I might be missing? I have also tried more simple Mouse events and they fire as long as I’m just clicking on the image, without attempting to drop files.

    Any tips would be greatly appreciated!

    1. Jimmy Ehrnström

      First I want to clarify that I meant well written compared to guides on the internet in general. On this blog almost all tutorials are very good. 🙂
      Secondly I managed to find a solution to my issue with the non-triggered Drop event. It was in short that I had to move a bit of my code from the DragEvent to the DragOver handlers.

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

Comments are closed.