Marty Zigman Marty Zigman
Prolecto Labs Accelerator Templates

Learn the NetSuite Model View Controller MVC SuiteScript 2.0 Pattern

NetSuite Technical



This article is relevant if you are a NetSuite Software Developer and you would like to learn the Model View Controller (MVC) pattern in a SuiteScript 2.0 pattern.

Background

As the principal for our NetSuite Systems Integration practice, an ongoing concern is the standards for quality we hold for the services we produce. Much of the work we do in the NetSuite platform involves customization. We favor SuiteScript over workflows for many reasons — mostly because the SuiteScript platform gives us maximum capacities to solve our clients’ concerns. Thus, when we are producing customizations, we create SuiteScript to glue together logic to produce our desired outcomes. Consequently, we value good SuiteScript.

Fundamentally, SuiteScript is effectively the name of the set of software technologies published by Oracle NetSuite to refine the NetSuite ERP system. Since the advent and release of SuiteScript version 2.0, the framework now follows a more common pattern we see in complex JavaScript-driven application development. However, the SuiteScript 2.0 pattern says nothing about a common software architectural pattern called Model-View-Controller (MVC).

One role I hold for our team members is guidance and leadership as they develop in their careers. One of our consultants, who possessed skill with SuiteScript 1.0, expressed a desire to become more valuable and learn the newer SuiteScript 2.0 pattern. I suggested that we work together to build a simple SuiteScript 2.0 application that uses the MVC pattern. The idea is that if you are going to commit to learning a new language construct, you might as well enhance your thinking processes at the same time.

Thus, I would like to thank Mike I. for stepping up to take the journey so that I could author this article and we too can help you learn the MVC pattern as it relates to NetSuite.

The Model-View-Controller (MVC) with NetSuite

The Model-View-Controller pattern is a way of thinking about business-based database-oriented software development to allow us to partition user requirements so that we can build and manage complex applications. The major power of this pattern is to help us conceptualize and reuse software modules to produce greater complexity while making it easier to maintain and produce less bug-prone code. You certainly do not have to develop your NetSuite applications in this manner. But if you do develop in this manner, you bend your thinking patterns in a more generalized and abstract manner thus affording you greater capacities for holding and building complexity using layers upon layers of more simplified and reusable logic constructs.

With this concept, I will break apart the purpose of each element. I wish the pattern was called the Controller Model View pattern because I think it is easier to conceptualize.  Hence, I will describe in that way first before Mike and I provide you with some working code and a short video presentation:

  1. (C)ontroller: the Controller is the governing program that listens for all program inputs, sets up or handles any environmental conditions, and couples together Models and Views to produce an application. In NetSuite, let’s consider a SuiteLet a good example of the Controller. It is reasonable to expect that your program must start in some manner. The program that starts is likely, but not always, the Controller. A SuiteLet is an easy approach to learn the pattern because it has a start, does some things, usually draws a page, and then finishes. We will use a SuiteLet here to learn.
  2. (M)odel: the Model is effectively the data organized in a logical structure. It can be as complex as you want and it can take any shape you desire. For example, a customer model may be described with a string for company name, and decimal for credit-limit. But it may have parent-child information because you want to know all the street addresses (a child structure) and desired foreign currencies (another child structure). The beauty of JavaScript is its object orientation which allows you to invent data structures hierarchically. Going further, a Model may be responsible for all the plumbing work to retrieve data from the NetSuite database, validate updated data, and insert new or delete existing data. Business rules can be contained in the Model to ensure information is logically structured as you intend.
  3. (V)iew: The View is the element that generally is responsible for user display. In NetSuite, a rich set of libraries are offered to produce a compelling and consistent user experience. Consider that the View can consume the Model and then know how to render the user interface according to the shape of the Model. The Model may have a way to hold information provided by the user that does not conform correctly (for example, all zip codes must be 5 digits and valid), and the View can consistently message to the user how the non-conforming information is rendered back to help the user try to update again.

More generally, consider that you can break apart the construction of your application into responsibility domains to help you amplify your efforts. When you develop software using this pattern, you can quickly educate other developers about your mental model because you can use business names to represent Model elements and you can use View names representing the fashion for which you will display information (e.g, a result list of records or a single detail record output format).

Sample NetSuite MVC SuiteScript 2.0 Program

Learning can be most meaningful when it is needed for real day-to-day use. Thus, in our NetSuite Systems Integration Practice, there are a number of saved searches that I would like to publish in a controlled manner to various constituents.  I sought to have a simple framework that would allow me to describe a saved search, and then I could deploy the MVC application to intelligently filter and display the saved search based on the user that was logged in. Admittedly, a simple NetSuite development challenge.  Yet, the challenge was quite good for learning the MVC pattern and to help Mike get up to speed on SuiteScript 2.0.

Video (1:11) of the Simple MVC Application

I recommend watching the short (1:11) video to learn what the application does so that when you review the code below, it will be much easier to understand each part.

NetSuite MVC Saved Search SuiteScript 2.0 SuiteLet Application

Assuming you watched the video to get oriented, I present below the three modules so you can interpret what is happening. I have provided a screenshot of how Mike named the program files so that you can better understand the SuiteScript file layout.

The Controller Program

In our Controller, we will assume that the application is running inside NetSuite by an authenticated user. To activate, all a user has to do is run the program. This Controller program can be deployed via NetSuite built-in menu tools.

//------------------------------------------------------------------
// Description: MVC Saved Search Controller Suitelet
// Developer: Michael I. with Marty Zigman
// Date: 20190513 via Code Review
// Notes: General MVC pattern for deploying a Saved Search via Saved Search
//------------------------------------------------------------------


/**
  * @NApiVersion  2.x
  * @NScripttype  Suitelet
  * @NModuleScope Public
*/

define(['N/record', 'N/runtime', 'N/search', './pri.Model', './pri.View'],
  
  function(record, runtime, search, model, view) {
	// note the model and view parameters are derived from ./pri.Model and ./pri.View in Define
	
    // Script Context
    function onRequest(context) {
      //note, these parameters are defined on NetSuite's script metadata definition and supplied with data during the deployment.
      var param_savedSearchId = runtime.getCurrentScript().getParameter({
        name : 'custscript_pri_bpa_ss_id'
      });

      var param_formTitle = runtime.getCurrentScript().getParameter({
        name : 'custscript_pri_bpa_form_title'
      });

      var param_tabLabel = runtime.getCurrentScript().getParameter({
        name : 'custscript_pri_bpa_tab_label'
      });

      var param_sublistLabel = runtime.getCurrentScript().getParameter({
        name : 'custscript_pri_bpa_sublist_label'
      });

      var param_fltrCriteria = runtime.getCurrentScript().getParameter({
        name : 'custscript_pri_bpa_ss_filter'
      });
      
      // Get User Information as this saved search must be running in a user context
      var objUser = runtime.getCurrentUser();
      var userId = objUser.id;
      var userName = objUser.name;              
      var userEmail = objUser.email;  
      var userRole = runtime.getCurrentUser().role;     // Alternate construct
      var userRoleId = runtime.getCurrentUser().roleId;

	  //diangostics and instrumentation
      log.debug({title : 'User Runtime Values', details : '| userId: ' + userId + ' | userEmail: ' + userEmail + ' | userName: ' + userName + ' | userRole: ' + userRole + ' | userRoleId: ' + userRoleId + ' | param_savedSearchId: '  + param_savedSearchId + ' | '});
      log.debug({title : 'Parameter Filter Criteria', details : param_fltrCriteria});
      
      // Check Method of the request.
      if (context.request.method === 'GET') {
        
        // setup Model and View Objects; null is okay
        var data = {};
        var httpForm = {};

        //validate we have info to call the model
        if (!isEmpty(userId) && !isEmpty(param_savedSearchId)) {
          try {
            var data = model.updateModel(userId, param_savedSearchId, param_fltrCriteria);
          }
          catch(e) { 
            log.error('Error during Model Module Call', e.toString());
          }
        } else {
        
          //off information if we do not have data 
          if (isEmpty(userId)) {
            log.error({title : 'Controller Module', details : 'Missing required User Id.'});
          }
          
          if (isEmpty(param_savedSearchId)) {
            log.error({title : 'Controller Module', details : 'Missing required Saved Search Id.'});
          }
        }
        
      //validate we have info to call the view
        try {
          if (!isEmpty(param_formTitle)) {
            view.setFormTitle(param_formTitle);
          }

          if (!isEmpty(param_tabLabel)) {
            view.setTabLabel(param_tabLabel);
          }

          if (!isEmpty(param_sublistLabel)) {
            view.setSubListLabel(param_sublistLabel);
          }

          //call the view passing it the model (called data)
          var httpForm = view.renderView(data);
        }
        catch(e) {
          log.error('Error during View Module Call', e.toString());
        }
               
        //write the view as a page
        if (!isEmpty(httpForm)) {
          context.response.writePage(httpForm);
        } else {
          log.error({title : 'Render Form', details : 'Empty form object. Nothing to render.'});
        }
      
      } else {
        
      } // Check Context Method
    } // Function onRequest
  
    // Entry Points
    return {
      onRequest : onRequest
    
    }; // Return Entry Points
    
    //utility function
    function isEmpty(val) {
      return (val == undefined || val == null || val == '');
    }
    
  } // Function(record, runtime, search, model, view)
); // Define

