Intercepting Events and Adding Custom Logic in Sage 300c Customizations

Sage details this well in their Order Entry Customization Tutorial (Section 4.3) in the SDK.

There are 2 ways:

  1. Unbind the existing handler from the event, and bind the event to your own custom handler. You can then call the original handler from your own custom handler.
  2. Interception Ajax calls using the ajaxSuccess and ajaxComplete functions.

Which one you decide to use, depends on your scenario and desired behaviour. For example, intercepting ajax calls may not be the way to go, if you need to do some custom processing before letting the existing event continue. Due to javascript’s asynchronous nature, you’d need to use the unbinding/binding method.

Sage has great examples of this in their SDK. We’ll show you another rudimentary example here:

In this example, we are working with OE Order Entry. We want to do some custom logic before letting a user post an order. Before allowing a post, we want to ask the server (our own custom controller) if it is ok to post the order. It will return a value isApproved, which will indicate if we should post the order or not.

Upon the page being loaded, we first save the original Post button’s handler, so we can use it later. Then, we unbind the Post button’s event, and replace it with our own:

var customPostHandler = null;
    // Post button event intercept
    if ($('#btnPost')[0] == undefined) return;
    
    var postHandlers = $('#btnPost').data('events').click;
    if (postHandlers && postHandlers.length > 0) {
        customPostHandler = postHandlers[0].handler;
    }

Next, we unbind the Post button’s click handler, and replace it with our own:

// unbind the original
$('#btnPost').unbind('click');
// bind the modified one
$('#btnPost').click(function () {
        //check with controller to see if this user is allowed
        var url = sg.utls.url.buildUrl("CU", "HutilityCustomization", "CheckCustomerApproverPost");
        var approverUserId = $('#txtApproverUserId').val();
        var approverPw = $('#txtApproverPw').val();
        var customerNumber = orderEntryUI.finderData.CustomerNumber;
        if (approverUserId && approverPw) {
            sg.utls.showKendoConfirmationDialog(
                function () {
                    sg.utls.ajaxPost(url, { user: approverUserId, pw: approverPw, customerNumber: customerNumber}, HutilityOrderEntryCustomizationUICallback.jbtest);
                },
                null, "Approve order with user [" + approverUserId + "].", "Demo");
        }
        else {
            sg.utls.showValidationMessage("Please enter the credit approver credentials");
        }
    });
}

Next, we’ll initialize a callback function (HutilityOrderEntryCustomizationUICallback.jbtest) which will handle the data returned from our custom controller. If the response has isApproved as true, we allow the posting to continue by calling customPostHandler():

var HutilityOrderEntryCustomizationUICallback = {

    jbtest: function (data) {
        console.log(data);
        if (data.Data != undefined) {
            if (data.Data.isApproved == false) {
                sg.utls.showValidationMessage("Could not post order: " + data.Data.message);
            }
            else {
                customPostHandler();
            }
        }
        else {
            console.log("returned data is undefined");
        }
    },

};

Making Ajax POST Request in Sage 300c Customizations

Here’s how to make a POST request, using Sage’s supplied ajaxPost function:

sg.utls.ajaxPost(url, { user: approverUserId, pw: approverPw, customerNumber: customerNumber}, HutilityOrderEntryCustomizationUICallback.jbtest);

Here we’re supplying it with a url, our post data, and a callback function to handle the response.

Originally we tried to make POST requests using JQuery, but ran into issues. After inspecting the requests in Chrome Dev Tools, we found that the Sage ajaxPost includes tokens in the request that must be acting as CSRF tokens.

Sage 300c Customization: Modals, Dialog Boxes and Prompts

Plain old javascript dialogs and prompts cannot be readily used in customizations, as the web screens run inside of frames.

For example, running the below code:

 var r = confirm("Are you sure you want to post for customer [" + data.model.CustomerNumber + "] from group [" + data.model.CustomerGroupCode + "]?");

Will be ignored by the browser, and the following will be logged in the console:

Ignored call to 'confirm()'. The document is sandboxed, and the 'allow-modals' keyword is not set.

