Showing posts with label HTTP POST. Show all posts

ASP.NET MVC - Post to controllers

When you first start out using the ASP.NET MVC framework, it might not be apparent the many different ways you can solve the same problem. One example of this is when you want to post form data from a view to a controller.

Most of the official examples when the ASP.NET MVC framework was in early preview showed how to use the HttpRequest object to gather the form data inside the controller.


public ActionResult Edit()
{
SalesOrderForm form = new SalesOrderForm();

string customerName = Request.Form["CustomerName"];
string address = Request.Form["CustomerAddress"];
string orderNumber = Request.Form["OrderNumber"];

return View(form);
}


In this way, you can use the Request object and pull the data out. I don't like to tie the controller directly to the HttpRequest object without any abstraction and I really dislike having use magic strings. It brings back not so fond memories of classical ASP for those that have used the Microsoft stack long enough. Thankfully, very few examples uses this way of gathering the form data.

An alternative way to pass the form data is to create an action method in the controller which will accept the values posted from the view. The view form fields must match the same names in the controller. A subset of our view shows the fields we are concerned with.


<% using (Html.BeginForm()) { %>

Fields



<%= Html.TextBox("CustomerName") %>




<%= Html.TextBox("Address") %>




<%= Html.TextBox("OrderNumber") %>






<% } %>


The controller action method must have matching variable names. The action method is pretty simple in our case and we just need the matching variable names.


[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(string customerName, string address, string orderNumber)
{

string myCustomerName = customerName;
string myAddress = address;
string orderNumber = orderNumber;

// Persist the data into our repository

return View();
}


It's a pretty simple exercise to match them but quickly gets unruly if we have more than a handful of parameters we need to pass in. Thankfully, there are a few better ways to handle it. In the next scenario we will use the FormCollection object and use the MVC framework to map it to a view model.


public class SalesOrderForm
{
public string CustomerName { get; set; }
public string Address { get; set; }
public string OrderNumber { get; set; }
}



[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(FormCollection formCol)
{
SalesOrderForm form = new SalesOrderForm();

UpdateModel(form);

// Persist to our repository

return View(form);
}


Our action method only needs to accept the FormCollection object. We instantiate our view model object and call the UpdateModel method which is method of the base class System.Web.Mvc.Controller. Any controller class that you create using the Visual Studio MVC template is subclassed from it and have this method available. Through reflection, it converts between the properties and the form data passed in. This gives us a pretty clean way of mapping the form data to our view model.

There is an even cleaner way to map the form data to a view model.


[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit([Bind(Prefix = "")]SalesOrderForm salesForm)
{
// Persist to the repository

return View(salesForm);
}


By using the Bind attribute, the controller will attempt to match up the properties with the form data. I think this is the cleanest way to map between the view and the view model and this is the way I usually handle form data that is posted to the controllers.

Handling SOAP faults with HttpWebResponse object

When you call web services, in normal cases you would use the web service proxy objects that gets generated when you add a web reference and not send in an HTTP POST to the web service. I had an interest case recently where we had to build a test client that accepted invalid SOAP requests with malformed XML as well as well-formed SOAP requests.

If the web service returns a normal response, we can handle the response and just parse the returned XML. The code is what you would expect

 
try
{

WebResponse webResponse = webRequest.GetResponse();

if (webResponse != null)
{
StreamReader sr = new StreamReader(webResponse.GetResponseStream());
response = sr.ReadToEnd().Trim();
}
}
catch (Exception ex)
{
// Handle the request
}


However, if the web service throws a SOAP fault when we try to get the response, the stack trace of the exception will not return the SOAP fault that you would expect. Instead we just a fairly generic error message and an HTTP 500 Internal Server error.



What's going on? If we use a HTTP sniffer to see the actual raw response we get some clues.
 
HTTP/1.1 500 Server Error
Server: Sun-ONE-Web-Server/6.1
Date: Wed, 15 Apr 2009 00:42:33 GMT
Content-length: 978
Content-type: text/xml; charset=utf-8
X-Powered-By: Servlet/2.4 JSP/2.0
X-Charles-Received-Continue: HTTP/1.1 100 Continue

 
soapenv:ServerSchema validation failed for request.5
Schema validation failed for request.
string value 'MVP_CUSTOMER' is not a valid enumeration value for CustomerType in namespace
http://acme.com/ws/customerorder/v1
MVP_CUSTOMER





It's apparent that the web server interprets a SOAP fault as an HTTP 500 error so it has nothing to do with HttpWebResponse class doing something invalid. When we sniffed the HTTP traffic, we could see the detailed SOAP fault information so why can't see it when we catch an exception?

We need to catch a WebException and then cast the WebException.Response into an HttpWebResponse object. Once we have a valid HttpWebResponse object, we can read the response stream and get the SOAP fault information out. We can then extract the information and turn it into a useful error messages for the logfile or the users.

 
try
{

WebResponse webResponse = webRequest.GetResponse();

if (webResponse != null)
{
StreamReader sr = new StreamReader(webResponse.GetResponseStream());
response = sr.ReadToEnd().Trim();
}
}

catch (WebException wex)
{
// We issued an HttpWebRequest, so the response will be an
// HttpWebResponse.
HttpWebResponse httpResponse = wex.Response as HttpWebResponse;

// Grab the response stream.
using (Stream responseStream = httpResponse.GetResponseStream())
{
using (StreamReader reader = new StreamReader(responseStream))
{
// Grab the body of the response as a string for parsing.
string failureReason = reader.ReadToEnd();

// If it failed with a SOAP fault, parse the response body into a usable exception.
ThrowIfSoapFault(failureReason);

// If it was not a SOAP fault, rethrow the original exception.
throw;
}
}

}



Most of the code shown in this example can be found at MSDN.