Wednesday, July 21, 2010

Microsoft Dynamics Marketplace

Microsoft announced the Dynamics Marketplace which will help people find applications from Microsoft Partners to help extend their Dynamics solution. 

Here are the highlights:
http://crm.dynamics.com/DynamicsMarketplace/landing.aspx

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

Wednesday, May 26, 2010

SharedVariables

I came across a good MSDN article about a plugin feature that I personally think most people overlook.  That feature is SharedVariables.  It is a way to pass data from the pre event to the post event in your plugin so that your post event plugin can use the data as needed.  It is probably very rare that you would ever need to use this functionality (I've only used it once out of the dozens upon dozens of plugins I've developed) but it is good to know it exists.

http://msdn.microsoft.com/en-us/library/cc151094.aspx 

Tuesday, May 11, 2010

ActivityParty Property Con't

Last month I made a post about grabbing an ActivityParty property from a DynamicEntity and recently someone on the CRM forums made a post asking how to add an ActivityParty property to a DynamicEntity.  This can be very confusing and not straightforward at all so I'm going to post the snippet that I answered with in hopes to help some people out.

            DynamicEntity email = new DynamicEntity("email");
            // setup email attributes

            DynamicEntity contactParty = new DynamicEntity("activityparty");
            contactParty.Properties.Add(new LookupProperty("partyid", new Lookup("contact", new Guid("Contact ID"))));

            email.Properties.Add(new DynamicEntityArrayProperty("to", new DynamicEntity[] { contactParty}));

Note:  Shan McArthur replied to the post on the forum saying that this becomes easier with the new XRM dlls from the latest SDK.  Hopefully I can play around with these new dlls soon and be able to post some updated snippets.

Sunday, May 9, 2010

Thursday, May 6, 2010

Changing a Form Field Label

A question was posted in the CRM Forums on how to change a form field's label using javascript onload.  One thing people might not consider is that if you just change the innerText of the field label and the field is required then you will override the asterisk image that is next to the label.

To change the field label correctly, we will need to find the label element and change it's text to our new label rather than overriding the innerText.  Below is an image of the DOM of a field label in Dev Toolbar where I'm highlighting the text that we are going to find and change.



Here's the script to do so:

function changeFieldLabel(fieldName, newLabel)
{
     var field = crmForm.all[fieldName + "_c"];
     if (field != null)
          crmForm.all[fieldName + "_c"].firstChild.firstChild.nodeValue = newLabel;
}

Which you can use like so:
changeFieldLabel("name", "New Name");

Thursday, April 29, 2010

Querying N:N Relationships

Native N:N Relationships are not considered Entities in CRM 4, therefore the web services will not support using a RetrieveRequest against the N:N table.  However it is possible to query the table using Fetch XML.  Below is a simple Fetch query to retrieve the records in the "systemuserroles" N:N table.

                   

"resultXml" would look something like this:




and now you can parse through the result to grab the data you need.

Wednesday, April 21, 2010

Delete Plugin "Gotcha"

Today I discovered a nice "gotcha" moment when trying to delete a custom entity record.  This is the error I was seeing:



After trying to use a debugger to step into the delete plugin and failing miserably, I finally discovered that the plugin had an image that was not being used.  Once I removed the image and re-registered the plugin, it triggered successfully. 

Further testing revealed that anytime you register a post image with a delete plugin then your plugin will not even trigger and throw the error shown above.  This makes complete sense because the record will not have a post image as it will be deleted.

Thursday, April 15, 2010

ActivityParty Property

The Properties of a DynamicEntity can be confusing at first but are pretty straightforward after you get the hang of it.  However, one property that can be hard to figure out is the ActivityParty.  It is actually an array of DynamicEntity and each DynamicEntity in that array has a Lookup property called "partyid".  Here's a code snippet for retrieving the "to" from an activity DynamicEntity in a plugin.

Note:  This snippet is just finding the first record in the To attribute.

DynamicEntity activity = (DynamicEntity)context.InputParameters.Properties["Target"];
if (activity.Properties.Contains("to"))
{
     DynamicEntity[] to = (DynamicEntity[])activity.Properties["to"];
     if (to.Count() > 0 && to[0].Properties.Contains("partyid"))
     {
          Lookup partyId = (Lookup)to[0].Properties["partyid"];
          // Run logic
     }
}

Saturday, April 10, 2010

Offline IFrames

Not all of your functionality will be possible in CRM Offline mode.  For example you may have some IFrames that require an internet connection.  For these scenarios you would not want to have the browser display an error to the user.  Just hiding the IFrame may not be sufficient enough either as it may confuse your users as to why it disappeared.  A nice solution in this case is to hide the IFrame as well as provide the user with a reason as to why they can't see it.

Here's a function to hide the IFrame and overlay it with div containing a custom message explaining why the user cannot see the IFrame.

function ReplaceOfflineIFrameWithMessage(iFrame, messageText)
{
    if (iFrame)
    {
        iFrame.style.display = "none";

        var messageDiv = document.createElement("div");
        messageDiv.innerText = messageText;
        messageDiv.textAlign = "center";
        messageDiv.style.fontSize = "12px";
        messageDiv.style.color = "red";
        messageDiv.style.border = "1px solid #6699cc";
        messageDiv.style.padding = "20px";

        document.getElementById(iFrame.id + '_d').appendChild(messageDiv);
    }
}

Then you can use the native CRM function IsOnline to see what mode the user is in and then call this function passing in the desired IFrame to hide and the custom message to display.

if (!IsOnline())
    ReplaceOfflineIFrameWithMessage(crmForm.all.IFRAME_TestIframe, "You cannot access this content offline.  Please click 'Go Online' in the Outlook client to access this content.");

Here's an example of how it looks:



Tuesday, April 6, 2010

How to Prevent Closing an Opportunity

If you ever need to execute some logic when a user clicks "Close Opportunity" from the Opportunity form, then a good technique is to wrap CRM's native close javascript function with some custom javascript.

The following javascript should be ran onload of the form.

First we will declare our function that will return a boolean based on whether the user can close the Opportunity or not.

canCloseOpp = function()
{
     var canClose = false;

     // validate if you can close the opp or not

     return canClose; 
} 
 
Then we find the "Close Opportunity" button by id and if it exists 
we will change the action property which will get executed when
the user clicks the button.  We will first call our custom function 
to see if the user can close the opportunity.  If it returns true we 
will call "complete" which is CRM's native function to close an 
opportunity.  If it returns false we will display an alert explaining
why the Opportunity can't be closed. 
 
var btnClose = document.getElementById('_MIcomplete');
if (btnClose)
{
   btnClose.action = 
      "if (canCloseOpp()) { complete(); }" +
      "else { alert('Opportunity can not be closed.'); }";
}

Monday, April 5, 2010

CRM Pop-ups

Working with CRM day in and day out, you will find that sometimes all the pop-ups can get on your nerves.  I offered a solution to someone on the CRM forums on how to change the way IE works to open pop-ups in a new tab (Which was also blogged about by Jim Glass).

Tools -> Options -> In the tab section, click Settings -> Open pop ups in a new tab

Note:  On a form, custom iframes will be noticeably larger in a full tab than in a pop-up, so expect this to look quite different. Most users will likely keep their IE settings default, so keep this in consideration when developing custom iframes.