Little Known DNN Features

Brian Dukes

We Want Your Feedback!

Download the DNN Summit  Mobile App now and take the survey at the end of the conference to be entered to win a $100 Amazon gift card!

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" />

JavaScript Libraries


@{
    JavaScript.RequestRegistration(
        "intl-tel-input", 
        new Version(14, 0, 6), 
        SpecificVersion.LatestMajor);
}

[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="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css" 
    Name="bootstrap" 
    Version="4.2.1" />

<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" />

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;
}

Create Repository

using DotNetNuke.Entities.Modules.Settings;

public class HtmlModuleSettingsRepository 
    : SettingsRepository<HtmlModuleSettings>
{
}

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();

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);

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);
    }
}

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);
    }
}

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;
    }
}

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>

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);
    }
}

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);
    }
}

Request JWT Token

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

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

Use JWT Token

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

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);
}

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/Summit/App_LocalResources/Prompt.resx";
}

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;
}

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>

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%20Summit &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 SummitSitemapProvider : 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();
    }
}

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="summitSitemapProvider" type="Engage.SummitSitemapProvider" />
  </providers>
</sitemap>

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="summitSitemapProvider" type="Summit.Dnn.Summit.SummitSitemapProvider" />
          </node>
        </nodes>
      </configuration>
    </install>
    <uninstall>
      <configuration>
        <nodes configfile="web.config">
          <node path="/configuration/dotnetnuke/sitemap/providers/add[@name='summitSitemapProvider']" action="remove" />
        </nodes>
      </configuration>
    </uninstall>
  </config>
</component>

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.

  • 267
Loading comments...

More from Brian Dukes