Develop CMS apps
Provides guidelines and recommendations for developing and packaging custom apps (add-ons) for CMS.
An app extends the functionality of the Optimizely website with initializable modules, gadgets, audience criteria, virtual path providers, page and search providers, and so on.
Develop an app
Optimizely and third parties can develop apps. The module system (or shell modules) defines an app's folder structure and configuration system and extends the user interface. You can use the module system to build any module.
Optimizely loads and processes app assemblies during site start-up. An app can contain components like InitializableModule
and plug-ins that require assembly scanning.
An app is packaged as a NuGet package with the following guidelines.
-
Place the app in a shell module directory. For example, a Google Maps dynamic content app could reside in
~/modules/EPiServer.Samples.AddOns.GoogleMaps/
.
-
Reference assembly names in Web Forms pages, controls, and strongly typed MVC views of your apps.
User control example:<%@ Control Language="C#" AutoEventWireup="false" CodeBehind="Map.ascx.cs" Inherits="EPiServer.Research.DynamicContent.Map, GoogleMapsDynamicContent" %>
Strongly typed MVC view example:
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<ColoredModel>" %> <%@ Assembly Name="EPiServer.Samples.Module" %> <%@ Import Namespace="EPiServer.Samples.Module.Models" %>
-
Make paths to included resources relative to the app directory in the modules directory. The
EPiServer.Shell.Paths
class provides methods to resolve URLs to resources located in the directory of the corresponding shell module directory. A specified assembly (or any type from the app assemblies) identifies the app module.Resolve a path to an app resource by type
// MapDynamicContent is type from app assembly: string pathToControl = Paths.ToResource(typeof(MapDynamicContent), "Map.ascx"); // load control using resolved path: page.LoadControl(pathToControl);
Resolve a path to a client resource
// register client script using resolved path: string pathToAddonScript = Paths.ToClientResource(typeof(MapDynamicContent), "ClientResources/MapContent.js"); Page.ClientScript.RegisterClientScriptInclude("MapContent.js", pathToAddonScript);
-
Use relative paths to resources in app GUI plug-ins. Plug-in attributesprovides properties for that.
-
GuiPlugInAttribute.UrlFromModuleFolder
– Define the URL to a control relative to the app module directory (found in theEPiServer.PlugIn
namespace). -
GuiPlugInAttribute.Url
– Returns a resolved URL to an app control (found in the EPiServer.PlugIn namespace). -
DynamicContentPlugInAttribute.ViewUrlFromModuleFolder
– Define the URL to view a control for this dynamic content, relative to the app module directory (found in theEPiServer.DynamicContent
namespace). -
DynamicContentPlugInAttribute.ViewUrl
– Returns a resolved URL to a view control (found in theEPiServer.DynamicContent
namespace).You should define paths to plug-in resources as follows:
[GuiPlugIn(UrlFromModuleFolder="Control.ascx")] [DynamicContentPlugIn(ViewUrlFromModuleFolder ="View.ascx")]
-
-
Specify the path to the template file, relative to the module folder, in the
Path
property of theRenderDescriptorAttribute
on a page or a block template. Do not make the path application-relative or an absolute virtual path. The system resolves the virtual path to the content template file in the app directory.
Example:[RenderDescriptor(Path = "Blocks/SampleBlockControl.ascx")]
The resolved virtual path to the block template in a public app is
~/modules/<package ID>/Blocks/SampleBlockControl.ascx
.The directory structure, where template files reside, must follow the namespace convention if you do not set the
Path
property ofRenderDescriptorAttribute
of a page or a block template.See Content types and Content templates.
-
Use the Optimizely platform
LocalizationService
model with embedded language files for apps and assemblies. Shell modules are automatically scanned for XML localization data. You also can use standard resource files. -
You can add required client resources on the page without modifying the templates, to inject styles, scripts, or HTML on the pages.
-
You should version client resources to avoid caching problems when you upgrade to a new version. See Client resources.
-
Keep configuration changes to a minimum.
-
The following are not supported:
- Configuration file
- Source code transformation
- PowerShell scripts and other tools
-
Third-party apps cannot use an
EPiServer
namespace.
Create an app package
- Create a package from an assembly, a project, or a convention-based working directory. See the tools and documentation available from Nuget to create a NuGet package.
- Create the NuGet (
nuspec
) manifest by running the following command: nuget spec - Edit the
nuspec
file and specify the information for your app package. If you create your package from a project, use replacement tokens for the app package ID, version, author, and description. The followingnuspec
file example is for the Google Maps app:
<?xml version="1.0"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>$id$</id>
<version>$version$</version>
<title>Google Maps dynamic content</title>
<authors>$author$</authors>
<owners />
<iconUrl>http://world.episerver.com/PageFiles/3/Icons/Nuget.png</iconUrl>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>Allows to add Google Maps as dynamic content on pages.</description>
<tags>EPiServerPublicModulePackage Google Maps Dynamic</tags>
<dependencies>
<dependency id="EPiServer.Samples.SampleFoundation" version="1.2" />
<dependency id="EPiServer.CMS.UI" version="7.5" />
</dependencies>
</metadata>
</package>
Public and protected apps
Public and protected apps differ in user access rights required to access the app files and routes.
-
Public – Accessed by any site user including anonymous users.
Make your app public only if it provides content such as page templates, dynamic content rendering controls, public MVC views, or client resources for the site visitor. If the size of the public content is relatively small, consider including it as an embedded resource in one of the app's binaries and make the app protected instead.
-
Protected – Accessed only by the authorized users within WebEditors or WebAdmins roles.
Consider making your app protected because it is a more secure approach.
Public and protected apps have the following file locations and virtual paths:
- Public app files reside in the
modules
folder, by default, under the site root (the same path where all the public shell modules are located). - Public apps have a default virtual path starting from
~/modules/<package ID>/
. - Protected app files are located by default in
~/modules/\_protected/<package ID>/
. - Protected apps have a default virtual path starting from
~/<EPiServerProtectedPath>/modules/<package ID>/
(for example/EPiServer/Modules/<package ID>
).
Note
Mark public apps with the
EPiServerPublicModulePackage
tag. Mark protected apps with theEPiServerModulePackage
tag.
Dependencies and versions
If your app requires installing other apps on the site, add these to the list of dependencies in the nuspec
file to ensure the other apps get installed when a user chooses to install your app. The installation aborts if the other apps cannot be installed in the environment.
App packages must follow the Semantic Versioning (SemVer) scheme for versioning for the package itself to have a common understanding of which versions are compatible and which versions introduce breaking changes.
SemVer means having version numbers in the format Major.Minor.Patch, where the different segments correspond to:
- Major – Breaking changes.
- Minor – New features, but backward compatible.
- Patch – Backwards-compatible bug fixes only.
For example, if you require the feature set from version 1.3 of a dependency, set [1.3,2)
as the version range, which accepts versions known to be compatible. When version 2.0 is released, it may or may not be compatible. If it is compatible, the version range can be changed to [1.3,3)
in the next update. Otherwise, the version range, after your code base is changed to run with version 2.0 of this dependency, is changed to something like [2.0,3)
. See NuGet Docs for version ranges in NuGet.
Prerequisite dependency on an installed product
Your app may depend on a product that is not yet installed and is a prerequisite for the app to function. Assemblies installed in the application and system packages (such as EPiServer.CMS.Core
and EPiServer.CMS.UI
) are represented in the NuGet environment as virtual packages to which you can add dependencies. These packages' names and version numbers are based on the assembly names and versions. Dependencies must be actual NuGet packages for Visual Studio-enabled apps.
Package directory structure
The package directory structure should follow the conventions in NuGet Docs. If you are going to create the package from a Visual Studio project, run the following command:
nuget pack AddOnProject.csproj
Alternatively, if you have prepared a NuGet manifest (nuspec
) file and a convention-based directory structure for the actual package content, you can create the package with the following command:
nuget.exe pack addondirectory\addon.nuspec
Use NuGet Package Explorer
Use the NuGet Package Explorer GUI tool to view metadata and create packages. The following example shows the Google Maps app package opened in NuGet Package Explorer.
Develop Visual Studio-enabled apps
You should attach dependencies to ordinary NuGet packages rather than as references to assemblies. In most scenarios, the primary assembly of the NuGet package matches the ID of the package.
Content files must have a path in nuspec
as they display on site.
Public apps
Paths for content files should include \modules\<packageid>
for public apps.
<file src="Settings.aspx" target="Content\modules\<packageId>\Settings.aspx" />
Protected apps
Paths for content files should include \modules\_protected\<packageid>\
for protected apps.
<file src="Views\Setting\Index.aspx" target="Content\modules\_protected\<packageId>\Views\Setting\Index.aspx" />
The NuGet package should contain a module.config file in the package root (for example, \<file src="module.config" target="Content\modules\<packageId>\module.config" />\
). The module.config
must contain a tags attribute that contains either EPiServerModulePackage
or EPiServerPublicModulePackage
, which is required for the Optimizely app user interface to distinguish the app from other shell modules. The module.config
can contain a description attribute that describes the module in the app UI, and should list assemblies the package contains. The following code sample shows a minimal module.config
.
<?xml version="1.0" encoding="utf-8"?>
<module loadFromBin="false"
description="Allows to run various support tools on the site."
tags=" EPiServerModulePackage ">
<assemblies>
<add assembly="DeveloperTools" />
</assemblies>
</module>
EPiServer.Packaging.Converter
converts old apps to a format that lets you install the app from Visual Studio. A readme file describes how to use the converter.
ZIP compressed content
Deliver content in your app as a compressed ZIP archive by compressing the whole content structure below the package folder to a zip file. Give it the same name as the package and place it within the package directory, for example:
Content\modules\<packageId>\<packageId>.zip
Optimizely scans module directories during startup and adds a virtual path provider for any archive found as long as it follows this convention.
You should NOT use this feature if your module contains large files because the content is kept in a memory cache. However, the feature is useful if you are developing a Visual Studio app that contains many small files because it prevents the module from adding the files to the Visual Studio project of the developers installing your app. If you want to debug files in an app that uses this functionality, extract the archive into its current directory and delete or rename the archive.
Execute custom code for apps
Execute custom code at the following extension points, when certain actions are performed:
- After the app installation
- After an app update
- Before an app uninstallation (does not trigger when uninstalling an app using Visual Studio)
If your app requires executing custom code only on web application start-up and does not need to be notified about installations, updates, or deletions, consider using IInitializableModule
.
Base class for your custom code
To execute custom code when the status of the app package changes, the custom code should include a class inherited from the abstract class EPiServer.Packaging.PackageInitializer
in the EPiServer.Packaging assembly:
public abstract class PackageInitializer: IInitializableModule, IPackageNotification {
#region Implementation of IInitializableModule
public virtual void ConfigureContainer(ServiceConfigurationContext context);
public virtual void Initialize(InitializationEngine context);
public virtual void Uninitialize(InitializationEngine context);
public virtual void Preload(string[] parameters);
#endregion
#region Implementation of IPackageNotification
public abstract void AfterInstall();
public abstract void AfterUpdate();
public abstract void BeforeUninstall();
#endregion
}
The PackageInitializer
class combines the IInitializableModule
and IPackageNotification
interfaces. Inheritors of this class are instantiated and executed by the Optimizely Framework initialization system in the same manner as for the regular IInitializableModule
.
The Initialize
method in PackageInitializer
determines whether the app (package ID) that contains the assembly with the inheriting class is newly installed, and calls the AfterInstall
method if necessary; if the app is newly updated, calls AfterUpdate
if necessary.
When you override the Initialize method, call the base implementation before proceeding with the initialization to ensure that the AfterInstall
and AfterUpdate
methods are executed before the initialization.
The BeforeUninstall
method is called before the package contents are removed when the user clicks Uninstall in the app system.
AfterInstall method
The AfterInstall
method is called after the app installation is complete and only the first time the application starts, (as opposed to the Initialize
method, which is called each time the application starts).
The following procedure shows the app installation and the point where the AfterInstall
method is called:
- The user clicks Install in the app management user interface.
- The app is deployed to the site.
- Site restart occurs automatically, or the user clicks Restart in the app management user interface.
- On system startup, the app assemblies load into the AppDomain.
- The Initialize method is called for the initializable modules.
- The
AfterInstall
method is called frombase.Initialize
.
To get custom code executed after an app installation:
- Create a class that inherits from
EPiServer.Packaging.PackageInitializer
. - Decorate the class with a
[ModuleDependency(typeof(EPiServer.Packaging.PackagingInitialization))]
attribute. - Override the
AfterInstall
method and put your custom code inside this method. - If the app requires custom initialization:
- Override the
Initialize
method. - Call
base.Initialize
in the overridden method body. - Place your custom initialization code after the
base.Initialize
method call.
- Override the
AfterUpdate method
The AfterUpdate
method executes after the app update is complete and is called only the first time the application starts after updating an app.
The following procedure shows the app installation and the point where the AfterUpdate
method is called:
- The user clicks Update in the app management user interface. The updated app is deployed to the site.
- Site restart occurs automatically, or the user clicks Restart in the app management user interface.
- Upon system startup, the app assemblies are loaded into the AppDomain.
- The Initialize method is called for the initializable modules.
AfterUpdate
is called frombase.Initialize
.
To execute custom code after an app update:
- Create a class that inherits from
EPiServer.Packaging.PackageInitializer
. - Decorate the class with
[ModuleDependency(typeof(EPiServer.Packaging.PackagingInitialization))]
attribute. - Override the
AfterUpdate
method and put your custom code inside this method. - If the app requires custom initialization:
- Override the
Initialize
method. - Call
base.Initialize
in theoverridden
method body. - Place your custom initialization code after the
base.Initialize
method call.
- Override the
BeforeUninstall method
The BeforeUninstall
method executes immediately after the user clicks Uninstall in the app management user interface, but before the uninstallation occurs. If an exception occurs in the BeforeUninstall
method, the uninstallation is aborted. It does not trigger when uninstalling an app using Visual Studio.
The following procedure shows the process of app uninstallation:
- The user clicks Uninstall in the app management user interface.
BeforeUninstall
is called.- The actual removal of the app is performed (but app assemblies are still loaded in the AppDomain).
- Restart occurs automatically, or the user clicks Restart in the app management user interface.
- Assemblies from the deleted app are no longer loaded into the AppDomain.
To execute custom code before app uninstallation:
- Create a class that inherits from
EPiServer.Packaging.PackageInitializer
. - Override the
BeforeUninstall
method and put your custom code inside this method.
Manage dependencies
If an app is dependent on other systems or apps, you must indicate these dependencies in the ModuleDependencies
element so that it calls initialization methods after those of the listed dependencies.
To load the app automatically when a dependency is loaded, set the type attribute to RunAfter
. This is needed for CMS UI apps; otherwise, they are not loaded.
Updated 5 months ago