I'm trying to find an example of a Kendo Grid MVC server wrapper using a Web API 2 controller and not having much luck. Is there a way to use those server wrappers with the controller generated by VS using "Add" -> "Controller" -> "Web API 2 Controller with actions, using Entity Framework?" Does using async controller actions affect this?
I've looked at the tutorials at Telerik (http://docs.telerik.com/kendo-ui/tutorials/asp.net/asp-net-hello-services) and at the sample project (http://www.telerik.com/support/code-library/binding-to-a-web-apicontroller-6cdc432b8326), but neither of those are using the new Web API 2 controllers that VS can automatically generate.
I'd prefer to be able to use the VS generated controllers, but my skills are apparently not up to adapting the existing examples to them (this is my first project with MVC/Web API). Has anyone else done this or should I just write controllers like in those two-year-old examples?
ETA: Just to include the starting point I've currently got:
I created models using EF Code First from database. The simple one I'm using to test this with is this one:
namespace TestProject.Models
{
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
[Table("TP.Industry")]
public partial class Industry
{
public Industry()
{
Companies = new HashSet<Company>();
}
public int IndustryId { get; set; }
public int IndustryCode { get; set; }
[Required]
[StringLength(150)]
public string IndustryName { get; set; }
public virtual ICollection<Company> Companies { get; set; }
}
}
I then created controllers using the "Web API 2 Controller with actions, using Entity Framework" option and checked "Use async controller actions" to get the following controller for that model:
using System;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Description;
using TestProject.Models;
namespace TestProject.Controllers
{
public class IndustryController : ApiController
{
private TestProjectContext db = new TestProjectContext();
// GET api/Industry
public IQueryable<Industry> GetIndustries()
{
return db.Industries;
}
// GET api/Industry/5
[ResponseType(typeof(Industry))]
public async Task<IHttpActionResult> GetIndustry(int id)
{
Industry industry = await db.Industries.FindAsync(id);
if (industry == null)
{
return NotFound();
}
return Ok(industry);
}
// PUT api/Industry/5
public async Task<IHttpActionResult> PutIndustry(int id, Industry industry)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (id != industry.IndustryId)
{
return BadRequest();
}
db.Entry(industry).State = EntityState.Modified;
try
{
await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!IndustryExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return StatusCode(HttpStatusCode.NoContent);
}
// POST api/Industry
[ResponseType(typeof(Industry))]
public async Task<IHttpActionResult> PostIndustry(Industry industry)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
db.Industries.Add(industry);
await db.SaveChangesAsync();
return CreatedAtRoute("DefaultApi", new { id = industry.IndustryId }, industry);
}
// DELETE api/Industry/5
[ResponseType(typeof(Industry))]
public async Task<IHttpActionResult> DeleteIndustry(int id)
{
Industry industry = await db.Industries.FindAsync(id);
if (industry == null)
{
return NotFound();
}
db.Industries.Remove(industry);
await db.SaveChangesAsync();
return Ok(industry);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
db.Dispose();
}
base.Dispose(disposing);
}
private bool IndustryExists(int id)
{
return db.Industries.Count(e => e.IndustryId == id) > 0;
}
}
}
Finally, I put the following Kendo UI MVC wrapper code in my view for the grid:
#(Html.Kendo().Grid<TestProject.Models.Industry>()
.Name("Grid")
.Columns(columns =>
{
columns.Bound(c => c.IndustryId);
columns.Bound(c => c.IndustryCode);
columns.Bound(c => c.IndustryName);
columns.Command(c =>
{
c.Edit();
c.Destroy();
});
})
.ToolBar(tools =>
{
tools.Create();
})
.Sortable()
.Pageable(pageable => pageable
.Refresh(true)
.PageSizes(true)
.ButtonCount(5))
.Filterable()
.DataSource(dataSource => dataSource
.Ajax()
.Model(model =>
{
model.Id(c => c.IndustryId);
})
.Read(read => read.Url("../api/Industry").Type(HttpVerbs.Get))
.Create(create => create.Url("../api/Industry").Type(HttpVerbs.Post))
.Update(update => update.Url("../api/Industry").Type(HttpVerbs.Put))
.Destroy(destroy => destroy.Url("../api/Industry").Type(HttpVerbs.Delete))
)
)
<script>
$(function () {
var grid = $("#Grid").data("kendoGrid");
// WebAPI needs the ID of the entity to be part of the URL e.g. PUT /api/Product/80
grid.dataSource.transport.options.update.url = function (data) {
return "api/Industry/" + data.IndustryId;
}
// WebAPI needs the ID of the entity to be part of the URL e.g. DELETE /api/Product/80
grid.dataSource.transport.options.destroy.url = function (data) {
return "api/Industry/" + data.IndustryId;
}
});
</script>
The grid returns no data and request for the api returns this 500 Internal Server Error:
This XML file does not appear to have any style information associated with it. The document tree is shown below.
<Error>
<Message>An error has occurred.</Message>
<ExceptionMessage>
The 'ObjectContent`1' type failed to serialize the response body for content type 'application/xml; charset=utf-8'.
</ExceptionMessage>
<ExceptionType>System.InvalidOperationException</ExceptionType>
<StackTrace/>
<InnerException>
<Message>An error has occurred.</Message>
<ExceptionMessage>
Type 'System.Data.Entity.DynamicProxies.Industry_5BBD811C8CEC2A7DB96D23BD05DB137D072FDCC62C2E0039D219F269651E59AF' with data contract name 'Industry_5BBD811C8CEC2A7DB96D23BD05DB137D072FDCC62C2E0039D219F269651E59AF:http://schemas.datacontract.org/2004/07/System.Data.Entity.DynamicProxies' is not expected. Consider using a DataContractResolver or add any types not known statically to the list of known types - for example, by using the KnownTypeAttribute attribute or by adding them to the list of known types passed to DataContractSerializer.
</ExceptionMessage>
<ExceptionType>
System.Runtime.Serialization.SerializationException
</ExceptionType>
<StackTrace>
at System.Runtime.Serialization.XmlObjectSerializerWriteContext.SerializeAndVerifyType(DataContract dataContract, XmlWriterDelegator xmlWriter, Object obj, Boolean verifyKnownType, RuntimeTypeHandle declaredTypeHandle, Type declaredType) at System.Runtime.Serialization.XmlObjectSerializerWriteContext.SerializeWithXsiTypeAtTopLevel(DataContract dataContract, XmlWriterDelegator xmlWriter, Object obj, RuntimeTypeHandle originalDeclaredTypeHandle, Type graphType) at System.Runtime.Serialization.DataContractSerializer.InternalWriteObjectContent(XmlWriterDelegator writer, Object graph, DataContractResolver dataContractResolver) at System.Runtime.Serialization.DataContractSerializer.InternalWriteObject(XmlWriterDelegator writer, Object graph, DataContractResolver dataContractResolver) at System.Runtime.Serialization.XmlObjectSerializer.WriteObjectHandleExceptions(XmlWriterDelegator writer, Object graph, DataContractResolver dataContractResolver) at System.Runtime.Serialization.XmlObjectSerializer.WriteObjectHandleExceptions(XmlWriterDelegator writer, Object graph) at System.Runtime.Serialization.DataContractSerializer.WriteObject(XmlWriter writer, Object graph) at System.Net.Http.Formatting.XmlMediaTypeFormatter.WriteToStream(Type type, Object value, Stream writeStream, HttpContent content) at System.Net.Http.Formatting.XmlMediaTypeFormatter.WriteToStreamAsync(Type type, Object value, Stream writeStream, HttpContent content, TransportContext transportContext, CancellationToken cancellationToken) --- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd(Task task) at System.Runtime.CompilerServices.TaskAwaiter.GetResult() at System.Web.Http.WebHost.HttpControllerHandler.<WriteBufferedResponseContentAsync>d__1b.MoveNext()
</StackTrace>
</InnerException>
</Error>
In fact, if I just scaffold a view on that model, like this:
#model IEnumerable<TestProject.Models.Industry>
#{
ViewBag.Title = "Industries";
}
<h2>Industries</h2>
<p>
#Html.ActionLink("Create New", "Create")
</p>
<table class="table">
<tr>
<th>
#Html.DisplayNameFor(model => model.IndustryCode)
</th>
<th>
#Html.DisplayNameFor(model => model.IndustryName)
</th>
<th></th>
</tr>
#foreach (var item in Model) {
<tr>
<td>
#Html.DisplayFor(modelItem => item.IndustryCode)
</td>
<td>
#Html.DisplayFor(modelItem => item.IndustryName)
</td>
<td>
#Html.ActionLink("Edit", "Edit", new { id=item.IndustryId }) |
#Html.ActionLink("Details", "Details", new { id=item.IndustryId }) |
#Html.ActionLink("Delete", "Delete", new { id=item.IndustryId })
</td>
</tr>
}
</table>
The "#foreach (var item in Model) {" line generates a "NullReferenceException: Object reference not set to an instance of an object." error.
It seems like even if the Kendo part wasn't right, at the very least all of the VS generated code should work correctly and it doesn't seem to be.
So, based on my research so far, there just don't seem to be any examples of using the Kendo MVC wrappers with Web API 2 controllers with async actions.
However, there now seems to be a Web API Editing section in the Kendo UI Getting Started docs (that I swear wasn't there before) that has a more up to date example that doesn't rely on adding the DataSourceRequestModelBinder.cs file to the project.
The controller setup is basically like the previous examples, but the .DataSource on the grid wrapper now looks like this:
.DataSource(dataSource => dataSource
.WebApi()
.Model(model =>
{
model.Id(i => i.IndustryId);
model.Field(i => i.IndustryId).Editable(false);
})
.Create(create => create.Url(Url.HttpRouteUrl("DefaultApi", new { controller = "Industries" })))
.Read(read => read.Url(Url.HttpRouteUrl("DefaultApi", new { controller = "Industries" })))
.Update(update => update.Url(Url.HttpRouteUrl("DefaultApi", new { controller = "Industries", id = "{0}" })))
.Destroy(destroy => destroy.Url(Url.HttpRouteUrl("DefaultApi", new { controller = "Industries", id = "{0}" })))
)
That .WebApi() method just wasn't showing up in any of my searches. This seems to work though, so I'm going to run with it. Thanks, all!
Here you can find a repository with more examples regarding the MVC technology. There should be an example that you are searching for.
Related
I want to check if username already exists in database using telerik mvc grid (batch edit). But The problem is in order to validate it, I need to pass applconst_id as a parameter in CheckDuplicateType function. But it always return "undefined", I don't know how to get the right value.
this is my model class:
public class masterTypeCont
{
[Key]
public Int32 applconst_id { get; set; }
[Required(ErrorMessage = "This field needs a value!")]
[Remote("CheckDuplicateType",
"Type",
AdditionalFields = "applconst_id",
ErrorMessage = "Code already exists.")]
public String note1 { get; set; }
}
and this is my controller:
[HttpGet]
public virtual JsonResult CheckDuplicateType(masterTypeCont tipe, String applconst_id)
{
Int32 intID = Convert.ToInt32(applconst_id);
return Json(typeEnt.ValidateCustomer(intID, tipe.note1), JsonRequestBehavior.AllowGet);
}
And this is ValidateCustomer function:
public bool ValidateCustomer(Int32 id, String nama) {
Int32 x = db.appl_const.Where(a =>
a.group_id == "CH_COMP_CODE" &&
a.note1.Trim() == nama.Trim() &&
a.applconst_id != id).Count();
Boolean res = x == 0 ? true : false;
return res;
}
This is my view:
#{
ViewBag.Title = "Type List";
Layout = "../Shared/_Layout.cshtml";
}
<div class="direct-content">
#using (Html.BeginForm())
{
#(Html.Telerik().Grid<ComplaintHandling.Models.masterTypeCont>()
.Name("TypeGrid")
.Localizable("id-ID")
.ToolBar(commands =>
{
commands.Insert();
commands.SubmitChanges();
})
.DataKeys(keys => keys
.Add(o => o.applconst_id)
.RouteKey("applconst_id"))
.DataBinding(dataBinding =>
{
dataBinding.Ajax()
.Select("_SelectAllType", "Type")
.Update("_SaveBatchType", "Type");
})
.Columns(columns =>
{
columns.Bound(o => o.applconst_id).Hidden();
columns.Bound(o => o.note1).Title("Name");
columns.Command(commands =>
{
commands.Delete().ButtonType(GridButtonType.Text);
}).Title("Command");
})
.ClientEvents(events => events
.OnEdit("OnEditGrid")
)
.Editable(editing => editing.Mode(GridEditMode.InCell))
.Selectable()
.Pageable(pager => pager.PageSize(15))
.Filterable()
.Sortable()
.Scrollable(x => x.Height("450px"))
.KeyboardNavigation()
.Resizable(x => x.Columns(true))
)
<input type="hidden" name="applconst_id" id="applconst_id" value="1">
}
</div>
I write hidden input there to contains the applconst_id, but still it value doesn't passed to controller.
Sorry for my bad english.
Any help will be deeply appreciated.
Thanks.
Try like this:
[HttpGet]
public virtual ActionResult CheckDuplicateType(masterTypeCont tipe)
{
var result = typeEnt.ValidateCustomer(tipe.applconst_id, tipe.note1);
return Json(result, JsonRequestBehavior.AllowGet);
}
Thanks for the answers before.
After doing some research, I finally found solution for my own problem.
I create a little trick there, maybe this isn't a good way, but at least it can solve my problem.
Basically, I just play with the name attribute of an element so that it value can be passed to controller.
Here, I will explain in detail how I've done this.
First, I changed Grid columns in my View to be like this:
Please concern on applconst_id hidden field. I add client template there.
columns.Bound(o => o.applconst_id).Hidden()
.ClientTemplate("<input class='typeID' name='common_id' value='<#= applconst_id #>'>");
columns.Bound(o => o.note1).Title("Nature of complaint");
Second, I add this code in "onEdit" function:
//this code change ID that is being edited, so that it can be passed to controller
var $inputID = $('.t-grid-edit-cell').prev().find('.typeID');
$inputID.attr('name', 'applconst_id');
//change back ID that is not being edited, so that it's not passed to controller
$('td:not(.t-grid-edit-cell)').prev().find('.typeID').attr('name','common_id');
Third, this is my controller:
[HttpGet]
public JsonResult CheckDuplicateType(String applconst_id, String note1)
{
Int32 IntID = Convert.ToInt32(applconst_id);
return Json(typeEnt.ValidateCustomer(IntID, note1), JsonRequestBehavior.AllowGet);
}
And my "ValidateCustomer" function stay the same.
I hope my solution can help other people who have the same problem with me.
Thanks.
Regards,
L.W.
I am populating DropDownList in View and getting this error on POST.
Error:
There is no ViewData item of type 'IEnumerable' that has the key 'SelectedCityName'.
Controller:
public ViewResult Register()
{
var Cities = ILocaRepo.Cities.ToList();
var Wards = ILocaRepo.Wards.ToList() ;
var model = new RegisterViewModel
{
City = Cities.Select(x => new SelectListItem
{
Value = x.CityID.ToString(),
Text = x.CityName
}),
Ward = Wards.Select(x => new SelectListItem
{
Value = x.WardID.ToString(),
Text = x.WardName
})
};
return View(model);
}
//
// POST: /Account/Register
[HttpPost]
public ActionResult Register(RegisterViewModel model)
{
if (ModelState.IsValid)
{
// Attempt to register the user
try
{
MembershipService.CreateUser(model.User.Username, model.User.Password, model.User.Name, model.SelectedCityName, model.SelectedWardName, model.User.Address, model.User.Phone, model.User.Email, "Member");
FormsAuthentication.SetAuthCookie(model.User.Username, false);
return RedirectToAction("ListProduct", "Product");
}
catch (ArgumentException ae)
{
ModelState.AddModelError("", ae.Message);
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
View :
<tr>
<td class="info_label">City</td>
<td>#Html.DropDownListFor(m=>m.SelectedCityName,Model.City,"-- Chọn thành phố --",new { #class = "dropdown" })</td>
</tr>
<tr>
<td class="info_label">Ward</td>
<td>#Html.DropDownListFor(m => m.SelectedWardName, Model.Ward, "-- Chọn quận --", new { #class = "dropdown" })</td>
</tr>
And the ViewModel:
public User User { get; set; }
public string SelectedCityName { get; set; }
public string SelectedWardName { get; set; }
public IEnumerable<SelectListItem> City { get; set; }
public IEnumerable<SelectListItem> Ward { get; set; }
How can i get SelectedCityName to pass as parameter of RegisterUser method?. Any help is appreciated.
Inside your POST action you are rendering the same view in case of error (return View(model);). But you forgot to assign the City and Ward properties, the same way you did in your GET action and they will be null when this view is rendered.
So:
// If we got this far, something failed, redisplay form
// and don't forget to rebind City and Ward
model.City = ILocaRepo.Cities.ToList().Select(x => new SelectListItem
{
Value = x.CityID.ToString(),
Text = x.CityName,
});
model.Ward = ILocaRepo.Wards.ToList().Select(x => new SelectListItem
{
Value = x.WardID.ToString(),
Text = x.WardName,
});
// Now we can safely redisplay the same view
return View(model);
The reason why those 2 properties are null inside your POST action is pretty simple and lies into the design of HTML forms. In HTML when you submit a form containing a <select> element (which is what those Html.DropDownListFor helpers are generating), only the selected value is sent to the server. The list of available values is not sent to the server simply because it is assumed that you already have it somewhere on the server (because you rendered this form in the first place). So it is your responsibility, if you decide to redisplay the form in your POST action, to re-populate those collections in order to be able to display the dropdown lists properly.
What I'm attempting to do is have a "Super" entity class for saved products. Depending on where the product is in the application, certain extra attributes are necessary (but not always).
For example, when the product is being used within a grid, I want to have a ViewModel that derives from the Entity (inheriting all of the common fields) and then add a unique identifier attribute like "rowNumber" for easy searching by the kendo ui grid CRUD.
I thought I had all of this working, but I've hit a snag... Everything renders fine and operates correctly until I click "save" for the batch grid. It executes the function and all the data is present, but when it returns from the CRUD, it breaks. In firebug, I see that an exception is being thrown, but it never finishes (the ajax spinner stays there) and all information within the exception is empty...
I'm not sure if this is some issue with c# not playing well with CSLA or not. I'm unsure.
Any help would be appreciated! I can't upload images because my rep isn't high enough or else I would put a picture of the exception, but I'll at least put what appears in the Firebug console. Everything else about it is empty though...
Exception with endless execution and no response:
GET http://localhost:32574/Exception/SystemException/00000000-0000-0000-0000-000000000000
Entity:
This file is auto-generated by a CodeSmith template so it is kind of nonsensical, but it holds field values that appear in the view (see below). The exception to what is on the view vs what is in the entity are fields that are not 'flattened' in the entity, as Kendo UI does not currently support this inside of editable grids.
ViewModel:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel.DataAnnotations;
namespace Project.MVC.Models
{
//The MetaData Class is where to put all annotations and validations
[MetadataType(typeof(Project.Business.Shipment.ItemMetaDataClass))]
public class ItemModel : Project.Business.Shipment.Item
{
public ItemModel()
{
}
public long rowNumber { get; set; }
public decimal Length { get; set; }
public decimal Width { get; set; }
public decimal Height { get; set; }
[Display(Name = "UoMDim")]
[UIHint("ItemGrid_RefUnitOfMeasurementListingDimension")]
public string DimensionUnitOfMeasure { get; set; }
[Display(Name = "UoMW")]
[UIHint("ItemGrid_RefUnitOfMeasurementListingWeight")]
public string WeightUnitOfMeasure { get; set; }
[Display(Name = "Weight")]
public decimal WeightValue { get; set; }
[Display(Name = "Type")]
[UIHint("ItemGrid_RefUnitTypeListing")]
public string QuantityUnitOfMeasure { get; set; }
[Display(Name = "Units")]
public decimal QuantityValue { get; set; }
}
}
Grid Controller:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Kendo.Mvc.UI;
using Kendo.Mvc.Extensions;
namespace Project.MVC.Controllers
{
[Authorize]
public class ItemGridController : Csla.Web.Mvc.Controller
{
public ActionResult GetProducts([DataSourceRequest]DataSourceRequest request)
{
Project.MVC.Models.ShipmentModel shipmentModel = (Project.MVC.Models.ShipmentModel)ControllerBase.State.Object;
return Json(shipmentModel.ItemModelList.ToDataSourceResult(request));
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult CreateProducts([DataSourceRequest]DataSourceRequest request, [Bind(Prefix = "models")]IEnumerable<Models.ItemModel> itemsToAdd)
{
Project.MVC.Models.ShipmentModel shipmentModel = (Project.MVC.Models.ShipmentModel)ControllerBase.State.Object;
var results = new List<Models.ItemModel>();
if (ModelState.IsValid)
{
foreach (Models.ItemModel newItem in itemsToAdd)
{
if (shipmentModel.ItemModelList.Count > 0)
{
var nextID = (from i in shipmentModel.ItemModelList
select i.rowNumber).Max() + 1;
newItem.rowNumber = nextID;
}
shipmentModel.ItemModelList.Add(newItem);
results.Add(newItem);
}
}
return Json(results.ToDataSourceResult(request, ModelState));
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult UpdateProducts([DataSourceRequest]DataSourceRequest request, [Bind(Prefix = "models")]IEnumerable<Models.ItemModel> itemsToUpdate)
{
Project.MVC.Models.ShipmentModel shipmentModel = (Project.MVC.Models.ShipmentModel)ControllerBase.State.Object;
var results = new List<Models.ItemModel>();
foreach (var item in itemsToUpdate)
{
Models.ItemModel target = shipmentModel.ItemModelList.Find(i => i.rowNumber == item.rowNumber);
if (target != null)
{
target = item;
}
}
return Json(ModelState.ToDataSourceResult());
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult DeleteProducts([DataSourceRequest]DataSourceRequest request, [Bind(Prefix = "models")]IEnumerable<Models.ItemModel> itemsToDelete)
{
Project.MVC.Models.ShipmentModel shipmentModel = (Project.MVC.Models.ShipmentModel)ControllerBase.State.Object;
foreach (var item in itemsToDelete)
{
shipmentModel.ItemModelList.Remove(item);
}
return Json(ModelState.ToDataSourceResult());
}
}
}
View:
#model Project.MVC.Models.ShipmentModel
#using Kendo.Mvc.UI
#(Html.Kendo().Grid<Project.MVC.Models.ItemModel>()
.Name("QuoteItemGrid")
.Columns(columns =>
{
columns.Bound(i => i.FreightClass)
.EditorTemplateName("ItemGrid_RefFreightClassListing")
.Width(50);
columns.Bound(i => i.Length).Width(30);
columns.Bound(i => i.Width).Width(30);
columns.Bound(i => i.Height).Width(30);
columns.Bound(i => i.DimensionUnitOfMeasure)
.EditorTemplateName("ItemGrid_RefUnitOfMeasurementListingDimension")
.Width(50);
columns.Bound(i => i.QuantityValue).Width(30);
columns.Bound(i => i.QuantityUnitOfMeasure)
.EditorTemplateName("ItemGrid_RefUnitTypeListing")
.Width(50);
columns.Bound(i => i.WeightValue).Width(30);
columns.Bound(i => i.WeightUnitOfMeasure)
.EditorTemplateName("ItemGrid_RefUnitOfMeasurementListingWeight")
.Width(50);
columns.Bound(i => i.NmfcCode).Width(50);
columns.Bound(i => i.ItemDescription).Width(100);
columns.Command(command =>
{
command.Destroy();
}).Width(60);
})
.ToolBar(toolbar =>
{
toolbar.Create();
toolbar.Save();
})
.Editable(editable => editable.Mode(GridEditMode.InCell).CreateAt(GridInsertRowPosition.Bottom))
.Pageable()
.Sortable()
.Scrollable()
.Resizable(resize => resize.Columns(true))
.DataSource(dataSource => dataSource
.Ajax()
.Batch(true)
.ServerOperation(false)
.Events(events => events.Error("QuoteItemGrid_ErrorHandler"))
.Model(model =>
{
model.Id(i => i.rowNumber);
model.Field(i => i.DimensionUnitOfMeasure).DefaultValue("in");
model.Field(i => i.WeightUnitOfMeasure).DefaultValue("lbs");
})
.Create(create => create.Action("CreateProducts", "ItemGrid"))
.Read(read => read.Action("GetProducts", "ItemGrid"))
.Update(update => update.Action("UpdateProducts", "ItemGrid"))
.Destroy(destroy => destroy.Action("DeleteProducts", "ItemGrid"))
)
)
I agree entirely. Ideally your CSLA .NET based objects are domain objects, not data objects, and so match the shape needed by the business requirements not by database tables or queries.
Sadly a lot of people end up (mis)using CSLA like an ORM and so have data-centric objects, thus missing a lot of the value of the framework, and necessitating the creation and maintenance of overly complex viewmodel types in addition to what (should have been) business types.
From an architectural perspective, my group ultimately thought it unwise to tie our database generated entities so closely to our view. Especially now that the project is being used in multiple places (.Net application, Webservices application, etc...).
So, in the end we ended up creating ViewModels for each entity so that we could be flexible with our entities across multiple platforms. This restructuring removed the issue above, as the entities are now never present within the view.
word of caution however: If you're using Kendo UI, you'll have to 'flatten' your view models that you want presented within a singular component that are deep objects (e.g. object within another object). At the moment, Kendo does not support this. We are using ValueInjecter.
Hopefully, this information will help someone else too.
I have an MVC3 Telerik app. This is my controller:
[GridAction]
public ActionResult Index(GridCommand command)
{
IEnumerable<Order> data = GetData(command);
var dataContext = new NorthwindDataContext();
//Required for pager configuration
ViewData["total"] = dataContext.Orders.Count();
return View(data);
}
[GridAction(EnableCustomBinding = true)]
public ActionResult _CustomBinding(GridCommand command)
{
IEnumerable<Order> data = GetData(command);
var dataContext = new NorthwindDataContext();
return View(new GridModel
{
Data = data,
Total = dataContext.Orders.Count()
});
}
//Utility method which does custom paging and sorting using Linq
private static IEnumerable<Order> GetData(GridCommand command)
{
var dataContext = new NorthwindDataContext();
IQueryable<Order> data = dataContext.Orders;
if (command.PageSize > 0)
{
data = data.Skip((command.Page - 1) * command.PageSize);
}
data = data.Take(5);
return data;
}
This is my razor View:
#model IEnumerable<Telerik.Order>
#(Html.Telerik().Grid(Model)
.Name("Grid")
.Columns(columns =>
{
columns.Bound(o => o.OrderID).Width(100);
columns.Bound(o => o.Customer.ContactName).Width(200);
columns.Bound(o => o.ShipAddress);
columns.Bound(o => o.OrderDate).Format("{0:MM/dd/yyyy}").Width(100);
})
.DataBinding(dataBinding =>
{
dataBinding.Ajax().Select("_CustomBinding", "Home").Enabled(true);
})
.Pageable(pager => pager.Total((int)ViewData["total"]))
.EnableCustomBinding(true)
.Sortable()
)
When I run the app, it loads fine, but when I click on a number in the paging row at the bottom, I get :
Error! The requested URL returned 500- Internal server error
It calls my function _CustomBinding and doesnt throw an error when i step through it. Whats causing this error?
This means that a server side exception has occurred in _CustomBinding method. You can check what the actual server response is - it would contain the stacktrace. Use Fiddler or your browser's developer tools to check what the server response is.
For the sake of simplicity, let's say I have a User model that has a List<Email> as one of its properties.
public class UserModel
{
public string UserName { get; set; }
public List<Email> Emails = new List<Email>();
}
public class Email
{
public string Address { get; set; }
}
In my view, I have a list of the emails:
<table>
#foreach(Email email in Model.Emails)
{
<tr>
<td>#Html.EditorFor(modelItem => email.Address)</td>
</tr>
}
</table>
Now let's say I want the user to be able to click a button that adds a new row to the table so that the user can add a new Email to the List that is bound to their User. How do I do this? Do I need to add the new row via javascript in a certain way so that it gets bound to the model when the page is posted? I have no idea how to approach this as I'm relatively new to MVC coming from WebForms.
After some researching, I found this blog post by Steven Anderson http://blog.stevensanderson.com/2010/01/28/editing-a-variable-length-list-aspnet-mvc-2-style/
It appears to be doing exactly what I want (except it is written in MVC2).
This is one of those places where MVC and WebForms dramatically diverge.
If I were doing this, I'd use AJAX to submit the new email address and return either a JSON object or the table of emails rendered as a Partial View. That way you don't have to reload the whole page. Here's and example that would return the HTML from the AJAX call, using jQuery because I'm not a fan of MVC's native AJAX functionality.
Original View:
#*HTML/Razor*#
#Html.Partial("EmailTable", Model.Emails)
#*HTML/Razor*#
Partial View: EmailTable
#model List<Email>
<table id='UserEmails'>
#foreach(var email in Model)
{
<tr>
<td>#Html.EditorFor(modelItem => email.Address)</td>
</tr>
}
</table>
Controller Action: AddEmail
public ActionResult AddEmail(string email, object someUserIdentifier){
//if email is valid
//add email to user's data store
//get new UserModel, user
return PartialView("EmailTable", user.Emails);
}
jQuery to handle the button click
function AddEmail(e){
var newEmailForm = $("<form />").attr("action", urlToController + "/AddEmail/").submit(SaveEmail);
$("<input/>").attr({type: "text", id="NewEmailAddress"}).appendTo(newEmailForm);
$("<input/>").attr("type", "submit").click(SaveEmail).appendTo(newEmailForm);
newEmailForm = $("<td />").append(newEmailForm);
newEmailForm = $("<tr />").append(newEmailForm);
$('#UserEmails').append(newEmailForm);
}
function SaveEmail(e){
var newEmail = $("#NewEmailAddress").val();
if (/*newEmail is valid*/){
$.ajax({
url: urlToController + "/AddEmail/",
data: { email: newEmail, someUserIdentifer: null/*or something useful*/ },
success: function(newTable){
$('#UserEmails').replaceWith(newTable);
},
error: function(xhr, status, error){
//display error
}
});
}
else{
//tell user what a valid email address looks like
}
return false;
}
I would use an extension method instead that you can use in other cases as well:
Extension:
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Text;
using System.Web.Mvc;
using System.Web.Mvc.Html;
public static class HtmlHelperExtensions
{
/// <summary>
/// Generates a GUID-based editor template, rather than the index-based template generated by Html.EditorFor()
/// </summary>
/// <typeparam name="TModel"></typeparam>
/// <typeparam name="TValue"></typeparam>
/// <param name="html"></param>
/// <param name="propertyExpression">An expression which points to the property on the model you wish to generate the editor for</param>
/// <param name="indexResolverExpression">An expression which points to the property on the model which holds the GUID index (optional, but required to make Validation* methods to work on post-back)</param>
/// <param name="includeIndexField">
/// True if you want this helper to render the hidden <input /> for you (default). False if you do not want this behaviour, and are instead going to call Html.EditorForManyIndexField() within the Editor view.
/// The latter behaviour is desired in situations where the Editor is being rendered inside lists or tables, where the <input /> would be invalid.
/// </param>
/// <returns>Generated HTML</returns>
public static MvcHtmlString EditorForMany<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, IEnumerable<TValue>>> propertyExpression, Expression<Func<TValue, string>> indexResolverExpression = null, bool includeIndexField = true) where TModel : class
{
var items = propertyExpression.Compile()(html.ViewData.Model);
var htmlBuilder = new StringBuilder();
var htmlFieldName = ExpressionHelper.GetExpressionText(propertyExpression);
var htmlFieldNameWithPrefix = html.ViewData.TemplateInfo.GetFullHtmlFieldName(htmlFieldName);
Func<TValue, string> indexResolver = null;
if (indexResolverExpression == null)
{
indexResolver = x => null;
}
else
{
indexResolver = indexResolverExpression.Compile();
}
foreach (var item in items)
{
var dummy = new { Item = item };
var guid = indexResolver(item);
var memberExp = Expression.MakeMemberAccess(Expression.Constant(dummy), dummy.GetType().GetProperty("Item"));
var singleItemExp = Expression.Lambda<Func<TModel, TValue>>(memberExp, propertyExpression.Parameters);
if (String.IsNullOrEmpty(guid))
{
guid = Guid.NewGuid().ToString();
}
else
{
guid = html.AttributeEncode(guid);
}
if (includeIndexField)
{
htmlBuilder.Append(_EditorForManyIndexField<TValue>(htmlFieldNameWithPrefix, guid, indexResolverExpression));
}
htmlBuilder.Append(html.EditorFor(singleItemExp, null, String.Format("{0}[{1}]", htmlFieldName, guid)));
}
return new MvcHtmlString(htmlBuilder.ToString());
}
/// <summary>
/// Used to manually generate the hidden <input />. To be used in conjunction with EditorForMany(), when "false" was passed for includeIndexField.
/// </summary>
/// <typeparam name="TModel"></typeparam>
/// <param name="html"></param>
/// <param name="indexResolverExpression">An expression which points to the property on the model which holds the GUID index (optional, but required to make Validation* methods to work on post-back)</param>
/// <returns>Generated HTML for hidden <input /></returns>
public static MvcHtmlString EditorForManyIndexField<TModel>(this HtmlHelper<TModel> html, Expression<Func<TModel, string>> indexResolverExpression = null)
{
var htmlPrefix = html.ViewData.TemplateInfo.HtmlFieldPrefix;
var first = htmlPrefix.LastIndexOf('[');
var last = htmlPrefix.IndexOf(']', first + 1);
if (first == -1 || last == -1)
{
throw new InvalidOperationException("EditorForManyIndexField called when not in a EditorForMany context");
}
var htmlFieldNameWithPrefix = htmlPrefix.Substring(0, first);
var guid = htmlPrefix.Substring(first + 1, last - first - 1);
return _EditorForManyIndexField<TModel>(htmlFieldNameWithPrefix, guid, indexResolverExpression);
}
private static MvcHtmlString _EditorForManyIndexField<TModel>(string htmlFieldNameWithPrefix, string guid, Expression<Func<TModel, string>> indexResolverExpression)
{
var htmlBuilder = new StringBuilder();
htmlBuilder.AppendFormat(#"<input type=""hidden"" name=""{0}.Index"" value=""{1}"" />", htmlFieldNameWithPrefix, guid);
if (indexResolverExpression != null)
{
htmlBuilder.AppendFormat(#"<input type=""hidden"" name=""{0}[{1}].{2}"" value=""{1}"" />", htmlFieldNameWithPrefix, guid, ExpressionHelper.GetExpressionText(indexResolverExpression));
}
return new MvcHtmlString(htmlBuilder.ToString());
}
}
Add a property to the model, which the EditorForMany helper will store the generated index in. Without this, the Html.Validation* methods will not work (see here for a deep-dive into “why” for the curious).
public class UserModel
{
public string UserName { get; set; }
public List<Email> Emails = new List<Email>();
}
public class Email
{
public string Address { get; set; }
public string Index { get; set; }
}
Substitute #Html.EditorFor(modelItem => email.Address) with:
#Html.EditorForMany(x => x.Emails, x => x.Index, false);
#Html.EditorForManyIndexField(x => x.Index)
(Note: If you are not in a <tr>, <tbody> or <ul> or similar the code would be #Html.EditorForMany(x => x.Emails, x => x.Index) and you would not need #Html.EditorForManyIndexField(x => x.Emails, x => x.Index) or #Html.EditorForManyIndexField(x => x.Index). Without setting Indexfield yourself your table would be badly formatted and therefore we do it like this.)
Now all of our problems are solved! You’ll see that Html.EditorForMany() uses GUIDs rather than numbers for indexes. This removes the need for us to tell our AJAX endpoint which indexes as been used; as our AJAX endpoint will instead just generate a new GUID. Html.EditorForMany() also takes care of seamlessly producing the .Index field for us as well.
All that’s left to do is to get our AJAX endpoint up and running. To do this, I define a new action in my Controller.
[OutputCache(NoStore = true, Duration = 0, VaryByParam = "*")]
public ActionResult AddEmail()
{
var user = new UserModel();
user.Emails.Add(new Email());
return View(user);
}
Create a new view Views\Shared\AddEmail.cshml;
#model DynamicListBinding.Models.UserModel
#{
Layout = null;
}
#Html.EditorForMany(x => x.Emails, x => x.Index, false);
Kudos to Matt for original article
First, your model definition needs some tweaking:
public class UserModel
{
public string UserName { get; set; }//not sure where to use this
//for listing
public List<Email> Emails { get; set; }
//for adding
public Email Email { get; set; }
public UserModel()
{
this.Emails = new List<Email>();//remember to populate in the controller
}
}
Next, what you can do is (not sure on your table implementation) display a list of the current emails, and then have a form section that can post a new email to add:
#model namespace.UserModel
#foreach(var email in Emails)
{
<div>#email.Address</div>
}
<script src="#Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
#using (Html.BeginForm()) {
#Html.ValidationSummary(true)
<fieldset>
<legend>New Email Details</legend>
<div class="editor-label">
#Html.LabelFor(model => model.Email.Address)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Email.Address)
#Html.ValidationMessageFor(model => model.Email.Address)
</div>
<p>
<input type="submit" value="Add Email" />
</p>
</fieldset>
}
Have you considered using a third party tools for this?
I have found this on CodeProject and it appears to meet your requirements. Yes it'll require a bit of tweaking, but it should do the job
http://www.codeproject.com/Articles/277576/AJAX-based-CRUD-tables-using-ASP-NET-MVC-3-and-jTa
Alternatively you can spend hours on implementing similar functionality in JavaScript/jQuery.