无限分级和tree结构数据增删改【提供Demo下载】

无限分级

很多时候我们不确定等级关系的层级,这个时候就需要用到无限分级了。

说到无限分级,又要扯到递归调用了。(据说频繁递归是很耗性能的),在此我们需要先设计好表机构,用来存储无限分级的数据。当然,以下都是自己捣鼓的结果,非标准。谁有更好的设计望不吝啬赐教。

说来其实也简单,就是一个ID和父ID的关系。

以此类推,Id需要是唯一的,ParenId需要是Id列里面存在即可。这样我们就实现无限分级了,如果再加一列Sort排序就更完美了。

jstree插件

官方地址:https://www.jstree.com/

为什么要用这个插件?因为有方便的api给我们做数据绑定,且支持节点拖动来实现增删改,个人觉得这个功能挺强大的。

Demo

下面我们来基于jstree插件来实现无限分级数据操作。以区域数据操作为例,用Code First的方式来编写demo代码。

创建Region实体

为了配合插件自动生成的节点id,我们这里使用的Node和ParentNode来存储上下级关系(而不是上面说的id和parentid,但是实际效果是一样的)。

/// <summary>
/// 区域
/// </summary>
public class Region
{
    /// <summary>
    /// 主键id
    /// </summary>
    public int Id { get; set; }
    /// <summary>
    /// 名称
    /// </summary>
    public string Name { get; set; }
    /// <summary>
    /// 节点
    /// </summary>
    public string Node { get; set; }
    /// <summary>
    /// 父节点
    /// </summary>
    public string ParentNode { get; set; }

}

 

满足jstree插件的数据对象Dto

为了适应jstree插件的数据要求,我们需要把上面的数据转换成树状的数据对象。 

/// <summary>
/// Dto
/// </summary>
public class RegionsTreeOutput
{
    /// <summary>
    /// Id
    /// </summary>
    public int RegionsId { get; set; }
    /// <summary>
    /// tree显示文本(对应region的name)
    /// </summary>
    public string text { get; set; }
    /// <summary>
    /// tree的id(对应Node)
    /// </summary>
    public string id { get; set; }      
    /// <summary>
    /// 子节点数据(此属性就体现的数据的层级关系)
    /// </summary>
    public List<RegionsTreeOutput> children { get; set; }
}
View Code

 

数据转换

  #region GetRegionTree 初始化数据获取 的辅助方法
        public RegionsTreeOutput LoadRegions(string id, List<Region> inRegions, RegionsTreeOutput outRegions)
        {

            List<Region> regions = inRegions.Where(t => t.ParentNode == id).ToList();
            if (outRegions == null)//加载父节点
            {
                outRegions = ToTreeData(regions[0]);
                LoadRegions(outRegions.id, inRegions, outRegions);
            }
            else//加载子节点
            {
                outRegions.children = ToTreesData(regions);
                if (regions.Count > 0)
                {
                    for (int i = 0; i < regions.Count; i++)
                    {
                        LoadRegions(regions[i].Node, inRegions, outRegions.children[i]);//递归调用
                    }
                }
            }
            return outRegions;
        }

        public RegionsTreeOutput ToTreeData(Region region)
        {
            var treeData = new RegionsTreeOutput();
            treeData.id = region.Node;
            treeData.text = region.Name;
            treeData.RegionsId = region.Id;            
            return treeData;
        }
        public List<RegionsTreeOutput> ToTreesData(List<Region> listRegion)
        {
            var regions = new List<RegionsTreeOutput>();
            for (int i = 0; i < listRegion.Count; i++)
            {
                regions.Add(ToTreeData(listRegion[i]));
            }
            return regions;
        }
        #endregion
View Code

 

初始化获取转换后的数据

 /// <summary>
 /// 初始化数据获取
 /// </summary>
 /// <returns></returns>
 public JsonResult GetResultData()
 {
     TreeDbContext db = new TreeDbContext();
     var regions = db.Regions.Where(t => true).ToList();
     var regionObj = LoadRegions("-1", regions, null);
     return Json(regionObj);
 }

以上后台的数据差不多就完成了。

前台数据加载

 $(function () {
            $.post("/Home/GetResultData", null, function (sData) {
                treeObj = $('#jstree_demo').jstree({
                    //, "checkbox"
                    'plugins': ["contextmenu", "dnd", "search", "state", "types", "wholerow"],
                    'core': {
                        "animation": 0,
                        "check_callback": true,
                        'force_text': true,
                        "themes": { "stripes": true },
                        'data': sData
                    },
                    "types": {
                        "default": {
                            "icon": "fa fa-folder icon-state-warning icon-lg"
                        },
                        "file": {
                            "icon": "fa fa-file icon-state-warning icon-lg"
                        }
                    },
                    "contextmenu": {
                        select_node: false,
                        show_at_node: true,
                        items: function (o, cb) {
                            //因为这里我们之后需要定义多个项,所以通过对象的方式返回
                            var actions = {};
                            //添加一个"新增"右键菜单
                            actions.create = {//这里的create其实阔以随意命名,关键是里面的 这里面的 action回调方法
                                "separator_before": false,//Create这一项在分割线之前
                                "separator_after": true,//Create这一项在分割线之后
                                "_disabled": false, //false表示 create 这一项可以使用; true表示不能使用
                                "label": "新增",  //Create这一项的名称 可自定义
                                "action": function (data) {  //点击Create这一项触发该方法,这理还是蛮有用的
                                    var inst = $.jstree.reference(data.reference),
                                         obj = inst.get_node(data.reference);//获得当前节点,可以拿到当前节点所有属性
                                    //新加节点,以下三行代码注释掉就不会添加节点
                                    inst.create_node(obj, {}, "last", function (new_node) {
                                        setTimeout(function () { inst.edit(new_node); }, 0);//新加节点后触发 重命名方法,即 创建节点完成后可以立即重命名节点
                                    });
                                }
                            };
                            if (o.id != "0001")//屏蔽对根节点的操作  “0001”改成根节点对应的真是id
                            {
                                //添加一个"重命名"右键菜单
                                actions.rename = {
                                    "separator_before": false,
                                    "separator_after": false,
                                    "_disabled": false, //(this.check("rename_node", data.reference, this.get_parent(data.reference), "")),
                                    "label": "重命名",
                                    "action": function (data) {
                                        var inst = $.jstree.reference(data.reference),
                                                obj = inst.get_node(data.reference);
                                        inst.edit(obj);
                                    }
                                }
                                //添加一个"删除"右键菜单
                                actions.delete = {
                                    "separator_before": false,
                                    "icon": false,
                                    "separator_after": false,
                                    "_disabled": false, //(this.check("delete_node", data.reference, this.get_parent(data.reference), "")),
                                    "label": "删除",
                                    "action": function (data) {
                                        var inst = $.jstree.reference(data.reference),
                                                obj = inst.get_node(data.reference);
                                        if (inst.is_selected(obj)) {
                                            inst.delete_node(inst.get_selected());
                                        }
                                        else {
                                            inst.delete_node(obj);
                                        }
                                    }
                                };
                            }
                            return actions;//返回右键菜单项
                        }
                    },
                });
            });
        });
View Code

 

其他操作

//删除节点
$('#jstree_demo').on('delete_node.jstree', function (e, data) {            
            var id = data.node.original.RegionsId;          
            $.ajax({
                type: "get",
                url: "/Home/DeleteRegion?id=" + id,
                success: function (sData) {

                }
            }); 
        });
//移动节点
$('#jstree_demo').on('move_node.jstree', function (e, data) {
            saveRegions(data);
        });
//修改名
$('#jstree_demo').on('rename_node.jstree', function (e, data) {            
            saveRegions(data);
        });
//保存
function saveRegions(data) {            
            var id = data.node.original.RegionsId;
            var name = data.node.text;//修改后的name
            //var oldName = data.old;//原name           
            //var pNode = $('#jstree_demo').jstree().get_node(data.node.parent).original.RegionsId;
            var josnData = { "Id": id, "Node": data.node.id, "ParentNode": data.node.parent, "Name": name };
            $.ajax({
                url: "/Home/SaveRegions",
                data: josnData,
                success: function (sData) {                   
                    data.node.original.RegionsId = sData;
                    data.node.state.opened = false;//是否展开
                }
            });
        }

当然,记得修改或是删除要取RegionsId这个对应后台实体的ID。

通过按钮来操作增删改

function createTree() {   
    var ref = $('#jstree_demo').jstree(true),
        sel = ref.get_selected();
    if (!sel.length) { return false; }
    sel = sel[0];
    sel = ref.create_node(sel, { "type": "file" });
    if (sel) {
        ref.edit(sel);
    }
};

function renameTree() {
    var ref = $('#jstree_demo').jstree(true),
        sel = ref.get_selected();
    if (!sel.length) { return false; }
    sel = sel[0];
    ref.edit(sel, function () {
        
    });
};

function deleteTree() {
    var ref = $('#jstree_demo').jstree(true),
        sel = ref.get_selected();
    if (!sel.length) { return false; }
    ref.delete_node(sel);
};

 

更加详细的细节请看demo。

链接:http://pan.baidu.com/s/1hrN5QvU 密码:c6b7

 


 2016.08.26更新

以上方式有问题:如果多个用户同时新建节点,会有重复的。(因为节点data.node.id是前端页面自动生成的) 

解决方法:(更改data.node.id的值从后台赋值)

前端:

$(function () {
            treeObj = $('#jstree_demo').jstree({
                //, "checkbox"
                'plugins': ["contextmenu", "dnd", "search", "state", "types", "wholerow"],
                'core': {
                    "animation": 0,
                    "check_callback": true,
                    'force_text': true,
                    "themes": { "stripes": true },
                    //修改点(1)  【可以给插件刷新】
                    'data': function (obj, callback) {
                        $.ajax({
                            "type": "post",
                            "url": "/Home/GetResultData",
                            "data": {},
                            "success": function (sData) {
                                callback(sData);//回调传请求得到的数据(这里可以把数据组装成插件需要的数据格式)
                            }
                        });
                    }
                },
//保存
function saveRegions(data) {
    var id = data.node.original.RegionsId;
    var name = data.node.text;//修改后的name
    //var oldName = data.old;//原name
    //var pNode = $('#jstree_demo').jstree().get_node(data.node.parent).original.RegionsId;
    //修改点(2)   【取自定义的父节点】
    var ParentNode = ($('#jstree_demo').jstree().get_node(data.node.parent).original.MyNode || data.node.parent);// data.node.parent;
    //修改点(3)   【取自定义的节点】
    var pNode = (data.node.original.MyNode || data.node.id);
    var josnData = { "Id": id, "Node": pNode, "ParentNode": ParentNode, "Name": name };
    $.ajax({
        url: "/Home/SaveRegions",
        data: josnData,
        type: "post",
        success: function (sData) { 
            data.node.original.RegionsId = sData.Id;
            //data.node.state.opened = true;//是否展开
            data.node.original.MyNode = sData.Node;

            //修改点(4)
            $.jstree.reference("#jstree_demo").refresh();//刷新jstree控件,重新加载数据()

        }
    });
} 

 后台:

input.Node = Guid.NewGuid().ToString();

 更新demo:http://pan.baidu.com/s/1gfdmQoN

 

【注意】

  • 不能使用select控件,可以使用div、input