To avoid this, we must use one of the numerous dialog box options available to us in Sage’s global.js.

There are A LOT of functions, so we’ll add them here as we learn how to use them.

Here are a few to start:

sg.utls.showValidationMessage

Using this, simply shows an error validation message.

It can used like this:

sg.utls.showValidationMessage("This is an error validation message!");

And results in this:

Example of sg.utls.showValidationMessage

sg.utls.showKendoConfirmationDialog

Using this shows a yes/no prompt.

It can be used like this:

    var approverUserId = $('#txtApproverUserId').val();
    var approverPw = $('#txtApproverPw').val();
    var customerNumber = orderEntryUI.finderData.CustomerNumber;
    if(approverUserId && approverPw)
    {
        sg.utls.showKendoConfirmationDialog(function () {
                sg.utls.ajaxPost(url, { user: approverUserId, pw: approverPw, customerNumber: customerNumber }, HutilityOrderEntryCustomizationUICallback.jbtest);
            },
            null, "Approve order with user [" + approverUserId + "].", "Demo");
    }
    else {
        sg.utls.showValidationMessage("Please enter the credit approver credentials");
    }

The above example shows a confirmation dialog if two textboxes have been filled in, otherwise it shows a validation error message. The confirmation dialog takes in 3 arguments: the function to run upon a “Yes”, the function to run upon a “No”, and the prompt message.

In this example, we’re sending an AJAX POST request to a custom controller if “Yes” is clicked.

The confirmation dialog shows like this:

Example of sg.utls.showKendoConfirmationDialog

 

More to come!

Initializing Finders in Sage 300c Web Customizations

This example shows a trimmed down portion of a javascript customization file, that only includes finder initialization.

In this example, the finder is for AR Customers, where the finder will only show the Customer ID field, and return that same field as the value.

HutilityOrderEntryCustomizationUI = {
    initCustomFinders: function () {
        sg.viewFinderHelper.setViewFinder("btnFinderCustomerTest", "txtFinderCustomerTest",
            {
                viewID: "AR0024",
                viewOrder: 0,
                displayFieldNames: ["IDCUST"],
                returnFieldNames: ["IDCUST"],
                filter: null,
                initKeyValues: [],
                parentValAsInitKey: true
            });
    },
}
// Initial Entry
$(function () {
    HutilityOrderEntryCustomizationUI.init();
    HutilityOrderEntryCustomizationUI.initCustomFinders();
});

In the above code, btnFinderCustomerTest would be the id of the button that triggers the finder, and txtFinderCustomerTest would be id of the textbox where the finder result value is placed.

The button and textbox DOM elements can be defined using the Sage UI Customization wizard or defined via javascript as we’ve described here.

Sage 300c Customization Tip: Adding UI Elements Using Javascript

Here’s a way to add UI elements to your customization using javascript, bypassing the UI wizard.

One reason you might not want to use the wizard, is that it may be missing certain HTML elements. One example is adding a password input textbox. As of August 2019, the wizard doesn’t provide this option.

In the below example, we’re adding a textbox and button combo to initialize later on as a finder. We’re also adding a textbox for a user id, and a corresponding password. We’ll be adding these right before the row where the Post button is, on the OE Order Entry screen.

Sage300c customization - programatically adding UI elements

The code above can be done more elegantly, please excuse it for the sake of illustration purposes!

This is nothing specific to Sage 300c web development, just showing it as an option that can be done.

 

Debugging Sage 300c C# Controller Code

In Visual Studio, attach to the process w3wp.exe

You may need to run Visual Studio as administrator.

Once attached to the process, navigate to your customized screen on the Sage 300c web UI.

In Visual Studio, open ensure the symbols are loaded for the controller dll.

VS2017 - Debug Modules

VS2017 - Symbols Loaded

Use your custom screen, and any breakpoints you’ve set should be hit.

Troubleshooting:

Make sure the pdb and the dll are matching versions, or VS will refuse to load symbols.

VS - symbols not loaded

Packaging and Installing a Sage 300c Web Screen Customization

So you have your customization files and a custom controller project. How do you get this installed?

