Showing posts with label JScript. Show all posts
Showing posts with label JScript. Show all posts

29 April 2009

Show both active and inactive records in the lookup view

I had a post about how to return both active and inactive records in the Quick Find View.
People then ask: how to show both active and inactive/deactivated records in the entity's Lookup View?

CRM MVP Batistuta Cai already had a post about a plug-in solution.

If the lookup entity is a system entity, you can also use this technique:

Let's start from an example: you have a custom entity call: MyEntity, you have setup a N:1 relationship between MyEntity and Opportunity, so the user can see an opportunity lookup field on the MyEntity form. Now you want to show users both active and inactive opportunities from that lookup field, all you need to do is put the below code into MyEntity.OnLoad() event:

crmForm.all.new_opportunityid.lookupclass = "alllookups";

The lookup class are controlled via xml files in %ProgramFiles%\Microsoft CRM\Server\ApplicationFiles\
If you take a look at the file: opportunity.xml, you may find a condition like: <condition attribute="statecode" operator="eq" value="0"/>

you can remove the condition, and then use this class, e.g: crmForm.all.new_opportunityid.lookupclass="opportunity"; However it's very much unsupported way(by changing files)! But if you open the file: alllookups.xml, you may find that the opportunity(object type="3") entity doesn't have such condition, so we can use this class to get all opportunities.

16 January 2009

CRM 4.0: Checkbox style Multi-Select Picklist

CRM 4.0 doesn't have many out-of-box user controls, e.g: a mulit-select picklist. The standard CRM picklist can only save one value in the database, it's not easy to extend this functionality, in addition, you have to deal with the Advanced Find feature.

You can make a picklist multi-selectable by enable the picklist mulitple attribute , e.g: crmForm.all.new_picklist.multiple = true; And then save the selected values somewhere else. However, it does not very impressive the user because the user has to use the CTRL key to select options, which is not user-friendly (Thanks for Alastair Westland (PM @ Parity) who work with me to improve the interface design:)

The script below will draw a checkbox style mulit-select picklist control on the CRM form, and then get options from the real picklist attribute. So how to use it?

1. Create a standard picklist attribute with all options in CRM, put it on the CRM Form. e.g: new_picklist;
2. Create another nvarchar attribute in CRM to save the selected text, put it on the CRM Form and hide the label. e.g: new_picklistvalue;
3. Put the following script in the Form.OnLoad() event.

*NOTE: There is a 'br' flag(var addBr = document.createElement(...) ) just been ignord by blogspot, please replace it when you paste the code!!!


/*
Checkbox style Multi-Select Picklist
author: Jim Wang @ January 2009
http://jianwang.blogspot.com
*/

// PL - the picklist attribute; PLV - used to save selected picklist values
var PL = crmForm.all.new_picklist;
var PLV = crmForm.all.new_picklistvalue;

if( PL != null && PLV != null )
{
PL.style.display = "none";
PLV.style.display = "none";

// Create a DIV container
var addDiv = document.createElement("<div style='overflow-y:auto; height:80px; border:1px #6699cc solid; background-color:#ffffff;' />");
PL.parentNode.appendChild(addDiv);

// Initialise checkbox controls
for( var i = 1; i < PL.options.length; i++ )
{
var pOption = PL.options[i];
if( !IsChecked( pOption.text ) )
var addInput = document.createElement("<input type='checkbox' style='border:none; width:25px; align:left;' />" );
else
var addInput = document.createElement("<input type='checkbox' checked='checked' style='border:none; width:25px; align:left;' />" );

var addLabel = document.createElement( "<label />");
addLabel.innerText = pOption.text;

var addBr = document.createElement( "<br />"); //it's a 'br' flag

PL.nextSibling.appendChild(addInput);
PL.nextSibling.appendChild(addLabel);
PL.nextSibling.appendChild(addBr);
}

// Check if it is selected
function IsChecked( pText )
{
if(PLV.value != "")
{
var PLVT = PLV.value.split("||");
for( var i = 0; i < PLVT.length; i++ )
{
if( PLVT[i] == pText )
return true;
}
}
return false;
}

// Save the selected text, this filed can also be used in Advanced Find
crmForm.attachEvent( "onsave" , OnSave);
function OnSave()
{
PLV.value = "";
var getInput = PL.nextSibling.getElementsByTagName("input");

for( var i = 0; i < getInput.length; i++ )
{
if( getInput[i].checked)
{
PLV.value += getInput[i].nextSibling.innerText + "||";
}
}
}
}


Note: Please be aware of this is an unsupported customization.

11 December 2008

CRM 4.0 Get attribute value from entity's GUID using JScript

Recently I have been asked many times about how to get attribute value from entity's GUID using JScript?
The following code demonstrate how to get a user's internalemailaddress by giving user's GUID.
*It has been modified to support multi-tenent deployment.


alert(GetAttributeValueFromID("systemuser", "09DF2AB7-E16D-DD11-88F3-0003FF884968", "internalemailaddress", "systemuserid"));

