Stephen Smith's Blog

Musings on Machine Learning…

Sage 300 Web UI SDK – Adding UI Controls

with 11 comments


In my last posting I showed how to quickly create an empty Sage 300 Web UI by running our two new wizards from Visual Studio. In this article we’ll look at how to add some visual controls to this project and talk a bit about some of the issues with doing this, namely about using our provided HTML helper functions and CSS styling.

We’re basically going to continue on and add the visual elements for the PJC Cost Types setup screen. We won’t write any JavaScript yet, so the only functionality will be that provided by the code generator and the default data binding support. This still give quite a bit as you can navigate, use the finder, delete records and save updates.

The UI Wizard discussed last week produces a simple starting page with the standard heading controls, the key field and the Save and Delete buttons. These are all wired up to Javascript and working. This makes our life much easier when adding the rest of the controls.

The only thing you need to do manually is change the Starting Page to: “/OnPremise/PM/CostType” on the Web tab of the Web project’s properties. Then it will compile and run yielding:


Adding the Parts

ASP.Net MVC Razor Views are a technique to dynamically generate our HTML by embedding C# code in an HTML template. When the HTML needs to go to the browser the C# code is executed and it usually generates more HTML into the template, so that pure dynamically generated HTML is transmitted to the Browser. The Razor View system is very extensible and it allows a lot of extensibility which we do by adding a large set of helper functions.

Below is the screen once we add some more controls. I showed with a record loaded since that part works with the generated code. The dates and bottom combo box aren’t working yet since we need to add some JavaScript code to help them out.


The source code for this screens Razor View (the partial view part) is:

(That didn’t work so well. Apparently WordPress ate all the div’s, I’ll do a bit of research to see if I can fix this, so a bit is missing from the below code. I also added some line breaks so the code doesn’t go off the right of the page).

@* Copyright © 2015 Sage *@
@model Sage.Web.Areas.PM.Models.CostTypeViewModel<Sage.PM.Models.CostType>
@using Sage.PM.Resources.Forms

@using Sage.CA.SBS.ERP.Sage300.Common.Web.AreaConstants
@using Sage.CA.SBS.ERP.Sage300.Common.Resources
@using Sage.CA.SBS.ERP.Sage300.Common.Web.HtmlHelperExtension
@using Sage.CA.SBS.ERP.Sage300.Common.Models.Enums
@using AnnotationsResx = Sage.CA.SBS.ERP.Sage300.Common.Resources.AnnotationsResx

@Html.ConvertToJsVariableUsingNewtonSoft("CostTypeViewModel", Model)

<section class="header-group">
    @Html.SageHeader3Label("CostTypeHeader", CostTypeResx.Entity)
    @if (Model.UserAccess.SecurityType.HasFlag(SecurityType.Modify))
        @Html.KoSageButton("btnNew", null, new { @value = CommonResx.CreateNew, @id = "btnNew",
             @class = "btn-primary" })
    @Html.Partial(Core.Menu, Model.UserAccess)

<section class="required-group">
    @Html.SageLabel(CommonResx.RequiredLegend, new { @class = "required" })

  @Html.SageLabel("CostTypeCode", CostTypeResx.CostTypeCode, new { @class = "required" })
  @Html.KoSageTextBoxFor(model => model.Data.CostTypeCode, new { @sagevalue = "Data.CostTypeCode",
      @valueUpdate = "'input'" }, new { @id = "txtCostTypeCode", @class = "default txt-upper",
      @formatTextbox = "alphaNumeric" })
  @Html.KoSageButton("btnLoadCostTypeCode", null, new { @id = "btnLoad", @class = "icon btn-go",
      @tabindex = "-1" })
  @Html.KoSageButton("btnFinderCostTypeCode", null, new { @class = "icon btn-search",
      @id = "btnFinderCostTypeCode", @tabindex = "-1" })
  @Html.ValidationMessageFor(model => model.Data.CostTypeCode)

@* End of generated header, next is code I wrote. *@

    @Html.SageLabelFor(model => model.Data.Description)
    @Html.KoSageTextBoxFor(model => model.Data.Description, new { @value = "Data.Description",
        @valueUpdate = "'input'" }, new { @id = "tbDescription", @class = "large" })
    @Html.ValidationMessageFor(model => model.Data.Description, null)

   @Html.SageLabelFor(model => model.Data.LastMaintained)
   @Html.KoSageTextBoxFor(model => model.Data.LastMaintained, new {
       @value = "Data.ComputedLastMaintainedDate" }, new { @disabled = "true", @class = "default" })

    @Html.KoSageCheckBox("chkStatus", false, new { @sagechecked = "Data.Status" },
         new { @id = "chkStatus" })
    @Html.SageLabel(CommonResx.InactiveAsOfDate, null, new { @for = "chkStatus", @class = "" })
    @Html.KoSageTextBox("txInactiveDate", new { @value = "Data.ComputedInactiveDate" },
         new { @disabled = true, @class = "default " })

    @Html.SageLabelFor(m => m.Data.CostClass, new { @id = "lblCostClass", @class = "" })
    @Html.KoSageDropDownList("Data_CostClass", new { @options = "CostClass", @sagevalue =
          "Data.CostClass", @optionsText = "'Text'", @optionsValue = "'Value'" },
          new { @class = "w188" })

