C# JSON generator for jsTree 3.0

2,318 views
Skip to first unread message

Roger Martin

unread,
Mar 24, 2014, 1:30:58 PM3/24/14
to jst...@googlegroups.com
I just spent a few days reworking my C# JSON generator for jsTree to work with 3.0 and thought I'd share some of that work here. This is the same code that will be used in the upcoming 3.2 release of the open source digital asset management web app Gallery Server Pro.

The idea is that you build up your tree using C# and then call ToJson() to get the JSON expected by jsTree. Here is a simple example:

private string GetjsTreeAsJson()
{
 
var tv = new TreeView();

 
var rootNode = new TreeNode
                 
{
                   
Text = "Root Node Title",
                   
ToolTip = "Tooltip for root node",
                   
Id = String.Concat("tv_1"),
                   
DataId = "1",
                   
Expanded = true
                 
};

  rootNode
.AddCssClass("jstree-root-node");

 
var childNode = new TreeNode
                 
{
                   
Text = "Child Node Title",
                   
ToolTip = "Tooltip for child node",
                   
Id = String.Concat("tv_2"),
                   
DataId = "2",
                   
NavigateUrl = "http://www.site.com"
                 
};
     
  rootNode
.Nodes.Add(childNode);
  tv
.Nodes.Add(rootNode);

 
return tv.ToJson();
}

I attached the TreeView class. Feel free to use it as you wish - I release it to the public domain. A few notes:

  • Download the source code for Gallery Server Pro to get a better feel for how it's used in a real app. www.galleryserverpro.com The current version uses jsTree 1.0 pre RC3, but by the time you read this GSP 3.2 may be out.
  • The class uses Json.NET to serialize the object to JSON, so be sure this is in your project.
  • I didn't clean up the code to purge all references to the rest of my project. You will have to make changes to get it to compile, but they should be minor.
  • The TreeView class provides core jsTree functionality. If you look at the GSP source code, you'll find higher level wrappers you might find useful. For example, there is a AlbumTreeViewBuilder class, an ASCX user control named albumtreeview.ascx, and a jQuery plugin named GspTreeView. (Again, the current GSP download uses jsTree 1.0 pre RC3, so you'll have to wait until 3.2 to see examples that interact with jsTree 3.0.)
  • See an example of a tree with hyperlinks here: http://demo.galleryserverpro.com/mg-dk/. If you log in with username Demo (password 'demo') and go to the create album page, you'll see a tree with checkbox functionality. There are other variations of the tree elsewhere in the app, such as a multi-select tree used on the Manage Roles page. They all use the TreeView class attached to this post.
Cheers,
Roger Martin
Creator and Lead Developer
Gallery Server Pro
TreeView.cs

Ivan Bozhanov

unread,
Mar 25, 2014, 2:51:35 AM3/25/14
to jst...@googlegroups.com
Thank you for this - I get a lot of requests for C# code, which I am not willing to write, as although I have a few small .net projects I do not feel confident enough to provide.

Once again - thank you for sharing!
Best regards,
Ivan

Travas Nolte

unread,
Mar 25, 2014, 10:25:26 PM3/25/14
to jst...@googlegroups.com
Hi Roger -

Thank you much for this post. I'd abandoned another TreeView control due to a lot of defects in the product, but I had written a lot of helpers to create classes similar to the TreeView class you've shared. I've been able to swap out their model with yours. I'm now getting Json back, but now I need to call in 'core' when wiring up jsTree...(I believe)

I know you mentioned that we'd need to wait until GSP 3.2 is released to see examples that interact with jsTree 3.0, but I was wondering if you have any rough examples that you might be able to share.

Many thanks again for posting. It's given me hope that I haven't lost all my time invested in the other product.

Travas

Roger Martin

unread,
Mar 27, 2014, 11:34:44 AM3/27/14
to jst...@googlegroups.com
OK, here's a sneak peak at the 3.0 compatible jQuery widget I made. It's essentially a wrapper around the jsTree widget. You invoke it like this in JavaScript:

    var json = getTreeJson(); // Get the JSON from the server-side TreeView.ToJson() method
    var treeOptions = {}; // Specify tree options. See $.fn.gspTreeView.defaults
    $('#treeDiv').gspTreeView(json, treeOptions);

If you pass null for the json variable, the widget will get the JSON by invoking a GET request against the URL specified in the option treeDataUrl.

Below is the gspTreeView widget. Note that it may have external references you need to fix. Two I can think of:
  • It calls gspShowMsg() when there is an error. It's basically just a jQuery UI dialog. You can find it in the GSP 3.1 source code.
  • When the JSON contains children: true, jsTree invokes a callback to an HTTP handler named gettreeview.ashx. I attached it.
Hope this helps.
Roger

  //#region gspTreeView plug-in

  $.fn.gspTreeView = function (data, options) {
    var self = this;
    var settings = $.extend({}, $.fn.gspTreeView.defaults, options);

    var getTreeDataAndRender = function () {
      $.ajax({
        type: "GET",
        url: options.treeDataUrl,
        contentType: "application/json; charset=utf-8",
        complete: function () {
          self.removeClass('gsp_wait');
        },
        success: function (tagTreeJson) {
          var tv = new GspTreeView(self, $.parseJSON(tagTreeJson), settings);
          tv.render();
        },
        error: function (response) {
          $.gspShowMsg("Action Aborted", response.responseText, { msgType: 'error', autoCloseDelay: 0 });
        }
      });
    };

    if (data == null) {
      getTreeDataAndRender();
    } else {
      var gspTv = new GspTreeView(this, data, settings);
      gspTv.render();
    }

    return this;
  };

  $.fn.gspTreeView.defaults = {
    clientId: '', // The ID of the HTML element containing the entire gallery. Used to scroll node into view in left pane. Omit if left pane scrolling not needed
    allowMultiSelect: false, // Indicates whether more than one node can be selected at a time
    albumIdsToSelect: null, // An array of the album IDs of any nodes to be selected during rendering
    checkedAlbumIdsHiddenFieldClientId: '', // The client ID of the hidden input field that stores a comma-separated list of the album IDs of currently checked nodes
    theme: 'gsp', // Used to generate the CSS class name that is applied to the HTML DOM element that contains the treeview. Ex: "gsp" is rendered as CSS class "jstree-gsp"
    requiredSecurityPermissions: 1, //ViewAlbumOrMediaObject
    navigateUrl: '', // The URL to the current page without query string parms. Used during lazy load ajax call. Example: "/dev/gs/gallery.aspx"
    enableCheckboxPlugin: false, // Indicates whether a checkbox is to be rendered for each node
    treeDataUrl: '' // The URL for retrieving tree data. Ignored when tree data is passed via data parameter
  };

  window.GspTreeView = function (target, data, options) {
    this.$target = target; // A jQuery object to receive the rendered treeview.
    this.TreeViewOptions = options;
    this.Data = data;
  };

  GspTreeView.prototype.render = function () {
    var self = this;
    
    this._updateNodeDataWithAlbumIdsToSelect();

    var jstreeOptions = {
      core: {
        data: function (node, cb) {
          if (node.id === '#') {
            return cb(self.Data);
          }

          $.ajax({
            url: window.Gsp.GalleryResourcesRoot + '/handler/gettreeview.ashx',
            data: {
              // Query string parms to be added to the AJAX request
              id: node.li_attr['data-id'],
              secaction: self.TreeViewOptions.requiredSecurityPermissions,
              sc: $.inArray('checkbox', this.settings.plugins) >= 0, // Whether checkboxes are being used
              navurl: self.TreeViewOptions.navigateUrl
            },
            dataType: "json",
            error: function (response, textStatus, errorThrown) {
              if (textStatus == "error") {
                alert("Oops! An error occurred while retrieving the treeview data. It has been logged in the gallery's event log.");
              }
            },
            success: function (data) {
              return cb(data);
            }
          });
          return null;
        },
        multiple: this.TreeViewOptions.allowMultiSelect,
        themes: {
          name: this.TreeViewOptions.theme,
          dots: false,
          icons: false,
          responsive: false
        }
      },
    };

    if (this.TreeViewOptions.enableCheckboxPlugin) {
      jstreeOptions.plugins = ['checkbox'];
      jstreeOptions.checkbox = {
        keep_selected_style: false,
        three_state: this.TreeViewOptions.allowMultiSelect
      };
    }

    this.$target.jstree(jstreeOptions)
      .on("ready.jstree", function (e, data) {
        self.onLoaded(e, data);
      })
      .on("changed.jstree", function (e, data) {
        self.onChangeState(e, data);
      })
      .on("deselect_node.jstree", function (e, data) {
        self.onDeselectNode(e, data);
      });
  };

  GspTreeView.prototype._storeSelectedNodesInHiddenFormField = function (data) {
    // Grab the data-id values from the top selected nodes, concatenate them and store them in a hidden
    // form field. This can later be retrieved by server side code to determine what was selected.
    if (this.TreeViewOptions.checkedAlbumIdsHiddenFieldClientId == null || this.TreeViewOptions.checkedAlbumIdsHiddenFieldClientId.length == 0)
      return;

    var topSelectedNodes = data.instance.get_top_selected(true);
    var albumIds = $.map(topSelectedNodes, function (val, i) {
      return val.li_attr['data-id'];
    }).join();

    $('#' + this.TreeViewOptions.checkedAlbumIdsHiddenFieldClientId).val(albumIds);
  };

  GspTreeView.prototype._updateNodeDataWithAlbumIdsToSelect = function () {
    // Process the albumIdsToSelect array - find the matching node in the data and change state.selected to true
    // Note that in many cases the nodes are pre-selected in server side code. This function isn't needed in those cases.
    if (Gsp.isNullOrEmpty(this.TreeViewOptions.albumIdsToSelect))
      return;

    var findMatch = function (nodeArray, dataId) {
      // Search nodeArray for a node having data-id=dataId, acting recursively
      if (Gsp.isNullOrEmpty(nodeArray))
        return null;

      var matchingNode = $.grep(nodeArray, function (n) { return n.li_attr['data-id'] === dataId; })[0] || null;

      if (matchingNode != null)
        return matchingNode;

      // Didn't find it, so recursively search node data
      $.each(nodeArray, function (idx, n) {
        matchingNode = findMatch(n.children, dataId);

        if (matchingNode != null) {
          return false; // Break out of $.each
        }
      });

      return matchingNode;
    };

    var self = this;
    $.each(this.TreeViewOptions.albumIdsToSelect, function (idx, id) {
      var node = findMatch(self.Data, id);

      if (node != null) {
        node.state.selected = true;
      }
    });
  };

  GspTreeView.prototype.onChangeState = function (e, data) {
    if (data.action == 'select_node') {
      var url = data.instance.get_node(data.node, true).children('a').attr('href');

      if (url != null && url.length > 1) {
        // Selected node is a hyperlink with an URL, so navigate to it.
        document.location = url;
        return;
      }
    }

    if (data.action == 'deselect_node' || data.action == 'select_node') {
      this._storeSelectedNodesInHiddenFormField(data);
    }
  };

  GspTreeView.prototype.onDeselectNode = function (e, data) {
    // Don't let user deselect the only selected node when allowMultiSelect=false
    if (!this.TreeViewOptions.allowMultiSelect && data.instance.get_selected().length == 0) {
      data.instance.select_node(data.node);
    }
  };

  GspTreeView.prototype.onLoaded = function (e, data) {
    this._storeSelectedNodesInHiddenFormField(data);

    // Scroll the left pane if necessary so that the selected node is visible
    if (this.TreeViewOptions.clientId.length < 1)
      return;

    var selectedIds = data.instance.get_selected();
    if (selectedIds != null && selectedIds.length == 1) {
      var nodeOffsetTop = $('#' + selectedIds[0]).position().top;
      var leftPaneHeight = $('#' + this.TreeViewOptions.clientId + '_lpHtml').height();
      if (nodeOffsetTop > leftPaneHeight) {
        $('#' + this.TreeViewOptions.clientId + '_lpHtml').animate({ scrollTop: nodeOffsetTop }, 200, "linear");
      }
    }
  };

  //#endregion gspTreeView plug-in

gettreeview.ashx.cs

Travas Nolte

unread,
Mar 29, 2014, 1:44:05 PM3/29/14
to jst...@googlegroups.com

Roger,

Thank you for generously sharing. Your code is very clean and easy to follow. Nice work

Travas

--
You received this message because you are subscribed to the Google Groups "jsTree" group.
To unsubscribe from this group and stop receiving emails from it, send an email to jstree+un...@googlegroups.com.
To post to this group, send email to jst...@googlegroups.com.
Visit this group at http://groups.google.com/group/jstree.
For more options, visit https://groups.google.com/d/optout.
Reply all
Reply to author
Forward
0 new messages