PDA

View Full Version : [CLOSED] Extending JsonWriter with a custom writer, but not coming through?



anup
Sep 01, 2014, 10:29 AM
Hi,

Perhaps I am missing how to do this but I am trying to create a custom writer for my tree panel story proxy so that when I delete nodes, I don't send all the data to the server, just the node id to delete.

To do that, I am trying a variation of the answer to this question:
http://stackoverflow.com/questions/8003975/ext-js-prevent-proxy-from-sending-extra-fields

But my variation is to subclass JsonWriter, to encapsulate this. When I do this, however, looking at the script generated by Ext.NET, I do not see my custom writer.

Here is an example:

c#


public class CustomWriter : JsonWriter
{
public override string InstanceOf
{
get { return "Ext.ux.CategoryNodeWriter"; }
}
}


(I notice there is only an InstanceOf that I can override, and although there is a Type, I am not sure I can (or want) to override it - I would like to keep it as Json).

The client side for this class is this:



Ext.define('Ext.ux.CategoryNodeWriter', {
extend: 'Ext.data.writer.Json',
alias: 'writer.categorynodewriter',

getRecordData: function(record, operation) {
console.log("getRecordData");
return operation.action === 'destroy' ? { 'id': record.get('id') } : this.callParent(arguments);
}
});


A sample ASPX to put this together (which includes the above JavaScript):



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

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

<ext:ResourcePlaceHolder Mode="ScriptFiles" />

<script>
// ################################################## ###################
// Custom Writer for category to control what data is sent to the server
// ################################################## ###################
Ext.define('Ext.ux.CategoryNodeWriter', {
extend: 'Ext.data.writer.Json',
alias: 'writer.categorynodewriter',

getRecordData: function(record, operation) {
console.log("getRecordData");
return operation.action === 'destroy' ? { 'id': record.get('id') } : this.callParent(arguments);
}
});
</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();
" />
</Listeners>
</ext:Button>
</Items>
</ext:Toolbar>
</TopBar>
<Root>
<ext:Node Text="Tasks" Expanded="true">
<Children>
<ext:Node Icon="Folder" Text="Delete this" Leaf="true">
<CustomAttributes>
<ext:ConfigItem Name="id" Value="categoryNode1" Mode="Value" />
<ext:ConfigItem Name="task" Value="Project: Shopping" Mode="Value" />
<ext:ConfigItem Name="user" Value="Tommy Maintz" Mode="Value" />
</CustomAttributes>
</ext:Node>
</Children>
</ext:Node>
</Root>
<Store>
<ext:TreeStore runat="server" AutoSync="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>
<cc1:CustomWriter />
</Writer>
</ext:AjaxProxy>
</Proxy>
</ext:TreeStore>
</Store>
</ext:TreePanel>
</body>
</html>


Steps:
1. Run the above (you may need to adjust the register tag once the writer class is added in)
2. Select the leaf node
3. Press delete

Notice the Loader URL and Destroy URL are intentionally not created - I don't need it - I just want to prove that only id is sent to the destroy URL, even though the URL itself will return a 404!

But in the console, you will not see my console.log statement (as the script does not instantiate my client side writer).

When I look in the script output, the writer is simply this:


writer:{type:"json"}

One workaround I found was I could implement getRecordData in my C# definition of my custom writer, for example:



public override JFunction GetRecordData
{
get { return new JFunction("myGetRecordData"); }
}


However, it means I don't get a fully encapsulated writer (I could of course configure this class to have a property so that the user could pass in the string or jFunction for the GetRecordData implementation and I could then encapsulate it inside my own components perhaps, but ideally I'd like to be able to use my own client side writer.

Have I missed something on how to get this to work (probably something obvious!) or is this a limitation at the moment?

Thanks!

anup
Sep 01, 2014, 12:41 PM
Just thinking about this further - a bigger flaw in my approach is this:

I remove a node from the UI, and then I sync with the store. If the store sync operation fails, the node has been removed from the UI when it may not need to be (and prevents the user from retrying, if for example it is a temporary server problem).

So I will probably revert my original approach to make a Direct Request and in the success handler remove the node, but, nonetheless, I would still be interested to see what I was doing wrong, for future reference...

Thanks!

Daniil
Sep 02, 2014, 1:05 PM
Hi Anup,

This is a key point.

writer: {
type:"json"
}

You should override the Type property (it is an analog of widgets' XType):

public class CustomWriter : JsonWriter
{
public override string InstanceOf
{
get
{
return "Ext.ux.CategoryNodeWriter";
}
}

protected override string Type
{
get
{
return "categorynodewriter";
}
}
}

Then it will be rendered as

writer: {
type: "categorynodewriter"
}
and your JavaScript will be taken into account.


Just thinking about this further - a bigger flaw in my approach is this:

I remove a node from the UI, and then I sync with the store. If the store sync operation fails, the node has been removed from the UI when it may not need to be (and prevents the user from retrying, if for example it is a temporary server problem).

So I will probably revert my original approach to make a Direct Request and in the success handler remove the node, but, nonetheless, I would still be interested to see what I was doing wrong, for future reference...

I think there is a good possibility to still go with a custom JsonWriter. You can listen to the Store's or the AjaxProxy's Exception listener

<Exception Handler="alert(operation.action + ' ' + operation.records[0].getId());" />
and restore a removed node if needed.

anup
Sep 02, 2014, 2:25 PM
Thanks for the note about overriding Type - while I had seen it, I saw it was protected, and for some reason when following some code, I had seen somewhere that was looking for type json or xml and doing something specific with those two types, so I may have confused myself about the type... Yes, it works just like xtype for components, so that part works.

However, rejecting changes seems to be a bit trickier. It looks like TreeStore does not have rejectChanges method, while Store does. Further, the Model has reject method, but it NodeInterface does not seem to. In addition, I tried to call reject changes like this (plus a few other variations):



<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) {
Ext.each(batch.operations || [], function(operation) {
Ext.each(operation.records || [], function(rec) {
rec.reject();
});
});
}
});
" />
</Listeners>
</ext:Button>


