MVC RouteLink being double encoded - asp.net-mvc

I have the following link defined in a page that will be built based on the route defined in the web.config
<%= Html.RouteLink(product.DisplayName, "ShopProductNames", new {Id = product.Id, ProductName = product.DisplayName.Replace(' ', '-') }) %>
I need to URL encode the DisplayName in the URL of that link, however, when I add encoding in the following way:
<%= Html.RouteLink(product.DisplayName, "ShopProductNames", new {Id = product.Id, ProductName = Url.Encode(product.DisplayName.Replace(' ', '-')) }) %>
It double encodes my DisplayName (in the URL), and I get an error in IIS.
My DisplayName property is not being encoded before it's passed to the page. Also, RouteLink does not appear to be Url encoding for the rendered link by default as it's not picking up spaces or ampersands when the page is rendered.
Anyone know what I'm doing wrong?
UPDATE: I'm actually referring to the URL generated by RouteLink, not the link text itself
UPDATE 2: here is the route I'm using
routes.MapRoute(
"ShopProductNames",
"Shop/{productName}/p/{id}/{*nameInfo}",
new
{
controller = "Product",
action = "Detail"
}
);

Look at HtmlHelper.cs file, line 140:
internal static string GenerateUrl(string routeName, string actionName, string controllerName, RouteValueDictionary routeValues, RouteCollection routeCollection, RequestContext requestContext, bool includeImplicitMvcValues)
{
RouteValueDictionary mergedRouteValues = RouteValuesHelpers.MergeRouteValues(actionName, controllerName, requestContext.RouteData.Values, routeValues, includeImplicitMvcValues);
VirtualPathData vpd = routeCollection.GetVirtualPath(requestContext, routeName, mergedRouteValues);
if (vpd == null) {
return null;
}
string modifiedUrl = PathHelpers.GenerateClientUrl(requestContext.HttpContext, vpd.VirtualPath);
return modifiedUrl;
}
Url is created by routeCollection.GetVirtualPath() method (System.Web.Routing.dll). Using Reflector you'll see that it uses Uri.EscapeDataString() internally (System.Web.Routing.ParsedRoute.Bind method)

Related

Opening PDF in browser at a specific page

