20

In a current project the client asked for the possibility of answering a questionnaire in two ways: using a Wizard (one question at a time) and Listing (all questions at once) in a single form. Both ways are already implemented.

The questions are loaded from the database per Manual's chapter using AJAX (this is super fast). The biggest chapter at the moment has 230 questions (each with 4 HTML input fields - input/text, select, etc). If the user selects such Chapter to answer in the Listing format, the <form> will contain at about 920 fields to be posted to the server.

I'm doing an AJAX POST request passing the data with jQuery's serialize method:

data: $("#questions :input").serialize()

This serialization takes 207.143ms to complete. I got this value debugging with Firebug in Firefox:

console.profile();
$("#questions :input").serialize();
console.profileEnd();

Again this is super fast...

The problem comes when hydrating the data received on the following action method:

public async Task<ActionResult> ListSaveAsync(IEnumerable<AnswerViewModel> questions)

As you see, the posted data is data bound to an IEnumerable<AnswerViewModel> questions. AnswerViewModel has only 4 fields to store each answer.

The thing is that it takes a considerable amount of time (precisely 10 seconds) after clicking the Save button to hit a breakpoint on this action method, that is, those 10 seconds are being spent in the model binder presumably.

An important thing to mention is that I'm using Steve Sanderson's @Html.BeginCollectionItem helper to help when materializing the ViewModel collection properties from the HTTP POST. See how the data gets in the ViewModel (Keys):

enter image description here

Do you know what I can try to do to optimize this?

I thought about 4 workarounds:

  1. Save back only the modified questions. To do this I'd need to store each answer value in a data-attribute when loading the listing and compare it with the actual value when submitting the <form> as this guy suggests here.

  2. Create AnswerViewModel JavaScript objects on the client side and pass them to the action method. Would this alleviate the Model Binder?

  3. Roll my own model binder... but I really don't know if it would be faster than the default one that comes with ASP.NET MVC. From what I've read the default model binder does a lot of reflection to set the values/hydrate the action's model parameter and this could be the bottleneck.

  4. Use FormCollection and enumerate through the posted data getting each value by key and performing validation manually as shown here.

What else do you suggest?


Update 1

I went with option 3 and implemented a custom Model Binder: AnswerModelBinder : IModelBinder and used it in that specific action method:

public async Task<ActionResult> ListSaveAsync(
             [ModelBinder(typeof(AnswerModelBinder))]List<AnswerViewModel> questions)

Now what took 10 seconds to complete takes only 2 seconds.

  • Looks like the default model binder validation checks [ ModelState ] has a big impact on performance.

Update 2

I just experienced it once again: having a List<Guid> as an action parameter and passing only 59 strings through a $.getJson call was taking ~3 seconds to hit a breakpoint in the 1st line of the action method. Changing the parameter type to List<string> made the whole thingy work in the blink of an eye.

An interesting fact is that inside the action method I did this:

List<Guid> userIds = resources.Select(Guid.Parse).ToList();

and it transforms the resources List<string> to a List<Guid> instantaneously.

For sure there's something buggy with ASP.NET model binder. I just would like to know what it is... :)

Community
  • 1
  • 1
Leniel Maccaferri
  • 100,159
  • 46
  • 371
  • 480
  • 1
    Sorry for the slow reply, Leniel. How many questions are you sending to the server? The model binder uses a lot of code to try to be very generic about parsing input from the request. I think in your case the best way to solve was the path you took. – OdeToCode Feb 21 '13 at 21:44
  • @OdeToCode Thanks Scott for taking a look... Nice you agree with the custom Model Binder approach. In this specific case I'm sending 230 questions to the server. If I also do option #1, I think it'll be even faster... ;) Maybe 1s instead of 2s since JavaScript is pretty fast. Have to test this. – Leniel Maccaferri Feb 21 '13 at 22:53
  • @OdeToCode Just complemented the code and implemented workaround #1 I mention above => when loading the questions I put the each input/select current value in a data-attribute and when saving I compare the current value with that previous value in the data attribute. I then use jQuery's filter function to help with this. Now Saving is instantaneous. :-) jQuery code is impressively fast! – Leniel Maccaferri Feb 22 '13 at 04:58
  • @OdeToCode: see Update 2 in the question. :) – Leniel Maccaferri May 24 '13 at 08:13

3 Answers3

2

You can use ServiceStack JsonSerializer which is pretty fast in benchmark results here is documentation http://mono.servicestack.net/docs/text-serializers/json-serializer and here is the benchmarks http://mono.servicestack.net/benchmarks/

Raj
  • 90
  • 10
1

This may not be the answer you are looking for, but it may help. Instead of using the FormCollection, try having a controller method accept a model in the signature and use a Ajax.BeginForm(). This will remove the need for the serialization, and allow MVC to do it's work. Also, having a model with List of type Question may be worth looking into. This approach will also seemingly remove the need to iterate through the values on the post as they will already be in the model.

Anthony Mason
  • 165
  • 1
  • 12
0

I haven't tried this but when I use integer indices the binder had no problems in binding to the IEnumerable. Since you are not actually using these Guids I'd replace them with integers. (0,1,2...)

I guess you could do this easily on the page that renders the form or using JS.

cellik
  • 2,116
  • 2
  • 19
  • 29