Little Known DNN Features

Brian Dukes

Engage

@bdukes@mastodon.cloud

  1. Client Resource Management
  2. Settings Framework
  3. Event Handlers
  4. Module Injection Filters
  5. Custom Permissions
  6. JWT
  7. Custom Prompt Commands
  8. Extension URL Providers
  9. Image Handler
  10. Sitemap Providers
  11. SQL Script Installers

Outline

Client Resource Management

JavaScript Libraries

<%@ Register TagPrefix="dnn" TagName="JQUERY" 
             src="~/Admin/Skins/jQuery.ascx" %>     
<%@ Register TagPrefix="dnn" TagName="JavaScriptLibraryInclude" 
             src="~/Admin/Skins/JavaScriptLibraryInclude.ascx" %>

<dnn:jQuery runat="server" />                                                                                             
<dnn:JavaScriptLibraryInclude runat="server" 
                              Name="html5shiv" 
                              Version="3.7.3" 
                              SpecificVersion="LatestMajor" />            
<dnn:JavaScriptLibraryInclude runat="server" 
                              Name="respond-minmax" 
                              Version="1.4.2" 
                              SpecificVersion="LatestMajor" />
# Web Forms

JavaScript Libraries

@{
    JavaScript.RequestRegistration(
        "intl-tel-input", 
        new Version(14, 0, 6), 
        SpecificVersion.LatestMajor);
}
# MVC/Razor & HTML
[js:{"jsname": "moment", "version": "2.22.2", "specific": "LatestMajor"}]

Ad Hoc Resource Names

<%@ Register TagPrefix="dnn" 
             Namespace="DotNetNuke.Web.Client.ClientResourceManagement" 
             Assembly="DotNetNuke.Web.Client" %>

<dnn:DnnCssInclude runat="server"
    FilePath="~/resources/shared/stylesheets/dnndefault/8.0.0/default.css"
    Priority="<%#FileOrder.Css.DefaultCss%>"
    Name="dnndefault"
    Version="8.0.0" />

<dnn:DnnJsInclude runat="server" 
    FilePath="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/js/bootstrap.min.js" 
    Name="bootstrap" 
    Version="4.0.0"
    ForceProvider="DnnFormBottomProvider"
    Priority="99" />
# No Libraries

Additional Attributes

<dnn:DnnCssInclude runat="server"
  FilePath="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
  HtmlAttributesAsString="integrity:sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z,crossorigin:anonymous">" />

<dnn:DnnJsInclude runat="server"
  FilePath="https://www.google.com/recaptcha/api.js"
  ForceProvider="DnnPageHeaderProvider"
  HtmlAttributesAsString="async:async,defer:defer" />
# async/defer
[JavaScript:{ path: "~/DesktopModules/ResourceManager/Scripts/dnn-resource-manager/dnn-resource-manager.esm.js",
              htmlAttributes: { type: "module" } }]
[JavaScript:{ path: "~/DesktopModules/ResourceManager/Scripts/dnn-resource-manager/dnn-resource-manager.js",
              htmlAttributes: { nomodule: "nomodule" } }]
[JavaScript:{ path: "https://www.google.com/recaptcha/api.js",
              htmlAttributes: { async: "async", defer: "defer" } }]

Settings Framework

Create Settings Class

using DotNetNuke.Entities.Modules.Settings;

public class HtmlModuleSettings
{
    [ModuleSetting(Prefix = "HtmlText_")]
    public bool ReplaceTokens { get; set; } = false;

    [ModuleSetting(Prefix = "HtmlText_")]
    public bool UseDecorate { get; set; } = true;

    [ModuleSetting(Prefix = "HtmlText_")]
    public int SearchDescLength { get; set; } = 100;

    [ModuleSetting]
    public int WorkFlowID { get; set; } = -1;
}
# Just add attributes

Create Repository

using DotNetNuke.Entities.Modules.Settings;

public class HtmlModuleSettingsRepository 
    : SettingsRepository<HtmlModuleSettings>
{
}
# Inherit base class

Read Settings

var repo = new HtmlModuleSettingsRepository();
var moduleSettings = repo.GetSettings(this.ModuleConfiguration);

chkReplaceTokens.Checked = moduleSettings.ReplaceTokens;
cbDecorate.Checked = moduleSettings.UseDecorate;
txtSearchDescLength.Text = moduleSettings.SearchDescLength.ToString();
# Pass ModuleInfo

Write Settings

var repo = new HtmlModuleSettingsRepository();
var moduleSettings = repo.GetSettings(this.ModuleConfiguration);

moduleSettings.ReplaceTokens = chkReplaceTokens.Checked;
moduleSettings.UseDecorate = cbDecorate.Checked;
moduleSettings.SearchDescLength = int.Parse(txtSearchDescLength.Text);

repo.SaveSettings(this.ModuleConfiguration, moduleSettings);
# Update and Save

Event Handlers

  • IFileEventHandlers
    • FileOverwritten
    • FileDeleted
    • FileRenamed
    • FileMoved
    • FileAdded
    • FileOverwritten
    • FileMetadataChanged
    • FileDownloaded
    • FolderAdded
    • FolderDeleted
    • FolderMoved
    • FolderRenamed
  • IFollowerEventHandlers
    • FollowRequested
    • UnfollowRequested
  • IFriendshipEventHandlers
    • FriendshipRequested
    • FriendshipAccepted
    • FriendshipDeleted
  • IModuleEventHandler
    • ModuleCreated
    • ModuleUpdated
    • ModuleRemoved
    • ModuleDeleted
  • IPortalEventHandlers
    • PortalCreated
  • IPortalSettingHandlers
    • PortalSettingUpdated
  • IProfileEventHandlers
    • ProfileUpdated
  • IPortalTemplateEventHandlers
    • TemplateCreated
  • IRoleEventHandlers
    • RoleCreated
    • RoleDeleted
    • RoleJoined
    • RoleLeft
  • ITabEventHandler
    • TabCreated
    • TabUpdated
    • TabRemoved
    • TabDeleted
    • TabRestored
    • TabMarkedAsPublished
  • ITabSyncEventHandler
    • TabSerialize
    • TabDeserialize
  • IUserEventHandlers
    • UserAuthenticated
    • UserCreated
    • UserDeleted
    • UserRemoved
    • UserApproved
    • UserUpdated

Portal Creation Example

using System.ComponentModel.Composition;
using DotNetNuke.Entities.Portals;

[Export(typeof(IPortalEventHandlers))]
public class NewPortalHandler : IPortalEventHandlers
{
    public void PortalCreated(object sender, PortalCreatedEventArgs args)
    {
        SetupSite(args.PortalId);
    }
}
# One Method

User Creation Example

using System.ComponentModel.Composition;
using DotNetNuke.Entities.Users;

[Export(typeof(IUserEventHandlers))]
public class NewUserHandler : IUserEventHandlers
{
    public void UserDeleted(object sender, UserEventArgs args) {}
    public void UserRemoved(object sender, UserEventArgs args) {}
    public void UserAuthenticated(object sender, UserEventArgs args) {}
    public void UserApproved(object sender, UserEventArgs args) {}
    public void UserUpdated(object sender, UpdateUserEventArgs args) {}
    public void UserCreated(object sender, UserEventArgs args)
    {
        SetupUser(args.User.UserID);
    }
}
# Other Methods

Module Injection Filter

Can Inject Module?

using DotNetNuke.Entities.Modules;
using DotNetNuke.Entities.Portals;
using DotNetNuke.UI.Modules;

public class StandardModuleInjectionFilter : IModuleInjectionFilter
{
    public bool CanInjectModule(ModuleInfo module, PortalSettings portalSettings)
    {
        var variationTerm = module.Terms.SingleOrDefault(
            t => t.GetTermPath().StartsWith(@"\\Behaviors\\A/B Test\\"));
        var isInAbTest = variationTerm != null;
        if (!isInAbTest)
        {
            return true;
        }

        var request = HttpContextSource.Current.Request;
        var variationCookie = request.Cookies[$"AB_{module.ModuleID}"];
        return variationCookie?.Value == variationTerm.Name;
    }
}
# Yes or No?

Custom Permissions

Declare Permissions

<moduleDefinitions>
    <moduleDefinition>
        <moduleControls>
            <!-- … -->
        </moduleControls>
        <permissions>
            <permission code="ENGAGE_AMS" 
                        key="APPROVE_MEMBERSHIP" 
                        name="Approve Membership" />
            <permission code="ENGAGE_AMS" 
                        key="SEND_MESSAGE" 
                        name="Send Messages" />
        </permissions>
    </moduleDefinition>
</moduleDefinitions>
# DNN Manifest

Check Access

using DotNetNuke.Security;
using DotNetNuke.Web.Api;

