سلام خدمت استاد عزیز و همه دوستان
تمرین جلسه 68 برای نمایش لیست دسته بندی ها در ادمین را من به این صورت نوشته ام جوریکه هم نمایش دسته ها به صورت سلسله مراتبی باشه و هم اینکه صفحه بندی داشته باشیم و هم این که بتوانیم با عنوان دسته بندی و وضعیت حذف بتوانیم فیلتر اعمال کنیم. من برای این کار مجبور شدم در متد هایی که در جلسه 68 نوشته شد تغییراتی اعمال کنم تا بتوانم آن چیزی که مد نظرم هست را پیاده سازی کنم. امیداوارم به کارتون اومده باشه و در صورتی که باگی داشت ممنون میشم اطلاع رسانی کنید.
اول از همه من تصمیم گرفتم که در هر صفحه تعداد 4 تا از دسته بندی های اصلی (ParentId==null) را نمایش دهم و در زیر هر دسته بندی اصلی ای ، زیر دسته هایش و زیردسته های زیردسته هایش را نمایش دهم . اینجوری هم پیجینگ به هم نمیخوره و هم این که میتونیم سلسله مراتبی دسته بندی ها را نمایش دهیم .
پس متد FilterAsync ریپوزیتوری را به این صورت نوشتم که فقط دسته بندی ها والد واکشی شوند و به همراه این واکشی لیست زیردسته ها و زیردسته های زیردسته های هر والدی هم واکشی شوند.(به کمک Include و ThenInclude)
کد CategoryRepository:
using Microsoft.EntityFrameworkCore;
using ShopStock.Domain.Contracts;
using ShopStock.Domain.Models.Categories;
using ShopStock.Infra.Data.Context;
using System;
using System.Collections.Generic;
using System.Text;
namespace ShopStock.Infra.Data.Repositories
{
public class CategoryRepository(EshopDbContext _context) : ICategoryRepository
{
public async Task<IQueryable<Category>> FilterAsync()
{
return _context.Categories.Where(c => c.ParentId == null)
.Include(c => c.Categories)
.ThenInclude(c => c.Categories)
.AsQueryable();
}
public async Task<IEnumerable<Category>> GetAllCategoryForMegaMenu()
{
return await _context.Categories.ToListAsync();
}
}
}
در مرحله بعد چون خاستم که در هر صفحه فقط 4 تا از دسته بندی های اصلی در هر صفحه نمایش داده شوند در کلاس AdminFilterCategoryViewModel ، کانستراکتور را وارد کردم که TakeEntity را تغییر دهم :
کد AdminFilterCategoryViewModel :
using ShopStock.Domain.Enums.Common;
using ShopStock.Domain.ViewModels.Common;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text;
namespace ShopStock.Domain.ViewModels.Category
{
public class AdminFilterCategoryViewModel : BasePaging<CategoryViewModel>
{
public AdminFilterCategoryViewModel()
{
TakeEntity = 4;
}
//public int? ParentId { get; set; }
[Display(Name = "عنوان دسته بندی")]
public string? Title { get; set; }
[Display(Name = "وضعیت حذف")]
public FilterDeleteStatus DeleteStatus { get; set; }
}
}
در این کلاس از ParentId برای اعمال فیلتر استفاده نکردم برا همین کامنتش کردم.
به کلاس CategoryViewModel دو تا فیلد Children و InnerChildren اضافه کردم که از جنس همین کلاس CategoryViewModel هستند و قراره تو مپینگ مقدار دهی بشوند.
کد CategoryViewModel :
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Text;
using static System.Net.WebRequestMethods;
namespace ShopStock.Domain.ViewModels.Category
{
public class CategoryViewModel
{
public int Id { get; set; }
public int? ParentId { get; set; }
[Display(Name = "عنوان دسته بندی")]
public string Title { get; set; }
public string? ImageName { get; set; }
public bool IsDeleted { get; set; }
public IEnumerable<CategoryViewModel>? Children { get; set; }
public IEnumerable<CategoryViewModel>? InnerChildren { get; set; }
}
}
در مرحله بعد سراغ CategoryMapper رفتم و تغییرات زیر را در آن اعمال کردم :
using ShopStock.Domain.Models.Categories;
using ShopStock.Domain.ViewModels.Category;
using System;
using System.Collections.Generic;
using System.Text;
namespace ShopStock.Application.Mapper
{
public static class CategoryMapper
{
public static IQueryable<CategoryViewModel> MapToCategoryViewModel(IQueryable<Category> categories)
{
return categories.Select(c => new CategoryViewModel()
{
Id = c.Id,
ParentId = c.ParentId,
Title = c.Title,
ImageName = c.ImageName,
IsDeleted = c.IsDeleted,
Children = c.Categories.Select(cc => new CategoryViewModel()
{
Id = cc.Id,
ParentId = cc.ParentId,
Title = cc.Title,
ImageName = cc.ImageName,
IsDeleted = cc.IsDeleted,
InnerChildren = cc.Categories.Select(ccc => new CategoryViewModel()
{
Id = ccc.Id,
ParentId = ccc.ParentId,
Title = ccc.Title,
ImageName = ccc.ImageName,
IsDeleted = ccc.IsDeleted,
})
})
});
}
}
}
اینجوری همیشه هر دسته بندی والدی با زیردسته ها و زیردسته های زیردسته هایش مپ میشه.
در متد سرویس هم برای اعمال فیلتر ، فیلتر عنوان را جوری پیاده سازی کرده ام که اگر دسته بندی ای در دیتابیس عنوانش شامل متن فیلتر بود، آن دسته بندی به همراه خانواده اش واکشی بشه که معلوم بشه این دسته بندی در کجای سلسله مراتب خانواده اش قرار گرفته (منظورم از خانواده اش یک دسته بندی اصلی و تمام زیردسته ها و زیردسته های زیردسته هایش است). همچین کاری که برای دیلیت انجام داده ام (که سه وضعیت داره) 1 - همه خانواده ها نشون داده میشن(چه عضو حذف شده داشته باشن چه نداشته باشن) 2 - اون خانواده هایی که حداقل یکی از اعضایشان حذف شده باشد(تا مشخص بشه که این عضو حذف شده در کجای سلسله مراتب چه خانواده ای قرار گرفته است).3 - فیلترینگ NotDeleted را سمت ویو انجام داده ام)
کد CategoryService :
using ShopStock.Application.Mapper;
using ShopStock.Application.Services.Interfaces;
using ShopStock.Domain.Contracts;
using ShopStock.Domain.Enums.Common;
using ShopStock.Domain.Models.Categories;
using ShopStock.Domain.ViewModels.Category;
using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Text;
namespace ShopStock.Application.Services.Implementation
{
public class CategoryService(ICategoryRepository _categoryRepository) : ICategoryService
{
public async Task<AdminFilterCategoryViewModel> FilterAsync(AdminFilterCategoryViewModel filter)
{
var query = await _categoryRepository.FilterAsync();
//if (filter.ParentId.HasValue)
//{
// query = query.Where(c => c.ParentId == filter.ParentId);
//}
//else
//{
// query = query.Where(c => c.ParentId == null);
//}
if (!string.IsNullOrEmpty(filter.Title))
{
query = query.Where(c => c.Title.Contains(filter.Title) || c.Categories.Any(cc => cc.Title.Contains(filter.Title)
|| c.Categories.Any(ccc => ccc.Categories.Any(cccc => cccc.Title.Contains(filter.Title)))));
}
switch (filter.DeleteStatus)
{
case FilterDeleteStatus.All:
break;
case FilterDeleteStatus.Deleted:
{
query = query.Where(c => c.IsDeleted
|| c.Categories.Any(cc => cc.IsDeleted
|| c.Categories.Any(ccc => ccc.Categories.Any(cccc => cccc.IsDeleted))));
break;
}
}
query = query.OrderByDescending(c => c.CreateDate);
await filter.Paging(CategoryMapper.MapToCategoryViewModel(query));
return filter;
}
public async Task<IEnumerable<Category>> GetAllCategoryForMegaMenu()
{
return await _categoryRepository.GetAllCategoryForMegaMenu();
}
}
}
کد CategoriesController:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using ShopStock.Application.Services.Interfaces;
using ShopStock.Domain.Models.Categories;
using ShopStock.Domain.ViewModels.Category;
using ShopStock.Infra.Data.Context;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace ShopStock.Web.Areas.Admin.Controllers
{
[Area("Admin")]
public class CategoriesController : Controller
{
private readonly ICategoryService _categoryService;
public CategoriesController(ICategoryService categoryService)
{
_categoryService = categoryService;
}
// GET: Admin/Categories
public async Task<IActionResult> Index(AdminFilterCategoryViewModel filter)
{
var filteredCategories = await _categoryService.FilterAsync(filter);
return View(filteredCategories);
}
}
}
کد Index.cshtml :
@using ShopStock.Domain.ViewModels.Category
@model AdminFilterCategoryViewModel
@{
ViewData["Title"] = "لیست دسته بندی ها";
ViewBag.Url = "/Admin/Categories";
}
<div class="accordion" id="accordionExample">
<div class="card shadow-sm accordion-item">
<h4 class="accordion-header" id="headingOne">
<button type="button" class="accordion-button collapsed" data-bs-toggle="collapse" data-bs-target="#accordionOne" aria-expanded="false" aria-controls="accordionOne">
فیلتر
</button>
</h4>
<div id="accordionOne" class="accordion-collapse collapse" data-bs-parent="#accordionExample" style="">
<div class="accordion-body">
<form asp-action="Index" id="filterForm">
<input type="hidden" name="PageId" value="@Model.PageId" />
<div class="row g-3">
<div class="col-md-6">
<div class="form-group">
<label asp-for="Title" class="form-label">تایتل</label>
<input asp-for="Title" class="form-control" />
</div>
</div>
</div>
<div class="row g-3 my-3">
<div class="col-md-3">
<div class="form-group">
<label asp-for="DeleteStatus" class="form-label"></label>
<select asp-for="DeleteStatus" class="form-control" asp-items="Html.GetEnumSelectList<ShopStock.Domain.Enums.Common.FilterDeleteStatus>()">
</select>
</div>
</div>
</div>
<div class="mt-4 d-flex gap-2">
<button type="submit" class="btn btn-success">
فیلتر
</button>
<a asp-action="Index" class="btn btn-outline-warning">
پاک کردن فیلترها
</a>
</div>
</form>
</div>
</div>
</div>
</div>
<hr />
<div class="card shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">@ViewData["Title"]</h5>
<a class="btn btn-success btn-sm" asp-action="Create">
<i class="bi bi-plus-lg"></i> افزودن دسته بندی
</a>
</div>
<div class="table-responsive">
<table class="table table-sm table-hover table-bordered align-middle text-nowrap mb-0">
<thead class="table-light">
<tr class="text-center">
<th>تصویر</th>
<th>عنوان</th>
<th>حذف شده</th>
<th class="text-center">عملیات</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Entities)
{
string rowClass = item.IsDeleted ? "table-danger" : "";
var children = item.Children;
<tr class="@rowClass" style="background-color:#E0FFFF" @((Model.DeleteStatus == ShopStock.Domain.Enums.Common.FilterDeleteStatus.NotDeleted &amp;amp;amp;&amp;amp;amp; item.IsDeleted) ? "style=Display:none" : "")>
<td>
<img class="img-thumbnail" style="max-width:60px ;border-radius:5px" src="/CategoryImages/@item.ImageName">
</td>
<td>@item.Title</td>
<td class="text-center">
@if (item.IsDeleted)
{
<span class="badge bg-danger">حذف شده</span>
}
else
{
<span class="badge bg-success">سالم</span>
}
</td>
<td class="text-center">
<div class="btn-group btn-group-sm">
<a class="btn btn-outline-primary"
asp-action="Edit"
asp-route-id="@item.Id">
ویرایش
</a>
<a class="btn btn-outline-dark"
asp-action="Details"
asp-route-id="@item.Id">
جزئیات
</a>
</div>
</td>
</tr>
@if (children.Any())
{
foreach (var child in children)
{
string rowClass2 = child.IsDeleted ? "table-danger" : "";
var innerChildren = child.InnerChildren;
<tr class="@rowClass2" @((Model.DeleteStatus == ShopStock.Domain.Enums.Common.FilterDeleteStatus.NotDeleted &amp;amp;amp;&amp;amp;amp; child.IsDeleted) ? "style=Display:none" : "")>
<td>
<img class="img-thumbnail" style="max-width:60px ;border-radius:5px" src="/CategoryImages/@child.ImageName">
</td>
<td>- - @child.Title</td>
<td class="text-center">
@if (child.IsDeleted)
{
<span class="badge bg-danger">حذف شده</span>
}
else
{
<span class="badge bg-success">سالم</span>
}
</td>
<td class="text-center">
<div class="btn-group btn-group-sm">
<a class="btn btn-outline-primary"
asp-action="Edit"
asp-route-id="@child.Id">
ویرایش
</a>
<a class="btn btn-outline-dark"
asp-action="Details"
asp-route-id="@child.Id">
جزئیات
</a>
</div>
</td>
</tr>
if (innerChildren.Any())
{
foreach (var innerChild in innerChildren)
{
string rowClass3 = innerChild.IsDeleted ? "table-danger" : "";
<tr class="@rowClass3" @((Model.DeleteStatus == ShopStock.Domain.Enums.Common.FilterDeleteStatus.NotDeleted &amp;amp;amp;&amp;amp;amp; innerChild.IsDeleted) ? "style=Display:none" : "")>
<td>
<img class="img-thumbnail" style="max-width:60px ;border-radius:5px" src="/CategoryImages/@innerChild.ImageName">
</td>
<td>- - - - @innerChild.Title</td>
<td class="text-center">
@if (innerChild.IsDeleted)
{
<span class="badge bg-danger">حذف شده</span>
}
else
{
<span class="badge bg-success">سالم</span>
}
</td>
<td class="text-center">
<div class="btn-group btn-group-sm">
<a class="btn btn-outline-primary"
asp-action="Edit"
asp-route-id="@innerChild.Id">
ویرایش
</a>
<a class="btn btn-outline-dark"
asp-action="Details"
asp-route-id="@innerChild.Id">
جزئیات
</a>
</div>
</td>
</tr>
}
}
}
}
}
</tbody>
</table>
</div>
<partial name="_AdminPagination" model="Model.GetCurrentPaging()" />
</div>
@section Scripts {
<script>
document.addEventListener("DOMContentLoaded", function () {
document.querySelectorAll(".page-item a.page-link").forEach(function (el) {
el.addEventListener("click", function (e) {
e.preventDefault(); // جلوگیری از رفتن به href
const href = this.getAttribute("href");
const urlParams = new URLSearchParams(href.split("?")[1]);
const pageId = urlParams.get("PageId");
// مقدار PageId را در فرم قرار بده
document.querySelector("#filterForm input[name='PageId']").value = pageId;
// فرم را سابمیت کن
document.getElementById("filterForm").submit();
});
});
});
</script>
}
چون در هنگام فیلترینگ ممکن است که چند صفحه نتیجه برگردد برای اینکه در هنگام پیمایش بین این صفحات مقادیر فیلترینگ حذف نشود یک قطعه کد جاوااسکریپت به انتها اضافه کرده ام که این مشکل را حل میکند. البته این جوری دیگه تو url مرورگر pageId نمایش داده نمیشه.
در ضمن من پارشیال صفحه بندی را به این صورت پیاده سازی کرده ام :
کد _AdminPagination.cshtml :
@using ShopStock.Domain.ViewModels.Common
@model PageingViewModel
@if (Model.PageCount>1)
{
<div class="d-flex justify-content-center align-items-center py-2">
<nav aria-label="Page navigation">
<ul class="pagination pagination-primary m-0">
<li class="page-item first @(Model.PageId == 1 ? "disabled" : "")">
<a class="page-link" href="@ViewBag.Url?PageId=1"><i class="tf-icon bx bx-chevrons-left"></i></a>
</li>
<li class="page-item prev @(Model.PageId == 1 ? "disabled" : "")">
<a class="page-link" href="@ViewBag.Url?PageId=@(Model.PageId - 1)"><i class="tf-icon bx bx-chevron-left"></i></a>
</li>
@for (int i = Model.StartPage; i <= Model.EndPage; i++)
{
<li class='page-item @((Model.PageId == i) ? "active" : "")'>
<a class="page-link" href="@ViewBag.Url?PageId=@i">@i</a>
</li>
}
<li class="page-item next @(Model.PageId == Model.PageCount ? "disabled" : "")">
<a class="page-link" href="@ViewBag.Url?PageId=@(Model.PageId + 1)"><i class="tf-icon bx bx-chevron-right"></i></a>
</li>
<li class="page-item last @(Model.PageId == Model.PageCount ? "disabled" : "")">
<a class="page-link" href="@ViewBag.Url?PageId=@(Model.PageCount)"><i class="tf-icon bx bx-chevrons-right"></i></a>
</li>
</ul>
</nav>
</div>
}

با تشکر