2

I have navbar elements in my _Layout.cshtml which depend on the controller being called. On a search page there will be no navigation but in order to keep the style of the site consistent the navbar itself will remain. I'm not sure what is the most accepted and idiomatic way of performing this work.

_Layout.cshtml

 (etc)
    <nav>
        <div class="container">
            <ul>
                @if (TempData.ContainsKey(KeyChain.ItemKeyTempDataKey))**
                {
                    var itemKey = TempData[KeyChain.ItemKeyTempDataKey] as ItemKey;
                    <li>@Html.ActionLink("Overview", "Index", "Overview", itemKey, new { })</li>
                    <li>@Html.ActionLink("Purchasing", "Index", "InvoiceMovementHistory", itemKey, new { })</li>
                    <li>@Html.ActionLink("Profit Trends", "Index", "SalesMovement", itemKey, new { })</li>
                    <li>@Html.ActionLink("Coupons", "Index", "Coupon", itemKey, new { })</li>
                    <li>@Html.ActionLink("Deals", "Index", "WebDeal", itemKey, new { })</li>
                    <li>@Html.ActionLink("Update Log", "Index", "UpdateLog", itemKey, new { })</li>

                }
            </ul>
        </div>
    </nav>
 (etc)

ItemKey.cs

public class ItemKey
{
    public string Upc { get; set; }
    public string VendorItemCode { get; set; }
    public int Vendor { get; set; }
}

UpdateLogViewModel.cs

public class UpdateLogViewModel
{
    public IEnumerable<UpdateLogEntryViewModel> UpdateLogEntries { get; set; }
}

UpdateLogController.cs

    public ActionResult Index(ItemKey itemKey)
    {
        TempData[KeyChain.ItemKeyTempDataKey] = itemKey;
        //etc uses itemkey to get data in order to generate updateLogViewModel
        return updateLogViewModel();
    }

Things I thought of

  1. Using TempData (as above) to display the navbar elements if the itemkey is populated. TempData, however, is kind of on its way out and feels hacky.

  2. Add a rendersection to the navbar, put the navbar elements in a renderaction and populating them in the section on every view that uses it (which is essentially every view EXCEPT the search view). This just violates DRY on overdrive, but seems to me to be the idiomatic thing.

  3. Derive a secondary sublayout that is an "itemlayout", which would be typed to itemkey and drops the tempdata check. At least provides compile-time checking as long as developers use the itemlayout for item subscreens. But, call me crazy, that's worse because now all of my derived view's viewmodels have to depend on the type from the itemlayout viewmodel. However, this has the advantage of making the dependency clear: if you're going to use this layout, you must derive from this viewmodel that contains an itemkey property. This seems like the most idiomatic way, but I hate the idea of a typed layout.

  4. Move the navbar on to every view page. I will almost certainly not do this, but it should be mentioned that the possibility exists.

So, is there another way I could perform this action idiomatically in MVC, or is one of the options I've listed above the preferred method?

C Bauer
  • 5,003
  • 4
  • 33
  • 62
  • Voting to close because this looks like a duplicate of a few questions. If it is not a duplicate, please let us know how it's different. – George Stocker Feb 13 '15 at 15:54
  • Hi George, I understand that this is marked as a duplicate, but I still do want to know about the rendering of the navbar itself (as you can see it also uses the tempdata check). Should I open a new question for that? – C Bauer Feb 13 '15 at 16:01
  • Do you have an actual problem that's different from the one you described? If it's not, then the specific answer is in one of those links I listed in my answer. If your question is about TempData (and not about how to render a Navbar on every page), then ask that question, but do a little digging to make sure you're not asking a duplicate. – George Stocker Feb 13 '15 at 16:16
  • Hi George, I've edited the question into a format that highlights the latter part of the question (displaying navbar elements) over the former (displaying links with active class automatically). – C Bauer Feb 13 '15 at 16:28

2 Answers2

2

TempData is a bad way to send data around in an ASP.NET MVC application. It's a holdover from the Viewstate days. It's a bad idea.

Instead of TempData, you can make your Navbar a RenderAction, and pass it information from each page it appears on (from the view). You can also use an HtmlHelper (outlined below) to render the links. There's no sense in having all this cooped up in the Layout.cshtml, since it'll have code that doesn't apply to it.

Effectively what you're trying to do is show the active page in a different style.

There are quite a few ways of doing that.

And K. Scott Allen has a blog post about the various methods he uses.

All of this tricks have one thing in common: They all suggest using an HTMLHelper that simply looks at the current page.

Community
  • 1
  • 1
George Stocker
  • 57,289
  • 29
  • 176
  • 237
  • We ended up taking this path by requiring a section called navbarItems on the layout that is required on all pages using it. – C Bauer Feb 24 '15 at 21:20
0

The most natural and canonical way to do this in MVC is by overriding partials. Create a specific controller called SearchController.

Then, create a partial called _Navigation.cshtml in your "Views\Shared" folder, like this:

<nav>
    <div class="container">
        <ul>
            <li>...</li>
            <li>...</li>
            <li>...</li>
        </ul>
    </div>
</nav>

And then, in "Views\Search" create another partial called _Navigation.cshtml like this:

<nav>
    <div class="container">
        <p>Nothing to see here.</p>
    </div>
</nav>

Then, in your layout, when you do this:

@Html.Partial("_Navigation")

The precedence of the view resolver will pick up the latter on the search page and the former everywhere else.

Edit: Based on what I can gather from your comments and updates, you have a controller action that receives some values in the query string and you want to persist those in your action links. The answer is easy.

Assuming the URL /UpdateLog?Upc=xyz&VendorItemCode=abc&Vendor=1 hits your UpdateLog.Index action, then in your view, your links just need to be, e.g.:

@Html.ActionLink("Purchasing", "Index", "InvoiceMovementHistory")

If InvoiceMovementHistory.Index also accepts those parameters, the MVC framework will automatically map the current route parameters to the target route when it is generating the link. No need for you to manage the values at all.

If they're not query string parameters but URL segments, the same applies.

This stateless passing of context from request to request via GET parameters is the ultimate "idiomatic" way of doing this sort of thing in MVC and on the web in general.

Use this in conjunction with the view overriding I described above and you have a very easy way of doing this and switching the nav off for specific pages. That said, given the emerging clarity I would forego the partials and just check a ViewBag.DisableNav property in your layout:

@if (!ViewBag.DisableNav)
{
    <nav>
        <div class="container">
            <ul>
                <li>@Html.ActionLink("Purchasing", "Index",
                                     "InvoiceMovementHistory")</li>
                <li>...</li>
                <li>...</li>
            </ul>
        </div>
    </nav>
}
Ant P
  • 24,820
  • 5
  • 68
  • 105
  • This would still require me to associate the layout with a container for the itemkey object, wouldn't it? And derive all viewmodels from the container? – C Bauer Feb 13 '15 at 18:46
  • My presumption is that this would render `itemkey` redundant given that in your example all it is used for is the conditional (that you would no longer need here). If you have other contextual uses of it then you should elaborate on those. – Ant P Feb 13 '15 at 18:49
  • I am SO sorry, I totally messed up the code when I was cleaning it up to not have too much information about my job. It is an argument to the URL actions. I've edited the usage of itemkey into the OP. – C Bauer Feb 13 '15 at 18:50
  • Where exactly does itemkey come from? If it comes from a route parameter on the current action then it will be *automatically* included in action links to other routes with the same parameter - no need to store/retrieve it yourself. If not, then adding view data to layouts is one of the scenarios in which I would typically add the value to `ViewBag` and try and centralize the logic for doing so (e.g. in a filter or `OnActionExecuting` handler etc.) – Ant P Feb 13 '15 at 18:54
  • You enter the site and search for an item by upc. The searchresults have the itemkey, and pass it through as an anonymous object to all controllers. We are currently pulling them from the tempdata in the layout because that's the only way we know how to 'pass up' a value to the layout from a derived view. – C Bauer Feb 13 '15 at 18:57
  • I see. In that case, if the logic for setting up `ItemKey` can be centralized (i.e. it does not have to be done explicitly on every controller action and can be done in a filter or in `OnActionExecuting`) then I would put it in the ViewBag. Otherwise, if every controller action deals with it independently, I would use a @section in your layout and have views render a partial in it as appropriate (with `@Html.Partial("_Navigation", itemkey)` and then treat itemkey as the model inside that partial. – Ant P Feb 13 '15 at 19:01
  • Just so you know - it is seeming increasingly like this question is not really about views but about how to pass the information among your controller actions - you might be better retargeting your question (as a separate question now that this one has answers already). – Ant P Feb 13 '15 at 19:05
  • Well it all circles back to the fact that even if I have the itemkey being rehydrated in a controller action, I still need the itemkey on the layout page somehow, which is already accomplished by using TempData. I just don't think TempData is the right way to go here. Realistically the same result occurs if I create a class that consumes the controller and sets its tempdata = itemkey and put it in every constructor. – C Bauer Feb 13 '15 at 19:07
  • Again, that's where using either ViewBag or a partial comes in. `ViewBag.ItemKey = someObj` in your controller would put it in the scope of the layout (and keep it out of session state, i.e. TempData) and if you don't like that then wrap the nav up in a partial and call it from each view. – Ant P Feb 13 '15 at 19:08
  • Yes, which is #2 that I mentioned in the list of things I thought of and was hoping to get confirmation for.. . – C Bauer Feb 13 '15 at 19:10
  • So ViewBag it is, then? It sounds like the perfect candidate. There's nothing inherently wrong with it for purposes like this and - in fact - if I had fully known your requirements from the off, I would have suggested it as the most canonical way of getting variable data into the layout. – Ant P Feb 13 '15 at 19:11
  • 1
    If I was going to move everything into the individual controllers, I'd just add the itemkey to the viewmodel of those controllers and call it there. If I was going to keep the code in the layout, I'm still where I was at the beginning but would just changed over to viewdata/viewbag. I'm not quite comfortable with it because it doesn't really fix anything, it just offloads it to a different string dictionary, but the smelly code remains. – C Bauer Feb 13 '15 at 19:19
  • There is nothing smelly about ViewBag in the scenario you have described - there is everything smelly about TempData. If this stuff is coming from a search, why isn't it in your routes (i.e. in query string parameters)? This question is becoming increasingly obscure. – Ant P Feb 13 '15 at 19:22
  • Who said it wasn't in my routes? The *layout has no model so I can't access the model properties to rehydrate the actionlink*. That's why I suggested option 3 in the OP. – C Bauer Feb 13 '15 at 19:24
  • 1
    If the values are in your route data, you don't need to rehydrate the values, the framework will do it for you when it finds matching values on the target controller. Like I explained half an hour ago. When I was probing for further information about the nature of the data and how it is populated. About which I am still really none the wiser. I am a little amazed at how you can be the one that is apparently getting frustrated here, to be honest. Again, this all depends on the composition of your controllers w.r.t. these values. Your question is not about what you think it is about. – Ant P Feb 13 '15 at 19:29
  • To be clear - if action A has parameter `param` and action B has parameter `param` and the view rendered by A calls `Html.Action("Click", "B")`, it will *automatically* resolve `param` unless you explicitly null it out (or B has a `param`less route with higher precedence) - no need to pass an anonymous object at all. If this is still irrelevant, then you need to what I suggested 40 minutes ago now and elaborate on the composition of your controller actions (including posting their signatures and salient code). – Ant P Feb 13 '15 at 19:44
  • So the partial view should be typed to itemkey and it will automatically resolve? I also added controller code to the OP – C Bauer Feb 13 '15 at 19:52
  • You don't even need that - if the relevant values are in your route (i.e. they are query string parameters or they are URL route params) and the target controller has equivalent parameters, those values are already in the view context and the framework will automatically try and include them in links where it can. I could be specific if you detailed precisely what route values you have that you are trying to carry across, what parameters they map from in the source and target controllers etc. – Ant P Feb 13 '15 at 19:53