It’s actually fairly easy, and Sage has a nice way to install them into individual companies for the web screens.

Packaging

Find the dll compiled by your controller project. In our case, it’s named Hutility.CU.Web.dll.

Also, find the bootstrapper XML file in your project. In our case, it’s named HutilityCUBootstrapper.xml.

Package those 2 files, and along with the UI’s manifest, js and XML files into a zip.

Packaging Sage 300 customization

Installing

Navigate to your Sage 300c Admin dashboard. If you’re running locally that would be at http://localhost/Sage300/Admin¬†

From that screen, import your zip file.

Sage 300c Import Customization

Once imported, click on your customization, and assign it to be available to a company.

Sage 300c Assign customization

Log out of the admin dashboard, and navigate back to the main Sage300c application at:

http://localhost/Sage300

Getting Started with Sage300c Customization Wizards

To kick things off, we’ll quickly go over the two main wizards you’ll use to create a web screen customization. Sample projects that use these are in the SDK on Github, but we’ll illustrate them here for a quick reference. You’ll find an example of an OE Order Entry screen customization, in the SDK, at: /docs/customization/Sage300SDK_WebScreenOrderEntryCustomizationTutorial.docx

The first wizard you’ll run, is for the client side web UI. You can find this wizard in the SDK, at: /bin/wizards/Sage.CA.SBS.ERP.Sage300.CustomizationWizard.exe

Sage 300c Web Customization Wizard

The screenshot above, is the 1st screen of this wizard. Sage has a more thorough walkthrough of how to use this wizard in their Order Entry tutorial referenced above.

Through this wizard, you add screens (OE Order Entry, AR Invoice Entry, etc.) and controls (textboxes, buttons, finders, etc.) to your customization. It will then produce the javascript, XML and manifest files that the web screens require to lay out the UI elements of your customization. The javascript file is the base from which you will be adding your custom UI logic.

Some HTML elements such as password boxes (i.e. an input field of type ‘password’) are currently not available as options via the wizard. A way to get them into your customization would be through javascript. That is, you would dynamically add the HTML elements to the DOM via javascript after the page has loaded.

The code and files generated from this wizard are for the client/UI side. This is enough for doing custom logic on the presentation layer, but what if you need to fetch data from the server? This is where the 2nd wizard comes in.

Before you can use the 2nd wizard, you’ll need to install Sage300UICustomizationSolution.vsix into Visual Studio (as of August 2019, that is for VS2017). This can be found in the SDK, at bin/wizards/Sage300UICustomizationSolution.vsix

Once installed, you’ll create a project in Visual Studio of type ‘Sage 300 UI Customization Wizard’.

Sage 300 UI Customization WIzard

From here, select the manifest file produced from the 1st wizard. Finish the wizard, and it will generate a project for a custom controller. If you encounter any issues at this step, verify that you have the latest version of Visual Studio, and ensure you are using the correct Sage 300 Web SDK version.

In future posts, we’ll go over how to package and install a customization, how to use the Sage javascript libraries, intercepting user actions and doing custom logic on the server side using a custom controller.

Sage300c Web Screen Customizations

We’ll be starting a new series of posts on Sage300c web screen customizations.

The purpose is to have a resource that has code snippets, gotchas and tips to help our internal developers and possibly anyone else looking to get started. Sage has provided their Sage 300 Web SDK as open source on Github, but we’re hoping to add some more code examples from a different perspective.

It’s a big move for 3rd party developers to go from VB6 screen customizations to the new web screens, so hopefully we can help some developers save some time on the way!

Writing IEnumerable data to CSV/Excel as a table (HuLib 1.0.7 feature)

A common requirement for us is exporting data to excel or CSV files. While it is not too daunting of a task, the frequency of it prompted me to look at a more concise way of writing it.

Based on the established practices we first get all the data we need to export into some form of IEnumerable and then have a function to export it to the desired target.

