[CLOSED] RadioGroup - isDirty evaluation

Page 1 of 2 12 LastLast
  1. #1

    [CLOSED] RadioGroup - isDirty evaluation

    Hello support team,
    please use the following test case, open the windows and close them without any change. When only one window is open, isDirty is evaluated as expected. However, if the second (and each subsequent) window is open, only the first open window evaluates isDirty correctly, all other windows are always Dirty.

    @model TestCases.Models.PointerModel
    
    @using Ext.Net;
    @using Ext.Net.MVC;
    
    @{
        ViewBag.Title = "Radio Group";
        Layout = null;
    
        var X = Html.X();
    }
    
    <!DOCTYPE html>
    
    <html>
    <head>
        <title>Ext.NET MVC Test Case</title>
    
        <style>
        </style>
    
        <script type="text/javascript">
            var createWindow = function (title, x, y) {
                return new Ext.window.Window({
                    renderTo: Ext.getBody(),
                    autoShow: true,
                    height: 250,
                    width: 500,
                    x,
                    y, 
                    items: [{
                        border: false,
                        xtype: "form",
                        items: [{
                            xtype: "fieldset",
                            items: [{
                                xtype: "textfield",
                                fieldLabel: "Pointer",
                                hideLabel: true,
                                name: "Pointer",
                                validateOnFocusLeave: true
                            }, {
                                xtype: "radiogroup",
                                items: [{
                                    xtype: "radiofield",
                                    name: "PointerForm",
                                    validateOnFocusLeave: true,
                                    value: true,
                                    boxLabel: "Default",
                                    inputValue: "Default"
                                }, {
                                    xtype: "radiofield",
                                    name: "PointerForm",
                                    validateOnFocusLeave: true,
                                    value: false,
                                    boxLabel: "Capitalized",
                                    inputValue: "Capitalized"
                                }, {
                                    xtype: "radiofield",
                                    name: "PointerForm",
                                    validateOnFocusLeave: true,
                                    value: false,
                                    boxLabel: "Abbreviated",
                                    inputValue: "Abbreviated"
                                }, {
                                    xtype: "radiofield",
                                    name: "PointerForm",
                                    validateOnFocusLeave: true,
                                    value: false,
                                    boxLabel: "Alternate",
                                    inputValue: "Alternate"
                                }],
                                fieldLabel: "Return form"
                            }],
                            layout: {
                                type: "vbox",
                                align: "stretch"
                            },
                            title: "Pointer"
                        }],
                        bodyPadding: 10
                    }],
                    layout: "fit",
                    title,
                    listeners: {
                        close: {
                            fn: function (item) {
                                var formPanel = Ext.ComponentQuery.query('form', this)[0];
                                console.log(title.toUpperCase());
                                showValues(formPanel);
                            }
                        }
                    }
                })
            }
    
            var showValues = function (formPanel) {
                var isDirty = false;
    
                formPanel.getForm().getFields().each(function (f) {
                    if (f.xtype === "radiogroup" || f.xtype === "radiofield") {
                        if (f.isDirty()) isDirty = true;
                        console.log("DETAIL_DIRTY", f.name + ", IsDirty: " + f.isDirty());
                        console.log("ORIG", f.originalValue);
                        console.log("VALUE", f.getValue());
                    }
                });
    
                if (isDirty) Ext.Msg.alert("Form Evaluation", "DIRTY");
            }
        </script>
    </head>
    
    <body>
        @(X.ResourceManager())
    
        @X.DisplayField().ID("version").ReadOnly(true).Margin(10).Width(200)
    
        @X.Button().Text("Open Window 1").Margin(10).OnClientClick("createWindow('Window 1', 200, 100)")
    
        @X.Button().Text("Open Window 2").Margin(10).OnClientClick("createWindow('Window 2', 400, 250)")
    </body>
    </html>
    
    <script type="text/javascript">
        Ext.onReady(function () {
            Ext.getCmp("version").setValue("Ext JS " + Ext.getVersion().version + " / " + "Ext.NET " + Ext.net.Version);
        });
    </script>
    If you take a closer look at the isDirty() function, you'll notice the weird getValue() result:
    Click image for larger version. 

