Stephen Smith's Blog

Musings on Machine Learning…

Error Reporting in Sage 300 ERP

with 18 comments


Introduction

A very important aspect of all applications is how they handle errors and how they inform the end user about these errors. When everything is entered properly and people take what is called the “happy path” through the program then there is no issue. But end users will stray from the “happy path” and other circumstances can conspire to cause the need for informing the user of unusual circumstances.

In our previous blog postings on using the .Net API we have been deferring our discussion of error reporting, but now we are going to tackle it and add error reporting to the ASP.Net MVC sample program we started last week. In doing so we will introduce some new concepts including starting to use JQuery and Microsoft’s Unobtrusive Ajax.

Errors

Generally when we refer to errors in this articles we will also mean warnings and informational messages. Often you hit save on a form and if something is wrong then you get a message box telling you what was wrong (and maybe even what to do about it). However this is bit of an oversimplification. Sage 300 ERP is a three tier client server application. Many of the errors originate in the business logic or the database server. These may be running as Windows services and have no way of doing any user interaction. They can’t simply popup a message box. The error message must be transmitted to where ever the UI is running (say in a web browser somewhere on the Internet) and displayed there in a nice form that fits in with the general design of the form.

Further there may be more than one error. It is certainly annoying to have error messages popup one at a time to be answered and it is also very annoying to Save have one error message appear and correct the thing wrong, hit save again and then have a further thing wrong and so on.

To solve these problems we collect errors, warnings and messages up into a collection which is maintained during processing and forwarded up to the higher levels when processing is complete. We see this in the .Net API with the Errors collection on the Session object. As processing proceeds any business logic component can add messages to this collection. This collection can then be processed by the UI and cleared after it has reported the errors.

All the actual error messages referenced from the business logic are stored in Windows resource files and one of these is provided for each language. So the API used by the Business Logic will access the correct language resource file for the language of the Sage 300 user associated with the session object.

Inside the collection, each error has a priority such as Severe Error, Error, Security, Warning or Message. This way we can decide further how to display the error. Perhaps have the error dialog title bar red for errors, blue for warnings and green for messages.

Exceptions

Inside our .Net API we will return a return code for simple things that should be handled by the program. For instance if we go to read a record, we return false if it doesn’t exist, true if it does. But for more abnormal things we will throw an exception. You should always catch these exceptions, since even if you are debugging a program, these messages can be very helpful.

Just because an exception is thrown, doesn’t mean there is an error on the error stack. The exception might be because of some other exception, say from the .Net runtime. Because of this you will see in our exception handler, that if there is no error from Sage 300 then we display the error that is part of the exception (perhaps from .Net).

Sample Program

To add error reporting to our ASP.Net MVC sample programming we are going to start introducing how to program richer functionality in the browser. As we left off you just hit submit, the processing was done and the page was refreshed completely. This is rather old school web programming and a bit better user interaction is expected these days. Now I’ve updated the sample ARInvEntry sample (located here) to do an Ajax request rather than a page submit request. As a result the page isn’t completely redrawn and we can do a bit more processing. In this case we will use JQuery to put up a dialog box with any messages, warnings or errors that were encountered. Normally when you save an Invoice there is no success message displayed, but here we will put up a success message as well.

First off now that we are programming in JavaScript in the Browser, this is an interpreted environment which means there is no compiler to catch dumb programming mistakes and typos. One thing is to watch the syntax highlighting in the editor and use intelli-sense to avoid some typos. The other things is that we are passing data structures from C# on the server to JavaScript on the client. Which means there is no validation that C# and JavaScript have the same understanding of the object. Further JavaScript tends to silently ignore errors and keep on going, so you can be rather mystified when things don’t work. Some good tools to look for errors are Firebug and Fiddler. Trying different browsers can help also.

First I changed the page submit call to an Ajax call using Microsoft’s unobtrusive JavaScript library.

        @using (Ajax.BeginForm("CreateInvoice", "Home", null,
             new AjaxOptions { OnSuccess = "showResult" }))

