Hugoware

The product of a web developer with a little too much caffeine

Using WebControls In ASP.NET MVC Views – Part 2

with 5 comments

Check out my newest blog post about using WebControls inside of MVC (source code included) :)

WebControls In MVC Series

In this post we continue the investigation if we can put WebControls inline with a MVC ViewPage. If you read my previous post on this topic, you’ll remember we got close, but couldn’t quite get it to work as expected.

You might be thinking “you shouldn’t use WebControls in Views!!” and you are absolutely right! I have two reasons for doing this project.

  1. A lot of investment has been put into creating WebControls. Some developers may not want to switch to MVC because they don’t want to lose everything they have already put together. Having this option available might help move them in that direction.
  2. I wanted to see if it was even possible to do!

I don’t really recommend mixing the two worlds together that much, but this might be handy for someone at some point somewhere (somehow…)

This is a series of posts to investigate if it is possible to use WebControls inline with MVC. The code in this post is not a final version and shouldn’t be used. Additionally, at the time of writing this I already have the code done for part 3 so I know the code gets better, but I still need do part 2 before I move to part 3. Hang in there! :)

It’s Just HTML

One thing I’ve kept repeating to myself while working on this is ‘it’s just HTML’ — If you think about it, we only need to get rendered content and post it back to the server in the same way that a normal WebForms page would do it. However, the ASP.net page lifecycle is not just HTML, in fact, there is a lot that goes into it.

If you remember, in the last attempt we we’re able to hook up events to our WebControls and then render them inline with the rest of our MVC content. We then used the BuildManager class to load an instance of a page and then process the request — but only to be met with a strange error message about our ViewState.

While I never did entirely figure out what the specific problem was I did suspect it had something to do that the ViewPage was some how having a problem with the ViewState that we were posting back that was actually rendered by the dynamically loaded page. If it doesn’t make sense, don’t worry — I’m still guessing here. :)

In any case, it appears that we were still on the right track. After a little more hacking I came up with the following code.

//snip...
/// <summary>
/// Class to allow WebForm Controls to be used inline with MVC
/// </summary>
public static class WebFormExtensionMethods {

	/// <summary>
	/// Executes a delegate to generate the control to render onto the page
	/// </summary>
	public static void RenderWebFormControl(this ViewPage page, Func<Control> gen) {
		WebFormExtensionMethods.RenderWebFormControl(page, gen());
	}

	/// <summary>
	/// Renders a WebControl onto the page that supports postbacks
	/// </summary>
	public static void RenderWebFormControl(this ViewPage page, Control control) {

		//the name of the loader container and the viewstate hack
		string path = "~/ViewPageWithWebForm.aspx";
		string mvcPrefix = "MVCSTATE";

		//load the container and add the control
		WebControlOutputPage template = (WebControlOutputPage)
			BuildManager.CreateInstanceFromVirtualPath(path, typeof(WebControlOutputPage));

		//add the control to the page
		template.Init += (s, e) => {
			template.Form.Controls.Add(control);
		};

		//capture the output to format
		template.Rendered += (output) => {

			//change the names of the viewstate items to prevent
			//postback errors
			XDocument doc = XDocument.Parse(output);
			foreach (var item in doc.Descendants()
				.Where(o => o.Attribute("name") is XAttribute &&
					o.Attribute("name").Value.StartsWith("__"))) {

				//change the state name
				item.Attribute("name").Value = string.Concat(
					mvcPrefix,
					item.Attribute("name").Value
					);
			}

			//display each of the elements inline
			foreach (XElement show in doc.XPathSelectElement("html/body/form").Elements()) {
				page.Response.Write(show.ToString());
			}

		};

		//generate a query string of all the requested items
		//there is probably a much better way to do this
		StringBuilder query = new StringBuilder();
		foreach (var item in page.Request.Params.AllKeys) {
			string name = item.Replace(mvcPrefix, string.Empty);
			query.Append(name + "=" + page.Server.UrlEncode(page.Request[item]));
			if (!page.Request.Params.AllKeys.Last().Equals(item)) { query.Append("&"); }
		}

		//create a new HttpRequest to process the request with
		HttpRequest req = new HttpRequest(
			path,
			page.Request.Url.AbsoluteUri,
			query.ToString()
			);

		//process the container page
		((IHttpHandler)template).ProcessRequest(
			new HttpContext(req, new HttpResponse(new StringWriter()))
			);
	}
}

/// <summary>
/// Page rendering class for WebControl output
/// </summary>
public class WebControlOutputPage : Page {

    //notify the calling parent the HTML is ready
    public event Action<string> Rendered;

    //render the output to work with
    protected override void Render(HtmlTextWriter writer) {
        using (StringWriter output = new StringWriter()) {
            using (HtmlTextWriter html = new HtmlTextWriter(output)) {
                base.Render(html);
                this.Rendered(output.ToString());
            }
        }
    }
}

You will also need an .ASPX page to load your controls with. Make sure it inherits from the WebControlOutputPage class above. Below is an example that will work.

<%@ Page Language="C#" Inherits="WebControlOutputPage" %>
<html>
    <head runat="server" />
    <body>
        <form id="pf" runat="server" />
    </body>
</html>

Since this code isn’t the final version I won’t explain too much of it and instead focus on the relevant parts.

A ViewState By Any Other Name Is… Uh…

I personally found HTML to be a little strange when it came to the name and id attributes on certain elements, it seemed to me that those were redundant, but hardly a real issue. In this instance, having the two attributes works to our advantage.

You’ll notice that in our code we append and remove a prefix to the names of the state containers, for example the __VIEWSTATE and __EVENTSTATE, on our page. By doing this, the id remains the same so Javascript can continue to access it but the name changes to avoid being caught as an error in the ViewPage.

You’ll also notice that we create a new HttpRequest and rename the state values to their original name. This allows any state information to slip past the ViewPage, but then be used by the WebControl Form. There is more than likely a better way to do that part, but for now it works — at least for this purpose.

So How About An Example?

The helper methods have changed up a bit to allow the control to be created by a delegate inline. This way it is a little easier to assign event handlers to your controls before they are rendered. Let’s put a simple button on the page.

<html>
    <body>
        <h2>WebControl Inline With MVC</h2>
        <form method="post" >

        <% this.RenderWebFormControl(() => {

               //create the button
               Button test = new Button() {
                   ID = "myButton"
               };

               //assign an event
               test.Click += (s, e) => {
                   test.Text += test.Text.Length.ToString();
               };               

               //return the control so it can be used
               return test;

           }); %>

        </form>
    </body>
</html>

By using a delegate we’re given a chance to assign a click handler to append some text onto our WebControl. After it is returned in the delegate it is rendered to the page. After we wrap the control with a form and then run it check out what we get.

postback-success

Success!! Our postback went through and our control is being updated as we wanted — and all inline with our MVC code!!

Not Quite There…

This code works pretty well, but there is a couple problems already. First, we can only have one control per page because each control outputs its own ViewState — so having a Button talk to a TextBox isn’t going work right now. Additionally, code that uses the __doPostback method is going to fail because that method isn’t added to the page yet.

Good news is that we solve those problems in the next post in this series! Hang in there and check back soon!

About these ads

Written by hugoware

August 7, 2009 at 1:10 am

5 Responses

Subscribe to comments with RSS.

  1. WebControls in MVC – Part 2 of ??? « Yet Another WebDev Blog…

    Thank you for submitting this cool story – Trackback from DotNetShoutout…

    DotNetShoutout

    August 7, 2009 at 6:42 am

  2. […] Using WebControls In ASP.NET MVC Views – Part 2 […]

  3. […] Part 2 – Simple Postbacks! […]


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: