ASP.Net MVC: Display file in browser, show filename in browser tab - c#

In my application I have a folder structure where you can upload all kinds of files (jpg, png, pdf, txt etc.) You can download them directly as a file or you can view the file in the browser.
To display the file in the browser, I use the folowing controller action:
public virtual ActionResult FileDisplay(int fileId, bool isFile)
{
var viewmodel = _presenter.GetDocumentDisplayViewModel(fileId, isFile);
return base.File(viewmodel.Data, viewmodel.MediaType);
}
The file is displayed in the browser like this:
https://i.stack.imgur.com/TXmTy.jpg
The browser tab shows my controller name "FileDisplay" and (for this example) the dimensions of the image.
Question:
How can I display the filename in the brower tab title in stead of the controller name? This for all file types.
1. Content-Dispostion header
I have found several posts where they say to add the Content-Disposition header to the response with the filename:
Returning a file to View/Download in ASP.NET MVC
Make a file open in browser instead of downloading it
But this doesn't work.
2. Add filename to the File constructor
return base.File(viewmodel.Data, viewmodel.MediaType, viewmodel.FileName);
If I do this, the file is downloaded in stead of displayed in the browser.
3. PDF file title
I have found out that sometimes the browser tab title is correct! If I display a PDF file in the browser that has a file title (title in pdf properties, not the file name) the browser title is correct:
https://i.stack.imgur.com/orAZS.jpg
Can someone help me with this?

Workaround for now:
I have created a new action and view with a fullscreen iframe so I can set the title of this container view. The IFrame source is calling my File directly.
public virtual ActionResult FileDisplay(string fileName, int fileId, bool isFile)
{
var viewModel = new IFrameDocumentDisplayViewModel
{
FileName = fileName,
FileId = fileId,
IsFile = isFile
};
return PartialView("IFrameFileDisplayView", viewModel);
}
public virtual ActionResult GetFile(int fileId, bool isFile)
{
var viewmodel = _presenter.GetDocumentDisplayViewModel(fileId, isFile);
return base.File(viewmodel.Data, viewmodel.MediaType);
}
#model OnView.Models.ViewModels.Document.IFrameDocumentDisplayViewModel
#{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>#Model.FileName</title>
</head>
<body>
<iframe style="border: 0; position:absolute; top:0; left:0; right:0; bottom:0; width:100%; height:100%" src="#Html.Raw(Url.Action("GetFile", "Document", new { fileId = Model.FileId, isFile = Model.IsFile}))"></iframe>
</body>
</html>
So for now I can set the page title. But I am still curious how I can accomplish it without IFrame.

What the browser display as the tab title is the last part of the URL (the browser has no idea what is your controller name), so you could reformat your url like this :
localhost/OnView.Web/Document/FileDisplay/1685/True/MyTabName
Then the tab title would be MyTabName.
But changing a whole URL schema for such purpose also seems like a workaround...
Sidenote : I have the same problem, but my url is like this : FileDisplay/3?isFile=true and my tab title is 3 (in chrome. For IE, the tab title is the domain).

Try This:-
public virtual ActionResult FileDisplay(int fileId, bool isFile)
{
var viewmodel = _presenter.GetDocumentDisplayViewModel(fileId, isFile);
ViewBag.Title= viewmodel.FileName;
return base.File(viewmodel.Data, viewmodel.MediaType);
}

Related

Download pdf rendered from backend

I have project written in Angular + TS + Net 6 Web api.
My goal is to have button. On button click, pdf should be created in backend and PDF downloaded to user.
PDF is long report which has some variables. I heard that PDF can be generated from HTML. To simplify, lets say it looks like this:
<html>
<head></head>
<body>
<div>My name is FirstParameter</div>
<div>My age is SecondParameter</div>
</body>
</html>
I believe FileStreamResult should be returned in backend. How to achieve it (prefer without 3rd party library)
.cs
[HttpGet]
[Route("DownloadPDF")]
public async Task<IActionResult> DownloadPDF(Guid userId)
{
var user = await _context.Users.FirstAsync(p=>p.id == userId);
string firstParameter = user.FirstName;
string secondParameter = user.SecondName;
// generating pdf as filestream
return File(fs, "application/pdf", "random.pdf");
}
.html
<a (click)="download('f58c8036-915a-4c39-bbb8-352e21eae49c')">Download pdf</a>
.ts
download(id) {
}

