Question

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:

{
  "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:

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:

public class Company {
    public Company() { }

    public Company(Company company) {
        this.Id = company.Id;
        this.Title = company.Title;
        this.Projects = company.Projects;
    }

    public int Id { get; set; }
    public string Title { get; set; }

    public virtual ICollection<Project> Projects { get; set; }
}

Controller:

public class CompanyController : ODataController
{
    private ApplicationDbContext db = new ApplicationDbContext();

    // GET odata/Company
    [Queryable]
    public IQueryable<Company> GetCompany()
    {
        return db.Companies;
    }

    // GET odata/Company(5)
    [Queryable]
    public SingleResult<Company> GetCompany([FromODataUri] int key)
    {
        return SingleResult.Create(db.Companies.Where(company => company.Id == key));
    }

    // PUT odata/Company(5)
    public async Task<IHttpActionResult> Put([FromODataUri] int key, Company company)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        if (key != company.Id)
        {
            return BadRequest();
        }

        db.Entry(company).State = EntityState.Modified;

        try
        {
            await db.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!CompanyExists(key))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }

        return Updated(company);
    }

    // POST odata/Company
    public async Task<IHttpActionResult> Post(Company company)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        db.Companies.Add(company);
        await db.SaveChangesAsync();

        return Created(company);
    }

    // PATCH odata/Company(5)
    [AcceptVerbs("PATCH", "MERGE")]
    public async Task<IHttpActionResult> Patch([FromODataUri] int key, Delta<Company> patch)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        Company company = await db.Companies.FindAsync(key);
        if (company == null)
        {
            return NotFound();
        }

        patch.Patch(company);

        try
        {
            await db.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!CompanyExists(key))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }

        return Updated(company);
    }

    // DELETE odata/Company(5)
    public async Task<IHttpActionResult> Delete([FromODataUri] int key)
    {
        Company company = await db.Companies.FindAsync(key);
        if (company == null)
        {
            return NotFound();
        }

        db.Companies.Remove(company);
        await db.SaveChangesAsync();

        return StatusCode(HttpStatusCode.NoContent);
    }

    // GET odata/Company(5)/Projects
    [Queryable]
    public IQueryable<Project> GetProjects([FromODataUri] int key)
    {
        return db.Companies.Where(m => m.Id == key).SelectMany(m => m.Projects);
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            db.Dispose();
        }
        base.Dispose(disposing);
    }

    private bool CompanyExists(int key)
    {
        return db.Companies.Count(e => e.Id == key) > 0;
    }
}

Finally, the KendoUI, HTML/Javascript is this:

<h2>Company List</h2>

<div id="company-data">
    <div class="col-md-3 col-sm-5 col-xs-5">
        <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>
        <div>
            <button class="btn btn-success btn-sm" id="btn-add-company"><span class="glyphicon glyphicon-plus"></span> Add</button>
            <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>
            <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>
            <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>
        </div>
    </div>
    <div class="col-md-9 col-sm-7 col-xs-7" data-bind="visible: hasSelection">
        <label for="company-title">Title:</label><br />
        <input id="company-title" data-bind="value: selectedItem.Title" ><br />
    </div>
</div>

<script type="text/x-kendo-template" id="list-template">
    <div class="company" style="cursor: pointer">
        <span data-bind="text: Title"></span>
    </div>
</script>

<script>
    $(function () {
        var firstSync = true;
        var companyVM = new kendo.observable({
            // Data Source.
            companies: new kendo.data.DataSource({
                type: 'odata',
                transport: {
                    create: {
                        url: '/odata/Company',
                        dataType: 'json',
                        type: 'POST'
                    },
                    read: {
                        url: '/odata/Company',
                        dataType: 'json'
                    },
                    update: {
                        url: function (data) {
                            return '/odata/Company(' + data.Id + ')';
                        },
                        dataType: 'json',
                        type: 'PATCH'
                    },
                    destroy: {
                        url: function (data) {
                            return '/odata/Company(' + data.Id + ')';
                        },
                        dataType: 'json',
                        type: 'DELETE'
                    },
                    parameterMap: function (options, operation) {
                        if (operation !== "read" && options) {
                            console.log(operation + '*: ' + kendo.stringify(options));
                            return {
                                models: kendo.stringify(options)
                            };
                        }
                        console.log(operation + ': ' + kendo.stringify(options));
                        return options;
                    }
                },
                schema: {
                    data: function (data) {
                        return data['value'];
                    },
                    total: function (data) {
                        return data['odata.count'];
                    },
                    model: {
                        id: 'Id',
                        fields: {
                            Title: { type: 'string' }
                        }
                    }
                },
                change: function () {
                    // We don't want to fire the first time the data loads because that counts as changed.
                    if (!firstSync)
                        companyVM.set('hasChanges', true);
                    else
                        firstSync = false;
                }
            }),

            // Properties.
            selectedItem: null,
            hasSelection: function () {
                return this.get('selectedItem') != null;
            },
            hasChanges: false,

            // Functions.
            clearSelection: function() {
                this.set('selectedItem', null);
                $('#company-list').getKendoListView().clearSelection();
            },
            saveChanges: function() {
                this.companies.sync();
                this.set('hasChanges', false);
            },
            deleteSelection: function () {
                if (confirm('Warning, deletion is permanent! Are you sure you wish to delete this item?')) {
                    this.companies.remove(this.selectedItem);
                    this.set('hasChanges', true);
                    this.clearSelection();
                }
            },

            // Events.
            OnSelect: function (e) {
                var list = $(e.sender.element).getKendoListView();
                var row = list.select();
                var item = list.dataSource.getByUid(row.data('uid'));

                this.set('selectedItem', item);
            }
        });

        kendo.bind($('#company-data'), companyVM);
    });
</script>
Was it helpful?

Solution

Question answered on Kendo support forums here.

The solution was to change the parameterMap function to:

parameterMap: function (data, operation) {
     return JSON.stringify(data);
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top