Sunday, June 20, 2010

ForceSubmit and Default Read-Only Lookups

I made quite the discovery the other day with the ForceSubmit property.  I was writing a plugin on pre-create of an entity where I needed to grab a lookup attribute from the entity being created.  I finished the plugin and then tried testing it out.

I went to create a new record for this entity and realized that the lookup I needed to populate was set to read-only in the form customizations.  I used the IE8 dev tool bar to execute some javascript that enabled the lookup and then set the ForceSubmit property to true so it would save the value.  I then populated the lookup and created the record.

I was greeted with this infamous error:
My first instinct was that my plugin wasn't working correctly, so I set a breakpoint but it wasn't getting triggered.  I then created a record without populating this lookup and it hit my breakpoint and worked successfully.  My next thought was that maybe ForceSubmit was the culprit.  To test this theory I enabled this lookup in the form customizations and tried again and it worked successfully.

In conclusion, if you make a lookup read-only using custom javascript, you can then set ForceSubmit to true on the lookup to save the value as needed.  On the other hand, if you default the lookup to read-only in the form customizations then you will not be able to set ForceSubmit to true to save the value.

Wednesday, June 16, 2010

Show Column Total from a CRM Grid

Update: Andriy a33ik Butenko pointed out that my solution won't work on all views because the specific column will probably not be in all of the views. I updated the solution to check if the column exists first, if not it will prompt the user that this functionality can't be done on the selected view.

A user on the CRM forums asked if it was possible to be able to show a column total from highlighted records in a CRM grid. I went ahead and wrote some javascript to implement this functionality. The javascript for this is fairly simple and will be executed from a button in the ISV Config for an entity grid. I will be using the native Opportunity grid and I will be calculating the Est. Revenue column from that grid.

First, we can't do this functionality if our column doesn't exist in the view, so we will create a function to check if our column exists (estimatedvalue in this case).  We will first create a function called 'doesColumnExist':
 
function doesColumnExist(columnName)
{

}

Then find the table that holds all of the column headers:

var gridBar = document.getElementById('gridBar');

Then find all of the headers:


var headers = gridBar.getElementsByTagName('TH');

Then loop through each header and see if the fieldname property matches the column being passed into the function:

for (var i = 0; i < headers.length; i++)
{
        var header = headers[i];
        if (header.fieldname == columnName)
        {
            return true;
        }
}
return false;

So our function now looks like this:

function doesColumnExist(columnName)
{
    var gridBar = document.getElementById('gridBar');
    var headers = gridBar.getElementsByTagName('TH');

    for (var i = 0; i < headers.length; i++)
    {
        var header = headers[i];
        if (header.fieldname == columnName)
        {
            return true;
        }
    }
    return false;
}

Now we need to check if our column exists, if not we alert the user that they need to select a different view to use this functionality:

if (!doesColumnExist('estimatedvalue'))
{
    alert('Cannot total Est. Revenue.  Please select a view that has the Est. Revenue column.');
}
else
{
    // calculate estimatedvalue
}

Then the rest of the code will go in the else statement and we will first get the selected records from the grid:

var selectedRecords = document.getElementById('crmGrid').InnerGrid.SelectedRecords;

Next we initialize a total revenue variable that we will display to the user and then loop through the selected records.

var totalRevenue = 0;
for (var i = 0; i < selectedRecords.length; i++)
{

}

In the for loop above we will first grab the third index of the current selected record, which is essentialy the TR element of the grid.

var record = selectedRecords[i][3];

Then we will grab the 5th child of the record, in which this case is the Est. Revenue column that we are totaling.  As this is a money field, we need to replace the dollar sign from the value of the cell in order to parse the value so we can get a sum:

var revenue = record.childNodes[5].innerText.replace('$', '');

Next is the tricky part.  Our value will have some hidden spaces in front and back of it.  Therefore we need to escape the value and then replace the special characters so we can finally parse the value to a float:

var escapedRevenue = escape(revenue).replace('%u202D', '').replace('%u202C', '');

Now that we have the unformatted value, we can parse it to a float and add it to our total:

totalRevenue += parseFloat(escapedRevenue);

Lastly, outside of our loop we can display the total to the user:

alert(totalRevenue);

The finished javascript should look like this:

function doesColumnExist(columnName)
{
    var gridBar = document.getElementById('gridBar');
    var headers = gridBar.getElementsByTagName('TH');
    for (var i = 0; i < headers.length; i++)
    {
        var header = headers[i];
        if (header.fieldname == columnName)
        {
            return true;
        }
    }
    return false;
}

if (!doesColumnExist('estimatedvalue'))
{
    alert('Cannot total Est. Revenue.  Please select a view that has the Est. Revenue column.');
}
else
{
    var selectedRecords = document.getElementById('crmGrid').InnerGrid.SelectedRecords;
    var totalRevenue = 0;
    for (var i = 0; i < selectedRecords.length; i++)
    {
        var record = selectedRecords[i][3];
        var revenue = record.childNodes[5].innerText.replace('$', '');
        var escapedRevenue = escape(revenue).replace('%u202D', '').replace('%u202C', '');
        totalRevenue += parseFloat(escapedRevenue);
    }
    alert(totalRevenue);
}

Now that the javascript is written, we can add it to the Opportunity's grid in the ISV Config:

<Entity name="opportunity">
 <Grid>
  <MenuBar>
   <Buttons>
    <Button JavaScript="function doesColumnExist(columnName) { var gridBar = document.getElementById('gridBar'); 
         var headers = gridBar.getElementsByTagName('TH'); for (var i = 0; i &lt; headers.length; i++) { 
         var header = headers[i]; if (header.fieldname == columnName) { return true; } } return false; } 
         if (!doesColumnExist('estimatedvalue')) { 
         alert('Cannot total Est. Revenue. Please select a view that has the Est. Revenue column.'); } 
         else { var selectedRecords = document.getElementById('crmGrid').InnerGrid.SelectedRecords; 
         var totalRevenue = 0; for (var i = 0; i &lt; selectedRecords.length; i++) { 
         var record = selectedRecords[i][3]; var revenue = record.childNodes[5].innerText.replace('$', ''); 
         var escapedRevenue = escape(revenue).replace('%u202D', '').replace('%u202C', ''); 
         totalRevenue += parseFloat(escapedRevenue); } alert('Total Selected Revenue: $' + totalRevenue); }">
     <Titles>
      <Title LCID="1033" Text="Total Revenue" />
     </Titles>
     <ToolTips>
      <ToolTip LCID="1033" Text="Total Revenue" />
     </ToolTips>
    </Button>
   </Buttons>
  </MenuBar>
 </Grid>
</Entity>


Note:  I encoded the javascript as it is being placed inside an XML attribute in the ISV Config.

It should look like the image below:

And when we click the button:

Wah-la!

Thursday, June 10, 2010

ForceSubmit Property

There are a lot of scenarios where you need to automatically populate a field when it is read-only or will be set read-only dynamically.  In this scenario you would want the value to be saved when the user clicks Save, but by default a field that is read-only will not submit.  To get a read-only field to submit, you can set the ForceSubmit property of the field to true using javascript:

crmForm.all.name.ForceSubmit = true;

What ForceSubmit actually does is always submit that attribute to be saved, whether or not it was actually modified on the form.  One thing to note is that if you have a plugin that is filtering on an attribute where ForceSubmit is set to true, then your plugin will always be triggered when the user saves the form.  Also if you set ForceSubmit to true on ownerid then it will always execute an Assign message whether or not the owner was actually changed.

Because of the way ForceSubmit works, the best way to use it would be to only set it if the field was actually changed (unless you want the field to always submit).



Monday, June 7, 2010

Simple CRM Tools

A colleague of mine recently turned me onto some very useful tools that are simple and easy use. Often times as a CRM developer, we find ourselves needing either the entity type code or the GUID of the record we’re on.  We can easily obtain this information by creating a bookmark that displays either the GUID or the entity type code, directly from the form we’re on.  As an added benefit, we can also set the values to our clipboard so they can be pasted as needed.

To create these bookmarks, just browse to any CRM page and add it to your favorites.  Then, you can edit the bookmark and set the URL to the javascript you would like executed.



Here's the javascript to do so:


Entity Type Code:
javascript:if (crmForm.ObjectTypeCode){clipboardData.setData("Text", crmForm.ObjectTypeCode.toString()); alert(crmForm.ObjectTypeCode);}

Entity ID:
javascript:if (crmForm.ObjectId){clipboardData.setData("Text", crmForm.ObjectId.toString()); alert(crmForm.ObjectId);} else { alert('No id yet!'); }


Another useful bookmark is to find the Fetch XML used by an Advanced Find query.  To do this, create a bookmark using the below javascript, then create a query in advanced find and click Find.  Once your results are displayed, click the bookmark you just created to display the Fetch XML and copy it to your clipboard.


Fetch XML:
javascript:if (document.getElementById('fetchxml')){clipboardData.setData("Text", document.getElementById('fetchxml').value.toString()); alert(document.getElementById('fetchxml').value);}