function GetAttributeValueFromID(sEntityName, sGUID, sAttributeName, sID)
{
var xml = "" +
"<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">" +
GenerateAuthenticationHeader() +
" <soap:Body>" +
" <RetrieveMultiple xmlns=\"http://schemas.microsoft.com/crm/2007/WebServices\">" +
" <query xmlns:q1=\"http://schemas.microsoft.com/crm/2006/Query\" xsi:type=\"q1:QueryExpression\">" +
" <q1:EntityName>"+sEntityName+"</q1:EntityName>" +
" <q1:ColumnSet xsi:type=\"q1:ColumnSet\">" +
" <q1:Attributes>" +
" <q1:Attribute>"+sAttributeName+"</q1:Attribute>" +
" </q1:Attributes>" +
" </q1:ColumnSet>" +
" <q1:Distinct>false</q1:Distinct>" +
" <q1:PageInfo>" +
" <q1:PageNumber>1</q1:PageNumber>" +
" <q1:Count>1</q1:Count>" +
" </q1:PageInfo>" +
" <q1:Criteria>" +
" <q1:FilterOperator>And</q1:FilterOperator>" +
" <q1:Conditions>" +
" <q1:Condition>" +
" <q1:AttributeName>"+sID+"</q1:AttributeName>" +
" <q1:Operator>Equal</q1:Operator>" +
" <q1:Values>" +
" <q1:Value xsi:type=\"xsd:string\">"+sGUID+"</q1:Value>" +
" </q1:Values>" +
" </q1:Condition>" +
" </q1:Conditions>" +
" </q1:Criteria>" +
" </query>" +
" </RetrieveMultiple>" +
" </soap:Body>" +
"</soap:Envelope>" +
"";

var xmlHttpRequest = new ActiveXObject("Msxml2.XMLHTTP");

xmlHttpRequest.Open("POST", "/mscrmservices/2007/CrmService.asmx", false);
xmlHttpRequest.setRequestHeader("SOAPAction","http://schemas.microsoft.com/crm/2007/WebServices/RetrieveMultiple");
xmlHttpRequest.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
xmlHttpRequest.setRequestHeader("Content-Length", xml.length);
xmlHttpRequest.send(xml);

// retrieve response and find attribute value
var result = xmlHttpRequest.responseXML.selectSingleNode("//q1:" + sAttributeName);
if (result == null)
return "";
else
return result.text;
}

30 September 2008

Customize the crmForm

There are some useful crmForm customization skills which I want to share:

1. Change a filed label style

/* change new_button field label style */
if (crmForm.all.new_button != null)
{
var field = crmForm.all.new_button_c;
field.style.fontWeight = 'bold'; // change font to bold
field.style.fontSize = '12px'; // change font size
field.style.color = '#ff0000'; //change font color
}

2. Replace a field to a button, and attach the onclick() event

/* replace new_button_d to a button */
if (crmForm.all.new_button != null)
{
var field = crmForm.all.new_button_d;
var html = "<table border='0' cellspacing='0' cellpadding='0'><tr><img width='32' height='32' style='cursor:hand' src='/_imgs/ico_32_134.gif' alt='Click button' onclick='Button_OnClick()' /></tr></table>";
field.innerHTML = html;
}
Button_OnClick = function()
{
alert("button clicked!");
}

3. Replace a field to a lable (use replaceNode())

/* replace new_button_d to a label */
if (crmForm.all.new_button != null)
{
var html = document.createElement( "<TD id='new_button_d'>");
html.innerText = "this is a lable";
crmForm.all.new_button_d.replaceNode(buttonText);
}

4. Append text under a field (you don't need to create an attribute for that)

/* append text under new_button */
if(crmForm.all.new_button != null)
{
var html= document.createElement( "<LABEL>");
html.innerText = "this is a text field";
crmForm.all.new_button.parentNode.appendChild(html);
}

18 September 2008

Double Click EmailAddress to Open in Outlook

There is a question on Microsoft CRM Forum asking how to: Double Click EmailAddress to Open in Outlook

I think it's worth to bring it here because it's also demonstrate how to send parameters in attachEvent method, so here is the answer:


/* Double Click EmailAddress to Open in Outlook, put into entity.OnLoad */
function CreateEmail(emailAddress)
{
return function()
{
if (emailAddress != null && emailAddress.value.length > 0)
{
window.navigate("mailto:" + emailAddress.value);
}
}
}

crmForm.all.emailaddress1.attachEvent('ondblclick', CreateEmail(crmForm.all.emailaddress1));
crmForm.all.emailaddress2.attachEvent('ondblclick', CreateEmail(crmForm.all.emailaddress2));
crmForm.all.emailaddress3.attachEvent('ondblclick', CreateEmail(crmForm.all.emailaddress3));

02 July 2008

CRM 4.0: Use JavaScript execute/call/launch CRM Workflow

I have a question from my colleague: How to use JavaScript execute workflow in CRM 4.0? The question also repeats very often in CRM Forums, no answer so far.

In CRM 3.0, Mitch Milam has described how to Launching a Workflow Rule from JavaScript, it works great. However, in CRM 4.0, the class: ExecuteWFProcessRequest has been deprecated, so it won’t work in CRM 4.0. Although there are many ways to launch a workflow, if you want to run it through JavaScript, here’s the trick:

/* the function */
ExecuteWorkflow = function(entityId, workflowId)
{
var xml = "" +
"<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">" +
GenerateAuthenticationHeader() +
" <soap:Body>" +
" <Execute xmlns=\"http://schemas.microsoft.com/crm/2007/WebServices\">" +
" <Request xsi:type=\"ExecuteWorkflowRequest\">" +
" <EntityId>" + entityId + "</EntityId>" +
" <WorkflowId>" + workflowId + "</WorkflowId>" +
" </Request>" +
" </Execute>" +
" </soap:Body>" +
"</soap:Envelope>" +
"";

var xmlHttpRequest = new ActiveXObject("Msxml2.XMLHTTP");
xmlHttpRequest.Open("POST", "/mscrmservices/2007/CrmService.asmx", false);
xmlHttpRequest.setRequestHeader("SOAPAction","http://schemas.microsoft.com/crm/2007/WebServices/Execute");
xmlHttpRequest.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
xmlHttpRequest.setRequestHeader("Content-Length", xml.length);
xmlHttpRequest.send(xml);
var resultXml = xmlHttpRequest.responseXML;
return(resultXml.xml);
}

/* call */
var theWorkflowId = "3FD2DD58-4708-43D7-A21B-F0F90A0AA9F2"; //change to your workflow Id
ExecuteWorkflow(crmForm.ObjectId, theWorkflowId);

Enjoy coding! :)

