Saturday, March 27, 2010

MSCRM 4.0 - Remove 'Add Existing xxxxx to this record' button - Another Approach

Microsoft Dynamics CRM users are often confused by the "Add Existing xxxxx to the record" button in the associated views. It's very common in your CRM projects, that you, as a CRM pro, are asked by your CRM users to have this button removed from the interface.

For instance, you could possibly be asked to remove "Add Existing Contact" button from account's entity form's Contacts associated view, as shown below.
CRM Associated View

Solution for Standard CRM Associated View

In order to get this done with a standard CRM associated view, you may simply copy the following script to the onLoad event of account entity's form.
/**
 * Hide "Add Existing xxxxx button" in a CRM associated view.
 * @author Daniel Cai, http://danielcai.blogspot.com/
 *
 * Parameters:
 * @param navItemId: LHS navigator's HTML element ID of the associated view.
                     It usually starts with "nav".
 * @param relName:   The relationship name that the associated view represents.
 */
function hideAddExistingButton(navItemId, relName) {  
    var clickActionPattern =  /loadArea\(['"]{1}([A-Za-z0-9_]+)['"]{1}(, ?['"]\\x26roleOrd\\x3d(\d)['"])*\).*/; 
    var iframe
      , roleOrd;  
 
    var removeAddExistingButton = function() {  
        var frameDoc = iframe.contentWindow.document;  
        if (!frameDoc) return;  
 
        var grid = frameDoc.all['crmGrid'];  
        if (!grid) return;  
 
        var otc = grid.GetParameter('otc');

        // Locate the "Add Existing" button using its magic id.  
        var btnId = (!roleOrd)
                  ? '_MBtoplocAssocOneToMany' + otc + relName.replace(/_/g, "")
                  : '_MBtoplocAssocObj' + otc + relName.replace(/_/g, "") + roleOrd;
  
        var btn = frameDoc.getElementById(btnId);  
        if (btn) {  
            btn.parentNode.removeChild(btn);  
        }  
    };  
 
    var onReadyStateChange = function() {  
        if (iframe.readyState === 'complete') {  
            removeAddExistingButton();  
        }  
    };  
 
    (function init() {  
        if (!crmForm.ObjectId) return;  
 
        var navItem = document.getElementById(navItemId);  
        if (!navItem) return;  
 
        var clickAction = navItem.getAttributeNode('onclick').nodeValue;  
        if (!clickAction || !clickActionPattern.test(clickAction))  
            return;  
 
        var areaId = clickAction.replace(clickActionPattern, '$1');  
        roleOrd = clickAction.replace(clickActionPattern, '$3');
 
        navItem.onclick = function loadAreaOverride() {  
            if (!roleOrd)
                loadArea(areaId);
            else
                loadArea(areaId, '\x26roleOrd\x3d' + roleOrd);

 
            iframe = document.getElementById(areaId + 'Frame');  
            if (!iframe) return;  
 
            iframe.attachEvent('onreadystatechange', onReadyStateChange);  
        }  
    })();  
}  

hideAddExistingButton('navContacts', 'contact_customer_accounts');
As documented in the code's comment, you will need to provide two parameters to call the JavaScript function.
  1. navItemId, the navigator HTML element ID of the associated view. You can easily find the ID using IE's developer tools as shown below.
    CRM Associated View - NavItem
  2. relName, the relationship name that the associated view represents. In our previous example, you can find the relationship name as shown below.
    Account-Contact Relationship
As mentioned previously, the script should be copied to the onLoad event for the form of the primary entity in the 1:N (one-to-many) relationship that the associated view represents, when you are working with different entity.

Solution for Associated View Loaded in IFrame

After you have implemented the above code, your CRM users may come to you saying, "We like that the Add Existing button has been removed, thanks for that, but..., can we move the associated view to the form and we still want to have that button removed? " Does that just happen so often in our day-to-day programming life? I guess you wouldn't be the only developer in the world that deals with the constant software change every day. At the end of day, you would never want to let your customer down, so you will be looking for a new solution. Here I have it prepared for you.

As I have previously implemented a snippet of script to handle moving associated view to IFrame field on CRM form, I am going to add a few lines of code to the original code so it serves both purposes now.
/**
 * Load an associated view into an IFrame, hide it from LHS navigation menu,
 * and remove "Add Existing" button in the associated view. 
 * @author Daniel Cai, http://danielcai.blogspot.com/
 * 
 * Parameters:
 * @param iframe:      The IFrame's object, e.g. crmForm.all.IFrame_Employer_Address
 * @param navItemId:   LHS navigator's HTML element ID of the associated view.
                       It usually starts with "nav".
 * @param relName:     The relationship name, this parameter is only required
 *                     when you want to remove "Add Existing" button.
 */
function loadAssociatedViewInIFrame(iframe, navItemId, relName)
{
    var clickActionPattern =  /loadArea\(['"]{1}([A-Za-z0-9_]+)['"]{1}(, ?['"]\\x26roleOrd\\x3d(\d)['"])*\).*/;
    var roleOrd;

    var getFrameSrc = function (areaId)
    {
        var url = "areas.aspx?oId=" + encodeURI(crmForm.ObjectId);
        url += "&oType=" + crmForm.ObjectTypeCode;
        url += "&security=" + crmFormSubmit.crmFormSubmitSecurity.value;
        url += "&tabSet=" + areaId;
        url += (!roleOrd) ?  "" : "&roleOrd=" + roleOrd;

        return url;
    };

    var removeAddExistingButton = function(frameDoc) {
        if (!frameDoc || !relName) return;

        var grid = frameDoc.all['crmGrid'];
        if (!grid) return;

        var otc = grid.GetParameter('otc');
        
        // Locate the "Add Existing" button using its magic id.
        var btnId = (!roleOrd)
                  ? '_MBtoplocAssocOneToMany' + otc + relName.replace(/_/g, "")
                  : '_MBtoplocAssocObj' + otc + relName.replace(/_/g, "") + roleOrd;
        var btn = frameDoc.getElementById(btnId);
        if (btn) {
            btn.parentNode.removeChild(btn);
        }
    };

    var onReadyStateChange = function() {
        if (iframe.readyState === 'complete') {
            var frameDoc = iframe.contentWindow.document;
            removeAddExistingButton(frameDoc);

            // Remove the padding space around the iframe
            frameDoc.body.scroll = "no";
            frameDoc.body.childNodes[0].rows[0].cells[0].style.padding = "0px";
        }
    };

    (function init() {
        if (!crmForm.ObjectId) return;

        var navItem = document.getElementById(navItemId);
        if (!navItem) return;

        var clickAction = navItem.getAttributeNode('onclick').nodeValue;
        if (!clickAction || !clickActionPattern.test(clickAction))
            return;

        navItem.style.display = 'none';

        var areaId = clickAction.replace(clickActionPattern, '$1');
        roleOrd = clickAction.replace(clickActionPattern, '$3');

        iframe.src = getFrameSrc(areaId);
        iframe.allowTransparency = true; // Get rid of the white area around the IFrame
        iframe.attachEvent('onreadystatechange', onReadyStateChange);
    })();
};

loadAssociatedViewInIFrame(crmForm.all.IFRAME_Contacts, 'navContacts', 'contact_customer_accounts');
PS: I do know before writing this blog, Dave Hawes has previously provided a solution for this, which was often referred as the ultimate solution in the community. However, there are a few issues with the implementation, which I think are quite important:
  • It's not really compatible with multi-lingual CRM installation, as it tries to locate the "Add Existing" button by searching the button's title. In the case that you need to work with multi-lingual CRM implementation, your code may become really nasty.
  • The code that calls the function will need to be changed if you ever need to change the child entity's display name, as the button's title will change consequently in this case, after the entity's display name has been changed.
  • With Dave's code, the "Add Existing" button could re-appear if the user resizes the form.
  • The code only works for custom 1:N relationships, not the system ones, as the code assumes that the navigation item's ID is "nav_" + areaId, which is not true when it's a system built-in relationship, such as the one between account and contact, that we have used as our example. This is basically a bug of the code, not necessarily the disadvantage of the approach though.
This is why I am trying to take a different approach, hopefully this is a better approach.

Finally, a few important notes about the solution:
  • Be advised, this is not a solution that's supported by Microsoft, regardless of the improvement.
  • Using the same technique, it's pretty easy to remove any other buttons in CRM associated views. All you need to do is to find the button's element ID using IE developer tools as I have shown in one of the above screen shots, then you can remove the button from DOM using the code: btn.parentNode.removeChild(btn); Hope it's not something difficult for you.

[Update - Dec 29, 2010] After assisting Dina through email today to make the script work for one of her N:N relationship views, I updated the script so that it now supports both 1:N and N:N relationships.

Hope this helps.

30 comments:

  1. Daniel,
    This looks great and I would like to use it because my users are confused by the button just like you said. However, it doesn't seem to be working for me. I debugged it using alerts and onload it seems to stop at this line: if (!clickAction || etc..) Any idea why that might be? thanks!

    ReplyDelete
  2. Hi Nathan,

    Make sure you have provided correct navItemId parameter, it usually starts with "nav".

    Since you have mentioned that you are using alert() function, I would recommend you read http://www.jonathanboutelle.com/mt/archives/2006/01/howto_debug_jav.html about how to setup IE properly for debugging.

    Cheers,
    Daniel

    ReplyDelete
  3. Thank a lot - worked perfect for me.

    Great generic approach, worked on my custom entity, too.

    Hopefully, this works on CRM 5 ;-))

    Thanks again,

    Kman

    ReplyDelete
  4. Hi Kman,

    There is a rumor that CRM5 has an option to turn on/off "Add Existing" button, but I can't confirm it.

    In case CRM5 doesn't have the new feature and my script doesn't work for CRM5, I will revisit this one and write a new script to support CRM5. ;)

    Cheers,
    Daniel

    ReplyDelete
  5. Hi there

    I have found that the when puting a view in an iframe that the toolbar across the top is read-only (can view but can't click). Is this a security issue? as want to be able to still 'Add...', 'Print' etc

    thanks

    ReplyDelete
  6. Hi Daniel

    in the 'Solution for Associated View Loaded in IFrame' I am having an error on:

    var btnId = '_MBtoplocAssocOneToMany' + grid.GetParameter('otc') + relName.replace(/_/g, "");

    I have more than 1 iframe on the page, is this the issue as it seems that no-one else has had this problem?

    Thanks

    ReplyDelete
  7. Hi,

    I am sorry for not being able to respond sooner.

    With regard to the security issue, please ensure that you have unchecked "Restrict cross-frame scripting" option for the iframe.

    For the error that you received about the associated view in iframe, please ensure that you have provided the relName parameter correctly. It's not about how many iframe's that you have on the same form. I have tested more than 3 iframe's on the same form without any problem.

    Hope this helps.

    ReplyDelete
  8. Good article and very useful. But...what are you referring to LHS navigator HTML element Id? How can you get that in a custom entity?

    ReplyDelete
  9. @David, you might want to look closer at the second screen shot, which tells you how to find its ID.

    Hope this helps.

    ReplyDelete
  10. Hi!
    firts thanks for this article, is very useful. But i have 2 custom entitys and it doesn't seem to be working for me.
    Any idea why ? thanks!!!!

    ReplyDelete
  11. @Bar, make sure that you have supplied correct parameters.

    ReplyDelete
  12. Daniel, just saw this post as you had recently referenced it in the Developer forum. It is nice, clean code, and obviously has some considerations above and beyond Dave Hawes' solution (to which I have contributed), but it does work for other buttons on the grid as well... unlike your offering. I've used his code to hide the "Assign" and "Delete" buttons from some grids. Of course doing so is only a gesture, since real control of these buttons can be had through permissions. But there are times when I just want a user to think they can't do something a certain way, so I can coral them into a particular path I have designed (such as assignment processed by Workflow).

    I like the use of the removeChild() method for stripping out the button. That is far more effective at removing the button than Dave Hawes' implementation of simply hiding it. The only advantage Dave Hawes' code gives is to reveal the button without reloading the frame (which is nice for frames embedded within a form).

    So, in summary, if your solution could work for other buttons and allow the restoration of buttons dynamically, I think it would certainly surpass the efforts of Mr. Hawes and I.

    All in all, great stuff, though.

    ReplyDelete
  13. @Dave, thanks for your comment. Actually I have an improved version that optionally hides other buttons, but there is extra code for each additional button, which basically determines the button's ID. It's a little inconvenient. This might be the only disadvantage when comparing to Dave Hawes' solution, I assume.

    Everything comes with pros and cons, I was trying to illustrate a different approach.

    Cheers,
    Daniel

    ReplyDelete
  14. WOW thank you so much! This is a great post. I used Dave Hawes solution in the past but it didn't work for the iframe which is what we use a lot. Thank you so much!

    ReplyDelete
  15. @Bar

    As Daniel said, make sure you update the parameters of the last line

    loadAssociatedViewInIFrame(crmForm.all.IFRAME_Contacts, 'navContacts', 'contact_customer_accounts');

    The first param is the name of your iframe. The second is the id of the view. This was the hardest to find but I ended up just following Daniel's information and downloading the IE developer tools (not standard in IE7) and then using those to get the ID. the last is just the relationship name. Update those 3 params and you should be all set, even for custom entities.

    ReplyDelete
  16. Daniel, how would you use this to show the associated history items in an iframe? The first parameter is just the iframe name so that's easy. The second parameter is navActivityHistory which can be obtained using the IE developer tools. But what is the 3rd parameter? I can't seem to figure out what the contact to history relationship name would be called. Thanks

    ReplyDelete
  17. @Anonymous, I have just updated the code so the third parameter is now optional. So you can pass the first two parameters, and then you will be good.

    Cheers,
    Daniel

    ReplyDelete
  18. @Jim, thanks for sharing the information.

    Cheers,
    Daniel

    ReplyDelete
  19. Daniel

    Anonymous above = Jim. In any case, thanks so much for posting this. It's exactly what I needed. For anybody else that would like to show history in an iframe simply paste daniel's code above into your js file, or inline on the onload. Then the function call is as follows (where the iframe name is the name of your iframe):

    loadAssociatedViewInIFrame(crmForm.all.IFRAME_history, 'navActivityHistory')

    Worked perfectly. I am also loading another iframe where I needed to remove the button so I used the thrid parameter on that one.

    Thanks again for posting this Daniel, great work.

    ReplyDelete
  20. Daniel,

    One more question for you regarding this. Everything is working very well except when we open our forms sometimes we get a "Navigation to the webpage was canceled" error. Refreshing the page won't work, we need to close the form and reopen it. It's not 100% of the time but it's very frequent. Any thoughts on this? Thanks

    ReplyDelete
  21. @Jim, that's really strange. What default URL do you use for the iframe? You may want to try to use "/_root/blank.aspx". If that doesn't solve the problem, you may right click inside the iframe or use IE developer tool to find out the actual url of the iframe.

    Let me know if I can of any further assistance.

    Cheers,
    -Daniel

    ReplyDelete
  22. Daniel,

    In the iframe attribute we use about:blank as the default page until it loads. The url that is being loaded when it can't load the specified page is res://ieframe.dll/navcancl.htm

    This only seems to be happening to iframes loading using the method specified here. let me try the /_root/blank.aspx for a little and see if that has any impact. Thanks

    ReplyDelete
  23. I have used your code to remove 'Add Existing' button and it works great. However, in addition, I would like to change the 'New xxx' button to read 'Change xxx'. What code can I add into your code above to do this?
    thank you

    ReplyDelete
  24. @DML, sorry for the late response. Please try the following code snippet:

    /**
    * Hide "Add Existing xxxxx button" in a CRM associated view.
    * @author Daniel Cai, http://danielcai.blogspot.com/
    *
    * Parameters:
    * @param navItemId: The navigation HTML element ID of the associated view.
    It usually starts with "nav".
    * @param relName: The relationship name that the associated view represents.
    */
    function hideAddExistingButton(navItemId, relName) {
    var clickActionPattern = /loadArea\(['"]{1}([A-Za-z0-9_]+)['"]{1}\).*/;
    var iframe;

    var renameNewButton = function() {
    debugger;
    var frameDoc = iframe.contentWindow.document;
    if (!frameDoc) return;

    var grid = frameDoc.all['crmGrid'];

    var btnId = '_MBlocAddRelatedToNonForm' + grid.GetParameter('otc') + crmForm.ObjectTypeCode + 'GUID';
    var btn = frameDoc.getElementById(btnId);
    if (btn) {
    btn.childNodes[0].childNodes[0].childNodes[1].innerText = btn.childNodes[0].childNodes[0].childNodes[1].innerText.replace(/New/, "Change");
    }
    };

    var removeAddExistingButton = function() {
    var frameDoc = iframe.contentWindow.document;
    if (!frameDoc) return;

    var grid = frameDoc.all['crmGrid'];

    // Locate the "Add Existing" button using its magic id.
    var btnId = '_MBtoplocAssocOneToMany' + grid.GetParameter('otc') + relName.replace(/_/g, "");
    var btn = frameDoc.getElementById(btnId);
    if (btn) {
    btn.parentNode.removeChild(btn);
    }
    };

    var onReadyStateChange = function() {
    if (iframe.readyState === 'complete') {
    removeAddExistingButton();
    renameNewButton();
    }
    };

    (function init() {
    if (!crmForm.ObjectId) return;

    var navItem = document.getElementById(navItemId);
    if (!navItem) return;

    var clickAction = navItem.getAttributeNode('onclick').nodeValue;
    if (!clickAction || !clickActionPattern.test(clickAction))
    return;

    var areaId = clickAction.replace(clickActionPattern, '$1');

    navItem.onclick = function loadAreaOverride() {
    loadArea(areaId);

    iframe = document.getElementById(areaId + 'Frame');
    if (!iframe) return;

    iframe.attachEvent('onreadystatechange', onReadyStateChange);
    }
    })();
    }

    ReplyDelete
  25. Hi, Daniel

    Thanks for your reply. I really appreciate it.

    I put your code in and I got an error msg - an unhandled exception occurred in iexplore.exe...I

    clicked to debug and it sent me to the "var renameNewButton = function() {

    debugger;

    "var frameDoc = iframe.contentWindow.documents;....

    any ideas as to why?

    ReplyDelete
  26. Hi Daniel, works great. How do I change this to hide the 'New' button? I am not a programmer so need a bit of spoon feeding.

    ReplyDelete
  27. Hi Daniel, I used the code to hide existing xxx button Solution for Standard CRM Associated View.
    It worked fine as expected. But when I resize the form I get 'Error on page' in the status bar (bottom left corner).

    Can you help resolving this error.
    Thanks

    ReplyDelete
  28. @MM, I don't see that happens to me. What's the error message when you try to either reload or close the page?

    ReplyDelete
  29. Hi
    I would appreciate your quick response to the following issue.

    I have added to code to the accounts form in order to hide 'More Actions' button within an IFrame, it works. However, we get the 'Mixed Content security warinig' (http & https). The IE is configured correctly to allow both types of content, we are using CRM online. The code you have provided does not refer to non-secure content, so I am not sure why we get this warning. I also tried the following:
    location.protocol+"//"+location.host+"/area..." still no luck.

    ReplyDelete
  30. @Hossein, I assume that you are using the iframe approach, in which case, would you try to set the iframe initial url to /_root/blank.aspx? Most likely, the error is not really relevant to the script.

    ReplyDelete