As promised, here is the code for the CRM Reading Pane add-on as promised, albeit a bit later in the week than I would have hoped. For now I've chosen to attach the Visual Studio project as a zip file. Next week I'll look into putting it on a hosted revision control system.
To use the Reading Pane, you will need to open up the Visual Studio solution file and search for all instances of "MODIFY:" These will be areas of the project where you will have to change the code to reflect your own CRM server address, organization name, etc. In addition, you will need to modify your Account entity to create a new tab with an Iframe to host the Reading Pane. The settings for the Iframe are shown below. Download - Code for Reading Pane  
Continue Entry»

Normally, when reading or searching for a particular CRM Activity, a user has to manually open up Activities one by one in new popup windows. This can turn into a very aggravating experience as system resources must be allocated to open each new window, leading to a delay in rendering each Activity. What if you could read and page through the Activities of an Entity similar to the reading pane in Outlook?
 In next week's post I'll show you how to bake a C# ASP.net page, blend in a TSQL query, and sprinkle some javascript spice to treat your users to an enhanced Activity searching and browsing experience. I've deployed this Activity Reading Pane feature to my company with great success! Users have marveled at the newfound ease of searching within the Activities of a particular Account or Contact. See you next week. 
Continue Entry»

During the course of CRM development, you'll inevitably need to test out security roles and permissions for users. To accomplish this, start off by creating a new Active Directory user explicitly for testing. An Active Directory account is required because Dynamics CRM uses AD to create new users. So, if a user does not exist in AD, a new CRM user account cannot be created for him. Once an AD account has been created, create a new CRM user account for the test user. Assign the roles to the test user that you would like to experiment.
Next, I would normally initiate a new Remote Desktop session to another computer and login as the CRM test user but recently a co-worker introduced me to Window's "Run as..." command.  The "Run as..." command allows you to locally run programs using the Active Directory credentials of another user. Impersonating another user on your local computer is a time saver and negates the need to have another computer set up just for testing purposes.
Continue Entry»

Experimental Feature: This feature is more of a proof of concept and is not ready for production environments.
A good practice of CRM is to keep up to date with news on your customers which can lead to interesting talking points. I thought it would be useful to create a context sensitive Iframe for Leads or Accounts, which would pull in the latest headlines based on the name of a customer. Now, this would only work for bigger, more well known customers, as their names are more likely to show up in the news.
I started off by looking for the RSS feeds of prominent news organizations including Reuters and Bloomberg, hoping to create contextual filters by using Yahoo Pipes. Haplessly, I found out that Bloomberg discontinued their RSS feed some time in late 2006. While looking at the Reuters feed API, I realized pulling RSS feeds from individual sites wasn't enough, what I really needed was a news aggregator. I came across Google News, which turned out to be exactly what I needed. Google News aggregates content from thousands of sources, distills similar articles and gives the ability to sort by article publication date. Checking the Google News documentation revealed that a Google News widget was released in February 2009. I decided to take this widget for a spin. The code below was modified from the widget sample code to grab the name of an Account as the query parameter. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="content-type" content="text/html; charset=utf-8"/> <title>Google News</title> <script type="text/javascript" src="http://www.google.com/jsapi"></script> <script type="text/javascript"> google.load("elements", "1", {packages : ["newsshow"]}); var newsTopic = window.parent.crmForm.all.name.value; //get name of account from parent frame function onLoad() { // Change the topic to business var options = { "queryList" : [ { "title" : newsTopic, //title of news box "q" : newsTopic, //query parameters "scoring" : "d", //order results by date "rsz" : "large", //return large result set (8 entries max) "ned" : "us" //limit results to US } ], "linkTarget" : "_blank" } var content = document.getElementById('content'); var newsShow = new google.elements.NewsShow(content, options); }
google.setOnLoadCallback(onLoad); </script> </head> <body style="font-family: Arial;border: 0 none; background-color: #EAF3FF"> <div id="content"></div> </body> </html>
Save this code into a file named GoogleNews.html and place it into "C:Program Files\Microsoft Dynamics CRM\CRMWeb\ISV\News" the ISV folder of your CRM 4.0 on premise install. By default, a 'News' folder does not exist so please create one. Next, go to Settings->Customizations->Customize Entities->Account->Form and add an Iframe onto the page. Give it the name and settings as shown below.  Publish the changes to Account and you should begin to see news items stream on Account pages. Unfortunately during my testing I found some flaws in this approach for pulling in news, namely:
- The news results sometimes displayed information unrelated to the Account. An Account named "American Airlines" would return results with both "American" and "Airlines" which does not necessarily relate the Account name.
- News results are limited to 8 stories
- News would sometimes show up in foreign languages, even with the parameter set for only US stories
 The Google News widget definitely has potential but for now it is not ready for production environments. On a related note, I noticed that the Google Finance pages tend to to use some other algorithm to determine the relevancy of an article to a company and as a result have more accurate articles. Now if there was some way to pull in the news stream from the Google Finance pages, I'd be the first one to sign up. 
Continue Entry»

Warning: This customization is unsupported by Microsoft. Make sure you back up your files in case any undesired affects happen. Research any unsupported changes and test them on your development environment before making any changes to production!
One of the limitations of custom entities created in CRM 4.0 is the inability to perform an entity rollup to show all of a custom entity's activities and the activities of its child entities. A working example of this rollup behavior is exhibited when you look at the Activity or History page for an Account entry. You will see all the Activities for the Account's child record as well as all the Activites for the Account's Contacts(its child entity).
To overcome this limitation, I did some research on how the Activity and History pages for Accounts generate the rollup. I fired up SQL Server Profiler to intercept any queries made to the SQL Server database while I loaded an Account's Activity page within Internet Explorer. The profiler showed that the stored procedure p_RollupByAccount performed all the heavy lifting. The p_RollupByAccount creates a temporary table called #RollupIds which stores all the GUIDs for the Account and its child Contacts and then a subsequent query pulls in all the activities for GUIDs in this table. Next, I created a custom entity called Manager (cc1_manager) and created a 1:N relationship to Contact (cc1_manager_to_contact) as shown below. Note that the Relationship Attribute is titled cc1_parentmanagerid, which we will use later.  Open up the p_RollupByAccount stored procedure and create a backup as we'll be making some changes. The original code has this section at the beginning of the stored procedure to find the currently opened Account, its sub Accounts and their Contacts. declare @Level int set @Level = 1 while (@@ROWCOUNT != 0 and @Level <= 5) BEGIN set @Level = @Level + 1 insert into #RollupIds (RollupId) ( select distinct AccountId from AccountBase acc (NOLOCK) join #RollupIds ri on ( acc.ParentAccountId = ri.RollupId ) where acc.AccountId not in ( select RollupId from #RollupIds ) ) END
insert into #RollupIds (RollupId) ( select distinct ContactId from ContactBase (NOLOCK) where AccountId = @AccountId )
We replace the section above with our modified code below. The code starts by checking to see if the @AccountId GUID passed in is in the cc1_managerBase table. If it is, we'll know it's a Manager GUID and look for Contacts that have a cc1_parentmanagerid set to the Manager GUID. If the GUID passed in is in the AccountBase table, we'll know it is an Account and process it using the original code. declare @Level int set @Level = 1 -- ModifiedSQL: Conditional logic to check if passed in @AccountId guid is the guid of a Manager IF exists (SELECT cc1_managerId FROM cc1_managerBase WHERE cc1_managerId = @AccountId) Begin -- ModifiedSQL: custom code to add managerID to the Rollup table -- @Level has been removed as we have not set a Manager to Manager relationship to establish a Manager/Sub Manager hierarchy while (@@ROWCOUNT != 0) BEGIN insert into #RollupIds (RollupId) ( select distinct cc1_managerId from cc1_managerBase acc (NOLOCK) -- ModifiedSQL: look in custom entity table cc1_managerBase where acc.cc1_managerId not in ( select RollupId from #RollupIds ) ) END insert into #RollupIds(RollupId) ( select distinct c.ContactId from ContactBase as c (NOLOCK) join dbo.ContactExtensionBase as ceb (NOLOCK)ON c.ContactId = ceb.ContactId join #RollupIds ri on ( ceb.cc1_parentmanagerid = ri.RollupId ) where not exists( select RollupId from #RollupIds where c.ContactId = RollupId ) ) End -- ModifiedSQL: Conditional logic to check if passed in @AccountId guid is the guid of an Account If exists (SELECT AccountID FROM AccountBase WHERE AccountID = @AccountId) Begin -- original code for account while (@@ROWCOUNT != 0 and @Level <= 5) BEGIN set @Level = @Level + 1 insert into #RollupIds (RollupId) ( select distinct AccountId from AccountBase acc (NOLOCK) join #RollupIds ri on ( acc.ParentAccountId = ri.RollupId ) where acc.AccountId not in ( select RollupId from #RollupIds ) ) END insert into #RollupIds (RollupId) ( select distinct ContactId from ContactBase (NOLOCK) where AccountId = @AccountId ) End
Now that we have the database half completed, we'll move onto the modifications to the UI. By design, Custom Entities call a different aspx page to pull up all the Activitiy and History items. Whereas Accounts call the page "/sfa/accts/areas.aspx" Custom Entities call "userdefined/areas.aspx" Unfortunately the page used by Custom Entities does not use a stored procedure to generate the entity rollup list, since entity rollups are not a feature of custom entities. This is the reason why I've chosen to use the p_RollupByAccount stored procedure and repuropose "/sfa/accts/areas.aspx" to work for both Accounts and our Custom Entity Managers. Navigate to the Customizations->Manager->Form->Form Properties->OnLoad Edit page to switch out the old "userdefined/areas.aspx" activity and history page with "/sfa/accts/areas.aspx" The javascript code below locates the "Activity" and "History" elements on the DOM of the page and replaces their onclick behaviour to open up "/sfa/accts/areas.aspx" instead. Since "/sfa/accts/areas.aspx" calls p_RollupByAccount by feeding in the GUID of the currently open object(be it an Account or a Manager), it'll grab the list of activity or history items just as we planned. var navActivities; var navActivityHistory; navActivities = document.all.navActivities; navActivityHistory = document.all.navActivityHistory; if (navActivities != null) { navActivities.onclick = function() { loadArea('areaActivities'); document.all.areaActivitiesFrame.src="/sfa/accts/areas.aspx?oId=" + crmForm.ObjectId + "&oType=1&security=852023&tabSet=areaActivities"; };} else { alert("navActivities Not Found"); } if (navActivityHistory != null) { navActivityHistory.onclick = function() { loadArea('areaActivityHistory'); document.all.areaActivityHistoryFrame.src="/sfa/accts/areas.aspx?oId=" + crmForm.ObjectId + "&oType=1&security=852023&tabSet=areaActivityHistory"; };} else { alert("navHistory Not Found"); }
Publish the changes to the Manager entity and you now have the ability to have activity entity rollups for custom entities. Use it wisely!
Continue Entry»

Dialing phone numbers by manually entering in numbers feels like an anachronism in this day and age. Fortunately for those office equiped with Cisco IP Phones, we can help make this practice a thing of the past. By creating a custom dial ActionButton and using javascript to create an HTTPS request to our Cisco Unified Communications Manager server, we can bake click to dial functionality for phone numbers, right into Dynamics CRM 4.0.
At work, we recently rolled out Cisco's Unified Personal Communicator(UPC) to much acclaim. UPC creates a sort of buddy list of your co-workers and allows you to quickly dial a colleague by simply double clicking on their name in the list. This is made possible with the tight integration between UPC and Cisco's IP Phones. I became enamored with the ease with which dialing co-workers had become and wanted to explore the possibility of adding this functionality to our own Dynamics CRM 4.0 system by leveraging existing resources. After digging around on the Cisco site, I came across the Cisco Web Dialer, an API which allows click to dial functionality through HTTPS or SOAP requests. The Web Dialer functionality can be implemented by first enabling the feature in Cisco Unified Communications Manager. Then, simply use javascript to open a new window pointing to "https://server:port/webdialer/Webdialer?destination=" where 'server' and 'port' are the Communications Manager server and port and 'destination' is the phone number you want to dial. Be careful to HTML escape the phone number, in case any parenthesis or dashes are present. However, Web Dialer intelligently strips these characters out when the call is actually placed and should append the dial out code to your phone number.  The first time you make a call using Web Dialer, it will present you with a login screen asking you to select a device to use to place the call. After these settings are in place, WebDialer generate a cookie for Internet Explorer. As long as the cookie is present, you won't be presented with these login screens, allowing you to initiate a call right after clicking the dial ActionButton. The javascript code below creates an ActionButton which I detailed in a previous post. The code has been modified to add the WebDialer functionality. I've used the attribute telephone1 (Main Phone) for this example. // //Add Phone icon and enable click to dial using IP Phone // function loadjscssfile(filename, filetype){ if (filetype=="js"){ //if filename is a external JavaScript file var fileref=document.createElement('script') fileref.setAttribute("type","text/javascript") fileref.setAttribute("src", filename) } else if (filetype=="css"){ //if filename is an external CSS file var fileref=document.createElement("link") fileref.setAttribute("rel", "stylesheet") fileref.setAttribute("type", "text/css") fileref.setAttribute("href", filename) } if (typeof fileref!="undefined") document.getElementsByTagName("head")[0].appendChild(fileref) } //load some styles to display link image and hovering link image loadjscssfile(location.host + "/ISV/styles/ipphone.css", "css")
var phoneTD = document.getElementById('telephone1_d'); //get td element var callIMG = document.createElement("img"); var callA = document.createElement("a");
//create img overlay to show "Dial number using Cisco Phone" when icon is hovered and store javascript to open link callIMG.style.width = "22px"; callIMG.style.height = "19px"; callIMG.setAttribute("title", "Dial number using Cisco Phone"); callIMG.setAttribute("alt", "Dial number using Cisco Phone"); callIMG.setAttribute("src", location.host + "/ISV/styles/blank.gif"); callIMG.onclick = function() {launchWebDialerServlet(crmForm.all.telephone1.value);return false;}; callIMG.style.visible = false;
//create dial ActionButton callA.setAttribute("href", "#"); callA.style.position = "absolute"; callA.className = "dialNumber"; callA.style.right = "3px"; callA.style.top = "4px"; callA.appendChild(callIMG);
phoneTD.style.position = "relative"; phoneTD.appendChild(callA);
//specify dimensions for web dialer and open window function launchWebDialerWindow( url ) { webdialer=window.open( url, "webdialer", "status=no, width=420, height=300, scrollbars=no, resizable=yes, toolbar=no" ); } //call Web Dialer service, replace 'server' and 'port' with the server name of Cisco Communication Manager and appropriate port function launchWebDialerServlet( destination ) { url = 'https://server:port/webdialer/Webdialer?destination=' + escape(destination); launchWebDialerWindow(url); }
The images and files used in the sample can be found here: Download ZipMake sure to put these files in "C:Program Files\Microsoft Dynamics CRM\CRMWeb\ISV\styles" the ISV folder of your CRM 4.0 on premise install. By default, a 'styles' folder does not exist so please create one. Adding click to dial functionality within CRM 4.0 is a relatively painless procedure and your users and customers should find the dialing functionality infinitely useful.
Continue Entry»

One disadvantage of using Internet Explorer as a browser as opposed to say Firefox is the absence of a built in client side spell checker. With CRM 4.0 working exclusively with Internet Explorer, spell checking becomes more than just a convenience but a necessity since Activities entered into the CRM are viewable by co-workers throughout your organization. One creative spell checking solution which utilizes Microsoft Office's spell checking capabilities through an ActiveX control is outlined on CRM MVP Jim Wang's blog. However, this solution requires some relaxation of ActiveX control security within IE in addition to unsupported modifications to the edit.aspx page on your CRM server. An alternative is to install the free lightweight IE addon, IE7Pro. IE7Pro is not affiliated with Microsoft but is built and maintained by Innoshock, a small group of independent developers.
Much like Firefox, IE7Pro generates revenue by creating a Google search bar and generating search referral traffic. This allows Firefox and IE7Pro to operate advertisement free since Google pays them for any link ads you click through your searches. As a result, you the end user get a great suite of add-ons for Internet Explorer. Using the IE7Pro search bar is optional and can be turned off within the settings. You'll want to configure IE7Pro as shown in the screen shot below, since in this post we are only interested in the spell checking feature.  Some of the pros and cons to using IE7Pro over Jim Wang's Microsoft Office solution are outlined here: Pros for using IE7Pro:
- Spell check as you type
- Fully functional and free of charge
- No chance of breaking in future CRM update rollups and upgrades
- Does not use ActiveX, so no need to relax ActiveX security settings in IE
- Non technical, no coding required, easy button click install
- Spell checking not limited to just CRM 4.0
Cons for using IE7Pro:
- No suggestion list for incorrectly spelled words
- Must individually install/maintain on user computers
- Might not catch all spelling errors(missed my errors with text and numbers e.g. "gr8")
I've found IE7Pro to be great at what it does, simple IE spell checking and your CRM 4.0 users should too!
Continue Entry»
|
About Me
Henry Bow
I'm a programmer analyst working in sunny Orange Country, California. Since the beginning of 2008, I've been developing on the MS Dynamics CRM 4.0 platform. This blog will help me jot down some of the tips and neat features I've developed along the way while give me a chance to dabble into the curious world of analytics and SEO.
hbow27@gmail.com
Feed Rss
|