Sproutcore datasources and creating new records with relationships - sproutcore

I'm trying to get my head around datasources and related models in sproutcore and am getting no where fast was wondering if anyone could maybe help me understand this all bit better.
Basically I have two related models Client and Brand, Clients can have many Brands and Brands can have a single Client, I have defined my models correctly and everything is pulling back as expected. The problem I'm having is working out how to create a new Brand and setup its relationship.
So on my Brand controller I have a createBrand method like so:
var brand = DBs.store.createRecord(DBs.Brand, {
title: this.get('title')
}, Math.floor(Math.random()*1000000));
brand.set('client', this.get('client'));
MyApp.store.commitRecords();
So as this is a new Brand I randomly generate a new ID for it (the second argument to createRecord). This is calling my createRecord in my datasource to create the new Brand, and then it also calls the updateRecord for the client.
The problem I'm having is that the clientUpdate is being passed the temporary (randomly generated id) in the relationship. How should I be structuring my creating of the new brand? Should I be waiting for the server to return the newly created brands ID and then updating the client relationship? If so how would I go about doing this?
Thanks
Mark

Right, after sitting in the sproutcore IRC channel and talking to mauritslamers he recommended creating a framework to handle all the server interactions for me manually.
So I setup a framework called CoreIo, which contains all my models, store and data source.
The data source is only used for fetching records from the server ie:
fetch: function(store, query) {
var recordType = query.get('recordType'),
url = recordType.url;
if (url) {
SC.Request.getUrl(CoreIo.baseUrl+url)
.header({ 'Accept': 'application/json'})
.json()
.notify(this, '_didFetch', store, query, recordType)
.send();
return YES;
}
return NO;
},
_didFetch: function (response, store, query, recordType) {
if (SC.ok(response)) {
store.loadRecords(recordType, response.get('body'));
store.dataSourceDidFetchQuery(query);
} else {
store.dataSourceDidErrorQuery(query, response);
}
},
Then the CoreIo framework has creation methods for my models ie:
CoreIo.createBrand = function (brand, client) {
var data = brand,
url = this.getModelUrl(CoreIo.Brand);
data.client_id = client.get('id');
SC.Request.postUrl(url)
.json()
.notify(this, this.brandDidCreate, client)
.send(data);
};
CoreIo.brandDidCreate = function (request, client) {
var json = request.get('body'),
id = json.id;
var ret = CoreIo.store.pushRetrieve(CoreIo.Brand, id, json);
var brand = CoreIo.store.find(CoreIo.Brand, id);
if (ret) {
client.get('brands').pushObject(brand);
}
};
I would then call into these 'actions' to create my new models which would setup the relationships as well.

Related

ASP.NET MVC + Dynamics NAV odata web services - how do I access related models?

