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.
If you've read my previous article Build a website with Kaliko CMS using WebForms you'll find that much of the content in this is the same but that the implementation is towards ASP.NET MVC.
Besides installing the framework and implement some basic things we will also touch on 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 on the latter. If you're interested in develop using WebForms I suggest reading Build a website with Kaliko CMS using WebForms.
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 controller that will control how the page is rendered. The page controller is a pretty normal controller with a standard action that recieves the requested page 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 MVC 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 MVC we select KalikoCMS.Mvc. (If you develop a WebForms project this is where you would select the WebForms provider instead.)
PM> Install-Package Install-Package KalikoCMS.Mvc
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, 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 controller and view. I will be using a layout view 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 |
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. |
MarkdownProperty |
Used for Markdown content (as an alternative to HtmlProperty) |
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
<span id="ArticleContent">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; }
}
}
}</span>
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.
Sometimes a page type might not contain one single property definition, it might just be needed to call an action in a controller that contains all the information needed. 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 and a display name. 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. The attribute also has a few optional parameters; a description, a preview image, a type array to limit which pages that can be created under a page of that type and settings for how children to the page should be sorted by default.
(In case you use WebForms the attribute also needs a template set, but the MVC provider wires this up through code.)
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 controllers and views for our page types. Create a new class in the Models\Pages folder of the project and name it SearchPage.
As no pages should be created under the search page we'll set the AllowedType
property in the attribute to an empty array. AllowedType
is used to declare which types can be created under a particular page typ. If omitted all page types will be available.
/Models/Pages/SearchPage.cs
namespace DemoSite.Models.Pages {
using KalikoCMS.Attributes;
using KalikoCMS.Core;
[PageType("SearchPage", "Search page", PageTypeDescription = "Used for search page", AllowedTypes = new Type[] {})]
public class SearchPage : 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\Pages. This one will just hold news posts, so it doesn't need any properties of its own.
We only want pages of the type NewsListPage (for building structures) and NewsPage under the news list, so we specify these in the AllowedTypes
property.
We want to show the child pages from latest to oldest, so we set DefaultChildSortOrder
to SortOrder.CreatedDate
and DefaultChildSortDirection
to SortDirection.Descending
. This can be manually changed in the page editor, but setting a default sort order ensures that it always will be applied to new pages of this type.
/Models/Pages/NewsListPage.cs
namespace DemoSite.Models.Pages {
using KalikoCMS.Attributes;
using KalikoCMS.Core;
[PageType("NewsList", "News list", PageTypeDescription = "Used for news archives",
DefaultChildSortOrder = SortOrder.CreatedDate,
DefaultChildSortDirection = SortDirection.Descending,
AllowedTypes = new[] { typeof(NewsListPage), typeof(NewsPage) })]
public class NewsListPage : 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 controller 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/Pages/StartPage.cs
namespace DemoSite.Models.Pages {
using KalikoCMS.Attributes;
using KalikoCMS.Core;
using KalikoCMS.PropertyType;
using PropertyTypes;
[PageType("StartPage", "Start page", PageTypeDescription = "Used for start page")]
public class StartPage : 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.
As no pages should be created under a news page we'll add an empty AllowedTypes
property in the attribute.
/Models/Pages/NewsPage.cs
namespace DemoSite.Models.Pages {
using KalikoCMS.Attributes;
using KalikoCMS.Core;
using KalikoCMS.PropertyType;
using KalikoCMS.Search;
[PageType("NewsPage", "News page", PageTypeDescription = "Used for news", AllowedTypes = new Type[] {})]
public class NewsPage : 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/Pages/ArticlePage.cs
namespace DemoSite.Models.Pages {
using KalikoCMS.Attributes;
using KalikoCMS.Core;
using KalikoCMS.PropertyType;
using KalikoCMS.Search;
[PageType("ArticlePage", "Article page", PageTypeDescription = "Used for articles")]
public class ArticlePage : 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 action in our page controller using RouteUtils.RedirectToController
and return a true, if not we return false.
In our case we will create an action in our page controller later on and call it Product which will - beside the current page - recieve a product id.
/Models/Pages/ProductList.cs
namespace DemoSite.Models.Pages {
using System;
using System.Web;
using KalikoCMS.Attributes;
using KalikoCMS.ContentProvider;
using KalikoCMS.Core;
using KalikoCMS.PropertyType;
using FakeStore;
[PageType("ProductList", "Product list page")]
public class ProductList : 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])) {
var page = PageFactory.GetPage(pageId);
var additionalRouteData = new Dictionary<string, object> {{"productId", remainingSegments[0]}};
RouteUtils.RedirectToController(page, "product", additionalRouteData);
return true;
}
return false;
}
}
}
That's it! We've completed all the page types. Now we just need to create the controllers and views.
Creating the controllers and views
We've created the classes that describes our page types, now we have to implement their visual respresentation by adding controllers and views.
The demo project should not be taken as a kind of best-practice, rather it is written in the way it is to be easier to follow.
Page controllers always gets a strongly typed version of the page as a parameter. In some cases this might be enough to pass directly to the view. But in order to separate logic and presentation as much as possible we're using view models in this demo.
Two things that we need from all of our view models is that they can contain the current page and the pages that should appear in the top menu. Therefore we create an interface that all page view models must use.
I will also separate the code that populates the view models from the controllers to separate builder classes.
Models/ViewModels/IPageViewModel.cs
namespace DemoSite.Models.ViewModels {
using System.Collections.Generic;
using KalikoCMS.Core;
public interface IPageViewModel<out T> where T : CmsPage {
T CurrentPage { get; }
IEnumerable<CmsPage> TopMenu { get; set; }
}
}
We'll create a basic implementation called PageViewModel that we can use as a generic page view model.
Models/ViewModels/PageViewModel.cs
namespace DemoSite.Models.ViewModels {
using System.Collections.Generic;
using KalikoCMS.Core;
public class PageViewModel<T> : IPageViewModel<T> where T : CmsPage {
public PageViewModel(T currentPage) {
CurrentPage = currentPage;
TopMenu = new List<CmsPage>();
}
public T CurrentPage { get; private set; }
public IEnumerable<CmsPage> TopMenu { get; set; }
}
}
And for that view model we create a model builder with a method called Create that takes a page as parameter. We add a method called SetBaseProperties that we can use from all the other page view models that we'll create to load standard information to the model.
Business/ViewModelBuilders/PageViewModelBuilder.cs
namespace DemoSite.Business.ViewModelBuilders {
using KalikoCMS;
using KalikoCMS.Configuration;
using KalikoCMS.Core;
using Models.ViewModels;
public class PageViewModelBuilder {
public static PageViewModel<T> Create<T>(T currentPage) where T : CmsPage {
var model = new PageViewModel<T>(currentPage);
SetBaseProperties(model);
return model;
}
public static void SetBaseProperties(IPageViewModel<CmsPage> model) {
model.TopMenu = PageFactory.GetChildrenForPage(SiteSettings.RootPage, x => x.VisibleInMenu);
}
}
}
Creating the layout
We'll start by creating the shared layout. Since we use a view model for all pages we can always expect that we get the current page and top menu.
We'll add a second section beside the body called HeadlineContent and also toggles class on the outer div depending on if we're on the start page or not.
We'll render the top menu based on the pages we get in the TopMenu property of our view model and toggle class to active if the menu item is the current page our one of it's ancestors.
Views/Shared/_Layout.cshtml
@using KalikoCMS.Configuration
@model DemoSite.Models.ViewModels.IPageViewModel<KalikoCMS.Core.CmsPage>
<!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>@Model.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>
<div class="@(Model.CurrentPage.PageId == SiteSettings.Instance.StartPageId ? "startpage" : "")">
<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>
-->
<ul class="nav navbar-nav navbar-right">
@foreach (var page in Model.TopMenu) {
if (Model.CurrentPage.PageId == page.PageId || Model.CurrentPage.ParentPath.Contains(page.PageId)) {
<li class="active"><a href="@page.PageUrl">@page.PageName</a></li>
}
else {
<li><a href="@page.PageUrl">@page.PageName</a></li>
}
}
</ul>
</div>
</nav>
@RenderSection("HeadlineContent", false)
</div>
<div class="container main-content">
@RenderBody()
<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>
<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 a real world scenario the URL to the search page should be dynamically loaded.)
Creating the start page controller and view
Create a new Controller in the Controllers folder and call it StartPageController.cs. Delete the default Index method and change it so that inherits from PageController<StartPage>
. All page controllers should inherit from PageController<T>
where T
is the page type. By doing that the controller is wired up to the routing and we get a strongly typed CurrentPage object passed to our default action (Index). Notice that you can see all properties defined in our StartPage 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 ToHtmlString()
. 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.
Controllers/StartPageController.cs
namespace DemoSite.Controllers {
using System.Web.Mvc;
using Business.ViewModelBuilders;
using KalikoCMS.Mvc.Framework;
using Models.Pages;
public class StartPageController : PageController<StartPage> {
public override ActionResult Index(StartPage currentPage) {
var model = StartPageViewModelBuilder.Create(currentPage);
return View(model);
}
}
}
As we also will need our new controller to pass a list of the latest news to our view we must create a new view model for the start page.
Models/ViewModels/StartPageViewModel.cs
namespace DemoSite.Models.ViewModels {
using System.Collections.Generic;
using KalikoCMS.Core;
using Pages;
public class StartPageViewModel : IPageViewModel<StartPage> {
public StartPageViewModel(StartPage currentPage)
{
CurrentPage = currentPage;
}
public StartPage CurrentPage { get; private set; }
public IEnumerable<CmsPage> TopMenu { get; set; }
public IEnumerable<CmsPage> LatestNews { get; set; }
}
}
We also implement a builder for our view model called StartPageViewModelBuilder. It will use SetBaseProperties to populate the model with the common properties and in addition fetch the latest news by searching for all news pages across the site.
Business/ViewModelBuilders/StartPageViewModelBuilder.cs
namespace DemoSite.Business.ViewModelBuilders {
using System.Collections.Generic;
using System.Linq;
using KalikoCMS;
using KalikoCMS.Core;
using KalikoCMS.Core.Collections;
using Models.Pages;
using Models.ViewModels;
public class StartPageViewModelBuilder {
public static StartPageViewModel Create(StartPage currentPage) {
var model = new StartPageViewModel(currentPage);
PageViewModelBuilder.SetBaseProperties(model);
model.LatestNews = GetLatestNews();
return model;
}
private static IEnumerable<CmsPage> GetLatestNews() {
var pageType = PageType.GetPageType(typeof(NewsPage));
var news = PageFactory.GetPages(pageType.PageTypeId);
news.Sort(SortOrder.StartPublishDate, SortDirection.Descending);
return news.Take(5);
}
}
}
Finally we'll create a view for our controller which uses our StartPageViewModel. We'll loop through the Slides property of our CurrentPage to create the carousel. We'll also write out our feature boxes as well as the latest news list.
Views/StartPage/Index.cshtml
@model DemoSite.Models.ViewModels.StartPageViewModel
@{
Layout = "~/Views/Shared/_Layout.cshtml";
}
@section HeadlineContent {
<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 Model.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>
}
<div class="row add-space flex-boxes">
<div class="col-lg-4">
<h2>@Model.CurrentPage.MainFeature.Header</h2>
<p>@Model.CurrentPage.MainFeature.Description</p>
<a href="@Model.CurrentPage.MainFeature.Url" class="btn btn-primary">Learn more »</a>
</div>
<div class="col-lg-4">
<h2>@Model.CurrentPage.SecondaryFeature.Header</h2>
<p>@Model.CurrentPage.SecondaryFeature.Description</p>
<a href="@Model.CurrentPage.SecondaryFeature.Url" class="btn btn-primary">Learn more »</a>
</div>
<div class="col-lg-4">
<h2>Latest news:</h2>
<ul class="list-unstyled">
@foreach (var newsPage in Model.LatestNews) {
<li>@newsPage.StartPublish.Value.ToShortDateString() <a href="@newsPage.PageUrl">@newsPage.PageName</a></li>
}
</ul>
-->
<a href="/news/" class="btn btn-primary">More news »</a>
</div>
</div>
Creating a bread crumbs navigation
We need a way to navigate back in the hierarchy on a couple of our page types, so we'll proceed with creating a bread crumbs menu, and since this is something we'll be reusing we'll make it a partial view. It will use a page view model and the ParentPath
method on the current page in order to render its parents.
Views/Partials/BreadCrumbsView.cshtml
@model DemoSite.Models.ViewModels.IPageViewModel<KalikoCMS.Core.CmsPage>
@if (Model.CurrentPage.ParentPath.Count == 0) {
// If no parents, don't render this
return;
}
<ol class="breadcrumb">
@foreach (var page in Model.CurrentPage.ParentPath.Reverse())
{
<li><a href="@page.PageUrl">@page.PageName</a></li>
}
</ol>
Creating the news list controller and view
We continue by creating the news list controller (/Controllers/NewsListPageController.cs). Since we want to have the news list pageable we need our action to be able to recieve information about what page of information to display. This can be done in different ways, but we will replace the default Index(NewsListPage currentPage) action with a new one that also accepts a page number. To tell the router that the default Index action shouldn't be used it's decorated with the NonAction
attribute.
Controllers/NewsListPageController.cs
namespace DemoSite.Controllers {
using System;
using System.Web.Mvc;
using Business.ViewModelBuilders;
using KalikoCMS.Mvc.Framework;
using Models.Pages;
public class NewsListPageController : PageController<NewsListPage> {
public ActionResult Index(NewsListPage currentPage, int page = 1) {
var model = NewsListPageViewModelBuilder.Create(currentPage, page);
return View(model);
}
[NonAction]
public override ActionResult Index(NewsListPage currentPage) {
throw new NotImplementedException();
}
}
}
In order to handle the paging we'll add the NuGet package PageList.Mvc to our project.
We create a new view model that besides the basic properties also has a paged news collection, a list of newsholders (useful to structure news) and the current page number.
Models/ViewModels/NewsListPageViewModel.cs
namespace DemoSite.Models.ViewModels {
using System.Collections.Generic;
using KalikoCMS.Core;
using PagedList;
using Pages;
public class NewsListPageViewModel : IPageViewModel<NewsListPage> {
public NewsListPageViewModel(NewsListPage currentPage) {
CurrentPage = currentPage;
}
public NewsListPage CurrentPage { get; private set; }
public IEnumerable<CmsPage> TopMenu { get; set; }
public IPagedList<NewsPage> News { get; set; }
public IEnumerable<NewsListPage> NewsHolders { get; set; }
public int Page { get; set; }
}
}
For this view model we create a model builder. It will fetch all news pages that are below the current page, sort them on start publish date and take the 5 latest news. It will also get all news lists below so that we can structure our news by year (with a news list representing a year).
We'll use PagedList<T>
instead of a normal List<T>
in order to page it.
Business/ViewModelBuilders/NewsListPageViewModelBuilder.cs
namespace DemoSite.Business.ViewModelBuilders {
using System.Collections.Generic;
using System.Linq;
using KalikoCMS;
using KalikoCMS.Core;
using KalikoCMS.Core.Collections;
using Models.Pages;
using Models.ViewModels;
using PagedList;
public class NewsListPageViewModelBuilder {
public const int PageSize = 10;
public static NewsListPageViewModel Create(NewsListPage currentPage, int page) {
var model= new NewsListPageViewModel(currentPage);
PageViewModelBuilder.SetBaseProperties(model);
model.News = new PagedList<NewsPage>(GetNews(currentPage), page, PageSize);
model.NewsHolders = GetNewsHolders(currentPage);
model.Page = page;
return model;
}
private static IEnumerable<NewsPage> GetNews(NewsListPage currentPage) {
var pageType = PageType.GetPageType(typeof(NewsPage));
var newsPages = PageFactory.GetPageTreeFromPage(currentPage.PageId, p => p.IsAvailable);
newsPages.Sort(SortOrder.StartPublishDate, SortDirection.Descending);
return newsPages.Where(x => x.PageTypeId == pageType.PageTypeId).Select(x => x.ConvertToTypedPage<NewsPage>());
}
private static IEnumerable<NewsListPage> GetNewsHolders(NewsListPage currentPage) {
var pageType = PageType.GetPageType(typeof(NewsListPage));
return PageFactory.GetChildrenForPageOfPageType(currentPage.RootId, pageType.PageTypeId).Select(x => x.ConvertToTypedPage<NewsListPage>());
}
}
}
p.IsAvailable ensures that the page is published, without it you will get unpublished pages as well.
We'll create a view for the news list that renders both the news and any additional news list that is a child to the current list. We'll also add the bread crumbs navigation.
Views/NewsListPage/Index.cshtml
@using DemoSite.Business.ViewModelBuilders
@using PagedList.Mvc
@model DemoSite.Models.ViewModels.NewsListPageViewModel
@{
Layout = "~/Views/Shared/_Layout.cshtml";
}
@Html.Partial("~/Views/Partials/BreadCrumbsView.cshtml", Model)
<h1>@Model.CurrentPage.PageName</h1>
<div class="row">
<div class="col-lg-9">
<ul class="list-unstyled">
@foreach (var newsPage in Model.News)
{
<li>
<h2><a href="@newsPage.PageUrl">@newsPage.PageName</a></h2>
<p>
(@newsPage.StartPublish.Value.ToShortDateString())
@newsPage.Preamble
<a href="@newsPage.PageUrl">Read more</a>
</p>
</li>
}
</ul>
@* Note: null controller reference will be fixed in version 1.0.1 *@
@Html.PagedListPager(Model.News, page => Url.Action(null, Model.CurrentPage.PageUrl.ToString().TrimStart('/'), new { page, pageSize = NewsListPageViewModelBuilder.PageSize }))
<p>Showing @Model.News.FirstItemOnPage to @Model.News.LastItemOnPage of @Model.News.TotalItemCount news</p>
</div>
<div class="col-lg-3">
<div class="list-group">
<span class="list-group-item active">Archive</span>
@foreach (var newsHolder in Model.NewsHolders)
{
<a href="@newsHolder.PageUrl" class="list-group-item">@newsHolder.PageName</a>
}
</div>
</div>
</div>
Creating the article page controller and view
The view for the article page type is pretty simple. We'll 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.
As the article page only needs to get the current page and top menu passed to the view we can use the generic PageViewModel we created earlier.
Controllers/ArticlePageController.cs
namespace DemoSite.Controllers {
using Business.ViewModelBuilders;
using KalikoCMS.Mvc.Framework;
using Models.Pages;
using System.Web.Mvc;
public class ArticlePageController : PageController<ArticlePage> {
public override ActionResult Index(ArticlePage currentPage) {
var model = PageViewModelBuilder.Create(currentPage);
return View(model);
}
}
}
We'll add a menu tree in our view that displays the complete tree from the root by creating a helper method that we can call recursively to render the tree.
Views/ArticlePage/Index.cshtml
@using KalikoCMS
@using KalikoCMS.Core
@model DemoSite.Models.ViewModels.PageViewModel<DemoSite.Models.Pages.ArticlePage>
@{
Layout = "~/Views/Shared/_Layout.cshtml";
}
@Html.Partial("~/Views/Partials/BreadCrumbsView.cshtml", Model)
<div class="row">
<div class="left-menu col-lg-3">
@RenderTree(PageFactory.GetPage(Model.CurrentPage.RootId))
</div>
<div class="col-lg-9">
@Model.CurrentPage.TopImage.ToHtml()
<h1>@Model.CurrentPage.Headline</h1>
<p class="preamble">@Model.CurrentPage.Preamble</p>
@Model.CurrentPage.MainBody
@if (Model.CurrentPage.Tags.Tags.Count > 0) {
<p class="tags">
This article was tagged with: <strong>@Model.CurrentPage.Tags</strong>
</p>
}
</div>
</div>
@* Recursive function that renders all pages in the current branch which is set to be visible in menus *@
@helper RenderTree(CmsPage page) {
<ul class="nav nav-pills nav-stacked">
@* Loop through all children *@
@foreach (CmsPage child in page.Children)
{
// Don't show pages that isn't visible in menus
if (!child.VisibleInMenu)
{
continue;
}
<li class="@(Model.CurrentPage.PageId == child.PageId ? "active" : "")">
<a href="@child.PageUrl">@child.PageName</a>
@if (child.HasChildren && (Model.CurrentPage.ParentPath.Contains(child.PageId) || Model.CurrentPage.PageId == child.PageId))
{
// Only expand selected node
@RenderTree(child)
}
</li>
}
</ul>
}
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.
Creating the news page controller and view
The thing that sets the news page apart is the list of related news. We create our controller and let it inherit from the PageController<T>
of our page type as usual.
Controllers/NewsPageController.cs
namespace DemoSite.Controllers {
using System.Web.Mvc;
using Business.ViewModelBuilders;
using KalikoCMS.Mvc.Framework;
using Models.Pages;
public class NewsPageController : PageController<NewsPage> {
public override ActionResult Index(NewsPage currentPage) {
var model = NewsPageViewModelBuilder.Create(currentPage);
return View(model);
}
}
}
We'll create a new view model for our page that also carries the related news.
Models/ViewModels/NewsPageViewModel.cs
namespace DemoSite.Models.ViewModels {
using System.Collections.Generic;
using KalikoCMS.Core;
using KalikoCMS.Search;
using Pages;
public class NewsPageViewModel : IPageViewModel<NewsPage> {
public NewsPageViewModel(NewsPage currentPage) {
CurrentPage = currentPage;
}
public NewsPage CurrentPage { get; private set; }
public IEnumerable<CmsPage> TopMenu { get; set; }
public SearchResult RelatedNews { get; set; }
}
}
We then create our model builder that besides setting the common properties also gets news related to the current page. All the magic happens in 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.
Business/ViewModelBuilders/NewsPageViewModelBuilder.cs
namespace DemoSite.Business.ViewModelBuilders {
using KalikoCMS.Search;
using Models.Pages;
using Models.ViewModels;
public class NewsPageViewModelBuilder {
public static NewsPageViewModel Create(NewsPage currentPage) {
var model = new NewsPageViewModel(currentPage);
PageViewModelBuilder.SetBaseProperties(model);
model.RelatedNews = SearchManager.Instance.FindSimular(currentPage, 0, 5);
return model;
}
}
}
Lets create the view and display the page properties as well as the related news list.
Views/NewsPage/Index.cshtml
@model DemoSite.Models.ViewModels.NewsPageViewModel
@{
Layout = "~/Views/Shared/_Layout.cshtml";
}
@Html.Partial("~/Views/Partials/BreadCrumbsView.cshtml", Model)
<div class="row">
<div class="col-lg-9">
<h1>@Model.CurrentPage.Headline</h1>
<p class="preamble">@Model.CurrentPage.Preamble</p>
@Model.CurrentPage.MainBody
</div>
<div class="col-lg-3">
<h2>Related news</h2>
<ul class="list-unstyled related">
@foreach (var searchHit in Model.RelatedNews.Hits) {
<li><a href="@searchHit.Path">@searchHit.Title</a></li>
}
</ul>
</div>
</div>
You find more about the search engine at the project site.
Creating the search page controller and view
The Index action in the controller for this page is the one that we'll post our search form to. We need to be able to recieve the query (parameter called q) with the search terms and an optional page (p) for the current pager value.
The first thing we do when we create the controller is to disable the default Index action. This is done by adding a NonAction
attribute and allows us to create a Index action with more parameters.
The frontend mostly consists of a search field and a little bit of JavaScript logic related to it's posting.
Controllers/SearchPageController.cs
namespace DemoSite.Controllers {
using System;
using System.Web.Mvc;
using Business.ViewModelBuilders;
using KalikoCMS.Mvc.Framework;
using Models.Pages;
public class SearchPageController : PageController<SearchPage> {
public ActionResult Index(SearchPage currentPage, string q = null, int page = 1) {
var model = SearchPageViewModelBuilder.Create(currentPage, q, page);
return View(model);
}
[NonAction]
public override ActionResult Index(SearchPage currentPage)
{
throw new NotImplementedException();
}
}
}
We'll create a view model for our search page that contains a list with the search result, the search query and the current pager value.
Models/ViewModels/SearchPageViewModel.cs
namespace DemoSite.Models.ViewModels {
using System.Collections.Generic;
using KalikoCMS.Core;
using KalikoCMS.Search;
using PagedList;
using Pages;
public class SearchPageViewModel : IPageViewModel<SearchPage> {
public SearchPageViewModel(SearchPage currentPage) {
CurrentPage = currentPage;
}
public SearchPage CurrentPage { get; private set; }
public IEnumerable<CmsPage> TopMenu { get; set; }
public IPagedList<SearchHit> SearchResult { get; set; }
public string Query { get; set; }
public int Page { get; set; }
}
}
In the model builder we performs the search and retrieves all hits. Normally you might want to limit the result to a particular page (through setting NumberOfHitsToReturn
and ReturnFromPosition
), but in the demo we're using PagedList and for it requires all hits in order to perform the paging.
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]
.
Business/ViewModelBuilders/SearchPageViewModelBuilder.cs
namespace DemoSite.Business.ViewModelBuilders {
using System.Collections.Generic;
using KalikoCMS.Search;
using Models.Pages;
using Models.ViewModels;
using PagedList;
public class SearchPageViewModelBuilder {
public const int PageSize = 5;
public static SearchPageViewModel Create(SearchPage currentPage, string query, int page) {
var model= new SearchPageViewModel(currentPage);
PageViewModelBuilder.SetBaseProperties(model);
model.SearchResult = new PagedList<SearchHit>(GetSearchResult(currentPage, query, page), page, PageSize);
model.Query = query;
model.Page = page;
return model;
}
private static List<SearchHit> GetSearchResult(SearchPage currentPage, string query, int page) {
var searchQuery = new SearchQuery(query) {
MetaData = new[] {"category", "summary"}
};
var result = SearchManager.Instance.Search(searchQuery);
return result.Hits;
}
}
}
In our view we render a search field populated with any search term we might have searched for and also render the search result using a PagedList.
Views/SearchPage/Index.cshtml
@using DemoSite.Business.ViewModelBuilders
@using OpenAccessRuntime.util.classhelper
@using PagedList.Mvc
@model DemoSite.Models.ViewModels.SearchPageViewModel
@{
Layout = "~/Views/Shared/_Layout.cshtml";
}
<div class="row">
<div class="col-lg-6 col-lg-push-3">
<div id="searchfield" class="input-group">
@Html.TextBox("query", Model.Query, new { @class = "form-control" })
<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">
@if (!string.IsNullOrEmpty(Model.Query))
{
if (Model.SearchResult.Any())
{
foreach (var searchHit in Model.SearchResult)
{
<p>
<a href="@searchHit.Path">@searchHit.Title</a><br />
<span class="url">@searchHit.Path</span><!----><br />
@if (!string.IsNullOrEmpty(searchHit.Excerpt)) {
@Html.Raw(searchHit.Excerpt)<br/>
}
else if (searchHit.MetaData.ContainsKey("summary") && !string.IsNullOrEmpty(searchHit.MetaData["summary"])) {
@Html.Raw(searchHit.MetaData["summary"])<br />
}
<span class="label label-warning">@searchHit.MetaData["category"]</span>
</p>
}
@Html.PagedListPager(Model.SearchResult, page => Url.Action(null, Model.CurrentPage.PageUrl.ToString().TrimStart('/'), new { q = Model.Query, page }))
<p>Showing @Model.SearchResult.FirstItemOnPage to @Model.SearchResult.LastItemOnPage of @Model.SearchResult.TotalItemCount search hits</p>
}
else
{
<p><i>No pages were found matching the search criteria.</i></p>
}
}
</div>
<script>
$(document).ready(function () {
$("#searchButton").click(doSearch);
$(
var keycode = (event.keyCode ? event.keyCode : event.which);
if (keycode == doSearch();
return false;
}
});
function doSearch() {
var query = $("#query").val();
var url = document.location.pathname + "?q=" + escape(query);
document.location = url;
};
});
</script>
We're getting close to finishing up our controllers and views, only the ones for products remain.
Creating the product list controller and view
In our controller we'll add two actions. The standard Index action will act as our product list page and the Product action will be responsible for displaying the product information from our page extender.
Controllers/ProductListPageController.cs
namespace DemoSite.Controllers {
using System.Web.Mvc;
using Business.FakeStore;
using Business.ViewModelBuilders;
using KalikoCMS.Mvc.Framework;
using Models.Pages;
public class ProductListPageController : PageController<ProductListPage> {
public override ActionResult Index(ProductListPage currentPage) {
var model = ProductListPageViewModelBuilder.Create(currentPage);
return View(model);
}
public ActionResult Product(ProductListPage currentPage, string productId) {
var model = ProductListPageViewModelBuilder.Create(currentPage);
model.SelectedProduct = FakeProductDatabase.GetProduct(productId);
return View("Product", model);
}
}
}
In our view model we'll add a list of products as well as the selected product. The selected product will only be used on the detail view. In a real world scenario you might want to consider to create a separate view model for the detail view.
Models/ViewModels/ProductListPageViewModel.cs
namespace DemoSite.Models.ViewModels {
using System.Collections.Generic;
using Business.FakeStore;
using KalikoCMS.Core;
using Pages;
public class ProductListPageViewModel : IPageViewModel<ProductListPage> {
public ProductListPageViewModel(ProductListPage currentPage)
{
CurrentPage = currentPage;
}
public ProductListPage CurrentPage { get; private set; }
public IEnumerable<CmsPage> TopMenu { get; set; }
public List<Product> Products { get; set; }
public Product SelectedProduct { get; set; }
}
}
In our model builder we'll just get a list of products from our faked external product database. In a real world scenario you might want to consider adding a layer of caching.
Business/ViewModelBuilders/ProductListPageViewModelBuilder.cs
namespace DemoSite.Business.ViewModelBuilders {
using FakeStore;
using Models.Pages;
using Models.ViewModels;
public class ProductListPageViewModelBuilder {
public static ProductListPageViewModel Create(ProductListPage currentPage) {
var model= new ProductListPageViewModel(currentPage);
PageViewModelBuilder.SetBaseProperties(model);
model.Products = FakeProductDatabase.GetProducts();
return model;
}
}
}
We display the headline and main body from our page and 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.
Views/ProductListPage/Index.cshtml
@model DemoSite.Models.ViewModels.ProductListPageViewModel
@{
Layout = "~/Views/Shared/_Layout.cshtml";
}
<div class="row">
<div class="col-lg-9">
<h1>@Model.CurrentPage.Headline</h1>
@Model.CurrentPage.MainBody
<ul class="list-unstyled products">
@foreach (var product in Model.Products) {
<li>
<h2><a href="@string.Format("{0}{1}/", Model.CurrentPage.PageUrl, product.Id)">@product.Name</a></h2>
<p>@product.Description</p>
</li>
}
</ul>
</div>
</div>
Creating the product detail view
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 created an action called Product in our controller (to match the call from our page extender). Although this is not a CMS page we will still get a strongly typed CmsPage
object called CurrentPage send to the action of the page that we're extending.
We create another view for our controller, this time for the Product action. The active product will be available in Model.SelectedProduct.
Views/ProductListPage/Product.cshtml
@model DemoSite.Models.ViewModels.ProductListPageViewModel
@{
Layout = "~/Views/Shared/_Layout.cshtml";
}
<div class="row">
<div class="left-menu col-lg-2">
<div class="list-group">
<span class="list-group-item active">Products</span>
@foreach (var product in Model.Products) {
<a href="@string.Format("{0}{1}", Model.CurrentPage.PageUrl, product.Id)" class="list-group-item">@product.Name</a>
}
</div>
</div>
<div class="col-lg-8">
<h1>@Model.SelectedProduct.Name</h1>
<p class="preamble">@Model.SelectedProduct.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>@Model.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>
That's it! We've now created all our controllers and views 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 your page types. The page types from which you can select depends on how the AllowedTypes
property of the parent page type was set up. For the root node and all pages where the property is not set all page types will be available.
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 either hit the save working copy or publish page button.
Each page can have one published version and one working copy at any time. Each version that have been previously published is stored as archived.
If you want to continue to work on your page before publishing Save working copy lets you do just that. Once you are pleased with the content you can go ahead and press Publish page.
If you want to see a list of all version of the page (and perhaps revert to an older version) you find them behind the Show versions 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 the URL segment to be something else, you can manually set it under the Show advanced options section.
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 Publish 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.
Drag and drop is also used to sort pages which parent is set to sort child pages using SortIndex.
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 ASP.NET MVC the CMS works well with WebForms. If you rather use WebForms there's an article on how to build a website with Kaliko CMS using WebForms.
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 choise is yours!
This idea to leave the choice 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
2015-08-29 First version
2016-01-24 Replaced custom property type with using CompositeProperty
CompositeProperty is a much easier way to build together complex property types based on already existing property types.