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

Subscribe to
The Daily Friction

A daily newsletter on automation and eliminating friction

Related Content

Who is the target audience for Power Pages

Who is the target audience for Power Pages

Power Pages is slowly getting better and better product. But then I look at the pricing page: https://powerpages.microsoft.com/en-us/pricing/ Who is the target audience for this product? If you need something 'quick and dirty' it is nice to have a website up and...

read more
Power Pages Secure?

Power Pages Secure?

Power Pages! They tell me it's easy and is very secure, it's built in! I am not fully convinced when I hear this. Why? Simple said: "Easy and Security don't mix". Making something more secure will make it less easy to use. Data separation The thing is, Dataverse is...

read more
How to use Environment Variables in your plugins

How to use Environment Variables in your plugins

Environment Variables Dynamics 365 / Power Apps solution can have Environment Variables. Often a Settings table (=entity) would be created to store configuration settings that differ between environments. We now can replace these with environment variables. These...

read more
Share This