In the last post I talked about implementing a custom route in order to improve the route handling work to support multiple languages in a web application. This custom route would remove the need of duplicate the route (with and without the language parameter) and in the first part of this series I showed an example where the view rendered the language selector using ActionLink for generating the urls, and the collection to iterate was expected to be as part of the view model.
But this is not always possible to do, in fact, the most common scenarios I’ve dealt with, involve putting this kind of component in a common place such as the Layout page or even a partial which is included in Layout page, but less frequent in a dedicated view. To achieve this separation, it’s common to use a Child Action for invoke a controller, which is going to execute an action and return usually a partial view.
If we take the solutions previously discussed and make a little refactoring: create an action (LanguageList) in the languages controller and move the code that renders the links to a partial view (_langSelector) and go to layout page and invoke it as:
@Html.Action("LanguageList", "Langs")
Sure you are going to see the results. The controller action should be like this:
[ChildActionOnly] public ActionResult LanguageList() { var langs = LanguageProviders.Current.GetList(); return PartialView("_langSelector", langs); }
The HTML rendered looks like this:
<a href="/Langs/LanguageList">English</a> <a href="/es-ES/Langs/LanguageList">Español</a> <a href="/fr-FR/Langs/LanguageList">Français</a>
As you can see, the urls always point to controller Langs and action LanguageList instead of pointing to current controller and action, why does it happen?
When we invoke a child action, a new request is started to the target action, that means, when the route asks for current request values, it obtains those from the new request, which means, the “original” route values were lost at the moment of making the child request.
As these values were lost, my solution is simply to provide them via parameter at invocation time. Once in the controller, the values must be passed to the view, may be using the ViewBag or even a more sophisticated solution could involve the creation of a view model with these data ready to be read by the view.
Having said that, here is the quick solution:
The controller action
[ChildActionOnly] public ActionResult LanguageList() { ViewBag.CurrentValues = RouteData.Values["CurrentValues"]; var langs = LanguageProviders.Current.GetList(); return PartialView("_langSelector", langs); }
At this point and only to remark, the attribute ChildActionOnly is strongly recommended in order to avoid this action to be called directly or even from an AJAX request if this is not intended to.
The invoke statement in the layout page.
@Html.Action("LanguageList", "Langs", new { CurrentValues = ViewContext.RouteData.Values } )
The goal of this new parameter is to pass the real current route values to the controller, so it will be able to grab them and pass to the view.
The partial view
@{ var currentValues = (RouteValueDictionary)ViewBag.CurrentValues; var tempValues = new RouteValueDictionary(); foreach (var item in currentValues) { tempValues.Add(item.Key, item.Value); } } @foreach (var lang in Model) { tempValues["lang"] = lang.Code; @Html.ActionLink(lang.Name, null, tempValues) @Html.Raw(" ") }
At this point, I consider important the lines that make a copy (clone) of the route data received from the controller, otherwise the route values should be modified inside the loop that follows it, and as a consequence, the dictionary will affect the rest of routes in the page, setting the last value in lang.Code forever in the route values if this value were not supplied explicitly when generating the links.
Well, I hope this little sand grain will help you to your day-to-day web programming task, any comment and ideas will be welcome.
No comments:
Post a Comment