Canvas App Custom Pages • Model Driven App Design

Canvas App Custom Pages allow you to build a lightweight App using the Canvas App authoring tools. The app is then embedded in the menu of Dynamics 365 or a Model Driven App.


This authoring experience provides a lot of flexibility in the design. If the App is not thoughtfully designed, the User Interface can be jarring for end users.


Below are steps to trick users into thinking they are using any other Model Driven App form. This blog assumes you have knowledge of basic Canvas App authoring tools and PowerFx Formulas.

The Cooptimize "Introduce Contacts" App screenshot

Tips for Canvas App Pages

Get Organized

Organize the Tree View using keyboard shortcuts.

Ribbon Bar Buttons ☑️

Put buttons at the top of the form, using concatenated emoji + text as the label. Reference the position and size of the button to the left to make the buttons responsive to design changes.

Responsive App Sections

Use Variables in the X, Y, Width, and Height parameters so the app will resize. Use global variables to control the initial position.

Building My App

I’ll be showing screenshots from the Cooptimize “Introduce Contacts” app. The purpose of this app is to send an email to two contacts to facilitate an introduction; for example, introducing a reference to a prospect. The app retrieves CRM details to generate a nicely formatted PDF and attach it to an introductory email.

I used a Canvas App Custom Page for this design. It makes sense as a Canvas App (not a Model Driven App) because the buttons trigger Flows, the data entered by the user is not stored in Dataverse, and this is more of a process than data entry. I designed a Custom Page because this experience should logically exist in the main menu with our Cooptimize CRM Environment.

The entire app is shown below. Does it look like a Dynamics 365 Sales form?

The Introduce Contacts App. Has two Select Contact fields, and some text boxes to facilitate the introduction. A series of buttons at the top use Power Automate to fill a Word Template and Send an email.
Nice App!

The process is to enter a few fields into this form, generate a document that pulls info from CRM and the form, review the document, and email the contacts. A user can do all those tasks without leaving this form.

Get Organized

Every field, label, button, and other object in the App needs to be named and organized. Having “Button2”, “Label6”, and “TextBox_copy_copy_copy” in a random order won’t cut it.

Unfortunately, the Canvas Apps designer does little to make this intuitive. Some common steps are:

  1. Rename every object when you add it to the App to something logical and consistent. Renaming helps select Objects and simplifies writing PowerFx code.
  2. All the Objects in the Tree View have an implied Layer Order from 1…Last. Objects can be manually reordered so the Tree View flows Top to Bottom and Left to Right. You MUST learn the Tree View keyboard shortcuts – otherwise reordering fields will make you go crazy!
  3. Put design elements like backgrounds and lines after Fields and Labels.

Introduce Contacts App • Tree View

Here’s the finished view of an organized Tree for Introduce Contacts.

Selecting an Object

When objects are grouped and ordered in the Tree, they becomes much easier to select for modification. The “Reset Form” button is fourth on the form and fourth in the Button Group.

Writing Code

When you rename all the Objects, PowerFx code becomes much easier to read and understand.

// TextBox_copy_copy.Y = TextBox_copy_copy_copy.Y + 40

TextBox_Section1.Y = ContactField_Section1.Y + 40

Ribbon Bar Buttons

Buttons in a Model Driven app are presented with an Icon + Text. Unfortunately in Canvas apps, Buttons don’t support Icons and Icons don’t display text.

🤔🤯

Sounds like a job for Emoji! You can use a Button with Emoji + Text to get a similar feel.

Button Label

When creating a ribbon bar, responsive design is helpful. To support this, the button labels are not defined on the button itself. They are defined in a variable in App.OnStart.

