Domanda

I have a list of questions that contains a list of options. They questions have a type field that allows the question to be a radio button or a checklist etc...

I am trying to create the view in such a way that I will get back the questions and the options. Right now I don't care about the responses.

As best I can tell I am following the rules for ASP.NET out of the box model binding for Lists which were blogged about in the following two places.

Scott Hanselman's blog: http://bit.ly/1fYAWCs
Phil Haack's blog: http://bit.ly/1fYBokd

Is it possible to do what I am trying to do with the out of the box model binder, or will I need to implement my own custom model binder?

If I do need to implement a custom model binder, can you point me to some straightforward examples or blog posts on how to go about that?

Ultimately I want to use the list of questions and the list of responses to write a custom attribute in data annotations to validate the model (by making sure every question has at least one answer), while also using unobtrusive validation on the client.

If there are better solutions I'm open to ideas.

Below is a contrived example to demonstrate my problem.

ViewModel:

public class Questionnaire
{
    private List<QuestionResponse> _questions;

    [UIHint("QuestionResponse")]
    public List<QuestionResponse> Questions
    {
        get { return _questions ?? (_questions = new List<QuestionResponse>()); }
        set { _questions = value; }
    }

    public List<int> Responses;
}

Model:

public class QuestionResponse
{
    public int QuestionId;

    public string QuestionType;

    public string QuestionCode;

    public string QuestionName;

    public string OpenResponse;

    public List<QuestionOptions> Options;
}

public class QuestionOptions
{
    public int OptionId;

    public string OptionType;

    public string OptionName;

    public string OptionDescription;

    public string OptionResponse;

    public bool Selected;

}

Controller Methods:

    [HttpGet]
    public ActionResult Questionnaire()
    {
        #region HardCodedDataz
        var questions = new List<QuestionResponse>
        {
            new QuestionResponse
            {
                QuestionId = 1,
                OpenResponse = null,
                QuestionCode = "1",
                QuestionName = "What kind of movies do you like?",
                QuestionType = "Checklist",
                Options = new List<QuestionOptions>
                {
                    new QuestionOptions
                    {
                        OptionDescription = "Horror",
                        OptionId = 1,
                        OptionName = "Horror",
                        OptionResponse = null,
                        OptionType = "boolean",
                        Selected = false
                    },
                    new QuestionOptions()
                    {
                        OptionDescription = "Family",
                        OptionId = 2,
                        OptionName = "Family",
                        OptionResponse = null,
                        OptionType = "boolean",
                        Selected = false
                    },
                    new QuestionOptions()
                    {
                        OptionDescription = "Comedy",
                        OptionId = 3,
                        OptionName = "Comedy",
                        OptionResponse = null,
                        OptionType = "boolean",
                        Selected = false
                    },
                    new QuestionOptions()
                    {
                        OptionDescription = "Adventure",
                        OptionId = 4,
                        OptionName = "Adventure",
                        OptionResponse = null,
                        OptionType = "boolean",
                        Selected = false
                    },
                    new QuestionOptions()
                    {
                        OptionDescription = "Drama",
                        OptionId = 5,
                        OptionName = "Drama",
                        OptionResponse = null,
                        OptionType = "boolean",
                        Selected = false
                    }
                }
            },
            new QuestionResponse()
            {
                QuestionId = 2,
                OpenResponse = null,
                QuestionCode = "2",
                QuestionName = "Which university did you attend in your first year of college?", 
                QuestionType = "RadioButton",
                Options = new List<QuestionOptions>
                {
                    new QuestionOptions()
                    {
                        OptionDescription = "Cornell",
                        OptionId = 1,
                        OptionName = "Cornell",
                        OptionResponse = null,
                        OptionType = "boolean",
                        Selected = false 
                    },
                    new QuestionOptions()
                    {
                        OptionDescription = "Columbia",
                        OptionId = 2,
                        OptionName = "Columbia",
                        OptionResponse = null,
                        OptionType = "boolean",
                        Selected = false 
                    },
                    new QuestionOptions()
                    {
                        OptionDescription = "Harvard",
                        OptionId = 3,
                        OptionName = "Harvard",
                        OptionResponse = null,
                        OptionType = "boolean",
                        Selected = false 
                    },
                    new QuestionOptions()
                    {
                        OptionDescription = "Yale",
                        OptionId = 4,
                        OptionName = "Yale",
                        OptionResponse = null,
                        OptionType = "boolean",
                        Selected = false 
                    },
                    new QuestionOptions()
                    {
                        OptionDescription = "Princeton",
                        OptionId = 5,
                        OptionName = "Princeton",
                        OptionResponse = null,
                        OptionType = "boolean",
                        Selected = false 
                    }
                }
            }
        };
        #endregion

        var model = new Questionnaire()
        {
            Questions = questions,
            Responses = new List<int>()
        };

        return PartialView(model);
    }

    [HttpPost]
    public JsonResult Questionnaire(List<QuestionResponse> questions, List<int> responses)
    {
        return Json(new
        {
            success = "some success message",
            error = "some error message"
        });
    }