I'm building an asp.net mvc application using Dynamics NAV Odata web services. Evertyhing is working fine and I created a controller for Service Orders using Linq queries. Then I got to the next step: accessing related models, and I'm stuck.
Lets take an example using page 5900 - Service Order and page 5903 - Service Item Lines:
Getting Service Orders or any other single model works great:
var query = from c in nav.ServiceOrder
select c;
But accessing related data fails:
var query = nav.ServiceOrder
.Expand(x => x.ServiceOrderServItemLines)
.Where(x => x.No == "SO000008");
I can access ServiceOrderServItemLines with the following url:
/DynamicsNAV71/OData/Company('the company')/ServiceOrder(Document_Type='Order',No='SO000008')/ServiceOrderServItemLines
But using expand does not seem to work.
Im not sure what the problem is. Are there no relations between the models?
If so, is there a way for me to add my own models with relations, and connect them to the odata service?
Or is it just a matter of expand not being supported in the service?
Any input would be much appreciated.
It seems that your serviceOrder has two keys. one is the Document_Type and one is the No. So please try
var query = nav.ServiceOrder
.Expand(x => x.ServiceOrderServItemLines)
.Where(x => x.Document_Type == "Order" && x.No == "SO000008");
If it doesn't solve your problem, Could you please provide the url sent by the expand query?
So after beating my head against this for some time I think I have an answer. I'll post my findings here, and hopefully it will save someone some trouble in the future.
Dynamics NAV (2013 R2) have relationships between header and list items as described here:
http://blogs.msdn.com/b/freddyk/archive/2009/05/28/handling-sales-orders-from-page-based-web-services-in-nav-2009sp1-and-rtm.aspx
In my case I want to create and access Service Item Lines for a Service Order.
Using SOAP to create Service Orders (Service Header table) and Service Item Lines can be done like this:
static void Main(string[] args)
{
ServiceOrder_Binding ctx = new ServiceOrder_Binding();
ctx.UseDefaultCredentials = true;
//Create a new Service Order
ServiceOrder so = NewSo(ctx);
//Add a couple of Service Item Lines to the Service Order
for (int i = 0; i < 5; i++)
NewSil(ctx, so.No);
}
private static ServiceOrder NewSo(ServiceOrder_Binding ctx)
{
ServiceOrder so = new ServiceOrder();
so.Customer_No = "50000";
so.Description = "New Service Order";
ctx.Create(ref so);
return so;
}
private static void NewSil(ServiceOrder_Binding ctx, string documentNo)
{
ServiceOrder so = ctx.Read(documentNo);
List<Service_Order_Line> SilList = so.ServItemLines.ToList();
Service_Order_Line Sil = new Service_Order_Line();
Sil.ServiceItemNo = "20";
Sil.Description = "New Service Item Line";
SilList.Add(Sil);
so.ServItemLines = SilList.ToArray();
ctx.Update(ref so);
}
Using Odata to read Service Orders (Service Header table) and Service Item Lines can be done like this:
static void Main(string[] args)
{
NAV ctx = new NAV(new Uri("http://localhost:7048/DynamicsNAV71/OData/Company('CRONUS Sverige AB')"));
ctx.UseDefaultCredentials = true;
//Eager loading - DOES NOT WORK!
var so = from s in ctx.ServiceOrder.Expand("ServiceOrderServItemLines")
where s.No == "SO000016"
select s;
//Lazy loading - WORKS!
var so2 = from s in ctx.ServiceOrder
where s.No == "SO000016"
select s;
ctx.LoadProperty(so2.First(), "ServiceOrderServItemLines");
}
Notice that lazy loading works, but eager loading doesn't.
For other related data, like Service Items for Service Item Lines, there doesn't seem to be any relationships. I will return with an update if I find something else, but what I ended up doing for now is passing related items in the ViewBag like this:
In the Controller Action:
var service_items = from s in ctx.ServiceItemList
where s.Customer_No.Equals(customerNo)
select s;
var serviceItemList = service_items.ToList();
ViewBag.serviceItemList = new SelectList(serviceItemList, "No", "Description");
In the View:
#Html.DropDownListFor(model => model.Service_Item_No, (IEnumerable<SelectListItem>)ViewBag.serviceItemList)
Hope this helps someone who like me is new to working with Dynamics NAV and Web Services :).

BreezeJS - Using expand

I am querying the server to get an entity with expand
function _loadIncidents() {
var deffered = Q.defer(),
queryObj = new breeze.EntityQuery().from('Incidents').expand(['Deployments', 'IncidentComments', 'DTasks', 'ExtendedProperties', 'IncidentEvents']);
dataRepository.fetchEntitiesByQuery(queryObj, true).then(function (incidents) {
var query = breeze.EntityQuery.from("DTasks"),
incidentIds = dataRepository.getEntitiesByQuerySync(query);
deffered.resolve();
}, function(err) {
deffered.reject(err);
});
return deffered.promise;
};
I am getting the results and all is fine, how ever when I query breeze cache to get the entities - I am getting empty collection. So when using expand does the expanded entities are added to the cache?
Yes the related entities identified in the expand should be in cache ... if the query is "correct" and the server interpreted your request as you intended.
Look at the payload of the response from the first request. Are the related entities present? If not, perhaps the query was not well received on the server. As a general rule, you want to make sure the data are coming over the wire before wondering whether Breeze is doing the right thing with those data.
I do find myself wondering about the spelling of the items in your expand list. They are all in PascalCase. Are they these the names of navigation properties of the Incident type? Or are they the names of the related EntityTypes? They need to be former (nav property names), not the latter.
I Had problem with the navigation property - as I am not using OData webapi not using EF , there is problem with the navigation properties so for the current time i just wrote
Object.defineProperty(this, 'Deployments', {
get: function () {
return (this.entityAspect && this.entityAspect.entityManager) ?
this.entityAspect.entityManager.executeQueryLocally(new breeze.EntityQuery("Deployments").
where('IncidentID', 'eq', this.IncidentID)) :
[];
},
set: function (value) { //used only when loading incidents from the server
if (!value.results) {
return;
}
var i = 0,
dataRepository = require('sharedServices/dataRepository');
for (i; i < value.results.length; i++) {
dataRepository.addUnchangedEntity('Deployment', value.results[i]);
}
},
enumerable: true
});

Breezejs EntityManager MetadataStore and fetchEntityByKey