18 May 2008

The mysterious CRM Lookup (III)

5. Let's have a look another common used filtered lookup example: I want the regarding field is set to open cases.

a. CRM 3.0

/* set the regarding to open case : Form.onLoad() */
crmForm.all.regardingobjectid.lookuptypes = "112";
crmForm.all.regardingobjectid.lookuptypeIcons = "/_imgs/ico_16_112.gif";

/* only show the active cases : Form.onLoad() */
if (crmForm.ObjectId != null)
{
crmForm.all.regardingobjectid.lookupbrowse = 1;
crmForm.all.regardingobjectid.additionalparams = "fetchXml="
+ "<fetch mapping='logical'><entity name='incident'><all-attributes /><filter>"
+ "<condition attribute='statecode' operator='eq' value='0' />"
+ "</filter></entity></fetch>";
}


b. CRM 4.0

In 4.0, the above feature is not supported anymore which means not working any more, but we can still use the method which mentioned in my previous post. Set 'statecode' as a find column of the Case Lookup View, then add the following code to the entity's onLoad():

/* set the regarding to open case : Form.onLoad() */
crmForm.all.regardingobjectid.lookuptypes = "112";
crmForm.all.regardingobjectid.lookuptypeIcons = "/_imgs/ico_16_112.gif";
crmForm.all.regardingobjectid.additionalparams = 'search=Active';


All good, now we are changing the requirement to: I want the regarding field is set to open cases which owned by the current user!

Ok, this simple and clean approach will not work for this a little complicated search. We need to build up a fetch query first. It's easy to use advanced find:

Look for: cases
-Status Equals Active
-Owner Equals Current User'

And then click Find, we get results.

Now, how can we get benefit from this Advanced Find and get the FetchXml statements from the query? Thanks Ronald Lemmen who first find a trick, in the result page, paste it in the IE Address Bar:

javascript:prompt("", resultRender.FetchXml.value);

Then you will get a prompt window, the value is the FetchXml statements which we need.

<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false"><entity name="incident"><attribute name="title"/><attribute name="ticketnumber"/><attribute name="createdon"/><attribute name="incidentid"/><order attribute="title" descending="false"/><filter type="and"><condition attribute="statecode" operator="eq" value="0"/><condition attribute="ownerid" operator="eq-userid"/></filter></entity></fetch>

Thanks Adi Katz who found a brilliant way to do it, and George modified it slightly. Basicly it overwrites the code-behind function, which I think it could be a risk. However so far so good after three months since it has been first released, I will post updates if it occurs any issue.(There is a supported filtered lookup product from Michael Höhne which is not free but great product.)

You need to modify lookupsingle.aspx file in the path \CRMWeb\_controls\lookup\lookupsingle.aspx
Add the following codes:
 
<script runat="server">
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
crmGrid.PreRender += new EventHandler(crmGrid_PreRender);
}
void crmGrid_PreRender(object sender, EventArgs e)
{
if (crmGrid.Parameters["search"] != null && crmGrid.Parameters["search"].StartsWith("<fetch"))
{
crmGrid.Parameters.Add("fetchxml", crmGrid.Parameters["search"]);
crmGrid.Parameters.Remove("searchvalue");
this._showNewButton = false;
}
}
</script>


And then you can filter it from entity's onLoad() just like what we did in CRM 3.0. Notice that we have pasted the FetchXml statements below(fetchStr).

/* set the regarding to open case which owned by current user : Form.onLoad() */
crmForm.all.regardingobjectid.lookuptypes = "112";
crmForm.all.regardingobjectid.lookuptypeIcons = "/_imgs/ico_16_112.gif";

var fetchStr = "<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false"><entity name="incident"><attribute name="title"/><attribute name="ticketnumber"/><attribute name="createdon"/><attribute name="incidentid"/><order attribute="title" descending="false"/><filter type="and"><condition attribute="statecode" operator="eq" value="0"/><condition attribute="ownerid" operator="eq-userid"/></filter></entity></fetch>";
crmForm.all.regardingobjectid.lookupbrowse = 1;
crmForm.all.regardingobjectid.additionalparams = "search=" + fetchStr;


That's it, now we reach the end. :)

11 May 2008

The mysterious CRM Lookup (II)

4. Now, how can we set/filter the lookup content? Because CRM3 and CRM4 are so different in lookup, so the solution is different. Let’s see a common example: In the Account record, only show the account owned contacts in the Primary Contact (primarycontactid) lookup.

a. CRM 3.0


/* CRM 3.0: only show account owned contacts in the primarycontactid lookup : Form.onLoad() */
if (crmForm.FormType == 2 && crmForm.ObjectId != null)
{
crmForm.all.primarycontactid.lookupbrowse = 1;
crmForm.all.primarycontactid.additionalparams = "fetchXml="
+ "<fetch mapping='logical'><entity name='contact'><all-attributes /><filter>"
+ "<condition attribute='accountid' operator='eq' value='" + crmForm.ObjectId + "' />"
+ "</filter></entity></fetch>";
}


b. CRM 4.0

As far as I know, there are two unsupported ways to do that. Because we don't need a complicated fetchxml in this case, we so could use the first approach:

1. Customize Contact entity, open Contacts Lookup View, click 'Add Find Column', add the Parent Customer (parentcustomerid), save and publish it.
2. Customize Account entity, put the following code into Form.onLoad() :


/* CRM 4.0: only show account owned contacts in the primarycontactid lookup : Form.onLoad() */
if (crmForm.FormType == 2 && crmForm.ObjectId != null)
{
var name = crmForm.all.name.DataValue;
crmForm.all.primarycontactid.additionalparams = 'search=' + name;
}

It's a nice approach which used the 'search' feature of the CRM lookup. When this parameter is specified it defaults the search string in the lookup dialog and applies the search when the dialog is opened.


It's great, now let's change the requirement:
How about: Only show the Parent Account (parentaccountid) owned contacts in the Primary Contact (primarycontactid) lookup.

We still need to repeat step (1), and then in the step (2):


/* CRM 4.0: only show parent account owned contacts in the primarycontactid lookup: Form.onLoad() */
FilterLookup = function(source, target)
{
if (IsNull(source) IsNull(target)) { return; }
var name = IsNull(source.DataValue) ? '' : source.DataValue[0].name;
target.additionalparams = 'search=' + name;
}

Also, we need to put the following code into parentaccountid.onChange():


/* CRM 4.0: only show parent account owned contacts in the primarycontactid lookup : parentaccountid.onChange() */
FilterLookup(crmForm.all.parentaccountid, crmForm.all.primarycontactid);


It's great too, now how about we add another requirement on the above example:
The Primary Contact (primarycontactid) should be automatically selected when this contact is the primary contact of the selected Parent Account (parentaccountid).

Although we could do it through AJAX, we can also do it through CRM 4.0 lookup field automatic resolutions technique. Thanks for Adi Katz, let's start from begin:

(1) Turn off the Parent Account (parentaccountid) "automatic resolutions in field" feature by double click the field on the Form.
(2) Put the following codes in Account.onLoad():


function OnAfterAccountSelect()
{
var contactLookup = crmForm.all.primarycontactid;
if( contactLookup.DataValue != null ) {return;}

contactLookup.AutoResolve = 1;
var accountLookup = crmForm.all.parentaccountid;
primaryContact = accountLookup.items[0].keyValues.primarycontactid;

contactLookup.SetFocus();
contactDiv = document.all.primarycontactid_d.getElementsByTagName("DIV")[0];
contactDiv.innerText = primaryContact.value;
contactLookup.Lookup( true , true , primaryContact.value , true );
}

function OnCrmPageLoad()
{
crmForm.all.parentaccountid.attachEvent( "onafterselect" , OnAfterAccountSelect );
}

OnCrmPageLoad();


The second approach which can deal with some complicated conditions will be introduced in the next post. :)

04 May 2008

The mysterious CRM Lookup (I)

1. As many of you already know that the CRM lookup field saves the GUID of the related entity. Let's have a deep view of the lookup field. When you create a relationship between two entities, one entity can reference the other entity through a lookup field. However it's not just create one attribute in the database, it means although you can only see one relationship created in CRM interface, there are some invisible attributes for internal/customizer use. Let's see an example, we know that we can reference some values from a lookup fields:


crmForm.all.regardingobjectid.DataValue[0].id; // The GUID of the lookup.
crmForm.all.regardingobjectid.DataValue[0].name; // The text value of the lookup.
crmForm.all.regardingobjectid.DataValue[0].typename; // The entity type name.


But how CRM get those values? Actually when user open a CRM record, those attributes are downloaded from CRM database to the 'containers' which can be seen from entity's customization.xml, it's a complex structure which I don't want to explain in this post. So when you create a relationship between two entities, the CRM system will create more than 2 'containers'(attributes) in the database to keep other information about the lookup field(e.g. id, typename etc).

2. Set the default value for a lookup field, let's take a look at the special lookup field again: regardingobjectid
In many cases, the regardingobjectid is default to Account, but how can we change the default value to Contact?

In the onLoad() event, you can set the default attributes by using setAttribute method which is unsupported by Microsoft, however it's a standard XML DOM method.

Let's see some examples:


crmForm.all.regardingobjectid.setAttribute("lookuptypes", "1,2"); //only show account and contact
crmForm.all.regardingobjectid.setAttribute("lookuptypeIcons", "/_imgs/ico_16_1.gif :/_imgs/ico_16_2.gif"); //set the icons
crmForm.all.regardingobjectid.setAttribute("defaulttype", "2"); //default to contact

Instead of using setAttribute method, you can also use CRM method directly:

crmForm.all.regardingobjectid.lookuptypes = "1,2";
crmForm.all.regardingobjectid.lookuptypeIcons = "/_imgs/ico_16_1.gif:/_imgs/ico_16_2.gif";
crmForm.all.regardingobjectid.defaulttype = "2";



3. If you have a look of the URL when you open a lookup window, you may see how CRM calls a lookup:



/lookupsingle.aspx?class=ActivityRegarding&objecttypes=1,2,3,4&browse=0&ShowNewButton=1&ShowPropButton=1&DefaultType=0

lookupsingle.aspx has some parameters which can be referenced by our developers, those parameters are:

Objecttypes : Entity code, e.g. Objecttypes = "1, 2" //show account and contact
DefaultType : the default lookup entity, e.g. DefaultType = "2" //default to contact
Browse : bool, 0 = show the "Look for" bar; 1 = browse model, hide the "Look for " bar.
ShowNewButton : bool, 0 = hide the "New" button; 1 = show the "New" button.
ShowPropButton : bool, 0 = hide the "Properties" button; 1 = show the "Properties" button.

In an IFRAME or a ISV solution, if you don't want users to see the 'New' Button, you can just set the URL to:
/lookupsingle.aspx?class=ActivityRegarding&objecttypes=1,2,3,4&browse=0&ShowNewButton=0&ShowPropButton=1&DefaultType=0