Save Razor Component as JPG/PNG/PDF via Download Link

I'm looking for a way to create a link that will create a screenshot of a Razor component and download it as a JPG, PNG or PDF through a Blazor Server application. Ideally, it will only contain the Razor component and all child components, but no parents, and the image will have the precise appearance of the current state displayed on the browser.
The only similar thing is capturing HTML canvases, but since I'm so new to Blazor, I'm not exactly sure how to apply that, and was wondering if there's a way of achieving something similar via C#. Any suggestions would be appreciated. Thanks :)
I don't think it will be easy to achieve that with C# alone, since the best approach to the problem is to convert the HTML data to canvas and export it as an image which is a hard process, but there are libraries available for that, in JS.
How to do it:
I'm gonna use this library since it seems to be the simplest: html2canvas
Here is the link to it's JS file: html2canvas.js
download it and place it in your wwwroot folder, then, include that in the _host.cshtml by adding this at the end of the body tag:
<script src="html2canvas.js"></script>
then add a JS function to call from C# so we can have the even handler written in C# by adding this after the previous code in _host.cshtml:
<script>
window.takeScreenshot = async function(id) {
var img = "";
await html2canvas(document.querySelector("#" + id)).then(canvas => img = canvas.toDataURL("image/png"));
var d = document.createElement("a");
d.href = img;
d.download = "image.png";
d.click();
return img;
}
</script>
This will automatically take a screenshot from the element and download it, also return its URL. Note that the component must be inside a div tag with an id, otherwise, you can't select it alone, example good child in parent:
Parent.razor
<div id="child"></div>
<Child />
</div>
To use this function, use the JsInterop class. Simply, inject (basically include) it in your razor component where you need this functionality by adding this at the top of the file:
#inject IJSRuntime JS
next, a function to do everything:
#inject IJSRuntime JS
#code {
public async System.Threading.Tasks.Task<string> TakeImage(string id)
{
return await JS.InvokeAsync<string>("takeScreenshot", id);
}
}
This function will return a data URL of an image taken from an element specified by the id parameter.
Sample usage with a button to take image:
#page "/screenshot"
#inject IJSRuntime JS
#code {
string image_url = "";
string child_id = "child";
public async System.Threading.Tasks.Task<string> TakeImage(string id)
{
return await JS.InvokeAsync<string>("takeScreenshot", id);
}
public async System.Threading.Tasks.Task ButtonHandler()
{
image_url = await TakeImage(child_id);
}
}
<button #onclick="ButtonHandler">Take image</button>
<h3>Actual component:</h3>
<div id=#child_id>
<ChildComponent />
</div>
<h3>Image:</h3>
<img src=#image_url />
<br />
<br />
<br />
<p>URL: #image_url</p>
Pressing the button will download the image, show it, show the raw URL, and save the URL to variable image_url.
You can shorten System.Threading.Tasks.Task to Task by adding #using System.Threading.Tasks to _Imports.razor file.
You can remove auto-download functionality by removing these 4 lines in the JS function:
var d = document.createElement("a");
d.href = img;
d.download = "image.png";
d.click();
If you want to take an image of the entire page automatically and download it without any user interaction:
modify the JS function and set the query selector to body and remove the id parameter:
<script>
window.takeScreenshot = async function() {
var img = "";
await html2canvas(document.querySelector("body")).then(canvas => img = canvas.toDataURL("image/png"));
var d = document.createElement("a");
d.href = img;
d.download = "image.png";
d.click();
return img;
}
</script
set the function to run when the document loads:
#inject IJSRuntime JS
#page "/component"
#code {
string image_url = "";
public async System.Threading.Tasks.Task<string> TakeImage()
{
return await JS.InvokeAsync<string>("takeScreenshot");
}
protected override async System.Threading.Tasks.Task OnAfterRenderAsync(bool firstRender)
{
if(firstRender) //Ensure this is the page load, not any page rerender
{
image_url = await TakeImage();
}
}
}
Special URL to automatically download image:
To generate a URL, which when loaded will download the image as the previous part, but only when that special URL is loaded, not the normal page URL.
To do this, I'm gonna use query strings, so the special URL will be like this: http://localhost:5001/page?img=true
For that, we need to get the URI, using NavigationManager, which can be injected like the IJSRuntime. For parsing the URI, we can use QueryHelpers class.
The final code will look like this:
#inject IJSRuntime JS
#inject NavigationManager Nav
#page "/component"
#code {
string image_url = "";
public async System.Threading.Tasks.Task<string> TakeImage()
{
return await JS.InvokeAsync<string>("takeScreenshot");
}
protected override async System.Threading.Tasks.Task OnAfterRenderAsync(bool firstRender)
{
if(firstRender) //Ensure this is the page load, not any page rerender
{
var uri = Nav.ToAbsoluteUri(Nav.Uri);
if (Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(uri.Query).TryGetValue("img", out var isImg))
{
if (System.Convert.ToBoolean(isImg.ToString()))
{
image_url = await TakeImage();
}
}
}
}
}
Now you can add ?img=true to the component's URL and you will get a screenshot of it.
Note that if the body/parent of the div has a background, and you want it to be in the image, you need to add background: inherit; to the CSS rules of the div containing the child component.

How to read an HTML file into an angular component so as to allow for the updating of the HTML file without redeploying code

I am trying to have a HTML file in my assets folder that is nothing but some header tags, dates and feature lists to serve as release notes for our website. I have an angular modal component that I want to read this file each time its route is called, rather than the alternative of having the HTML in the component itself which would require us to redeploy anytime we updated the release notes.
As mentioned I originally had this as part of my components HTML file but this was then being compiled into javascript each time and unable to be updated without a redeploy. Everything I have tried to search for doing something similar seems to be pointing me to just doing it that way.
ReleaseNotes.html
<!DOCTYPE html>
<html lang='en' xmlns='http://www.w3.org/1999/xhtml'>
<body>
<h1>Example header one</h1>
<h3>03/01/2019</h3>
<h4>Patch 1.03 Title</h4>
<ul>
<li>Feature that was added</li>
<li>Feature that was added</li>
<li>Feature that was added</li>
</ul>
<hr>
release-notes-modal.component.ts
export class ReleaseNotesModalComponent implements OnInit {
faTimesCircle = faTimesCircle;
contents: string;
constructor(public dialogRef: MatDialogRef<ReleaseNotesModalComponent>) {
//this.contents = System.IO.File.ReadAllText("ReleaseNotes.html");
}
ngOnInit() {
}
close() {
this.dialogRef.close();
}
}
There are a few ways you can accomplish this. This is how I've done this in the past.
In a Controller in the c# application, you would read the html file and return it:
[HttpGet]
[Route("releasenotes")]
public async Task<IActionResult> ReadReleaseNotes()
{
var viewPath = Path.GetFullPath(Path.Combine(HostingEnvironment.WebRootPath, $#"..\Views\Home\releasenotes.html"));
var viewContents = await System.IO.File.ReadAllTextAsync(viewPath).ConfigureAwait(false);
return Content(viewContents, "text/html");
}
Then in the angular application in a service you would call this method and retrieve this file as follows:
getReleaseNotes(): Observable<string> {
return this.http
.get([INSERT_BASE_URL_HERE] + '/releasenotes', { responseType: 'text' });
}
You can then utilize that in the ReleaseNotesModalComponent through something like this:
#Component({
template: '<span [innerHTML]="contents"></span>'
})
export class ReleaseNotesModalComponent implements OnInit {
faTimesCircle = faTimesCircle;
contents: string;
constructor(public dialogRef: MatDialogRef<ReleaseNotesModalComponent>, private service: ReleaseNotesService) {
service.getReleaseNotes(html => this.contents = html);
}
ngOnInit() {
}
close() {
this.dialogRef.close();
}
}
For the Angular side of things, I created a StackBlitz example.

