<script type="text/javascript"> $(document).ready(function () { $("form").submit(function () { $(this).validate(); if (!$(this).valid()) { console.log("validation error"); //note: here return false will stop the submit return false; } }); }); </script>
دریافت آخرین نگارش Froala WYSIWYG Editor
برای دریافت فایلهای آخرین نگارش این ادیتور وب میتوانید به سایت آن، قسمت دریافت فایلها مراجعه نمائید.
http://editor.froala.com/download
و یا به این آدرس مراجعه کنید:
https://github.com/froala/wysiwyg-editor/releases
ساختار پروژه و نحوهی کپی فایلهای آن
در هر دو مثالی که فایلهای آنرا از انتهای بحث میتوانید دریافت کنید، این ساختار رعایت شده است:
فایلهای CSS و فونتهای آن، در پوشهی Content قرار گرفتهاند.
فایلهای اسکریپت و زبان آن (که دارای زبان فارسی هم هست) در پوشهی Scripts کپی شدهاند.
یک نکته
فایل font-awesome.css را نیاز است کمی اصلاح کنید. مسیر پوشهی فونتهای آن اکنون با fonts شروع میشود.
تنظیمات اولیه
تفاوتی نمیکند که از وب فرمها استفاده میکنید یا MVC، نحوهی تعریف و افزودن پیش نیازهای این ادیتور به نحو ذیل است:
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title></title> <link href="Content/font-awesome.css" rel="stylesheet" /> <link href="Content/froala_editor.css" rel="stylesheet" /> <script src="Scripts/jquery-1.10.2.min.js"></script> <script src="Scripts/froala_editor.min.js"></script> <script src="Scripts/langs/fa.js"></script> </head> <body> <form id="form1" runat="server"> </form> </body> </html>
استفاده از Froala WYSIWYG Editor در ASP.NET MVC
در ادامه نحوهی فعال سازی ادیتور وب Froala را در یک View برنامههای ASP.NET MVC ملاحظه میکنید:
@{ ViewBag.Title = "Index"; } <style type="text/css"> /*تنظیم فونت پیش فرض ادیتور*/ .froala-element { } </style> @using (Html.BeginForm(actionName: "Index", controllerName: "Home")) { @Html.TextArea(name: "Editor1") <input type="submit" value="ارسال" /> } @section Scripts { <script type="text/javascript"> $(function () { $('#Editor1').editable({ buttons: ["bold", "italic", "underline", "strikeThrough", "fontFamily", "fontSize", "color", "formatBlock", "align", "insertOrderedList", "insertUnorderedList", "outdent", "indent", "selectAll", "createLink", "insertImage", "insertVideo", "undo", "redo", "html", "save", "inserthorizontalrule"], inlineMode: false, inverseSkin: true, preloaderSrc: '@Url.Content("~/Content/img/preloader.gif")', allowedImageTypes: ["jpeg", "jpg", "png"], height: 300, language: "fa", direction: "rtl", fontList: ["Tahoma, Geneva", "Arial, Helvetica", "Impact, Charcoal"], autosave: true, autosaveInterval: 2500, saveURL: '@Url.Action("FroalaAutoSave", "Home")', saveParams: { postId: "123" }, spellcheck: true, plainPaste: true, imageButtons: ["removeImage", "replaceImage", "linkImage"], borderColor: '#00008b', imageUploadURL: '@Url.Action("FroalaUploadImage", "Home")', imageParams: { postId: "123" }, enableScript: false }); }); </script> }
سپس این ادیتور را بر روی المان TextArea قرار گرفته در صفحه، فعال میکنیم.
در قسمت مقادیر buttons، تمام حالات ممکن پیش بینی شدهاند. هر کدام را که نیاز ندارید، حذف کنید.
نحوهی تعریف زبان و راست به چپ بودن این ادیتور را با مقدار دهی پارامترهای language و direction ملاحظه میکنید.
پارامترهای autosave، saveURL و saveParams کار تنظیم ارسال خودکار محتوای ادیتور را جهت ذخیرهی آن در سرور به عهده دارند. بر اساس مقدار autosaveInterval میتوان مشخص کرد که هر چند میلی ثانیه یکبار اینکار باید انجام شود.
/// <summary> /// ذخیره سازی خودکار /// </summary> [HttpPost] [ValidateInput(false)] public ActionResult FroalaAutoSave(string body, int? postId) // نام پارامتر بادی را تغییر ندهید { //todo: save body ... return new EmptyResult(); }
چون قرار است تگهای HTML به سرور ارسال شوند، ویژگی ValidateInput به false تنظیم شدهاست.
saveParams آن، برای مقدار دهی پارامترهای اضافی است که نیاز میباشند تا به سرور ارسال شوند. مثلا شماره مطلب جاری نیز به سرور ارسال گردد.
در اینجا نام پارامتری که ارسال میگردد، دقیقا مساوی body است. بنابراین آنرا تغییر ندهید.
پارامترهای imageUploadURL و imageParams برای فعال سازی ذخیره تصاویر آن در سرور کاربرد دارند.
اکشن متد مدیریت کنندهی آن به نحو ذیل میتواند تعریف شود:
// todo: مسایل امنیتی آپلود را فراموش نکنید /// <summary> /// ذخیره سازی تصاویر ارسالی /// </summary> [HttpPost] public ActionResult FroalaUploadImage(HttpPostedFileBase file, int? postId) // نام پارامتر فایل را تغییر ندهید { var fileName = Path.GetFileName(file.FileName); var rootPath = Server.MapPath("~/images/"); file.SaveAs(Path.Combine(rootPath, fileName)); return Json(new { link = "images/" + fileName }, JsonRequestBehavior.AllowGet); }
خروجی آن برای مشخص سازی محل ذخیره سازی تصویر در سرور باید یک خروجی JSON دارای خاصیت و پارامتر link به نحو فوق باشد (این مسیر، یک مسیر نسبی است؛ نسبت به ریشه سایت).
imageParams آن برای مقدار دهی پارامترهای اضافی است که نیاز میباشند تا به سرور ارسال شوند. مثلا شماره مطلب جاری نیز به سرور ارسال گردد.
استفاده از Froala WYSIWYG Editor در ASP.NET Web forms
تمام نکاتی که در قسمت تنظیمات ASP.NET MVC در مورد ویژگیهای سمت کلاینت این ادیتور ذکر شد، در مورد وب فرمها نیز صادق است. فقط قسمت مدیریت سمت سرور آن اندکی تفاوت دارد.
<%@ Page Title="" Language="C#" MasterPageFile="~/Site.Master" ValidateRequest="false" EnableEventValidation="false" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="FroalaWebFormsTest.Default" %> <%--اعتبارسنجی ورودی غیرفعال شده چون باید تگ ارسال شود--%> <%--همچنین در وب کانفیگ هم تنظیم دیگری نیاز دارد--%> <asp:Content ID="Content1" ContentPlaceHolderID="head" runat="server"> </asp:Content> <asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" runat="server"> <%--حالت کلاینت آی دی بهتر است تنظیم شود در اینجا--%> <asp:TextBox ID="txtEditor" ClientIDMode="Static" runat="server" Height="199px" TextMode="MultiLine" Width="447px"></asp:TextBox> <br /> <asp:Button ID="btnSave" runat="server" OnClick="btnSave_Click" Text="ارسال" /> <style type="text/css"> /*تنظیم فونت پیش فرض ادیتور*/ .froala-element { } </style> <script type="text/javascript"> $(function () { $('#txtEditor').editable({ buttons: ["bold", "italic", "underline", "strikeThrough", "fontFamily", "fontSize", "color", "formatBlock", "align", "insertOrderedList", "insertUnorderedList", "outdent", "indent", "selectAll", "createLink", "insertImage", "insertVideo", "undo", "redo", "html", "save", "inserthorizontalrule"], inlineMode: false, inverseSkin: true, preloaderSrc: 'Content/img/preloader.gif', allowedImageTypes: ["jpeg", "jpg", "png"], height: 300, language: "fa", direction: "rtl", fontList: ["Tahoma, Geneva", "Arial, Helvetica", "Impact, Charcoal"], autosave: true, autosaveInterval: 2500, saveURL: 'FroalaHandler.ashx', saveParams: { postId: "123" }, spellcheck: true, plainPaste: true, imageButtons: ["removeImage", "replaceImage", "linkImage"], borderColor: '#00008b', imageUploadURL: 'FroalaHandler.ashx', imageParams: { postId: "123" }, enableScript: false }); }); </script> </asp:Content>
به علاوه ClientIDMode=Static نیز تنظیم شدهاست، تا بتوان از ID تکست باکس قرار گرفته در صفحه، به سادگی در کدهای سمت کاربر جیکوئری استفاده کرد.
اگر دقت کرده باشید، save urlها اینبار به فایل FroalaHandler.ashx اشاره میکنند. محتوای این Genric handler را ذیل مشاهده میکنید:
using System.IO; using System.Web; using System.Web.Script.Serialization; namespace FroalaWebFormsTest { public class FroalaHandler : IHttpHandler { //todo: برای اینکارها بهتر است از وب ای پی آی استفاده شود //todo: یا دو هندلر مجزا یکی برای تصاویر و دیگری برای ذخیره سازی متن public void ProcessRequest(HttpContext context) { var body = context.Request.Form["body"]; var postId = context.Request.Form["postId"]; if (!string.IsNullOrWhiteSpace(body) && !string.IsNullOrWhiteSpace(postId)) { //todo: save changes context.Response.ContentType = "text/plain"; context.Response.Write(""); context.Response.End(); } var files = context.Request.Files; if (files.Keys.Count > 0) { foreach (string fileKey in files) { var file = context.Request.Files[fileKey]; if (file == null || file.ContentLength == 0) continue; //todo: در اینجا مسایل امنیتی آپلود فراموش نشود var fileName = Path.GetFileName(file.FileName); var rootPath = context.Server.MapPath("~/images/"); file.SaveAs(Path.Combine(rootPath, fileName)); var json = new JavaScriptSerializer().Serialize(new { link = "images/" + fileName }); // البته اینجا یک فایل بیشتر ارسال نمیشود context.Response.ContentType = "text/plain"; context.Response.Write(json); context.Response.End(); } } context.Response.ContentType = "text/plain"; context.Response.Write(""); context.Response.End(); } public bool IsReusable { get { return false; } } } }
یک نکتهی امنیتی مهم
<location path="upload"> <system.webServer> <handlers accessPolicy="Read" /> </system.webServer> </location>
کدهای کامل این مطلب را در ادامه میتوانید دریافت کنید
Froala-Sample
- با نصب و اجرای Visual Studio 2013 Express for Web یا Visual Studio 2013 شروع کنید.
- یک پروژه جدید بسازید (از صفحه شروع یا منوی فایل)
- گزینه #Visual C و سپس ASP.NET Web Application را انتخاب کنید. نام پروژه را به "WebFormsIdentity" تغییر داده و OK کنید.
- در دیالوگ جدید ASP.NET گزینه Empty را انتخاب کنید.
دقت کنید که دکمه Change Authentication غیرفعال است و هیچ پشتیبانی ای برای احراز هویت در این قالب پروژه وجود ندارد.
افزودن پکیجهای ASP.NET Identity به پروژه
دقت کنید که نصب کردن این پکیج وابستگیها را نیز بصورت خودکار نصب میکند: Entity Framework و ASP.NET Idenity Core.
افزودن فرمهای وب لازم برای ثبت نام کاربران
در دیالوگ باز شده نام فرم را به Register تغییر داده و تایید کنید.
فایل ایجاد شده جدید را باز کرده و کد Markup آن را با قطعه کد زیر جایگزین کنید.
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Register.aspx.cs" Inherits="WebFormsIdentity.Register" %> <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title></title> </head> <body style=" <form id="form1" runat="server"> <div> <h4 style="Register a new user</h4> <hr /> <p> <asp:Literal runat="server" ID="StatusMessage" /> </p> <div style="margin-bottom:10px"> <asp:Label runat="server" AssociatedControlID="UserName">User name</asp:Label> <div> <asp:TextBox runat="server" ID="UserName" /> </div> </div> <div style="margin-bottom:10px"> <asp:Label runat="server" AssociatedControlID="Password">Password</asp:Label> <div> <asp:TextBox runat="server" ID="Password" TextMode="Password" /> </div> </div> <div style="margin-bottom:10px"> <asp:Label runat="server" AssociatedControlID="ConfirmPassword">Confirm password</asp:Label> <div> <asp:TextBox runat="server" ID="ConfirmPassword" TextMode="Password" /> </div> </div> <div> <div> <asp:Button runat="server" OnClick="CreateUser_Click" Text="Register" /> </div> </div> </div> </form> </body> </html>
این تنها یک نسخه ساده شده Register.aspx است که از چند فیلد فرم و دکمه ای برای ارسال آنها به سرور استفاده میکند.
فایل کد این فرم را باز کرده و محتویات آن را با قطعه کد زیر جایگزین کنید.
using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.EntityFramework; using System; using System.Linq; namespace WebFormsIdentity { public partial class Register : System.Web.UI.Page { protected void CreateUser_Click(object sender, EventArgs e) { // Default UserStore constructor uses the default connection string named: DefaultConnection var userStore = new UserStore<IdentityUser>(); var manager = new UserManager<IdentityUser>(userStore); var user = new IdentityUser() { UserName = UserName.Text }; IdentityResult result = manager.Create(user, Password.Text); if (result.Succeeded) { StatusMessage.Text = string.Format("User {0} was created successfully!", user.UserName); } else { StatusMessage.Text = result.Errors.FirstOrDefault(); } } } }
کد این فرم نیز نسخه ای ساده شده است. فایلی که بصورت خودکار توسط VS برای شما ایجاد میشود متفاوت است.
کلاس IdentityUser پیاده سازی پیش فرض EntityFramework از قرارداد IUser است. قرارداد IUser تعریفات حداقلی یک کاربر در ASP.NET Identity Core را در بر میگیرد.
کلاس UserStore پیاده سازی پیش فرض EF از یک فروشگاه کاربر (user store) است. این کلاس چند قرارداد اساسی ASP.NET Identity Core را پیاده سازی میکند: IUserStore, IUserLoginStore, IUserClaimStore و IUserRoleStore.
کلاس UserManager دسترسی به APIهای مربوط به کاربران را فراهم میکند. این کلاس تمامی تغییرات را بصورت خودکار در UserStore ذخیره میکند.
کلاس IdentityResult نتیجه یک عملیات هویتی را معرفی میکند (identity operations).
پوشه App_Data را به پروژه خود اضافه کنید.
فایل Web.config پروژه را باز کنید و رشته اتصال جدیدی برای دیتابیس اطلاعات کاربران اضافه کنید. این دیتابیس در زمان اجرا (runtime) بصورت خودکار توسط EF ساخته میشود. این رشته اتصال شبیه به رشته اتصالی است که هنگام ایجاد پروژه بصورت خودکار برای شما تنظیم میشود.
<?xml version="1.0" encoding="utf-8"?> <!-- For more information on how to configure your ASP.NET application, please visit http://go.microsoft.com/fwlink/?LinkId=169433 --> <configuration> <configSections> <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 --> <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" /> </configSections> <connectionStrings> <add name="DefaultConnection" connectionString="Data Source=(LocalDb)\v11.0;AttachDbFilename=|DataDirectory|\WebFormsIdentity.mdf;Initial Catalog=WebFormsIdentity;Integrated Security=True" providerName="System.Data.SqlClient" /> </connectionStrings> <system.web> <compilation debug="true" targetFramework="4.5" /> <httpRuntime targetFramework="4.5" /> </system.web> <entityFramework> <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework"> <parameters> <parameter value="v11.0" /> </parameters> </defaultConnectionFactory> <providers> <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" /> </providers> </entityFramework> </configuration>
همانطور که مشاهده میکنید نام این رشته اتصال DefaultConnection است.
روی فایل Register.aspx کلیک راست کنید و گزینه Set As Start Page را انتخاب کنید. اپلیکیشن خود را با کلیدهای ترکیبی Ctrl + F5 اجرا کنید که تمام پروژه را کامپایل نیز خواهد کرد. یک نام کاربری و کلمه عبور وارد کنید و روی Register کلیک کنید.
ASP.NET Identity از اعتبارسنجی نیز پشتیبانی میکند، مثلا در این مرحله میتوانید از اعتبارسنج هایی که توسط ASP.NET Identity Core عرضه میشوند برای کنترل رفتار فیلدهای نام کاربری و کلمه عبور استفاده کنید. اعتبارسنج پیش فرض کاربران (User) که UserValidator نام دارد خاصیتی با نام AllowOnlyAlphanumericUserNames دارد که مقدار پیش فرضش هم true است. اعتبارسنج پیش فرض کلمه عبور (MinimumLengthValidator) اطمینان حاصل میکند که کلمه عبور حداقل 6 کاراکتر باشد. این اعتبارسنجها بصورت propertyها در کلاس UserManager تعریف شده اند و میتوانید آنها را overwrite کنید و اعتبارسنجی سفارشی خود را پیاده کنید. از آنجا که الگوی دیتابیس سیستم عضویت توسط Entity Framework مدیریت میشود، روی الگوی دیتابیس کنترل کامل دارید، پس از Data Annotations نیز میتوانید استفاده کنید.
تایید دیتابیس LocalDbIdentity که توسط EF ساخته میشود
گره (DefaultConnection (WebFormsIdentity و سپس Tables را باز کنید. روی جدول AspNetUsers کلیک راست کرده و Show Table Data را انتخاب کنید.
پیکربندی اپلیکیشن برای استفاده از احراز هویت OWIN
نصب پکیجهای احراز هویت روی پروژه
به دنبال پکیجی با نام Microsoft.Owin.Host.SystemWeb بگردید و آن را نیز نصب کنید.
پکیج Microsoft.Aspnet.Identity.Owin حاوی یک سری کلاس Owin Extension است و امکان مدیریت و پیکربندی OWIN Authentication در پکیجهای ASP.NET Identity Core را فراهم میکند.
پکیج Microsoft.Owin.Host.SystemWeb حاوی یک سرور OWIN است که اجرای اپلیکیشنهای مبتنی بر OWIN را روی IIS و با استفاده از ASP.NET Request Pipeline ممکن میسازد. برای اطلاعات بیشتر به OWIN Middleware in the IIS integrated pipeline مراجعه کنید.
افزودن کلاسهای پیکربندی Startup و Authentication
فایل Startup.cs را باز کنید و قطعه کد زیر را با محتویات آن جایگزین کنید تا احراز هویت OWIN Cookie Authentication پیکربندی شود.
using Microsoft.AspNet.Identity; using Microsoft.Owin; using Microsoft.Owin.Security.Cookies; using Owin; [assembly: OwinStartup(typeof(WebFormsIdentity.Startup))] namespace WebFormsIdentity { public class Startup { public void Configuration(IAppBuilder app) { // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=316888 app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, LoginPath = new PathString("/Login") }); } } }
این کلاس حاوی خاصیت OwinAttribute است که کلاس راه انداز OWIN را نشانه گذاری میکند. هر اپلیکیشن OWIN یک کلاس راه انداز (startup) دارد که توسط آن میتوانید کامپوننتهای application pipeline را مشخص کنید. برای اطلاعات بیشتر درباره این مدل، به OWIN Startup Class Detection مراجعه فرمایید.
افزودن فرمهای وب برای ثبت نام و ورود کاربران
using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.EntityFramework; using Microsoft.Owin.Security; using System; using System.Linq; using System.Web; namespace WebFormsIdentity { public partial class Register : System.Web.UI.Page { protected void CreateUser_Click(object sender, EventArgs e) { // Default UserStore constructor uses the default connection string named: DefaultConnection var userStore = new UserStore<IdentityUser>(); var manager = new UserManager<IdentityUser>(userStore); var user = new IdentityUser() { UserName = UserName.Text }; IdentityResult result = manager.Create(user, Password.Text); if (result.Succeeded) { var authenticationManager = HttpContext.Current.GetOwinContext().Authentication; var userIdentity = manager.CreateIdentity(user, DefaultAuthenticationTypes.ApplicationCookie); authenticationManager.SignIn(new AuthenticationProperties() { }, userIdentity); Response.Redirect("~/Login.aspx"); } else { StatusMessage.Text = result.Errors.FirstOrDefault(); } } } }
فایل Login.aspx را باز کنید و کد Markup آن را مانند قطعه کد زیر تغییر دهید.
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Login.aspx.cs" Inherits="WebFormsIdentity.Login" %> <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title></title> </head> <body style="font-family: Arial, Helvetica, sans-serif; font-size: small"> <form id="form1" runat="server"> <div> <h4 style="font-size: medium">Log In</h4> <hr /> <asp:PlaceHolder runat="server" ID="LoginStatus" Visible="false"> <p> <asp:Literal runat="server" ID="StatusText" /> </p> </asp:PlaceHolder> <asp:PlaceHolder runat="server" ID="LoginForm" Visible="false"> <div style="margin-bottom: 10px"> <asp:Label runat="server" AssociatedControlID="UserName">User name</asp:Label> <div> <asp:TextBox runat="server" ID="UserName" /> </div> </div> <div style="margin-bottom: 10px"> <asp:Label runat="server" AssociatedControlID="Password">Password</asp:Label> <div> <asp:TextBox runat="server" ID="Password" TextMode="Password" /> </div> </div> <div style="margin-bottom: 10px"> <div> <asp:Button runat="server" OnClick="SignIn" Text="Log in" /> </div> </div> </asp:PlaceHolder> <asp:PlaceHolder runat="server" ID="LogoutButton" Visible="false"> <div> <div> <asp:Button runat="server" OnClick="SignOut" Text="Log out" /> </div> </div> </asp:PlaceHolder> </div> </form> </body> </html>
محتوای فایل Login.aspx.cs را نیز مانند لیست زیر تغییر دهید.
using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.EntityFramework; using Microsoft.Owin.Security; using System; using System.Web; using System.Web.UI.WebControls; namespace WebFormsIdentity { public partial class Login : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) { if (User.Identity.IsAuthenticated) { StatusText.Text = string.Format("Hello {0}!", User.Identity.GetUserName()); LoginStatus.Visible = true; LogoutButton.Visible = true; } else { LoginForm.Visible = true; } } } protected void SignIn(object sender, EventArgs e) { var userStore = new UserStore<IdentityUser>(); var userManager = new UserManager<IdentityUser>(userStore); var user = userManager.Find(UserName.Text, Password.Text); if (user != null) { var authenticationManager = HttpContext.Current.GetOwinContext().Authentication; var userIdentity = userManager.CreateIdentity(user, DefaultAuthenticationTypes.ApplicationCookie); authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = false }, userIdentity); Response.Redirect("~/Login.aspx"); } else { StatusText.Text = "Invalid username or password."; LoginStatus.Visible = true; } } protected void SignOut(object sender, EventArgs e) { var authenticationManager = HttpContext.Current.GetOwinContext().Authentication; authenticationManager.SignOut(); Response.Redirect("~/Login.aspx"); } } }
- متد Page_Load حالا وضعیت کاربر جاری را بررسی میکند و بر اساس وضعیت Context.User.Identity.IsAuthenticated تصمیم گیری میکند.
- متد SignIn
- پروژه را با Ctrl + F5 اجرا کنید و کاربر جدیدی بسازید. پس از وارد کردن نام کاربری و کلمه عبور و کلیک کردن دکمه Register باید بصورت خودکار به سایت وارد شوید و نام خود را مشاهده کنید.
- همانطور که مشاهده میکنید در این مرحله حساب کاربری جدید ایجاد شده و به سایت وارد شده اید. روی Log out کلیک کنید تا از سایت خارج شوید. پس از آن باید به صفحه ورود هدایت شوید.
- حالا یک نام کاربری یا کلمه عبور نامعتبر وارد کنید و روی Log in کلیک کنید.
Unobtrusive Ajax چیست؟
در حالت معمولی، با استفاده از متد ajax جیکوئری، کار ارسال غیرهمزمان اطلاعات، به سمت سرور صورت میگیرد. چون در این روش کدهای جیکوئری داخل صفحات برنامههای ما قرار میگیرند، به این روش، «روش چسبنده» میگویند. اما با استفاده از افزونهی «jquery.unobtrusive-ajax.min.js» مایکروسافت، میتوان این کدهای چسبنده را تبدیل به کدهای غیرچسنبده یا Unobtrusive کرد. در این حالت، پارامترهای متد ajax، به صورت ویژگیها (attributes) به شکل data-ajax به المانهای مختلف صفحه اضافه میشوند و به این ترتیب، افزونهی یاد شده به صورت خودکار با یافتن مقادیر ویژگیهای data-ajax، این المانها را تبدیل به المانهای ایجکسی میکند. در این حالت به کدهایی تمیزتر و عاری از متدهای چسبندهی ajax قرار گرفتهی در داخل صفحات وب خواهیم رسید.
روش طراحی Unobtrusive را در کتابخانههای معروفی مانند بوت استرپ هم میتوان مشاهده کرد.
پیشنیازهای فعال سازی Unobtrusive Ajax در ASP.NET Core 1.0
توزیع افزونهی «jquery.unobtrusive-ajax.min.js» مایکروسافت، از طریق bower صورت میگیرد که پیشتر در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 14 - فعال سازی اعتبارسنجی ورودیهای کاربران» با آن آشنا شدیم. در اینجا نیز برای دریافت آن، تنها کافی است فایل bower.json را به نحو ذیل تکمیل کرد:
{ "name": "asp.net", "private": true, "dependencies": { "bootstrap": "3.3.6", "jquery": "2.2.0", "jquery-validation": "1.14.0", "jquery-validation-unobtrusive": "3.2.6", "jquery-ajax-unobtrusive": "3.2.4" } }
[ { "outputFileName": "wwwroot/css/site.min.css", "inputFiles": [ "bower_components/bootstrap/dist/css/bootstrap.min.css", "content/site.css" ] }, { "outputFileName": "wwwroot/js/site.min.js", "inputFiles": [ "bower_components/jquery/dist/jquery.min.js", "bower_components/jquery-validation/dist/jquery.validate.min.js", "bower_components/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js", "bower_components/jquery-ajax-unobtrusive/jquery.unobtrusive-ajax.min.js", "bower_components/bootstrap/dist/js/bootstrap.min.js" ], "minify": { "enabled": true, "renameLocals": true }, "sourceMap": false } ]
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>@ViewBag.Title - My ASP.NET Application</title> <link href="~/css/site.min.css" rel="stylesheet" /> </head> <body> <div> <div> @RenderBody() </div> </div> <script src="~/js/site.min.js" type="text/javascript" asp-append-version="true"></script> @RenderSection("Scripts", required: false) </body> </html>
استفاده از معادلهای واقعی Unobtrusive Ajax در ASP.NET Core 1.0
واقعیت این است که HTML Helper ایجکسی حذف شدهی از ASP.NET Core 1.0، کاری بجز افزودن ویژگیهای data-ajax را که توسط افزونهی jquery.unobtrusive-ajax.min.js پردازش میشوند، انجام نمیدهد و این افزونه مستقل است از مباحث سمت سرور و به نگارش خاصی از ASP.NET گره نخورده است. بنابراین در اینجا تنها کاری را که باید انجام داد، استفاده از همان ویژگیهای اصلی است که این افزونه قادر به شناسایی آنها است.
خلاصهی آنها را جهت انتقال کدهای قدیمی و یا تهیهی کدهای جدید، در جدول ذیل میتوانید مشاهده کنید:
HTML attribute | AjaxOptions |
data-ajax-confirm | Confirm |
data-ajax-method | HttpMethod |
data-ajax-mode | InsertionMode |
data-ajax-loading-duration | LoadingElementDuration |
data-ajax-loading | LoadingElementId |
data-ajax-begin | OnBegin |
data-ajax-complete | OnComplete |
data-ajax-failure | OnFailure |
data-ajax-success | OnSuccess |
data-ajax-update | UpdateTargetId |
data-ajax-url | Url |
در ASP.NET Core 1.0، به علت حذف متدهای کمکی Ajax دیگر خبری از AjaxOptions نیست. اما اگر علاقمند به انتقال کدهای قدیمی به ASP.NET Core 1.0 هستید، معادلهای اصلی این پارامترها را میتوانید در ستون HTML attribute مشاهده کنید.
چند نکته:
- اگر قصد استفادهی از این ویژگیها را دارید، باید ویژگی "data-ajax="true را نیز حتما قید کنید تا سیستم Unobtrusive Ajax فعال شود.
- ویژگی data-ajax-mode تنها با ذکر data-ajax-update (و یا همان UpdateTargetId پیشین) معنا پیدا میکند.
- ویژگی data-ajax-loading-duration نیاز به ذکر data-ajax-loading (و یا همان LoadingElementId پیشین) را دارد.
- ویژگی data-ajax-mode مقادیر before، after و replace-with را میپذیرد. اگر قید نشود، کل المان با data دریافتی جایگزین میشود.
- سه callback قابل تعریف data-ajax-complete، data-ajax-failure و data-ajax-success، یک چنین پارامترهایی را از سمت سرور در اختیار کلاینت قرار میدهند:
parameters | Callback |
xhr, status | data-ajax-complete |
data, status, xhr | data-ajax-success |
xhr, status, error | data-ajax-failure |
برای مثال میتوان ویژگی data-ajax-success را به نحو ذیل در سمت کلاینت مقدار دهی کرد:
data-ajax-success = "myJsMethod"
function myJsMethod(data, status, xhr) { }
return Json(new { param1 = 1, param2 = 2, ... });
مثالهایی از افزودن ویژگیهای data-ajax به المانهای مختلف
در حالت استفاده از Form Tag Helpers که در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 12 - معرفی Tag Helpers» بررسی شدند، یک فرم ایجکسی، چنین تعاریفی را پیدا خواهد کرد:
با این ViewModel فرضی
using System.ComponentModel.DataAnnotations; namespace Core1RtmEmptyTest.ViewModels.Account { public class RegisterViewModel { [Required] [EmailAddress] [Display(Name = "Email")] public string Email { get; set; } } }
@using Core1RtmEmptyTest.ViewModels.Account @model RegisterViewModel @{ } <form method="post" asp-controller="TestAjax" asp-action="Index" asp-route-returnurl="@ViewBag.ReturnUrl" class="form-horizontal" role="form" data-ajax="true" data-ajax-loading="#Progress" data-ajax-success="myJsMethod"> <input asp-for="Email" class="form-control" /> <span asp-validation-for="Email" class="text-danger"></span> <button type="submit">ارسال</button> <div id="Progress" style="display: none"> <img src="images/loading.gif" alt="loading..." /> </div> </form> @section scripts{ <script type="text/javascript"> function myJsMethod(data, status, xhr) { alert(data.param1); } </script> }
این View از کنترلر ذیل استفاده میکند:
using Core1RtmEmptyTest.ViewModels.Account; using Microsoft.AspNetCore.Mvc; namespace Core1RtmEmptyTest.Controllers { public class TestAjaxController : Controller { public IActionResult Index() { return View(); } [HttpPost] public IActionResult Index([FromForm]RegisterViewModel vm) { var ajax = isAjax(); if (ajax) { // it's an ajax post } if (ModelState.IsValid) { //todo: save data return Json(new { param1 = 1, param2 = 2 }); } return View(); } private bool isAjax() { return Request?.Headers != null && Request.Headers["X-Requested-With"] == "XMLHttpRequest"; } } }
و یا Action Link ایجکسی نیز به صورت خلاصه به این نحو قابل تعریف است:
<div id="EmployeeInfo"> <a asp-controller="MyController" asp-action="MyAction" data-ajax="true" data-ajax-loading="#Progress" data-ajax-method="POST" data-ajax-mode="replace" data-ajax-update="#EmployeeInfo"> Get Employee-1 info </a> <div id="Progress" style="display: none"> <img src="images/loading.gif" alt="loading..." /> </div> </div>
نکتهای در مورد اکشن متدهای ایجکسی در ASP.NET Core 1.0
همانطور که در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 18 - کار با ASP.NET Web API»، قسمت «تغییرات Model binding پیش فرض، برای پشتیبانی از ASP.NET MVC و ASP.NET Web API» نیز ذکر شد:
public IActionResult Index([FromBody] MyViewModel vm) { return View(); }
چگونه با استفاده از لوسین مطالب را ایندکس کنیم؟
چگونه از افزونه jQuery Auto-Complete استفاده کنیم؟
نحوه استفاده صحیح از لوسین در ASP.NET
اگر به جستجوی سایت دقت کرده باشید، قابلیت ارائه پیشنهاداتی به کاربر توسط یک Auto-Complete به آن اضافه شدهاست. در مطلب جاری به بررسی این مورد به همراه دو مثال Web forms و MVC پرداخته خواهد شد.
قسمت عمده مطلب جاری با پیشنیازهای یاد شده فوق یکی است. در اینجا فقط به ذکر تفاوتها بسنده خواهد شد.
الف) دریافت لوسین
از طریق NuGet آخرین نگارش را دریافت و به پروژه خود اضافه کنید. همچنین Lucene.NET Contrib را نیز به همین نحو دریافت نمائید.
ب) ایجاد ایندکس
کدهای این قسمت با مطلب برجسته سازی قسمتهای جستجو شده، یکی است:
using System.Collections.Generic; using System.IO; using Lucene.Net.Analysis.Standard; using Lucene.Net.Documents; using Lucene.Net.Index; using Lucene.Net.Store; using LuceneSearch.Core.Model; using LuceneSearch.Core.Utils; namespace LuceneSearch.Core { public static class CreateIndex { static readonly Lucene.Net.Util.Version _version = Lucene.Net.Util.Version.LUCENE_30; public static Document MapPostToDocument(Post post) { var postDocument = new Document(); postDocument.Add(new Field("Id", post.Id.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED)); var titleField = new Field("Title", post.Title, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS); titleField.Boost = 3; postDocument.Add(titleField); postDocument.Add(new Field("Body", post.Body.RemoveHtmlTags(), Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS)); return postDocument; } public static void CreateFullTextIndex(IEnumerable<Post> dataList, string path) { var directory = FSDirectory.Open(new DirectoryInfo(path)); var analyzer = new StandardAnalyzer(_version); using (var writer = new IndexWriter(directory, analyzer, create: true, mfl: IndexWriter.MaxFieldLength.UNLIMITED)) { foreach (var post in dataList) { writer.AddDocument(MapPostToDocument(post)); } writer.Optimize(); writer.Commit(); writer.Close(); directory.Close(); } } } }
ج) تهیه قسمت منبع داده Auto-Complete
namespace LuceneSearch.Core.Model { public class SearchResult { public int Id { set; get; } public string Title { set; get; } } }
using System.Collections.Generic; using System.IO; using Lucene.Net.Index; using Lucene.Net.Search; using Lucene.Net.Store; using LuceneSearch.Core.Model; using LuceneSearch.Core.Utils; namespace LuceneSearch.Core { public static class AutoComplete { private static IndexSearcher _searcher; /// <summary> /// Get terms starting with the given prefix /// </summary> /// <param name="prefix"></param> /// <param name="maxItems"></param> /// <returns></returns> public static IList<SearchResult> GetTermsScored(string indexPath, string prefix, int maxItems = 10) { if (_searcher == null) _searcher = new IndexSearcher(FSDirectory.Open(new DirectoryInfo(indexPath)), true); var resultsList = new List<SearchResult>(); if (string.IsNullOrWhiteSpace(prefix)) return resultsList; prefix = prefix.ApplyCorrectYeKe(); var results = _searcher.Search(new PrefixQuery(new Term("Title", prefix)), null, maxItems); if (results.TotalHits == 0) { results = _searcher.Search(new PrefixQuery(new Term("Body", prefix)), null, maxItems); } foreach (var doc in results.ScoreDocs) { resultsList.Add(new SearchResult { Title = _searcher.Doc(doc.Doc).Get("Title"), Id = int.Parse(_searcher.Doc(doc.Doc).Get("Id")) }); } return resultsList; } } }
برای نمایش Auto-Complete نیاز به منبع داده داریم که نحوه ایجاد آنرا در کدهای فوق ملاحظه میکنید. در اینجا توسط جستجوی سریع لوسین و امکانات PrefixQuery آن، به تعدادی مشخص (maxItems)، رکوردهای یافت شده را بازگشت خواهیم داد. خروجی حاصل لیستی است از SearchResultها شامل عنوان مطلب و Id آن. عنوان را به کاربر نمایش خواهیم داد؛ از Id برای هدایت او به مطلبی مشخص استفاده خواهیم کرد.
د) نمایش Auto-Complete در ASP.NET MVC
using System.Text; using System.Web.Mvc; using LuceneSearch.Core; using System.Web; namespace LuceneSearch.Controllers { public class HomeController : Controller { static string _indexPath = HttpRuntime.AppDomainAppPath + @"App_Data\idx"; public ActionResult Index(int? id) { if (id.HasValue) { //todo: do something } return View(); //Show the page } public virtual ActionResult ScoredTerms(string q) { if (string.IsNullOrWhiteSpace(q)) return Content(string.Empty); var result = new StringBuilder(); var items = AutoComplete.GetTermsScored(_indexPath, q); foreach (var item in items) { var postUrl = this.Url.Action(actionName: "Index", controllerName: "Home", routeValues: new { id = item.Id }, protocol: "http"); result.AppendLine(item.Title + "|" + postUrl); } return Content(result.ToString()); } } }
@{ ViewBag.Title = "جستجو"; var scoredTermsUrl = Url.Action(actionName: "ScoredTerms", controllerName: "Home"); var bulletImage = Url.Content("~/Content/Images/bullet_shape.png"); } <h2> جستجو</h2> <div align="center"> @Html.TextBox("term", "", htmlAttributes: new { dir = "ltr" }) <br /> جهت آزمایش lu را وارد نمائید </div> @section scripts { <script type="text/javascript"> EnableSearchAutocomplete('@scoredTermsUrl', '@bulletImage'); </script> }
function EnableSearchAutocomplete(url, img) { var formatItem = function (row) { if (!row) return ""; return "<img src='" + img + "' /> " + row[0]; } $(document).ready(function () { $("#term").autocomplete(url, { dir: 'rtl', minChars: 2, delay: 5, mustMatch: false, max: 20, autoFill: false, matchContains: false, scroll: false, width: 300, formatItem: formatItem }).result(function (evt, row, formatted) { if (!row) return; window.location = row[1]; }); }); }
- ابتدا ارجاعاتی را به jQuery، افزونه Auto-Complete و اسکریپت سفارشی تهیه شده، در فایل layout پروژه تعریف خواهیم کرد.
در اینجا سه قسمت را مشاهده میکنید: کدهای کنترلر، View متناظر و اسکریپتی که Auto-Complete را فعال خواهد ساخت.
- قسمت مهم کدهای کنترلر، دو سطر زیر هستند:
result.AppendLine(item.Title + "|" + postUrl); return Content(result.ToString());
return Content هم سبب بازگشت این اطلاعات به افزونه خواهد شد.
- کدهای View متناظر بسیار ساده هستند. تنها نام TextBox تعریف شده مهم میباشد که در متد جاوا اسکریپتی EnableSearchAutocomplete استفاده شده است. به علاوه، نحوه مقدار دهی آدرس دسترسی به اکشن متد ScoredTerms نیز مهم میباشد.
- در متد EnableSearchAutocomplete نحوه فراخوانی افزونه autocomplete را ملاحظه میکنید.
جهت آن، به راست به چپ تنظیم شده است. با 2 کاراکتر ورودی فعال خواهد شد با وقفهای کوتاه. نیازی نیست تا انتخاب کاربر از لیست ظاهر شده حتما با عبارت جستجو شده صد در صد یکی باشد. حداکثر 20 آیتم در لیست ظاهر خواهند شد. اسکرول بار لیست را حذف کردهایم. عرض آن به 300 تنظیم شده است و نحوه فرمت دهی نمایشی آنرا نیز ملاحظه میکنید. برای این منظور از متد formatItem استفاده شده است. آرایه row در اینجا در برگیرنده اعضای Title و Id ارسالی به افزونه است. اندیس صفر آن به عنوان دریافتی اشاره میکند.
همچنین نحوه نشان دادن عکس العمل به عنصر انتخابی را هم ملاحظه میکنید (در متد result مقدار دهی شده). window.location را به عنصر دوم آرایه row هدایت خواهیم کرد. این عنصر دوم مطابق کدهای اکشن متد تهیه شده، به آدرس یک صفحه اشاره میکند.
ه) نمایش Auto-Complete در ASP.NET WebForms
قسمت عمده مطالب فوق با وب فرمها نیز یکی است. خصوصا توضیحات مرتبط با متد EnableSearchAutocomplete ذکر شده.
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="LuceneSearch.WebForms.Default" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width" /> <title>جستجو</title> <link href="Content/Site.css" rel="stylesheet" type="text/css" /> <script src="Scripts/jquery-1.7.1.min.js" type="text/javascript"></script> <script src="Scripts/jquery.autocomplete.js" type="text/javascript"></script> <script src="Scripts/custom.js" type="text/javascript"></script> </head> <body dir="rtl"> <h2> جستجو</h2> <form id="form1" runat="server"> <div align="center"> <asp:TextBox runat="server" dir="ltr" ID="term"></asp:TextBox> <br /> جهت آزمایش lu را وارد نمائید </div> </form> <script type="text/javascript"> EnableSearchAutocomplete('Search.ashx', 'Content/Images/bullet_shape.png'); </script> </body> </html>
using System.Text; using System.Web; using LuceneSearch.Core; namespace LuceneSearch.WebForms { public class Search : IHttpHandler { static string _indexPath = HttpRuntime.AppDomainAppPath + @"App_Data\idx"; public void ProcessRequest(HttpContext context) { string q = context.Request.QueryString["q"]; if (string.IsNullOrWhiteSpace(q)) { context.Response.Write(string.Empty); context.Response.End(); } var result = new StringBuilder(); var items = AutoComplete.GetTermsScored(_indexPath, q); foreach (var item in items) { var postUrl = "Default.aspx?id=" + item.Id; result.AppendLine(item.Title + "|" + postUrl); } context.Response.ContentType = "text/plain"; context.Response.Write(result.ToString()); context.Response.End(); } public bool IsReusable { get { return false; } } } }
در اینجا بجای Controller از یک Generic handler استفاده شده است (Search.ashx).
result.AppendLine(item.Title + "|" + postUrl); context.Response.Write(result.ToString());
کدهای کامل مثال فوق را از اینجا میتوانید دریافت کنید:
همچنین باید دقت داشت که پروژه MVC آن از نوع MVC4 است (VS2010) و فرض براین میباشد که IIS Express 7.5 را نیز پیشتر نصب کردهاید.
کلمه عبور فایل: dotnettips91
واژهی کلیدی جدید in
C# 7.2، واژهی کلیدی جدیدی را به نام in جهت تعریف پارامترها، معرفی کردهاست. زمانیکه از آن استفاده میشود به این معنا است که value type ارسالی به آن، توسط ارجاعی از آن، در اختیار متد قرار میگیرد و نه توسط مقدار کپی شدهی آن (حالت پیشفرض) و همچنین متد استفاده کنندهی از آن، مقدار این شیء را تغییر نمیدهد.
واژهی کلیدی in مکمل واژههای کلیدی ref و out است که پیشتر به همراه زبان #C ارائه شده بودند:
- واژهی کلیدی out: مقدار آرگومان مزین شدهی توسط آن، باید درون متد تنظیم شود و صرفا کاربرد ارائهی یک خروجی اضافهتر توسط آن متد را دارد.
- واژهی کلیدی ref: مقدار آرگومان مزین شدهی توسط آن، ممکن است درون متد تنظیم شود، یا خیر و همچنین توسط ارجاع به آن منتقل میشود.
- واژهی کلیدی in: مقدار آرگومان مزین شدهی توسط آن، درون متد تغییر نخواهد کرد و همچنین توسط ارجاع به آن منتقل میشود.
برای مثال اگر پارامترهای value type متد زیر را از نوع in معرفی کنیم، امکان تغییر مقدار آنها درون متد وجود نخواهد داشت:
public static int Add(in int number1, in int number2) { number1 = 5; // Cannot assign to variable 'in int' because it is a readonly variable return number1 + number2; }
واژهی کلیدی جدید in تا چه اندازهای بر روی کارآیی برنامه تاثیر دارد؟
زمانیکه یک value type را به متدی ارسال میکنیم، ابتدا به مکان جدیدی از حافظه کپی شده و سپس مقدار clone شدهی آن، به متد ارسال میشود. با استفاده از واژهی کلیدی in، دقیقا همان ارجاع به مقدار اولیه، به متد ارسال خواهد شد؛ بدون ایجاد کپی اضافهتری از آن. برای بررسی تاثیر این عملیات بر روی کارآیی برنامه، میتوان از BenchmarkDotNet استفاده کرد. برای این منظور ابتدا ارجاعی را به BenchmarkDotNet اضافه میکنیم:
<ItemGroup> <PackageReference Include="BenchmarkDotNet" Version="0.10.12" /> </ItemGroup>
using BenchmarkDotNet.Attributes; namespace CS72Tests { public struct Input { public decimal Number1; public decimal Number2; } [MemoryDiagnoser] public class InBenchmarking { const int loops = 50000000; Input inputInstance = new Input(); [Benchmark(Baseline = true)] public decimal RunNormalLoop_Pass_By_Value() { decimal result = 0M; for (int i = 0; i < loops; i++) { result = Run(inputInstance); } return result; } [Benchmark] public decimal RunInLoop_Pass_By_Reference() { decimal result = 0M; for (int i = 0; i < loops; i++) { result = RunIn(in inputInstance); } return result; } public decimal Run(Input input) { return input.Number1; } public decimal RunIn(in Input input) { return input.Number1; } } }
static void Main(string[] args) { var summary = BenchmarkRunner.Run<InBenchmarking>();
با این خروجی نهایی:
Method | Mean | Error | StdDev | Scaled | Allocated | ---------------------------- |----------:|---------:|---------:|-------:|----------:| RunNormalLoop_Pass_By_Value | 280.04 ms | 2.219 ms | 1.733 ms | 1.00 | 0 B | RunInLoop_Pass_By_Reference | 91.75 ms | 1.733 ms | 1.780 ms | 0.33 | 0 B |
امکان استفادهی از واژهی کلیدی in در حین تعریف متدهای الحاقی
در حین تعریف متدهای الحاقی، واژهی کلیدی in باید پیش از واژهی کلیدی this ذکر شود:
public static class Factorial { public static int Calculate(in this int num) { int result = 1; for (int i = num; i > 1; i--) result *= i; return result; } }
int num = 3; Console.WriteLine($"(in num) -> {Factorial.Calculate(in num)}"); Console.WriteLine($"(num) -> {Factorial.Calculate(num)}"); Console.WriteLine($"num. -> {num.Calculate()}");
(in num) -> 6 (num) -> 6 num. -> 6
و به طور کلی استفادهی از in در مکانهای ذیل مجاز است:
• methods
• delegates
• lambdas
• local functions
• indexers
• operators
محدودیتهای استفادهی از پارامترهای in
الف) محدودیت استفاده از پارامترهای in در تعریف overloads
مثال زیر را در نظر بگیرید:
public class CX { public void A(Input a) { Console.WriteLine("int a"); } public void A(in Input a) { Console.WriteLine("in int a"); } }
اگر سعی کنیم وهلهای از این کلاس را ایجاد کرده و از متدهای A آن استفاده کنیم:
public class Y { public void Test() { var inputInstance = new Input(); var cx = new CX(); cx.A(inputInstance); // The call is ambiguous between the following methods or properties: 'CX.A(Input)' and 'CX.A(in Input)' } }
ب) پارامترهای از نوع in را در متدهای iterator نمیتوان استفاده کرد:
public IEnumerable<int> B(in int a) // Iterators cannot have ref or out parameters { Console.WriteLine("in int a"); yield return 1; }
ج) پارامترهای از نوع in را در متدهای async نمیتوان استفاده کرد:
public async Task C(in int a) // Async methods cannot have ref or out parameters { await Task.Delay(1000); }
تاثیر کار با متدهای داخلی تغییر دهندهی وضعیت یک struct
مثال زیر را درنظر بگیرید. به نظر شما خروجی آن چیست؟
using System; namespace CS72Tests { struct MyStruct { public int MyValue { get; set; } public void UpdateMyValue(int value) { MyValue = value; } } public static class TestInStructs { public static void Run() { var myStruct = new MyStruct(); myStruct.UpdateMyValue(1); UpdateMyValue(myStruct); Console.WriteLine(myStruct.MyValue); } static void UpdateMyValue(in MyStruct myStruct) { myStruct.UpdateMyValue(5); } } }
در ابتدا مقدار struct را به 1 تنظیم و سپس ارجاع آنرا به متدی دیگر که مقدار آنرا به 5 تنظیم میکند، ارسال کردیم. در این حالت برنامه بدون مشکل کامپایل و اجرا میشود. علت اینجا است که کامپایلر #C زمانیکه متدی را در داخل یک struct فراخوانی میکند، یک clone از آن struct را ایجاد کرده و متد را بر روی آن clone اجرا میکند؛ چون نمیداند که آیا این متد وضعیت و مقدار این struct را تغییر میدهد یا خیر. در این حالت کپی اصلی بدون تغییر باقی میماند (در نهایت عدد 1 را مشاهده خواهیم کرد)، اما در آخر فراخوان، ارجاعی از struct را دریافت نکرده و بر روی کپی آن کار میکند. بنابراین مزیت بهبود کارآیی، از دست خواهد رفت.
البته در اینجا اگر میخواستیم مقدار MyValue را مستقیما تغییر دهیم، کامپایلر از آن جلوگیری میکرد و این کد هیچگاه کامپایل نمیشد:
static void UpdateMyValue(in MyStruct myStruct) { myStruct.MyValue = 5; // Cannot assign to a member of variable 'in MyStruct' because it is a readonly variable myStruct.UpdateMyValue(5); }
ASP.NET MVC #21
@{ var postUrl = Url.Action(actionName: "Delete", controllerName: "Student"); } <div class="deleteDialog"> <div> آیتمهای انتخاب شده حذف خواهند شد. آیا تأیید میکنید؟ </div> <p> <input type="submit" id="btn_SubmitDelete" value="حذف" /> <input type="submit" id="btn_CancelDelete" value="انصراف" /> </p> </div> <script type="text/javascript"> $(function () { $("#btn_SubmitDelete").click(function (e) { var button = $(this); e.preventDefault(); var data = "1,3,8,9"; $.ajax({ type: "POST", url: "@postUrl", data: JSON.stringify({ ids: data}), contentType: "application/json; charset=utf-8", dataType: 'json', cache: false, beforeSend: function () { }, success: function (html) { alert(html); $(".deleteDialog").parent("div").css("display", "none"); }, complete: function () { button.removeAttr('disabled'); button.val("حذف"); } }); }); $("#btn_CancelDelete").click(function (e) { e.preventDefault(); var button = $(this); $(".deleteDialog").parent("div").css("display", "none"); }); }); </script>
[HttpGet] public ActionResult Delete() { return PartialView("Pv_Delete"); } [HttpPost] [AjaxOnly] public ActionResult Delete(string ids) { var allIds = ids.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries).ToList(); Thread.Sleep(2000); if (true) { return Json(new { result = "ok" }, JsonRequestBehavior.AllowGet); } return Json(new { result = "error" }); }
هدف، ارائه راهحلی برای نمایش جدولی اطلاعات، جستجو، مرتب سازی و صفحه بندی و همچنین انجام عملیات ثبت، ویرایش و حذف بر روی آنها به صورت Ajaxای در بخش back office نرم افزار میباشد.
پیش نیازها:
- طراحی یک گرید با Angular و ASP.NET Core - قسمت اول - پیاده سازی سمت سرور (قسمت اول این سری 3 قسمتی، شامل توضیحات کاملی در مورد دلیل وجود یکسری واسط، کلاس پایه و متدهای کمکی است که در مقاله جاری هم آنها را مشاهده خواهید کرد.)
ایده کار به این شکل میباشد که برای نمایش اطلاعات به صورت جدولی با قابلیتهای مذکور، لازم است یک اکشن Index برای نمایش اولیه و صفحه اول اطلاعات صفحه بندی شده و اکشن متدی به نام List برای پاسخ به درخواستهای صفحه بندی، مرتب سازی، تغییر تعداد آیتمها در هر صفحه و همچنین جستجو، داشته باشیم که این اکشن متد List، بعد از واکشی اطلاعات مورد نظر از منبع داده، آنها را به همراه اطلاعاتی که در کوئری استرینگ درخواست جاری وجود دارد در قالب یک PartialView به کلاینت ارسال کند.
ایجاد مدلهای پایه
همانطور که در مقاله «طراحی و پیاده سازی ServiceLayer به همراه خودکارسازی Business Validationها» مطرح شد، برای پیاده سازی متدهای GetPagedList در ApplicationServiceها از الگوی Request/Response استفاده میکنیم. برای این منظور واسط و کلاسهای زیر را خواهیم داشت:
واسط IPagedQueryModel
public interface IPagedQueryModel { int Page { get; set; } int PageSize { get; set; } /// <summary> /// Expression of Sorting. /// </summary> /// <example> /// Examples: /// "Name_ASC" /// </example> string SortExpression { get; set; } }
این واسط قراردادی میباشد برای نوع و نام پارامترهایی که توسط کلاینت به سرور ارسال میشود. پراپرتی SortExpression آن، نام و ترتیب مرتب سازی را مشخص میکند؛ برای این منظور FieldName_ASC و FieldName_DESC به ترتیب برای حالات مرتب سازی صعودی و نزولی براساس FieldName مقدار دهی خواهد شد.
برای جلوگیری از تکرار این خصوصیات در مدلهای کوئری مربوط به موجودیتها، میتوان کلاس پایهای به شکل زیر در نظر گرفت که پیاده ساز واسط بالا میباشد:
public class PagedQueryModel : IPagedQueryModel, IShouldNormalize { public int Page { get; set; } public int PageSize { get; set; } /// <summary> /// Expression of Sorting. /// </summary> /// <example> /// Examples: /// "Name_ASC" /// </example> public string SortExpression { get; set; } public virtual void Normalize() { if (Page < 1) Page = 1; if (PageSize < 1) PageSize = 10; if (SortExpression.IsEmpty()) SortExpression = "Id_DESC"; } }
مدل بالا علاوه بر پیاده سازی واسط IPagedQueryModel، پیاده ساز واسط IShouldNormalize نیز میباشد؛ دلیل وجود چنین واسطی در مقاله «طراحی و پیاده سازی ServiceLayer به همراه خودکارسازی Business Validationها» توضیح داده شده است:
پیاده سازی واسط IShouldNormalize باعث خواهد شد که قبل از اجرای خود متد، این نوع پارامترها با استفاده از یک Interceptor شناسایی شده و متد Normalize آنها اجرا شود.
کلاس PagedQueryResult
public class PagedQueryResult<TModel> { public PagedQueryResult() { Items = new List<TModel>(); } public IEnumerable<TModel> Items { get; set; } public long TotalCount { get; set; } }
دلیل وجود کلاس بالا در مقاله «طراحی یک گرید با Angular و ASP.NET Core - قسمت اول - پیاده سازی سمت سرور» توضیح داده شده است:
عموما ساختار اطلاعات صفحه بندی شده، شامل تعداد کل آیتمهای تمام صفحات (خاصیت TotalItems) و تنها اطلاعات ردیفهای صفحهی جاری درخواستی (خاصیت Items) است و چون در اینجا این Items از هر نوعی میتواند باشد، بهتر است آنرا جنریک تعریف کنیم.
کلاس PagedListModel
همانطور که در اول بحث توضیح داده شد، لازم است اطلاعاتی را که کلاینت از طریق کوئری استرینگ برای صفحه بندی و ... ارسال کرده بود نیز به PartialView ارسال کنیم. این قسمت کار ایده اصلی این روش را در بر میگیرد؛ اگر نخواهیم اطلاعات کوئری استرینگ دریافتی از کلاینت را دوباره به PartialView ارسال کنیم، مجبور خواهیم بود تمام کارهای مربوط به تشخیص آیکن مرتب سازی ستونهای جدول، ریست کردن المنتهای مربوط به صفحه بندی و مرتب سازی را در در زمان انجام جستجو و یکسری کارهای از این قبل را در سمت کلاینت مدیریت کنیم که هدف مقاله جاری پیاده سازی این روش نمیباشد.
public class PagedListModel<TModel> { public IPagedQueryModel Query { get; set; } public PagedQueryResult<TModel> Result { get; set; } }
پراپرتی Query در برگیرنده پارامتر ورودی اکشن متد List میباشد که پراپرتیهای آن با مقادیر موجود در کوئری استرینگ درخواست جاری مقدار دهی شدهاند؛ البته بدون وجود کلاس بالا نیز به کمک ViewBag میشود این اطلاعات ترکیبی را به ویو ارسال کرد که پیشنهاد نمیشود.
متد GetPagedListAsync موجود در CrudApplicationService
public abstract class CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel, TPagedQueryModel, TDynamicQueryModel> : ApplicationService, ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, TPagedQueryModel, TDynamicQueryModel> where TEntity : Entity, new() where TCreateModel : class where TEditModel : class, IModel where TModel : class, IModel where TDeleteModel : class, IModel where TPagedQueryModel : PagedQueryModel, new() where TDynamicQueryModel : DynamicQueryModel { #region Properties protected IQueryable<TEntity> UnTrackedEntitySet => EntitySet.AsNoTracking(); public IUnitOfWork UnitOfWork { get; set; } public IMapper Mapper { get; set; } protected IDbSet<TEntity> EntitySet => UnitOfWork.Set<TEntity>(); #endregion #region ICrudApplicationService Members #region Methods public virtual async Task<PagedQueryResult<TModel>> GetPagedListAsync(TPagedQueryModel model) { Guard.ArgumentNotNull(model, nameof(model)); var query = ApplyFiltering(model); var totalCount = await query.LongCountAsync().ConfigureAwait(false); var result = query.ProjectTo<TModel>(Mapper.ConfigurationProvider); result = result.ApplySorting(model); result = result.ApplyPaging(model); return new PagedQueryResult<TModel> { Items = await result.ToListAsync().ConfigureAwait(false), TotalCount = totalCount }; } #endregion #endregion #region Protected Methods /// <summary> /// Apply Filtering To GetPagedList and GetPagedListAsync /// </summary> /// <param name="model"></param> /// <returns></returns> protected virtual IQueryable<TEntity> ApplyFiltering(TPagedQueryModel model) { Guard.ArgumentNotNull(model, nameof(model)); return UnTrackedEntitySet; } #endregion }
در بدنه این متد، ابتدا عملیات جستجو توسط متد ApplyFiltering انجام میشود. این متد به صورت پیش فرض هیچ شرطی را بر روی کوئری ارسالی به منبع داده اعمال نمیکند؛ مگر اینکه توسط زیر کلاسها بازنویسی شود و فیلترهای مورد نیاز اعمال شوند. سپس تعداد کل آیتمهای فیلتر شده محاسبه شده و بعد از عملیات Projection، مرتب سازی و صفحه بندی انجام میگیرد. برای مباحث مرتب سازی و صفحه بندی از دو متد زیر کمک گرفته شدهاست:
public static class QueryableExtensions { public static IQueryable<TModel> ApplySorting<TModel>(this IQueryable<TModel> query, IPagedQueryModel request) { Guard.ArgumentNotNull(request, nameof(request)); Guard.ArgumentNotNull(query, nameof(query)); return query.OrderBy(request.SortExpression.Replace('_', ' ')); } public static IQueryable<TModel> ApplyPaging<TModel>(this IQueryable<TModel> query, IPagedQueryModel request) { Guard.ArgumentNotNull(request, nameof(request)); Guard.ArgumentNotNull(query, nameof(query)); return request != null ? query.Page((request.Page - 1) * request.PageSize, request.PageSize) : query; } }
به منظور مرتب سازی از کتابخانه System.Liq.Dynamic کمک گرفته شدهاست.
نکته: مشخص است که این روش، وابستگی به وجود متد GetPagedListAsync ندارد و صرفا برای تشریح ارتباط مطالبی که قبلا منتشر شده بود، مطرح شد.
پیاده سازی اکشن متدهای Index و List
public partial class RolesController : BaseController { #region Fields private readonly IRoleService _service; private readonly ILookupService _lookupService; #endregion #region Constractor public RolesController(IRoleService service, ILookupService lookupService) { Guard.ArgumentNotNull(service, nameof(service)); Guard.ArgumentNotNull(lookupService, nameof(lookupService)); _service = service; _lookupService = lookupService; } #endregion #region Index / List [HttpGet] public virtual async Task<ActionResult> Index() { var query = new RolePagedQueryModel(); var result = await _service.GetPagedListAsync(query).ConfigureAwait(false); var pagedList = new PagedListModel<RoleModel> { Query = query, Result = result }; var model = new RoleIndexViewModel { PagedListModel = pagedList, Permissions = _lookupService.GetPermissions() }; return View(model); } [HttpGet, AjaxOnly, NoOutputCache] public virtual async Task<ActionResult> List(RolePagedQueryModel query) { var result = await _service.GetPagedListAsync(query).ConfigureAwait(false); var model = new PagedListModel<RoleModel> { Query = query, Result = result }; return PartialView(MVC.Administration.Roles.Views._List, model); } #endregion }
به عنوان مثال در بالا کنترلر مربوط به گروههای کاربری را مشاهده میکنید. به دلیل اینکه علاوه بر مباحث صفحه بندی و مرتب سازی، امکان جستجو بر اساس نام و دسترسیهای گروه کاربری را نیز نیاز داریم، لازم است مدل زیر را ایجاد کنیم:
public class RolePagedQueryModel : PagedQueryModel { public string Name { get; set; } public string Permission { get; set; } }
در این مورد خاص لازم است لیست دسترسیهای موجود درسیستم به صورت لیستی برای انتخاب در فرم جستجو مهیا باشد. فرم جستجو در ویو مربوط به اکشن Index قرار میگیرد و قرار نیست به همراه پارشال ویو List_ در هر درخواستی از سرور دریافت شود. لذا لازم است مدلی برای ویو Index در نظر بگیریم که به شکل زیر میباشد:
public class RoleIndexViewModel { public RoleIndexViewModel() { Permissions = new List<LookupItem>(); } public IReadOnlyList<LookupItem> Permissions { get; set; } public PagedListModel<RoleModel> PagedListModel { get; set; } }
پراپرتی PagedListModel در برگیرنده اطلاعات مربوط به نمایش اولیه جدول اطلاعات میباشد و پراپرتی Permissions لیست دسترسیهای موجود درسیستم را به ویو منتقل خواهد کرد. اگر ویو ایندکس شما به داده اضافه ای نیاز ندارد، از ایجاد مدل بالا صرف نظر کنید.
ویو Index.cshtml
@model RoleIndexViewModel @{ ViewBag.Title = L("Administration.Views.Role.Index.Title"); ViewBag.ActiveMenu = AdministrationMenuNames.RoleManagement; } <div class="row"> <div class="col-md-12"> <div id="filterPanel" class="panel-collapse collapse" role="tabpanel" aria-labelledby="filterPanel"> <div class="panel panel-default margin-bottom-5"> <div class="panel-body"> @using (Ajax.BeginForm(MVC.Administration.Roles.List(), new AjaxOptions { UpdateTargetId = "RolesList", HttpMethod = "GET" }, new { id = "filterForm", data_submit_on_reset = "true" })) { <div class="row"> <div class="col-md-3"> <input type="text" name="Name" class="form-control" value="" placeholder="@L("Administration.Role.Fields.Name")" /> </div> <div class="col-md-3"> @Html.DropDownList("Permission", Model.Permissions.ToSelectListItems(), L("Administration.Views.Role.FilterBy.Permission"),new {@class="form-control"}) </div> <div class="col-md-3"> <button type="submit" role="button" class="btn btn-info"> @L("Commands.Filter") </button> <button type="reset" role="button" class="btn btn-default"> <i class="fa fa-close"></i> @L("Commands.Reset") </button> </div> </div> } </div> </div> </div> </div> </div> <div class="row"> <div class="col-md-12" id="RolesList"> @{Html.RenderPartial(MVC.Administration.Roles.Views._List, Model.PagedListModel);} </div> </div>
فرم جستجو باید دارای ویژگی data_submit_on_reset با مقدار "true" باشد. به منظور پاکسازی فرم جستجو و ارسال درخواست جستجو با فرمی خالی از داده، برای بازگشت به حالت اولیه از تکه کد زیر استفاده خواهد شد:
$(document).on("reset", "form[data-submit-on-reset]", function () { var form = this; setTimeout(function () { $(form).submit(); }); });
در ادامه پارشال ویو List_ با داده ارسالی به ویو Index، رندر شده و کار نمایش اولیه اطلاعات به صورت جدولی به اتمام میرسد.
پارشال ویو List.cshtml_
@model PagedListModel<RoleModel> @{ Layout = null; var rowNumber = (Model.Query.Page - 1) * Model.Query.PageSize + 1; var refreshUrl = Url.Action(MVC.Administration.Roles.List().AddRouteValues(new RouteValueDictionary(Model.Query.ToDictionary()))); } <div class="panel panel-default margin-bottom-5"> <table class="table table-bordered table-hover" id="RolesListTable" data-ajax-refresh-url="@refreshUrl" data-ajax-refresh-update="#RolesList"> <thead> <tr> <th style="width: 5%;"> # </th> <th class="col-md-3 sortable"> @Html.SortableColumn("Name", L("Administration.Role.Fields.Name"), Model.Query, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary))) </th> <th class="col-md-3 sortable"> @Html.SortableColumn("DisplayName", L("Administration.Role.Fields.DisplayName"), Model.Query, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary))) </th> <th class="col-md-3 sortable"> @Html.SortableColumn("IsDefault", L("Administration.Role.Fields.IsDefault"), Model.Query, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary))) </th> <th style="width: 5%;"></th> </tr> </thead> <tbody> @foreach (var role in Model.Result.Items) { <tr> <td>@(rowNumber++.ToPersianNumbers())</td> <td>@role.Name</td> <td>@role.DisplayName</td> <td class="text-center">@Html.DisplayFor(a => role.IsDefault)</td> <td class="text-center operations"> <div class="btn-group"> <span class="fa fa-ellipsis-h dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span> <ul class="dropdown-menu dropdown-menu-left"> <li> <a href="#" role="button" data-ajax="true" data-ajax-method="GET" data-ajax-update="#main-modal div.modal-content" data-ajax-url="@Url.Action(MVC.Administration.Roles.Edit(role.Id))" data-toggle="modal" data-target="#main-modal"> <i class="fa fa-pencil"></i> @L("Commands.Edit") </a> </li> <li> <a href="#" role="button" id="delete-@role.Id" data-delete-url="@Url.Action(MVC.Administration.Roles.Delete())" data-delete-model='{"Id":"@role.Id","RowVersion":"@Convert.ToBase64String(role.RowVersion)"}'> <i class="fa fa-trash"></i> @L("Commands.Delete") </a> </li> </ul> </div> </td> </tr> } </tbody> </table> </div> <div class="row"> <div class="col-md-8"> @Html.Pager(Model, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary))) @Html.PageSize("RolesList", Model.Query, routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)), new { @class = "margin-right-5" }, "filterForm") @Html.Refresh("RolesList", Model.Query, routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)), L("Commands.Refresh")) </div> </div>
به ترتیب فایل بالا را بررسی میکنیم:
var refreshUrl = Url.Action(MVC.Administration.Roles.List().AddRouteValues(new RouteValueDictionary(Model.Query.ToDictionary())));
refreshUrl برای ارسال درخواست به اکشن متد List در نظر گرفته شدهاست که در کوئری استرینگ مربوط به خود، اطلاعاتی (مرتب سازی، شماره صفحه، اطلاعات جستجو و همچنین تعداد آیتمهای موجود در هر صفحه) را دارد که حالت فعلی گرید را میتوانیم دوباره از سرور درخواست کنیم.
<table class="table table-bordered table-hover" id="RolesListTable" data-ajax-refresh-url="@refreshUrl" data-ajax-refresh-update="#RolesList">
دو ویژگی data-ajax-refresh-url و data-ajax-refresh-update برای جدولی که لازم است عملیات CRUD را پشتیبانی کند، لازم میباشد. در قسمت دوم به استفاده از این دو ویژگی در هنگام عملیات ثبت، ویرایش و حذف خواهیم پرداخت.
<th class="col-md-3 sortable"> @Html.SortableColumn("Name", L("Administration.Role.Fields.Name"), Model.Query, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary))) </th>
ستونی که امکان مرتب سازی را دارد باید th آن، کلاس sortable را داشته باشد. همچنین باید از هلپری که پیاده سازی آن را در ادامه خواهیم دید، استفاده کنیم. این هلپر، نام فیلد، عنوان ستون، مدل Query و همچین یک urlFactory را در قالب یک Func<RouteValueDictionary,string> دریافت میکند.
پیاده سازی هلپر SortableColumn
public static MvcHtmlString SortableColumn(this HtmlHelper html, string columnName, string columnDisplayName, IPagedQueryModel queryModel, string updateTargetId, Func<RouteValueDictionary, string> urlFactory) { var dictionary = queryModel.ToDictionary(); var routeValueDictionary = new RouteValueDictionary(dictionary) { ["SortExpression"] = !queryModel.SortExpression.StartsWith(columnName) ? $"{columnName}_DESC" : queryModel.SortExpression.EndsWith("DESC") ? $"{columnName}_ASC" : queryModel.SortExpression.EndsWith("ASC") ? string.Empty : $"{columnName}_DESC" }; var url = urlFactory(routeValueDictionary); var aTag = new TagBuilder("a"); aTag.Attributes.Add("href", "#"); aTag.Attributes.Add("data-ajax", "true"); aTag.Attributes.Add("data-ajax-method", "GET"); aTag.Attributes.Add("data-ajax-update", updateTargetId.StartsWith("#") ? updateTargetId : $"#{updateTargetId}"); aTag.Attributes.Add("data-ajax-url", url); aTag.InnerHtml = columnDisplayName; var iconCssClass = !queryModel.SortExpression.StartsWith(columnName) ? "fa-sort" : queryModel.SortExpression.EndsWith("DESC") ? "fa-sort-down" : "fa-sort-up"; var iTag = new TagBuilder("i"); iTag.AddCssClass($"fa {iconCssClass}"); return new MvcHtmlString($"{aTag}\n{iTag}"); }
ابتدا مدل Query با متد الحاقی زیر تبدیل به دیکشنری میشود. این کار از این جهت مهم است که پراپرتیهای لیست موجود در مدل Query، لازم است به فرم خاصی به سرور ارسال شوند که در تکه کد زیر مشخص میباشد.
public static IDictionary<string, object> ToDictionary(this object source) { return source.ToDictionary<object>(); } public static IDictionary<string, T> ToDictionary<T>(this object source) { if (source == null) throw new ArgumentNullException(nameof(source)); var dictionary = new Dictionary<string, T>(); foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(source)) { AddPropertyToDictionary(property, source, dictionary); } return dictionary; } private static void AddPropertyToDictionary<T>(PropertyDescriptor property, object source, IDictionary<string, T> dictionary) { var value = property.GetValue(source); var items = value as IEnumerable; if (items != null && !(items is string)) { var i = 0; foreach (var item in items) { dictionary.Add($"{property.Name}[{i++}]", (T)item); } } else if (value is T) { dictionary.Add(property.Name, (T)value); } }
در متد بالا، از TypeDescriptor که یکی دیگر از ابزارهای دسترسی به متا دیتای انوع دادهای است، استفاده شده و خروجی نهایی آن یک دیکشنری با کلیدهایی با اسامی پراپرتیهای وهله ورودی میباشد.
در ادامه پیاده سازی هلپر SortableColumn، از دیکشنری حاصل، یک وهله از RouteValueDictionary ساخته میشود. در زمان رندر شدن PartialView لازم است مشخص شود که برای دفعه بعدی که بر روی این ستون کلیک میشود، باید چه مقداری با پارامتر SortExpression موجود در کوئری استرینگ ارسال شود. از این جهت برای پشتیبانی ستون، از حالتهای مرتب سازی صعودی، نزولی و برگشت به حالت اولیه بدون مرتب سازی، کد زیر را خواهیم داشت:
var routeValueDictionary = new RouteValueDictionary(dictionary) { ["SortExpression"] = !queryModel.SortExpression.StartsWith(columnName) ? $"{columnName}_DESC" : queryModel.SortExpression.EndsWith("DESC") ? $"{columnName}_ASC" : queryModel.SortExpression.EndsWith("ASC") ? string.Empty : $"{columnName}_DESC" };
در ادامه urlFactory با routeValueDictionary حاصل، Invoke میشود تا url نهایی برای مرتب سازیهای بعدی را از طریق یک لینک تزئین شده با data اتریبیوتهای Unobtrusive Ajax در th مربوطه قرار دهیم.
برای مباحث صفحه بندی، بارگزاری مجدد و تغییر تعداد آیتمها در هر صفحه، از سه هلپر زیر کمک خواهیم گرفت:
<div class="row"> <div class="col-md-8"> @Html.Pager(Model, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary))) @Html.PageSize("RolesList", Model.Query, routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)), new { @class = "margin-right-5" }, "filterForm") @Html.Refresh("RolesList", Model.Query, routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)), L("Commands.Refresh")) </div> </div>
پیاده سازی هلپر Pager
public static MvcHtmlString Pager<TModel>(this HtmlHelper html, PagedListModel<TModel> model, string updateTargetId, Func<RouteValueDictionary, string> urlFactory) { return html.PagedListPager( new StaticPagedList<TModel>(model.Result.Items, model.Query.Page, model.Query.PageSize, (int)model.Result.TotalCount), page => { var dictionary = model.Query.ToDictionary(); var routeValueDictionary = new RouteValueDictionary(dictionary) { ["Page"] = page }; return urlFactory(routeValueDictionary); }, PagedListRenderOptions.EnableUnobtrusiveAjaxReplacing( new PagedListRenderOptions { DisplayLinkToFirstPage = PagedListDisplayMode.Always, DisplayLinkToLastPage = PagedListDisplayMode.Always, DisplayLinkToPreviousPage = PagedListDisplayMode.Always, DisplayLinkToNextPage = PagedListDisplayMode.Always, MaximumPageNumbersToDisplay = 6, DisplayItemSliceAndTotal = true, DisplayEllipsesWhenNotShowingAllPageNumbers = true, ItemSliceAndTotalFormat = $"تعداد کل: {model.Result.TotalCount.ToPersianNumbers()}", FunctionToDisplayEachPageNumber = page => page.ToPersianNumbers(), }, new AjaxOptions { AllowCache = false, HttpMethod = "GET", InsertionMode = InsertionMode.Replace, UpdateTargetId = updateTargetId })); }
در متد بالا از کتابخانه PagedList.Mvc استفاده شدهاست. یکی از overloadهای متد PagedListPager آن، یک پارامتر از نوع Func<int, string> به نام generatePageUrl را دریافت میکند که امکان شخصی سازی فرآیند تولید لینک به صفحات بعدی و قبلی را به ما میدهد. ما نیز از این امکان برای افزودن اطلاعات موجود در مدل Query، به کوئری استرینگ لینکهای تولیدی استفاده کردیم و صرفا برای لینکهای ایجادی لازم بود مقادیر پارامتر Page موجود در کوئری استرینگ تغییر کند که در کد بالا مشخص میباشد.
پیاده سازی هلپر PageSize
public static MvcHtmlString PageSize(this HtmlHelper html, string updateTargetId, IPagedQueryModel queryModel, Func<RouteValueDictionary, string> urlFactory, object htmlAttributes = null, string filterFormId = null, params int[] numbers) { if (numbers.Length == 0) numbers = new[] { 10, 20, 30, 50, 100 }; var dictionary = queryModel.ToDictionary(); var routeValueDictionary = new RouteValueDictionary(dictionary) { [nameof(IPagedQueryModel.Page)] = 1 }; routeValueDictionary.Remove(nameof(IPagedQueryModel.PageSize)); var url = urlFactory(routeValueDictionary); var formTag = new TagBuilder("form"); formTag.Attributes.Add("action", url); formTag.Attributes.Add("method", "GET"); formTag.Attributes.Add("data-ajax", "true"); formTag.Attributes.Add("data-ajax-method", "GET"); formTag.Attributes.Add("data-ajax-update", updateTargetId.StartsWith("#") ? updateTargetId : $"#{updateTargetId}"); formTag.Attributes.Add("data-ajax-url", url); if (htmlAttributes != null) formTag.MergeAttributes(HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); formTag.AddCssClass("form-inline inline"); var items = numbers.Select(number => new SelectListItem { Value = number.ToString(), Text = number.ToString().ToPersianNumbers(), Selected = queryModel.PageSize == number }); formTag.InnerHtml = html.DropDownList(nameof(IPagedQueryModel.PageSize), items, new { @class = "form-control page-size", onchange = "$(this.form).submit();" }).ToString(); if (filterFormId.IsEmpty()) return new MvcHtmlString($"{formTag}"); // ReSharper disable once MustUseReturnValue var scriptBlock = $"<script type=\"text/javascript\"> if(window.jQuery){{$('form#{filterFormId}').find('input[name=\"{nameof(IPagedQueryModel.PageSize)}\"]').remove();\n $('form#{filterFormId}').append(\"<input type='hidden' name='{nameof(IPagedQueryModel.PageSize)}' value='{queryModel.PageSize}'/>\")}}</script>"; return new MvcHtmlString($"{formTag}\n{scriptBlock}"); }
ایده کار به این صورت است که یک المنت select، درون یک المنت form قرار میگیرد و در زمان change آن، فرم مربوطه submit میشود.
formTag.InnerHtml = html.DropDownList(nameof(IPagedQueryModel.PageSize), items, new { @class = "form-control page-size", onchange = "$(this.form).submit();" }).ToString();
در زمان تغییر تعداد نمایشی آیتمها در هر صفحه، لازم است حالت فعلی گرید حفظ شود و صرفا پارامتر Page ریست شود.
نکته مهم: در این طراحی اگر فرم جستجویی دارید، در زمان جستجو هیچیک از پارامترهای مربوط به صفحه بندی و مرتب سازی به سرور ارسال نخواهند شد (در واقع ریست میشوند) و کافیست یک درخواست GET معمولی با ارسال محتویات فرم به سرور صورت گیرد؛ ولی لازم است PageSize تنظیم شده، در زمان اعمال فیلتر نیز به سرور ارسال شود. از این جهت اسکریپتی برای ایجاد یک input مخفی در فرم جستجو نیز هنگام رندر شدن PartialView در صفحه تزریق میشود.
var scriptBlock = $"<script type=\"text/javascript\"> if(window.jQuery){{$('form#{filterFormId}').find('input[name=\"{nameof(IPagedQueryModel.PageSize)}\"]').remove();\n $('form#{filterFormId}').append(\"<input type='hidden' name='{nameof(IPagedQueryModel.PageSize)}' value='{queryModel.PageSize}'/>\")}}</script>";
پیاده سازی هلپر Refresh
public static MvcHtmlString Refresh(this HtmlHelper html, string updateTargetId, IPagedQueryModel queryModel, Func<RouteValueDictionary, string> urlFactory, string label = null) { var dictionary = queryModel.ToDictionary(); var routeValueDictionary = new RouteValueDictionary(dictionary); var url = urlFactory(routeValueDictionary); var aTag = new TagBuilder("a"); aTag.Attributes.Add("href", "#"); aTag.Attributes.Add("role", "button"); aTag.Attributes.Add("data-ajax", "true"); aTag.Attributes.Add("data-ajax-method", "GET"); aTag.Attributes.Add("data-ajax-update", updateTargetId.StartsWith("#") ? updateTargetId : $"#{updateTargetId}"); aTag.Attributes.Add("data-ajax-url", url); aTag.AddCssClass("btn btn-default"); var iTag = new TagBuilder("i"); iTag.AddCssClass("fa fa-refresh"); aTag.InnerHtml = $"{iTag} {label}"; return new MvcHtmlString(aTag.ToString()); }
متد بالا نیز به مانند refreshUrl که پیشتر مطرح شد، برای بارگزاری مجدد حالت فعلی گرید استفاده میشود و از این جهت است که مقادیر مربوط به کلیدهای routeValueDictionary را تغییر ندادهایم.
روش دیگر برای مدیریت این چنین کارهایی، استفاده از یک المنت form و قرادادن کل گرید به همراه یک سری input مخفی معادل با پارامترهای دریافتی اکشن متد List و مقدار دهی آنها در زمان کلیک بر روی دکمههای صفحه بندی، بارگزاری مجدد، دکمه اعمال فیلتر و لیست آبشاری تنظیم تعداد آیتمها، درون آن نیز میتواند کار ساز باشد؛ اما در زمان پیاده سازی خواهید دید که پیاده سازی آن خیلی سرراست، به مانند پیاده سازی موجود در مطلب جاری نخواهد بود.
در قسمت دوم، به پیاده سازی عملیات ثبت، ویرایش و حذف برپایه مودالهای بوت استرپ و افزونه Unobtrusive Ajax خواهیم پرداخت.
کدهای کامل قسمت جاری، بعد از انتشار قسمت دوم، در مخزن گیت هاب شخصی قرار خواهد گرفت.
آشنایی با کتابخانه NHibernate Validator
پروژه جدیدی به پروژه NHibernate Contrib در سایت سورس فورج اضافه شده است به نام NHibernate Validator که از آدرس زیر قابل دریافت است:
این پروژه که توسط Dario Quintana توسعه یافته است، امکان اعتبار سنجی اطلاعات را پیش از افزوده شدن آنها به دیتابیس به دو صورت دستی و یا خودکار و یکپارچه با NHibernate فراهم میسازد؛ که امروز قصد بررسی آنرا داریم.
کامپایل پروژه اعتبار سنجی NHibernate
پس از دریافت آخرین نگارش موجود کتابخانه NHibernate Validator از سایت سورس فورج، فایل پروژه آنرا در VS.Net گشوده و یکبار آنرا کامپایل نمائید تا فایل اسمبلی NHibernate.Validator.dll حاصل گردد.
بررسی مدل برنامه
در این مدل ساده، تعدادی پزشک داریم و تعدادی بیمار. در سیستم ما هر بیمار تنها توسط یک پزشک مورد معاینه قرار خواهد گرفت. رابطه آنها را در کلاس دیاگرام زیر میتوان مشاهده نمود:
به این صورت پوشه دومین برنامه از کلاسهای زیر تشکیل خواهد شد:
namespace NHSample5.Domain
{
public class Patient
{
public virtual int Id { get; set; }
public virtual string FirstName { get; set; }
public virtual string LastName { get; set; }
}
}
using System.Collections.Generic;
namespace NHSample5.Domain
{
public class Doctor
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual IList<Patient> Patients { get; set; }
public Doctor()
{
Patients = new List<Patient>();
}
}
}
ساختار کلی این پروژه را در شکل زیر مشاهده میکنید:
اطلاعات این برنامه بر مبنای NHRepository و NHSessionManager ایی است که در قسمتهای قبل توسعه دادیم و پیشنیاز ضروری مطالعه آن میباشند (سورس پیوست شده شامل نمونه تکمیل شده این موارد نیز هست). همچنین از قسمت ایجاد دیتابیس از روی مدل نیز صرفنظر میشود و همانند قسمتهای قبل است.
تعریف اعتبار سنجی دومین با کمک ویژگیها (attributes)
فرض کنید میخواهیم بر روی طول نام و نام خانوادگی بیمار محدودیت قرار داده و آنها را با کمک کتابخانه NHibernate Validator ، اعتبار سنجی کنیم. برای این منظور ابتدا فضای نام NHibernate.Validator.Constraints به کلاس بیمار اضافه شده و سپس با کمک ویژگیهایی که در این کتابخانه تعریف شدهاند میتوان قیود خود را به خواص کلاس تعریف شده اعمال نمود که نمونهای از آن را مشاهده مینمائید:
using NHibernate.Validator.Constraints;
namespace NHSample5.Domain
{
public class Patient
{
public virtual int Id { get; set; }
[Length(Min = 3, Max = 20,Message="طول نام باید بین 3 و 20 کاراکتر باشد")]
public virtual string FirstName { get; set; }
[Length(Min = 3, Max = 60, Message = "طول نام خانوادگی باید بین 3 و 60 کاراکتر باشد")]
public virtual string LastName { get; set; }
}
}
مثالی دیگر:
جهت اجباری کردن و همچنین اعمال Regular expressions برای اعتبار سنجی یک فیلد میتوان دو ویژگی زیر را به بالای آن فیلد مورد نظر افزود:
[NotNull]
[Pattern(Regex = "[A-Za-z0-9]+")]
تعریف اعتبار سنجی با کمک کلاس ValidationDef
راه دوم تعریف اعتبار سنجی، کمک گرفتن از کلاس ValidationDef این کتابخانه و استفاده از روش fluent configuration است. برای این منظور، پوشه جدیدی را به برنامه به نام Validation اضافه خواهیم کرد و سپس دو کلاس DoctorDef و PatientDef را به آن به صورت زیر خواهیم افزود:
using NHibernate.Validator.Cfg.Loquacious;
using NHSample5.Domain;
namespace NHSample5.Validation
{
public class DoctorDef : ValidationDef<Doctor>
{
public DoctorDef()
{
Define(x => x.Name).LengthBetween(3, 50);
Define(x => x.Patients).NotNullableAndNotEmpty();
}
}
}
using NHSample5.Domain;
using NHibernate.Validator.Cfg.Loquacious;
namespace NHSample5.Validation
{
public class PatientDef : ValidationDef<Patient>
{
public PatientDef()
{
Define(x => x.FirstName)
.LengthBetween(3, 20)
.WithMessage("طول نام باید بین 3 و 20 کاراکتر باشد");
Define(x => x.LastName)
.LengthBetween(3, 60)
.WithMessage("طول نام خانوادگی باید بین 3 و 60 کاراکتر باشد");
}
}
}
استفاده از قیودات تعریف شده به صورت دستی
میتوان از این کتابخانه اعتبار سنجی به صورت مستقیم نیز اضافه کرد. روش انجام آنرا در متد زیر مشاهده مینمائید.
/// <summary>
/// استفاده از اعتبار سنجی ویژه به صورت مستقیم
/// در صورت استفاده از ویژگیها
/// </summary>
static void WithoutConfiguringTheEngine()
{
//تعریف یک بیمار غیر معتبر
var patient1 = new Patient() { FirstName = "V", LastName = "N" };
var ve = new ValidatorEngine();
var invalidValues = ve.Validate(patient1);
if (invalidValues.Length == 0)
{
Console.WriteLine("patient1 is valid.");
}
else
{
Console.WriteLine("patient1 is NOT valid!");
//نمایش پیغامهای تعریف شده مربوط به هر فیلد
foreach (var invalidValue in invalidValues)
{
Console.WriteLine(
"{0}: {1}",
invalidValue.PropertyName,
invalidValue.Message);
}
}
//تعریف یک بیمار معتبر بر اساس قیودات اعمالی
var patient2 = new Patient() { FirstName = "وحید", LastName = "نصیری" };
if (ve.IsValid(patient2))
{
Console.WriteLine("patient2 is valid.");
}
else
{
Console.WriteLine("patient2 is NOT valid!");
}
}
در اینجا اگر سعی در اعتبار سنجی یک پزشک نمائیم، نتیجهای حاصل نخواهد شد زیرا هنگام استفاده از کلاس ValidationDef، باید نگاشت لازم به این قیودات را نیز دقیقا مشخص نمود تا مورد استفاده قرار گیرد که نحوهی انجام این عملیات را در متد زیر میتوان مشاهده نمود.
public static ValidatorEngine GetFluentlyConfiguredEngine()
{
var vtor = new ValidatorEngine();
var configuration = new FluentConfiguration();
configuration
.Register(
Assembly
.GetExecutingAssembly()
.GetTypes()
.Where(t => t.Namespace.Equals("NHSample5.Validation"))
.ValidationDefinitions()
)
.SetDefaultValidatorMode(ValidatorMode.UseExternal);
vtor.Configure(configuration);
return vtor;
}
FluentConfiguration آن مجزا است از نمونه مشابه کتابخانه Fluent NHibernate و نباید با آن اشتباه گرفته شود (در فضای نام NHibernate.Validator.Cfg.Loquacious تعریف شده است).
در این متد کلاسهای قرار گرفته در پوشه Validation برنامه که دارای فضای نام NHSample5.Validation هستند، به عنوان کلاسهایی که باید اطلاعات لازم مربوط به اعتبار سنجی را از آنان دریافت کرد معرفی شدهاند.
همچنین ValidatorMode نیز به صورت External تعریف شده و منظور از External در اینجا هر چیزی بجز استفاده از روش بکارگیری attributes است (علاوه بر امکان تعریف این قیودات در یک پروژه class library مجزا و مشخص ساختن اسمبلی آن در اینجا).
اکنون جهت دسترسی به این موتور اعتبار سنجی تنظیم شده میتوان به صورت زیر عمل کرد:
/// <summary>
/// استفاده از اعتبار سنجی ویژه به صورت مستقیم
/// در صورت تعریف آنها با کمک
/// ValidationDef
/// </summary>
static void WithConfiguringTheEngine()
{
var ve2 = VeConfig.GetFluentlyConfiguredEngine();
var doctor1 = new Doctor() { Name = "S" };
if (ve2.IsValid(doctor1))
{
Console.WriteLine("doctor1 is valid.");
}
else
{
Console.WriteLine("doctor1 is NOT valid!");
}
var patient1 = new Patient() { FirstName = "وحید", LastName = "نصیری" };
if (ve2.IsValid(patient1))
{
Console.WriteLine("patient1 is valid.");
}
else
{
Console.WriteLine("patient1 is NOT valid!");
}
var doctor2 = new Doctor() { Name = "شمس", Patients = new List<Patient>() { patient1 } };
if (ve2.IsValid(doctor2))
{
Console.WriteLine("doctor2 is valid.");
}
else
{
Console.WriteLine("doctor2 is NOT valid!");
}
}
نکته مهم:
فراخوانی GetFluentlyConfiguredEngine نیز باید یکبار در طول برنامه صورت گرفته و سپس حاصل آن بارها مورد استفاده قرار گیرد. بنابراین نحوهی صحیح دسترسی به آن باید حتما از طریق الگوی Singleton که در قسمتهای قبل در مورد آن بحث شد، انجام شود.
استفاده از قیودات تعریف شده و سیستم اعتبار سنجی به صورت یکپارچه با NHibernate
کتابخانه NHibernate Validator زمانیکه با NHibernate یکپارچه گردد دو رخداد PreInsert و PreUpdate آنرا به صورت خودکار تحت نظر قرار داده و پیش از اینکه اطلاعات ثبت و یا به روز شوند، ابتدا کار اعتبار سنجی خود را انجام داده و اگر اعتبار سنجی مورد نظر با شکست مواجه شود، با ایجاد یک exception از ادامه برنامه جلوگیری میکند. در این حالت استثنای حاصل شده از نوع InvalidStateException خواهد بود.
برای انجام این مرحله یکپارچه سازی ابتدا متد BuildIntegratedFluentlyConfiguredEngine را به شکل زیر باید فراخوانی نمائیم:
/// <summary>
/// از این کانفیگ برای آغاز سشن فکتوری باید کمک گرفته شود
/// </summary>
/// <param name="nhConfiguration"></param>
public static void BuildIntegratedFluentlyConfiguredEngine(ref Configuration nhConfiguration)
{
var vtor = new ValidatorEngine();
var configuration = new FluentConfiguration();
configuration
.Register(
Assembly
.GetExecutingAssembly()
.GetTypes()
.Where(t => t.Namespace.Equals("NHSample5.Validation"))
.ValidationDefinitions()
)
.SetDefaultValidatorMode(ValidatorMode.UseExternal)
.IntegrateWithNHibernate
.ApplyingDDLConstraints()
.And
.RegisteringListeners();
vtor.Configure(configuration);
//Registering of Listeners and DDL-applying here
ValidatorInitializer.Initialize(nhConfiguration, vtor);
}
SingletonCore()
{
Configuration cfg = DbConfig.GetConfig().BuildConfiguration();
VeConfig.BuildIntegratedFluentlyConfiguredEngine(ref cfg);
//با همان کانفیگ تنظیم شده برای اعتبار سنجی باید کار شروع شود
_sessionFactory = cfg.BuildSessionFactory();
}
از این لحظه به بعد، نیاز به فراخوانی متدهای Validate و یا IsValid نبوده و کار اعتبار سنجی به صورت خودکار و یکپارچه با NHibernate انجام میشود. لطفا به مثال زیر دقت بفرمائید:
/// <summary>
/// استفاده از اعتبار سنجی یکپارچه و خودکار
/// </summary>
static void tryToSaveInvalidPatient()
{
using (Repository<Patient> repo = new Repository<Patient>())
{
try
{
var patient1 = new Patient() { FirstName = "V", LastName = "N" };
repo.Save(patient1);
}
catch (InvalidStateException ex)
{
Console.WriteLine("Validation failed!");
foreach (var invalidValue in ex.GetInvalidValues())
Console.WriteLine(
"{0}: {1}",
invalidValue.PropertyName,
invalidValue.Message);
log4net.LogManager.GetLogger("NHibernate.SQL").Error(ex);
}
}
}
/// <summary>
/// استفاده از اعتبار سنجی یکپارچه و خودکار
/// </summary>
static void tryToSaveValidPatient()
{
using (Repository<Patient> repo = new Repository<Patient>())
{
var patient1 = new Patient() { FirstName = "Vahid", LastName = "Nasiri" };
repo.Save(patient1);
}
}
نکته:
اگر کار ساخت database schema را با کمک کانفیگ تنظیم شده توسط کتابخانه اعتبار سنجی آغاز کنیم، طول فیلدها دقیقا مطابق با حداکثر طول مشخص شده در قسمت تعاریف قیود هر یک از فیلدها تشکیل میگردد (حاصل از اعمال متد ApplyingDDLConstraints در متد BuildIntegratedFluentlyConfiguredEngine ذکر شده میباشد).
public static void CreateValidDb()
{
bool script = false;//آیا خروجی در کنسول هم نمایش داده شود
bool export = true;//آیا بر روی دیتابیس هم اجرا شود
bool dropTables = false;//آیا جداول موجود دراپ شوند
Configuration cfg = DbConfig.GetConfig().BuildConfiguration();
VeConfig.BuildIntegratedFluentlyConfiguredEngine(ref cfg);
//با همان کانفیگ تنظیم شده برای اعتبار سنجی باید کار شروع شود
new SchemaExport(cfg).Execute(script, export, dropTables);
}
دریافت سورس کامل قسمت دهم
مشکل: زمانیکه یک AsyncPostback در آپدیت پنلASP.Net Ajax رخ دهد، پس از پایان کار، پلاگین جیکوئری که در حال استفاده از آن بودید و در هنگام بارگذاری اولیه صفحه بسیار خوب کار میکرد، اکنون از کار افتاده است و دیگر جواب نمیدهد.
قبل از شروع، نیاز به یک سری پیش زمینه هست (شاید بر اساس روش استفاده شما از آن پلاگین جیکوئری، مشکل را حل کنند):
الف) رفع تداخل جیکوئری با سایر کتابخانههای مشابه.
ب) آشنایی با jQuery Live جهت بایند رخدادها به عناصری که بعدا به صفحه اضافه خواهند شد.
ج) تزریق اسکریپت به صفحه در حین یک AsyncPostback رخ داده در آپدیت پنل
علت بروز مشکل:
علت رخدادن این مشکل (علاوه بر قسمت الف ذکر شده)، عدم فراخوانی document.ready تعریف شده، جهت بایند مجدد پلاگین jQuery مورد استفاده شما پس از هر AsyncPostback رخ داده در آپدیت پنل ASP.Net Ajax است. راه حل استاندارد جیکوئری هم همان مورد (ب) فوق میباشد، اما ممکن است جهت استفاده از آن نیاز به بازنویسی یک پلاگین موجود خاص وجود داشته باشد، که آنچنان مقرون به صرفه نیست.
مثالی جهت مشاهدهی این مشکل در عمل:
میخواهیم افزونهی Colorize - jQuery Table را به یک گرید ویوو ASP.Net قرار گرفته درون یک آپدیت پنل اعمال کنیم.
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="UpdatePanelTest.aspx.cs"
Inherits="TestJQueryAjax.UpdatePanelTest" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<asp:ScriptManager ID="sm" runat="server">
<Scripts>
<asp:ScriptReference Path="~/js/jquery.js" />
<asp:ScriptReference Path="~/js/jquery.colorize-1.6.0.js" />
</Scripts>
</asp:ScriptManager>
<asp:UpdatePanel ID="uppnl" runat="server">
<ContentTemplate>
<asp:GridView ID="GridView1" runat="server" AllowPaging="True"
OnPageIndexChanging="GridView1_PageIndexChanging">
</asp:GridView>
</ContentTemplate>
</asp:UpdatePanel>
</form>
<script type="text/javascript">
$(document).ready(function() {
$('#<%=GridView1.ClientID %>').colorize();
});
</script>
</body>
</html>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
namespace TestJQueryAjax
{
public partial class UpdatePanelTest : System.Web.UI.Page
{
void BindTo()
{
List<string> rows = new List<string>();
for (int i = 0; i < 1000; i++)
rows.Add(string.Format("row{0}", i));
GridView1.DataSource = rows;
GridView1.DataBind();
}
protected void Page_Load(object sender, EventArgs e)
{
if (!Page.IsPostBack)
{
BindTo();
}
}
protected void GridView1_PageIndexChanging(object sender, GridViewPageEventArgs e)
{
GridView1.PageIndex = e.NewPageIndex;
BindTo();
}
}
}
راه حل:
از ویژگیهای ذاتی ASP.Net Ajax باید کمک گرفت برای مثال:
<script type="text/javascript">
$(document).ready(function() {
$('#<%=GridView1.ClientID %>').colorize();
});
function pageLoad(sender, args) {
if (args.get_isPartialLoad()) {
$('#<%=GridView1.ClientID %>').colorize();
}
}
</script>
متد استاندارد pageLoad به صورت خودکار پس از هر AsyncPostback رخ داده در آپدیت پنل ASP.Net Ajax فراخوانی میشود (و همچنین پس از پایان پردازش و بارگذاری اولیه DOM صفحه). در این متد بررسی میکنیم که آیا یک partial postback رخ داده است؟ اگر بله، مجددا عملیات بایند افزونه به گرید را انجام میدهیم و مشکل برطرف خواهد شد.
برای مطالعه بیشتر