Wednesday, August 27, 2008

Show / Hide Crm Form Toolbar buttons

This is another feature i really thinks ms should consider adding to crm 5.0.
I'm not talking about the code itself but the ability to provide function level security which should include hiding/displaying toolbar buttons, left navigation items, menu items and crm fields (attributes).

The snippet handles both hiding and showing of buttons and spacers.
Paste the code inside the onload event and use the ShowHideToolbarButton with the relevant parameters.



In order to support a multilingual environment you need to separate the button title values with a pipe ("|"). See the first example (save button) bellow.



var Spacer = {
Right : 1, // hides a right spacer if it exists
Left : 2, // hides a left spacer if it exists
Both : 3, // and so on...
None : 4
}

var Display = {
Show : "inline",
Hide : "none"
}

function OnCrmPageLoad()
{
//Configure Display when the form loads.
ConfigreToolbarDisplay();
//Configure the display each time a user manually changes the window width size.
attachEvent("onresize",ConfigreToolbarDisplay);
}

function ConfigreToolbarDisplay()
{
// Opportunity toolbar buttons
// English | German | Spanish
ShowHideToolbarButton( "Save|Speichern|Guardar" , Spacer.None , Display.Hide );
ShowHideToolbarButton( "Follow Up" , Spacer.Right , Display.Hide );
ShowHideToolbarButton( "Recalculate" , Spacer.Left , Display.Hide );
ShowHideToolbarButton( "Attach a File" , Spacer.Both , Display.Hide );
ShowHideToolbarButton( "Follow Up" , Spacer.Right , Display.Show );
}

function ShowHideToolbarButton( btnTitle , spacer , state )
{
if( isNullOrEmpty( document.all.mnuBar1 ) )
return;
if( isNullOrEmpty( btnTitle ) )
return;
if( isNullOrEmpty( spacer ) )
spacer = ToolbarSpacer.None;
if( isNullOrEmpty( state ) )
state = ButtonDisplay.Hide;

//Get all toolbar buttons
var toolBarButtons = document.all.mnuBar1.rows[0].cells[0].childNodes[0].childNodes;

//Loop through each button
for (var i = 0 ; i < toolBarButtons.length ; i++)
{
var button = toolBarButtons[i];
if( button.title.match(btnTitle) != null ||
button.innerText.match(btnTitle) != null)
{
button.style.display = state;
switch(spacer)
{
case Spacer.Right:
ShowHideSpacer( button.nextSibling );
break;
case Spacer.Left:
ShowHideSpacer( button.previousSibling );
break;
case Spacer.Both:
ShowHideSpacer( button.nextSibling );
ShowHideSpacer( button.previousSibling );
break;
}

return;
}
}
function ShowHideSpacer( btnSpacer ){
if( !isNullOrEmpty( btnSpacer ) )
btnSpacer.style.display = state;
}
function isNullOrEmpty( obj ){
return obj == null || typeof(obj) == "undefined" || obj == "";
}
}

OnCrmPageLoad();

Thursday, August 21, 2008

Referencing an External JS File From a CRM Form




Benefits:

1. Full VS intellisense.
2. Ability to set break points (F9). You can’t do that if your code resides in the
onload/onsave events.
3. Quick development cycle as oppose to the edit -> save -> publish routine.
4. Don't forget to paste the code back in the onload event if you want this to work offline.


/* This goes in the entity onload event */

//create a new script Element
var script2Load = document.createElement("SCRIPT");
script2Load.language = "javascript";
/*
set the src (url) Property with a random number to avoid caching
if you don't set the random number ie will cach your file and you might need to ctrl + f5 to see the changes you make while developing.
*/
script2Load.src = "/ISV/Entity/account.js?nocach=" + Math.random(); //Just an example
//append the element to the head tag (global scope)
document.getElementsByTagName("HEAD")[0].appendChild(script2Load);
//or document.documentElement.childNodes[0].appendChild(script2Load);

/* this goes in the account.js file */

