Create Custom Controls using PowerApp Component Framework

So many new features came up in April release but the one I was very excited about was creating custom controls using PowerApp Component Framework. If you are not sure what custom control is please read this article.

I am documenting my experience of developing the custom controls (end-to-end) in this blog.

Prepare your development environment

Installations

  1. Install npm from here. NTS or Current – any version of npm will do
  2. If you don’t have Visual Studio 2017 then download and install VS2017
  3. Download and Install PowerApp CLI on your machine from here

Currently PowerApps CLI is supported only on Windows 10

Create & configure PowerApp Component Framework components

  1. Create or identify your working folder. In my case it is “C:\Users\dnaglekar\Source\Workspaces\xxx\PowerAppCustomControl”
  2. Open Visual Studio command prompt
  3. Navigate to you recently created folder (step 1) using cd command. In my case it is
    cd C:\Users\dnaglekar\Source\Workspaces\xxx\PowerAppCustomControl
  4. Run the pac command to create new component project. Command is as follows:
    pac pcf init --namespace [Your Namespace] --name [Your Component Name] --template [Component Type]
    

    In my case, following is the pac command:

    pac pcf init --namespace DanzMaverick --name Tags --template field

    As of today there are two component types: field and dataset.

  5. Now we want to retrieve all project dependencies. To do that we want to run the following command:
    npm install

Develop custom control

Now that the project is ready; navigate in your project folder. You can use any development tool to implement your custom control. I have used Visual Studio 2017. Below are the steps to be followed to successfully develop your control.

Following will be the file structure you will find after all project dependencies are installed.

ProjectFiles

As you can see there is one folder named “Tags” (you will see different folder as per the name of your control), that was the name of the control I gave in my pac command. Navigate inside that particular folder and you will find the following structure.

ProjectSubFolderFiles

Configure Manifest file

ControlManifest.Input.xml

This is a very important file. It contains the component’s definition. Open this file in your favorite developer interface.

First thing you would need to change in this file is the definition of the custom control. You will see an example configuration with a tag named “control” – shown below:

<control namespace="DanzMaverick" constructor="Tags" version="0.0.1" display-name-key="Control_Display_Key" description-key="Control_Desc_Key" control-type="standard">
Following are the attributes of control tag:
  • namespace: this was provided earlier in the pac command
  • constructor: this is the name of your control that you had provided in the pac command
  • version: change it if needed; else keep what is
  • display-name-key: change it to be the display name (no spaces) of your custom control
  • description-key: change it to be the description of your custom control that you want to show in D365
  • control-type: do not change this

Next you will find is a tag named “property” – shown below:

<property name="sampleProperty" display-name-key="Property_Display_Key" description-key="Property_Desc_Key" of-type="SingleLine.Text" usage="bound" required="true" />

Following are the attributes of property tag:

  • name: change it to be the name of your control
  • display-name-key: change it to be the display name (no spaces) of your custom control
  • description-key: change it to be the description of your custom control that you want to show in D365
  • of-type: if your control is going to support only single data-type then use of-type attribute.
    Valid values are:

    • Currency
    • DateAndTime.DateAndTime
    • DateAndTime.DateOnly
    • Decimal
    • Enum
    • FP
    • Multiple
    • Optionset
    • SingleLine.Email
    • SingleLine.Phone
    • SingleLine.Text
    • SingleLine.TextArea
    • SingleLine.Ticker
    • SingleLine.URL
    • TwoOptions
    • Whole.None

    More information about of-type values is here

  • of-type-group: if your control is going to support multiple data-type then use of-type-group attribute. If you use this attribute then you have to define the type-group tag and the name of that type-group should be mentioned here.
    Below is a sample for defining a type-group:

    <type-group name="numbers"> 
      <type>Whole.None</type>
      <type>Currency</type>
      <type>FP</type>
      <type>Decimal</type>
     </type-group>
    

