I am working on an MVC page that is supposed to present a form based on XML file.
The XML is a serialized class object of type 'settings' which can contain arrays of booleans, strings and integers - and an array of zero or more identical sub elements of the same class (recursive nesting of elements to reflect the flexible structure of XML).
This is supposed to be a generic solution for some configuration XML files.
The MVC page is supposed to display the form, allow to edit it and save the settings - serialize the class back to the config XML file. This works OK, i.e. I can edit data and save it.
It is also supposed to allow to dynamically add elements (apologies, layout is a very initial version, it does not show nesting of the elements Root=>Config(s)=>Stage(s)=>Language(s).
Now, adding the items to the form works OK from the visual point of view, however when the form is posted, the SubElements arrays do not contain the new items.
Let me show you some code now:
public class ConfigModel : IPluginConfiguration
{
public ConfigModel()
{
}
public StringPluginSetting[] StringSettings { get; set; }
public BooleanPluginSetting[] BooleanSettings { get; set; }
public IntegerPluginSetting[] IntegerSettings { get; set; }
public PickListPluginSetting[] PickListSettings { get; set; }
public PluginConfigElement[] SubElements { get; set; }
public int PluginFrequencyMinutes { get; set; }
public bool IsDeactivated { get; set; }
public bool IsTemplate { get; set; }
}
Main view
@model TestAppForConfigHelpers.Models.ConfigModel
@if (Model != null)
{
using (Html.BeginForm())
{
@Html.Partial("_GlobalSettings")
<br/>
<br/>
for (int i = 0; i < Model.SubElements.Length; i++)
{
<br/>
<p>Config @i</p>
<br/>
@Html.EditorFor(m => Model.SubElements[i])
}
<br/>
<input type="submit" value="Save configuration"/>
}
}
Editor template:
@model ConfigHelperTest.ConfigurationHelper.PluginConfigElement
<div>
<table id="parentTable_@Model.GetHashCode()" class="table-bordered" style="margin-left: @string.Format(HomeController.Margin+"px")">
<tr class="label-primary">
<td colspan="99"><input type="button" class="btn-xs" value="Add" id="@string.Format("addNewElement" + Model.GetHashCode())" />@Model.ElementType</td>
</tr>
<tr>
<td>
@Html.HiddenFor(model => model.ElementType)
@Html.EditorFor(model => model.StringSettings)
</td>
</tr>
</table>
@if (Model.SubElements != null)
{
for (int i = 0; i < Model.SubElements.Length; i++)
{
@Html.EditorFor(m => Model.SubElements[i])
}
}
</div>
Javascript that adds elements:
<script type="text/javascript">
$(document).ready(function() {
$('#addNewElement'+@Model.GetHashCode()).on('click', function() {
var modelDataJson = '@Html.Raw(Json.Encode(Model))';
$.ajax({
url: '@(Url.Action("AddNewElement", "Home"))',
type: 'post',
data: { elementInJson: modelDataJson},
success: function(partialView) {
var id = 'parentTable_' + @Model.GetHashCode();
$('#'+id).parent().parent().append(partialView);
}
});
});
});
</script>
Controller action that is called by JavaScript
public ActionResult AddNewElement(string elementInJson)
{
PluginConfigElement element =
new JavaScriptSerializer().Deserialize<PluginConfigElement>(elementInJson);
//simply add a new one based on the source one, with all subelements etc
return PartialView("~/Views/Shared/EditorTemplates/PluginConfigElement.cshtml", element);
}
Any help appreciated!
Cheers
Updated code
Based on Stephen's suggestions, I have updated the code a bit - removed the controller action as not needed, stopped using loop for editor template and changed how javascript on click event is triggered.
Main view:
@if (Model != null)
{
using (Html.BeginForm())
{
<div id="formDiv">
@Html.Partial("_GlobalSettings")
<br/>
<br/>
@Html.EditorFor(m => Model.SubElements)
<br/>
<input type="submit" value="Save configuration"/>
</div>
}
}
Editor template + javascript:
@model ConfigHelperTest.ConfigurationHelper.PluginConfigElement
<div>
<table class="table-bordered" >
<tr class="label-primary">
<td colspan="99"><input type="button" class="btn-xs addNewElement" value="Add" onclick="addNewElement(this)" />@Model.ElementType</td>
</tr>
<tr>
<td>
@Html.HiddenFor(model => model.ElementType)
@Html.EditorFor(model => model.StringSettings) //custom editor template below
//@Html.EditorFor(model => model.BooleanSettings) not used now for simplicity
</td>
</tr>
</table>
@if (Model.SubElements != null)
{
@Html.EditorFor(m => Model.SubElements)
}
</div>
<script type="text/javascript">
function addNewElement(element) {
//todo
}
</script>
Now, I wanted to use the approach suggested in this answer, but I don't know where should I put the hidden template div - and more importantly, what should this hidden div contain, if I am already using an editor template for arrays of possible setting objects (strings, integers, booleans)...
<div id="NewSubElement" style="display: none">
</div>
The editor template for settings arrays
@model ConfigHelperTest.ConfigurationHelper.PluginSetting
@if (Model != null)
{
<tr>
<td><b>
@Html.HiddenFor(m => m.Name)
@Html.DisplayFor(m => m.Name)
</b>
</td>
<td>
@Html.EditorFor(m => m.Value)
@Html.HiddenFor(m => m.IsOptional)
</td>
</tr>
}
Config @i
` in the `EditorTemplate` But the real issue is that your posting back the original model, not any edits as I previously noted And have your parameter as a string and then deserializing it is pointless - let MVC work as it was designed to work.