0

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).

enter image description here

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>
}
Community
  • 1
  • 1
Bartosz
  • 4,406
  • 7
  • 41
  • 80
  • I'm not sure if this will fix it, but you can try adding a contentType to your ajax call. – tcrite Feb 23 '16 at 17:45
  • Like this: contentType: 'application/json' – tcrite Feb 23 '16 at 17:46
  • 1
    You have multiple errors First razor code is parsed on the server befote its sent to the view so `var modelDataJson = '@Html.Raw(Json.Encode(Model))';` is just assigning the original model to the variable which is rather pointless. And if you were to post it back then it needs to be `data: modelDataJson,` and the controller changed to `public ActionResult AddNewElement(ConfigModel model)` but you not even using the `EditorTemplate` correctly - in the main view it should be just `@Html.EditorFor(m => Model.SubElements)` (no loop). –  Feb 24 '16 at 02:08
  • @StephenMuecke, thanks for you comment! I have used the editor template in a loop to control spacing between the elements of the form, does it affect anything if it's done like that? Also, would changing postback and controller help in any way? The model is correctly posted and deserialized in the component, and my goal is to add a new one exactly like the previous one (copy the node, basically). Thanks again! – Bartosz Feb 24 '16 at 08:01
  • You should not be using `EditorFor()` in a loop (its designed to take `IEnumerabe` = just add the `

    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.
    –  Feb 24 '16 at 11:16
  • 1
    And it seems you want an instance of `PluginConfigElement` in the controller which you will wont get because the model you posting back is `ConfigModel`. Its unclear what your expecting to to post And FGS, remove `id="@string.Format("addNewElement" + Model.GetHashCode())"` and replace it with `class="addNewElement"` and change the script to `$('.addNewElement').click(function() {` –  Feb 24 '16 at 11:22
  • @StephenMuecke OK, can you tell me how could I achieve what I want to do? The model that I post (currently as model, not string anymore) is a subelement that is supposed to be simply duplicated and added to it's parent... – Bartosz Feb 24 '16 at 11:24
  • 1
    Its not clear what your trying to do. But if your just wanting to dynamically add new elements in the view, then look at the answers [here](http://stackoverflow.com/questions/29161481/post-a-form-array-without-successful/29161796#29161796) and [here](http://stackoverflow.com/questions/28019793/submit-same-partial-view-called-multiple-times-data-to-controller/28081308#28081308) There is really no need to be making a call to the controller at all unless your wanting to save something in the method –  Feb 24 '16 at 11:27
  • @StephenMuecke - yes, I want to dynamically add new subelements in the view - however, when I submit the form, I want these new elements to be included in the model... I will read your links, thanks! – Bartosz Feb 24 '16 at 11:39
  • Then the links will show you the correct way to do it. –  Feb 24 '16 at 11:40
  • @StephenMuecke - I have read you links, but I am now unsure where should I put this 'hidden template' div - would you please advise? Thanks! – Bartosz Feb 24 '16 at 12:48
  • Its late and I need some sleep, but will add some more info tomorrow. The 'template' just needs to be located outside the `form` (so it does not get posted back). And if your still struggling, edit your question to show the actual html generated by your view for one of the `EditorTemplate` and I'll have a look at what the 'tempate' should look like –  Feb 24 '16 at 12:56
  • @StephenMuecke - Thanks, have a good night!:) – Bartosz Feb 24 '16 at 13:07
  • Its still not clear what it is your wanting to dynamically generate. What for example do you want to happen when you click the 1st 'Add' button in your image? And what do you want to happen if your click the 2nd 'Add' button in your image? –  Feb 25 '16 at 04:33
  • @StephenMuecke - thanks for following up. Image was not clear, I am replacing it with a better one, showing the dependencies of each element. Basically, it is a tree structure, as this is supposed to be a reflection of an XML config file. From the bottom, pressing add on a Language node should add a new Language node under the same workflow stage. Pressing Add on a workflow stage should add a new workflow stage with all the child language pairs under the same config node. It should basically create a copy of the element with all children and add it under the the parent. – Bartosz Feb 25 '16 at 08:48
  • @StephenMuecke This is why it appears pretty difficult - I tried it yesterday a couple of times with BeginCollectionItem etc, but I only managed to get some unexpected results, like flattened structure when elements were added, or elements added in incorrect parents - because I couldnt get the bindings to work properly... – Bartosz Feb 25 '16 at 08:52
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/104599/discussion-between-stephen-muecke-and-bartosz). –  Feb 26 '16 at 03:48

0 Answers0