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.