views:

59

answers:

2

I'm quite new to both ASP.Net and MVC.

I got the following code in my master page:

  <div id="main-menu" class="menu">
  <%
   var items = (IList<CompanyName.Framework.Web.MenuItem>)ViewData["MainMenu"];
   if (items.Count > 0)
   {
    %><ul><%
    foreach (var item in items)
    {
     if (!string.IsNullOrEmpty(item.RequiredRole) && !System.Threading.Thread.CurrentPrincipal.IsInRole(item.RequiredRole))
      continue;

     %><li><a href="<%= item.Uri %>"><%= item.Title %></a></li><%
    }
    %></ul><%
   }
  %>
 </div>

Can I move the code to another file or refactor the code in any way?

edit:

My ApplicationController that all controllers derive:

public class ApplicationController : Controller
{
    List<MenuItem> _mainMenu = new List<MenuItem>();
    List<MenuItem> _contextMenu = new List<MenuItem>();

    protected IList<MenuItem> MainMenu
    {
        get { return _mainMenu; }
    }

    protected IList<MenuItem> ContextMenu
    {
        get { return _contextMenu; }
    }

    protected string PageTitle { get; set; }

    protected override void OnResultExecuting(ResultExecutingContext filterContext)
    {
        ViewData["PageTitle"] = PageTitle;
        ViewData["MainMenu"] = MainMenu;
        ViewData["ContextMenu"] = ContextMenu;
        base.OnResultExecuting(filterContext);
    }
}
+3  A: 

Yes, you can just put that block into an .ascx file and use:

<% html.RenderPartial("myPartialFile.asx"); %>

The above assumes that myPartialFile.ascx is located in the same folder as your master page, usually, the Views/Shared folder.

awrigley
In fact the partial must be in the same folder OR in the Views/Shared folder. No need to move both there. Even though you are of course right about master pages being usually shared.
Adrian Grigore
+1 - exactly what i was going to suggest L:)
jim
+7  A: 

Here are a couple of suggestions:

Improvement number 1: use view models and strongly typed views instead of ViewData

public ActionResult Index()
{
    // TODO: Fetch this data from a repository
    var menus = new[] {
        new MenuItem(), new MenuItem()
    }.ToList();

    return View(menus);
}

and then in your view:

<div id="main-menu" class="menu">
    <%
        if (Model.Count > 0)
        {
            %><ul><%
            foreach (var item in Model)
            {
                if (!string.IsNullOrEmpty(item.RequiredRole) && !System.Threading.Thread.CurrentPrincipal.IsInRole(item.RequiredRole))
                    continue;

                %><li><a href="<%= item.Uri %>"><%= item.Title %></a></li><%
            }
            %></ul><%
        }
    %>
</div>

Still horrible and completely unreadable tag soup.


Improvement number 2: use editor/display templates:

In ~/Views/Home/DisplayTemplates/MenuItem.ascx:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<CompanyName.Framework.Web.MenuItem>" %>

<% if (!string.IsNullOrEmpty(Model.RequiredRole) &&
       System.Threading.Thread.CurrentPrincipal.IsInRole(Model.RequiredRole)) { %>
    <li>
        <a href="<%= Model.Uri %>"><%= Model.Title %></a>
    </li>
<% } %>

And then in your main view:

<div id="main-menu" class="menu">
    <ul>
        <%= Html.DisplayForModel() %>
    </ul>
</div>

Improvement number 3: Avoid coding business rules in a view. So in your view model add a property:

public bool IsLinkVisible
{
    get
    {
        return !string.IsNullOrEmpty(RequiredRole) &&
               Thread.CurrentPrincipal.IsInRole(RequiredRole);
    }
}

so that your display template now looks like this:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<CompanyName.Framework.Web.MenuItem>" %>
<% if (Model.IsLinkVisible) { %>
    <li>
        <a href="<%= Model.Uri %>"><%= Model.Title %></a>
    </li>
<% } %>

Improvement number 4: Write a custom HTML helper to render this anchor because writing C# in a view is still ugly and untestable:

public static class HtmlExtensions
{
    public static MvcHtmlString MenuItem(this HtmlHelper<MenuItem> htmlHelper)
    {
        var menuItem = htmlHelper.ViewData.Model;
        if (!menuItem.IsLinkVisible)
        {
            return MvcHtmlString.Empty;
        }
        var li = new TagBuilder("li");
        var a = new TagBuilder("a");
        a.MergeAttribute("href", menuItem.Uri);
        a.SetInnerText(menuItem.Title);
        li.InnerHtml = a.ToString();
        return MvcHtmlString.Create(li.ToString());
    }
}

and finally your display template:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<CompanyName.Framework.Web.MenuItem>" %>
<%= Html.MenuItem() %>
Darin Dimitrov
+1 nice improvement flow!!
jim
The problem is that I use my BusinessObjects as models for most views. The MainMenu (and a ContextMenu) are properties on the ApplicationControlLer and are assigned to the ViewData before the ActionResult is executed. Sure, I could do a ApplicationViewData<T> which takes the business object as type argument. But that doesn't seem as a clean solution as properties in the controller and using ViewData["MainMenu"] in the master page, since only the master page is affected by the latter. I like your solution, but how can I use it without have to use typed views?
jgauffin
If it is in the master page it means that all views need this property. So you could make all your view models derive from a base view model which contains the menu as property. This property could then be populated in an action filter to avoid doing it in all actions and then the master page could be strongly typed to this base view model.
Darin Dimitrov
The last comment was before I saw Improvement #4. I'll stick to it and the IsLinkVisible and have the rest in the MasterPage (2 line display template doesnt seem very effient).
jgauffin
I don't use special ViewModels. I use my business objects that are used in the rest of the application (it's not just a web) as models in the views.
jgauffin
Yes, that's not very good and it may lead to ugly code in the views.
Darin Dimitrov
Why? The views are still typed. If I need to have two businessobjects in the same view I'll just create a ViewModel wrapping those two business objects.
jgauffin
ViewModels are not for just wrapping business objects. They should also contain only the properties needed by the view. They are designed for a given view. So that if you need to business objects in a view you shouldn't wrap them in a view model but you should rather create a view model containing a mixture of the properties of those two business objects. I would suggest you looking at [AutoMapper](http://automapper.codeplex.com/) which could automate conversions between objects.
Darin Dimitrov
Sorry, I still disagree. The website is built to manage the business objects (through a service layer), why should I then create a ViewModel that are like a stripped down business object? No harm in having some properties that arent used in the view. If it looks like duck and quacks like duck it's probably a duck (and not a ViewModel).
jgauffin
Well, I won't be trying to convince you because everyone has it's own opinion and vision and I respect yours. IMO there could be harm in having properties that aren't used in a view, there's mass assignment injection attacks which could directly modify business object properties which are never meant to be modified in a view like `IsAdmin` for example. I agree that the web site is designed to manage business objects, there's no doubt here, it's just that the you can see the web site as a GUI representation of a business object, thus the need of view models.
Darin Dimitrov
Also how do you handle things like validation? Imagine for example that there's a user requirement that a form is split in two steps like a wizard. If you use the business object directly how do you know which property to validate? Some will be null because the user left them blank, others will be null because they never appear in the GUI, this could quickly turn into a nightmare. But once again I don't want to argue, it's just my 2 cents :-)
Darin Dimitrov
Mass Assignment attacks are a valid point, but no problem here since it's intranet sites and I hardly think that anyone on the intranet would do a attack. Also, they can only change stuff they have access to (the role based security..). Wizards are no problem, jquery to the rescue. I would never create a wizard that does postbacks (horribly thought). I'm no newbie when it comes to application architecture or programming, just when it comes to asp.net. Validations are made by attributes and Validation Application Block.
jgauffin
what I wanted to know whas: How do I create a partial and call it with a parameter from the ViewData dictionary?
jgauffin
The `Html.RenderPartial` helper method has an overload allowing you to pass additional values as second argument.
Darin Dimitrov
How do I get that parameter in the partial?
jgauffin
`ViewData["parameterName"]`.
Darin Dimitrov