After you configured control and property tags; you should move towards the end of manifest file. You’ll find resource tag. This tag lists three type of child tags; code, css & resx.

  • code: it contains the relative path to the typescript file that will contain the code for our custom control
  • css: it contains the relative path of the css file that our custom control will use while rendering the controls
  • resx: it contains the relative path of resx file that will contain localized string

Make sure that if you are referencing relative path; it is correct

My final manifest file looks like below:

<?xml version="1.0" encoding="utf-8" ?>
<manifest>
  <control namespace="DanzMaverick" constructor="Tags" version="0.0.1" display-name-key="DanzMaverick.Tags" description-key="Takes your comma separated string and converts it into Tags" control-type="standard">
    <!-- property node identifies a specific, configurable piece of data that the control expects from CDS -->
    <type-group name="forTags">
      <type>SingleLine.Text</type>
      <type>SingleLine.TextArea</type>
    </type-group>
    <property name="Tags" display-name-key="Tags" description-key="Tags" of-type-group="forTags" usage="bound" required="true" />
    <!-- 
      Property node's of-type attribute can be of-type-group attribute. 
      Example:
      <type-group name="numbers">
        <type>Whole.None</type>
        <type>Currency</type>
        <type>FP</type>
        <type>Decimal</type>
      </type-group>
      <property name="sampleProperty" display-name-key="Property_Display_Key" description-key="Property_Desc_Key" of-type-group="numbers" usage="bound" required="true" />
    -->
    <resources>
      <code path="index.ts" order="1"/>
      <css path="css/index.css" order="1" />
      <!-- UNCOMMENT TO ADD MORE RESOURCES
      <css path="css/Tags.css" order="1" />
      <resx path="strings/Tags.1033.resx" version="1.0.0" />
      -->
    </resources>
  </control>
</manifest>

Implement logic in typescript

First open the index.ts file in your favorite development tool. I am using Visual Studio 2017. You will find following methods:

  • init: this will be the first method that system will invoke. All your design should happen in this method
  • updateView: this method is invoked when property bag is changed; which includes fields, data-sets, global variables such as height and/or width
  • getOutputs: this method is called prior to receiving any data
  • destroy: add your cleanup code here

In my case as I am creating a control to read a comma-separated value in the text field and create tag-like elements. So follow along with manipulating the index.ts file.

HTML Elements

Let’s start by adding HTML elements we need. As we are writing a .ts file we do not have HTML tags (per say). But using the power of typescript we are going to create dynamic HTML elements in the form of variables and later use them in the init function to add it on the control.

import { IInputs, IOutputs } from "./generated/ManifestTypes";

export class Tags implements ComponentFramework.StandardControl {

/**
* Variables for HTML element
*/
private tagsElement: HTMLElement;
private spaceElement: HTMLElement;
private refreshButton: HTMLElement;

I have added an element for my tags and space. I have also added a button for refresh. I am going for the following look & feel.

Pill

Global Variables

Now that I have decided what all HTML elements I need; let’s decide all variables I may need during the implementation process. I have decided I will need a variable to store the value that will contain a comma-separated string. So adding to the above code tagsString will be the variable that will contain the attribute value.

import { IInputs, IOutputs } from "./generated/ManifestTypes";

export class Tags implements ComponentFramework.StandardControl {

/**
* Variables for HTML element
*/
private tagsElement: HTMLElement;
private spaceElement: HTMLElement;
private refreshButton: HTMLElement;

/**
* Variables for Properties
*/
private tagsString: string;

As I said it is not an HTML so we have to define variable for all. Hence, let’s define variable for event listener for the “refresh button”. Adding to the same code and expanding it – “refreshClicked” will be my event listener when “Refresh” button is clicked.

import { IInputs, IOutputs } from "./generated/ManifestTypes";

export class Tags implements ComponentFramework.StandardControl {

/**
* Variables for HTML element
*/
private tagsElement: HTMLElement;
private spaceElement: HTMLElement;
private refreshButton: HTMLElement;
private divElement: HTMLElement;

/**
* Variables for Properties
*/
private tagsString: string;

/**
* Variables for Event Listener
*/
private refreshClicked: EventListenerOrEventListenerObject;

We also need some local variable to capture the input parameter from init function. Expanding the code; it looks like below.

import { IInputs, IOutputs } from "./generated/ManifestTypes";

export class Tags implements ComponentFramework.StandardControl {

/**
* Variables for HTML element
*/
private tagsElement: HTMLElement;
private spaceElement: HTMLElement;
private refreshButton: HTMLElement;
private divElement: HTMLElement;

/**
* Variables for Properties
*/
private tagsString: string;

/**
* Variables for Event Listener
*/
private refreshClicked: EventListenerOrEventListenerObject;

/**
* Local Variables
*/
private localContext: ComponentFramework.Context;
private localNotifyOutputChanged: () => void;
private localContainer: HTMLDivElement;
init() function

In this function, let’s first initialize local variables from the input parameters. Then create a functions to handle events; in my case, I have created refreshClick() function. Now, bind the event handlers to the variables defined for event listening.

From here onward the logic to add the HTML elements may defer based on the logic, in my case, I am adding refresh button first to the body of the control; to do so I have written the below code.

// Refresh button
this.refreshButton = document.createElement("button");
this.refreshButton.setAttribute("type", "button");
this.refreshButton.setAttribute("value", "Refresh");
this.refreshButton.setAttribute("class", "btn btn-default btn-sm glyphicon glyphicon-refresh");
this.refreshButton.addEventListener("click", this.refreshClick);

// Add elements to container
this.localContainer.appendChild(this.refreshButton);

Here, I initialized my variable as input tag using document.createElement. Then I defined some attributes to that tag; in this case, type & value. Later, I added the event listener to that element. Finally, I add my element to the div container.

Now, I read the attribute value bound to the control from the context, split the values based on comma and in the for-loop I initialize my tags tag as span HTML element and set class attributes on that element.

Below is my entire init() function

/**
* Used to initialize the control instance. Controls can kick off remote server calls and other initialization actions here.
 * Data-set values are not initialized here, use updateView.
 * @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to property names defined in the manifest, as well as utility functions.
 * @param notifyOutputChanged A callback method to alert the framework that the control has new outputs ready to be retrieved asynchronously.
 * @param state A piece of data that persists in one session for a single user. Can be set at any point in a controls life cycle by calling 'setControlState' in the Mode interface.
 * @param container If a control is marked control-type='starndard', it will receive an empty div element within which it can render its content.
 */
public init(context: ComponentFramework.Context, notifyOutputChanged: () => void, state: ComponentFramework.Dictionary, container: HTMLDivElement) {
	// Init local variables
	this.localContext = context;
	this.localNotifyOutputChanged = notifyOutputChanged;
	this.localContainer = container;

	// Register EventHandler
	this.refreshClicked = this.refreshClick.bind(this);

	// Refresh button
	this.refreshButton = document.createElement("button");
	this.refreshButton.setAttribute("type", "button");
	this.refreshButton.setAttribute("value", "Refresh");
	this.refreshButton.setAttribute("class", "btn btn-default btn-sm glyphicon glyphicon-refresh");
	this.refreshButton.addEventListener("click", this.refreshClick);

	// Add elements to container
	this.localContainer.appendChild(this.refreshButton);

	// CRM attributes bound to the control properties. 
	// @ts-ignore 
	var crmTagStringsAttribute = context.parameters.Tags.attributes.LogicalName;

	// @ts-ignore 
	var crmTagStringsAttributeValue = Xrm.Page.getAttribute(crmTagStringsAttribute).getValue();
	var data = crmTagStringsAttributeValue.split(",");

	for (var i in data) {
		// Create controls
		// Tag element
		this.tagsElement = document.createElement("span");
		this.tagsElement.setAttribute("class", "badge badge-pill badge-primary");
		var ele = this.localContainer.appendChild(this.tagsElement);
		ele.innerHTML = data[i];

		// Space element
		this.spaceElement = document.createElement("span");
		var space = this.localContainer.appendChild(this.spaceElement);
		space.innerHTML = "  ";
	}
}

Below is my refreshClick() function

/**
* Custom Event Handlers
 */
public refreshClick(evnt: Event): void {
	this.localNotifyOutputChanged();
}
updateView() function

In here, we need to refresh the HTML elements based on the new data entered on the screen.

Below is my updateView() function

/**
* Called when any value in the property bag has changed. This includes field values, data-sets, global values such as container height and width, offline status, control metadata values such as label, visible, etc.
 * @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to names defined in the manifest, as well as utility functions
 */
public updateView(context: ComponentFramework.Context): void {
	// Add code to update control view

        // CRM attributes bound to the control properties. 
        // @ts-ignore 
        var crmTagStringsAttribute = this.localContext != null && this.localContext.parameters != null ? this.localContext.parameters.Tags.attributes.LogicalName : null;

        // @ts-ignore 
        var crmTagStringsAttributeValue = crmTagStringsAttribute != null ? Xrm.Page.getAttribute(crmTagStringsAttribute).getValue() : "red,green,blue";
        var data = crmTagStringsAttributeValue.split(",");

        // Delete all elements first
        var tagCollection = this.localContainer.getElementsByTagName("span");
        var loopLength = tagCollection.length;
        for (var ti = 0; ti < loopLength; ti++) {
            this.localContainer.removeChild(tagCollection[0]);
        }

        // Add new tags
        for (var i in data) {
            // Create controls
            // Tag element
            this.tagsElement = document.createElement("span");
            this.tagsElement.setAttribute("class", "badge badge-pill badge-primary");

            var ele = this.localContainer.appendChild(this.tagsElement);
            ele.innerHTML = data[i];

            // Space element
            this.spaceElement = document.createElement("span");

            var space = this.localContainer.appendChild(this.spaceElement);
            space.innerHTML = "  ";
        }
}

Use // @ts-ignore to use Xrm.Page module

My entire index.ts code file

import { IInputs, IOutputs } from "./generated/ManifestTypes";

export class Tags implements ComponentFramework.StandardControl {

