Представляю вашему вниманию ещё одну реализацию AjaxGrid на ASP.Net MVC 3.
В статье рассказывается как создать табличную форма с inline редактированием, ajax сортировкой и ajax пейджером.
Данная реализация — компиляция из нескольких доступных компонентов.
Как это обычно бывает, по рабочей необходимости мне понадобилось отображать табличные данные с возможностью их редактирования. Примерно как на скриншоте:
Перед тем как приступим установим AjaxGridScaffolder
PM> Install-Package AjaxGridScaffolder
Создаём слой данных
Сущность.
namespace HabrahabrMVC.Domain.Entities { public class RealtyObject { public int Id { get; set; } public string City { get; set; } public string Street { get; set; } public string House { get; set; } public double Cost { get; set; } public bool Visible { get; set; } } }
Интерфейс.
namespace HabrahabrMVC.Domain.Abstract { public interface IRepository { IQueryable<RealtyObject> GetAll(); void Save(RealtyObject objectToSave); } }
Контекст.
namespace HabrahabrMVC.Domain.Concrete { public class EFContext : DbContext { public DbSet<RealtyObject> RealtyObjects { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); } } }
Реализация интерфейса.
namespace HabrahabrMVC.Domain.Concrete { public class EFRepository :IRepository { EFContext _db = new EFContext(); public void Save(Entities.RealtyObject objectToSave) { _db.Entry(_db.RealtyObjects.SingleOrDefault(z=>z.Id==objectToSave.Id)).CurrentValues.SetValues(objectToSave); _db.Entry<RealtyObject>(_db.RealtyObjects.SingleOrDefault(z => z.Id == objectToSave.Id)).State = System.Data.EntityState.Modified; _db.SaveChanges(); } public IQueryable<RealtyObject> GetAll() { return _db.RealtyObjects.AsQueryable(); } } }
Создаём контроллер
ObjectsView:
1)Для этого щёлкаем правой кнопкой в Object Explorer'e(Обозреватель объектов) на папке Controllers, выбираем Add->Controller… (Добавить->Контроллер...).
2)Пишем название ObjectsViewController.
3)Шаблон — Ajax Grid Controller
4)Класс модели — RealtyObject (HabraHabrMVC3.Domain.Entities)
5)Класс контекста данных — EFContext (HabraHabrMVC3.Domain.Concrete)
6)Добавить.
После того как мастер сгенерировал нам код, добавляем конструктор в контроллёр, для того чтобы, Ninject корректно инжектировал наш слой данных.
//private EFContext db = new EFContext(); private IRepository db; public ObjectsViewController(IRepository dbparam) { db = dbparam; }
Теперь надо в сгенерированном коде поменять источники данных:
db.RealtyObjects на db.GetAll()
db.RealtyObjects.Find(id) на db.GetAll().SingleOrDefault(z=>z.Id==id)
Сейчас мне не нужны экшены по созданию, редактированию и удалению данных, я их удалю.
Также в view GridData.cshtml удалил кнопки редактирования и удаления.
Сейчас не заработает строчка:
ObjectQuery<RealtyObject> realtyobjects = (db as IObjectContextAdapter).ObjectContext.CreateObjectSet<RealtyObject>();
т.к. db не поддерживает IObjectContextAdapter.
Поэтому добавим метод раширения Linq.
Я создал отдельный статический класс OrderByHelper.
namespace HabraHabrMVC3.Infrastructure { public static class OrderByHelper { public static IEnumerable<T> OrderBy<T>(this IEnumerable<T> enumerable, string orderBy) { return enumerable.AsQueryable().OrderBy(orderBy).AsEnumerable(); } public static IQueryable<T> OrderBy<T>(this IQueryable<T> collection, string orderBy) { foreach (OrderByInfo orderByInfo in ParseOrderBy(orderBy)) collection = ApplyOrderBy<T>(collection, orderByInfo); return collection; } private static IQueryable<T> ApplyOrderBy<T>(IQueryable<T> collection, OrderByInfo orderByInfo) { string[] props = orderByInfo.PropertyName.Split('.'); Type type = typeof(T); ParameterExpression arg = Expression.Parameter(type, "x"); Expression expr = arg; foreach (string prop in props) { // use reflection (not ComponentModel) to mirror LINQ PropertyInfo pi = type.GetProperty(prop); expr = Expression.Property(expr, pi); type = pi.PropertyType; } Type delegateType = typeof(Func<,>).MakeGenericType(typeof(T), type); LambdaExpression lambda = Expression.Lambda(delegateType, expr, arg); string methodName = String.Empty; if (!orderByInfo.Initial && collection is IOrderedQueryable<T>) { if (orderByInfo.Direction == SortDirection.Ascending) methodName = "ThenBy"; else methodName = "ThenByDescending"; } else { if (orderByInfo.Direction == SortDirection.Ascending) methodName = "OrderBy"; else methodName = "OrderByDescending"; } //TODO: apply caching to the generic methodsinfos? return (IOrderedQueryable<T>)typeof(Queryable).GetMethods().Single( method => method.Name == methodName && method.IsGenericMethodDefinition && method.GetGenericArguments().Length == 2 && method.GetParameters().Length == 2) .MakeGenericMethod(typeof(T), type) .Invoke(null, new object[] { collection, lambda }); } private static IEnumerable<OrderByInfo> ParseOrderBy(string orderBy) { if (String.IsNullOrEmpty(orderBy)) yield break; string[] items = orderBy.Split(','); bool initial = true; foreach (string item in items) { string[] pair = item.Trim().Split(' '); if (pair.Length > 2) throw new ArgumentException(String.Format("Invalid OrderBy string '{0}'. Order By Format: Property, Property2 ASC, Property2 DESC", item)); string prop = pair[0].Trim(); if (String.IsNullOrEmpty(prop)) throw new ArgumentException("Invalid Property. Order By Format: Property, Property2 ASC, Property2 DESC"); SortDirection dir = SortDirection.Ascending; if (pair.Length == 2) dir = ("desc".Equals(pair[1].Trim(), StringComparison.OrdinalIgnoreCase) ? SortDirection.Descending : SortDirection.Ascending); yield return new OrderByInfo() { PropertyName = prop, Direction = dir, Initial = initial }; initial = false; } } private class OrderByInfo { public string PropertyName { get; set; } public SortDirection Direction { get; set; } public bool Initial { get; set; } } private enum SortDirection { Ascending = 0, Descending = 1 } } }
И тогда новый вид action GridData:
public ActionResult GridData(int start = 0, int itemsPerPage = 20, string orderBy = "Id", bool desc = false) { Response.AppendHeader("X-Total-Row-Count", db.GetAll().Count().ToString()); var realtyobjects = db.GetAll().OrderBy(orderBy + (desc ? " desc" : "")); return PartialView(realtyobjects.Skip(start).Take(itemsPerPage)); }
Сейчас у нас получилась прекрасная Ajax таблица с сортировкой и постраничным просмотром.
Добавление inline редактирования
Добавляем ещё один метод расширения, только уже для HTML хелпера:
namespace System.Web { public static class HtmlPrefixScopeExtensions { private const string idsToReuseKey = "__htmlPrefixScopeExtensions_IdsToReuse_"; public static IDisposable BeginCollectionItem(this HtmlHelper html, string collectionName) { var idsToReuse = GetIdsToReuse(html.ViewContext.HttpContext, collectionName); string itemIndex = idsToReuse.Count > 0 ? idsToReuse.Dequeue() : Guid.NewGuid().ToString(); // autocomplete="off" is needed to work around a very annoying Chrome behaviour whereby it reuses old values after the user clicks "Back", which causes the xyz.index and xyz[...] values to get out of sync. html.ViewContext.Writer.WriteLine(string.Format("<input type="hidden" name="{0}.index" autocomplete="off" value="{1}" />", collectionName, html.Encode(itemIndex))); return BeginHtmlFieldPrefixScope(html, string.Format("{0}[{1}]", collectionName, itemIndex)); } public static IDisposable BeginHtmlFieldPrefixScope(this HtmlHelper html, string htmlFieldPrefix) { return new HtmlFieldPrefixScope(html.ViewData.TemplateInfo, htmlFieldPrefix); } private static Queue<string> GetIdsToReuse(HttpContextBase httpContext, string collectionName) { // We need to use the same sequence of IDs following a server-side validation failure, // otherwise the framework won't render the validation error messages next to each item. string key = idsToReuseKey + collectionName; var queue = (Queue<string>)httpContext.Items[key]; if (queue == null) { httpContext.Items[key] = queue = new Queue<string>(); var previouslyUsedIds = httpContext.Request[collectionName + ".index"]; if (!string.IsNullOrEmpty(previouslyUsedIds)) foreach (string previouslyUsedId in previouslyUsedIds.Split(',')) queue.Enqueue(previouslyUsedId); } return queue; } private class HtmlFieldPrefixScope : IDisposable { private readonly TemplateInfo templateInfo; private readonly string previousHtmlFieldPrefix; public HtmlFieldPrefixScope(TemplateInfo templateInfo, string htmlFieldPrefix) { this.templateInfo = templateInfo; previousHtmlFieldPrefix = templateInfo.HtmlFieldPrefix; templateInfo.HtmlFieldPrefix = htmlFieldPrefix; } public void Dispose() { templateInfo.HtmlFieldPrefix = previousHtmlFieldPrefix; } } } }
Заменим view GridData на
@model IEnumerable<HabraHabrMVC3.Domain.Entities.RealtyObject> @if (Model.Count() > 0) { foreach (var item in Model) { <text>@Html.Partial("Edit", item)</text> } }
А view Edit на:
@model HabraHabrMVC3.Domain.Entities.RealtyObject @using (Html.BeginCollectionItem("objects")) { <tr> <td> @Html.EditorFor(model => model.City) @Html.ValidationMessageFor(model => model.City) </td> <td> @Html.EditorFor(model => model.Street) @Html.ValidationMessageFor(model => model.Street) </td> <td> @Html.EditorFor(model => model.House) @Html.ValidationMessageFor(model => model.House) </td> <td> @Html.EditorFor(model => model.Cost) @Html.ValidationMessageFor(model => model.Cost) </td> <td> @Html.EditorFor(model => model.Visible) @Html.ValidationMessageFor(model => model.Visible) </td> </tr> }
В view Index добавим:
@using (Html.BeginForm("Save", "ObjectsView", FormMethod.Post)) { <table id="AjaxGrid"> ...code... </table> <input type="submit" id="save" value="Сохранить" /> }
Добавляем action Save:
[HttpPost] public ActionResult Save(ICollection<RealtyObject> objects) { foreach(var item in objects) { db.Save(item); } return RedirectToAction("Index"); }
Здесь важно, чтобы совпадали имя коллекции в @using (Html.BeginCollectionItem("objects")) и название параметра в методе action Save(ICollection objects).
Всё, получилась табличная форма с inline редактированием, ajax сортировкой и ajax пейджером.
Список используемой литературы в интернетах:
2)Editing a variable length list, ASP.NET MVC 2-style
3)Dynamic SQL-like Linq OrderBy Extension
Автор: slrzz