Now the request will be made with Ajax and any returned data will be passed to the showResult JavaScript function. Pretty easy change, but there is one major missing piece. We must include the JavaScript library for this. The JavaScript file for any library needs to be added to the Scripts section of the project and then an entry needs to be added to BundleConfig.cs. Here we added jquery.unobtrsive-ajax.js to the jquery bundle.

     bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
         "~/Scripts/jquery-{version}.js",
         "~/Scripts/jquery-ui-{version}.js",
         "~/Scripts/jquery.unobtrusive-ajax.js"));

We also added the jquery-ui-{version}.js. The framework will figure out which version to fill in from the js files in the scripts folder. This way we can update these without having to change any C# code. If we didn’t add the unobtrusive library then nothing would happen and we wouldn’t get any errors because nothing would be hooked up to fail. When having trouble with JavaScript always check you have the correct libraries added. Similarly we have added the css and images for JQuery to the project. You could put these in script tags in your HTML, but the bundles help with eventual deployment as we’ll see in a later article. These bundles are referenced in the cshtml files to actually include them in the generated HTML and one gotcha is that sometimes the order of including them matters due to namespace conflicts, for instance the bootstrap bundle must be placed before the jQuery bundle or something in JQuery won’t work due to some naming conflicts.

Now we add the showResult function and what it calls.

function showResult(data)
{
    $.fn.showMessageDialog(data.Warnings, data.Errors, data.Messages);
}
(function ($) {
    $.fn.showMessageDialog = function (warnings, errors, messages) {
        $("#infoWarningsBlock").hide();
        $("#infoSuccess").hide();

        if ((warnings != null && warnings.length > 0) ||
            (errors != null && errors.length > 0) ||
            (messages != null && messages.length > 0)) {

            var text = "";
            var i;
            if (messages != null && messages.length > 0) {
                $("#infoSuccess").show();
                for (i = 0; i < messages.length; i++) {
                    text = text + "<li>" + messages[i] + "</li>";
                }
            }
            $("#infoSuccess").html(text);
            text = "";
            if (errors != null && errors.length > 0) {
                $("#infoWarningsBlock").show();
                for (i = 0; i < errors.length; i++) {
                    text = text + "<li>" + errors[i] + "</li>";
                }
            }
            if (warnings != null && warnings.length > 0) {
                $("#infoWarningsBlock").show();
                for (i = 0; i < warnings.length; i++) {
                    text = text + "<li>" + warnings[i] + "</li>";
                }
            }
            $("#infoWarnings").html(text);

            $("#informationDialog").dialog({
                title: "Information",
                modal: true,
                buttons: {
                    Ok: function () {
                        $(this).dialog("close");
                        $(this).dialog("destroy");
                    }
                }
            });
        }
    }

}(jQuery));

Besides setting up the HTML to display, this code mostly relies on the JQuery dialog function to display a nice message/error/warning dialog type box in the Browser using an iFrame.

This routine requires a bit of supporting HTML which we put in _Layout.cshtml file so it can be shared.

    <div id="informationDialog" style="display: none;">
        <ul id="infoSuccess" style="display: none;"></ul>
        <div id="infoWarningsBlock" style="display: none;">
            <div id="infoWarningsHeader">
                <h4>Warnings/Errors</h4>
            </div>
            <ul id="infoWarnings"></ul>
        </div>
    </div>

Notice it has display set to none so it is hidden on the main page.

From the C# code on the server here is the error handler:

        //
        // Simple generic error handler.
        //
        private void MyErrorHandler(Exception e)
        {
            if (session.Errors == null)
            {
                Errors.Add(e.Message);
                Console.WriteLine(e.Message);
            }
            else
            {
                if (session.Errors.Count == 0)
                {
                    Errors.Add(e.Message);
                    Console.WriteLine(e.Message);
                }
                else
                {
                    copyErrors();
                }
            }
        }

        private void copyErrors()
        {
            int iIndex;

            for (iIndex = 0; iIndex < session.Errors.Count; iIndex++)
            {
                switch (session.Errors[iIndex].Priority)
                {
                    case ErrorPriority.Error:
                    case ErrorPriority.SevereError:
                    case ErrorPriority.Security:
                    default:
                        Errors.Add(session.Errors[iIndex].Message);
                        break;
                    case ErrorPriority.Warning:
                        Warnings.Add(session.Errors[iIndex].Message);
                        break;
                    case ErrorPriority.Message:
                        Messages.Add(session.Errors[iIndex].Message);
                        break;

                }
                Console.WriteLine(session.Errors[iIndex].Message);
            }
            session.Errors.Clear();
        }

