Brian Dukes
Work at Engage Software, developing websites on the DNN Platform (a DNN MVP). I also serve Jesus at City Lights Church.
Brian Dukes
Engage
@bdukes@mastodon.cloud
<%@ 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.RequestRegistration(
"intl-tel-input",
new Version(14, 0, 6),
SpecificVersion.LatestMajor);
}
# MVC/Razor & HTML
[js:{"jsname": "moment", "version": "2.22.2", "specific": "LatestMajor"}]
<%@ 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
<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" } }]
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
using DotNetNuke.Entities.Modules.Settings;
public class HtmlModuleSettingsRepository
: SettingsRepository<HtmlModuleSettings>
{
}
# Inherit base class
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
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
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
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
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
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?
<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
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
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
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"
}
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
[]
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
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
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
<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
?mode=…
|
&…
|
|
---|---|---|
profilepic
|
userid=330894
|
|
file
|
file=/Images/Logos.jpg
|
|
file
|
url=https://placebear.com/200/100
|
|
securefile
|
fileid=317624
|
|
placeholder
|
w=200&h=100
|
&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 |
&text=banner |
|
&color=BlueViolet |
|
&backcolor=orange |
|
&text=DNN%20Connect &color=%231e355e&backcolor=%23e77e3a |
&w=100
|
|
&size=xxs
|
|
&w=200
|
|
&w=200
|
|
&w=60
|
|
&w=40
|
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
<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
<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
<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
<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
<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
<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
By Brian Dukes
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.
Work at Engage Software, developing websites on the DNN Platform (a DNN MVP). I also serve Jesus at City Lights Church.