Opening PDF in browser at a specific page - asp.net-mvc

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.

Related

How to download a file to client from server?

I have an MVC project where I'd like the user to be able to download a an excel file with a click of a button. I have the path for the file, and I can't seem to find my answer through google.
I'd like to be able to do this with a simple button I have on my cshtml page:
<button>Button 1</button>
How can I do this? Any help is greatly appreciated!
If the file is not located inside your application folders and not accessible directly from the client you could have a controller action that will stream the file contents to the client. This could be achieved by returning a FileResult from your controller action using the File method:
public ActionResult Download()
{
string file = #"c:\someFolder\foo.xlsx";
string contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
return File(file, contentType, Path.GetFileName(file));
}
and then replace your button with an anchor pointing to this controller action:
#Html.ActionLink("Button 1", "Download", "SomeController")
Alternatively to using an anchor you could also use an html form:
#using (Html.BeginForm("Download", "SomeController", FormMethod.Post))
{
<button type="submit">Button 1</button>
}
If the file is located inside some non-accessible from the client folder of your application such as App_Data you could use the MapPath method to construct the full physical path to this file using a relative path:
string file = HostingEnvironment.MapPath("~/App_Data/foo.xlsx");
HTML:
<div>#Html.ActionLink("UI Text", "function_name", "Contoller_name", new { parameterName = parameter_value },null) </div>
Controller:
public FileResult download(string filename) {
string path = "";
var content_type = "";
path = Path.Combine("D:\file1", filename);
if (filename.Contains(".pdf"))
{
content_type = "application/pdf";
}
return File(path, content_type, filename);
}

hide parameters passing to controller in address bar (URL rewrite or something else)

