I would like to try this
Telerik Grid throw a circular reference exception when I try to use an entityframework poco class into his binding. The code mentioned in the link propose to replace the json serializer used by Telerik with the NewtonSoft one. But Telerik Grid never call the create method from CustomGridActionResultFactory injected into the Grid. Does someone know the problem about this code (link above)?
There is a code library project which shows how to create a custom GridActionResultFactory. You may find it helpful.
You should use a "special" View Model for the TelerikGrid.
For examle if you use a Database Model something like this
public class Master {
public int Id { get; set; }
public string Name { get; set; }
public List<Detail> Details { get; set; }
}
public class Detail {
public int Id { get; set; }
public string Name { get; set; }
public Master Master { get; set; }
}
You need to create View Model as given below
public class MasterView {
public int Id { get; set; }
public string Name { get; set; }
}
public class DetailView {
public int Id { get; set; }
public string Name { get; set; }
public int MasterId { get; set; }
}
If you like me, think that creating a separate view models just for the telerik grid is overkill, you could feed your own Json to the grid created from a list of anonymous objects:
//This goes in the View:
Html.Telerik().Grid(Model)
.Name("Grid")
.Columns(columns =>
{
// your column mappings go here
})
.DataBinding(dataBinding => dataBinding.Ajax().Select("_yourMethodReturningJson", "YourControllerName", new { yourJsonMethodParameter = yourViewModel.someField }))
.Pageable()
.Sortable()
.Render();
And the Json method in your controller looks like this:
public JsonResult _yourMethodReturningJson(YourType? yourJsonMethodParameter)
{
var list = database.SomeCollection.Select(x => new
{
SomeColumnName = x.SomeField
});
return Json(list, JsonRequestBehavior.AllowGet);
}
Your could use a better Json library here if you like: http://james.newtonking.com/pages/json-net.aspx
Related
There is a class (Model) User.cs:
namespace BookStore
{
using System;
using System.Collections.Generic;
public class User
{
public int user_id { get; set; }
public string user_login_name { get; set; }
public string user_first_name { get; set; }
public string user_last_name { get; set; }
}
}
I want to create a ViewModel to then use it in Views. How should I do it? I create ViewModels folder in the project, create a class UserViewModel.cs in it, then what? I just copy and paste the original User.cs content and put limitations? Like this:
namespace BookStore.Models
{
public class UserViewModel
{
[Editable(false)]
public int user_id { get; private set; }
public string user_login_name { get; set; }
public string user_first_name { get; set; }
public string user_last_name { get; set; }
}
}
I did so and now my UserController says it can't implicitly convert type BookStore.User to BookStore.UserViewModel:
public class UserController : Controller
{
private DBEntities db = new DBEntities();
public ActionResult UserDetails()
{
UserViewModel user = db.Users.FirstOrDefault(u => u.user_login_name==User.Identity.Name);
if (user == null)
return RedirectToAction("Create");
else
return View(user);
}
I know it is important in MVC to separate concerns, domain models and view models should be used for different purposes, but why is there no any detailed info on how to create ViewModels correctly?
Using Web API and OData, I have a service which exposes Data Transfer Objects instead of the Entity Framework entities.
I use AutoMapper to transform the EF Entities into their DTO counter parts using ProjectTo():
public class SalesOrdersController : ODataController
{
private DbContext _DbContext;
public SalesOrdersController(DbContext context)
{
_DbContext = context;
}
[EnableQuery]
public IQueryable<SalesOrderDto> Get(ODataQueryOptions<SalesOrderDto> queryOptions)
{
return _DbContext.SalesOrders.ProjectTo<SalesOrderDto>(AutoMapperConfig.Config);
}
[EnableQuery]
public IQueryable<SalesOrderDto> Get([FromODataUri] string key, ODataQueryOptions<SalesOrderDto> queryOptions)
{
return _DbContext.SalesOrders.Where(so => so.SalesOrderNumber == key)
.ProjectTo<SalesOrderDto>(AutoMapperConfig.Config);
}
}
AutoMapper (V4.2.1) is configured as follows, note the ExplicitExpansion() which prevents serialisation auto expanding navigation properties when they are not requested:
cfg.CreateMap<SalesOrderHeader, SalesOrderDto>()
.ForMember(dest => dest.SalesOrderLines, opt => opt.ExplicitExpansion());
cfg.CreateMap<SalesOrderLine, SalesOrderLineDto>()
.ForMember(dest => dest.MasterStockRecord, opt => opt.ExplicitExpansion())
.ForMember(dest => dest.SalesOrderHeader, opt => opt.ExplicitExpansion());
ExplicitExpansion() then creates a new problem where the following request throws an error:
/odatademo/SalesOrders('123456')?$expand=SalesOrderLines
The query specified in the URI is not valid. The specified type member 'SalesOrderLines' is not supported in LINQ to Entities
The navigation property SalesOrderLines is unknown to EF so this error is pretty much what I expected to happen. The question is, how do I handle this type of request?
The ProjectTo() method does have an overload that allows me to pass in an array of properties that require expansion, I found & modified the extension method ToNavigationPropertyArray to try and parse the request into a string array:
[EnableQuery]
public IQueryable<SalesOrderDto> Get([FromODataUri] string key, ODataQueryOptions<SalesOrderDto> queryOptions)
{
return _DbContext.SalesOrders.Where(so => so.SalesOrderNumber == key)
.ProjectTo<SalesOrderDto>(AutoMapperConfig.Config, null, queryOptions.ToNavigationPropertyArray());
}
public static string[] ToNavigationPropertyArray(this ODataQueryOptions source)
{
if (source == null) { return new string[]{}; }
var expandProperties = string.IsNullOrWhiteSpace(source.SelectExpand?.RawExpand) ? new List<string>().ToArray() : source.SelectExpand.RawExpand.Split(',');
for (var expandIndex = 0; expandIndex < expandProperties.Length; expandIndex++)
{
// Need to transform the odata syntax for expanding properties to something EF will understand:
// OData may pass something in this form: "SalesOrderLines($expand=MasterStockRecord)";
// But EF wants it like this: "SalesOrderLines.MasterStockRecord";
expandProperties[expandIndex] = expandProperties[expandIndex].Replace(" ", "");
expandProperties[expandIndex] = expandProperties[expandIndex].Replace("($expand=", ".");
expandProperties[expandIndex] = expandProperties[expandIndex].Replace(")", "");
}
var selectProperties = source.SelectExpand == null || string.IsNullOrWhiteSpace(source.SelectExpand.RawSelect) ? new List<string>().ToArray() : source.SelectExpand.RawSelect.Split(',');
//Now do the same for Select (incomplete)
var propertiesToExpand = expandProperties.Union(selectProperties).ToArray();
return propertiesToExpand;
}
This works for expand, so now I can handle a request like the following:
/odatademo/SalesOrders('123456')?$expand=SalesOrderLines
or a more complicated request like:
/odatademo/SalesOrders('123456')?$expand=SalesOrderLines($expand=MasterStockRecord)
However, more complicated request that try to combine $select with $expand will fail:
/odatademo/SalesOrders('123456')?$expand=SalesOrderLines($select=OrderQuantity)
Sequence contains no elements
So, the question is: am I approaching this the right way?
It feels very smelly that I would have to write something to parse and transform the ODataQueryOptions into something EF can understand.
It seems this is a rather popular topic:
odata-expand-dtos-and-entity-framework
how-to-specify-the-shape-of-results-with-webapi2-odata-with-expand
web-api-queryable-how-to-apply-automapper
how-do-i-map-an-odata-query-against-a-dto-to-another-entity
While most of these suggest using ProjectTo, none seem to address serialisation auto expanding properties, or how to handle expansion if ExplictExpansion has been configured.
Classes and Config below:
Entity Framework (V6.1.3) entities:
public class SalesOrderHeader
{
public string SalesOrderNumber { get; set; }
public string Alpha { get; set; }
public string Customer { get; set; }
public string Status { get; set; }
public virtual ICollection<SalesOrderLine> SalesOrderLines { get; set; }
}
public class SalesOrderLine
{
public string SalesOrderNumber { get; set; }
public string OrderLineNumber { get; set; }
public string Product { get; set; }
public string Description { get; set; }
public decimal OrderQuantity { get; set; }
public virtual SalesOrderHeader SalesOrderHeader { get; set; }
public virtual MasterStockRecord MasterStockRecord { get; set; }
}
public class MasterStockRecord
{
public string ProductCode { get; set; }
public string Description { get; set; }
public decimal Quantity { get; set; }
}
OData (V6.13.0) Data Transfer Objects:
public class SalesOrderDto
{
[Key]
public string SalesOrderNumber { get; set; }
public string Customer { get; set; }
public string Status { get; set; }
public virtual ICollection<SalesOrderLineDto> SalesOrderLines { get; set; }
}
public class SalesOrderLineDto
{
[Key]
[ForeignKey("SalesOrderHeader")]
public string SalesOrderNumber { get; set; }
[Key]
public string OrderLineNumber { get; set; }
public string LineType { get; set; }
public string Product { get; set; }
public string Description { get; set; }
public decimal OrderQuantity { get; set; }
public virtual SalesOrderDto SalesOrderHeader { get; set; }
public virtual StockDto MasterStockRecord { get; set; }
}
public class StockDto
{
[Key]
public string StockCode { get; set; }
public string Description { get; set; }
public decimal Quantity { get; set; }
}
OData Config:
var builder = new ODataConventionModelBuilder();
builder.EntitySet<StockDto>("Stock");
builder.EntitySet<SalesOrderDto>("SalesOrders");
builder.EntitySet<SalesOrderLineDto>("SalesOrderLines");
I have created an Automapper explicit navigation expansion utility function that should work with N-deph expands. Posting it here since it might help someone.
public List<string> ProcessExpands(IEnumerable<SelectItem> items, string parentNavPath="")
{
var expandedPropsList = new List<String>();
if (items == null) return expandedPropsList;
foreach (var selectItem in items)
{
if (selectItem is ExpandedNavigationSelectItem)
{
var expandItem = selectItem as ExpandedNavigationSelectItem;
var navProperty = expandItem.PathToNavigationProperty?.FirstSegment?.Identifier;
expandedPropsList.Add($"{parentNavPath}{navProperty}");
//go recursively to subproperties
var subExpandList = ProcessExpands(expandItem?.SelectAndExpand?.SelectedItems, $"{parentNavPath}{navProperty}.");
expandedPropsList = expandedPropsList.Concat(subExpandList).ToList();
}
}
return expandedPropsList;
}
You can call it with :
var navExp = ProcessExpands(options?.SelectExpand?.SelectExpandClause?.SelectedItems)
it will return a list with ["Parent" ,"Parent.Child"]
I never really managed to work this one out. The ToNavigationPropertyArray() extension method helps a little, but does not handle infinite depth navigation.
The real solution is to create Actions or Functions to allow clients to request data requiring a more complicated query.
The other alternative is to make multiple smaller/simple calls then aggregate the data on the client, but this isn't really ideal.
When you want to mark something for explicit expansion in AutoMapper, you need to also opt-back-in when calling ProjectTo<>().
// map
cfg.CreateMap<SalesOrderHeader, SalesOrderDto>()
.ForMember(dest => dest.SalesOrderLines, opt => opt.ExplicitExpansion());
// updated controller
[EnableQuery]
public IQueryable<SalesOrderDto> Get()
{
return _dbContext.SalesOrders
.ProjectTo<SalesOrderDto>(
AutoMapperConfig.Config,
so => so.SalesOrderLines,
// ... additional opt-ins
);
}
While the AutoMapper wiki does state this, the example is perhaps a little misleading by not including the paired ExplicitExpansion() call.
To control which members are expanded during projection, set ExplicitExpansion in the configuration and then pass in the members you want to explicitly expand:
I am a beginner ASP.net, I want to display all comment in post. Detail() method in PostController only shows post content, I don't know how to handle Detail() method to show commnent in post.
Hope someone can help me, Thank so much.
I have two model
public class Post
{
public int PostID { get; set; }
public string Title{ get; set; }
public string Content{ get; set; }
}
public class Comment
{
public int CommentID { get; set; }
public int PostID { get; set; }
public string Content { get; set; }
}
and this is Post Controller
public class PostController : Controller
{
//////
public ViewResult Detail(int id )
{
Post viewpost= (repository.Posts.Single(p => p.PostID == id));
return View(viewpost);
}
////////////////
}
Moralus has a good answer. If you don't want to change your Post model to have a navigation property to its comments, if out of your control or something, then you would have to make something like a ViewPostModel
public class ViewPostModel
{
public Post Post { get; set; }
public List<Comment> Comments { get; set; }
}
Then, you would have to query your repository for both lists:
public class PostController : Controller
{
public ViewResult Detail(int id )
{
ViewPostModel viewpost = new ViewPostModel();
viewPost.Post = repository.Posts.Single(p => p.PostID == id);
viewPost.Comments = repository.Comments.Where(c=> c.PostID = id).ToList();
return View(viewpost);
}
}
So first of all you should have a navigation property from post to it's comment.
something like this:
public class Post
{
public int PostID { get; set; }
public string Title{ get; set; }
public string Content{ get; set; }
public virtual IEnummrable<Comment> Comments {get; set;}
}
Note that if your using DB-First to do that you should edit EDMX
Then just take the comments from the repository like that:
public ViewResult Detail(int id )
{
Post viewpost= (repository.Posts.Include("Comments").Single(p => p.PostID == id));
return View(viewpost);
}
And in your view iterate trought your comments and display them like that
#foreach (var comment in Model.Comments)
{
#comment.Content
}
EDIT:
All of the above is relevant only if you use EF\ Linq-2-SQL (thanks #tostringtheory)
Also you can use the ViewData put the comments to the View.
eg:
Action:
ViewData["Comments"] = repository.Comments.Where(c=> c.PostID = id).ToList();
View:
var comments =ViewData["Comments"] as List;
...and you can use the var-comments
I'm having problems trying to validate a drop down list, I've looked at similar questions on here and tried the suggestions but still no luck. One I haven't tried is making my Benefit Id nullable, but is that a good idea? many thanks
Model I'm trying to validate:
public class Benefit
{
public int Id { get; set; }
public string Name { get; set; }
}
View model:
public class LookupVm
{
public SelectList Benefits { get; set; }
}
Controller set up:
var model = new LookupVm
{
Benefits = new SelectList(_repository.Benefits.OrderBy(n => n.Name).ToList(), "Id", "Name")
}
The view:
#Html.DropDownListFor(benefits => Model.Benefits.SelectedValue, Model.Benefits, "-Select-")
#Html.ValidationMessageFor(benefits => Model.Benefits.SelectedValue)
You can add a SelectedBenefit property to you view model
public class LookupVm
{
public int SelectedBenefit { get; set;}
public SelectList Benefits { get; set; }
}
Then add on top of the view
#model LookupVm
And then dropdown list must be something like this:
#Html.DropDownListFor(model => model.SelectedBenefit, model.Benefits, "-Select-")
#Html.ValidationMessageFor(model => model.SelectedBenefit)
You will get the selected id on SelectedBenefit property and it will be a required field.
Something very simple but I am looking for the best way to do it. I have a Movie entity, each Movie can be in one Language only(a lookup table with English, French,etc...). Now I'm trying to load all the available languages in the lookup in the Movie Create Page, the Movie View Model:
namespace Project.ViewModels {
public class Movie {
[Key]
public int ID { get; set; }
public string Title { get; set; }
public string Rating { get; set; }
public string Director { get; set; }
public string Plot { get; set; }
public string Link { get; set; }
public string Starring { get; set; }
public int DateCreated { get; set; }
public string Genre { get; set; }
[Display(Name = "Language")]
public int LanguageID { get; set; }
// Navigational Properties
public virtual MovieLanguage Language { get; set; }
}
}
The MovieLanguage View model:
namespace MAKANI.ViewModels {
public class MovieLanguage {
[Key]
public int ID { get; set; }
public string Language { get; set; }
public virtual ICollection<Movie> Movies { get; set; }
}
}
The controller action:
public ActionResult MovieCreate() {
using (MAKANI.Models.Entities db = new MAKANI.Models.Entities()) {
List<Models.MoviesLanguages> enLanguages = db.MoviesLanguages.ToList();
IEnumerable<SelectListItem> selectList =
from m in enLanguages
select new SelectListItem {
Text = m.Language,
Value = m.ID.ToString()
};
ViewBag.SelectLanguage = selectList.ToList();
return View();
}
}
And in the View page i have
<div class="editor-field">
#Html.DropDownList("Language", ViewBag.SelectLanguage);
</div>
Howver I am getting this error in the View:
'System.Web.Mvc.HtmlHelper' has no applicable method named 'DropDownList' but appears to have an extension method by that name. Extension methods cannot be dynamically dispatched. Consider casting the dynamic arguments or calling the extension method without the extension method syntax
Not sure what the problem might be?
Another questions regarding this approach:
Should a create a view model for the MovieLanguage entity in the first place, knowing that it servers only as a lookup table(so it doesnt require any Create/Edit/Delete action, Only List/Read maybe), should I be depending on the EF entities directly in that case?
Have a Languages Collection Property in your Movie ViewModel and a SelectedLanguage Property to get the selected Language ID when the form submits. It is not necessary that your ViewModel should have all the properties like your domain model. Have only those properties which the View needs.
public class Movie
{
public int ID { set;get;}
public string Title { set;get;}
//Other Relevant Properties also.
public IEnumerable<SelectListItem> Languages { set;get;}
public int SelectedLanguage { set;get;}
public Movie()
{
Languages =new List<SelectListItem>();
}
}
Now in your GET Action, Create an object of your Movie ViewModel and set the Languages Collection property and send that to the View. Try to avoid using ViewBag for passing data like this. ViewBag makes our code dirty.Why not use the strongly typed ViewModels to its full extent ?
public ActionResult CreateMovie()
{
var vm=new Movie();
// TO DO : I recommend you to abstract code to get the languages from DB
// to a different method so that your action methods will be
// skinny and that method can be called in different places.
var enLanguages = db.MoviesLanguages.ToList();
vm.Languages= = from m in enLanguages
select new SelectListItem {
Text = m.Language,
Value = m.ID.ToString()
};
return View(vm);
}
And in your view which is strongly typed to our Movie ViewModel, use the DropDownListFor Hemml helper method
#model Movie
#using(Html.Beginform())
{
#Html.DropDownListFor(x => x.SelectedLanguage,
new SelectList(Model.Languages, "Value", "Text"), "Select Language")
<input type="submit" />
}
Now when you post the form, you will get the selected languageId in the SelectedLanguage Property of your ViewModel
[HttpPost]
public ActionResult CreateMovie(Movie model)
{
If(ModelState.IsValid)
{
//check model.SelectedLanguage property here.
//Save and Redirect (PRG pattern)
}
//you need to reload the languages here again because HTTP is stateless.
return View(model);
}