When it comes to software, a component or service which survives for an extended period of time without substantial refactoring is generally considered to be an anomoly. However, there are cases when a solution fulfills the business requirements so completely that you have the luxury of leaving it on autopilot for quite some time. Generally, this only happens in situations where a broad set of use cases were examined up front, the primary stakeholders were accurately identified, clear goals were established, multiple prototypes were developed, user participation resulted in continuous feedback loops, and the architecture and implementation were properly abstracted from external factors. The Skinning Engine in DotNetNuke is an example of a solution which has stood the test of time and has served the community well.
Originally developed in 2003 and released with DotNetNuke 2.0 in 2004, the skinning engine had some very clear goals in mind - the most important being that it needed to be focussed on designers rather than developers. To accomplish this goal, the ultimate solution needed to abstract the inner workings of DotNetNuke and ASP.NET application framework from the designer. It also needed to leverage standard web design elements and allow designers to use their design tools of choice. Finally, it needed to encourage creativity, and allow for portability so that skins could easily be installed in sites without developer intervention.
Clearly, the initial release of the skinning engine was a large success and it helped accelerate the growth of the platform in the early stages. Designers appreciated the flexibility of the solution as well as the clean abstraction of form and function. An entire commercial ecosystem developed around skins and there are now thousands of professional skin offerings available in the community.
When ASP.NET 2.0 came onto the scene in 2005 there was some trepidation that that heavily marketed "Master Pages" concept would replace the DotNetNuke skinning engine. However, it did not take long to realize that a Master Page is not a designer solution - it is a developer solution. Therefore, it did not fulfill the same set of business requirements, or cater to the same stakeholders. Combined with the fact that Master Pages are not portable, it was fairly clear that the DotNetNuke skinning engine was the superior architecture for the future.
Over the years, some minor enhancements have been made to the original skinning engine to add more flexibility and provide better abstraction; however, for the most part the basic architecture has remained relatively unchanged. This has been a real benefit to the community as it means that skins created for DotNetNuke 2.0 and 3.0 and 4.0 continue to be compatible with the most recent versions of the platform.
With DotNetNuke 5.0, a number of new enhancements have been introduced to the skinning engine which provide additional power and flexibility to designers. In order to maintain compatibility with previous versions of the platform, the new features are completely optional so that they can be leveraged at the discretion of designers.
1. Packaging and Distribution
As I mentioned earlier, the portability aspect has long been one of the greatest benefits of the DotNetNuke skinning architecture. Originally a skin "package" was simply a group of files ( ie. HTML, CSS, images, etc... ) comprising a single skin, compressed into a standard ZIP file. Later, we modified the definition of a skin package so that a designer could create a more comprehensive design in a single ZIP file which internally included both a skin package ( skins.zip ) and container package( containers.zip ). This format represents the vast majority of skin packages today.
Although there is nothing inherently wrong with the current model, there are some limitations which have been identified. From a high level, the lack of a consistent packaging model across all DotNetNuke features has resulted in additional complexity in the application and a less than optimal user experience. In addition, uninstall and dependency management is not handled elegantly and there is too much redundancy in terms of maintaining multiple independent package formats. As a result, a new common packaging model has been introduced in DNN 5.0 which is designed to address some of these limitations.
There is now a "manifest" for all package types. A manifest is used to describe attributes about a package as well as identify all resources included in the package. Skins never supported the concept of a manifest or versioning in the past, but in the new model there is now the ability for a designer to include additional information to describe the content and creator of the package. Attributes include the owner of the package, the organization name, a website URL for further information, an email address for support inquiries, and a version identifier for identifying the specific release of the package. There is also the ability to include a license agreement which an end-user must accept during installation of the package, as well as release notes which can provide a summary of fixes or enhancements included in each version.
In DNN 5.0, packages also have the ability to contain additional resources. So why is this relevant for skins? Well consider the situation where a skin relies on a custom skin object. This effectively means that both the skin and skin object need to be installed at the same time in order for the skin to function correctly - a concept which was not possible in the past. In DNN 5.0 it is now possible to bundle skins, containers, and skin objects in the same package for distribution.
And since most people are apprehensive about adopting new features due to the perceived learning curve, we have included the ability to create the manifest file and skin package very simply using a new wizard-based Skin Packager option which is now included in the core platform. In most cases, taking advantage of the new skin package format is as simple as stepping through the wizard to produce a new ZIP file.
2. Skin Object Definition and Attributes
Skin objects are a critical part of the DotNetNuke skinning architecture. They basically represent controls which are included in a skin to provide improved functionality and usability. In order to abstract the details of skin objects from designers, DotNetNuke has always supported a notion of [TOKENS] within HTML skin files. The idea behind tokens is that a designer could include them within their design and then the system would perform the necessary substitution with the actual server-side control when the skin was installed. This worked well; however, there was one significant flaw. Most skin objects have a variety of attributes which affect their behavior and there was no way to elegantly include these attribute specifications within the HTML skin file. As a result, a second XML file needed to be handcrafted by designers to include the attributes for the skin. Now XML is not a designer-friendly format and is not well supported by most design tools. So basically one of the primary goals of the skinning solution was somewhat compromised by this situation.
Not wanting a let a problem go unsolved, Nik Kalyani began experimenting with a number of approaches for dealing with this issue and ultimately made a proposal which was so simple, yet elegant, that I am surprised it took this long to be revealed.
With the proliferation of Flash on the web, the support for <object> tags has grown substantially over the years. <object> tags are really intended for client-side control specifications; however, there is nothing stopping us from leveraging them for our own purposes. And the great thing about <object> tags is that they already provide support for an arbitrary list of parameters which can be managed in most design tools. So lets look at an example:
Instead of having an HTML file with a token:
and an XML file with:
I can rewrite this within a single HTML file as:
<param name="ShowWeb" value="True" />
<param name="ShowSite" value="True" />
When the skin is installed, the system will automatically convert the <object> tag and parameters into an ASP.NET user control declaration:
<dnn:SEARCH runat="server" id="dnnSEARCH" showWeb="True" showSite="True" />
This is a great simplication for designers and should make the DotNetNuke skinning engine even more designer-friendly.
3. [TEXT] Skin Object / Localization
While the DotNetNuke framework has supported localization since DNN 3.0, there has never been a capability to support localization in skins. At Sebastian Leupold's insistence, a new skin object was introduced into DNN 5.0 which provides both localization support as well as a number of other very useful features. So lets look at an example:
In the ASCX skin file, you could include the following declaration:
<dnn:TEXT runat="server" id="dnnTEXT" Text="Test" resourceKey="Test" />
Effectively what this means is that the system should display the default text of "Test" in the skin at the location where the skin object is specified. But what about the second parameter named "resourceKey" ? Well, basically this is telling the system to look in a related resource file for a key named "Test" and if it exists, use the string value which is included. Resource files for skins follow the same model as that for Modules; with any RESX files residing in an App_LocalResources folder beneath the Skin folder.
And for even more power consider the following:
<dnn:TEXT runat="server" id="dnnTEXT" Text="Test" resourceKey="Test" replaceTokens="True" />
The "replaceTokens" attribute indicates that the system should use the token replace functionality to dynamically substitute tokens with environment values at run-time. This can be very powerful if you would like to include items like page name, site name, etc... in your skin.
4. Parser Optimization
The skin parser has always supported a concept referred to as 'named instances'. This allowed you to have multiple definitions for the same type of skin object in a skin or container. This was done in HTML skins by using a notation such as [ACTIONBUTTON:1] and [ACTIONBUTTON:2]. When the skin was parsed, the system created the ASCX file and included user control registrations and substitutions for each named instance:
<%@ Register TagPrefix="dnn" TagName="ACTIONBUTTON1" Src="~/Admin/Containers/ActionButton.ascx" %>
<%@ Register TagPrefix="dnn" TagName="ACTIONBUTTON2" Src="~/Admin/Containers/ActionButton.ascx" %>
<dnn:ACTIONBUTTON1 runat="server" id="dnnACTIONBUTTON1" CommandName="AddContent.Action" DisplayIcon="True" DisplayLink="True" />
<dnn:ACTIONBUTTON2 runat="server" id="dnnACTIONBUTTON2" CommandName="SyndicateModule.Action" DisplayIcon="True" DisplayLink="True" />
Although this works, it is not very efficient. There is no reason why each named instance should have its own individual control registration. Since each named instance is related to the same skin object, there only needs to be a single control registration:
<%@ Register TagPrefix="dnn" TagName="ACTIONBUTTON" Src="~/Admin/Containers/ActionButton.ascx" %>
<dnn:ACTIONBUTTON runat="server" id="dnnACTIONBUTTON1" CommandName="AddContent.Action" DisplayIcon="True" DisplayLink="True" />
<dnn:ACTIONBUTTON runat="server" id="dnnACTIONBUTTON2" CommandName="SyndicateModule.Action" DisplayIcon="True" DisplayLink="True" />
In DNN 5.0 the parser was modified to include this optimization.