View:

@using TestApplication.Models
@model  Questionnaire

@using (Html.BeginForm("Questionnaire", "Home", FormMethod.Post))
{
    <div>
        @Html.EditorFor(m => m.Questions)
    </div>
    <div>
        <button type="submit">Save</button>
    </div>
}

Custom Editor Template:
Note: the hidden fields are solely for the sake of populating the model on the post request.

@using System.Web.Mvc.Html
@using TestApplication.Models
@model  List<QuestionResponse>
@if (Model != null)
{
    for (int i = 0; i < Model.Count; i++)
    {
        <p><strong>@Model[i].QuestionName</strong></p>
        @Html.HiddenFor(m => m[i].QuestionName);
        @Html.HiddenFor(m => m[i].QuestionType)
        @Html.HiddenFor(m => m[i].QuestionCode)
        @Html.HiddenFor(m => m[i].QuestionId)

        if (Model[i].QuestionType == "Checklist")
        {
             for (int j = 0; j < Model[i].Options.Count; j++)
             {   
                 <div>
                     <label>@Model[i].Options[j].OptionDescription</label>
                     @Html.CheckBoxFor(m => m[i].Options[j].Selected)
                     @Html.HiddenFor(m => m[i].Options[j].OptionDescription)
                     @Html.HiddenFor(m => m[i].Options[j].OptionName)
                     @Html.HiddenFor(m => m[i].Options[j].OptionId)
                     @Html.HiddenFor(m => m[i].Options[j].OptionType)
                 </div>
             }
        }
        else if (Model[i].QuestionType == "RadioButton")
        {
             for (int j = 0; j < Model[i].Options.Count; j++)
             {
                 var name = "Questions[" + i + "].Options[" + j + "].Selected";
                 var id = Model[i].Options[j].OptionId;
                 <div>
                     <label>@Model[i].Options[j].OptionDescription</label>
                     <input id="@id" type="radio" name="@name" value="" />
                     @Html.HiddenFor(m => m[i].Options[j].OptionDescription)
                     @Html.HiddenFor(m => m[i].Options[j].OptionName)
                     @Html.HiddenFor(m => m[i].Options[j].OptionId)
                     @Html.HiddenFor(m => m[i].Options[j].OptionType)
                 </div>
             }
        }
    }
}
È stato utile?

Soluzione

After looking into the asp.net source code I discovered that the framework expects your model to be composed of properties and that simple fields won't work.

Below is the BindProperties Method from the source code. If your complex model has no properties, then the properties collection will be empty and the for loop in which the binding occurs will exit.

    private void BindProperties(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        PropertyDescriptorCollection properties = GetModelProperties(controllerContext, bindingContext);
        Predicate<string> propertyFilter = bindingContext.PropertyFilter;

        // Loop is a performance sensitive codepath so avoid using enumerators.
        for (int i = 0; i < properties.Count; i++)
        {
            PropertyDescriptor property = properties[i];
            if (ShouldUpdateProperty(property, propertyFilter))
            {
                BindProperty(controllerContext, bindingContext, property);
            }
        }
    }

If you absolutely must use simple fields then a custom model binder would be appropriate.

Similarly if you try to use DataAnnotations on public fields then you will run into this issue DataAnnotations on public fields vs properties in MVC

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top