Capturing Output from ASP.Net Pages
One question I frequently see asked is how to capture output in ASP.Net applications. There are a variety of ways to accomplish this task depending on whether you need to capture the current page output or the capture the output of another page altogether. I typically have two separate scenarios for this:
1. Capture Current Page Content
I need to capture the current page content and either fix up the content in some way, or I need to capture it so that I can do things like email it, save it etc.
2. Capture Separate Page Content
In this scenario I usually need to run a page that is unrelated to the current page and capture the output. An example might be a mail merged document, or a confirmation template or even a user defined plug in that returns content to the current page.
Capturing the Current Page
For number 1 the process involves using the Render() method of the page to capture the output it generates and then manipulating the content. A couple of scenarios where I’ve used this approach is to capture the output from the current page and then email it to the customer. For example, in my West Wind Web Store the final confirmation page contains the order confirmation text that is then also mailed to the customer in HTML format. The code to do this looks something like this:
/// <summary>
/// Overridden to handle Confirmation of the order by
/// capturing the HTTP output and emailing it.
/// </summary>
/// <param name="writer"></param>
protected override void Render(HtmlTextWriter writer)
{
// *** Write the HTML into this string builder
StringBuilder sb = new StringBuilder();
StringWriter sw = new StringWriter(sb);
HtmlTextWriter hWriter = new HtmlTextWriter(sw);
base.Render(hWriter);
// *** store to a string
string PageResult = sb.ToString();
// *** Write it back to the server
writer.Write(PageResult);
// *** Now strip out confirmation part
string Confirmation = wwUtils.ExtractString(PageResult,
"<!-- Start Confirmation Display --->",
"<!-- End Confirmation Display --->",false);
if (wwUtils.Empty(Confirmation))
return;
// *** Add the header into the page
Confirmation = string.Format(
"<h2>{0} Order Confirmation</h2>\r\n{1}",
App.Configuration.CompanyName,Confirmation);
// Now create a full HTML doc and add styles required for email (no stylesheet)
sb = new StringBuilder();
sb.Append(@"
<html>
<head>
<meta http-equiv='content-type' content='text/html;charset=UTF-8'>
<style>
body
{
FONT-FAMILY: Verdana;
FONT-SIZE: 10pt;
FONT-WEIGHT: normal;
MARGIN-TOP: 0pt;
PADDING-TOP: 0pt
}
…
</style>
</head>
<body>
");
sb.Append(Confirmation);
WebStoreUtils.SendEmail(App.Configuration.CompanyName + " Order Confirmation #: " +
Invoice.GetTypedDataRow().Invno,
sb.ToString(),
Invoice.Customer.GetTypedDataRow().Email);
}
This code basically captures the output of the current page by intercepting the Render() method and hijacking the HtmlTextWriter used to output the page’s content. The code points the TextWriter at a StringBuilder object that is used to gain access to the generated Html content. Once captured the content is then written out into the writer that was passed into the method which causes the output to be sent back to the ASP.Net pipeline for display.
At this point we have a string of the HTML content that is then further fixed up; in this case the code extracts the ‘main’ block of text from the HTML page which is delimited with a comment block so it’s easy to extract using a custom string extraction method in my utility library. I’m extracting the core text only because the header of the online document points at a style sheet and includes some script code that I don’t want or need for the email message, so the extraction retrieves just what’s actually required. The result HTML is then marked up with the necessary HTML header and styles to make it display correctly as a standalone HTML document that is to be displayed inside of an email client. I don’t use images either to avoid Outlooks blocking of images and instead inject a text header that replaces the Store’s usual image and link banner (stripped out above the <!-- Start Confirmation Display --->" comment). When the markup’s complete the text is simply sent to the Application’s stock SendMail method that sends off the confirmation message.
This approach works well and is very efficient if the content you need to retrieve exactly matches the content that you are already generating in your page.
Capturing content from another Page in the Current Application
If you need to capture content from another page altogether you can take advantage of a very powerful function in the Server object: Server.Execute(). Server.Execute() let’s you run another ASP.Net page, pass in a TextWriter object and then return control back to the current page, which can then retrieve the TextWriter and its content easily.
I use this concept all the time for performing mail merge operations. For example in the West Wind Web Store I frequently want to generate email messages for customers that are used to let users know about order status or failures like declined credit cards. These forms are stored in a separate Templates directory of the Web Site and contain form letters that customize the messages to the customer and their order. These pages typically are plain text messages, not HTML. For example, here is the Declined Order Template:
<%@ Page language="c#"%>
<%@ Assembly name="wwWebStore" %>
<%@ Assembly name="wwBusiness" %>
<%@ import namespace="Westwind.WebStore" %>
<%@ import namespace="Westwind.WebStore.Admin" %>
<%@ import namespace="System.Data" %>
<script runat="server">
public DataRowContainers.wws_customersRow Cust = null;
public DataRowContainers.wws_invoiceRow Inv = null;
</script>
<%
// Put user code to initialize the page here
busInvoice Invoice = WebStoreFactory.GetbusInvoice();
string Pk = Request.QueryString["Id"];
int NumPk = Int32.Parse(Pk);
string Test = Request.QueryString["Test"];
Invoice.Load(NumPk);
this.Inv = Invoice.GetTypedDataRow();
this.Cust = Invoice.Customer.GetTypedDataRow();
%>
<%= string.Format("{0:D}",Inv.Invdate) %>
Hi <%= Cust.Firstname.Trim() %>,
Your order # <%= Inv.Invno.Trim() %> for <%= string.Format("{0:C}",Inv.Invtotal) %> has been declined!
Your credit card was declined for this transaction.
Please check your card and especially the card billing info
provided and resubmit the order or use another card for
the transaction.
Reason for decline:
<%= HttpUtility.UrlDecode((string) Inv.Ccresultx) %>
You can re-submit the order at:
<%= Westwind.WebStore.App.Configuration.StoreBaseUrl %>ReloadOrder.aspx?InvNo=<%= Inv.Invno.Trim() %>
All of your contact information is still online so you won't
have to reenter this information. Simply reselect your items
to purchase.
Regards,
+++ Rick ---
West Wind Technologies
http://www.west-wind.com/
http://www.west-wind.com/wwThreads/
-----------------------------------
Making waves on the Web
In this case the page does not use CodeBehind (like the rest of the application), although it could. To redirect to this and any other templates in the project I use a generic static application method that performs what I refer to a TextMerge:
public static string AspTextMerge(string TemplatePageAndQueryString)
{
string MergedText = "";
// *** Save the current request information
HttpContext Context = HttpContext.Current;
// *** Fix up the path to point at the templates directory
TemplatePageAndQueryString = Context.Request.ApplicationPath +
"/templates/" + TemplatePageAndQueryString;
// *** Now call the other page and load into StringWriter
StringWriter sw = new StringWriter();
try
{
// *** IMPORTANT: Child page's FilePath still points at current page
// QueryString provided is mapped into new page and then reset
Context.Server.Execute(TemplatePageAndQueryString,sw);
MergedText = sw.ToString();
}
catch(Exception ex)
{
System.Diagnostics.Debug.Assert(false,ex.Message);
MergedText = null;
}
return MergedText;
}
This method accepts a URL that’s relative to my application’s Templates directory and that can contain a querystring. Note that I’m retrieving the current HttpContext here since this is a static method that has no access to the Page – using HttpContext.Current makes it possible that the caller doesn’t have to pass in his context to the page.
To perform a text merge from anywhere then becomes a single static method call that looks like this:
string MergedLetter = WebStoreUtils.AspTextMerge(
"DeclinedOrder_Template.aspx?id=" + Inv.Invno );
This is very cool! With this mechanism you can basically extend the functionality of ASP.Net outside of the current request processing. It’s possible to perform sophisticated side tasks that are unrelated to the current request processing in this fashion. In fact, it’s great for building template generators etc.
The one caveat here is that if you have a failure inside of this page you will not know what actually failed. Unlike a standard ASP.Net page you will not get a nicely formatted ASPX error page returned as a string, but the page will simply fail. If you need to find out what went wrong you have to walk the Exception stack.
Capturing content from another Page in the Another ASP.Net Application
Both of the above approaches work great, but they require that you capture content out of the current application. What then? Well there’s always the obvious solution: You can always access the other application via a plain HTTP request:
public static string RetrieveHttpContent(string Url,ref string ErrorMessage)
{
string MergedText = "";
System.Net.WebClient Http = new System.Net.WebClient();
// Download the Web resource and save it into a data buffer.
try
{
byte[] Result = Http.DownloadData(Url);
MergedText = Encoding.Default.GetString(Result);
}
catch(Exception ex)
{
ErrorMessage = ex.Message;
return null;
}
return MergedText;
}
If you hit the local Web Server the performance of this operation is surprisingly fast so you shouldn’t worry too much about performance. Of course you can also use this to retrieve content from other Web sites if necessary and it’s a nice and easy wrapper to have around to quickly retrieve content from any URL in string format.
What about non-Web applications?
If you want template processing in WinForms applications – well you can do that too, using the ASP.Net runtime hosted in a Fat or Smart Client application. The ASP.Net Engine is fully self contained and you can actually host it natively in your own applications – although it is pretty resource intensive. I wrote an extensive article about this a while back and it includes a class that makes the process of executing ASP.Net pages from the file system fairly straight forward. With this class a WinForm application can then use a generic method to parse page templates like the Declined Order Template shown earlier with code like this:
public static string MergeText(string AspxPage,string QueryString, ref string ErrorMessage)
{
// *** Use the runtime host class to run the Template page
Westwind.AspRuntimeHost.wwAspRuntimeHost Host = new Westwind.AspRuntimeHost.wwAspRuntimeHost();
Host.cPhysicalDirectory = Directory.GetCurrentDirectory() + "\\Templates\\";
Host.cVirtualPath = "/Templates";
Host.cApplicationBase = Directory.GetCurrentDirectory();
Host.cConfigFile = Host.cPhysicalDirectory + "web.config";
if (!Host.Start())
{
ErrorMessage = Host.cErrorMsg;
return "";
}
string Message = Host.ProcessRequestToString(AspxPage,QueryString);
if (Host.bError)
ErrorMessage = Host.cErrorMsg;
Host.Stop();
return Message;
}
The wwAspRuntimeHost class makes short work of calling an ASP.Net page in the local file system assuming the directory it sits in is properly set up: It must contain a \bin directory as well as a web.config and global.asax file for ASP.Net to be able to start up in the directory. In this application I also have a Templates subdirectory into which I store custom templates – in fact the same templates that the online Web site uses. This directory then becomes my ‘Web Root’ to the ASP. Net runtime and I store all templates in this directory.
Then, to call one of the templates is as easy easy as:
public void DeclinedOrderEmail()
{
string ErrorMessage = "";
string Message = WebStoreUtils.MergeText("DeclinedOrder_Template.aspx",
"id=" + Invoice.Pk.ToString(),ref ErrorMessage);
if (ErrorMessage != "")
{
MessageBox.Show(ErrorMessage,App.WWSTORE_APPNAME,
MessageBoxButtons.OK,MessageBoxIcon.Exclamation);
return;
}
// *** Display Email with merged text
wwUtils.GoUrl("mailto:" +
this.Invoice.Customer.DataRow["Email"].ToString().TrimEnd() +
"?Subject=Re: West Wind Technologies Order Confirmation #" +
this.Invoice.DataRow["InvNo"].ToString().TrimEnd() +
"&Body=" +
Westwind.InternetTools.wwHttpUtils.UrlEncode(Message).Replace("+"," "));
}
In this example I generate a Declined Order notice which is then displayed in my default Email client ready for sending.
Summary
TextMerge functionality is something that I use in almost every application, and with ASP. Net you have a number of options available to you. The fact that the ASP.Net runtime is so flexible and that it exposes the ability to basically execute another page makes it very easy to build very sophisticated functionality that is not directly related to a Web Request. The fact that you can in fact plug the ASP.Net runtime into your own applications is one hell of a cool feature that opens many opportunities for customization and extensibility – it’s basically a pre-made execution and template engine that’s ready for you to be exploited. Have fun with it…
The Voices of Reason
# re: Capturing Output from ASP.Net Pages
# re: Capturing Output from ASP.Net Pages
Thanks, Rich
# re: Capturing Output from ASP.Net Pages
# re: Capturing Output from ASP.Net Pages
Now I know why : it doesnt get listed in any of the feeds.
The post say its was submitted the 8th of june but I can't find a trace of it in my rss client. You know what's going on?
# re: Capturing Output from ASP.Net Pages
Context.Server.Execute(TemplatePageAndQueryString);
I need to post information to another page every 10 min using a form eg
<form id="Form1" method=”get” action=”URL”>
<INPUT type="hidden" value="<%”GETcyberprop()”%>" name=" msisdn">
</form
Please Email me if you know : johan@fleetcube.com
# re: Capturing Output from ASP.Net Pages
# re: Capturing Output from ASP.Net Pages
in other words :
Is it possible to get the HTML output according to the following steps :
1) Dim Page as New MyApplication.SpecificPageofMyApplication
2) Dim StrOuput as String = GetOutput(Page)
# re: Capturing Output from ASP.Net Pages
While I CAN access ASP.Net objects, such as string, IDictionary, or DataSet, I CANNOT get a reference to my custom objects, such as MyNamespace.testObj, from HttpContext.Current.Items or even from HttpContext.Current.Session. Note that items were stored in these containers prior to the Server.Execute call.
Example code:
System.Web.HttpContext ctx = System.Web.HttpContext.Current.Current;
// assignment successful
System.Data.DataSet myDataSet = (System.Data.DataSet) ctx.Items["testDataSet"];
// assignment attempt generates error, invalid cast; curiously, object appears in VS Watch ctx._items
MyNamespace myTestObj = (MyNamespace.testObj) ctx.Items["testObj"];
Thanks in advance for any insight.
# re: Capturing Output from ASP.Net Pages
Clarification of previous post:
myDLL_ONE.dll calls Server.Execute("/myPage.aspx"); myPage.aspx is located in myDLL_TWO.dll.
If the custom objects are created in myDLL_ONE.dll, then there is no problem accessing the objects within /myPage.aspx.
However, if the custom objects are created as part of myDLL_TWO.dll, then i receive an error, invalid cast object reference not found.
**************
# re: Capturing Output from ASP.Net Pages
# re: Capturing Output from ASP.Net Pages
# re: Capturing Output from ASP.Net Pages
Probably I will end up adding a querystring check on this other page that would trigger this.MasterPageFile = ""; or something similar in Page_Load, but any suggestion to a more elegant approach that would let the other page unchanged is welcome :-)
# re: Capturing Output from ASP.Net Pages
# re: Capturing Output from ASP.Net Pages
I used it to cache result of a cube report.
# re: Capturing Output from ASP.Net Pages
# re: Capturing Output from ASP.Net Pages
// *** Write the HTML into this string builder
StringBuilder _sb = new StringBuilder();
StringWriter _sw = new StringWriter(_sb);
HtmlTextWriter _hWriter = new HtmlTextWriter(_sw);
base.Render(_hWriter);
// *** Write it back to the server
writer.Write(_htmlOutPut);
# re: mailmerge in ASP.Net
# re: Capturing Output from ASP.Net Pages
# re: Capturing Output from ASP.Net Pages
# re: Capturing Output from ASP.Net Pages
I've created a custom page class that inherits from the ui.Page and overridded render as per your example and I've got the caching engine working nicely.
Then I came to a page that uses some inline code :
<% response.Write("<p>hello world</p>") %>
for whatever reason this seems to render out of turn and it always appears at the top of the page. Then when I look at my cache file this inline HTML was missing.
So it would appear it rendered after the render method to the top of the page or something got confused in the render override / page lifecycle.
CAn you shed any light on this?
# re: Capturing Output from ASP.Net Pages
If you need to capture this sort of thing you can look into using a Response Filter which allows you to look at anything that gets written into the stream.
# re: Capturing Output from ASP.Net Pages
I also found this after some Googling
http://support.microsoft.com/default.aspx?scid=kb;en-us;306575
(ee troubleshooting section)
I think the easiest solution to this particular problem may be to set the innnerhtml value from the codebehind. Or maybe create a webrequest to grab the content.
Does the same apply to resource file values, how do they get rendered when inline?
# re: Capturing Output from ASP.Net Pages
# re: Capturing Output from ASP.Net Pages
I am creating a simple survey form, uSurvey.aspx. Users visiting the page able to fill out the survey form and click save. I would like the the page, uSurvey.aspx to be saved (with all filled textbox) on the web server as something like Answer_UserID.html after the user click on "Save" button. The question is that using "override void Render(HtmlTextWriter writer)" method will only save the initial empty page or will it save the page with user entered data?
Thank you for your help!
-Ron
# re: Capturing Output from ASP.Net Pages
so i just changed
<% Response.Write("string"); %> to <%=@" string " %>
If in my master page i setup an override Render method as a developer I mean assemble this whole page and send it to my render method for space removal - that is what i'm using it for.
Is this effect with response output not being captured in override render method a bug which may be fixed in the future?
Just doesn't make sense to me because I'm not fully overriding how the page is Rendered.
# re: Capturing Output from ASP.Net Pages
If you want to capture the complete output you need to hook up a Response.Filter...
# re: Capturing Output from ASP.Net Pages
Question: how to send the current page as a body output but also as an ATTACHMENT?
Please help or advise!
# re: Capturing Output from ASP.Net Pages
# re: Capturing Output from ASP.Net Pages
AeyGee
# re: Capturing Output from ASP.Net Pages
# re: Capturing Output from ASP.Net Pages
The Render.Filter property does not work for me as it only returns partial HTML that's sent to the client and the code I need to change is not in it. Is there any way I can apply the modification within the Render override?
# re: Capturing Output from ASP.Net Pages
# re: Capturing Output from ASP.Net Pages
The URL I'm sending works fine in a separate browser window; when I try
to execute my asp.net 1.1 code to scrape the url, I'm getting
an exception "The remote server returned an error: (500) Internal Server Error."
I checked the IIS log and it looks like the error is 80020009|Exception_occurred.__ 500 .
Is there anything I can change in the asp.net app to set up the environment so it acts just like the standalone browser? I saw some posts that got similar results (but no ideas were given as to what could be changed on the asp.net side)
Thanks!
# re: Capturing Output from ASP.Net Pages
Great article.
Is it possible to take the string and stuff it into a file on a hard drive asynchronously?
I would like to render the page and save the file via a business object in a background thread to speed up performance.
Is this possible in the Render method?
Any tips to increase performace of saving this file in the render method would be appreciated.
# re: Capturing Output from ASP.Net Pages
# re: Capturing Output from ASP.Net Pages
# re: Capturing Output from ASP.Net Pages
'Code scrap start
Dim strHTML As String
Dim ms As New IO.MemoryStream
Dim sw As New IO.StreamWriter(ms)
Dim tw As New HtmlTextWriter(sw)
---> Me.Render(tw)
sw.Flush()
Dim sr As New IO.StreamReader(ms)
strHTML = sr.ReadToEnd
'Code scrap end
In theory, this code should work.But when execution reaches the line specified with arrow the following message appears in VB developer IDE with green colored line and arrow.
"RegisterForEventValidation can only be called during Render();"
After I have the HTML source code, I will send it by putting that in a mail body.
Please tell me what I am doing wrong.A more practical solution will help me the most.
# re: Capturing Output from ASP.Net Pages
I need to create a PDFHtml embeded my form in it, then have user click on option then email to their customer.
Your method will work just fine with the email attachment. Do you have any source of create PDF form and email it?
# re: Capturing Output from ASP.Net Pages
It's really what i want to know.
# re: Capturing Output from ASP.Net Pages
Thanks a lot
# re: Capturing Output from ASP.Net Pages
Create page dynamically
Dim page as New MySpecificPageClass page.MasterPageFile="path to masterpage" page.dostuffthataddscontrolsandsuch() dim Output as String = page.renderpage()
specifically renderpage
note i have no control over the masterpage only the dynamically generated page (MySpecificPageClass)
Any help would be greatly appreciated.
Morder
# re: Capturing Output from ASP.Net Pages
This goes in the calling page
Dim p As New PageThatIMessWithDynamically Dim sw As New System.IO.StringWriter p.AppRelativeVirtualPath = HttpContext.Current.Request.AppRelativeCurrentExecutionFilePath p.SetBody("Here is the body!") p.ProcessRequest(HttpContext.Current) Dim output As String = p.Output.ToString
This is the PageThatIMessWithDynamically
Dim _sb As New System.Text.StringBuilder Public ReadOnly Property Output() As System.Text.StringBuilder Get Return _sb End Get End Property Protected Overrides Sub Render(ByVal writer As System.Web.UI.HtmlTextWriter) _sb = New System.Text.StringBuilder Dim sw As New IO.StringWriter(_sb) writer = New HtmlTextWriter(sw) MyBase.Render(writer) End Sub
SetBody() is a function i use to write data to the page
Obviously somewhere in the PageIMess... is where I put all the code to design the page I want before I call ProcessRequest
Hopefully this helps someone else out there and if there are ways to make it better feel free to post a reply here - Thanks Rick Strahl for your immense knowledge - hope I see you in vegas this year.
# re: Capturing Output from ASP.Net Pages
Thanks.
# re: Capturing Output from ASP.Net Pages
I am sendig a forms authentication report.aspx as a mail.I am getting the login.aspx page instead of report.aspx.
I tried in many ways to send mail but I successed here upto to do login programmatically.I am getting error like 'The remote server returned an error: (500) Internal Server Error.' at companyOpen.aspx page .
my report.aspx page root is
http://localhost/abc/xyz/lmn/report.aspx
In my application the user will redirect to companyOpen.aspx page after login.Then he should select the company from dropdown list then only he redirect to specified page.
Please help me that how can I send a web page as email which is forms authenticated.
Thanks in advance
# re: Capturing Output from ASP.Net Pages
In code behind page if i am writing "Hello World" then it is not saved html page, But if i am writing "Hello World" in html page like <div>Hello World</div> then after running the page i am able to save my .aspx page to .html page.
Would you give me a solution for this?
# re: Capturing Output from ASP.Net Pages
Very useful post, has been very helpful, thanks!
I need a little advice...I am using the idea for scraping the current page during the render phase. I was originally saving this output to session and then sending this in an email at a later phase which is ok until servers don't use session correctly. I am now using Viewstate instead for persisting data. this of course is already written in a previous phase. Is there another way to save this content for later use (without using session or viewstate)?
My scenario uses the Wizard Control which on the last but one captures review data, and the finish click is where the email is sent.
Your thoughts please!
Cheers,
Billy
# re: Capturing Output from ASP.Net Pages
# re: Capturing Output from ASP.Net Pages
# re: Capturing Output from ASP.Net Pages
# re: Capturing Output from ASP.Net Pages
# re: Capturing Output from ASP.Net Pages
# re: Capturing Output from ASP.Net Pages
# re: Capturing Output from ASP.Net Pages
# re: Capturing Output from ASP.Net Pages
Does this work in static class as well? As I'm getting assertion failure error. Please help. Thanks 😃
# re: Capturing Output from ASP.Net Pages