Select Page

Dynamic Surveys in Microsoft Dynamics, Part III: The ViewModel

by | Last updated on Aug 26, 2024

This is part III in a series of posts about using the InRule BRMS along with Microsoft Dynamics CRM to create a dynamic, guided survey experience. This post assumes a working familiarity with JavaScript, the KnockoutJS MVVM framework, and a little bit of Dynamics CRM. Just tuning in? Check out the other posts in this series for more background:

Part I: The business use case for dynamic surveys

Part II: Managing the client-side dependencies for the survey

Part III: Structuring the view model (this post)

There’s so much material to cover that I can’t possibly get into the amount of detail that I’d like to without writing a hefty book’s worth of content. As a result, I’ve tried to focus on the aspects that I’ve found the most challenging or obscure as I went through the development process. Your feedback is appreciated!

Defining a ViewModel (for context)

A ViewModel is typically declared as a JavaScript Constructor function. In my script resource, I begin by declaring and creating a function. The name of the function is upper-case because in JavaScript the convention is for constructor functions to begin with an upper-case letter; all other functions should be lower-cased. The self alias variable is used to provide a consistent reference to the ViewModel, regardless of what the current definition of this is.

 IrSurvey.IrSurveyViewModel = function IrSurveyViewModel(opts) {            
var self = this;
            //…
};
 

Notice how properties are declared not just for the data elements that we want rendered, but also for metadata that describes various aspects of the UI itself. For example, the value of isLoading is used to determine whether or not an indeterminate progress spinner should be displayed to indicate a long-running operation, while the questionIndex property stores the position of the currently active question within the greater list of questions.

 self.isLoading = ko.observable(false);
           self.surveyId = ko.observable();
           self.questionIndex = ko.observable(0);
           self.crmEntity = ko.observable();
           self.questionList = ko.observableArray();
self.lastRuleResponse = ko.observable();
//…etc… 