function OnCrmPageLoad()
{

alert('doing stuff');
}

function CallMeFromAnywhare()
{
alert();
}

/*
expose the function to the window scope.
This is important if your going to paste the file back into the entity onload event to support offline mode or a redeployment becouse the script is loaded into the head tag (global scope) and the onload event is a third level function which makes the functions you write unavailable to external calls.
*/
window.CallMeFromAnywhare = CallMeFromAnywhare;

/* last Line */
OnCrmPageLoad();

Wednesday, August 20, 2008

Creating Collapsible Sections

This is a nice space saver, especially for "crowded" forms. Would be even nicer if ms provided this out of the box. You can set the initial state of the section in the onload event.



function OnCrmPageLoad() {
/* false - collapsed, true - expanded */
//First Tab, Second Section, Expanded
ConvertSection(0,1,true);
//Second Tab, Second Section, Collapsed
ConvertSection(1,1,false);
}

function ConvertSection( tabIndex , sectionIndex , state ) {
var sec = document.getElementById( "tab" + tabIndex );
var td = sec.childNodes[0].rows[ sectionIndex ].cells[0].childNodes[0].rows[0].cells[0];
var secHTML = td.innerHTML;
state = (typeof(state) == "undefined")? false:!state;
var src = (state == false)? "/_imgs/tree/dashPlus.gif":"/_imgs/tree/dashMinus.gif";
td.innerHTML = "<NOBR style='VERTICAL-ALIGN: middle;cursor:hand' onclick='excoSection(this)'><IMG src='" + src + "' align='middle' /> " + td.innerHTML + "</NOBR>";
td.childNodes[0].childNodes[0].state = state;
excoSection(td.childNodes[0]);
}

/* Toggle SectionState */
function excoSection( sec ) {
sec = sec.childNodes[0];
sec.state = !sec.state;
sec.src = (sec.state)? "/_imgs/tree/dashMinus.gif":"/_imgs/tree/dashPlus.gif";
var display = (sec.state)? "inline" :"none";
var tblsec = sec.parentElement.parentElement.parentElement.parentElement;
for( var i =1 ; i < tblsec.rows.length ; i++ )
tblsec.rows[i].style.display = display;
}
//Expose the toggling function at the window level
this.excoSection = excoSection;

OnCrmPageLoad();

Tuesday, August 19, 2008

Hiding Lookup Buttons

Generally speaking, Microsoft CRM has two types of buttons or families. The first family is directly related to the user security roles e.g. "New" and "Save". The other is not related to a certain supported mechanism and can not be hidden using supported means. Never the less if you have such requirement you can always use unsupported measures. Here is an example of how to hide the lookup "New" and "Properties" buttons using both supported and unsupported routes.


Supported: The New button can be disabled by reducing the create rights on the user role. The button is still shown but is grayed out and the user can not use it. The Properties button can not be hidden in any supported way.



Unsupported: Use the following code inside the crmForm onload event.


function OnCrmPageLoad()
{
var aLookup = crmForm.all.<LookupId>;
aLookup.AddParam( “ShowNewButton” , “0” );
aLookup.AddParam( “ShowPropButton” , “0” );
aLookup.showproperty = false;
}

OnCrmPageLoad();

Saturday, August 16, 2008

Controlling Data Entry

Disabling and enabling fields that depend on certain logic is a common request. This can also complicate things especially when the form is “crowded” with fields and the number of dependencies is “overwhelming”. Binding to each field separately and wrapping it with specific conditions can make quite a mess. A while back I was searching for a way to control this and came up with a simple approach I call chaining.

Each parent control is chained to a set of children (comma separated list) and is given a String “Condition” or a function pointer that makes the necessary validation checks. If the validation fails the fields are disabled. All fields enjoy a basic and automatic validation which checks that the field “Data Value” does not equal null. All other validations are specified by the user (programmer).








function OnCrmPageLoad()
{
//Chain Account Name to Account number, if the name is not ADI the disable the field
Chain( "name" , "accountnumber" , "crmForm.all.name.DataValue == 'ADI'" );
//The condition is a function pointer that is called when the change event is raised
Chain( "accountnumber", "parentaccountid,primarycontactid" , TestIfAccountNumberHas10Digits );
}

function Chain( parent , children , condition /*optional*/ )
{
//Create an array on children
children = children.split(',');
//reference the parent control
var Parent = document.getElementById( parent );

//if the parent onchange event is not registered, this prevents multiple binds to the same control
if(!Parent.Registered)
{
Parent.attachEvent( "onchange" , OnChainReaction );
Parent.Registered = true;
}

//Each parent holds an array of its child controls
Parent.Sons = (Parent.Sons)?Parent.Sons:[];
//Save the condition on the parent control (using expando)
Parent.Condition = condition;

for( var i = 0 ; i < children.length ; i++ )
{
var Child = Parent.Sons[ i ] = document.getElementById( children[i] );
Child.Disabled = Parent.DataValue == null;
if(!Child.Registered)
{
Child.attachEvent( "onchange" , OnChainReaction );
Child.Registered = true;
}

//Each child holds a reference to its parent. this can be used in code if needed
Child.Parents = (Child.Parents)?Child.Parents:[];
Child.Parents[ i ] = parent;
}


//Start Validating the control
Parent.FireOnChange();
}

function OnChainReaction()
{
//Current field
var Control = event.srcElement;
if( !Control.Sons ) return;

//Basic (internal) validation check
var enabled = Control.DataValue != null;

switch( typeof( Control.Condition ) )
{
case "string": //e.g. 'crmForm.all..DataValue == "TEST"'
enabled = eval(Control.Condition) && enabled;
break;
case "function": //e.g. MyFunction (with out the brackets)
enabled = Control.Condition() && enabled;
break;
}

//Disable or enable the children
for( var i = 0 ; i < Control.Sons.length; i++ )
Control.Sons[i].Disabled = !enabled;
}

//Our test function must return a boolean value - true,false
function TestIfAccountNumberHas10Digits()
{
var accountnumber = crmForm.all.accountnumber;
return (accountnumber.DataValue) ? (accountnumber.DataValue.length == 10):false;
}

OnCrmPageLoad();

Retrieve Lookup Information

Let’s assume that you're interested in getting the account number from the parent account lookup on the account entity. Basically, the supported way of doing that is binding to the parentaccountid onchange event, extracting the GUID ( crmForm.all.parentaccountid[0].id) or Name (crmForm.all.parentaccountid[0].name) and using that in an Ajax call to retrieve the account number. The problem with that is you need to make yet another call to the server.
There is another way, unsupported though, which uses the items array that is exposed by the lookup. Here is the complete code example:




function OnCrmPageLoad() {
crmForm.all.parentaccountid.attachEvent("onchange",OnParentAccountChange);
}

function OnPatentAccountChange() {
var parentAccount = crmForm.all.parentaccountid;
if( parentAccount.DataValue != null )
{
var lookupColumns = parentAccount.items[0].keyValues;
alert( lookupColumns.accountnumber.value);
//alert( lookupColumns.<Attribute Schema Name>.value);
}
}

OnCrmPageLoad();




The lookupColumns expose the entire column set you see on the lookup dialog by name.

Disable CRM Form

All MS CRM Controls have a common Disabled Property.
In order to disable the entire form you need to disable each and every control.


function OnCrmPageLoad()
{
ToggleFormFields(true);
}

function ToggleFormFields( disable )
{
for( var i = 0 ; i < crmForm.all.length ; i++ )
{
if( crmForm.all[ i ].req )
crmForm.all[ i ].Disabled = disable;
}
}

OnCrmPageLoad();

Thursday, August 14, 2008

Creating CRM Field Mask / Format

