Create a custom control

1. Overview

This sample will show how to create a custom control that users can add to dashboards and other views just like built-in components. This is part 1 of 2, where the focus will be on a control that does not bind directly to Dundas BI data. This example demonstrates a sticky note control.

This article walks through:

> How to create a Dundas BI extension
> How to create a custom control

A custom dashboard with sticky note controls and a cork board background image
A custom dashboard with sticky note controls and a cork board background image

2. Getting started

The following prerequisites must be installed on your computer:

  • Visual Studio 2015, or higher
  • Microsoft .NET Framework 4.6.1
  • A deployed Dundas BI instance

2.1. Downloading sample solution

To download the custom sample control solution, click here.

(A solution is also available for Dundas BI versions 2 through 5.)

2.2. Extracting sample to SDK folder

This sample is designed to automatically publish the extension to the instance, but must be extracted to the SDK folder within the instance:

  1. Find the SDK folder for your instance. It is located at [instance root]\sdk.
  2. Extract the SampleControl.zip to the SDK folder.
  3. You should see the following result:

    Samples added to SDK folder
    Samples added to SDK folder

    With the following structure:

    [instance root]
        • sdk
            • Samples
                • SampleControl

Important
If the sample is not in this specific folder, it will not work correctly.

2.3. Opening solution

To open the Visual Studio solution:

  1. Right-click on the Microsoft Visual Studio shortcut, and click Run as administrator.
  2. Click the File menu, then Open Solution.
  3. Choose the solution located at: [instance root]\sdk\Samples\SampleControl\SampleControl.sln

3. The project

The project is a class library.

  • mycompany.controls.sample.stickynote.adapter.js - Contains the sticky note adapter class.
  • mycompany.controls.sample.stickynote.css - The custom style sheet for the sticky note adapter.
  • mycompany.controls.sample.stickynote.info.js - Gets the information about the control (such as what adapters it supports).
  • PublishExtension.targets - Used for auto publishing the extension after the build succeeds. This also copies the stylesheets, and hooks them up to the style override file.
  • SampleControlExtensionPackageInfo.cs - Contains the package information about the extension package.
  • StickyNoteAdapterInfo.cs - Contains the connection to JavaScript files, and defines the full JavaScript name.
  • StickyNoteExtension.cs - Contains the information about the adapters.
  • stickyNoteIcon.png - An icon to represent the sticky note in the toolbar.

3.1. Publish extension targets

This sample has a mechanism to automatically publish the extension, and copy the mycompany.controls.sample.stickynote.css file to the style overrides folder. Also, the stickyNoteIcon.png will be placed in the instance's images folder. Next, it will add the following line to the styles override file if it does not exist:

@import url("mycompany.controls.sample.stickynote.css");    

This mechanism is the PublishExtension.targets file which overrides the AfterBuild target. This will create the following files after successfully compiling the solution:

    • [instance root]\www\BIWebsite\App_Data\Extensions\SampleControl\bin\MyCompany.Sample.SampleControl.dll

    • [instance root]\www\BIWebsite\Content\Override\mycompany.controls.sample.stickynote.css

    • [instance root]\www\BIWebsite\Content\Images\icons\stickyNoteIcon.png

It will then touch the web.config to force the web application to reset.

Important
Some instances will not have a Dundas.BI.Web.Core.dll assembly in the [instance root]\sdk\bin folder. On first load of the project you will see a broken reference. In this case one will be copied to this folder in the BeforeBuild event when building the project for the first time.

3.2. ExtensionPackageInfo class

In order for a custom adapter or package to be read by Dundas BI, it needs to contain a class that extends the ExtensionPackageInfo class. This class contains the extension package information.

/// <summary>
/// This class contains the package information about the extension package.
/// </summary>
public class SampleControlExtensionPackageInfo : ExtensionPackageInfo
{
	#region Public Properties

	/// <summary>
	/// Gets the name of the extension package author.
	/// </summary>
	public override string Author
	{
		get { return "Dundas Data Visualization Sample Author"; }
	}