My previous approach was to use one of our writer classes (ie BufferedExcelWriter or CSVWriter to write each field one by one. This is quick to write and looks like this:

  1. Write each header one by one with .Write(“header name”)
  2. Loop through rows of the IEnumerable
    1. For each of the columns, we need to write the value
    2. Write a newline

So, this is not too difficult to implement but you end up with many lines and the code for the headers is separate from that of the data in the table. This can lead to a bit more complication in the event we need to rearrange, add/remove columns since it becomes quite easy to accidentally misalign columns.

I thought it would be nice if instead, we could just have a single call that includes defining the columns and what data they should have rather than directly iterating through them. This may not be the best way to go in all cases – you are omitting looping in favour of just using a single function to get each value, but for our exports, it usually satisfies our requirements.

Example

Say we have the following model class:

class TestData
{
	public string Name { get; set; }
	public string Description { get; set; }
	public decimal Amount { get; set; }

	public TestData(string name, string description, decimal amount)
	{
		Name = name;
		Description = description;
		Amount = amount;
	}
}

We want to export all 3 properties per line.

The old approach would look like this (for Excel):

BufferedExcelWriter writer = new BufferedExcelWriter();
writer.AddSheet("Sheet1");
writer.Write("Name");
writer.Write("Description");
writer.WriteLine("Amount");
writer.WriteLine();
foreach (TestData data in testData)
{
	writer.Write(data.Name);
	writer.Write(data.Description);
	writer.Write(data.Amount);
}

writer.Save("New File.xlsx");

This works and gives us what we want but can get tedious.

The new approach makes use of some new things in HuLib, namely ExportTableBuilder:

BufferedExcelWriter writer = new BufferedExcelWriter();
writer.AddSheet("Sheet1");

writer.WriteTable(testData, new ExportTableBuilder()
	.AddColumn("Name", d => d.Name)
	.AddColumn("Description", d => d.Description)
	.AddColumn("Amount", d => d.Amount));
	
writer.Save("New File.xlsx");

Now we have all of the writing content to the file down to a single call! You can see that it is very clear which header corresponds to which value, and it is impossible to misalign the header to the value.

Seeing that this would reduce the number of calls for generating content to 1 in most cases I took it one step further and added extensions to CSV and Excel to export to a file directly:

testData.ToExcel("New File.xlsx", new ExportTableBuilder()
	.AddColumn("Name", d => d.Name)
	.AddColumn("Description", d => d.Description)
	.AddColumn("Amount", d => d.Amount));

Now you can get a list of objects into a CSV / Excel file without worrying about which class to use.

Note: there is currently no support for formatting (in Excel)

Functions of note:

// Some list of whatever model you want
List testData = new List()
{
	new TestData("Apple", "A fruit", 30),
	new TestData("Banana", "Also a fruit", 22),
	new TestData("Flamingo", "Not a fruit", 4),
};

// Table definition
ExportTableBuilder builder = new ExportTableBuilder()
	.AddColumn("Name", d => d.Name)
	.AddColumn("Description", d => d.Description)
	.AddColumn("Amount", d => d.Amount);

// Export to Excel
Excel.Export(testData, "New File.xlsx", builder);
testData.ToExcel("New File.xlsx", builder); // This and the line above are equivalent

// Export to CSV
CSV.Export(testData, "New File.csv", builder);
testData.ToCSV("New File.csv", builder); // This and the line above are equivalent

These functions are made available via an extension to the IWriter interface which both BufferedExcelWriter and CSVWriter now implement. If you want to enable the table export to another type of writer just implement IWriter and you will have access to the WriteTable functionality.

Extra – Naming based on property

Just added in the latest preview 1.0.7 build is the ability to infer the header name based on the property or field. If no header is specified and there is only an expression, the header will be set to the provided property or field’s display name attribute (if it exists) or its own name.

class TestData
{
	[DisplayName("Name2")]
	public string Name { get; set; }
	public string Description { get; set; }
	public decimal Amount { get; set; }
}

ExportTableBuilder<TestData> builder = new ExportTableBuilder<TestData>()
	.AddColumn(d => d.Name) // Header will be Name2
	.AddColumn(d => d.Description) // Header will be Description
	.AddColumn("Amount2", d => d.Amount); // Header will be Amount2

This is just a convenience addition that does not add more functionality but can be more convenient in some cases.