This post describes a complete module built on Services Framework, HTML, and Javascript. It is intended to serve as a Services Framework example with more meat. It also shows how easy Services Framework makes it to step away from traditional code-behind style development. Detailed reference material on Services Framework can be found in the Wiki.
The entire source for this module can be found on github. The module was originally written on 6.2/MVC as part of my Day Of DNN Charlotte 2012 presentation. For those of you interested in converting from MVC to WebAPI you should look at my previous blog post on the topic. I have also maintained an MVC_62 branch in the github repo so you can compare the MVC and WebAPI versions, as well as review the check-ins that I made in the conversion process.
The example module is called ExploreSettings and allows users to view and edit host and portal level settings of a DNN site. Here are the core requirements that the module must meet:
- Allows host users to view and change host and portal settings
- Allows users with view module permission to view portal settings
- Allows users with edit module permission to edit portal settings
In this post I am going to jump over the basics and head straight for the meat. If you need a refresher on the basics of Services Framework, check out my earlier blog post.
Controller
First a look at the controller and it's methods
[SupportedModules("ExploreSettings")]
public class SettingsController : DnnApiController
{....}
[SupportedModules("ExploreSettings")]
locks this controller to the ExploreSettings module. Any request that does not provide a tab and module id that matches an ExploreSettings module will be refused. This ensures that all the DnnModuleAuthorize attributes used on the methods will in fact be authorized against an instance of ExploreSettings, and not any random module instance.
[HttpGet]
public HttpResponseMessage HostSettings()
{
return Request.CreateResponse(HttpStatusCode.OK, HostController.Instance.GetSettingsDictionary());
}
The HostSettings method supports getting a list of all the host settings in the system. There are no authorization attributes applied to this method because the default behavior is to require host level access which is exactly what we want for this method. We could explicitly apply the RequiresHost attribute and it would make no difference. The RequiresHost attribute is primarily meant for situations where a lower level of authorization is applied to the controller, and a few specific methods need to be elevated back to requiring host level access.
The code inside the method is straight forward. We simply fetch a dictionary containing all the host settings for the system, and let the WebAPI content negotiation take care of converting the dictionary to JSON or XML or whatever format is appropriate for the client.
[HttpGet]
[DnnModuleAuthorize(AccessLevel = SecurityAccessLevel.View)]
public HttpResponseMessage CurrentPortalSettings()
{
return Request.CreateResponse(HttpStatusCode.OK, TestablePortalController.Instance.GetPortalSettingsDictionary(PortalSettings.PortalId));
}
The CurrentPortalSettings method supports getting a list of all the portal settings in the current portal. Current was prefixed to the name because DnnApiController already has a PortalSettings property.
[DnnModuleAuthorize(AccessLevel = SecurityAccessLevel.View)]
ensures that only users with view permissions to a module can call this method. DnnModuleAuthorize should always be used in concert with SupportedModules. Without SupportedModules it would be possible to authorize against view permissions for any module instance. Since most sites will have at least one page visible to anonymous users, it would be possible for a hacker to call this function without any user credentials at all.
public class UpdateSettingDTO
{
public string Key { get; set; }
public string Value { get; set; }
}
[HttpPost]
[ValidateAntiForgeryToken]
public HttpResponseMessage UpdateHostSetting(UpdateSettingDTO submitted)
{
HostController.Instance.Update(submitted.Key, submitted.Value);
return Request.CreateResponse(HttpStatusCode.OK);
}
UpdateHostSetting supports the ability for hosts to edit host settings. Again there is no Authorize attribute so this method defaults to requiring a host user.
A big difference between MVC and WebAPI is in how they process the bodies of requests. MVC would analyze the body and map it to individual parameters in the method signature. WebAPI treats all bodies as a single object and will return create a single POCO from the request body. The UpdateSettingDTO gives WebAPI a POCO to map the request body too.
[ValidateAntiForgeryToken]
This attribute helps to prevent CSRF attacks on services by setting an encrypted cookie and hidden field when a web page is requested. When submitted the cookie and a header are submitted and validated to ensure that they match. Since a 3rd party site will not be able to access the cookie value they will not be able to set ann appropriate header in a CSRF style attack. Use of ValidateAntiForgeryToken on all services intended for AJAX use is recommended.
[HttpPost]
[ValidateAntiForgeryToken]
[DnnModuleAuthorize(AccessLevel = SecurityAccessLevel.Edit)]
public HttpResponseMessage UpdatePortalSetting(UpdateSettingDTO submitted)
{
TestablePortalController.Instance.UpdatePortalSetting(PortalSettings.PortalId, submitted.Key, submitted.Value);
return Request.CreateResponse(HttpStatusCode.OK);
}
UpdatePortalSetting supports the requirement for users with edit rights to the ExploreSettings module to be able to update a portal setting. There is nothing new that here that was not already covered on a previous controller method.
That's it for the service controller. It's pretty straight forward, now I'll move on to the client.
Code Behind
public partial class ExploreSettings : PortalModuleBase
{
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
ServicesFramework.Instance.RequestAjaxAntiForgerySupport();
}
}
The above code is the entire code behind for this module. I smile every time I see it again. :)
ServicesFramework.Instance.RequestAjaxAntiForgerySupport()
This call enables anti-forgery support on any page that the module is included in. Be sure to call this method whenever you are using services that have the ValidateAntiForgeryToken attribute. It also registers the services framework AJAX helper script and jQuery.
In the Browser
The bulk of the code for the ExploreSettings module is straight forward HTML and Javascript. All of the HTML and Javascript for this module lives inside the ExploreSettings.ascx file. I will only highlight pieces unique to working with the Services Framework.
$(document).ready(function () {
var moduleScope = $('#<%=ScopeWrapper.ClientID %>'),
self = moduleScope,
sf = $.ServicesFramework(<%=ModuleId %>);
The moduleScope technique used in this module is not Services Framework specific, but it is a good technique to ensure that your module will work properly when there is more than one instance of the module on a page. It also helps prevent accidental conflicts with 3rd party modules that may share the same page.
sf = $.ServicesFramework(<%=ModuleId %>)
This line initializes the ServicesFramework plugin and prepares it to work with the current module instance.
self.updateKey = function() {
var postData = { key: $("#key", moduleScope).text(), value: $("#value", moduleScope).val() };
var action = self.getUpdateAction();
$.ajax({
type: "POST",
url: sf.getServiceRoot('ExploreSettings') + "Settings/" + action,
data: postData,
beforeSend: sf.setModuleHeaders
}).done(function() {
self.loadSettings();
}).fail(function (xhr, result, status) {
alert("Uh-oh, something broke: " + status);
});
};
updateKey is a pretty typical method for posting data via jQuery. There are only two lines specific to Services Framework.
url: sf.getServiceRoot('ExploreSettings') + "Settings/" + action
The getServiceRoot method will generate the correct URL to use for calling a service in the current portal up to the /API/. The developer can then easily append the remainder of the the url, typically it is for the {controller}/{action} portion when using RPC style services.
beforeSend: sf.setModuleHeaders
setModuleHeaders takes care of several things for you. It sets the tab and module id headers which is what enables DnnModuleAuthorize and the DnnApiController.ActiveModule property. It also sets the anti-forgery header to support the ValidateAntiForgeryToken attribute.
That is really all there is to it. By writing a small service with the Services Framework, and a tiny bit of boilerplate code behind, it is suddenly easy to write modern DNN modules entirely in HTML and Javascript. You are not limited to plain javascript and jQuery either, you can mix in whatever javascript library you prefer. Many people have been using Knockout with Services Framework recently. I find the whole thing an exciting breath of fresh air.