以下内容翻译自Building Websites with the ASP.NET Community Starter Kit by K. Scott Allen and Cristian Darie for Packt Publishing,以下内容是该书的第8章,详细介绍如何扩展CSK来增加FAQ功能. 如果你想了解 ASP.NET Community Starter Kit的详细信息,可以到www.asp.net浏览和下载,它是一个免费的开源项目.假如你想建造一个稳健而灵活的ASP.NET网站,CSK将是一个很好的出发点. 

创建一个新模块 
每一个社区网站都会有不同的需求要实现。虽然本身的CSK类库有很大的灵活性,但完全拥有源代码意味者你可以在这个高品质的网站基础上增加定制的额外功能。在这篇文章里,我们将详细介绍如何在已有的框架中增加一个全新的功能FAQ(Frequently Asked Questions),并如何做到与已有的模块无缝衔接。 

在真正开始之前,先提醒大家一下,CSK是一个不断修改和升级的项目,所以在动手增加新模块之前,先到网上查找一下别人是否已经实现了这项功能,或者关注一下CSK是否增加了新的特性。 

模块设计 
在你实现一个用于CSK的模块之前,首先要明白你要增加的特性到底是什么,然后决定由CSK中已有的哪些模块来实现这个功能。 

首先让我们对FAQ的需求列一个大致的清单: 

l 一个FAQ由一个问题、一个答案,一个描述或介绍和一些相关参照的链接组成, 

l 社区成员能够对某个问题加评注或评级,当有新的问题时通过邮件提醒, 

l 如果版主同意,社区成员可以发布新的问题主题。 

你当然可以只用一个HTML文件列出所以的问题和答案,但那样就限制了用户的交互性操作(评注、评级、邮件等) 

在CSK所附带的数据库中有表Community_ContentPages,其中包含了社区页面的大部分信息如作者、浏览计数和介绍等。由于要存放与FAQ相关的答案、参考链接等属性,我们再增加一个表Community_Faqs: 


然后我们可以创建保存FAQ信息的类了。在下面的图中,FaqInfo类继承自ContentInfo,它可以保存一般内容信息项的大部分属性。每一个模块都会有一个自己的Utility类来读取、添加和编辑内容。所以对于FAQ的模块,我们还要创建一个FaqUtility类。 


我们还要创建给Code-Behind页面使用的类来显示和编辑FAQ。CSK中是通过SkinnedCommunityControl来使不同页面显示不同的界面。CSK中也包含了其它实现了常用功能的基类可用于增加(ContentAddPage),编辑(ContentEditPage)和显示(ContentItemPage),下面的图中显示了这些类的继承关系 


另外,我们还需要创建从WebControl继承的类来显示FAQ的内容。通常,每一个属性显示时放在不同的控件中,并且该控件可以用最适合的风格来显示内容。下面的图中显示了这些最终在这个模块中会使用的控件,它们最终都是从WebControl继承而来。 


模块创建过程 
构建模块的过程我们将采用自底向上底方式,从数据库建立开始,到表现层的界面和主题设置结束。我们将沿用在CSK中已经约定的命名模式,保持与其它模块的风格一致。如,在书的模块中要从Community_Books表中读取信息,那么相应的类就是BookInfo。 

这样我们就用一个叫做Community_Faqs的表,对应的类叫做FaqInfo。当然你可能想另外加上唯一标示防止将来的CSK中包含这个模块。例如你在ABC公司工作,那么这个表名可以叫做Community_ABCFaqs来减少将来可能出现的名称冲突。 

我们将使用下面的步骤来构建FAQ模块。你可以参照这些步骤构建你自己的模块: 

1. 创建一个新的表(Community_FAqs)来保存新模块的附加字段信息, 

2. 创建用于添加、编辑、和选择一个FAQ的存储过程,另外还要一个从给定范围内读取所有FAQ的存储过程, 

3. 创建一个维护的存储过程通过填充Community_PageTypes和Community_NamedPages表来初始化FAQ模块, 

4. 创建一个FaqInfo的类保存一条FAQ的信息, 

5. 创建一个FaqUtility的类通过访问数据库来调用前面创建的与FAQ相关的存储过程, 

6. 创建从WebControl继承的控件显示不同的字段,这些控件叫做FaqQuestion,FaqInro,FaqAnswer,FaqReference和FaqEditContent 

7. 创建从SkinnedCommunityControl继承的类来包含下一步创建的显示页面背后的逻辑。这些类叫做AddFaq,EditFaq,FaqSection,和Faq 

8. 创建新模块使用的内容显示页面,包括有Faqs_AddFaq.ascx,Faqs_FaqSection.ascx和Faq_Faq.ascx。我们将使用Faqs_AddFaq增加和编辑FAQ。另外你还要在Communityes\Common\Themes\Defalult\Skins\ContentSkins目录下创建默认界面文件,例如Robotics和Professional的主题。 

