Asp.net MVC is really a great platform to build performant and very controllable web applications. But sometimes some functionality is scarcely documented if at all. Internet proves (thank you Google) to be an invaluable wealth of information in this regard since other developers are usually solving similar problems. But sometimes you either don't find usable information or you want to test things for yourself as well and learn something new as you go along.
Something similar happened to me on my previous project. I had to dynamically generate a list of objects in the browser and then reliably POST them back to web server so that data would be reliably model bound and validated as well. I wanted to avoid manual validation at all costs because it would unnecessarily overcomplicate server code.
The problem in detail
I had to develop functionality for sending messages to other users. So I've written a HttpPost
controller action that looked similar to this:
public ActionResult SendPrivateMessage(Message message) { ... }
At first this looked pretty straight forward if you think of an object of type Message
. It would most probably have a title and a message body. But thinking further this class would also need a list of recipients. So I've written a view model class similar to this:
1: public class Message
2: {
3: /// <summary>
4: /// Message identifier. Only set when fetching messages.
5: /// </summary>
6: public int? ID { get; set; }
7:
8: /// <summary>
9: /// Message title.
10: /// </summary>
11: [Required]
12: public string Title { get; set; }
13:
14: /// <summary>
15: /// Message body.
16: /// </summary>
17: [Required]
18: public string Body { get; set; }
19:
20: /// <summary>
21: /// A list of message recipients.
22: /// </summary>
23: [Required]
24: public List<Recipient> Recipients { get; }
25: }
As we can see in this class there's a list of recipients of type List<Recipient>
.
Recipient
class looks like this:
1: public class Recipient
2: {
3: /// <summary>
4: /// Recipient identifier.
5: /// </summary>
6: [Required]
7: public int ID { get; set; }
8:
9: /// <summary>
10: /// Recipient's public display name.
11: /// </summary>
12: [Required]
13: public string PublicName { get; set; }
14: }
The main objective
The main question here is: How do I prepare a client side <form>
that would have an arbitrary number of fields with recipients so that my controller action would be able to model bind it to my Message
class.
Client side data preparation
As much as it looks complicated at first it turns out to be pretty straight forward when you make it work. The non trivial part here are of course recipients, because I have to generate these on the client side and name the input fields correctly so default model binder will be able to bind them to my action parameter.
Ok so based on upper classes field naming should look exactly like this:
1: <!-- These two are trivial -->
2: <input type="text" name="message.Title" value="" />
3: <textarea name="message.Body"></textarea>
4:
5: <!-- And here are recipients -->
6: <input type="hidden" name="message.Recipients[0].PublicName" value="John Doe" />
7: <input type="hidden" name="message.Recipients[0].Id" value="1" />
8: <input type="hidden" name="message.Recipients[1].PublicName" value="Jane Doe" />
9: <input type="hidden" name="message.Recipients[1].Id" value="2" />
10: ...
All I had to guarantee is that:
- every recipient has two input elements (of any type; could be text boxes or hidden fields) with names that correlate to class property names
- recipients list index must start with index 0
- recipients list index mustn't have any gaps
Recipients (input fields) were added/deleted using Javascript. Recipients index number re-indexing was of course done the same way so the second and third requirements were met when a user added/deleted a recipient.
Since I created this functionality to be as usable as possible, a user was able to:
- input few characters into a textbox and an Ajax call returned a
List<Recipient>
JSON formatted data I could consume and display a list of possible recipients to the user - select a recipient from a displayed list which added it to the list of selected recipients - at this point new input fields were added and re-indexing was executed
- delete an already added recipient from the list of selected recipients - at his point index gap should be removed (when user didn't delete last entered recipient)
All's well that ends well
Doing my client side form this way I was able to keep my controller action signature as shown at the beginning of this post. My posted data was automatically validated by the framework as well. So my server code didn't have to become more complex due to this at all. It's a win-win situation.
can you post the code sample please
ReplyDelete@J@F: Which part of code makes you most problems? My solution is very problem specific and most likely wouldn't be too helpful for you. The outlined functionality is completely relevant. But if you can point out the main problem I may be able to help you better...
ReplyDeleteHi Robert,
ReplyDeleteThe example your showed above is very close to what I need to implement:
Survey Form
Queqsion 1 Which color you like more ?
RadioButton * Blue
RadioButton * Red
RadioButton * Green
Queqsion 2 Check, check, how many days month can have?
CheckBox 28
CheckBox 11
CheckBox 31
CheckBox 18
Question 3 What's your favorite song?
TextBox: ……..(any text)
< Previous Page (button) Next Page> (button)
I need to built questions (number is know only in runtime), with
diffident type of answers (controls) (it is also known only in runtime),
and save answers, loading next (page) batch of Questions
But I can't get how to build and show complex type of controls like RadioButtonList on the page (using MVC 3 and preferably Razor view engine)
I would be very grateful, if you cold show me direction (example) - how to solve this problem.
Thanks,
Aleks
How I would go about this problem...
ReplyDelete1. Common parent class QuestionBase
2. As many partial views as there are question types each responsible of displaying that particular question type form.
3. Parent view would be of model type IList<QuestionBase> or IEnumerable<QuestionBase> and would iterate through all questions and RenderPartial for each based on their actual type.
4. Every partial question would have a reference to a particular javascript function that would be able to collect question data into a javascript object.
5. On submit there would be a javascript function that would call individual question functions and generate an array of questions.
6. Use my other plugin toDictionary to convert this array of objects (plugin link here).
Additional directions
1. Stay away from Asp.net web forms server controls (like RadioButtonList) in Asp.net MVC. It will make your life much easier. You should either decide to write your application purely in MVC or purely in WebForms. Don't mix and match.
2. It would be a bit more complicated to model bind to concrete QuestionBase inherited objects but using a separate POST object classes could do the trick. Using additional action filters or model binders is an additional step to use to automate this even further.
Hope this helps.
Thanks a lot Robert,
DeleteYou gave a more detailed explanation than I expected.
That web page is already implemented using ASP.NET web form, but intention of moving towards MVC is based on new upcoming features which MVC 4 promises to support of mobile dev.
I need to show same page on mobile devices as well.
But now I can see that in this particular case web forms give me much less problems than MVC for solving this task (conducting a survey).
The only thing which needs to be done is manage with showing survey on mobile browsers (I mean it should take into account screen resolution, magnification of controls if needed..etc)
Could you suggest any approach/.net solution for mobile devices?
Thanks,
Aleks
I haven't done anything in this regard, but searching the internet for display modes webforms mobile should give you some relevant results.
DeleteThat's the best advice I can give you at the moment in this regard.
Hello Robert, for my application, I need to implement something very similar to your use case using MVC and jQuery. Can you please provide me the complete code that you wrote? In case you can't have the code here, you can mail me the code at yasir.m.syed at gmail.com.
ReplyDeleteHi Yasir. Can you be a bit more specific what are the problems that you're facing? It would be easier for me to provide some more direct and useful code.
DeleteHi Robert,
ReplyDeleteHow did you deal with validation errors?
Let's say you have business validation happening in your controller, something that would go:
"user x is not allowed to send a message to recipient y".
Then what you may want to display is a validation message next to the wrong recipient. The problem is, the default mvc mechanism for retrieving attempted values would not work because the recipient list is dynamically created on the client side. How did you solve this?
Thanks, nice post by the way