It separates the errors, warnings and messages. Also the copyErrors method was separated out so it can be called in the case of success, in case there are any warnings or other messages that should be communicated.

The controller now just passes the variables from the model back to the web browser.

        public JsonResult CreateInvoice(Models.CreateInvoice crInvObj)
        {
            ResultInfo results = new ResultInfo();

            results.Messages = crInvObj.Messages;
            results.Errors = crInvObj.Errors;
            results.Warnings = crInvObj.Warnings;

            crInvObj.DoCreateInvoice();

            return Json(results);      
        }

All the work here is done by the Json method which translates all the C# objects into Json objects. It even translates the C# list collections into JavaScript arrays of strings. So for the most part this handles the communication back to the Browser and the JavaScript world.

error2

Summary

We discussed the Sage 300 ERP error reporting architecture and saw how to integrate that into our ASP.Net MVC sample program. Since this was our first use of Ajax and JQuery, we had a bit of extra work to do to set all that up to operate. Still we didn’t have to write much JavaScript and the framework handled all the ugly details of Ajax communications and translating data between what the server understands and what the browser understands.

Update 2015/01/29: Sometimes View calls will return a non-zero return code for special informational purposes. These will cause the API to throw an exception. To handle these you need to catch the exception, check the View’s LastReturnCode property to see what it is and then continue processing. Perhaps a bit of a pain to do this, but it is the exception rather than the rule.

18 Responses

