65.9K
CodeProject 正在变化。 阅读更多。
Home

JumpyForum: 受 Code Project 论坛 / 讨论 /留言板启发

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (95投票s)

2006年12月15日

6分钟阅读

viewsIcon

457060

downloadIcon

5767

一个非常简单的论坛,其功能类似于 Code Project 讨论板,用户可以在树状视图中查看多条消息,使用 C#, ASP.NET, SQL Server 和 JavaScript 实现。(现在符合 W3C HTML 4.01 标准)

Jumpy Forum

新窗口 在线演示 - Jumpy Forum

引言

本文介绍了一个受 Code Project 论坛启发并与之类似的讨论板。我相信,你们都会同意 Code Project 上的讨论板是最好的之一。我曾尝试在 CP 和其他地方搜索类似的项目,但所有给出的示例都是用经典 ASP/PHP 编写的,而且非常复杂。我的努力是利用 ASP.NET、JavaScript 和 SQL Server 创建一个非常简单易懂的论坛,并具备相同的功能。

在你们兴致勃勃之前,我只想提醒你们关于 JumpyForum 的优势,这是一个非常简单的(可以说是初学者级别)版本,你们可能需要考虑更多才能真正拥有一个具有 CP 级别质量、可扩展性和耐用性的论坛。JumpyForum 只会为您提供一个简单且可行的起点,之后,广阔的天地由你们去探索。

目标是创建一个像 Code Project 一样好(甚至更好)的简单且可扩展的讨论板/论坛系统

  1. 可用性:用户可以添加类型为/新闻/一般/笑话和问题类型的评论。其他用户可以以一般/新闻/问题/笑话或回复的形式进行回复
  2. 丰富的显示:论坛显示应保留层次结构和每条评论后的日期/时间
  3. 可重用性:论坛的全部功能应易于插入任何表以供重用
  4. 一致性:如果一条消息被删除,所有后续的子消息也应被层级删除
  5. 可扩展性:论坛应易于扩展到不同的文章或不同的论坛
  6. 性能:评论的显示不应花费太多时间

* 免责声明:它看起来确实像 Code Project 论坛,但只声称这是实现它的方式之一。

实际应用

为了引起你们的兴趣,这是完成后的样子。在此处查看 演示

JumpyForum in Action

假设

这些是最初的想法,但为了使其成为一个通用、可重用且可扩展的模块,我做了一些假设

  • 我们在 SQL Server 中有一个表 CP_Forum_Comments(参见下面的截图)
  • 该表将包含如图所示的字段,包括 ArticleID(文章表的外部键)。这样做的想法是为每篇文章提供不同的论坛。Article ID 可以更改为 ForumId 或任何你想要的名称。你可以根据自己的需求进行完全扩展。
  • 其他字段基本不言自明。ParentID 是树状视图中评论的父 ID(回复的消息的 ID)。
  • Indent 字段是归因于 JumpyForum 中消息缩进的关键字段。

评论表结构 CP_FORUMS_COMMENT
(创建表 SQL 查询已附加在源代码中)

Rating Table

Actions

这些是使整个模块工作的操作

  • 数据访问
  • 论坛显示和用户界面
  • 论坛的层级存储过程(核心)
  • 添加评论
  • 添加回复
  • 删除评论并层级删除(此功能目前在 CodeProject 论坛中不存在,这就是为什么有时我们会看到孤立的消息,如果父消息被删除)

数据访问类

有一个标准的DataAccess类,clsDataAccess.cs,它处理所有数据相关的操作。

代码

这里的主要函数是 getForumData,它调用存储过程。为了让你一窥数据访问方法,我只保留了其他函数的名称。

 using
       public class clsDataAccess { public
       clsDataAccess()

       { } SqlConnection mycon = new
       SqlConnection( ConfigurationSettings.AppSettings["ConnectionString"]);
        //Opens database connection in SQL SERVER

       public bool openConnection()
       public void closeConnection()

       public SqlDataReader getForumData(int ArticleId)
        {
            SqlCommand sqlCommand = new SqlCommand();
            sqlCommand.CommandType= CommandType.StoredProcedure;
            sqlCommand.CommandText = "ShowHierarchyForum";
            SqlParameter newSqlParam = new SqlParameter();
            newSqlParam.ParameterName = "@ArticleId";
            newSqlParam.SqlDbType = SqlDbType.Int ;
            newSqlParam.Direction = ParameterDirection.Input;
            newSqlParam.Value = ArticleId;
            sqlCommand.Parameters.Add(newSqlParam);

            SqlParameter newSqlParam2 = new SqlParameter();
            newSqlParam2.ParameterName = "@Root";
            newSqlParam2.SqlDbType = SqlDbType.Int ;
            newSqlParam2.Direction = ParameterDirection.Input;
            newSqlParam2.Value = 0;
            sqlCommand.Parameters.Add(newSqlParam2);
            sqlCommand.Connection=mycon;

            SqlDataReader myr = sqlCommand.ExecuteReader
                    (CommandBehavior.CloseConnection);
            return myr;
        }
    }
}

