PDA

View Full Version : [CLOSED] TreeStore rejectChanges not working



anup
Dec 23, 2014, 3:51 PM
Hi,

This is a follow up from this thread about Ext.NET 2:
http://forums.ext.net/showthread.php?42721-Extending-JsonWriter-with-a-custom-writer-but-not-coming-through

In that previous thread, we found a way to implement rejectChanges and commitChanges methods on the TreeStore because Ext JS 4 didn't have such methods.

Now, Ext JS 5 seems to, but, I am finding at least in my scenario it is not working.

Here is a modified version of the code posted in the previous thread, removing the tree store override, etc. I've also removed the custom writer stuff that the original thread started off about as it is not relevant here:



<%@ Page Language="C#" %>

<!DOCTYPE html>
<html>
<head runat="server">
<title>Title</title>

<ext:ResourcePlaceHolder Mode="ScriptFiles" />
</head>
<body>
<ext:ResourceManager runat="server" />

<ext:TreePanel ID="TreePanel1" runat="server" Width="300" Height="200">
<TopBar>
<ext:Toolbar runat="server">
<Items>
<ext:Button Text="Delete" Icon="Delete">
<Listeners>
<Click Handler="
var tree = #{TreePanel1},
node = tree.getSelectionModel().getSelection()[0];

if (!node) return;
node.remove();
tree.getStore().sync({
failure: function(batch) {
tree.getStore().rejectChanges();
},
success: function() {
tree.getStore().commitChanges();
}
});
" />
</Listeners>
</ext:Button>
</Items>
</ext:Toolbar>
</TopBar>
<Root>
<ext:Node Text="Tasks" Expanded="true">
<Children>
<ext:Node Icon="Folder" Text="Delete this 0" Leaf="true" />
<ext:Node Icon="Folder" Text="Delete this 1" Leaf="true" />
<ext:Node Icon="Folder" Text="Delete this 2" Leaf="true" />
</Children>
</ext:Node>
</Root>
<Store>
<ext:TreeStore runat="server" AutoSync="false" ShowWarningOnFailure="false">
<Proxy>
<ext:AjaxProxy Url="TreeLoader.ashx">
<API Destroy="Destroy.ashx" />
<Writer>
<ext:JsonWriter RootProperty="data" Encode="true" />
</Writer>
</ext:AjaxProxy>
</Proxy>
</ext:TreeStore>
</Store>
</ext:TreePanel>
</body>
</html>



And destroy.ashx:



public class Destroy : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
throw new NotSupportedException();
}

public bool IsReusable
{
get { return false; }
}
}


Steps:
- Select a category node
- Press the delete button

Expect:
- Server returns error (as expected)
- Client side failure handler is invoked (as expected)
- It calls tree.getStore().rejectChanges(); but the tree does not show the node that failed to delete again...
- By comparison, the Ext.NET 2 version does show the node that failed to delete

Any thoughts/suggestions?

If you have access to Ext JS 5.1 beta, I am happy to wait to see if that fixes this?

Thanks!

anup
Dec 24, 2014, 9:36 AM
I figured out a solution for this.

Here is the code, followed by explanation:



<%@ Page Language="C#" %>
<%@ Register TagPrefix="cc1" Namespace="Ext.Net3.Tests.FromNuGet.Trees.CustomWriter" Assembly="Ext.Net3.Tests.FromNuGet" %>

<!DOCTYPE html>
<html>
<head runat="server">
<title>Title</title>

<ext:ResourcePlaceHolder Mode="ScriptFiles" />

<script>
/**
* @author Johan Vandeplas
* Courtesy: http://www.sencha.com/forum/showthread.php?230409-rejectChanges-Ext.data.TreeStore
*/
Ext.define('My.overrides.data.NodeInterface', {
override: 'Ext.data.NodeInterface',
statics: {
getPrototypeBody: function () {
var me = this,
protBody = me.callParent(arguments),
removeFn = protBody.remove;


protBody.remove = function () {
var me = this;
me.removedFromNode = me.parentNode;
me.removedFromIdx = me.parentNode.indexOf(me);
removeFn.apply(this, arguments);
};
return protBody;
}
}
});