9. 创建定义该模块页面风格的CSS文件和主题相关的CSS文件,并放到Communities\Common\Themes\Defalult\Styles。 

下面将对每一个步骤作详细的解释。 

Community_Faq表 
大部分模块共有的信息如标题、描述和浏览计数放在Community_ContentPages表中。针对该FAQ模块的附加信息则需要另加一个表存放。例如,我们可以把FAQ的问题放在Page_title字段中,FAQ的介绍放在Page_description字段.另外,我们还需要存放FAQ的答案和附加的参考索引等信息,所以还要创建下面的表: 

CREATE TABLE [Community_Faqs] ( 

[Faq_ContentPageID] [int] NOT NULL , 

[Faq_Answer] [ntext] NOT NULL , 

[Faq_Reference] [ntext] NULL, 

CONSTRAINT [PK_Community_Faqs] PRIMARY KEY CLUSTERED 



[Faq_ContentPageID] 

), 

CONSTRAINT [FK_Community_Faqs_Community_ContentPages] 

FOREIGN KEY 



[Faq_ContentPageID] 

) REFERENCES [Community_ContentPages] ( 

[contentPage_id] 

) ON DELETE CASCADE 

可以看到表的命名方式和数据类型与CSK中的其它模块都是一致的,这样的编程习惯在国内实在是难以做到。 

我们存储FAQ的答案和参考索引的字段是ntext类型,这是为了能支持大数据量的文本(可高达1GB)。还要注意的是Faq_Answer是一个必要字段而Faq_Reference可以是空值。我们的主键(Faq_ContentpageID)关联到Community_Contentpages表中的附加内容。另外一个要注意的细节是外键的约束(级联删除)的使用,保证的数据关联的完整性,并且节省了程序代码。 

CSK使用名为Community_ContentpagesDeleteContentPage(太长了吧)的存储过程删除Community_ContentPages表中的记录。当这个存储过程删除记录时,服务器根据外键自动删除FAQ表中的对应记录。 

增加存储过程 
下面我们要做的是建立增加、编辑、读取单个、读取多个记录的存储过程,在代码中不会出现SQL的代码。从封装性和安全性来看,这样做是一种很好的习惯。 

Community_FaqsAddFaq 
下面是增加一条新的FAQ记录的存储过程。我们不需要给两个表中的每个字段填充数值。例如,我们不需要填充Contentpage_ViewCount列(默认为0),也不需要在ContentPage_DateCommented中填写日期。 

CREATE PROCEDURE Community_FaqsAddFaq 

@communityID int, 
@sectionID int, 
@username nvarchar(50), 
@topicID int, 
@question nvarchar(100), 
@introduction nvarchar(500), 
@metaDescription nvarchar(250), 
@metaKeys nvarchar(250), 
@moderationStatus int, 
@answer ntext, 
@reference ntext 

AS 
DECLARE @ContentPageID int 
DECLARE @pageType int 
SET @pageType = dbo.Community_GetPageTypeFromName('Faq') 
DECLARE @userID int 
SET @userID = dbo.Community_GetUserID(@communityID, @username); 
BEGIN TRAN 
EXEC @ContentPageID = Community_AddContentPage 
@communityID, 
@sectionID, 
@userID, 
@question, 
@introduction, 
@metaDescription, 
@metaKeys, 
@pageType, 
@moderationStatus, 
@topicID 

INSERT Community_Faqs 

Faq_ContentPageID, 
Faq_Answer, 
Faq_Reference 


VALUES 

@ContentPageID, 
@answer, 
@reference 

COMMIT TRAN 
RETURN @ContentPageID 
注意这里使用了两个CSK提供的UDF(User Defined Function),第一个UDF取回页面类型,CSK中每个模块都有唯一的页面类型。第二个存储过程是根据Community和用户名取回UserId。 

由于我们要在两个不同表中插入记录,所以在这里使用了事务来保证操作的原子性。其中往Community_ContentPages表中插入记录是通过调用Communit_AddContentPage这个存储过程来完成的,把FAQ的问题作为@Title参数、介绍作为@Description参数。AddContentPage执行完后返回新增记录的主键值,该数值被用于往Community_Faqs中新增记录。 

在CSK中所有新增记录的存储过程必须返回主键值作为结果。 

得到新的ContentPageID数值在系统的上层是很有用的,这一点我们将在后面写数据访问组件时看到。 

Community_FaqsEditFaq 
这个用于修改已有的FAQ记录的存储过程需要的参数比前面少了很多。因为有些字段在我们增加记录之后就不会再被修改,如区域编号等。其代码如下: 

CREATE PROCEDURE Community_FaqsEditFaq 

@communityID int, 
@contentPageID int, 
@username NVarchar(50), 
@topicID int, 
@question NVarchar(100), 
@introduction NVarchar(500), 
@metaDescription NVarchar(250), 
@metaKeys NVarchar(250), 
@answer Text, 
@reference Text 