The Model Program

In our Model, we decided to allow the native Saved Search API resultset to represent the Model shape. There was no need to do any extra work because the resultset is rich with metadata and that is very valuable for Controller and View work.  The Model is supplied information from the Controller so it can decide how to act on retrieving the information. Pay attention to how the logged-in user’s internalid is used to filter out and display only relevant information related to that specific user.

//------------------------------------------------------------------
// Description: MVC Saved Search Model
// Developer: Michael I, with Marty Zigman
// Date: 20190513 via Code Review
// Notes: to be used via the Controller, note no scope or other
//        information in @NApi elements as this is included
//        in pri.controller logic
//------------------------------------------------------------------

/**
  * @NApiVersion  2.x
*/

define(['N/record', 'N/search'],
  
  function(record, search) {
    
    var objSavedSearch = {};
    var objResultSet = {};
    
    function updateModel(userId, searchId, filter) {
      
      if (!isEmpty(userId) && !isEmpty(searchId)) {
        
        log.debug ({title : 'Passed Filter String', details : filter});
        
        if (isJSON(filter)) {
          log.debug({title : 'Update Model', details : 'Have valid JSON string.'});
        
          try {
            //load up the saved search
        	objSavedSearch = search.load({ id : searchId });
  
            // Convert passed String Parameter into an Array of JSON Objects
        	var jsonFilters = {};
        	if (filter){
            	var jsonFilters = JSON.parse(filter);
            } 
  
            //allow the saved search to work without additional filters
        	for (var row = 0; row < jsonFilters.length; row++) {        
              
              var filterName = jsonFilters[row].name;
              var filterJoin = jsonFilters[row].join;
              var filterOperator = jsonFilters[row].operator;
              var filterValues = jsonFilters[row].values.replace('*userId*', userId);

              log.debug({title : 'Filter Value[' + row + ']' , details : ' | name = ' + filterName + ' | join = ' + filterJoin + ' | operator = ' + filterOperator + ' | values = ' + filterValues + ' | '});

              var objFilterSearch = search.createFilter({
                name : filterName,
                join : filterJoin, 
                operator : filterOperator,
                values : filterValues
              });
              objSavedSearch.filters.push(objFilterSearch);
            
        	} 
            
            //run the search
        	objResultSet = objSavedSearch.run();
          }
          catch(e) {
            log.error('Error During Model Update', e.toString());
          }
        
        } else {
          
          log.error({title : 'Update Model', details : 'Not a valid JSON string. Please check format.'});
          log.error({title : 'Invalid JSON Format', details : filter});
        
        } 
      
      } else {
        
        if (isEmpty(userId)) {
          log.error({title : 'Model Module', details : 'Missing required User Id.'});
        }
        if (isEmpty(searchId)) {
          log.error({title : 'Model Module', details : 'Missing required Saved Search Id.'});
        }
      }
      
      //return the search result set as the model
      return objResultSet;
    
    }
    
    // Entry Points
    return {
      updateModel : updateModel
    
    };  // Return Entry Points
    
    
    // ====================
    // Supporting Functions
    // ====================
    function isJSON(str) {
      try { 
        JSON.parse(str);
      }
      catch(e) {
        return false;
      }
      return true;
    }
    
    function isEmpty(val) {
      return (val == undefined || val == null || val == '');
    }
  
  } // Function (record, search)
); // Define

