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:
- (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.
- (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.
- (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.
Great article I am testing this, hope get control on this.
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
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