Create custom export and delivery providers

1. Overview

This sample shows how to use custom export and delivery providers to allow users to export and deliver Dundas BI exports through notifications directly to Slack channels. You will learn how to do the following:

  • Create a Dundas BI extension
  • Define your own application configuration settings
  • Create a custom export provider
  • Create a custom delivery provider

Note
This is meant as a guide for creating an export provider and a delivery provider. It is not intended as a tutorial in connecting to Slack.

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. Download the sample solution

To download the Slack sample solution, click here.

2.2. Extract the solution to the SDK folder

This sample is designed to automatically publish the extension to your Dundas BI instance. First, you must extract SlackSample.zip to the SDK folder within the instance:

  1. Find the SDK folder for your instance. It is located at [instance root]\sdk.
  2. Extract SlackSample.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
          • SlackSample

    Important
    The sample will not work correctly if it is not in this folder.

2.3. Open the solution

To open the Visual Studio solution:

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

3. The project

The project is a class library.

  • Images\slack.png - The image used in the UI.
  • app.config - The app.config used by this assembly.
  • packages.config - Contains the package information for the Newtonsoft.Json, and SlackAPI packages that are used by this extension.
  • PublishExtensions.targets - Used for auto publishing the extension after the build succeeds.
  • SlackApplicationConfiguration.cs - Contains the method used for registering the application configuration settings for Dundas BI.
  • SlackDeliveryProvider.cs - This class contains the Slack delivery provider.
  • SlackException.cs - This class extends exception to allow for throwing Slack specific exceptions.
  • SlackExportProvider.cs - This class contains the Slack export provider.
  • SlackExtensionPackageInfo.cs - This class contains the package information about the extension package.
  • SlackExtensionProviderSettingIds.cs - This class contains the ID's used by the extensions.
  • SlackHelper.cs - This helper class contains the logic for calling the SlackAPI and doing tasks on Slack.
  • StreamExtension.cs - This extension class contains a method for converting stream to a byte array.

3.1. Publish extension targets

This sample has a mechanism to automatically publish the extension. This mechanism is the PublishExtension.targets file, which overrides the BeforeBuild and AfterBuild target. This will create the following files after successfully compiling the solution:

[instance root]\sdk\bin\Dundas.BI.Export.StandardExportProviders - CEF.dll

[instance root]\www\BIWebsite\App_Data\Extensions\SlackSample\bin\SlackSample.dll

[instance root]\www\BIWebsite\App_Data\Extensions\SlackSample\bin\SlackAPI.dll

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

3.2. ExtensionPackageInfo class

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

/// <summary>
/// This class represents the extension package info for the Slack sample. 
/// </summary>
public class SlackExtensionPackageInfo : ExtensionPackageInfo
{
	#region Static Fields

	internal static Guid ModuleId = new Guid("676eb344-c76b-438e-a85f-05891a6f016e");

	#endregion Static Fields

	#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 "Slack Sample"; }
	}

    /// <summary>
    /// Gets the unique identifier of the extension package.
    /// </summary>
    public override Guid Id
	{
		get { return ModuleId; }
	}

	public override string Name
	{
		get { return "Slack Sample"; }
	}

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

	#endregion Public Properties

	#region Public Methods

	/// <summary>
	/// Called after the extension package is loaded during engine startup.
	/// </summary>
	public override void OnLoaded()
	{
		SlackApplicationConfiguration.RegisterSettings();
	}

	#endregion Public Methods
}

3.3. Adding application configuration settings

Slack will require information for you to authenticate and post to the channels. In the example above, the SlackExtensionPackageInfo class overrides the OnLoaded method, and a RegisterSettings method is called that adds the settings to Dundas BI.

The following example will add SlackUserId, SlackTeamId, and SlackPasswordId properties to Dundas BI:

internal static class SlackApplicationConfiguration
{
    #region Internal Methods