The View Program

In our Controller, the major input is the View. Thus, the View expects the shape of the Model to be a Saved Search Result object and as such, understands how to interrogate the object so it can display information it otherwise knows nothing about. Under this pattern, any type of search, including summaries, can be used to display data.

//------------------------------------------------------------------
// Description: MVC Saved Search View
// Developer: Michael I. with Marty Zigman
// Date: 20190513 via Code Review
// Notes: General MVC pattern (the View) for deploying a Saved Search
//------------------------------------------------------------------

/**
  * @NApiVersion  2.x
*/

define(['N/record', 'N/search', 'N/ui/serverWidget'],
  
  function(record, search, ui) {

    //arbitrary constraints on the rows returned if not passed via the controller.
	var CONST_STARTROW = 0;
    var CONST_ENDROW = 1000;
    
    var objForm = {}
    var objTab = {};
    var objSublist = {};
    
    var formTitle = ' ';
    var tabLabel = ' ';
    var sublistLabel = ' ';
    
    function renderView(resultSet, startRow, endRow) {
      
      if (isEmpty(startRow) || isNaN(startRow)) {
        startRow = CONST_STARTROW;
      }
      if (isEmpty(endRow) || isNaN(endRow)) {
        endRow = CONST_ENDROW;
      }

      if (!isEmpty(resultSet)) {

        try {
        
          var objForm = ui.createForm({
            title : formTitle
          });
      
          var objTab = objForm.addTab({ 
            id : 'custpage_tab',
            label : tabLabel 
          });
          
		  // Column Sublist Labels
          var objSublist = objForm.addSublist({
            id : 'custpage_sublist',
            type : ui.SublistType.LIST,
            label : sublistLabel,
            tab : 'custpage_tab'
          });
      
          // Column Heading Labels 
          var c = 0;
          resultSet.columns.forEach(function(col) { 
            var colName = 'custpage_col'  + c;
            objSublist.addField({
              id : colName,
              label : col.label,
              type : ui.FieldType.TEXT
            });
            c++;
          });

          var objResultSetSublist = resultSet.getRange(startRow, endRow);
      
        }
        catch(e) {
          
          log.error('Error During View Render', e.toString());
        
        }
        
        try {
        
          // Row Data
          for (var row = 0; row < objResultSetSublist.length; row++) {        
          
            var resultRow = objResultSetSublist[row];
            var c = 0;
            
            for (var col in resultRow.columns) {
              
              var colName = 'custpage_col' + c;
              c++;
              var fieldValue;
              var fieldText;
              var colValue = ' '; // Default column value
              
              fieldValue = resultRow.getValue(resultRow.columns[col]);
              fieldText = resultRow.getText(resultRow.columns[col]);
              
              if (fieldText) {
                colValue = fieldText;
              } else if (fieldValue) {
                colValue = fieldValue;
              }
              
              objSublist.setSublistValue({ 
                id : colName, 
                value : colValue,
                line : row 
              });
              
            } // End col
          } // End row
        
        }
        catch(e) {

          log.error('Error During View Row Data', e.toString());

        }
       
      } else {
        
        log.error({title : 'Error View Module', details : 'Empty result set. No data to display.'});
        
      }

      return objForm;
    
    } // function renderView()
    
    // Entry Points
    return {
      renderView : renderView,
      setFormTitle : setFormTitle,
      setTabLabel : setTabLabel,
      setSubListLabel : setSubListLabel
    
    };  // Return Entry Points
  
  
    // ================================
    // Supporting Entry Point Functions
    // ================================
    function setFormTitle(txtTitle) {
      if (!isEmpty(txtTitle)) {
        log.debug({title : 'setFormTitle', details : 'Setting the text to ' + '"' + txtTitle + '".'});
        formTitle = txtTitle;
      }
    }
    
    function setTabLabel(txtLabel) {
      if (!isEmpty(txtLabel)) {
        log.debug({title : 'setTabLabel', details : 'Setting the text to ' + '"' + txtLabel + '".'});
        tabLabel = txtLabel;
      }
    }
    
    function setSubListLabel(txtLabel) {
      if (!isEmpty(txtLabel)) {
        log.debug({title : 'setSubListLabel', details : 'Setting the text to ' + '"' + txtLabel + '".'});
        sublistLabel = txtLabel;
      }
    }
    
    function isEmpty(val) {
      return (val == undefined || val == null || val == '');
    }
  
  } // Function (record, search, ui)
); // Define

