MS MVC Gets Blocks From Rails
Updated 2008-01-16:
Check out the refactored version which drops the reflection and uses a straight response filter.
Sergio Pereira and I started writing some stories today for Javascript helpers in Mvc Contrib. During our talk we had to deal with rendering html elements with inner html and javascript blocks ensuring they all got closed but still allowing the ultimate in flexibility for the developer.MvcToolkit goes about this with a using(…) pattern on their FormHelper. The one disadvantage of this is that the using block will be limited to only a single block. What happens when you need to render a block for onSuccess and onFailure?
Sergio brought up how powerful ruby blocks are and how cool it would be if we could use lambdas to do something similar.
Sergio proposed being able to do something like this.
<% Ajax.Tag(”div“, “/controller/action/123“,
new {CssClass=”bigSquare“}, myDiv =>
{ %>
Some Html here
Some helper: <%= Html.Link(”Click me“, “/other/non-ajax/url“) %>
}) %>
What is myDiv? myDiv is the blocks outer HtmlElement object so inside of your lambda you can manipulate its attributes and perform some other cool stuff we are still cooking up. While the lambda executes we capture the output and defer rendering to the response until after the lambda is finished. This allows us to render the HtmlElement with any modifications you made.
So how do we do this, well, we needed a pretty big hack because our delegate is being called from another anonymous delegate which has the HtmlTextWriter as a local variable. This means that we cannot use IHttpContext.SwitchWriter to capture our output.
Any way to get a fix for this MS?
So in reflector I noticed my anonymous type for my delegate had a public field __w which was the HtmlTextWriter. Using some reflection I was able to code up my own SwitchWriter which took care of everything.
The helper code is pretty simple.
public static void FromTag(this AjaxHelper helper ,string tag ,string url ,object options, Action<Element> innerHtml) { Element element = new Element(tag); HtmlTextWriter responseWriter = null; if(innerHtml != null) { try { using(StringWriter innerWriter = new StringWriter()) using (HtmlTextWriter innerHtmlWriter = new HtmlTextWriter(innerWriter)) { responseWriter = SwitchWriter(innerHtml, innerHtmlWriter); innerHtml(element); innerHtmlWriter.Flush(); element.InnerHtml = innerWriter.ToString(); } } finally { if(responseWriter != null) { SwitchWriter(innerHtml, responseWriter); } } } RenderElement(new HtmlTextWriter( helper.ViewContext.HttpContext.Response.Output), element); }
The switch writer is pretty basic, it was tracking it down that took a little bit of time.
public static HtmlTextWriter SwitchWriter(object obj , HtmlTextWriter newWriter) { Type actionType = obj.GetType(); object target = actionType.GetField(”_target“, BindingFlags.NonPublic | BindingFlags.Instance) .GetValue(obj); Type targetType = target.GetType(); HtmlTextWriter response = targetType.GetField(”__w“) .GetValue(target) as HtmlTextWriter; targetType.GetField(”__w“).SetValue(target, newWriter); return response; }
With the above in place I can call it with something like this.
<% Ajax.FromTag("div“, “/Home/About“,
new object(),
myDiv => { myDiv.Id = “Justice“; %><h6>
<%=Html.ActionLink(”Where is Justice?“, “About“)%>
</h6>
<% }); %>
And it renders something like this… no we didn’t spike any ajax stuff yet.
<div id=“Justice”> <h6><a href=“/Home/About”>Where is Justice?</a></h6> </div>
You may wonder why we do not return a string from our helper. Asp.Net currently can’t handle that. It thinks its a block and not a simple expression so using the regular <%= %> will not work. Instead of having you call <%Response.Write(…);%> we thought it would be easier if the helper just rendered it for you.
This got me excited about some the cool things we can really start to do. I like getting an instance of Element to set properties on, this can help reduce the abuse that anonymous types are taking in mvc right now. It would be great if there could be patch so we do not have to resort to reflection to switch writers, but I quote Sergio
how do you feel about that __w hack? For me it looks prettier by the minute
Let me know what you think,
Cheers
Beautiful.
Hi Adam
I really like this approach - I’ve been using a similar technique for some helpers I’ve been working on.
One issue that comes to mind is that VB does not support multi-line lambdas. If this becomes part of MvcContrib, do you think it would be a good idea to support an alternative syntax for VB users, or leave this as a C#-only feature?
Cheers,
Jeremy
VB?? Who uses that ?
It’s MVC, not MVB
It’s been years since I stopped tracking VB features and I’m really shocked to learn that VB still doesn’t have anonymous methods. I’m glad I don’t do VB anymore because having used lambdas now I’m hooked up on it and I wish I’ll never have to use a language that doesn’t have them anymore.
Any VB expert to suggest an alternative syntax or what would be a natural syntax for VB? Maybe the “Using() … End Using” block?
MVB….pfft
I agree with you about lambdas. I was quite disappointed to that VB only has a half-hearted implementation.
@Jeremy
You mentioned you were already doing something similar, how are you switching the writers to capture the output?
VB.Net support is going to be tough. Just googling the issue it already seems to be problem for vb guys because so many examples now are c# with anonymous delegates and now lambdas. I don’t think the using pattern will work, it almost seems like you will need a separate function in a script block for the delegate. As I understand it, you should still be able to switch writers so you can move in out of < % %> blocks to get the same effect.
Hi Adam,
It turns out that _w references the same instance as Response.Output so I used a custom Stream object and set the Response.Filter property to intercept each call to _w.Write. This works so long as Response.Buffer is set to false.
Its certainly a hack…but I’m not sure if its more or less of a hack than using reflection. I was surprised that didn’t call Response.Write directly. That would have made things much easier
Hmm…the comment box seems to have lost my angle brackets. That last line was supposed to say “I was surprised that %= % didn’t call Response.Write directly”
I sent an email to Sergio with another approach to do this that doesn’t require reflection. I used a Response Filter and well timed calls to Flush().
Thanks Phil,
That solution is much better. Jeremy pointed that one out too.
Thank God for blogs, otherwise we would have some ugly code with reflection, type caching, and dynamic methods.
I like the page you are serving up there
I just started going back through these entries in prep for the eventual return of my machine (still don’t have it).
[…] rendering (see this post on Adam Tybor’s blog for more […]