    /// <summary>
    /// Registers the application configuration settings for the slack extension.
    /// </summary>
    internal static void RegisterSettings()
    {
        IAppConfigService appConfigService = Engine.Current.GetService<IAppConfigService>();

        AppSettingProperties appSettingsProperties = new AppSettingProperties(
            SlackExtensionProviderSettingIds.SlackUserIdId,
            "SlackUserId",
            SlackExtensionPackageInfo.ModuleId,
            typeof(string)
        );
        appSettingsProperties.CategoryName = "Slack";
        appSettingsProperties.Description = "Slack User ID";

        appSettingsProperties.DefaultValue = string.Empty;
        appConfigService.RegisterSetting(appSettingsProperties);

        appSettingsProperties = new AppSettingProperties(
            SlackExtensionProviderSettingIds.SlackTeamIdId,
            "SlackTeamId",
            SlackExtensionPackageInfo.ModuleId,
            typeof(string)
        );
        appSettingsProperties.CategoryName = "Slack";
        appSettingsProperties.Description = "Slack Team ID";

        appSettingsProperties.DefaultValue = string.Empty;
        appConfigService.RegisterSetting(appSettingsProperties);

        appSettingsProperties = new AppSettingProperties(
            SlackExtensionProviderSettingIds.SlackPasswordId,
            "SlackPasswordId",
            SlackExtensionPackageInfo.ModuleId,
            typeof(string)
        );
        appSettingsProperties.CategoryName = "Slack";
        appSettingsProperties.Description = "Slack Password";
        appSettingsProperties.IsPassword = true;
        appSettingsProperties.IsEncrypted = true;
        appSettingsProperties.DefaultValue = string.Empty;
        appConfigService.RegisterSetting(appSettingsProperties);
    }

    #endregion Internal Methods
}

Slack settings in the application configuration settings
Slack settings in the application configuration settings

3.4. Create export provider

An export provider defines a new Export Format option when users share a view (such as a dashboard or report), set up a notification, or both.

The export provider workflow
The export provider workflow

To create an export provider, you need to extend the ExportProvider class.

namespace SlackSample
{
    /// <summary>
    /// This class extends export provider in order to create a slack export provider.
    /// </summary>
    public class SlackExportProvider : ExportProvider
    {

    }
}

3.4.1. Define the export component information

/// <summary>
/// Gets the standard component description.
/// </summary>
public override string ComponentDescription
{
	get
	{
		return "Slack export sample that provides export from Dundas BI to a Slack channel.";
	}
}

/// <summary>
/// Gets the component ID.
/// </summary>
public override Guid ComponentId
{
	get
	{
		return new Guid("37f781f6-1fa7-4b82-a410-3031de28ac42");
	}
}

/// <summary>
/// Gets the component name.
/// </summary>
public override string ComponentName
{
	get
	{
		return "Slack";
	}
}

The component name and description will be visible in the Share menu in the toolbar.

3.4.2. Define whether the export creates a file

To define whether the export creates a file, the CreatesFile property should be overridden with true, or false. In the example below, the CreatesFile property returns false as the expected export will end up as a post in a Slack channel:

/// <summary>
/// Gets a value indicating whether this export provider creates a file or not.
/// </summary>
public override bool CreatesFile
{
    get
    {
        return false;
    }
}

If a file is created, the MimeType and FileExtension properties should return appropriate values. In the example below, these properties return an empty string as a file is not being created:

/// <summary>
/// Gets the file extension of the export result.
/// </summary>
public override string FileExtension
{
	get
	{
		return string.Empty;
	}
}


/// <summary>
/// Gets the MIME type of the export result.
/// </summary>
public override string MimeType
{
	get
	{
		return string.Empty;
	}
}

3.4.3. Define the export provider kind

The Slack provider does not create a file of a specific format instead just posts an image to Slack. This means it should be given a value of Share for the ProviderKinds property. This property determines the availability of the export provider.

/// <summary>
/// Gets a value indicating whether this export provider is available from 
/// share, notifications or both.
/// </summary>
public override ExportProviderKinds ProviderKinds
{
	get
	{
		return ExportProviderKinds.Share;
	}
}

3.4.4. Define the export properties and the user interface

The export provider allows you to define properties that can be set by users. In the example below, a StringProperty is defined to determine the slack channel to post the export to, and two more are used for the Slack title and initial comment:

/// <summary>
/// Gets the export provider specific property descriptors.
/// </summary>
public override IReadOnlyList<ProviderProperty> PropertyDescriptors
{
	get
	{
        List<ProviderProperty> properties = new List<ProviderProperty>();

        ProviderProperty propertyDescriptor = new StringProperty(
            SlackExtensionProviderSettingIds.SlackChannelPropertyId,
            "Slack Channel",
            "The slack channel to post to",
            "general", ValidValuesSource.None
        );
        propertyDescriptor.IsValueRequired = true;

        properties.Add(propertyDescriptor);

        ProviderProperty titlePropertyDescriptor = new StringProperty(
            SlackExtensionProviderSettingIds.SlackTitlePropertyId,
            "Slack Title",
            "The slack title of the Slack post",
            string.Empty, ValidValuesSource.None
        );
        propertyDescriptor.IsValueRequired = false;

        properties.Add(titlePropertyDescriptor);

        ProviderProperty initialCommentPropertyDescriptor = new StringProperty(
            SlackExtensionProviderSettingIds.SlackTitlePropertyId,
            "Slack Initial Comment",
            "The commeent of the Slack post",
            string.Empty, ValidValuesSource.None
        );
        propertyDescriptor.IsValueRequired = false;

        properties.Add(initialCommentPropertyDescriptor);

        return properties;
    }
}

