Monday, September 29, 2008

Embedding related record link in Email


The code below is not a replacement to the well known workflow + plug-in solution. This is a simple service function that I usually add to the email entity onload event, so when a user clicks on the send email toolbar button the opener ( related entity ) link is embedded in the email body.

Users find this extremely comfortable since even if you don’t send the email to a co-worker as a direct reference when the email comes back it very handy to be able to open the record from within outlook like a bookmark.


function OnCrmPageLoad()
{
if( crmForm.FormType == 1 && opener )
{
var opnrLoc = opener.location;
var relUrl = "http://" + opnrLoc.host + opnrLoc.pathname + opnrLoc.search;
var ifrmDoc = document.all.descriptionIFrame.contentWindow.document;

var relLink = iframeDoc.createElement( "A" );
relLink.href = url;
relLink.innerText = "Related Record";
ifrmDoc.body.appendChild( relLink );
}
}

OnCrmPageLoad();

Friday, September 26, 2008

Decorating CrmGrid Columns


This How-To Post is about decorating grid columns. Once you understand the mechanics behind the code the rest is a simple (or not) manifest of your / customer’s imagination. You should consider the ramifications of using this type of functionality since this might not upgrade to the next version.

The general idea is to override a sitemap link, contacts SubArea link in the workplace Area customers Group in this case, with a new url which contains an IFRAME to the original / Desired CrmGrid.

The JavaScript code in the new aspx page has 2 functionalities:
1. Handles the IFRAME and CrmGird loads and between Refreshes.
2. Contains the code you provide to Decorate the column cells.

So basically if you need to clone this solution for other purposes you need only change the second (2) part.

Steps to follow:
1. Export the sitemap from customization (Settings → customization → export customizations).
2. Save the exported sitemap.zip and extract its contents.
3. Open the constomization.xml file in Visual Studio or Notepad.
4. Locate the contacts (nav_conts) SubArea Node. Here is the actual sitemap fragment


<Group Id="Customers"
ResourceId="Group_Customers"
DescriptionResourceId="Customers_Description">

<SubArea Id="nav_accts"
Entity="account"
DescriptionResourceId="Account_SubArea_Description" />

<SubArea Id="nav_conts"
Entity="contact"
DescriptionResourceId="Contact_SubArea_Description"
Url="/ISV/GridVIews/Contacts.aspx" />
</Group>


5. Add Url attribute to the following url: /ISV/GridVIews/Contacts.aspx
6. Import the customization.xml file back to crm (Settings → Customization → Import Customizations)
7. Add a new aspx under the isv folder, I put it inside ISV/GridViews folder. The aspx does not need a code behind.

Note! If you need to run server side code then you must create your own IIS application to be able to compile your code.

8. Copy the following page html to the Contacts.aspx and Save.
9. In order for the sitemap changes to take effect close and reopen crm in IE.



<%@ Page Language="C#" %>

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Untitled Page</title>
<script language="javascript">

attachEvent( "onload" , OnPageLoad );
//IFrame ID
var contsGrid;
//IFrame document object
var iframeDoc;
//Views picklist
var SavedQuerySelector;
//The grid object
var crmGrid;

/*
Load the contacts grid view,
You may change the IFRAME url to suit your needs
*/
function OnPageLoad()
{
contsGrid = document.all.contsGrid;
var contsUrl = "/" + top.ORG_UNIQUE_NAME + "/_root/homepage.aspx?etc=2";
contsGrid.src = contsUrl;
contsGrid.onreadystatechange = OnGridViewReady;
}

/*
When the IFRAME is ready then:
Attach to the grid refresh and selector change events.
Since the grid is already loaded call it for the first time.
*/
function OnGridViewReady()
{
if( contsGrid.readyState != "complete" )
return;
iframeDoc = contsGrid.contentWindow.document;

//make sure the selector exists
SavedQuerySelector = iframeDoc.all.SavedQuerySelector;
if( SavedQuerySelector )
iframeDoc.all.SavedQuerySelector.attachEvent( "onchange" , OnGridReadyChangeLayout );

//make sure the grid exists
crmGrid = iframeDoc.all.crmGrid;
if( crmGrid )
{
iframeDoc.all.crmGrid.attachEvent( "onrefresh" , OnGridReadyChangeLayout );
//change the layout for the first time
OnGridReadyChangeLayout();
}
}

/*
This function (callback) is called every time the
grid view refreshes either by the selector or refresh button.

IF the Data( InnerGrid ) is not ready then the setTimeout is called.
*/
function OnGridReadyChangeLayout()
{
if( !crmGrid.InnerGrid )
return setTimeout( OnGridReadyChangeLayout , 100 );

/* Put your implementations under this line */
DecorateEmailColumn();
}

/*
This is an example of how to transform the email column into an
Active mailto link

IF the Column you want to Decorate is found then:
FOR each row in the grid
IF the column has text
1. SAVE the column text
2. CLEAR the column inner NOBR element html
3. CREATE a link element
4. APPEND the link element to the NOBR element

*/
function DecorateEmailColumn()
{
var emailColIndex = crmGrid.InnerGrid.FindColumnIndex("emailaddress1");
if( emailColIndex == -1 )
return;

for( var i = 0 ; i < crmGrid.InnerGrid.AllRecords.length ; i++ )
{
emailCell = crmGrid.InnerGrid.AllRecords[ i ][3].cells[ emailColIndex ];
if( emailCell.innerText == "" ) continue;

var emailText = emailCell.innerText;
emailCell.childNodes[0].innerHTML = "";
var link = iframeDoc.createElement("<A style='text-decoration:underline'>");
link.href = "mailto:" + emailText;
link.innerText = emailText;
emailCell.childNodes[0].appendChild( link );
}
}

</script>
</head>
<body scroll="no" style="margin:0px">
<iframe id="contsGrid"
src="about:blank"
style="width:100%;height:100%"
frameborder="0"
scrolling="no">
</iframe>
</body>
</html>

Thursday, September 25, 2008

Enhancing user experience using Shortcuts


If you get around the blog you’ll notice I targeted a wide range of dynamics usability issues. I’ve done so since I my experience taught me that application “interactivity” plays a major role in end users satisfaction which most agree to be one of the important key factors in successful dynamics implementations.

Most customers will identify the need to shorten the time and complexity (in clicks) it takes to accomplish a certain task and of course invest some of the project founds in building comfortable shortcuts like adding buttons on the grids or even replacing menu items with toolbar buttons. It seems odd that the whole shortcut mechanism is hardly ever used.

Up to a certain point users won’t mind switching between keyboard and mouse in order to fill out an entire form but as users get more and more acquainted with dynamics they search for better and more productive ways to interact with the system. Suddenly switching between devices seems like a waist of good energy. I asked my self that same question! Way can’t I just press (Alt + tab #) to navigate between tabs, or (Alt + S) to send out an email when I finish filling out the form. What about reassigning the form to another user, why do I need to navigate to the third tab to do so, can’t I just ( Alt + w ) to open the assignment window and of course the list goes on and on.

In order to address this I’ve created a very handy utility class which facilitates the creation of shortcuts on all types of crm functions i.e menu items, toolbar buttons, navigation links, fields and tabs. The nice thing about this feature is that it teaches the user to use those shortcuts from a very early stage. Each time the user uses the mouse to click on a “function” with a shortcut the shortcut combination is presented to him and soon he learns to use that instead.

In order to create shortcuts you need to create an instance of the ShortcutMaker class. This class facilitates the creation of all shortcuts on the form.
To create a new shortcut simply use the Add method by passing it a “function” id then use the return Shortcut object SetKeys method to set the special key (Alt or Ctrl), the accompanying letter (Capital) and the type of the function (Button, Menu item, Navigation link and so on). To finish the job use the ShortcutMaker Create method by passing the Shortcut object.


function OnCrmPageLoad()
{
//Create an instance of the ShortcutMaker class
var scMaker = new ShortcutMaker();

//Create a shortcut on the Send Email Button
var FollowUpSC = scMaker.Add( "_MBdocumentallRelatedInformationPaneExpanddocumentallRelatedInformationPaneLoadContextDatafollowup" );
FollowUpSC.SetKeys( scMaker.Keys.Alt , "O" , scMaker.UIType.Button );
scMaker.Create( FollowUpSC );

//Create a shortcut on the toolbar Follow up button
var SendEmailSC = scMaker.Add( "_MBlocAddActTo4202" );
SendEmailSC.SetKeys( scMaker.Keys.Alt , "E" , scMaker.UIType.Button );
scMaker.Create( SendEmailSC );

//Create a shortcut on the History naviation link
var actHistorySC = scMaker.Add( "navActivityHistory" );
actHistorySC.SetKeys( scMaker.Keys.Alt , "H" , scMaker.UIType.Navigation );
scMaker.Create( actHistorySC );

//Create a shortcut on the owner lookup field
var owneridSC = scMaker.Add( "ownerid" );
owneridSC.SetKeys( scMaker.Keys.Alt , "W" , scMaker.UIType.Field );
scMaker.Create( owneridSC );

//Create shortcuts on all form Tabs
var generalTabSC = scMaker.Add( "tab0Tab" ); //General Tab
generalTabSC.SetKeys( scMaker.Keys.Alt , "1" , scMaker.UIType.Tab );
scMaker.Create( generalTabSC );
var notesTabSC = scMaker.Add( "tab1Tab" ); //Notes Tab
notesTabSC.SetKeys( scMaker.Keys.Alt , "2" , scMaker.UIType.Tab );
scMaker.Create( notesTabSC );
var moreTabSC = scMaker.Add( "tab2Tab" ); //Another Tab
moreTabSC.SetKeys( scMaker.Keys.Alt , "3" , scMaker.UIType.Tab );
scMaker.Create( moreTabSC );

//Create shortcut on the AddNote menu item
var addnoteSC = scMaker.Add( "_MIlocAddObjTo5" );
addnoteSC.SetKeys( scMaker.Keys.Alt , "G" , scMaker.UIType.Menu );
scMaker.Create( addnoteSC );
}

function ShortcutMaker()
{
var Instance = this;
var ShortCuts = [];

Instance.Keys = {
Alt : "18",
Ctrl : "17"
}

Instance.UIType = {
Navigation : 1,
Button : 2 ,
Field : 3,
Menu : 4,
Tab : 5
}

Instance.Add = function( controlId ){
return new ShortCut( controlId );
}

Instance.Create = function( shortcut )
{
if( !shortcut )
return;

var hashName = shortcut.SpecialKey + shortcut.KeyCode;
if( shortcut.Control )
{
var controlWithTitle = shortcut.Control;
if( shortcut.UiType == Instance.UIType.Navigation )
controlWithTitle = controlWithTitle.childNodes[1];
controlWithTitle.title = controlWithTitle.title + " (" + shortcut.SpecialKeyName + " + " + shortcut.Letter + ")";
controlWithTitle.title = controlWithTitle.title.replace( /^\s/ , "" );
}
ShortCuts[ hashName ] = shortcut;
}

Instance.OnKeyDownCheckShortcuts = function()
{
var hashName = getSpecialKey( event ) + event.keyCode;
var shortcut = ShortCuts[ hashName ];
if( shortcut )
{
if( shortcut.UiType == Instance.UIType.Menu )
window.execScript( shortcut.Control.action );
else
shortcut.Control.click();

event.cancelBubble = true;
event.returnValue = false;
return false;
}
}

function getSpecialKey( evt ) {
if( evt.altKey ) return Instance.Keys.Alt;
else if( evt.ctrlKey ) return Instance.Keys.Ctrl;
return "0";
}

function ShortCut( controlId )
{
this.Control = document.getElementById( controlId );
this.Letter = "";
this.SpecialKey = 0;
this.KeyCode = 0;
this.UiType = 0;

this.SetKeys = function( spKey , letter , uiType )
{
if( !spKey || !letter )
return;

this.Letter = letter;
this.KeyCode = letter.charCodeAt(0);
this.SpecialKey = spKey;
this.SpecialKeyName = getSpecialKey(spKey);
this.UiType = uiType;
}

function getSpecialKey( spKey ) {
if( spKey == Instance.Keys.Alt ) return "Alt";
else if( spKey == Instance.Keys.Ctrl ) return "Ctrl";
}
}

function Initialize(){
document.attachEvent( "onkeydown" , Instance.OnKeyDownCheckShortcuts );
}

Initialize();
}

OnCrmPageLoad();

Monday, September 22, 2008

CRM Master Detail Page – Part 1


This is a nice and simple solution you can use to enable inline navigation between CRM entities OR in other word a master detail display. This is also considered an excellent alternative to not having the ability to edit the crm grid since the user doesn’t have to leave the current entity window which completely eliminates the open - close page problem and the entity fields are always available for editing.

The idea behind the solution is to create an iframe using customization, set the iframe src attribute to any view you like to show and use this code in the onload event. The only thing the code does is override the iframe window.open function which means that when the user double clicks on a record, instead of the opening a new window, the current window url is replaced with to the selected record url.


function OnCrmPageLoad()
{
/* Reference the IFrame */
var IFRAME_test = document.all.IFRAME_test;
/* Assign the IFrame URL */
IFRAME_test.src = DecideOnTheIFrameGridUrl();
/* Override the onreadystatechange Event */
IFRAME_test.onreadystatechange = function ()
{
/* if the IFrame is ready == 'complete' */
if( IFRAME_test.readyState != 'complete' )
return;
/* Remove the IFrame border since the grid already has one */
IFRAME_test.style.border = "0px";

/* Remove the IFrame padding and scroll */
var IframeWindo = IFRAME_test.contentWindow;
IframeWindo.document.body.scroll = "no";
IframeWindo.document.body.style.padding = "0px";

/*
This is what you really after!
Override the IFrame window.open function
So the selected record url replaces the current one.
*/
IframeWindo.open = function( url )
{
location.href = url;
return false;
}
}

}

function DecideOnTheIFrameGridUrl()
{
//This will show the entity default view
return "/" + ORG_UNIQUE_NAME + "/_root/homepage.aspx?etc=" + crmForm.ObjectTypeCode;
}

OnCrmPageLoad();

Sunday, September 21, 2008

JavaScript Ajax Wrapper

The idea behind this post is not about calling a specific crmservice but a general idea of how you can facilitate calls to any webservice, whether it’s a custom one you built your self or one of crm’s existing services. Although the code introduces working classes they can be further develop to support more advanced requirement.

The OnCrmPageLoad contains several calls using the WebServiceStartInfo and the WebRequest classes both to crmservice and a custom service I made to demonstrate the usage. The concept I follow is comprised of 2 steps. The first step is about filling the information required to make an Ajax call. The second and last step just executes the requests and hands over the results back to you.

The nice thing about the WebServiceStartInfo is that it builds the entire soap message for you. This makes life very easy since you can focus more on the business requirement and less on the technology. Never the less, a good grasp over Ajax is needed in order to fill the StartInfo with the correct data.

Note. The easiest way of getting the soap needed to make a request is to open the asmx file in Internet explorer. This will assist you when you construct complex webmethod parameters using the Parameters.Add method of the WebServiceStartInfo and Parameter classes.

The fist part describes the WebService.aspx code behind:


namespace MyAppServices
{
/// <summary>
/// Summary description for WebService1
/// </summary>
[WebService(Namespace = "http://www.mycompany.com/demo")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[ToolboxItem(false)]
public class WebService1 : System.Web.Services.WebService
{

[WebMethod]
public string Hello()
{
return "Hello";
}

[WebMethod]
public string HelloUser(String userName)
{
return String.Format("Hello {0}", userName);
}

[WebMethod]
public String HelloOnceMore( String[] Parameters )
{
return String.Join( "," , Parameters );
}

[WebMethod]
public string HelloAgain(ComplexParameter userInfo)
{
return String.Format("Hello {0} {1}", userInfo.FirstName, userInfo.LastName);
}

[Serializable]
public class ComplexParameter
{
public String FirstName;
public String LastName;
}
}
}



This part describes the client side usage called from crm onload event / external file.


function OnCrmPageLoad ()
{
/* call the CrmService Fetch method. */
var fetchXml = _HtmlEncode('<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false"><entity name="systemuser"><attribute name="fullname"/><attribute name="businessunitid"/><attribute name="title"/><attribute name="address1_telephone1"/><attribute name="systemuserid"/><order attribute="fullname" descending="false"/><filter type="and"><condition attribute="systemuserid" operator="eq-userid"/></filter></entity></fetch>');

var crmInfo = new WebServiceStartInfo();
/* machine.domain:port */
crmInfo.Host = "http://moss.litwareinc.com:5555";
/* crm service default namespace */
crmInfo.XmlNS = "http://schemas.microsoft.com/crm/2007/WebServices";
/* crmservice url */
crmInfo.Asmx = "/mscrmservices/2007/crmservice.asmx"
crmInfo.Method = "Fetch";
/* include a CrmAuthentication Token */
crmInfo.CrmToken = true;
crmInfo.Async = true;
/* call this function when done */
crmInfo.Callback = OnEndRequest;
/* the Fetch method parameter */
crmInfo.Parameters.Add( "fetchXml" , fetchXml );

/* Assign the start info to the webrequest */
var request = new WebRequest( crmInfo );
request.Execute();

// --------------------------------------------------------------
/* this part calls the custom web method above */
var customInfo = new WebServiceStartInfo();
/* the webservice resided on the default website on port 80 */
customInfo.Host = "http://moss.litwareinc.com";
/* the default namespace I gave the WebService1 class */
customInfo.XmlNS = "http://www.mycompany.com/demo";
/* the webservice url */
customInfo.Asmx = "/MyAppServices/WebService1.asmx"
customInfo.Method = "Hello";
/* we don’t need a token since we are not calling crm services */
customInfo.CrmToken = false;
customInfo.Async = true;
customInfo.Callback = OnEndRequest;

request = new WebRequest( customInfo );
request.Execute();

// -------------------------------------------------------------
/*
since we already have a startinfo object I just changed the property
values to show more advance usage in this case the HelloUser method
receives a parameter.

Signature:
[WebMethod]
public string HelloUser( string username );
*/
customInfo.Method = "HelloUser";
customInfo.Parameters.Add( "userName" , "Adi Katz" );

request = new WebRequest( customInfo );
request.Execute();

// -------------------------------------------------------------
/*
The following method signature is comprised of a string array
[WebMethod]
public string HelloOnceMore( string[] Parameters );
*/
customInfo.Method = "HelloOnceMore";
var StringArray = customInfo.Parameters.Add( "Parameters" );
StringArray.Parameters.Add( "string" , "Adi" );
StringArray.Parameters.Add( "string" , "Katz" );
request = new WebRequest( customInfo );
request.Execute();

// -------------------------------------------------------------
/*
The following web method signature is comprised of a complex type
called ComplexParameter which is decorated with the [Serializable]
attribute and contains a FirstName and LastName.

public class ComplexParameter
{
public string FirstName;
public string LastName;
}

Signature:
[WebMethod]
public string HelloAgain( ComplexParameter complexParameter );

*/
customInfo.Method = "HelloAgain";
customInfo.Async = false;

var ComplexParameter = customInfo.Parameters.Add( "userInfo" );
ComplexParameter.Parameters.Add( "FirstName" , "Bill" );
ComplexParameter.Parameters.Add( "LastName" , "Gates" );

request = new WebRequest( customInfo );
var result = request.Execute();
alert(result.text);
}

function OnEndRequest( requestObj )
{
alert( requestObj.responseXML.text );
}

/* Holds all the information needed to call a webservice */
function WebServiceStartInfo()
{
/* WebMethod Name */
this.Method = "";
/* Server Name + Port */
this.Host = ""
/* The Default XML Namespace */
this.XmlNS = "";
/* The WebService Url */
this.Asmx = "";
/* Holds the soap envelop */
this.Soap = new StringBuilder();
this.Async = false;
/* Holds the function (pointer) to call if Async is true */
this.Callback = null;
/* Flag which specify whether to include the crm token */
this.CrmToken = false;
/* Holds the WebMothod parameters list */
this.Parameters = new ParameterCollection();
/* Returns a valid SoapAction */
this.GetSoapAction = function() {
return this.XmlNS + "/" + this.Method;
}
/* Gets the complete url to call */
this.GetUrl = function() {
return this.Host + this.Asmx;
}
/* return the entire soap envelop xml */
this.ToString = function()
{
/* clear the soap former calls */
this.Soap.Clear();

this.Soap.Append( '<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">' );
if( this.CrmToken )
this.Soap.Append( GenerateAuthenticationHeader() );

this.Soap.Append( '<soap:Body>' );
this.Soap.Append('<').Append(this.Method);
this.Soap.Append(' xmlns="').Append(this.XmlNS).Append('"');

if( this.Parameters.List.length > 0 )
{
this.Soap.Append('>');
for( var i = 0 ; i < this.Parameters.List.length ; i++ )
this.Soap.Append( this.Parameters.List[i].ToString() );
this.Soap.Append('</').Append(this.Method).Append('>');
}
else
{
this.Soap.Append(' />');
}

this.Soap.Append( '</soap:Body>' );
this.Soap.Append( '</soap:Envelope>' );

return this.Soap.ToString();
}
/* A collection of parameters */
function ParameterCollection()
{
/* Parameters list */
this.List = [];
/*
Adds a new parameter and returns a reference so
further child parameters can be added.

for example:
var ComplexParam = Object.Parameters.Add( "ComplexParameter" )
ComplexParam.Parameters.Add( "FirstName" , "Adi" );

Result Xml
<ComplexParam>
<FirstName>Adi</FirstName>
</ComplexParam>

Signature
[WebMethod]
public string MethodName( ComplexParam complexParam );
*/
this.Add = function( name , value )
{
var parameter = new Parameter( name , value );
this.List[ this.List.length ] = parameter;
return parameter;
}
}
/* A single parameter */
function Parameter( name , value )
{
/*
Holds either the Property Name or type if this is an array
for example:
var StringArray = Object.Parameters.Add( "StringArray" );
StringArray.Parameters.Add( "string" , "Adi" );
StringArray.Parameters.Add( "string" , "Katz" );

Result Xml
<StringArray>
<string>Adi</string>
<string>Katz</string>
</StringArray>

Signature:
[WebMethod]
public string MethodName( string[] StringArray );
*/
this.Name = name;
this.Value = value;
/* List of child parameters */
this.Parameters = new ParameterCollection();
/* Internal string builder */
this.Xml = new StringBuilder();

this.ToString = function()
{
this.Xml.Append('<').Append(this.Name).Append('>');
if( this.Value != null ) this.Xml.Append(this.Value);
else if( this.Parameters.List.length > 0 )
for( var i = 0 ; i < this.Parameters.List.length ; i++ )
this.Xml.Append( this.Parameters.List[i].ToString() );
this.Xml.Append('</').Append(this.Name).Append('>');
return this.Xml.ToString();
}
}
/*
Utility class that uses an array to append new strings
and uses the join method to concatenate them
*/
function StringBuilder()
{
/* String elements*/
this.Parts = [];
/*
Appends a new string into the array and
returns a reference to the String Builder so you can write:
Object.Append('String A').Append('String B');
*/
this.Append = function( text ) {
this.Parts[this.Parts.length] = text;
return this;
}
/* Re-Initialized the parts array for consequent calls */
this.Clear = function(){
this.Parts = [];
}
/* returns the parts as string */
this.ToString = function() {
return this.Parts.join("");
}
}
}

function WebRequest( startInfo )
{
var WebInfo = this;
/* Structure that holds the WebService information */
WebInfo.StartInfo = startInfo;
/* Executes the ws call */
WebInfo.Execute = function ( startInfo )
{
if( !isNullOrEmpty( startInfo ) )
WebInfo.StartInfo = startInfo;

var xmlHttp = new ActiveXObject("Msxml2.XMLHTTP");
xmlHttp.open("POST", WebInfo.StartInfo.GetUrl() , WebInfo.StartInfo.Async ); //Sync Request
xmlHttp.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
xmlHttp.setRequestHeader("SOAPAction", WebInfo.StartInfo.GetSoapAction() );

if( WebInfo.StartInfo.Async )
{
/* Calls the callback */
xmlHttp.onreadystatechange = function() {
/*
Calls the user callback function only when the readystate is complete
so the user does not have to check the readyState him self.
*/
if( xmlHttp.readyState != 4 ) return;
WebInfo.StartInfo.Callback( xmlHttp )
} ;
/* Execute */
xmlHttp.send( WebInfo.StartInfo.ToString() );
}
else
{
xmlHttp.send( WebInfo.StartInfo.ToString() );
return xmlHttp.responseXML;
}
}

function isNullOrEmpty( o ){
return o == null || typeof( o ) == "undefined";
}
}

OnCrmPageLoad();

Friday, September 19, 2008

CRM 4.0 inline spell checker

I was looking integrate an inline spellchecker with Input and Textarea fields in crm. The idea was to enable spelling checks on any nvarchar field that users can activate by pressing shortcut combination keys e.g. Alt + Z. During my quest I stumbled on jianwang blog and saw the following post which uses the local machine (user) Microsoft word spell checker. I really liked the idea but hated the clumsy UI. So eventually I decided to try integrating my custom tool tip post with a simple retrieval of the Spelling Errors suggestion list. And, as always, what started as a simple programming task turned out to be a pain in the butt since I had to mimic the spell checker UI functionality.


I think the result came out pretty cool and since there are plenty of free spell checker out there why not add another one.
The usage is very simple! You create an instance of the InlineSpellChecker class decide on a shortcut combination key (default keyCode is 90 [z]) add the field or fields you what to enable spell checking on and you’re done.


The spell checker object is not called until the user presses Alt + z which mean the code does not affect load time. The first time might take a second or 2 but after that it works pretty quickly. I didn’t test it with large data so that might have an impact as well.


The UI have 4 buttons, the first 2 allow you to navigate between the list of spelling errors and the last 2 enables you to replace the error(s) with one of the selected suggestions. The field focus is never lost so you can open the spell checker and continue writing while it is open. The user can press Alt + z while writing to refresh the spelling errors suggestion list to accommodate the new written words.


When you select a suggestion it is painted in red. If you replace 1 occurrence then the suggestion is painter in orange. If all occurrences have been replaced the suggestion is painted in green. The current spelling error is also highlighted so the user can keep track. The image bellow displays a graphical representation of the above.


My only disclaimer is that you give me your feedback should you choose to use it in your customer’s application.






 
var Spellcheker;

function OnCrmPageLoad()
{
Spellcheker = new InlineSpellcheker();
Spellcheker.KeyCode = 90;
Spellcheker.AddField("gi_name");
}


function InlineSpellcheker()
{
/* Private Fields */
var Instance = this;
var mswordApp;
var activeDoc;

/* Public Fields */
Instance.KeyCode = 90; //Z
Instance.Fields = [];
Instance.SBox = new SuggestionBox();

/* Public Methods */
/* Add Fields with spell checking */
Instance.AddFields = function()
{
for( var i = 0 ; i < arguments.length; i++ )
{
var field = document.getElementById( arguments[ i ] );
if( isNullOrEmpty( field )) return;

Instance.Fields[ field.id ] = field;
field.title = "Press Alt + ";
field.title += String.fromCharCode(Instance.KeyCode);
field.title += " to use SpellChecker";

field.attachEvent( "onkeydown" , onSpellCheckingTest );
field.attachEvent( "onfocusout" , onLostFocusCleanUp );
}
}
/* Add a Field with spell checking*/
Instance.AddField = function( fieldId ){
Instance.AddFields( fieldId );
}
/* Kill word application */
Instance.Quite = function()
{
if( !isNullOrEmpty( mswordApp ) )
{
mswordApp.ActiveDocument.Close( 0 );
mswordApp.Quit( 0 );
}
activeDoc = null;
mswordApp = null;
}

/* Event Callbacks */
function onLostFocusCleanUp()
{
var field = Instance.Fields[ event.srcElement.id ];
if( isNullOrEmpty( field ) ) return;

field.SpellingErrors = null;
field.Box = null;
Instance.SBox.Clean();
}

function onSpellCheckingTest()
{

var scKeyStroke = event.altKey && event.keyCode == Instance.KeyCode;
if( scKeyStroke == true )
{
var field = Instance.Fields[ event.srcElement.id ];
if( isNullOrEmpty( field ) || isNullOrEmpty( field.DataValue ) )
return Instance.SBox.Hide();

Instance.SBox.Load( field );

var result = InitializeWordObject();
if( result != "" ) return alert( "ok" + result );

field.SpellingErrors = new SpellingErrors();

var textParts = field.DataValue.split( /\W/ );
var foundSuggestions = false;

for( var i = 0 ; i < textParts.length ; i++ )
{
var word = textParts[ i ].replace(/\W*/gi,"");
if( field.SpellingErrors.GetByValue( word ) != null )
continue;

try
{
activeDoc.Content = textParts[i];
activeDoc.LanguageDetected = false;
var range = activeDoc.Range(0,activeDoc.Range().End);
range.DetectLanguage();

if( range.SpellingErrors.Count > 0 )
{
var result = range.GetSpellingSuggestions();
if( result.Count > 0 )
{
var suggestion = new Suggestion(word);
for( var k = 1 ; k < result.Count ; k++ )
suggestion.AddOption( result.Item( k ).name );
field.SpellingErrors.Add( suggestion );
foundSuggestions = true;
}
}
}
catch( e )
{
Instance.Quite();
return alert( e.message );
}
}

if( !foundSuggestions ) Instance.SBox.Hide();
else Instance.SBox.UpdateUI( 0 );

return false;
}
}

/* Private Members */
function InitializeWordObject()
{
try
{
if( !isNullOrEmpty( mswordApp ) ) return "";
attachEvent( "onunload" , Instance.Quite );
mswordApp = new ActiveXObject( "Word.Application" );
mswordApp.Visible = false;
mswordApp.Application.Visible = false;
activeDoc = mswordApp.Documents.Add();
return "";
}
catch( wordErr )
{
Instance.Quite();
return wordErr.message;
}
}

/* Classes */
function Suggestion( word )
{
/* Public Fields */
this.Word = word;
this.Replacement = "";
this.Options = [];
this.IsReplacedOnce = false;
this.IsReplacedAll = false;

/* Public Methods */
this.AddOption = function( name ){
this.Options[ this.Options.length ] = name;
}
}

/* Collection of Spelling Errors Suggestions */
function SpellingErrors()
{
/* Public Fields */
this.Keys = [];
this.Vals = [];

/* Public Methods */
this.Add = function( suggestion )
{
if( this.Vals[ suggestion.Word ] == null )
{
this.Keys[ this.Keys.length ] = suggestion;
this.Vals[ suggestion.Word ] = suggestion;
}
}
/* Retrieve suggestion by word */
this.GetByValue = function( word ){
return this.Vals[ word ];
}
/* Retrieve suggestion by position */
this.GetByIndex = function( index )
{
if( this.Keys[ index ] != null )
return this.Vals[ this.Keys[ index ].Word ];
return null;
}
}

/* Suggestion Box Popup */
function SuggestionBox()
{
/* Private Fields */
var Box = this;
var SBoxPopup;

/* Public Fields */
Box.Field = null;

Box.Load = function( field )
{
Box.Field = field;

var loadingHTML = "<table height='100%' width='100%' style='cursor:wait'>";
loadingHTML += "<tr><td valign='middle' align='center'>";
loadingHTML += "<img alt='' src='/_imgs/AdvFind/progress.gif'/>";
loadingHTML += "<div/><b>Loading...</b>";
loadingHTML += "</td></tr>";
loadingHTML += "</table>";

SBoxPopup.document.all.divList.innerHTML = loadingHTML;

var Width = 241;
var Height = 140;
var Position = GetControlPostion(field);
var Left = Position.X + 1;
var Top = Position.Y + 1;
SBoxPopup.show( Left , Top , Width , Height , null );
}

/* Public Methods */

/* Hide the popup is it exists */
Box.Hide = function() {
if( SBoxPopup ) SBoxPopup.hide();
}
/* Clean popup control expando objects and html*/
Box.Clean = function()
{
with( SBoxPopup.document.all )
{
btnReplace.SelectedOption = null;
btnReplaceAll.SelectedOption = null;
spnReplace.innerHTML = "";
}
}
/* Update the current selection */
Box.UpdateSelection = function( index , newWord )
{
var suggestion = Box.Field.SpellingErrors.GetByIndex(index);
if( !isNullOrEmpty( suggestion ) )
{
suggestion.Replacement = newWord;
with( SBoxPopup.document.all )
{
spnReplace.innerHTML = "<B style='color:red'>" + newWord + "</B>";
btnReplace.SelectedOption = suggestion;
btnReplaceAll.SelectedOption = suggestion;
}
}
}
/* Update the Popup UI with current word selection */
Box.UpdateUI = function( index )
{
var spellingErrors = Box.Field.SpellingErrors;
if( !isNullOrEmpty( spellingErrors ) && spellingErrors.Keys.length > 0)
{
var create = SBoxPopup.document.createElement;
with( SBoxPopup.document.all )
{
divList.innerHTML = "";
var suggestion = spellingErrors.GetByIndex( index );
spnWord.innerText = suggestion.Word;
spnReplace.innerHTML = getReplacementHTML( suggestion );

for( var i = 0 ; i < suggestion.Options.length ; i++ )
{
var n = create( "SPAN" );
n.innerHTML = "&nbsp;" + ( i + 1 ) + ".&nbsp;";

var a = create( "<A style='width:85%;padding:1px'>" );
a.href = "#";
a.onclick = function(){ Box.UpdateSelection( index , this.innerText ); };
a.onmouseover = function(){ this.style.backgroundColor = 'gold' ; };
a.onmouseout = function(){ this.style.backgroundColor = 'white'; };
a.Box = Box;
a.innerText = suggestion.Options[ i ];

var br = create("<BR style='line-height:2px'>");

divList.appendChild( n );
divList.appendChild( a );
divList.appendChild( br );
}

var spLen = Box.Field.SpellingErrors.Keys.length;
var iPrev = index - 1 < 0 ? 0 : index - 1;
var iNext = index + 1 > spLen - 1 ? spLen - 1 : index + 1;

btnPrev.Box = Box;
btnNext.Box = Box;
btnReplace.Box = Box;
btnReplaceAll.Box = Box;

btnReplace.SelectedOption = null;
btnReplaceAll.SelectedOption = null;

btnPrev.onclick = function(){ this.Box.UpdateUI( iPrev ); }
btnNext.onclick = function(){ this.Box.UpdateUI( iNext ); }
btnReplace.onclick = function(){ this.Box.Replace( this ); }
btnReplaceAll.onclick = function(){ this.Box.ReplaceAll( this );}

var range = Box.Field.createTextRange();
var found = range.findText(suggestion.Word,1,2);
if( found )
{
range.expand(suggestion.Word);
range.select();
}
}
}
}
/* Replace a single occurance of the current error */
Box.Replace = function( btn )
{
var suggestion = btn.SelectedOption;
if( suggestion )
{
var re = new RegExp( "\\b" + btn.SelectedOption.Word + "\\b" , "i" );
var result = replace( btn , re );
suggestion.IsReplacedOnce = result != Box.Field.DataValue;
suggestion.IsReplacedAll = result == Box.Field.DataValue;
Box.Field.DataValue = result;
setReplacementHTML( suggestion );
}
else
{
setNullSuggestionHTML();
}
}
/* Replace all occurances of the current error */
Box.ReplaceAll = function( btn )
{
var suggestion = btn.SelectedOption;
if( suggestion )
{
var re = new RegExp( "\\b" + btn.SelectedOption.Word + "\\b" , "ig" );
var result = replace( btn , re );
suggestion.IsReplacedAll = true;
Box.Field.DataValue = result;
setReplacementHTML( suggestion );
}
else
{
setNullSuggestionHTML();
}
}

/* Private */

function replace( btn , re )
{
var suggestion = btn.SelectedOption;
if( !isNullOrEmpty( Box.Field.DataValue ) &&
!isNullOrEmpty( suggestion ) )
{
return Box.Field.DataValue.replace( re , suggestion.Replacement );
}
}

function GetControlPostion( control )
{
var Position = new Object();
var controlHeight = control.offsetHeight;
var iY = 0, iX = 0;
while( control != null )
{
iY += control.offsetTop;
iX += control.offsetLeft;
control = control.offsetParent;
}
Position.X = iX + screenLeft;
Position.Y = iY + screenTop + controlHeight;
return Position;
}

function Initialize()
{
SBoxPopup = window.createPopup();

var baseStyleUrl = "/" + ORG_UNIQUE_NAME + "/_common/styles";
with( SBoxPopup.document )
{
createStyleSheet( baseStyleUrl + "/global.css.aspx?lcid=" + USER_LANGUAGE_CODE );
createStyleSheet( baseStyleUrl + "/fonts.aspx?lcid=" + USER_LANGUAGE_CODE );
createStyleSheet( baseStyleUrl + "/controls.css.aspx?lcid=" + USER_LANGUAGE_CODE );
body.innerHTML = getSBoxHTML();
}
}

function getSBoxHTML()
{
var html = "";
html += "<DIV style='width:100%;height:100%;border:1px solid gray;background-color: #d8e8ff;filter: progid:DXImageTransform.Microsoft.Gradient(GradientType=0,StartColorStr=#eff3ff,EndColorStr=#84aed6);padding-left:2px;font:12 px tahoma'>";

html += "<TABLE cellpadding='1' cellspacing='1' style='width:100%'>";
html += "<TR>";
html += "<TD width='40%' height='20' valign='middle'>";
html += "<NOBR>";
html += "<IMG src='/_imgs/ico_mailmerge.gif' align='top'/>&nbsp;";
html += "<B><SPAN style='margin-top:5px' id='spnWord'></SPAN></B>";
html += "</NOBR>";
html += "</TD>";

html += "<TD>";
html += "<NOBR>";
html += "<IMG src='/_imgs/SetRegarding.gif' align='top'/>&nbsp;";
html += "<B><SPAN id='spnReplace'></SPAN></B>";
html += "</NOBR>";
html += "</TD>";

html += "<TR>";
html += "<TD>";
html += "<BUTTON id='btnPrev' class='ms-crm-Button' style='height:25'>Previous</BUTTON>";
html += "</TD>";
html += "<TD rowspan='4' width='65%' height='20'>";
html += "<DIV id='divList' style='overflow-y:scroll;width:95%;height:110;border:1px solid black;background-color:white'>";
html += "</DIV>";
html += "</TD>";
html += "</TR>";

html += "<TR>";
html += "<TD>";
html += "<BUTTON id='btnNext' class='ms-crm-Button' style='height:25'>Next</BUTTON>";
html += "</TD>";
html += "</TR>";

html += "<TR>";
html += "<TD>";
html += "<BUTTON id='btnReplace' class='ms-crm-Button' style='height:25'>Replace</BUTTON>";
html += "</TD>";
html += "</TR>";

html += "<TR>";
html += "<TD>";
html += "<BUTTON id='btnReplaceAll' class='ms-crm-Button' style='height:25'>Replace All</BUTTON>";
html += "</TD>";
html += "</TR>";

html += "</DIV>";
return html;
}

function getReplacementColor( suggestion ){
if( suggestion.IsReplacedAll ) return "green";
else if( suggestion.IsReplacedOnce ) return "orange";
else return "red";
}

function getReplacementHTML( suggestion ){
var color = getReplacementColor( suggestion );
return "<B style='color:" + color + "'>" + suggestion.Replacement + "</B>";
}

function setReplacementHTML( suggestion ){
SBoxPopup.document.all.spnReplace.innerHTML = getReplacementHTML( suggestion );
}

function setNullSuggestionHTML(){
SBoxPopup.document.all.spnReplace.innerHTML = "<B>Select a suggestion</b>";
}

Initialize();
}

//Utility
function isNullOrEmpty( o ){
return o == null || typeof( o ) == "undefined" || o == "";
}
}

OnCrmPageLoad();

Wednesday, September 17, 2008

Cloning an Entity Part 3 – using Ajax

This is a complementary post to my two previous post regarding how to clone an entity using JavaScript.The main idea, as the title suggests, is to build a soap envelope utilizing the create message gather the entity values and send them to CrmService.Create web method. The method returns a create result of the newly created (cloned) entity which is used to create a valid entity url
e.g. edit.aspx?id={cloned entity id}.


I also included a dynamic popup similar to ms yellow customization popup since this is a server side operation. If an error is returned then the popup inner message is changed and displays the error information.


I added a simple prompt (OnCrmPageLoad) instead of an ISV button. You can check the “cloning an entity using JavaScript” for details on how to implement the button.


if you don’t want to include the popup in your solution , should you choose this method for cloning, then simply set the ajaxCloner.Progress.Visible to false. If you need to clone specific fields then use the AddField method. If you don’t use the method the cloner will send all non null fields to CrmService.Create. Finally use the Clone method to rap up the process.


I bet some of you are asking your self why not send the entity id and selected attributes names to a custom web method and do every thing there. The answer is that you can. The major advantage this method has to offer over a custom web method is that it saves you a Query(expression) to crm since the entity values are already available to you. On the other hand you're sending more information over the network


This might seem a lot, i hope you finod this to be self explanatory.
I added general summery remarks on top of each functional code block and direct links to ms sdk files above.









/* simulate ISV button */
function OnCrmPageLoad()
{
if( prompt("Execute?") == "ok" )
CloneEntity();
}

/*
Call this method from the ISV button.
Create a new cloner, set whether to show a progress popup.
Set the popup default message.
Add specific candidate fields (optional). Otherwise all the form is cloned
Start the cloning process
*/
function CloneEntity()
{
var ajaxCloner = new AjaxCloner();
ajaxCloner.Progress.Visible = true;
ajaxCloner.Progress.Message = "Cloning Account";
//ajaxCloner.AddField(“firstname”);
ajaxCloner.Clone();
}

/*
Cloner class.
Sets the progress popup default values.
AddField – add new candidate fields to be cloned
Clone – Start the cloning process
*/
function AjaxCloner()
{
var Instance = this;

Instance.Progress = new ProgressPopup();
Instance.Progress.Visible = false;
Instance.Progress.Message = "Cloning...";

Instance.Request = null;
Instance.Fields = null;

Instance.AddField = function( fieldId )
{
if( Instance.Fields == null )
Instance.Fields = [];

var field = document.getElementById(fieldId);
if( field ) Instance.Fields[fieldId] = field;
}

Instance.Clone = function()
{
Instance.Progress.Show();
var list = Instance.Fields == null ? crmForm.all : Instance.Fields;
var xml = "<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\">";
xml += GenerateAuthenticationHeader();
xml += "<soap:Body>";
xml += "<Create xmlns=\"http://schemas.microsoft.com/crm/2007/WebServices\">";
xml += "<CreateDuplicatesOptionalParameter><value>false</value></CreateDuplicatesOptionalParameter>";
xml += "<PersistInSyncOptionalParameter><value>false</value></PersistInSyncOptionalParameter>";
xml += "<entity xsi:type='" + crmForm.ObjectTypeName + "'>";
for( var item in list )
{
var field = list[item];
if( field.DataValue == null || !field.req ) continue;

switch( field.tagName )
{
case "IMG": //Lookup
if( field.lookupstyle == "multi" )
{
xml += "<" + field.id + ">";
for( var i = 0 ; i < field.DataValue.length ; i++ )
{
xml += "<activityparty>";
xml += "<partyid type='" + field.DataValue[i].typename + "'>"
xml += field.DataValue[i].id;
xml += "</partyid>";
xml += "</activityparty>";
}
xml += "</" + field.id + ">";
}
else
{
xml += "<" + field.id + " type='" + field.DataValue[0].typename + "'>"
xml += field.DataValue[0].id;
xml += "</" + field.id + ">";
}
break;
case "TABLE": //DateTime
xml += "<" + field.id + ">";
xml += field.InnerText;
xml += "</" + field.id + ">";
break;
default: //All the rest
if( field.type == "hidden" || field.id == "statuscode")
continue;
xml += "<" + field.id + ">";
xml += field.DataValue;
xml += "</" + field.id + ">";
break;
}
}
xml += "</entity>";
xml += "</Create>";
xml += "</soap:Body>";
xml += "</soap:Envelope>";

Instance.Request = CreateXmlHttp();
Instance.Request.open("POST", "/mscrmservices/2007/crmservice.asmx", true );
Instance.Request.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
Instance.Request.setRequestHeader("SOAPAction", "http://schemas.microsoft.com/crm/2007/WebServices/Create");
Instance.Request.onreadystatechange = OnCloneEnd;
Instance.Request.send(xml);
}

/*
Cloning callback.
Checks the request status. If it’s ready and the request succeeded then
Extract the cloned entity id, build a valid entity url, close the popup and open a new window
*/
function OnCloneEnd()
{
if( Instance.Request.readyState != 4 )
return;
if( Instance.Request.status != 200 )
return HandleError();

var resultId = Instance.Request.responseXML.selectSingleNode("//CreateResult").text;
var url = location.pathname + "?id=" + resultId;
var features = "top=" + screenTop + ",";
features += "left=" + screenLeft + ",";
features += "width=" + document.body.offsetWidth + ",";
features += "height=" + document.body.offsetHeight;

Instance.Progress.Hide();
window.open ( url , "" , features );
}

/*
If the request generated an error then display the error to the user.
*/
function HandleError()
{
var sHTML = Instance.Request.responseXML.text;
if( Instance.Progress )
Instance.Progress.UpdateMessage( sHTML );
}

/*
This class represents the progress popup object.

*/
function ProgressPopup()
{
var Popup = this;
var Control = window.createPopup();

Popup.Message = "";
Popup.Visible = false;

Popup.Show = function()
{
if( !Popup.Visible ) return;

bulidPopup();
Popup.Control.show(( window.document.body.clientWidth - 250 )/2,
( window.document.body.clientHeight - 160 )/2,
250, 160, document.body);
}

Popup.Hide = function()
{
if( !Popup.Visible ) return;
Popup.Control.hide();
}

Popup.UpdateMessage = function( html )
{
if( !Popup.Visible ) return;
Popup.Control.document.all.msgDiv.innerHTML = getPopupHTML(html);
}

function bulidPopup()
{
if( Popup.Control ) return;

Popup.Control = window.createPopup();

with( Popup.Control.document )
{
createStyleSheet("/" + ORG_UNIQUE_NAME + "/_common/styles/global.css.aspx?lcid=" + USER_LANGUAGE_CODE);
createStyleSheet("/" + ORG_UNIQUE_NAME + "/_common/styles/fonts.aspx?lcid=" + USER_LANGUAGE_CODE);

var outerHTML = "<DIV id='msgDiv' class='actionMsgBox' style='width:100%;height:100%;backgroundColor:#ffffee;border:2px solid #000000;'>"
var msgDiv = createElement(outerHTML);
body.appendChild(msgDiv);
msgDiv.innerHTML = getPopupHTML(Popup.Message);
}
}

function getPopupHTML( innerText )
{
var popupHTML = "<table width=100% height=100%>";
popupHTML += "<tr>";
popupHTML += "<td style='font: bold 15px;text-align: center; vertical-align: middle; cursor: wait; color: #000099'>";
popupHTML += innerText;
popupHTML += "</td>";
popupHTML += "</tr>";
popupHTML += "</table>";

return popupHTML;
}
}
}

window.CloneEntity = CloneEntity;

OnCrmPageLoad();

Tuesday, September 16, 2008

Cloning an entity Part 2 - Addressable Forms

As you know crm 4.0 has a new feature called addressable forms. This allows you to fill a new entity Form from scratch by sending Named (Field Names) parameters inside the Query String e.g. /edit.aspx?fieldname1=value1&fieldname2=value2 .


The reason I decided to write about this is I thought this could be a more supported replacement to my “cloning an entity with javascript” post which uses the opener to get the cloning entity information. This approach is not without limitations since the amount of data you can pass in the query string is restricted to 2083 characters.


I found ms sdk somewhat missing! For example, Ms states you need to pass a DateTime field “text” as a valid value. The problem is ms don’t expose any supported property that returns that text so I ended using the {DateTime Field}.InnerText instead. (On second thought I have the feeling that this is what they meant).


Although the owner lookup is a single lookup it needs the entity typename (“systemuser”), didn’t see any reference about that in the sdk.
You might find other issue which I missed.


The AddressableForm object is a nice wrapper which makes it easy to use this feature.
The object has an AddField method which accepts a Field ID to be passed to the new window. If you don’t specify any fields the class will try to clone the entire field set. Finally use the open method to complete the job.


In the following example i simply created a condition in the OnCrmPageLoad to test this. You should create an isv.config button that calls the CloneEntity function.


function OnCrmPageLoad()
{
if( crmForm.FormType == 2 )
CloneEntity();
}

function CloneEntity()
{
var addrForm = new AddressableForm();
//Optional , if you don't specify any field all fields that are not null are cloned
addrForm.AddField("firstname");
addrForm.AddField("ownerid");
addrForm.AddField("defaultpricelevelid");
addrForm.Open();
}

function AddressableForm()
{
var Instance = this;
var url = location.pathname + "?";
var search = "";

Instance.Fields = [];
Instance.AddField = function( fieldId )
{
var field = document.getElementById( fieldId );
if( field ) Instance.Fields[Instance.Fields.length] = field;
}

Instance.Open = function()
{
var list = Instance.Fields.length > 0 ? Instance.Fields : crmForm.all;

for( var i = 0 ; i < list.length ; i++ )
{
if( list[i].req && list[i].DataValue )
FillAddressableParameter(list[i]);
}

url = url + search.replace(/&$/gi,"");

var features = "toolbars=0,top=" + screenTop + ",";
features += "left=" + screenLeft + ",";
features += "width=" + document.body.offsetWidth + ",";
features += "height=" + document.body.offsetHeight;
window.open( url , "" , features );
}

function FillAddressableParameter( element )
{
var elementId = element.id;
var elementDv = element.DataValue;

switch( element.tagName )
{
case "IMG": //lookup
search += elementId + "=" + elementDv[0].id + "&";
search += elementId + "name=" + elementDv[0].name + "&";
/*
Special Cases
Condition1 - checks if this is a customer lookup
Condition2 - checks if this is an owner lookup
*/
if( element.lookuptypes.indexOf(',') != -1 || element.lookuptypes == "8")
search += elementId + "type=" + elementDv[0].typename + "&";
break;
case "TABLE": // datetime
search += elementId + "=" + element.InnerText + "&";
break;
default:
search += elementId + "=" + elementDv + "&";
break;
}
}
}

window.CloneEntity = CloneEntity;

OnCrmPageLoad();

Monday, September 15, 2008

Display Fetch in Iframe


The fetch viewer is a very handy utility class that accepts advance find parameters and “injects” the result into any IFRAME.

The nice thing about it is that it does not require any server side deployment (no code behind). The code creates a dynamic FORM fills the required advanced find parameters and submits the request to the fetchdata.aspx which is the advanced find result page. The code also takes care of iframe padding and presents a loading gif until the page is rendered.


In order for this to work you need to specify all the required parameters such as FetchXml , LayoutXml , EntityName and the DefaultQueryId for the requested entity. The idea is to create a complete request using the advanced find window, concatenate fetchxml dynamic values, assign the predefined masterpiece to the FetchViewer class and use the Refresh method to finish the job.

You can get the fetch parameters by pasting the following code in the advanced find window addressbar.



javascript:void( new function(){ prompt("Fetch Parameters:",getFetchParams());function getFetchParams(){ return "FetchXml:\n" + advFind.FetchXml + "\n\n" + "LayoutXml:\n" + advFind.LayoutXml + "\n\n" + "EntityName:\n" + advFind.EntityName + "\n\n" + "DefaultAdvancedFindViewId:\n" + advFind.DefaultAdvancedFindViewId } } )



Please notice, I’ve revised the code to support iframe positioning on any tab.
Iframes that aren’t located on default tab should be handled a bit differently since the iframe url is handled by crm (even if the initial url is about:blank).

When the form loads call the RegisterOnTab with the iframe position tabIndex. If the iframe is located on the default tab the refresh method will be called immediately otherwise crm will trigger the onreadystatechange event which will eventually call the Refresh method. The RegisterOnTab should be called only once per iframe and only from the onload event. Afterwards, if you need to revise the iframe Display call the iframe FetchViewer.Refresh method directly.



I upgraded the FetchViewer class and now it supports parent context (mapping) and grid refresh. You can read more about it here


function OnCrmPageLoad()
{
window.fetchAccounts = new FetchViewer("IFRAME_test1");
fetchAccounts.FetchXml = getFetchXml();
fetchAccounts.LayoutXml = getLayoutXml();
fetchAccounts.Entity = "account";
fetchAccounts.QueryId = "{00000000-0000-0000-00AA-000010001001}";
fetchAccounts.RegisterOnTab(0); //IFRAME ON THE DEFAULT TAB

window.fetchAccounts1 = new FetchViewer("IFRAME_test");
fetchAccounts1.FetchXml = getFetchXml();
fetchAccounts1.LayoutXml = getLayoutXml();
fetchAccounts1.Entity = "account";
fetchAccounts1.QueryId = "{00000000-0000-0000-00AA-000010001001}";
fetchAccounts1.RegisterOnTab(2); //IFRAME ON THE THIRD TAB (index eq 2)

crmForm.all.gi_name.attachEvent( "onchange" , WhenThisFieldChangesAndCallRefresh );
}

function WhenThisFieldChangesAndCallRefresh()
{
//Change the fetchAccounts.FetchXml ( for example ) and Refresh
fetchAccounts.Refresh();
}

function getFetchXml(){
return '<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="true"><entity name="account"><attribute name="name"/><attribute name="address1_city"/><attribute name="primarycontactid"/><attribute name="telephone1"/><attribute name="accountid"/><order attribute="name" descending="false"/><filter type="and"><condition attribute="ownerid" operator="eq-userid"/><condition attribute="statecode" operator="eq" value="0"/></filter><link-entity name="contact" from="contactid" to="primarycontactid" visible="false" link-type="outer" alias="accountprimarycontactidcontactcontactid"><attribute name="emailaddress1"/></link-entity></entity></fetch>';
}

function getLayoutXml(){
return '<grid name="resultset" object="1" jump="name" select="1" icon="1" preview="1"><row name="result" id="accountid"><cell name="name" width="300" /><cell name="telephone1" width="100" /><cell name="address1_city" width="100" /><cell name="primarycontactid" width="150" /><cell name="accountprimarycontactidcontactcontactid.emailaddress1" width="150" disableSorting="1" /></row></grid>';
}

function FetchViewer( iframeId )
{
var Instance = this;
var vDynamicForm;
var m_iframeTab;
var m_iframeDoc;

Instance.Entity = "";
Instance.Iframe = null;
Instance.FetchXml = "";
Instance.QueryId = "";
Instance.LayoutXml = "";

Instance.RegisterOnTab = function( tabIndex )
{
Instance.Iframe = document.getElementById( iframeId );

if( !Instance.Iframe )
return alert( "Iframe " + iframeId + " is undefined" );

m_iframeDoc = getIframeDocument();
var loadingGifHTML = "<table height='100%' width='100%' style='cursor:wait'>";
loadingGifHTML += "<tr>";
loadingGifHTML += "<td valign='middle' align='center'>";
loadingGifHTML += "<img alt='' src='/_imgs/AdvFind/progress.gif'/>";
loadingGifHTML += "<div/><b>Loading View...</b>";
loadingGifHTML += "</td></tr></table>";
m_iframeDoc.body.innerHTML = loadingGifHTML;

if( parseInt( "0" + tabIndex ) == 0 ) Instance.Refresh();
else Instance.Iframe.attachEvent( "onreadystatechange" , RefreshOnReadyStateChange );
}

function RefreshOnReadyStateChange()
{
if( Instance.Iframe.readyState != 'complete' )
return;

Instance.Refresh();
}

Instance.Refresh = function()
{
if( !Instance.Iframe )
return alert( "Iframe " + iframeId + " is undefined" );

m_iframeDoc = getIframeDocument();

Instance.Iframe.detachEvent( "onreadystatechange" , RefreshOnReadyStateChange );

var create = m_iframeDoc.createElement;
var append1 = m_iframeDoc.appendChild;
vDynamicForm = create("<FORM name='vDynamicForm' method='post'>");

var append2 = vDynamicForm.appendChild;
append2(create("<INPUT type='hidden' name='FetchXml'>"));
append2(create("<INPUT type='hidden' name='LayoutXml'>"));
append2(create("<INPUT type='hidden' name='EntityName'>"));
append2(create("<INPUT type='hidden' name='DefaultAdvFindViewId'>"));
append2(create("<INPUT type='hidden' name='ViewType'>"));
append1( vDynamicForm );

vDynamicForm.action = prependOrgName("/AdvancedFind/fetchData.aspx");
vDynamicForm.FetchXml.value = Instance.FetchXml;
vDynamicForm.LayoutXml.value = Instance.LayoutXml;
vDynamicForm.EntityName.value = Instance.Entity;
vDynamicForm.DefaultAdvFindViewId.value = Instance.QueryId;
vDynamicForm.ViewType.value = 1039;
vDynamicForm.submit();

Instance.Iframe.attachEvent( "onreadystatechange" , OnViewReady );
}

function OnViewReady()
{
if( Instance.Iframe.readyState != 'complete' ) return;

Instance.Iframe.style.border = 0;
Instance.Iframe.detachEvent( "onreadystatechange" , OnViewReady );
m_iframeDoc = getIframeDocument();
m_iframeDoc.body.scroll = "no";
m_iframeDoc.body.style.padding = "0px";
}

function getIframeDocument(){
return Instance.Iframe.contentWindow.document;
}

}

OnCrmPageLoad();

Sunday, September 14, 2008

CRM 4.0 Multi Picklist

This post is an extension to my previous post. I have to say I had to agree with the client on this one. Generally when you need a multi picklist you can simply create a new entity to hold the values, create a N:N relationship with the relevant entity and use the left navigation link or display the N:N view in an iframe. But what do you do when you need to put 13 “Multi picklists” on a form. This case is even harder to “plead” when the entity truly holds a single attribute. Eventually I built this multi picklist using this simple js which I hope can save you time if your client chooses not to take the easy way out..

I can't disclose the original entity so I integrated the code on the contact's addresstype attribute for the sake of this post.




Because crm does not support multi value storage for a single field you need to add a new attribute that acts as storage for the user selections.
In the example below the original picklist is new_visa and the new_visa_s is used as “storage”.

The Size property defines the picklist.size / container (TD) row span. The picklist original size is 1 (row span) which mean it will occupy a single row like a regular text field. If you want the picklist to spawn more rows you need to create blank spaces under the picklist field. The code changes the form structure so the picklist does not push the next row elements down as it grows in size.



Finally call the transform method to initiate the transformation form a single to a multi picklist
I only used this on a 1:1 form layout so I can’t say if this works for all form layouts.


function OnCrmPageLoad()
{
var multiPicklist = new MultiPicklist("new_visa","new_visa_s");
multiPicklist.Size = 3;
multiPicklist.Transform();
}

function MultiPicklist( picklistId , storageId )
{
var Instance = this;
var getElem = document.getElementById;
Instance.Picklist = getElem( picklistId );
Instance.Storage = getElem( storageId );
Instance.Size = 1;

Instance.Transform = function()
{
if( !Instance.Picklist )
return alert( "Picklist " + picklistId + " is undefined" );

if( !Instance.Storage )
return alert( "Picklist Storage " + storageId + " is undefined" );

//Hide the storage attribute
hideStorage();
//fix the layout so the picklist won’t push the next row down
fixLayout();
//this is what makes the picklist a multi picklist
Instance.Picklist.multiple = true;
Instance.Picklist.size = Instance.Size * 2;
//Attach to the onsave event
crmForm.attachEvent( "onsave" , Instance.OnFormSave );

//parse the option list
var optionList;
if((optionList = getStorageOptions()) == null)
return;
//re-select the picklist options
for( var i = optionList.length - 1 ; i >= 0 ; i-- )
{
for( var k = Instance.Picklist.options.length - 1 ; k >= 0 ; k-- )
{
var option = Instance.Picklist.options[k];
if( option.value == optionList[i] )
option.selected = true;
}
}
}

Instance.OnFormSave = function()
{
//concatenate the selected options
var optionSummery = "";
for( var k = 0 ; k < Instance.Picklist.options.length ; k++ )
{
var option = Instance.Picklist.options[k];
if( option.selected )
optionSummery += option.value + ",";
}
//remove trailing comma
Instance.Storage.DataValue = optionSummery.replace(/,$/gi,"");
}

function fixLayout()
{
/*
The idea is to find the current picklist position cell in the row
Go down the number of rows as specified by the Size Property
Delete the cells below the picklist and change the picklist cell rowspan attribute
*/
var countTD = 0;
var picklist_d = getElem( picklistId + "_d" );
var picklist_c = getElem( picklistId + "_c" );
var currentTR = picklist_d.parentElement;
var currentTD = picklist_c.previousSibling;

while( currentTD && currentTD.tagName == "TD" )
{
countTD++;
currentTD = currentTD.previousSibling;
}

for( var i = 1 ; i < Instance.Size ; i++ )
{
currentTR = currentTR.nextSibling;
if( currentTR )
{
currentTR.deleteCell(countTD);
currentTR.deleteCell(countTD);
}
}

picklist_d.rowSpan = Instance.Size;
picklist_c.rowSpan = Instance.Size;
}

function getStorageOptions()
{
if( Instance.Storage.DataValue == null )
return null;
return Instance.Storage.DataValue.split(',');
}

function hideStorage()
{
getElem( storageId + "_d" ).style.visibility = "hidden";
getElem( storageId + "_c" ).style.visibility = "hidden";
Instance.Storage.style.visibility = "hidden";
}
}

OnCrmPageLoad();

Saturday, September 13, 2008

CRM 4.0 Dynamic Forms

I have a client that has a “simple” request. He wants to re-arrange the form sections and fields according to a picklist selected value.

When I first heard his request I ruled out any possibility of satisfying his needs since crm forms are static and the form structure is defined at design time. Anyway I returned back to the office and wrote this piece of infrastructure that allows you to move any field or section at will.

If you have a similar requirement this will make your life very easy.

The SwitchF method is responsible for moving the field around. The first parameter is the name of the field to be moved. The second parameter is an anchor which specifies where to move the field to. The code actually swaps the fields so you also need to address the second parameter (field) positioning as well.

The SwitchS method is responsible for moving the sections. Use the FormOrganizer GetSection Method to retrieve a reference to each section and then pass the sections to the SwitchS method. The GetSection method receives the tab and section index in accordance to their position in the DOM structure. note that the indices are zero based so the first tab is at position 0 and the third section is at position 2.

The Infrastructure also takes care of hiding fields you don’t want to see.
The code relies on the fields being available on the form and does not build them for you.

The sample is taken from the contact entity.


function OnCrmPageLoad()
{
//Create a FormOrganizer Object
var frmOrg = new FormOrganizer();
//Swap parentcustomerid lookup with salutation position
frmOrg.SwitchF("parentcustomerid","salutation");
//Swap middlename with lastname and so on...
frmOrg.SwitchF("middlename","lastname");
frmOrg.SwitchF("jobtitle","telephone1");
frmOrg.SwitchF("telephone1","preferredcontactmethodcode");
//Hide pager field
frmOrg.Hide("pager");
//Retrieve the the second section at the first tab
var tab1Sec2 = frmOrg.GetSection( 0 , 1 );
//Retrieve the third section at the second tab
var tab2Sec3 = frmOrg.GetSection( 1 , 2 );
frmOrg.SwitchS(tab1Sec2,tab2Sec3);
}

function FormOrganizer()
{
this.GetSection = function( tabIndex , secIndex )
{
var sec = document.getElementById( "tab" + tabIndex );
return sec.childNodes[0].rows[ secIndex ];
}

this.SwitchS = function( secA , secB )
{
if( !secA || !secB ) return;
secA.swapNode(secB);
}

this.Hide = function( controlId )
{
var getElem = document.getElementById;
var A = getElem( controlId );
var A_d = getElem( A.id + "_d" );
var A_c = getElem( A.id + "_c" );
A_d.style.visibility = "hidden";
A_c.style.visibility = "hidden";
A.style.visibility = "hidden";
}

this.SwitchF = function( controlA , controlB )
{
var getElem = document.getElementById;
var A = getElem( controlA );
var B = getElem( controlB );

if( !A || !B ) return;

var A_d = getElem( A.id + "_d" );
var A_c = getElem( A.id + "_c" );

var B_d = getElem( B.id + "_d" );
var B_c = getElem( B.id + "_c" );

var A_d_colSpan = A_d.colSpan;
var B_d_colSpan = B_d.colSpan;

A_d.colSpan = B_d_colSpan;
B_d.colSpan = A_d_colSpan;

A_d.swapNode(B_d);
A_c.swapNode(B_c);
}
}

OnCrmPageLoad();

Friday, September 12, 2008

CRM Form Validator

A few months back, I did a tech presentation to one of my client’s development team. As we where going over customization, talking about all the amazing features dynamics has to offer out of the box, the pm realized that the platform does not provide a solid validation mechanism that would support their complicated requirements. He was right, obviously, and seemed bothered about it. Of course the dev guys told that this is not a big deal.

Never the less, I already had a solid answer that was part of my infrastructure and so just for the sake of discussion I asked him whether he would you be satisfied with a solution that requires him to write a single line of code per validation request. His answer was obvious, but he wanted proof and so I drew these few lines on his white board:


var frmValidator = new FormValidator(true);
frmValidator.AddRule( "accountnumber" , "^.{10}$" , "Account Number must contain 10 letters" );



Luckily, the pm had development skills and so he immediately understood what I wrote.
The CTO mentioned that “most” fields can’t rely on just a single regular expression validation.
I can’t argue with that, I said, and drew the following punch lines:


//Multiple Validation per field
frmValidator.AddRule( "accountnumber" , "^.{10}$" , "Account Number must contain 10 letters" );
frmValidator.AddRule( "accountnumber" , "[a-z]{4}-[0-9]{2}-[A-Z]{2}" , "Account Number Format should be 'abcd-12-EF'" );
//OR
//Ability to write custom validation functions
frmValidator.AddRule( "name" , KeepEveryoneHappy , "I am not happy" );

function KeepEveryoneHappy()
{
return typeof("and they lived happily ever after") == "string"
}


Did I here a laugh, bad coding joke.

This piece of code is a crucial part of my infrastructure template. I never start coding client side tasks from scratch. You should consider building your own JavaScript template that encompasses all common development tasks. The grand idea is to put all your effort and time (is money) in building your customer’s business logic and save as much time when it comes to technology. That is what a dynamic(s) platform is about.

The FormValidator can either present a single validation message and focus the underline field of summarize all validations into one alert message. This is set by passing the true / false flag to the FormValidator constructor.


var frmValidator = new FormValidator(true);


The FormValidator has a simple hash ( dictionary ) table which enables you to write multiple validation for a single field. So if you can’t or don’t master regex you can accomplish that task by providing a number of simple validadtion expressions instead on just one.


frmValidator.AddRule( "accountnumber" , "^.{10}$" , "Account Number must contain 10 letters" );
frmValidator.AddRule( "accountnumber" , "[a-z]{4}-[0-9]{2}-[A-Z]{2}" , "Account Number Format should be
'abcd-12-EF'" );


If the validation requires a more complex solution or you need to make extra checks that can’t be achieved via regex you can specify a function pointer that makes the check. The function return value must be Boolean.


frmValidator.AddRule( "name" , KeepEveryoneHappy , "I am not happy" );

function KeepEveryoneHappy()
{
return typeof("and they lived happily ever after") == "string"
}


The AddRule method receives the name of the field , a regex or a function pointer and the error message to display when things go wrong.

I hope you find this a useful.
Here is the entire example:


function OnCrmPageLoad()
{
var frmValidator = new FormValidator(true);
//Multiple Validation per field
frmValidator.AddRule( "accountnumber" , "^.{10}$" , "Account Number must contain 10 letters" );
frmValidator.AddRule( "accountnumber" , "[a-z]{4}-[0-9]{2}-[A-Z]{2}" , "Account Number Format should be 'abcd-12-EF'" );

//Ablity to write cusotm validation functions
frmValidator.AddRule( "name" , KeepEveryoneHappy , "Name must be longer then 2 letters" );
}

function KeepEveryoneHappy()
{
return true; //and they lived happily ever after
}



function FormValidator(mode)
{
var Instance = this;

Instance.Mode = mode;
Instance.Keys = [];
Instance.Values = [];
Instance.Summery= "";

Instance.AddRule = function( controlId , validation , errorMessage )
{
if( isNullOrEmpty(Instance.Values[controlId]) )
{
Instance.Keys[Instance.Keys.length] = controlId;
Instance.Values[controlId] = [];
}
var iControlValidations = Instance.Values[controlId].length;
var control = document.getElementById( controlId );
if( isNullOrEmpty( control ) )
return alert( "The Field: " + controlId + " is undefined" );
var rule = new ValidationRule( control , validation , errorMessage );
Instance.Values[controlId][iControlValidations] = rule;
}

function ValidationRule( control , validation , errorMessage )
{
this.Control = control;
this.Validation = validation;
this.Message = errorMessage;
}

function OnCrmPageSave()
{
var valid4Submit = true;
for( var i = 0 ; i < Instance.Keys.length ; i++ )
{
var controlValidations = Instance.Values[Instance.Keys[i]]
for( var j = 0 ; j < controlValidations.length ; j++ )
{
var rule = controlValidations[j];
switch( typeof(rule.Validation) )
{
case "string":
var DataValue = rule.Control.DataValue;
if( DataValue != null )
valid4Submit = rule.Control.DataValue.match( rule.Validation ) != null;
else
valid4Submit = true;
break;
case "function":
valid4Submit = rule.Validation()
break;
}

if( !Instance.Mode && !valid4Submit )
{
alert( rule.Message );
rule.Control.focus();
return ( event.returnValue = valid4Submit );
}
else if( !valid4Submit )
{
Instance.Summery += rule.Message + "\n";
}
}
}

ShowSummery();
return ( event.returnValue = valid4Submit );
}

function ShowSummery()
{
if( !isNullOrEmpty(Instance.Summery) )
{
alert( Instance.Summery );
Instance.Summery = "";
}
}

function Init(){
crmForm.attachEvent( "onsave" , OnCrmPageSave );
}

function isNullOrEmpty( obj ){
return obj == null || typeof(obj) == "undefined" || obj == "";
}

Init();
}

OnCrmPageLoad();

Thursday, September 11, 2008

CRM 4.0 Fields How To Tips

It occurred to me that most of the samples in my blog are more of a “complete solution” or too complicated. Sometimes you’re only looking for a line or two to integrate with your own code.In order to address that issue I have decided to write a series of How-To posts which are simple and usually address a specific requirement.

The how-to posts are categorized by common client side development tasks like CRM Form, CRM Fields, CRM IFRAME, CRM Lookup and so on. Each time something new comes up I’ll simply update the relevant “Repository” How-To post. If you have a specific requirement that you don’t see in my post and you feel that should be then comment on that and if it is missing I’ll share that solution when I can.

This How-To post addresses CRM Field development tasks:

Binding to a field change event.
this method is easier and more productive then putting all your code in each field onchage handler.


crmForm.all.<FieldId>.attachEvent( "onchange" , On<FieldId>Change );

function On<FieldId>Change()
{

}



Disalbe Field

crmForm.all.<FieldId>.Disabled = true; //false


Check if a field is null
Replace the <FieldId> with the proper field name

var fieldIsNull = (crmForm.all..DataValue == null);
alert( fieldIsNull );

CRM 4.0 Tab How To Tips

It occurred to me that most of the samples in my blog are more of a “complete solution” or too complicated. Sometimes you’re only looking for a line or two to integrate with your own code.In order to address that issue I have decided to write a series of How-To posts which are simple and usually address a specific requirement.

The how-to posts are categorized by common client side development tasks like CRM Form, CRM Fields, CRM IFRAME, CRM Lookup and so on. Each time something new comes up I’ll simply update the relevant “Repository” How-To post. If you have a specific requirement that you don’t see in my post and you feel that should be then comment on that and if it is missing I’ll share that solution when I can.


This How-To post addresses CRM Tab development tasks:

Select a specific Tab (First Tab index is 0)


var tabIndex = 2; //Third Tab
var tab = document.getElementById("tab" + tabIndex + "Tab");
if( tab ) tab.click();



Hide Specific Tab (First Tab index is 0)



var tabIndex = 1; //Second Tab
var tab = document.getElementById("tab" + tabIndex + "Tab");
if( tab ) tab.style.display = 'none'; //'inline'

CRM 4.0 Form How To Tips

It occurred to me that most of the samples in my blog are more of a “complete solution” or too complicated. Sometimes you’re only looking for a line or two to integrate with your own code.
In order to address that issue I have decided to write a series of How-To posts which are simple and usually address a specific requirement.

The how-to posts are categorized by common client side development tasks like CRM Form, CRM Fields, CRM IFRAME, CRM Lookup and so on. Each time something new comes up I’ll simply update the relevant “Repository” How-To post. If you have a specific requirement that you don’t see in my post and you feel that should be then comment on that and if it is missing I’ll share that solution when I can.

This How-To post addresses CRM Form development tasks:

Addressing Form Type (crmForm.FormType) in script.
This should be the BASIC TEMPLATE for all your scripts. Since crmForm has more then 1 state (type) your code should always address that.
If for example the create and update Types share the same code than simply put your code in one method and call it from the other.


/* Form Types */
var FormTypes =
{
Undefined : 0,
Create : 1,
Update : 2,
ReadOnly : 3,
Disabled : 4,
QuickCreate : 5,
BulkEdit : 6
}

/* Script Entiry Point */
function OnCrmPageLoad()
{
switch( crmForm.FormType )
{
case FormTypes.Create:OnNewFormLoad();break;
case FormTypes.Update:OnUpdateFormLoad();break;
case FormTypes.ReadOnly:OnReadOnlyFormLoad();break;
case FormTypes.Disabled:OnDisabledFormLoad();break;
case FormTypes.QuickCreate:OnQuickCreateFormLoad();break;
case FormTypes.BulkEdit:OnBulkEditFormLoad();break;
case FormTypes.Undefined:alert("Error");break;
}
}

/* Implement each Form Type you wish to address */
function OnNewFormLoad(){}
function OnUpdateFormLoad(){}
function OnReadOnlyFormLoad(){}
function OnDisabledFormLoad(){}
function OnQuickCreateFormLoad(){}
function OnBulkEditFormLoad(){}

OnCrmPageLoad();



Change Field Required level

/*
Parameter1 - Field scheme name
Parameter2 - is required
*/
crmForm.SetFieldReqLevel( "<FieldId>" , true );


Check if the form has changed ( by user or code ) Since it was loaded

alert( crmForm.IsDirty() );


Binding to crmForm save event
This coding method is easier to manage and more productive then putting your code In the OnSave event box.

crmForm.attachEvent( "onsave" , OnCrmPageSave );

This relates to the former bullet.
The code checks the form validity. If its not valid then The returnValue is set to false and the form save does not fire.


function OnCrmPageSave()
{
var valid4Submit = true;
if( /* Form Is NOT Valid */ )
valid4Submit = false;

return (event.returnValue = valid4Submit);
}


Retrieve Entity Object Type Code

alert( crmForm.ObjectTypeCode );


Retrieve Entity Schema Name

alert( crmForm.ObjectTypeName );


Remove (detach) MS window close alert

crmForm.detachCloseAlert();

Wednesday, September 10, 2008

CRM Lookup Preview


I thought the best way to appreciate some of the samples I posted would be to combine them into a meaningful solution.
I came up with this neat idea that I call “Lookup Preview” much like the grid preview feature.
It started out simple but as the hours flew I decided to make it a generic solution to support all lookup types and common fetch requests.

The “lookup preview” combines the “custom tooltip” and “Ajax using fetch message” posts.
Because the result came out more then I intended it to be (in other words complicated) I decided to make it programmer friendly and build a comfortable API around it. So I won’t dive into the bit and bytes but rather write about how to implement the solution.

I few notes before I start, just in case someone from ms is reading this. I do believe that one of the major disadvantages working with crm 4.0 is the lack of Interactive API on the client side. Ms will either create a complete solution much like the form assistant and lookup control or leave a black hole. As dynamics becomes more and more a platform and less a crm product it requires more entry points for development purposes. I do believe that ms is going that way and If you ask me version 10 will be called dynamics10.0 framework, who would even remember that this was ever a crm application.

Now, at some point, it passed my mind that this could actually be a sellable “add-on”. As it may, the only way to interact with the crmForm is by writing unsupported DOM manipulations. So unless someone at ms is interested in investing in my Lookup Preview the solution is free for all.

I’ll try to update the solution on regular basis and add more features to it when I can. If you’re having any issues please comment on them and I’ll try to response when I can.

Ok, let’s start...

The basic idea behind the lookup preview is the ability to create some sort of fetch that retrieves information related to the current lookup DataValue. The nice thing about FetchXml in v4.0 is that you can also retrieve information from an entity related entities so, for example, if I wanted to show the user business unit on an account owner lookup all I need is to create a fetch that implements the linked entity (business unit) column name. This is actually a 3 levels deep solutions since you can see the current account data with the system user data and all related system user entities. Quite handy I’d say.

The API is very simple to use. The following example refers to the “to” lookup on the email entity.

The “to” lookup, as the regarding and customer lookups, can contain several entity types. Therefore it is mandatory to provide fetch information for each entity type. Although I could have taken the types from the lookup itself I decided that you may choose not to show preview for some entity types and thus made it a public choice.

The OnCrmPageLoad function describes the usage. If you have any questions, again, you are welcome to comment.









function OnCrmPageLoad()
{
/*
Reference the to Lookup, Provide the lookup (control) id.
*/
var PrvToLui = new LookupPreview("to");

/*
Add Account Preview Information ,
returns an account entity wrapper object
*/
var accountEntity = PrvToLui.AddEntity("account");

/*
Add Account Preview Attributes,
Provide lable and schema name for each attribute
*/
accountEntity.AddAttribute("Street","address1_line1");
accountEntity.AddAttribute("City","address1_city");
accountEntity.AddAttribute("State","address1_stateorprovince");
accountEntity.AddAttribute("Zip","address1_postalcode");

/*
Add Related Entity (Primary Contact) Information
AddLinked requires the entity name and
lookup field schema name on the account.
*/

var contactEntity = accountEntity.AddLinked("contact","primarycontactid");
contactEntity.AddAttribute("Primary Contact","fullname");
contactEntity.AddAttribute("Contact Phone","telephone1");

/* ------------------------------------------------------------------ */

//Contact Entity Preview
var contactEntity = PrvToLui.AddEntity("contact");
contactEntity.AddAttribute("Street","address1_line1");
contactEntity.AddAttribute("City","address1_city");
contactEntity.AddAttribute("State","address1_stateorprovince");
contactEntity.AddAttribute("Zip","address1_postalcode");
/* ------------------------------------------------------------------ */

//Lead Entity Preview
var leadEntity = PrvToLui.AddEntity("lead");
leadEntity.AddAttribute("Full Name","fullname");
/* ------------------------------------------------------------------ */

//Systemuser Entity Preview
var userEntity = PrvToLui.AddEntity("systemuser");
userEntity.AddAttribute("Preferred Phone","preferredphonecode");
userEntity.AddAttribute("Main Phone","address1_telephone1");

var buEntity = userEntity.AddLinked("businessunit","businessunitid");
buEntity.AddAttribute("Business Unit","name");
}

function LookupPreview( lookupId )
{
var Instance = this;
Instance.Lookup = document.getElementById( lookupId );

if(isNullOrEmpty(Instance.Lookup))
return;

//Public
Instance.Entities = [];

Instance.AddEntity = function(entityName)
{
var Entity = new Object();
Entity.Name = entityName;
Entity.AddAttribute = function( labelName , attrName )
{
var Attributes = Instance.Entities[entityName].Attributes;
var attribute = new Attribute(entityName , attrName , labelName , attrName);
Attributes[Attributes.length] = attribute;
DisplayAttributes[DisplayAttributes.length] = attribute;
}

Entity.Attributes = [];
Entity.LinkedByName = [];
Entity.LinkedByIndex = [];
Entity.AddLinked = function( lnkEntityName , referencingAttribute )
{
var LinkEntity = new Object();
LinkEntity.Name = lnkEntityName;
LinkEntity.Attributes = [];
LinkEntity.RefAttribute = referencingAttribute;
LinkEntity.AddAttribute = function( labelName , attrName )
{
var attribute = new Attribute(entityName , attrName , labelName , LinkEntity.RefAttribute + "." + attrName);
LinkEntity.Attributes[LinkEntity.Attributes.length] = attribute;
DisplayAttributes[DisplayAttributes.length] = attribute;
}
Entity.LinkedByIndex[Entity.LinkedByIndex.length] = Entity.LinkedByName[name] = LinkEntity;
return LinkEntity;
}
Instance.Entities[entityName] = Entity;
return Entity;
}

Instance.Show = function(dataElement)
{
var control = Instance.Lookup;
var DataValue = control.DataValue;

if( isNullOrEmpty(DataValue) )
return;
if( isNullOrEmpty(Instance.Entities[DataValue[dataElement.Index].typename]) )
return;

TooltipPopup = window.createPopup();

if( !dataElement.PreviewHTML )
{
var ToolTipHTML = "<fieldset style='width:100%;height:100%;border:1px solid gray;background-color: #d8e8ff;filter: progid:DXImageTransform.Microsoft.Gradient(GradientType=0,StartColorStr=#eff3ff,EndColorStr=#c6dfff);padding-left:2px;'><legend style='background-color:#eff3ff;width:100;padding-left:13px;border:1px solid gray;font-size:12px;font-family:tahoma'><b>Preview</b></legend>";
var xmlHttp = CreateXmlHttp();
var xml = "<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\"";
xml += " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"";
xml += " xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">";
xml += GenerateAuthenticationHeader();
xml += "<soap:Body>";
xml += "<Fetch xmlns=\"http://schemas.microsoft.com/crm/2007/WebServices\">";
xml += "<fetchXml>";

var fetchxml = "<fetch version='1.0' output-format='xml-platform' mapping='logical' distinct='false'>";
var DataVal = control.DataValue[dataElement.Index];
var entity = DataVal.typename;
fetchxml += "<entity name='" + entity + "'>";
var attributes = Instance.Entities[entity].Attributes;
for( var i = 0 ; i < attributes.length ; i++ )
fetchxml += "<attribute name='" + attributes[i].Name + "'/>";
fetchxml += "<filter type='and'>";
fetchxml += "<condition attribute='" + entity + "id' operator='eq' value='" + DataVal.id + "'/>";
fetchxml += "</filter>";

for( var i = 0 ; i < Instance.Entities[entity].LinkedByIndex.length ; i++ )
{
var linked = Instance.Entities[entity].LinkedByIndex[i];
fetchxml += "<link-entity name='" + linked.Name + "' from='" + linked.Name + "id' to='" + linked.RefAttribute+ "' visible='false' link-type='outer'>";
for( var j = 0 ; j < linked.Attributes.length ; j++ )
fetchxml += "<attribute name='" + linked.Attributes[j].Name + "'/>";
fetchxml += "</link-entity>";
}
fetchxml += "</entity>";
fetchxml += "</fetch>";

xml += _HtmlEncode(fetchxml);
xml += "</fetchXml>";
xml += "</Fetch>";
xml += "</soap:Body>";
xml += "</soap:Envelope>";

xmlHttp.open("POST", "/mscrmservices/2007/crmservice.asmx", false );
xmlHttp.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
xmlHttp.setRequestHeader("SOAPAction", "http://schemas.microsoft.com/crm/2007/WebServices/Fetch");
xmlHttp.send(xml);

var resultDoc = loadXmlDocument(xmlHttp.responseXML.text);
var previewHtml = "<br style='line-height:2px'/><table width='100%' style='font:12 px tahoma'>";
dataElement.Width = 0;

for( var i = 0 , j = 1; i < DisplayAttributes.length ; i++ )
{
var attribute = DisplayAttributes[i];
if( attribute.Entity != entity ) continue;

var attrNode = resultDoc.selectSingleNode("//" + attribute.XPathName );
var attrValue = (attrNode)? attrNode.text:"";

dataElement.Height = (j++) * 22;
var maxLength = ( attrValue.length + attribute.Label.length ) * 9;
if( dataElement.Width < maxLength )
dataElement.Width = maxLength;
previewHtml += "<tr><td style='padding:3px 0px 0px 0px;color:brown'><nobr>" + attribute.Label + "</nobr></td><td><nobr>" + attrValue + "</nobr></td></tr>";

}

dataElement.Height += 20;

ToolTipHTML += previewHtml;
ToolTipHTML += "</table></fieldset>";
dataElement.PreviewHTML = ToolTipHTML;
}

TooltipPopup.document.body.innerHTML = dataElement.PreviewHTML;

var Position = getControlPostion();
var Left = Position.X + 1;
var Top = Position.Y + 5;

TooltipPopup.show( Left , Top - dataElement.parentElement.scrollTop , dataElement.Width, dataElement.Height , null );
}

Instance.Hide = function()
{
if( TooltipPopup )
TooltipPopup.hide();
}

Instance.OnLookupChange = function()
{
var jump = 0;
if( Instance.Lookup.DataValue != null )
{
var DataElementsLen = Instance.Lookup.parentElement.previousSibling.childNodes[0].childNodes.length;
var DataValuesLen = Instance.Lookup.DataValue.length;
jump = (DataElementsLen == DataValuesLen)? 1:2;

for( var i = 0 ; i < DataValuesLen*jump ; i+=jump )
{
var DataValueElemet = Instance.Lookup.parentElement.previousSibling.childNodes[0].childNodes[i];
if( !isNullOrEmpty(DataValueElemet.onmouseover) ) continue;

DataValueElemet.Preview = Instance;
DataValueElemet.Index = i/jump;
DataValueElemet.onmouseover = function(){
this.Preview.Show(this);
}

DataValueElemet.onmouseout = function(){
this.Preview.Hide();
}
}
}
}

//Private
var TooltipPopup;
var DisplayAttributes;

function Init()
{
DisplayAttributes = [];
Instance.Lookup.attachEvent( "onchange" , Instance.OnLookupChange );
Instance.OnLookupChange(); //First Time
}

function Attribute(entityName,attrName,attrLabel,attrXpathName)
{
this.Name = attrName;
this.Entity = entityName;
this.Label = attrLabel;
this.XPathName = attrXpathName;
}

function getControlPostion()
{
control = event.srcElement;
var Position = new Object();
var controlHeight = control.offsetHeight;
var iY = 0, iX = 0;
while( control != null )
{
iY += control.offsetTop;
iX += control.offsetLeft;
control = control.offsetParent;
}
Position.X = iX + screenLeft;
Position.Y = iY + screenTop + controlHeight;
return Position;
}

function isNullOrEmpty( obj ){
return obj == null || typeof(obj) == "undefined" || obj == "";
}

Init();
}

OnCrmPageLoad();