X
dnn.blog.
Back

DNN module development with AngularJS (Part 6)

In this part of the post series we'll convert our module to a modern SPA module running on DNN 8+. The main problem here is, that we do not have any codebehind file where we can prepare our tokens, so we have to find another way for this. But first we start with some simple task:

Replacing View.ascx with View.html

A SPA module has no ascx file as control. Instead it uses a html file. So lets add a new View.htmlto the root of our project with the following content:

[JavaScript:{ jsname:"AngularJS"}]
[JavaScript:{ jsname:"angular-route"}]
[JavaScript:{ jsname:"angular-ng-dialog"}]
[Css:{ path: "~/Resources/libraries/angular-ng-dialog/00_05_01/ngDialog.min.css"}]
[Css:{ path: "~/Resources/libraries/angular-ng-dialog/00_05_01/ngDialog-theme-default.min.css"}]
[JavaScript:{ jsname:"angular-ui-sortable"}]
[JavaScript:{ jsname:"angular-ng-progress"}]
[Css:{ path: "~/Resources/libraries/angular-ng-progress/01_00_07/ngProgress.min.css"}]
[JavaScript:{ path: "~/DesktopModules/Angularmodule/Script/app.js", priority:40}]
[JavaScript:{ path: "~/DesktopModules/Angularmodule/Script/Service/itemService.js", priority:100}]
[JavaScript:{ path: "~/DesktopModules/Angularmodule/Script/Controller/itemController.js", priority:100}]

<!--
[DNNtc.ModuleDependencies(DNNtc.ModuleDependency.CoreVersion, "07.00.00")]
[DNNtc.PackageProperties("Angularmodule", 1, "Angular module", "An AngularJS sample module", "BBAngular.png", "Torsten Weggen", "bitboxx solutions", "http://www.bitboxx.net", "info@bitboxx.net", false)]
[DNNtc.ModuleProperties("Angularmodule", "Angular module", 0)]
[DNNtc.ModuleControlProperties("", "Angularmodule", DNNtc.ControlType.View, "", false, false)]
-->

<div id="itemApp[ModuleContext:ModuleId]">
    <div ng-view>Loading...</div>
</div>
<script>
    angular.element(document).ready(function () {
        function init(appName, moduleId, apiPath) {
            var sf = $.ServicesFramework(moduleId);
            var httpHeaders = { "ModuleId": sf.getModuleId(), "TabId": sf.getTabId(), "RequestVerificationToken": sf.getAntiForgeryValue() };
            var localAppName = appName + moduleId;
            var application = angular.module(localAppName, [appName])
                .constant("serviceRoot", sf.getServiceRoot(apiPath))
                .constant("moduleProperties", '[ModuleProperties:All]')
                .config(function($httpProvider,$routeProvider) {
                    // Extend $httpProvider with serviceFramework headers
                    angular.extend($httpProvider.defaults.headers.common, httpHeaders);
                });
            return application;
        };

        var app = init("itemApp", [ModuleContext:ModuleId], "Angularmodule");
        var moduleContainer = document.getElementById("itemApp[ModuleContext:ModuleId]");
        angular.bootstrap(moduleContainer, [app.name]);
    });
</script>

As you can see, we add our javascript libraries now with tokens that DNN provides for us.The comment section is the replacement for the attributes in codebehind of the ascx that define our packaging in release build mode.

The main difference is the usage of a new token that we are defining by ourself. In the DNN 7 version, it looks like:

    .constant("userlist", "<%=Users%>");
    .constant("resources", "<%=Resources%>");
    .constant("editable", "<%=Editable%>");
    .constant("moduleId", "<%=ModuleId%>");
    .constant("settings", "<%=ModuleSettings%>");

now we are using only one. That is only for simplicity. You can create for every constant an own token, but it is easier to put it all in one and split it on client side

    .constant("moduleProperties", '[ModuleProperties:All]')

Businesscontroller

Our project does not have a businesscontroller yet, so we'll create one now. Normally the businesscontroller is the place where you implement the import/export of module data (interface IPortable) or the search (ISearchable). We will use it for the token generation and so we have to implement ICustomTokenProvider. For that we have to create a GetTokens method:

using System.Web.UI;
using DotNetNuke.Entities.Modules;
using DotNetNuke.Services.Tokens;
using DotNetNuke.UI.Modules;
using System.Collections.Generic;

namespace Angularmodule.Controller
{
    /// ----------------------------------------------------------------------------- 
    /// <summary> 
    /// The Businesscontroller class for Angularmodule
    /// </summary> 
    /// ----------------------------------------------------------------------------- 
    [DNNtc.BusinessControllerClass()]
    public class BusinessController : ICustomTokenProvider
    {
        private static BusinessController _instance;

        public static BusinessController Instance
        {
            get
            {
                if (_instance == null)
                {
                    _instance = new BusinessController();
                }
                return _instance;
            }
        }


