asp.net-mvc-4upshot

How to upload related entities via MVC4 upshot


I get a simple DTO entity A loaded into my upshot viewmodel which is happily viewable via Knockoutjs.

My DTO A contains a List entities. So I can foreach over the elements inside A.

again:

class A
    {
       someprop;
        List<B> childB;
    }
Class B
{
   somepropB;
}

So far so good. I can iterated over the data with no problem. But if I change "someprop" inside an instance of A and SaveAll the server will not respond at all. The updateData controlle method is not even invoked. If I clear the childB.Clear() before transmitting it to the client, all is fine.

It seems the upshot is not able to update entities with collections?


Solution

  • There is a bit of work to do if you want such a scenario to work. Upshot only turns the parent entities in observable items. So only the javascript representation of class A is a knockout observable, the javascript representation of class B is not. Therefore Upshot is not aware of any changes in associated objects.

    The solution is to map the entities manually. To make my life easier, I've used code from my 'DeliveryTracker' sample application in the code snippets below. In my blog article you can see an example of manual mapping: http://bartjolling.blogspot.com/2012/04/building-single-page-apps-with-aspnet.html so my examples below are working on the 'delivery' and 'customer' objects.

    The server-side domain model

    namespace StackOverflow.q9888839.UploadRelatedEntities.Models
    {
        public class Customer
        {
            [Key]
            public int CustomerId { get; set; }
    
            public string Name { get; set; }
            public string Address { get; set; }
            public double Latitude { get; set; }
            public double Longitude { get; set; }
    
            public virtual ICollection<Delivery> Deliveries { get; set; }
        }
    
        public class Delivery
        {
            [Key]
            public int DeliveryId { get; set; }
            public string Description { get; set; }
            public bool IsDelivered { get; set; }
    
            [IgnoreDataMember] //needed to break cyclic reference
            public virtual Customer Customer { get; set; }
            public virtual int CustomerId { get; set; }
        }
    
        public class AppDbContext : DbContext
        {
            public DbSet<Customer> Customers { get; set; }
            public DbSet<Delivery> Deliveries { get; set; }
        }
    }
    

    The data service controller

    The data service controller exposes the data conforming to OData standards on "http://localhost:[yourport]/api/dataservice/GetCustomers". In order to be able to update both customers and deliveries you need to define an UpdateCustomer AND UpdateDelivery function

    namespace StackOverflow.q9888839.UploadRelatedEntities.Controllers
    {
        public class DataServiceController : DbDataController<AppDbContext>
        {
            //Service interface for Customer
            public IQueryable<Customer> GetCustomers()
            {
                return DbContext.Customers.Include("Deliveries").OrderBy(x => x.CustomerId);
            }
            public void InsertCustomer(Customer customer) { InsertEntity(customer); }
            public void UpdateCustomer(Customer customer) { UpdateEntity(customer); }
            public void DeleteCustomer(Customer customer) { DeleteEntity(customer); }
    
            //Service interface for Deliveries
            public void InsertDelivery(Delivery delivery) { InsertEntity(delivery); }
            public void UpdateDelivery(Delivery delivery) { UpdateEntity(delivery); }
            public void DeleteDelivery(Delivery delivery) { DeleteEntity(delivery); }
        }
    }
    

    Client-side domain model

    Add a new javascript file containing your client-side model. Here I'm explicitly turning every property into an knockout observable. The key for solving your problem is the line inside the constructor of the Customer object where I'm mapping the incoming deliveries into an observable array

    /// <reference path="_references.js" />
    (function (window, undefined) {
    
        var deliveryTracker = window["deliveryTracker"] = {}; //clear namespace
    
        deliveryTracker.DeliveriesViewModel = function () {
            // Private
            var self = this;
            self.dataSource = upshot.dataSources.Customers;
            self.dataSource.refresh();
            self.customers = self.dataSource.getEntities();
        };
    
        deliveryTracker.Customer = function (data) {
            var self = this;
    
            self.CustomerId = ko.observable(data.CustomerId);
            self.Name = ko.observable(data.Name);
            self.Address = ko.observable(data.Address);
            self.Latitude = ko.observable(data.Latitude);
            self.Longitude = ko.observable(data.Longitude);
    
            self.Deliveries = ko.observableArray(ko.utils.arrayMap(data.Deliveries, function (item) {
                return new deliveryTracker.Delivery(item);
            }));
    
            upshot.addEntityProperties(self, "Customer:#StackOverflow.q9888839.UploadRelatedEntities.Models");
        };
    
        deliveryTracker.Delivery = function (data) {
            var self = this;
    
            self.DeliveryId = ko.observable(data.DeliveryId);
            self.CustomerId = ko.observable(data.CustomerId);
            self.Customer = ko.observable(data.Customer ? new deliveryTracker.Customer(data.Customer) : null);
            self.Description = ko.observable(data.Description);
            self.IsDelivered = ko.observable(data.IsDelivered);
    
            upshot.addEntityProperties(self, "Delivery:#StackOverflow.q9888839.UploadRelatedEntities.Models");
        };
    
        //Expose deliveryTracker to global
        window["deliveryTracker"] = deliveryTracker;
    })(window);
    

    The View

    In the index.cshtml you initialize Upshot, specify custom client mapping and bind the viewmodel

    @(Html.UpshotContext(bufferChanges: false)
                  .DataSource<StackOverflow.q9888839.UploadRelatedEntities.Controllers.DataServiceController>(x => x.GetCustomers())
                  .ClientMapping<StackOverflow.q9888839.UploadRelatedEntities.Models.Customer>("deliveryTracker.Customer")
                  .ClientMapping<StackOverflow.q9888839.UploadRelatedEntities.Models.Delivery>("deliveryTracker.Delivery")
    )
    <script type="text/javascript">
        $(function () {
            var model = new deliveryTracker.DeliveriesViewModel();
            ko.applyBindings(model);
        });
    </script>
    
    <section>
    <h3>Customers</h3>
        <ol data-bind="foreach: customers">
            <input data-bind="value: Name" />
    
            <ol data-bind="foreach: Deliveries">
                <li>
                    <input type="checkbox" data-bind="checked: IsDelivered" >
                        <span data-bind="text: Description" /> 
                    </input>
                </li>
            </ol>
        </ol>
    </section>
    

    The Results

    When navigating to the index page, the list of customers and related deliveries will be loaded asynchronously. All the deliveries are grouped by customer and are pre-fixed with a checkbox that is bound to the 'IsDelivered' property of a delivery. The customer's name is editable too since it's bound to an INPUT element

    I don't have enough reputation to post a screenshot so you will have to do without one

    When checking or unchecking the IsDelivered checkbox now, Upshot will detect the change and post it to the DataService Controller

    [{"Id":"0",
        "Operation":2,
        "Entity":{
            "__type":"Delivery:#StackOverflow.q9888839.UploadRelatedEntities.Models",
            "CustomerId":1,
            "DeliveryId":1,
            "Description":"NanoCircuit Analyzer",
            "IsDelivered":true
        },
        "OriginalEntity":{
            "__type":"Delivery:#StackOverflow.q9888839.UploadRelatedEntities.Models",
            "CustomerId":1,
            "DeliveryId":1,
            "Description":"NanoCircuit Analyzer",
            "IsDelivered":false
        }
    }]
    

    When modifying the customer's name, Upshot will submit the changes when the input box loses focus

    [{
        "Id": "0",
        "Operation": 2,
        "Entity": {
            "__type": "Customer:#StackOverflow.q9888839.UploadRelatedEntities.Models",
            "Address": "Address 2",
            "CustomerId": 2,
            "Latitude": 51.229248,
            "Longitude": 4.404831,
            "Name": "Richie Rich"
        },
        "OriginalEntity": {
             "__type": "Customer:#StackOverflow.q9888839.UploadRelatedEntities.Models",
            "Address": "Address 2",
            "CustomerId": 2,
            "Latitude": 51.229248,
            "Longitude": 4.404831,
            "Name": "Rich Feynmann"
        }
    }]
    

    So if you follow above procedure, Upshot will both update parent and child entities for you.