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();