Form Submissions and Request Redirection in the Web Control Framework
October 25, 2011 •
When using the Web Control Framework in Web Connection one question comes up frequently:
How do I move off a page and on to the next page when I’ve completed my form input?
This questions stems from the WCF’s forms based metaphor where by default every page posts back to itself. So when you have an input form filled with fields, those fields are filled out by the user and when he or she presses the Save button the form data is posted right back to the same page that holds all the form data.
This ‘Postback’ mechanism is actually quite common in forms centric input pages because user input typically has to be validated and if there’s an error the page has to be redisplayed with all the user entered values, along with some error information that describes what needs to be fixed. In the Web Control Framework this is the default mode operandi.
If you look at a Web Control Framework page in the markup you see:
<form id="form1" runat="server">
Which when rendered generates:
<form id="form1" method="post">
Overriding the <form> Target
Notice that no target is specified here, which makes browser resubmit the page to the current page. You can easily override this behavior however and simply force the page to submit to a completely different URL with:
<form id="form1" runat="server" target="SaveCustomer.ttk">
Now the form will submit to a completely different Web Control framework page. For WCF applications though this doesn’t make a lot of sense because all the benefits of the WCF – named form controls, automatic control postback values etc. – are all tied to the original page the POST operation originated from. So in SaveCustomer.ttk to capture all the form variables posted would require manual Request.Form("txtName") type logic to get all the names.
Frankly if you go this route, it’s better to use a classic Web Connection templates or scripts to render your views rather than going through a WCF page. The only benefit WCF presents in this scenario is easier layout.
Making Postbacks work for you
WCF pages are easy to work with because they are effectively classes that encapsulate a single form’s operation. They isolate all behavior associated with the capture, valdation and saving of a form and facilitate that process by easily binding and unbinding data from the UI into underlying data structures.
The process normally is:
- Display data in Page OnLoad or OnPreRender
- Capture Data in a Button Click Event(s)
Here’s an overly simple example of a page that uses a business object and displays and edits a customer object:
**************************************************************
DEFINE CLASS Customer_Page as WWC_WEBPAGE OF WWC_WEBPAGE_FILE
**************************************************************
*** Your Implementation Page Class - put your code here
*** This class acts as base class to the generated page below
**************************************************************
oCustomer = null
oLookups = null
#IF .F.
LOCAL this as Customer_Page_WCSX OF Customer_Page.prg
#ENDIF
FUNCTION OnLoad()
this.oCustomer = CREATEOBJECT("ttCustomer")
this.oLookups = CREATEOBJECT("ttLookups")
lcId = Request.QUeryString("Id")
this.oLookups.GetStates("TStates")
this.txtState.DataSource = "TStates"
this.oLookups.GetCountries("Tcountries")
this.txtCountryId.DataSource = "TCountries"
IF !this.oCustomer.Load(VAL(lcID) )
this.oCustomer.New()
ENDIF
IF !THIS.IsPostBack
THIS.DataBind()
ENDIF
ENDFUNC
************************************************************************
* btnSubmit_Click
****************************************
*** Function:
*** Assume:
*** Pass:
*** Return:
************************************************************************
FUNCTION btnSubmit_Click()
this.UnbindData()
IF this.BindingErrors.Count > 0
this.ErrorDisplay.ShowError(this.BindingErrors.ToHtml(),"Please correct the following:")
RETURN
ENDIF
IF !this.oCustomer.Validate()
this.AddValidationErrorsToBindingErrors(this.oCustomer.oValidationErrors)
this.ErrorDisplay.ShowError(this.BindingErrors.ToHtml(),"Please correct the following:")
RETURN
ENDIF
IF !this.oCustomer.Save()
THIS.ErrorDisplay.ShowError("Unable to save: " + this.oCustomer.cErrorMsg)
RETURN
ENDIF
this.ErrorDisplay.ShowMessage("Customer saved")
ENDFUNC
* btnSubmit_Click
ENDDEFINE
This works great and is pretty easy to understand.
But back to the original question now: How do we handle navigating off this page after a record has been saved?
In the example above we show error messages in the case of an error which is fine. If there are validation or other input errors we want to stay on the input page and display error messages (ShowError( BindingErrors.ToHtml() ) for example.
However when we’ve saved data most of the time we want to display a quick note that says “Customer Saved” (as I do in the sample above), but then we’re still stuck on the customer page which may or may not what you want.
One easy way to handle this is to redirect:
IF !this.oCustomer.Save()
THIS.ErrorDisplay.ShowError("Unable to save: " + this.oCustomer.cErrorMsg)
RETURN
ENDIF
this.ErrorDisplay.ShowMessage("Customer saved")
Response.Redirect("default.ttk")
But when you do this you don’t get to actually see the Customer Saved message. A Redirect immediately redirects without ever showing the original page and so no Customer Saved message.
Using the Refresh HTTP Header
What’s really needed in this example is to briefly display the confirmation message and THEN navigate off the page to the default page. You can use the Refresh HTTP header to accomplish this with the following code:
IF !this.oCustomer.Save()
THIS.ErrorDisplay.ShowError("Unable to save: " + this.oCustomer.cErrorMsg)
RETURN
ENDIF
this.ErrorDisplay.ShowMessage("Customer saved")
*** Redirect in 4 seconds
Response.AppendHeader("Refresh","4; url=default.ttk")
This solves the problem nicely! Now the page displays the Customer saved message for a few seconds and then after 4 seconds navigates off to the new URL specified.
This solves the confirmation display problem and still allows us to leave the confirmation display logic on the current page, rather than having to display a separate confirmation page.
Additionally this is also super useful if you need to capture cookies or new session assignments etc. On older browsers an HTTP 302/303 Location request, did not send additional HTTP header information to browsers so persistence token like Cookies were invariably lost. To be save you shouldn’t set Cookies (or new Sessions) when calling a Redirect as these cookies may get lost. Using the approach above of first redisplaying the page before redirecting ensures that that page is fully posted back and any cookies etc. get set properly.
Keep in mind that both Response.Redirect and the HTTP Refresh header allow only for GET operations: You can’t POST data to the target page. At best you can pass data on the query string to the new page.
To Postback or not to Postback
There’s been a lot of discussion around whether Postbacks to the same page are a good practice or not. In my opinion most of the time they are the right choice. Even in MVC style applications that use scripts/templates it’s not uncommon to see same page Postbacks manually handled. 90% of the time same page form postbacks simply make sense because you can share the same code for display and save operations – most of the time a lot of logic is shared between these operations.
In the few cases where Postbacks are confusing you can choose to override the target and send the POST data to a separate page. Or if you are completing a task that needs you to move on Response.Redirect lets you explicitly move on to another page.