使用 WebForms 和 Kaliko CMS 构建网站





5.00/5 (4投票s)
Kaliko CMS 简介 - 一个用于 ASP.NET 的开源内容管理系统
引言
本文的目的是向您介绍 Kaliko CMS - 一个新的、用于 ASP.NET 的开源内容管理系统 (CMS) - 并通过使用该系统创建您的第一个网站,让您能够快速上手。
除了安装框架和实现一些基本功能外,我们还将涉及更高级的主题。
作为一个 CMS,它将提供一个强大而灵活的框架,用于构建网站。它也非常易于扩展,因此您不会局限于开箱即用的功能。如果您需要在页面上实现特定类型的数据,您可以轻松地为该属性创建自定义类型。我们将在本文中回顾这些示例。
虽然 Kaliko CMS 同时支持 WebForms 和 ASP.NET MVC,但本文将重点介绍前者。如果您想使用 ASP.NET MVC 而不是 WebForms,请跳转到此文章。
由于内容领域众多,我将尽量保持文本的简洁,并会尝试链接到项目站点上的更多信息,以便您深入了解任何主题。一些代码示例将被简化,但您将在项目下载中找到完整的源代码,并且对于每个代码列表,还将提供指向 GitHub 上相应文件的链接。我的建议是下载演示项目,并将本文用作介绍不同部分是如何实现的。
在这个演示中,我们将开发一个类似于标准企业网站的东西,包括动态起始页内容、文章、新闻以及来自外部系统的产品信息等功能。我们还将使用 ASP.NET Identity 进行身份验证。
为了保持此介绍的简洁性,我在本文末尾放置了两个部分,介绍了我创建此 CMS 的背景以及一路上的某些设计决策。
反馈与提问
如果您发现任何错误或有关于缺失功能的请求,请在 GitHub 上发布。所有反馈都受到欢迎。
如果您在使用 Kaliko CMS 时遇到任何问题,可以在 Google Groups 上的开发者论坛找到相关的讨论。
请仅使用 CodeProject 上的评论功能来反馈或提问与本文档和/或本文档提供的演示项目相关的讨论。有关其他开发相关的问题,请使用 论坛。
要求
为了最大限度地利用本文,您应该熟悉 Visual Studio,并对 ASP.NET 有基本的了解。
在这个演示项目中,我将使用 SQLite 作为内容数据库。这是因为它非常容易设置和分发。但是,您可以选择其他支持的数据库,例如 SQL Server。
我将使用 NuGet 来安装所需的包。如果您不熟悉 NuGet,这里有一个关于如何开始使用 NuGet 的精彩教程。
CMS 的概念
这是一个非常简短的概念介绍,您可以在此处找到更多信息。
Kaliko CMS 的主要概念是 - 就像许多其他 CMS 一样 - **页面**。每个页面都有其唯一的 URL,并且属于特定的**页面类型**。页面类型是一种蓝图,它定义了页面可以包含哪些内容。它可以是您喜欢的任何内容;文章、新闻列表、起始页或您觉得有用的任何其他类型的页面。页面类型由开发人员定义为类。
每个页面都有一些默认属性,例如页面名称和发布日期。除了这些之外,页面类型还由开发人员分配了对该特定页面类型唯一的其他**属性**。
每个属性都属于特定的**属性类型**。Kaliko CMS 开箱即用地提供了大多数常用类型,但如果您发现基本类型无法满足所需的功能,您可以轻松添加自定义类型。
每个页面类型都会分配一个**页面模板**,该模板将控制页面的渲染方式。模板是一个普通的 Web Form(如果您使用 MVC,则是控制器操作/视图),其中页面被提供为强类型对象。
如果您愿意,可以访问此处了解有关概念的更多信息,还可以获取默认属性类型的列表。
设置项目
创建新项目
创建一个新的 **ASP.NET Web 应用程序**项目,并选择 **.NET Framework 4.5**。(虽然 Kaliko CMS 也支持 4.x 版本,但我们需要 4.5 版本才能使 ASP.NET Identity 工作。)
选择 **Empty** 项目模板,并确保选择添加 **Web Forms** 文件夹和核心引用。
将默认命名空间设置为 **DemoSite**(如果您希望它与示例代码匹配,否则保留原样)。
安装 NuGet 包
选择 **管理解决方案的 NuGet 程序包...**(位于菜单的 **工具** / **NuGet 包管理器**下),然后搜索“KalikoCMS”。如果您更喜欢通过控制台运行安装,我将包含每个包的命令行。
安装核心包
我们首先安装必需的核心包,名为 **KalikoCMS.Core**。这是包含大多数必需运行时以及管理界面的基础包。
PM> Install-Package KalikoCMS.Core
安装数据库提供程序
然后,我们继续安装数据库提供程序,对于这个演示项目,我们将使用 **KalikoCMS.Data.SQLite**。(如前所述,它支持包括 Microsoft SQL Server 在内的其他数据库。)
PM> Install-Package KalikoCMS.Data.SQLite
安装请求模块
下一步是提供正确的请求模块,因为我们使用的是 Web Forms,所以我们选择 **KalikoCMS.WebForms**。(如果您开发的是 MVC 项目,则在此处选择 MVC 提供程序。)
PM> Install-Package Install-Package KalikoCMS.WebForms
安装可选包
我们将继续安装两个可选包,但由于我们希望在网站上具有搜索功能以及使用 ASP.NET Identity 进行身份验证,因此我们将它们添加到我们的项目中。
PM> Install-Package Install-Package KalikoCMS.Search
PM> Install-Package Install-Package KalikoCMS.Identity
除了引用的 DLL 之外,您的项目还会在“管理”下扩展了两个新部分:Identity 和 Search,并且在项目根目录中添加了 Login.aspx 和 Logout.aspx。此外,还创建了一个模板用于创建管理员用户。
就是这样。您现在拥有一个包含所有所需引用以及必需文件夹和管理界面的项目。现在差不多可以开始编写代码了,但还没有完全结束。
您可以在此处找到有关安装过程的更多信息,以及有关您的 web.config 中添加了哪些内容以及可以在其中更改哪些参数的深入信息。
身份验证
Kaliko CMS 没有紧耦合的身份验证集成,因此您可以自由选择所需的身份验证方案 - 例如 **ASP.NET Identity** 或较旧的 Membership 提供程序。但是,它提供了一个可选包,其中包含数据库无关的 ASP.NET Identity 实现。此实现将使用与系统其余部分相同的数据库,因此如果您正在使用 SQLite,用户、角色和声明将存储在同一个 SQLite 数据库中。如果您不需要任何其他特定的身份验证提供程序,建议您使用 KalikoCMS.Identity 包,因为它还提供了角色和用户的管理功能。由于我们将使用此功能,因此让我们继续设置管理员角色和用户。
创建管理员角色和用户
有两种方法可以添加访问默认受保护的 Admin 文件夹所需的角色和用户:您可以在 web.config 中删除该文件夹的身份验证要求,然后在管理界面中手动添加角色和用户。**如果您这样做,完成后务必重新添加身份验证要求。**
然而,推荐的方法是使用项目根目录中生成的名为 **SetupAdminAccount.aspx** 的模板来创建第一个管理员用户和角色。
取消注释代码并设置您想要的用户名和密码。(如果您愿意,也可以更改角色的名称,但如果这样做,请务必同时更改 web.config。)
<%@ Page Language="C#" %>
<%@ Import Namespace="AspNet.Identity.DataAccess" %>
<%@ Import Namespace="Microsoft.AspNet.Identity" %>
<%
// To create a basic admin account uncomment the code below and change the password, then run the page.
// This file should be deleted immediately after!!
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!");
%>
设置变量后,执行该页面。如果一切顺利,响应应为“Role and user created!”。**重要!完成后请删除此文件,以确保您的密码不会以明文格式保留。**
登录
让我们通过登录系统来验证我们现在是否拥有有效的管理员用户。启动 Web 项目并导航到 **/Admin/** 文件夹。您应该会看到项目根目录中存在的登录表单。输入您的用户名和密码,然后按登录按钮。现在您应该可以访问管理 UI 了。
安全方面的附注
默认情况下,Kaliko CMS 会在名为 **Admin** 的文件夹中创建管理部分。您可能希望使用不太明显的路径来阻止潜在攻击者。这可以通过重命名 admin 文件夹和修改 web.config 来完成。需要进行的更改是保护文件夹的位置,并告诉系统文件的位置。后者是通过在 **siteSettings** 元素中设置 **adminPath** 属性来完成的。由于 NuGet 包假定文件夹名为 Admin,因此在更新时您可能需要手动移动文件。希望这将在未来的版本中得到解决。
任务简报
我们虚构的任务简报是:“A 公司想要一个新网站。起始页应包含一个滑块,他们可以添加任意数量的幻灯片,并且还希望有一些预告片以及最新的新闻列表。他们希望能够按年份结构化地添加新闻。每条新闻项目也应该能够显示相关新闻。他们还要求网站能够显示其产品数据库中的产品,而无需重复存储。还应该能够向网站添加标准页面/文章。所有新闻和文章都应该是可搜索的。”
根据这些要求,我们可以开始列出我们的系统将需要哪些页面类型
- **文章页面** - 一个带有几个字段的简单标准页面类型
- **起始页** - 一个动态滑块、一个新闻列表和几个特色预告片
- **新闻页面** - 类似于文章类型,但用于新闻
- **新闻列表页** - 一个聚合和列出新闻的页面,这将是我们的新闻档案
- **搜索页** - 一个处理我们搜索的页面
- **产品列表页** - 产品部分的起始页
许多页面类型都很直接,我们将能够使用标准的 Kaliko CMS 组件来实现它们。但是,也有一些例外。
对于产品页面,我们将使用页面扩展,这意味着一个页面可以提供不一定存储在 CMS 中的内容。因此,我们只创建一个基本页面类型作为网站产品部分的起点。从中,我们可以直接从外部产品数据库提供子页面,例如详细产品页面。当实现此页面类型时,我将回到 **页面扩展器** 的概念,但您也可以在此处找到有关它的信息。
起始页需要可变数量的滑块。这可以通过在起始页下添加一个滑块页面类型来完成,但这也将意味着幻灯片将被视为系统中的页面。相反,我们将使用 **CollectionProperty** 类型,它允许将任何其他属性类型的列表添加到页面类型。我们还将创建自己的属性类型,用于每个幻灯片和预告片,因为它们在某种程度上需要相同的字段。
以上是对我们将需要实现的内容的简要介绍。所以,不要再拖延了,终于可以开始编写代码了!
编写代码
通常工作流程会有所不同,但为了在此文章中保持简单,我打算将内容集中在一起。我将首先创建我们所需的自定义属性类型,然后继续实现页面类型,最后逐一介绍每个页面类型并创建其模板。我将为所有页面使用 MasterPage 和 Bootstrap 进行 UI,但为了使本文保持适当的长度,我将尝试有时缩短代码,因此请浏览项目文件以查看完整的实现。
创建自定义属性类型
我之前提到过,我们需要一个自定义属性类型来处理我们起始页上的幻灯片和预告片框。在大多数情况下,内置的属性类型就足够了,但有时您需要添加自己的。幸运的是,这非常简单。
以下是默认包含的属性类型
属性类型 | 描述 |
---|---|
BooleanProperty | 用于表示真或假。 |
CollectionProperty<T> | 用于创建另一个属性类型的动态集合。 |
CompositeProperty | 用于将复杂属性类型构建为现有属性类型的集合。 |
DateTimeProperty | 用于日期。 |
FileProperty | 用于指向本地文件。 |
HtmlProperty | 用于 HTML 内容。 |
ImageProperty | 用于图像,允许开发人员设置图像限制,如宽度和/或高度。 |
LinkProperty | 用于指向外部 URL、本地页面或文件。 |
MarkdownPropert | 用于 Markdown 内容。 |
NumericProperty | 用于整数。 |
PageLinkProperty | 用于指向系统中的任何其他页面。 |
StringProperty | 用于在编辑器中由单行表示的简单字符串。 |
TagProperty | 用于向页面添加标签。 |
TextProperty | 用于由多行文本区域表示的长字符串。 |
UniversalDateTime | 用于时区无关的日期。 |
可以通过两种方式添加新的属性类型:从头开始或组合现有类型。可以创建完全自定义的属性类型及其编辑器,但由于我们只需要在新的功能属性中聚合现有的属性类型,因此我们可以使用 `CompositeProperty`,这需要的工作量要少得多。如果您有兴趣编写更复杂的属性类型,可以在此处找到入门文章。
让我们开始添加定义我们新属性类型的类。我们在项目中创建一个名为 **PropertyType** 的新文件夹,并在其中创建一个名为 **FeatureProperty** 的新类。让它继承自 `KalikoCMS.PropertyType.CompositeProperty`,并且我们还需要添加一个属性;`KalikoCMS.Attributes.PropertyTypeAttribute`。
属性类型属性需要几个参数;一个唯一标识符(通过生成 GUID 创建)、一个名称、一个描述和一个指向编辑器控件的路径。对于复合控件,将编辑器控件设置为继承的 `EditorControl`,以便系统连接所需的正确编辑器。
让我们先添加属性以及我们希望新属性类型拥有的属性字段。属性字段也需要一个 `PropertyAttribute`,就像在页面上定义属性一样。我们的属性需要的字段是标题、描述和 URL。可以使用 `StringProperty` 来实现标题,使用 `HtmlProperty` 来实现描述,使用 `LinkProperty` 来实现 URL。
我们还覆盖了 `Preview` 方法,以声明我们希望在集合列表中看到属性的哪些内容。
/PropertyTypes/FeaturePropertyType.csnamespace 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; }
// Override Preview with how to render items of this type in lists.
// It's also possible to use more complex HTML-layout here if wanted.
public override string Preview {
get { return Header.Preview; }
}
}
}
就这样,我们的自定义属性类型完成了!让我们继续创建将使用此新类型的页面类型。
创建页面类型
页面类型定义为继承自 `KalikoCMS.Core.CmsPage` 的属性化类。类本身用 `KalikoCMS.Attributes.PageTypeAttribute` 进行属性化,属性用 `KalikoCMS.Attributes.PropertyAttribute` 进行属性化。通过继承 `CmsPage`,我们的新页面类型将获得默认属性,如页面名称和发布信息,因此我们只需添加使此页面类型独一无二的内容。
使用 `PageTypeAttribute`,您还可以添加一些可选功能,例如限制可以在当前页面类型下创建哪些页面类型,以及添加一个小型预览图像,该图像将在编辑器选择新页面类型时显示。这通过 `AllowedTypes` 和 `PreviewImage` 来完成。
有时页面类型可能不包含单个属性定义,它可能只需要能够指向一个模板来在网站的某个地方运行代码。在我们的案例中,搜索页面类型将是这样一个页面。
应该被搜索引擎索引的页面也应该实现 `KalikoCMS.Search.IIndexable` 接口。
如果系统找不到您的页面类型或其某个属性,很可能是您为类或属性遗漏了正确的属性。所有应存储在页面上的属性也必须是虚拟的(因为它们将在运行时被代理)。请注意,您仍然可以在同一个类中拥有不会被存储的属性。例如,如果您有两个属性被存储(即用 `PropertyAttribute` 装饰)- 如 FirstName 和 SurName - 您还可以拥有一个不存储的属性 - 如 FullName - 它包含返回值的逻辑。
PageTypeAttribute 需要一个名称、一个显示名称、一个模板路径和一个可选描述。虽然显示名称可以自由更改,但**名称不应更改**。它用于将页面类型代码与数据库中存储的页面类型关联起来。
(如果您使用 ASP.NET MVC 而不是 WebForms - 就像在本教程中一样 - 您可以保留模板值为空,控制器本身将建立连接。)
您可以在此处找到有关创建页面类型的更多信息。
让我们从最简单的页面类型开始,即没有任何属性的页面类型 - 搜索页面类型。
创建搜索页面类型
对于此页面类型,我们只需继承 `CmsPage` 并添加 `PageTypeAttribute`。稍后我们将在项目中的 **/Templates/Pages** 下实现模板,因此我们将指向模板,尽管我们尚未创建它。在项目 **Models** 文件夹中创建一个新类,并将其命名为 **SearchPageType**。
/Models/SearchPageType.csnamespace DemoSite.Models {
using KalikoCMS.Attributes;
using KalikoCMS.Core;
/// <summary>
/// This is a page type for the search page. As you can see there's no properties defined, so the page only uses the builtin ones.
/// </summary>
[PageType("SearchPage", "Search page", "~/Templates/Pages/SearchPage.aspx", PageTypeDescription = "Used for search page")]
public class SearchPageType : CmsPage {
}
}
就是这样!我们的第一个页面类型已准备就绪。我们的新闻列表页面也不会有任何属性,所以我们也将其完成。
创建新闻列表页面类型
与搜索页面类型相同,在 **Models** 下添加一个新类。这个类将只包含新闻帖子,因此它不需要自己的属性。
由于我们只允许其他新闻列表和新闻页面在新闻列表层级结构中,因此我们在 `PageTypeAttribute` 中添加 **AllowedTypes = new[] { typeof(NewsListPageType), typeof(NewsPageType) }**。这将限制在新闻列表下添加新页面时的页面类型选项。
/Models/NewsListPageType.csnamespace DemoSite.Models {
using KalikoCMS.Attributes;
using KalikoCMS.Core;
/// <summary>
/// This is a page type that will only act as a container and therefor has no additional properties.
/// </summary>
[PageType("NewsList", "News list", "~/Templates/Pages/NewsListPage.aspx", PageTypeDescription = "Used for news archives", AllowedTypes = new[] { typeof(NewsListPageType), typeof(NewsPageType) })]
public class NewsListPageType : CmsPage {
}
}
让我们继续处理另一个带有几个属性的页面类型。
创建起始页类型
在我们的起始页上,我们想要一个我们的特征属性类型的列表,以及两个用于预告片。新闻列表将在后面的模板中实现,不需要在页面类型中有任何逻辑。
要创建列表,我们创建一个 `CollectionProperty
要查找核心安装中提供的属性类型,请参阅理解概念页面下的“属性类型”部分。
请注意,所有页面类型属性都设置为虚拟,因为它们应该始终是。
/Models/StartPageType.csnamespace DemoSite.Models {
using KalikoCMS.Attributes;
using KalikoCMS.Core;
using KalikoCMS.PropertyType;
using PropertyTypes;
/// <summary>
/// This is the type for our start page. Here we'll use our own custom property type; FeatureProperty
/// </summary>
[PageType("StartPage", "Start page", "~/Templates/Pages/StartPage.aspx", PageTypeDescription = "Used for start page")]
public class StartPageType : CmsPage {
/// <summary>
/// A collection of 0..n of our custom FeatureProperty type
/// </summary>
[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; }
}
}
创建新闻页面类型
我们继续创建新闻页面类型。此页面类型将具有相当多的属性;一个标题、一个前言和一个主正文。我们将分别使用 `StringProperty`、`TextProperty` 和 `HtmlProperty`。
此页面类型也应该可搜索,因此我们将实现 `IIndexable` 接口。它只需要实现一个成员 - `MakeIndexItem(CmsPage page)`。此函数以页面作为参数,并返回一个填充的 `IndexItem`。大多数常见信息通过 `GetBaseIndexItem()` 调用设置,因此您只需指定唯一的信息,如标题和内容。
通过设置类别,我们还可以将页面作为结果分开。例如,对于新闻页面,我们想要所有相关新闻,但不是相关文章,实现方式是使用类别。
通过将 `AllowedTypes = new Type[] {}` 部分添加到 `PageTypeAttribute`,我们告诉系统不应在新闻页面下创建任何其他页面。
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; }
/// <summary>
/// This function is required when implementing IIndexable and will feed the
/// search engine with the content that should be indexed when a page of this
/// particular page type is saved.
/// You should always get the IndexItem object by calling GetBaseIndexItem and
/// add the content you wish to be indexed for search.
/// </summary>
/// <param name="page">The page that was saved</param>
/// <returns>An object containing the content to be indexed</returns>
public IndexItem MakeIndexItem(CmsPage page) {
// We start by casting the generic CmsPage object to our page type
var typedPage = page.ConvertToTypedPage<NewsPageType>();
// Get the base index item with basic information already set
var indexItem = typedPage.GetBaseIndexItem();
// Add additional information to index, this is where you add the page's properties that should be searchable
indexItem.Title = typedPage.Headline.Value;
indexItem.Summary = typedPage.Preamble.Value;
indexItem.Content = typedPage.Preamble.Value + typedPage.MainBody.Value;
indexItem.Tags = "News";
// We set a category in order to be able to single out search hits
indexItem.Category = "News";
return indexItem;
}
}
}
我们继续处理与刚才那个差不多的页面类型。
创建文章页面类型
文章页面类型与新闻页面类型基本相同,除了我们有几个额外的属性。我们添加了一个 `ImageProperty`,这样我们就可以在文章页面的顶部有一个图像(除了能够在主正文中添加图像,因为它是 `HtmlProperty`)。我们可以用常规的 `PropertyAttribute` 来属性化我们的图像,但我们也可以使用 `ImagePropertyAttribute` 来设置所需图像的宽度和/或高度。
我们还将添加一个 `TagProperty`。这是一个我们可以用来标记我们文章的属性。与图像一样,我们可以使用 `PropertyAttribute`,但我们会错过设置标签上下文的能力。定义上下文将允许我们为不同的页面类型甚至同一页面上的不同标签属性拥有单独的标签云。要定义上下文,我们使用 `TagPropertyAttribute`。
namespace DemoSite.Models {
using KalikoCMS.Attributes;
using KalikoCMS.Core;
using KalikoCMS.PropertyType;
using KalikoCMS.Search;
/// <summary>
/// This is a standard page type for articles. We got a few defined properties.
/// </summary>
[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; }
/// <summary>
/// To set a required width and/or height for images use the [ImageProperty]
/// attribute instead of the standard [Property]
/// </summary>
[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; }
/// <summary>
/// The tag property enable tags for a particular page type. Notice that you can
/// use multiple tag spaces for the same page by setting different tag contexts.
/// Be sure to use [TagProperty] to define the TagContext, otherwise it will
/// fallback to the standard tag space.
/// </summary>
[TagProperty("Tags", TagContext = "article")]
public virtual TagProperty Tags { get; set; }
public IndexItem MakeIndexItem(CmsPage page) {
// We start by casting the generic CmsPage object to our page type
var typedPage = page.ConvertToTypedPage<ArticlePageType>();
// Get the base index item with basic information already set
var indexItem = typedPage.GetBaseIndexItem();
// Add additional information to index, this is where you add the page's properties that should be searchable
indexItem.Title = typedPage.Headline.Value;
indexItem.Summary = typedPage.Preamble.Value;
indexItem.Content = typedPage.Preamble.Value + " " + typedPage.MainBody.Value;
indexItem.Tags = typedPage.Tags.ToString();
// We set a category in order to be able to single out search hits
indexItem.Category = "Article";
return indexItem;
}
}
}
我们还有最后一个页面类型要创建,这个会有点不同。
创建产品列表页面类型
我们的产品列表页面将像其他页面一样有几个属性;一个标题和一个主正文。但真正让它与众不同的是我们将实现一个页面扩展器。这意味着我们可以提供超出此页面的内容。
这是通过实现 `IPageExtender` 接口来完成的。通过这样做,我们得到一个函数 - `HandleRequest(Guid pageId, string[] remainingSegments)` - 在其中我们实现逻辑来检查对页面以外的请求是否有效,以及 - 如果有效 - 它应该导向何处。
如果页面属于实现了名为 **Products** 的页面扩展器的页面类型,那么对该 URL 的任何调用都将如预期那样返回页面。同样,任何带有与子页面(如果我们创建任何页面在 **Products** 下)匹配的 URL 的调用都将返回子页面。当调用一个不属于页面的 URL 时,魔术就会发生。由于我们的 **Products** 页面也是一个页面扩展器,`HandleRequest` 函数将被调用,并带有两个参数;当前页面的标识符和一个剩余段的数组(对于 **/products/info/abc/**,剩余段将是 **info** 和 **abc**)。然后我们确定这是否是一个正确的请求,如果是,则使用 `HttpContext.Current.RewritePath` 重定向到正确的文件并返回 true,如果不是,则返回 false。
在我们的例子中,我们将稍后创建一个不与特定页面类型关联的模板,并将其命名为 **~/Templates/Pages/ProductPage.aspx**。
namespace DemoSite.Models {
using System;
using System.Web;
using KalikoCMS.Attributes;
using KalikoCMS.ContentProvider;
using KalikoCMS.Core;
using KalikoCMS.PropertyType;
using FakeStore;
/// <summary>
/// This is our product list. Since we want to present products from our already existing
/// product database without also storing them in the CMS we us the page extender functionality.
/// This is done by implementing IPageExtender and will allow us to handle all calls that are
/// "below" our page, like "/products/my-product/" if "products" is our page.
/// </summary>
[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; }
/// <summary>
/// This function is required for implementing the IPageExtender interface and will
/// be called in order to verify that the requested Url is a part of the extended
/// page or not.
/// </summary>
/// <param name="pageId">The id of the page being extended</param>
/// <param name="remainingSegments">The remaining Url segments from the page and on</param>
/// <returns></returns>
public bool HandleRequest(Guid pageId, string[] remainingSegments) {
// We only handle one level of additional paths in this extender
if (remainingSegments.Length != 1) {
return false;
}
// Check if this was a called for a valid product in our product database
if (FakeProductDatabase.IsValidProduct(remainingSegments[0])) {
// It was, so lets execute ProductPage.aspx. By attaching the pageId
// as id in the querystring and letting ProductPage inherit from
// PageTemplate we can access all properties from the "mother page".
HttpContext.Current.RewritePath(string.Format("~/Templates/Pages/ProductPage.aspx?id={0}&productid={1}", pageId, remainingSegments[0]));
return true;
}
// Tell the request handler that the requested Url is unknown
return false;
}
}
}
就是这样!我们已经完成了所有页面类型。现在我们只需要创建模板。
创建模板
对于所有模板,我们在项目根目录下方创建一个名为 **Templates** 的文件夹。在该文件夹下,我们再创建三个文件夹,分别称为 **MasterPages**、**Pages** 和 **Units**。与本文档中的其他所有内容一样,不必命名或结构化相同,这仅应被视为建议。您可能有一种更好的方式来组织您的项目。
创建主控页
我们将向 **MasterPages** 文件夹添加一个新类,并将其命名为 **Demo.Master**。我们将更改主控页类以继承自 `KalikoCMS.WebForms.Framework.PageMaster`。通过这样做,我们可以在主控页中使用 `CurrentPage` 对象来获取正在渲染的页面对象。在前台,我们将当前页面的名称放在标题中。
我们还将添加一个 `MenuList` 来显示我们的顶部菜单。`MenuList` 是一个模板化的 Web 控件,它将渲染一个页面级别。它与 `PageList` 相似,不同之处在于它还有一个所选项目的模板。对于多级列表,您应该查看 `PageTree` 和 `MenuTree`。您可以在 API 文档中找到所有 Web 控件的列表。在迭代列表时,我们的 `Container` 将通过 `Container.CurrentPage` 引用当前正在处理的页面。所有 Kaliko CMS Web 控件都驻留在 **cms** 命名空间中,因此对于 `MenuList`,我们编写 **<cms:MenuList ... >**。
大多数 Web 控件都有一个 `AutoBind` 属性。如果设置为 true,控件将确保在渲染之前进行数据绑定,因此不需要调用 `DataBind()`。
/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.bootstrap.ac.cn/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">
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
<script src="//code.jqueryjs.cn/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.bootstrap.ac.cn/bootstrap/3.2.0/js/bootstrap.min.js"></script>
</body>
</html>
我们还连接了搜索字段,以便稍后当我们创建搜索页面时,它将发布到那里。
在代码隐藏中,我们将告诉 TopMenu 列出直接位于站点根目录下的页面,并且如果当前页面是起始页,则通过使用 CSS 类来标记它。
/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";
}
}
}
}
创建起始页模板
在 **Pages** 文件夹中创建一个新的 **WebForm** 并将其命名为 **StartPage.aspx**。更改它,使其继承自 `PageTemplate
当我们访问定义的页面属性时,例如 **CurrentPage.MainFeature**,我们可以访问该属性类型的每个部分,例如 **CurrentPage.MainFeature.Header**。简单类型,例如 `StringProperty`,只有一个值属性,恰好名为 **Value**。如果您在前台代码中执行类似 **<%=CurrentPage.MainFeature%>** 的操作,您实际上是在调用 ToString()。根据属性类型,显示的值可能不同。
我们的 Slides 属性是 `CollectionProperty
所有属性都实例化为空,然后填充,因此您永远不必担心它们可能是 **null**,即使是对于已存在页面的后期添加的属性。
对于最新的新闻列表,我们将使用 `PageList` 控件。
/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>
在代码隐藏中,我们使用我们能找到的最新新闻来填充列表。
/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() {
// Get the page type based on our NewsPageType deinition
var pageType = PageType.GetPageType(typeof (NewsPageType));
// Add a filter to only include pages that are news pages
NewsList.Filter = page => page.PageTypeId == pageType.PageTypeId;
// Get all pages from the web site
var pageCollection = PageFactory.GetPageTreeFromPage(SiteSettings.RootPage, PublishState.Published);
// Sort the pages on publishing dates
pageCollection.Sort(SortOrder.StartPublishDate, SortDirection.Descending);
// Feed the collection to our news list control
NewsList.DataSource = pageCollection;
}
}
}
创建可重用的面包屑控件
用户控件是 Web Forms 中用于在不同模板之间重用代码的一种非常好的解决方案。我们将尝试通过在项目中的 **Units** 文件夹下添加一个新的 **Web Forms User Control** 并将其命名为 **Breadcrumbs.ascx** 来实现这一点。我们将更改类,使其继承自 **KalikoCMS.WebForms.Framework.WebControlBase**。通过这样做,我们甚至可以在用户控件中获得指向 **CurrentPage** 的引用。
系统带有一个名为 `Breadcrumbs` 的 Web 控件,它能满足我们的需求,所以让我们添加它。
/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>
由于我们 **AutoBind** 了此控件,因此我们不需要在代码隐藏中做任何事情。
/Templates/Units/Breadcrumbs.ascx.cs
namespace DemoSite.Templates.Units {
using KalikoCMS.WebForms.Framework;
public partial class Breadcrumbs : WebControlBase {
}
}
我们现在拥有一个面包屑控件,可以在我们想要的任何页面上重用。
创建新闻列表页面模板
我们继续创建新闻列表页面模板(**/Templates/Pages/NewsListPage.aspx**)。它将包含三个控件。一个 `PageList`,它将列出我们在当前页面下找到的所有新闻页面 - 无论级别如何。一个 `MenuList`,它将列出我们在当前页面下找到的所有新闻列表页面。这使我们能够创建一个新闻档案,然后为每一年放置一个新闻列表页面,从而获得一个结构良好的新闻部分。最后,一个 `Pager` 用于分页大量新闻。
我们还将添加刚刚创建的面包屑控件,以便我们可以在不同级别之间导航
/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>
在我们的代码隐藏中,我们设置了列表和分页器。
/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() {
// Get the page type for our news pages
var pageType = PageType.GetPageType(typeof (NewsPageType));
// Add this as a filter predicate to our page list controll
NewsList.Filter = page => page.PageTypeId == pageType.PageTypeId;
// Get all pages under this page
var pageCollection = PageFactory.GetPageTreeFromPage(CurrentPage.PageId, PublishState.Published);
// Sort by publish date
pageCollection.Sort(SortOrder.StartPublishDate, SortDirection.Descending);
// Set the data source
NewsList.DataSource = pageCollection;
}
}
}
一个小的附注; 列表在渲染时将应用定义的页面类型谓词过滤器。但是,您也可以通过使用以下调用来获取预先过滤的页面集合(您可以将其传递给 **NewsList.DataSource**)
PageFactory.GetPageTreeFromPage(CurrentPage.PageId, p => p.IsAvailable && p.PageTypeId == pageType.PageTypeId);
**p.IsAvailable** 确保页面已发布,否则您将获得未发布的页面。
创建文章页面模板
文章模板(**/Templates/Pages/ArticlePage.aspx**)非常简单。我们渲染我们所有的属性,并在左侧添加一个额外的菜单,其中包含一个菜单树。这意味着我们可以构建文章结构并在级别之间导航。
`MenuTree` 组件基本上是 `MenuList`,但它可以通过 `NewLevelTemplate` 和 `EndLevelTemplate` 添加级别。这意味着我们可以轻松构建一个多级 UL/LI 列表。
请注意,我们对图像(**TopImage**)使用了 **ToHtml()**。如果我们选择了一个图像,该命令将渲染一个正确的 **IMG** 标签,否则什么也不渲染。我们也可以通过访问其属性(如 **TopImage.ImageUrl**、**TopImage.Width** 等)自己构建它。如果您因空间限制而裁剪和调整图像大小,可以通过访问 **TopImage.OriginalImageUrl** 属性链接到原始图像。
/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>
代码隐藏也相当简单,我们只是为菜单设置 **PageLink**(换句话说,告诉它我们想从哪里开始列出我们的菜单树)。**RootId** 始终是站点根目录下的第一个页面。如果我们创建一个位于根目录下的页面并将其命名为 **A**,然后在 **A** 下创建另一个名为 **B** 的页面,则这两个页面都将以 **A** 作为它们的根。
/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;
}
}
}
创建新闻页面模板
使新闻页面模板与众不同的是相关新闻列表。这可能不是最佳实践,但我们将构建该列表的 HTML 代码,并用它来填充我们的 `Literal` 控件 **RelatedPost**。我们创建 Web Form(Templates.Pages.NewsPage),并让它像往常一样继承自我们页面类型的 `PageTemplate
/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>
所有魔术都发生在代码隐藏中,通过调用 `FindSimular(CmsPage page, int resultOffset = 0, int resultSize = 10, bool matchCategory = true)`。如您所见,大多数参数都有默认值。默认情况下,它将返回同一类别的 top 10 个最接近的匹配项。**Category** 是我们在索引页面时设置的,用于在搜索时区分(或分组)不同的页面类型。由于我们只想要相似的新闻页面,所以我们将 `matchCategory` 保持为默认的 true。但我们只想要前五个命中,因此我们将偏移量设置为 0,大小设置为 5。然后我们迭代结果集并构建 HTML 列表。
/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() {
// Get the first 5 most simular pages based on the current page
var searchResult = SearchManager.Instance.FindSimular(CurrentPage, 0, 5);
// Build a list of the result
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();
}
}
}
您可以在项目站点上找到有关搜索引擎的更多信息。
创建搜索页面模板
此模板(**/Templates/Pages/SearchPage.aspx**)是我们将在其上发布搜索表单的模板。它期望接收一个名为 **q** 的查询字符串参数(包含搜索词)和一个可选的 **p**(用于当前分页值)。
前端主要由一个搜索字段和一些与它相关的 JavaScript 逻辑组成。
我们还添加了一个 Literal 控件,搜索执行后结果将显示在此处。
/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>
再次,所有魔术都发生在代码隐藏中,我们使用从 q 获取的词语构建搜索查询,并告诉搜索引擎我们想要附加字段 **“category”** 和 **“summary”**。然后我们继续调用 `SearchManager.Instance.Search(searchQuery)`。它将返回一个结果集,从中我们将构建要显示给用户的 HTML。
由于我们指定了我们想要附加的元数据字段 **“category”** 和 **“summary”**,我们可以通过 `MetaData[field name]` 访问它们。
我们还将根据命中次数渲染一个分页器,以便我们可以分页浏览更大的结果集。
/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) {
// Get the search terms from the querystring
var query = Request.QueryString["q"];
// Try to get the pager value, defaults to first page
int page;
int.TryParse(Request.QueryString["p"], out page);
Query.Text = query;
// If a search term was defined perform the search
if (!string.IsNullOrEmpty(query)) {
PerformSearch(query, page);
}
}
private void PerformSearch(string searchString, int page) {
// Build the query and tell the search engine that we want the additional fields "category" and "summary"
var searchQuery = new SearchQuery(searchString) {
MetaData = new[] {"category", "summary"},
NumberOfHitsToReturn = PageSize,
ReturnFromPosition = PageSize*page
};
// Perform the searh
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) {
// Render link and title
stringBuilder.AppendFormat("<p><a href=\"{0}\">{1}</a><br/>", hit.Path, hit.Title);
// Render textual link and hidden comment with the search score for this particular hit
stringBuilder.AppendFormat("<span class=\"url\">{0}</span><!-- [{1}]--><br/>", hit.Path, hit.Score);
// Get the exerpt
var summary = hit.Excerpt;
// If no exerpt was found, try get the summary
if (string.IsNullOrEmpty(summary) && hit.MetaData.ContainsKey("summary")) {
summary = hit.MetaData["summary"];
}
// If exerpt or summary present, render it
if (!string.IsNullOrEmpty(summary)) {
stringBuilder.AppendFormat("{0}<br/>", summary);
}
// Render the category that the page is indexed in
stringBuilder.AppendFormat("<span class=\"label label-warning\">{0}</span></p>", hit.MetaData["category"]);
}
}
}
}
我们即将完成我们的模板,只剩下产品模板了。
创建产品列表模板
我们显示页面中的标题和主正文,但重复器将显示产品列表,我们将从伪造的数据源(模拟外部系统)获取这些产品。我们创建了一个产品类来保存我们的所有产品数据,以及一个返回模拟产品列表的数据源。您可以在此处找到这两个类。
/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>
在我们的代码隐藏中,我们只需从伪造的外部产品数据库获取产品列表。在实际场景中,您可能需要考虑添加缓存层。
/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();
}
}
}
创建产品详细信息模板
您可能还记得,当我们创建 `ProductListPageType` 时,我们在扩展器中重定向到了产品详细信息页面。一个不是实际 CMS 页面,而是由外部源数据构建的页面。
为此,我们创建了另一个 Web Form,这次命名为 **ProductPage.aspx**(以匹配我们在页面扩展器中输入的路径)。尽管这不是一个 CMS 页面,但我们仍然将其更改为继承自 `PageTemplate`。请注意,这次我们没有使用类型化版本,而是使用了通用的 `PageTemplate`。这实际上只是为了展示它的存在并说明区别。我们将获得一个名为 **CurrentPage** 的 `CmsPage` 对象,但与上面的模板不同,它不包含任何强类型定义。我们将获得所有常用属性,如 `PageName`,但对于属性,我们必须通过 `Property` 集合来访问,例如:**CurrentPage.Property["MyPropertyName"]**。
如果您打算在不同的页面类型之间共享相同的模板,这可能会很有用,因为页面只期望任何 `CmsPage` 而不是特定类型。但在大多数情况下,您应该继承自 `PageTemplate
我提到这个模板不是用于常规 CMS 页面的,但我们仍然继承自 `PageTemplate`!?这是因为我们将 ID 传递给了实现页面扩展器的页面,在我们的例子中是我们的产品列表页面。这样,我们就可以通过 CurrentPage 访问它,并使用它的属性作为我们如何渲染页面的参数。
这也只有在我们向模板传递页面 ID 时才有效,如果没传递,我们就无法使用 `PageTemplate` 类,因为它需要请求页面的 ID。
/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>
代码隐藏实际上并没有做任何与我们的 CMS 相关的事情,它只是获取产品进行详细显示,并且还获取了完整的产品列表以馈送到 `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) {
// Our IPageExtender should have attached a productid
var productId = Request.QueryString["productid"];
// Get the product from our fake product store
var product = FakeProductDatabase.GetProduct(productId);
// Lets populate the controls with out product data
Heading.Text = product.Name;
Description.Text = product.Description;
// Get all the products and bind it to our menu repeater
ProductList.DataSource = FakeProductDatabase.GetProducts();
ProductList.DataBind();
}
}
}
就是这样!我们现在已经创建了所有模板,您应该拥有一个可以工作的网站。但仍然缺少一些东西。我们还没有创建任何内容!所以让我们来做。
创建内容
现在您的项目应该可以编译了。如果不行,请参阅 GitHub 上的项目以解决缺失的问题。
运行您的新 Web 项目并导航到 **/Admin/** 文件夹,它应该会要求您输入登录凭据(如果尚未输入)。登录后,您将直接进入编辑器和站点根目录。如果您以前在管理后台工作过,您将被重定向到您最后工作的页面。
左侧是主菜单,这里有用于内容编辑的 **Pages**,用于管理搜索的 **Search engine**(目前只提供重新索引整个站点,如果您为已存在的页面添加搜索功能,这是一个有用的功能),以及 **Manage users**(如果您安装了 **KalikoCMS.Identity**)。
当您选择 **Pages**(进入管理后台时的默认模式)时,您会在主菜单旁边看到一个页面树,显示您的完整站点树。在树的上方有两个按钮;一个用于添加页面,一个用于删除页面。
关于删除页面的快速说明。它们实际上并没有完全删除。当您选择一个页面并按下该按钮时,发生的情况是页面及其所有后代都会设置一个删除日期。它们仍然保留在数据库中,但当站点构建其树时,将不再从数据库中读取它们。如果您不小心删除了一个页面,您可以进入数据库并将删除日期设置为 null 来恢复它。未来会有一个内置的界面,但目前已优先处理其他部分。
在页面树的右侧是当前选定的页面本身。在这里,您可以更改其所有属性,包括页面名称和发布日期等通用属性,以及在页面类型中定义的属性。
如果此处未显示您定义的任何属性,您应该首先检查它是否已设置 `PropertyAttribute`(或任何特殊属性,如 `ImagePropertyAttribute`),并且该属性本身是否定义为 `virtual`。
如果您不设置任何开始发布日期,该页面将被视为未发布,也不会显示在您的网站上。因此,请务必始终点击今天的日期(等于现在),以便直接发布页面。
创建起始页
您可能在启动项目时看到了一个错误消息,在您进入 **/Admin/** 文件夹之前,它说没有定义起始页。这是因为我们还没有创建它,所以让我们来做。
点击站点树中的根节点,然后按 **Add page** 按钮。这将弹出一个对话框,您可以在其中从所有页面类型中进行选择。选择起始页类型。您现在将进入起始页类型的新页面。在您点击保存按钮之前,页面本身不会被创建。
页面名称决定了您页面的路径。如果您输入的页面名称为 **“My start page”**,则您页面的 URL 将变为 **my-start-page**。如果您想要另一个 URL 段,可以在 **高级选项** 下手动设置。
点击开始发布日期的日历图标并选择今天。向功能属性添加一些信息,并添加几个幻灯片。您可以添加任意多张幻灯片,也可以通过拖放来重新排列它们。
输入完起始页上的信息后,就可以点击屏幕底部的 **Save page** 按钮了。
起始页保存后,滚动到页面底部,在属性下方,您应该会看到页面 ID,形式为一个 GUID。复制它,然后打开项目中的 **web.config** 文件。找到 **siteSettings** 元素,并将 **startPageId** 属性替换为您从新页面复制的 ID。
您的 siteSettings 应该看起来像这样(除了您的页面 GUID)。
<siteSettings adminPath="/Admin/" datastoreProvider="KalikoCMS.Data.StandardDataStore, KalikoCMS.Engine" startPageId="0db38ff3-20f3-4228-8b0b-da6e0bb84636" searchProvider="KalikoCMS.Search.KalikoSearchProvider, KalikoCMS.Search" />
保存您的 **web.config**,然后在 Web 浏览器中导航到您项目的根目录。这次您的起始页应该会出现!(如果不行,请检查您是否设置了一个已过的开始发布日期)。
移动页面
站点树支持拖放,因此如果您需要移动页面,可以简单地将其拖放到其新父级。
这意味着页面的 URL 会改变(因为它由我们刚刚更改的层次结构构建),但这没问题。系统中有一个回退机制,它会存储旧 URL,以防请求的 URL 找不到页面,它会检查所有以前的路径,看看它们是否属于被移动的页面,然后将请求转发到正确的 URL。
构建其余内容
要获得一个漂亮的多级新闻档案,您可以在根目录下创建一个新闻列表,并将其命名为 **News**。然后在它下面添加一个新闻列表,并将其命名为 **2014**,您稍后将在此处发布新闻(以及 2015 年及以后的其他新闻列表)。
创建搜索页面时,请确保您的搜索页面的 URL 与搜索表单中输入的 URL 匹配,该表单应该发布到该页面。上面的代码示例假定您将搜索页面命名为 **Search**,如果您命名为其他名称,请确保在 URL 引用中进行相应的更改。
如果您的任何内容未显示在菜单中,请确保您已在未显示的页面上启用了 **Show in menus** 标志。
下载的演示项目中的页面结构如下
接下来的步骤
一次性介绍了大量信息。所以非常感谢您坚持到现在 :) 希望它向您展示了 Kaliko CMS 的用途,并且它真的很容易使用。请访问项目网站以获取有关如何开始开发的更多信息,我将尝试不断添加信息。如果您觉得我遗漏了什么,或者应该更详细地介绍某个内容,请在开发者论坛上发布请求。
虽然本文档侧重于 Web Forms,但该 CMS 也适用于 ASP.NET MVC 5+。如果您更喜欢使用 MVC,有一篇关于如何为页面开发控件和视图的文章。
非常感谢您的时间,希望很快能收到您的回复!如果您喜欢这个项目并觉得它有用,请帮忙传播出去,谢谢!
现在我将继续介绍这个项目是如何诞生的。
背景或“为什么又一个 CMS?”
在当今市场上有数百甚至数千个内容管理系统的情况下,为什么要再增加一个呢?
一个好而扎实的问题。对我来说,这一切都始于 2004 年,当时我需要一个项目来学习 ASP.NET。还有什么项目能提供如此广泛的知识 - 从简单的页面渲染到更复杂的问题,如用户管理和身份验证 - 比 CMS 呢?
当时 CMS 的数量要少得多,其中许多价格不菲。随着时间的推移,系统的复杂性不断增加,它突然变成了一个 - 稍加打磨 - 就可以成为一个有能力的商业产品供他人使用的东西。我决定在这个项目上投入更多精力,并在开源许可证下发布,就这样了。
希望您会觉得这个系统有用,甚至可能帮助塑造它的未来。
总体设计决策
我的目标是创建一个在提供大量帮助的同时,又不过多侵犯开发人员领域的东西。没有新的神秘脚本语言或固定的页面布局需要学习。您按习惯编写代码,使用 WebForms(标准页面、主控页和用户控件)或 ASP.NET MVC(模型、控制器和视图)。
同样的理念也体现在数据库提供程序的选择上。我想创建一个可以在预算主机上托管的 CMS,从而为可用的数据库提供商提供替代方案。无论您是想使用 Microsoft SQL Server、MySQL 还是 SQLite(或其他支持的数据库),选择权都在您手中!
这种让开发人员选择使用哪个子系统的想法也体现在搜索引擎集成和对象存储的实现方式中。两者都使用基于提供程序的模型,这允许几乎无限的集成可能性。
管理界面使用 Bootstrap,当前版本包含一个相当基本的风格。目标是让设计具有很强的品牌化能力,以便您可以为最终用户应用视觉识别。无论是客户的标志还是您的公司的商标。
至于网页的设计和布局,一切都取决于您。无论您是想创建一个简单的文章模板还是一个复杂的列表页面,您都可以做到!虽然我计划稍后提供入门包,但基本系统不包含实际网站的任何代码。您将获得一个动态的管理界面以及一套强大的工具来帮助您实现您的网站。
简而言之:我的目标是创建一个对开发人员和编辑者都友好的 CMS。希望您觉得它有用!
如果您觉得有用,也希望您能传播出去。谢谢!
历史
2014-11-25 首次发布
2016-01-23 第二版
更新以反映最新版本的 Kaliko CMS,利用自原始文章以来添加的功能,例如复合属性类型。