Wednesday, November 19, 2014

Avoiding form reload when switching CRM forms based on a field

Quite often we define different forms for a given entity and we do a form switch based on a field rather than based on the security role of the user. The problem we observe with this approach is that there is often a double form load because when the wrong form loads then record re-loads using the appropriate form. This post offers a solution for avoiding the double form load in this scenario.

Say you have different types of opportunity which are identified by an “Opportunity Type” field and each opportunity type has its own form and BPF. The traditional approach would be to have a JavaScript on load of the form which will navigate to the appropriate form. The issue is that CRM web application loads by default the form that was last used by the given user. So if the user was looking at an opportunity of type “New Sale” and then opens an opportunity of type “New Service Contract” then the service contract opportunity will first load using the “new sale” form and after that form is loaded it will open the correct “service contract” form. The effect is bad user experience because the form loads twice so it doubles the form load time.

I have come across a solution that prevents this double form loading from happening which was suggested by a colleague. The solution is based on the premise that we can change which is the last viewed form for a given user on the fly when the user is retrieving a record. So you can intercept the retrieve operation from a plugin and then look at the type of opportunity that the user is retrieving and then update the user’s last viewed form to be the correct form for the opportunity type that is just getting retrieved. This way, the opportunity is always loaded in the correct form without a form switch.

The solution is a two-step process for the retrieve plugin:

1. In the pre-operation you must make sure to include the “opportunity type” field in the ColumnSet so that it is available later on for you to decide which is the appropriate form for the record.

2. In the post-operation you will have the “opportunity type” value and then you can update the user’s last viewed form to the correct form for the given opportunity type. To do so you simply need to modify the UserEntityUISettings entity for the given user and set the lastviewedformxml field.

Here is some sample code to use from your Retrieve plugin:

on Retrieve plugin
  1. var pluginContext = (IPluginExecutionContext)context;
  2. if (pluginContext.IsPreOperationStage())
  3. {
  4.     var columns = (ColumnSet)pluginContext.InputParameters["ColumnSet"];
  5.     if (!columns.Columns.Contains(OpportunityTypeAttributeName))
  6.         columns.AddColumn(OpportunityTypeAttributeName);
  7. }
  8. else if (pluginContext.IsPostOperationStage())
  9. {
  10.     var currentEntity = context.GetEntityFromContext();
  11.     if (currentEntity == null)
  12.         return;
  13.  
  14.     SetForm(currentEntity.ToEntity<Opportunity>(), service, context.UserId, tracingService);
  15. }

 

Set the correct form
  1. private void SetForm(Opportunity opp, IOrganizationService service, Guid userId)
  2. {
  3.     var query = new QueryExpression(UserEntityUISettings.EntityLogicalName);
  4.     query.Criteria.AddCondition("ownerid", ConditionOperator.Equal, userId);
  5.     query.Criteria.AddCondition("objecttypecode", ConditionOperator.Equal, Opportunity.EntityTypeCode);
  6.     query.ColumnSet = new ColumnSet("lastviewedformxml");
  7.     var settings = service.RetrieveMultiple(query).Entities;
  8.  
  9.     // Some users such as SYSTEM have no UserEntityUISettings, so skip.
  10.     if (settings == null || settings.Count != 1 || opp.pwc_OpportunityType == null) return;
  11.  
  12.     var setting = settings[0].ToEntity<UserEntityUISettings>();
  13.     string formToUse;
  14.     switch ((pwc_contractedengineservice)opp.pwc_OpportunityType.Value)
  15.     {
  16.         case pwc_contractedengineservice.NewSale:
  17.             formToUse = String.Format("<MRUForm><Form Type=\"Main\" Id=\"{0}\" /></MRUForm>", AdHocEngineServiceSaleFormId);
  18.             break;
  19.         case pwc_contractedengineservice.ServiceContract:
  20.             formToUse = String.Format("<MRUForm><Form Type=\"Main\" Id=\"{0}\" /></MRUForm>", ContractedEngineServiceSaleFormId);
  21.             break;
  22.         default:
  23.             return;
  24.     }
  25.     if (!formToUse.Equals(setting.LastViewedFormXml, StringComparison.InvariantCultureIgnoreCase))
  26.     {
  27.         // Only update if the last viewed form is not the one required for the given opportunity type
  28.         var s = new UserEntityUISettings { Id = setting.Id, LastViewedFormXml = formToUse };
  29.         service.Update(s);
  30.     }
  31. }

 

Regarding the supportability of this approach it seems to be a grey area. Technically you are doing operations via the SDK and using regular plugins which seems like a supported approach. The only problem is that the lastviewedformxml field of the UserEntityUISettings which is not documented in the SDK (although the entity seems valid for update).

A few things to consider is that if you are able to make this work with simple security roles then it would be simpler to configure the forms using security roles and you would not need this work-around. Consider as well whether you really need multiple forms or you can have other approaches such as JavaScript to hide/show sections depending on the type of record. I have posted some guidance regarding the different options available: http://gonzaloruizcrm.blogspot.ca/2014/07/different-entity-flavours-new-entity.html

23 comments:

  1. Hi Gonzalo,
    great Blog & great News to hear, that there is someone who tries to eliminate this annoying formchanger-problem. MS ignores the real user Problems here permanently.
    thx for that.
    Have you tested this in crm2015?
    What makes me uncertain is your justified objection "gray area" & "..not dokumented in SDK" ... hmmm
    Maybe we'll try it ..
    the temptation is too big, that this damned formchanger could be history :-)
    THX THX THX
    Greets PeB

    ReplyDelete
  2. Hi Gonzalo,
    great post! Had this idea already in June this year but never had the opportunity to test it fully. Seems supported if you read through http://msdn.microsoft.com/en-us/library/gg327834.aspx. Why? If the strongly typed classes generated by the code generation tool (CrmSvcUtil.exe) contains the attributes, I would tend to say this is supported. Best regards, Philip

    ReplyDelete
  3. Hola Gonzalo, no he podido lograr que me funcione utilizando el post-operation en el mensaje Retrieve, me puedes ayudar??

    Gracias,

    private void SetForm(Entity caso, IOrganizationService service, Guid userId, string nombreFormulario)
    {
    RetrieveEntityRequest request = new RetrieveEntityRequest();
    request.LogicalName = "incident";

    // Retrieve the MetaData.
    RetrieveEntityResponse response = (RetrieveEntityResponse)service.Execute(request);
    int objecttypecode = response.EntityMetadata.ObjectTypeCode.Value;

    var query = new QueryExpression("userentityuisettings");
    query.Criteria.AddCondition("ownerid", ConditionOperator.Equal, userId);
    query.Criteria.AddCondition("objecttypecode", ConditionOperator.Equal, objecttypecode);
    query.ColumnSet = new ColumnSet("lastviewedformxml");

    var settings = service.RetrieveMultiple(query).Entities;

    var setting = settings[0].ToEntity();

    string idFormToUse = string.Empty;
    var query2 = new QueryExpression("systemform");
    query2.Criteria.AddCondition("name", ConditionOperator.Equal, nombreFormulario);
    query2.ColumnSet = new ColumnSet("formid");

    var form = service.RetrieveMultiple(query2).Entities;
    if (form != null)
    {
    string guidformateado = form[0].Id.ToString();
    idFormToUse = string.Format("", guidformateado);

    if (setting.Contains("lastviewedformxml"))
    {
    string last = setting.Attributes["lastviewedformxml"].ToString();
    if (!idFormToUse.Equals(last, StringComparison.InvariantCultureIgnoreCase))
    {
    Entity userUI = new Entity("userentityuisettings");
    userUI.Id = setting.Id;
    userUI.Attributes["lastviewedformxml"] = idFormToUse;
    service.Update(userUI);
    }
    }
    }
    else
    {
    return;
    }
    }

    ReplyDelete
  4. Great post, thanks! It appears that you are using some extension methods on IPluginExecutionContext, that help with checking the Stage and getting the current entity. Am I correct? I don't see any method named GetEntityFromContext on that interface. Also, are you using RetrieveMultiple to get the entity to avoid a recursive call in the Retrieve plugin, or using some other handling (SharedVariables??) to avoid recursive calls? Thanks again!

    ReplyDelete
    Replies
    1. FWIW, and perhaps for the benfits of others who come across this, I just got this working. After including a generated entity types class from CrmSvcUtil, I replaced the custom references and the custom methods that Mr. Ruiz was using. For the GetEntityFromContext method you mention, I ended up using the standard EntityReference that the Retrieve Target provides and then performing a RetrieveMultiple query using the target from the Retrieve to avoid a recursive call.

      Delete
  5. This Code works however the value is modified after the form loads. please advise

    ReplyDelete
  6. CRM gets the value of LastViewedFormXml before geting the value of the record

    ReplyDelete
    Replies
    1. This comment has been removed by the author.

      Delete
  7. Thank you for the example! Unfortunately I got a problem, that the form only get switched at my second visit to a given entity. It seems, that your code updates the last viewed form correctly, but the update gets ignored on the visit which triggered the update. Did you got a similar behavior?

    ReplyDelete
    Replies
    1. Make sure you register the plugin as synchronous and in the steps specified.

      Delete
    2. I got the same problem in my CRM 2011. The plugin is registered as synchronous at pre-operation (20) and post-operation (40) but no lucky. The form is switched at the second visit.

      Delete
  8. Thanks for the great idea. I was wondering if you could help me out with the pre- and post-operation bits though? I am not familiar with the means you are using to determine the stage of the plugin (IsPreOperationStage method) and hoped you could provide a link or code for how you extended the context to include those methods.

    ReplyDelete
  9. This comment has been removed by a blog administrator.

    ReplyDelete
  10. This does not work for custom entity. The retrieve plugin gets called for both 20 & 40 stage and the settings get updated too but form load doesnt reflect the changes. this definitely works with contact entity because the plugin is fired twice but for custom this is called only once.

    ReplyDelete
  11. This does not work for custom entity. The retrieve plugin gets called for both 20 & 40 stage and the settings get updated too but form load doesnt reflect the changes. this definitely works with contact entity because the plugin is fired twice but for custom this is called only once.

    ReplyDelete
  12. Great Work!
    Code is working Perfect! But There is one Problem
    I have Registered plug on retrieve of incident in crm2015, and also write log on first line of execute method.
    i observed that for 1 request plug in fires 2 time for each (Pre & post)

    how we can avoid this

    Please help if you have any idea

    ReplyDelete
  13. Hi Gonzalo,
    I had this implemented and it was great, but after upgrade to CRM Online 2015 Update 1 it stop working. Seems to me that the new rendeing engine has something to do but is only a guess. Do you know anything about changes that may affect this approach?

    ReplyDelete
  14. Hi Guys,

    After update to 2015 Spring Release (7.1) this approach stop working. With the new rendering engine the UI setting is not being pulled from server (this is my guess) and changing that setting from a plugin doesn't have effect on the displayed form.
    Does anyone have some alternative to this? I been looking for alternatives and seems that the only one is reloading (or redirecting) the right form.

    ReplyDelete
    Replies
    1. You can make this work in 2016 by going to Settings->Adminsitration->System settings->in the general tab select ‘Yes’ under Use legacy form rendering. Note that this option might be removed in future releases of CRM.

      Delete
  15. Thanks to your famous colleague for this great post :)

    ReplyDelete
    Replies
    1. Yes you're the man! Turns out this is unsupported and breaks in future versions though unfortunately

      Delete
    2. I know it's also not supported but I think it breaks because the formid is stored in the localStorage. When we change the url value with the correct formid in the localStorage for the key ___ it seems to be working. So for the moment we just need to wait Microsoft proposes a way to choose the form to use depending on some conditions, use javascript to switch on load or show/hide tabs with everything on the same form...

      Delete
    3. There is a work around that makes this solution work in crm 2015 update 1 and up (so works in latest build of crm 2016 as well). Go to Settings - Administration - System Settings - General tab: Select ‘Yes’ under Use legacy form rendering to enable this mode for all users

      This will disable Turbo Forms which is what CRM uses since 2015 update 1 which basically stores all settings in localstorage to avoid a round trip to server to fetch the formId. Keep in mind that turbo forms makes the loading faster so this is a trade off. Also MS. might remove setting that legacy form option from the system settings in future releases.

      Delete