[CLOSED] RadioGroup - isDirty evaluation

Hybrid View

Previous Post Previous Post   Next Post Next Post
  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:	79 
Size:	24.9 KB 
ID:	25527
    Click image for larger version. 

Name:	img2.png 
Views:	62 
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

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