It's cool so far! But how about if want to hide the 'New' button in CRM?
You can't just say: crmForm.all.regardingobjectid.ShowNewButton = 0; it doesn't work. But what you can do is in the onLoad() event, use attachEvent method to attach a setadditionalparams event for the regardingobjectid. Again, those methods are all unsupported customisations, however, those are widely used in the Web development.

/*
Function: show/hide the 'New' button of lookup
bShow = 0 : hide the New Button
bShow = 1 : show the New Buton
*/
function NewButton(bShow)
{
return function()
{
crmForm.all.regardingobjectid.AddParam("ShowNewButton", bShow);
}
}
crmForm.all.regardingobjectid.attachEvent("setadditionalparams",NewButton(0));

08 April 2008

How to format a number field(integer) without showing commas?

There is an interesting topic on the CRM Forum about how to format an integer field without commas. For example, if you type: 123456 in an integer field, you may see 123,456 once it loses focus. CRM automatically add a ',' between 3 numbers. There's a global setting to get rid of it, however, it will get rid of all integer field format. How about if you just want to remove the format for just one field?

As you may know, MSCRM uses htc files to format the different type of input fields as same as email address etc, see my another post.

If you want to get the value of an attribute, in CRM we use: DataValue, e.g: crmForm.all.new_number.DataValue;
Notice that the DataValue is the real data saved in the database.

However, if you want to get the formatted value, you may use this:
crmForm.all.new_number.value;

So the trick is give the DataValue overwrites the value property.

Put the following code into the entity's onLoad() event, and the same code puts into the field's onChange() event.

if(crmForm.all.new_number != null && crmForm.all.new_number.DataValue != null)
{
crmForm.all.new_number.value = crmForm.all.new_number.DataValue;
}

Enjoy it!

05 April 2008

Add client-side Microsoft Office Word Spell Checker in CRM Email entity


There are many CRM spell checker add-ons on the Internet, most of those are free, but they are all server-side spell checkers.

Today let's introduce the new client-side Word Spell Checker which uses Microsoft Office Word spell check function. It does give users a familiar feeling and larger database. It also uses Word's spell check settings. So I think users will love it. It has been used for one of our clients live environment for half year, and it has great feedbacks.

As you can see, the client machine must have Microsoft Word installed for using this technique.(My clients are using Microsoft Office Word 2003, I'm not sure if it's compatible with Word 2007, please feel free to test this solution)

The file needs to be modified is: CRMWeb\Activities\email\edit.aspx

You may already recognized that it's an unsupported customization, so please make backups just in case your customization could damage the system and also could be overwrite by Hotfixs/Rollups.

Just simply add a function:


/*  Microsoft Office Word Spelling Check*/
 
function SpellCheck(field)
{
window.frames[field].document.execCommand("Copy");
 textRange = window.frames[field].document.body.createTextRange();
 textRange.execCommand("Copy");
 
 try
 {
   var oWord = new ActiveXObject("Word.Application");
   oWord.Visible = false;
   oWord.Documents.Add();
   oWord.Top = -2000;
   oWord.Selection.Paste();
   oWord.ActiveDocument.CheckSpelling();
   oWord.Selection.WholeStory();
   oWord.Selection.Copy();
   oWord.ActiveDocument.Close(0);
   window.frames[field].focus();
   window.frames[field].document.execCommand("SelectAll");
   window.frames[field].document.execCommand("Paste");
 }
 catch(err)
 {
   alert("Error loading Microsoft Word Spelling Check: " + err);
 }
 finally
 {
   oWord.Quit(0);
 }
 
 alert("Spelling Check Finished!");
 
}

You also need to modify the isv.config.xml fie to call this function:


<Entity name="email">
<ToolBar ValidForCreate="1" ValidForUpdate="1">
<Button Title="Spell Check" ToolTip="Spell Check" Icon="/_imgs/ico_18_home.gif" JavaScript="SpellCheck('descriptionIFrame');" />
</ToolBar>
</Entity>

What the function does is copy the text user typed in the Email body(descriptionIFrame), and paste it to a Word document, the document is unseenable because it has been moved out of the screen(oWord.Top = -2000) , then the function call Word.CheckSpelling() method to check the text just pasted. After correct all words, it will paste the whole text back to the Email body, and close the Word process.

23 February 2008

Show how many activities/history associated with a record


Sometimes we want to see how many activities/history associated with a record, it's nice to have a number just like the Outlook Inbox.

Here's the code, it works fine on both CRM 3.0 and 4.0 (slightly different). Please notice that you have to reload the record to get the current number after you add/close an activity.


var buXml = GetRegardingActivity();

if(buXml != null)
{
var buNodes = buXml.selectNodes("//BusinessEntity/statecode"); // CRM 3.0

//var buNodes = buXml.selectNodes("//BusinessEntity/q1:statecode"); // CRM 4.0
var iActivity = 0;
var iHistory = 0;

if(buNodes != null )
{
/*get values*/
for( i = 0; i < buNodes.length; i++)
{
switch(buNodes[i].text)
{
case "Open" : iActivity++; break;
case "Scheduled" : iActivity++; break;
case "Completed" : iHistory++; break;
case "Canceled" : iHistory++; break;
}
}

if(document.getElementById('navActivities') != null)
{
document.getElementById('navActivities').getElementsByTagName('NOBR')[0].innerText = document.getElementById('navActivities').getElementsByTagName('NOBR')[0].innerText + " (" + iActivity + ")";
}

if(document.getElementById('navActivityHistory') != null)
{
document.getElementById('navActivityHistory').getElementsByTagName('NOBR')[0].innerText = document.getElementById('navActivityHistory').getElementsByTagName('NOBR')[0].innerText + " (" + iHistory + ")";
}
}
}

