kendo MVC wrapper grid. How do i specify the data type of a nested model field? - model-view-controller

I have a project work work with hundreds of grids. On one grid in particular, the original designer created a view and added it as a nested model inside our driver model. This is a problem because Kendo seems to not be able to detect the field type of a field nested this way. When it can't, it assumes the field is a string.
In my particular case, I have two fields that contain dates that we want to filter on, but the date filtering is broken because of this.
I was able to find numerous examples of specifying it in the data source, but only for jquery grids (one example: https://docs.telerik.com/kendo-ui/knowledge-base/use-nested-model-properties ). I can't convert this to mvc because the methods aren't lining up.
Any idea on how to do this in mvc? Note: I also tried just changing the UI on the filter to date and that didn't work either. I got a stack trace claiming date needed to be a string.

As far as I understand the model passed to the Grid's field is structured like this:
public class OrderViewModel
{
public int OrderID
{
get;
set;
}
public CityModel ShipCity
{
get;
set;
}
}
public class CityModel
{
public int CityID
{
get;
set;
};
public string CityName
{
get;
set;
};
}
Then you can utilize the ClientTemplate configuration to get the CityName to populate the column:
columns.Bound(p => p.ShipCity).ClientTemplate("#=ShipCity.CityName#");
For more information refer to the Kendo UI Templates article
or
this forum answer that shows how to set a default to a nested field in a Grid's DataSource Model configuration.

Unfortunately, according to Telerik , the fact is that you simply can't do what I was trying to do. The solution is to flatten the model. I pulled the questionable fields into notmapped fields in the master model.
The relevant part of their response:
In MVC the nested properties are properties to another Model. As the Telerik UI Grid needs a flat structure of data, the nested properties could be shown with the help of a ClientTemplate, but they will always be string typed.
In order to achieve the desired behavior, I would recommend making the pointed Date fields - part of the Main Model and using them as flat data out of the box.
Link: https://www.telerik.com/forums/kendo-mvc-wrapper-grid-how-do-i-specify-the-data-type-of-a-nested-model-field
The caveat here is you can't filter a linq-for-queries datasource to a notmapped field because it doesn't exist in it but in my case, a .toList() didn't cause any performance problems because the dataset was small, with only a few hundred records.
However, you can edit the incoming DatasourceRequest object to modify the filters and sorts to point at the sub model, after your controller read is hit.

Related

OData EDM model - auto-expanding a nested Entity Type

I'm using OData v4 and the models are configured with the EdmModel Builder for our .NET Web API.
I have two models defined like this:
public class Customer
{
public int CustomerId { get; set; }
public string CustomerName { get; set; }
}
public class Order
{
public int OrderId { get; set; }
public string Category { get; set; }
public Customer OrderCustomer { get; set;}
}
These models have corresponding controllers and are registered as follows:
builder.EntitySet<Customer>("Customers")
.EntityType
.HasKey(x => x.CustomerId);
builder.EntitySet<Order>("Orders")
.EntityType
.HasKey(x => x.OrderId)
.Expand(
maxDepth: 2,
expandType: SelectExpandType.Automatic,
properties: nameof(Order.OrderCustomer));
I'm able to make OData requests to both these endpoints as follows:
/Customers/{Id} and
/Orders/{Id}.
I'm expecting that when I query the Orders, the nested EntitySet Customers will auto expand since I've set expandType: SelectExpandType.Automatic on the Order EntitySet. However, I can't get the CustomerOrder property to auto-expand on the Customer and I have to call the request with an expand parameter:
/Orders/{Id}?$expand=OrderCustomer.
I think this is because both Customer and Order are registered as EntitySets, so OData expects that an expand parameter be provided if they are nested. Is there a way to get the OrderCustomer property to auto-expand (i.e. without the need for the expand parameter to be provided)? My understanding of OData/ Edm Models is pretty elementary so any help is appreciated.
Your fluent configuration is correct, for OData v4, that will work for both collection and item queries.
If it is not working for you there are 3 possible issues:
You do not appear to be using the OData v4 URL convention for item queries, in v4 the expected URL is:
/Orders({Id})
This brings into question how you modified the router to support the v3 syntax, there are multiple variation on how to implement the v3 routes so it is possible that changes made in this area could affect the way that default expansion and selection is applied, or if it should apply.
You may not be including the navigation data in your data query. If the data is not retrieved from the data store, then it stands to reason that it will not be in the output recordset. If you are manually using the ODataQueryOptions.ApplyTo() to apply the user request to your query, then this will not take into account the configuration on the model, it will only apply thequery options that the caller has specified.
The caller might be specifying an Empty $expand= which will cancel out the auto configuration. Even if the originating caller has not specified any query options it is common enough for OData APIs to have standard or custom middleware running that might be manipulating the request query strings. To verify the URL is untampered, log it in your GET method handler and make sure the $expand is not specified.
As with the previous point, the ODataQueryOptions parameter in your GET method should NOT show any value for the SelectExpand if you want the auto configuration to be applied.
Finally, the last place to check is that you haven't overriden the default EnableQueryAttribute. If you have implemented your own custom implementation of EnableQueryAttribute then make sure that you still call the base implementation to correctly apply the ODataQueryOptions AND the schema defaults to the underlying IQueryable result.
In addition to the answer provided by Chris Schaller.
Another issue I had was caused by the casing on the property. For example, I have camelCasing enabled on the builder
builder.EnableLowerCamelCase();
This means the naming in the Expand on the configuration needed to be updated to match.
.Expand(
maxDepth: 2,
expandType: SelectExpandType.Automatic,
properties: "orderCustomer"); // <-- camelCase
This seems to be required even if EnablePropertyNameCaseInsensitive is enabled in the ODataOptions.

How do I bypass the limitations of what MVC-CORE controllers can pass to the view?

From what I've read, I'm supposed to be using ViewModels to populate my views in MVC, rather than the model directly. This should allow me to pass not just the contents of the model, but also other information such as login state, etc. to the view instead of using ViewBag or ViewData. I've followed the tutorials and I've had both a model and a viewmodel successfully sent to the view. The original problem I had was that I needed a paginated view, which is simple to do when passing a model alone, but becomes difficult when passing a viewmodel.
With a model of
public class Instructor {
public string forename { get; set; }
public string surname { get; set; }
}
and a viewmodel of
public class InstructorVM {
public Instructor Instructors { get; set; }
public string LoggedIn { get; set; }
}
I can create a paginated list of the instructors using the pure model Instructor but I can't pass InstructorVM to the view and paginate it as there are other properties that aren't required in the pagination LoggedIn cause issues. If I pass InstructorVM.Instructors to the view, I get the pagination, but don't get the LoggedIn and as this is just the model, I may has well have passed that through directly.
An alternative that was suggested was to convert/expand the viewmodel into a list or somesuch which would produce an object like this that gets passed to the view
instructor.forename = "dave", instructor.surname = "smith", LoggedIn="Hello brian"
instructor.forename = "alan", instructor.surname = "jones", LoggedIn="Hello brian"
instructor.forename = "paul", instructor.surname = "barns", LoggedIn="Hello brian"
where the LoggedIn value is repeated in every row and then retrieved in the row using Model[0].LoggedIn
Obviously, this problem is caused because you can only pass one object back from a method, either Instructor, InstructorVM, List<InstructorVM>, etc.
I'm trying to find out the best option to give me pagination (on part of the returned object) from a viewmodel while not replicating everything else in the viewmodel.
One suggestion was to use a JavaScript framework like React/Angular to break up the page into a more MVVM way of doing things, the problem with that being that despite looking for suggestions and reading 1001 "Best JS framework" lists via Google, they all assume I have already learned all of the frameworks and can thus pick the most suitable one from the options available.
When all I want to do is show a string and a paginated list from a viewmodel on a view. At this point I don't care how, I don't care if I have to learn a JS framework or if I can do it just using MVC core, but can someone tell me how to do this thing I could do quite simply in ASP.NET? If it's "use a JS framework" which one?
Thanks
I'm not exactly sure what the difficulty is here, as pagination and using a view model aren't factors that play on one another. Pagination is all about selecting a subset of items from a data store, which happens entirely in your initial query. For example, whereas you might originally have done something like:
var widgets = db.Widgets.ToList();
Instead you would do something like:
var widgets = db.Widgets.Skip((pageNumber - 1) * itemsPerPage).Take(itemsPerPage).ToList();
Using a view model is just a layer on top of this, where you then just map the queried data, no matter what it is onto instances of your view model:
var widgetViewModels = widgets.Select(w => new WidgetViewModel
{
...
});
If you're using a library like PagedList or similar, this behavior may not be immediately obvious, since the default implementation depends on having access to the queryset (in order to do the skip/take logic for you). However, PagedList, for example has StaticPagedList which allows you to create an IPagedList instance with an existing dataset:
var pagedWidgets = new StaticPagedList<WidgetViewModel>(widgetViewModels, pageNumber, itemsPerPage, totalItems);
There, the only part you'd be missing is totalItems, which is going to require an additional count query on the unfiltered queryset.
If you're using a different library, there should be some sort of similar functionality available. You'll just need to confer with the documentation.

Razor partial view prefix field names

When using Razor to render a form for a complex Model that has sub-models, we'd usually use Partial Views to render the sub-models.
Simple example Model:
public class BlogPost
{
public string Content { get; set; }
public List<Comment> Comments { get; set; }
}
public class Comment
{
public string Content { get; set; }
}
BlogPost.cshtml:
#model BlogPost
#Html.TextAreaFor(x => x.Content)
#for (int i = 0; i < Model.Comments.Count; i++)
{
#Html.Partial('Comment', Model.Comments[i])
}
Comment.cshtml:
#model Comment
#Html.TextAreaFor(x => x.Content)
Now for the issue:
Say we want to send the values of all fields to a controller action that takes BlogPost as a parameter. The fields are going to be posted back to the controller like so:
Content=The+content+of+the+BlogPost&Content=The+first+Comment&Content=The+second+Comment
But what we need to have MVC map them correctly to the BlogPost view model, we need this naming convention:
Content=The+content+of+the+BlogPost&Comments[0].Content=The+first+Comment&Comments[1].Content=The+second+Comment
How can this be achieved in a clean way? We can only think of two ways which both seem to compromise the design:
Either we pass the BlogPost as a model to the partial view such that we can define the text area like so: #Html.TextAreaFor(x => x.Comments[i].Content). But this means we couple the partial view for comments to the parent view model - you could think of a scenario where the same partial view should be used in a different context, which is not possible if the partial view depends on the parent view model. Futhermore, the i would have to be passed to the partial view somehow.
Or we fall back to explicitely defining the name of every single field with strings: #Html.TextArea(ViewBag.Prefix + ".Content").
Is there any way to tell the partial view to apply a certain prefix to all field names?
chiccodoro,
if you create and EditorFor template of type Comment, mvc will handle all of this beautifully for you. However, that will only work well in a scenario where the rows are already present in the DB. Exampel from SO:
Submiting Parent & Children in razor
If you need to create new rows on the fly, then you'll have to use a little bit of trickery to allow the fields to operate as required. I used an article from steven sandersons website which allows you to add collection items at runtime and still retains unobtrusive validation etc. see this SO question and related article ref:
Editing a Variable Length List, ASP.NET MVC 3 Style with Table

Automatical bind of datetime fields in MVC3

I have a page with many date fields, which can be dynamically added on client side. I have a DatTime? property on editor template for this field:
[Display(Name = "Bar Admission Date")]
public DateTime? AdmissionDate { get; set; }
When I'm submitting a form I get a null data in AdmissionDate field because binder doesn't know the format of the field.
I have 2 ideas of how t oovercome this issue:
Make a string field in model and parse it on a server side. Simple and pretty quick.
Write a custom model binder for date fields. I don't like this solution because I don't know the keys for all fields that I will use.
Is there better solution? I searched how can I overload TextboxFor method in order to pass it a culture, but I didn't find
Sounds like you should use an enumerable (IList/ICollection) of DateTime?. Phil Haacked has a good article on model binding to a list (even when the number of items is dynamic).
Updated
As for the formatting problem, I would look at how to set the culture for the project/model binder.

DropDownList Client Side Validation is validating when it should not be. (MVC3, Razor)

I am still learning MVC3 and Razor, so this is perhaps a simple question.
On a view I have a DropDownList whose sole purpose is to help filter (via AJAX) a second drop down list:
#Html.DropDownList("Segments", "-- select segment --")
There is a Segments property of the ViewModel that is defined as:
public IEnumerable<SelectListItem> Segments { get; set; }
There is JavaScript that handles the change event for this DropDownList and populates another DropDownList with appropriate values. That other DropDownList is defined like this:
#Html.DropDownListFor(m => m.fafhProdRecId, Enumerable.Empty<SelectListItem>(), "-- select product recommendation --")
This all works fine until I submit. When I submit, I get a validation error on the Segments drop down list!
Now -- there should be absolutely NO validation on the segments DropDownList -- there shouldn't be any client side validation on EITHER drop down list, for that matter.
But when I try to submit, I get the validation error message back:
The value '1' is invalid.
I have no idea why this is happening.
I have no idea how to decorate the Segments property to say that it is NOT required.
I have no idea how to tell the unobtrusive javascript validator that it is, in fact, being quite obtrusive.
In your ViewModel class add [Bind(Exclude = "Segments")]
From: Using Data Annotations for Model Validation
make sure that your Model has fafhProdRecId as nullable, I imagine it's declared as:
public int fafhProdRecId { get; set; }
change this to:
public int? fafhProdRecId { get; set; }
hopefully, that should resolve the issue as this effectively makes the model field nullable (assuming the db field IS nullable too of course).

Resources