Home » Sage CRM Hub » Sage CRM and AJAX
h

Table of Contents

$

Get Started with Sage CRM and AJAX

$

Use Advanced Sage CRM and AJAX Techniques

How to Get Started with Sage CRM and AJAX

When Gmail originally came out, it was a huge leap forward. The user experience was unlike other web applications at the time, and the heat behind the sizzle was an extensive use of AJAX.

AJAX, shorthand for Asynchronous JavaScript and XML, simply means that web browsers can exchange data with web servers behind the scenes.

Coupled with this technology is the ability to update parts of a web page without having to reload the whole page. These techniques result in better page performance, increased development flexibility, and oftentimes a better user experience.

Within the realm of Sage CRM development, AJAX is a very important technology because it adds a great deal of flexibility to customizations, especially with OOTB screens.

We will demonstrate a classic example on this page – adding a read-only screen to the Company Summary tab.

Sage CRM and Ajax

A Mishmash of Technologies

AJAX requires both client-side and server-side code to work in harmony, which requires a fair amount of boilerplate code.

To rectify this situation, we’ll be working towards creating generic functionality that can be reused with all of our customizations. Specifically, we’ll start by creating a generic AJAX handler in .NET for server-side processing and a JavaScript wrapper to simplify the client-side processing of the AJAX requests and responses.

We want to add a cautionary note that the following code samples utilize advanced features of the .NET framework such as partial classes, abstract classes, LINQ, reflection, and so on.

If these technologies are outside your current scope of knowledge, don’t worry – you can still make use of this in your own projects with minimal effort. And, if you want, we have written an article to familiarize you with .NET Controls for Sage CRM.

Server, I Have a Request

First, setup a CRM .NET SDK class library project with a target framework of v3.5. Add references to the CRM assemblies.

Because we’re dealing with AJAX only, it’s unnecessary to rig up tabs or anything to that effect. You simply need to configure the build properties of your project to output to the CustomDotnet folder within your Sage CRM installation directory.

Once your project is setup, add the following code. If you’re interested in the mechanics of this code, comments are sprinkled throughout.

using System;
using System.Reflection;
using System.Linq;
using Sage.CRM.WebObject;
using Sage.CRM.Utils;
using Sage.CRM.Data;
namespace CrmAjaxHandler.Part1
{
///
/// Sage CRM requires this static class.
/// NOTE: The partial keyword has been added to aid reusability.
///

public static partial class AppFactory
{
public static void AjaxPostToCrm(ref Web AretVal)
{
AretVal = new CrmAjaxHandler();
}
}

///
/// An instance of the CRM Web object used to handle all incoming AJAX requests.
///
class CrmAjaxHandler : Web
{
public override void BuildContents()
{
try
{
// validate the class name was passed from the client
var dotNetClassName = Dispatch.QueryField("DotNetClassNameToInvoke");

if (string.IsNullOrEmpty(dotNetClassName))
throw new Exception(".NET class name name must be passed using the querystring key 'DotNetClassNameToInvoke'.");

// find the type information for the class
// NOTE: if the same class name is defined multiple times in different namespaces
// w/in this assembly, the first one will be instantiated
var type = (from t in Assembly.GetExecutingAssembly().GetTypes()
where t.IsClass && t.Name == dotNetClassName
select t).FirstOrDefault();

// if the class doesn't exist, throw an error
if (type == null)
throw new Exception(string.Format(".NET class name '{0}' could not be located for any namespace.", dotNetClassName));
// otherwise, process the request and return the response
else
{
// invoke an instance of the desired class
var request = (CrmAjaxRequest)Activator.CreateInstance(type);

// pass the Dispatch info, which has querystring content and posted data
request.CrmDispatch = this.Dispatch;

// invoke the overriden method to process the request
var ajaxResponse = request.ProcessAjaxRequest();

// prevents extra HTML stuff (i.e. only returns what is in AddContent())
OverrideContent(null);

// return only the string from ProcessAjaxRequest()
AddContent(ajaxResponse);
}
}
catch (Exception ex)
{
AddContent("Houston, we have a problem... " + ex.Message);
}
}
}

///
/// This abstract class passes the Dispatch object and
/// ensures a consistent method is used to process the request.
///
public abstract class CrmAjaxRequest
{
public Dispatch CrmDispatch;

public abstract string ProcessAjaxRequest();
}
}

Putting the J in AJAX

First, create a file called CrmAjax.js with the following code:

var ajaxDotNetDllName;
var ajaxDotNetMethodName = "AjaxPostToCrm";

function PostToCrm(dotNetDllName, dotNetClassName, urlVarsObj, callback, writeErrorToSelector) {
ajaxDotNetDllName = dotNetDllName;
try {
var baseAjaxUrl = GetBaseAjaxUrl();
var ajaxUrl = baseAjaxUrl + '&DotNetClassNameToInvoke=' + dotNetClassName;

// loop through query string object
for (var key in urlVarsObj) {
ajaxUrl += '&' + key + '=' + urlVarsObj[key];
}

$.post(ajaxUrl, function (data) {
callback(data);
});
}
catch (ex) {
var msg = GetFormattedError(ex);
console.log(msg);
$(writeErrorToSelector).append(msg);
}
}

function GetBaseAjaxUrl() {
var currentUrl = window.location.href;
var baseUrl;
var virtualDirIdx;
// check if page is standard system action
if ((virtualDirIdx = currentUrl.indexOf("eware.dll")) != -1)
baseUrl = currentUrl.substring(0, virtualDirIdx);
// check if page is custom
else if ((virtualDirIdx = currentUrl.indexOf("CustomPages")) != -1)
baseUrl = currentUrl.substring(0, virtualDirIdx);
else {
var errorMsg = 'Base CRM path could not be determined.';

if(console)
console.log(errorMsg);

throw errorMsg;
}

var params = "";
params += "SID=" + GetUrlVar("SID");
params += "&Act=432";
params += "&dotnetdll=" + ajaxDotNetDllName;
params += "&dotnetfunc=" + ajaxDotNetMethodName;

return baseUrl + '/eware.dll/Do?' + params;
}

function GetFormattedError(ex) {
var msg = 'Oops, an error has occured:';
for (var prop in ex) {
msg += prop + ": " + ex[prop] + "
"; } return msg; } function GetUrlVar(varName) { url = unescape(self.document.location); if (varstring = url.substring(url.indexOf('?') + 1)) { varpairs = varstring.split('&'); for (i = 0; i < varpairs.length; i++) { varpair = varpairs[i].split('='); if (varName == varpair[0]) { return varpair[1]; } } } return false; }

Functions of the Functions

PostToCrm – this is the wrapper function that encapsulates the client-side AJAX call and processing. This is the one you’ll want to call from your JavaScript file to make an AJAX call. Ultimately, this in turn wraps the $.post function provided by jQuery to execute a post request. The parameters are as follows:

  • dotNetDllName – the relative path to the CustomDotNet folder and name of the .NET DLL. If the assembly is located in a sub-folder, you can pass the path like “Ignite/CrmAjaxHandler.dll” where “Ignite” is a sub-folder of the CustomDotNet folder.
  • dotNetClassName – the name of the .NET class you wish to invoke. The class must derive from the abstract class named CrmAjaxRequest in your .NET project.
  • urlVarsObj – a JavaScript object defining the key/value pairs to be added to the query string. For example, if you want to pass the person ID from a page, you may pass something like { Key2: 1552 }.
  • Callback – AJAX calls require a callback function to execute once the response is returned. Note that this call is ASYNCHRONOUS as is, meaning the rest of your code will continue execution immediately after PostToCrm is called; the callback will be executed once the server returns a response. This is default behavior for jQuery AJAX functions.
  • writeErrorToSelector – in the event of an error, you’ll no doubt want to display it somewhere. In fact, issues in your CRM customizations that use AJAX may go unnoticed if you’re not diligent in how you report errors. Fiddler, the web debugging tool, is your friend here. As far as this parameter goes, simply pass in a jQuery selector and the error information will be appended to the matching DOM elements.

GetBaseAjaxUrl – this function parses the current page’s URL to build the base URL needed to invoke our method within the .NET assembly. To have CRM invoke a .NET method, the following parameters are required:

  • SID – this is the Session ID for the logged-in user.
  • Act – this must be 432, which is the action code for invoking .NET.
  • DotNetDll – this specifies the name for the .NET class library assembly (i.e. DLL). The DLL must be located within the CustomDotNet folder of your CRM install.
  • DotNetFunc – the .NET method name to invoke within the AppFactory class.

Note: When parsing the URL, both “eware.dll” and “CustomPages” are checked. This permits the AJAX wrapper to function in both custom pages and OOTB ones.

GetFormattedError – this function simply appends the exception properties together in case there is an exception thrown.

GetUrlVar – this is a helper function that returns the value for a particular query string key. In this case, we’re retrieving the SID key (i.e. Session ID) from the URL which must be passed in any Sage CRM URL.

Seeing it in Action

All of this code may seem a bit overwhelming thus far but that’s the point of this article series. AJAX calls DO require some legwork. However, with the handler code now in place, AJAX is now a breeze. Let’s see it in action.

First, add a new implementation of CrmAjaxRequest into your .NET project.

The ProcessAjaxRequest method (required when inheriting from CrmAjaxRequest) returns the count of open cases for the company.

///
/// Here's the CrmAjaxHandler in action!
///

public class GetCompanyStatistics : CrmAjaxRequest
{
public override string ProcessAjaxRequest()
{
// use the derived CrmDispatch to retrieve querystring info
var companyId = CrmDispatch.QueryField("CompanyId");

// TIP: Validate your querystring parameters and return self-explanatory
// exceptions when there are validation issues
if(string.IsNullOrEmpty(companyId))
throw new Exception("CompanyId is a required Querystring parameter.");
QuerySelect qry = new QuerySelect();
qry.SQLCommand = string.Format(@"SELECT COUNT(*) AS CountOfOpenCases 
FROM Cases
WHERE Case_PrimaryCompanyId = {0}
AND Case_Closed IS NULL
AND Case_Deleted IS NULL", companyId);

qry.ExecuteReader();

if (!qry.Eof())
return qry.FieldValue("CountOfOpenCases");
else
return "0";
}
}

Create a new JavaScript file named CompanyStatistics.js. Outside of calling PostToCrm, the client-side AJAX wrapper, this code also appends an empty screen to the Company Summary tab to display the company statistics.

$(document).ready(function () {

// create an empty clone of the existing Contact screen
AddCompanyStatsScreen();

var companyId = getURLVar("Key1");

PostToCrm('Ignite/CrmAjaxHandler.dll', 'GetCompanyStatistics', { CompanyId: companyId }, function (response) {
$('#CountOfOpenCases').text(response);
}, 'body');
})

function AddCompanyStatsScreen() {
// get table rows that make up existing Contact screen
var rowForScreenTab = $("a.PANEREPEAT:contains('Contact')").closest('table').closest('tr');
var rowForScreenGap = rowForScreenTab.prev();
var rowForScreenContent = rowForScreenTab.next();
var rowForScreenBorder = rowForScreenContent.next();

// get table that holds screen rows
var screenContainer = rowForScreenTab.closest('table');

// modify table rows before adding to DOM
var rowForScreenGapMod = rowForScreenGap.clone();
var rowForScreenTabMod = rowForScreenTab.clone();
rowForScreenTabMod.find('a.PANEREPEAT').text('Company Statistics');

var rowForScreenContentMod = rowForScreenContent.clone();
rowForScreenContentMod.find('table.CONTENT > tbody')
.html('<br /><span class="VIEWBOXCAPTION"># of Open Cases:</span><br /><span id="CountOfOpenCases" class="VIEWBOX" style="width: '100px';"> </span></p><p>');

var rowForScreenBorderMod = rowForScreenBorder.clone();

// add rows to the table
screenContainer.append(rowForScreenGapMod)
.append(rowForScreenTabMod)
.append(rowForScreenContentMod)
.append(rowForScreenBorderMod);
}

Finally, add the following script references to the Custom Content of the Company Entry Screen (CompanyBoxLong):

<script src="../CustomPages/Ignite/CrmAjaxHandler/CrmAjax.js"></script>
<script src="../CustomPages/Ignite/CrmAjaxHandler/CompanyStatistics.js"></script>

$.ajaxComplete()

There you have it – a simpler way to use AJAX in Sage CRM.

In the next section, we will expand this functionality considerably by allowing JavaScript objects to be included with the AJAX request, and add the necessary deserialization/serialization of objects for both JavaScript and .NET.

We’ll also incorporate a structured AJAX response to wrap the response data plus include a response status and optional message. You’re well on your way to mastering AJAX in Sage CRM.

h

Return to Top

How to Use Advanced Sage CRM and AJAX Techniques

In the first section, we set the stage for a simpler, reusable approach to using AJAX within our CRM customizations.

As a simple illustration, we passed a single query string parameter (company ID), posted no data, and responded with the value of interest (# of open cases).

This was accomplished via a JavaScript wrapper for our AJAX call and a generic .NET AJAX handler to process the request and return a response.

This time around, we’ll extend this functionality considerably to include serialization and deserialization of both JavaScript and .NET objects, a structured AJAX response, and improved error handling. After incorporating the following code into your projects, you’ll have everything you need to simplify AJAX development within Sage CRM.

In the previous section, we added a Company Statistics screen to the Company Summary tab.

We’ll continue in this direction by adding two additional statistics. Having three values returned instead of just one within a single AJAX call necessitates that we expand our AJAX framework to allow objects.

The resulting customization will have three statistics – # of People, # of Open Cases, and # of Open Opportunities – as outlined in the screenshot.

JSON – A Chip off the Old Block

Before discussing serialization/deserialization, it’s important to first understand what JSON is.

JSON, an acronym for JavaScript Object Notation, is a text-based, human-readable standard for data exchange. JSON was modeled after JavaScript’s “object literal notation” and the two are often confused.

Given this, let us be clear that the code in the following example is not JSON.

The JavaScript defines an array of objects using object literal notation. These are actual JavaScript objects whose properties can be accessed using the dot operator as illustrated.

In contrast, the JSON representation of these JavaScript objects is:

[{"Name":{"First":"John","Last":"Deaux"},"Age":35},{"Name":{"First":"Susie","Last":"Q"},"Age":42}]

Do you see the similarity? It’s no wonder why folks get confused.

Serialization & Deserialization

Now that you know the skinny on JSON, you’re ready to learn about serialization and deserialization.

In both JavaScript and .NET, it’s in our best interest to work with actual objects instead of text. If this is the case, how do we transfer a .NET object to JavaScript and vice versa? You guessed it – we need serialization and deserialization.

Serialization is simply the process of converting an object (or objects) into a text representation (JSON, in this case). Deserialization is the reverse – converting a text representation to an object (or objects).

Both JavaScript and .NET have means to accomplish these conversions. With the primer out of the way, let’s add some code.

Server, I Have a Request – v2

Taking the .NET project from the previous article, add the following assembly references:

  • System.Runtime.Serialization.dll – where the System.Runtime.Serialization namespace resides, which includes the DataContract and DataMember attributes. These attributes are needed when deserializing JSON into .NET objects.
  • System.Web.Extensions.dll – where the System.Web.Script.Serialization namespace resides, which includes the JavaScriptSerializer class. This class allows .NET objects to be serialized to JSON with minimal effort on our part.
  • System.ServiceModel.Web.dll – where the System.Runtime.Serialization.Json namespace resides, which includes the DataContractJsonSerializer class. This class is responsible for marrying JSON data with classes and properties/fields decorated with DataContract and DataMember attributes.

With that out of the way, overwrite the old AJAX handler code with the below code. If you’re interested in the mechanics of the code, there are comments throughout.

using System;
using System.IO;
using System.Text;
using System.Linq;
using System.Reflection;
using System.Collections.Generic;
using System.Runtime.Serialization; // System.Runtime.Serialization.dll
using System.Web.Script.Serialization; // System.Web.Extensions.dll
using System.Runtime.Serialization.Json; // System.ServiceModel.Web.dll
using Sage.CRM.WebObject;
using Sage.CRM.Utils;
using Sage.CRM.Data;

namespace CrmAjaxHandler.Part2
{
///
/// Sage CRM requires this static class.
/// NOTE: The partial keyword has been added to aid reusability.
///

public static partial class AppFactory
{
public static void AjaxPostToCrm(ref Web AretVal)
{
AretVal = new CrmAjaxHandler();
}
}

///
/// An instance of the CRM Web object used to handle all incoming AJAX requests.
///

class CrmAjaxHandler : Web
{
public override void BuildContents()
{
CrmAjaxResponse response;

// the current assembly name for error reporting purposes
string executingTypeName = Assembly.GetExecutingAssembly().GetName().FullName;

try
{
// prevents extra HTML stuff (i.e. only returns what is in AddContent())
OverrideContent(null);

// validate the class name was passed from the client
var dotNetClassName = Dispatch.QueryField("DotNetClassNameToInvoke");

if (string.IsNullOrEmpty(dotNetClassName))
throw new Exception(".NET class name name must be passed using the querystring key 'DotNetClassNameToInvoke'.");

var type = (from t in Assembly.GetExecutingAssembly().GetTypes()
where t.IsClass && t.Name == dotNetClassName
select t).FirstOrDefault();

if (type == null)
throw new Exception(string.Format(".NET class name '{0}' could not be located for any namespace.", dotNetClassName));
else
{
// prepend the ajax request class to be invoked for error reporting purposes
executingTypeName = string.Format("{0} (Assembly: {1})", type.FullName, executingTypeName);

// invoke an instance of the desired class
var request = Activator.CreateInstance(type);

// if is AJAX call with data, deserialize the data
if (type.BaseType.Name != "CrmAjaxRequest")
{
var data = this.Dispatch.ContentField("data");

MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(data));

// get the generic type specified in the derived type (e.g. Person in SomeAjaxClass : CrmAjaxRequestWithData )
var typeOfT = request.GetType().BaseType.GetGenericArguments()[0];

// generate objects from posted JSON via classes decorated with DataContract and DataMember attributes
DataContractJsonSerializer deserializer = new DataContractJsonSerializer(typeOfT);
var deserialized = Convert.ChangeType(deserializer.ReadObject(stream), typeOfT);

type.GetField("Data", BindingFlags.Public | BindingFlags.Instance).SetValue(request, deserialized);
}

type.GetField("CrmDispatch", BindingFlags.Public | BindingFlags.Instance).SetValue(request, this.Dispatch);

response = (CrmAjaxResponse)request.GetType().GetMethod("ProcessAjaxRequest").Invoke(request, null);
}
}
// if an exception occurs, override the present CrmAjaxResponse and return the error details
catch (Exception ex)
{
if (ex.InnerException != null)
ex = ex.InnerException;

var errorMsg = string.Format("An unexpected error has occurred calling {0}.", executingTypeName);
var errorData = string.Format(@"<b>Message:</b> {0}

<b>Source:</b> {1}

<b>TargetSite:</b> {2}

<b>StackTrace:</b> {3}", ex.Message, ex.Source, ex.TargetSite, ex.StackTrace);

// NOTE: don't serialize the Exception object; this results in a circular reference exception
response = new CrmAjaxResponse(CrmAjaxResponseStatus.Error, errorMsg, errorData);
}

// success or not, serialize the instance of CrmAjaxResponse as JSON and return to the browser
AddContent(Serialize(response));
}

// enumerations are serialized as integers, so create an anonymous type and swap out with a string
string Serialize(CrmAjaxResponse response)
{
return new JavaScriptSerializer().Serialize(new
{
Status = Enum.GetName(typeof(CrmAjaxResponseStatus), response.Status),
Message = response.Message,
Data = response.Data
});
}
}

public enum CrmAjaxResponseStatus
{
Success,
Warning,
Error
}

///
/// A wrapper for responding to a CrmAjaxRequest.
///

public class CrmAjaxResponse
{
public CrmAjaxResponseStatus Status;

public string Message;

public object Data;

public CrmAjaxResponse(CrmAjaxResponseStatus status)
: this(status, "", "") { }

public CrmAjaxResponse(CrmAjaxResponseStatus status, Object data)
: this(status, "", data) { }

public CrmAjaxResponse(CrmAjaxResponseStatus status, string message, Object data)
{
Status = status;
Message = message;
Data = data;
}
}

///
/// An AJAX request with posted data.
///

/// The parent type (e.g. List for a JSON array of Person objects)
public abstract class CrmAjaxRequestWithData
{
public Dispatch CrmDispatch;

public abstract CrmAjaxResponse ProcessAjaxRequest();

public T Data;

public CrmAjaxRequestWithData() { }
}

///
/// An AJAX request wrapper for querystring-only data.
///

public abstract class CrmAjaxRequest : CrmAjaxRequestWithData
{
protected CrmAjaxRequest()
{
base.Data = "";
}
}
}

Putting the J in AJAX – v2

Add a new JavaScript file called json2.js. The source code can be retrieved from here: https://github.com/douglascrockford/JSON-js/blob/master/json2.js. Add the JavaScript reference to the Custom Content of the Company Entry Screen (CompanyBoxLong).

NOTE: json2.js was developed by Douglas Crockford, a prominent contributor to the JavaScript language and JSON. This particular JavaScript library is essential because it includes JSON.stringify, the function responsible for serializing JavaScript objects to JSON. Also of great importance, jQuery includes the $.parseJSON function which is used to deserialize JSON into JavaScript objects.

Overwrite the code in CrmAjax.js with the following to add the JavaScript wrapper for Sage CRM AJAX calls.

/*
NOTE: json2.js required for JSON object. See https://github.com/douglascrockford/JSON-js/blob/master/json2.js.
*/

var ajaxDotNetDllName;
var ajaxDotNetMethodName = "AjaxPostToCrm";

function PostToCrm(dotNetDllName, dotNetClassName, urlVarsObj, dataToPostObj, callback, writeErrorToSelector) {

ajaxDotNetDllName = dotNetDllName;

try {

var baseAjaxUrl = GetBaseAjaxUrl();

var ajaxUrl = baseAjaxUrl + '&DotNetClassNameToInvoke=' + dotNetClassName;

// loop through query string object
if (typeof urlVarsObj === 'object') {
for (var key in urlVarsObj) {
ajaxUrl += '&' + key + '=' + urlVarsObj[key];
}
}

var dataToPost = 'data=' + encodeURIComponent(JSON.stringify(dataToPostObj));

$.post(ajaxUrl, dataToPost, function (data) {

var response = $.parseJSON(data);

if (response.Status == "Error")
HandleAjaxResponseError(response, writeErrorToSelector);
else
callback(response);
});
}
catch (ex) {
HandleException(ex, writeErrorToSelector);
}
}

function GetBaseAjaxUrl() {

var currentUrl = window.location.href;

var baseUrl;
var virtualDirIdx;

// check if page is standard system action
if ((virtualDirIdx = currentUrl.indexOf("eware.dll")) != -1)
baseUrl = currentUrl.substring(0, virtualDirIdx);
// check if page is custom
else if ((virtualDirIdx = currentUrl.indexOf("CustomPages")) != -1)
baseUrl = currentUrl.substring(0, virtualDirIdx);
else {

var errorMsg = 'Base CRM path could not be determined.';

if (console)
console.log(errorMsg);

throw errorMsg;
}

var params = "";

params += "SID=" + GetUrlVar("SID");
params += "&Act=432";
params += "&dotnetdll=" + ajaxDotNetDllName;
params += "&dotnetfunc=" + ajaxDotNetMethodName;

return baseUrl + '/eware.dll/Do?' + params;
}

function HandleAjaxResponseError(response, writeErrorToSelector) {

if (console)
console.log(response.Data.REPLACE(cast(cast(/
/g as nvarchar(max)) as nvarchar(max)),cast(cast( 'rn').replace(//g as nvarchar(max)) as nvarchar(max)),cast(cast( '' as nvarchar(max as nvarchar(max))))).replace(//g, ''));

$(writeErrorToSelector).append(response.Message + '
' + response.Data);
}

function HandleException(ex, writeErrorToSelector) {

var msg = 'An unexpected error has occurred:
';

for (var prop in ex) {
msg += prop + ": " + ex[prop] + "
";
}

if (console)
console.log(msg);

$(writeErrorToSelector).append(msg);
}

function GetUrlVar(varName) {
url = unescape(self.document.location);
if (varstring = url.substring(url.indexOf('?') + 1)) {
varpairs = varstring.split('&');
for (i = 0; i < varpairs.length; i++) { varpair = varpairs[i].split('='); if (varName == varpair[0]) { return varpair[1]; } } } return false; } 

CRM… You’ve Been Ajaxified!

We’re now ready to retrieve some company statistics!

To better illustrate how objects come into play, we added a generic GetStatistics AJAX handler.

The thought here is that we’ll pass some data indicating what type of statistics we want returned. Following this implementation, we can post an array of JavaScript objects indicating the stats we want returned and see serialization/deserialization in action.

First, add the helper class to retrieve the company statistics. Also included here is the definition for a Statistic.

Take note of the DataContract and DataMember attributes; these are necessary to deserialize from JSON. Also ensure class and properties/fields are public for any classes that will be deserialized; otherwise, they won’t deserialize.

///
/// An example definition for a struct (or class) that can be generated via JSON deserialization.
///
[DataContract]
public struct Statistic
{
[DataMember]
public string Name;

[DataMember]
public string EntityType;

[DataMember]
public int EntityId;

[DataMember]
public object Value;
}

public class CompanyStatistics
{
public static int GetCountOfPeople(int companyId)
{
QuerySelect qry = new QuerySelect();

/* # of People */

qry = new QuerySelect();

qry.SQLCommand = string.Format(@"SELECT COUNT(*) AS CountOfPeople
FROM Person
WHERE Pers_CompanyId = {0}
AND Pers_Deleted IS NULL", companyId);

qry.ExecuteReader();

if (!qry.Eof())
return int.Parse(qry.FieldValue("CountOfPeople"));
else
return 0;
}

public static int GetCountOfOpenCases(int companyId)
{
QuerySelect qry = new QuerySelect();

qry.SQLCommand = string.Format(@"SELECT COUNT(*) AS CountOfOpenCases
FROM Cases
WHERE Case_PrimaryCompanyId = {0}
AND Case_Closed IS NULL
AND Case_Deleted IS NULL", companyId);

qry.ExecuteReader();

if (!qry.Eof())
return int.Parse(qry.FieldValue("CountOfOpenCases"));
else
return 0;
}

public static int GetCountOfOpenOpportunities(int companyId)
{
QuerySelect qry = new QuerySelect();

qry.SQLCommand = string.Format(@"SELECT COUNT(*) AS CountOfOpenOpportunities
FROM Opportunity
WHERE Oppo_PrimaryCompanyId = {0}
AND Oppo_Closed IS NULL
AND Oppo_Deleted IS NULL", companyId);

qry.ExecuteReader();

if (!qry.Eof())
return int.Parse(qry.FieldValue("CountOfOpenOpportunities"));
else
return 0;
}
}

Next, add the CrmAjaxRequest handler.

This is the critical part. In particular, note that GetStatistics inherits from CrmAjaxRequestWithData<List<Statistic>>.

The base class of importance is CrmAjaxRequestWithData<T>. T, the generic type, specifies what the posted data should be deserialized to. Everything else happens automatically within our AJAX handler based on the type specified for CrmAjaxRequestWithData.

///
/// An example implementation of CrmAjaxRequestWithData for a JSON array.
/// In this case, the posted data (i.e. JSON) will be parsed from an array of statistic objects.
///
public class GetStatistics : CrmAjaxRequestWithData <list>
{
public override CrmAjaxResponse ProcessAjaxRequest()
{
// Process the action. Use querystring fields from CrmDispatch as needed.

var response = new List();            

// Use Data to reach parent object (T) for deserialized data posted from browser.
foreach (Statistic statistic in Data) {
if (statistic.EntityType == "Company") {
switch (statistic.Name) {

case "CountOfPeople":
var countOfPeople = CompanyStatistics.GetCountOfPeople(statistic.EntityId);
response.Add(new { 
Name = statistic.Name, Value = countOfPeople 
}
);
break;

case "CountOfOpenCases":
var countOfOpenCases = CompanyStatistics.GetCountOfOpenCases(statistic.EntityId);
response.Add(new { 
Name = statistic.Name, 
Value = countOfOpenCases 
}
);
break;

case "CountOfOpenOpportunities":
var CountOfOpenOpportunities = CompanyStatistics.GetCountOfOpenOpportunities(statistic.EntityId);
response.Add(new { 
Name = statistic.Name, 
Value = CountOfOpenOpportunities 
}
);
break;
}
}

// handle other entity stats 
}
return new CrmAjaxResponse(CrmAjaxResponseStatus.Success, "Congrats, your request was a success!", response);
}
}

Lastly, overwrite the JavaScript file named CompanyStatistics.js with the following code.

Here, we’re posting an array of JavaScript objects with matching property names. The array of objects is first serialized to JSON client-side, and then posted to the server, and finally deserialized server-side to a List of Statistic objects.

$(document).ready(function () {
// create an empty clone of the existing Contact screen
AddCompanyStatsScreen();
var companyId = GetUrlVar("Key1");
// must be from the recent list
if (!companyId) {
var recentValue = GetUrlVar("RecentValue");
companyId = recentValue.substring(recentValue.lastIndexOf("X") + 1);}

// an array of objects to post with the request, indicating the statistics
// we want returned
var statsToRequest = [
{Name: "CountOfOpenCases",EntityType: "Company",EntityId: companyId}, 
{Name: "CountOfOpenOpportunities",EntityType: "Company",EntityId: companyId}, 
{Name: "CountOfPeople",EntityType: "Company",EntityId: companyId}]
PostToCrm('Ignite/CrmAjaxHandler.dll', 'GetStatistics', null, statsToRequest, function (response) {

// this is the response status - Success, Warning, or Error
var status = response.Status;

// this is the optional message
var message = response.Message;

// this is the deserialized object or objects
var stats = response.Data;

// in this case, we serialized a .NET List of objects, which will be deserialized in
// JavaScript as an array of objects; note the Name and Value properties as returned
// from the .NET side
for (var idx in stats) {
$('#' + stats[idx].Name).text(stats[idx].Value);}}, 'body');});

function AddCompanyStatsScreen() {
// get table rows that make up existing Contact screen
var rowForScreenTab = $("a.PANEREPEAT:contains('Contact')").closest('table').closest('tr');
var rowForScreenGap = rowForScreenTab.prev();
var rowForScreenContent = rowForScreenTab.next();
var rowForScreenBorder = rowForScreenContent.next();

// get table that holds screen rows
var screenContainer = rowForScreenTab.closest('table');

// modify table rows before adding to DOM
var rowForScreenGapMod = rowForScreenGap.clone();
var rowForScreenTabMod = rowForScreenTab.clone();
rowForScreenTabMod.find('a.PANEREPEAT').text('Company Statistics');
var rowForScreenContentMod = rowForScreenContent.clone();
rowForScreenContentMod.find('table.CONTENT > tbody').html('<tr><td valign="TOP">
<span class="VIEWBOXCAPTION"># of People:
</span><span id="CountOfPeople" class="VIEWBOX" style="width: '100px';">
&nbsp;</span></td><td valign="TOP">
<span class="VIEWBOXCAPTION"># of Open Cases:
</span><span id="CountOfOpenCases" class="VIEWBOX" style="width: '100px';">
&nbsp;</span></td><td valign="TOP">
<span class="VIEWBOXCAPTION"># of Open Opportunities:
</span>
<span id="CountOfOpenOpportunities" class="VIEWBOX" style="width: '100px';">
&nbsp;</span></td></tr>');

var rowForScreenBorderMod = rowForScreenBorder.clone();

// add rows to the table
screenContainer
.append(rowForScreenGapMod)
.append(rowForScreenTabMod)
.append(rowForScreenContentMod)
.append(rowForScreenBorderMod);
}

To the Tune of Fiddler

Viewing the HTTP request/response of the AJAX call in Fiddler provides insights into what is happening behind the scenes.

The Request

Note that the .NET class to invoke is GetStatistics, the .NET class we created to handle the request and return a structured response. Within the body of the request, we see a data field has been posted with the JSON of the JavaScript objects we passed to the PostToCrm function. This is the result of calling JSON.stringify within our wrapper function to deserialize the data.

The Response

The response itself is also JSON, representing a structured response with three key properties: Status, Message, and Data.

  • Status – this comes from the CrmAjaxResponseStatus value set in the AJAX response. In the case of an exception, this is automatically set to Error. Otherwise, you have control to set this to Success, Warning, or Error depending upon your goals.
  • Message – an optional property that can be used to communicate additional information about the AJAX response.
  • Data – this is the serialized data for whatever was returned from the server. From our AJAX wrapper in JavaScript, this is automatically deserialized and can be accessed as JavaScript objects.

No Data, No Worries

As a final note, you can inherit from CrmAjaxRequest when an AJAX call doesn’t require posted data.

/// An example implementation of CrmAjaxRequest (i.e. no posted data).
public class ExampleWithoutData : CrmAjaxRequest {
public override CrmAjaxResponse ProcessAjaxRequest(){
// Process the action. Use querystring fields from Dispatch as needed.
return new CrmAjaxResponse(CrmAjaxResponseStatus.Success, "no data!");
}
}

AJAX – the CRM Revolution

As users demand more from their CRM customizations, AJAX usage will become an increasing necessity.

As CRM developers, our efforts must be focused on richer user experiences with AJAX at the foundation. But there’s no need to fret, fellow coder.

You have been armed with the means to make Sage CRM ajaxification a breeze.Now all you must do is thrust your keyboard into the air and join the CRM revolution!

h

Return to Top