InforCRM client-side validation
Posted By: nicocrm on August 24th, 2015 in General
No Gravatar

In most case I try to do the validation for InforCRM form input on the server side. This is generally easier to implement (sometimes a lot easier), more reliable, and the performance is usually close (sometimes even better, if there are a lot of queries involved, as each one requires an sdata round trip when performed from the client side!) Reporting an error to the user and interrupting processing is very easy, all that is required is throwing an instance of a Sage.Platform.Application.ValidationException object:

throw new ValidationException("No way!");

However sometimes the server-side validation is not enough. The best example is when you need to ask confirmation from the user. This always requires some javascript to do the actual prompt – you can still drive the process from the server side by using hidden buttons triggered from the script but it can get cumbersome very quickly. Client-side validation can also result in a more responsive interface, especially when it can be performed synchronously (without an sdata query).

Here is a simple example that gives the user a confirmation prompt if they have not selected enough products on a new opportunity:


    String script = String.Format(@"
require(['dojo/ready', 'dojo/on', 'dojo/dom', 'dijit/registry'], function(ready, on, dom, registry) {{
    function onSaveClick(e) {{
        var grid = registry.byId('OpportunityProductsgrdOppProducts');
        var prodCount = grid.store.dataCacheToArray().length;
        if(prodCount <= 1){{
            if(!confirm('Are you sure you wish to submit this opportunity with less than 2 products?')) {{
                e.preventDefault();
            }}
        }}
    }}

    ready(function() {{
        on(dom.byId('{0}'), 'click', onSaveClick);
        on(dom.byId('{1}'), 'click', onSaveClick);
    }});
}});
", cmdSave.ClientID, cmdSaveNew.ClientID);
ScriptManager.RegisterStartupScript(Page, GetType(), "ProductValidation", script, true);

If you need an asynchronous validation this is a little bit more complex because you need to interrupt the normal flow of the button then resume it if the validation succeeds. You can use __doPostBack to that effect. Here is the same validation, using an asynchronous dialog. Keep in mind raiseQueryDialog will return immediately and note the e.preventDefault() that is invoked before the dialog actually returns a result.

    String script = String.Format(@"
require(['dojo/ready', 'dojo/on', 'dojo/dom', 'dijit/registry'], function(ready, on, dom, registry) {{
    function onSaveClick(e) {{
        var grid = registry.byId('OpportunityProductsgrdOppProducts');
        var prodCount = grid.store.dataCacheToArray().length;
        if(prodCount <= 1){{
            Sage.UI.Dialogs.raiseQueryDialog('Not enough products', 
                'Are you sure you wish to submit this opportunity with less than 2 products?', 
                function(result) {{                    
                    if(result) {{
                        __doPostBack(e.target.name, '');
                    }}
                }}, 'OK', 'Cancel');
            e.preventDefault();            
        }}
    }}

    ready(function() {{
        on(dom.byId('{0}'), 'click', onSaveClick);
        on(dom.byId('{1}'), 'click', onSaveClick);
    }});
}});
", cmdSave.ClientID, cmdSaveNew.ClientID);
ScriptManager.RegisterStartupScript(Page, GetType(), "ProductValidation", script, true);

Accessing InforCRM control values from Javascript
Posted By: nicocrm on July 1st, 2015 in General, Saleslogix
No Gravatar

Often when one needs to access the value set in an HTML control from the client side script it is tempting to reach into the DOM object and retrieve it directly, using something like this for example:

var c = document.getElementById('TabControl_element_AccountOpportunities_element_view_AccountOpportunities_AccountOpportunities_InactiveTotal_InputCurrency_CurrencyTextBox');
// this will retrieve the value as a string - still needs to be parsed as a number!
var value = c.value;

Then to set the value something like:

// need to implement "formatValue" somehow!
c.value = formatValue(value);

The hard-coded id is not great of course but the main problem with the approach in my opinion is the fact that you’ll bypass the localization logic and have to parse / format the numbers yourself. Another potential issue is that because the logic of the “dijit” (the client-side widget used to render and control the currency textbox) is bypassed it will cause its internal state to be inconsistent. Often it is more reliable to access the dijit instead, using code such as:

var c = dijit.byId('TabControl_element_AccountOpportunities_element_view_AccountOpportunities_AccountOpportunities_InactiveTotal_InputCurrency_CurrencyTextBox');
// this will retrieve the "parsed" value as a number
var value = c.get('value');

And to set it:

c.set('value', 3000);

If you have some good examples of accessing the controls from client side script please share them in the comments!


Conditional styling of InforCRM grid row with onStyleRow
Posted By: nicocrm on June 12th, 2015 in Saleslogix
No Gravatar

This is a very common request – the customer wants to see some rows highlighted in the data grid based on the data.  For example, they might want to see opportunities with a sales potential above $1M highlighted in green.  How might we achieve that in InforCRM?

There is no mechanism “out of the box” for it, but it is definitely possible to leverage the underlying capabilities of the dojo grid (see grid row styling) to achieve the look. The challenge becomes then converting the dojo documentation into something that we can use in Saleslogix.

First step: convert the code from declarative style to procedural style (we won’t be able to use the declarative style from InforCRM since the grid is instantiated via code):

grid.on('StyleRow', function (gridRow) {
    var item = grid.getItem(gridRow.index);
    var pot = grid.store.getValue(item, "SalesPotential");
    if (pot > 1000000) {
        gridRow.customStyles += "color: red;";
    }
});

Second step: how do we get that code to run inside of InforCRM?? Unfortunately the grid is created inside of an anonymous function that runs via a setTimeout call, so it’s pretty hard to
hook into it directly. But we know the grid’s id (it’s hardcoded in the javascript file) so we can just register a script to run at a short interval until the grid is available, then add our style code to it.
For good measure we’ll add a call to resize() – this forces the grid to repaint its rows, applying our styles, in the unlikely event that it already got the rows built by the time our function runs.

ScriptManager.RegisterStartupScript(this, GetType(), "StyleGridRows", @"
require(['dijit/registry'], function(registry) {
    var i = setInterval(function() {
        var grid = registry.byId('AccountOpportunitiesgrdOpportunities');
        if(grid) {
            clearInterval(i);
            grid.on('StyleRow', function (gridRow) {
                var item = grid.getItem(gridRow.index);
                var pot = grid.store.getValue(item, 'SalesPotential');
                if (pot > 1000000) {
                    gridRow.customStyles += 'color: red;';
                }
            });
            grid.resize();
        }
    }, 250);    
});
", true);

I hard coded the id, but that’s OK, because it’s hard coded in the AccountOpportunities.js script also. Search for “id:” in that file, or use the Chrome developer tool, to find out what the id is.
Be sure to get the grid’s id and not the “gridNodeId” (which will end in _Grid) – that one is the container and does not have the style functions.

Here is the end result… obviously you can get “customStyles” as fancy as you want, you can also use “customClasses” to use a custom CSS class:

Although the example is simplistic this is a powerful visualization aid and I think once it is prettied up a little bit the users will like it. Leave a comment if you have an interesting use of the onStyleRow event!


Cross domain SData requests
Posted By: nicocrm on April 2nd, 2015 in General
No Gravatar

For Saleslogix SData is the key technology on the server side that enables us to build rich client side applications. Single-page applications are all the rage now and with sdata they can communicate with the Saleslogix database without the need to build a custom web service. However there is a key security limitation which is that your client app can only communicate with a service hosted on the same server, in some cases this may not be convenient. CORS (Cross-origin resource sharing) is a mechanism that lets us call the sdata service from a page that runs on another server (note that this applies only to client code that runs in the browser – if you run your code from a server-side app such as the C# code of an ASP.NET page, you need not worry about any of this!)

In theory this is pretty simple – you just configure a header on the server to let browsers know it is OK to post cross-domain requests to it. However there are a few complications, some of them are due to sdata, some of them to confusing information, so I thought I would summarize the requirements here. Note that I have not tested any of this with Windows authentication and I don’t even know if it is possible to do cross-domain requests with Windows credentials.

  • Change “verb” under handlers in web.config to “GET,POST,PUT,DELETE”. With CORS request the browser will do a “pre-flight” request using the “OPTIONS” verb and no credentials to “test the waters” – i.e. determine what the server is configured to let it send. We need sdata to not handle the “OPTIONS” verb which will be lacking credentials, otherwise it will return a 401 error and the browser will give up on the rest of the request!
  • Under the global server settings in IIS (not the site’s), go under the custom headers and add the following:
    • Access-Control-Allow-Headers: Authorization, Content-Type, X-Authorization, X-Requested-With, X-Authorization-Mode (the first one is the standard authorization header, the rest is custom headers that are used by the sdata javascript client library. If you are using custom javascript code and not the client library you might be able to stick to “Authorization” there)
    • Access-Control-Allow-Methods: GET,POST,PUT,OPTIONS
    • Access-Control-Allow-Origin: * (you can also set it to a specific domain instead of * if you want to limit requests to pages served from that domain but since the authentication credentials need to be known to the code making the requests it does not really give you any additional security)
    • You do NOT need to set Access-Control-Allow-Credentials. That one is only used for session-based authentication (i.e. cookies)
  • Likewise you do NOT need to set “withCredentials” on the HTTP request – that is again only used for cookie-based authentication

If all works well you should see the 2 requests sent by your code: the OPTIONS request which will return an empty body and a “200″ status, and the actual Sdata request which will return the data. If it does not work watch the console as the browser will try to print helpful error message to point out why it was not able to send a request.


Automatic resizing of the Saleslogix detail area for small screens
Posted By: nicocrm on October 21st, 2014 in General
No Gravatar

Wanted to share a small trick I used to make Saleslogix more usable on laptops, especially if you have customized screens that are very tall. On devices with a low resolution height, tall screens can push the resize handle low enough that it can’t be reached, and since there is no “global” scrollbar on the right side that will act for the whole page the users can’t get to the tabs at all! One solution would be to constrain the height of the detail panel… but then users with larger displays who may be able to enjoy viewing the full screen WITH the tabs are no longer able to! So I reached into the “responsive web design” bag of tricks and pulled a tiny media query to constrain the height only in case the screen’s resolution was low enough:

/* Hack to prevent stretching the main content so far 
   that it hides the resize handle */
@media(max-height: 900px) {
    #mainContentDetails     
    {
        max-height: 550px;
    }
}
/* even smaller screens */
@media(max-height: 700px) {
    #mainContentDetails     
    {
        max-height: 60%;
    }
}

Put that in your site’s shared CSS (you can also put it on a style tag on the page) and it will resize the content automatically so as to not push the tabs off screen, giving you a nice scrollbar instead. The 550px seemed like a good number for the 900 resolution point (which is where a lot of laptops are at once you take away the start bar and the browser chrome) and the 60% takes care of the tinier cases (it’s not ideal because at some resolutions you might want to stretch it a little bit taller and the CSS won’t let you! But you could always create more resolution points if needed). You could also use some smart Javascript to calculate the optimal height on the fly but I went with the quick & clean solution.

Note that in practice you’ll probably want to make the rule a little bit more specific so that certain pages are left to stretch out as much as they want. If your “Account” form in particular was very tall for example, you could use something like this to apply the rule only to the account detail page:

/* Hack to prevent stretching the main content so far 
   that it hides the resize handle */
@media(max-height: 900px) {
    form[action^=Account.aspx] #mainContentDetails     
    {
        max-height: 550px;
    }
}
/* even smaller screens */
@media(max-height: 700px) {
    form[action^=Account.aspx] #mainContentDetails     
    {
        max-height: 60%;
    }
}