Create custom export and delivery providers

1. Overview

This sample shows how to add custom export formats and custom notification delivery methods using Slack channels as an example for both. You can adapt this sample to export and deliver content to the format or platform of your choice.

The sample demonstrates how to do the following:

  • Create a Dundas BI extension
  • Define your own application configuration settings
  • Create a custom export provider - adding a custom export format
  • Create a custom delivery provider - adding a custom notification delivery method

Note
This is meant as a guide for creating an export provider and a delivery provider and not necessarily a tutorial for best practices in using the Slack API.

2. Getting started

The current version of the sample solution targets both .NET Framework and .NET Core so that the packaged extension will work for Dundas BI version 7 or higher on all platforms.

The following prerequisites must be installed on your computer to build the provided sample without modifications:

  • Visual Studio 2017 or higher
  • Microsoft .NET Framework 4.7.2
  • Microsoft .NET Core 3.1

You can modify the sample project to target different versions of .NET if preferred depending on the version used by your Dundas BI instance.

The Dundas BI NuGet packages referenced as dependencies are initially set to version 7.0.1. If your version of Dundas BI is newer and the APIs you are using may have changed, you can update the version of these package references.

This sample also references the assembly Dundas.BI.Export.StandardExportProviders - CEF.dll and version 7.0.1 is provided. Updating this file is not typically needed, but a newer version of this DLL can be found within the App_Data folder of your installed instance: within subfolders Extensions\Dundas.StdExport\bin, and a final additional subfolder named either netfw or netcore.

2.1. Download the sample solution

To download the Slack sample solution, click here.

This sample was updated on April 18, 2022 to refer to updated Slack API methods.

(A sample solution is also available for Dundas BI version 6 that used older API methods.)

2.2. Open the solution

Extract SlackSample.zip to a folder and open the solution in Microsoft Visual Studio to build the extension.

To use the option to publish directly to your Dundas BI instance, run Visual Studio as an administrator before opening the solution. For example, you can right-click Visual Studio in the start menu and find the Run as administrator option.

The solution file is located at:
[Extracted folder]\SlackSample\SlackSample.sln

2.3. Slack prerequisites

If you are using or adapting the provided sample to connect to Slack:

  • Create a Slack app to represent this extension so it can authenticate and perform actions in Slack.
  • Add OAuth scopes to provide the app with the required permissions for calling the desired API methods. The provided sample was tested with the scopes channels:read, chat:write, chat:write.public, and files:write.
  • After installing the app to your workspace, copy the provided OAuth Token value. After installing your extension into Dundas BI, you will need to enter this value into its custom configuration setting so it can authenticate to Slack.

For a detailed walkthrough of these steps, see Basic app setup in Slack's API reference.

The provided sample references the open source SlackAPI NuGet package for easier access to Slack's API from C#, which will be automatically loaded when the solution is opened.

3. The project

The project is a class library.

  • Images\slack.png - The image used in the UI.
  • PublishExtensionTemplate.props - Used for auto publishing the extension after the build succeeds, and defines extension properties, and files.
  • 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 Slack API and performing tasks on Slack.
  • StreamExtension.cs - This extension class contains a method for converting stream to a byte array.

3.1. Publish Extension Template Props

This sample has a mechanism to automatically publish the extension when building, which is the Dundas.BI.PublishExtension NuGet package. When this package is added to the project, it creates a PublishExtensionTemplate.props file containing MSBuild property and item groups, which define how to create and publish the extension.

When the DtFilePath property is set to the file path of the dt tool of a Dundas BI instance, it will then publish the extension directly to that instance when you build the solution. It will also touch the web.config file to force the web application to reset.

If the DtFilePath property is not set, it will create a .zip file you can add to your Dundas BI instance using the Extensions screen in the administration UI. After building the solution with default settings and the solution configuration set to Release, this .zip file can be found in the bin\Release\netcoreapp3.1 subfolder of your solution. It targets both .NET Framework and .NET Core.

For more details on using this package to automate publishing extensions, see Using the Dundas.BI.PublishExtension NuGet package.

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 ExtensionPackageInfo2 class. This class contains the package information about the extension.

