ASP.NET MVC / [Из песочницы] In-line редактирование табличных данных в ASP.Net MVC 3

в 14:39, , рубрики: Новости, метки:

ASP.NET MVC / [Из песочницы] In-line редактирование табличных данных в ASP.Net MVC 3 Представляю вашему вниманию ещё одну реализацию 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 пейджером.

Список используемой литературы в интернетах:

1)Ajax Grid Scaffolder

2)Editing a variable length list, ASP.NET MVC 2-style

3)Dynamic SQL-like Linq OrderBy Extension

Скачать полученный результат.

Автор: slrzz


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js