با سلام. یک سوال در رابطه با جدول UserToken داشتم. من برای ذخیره کردن توکن برای کاربران میخوام از این جدول استفاده کنم و فیلدهای LoginProvider و Name هم به عنوان کلید اصلی در نظر گرفته شده است. در هنگام درج رکورد چه مقادیری باید به این دو فیلد بدهم. یا باید جدول دیگه ای برای این کار اضافه کنم؟
نظرات مطالب
EF Code First #3
چطور من میتونم در code first یک جدول بدون کلید داشته باشم ؟
یک جدول طراحی کنید به نام mass mail که در آن یک Content به علاوه فیلد IsDone وجود داره. مدیر سیستم متن خودش رو به صورت معمول در این جدول ثبت کنه. در وظیفهی پس زمینه، رکوردهایی را که IsDone آنها false است یکی یکی یافته و پردازش کنید. بعد از اتمام کار، IsDone هر رکورد را true کنید.
مزیت این روش اینه که دو ترد کاملا مجزای رابط کاربری و ترد وظیفهی پس زمینه، هیچ تداخل و تماسی با هم نخواهند داشت تا سبب کرش برنامه شوند.
یک سناریوی فرضی را در نظر بگیرید. اگر بخواهیم IdentityDbContext و دیگر DbContextهای اپلیکیشن را ادغام کنیم چه باید کرد؟ مثلا یک سیستم وبلاگ که برخی کاربران میتوانند پست جدید ثبت کنند، برخی تنها میتوانند کامنت بگذارند و تمامی کاربران هم اختیارات مشخص دیگری دارند. در چنین سیستمی شناسه کاربران (User ID) در بسیاری از مدلها (موجودیتها و مدلهای اپلیکیشن) وجود خواهد داشت تا مشخص شود هر رکورد به کدام کاربر متعلق است. در این مقاله چنین سناریو هایی را بررسی میکنیم و best practiceهای مربوطه را مرور میکنیم.
پس از ورود به سایت بعنوان یک مدیر، میتوانید ToDoهای ثبت شده توسط تمام کاربران را مشاهده کنید.
در این پست یک اپلیکیشن ساده ToDo خواهیم ساخت که امکان تخصیص to-doها به کاربران را نیز فراهم میکند. در این مثال خواهیم دید که چگونه میتوان مدلهای مختص به سیستم عضویت (IdentityDbContext) را با مدلهای دیگر اپلیکیشن مخلوط و استفاده کنیم.
تعریف نیازمندیهای اپلیکیشن
- تنها کاربران احراز هویت شده قادر خواهند بود تا لیست ToDoهای خود را ببینند، آیتمهای جدید ثبت کنند یا دادههای قبلی را ویرایش و حذف کنند.
- کاربران نباید آیتمهای ایجاد شده توسط دیگر کاربران را ببینند.
- تنها کاربرانی که به نقش Admin تعلق دارند باید بتوانند تمام ToDoهای ایجاد شده را ببینند.
پس بگذارید ببینیم چگونه میشود اپلیکیشنی با ASP.NET Identity ساخت که پاسخگوی این نیازمندیها باشد.
ابتدا یک پروژه ASP.NET MVC جدید با مدل احراز هویت Individual User Accounts بسازید. در این اپلیکیشن کاربران قادر خواهند بود تا بصورت محلی در وب سایت ثبت نام کنند و یا با تامین کنندگان دیگری مانند گوگل و فیسبوک وارد سایت شوند. برای ساده نگاه داشتن این پست ما از حسابهای کاربری محلی استفاده میکنیم.
در مرحله بعد ASP.NET Identity را راه اندازی کنید تا بتوانیم نقش مدیر و یک کاربر جدید بسازیم. میتوانید با اجرای اپلیکیشن راه اندازی اولیه را انجام دهید. از آنجا که سیستم ASP.NET Identity توسط Entity Framework مدیریت میشود میتوانید از تنظیمات پیکربندی Code First برای راه اندازی دیتابیس خود استفاده کنید.
در قدم بعدی راه انداز دیتابیس را در Global.asax تعریف کنید.
Database.SetInitializer<MyDbContext>(new MyDbInitializer());
ایجاد نقش مدیر و کاربر جدیدی که به این نقش تعلق دارد
اگر به قطعه کد زیر دقت کنید، میبینید که در خط شماره 5 متغیری از نوع UserManager ساخته ایم که امکان اجرای عملیات گوناگونی روی کاربران را فراهم میکند. مانند ایجاد، ویرایش، حذف و اعتبارسنجی کاربران. این کلاس که متعلق به سیستم ASP.NET Identity است همتای SQLMembershipProvider در ASP.NET 2.0 است.
در خط 6 یک RoleManager میسازیم که امکان کار با نقشها را فراهم میکند. این کلاس همتای SQLRoleMembershipProvider در ASP.NET 2.0 است.
در این مثال نام کلاس کاربران (موجودیت کاربر در IdentityDbContext) برابر با "MyUser" است، اما نام پیش فرض در قالبهای پروژه VS 2013 برابر با "ApplicationUser" میباشد.
public class MyDbInitializer : DropCreateDatabaseAlways<MyDbContext> { protected override void Seed(MyDbContext context) { var UserManager = new UserManager<MyUser>(new UserStore<MyUser>(context)); var RoleManager = new RoleManager<IdentityRole>(new RoleStore<IdentityRole>(context)); string name = "Admin"; string password = "123456"; //Create Role Admin if it does not exist if (!RoleManager.RoleExists(name)) { var roleresult = RoleManager.Create(new IdentityRole(name)); } //Create User=Admin with password=123456 var user = new MyUser(); user.UserName = name; var adminresult = UserManager.Create(user, password); //Add User Admin to Role Admin if (adminresult.Succeeded) { var result = UserManager.AddToRole(user.Id, name); } base.Seed(context); } }
حال فایلی با نام Models/AppModels.cs بسازید و مدل EF Code First اپلیکیشن را تعریف کنید. از آنجا که از EF استفاده میکنیم، روابط کلیدها بین کاربران و ToDoها بصورت خودکار برقرار میشود.
public class MyUser : IdentityUser { public string HomeTown { get; set; } public virtual ICollection<ToDo> ToDoes { get; set; } } public class ToDo { public int Id { get; set; } public string Description { get; set; } public bool IsDone { get; set; } public virtual MyUser User { get; set; } }
در قدم بعدی با استفاده از مکانیزم Scaffolding کنترلر جدیدی بهمراه تمام Viewها و متدهای لازم برای عملیات CRUD بسازید. برای اطلاعات بیشتر درباره نحوه استفاده از مکانیزم Scaffolding به این لینک مراجعه کنید.
لطفا دقت کنید که از DbContext فعلی استفاده کنید. این کار مدیریت دادههای Identity و اپلیکیشن شما را یکپارچهتر میکند. DbContext شما باید چیزی شبیه به کد زیر باشد.
public class MyDbContext : IdentityDbContext<MyUser> { public MyDbContext() : base("DefaultConnection") { } protected override void OnModelCreating(DbModelBuilder modelBuilder) { public System.Data.Entity.DbSet<AspnetIdentitySample.Models.ToDo> ToDoes { get; set; } }
تنها کاربران احراز هویت شده باید قادر به اجرای عملیات CRUD باشند
برای این مورد از خاصیت Authorize استفاده خواهیم کرد که در MVC 4 هم وجود داشت. برای اطلاعات بیشتر لطفا به این لینک مراجعه کنید.
[Authorize] public class ToDoController : Controller
کنترلر ایجاد شده را ویرایش کنید تا کاربران را به ToDoها اختصاص دهد. در این مثال تنها اکشن متدهای Create و List را بررسی خواهیم کرد. با دنبال کردن همین روش میتوانید متدهای Edit و Delete را هم بسادگی تکمیل کنید.
یک متد constructor جدید بنویسید که آبجکتی از نوع UserManager میپذیرد. با استفاده از این کلاس میتوانید کاربران را در ASP.NET Identity مدیریت کنید.
private MyDbContext db; private UserManager<MyUser> manager; public ToDoController() { db = new MyDbContext(); manager = new UserManager<MyUser>(new UserStore<MyUser>(db)); }
اکشن متد Create را بروز رسانی کنید
هنگامی که یک ToDo جدید ایجاد میکنید، کاربر جاری را در ASP.NET Identity پیدا میکنیم و او را به ToDoها اختصاص میدهیم.
public async Task<ActionResult> Create ([Bind(Include="Id,Description,IsDone")] ToDo todo) { var currentUser = await manager.FindByIdAsync (User.Identity.GetUserId()); if (ModelState.IsValid) { todo.User = currentUser; db.ToDoes.Add(todo); await db.SaveChangesAsync(); return RedirectToAction("Index"); } return View(todo); }
اکشن متد List را بروز رسانی کنید
در این متد تنها ToDoهای کاربر جاری را باید بگیریم.
public ActionResult Index() { var currentUser = manager.FindById(User.Identity.GetUserId()); return View(db.ToDoes.ToList().Where( todo => todo.User.Id == currentUser.Id)); }
تنها مدیران سایت باید بتوانند تمام ToDoها را ببینند
بدین منظور ما یک اکشن متد جدید به کنترل مربوطه اضافه میکنیم که تمام ToDoها را لیست میکند. اما دسترسی به این متد را تنها برای کاربرانی که در نقش مدیر وجود دارند میسر میکنیم.
[Authorize(Roles="Admin")] public async Task<ActionResult> All() { return View(await db.ToDoes.ToListAsync()); }
نمایش جزئیات کاربران از جدول ToDo ها
از آنجا که ما کاربران را به ToDo هایشان مرتبط میکنیم، دسترسی به دادههای کاربر ساده است. مثلا در متدی که مدیر سایت تمام آیتمها را لیست میکند میتوانیم به اطلاعات پروفایل تک تک کاربران دسترسی داشته باشیم و آنها را در نمای خود بگنجانیم. در این مثال تنها یک فیلد بنام HomeTown اضافه شده است، که آن را در کنار اطلاعات ToDo نمایش میدهیم.
@model IEnumerable<AspnetIdentitySample.Models.ToDo> @{ ViewBag.Title = "Index"; } <h2>List of ToDoes for all Users</h2> <p> Notice that we can see the User info (UserName) and profile info such as HomeTown for the user as well. This was possible because we associated the User object with a ToDo object and hence we can get this rich behavior. 12: </p> <table class="table"> <tr> <th> @Html.DisplayNameFor(model => model.Description) </th> <th> @Html.DisplayNameFor(model => model.IsDone) </th> <th>@Html.DisplayNameFor(model => model.User.UserName)</th> <th>@Html.DisplayNameFor(model => model.User.HomeTown)</th> </tr> 25: 26: @foreach (var item in Model) 27: { 28: <tr> 29: <td> 30: @Html.DisplayFor(modelItem => item.Description) 31: </td> 32: <td> @Html.DisplayFor(modelItem => item.IsDone) </td> <td> @Html.DisplayFor(modelItem => item.User.UserName) </td> <td> @Html.DisplayFor(modelItem => item.User.HomeTown) </td> </tr> } </table>
صفحه Layout را بروز رسانی کنید تا به ToDoها لینک شود
<li>@Html.ActionLink("ToDo", "Index", "ToDo")</li> <li>@Html.ActionLink("ToDo for User In Role Admin", "All", "ToDo")</li>
حال اپلیکیشن را اجرا کنید. همانطور که مشاهده میکنید دو لینک جدید به منوی سایت اضافه شده اند.
ساخت یک ToDo بعنوان کاربر عادی
روی لینک ToDo کلیک کنید، باید به صفحه ورود هدایت شوید چرا که دسترسی تنها برای کاربران احراز هویت شده تعریف وجود دارد. میتوانید یک حساب کاربری محلی ساخته، با آن وارد سایت شوید و یک ToDo بسازید.
پس از ساختن یک ToDo میتوانید لیست رکوردهای خود را مشاهده کنید. دقت داشته باشید که رکوردهایی که کاربران دیگر ثبت کرده اند برای شما نمایش داده نخواهند شد.
مشاهده تمام ToDoها بعنوان مدیر سایت
روی لینک ToDoes for User in Role Admin کلیک کنید. در این مرحله باید مجددا به صفحه ورود هدایت شوید چرا که شما در نقش مدیر نیستید و دسترسی کافی برای مشاهده صفحه مورد نظر را ندارید. از سایت خارج شوید و توسط حساب کاربری مدیری که هنگام راه اندازی اولیه دیتابیس ساخته اید وارد سایت شوید.
User = Admin Password = 123456
چندین روش برای انجام مقایسه حساس به حروف کوچک و بزرگ (case sensitive) در SQL Server وجود دارد که در ادامه آنها را مرور خواهیم کرد:
ابتدا جدول موقتی زیر را جهت آزمایشات بعدی در نظر بگیرید
CREATE TABLE #tblTest
(
f1 NVARCHAR(50)
)
INSERT INTO #tblTest (f1) VALUES('Test1')
INSERT INTO #tblTest (f1) VALUES('TEST1')
الف) استفاده از collation صحیح
عموما هنگام نصب اس کیوال سرور از collation غیرحساس به کوچکی و بزرگی حروف استفاده میشود و این مورد سبب میشود که پیش فرض ایجاد دیتابیسها نیز به همین صورت باشد (هر چند کاملا قابل کنترل و تنظیم است). به صورت پویا میتوان این collation را در کوئریها نیز اعمال نمود. برای مثال:
SELECT f1 FROM #tblTest WHERE f1 COLLATE SQL_Latin1_General_CP1_CS_AS = 'Test1'
ب) استفاده از تابع BINARY_CHECKSUM اس کیوال سرور (نوعی الگوریتم ویژه، شبیه به امضای دیجیتال و هش کردن اطلاعات است)
SELECT f1 FROM #tblTest WHERE BINARY_CHECKSUM(f1) = BINARY_CHECKSUM('Test1')
SELECT f1 FROM #tblTest WHERE hashbytes('md5',f1) = hashbytes('md5',N'Test1')
SELECT f1 FROM #tblTest WHERE convert(varbinary(50),f1) = convert(varbinary(50),N'Test1')
- خیر. چندین نوع استراتژی برای تعیین PK وجود دارند که یکی از آنها فیلدهای Identity است و این تنها روش و الزاما بهترین روش نیست.
- مثلا زمانیکه با ORMها کار میکنید استفاده از فیلدهای Identity در حین ثبت تعداد بالایی از رکوردها مشکل ساز میشوند. چون این فیلدها تحت کنترل دیتابیس هستند و نه برنامه، ORM نیاز دارد پس از هربار Insert یکبار آخرین Id را از بانک اطلاعاتی واکشی کند. همین مساله یعنی افت سرعت در تعداد بالای Insertها (چون یکبار کوئری Insert باید ارسال شود و یکبار هم یک Select اضافی دوم برای دریافت Id تولیدی توسط دیتابیس).
- روش دوم تعیین PK استفاده از نوع Guid است. در این حالت، هم مشکل حذف رکوردها و خالی شدن یک شماره را در این بین ندارید و هم چون عموما تحت کنترل برنامه است، سرعت کار کردن با آن بالاتر است. فقط تنها مشکل آن زیبا نبودنش است در مقایسه با یک عدد ساده فیلدهای Identity.
در مورد فیلدهای Identity، تغییر شماره Id به صلاح نیست چون:
الف) همانطور که عنوان کردید روابط بین جداول را به هم خواهد ریخت.
ب) در یک وب سایت و یا هر برنامهای، کلا آدرسها و ارجاعات قدیمی را از بین میبرد. مثلا فرض کنید شماره این مطلب 1381 است و شما آنرا یادداشت کردهاید. در روزی بعد، برنامه نویس شماره Idها را کلا ریست کرده. در نتیجه یک هفته بعد شما به شماره 1381 ایی خواهید رسید که تطابقی با مطلب مدنظر شما ندارد (حالا فرض کنید که این عدد شماره پرونده یک شخص بوده یا شماره کاربری او و نتایج و خسارات حاصل را درنظر بگیرید).
ج) این خوب است که در بین اطلاعات یک ردیف خالی وجود دارد. چون بر این اساس میتوان بررسی کرد که آیا واقعا رکوردی حذف شده یا خیر. گاهی از اوقات کاربران ادعا میکنند که اطلاعات ارسالی آنها نیست در حالیکه نبود این رکوردها به دلیل حذف بوده و نه عدم ثبت آنها. با بررسی این Idها میشود با کاربران در این مورد بحث کرد و پاسخ مناسبی را ارائه داد.
و اگر شمارهای که به کاربر نمایش میدهید فقط یک شماره ردیف است (و از این لحاظ میخواهید که حتما پشت سرهم باشد)، بهتر است یک View جدید ایجاد کنید تا این Id خود افزاینده را تولید کند (بدون استفاده از pk جدول).
پ.ن.
هدف من از این توضیحات صرفا عنوان این بود که به PK به شکل یک فیلد read only نگاه کنید. این دقیقا برخوردی است که Entity framework با این مفهوم دارد و صحیح است و اصولی. اگر در یک کشور هر روزه عدهای به رحمت ایزدی میروند به این معنا نیست که سازمان ثبت احوال باید شماره شناسنامهها را هر ماه ریست کند!
- مثلا زمانیکه با ORMها کار میکنید استفاده از فیلدهای Identity در حین ثبت تعداد بالایی از رکوردها مشکل ساز میشوند. چون این فیلدها تحت کنترل دیتابیس هستند و نه برنامه، ORM نیاز دارد پس از هربار Insert یکبار آخرین Id را از بانک اطلاعاتی واکشی کند. همین مساله یعنی افت سرعت در تعداد بالای Insertها (چون یکبار کوئری Insert باید ارسال شود و یکبار هم یک Select اضافی دوم برای دریافت Id تولیدی توسط دیتابیس).
- روش دوم تعیین PK استفاده از نوع Guid است. در این حالت، هم مشکل حذف رکوردها و خالی شدن یک شماره را در این بین ندارید و هم چون عموما تحت کنترل برنامه است، سرعت کار کردن با آن بالاتر است. فقط تنها مشکل آن زیبا نبودنش است در مقایسه با یک عدد ساده فیلدهای Identity.
در مورد فیلدهای Identity، تغییر شماره Id به صلاح نیست چون:
الف) همانطور که عنوان کردید روابط بین جداول را به هم خواهد ریخت.
ب) در یک وب سایت و یا هر برنامهای، کلا آدرسها و ارجاعات قدیمی را از بین میبرد. مثلا فرض کنید شماره این مطلب 1381 است و شما آنرا یادداشت کردهاید. در روزی بعد، برنامه نویس شماره Idها را کلا ریست کرده. در نتیجه یک هفته بعد شما به شماره 1381 ایی خواهید رسید که تطابقی با مطلب مدنظر شما ندارد (حالا فرض کنید که این عدد شماره پرونده یک شخص بوده یا شماره کاربری او و نتایج و خسارات حاصل را درنظر بگیرید).
ج) این خوب است که در بین اطلاعات یک ردیف خالی وجود دارد. چون بر این اساس میتوان بررسی کرد که آیا واقعا رکوردی حذف شده یا خیر. گاهی از اوقات کاربران ادعا میکنند که اطلاعات ارسالی آنها نیست در حالیکه نبود این رکوردها به دلیل حذف بوده و نه عدم ثبت آنها. با بررسی این Idها میشود با کاربران در این مورد بحث کرد و پاسخ مناسبی را ارائه داد.
و اگر شمارهای که به کاربر نمایش میدهید فقط یک شماره ردیف است (و از این لحاظ میخواهید که حتما پشت سرهم باشد)، بهتر است یک View جدید ایجاد کنید تا این Id خود افزاینده را تولید کند (بدون استفاده از pk جدول).
پ.ن.
هدف من از این توضیحات صرفا عنوان این بود که به PK به شکل یک فیلد read only نگاه کنید. این دقیقا برخوردی است که Entity framework با این مفهوم دارد و صحیح است و اصولی. اگر در یک کشور هر روزه عدهای به رحمت ایزدی میروند به این معنا نیست که سازمان ثبت احوال باید شماره شناسنامهها را هر ماه ریست کند!
پیشنیازها
فعال سازی و پردازش صفحات پویای افزودن، ویرایش و حذف رکوردهای jqGrid در ASP.NET MVC
اعتبارسنجی سفارشی سمت کاربر و سمت سرور در jqGrid
پیشتر با نحوهی فعال سازی صفحات پویای افزودن، ویرایش و حذف رکوردهای jqGrid آشنا شدیم. اما ... شاید علاقمند نباشید که اصلا از این صفحات استفاده کنید. شاید به نظر شما با کلیک بر روی دکمهی + افزودن یک رکورد جدید، بهتر باشد داخل خود گرید، یک سطر خالی جدید باز شده تا بتوان آنرا پر کرد. شاید این نحو کار کردن با گرید، از دید عدهای طبیعیتر باشد نسبت به حالت نمایش صفحات popup افزودن و یا ویرایش رکوردها. در ادامه این مورد را بررسی خواهیم کرد.
فعال سازی افزودن، ویرایش و حذف Inline
فعال سازی ویرایش و حذف Inline را پیشتر نیز بررسی کرده بودیم. تنها کافی است یک ستون جدید را با 'formatter: 'actions تعریف کنیم. به صورت خودکار، دکمهی ویرایش، حذف، ذخیره سازی و لغو Inline ظاهر میشوند و همچنین بدون نیاز به کدنویسی بیشتری کار میکنند.
اما در کدهای ذیل اندکی این ستون را سفارشی سازی کردهایم. در قسمت formatter آن، دکمههای edit و delete یک سطر جدید توکار اضافه شده را حذف کردهایم. زیرا در این حالت خاص، وجود این دکمهها ضروری نیستند. بهتر است در این حالت دکمههای save و cancel ظاهر شوند:
قسمتی که کار فعال سازی Inline Add را انجام میدهد، تعریف مرتبط با inlineNav است که به انتهای تعاریف متداول گرید اضافه شدهاست.
در اینجا 4 دکمهی ویرایش، جدید، ذخیره و لغو، در نوار pager پایین گرید ظاهر خواهند شد (سمت چپ؛ سمت راست همان دکمههای نمایش فرمهای پویا هستند).
سپس باید دو قسمت مهم addParams و editParams آنرا مقدار دهی کرد.
در قسمت addParams، مشخص میکنیم که ID ردیف اضافه شده، مساوی کلمهی _empty باشد. اگر به کدهای formatter ستون action دقت کنید، از این ID برای تشخیص افزوده شدن یک ردیف جدید استفاده شدهاست.
position در اینجا به معنای محل افزوده شدن یک ردیف خالی است. مقدار پیش فرض آن first است؛ یعنی همیشه در اولین ردیف گرید، این ردیف جدید اضافه میشود. در اینجا به last تنظیم شدهاست تا در پایین گرید و پس از رکوردهای موجود، نمایش داده شود.
useDefValues سبب استفاده از مقادیر پیش فرض تعریف شده در ستونهای گرید در حین افزوده شدن یک ردیف جدید میگردد.
addRowParams و editParams هر دو ساختار تقریبا یکسانی دارند که به نحو ذیل تعریف میشوند:
در ابتدای کار مشخص میکنیم که آدرسهای ذخیره سازی اطلاعات در سمت سرور برای حالتهای Add و Edit کداماند.
تنظیم restoreAfterError به false بسیار مهم است. اگر در سمت سرور خطای اعتبارسنجی گزارش شود و restoreAfterError مساوی true باشد (مقدار پیش فرض)، کاربر مجبور خواهد شد اطلاعات را دوباره وارد کند.
در روال رویدادگران oneditfunc دکمهی save و cancel ردیف را که مخفی هستند، ظاهر میکنیم (مکمل formatter ستون action است).
در قسمت successfunc، پس از پایان موفقیت آمیز کار، متد reloadGrid را فراخوانی میکنیم. اینکار سبب میشود تا Id واقعی رکورد، از سمت سرور دریافت شود. از این Id برای ویرایش و همچنین حذف، استفاده خواهد شد. علت استفاده از setTimeout در اینجا این است که اندکی به DOM فرصت داده شود تا کارش به پایان برسد.
در قسمت errorfunc خطاهای اعتبارسنجی سفارشی سمت سرور را میتوان دریافت و سپس توسط متد توکار info_dialog به کاربر نمایش داد.
یک نکتهی مهم در مورد ارسال خطاهای اعتبارسنجی از سمت سرور در حالت Inline Add
روال رویداد گردان errorfunc، اگر مقدار StatusCode بازگشتی از سمت سرور مساوی 200 باشد (حالت عادی و موفقیت آمیز)، مقدار stat مساوی error را باز نمیگرداند. به همین جهت است که در کدهای فوق، مقدار دهی this.Response.StatusCode را به 500 مشاهده میکنید. هر عددی غیر از 200 در اینجا به error تفسیر میشود. همچنین اگر این StatusCode سمت سرور تنظیم نشود، گرید فرض را بر موفقیت آمیز بودن عملیات گذاشته و successfunc را فراخوانی میکند.
مدیریت StatusCodeهای غیر از 200 در حالت کار با فرمهای jqGrid
اگر هر دو حالت Inline Add و فرمهای پویا را فعال کردهاید، بازگشت StatusCode = 500 سبب میشود تا دیگر نتوان خطاهای سفارشی سمت سرور را در بالای فرمها به کاربر نمایش داد و در این حالت تنها یک internal server error را مشاهده خواهند کرد. برای رفع این مشکل فقط کافی است روال رویدادگران errorTextFormat را مدیریت کرد:
errorTextFormat تنها در حالتیکه StatusCode بازگشتی از طرف سرور مساوی 200 نیست، فراخوانی میشود. در اینجا میتوان response دریافتی را آنالیز و سپس پیام خطای سفارشی آنرا جهت نمایش در فرمهای پویای گرید، بازگشت داد.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید:
jqGrid09.zip
فعال سازی و پردازش صفحات پویای افزودن، ویرایش و حذف رکوردهای jqGrid در ASP.NET MVC
اعتبارسنجی سفارشی سمت کاربر و سمت سرور در jqGrid
پیشتر با نحوهی فعال سازی صفحات پویای افزودن، ویرایش و حذف رکوردهای jqGrid آشنا شدیم. اما ... شاید علاقمند نباشید که اصلا از این صفحات استفاده کنید. شاید به نظر شما با کلیک بر روی دکمهی + افزودن یک رکورد جدید، بهتر باشد داخل خود گرید، یک سطر خالی جدید باز شده تا بتوان آنرا پر کرد. شاید این نحو کار کردن با گرید، از دید عدهای طبیعیتر باشد نسبت به حالت نمایش صفحات popup افزودن و یا ویرایش رکوردها. در ادامه این مورد را بررسی خواهیم کرد.
فعال سازی افزودن، ویرایش و حذف Inline
فعال سازی ویرایش و حذف Inline را پیشتر نیز بررسی کرده بودیم. تنها کافی است یک ستون جدید را با 'formatter: 'actions تعریف کنیم. به صورت خودکار، دکمهی ویرایش، حذف، ذخیره سازی و لغو Inline ظاهر میشوند و همچنین بدون نیاز به کدنویسی بیشتری کار میکنند.
اما در کدهای ذیل اندکی این ستون را سفارشی سازی کردهایم. در قسمت formatter آن، دکمههای edit و delete یک سطر جدید توکار اضافه شده را حذف کردهایم. زیرا در این حالت خاص، وجود این دکمهها ضروری نیستند. بهتر است در این حالت دکمههای save و cancel ظاهر شوند:
$('#list').jqGrid({ caption: "آزمایش نهم", // .... colModel: [ { name: 'myac', width: 80, fixed: true, sortable: false, resize: false, //formatter: 'actions', formatter: function (cellvalue, options, rowObject) { if (cellvalue === undefined && options.rowId === "_empty") { // در حالت نمایش ردیف توکار جدید دکمههای ویرایش و حذف معنی ندارند options.colModel.formatoptions.editbutton = false; options.colModel.formatoptions.delbutton = false; } return $.fn.fmatter.actions(cellvalue, options, rowObject); }, formatoptions: { keys: true, afterSave: function (rowid, response) { }, delbutton: true, delOptions: { url: "@Url.Action("DeleteUser", "Home")" } } } ], //... }).navGrid( '#pager', //... ) .jqGrid('gridResize', { minWidth: 400, minHeight: 150 }) .jqGrid('inlineNav', '#pager', { edit: true, add: true, save: true, cancel: true, edittext: "ویرایش", addtext: "جدید", savetext: "ذخیره", canceltext: "لغو", addParams: { // اگر میخواهید ردیفهای جدید در ابتدا ظاهر شوند، این سطر را حذف کنید position: "last", //ردیفهای جدید در آخر ظاهر میشوند rowID: '_empty', useDefValues: true, addRowParams: getInlineNavParams(true) }, editParams: getInlineNavParams(false) });
در اینجا 4 دکمهی ویرایش، جدید، ذخیره و لغو، در نوار pager پایین گرید ظاهر خواهند شد (سمت چپ؛ سمت راست همان دکمههای نمایش فرمهای پویا هستند).
سپس باید دو قسمت مهم addParams و editParams آنرا مقدار دهی کرد.
در قسمت addParams، مشخص میکنیم که ID ردیف اضافه شده، مساوی کلمهی _empty باشد. اگر به کدهای formatter ستون action دقت کنید، از این ID برای تشخیص افزوده شدن یک ردیف جدید استفاده شدهاست.
position در اینجا به معنای محل افزوده شدن یک ردیف خالی است. مقدار پیش فرض آن first است؛ یعنی همیشه در اولین ردیف گرید، این ردیف جدید اضافه میشود. در اینجا به last تنظیم شدهاست تا در پایین گرید و پس از رکوردهای موجود، نمایش داده شود.
useDefValues سبب استفاده از مقادیر پیش فرض تعریف شده در ستونهای گرید در حین افزوده شدن یک ردیف جدید میگردد.
addRowParams و editParams هر دو ساختار تقریبا یکسانی دارند که به نحو ذیل تعریف میشوند:
function getInlineNavParams(isAdd) { return { // استفاده از آدرسهای مختلف برای حالات ویرایش و ثبت اطلاعات جدید url: isAdd ? '@Url.Action("AddUser", "Home")' : '@Url.Action("EditUser","Home")', key: true, restoreAfterError: false, // این مورد سبب میشود تا اعتبارسنجی سمت سرور قابل اعمال شود oneditfunc: function (rowId) { // نمایش دکمههای ذخیره و لغو داخل همان سطر $("#jSaveButton_" + rowId).show(); $("#jCancelButton_" + rowId).show(); }, successfunc: function () { var $self = $(this); setTimeout(function () { $self.trigger("reloadGrid"); // دریافت کلید اصلی ردیف از سرور }, 50); }, errorfunc: function (rowid, response, stat) { if (stat != 'error') // this.Response.StatusCode == 200 return; var result = $.parseJSON(response.responseText); if (result.success === false) { //نمایش خطای اعتبار سنجی سمت سرور پس از ویرایش یا افزودن $.jgrid.info_dialog($.jgrid.errors.errcap, '<div class="ui-state-error">' + result.message + '</div>', $.jgrid.edit.bClose, { buttonalign: 'center' }); } } }; }
تنظیم restoreAfterError به false بسیار مهم است. اگر در سمت سرور خطای اعتبارسنجی گزارش شود و restoreAfterError مساوی true باشد (مقدار پیش فرض)، کاربر مجبور خواهد شد اطلاعات را دوباره وارد کند.
در روال رویدادگران oneditfunc دکمهی save و cancel ردیف را که مخفی هستند، ظاهر میکنیم (مکمل formatter ستون action است).
در قسمت successfunc، پس از پایان موفقیت آمیز کار، متد reloadGrid را فراخوانی میکنیم. اینکار سبب میشود تا Id واقعی رکورد، از سمت سرور دریافت شود. از این Id برای ویرایش و همچنین حذف، استفاده خواهد شد. علت استفاده از setTimeout در اینجا این است که اندکی به DOM فرصت داده شود تا کارش به پایان برسد.
در قسمت errorfunc خطاهای اعتبارسنجی سفارشی سمت سرور را میتوان دریافت و سپس توسط متد توکار info_dialog به کاربر نمایش داد.
یک نکتهی مهم در مورد ارسال خطاهای اعتبارسنجی از سمت سرور در حالت Inline Add
if (_usersInMemoryDataSource.Any( user => user.Name.Equals(postData.Name, StringComparison.InvariantCultureIgnoreCase))) { this.Response.StatusCode = 500; //این مورد برای افزودن داخل ردیفهای گرید لازم است return Json(new { success = false, message = "نام کاربر تکراری است" }, JsonRequestBehavior.AllowGet); }
مدیریت StatusCodeهای غیر از 200 در حالت کار با فرمهای jqGrid
اگر هر دو حالت Inline Add و فرمهای پویا را فعال کردهاید، بازگشت StatusCode = 500 سبب میشود تا دیگر نتوان خطاهای سفارشی سمت سرور را در بالای فرمها به کاربر نمایش داد و در این حالت تنها یک internal server error را مشاهده خواهند کرد. برای رفع این مشکل فقط کافی است روال رویدادگران errorTextFormat را مدیریت کرد:
$('#list').jqGrid({ caption: "آزمایش نهم", //......... }).navGrid( '#pager', //enabling buttons { add: true, del: true, edit: true, search: false }, //edit option { //......... errorTextFormat: serverErrorTextFormat }, //add options { //......... errorTextFormat: serverErrorTextFormat }, //delete options { //......... }) .jqGrid('gridResize', { minWidth: 400, minHeight: 150 }) .jqGrid('inlineNav', '#pager', { //......... }); function serverErrorTextFormat (response) { // در حالتیکه وضعیت خروجی از سرور 200 نیست فراخوانی میشود var result = $.parseJSON(response.responseText); if (result.success === false) { return result.message; } return "لطفا ورودیهای وارد شده را بررسی کنید"; }
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید:
jqGrid09.zip
- اگر فکر میکنید که 2 بار چک کردن به ازای هر درخواست زیاد هست، احتمالا با ASP.NET Identity کار نکردید! در ASP.NET Identity اگر بررسی اعتبار کاربر را به ازای هر درخواست رسیده فعال کنید (بجای مقدار پیشفرض چند دقیقهای آن، این مقدار را صفر کنید تا به ازای هر درخواست انجام شود)، همین یک مورد 5 کوئری را شامل میشود. برای نمونه در ASP.NET Core 2.X این بررسیها شامل 5 کوئری به جداول AspNetUser, AspNetUserClaims, AspNetUserRoles, AspNetRoles, AspNetRoleClaims هستند.
- 2 بار بررسی بانک اطلاعاتی برای بانکهای اطلاعاتی امروزی هیچ سرباری ندارد و ضمن اینکه خودشان مباحث کش کردن اطلاعات ویژهای را هم برای کوئریهای پر استفاده دارند؛ مانند buffer cache در SQL Server که تا حد مصرف حافظهی کل سرور هم میتواند پیش رود.
- استفاده از متغیرهای استاتیک و حافظهی سرور برای کش کردن، مقیاس پذیر نیست. در این موارد روش توصیه شده، استفاده از بانک اطلاعاتی Key/Value فوق سریع Redis هست. فقط مشکل تمام کشها، هماهنگ سازی اطلاعات آنها با بانک اطلاعاتی اصلی است که باید مدنظر باشند.
- برای ایجاد بانک اطلاعاتی از روی کلاسها و مدلهای برنامه، نیاز به ()context.Database.Migrate خواهید داشت.
- ضمنا مایکروسافت روش «db first» را خیلی وقت هست که کنار گذاشته؛ از زمان ارائهی «Entity framework code-first». آگاهی از تاریخچهی تغییرات علم و فناوریها، سردرگمی شما را برای انتخاب آنها کمتر میکند.
با تکامل SQL server و بهبودهای حاصل شده، یک سری از ویژگیهای موجود صرفا جهت حفظ سازگاری با نگارشهای قبلی ارائه میشوند. لیست کامل آنها را در آدرس زیر میتوان مشاهده نمود:
Deprecated Database Engine Features in SQL Server 2008
لیست بلند بالایی است. اما در یک محیط کاری، نوعهای زیر از سایر موارد ذکر شده بیشتر مورد استفاده قرار میگیرند:
منسوخ شدهها: text ، ntext و image . جایگزینها : varchar ، nvarchar و varbinary از نوع max دار
عموما علت استفاده از نوعهای text یا ntext (نمونه یونیکد text) ، مشخص نبودن تعداد کاراکتری است که کاربر قرار است وارد کند. برای مثال یک سایت خبری ایجاد کردهاید و طول محتوای خبر ثبت شده در بانک اطلاعاتی از یک خبر به خبر دیگر کاملا متفاوت است. در اینجا برای حل این مشکل از نوعهای text یا ntext استفاده میشد (این مورد تا اسکیوال سرور 2000 توصیه میشود).
varchar max تا 2,147,483,648 کاراکتر را میتواند ذخیره کند، یعنی تا 2 GB و nvarchar max تا نصف این مقدار را. در اس کیوال سرور 2000 محدودیت 8000 کاراکتر برای نوع vrachar وجود داشت (و نوع nvrachar تا 4000 کاراکتر).
مزایای استفاده از نوعهای max دار (از اس کیوال سرور 2005 به بعد) :
- بهبود کارآیی کوئریهای جستجو نسبت به نوعهای Text
- اگر مطلب تشخیص کمبود ایندکسها را دنبال کرده باشید، در آنجا ذکر شد که در قسمت included columns نمیتوان از text و ntext استفاده کرد اما نوعهای max دار متنی مجازند.
- امکان استفاده از فیلدهای max دار برای مرتب سازی کوئری مجاز است. (به شخصه با این مورد زیاد برخورد داشتم. برای مثال امکان سورت کردن یک گرید را در ASP.Net فراهم کردهاید و کاربر با کلیک بر روی سر ستون فیلدی از نوع ntext با یک خطا متوقف خواهد شد)
- امکان استفاده از نوعهای Text بهعنوان متغیر در رویههای ذخیره شده یا توابع T-SQL مهیا نیست اما این محدودیت در نوعهای max دار برطرف شده است.
- نوعهای text را در توابع REPLACE ، CHARINDEX و SUBSTRINGنمیتوان بکار برد (برخلاف نوعهای متنی max دار).
Timeout expired. The timeout period elapsed prior to completion of the operation or the server is not responding.
بعد از این تغییر به سادگی میتوان عملیات ارتقاء را بدون نگرانی از بروز خطای فوق انجام داد.