A do-it-yourself ASP.NET content management system in just a few hundred easy steps
Update: this post is a bit dated, since I am no longer working on the project described, but I am still thinking about turning this into a sample project that may help others that could benefit from putting together their own CMS.
I have been running a public-facing content-managed website for a number of years. It’s purpose has turned out to be more theoretical than practical, due to the fact that most of its planned uses never materialized, even after development was complete (ouch!).
The present problem to be solved is that the existing website is far more complex than is really necessary. It consumes server resources out of proportion to its importance, and requires a level of programmer expertise that is just plain unreasonable under the circumstances. There were good reasons for this when the site was created, but times have changed and what is needed now is a simple, content-managed database query website.
Simple? Content-managed? Can I use those two words together? Well, yes and no. The simplest way to achieve that is to buy a commercial Content Management System (CMS) and have the vendor support it. That’s not really so simple, though. There are those maintenance fees, of course, although you’re either going to have to pay a vendor for support or pay a web programmer to maintain it. Then there are the potential issues with the vendor changing directions, cutting their customer support, raising annual fees, and/or going out of business.
I lean toward the do-it-yourself, supported-by-a-programmer approach for several reasons:
- ASP.Net will be around even if my favorite CMS vendor no longer is (or if any of the other issues above arise)
- ASP.Net already contains most of what is required to implement a simple CMS. The programmer just needs to add glue and user interface.
- If the internals of the DIY CMS are simple and clean, there are lots of capable people around that could potentially tweak it for you. Once the glue is in place and running, deep esoteric understanding of ASP.Net should not be required to maintain the CMS or even to upgrade to newer versions of ASP.Net. (Making major enhancements might be a different matter.)
My CMS consists of two websites, a “front end” and a “back end”, and a SQL Server content database. Pages are served from the database rather than from aspx files on the web server. To do this, the CMS front end maps the incoming virtual URLs on to physical “page template” URLs, using the ASP.Net Routing feature. The page templates can then access the fields belonging to the underlying database record when rendering content.
The page templates are aspx files, and the template file to use for a particular web page is specified as a property of the web page. Most pages will use a default template, but it can be useful in some circumstances to create special aspx templates. Actually, at this early stage, I only have one template file and I will be happy if I don’t need to add any more.
My main purpose for the website is to host SQL Server Reporting Services reports, but it could potentially be used for many other things as well.
Routing
Making this virtual-to-physical mapping work is indeed simple. At Application_Start time the CMS builds an ASP.Net routing table containing all of the virtual URLs defined in the database, mapping them to the appropriate physical page templates, and each individual route also contains a DataToken that specified the database primary key for the actual data comprising the web page content.
URL Authorization
The content database also contains access security (authorization) information for each page. Unfortunately, the built-in ASP.Net URL Authorization scheme only accepts authorization data that is embedded in web.config files in the website root and possibly in other physical subdirectories, making it pretty much useless for a website that draws its content from a database. Fortunately, you can write your own custom UrlAuthorizationModule that knows what to do.
The custom UrlAuthorizationModule, again, is quite simple. It registers with the AuthorizeRequest event and when it sees an URL that is defined in the content database, it performs an authorization check starting with the target page and proceeding up through any parents to the root page, checking the role assignments stored in the database against the current logged-in user, if any. If at any point in this scan it finds that the user does not have the required access, it fails the request with a 401 HTTP_STATUS_DENIED status response. ASP.Net then responds automatically according to web.config and other login control settings. In my CMS, if the user is not logged in then an automatic redirect to the login page occurs, as is typical in ASP.Net applications.
Site Map
The remaining major custom components of my CMS are a custom SiteMapProvider and a custom SiteMapNode to go with it, each inheriting from the corresponding standard component. The extension to SiteMapNode is trivial — I added an IsHidden property that is used to automatically “trim” certain pages that I don’t want showing up in the site map. Currently this is limited to certain special pages like the Login page and the Page Not Found page, but it would be a simple matter to add a HideFromSiteMap flag in the web page database record to allow any page to be hidden this way.
The custom SiteMapProvider loads itself from the content database using some of the same information used to build the routing table. The site map node key property is in fact the same as the route property of the corresponding page route, and likewise the two Url properties are the same.
The default security trimming behavior of the SiteMapProvider object, implemented in the IsAccessibleToUser method, must be overridden; otherwise the standard UrlAuthorizationModule authorization will be called to authorize against web.config. Since I have my own custom UrlAuthorizationModule that authorizes against the database, I simply broke the authorization logic out into a separate static method (as is done in the standard version of the module) and then I called that method, CheckUrlAccessForPrincipal, from the override in my custom SiteMapProvider.
WebPageMap
I made two major errors early in the development of this website. First, I didn’t see that using Routing was important. I used URL rewriting instead, and eventually discovered that it has some major quirks (I won’t call them bugs since they have been around long enough to become “features”). Rather than dig myself deeper, I then switched to Routing, which handles the quirks for me.
The other error was to try to use the SiteMap or the routing tables to do things they weren’t intended to do. When I was using my own rewriting scheme, I used the SiteMap as my route table, rather than create a separate one. In an ideal ASP.Net world, authorization, routing, and site maps might all be integrated. In this ASP.Net world, however, they most definitely are not.
The problem with using the SiteMap as a routing table is that when you finally get security trimming working right, the SiteMap will trim your routes! That doesn’t work out well at all. What I finally did, that is simple and works extremely well, is to create an object I called WebPageMap, that keeps track of essential information about all the active pages in the content database. Since this map is memory resident, I kept it small by not including the actual content data from the web pages, but it contains enough information to perform authorization, build the route table, and build the site map.
Content Editing
There is not much point in having a CMS if you don’t have a good way to edit the content residing in the database. My approach, so far, has been to create a “back end” website using ASP.Net dynamic data web pages to “scaffold” the content database. This is actually a very flexible tool that can be customized to provide a good end-user (content editor) experience, although I haven’t done that yet.
The hardest part of the back end was configuring it to provide easy HTML editing. I had had some experience with TinyMCE and that is what I used. You can create custom editing controls in a dynamic data website for doing just that sort of thing, and I did. The tricky part is getting TinyMCE to encode “dangerous” characters in a way that keeps .Net Framework 4.x happy. Fortunately, I had prior experience with that, and it wasn’t too hard once I realized that the Encoding:XML option of TinyMCE really was still there in the latest version, and just not mentioned anywhere in the documentation. There’s also a trick for fixing up apostrophes that must be applied.
Conclusion
The result of these design choices was a surprisingly simple but very capable content management system, or at least an early, proof-of-concept of one. If I am able to make time for it, I will describe the custom components in greater detail and perhaps even include source code.
There is one other aspect of my CMS that I have not even begun to describe, and that is a second level of template processing, “content templates” as opposed to “page templates,” that I developed for the earlier Data Portal. Where page templates are custom aspx pages, content templates are chunks of HTML in which a user (content editor) can embed tokens that are replaced with canned controls when the page is rendered. Having this level of template can largely obviate the need to develop new aspx page templates. Instead, you add new tokens (“widgets”) to the token parser and the editors can insert them using their HTML content editor and format away to their hearts’ content.
I will definitely try to describe the content template system, which by the way is embedded in an aspx page template using a custom control, in a future post.
Pingback:Fun with TreeView - Megan's Space