Work with NetSuite Experts who Hold High Standards

In this article, some of the mystery of the MVC software development pattern should now be known. Once you get in the habit of developing software in this manner, you begin to exercise your capacity to conceptualize and hold more and more difficult-to-solve requirements. Practitioners that can invent, constitute, hold, and build logic systems with increasing complexity are generally considered more valuable to those individuals that can not. Our firm holds this standard — it is one criterion I use to distinguish junior versus senior technical professionals.

If you found this article valuable, sign up to receive email notifications. If you would like to consider working with our team, or desire better NetSuite application customization, let’s have a conversation.

Marty Zigman

Holding all three official certifications, Marty is regarded as the top NetSuite expert and leads a team of senior professionals at Prolecto Resources, Inc. He is a former Deloitte & Touche CPA and has held CTO roles. For over 30 years, Marty has produced leadership in ERP, CRM and eCommerce business systems. Contact Marty to set up a conversation.

More Posts - Website - Twitter - Facebook - LinkedIn - YouTube

About Marty Zigman

Marty Zigman

Holding all three official certifications, Marty is regarded as the top NetSuite expert and leads a team of senior professionals at Prolecto Resources, Inc. He is a former Deloitte & Touche CPA and has held CTO roles. For over 30 years, Marty has produced leadership in ERP, CRM and eCommerce business systems. Contact Marty to set up a conversation.

Biography • Website • X (Twitter) • Facebook • LinkedIn • YouTube

3 thoughts on “Learn the NetSuite Model View Controller MVC SuiteScript 2.0 Pattern

  1. Akber Alwani says:

    Great article I am testing this, hope get control on this.

  2. AAlwani says:

    I have these scripts but this seems not working. 1st the Controller is applied as Suitelet and the other 2 parts are applied as standalone files in Suitescript folders.

    The issue I get is that I do not see parameter screens, do I need to manually create those parameters? how it is shown in video is not applicable in Suitelet

  3. Marty Zigman says:

    Hello AAlwani,

    I see the confusion. The script parameters defined here are part of the Suitelet definition.

    I have updated the article to indicate this to help prevent confusion.

    Marty

Leave a Reply

Your email address will not be published. Required fields are marked *