Need to pass the Rowdata to another view on link click

Here is my JQGrid Code:
click: function (e) {
debugger;
var id = $(e.target).closest("tr.jqgrow").attr("id");
rowdata = jQuery("#EmpTable").getRowData(id);
Data = { Id: rowdata.Id, Name: rowdata.Name, Designation: rowdata.Designation };
var url = 'http://localhost:50428/Script/Edit/';
return $.post(url, Data);
}
here is my controller code where the data is collecting
[HttpPost]
public ActionResult Edit(FormCollection form)
{
gridmodel properties = new gridmodel();
properties.Id = Convert.ToInt32(form["id"]);
properties.Name = form["Name"];
properties.Designation = form["Designation"];
ViewBag.id = properties.Id;
ViewBag.name = properties.Name;
ViewBag.designation = properties.Designation;
return View();
}
Now here is my View code
the data that is passing from the controller to the view
#model MVC5_JQGrid.Models.gridmodel
#{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Edit</title>
</head>
<body>
<div>
Id:#ViewBag.id
<br />
Name:#ViewBag.name
<br />
Designation:#ViewBag.designation
</div>
</body>
</html>
But i am unable to load this page but in network ---> Response body i can see that these values are assigned
Update
Hello Venkata thanks for the answer i will keep the points which you mentioned and +1 from my side for the observation ,coming to the problem. it was resolved from oleg suggestion:(Here is the answer given by Oleg) The reason of your problem is the usage of $.post(url, Data); which just send data with respect of $.ajax({url: "/Script/Edit", data: Data, type: "POST"});. You need to do $.submit instead. If you would use HTTP GET then you can just assign new URL which includes parameters to location.href (something like location.href = "/Script/Edit?" + $.param(Data)). In case of HTTP POST one need to build with elements which contains (or just have on the page hidden form with all required elements with required name attributes) and use $.submit.
Thanks for the help from oleg and Venkata
Few Changes required in your code:
Observation and Notes:
Before fixing, we should know few points here regarding architecture, control-flow and standards
jQuery AJAX requests get response to its success callback handler(in your code you've missed success call)
In success function We should build or append or set HTML to target placeholder tag in existing page (let us say you have div for Edit section like <div id='editSection'>...</div> in your jqGrid page. you should bind the responseView to editSection div)
When we send AJAX request: In Controller.Action Instead of return View(); we should have return PartialView();
In your view.cshtml give preference to bind elements with Model rather than ViewData. You can get model to view if you do return PartialView(model); in the Action.
Try to reduce usage of ViewData or ViewBag in view.cshtml. Also business logic not recommended in Views.
Try to Follow best practices: Capitalization Conventions (in your code change gridmodel class name to GridModel)
By default routing url template will have {controller}/{action} (from your url Script is controller and Edit is action)
Make sure that you're passing correct Controller and Action names (Is Edit action located in the Controller with name ScriptController?)
Changes in JavaScript JQGrid Code:
url = '/Script/Edit/';
return $.post(url, Data).success(function(response){
//response datatype can be JSON or XML or HTML or text (In your case HTML Edit.cshtml View)
//update you target html tag with response view.
$('#editSection').html(responseView);
});
Changes in Controller:
[HttpPost]
public ActionResult Edit(FormCollection form)
{
var properties = new gridmodel();
properties.Id = Convert.ToInt32(form["id"]);
properties.Name = form["Name"];
properties.Designation = form["Designation"];
ViewBag.id = properties.Id;
ViewBag.name = properties.Name;
ViewBag.designation = properties.Designation;
return PartialView();
}
You can change above Edit Action like below if action require input parameters only id, name & designation
[HttpPost]
public ActionResult Edit(int id, string name, string designation)
{
var gridModel = new GridModel();
gridModel.Id = id;
gridModel.Name = name;
gridModel.Designation = designation;
ViewBag.id = gridModel.Id;
ViewBag.name = gridModel.Name;
ViewBag.designation = gridModel.Designation;
return PartialView(gridModel);
}
Hello Venkata thanks for the answer i will keep the points which you mentioned and +1 from my side for the observation ,coming to the problem. it was resolved from oleg suggestion:(Here is the answer given by Oleg) The reason of your problem is the usage of $.post(url, Data); which just send data with respect of $.ajax({url: "/Script/Edit", data: Data, type: "POST"});. You need to do $.submit instead. If you would use HTTP GET then you can just assign new URL which includes parameters to location.href (something like location.href = "/Script/Edit?" + $.param(Data)). In case of HTTP POST one need to build with elements which contains (or just have on the page hidden form with all required elements with required name attributes) and use $.submit. Thanks for the help from oleg and Venkata

Dynamically Set title of page

I want to set the Title of Web page which is returning the PDF file stream as:
public ActionResult PrintInvoice(long ID)
{
var data = db.Documents.Where(x => x.InvoiceNumber == ID);
ReportDocument rd = new ReportDocument();
rd.Load(Server.MapPath("~/Reports/InvoiceDocument.rpt"));
Stream stream = rd.ExportToStream(CrystalDecisions.Shared.ExportFormatType.PortableDocFormat);
stream.Seek(0, SeekOrigin.Begin);
return new FileStreamResult(stream, "application/pdf"); //For Showing PDF in Browser itself
}
and on this Page I want to set the Title.
How can I set the Title on this Page.
Currently the Title on the Page looks like as shown below in Image::
Have a look at HTTP Headers, like in that thread.
Try something like :
Response.AppendHeader("Content-Disposition", "inline; filename=your page title");
Also have a look at this thread which recommend :
return File(stream, "application/pdf", "your page title");
Keep in mind that this kind of data can be executed differently from different browsers.
This is what I ended up doing in my situation.
The controller code below consists of two actions. The first action returns a model I can use for setting the page title (this could just be a string depending on your use case). The second action is for getting the file contents. In my case I was storing the file contents in a database so I use an id to fetch the document.
The second action also sets the response headers so that the file name shows up properly when they try to download the file.
public IActionResult PreviewDocument(int id)
{
Document document = _legislationFolderService.GetDocument(id);
if (document == null)
return NotFound($"Could not find document with id of {id}");
return View(document);
}
public IActionResult PreviewDocumentContents(int id)
{
DocumentContents documentContents = _legislationFolderService.GetDocumentContents(id);
if (documentContents == null)
return NotFound($"Could not find contents for document with id of {id}");
Response.Headers.Add("Content-Disposition", $"inline; filename={documentContents.Document.Name}.pdf");
return new FileStreamResult(new MemoryStream(documentContents.Contents), "application/pdf");
}
In the view below (PreviewDocument.cshtml) I used an iframe to fill the page and link to the PreviewDocumentContents action. I didn't want the layout included from my main template so I set that to null and put up a basic html structure for the page, where I set the title right in the html.
#model EFloorFiles.Service.Models.Document
#{
Layout = null;
ViewBag.Title = Model.Name;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>#ViewData["Title"] - E-Floor Files</title>
<style type="text/css">
body, html {
width: 100%;
height: 100%;
overflow: hidden;
margin: 0;
}
iframe {
width: 100%;
height: 100%;
border: none;
}
</style>
</head>
<body>
<iframe src="#Url.Action("PreviewDocumentContents", new { id = Model.Id })"></iframe>
</body>
</html>

Categories