AS 
DECLARE @UserID int 
SET @UserID = dbo.Community_GetUserID(@communityID, @username) 
EXEC Community_EditContentPage 
@contentPageID, 
@userID, 
@question, 
@introduction, 
@metaDescription, 
@metaKeys, 
@topicID 
UPDATE Community_Faqs SET 
Faq_Answer = @answer, 
Faq_Reference = @reference 
WHERE Faq_ContentPageID = @contentPageID 
这里我们又一次使用了CSK中的存储过程Community_ContentPages,然后用Update的SQL更新Community_Faqs。与新增FAQ的存储过程相比,这里没有使用事务来保证更新操作的原子性。我们遵循了CSK中已经建立的模式,这里都不使用事务。也许是设计者认为更新操作失败的可能性比新增要小的多,减少不必要的资源锁定来提高系统数据吞吐能力吧。 

Community_FaqsGetFaqs 
接下来要写的存储过程是返回某个社区内给定范围的所有FAQ记录。这个名为Community_GetPagedSortedContent的存储过程,在原来的基础上增加了针对FAQ列的参数,并用IndexID排序。 

CREATE PROCEDURE Community_FaqsGetFaqs 

@communityID int, 
@username NVarchar(50), 
@sectionID int, 
@pageSize int, 
@pageIndex int, 
@sortOrder NVarchar(50) 

AS 
DECLARE @currentDate DATETIME 
SET @currentDate = GetUtcDate() 
SELECT 
null Faq_Answer, 
null Faq_Reference, 
Content.* 
FROM 
dbo.Community_GetPagedSortedContent 

@communityID, 
@username, 
@sectionID, 
@currentDate, 
@sortOrder, 
@pageSize, 
@pageIndex, 
default 
) Content 
ORDER BY 
IndexID 
这个存储过程使用了两个减少代码和将来维护量的技巧。首先,我们使用了Content.*作为返回的结果,这里还使用了CSK中的存储过程。从效率上来说,取回所有的列比让数据库推算哪些列是需要的在返回要更有效率。而且,在这里设计师为了更好的兼顾了维护性。如果将来Community_Contentpages的结构有了修改(如增加列),并不需要修改或测试相关的存储过程。 

第二个要指出的是结果集中的两个空列(Faq_Answer和Faq_Reference)。后面我们将写一个FaqInfo组件来保存来自这个存储过程的多条记录和后面一个存储过程的单条记录。由于我们想使用同一个组件来实现这两种操作,所以我们要填充所有列的信息。因为这两个列可能占用很大空间,而且不会在FAQ的统计列表中显示,所以这里我们就用NULL值来代替。 

Community_FaqsGetFaq 
这个存储过程我们要用来取得一条单独的记录和其它相关的功能。它还需要增加这个页面的访问计数和告诉用户开始读取页面的内容。这些任务是通过执行CSK的Community_ContnetpagesTrackStats过程来完成的,整个过程代码如下: 

CREATE PROCEDURE Community_FaqsGetFaq 

@communityID INT, 
@username NVarchar(50), 
@contentPageID int 

AS 
DECLARE @userID INT 
SET @userID = dbo.Community_GetUserID(@communityID, @username) 
-- Update ViewCount and HasRead Stats 
EXEC Community_ContentPagesTrackStats @userID, @contentPageID 
DECLARE @currentDate DATETIME 
SET @currentDate = GetUtcDate() 
SELECT 
Faq_Answer, 
Faq_Reference, 
Content.* 
FROM 
dbo.Community_GetContentItem( 
@communityID, 
@userID, 
@currentDate) Content 
JOIN Community_Faqs (nolock) 
ON ContentPage_ID = Faq_ContentPageID 
WHERE 
ContentPage_ID = @contentPageID 
注意由于要显示明细,这里我们读取了Faq_Answer和Faq_Reference列的实际值。这里我们在join连接Community_Faqs表时使用了nolock的选项,这将允许我们执行脏读取而不会有任何警告提示。(和数据库锁定机制有关) 

初始化FAQ模块 
每一个社区模块都有一个维护用的存储过程用来填充数据库中与模块运行关的信息设置内容。譬如我们要通过在Community_PageTypes中增加记录来注册页面类型:一条信息是关于显示FAQ列表的页面,另一条是显示单个FAQ详细信息的页面。为了遵循CSK一致的命名规则我们把这个存储过程叫做Community_MaintenanceInitializeFaqs,其中部分代码摘录如下: 

IF NOT EXISTS (SELECT * FROM Community_PageTypes 
WHERE pageType_Name='Faq Section') 
BEGIN 
INSERT Community_PageTypes 

pageType_name, 
pageType_description, 
pageType_pageContent, 
pageType_IsSectionType, 
pageType_ServiceSelect 

