Friday, December 02, 2011

MSCRM 2011: Filtered Lookup for "Add Existing..." Button of a CRM N:N View

I have previously documented a way to add lookup filtering capability to MSCRM4 "Add Existing..." button in a CRM N:N view. There was a question in the blog post today about whether this is possible in CRM 2011. So here is this blog post that will walk you through the implementation process.

Prerequisites

To go through the customizations in this blog post, you need to understand how CRM ribbon customization works, and also you need to know how to work with CRM Web Resources and solution export/import functionalities. It should also be noted that this blog post uses CRM Developer Toolkit. In case you are not familiar with the toolkit, you can have a look at my another blog post that illustrates some typical development scenarios which you might find useful. This blog post might be able to demonstrate you how the toolkit can help improve your productivity when developing CRM scripts.

Business Scenario

Let's say we have a custom entity called Project, which has a many-to-many relationship to CRM account entity. In Project entity form, I added a sub-grid which points to account entity, as shown below.


In the above screen, we have a Add Existing Button when the sub-grid is selected. When the button is clicked, we will be prompted the lookup entity's default Lookup View, as shown below.



What we are trying to achieve is to add some filtering capability to the "Add Existing ..." button so CRM only shows the account records that we are interested.

Let's get started.


Steps

Step 1: Create CRM JavaScript Web Resources

  1. In Visual Studio 2010, create a CRM project. (Please refer to the blog post that I just mentioned for detailed instructions about how to do this)
  2. In CrmPackage project, right-click WebResouces –> Script (JScript), and choose "Add" –> "New Item" to create a new JavaScript file. Let's call the file Project.js.


  3. Add the following script to the JS file (I borrowed the fetchXml and layoutXml combination from SDK document).
    function addExistingFromSubGridCustom(params) {
    
        var relName = params.gridControl.getParameter("relName"),
            roleOrd = params.gridControl.getParameter("roleOrd"),
            viewId = "{00000000-0000-0000-0000-000000000001}"; // a dummy view ID
        
        var customView = {
            fetchXml: params.fetchXml,
            id: viewId, 
            layoutXml: params.layoutXml,
            name: "Filtered Lookup View",
            recordType: params.gridTypeCode,
            Type: 0
        };
    
        var lookupItems = LookupObjects(null, "multi", params.gridTypeCode, 0, null, "", null, null, null, null, null, null, viewId, [customView]);
        if (lookupItems && lookupItems.items.length > 0) {
            AssociateObjects(crmFormSubmit.crmFormSubmitObjectType.value, crmFormSubmit.crmFormSubmitId.value, params.gridTypeCode, lookupItems, IsNull(roleOrd) || roleOrd == 2, "", relName);
        }
    }
    
    function addExistingFromSubGridAccount(gridTypeCode, gridControl) {
        addExistingFromSubGridCustom({
            gridTypeCode: gridTypeCode,
            gridControl: gridControl,
            fetchXml: "<fetch version='1.0' " +
                           "output-format='xml-platform' " +
                           "mapping='logical'>" +
                       "<entity name='account'>" +
                       "<attribute name='name' />" +
                       "<attribute name='address1_city' />" +
                       "<order attribute='name' " +
                               "descending='false' />" +
                       "<filter type='and'>" +
                           "<condition attribute='ownerid' " +
                                       "operator='eq-userid' />" +
                           "<condition attribute='statecode' " +
                                       "operator='eq' " +
                                       "value='0' />" +
                       "</filter>" +
                       "<attribute name='primarycontactid' />" +
                       "<attribute name='telephone1' />" +
                       "<attribute name='accountid' />" +
                       "<link-entity alias='accountprimarycontactidcontactcontactid' " +
                                       "name='contact' " +
                                       "from='contactid' " +
                                       "to='primarycontactid' " +
                                       "link-type='outer' " +
                                       "visible='false'>" +
                           "<attribute name='emailaddress1' />" +
                       "</link-entity>" +
                       "</entity>" +
                   "</fetch>",
            layoutXml: "<grid name='resultset' " +
                                 "object='1' " +
                                 "jump='name' " +
                                 "select='1' " +
                                 "icon='1' " +
                                 "preview='1'>" +
                             "<row name='result' " +
                                  "id='accountid'>" +
                               "<cell name='name' " +
                                     "width='300' />" +
                               "<cell name='telephone1' " +
                                     "width='100' />" +
                               "<cell name='address1_city' " +
                                     "width='100' />" +
                               "<cell name='primarycontactid' " +
                                     "width='150' />" +
                               "<cell name='accountprimarycontactidcontactcontactid.emailaddress1' " +
                                     "width='150' " +
                                     "disableSorting='1' />" +
                             "</row>" +
                           "</grid>"
    
        });
    }
  4. After the script has been saved, you can right click CrmPackage project in Visual Studio Solution Explorer window, and choose "Deploy" to deploy the script to CRM Server. In our case, we will have a Web Resource in CRM called new_Project.

Step 2: Customize CRM Ribbon to Hook up the JS Function

  1. In CRM, create a new solution, and add account entity (the entity in the sub-grid) to the solution.
  2. Export the solution to a .zip file, and extract the file somewhere in your local file system.
  3. Open the customizations.xml from the extracted file, and locate RibbonDiffXml section in the file, under the account entity.


          <RibbonDiffXml>
            <CustomActions />
            <Templates>
              <RibbonTemplates Id="Mscrm.Templates"></RibbonTemplates>
            </Templates>
            <CommandDefinitions />
            <RuleDefinitions>
              <TabDisplayRules />
              <DisplayRules />
              <EnableRules />
            </RuleDefinitions>
            <LocLabels />
          </RibbonDiffXml>
  4. What we need to change is the CommandDefinitions part, which we can get the template code from applicationribbon.xml file (or <entityname>ribbon.xml if it's a system entity) in CRM SDK's resources\exportedribbonxml folder. 
  5. In applicationribbon.xml file (or <entityname>ribbon.xml), locate <CommandDefinition Id="Mscrm.AddExistingRecordFromSubGridAssociated"> line (You can search by addExistingFromSubGridAssociated to get there).
  6. Copy the xml content under <CommandDefinition Id="Mscrm.AddExistingRecordFromSubGridAssociated"> (including itself and its closing tag as well) to the customizations.xml file, so it should look like this.
  7. Modify the JavaScriptFunction part to point to the custom JS function that we previously developed, so it should be something like this:


    The following is the final customization xml.
      <CommandDefinition Id="Mscrm.AddExistingRecordFromSubGridAssociated">
        <EnableRules>
          <EnableRule Id="Mscrm.AppendPrimary" />
          <EnableRule Id="Mscrm.AppendToPrimary" />
          <EnableRule Id="Mscrm.EntityFormIsEnabled" />
        </EnableRules>
        <DisplayRules>
          <DisplayRule Id="Mscrm.AddExisting" />
          <DisplayRule Id="Mscrm.ShowForManyToManyGrids" />
          <DisplayRule Id="Mscrm.AppendPrimary" />
          <DisplayRule Id="Mscrm.AppendToPrimary" />
          <DisplayRule Id="Mscrm.AppendSelected" />
          <DisplayRule Id="Mscrm.AppendToSelected" />
          <DisplayRule Id="Mscrm.CanWriteSelected" />
        </DisplayRules>
        <Actions>
          <JavaScriptFunction FunctionName="addExistingFromSubGridAccount" Library="$webresource:new_Project">
            <CrmParameter Value="SelectedEntityTypeCode" />
            <CrmParameter Value="SelectedControl" />
          </JavaScriptFunction>
        </Actions>
      </CommandDefinition>
    Note that your JS webresource name could be in different format. You can find out that by openning the web resource in CRM, which tells you the path to the web resource file.

  8. Save the file, and zip it back to the original solution export file.
  9. Import the solution file, and publish changes. If everything goes OK, you will see a filtered lookup like the following.

Final Notes

  1. This is an unsupported customization, since we are using undocumented CRM built-in JS functions.
  2. addExistingFromSubGridCustom function is a re-usable function which is what you really need. addExistingFromSubGridAccount function is the actual one that's going to be called in your ribbon command button's customization.
  3. The filter that I have implemented didn't use any context information (such a field value) on the CRM form, you can easily add this in the function when constructing the fetchXml string, if desired.
  4. The above change will apply to all account subgrid regardless of the hosting entity. It should be possible to handle it differently by getting some context information from the form, or probably pass some extra information to the function.
  5. The code didn't have much comments, but hopefully the sample has provided enough information for you to start with.
By the way, an off-topic note, this blog has just reached 100K page views today. It took almost two years to reach 50K pageviews last time, but it only takes a few months to reach another one, although I don't currently produce as many posts as I did in the beginning of my blogging adventure. Good to see the growth of the CRM community.

Hope this helps.

12 comments:

  1. Great Post. This is going to be really helpful for those CRM developers who are doing customization on daily base. Here,only thing I want to add is when you copy the 'CommandDefinition' part Xml file, make sure the 'Id' matches with the ribbon button Id(It could be Mscrm.AddExistingRecordFromSubGridAssociated or Mscrm.AddExistingRecordFromSubGridStandard),also the relationship could be one to many or many to many, so you might need to change or . Then you are good to go.

    ReplyDelete
  2. Continue my last comment, for whatever reason, the text I typed didn't show. I mean you need change to if the relationship is one to many.

    ReplyDelete
  3. Thanks Jack for the info.

    As Jack has pointed out, when the relationship is one to many, the ribbon button ID should be Mscrm.AddExistingRecordFromSubGridStandard.

    ReplyDelete
  4. That's one thing. There is another thing I also want to point out is you also need to change the 'DisplayRule' from Id="ShowForManyToManyGrids" to Id="ShowForOneToManyGrids" when the relationship is changed. otherwise, it wouldn't work.

    ReplyDelete
  5. Very nice article Daniel. Thanks for sharing!

    ReplyDelete
  6. Hello Sir,
    Thank u so much for Post, its truly helpful..
    But I am not much familiar with Fetchxml,
    I have three Custom Enitity (T,S,A)
    T has 1:N relation with A
    T has N:N relation with S
    A has 1:N relation with S
    I have forms for T which have lookup for A and subgrid for S,
    When Add existing button of Subgrid is clicked it should show record as per selection of enity A(lookup)
    so if user selects one of the Account(which is lookup) related S record show be shown in lookup of Add Existing button of Subgrid.
    kindly need your help

    ReplyDelete
  7. @Sneha, you have to make yourself comfortable working with FetchXml, I consider it a key skill to survive in CRM development practice. In case you don't know, when you create a CRM advanced find view, there is a ribbon button on the far right which allows you to download the FetchXml query. This seems to be the easiest way to create a FetchXml query. Actually learning FetchXml is not that hard, in fact, it's quite fun.

    Hope this helps.

    ReplyDelete
  8. thanks a lot sir...
    I shall start it from today...

    ReplyDelete
  9. Great post....It worked for me. But I have a problem.
    I want to display an attribute from a linked entity in the view. I was able to access that attribute in Fetch XML.
    But I couldn't able to add that cell in Layout XML.
    I tried by giving 'Alias' to the linked entity and access the attribute in Layout XML as 'alias.attribute', didn't work.
    Can you please tell me how to do this?
    Thank you.

    ReplyDelete
    Replies
    1. @Sushma, I am not sure why it would happen to you. I will try to check if the aliases in FetchXML and LayoutXML are matched. In fact, I have shown the linked entity in my sample. You may be able to find some hints from my code.

      Delete
  10. Hi Daniel,

    great article, but i did not get it to work in my case :-(
    Can you help me?


    I have 3 Entities. new_Event, new_Participant and productpricelevel
    Between participant and productpricelevel i have an n:m relation. Between event and participang 1:n

    Now i want to filter the productpricelevels on the pricelist saved on event.
    In first step I just wrote a new view which displays all productpricelevels, to see if it is working - but no...
    My Situation is a little bit different as you wrote. You added a Subgrid to the form - i dont have this. I have just the navigation on the left site. Is it possible to filter there?

    Thanks in advance
    Pascal

    ReplyDelete