I am trying to implement a tree view in my application. I am using MVC2 Preview 1, and SubSonic 3 SimpleRepository. I am new to both MVC and Linq.
My problem is that I am not sure how to add a list of child nodes to the model record that I am passing back to the View. So I have added a IEnumerable called Children to my model class that I populate in the controller action:
public class Category
{
public Category()
{
}
public int ID { get; set; }
public int? ParentId { get; set; }
[Required(ErrorMessage="Name is required")]
public string Name { get; set; }
[SubSonicIgnore]
public IEnumerable<Category> Children { get; set; }
}
Then in the controller action, I pull all of the records and iterate through updating the Children member:
public ActionResult Index()
{
var categories = _repo.All<Category>();
foreach (var c in categories)
{
c.Children = from p in _repo.All<Category>()
where p.ParentId == c.ID
orderby p.Name
select p;
}
return View(categories);
}
My 2 questions are #1 Why does this not work? Outside of the scope of the loop my changes are lost. #2 In a general sense, is this the right approach? Putting this code in the controller feels like a hack.
As for why it doesn't work, I suspect deferred execution is getting you. If you wrap the Linq query like so( from ... select p).ToList() it will cause the query to be evaluated.
Regarding the approach, it is data access in the presentation tier, so generally speaking, that would be something to avoid.
Related
I am trying to perform a query using Breeze that will return a filtered selection of child entities. I have two custom dtos defined as follows:
#region Dto Models
public class ProductDto {
public int ProductDtoId { get; set; }
public int ProductClassId { get; set; }
public ICollection<ProductRequiredInputDto> RequiredInputs { get; set; }
}
public class ProductRequiredInputDto
{
public int ProductRequiredInputDtoId { get; set; }
public string Product { get; set; }
public string Capacity { get; set; }
public string Electrical { get; set; }
//Navigation properties
public virtual ProductDto ProductDto { get; set; }
}
#endregion
My first query is to simply return a populated ProductDto model.
var query1a = this.entityQuery.from('ProductModel')
return this.entityManager.executeQuery(query1a) // returns a promise
.then(data => { this.product = data.results}
When I make a call to my web api controller everything works as expected as I receive a singular ProductDto model populated with a collection of ProductRequiredInputDto models. Here is a sample:
0: ProductDto__IPE_Data_DtoModels
ProductClassId: 1
ProductDtoId: 1
RequiredInputs: Array[40]
_backingStore: Object
ProductClassId: 1
ProductDtoId: 1
RequiredInputs: Array[40]
Now, what I am trying to achieve is to perform a second query on the ProductDto model that will return a filtered array of ProductRequiredDto models from the RequiredInputs property. I have looked over the Breeze examples and samples but have not been able to find a solution to this particular question.
Short answer: No I don't think you can filter on ICollection Navigation Properties from the EntityQuery.
Longer answer: You can write a custom method on the controller that uses .Include("RequiredInputs") and you can use LINQ to perform the filtering you want on the controller.
Aside: I find it peculiar that you don't have a ProductDtoID property on the ProductRequiredInputDto object.
Is it absolutely necessary to call the function that retrieves ProductDto? Because it doesn't sound logical to me. I would create a controller function:
[HttpGet]
public IQueryable<ProductRequiredInputDto> ProductRequiredInputDtos()
{
return _contextProvider.ProductRequiredInputDto;
}
And use a client side query in the lines of:
var idPredicate = breeze.Predicate.create('id', '==', yourSelectedProductDtoId);
var yourPredicate = breeze.Predicate.create('yourProductRequiredInputDtosProperty, 'yourOperator, 'yourValue');
var query = entityQuery.from('ProductRequiredInputDtos').where(idPredicate).and(yourPredicate);
Jonathan's method would also work, but then you have a specialized controller function for one type of call and those pile up quickly (unless you make them general by receiving params but that's another story). This way you can do any query on this model from your client without cluttering the controller up.
I have a database created by EF code-first, where there are two major classes: 'categories', and: 'products'.
Each category has a navigation property of a list of products (those that are associated with that category).
I've created a web api returning an 'IQueryable' - all the products associated with a specific category.
This is the api controller - which works just fine when called via a url:
[HttpGet]
public IQueryable<Product> ProductsFilteredByCategory(int categoryId)
{
return _contextProvider.Context.Categories.Include("Products").First(c => c.CategoryId == categoryId).Products.AsQueryable();
}
However, when I make a call from breeze via the following function:
var getProducts = function (productsObservable, parentCategoryId) {
var query = EntityQuery.from("ProductsFilteredByCategory")
.withParameters({ categoryId: parentCategoryId });
return manager.executeQuery(query)
.then(querySucceeded)
.fail(queryFailed);
}
I get the following error: 'undefined is not a function'.
Strangely, when I change the controller on the server side to return just a plain 'IQyeryable' list of all products - there are no errors... but this is not what I need - I only need those products which are associated with a specific category...
Thanks for any help !
Elior
You don't have to use the parameter in your controller method, you can remove the parameter and declare it in your breeze entity query with .where() the good thing about doing it this way is your controller doesn't have to be so specific.
Also, you don't need to use .include() as breeze allows you to expand the navigation property with .expand()
This leverages more of what breeze can help you with in your project.
Change ProductsFilteredByCategory(int categoryId) to
Public IQueryable<Product> Products()
{
return _contextProvider.Context.Products();
}
And your breeze query to
var query = EntityQuery.from('Products')
.where('categoryId', '==', categoryId)
.expand('Category');
If you don't want to do this, then I think you need to redo your controller method. You are supposed to be returning Product entity but you are querying for categories.
Thank you both very much for the support, I do appreciate it.
Kadumel, I could not use your solution because each 'Product' can belong to more than one 'Category' so there's no reference form 'Product' to 'Category'. However, I did change the controller method as you suggested and now things work.
Here's the code the represents everything so that hopefully it will also help others with similar situations. I'll be happy to hear and suggestions for improving the code.
public class Category
{
[Key]
public int CategoryId { get; set; }
[Required]
public string Name { get; set; }
public Int16? Order { get; set; }
[ForeignKey("ParentCategoryId")]
public Category ParentCategory { get; set; }
public int? ParentCategoryId { get; set; }
[ForeignKey("ParentCategory")]
[InverseProperty("ParentCategory")]
public List<Category> SubCategories { get; set; }
[ForeignKey("ProductId")]
public List<Product> Products { get; set; }
}
public class Product
{
[Key]
public int ProductId { get; set; }
[Required]
public string Name { get; set; }
public string Desc { get; set; }
public int Price { get; set; }
//[ForeignKey("CategoryId")]
//[InverseProperty("Products")]
//public List<Category> Categories { get; set; }
}
(as you can see I commented out the backwards list of links from 'Product' to 'Category' which could have been used as a backwards reference from 'Product' to 'Category' as Kdumel suggested I use these links, but it seems to me it's too 'heavey' to have so many references when I can do without them, do you agree?)
This is the code in the controller:
[HttpGet]
public IQueryable<Category> CategoryWithProducts(int categoryId)
{
return _contextProvider.Context.Categories.Include("Products").Where(c => c.CategoryId == categoryId);
}
and this is the breeze code:
var getProducts = function (productsObservable, parentCategoryId) {
var query = EntityQuery.from("CategoryWithProducts")
.withParameters( {categoryId: parentCategoryId } )
.select("Products");
return manager.executeQuery(query)
.then(querySucceeded)
.fail(queryFailed);
function querySucceeded(data) {
if (productsObservable) {
productsObservable(data.results[0].products);
}
log('Retrieved [Products] from remote data source',
data, true);
}
};
As you can see, the result is a single 'Category' from which I retrieve the 'products' in the 'querySuccedded()' function.
This works, but I was hoping to use a different approach which did NOT work: instead of passing 'parentCategoryId' I tried to pass the actual object and not the ID as 'parentCategoryObj', and then I thought of using the following line to load the 'products' navigation property without making an explicit call to the breeze controller:
parentCategoryObj.entityAspect.loadNavigationProperty("products")
However, this resulted in no 'products' being loaded as if the navigation property is null. Oddly, when I changed the word "products" to "subCategories" in this line just to check if navigation properties are the problem - the 'subCategories' data was loaded properly. I did not understand why one navigation property behaves differently from another (both are lists). I read more about this and noticed the currently Breeze does not support "Many to Many" relationships, I assume this is the reason, am I right?...
Thanks you guys again, It is a releif to know good people are willing to help !
Elior
I use breezejs in my Durandal web application.
Here is my code to get my invoice & lines behind it:
var getInvoiceById = function (invoiceId, invoiceObservable, forceRemote) {
// Input: invoiceId: the id of the invoice to retrieve
// Input: forceRemote: boolean to force the fetch from server
// Output: invoiceObservable: an observable filled with the invoice
if (forceRemote)
queryCacheInvoice = {};
var query = entityQuery.from('Invoices')
.where('id', '==', invoiceId)
.expand("Client, Client.Contacts, Lines")
.orderBy('Lines.Number');
var isInCache = queryCacheInvoice[invoiceId];
if (isInCache && !forceRemote) {
query = query.using(breeze.FetchStrategy.FromLocalCache);
} else {
queryCacheInvoice[invoiceId] = true;
query = query.using(breeze.FetchStrategy.FromServer);
}
return manager.executeQuery(query)
.then(querySucceeded)
.fail(queryFailed);
function querySucceeded(data) {
invoiceObservable(data.results[0]);
}
};
And here is the models for Invoice:
public class Invoice
{
[Key]
public int Id { get; set; }
public string Number { get; set; }
public DateTime? Date { get; set; }
public int? ClientId { get; set; }
public string Comment { get; set; }
public double? TotalExclVAT { get; set; }
public double? TotalInclVAT { get; set; }
public double? TotalVAT { get; set; }
public bool? WithoutVAT { get; set; }
public virtual List<InvoiceLine> Lines { get; set; }
public virtual Client Client { get; set; }
}
Please notice that for each invoice I have many invoice lines:
public virtual List<InvoiceLine> Lines { get; set; }
And here is the models for InvoiceLine:
public class InvoiceLine
{
[Key]
public int Id { get; set; }
[Required]
public int Number { get; set; }
[Required]
public string Description { get; set; }
public int InvoiceId { get; set; }
public Invoice Invoice { get; set; }
}
The problem: when I execute this breeze query I got the error below:
Error retreiving data. unable to locate property: Lines on type: Invoice
The problem is around the orderBy clause. I have a 1-to-many relationship between the Invoice and the InvoiceLine so it seems I cannot perform an order by in this case.
My question: how to proceed to be able to sort my lines of invoice by number?
Thanks.
Short answer: You can't. This is a limitation of Entity Framework, not Breeze.
You cannot filter, select, or order the related entities that you include with "expand" in an EF LINQ query.
You will probably manage the sort order of related entities on the client, e.g., the display of your order line items.
Note also that the collection of entities returned by a Breeze navigation path is unordered. I wasn't sure what happens if you tried to sort a Breeze entity navigation collection (e.g., Order.LineItems). I was afraid that would cause Breeze to think that you had made changes to the entities ... because a sort would seem to remove-and-add entities to the collection as it sorted. Your EntityManager would think there were changes pending when, in fact, nothing of substance has changed.
I tried an experiment and it all seems to work fine. I did something like this with Northwind:
fetched the Orders of a Customer ("Let's Stop N Shop")
checked the sequence of cust.Orders(); they have unordered OrderIDs: [10719, 10735, 10884, 10579]
executed a line like this: cust.Orders().sort(function(left, right){return left.OrderID() < right.OrderID()? -1 : 1})
checked the sequence of cust.Orders() again; this time they are sorted: [10579, 10719, 10735, 10884]
checked the customer's EntityManager.hasChanges() ... still false (no changes).
I confess that I am happily surprised. I need to write a proper test to ensure that this works reliably. And I have to make sure that the Knockout binding to the navigation property displays them in the sorted order. But I'm encouraged so far.
Important Notes:
Breeze won't keep the list sorted. You'll have to do that if you add new orders or if Breeze adds new orders to the collection as a result of subsequent queries.
Your sort affects every view that is bound to this navigation property. If you want each view to have its own sort of the entities in that collection, you'll have to maintain separate, view-specific collections that shadow the navigation property collection.
If I am wrong about all of this, you'll have to manage a shadow collection of the related entities, sorted as you wish, for each ViewModel.
Update 2 June
I suspected that we would have to let KO know about the array change after sort by calling valueHasMutated. I took 15 minutes for an experiment. And it seems to work fine.
I created a new KO Todo app from the ASP.NET SPA template (there's currently a phantom complaint about a missing jQuery.UI library which is totally unnecessary anyway).
I added a "sort link" to the index.cshtml just above the delete-TodoList link:
Sort
Then I implemented it in viewModel.js:
var sortAscending = true;
var viewmodel = {
...
sortList: sortList,
...
};
...
function sortList(list) {
list.todos().sort(function(left, right) {
return (sortAscending ? 1 : -1) *
(left.title().toLowerCase() < right.title().toLowerCase() ? -1 : 1);
});
sortAscending = !sortAscending; // reverse sort direction
list.todos.valueHasMutated(); // let KO know that we've sorted
}
Works like a charm. Sorting, adding, deleting Todos at will. The app is saving when expected as I add and delete ... but not during save.
Give valueHasMutated a try.
I have a simple Parent/Child table and I want to populate with one SQL statement using Entity Framework. Something like this:
public class Parent
{
public long ParentId { get; set; }
public virtual List<Child> Children { get; set; }
}
public class Child
{
public long ChildId { get; set; }
public long ParentId { get; set; }
public DateTime DateCreated { get; set; }
}
public Parent GetParent (long parentId)
{
IQueryable<Parent> query =
(from a in db.Parents.Include("Children")
where a.ParentId == parentId
select a);
return query.FirstOrDefault();
}
This seems to work in fetching the Parent and all Children in one SQL Statement. What I want to do now is order the children list by the DateCreated field, but I can't figure out how to do that. I've seen posts about using ThenBy() but don't see how to inject in the above Linq. Is there a better way I should be formulating the Linq?
-shnar
Unfortunately this is not supported. You cannot order included records by Linq-to-entities. Only parent records can be ordered. But you can still do something like:
var parent = GetParent(id);
var orderedChildren = parent.Children.OrderBy(c => c.DateCreated);
I have the following code in my controller:
public ActionResult Details(int id)
{
var dataContext = new BuffetSuppliersDBDataContext();
var supplier = (from m in dataContext.BO_Suppliers
where m.SupplierID == id
select m).FirstOrDefault();
ViewData.Model = supplier;
return View();
}
This renders a view which contains the properties returned from the linq to sql query. What I need to do now is add another query which will return x amount of ratings for each supplier, i will then loop through the records in the view and display the ratings.
How can I push the results of my ratings query into the view along with what is already there?
Your best option would to be create a class that you can pass into your view.
public class SupplierDetail
{
public Supplier { get; set; }
public SupplierRating { get; set; }
}
public class SupplierDetailViewData
{
public IEnumerable<SupplierDetail> SupplierDetails { get; set; }
}
Then in your controller action use a join and select a new SupplierDetail class in the LINQ query. After that you can create a strongly-typed view by using the code-behind and changing it to this...
public partial class Details : ViewPage<SupplierDetailViewData>
{
}
After that, in your view -- ViewData.Model will be SupplierDetailViewData. Of course the second part is optional but it does make for better compile-time validation.