	/// <summary>
	/// Gets the copyright text associated with the extension package.
	/// </summary>
	public override string Copyright
	{
		get { return "Dundas Data Visualization, Inc."; }
	}

	/// <summary>
	/// Gets the localized display name of the extension package.
	/// </summary>
	public override string DisplayName
	{
		get { return "Sample Control Extension Package Info"; }
	}

	/// <summary>
	/// Gets the unique identifier of the extension package.
	/// </summary>
	public override Guid Id
	{
		get { return new Guid("38e2037a-6f0e-43d0-88d1-336d93374674"); }
	}

	/// <summary>
	/// Gets the name of the extension package.
	/// </summary>
	public override string Name
	{
		get { return "Sample Control Extension Package"; }
	}

	/// <summary>
	/// Gets the version of the extension package.
	/// </summary>
	public override Version Version
	{
		get { return new Version(0, 0, 1); }
	}

	#endregion Public Properties
}
        

3.3. Defining the AdapterInfo

Controls are 'plugged into' Dundas BI using adapters. Your adapter class is what will allow your custom control to behave the same as any built-in control in Dundas BI.

To create a custom adapter, you will first need to extend the AdapterInfo Class to provide information about it. In the constructor, you should give the full name of the JavaScript version of the Info class. In the case of the sticky note example, this is mycompany.controls.sample.stickynote.Info.

using Dundas.BI.Web.Extensibility.Parameters;

  ...

namespace MyCompany.Sample.SampleControl
{
    /// <summary>
    /// This class represents a stickynote adapter.
    /// </summary>
    public class StickyNoteAdapterInfo : AdapterInfo
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="StickyNoteAdapterInfo" /> class.
        /// </summary>
        public StickyNoteAdapterInfo()
        {
	        this.FullJavaScriptName = "mycompany.controls.sample.stickynote.Info";
        }        
        ...
    }
}

3.3.1. Overriding the GetResources method

The GetResources method is where the JavaScript file contents are defined. This example demonstrates how to read the project's embedded resource JavaScript files, and return them as a list of strings:

/// <summary>
/// Gets all the JavaScript resources as <see cref="T:System.string" />s for this info. 
/// The order they are returned should be the ordered they need to be included in.
/// </summary>
/// <returns>
/// The JavaScript resources as <see cref="T:System.string" />s. This might be null.
/// </returns>
public override IList<string> GetResources()
{
	return new List<string>
	{
		GetResourceFromCurrentAssembly(
		    "MyCompany.Sample.SampleControl.mycompany.controls.sample.stickynote.info.js"
		),
		GetResourceFromCurrentAssembly(
		    "MyCompany.BI.Sample.SampleControl.mycompany.controls.sample.stickynote.adapter.js"
		)
	};
}

/// <summary>
/// Gets the resource as string from current assembly.
/// </summary>
/// <param name="name">The full path to the resource.</param>
/// <returns>The resource as <see cref="T:System.string" />.</returns>
private string GetResourceFromCurrentAssembly(string name)
{
	System.Reflection.Assembly currentAssembly = typeof(StickyNoteAdapterInfo).Assembly;
	using (StreamReader streamReader = 
		new StreamReader(currentAssembly.GetManifestResourceStream(name)))
	{
		return streamReader.ReadToEnd();
	}
}

3.4. Implementing the IAdapterExtension interface

The IAdapterExtension Interface is used to define all the adapter info objects for a particular extension. The following demonstrates this interface implementation for the sticky note:

/// <summary>
/// The class represents the Sticky Note extension.
/// </summary>
public class StickyNoteExtension : IAdapterExtension
{
    /// <summary>
    /// Gets the all the adapter info objects for this particular extension.
    /// </summary>
    /// <returns>
    /// The adapter info objects for this extension.
    /// </returns>
    public IList<Web.Extensibility.Parameters.AdapterInfo> GetInfos()
    {
        return new List<AdapterInfo>() { new StickyNoteAdapterInfo() };
    }
}

3.5. The JavaScript info class

The sticky note info class extends the dundas.view.controls.Info Class. In the example below only the getControlInfos method was overridden. In this method we are passing the meta information about the control and what adapters it supports.

