Below is a example of what I was suggesting in my comments.
It does not include the actual menu logic/plugin as that is assumed to be provided already. It is pretty much everything you would need to add to a fresh MVC application step-by-step. If it is missing anything you need just ask. I will post this as a tutorial on my website at some point.
1. MenuItem class/table
You only need a single table to hold both menu items and sub menus. The only difference is whether they actually have any child items. Menu items are just are id's and text (you could add hyperlinks etc if you wanted).
Using Code-first I added the MenuItems table with this class:
public class MenuItem
{
// Unique id of menu item
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public virtual int MenuItemId { get; set; }
// Tesxt to display on menu item
[Required]
public virtual string MenuItemName { get; set; }
// Sequential display order (within parent item)
public virtual int DisplayOrder { get; set; }
// Foreign key value
public virtual int? ParentMenuItemId { get; set; }
[ForeignKey("ParentMenuItemId")]
// Parent menu item
public virtual MenuItem ParentMenuItem { get; set; }
// Child menu items
public virtual ICollection<MenuItem> ChildItems { get; set; }
}
2. Add reorder action
This is the one that actually reorders the items. It is called via Ajax on the page using a URL like /menu/reorder/2?after=3
which will move item id 2 to after item id 3 in the database.
In the first version of my reordering I used to pass a position, item id and parent id, but found in production, due to complex parent relationships, that was not as useful as simply saying "which item id you want to move", and "which item id you want it placed after" (0 meaning place it first).
/// <summary>
/// Reorder the passed element , so that it appears after the specific element
/// </summary>
/// <param name="id">Id of element to move</param>
/// <param name="after">Id of element to place after (or 0 to place first)</param>
/// <returns>Unused</returns>
public ContentResult Reorder( int id, int after )
{
var movedItem = this.context.MenuItem.Find(id);
// Find all the records that have the same parent as our moved item
var items = this.context.MenuItem.Where(x => x.ParentMenuItemId == movedItem.ParentMenuItemId).OrderBy(x => x.DisplayOrder);
// Where to insert the moved item
int insertionIndex = 1;
// Display order starts at 1
int displayOrder = 1;
// Iterate all the records in sequence, skip the insertion value and update the display order
foreach (var item in items)
{
// Skip the one to move as we will find it's position
if (item.MenuItemId != id)
{
// Update the position
item.DisplayOrder = displayOrder;
if (item.MenuItemId == after)
{
// Skip the insertion point for subsequent entries
displayOrder++;
// This is where we will insert the moved item
insertionIndex = displayOrder;
}
displayOrder++;
}
}
// Now update the moved item
movedItem.DisplayOrder = insertionIndex;
this.context.SaveChanges();
return Content(insertionIndex.ToString());
}
3. View holding the top level menu
Index Action
//
// GET: /Menu/
public ActionResult Index()
{
// Return the top level menu item
var rootMenu = context.MenuItem.SingleOrDefault(x => x.ParentMenuItemId == null);
return View(rootMenu);
}
Index.cshtml
This contains the code for the sortable reordering via an Ajax call.
@model jQuery.Vero.Menu.MvcApplication.Models.MenuItem
<h2>Test Menu gets rendered below</h2>
<ul id="menu" class="menu">
@foreach (var menuItem in Model.ChildItems.OrderBy(x=>x.DisplayOrder))
{
@Html.Action("Menu", new { id = menuItem.MenuItemId })
}
</ul>
@section scripts{
<script type="text/javascript">
$(function () {
var $menu = $("#menu");
// Connection menu plugin here
...
// Now connect sortable to the items
$menu.sortable({
update: function(event, ui)
{
var $item = $(ui.item);
var $itemBefore = $item.prev();
var afterId = 0;
if ($itemBefore.length)
{
afterId = $itemBefore.data('id');
}
var itemId = $item.data('id');
$item.addClass('busy');
$.ajax({
cache: false,
url: "/menu/reorder/" + itemId + "?after=" + afterId,
complete: function() {
$item.removeClass('busy');
}
});
}
});
});
</script>
}
4. Recursive Partial View
To display the menu item, I use a recursive action and view.
Menu Action
/// <summary>
/// Render one level of a menu. The view will call this action for each child making this render recursively.
/// </summary>
/// <returns></returns>
public ActionResult Menu(int id)
{
var items = context.MenuItem.Find(id);
return PartialView(items);
}
Menu Partial View - Menu.cshtml
@model jQuery.Vero.Menu.MvcApplication.Models.MenuItem
<li id="Menu@(Model.MenuItemId)" class="menuItem" data-id="@(Model.MenuItemId)">
@Model.MenuItemName
@if (Model.ChildItems.Any())
{
<ul class="menu">
@foreach (var menuItem in Model.ChildItems.OrderBy(x => x.DisplayOrder))
{
@Html.Action("Menu", new { id = menuItem.MenuItemId })
}
</ul>
}
</li>
5. Additional Styles
These are the only styles I added to the test application. Note I add and remove a busy
class on the dropped item so it can show progress etc while the Ajax call is being processed. I use a progress spinner in my own apps.
.menu {
list-style: none;
}
.menu .menuItem{
border: 1px solid grey;
display: block;
}
.menu .menuItem.busy{
background-color: green;
}
6. Sample data
This is an example of the hierarchical menu items I entered (produces screenshot below).
7. Onscreen example of hierarchy
This shows the hierarchy the above code renders. You would apply a menu plugin to the top level UL.