This article aims to demonstrate how to effectively use FCKEditor and CKFinder to create content management functionality using C# and SQL Server.
FCKEditor is a long standing open source project to develop a lightweight but powerful cross-platform in-line HTML editor. It supports most major browsers. You can find out more about FCKEditor here. Combined with the ajax file manager CKFinder its a killer combination.
Index
- Running the application
- Architecture
- The FrameworxLite namespace
- Embedding FCKEditor in .NET applications
- Editing Content in .NET
- Content Delivery
- Summary & source code
What this article contains.
This isn't supposed to be an example of cutting edge C# coding. Its primarily about FCKEditor and how it can be used with .NET. The administration area for the CMS has no security, its been removed from the original application. People have many different ways of implementing user logins, I'll leave it up to you.
The End Result.
The final application is a fully functioning website and CMS called FrameworxLite, with a database-driven backend displaying content editing and formatted using FCKEditor, with images being added using CKfinder. FrameworxLite is a cut down version of a much larger CMS developed in-house by my company, Frame Digital which i'm a joint partner in.
You'll need IIS and SQLServer to run it. The entire application is released under the GNU General Public License. In short, it's free and you can do with it as you want, but I can't support it.
There is a functioning demo running at frameworxlite.psykoptic.com. You can download the source code for this article here (1.2Mb).
Running the application [skip]
The site is coded in C#, using the 3.5 version of the .NET frame work. It won't work for v2.0. You'll need IIS and SQL Server 2005 or later. The site is setup so that it must run from the domain root, eg
http://localhost/ - will work
http://localhost/myapp/ - won't work
IIS is available in XP Pro and most versions of Vista. If you don't have SQL Server, you can download the Express Version here. To get the best out of this article, you'll also need to have Visual Studio 2008 installed. If you don't have it, you can download Visual Studio Express from Microsoft.
You'll also need to create the FrameworxLite database. You can either run the SQL script in the \database\sql\ directory in SQL Server or use the database installer in \database\db-installer\
Once you have the site configured in IIS and the database setup you'll need to modify the web.config file for your database settings:
<connectionStrings>
<add name="AppConnString" connectionString="Server=.\SQLEXPRESS;UID=[userid]; PWD=[password];Database=FrameworxLite;" providerName="System.Data.SqlClient" />
</connectionStrings>
Once everything is is up and running you should see the demo site homepage.
The architecture bit [skip]
FrameworxLite has a public UI and an administration area available from http://[domain]/admin
The application is an example of a multi-tier entity/service architecture. The UI componets talk to the entities, the entities to the services, the services to the data access components. An entity class contains the basic data construct and the Service Layer handles the business logic translation between the entities and data access layer (a class named DataObject). This diagram outlines the seperate tiers interacting during a load/page postback life cycle.
The CMS application is structured like this:
CMS Architecture
Each module is independent of one another and uses FCKEditor and CKFinder differently.
- Pages
Core Page content management functionality. FCKEditor is used to format page content
- News
News module. FCKEditor is used for HTML formatting and CKFinder is used to select summary images
- File Manager
Main install of CKfinder for managing files and images.
The FrameworxLite namespace
The main structure is as follows:
/BaseClass/
- Frameworx.cs. core base class for CMS modules
- PageBase.cs base class for website pages
/Helper/
This directory contains a number of helper classes with varying functionality:
- DataObject.cs this is the main database access layer
- DynamicBuilder.cs translates database objects to Entity objects
- FCK.cs used for embedded FCKeditor in CMS modules
- Tools.cs various small helper functions
/Interface/
Interface classes.
- ICMS.cs - Interface used for CMS
- IMaster.cs interface used for Masterpages (if required)
/Entity/
Contains all the entity classes for the project. Business entities are used to pass data between component and services. In this case our entities represent the news and page data.
/Service/
Contains all the service classes used for the project. The service layer is used to provide translator components that translate entities between the UI and raw data.
Embedding FCKEditor in .NET applications [skip]
FrameworxLite tries to make adding new instances of FCKEditor as easy as possible. A quick outline:
- The assembly FredCK.FCKeditorV2.dll must be added to the /bin/ directory
- The web.config file at /admin/web.config contains settings using by the application and FCKEditor:
- FCKBasePath - the path to the FCKEditor install
- The FredCK.FCKeditorV2 assembly is registered at <system.web>..<pages>..<controls> This is so we don't have to register it on a per-page basis
- The Helper.FCK class is used to instantiate an instance of FCKeditor
- FrameworxLite uses a custom default ToolbarSet, the ability to create forms etc has been removed.
This is the full FCK.Setup method:
/// <summary>
/// Applies settings to the supplied FCK instance
/// </summary>
/// <param name="instance">The instance.</param>
/// <param name="toolbarset">ToolbarSet defined in the FCKconfig.js file</param>
/// <param name="height">Height in pixels</param>
public static void Setup(FCKeditor instance, string toolbarset, string height)
{
if (instance == null)
{
return;
}
if (toolbarset.Length == 0)
{
toolbarset = "Default";
}
if (height.Length == 0)
{
height = "400";
}
string basePath = ConfigurationManager.AppSettings["FCKBasePath"];
instance.BasePath = basePath;
instance.SkinPath = basePath + "editor/skins/default/";
instance.Height = Unit.Parse(height);
instance.ToolbarSet = toolbarset;
}
Using a method means we can consistently embed FCKEditor without using the same code over and over again. Now FCKEditor can be embedded by a simple call, specifying the FCKEditor instance name, ToolBarSet and Height each time:
FCK.Setup(FCKContent, "Default", "400");
Editing content using .NET [skip]
The underlying service layer is where most of the magic happens. The services handle all the data inserts, updates and population of empty entities. Explaining how this works in detail is beyond the scope of this article.
The entities are mapped to DataReaders making use of DynamicBuilder, an excellent class library posted on CodeProject by Herbrandson. You can read more about this on codeproject.com.
Each CMS module follows the same load/postback cycle - this example uses the News edit module:
Page_Load
Load the page, setup FCKEditor and configure default values such as the page header, button visibility and any form default values. We store the unique id in ViewState so it can be used later for updating or deleting.
public void Page_Load(object sender, System.EventArgs e)
{
int id = Tools.RequestAsInt("ID");
FCK.Setup(FCKContent, "Default", "");
if (!IsPostBack)
{
// decide what buttons to display
if (id > 0)
{
ViewState["id"] = id;
this.GetRecord(id);
btnDelete.Visible = true;
this.SetBread("News articles / Edit article");
}
else
{
this.SetBread("News articles / Create new article");
chkEnabled.Checked = true;
}
}
}
GetRecord
If record id has been submitted create a new News entity and use the NewsSvc.GetByID method to load from the database and populate the entity.
Note the service makes use of the DataObject class, which handles database access. Employing the using directive ensures that database connections are created then disposed of after use. If you don't use this, then pretty soon you'll have open database connections everywhere. This is a Bad Thing™.
If we get a valid entity returned from the service, populate the form with the news entity values, and store in the ViewState for saving on PostBack.
/// <summary>
/// load current record
/// </summary>
/// <param name="id">id of item</param>
private void GetRecord(int id)
{
News news = null;
using (var newsSvc = new NewsSvc())
{
news = newsSvc.GetByID(id);
}
if (news != null)
{
txtTitle.Text = news.Title;
txtSummary.Text = news.Summary;
txtURL.Text = news.URL;
FCKContent.Value = news.Article;
chkEnabled.Checked = news.Enabled;
if (news.Image.Length > 0)
{
imgThumb.ImageUrl = news.Image;
}
}
else
{
Response.Redirect("List.aspx");
}
}
Updating and creating new pages
The same method is used for creating and editing news articles. If a valid id is stored in the ViewState we are updating, otherwise we are creating a new article.
We populate the entity with values from the form the send it to the NewsSvc for processing.
/// <summary>
/// admin/Update item
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
protected void btnSave_Click(object sender, EventArgs e)
{
if (Page.IsValid)
{
var news = new News();
news.ID = Convert.ToInt16(ViewState["id"]);
if (news.ID > 0)
{
news.IsNew = false;
}
news.Title = txtTitle.Text;
news.Summary = txtSummary.Text;
news.URL = txtURL.Text;
news.Article = FCKContent.Value;
news.Enabled = chkEnabled.Checked;
news.Image = txtImage.Text.Length > 0 ? txtImage.Text : imgThumb.ImageUrl;
using (var newsSvc = new NewsSvc())
{
newsSvc.Save(news);
}
Response.Redirect("List.aspx");
}
}
The NewsSvc.Save method figures out whether the entity requires updating or saving depending on the .IsNew property and offloads the entity to the relevant internal method.
The entire process can be outlined in this diagram:
postback process
Content Delivery [skip]
Page content is embedded in the website using one of two methods.
- Directly on a page implementing the user control in /usercontrols/PageContent.ascx
- Using virtual pages, created in the CMS with a specific URL.
Using the UserControl
The usercontrol is easy. Just add it to any page and complete the Page_Id property. The page_id is the unique id of the page content in the database table CMS_Page.
<CMS:PageContent ID="PageContent1" EmbedMeta="false" EmbeddTitle="false" PageID="1" runat="server" />
It has a number of properties:
- PageID - content PageID found in the database table [CMS_Page].[id]
- EmbedMeta - use the page meta Keyword and Description information associated with this content and add to the page header. Defaults to true.
- EmbedTitle - use the page title information associated with this content and add to the page header. Defaults to true.
Using Virtual Pages
This method is even easier. You don't have to create a physical page on the file system. Enter a URL when creating a page and the request will be handed off to the /get-virtual.aspx page, embedding the content.
Configuring a virtual page
Why use two ways to embed content?
Sometimes people want complicated pages with multiple content elements and functionality, sometimes its just a page of information, such as the license page on the demo. Having two methods makes the application flexible for different implementations.
The demo site show a number of uses, such as virtual pages, using two controls on a page and a combined page with news articles and content.
Caching
Content loaded from the front-end of the website is cached - after all, content is not getting updated every minute, so there's no need to keep hitting the database everytime the page loads.
The PageSvc.GetById method uses the .NET HttpRuntime.Cache to store each page for 10 minutes. If a page is updated in the CMS, the cached object is destroyed.
/// <summary>
/// Returns CMS_Page entity, with cachable options
/// </summary>
/// <param name="pageID"></param>
/// <param name="UseCached">used cached version of item</param>
/// <returns>An entity of type Entity.PageData</returns>
/// <remarks></remarks>
public Entity.CMS_Page GetByID(int pageID, bool UseCached)
{
CMS_Page GetByIDReturn = null;
if (UseCached)
{
GetByIDReturn = (CMS_Page)HttpRuntime.Cache.Get("Pageid" + pageID); ;
}
if (GetByIDReturn == null)
{
const string SQL = "SELECT " + Columns + ", CMS_Section.[Name] AS SectionName, CMS_Section.ID AS SectionID FROM CMS_Page INNER JOIN CMS_Section ON CMS_Page.SectionID = CMS_Section.ID WHERE CMS_Page.ID=@pageID;";
_data.CommandText = SQL;
_data.Parameters.Add("@pageID", SqlDbType.Int).Value = pageID;
GetByIDReturn = _data.LoadEntity<CMS_Page>(_data.GetReader());
if (UseCached && GetByIDReturn != default(CMS_Page))
{
// add to cache
HttpRuntime.Cache.Insert("Pageid" + pageID, GetByIDReturn, null,
DateTime.Now.AddMinutes(10), Cache.NoSlidingExpiration);
}
}
return GetByIDReturn;
}
Summary
FrameworxLite contains a lot of code, way too much to cover in one article. Its also based on a larger application so there may be some items that seem redundant or could have been put to better use. Well, they probably were at one point :-)
Most people I see on the FCKEditor forums repeatedly ask the same questions, usually about basic configuration issues, often associated with not having the correct paths setup. Having a fully working application where everything is already setup and working I hope will make it easier to understand how all the pieces of the jigsaw fall into place.
Download the source code for this article here.(1.2Mb)
View functioning demo here.
Back to top