MVC: How to use FormPanels and TabPanel to edit and bind a collection of items?

  1. #1

    MVC: How to use FormPanels and TabPanel to edit and bind a collection of items?

    Hi,

    How can I edit items, add items and submit a collection of items in a TabPanel?

    I have a collection of items which I want to edit, add new items and submit the changes to the controller. Each item has his own FormPanel
    in a Tab.

    I changed the the FormPanelFor example to test my needs
    http://mvc.ext.net/#/Models/FormPanelFor/

    Currently it is possible to edit the initial tabs and submit the initial collection items to the controller. It is also possible to add new Tabs with
    a FormPanel for new objects. But the objects of these tabs are not submited, and the FormPanel of these objects disappears if a further Tab is added.

    I have a notion what the reason is but I have no solution. I assume the reason is a missing unique identifier for the dynamically added FormPanel. Since the initial view is done by looping over an array the additional FormPanels are added in a other way there is no identifaction possible.

    Is it possible to pass a uniqueID to [CODE]Html.X().FormPanelFor[CODE] so that all objcts are submited? Or are there any better solutions to edit and bind a collection within ext.net mvc?


    Model: FormPanelForModel.cs
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using System.ComponentModel.DataAnnotations;
    
    namespace Ext.Net.MVC.Examples.Areas.Models.Models
    {
        public class FormPanelEmployee
        {
    
            private static int NEXT_ID = 1;
    
            [Field(FieldType=typeof(Ext.Net.Hidden))]
            public int ID { get; }
    
            public string Name { get; set; }
            public string Surname { get; set; }
    
            public DateTime DateOfBirth { get; set; }
    
            public bool IsActive { get; set; }
    
            [UIHint("Department")]
            public FormPanelDepartment Department { get; set; }
    
    
            public FormPanelEmployee()
            {
                ID = NEXT_ID++;
            }
    
            public static List<FormPanelEmployee> GetAll()
            {
                return new List<FormPanelEmployee>
                {
                   new FormPanelEmployee
                   {                   
                       Name = "Nancy",
                       Surname = "Davolio",
                       DateOfBirth = DateTime.Now,
                       IsActive = true,
                       Department = FormPanelDepartment.GetAll()[0]
                   },
                   new FormPanelEmployee
                   {                
                       Name = "Andrew",
                       Surname = "Fuller",
                       Department = FormPanelDepartment.GetAll()[2]
                   }
                };
            }
        }
    
        public class FormPanelDepartment
        {
            public int ID { get; set; }
            public string Name { get; set; }
    
            public static List<FormPanelDepartment> GetAll()
            {
                return new List<FormPanelDepartment>
                {
                    new FormPanelDepartment { ID = 1, Name = "Department A" },
                    new FormPanelDepartment { ID = 2, Name = "Department B" },
                    new FormPanelDepartment { ID = 3, Name = "Department C" }
                };
            }
        }
    }
    Controller: FormPanelForController.cs
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using System.Web.Mvc;
    using Ext.Net.MVC.Examples.Areas.Models.Models;
    
    namespace Ext.Net.MVC.Examples.Areas.Models.Controllers
    {
        public class FormPanelForController : Controller
        {
            public ActionResult Index()
            {
                return View(FormPanelEmployee.GetAll());
            }
    
            public ActionResult Submit(List<FormPanelEmployee> employee)
            {
                X.Msg.Alert("Employee", JSON.Serialize(employee)).Show();
                return this.Direct();
            }
    
            public ActionResult AddTab(string containerId)
            {
                ViewData.Model = new FormPanelEmployee();
                var result = new Ext.Net.MVC.PartialViewResult
                {
                    ViewData = this.ViewData,
                    ViewName = "Tab",
                    ContainerId = containerId,
                    RenderMode = RenderMode.AddTo
                };
    
                this.GetCmp<TabPanel>(containerId).SetLastTabAsActive();
                return result;
            }
        }
    }
    View: Index.cshtml

    @model List<Ext.Net.MVC.Examples.Areas.Models.Models.FormPanelEmployee>
    @{
        ViewBag.Title = "Index";
        Layout = "~/Views/Shared/_BaseLayout.cshtml";
    }
    @section example
    {
        <h1>FormPanel for Model</h1>
        <p>Automatically build a form panel for a model</p>
        <p>If you running that example in your project, please do not forget to put Customer.cshtml and Department.cshtml to the Views\Shared\EditorTemplates folder</p>
        @(
        Html.X().FormPanel()
                .BodyPadding(5)
                .DefaultAnchor("100%")
                .Width(800)
                .Items(Html.X().TabPanel()
                 .ID("TabPanel1")
                 .Width(860)
                 .Height(860)
                 .DeferredRender(false)
                 .Items(tabitems =>
                 {
                     for (int i = 0; i < Model.Count; i++)
                     {
                         tabitems.Add(Html.X().FormPanelFor(m => Model[i]).Title("Employee_"+ Model[i].ID));
                     }
                 }
                )
                .TabBar(Html.X().Button()
                 .Icon(Icon.Add)
                 .Flat(true)
                 .DirectEvents(de =>
                 {
                     de.Click.Url = Url.Action("AddTab");
                     de.Click.ExtraParams.Add(new { containerId = "TabPanel1" });
                 })
                 )
                 )
                 .Buttons(Html.X().Button()
                    .Text("Submit")
                    .DirectClickUrl(Url.Action("Submit"))
                )
        )
    }
    PartialView: Tab.cshtml

     
    @model Ext.Net.MVC.Examples.Areas.Models.Models.FormPanelEmployee
    
    @(Html.X().FormPanelFor(m => Model)
        .Title("Employee_" + Model.ID)
        .Closable(true)
        .BodyPadding(6)
    )
    Last edited by marjot2112; Apr 13, 2016 at 7:29 AM. Reason: Update Code
  2. #2
    Hello @marjot2112!

    I couldn't fully run your example on my side. It seems the AddTab() action you developed to create the tab is missing, and the AddFilterTab() defined on your controller does not do it by its current shape.

    So I am not able to exactly reproduce the ID conflict you are talking about. I have to me that the IDs are automatically generated with a custom unique identifier when you are using partial views and do not explicitly specify the ID of the component, so it is unlikely to have conflicts.

    But also there are lots of situations and maybe you hit one where a special treatment of IDs is necessary, would need to be able to run your code to see.

    By the way, your code has some quirks like readonly variables being assigned to (ID, on your model) so probably you mistook code while pasting it on the example, so that might be the reason I can't just run it. Please double check your provided code to see if you find any mistakes and correct them.

    Besides running the code, I can think on a couple examples that may just do what you want.

    Also, we'd advice you to spend some time browsing our WebForms examples too. You will probably find good examples there that are absent from the MVC Examples Explorer. We have most examples on the WebForms version, and to avoid unnecessary repetitions, we do not port every single example to MVC (it would be but a mechanical translation from ASPX to Razor syntax that would not add substantial news at the expense of manual reinterpretation of examples).

    Some suggestions that might help you tinker your view:
    - A grid with entries that, when expanded shows a form for editing: RowExpander Plugin with FormPanel detail.
    - You probably already know this: add tabs with partial views
    - WebForms' form with editable details on entries: Form details.
    - Like the above on MVC: FormPanel

    Sorry if these are not helpful at all, maybe if you can review your example we'd be able to provide you a better feedback by pointing how to make your actual use case to work.

    Hope this helps!
    Fabrício Murta
    Developer & Support Expert
  3. #3
    Hello fabricio.murta,

    thanks for your help. I updated my code to correct the errors. You should now, be able to reproduce my problems.
    It would be great, if you can give me a suggestion how to get correct data binding for my use case.

    Regarding die WebForms Example, for me it would be a great help if all WebForms Examples are translated to Razor syntax. I have no experice with ASPX. So if all examples are available in Razor I do not have to learn a new syntax which I probably wont use in the future.

    Thanks
  4. #4
    I made some progress but I still unable to submit my data to the controller.

    The client site collection of the data works for all use cases.
    alert(this.up('form').getForm().getValues(true));)
    But how can I configure the submit button, that the controller method
      public ActionResult Submit(List<FormPanelEmployee> employeeList)
    is called correctly.

    Any suggestions? What is the best way to submit the values of the FormPanels in the Tabs as a List of Objects?


    Here is the partial working code:

    Model: FormPanelForModel.cs
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using System.ComponentModel.DataAnnotations;
    
    namespace Ext.Net.MVC.Examples.Areas.Models.Models
    {
        public class FormPanelEmployee
        {
    
            private static int NEXT_ID = 1;
    
            [ModelField(IDProperty = true)]
            [Field(FieldType=typeof(Ext.Net.Hidden))]
            public int ID { get; set; }
    
            public string Name { get; set; }
            public string Surname { get; set; }
    
            public DateTime DateOfBirth { get; set; }
    
            public bool IsActive { get; set; }
    
            [UIHint("Department")]
            public FormPanelDepartment Department { get; set; }
    
    
            public FormPanelEmployee()
            {
                ID = NEXT_ID++;
            }
    
            public static List<FormPanelEmployee> GetAll()
            {
                return new List<FormPanelEmployee>
                {
                   new FormPanelEmployee
                   {                   
                       Name = "Nancy",
                       Surname = "Davolio",
                       DateOfBirth = DateTime.Now,
                       IsActive = true,
                       Department = FormPanelDepartment.GetAll()[0]
                   },
                   new FormPanelEmployee
                   {                
                       Name = "Andrew",
                       Surname = "Fuller",
                       Department = FormPanelDepartment.GetAll()[2]
                   }
                };
            }
        }
    
        public class FormPanelDepartment
        {
            public int ID { get; set; }
            public string Name { get; set; }
    
            public static List<FormPanelDepartment> GetAll()
            {
                return new List<FormPanelDepartment>
                {
                    new FormPanelDepartment { ID = 1, Name = "Department A" },
                    new FormPanelDepartment { ID = 2, Name = "Department B" },
                    new FormPanelDepartment { ID = 3, Name = "Department C" }
                };
            }
        }
    }
    Controller: FormPanelForController.cs

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using System.Web.Mvc;
    using Ext.Net.MVC.Examples.Areas.Models.Models;
    
    namespace Ext.Net.MVC.Examples.Areas.Models.Controllers
    {
        public class FormPanelForController : Controller
        {
            public ActionResult Index()
            {
                return View(FormPanelEmployee.GetAll());
            }
    
            public ActionResult Submit(List<FormPanelEmployee> employeeList)
            {
                X.Msg.Alert("EmployeeList:", JSON.Serialize(employeeList)).Show();
                return this.Direct();
            }
    
    
            public ActionResult AddTab(string containerId)
            {
    
                ViewData.Model = new FormPanelEmployee();
    
                var result = new Ext.Net.MVC.PartialViewResult
                {
                    ViewData = this.ViewData,
                    ViewName = "Tab",
                    ContainerId = containerId,
                    RenderMode = RenderMode.AddTo
                };
    
                this.GetCmp<TabPanel>(containerId).SetLastTabAsActive();
    
                return result;
            }
    
    
        }
    }

    View: index.cshtml
    @model List<Ext.Net.MVC.Examples.Areas.Models.Models.FormPanelEmployee>
    @{
        ViewBag.Title = "Index";
        Layout = "~/Views/Shared/_BaseLayout.cshtml";
    }
    @section example
    {    
        @(
        Html.X().FormPanel()
                .ID("MasterFormPanel")
                .BodyPadding(5)
                .DefaultAnchor("100%")
                .Width(800)
                .Items(Html.X().TabPanel()
                               .ID("TabPanel1")
                               .Width(860)
                               .Height(280)
                               .DeferredRender(false)
                               .Items(tabitems =>
                               {
                                   foreach (var employee in Model)
                                   {
                                       tabitems.Add(Html.X().FormPanel()
                                                 .ID("FormPanel" + employee.ID)
                                                 .Title("Employee")                                             
                                                 .Closable(true)
                                                 .BodyPadding(10)
                                                 .ItemsFromPartial("EmployeeEditor", employee));
                                   }
                               }
                 )
                 .TabBar(Html.X().Button()
                 .Icon(Icon.Add)
                 .Flat(true)
                 .DirectEvents(de =>
                 {
                     de.Click.Url = Url.Action("AddTab");
                     de.Click.ExtraParams.Add(new { containerId = "TabPanel1" });
                 })
                 )
                 )
                 .Buttons(Html.X().Button()
                    .Text("Submit")
                    .OnClientClick("alert(this.up('form').getForm().getValues(true));")                                
                )
        )
    }
    Partial View: Tab.cshtml
     
    @model Ext.Net.MVC.Examples.Areas.Models.Models.FormPanelEmployee
    
    @(Html.X().FormPanel()
              .Title("Employee")
              .ID("FormPanel" + Model.ID)
              .Closable(true)
              .BodyPadding(10)
              .ItemsFromPartial("EmployeeEditor", Model)
    )
    Partial View: EmployeeEditor.cshtml
    @model Ext.Net.MVC.Examples.Areas.Models.Models.FormPanelEmployee
    
    
    @(Html.X().TextFieldFor(model => Model.Name)
        .ID("TextFieldName" + Model.ID))
    
    @(Html.X().TextFieldFor(model => Model.Surname)
        .ID("TextFieldSurname" + Model.ID))
    
    @(Html.X().DateFieldFor(model => Model.DateOfBirth)
        .ID("DateFieldDateOfBirth" + Model.ID))
    
    @(Html.X().CheckboxFor(model => Model.IsActive)
        .ID("CheckBoxIsActive" + Model.ID))
    
    @(Html.X().ComboBoxFor(model => Model.Department)
        .ID("ComboBoxDepartment" + Model.ID))
  5. #5
    Hello @marjot2112!

    Check if this helps you:

    1. change the controller's Submit() action to:
            public ActionResult Submit(FormCollection employeeList)
            {
                string fieldList = "";
                
                foreach (string fieldName in employeeList.Keys) {
                    fieldList += fieldName + "[" + employeeList[fieldName] + "], ";
                }
                X.Msg.Alert("What's transmitted:", fieldList).Show();
                return this.FormPanel(true);
            }
    And on your main view, have this to submit the form:
    1. The submit button definition will just call a JavaScript function passing the form as argument
    Html.X().Button()
        .Text("Submit")
        .OnClientClick("submitEmployees(this.up('form'));")
    2. This script block may go to your view's "header" section
    <script type="text/javascript">
        var submitEmployees = function (formHandle) {
            formValues = formHandle.getForm().getValues(true);
            formHandle.submit({
                url: "@Url.Action("Submit")",
                waitMsg: 'submitting',
                success: function (form, action) {
                    App.StatusLabel.setText("Status: submit success");
                },
                failure: function (form, action) {
                    if (action.failureType === Ext.form.action.Action.CONNECT_FAILURE) {
                        App.StatusLabel.setText("Status: Error - " + action.response.status + ': ' +
                            action.response.statusText);
                    } else {
                        App.StatusLabel.setText("Status: submit failure. failure level: " + action.failureType);
                    }
                }
            });
        }
    </script>
    With these changes you'll see that all the trouble was worth naught. The form wouldn't submit at all! Waste of time? Not really, the form has an empty field that fails client-side validation. You may want either allow the field blank or provide a meaningful error message by fiddling with the JavaScript block above. Take your time to understand what happens there, and don't refrain from adding breakpoints there to see when which part is run depending on the success of sumission of the form.

    To get rid of unexpected problems and focus on the submission success/failure of the form, I've removed the department field (which was not filling for me -- didn't dig into as it wasn't the question) and given the second employee a 25-year-old date of birth. Words into code, the changes were:
    - In your model definition, the "Andrew" employee now got this line of code:
    DateOfBirth = DateTime.Now.AddYears(-25),
    - In your EmployeeEditor.cshtml partial view, removed the ComboBoxFor line completely (thus removing the 'Department' field from the forms). If you can't get this field to work the way you need, let us know (probably on a new forum thread so we keep things organized -- you can reuse the example here with the reviewed submit feature if it is required to reproduce the issue and transcribe the example there).

    Oh, and last thing, you may notice I have added some App.StatusLabel code on JavaScript. It basically replaces the alert() calls. I actually added this line after the form panel on your Index.cshtml view:
    @Html.X().Label().ID("StatusLabel")
    So the status success/failures are printed on this "status line" to help know what's happening without clicking ok here and there.

    Sorry, it may get confusing for you to follow my reply as I provided but a patch set to your example. I see that you based your sample on the Examples Explorer environment (with sections and layouts and no ResourceManager() at the view at all), so for a clean project I had to write the code differently, not to mean different namespaces. In order to avoid replying with a polluted version, I tried just the bits and pieces. If it reveals itself hard to follow, I can share the full resulting code (that may just work both on clean projects -and- in the Examples Explorer project).

    Well, hope this helps! The working form submit example was based on this example: Models > Submit
    Fabrício Murta
    Developer & Support Expert
  6. #6
    Hi fabricio.murta,

    great, thank you very much, that works for me.

    But one last question regarding the binding of collections with ext.net.

    If I have a view which use a list of objects as model, are there any possibilities for an autobinding, so the controller post method will receive
    an instance of that collection? Or has this always to be done by getting the information from the FormCollection?

    Since I made the experiance that this works for a fixed size array of objects, I expected that these could also be realized for collection of objects.

    e.g.
    @model List<Ext.Net.MVC.Examples.Areas.Models.Models.FormPanelEmployee>
    public ActionResult Submit(List<FormPanelEmployee> employeeList)
    If there are autobinding possibilities, what are the rules?
  7. #7
    Yes, indeed! Have a closer look on the example I've mentioned, Models > Submit. It'll show you when it was used direct binding.

    I didn't dig too down on this but it looks like that, because your form has multi tabs and the forms overlaps, the submission gets confused. It validates the form fields on the hidden tabs on submit but effectively sends only the currently displayed tab. That's why it was necessary to use FormCollection. It neither sent all the items nor considered only the current open form during submission.

    I believe a better approach to work with collections of entries would be on grid panels, which are naturally based on a collection of the same model. Have you seen the row expander examples that might be similar to what you need?
    - RowExpander component with FormPanel detail

    You can save each entry as they are edited, or let the changes reflect the grid panel and then in a single submit sync all.

    This also might be an interesting example, which shows the changes saving: Grid with Batch saving

    I hope this helps!
    Fabrício Murta
    Developer & Support Expert

Similar Threads

  1. Replies: 8
    Last Post: May 31, 2013, 12:16 PM
  2. [CLOSED] Bind editor to nested class collection
    By jpadgett in forum 1.x Legacy Premium Help
    Replies: 2
    Last Post: May 02, 2011, 7:21 PM
  3. [CLOSED] Menu Items Collection
    By HOWARDJ in forum 1.x Legacy Premium Help
    Replies: 3
    Last Post: Aug 03, 2009, 10:59 AM
  4. Replies: 5
    Last Post: May 27, 2009, 11:59 AM
  5. how to bind store with HashTable Collection
    By akash in forum 1.x Help
    Replies: 3
    Last Post: May 21, 2009, 2:11 AM

Tags for this Thread

Posting Permissions