I have a SPA application (durandaljs), and I have a specific route where I map the "id" of the entity that I want to fetch.
The template is "/#/todoDetail/:id".
For example, "/#/todoDetail/232" or "/#/todoDetail/19".
On the activate function of viewmodel, I get the route info so I can grab the id. Then I create a new instance of breezejs EntityManager to get the entity with the given id.
The problem is when I call manager.fetchEntityByKey("Todos", id), the EntityManager doesn't have yet the metadata from the server, so it throwing exception "Unable to locate an 'Type' by the name: Todos".
It only works if first I execute a query against the store (manager.executeQuery), prior to calling fetchEntityByKey.
Is this an expected behavior or a bug ? Is there any way to auto-fecth the metadata during instantiation of EntityManager ?
note: I believe it's hard to use a shared EntityManager in my case, because I want to allow the user directly type the route on the browser.
EDIT: As a temporary workaround, I'm doing this:
BreezeService.prototype.get = function (id, callback) {
var self = this;
function queryFailed(error) {
app.showMessage(error.message);
callback({});
}
/* first checking if metadatastore was already loaded */
if (self.manager.metadataStore.isEmpty()) {
return self.manager.fetchMetadata()
.then(function (rawMetadata) {
return executeQuery();
}).fail(queryFailed);
} else {
return executeQuery();
}
/* Now I can fetch */
function executeQuery() {
return self.manager.fetchEntityByKey(self.entityType, id, true)
.then(callback)
.fail(queryFailed);
}
};
You've learned about fetchMetadata. That's important. If you application can begin without issuing a query, you have to use fetchMetadata and wait for it to return before you can perform any operations directly on the cache (e.g., checking for an entity by key in the cache before falling back to a database query).
But I sense something else going on because you mentioned multiple managers. By default a new manager doesn't know the metadata from any other manager. But did you know that you can share a single metadataStore among managers? You can.
What I often do (and you'll see it in the metadata tests in the DocCode sample), is get a metadataStore for the application, write an EntityManager factory function that creates new managers with that metadataStore, and then use the factory whenever I'm making new managers ... as you seem to be doing when you spin up a ViewModel to review the TodoDetail.
Coming from a Silverlight background where I used a lot of WCF RIA Services combined with Caliburn Micro, I used this approach for integrating Breeze with Durandal.
I created a sub folder called services in the App folder of the application. In that folder I created a javascript file called datacontext.js. Here is a subset of my datacontext:
define(function (require) {
var breeze = require('lib/breeze'); // path to breeze
var app = require('durandal/app'); // path to durandal
breeze.NamingConvention.camelCase.setAsDefault();
// service name is route to the Web API controller
var serviceName = 'api/TeamData',
// manager is the service gateway and cache holder
manager = new breeze.EntityManager(serviceName),
store = manager.metadataStore;
function queryFailed(error) {
app.showMessage("Query failed: " + error.message);
}
// constructor overrides here
// included one example query here
return datacontext = {
getSponsors: function (queryCompleted) {
var query = breeze.EntityQuery.from("Sponsors");
return manager
.executeQuery(query)
.then(queryCompleted)
.fail(queryFailed)
}
};
}
Then in your durandal view models you can just require the services/datacontext. For example, here is part of a sample view model from my app:
define(function (require) {
var datacontext = require('services/datacontext');
var ctor = function () {
this.displayName = 'Sponsors',
this.sponsors = ko.observable(false)
};
ctor.prototype.activate = function () {
var that = this;
return datacontext.getSponsors(function (data) { that.sponsors(data.results) });
}
return ctor;
});
This will allow you to not worry about initializing the metadata store in every view model since it is all done in one place.

Entity Framework - Disconnexted Behavior in nTier

I am new to EF but I will try my best to describe the scenario. I have 3 tables in My DB namely RecommendationTopic, Recommendation and Question. Each RecommendationTopic can have multiple Recommendations and each Recommendation may have multiple questions. Assume that I already have predefined questions in my Question table.
I have one service that returns me list of questions like below:
public List<Question> FetchQuestions(int categoryID)
{
using (Entities context = new Entities())
{
questions = context.Questions.Where(i => i.ID >= 0).ToList();
}
}
I have another service which is used to create RecommendationTopic and Recommendation whose code is something like below:
public void ManageRecommendation(RecommendationTopic recommendationTopic)
{
using (Entities context = new Entities())
{
context.AddObject("RecommendationTopics", recommendationTopic);
context.SaveChanges();
}
}
My client code looks like below:
List<Question> questions;
using (QuestionServiceClient client = new QuestionServiceClient())
{
questions = client.FetchQuestions();
}
using (RecommendationServiceClient client = new RecommendationServiceClient())
{
RecommendationTopic rTopic = new RecommendationTopic();
rTopic.CategoryID = 3;
rTopic.Name = "Topic From Client";
Recommendation rCom = new Recommendation();
rCom.Text = "Dont!";
rCom.RecommendationTopic = rTopic;
rCom.ConditionText = "Some condition";
rCom.Questions.Add(questions[0]);
rCom.Questions.Add(questions[1]);
client.ManageRecommendation(rTopic);
}
Since the client makes 2 separate service calls, the context would be different for both the calls. When I try to run this and check the EF profiler, it not only generates query to insert into RecommendationTopic and Recommendation but also Question table!
I am sure this is caused due to different context for both the calls as when I execute a similar code within a single context, it works as it's supposed to work.
Question is, how do I make it work in a disconnected scenario?
My client could be Silverlight client where I need to fill a Question drop down with a separate call and save Recommendation topic in a separate call. For this reason I am using self tracking entities as well.
Any input appreciated!
-Vinod
If you are using STEs (self tracking entities) your ManageRecommendation should look like:
public void ManageRecommendation(RecommendationTopic recommendationTopic)
{
using (Entities context = new Entities())
{
context.RecommendationTopics.ApplyChanges(recommendationTopic);
context.SaveChanges();
}
}
Calling AddObject skips self tracking behavior of your entity. If you are not using STEs you must iterate through all questions and change their state to Unchanged:
public void ManageRecommendation(RecommendationTopic recommendationTopic)
{
using (Entities context = new Entities())
{
context.RecommendationTopics.AddObject(recommendationTopic);
foreach (var question in recommendationTopic.Questions)
{
context.ObjectStateManager.ChangeObjectState(recommendationTopic, EntityState.Unchanged);
}
context.SaveChanges();
}
}

what's the best practice to attach a entity object which is detached from anthoer ObjectContext?

As mentioned in the title, how many methods are available?
I just have this case: I get a entity object from one ObjectContext, and then I detach the entity obejct from OjbectContext object, and return it.
Later, if I make some changes on this object, and I want to save the changes back to database. I think I should write like this, right? (Well, this works for me.)
public Url GetOneUrl()
{
Url u;
using(ServicesEntities ctx = new ServicesEntities())
{
u = (from t in ctx.Urls select t).FirstOrDefault<Url>();
ctx.Detach(u);
}
return u;
}
public void SaveToDB(Url url)
{
using(ServicesEntities ctx = new ServicesEntities())
{
var t = ctx.GetObjectByKey(_Url.EntityKey) as Url;
ctx.Detach(t);
ctx.Attach(url);
ctx.ObjectStateManager.ChangeObjectState(url, System.Data.EntityState.Modified);
ctx.SaveChanges();
}
}
Url url = GetOneUrl();
url.UrsString = "http://google.com"; //I just change the content.
SaveToDB(url);
OR
public void SaveToDB(Url url)
{
using(ServicesEntities ctx = new ServicesEntities())
{
var t = ctx.GetObjectByKey(_Url.EntityKey) as Url;
t = url; //this will make t.UrlString becomes "http://google.com"
ctx.ApplyCurrentValues<Url>("Urls", t);
ctx.SaveChanges();
}
}
This way is also works for me.
The first way will generate sql statement to update all the columns of Url table, but the second method will provide a sql script only update the "UrlString" Columns.
Both of them will have to retrieve a temp entity object from database which is the 't' in above code.
Are there any other methods to achieve this purpose? Or other better method you know about it? Or any official solution about this topic?
Many Thanks.
I don't understand your first example. Why do you first get entity from ObjectContext? It is not needed because you have just created new instance of the context. You can just use:
public void SaveToDB(Url url)
{
using(ServicesEntities ctx = new ServicesEntities())
{
ctx.Attach(url);
ctx.ObjectStateManager.ChangeObjectState(url, System.Data.EntityState.Modified);
ctx.SaveChanges();
}
}
In your second example you can just call:
public void SaveToDB(Url url)
{
using(ServicesEntities ctx = new ServicesEntities())
{
var t = ctx.GetObjectByKey(_Url.EntityKey) as Url; // Ensures that old values are loaded
ctx.ApplyCurrentValues<Url>("Urls", url);
ctx.SaveChanges();
}
}
Now the difference between two approaches is clear. First approach (Attach) does not need to query the DB first. Second approach (ApplyCurrentValues) needs to query the DB first to get old values.
You can use two additional approaches. First is just extension of your former approach. It allows you defining which properties were changed. Second approach is manual synchronization with loaded entity. This approach doesn't use any special methods. You will simply set loaded entity's properties to required values manually. This approach is useful if you work with object graph instead of single entity because EF is not able to automatically synchronize changes in relations.

Resources