I have the following routes:
routes.MapRoute("Event Overview", "{city}/{type}/{id}",
new {city="LA", controller = "BaseEvent", action = "EventOverview"}, new {city = new CityConstraint()});
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
And, several links on my site:
#Html.ActionLink("Make", "EventOverview", "BaseEvent", new { id = eventInfo.Key.OID, type = eventInfo.Key.XPObjectType.TypeName.GetShortTypeName(), activeTab = "#scheduleLink", session = eventInfo.Key.EventSchedules[0].SessionId, hall = eventInfo.Key.EventSchedules[0].HallId, client = eventInfo.Key.EventSchedules[0].BasePlace.PremieraClientId}, null)
#Html.ActionLink("Make", "EventOverview", "BaseEvent", new { id = eventInfo.Key.OID, type = eventInfo.Key.XPObjectType.TypeName.GetShortTypeName(), activeTab = "#scheduleLink", }, null)
This is `EventOverview action:
public ActionResult EventOverview(int id, string type, string activeTab,string hall, string session, string client, string count)
{
var model = CreateEventViewData<EventViewData>(id, type);
model.ActiveTab = activeTab;
model.ScheduleCount = count;
model.SessionId = session;
model.HallId = hall;
model.ClientId = client;
return View("Controls/EventsInfo/EventInfo", model);
}
In the first link passing many parameters, and all shows in browser's address field:
This is for firts link:
http://localhost:62291/LA/Film/36?activeTab=%23scheduleLink&session=15&hall=65&client=2&count=1
This is for second link:
http://localhost:62291/LA/Film/36?activeTab=%23scheduleLink
I want something like that:
http://localhost:62291/LA/Film/36
What ways to hide parameters in an address line are?
UPDATE:
$(document).ready(function () {
var link = $(".btn_buy_ticket").find("a").click(function (e) {
e.preventDefault();
$.post($(this).attr("href"));
});
})
[HttpPost]
public ActionResult EventOverview(int id) // just for test
{
return RedirectToAction("EventOverview", new {id = id});
}
public ActionResult EventOverview(int id, string type, string activeTab,string hall, string session, string client, string count)
{
var model = CreateEventViewData<EventViewData>(id, type);
model.ActiveTab = activeTab;
model.ScheduleCount = count;
model.SessionId = session;
model.HallId = hall;
model.ClientId = client;
return View("Controls/EventsInfo/EventInfo", model);
}
All actions are called, but my EventInfo view not loaded.
You could use POST instead of GET. So you could replace the link with a form containing hidden fields for the parameters that you don't want to appear in the query string:
#using (Html.BeginForm("EventOverview", "BaseEvent", new { id = eventInfo.Key.OID, type = eventInfo.Key.XPObjectType.TypeName.GetShortTypeName() }, FormMethod.Post, null))
{
#Html.Hidden("activeTab", "#scheduleLink")
#Html.Hidden("session", eventInfo.Key.EventSchedules[0].SessionId)
#Html.Hidden("hall", eventInfo.Key.EventSchedules[0].HallId)
#Html.Hidden("client", eventInfo.Key.EventSchedules[0].BasePlace.PremieraClientId)
<button type="submit">Make</button>
}
How to hide URL Parameters
If you want to hide URL parameters, you go to debug properties and you select the radiobutton option and specify thatc
specificpage:
http://localhost:61757/TicketDataUpload/templateupload?projectid=6497&userid=7336
This is the parameter URL. If you want to hide like this:
http://localhost:61757/Controller/index
You have to put in a specific page as when you open the page, it will not display URL parameters.
My (kinda ugly) solution: leave the code behind as is, then after the html doc has loaded (at which point the server has already made use of your query string data), use history.pushState in javascript to change the url in the address bar like so:
$(document).ready(function () {
let hrefWithoutQueryString = window.location.href.split('?')[0];
// args below are 'state' (irrelevant for me), 'title' (so far,
// ignored by most browsers) and 'url' (will appear in address bar)
history.pushState({ }, '', hrefWithoutQueryString);
});
For a split second the query string will appear in the address bar, but after the js above has run, everything including and after the '?' in your url will be gone.
Obviously, a side effect is that it alters your browser's session history but this wasn't an issue for me.
And note that I have passed in an empty state into pushState--again, this was not an issue for me but could cause problems depending on how the routing of your application is set up and whether it makes use of the state variable.

Ambient values in mvc2.net routing

I have following two routes registered in my global.asax file
routes.MapRoute(
"strict",
"{controller}.mvc/{docid}/{action}/{id}",
new { action = "Index", id = "", docid = "" },
new { docid = #"\d+"}
);
routes.MapRoute(
"default",
"{controller}.mvc/{action}/{id}",
new { action = "Index", id = "" },
new { docConstraint = new DocumentConstraint() }
);
and I have a static "dashboard" link in my tabstrip and some other links that are constructed from values in db here is the code
<ul id="globalnav" class = "t-reset t-tabstrip-items">
<li class="bar" id = "dashboard">
<%=Html.ActionLink("dash.board", "Index", pck.Controller, new{docid =string.Empty,id = pck.PkgID }, new { #class = "here" })%>
</li>
<%
foreach (var md in pck.sysModules)
{
%>
<li class="<%=liClass%>">
<%=Html.ActionLink(md.ModuleName, md.ActionName, pck.Controller, new { docid = md.DocumentID}, new { #class = cls })%>
</li>
<%
}
%>
</ul>
Now my launching address is localhost/oa.mvc/index/11 clearly matching the 2nd route. But when I visit any page that has mapped to first route and then come back to dash.board link it shows me localhost/oa.mvc/7/index/11 where 7 is docid and picked from previous Url.
I understand that my action method is after docid and changing it would not clear the docid.
My question here is, can I remove docid in this scenario without changing the route?
I have the same "not clearing out" value problem...
I've stepped into source code and I don't understand the reason for being of segment commented as : // Add all current values that aren't in the URL at all
# System\Web\Routing\ParsedRoute.cs, public BoundUrl Bind(RouteValueDictionary currentValues, RouteValueDictionary values, RouteValueDictionary defaultValues, RouteValueDictionary constraints) method from line 91 to line 100
While the clearing process is correctly handled in method preceding steps, this code "reinjects" the undesired parameter into acceptedValues dictionary!?
My routing is defined this way:
routes.MapRoute(
"Planning",
"Plans/{plan}/{controller}/{action}/{identifier}",
new { controller = "General", action = "Planning", identifier = UrlParameter.Optional },
new { plan = #"^\d+$" }
);
// default application route
routes.MapRoute(
"Default",
"{controller}/{action}/{identifier}",
new {
controller = "General",
action = "Summary",
identifier = UrlParameter.Optional,
plan = string.Empty // mind this default !!!
}
);
This is very similar to what you're using. But mind my default route where I define defaults. Even though my default route doesn't define plan route value I still set it to string.Empty. So whenever I use Html.ActionLink() or Url.Action() and I want plan to be removed from the URL I call it the usual way:
Url.Action("Action", "Controller", new { plan = string.Empty });
And plan is not included in the URL query string any more. Try it out yourself it may work as well.
Muhammad, I suggest something like this :
(written 5 mn ago, not tested in production)
public static class MyHtmlHelperExtensions {
public static MvcHtmlString FixActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, object routeValues, object htmlAttributes) {
var linkRvd = new RouteValueDictionary(routeValues);
var contextRvd = htmlHelper.ViewContext.RouteData.Values;
var contextRemovedRvd = new RouteValueDictionary();
// remove clearing route values from current context
foreach (var rv in linkRvd) {
if (string.IsNullOrEmpty((string)rv.Value) && contextRvd.ContainsKey(rv.Key)) {
contextRemovedRvd.Add(rv.Key, contextRvd[rv.Key]);
contextRvd.Remove(rv.Key);
}
}
// call ActionLink with modified context
var htmlString = htmlHelper.ActionLink(linkText, actionName, controllerName, routeValues, htmlAttributes);
// restore context
foreach (var rv in contextRemovedRvd) {
contextRvd.Add(rv.Key, rv.Value);
}
return htmlString;
}
}
This is such a frustrating problem and I would venture to say that it is even a bug in ASP.Net MVC. Luckily it's an easy fix using ActionFilters. If you are using MVC3 then I would just put this as a global attribute to clear out ambient values. I made this attribute discriminatory, but you can change it to clear all attributes.
The assumption here is that by the time the Result is executing (your view most likely), you have already explicitly specified all your ActionLinks and Form Actions. Thus this will execute before they (the links) are evaluated, giving you a new foundation to generate them.
public class ClearAmbientRouteValuesAttribute : ActionFilterAttribute
{
private readonly string[] _keys;
public ClearAmbientRouteValuesAttribute(params string [] keys)
{
if (keys == null)
_keys = new string[0];
_keys = keys;
}
public override void OnResultExecuting(ResultExecutingContext filterContext)
{
foreach (var key in _keys) {
// Why are you sticking around!!!
filterContext.RequestContext.RouteData.Values.Remove(key);
}
}
}
// Inside your Global.asax
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new HandleErrorAttribute());
filters.Add(new ClearAmbientRouteValuesAttribute("format"));
}
Hope this helps someone, cause it sure helped me. Thanks for asking this question.
In this particular scenario I have two recommendations:
Use named routes. The first parameter to the MapRoute method is a name. To generate links use Html.RouteLink() (and other similar APIs). This way you'll always choose the exact route that you want and never have to wonder what gets chosen.
If you still want to use Html.ActionLink() then explicitly set docid="" to clear out its value.
Here's how I solved my problem, it may take a little adapting to get it to work, but I felt like I could get what I needed and just use routing more or less normally:
Excerpted from Apress Pro ASP.Net.MVC 3 Framework:
A value must be available for every segment variable defined in the URL pattern.
To find values for each segment variable, the routing system looks first at the
values we have provided (using the properties of anonymous type), then the
variable values for the current request, and finally at the default values defined in
the route. (We return to the second source of these values later in this chapter.)
None of the values we provided for the segment variables may disagree with the
default-only variables defined in the route. These are variables for which default
values have been provided, but which do not occur in the URL pattern. For
example, in this route definition, myVar is a default-only variable:
routes.MapRoute("MyRoute", "{controller}/{action}",
new { myVar = "true" });
For this route to be a match, we must take care to not supply a value for myVar or to make
sure that the value we do supply matches the default value.
The values for all of the segment variables must satisfy the route constraints. See
the “Constraining Routes” section earlier in the chapter for examples of different
kinds of constraints.
Basically I used the rule about a route not matching if it doesn't define a segment, but has a default variable used to give me a little more control over whether a route was chosen for outbound routing or not.
Here's my fixed routes, notice how I specify a value for category that would never be valid and don't specify a segment for category. This means that route will be skipped if I have a category, but will use it if I only have a page:
routes.MapRoute(null, "receptionists/faq/{page}", new { controller = "Receptionist", action = "Faq", page = 1, category = (Object)null }, new { page = #"^\d+$" });
routes.MapRoute(null, "receptionists/faq/{category}/{page}", new { controller = "Receptionist", action = "Faq", page = 1 }, new { category = #"^\D+$", page = #"^\d+$" });
For Category Links
#Html.ActionLink("All", "Faq", new { page = 1 })
#foreach (var category in Model.Categories)
{
#Html.ActionLink(category.DisplayName, "faq", new { category = category.DisplayName.ToLower(), page = 1 })
}
For Page Links
#for (var p = 1; p <= Model.TotalPages; p++)
{
#Html.ActionLink(p.ToString(), "Faq", new { page = p, category = Model.CurrentCategory})
}

MVC RouteLink being double encoded

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)

Is there a way to include a fragment identifier when using Asp.Net MVC ActionLink, RedirectToAction, etc.?

I want some links to include a fragment identifier. Like some of the URLs on this site:
Debugging: IE6 + SSL + AJAX + post form = 404 error#5626
Is there a way to do this with any of the built-in methods in MVC? Or would I have to roll my own HTML helpers?
As Brad Wilson wrote, you can build your own link in your views by simply concatenating strings. But to append a fragment name to a redirect generated via RedirectToAction (or similar) you'll need something like this:
public class RedirectToRouteResultEx : RedirectToRouteResult {
public RedirectToRouteResultEx(RouteValueDictionary values)
: base(values) {
}
public RedirectToRouteResultEx(string routeName, RouteValueDictionary values)
: base(routeName, values) {
}
public override void ExecuteResult(ControllerContext context) {
var destination = new StringBuilder();
var helper = new UrlHelper(context.RequestContext);
destination.Append(helper.RouteUrl(RouteName, RouteValues));
//Add href fragment if set
if (!string.IsNullOrEmpty(Fragment)) {
destination.AppendFormat("#{0}", Fragment);
}
context.HttpContext.Response.Redirect(destination.ToString(), false);
}
public string Fragment { get; set; }
}
public static class RedirectToRouteResultExtensions {
public static RedirectToRouteResultEx AddFragment(this RedirectToRouteResult result, string fragment) {
return new RedirectToRouteResultEx(result.RouteName, result.RouteValues) {
Fragment = fragment
};
}
}
And then, in your controller, you'd call:
return RedirectToAction("MyAction", "MyController")
.AddFragment("fragment-name");
That should generate the URL correctly.
We're looking at including support for this in our next release.
In MVC3 (and possibly earlier I haven't checked), you can use UrlHelper.GenerateUrl passing in the fragment parameter. Here's a helper method I use to wrap the functionalityL
public static string Action(this UrlHelper url, string actionName, string controllerName, string fragment, object routeValues)
{
return UrlHelper.GenerateUrl(
routeName: null,
actionName: actionName,
controllerName: controllerName,
routeValues: new System.Web.Routing.RouteValueDictionary(routeValues),
fragment: fragment,
protocol: null,
hostName: null,
routeCollection: url.RouteCollection,
requestContext: url.RequestContext,
includeImplicitMvcValues: true /*helps fill in the nulls above*/
);
}
#Dominic,
I'm almost positive that putting that in the route will cause routing issues.
#Ricky,
Until MVC has support for this, you can be a little more "old school" about how you make your routes. For example, you can convert:
<%= Html.ActionLink("Home", "Index") %>
into:
<a href='<%= Url.Action("Index") %>#2345'>Home</a>
Or you can write your own helper that does essentially the same thing.
The short answer is: No. In ASP.NET MVC Preview 3 there's no first-class way for including an anchor in an action link. Unlike Rails' url_for :anchor, UrlHelper.GenerateUrl (and ActionLink, RedirectToAction and so on which use it) don't have a magic property name that lets you encode an anchor.
As you point out, you could roll your own that does. This is probably the cleanest solution.
Hackily, you could just include an anchor in a route and specify the value in your parameters hash:
routes.MapRoute("WithTarget", "{controller}/{action}/{id}#{target}");
...
<%= Html.ActionLink("Home", "Index", new { target = "foo" })%>
This will generate a URL like /Home/Index/#foo. Unfortunately this doesn't play well with URL parameters, which appear at the end of the URL. So this hack is only workable in really simple circumstances where all of your parameters appear as URL path components.
This is a client side solution but if you have jquery available you can do something like this.
<script language="javascript" type="text/javascript">
$(function () {
$('div.imageHolder > a').each(function () {
$(this).attr('href', $(this).attr('href') + '#tab-works');
});
});
</script>
Fragment identifiers are supported in MVC 5. See ActionLink's overloads at https://msdn.microsoft.com/en-us/library/dd460522(v=vs.118).aspx and https://msdn.microsoft.com/en-us/library/dd492938(v=vs.118).aspx.

Resources