I will try to provide you with some hints that might give you some ideas.
As always let's start by defining a view model which will represent the UI:
public class MyViewModel
{
public ControlViewModel[] Controls { get; set; }
}
public abstract class ControlViewModel
{
public abstract string Type { get; }
public bool Visible { get; set; }
public string Label { get; set; }
public string Name { get; set; }
}
public class TextBoxViewModel : ControlViewModel
{
public override string Type
{
get { return "textbox"; }
}
public string Value { get; set; }
}
public class CheckBoxViewModel : ControlViewModel
{
public override string Type
{
get { return "checkbox"; }
}
public bool Value { get; set; }
}
public class DropDownListViewModel : TextBoxViewModel
{
public override string Type
{
get { return "ddl"; }
}
public SelectList Values { get; set; }
}
So we have defined some of the basic controls we would like to handle in our application. The next step would be to have a repository method which will query the database, fetch the XML and then a mapping layer that will finally provide us with an instance of our view model. I am leaving this as out of scope for this answer as there are many ways you could implement it (XmlSerializer, XDocument, XmlReader, ...).
I suppose that you already have an instance of the view model. Like this:
public ActionResult Index()
{
var model = new MyViewModel
{
Controls = new ControlViewModel[]
{
new TextBoxViewModel
{
Visible = true,
Label = "label 1",
Name = "TextBox1",
Value = "value of textbox"
},
new CheckBoxViewModel
{
Visible = true,
Label = "check label",
Name = "CheckBox1",
Value = true
},
new DropDownListViewModel
{
Visible = true,
Label = "drop label",
Name = "DropDown1",
Values = new SelectList(
new[]
{
new { Value = "1", Text = "text 1" },
new { Value = "2", Text = "text 2" },
new { Value = "3", Text = "text 3" },
}, "Value", "Text", "2"
)
}
}
};
return View(model);
}
So I have hardcoded some values in order to illustrate the concept, but normally this controller action would look something like this once you implement the repository and mapping layer:
public ActionResult Index()
{
string xml = _repository.GetControls();
var model = Mapper.Map<string, MyViewModel>(xml);
return View(model);
}
OK, now let's move to the corresponding Index.cshtml
view which will contain the form:
@model MyViewModel
@using (Html.BeginForm())
{
for (int i = 0; i < Model.Controls.Length; i++)
{
if (Model.Controls[i].Visible)
{
<div>
@Html.HiddenFor(x => x.Controls[i].Type)
@Html.HiddenFor(x => x.Controls[i].Name)
@Html.EditorFor(x => x.Controls[i])
</div>
}
}
<input type="submit" value="OK" />
}
OK, so now we can define the corresponding editor templates for the controls we would like to handle:
~/Views/Shared/EditorTemplates/TextBoxViewModel.cshtml
@model AppName.Models.TextBoxViewModel
@Html.LabelFor(x => x.Value, Model.Label)
@Html.TextBoxFor(x => x.Value)
~/Views/Shared/EditorTemplates/CheckBoxViewModel.cshtml
@model AppName.Models.CheckBoxViewModel
@Html.CheckBoxFor(x => x.Value)
@Html.LabelFor(x => x.Value, Model.Label)
~/Views/Shared/EditorTemplates/DropDownListViewModel.cshtml
@model AppName.Models.DropDownListViewModel
@Html.LabelFor(x => x.Value, Model.Label)
@Html.DropDownListFor(x => x.Value, Model.Values)
So far, so good. At this stage you should be able to render a form containing the dynamic controls. But of course such a form is pretty useless to anyone. What would be nice is to have the possibility of POSTing this form and capturing the values entered by the user in a controller action so that we could process them.
The controller action would look like this:
[HttpPost]
public ActionResult Index(MyViewModel model)
{
... process the values
}
Now that would be nice but of course it won't work because the ControlViewModel
view model is an abstract class and the default model binder has no clue about which specific implementation to instantiate. So we need to help him => by writing a custom model binder:
public class ControlModelBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
var type = bindingContext.ValueProvider.GetValue(bindingContext.ModelName + ".Type");
object model = null;
switch (type.AttemptedValue)
{
case "textbox":
{
model = new TextBoxViewModel();
break;
}
case "checkbox":
{
model = new CheckBoxViewModel();
break;
}
case "ddl":
{
model = new DropDownListViewModel();
break;
}
default:
{
throw new NotImplementedException();
}
};
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType());
return model;
}
}
which will be registered in Application_Start
and associated to the ControlViewModel
type):
ModelBinders.Binders.Add(typeof(ControlViewModel), new ControlModelBinder());