Support for AJAX form post and response in Razor Scripts

853 views
Skip to first unread message

Robert J. Foster

unread,
Aug 9, 2012, 4:15:39 PM8/9/12
to umbra...@googlegroups.com

Hi all,

 

I’ve been investigating what it would take to make Umbraco play nice with Razor scripts that want to “abort” the normal page content and return custom content – JSON strings in reply to an AJAX call, for example.

 

Been stewing on this topic for the past week or so, trying to come up with an elegant way to do something like this.  Having found a post by joeriks (http://joeriks.com/2011/11/06/separating-html-and-logic-in-razor-webpages-or-umbraco-macroscript/) which also touches on returning a JSON string in response to an AJAX post, I’ve attempted to delve into the best way to do such a thing with Umbraco without having to create a special Document Template as demonstrated in said post.

 

Tl;dr…

I succeeded.

 

I’ve now got some working code against the 4.8 branch, and I’m presently trying to check it into my fork to create a pull request against (authentication issues for some reason are preventing me).  So now I’m going to create a pull request.

 

Arrgggh… looks like I stuffed up a previous pull request.  Bugger.  So now the pull request “Master ContentType support enh” has the changes from this commit in it as well.  Sorry ‘bout that – not really sure how to clean it up now.  Will gladly take any advice.

 

I’ll have to write up some documentation on how it works and how to take advantage of it of course, but having seen questions/posts/etc. about this kind of thing kicking around for some time now, I’m guessing it may be useful for others?

 

My next question is, how fast can I get it into the core? J  Wouldn’t mind seeing it in the next release ;P

 

Here’s my User Story:

 

As a developer, I want to render a form with a Razor Macro, and instead of refreshing the page on submit, I want to retrieve a JSON string and use Knockout.js or something similar to update the UI based on the response.  I would also like to be able to have the Razor Script handle the form post and reply without relying on external libraries/modules/etc.

 

Robert Foster

R & E Foster & Associates

Refactored i.T

m: 0418 131 065 | e: rfo...@refoster.com.au

www: http://refactored.com.au/blog

 

Peter Josefson

unread,
Aug 10, 2012, 6:57:12 AM8/10/12
to umbra...@googlegroups.com
Hi Robert,
 
I think there is a much easier solution for this, that doesn't require any changes to umbraco at all:
 
Post to an alternate template (or a separate page that uses another template). You can easily modify the post URL using an onpost event handler on the form - or why not just post the form from js and cancel the ordinary post (return false from the event handler). Using an alternate template is as easy as "normal page url/template name", and saves you from having to create an additional page.
 
The alternate template would not inherit from any other template, it would just call the razor script (or, for that matter, a method on some object somewhere - even easier) via a macro.
 
We do this quite frequently when generating XML, jSon, PDF or other non-HTML content.
 
Or have I completely misunderstood what you're trying to achieve?
Caution: There is an existing API for this - the umbraco base API. From my limited experience from it, however, it is severely broken. It only does queries, not posts, so it's pretty useless - the maximum query string length usually explodes, and it also doesn't encode query parameters so user input regurlarly makes it fail (without lots of tweaks - I inherited a project that used it and I now do base65 encoding (and "=" encoding on top of that) and have adjusted the query string limits to ridiculous values - but maybe there's another way I don't know of...
 
Best regards,
Peter Josefson

 

Robert J. Foster

unread,
Aug 10, 2012, 7:43:13 AM8/10/12
to umbra...@googlegroups.com

Hi Peter,

The idea is to create a system that relies on nothing more than the Razor Macro Sxript itself – I wanted it to be set up so that developers wouldn’t have to build a dll or a add an alternate template in order to just return data in response to an AJAX post/get…  I have used both of those techniques at times too, but sometimes I just want to use a script without having to build more infrastructure.

 

This change allows you to build a Razor script that can return JSON data while cancelling the usual page content that the Macro might be rendered within (patch that makes this possible can be viewed in the existing pull request).

 

An example might be the following script (I haven’t included the Javascript (jQuery/knockout) that handles the form post/response – that’s trivial) – stripped down as much as I could… :

 

@inherits umbraco.MacroEngines.DynamicNodeContext

 

@* Render something *@

<form method="post" action="#" id="addProductToCart" data-bind="submit: addToCart">

    <input type="hidden" id="formId" name="formId" value="@FormId" /><input type="hidden" id="productId" name="productId" value="@product.Id" />

    <span class="price">@product.FormatCurrency(@product.UnitPriceIncTax)</span>

    <label for="productQty">Quantity:</label><input type="number" id="productQty" name="productQty" value="1" />

    <button type="submit">Add to Cart</button>

</form>

 

 

@functions{

    // FormId is used to ensure that this script can differentiate between

    // the form rendered by this script and any other form.

    public string FormId {get; private set;}

    public string Message { get; private set; }

   

    protected override void InitializePage()

    {

        base.InitializePage();

        FormId = "addProductToCart";

        if (IsPost)

        {

            var formId = Request["formId"];

            var sQty = Request["productQty"];

            var sProductId = Request["productId"];

            int productId = 0; int qty = 0;

           

            // If some of the elements don't match up or are invalid, then do nothing.

            var IsValid = (formId == FormId &&

                    int.TryParse(sProductId, out productId) &&

                    int.TryParse(sQty, out qty));

            if (IsValid)

            {

                // Update the Cart.

                Library.ShoppingCart.Add(productId, qty);

            }

            else

            {

                Message = "Please check the quantity and try again.";

            }

            var json = Json.Encode(new { isValid = IsValid, message = Message, itemCount =

                                    ShoppingCart.TotalItemCount, totalValue = ShoppingCart.TotalPrice });

 

            //Write the data to the output and close the connection.

            Response.Clear();

            Response.CacheControl = "no-cache";

            Response.AddHeader("Content-Type", "application/json");

            Response.AddHeader("Pragma", "no-cache");

            Response.Write(json);

           

            // Complete Request is a method on the BaseContext, and is a wrapper for

            // Context.ApplicationInstance.CompleteRequest();

            // It also sets a flag on the Macro that indicates that we want to terminate page

            // rendering.

            CompleteRequest();

Peter Josefson

unread,
Aug 10, 2012, 8:05:11 AM8/10/12
to umbra...@googlegroups.com
Hi,
 
Then I understood correctly the first time... I usually actually solve that scenario using a custom handler in a DLL (OR a custom template and Razor or a call into a DLL from the template - but only if it's really simple) - but writing that and modding the web.config for it may be a bit over the top for most people (but if you've done it once, it's really quite trivial - and I of course stuff all my AJAX calls in the same handler).
 
So, why not? There's no crime in making things even simpler, as long as it doesn't complicate things too much under the hood... :)
 
And... if your solution (haven't looked at the source) allows overriding other stuff in the base class, it could of course solve other needs as well...
 
/Peter

Robert J. Foster

unread,
Aug 10, 2012, 8:30:15 AM8/10/12
to umbra...@googlegroups.com

Hey Peter,

It actually shouldn’t change anything under the hood in the normal running of things – it simply allows a script to tell the ASP.Net Event Handler pipeline to skip to the end, and set flag so that Umbraco can ignore normally rendered content.

 

It should be possible to build in some convenience methods to do things like form generation/handling in the Macro’s BaseContext that could be hooked into to do the custom work by the script while also implementing some security measures to prevent things like XSS and the like – currently it’s up to the script to implement these kind of things.  Possibly even take advantage of some of the MVC technology.

 

I see this as the little brother of a full blown MVC integration into Umbraco… you can have a pseudo-controller implementation in the script and allow it to handle the response appropriately without interference.

 

Robert Foster

R & E Foster & Associates

Refactored i.T

m: 0418 131 065 | e: rfo...@refoster.com.au

--
You received this message because you are subscribed to the Google Groups "Umbraco development" group.
To post to this group, send email to umbra...@googlegroups.com.
To unsubscribe from this group, send email to umbraco-dev...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msg/umbraco-dev/-/P3Vk057uR-YJ.
For more options, visit https://groups.google.com/groups/opt_out.
 
 

Morten Bock Sørensen

unread,
Aug 10, 2012, 1:51:50 PM8/10/12
to umbra...@googlegroups.com
I'm not sure I understand this feature. Is the point to put the behaviour in the same place as the rendering of the form? Because that seems a bit anti-MVC to me?

Wouldn't it be cleaner to have the logic in a separate file, and the expose that on a different url? Maybe defined by convention? Such as /macroaction/nameofmacro

It could call a macro if one exists with that name, or fall back to a razor file, if there is one with that name.

I have not looked at the pulls request yet, but is the sample in this thread based on how it would be done before, or after the patch? The whole request.Clear() looks a bit out of place to me?

Sent from my Windows Phone

From: Robert J. Foster
Sent: 10-08-2012 14:30
To: umbra...@googlegroups.com
Subject: RE: Support for AJAX form post and response in Razor Scripts

image001.gif

Robert J. Foster

unread,
Aug 10, 2012, 7:06:16 PM8/10/12
to umbra...@googlegroups.com
Hi Morten,
In some ways I agree with you about the anti-MVC bit - I've based the example on joeriks' post (http://joeriks.com/2011/11/06/separating-html-and-logic-in-razor-webpages-or-umbraco-macroscript/) as I've noticed a few posts in forums and even the current issue tracker attempting to do something similar (most of them attempt to use Response.End() or Response.Redirect() instead which results in "unexpected" side effects - both methods throw ThreadAbortException, which causes Umbraco to report error messages in trace output).

I should probably attempt to separate out the example I gave from and the actual functionality of the patch… but in response to the request.Clear() bit below...

If I leave out the Response.Clear() statement, an exception will most commonly be thrown (depending on what's rendered prior to the macro) as I'm wanting to add headers to the response and then add my custom response content.  All the headers do is tell the requesting code to expect a JSON string, and in many cases, that's handled differently by a browser than say the normal html content, just as XML content would be.  I could leave the header manipulation out entirely probably. 

The example is purely to show what's possible with the patch.  Prior to the patch, you would end up with the JSON text rendered followed by the normally rendered content of the page.  The problem with this is that it breaks most javascript JSON deserializers when they find non-whitespace characters after the JSON string; and in addition it nullifies the benefit of retrieving a light fragment of data instead of the entire page when using AJAX.

What Umbraco does when rendering a page, is to actually buffer the rendered content in a string variable and then dump the whole lot to the stream at the end of the Render method in the code behind for default.aspx.  This patch makes it possible to instruct Umbraco to ignore any rendered content and just return - of course, the macro instructing Umbraco to do so would have to use Response.Write() to provide the content if it calls CompleteRequest(), or there would be no point. I haven't tested yet what would happen if more than one macro were on the page attempting to do the same sort of stuff, but my prediction would be that it would render the first, and the second would be effectively ignored - will have to test that hypothesis though - It's possible both would render out.

Another example where this could be useful is Response.Redirect() inside a macro.  Calling Response.Redirect("/some/page") has the side affect of throwing ThreadAbortException (and is a rather expensive operation).  however if you did this:

Response.Redirect("/some/page", false);
CompleteRequest();

You would in theory get your redirect with very little consequence.

Robert Foster

Morten Bock Sørensen

unread,
Aug 12, 2012, 6:19:16 AM8/12/12
to umbra...@googlegroups.com
On Saturday, August 11, 2012 1:06:16 AM UTC+2, Robert Foster wrote:
I should probably attempt to separate out the example I gave from and the actual functionality of the patch
 
Ok, I took a look at the patch, and as you said, it does not really have anything to do with the Ajax/post stuff.

A couple of things I am wondering. What is the performance on the recursive looping through all the controls in order to check the .CompleteRequestCalled property? Since it would be run on every single request, I think it is important. And I guess it would hurt more (if anything) on pages with a lot of controls. maybe it's not a problem, but it would be worth testing.

The other, as you mentioned, is how it works when multiple macros try to take advantage of this technique. But I guess that is not really the responsibility of the core, just as we cannot control when people decide to invoke Response.Redirect().

/Bock

Robert J. Foster

unread,
Aug 12, 2012, 7:04:12 AM8/12/12
to umbra...@googlegroups.com

Hi Morten,

Given that the controls have all been rendered and are in memory by the time the controls are being iterated, I don’t think the performance impact will be significant.  I have been thinking however that using an event somehow might be a better way to go… perhaps  just haven’t had time to explore that path lately. 

 

Perhaps the way forward on that one is move the CompleteRequest() method  to UmbracoContext or one of the related classes  – then we could just check the context to see whether or not we are meant to bypass content rendering…  would certainly remove the need to iterate through the controls…

 

Rob.

 

Reply all
Reply to author
Forward
0 new messages