I'm using pechkin.synchronized to convert from HTML to PDF. On the first http request it works fine, but after that it gets stuck on the convert method and doesn't doesn't do anything after that.
Here's my controller action method:
public ActionResult ToPdf(int id)
{
var order = _orderBll.GetById(id);
var viewHtml = order.Body;
byte[] pdfBuf = new SimplePechkin(new GlobalConfig()).Convert(viewHtml);
return File(pdfBuf, "application/pdf");
}
Try using SynchronizedPechkin.
See:
Why my Web App hangs on the "easy to use" code example below?
Why my Web App hangs/crashes even after I've started using SynchronizedPechkin
Unfortunately, Pechkin is a dead project and has many unresolved issues. You can avoid these by using Tuespechkin's ThreadSafeConverter, Pechkin's development is continuing there.
Example:
IConverter converter =
new ThreadSafeConverter(
new PdfToolset(
new Win32EmbeddedDeployment(
new TempFolderDeployment())));
// Keep the converter somewhere static, or as a singleton instance!
// Do NOT run the above code more than once in the application lifecycle!
byte[] result = converter.convert(document);
I had the same problem with my application too. So i download Synchronized Pechkin from Nuget manager. Your code will look like:
using Pechkin;
using Pechkin.Synchronized;
public ActionResult ToPdf(int id)
{
var order = _orderBll.GetById(id);
var viewHtml = order.Body;
byte[] pdfBuf = new SynchronizedPechkin(new GlobalConfig()).Convert(viewHtml);
return File(pdfBuf, "application/pdf");
}
Related
I have a .NET 7 web app, where I have a controller that results in a sitemap.xml file. When I run the application locally, I get an XML file as a result with this content:
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"/>
And it looks like this:
However, when this is pushed to production (everything is hosted as a web app on Azure), the same endpoint returns nothing. It does recognize the endpoint and looks like this:
My code to generate this, is shown below:
[Route("/sitemap.xml")]
public async Task SitemapXml()
{
var countries = await _countryService.GetBySpecificationAsync(new CountrySpecification()
{
Take = int.MaxValue
});
Response.ContentType = "application/xml";
using (var xml = XmlWriter.Create(Response.Body, new XmlWriterSettings { Indent = true }))
{
xml.WriteStartDocument();
xml.WriteStartElement("urlset", "http://www.sitemaps.org/schemas/sitemap/0.9");
xml.WriteEndElement();
}
}
My question:
I am completely lost. At first I thought it was because I didn't add support for static files and this is considered a static file, but I do have:
app.UseStaticFiles();
In the Program.cs.
Any hints where I should be starting?
I spent some time this week wanting to answer this question, and I have time now.
The main issue with your attempt is you are not returning XML results. To do so I suggest using IActionResult interface.
Now time to create sitemap.xml. IMO there are 2 ways to go from here, either using a library OR writing your own sitemap method.
I will start with a library. For instance, there is a very simple library (NuGet) called SimpleMvcSitemap.Core. Install it in your project, and in your controller insert the following code:
[Route("/sitemap.xml")]
public async Task<IActionResult> SitemapXml()
{
// your await call etc
List<SitemapNode> nodes = new List<SitemapNode>
{
new SitemapNode(Url.Action("Index","Home")),
new SitemapNode(Url.Action("About","Home")),
//other nodes
};
return new SitemapProvider().CreateSitemap(new SitemapModel(nodes));
}
Btw for this test, I created an asp.net MVC .net 7 project.
I have deployed the solution to azure and it works both on local development and on azure. Here is the result:
If you do want to do it manually, you can do following
var listUrls = new List<string>
{
Url.Action("Index", "Home"),
Url.Action("About", "Home")
};
return new SitemapResult(listUrls);
And here is the implementation:
public class SitemapResult : ActionResult
{
private readonly IEnumerable<string> _urls;
public SitemapResult(IEnumerable<string> urls)
{
_urls = urls;
}
public override async Task ExecuteResultAsync(ActionContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var response = context.HttpContext.Response;
response.ContentType = "application/xml; charset=utf-8";
var settings = new XmlWriterSettings() { Async = true, Encoding = Encoding.UTF8, Indent = false };
using (var writer = XmlWriter.Create(response.Body, settings))
{
WriteToXML(writer);
await writer.FlushAsync();
}
}
private void WriteToXML(XmlWriter writer)
{
writer.WriteStartDocument();
// Write the urlset.
writer.WriteStartElement("urlset", "http://www.sitemaps.org/schemas/sitemap/0.9");
// url element
foreach (var item in _urls)
{
writer.WriteStartElement("url");
// loc
writer.WriteStartElement("loc");
writer.WriteValue(item);
writer.WriteEndElement();
writer.WriteEndElement();
}
writer.WriteEndElement();
writer.WriteEndDocument();
}
}
The manual way is also deployed on azure and works, but in the manual way you need to do a lot of work that is already done in a library. To be fair both above outcome is inspired form the question How to dynamically create a sitemap.xml in .NET core 2?.
from this msdn magazine: "A controller that returns void will produce an EmptyResult." I assume this holds true also for Task.
So maybe you need to change your return type of your method from Task to Task<IActionResult> (or whatever suits you most) and return the content with any of these availablle methods.
Then though, I cannot understand why without these mods is currently working locally.
I am trying to get the full contents of my modules From Zoho to our local Server. The deluge code does work as it returns to me the data which is being sent via the API. However, once it reaches the API, it is null. Any idea?
Below is the deluge code:
// Create a map that holds the values of the new contact that needs to be created
evaluation_info = Map();
evaluation_info.put("BulkData",zoho.crm.getRecords("Publishers"));
data = Map();
data.put(evaluation_info);
response = invokeurl
[
url :"https://zohoapi.xxxxx.com/publisher/publish"
type :POST
parameters:data
connection:"zohowebapi"
];
info data; (data returns all the data from publishers)
Here is my ASP.NET core restful API. It does ping it and create the file but the content of the file is null.
Route("[controller]")]
[ApiController]
public class PublisherController : ControllerBase
{
[HttpGet("[action]"), HttpPost("[action]")]
public void Publish(string data)
{
(it's already null when it comes here. why?)
string JSONresult = JsonConvert.SerializeObject(data);
string path = #"C:\storage\journalytics_evaluationsv2.json";
using (var file = new StreamWriter(path, true))
{
file.WriteLine(JSONresult.ToString());
file.Close();
}
}
}
}
What am I missing? Thank you
After contacting Zoho support, the solution he offered was to loop through the data in order to get all the contents from a module (if they are more than 200 records. With the solution provided, one doesn't really need the deluge code anymore as long as you have the ZOHO api set to your account in code. This was my final solution. This solution is not scalable at all. It's best to work with the BULK CSV.
// Our own ZohoAPI which lets us connect and authenticate etc. Yours may look slightly different
ZohoApi zohoApi = new ZohoApi();
zohoApi.Initialize();
ZCRMRestClient restClient = ZCRMRestClient.GetInstance();
var allMedicalJournals = new List<ZCRMRecord>();
for (int i = 1; i <= 30; i++)
{
List<ZCRMRecord> accountAccessRecords2 =
restClient.GetModuleInstance("Journals").SearchByCriteria("Tag:equals:MedicalSet", i, 200).BulkData.ToList();
foreach (var newData in accountAccessRecords2)
allMedicalJournals.Add(newData);
}
I'm encountering a problem sending files stored in a database back to the user in ASP.NET MVC. What I want is a view listing two links, one to view the file and let the mimetype sent to the browser determine how it should be handled, and the other to force a download.
If I choose to view a file called SomeRandomFile.bak and the browser doesn't have an associated program to open files of this type, then I have no problem with it defaulting to the download behavior. However, if I choose to view a file called SomeRandomFile.pdf or SomeRandomFile.jpg I want the file to simply open. But I also want to keep a download link off to the side so that I can force a download prompt regardless of the file type. Does this make sense?
I have tried FileStreamResult and it works for most files, its constructor doesn't accept a filename by default, so unknown files are assigned a file name based on the URL (which does not know the extension to give based on content type). If I force the file name by specifying it, I lose the ability for the browser to open the file directly and I get a download prompt. Has anyone else encountered this?
These are the examples of what I've tried so far.
//Gives me a download prompt.
return File(document.Data, document.ContentType, document.Name);
//Opens if it is a known extension type, downloads otherwise (download has bogus name and missing extension)
return new FileStreamResult(new MemoryStream(document.Data), document.ContentType);
//Gives me a download prompt (lose the ability to open by default if known type)
return new FileStreamResult(new MemoryStream(document.Data), document.ContentType) {FileDownloadName = document.Name};
Any suggestions?
UPDATE:
This questions seems to strike a chord with a lot of people, so I thought I'd post an update. The warning on the accepted answer below that was added by Oskar regarding international characters is completely valid, and I've hit it a few times due to using the ContentDisposition class. I've since updated my implementation to fix this. While the code below is from my most recent incarnation of this problem in an ASP.NET Core (Full Framework) app, it should work with minimal changes in an older MVC application as well since I'm using the System.Net.Http.Headers.ContentDispositionHeaderValue class.
using System.Net.Http.Headers;
public IActionResult Download()
{
Document document = ... //Obtain document from database context
//"attachment" means always prompt the user to download
//"inline" means let the browser try and handle it
var cd = new ContentDispositionHeaderValue("attachment")
{
FileNameStar = document.FileName
};
Response.Headers.Add(HeaderNames.ContentDisposition, cd.ToString());
return File(document.Data, document.ContentType);
}
// an entity class for the document in my database
public class Document
{
public string FileName { get; set; }
public string ContentType { get; set; }
public byte[] Data { get; set; }
//Other properties left out for brevity
}
public ActionResult Download()
{
var document = ...
var cd = new System.Net.Mime.ContentDisposition
{
// for example foo.bak
FileName = document.FileName,
// always prompt the user for downloading, set to true if you want
// the browser to try to show the file inline
Inline = false,
};
Response.AppendHeader("Content-Disposition", cd.ToString());
return File(document.Data, document.ContentType);
}
NOTE: This example code above fails to properly account for international characters in the filename. See RFC6266 for the relevant standardization. I believe recent versions of ASP.Net MVC's File() method and the ContentDispositionHeaderValue class properly accounts for this. - Oskar 2016-02-25
I had trouble with the accepted answer due to no type hinting on the "document" variable: var document = ... So I'm posting what worked for me as an alternative in case anybody else is having trouble.
public ActionResult DownloadFile()
{
string filename = "File.pdf";
string filepath = AppDomain.CurrentDomain.BaseDirectory + "/Path/To/File/" + filename;
byte[] filedata = System.IO.File.ReadAllBytes(filepath);
string contentType = MimeMapping.GetMimeMapping(filepath);
var cd = new System.Net.Mime.ContentDisposition
{
FileName = filename,
Inline = true,
};
Response.AppendHeader("Content-Disposition", cd.ToString());
return File(filedata, contentType);
}
To view file (txt for example):
return File("~/TextFileInRootDir.txt", MediaTypeNames.Text.Plain);
To download file (txt for example):
return File("~/TextFileInRootDir.txt", MediaTypeNames.Text.Plain, "TextFile.txt");
note: to download file we should pass fileDownloadName argument
Darin Dimitrov's answer is correct. Just an addition:
Response.AppendHeader("Content-Disposition", cd.ToString()); may cause the browser to fail rendering the file if your response already contains a "Content-Disposition" header. In that case, you may want to use:
Response.Headers.Add("Content-Disposition", cd.ToString());
I believe this answer is cleaner, (based on
https://stackoverflow.com/a/3007668/550975)
public ActionResult GetAttachment(long id)
{
FileAttachment attachment;
using (var db = new TheContext())
{
attachment = db.FileAttachments.FirstOrDefault(x => x.Id == id);
}
return File(attachment.FileData, "application/force-download", Path.GetFileName(attachment.FileName));
}
Below code worked for me for getting a pdf file from an API service and response it out to the browser - hope it helps;
public async Task<FileResult> PrintPdfStatements(string fileName)
{
var fileContent = await GetFileStreamAsync(fileName);
var fileContentBytes = ((MemoryStream)fileContent).ToArray();
return File(fileContentBytes, System.Net.Mime.MediaTypeNames.Application.Pdf);
}
FileVirtualPath --> Research\Global Office Review.pdf
public virtual ActionResult GetFile()
{
return File(FileVirtualPath, "application/force-download", Path.GetFileName(FileVirtualPath));
}
Action method needs to return FileResult with either a stream, byte[], or virtual path of the file. You will also need to know the content-type of the file being downloaded. Here is a sample (quick/dirty) utility method. Sample video link
How to download files using asp.net core
[Route("api/[controller]")]
public class DownloadController : Controller
{
[HttpGet]
public async Task<IActionResult> Download()
{
var path = #"C:\Vetrivel\winforms.png";
var memory = new MemoryStream();
using (var stream = new FileStream(path, FileMode.Open))
{
await stream.CopyToAsync(memory);
}
memory.Position = 0;
var ext = Path.GetExtension(path).ToLowerInvariant();
return File(memory, GetMimeTypes()[ext], Path.GetFileName(path));
}
private Dictionary<string, string> GetMimeTypes()
{
return new Dictionary<string, string>
{
{".txt", "text/plain"},
{".pdf", "application/pdf"},
{".doc", "application/vnd.ms-word"},
{".docx", "application/vnd.ms-word"},
{".png", "image/png"},
{".jpg", "image/jpeg"},
...
};
}
}
If, like me, you've come to this topic via Razor components as you're learning Blazor, then you'll find you need to think a little more outside of the box to solve this problem. It's a bit of a minefield if (also like me) Blazor is your first forray into the MVC-type world, as the documentation isn't as helpful for such 'menial' tasks.
So, at the time of writing, you cannot achieve this using vanilla Blazor/Razor without embedding an MVC controller to handle the file download part an example of which is as below:
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
[Route("api/[controller]")]
[ApiController]
public class FileHandlingController : ControllerBase
{
[HttpGet]
public FileContentResult Download(int attachmentId)
{
TaskAttachment taskFile = null;
if (attachmentId > 0)
{
// taskFile = <your code to get the file>
// which assumes it's an object with relevant properties as required below
if (taskFile != null)
{
var cd = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment")
{
FileNameStar = taskFile.Filename
};
Response.Headers.Add(HeaderNames.ContentDisposition, cd.ToString());
}
}
return new FileContentResult(taskFile?.FileData, taskFile?.FileContentType);
}
}
Next, make sure your application startup (Startup.cs) is configured to correctly use MVC and has the following line present (add it if not):
services.AddMvc();
.. and then finally modify your component to link to the controller, for example (iterative based example using a custom class):
<tbody>
#foreach (var attachment in yourAttachments)
{
<tr>
<td>#attachment.Filename </td>
<td>#attachment.CreatedUser</td>
<td>#attachment.Created?.ToString("dd MMM yyyy")</td>
<td><ul><li class="oi oi-circle-x delete-attachment"></li></ul></td>
</tr>
}
</tbody>
Hopefully this helps anyone who struggled (like me!) to get an appropriate answer to this seemingly simple question in the realms of Blazor…!
I am new to Google APIs. I want to know how to call Google Dialogflow API in C# to get intent form the input text. But I can't find any example to call Dialogflow using C#.
Please provide some example to call Dialogflow from C#.
If I understand your question correctly you want to call the DialogFlow API from within a C# application (rather than writing fulfillment endpoint(s) that are called from DialogFlow. If that's the case here's a sample for making that call:
using Google.Cloud.Dialogflow.V2;
...
...
var query = new QueryInput
{
Text = new TextInput
{
Text = "Something you want to ask a DF agent",
LanguageCode = "en-us"
}
};
var sessionId = "SomeUniqueId";
var agent = "MyAgentName";
var creds = GoogleCredential.FromJson("{ json google credentials file)");
var channel = new Grpc.Core.Channel(SessionsClient.DefaultEndpoint.Host,
creds.ToChannelCredentials());
var client = SessionsClient.Create(channel);
var dialogFlow = client.DetectIntent(
new SessionName(agent, sessionId),
query
);
channel.ShutdownAsync();
In an earlier version of the DialogFlowAPI I was running into file locking issues when trying to re-deploy a web api project which the channel.ShutDownAsync() seemed to solve. I think this has been fixed in a recent release.
This is the simplest version of a DF request I've used. There is a more complicated version that passes in an input context in this post:
Making DialogFlow v2 DetectIntent Calls w/ C# (including input context)
(Nitpicking: I assume you know DialogFlow will call your code as specified/registered in the action at DialogFlow? So your code can only respond to DialogFlow, and not call it.)
Short answer/redirect:
Don't use Google.Apis.Dialogflow.v2 (with GoogleCloudDialogflowV2WebhookRequest and GoogleCloudDialogflowV2WebhookResponse) but use Google.Cloud.Dialogflow.v2 (with WebhookRequest and WebhookResponse) - see this eTag-error. I will also mention some other alternatives underneath.
Google.Cloud.Dialogflow.v2
Using Google.Cloud.Dialogflow.v2 NuGet (Edit: FWIW: this code was written for the beta-preview):
[HttpPost]
public dynamic PostWithCloudResponse([FromBody] WebhookRequest dialogflowRequest)
{
var intentName = dialogflowRequest.QueryResult.Intent.DisplayName;
var actualQuestion = dialogflowRequest.QueryResult.QueryText;
var testAnswer = $"Dialogflow Request for intent '{intentName}' and question '{actualQuestion}'";
var dialogflowResponse = new WebhookResponse
{
FulfillmentText = testAnswer,
FulfillmentMessages =
{ new Intent.Types.Message
{ SimpleResponses = new Intent.Types.Message.Types.SimpleResponses
{ SimpleResponses_ =
{ new Intent.Types.Message.Types.SimpleResponse
{
DisplayText = testAnswer,
TextToSpeech = testAnswer,
//Ssml = $"<speak>{testAnswer}</speak>"
}
}
}
}
}
};
var jsonResponse = dialogflowResponse.ToString();
return new ContentResult { Content = jsonResponse, ContentType = "application/json" }; ;
}
Edit: It turns out that the model binding may not bind all properties from the 'ProtoBuf-json' correctly (e.g. WebhookRequest.outputContexts[N].parameters),
so one should probably use the Google.Protobuf.JsonParser (e.g. see this documentation).
This parser may trip over unknown fields, so one probably also wants to ignore that. So now I use this code (I may one day make the generic method more generic and thus useful, by making HttpContext.Request.InputStream a parameter):
public ActionResult PostWithCloudResponse()
{
var dialogflowRequest = ParseProtobufRequest<WebhookRequest>();
...
var jsonResponse = dialogflowResponse.ToString();
return new ContentResult { Content = jsonResponse, ContentType = "application/json" }; ;
}
private T ParseProtobufRequest<T>() where T : Google.Protobuf.IMessage, new()
{
// parse ProtoBuf (not 'normal' json) with unknown fields, else it may not bind ProtoBuf correctly
// https://github.com/googleapis/google-cloud-dotnet/issues/2425 "ask the Protobuf code to parse the result"
string requestBody;
using (var reader = new StreamReader(HttpContext.Request.InputStream))
{
requestBody = reader.ReadToEnd();
}
var parser = new Google.Protobuf.JsonParser(JsonParser.Settings.Default.WithIgnoreUnknownFields(true));
var typedRequest = parser.Parse<T>(requestBody);
return typedRequest;
}
BTW: This 'ProtoBuf-json' is also the reason to use WebhookResponse.ToString() which in turn uses Google.Protobuf.JsonFormatter.ToDiagnosticString.
Microsoft's BotBuilder
Microsoft's BotBuilder packages and Visual Studio template.
I havent't used it yet, but expect approximately the same code?
Hand written proprietary code
A simple example of incoming request code (called an NLU-Response by Google) is provided by Madoka Chiyoda (Chomado) at Github. The incoming call is simply parsed to her DialogFlowResponseModel:
public static async Task<HttpResponseMessage> Run([...]HttpRequestMessage req, [...]CloudBlockBlob mp3Out, TraceWriter log)
...
var data = await req.Content.ReadAsAsync<Models.DialogFlowResponseModel>();
Gactions
If you plan to work without DialogFlow later on, please note that the interface for Gactions differs significantly from the interface with DialogFlow.
The json-parameters and return-values have some overlap, but nothing gaining you any programming time (probably loosing some time by starting 'over').
However, starting with DialogFlow may gain you some quick dialog-experience (e.g. question & answer design/prototyping).
And the DialogFlow-API does have a NuGet package, where the Gactions-interface does not have a NuGet-package just yet.
First, some context:
Language - C#
Platform - .Net Framework 4.5
Project type - ASP.Net MVC 4
I am trying to determine which View in an MVC project is handling an explicit call to the following method. The MSDN docs for the method are here: http://msdn.microsoft.com/EN-US/library/dd492930.aspx
protected internal ViewResult View(
Object model
)
The original Author is using a View to generate a PDF file with a third-party library. I need to modify the view to include additional information.
The problem: I'm having trouble finding which View to modify. There are hundreds of them, and (IMHO) they are poorly named and organized. The basic process for generating a PDF looks like this. I'm getting confused in between steps 3 and 4.
An Entity's ID is passed to an ActionResult
The Entity is retrieved from the backing store
The model is passed to the Controller.View method mentioned above:
var viewModel = View(model);
var xmlText = RenderActionResultToString(viewModel);
The resulting ViewResult is used with an instance of ControllerContext to generate HTML as if being requested by a browser.
The resulting HTML is passed to the third-party tool and converted to a PDF.
I understand everything else very clearly. What I don't understand is how the call to View(model) determines which View file to use when returning the ViewResult. Any help greatly appreciated!
I'm including the code below, in case it helps anybody determine the answer.
The ActionResult:
public ActionResult ProposalPDF(String id, String location, bool hidePrices = false)
{
var proposal = _adc.Proposal.GetByKey(int.Parse(id));
var opportunity = _adc.Opportunity.GetByKey(proposal.FkOpportunityId.Value);
ViewData["AccountId"] = opportunity.FkAccountId;
ViewData["AccountType"] = opportunity.FkAccount.FkAccountTypeId;
ViewData["Location"] = location;
ViewData["HidePrices"] = hidePrices;
return ViewPdf(proposal);
}
The ViewPDF method:
protected ActionResult ViewPdf(object model)
{
// Create the iTextSharp document.
var document = new Document(PageSize.LETTER);
// Set the document to write to memory.
var memoryStream = new MemoryStream();
var pdfWriter = PdfWriter.GetInstance(document, memoryStream);
pdfWriter.CloseStream = false;
document.Open();
// Render the view xml to a string, then parse that string into an XML dom.
var viewModel = View(model);
var xmlText = RenderActionResultToString(viewModel);
var htmlPipelineContext = new HtmlPipelineContext();
htmlPipelineContext.SetTagFactory(Tags.GetHtmlTagProcessorFactory());
//CSS stuff
var cssResolver = XMLWorkerHelper.GetInstance().GetDefaultCssResolver(false);
var cssResolverPipeline = new CssResolverPipeline(cssResolver, new HtmlPipeline(htmlPipelineContext, new PdfWriterPipeline(document, pdfWriter)));
var xmlWorker = new XMLWorker(cssResolverPipeline, true);
var xmlParser = new XMLParser(xmlWorker);
xmlParser.Parse(new StringReader(xmlText));
// Close and get the resulted binary data.
document.Close();
var buffer = new byte[memoryStream.Position];
memoryStream.Position = 0;
memoryStream.Read(buffer, 0, buffer.Length);
// Send the binary data to the browser.
return new BinaryContentResult(buffer, "application/pdf");
}
The RenderActionResultToString helper method:
protected string RenderActionResultToString(ActionResult result)
{
// Create memory writer.
var sb = new StringBuilder();
var memWriter = new StringWriter(sb);
// Create fake http context to render the view.
var fakeResponse = new HttpResponse(memWriter);
var fakeContext = new HttpContext(System.Web.HttpContext.Current.Request, fakeResponse);
var fakeControllerContext = new ControllerContext(new HttpContextWrapper(fakeContext), this.ControllerContext.RouteData, this.ControllerContext.Controller);
var oldContext = System.Web.HttpContext.Current;
System.Web.HttpContext.Current = fakeContext;
// Render the view.
result.ExecuteResult(fakeControllerContext);
// Restore data.
System.Web.HttpContext.Current = oldContext;
// Flush memory and return output.
memWriter.Flush();
return sb.ToString();
}
I'm not exactly sure what you're asking, but, when you call View(model) the view that is chosen is based upon conventions.
Here is an example:
public class HerbController : Controller {
public ActionResult Cilantro(SomeType model) {
return View(model)
}
}
That will look for a view file called Cilantro.cshtml in a folder called Herb (Views/Herb/Cilantro.cshtml). The framework will also look in the Shared directory as well in case it is a view that is meant to be shared across multiple results.
However, you may also want to look at the Global.asax file to see if there are any custom view paths being setup for the view engine. The example I gave above is based upon the default conventions of ASP.NET MVC. You can override them to meet your needs better if needed.
The convention for views is that they are in a folder named after the controller (without "Controller") and the .cshtml file inside that folder is named after the calling action. In your case, that should be:
~/Views/[Controller]/ProposalPdf.cshtml
The logic to determine which view template will be used is in the ViewResult that is returned from the call
var viewModel = View(model);
And how the view is selected is determined by the configured ViewEngine(s), but it will use the current Area, Controller and Action route values to determine what view should be served.
What the route values are for the ProposalPDF action will depend on how your routing is configured, but assuming the defaults, the action route value will be ProposalPDF, the controller route value will be the name of the controller class in which this action resides (minus the Controller suffix) and the area will be the area folder in which the controller lives, with a value of empty string if in the default controller folder. Then using these route values, a view will be looked up in the Views folder using the following convention
~/Views/{Area}/{Controller}/{View}.cshtml
There is always Glimpse that can help with providing runtime Diagnostics too, such as which View file was used to serve up the returned page, although I'm not sure how this would look when a ViewResult is executed internally to provide the contents of a file.