The export provider will automatically create a simple UI for properties that appears in the export dialog. In the example below, a custom user interface will be used in order to load the known Slack channels in a drop down:

/// <summary>
/// Gets a value indicating whether this export provider is using a custom configuration UI.
/// </summary>
public override bool HasCustomConfigurationUI
{
    get
    {
        return true;
    }
}

/// <summary>
/// Gets the custom configuration UI based on the requested content type.
/// </summary>
/// <param name="contentType">The content type that the UI is requested for.</param>
/// <returns>
/// The string holding the custom UI, or 
/// <see langword="null" /> if none is supported for the content type.
/// </returns>
public override string GetCustomConfigurationUI(ContentType contentType)
{
    if (contentType == null)
    {
        return null;
    }

    // We support HTML.
    if (contentType.Equals(MediaTypeNames.Text.Html))
    {
        return SlackExportUI;
    }

    return null;
}

/// <summary>
/// Gets the HTML used to define the Slack export UI.
/// </summary>
/// <value>
/// The HTML used to define the Slack export UI.
/// </value>
internal static string SlackExportUI
{
    get
    {
        return string.Concat(
            ChannelsUI,
            SlackTitleUI,
            SlackInitialCommentUI
        );
    }
}

/// <summary>
/// Gets the HTML used to define the channels property.
/// </summary>
/// <value>
/// The HTML used to define the channels property.
/// </value>
internal static string ChannelsUI
{
    get
    {
        StringBuilder uiBuilder = new StringBuilder();

        uiBuilder.Append("<div>");

        uiBuilder.Append("\t<p>Slack Channel</p>");
                
        uiBuilder.AppendLine();
        uiBuilder.Append("\t<select title=\"Set the slack channel to post to:\" id=\"");
        uiBuilder.Append(SlackExtensionProviderSettingIds.SlackChannelPropertyId);
        uiBuilder.Append("\" name=\"");
        uiBuilder.Append(SlackExtensionProviderSettingIds.SlackChannelPropertyId);
        uiBuilder.Append("\" data-valuetype=\"SingleString\" >");

        foreach (Channel channel in SlackHelper.GetSlackChannels())
        {
            uiBuilder.AppendLine();
            uiBuilder.Append("\t\t<option value=\"");
            uiBuilder.Append(channel.name);
            uiBuilder.Append("\" >");
            uiBuilder.Append(channel.name);
            uiBuilder.Append("</option>");
            uiBuilder.AppendLine();
        }

        uiBuilder.AppendLine("</select>");

        uiBuilder.Append("</div>");

        return uiBuilder.ToString();
    }
}

/// <summary>
/// Gets the HTML used to define the slack title propery.
/// </summary>
/// <value>
/// The HTML used to define the slack title propery.
/// </value>
internal static string SlackTitleUI
{
    get
    {
        return GenerateSimpleStringUI(
            SlackExtensionProviderSettingIds.SlackTitlePropertyId,
            "Title (Optional)"
        );
    }
}

/// <summary>
/// Gets the slack initial comment UI.
/// </summary>
/// <value>
/// The slack initial comment UI.
/// </value>
internal static string SlackInitialCommentUI
{
    get
    {
        return GenerateSimpleStringUI(
            SlackExtensionProviderSettingIds.SlackInitialCommentPropertyId,
            "Initial Comment (Optional)", 
            true
        );
    }
}

/// <summary>
/// Generates the simple string UI.
/// </summary>
/// <param name="propertyId">The property identifier.</param>
/// <param name="nameOfProperty">The name of property.</param>
/// <returns>An HTML string for the property specified.</returns>
public static string GenerateSimpleStringUI(Guid propertyId, string nameOfProperty, bool isMultiline = false)
{
    if (isMultiline)
    {
        return string.Format(
            CultureInfo.CurrentCulture,
            @"<div>
                <p>{1}</p>
                <textarea 
                    rows=""5"" 
                    name=""{0}"" 
                    id=""{0}"" 
                    data-valuetype=""SingleString"" />
                </div>",
            propertyId,
            nameOfProperty
        );
    }
    else
    {
        return string.Format(
            CultureInfo.CurrentCulture,
            @"<div>
                <p>{1}</p>
                <input 
                    type=""text"" 
                    name=""{0}"" 
                    id=""{0}"" 
                    data-valuetype=""SingleString"" />
                </div>",
            propertyId,
            nameOfProperty
        );
    }
}

