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!


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%;
    }
}

Saleslogix Mobile, Safari, iOS and Your Sanity
Posted By: Alex.Cottner on July 15th, 2014 in General
No Gravatar

Today I troubleshooted a strange issue with Saleslogix mobile on iOS. Whenever the user would hit the back button in the mobile client, they would actually get returned to the previous website they were on rather than the previous form in the mobile. After a lot of head scratching, I discovered this was due to a single setting being enabled in Safari. If the “Show Favorites Bar” setting has been enabled in Safari, this unexpected behavior will happen.

Update: A discussion with a Saleslogix support rep revealed that this, as well as some other iOS problems, can be resolved by setting the default document to index-nocache.aspx.