// Set Button Text in a Collection so the Length can be determined.
ClearCollect(ButtonGroup_Text,
    {
        ButtonName: "GenerateDocumentButton",
        ButtonText: "📄 Generate Document"
        AccessibleLabel: "Generate Document"
    },...

In Button.Text the variable is retrieved.

GenerateDocumentButton.Text =
LookUp(ButtonGroup_Text, ButtonName = "GenerateDocumentButton", ButtonText)

// Don't forget Button.AccessibleLabel when using emoji! Users probably don't want their screen reader saying "Crying Laughing Face Crying Laughing Face".

GenerateDocumentButton.AccessibleLabel =
LookUp(ButtonGroup_Text, ButtonName = "GenerateDocumentButton", AccessibleLabel)

Button Width

This is where the app starts to have a responsive design. Make the width of the button conditional to the length of the button label. As you adjust labels, the button size will adjust. Model Driven App buttons also have variable sizes dependent on the label.

Button.Width = 
Len(LookUp(ButtonGroup_Text, ButtonName = "GenerateDocumentButton", ButtonText))*10

⚠️ Getting the value of the button label directly is impossible. Hence storing the text in a parameter.

// This looks logical. It won't work. Red squiggly of doom.
Len(Button.Text);

Next Button

Model Driven app buttons are all adjacent. Use a Formula to figure out the X position of the next Button by adding X + Width from the button to the left.

NextButton.X = 
GenerateDocumentButton.X + GenerateDocumentButton.Width

Button Color

Model Driven App buttons are white with a light grey Hover Fill (mouse over) color. Also, change the Disabled text property to grey so it’s obvious when the button is un-clickable.

Button Enabled/Disabled

Don’t allow the user to click a button that won’t work. Buttons should be disabled until the proper conditions are met.

// The user must fill out the form before the button is available.
GenerateDocumentButton.DisplayMode = 
If(
    Or(
        IsBlank(ContactField_Section1.Selected),
        IsBlank(TextBox_Section1.Value),
        IsBlank(ContactField_Section2.Selected),
        IsBlank(TextBox_Section2.Value)
    ),
    DisplayMode.Disabled,
    DisplayMode.Edit
)


// This is a long running process (Power Automate). Disable the button so the user doesn't click the button a bunch of times.
GenerateDocumentButton.OnSelect = 
GenerateDocumentButton.DisplayMode.Disabled;
...

Lines

There’s no line Object!?!

Don’t be fooled by the “Horizontal Line” Icon.

Admiral Ackbar meme. "It's a Trap. Use a Rectangle."

Draw a Really Thin Rectangle

Insert a rectangle with Height 1 and Border 1. Use light grey border color to match a Model Driven App line.

App Sections

For Introduce Contacts I wanted to make the App look like a Model Driven 2-Column Page with three Sections.

Alignment

I did all my formulas in multiples of 10px to align objects. This is similar to how it works with Power BI’s “Snap to Grid” and will look similar to Model Driven spacing.

Section Starting Positions

Set global variables as the positioning starting points in App.OnStart. These values can then be adjusted as you work through the exact field spacing.

Set(LeftColumn_x, 20);
Set(RightColumn_x, 640);
Set(TopSection_y, 120);
Set(BottomSection_y, 380);
Set(ButtonHeight, 40);
Set(SectionWidth, 600);

The body of my app is not currently responsive. If I built everything right, I should be able to tweak these variables based on functions like App.Width. This was good enough.

Section Rectangle

Every Object needs an X, Y, Width, and Height. The fewer hard coded values, the more responsive the App will be.

The even bigger benefit to assigning variables is alignment! Setting variables allows resizing and moving objects without using the frustrating drag and drop.

Below are some settings for the first rectangle.

// Outline_Section1 Rectangle
Outline_Section1.X = LeftColumn_x
Outline_Section1.Y = TopSection_y
Outline_Section1.Width = SectionWidth
Outline_Section1.Height = 240

Other Section Objects

Just like the button settings, as additional Objects are added the position and size settings can refer to adjacent objects.

// TextBox_Section1 Example
TextBox_Section1.X = Outline_Section1.X
TextBox_Section1.Y = Separator_Section1.Y + 20
TextBox_Section1.Width = Outline_Section1.Width
TextBox_Section1.Height = 80

Things I Did Not Solve

Tab Order

I could not figure out how to change the order of tab stops in this app. The order always goes top to bottom through the fields. There is a tabOrder property, but not on the input fields. This is a bad user experience and an abysmal accessibility experience.

PDF Display

My app would have been simpler to use if I could display the PDF in the form. Instead the user opens the PDF in a new window. Custom Pages don’t support the PDF viewer.

Themes

Dynamics 365 and Model Driven Apps have environment Theme settings. I did a lot of manual color picking and I’m curious if tying the app design into a Theme stored in the Dataverse environment would have worked. I also didn’t investigate storing theme parameters as variables.

Default Values

I made a lot of changes after I did a working prototype. I wonder if there’s a more efficient way to default values and parameters from the start.

Fully Responsive Design

It would have liked to try out a fully responsive design, including support for mobile. But I already spent about 10x the time on this app than what it is worth.

Final Thoughts

Would an average user be fooled that this App was built using the same tools as the other Dynamics 365 Forms? I think they would. In fact, they probably will ask for Power Automate Buttons to be added in the Ribbon Bar of other forms. 😅 That’s a blog someone else will have to write…

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s