[SupportedModules("Engage: AMS Dashboard")]
public class MessageController : DnnApiController
{
    private static readonly SecurityAccessLevel Edit = SecurityAccessLevel.Edit;

    [DnnModuleAuthorize(AccessLevel = Edit, PermissionKey = "SEND_MESSAGE")]
    public HttpResponseMessage Post(PostMessageModel model)
    {
        var result = MessageSender.Send(
            model.From, 
            model.To, 
            model.Content);
        return this.Request.CreateResponse(HttpStatusCode.OK, result);
    }
}
# Web API

JWT

Install JWT Auth Handler

Allow JWT Auth

using DotNetNuke.Web.Api;

[DnnAuthorize(AuthTypes = "JWT")]
public class PendingMembershipsController : DnnApiController
{
    public HttpResponseMessage Get()
    {
        var result = Enumerable.Empty<object>()
        return this.Request.CreateResponse(HttpStatusCode.OK, result);
    }
}
# Auth Type

Request JWT Token

POST https://dnn.connect/API/JwtAuth/mobile/login HTTP/2.0
Host: dnn.connect
Content-Type: application/json
Content-Length: 33

{"u":"sitemanager","p":"dnnhost"}
# Built-in API
{
  "userId":20,
  "displayName":"Site Manager",
  "accessToken":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzaWQiOiIwOGQ2OTA5OGI4YmU0Y2IzOGFlYmNhNGYxZmMyMDUzMCIsInJvbGUiOlsiUmVnaXN0ZXJlZCBVc2VycyIsIlN1YnNjcmliZXJzIl0sImlzcyI6InN1bW1pdC5sb2NhbCIsImV4cCI6MTU0OTk0NTA5MiwibmJmIjoxNTQ5OTQxMTkyfQ.xtXOEuZn21RR6B8Aps3JUE-JWwHPWTx03FBc1YyOTd0",
  "renewalToken":"ZcEN0LbU/iwbmKIkKJsQx1d/wxbwh0CE3J+sGH++XynQVc92rNxNmtspKV/xpTBC"
}

Use JWT Token

GET https://dnn.connect/API/EngageAms/PendingMemberships HTTP/2.0
Host: dnn.connect
Content-Type: application/json
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzaWQiOiIwOGQ2OTA5OGI4YmU0Y2IzOGFlYmNhNGYxZmMyMDUzMCIsInJvbGUiOlsiUmVnaXN0ZXJlZCBVc2VycyIsIlN1YnNjcmliZXJzIl0sImlzcyI6InN1bW1pdC5sb2NhbCIsImV4cCI6MTU0OTk0NTA5MiwibmJmIjoxNTQ5OTQxMTkyfQ.xtXOEuZn21RR6B8Aps3JUE-JWwHPWTx03FBc1YyOTd0
# Bearer Token
[]

Custom Prompt Commands

Barebones Example

using DotNetNuke.Entities.Portals;
using DotNetNuke.Entities.Users;
using DotNetNuke.Services.Localization;

using global::Dnn.PersonaBar.Library.Prompt;
using global::Dnn.PersonaBar.Library.Prompt.Models;

public class ListPendingMembershipsCommand : IConsoleCommand
{
    public void Initialize(string[] args, PortalSettings portalSettings, UserInfo userInfo, int activeTabId)
    {
    }

    public ConsoleResultModel Run()
    {
        var memberships = Membershipper.GetPending();
        return new ConsoleResultModel
               {
                   Data = memberships,
                   Records = memberships.Length,
                   FieldOrder = new []
                                {
                                    nameof(Membership.Username),
                                    nameof(Membership.Date),
                                }
               };
    }

    public bool IsValid() => true;
    public string ValidationMessage => "Don't worry, be happy";
    public string LocalResourceFile =>  "~/DesktopModules/Summit/App_LocalResources/Prompt.resx";
    public string ResultHtml => Localization.GetString("ResultHtml", this.LocalResourceFile);
}
# Minimum

Barebones Example

More Realistic Example

using System;

using DotNetNuke.Entities.Portals;
using DotNetNuke.Entities.Users;
using DotNetNuke.Services.Exceptions;

using global::Dnn.PersonaBar.Library.Prompt;
using global::Dnn.PersonaBar.Library.Prompt.Attributes;
using global::Dnn.PersonaBar.Library.Prompt.Models;

