主题:  有兴趣讨论无限分类法吗

蓝鲸

职务:版主
等级:5
金币:42.1
发贴:2614
#12004/9/12 3:59:53
在做软件是,碰上这个问题,要把一个商店的分类用树形来表示。
商品分类应该是无限子类划分的,怎样设计好一些。用数据库还是用XML。
由于以前搞设计,编程方面较生了,.NET也则开始学,觉得还是用数据库比较熟悉些。由于以前没这方面经验,所以对数据库设计感到比较辣手。
想了很多,比如链表,用一个Parent(父关系)、Child(子链接),后来舍弃了Child,就用Parent,只认父亲,于是设计数据库格式如下:
表名:Sort
字段:SortID(主键,自增长)、ParentID(父类ID号)、SortName(名称)、IsEnd(是否为最终类,因设计时考虑最终类下不能再分,最终类下只能具体商品。相反不是最终类下,只能再设类别,而不能下设具体商品)

数据库建好了,这是最简结构。现在说明一下库的使用方法。
Sort表最重要的字段就是ParentID,用于链接分类的关系。比如家族,你只要认准你父亲,而不必认爷爷,爷爷是父亲认的,即你-->父亲-->爷爷。只经认准各自的父亲(parent),这条链就链在一起了。再进一步,爷爷可以有兄弟,但这不是你考虑的,只要认得祖宗,而不必顾及傍支(即爷爷兄弟下的后代),傍支是别人考虑的事。
再说到数据库了,最顶级的类,ParentID = 0,说明没有父类;
如果一个类,它的父类是2号,则ParentID = 2;

以下是数据库的一张截图,可以看一下表的结构

图片如下:


非常大鱼

蓝鲸

职务:版主
等级:5
金币:42.1
发贴:2614
#22004/9/12 4:04:48
万里长征,这才开始,要想使用它有很多问题。
比如树形搜索、删除节点,得到每个结点信息等。
以下是我用C#写的,用TreeView来表示分类树形

图片如下:


非常大鱼

蓝鲸

职务:版主
等级:5
金币:42.1
发贴:2614
#32004/9/12 4:33:53
要定清楚,真是头大了,我不善写教程,只是提供一个思考方法。各位有好的思考方法也拿上来共享。
用树形来表示,化了我非常大精力,树形不好控制(刚用.Net不好意思),比如你点击一个子节点(node),要显示该节点的信息(包括名称或其它信息)。

要说明这个TreeView显示树形,先要知道一下TreeView添加节点方法。
TreeView字节点对象为TreeNode,可以用以下方法建立
TreeNode tn = new TreeNode("CustomerName");
在TreeView中添加节点
TreeView1.Nodes[1].Nodes[2].Add(tn);
如果想在tn节点上再加字节点
TreeNode tn2 = new TreeNode("Name2");
tn.Add(tn2);


非常大鱼

蓝鲸

职务:版主
等级:5
金币:42.1
发贴:2614
#42004/9/12 4:53:02
以上只可建立固定的树,动态数的文字、及树的多少都是不确定的,如果都是tn1,tn2.....要定到什么时候去。所以考虑用数组存放这些Node的集合
ArrayList arrNode = new ArrayList();

一个简单的添加树的函数(是个思路,其实到现在还不完全)
nodeIndex:父节点在数组中的位置;
NodeName:Node的名称;
public void AddNode(int nodeIndex, string NodeName)
{
    TreeNode tn = new TreeNode(NodeName);
    ((TreeNode)arrNode[nodeIndex]).Add(tn);
    // 把新的Node添加到数组中去
    arrNode.Add(tn);
}

嘿嘿,这是个不成器的函数,看似实现了,其实还没有。
因为从数据库中读出的了不会告知你应该在数组的那条下增加节点。从数据库中读出来的只有自身ID号及 ParentID。这要怎么实现,确实伤脑,因为数组中看不到任何关于ID及ParentID信息。
这个函数还要大大修改,在什么地方可以使每个Node具有ID及ParentID属性。我终于想办法编写自己的TreeNode组件了。

太困了,有时间再写下去。


非常大鱼

蓝鲸

职务:版主
等级:5
金币:42.1
发贴:2614
#52004/9/12 10:46:24
接下来,就得为TreeNode做些扩充。
新建组件,定义一个TreeNode的派生类:
public class ExNode : System.Windows.Forms.TreeNode

然后定义一个结构:
    public struct Sort
    {
        public int ID;
        public string Name;
        public int ParentID;
        public bool IsEnd;
        public bool Disable;
    }
