فضای نام MVC
ایجاد مدلها و موجودیتهای برنامهی Minimal Blog
در مثال این سری، هر نویسنده میتواند بلاگ خاص خودش را داشته باشد و در هر بلاگ، تعدادی مقاله منتشر کند. هر مقاله هم میتواند تعدادی تگ یا گروه مرتبط را داشته باشد.
ساختار ابتدایی موجودیت نویسندگان مطالب بلاگ
این موجودیتها در پوشهی جدیدی به نام Model پروژهی MinimalBlog.Domain اضافه خواهند شد:
namespace MinimalBlog.Domain.Model; public class Author { public int Id { get; set; } public string FirstName { get; set; } = default!; public string LastName { get; set; } = default!; public string FullName => FirstName + " " + LastName; public DateTime DateOfBirth { get; set; } public string? Bio { get; set; } }
ساختار ابتدایی موجودیت مقالات بلاگ
namespace MinimalBlog.Domain.Model; public class Article { public Article() { Categories = new List<Category>(); } public int Id { get; set; } public string Title { get; set; } = default!; public string? Subtitle { get; set; } public string Body { get; set; } = default!; public int AuthorId { get; set; } public Author Author { get; set; } = default!; public DateTime DateCreated { get; set; } public DateTime LastUpdated { get; set; } public int NumberOfLikes { get; set; } public int NumberOfShares { get; set; } public ICollection<Category> Categories { get; set; } }
ساختار ابتدایی بلاگهای اختصاصی قابل تعریف
namespace MinimalBlog.Domain.Model; public class Blog { public Blog() { Contributors = new List<Author>(); } public int Id { get; set; } public string Name { get; set; } = default!; public string Description { get; set; } = default!; public DateTime CreatedDate { get; set; } public int AuthorId { get; set; } public Author Owner { get; set; } = default!; public ICollection<Author> Contributors { get; } }
ساختار ابتدایی گروههای مقالات بلاگ
namespace MinimalBlog.Domain.Model; public class Category { public Category() { Articles = new List<Article>(); } public int Id { get; set; } public string Name { get; set; } = default!; public string Description { get; set; } = default!; public ICollection<Article> Articles { get; set; } }
معرفی موجودیتهای تعریف شده به لایهی Dal
لایهی Dal جایی است که DbContext برنامه تعریف میشود و موجودیتها فوق توسط DbSetها در معرض دید EF-Core قرار میگیرند. به همین جهت ابتدا فایل MinimalBlog.Dal.csproj را به صورت زیر تغییر میدهیم:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net6.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> <ProjectReference Include="..\MinimalBlog.Domain\MinimalBlog.Domain.csproj" /> </ItemGroup> <ItemGroup> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.2" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.2" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.2"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> </ItemGroup> </Project>
به علاوه تعاریفی مانند ImplicitUsings و Nullable را هم مشاهده میکنید که با توجه به استفادهی از فایل Directory.Build.props غیرضروری و تکراری هستند؛ چون این فایل مخصوص به همراه این تعاریف سراسری نیز هست.
سپس به پروژهی Dal، کلاس جدید MinimalBlogDbContext را به صورت زیر اضافه میکنیم تا مدلهای برنامه را در معرض دید EF-Core قرار دهد:
using Microsoft.EntityFrameworkCore; using MinimalBlog.Domain.Model; namespace MinimalBlog.Dal; public class MinimalBlogDbContext : DbContext { public MinimalBlogDbContext(DbContextOptions options) : base(options) { } public DbSet<Article> Articles { get; set; } = default!; public DbSet<Category> Categories { get; set; } = default!; public DbSet<Author> Authors { get; set; } = default!; public DbSet<Blog> Blogs { get; set; } = default!; }
ایجاد و اجرای Migrations
پس از تعریف MinimalBlogDbContext، اکنون نوبت به ایجاد کلاسهای Migrations و اجرای آنها جهت ایجاد بانک اطلاعاتی متناظر با مدلهای برنامه است. برای این منظور در ابتدا به پروژهی Api مراجعه کرده و فایل appsettings.json را به صورت زیر تکمیل میکنیم:
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "ConnectionStrings": { "Default": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=MinimalBlog;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False" } }
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net6.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> </PropertyGroup> <ItemGroup> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.2" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.2" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.2"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> </ItemGroup> <ItemGroup> <ProjectReference Include="..\MinimalBlog.Domain\MinimalBlog.Domain.csproj" /> <ProjectReference Include="..\MinimalBlog.Dal\MinimalBlog.Dal.csproj" /> </ItemGroup> </Project>
using Microsoft.EntityFrameworkCore; using MinimalBlog.Dal; var builder = WebApplication.CreateBuilder(args); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var connectionString = builder.Configuration.GetConnectionString("Default"); builder.Services.AddDbContext<MinimalBlogDbContext>(opt => opt.UseSqlServer(connectionString));
پس از این تنظیمات به پوشهی MinimalBlog.Dal وارد شده و فایل _01-add_migrations.cmd را اجرا میکنیم (این فایل به همراه پیوست قسمت اول، ارائه شدهاست). محتوای این فایل به صورت زیر است:
For /f "tokens=2-4 delims=/ " %%a in ('date /t') do (set mydate=%%c_%%a_%%b) For /f "tokens=1-2 delims=/:" %%a in ("%TIME: =0%") do (set mytime=%%a%%b) dotnet tool update --global dotnet-ef --version 6.0.2 dotnet build dotnet ef migrations --startup-project ../MinimalBlog.Api/ add V%mydate%_%mytime% --context MinimalBlogDbContext pause
البته چون کدهای تولید شدهی به صورت خودکار الزاما با Roslyn analyzers برنامه سازگاری ندارند، آنها را توسط فایل مخصوص editorconfig. قرار گرفتهی در پوشهی MinimalBlog.Dal\Migrations، از لیست آنالیزهای برنامه خارج میکنیم. محتوای این فایل ویژه به صورت زیر است:
[*.cs] generated_code = true
پس از ایجاد کلاسهای Migration، اکنون نوبت به اجرای آنها بر روی بانک اطلاعاتی است که در رشتهی اتصالی مشخص کردیم. برای اینکار فایل MinimalBlog.Dal\_02-update_db.cmd را اجرا میکنیم که چنین محتویاتی دارد:
dotnet tool update --global dotnet-ef --version 6.0.2 dotnet build dotnet ef --startup-project ../MinimalBlog.Api/ database update --context MinimalBlogDbContext pause
ویژگی های جدید در SQL Server 2019
Features and performance
Sql server 2019 will help us by making data integration, Management and intelligence easier and more intuitive than ever earlier.
Major Key points
-
Single virtual data layer
-
Data virtualization and Integrating Data
-
No data replication and Managing all data
-
Spark Built-In
-
Unified platform for big data analytics
-
Spark jobs
-
Train machine learning models
Asp.Net Identity #2
در مرحلهی بعد سه پکیج زیر را باید نصب کنیم :
- Microsoft.AspNet.Identity.EntityFramework
- Microsoft.AspNet.Identity.OWIN
- Microsoft.Owin.Host.SystemWeb
<connectionStrings> <add name="IdentityDb" providerName="System.Data.SqlClient" connectionString="Data Source=(localdb)\v11.0;Initial Catalog=IdentityDb;Integrated Security=True;Connect Timeout=15;Encrypt=False;TrustServerCertificate=False;MultipleActiveResultSets=True"/> </connectionStrings>
<appSettings> <add key="webpages:Version" value="3.0.0.0" /> <add key="webpages:Enabled" value="false" /> <add key="ClientValidationEnabled" value="true" /> <add key="UnobtrusiveJavaScriptEnabled" value="true" /> <add key="owin:AppStartup" value="Users.IdentityConfig" /> </appSettings>
ساخت کلاس User
using System; using Microsoft.AspNet.Identity.EntityFramework; namespace Users.Models { public class AppUser : IdentityUser { // پروپرتیهای اضافی در اینجا } }
ساخت کلاس Database Context برنامه
using System.Data.Entity; using Microsoft.AspNet.Identity.EntityFramework; using Users.Models; namespace Users.Infrastructure { public class AppIdentityDbContext : IdentityDbContext<AppUser> { public AppIdentityDbContext() : base("IdentityDb") { } static AppIdentityDbContext() { Database.SetInitializer<AppIdentityDbContext>(new IdentityDbInit()); } public static AppIdentityDbContext Create() { return new AppIdentityDbContext(); } } public class IdentityDbInit : DropCreateDatabaseIfModelChanges<AppIdentityDbContext> { protected override void Seed(AppIdentityDbContext context) { PerformInitialSetup(context); base.Seed(context); } public void PerformInitialSetup(AppIdentityDbContext context) { // initial configuration will go here } } }
ساخت کلاس User Manager
کلاس UserManager حاوی متدهای بالا است. اگر دقت کنید، میبینید که تمامی متدهای بالا به کلمهی Async ختم میشوند. زیرا Asp.Net Identity تقریبا کل ویژگیهای برنامه نویسی Async را پیاده سازی کرده است و این بدین معنی است که عملیات میتوانند به صورت غیر همزمان اجرا شده و دیگر فعالیتها را بلوکه نکنند.
using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.EntityFramework; using Microsoft.AspNet.Identity.Owin; using Microsoft.Owin; using Users.Models; namespace Users.Infrastructure { public class AppUserManager : UserManager<AppUser> { public AppUserManager(IUserStore<AppUser> store) : base(store) { } public static AppUserManager Create( IdentityFactoryOptions<AppUserManager> options, IOwinContext context) { AppIdentityDbContext db = context.Get<AppIdentityDbContext>(); AppUserManager manager = new AppUserManager(new UserStore<AppUser>(db)); return manager; } } }
زمانی که Identity نیاز به وهلهای از کلاس AppUserManager داشته باشد، متد استاتیک Create را صدا خواهد زد. این عمل زمانی اتفاق میافتد که عملیاتی بر روی دادههای کاربر انجام گیرد. برای ساخت وهلهای از کلاس AppUserManager نیاز به کلاس <UserStor<AppUser دارد. کلاس <UserStore<T یک پیاده سازی از رابط <IUserStore<T توسط EF میباشد که وظیفهی آن فراهم کنندهی پیاده سازی Storage-Specific متدهای تعریف شده در کلاس User Manager (که در اینجا AppUserManager ) میباشد. برای ساخت <UserStore<T نیاز به وهلهای از کلاس AppIdentityDbContext میباشد که از طریق Owin به صورت زیر قابل دریافت است:
AppIdentityDbContext db = context.Get<AppIdentityDbContext>();
یک پیاده سازی از رابط IOwinContext، به عنوان پارامتر به متد Create پاس داده میشود. در این پیاده سازی، یک تابع جنریک به نام Get تعریف شده که اقدام به برگشت وهله ای از اشیای ثبت شدهی در کلاس شروع Owin مینماید.
ساخت کلاس شروع Owin
اگر خاطرتان باشد یک کلید در قسمت AppSettings فایل Web.config به صورت زیر تعریف کردیم:
<add key="owin:AppStartup" value="Users.IdentityConfig" />
قسمت Value کلید بالا از دو قسمت تشکیل شده است: Users بیانگر فضای نام برنامهی شماست و IdentityConfig بیانگر کلاس شروع میباشد. (البته شما میتوانید هر نام دلخواهی را برای کلاس شروع بگذارید. فقط دقت داشته باشید که نام کلاس شروع و مقدار، با کلیدی که تعریف میکنید یکی باشد)
Owin مستقل از ASP.NET اعلام شده است و قراردادهای خاص خودش را دارد. یکی از این قراردادها تعریف یک کلاس و وهله سازی آن به منظور بارگذاری و پیکربندی میان افزار و انجام دیگر کارهای پیکربندی که نیاز است، میباشد. به طور پیش فرض این کلاس Start نام دارد و در پوشهی App_Start تعریف میشود. این کلاس حاوی یک متد به نام Configuration میباشد که بوسیله زیرساخت Owin فراخوانی میشود و یک پیاده سازی از رابط Owin.IAppBuilder به عنوان آرگومان به آن پاس داده میشود که کار پشتیبانی از Setup میان افزار مورد نیاز برنامه را برعهده دارد.
using Microsoft.AspNet.Identity; using Microsoft.Owin; using Microsoft.Owin.Security.Cookies; using Owin; using Users.Infrastructure; namespace Users { public class IdentityConfig { public void Configuration(IAppBuilder app) { app.CreatePerOwinContext<AppIdentityDbContext>(AppIdentityDbContext.Create); app.CreatePerOwinContext<AppUserManager>(AppUserManager.Create); app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, LoginPath = new PathString("/Account/Login"), }); } } }
رابط IAppBuilder بوسیله تعدادی متد الحاقی که در کلاسهایی که در فضای نام Owin تعریف شدهاند، تکمیل شده است. متد CreatePerOwinContext کار ساخت وهلهای از کلاس AppUserManager و کلاس AppIdentityDbContext را برای هر درخواست بر عهده دارد. این مورد تضمین میکند که هر درخواست، به یک دادهی تمیز از Asp.Net Identity دسترسی خواهد داشت و نگران اعمال همزمانی و یا کش ضعیف داده نخواهد بود. متد UseCookieAuthentication به Asp.Net Identity میگوید که چگونه از کوکیها برای تصدیق هویت کاربران استفاده کند که Optionهای آن از طریق کلاس CookieAuthenticationOptions مشخص میشود. مهمترین قسمت در اینجا پروپرتی LoginPath میباشد و مشخص میکند که کلاینتهای تصدیق هویت نشده، هنگام دسترسی به یک منبع محافظت شده، به کدام URL هدایت شوند که توسط یک رشته به متد PathString پاس داده میشود.
خوب دوستان برپایی سیستم Identity به پایان رسید. انشالله در قسمت بعدی به چگونگی استفادهی از این سیستم خواهیم پرداخت.
EF Code First #2
- اکثر خطاهای EF به صورت inner exception است. یعنی صفحه نمایش استثناء رو باید باز کنید و کمی درخت نمایش داده شده را پیمایش کنید تا به inner exception برسید.
- ریز مسایل به روز رسانی بانک اطلاعاتی، در قسمتهای 4 و 5 این سری بررسی شده. عجله نکنید. قدم به قدم ...
درخواست راهنمایی بیشتر
بنده با مطالعه (بخشی از) این مطالب:
مسیر راه ASP.NET MVC ،
بررسی مفاهیم معکوس سازی وابستگیها و ابزارهای مرتبط با آن ،
AutoMapper ،
چک لیست تهیه یک برنامه ASP.NET MVC ،
تا حد زیاد نحوه پیاده سازی back end پروژه را درک کردم (البته ابتدا پروژه فروشگاه اینترنتی شهر طلایی من که سادهتر است را بررسی کردم).
با این وجود بخش هایی از پروژه از نظر بنده همچنان پیچیده است!
- مدیریت کاربران، نقشها و تعیین دسترسی ها؛
- نحوه لاگ کردن فعالیت ها؛
- ارسال و دریافت پیام؛
- نحوه پیادهسازی front end پروژه (به طور خاص)،
لطفا در صورت امکان در این موارد (به ویژه مورد آخر طراحی UI) اگر منبعی یا مسیر راهی وجود دارد، معرفی نمایید.
با تشکر،
public enum CommentType { Article, Video, Event } public class Comment { public int Id { get; set; } public string CommentText { get; set; } public string User { get; set; } public int? TypeId { get; set; } public CommentType CommentType { get; set; } } public class Article { public int Id { get; set; } public string Title { get; set; } public string Slug { get; set; } public string Description { get; set; } } public class Video { public int Id { get; set; } public string Url { get; set; } public string Description { get; set; } } public class Event { public int Id { get; set; } public string Name { get; set; } public DateTimeOffset? Start { get; set; } public DateTimeOffset? End { get; set; } } public class MyDbContext : DbContext { public DbSet<Article> Articles { get; set; } public DbSet<Video> Videos { get; set; } public DbSet<Event> Events { get; set; } public DbSet<Comment> Comments { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder options) => options.UseSqlite("Data Source=polymorphic.db"); }
var articleComments = dbContext.Comments .Where(x => x.CommentType == CommentType.Article && x.TypeId.Value == 1); foreach (var articleComment in articleComments) { Console.WriteLine(articleComment.CommentText); }
public class Comment { public int Id { get; set; } public string CommentText { get; set; } public string User { get; set; } public virtual ICollection<ArticleComment> ArticleComments { get; set; } public virtual ICollection<VideoComment> VideoComments { get; set; } public virtual ICollection<EventComment> EventComments { get; set; } } public class Article { public Article() { ArticleComments = new HashSet<ArticleComment>(); } public int Id { get; set; } public string Title { get; set; } public string Slug { get; set; } public string Description { get; set; } public virtual ICollection<ArticleComment> ArticleComments { get; set; } } public class Video { public Video() { VideoComments = new HashSet<VideoComment>(); } public int Id { get; set; } public string Url { get; set; } public string Description { get; set; } public virtual ICollection<VideoComment> VideoComments { get; set; } } public class Event { public Event() { EventComments = new HashSet<EventComment>(); } public int Id { get; set; } public string Name { get; set; } public DateTimeOffset? Start { get; set; } public DateTimeOffset? End { get; set; } public virtual ICollection<EventComment> EventComments { get; set; } } public class MyDbContext : DbContext { public DbSet<Article> Articles { get; set; } public DbSet<ArticleComment> ArticleComments { get; set; } public DbSet<Video> Videos { get; set; } public DbSet<VideoComment> VideoComments { get; set; } public DbSet<Event> Events { get; set; } public DbSet<EventComment> EventComments { get; set; } public DbSet<Comment> Comments { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder options) => options.UseSqlite("Data Source=polymorphic.db"); protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<ArticleComment>(entity => { entity.HasKey(e => new { e.CommentId, e.ArticleId }) .HasName("PK_dbo.ArticleComments"); entity.HasIndex(e => e.ArticleId) .HasName("IX_ArticleId"); entity.HasIndex(e => e.CommentId) .HasName("IX_ArticleCommentId"); entity.HasOne(d => d.Article) .WithMany(p => p.ArticleComments) .HasForeignKey(d => d.ArticleId) .HasConstraintName("FK_dbo.ArticleComments_dbo.Articles_ArticleId"); entity.HasOne(d => d.Comment) .WithMany(p => p.ArticleComments) .HasForeignKey(d => d.CommentId) .HasConstraintName("FK_dbo.ArticleComments_dbo.Comments_CommentId"); }); modelBuilder.Entity<VideoComment>(entity => { entity.HasKey(e => new { e.CommentId, e.VideoId }) .HasName("PK_dbo.VideoComments"); entity.HasIndex(e => e.VideoId) .HasName("IX_VideoId"); entity.HasIndex(e => e.CommentId) .HasName("IX_VideoCommentId"); entity.HasOne(d => d.Video) .WithMany(p => p.VideoComments) .HasForeignKey(d => d.VideoId) .HasConstraintName("FK_dbo.VideoComments_dbo.Videos_VideoId"); entity.HasOne(d => d.Comment) .WithMany(p => p.VideoComments) .HasForeignKey(d => d.CommentId) .HasConstraintName("FK_dbo.VideoComments_dbo.Comments_CommentId"); }); modelBuilder.Entity<EventComment>(entity => { entity.HasKey(e => new { e.CommentId, e.EventId }) .HasName("PK_dbo.EventComments"); entity.HasIndex(e => e.EventId) .HasName("IX_EventId"); entity.HasIndex(e => e.CommentId) .HasName("IX_EventCommentId"); entity.HasOne(d => d.Event) .WithMany(p => p.EventComments) .HasForeignKey(d => d.EventId) .HasConstraintName("FK_dbo.EventComments_dbo.Events_EventId"); entity.HasOne(d => d.Comment) .WithMany(p => p.EventComments) .HasForeignKey(d => d.CommentId) .HasConstraintName("FK_dbo.EventComments_dbo.Comments_CommentId"); }); } }
var article = new Article { Title = "Article A", Slug = "article_a", Description = "No Description" }; var comment = new Comment { CommentText = "It's great", User = "Sirwan" }; dbContext.ArticleComments.Add(new ArticleComment { Article = article, Comment = comment }); dbContext.SaveChanges(); var articleOne = dbContext.Articles .Include(article => article.ArticleComments) .ThenInclude(comment => comment.Comment) .First(article => article.Id == 1); var article1Comments = articleOne.ArticleComments.Select(x => x.Comment); Console.WriteLine(article1Comments.Count());
public class Comment { public int Id { get; set; } public string CommentText { get; set; } public string User { get; set; } // Article public virtual Article Article { get; set; } public int? ArticleId { get; set; } // Video public virtual Video Video { get; set; } public int? VideoId { get; set; } // Event public virtual Event Event { get; set; } public int? EventId { get; set; } } public class Article { public int Id { get; set; } public string Title { get; set; } public string Slug { get; set; } public string Description { get; set; } public virtual ICollection<Comment> Comments { get; set; } } public class Video { public int Id { get; set; } public string Url { get; set; } public string Description { get; set; } public virtual ICollection<Comment> Comments { get; set; } } public class Event { public int Id { get; set; } public string Name { get; set; } public DateTimeOffset? Start { get; set; } public DateTimeOffset? End { get; set; } public virtual ICollection<Comment> Comments { get; set; } } public class MyDbContext : DbContext { public DbSet<Article> Articles { get; set; } public DbSet<Video> Videos { get; set; } public DbSet<Event> Events { get; set; } public DbSet<Comment> Comments { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder options) => options.UseSqlite("Data Source=polymorphic.db"); }
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Comment>(entity => entity.HasCheckConstraint("CHECK_FKs", "(`ArticleId` IS NOT NULL) AND (`VideoId` IS NOT NULL) AND (`EventId` IS NOT NULL)")); }
var articles = dbContext.Articles .Include(x => x.Comments).Where(x => x.Id == 1); foreach (var article in articles) { Console.WriteLine($"{article.Title} - Comments: {article.Comments.Count}"); } var comment = dbContext.Comments.Include(x => x.Article) .FirstOrDefault(x => x.Id == 1); Console.WriteLine(comment?.Article.Title);
قصد داریم تا مانند مثال قسمت قبل، مجموعه ای از اطلاعات مربوط به مرورگرهای مختلف را در یک جدول نشان دهیم، اما این بار منبع داده ما فرق میکند. منبع داده از طرف سرور فراهم میشود. هر مرورگر - همان طور که در قسمت قبل مشاهده نمودید - شامل اطلاعات زیر خواهد بود:
- موتور رندرگیری (Engine)
- نام مرورگر (Name)
- پلتفرم (Platform)
- نسخه موتور (Version)
- نمره سی اس اس (Grade)
به همین دلیل در سمت سرور، کلاسی خواهیم ساخت که نمایانگر یک مرورگر باشد. بدین صورت:
public class Browser { public int Id { get; set; } public string Engine { get; set; } public string Name { get; set; } public string Platform { get; set; } public float Version { get; set; } public string Grade { get; set; } }
این روش، یکی از امکانات jQuery DataTables است که با استفاده از آن، کلاینت تنها یک مصرف کننده صرف خواهد بود و وظیفه پردازش اطلاعات - یعنی تعداد رکوردهایی که برگشت داده میشود، صفحه بندی، مرتب سازی، جستجو، و غیره - به عهده سرور خواهد بود.
برای به کار گیری این روش، اولین کار این است که ویژگی bServerSide را true کنیم، مثلا بدین صورت:
var $table = $('#browsers-grid'); $table.dataTable({ "bServerSide": true, "sAjaxSource": "/Home/GetBrowsers" });
همچنین ویژگی sAjaxSource را به Url ی که باید دادهها از آن دریافت شوند مقداردهی میکنیم.
به صورت پیش فرض مقدار ویژگی bServerSide مقدار false است؛ که یعنی منبع داده این پلاگین از سمت سرور خوانده نشود. اگر true باشد منبع داده و خیلی اطلاعات دیگر مربوط به دادههای درون جدول باید از سرور به مرورگر کاربر پس فرستاده شوند. با true کردن مقدار bServerSide، آنگاه DataTables اطلاعاتی را راجع به شماره صفحه جاری، اندازه هر صفحه، شروط فیلتر کردن داده ها، مرتب سازی ستون ها، و غیره را به سرور میفرستد. همجنین انتظار میرود تا سرور در پاسخ به این درخواست، دادههای مناسبی را به فرمت JSON به مرورگر پس بفرستد. در حالتی که bServerSide مقدار true به خود بگیرد، پلاگین فقط رابطه متقابل بین کاربر و سرور را مدیریت میکند و هیچ پردازشی را انجام نمیدهد.
در این درخواست XHR یا Ajax ی پارامترهایی که به سرور ارسال میشوند اینها هستند:
iDisplayStart عدد صحیح
نقظه شروع مجموعه داده جاری
iDisplayLength عدد صحیح
تعداد رکوردهایی که جدول میتواند نمایش دهد. تعداد رکوردهایی که از طرف سرور برگشت داده میشود باید با این عدد یکسان باشند.
iColumns عدد صحیح
تعداد ستونهایی که باید نمایش داده شوند.
sSearch رشته
فیلد جستجوی عمومی
bRegex بولین
اگر true باشد معنی آن این است که میتوان از عبارات باقاعده برای جستجوی عبارتی خاص در کل ستونهای جدول استفاده کرد. مثلا در کادر جستجو نوشت :
^[1-5]$
sSearch_(int) رشته
فیلتر مخصوص هر ستون. اگر از ویژگی multi column filtering پلاگین استفاده شود به صورت sSearch0 ، sSearch1 ، sSeach2 و ... به طرف سرور ارسال میشوند. شماره انتهای هر کدام از پارامترها بیانگر شماره ستون جدول است.
bRegex_(int) بولین
اگر true باشد، بیان میکند که میتوان از عبارت با قاعده در ستون شماره int جهت جستجو استفاده کرد.
bSortable_(int) بولین
مشخص میکند که آیا یک ستون در سمت کلاینت، قابلیت مرتب شدن بر اساس آن وجود دارد یا نه. (در اینجا int اندیس ستون را مشخص میکند)
iSortingCols عدد صحیح
تعداد ستون هایی که باید مرتب سازی بر اساس آنها صورت پذیرد. در صورتی که از امکان multi column sorting استفاده کنید این مقدار میتواند بیش از یکی باشد.
iSortCol_(int) عدد صحیح
شماره ستونی که باید بر اساس آن عملیات مرتب سازی صورت پذیرد.
sSortDir_(int) رشته
نحوه مرتب سازی ؛ شامل صعودی (asc) یا نزولی (desc)
mDataProp_(int) رشته
اسم ستونهای درون جدول را مشخص میکند.
sEcho رشته
اطلاعاتی که datatables از آن برای رندر کردن جدول استفاده میکند.
شکل زیر نشان میدهد که چه پارامترهایی به سرور ارسال میشوند.
بعضی از این پارامترها بسته به تعداد ستونها قابل تغییر هستند. (آن پارامترهایی که آخرشان یک عدد هست که نشان دهنده شماره ستون مورد نظر میباشد)
در پاسخ به هر درخواست XHR که datatables به سرور میفرستد، انتظار دارد تا سرور نیز یک شیء json را با فرمت مخصوص که شامل پارامترهای زیر میشود به او پس بفرستد:
iTotalRecords عدد صحیح
تعداد کل رکوردها (قبل از عملیات جستجو) یا به عبارت دیگر تعداد کل رکوردهای درون آن جدول از دیتابیس که دادهها باید از آن دریافت شوند. تعداد کل رکوردهایی که در طرف سرور وجود دارند. این مقدار فقط برای نمایش به کاربر برگشت داده میشود و نیز از آن برای صفحه بندی هم استفاده میشود.
iTotalDisplayRecords عدد صحیح
تعداد کل رکوردها (بعد از عملیات جستجو) یا به عبارت دیگر تعداد کل رکوردهایی که بعد از عملیات جستجو پیدا میشوند نه فقط آن تعداد رکوردی که به کاربر پس فرستاده میشوند. تعداد کل رکوردهایی که با شرط جستجو مطابقت دارند. اگر کاربر چیزی را جستجو نکرده باشد مقدار این پارامتر با پارامتر iTotalRecords یکسان خواهد بود.
sEcho عدد صحیح
یک عدد صحیح است که در قالب رشته در تعامل بین سرور و کلاینت جا به جا میشود. این مقدار به ازاء هر درخواست تغییر میکند. همان مقداری که مرورگر به سرور میدهد را سرور هم باید به مرورگر تحویل بدهد. برای جلوگیری از حملات XSS باید آن را تبدیل به عدد صحیح کرد. پلاگین DataTables مقدار این پارامتر را برای هماهنگ کردن و منطبق کردن درخواست ارسال شده و جواب این درخواست استفاده میکند. همان مقداری که مروگر به سرور میدهد را باید سرور تحویل به مرورگر بدهد.
sColumns رشته
اسم ستونها که با استفاده از کاما از هم جدا شده اند. استفاده از آن اختیاری است و البته منسوخ هم شده است و در نسخههای جدید jQuery DataTables از آن پشتیبانی نمیشود.
aaData آرایه
همان طور که قبلا هم گفتیم، مقادیر سلول هایی را که باید در جدول نشان داده شوند را در خود نگهداری میکند. یعنی در واقع دادههای جدول در آن ریخته میشوند. هر وقت که DataTables دادههای مورد نیازش را دریافت میکند، سلولهای جدول html مربوطه اش را از روی آرایه aaData ایجاد میکند. تعداد ستونها در این آرایه دو بعدی، باید با تعداد ستونهای جدول html مربوطه به آن یکسان باشد
شکل زیر پارامترها دریافتی از سرور را نشان میدهند:
همان طور که گفتیم، کلاینت به سرور یک سری پارامترها را ارسال میکند و آن پارامترها را هم شرح دادیم. برای دریافت این پارامترها طرف سرور، احتیاج به یک مدل هست. این مدل به صورت زیر پیاده سازی خواهد شد:
/// <summary> /// Class that encapsulates most common parameters sent by DataTables plugin /// </summary> public class jQueryDataTableParamModel { /// <summary> /// Request sequence number sent by DataTable, /// same value must be returned in response /// </summary> public string sEcho { get; set; } /// <summary> /// Text used for filtering /// </summary> public string sSearch { get; set; } /// <summary> /// Number of records that should be shown in table /// </summary> public int iDisplayLength { get; set; } /// <summary> /// First record that should be shown(used for paging) /// </summary> public int iDisplayStart { get; set; } /// <summary> /// Number of columns in table /// </summary> public int iColumns { get; set; } /// <summary> /// Number of columns that are used in sorting /// /// </summary> public int iSortingCols { get; set; } /// <summary> /// Comma separated list of column names /// </summary> public string sColumns { get; set; } }
مدل بایندر mvc وظیفه مقداردهی به خصوصیات درون این کلاس را بر عهده دارد، بقیه پارامترهایی که به سرور ارسال میشوند و در این کلاس نیامده اند، از طریق شیء Request در دسترس خواهند بود.
اکشن متدی که مدل بالا را دریافت میکند، میتواند به صورت زیر پیاده سازی شود. این اکشن متد وظیفه پاسخ دادن به درخواست DataTables بر اساس پارامترهای ارسال شده در مدل DataTablesParam را دارد. خروجی این اکشن متد شامل پارارمترهای مورد نیاز پلاگین DataTables برای تشکیل جدول است که آنها را هم شرح دادیم.
public JsonResult GetBrowsers(jQueryDataTableParamModel param) { IQueryable<Browser> allBrowsers = new Browsers().CreateInMemoryDataSource().AsQueryable(); IEnumerable<Browser> filteredBrowsers; // Apply Filtering if (!string.IsNullOrEmpty(param.sSearch)) { filteredBrowsers = new Browsers().CreateInMemoryDataSource() .Where(x => x.Engine.Contains(param.sSearch) || x.Grade.Contains(param.sSearch) || x.Name.Contains(param.sSearch) || x.Platform.Contains(param.sSearch) ).ToList(); float f; if (float.TryParse(param.sSearch, out f)) { filteredBrowsers = filteredBrowsers.Where(x => x.Version.Equals(f)); } } else { filteredBrowsers = allBrowsers; } // Apply Sorting var sortColumnIndex = Convert.ToInt32(Request["iSortCol_0"]); Func<Browser, string> orderingFunction = (x => sortColumnIndex == 0 ? x.Engine : sortColumnIndex == 1 ? x.Name : sortColumnIndex == 2 ? x.Platform : sortColumnIndex == 3 ? x.Version.ToString() : sortColumnIndex == 4 ? x.Grade : x.Name); var sortDirection = Request["sSortDir_0"]; // asc or desc filteredBrowsers = sortDirection == "asc" ? filteredBrowsers.OrderBy(orderingFunction) : filteredBrowsers.OrderByDescending(orderingFunction); // Apply Paging var enumerable = filteredBrowsers.ToArray(); IEnumerable<Browser> displayedBrowsers = enumerable.Skip(param.iDisplayStart). Take(param.iDisplayLength).ToList(); return Json(new { sEcho = param.sEcho, iTotalRecords = allBrowsers.Count(), iTotalDisplayRecords = enumerable.Count(), aaData = displayedBrowsers }, JsonRequestBehavior.AllowGet); }
تشریح اکشن متد GetBrowsers :
این اکشن متد از مدل jQueryDataTableParamModel به عنوان پارامتر ورودی خود استفاده میکند. این مدل همان طور هم که گفتیم، شامل یک سری خصوصیت است که توسط پلاگین jQuery DataTables مقداردهی میشوند و همچنین مدل بایندر mvc وظیفه بایند کردن این مقادیر به خصوصیات درون این کلاس را بر عهده خواهد داشت. درون بدنه اکشن متد GetBrowsers دادهها بعد از اعمال عملیات فیلترینگ، مرتب سازی، و صفحه بندی به فرمت مناسبی درآمده و به طرف مرورگر فرستاده خواهند شد.
برای پیاده سازی کدهای طرف کلاینت نیز، درون یک View کدهای زیر قرار خواهند گرفت:
$(function () { var $table = $('#browsers-grid'); $table.dataTable({ "bProcessing": true, "bStateSave": true, "bServerSide": true, "bFilter": true, "sDom": 'T<"clear">lftipr', "aLengthMenu": [[5, 10, 25, 50, -1], [5, 10, 25, 50, "All"]], "bAutoWidth": false, "sAjaxSource": "/Home/GetBrowsers", "fnServerData": function (sSource, aoData, fnCallback) { $.ajax({ "dataType": 'json', "type": "POST", "url": sSource, "data": aoData, "success": fnCallback }); }, "aoColumns": [ { "mDataProp": "Engine" }, { "mDataProp": "Name" }, { "mDataProp": "Platform" }, { "mDataProp": "Version" }, { "mDataProp": "Grade" } ], "oLanguage": { "sUrl": "/Content/dataTables.persian.txt" } }); });
تشریح کدها:
fnServerData :
این متد، در واقع نحوه تعامل سرور و کلاینت را با استفاده از درخواستهای XHR مشخص خواهد کرد.
oLanguage :
برای فعال سازی زبان فارسی، فیلدهای مورد نیاز ترجمه شده و در یک فایل متنی قرار داده شده اند. کافی است آدرس این فایل متنی به ویژگی oLanguage اختصاص داده شوند.
مثال این قسمت را از لینک زیر دریافت کنید:
DataTablesTutorial04.zip
لازم به ذکر است پوشه bin، obj، و packages جهت کاهش حجم این مثال از solution حذف شده اند. برای اجرای این مثال از اینجا کمک بگیرید.
مطالعه بیشتر
برای مطالعه بیشتر در مورد این پلاگین و نیز پیاده سازی آن در MVC میتوانید به لینک زیر نیز مراجعه بفرمائید که بعضی از قسمتهای این مطلب هم از مقاله زیر استفاده کرده است:
jQuery DataTables and ASP.NET MVC Integration - Part I