    /**
     * Variables for HTML element
     */
    private tagsElement: HTMLElement;
    private spaceElement: HTMLElement;
    private refreshButton: HTMLElement;
    private divElement: HTMLElement;

    /**
     * Variables for Properties
     */
    private tagsString: string;

    /**
     * Variables for Event Listener
     */
    private refreshClicked: EventListenerOrEventListenerObject;

    /**
     * Local Variables
     */
    private localContext: ComponentFramework.Context;
    private localNotifyOutputChanged: () => void;
    private localContainer: HTMLDivElement;

	/**
	 * Empty constructor.
	 */
    constructor() {

    }

    /**
	 * Used to initialize the control instance. Controls can kick off remote server calls and other initialization actions here.
	 * Data-set values are not initialized here, use updateView.
	 * @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to property names defined in the manifest, as well as utility functions.
	 * @param notifyOutputChanged A callback method to alert the framework that the control has new outputs ready to be retrieved asynchronously.
	 * @param state A piece of data that persists in one session for a single user. Can be set at any point in a controls life cycle by calling 'setControlState' in the Mode interface.
	 * @param container If a control is marked control-type='starndard', it will receive an empty div element within which it can render its content.
	 */
    public init(context: ComponentFramework.Context, notifyOutputChanged: () => void, state: ComponentFramework.Dictionary, container: HTMLDivElement) {
        // Init local variables
        this.localContext = context;
        this.localNotifyOutputChanged = notifyOutputChanged;
        this.localContainer = container;

        // Register EventHandler
        this.refreshClicked = this.refreshClick.bind(this);

        // Refresh button
        this.refreshButton = document.createElement("button");
        this.refreshButton.setAttribute("type", "button");
        this.refreshButton.setAttribute("value", "Refresh");
        this.refreshButton.setAttribute("class", "btn btn-default btn-sm glyphicon glyphicon-refresh");
        this.refreshButton.addEventListener("click", this.refreshClick);

        // Add elements to container
        this.localContainer.appendChild(this.refreshButton);

        // CRM attributes bound to the control properties. 
        // @ts-ignore 
        var crmTagStringsAttribute = context.parameters.Tags.attributes.LogicalName;

        // @ts-ignore 
        var crmTagStringsAttributeValue = Xrm.Page.getAttribute(crmTagStringsAttribute).getValue();
        var data = crmTagStringsAttributeValue.split(",");

        for (var i in data) {
            // Create controls
            // Tag element
            this.tagsElement = document.createElement("span");
            this.tagsElement.setAttribute("class", "badge badge-pill badge-primary");

            var ele = this.localContainer.appendChild(this.tagsElement);
            ele.innerHTML = data[i];

            // Space element
            this.spaceElement = document.createElement("span");

            var space = this.localContainer.appendChild(this.spaceElement);
            space.innerHTML = "  ";
        }
    }


