PDA

View Full Version : How to save data in grid with AutoAsync()?



AntonCharov
Mar 17, 2020, 1:53 PM
This is demo controller. I use EF 6. When i try to change field it's shown 500 status code. What i'm doing wrong?


public ActionResult ShowAllRequests()
{

IEnumerable<Request> list = new List<Request>(new Request[] {
new Request() { RequestID = 1, IsDone = true, RequestName = "r1",
Brigade = new Brigade() { BrigadeID = 1, BrigadeName = "b1"} },
new Request() { RequestID = 2, IsDone = true, RequestName = "r2",
Brigade = new Brigade() { BrigadeID = 2, BrigadeName = "b2"}}
});

return PartialView("RequestsGrid", list);
}

public ActionResult GetBrigades()
{
IEnumerable<BrigadeComboBox> list =
new List<BrigadeComboBox>(new BrigadeComboBox[] {
new BrigadeComboBox { BrigadeName = "b1", Brigade = new Brigade() { BrigadeID = 1, BrigadeName = "b1"} },
new BrigadeComboBox { BrigadeName = "b2", Brigade = new Brigade() { BrigadeID = 2, BrigadeName = "b2"} }
});

return this.Store(list);
}

public ActionResult RequestHandleChanges(StoreDataHandler handler)
{
List<Request> requests = handler.ObjectData<Request>();

if (handler.Action == StoreAction.Update)
{

}

return handler.Action == StoreAction.Update ? (ActionResult)this.Store(requests) : (ActionResult)this.Content(""); ;
}


Models



public class Request
{
[Field(FieldType = typeof(Ext.Net.Hidden))]
public int RequestID { get; set; }
[Display(Name = "Название")]
public string RequestName { get; set; }
[Display(Name = "Комментарий")]
public string Comment { get; set; }
[Display(Name = "Дата начала")]
public DateTime Start { get; set; }
[Display(Name = "Дата окончания")]
public DateTime End { get; set; }
[Display(Name = "Закрыто")]
public bool IsDone { get; set; }
[UIHint("Brigade")]
public virtual Brigade Brigade { get; set; }
}

public class Brigade
{
[Field(FieldType = typeof(Ext.Net.Hidden), Ignore = true)]
[ModelField(IDProperty = true, UseNull = true)]
public int BrigadeID { get; set; }

[Required]
[PresenceValidator]
[Display(Name = "Название")]
public string BrigadeName { get; set; }
public virtual ICollection<Request> Requests { get; set; }
}

public class BrigadeComboBox
{
public string BrigadeName;
public Brigade Brigade;
}


This is my cshtml. It loads on main page by @Html.Partail("ShowAllRequests")



@model IEnumerable<GeoSystem.Models.Request>

<script>
var brigadeRenderer = function (value) {
if (!Ext.isEmpty(value)) {
return value.BrigadeName;
}

return value;
};

var onStoreException = function (proxy, response, operation) {
var error = operation.getError(),
message = Ext.isString(error) ? error : ('(' + error.status + ')' + error.statusText);

Ext.net.Notification.show({
iconCls: 'icon-exclamation',
html: message + "\n<br /><b>Proxy type:</b> " + proxy.type,
title: 'EXCEPTION',
scrollable: 'both',
hideDelay: 5000,
width: 300,
height: 200
});
};
</script>

@(Html.X().Store()
.ID("BrigadeStore")
.Model(Html.X().Model()
.Fields(
new ModelField("brigade", ModelFieldType.Object) { Mapping = "Brigade" },
new ModelField("BrigadeName", ModelFieldType.String) { Mapping = "BrigadeName" }
)
)
.Proxy(Html.X().AjaxProxy()
.Url(Url.Action("GetBrigades"))
.Reader(Html.X().JsonReader().RootProperty("data"))
)
)

@(Html.X().GridPanel()
.ID("GridPanelRequest")
.Store(Html.X().Store()
.Model(Html.X().Model()
.Fields(
new ModelField("ID", ModelFieldType.Int),
new ModelField("RequestName"),
new ModelField("Comment"),
new ModelField("Start", ModelFieldType.Date),
new ModelField("End", ModelFieldType.Date),
new ModelField("IsDone", ModelFieldType.Boolean),
new ModelField()
{
Name = "Brigade",
Type = ModelFieldType.Object
}
)
)
.AutoSync(true)
.ShowWarningOnFailure(true)
.Listeners(l =>
{
l.Exception.Fn = "onStoreException";
l.Exception.Buffer = 10;
})
.SyncUrl(Url.Action("RequestHandleChanges"))
.DataSource(Model)
)
.Icon(Icon.Table)
.Frame(true)
.Title("Заявки")
.Height(430)
.Width(700)
.StyleSpec("margin-top: 10px;")
.ColumnModel(
Html.X().Column()
.Text("Номер")
.DataIndex("Id"),

Html.X().Column()
.Text("Название")
.DataIndex("RequestName")
.Editor(
Html.X().TextField().AllowBlank(false)
),

Html.X().Column()
.Text("Комментарий")
.DataIndex("Comment")
.Editor(
Html.X().TextField().AllowBlank(false)
),

Html.X().DateColumn()
.Text("Начало")
.DataIndex("Start")
.Editor(
Html.X().DateField().AllowBlank(false)
),

Html.X().DateColumn()
.Text("Конец")
.DataIndex("End")
.Editor(
Html.X().DateField().AllowBlank(false)
),

Html.X().BooleanColumn()
.Text("Завершено")
.DataIndex("IsDone")
.Editor(
Html.X().TextField().AllowBlank(false)
),

Html.X().Column()
.Text("Бригада")
.DataIndex("Brigade")
.Width(240)
.Renderer("brigadeRenderer")
.Editor(
Html.X().ComboBox()
.QueryMode(DataLoadMode.Remote)
.TriggerAction(TriggerAction.All)
.StoreID("BrigadeStore")
.ValueField("brigade")
.DisplayField("BrigadeName")
)
)
.Plugins(
Html.X().CellEditing()
)
)

fabricio.murta
Mar 18, 2020, 3:38 AM
Hello @AntonCharov!

After just a couple reviews I could get your sample "working" in my side, I can reproduce the issue and I see you did right like the GridPanel_Update > AutoSave (https://mvc5.ext.net/GridPanel_Update/AutoSave) example does! Yet, you get an exception thrown when the action is called.

The PartialView bit proved unnecessary, all I did was, export the ShowAllRequests value to a static variable (could use it as a "database" later, effectively changing its data if need be), then include its action to return View(AllRequests).

Basically controller code turned into:



public ActionResult GridAutoSync() => View(AllRequests); // use the partial view as view

public static IEnumerable<Request> AllRequests = new List<Request>(new Request[] {
new issuesModel.C62861.Request() { RequestID = 1, IsDone = true, RequestName = "r1",
Brigade = new issuesModel.C62861.Brigade() { BrigadeID = 1, BrigadeName = "b1"}
},
new issuesModel.C62861.Request() { RequestID = 2, IsDone = true, RequestName = "r2",
Brigade = new issuesModel.C62861.Brigade() { BrigadeID = 2, BrigadeName = "b2"}}
});

public ActionResult ShowAllRequests()
{
return PartialView("RequestsGrid", AllRequests);
}


And, of course, add ResourceManager and some HTML context to the view. The same error was reproduced.

What's interesting here is, I couldn't break within the Action code when running in Debug mode, so something is breaking before it gets down to that method; I'll have to further investigate it to tell what exactly is going on.

Great job making the test case, thank you! We'll get back to you shortly with the outcome of the investigation!

fabricio.murta
Mar 18, 2020, 4:14 AM
Hello again, @AntonCharov!

I found the issue to be with the 'ID' client-side field you are mapping from server-side RequestID one.

It breaks before getting in the actual MVC Action because the client is sending two conflicting fields -- C#-side. In fact, the two fields don't conflict client-side as they are, respectively, ID and id. When turned into a dictionary in C#, the dictionary won't allow the two identical (case-insensitive) fields and throw an exception before the payload is even passed to the method.

The obvious way to fix this is by changing the field name in your line 47, new ModelField("ID", ModelFieldType.Int), to something not matching id in lowercase.

But I'm sure you won't be happy with this "magic" lowercase ID being transmitted back and forth the server if you don't use it and don't even know what it is for. And here's the reason this additional ID field gets created: Whenever you don't specify a model which field in it is the unique ID property, Ext JS (client side portion of Ext.NET) will always maintain an internal internalId property to the grid data's record. And again, assuming you don't have a unique ID property (as you didn't inform one), Ext.NET passes it back into the server to ensure a means to uniquely identify an element in the record set; thus, the id field is sent back.

So, if you already have your unique ID field, all you need to do is inform this in your Model() specification:



.IDProperty("ID")


So you won't even need to rename again your "ID" client-side data index.

If still lost, all you need to do is, add the IDProperty line mentioned above after your view code's line 45 (as line 46 of the code, so that it is tied to the .Model() scope).

Now, to get the exact exception that is thrown in that case, all you needed to do is open browser developer tools, switch to the 'network' tab then, after opening the page, before changing a cell, clear the list. Do the change, ignore the debugger stopping due to client-side exception, then check the access returning 500 error in the log. When you open the request, you will be able to see a JSON-formatted payload with the "doubled" ID references, and in the response side you'll be able to see the stack trace (it is text-only formatted within a XML/HTML comment block in the end of the response).

Hope this helps!

AntonCharov
Mar 18, 2020, 11:00 AM
Thank you! This is very useful information. I have added IDProperty() to the model. The method argument is mapping name on server side?
I got the following code. it works correctly and status code 500 is gone. About @(Html.X().ResourceManager()) - i have it on my main page.



@model IEnumerable<GeoSystem.Models.Request>

<script>
var brigadeRenderer = function (value) {
if (!Ext.isEmpty(value)) {
return value.BrigadeName;
}

return value;
};

var onStoreException = function (proxy, response, operation) {
var error = operation.getError(),
message = Ext.isString(error) ? error : ('(' + error.status + ')' + error.statusText);

Ext.net.Notification.show({
iconCls: 'icon-exclamation',
html: message + "\n<br /><b>Proxy type:</b> " + proxy.type,
title: 'EXCEPTION',
scrollable: 'both',
hideDelay: 5000,
width: 300,
height: 200
});
};
</script>

@(Html.X().Store()
.ID("BrigadeStore")
.Model(Html.X().Model()
.Fields(
new ModelField("brigade", ModelFieldType.Object) { Mapping = "Brigade" },
new ModelField("BrigadeName", ModelFieldType.String) { Mapping = "BrigadeName" }
)
)
.Proxy(Html.X().AjaxProxy()
.Url(Url.Action("GetBrigades"))
.Reader(Html.X().JsonReader().RootProperty("data"))
)
)

@(Html.X().GridPanel()
.ID("GridPanelRequest")
.Store(Html.X().Store()
.Model(Html.X().Model().IDProperty("RequestID")
.Fields(
//new ModelField("ID", ModelFieldType.Int),
new ModelField("RequestName"),
new ModelField("Comment"),
new ModelField("Start", ModelFieldType.Date),
new ModelField("End", ModelFieldType.Date),
new ModelField("IsDone", ModelFieldType.Boolean),
new ModelField()
{
Name = "Brigade",
Type = ModelFieldType.Object
}
)
)
.AutoSync(true)
.ShowWarningOnFailure(true)
.Listeners(l =>
{
l.Exception.Fn = "onStoreException";
l.Exception.Buffer = 10;
})
.SyncUrl(Url.Action("RequestHandleChanges"))
.DataSource(Model)
)
.Icon(Icon.Table)
.Frame(true)
.Title("Заявки")
.Height(430)
.Width(700)
.StyleSpec("margin-top: 10px;")
.ColumnModel(
//Html.X().Column()
// .Text("Номер")
// .DataIndex("ID"),

Html.X().Column()
.Text("Название")
.DataIndex("RequestName")
.Editor(
Html.X().TextField().AllowBlank(false)
),

Html.X().Column()
.Text("Комментарий")
.DataIndex("Comment")
.Editor(
Html.X().TextField().AllowBlank(false)
),

Html.X().DateColumn()
.Text("Начало")
.DataIndex("Start")
.Editor(
Html.X().DateField().AllowBlank(false)
),

Html.X().DateColumn()
.Text("Конец")
.DataIndex("End")
.Editor(
Html.X().TextField()
),

Html.X().CheckColumn()
.Text("Завершено")
.DataIndex("IsDone")
.Editable(true),

Html.X().Column()
.Text("Бригада")
.DataIndex("Brigade")
.Width(240)
.Renderer("brigadeRenderer")
.Editor(
Html.X().ComboBox()
.QueryMode(DataLoadMode.Remote)
.TriggerAction(TriggerAction.All)
.StoreID("BrigadeStore")
.ValueField("brigade")
.DisplayField("BrigadeName")
)
)
.Plugins(
Html.X().CellEditing()
)
)



in this case, the client sends the following message when the change is made

25316
It puts the data in Request Payload. How do I get this on the server side? Both JsonData and ObjectData<Request>() return null.


public ActionResult RequestHandleChanges(StoreDataHandler handler)
{
String data = handler.JsonData;
List<Request> requests = handler.ObjectData<Request>();
}


When i use StoreForModel for other table



@model IEnumerable<GeoSystem.Models.Brigade>

@(Html.X().GridPanel()
.ID("GridPanelBrigade")
.Store(
Html.X().StoreForModel().ID("StoreBrigade")
.AutoSync(true)
.ShowWarningOnFailure(false)
.SyncUrl(Url.Action("BrigadeHandleChanges"))
)

It puts the data in Form Data and json converter in BrigadeHandleChanges - handler.ObjectData<Brigade>(); retuns a list of objects.

fabricio.murta
Mar 18, 2020, 7:45 PM
Hello, @AntonCharov!


The method argument is mapping name on server side?

The answer for this is no, it won't map back the value if you don't explicitly let Ext.NET know you want so. As you can see in the payload sent back to server when you change anything, the ID field is sent. There is no information in C# code telling in the json parsing rules the ID field should be mapped into RecordID so, it is not going to happen.

To let Ext.NET know the mapping you did server > client back client > server, one way to do this is by using the Server Field Mapping concept highlighted in this example: Grid Panel > Data Presentation > Server Mapping (https://mvc5.ext.net/#/GridPanel_Data_Presentation/Server_Mapping/).


It puts the data in Request Payload. How do I get this on the server side? Both JsonData and ObjectData<Request>() return null.

You should check the request payload (data client sends), if it matches the mapping rules for Json. By commenting the "ID" field, I'm surprised the RequestID one is sent back to server from client. Using the StoreFor is a way to ensure you are using the right mapping back and forth even when you change server-side code. Are you sure the screenshot of the payload you sent matches the code sample you provided for the view?


When i use StoreForModel for other table (...) It puts the data in Form Data and json converter in BrigadeHandleChanges - handler.ObjectData<Brigade>(); retuns a list of objects.

This is, as far as I see in our corresponding example (GridPanel_Update > AutoSave (https://mvc5.ext.net/GridPanel_Update/AutoSave)), what you should expect. When you cast it into Brigade it should return a list of Brigade objects.

If not, then there may be something in the way of the mapping to json and back. At some point I noticed from Brigade you reference Request and the other way around. Can you imagine how an infinite loop reference would be represented in json, as it does not really implement a pointer or reference type?

The way you have your shared source code it looks reasonable not to be mapped back unless Server Mapping or a matching set of fields is used in the grid's model; so, either:
- use the very same name of the model fields corresponding to the received json object
- "flatten" the object structure down to a simpler one (without object references, lists, if possible) to avoid issues; you can, for instance, have just the ID of the brigade, then have another store with the possible values, if that's something similar to what's done in this example: Grid Panel > Data Presentation > Field Mapping (https://mvc5.ext.net/#/GridPanel_Data_Presentation/Field_Mapping/)
- use the <component>For approach to automatically do the mapping from-to server data. It seems to be what you tried without luck; we do have sample working with using StoreForModel, so we'd need to take a look how you are trying to implement it.

Here our examples using StoreForModel if you want to take a look on other possible approaches and scenarios:

- GridPanel > Paging_and_Sorting > Remote (https://mvc5.ext.net/GridPanel_Paging_and_Sorting/Remote)
- GridPanel > Update > AutoSave (https://mvc5.ext.net/GridPanel_Update/AutoSave)
- GridPanel > Update > Batch (https://mvc5.ext.net/GridPanel_Update/Batch)
- GridPanel > Update > Restful (https://mvc5.ext.net/GridPanel_Update/Restful)

This is still within the AutoSync issue scope, so I think it's okay to continue the issue here if you don't want to create a new thread. The only example we have using AutoSync is the AutoSave one we linked several times here.