What do we want to build ?
To keep it simple but not too oversimplified we will create a tasklist app. Taskitems should be added, edited and deleted and they should be sortable with drag and drop. And it should look nice, so we'll make use of bootstrap for the design:
Starting the project
Let's begin to set up our project. As told before in part 1 of this blog series, we will begin with a plain vanilla class library project in Visual Studio. So there is no need to install some module templates and use them. For learning purposes its the best to do this all manually. If you really understand all needed steps you can use these later but for now we'll stick with this. First thing you have to do is a new installation of DNN 7 (latest version) on your development machine. I think I do not have to explain this...
After having your DNN installation up and running we can start with the new module. In Visual Studio, create a new ClassLibrary project:
Be sure to create your module in the DesktopModules folder and that Create directory for solution is unchecked.
After successful creation of the project open the project settings. Go to the Build tab and change the output location to ..\..\bin so that the compiled dll goes to the bin folder of your DNN installation.
The next thing we need to do is adding some references. The most missing libraries are located in the bin folder, the rest is from the global assembly cache.
Tip: If you create a separate directory on your machine that includes all the DNN bin files and name it for e.g. DNN_8.0.4 (holding these version specific files) and reference from this directory instead of your DNN installation you can target this DNN version with your module but work and develop in a newer one!
After adding all the references check if the reference properties for all references are set to copy local:false. In the other case you get the DNN Dlls overriden everytime you compile (in worst case with an older version, see tip!)
The Model
In our module we want to deal with "items", so our database table we'll use looks like this:
To create our table in the database, you can invoke this script in your DNN installation on Host > SQL :
CREATE TABLE {databaseOwner}[{objectQualifier}Angularmodule_Items](
[ItemId] [int] IDENTITY(1,1) NOT NULL,
[ItemName] [nvarchar](max) NOT NULL,
[ItemDescription] [nvarchar](max) NOT NULL,
[AssignedUserId] [int] NULL,
[ModuleId] [int] NOT NULL,
[Sort] [int] NOT NULL DEFAULT ((0)),
[CreatedOnDate] [datetime] NOT NULL,
[CreatedByUserId] [int] NOT NULL,
[LastModifiedOnDate] [datetime] NOT NULL,
[LastModifiedByUserId] [int] NOT NULL,
CONSTRAINT [PK_BBAngular_Items] PRIMARY KEY CLUSTERED
(
[ItemId] ASC
) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
So if we want to use this table in a standard DAL2 way, we need a poco (=plain old common object) class where all the fields are matched to properties of this class. So lets first create a folder named "Model" and then add a new class inside named ItemInfo:
using System;
using System.Web.Caching;
using DotNetNuke.ComponentModel.DataAnnotations;
namespace Angularmodule.Model
{
// Name of the corresponding database table
[TableName("Angularmodule_Items")]
//setup the primary key for table
[PrimaryKey("ItemId", AutoIncrement = true)]
//configure caching using PetaPoco
[Cacheable("BBAngular_Items_", CacheItemPriority.Default, 20)]
//scope the objects to the ModuleId of a module on a page (or copy of a module on a page)
[Scope("ModuleId")]
public class ItemInfo
{
///<summary>
/// The ID of your object with the name of the ItemName
///</summary>
public int ItemId { get; set; }
///<summary>
/// A string with the title of the Item
///</summary>
[ColumnName("ItemName")]
public string Title { get; set; }
///<summary>
/// A string with the description of the object
///</summary>
[ColumnName("ItemDescription")]
public string Description { get; set; }
///<summary>
/// User id of the assigned user for the object
///</summary>
public int? AssignedUserId { get; set; }
///<summary>
/// The username of the assigned user for the object
///</summary>
[ReadOnlyColumn]
public string AssignedUserName { get; set; }
///<summary>
/// The ModuleId of where the object was created and gets displayed
///</summary>
public int ModuleId { get; set; }
///<summary>
/// The Sort field defines the display order
///</summary>
public int Sort { get; set; }
///<summary>
/// User id of the user who created the object
///</summary>
public int CreatedByUserId { get; set; }
///<summary>
/// An integer for the user id of the user who last updated the object
///</summary>
public int LastModifiedByUserId { get; set; }
///<summary>
/// The date the object was created
///</summary>
public DateTime CreatedOnDate { get; set; }
///<summary>
/// The date the object was last updated
///</summary>
public DateTime LastModifiedOnDate { get; set; }
}
}
Please be aware of the attributes on properties Description and Title that are helpful to use a different property name than fieldname. And have also a look on the attribute ReadOnlyColumn which has no corresponding field in the table and is filled in SELECT scenarios by a JOIN to the Users table.
The Controller
For the communication to the database we need an AppController. Create a new folder named Controller and add a new class AppController.cs inside:
using System.Data;
using Angularmodule.Model;
using DotNetNuke.Data;
using System.Collections.Generic;
namespace Angularmodule.Controller
{
/// <summary>
/// Class AppController.
/// </summary>
public class AppController
{
/// <summary>
/// The private field holding the instance
/// </summary>
private static AppController _instance;
/// <summary>
/// Gets the instance (singleton pattern).
/// </summary>
/// <value>The instance.</value>
public static AppController Instance
{
get
{
if (_instance == null)
{
_instance = new AppController();
}
return _instance;
}
}
/// <summary>
/// Gets the items.
/// </summary>
/// <param name="moduleId">The module identifier.</param>
/// <returns>IEnumerable<ItemInfo>.</returns>
public IEnumerable<ItemInfo> GetItems(int moduleId)
{
using (IDataContext ctx = DataContext.Instance())
{
string sql = "SELECT items.*, users.username as AssignedUserName" +
" FROM {databaseOwner}[{objectQualifier}Angularmodule_Items] items" +
" LEFT OUTER JOIN [{objectQualifier}Users] users ON items.AssignedUserId = users.UserID" +
" WHERE ModuleId = @0 ORDER BY Sort";
return ctx.ExecuteQuery<ItemInfo>(CommandType.Text, sql, moduleId);
}
}
/// <summary>
/// Gets the item.
/// </summary>
/// <param name="itemId">The item identifier.</param>
/// <param name="moduleId">The module identifier.</param>
/// <returns>ItemInfo.</returns>
public ItemInfo GetItem(int itemId)
{
ItemInfo item;
using (IDataContext ctx = DataContext.Instance())
{
var rep = ctx.GetRepository<ItemInfo>();
item = rep.GetById(itemId);
}
return item;
}
/// <summary>
/// Creates a new item.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>System.Int32.</returns>
public int NewItem(ItemInfo item)
{
using (IDataContext ctx = DataContext.Instance())
{
var rep = ctx.GetRepository<ItemInfo>();
rep.Insert((ItemInfo)item);
return item.ItemId;
}
}
/// <summary>
/// Updates the item.
/// </summary>
/// <param name="item">The item.</param>
public void UpdateItem(ItemInfo item)
{
using (IDataContext ctx = DataContext.Instance())
{
var rep = ctx.GetRepository<ItemInfo>();
rep.Update((ItemInfo)item);
}
}
/// <summary>
/// Deletes the item.
/// </summary>
/// <param name="itemId">The item identifier.</param>
public void DeleteItem(int itemId)
{
using (IDataContext ctx = DataContext.Instance())
{
string sql = "DELETE FROM {databaseOwner}[{objectQualifier}Angularmodule_Items] WHERE ItemId = @0";
ctx.Execute(CommandType.Text, sql, itemId);
}
}
/// <summary>
/// Sets the sort field of the item to an integer value corresponding to its display sort order
/// </summary>
/// <param name="itemId">The item identifier.</param>
/// <param name="moduleId">The module identifier.</param>
public void SetItemOrder(int itemId, int sort)
{
using (IDataContext ctx = DataContext.Instance())
{
string sql = "UPDATE {databaseOwner}[{objectQualifier}Angularmodule_Items] SET Sort = @1 WHERE ItemId = @0";
ctx.Execute(CommandType.Text, sql, itemId, sort);
}
}
}
}
This controller class is accessible with its Instance property, there is no need to create an extra object (singleton pattern). The other methods are the DAL2 CRUD methods for managing the item table plus an extra method to set the order value of an item when the list is reordered interactively by the user.
See http://www.bitboxx.net/Blog/Post/190/Using-DAL-2-in-a-real-world-module for more information
The WebApi service
Now its time to add the WebApi service layer. Add a new folder named WebApi to your project and create a file named RouteMapper.cs inside. This file defines the route for your service:
using DotNetNuke.Web.Api;
namespace Angularmodule.WebApi
{
/// <summary>
/// Class Routemapper.
/// </summary>
public class Routemapper : IServiceRouteMapper
{
/// <summary>
/// Registers the routes.
/// </summary>
/// <param name="routeManager">The route manager.</param>
public void RegisterRoutes(IMapRoute routeManager)
{
routeManager.MapHttpRoute("Angularmodule", "default", "{controller}/{action}",
new[] { "Angularmodule.WebApi" });
}
}
}
In short that means that you can address this webservice under the url
~/desktopmodules/angularmodule/api/{controller}/{action}
See http://www.dnnsoftware.com/community-blog/cid/142400/getting-started-with-services-framework-webapi-edition for more information.
Let's test this! First we need a (WebApi) ItemController, so lets add this as a new class ItemsController.cs to the WebApi folder:
using DotNetNuke.Web.Api;
using System.Net;
using System.Net.Http;
using System.Web.Http;
namespace Angularmodule.WebApi
{
public class ItemController : DnnApiController
{
/// <summary>
/// API that returns Hello world
/// </summary>
/// <returns></returns>
[HttpGet]
[AllowAnonymous]
public HttpResponseMessage HelloWorld()
{
return Request.CreateResponse(HttpStatusCode.OK, "Hello World!");
}
}
}
The attributes here say that we use the http GET method for our call [HttpGet] and that also a not logged in user is able to use this method [AllowAnonymous].
Now we can (hopefully) see some first results in the browser! Compile the project and enter the following url in the browser:
http://(yourdomain)/desktopmodules/angularmodule/api/item/helloworld
Now you should see something like this:
If it works, we can change the content of this file now and include all the routines we really need:
using Angularmodule.Controller;
using Angularmodule.Model;
using DotNetNuke.Security;
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using System.Collections.Generic;
using DotNetNuke.Web.Api;
namespace Angularmodule.WebApi
{
[SupportedModules("Angularmodule")]
public class ItemController : DnnApiController
{
/// <summary>
/// API that returns Hello world
/// </summary>
[HttpGet]
[ActionName("test")] // /API/item/test
[AllowAnonymous]
public HttpResponseMessage HelloWorld()
{
return Request.CreateResponse(HttpStatusCode.OK, "Hello World!");
}
/// <summary>
/// API that creates a new item in the item list
/// </summary>
[HttpPost]
[ValidateAntiForgeryToken]
[ActionName("new")] // /API/item/new
[DnnModuleAuthorize(AccessLevel = SecurityAccessLevel.Edit)]
public HttpResponseMessage AddItem(ItemInfo item)
{
try
{
item.CreatedByUserId = UserInfo.UserID;
item.CreatedOnDate = DateTime.Now;
item.LastModifiedByUserId = UserInfo.UserID;
item.LastModifiedOnDate = DateTime.Now;
AppController.Instance.NewItem(item);
return Request.CreateResponse(HttpStatusCode.OK, item);
}
catch (Exception ex)
{
return Request.CreateResponse(HttpStatusCode.InternalServerError, ex.Message);
}
}
/// <summary>
/// API that deletes an item from the item list
/// </summary>
/// <returns></returns>
[HttpPost]
[ValidateAntiForgeryToken]
[ActionName("delete")] // /API/item/delete
[DnnModuleAuthorize(AccessLevel = SecurityAccessLevel.Edit)]
public HttpResponseMessage DeleteItem(ItemInfo item)
{
try
{
AppController.Instance.DeleteItem(item.ItemId);
return Request.CreateResponse(HttpStatusCode.OK, true.ToString());
}
catch (Exception ex)
{
return Request.CreateResponse(HttpStatusCode.NotFound, ex.Message);
}
}
/// <summary>
/// API that creates a new item in the item list
/// </summary>
[HttpPost]
[ValidateAntiForgeryToken]
[ActionName("edit")] // /API/item/edit
[DnnModuleAuthorize(AccessLevel = SecurityAccessLevel.Edit)]
public HttpResponseMessage UpdateItem(ItemInfo item)
{
try
{
item.LastModifiedByUserId = UserInfo.UserID;
item.LastModifiedOnDate = DateTime.Now;
AppController.Instance.UpdateItem(item);
return Request.CreateResponse(HttpStatusCode.OK, true.ToString());
}
catch (Exception ex)
{
return Request.CreateResponse(HttpStatusCode.InternalServerError, ex.Message);
}
}
/// <summary>
/// API that returns an item list
/// </summary>
[HttpPost, HttpGet]
[ValidateAntiForgeryToken]
[ActionName("list")] // /API/item/list
[DnnModuleAuthorize(AccessLevel = SecurityAccessLevel.View)]
public HttpResponseMessage GetModuleItems()
{
try
{
var itemList = AppController.Instance.GetItems(ActiveModule.ModuleID);
return Request.CreateResponse(HttpStatusCode.OK, itemList.ToList());
}
catch (Exception ex)
{
return Request.CreateResponse(HttpStatusCode.InternalServerError, ex.Message);
}
}
/// <summary>
/// API that returns a single item
/// </summary>
[HttpGet]
[ValidateAntiForgeryToken]
[ActionName("byid")] // /API/item/byid
[DnnModuleAuthorize(AccessLevel = SecurityAccessLevel.View)]
public HttpResponseMessage GetItem(int itemId)
{
try
{
var item = AppController.Instance.GetItem(itemId);
return Request.CreateResponse(HttpStatusCode.OK, item);
}
catch (Exception ex)
{
return Request.CreateResponse(HttpStatusCode.InternalServerError, ex.Message);
}
}
/// <summary>
/// API that reorders an item list
/// </summary>
[HttpPost, HttpGet]
[ValidateAntiForgeryToken]
[ActionName("reorder")] // /API/item/reorder
[DnnModuleAuthorize(AccessLevel = SecurityAccessLevel.Edit)]
public HttpResponseMessage ReorderItems(List<ItemInfo> sortedItems)
{
try
{
foreach (var item in sortedItems)
{
AppController.Instance.SetItemOrder(item.ItemId, item.Sort);
}
return Request.CreateResponse(HttpStatusCode.OK);
}
catch (Exception ex)
{
return Request.CreateResponse(HttpStatusCode.InternalServerError, ex.Message);
}
}
}
}
Again, please have a look at the attributes:
- [ValidateAntiForgeryToken] - if no antiforgery token is added to the call it will fail (more about this later)
- [ActionName("{name}")] - Instead of using the method name, you can explicitely define an action name
- [DnnModuleAuthorize(AccessLevel = SecurityAccessLevel.Edit)] - only logged in users with edit permissions are able to use this method.
and, in front of the ItemController class definition:
- [SupportedModules("Angularmodule")] - Only calls from this specific module are allowed
Attention! Make sure you have the correct casing! If the case does not exactly fit the module name, you will get authentication errors when calling your WebApi methods! )
You can download the complete project here