function GetRegardingActivity()
{
var xml = "" +
"<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">" +
" <soap:Body>" +
" <query xmlns:q1=\"http://schemas.microsoft.com/crm/2006/Query\" xsi:type=\"q1:QueryExpression\" xmlns=\"http://schemas.microsoft.com/crm/2006/WebServices\">" +
" <q1:EntityName>activitypointer</q1:EntityName>" +
" <q1:ColumnSet xsi:type=\"q1:ColumnSet\">" +
" <q1:Attributes>" +
" <q1:Attribute>statecode</q1:Attribute>" +
" </q1:Attributes>" +
" </q1:ColumnSet>" +
" <q1:Distinct>false</q1:Distinct>" +
" <q1:Criteria>" +
" <q1:FilterOperator>And</q1:FilterOperator>" +
" <q1:Conditions>" +
" <q1:Condition>" +
" <q1:AttributeName>regardingobjectid</q1:AttributeName>" +
" <q1:Operator>Equal</q1:Operator>" +
" <q1:Values>" +
" <q1:Value xsi:type=\"xsd:string\">" + crmForm.ObjectId + "</q1:Value>" +
" </q1:Values>" +
" </q1:Condition>" +
" </q1:Conditions>" +
" </q1:Criteria>" +
" </query>" +
" </soap:Body>" +
"</soap:Envelope>" +
"";

var xmlHttpRequest = new ActiveXObject("Msxml2.XMLHTTP");
xmlHttpRequest.Open("POST", "/mscrmservices/2006/CrmService.asmx", false);
xmlHttpRequest.setRequestHeader("SOAPAction","http://schemas.microsoft.com/crm/2006/WebServices/RetrieveMultiple");
xmlHttpRequest.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
xmlHttpRequest.setRequestHeader("Content-Length", xml.length);
xmlHttpRequest.send(xml);

var resultXml = xmlHttpRequest.responseXML;
return resultXml;
}

26 January 2008

CRM 4.0 : get UserId, BusinessUnitId, OrganizationId on client-side JScript (WhoAmIRequest)

I'm sure it's in SDK, but to be clear I made it as a JScript function, so you can easly get UserId, BusinessUnitId and OrganisationId from client-side WhoAmIRequest, it's a good example.


function GetCurrentUserInfo()
{
var SERVER_URL = "http://CRM";
var xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
xmlhttp.open("POST", SERVER_URL + "/mscrmservices/2007/crmservice.asmx", false);
xmlhttp.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
xmlhttp.setRequestHeader("SOAPAction", "http://schemas.microsoft.com/crm/2007/WebServices/Execute");

var soapBody = "<soap:Body>"+
"<Execute xmlns='http://schemas.microsoft.com/crm/2007/WebServices'>"+
"<Request xsi:type='WhoAmIRequest' />"+
"</Execute></soap:Body>";

var soapXml = "<soap:Envelope " +
"xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/' "+
"xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' "+
"xmlns:xsd='http://www.w3.org/2001/XMLSchema'>";

soapXml += GenerateAuthenticationHeader();
soapXml += soapBody;
soapXml += "</soap:Envelope>";

xmlhttp.send(soapXml);
xmlDoc=new ActiveXObject("Microsoft.XMLDOM");
xmlDoc.async=false;
xmlDoc.loadXML(xmlhttp.responseXML.xml);

var userid = xmlDoc.getElementsByTagName("UserId")[0].childNodes[0].nodeValue;
var buid = xmlDoc.getElementsByTagName("BusinessUnitId")[0].childNodes[0].nodeValue;
var orgid = xmlDoc.getElementsByTagName("OrganizationId")[0].childNodes[0].nodeValue;

alert("UserId: " + userid + "\r\nBusinessUnitId: " + buid + "\r\nOrganizationId: " + orgid);

}

19 January 2008

CRM 4.0 : Check current user's security role using JavaScript

It's a common question about how to show/hide fields based on user's security roles.
Ronald Lemmen had a very popular post on his blog about how to use 'RemoteCommand' to achieve that in CRM 3.0. Because 'RemoteCommand' is for internal use and unsupported, it doesn't work in CRM 4.0.
Michael H?hne also had a great post about how to access web service using client-side JavaScript. Since CRM 4.0 web service EndPoint changed, some people get 401 authorization error.

Here's code which works great in CRM 4.0, the function UserHasRole("ROLE_NAME") returns true if the current has the role, returns false if it doesn't. GetCurrentUserRoles() function generated by Michael H?hne's tool with some changes. Thanks for Regan who point out that using GenerateAuthenticationHeader() instead of hard-coding the Organization's Name.



//check if the current user has the 'System Administrator' role
alert(UserHasRole("System Administrator"));

function UserHasRole(roleName)
{
//get Current User Roles, oXml is an object
var oXml = GetCurrentUserRoles();
if(oXml != null)
{
//select the node text
var roles = oXml.selectNodes("//BusinessEntity/q1:name");
if(roles != null)
{
for( i = 0; i < roles.length; i++)
{
if(roles[i].text == roleName)
{
//return true if user has this role
return true;
}
}
}
}
//otherwise return false
return false;
}

