Modifying Page Output
page 1 of 1
Published: 09 Oct 2003
Unedited - Community Contributed
Abstract
Intercept and modify the output created from a ASP.Net page.
by Robert Boedigheimer
Feedback
Average Rating: This article has not yet been rated.
Views (Total / Last 10 Days): 27240/ 54

Background

Have you ever needed to access the output from a web page and modify it before it was sent to the client? When an ASP.Net web page contains several user controls, .aspx HTML content, data bound server controls, and content added by the code behind file, it can be time consuming to make changes that would impact all of these areas (especially when multiple pages are involved). Often it is useful to allow the page to be created, then simply access the output that is about to be sent to the client, but modify it in some way. The idea is to create a custom filter that the Response (HttpResponse class) object uses to write the content. The filter references a Stream object, so it is necessary to derive a custom class from the abstract Stream object. The Stream class has a Write( ) method which is where the code to modify the output should be placed. Once the class is created, the Response.Filter can be changed to use the custom filter, and the content can be modified by a single method before being sent to the client. This is very powerful concept when combined with regular expressions, which allow selective matching of content and replacements.

Implementation

Basic filtering
System.IO.Stream is an abstract class, so it it cannot be instantiated, and can only serve as a base class for other classes. The first step, therefore, is to create a derived class and implement the abstract methods of the System.IO.Stream class. Create a new class file in a project and replace its code with the following, which is a basic implementation of the derived class provided from the help for Response.Filter in Visual Studio.Net (converts all content to upper case):
using System;
using System.IO;

namespace ResponseFilters
{
  public class UpperCaseFilter : Stream
  // This filter changes all characters passed through it to uppercase.
  {
      private Stream _sink;
      private long _position;

      public UpperCaseFilter(Stream sink)
      {
          _sink = sink;
      }

      // The following members of Stream must be overriden.
      public override bool CanRead
      {
         get { return true; }
      }

      public override bool CanSeek
      {
         get { return true; }
      }

      public override bool CanWrite
      {
         get { return true; }
      }

      public override long Length
      {
         get { return 0; }
      }

      public override long Position
      {
         get { return _position; }
         set { _position = value; }
      }

      public override long Seek(long offset, System.IO.SeekOrigin direction)
      {
         return _sink.Seek(offset, direction);
      }

      public override void SetLength(long length)
      {
         _sink.SetLength(length);
      }

      public override void Close()
      {
         _sink.Close();
      }

      public override void Flush()
      {
         _sink.Flush();
      }

      public override int Read(byte[] buffer, int offset, int count)
      {
         return _sink.Read(buffer, offset, count);
      }

      // The Write method actually does the filtering.
      public override void Write(byte[] buffer, int offset, int count)
      {
         byte[] data = new byte[count];
         Buffer.BlockCopy(buffer, offset, data, 0, count);

         for (int i = 0; i < count; i++)
         // Change lowercase chars to uppercase.
         {
            if (data[i] >= 'a' && data[i] <= 'z')
               data[i] -= ('a'-'A');
         }

         _sink.Write(data, 0, count);
       
      }
   }
}

A page that wants to implement the given filter can then simply just replace the existing default filter with this in the Page_Load( ) method of the page:
private void Page_Load(object sender, System.EventArgs e)
{
 Response.Filter = new ResponseFilters.UpperCaseFilter(Response.Filter);
}

After compiling and running the page, you will see that ALL of the page has been converted to upper case (including the META tags, HTML tags, etc). This demonstrates the power of this technique, imagine having to modify a series of pages that were all lower case in the .aspx page, code behind, user controls, etc. But if you now drag a ASP.Net button control unto the form, and simply hit the button to cause a postback, you will get an exception that says The View State is invalid for this page and might be corrupted. What happened? If you go back and view the HTML source for the page, you will see that the __VIEWSTATE hidden field that manages viewstate for ASP.Net pages was also converted to all uppercase. This field stores values for server controls, and it uses Message Authentication Code (MAC) to determine if the data has been modified. The MAC is a value computed based on the original data and then added to the data. When the page is posted, the server can compute the MAC again and compare to the one sent to determine if the data has been modified. Guess what, its been modified to be all capital letters! So although this is powerful, it is too granular of an approach.
Regular Expressions to Control Modifications
The next step is to target changes to only particular portions of a page. The changes required to provide the custom modifications must be done to the Write( ) method of the class. ASP.Net appears to call this method for every 28k worth of data (and in some other instances as well). To ensure that matches are not missed across this boundary, it is necessary to buffer the information internally. This sample code requires that the page must end in </html> so that it knows when the last portion is being written so it can do the actual write of the locally buffered data. For example, the following code can be placed in the Write( ) method to convert instances of the text "test" with "newTest":
public override void Write(byte[] buffer, int offset, int count) 
{ 
  //Get a string version of the buffer
  string szBuffer = System.Text.UTF8Encoding.UTF8.GetString(buffer, offset, count);

  //Look for the end of the HTML file
  Regex oEndFile = new Regex("</html>", RegexOptions.IgnoreCase);
  if (oEndFile.IsMatch(szBuffer))
  {
    //Append the last buffer of data
    oOutput.Append(szBuffer);
    
    //Get back the complete response for the client
    string szCompleteBuffer = oOutput.ToString();
    
    Regex oRegEx = new Regex("test", RegexOptions.IgnoreCase);
    if (oRegEx.IsMatch(szCompleteBuffer)) 
    {
      //Found string, so replace all occurences with the new value (use a non-case sensitive
      // match)
      string newBuffer = Regex.Replace(szCompleteBuffer, "test", "newTest", RegexOptions.IgnoreCase);
      
      //Set the reference so can use same code below...
      szCompleteBuffer = newBuffer;
    }

    //No match, so write out original data
    byte[] data = System.Text.UTF8Encoding.UTF8.GetBytes(szCompleteBuffer);
    _sink.Write(data, 0, data.Length);
  }
  else
  {
    oOutput.Append(szBuffer);
  }
}

It is also necessary to add a "using System.Text;" to the top of the class file, and change the original line from:
  private long _position;

to:
  private long _position; 
  StringBuilder oOutput = new StringBuilder();

Now modify a page to include some instances of the text "test", and see that they are modified to "newTest" when the page is accessed. Be aware that the example changes instances such as "test1" to "newTest1", it does not change only complete words (although this can be done with more sophisticated regular expressions).

Conclusion

The ability to modify the content created by an ASP.Net page is very powerful. We had a problem where a few pages on a site needed to have the path of <img> references replaced with another value. The problem was that the image tags were in several shared user controls on those pages, so using any other technique to modify the path at runtime would have had a performance impact for all uses of the control. It made more sense to use the above technique on the few pages that required it, rather than modify many user controls and page tags. The technique is limited only by your imagination, as any content created by an ASP.Net page can be modified using this technique.

Send comments or questions to robertb@aspalliance.com.


User Comments

Title: very useful   
Name: Mizzo
Date: 2008-07-04 9:12:43 AM
Comment:
thanks, very useful
Title: Beware with Ajax   
Name: Pete Nash
Date: 2008-03-26 7:45:46 AM
Comment:
Really helpful article, even today. But it stumped me with ajax controls as they do not obviously send the HTML tag as the end delimiter. To overcome this I also check the buffer size is >= 28670, to determine if it is the last packet, and its working again. Cheers.
Title: It's really great   
Name: Pravangsu Biswas
Date: 2008-02-22 3:17:55 PM
Comment:
Thank you very much Mr. Robert Boedigheimer. It solved my problem to modify the HTML output of the page conditionally .
Title: It's really g8 which i have found on internet   
Name: Kunal Singh
Date: 2007-07-25 5:46:44 AM
Comment:
Thank you very much to author of above article.I'm g8 to u that it solved many problem when i was doing XHTML code
Title: Removing whitespace from asp responses   
Name: Brian
Date: 2005-06-03 2:32:30 AM
Comment:
Does anyone know if these filters can be applied to non-asp.net responses too? I am trying to use the new wildcard mapping (.*) in IIS to replace my current ISAPI, but i need to filter all requests (ie. classic ASP too) instead of just asp.net. So far it looks like i can perform actions before the request gets handed to asp but not post-processing.
Title: Modifying Page Output   
Name: johnny johnston
Date: 2005-04-13 3:20:20 PM
Comment:
this article is a God send!
THANK YOU for writing it.
Title: Modifying Page Output   
Name: Charlie G.
Date: 2004-11-17 4:26:16 PM
Comment:
This article saved my project. I need to use this techinique to remove the querystring from the response in order to work with my reverse proxy.

Thanks!
Title: Modifying Page Output   
Name: Al Haveriku
Date: 2004-07-15 2:45:52 PM
Comment:
This is just great. It helped me a lot.

thanks






Community Advice: ASP | SQL | XML | Regular Expressions | Windows


©Copyright 1998-2017 ASPAlliance.com  |  Page Processed at 2017-07-28 2:38:41 AM  AspAlliance Recent Articles RSS Feed
About ASPAlliance | Newsgroups | Advertise | Authors | Email Lists | Feedback | Link To Us | Privacy | Search