I am using a custom html helper #Html.ActionLink to find and return a PDF file to the browser and open in a new tab. The issue I am now having is when I am trying to open the PDF on a specific page.
The file is found, returned and opened just fine when NOT trying to specify a page parameter for the PDF to open on. Therefore, there must be some issue with my approach to setting the parameter.
According to Adobe documentation, #page=? parameters can be appended to the end of a URL to open a PDF at a specific page. However, my approach for doing so is not working.
See Adobe documentation on the subject here.
Razor helper in use:
#Html.FileLink("Document Link", "\\MyLocation\\MyDocument.pdf", "4", new { #target = "_blank" })
Helper method:
public static MvcHtmlString FileLink(this HtmlHelper helper, string LinkText, string FilePath, string PageNumber, object htmlAttributes = null)
{
return helper.ActionLink(LinkText, "ShowFile", "Home", new { path = System.Uri.EscapeDataString(FilePath), page = PageNumber }, htmlAttributes);
}
ShowFile method:
public ActionResult ShowFile(string path, string page)
{
// My attempt at passing and setting the page parameter!
path = System.Uri.UnescapeDataString(path + "#page=" + page);
// Get actual path to file, file name
var filePath = string.Format("{0}{1}", ConfigurationManager.AppSettings["DocumentsRoot"], path);
// Get MIME type
var contentType = MimeMapping.GetMimeMapping(path);
// Return file
return File(filePath, contentType);
}
The page parameter in the adobe documentation is an anchor tag (e.g. #page=5), not a query string parameter (?page=5). You can use a different ActionLink override to specify this at the same time:
Html.ActionLink(LinkText, "ShowFile", "Home", null, null, "page=" + PageNumber,
new { path = System.Uri.EscapeDataString(FilePath) }, null)
This will generate a link which looks like...
/Home/ShowFile?path=myfilename.txt#page=5
instead of
/Home/ShowFile?path=myfilename.txt&page=5
You can then remove the page parameter from your ShowFile method, because it is only needed on the client side.

Method not found: 'System.Web.Mvc.MvcHtmlString System.Web.Mvc.Html.LinkExtensions.RouteLink(System.Web.Mvc.HtmlHelper, System.String, System.Object)'

I have a project that it had been developed under MVC1 and after aa few months ago I upgraded it in MVC2.
Everything was well, uppon the day I needed to format my computer.
And what can goes wrong with a format? I don't know Laughing
I have installed the MVC2 after the formating,I import the existing project, I build it, and no error displayed while it been build, but from the time I've uploaded it in the production server I am getting this error.
Method not found:
'System.Web.Mvc.MvcHtmlString
System.Web.Mvc.Html.LinkExtensions.RouteLink(System.Web.Mvc.HtmlHelper,
System.String, System.Object)'.
I can't understand what caused the problem.
Certainly I assumed that is have to do with MVC1 and I referenced it, but with no luck again.
Theese are the methods that the problem is came from
public static string PagerLinks(this HtmlHelper htmlHelper, string controllerName,
string actionName, int pageCount, int pageIndex, string cssClass, string moreTextCssClass,
string currentPageCssClass, int totalProducts) {
return PagerLinks(htmlHelper, controllerName, actionName, pageCount, pageIndex, "First",
"Previous", "Next", "Last", "more pages", cssClass, moreTextCssClass,
currentPageCssClass, totalProducts);
}
public static string PagerLinks(this HtmlHelper htmlHelper, string controllerName,
string actionName, int pageCount, int pageIndex, string firstPage, string previousPage,
string nextPage, string lastPage, string moreText, string cssClass, string moreTextCssClass,
string currentPageCssClass, int totalProducts) {
var builder = new StringBuilder();
if (String.IsNullOrEmpty(controllerName))
throw new Exception("controllerName and actionName must be specified for PageLinks.");
if (pageCount <= 1)
return String.Empty;
if (String.IsNullOrEmpty(cssClass)) builder.Append("<div>");
else builder.Append(String.Format("<div class=\"{0}\">", cssClass));
builder.Append(string.Format("{0} <b>{1}</b>", "total", totalProducts));
builder.Append("<br />");
if (pageIndex != 1) {
// first page link
builder.Append(
htmlHelper.RouteLink( "First", new {
controller = controllerName,
action = actionName,
id = 1
}, new { title = firstPage }
)
);
////
///
more code
///
///
}
This is the call syntax
<%=Html.PagerLinks((string)ViewData["Controller"], "Page", (int)ViewData["TotalPages"], (int)ViewData["Page"], "theCssClass", "theMoreTextCssClass", "theCurrentPageCssClass", (int) ViewData["TotalProducts"])%>
Inside PagerLinks method exists this line that I supposed that this is ,
as the error message said, the error.
htmlHelper.RouteLink( "First", new {controller = controllerName,action = actionName,id = 1}, new { title = firstPage })
but the I am getting the error only when i call it via
<%=Html.PagerLinks((string)ViewData["Controller"], "Page", (int)ViewData["TotalPages"], (int)ViewData["Page"], "theCssClass", "theMoreTextCssClass", "theCurrentPageCssClass", (int) ViewData["TotalProducts"])%>
if I comment out the above line and replaced it with the below line just for testing, nothing happens
htmlHelper.RouteLink( "First", new {controller = controllerName,action = actionName,id = 1}, new { title = firstPage })
I'll appreciate any suggestions
Check for any refernces in your web.config and make sure it is referencing v2 for System.Web.MVC.
I got this myself when updating from MVC1 to MVC2 on:
<%=Html.ActionLink<AccountController>(a=>a.LogOut(),"Logout") %>
If you use MVC Futures (as I did in ActionLink above) or MVC Contrib, don't forget to update these to MVC2. It surely solved my problem.
Good luck/Lasse
When I had this problem, I updated my version of the System.Web.MVC.dll in
C:\Program Files (x86)\Microsoft ASP.NET\ASP.NET MVC 2\Assemblies and then stopped/restarted my web servers and all was right with the world.

How to leave URL parameters unescaped in ASP.NET MVC?

I've noticed the returnurl URL parameter on the Stackoverflow login/logout links are not escaped but when I try to add path as a parameter to a route it gets escaped.
So /login?returnurl=/questions/ask shows /login?returnurl=%2fquestions%2fask and it's kind of ugly. How do I get it to not escape the returnurl value?
Here's what I'm doing in the code:
Html.ActionLink("Login", "Login", "Account", new { returnurl=Request.Path }, null)
How do I get it to not escape the
returnurl value
How's about this?
var url = Url.Action("Login", "Account", new {returnurl = Request.Path});
var unEncodedUrl = HttpUtility.UrlDecode(url);
Response.Write("<a href='" + unEncodedUrl + "'>...</a>");
Be sure that's what you want though, URL encoding has its purpose.
I understand one of the comments about encoding happening for a reason; this would only be an exception, not the rule.
Here's what I put together, how can it be improved?
public static string ActionLinkNoEscape(this HtmlHelper html, string linkText, string actionName, string controllerName, object values, object htmlAttributes)
{
RouteValueDictionary routeValues = new RouteValueDictionary(values);
RouteValueDictionary htmlValues = new RouteValueDictionary(htmlAttributes);
UrlHelper urlHelper = new UrlHelper(html.ViewContext.RequestContext, RouteTable.Routes);
string url = urlHelper.Action(actionName, controllerName);
url += "?";
List<string> paramList = new List<string>();
foreach (KeyValuePair<string, object> pair in routeValues)
{
object value = pair.Value ?? "";
paramList.Add(String.Concat(pair.Key, "=", Convert.ToString(value, CultureInfo.InvariantCulture)));
}
url += String.Join("&", paramList.ToArray());
TagBuilder builder = new TagBuilder("a");
builder.InnerHtml = string.IsNullOrEmpty(linkText) ? "" : HttpUtility.HtmlEncode(linkText);
builder.MergeAttributes<string, object>(htmlValues);
builder.MergeAttribute("href", url);
return builder.ToString(TagRenderMode.Normal);
}
The parameter is not unescaped. You'll notice the URL:
http://stackoverflow.com/users/login?returnurl=%2fquestions%2fask
does actually work - SO is reading and unescaping that parameter as normal. If you wanted to include other out-of-bounds characters such as '&' in the parameter you would still have to escape them.
The trick is merely that the '/' character in particular does not need to be %-escaped in query parameters. It does have to be escaped in other contexts such as in a path part, so URLEncode always encodes it, to be safe.
If you just want the URL to look prettier, simply escape the parameter as normal (which you must do to escape all the other characters that must be handled correctly), and then do a string replace on '%2f' with '/'.
My solutionto a similar problem was to write my own extension. After digging around in the code I couldn't find a way to do it otherwise. Yours might look like this.
public static class HtmlHelperExtensions
{
public static string LoginLinkWithReturnUrl( this HtmlHelper helper,
string linkText,
string action,
string controller,
string returnUrl,
object htmlAttributes )
{
TagBuilder builder = new TagBuilder("a");
builder.Attributes.Add( "href",
string.Format( "/{0}/{1}?returnurl={2}",
controller,
action,
returnUrl ) );
var attrDict = new RouteValueDictionary( htmlAttributes );
builder.MergeAttributes( attrDict );
builder.InnerHtml = linkText;
return builder.ToString();
}
}
I think I had the same problem making and using a UrlHelper so I went with the string.Format mechanism instead. YMMV.
I don't believe there's a way around it that's built into the framework. The actual construction of the URL happens in the System.Web.Routing.ParsedRoute.Bind method and there aren't any conditions used to prevent the escaping.
Looks like an extension method is the way to go but one that is slightly more robust than the one mentioned previously.

How do I access query string parameters in asp.net mvc?

I want to have different sorting and filtering applied on my view
I figured that I'll be passing sorting and filtering params through query string:
#Html.ActionLink("Name", "Index", new { SortBy= "Name"})
This simple construction allows me to sort. View comes back with this in query string:
?SortBy=Name
Now I want to add filtering and i want my query string to end up with
?SortBy=Name&Filter=Something
How can I add another parameter to list of already existing ones in ActionLink? for Example:
user requests /Index/
view has
#Html.ActionLink("Name", "Index", new { SortBy= "Name"})
and
#Html.ActionLink("Name", "Index", new { FilterBy= "Name"})
Links: The first one looks like /Index/?SortBy=Name and The second is /Index/?FilterBy=Name
I want when user pressed sorting link after he applied some filtering - filtering is not lost, so i need a way to combine my params.
My guess is there should be a way to not parse query string, but get collection of parameters from some MVC object.
so far the best way I figured out is to create a copy of ViewContext.RouteData.Values
and inject QueryString values into it.
and then modify it before every ActionLink usage.
still trying to figure out how to use .Union() instead of modifying a dictionary all the time.
<% RouteValueDictionary tRVD = new RouteValueDictionary(ViewContext.RouteData.Values); %>
<% foreach (string key in Request.QueryString.Keys )
{
tRVD[key]=Request.QueryString[key].ToString();
} %>
<%tRVD["SortBy"] = "Name"; %>
<%= Html.ActionLink("Name", "Index", tRVD)%>
My solution is similar to qwerty1000's. I created an extension method, ActionQueryLink, that takes in the same basic parameters as the standard ActionLink. It loops through Request.QueryString and adds any parameters found to the RouteValues dictionary that are not already present (so we can overwrite the original query string if needed).
To preserve the existing string but not add any keys the usage would be:
<%= Html.ActionQueryLink("Click Me!","SomeAction") %>
To preserve the existing string and add new keys the user would be:
<%= Html.ActionQueryLink("Click Me!","SomeAction", new{Param1="value1", Param2="value2"} %>
The code below is for the two usages, but it should be pretty easy to add other overloads to match the other ActionLink extensions as needed.
public static string ActionQueryLink(this HtmlHelper htmlHelper,
string linkText, string action)
{
return ActionQueryLink(htmlHelper, linkText, action, null);
}
public static string ActionQueryLink(this HtmlHelper htmlHelper,
string linkText, string action, object routeValues)
{
var queryString =
htmlHelper.ViewContext.HttpContext.Request.QueryString;
var newRoute = routeValues == null
? htmlHelper.ViewContext.RouteData.Values
: new RouteValueDictionary(routeValues);
foreach (string key in queryString.Keys)
{
if (!newRoute.ContainsKey(key))
newRoute.Add(key, queryString[key]);
}
return HtmlHelper.GenerateLink(htmlHelper.ViewContext.RequestContext,
htmlHelper.RouteCollection, linkText, null /* routeName */,
action, null, newRoute, null);
}
<%= Html.ActionLink("Name", "Index", new { SortBy= "Name", Filter="Something"}) %>
To preserve the querystring you can:
<%= Html.ActionLink("Name", "Index",
String.IsNullOrEmpty(Request.QueryString["SortBy"]) ?
new { Filter = "Something" } :
new { SortBy=Request.QueryString["SortBy"], Filter="Something"}) %>
Or if you have more parameters, you could build the link manually by using taking Request.QueryString into account.
Use ActionLinkCombined instead of ActionLink
public static string ActionLinkCombined(this HtmlHelper htmlHelper, string linkText, string actionName,
object routeValues)
{
var dictionary = new RouteValueDictionary();
foreach (var pair in htmlHelper.ViewContext.Controller.ValueProvider)
dictionary[pair.Key] = pair.Value.AttemptedValue;
if (routeValues != null)
{
foreach (PropertyDescriptor descriptor in TypeDescriptor.GetProperties(routeValues))
{
object o = descriptor.GetValue(routeValues);
dictionary[descriptor.Name] = o;
}
}
return htmlHelper.ActionLink(linkText, actionName, dictionary);
}
MVC4
#Html.ActionLink("link text","action",new { #id = 5, #name = "textName", #abc = "abc" })
OR
#Html.ActionLink("link text", "action", "controller", new { #id = 5, #name = "textName", #abc = "abc" }, new { #class = "cssClass" })
querystring would be like:
yourDomainRout/action/5?name=textName&abc=abc
it would have class="cssClass"

GetVirtualPath method in mvc

Can anyone explain how the virtual path is being calculating?
According to the RouteData.Values or according to the url pattern?
I'm trying to remove some routedata values but still the virtual path is not changes.
I have a problem that the virtual path return with redundant slash at the beginning of the URL like : /he/controller/action the slash before culture is redundant...
I'm using custom routes like the following
routes.Add("Default",
new CustomRoute("{culture}/{controller}/{action}/{id}",
new
{
controller = "Desktop",
action = "Index",
culture = "he-IL",
guid = "",
id = UrlParameter.Optional
}));
routes.Add("Wizard_" + wizard,
new CustomRoute("{guid}/{culture}/" + wizardName + "/{action}/{id}",
new
{
controller = wizard,
action = "Index",
culture = "he-IL",
guid = "",
id = UrlParameter.Optional
}));
the problem is when using Url.Action(action, controller) method and the action is in the wizard controller, so the URL for the action is wizard format like {guid}/{culture}/" + wizard + "/{action}/{id}
bu the guid value is empty and the returned URL is //he-il/controller/action
instead of /he-il/controller/action
The CustomRoute class:
public class CustomRoute : Route
{
private List<string> _wizards;
public CustomRoute(string uri, object defaults)
: base(uri, new RouteValueDictionary(defaults), new MvcRouteHandler())
{
_wizards = new List<string>();
FillWizards(ref _wizards);
DataTokens = new RouteValueDictionary();
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
bool hasGuid = httpContext.Request.RequestContext.RouteData != null
&& httpContext.Request.RequestContext.RouteData.Values != null
&& httpContext.Request.RequestContext.RouteData.Values.ContainsKey("guid")
&& !httpContext.Request.RequestContext.RouteData.Values["guid"].ToString().Equals(Guid.Empty);
var routeData = base.GetRouteData(httpContext);
if (routeData == null)
return null;
bool isWizard = _wizards.Contains(routeData.Values["controller"].ToString());
Debug.WriteLine("Controller: " + routeData.Values["controller"] + " action: " + routeData.Values["action"] + " Is wizard: " + isWizard + " has guid: " + hasGuid);
if (isWizard && !hasGuid)
{
if (string.IsNullOrEmpty(routeData.Values["guid"].ToString()))
{
routeData.Values["guid"] = Guid.NewGuid().ToString("N");
}
}
return routeData;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
VirtualPathData path;
path = base.GetVirtualPath(requestContext, values);
return path;
}
private void FillWizards(ref List<string> items)
{
var _configuration = ObjectFactory.GetInstance<IConfiguration>();
List<string> wizards = _configuration.GetParamValue<string>("SessionUniqueWizards", "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList();
items = wizards;
}
}
The reason why your guid parameter is missing is because
There is no guid parameter in your call to Url.Action(action, controller).
There is (apparently) no guid parameter in the current request. That is, the current route that is being hit has no guid route value.
You have specified the default value for the guid (guid = ""). Since empty string is what you specified as the default, empty string is what you are getting by default.
For the URL to build correctly, the guid has to come from somewhere. MVC always passes matching route values from the current request when building outgoing URLs, but since not all of your URLs have a guid you need to specify it for the pages where it does not exist in the context:
Url.Action("Index", "Search", new { guid = "a565f84f9152495792d433f5bd26000f")
This is the normal way to do it. Typically, if you are building the link for a CRUD operation you are doing so within a list of entities.
For example, for a Product entity, you would normally have links for Edit Product and Delete Product for each Product in a list that would look something like this:
<tr>
<td>model.ProductName</td>
<td>#Html.ActionLink("Product", "Edit", new { guid = model.ProductId })</td>
<td>#Html.ActionLink("Product", "Delete", new { guid = model.ProductId })</td>
</tr>
There would also typically be a link to add a new entity that has no identifier.
#Html.ActionLink("Product", "Add")
But it is unclear from your example how a "wizard" would be created. Creating a new identifier should normally be a function of the Add method, not that of a route.
But there seems to be an issue where you are randomly generating a GUID within your route, so it is unclear how you expect this value to be maintained from one request to the next.

Resources