	/**
	 * Called when any value in the property bag has changed. This includes field values, data-sets, global values such as container height and width, offline status, control metadata values such as label, visible, etc.
	 * @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to names defined in the manifest, as well as utility functions
	 */
    public updateView(context: ComponentFramework.Context): void {
        // Add code to update control view

        // CRM attributes bound to the control properties. 
        // @ts-ignore 
        var crmTagStringsAttribute = this.localContext != null && this.localContext.parameters != null ? this.localContext.parameters.Tags.attributes.LogicalName : null;

        // @ts-ignore 
        var crmTagStringsAttributeValue = crmTagStringsAttribute != null ? Xrm.Page.getAttribute(crmTagStringsAttribute).getValue() : "red,green,blue";
        var data = crmTagStringsAttributeValue.split(",");

        // Delete all elements first
        var tagCollection = this.localContainer.getElementsByTagName("span");
        var loopLength = tagCollection.length;
        for (var ti = 0; ti < loopLength; ti++) {
            this.localContainer.removeChild(tagCollection[0]);
        }

        // Add new tags
        for (var i in data) {
            // Create controls
            // Tag element
            this.tagsElement = document.createElement("span");
            this.tagsElement.setAttribute("class", "badge badge-pill badge-primary");

            var ele = this.localContainer.appendChild(this.tagsElement);
            ele.innerHTML = data[i];

            // Space element
            this.spaceElement = document.createElement("span");

            var space = this.localContainer.appendChild(this.spaceElement);
            space.innerHTML = "  ";
        }
    }

	/** 
	 * It is called by the framework prior to a control receiving new data. 
	 * @returns an object based on nomenclature defined in manifest, expecting object[s] for property marked as “bound” or “output”
	 */
    public getOutputs(): IOutputs {
        return {
            Tags: this.tagsString
        };
    }

	/** 
	 * Called when the control is to be removed from the DOM tree. Controls should use this call for cleanup.
	 * i.e. cancelling any pending remote calls, removing listeners, etc.
	 */
    public destroy(): void {
        // remove the event handlers. 
        this.refreshButton.removeEventListener("click", this.refreshClick);
    }