The reject method doesn't seem to work. When inspecting the removed record, while it is there in the store's removed records collection, the parentNode, nextSibling and previousSibling nodes are all null, so it seems like the context as to where this node should go back is lost, unless this is all captured manually before doing the remove, which seems a bit brittle and subject to failure in future if the framework changes?

Do you know of a more elegant way to roll back such a change?

comment #9 seems promising from my little testing (basically added a few more nodes in the earlier example and in the sync failure handler, called tree.getStore().rejectChanges(); instead of all the other code above. The author of that comment says he has not tested it much (nor have I!).

http://www.sencha.com/forum/showthread.php?230409-rejectChanges-Ext.data.TreeStore



<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();
console.log('failed', arguments);
}
});
" />
</Listeners>
</ext:Button>

anup
Sep 02, 2014, 2:54 PM
In addition to applying the changes I noted in that link to the sencha forums, it would seem to make sense to also implement commitChanges as implemented in Store, so it is available on TreeStore. This allows you to commitChanges for a treeStore on the successHandler, but rejectChanges in the failure handler.

This I think is important in case you do multiple operations at different times, and if the final delete operation fails, the rejectChanges may show up all your other successfully removed nodes, because there was no commit for them! (It seems like commit on the tree node itself is no use, because the treeStore doesn't seem to watch for it, from what I could see).

Here is the updated example I have so far:



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

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

<ext:ResourcePlaceHolder Mode="ScriptFiles" />

<script>
// ################################################## ###################
// Custom Writer for category to control what data is sent to the server
// ################################################## ###################
Ext.define('Ext.ux.CategoryNodeWriter', {
extend: 'Ext.data.writer.Json',
alias: 'writer.categorynodewriter',

getRecordData: function(record, operation) {
console.log("getRecordData", record, record.getOwnerTree());
return operation.action === 'destroy' ? { 'id': record.get('id') } : this.callParent(arguments);
}
});

/**
* @author Johan Vandeplas
*/
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',

/**
* removes node or an array of nodes from its parent.
* @param {Ext.data.NodeInterface/[Ext.data.NodeInterface]}nodes
* @param {Boolean} [destroy=false] True to destroy the node upon removal.
*/
remove: function (nodes, destroy) {
nodes = Ext.isArray(nodes) ? nodes : [nodes];
Ext.each(nodes, function (node) {
node.remove(destroy === true);
});
},

/**
* @returns {Array}
*/
getRejectRecords: function () {
return Ext.Array.push(this.getNewRecords(), this.getUpdatedRecords(), this.getRemovedRecords());
},

/**
* 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.removed;
len = recs.length;
for (i = len - 1; i >= 0; i--) {
rec = recs[i];
rec.removedFromNode.insertChild(rec.removedFromIdx , rec);
delete rec.removedFromNode;
delete rec.removedFromIdx;
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.
me.removed.length = 0;
},

/**
* Commits all Records with {@link #getModifiedRecords outstanding changes}. To handle updates for changes,
* subscribe to the Store's {@link #event-update update event}, and perform updating when the third parameter is
* Ext.data.Record.COMMIT.
*/
commitChanges : function(){
var me = this,
recs = me.getModifiedRecords(),
len = recs.length,
i = 0;

for (; i < len; i++){
recs[i].commit();
}

// 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.
me.removed.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">
<CustomAttributes>
<ext:ConfigItem Name="id" Value="categoryNode0" Mode="Value" />
<ext:ConfigItem Name="task" Value="Project: Shopping" Mode="Value" />
<ext:ConfigItem Name="user" Value="Tommy Maintz" Mode="Value" />
</CustomAttributes>
</ext:Node>
<ext:Node Icon="Folder" Text="Delete this 1" Leaf="true">
<CustomAttributes>
<ext:ConfigItem Name="id" Value="categoryNode1" Mode="Value" />
<ext:ConfigItem Name="task" Value="Project: Shopping" Mode="Value" />
<ext:ConfigItem Name="user" Value="Tommy Maintz" Mode="Value" />
</CustomAttributes>
</ext:Node>
<ext:Node Icon="Folder" Text="Delete this 2" Leaf="true">
<CustomAttributes>
<ext:ConfigItem Name="id" Value="categoryNode2" Mode="Value" />
<ext:ConfigItem Name="task" Value="Project: Shopping" Mode="Value" />
<ext:ConfigItem Name="user" Value="Tommy Maintz" Mode="Value" />
</CustomAttributes>
</ext:Node>
</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>
<cc1:CustomWriter />
</Writer>
</ext:AjaxProxy>
</Proxy>
</ext:TreeStore>
</Store>
</ext:TreePanel>
</body>
</html>

Daniil
Sep 03, 2014, 5:37 AM
Yes, there is definitely a lack in the API of TreeStore/TreePanel. It is nice that you've been able to overcome with that. Thank you for sharing the solution.

anup
Dec 24, 2014, 9:37 AM
Changes to get the rejectChanges() to work for Ext.NET 3 mentioned here:

http://forums.ext.net/showthread.php?49461-TreeStore-rejectChanges-not-working