I am implementing a MVC3/Razor web application that retrieves some of its \"fields\" a user can edit from other services so the list of properties to edit in a view is completel
My first ASP MVC task involved building a dynamic form, and it isn't straight forward.
Basically you can't use the built in helpers or validation because they expect strongly typed objects.
My view basically loops through the input fields, checks the DataType (bool, int, string, datetime etc) and builds an editor directly for that type.
You also have to do all your validation manually, decorating the types with attributes to do this doesn't work, see my question if(ModelState.IsValid) doesn't work with FormsCollection. What to use instead?
Razor View logic (we use DevExpress MVC extensions, but you get the drift)
The form object and it's Fields collection are our bespoke objects that describe how the form should look (the page gathers criteria for searches, which is why you'll see criteria and search type names in the code).
<table id="criteriaTable">
@foreach (var field in form.Fields)
{
<tr id="criteriaTableRow">
@if (field.IsVisible)
{
<td id="criteriaTableLabelCol">
@Html.DevExpress().Label(s => s.Text = field.Caption).GetHtml()
</td>
<td id="criteriaTableEditCol">
@if (field.Type == typeof(bool))
{
@Html.CheckBox(s =>
{
s.Checked = field.IsBoolSet;
s.Name = field.Name;
s.ClientEnabled = !field.IsReadonly;
}).GetHtml()
}
else if (field.Type == typeof(DateTime))
{
Html.DevExpress().DateEdit(s =>
{
s.Name = field.Name;
s.ClientEnabled = !field.IsReadonly;
if (!string.IsNullOrEmpty(field.Value))
{
DateTime dateValue;
if (DateTime.TryParse(field.Value, out dateValue))
s.Date = dateValue;
}
}).GetHtml();
}
else if (field.ListValues.Count > 0)
{
<input type="hidden" id="@("initiallySelected" + field.Name)" value="@field.Value" />
Html.DevExpress().ListBox(s =>
{
s.Name = field.Name;
s.ClientVisible = field.IsVisible;
s.ClientEnabled = !field.IsReadonly;
s.Properties.SelectionMode = DevExpress.Web.ASPxEditors.ListEditSelectionMode.CheckColumn;
s.Properties.TextField = "Name";
s.Properties.ValueField = "Value";
s.Properties.ValueType = typeof(string);
//s.Properties.EnableClientSideAPI = true;
foreach (var item in field.ListValues)
{
s.Properties.Items.Add(item.Name, item.Value);
}
//s.Properties.ClientSideEvents.SelectedIndexChanged = "MultiSelectListChanged";
s.Properties.ClientSideEvents.Init = "MultiSelectListInit";
}).GetHtml();
}
else
{
//Html.TextBox(field.Name, field.Value)
Html.DevExpress().TextBox(s =>
{
s.Name = field.Name; s.Text = field.Value;
}).GetHtml();
}
@Html.ValidationMessage(field.Name)
<input type="hidden" name="@("oldvalue_" + field.Name)" value="@field.Value" />
<input type="hidden" name="@("olduse_" + field.Name)" value="@(field.IncludeInSearch ? "C" : "U")" />
</td>
<td id="criteriaTableIncludeCol">
@Html.DevExpress().CheckBox(s =>
{
s.Checked = field.IncludeInSearch;
s.Name = "use_" + field.Name;
s.ClientEnabled = (!field.IsMandatory);
}).GetHtml()
</td>
}
</tr>
}
</table>
The Controller action accepts a Forms Collection. I loop through the formsCollection looking for the control names I specified in the view.
[HttpPost]
public ActionResult QueryCriteria(FormCollection formCollection)
{
var isValid = true;
foreach (var field in form.Fields)
{
var value = (formCollection[field.Name] ?? "").Trim();
...
If there are any validation errors I can specify control level validation by adding a ModelError directly to the model e.g.
ModelState.AddModelError(field.Name, "This is a mandatory field");
and I return the View if there are validation errors.
Hope this helps.