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 2017 or higher
  • Microsoft .NET Framework 4.7.2 or higher, and Microsoft .NET Core 3.1 or higher
  • A deployed instance of Dundas BI - this article details extensions for version 7 and higher

2.1. Download the sample solution

To download the Slack sample solution, click here.

(A sample solution is also available for Dundas BI version 6.)

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

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 the solution configuration set to Release, this .zip file can be found in the bin\Release\netcoreapp[x] subfolder of your solution. It targets both .NET Framework and .NET Core.

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>
/// This class represents the extension package info for the Slack sample. 
/// </summary>
public class SlackExtensionPackageInfo : ExtensionPackageInfo2
{
        /// <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 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:
Phone: 9am-6pm, ET, Mon-Fri
Email: 7am-6pm, ET, Mon-Fri