Recently, I was working with a fairly complex data model that required a simple input form. Being the lazy programmer I am, I did not feel like crunching out the HTML required to generate this form by hand. So instead, I wrote an HTML helper class to generate the form for me.
Model
Here is the data model we are going to be working with in this example.
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public Address Address{ get; set; }
public Gender Gender { get; set; }
}
public class Address
{
public string Address1 { get; set; }
public string Address2 { get; set; }
public string City { get; set; }
public string State { get; set; }
}
public enum Gender
{
Male,
Female
}
Goal
The goal here is to have a View that needs only call this helper to generate the textboxes and dropdown lists used for entering data. This way we avoid manually typing all of those angle brackets. So our View will look something like…
<h2>Dynamic Form</h2>
<%= Html.ValidationSummary("Create was unsuccessful. Please correct the errors and try again.") %>
<% using (Html.BeginForm()) {%>
<%= Html.DynamicInputs(Model, "Enter Person Information", string.Empty) %>
<p>
<input type="submit" value="Submit" />
</p>
<% } %>
Dynamic Input Helper
The DynamicInputHelper will enumerate the data model properties and output forms elements responsible for receiving input from the user based on the data type. This is pretty simple.
public static class DynamicFormHelper
{
public static string DynamicInputs<T>(this HtmlHelper helper, T obj, string heading, string prefix)
where T : class
{
// Contruct id for model binding
if (!string.IsNullOrEmpty(prefix))
prefix += ".";
// Make sure we have an instance of the obj
if (obj == null)
obj = (T)Activator.CreateInstance(typeof(T));
var type = obj.GetType();
var sb = new StringBuilder().Append("<fieldset>");
sb.AppendFormat("<legend>{0}</legend>", heading);
foreach (var propertyInfo in type.GetProperties())
{
if ((propertyInfo.PropertyType.IsValueType && !propertyInfo.PropertyType.IsEnum) || propertyInfo.PropertyType.FullName == typeof(string).FullName)
{
sb.AppendFormat(
"<p><label for=\"{0}\">{1}:</label>" +
"<input type=\"text\" id=\"{0}\" name=\"{0}\" value=\"{2}\"></p>",
prefix + propertyInfo.Name,
propertyInfo.Name.SpaceCamelCase(),
propertyInfo.GetValue(obj, null));
}
else if (propertyInfo.PropertyType.IsEnum)
{
sb.AppendFormat("<p><label for=\"{0}\">{0}:</label>", propertyInfo.PropertyType.Name.SpaceCamelCase());
sb.Append(
helper.SelectFromEnum(propertyInfo.PropertyType,
prefix + propertyInfo.PropertyType.Name.SpaceCamelCase()));
sb.Append("</p>");
}
else
{
var child = Convert.ChangeType(propertyInfo.GetValue(obj, null) ??
Activator.CreateInstance(propertyInfo.PropertyType), propertyInfo.PropertyType);
sb.AppendFormat(
helper.DynamicInputs(child, propertyInfo.Name, prefix + propertyInfo.Name)
);
}
}
sb.Append("</fieldset>");
return sb.ToString();
}
public static string SelectFromEnum(this HtmlHelper helper, Type type, string name)
{
var sb = new StringBuilder();
sb.AppendFormat("<select id=\"{0}\" name=\"{0}\">", name);
foreach (var e in Enum.GetNames(type))
{
sb.AppendFormat("<option value=\"{0}\">{1}</option>", (int) Enum.Parse(type, e, true), e.SpaceCamelCase());
}
sb.Append("</select>");
return sb.ToString();
}
public static string SpaceCamelCase(this string camelCase)
{
if (camelCase == null)
throw new ArgumentException("Null is not allowed for StringUtils.FromCamelCase");
var sb = new StringBuilder(camelCase.Length + 10);
bool first = true;
char lastChar = '\0';
foreach (char ch in camelCase)
{
if (!first &&
(char.IsUpper(ch) ||
char.IsDigit(ch) && !char.IsDigit(lastChar)))
sb.Append(' ');
sb.Append(ch);
first = false;
lastChar = ch;
}
return sb.ToString(); ;
}
}
Finished Product
If all goes according to plan you should end up with a finished product that looks something like this. The coolest part about this form is that it is rendered so the Model Binder will know how to construct the object on the server when the request is received.

Update 9/30/2009: As pointed out in the comments below, check out Input Builders MvcContrib for a more complete and robust solution for your form generation needs.