Ext.define('My.overrides.data.TreeStore', {
override: 'Ext.data.TreeStore',

/**
* brings the store to previous state (rejects all uncommitted changes)
*/
rejectChanges: function () {
var me = this,
recs = me.getRejectRecords(),
len = recs.length,
i = 0,
rec;

for (; i < len; i++) {
rec = recs[i];
rec.reject();
if (rec.phantom) {
me.remove(rec);
}
}

// Restore removed records back to their original positions
recs = me.getRemovedRecords(); // <-- only change here it to replace the null records property with getRemovedRecords();
if (recs) {
len = recs.length;
for (i = len - 1; i >= 0; i--) {
rec = recs[i];

if (rec.removedFromNode) { // new change. Using if, to preserve existing functionality
rec.removedFromNode.insertChild(rec.removedFromIdx , rec);
} else {
me.insert(rec.removedFrom || 0, rec);
}
rec.reject();
}

// Since removals are cached in a simple array we can simply reset it here.
// Adds and updates are managed in the data MixedCollection and should already be current.
recs.length = 0;
}
}
});
</script>
</head>
<body>
<ext:ResourceManager runat="server" />

<ext:TreePanel ID="TreePanel1" runat="server" Width="300" Height="200">
<TopBar>
<ext:Toolbar runat="server">
<Items>
<ext:Button Text="Delete" Icon="Delete">
<Listeners>
<Click Handler="
var tree = #{TreePanel1},
node = tree.getSelectionModel().getSelection()[0];

if (!node)
return;

node.remove();

tree.getStore().sync({
failure: function(batch) {
tree.getStore().rejectChanges();
},
success: function() {
tree.getStore().commitChanges();
}
});
" />
</Listeners>
</ext:Button>
</Items>
</ext:Toolbar>
</TopBar>
<Root>
<ext:Node Text="Tasks" Expanded="true">
<Children>
<ext:Node Icon="Folder" Text="Delete this 0" Leaf="true" />
<ext:Node Icon="Folder" Text="Delete this 1" Leaf="true" />
<ext:Node Icon="Folder" Text="Delete this 2" Leaf="true" />
</Children>
</ext:Node>
</Root>
<Store>
<ext:TreeStore runat="server" AutoSync="false" ShowWarningOnFailure="false">
<Model>
<ext:Model runat="server">
<Fields>
<ext:ModelField Name="task" />
<ext:ModelField Name="user" />
</Fields>
</ext:Model>
</Model>
<Proxy>
<ext:AjaxProxy Url="TreeLoader.ashx">
<API Destroy="Destroy.ashx" />
<Writer>
<ext:JsonWriter RootProperty="data" Encode="true"/>
</Writer>
<ExtraParams>
<ext:Parameter Name="example" Value="1" Mode="Raw" />
</ExtraParams>
</ext:AjaxProxy>
</Proxy>
</ext:TreeStore>
</Store>
</ext:TreePanel>
</body>
</html>


To test:
- Just try to delete a node. It will try to call the Destroy.ashx defined in the API which doesn't exist, which is fine for this test
- Because delete will fail, reject changes will be called and work.

This is very similar to the previous post for Ext.NET 2, with the following changes:

I override TreeStore as before, but the only method I need to change is rejectChanges.

rejectChanges method has this change:

Original:



me.insert(rec.removedFrom || 0, rec);


New code:



if (rec.removedFromNode) { // new change. Using if, to preserve existing functionality
rec.removedFromNode.insertChild(rec.removedFromIdx , rec);
} else {
me.insert(rec.removedFrom || 0, rec);
}


Explanation:

Keeping the NodeInterface override provide earlier it tracks the tree nods removed with additional properties, removedFromNode and removedfFromIdx. Using that we can re-insert the removed node under the right parent in the right place. Without this, the base store (typically used for grids etc) doesn't know about parents.

Hope this helps people.

I will post a similar response to the sencha.com thread if it helps people upgrade there, too.

anup
Dec 24, 2014, 10:09 AM
Just a note that while this works for Ext.NET 3, I don't think it will work for Ext.NET 3.1 (as it does not quite work in Ext JS 5.1.0)

Here is a sencha fiddle test:
https://fiddle.sencha.com/#fiddle/fi5

The same code doesn't work if you change the framework to Ext JS 5.1.0. The parent node (root) gets removed at the same time the selected node is removed...! I will try to look into that at some point...

