Aidan Garnish

Collaboration Not Competition

Book Hashtags, Seth Godin and a New Website

This is a bit off topic for this blog so if you came looking for SharePoint tips look away now!

Last week Seth Godin blogged about using hashtags on Twitter to keep track of the conversations going on around books.

I think that this is a great idea as it allows readers to easily find and engage with people reading the same books and it also provides a way for the author to get involved and to show up at the conversations that their books are provoking.

There seemed to be a couple of issues with this approach though:

  1. How do you find the "official" hashtag for each book? (Assuming that the author/publisher hasn't thought to mention it somewhere in the book)
  2. Twitter search only returns Tweets from the last few days so the tail end of the conversation is soon lost

For those reasons I have created bookhashtags.com. The site acts as a place where readers can come and find the "official" hashtag for their book and also as a place that displays the entire conversation going on around each book hashtag that has been indexed.

What do you think? Is this side conversation a useful addition to your enjoyment of a book? Which books/hashtags would you like to be added? If you have a book/hashtag to add you can add it here.

Using JQuery to round averages in a SharePoint view

I had a question from a user asking whether it was possible to round the Average that is displayed on SharePoint views to 2.d.p.

Out of the box this is not possible but with a little bit of JQuery we can find the average and round it to the required d.p.

<script src="jquery.js"></script>
<script>
$(document).ready(function(){
   $('b').each(function(index){
   var value = $(this).html();
   var num = value.replace('Average = ','');
   var result = Math.round(num*Math.pow(10,2))/Math.pow(10,2);
   $(this).replaceWith('<b>Average = '+result+'</b>');
   });
});
</script>

Dynamically change InfoPath submit connection properties

If you want to change the properties of the data connection that your InfoPath form is using on submit you can do this in code. You might want to do this if you have lots of sites that use the same structure and each site contains a library that holds a certain type of form. Eg. A project site contains a library for project changes that are submitted using an InfoPath form. Each project site has a project changes library but you don't want to have to create a new project change form with a different submit connection for every site.

In the submit event use the following for SharePoint 2007

SPWeb web = SPContext.Current.Web;
FileSubmitConnection dc = (FileSubmitConnection)DataConnections["Submit"];
dc.FolderUrl = web.Url + "/[library name]";
dc.Execute();

e.CancelableArgs.Cancel = false;

In SharePoint 2010 there is also the option of using the ServerInfo class that has a property called SharePointListUrl to provide the current list context which can be used as follows

FileSubmitConnection dc = (FileSubmitConnection)DataConnections["Submit"];
dc.FolderUrl = ServerInfo.SharePointListUrl;
dc.Execute();
e.CancelableArgs.Cancel = false;

SharePoint 2010 SPTimer job won't activate

...and the error you see in ULS viewer is "The SPPersistedObject, XXXXXXXXXXX, could not be updated because the current user is not a Farm Administrator"

Solution can be found here - http://unclepaul84.blogspot.com/2010/06/sppersistedobject-xxxxxxxxxxx-could-not.html - cheers Paul!

Script to turn off remote administration security is:

# AUTHOR: Paul Kotlyar
# CONTACT:
unclepaul84@gmail.com
# DESCRIPTION: sets an option on content web service that allows updating of SP Administration objects such as SPJobDefinition from content web applications
function Set-RemoteAdministratorAccessDenied-False()
{
 # load sharepoint api libs
 [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint") > $null
 [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Administration") > $null

  # get content web service
 $contentService = [Microsoft.SharePoint.Administration.SPWebService]::ContentService
  # turn off remote administration security
 $contentService.RemoteAdministratorAccessDenied = $false
  # update the web service
 $contentService.Update()
  
}

Set-RemoteAdministratorAccessDenied-False

SharePoint 2010 newsfeed is empty

One of the great new features in SharePoint 2010 is the "My Newsfeed" section of My Site.
 
This is similar to the wall on Facebook and allows you to see the activities of your colleagues such as comments they are making and items they are tagging.
 
After installing SharePoint 2010 I added a load of colleagues and then sat back in anticipation of a steady stream of activity but nothing happened. Instead the message,  "There are no activities in your newsfeed. Stay connected by adding colleagues and interests." kept on staring back at me.
 
It turns out there is a timer job that needs to be enabled in order for the news feed to be populated.
 
This can be found in Central Administration -> Monitoring -> Review job definitions -> [User Profile Service Application] - Activity Feed Job
 
Simply click on the link for this timer job and set up the schedule. If you want to see immediate results click the "Run Now" button.

Dynamics CRM - Set default unit on product line

When picking a product on a CRM entity like quote product or order product the user also has to pick a unit of measure for the product. Wouldn't it be useful if CRM filled in the unit of measure field for the user based on the default unit for the product? Of course it would, but it doesn't do this out of the box.

My solution for this involves using a generic handler to get the default unit for a specific product and some JavaScript to call the handler asynchronously.

The JavaScript that is placed in the onchange event of the productid field looks like this:

var lookupItem = new Array;
// Get the lookup for the primarycontactid attribute on the account form.
lookupItem = crmForm.all.productid.DataValue;
if (lookupItem[0] != null)
{
  InitXmlHttp();
  xmlhttp.onreadystatechange= XMLHttpRequestCompleted;
  xmlhttp.open("GET", "../../../TSG.CRM.GenericHandlers/QuoteProductSetDefaultUnits.ashx?
id="+lookupItem[0].id , true );
  xmlhttp.send(null);
}
 
function InitXmlHttp() {
  // Attempt to initialize xmlhttp object
    try
    {
        xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
    }
    catch (e)
    {
        // Try to use different activex object
        try
        {
            xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
        }
        catch (E)
        {
            xmlhttp = false;
        }
    }
    
    // If not initialized, create XMLHttpRequest object
    if (!xmlhttp && typeof XMLHttpRequest!='undefined')
      {     
            xmlhttp = new XMLHttpRequest();
      }    
}
 
function XMLHttpRequestCompleted()
{
        if (xmlhttp.readyState==4)
    {
        try
        {
           var lookupData = new Array();
           var lookupItem = new Object();
           var response = new Array();
           response = xmlhttp.responseText.split(',');
                             
           lookupItem.id = response[0];
           lookupItem.typename = response[1];
           lookupItem.name = response[2];
           lookupData [0] = lookupItem;
           crmForm.all.uomid.DataValue = lookupData ;
         }
        catch (e)
        {
           alert('Something has gone wrong!');
        }
    }
}
 

The generic handler code looks like this:

using System;
using System.Collections;
using System.Data;
using System.Linq;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Xml.Linq;
using System.Text;
using CRM.GenericHandlers.CRMService;
using System.Configuration;
 
namespace CRM.GenericHandlers
{
    /// <summary>
    /// Summary description for $codebehindclassname$
    /// </summary>
    [WebService(Namespace = "http://tempuri.org/")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    public class QuoteProductSetDefaultUnits : IHttpHandler
    {
 
        public void ProcessRequest(HttpContext context)
        {
            string productid = context.Request.QueryString["id"];
 
            try
            {
                CrmAuthenticationToken token = new CrmAuthenticationToken();
                token.AuthenticationType = 0; 
                //add an <appsetting> to web.config for your organisation name
                token.OrganizationName = ConfigurationManager.AppSettings["OrgName"].ToString();
 
                CrmService service = new CrmService();
                service.CrmAuthenticationTokenValue = token;
                service.Credentials = System.Net.CredentialCache.DefaultCredentials;
 
                //create the ColumnSet that indicates properties to be retrieved
                ColumnSet productCols = new ColumnSet();
                ColumnSet uomCols = new ColumnSet();
               
                //set the properties of the ColumnSet
                productCols.Attributes = new string[] { "productid", "defaultuomid" };
                uomCols.Attributes = new string[] { "uomid", "name" };
                
                //retrieve quote
                Guid ProductID = new Guid(productid);
                product product = (product)service.Retrieve("product", ProductID, productCols);
 
                Guid uomID = product.defaultuomid.Value;
                uom productUom = (uom)service.Retrieve("uom", uomID, uomCols);
 
                string productUomName = productUom.name;
 
                string[] arrLookupItem = new string[3];
                arrLookupItem[0] = uomID.ToString();
                arrLookupItem[1] = "uom";
                arrLookupItem[2] = productUomName;
 
               context.Response.Write("{"+uomID+"},uom,"+productUomName); 
 
            }
            catch (SoapException ex)
            {
                context.Response.Write(ex.Message);
            }
 
        }
 
        public bool IsReusable
        {
            get
            {
                return false;
            }
        }
    }
}
 

XmlHttpRequest Cache Busters

The call to a generic handler in the post Using generic handlers in Dynamics CRM seemed to only be firing once. The alert was being displayed to say that the code had run successfully but I could see that after the first call it wasn't copying the entity as it should have.

It turns out this was because the xmlhttprequest was being cached and the fix was fairly simple. By adding an extra parameter to the url called by the request we can trick it into thinking that a completely new call is being made which prevents the cached result from being used.

Simply add the following to the query string: "bustCache=" + Math.random();

Using generic handlers in Dynamics CRM

Sometimes you need to add functionality to Microsoft Dynamics CRM that cannot be easily achieved by simply adding JavaScript to form and field events.

One way to add extra complex functionality to CRM is to use a generic handler .ashx code file.

For example if you want to copy a quote and it's quote products this is not easily achieved using JavaScript and is not possible using the standard CRM workflows.

To make use of a .ashx generic handler is a 3 step process:

  1. Create the .ashx handler that performs the required functions and deploy it to your CRM server
  2. Add a JavaScript function to a form that will call the .ashx and handle the returned result
  3. Update the ISV.config to add a button to the relevant entity so that the user can trigger the functionality

1. Create .ashx handler

To create the handler you will need a new Visual Studio project. Add a Generic Handler (.ashx) code file to the project and then add references to the CRM web services.

Add your code to provide the desired functionality.

Click this link to download the .ashx code files for copying a Dynamics CRM quote 

The application then needs to be deployed to IIS.

2. Add a JavaScript function to call the handler and display the returned result

Add the JavaScript to the onload event of the form where this functionality will be called. In this example of copying a quote add the function to the onload event of the Quote main form.

CopyQuote = function()
{
InitXmlHttp();
xmlhttp.onreadystatechange= XMLHttpRequestCompleted;
// use the url to your generic handler project here - in this case 
//I have deployed the project to the website that is running CRM
xmlhttp.open("GET", http://YourCRMServer/CRM.GenericHandlers/CopyQuote.ashx?id=
+crmFormSubmit.crmFormSubmitId.value , true );
xmlhttp.send(null);
function InitXmlHttp() {
  // Attempt to initialize xmlhttp object
    try
    {
        xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
    }
    catch (e)
    {
        // Try to use different activex object
        try
        {
            xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
        }
        catch (E)
        {
            xmlhttp = false;
        }
    }
    
    // If not initialized, create XMLHttpRequest object
    if (!xmlhttp && typeof XMLHttpRequest!='undefined')
      {     
            xmlhttp = new XMLHttpRequest();
      }
      // Define function call for when Request obj state has changed
      xmlhttp.onreadystatechange=XMLHttpRequestCompleted;
}
function XMLHttpRequestCompleted()
{
        if (xmlhttp.readyState==4)
        {
            try
            {
                alert(xmlhttp.responseText);
            }
            catch (e)
            {
           }
        }
    }
}
 

3. Update ISV.config to add a button to call the CopyQuote() JavaScript

Export the ISV.config file and add the following to the Quote section:

<Button JavaScript="CopyQuote();">
    <Titles>
            <Title LCID="1033" Text="Copy Quote" />
          </Titles>
              <ToolTips>
                <ToolTip LCID="1033" Text="Copy Quote" />
              </ToolTips>
        </Button>
 

Save the updated ISV.config file and import it back into CRM.

The result of this is that you now have a button on the Quote form called Copy Quote. When this button is clicked the JavaScript calls the .ashx file which copies the quote and then sends a message back to the JavaScript to let the user know if the operation has been successful.

Create Dynamics CRM entity mapping for lookup field

When editing attributes on CRM entities it is often necessary to pass that attribute through to the entity in the next stage in the sales process.

For example you add a contact name to the quote entity and you want that contact to be passed through to the order that is created from the quote.

To do this for standard fields it is simply a case of opening the customisation screen for the entity, adding a new attribute and then adding a new mapping to the relevant relationship. In the case of quotes and orders this would be the N:1 quote to order relationship.

Where things become a little bit trickier is if you want to create a mapping between two entities for an attribute that is a lookup. This is because it is not possible to simply add a lookup field as an attribute.

To do this for the case of the contact lookup being passed from quote to order do the following:

  • Create a new N:1 relationship on the order entity. The Primary Entity for the realtionship will be Contact.
  • Give it a display name of Contact and leave all the other fields as default.
  • Click Save and Close - this will have created a new attribute called Contact which we can now use to map the Quote Contact attribute to.
  • Edit the Quote to Order 1:N relationship and add a new mapping between Contact in the source and target entities.
  • Click OK.
  • Now you can add Contact to the forms and views where it needs to be visible.

Getting started with JavaScript in Dynamics CRM 4.0

This is a blog that is usually about SharePoint and SharePoint development but recently I have been starting to do some work with Microsoft Dynamics CRM 4.0. Initially this work was about integrating SharePoint document libraries with CRM but has now gone beyond that into actual customisation and development of CRM itself.

CRM 4.0 has been around for a while now and many companies who have CRM will also have a SharePoint deployment so although it is a bit off topic I'm sure there are plenty of SharePoint people who will find a bit of CRM information helpful.

As with SharePoint there are lots of options for customising CRM. The first option is customisation of existing entities and forms. This allows you to add attributes to the various objects in CRM such as customers or orders. It is also possible to add JavaScript to the forms to add logic, validation or additional calculations into the forms and this is what this post will take a look at. Finally it is possible to add aspx pages or use ashx handlers in conjunction with the CRM web services to create virtually any extra complex business logic or functionality that is required - later posts will deal with this.

When customising CRM forms it is possible to easily add JavaScript to the on load and on save events of each form or to the on change event of any field.

A few hints and tips:

  • To reference an attribute on a form use crmForm.all.attributename.
  • If the attribute is not a string use .DataValue to return the value held in the attribute. e.g. crmForm.all.attributename.DataValue.
  • DataValue should also be used when creating comparison statements. E.g. if(crmForm.all.isapproved.DataValue == true)
  • If you have a calculated field that is set to read only on the form then you will need to make sure that it is set to ForceSubmit = true otherwise it will not be saved. To do this use the following - crmForm.all.attributename.ForceSubmit = true;
  • To hide a field or menu item use crmForm.all.attributname.style.display = "none";
  • To disable a field use - crmForm.all.attributename.Disabled = false; or use true; to disable the field.

It is possible to set the default for a lookup field. The following example script sets a default price list for the price list field:
 

if(crmForm.all.pricelevelid.DataValue == null)
{
//Create an array to set as the DataValue for the price list lookup control. 
var lookupData = new Array(); 
//Create an Object add to the array. 
var lookupItem= new Object(); 
//Set the id, typename, and name properties to the object. 
lookupItem.id = '{0049EC3B-1257-DF11-8A46-00155D025108}'; 
lookupItem.typename = 'pricelevel'; 
lookupItem.name = 'Test Price List'; 
// Add the object to the array. 
lookupData[0] = lookupItem; 
// Set the value of the lookup field to the value of the array. 
crmForm.all.pricelevelid.DataValue = lookupData;
}

Use the following JavaScript to call CRM web services to return information on the current user - this is then used to set a user lookup field to be the current user:

var xml = "" + 
"<?xml version=\"1.0\" encoding=\"utf-8\"?>" + 
"<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:xsi=
\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/
XMLSchema\">" + 
GenerateAuthenticationHeader() +
" <soap:Body>" + 
" <RetrieveMultiple xmlns=\"http://schemas.microsoft.com/crm/2007/WebServices\">" + 
" <query xmlns:q1=\"http://schemas.microsoft.com/crm/2006/Query\" xsi:type=\
"q1:QueryExpression\">" + 
" <q1:EntityName>systemuser</q1:EntityName>" + 
" <q1:ColumnSet xsi:type=\"q1:ColumnSet\">" + 
" <q1:Attributes>" + 
" <q1:Attribute>businessunitid</q1:Attribute>" + 
" <q1:Attribute>firstname</q1:Attribute>" + 
" <q1:Attribute>fullname</q1:Attribute>" + 
" <q1:Attribute>lastname</q1:Attribute>" + 
" <q1:Attribute>organizationid</q1:Attribute>" + 
" <q1:Attribute>systemuserid</q1:Attribute>" + 
" </q1:Attributes>" + 
" </q1:ColumnSet>" + 
" <q1:Distinct>false</q1:Distinct>" + 
" <q1:Criteria>" + 
" <q1:FilterOperator>And</q1:FilterOperator>" + 
" <q1:Conditions>" + 
" <q1:Condition>" + 
" <q1:AttributeName>systemuserid</q1:AttributeName>" + 
" <q1:Operator>EqualUserId</q1:Operator>" + 
" </q1:Condition>" + 
" </q1:Conditions>" + 
" </q1:Criteria>" + 
" </query>" + 
" </RetrieveMultiple>" + 
" </soap:Body>" + 
"</soap:Envelope>" + 
"";

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

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

var resultXml = xmlHttpRequest2.responseXML;
var entityNode = resultXml.selectSingleNode("//RetrieveMultipleResult/BusinessEntities/
BusinessEntity");

var firstNameNode = entityNode.selectSingleNode("q1:firstname");
var lastNameNode = entityNode.selectSingleNode("q1:lastname");
var fullNameNode = entityNode.selectSingleNode("q1:fullname");
var systemUserIdNode = entityNode.selectSingleNode("q1:systemuserid");
var businessUnitIdNode = entityNode.selectSingleNode("q1:businessunitid");
var organizationIdNode = entityNode.selectSingleNode("q1:organizationid");

//Create an array to set as the DataValue for the the lookup control. 
var lookupData = new Array(); 
//Create an Object add to the array. 
var lookupItem= new Object(); 
//Set the id, typename, and name properties to the object. 
lookupItem.id = systemUserIdNode.text; 
lookupItem.typename = 'systemuser'; 
lookupItem.name = fullNameNode.text; 
// Add the object to the array. 
lookupData[0] = lookupItem; 
// Set the value of the lookup field to the value of the array. 
crmForm.all.approvedby.DataValue = lookupData;
crmForm.all.approvedby.ForceSubmit = true;


 

Using JavaScript is a great way to customise CRM as any script you add is saved as part of the form so when you come to deploy them they are included in the export customisations file. In my customisation of CRM I have tried to use this approach wherever possible but sometimes what you want to achieve cannot be done without cracking open Visual Studio and this is what I plan on taking a look at in my next post.