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 ---
Rick Strahl
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…
Other Posts you might also like