Note
It is highly recommended that your JavaScript files use the language's "use strict" directive. Strict mode changes previously-accepted syntax that could cause problems into real errors. For more information on strict mode, click here.

"use strict";

window.mycompany = {};
window.mycompany.controls = {};
window.mycompany.controls.sample = {};
window.mycompany.controls.sample.stickynote = {};

// Sandbox.
(function ($) {
    // The sticky note control class infos.
    mycompany.controls.sample.stickynote.Info = dundas.view.controls.Info.extend({
        init: function () {
            /// <summary>
            /// Initializes the Info. Called during construction.
            /// </summary>
            
        },
        getControlInfos: function () {
            /// <summary>
            /// Gets the information about the control (such as what adapters it supports).
            /// </summary>
            /// <returns type="Object">
            /// Meta-information about the control and what adapters it supports.
            /// </returns>
            var infos = [{
                "categoryName": "Components",
                "subCategoryName": "Fun",
                "caption": "Stickynote extension",
                "description": "Simple stickynote sample control",
                "adapterObjectName": 'mycompany.controls.sample.stickynote.Adapter',
                "defaultWidth": 350,
                "defaultHeight": 200,
                "toolbarIconUrl": dundas.Utility.getContextualUrlPath(
                    'Content/Images/icons/stickyNoteIcon.png'
                )
            }];
            return infos;
        }
    });
})(jQuery);

The sticky note adapter in the toolbar.
The sticky note adapter in the toolbar.

3.6. The JavaScript adapter class

The adapter class is what allows your custom control to work with Dundas BI. It extends the dundas.view.controls.Adapter Class. The following demonstrates how to create this class:

(function ($) {
    mycompany.controls.sample.stickynote.Adapter = dundas.view.controls.Adapter.extend({
        init: function (adapterOptions) {
            /// <summary>
            /// Initializes the Adapter. Called during construction.
            /// </summary>
            /// <param name="adapterOptions" type="Object" optional="true">(optional) 
            /// An object literal parameter specifying default values.
            /// </param>

            // Call base.
            this._super(adapterOptions);
        }
})(jQuery);

3.6.1. Creating the control and elements

The place to define the control and elements is on the adapter's onLoaded method. This is called when the adapter is loaded and set up. The control should be set up by the adapter now and added to the DOM, using the container element in the container property. Implementers of this method must call this._super() to invoke the base implementation for inherited actions and events to be set up correctly.

In the following example, we create three DOM elements in a _createControl method if our control has not been initialized. Each element is a div element.

onLoaded: function () {
    /// <summary>
    /// Called when the adapter is loaded and setup.
    /// The control should be setup by the adapter now and added to the DOM (container property).
    /// Implementers of this method must call this._super() at the end if they want actions
    /// and events to be automatically setup.
    /// </summary>

    // Setup my UI control here.
    if (this.control == null) {
        this._createControl();
    }

    this._super();

    this._stickyNoteContainer.click(function () {
        $(this).fadeOut("fast");
    });
},

_createControl: function() {
    // Control.

    this._stickyNoteContainer = $(document.createElement("div"))
        .addClass(Constants.StickyNoteContainerCssClass)
        .appendTo($(this.container));

    this.control = $(document.createElement("div"))
        .addClass(Constants.StickyNoteControlCssClass)
        .appendTo(this._stickyNoteContainer);

    // Label.
    this._label = $(document.createElement("div"))
        .addClass(Constants.StickyNoteControlLabelCssClass)
        .appendTo(this.control);

    // Set default font family and size
    this.fontFamily = "'Reenie Beanie'";
    this.fontSize = "36px";

    // Set default properties for the control.
    this.stickynoteText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";

    var randomAngle = Math.floor((Math.random() * 4));
    randomAngle = (Math.random() < 0.5) ? -randomAngle : randomAngle;

    this.angle = randomAngle;

    var randomColorIndex = Math.floor(Math.random() * 3);

    switch (randomColorIndex) {
        case 0:
            this.background = new dundas.controls.SolidColorBrush(dundas.controls.Color.fromString("#ffc"));
            break;
        case 1:
            this.background = new dundas.controls.SolidColorBrush(dundas.controls.Color.fromString("#cfc"));
            break;
        case 2:
            this.background = new dundas.controls.SolidColorBrush(dundas.controls.Color.fromString("#ccf"));
            break;
    }
}

3.6.2. Defining the properties

Control adapters can expose 'properties' to allow users to customize the control's settings using the Properties window in the UI.

Note
Defining properties is optional if users do not need to change settings.

The next step is to override the getControlProperties method. In this method, we will return an Object Descriptor that lists and describes the available properties on the control or one of its elements. The following example shows the implementation of this method for the text, angle, and background properties for the sticky note adapter:

getControlProperties: function (path) {
    /// <summary>
    /// Called to get the object with the list of properties on the root control, or any sub-element
    /// of the control.
    /// </summary>
    /// <param name="path" type="Array" elementType="dundas.view.controls.PropertyPathItem" optional="true">(optional) The path to the sub-element 
    /// of this control. Undefined if the root is requested, otherwise an array of plain objects with properties.</param>
    /// <returns type="dundas.controls.ObjectDescriptor">An object with the list of properties on the desired element.</returns>

    var objectDescriptor,
        propertyDescriptor,
        elementName;

    // Base path properties.
    if (!path) {
        // Object descriptor.
        objectDescriptor = this._super(path);

        // Label text.
        propertyDescriptor = new dundas.StringPropertyDescriptor();
        propertyDescriptor.name = "Stickynote Text";
        propertyDescriptor.id = "stickynoteText";
        propertyDescriptor.category = "GS_PropertyGrid_Category_Common".localize();
        propertyDescriptor.description = "Sample Sticky note control";
        propertyDescriptor.value = this.stickynoteText;
        propertyDescriptor.defaultValue = "Lorem Ipsum";
        propertyDescriptor.section = dundas.controls.PropertyGridSections.Text;

        objectDescriptor.addProperty(propertyDescriptor);

        // Angle.
        propertyDescriptor = new dundas.NumericPropertyDescriptor;
        propertyDescriptor.name = "Angle";
        propertyDescriptor.id = "angle";
        propertyDescriptor.category = "GS_PropertyGrid_Category_Common".localize();
        propertyDescriptor.description = "The angle.";
        propertyDescriptor.value = this.angle;
        propertyDescriptor.defaultValue = -3;
        propertyDescriptor.section = dundas.controls.PropertyGridSections.Look;

        objectDescriptor.addProperty(propertyDescriptor);

        // Background.
        propertyDescriptor = new dundas.PropertyDescriptor();
        propertyDescriptor.name = "Background";
        propertyDescriptor.id = "background";
        propertyDescriptor.category = "GS_PropertyGrid_Category_Common".localize();
        propertyDescriptor.description = "The background";
        propertyDescriptor.value = this.background;
        propertyDescriptor.defaultValue = new dundas.controls.SolidColorBrush(dundas.controls.Color.fromString("#cfc"));
        propertyDescriptor.isNullable = true;
        propertyDescriptor.section = dundas.controls.PropertyGridSections.Look;

        objectDescriptor.addProperty(propertyDescriptor);
    }
           
    return objectDescriptor;
}

The property descriptors above expose settings to the UI. The real properties in code that store settings values could be defined either on the adapter class, or in the control itself, depending on your implementation. Each adapter class should implement a __classtype property, however.

The following example stores settings values in properties defined on the adapter, and applies the settings values directly to the DOM when set:

__properties: {
    "__classType": {
        get: function () {
            return "mycompany.controls.sample.stickynote.Adapter";
        }
    },

    "stickynoteText": {
        /// <summary>
        /// Gets or sets the text label displayed in the control.
        /// </summary>
        /// <value type="String">The value to display.</value>
        get: function () {
            return this._stickynoteText;
        },
        set: function (value) {
            this._stickynoteText = value;
            if(this._label) {
                this._label.text(value);
            }
        },
        notEnumerable: true
    },

    "background": {
        /// <summary>
        /// Gets or sets the background property of the control.
        /// </summary>
        /// <value type="dundas.controls.SolidColorBrush" mayBeNull="true"></value>
        get: function () {
            return this._background;
        },
        set: function (brush) {
            this._background = brush;
            if (this.control) {
                if (brush) {
                    brush.applyToElement(this.control.get(0), 'background');
                }
                else {
                    $(this.container).css({ background: '' });
                }
            }
        },
        notEnumerable: true
    },

    "angle": {
        /// <summary>
        /// Gets or sets the angle of the control.
        /// </summary>
        /// <value type="Number" mayBeNull="true">The angle of the control.</value>
        get: function () {
            return this._angle;
        },
        set: function (value) {
            this._angle = value;
            if (this.control) {
                this.control.css("-o-transform", "rotate(" + value + "deg)");
                this.control.css("-webkit-transform", "rotate(" + value + "deg)");
                this.control.css("-moz-transform", "rotate(" + value + "deg)");

            }
        },
        notEnumerable: true
    }
}

Note
If you define properties on the adapter, they should set notEnumerable to true as shown above to prevent them from being serialized (properties beginning with an underscore are already not serialized). The control's state is saved separately, as shown in a later section.

3.6.3. Setting the adapter name prefix

It is recommended to override the _getAdapterNamePrefix method to return a nice name for when the control is added to the dashboard. For example, the following code example will result in the name sequence (stickynote1, stickynote2, etc.):

_getAdapterNamePrefix: function () {
/// <summary>
/// Should be overridden by implementing adapters to return a nice name for name generation 
/// (just prefix).
/// </summary>
/// <returns type="String">The prefix to use for any generated names.</returns>
    return "stickynote";
}

The sticky note adapter name prefix
The sticky note adapter name prefix

3.6.4. Getting and saving the state of the control

Note
This is not required if there are no user settings (such as properties) that need to be saved.

In order for the state to be saved, the adapter must override the stringifyControl method. This returns a string version of the underlying control's state. To load the control state, the adapter must override the parseControl method, to accept the string version of the state and restore it to the control. The base implementation should be invoked by calling this._super to correctly handle inherited properties.

The following is the sticky note implementation for getting and saving the state of the control.

// Helper methods.
var Utility = {
    parseItem: function (item) {
        return Class.fromJSON(JSON.stringify(item || {}));
    },
    parseItems: function (items) {
        return $.map(items || [], function (item, index) {
            return Utility.parseItem(item);
        });
    }
};

   ...

stringifyControl: function (isStyleString) {
    /// <summary>
    /// Called to instruct this adapter to get a stringified version of the underlying control.
    /// </summary>
    /// <param name="isStyleString" type="Boolean" optional="true">(optional) Indicates whether this stringify request is for a style.</param>
    /// <returns type="String">A string representation of the underlying control which can be stored.</returns>
           
    // Call the base.
    var o = JSON.parse(this._super()) || {};

    o.background = this.background;
    o.angle = this.angle;
    o.stickynoteText = this.stickynoteText;

    return JSON.stringify(o);
},

parseControl: function (objectString, isStyleString) {
    /// <summary>
    /// Called to instruct the adapter to set the control up based on the stringified version.
    /// </summary>
    /// <param name="objectString" type="String">The stringified version of the underlying control.</param>
    /// <param name="isStyleString" type="Boolean" optional="true">(optional) Indicates whether this stringify request is for a style.</param>
            
    var o = JSON.parse(objectString);

    if (this.control == null) {
        this._createControl();
    }

    this.background = Utility.parseItem(o.background);
    this.stickynoteText = o.stickynoteText;
    this.angle = o.angle;

    // Call the base.
    this._super(objectString);
}

4. Data controls and visualizations

To continue with part two of this article that focuses on adding data binding to the control, see Create a custom data visualization control.

5. See also

.NET

JavaScript

 

Dundas Data Visualization, Inc.
500-250 Ferrand Drive
Toronto, ON, Canada
M3C 3G8

North America: 1.800.463.1492
International: 1.416.467.5100

Dundas Support Hours: 7am-6pm, ET, Mon-Fri