function GetCurrentUserRoles()
{
var xml = "" +
"<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">" +
GenerateAuthenticationHeader() +
" <soap:Body>" +
" <RetrieveMultiple xmlns=\"http://schemas.microsoft.com/crm/2007/WebServices\">" +
" <query xmlns:q1=\"http://schemas.microsoft.com/crm/2006/Query\" xsi:type=\"q1:QueryExpression\">" +
" <q1:EntityName>role</q1:EntityName>" +
" <q1:ColumnSet xsi:type=\"q1:ColumnSet\">" +
" <q1:Attributes>" +
" <q1:Attribute>name</q1:Attribute>" +
" </q1:Attributes>" +
" </q1:ColumnSet>" +
" <q1:Distinct>false</q1:Distinct>" +
" <q1:LinkEntities>" +
" <q1:LinkEntity>" +
" <q1:LinkFromAttributeName>roleid</q1:LinkFromAttributeName>" +
" <q1:LinkFromEntityName>role</q1:LinkFromEntityName>" +
" <q1:LinkToEntityName>systemuserroles</q1:LinkToEntityName>" +
" <q1:LinkToAttributeName>roleid</q1:LinkToAttributeName>" +
" <q1:JoinOperator>Inner</q1:JoinOperator>" +
" <q1:LinkEntities>" +
" <q1:LinkEntity>" +
" <q1:LinkFromAttributeName>systemuserid</q1:LinkFromAttributeName>" +
" <q1:LinkFromEntityName>systemuserroles</q1:LinkFromEntityName>" +
" <q1:LinkToEntityName>systemuser</q1:LinkToEntityName>" +
" <q1:LinkToAttributeName>systemuserid</q1:LinkToAttributeName>" +
" <q1:JoinOperator>Inner</q1:JoinOperator>" +
" <q1:LinkCriteria>" +
" <q1:FilterOperator>And</q1:FilterOperator>" +
" <q1:Conditions>" +
" <q1:Condition>" +
" <q1:AttributeName>systemuserid</q1:AttributeName>" +
" <q1:Operator>EqualUserId</q1:Operator>" +
" </q1:Condition>" +
" </q1:Conditions>" +
" </q1:LinkCriteria>" +
" </q1:LinkEntity>" +
" </q1:LinkEntities>" +
" </q1:LinkEntity>" +
" </q1:LinkEntities>" +
" </query>" +
" </RetrieveMultiple>" +
" </soap:Body>" +
"</soap:Envelope>" +
"";

var xmlHttpRequest = new ActiveXObject("Msxml2.XMLHTTP");

xmlHttpRequest.Open("POST", "/mscrmservices/2007/CrmService.asmx", false);
xmlHttpRequest.setRequestHeader("SOAPAction"," http://schemas.microsoft.com/crm/2007/WebServices/RetrieveMultiple");
xmlHttpRequest.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
xmlHttpRequest.setRequestHeader("Content-Length", xml.length);
xmlHttpRequest.send(xml);

var resultXml = xmlHttpRequest.responseXML;
return(resultXml);
}



Due to Bloger's format issue, I have rewrite the code, it should work for everyone now.:)

12 January 2008

Dynamic Picklist, load values from a XML file



There are some dynamicpicklist examples on the Internet. You may find it from CRM SDK as well, and there's A (slightly) different approach to dynamic picklists from Greg Owens.

But none of those can meet my client's requirement. We have 2 picklists(which is normal), but picklist2 has hundreds of items. And those picklists repeat 3 times on all activites. Furthermore, they don't want to use another entity. So I create a XML file to keep the data, and using it dynamicly fill out picklist2 when picklist1 has value selected. It works great, and you only need to maintain one XML file.

This is a very simple XML to do the demonstration, put it ('section.xml') into the root folder of CRMWeb. The idea is: when user selects type1 in picklist1, then picklist2 only shows item1, item2, item3; when user selects type2 in picklist1,the picklist2 only shows item4, item5, item6.


<?xml version="1.0" encoding="utf-8" ?>
<Section>
<type1>
<item>item1</item>
<item>item2</item>
<item>item3</item>
</type1>
<type2>
<item>item4</item>
<item>item5</item>
<item>item6</item>
</type2>
</Section>


Suppose there are two picklists on crmForm, picklist2's value depends on picklist1's selection.
In my example:
picklist1 is: new_type1, it has two values: 'typeA/typeB' and 'typeC/typeD'
picklist2 is: new_section1, it doesn't have any value, and I add another nvarchar attribute: new_section1Text to save it's value(see example)



/*
Form.onLoad() event
GetItems() is a global function to get section's list items based on type's selection
typeValue: picklist1's DataValue
section: picklist2(object)
sectionText: picklist2.SelectedText
*/

GetItems = function(typeValue, section, sectionText)
{
//clean the section object
section.length = 0;

//it is the index of picklist2.SelectedText in XML file
var sectionTextIndex = 0;

//get the typeName, used for XML node
var typeName = 0;

switch(typeValue)
{
case "1" : typeName = "type1"; break;
case "2" : typeName = "type2"; break;
case "0" : return;
}

//load XML file
var xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
xmlDoc.async = false;
xmlDoc.load("/section.xml");

//get all items under this type
xmlDoc = xmlDoc.getElementsByTagName(typeName)[0];
var items = xmlDoc.getElementsByTagName('item');

//insert all items into section object
for(var i=0; i<items.length; i++)
{
section.AddOption(items(i).firstChild.nodeValue, i+1);
if((sectionText != null)&&(sectionText.DataValue == items(i).firstChild.nodeValue))
{
sectionTextIndex = i+1;
}
}

return sectionTextIndex;
}

GetPicklist(crmForm.all.new_type1, crmForm.all.new_section1, crmForm.all.new_section1text);

function GetPicklist(type, section, sectionText)
{
if(sectionText.DataValue != null)
{
//select the right one
section.DataValue = GetItems(type.DataValue, section, sectionText);
}
}

/*
Form.onSave() event, save the current new_section1's selectedText
Becasue we need to add this option to new_section1 on Form.onLoad()
*/

SetPicklist(crmForm.all.new_section1 , crmForm.all.new_section1text);