VALUES 

'FAQ Section', 
'Contains FAQs in a question and answer style format', 
'ASPNET.StarterKit.Communities.Faqs.FaqSection', 
1, 
'Community_FaqsServiceSelect' 

END 
ELSE 
PRINT 'WARNING: The FAQ Module has already been registered.' 
由于CSK会缓存Community_NamePages的数据所以只会读取数据一次。如果你在这些表中作了修改,需要重新启动Web程序来使修改生效。 

这个维护的存储过程还需要注册一些新模块重要使用的静态显示页面,这里包括新增和编辑的页面,并且你必须使用与你将要创建的ASPX文件完全相同的名称作为注册信息。 

下面是代码中的相关部分: 

IF NOT EXISTS (SELECT * FROM Community_NamedPages 
WHERE namedPage_Path='/Faqs_AddFaq.aspx') 
BEGIN 
INSERT Community_NamedPages 

namedPage_name, 
namedPage_path, 
namedPage_pageContent, 
namedPage_title, 
namedPage_description, 
namedPage_sortOrder, 
namedPage_isVisible, 
namedPage_menuID 

VALUES 

'AddFaq', 
'/Faqs_AddFaq.aspx', 
'ASPNET.StarterKit.Communities.Faqs.AddFaq', 
'Add FAQ', 
'Enables users to add a new FAQ', 
0, 
1, 


END 
ELSE 
PRINT 'WARNING: /Faqs_AddFaq.aspx has already been registered 
as a NamedPage.' 
其中namedPage_pageContent参数是CSK调用该静态页面时要使用的code-behind类的完整路径:ASPNET.StarterKit.Communities.Faqs.AddFaq。 

FAQ组件 
FaqInfo 
所有FAQ模块中的C#代码都将放在Engine\Modules\Faqs目录中。首先在一个Components的目录内增加helper类。每个CSK的模块都应该放在不同的名称空间,即在ASPNET.StarterKit.Communities后再加上模块名称作为限定。 

using System; 
using System.Data.SqlClient; 
namespace ASPNET.StarterKit.Communities.Faqs 

public class FaqInfo : ContentInfo 

public FaqInfo(SqlDataReader dr) : base(dr) 

if(dr["Faq_Answer"] != DBNull.Value) 

_answerText = (string)dr["Faq_Answer"]; 

if(dr["Faq_Reference"] != DBNull.Value) 

_referenceText = (string)dr["Faq_Reference"]; 


public string AnswerText 

get { return _answerText; } 
set { _answerText = value; } 

public string ReferenceText 

get { return _referenceText; } 
set { _referenceText = value; } 

public string QuestionText 

get { return base.Title; } 
set { base.Title = value; } 

public string IntroText 

get { return base.BriefDescription; } 
set { base.BriefDescription = value; } 

private string _answerText; 
private string _referenceText; 


FaqInfo类初始化时需要一个SqlDataReader类的实例。在我们下一个类中将会有数据访问的代码来创建一个SqlDataReader。 

FaqUtility 
按照CSK中的编码惯例,我们将把数据访问的过程都放在一个utility类的静态方法中。对应于每一个与FAQ相关的存储过程都会有一个静态方法来调用。(除了用于系统维护的存储过程之外,因为只有在站点初始化时才会用到它)。每个静态方法都需要与之相对应的参数用于传递到存储过程中去。 

下面是AddFaq的方法: 

public static int AddFaq( 
string username, 
int sectionID, 
int topicID, 
string question, 
string introduction, 
string answer, 
string reference, 
int moderationStatus) 

SqlConnection conPortal = new SqlConnection( 
CommunityGlobals.ConnectionString); 
SqlCommand cmdAdd = new SqlCommand( 
"Community_FaqsAddFaq", conPortal); 
cmdAdd.CommandType = CommandType.StoredProcedure; 
cmdAdd.Parameters.Add("@RETURN_VALUE", 
SqlDbType.Int).Direction = 
ParameterDirection.ReturnValue; 
cmdAdd.Parameters.Add("@communityID", 
CommunityGlobals.CommunityID); 
cmdAdd.Parameters.Add("@sectionID", sectionID); 
cmdAdd.Parameters.Add("@username", username); 
cmdAdd.Parameters.Add("@topicID", topicID); 
cmdAdd.Parameters.Add("@question", question); 
cmdAdd.Parameters.Add("@introduction", introduction); 
cmdAdd.Parameters.Add("@metaDescription", 
ContentPageUtility.CalculateMetaDescription(introduction)); 
cmdAdd.Parameters.Add("@metaKeys", 
ContentPageUtility.CalculateMetaKeys(introduction)); 
cmdAdd.Parameters.Add("@moderationStatus", moderationStatus ); 
cmdAdd.Parameters.Add("@answer", SqlDbType.NText); 
cmdAdd.Parameters.Add("@reference", SqlDbType.NText); 
cmdAdd.Parameters["@answer"].Value = answer; 
cmdAdd.Parameters["@reference"].Value = reference; 

conPortal.Open(); 
cmdAdd.ExecuteNonQuery(); 
int result = (int)cmdAdd.Parameters["@RETURN_VALUE"].Value; 
SearchUtility.AddSearchKeys(conPortal, sectionID, result, 
question, introduction); 
conPortal.Close(); 
return result; 

请注意AddFaq方法内部还调用了SearchUtility类的方法产生了有关内容的查询关键字,另外它还返回了新增对象的唯一标示符。这里CSK需要做的一个改进是在代码中增加 try catch finally来保证执行数据库连接的Close方法。虽然发生异常的可能性很小,但对于一个大容量的社区网站来说,你无法承受浪费浪费数据库连接可能带来的性能下降。 

在FaqUtility中的另外两个方法是GetFaqs和GetFaqInfo。GetFaqs在SqlDataReader中循环读取记录并返回一个FaqInfo对象的ArrayList,而GetFaqInfo只返回由数据库中一条记录填充的FaqInfo对象。 

public static ContentInfo GetFaqInfo(string username, 
int contentPageID) 

FaqInfo faq = null; 
SqlConnection conPortal = new SqlConnection( 
CommunityGlobals.ConnectionString); 
SqlCommand cmdGet = new SqlCommand( 
"Community_FaqsGetFaq", conPortal); 
cmdGet.CommandType = CommandType.StoredProcedure; 
cmdGet.Parameters.Add( 
"@communityID", CommunityGlobals.CommunityID); 
cmdGet.Parameters.Add("@username", username); 
cmdGet.Parameters.Add("@contentPageID", contentPageID); 
conPortal.Open(); 
SqlDataReader dr = cmdGet.ExecuteReader(); 
if (dr.Read()) 
faq = new FaqInfo(dr); 
conPortal.Close(); 
return faq; 

public static ArrayList GetFaqs(string username, int sectionID, 
int pageSize, int pageIndex, 
string sortOrder) 

SqlConnection conPortal = new 
SqlConnection(CommunityGlobals.ConnectionString); 
SqlCommand cmdGet = new SqlCommand( "Community_FaqsGetFaqs", 
conPortal); 
cmdGet.CommandType = CommandType.StoredProcedure; 
cmdGet.Parameters.Add("@communityID", 
CommunityGlobals.CommunityID); 
cmdGet.Parameters.Add("@username", username); 
cmdGet.Parameters.Add("@sectionID", sectionID); 
cmdGet.Parameters.Add("@pageSize", pageSize); 
cmdGet.Parameters.Add("@pageIndex", pageIndex); 
cmdGet.Parameters.Add("@sortOrder", sortOrder); 

ArrayList faqs = new ArrayList(); 
conPortal.Open(); 
SqlDataReader dr = cmdGet.ExecuteReader(); 
while (dr.Read()) 
faqs.Add(new FaqInfo(dr)); 
conPortal.Close(); 
return faqs; 

这里有比较重要的一点是GetFaqInfo使用了以上特定的参数列表.在类框架中将通过其它的代理类来调用此方法所以参数必须一致.在后面我们写内容页面时你将会看到它是如何工作的。 
我们的数据访问层现在已经完成了。如果你按照这个方式创建了模块,你已经可以开始编译和解决错误了。你可以考虑写一个驱动页面来试验以上FaqUtility 中的4个静态方法,并检查在表Community_Faqs 和 Community_ContentPages中的结果是否正确。 
FAQ WebControls 
在CSK中把显示内容的功能分成几个较小的控件。例如在Engine\Framework\ContentPages\Controls目录下,你会看到一个显示标题的控件(文件名Title.cs),还要内容摘要的控件(BriefDescription.cs)。我们需要为FAQ再增加两个特定的控件,一个显示答案和参考,另一个为授权用户显示编辑内容的链接。 

FaqAnswer 和 FaqReference 
在这一层的所有控件都是从.NET Framework的WebControl控件继承而来。我们只需简单地设置控件的CssClass属性,从当前HttpContext中获取要显示的文本,然后重载RenderContents方法写出该文本。 

下面的控件将被创建在Engine\Module\Faqs\Controls中,用来显示FAQ的答案和参考: 

using System; 
using System.Web; 
using System.Web.UI; 
using System.Web.UI.WebControls; 
using ASPNET.StarterKit.Communities.Faqs; 
using System.ComponentModel; 
namespace ASPNET.StarterKit.Communities 

[Designer(typeof(ASPNET.StarterKit.Communities.CommunityDesigner))] 
public class FaqAnswer : WebControl 

public FaqAnswer() : base() 

CssClass = "faqAnswerText"; 
if(Context != null) 

Object faqInfo = Context.Items["ContentInfo"]; 
if(faqInfo != null) 

_text = ((FaqInfo)faqInfo).AnswerText; 



public string Text 

get { return _text; } 
set { _text = value; } 

override protected void RenderContents( 
HtmlTextWriter writer) 

SectionInfo objSectionInfo = 
(SectionInfo)Context.Items["SectionInfo"]; 
writer.Write( 
CommunityGlobals.FormatText( 
objSectionInfo.AllowHtmlInput, 
objSectionInfo.ID, _text)); 

private string _text; 


请注意我们前面的控件是在ASPNET.StarterKit.Communities.Faqs命名空间中,但是被放在上一层命名空间中。这是因为CSK所使用的界面文件都指向ASPNET.StarterKit.Communities。下面这行标记就为以上控件指定了界面: 

<community:FaqAnswer Runat="Server" ID="Answer1" NAME="Answer1"/> 

FaqRefrence控件看上去与FaqAnswer非常相似。都重载了RnderContents方法并使用CommunityGlobals类对输出文本进行转换和格式化。 

FaqEditContent 
每一个CSK模块都会使用一个从EditContent继承而来的控件来为授权用户显示编辑链接,使用户能够添加、删除、移动、评注和控制内容。我们所要做的只是设置合适的URL属性。在基类中的逻辑判断来决定何时显示正确的链接。所有的动作在类的创建过程完成如下: 

public FaqEditContent() 

if (Context != null) 

PageInfo pageInfo = (PageInfo)Context.Items["PageInfo"]; 
int contentPageID = pageInfo.ID; 
AddUrl = "Faqs_AddFaq.aspx"; 
EditUrl = String.Format( 
"Faqs_EditFaq.aspx?id={0}", 
contentPageID); 
DeleteUrl = String.Format( 
"ContentPages_DeleteContentPage.aspx?id={0}", 
contentPageID); 
MoveUrl = String.Format( 
"ContentPages_MoveContentPage.aspx?id={0}", 
contentPageID); 
CommentUrl = String.Format( 
"Comments_AddComment.aspx?id={0}", 
contentPageID); 
ModerateUrl = "Moderation_ModerateSection.aspx"; 


Content 类 
在传统的ASP.NET程序范例中,内容类都是code-behind文件。由于CSK允许用户进行更高层的定制所以在这方面显得略有不同,我们将不能用IDE来保持窗口界面和代码保持同步。他们之间存在一个不确定的关联,因为每个代码要支持不同界面的多个窗口文件。所以我们要手动维护页面控件和事件绑定。这个任务虽不困难但需要编程时对控件名称等细节更加注意。 

我们一共有4个内容类需要实现用: 

l Faq:显示一个FAQ记录 

l FaqSection:显示一个FAQ列表 

l AddFaq:输入FAQ的内容 

l EditFaq:修改FAQ的内容 

写一个内容类所需要的代码数量可能相差很大。如果使用CSK中的ContentItemPage 和ContentListPage类,我们只需要写很少的代码就可以显示FAQ和FAQ列表,下面就先看看这两个类 

Faq 和 FaqSection 
public class FaqSection : ContentListPage 

public FaqSection() : base() 

SkinFileName = _skinFileName; 
GetContentItems = _getContentItems; 

string _skinFileName = "Faqs_FaqSection.ascx"; 
GetContentItemsDelegate _getContentItems = 
new GetContentItemsDelegate(FaqUtility.GetFaqs); 

在这里,我们需要设定SkinFileName属性来指定实际的界面文件名。所有从SkinnedCommunityControl(ContentListPage的基类)继承的控件都需要这一步骤装载合适的文件。 

在写FaqUtility类的数据访问方法时,我们提到过需要建立一个代理方法由GetContentItemsDelegate调用。在基类中将使用这一代理方法在指定区域内读取和显示FAQ。按照此模式,在Faq类初始化时会指定Faqs_Faq.ascx为界面文件,并把代理方法指向FaqUtility.GetFaq以供需要时调用。 

AddFaq和EditFaq 
这两个类的实现有一定的难度。因为用来新增和修改内容的方法在各模块中都可能用到,并且没有基类可以继承以减少工作量。反而是需要我们寻找适合该模块的控件去读取和设置数值,并响应用户事件触发相关的数据访问过程。 

在实现这些功能之前,我们先要规划页面上用于输入和显示的控件: 

l TextBox:用于FAQ问题 

l TextBox:用于FAQ介绍 

l TopicPicker:用于FAQ列表中选取主题 

l HtmlTextBox:用于FAQ答案 

l HtmlTextBox:用于FAQ参考 

另外,我们还需要5个对应控件用于显示FAQ,如前面写好的FaqAnswer用来显示答案。下面是EditFaq的创建方法示例: 

public EditFaq() : base() 

SkinFileName = _skinFileName; 
SectionContent = _sectionContent; 

this.SkinLoad += new SkinLoadEventHandler(SkinLoadFaq); 
this.Preview += new PreviewEventHandler(PreviewFaq); 
this.Submit += new SubmitEventHandler(SubmitFaq); 

在该方法中初始化了用于显示的界面文件名和SectionContent属性(后面将有说明),然后把基类中的事件与响应方法建立了关联。 

ContentEditPage这一事件响应函数中要包含以下逻辑:装载界面文件,处理预览按钮的点击和提交按钮的点击。 

void SkinLoadFaq(Object s, SkinLoadEventArgs e) 

txtQuestion = (TextBox)GetControl(e.Skin, "txtQuestion"); 

// continue initializing all controls with GetControl . . . 

正如我们在前面章节所讨论的那样,CSK动态装载界面(ASCX)文件显示特定主题。如果你需要通过程序与界面上的控件交互操作,首先要得到该控件实例的引用,如我们编辑FAQ时需要得到TextBox对象中的内容。你可以通过SkinnedComunityControl基类中的GetControl或GetOptionalControl方法来实现,在这里只有显示问题的TextBox需要被引用: 

protected override void OnLoad(EventArgs e) 

if (!Page.IsPostBack) 

ContentPageID = Int32.Parse( 
Context.Request.QueryString["id"]); 

FaqInfo faqInfo = 
(FaqInfo)FaqUtility.GetFaqInfo( 
objUserInfo.Username, ContentPageID); 

EnsureChildControls(); 
txtAnswer.Text = faqInfo.AnswerText; 
dropTopics.SelectedTopicID = faqInfo.TopicID; 
txtIntro.Text = faqInfo.IntroText; 
txtQuestion.Text = faqInfo.QuestionText; 
txtReference.Text = faqInfo.ReferenceText; 


当页面加载时,我们需要从数据库中获取已有FAQ的信息。CSK会在查询参数中传递该内容的ID,然后我们取出ID再传给FaqUtility类中的GetFaqInfo方法。一旦得到了FAQ对象,就在页面上显示其信息。 

void PreviewFaq(Object s, EventArgs e) 

if(objSectionInfo.EnableTopics) 
topicPreview.Name = dropTopics.SelectedItem.Text; 
questionPreview.Text = txtQuestion.Text; 
introductionPreview.Text = txtIntro.Text; 
answerPreview.Text = txtAnswer.Text; 
referencePreview.Text = txtReference.Text; 

当用户点击预览按钮后,我们要把所有编辑的内容转移到预览控件中,在那里编辑内容按照定义的风格被渲染和显示。使作者能更有效观察和设计页面在实际运行时的外观。ContentEdit类会保证作者看到的页面中有预览面板并观察结果。 

void SubmitFaq(Object s, EventArgs e) 

if (Page.IsValid) 

// Get Topic 
int topicID = -1; 
if (objSectionInfo.EnableTopics) 
topicID = Int32.Parse(dropTopics.SelectedItem.Value); 

FaqUtility.EditFaq( 
objUserInfo.Username, 
objSectionInfo.ID, 
ContentPageID, 
topicID, 
txtQuestion.Text, 
txtIntro.Text, 
txtAnswer.Text, 
txtReference.Text); 

Context.Response.Redirect(CommunityGlobals.CalculatePath( 
String.Format("{0}.aspx", ContentPageID))); 


SumbitFaq事件响应函数通过FaqUtility类把内容更新到数据库中。一旦更新结束,我们发给用户一个提示并离开编辑页面,然后重定向到浏览页面查看已更新的FAQ。 

int ContentPageID 

get { return (int)ViewState["ContentPageID"]; } 
set { ViewState["ContentPageID"] = value; } 

TextBox txtQuestion; 
TopicPicker dropTopics; 
TextBox txtIntro; 
HtmlTextBox txtAnswer; 
HtmlTextBox txtReference; 
DisplayTopic topicPreview; 
Title questionPreview; 
BriefDescription introductionPreview; 
FaqAnswer answerPreview; 
FaqReference referencePreview; 
string _skinFileName = "Faqs_AddFaq.ascx"; 
string _sectionContent = 
"ASPNET.StarterKit.Communities.Faqs.FaqSection"; 

AddFaq页面与EditFaq的页面基本类似,只是要增加用不同页面显示内容的选项。 

FAQ Page Content Skins 
我们的FAQ模块需要有3个界面: 

l 显示单个FAQ详细内容的界面 

l 显示多个FAQ列表的界面 

l 新增或修改一个FAQ的界面 

我们至少要在默认主题目录中创建这3个界面文件。如果希望有不同界面还可以继续增加其它的主题界面。对于新增或修改FAQ的页面可能如下: 


这里要确定界面文件名与内容页面类中SkinFileName属性的赋值吻合。还要注意控件名称,必须与调用类中GetControl方法所用参数名相同。 

最简便的方法是从一个已有的界面窗口开始创建界面,因为你还要保持与基类的控件相同。应该还记得我们的AddFaq类是从ContentAddPage继承而来,他需要界面上有一些特定的控件,如名为pnlFormde面板和名为btnAdd的按钮。我们先看以下Faq_AddFaq的界面: 

<%@ Control %> 
<%@ Register TagPrefix="community" 
Namespace="ASPNET.StarterKit.Communities" 
Assembly="ASPNET.StarterKit.Communities" %> 
<community:SectionTitle 
CssClass="Form_Title" 
Runat="Server" 
ID="Sectiontitle1" 
NAME="Sectiontitle1"/> 
<p class="Form_Description"> 
Use this form to add or edit an FAQ. 
</p> 
<asp:Panel id="pnlForm" Runat="Server"> 
<TABLE cellSpacing="0" cellPadding="3" width="520" 
class="Form_Table"> 
<TR> 
<TD class="Form_SectionRow"> 
FAQ Form 
</TD> 
</TR> 
<tr class="Form_LabelRow"> 
<td > 
<span class="Form_LabelText">Question:</span> 
<asp:RequiredFieldValidator 
ControlToValidate="txtQuestion" 
Text="(Required)" 
Runat="Server" 
ID="Requiredfieldvalidator1" 
NAME="Requiredfieldvalidator1"/><br> 
<asp:TextBox id="txtQuestion" 
CssClass="Form_Field" 
columns="40" 
runat="server"> 
</asp:TextBox> 
</td> 
</tr> 
... 
用于显示内容的界面相对容易构建,你只需按照合适的位置排列控件即可。下面是显示单独FAQ的界面: 

<%@ Control %> 
<%@ Register 
TagPrefix="community" 
Namespace="ASPNET.StarterKit.Communities" 
Assembly="ASPNET.StarterKit.Communities" %> 
<table width="100%" cellspacing="0" cellpadding="11" 
class="Faq_Table"> 
<tr> 
<td class="Faq_IntroCell">> 
<div align="right"> 
<community:DisplayTopic Runat="Server" 
ID="Displaytopic1" 
NAME="Displaytopic1" /> 
</div> 
<community:Title Runat="Server" ID="Title1" 
NAME="Title1" /> 
<br> 
<br> 
Posted by 
<community:Author CssClass="Faq_AuthorLink" 
Runat="Server" ID="Author1" 
NAME="Author1" /> 
on 
<community:DateCreated Runat="Server" 
ID="Datecreated1" 
NAME="Datecreated1" /> 
<br> 
<br> 
<community:BriefDescription Runat="Server" 
ID="Introduction1" 
NAME="Introduction1" /> 
</td> 
</tr> 
<tr> 
<td class="Faq_AnswerCell"> 
<community:FaqAnswer Runat="Server" 
ID="Answer1" NAME="Answer1" /> 
</td> 
</tr> 
<tr> 
<td class="Faq_AnswerCell"> 
<community:FaqReference Runat="server" 
ID="Reference1" 
Name="Reference1" /> 
</td> 
</tr> 
<tr> 
<td class="Faq_BodyCell"> 
<br> 
<community:Rating SubmitText="Rate Item" 
Runat="Server" ID="Rating1" 
NAME="Rating1" /> 
</td> 
</tr> 
</table> 

<table width="100%" cellspacing="0" cellpadding="11"> 
<tr> 
<td> 
<div class="Content"> 
<community:Notify 
Text="Notify me when a new comment is posted" 
Runat="Server" ID="Notify1" NAME="Notify1" /> 
<p> 
<community:Comments Runat="Server" 
ID="Comments1" 
NAME="Comments1" /> 
<p> 
<community:FaqEditContent 
CommentText="Add Your Comment" 
EditText="Edit this FAQ" 
DeleteText="Delete this FAQ" 
Runat="Server" ID="Faqeditcontent1" 
NAME="Faqeditcontent1" /> 
</div> 
</td> 
</tr> 
</table> 
在这个界面中,我们使用了自己创建的Web控件(包括只对特定用户显示的FaqEditContent链接)。有了这些界面文件,构建新模块就只剩下最后一步了。 

模块风格 
在创建新界面和Web控件时,要明确你在代码中放置的CSS名称。在模块中对界面有新的要求时我们要修改这些CSS文件。由于CSK中不提供默认的CSS设置,所以你需要对每个模块进行定义。 

系统整合 
现在,你可以启动和测试新的模块。运行的结果就像下面的图形: 


总结 
在CSK中创建新的模块需要与已有的架构进行交互,所以首先对CSK的整体架构进行了解。这篇文章对CSK的架构做了介绍,并详细介绍如何增加一个自定义的新模块,以及实现过程中所需要遵循的编程模式和命名风格。