[ConsoleCommand(name: "send-message", category: "Summit", description: "SendMessage_Description")]
public class SendMessageCommand : ConsoleCommandBase
{
    [FlagParameter(flag: FlagTo, description: "SendMessage_FlagTo", type: "String", Required = true)]
    private const string FlagTo = "to";

    [FlagParameter(FlagFrom, "SendMessage_FlagFrom", "String")]
    private const string FlagFrom = "from";

    [FlagParameter(FlagContent, "SendMessage_FlagContent", "String", Required = true)]
    private const string FlagContent = "content";

    private string From { get; set; }
    private string To { get; set; }
    private string Content { get; set; }

    public override void Init(string[] args, PortalSettings portalSettings, UserInfo userInfo, int activeTabId)
    {
        this.To = GetFlagValue(flag: FlagTo, fieldName: nameof(To), defaultVal: string.Empty, required: true);
        this.From = GetFlagValue(FlagFrom, nameof(From), defaultVal: userInfo.Username);
        this.Content = GetFlagValue(FlagContent, nameof(Content), string.Empty, required: true);
    }

    /// <inheritdoc />
    public override ConsoleResultModel Run()
    {
        try
        {
            MessageSender.Send(this.From, this.To, this.Content);
            return new ConsoleResultModel(LocalizeString("Message Sent"));
        }
        catch (Exception exc)
        {
            Exceptions.LogException(exc);
            return new ConsoleErrorResultModel(exc.Message);
        }
    }

    public override string LocalResourceFile => "~/DesktopModules/Connect/App_LocalResources/Prompt.resx";
}
# Base Class, Attributes, Flags

More Realistic Example

Extension URL Providers

Create Provider Class

public class SocialUrlProvider : DotNetNuke.Entities.Urls.ExtensionUrlProvider
{
    public override bool AlwaysUsesDnnPagePath(int portalId) => true;
    public override Dictionary<string, string> GetProviderPortalSettings() => null;
    public override bool CheckForRedirect(…) => true;
    public override string ChangeFriendlyUrl(…) => "";
    public override string TransformFriendlyUrlToQueryString(…) => "";

    // optional, defaults to false
    public override bool AlwaysCallForRewrite(int portalId) => true;
}
# Base Class

Package Provider

<component type="UrlProvider">
  <urlProvider>
    <name>DNN Social Url Extension Provider</name>
    <type>DotNetNuke.Modules.SocialUrlProvider.SocialUrlProvider</type>
    <settingsControlSrc>DesktopModules/DNN_SocialUrlProvider/Settings.ascx</settingsControlSrc>
    <redirectAllUrls>false</redirectAllUrls>
    <replaceAllUrls>false</replaceAllUrls>
    <rewriteAllUrls>false</rewriteAllUrls>
    <desktopModule>Social Groups</desktopModule>
  </urlProvider>
</component>
# DNN Manifest

Image Handler

Modes

https://dnnsoftware.com/dnnimagehandler.ashx
?mode=… &…
profilepic userid=330894
file file=/Images/Logos.jpg
file url=https://placebear.com/200/100
securefile fileid=317624
placeholder w=200&h=100

Filters

/dnnimagehandler.ashx?mode=profilepic
&greyscale=1
&invert=1
&contrast=-100 / &contrast=100 -100 to 100
&gamma=0.2 / &gamma=5.0 0.2 to 5
&brightness=-100 / &brightness=150 -255 to 255
&rotateflip=RotateNoneFlipY / &rotateflip=Rotate180FlipNone None, 90, 180, 270, X, Y, XY

Placeholder

/dnnimagehandler.ashx?mode=placeholder&w=400&h=75
&text=banner
&color=BlueViolet
&backcolor=orange
&text=DNN%20Connect &color=%231e355e&backcolor=%23e77e3a

Resize

/dnnimagehandler.ashx?mode=securefile
&w=100
&size=xxs
&w=200&h=50&resizemode=fill
&w=200&h=50&resizemode=crop
&w=60&backcolor=DarkSeaGreen&resizemode=fitsquare
&w=40&backcolor=DarkOrchid&border=10

Sitemap Provider

Implement Provider

using System.Collections.Generic;
using System.Linq;
using DotNetNuke.Common;
using DotNetNuke.Entities.Portals;
using DotNetNuke.Services.Sitemap;