为了显示,我们需要以下图像。我从 CodeProject 拿来的,虽然这些图像在互联网上也很容易找到。

论坛显示和用户界面

首先,我们假设我们正在以我们需要的确切顺序获取所有记录(由嵌套的层级存储过程处理),并在论坛页面上显示它们。考虑论坛的动态显示和跳转功能。

我们在这里做的是显示与文章相关的所有记录,然后隐藏除标题外的所有记录。当用户点击一条消息时,该特定消息将被显示,其他消息将被隐藏。一个简单的 JavaScript 函数可以通过 on/off 状态来实现这一点。这是神奇的 JavaScript:

function OnOffPost(e)
{
   if ( !e ) e = window.event;
   var target = e.target ? e.target : e.srcElement;

   while ( target && target.id != 'LinkTrigger' )
      target = target.parentNode;
   if ( !target || target.id != 'LinkTrigger' )
      return;

   if (Selected)
   {
      var body = document.getElementById(Selected + "ON");
      if (body)
         body.style.display = 'none';
      var head = document.getElementById(Selected + "OFF");
      if (head)
         head.bgColor = '#EDF8F4';
   }

   if (Selected == target.name) // just collapse

      Selected="";
   else
   {
      Selected = target.name;
      var body = document.getElementById(Selected + "ON");
      if (body)
      {
         if (body.style.display=='none')
            body.style.display='';
         else
            body.style.display = 'none';
      }
      var head = document.getElementById(Selected + "OFF");
      if (head)
         head.bgColor = '#B7DFD5';

      if ( body && head && body.style.display != 'none' )
      {
         document.body.scrollTop = FindPosition(head, "Top") - 
                `    document.body.clientHeight/10;
         OpenMessage(target.name, true);
      }
   }

   if ( e.preventDefault )
      e.preventDefault();
   else
      e.returnValue = false;
   return false;
} 

更多 JavaScript 函数用于打开消息并查找消息的位置

function OpenMessage(msgID, bShowTop) {
   var msgHeader = document.getElementById(msgID + "OFF");
   var msgBody = document.getElementById(msgID + "ON");

   var MyBody = document.body;
   var top = FindPosition(msgHeader, 'Top');
   var bottom = FindPosition(msgBody, 'Top') + msgBody.offsetHeight;

   if ( MyBody.scrollTop > top && !bShowTop)
      MyBody.scrollTop = top - document.body.clientHeight/10;
   if ( MyBody.scrollTop+MyBody.clientHeight < bottom )
      MyBody.scrollTop = bottom-MyBody.clientHeight;
   if ( MyBody.scrollTop > top && bShowTop)
      MyBody.scrollTop = top - document.body.clientHeight/10;
}

function FindPosition(i,which)
{
   iPos = 0
   while (i!=null)
   {
      iPos += i["offset" + which];
      i = i.offsetParent;
   }
   return iPos
}

JavaScript On/Off 函数和 JavaScript 本身一样古老,不需要太多解释。你只需要将元素 ID 作为参数传递给点击事件,该事件会调用方法来更改该 ID 的显示。它已包含在源代码中。

用户界面中显示的数据

为了在页面上显示存储过程结果的所有记录,我们需要两样东西:一个 reader.read 循环和一个 stringbuilder 来创建带有所有消息的整个页面。以下是我们处理 timemessage typeindent 的方式,其余的只是与 reader 中的数据一起附加到 string builder。

while (myReader.Read())
{
DateTime dt1 = DateTime.Now;
DateTime dt2 = Convert.ToDateTime(myReader["DateAdded"].ToString());
TimeSpan ts = dt1.Subtract(dt2);
string mytimeago = "";

if (Convert.ToInt32(ts.TotalDays) !=0) mytimeago = "" + 
        Math.Abs(Convert.ToInt32(ts.TotalDays))+ " Days  ago";
else {
     if            // dealing with the time 

    ((Convert.ToInt32(ts.TotalMinutes) < 5)&&
    (Convert.ToInt32(ts.TotalHours)==0))
    {  mytimeago="Just   Posted"; }
        else
        if ((Convert.ToInt32(ts.TotalMinutes) >  5)&&
        (Convert.ToInt32(ts.TotalHours)==0))
            {
            mytimeago  =  Convert.ToInt32(ts.TotalMinutes) % 60 + " Mins ago";
            }
            else if(Convert.ToInt32(ts.TotalHours)!=0)
            {
            mytimeago  = "" + Convert.ToInt32(ts.TotalHours) + "
            Hours " + Convert.ToInt32(ts.TotalMinutes) % 60 + " Mins ago";
            }
            else
            {
            mytimeago  =  Convert.ToInt32(ts.TotalMinutes) % 60 + " Mins ago";
            }


    }
    // Indentation of the image and the new image.gif

    string newimg ="";
    if (String.Compare(mytimeago,"Just Posted")==0)
    newimg = "<'JumpyForum/new.gif' border=0>";
    int myindent = 4;
    if (Convert.ToInt32(myReader["Indent"])<=4)
        myindent = 16 * Convert.ToInt32(myReader["Indent"]);
    else if (Convert.ToInt32(myReader["Indent"])<=8)
        myindent = 15 * Convert.ToInt32(myReader["Indent"]) ;
    else if (Convert.ToInt32(myReader["Indent"])<=16)
        myindent = 14 * Convert.ToInt32(myReader["Indent"]) ;
    else if (Convert.ToInt32(myReader["Indent"])<=20)
        myindent = Convert.ToInt32(13.5 * 
                Convert.ToDouble(myReader["Indent"]));
    else if (Convert.ToInt32(myReader["Indent"])<=24)
        myindent = 13 * Convert.ToInt32(myReader["Indent"]);
    else if (Convert.ToInt32(myReader["Indent"])<=28)
        myindent = Convert.ToInt32(12.7 * 
                Convert.ToDouble(myReader["Indent"]));
    else if (Convert.ToInt32(myReader["Indent"])<=32)
        myindent =  Convert.ToInt32(12.4 * 
                Convert.ToDouble(myReader["Indent"]));


     // Message Type           

    if (Convert.ToInt32(myReader["CommentType"].ToString())==1)
        sb.Append("<'JumpyForum/general.gif' align=absMiddle> </TD>");
    if (Convert.ToInt32(myReader["CommentType"].ToString())==2)
        sb.Append("<'JumpyForum/info.gif' align=absMiddle> </TD>");
    if (Convert.ToInt32(myReader["CommentType"].ToString())==3)
        sb.Append("<'JumpyForum/answer.gif' align=absMiddle> </TD>");
    if (Convert.ToInt32(myReader["CommentType"].ToString())==4)
        sb.Append("<'JumpyForum/question.gif' align=absMiddle> </TD>");
    if (Convert.ToInt32(myReader["CommentType"].ToString())==5)
        sb.Append("<'JumpyForum/game.gif' align=absMiddle> </TD>");
}

现在让我们跳转到论坛的主要部分,即显示层级数据。

论坛的层级存储过程

这是完成此功能的嵌套层级存储过程。我敢打赌,嵌套层级存储过程可能相当令人头疼,但了解细节是有益的。我们传递两个参数:

  1. Articleid 以获取该文章的所有消息,以及
  2. Rootid 消息的主键,以获取该消息的所有子消息。

此处有一个“注意事项”,嵌套层级存储过程不会超过 32 个子级别。我们在这里做的是在一个嵌套循环中创建一个临时表,然后在最后抛出临时表的记录。存储过程已附加到源代码中。

CREATE                          PROC dbo.ShowHierarchyForum
(
    @Root int,
    @ArticleId int
)
AS
BEGIN

if not exists (select name from [tempdb].[dbo].[sysobjects] 
                where name like '#YourLocalTempTable%')
create table #YourLocalTempTable (Id int, ParentId int,ArticleId int, 
                            Title nVarchar(250),
              username nvarchar(50),UserEmail nvarchar(50), 
                    Description nvarchar(2000),Indent int,
              DateAdded datetime,UserProfile nvarchar(100), 
                    CommentType tinyint)


    SET NOCOUNT ON
    DECLARE @CID int, @PID int, @Title varchar(250)

    insert into #YourLocalTempTable SELECT CP_FORUM_Comments.Id , 
                            ParentId ,ArticleId ,
                Title,username ,UserEmail ,Description ,Indent ,
                    DateAdded ,UserProfile, CommentType
                from CP_FORUM_Comments WHERE ID = @Root and ArticleId = 
                                @ArticleId

    SET @CID = (SELECT MAX(ID) FROM CP_FORUM_Comments  WHERE ParentID = @Root)

    WHILE @CID IS NOT NULL
    BEGIN
        EXEC dbo.ShowHierarchyForum @CID, @ArticleId
        SET @CID = (SELECT MAX(ID) FROM CP_FORUM_Comments 
                    WHERE ParentID = @Root

        AND ID < @CID and ArticleId = @ArticleId)


    END
END

if @@NESTLEVEL =1
select * from #YourLocalTempTable
GO

Add New comment

添加评论

这和任何 Add 记录一样简单。添加新评论时 Parentid = 0。为了增添一丝品味,我包含了 HTMLAREA 控件。更多关于这个出色控件的信息 在此,由 Fraser Cain 提供。

SqlConnection myC =new SqlConnection();
myC.ConnectionString=ConfigurationSettings.AppSettings["ConnectionString"];
string sqlQuery="INSERT into " + 
ConfigurationSettings.AppSettings["CommentTable"] +
"(ParentId,ArticleId,Title,UserName,UserEmail,Description,Indent,UserProfile)
VALUES ('" +mParentId + "','" + mArticleId +  "','" + mTitle +  "','" + 
mUserName +  "','" +
mUserEmail +  "','" + mDescription + "','" + mIndent + "','" +
"https://codeproject.org.cn/script/profile/whos_who.asp?id=81898" + "')";
myC.Open();
SqlCommand myCommand=new SqlCommand();
myCommand.CommandText=sqlQuery;
myCommand.Connection=myC;
int i=myCommand.ExecuteNonQuery();
myC.Close();
lblStatus.ForeColor = Color.Green ;
lblStatus.Text ="Status: Success";
Response.Redirect("Forum.aspx?id=" + articleid );  

Add New comment

添加回复

这里只需要提到一点,parentidindent 是从父消息中获取并相应更新的。

删除评论

同一个层级存储过程被修改为在根消息被删除时层级删除 Forum 中的所有子消息。DeleteHierarchyForum

CREATE  PROC dbo.DeleteHierarchyForum
(
    @Root int,
    @ArticleId int
)
AS
BEGIN

if not exists (select name from [tempdb].[dbo].[sysobjects]
            where name like '#YourLocalTempTable%')
create table #YourLocalTempTable (Id int, ParentId int,ArticleId int,
    Title nVarchar(250),
    username nvarchar(50),UserEmail nvarchar(50),Description nvarchar(2000),
    Indent int,DateAdded datetime,UserProfile nvarchar(100))

    SET NOCOUNT ON
    DECLARE @CID int, @PID int, @Title varchar(250)
    insert into #YourLocalTempTable SELECT CP_FORUM_Comments.Id ,
    ParentId ,ArticleId ,
    Title,username ,UserEmail ,Description ,Indent ,DateAdded ,UserProfile
    from CP_FORUM_Comments WHERE ID = @Root and ArticleId = @ArticleId

    SET @CID = (SELECT MAX(ID) FROM CP_FORUM_Comments  WHERE ParentID = @Root)

    WHILE @CID IS NOT NULL
    BEGIN
        EXEC dbo.DeleteHierarchyForum @CID, @ArticleId
        SET @CID = (SELECT MAX(ID) FROM CP_FORUM_Comments
        WHERE ParentID = @Root AND ID < @CID and ArticleId = @ArticleId)
    END
END

if @@NESTLEVEL =1
Delete from CP_FORUM_Comments where CP_FORUM_Comments.Id in 
                (select ID from #YourLocalTempTable)

GO

微笑,我们完成了。

Jumpy Forum 运行中:多条记录

JumpyForum in Action

文章历史

  • 2006 年 12 月 15 日:首次发布
  • 2006 年 12 月 18 日:内容更新
  • 2007 年 1 月 12 日:支持分页和上次访问
  • 2007 年 1 月 17 日:根据用户要求通过 W3C 标准验证
    符合 HTML 4.01 Transitional 标准
  • 2007 年 1 月 19 日:修复了一个 JavaScript 错误

以及感谢

感谢您一路阅读!希望您觉得这篇文章有用,如果您觉得有用,请给我投票/评论,并请多保重。

© . All rights reserved.