Monday 28 February 2011

Response.Filter and UpdatePanels

Carrying on from my last blog, have you ever tried to use a response filter with UpdatePanels? If so, then I would imagine you've come across this error message:

"The message received from the server could not be parsed. Common causes for this error are when the response is modified by calls to Response.Write(), response filters, HttpModules, or server trace is enabled."



The problem is, the JavaScript receiving the text from the server will expect it in a certain format, as discussed in a previous post. If the text it receives doesn't conform to that format, or doesn't validate correctly (say for example, the length of the text sent doesn't match the length registered for it) then you'll get the above error.


There is a solution to this though, as we now know the format of the UpdatePanel response AND we know how response filters work, we can combine our knowledge of the two to do the following:

  1. Capture all the output using Response.Filter, taking into account "chunking".
  2. Create "UpdatePanelResponse" objects which will parse the output into objects that the JavaScript on the client expects.
  3. Transform the output.
  4. Output it to the client using our "UpdatePanelResponse" object to structure the output into the format we need.
Ok, so I'm now going to go through each step, detailing the code as we go...


Step 1 - Capture all the output using Response.Filter

Using Response.Filter, we need to capture all the output but we've got to remember that Response.Filter uses chunking. Essentially, the page output will be cut up into "chunks" of around about 16kb, however, to create our UpdatePanelResponse objects, we'll need the whole output. So, we need to grab each chunk as it comes and "cache" it, when we've grabbed the entire output we can put it all back together and then use it to create our objects. There's already a very good implementation out there implemented by Rick Strahl (http://west-wind.com/weblog/posts/72596.aspx) so I'm going to use his implementation with one minor modification, I'm going to make it implement the following interface:


public interface IFilterStream
{        
    event Func<String, String> TransformString; 
}

You'll see why a bit later. The thing I like about Rick's implementation is that he exposes a few events that are useful but for this particular example, the TransformWriteString event is the most important. This event is raised when all the output has been captured then allows you to call a method which receives a string and returns a string. The returned value is then the value that's returned to the client so, we can simply grab his implementation, map a handler to the TransformWriteString that'll do our output transformation and we're good to go. Nice and simple.

Step 2 - Create "UpdatePanelResponse" objects

Ok, the plan here is to parse the output string into objects that represent the UpdatePanel response string (essentially, the diagram included in this post). Once we've created all the objects, we need a way of performing transformations on the text and then writing all of it back out again in the format that the ScriptManager on the client will understand.
So, first off, lets define the class that'll represent an UpdatePanel response string...

internal class UpdatePanelFormat
{
    internal UpdatePanelFormat()
    {
    }
    
    internal Func<String, String> TransformMethod
    {
        get;
        set;
    }
    internal string Text
    {
        get;
        set;
    }
    
    internal int Length
    {
        get
        {
            return this.Text.Length;
        }
    }
    
    internal string Info
    {
        get;
        set;
    }
    
    internal string Type
    {
        get;
        set;
    }

    private bool _hasTransformed = false;
    private void Transform()
    {
        if (!_hasTransformed)
        {
            this.Text = this.TransformMethod(this.Text);
            _hasTransformed = true;
        }
    }

    public override string ToString()
    {
        this.Transform();
        return this.Length + "|" + this.Type + "|" + this.Info + "|" + this.Text + "|";
    }
}

As you can see, this object defines the four components of an UpdatePanel response string. It also gives us a method to transform the data by defining a delegate which takes a string and also returns one. The idea being that the string coming into the method will be the original text and the string being returned will be the string that we've transformed. Finally, the ToString method returns the string in it's expected format.

So, now we have that, we need a class that will go through the entire output string and create these objects. So, here's my implementation of that. Bare in mind that the above class is defined as internal. The only other class within my implementation that is within the same assembly as that class is the one defined below.

public class UpdatePanelResponse
{
    private static UpdatePanelResponse _instance;
    public static UpdatePanelResponse Instance
    {
        get
        {
            if (_instance == null)
                _instance = new UpdatePanelResponse();
                return _instance;
        }
    }
    public Func<String, String> Transform
    {
        get;
        set;
    }
 
    public string GetTransformedText(string responseText)
    {
        List<UpdatePanelFormat> list = this.CreateIndividualFormat(responseText);
        StringBuilder sb = new StringBuilder();
        foreach (UpdatePanelFormat fmt in list)
        {
            sb.Append(fmt.ToString());
        }
        return sb.ToString();
    }
        
    private List<UpdatePanelFormat> CreateIndividualFormat(string text)
    {
        string[] components = text.Split('|');
        List<UpdatePanelFormat> callbacks = new List<UpdatePanelFormat>();
        for (int i = 0; i < components.Length - 1; i = i + 4)
        {
            UpdatePanelFormat cb = new UpdatePanelFormat();
            cb.TransformMethod = Transform;
    
            if (i + 1 < components.Length)
                cb.Type = components[i + 1];
            if (i + 2 < components.Length)
                cb.Info = components[i + 2];
            if (i + 3 < components.Length)
                cb.Text = components[i + 3];
        
            if (i + 4 < components.Length)
            {
                int j = i + 4;
                StringBuilder sb = new StringBuilder(cb.Text);
                while (true)
                {
                    if (j >= components.Length -1)
                        break;
                    
                    int t;                        
                    string v = components[j];
                    if (Int32.TryParse(v, out t))
                        break;
                    
                    sb.Append("|" + v); // Add the | that we split by.
                    j++;
                    i++;
                }
                
                cb.Text = sb.ToString();
            }
            
            callbacks.Add(cb);
        }
        return callbacks;
    }
}

Ok, so, again, we have our delegate which takes and returns a string. This is passed on to our UpdatePanelFormat class. Remember, this is our outward facing class (the other is defined as internal). Then we have our CreateIndividualFormat method. This essentially goes through the entire string, splitting by the | character which is the separator for each individual piece of information and then creating our UpdatePanelFormat objects with this information. Finally, we have our GetTransformedText method. This takes the entire output string, passes it to our CreateIndividualFormat method which will create all of our objects. It'll then go through each of these objects, appending the transformed string to a StringBuilder instance and will then, finally, return the entire transformed text.

Step 3 and 4

We now have a method of formatting our output string, now all we need to do is give our UpdatePanelResponse object a Transform method to work with and then to send it all back to the client. We do both of these things when we map everything together within the global.asax.cs. In any application I've ever dealt with, I have an UpdatePanel surround virtually everything and then other UpdatePanels within that, so, if a postback is ever made, it will always run through an UpdatePanel, with that knowledge, we can do something like this:

void Application_BeginRequest(object sender, EventArgs e)
{
    HttpApplication app = sender as HttpApplication;

    if (app.Request.FilePath.EndsWith(".aspx"))
    {
        IFilterStream s;
        if (app.Request.UrlReferrer != null &&  app.Request.UrlReferrer.AbsolutePath == app.Request.Url.AbsolutePath)
        {
            s = new ResponsePostbackStream(app.Response.Filter);
            s.TransformString += new Func<string, string>(p_TransformString);
        }
        else
        {
    s = new FilterStream(app.Response.Filter);
            s.TransformString += new Func<string, string>(s_TransformString);          
        }

        app.Response.Filter = (System.IO.Stream)s;
    }
}

string p_TransformString(string arg)
{
    UpdatePanelResponse.Instance.Transform = s_TransformString;
    return UpdatePanelResponse.Instance.GetTransformedText(arg);
}

string s_TransformString(string arg)
{        
    return arg.Replace("/TestSite/", "/LiveEnv/");
}

As you can see, if we're NOT a postback, I use the same implementation as defined within http://clementscode.blogspot.com/2011/02/responsefilter-what-how-and-why.html except within the Write method, I raise the TransformString event and I also make the class implement the IFilterStream interface. This maps directly to s_TransformString which will replace all instances of /TestSite/ with /LiveEnv/. If we are a postback then we need to do a little bit more work, essentially, we map to p_TransformString which uses the UpdatePanelResponse object we've just created. It sets the Transform delegate to s_TranformWriteString so the exact same transformation that is taking place on normal requests, is also happening on postbacks. It then grabs and returns the transformed text which is output to the client.

And we're done! The JavaScript error will now longer appear.

Just bare in mind when using this that it'll obviously affect performance. How much will depend on your site, how big the pages are and what your transformations actually consist of so you should probably test it first to assess the impact. Equally, it could also have implications on your applications memory usage, the ResponseFilterStream implementation can use a fair amount of memory, as Rick describes in his blog post, again, you should monitor this and make sure it doesn't affect your application too much.

There's a fair amount of code there, some of which I may not have described very well so here's the source code zipped up.
Feel free to play around with it till your hearts content.

1 comment:

  1. Thanks! Found this after a lot of searching. I just wanted to make sure that Response.Redirects on the page, didn't redirect my page somewhere else. This will help me to remove the redirect code from the response.

    ReplyDelete