    /**
     * Custom Event Handlers
     */
    public refreshClick(evnt: Event): void {
        this.localNotifyOutputChanged();
    }
}

Customize UI using CSS

As you may have observed I have used class attributes on my HTML tags but that would not render unless I add the class definitions in the .css file

.badge-primary {
    color: #fff;
    background-color: #007bff;
}

.badge-pill {
    padding-right: 0.6em;
    padding-left: 0.6em;
    border-radius: 10rem;
}

.badge {
    display: inline-block;
    padding: 0.25em 0.4em;
    font-size: 75%;
    font-weight: bold;
    line-height: 1;
    color: #fff;
    text-align: center;
    white-space: nowrap;
    vertical-align: baseline;
    border-radius: 0.25rem;
}

button {
    text-rendering: auto;
    color: initial;
    letter-spacing: normal;
    word-spacing: normal;
    text-transform: none;
    text-indent: 0px;
    text-shadow: none;
    display: inline-block;
    text-align: start;
    margin: 0em;
    font: 400 13.3333px Arial;
}

.btn-sm {
    padding: 5px 10px;
    font-size: 12px;
    line-height: 1.5;
    border-radius: 3px;
}

.btn-default {
    color: #333;
    background-color: #fff;
    border-color: #ccc;
}

.btn {
    display: inline-block;
    padding: 6px 12px;
    margin-bottom: 0;
    font-size: 14px;
    font-weight: 400;
    line-height: 1.42857143;
    text-align: center;
    white-space: nowrap;
    vertical-align: middle;
    -ms-touch-action: manipulation;
    touch-action: manipulation;
    cursor: pointer;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
    background-image: none;
    border: 1px solid transparent;
    border-radius: 4px;
}

.glyphicon {
    position: relative;
    top: 1px;
    display: inline-block;
    font-family: 'Glyphicons Halflings';
    font-style: normal;
    font-weight: 400;
    line-height: 1;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

.glyphicon-refresh:before {
    content: "\e031";
}

Build the project

Again, open Visual Studio command prompt and navigate to the folder where the index.ts file resides using cd command.

To build the package, run npm run build command as shown below.

build-command

If the build is succeeded, then we can test it by running the control in a browser. To do so, you will need to use npm start command as shown below. In my case, as init() function needs an input to load I have hard-coded some default values to test.

run-command

When testing in the browser; screen might look something like this. In my case as I do not have any output sent back I don’t see anything in there but if you have output sent back from the control you’ll see your output variables values as well.

testing-screen

You can use debugger in your index.ts file and using F12 you can do a step-by-step debugging

Create a solution package for D365 CE

For this create a new directory inside your control folder. In my case I have created a folder named “deployment” inside the “Tags” folder.

deployment-folder

Now, use cd command to navigate inside this newly created folder and run the following command to create a new solution project for D365 CE.

pac solution init --publisherName [publisher name] --customizationPrefix [publisher prefix]

For example, in my case the command was

pac solution init --publisherName DanzMaverick --customizationPrefix dm

Once the solution project is created we need to add the component into this solution. To do this, we need to use the following command. The path needs to be where the project file (pcfproj) resides

pac solution add-reference --path [path or relative path of your PowerApps component framework project on disk]

In my case the command was

pac solution add-reference --path C:\Users\dnaglekar\Source\Workspaces\xxx\PowerAppCustomControl

deployment-screen

Once the above command is executed, you’ll see deployment.cdsproj created.

deployment-folder-after-command-executes

We now have to run few more commands to create the .zip file we need for importing the solution in D365 CE. To do so, we need to execute msbuild /t:restore command followed by msbuild command.

After running both the command, if you navigate to your “deployment” folder (bin->debug) you should see a .zip file created.

SolutionZipFile

Import this solution zip file in any of your favorite D365 CE instance and publish customization.

Custom Control in D365 CE

Let’s look at how to configure the custom control in D365 CE. I have created a custom attribute called “Tag” on “Account” entity which contains comma-separated value. I am going to customized the form to add a control to this attribute.

Customization-Custom-Control.gif

Once the form is customized and published; let’s navigate to the Account form and see the custom control in action.

CustomControl-Final

Update-InAction

Based on the data it created several tags. A basic custom control to give you a glimpse of what we can do with it.

Please leave a feedback or comment below. Thank you

Advertisements

4 thoughts on “Create Custom Controls using PowerApp Component Framework

  1. I get an error every time I do the last step when creating the solution package. When I run the command “msbuild” I get the following message:

    C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\bin
    \Microsoft.Common.CurrentVersion.targets(2628,7): error MSB4057: The target “Cr
    eateManifestResourceNames” does not exist in the project. [C:\PCF\TestControls\
    TestControls.pcfproj]

    This test project was created following the instructions documented here but none of the customizing. I have updated NPM and Node.js to the latest version and still get the error. Any ideas?

    Like

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 )

Google photo

You are commenting using your Google 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