该结构和数据库中相吻合。

该类的代码较简单,如下:
using System;
using System.Collections;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Windows.Forms;


namespace CrystalBiz
{
    public struct Sort
    {
        public int ID;
        public string Name;
        public int ParentID;
        public bool IsEnd;
        public bool Disable;
    }

    /// <summary>
    /// ExNode 的扩展
    /// </summary>
    public class ExNode : System.Windows.Forms.TreeNode
    {
        
        private Sort mySort;

        public Sort Sort
        {
            get
            {
                return mySort;
            }
            set
            {
                mySort = value;
                this.Text = mySort.Name;
            }
        }

        public ExNode()
        {
            mySort = new Sort();
        }



    }
}


非常大鱼

蓝鲸

职务:版主
等级:5
金币:42.1
发贴:2614
#62004/9/12 10:56:44
定义好的的类,就只要把原来的TreeNode换成ExNode就行了。
这个AddNode函数现在可以改了。
        /// <summary>
        /// 在treeView中增加Node,并把Node加入到数组,方便查询
        /// </summary>
        private void AddNode(Sort st)
        {
            ExNode pNode = new ExNode(); //要挂接的父结点
            ExNode nd = new ExNode(); //本结点
            nd.Sort = st;
            
            // 添加到TreeView和数组
            arrNode.Add(nd);
            
            if (nd.Sort.ParentID == 0)
            {
                // 最上级分类,在根节点添加
                trvSort.Nodes[0].Nodes.Add(nd);
            }
            else
            {
                // 在根目录中搜索父类,如果ParentID号与数组的ID相合,则找到
                for (int i = 1; i <= arrNode.Count; i++)
                {
                    if (((ExNode)arrNode[i-1]).Sort.ID == st.ParentID)
                    {
                        pNode = (ExNode)arrNode[i-1];
                        break;
                    }
                }
                // 在找到的父节点下添加新节点
                pNode.Nodes.Add(nd);
            }
        }


非常大鱼

蓝鲸

职务:版主
等级:5
金币:42.1
发贴:2614
#72004/9/12 11:07:23
最后一步了,胜利在望

        /// <summary>
        /// 重设商品分类的 TreeView
        /// ResetSortView() 函数
        /// </summary>
        #region ResetSortView()函数实现
        private void ResetSortView()
        {
            trvSort.Nodes.Clear();
            arrNode.Clear();

            ExNode nd = new ExNode();
            //
            // 添加商品总类
            //
            Sort mySort = new Sort();
            mySort.ID = 0;
            mySort.Name = "商品总类";
            mySort.ParentID = -1;
            mySort.IsEnd =false;
            mySort.Disable = false;            
            nd.Sort = mySort;
            nd.ImageIndex = 0;
            nd.SelectedImageIndex = 0;
            trvSort.Nodes.Add(nd);
            arrNode.Add(nd);
            // 打开数据库
            // 不好意思,我把数据库打开都定义类了,全包装在DBClass内
            // 这样换成SQL SERVER就省力些
            // DataSet这里也封装,为myDB.DBDataSet
            // 懒得定数据库了,如果不熟悉数据库,快学习一下
            string sql = "Select * From MerchandiseSort Order by MerchandiseSortID";
            DBClass myDB = new DBClass();
            myDB.DBOpen();
            myDB.CreateAdapter(sql);
            myDB.FillDataSet();
            //
            // 把数据记录逐一添加到树开上去
            //
            for (int i = 1; i <= myDB.DBDataSet.Tables[0].Rows.Count; i++)
            {
                mySort.ID = (int)myDB.DBDataSet.Tables[0].Rows[i-1]["MerchandiseSortID"];
                mySort.Name = myDB.DBDataSet.Tables[0].Rows[i-1]["Name"].ToString();
                mySort.ParentID = (int)myDB.DBDataSet.Tables[0].Rows[i-1]["ParentID"];
                mySort.IsEnd = (bool)myDB.DBDataSet.Tables[0].Rows[i-1]["IsEnd"];
                mySort.Disable = (bool)myDB.DBDataSet.Tables[0].Rows[i-1]["Disable"];
                AddNode(mySort);
            }

            myDB.DBClose();
            trvSort.ExpandAll();

        }


非常大鱼

蓝鲸