function SetPicklist(section, sectionText)
{
if(section.SelectedText != "")
{
sectionText.DataValue = section.SelectedText;
section.length = 0;
}
}

/*
new_type1.onChange() event to call GetItem() funciton, passing new_type1.DataValue and new_section1, sectionText as parameters
*/
if(crmForm.all.new_type1.DataValue != null)
{
GetItems(crmForm.all.new_type1.DataValue, crmForm.all.new_section1, null);
}
else
{
crmForm.all.new_section1.options.length = 0;
}

05 December 2007

How to setting focus to a Tab?

There are many discusses in CRM Forum, about how to setting focus to a Tab?
For example, I want the default tab1Tab to be the default tab when I open a record.

So if you put this code in onLoad() event:

document.getElementById('tab1Tab').focus();

It doesn't work as you expected.

But, if you use:

crmForm.all.tab1Tab.click();

It will work like a dream. :)

20 November 2007

Hide/remove/move/change entity level tags at runtime


/* Removing Sub-Account navigation bar at runtime */
if(document.all.navSubAct != null)
{
navSubAct.style.display = 'none';
}

/* Hiding Service Tag at runtime */
if(document.all._NA_CS != null)
{
document.all._NA_CS.style.display = 'none';
}

/* Moving Case under details group at runtime */
if((document.all.navService != null) && (document.all._NA_Info != null))
{
document.all._NA_Info.appendChild(navService);
}

/* Changing group name from 'Sales' to 'Management' */
if (document.all._NA_SFA != null)
{
document.getElementById("_NA_SFA").innerHTML = document.getElementById("_NA_SFA").innerHTML.replace("Sales","Management");
}

11 November 2007

CRM how to hide field / label / line / section / tab


/*hide field only*/
crmForm.all.field.style.display = 'none';

/*hide field and this field's label*/
crmForm.all.field.style.display = 'none';
crmForm.all.field_c.style.display = 'none';

/*hide field and the whole line which contains this field*/
crmForm.all.field.parentElement.parentElement.style.display = 'none';

/*hide field and the section which contains this field*/
crmForm.all.field.parentElement.parentElement.parentElement.style.display = 'none';

/*hide a tab(tab number comes from 0)*/
crmForm.all.tab2Tab.style.display = 'none';

08 November 2007

How to disable / readOnly CRM fields / iframes

Sometimes we need to disable all fields in CRM, so a function could help!
Also, you don't want to diable INPUT/TEXTAREA nodes, because it will become unreadble (gray #808080) if you disable it, so I prefer to set those nodes readOnly. (only INPUT and TEXTAREA support readOnly property!)


/*
setDisabled function
1. set readOnly property for INPUT/TEXTAREA nodes
2. disable other nodes
id: element's Id
ignoreNodes: nodes can be ignored
nodesDisabled: bool, true = set disable/readonly
*/

function setDisabled(id, ignoreNodes, nodesDisabled)
{
var node, nodes;
nodes = id.getElementsByTagName('*');
if(!nodes)
return;

var i = nodes.length;
while (i--)
{
node = nodes[i];
if(node.nodeName && !(node.nodeName.toLowerCase() in ignoreNodes))
{
if((node.nodeName == "INPUT") || (node.nodeName == "TEXTAREA"))
{
node.readOnly = nodesDisabled;
}
else
{
node.disabled = nodesDisabled;
}
}
}
}

/*disable/readonly fields*/
setDisabled(document.getElementById("areaForm"), {table:'', iframe:'', div:'', form:'', col:'', colgroup:'', lable:'', span:'', tbody:'', body:'', tr:'', td:''}, true);

/*disable IFRAME*/
try
{
window.setTimeout(iframeDisabled, 3000);
}
catch(err)
{
alert("System busy, please try again later!" + err);
window.close();
}

function iframeDisabled()
{
setDisabled(document.frames("IFRAME_1").document.getElementById("mnuBar1"),{}, true);
}

20 October 2007

Automatically resolve e-mail sender to selected contact emailaddress1. Part I

We faced two problems in CRM 3.0 Email, the first problem was:
When an email(senderA@example.com) sent from outside world into CRM system, if sender's email address isn't in the system, then CRM will ask you to associate it with an existing record(only associate with a Contact record in our case). But once you select a contact, it looks associated, but it doesn't update the Contact's email field. You have to open the Contact record and manually copy the sender's email address into Contact's email field.

Regarding Microsoft KB: 922116, it is by design! Our requirement is make it automatically copy the sender's email address into the assocaited Contact.emailaddress1 field.

To achieve that, I made an unsupported change(again? :))


/*
Automatically resolve e-mail sender to selected contact emailaddress1
Microsoft KB: 922116, http://support.microsoft.com/kb/922116
\_controls\PartyList\resolve.aspx
*/

function applychanges()
{
//update cotnact emailaddress1
if((document.getElementById("crmExistingLookup").DataValue != null)
&&(document.getElementById("crmExistingLookup").DataValue[0] != null)
&&(document.getElementById("crmExistingLookup").DataValue[0].type == 2))
{
var eml = document.getElementById("txtName").value;
var cid = document.getElementById("crmExistingLookup").DataValue[0].id;
var connection = new ActiveXObject("ADODB.Connection");
var connectionString = "Provider=SQLOLEDB; Server=_db; Database=_mscrm; Integrated Security=SSPI";

connection.Open(connectionString);
var sql = "UPDATE FilteredContact SET emailaddress1='" + eml +"'WHERE contactid = '" + cid + "'" ;
rs = new ActiveXObject("ADODB.Recordset");
rs.Open(sql, connection, 1, 2);

connection.Close();

}

....

}