Name:	img1.png 
Views:	21 
Size:	24.9 KB 
ID:	25527
    Click image for larger version. 

Name:	img2.png 
Views:	13 
Size:	13.7 KB 
ID:	25528

    I found that the problem lies in the assigned of the name config parameter: PointerForm. The createWindow function is an exact copy of the code generated from this snippet used in the real application:

        public enum PointerForm
        {
            Default = 760001,
            Capitalized = 760002,
            Abbreviated = 760003,
            Alternate = 760004
        }
    
        public class PointerModel
        {
            public string Pointer { get; set; }
    
            public PointerForm PointerForm { get; set; } = PointerForm.Default;
        }
        @(X.Window().Title("Window1").Width(500).Height(250).Layout(LayoutType.Fit)
                .Listeners(l => l.Close.Handler = "var formPanel = Ext.ComponentQuery.query('form', this)[0]; console.log('WINDOW 1'); showValues(formPanel)")
                .Items(
                    X.FormPanel().Border(false).BodyPadding(10)
                        .Items(
                            X.FieldSet().Title("Pointer")
                                .LayoutConfig(new VBoxLayoutConfig { Align = VBoxAlign.Stretch })
                                .Items(
                                    X.TextFieldFor(m => m.Pointer, false).HideLabel(true),
                                    X.RadioGroupFor(m => m.PointerForm, new List<Radio.Config> {
                                        new Radio.Config{ BoxLabel = "Default", InputValue = TestCases.Models.PointerForm.Default.ToString() },
                                        new Radio.Config{ BoxLabel = "Capitalized", InputValue = TestCases.Models.PointerForm.Capitalized.ToString() },
                                        new Radio.Config{ BoxLabel = "Abbreviated", InputValue = TestCases.Models.PointerForm.Abbreviated.ToString() },
                                        new Radio.Config{ BoxLabel = "Alternate", InputValue = TestCases.Models.PointerForm.Alternate.ToString() }
                                                            })
                                        .GroupName("PointerForm")
                                        .FieldLabel("Return form")
                                )
                        )
                )
            )
    If you remove the name assignment or if you assign a unique name, it works as expected. Since this is an assignment made behind the scenes, I think this is an error.

    Please can you take a look and check what's wrong?

    Thank you for your help.

    Ext JS 7.1.0.46 / Ext.NET 5.1.0

    Kind regards
    Dan
    Last edited by fabricio.murta; May 14, 2021 at 6:28 PM.
  2. #2
    Hello Dan!

    Thanks for the test case, it really lets us see what you do, and I believe I understand most of the issue you're facing.

    The only part I am not sure is:

    Quote Originally Posted by NewLink
    If you remove the name assignment or if you assign a unique name, it works as expected. Since this is an assignment made behind the scenes, I think this is an error.
    In particular, the second sentence.

    I mean, if you have a single form, and build two radio groups with the same form handle (which is the name field), I would expect side effects once the second one is rendered.

    What happens when you create a radio group, all entries with the same name is, you bind the form value to the checked radio item. Usually, one item in the radio group should be "checked". The form would then get something like PointerForm='Default'.

    Now imagine you create a new radio group, all entries therein with the same PointerForm form handle (name). Once the new radio group is rendered it will instruct the checked item in that radio group to be its first item. Which in the case, is also Default. So, fine, form-wise, we'd get again PointerForm='Default'. You can imagine what'd happen as you select different values in each different radio boxes and submit its selection as a form (postback or an AJAX request).

    Server side, or form-side, as you select value in one radio, you'd be changing what the form thinks is the value across all radio fields. If you don't use to submit it, then fine, shouldn't be a problem for you.

    But when we go down to Ext JS components, inconsistencies will still occur. From the first radio group event handlers, it may see it has the "default" value, and it does have a "default" value; but then, an internal ID would point the selected radio item something not its own "Default" radio field item (but the "default" radio field over there, in the second radio group). Then voila, it's dirty albeit the resolved value says the same, the actual selected item is something else...

    Not sure it is easy to follow the whole story, so let me make it short:

    - you may use different ID for the radiogroup inner items every time the window is rendered. Now you can render the window multiple times simultaneously
    - you may disallow more than one window containing that radio group to be displayed at once; to see what I'm talking about, just in your example, click button 1, close the window, then proceed to the next window, ensuring only one window is open at a time.

    So while the name in a radio group should be the same across all entries, if you want to show two independent radio groups simultaneously, each should have their own ID. I mean, suppose you want to get your code and draw two independent radio groups in the same window:

    [{
        xtype: "radiogroup",
        items: [{
            xtype: "radiofield",
            name: "PointerFormA",
            validateOnFocusLeave: true,
            value: true,
            boxLabel: "Default",
            inputValue: "Default"
        }, {
            xtype: "radiofield",
            name: "PointerFormA",
            validateOnFocusLeave: true,
            value: false,
            boxLabel: "Capitalized",
            inputValue: "Capitalized"
        }, {
            xtype: "radiofield",
            name: "PointerFormA",
            validateOnFocusLeave: true,
            value: false,
            boxLabel: "Abbreviated",
            inputValue: "Abbreviated"
        }, {
            xtype: "radiofield",
            name: "PointerFormA",
            validateOnFocusLeave: true,
            value: false,
            boxLabel: "Alternate",
            inputValue: "Alternate"
        }]
    },{
        xtype: "radiogroup",
        items: [{
            xtype: "radiofield",
            name: "PointerFormB",
            validateOnFocusLeave: true,
            value: true,
            boxLabel: "Default",
            inputValue: "Default"
        }, {
            xtype: "radiofield",
            name: "PointerFormB",
            validateOnFocusLeave: true,
            value: false,
            boxLabel: "Capitalized",
            inputValue: "Capitalized"
        }, {
            xtype: "radiofield",
            name: "PointerFormB",
            validateOnFocusLeave: true,
            value: false,
            boxLabel: "Abbreviated",
            inputValue: "Abbreviated"
        }, {
            xtype: "radiofield",
            name: "PointerFormB",
            validateOnFocusLeave: true,
            value: false,
            boxLabel: "Alternate",
            inputValue: "Alternate"
        }],
    }]
    So syntax typos aside (I have not really compiled the code above), see how the first radio group gets PointerFormA and the second, PointerFormB so both any eventual post back or ajax request discerns each radio group's actual value. Client-side, when the radio group checks the value given a radio group's name, one group would never have as current selection an item residing in another radio-group.

    I hope I understand correctly the problem you're facing, but let us know if I got it all wrong. The fact is that binding two different form fields items a same name is prone to errors at some point, especially client-server communication -- but client-side mechanics are not exempt of inconsistencies whatsoever. Maybe I don't see the error in it, but I just didn't look from the correct perspective?
    Fabrício Murta
    Developer & Support Expert
  3. #3
    Hi Fabrício,
    thank you for the thorough answer. I'm sorry I didn't explain it enough ...

    As I stated, the code used in the real application is something like this:

    @(X.Window().Title("Window1").Width(500).Height(250).Layout(LayoutType.Fit)
        .Listeners(l => l.Close.Handler = "var formPanel = Ext.ComponentQuery.query('form', this)[0]; console.log('WINDOW 1'); showValues(formPanel)")
        .Items(
            X.FormPanel().Border(false).BodyPadding(10)
                .Items(
                    X.FieldSet().Title("Pointer")
                        .LayoutConfig(new VBoxLayoutConfig { Align = VBoxAlign.Stretch })
                        .Items(
                            X.TextFieldFor(m => m.Pointer, false).HideLabel(true),
                            X.RadioGroupFor(m => m.PointerForm, new List<Radio.Config> {
                                new Radio.Config{ BoxLabel = "Default", InputValue = TestCases.Models.PointerForm.Default.ToString() },
                                new Radio.Config{ BoxLabel = "Capitalized", InputValue = TestCases.Models.PointerForm.Capitalized.ToString() },
                                new Radio.Config{ BoxLabel = "Abbreviated", InputValue = TestCases.Models.PointerForm.Abbreviated.ToString() },
                                new Radio.Config{ BoxLabel = "Alternate", InputValue = TestCases.Models.PointerForm.Alternate.ToString() }
                                                    })
                                .FieldLabel("Return form")
                        )
                )
        )
    )
    So it is not a single form with two radio groups. In fact, it is one edit form that you can open for as many different records as you want, edit the data and save it again.

    There is no Name specification for each Radion.Config, it is created automatically from the name given by the used model (in our case PointerForm). If you use GroupName to assign anything else to ensure that the name becomes unique, it will stop working because the data binding fails. Just to understand what happens when evaluating isDirty, I captured the generated code sent to the client and used it in my example to show where the problem is.

    The question is what to do in the MVC form to ensure the proper functionality. For all components on the edit form (which is much complex in the real app) it works fine just the RadioGroup interferes with other individual forms opened for the same data model.

    Thank you for your help.

    Kind regards
    Dan
  4. #4
    Hello Dan! Sorry for the long missed shot; I guess I was a bit confused because I focused in the test case you provided.

    I understand you are having a problem with that Razor code block rendering conflicted form names; but I am still now sure how I'd add that block to your test case; is it not something you could yourself reproduce in a test case?

    All in all, I still think you need to ensure the radio groups are getting discrete form names so they don't conflict; especially if you are using partial views, one view can't tell another view has an ID or name already allocated; so in that case you need to ensure the ID is unique; sometimes a safe bet is using a GUID, although it may not look very friendly in the client-side code; anything that makes the radio group's names unique should do.

    Or, maybe there's an obvious way to draw the test case with the code block you provided and I am missing the point again?
    Fabrício Murta
    Developer & Support Expert
  5. #5
    Hi Fabrício,
    maybe the test case I provided was a bit confusing, so let's try this one (the data model as above):

    @model TestCases.Models.PointerModel
    
    @using Ext.Net;
    @using Ext.Net.MVC;
    
    @{
        Layout = null;
        var X = Html.X();
    }
    
    <!DOCTYPE html>
    
    <html>
    <head>
        <title>Ext.NET MVC Test Case</title>
    
        <script type="text/javascript">
            var showValues = function (formPanel) {
                var isDirty = false;
    
                formPanel.getForm().getFields().each(function (f) {
                    if (f.isDirty()) isDirty = true;
                    console.log("DETAIL_DIRTY", f.name + ", IsDirty: " + f.isDirty());
                    console.log("ORIG", f.originalValue);
                    console.log("VALUE", f.getValue());
                });
    
                if (isDirty) Ext.Msg.alert("", "DIRTY");
            }
        </script>
    
    </head>
    
    <body>
        @(X.ResourceManager())
    
        @(X.Window().Title("Window 1").Layout(LayoutType.Fit).Width(500).Height(250).X(200).Y(100)
            .Listeners(l => l.Close.Handler = "var formPanel = Ext.ComponentQuery.query('form', this)[0]; console.log('WINDOW 1'); showValues(formPanel)")
            .Items(
                X.FormPanel().Border(false).BodyPadding(10)
                    .Items(
                        X.FieldSet().Title("Pointer")
                            .LayoutConfig(new VBoxLayoutConfig { Align = VBoxAlign.Stretch })
                            .Items(
                                X.TextFieldFor(m => m.Pointer, false).HideLabel(true),
                                X.RadioGroupFor(m => m.PointerForm, new List<Radio.Config> {
                                        new Radio.Config{ BoxLabel = "Default", InputValue = TestCases.Models.PointerForm.Default.ToString() },
                                        new Radio.Config{ BoxLabel = "Capitalized", InputValue = TestCases.Models.PointerForm.Capitalized.ToString() },
                                        new Radio.Config{ BoxLabel = "Abbreviated", InputValue = TestCases.Models.PointerForm.Abbreviated.ToString() },
                                        new Radio.Config{ BoxLabel = "Alternate", InputValue = TestCases.Models.PointerForm.Alternate.ToString() }
                                    })
                                    .FieldLabel("Return")
                            )
                    )
            )
        )
    
        @(X.Window().Title("Window 2").Layout(LayoutType.Fit).Width(500).Height(250).X(400).Y(250)
            .Listeners(l => l.Close.Handler = "var formPanel = Ext.ComponentQuery.query('form', this)[0]; console.log('WINDOW 2'); showValues(formPanel)")
            .Items(
                X.FormPanel().Border(false).BodyPadding(10)
                    .Items(
                        X.FieldSet().Title("Pointer")
                            .LayoutConfig(new VBoxLayoutConfig { Align = VBoxAlign.Stretch })
                            .Items(
                                X.TextFieldFor(m => m.Pointer, false).HideLabel(true),
                                X.RadioGroupFor(m => m.PointerForm, new List<Radio.Config> {
                                        new Radio.Config{ BoxLabel = "Default", InputValue = TestCases.Models.PointerForm.Default.ToString() },
                                        new Radio.Config{ BoxLabel = "Capitalized", InputValue = TestCases.Models.PointerForm.Capitalized.ToString() },
                                        new Radio.Config{ BoxLabel = "Abbreviated", InputValue = TestCases.Models.PointerForm.Abbreviated.ToString() },
                                        new Radio.Config{ BoxLabel = "Alternate", InputValue = TestCases.Models.PointerForm.Alternate.ToString() }
                                    })
                                    .FieldLabel("Return")
                            )
                    )
            )
        )
    </body>
    </html>
    When you close the first window, it will be incorrectly evaluated as dirty. In my opinion, there's nothing wrong with the form code and the problem is how RadioGroup is handled.

    Kind regards
    Dan
  6. #6
    Hello again, Dan!

    Thanks for the test case! I think it supports my point even more; we need to ensure those groups get unique names throughout the form.

    There's not much else we can do to automatically handle this naming as the RadioGroupFor guesses the name to give to the group from the entity name passed as items. If you are going to draw the same entity in more than one radio group using RadioGroupFor you should pass its htmlFieldName value to give it an unique value throughout the form.

    For instance, in your example it is enough to change one of the X.RadioGroupFor() occurrences to:

    X.RadioGroupFor(m => m.PointerForm, new List<Radio.Config> {
            new Radio.Config{ BoxLabel = "Default", InputValue = issuesModel.c63134.PointerForm.Default.ToString() },
            new Radio.Config{ BoxLabel = "Capitalized", InputValue = issuesModel.c63134.PointerForm.Capitalized.ToString() },
            new Radio.Config{ BoxLabel = "Abbreviated", InputValue = issuesModel.c63134.PointerForm.Abbreviated.ToString() },
            new Radio.Config{ BoxLabel = "Alternate", InputValue = issuesModel.c63134.PointerForm.Alternate.ToString() }
        }, "PointerFormRadio1")
    Then the other, that will get PointerFormRadio would no longer conflict with it.

    You may be annoyed as for why other components don't suffer this? Well, usually the components get their form name attribute from the component's ID in the HTML DOM; and that's already an unique value, so won't be prone to errors; the radio group "needs" the same name to everything in the same group to track changes -- but each one on their own: different groups, different names.

    Hope this helps!
    Fabrício Murta
    Developer & Support Expert
  7. #7
    Hi Fabrício,
    I'm not sure if your conclusion is correct. The test case is not about one form with two or more radio groups at all. If that were the case, you would be right.

    In a real application, however, completely independent views are opened with the data of the selected record. The view contains a form with unique IDs of all components and with names that are linked to the respective names specified in the model (both set by the Ext framework automatically).

    I also think that this statement is not always valid:
    components get their form name attribute from the component's ID in the HTML DOM; and that's already an unique value
    In the test case above, the TextField component gets its name from a model that is not unique and yet does not create any conflict.

    I wanted to create a simple example, but in this case, it may not have been the right decision. So I prepared a more complex test case, which better describes the real situation:

    Model
    public enum PointerForm
    {
        Default = 760001,
        Capitalized = 760002,
        Abbreviated = 760003,
        Alternate = 760004
    }
    
    public class PointerModel
    {
        public int Id { get; set; }
    
        public string Pointer { get; set; }
    
        public PointerForm PointerForm { get; set; } = PointerForm.Default;
    }
    Controller
    public class RadioGroupController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }
    
        public ActionResult OpenDetail(int id)
        {
            PointerModel model = new PointerModel();
    
            model.Id = id;
            model.Pointer = "Pointer " + id;
            model.PointerForm = (PointerForm)760001 + id;
    
            return new Ext.Net.MVC.PartialViewResult {
                ViewName = "Detail",
                Model = model,
                RenderMode = RenderMode.Config
            };
        }
    
        public ActionResult Submit(PointerModel model)
        {
            X.Msg.Alert("Model", JSON.Serialize(model)).Show();
            return this.Direct();
        }
    }
    Index
    @using Ext.Net;
    @using Ext.Net.MVC;
    
    @{
        Layout = null;
    
        var X = Html.X();
    }
    
    <!DOCTYPE html>
    
    <html>
    <head>
        <title>Ext.NET MVC Test Case</title>
    
        <script type="text/javascript">
            var createComponentHandler = function (configList) {
                var config = configList[0],
                    id = configList.id;
    
                config.title = "Record " + id;
                config.x = config.x * id;
                config.y = config.y * id;
                config.renderTo = Ext.getBody();
    
                Ext.create(config);
            }
        </script>
    </head>
    
    <body>
        @(X.ResourceManager())
    
        @(Html.X().Button()
            .Text("Open Record Id = 1")
            .Margin(10)
            .DirectEvents(de => {
            de.Click.ExtraParams.Add(new Parameter { Name = "id", Value = "1", Mode = ParameterMode.Raw });
            de.Click.Url = Url.Action("OpenDetail");
            de.Click.Success = MvcUtils.StringifyScriptBlock(
                @<text>
                    <script type="text/javascript">
                        var cmpObj = eval(Ext.decode(response.responseText));
                        cmpObj.id = extraParams.id;
                        Ext.net.loadResources(cmpObj, createComponentHandler, this);
                    </script>
                </text>);
            })
        )
    
        @(Html.X().Button()
            .Text("Open Record Id = 2")
            .Margin(10)
            .DirectEvents(de => {
            de.Click.ExtraParams.Add(new Parameter { Name = "id", Value = "2", Mode = ParameterMode.Raw });
            de.Click.Url = Url.Action("OpenDetail");
            de.Click.Success = MvcUtils.StringifyScriptBlock(
                @<text>
                    <script type="text/javascript">
                        var cmpObj = eval(Ext.decode(response.responseText));
                        cmpObj.id = extraParams.id;
                        Ext.net.loadResources(cmpObj, createComponentHandler, this);
                    </script>
                </text>);
            })
        )
    </body>
    </html>
    Detail
    @model TestCases.Models.PointerModel
    
    @using Ext.Net;
    @using Ext.Net.MVC;
    
    @{
        Layout = null;
    
        var X = Html.X();
    
        var guid = Guid.NewGuid().ToString();
    }
    
    @(X.Window().Layout(LayoutType.Fit).Width(500).Height(250).X(100).Y(120)
        .Listeners(l => l.Close.Handler = MvcUtils.StringifyScriptBlock(
            @<text>
                <script type="text/javascript">
                    var formPanel = Ext.ComponentQuery.query('form', this)[0],
                        isDirty = false;
    
                    formPanel.getForm().getFields().each(function (f) {
                        if (f.isDirty()) isDirty = true;
                        console.log("DETAIL_DIRTY", f.name + ", IsDirty: " + f.isDirty());
                        console.log("ORIG", f.originalValue);
                        console.log("VALUE", f.getValue());
                    });
    
                    if (isDirty) Ext.Msg.alert("", "DIRTY");
                </script>
            </text>)
        )
        .Items(
            X.FormPanel().Border(false).BodyPadding(10)
                .Items(
                    X.FieldSet().Title("Pointer")
                        .LayoutConfig(new VBoxLayoutConfig { Align = VBoxAlign.Stretch })
                        .Items(
                            X.HiddenFor(m => m.Id, false),
                            X.TextFieldFor(m => m.Pointer, false).HideLabel(true),
                            X.RadioGroupFor(m => m.PointerForm, new List<Radio.Config> {
                                    new Radio.Config{ BoxLabel = "Default", InputValue = TestCases.Models.PointerForm.Default.ToString() },
                                    new Radio.Config{ BoxLabel = "Capitalized", InputValue = TestCases.Models.PointerForm.Capitalized.ToString() },
                                    new Radio.Config{ BoxLabel = "Abbreviated", InputValue = TestCases.Models.PointerForm.Abbreviated.ToString() },
                                    new Radio.Config{ BoxLabel = "Alternate", InputValue = TestCases.Models.PointerForm.Alternate.ToString() }
                                }/*, guid*/)
                                .FieldLabel("Return")
                        //.GroupName(guid)
    
                        ),
                    X.Button().Text("Submit").DirectClickUrl(Url.Action("Submit"))
                )
        )
    )
    If you use the GUID for the RadioGroup name, isDirty is evaluated correctly, but we lose the value for RadioGroup upon submit (or more precisely, the value is sent with a name that the model doesn't know). If you let the framework assign a name, the values ​​sent are correct, but isDirty fails.

    Is this expected behavior?

    Thank you for your feedback.

    Kind regards
    Dan
  8. #8
    Hello Dan!

    Quote Originally Posted by NewLink
    I'm not sure if your conclusion is correct. The test case is not about one form with two or more radio groups at all. If that were the case, you would be right.
    I understand I may be looking at your problem from a different perspective, and I don't think you'd persist on that it may not be fair, without a reason.

    Quote Originally Posted by NewLink
    In a real application, however, completely independent views are opened with the data of the selected record. The view contains a form with unique IDs of all components and with names that are linked to the respective names specified in the model (both set by the Ext framework automatically).
    In general, Ext.NET should be able to set unique IDs and names that do not conflict, automatically. And one exception would be the RadioGroupForm the way you drawn in the previous test case. The "dirty" side effect one radio group gets is because both radio group get the same name setting. You set one, query the other, other shows change. Give each one an unique name handle, problem is gone. This is just a limitation, Ext.NET has no means to ensure the name bound is unique -- it may be possible and all, but it is just not a feature it supports currently.

    Quote Originally Posted by NewLink
    I also think that this statement is not always valid:

    "components get their form name attribute from the component's ID in the HTML DOM; and that's already an unique value"
    I have no objection to your point, especially removing the "usually" prefix in that statement you quoted, it really sounds like it should always happen; so we'd better stick to the relative term; exceptions, like the one I am going to quote below, will always be there.

    Quote Originally Posted by NewLink
    In the test case above, the TextField component gets its name from a model that is not unique and yet does not create any conflict.
    If two or more fields in a form have the same name attribute in DOM, there's no way for a submission to tell those fields apart. If you don't get issues with that, it is because you are never actually making a submission that get values based in their name attribute. Radio fields are grouped by their name attribute; so there are more to conflict with radio groups than there are to text fields.

    I will be checking the test case you provided and will post a follow up here soon, so bear on with us!
    Fabrício Murta
    Developer & Support Expert
  9. #9
    Hello again, Dan! I finally gave your test case a good run, I think I may finally get what you mean!

    Quote Originally Posted by NewLink
    If you use the GUID for the RadioGroup name, isDirty is evaluated correctly, but we lose the value for RadioGroup upon submit (or more precisely, the value is sent with a name that the model doesn't know). If you let the framework assign a name, the values ​​sent are correct, but isDirty fails.

    Is this expected behavior?
    And I am afraid yes, this is the expected behavior. Is has its nice side, where it really passes in the right value when you do the submit() call, but the two radio groups with the same handler simply won't behave well if used simultaneously due to the client-side logic involved in evaluating the checked value it has (which, to my surprise, does not affect the value code behind receives, due to a DOM scope aspect of how the submit data is crafted).

    It looks like this is a case where model binding won't help you, exactly because you need to bind the same model to different components. You should go with manual form binding in that case.

    There are, though, some alternatives that may help (or allow) you to still use model binding to the window:

    1. stop using windows, bind partial views to distinct form tags: One approach would be to bind different forms for each window. The catch here though, by the "floating" nature of the windows, they are not getting "into" a specific form (or container within a form) so easily. In fact, windows' show() code renders them to the body tag, dom-wise. Simply changing the windows into panels, and then using partial view's RenderMode = RenderMode.AddTo + ContainerId = form_or_container_id would make this alternative possible, but I doubt this will fulfill your scenario.

    2. iframes for total separation: Yet another option would be, instead of bringing the windows as partial views, draw the windows from the page itself (to avoid extra server round-trips) and use a loader with mode=Frame to load not partial views but full blown views within them containing the contents. Then it will get the logical separation it needs (the windows will be drawing an iframe as their contents) for the radio groups with same handles not to interfere with each other.

    3. Make isDirty() your own: Assuming this misled isDirty() is the only issue you have, it may be possible to implement another means to identify whether the radio group has been changed, like binding a change event, or remembering initial value and checking what it currently has... but this in itself might be a challenge due to how a radio group relates to its radio fields, how it was designed. I am not sure it is a good idea as you may fix one issue and uncover a number of "design-constrained" issues (other form handle related conflicts).

    4. Spread the models: I am not sure I should even delve into this possibility but -- well -- this is a possible solution after all! Explore a bit of class inheritance and make different models; you'd need N unique classes to display N simultaneous radio groups client-side. Okay, let's go to another alternative already, shall we?

    5. Ensure single instance: If you allow only one window to be rendered at a given time, you are not going to run into those problems. While this may be the easiest solution and possibly something you may consider, it was clear since the beginning that your scenario required the two (or more) windows to be shown at the same time -- so it would basically mean to hurt user experience, on your real-world functional requirements.

    The model binding feature is something that helps speeding and simplifying many cases but it simply can't cover all use case scenarios, and basically in the problem discussed in this thread, I am compelled to believe that's due to its 1:1 mapping of form names into class members. A means to "map" them would help in this case.

    Hope you understand, this is a limitation due to how radio groups are designed to evaluate their status; you can have two radio groups with the same name handle as long as each one of them are in separate form tags in the rendered HTML DOM.

    If you are interested in any of the (1)...(5) approaches proposed above and are having trouble to envision what I meant (including the "manual binding" approach that I didn't number) I can try to expand on them. This is already going to be a long read so I should not be even more detailed. But I really hope in the end it is helpful for the problem you're facing.
    Fabrício Murta
    Developer & Support Expert
  10. #10
    The response is really long and tiring, so here's a summary:
    - model binding won't work for your scenario
    - when model binding doesn't help, you should use manual binding (don't use <component>For())
    - there are a number of options if you really want to keep using manual binding, but it is really up to you whether one or more of them are worth it (and for that, reading the long last post would be required).

    Hope this helps!
Page 1 of 2 12 LastLast

Similar Threads

  1. [CLOSED] Problem with isDirty
    By infonext in forum 2.x Legacy Premium Help
    Replies: 2
    Last Post: Jan 27, 2014, 7:16 PM
  2. [CLOSED] Regex evaluation freezes the browser
    By vadym.f in forum 1.x Legacy Premium Help
    Replies: 5
    Last Post: Feb 05, 2013, 12:26 PM
  3. Replies: 2
    Last Post: Oct 10, 2012, 6:27 PM
  4. isDirty and textField
    By houdatahbaz in forum 1.x Help
    Replies: 4
    Last Post: Sep 01, 2011, 10:23 PM
  5. form isDirty
    By [WP]joju in forum 1.x Help
    Replies: 5
    Last Post: Jun 22, 2010, 10:53 PM

Posting Permissions