[CLOSED] Bug in TreeStore

  1. #1

    [CLOSED] Bug in TreeStore

    Support,

    So this is a really nasty bug. Took a while to track it down and reproduce but the problem is that the Remove function isnt actually removing the Nodes from the TreeStore. It only fails in a specific case. The case is caused by a specific usage that causes the internal store to destabilize and then any Remove thereafter is broken.

    to test:
    step #1: run the example as is and notice the Messages and Archives exist.
    step #2: only uncomment test #2 and you will see that Messages and Archives are properly removed.
    step #3. comment test#2 out and uncomment test #3. notice that even though remove is called, Messages and Archives are STILL present. This is because the remove function on t3 fails internally destabilizing the store so any remove after that call is broken.

    thanks,
    /Z

    <%@ Page Language="C#" %>
    
    
    <!DOCTYPE html>
    
    
    <html>
    <head runat="server">
        <title>Multi Node Tree List built using markup - Ext.NET Examples</title>
        <link href="/resources/css/examples.css" rel="stylesheet" />
    
    
    <script runat="server">
        protected void Page_Load(object sender, EventArgs e)
        {
            if (!X.IsAjaxRequest)
            {
                //test #1
                //run with both there
                //Messages and Archives should be gone
                var t1 = TreeStore1.GetNodeById("test1");
                var t2 = TreeStore1.GetNodeById("test2");
    
    
                //start test #2
                /*
                //run with both removed
                //Messages and Archives should be gone
                t1.Remove();
                t2.Remove();
    
    
                //end test #2
                */
    
    
                //test #3
                /*
                //remove a "blank" that doesnt exist
                //run with both removed
                //Messages and Archives should be gone
                //bug is that the blank seems to screw up the Treestore and after removing the "fake" blank, nothing is removed anymore. this is a critical issue that was 
                //causing menus to show for users that didnt have access to them. we temporarily fixed by excluding the blanks.
                var t3 = TreeStore1.GetNodeById("");    //why does this return a values?? we expected NULL and then we null checked it. but it returns a valid value for something reason.
                //js created by this removal screws up the tree for future removals
                t3.Remove();
    
    
                t1.Remove();
                t2.Remove();
    
    
                //end test #3
                */
            }
    
    
        }
    
    
    </script>
    
    
        <script type="text/javascript">
            var syncSettings = function () {
                App.ExpanderOnlyToggle.setChecked(App.TreeList1.getExpanderOnly());
                App.SingleExpandToggle.setChecked(App.TreeList1.getSingleExpand());
                App.MicroToggleBtn.setPressed(App.TreeList1.getMicro());
            }
    
    
            var handleSEToggle = function (me) {
                if (me.checked) {
                    // If setting Single Expand, collapse everyone to ensure only one
                    // node ever gets expanded. Do not recurse into nodes, as manually
                    // collapsing a node does not collapse its children.
                    App.TreeList1.getStore().getRoot().collapseChildren(false);
                }
    
    
                App.TreeList1.setSingleExpand(me.checked);
            }
    
    
            var handleNavToggle = function (button, pressed) {
                var treelist = App.TreeList1,
                    ct = treelist.ownerCt;
    
    
                treelist.setExpanderFirst(!pressed);
                treelist.setUi(pressed ? 'nav' : null);
                treelist.setHighlightPath(pressed);
                ct[pressed ? 'addCls' : 'removeCls']('treelist-with-nav');
    
    
                // If switching to Nav while already on Micro, adjust the width
                if (treelist.getMicro()) {
                    ct.setWidth(treelist.toolsElement.getWidth())
                }
    
    
                if (Ext.isIE8) {
                    this.repaintList(treelist);
                }
            }
    
    
            var handleMicroToggle = function (me) {
                var tl = App.TreeList1,
                    ct = tl.ownerCt;
                App.TreeList1.setMicro(me.pressed);
    
    
                if (me.pressed) {
                    tl.macroWidth = ct.getWidth();
                    ct.setWidth(tl.toolsElement.getWidth());
                } else {
                    if (tl.macroWidth === undefined) {
                        tl.macroWidth = 200;
                    }
                    ct.setWidth(tl.macroWidth);
                }
            }
    
    
            var handleTLSel = function (me, node) {
                var pane = App.SelectionPanel,
                    cn = node,
                    selPath = [];
    
    
                while (!cn.isRoot()) {
                    selPath.push(cn.data.text);
                    cn = cn.parentNode;
                }
                alert("Selected: <br /><b>" + selPath.reverse().join(" > ") + "</b>");
            }
        </script>
    
    
    </head>
    <body>
        <form runat="server">
            <ext:ResourceManager runat="server" Theme="Triton">
                <Listeners>
                    <DocumentReady Fn="syncSettings" />
                </Listeners>
            </ext:ResourceManager>
    
    
            <h1>Tree List Component Overview</h1>
    
    
            <p>
                The <b>TreeList</b> component is a simplified and lightweight version of the <b>TreePanel</b>, useful for simple and responsive navigation trees.
            </p>
    
    
            <ext:Window
                ID="Window1"
                runat="server"
                Title="TreeList"
                IconCls="x-fa fa-gears"
                Width="500"
                Height="450"
                Closable="False"
                Resizable="false"
                Layout="BorderLayout">
                <HeaderConfig runat="server">
                    <Items>
                        <ext:Button runat="server" Text="Options">
                            <Menu>
                                <ext:Menu runat="server" ID="OptionsMenu">
                                    <Items>
                                        <ext:CheckMenuItem
                                            ID="ExpanderOnlyToggle"
                                            runat="server"
                                            Text="Expander Only"
                                            Handler="App.TreeList1.setExpanderOnly(this.checked)" />
                                        <ext:CheckMenuItem ID="SingleExpandToggle" runat="server" Text="Single Expand">
                                            <Listeners>
                                                <CheckChange Fn="handleSEToggle" />
                                            </Listeners>
                                        </ext:CheckMenuItem>
                                    </Items>
                                </ext:Menu>
                            </Menu>
                        </ext:Button>
                        <ext:Button
                            ID="NavToggleBtn"
                            runat="server"
                            Text="Nav"
                            EnableToggle="true">
                            <Listeners>
                                <Toggle Fn="handleNavToggle" />
                            </Listeners>
                        </ext:Button>
                        <ext:Button
                            ID="MicroToggleBtn"
                            runat="server"
                            Text="Micro"
                            EnableToggle="true">
                            <Listeners>
                                <Toggle Fn="handleMicroToggle" />
                            </Listeners>
                        </ext:Button>
                    </Items>
                </HeaderConfig>
                <Items>
                    <ext:Panel
                        ID="TreeListPanel"
                        runat="server"
                        Width="250"
                        Border="false"
                        Region="West"
                        Split="true"
                        Scrollable="Both">
                        <LayoutConfig>
                            <ext:VBoxLayoutConfig Align="Stretch" />
                        </LayoutConfig>
                        <Items>
                            <ext:TreeList runat="server" ID="TreeList1">
                                <Listeners>
                                    <SelectionChange Fn="handleTLSel" />
                                </Listeners>
                                <Store>
                                    <ext:TreeStore ID="TreeStore1" runat="server">
                                        <Root>
                                            <ext:Node Expanded="true">
                                                <Children>
                                                    <ext:Node Text="Home" IconCls="x-fa fa-home">
                                                        <Children>
                                                            <ext:Node NodeID="test1" Text="Messages" IconCls="x-fa fa-inbox" Leaf="true" />
                                                            <ext:Node NodeID="test2" Text="Archive" IconCls="x-fa fa-database">
                                                                <Children>
                                                                    <ext:Node Text="First" IconCls="x-fa fa-sliders" Leaf="true" />
                                                                    <ext:Node Text="No Icon" IconCls="" Leaf="true" />
                                                                </Children>
                                                            </ext:Node>
                                                            <ext:Node Text="Music" IconCls="x-fa fa-music" Leaf="true" />
                                                            <ext:Node Text="Video" IconCls="x-fa fa-film" Leaf="true" />
                                                        </Children>
                                                    </ext:Node>
                                                    <ext:Node Text="Users" IconCls="x-fa fa-user">
                                                        <Children>
                                                            <ext:Node Text="Tagged" IconCls="x-fa fa-tag" Leaf="true" />
                                                            <ext:Node Text="Inactive" IconCls="x-fa fa-trash" Leaf="true" />
                                                        </Children>
                                                    </ext:Node>
                                                    <ext:Node Text="Groups" IconCls="x-fa fa-group" Leaf="true" />
                                                    <ext:Node Text="Settings" IconCls="x-fa fa-wrench">
                                                        <Children>
                                                            <ext:Node Text="Sharing" IconCls="x-fa fa-share-alt" Leaf="true" />
                                                            <ext:Node Text="Notifications" IconCls="x-fa fa-flag" Leaf="true" />
                                                            <ext:Node Text="Network" IconCls="x-fa fa-signal" Leaf="true" />
                                                        </Children>
                                                    </ext:Node>
                                                </Children>
                                            </ext:Node>
                                        </Root>
                                    </ext:TreeStore>
                                </Store>
                            </ext:TreeList>
                        </Items>
                    </ext:Panel>
                    <ext:Panel
                        ID="SelectionPanel"
                        runat="server"
                        Region="Center"
                        BodyPadding="10" />
                </Items>
            </ext:Window>
        </form>
    </body>
    </html>
    Last edited by fabricio.murta; Apr 11, 2019 at 5:38 PM.
  2. #2
    Hello @Z!

    This happens because an exception is thrown when you call null.remove(), as t3 is null. As the error can only be triggered at run time (client-side), the rest of the code simply is not run. I believe that's usually how it happens in JavaScript, isn't it?

    For instance, if you split the code in two direct events (click of a button), where one removes that invalid t3, and another that removes the valid t1 and t2, then you create individual buttons for these direct events; if you click first, you won't get an exception from server-side, as it just enqueues the client-side commands to get the node (and that happens successfully, it can't say from server side the ID is valid or not); but you'll get the exception client side, and any remaining commands in the t3 direct event won't run. But if then you click for the t1, t2 direct event handler, it will run normally.

    The above paragraph may be a little too cramped on the information. What I'm trying to say is that if you had something like this in your code sample:

    within the <script runat=server /> tag:

    // Runs to completion even if direct event above triggers an exception.
    protected void Test2(object sender, DirectEventArgs e)
    {
        var t1 = TreeStore1.GetNodeById("test1");
        var t2 = TreeStore1.GetNodeById("test2");
    
        t1.Remove();
        t2.Remove();
    }
    
    // Breaks. It runs in code behind to completion, yet throws an exception
    // on client side.
    protected void Test3(object sender, DirectEventArgs e)
    {
        var t1 = TreeStore1.GetNodeById("test1");
        var t2 = TreeStore1.GetNodeById("test2");
        var t3 = TreeStore1.GetNodeById("");
    
        t3.Remove();
    
        t1.Remove();
        t2.Remove();
    }
    within the panel's, <HeaderConfig><Items> tag:

    <ext:Button runat="server" Text="Run test 2" OnDirectClick="Test2" />
    <ext:Button runat="server" Text="Run test 3" OnDirectClick="Test3" />
    (could be anywhere else in the page, just as long as the buttons are visible/clickable).

    With the code above, although you'd get an exception (the Remove() method won't exist on null/undefined t3) with the Run Test 3 button, you'd still be able to go ahead and remove the entries from the tree when you click the Run Test 3 button.

    Now, while this may not really be a bug, it is indeed a limitation as Ext.NET can't throw an exception on something that relies on client-side execution result; else we could just add a try-catch block and have the exception properly treated.

    For example, if we want to ignore invalid IDs, this would be one solution:

    DirectEvent handler (in <script runat=server/> block):

    // Wraps a client-side try-catch via the tryRemoveNode() client-side method.
    protected void Test4(object sender, DirectEventArgs e)
    {
        foreach (var id in new string[] { "", "test1", "test2" })
        {
            X.AddScript("tryRemoveNode(' " + id + "');");
        }
    }
    Client-side method (in <script type="text/javascript"/> block):

    var tryRemoveNode = function (id) {
        var node = App.TreeStore1.getNodeById(id);
    
        if (node !== null) {
            node.Remove();
            Ext.toast("Node removed: " + id);
        } else {
            Ext.toast("Tried to remove non-existing node: " + id);
        }
    }
    trigger button:

    <ext:Button runat="server" Text="Run test 4" OnDirectClick="Test4" />
    I hope this clarifies the matter for you!
    Fabrício Murta
    Developer & Support Expert
  3. #3
    So we are actually doing this in code behind. For the purposes of a standalone test case, i re-wrote it in JS only so it would be easier for you to test.
    To work around the problem, we now do a blank string check.
    /Z
  4. #4
    also,
    var node = App.TreeStore1.getNodeById("");
    seems to always return something when it should return null.
  5. #5
    Hello @Z!

    Quote Originally Posted by Z
    So we are actually doing this in code behind.
    Yes, the <script runat="server"/> tag just wraps code behind logic in the same aspx file, so as far as I understand, your test case issue is triggered because code behind doesn't see the client-side exception that is thrown at run time.

    Quote Originally Posted by Z
    seems to always return something when it should return null.
    Interesting, I had the "Test 4" button code call the expected toast() when the id was or was not in the store.

    > App.TreeStore1.getNodeById("");
    <- null
    > App.TreeStore1.getNodeById("test1")
    <- constructor*{_origAddListener: ƒ, addListener: ƒ, _generateBusFn: ƒ, eventOptionsRe: /^(?:scope|delay|buffer|onFrame|single|stopEvent|p…ement|destroyable|vertical|horizontal|priority)$/, $observableInitialized: true,*…}
    > App.TreeStore1.getNodeById("someInvalidId")
    <- null
    Just in case; var node = App.TreeStore1.getNodeById(""), will always return undefined because the attribution itself does not return anything, just binds the call result to the variable.

    What values are you getting when you call it with an invalid ID?
    Fabrício Murta
    Developer & Support Expert
  6. #6
    we had ONE node without a NodeID attribute. Therefore, all calls to getNodeById("") returned that node. That one node allowed this issue to propagate since we did already have null check.
    /Z
    Last edited by geoffrey.mcgill; Apr 11, 2019 at 5:16 PM.
  7. #7
    Hello @Z!

    That's a little odd, in the example you provided, several nodes don't have a NodeID, yet none is returned by the call. So maybe you actually had one with an -empty NodeID- instead? I wouldn't actually expect this to even be retrievable, but it could happen depending on the implementation in Ext JS.

    So, in the end you could solve the issue by also handling the empty-id entries from your data? Or is there still something left to have this working?
    Fabrício Murta
    Developer & Support Expert
  8. #8
    I made sure all had NodeID and that the handler excluded the ""
    /Z
  9. #9
    Hello @Z!

    Cool, thanks for the feedback, and glad you could get it working! I guess we may mark this as closed now, right? Let us know otherwise.
    Fabrício Murta
    Developer & Support Expert

Similar Threads

  1. [CLOSED] TreeGrid with treestore
    By vmehta in forum 3.x Premium Help
    Replies: 8
    Last Post: Nov 19, 2015, 6:54 AM
  2. [CLOSED] Sorting Treestore
    By elbanna23 in forum 2.x Premium Help
    Replies: 4
    Last Post: Nov 20, 2014, 8:53 AM
  3. [CLOSED] how to enumerate treestore?
    By hdsoso in forum 2.x Premium Help
    Replies: 1
    Last Post: Jul 31, 2014, 11:30 AM
  4. [CLOSED] TreeStore with IHierarchicalDataSource
    By bbros in forum 2.x Premium Help
    Replies: 8
    Last Post: Oct 26, 2013, 9:25 AM
  5. [CLOSED] TreeStore issue with 2.2
    By paulc in forum 2.x Premium Help
    Replies: 3
    Last Post: Aug 19, 2013, 9:24 AM

Posting Permissions