职务:版主
等级:5
金币:42.1
发贴:2614
#82004/9/12 11:26:17
结束语
这代码肯定有问题,比如最后的,可用DataReader的可能效率更高,但我是边学边做,来不及考虑这些,而且其它事也很多,事情总要分轻重,解决大的,再追求性能。
我较喜欢艺术,以前这种追求完美的思想,在这里没发挥。否则,为了一段代码苦思冥相,结果可能什么也做不了。设计程序整体思想最要紧,整体好了,内部稍差,也不失为优秀工程,因为内部分块都可以不断改进的。

另外我在网上看了些文章,有关于XML,还没来得及仔细研究。如果有兴趣,大家讨论一下。有朋友建议搜索用递归,但我这里用了数组,不知那个更好。另外一些添加子类的方法还没写,可能比较简单些。较复杂的是删除子节点,如删除(中止)节点,则其下的子节点要全部删除,这就要搜索全部的子树了。这方面我是弱项,所以用了个折中的办法,即如果还有子类的话,就不能删除,正象DOS的删除目录法。这个我肯定以后要化时间再加强的,但不是现在。

我这人就这样,不会写最好的程序,只会写更好的程序。所以也不求程序最高效率。如果想一起讨论这个思路的话来找我好了,但如果要找完美的东西,别来找我。


非常大鱼

缺缺

职务:管理员
等级:8
金币:41.0
发贴:9620
#92004/9/12 12:01:21
写的不错.
我最近也在狂啃C#



janlay

职务:管理员
等级:7
金币:28.0
发贴:7244
#102004/9/12 16:01:34
关于搜索子目录的问题,可以通过将树形目录映射成线程列表的方式来处理.具体做法就是引用一个称之为"路径"的概念,给每个目录项一个路径,父目录的路径是包含子目录路径中的.这样,只需要搜索包含的路径就可以通过一次查询得到目录内所有的子目录树了.



=ridincal=

职务:管理员
等级:7
金币:20.0
发贴:5886
#112004/9/12 16:04:33
鼓励分享学习心得

TreeViewControl在.Net中被封装的非常对象化,是一种对树型数据结构的很好应用,研究其htc代码会更有收获

另,对于树型数据的生成和管理最正规的方式是使用迭代,TreeViewControl本身也是这样去做的。这样能使代码更加简洁且有弹性。(你会发现你数据表中的 IsEnd 是冗余数据)



蓝鲸

职务:版主
等级:5
金币:42.1
发贴:2614
#122004/9/12 16:38:31
janlay,谢谢指点。
我也想过这个,原来想增加一个字段就是PATH路径。不过觉得操作上比较难,也就是父目录改变以后,所有的子目录路径均要改变。可能我没有理解,是否可以稍加指点一些。

ridincal的迭代方法,怪自己没好好学数学及数据结构,很想知道这方面的介绍。


非常大鱼

janlay

职务:管理员
等级:7
金币:28.0
发贴:7244
#132004/9/12 17:03:42
路径应该由系统自行维护,也就是要考虑记录的添加、删除和更新时所要做的处理。更改父目录时,要多做几次更新,操作虽然麻烦一点,但是还是可以克服的。

从规范化角度来看,这个路径和表中的IsEnd一样,都是冗余的,它们的作用是可以较好地提高查询效率,见仁见智,我认为还是值得的



蓝鲸

职务:版主
等级:5
金币:42.1
发贴:2614
#142004/9/12 18:03:16
经你这么一说,我有点开巧了。我觉得路径用ID号较好
如:0\12\55\98,55的子节点可以是0\12\55\97\126,55以前的路径均相同。
如果要改变55的父类:0\18\55
则所有含有路径"0\12\55\"全替换掉"0\18\55"
这样可能用UPDATE语句了。

ISEnd可能名字有些误会,其实不是终结的意思。其实这个字段是分类下只有实体商品,不能再分类,因再分类可能类别不清晰,所以十脆说明以下不能再分。


非常大鱼

=ridincal=

职务:管理员
等级:7
金币:20.0
发贴:5886
#152004/9/12 19:21:42
蓝鲸在上个帖子中说
引用:

ridincal的迭代方法,怪自己没好好学数学及数据结构,很想知道这方面的介绍。


for example:

private void AddTreeNode(DataNode dND)
{
    foreach(DataNode dn in dND.ChildNodes)
    {
         TreeNode nd = new TreeNode();
         nd.Name=dn.NodeName;
         nd.OtherAttribute="";
         ......
         dND.Nodes.Add(nd);
         AddTreeNode(nd);
    }
}


部分为伪码,要点是数据余操作分离,DataNode 数据结构需要根据数据表自行构建