/// <summary>Initializes a new instance of the
///<see cref="SlackExtensionPackageInfo"/> class.</summary>
/// <param name="extensionManifest">The extension manifest.</param>
public SlackExtensionPackageInfo(ExtensionManifest extensionManifest)
	: base(extensionManifest)
{
}

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

3.3. Adding application configuration settings

Slack requires an OAuth Token value from the extension to authenticate and post to the channels, and this sample defines a custom application configuration setting to store this value so that a Dundas BI administrator can easily set and change it as needed through the Dundas BI interface.

In the sample code in the previous section, the SlackExtensionPackageInfo class overrides the OnLoaded method, and the RegisterSettings method below is called that adds the required Slack OAuth Token value as a custom configuration setting:

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.SlackOAuthTokenId,
            "Slack OAuth Token",
            SlackExtensionPackageInfo.ModuleId,
            typeof(string)
        );
        appSettingsProperties.CategoryName = "Slack";
        appSettingsProperties.Description = "Paste the OAuth Token provided for the Slack app on the OAuth & Permissions page.";

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

    #endregion Internal Methods
}

Setting the required OAuth Token value in Dundas BI
Setting the required OAuth Token value in Dundas BI

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 provided sample includes a separate delivery provider for notifications, so the export provider would be redundant for notifications and is only needed in the Share menu of the toolbar, and we return only the value Share for the ProviderKinds property.

/// <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 each time they export content.

In the example below, a StringProperty is defined to determine the Slack channel to post the export to, and two more are used for a title (caption) and an optional initial comment displayed at the top of the post:

/// <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 title of the Slack post",
            string.Empty, ValidValuesSource.None
        );
        propertyDescriptor.IsValueRequired = false;

        properties.Add(titlePropertyDescriptor);

        ProviderProperty initialCommentPropertyDescriptor = new StringProperty(
            SlackExtensionProviderSettingIds.SlackTitlePropertyId,
            "Slack Initial Comment",
            "A comment for 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. The provided sample defines a custom user interface instead in order to list the available Slack channels in a drop down for the user to choose from:

/// <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 SlackHelper.SlackExportUI;
    }

    return null;
}

The custom UI HTML is returned by these helper methods and properties:

/// <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(
            SlackHelper.ChannelsUI,
            SlackHelper.SlackTitleUI,
            SlackHelper.SlackInitialCommentUI
        );
    }
}

/// <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>
/// 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("<fieldset>");
        uiBuilder.Append("\t<label for=\"");
        uiBuilder.Append(SlackExtensionProviderSettingIds.SlackChannelPropertyId);
        uiBuilder.Append("\">Slack Channel</label>");
        uiBuilder.AppendLine();
        uiBuilder.Append("\t<select title=\"Select a 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("</fieldset>");

        return uiBuilder.ToString();

    }
}

/// <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,
            @"<fieldset>
                <label for=""{0}"">{1}</label>
                <textarea 
                    rows=""5"" 
                    name=""{0}"" 
                    id=""{0}"" 
                    data-valuetype=""SingleString"" />
                </fieldset>",
            propertyId,
            nameOfProperty
        );
    }
    else
    {
        return string.Format(
            CultureInfo.CurrentCulture,
            @"<fieldset>
                <label for=""{0}"">{1}</label>
                <input 
                    type=""text"" 
                    name=""{0}"" 
                    id=""{0}"" 
                    data-valuetype=""SingleString"" />
                </fieldset>",
            propertyId,
            nameOfProperty
        );
    }
}

Note
The UI is connected to the property descriptors by ID, which we passed as the first argument to each StringProperty constructor. The UI defines an HTML element that has the attribute data-valuetype="SingleString" and provides the ID matching the property descriptor in 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 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;
}

A Dundas BI view posted to a Slack channel
A 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 DeliveryKind property defines what kind of delivery is supported by your provider. This should return NotificationDeliveryKind.ExtensionProvider for a custom delivery method as in this example.

/// <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 Deliver method is responsible for delivering the exported content to your custom destination. 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:
Phone: 9am-6pm, ET, Mon-Fri
Email: 7am-6pm, ET, Mon-Fri