public class ConnectSitemapProvider : SitemapProvider
{
    public override List<SitemapUrl> GetUrls(int portalId, PortalSettings ps, string version)
    {
        return (from thing in Repository.GetThings()
                where !thing.IsDraft
                let url = Globals.NavigateURL(
                                              thing.TabId, 
                                              string.Empty, 
                                              $"slug={thing.Slug}")
                select new SitemapUrl
                       {
                           LastModified = thing.LastModified,
                           Priority = 0.6F,
                           Url = url,
                           ChangeFrequency = SitemapChangeFrequency.Hourly,
                       })
                .ToList();
    }
}
# Base Class

Add to web.config

<sitemap defaultProvider="coreSitemapProvider">
  <providers>
    <clear />
    <add name="coreSitemapProvider" type="DotNetNuke.Services.Sitemap.CoreSitemapProvider, DotNetNuke" providerPath="~\Providers\MembershipProviders\Sitemap\CoreSitemapProvider\" />
    <add name="connectSitemapProvider" type="Engage.ConnectSitemapProvider" />
  </providers>
</sitemap>
# Add to List

XML Merge

<component type="Config">
  <config>
    <configFile>web.config</configFile>
    <install>
      <configuration>
        <nodes configfile="web.config">
          <node path="/configuration/dotnetnuke/sitemap/providers" 
                action="update" key="name" collision="ignore">
            <add name="connectSitemapProvider" type="Engage.ConnectSitemapProvider" />
          </node>
        </nodes>
      </configuration>
    </install>
    <uninstall>
      <configuration>
        <nodes configfile="web.config">
          <node path="/configuration/dotnetnuke/sitemap/providers/add[@name='connectSitemapProvider']" action="remove" />
        </nodes>
      </configuration>
    </uninstall>
  </config>
</component>
# Packaging

SQL Script Installers

Basic Example

<component type="Script">
  <scripts>
    <basePath>DesktopModules\MVC\Engage\Example</basePath>
    <script type="Install">
      <name>0.2.0.sql</name>
      <version>0.2.0</version>
    </script>
    <script type="Install">
      <name>0.3.0.sql</name>
      <version>0.3.0</version>
    </script>
    <script type="UnInstall">
      <name>Uninstall.sql</name>
    </script>
  </scripts>
</component>
# Versioned Scripts

Install Script

<component type="Script">
  <scripts>
    <basePath>DesktopModules\MVC\Engage\Example</basePath>
    <script type="Install">
      <name>Install.sql</name>
      <version>1.0.0</version>
    </script>
    <script type="Install">
      <name>0.3.0.sql</name>
      <version>0.3.0</version>
    </script>
    <script type="UnInstall">
      <name>Uninstall.sql</name>
    </script>
  </scripts>
</component>
# One Time Only

Pre-Upgrade Script

<component type="Script">
  <scripts>
    <basePath>DesktopModules\MVC\Engage\Example</basePath>
    <script type="install">
      <name>Install.sql</name>
      <version>1.0.0</version>
    </script>
    <script type="preupgrade">
      <name>disable_permissions.sql</name>
    </script>
    <script type="install">
      <name>0.3.0.sql</name>
      <version>0.3.0</version>
    </script>
    <script type="postupgrade">
      <name>enable_permissions.sql</name>
    </script>
    <script type="uninstall">
      <name>Uninstall.sql</name>
    </script>
  </scripts>
</component>
# Not Installs

Post-Upgrade Script

<component type="Script">
  <scripts>
    <basePath>DesktopModules\MVC\Engage\Example</basePath>
    <script type="install">
      <name>Install.sql</name>
      <version>1.0.0</version>
    </script>
    <script type="install">
      <name>0.3.0.sql</name>
      <version>0.3.0</version>
    </script>
    <script type="postupgrade">
      <name>views.sql</name>
    </script>
    <script type="postupgrade">
      <name>procedures.sql</name>
    </script>
    <script type="uninstall">
      <name>Uninstall.sql</name>
    </script>
  </scripts>
</component>
# Every Time

Questions?

Little Known DNN Features

By Brian Dukes

Little Known DNN Features

Over the years, DNN has accumulated many helpful little features that get documented once (if at all) and then forgotten, only available to the few who go looking through the source code and discover a gem. Let's review the development features you didn't even know you were missing. I've been digging through DNN's source code for over 12 years, and have accumulated many rarely used features and capabilities built into DNN's platform that may be just the right approach for the solution you're building or the problem you've encountered. This talk will explore some of the latent possibilities that DNN provides if you just know where to look.

  • 372