I have a KendoUI DataSource linked up to a WebApi 2 OData controller and am having problems with update operations. I can create and delete just fine.
When I make the call to sync the datasource to the server after making any updates I get a 400 error:
Debugging in Visual Studio shows that the patch function is being passed the Id but not the Company object. Firebug shows that the PATCH request looks like this when I change the title of "Test Company" to "Test Company test" and click save:
I have a hunch there is something wonky about this that the server doesn't understand.
The model is simple and I left the controller as whatever VS generated for me:
Model:
Controller:
Finally, the KendoUI, HTML/Javascript is this:
When I make the call to sync the datasource to the server after making any updates I get a 400 error:
{
"odata.error":{
"code":"","message":{
"lang":"en-US","value":"The request is invalid."
},"innererror":{
"message":"patch : Invalid JSON. A token was not recognized in the JSON content.\r\n","type":"","stacktrace":""
}
}
}
Debugging in Visual Studio shows that the patch function is being passed the Id but not the Company object. Firebug shows that the PATCH request looks like this when I change the title of "Test Company" to "Test Company test" and click save:
models=%7B%22Id%22%3A1026%2C%22Title%22%3A%22Test+Company+test%22%7D
I have a hunch there is something wonky about this that the server doesn't understand.
The model is simple and I left the controller as whatever VS generated for me:
Model:
01.
public
class
Company {
02.
public
Company() { }
03.
04.
public
Company(Company company) {
05.
this
.Id = company.Id;
06.
this
.Title = company.Title;
07.
this
.Projects = company.Projects;
08.
}
09.
10.
public
int
Id {
get
;
set
; }
11.
public
string
Title {
get
;
set
; }
12.
13.
public
virtual
ICollection<Project> Projects {
get
;
set
; }
14.
}
Controller:
001.
public
class
CompanyController : ODataController
002.
{
003.
private
ApplicationDbContext db =
new
ApplicationDbContext();
004.
005.
// GET odata/Company
006.
[Queryable]
007.
public
IQueryable<Company> GetCompany()
008.
{
009.
return
db.Companies;
010.
}
011.
012.
// GET odata/Company(5)
013.
[Queryable]
014.
public
SingleResult<Company> GetCompany([FromODataUri]
int
key)
015.
{
016.
return
SingleResult.Create(db.Companies.Where(company => company.Id == key));
017.
}
018.
019.
// PUT odata/Company(5)
020.
public
async Task<IHttpActionResult> Put([FromODataUri]
int
key, Company company)
021.
{
022.
if
(!ModelState.IsValid)
023.
{
024.
return
BadRequest(ModelState);
025.
}
026.
027.
if
(key != company.Id)
028.
{
029.
return
BadRequest();
030.
}
031.
032.
db.Entry(company).State = EntityState.Modified;
033.
034.
try
035.
{
036.
await db.SaveChangesAsync();
037.
}
038.
catch
(DbUpdateConcurrencyException)
039.
{
040.
if
(!CompanyExists(key))
041.
{
042.
return
NotFound();
043.
}
044.
else
045.
{
046.
throw
;
047.
}
048.
}
049.
050.
return
Updated(company);
051.
}
052.
053.
// POST odata/Company
054.
public
async Task<IHttpActionResult> Post(Company company)
055.
{
056.
if
(!ModelState.IsValid)
057.
{
058.
return
BadRequest(ModelState);
059.
}
060.
061.
db.Companies.Add(company);
062.
await db.SaveChangesAsync();
063.
064.
return
Created(company);
065.
}
066.
067.
// PATCH odata/Company(5)
068.
[AcceptVerbs(
"PATCH"
,
"MERGE"
)]
069.
public
async Task<IHttpActionResult> Patch([FromODataUri]
int
key, Delta<Company> patch)
070.
{
071.
if
(!ModelState.IsValid)
072.
{
073.
return
BadRequest(ModelState);
074.
}
075.
076.
Company company = await db.Companies.FindAsync(key);
077.
if
(company ==
null
)
078.
{
079.
return
NotFound();
080.
}
081.
082.
patch.Patch(company);
083.
084.
try
085.
{
086.
await db.SaveChangesAsync();
087.
}
088.
catch
(DbUpdateConcurrencyException)
089.
{
090.
if
(!CompanyExists(key))
091.
{
092.
return
NotFound();
093.
}
094.
else
095.
{
096.
throw
;
097.
}
098.
}
099.
100.
return
Updated(company);
101.
}
102.
103.
// DELETE odata/Company(5)
104.
public
async Task<IHttpActionResult> Delete([FromODataUri]
int
key)
105.
{
106.
Company company = await db.Companies.FindAsync(key);
107.
if
(company ==
null
)
108.
{
109.
return
NotFound();
110.
}
111.
112.
db.Companies.Remove(company);
113.
await db.SaveChangesAsync();
114.
115.
return
StatusCode(HttpStatusCode.NoContent);
116.
}
117.
118.
// GET odata/Company(5)/Projects
119.
[Queryable]
120.
public
IQueryable<Project> GetProjects([FromODataUri]
int
key)
121.
{
122.
return
db.Companies.Where(m => m.Id == key).SelectMany(m => m.Projects);
123.
}
124.
125.
protected
override
void
Dispose(
bool
disposing)
126.
{
127.
if
(disposing)
128.
{
129.
db.Dispose();
130.
}
131.
base
.Dispose(disposing);
132.
}
133.
134.
private
bool
CompanyExists(
int
key)
135.
{
136.
return
db.Companies.Count(e => e.Id == key) > 0;
137.
}
138.
}
Finally, the KendoUI, HTML/Javascript is this:
001.
<h2>Company List</h2>
002.
003.
<div id=
"company-data"
>
004.
<div class=
"col-md-3 col-sm-5 col-xs-5"
>
005.
<div id=
"company-list"
style=
"padding: 0px; height: 500px; overflow: auto"
data-role=
"listview"
data-template=
"list-template"
data-bind=
"source: companies, events: {change: OnSelect}"
data-selectable=
"true"
></div>
006.
<div>
007.
<button class=
"btn btn-success btn-sm"
id=
"btn-add-company"
><span class=
"glyphicon glyphicon-plus"
></span> Add</button>
008.
<button class=
"btn btn-danger btn-sm"
id=
"btn-delete-company"
data-bind=
"visible: hasSelection, click: deleteSelection"
><span class=
"glyphicon glyphicon-remove"
></span> Delete</button>
009.
<button class=
"btn btn-default btn-sm"
id=
"btn-clear-company"
data-bind=
"visible: hasSelection, click: clearSelection"
><span class=
"glyphicon glyphicon-ban-circle"
></span> Clear</button>
010.
<button class=
"btn btn-primary btn-sm btn-block"
id=
"btn-save"
data-bind=
"visible: hasChanges, click: saveChanges"
><span class=
"glyphicon glyphicon-cloud-upload"
></span> Save All</button>
011.
</div>
012.
</div>
013.
<div class=
"col-md-9 col-sm-7 col-xs-7"
data-bind=
"visible: hasSelection"
>
014.
<label
for
=
"company-title"
>Title:</label><br />
015.
<input id=
"company-title"
data-bind=
"value: selectedItem.Title"
><br />
016.
</div>
017.
</div>
018.
019.
<script type=
"text/x-kendo-template"
id=
"list-template"
>
020.
<div class=
"company"
style=
"cursor: pointer"
>
021.
<span data-bind=
"text: Title"
></span>
022.
</div>
023.
</script>
024.
025.
<script>
026.
$(
function
() {
027.
var
firstSync =
true
;
028.
var
companyVM =
new
kendo.observable({
029.
// Data Source.
030.
companies:
new
kendo.data.DataSource({
031.
type:
'odata'
,
032.
transport: {
033.
create: {
034.
url:
'/odata/Company'
,
035.
dataType:
'json'
,
036.
type:
'POST'
037.
},
038.
read: {
039.
url:
'/odata/Company'
,
040.
dataType:
'json'
041.
},
042.
update: {
043.
url:
function
(data) {
044.
return
'/odata/Company('
+ data.Id +
')'
;
045.
},
046.
dataType:
'json'
,
047.
type:
'PATCH'
048.
},
049.
destroy: {
050.
url:
function
(data) {
051.
return
'/odata/Company('
+ data.Id +
')'
;
052.
},
053.
dataType:
'json'
,
054.
type:
'DELETE'
055.
},
056.
parameterMap:
function
(options, operation) {
057.
if
(operation !==
"read"
&& options) {
058.
console.log(operation +
'*: '
+ kendo.stringify(options));
059.
return
{
060.
models: kendo.stringify(options)
061.
};
062.
}
063.
console.log(operation +
': '
+ kendo.stringify(options));
064.
return
options;
065.
}
066.
},
067.
schema: {
068.
data:
function
(data) {
069.
return
data[
'value'
];
070.
},
071.
total:
function
(data) {
072.
return
data[
'odata.count'
];
073.
},
074.
model: {
075.
id:
'Id'
,
076.
fields: {
077.
Title: { type:
'string'
}
078.
}
079.
}
080.
},
081.
change:
function
() {
082.
// We don't want to fire the first time the data loads because that counts as changed.
083.
if
(!firstSync)
084.
companyVM.set(
'hasChanges'
,
true
);
085.
else
086.
firstSync =
false
;
087.
}
088.
}),
089.
090.
// Properties.
091.
selectedItem:
null
,
092.
hasSelection:
function
() {
093.
return
this
.get(
'selectedItem'
) !=
null
;
094.
},
095.
hasChanges:
false
,
096.
097.
// Functions.
098.
clearSelection:
function
() {
099.
this
.set(
'selectedItem'
,
null
);
100.
$(
'#company-list'
).getKendoListView().clearSelection();
101.
},
102.
saveChanges:
function
() {
103.
this
.companies.sync();
104.
this
.set(
'hasChanges'
,
false
);
105.
},
106.
deleteSelection:
function
() {
107.
if
(confirm(
'Warning, deletion is permanent! Are you sure you wish to delete this item?'
)) {
108.
this
.companies.remove(
this
.selectedItem);
109.
this
.set(
'hasChanges'
,
true
);
110.
this
.clearSelection();
111.
}
112.
},
113.
114.
// Events.
115.
OnSelect:
function
(e) {
116.
var
list = $(e.sender.element).getKendoListView();
117.
var
row = list.select();
118.
var
item = list.dataSource.getByUid(row.data(
'uid'
));
119.
120.
this
.set(
'selectedItem'
, item);
121.
}
122.
});
123.
124.
kendo.bind($(
'#company-data'
), companyVM);
125.
});
126.
</script>