Tuesday, July 17, 2012

Learning MVC: Vertical Pop-Out Menu

Adding a pop-out menu to the application turned out to be two major steps. The second step can be broken down into obvious sub-steps too:

  • Create HTML structure for the menu and apply the CSS
  • Create a ViewModel for the menu partial view and fill it with data
    • Create a ViewModel
    • Create a controller to fill it with data
    • Modify the view to render the ViewModel properly

The first step I'm not describing here because I used a well-written tutorial[1]. I will only record here the styles I added to Site.css:

#menu {
    width: 12em;
    background: #eee;
}

#menu ul {
    list-style: none;
    margin: 0;
    padding: 0;
}

#menu a, #menu h2 {
    font: bold 11px/16px arial, helvetica, sans-serif;
    display: block;
    border-width: 1px;
    border-style: solid;
    border-color: #ccc #888 #555 #bbb;
    margin: 0;
    padding: 2px 3px;
}

#menu h2 {
    color: #fff;
    background: #000;
    text-transform: uppercase;
}

#menu a {
    color: #000;
    background: #efefef;
    text-decoration: none;
}

#menu a:hover {
    color: #a00;
    background: #fff;
}

#menu li {position: relative;}

#menu ul ul ul {
    position: absolute;
    top: 0;
    left: 100%;
    width: 100%;
}

div#menu ul ul ul,
div#menu ul ul li:hover ul ul
{display: none;}

div#menu ul ul li:hover ul,
div#menu ul ul ul li:hover ul
{display: block;}

The second step I'll write down in more detail.

1. Create a partial view for the left sidebar. I decided to render the partial view by calling Html.Action, so I modified the div that holds the left sidebar in _Layout.shtml to look like this:

<div id="left-sidebar">
 @Html.Action("MenuResult", "LeftMenu")
</div>

Then I created a partial view called MenuResult.shtml and placed it in the Shared folder. This is how the HTML structure looks like:

@model Recipes.Models.LeftMenuViewModel

@{ Layout = null; }

<div id="menu">
    <ul>
        <li><h2>Recipes Menu</h2>
            <ul>
                <li>@Html.ActionLink("Recipes", "../Recipe/Index")
                    <ul>
                         <li>Test menu item
                            <ul>
                                    <li>Test child menu item</li>
                            </ul>
                        </li>                           
                    </ul>
                </li>
            </ul>
        </li>
    </ul>
</div>

2. ViewModel. After I was satisfied with the way the "stub" menu works, I started working on the model for the partial view. My first attempt looked something like this, a pretty simple model:

public class LeftMenuViewModel
{
 public List<Category> Categories { get; set; }
}

And a pretty simple nested foreach iterator that attempts to render the view:

<ul>
 <li>@Html.ActionLink("Recipes", "../Recipe/Index")
  <ul>
   @foreach(var cat in Model.Categories)
   {
    <li>@Html.ActionLink(cat.CategoryName, @Html.CategoryAction(cat.CategoryID).ToString())
    <ul>
     @foreach(var subcat in cat.SubCategories)
     {
      <li>@Html.ActionLink(subcat.SubCategoryName, @Html.SubCategoryAction(subcat.SubCategoryID).ToString())</li>
     }
    </ul>
   </li>                           
   }
  </ul>
 </li>
</ul>

3. Controller. The initial version of the controller looked something like this:

public class LeftMenuController : Controller
{
 public PartialViewResult MenuResult()
 {
  LeftMenuViewModel viewModel = new LeftMenuViewModel();

  using (RecipesEntities db = new RecipesEntities())
  {
   viewModel.Categories = db.Categories.ToList();
   foreach (var cat in viewModel.Categories)
   {
    cat.SubCategories = db.SubCategories.Where(s => s.CategoryID == cat.CategoryID).ToList();
   }
  }
  return PartialView(viewModel);
 }
}

That was the initial attempt, but when I ran this, I was presented with the following exception: The ObjectContext instance has been disposed and can no longer be used for operations that require a connection.

The ObjectContext instance has been disposed

That looked a bit weird to me - I can see the elements of the collection, but the application refuses to iterate through them, complaining about the context. I found a couple of thoughtful posts on the reason for such a behaviour. First post [2] suggested that most likely, the execution of the query was deferred and now, in the view, when it actually tries to execute the query, I have disposed of the DbContext already and it fails. The suggestion was to convert the query results to list using .ToList() so the query gets executed before disposing. That did not work. Another post [3] suggested replacing the foreach iterator with a for one for a number of reasons, but that did not help either.

I gave it some thought and chose an easy way out - remove the dependency on the LINQ and complex entity objects and create my own very simple class to use in the view model. Here is the final code, which worked for me:

View Model

public class LeftMenuViewModel
{
 public List<MenuElement> elements { get; set; }
}

public class MenuElement
{
 public int id { get; set; }
 public string name { get; set; }
 public List<MenuElement> children { get; set; }

}

Controller

public class LeftMenuController : Controller
{
 public PartialViewResult MenuResult()
 {
  LeftMenuViewModel viewModel = new LeftMenuViewModel();
  viewModel.elements = new List<MenuElement>();

  using (RecipesEntities db = new RecipesEntities())
  {
   List<Category> cats = db.Categories.ToList();
   foreach (var category in cats)
   {
                MenuElement element = new MenuElement() {id = category.CategoryID, name = category.CategoryName, children = new List<MenuElement>()};

    List<SubCategory> subcats =
     db.SubCategories.Where(s => s.CategoryID == category.CategoryID).ToList();

    foreach (var subcat in subcats)
    {
     element.children.Add(new MenuElement(){id = subcat.SubCategoryID, name = subcat.SubCategoryName} );
    }
    viewModel.elements.Add(element);
   }
  }
  return PartialView(viewModel);
 }
}

View

@model Recipes.Models.LeftMenuViewModel

@{ Layout = null; }

<div id="menu">
    <ul>
        <li><h2>Recipes Menu</h2>
            <ul>
                <li>@Html.ActionLink("Recipes", "../Recipe/Index")
                    <ul>

                        @for(int i=0; i<Model.elements.Count(); i++)
                        {
                         <li>@Html.ActionLink(Model.elements[i].name, @Html.CategoryAction(Model.elements[i].id).ToString())
                            <ul>
                                @for(int j=0; j<Model.elements[i].children.Count(); j++)
                                {
                                    <li>@Html.ActionLink(Model.elements[i].children[j].name, @Html.SubCategoryAction(Model.elements[i].children[j].id).ToString())</li>
                                }
                            </ul>
                        </li>                           
                        }
                    </ul>
                </li>
            </ul>
        </li>
    </ul>
</div>

While this looks a bit more complex compared to the initial attempt, I think there is really much less space for error. Here is how the menu looks like:

Left side pop-out menu

References:

CSS Pop-Out Menu Tutorial
The ObjectContext instance has been disposed and can no longer be used for operations that require a connection
MVC Razor view nested foreach's model by . Also posted on my website

No comments: