Save a Rendered Razor View as HTML string - c#

Is it possible, once a Razor View has been rendered in the browser, that the HTML and the contents of the mark up (images, tables, data etc.) can be saved as a string or other type?
I want to be able to generate the Razor View for the customer to check everything is ok in the output, then I want them to click a button that saves all of the HTML (Without all of the razor markup etc.).
How do you pass the HTML back to an Action, if it has to be processed pre-rendering, then how can this also be done.
I can then use this to generate PDF's and also save time on processing, as I will save the string in a database.
BTW, this is not a partial view nor will it use partial views, also I know there are still some things to fix in the Razor View, I am more interested in the saving the HTML at this point.
TIA
HTML Pre rendering
HTML Post Rendering

You can use Middleware to obtain a copy of the HTML that is being sent to the browser. Create a class named ResponseToString with the following content:
public class ResponseToStringMidleware
{
RequestDelegate _next;
public ResponseToStringMidleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
var isHtml = context.Response.ContentType?.ToLower().Contains("text/html");
Stream responseBody = context.Response.Body;
using (var memoryStream = new MemoryStream())
{
context.Response.Body = memoryStream;
await _next(context);
if (context.Response.StatusCode == 200 && isHtml.GetValueOrDefault())
{
memoryStream.Position = 0;
string html = new StreamReader(memoryStream).ReadToEnd();
// save the HTML
}
memoryStream.Position = 0;
await memoryStream.CopyToAsync(responseBody);
}
}
}
Replace the // save the HTML with some code to persist the HTML as required. Register the Middleware in your Startup's Configure method early:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseMiddleware<ResponseToStringMidleware>();
...
}
Further infomration: Middleware in Razor Pages

Related

Add content to every view or component in ASP NET MVC

I'm using ASP .NET Core MVC (views and controllers).
Is there a way to add some additional output to all *.cshtml files using middleware, filter or something similar?
I would like to display the path(s) of all cshtml-files like the view itself, partial views, layout-file or components, that are part of the current page.
This is how it should look like:
Right now, I have to add this line to the *.cshtml files, one by one:
#using WkOne.AuthorizationServer.ViewModels;
#model IEnumerable<UserViewModel>
#{
Layout = "_Layout3Cols";
ViewData["Title"] = "Users";
}
<!-- I need this line in every cshtml file -->
<!-- \ -->
<div style="font-size: small;background-color: #CFC;">Path: #Path.ToString() </div>
<table class="table">
<!-- ... and so on... -->
But what I'm looking for is a way to do this in central place.
Any suggestions?
MVC project returns the html codes(razor codes has already been complied to html,so your codes shouldn't contain razor codes) which contained in response body to browser,
The response body could write but couldn't be read ,if you want to add the html codes somewhere you want ,I think you need to replace the default body
I tried as below and added the text "hi"
public class CusTestMiddleware
{
private readonly RequestDelegate _next;
public CusTestMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
var response = context.Response;
var responseOriginalBody = response.Body;
using var memStream = new MemoryStream();
response.Body = memStream;
await _next(context);
var targetstr = "<a>hi</a>";
byte[] targetbyte = Encoding.UTF8.GetBytes(targetstr);
memStream.Write(targetbyte);
memStream.Position = 0;
var responseReader = new StreamReader(memStream);
var responseBody = await responseReader.ReadToEndAsync();
memStream.Position = 0;
await memStream.CopyToAsync(responseOriginalBody);
response.Body = responseOriginalBody;
}
}

ASP.Net Core - Dynamically generate index.html

The standard way to provide an index.html to the client with ASP.Net core is to use the middleware app.UseStaticFiles(); on IApplicationBuilder instance in the Configure method of the StartUp class. This provides a static index.html file from the wwwroot folder. Is there a way to provide an index.html to the client, that is dynamically generated in code on request?
There is a way to change the SPA files content served from the static folder.
You should do the following:
update your usage of UseStaticFiles, it should have a lambda provided for the OnPrepareResponse property; See this MS DOC
It is called every time the static file is returned to the client;
You can analyze the file name to ensure it is index.html;
Your code can completely change the content that will be returned. Just generate a file dynamically to replace index.html, and write it as a response body by writing to a stream. You might need to clear or save original body content before modifying and returning it if your writing has failed.
See code example to get you started:
var staticFileOptions = new StaticFileOptions
{
OnPrepareResponse = context =>
{
// Only for Index.HTML file
if (context.File.Name == "index.html") {
var response = context.Context.Response;
var str = "This is your new content";
var jsonString = JsonConvert.SerializeObject(str);
// modified stream
var responseData = Encoding.UTF8.GetBytes(jsonString);
var stream = new MemoryStream(responseData);
// set the response body
response.Body = stream;
}
}
};
app.UseStaticFiles(staticFileOptions);

How can one generate and save a file client side using Blazor?

I want to have a SPA that's doing all the work client side and even generating some graphs/visuals.
I'd like to be able to have the user click buttons and save the visuals, tables, and other things from the page (that are both seen and not seen, so right click save or copy/paste is not always an option).
How do I call a function from the webassembly/blazor library, get it's results and save it as a file on the client side?
the idea is something like this...?
cshtml
<input type="file" onchange="#ReadFile">
<input type="file" onchange="#SaveFile">
#functions{
object blazorObject = new blazorLibrary.SomeObject();
void ReadFile(){
blazorObject.someFunction(...selectedFile?...);
}
void SaveFile(){
saveFile(...selectedFile..?)
}
}
Creator of Blazor Steve Sanderson used JavaScript interop for similar task during one of his last presentations.
You can find example on BlazorExcelSpreadsheet
Solution includes three parts:
1) JavaScript
function saveAsFile(filename, bytesBase64) {
var link = document.createElement('a');
link.download = filename;
link.href = "data:application/octet-stream;base64," + bytesBase64;
document.body.appendChild(link); // Needed for Firefox
link.click();
document.body.removeChild(link);
}
2) C# interop wrapper
public static class FileUtil
{
public async static Task SaveAs(IJSRuntime js, string filename, byte[] data)
{
await js.InvokeAsync<object>(
"saveAsFile",
filename,
Convert.ToBase64String(data));
}
}
3) Call from your component
#inject IJSRuntime js
#functions {
void DownloadFile() {
var text = "Hello, world!";
var bytes = System.Text.Encoding.UTF8.GetBytes(text);
FileUtil.SaveAs(js, "HelloWorld.txt", bytes);
}
}
You can see it an action in Blazor Fiddle
Add a link
<a class="form-control btn btn-primary" href="/download?name=test.txt" target="_blank">Download</a>
Add Razor Page with a route
2.1. Create Razor page 'Download.cshtml' or another name... 'PewPew.cshtml'... does not matter
2.2. Put the next code in the created page
#page "/download"
#model MyNamespace.DownloadModel
Edit Download.cshtml.cs file
public class DownloadModel : PageModel
{
public IActionResult OnGet(string name) {
// do your magic here
var content = new byte[] { 1, 2, 3 };
return File(content, "application/octet-stream", name);
}
}
I created a repository and nuget package which solves and simplifies this issue please take a look: https://github.com/arivera12/BlazorDownloadFile
The solution proposed by Eugene work, but have one drawback. If you try to do it with large files the browser hangs while downloading the blob to the client side. Solution I have found is to change that code a bit and store files in temporary directory and let the server use its mechanics for serving files instead of pushing it as a blob.
In the server configuration add :
app.UseStaticFiles();
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(___someTempDirectoryLocation___, "downloads")),
RequestPath = "/downloads"
});
This will add static link to a download folder somewhere on your system. Put any files you want to be available for download in there.
Next You can use either link to that file using http://pathToYourApplication/downloads/yourFileName or use simplified save javascript from main example:
function saveAsFile(filename) {
var link = document.createElement('a');
link.download = filename;
link.href = "/downloads/" + filename;
document.body.appendChild(link); // Needed for Firefox
link.click();
document.body.removeChild(link);
}
which will push it to user browser for you.
I did it thus:
Added a new DownloadController.cs to a folder called Controllers
[Controller, Microsoft.AspNetCore.Mvc.Route("/[controller]/[action]")]
public class DownloadController : Controller
{
private readonly IDataCombinerService DataCombinerService;
private readonly IDataLocatorService DataLocatorService;
public DownloadController(IDataCombinerService dataCombinerService, IDataLocatorService dataLocatorService)
{
DataCombinerService = dataCombinerService;
DataLocatorService = dataLocatorService;
}
[HttpGet]
[ActionName("Accounts")]
public async Task<IActionResult> Accounts()
{
var cts = new CancellationTokenSource();
var Accounts = await DataCombinerService.CombineAccounts(await DataLocatorService.GetDataLocationsAsync(cts.Token), cts.Token);
var json = JsonSerializer.SerializeToUtf8Bytes(Accounts, Accounts.GetType(), new JsonSerializerOptions(JsonSerializerDefaults.Web) { WriteIndented = true });
var stream = new MemoryStream(json);
var fResult = new FileStreamResult(stream, MediaTypeNames.Application.Json)
{
FileDownloadName = $"Account Export {DateTime.Now.ToString("yyyyMMdd")}.json"
};
return fResult;
}
[HttpGet]
public IActionResult Index()
{
return View();
}
}
Strictly speaking async isn't required here as it doesn't need to process anything else, but that method is used to display the same results on screen when it is.
Then inside Startup.cs
app.UseEndpoints(endpoints =>
add:
endpoints.MapControllerRoute(
name: "default",
defaults: new { action = "Index" },
pattern: "{controller}/{action}");
endpoints.MapControllers();
Again the defaults isn't strictly speaking required, it's a standard MVC Controller.
This then functions just like a classic MVC response, so you can send back any files, from any source you like. It may be helpful to have a middleware service to hold temporary data between the view and the downloader controller so the client is downloading the same data.
Eugene's answer didn't work for me for some reason, but there is now official documentation on how to do this, which is very similar and works well in my Blazor Server app.
Add these JavaScript methods to your _Host.cshtml file:
<script type="text/javascript">
async function downloadFileFromStream(fileName, contentStreamReference) {
const arrayBuffer = await contentStreamReference.arrayBuffer();
const blob = new Blob([arrayBuffer]);
const url = URL.createObjectURL(blob);
triggerFileDownload(fileName, url);
URL.revokeObjectURL(url);
}
function triggerFileDownload(fileName, url) {
const anchorElement = document.createElement('a');
anchorElement.href = url;
if (fileName) {
anchorElement.download = fileName;
}
anchorElement.click();
anchorElement.remove();
}
</script>
In your .razor page file, add:
#using System.IO
#inject IJSRuntime JS
<button #onclick="DownloadFileFromStream">
Download File From Stream
</button>
#code {
private Stream GetFileStream()
{
var randomBinaryData = new byte[50 * 1024];
var fileStream = new MemoryStream(randomBinaryData);
return fileStream;
}
private async Task DownloadFileFromStream()
{
var fileStream = GetFileStream();
var fileName = "log.bin";
using var streamRef = new DotNetStreamReference(stream: fileStream);
await JS.InvokeVoidAsync("downloadFileFromStream", fileName, streamRef);
}
}

Support image file upload in .NET Core Web API

I'm developing a Web API with .Net Core, where I need to allow a client to upload a list of files (mostly images) and save them to the server.
The problem is that when the image is uploaded, and I try to open it from the folder where it was saved, it seems like it's corrupted and its size is not the same as the initial one.
Here is the code of my controller:
[Route("api/TestUpload")]
[Consumes("multipart/form-data")]
public class TestUploadController : Controller
{
private readonly IHostingEnvironment _env;
private readonly ApplicationContext _context;
public TestUploadController(ApplicationContext context, IHostingEnvironment env)
{
_context = context;
_env = env;
}
// GET: /<controller>/
public IActionResult Index()
{
return View();
}
[HttpPost("upload")]
public async Task<IActionResult> Post([FromForm]IList<IFormFile> files)
{
long size = files.Sum(f => f.Length);
var uploads = Path.Combine(_env.WebRootPath, "uploads");
foreach (var formFile in files)
{
if (formFile.Length > 0)
{
var filePath = Path.Combine(uploads, formFile.FileName);
using (var fileStream = new FileStream(Path.Combine(uploads, formFile.FileName), FileMode.Create))
{
await formFile.CopyToAsync(fileStream);
fileStream.Flush();
}
}
}
return Ok(new { size });
}
}
The only files that seem to be uploaded and saved fine are text and html files. I've tried with a single file instead of a list, same thing.
I've tried several code variations and none seem to work, I'm probably doing something wrong but I just can't figure out what it is.
Here is a screenshot of a test with Advanced REST client:
ARC screenshot
And here is what I mean by "it seems like it's corrupted": Windows Photo Viewer screenshot
Any help is appreciated!
The are two problems which may cause confusion here:
Media types which are supported by service which define what formats of data can be uploaded correctly to the server.
Format of image uploaded which is different than image stored on source computer since none of image media format is supported by yur WebAPI.
In case like your WebAPI client HTTP POST message contains a payload - image, in HTTP header the Content-Type specifies the format of the message body. This tells the Web API how to parse the contents of the message body. For example, if an HTTP post message to your API contains a PNG image, the POST request might have the following headers:
Content-Length: 95267
Content-Type: image/png
Adding correct media formats attributes to you WebAPI controller class shuld support indicated in them image formats:
[Route("api/TestUpload")]
[Consumes("multipart/form-data")]
[Consumes("image/jpg")]
[Consumes("image/png")]
[Consumes("image/gif")]
public class TestUploadController : Controller
{
}
In the case of any further problems or need to support image formats not available out of box create custom MediaFormatter clas derived from for instance BufferedMediaFormatter and use it in your project.

Compression Action Filter ASP.NET Core

I need to create Compression Action Filter using ASP.NET Core
I found some examples for MVC 5 like:
public class CompressAttribute : ActionFilterAttribute {
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var encodingsAccepted = filterContext.HttpContext.Request.Headers["Accept-Encoding"];
if (string.IsNullOrEmpty(encodingsAccepted)) return;
encodingsAccepted = encodingsAccepted.ToLowerInvariant();
var response = filterContext.HttpContext.Response;
if (encodingsAccepted.Contains("deflate"))
{
response.AppendHeader("Content-encoding", "deflate");
response.Filter = new DeflateStream(response.Filter, CompressionMode.Compress);
}
else if (encodingsAccepted.Contains("gzip"))
{
response.AppendHeader("Content-encoding", "gzip");
response.Filter = new GZipStream(response.Filter, CompressionMode.Compress);
}
}
But when I tried to use it with
using Microsoft.AspNet.Http;
It didn't work at all:
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.Filters;
using Microsoft.Extensions.Primitives;
namespace App.Filterss
{
public class CompressionFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
HttpRequest request = context.HttpContext.Request;
string acceptEncoding = request.Headers["Accept-Encoding"];
if (string.IsNullOrEmpty(acceptEncoding)) return;
acceptEncoding = acceptEncoding.ToUpperInvariant();
HttpResponse response = context.HttpContext.Response;
if (acceptEncoding.Contains("GZIP"))
{
response.Headers.Add("Content-Encoding", "gzip");
response.Body = new GZipStream(response.Body, CompressionMode.Compress);
}
else if (acceptEncoding.Contains("DEFLATE"))
{
response.Headers.Add("Content-encoding", "deflate");
response.Body = new DeflateStream(response.Body, CompressionMode.Compress);
}
}
}
}
I just get 200 http responce with no content at all.
Can anybody tip me how to perform compressing using filter?
This is a new feature at ASP.net Core 1.1.
This is the packages you will need.
https://www.nuget.org/packages/Microsoft.AspNetCore.ResponseCompression/
here is some official microsoft video showing how to use it:
https://youtu.be/IfLg6LQCl-Y?t=223
some background information:
https://github.com/aspnet/BasicMiddleware/issues/34
Code:
Project.json:
"dependencies": {
...,
"Microsoft.AspNetCore.ResponseCompression": "1.0.0"
}
Startup.cs:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
...
//Add Middleware
app.UseResponseCompression();
....
}
public void ConfigureServices(IServiceCollection services)
{
//Configure Compression level
services.Configure<GzipCompressionProviderOptions>(options => options.Level = CompressionLevel.Fastest);
//Add Response compression services
services.AddResponseCompression(options =>
{
options.Providers.Add<GzipCompressionProvider>();
});
}
Check out rynowak's answer on an GitHib issue for ASP.NET Core 1.0
1). If you're on IIS why not just use dynamic compression at the
server level?
https://technet.microsoft.com/en-us/library/cc753681%28v=ws.10%29.aspx
2). Our default streams are not intended to be read. By default we do
some buffering in these streams, but we don't buffer the whole
response because that would use an awful lot of memory. Once the
amount of data we're willing to buffer has been written, it goes out
on the wire and we don't store it any more. There are some techniques
to enable buffering, and one of them is to replace the stream.
3). If you want to mess with the response stream for an MVC action (or
all MVC actions), the right filter stage is a result filter (
IResultFilter or IAsyncResultFilter ).
Thing of it this way, calling View() in your controller is creating
an IActionResult , which will be executed by MVC later, it's not
actually executing the view inline, and that's why messing with the
stream in that phase doesn't do what you want.
Result Filters actually surround the execution of the view code.
4). If you want to mess with the response stream for your whole
application and don't want to/can't use gzip support provided by your
server, then middleware is a good choice. (Compare to filters, which
allow you to scope to Controller/Action).
If you still having issues with the middleware approach used above:
have you tried setting a break point in the action in question to make
sure it's actually hit? have you removed your action filter code
(which wouldn't do what you want)?

Categories

Resources