-
Notifications
You must be signed in to change notification settings - Fork 150
Modules for Developers (v1.x)
Place the module DLL file into App_Data/BetterCMS/Modules folder and it will be loaded dynamically at run time, or add the module assembly as a reference.
Currently, there is no simple way to setup a new project for a Better CMS module implementation in Visual Studio. Follow the instructions below to prepare it manually. And because there are two types of modules, with or without GUI, lets start with simpler one: without GUI:
- Add a new Class Library (for module without GUI) or ASP.NET Empty Web Application (for module with GUI) project to the solution.
- Install the Better CMS NuGet package to this project.
- Add module descriptor class (note: a new ModuleId Guid must be generated for each new module):
using BetterCms.Core.Modules;
using System;
namespace BetterCms.Module.DemoNewsletter
{
public class DemoNewsletterDescriptor : ModuleDescriptor
{
internal const string ModuleName = "DemoNewsletter";
internal const string ModuleId = "bb29419b-55fd-4521-a351-99c43c86c58e";
internal const string ModuleAreaName = "bcms-demonewsletter";
public DemoNewsletterDescriptor(ICmsConfiguration configuration) : base(configuration) { }
public override Guid Id
{
get { return new Guid(ModuleId); }
}
public override string Name
{
get { return ModuleName; }
}
public override string Description
{
get { return "Demo newsletter module short description goes here."; }
}
}
}
- (Optional) If the website and module are on the same solution, add a post-build event into the module project properties to copy module DLL into the website App_Data folder. Example:
copy "$(TargetDir)$(TargetName).dll" "$(SolutionDir)BetterCmsDemoProject\App_Data\BetterCms\Modules" /Y
copy "$(TargetDir)$(TargetName).pdb" "$(SolutionDir)BetterCmsDemoProject\App_Data\BetterCms\Modules" /Y
- Create the database migration scripts:
using FluentMigrator;
using BetterCms.Core.DataAccess.DataContext.Migrations;
using BetterCms.Core.Models;
namespace BetterCms.Module.DemoNewsletter.Models.Migrations
{
[Migration(201305141100)]
public class InitialSetup : DefaultMigration
{
public InitialSetup() : base(DemoNewsletterDescriptor.ModuleName) { }
public override void Up()
{
Create.Table("Subscribers").InSchema(SchemaName)
.WithCmsBaseColumns()
.WithColumn("Email").AsString(MaxLength.Email).NotNullable();
}
public override void Down()
{
Delete.Table("Subscribers").InSchema(SchemaName);
}
}
}
- Add a class for the migration versions meta data:
using FluentMigrator.VersionTableInfo;
namespace BetterCms.Module.DemoNewsletter.Models.Migrations
{
[VersionTableMetaData]
public class MigrationVersioning : IVersionTableMetaData
{
public string SchemaName { get { return "bcms_" + DemoNewsletterDescriptor.ModuleName; } }
public string TableName { get { return "VersionInfo"; } }
public string ColumnName { get { return "Version"; } }
public string UniqueIndexName { get { return "uc_VersionInfo_Version_" + DemoNewsletterDescriptor.ModuleName; } }
}
}
- Create serialize-able data entity classes in Models as the example:
using BetterCms.Core.Models;
using System;
namespace BetterCms.Module.DemoNewsletter.Models
{
[Serializable]
public class Subscriber : EquatableEntity<Subscriber>
{
public virtual string Email { get; set; }
}
}
- Add mappings:
using BetterCms.Core.Models;
namespace BetterCms.Module.DemoNewsletter.Models.Maps
{
public class SubscriberMap : EntityMapBase<Subscriber>
{
public SubscriberMap() : base(DemoNewsletterDescriptor.ModuleName)
{
Table("Subscribers");
Map(f => f.Email).Not.Nullable().Length(MaxLength.Email);
}
}
}
Better CMS modules can support multiple languages. For this purpose, add a resource file DemoNewsletterGlobalization.resx
to Content/Resources
with a Public
access modifier. For example:
CreateSubscriber_CreatedSuccessfully_Message Newsletter subscriber created successfully.
DeleteSubscriber_Confirmation_Message Are you sure you want to delete newsletter subscriber {0}?
DeleteSubscriber_DeletedSuccessfully_Message Newsletter subscriber deleted successfully.
EditSubscriber_IvalidEmail_Message Subscriber email is invalid.
SiteSettings_NewsletterSubscribers_Email_Title Email Address
SiteSettings_NewsletterSubscribersMenuItem Newsletter subscribers
SiteSettings_NewsletterSubscribers_Title Newsletter subscribers
And now these resources can be used, for example in a ViewModels:
using System;
using System.ComponentModel.DataAnnotations;
using BetterCms.Core.Models;
using BetterCms.Module.DemoNewsletter.Content.Resources;
using BetterCms.Module.Root;
using BetterCms.Module.Root.Content.Resources;
using BetterCms.Module.Root.Mvc.Grids;
namespace BetterCms.Module.DemoNewsletter.ViewModels
{
public class SubscriberViewModel : IEditableGridItem
{
[Required(ErrorMessageResourceType = typeof(RootGlobalization), ErrorMessageResourceName = "Validation_RequiredAttribute_Message")]
public virtual Guid Id { get; set; }
[Required(ErrorMessageResourceType = typeof(RootGlobalization), ErrorMessageResourceName = "Validation_RequiredAttribute_Message")]
public virtual int Version { get; set; }
[Required(ErrorMessageResourceType = typeof(RootGlobalization), ErrorMessageResourceName = "Validation_RequiredAttribute_Message")]
[StringLength(MaxLength.Email, ErrorMessageResourceType = typeof(RootGlobalization), ErrorMessageResourceName = "Validation_StringLengthAttribute_Message")]
[RegularExpression(RootModuleConstants.EmailRegularExpression, ErrorMessageResourceType = typeof(DemoNewsletterGlobalization), ErrorMessageResourceName = "EditSubscriber_IvalidEmail_Message")]
public virtual string Email { get; set; }
}
}
or in controller actions (note: the following code is from a controller that will be created later on in the tutorial):
[HttpPost]
[BcmsAuthorize(RootModuleConstants.UserRoles.Administration)]
public ActionResult DeleteSubscriber(string id, string version)
{
var request = new SubscriberViewModel { Id = id.ToGuidOrDefault(), Version = version.ToIntOrDefault() };
var success = GetCommand<DeleteSubscriberCommand>().ExecuteCommand(request);
if (success)
{
if (!request.Id.HasDefaultValue())
{
Messages.AddSuccess(DemoNewsletterGlobalization.DeleteSubscriber_DeletedSuccessfully_Message);
}
}
return WireJson(success);
}
For this purpose, use commands that will be called from controller actions. For example:
using System.Linq;
using BetterCms.Core.DataAccess.DataContext;
using BetterCms.Core.Mvc.Commands;
using BetterCms.Module.DemoNewsletter.Models;
using BetterCms.Module.DemoNewsletter.ViewModels;
using BetterCms.Module.Root.Mvc;
using BetterCms.Module.Root.Mvc.Grids.Extensions;
using BetterCms.Module.Root.Mvc.Grids.GridOptions;
using BetterCms.Module.Root.ViewModels.SiteSettings;
namespace BetterCms.Module.DemoNewsletter.Commands
{
public class GetSubscriberListCommand : CommandBase, ICommand<SearchableGridOptions, SearchableGridViewModel<SubscriberViewModel>>
{
public SearchableGridViewModel<SubscriberViewModel> Execute(SearchableGridOptions request)
{
request.SetDefaultSortingOptions("Email");
var query = Repository.AsQueryable<Subscriber>();
if (!string.IsNullOrWhiteSpace(request.SearchQuery))
{
query = query.Where(a => a.Email.Contains(request.SearchQuery));
}
var subscribers = query
.Select(subscriber =>
new SubscriberViewModel
{
Id = subscriber.Id,
Version = subscriber.Version,
Email = subscriber.Email
});
var count = query.ToRowCountFutureValue();
subscribers = subscribers.AddSortingAndPaging(request);
return new SearchableGridViewModel<SubscriberViewModel>(subscribers.ToList(), request, count.Value);
}
}
}
using BetterCms.Core.DataAccess.DataContext;
using BetterCms.Core.Mvc.Commands;
using BetterCms.Module.DemoNewsletter.Models;
using BetterCms.Module.DemoNewsletter.ViewModels;
using BetterCms.Module.Root.Mvc;
namespace BetterCms.Module.DemoNewsletter.Commands
{
public class SaveSubscriberCommand : CommandBase, ICommand<SubscriberViewModel, SubscriberViewModel>
{
public SubscriberViewModel Execute(SubscriberViewModel request)
{
var isNew = request.Id.HasDefaultValue();
var subscriber = isNew ? new Subscriber() : Repository.AsQueryable<Subscriber>(w => w.Id == request.Id).FirstOne();
subscriber.Email = request.Email;
subscriber.Version = request.Version;
Repository.Save(subscriber);
UnitOfWork.Commit();
return new SubscriberViewModel
{
Id = subscriber.Id,
Version = subscriber.Version,
Email = subscriber.Email
};
}
}
}
using BetterCms.Core.Mvc.Commands;
using BetterCms.Module.DemoNewsletter.Models;
using BetterCms.Module.DemoNewsletter.ViewModels;
using BetterCms.Module.Root.Mvc;
namespace BetterCms.Module.DemoNewsletter.Commands
{
public class DeleteSubscriberCommand : CommandBase, ICommand<SubscriberViewModel, bool>
{
public bool Execute(SubscriberViewModel request)
{
Repository.Delete<Subscriber>(request.Id, request.Version);
UnitOfWork.Commit();
return true;
}
}
}
Use the commands in controller actions:
using System.Web.Mvc;
using BetterCms.Core.Security;
using BetterCms.Module.DemoNewsletter.Commands;
using BetterCms.Module.DemoNewsletter.Content.Resources;
using BetterCms.Module.DemoNewsletter.ViewModels;
using BetterCms.Module.Root;
using BetterCms.Module.Root.Mvc;
using BetterCms.Module.Root.Mvc.Grids.GridOptions;
using Microsoft.Web.Mvc;
namespace BetterCms.Module.DemoNewsletter.Controllers
{
[ActionLinkArea(DemoNewsletterDescriptor.ModuleAreaName)]
public class SubscriberController : CmsControllerBase
{
[BcmsAuthorize(RootModuleConstants.UserRoles.Administration)]
public ActionResult ListTemplate()
{
var view = RenderView("List", null);
var subscribers = GetCommand<GetSubscriberListCommand>().ExecuteCommand(new SearchableGridOptions());
return ComboWireJson(subscribers != null, view, subscribers, JsonRequestBehavior.AllowGet);
}
[BcmsAuthorize(RootModuleConstants.UserRoles.Administration)]
public ActionResult SubscribersList(SearchableGridOptions request)
{
var model = GetCommand<GetSubscriberListCommand>().ExecuteCommand(request);
return WireJson(model != null, model);
}
[HttpPost]
[BcmsAuthorize(RootModuleConstants.UserRoles.Administration)]
public ActionResult SaveSubscriber(SubscriberViewModel model)
{
var success = false;
SubscriberViewModel response = null;
if (ModelState.IsValid)
{
response = GetCommand<SaveSubscriberCommand>().ExecuteCommand(model);
if (response != null)
{
if (model.Id.HasDefaultValue())
{
Messages.AddSuccess(DemoNewsletterGlobalization.CreateSubscriber_CreatedSuccessfully_Message);
}
success = true;
}
}
return WireJson(success, response);
}
[HttpPost]
[BcmsAuthorize(RootModuleConstants.UserRoles.Administration)]
public ActionResult DeleteSubscriber(string id, string version)
{
var request = new SubscriberViewModel { Id = id.ToGuidOrDefault(), Version = version.ToIntOrDefault() };
var success = GetCommand<DeleteSubscriberCommand>().ExecuteCommand(request);
if (success)
{
if (!request.Id.HasDefaultValue())
{
Messages.AddSuccess(DemoNewsletterGlobalization.DeleteSubscriber_DeletedSuccessfully_Message);
}
}
return WireJson(success);
}
}
}
To place HTML representation, use regular views. Create the view List.cshtml
under the Views/Shared
folder of the module root project:
@using System.Web.Mvc.Html
@using BetterCms.Module.DemoNewsletter.Content.Resources
@using BetterCms.Module.Root;
@using BetterCms.Module.Root.Mvc.Grids;
@using BetterCms.Module.Root.ViewModels.Shared;
@{
var gridViewModel = new EditableGridViewModel
{
Columns = new List<EditableGridColumn>
{
new EditableGridColumn(DemoNewsletterGlobalization.SiteSettings_NewsletterSubscribers_Email_Title, "Email", "email")
{
AutoFocus = true
}
}
};
}
<div class="bcms-scroll-window">
@Html.Partial(RootModuleConstants.EditableGridTemplate, gridViewModel)
</div>
Note: All the Views, JavaScript files and CSS files should be marked as embedded resources.
This can be done in Visual Studio by selecting "file properties" and changing the "Build Action" type to Embedded resource
:
Place your CSS files into the Content/Styles
folder and override the RegisterCssIncludes()
method in the module descriptor:
public override IEnumerable<CssIncludeDescriptor> RegisterCssIncludes()
{
return new[]
{
new CssIncludeDescriptor(this, "somemodulestyle.css"),
};
}
In regard to JavaScripts: you need to add them as an embedded resources in to the Scripts
folder under the module project root, the create JavaScript modules descriptors and register them in the module descriptor.
bcms.demonewsletter.js
script is an example:
/*jslint unparam: true, white: true, browser: true, devel: true */
/*global bettercms */
bettercms.define('bcms.demonewsletter', ['bcms.jquery', 'bcms', 'bcms.modal', 'bcms.siteSettings', 'bcms.dynamicContent', 'bcms.ko.extenders', 'bcms.ko.grid'],
function ($, bcms, modal, siteSettings, dynamicContent, ko, kogrid) {
'use strict';
var newsletter = {},
selectors = {},
links = {
loadSiteSettingsSubscribersUrl: null,
loadSubscribersUrl: null,
saveSubscriberUrl: null,
deleteSubscriberUrl: null
},
globalization = {
subscriberDialogTitle: null,
deleteSubscriberDialogTitle: null
};
newsletter.links = links;
newsletter.globalization = globalization;
newsletter.selectors = selectors;
var SubscribersListViewModel = (function (_super) {
bcms.extendsClass(SubscribersListViewModel, _super);
function SubscribersListViewModel(container, items, gridOptions) {
_super.call(this, container, links.loadSubscribersUrl, items, gridOptions);
}
SubscribersListViewModel.prototype.createItem = function (item) {
return new SubscriberViewModel(this, item);
};
return SubscribersListViewModel;
})(kogrid.ListViewModel);
var SubscriberViewModel = (function (_super) {
bcms.extendsClass(SubscriberViewModel, _super);
function SubscriberViewModel(parent, item) {
_super.call(this, parent, item);
var self = this;
self.email = ko.observable().extend({ required: "", email: "", maxLength: { maxLength: ko.maxLength.email } });
self.registerFields(self.email);
self.email(item.Email);
}
SubscriberViewModel.prototype.getDeleteConfirmationMessage = function () {
return $.format(globalization.deleteSubscriberDialogTitle, this.email());
};
SubscriberViewModel.prototype.getSaveParams = function () {
var params = _super.prototype.getSaveParams.call(this);
params.Email = this.email();
return params;
};
return SubscriberViewModel;
})(kogrid.ItemViewModel);
function initializeSiteSettingsNewsletterSubscribers(container, json) {
var data = (json.Success == true) ? json.Data : {};
var viewModel = new SubscribersListViewModel(container, data.Items, data.GridOptions);
viewModel.deleteUrl = links.deleteSubscriberUrl;
viewModel.saveUrl = links.saveSubscriberUrl;
ko.applyBindings(viewModel, container.get(0));
}
newsletter.loadSiteSettingsNewsletterSubscribers = function () {
dynamicContent.bindSiteSettings(siteSettings, links.loadSiteSettingsSubscribersUrl, {
contentAvailable: function (json) {
var container = siteSettings.getModalDialog().container.find('.bcms-rightcol');
initializeSiteSettingsNewsletterSubscribers(container, json);
}
});
};
newsletter.loadDialogNewsletterSubscribers = function () {
modal.edit({
title: newsletter.globalization.subscriberDialogTitle,
disableSaveDraft: true,
isPreviewAvailable: false,
disableSaveAndPublish: true,
onLoad: function(dialog) {
dynamicContent.bindDialog(dialog, links.loadSiteSettingsSubscribersUrl, {
contentAvailable: function (dialog, json) {
var container = dialog.container.find('.bcms-scroll-window');
initializeSiteSettingsNewsletterSubscribers(container, json);
}
});
}
});
};
newsletter.init = function () {
console.log('Initializing bcms.demonewsletter module.');
};
bcms.registerInit(newsletter.init);
return newsletter;
});
JavaScript modules descriptors are used to provide links and globalization strings that are in the server (C# side).
An example JavaScript module descriptor created in the Registration
folder under the module project root DemoNewsletterJsModuleIncludeDescriptor.cs
:
using BetterCms.Core.Modules;
using BetterCms.Core.Modules.Projections;
using BetterCms.Module.DemoNewsletter.Content.Resources;
using BetterCms.Module.DemoNewsletter.Controllers;
namespace BetterCms.Module.DemoNewsletter.Registration
{
public class DemoNewsletterJsModuleIncludeDescriptor : JsIncludeDescriptor
{
public DemoNewsletterJsModuleIncludeDescriptor(ModuleDescriptor module) : base(module, "bcms.demonewsletter")
{
Links = new IActionProjection[]
{
new JavaScriptModuleLinkTo<SubscriberController>(this, "loadSiteSettingsSubscribersUrl", c => c.ListTemplate()),
new JavaScriptModuleLinkTo<SubscriberController>(this, "loadSubscribersUrl", c => c.SubscribersList(null)),
new JavaScriptModuleLinkTo<SubscriberController>(this, "saveSubscriberUrl", c => c.SaveSubscriber(null)),
new JavaScriptModuleLinkTo<SubscriberController>(this, "deleteSubscriberUrl", c => c.DeleteSubscriber(null, null)),
};
Globalization = new IActionProjection[]
{
new JavaScriptModuleGlobalization(this, "subscriberDialogTitle", () => DemoNewsletterGlobalization.SiteSettings_NewsletterSubscribers_Title),
new JavaScriptModuleGlobalization(this, "deleteSubscriberDialogTitle", () => DemoNewsletterGlobalization.DeleteSubscriber_Confirmation_Message),
};
}
}
}
Additionally, the module descriptor needs to be initialized in module descriptor. Do so by initializing in constructor and overriding RegisterJsIncludes() method. In our example cycle, add the following usages to module descriptor:
using BetterCms.Module.DemoNewsletter.Registration;
using System.Collections.Generic;
Add local variable, update constructor and override RegisterJsIncludes:
private readonly DemoNewsletterJsModuleIncludeDescriptor newsletterJsModuleIncludeDescriptor;
public DemoNewsletterDescriptor(ICmsConfiguration configuration) : base(configuration)
{
newsletterJsModuleIncludeDescriptor = new DemoNewsletterJsModuleIncludeDescriptor(this);
}
public override IEnumerable<JsIncludeDescriptor> RegisterJsIncludes()
{
return new[] { newsletterJsModuleIncludeDescriptor };
}
In the default CMS configuration, JavaScripts resources are taken from the CDN for better performance. However in this case, when a custom module is used, you must update the cms.config
file from:
useMinifiedResources="true"
resourcesBasePath="//d3hf62uppzvupw.cloudfront.net/{bcms.version}/"
to:
useMinifiedResources="false"
resourcesBasePath="(local)"
Otherwise, JavaScript errors will be seen in the browser console - JavaScripts will be not found.
Alternatively, instead of updating cms.config
, update the module descriptor with the source code below:
using BetterCms.Core.Mvc.Extensions;
[...]
private string minJsPath;
private string minCssPath;
public override string BaseModulePath
{
get { return VirtualPath.Combine("/", "file", AreaName); }
}
public override string MinifiedJsPath
{
get { return minJsPath ?? (minJsPath = VirtualPath.Combine(JsBasePath, string.Format("bcms.{0}.js", Name.ToLowerInvariant()))); }
}
public override string MinifiedCssPath
{
get { return minCssPath ?? (minCssPath = VirtualPath.Combine(CssBasePath, string.Format("bcms.{0}.css", Name.ToLowerInvariant()))); }
}
Now, all the resources for default modules will be loaded from the CDN and your module resources from the local server.
To add a module to Site Settings, update the module descriptor by overriding RegisterSiteSettingsProjections() method:
using BetterCms.Module.DemoNewsletter.Content.Resources;
using BetterCms.Core.Modules.Projections;
using BetterCms.Module.Root;
using Autofac;
[...]
public override IEnumerable<IPageActionProjection> RegisterSiteSettingsProjections(ContainerBuilder containerBuilder)
{
return new IPageActionProjection[]
{
new SeparatorProjection(9999),
new LinkActionProjection(newsletterJsModuleIncludeDescriptor, page => "loadSiteSettingsNewsletterSubscribers")
{
Order = 9999,
Title = page => DemoNewsletterGlobalization.SiteSettings_NewsletterSubscribersMenuItem,
CssClass = page => "bcms-sidebar-link",
AccessRole = RootModuleConstants.UserRoles.MultipleRoles(RootModuleConstants.UserRoles.Administration)
}
};
}
To add a button to the site menu, update the module descriptor by overriding RegisterSidebarMainProjections() method:
using BetterCms.Module.DemoNewsletter.Content.Resources;
using BetterCms.Core.Modules.Projections;
using BetterCms.Module.Root;
using Autofac;
[...]
public override IEnumerable<IPageActionProjection> RegisterSidebarMainProjections(ContainerBuilder containerBuilder)
{
return new IPageActionProjection[]
{
new SeparatorProjection(40) { CssClass = page => "bcms-sidebar-separator" },
new ButtonActionProjection(newsletterJsModuleIncludeDescriptor, page => "loadDialogNewsletterSubscribers")
{
Order = 900,
Title = page => DemoNewsletterGlobalization.SiteSettings_NewsletterSubscribersMenuItem,
CssClass = page => "bcms-sidemenu-btn",
AccessRole = RootModuleConstants.UserRoles.Administration
},
};
}
To grant access for specific user roles in controller, use the "BcmsAuthorize" attribute for controller action as follows:
[BcmsAuthorize(RootModuleConstants.UserRoles.Administration)]
public ActionResult ListTemplate()
{
var view = RenderView("List", null);
var subscribers = GetCommand<GetSubscriberListCommand>().ExecuteCommand(new SearchableGridOptions());
return ComboWireJson(subscribers != null, view, subscribers, JsonRequestBehavior.AllowGet);
}
If you need to ensure user access rights in the command, CommandBase
has DemandAccess
method that will raise SecurityException
if user is not in role.
Additionally, if you need user role specific functionality in java script. Include 'bcms.security' module and:
if (!security.IsAuthorized(["BcmsEditContent", "BcmsPublishContent"])) {
[...]
}
Currently there are 4 roles defined in BetterCms.Module.Root.UserRoles
:
- BcmsEditContent
- BcmsPublishContent
- BcmsDeleteContent
- BcmsAdministration