Microsoft has been making great strides towards providing the Unified Interface across all devices for Dynamics 365.  As part of this Unified Interface, they have implemented the Custom Controls Framework.  However, to date, no one has released any developer documentation on how to create custom controls -- the only documentation shows how to use controls provided out-of-the-box. This blog series will show how you can create your very own custom controls!

Disclaimer: this is technically unsupported - it is provided for educational purposes only.

Introduction

This post, and the next few, will explore the functionality offered by the Custom Controls Framework (CCF).   

The Custom Controls Framework allows you to create your own controls that change how form elements are rendered.  Custom controls are conceptually very simple -- they get data from the system when they load, and they report back when the data is changed by the user.  To facilitate development, Microsoft has provided a very easy-to-use framework.  All the developer needs to deal with is building the UI for the control.  Let's dig in to the details!

Each control is comprised of a few key components:

  • Control.js
  • ControlManifest.xml
  • Images
  • Libraries
  • Styles (CSS)
  • Strings (RESX)

We'll discuss the first two in this post and the others in future posts.  To frame the discussion, we will walk through creating a custom control for a decimal field which displays the value as a percentage.

Control.js

The Control.js file is the heart of a custom control -- it defines how the control renders.  It consists of a class that implements, at a minimum, three methods: init, updateView, and getOutputs -- which are called by the Dynamics 365 runtime when it is rendering the control.  Although this file must be in JavaScript, it is much easier/cleaner to write it in TypeScript and compile it to JavaScript.  This is what Microsoft is doing to generate the out-of-the-box controls.

Each control must be in a namespace which can be named anything you want.  If you change the namespace, remember it for later -- you need to put it in the manifest file.

The following code is the bare minimum needed to create a custom control.

namespace BGuidinger.Samples {
    export class PercentageControl {
        private props;

        init = function (context, notifyOutputChanged) {
            this.props = {
                onBlur: () => {
                    notifyOutputChanged();
                },
                onChange: (e) => {
                    this.props.value = e.target.value;
                }
            };
        }

        updateView = function (context) {
            this.props.value = context.parameters.value.raw;
            return context.factory.createElement("TEXTINPUT", this.props);
        }

        getOutputs() {
            return { value: this.props.value }
        }
    }
}

The context parameter to updateView has a lot of properties on it, but the most important is parameters.  This contains information about the control, including the  properties we specify in the control manifest (discussed next).  The context also contains the element creation factory which is basically a wrapper around React's createElement.  It is important to use this versus document.createElement as the latter creates a DOM element, and the document context may not be available on all devices.

Since this code is TypeScript, we need to transpile it to JavaScript.  We can do this using the TypeScript Compiler (tsc).

tsc PercentageControl.ts

With the generated JavaScript file, we can create our manifest file and add a reference to it.

ControlManifest.xml

The manifest file defines the behavior of the control.  It specifies the field types it works on, which platforms it is available on (web/phone/tablet), and most importantly, the resources that make up the control.  We can give the control a name and description key -- this matches a key from the RESX file.  We can also specify additional third-party libraries to include.

Also of importance is the control-type.  By setting this to "virtual", Dynamcis 365 will render this control using React's Virtual DOM.  From what I can tell, some controls are still using the "old" method (with jQuery), but I'm assuming those will be transitioned to virtual controls as well.  This will probably happen when v10 is rolled out as Steve Mordue alludes to on his blog.  If you've used React before, you know how much faster it is versus direct DOM manipulation using jQuery.

<manifest>
  <control namespace="BGuidinger.Samples" constructor="PercentageControl" version="1.0.0" control-type="virtual" display-name-key="CC_PercentageControl_Name" description-key="CC_PercentageControl_Desc">
    <modes>
      <read />
      <edit />
    </modes>
    <property name="value" display-name-key="CC_Value" description-key="CC_Value_Desc" of-type="Decimal" usage="bound" required="true" hidden="true" />
    <AvailableOn>
      <web classic="enable" />
      <phone />
      <tablet />
    </AvailableOn>
    <resources>
      <code path="PercentageControl.js" order="1" />
      <resx path="strings/PercentageControl.1033.resx" version="1.0.0" />
    </resources>
  </control>
</manifest>

There is additional functionality not shown here, such as including third-party libraries, stylesheets, and adding additional properties (which are configurable when setting up the control on a field in Dynamics 365).

Deployment

Now that we have our files, how do we add this to a solution and deploy it to Dynamics 365?  Well, in the absence of any supported tools to do this, we must resort to hacking the solution.xml and customizations.xml files.  Fortunately, this is pretty straightforward.

First, modify the solution.xml file and add a new RootComponent.  This component has a type of 66 which is a Custom Control.

<ImportExportXml>
  <SolutionManifest>
    <RootComponents>
      <RootComponent type="66" schemaName="BGuidinger.Samples.PercentageControl" behavior="0" />
    </RootComponents>
  </SolutionManifest>
</ImportExportXml>

Next, modify the customizations.xml to tell it where the manifest file is located.  Notice that we only tell the solution where the manifest is.  The rest of the files are referenced directly from the manifest.  This lets you setup the folder structure however you see fit.

<ImportExportXml xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <CustomControls>
    <CustomControl>
      <Name>Manifest0.xml</Name>
      <FileName>/Controls/PercentageControl/ControlManifest.xml</FileName>
    </CustomControl>
  </CustomControls>
  <Languages>
    <Language>1033</Language>
  </Languages>
</ImportExportXml>

With that, you can package all of the components up and import your solution into your environment.  Don't forget to publish the customizations!

Configuration

Now that we have the custom control in the system, we can configure it.  Open up a form that has a decimal field on it.  Edit the field properties and flip to the Custom Controls tab, then add a new control.  You'll notice that the Percentage control is in the list now!  When you click it you will see the information from our manifest, including the description and modes.

CustomControls_Add.PNG

After you add the control, make sure you enable it for Web (and/or Tablet/Phone), and then save and publish your form.  Since these controls only work on Unified Interface apps (i.e. Sales Hub, phone/tablet app, or the new App for Outlook), open one of those up and check out the form.  You should see the custom control!

Here it is on the Web app:

CustomControl_UI_Web.PNG

And here it is on the mobile app:

CustomControl_UI_Phone.PNG

This control is very basic.  It has no styles, no validation/error messages, and doesn't accept any properties.  Through the next few posts, we will explore the available functionality to enhance the control.  Stay tuned!

You can download the solution file with all of the components here.

Comments

Andreas Cieslik

Great article! Can't wait for the next part!

Andreas Cieslik

Bob Guidinger

You guys shouldn't have to wait long - I'm planning on posting it tomorrow!

Bob Guidinger