Yet another feature i would like to see MS provide out of the box. Quite simple to implement.
Call the Mask function providing the field id and mask (# acts as a placeholder) and you're done.
The end user only has to fill in the numbers e.g. 97356568987




function OnCrmPageLoad()
{
Mask( "telephone2" , "+(###)-(#)###-###" );
Mask( "gi_visa" , "####-####-####-####" );
Mask( "gi_scid" , "#-########-#" );
}


function Mask( fieldId , mask )
{
field = document.getElementById(fieldId);
field.mask = mask.split("");
field.regex = new RegExp(escapeRegEx(mask.replace(/#/gi,"").split("")),"gi");
field.title += " " + mask;
field.attachEvent( "onchange" , MaskOnFieldChange );
}

function escapeRegEx( chars )
{
var regChars = "+_)(*^$[]-?{}"; //Add all regexp chars id needed
var regExprs = "";
var run2Index = chars.length - 1;

for( var i = 0 ; i < run2Index ; i++ ) Concat( chars[i] , "|" );


Concat(chars[run2Index]);


function Concat( c , d ){
regExprs += (( regChars.indexOf(c) != -1 )? "\\":"" ) + c + d;
}

return regExprs;
}


function MaskOnFieldChange()
{
var field = event.srcElement;
if( field.DataValue == null ) return;

var arrDataValue = field.DataValue.replace(field.regex,"").split("");
var arrResult = [];

for(var i=0 , j=0 ; i < field.mask.length ;i++)
arrResult[i] = (field.mask[i] != "#")?field.mask[i]:arrDataValue[j++];

field.DataValue = arrResult.join("");
}

OnCrmPageLoad();

Adjust CRM Windows Position and Size

CRM Form windows have fixed width and height that cover most of the user screen. There are cases when an entity contains only a small set of fields and you want to adjust the window layout (Position and Size) in order to make it more usable. Now, this is more of a reactive approach because the form first loads in its original size and only then the script kicks in and changes that for us. So until ms adds this as an OOB (“out of box”) feature you can use the following script in your onload event.



function OnCrmPageLoad()
{
//AdjustWindow( 300 , 400 , false , 10 , 20 );
AdjustWindow( 300 , 400 , true );
}

function AdjustWindow( width , height , center , posX , posY )
{
if( center == true )
{
posX = (screen.width - width)/2;
posY = (screen.height - height)/2;
}

window.resizeTo( width , height );
window.moveTo( posX , posY );
}

OnCrmPageLoad();

Wednesday, August 13, 2008

Convert a Text Field to Label

Sometimes we need to display static text inside the crmForm. The simplest way to go about it is to create a text field, disable it and remove its borders.


function OnCrmPageLoad()
{
ConvertControlToLabel( "ControlId");
}

function ConvertControlToLabel( controlId )
{
var control = document.getElementById( controlId );
if( control )
{
control.Disabled = true;
control.style.border = "0px";
}
}

OnCrmPageLoad();

Show / Hide CRM Form Section

Here is a common requirement that pops up every now and then.


function OnCrmPageLoad()
{
//Hide the Second Section in the first Tab
ToggleSection( 0 , 1 , "none" /* "inline" */);
}

// Tabs and Section Collections are zero based
function ToggleSection( tabIndex , sectionIndex , displayType )
{
var sec = document.getElementById( "tab" + tabIndex );
sec.childNodes[0].rows[ sectionIndex ].style.display = displayType;
}

//Entry Point
OnCrmPageLoad();

Stripping HTML Tags From an Email

When converting an email to case or lead you sometimes want to transfer the email body (description field) as well. The problem is that the field usually contains HTML tags which make the data unreadable. here is a simple method you can use to strip the HTML tags.


function OnCrmPageLoad()
{
//Create a factitious DOM element
var stubDescription = document.createElement( "<SPAN style='width:1px;height:1px'>");
//This field contains the email body (HTML)
var description = crmForm.all.description;
//Assign the HTML to the factitious DOM element
stubDescription.innerHTML = description.DataValue;
//Add it to the html document
document.body.appendChild( stubDescription );
//Assign the stripped body back to the description field
description.DataValue = stubDescription.innerText;
//remove the factitious DOM element
document.body.removeChild( stubDescription );
}

//Entry Point
OnCrmPageLoad();



The code can run within the email / lead / case or any other entity for that matter.
The entity which implements this code should have a description field which holds the initial html message.

Handling CRM Tabs

The first piece of code is a helper function which enables you to reference a tab by name.

The second one re-aranges the tab order from a top - down to a left - right order which makes more sence to most users.


function OnCrmPageLoad()
{
var tab = GetTab( "Notes" );
}

/* Get tab by name */
function GetTab( name )
{
var tabs = crmTabBar.getElementsByTagName("LI");
for( var i = 0 ; i < tabs.length ; i++ )
{
if( tabs[ i ].innerText == name )
return tabs[ i ];
}
return null;
}

OnCrmPageLoad();



function OnCrmPageLoad()
{
ReArangeTabIndex();
}

function ReArangeTabIndex()
{
for( var i = 0 ; i < crmForm.all.length ; i++ )
if( crmForm.all[ i ].tabIndex )
crmForm.all[ i ].tabIndex = 1000 + (i*10);
}
}

OnCrmPageLoad();

Convert Text Field to Picklist

The Code uses a simpe trick which takes the text field id , assigns it to a new picklist field and deletes it from the form, when the crmForm is saved the picklist is recognized as a valid attribute and its data is sent to the server "for safe keeping...".


function CrmPageOnLoad() {
ConvertTextToPickList( "[ControlId]");
}

function ConvertTextToPickList( controlId ) {
//Referance the Text field
var textControl = document.getElementById( controlId );
//Create a new Picklist (SELECT) control
var picklistControl = document.createElement( "SELECT" );
//Copy the Text field properties to the picklist
picklistControl.id = textControl.id;
picklistControl.req = textControl.req;
//Set Required Style
picklistControl.className = "ms-crm-SelectBox "; //"selectBox " v3.0
AddPickListValues( picklistControl );
//load the picklist selected value from the text field (saved) datavalue
picklistControl.value = textControl.DataValue;
//append the picklist to the document
textControl.parentElement.appendChild( picklistControl );
//remove the text field , we don't need it anymore.
textControl.parentElement.removeChild( textControl );
//Done
}

function AddPickListValues( picklistControl ) {
var picklistValues = GetPickListValues();
for( var i = 0 ; i < picklistValues.length; i++ )
{
//Create a new Option
var option = document.createElement( "OPTION" );
//In this case the value and text are the same
option.value = option.innerText = picklistValues[i];
//Add the option to the picklist
picklistControl.appendChild( option );
}
}

function GetPickListValue() {
//This is just a stub , you can load the values from the server as well
return [ 'A' , 'B' , 'C' ];
}

CrmPageOnLoad();

Make IFRAME Disabled / ReadOnly


//Paste the code inside the entity onload event box

var IFRAME_Test;
var IFRAME_Test_Disable_Message = "IFRAME Disabled...";

function OnCrmPageLoad() {
//Reference the IFRAME
IFRAME_Test = document.all.IFRAME_Test;
//Bind to its ready state event (wait until the iframe is fully loaded)
IFRAME_Test.attachEvent( "onreadystatechange" , OnTestIframeReady );
}

function OnTestIframeReady() {
if( IFRAME_Test.readyState != "complete" )
return;
//Override the onmousedown event
IFRAME_Test.contentWindow.document.onmousedown = OnTestIframeMouseDown;
}

function OnTestIframeMouseDown() {
alert( IFRAME_Test_Disable_Message );
/* or use */
//the window is put beyond the user's desktop and is immediately closed
var stubWin = window.open(‘about:blank’,’’,’ toolbars=0,width=100,height=100,top=10000,left=10000’);
stubWin.close();
return false;
}

//Entry point
OnCrmPageLoad();