Subscribe to comments with RSS.

  1. […] Introduction A very important aspect of all applications is how they handle errors and how they inform the end user about these errors. When everything is entered properly and people take what is c…  […]

  2. Really well done Stephen, this series is fantastic.

    Aslan Kanzas

    December 12, 2013 at 9:40 pm

  3. […] Add error handling as described in the last project. […]

  4. […] fields make sure you have an error handler to show the errors after an exception as explained here. If you want to get at these values they are in CSOPTFD CS0012 (a detail of CSOPTFH CS0011). You […]

  5. hi Stephen

    please if i create simple website it will work ok this is my question

    shadi

    March 23, 2014 at 9:58 am

    • You should be able to do this. Plus look at the sample programs that are linked from the .Net programming and MVC articles.

      smist08

      March 23, 2014 at 4:12 pm

  6. […] .Net Using Browse Filters in the Sage 300 ERP .Net API Using the Sage 300 .Net API from ASP.Net MVC Error Reporting in Sage 300 ERP Sage 300 ERP Metadata Sage 300 ERP Optional […]

  7. Hi Stephen-
    I am working with IC Adjustments (IC0120 and IC0110), and I think I may be encountering an example of the “informational message” type of exception. Based on manually entering the data, I BELIEVE this is happening because of a warning message that appears in the API – when the adjustment would put the inventory level negative. I can successfully catch the exception, and ignore it , but I do NOT get any messages in the Errors collection (as you suggest), but there is also no other indication of what is wrong. (LastErrorCode is 0)

    Am wondering 1) is there any other mechanism to determine the specifics of the issue?( It’s a COMException)
    and 2) is there any way for me to programmatically “reply” to the warning?

    (Note: I believe that if I know what the issue / warning is, I don’t need to programmatically reply, but rather just continue processing according to the action I want to take.)

    Thanks in advance for any info!

    Blair Hadfield

    May 15, 2015 at 6:18 pm

    • I guess you could run RVSpy to see if you can see any unusual View activity. Usually the view would return a special error code like 6000 to indicate to a UI to prompt for something.

      smist08

      May 16, 2015 at 4:17 pm

      • Thanks very much – I’ll give that a try and see if I can see any indicators…

        Kind regards,
        Blair

        Blair Hadfield

        May 16, 2015 at 8:42 pm

  8. Hi Stephen –

    We have been using the .net API (Advantage) with various systems, the issue comes in now that we need to continue from the detailed entered when a warning appears on the front en as one would have clicked ‘OK’. Any advice?

    GF VAN DEN HEEVER

    June 15, 2016 at 9:34 am

    • Usually warnings won’t throw and exception, the operation like update or insert will have succeeded, just with a warning message left on the error stack. There are cases when the views have a process command that is called to determine if a yes or no question should be asked of the user, but again this is optional.

      smist08

      June 15, 2016 at 4:18 pm

      • To add a little to this… If you are using a Try{}Catch{} block, you can receive a COM exception before you receive any meaningful messages. This stumped me for a time until your information lead me to the error stack. The error stack is part of the COM model so the exceptions don’t make it to the standard .Net exception handling.

        FYI – There is very little on using the .Net wrapper for the Sage API. Your blog is one of the few things that helped me. I’m a proficient C# developer and I’m familiar with the Sage 300 data structures. Your posts fill in the gap and really helped. Thank you. Your explanations were helpful in many ways but mostly for explaining some big picture concepts I did not understand.

        Richard Notman

        July 17, 2016 at 3:42 am

      • The reason it uses the error stack is that there can be multiple errors, so it needs to be a collection. Glad my blog was useful to you.

        smist08

        July 17, 2016 at 10:03 pm

  9. Hi there,

    I’m getting a COM exception trying to insert in PO0700; the exception contains no meaningful message (“Error HRESULT E_FAIL has been returned from a call to a COM component.”, error value -2147467259), session errors collection is empty, and LastReturnCode is 0. I’m stumped, almost tempted to ditch the COM interop API and rewrite everything in VBA. Any ideas?

    Rubberduck VBA

    May 30, 2017 at 8:18 pm

    • It sounds like PO crashed. Perhaps run RVSpy to get an idea as to where (which sometimes gives a hint as to why).

      smist08

      May 30, 2017 at 8:22 pm

      • Aaah, that sparked some hope, thanks! …but no luck! It would be nice to be able to copy from the tool’s listbox – anyway at a glance the last two lines in RVSpy don’t *seem* to contain anything new either:

        [2fbc.7ff.3724] PO0700: PORCPH [16:29:43.09].Insert(view=0x0C38004C)
        [2fbc.7ff.3724] 1 <==[16:29:43.09;t=0;ovh=0] {**ERROR**}

        I can see the .Put calls before that, that correspond to what my own application logged (removed actual values):

        Calling View("PO0700").Browse("RCPNUMBER = 012345R1", true)
        Calling View("PO0700").GoTop()
        Calling View("PO0700").RecordCreate(DelayKey)
        Calling View("PO0700").Fields.FieldByName("VDCODE").SetValue
        Calling View("PO0700").Fields.FieldByName("DATE").SetValue
        Calling View("PO0700").Fields.FieldByName("PONUMBER").SetValue
        Calling View("PO0700").Fields.FieldByName("RCPNUMBER").SetValue
        Calling View("PO0700").Insert()
        A first chance exception of type 'System.Runtime.InteropServices.COMException' occurred in ACCPAC.Advantage.Server.dll

        ….and just as I noticed the "1 <==" being the return/error code, I went to my still-running debugger to re-verify the view's LastReturnCode, and noticed RVSpy posting a bunch of new entries… that would explain the LastReturnCode being 0 every time I check it in a running debugger… but not what's going on.

        Am I reading the RVSpy output correctly?

        Rubberduck VBA

        May 30, 2017 at 8:46 pm

      • The 1 returned from the insert is the problem. Its very strange that there isn’t an error on the error stack as a result.

        smist08

        May 30, 2017 at 10:21 pm


Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.