@* End of my code, next is the generated footer. *@

<section class="footer-group">
   @if (Model.UserAccess.SecurityType.HasFlag(SecurityType.Modify))
      @Html.KoSageButton("btnSave", new { }, new { @value = CommonResx.Save, @id = "btnSave",
            @class = "btn-primary" })
      @Html.KoSageButton("btnDelete", new { }, new { @value = CommonResx.Delete, @id = "btnDelete",
            @class = "btn-primary" })


I put comments around the code I wrote so you can see what is generated by the code generation wizard versus the code you add later. Basically this is a mixture of C# code (each line starts with @) and HTML which is in the angle brackets.

There isn’t much layout in this file because this is handled by the CSS. For simple screens like this one there are sufficient styles in the provided Sage standard CSS file that we don’t need to add any CSS. As a result, the HTML is actually fairly simple and really just used to logically group things.

Notice that we use Sage provided extension functions to create all the controls. This provides us with the hooks to provide quite a bit of standard functionality. For instance, we don’t want any hard coded strings in our HTML, otherwise we would force our translators to produce a different copy of the HTML for each language and then we would have to maintain all these files. Here we just use the helper function and it will look up the correct string from the language resource appropriate for the user’s language setting. This also gives us the ability to change the underlying control without changing all the HTMLs. So we can use a different date picker control for instance by changing the code our helper function emits rather than editing each HTML individually. Basically giving us a lot of global control over the behavior of the product.

These helper functions also can setup databinding. Any helper that start with ko will bind the data to the model (more precisely the viewmodel). We used ko since we use knockout.js for databinding which perhaps isn’t the best choice of function naming since again we can change the mechanism in the background without effecting the application code.

Notice there is a partial view called _Localization.cshtml that is included. This provides any localized strings that are needed by JavaScript. So anything referenced in here will be generated in the correct language when the page is loaded.

There is a strange call to “ConvertToJsVariableUsingNewtonSoft” near the top of the file. This is to load a copy of the model into JavaScript during page loading. This means we don’t need to do an initialization RPC call to get the model (Sage 300 View) meta data. Basically the usual empty screen then has the default data and meta data as a starting point.


This was a quick look at the Razor View part of our Web UIs. This is where the controls and layout are specified. Layout is handled by CSS and data binding is provided to greatly reduce required coding. Next we’ll start to look at the JavaScript that runs behind the scenes in the Browser.


Written by smist08

December 5, 2015 at 12:11 am

11 Responses