There’s a lot of interesting things going on in the ViewModel, but in the interest of brevity I’d like to highlight the lastRuleResponse property. This observable field holds whatever response was last received from the rule engine (as expressed via a custom action and plugin – all part of the InRule ROAD team’s CRM Integration Framework.This allows me to set up a subscription to that property that notifies me when rules have been applied and new information may be available from the server.

 self.lastRuleResponse.subscribe(function (rr) {
                var notify = getNotificationsFromRules(rr);
                _.forEach(notify, function (n) {
                    self.notifications.push(n.message);
                });
                
                var warnings = getWarningsFromRuleResponse(rr);                
                warnings.forEach(function(w) {
                    Mscrm.Utilities.setReturnValue(w);
                    Xrm.Utility.alertDialog(w.Message);
                });
                var validations = getValidationsFromRuleResponse(rr);
                validations.forEach(function(v) {
                    Mscrm.Utilities.setReturnValue(v);
                    Xrm.Utility.alertDialog(v.Message);
                });
                setSurveyIdFromRules(rr);
                loadQuestionsFromCrm();
            }); 

The code above creates a subscription that extracts any notifications, validations, or warnings from the response. These are passed to other components for specific handling. The penultimate line extracts and sets the survey’s ID (because rules may need to be ran initially to assign/create the survey instance), while the last line kicks off a request to retrieve a list of questions from the remote server.

The getNotificationsFromRules and related functions don’t have their code listed here because they are simple mapping functions that exist to safely extract and shape data – nothing very interesting going on in those!

Something that *is* interesting is this line: Mscrm.Utilities.setReturnValue(v); this call passes data back to the survey launcher which allows the hosting form to react to changes in the survey – see The Launcher section from the previous post in this series for more on how that works.

Loading Questions

The survey code uses the Dynamics JavaScript SDK+SOAP endpoint to query the server for question data, but it could just as easily use the OData or the new WebAPI endpoints that are available in newer versions of Dynamics CRM. All the code needs to do is issue a query that returns any question associated with a given survey instance. Not having to write a whole bunch of data access code is part of what gives the dynamic survey such flexibility. The loadQuestionsFromCrm method looks a bit funky because it defines success and error callbacks at the top of the method rather than inline or at the bottom of the function, so it may be easier to see what’s going on by starting with the actual AJAX request to the server.

The loadSurveyQuestionsUsingLinks method is what I call a “garbage pail method” because it hides a lot of smelly, ugliness under a nice clean lid. The smelliness has to do with the fact that the shape of the data coming from Dynamics doesn’t match the shape needed by the ViewModel. Rather than forcing the ViewModel to understand how Dynamics shapes its data, this function serves the purpose of insulating the rest of the components from having that knowledge. Using a single function as a gatekeeper/translator simplifies the rest of my code immensely. To help accomplish this, I’m using Underscore (http://underscorejs.org), a JavaScript utility framework well worth checking out.

 function loadSurveyQuestionsUsingLinks(surveyResultId, successCallback, errorCallback) {
            var questionQry,
                queryResults;
            questionQry = new Sdk.Query.QueryByAttribute(IrSurvey.CrmSurveyQuestionEntityTypeName);
            crmQuestionAttributeList.forEach(function (att) {
                questionQry.addColumn(att);
            });
            questionQry.addAttributeValue(new Sdk.Guid(IrSurvey.SurveyPkAttribute, surveyResultId));    
            
            Sdk.Async.retrieveMultiple(questionQry,
                function (res) {
                    queryResults = res.getEntities().toArray().map(function (e) {
                        var vw = e.view().attributes;
                        var keys = _.keys(vw);
                        var fixedKeys = [];
                        var mapped = _.map(keys, function (k) {
                            var val = vw[k];
                            var objKeys = k.split('.');
                            if (objKeys[1]) {
                                fixedKeys.push(objKeys[1]);
                                var ret = {};
                                ret[objKeys[1]] = val;
                                return val;
                            }
                            else {
                                fixedKeys.push(k);
                                return val;
                            }
                        });
                        return _.object(fixedKeys, mapped);
                    });
                    successCallback(queryResults);
                },
                errorCallback);
        }
 

After the “garbage pail” of the loadSurveyQuestionsUsingLinks function, the rest of the question loading process should be a breeze! The success callback is fired and the list of questions is iterated through and used to instantiate new IrSurveyQuestionViewModel instances as part of the map expression. The resulting array is then sorted to arrange questions oldest to newest, existing questions are cleared, and new ones pushed onto the questionList. The last thing that the success handler does is tell the ViewModel to set the current question to be the first question which does not have a value for its answer property. From there, Knockout manages the rest of the process of updating the proper DOM elements and notifying other subscribers of the new data.

 function loadQuestionsFromCrm(surveyId) {
               //…snip… guard logic and housekeeping
 
                function success(entities) {
                    self.isLoading(false);
                    self.notifications.push("loaded questions. processing...");
                    var questions = _.map(entities,
                        function (q) { return new IrSurvey.IrSurveyQuestionViewModel(q, self); })
                        .sort(function (left, right) { return left.createdOn() - right.createdOn(); });
                    self.questionList.removeAll();
                    questions.forEach(function (q) { self.questionList.push(q); });
                    navigateToFirstUnansweredQuestion();
 
                };
                function error(error) {
                    self.isLoading(false);
                    self.notifications.push(error);
                };
 
                loadSurveyQuestionsUsingLinks(surveyId, success, error);
}

Conclusion

I hope that this post has provided you with a basic idea of the challenges and concerns surrounding the implementation and design of the Survey’s object model. I’ve tried to focus on the things that I think can trip up the unsuspecting developer, but doubtless I’ve left some gaps. Is there too much detail? Not enough detail? Let me know in the comments. I’m currently indecisive between wanting to describe how the HTML of the UI gets rendered and going over how the Rule Application is constructed, so if there’s something you’d like to see definitely chime in!

Editor’s Note: Check out the final post in this blog series: Dynamic Surveys in Dynamics CRM: The Rules.

BLOG POSTS BY TOPIC:

FEATURED ARTICLES:

STAY UPDATED ON THE LATEST FROM INRULE

We'd love to send you monthly updates! Learn about our webinars and newly published content by subscribing to our emails. We'll never share your email address and you can easily unsubscribe at any time.