ASP.NET MVC–Displaying a tree view using a recursive declarative helper and jQuery
Problem
-
You want to display your model using a a nested <ul>, <li> structure
-
You want to be able to click on a leaf and perform an action
Context
We will use the model defined here and in order to hold our nested structure we will define a class named Folder:
public class Folder { public Folder( ) { this.Subfolders = new List<Folder>( ); } public int Id { get; set; } public string Name { get; set; } public string Type { get; set; } public IList<Folder> Subfolders { get; private set; } public bool IsLeaf { get { return this.Subfolders.Count == 0; } } }
There is nothing complicated about this class. It has some properties and a list of subfolders.
Let’s create a controller called TreeViewController:
public partial class TreeViewController : Controller { private readonly IContinentRepository _continentRepository; private readonly ICityRepository _cityRepository; // If you are using Dependency Injection, you can delete the following constructor public TreeViewController( ) : this( new ContinentRepository( ) , new CityRepository( ) ) { } public TreeViewController( IContinentRepository continentRepository , ICityRepository cityRepository ) { this._continentRepository = continentRepository; this._cityRepository = cityRepository; } [HttpGet] public virtual ActionResult Index( ) { return View( this.Atlas( ) ); } [HttpGet] public virtual ActionResult GetCity( int cityId ) { var city = this._cityRepository.Find( cityId ); return Json( new { Name = city.Name , Population = city.Population } , JsonRequestBehavior.AllowGet ); } private IList<Folder> Atlas( ) { IList<Folder> continents = new List<Folder>( ); foreach ( var continent in this._continentRepository.All ) { var continentFolder = new Folder { Id = continent.Id , Name = continent.Name , Type = "continent" }; foreach ( var country in continent.Countries ) { var countryFolder = new Folder { Id = country.Id , Name = country.Name , Type = "country" }; foreach ( var city in country.Cities ) { var cityFolder = new Folder { Id = city.Id , Name = city.Name , Type = "city" }; countryFolder.Subfolders.Add( cityFolder ); } continentFolder.Subfolders.Add( countryFolder ); } continents.Add( continentFolder ); } return continents; } }
We have a Index action that will return our view. The Index view receives a list of continents, countries and cities wrapped in a nested Folder structure (the Atlas function is just a quick and dirty way of creating the nested Folder structure). There is also a GetCity action that will return the city information when we will click it in the tree view.
Index.cshtml:
@model IEnumerable<Folder> @helper TreeView( IEnumerable<Folder> folders ) { foreach ( var folder in folders ) { <li> @if ( folder.IsLeaf ) { <span class="leaf @folder.Type" id="@folder.Id">@folder.Name</span> } else { <span class="folder">@folder.Name</span> <ul> @TreeView( folder.Subfolders ) </ul> } </li> } } <ul id="continentFolders"> @TreeView( Model ) </ul>
The @helper keyword is a new feature introduced by the Razor view engine, called a declarative helper (for a comparison of MVC 3 Helpers see Jon Galloway’s article in the References section). The FolderTree helper is nothing more than a recursive function that will output <ul> and <li> tags based on the folders parameter.
If we run this now it will look like this:
Adding a jQuery Tree view plugin
We can make our tree view more functional by using a jQuery tree plugin (see the References section):
<script type="text/javascript"> $(document).ready(function () { $('#continentFolders').treeview({ collapsed: true }); $(".leaf.city").click(function () { $.ajax({ url: '@Url.Action( MVC.Various.TreeView.GetCity( ) )', data: { cityId: $(this).attr("id") }, type: 'GET', success: function (data) { alert("Selected city: " + data.Name + ", population: " + data.Population.toString()); } }); }); }); </script>
when a city is clicked we make an ajax request to the GetCity action and show some city details.
See it in action
References
Comparing MVC 3 Helpers, by Jon Galloway | jQuery plugin: Treeview | Another implementation I have found, by Roberto Hernández