Subscribe to comments with RSS.

  1. […] Introduction In my last posting I showed how to quickly create an empty Sage 300 Web UI by running our two new wizards from Visual Studio. In this article we’ll look at how to add some visual contr…  […]

  2. Hi Stephen, thanks for the SDK beta release. Regarding UI development, Kendo UI is used for the app grid layout, does it a requirement for developing Sage 300c or I can use other alternatives? such as jQuery UI or jQuery with Twitter Bootstrap, etc.

    • Its what our framework supports. You can use any control you wish, after all its just programming, but you will have to develop the framework components to support editing and virtual scrolling, workout the CSS and we won’t be able to support you since we won’t know about it.


      December 5, 2015 at 12:46 am

      • Thank you.
        (It was more about licensing concern rather than technical aspect)


        December 7, 2015 at 6:59 pm

  3. Hi Stephen,

    I have tried to create some custom UI for IC/Item, OE/Credit/Debit Notes, etc., following your instruction yesterday; however, I encountered one small issue that the generated Item controller (as well as your PM sample) inherently calls the base class controller (MultitenantControllerBase) with conflict parameters (which no longer has the overload constructor that accepts string type screenName as the second parameter)

    (I know that It can be fixed using one of the provided ScreenName enumerators; however, the PMCostType or other screen names are not available)

    Also, regarding custom mod for pre-created screen, do we have the ability to customize those UI (add components, change label name, restraint the default searcher datasource, etc.)?

    Thank you!

    • I think that means you didn’t install the newer Sage 300 that we provided with the beta SDK. You need this newer Sage 300 to solve these sort of problems.

      For customization that will be one of the big features of the next release which is slated for August, 2016.


      December 9, 2015 at 11:33 pm

  4. […] Last week we talked about adding UI controls to our Web UI project. In the early days of the Web, this would be all you needed to do. The user would just enter the data and hit save (or submit). However modern Web applications are expected to be more responsive and interactive. This is achieved by adding JavaScript code which can perform local processing or make calls to the server for additional data or to perform additional actions. Most modern Web applications contain quite a bit of JavaScript and achieve quite a high degree of user interaction. […]

  5. Hi Stephen,

    I found the the “Inactive(as of)” functionality does not work in ‘PM Cost Type”, and I didn’t find the JavaScript Code related, Could you please provide some sample code Code for that?


    January 4, 2016 at 11:03 am

  6. Hi Stephen,

    I’ve created a custom optional field editor (ViewID: IC0313)
    Finder is working and information regarding that optional fields are populated correctly.
    However, when I attempt to save the changing record, I got “Method’s type signature is not Interop compatible.” exception.

    The viewmodel seems to have valid data values.
    Any ideas?
    Thank you.

    Thanh @ North49

    January 6, 2016 at 6:34 pm

    • In the wizard generated the Mapper:
      public override void Map(T model, IBusinessEntity entity)

      entity.SetValue(ItemOptionalField.Index.TextValue, model.TextValue);
      entity.SetValue(ItemOptionalField.Index.AmountValue, model.AmountValue);
      entity.SetValue(ItemOptionalField.Index.NumberValue, model.NumberValue);
      entity.SetValue(ItemOptionalField.Index.IntegerValue, model.IntegerValue);
      entity.SetValue(ItemOptionalField.Index.YesNoValue, model.YesNoValue);
      entity.SetValue(ItemOptionalField.Index.DateValue, model.DateValue);
      entity.SetValue(ItemOptionalField.Index.TimeValue, model.TimeValue);


      For optional filed value setting, it should be based on value type to set, the above line should be changed as:

      switch (model.Type)
      case Models.Enums.Type.Text:
      entity.SetValue(ItemOptionalField.Index.TextValue, model.TextValue);
      case Models.Enums.Type.Integer:
      entity.SetValue(ItemOptionalField.Index.IntegerValue, model.IntegerValue);
      case Models.Enums.Type.Amount:
      entity.SetValue(ItemOptionalField.Index.AmountValue, model.AmountValue);
      case Models.Enums.Type.Number:
      entity.SetValue(ItemOptionalField.Index.NumberValue, model.NumberValue);
      case Models.Enums.Type.Date:
      entity.SetValue(ItemOptionalField.Index.DateValue, DateUtil.GetDate(model.DateValue, null));
      case Models.Enums.Type.Time:
      var timeValue = model.Value == string.Empty ? null : model.Value;
      entity.SetValue(ItemOptionalField.Index.TimeValue, timeValue);
      case Models.Enums.Type.YesNo:
      entity.SetValue(ItemOptionalField.Index.YesNoValue, model.YesNoValue);

      Also to working on this screen, we need several manual changes:

      1) This model Type field conflict with System.Type
      2) In the mapper, use EnumUtility.GetEnum to get some enum field, the enum value use “No” or “Yes”, but retrieved entity field value may be “False” or “True”, this need code changes.

      public enum AllowBlank

      /// Gets or sets No

      [EnumValue(“No”, typeof(ItemOptionalFieldResx))]
      No = 1,


      /// Gets or sets Yes

      [EnumValue(“Yes”, typeof(ItemOptionalFieldResx))]
      Yes = 0,


      Such as change:
      //model.AllowBlank = EnumUtility.GetEnum(entity.GetValue(ItemOptionalField.Index.AllowBlank));
      //model.Validate = EnumUtility.GetEnum(entity.GetValue(ItemOptionalField.Index.Validate));
      //model.ValueSet = EnumUtility.GetEnum(entity.GetValue(ItemOptionalField.Index.ValueSet));
      model.AllowBlank = (AllowBlank)(entity.GetValue(ItemOptionalField.Index.AllowBlank));
      model.Validate = (Validate)(entity.GetValue(ItemOptionalField.Index.Validate));
      model.ValueSet = (ValueSet)(entity.GetValue(ItemOptionalField.Index.ValueSet));

      3) This screen need two keys, currently we only support one key generated code. For this screen to work, we need do some manual changes, such as:

      protected override Expression<Func> GetUpdateExpression(T model)
      return entity => (entity.ItemNumber.StartsWith(model.ItemNumber));

      public override void MapKey(T model, IBusinessEntity entity)
      entity.SetValue(ItemOptionalField.Index.ItemNumber, model.ItemNumber);

      protected override Expression<Func> GetUpdateExpression(T model)
      return entity => (entity.ItemNumber.StartsWith(model.ItemNumber)) && (entity.OptionalField.StartsWith(model.OptionalField));

      public override void MapKey(T model, IBusinessEntity entity)
      entity.SetValue(ItemOptionalField.Index.ItemNumber, model.ItemNumber);
      entity.SetValue(ItemOptionalField.Index.OptionalField, model.OptionalField);

      Also need to change TimeValue field type from TimeSpan to string in Model and Mapper to make it work.


      February 11, 2016 at 6:36 pm

  7. Hi Stephen,

    I am getting the following error on the lines:
    private void CreateBusinessEntitiesInternal()
    _businessEntity = OpenEntity(CostType.EntityName);

    which is found in the Repository.cs (I have tried it with a few different Views and always hit a roadblock here).

    I am unsure what could be causing this but was hoping you have seen this before, the error says:
    An exception of type ‘System.NullReferenceException’ occurred in Sage.CA.SBS.ERP.Sage300.Common.Cryptography.dll but was not handled in user code


    September 12, 2016 at 11:22 pm

Leave a Reply to Linda Cancel reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

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

%d bloggers like this: