Improving the Form Switcher

by Sep 22, 2022

In Microsoft Dynamics 365, Model Driven Apps in the Power Platform, you can create multiple main forms for the same table or TOFKAE (The Object Formally Known As Entity).

form switcher

When to use multiple forms

This can be handy because you can assign different security roles to each form so you can present a form that is optimized for how they use the application for distinct groups in your organization.

You can also have different forms to reprensent distinct types you store in the table, for example Prospect and Customer in the table Account. This type is usually solved stored as Choice or TOFKAOS (The Object Formally Known As OptionSet). Normally I advise to show/hide tabs, sections and fields based on the type but if behavior is so different between these types it could merit to create a separate form.

The Form Switcher

For the last reason to use multiple forms, the type of a record, we have a problem. We don’t know which form to show until we have loaded the data from Dataverse.

For this the solution is the so-called Form Switcher: some JavaScript code that decides which form needs to be shown when the data is being loaded and navigates to that specific form. The code looks like this:

var targetFormId = [Your Target Form Id];
var availableForms = Xrm.Page.ui.formSelector.items.get();

for (var i in availableForms) {
     var form = availableForms[i];
       if (form.getId().toLowerCase() == targetFormId.toLowerCase()) {
                form. Navigate();
       }
}

The working primarily depends on the FormSelector and Navigate methods. The first method retrieves the available forms and the second method forces the form to navigate away from the current form to the specified form.

Before refactoring

Recently I was working for a customer who had almost twenty forms for one table. The table contained distinct kinds of services, each with their own behavior.

Switching forms happened a lot and was noticeable, so even tweaking performance a little bit could help.

The code looked like below, where I shortened it a little bit for readability. On some forms there were required fields that were not needed on other forms and could prevent the form switching from happing. Therefor there is code to make them non-required before switching.

function switchForm() {
    var type = Xrm.Page.getAttribute("as_servicetypecode").getValue();
    if (type != null) {
        var formid;
        switch (type) {
            case 1001:  // Service A
                formid = "381C6F6B-524A-41EC-9E8D-6DC5C8F6533C";
                break;
            case 1002: // Service B
                formid = "08518A1A-28CA-451D-880E-EC30B4480E51";
                break;
            case 1003: // Service C
                formid = "EEB5C117-066E-4A84-863D-A36DF26ED72A";
                break;
            case 1004: // Service D
                formid = "BE2E9DA2-C63D-4462-9B78-741E40977E68";
                break;
           // 15 more forms...
        }
        if (formid != null) {
            var items = Xrm.Page.ui.formSelector.items.get();
            for (var i in items) {
                var item = items[i];
                var itemId = item.getId();
                if (itemId.toLowerCase()  == formid.toLowerCase() && Xrm.Page.ui.formSelector.getCurrentItem().getId() != formid.toLowerCase()) {
                    try { Xrm.Page.getAttribute("as_debtorid").setRequiredLevel("none"); } catch (e) { }
                    try { Xrm.Page.getAttribute("as_rayonid").setRequiredLevel("none"); } catch (e) { }
                    try { Xrm.Page.getAttribute("as_firstmomentofarrival").setRequiredLevel("none"); } catch (e) { }
                    try { Xrm.Page.getAttribute("as_waitingtimestart").setRequiredLevel("none"); } catch (e) { }
                    try { Xrm.Page.getAttribute("as_waitingtimeend").setRequiredLevel("none"); } catch (e) { }
                   // more required fields

                    item. Navigate();
                }
            }
        }
    }
}

Improving the Form Switcher

The following refactorings I do always when working on existing JavaScript in Dynamics 365:

  • adding executionContext
  • replacing var with let or const
  • invert if-statements and use early return

Especially inverting the if-statements and using early returns makes the code much readable, like:

    const serviceType = formContext.getAttribute("as_servicetypecode").getValue();
    if (serviceType == null) return;

The following if-statement was placed inside the loop, while it only needs to be checked once, so I placed it outside of the loop:

  if (targetFormId === formContext.ui.formSelector.getCurrentItem().getId()) return;

Then we have the big case-statement, which I shortened in the example. My proposal is to change this into an associative array that contains the mapping from the serviceType code to the formId. The primary reason is that it makes the code much clearer and adding a new form is simpler.

My gut feeling is also that an array is going to be faster than a switch-statement (I could be wrong). I also moved the array outside of the method scope, so that it doesn’t need to be recreated every time that the method is being called:

const serviceTypeToFormIdMapping = { // must be lowercase id's!
    1001: "381c6f6b-524a-41ec-9e8d-6dc5c8f6533c", // Service A
    1002: "08518a1a-28ca-451d-880e-ec30b4480e51", // Service B
    1003: "eeb5c117-066e-4a84-863d-a36df26ed72a", // Service C
    1004: "be2e9da2-c63d-4462-9b78-741e40977e68" // Service D
    // many more...
}

function switchForm(executionContext) {
    const targetFormId = serviceTypeToFormIdMapping[1002];
}

Xrm always returns lowercase Id’s. Therefore, we store the formId’s as lowercase in the array, and therefore we can remove the calls to .toLowerCase(). It’s a micro-optimization, but it’s being called multiple times.

I also wanted to improve making the fields non-required, by making the code more generic and removing the try...catch statements:

const noneRequiredAttrNames = [
    "as_debtorid", "as_rayonid", "as_clientid", "as_firstmomentofarrival", "as_waitingtimestart",
    "as_waitingtimeend"];

for (const attrName of noneRequiredAttrNames) {
     const attr = formContext.getAttribute(attrName);
     if (attr != null && attr.getRequiredLevel() !== "none") attr.setRequiredLevel("none");
}

I tried to do the iterating through the forms in parallel, but I didn’t see any performance benefit, because JavaScript is mostly single threaded, so I abandoned that. But I choose to use for...of statement instead of .forEach because Google tells me it’s still a little bit faster.

I also added a return statement after the Navigate() method, so that the iterating really stops after finding the match and navigating away:

    for (let form of availableForms) {
        // logic
        form.Navigate();
        return;
    }

After refactoring

Combining all the refactorings together this results in the new improved Form Switcher:

const serviceTypeToFormIdMapping = { // must be lowercase id's!
    1001: "381c6f6b-524a-41ec-9e8d-6dc5c8f6533c", // Service A
    1002: "08518a1a-28ca-451d-880e-ec30b4480e51", // Service B
    1003: "eeb5c117-066e-4a84-863d-a36df26ed72a", // Service C
    1004: "be2e9da2-c63d-4462-9b78-741e40977e68" // Service D
    // many more...
}

const noneRequiredAttrNames = [
    "as_debtorid", "as_rayonid", "as_clientid", "as_firstmomentofarrival", "as_waitingtimestart",
    "as_waitingtimeend"];

function switchForm(executionContext) {
    const formContext = executionContext.getFormContext();
    const serviceType = formContext.getAttribute("as_servicetypecode").getValue();
    if (serviceType == null) return;

    const targetFormId = serviceTypeToFormIdMapping[serviceType];
    if (targetFormId == null) return;

    if (targetFormId === formContext.ui.formSelector.getCurrentItem().getId()) return;

    const availableForms = formContext.ui.formSelector.items.get();
    for (const form of availableForms) {
        if (form.getId() !== targetFormId) continue;
        
        for (const attrName of noneRequiredAttrNames) {
            const attr = formContext.getAttribute(attrName);
            if (attr != null && attr.getRequiredLevel() !== "none") attr.setRequiredLevel("none");
        }

        form.Navigate();
        return;
    }
}

💡Tip: You want to hide the form selector for the user because it doesn’t make sense for them to switch themselves? Then make the forms invisible, which removes them from the form selector:

formContext.ui.formSelector.items.forEach(form => form.setVisible(false));
Remy van Duijkeren

Remy van Duijkeren

Power Platform Advisor

Microsoft Power Platform Advisor with over 25 years of experience in IT with a focus on (marketing) automation and integration.

Helping organizations with scaling their business by automating processes on the Power Platform (Dynamics 365).

Expert in Power Platform, Dynamics 365 (Marketing & Sales) and Azure Integration Services. Giving advice and strategy consultancy.

Services:
– Strategy and tactics advise
– Automating and integrating

Subscribe to
The Daily Friction

A daily newsletter on automation and eliminating friction

Related Content

External authentication with Dataverse ServiceClient

For a while now we can use the new Dataverse ServiceClient that replaced the old CrmServiceClient. It has three big improvements: Works for .NET 5.0 and up (.NET Core) Uses the newer MSAL.NET instead of ADAL.NET (which is out of support) for authentication Support for...

read more

Everyone got ALM wrong in Dynamics 365 / Dataverse

For ages, we've been ferociously encouraging the integration of developer practices, such as source control and ALM, into the Dynamics 365/Dataverse realm. The ultimate truth The revered 'Master Branch' in source control, has always been the sole fountainhead from...

read more
Early-Bound Classes for .NET 4.6.2 and 6.0

Early-Bound Classes for .NET 4.6.2 and 6.0

You like to use strong types in .NET when working with Dataverse / Dynamics 365? Are you into Early-Bound Classes? Generating entity classes? You can use CrmSvcUtil for this, but I personally like to use XrmContext from Delegate to do this, because it creates smaller...

read more