Introduction
The purpose of this article is to introduce you to Kaliko CMS - a new open source content management system (CMS) for ASP.NET - and to get you up and running by creating your first website using the system.
Besides installing the framework and implement some basic things we will also touch more advanced subjects.
As a CMS it will provide a powerful and flexible framework to use when building websites. It's also quite extensible, so you won't be limited by what's included out-of-the-box. If you need a particular type of data on a page you can easily create a custom type for that property. We will return to such examples in this article.
While Kaliko CMS supports both WebForms and ASP.NET MVC this article will focus the former. If you want to use ASP.NET MVC instead of WebForms then goto this article instead.
As there are plenty of areas to cover I'll try to keep the text as brief as possible and will also try to link to more information over at the project site if you wish to dig deeper into any subject. Some of the code samples will be abridged, but you'll find the full source code in the project download and also a link to the corresponding file at GitHub for each code listing. My suggestion is to download the demo project and use this article as an introduction to how the different parts were made.
For this demo we will be developing something that resembles a standard corporate web site including features such as dynamic start page content, articles, news and product information from an external system. We will also use ASP.NET Identity for authentication.
To keep this introduction brief I've placed two sections at the end of this article on the background why I created this CMS and also some design decisions made along the way.
For feedback and questions
If you find any bugs or have a request for a feature that you think is missing, please post it over at GitHub. All feedback is welcomed.
If you run into any question or problem while using Kaliko CMS there's a forum for developers over at Google Groups.
Please only use the commenting function here at CodeProject for feedback or questions concerning this article and/or the demo project provided herein. For other development related questions please use the forum.
Requirements
In order to get the most out of this article you should know your way around Visual Studio and have a basic understanding of ASP.NET.
In this demo project I will be using SQLite as database for the content. This is because it's really easy to set up and to redistribute. You can however choose any of the other supported databases such as SQL Server.
I will be using NuGet in order to install the packages that are required. If you're not familiar with NuGet there's a great tutorial on how to get started with NuGet here.
Concept of the CMS
This is a very short introduction of the concepts, you will find more information here.
The main concept in Kaliko CMS is - like in many other CMS:s - pages. Each page has it's unique URL and is of a certain page type. A page type is a kind of blue print that defines what content a page can carry. It can be anything you like; an article, a news list, a start page or any other type of page you find useful. Page types are defined as classes by the developer.
Every page has a few default properties such as page name and publishing dates. In addition to those the page type is assigned additional properties by the developer that is unique for that particular page type.
Each property is of a certain property type. Kaliko CMS provides the most common types right out of the box but you can easily add your custom types with ease if you find that the basic types don't provide the functionality needed.
Each page type is assigned a page template that will control how the page is rendered. The template is a normal Web Form (or a controller action/view if you use MVC) where the page is provided as as strongly typed object.
If you want, you can go here to learn more about the concept and also get a list of the default property types.
Setting up the project
Creating a new project
Create a new ASP.NET Web Application project and select .NET Framework 4.5. (Although Kaliko CMS also works on version 4 we will need 4.5 in order for ASP.NET Identity to work.)
Select the Empty project template and be sure to select to add Web Forms folders and core references.
Set the default namespace to DemoSite (if you want it to correspond to the sample code, otherwise leave as is).
Install the NuGet packages
Select Manage NuGet Packages for Solution.. (found under Tools / NuGet Package Manager in the menu) and search for "KalikoCMS". If you rather prefer running the installation through the console I will include the command line for each package.
Install the core package
We start by installing the core package that is required, called KalikoCMS.Core. This is the base package that includes most of the required runtimes as well as the administration interface.
PM> Install-Package KalikoCMS.Core
Install a database provider
We then proceeds by installing the database provider which for this demo project will be KalikoCMS.Data.SQLite. (As mentioned before there are support for other databases including Microsoft SQL Server.)
PM> Install-Package KalikoCMS.Data.SQLite
Install a request module
Next step is to provide the proper request module, since we're using Web Forms we select KalikoCMS.WebForms. (If you develop a MVC project this is where you would select the MVC provider instead.)
PM> Install-Package Install-Package KalikoCMS.WebForms
Install optional packages
We'll continue with two optional packages, but since we want search functionality on the site as well as authentication using ASP.NET Identity we'll add them to our project.
PM> Install-Package Install-Package KalikoCMS.Search
PM> Install-Package Install-Package KalikoCMS.Identity
Besides the referenced DLL:s your project has been extended with two new sections under Admin called Identity and Search as well as a Login.aspx and Logout.aspx in the root of the project. Additionally a template has been created to create an admin user.
That's it. You now have a project with all the references we will be needing as well as the required folders and administration interface. It's almost time to start writing some code, but not quite yet.
You can find more information about the installation process together with some in-depth information about what has been added to your web.config and what parameters you can change there.
Authentication
Kaliko CMS doesn't have a tightly coupled authentication integration, so you are free to choose the authentication scheme you want - such as ASP.NET Identity or the older Membership providers. However it does provide an optional package with a database independent ASP.NET Identity implementation. This implementation will use the same database as the rest of the system, so if you are using SQLite the users, roles and claims will be stored in that same SQLite database. If you don't require any other specific authentication provider it's recommended that you use the KalikoCMS.Identity package as it also provides administration of roles and users as well. As we will be using this lets continue by setting up the admin role and user.
Creating admin role and user
There are two ways to add the role and user necessary to access the Admin folder that is by default protected. Either you remove the authentication requirement for the folder in web.config and in the administration interface manually add the role and user. If you do, be very sure to put the authentication requirements back when you're done.
The recommended approach however is to use the generated template called SetupAdminAccount.aspx in the root of your project to create the very first admin user and role.
Uncomment the code and set the username and password you want. (If you want you can also change the name of the role, but if you do be sure to change it in the web.config as well.)
<%@ Page Language="C#" %>
<%@ Import Namespace="AspNet.Identity.DataAccess" %>
<%@ Import Namespace="Microsoft.AspNet.Identity" %>
<%
var userName = "admin";
var password = "my-secret-password-goes-here";
var roleName = "WebAdmin";
var roleManager = new RoleManager<IdentityRole, Guid>(new RoleStore());
var role = roleManager.FindByName(roleName);
if (string.IsNullOrEmpty(password)) {
throw new Exception("You need to set a secure password!");
}
if (role == null) {
role = new IdentityRole(roleName);
roleManager.Create(role);
}
var userManager = new UserManager<IdentityUser, Guid>(new UserStore());
var user = userManager.FindByName(userName);
if (user == null) {
user = new IdentityUser(userName);
var result = userManager.Create(user, password);
if (!result.Succeeded) {
throw new Exception("Could not create user due to: " + string.Join(", ", result.Errors));
}
}
userManager.AddToRole(user.Id, roleName);
Response.Write("Role and user created!");
%>
Once you set the variables execute the page. If everything went well the response should be "Role and user created!". Important! Once done delete this file to ensure that your password doesn't remain in clear text format.
Login
Lets verify that we now have a valid admin user by logging into the system. Start the web project and navigate to the /Admin/-folder. You should be presented with the login form present in your project root. Enter your username and password and press the Log in-button. You should now have access to the administration UI.
Side note about security
By default Kaliko CMS creates the administration parts in a folder called Admin. You may want to use a less obvious path in order to keep potential attackers away. This can be done by renaming the admin-folder and making changes to the web.config. The changes to be made is the protected location of the folder and also telling the system where to find the files. The latter is made by setting the attribute adminPath in the siteSettings element. As the NuGet packages assumes that the folder is named Admin you might need to manually move files if you do an update. Hopefully this will be solved in future versions.
Mission brief
Our fictitious mission is brief is: "Company A wants a new web site. The start page should have a slider where they could add how many slides they want to and they also want a couple of teasers as well as a list of the latest news. They want to be able to add news structured per year. Each news item should also be able to show related news. They also require the site to be able display products from their product database without storing it double. It should also be possible to add standard pages/articles to the web site. All news and articles should be searchable.".
From these requirements we can begin to list what page types our system will need:
- Article page - a simple standard page type with a few fields
- Start page - a dynamic slider, a news listing and a couple of feature teasers
- News page - simular to the article type but used for news
- News list page - a page that aggregates and lists news, this will be our news archive
- Search page - a page that handles our search
- Product list page - a start page for the product section
Many of the page types are straight forward and we will be able to use standard Kaliko CMS components to implement these. However there are a few exceptions.
For the product pages we'll use page extension, that means that a page can serve content that isn't necessarily stored in the CMS. Therefore we only create one basic page type to act as a start for the product section on the site. From it we can provide sub pages directly from the external product database like detailed product pages. I'll return to the concept of page extenders when we implement this page type, but you can also find information about it here.
The start page needs a dynamic amount of sliders. This can be done by adding a slider page type under the start page but that would also mean that the slides would be treated as pages in the system. Instead we'll use the CollectionProperty type that allows a list of any other property type to be added to a page type. We'll also create our own property type to use both for each slide but also for the teasers as they kind of require the same fields.
That's a brief introduction to what we will need to implement. So lets not delay it any further, it's finally time to write some code!
Writing the code
Usually the workflow will be a bit different, but I'm aiming to keeping things together for simplicity in this article. I'll start by creating the custom property type we need, then continue with implementing the page types and lastly go through each page type and create its template. I will be using a MasterPage for all pages as well as Bootstrap for UI but in order to keep this article at a decent length I will try to shorten the code at times, therefore please browse the project files to see the full implementation.
Create a custom property type
I mentioned before that we needed a custom property type to handle both the slides and the teaser boxes on our start page. In most cases the built in property types are sufficient, but sometimes you'll need to add your own. Luckily that's pretty easy.
These are the property types included by default:
Property type |
Description |
BooleanProperty |
Used to represent true or false. |
CollectionProperty<T> |
Used to create dynamic collections of another property type. |
CompositeProperty |
Used to build complex property types as a set of existing property types. |
DateTimeProperty |
Used for dates. |
FileProperty |
Used to point to local files. |
HtmlProperty |
Used for HTML content. |
ImageProperty |
Used for images, allows the developer to set up image restrictions such as width and/or height. |
LinkProperty |
Used to point to either an external URL, a local page or file. |
MarkdownPropert |
Used for Markdown content. |
NumericProperty |
Used for integers. |
PageLinkProperty |
Used to point towards any other page in the system. |
StringProperty |
Used for simple strings represented by a single line in the editor. |
TagProperty |
Used to add tags to a page. |
TextProperty |
Used for longer strings represented by a multi line text area in the editor. |
UniversalDateTime |
Used for time zone independent dates. |
Adding a new property type can be done in two ways; from scratch or bringing together existing types. It's possible to create a completely customized property type with it's own editor, but since we only need to aggregate existing property types in our new feature property we can do this using the CompositeProperty
which requires far less work. If you're interested in writing a more complex property type you'll find a starter article here.
Lets start by adding the class that defines our new property type. We create a new class called FeatureProperty inside a new folder called PropertyType in our project. Let it inherit from KalikoCMS.PropertyType.CompositeProperty
and we also need to add an attribute; KalikoCMS.Attributes.PropertyTypeAttribute
.
The property type attribute needs a few parameters; a unique identifier (created by generating a Guid), a name, a description and a path to the editor control. In the case of composite controls set the editor control to the inherited EditorControl
in order for the system to wire up the correct editors needed.
Let's start by adding the attribute and the property fields that we want our new property type to have. The property fields also needs a PropertyAttribute
, just like when defining properties on pages. The fields our property require is a header, a description and a URL. The header can be done using a StringProperty
, the description by a HtmlProperty
and the URL by a LinkProperty
.
We also override the Preview
method in order to declare what of the property's content we want to see in collection lists.
/PropertyTypes/FeaturePropertyType.cs
namespace DemoSite.PropertyTypes {
using KalikoCMS.Attributes;
using KalikoCMS.PropertyType;
[PropertyType("9033A828-B49A-4A19-9C20-0F9BEBBD3273", "Feature", "Feature", EditorControl)]
public class FeatureProperty : CompositeProperty {
[Property("Header")]
public StringProperty Header { get; set; }
[Property("Feature body")]
public HtmlProperty Description { get; set; }
[Property("Featured link")]
public LinkProperty Url { get; set; }
public override string Preview {
get { return Header.Preview; }
}
}
}
That's it, our custom property type is done! Let's move on and create our page types where this new type will come to use.
Create the page types
Page types are defined as attributed classes inheriting from KalikoCMS.Core.CmsPage
. The class itself is attributed with KalikoCMS.Attributes.PageTypeAttribute
and the properties with KalikoCMS.Attributes.PropertyAttribute
. By inheriting CmsPage
our new page type will get default properties like page name and publishing information, so we only need to add what makes this page type unique.
With the PageTypeAttribute
you can also add a few optional features such as limiting which page types that can be created under the current page type as well as adding a small preview image that will be displayed when an editor is selecting page type for a new page. This is done with AllowedTypes
and PreviewImage
.
Sometimes a page type might not contain one single property definition, it might just be needed to be able to point out a template to run some code somewhere on the web site. I our case the search page type will be one such page.
Pages that should be indexed by the search engine should also implement the interface KalikoCMS.Search.IIndexable
.
If the system doesn't find your page type or one of it's properties it's most likely that you've left out the proper attribute for either the class or the property. All properties that should be stored on the page must also be made virtual (since they will be proxied out at runtime). Please note that you still can have properties in the same class that won't be stored. For instance if you have two properties that are stored (in other words are decorated with the PropertyAttribute
) - like FirstName and SurName - you can also have a property that's not - like FullName - that instead contains logic to return a value.
The PageTypeAttribute requires a name, a display name, a template path and an optional description. While the display name can be changed freely the name shouldn't be changed. It's used to tie together the page type code with the page type stored in the database.
(In case you use ASP.NET MVC rather than WebForms - like in this tutorial - you can leave the template value blank, the controller it self will wire up the connection.)
More about creating page types can be found here.
Let's start with the simplest page type, the one without any properties - the search page type.
Creating the search page type
For this page type we just inherit CmsPage
and add a the PageTypeAttribute
. We will later be implementing the templates under /Templates/Pages in our project, so we will point out the template although we haven't yet created it. Create a new class in the Models folder of the project and name it SearchPageType.
/Models/SearchPageType.cs
namespace DemoSite.Models {
using KalikoCMS.Attributes;
using KalikoCMS.Core;
[PageType("SearchPage", "Search page", "~/Templates/Pages/SearchPage.aspx", PageTypeDescription = "Used for search page")]
public class SearchPageType : CmsPage {
}
}
That's it! Our first page type is ready. Our news list page won't have any properties either, so let's do that one also.
Creating the news list page type
Same as for the search page type, add a new class under Models. This one will just hold news posts, so it doesn't need any properties of its own.
Since we only want to allow other news lists and news pages in the news list hierarchy we'll add AllowedTypes = new[] { typeof(NewsListPageType), typeof(NewsPageType) } in the PageTypeAttribute
. This will limit the page type options when adding a new page under a news list.
/Models/NewsListPageType.cs
namespace DemoSite.Models {
using KalikoCMS.Attributes;
using KalikoCMS.Core;
[PageType("NewsList", "News list", "~/Templates/Pages/NewsListPage.aspx", PageTypeDescription = "Used for news archives", AllowedTypes = new[] { typeof(NewsListPageType), typeof(NewsPageType) })]
public class NewsListPageType : CmsPage {
}
}
Let's continue with another one with a few properties.
Creating the start page type
On our start page we want a list of our feature property type as well as two for the teasers. The news list will be implemented in the template later and doesn't require any logic in the page type.
To create the list we create a property of the CollectionProperty<T>
type with the property type we want in our list as T
. Notice that we didn't have to write any code in our FeatureProperty
type in order to make it work in a collection. That's thanks to CollectionProperty<T>
that adds this functionality to any property type.
To find what property types that are shipped with the core installation see the section under Property types on the Understanding the concept page.
Note that all page type properties are made virtual as they always should.
/Models/StartPageType.cs
namespace DemoSite.Models {
using KalikoCMS.Attributes;
using KalikoCMS.Core;
using KalikoCMS.PropertyType;
using PropertyTypes;
[PageType("StartPage", "Start page", "~/Templates/Pages/StartPage.aspx", PageTypeDescription = "Used for start page")]
public class StartPageType : CmsPage {
[Property("Main feature slides")]
public virtual CollectionProperty<FeatureProperty> Slides { get; set; }
[Property("Main feature")]
public virtual FeatureProperty MainFeature { get; set; }
[Property("Secondary feature")]
public virtual FeatureProperty SecondaryFeature { get; set; }
}
}
Creating the news page type
We continue by creating the news page type. This page type will have a quite a few properties; a headline, a preamble and a main body. We'll be using the StringProperty
, the TextProperty
and the HtmlProperty
accordingly.
This page type should also be searchable, so we'll implement the IIndexable
interface. It only requires one member to be implemented - MakeIndexItem(CmsPage page)
. This function gets a page as parameter in and returns a populated IndexItem
in return. Most common information is set through the GetBaseIndexItem()
call, so you only need to specify unique information such as the title and content.
By setting the category we can also keep the page apart as a result. Like for instance with the news page want all related news but not the related articles the way to do it is by using category.
By adding the AllowedTypes = new Type[] {}-part to the PageTypeAttribute
we tell the system that no other pages should be able to create under a news page.
/Models/NewsPageType.cs
namespace DemoSite.Models {
using KalikoCMS.Attributes;
using KalikoCMS.Core;
using KalikoCMS.PropertyType;
using KalikoCMS.Search;
[PageType("NewsPage", "News page", "~/Templates/Pages/NewsPage.aspx", PageTypeDescription = "Used for news", AllowedTypes = new Type[] {})]
public class NewsPageType : CmsPage, IIndexable {
[Property("Headline")]
public virtual StringProperty Headline { get; set; }
[Property("Preamble")]
public virtual TextProperty Preamble { get; set; }
[Property("Main body")]
public virtual HtmlProperty MainBody { get; set; }
public IndexItem MakeIndexItem(CmsPage page) {
var typedPage = page.ConvertToTypedPage<NewsPageType>();
var indexItem = typedPage.GetBaseIndexItem();
indexItem.Title = typedPage.Headline.Value;
indexItem.Summary = typedPage.Preamble.Value;
indexItem.Content = typedPage.Preamble.Value + typedPage.MainBody.Value;
indexItem.Tags = "News";
indexItem.Category = "News";
return indexItem;
}
}
}
We continue with the page type that's mostly the same as the one we just did.
Creating the article page type
The article page type is pretty much the same as the news page type except that we have a couple more properties. We're adding an ImageProperty
so that we can have an image at the top of our article pages (besides also being able to add images in the main body, since it's a HtmlProperty
). We can attribute our image with the regular PropertyAttribute
, but we can also use ImagePropertyAttribute
instead and be able to set desired width and/or height of the image.
We will also add a TagProperty
. This is a property that we can use to tag our articles. As with the image we could use PropertyAttribute
but we would miss out on the abilitity to set a context for our tags. Defining a context would allow us to have separate tag clouds for different page types or even different tag properties on the same page. To define the context we use the TagPropertyAttribute
instead.
/Models/ArticlePageType.cs
namespace DemoSite.Models {
using KalikoCMS.Attributes;
using KalikoCMS.Core;
using KalikoCMS.PropertyType;
using KalikoCMS.Search;
[PageType("ArticlePage", "Article page", "~/Templates/Pages/ArticlePage.aspx", PageTypeDescription = "Used for articles")]
public class ArticlePageType : CmsPage, IIndexable {
[Property("Headline")]
public virtual StringProperty Headline { get; set; }
[ImageProperty("Top image", Width = 848, Height = 180)]
public virtual ImageProperty TopImage { get; set; }
[Property("Preamble")]
public virtual TextProperty Preamble { get; set; }
[Property("Main body")]
public virtual HtmlProperty MainBody { get; set; }
[TagProperty("Tags", TagContext = "article")]
public virtual TagProperty Tags { get; set; }
public IndexItem MakeIndexItem(CmsPage page) {
var typedPage = page.ConvertToTypedPage<ArticlePageType>();
var indexItem = typedPage.GetBaseIndexItem();
indexItem.Title = typedPage.Headline.Value;
indexItem.Summary = typedPage.Preamble.Value;
indexItem.Content = typedPage.Preamble.Value + " " + typedPage.MainBody.Value;
indexItem.Tags = typedPage.Tags.ToString();
indexItem.Category = "Article";
return indexItem;
}
}
}
We got one last page type to create, and this one will be a bit different.
Creating the product list page type
Our product list page will have a few properties like the other; a headline and a main body. But what really sets this apart is that we will implement a page extender. That means that we can serve content beyond just this page.
This is done by implementing the IPageExtender
interface. By doing so we get a function - HandleRequest(Guid pageId, string[] remainingSegments)
- where we implement the logic that checks if the request made beyond the page is a valid one and - if so - where it should lead.
If we have a page that is of a page type that implements a page extender called Products, then any call made to that url will return the page - as expected. Also any call with a url that matches a sub page (if we create any page under Products) of that page will return the sub page. The magic happens when we call for a url that doesn't belong to a page. Since our Products page also is a page extender, the HandleRequest
function will be called with two parameters; the identity of the current page and an array of remaining segments (for /products/info/abc/ the remaining segments would be info and abc). We then determine if this is a proper request and if it is redirects to the right file using HttpContext.Current.RewritePath
and return a true, if not we return false.
In our case we will create a template not connected to a particular page type later on and call it ~/Templates/Pages/ProductPage.aspx.
/Models/ProductListType.cs
namespace DemoSite.Models {
using System;
using System.Web;
using KalikoCMS.Attributes;
using KalikoCMS.ContentProvider;
using KalikoCMS.Core;
using KalikoCMS.PropertyType;
using FakeStore;
[PageType("ProductList", "Product list page", "~/Templates/Pages/ProductListPage.aspx")]
public class ProductListType : CmsPage, IPageExtender {
[Property("Headline")]
public virtual StringProperty Headline { get; set; }
[Property("Main body")]
public virtual HtmlProperty MainBody { get; set; }
public bool HandleRequest(Guid pageId, string[] remainingSegments) {
if (remainingSegments.Length != 1) {
return false;
}
if (FakeProductDatabase.IsValidProduct(remainingSegments[0])) {
HttpContext.Current.RewritePath(string.Format("~/Templates/Pages/ProductPage.aspx?id={0}&productid={1}", pageId, remainingSegments[0]));
return true;
}
return false;
}
}
}
That's it! We've completed all the page types. Now we just need to create templates.
Creating the templates
For all templates we create a folder under the project root called Templates. And under that folder we create three more folders called MasterPages, Pages and Units. As with everything else in this article, it's not necessary to name or structure this the same way, it should only be seen as a suggestion. You might have a better way that you want to structure your project in.
Creating the master page
We'll add a new class to our MasterPages folder and call it Demo.Master. We change so that our master page class inherits from KalikoCMS.WebForms.Framework.PageMaster
. By doing that we can use the object CurrentPage
in our master page in order to get the page object that we are rendering. In the front end we'll put the current page's name in the title.
We'll also add a MenuList
to display our top menu. MenuList
is a templated web control that will render a level of pages. It's simular to PageList
with the exception that it also has a template for selected item. For multi-level lists you should take a look at PageTree
and MenuTree
. You find a list of all web controls in the API documentation. While itterating through a list our Container
will hold a reference to the currently processed page through Container.CurrentPage
. All Kaliko CMS web controls resides in the namespace cms, so for MenuList
we write <cms:MenuList ... >.
Most web controls have an AutoBind
property. If set to true the control will ensure that it gets databinded before rendering, so no call to DataBind()
is necessary.
/Templates/MasterPages/Demo.Master
<%@ Master Language="C#" AutoEventWireup="true" CodeBehind="Demo.master.cs" Inherits="DemoSite.Templates.MasterPages.Demo" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><%=CurrentPage.PageName %></title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
<link rel="stylesheet" href="/Assets/Css/DemoSite.css" />
<link href='http://fonts.googleapis.com/css?family=Open+Sans+Condensed:700' rel='stylesheet' type='text/css'>
<link href="http://fonts.googleapis.com/css?family=Open+Sans:400,600,300" rel="stylesheet" type="text/css">
-->
-->
<script src="//code.jquery.com/jquery-1.11.0.min.js"></script>
</head>
<body>
<form id="Form1" runat="server">
<asp:Panel ID="Container" runat="server">
<nav role="navigation" class="navbar navbar-default navbar-static-top">
<div class="container">
<div class="navbar-header">
<a href="/" class="navbar-brand">Demo project</a>
</div>
<div class="navbar-form navbar-right navbar-search" role="search">
<div class="form-group">
<div class="input-group">
<input id="search-field" type="text" placeholder="Search" class="form-control">
<span class="input-group-btn">
<button id="search-button" type="button" class="btn btn-primary"><i class="glyphicon glyphicon-search"></i></button>
</span>
</div>
</div>
</div>
<cms:MenuList ID="TopMenu" AutoBind="True" runat="server">
<HeaderTemplate>
<ul class="nav navbar-nav navbar-right">
</HeaderTemplate>
<ItemTemplate>
<li><a href="<%#Container.CurrentPage.PageUrl %>"><%#Container.CurrentPage.PageName %></a></li>
</ItemTemplate>
<SelectedItemTemplate>
<li class="active"><a href="<%#Container.CurrentPage.PageUrl %>"><%#Container.CurrentPage.PageName %></a></li>
</SelectedItemTemplate>
<FooterTemplate>
</ul>
</FooterTemplate>
</cms:MenuList>
</div>
</nav>
<asp:ContentPlaceHolder ID="HeadlineContent" runat="server" />
</asp:Panel>
<div class="container main-content">
<asp:ContentPlaceHolder ID="MainContent" runat="server" />
<hr />
<footer>
<p class="pull-right"><a href="#"><i class="glyphicon glyphicon-chevron-up"></i> Back to top</a></p>
<p>© <%=DateTime.Today.Year %> Company, Inc.</p>
</footer>
</div>
</form>
<script>
$(document).ready(function () {
$('#search-button').click(doSearch);
$('#search-field').keypress(function (event) {
var keycode = (event.keyCode ? event.keyCode : event.which);
if (keycode == '13') {
doSearch();
return false;
}
});
function doSearch() {
document.location.href = "/search/?q=" + escape($('#search-field').val());
}
});
</script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
</body>
</html>
We also wired up the search field so that when we later create our search page it will post there.
In code behind we'll just tell TopMenu to list pages that's are directly under the site root and also if the current page is the start page add mark that through the use of a CSS class.
/Templates/MasterPages/Demo.Master.cs
namespace DemoSite.Templates.MasterPages {
using KalikoCMS.Configuration;
using KalikoCMS.WebForms.Framework;
public partial class Demo : PageMaster {
protected override void OnLoad(System.EventArgs e) {
base.OnLoad(e);
TopMenu.PageLink = SiteSettings.RootPage;
if (CurrentPage.PageId == SiteSettings.Instance.StartPageId) {
Container.CssClass = "startpage";
}
}
}
}
Creating the start page template
Create a new WebForm in the Pages folder and call it StartPage.aspx. Change it so that inherits from PageTemplate<StartPageType>
. All page templates should inherit from PageTemplate<T>
where T
is the page type. By doing that we get a strongly typed CurrentPage object. Notice that you can see all properties defined in our StartPageType class by accessing CurrentPage.
When we access a defined page property, such as CurrentPage.MainFeature we can access each part of that property type, like CurrentPage.MainFeature.Header. Simpler types, like the StringProperty
only have a single value property that happens to be named Value. If you in front-end code do something like <%=CurrentPage.MainFeature%>, what you actually do is calling ToString(). Depending on property type the displayed value may vary.
Our Slides property that is of a CollectionProperty<FeatureProperty>
type will expose an List<T>
called Items (where T
is - in our case - FeatureProperty
). This can be itterated over in order to render all our slides.
All properties are instantiated as empty and then filled, so you never have to worry that they might be null, not even for later added properties to already existing pages.
For the latest news list we will use a PageList
control.
/Templates/Pages/StartPage.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="StartPage.aspx.cs" Inherits="DemoSite.Templates.Pages.StartPage" MasterPageFile="../MasterPages/Demo.Master" %>
<asp:Content ContentPlaceHolderID="HeadlineContent" runat="server">
<div id="carousel-jumbotron" class="carousel slide" data-ride="carousel">
<div class="container">
<div class="carousel-inner" role="listbox">
<%
var count = 0;
foreach (var slide in CurrentPage.Slides.Items) { %>
<div class="item <%=count==0 ? "active" : "" %>">
<div class="jumbotron">
<div class="container">
<h1><%=slide.Header %></h1>
<p><%=slide.Description %></p>
<a href="<%=slide.Url %>" class="btn btn-primary btn-lg">Learn more »</a>
</div>
</div>
</div>
<%
count++;
} %>
</div>
<ol class="carousel-indicators">
<% for (var i = 0; i < count; i++) { %>
<li data-target="#carousel-jumbotron" data-slide-to="<%=i %>" class="<%=i == 0 ? "active" : "" %>"></li>
<% } %>
</ol>
</div>
<a class="left carousel-control" href="#carousel-jumbotron" role="button" data-slide="prev">
<span class="glyphicon glyphicon-chevron-left"></span>
<span class="sr-only">Previous</span>
</a>
<a class="right carousel-control" href="#carousel-jumbotron" role="button" data-slide="next">
<span class="glyphicon glyphicon-chevron-right"></span>
<span class="sr-only">Next</span>
</a>
</div>
</asp:Content>
<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">
<div class="row add-space flex-boxes">
<div class="col-lg-4">
<h2><%=CurrentPage.MainFeature.Header %></h2>
<p><%=CurrentPage.MainFeature.Description %></p>
<a href="<%=CurrentPage.MainFeature.Url %>" class="btn btn-primary">Learn more »</a>
</div>
<div class="col-lg-4">
<h2><%=CurrentPage.SecondaryFeature.Header %></h2>
<p><%=CurrentPage.SecondaryFeature.Description %></p>
<a href="<%=CurrentPage.SecondaryFeature.Url %>" class="btn btn-primary">Learn more »</a>
</div>
<div class="col-lg-4">
<h2>Latest news:</h2>
<cms:PageList ID="NewsList" AutoBind="True" PageSize="5" runat="server">
<HeaderTemplate>
<ul class="list-unstyled">
</HeaderTemplate>
<ItemTemplate>
<li><%#Container.CurrentPage.StartPublish.Value.ToShortDateString() %> <a href="<%#Container.CurrentPage.PageUrl %>"><%#Container.CurrentPage.PageName %></a></li>
</ItemTemplate>
<FooterTemplate>
</ul>
</FooterTemplate>
</cms:PageList>
<a href="/news/" class="btn btn-primary">More news »</a>
</div>
</div>
</asp:Content>
In the code-behind we populate the list with the latest news we can find.
/Templates/Pages/StartPage.aspx.cs
namespace DemoSite.Templates.Pages {
using KalikoCMS;
using KalikoCMS.Configuration;
using KalikoCMS.Core;
using KalikoCMS.Core.Collections;
using KalikoCMS.WebForms.Framework;
using Models;
using System;
public partial class StartPage : PageTemplate<StartPageType> {
protected void Page_Load(object sender, EventArgs e) {
PopulateNewsList();
}
private void PopulateNewsList() {
var pageType = PageType.GetPageType(typeof (NewsPageType));
NewsList.Filter = page => page.PageTypeId == pageType.PageTypeId;
var pageCollection = PageFactory.GetPageTreeFromPage(SiteSettings.RootPage, PublishState.Published);
pageCollection.Sort(SortOrder.StartPublishDate, SortDirection.Descending);
NewsList.DataSource = pageCollection;
}
}
}
Creating a reusable breadcrumbs control
User controls are a pretty neat solution in Web Forms for reusing code between different templates. We will try it out by adding a new Web Forms User Control under the Units folder in our project, calling it Breadcrumbs.ascx. We change so that the class inherits from KalikoCMS.WebForms.Framework.WebControlBase. By doing that we get a reference to CurrentPage even in our user control.
The system comes with a web control called Breadcrumbs
which does just what we need, so let's add it.
/Templates/Units/Breadcrumbs.ascx
<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="Breadcrumbs.ascx.cs" Inherits="DemoSite.Templates.Units.Breadcrumbs" %>
<cms:Breadcrumbs ID="Breadcrumbs1" AutoBind="True" RenderCurrentPage="false" runat="server">
<HeaderTemplate>
<ol class="breadcrumb">
</HeaderTemplate>
<ItemTemplate>
<li><a href="<%#Container.CurrentPage.PageUrl %>"><%#Container.CurrentPage.PageName %></a></li>
</ItemTemplate>
<FooterTemplate>
</ol>
</FooterTemplate>
</cms:Breadcrumbs>
As we AutoBind this control we don't need anything in the code-behind.
/Templates/Units/Breadcrumbs.ascx.cs
namespace DemoSite.Templates.Units {
using KalikoCMS.WebForms.Framework;
public partial class Breadcrumbs : WebControlBase {
}
}
We now have a breadcrumbs control that we can reuse on any page we want.
Creating the news list page template
We continue by creating the news list page template (/Templates/Pages/NewsListPage.aspx). It will contain three controls. One PageList
that will list all news pages that we found under the current page - regardless of level. One MenuList
that will list all news list pages we found under the current page. This allows us to create a news archive that we then put a news list page for each year under and get a pretty structured news section. And last one Pager
in order to page through large sets of news.
We will also add the breadcrumbs control we just created so that we can get from one level to another
/Templates/Pages/NewsListPage.aspx
<%@ Page Title="" Language="C#" MasterPageFile="~/Templates/MasterPages/Demo.Master" AutoEventWireup="true" CodeBehind="NewsListPage.aspx.cs" Inherits="DemoSite.Templates.Pages.NewsListPage" %>
<%@ Register TagPrefix="site" tagName="Breadcrumbs" src="../Units/Breadcrumbs.ascx" %>
<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">
<site:Breadcrumbs ID="Breadcrumbs1" runat="server" />
<h1><%=CurrentPage.PageName %></h1>
<div class="row">
<div class="col-lg-9">
<cms:PageList ID="NewsList" AutoBind="True" PageSize="10" runat="server">
<HeaderTemplate>
<ul class="list-unstyled">
</HeaderTemplate>
<ItemTemplate>
<li>
<h2><a href="<%#Container.CurrentPage.PageUrl %>"><%#Container.CurrentPage.PageName %></a></h2>
<p>
(<%#Container.CurrentPage.StartPublish.Value.ToShortDateString() %>)
<%#Container.CurrentPage.Property["Preamble"] %>
<a href="<%#Container.CurrentPage.PageUrl%>">Read more</a>
</p>
</li>
</ItemTemplate>
<FooterTemplate>
</ul>
</FooterTemplate>
</cms:PageList>
<cms:Pager ID="NewsPager" AutoBind="True" runat="server" />
</div>
<div class="col-lg-3">
<cms:MenuList ID="YearList" AutoBind="True" SortOrder="PageName" runat="server">
<HeaderTemplate>
<div class="list-group">
<span class="list-group-item active">Archive</span>
</HeaderTemplate>
<ItemTemplate>
<a href="<%#Container.CurrentPage.PageUrl %>" class="list-group-item"><%#Container.CurrentPage.PageName %></a>
</ItemTemplate>
<FooterTemplate>
</div>
</FooterTemplate>
</cms:MenuList>
</div>
</div>
</asp:Content>
In our code-behind we set up both lists and the pager.
/Templates/Pages/NewsListPage.aspx.cs
namespace DemoSite.Templates.Pages {
using System;
using KalikoCMS;
using KalikoCMS.Core;
using KalikoCMS.Core.Collections;
using KalikoCMS.WebForms.Framework;
using Models;
public partial class NewsListPage : PageTemplate<NewsListPageType> {
protected override void OnLoad(EventArgs e) {
base.OnLoad(e);
PopulateNewsList();
NewsPager.TargetControl = NewsList;
YearList.PageLink = CurrentPage.RootId;
}
private void PopulateNewsList() {
var pageType = PageType.GetPageType(typeof (NewsPageType));
NewsList.Filter = page => page.PageTypeId == pageType.PageTypeId;
var pageCollection = PageFactory.GetPageTreeFromPage(CurrentPage.PageId, PublishState.Published);
pageCollection.Sort(SortOrder.StartPublishDate, SortDirection.Descending);
NewsList.DataSource = pageCollection;
}
}
}
A bit of a side note; The list will apply the defined page type predicate filter when rendering. But you could also get the page collection pre-filtered (which you can pass to NewsList.DataSource) by using the following call:
PageFactory.GetPageTreeFromPage(CurrentPage.PageId, p => p.IsAvailable && p.PageTypeId == pageType.PageTypeId);
p.IsAvailable ensures that the page is published, without it you will get unpublished pages as well
Creating the article page template
The template for the article (/Templates/Pages/ArticlePage.aspx) is pretty simple. We render all our properties and add an additional menu to the left with a menu tree. That means that we can build a structure of articles and navigate between the levels.
The MenuTree
component is basically a MenuList
but with the exception that it can add levels through the NewLevelTemplate
and EndLevelTemplate
. This means that we easily can build a multi-level UL/LI-list.
Notice that we use ToHtml() on our image (TopImage). That command will render an proper IMG-tag if we have selected an image otherwise nothing. We could also built it ourself by accessing its properties such as TopImage.ImageUrl, TopImage.Width etc. If you crop and resize the image due to space restrains you can link to the original image by accessing the TopImage.OriginalImageUrl property.
/Templates/Pages/ArticlePage.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="ArticlePage.aspx.cs" Inherits="DemoSite.Templates.Pages.ArticlePage" MasterPageFile="../MasterPages/Demo.Master" %>
<%@ Register TagPrefix="site" tagName="Breadcrumbs" src="../Units/Breadcrumbs.ascx" %>
<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">
<site:Breadcrumbs runat="server" />
<div class="row">
<div class="left-menu col-lg-3">
<cms:MenuTree ID="LeftMenu" AutoBind="True" runat="server">
<StartItemTemplate><li></StartItemTemplate>
<EndItemTemplate></li></EndItemTemplate>
<NewLevelTemplate><ul class="nav nav-pills nav-stacked"></NewLevelTemplate>
<EndLevelTemplate></ul></EndLevelTemplate>
<ItemTemplate>
<a href="<%#Container.CurrentPage.PageUrl %>"><%#Container.CurrentPage.PageName %></a>
</ItemTemplate>
<SelectedItemTemplate>
<a href="<%#Container.CurrentPage.PageUrl %>" class="active"><%#Container.CurrentPage.PageName %></a>
</SelectedItemTemplate>
</cms:MenuTree>
</div>
<div class="col-lg-9">
<%=CurrentPage.TopImage.ToHtml() %>
<h1><%=CurrentPage.Headline %></h1>
<p class="preamble"><%=CurrentPage.Preamble %></p>
<%=CurrentPage.MainBody %>
<% if (CurrentPage.Tags.Tags.Count > 0) { %>
<p class="tags">
This article was tagged with: <strong><%=CurrentPage.Tags %></strong>
</p>
<% } %>
</div>
</div>
</asp:Content>
The code-behind is also pretty simple, we just set the PageLink for our menu (in other words telling it from where we want to start listing our menu tree). The RootId is always the first page in a branch directly under the site root. If we create a page right under the root and call it A and then create another page, called B, under A both of the pages will reference A as their root.
/Templates/Pages/ArticlePage.aspx.cs
namespace DemoSite.Templates.Pages {
using KalikoCMS.WebForms.Framework;
using DemoSite.Models;
using System;
public partial class ArticlePage : PageTemplate<ArticlePageType> {
protected void Page_Load(object sender, EventArgs e) {
LeftMenu.PageLink = CurrentPage.RootId;
}
}
}
Creating the news page template
The thing that sets the news page template apart is the list of related news. It might not be the best practise, but we'll build the HTML for that list in code-behind and populate our Literal
control called RelatedPost with it. We create our Web Form (Templates.Pages.NewsPage) and let it inherit from the PageTemplate<T>
of our page type as usual.
/Templates/Pages/NewsPage.aspx
<%@ Page Title="" Language="C#" MasterPageFile="~/Templates/MasterPages/Demo.Master" AutoEventWireup="true" CodeBehind="NewsPage.aspx.cs" Inherits="DemoSite.Templates.Pages.NewsPage" %>
<%@ Register TagPrefix="site" TagName="Breadcrumbs" Src="../Units/Breadcrumbs.ascx" %>
<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">
<site:Breadcrumbs ID="Breadcrumbs1" runat="server" />
<div class="row">
<div class="col-lg-9">
<h1><%=CurrentPage.Headline %></h1>
<p class="preamble"><%=CurrentPage.Preamble %></p>
<%=CurrentPage.MainBody %>
</div>
<div class="col-lg-3">
<h2>Related news</h2>
<asp:Literal runat="server" ID="RelatedPosts" />
</div>
</div>
</asp:Content>
All the magic happens in the code-behind with the call to FindSimular(CmsPage page, int resultOffset = 0, int resultSize = 10, bool matchCategory = true)
. As you can see most of the parameters have default values. By default it will return the top 10 closest matches for the same category. Category is something we set when the page is indexed to keep different page types a part (or group) during search. As we want only the simular news pages we leave matchCategory
to its default true. But we only want the first five hits therefore we set the offset to 0 and the size to 5. We then itterate over our result set and build the HTML-list.
/Templates/Pages/NewsPage.aspx.cs
namespace DemoSite.Templates.Pages {
using System;
using System.Text;
using KalikoCMS.Search;
using KalikoCMS.WebForms.Framework;
using Models;
public partial class NewsPage : PageTemplate<NewsPageType> {
protected override void OnLoad(EventArgs e) {
base.OnLoad(e);
RelatedPosts.Text = RenderRelatedPosts();
}
private string RenderRelatedPosts() {
var searchResult = SearchManager.Instance.FindSimular(CurrentPage, 0, 5);
var stringBuilder = new StringBuilder();
stringBuilder.Append("<ul class=\"list-unstyled related\">");
foreach (var searchHit in searchResult.Hits) {
stringBuilder.AppendFormat("<li><a href=\"{0}\">{1}</a></li>", searchHit.Path, searchHit.Title);
}
stringBuilder.Append("</ul>");
return stringBuilder.ToString();
}
}
}
You find more about the search engine at the project site.
Creating the search page template
This template (/Templates/Pages/SearchPage.aspx) is the one that we'll post our search form to. It expect to get a querystring parameter called q with the search terms and an optional p for the current pager value.
The frontend mostly consists of a search field and a little bit of JavaScript logic related to it's posting.
We also add a Literal where the result will be shown once a search is executed.
/Templates/Pages/SearchPage.aspx
<%@ Page Title="" Language="C#" MasterPageFile="~/Templates/MasterPages/Demo.Master" AutoEventWireup="true" CodeBehind="SearchPage.aspx.cs" Inherits="DemoSite.Templates.Pages.SearchPage" %>
<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">
<div class="row">
<div class="col-lg-6 col-lg-push-3">
<div id="searchfield" class="input-group">
<asp:TextBox ID="Query" CssClass="form-control" runat="server" />
<span class="input-group-btn">
<button id="searchButton" class="btn btn-primary" type="button"><i class="glyphicon glyphicon-search"></i> Search</button>
</span>
</div>
</div>
</div>
<div id="searchresults">
<asp:Literal ID="Result" runat="server" />
</div>
<script>
$(document).ready(function () {
$("#searchButton").click(doSearch);
$('#<%=Query.ClientID %>').keypress(function (event) {
var keycode = (event.keyCode ? event.keyCode : event.which);
if (keycode == '13') {
doSearch();
return false;
}
});
function doSearch() {
var query = $("#<%=Query.ClientID %>").val();
var url = document.location.pathname + "?q=" + escape(query);
document.location = url;
};
});
</script>
</asp:Content>
Once again, the magic happens in the code-behind where we build a search query with the terms we get from q and tells the search engine that we want the additional fields "category" and "summary". We then go ahead be calling SearchManager.Instance.Search(searchQuery)
. It will return a result set from which we will build the HTML to display to the user.
Since we specified that we wanted the additional meta data fields "category" and "summary" we can access them through MetaData[field name]
.
We will also from the number of hits render a pager so that we can page through larger result sets.
/Templates/Pages/SearchPage.aspx.cs
namespace DemoSite.Templates.Pages {
using System;
using System.Text;
using KalikoCMS.WebForms.Framework;
using KalikoCMS.Search;
using Models;
public partial class SearchPage : PageTemplate<SearchPageType> {
private const int PageSize = 5;
protected void Page_Load(object sender, EventArgs e) {
var query = Request.QueryString["q"];
int page;
int.TryParse(Request.QueryString["p"], out page);
Query.Text = query;
if (!string.IsNullOrEmpty(query)) {
PerformSearch(query, page);
}
}
private void PerformSearch(string searchString, int page) {
var searchQuery = new SearchQuery(searchString) {
MetaData = new[] {"category", "summary"},
NumberOfHitsToReturn = PageSize,
ReturnFromPosition = PageSize*page
};
var result = SearchManager.Instance.Search(searchQuery);
var stringBuilder = new StringBuilder();
if (result.NumberOfHits > 0) {
stringBuilder.AppendFormat("<p>{0} hits ({1} seconds)</p>", result.NumberOfHits, decimal.Round((decimal)result.SecondsTaken, 3));
RenderResultList(result, stringBuilder);
RenderPager(searchString, page, result, stringBuilder);
}
else {
stringBuilder.Append("<p><i>No pages were found matching the search criteria.</i></p>");
}
Result.Text = stringBuilder.ToString();
}
private void RenderPager(string searchString, int page, SearchResult result, StringBuilder stringBuilder) {
var numberOfPages = (int)Math.Ceiling((double)result.NumberOfHits/PageSize);
stringBuilder.Append("<ul class=\"pagination\">");
for (var i = 0; i < numberOfPages; i++) {
var url = Request.Path + "?q=" + Server.UrlEncode(searchString) + "&p=" + i;
stringBuilder.AppendFormat("<li {0}><a href=\"{1}\">{2}</a></li>", (i == page ? "class=\"active\"" : ""), url, (i + 1));
}
stringBuilder.Append("</ul>");
}
private static void RenderResultList(SearchResult result, StringBuilder stringBuilder) {
foreach (var hit in result.Hits) {
stringBuilder.AppendFormat("<p><a href=\"{0}\">{1}</a><br/>", hit.Path, hit.Title);
stringBuilder.AppendFormat("<span class=\"url\">{0}</span><!-- [{1}]--><br/>", hit.Path, hit.Score);
var summary = hit.Excerpt;
if (string.IsNullOrEmpty(summary) && hit.MetaData.ContainsKey("summary")) {
summary = hit.MetaData["summary"];
}
if (!string.IsNullOrEmpty(summary)) {
stringBuilder.AppendFormat("{0}<br/>", summary);
}
stringBuilder.AppendFormat("<span class=\"label label-warning\">{0}</span></p>", hit.MetaData["category"]);
}
}
}
}
We're getting close to finishing up our templates, only the ones for products remain.
Creating the product list template
We display the headline and main body from our page, but the repeater will display a list of products that we'll get from a faked data source (to simulate an external system). We've created a product class that will carry all our product data and a data source that will return a simulated list of products. You find both classes here.
/Templates/Pages/ProductListPage.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="ProductListPage.aspx.cs" Inherits="DemoSite.Templates.Pages.ProductListPage" MasterPageFile="../MasterPages/Demo.Master" %>
<%@ Import Namespace="DemoSite.FakeStore" %>
<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">
<div class="row">
<div class="col-lg-9">
<h1><%=CurrentPage.Headline %></h1>
<%=CurrentPage.MainBody %>
<asp:Repeater runat="server" ID="ProductList">
<HeaderTemplate>
<ul class="list-unstyled products">
</HeaderTemplate>
<ItemTemplate>
<li>
<h2><a href="<%#string.Format("{0}{1}/", CurrentPage.PageUrl, ((Product)Container.DataItem).Id) %>"><%#((Product)Container.DataItem).Name %></a></h2>
<p><%#((Product)Container.DataItem).Description %></p>
</li>
</ItemTemplate>
<FooterTemplate>
</ul>
</FooterTemplate>
</asp:Repeater>
</div>
</div>
</asp:Content>
In our code-behind we'll just get a listed of products from our faked external product database. In a real world scenario you might want to consider adding a layer of caching.
/Templates/Pages/ProductListPage.aspx.cs
namespace DemoSite.Templates.Pages {
using KalikoCMS.WebForms.Framework;
using Models;
using FakeStore;
public partial class ProductListPage : PageTemplate<ProductListType> {
protected override void OnLoad(System.EventArgs e) {
base.OnLoad(e);
ProductList.DataSource = FakeProductDatabase.GetProducts();
ProductList.DataBind();
}
}
}
Creating the product detail template
You might remember from when we created our ProductListPageType
that we in the extender made a redirect to a product detail page. A page that isn't an actual CMS page but instead is built with data from the external source.
For this we create yet another Web Form, this time called ProductPage.aspx (to match the path we entered in our page extender). Although this is not a CMS page we will still change so that inherit from PageTemplate
. Notice that we don't use the typed version this time but the generic PageTemplate
. This is actually only to show it's existance and to show what differs. We will get a CmsPage
object called CurrentPage, but unlike the templates above it will not contain any strongly typed definitions. We'll get all the common ones like PageName
, but for properties we would have to go through the Property
collection like: CurrentPage.Property["MyPropertyName"].
This could be usefull if you plan to share the same template between different page types as the page only expects any CmsPage
and not a specific type. But in most cases you should inherit from PageTemplate<T>
since that will give you a nice strongly typed object instead. So only use this generic version if you really need it!
I mentioned that this template isn't for a regular CMS page and yet we inherit from PageTemplate
!? This is because we pass along the id to the page that is the page extender, in our case our products list page. That way we can access it through CurrentPage and be able to use its properties for things like parameters on how we should render our page.
This also only works since we passes the page id to our template, if we didn't we couldn't use the PageTemplate
class since it requires the id of the requested page.
/Templates/Pages/ProductPage.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="ProductPage.aspx.cs" Inherits="DemoSite.Templates.Pages.ProductPage" MasterPageFile="../MasterPages/Demo.Master" %>
<%@ Register TagPrefix="site" tagName="Breadcrumbs" src="../Units/Breadcrumbs.ascx" %>
<%@ Import Namespace="DemoSite.FakeStore" %>
<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">
<div class="row">
<div class="left-menu col-lg-2">
<asp:Repeater runat="server" ID="ProductList">
<HeaderTemplate>
<div class="list-group">
<span class="list-group-item active">Products</span>
</HeaderTemplate>
<ItemTemplate>
<a href="<%#string.Format("{0}{1}/", CurrentPage.PageUrl, ((Product)Container.DataItem).Id) %>" class="list-group-item"><%#((Product)Container.DataItem).Name %></a>
</ItemTemplate>
<FooterTemplate>
</div>
</FooterTemplate>
</asp:Repeater>
</div>
<div class="col-lg-8">
<h1><asp:Literal runat="server" ID="Heading" /></h1>
<p class="preamble"><asp:Literal runat="server" ID="Description" /></p>
<p>
This is no ordinary page. Although it has it's own URL all this information is kept in another system. This page is generated from a fake product database
using the <code>IPageExtender</code> functionality. This is a great way to present information without the need to store them in two places.
</p>
<p>
We can always access the ancestor page (the one implementing the extender) by using <code>CurrentPage</code>. In this case our ancestor is <b><%=CurrentPage.PageName %></b>.
</p>
<p>
To learn more about how to extend your pages with content from other systems <a href="http://kaliko.com/cms/get-started/page-extenders/">learn about page extenders here</a>.
</p>
</div>
</div>
</asp:Content>
The code-behind doesn't actually do anything related to our CMS, it just gets the product for detailed display and also gets a complete list of products to feeds into a Repeater
.
/Templates/Pages/ProductPage.aspx.cs
namespace DemoSite.Templates.Pages {
using KalikoCMS.WebForms.Framework;
using FakeStore;
using System;
public partial class ProductPage : PageTemplate {
protected void Page_Load(object sender, EventArgs e) {
var productId = Request.QueryString["productid"];
var product = FakeProductDatabase.GetProduct(productId);
Heading.Text = product.Name;
Description.Text = product.Description;
ProductList.DataSource = FakeProductDatabase.GetProducts();
ProductList.DataBind();
}
}
}
That's it! We've now created all our templates and you should have a working web site. Still there is something missing. We haven't yet created any content! So lets do that.
Creating content
Your project should compile without any problem now. If it doesn't please refer to the project over at GitHub in order to work out what's missing.
Run your new web project and navigate over to the /Admin/ folder, it should ask for your login credentials (if not already entered). Once logged in you come directly to the editor and the site root. If you previously been working in the the admin you will be redirected to the page that you last worked on.
To the left you have the main menu, here you'll have Pages for content editing, Search engine for managing search (currently only offering to reindex the whole site which is a useful feature if you add searh functionality to already existing pages) and Manage users (if you did install KalikoCMS.Identity).
When you select Pages (which is the default mode when entering admin) you will get a page tree next to the main menu that shows your complete site tree. Above the tree you have to buttons; one to add pages and one to delete then.
A quick note about deleting pages. They are not actually deleted completly. What happen when you select a page and push that button is that the page and all its descendents gets a delete date set. They still remain in the database but they won't be read from there anymore when the site build its tree. If you accidently deleted a page you can restore it by going into the database and set the delete date to null. This will get a built in interface in the future, but currently other parts have been prioritized.
To the right from the page tree is the currently selected page itself. Here you can change all its properties, both the common ones like page name and publishing dates but also the ones defined in the page type.
If any of the properties you defined don't show up here you should start by checking so that it has the PropertyAttribute
(or any of the specialized attributes like ImagePropertyAttribute
) sat and that the property itself is defined as virtual
.
If you don't set any start publish date the page is considered to not be published and won't be showing up on your web site. So be sure to always click todays date (which is equal to now) in order to directly publish pages.
Creating a start page
You might have noticed an error message when you started the project, before you got to the /Admin/ folder it said that no start page had been defined. And that's because we haven't yet created it, so lets do that.
Click the root in site tree and then hit the Add page button. This will bring up a dialogue where you can select from all of your page types. Select the start page type. You now end up on a new page of the start page type. The page itself will not be created until you hit the save button.
The page name is what gives your page its path. If you enter a page name that is "My start page" the URL to your page will become my-start-page. If you want another URL segment you can manually set this under advanced options.
Click on the calendar icon for the start publish date and select today. Add some information to the feature properties and add few slides. You can add as many slides you want and you can also rearrange them through drag and drop.
When you have entered the information you want on your start page it's time to hit the Save page button located at the bottom of the screen.
Once the start page has been saved scroll down to the bottom of the page, below the properties you should see the page id in form of a Guid. Copy it and open the web.config file in your project. Locate the element siteSettings and the replace the attribute startPageId with the id that you copied from your new page.
Your siteSettings should look something like this (except with your page Guid):
<siteSettings adminPath="/Admin/" datastoreProvider="KalikoCMS.Data.StandardDataStore, KalikoCMS.Engine" startPageId="0db38ff3-20f3-4228-8b0b-da6e0bb84636" searchProvider="KalikoCMS.Search.KalikoSearchProvider, KalikoCMS.Search" />
Save your web.config and navigate to your projects root in the web browser. This time your start page should appear! (If it doesn't, check that you have set a start publish day that has passed).
Moving pages
The site tree supports drag and drop, so if you need to move a page you can simple drag it to its new parent.
This means that the URL to the page is changed (since it's built by the hierarchy that we just changed), but that's not a problem. There's a fallback in the system that will store the old URL and in case a request is made and no page can be found for the URL it will check with all previous paths to see if it belongs to a page that was moved and then forward the request to the correct URL.
Building the rest of the content
To get a nice multi-level news archive you can create a news list under the root and call it News. Then add a news list under it and call it 2014 where you later publish news (and additional news lists for 2015 and so on).
When you create your search page, make sure that your search page's URL matches the one that is entered into the search form which should post to that page. The code sample above assumes that you name your search page to Search, if you name it anything else, make sure you do the proper changes in URL references.
If any of your content doesn't show up in menus make sure that you have enabled the Show in menus flag on the page that doesn't show up.
This is how the pages are structured in the downloadable demo project:
Next step from here
That was a whole lot of information in one go. So thanks for sticking with me this far Hopefully it has shown you what Kaliko CMS can be used for and that it's really easy to work with. Please visit the projects web site for more information on how to get started developing, I will try to add information to it continuously. If you feel that I missed something or that I should go into something more detailed, please post a request over at the developer forum.
Although this article was centered around Web Forms the CMS works well with ASP.NET MVC 5+. If you rather use MVC there's an article on how to develop controls and views for your pages.
I thank you very much for your time and hope to hear from you! And if you use and like this project, please spread the word, thank you!
I'll now continue with a section about how this project came to be.
Background or "Why yet another CMS?"
With todays market consisting of hundreds if not tousends of Content Management Systems, why add yet another one?
A good and solid question. For me it all started back in 2004 when I needed a project in order to learn ASP.NET. And what other project would give such a wide variaty of knowledge - ranging from simple page rendering to more complex issues such as user management and authentication than a CMS?
At the time there were a lot less CMS:s around, many of them costing a smaller fortune. Over time the complexity of the system grew and it suddenly was something that - with a bit of polish - could become a competent product for others to use as well. I decided to put a little bit more work into the project and release it under an open source license, and here it is.
Hopefully you'll find this system usefull and maybe even help to form its future.
Overall design decitions
My goal with the structure of the system is to create something that gives a lot of help but in the same time doesn't trespass too much on the developers domain. There are no new cryptic script language or fixed page layouts to learn. You write the code as you are used to, using WebForms (standard pages, master pages and user controls) or ASP.NET MVC (model, controllers and views).
The same philosophy goes for the choise of database provider. I wanted to create a CMS that could be hosted even on a budget host, thus providing alternatives for which database provider that is available. Whether you want to go for Microsoft SQL Server, MySQL or SQLite (or other supported database) the choice is yours!
This idea to leave the choise of what sub system to use to the developer is also reflected in the way search engine integration and object storage is implemented. Both uses a provider based model which allow for almost unlimited integration possibilities.
The administration interface uses Bootstrap and the current release contains a pretty basic theme. The aim is to let the design be pretty brandable so that you can apply a visual recognition towards the end users of the system. Whether it's the customers logo or your firms trademark towards your customers.
When it comes to the design and layout of the web pages, it's all up to you. Whether you want to create a simple article template or a complex list page you can do it! Although I plan to provide starter packs later on the basic system does not contain any code for the actual web site. What you get is a dynamic administration interface together with a wide palette of powerfull tools to help you implement your web site.
In short: My goal is to create a CMS that is both developer and editor friendly. I hope you'll find it useful!
And if you do I also hope you spread the word. Thanks!
History
2014-11-25 First version
2016-01-23 Second version
Updated to reflect the latest version of Kaliko CMS, utilizing features added since the original article such as the composite property types.