        public IDictionary<string, IPropertyAccess> GetTokens(Page page, ModuleInstanceContext moduleContext)
        {
            var tokens = new Dictionary<string, IPropertyAccess>();
            tokens["moduleproperties"] = new ModulePropertiesPropertyAccess(moduleContext);
            return tokens;
        }
    }
}

In here we define our token moduleproperties. Important for the packaging in release mode is the attribute [DNNtc.BusinessControllerClass()] !

ModulePropertiesPropertyAccess

The workhorse for the generation of our token is in the class ModulePropertiesPropertyAccess which should implement another interface IPropertyAccess :

using System.Collections;
using System.Collections.Generic;
using System.Dynamic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Resources;
using System.Web;
using DotNetNuke.Entities.Modules;
using DotNetNuke.Entities.Portals;
using DotNetNuke.Entities.Users;
using DotNetNuke.Services.Localization;
using DotNetNuke.Services.Tokens;
using DotNetNuke.UI.Modules;
using Newtonsoft.Json;

namespace Angularmodule.Controller
{
    public class ModulePropertiesPropertyAccess : IPropertyAccess
    {
        private ModuleInstanceContext _moduleContext;

        public ModulePropertiesPropertyAccess(ModuleInstanceContext moduleContext)
        {
            _moduleContext = moduleContext;
        }

        public string GetProperty(string propertyName, string format, CultureInfo formatProvider, UserInfo accessingUser, Scope accessLevel, ref bool propertyNotFound)
        {
            string retVal = "";

            int moduleId = _moduleContext.ModuleId;
            int portalId = _moduleContext.PortalId;
            int tabId = _moduleContext.TabId;
            ModuleInfo module = new ModuleController().GetModule(moduleId, tabId);

            string moduleDirectory = "/" + _moduleContext.Configuration.ControlSrc;
            moduleDirectory = moduleDirectory.Substring(0, moduleDirectory.LastIndexOf('/') + 1);


            switch (propertyName.ToLower())
            {
                case "all":
                    dynamic properties = new ExpandoObject();
                    properties.Resources = GetResources(module);
                    properties.Settings = _moduleContext.Settings;
                    properties.IsEditable = _moduleContext.IsEditable;
                    properties.EditMode = _moduleContext.EditMode;
                    properties.IsAdmin = accessingUser.IsInRole(PortalSettings.Current.AdministratorRoleName);
                    properties.ModuleId = _moduleContext.ModuleId;
                    properties.PortalId = _moduleContext.PortalId;
                    properties.UserId = accessingUser.UserID;
                    properties.HomeDirectory = PortalSettings.Current.HomeDirectory.Substring(1);
                    properties.ModuleDirectory = moduleDirectory;
                    properties.RawUrl = HttpContext.Current.Request.RawUrl;
                    properties.PortalLanguages = GetPortalLanguages();
                    properties.CurrentLanguage = System.Threading.Thread.CurrentThread.CurrentCulture.Name;
                    properties.Users = UserController.GetUsers(_moduleContext.PortalId).Cast<UserInfo>().Select(u => new { text = u.Username, id = u.UserID }).ToList();
                    return JsonConvert.SerializeObject(properties);
            }
            return "";
        }

        public CacheLevel Cacheability
        {
            get { return CacheLevel.notCacheable; }
        }

        private Dictionary<string, string> GetResources(ModuleInfo module)
        {
            System.IO.FileInfo fi = new System.IO.FileInfo(HttpContext.Current.Server.MapPath("~/" + _moduleContext.Configuration.ModuleControl.ControlSrc + ".resx"));
            string physResourceFile = fi.DirectoryName + "/App_LocalResources/" + fi.Name;
            string relResourceFile = "/DesktopModules/" + module.DesktopModule.FolderName + "/App_LocalResources/" + fi.Name;
            if (File.Exists(physResourceFile))
            {
                using (var rsxr = new ResXResourceReader(physResourceFile))
                {
                    var res = rsxr.OfType<DictionaryEntry>()
                        .ToDictionary(
                            entry => entry.Key.ToString().Replace(".", "_"),
                            entry => Localization.GetString(entry.Key.ToString(), relResourceFile));

                    return res;
                }
            }
            return new Dictionary<string, string>();
        }

        private List<string> GetPortalLanguages()
        {
            List<string> languages = new List<string>();
            LocaleController lc = new LocaleController();
            Dictionary<string, Locale> loc = lc.GetLocales(_moduleContext.PortalId);
            foreach (KeyValuePair<string, Locale> item in loc)
            {
                string cultureCode = item.Value.Culture.Name;
                languages.Add(cultureCode);
            }
            return languages;
        }
    }
}

As said before, we only define one token all. This token is build as an Expando object where we add all the properties we need and return the JSON serialized object that is then inserted at runtime in our View.html:

switch (propertyName.ToLower())
{
    case "all":
        dynamic properties = new ExpandoObject();
        properties.Resources = GetResources(module);
        properties.Settings = _moduleContext.Settings;
        properties.IsEditable = _moduleContext.IsEditable;
        properties.EditMode = _moduleContext.EditMode;
        properties.IsAdmin = accessingUser.IsInRole(PortalSettings.Current.AdministratorRoleName);
        properties.ModuleId = _moduleContext.ModuleId;
        properties.PortalId = _moduleContext.PortalId;
        properties.UserId = accessingUser.UserID;
        properties.HomeDirectory = PortalSettings.Current.HomeDirectory.Substring(1);
        properties.ModuleDirectory = moduleDirectory;
        properties.RawUrl = HttpContext.Current.Request.RawUrl;
        properties.PortalLanguages = GetPortalLanguages();
        properties.CurrentLanguage = System.Threading.Thread.CurrentThread.CurrentCulture.Name;
        properties.Users = UserController.GetUsers(_moduleContext.PortalId).Cast<UserInfo>().Select(u => new { text = u.Username, id = u.UserID }).ToList();
        return JsonConvert.SerializeObject(properties);
}
return "";

If you want to create more tokens you are free to add more cases to the switch statement!

itemController.js

Because we shrinked the constants to one item we have to change our itemController a little bit. Now our Controller has less dependency injected parameters and only one JSON.parse statement:

(function () {
    "use strict";

    angular
        .module("itemApp")
        .controller("itemController", itemController);

    itemController.$inject = ["$scope", "$window", "$log", "ngDialog", "ngProgress", "itemService", "moduleProperties"];
    
    function itemController($scope, $window, $log, ngDialog, ngProgress, itemService, moduleProperties) {

        var vm = this;
        vm.Items = [];
        vm.AddEditTitle = "";
        vm.EditIndex = -1;

        var moduleProps = JSON.parse(moduleProperties);

        vm.UserList = moduleProps.Users;
        vm.localize = moduleProps.Resources;
        vm.settings = moduleProps.Settings;
        vm.EditMode = moduleProps.IsEditable && moduleProps.EditMode;
        vm.ModuleId = parseInt(moduleProps.ModuleId);

DNN needs to know that our module is a SPA module now

We need to edit our extension in DNN a little bit to let DNN know that our module is a SPA module. Go to Host > Extensions or Settings > Extensions in DNN9 and edit your module. You could leave the Package Information as it is.

  1. Add the Business Controller Class in Extension Settings: Fill in Angularmodule.Controller.BusinessController,Angularmodule here.

  2. Go to Module Definitions and edit the Angularmodule: We only have one Module Control ("Angularmodule View"). Edit this and select as Source File our View.html instead of View.ascx.

  3. For the packaging process our project.targets build file needs to know that .html is a codefile also (around line 140):

  <Target Name="DnnFileCreation">
    <CreateItem Include="$(MSBuildProjectDirectory)\**\*.vb;$(MSBuildProjectDirectory)\**\*.cs;$(MSBuildProjectDirectory)\**\*.html;">
      <Output TaskParameter="Include" ItemName="CodeFiles" />
    </CreateItem>
  1. Rename View.ascx.resx to View.html.resx

Now you are ready to go!

As always: You can download the complete project from here

Hope you enjoyed this blog post series. Feel free to comment or show your enthusiasm! And if someone tells me how this could be done with Angular 2 and Typescript I'll be happy to add a seventh part also. Or maybe as a 2sxc-App...

Back

about.me.

Torsten WeggenMy name is Torsten Weggen and I am CEO of indisoftware GmbH in Hanover, Germany. I'm into DNN since 2008. Before this, I did a lot of desktop stuff mainly coded with Visual Foxpro (see http://www.auktionsbuddy.de). 

I'm programmer, husband, father + born in 1965.

Please feel free to contact me if you have questions.

Latest Posts

DNN module development with AngularJS (Part 6)
12/16/2016 7:00 AM | Torsten Weggen
DNN module development with AngularJS (Part 5)
12/16/2016 6:00 AM | Torsten Weggen
DNN module development with AngularJS (Part 4)
12/16/2016 5:00 AM | Torsten Weggen
DNN module development with AngularJS (Part 3)
12/16/2016 4:00 AM | Torsten Weggen
DNN module development with AngularJS (Part 2)
12/16/2016 3:00 AM | Torsten Weggen
DNN module development with AngularJS (Part 1)
12/15/2016 7:19 AM | Torsten Weggen
Blogging in DNN with Markdown Monster by Rick Strahl
11/27/2016 1:14 PM | Torsten Weggen
Creating a global token engine
11/18/2016 10:25 AM | Torsten Weggen
DnnImagehandler - Hot or not ?
2/21/2015 11:52 PM | Torsten Weggen
Rapid Module Development Part 2 - The multilanguage thing…
4/7/2014 7:32 PM | Torsten Weggen

My Twitter

Torsten Weggen 4/25/2017

RT @WaldkauzFolk: "A magical pagan folk release getting a lot of inspiration of myth, legends and tales from countries all over... https://…

Torsten Weggen 3/22/2017

Just released the merch shop for my daughters medieval band https://t.co/wzTm45ej32, made with https://t.co/0Yws7wvuvd

Torsten Weggen 12/18/2016

RT @jcsrb: retweet if your first IDE was blue https://t.co/cECBrnYsXa