Note
The UI is connected to a PropertyDescriptor using the PropertyId. The UI defines an HTML element that has the attribute data-valuetype="SingleString" and defines the PropertyId value for the name and id attributes.

The slack export provider user interface
The slack export provider user interface

3.4.5. Defining what performs the export

The Export method is the one responsible for the exporting of the view, and then returning a result. The following example uses a PNG export provider to get an image, and then posts that image to a channel in Slack:

/// <summary>
/// Performs an export.
/// </summary>
/// <param name="request">The export request.</param>
/// <returns>
/// The export result.
/// </returns>
public override ExportResult Export(ExportRequest request)
{
	Dundas.BI.Export.StandardExportProviders.PngProvider pngProvider = 
		new Dundas.BI.Export.StandardExportProviders.PngProvider();

	ExportRequest exportRequest = new ExportRequest(
        Dundas.BI.Export.StandardExportProviders.StandardExportProviderIds.Png, 
        request.ViewId, 
        request.ViewParameters
    );

	ExportResult result = pngProvider.Export(exportRequest);

    var channelValue = request.ProviderSettings.FirstOrDefault(
        setting => setting.ParameterId.Equals(SlackExtensionProviderSettingIds.SlackChannelPropertyId)
    );

    var titleValue = request.ProviderSettings.FirstOrDefault(
        setting => setting.ParameterId.Equals(SlackExtensionProviderSettingIds.SlackTitlePropertyId)
    );

    var initialCommentValue = request.ProviderSettings.FirstOrDefault(
        setting => setting.ParameterId.Equals(SlackExtensionProviderSettingIds.SlackInitialCommentPropertyId)
    );

    SlackHelper.PostFile( 
        result.Content.ToByteArray(), 
        string.Format(
            CultureInfo.InvariantCulture, 
            "{0}.png", 
            exportResult.Name
        ),
        "png", 
        channelValue.ToString(),
        titleValue.ToString(),
        initialCommentValue.ToString()
    );

	return result;
}

Note
The channel property is available in the requests provider settings.

The Dundas BI view posted to a Slack channel
The Dundas BI view posted to a Slack channel

3.5. Create delivery provider

A delivery provider defines a new Delivery Method in the delivery options for notifications, which delivers the exported content to a destination of your choice.

The delivery provider workflow
The delivery provider workflow

In order to create a delivery provider, you need to extend the DeliveryProvider class.

namespace SlackSample
{
    /// <summary>
    /// This class creates a Slack delivery provider.
    /// </summary>
    /// <seealso cref="Dundas.BI.Notifications.Delivery.DeliveryProvider" />
    public class SlackDeliveryProvider : DeliveryProvider
	{
	}
}

3.5.1. Define the delivery component information, user interface and properties

The export component information, user interface and properties will defined the same way as they were above in the export provider. For the full implementation of these parts of the code, see the SlackDeliveryProvider.cs in the attached Slack sample solution.

3.5.2. Define the notification delivery kind

The NotificationDeliveryKind property defines what kind of delivery the provider provides. In the example below, this property returns NotificationDeliveryKind.ExtensionProvider.

/// <summary>
/// Gets the kind of delivery the provider provides.
/// </summary>
public override NotificationDeliveryKind DeliveryKind
{
	get
	{
		return NotificationDeliveryKind.ExtensionProvider;
	}
}

3.5.3. Define what performs the delivery

The Delivery method is the one responsible for the delivering the view, and then returning a result. The following example posts an export result to a channel in Slack:

/// <summary>
/// Delivers a set of export results.
/// </summary>
/// <param name="request">The delivery request.</param>
/// <param name="exportResults">The set of export results to deliver.</param>
public override void Deliver(DeliveryRequest request, ICollection<ExportResult> exportResults)
{
    var channelValue = request.DeliverySettings.ProviderSettings.FirstOrDefault(
        setting => setting.ParameterId.Equals(SlackExtensionProviderSettingIds.SlackChannelPropertyId)
    );

    var titleValue = request.DeliverySettings.ProviderSettings.FirstOrDefault(
        setting => setting.ParameterId.Equals(SlackExtensionProviderSettingIds.SlackTitlePropertyId)
    );

    foreach (ExportResult exportResult in exportResults)
	{
                
		SlackHelper.PostFile(
			exportResult.Content.ToByteArray(),
			string.Format(
				CultureInfo.InvariantCulture, 
				"{0}.{1}", 
				result.Name,
				exportResult.FileExtension
			),
			exportResult.FileExtension,
			channelValue.ToString(),
			titleValue.ToString(),
			request.ActionText
		);
	}
}

4. See also

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