Daniil
Dec 25, 2014, 8:58 AM
The same code doesn't work if you change the framework to Ext JS 5.1.0.

Anup, let's look into that in greater details after incorporating ExtJS 5.1.0. Does that sound OK for you?

anup
Dec 25, 2014, 9:07 AM
That's perfectly fine. Thanks.

anup
Jan 17, 2015, 8:50 PM
So. I've just started to have a cursory look at this.

Using the ASPX or the Fiddle Example, what is different is what happens in the first line of reject changes:



rejectChanges: function () {
var me = this,
recs = me.getRejectRecords(),
len = recs.length,
i = 0,
rec;

When stepping through getRejectRecords(), it returns the root node, because root node has phantom property set to true. This causes it to be returned as a possible rejected record from inside getRejectRecords based on the filter it is applying. And this is why it gets removed, when it should not.

What I don't get is why it is set to true. According to the Sencha documentation for that property it is "True when the record does not yet exist in a server-side database. Any record which has a real database identity set as its idProperty is NOT a phantom -- it's real." (And default is false).

I checked and the idProperty is set to "id", and in my production code I can also see that the idProperty is set to "id" and that the id property itself is set to the id of the record from the server.

I have not had the time to look to see where this gets set yet.

If you have any thoughts let me know (no rush on weekend though!). I will resume looking into this on Monday.

Daniil
Jan 20, 2015, 9:39 AM
Hi Anup,

Thank you for the investigation. It really helped me to narrow the problem down faster.

ExtJS 5.0.1 - root.phantom is false
ExtJS 5.1.0 - root.phantom is true

I check the phantom just after the initial page load using your test case.

The following line has appeared in ExtJS 5.1.0 in the Ext.data.TreeStore's updateRoot method.

// Because of the application of an ID, this client-created root will not be phantom.
// Ensure it is correctly flagged as a phantom.
// AFTER being registered and joined, otherwise onNodeInsert will set the needsSync flag.
newRoot.phantom = true;

There is a couple of things I could ask.

1. Why .updateRoot() is being called at the initial page load? There is no update, it is just an initial root setting. Though, maybe, it is some internal logic under the ExtJS hood.

2. Anyways, it looks to be a defect to have an initial root setting be treated as a phantom, doesn't it? Though, I am not entirely sure about possible underlying reasons.

As a workaround, I can suggest this listener for the TreeStore.

<Listeners>
<RootChange Handler="item.phantom = false;" Single="true" />
</Listeners>

By the way, searching some clarifications on the Sencha forums I've found this very fresh discussion that is related to our topic.
http://www.sencha.com/forum/showthread.php?297140

If you would like to clarify our doubts asking Evan in that thread (or, maybe, create a separate bug report), we don't mind:)

Anyways, I hope the workaround helps.

anup
Jan 20, 2015, 4:02 PM
Thanks for the investigation.

I raised a bug report at Sencha per your suggestion:
http://www.sencha.com/forum/showthread.php?297171-Tree-root-node-updated-in-5.1-to-be-phantom&p=1085193#post1085193

Thanks also for the workaround. I confirm that works in my various tests. Much appreciated!

I've also updated the Sencha Fiddle I mentioned earlier (with a commented out workaround matching your workaround so they can see the problem first. If you uncomment the rootChange listener, it then works).

I think you can close this for now. We will see what they say regarding the bug report, whether it was 5.0 that was wrong in their view or whether their change for 5.1.0 had this unforeseen consequence...

Daniil
Jan 20, 2015, 6:05 PM
Thanks a lot for reporting to Sencha. They opened a bug.

Created our Issue to track it.
https://github.com/extnet/Ext.NET/issues/644

I am closing this thread for now. If any new information appears I will post a follow-up.

Daniil
Jun 26, 2015, 9:52 AM
This issue is marked as fixed in ExtJS 5.1.1.
https://www.sencha.com/forum/showthread.php?297171

So, the fix already came to SVN trunk with the recent ExtJS 5.1.1 upgrade we did. It goes to the upcoming Ext.NET 3.2.0 release.

anup
Jun 26, 2015, 2:03 PM
Thanks for updating here. I can confirm with latest code removing the manual workaround things work fine.