if (ModelState.IsValid) { // find user by username var user = await _signInManager.UserManager.FindByNameAsync(Input.Username); // validate username/password using ASP.NET Identity if (user != null && (await _signInManager.CheckPasswordSignInAsync(user, Input.Password, true)) == SignInResult.Success) { await _events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id, user.UserName, clientId: context?.Client.ClientId)); // only set explicit expiration here if user chooses "remember me". // otherwise we rely upon expiration configured in cookie middleware. AuthenticationProperties props = null; if (LoginOptions.AllowRememberLogin && Input.RememberLogin) { props = new AuthenticationProperties { IsPersistent = true, ExpiresUtc = DateTimeOffset.UtcNow.Add(LoginOptions.RememberMeLoginDuration) }; }; // issue authentication cookie with subject ID and username var isuser = new IdentityServerUser(user.Id) { DisplayName = user.UserName }; await HttpContext.SignInAsync(isuser, props); if (context != null) { if (context.IsNativeClient()) { // The client is native, so this change in how to // return the response is for better UX for the end user. return this.LoadingPage(Input.ReturnUrl); } // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null return Redirect(Input.ReturnUrl); } // request for a local page if (Url.IsLocalUrl(Input.ReturnUrl)) { return Redirect(Input.ReturnUrl); } else if (string.IsNullOrEmpty(Input.ReturnUrl)) { return Redirect("~/"); } else { // user might have clicked on a malicious link - should be logged throw new Exception("invalid return URL"); } } await _events.RaiseAsync(new UserLoginFailureEvent(Input.Username, "invalid credentials", clientId: context?.Client.ClientId)); ModelState.AddModelError(string.Empty, LoginOptions.InvalidCredentialsErrorMessage); }
برای نمونه فایل نمونه سؤالات مصاحبات دات را از اینجا دریافت کنید. یک چنین شکلی دارد:
خوب! چطور میتوان این Watermark را حذف یا حداقل نامرئی کرد؟
برای پاسخ به این سؤال نیاز است ابتدا با نحوه Watermark دار کردن صفحات یک فایل PDF آشناشویم.
الف) ایجاد یک فایل PDF ساده
private static void createPdfFile(string pdfFile) { using (var fs = new FileStream(pdfFile, FileMode.Create, FileAccess.Write, FileShare.None)) { using (var doc = new Document(PageSize.A4)) { using (var witier = PdfWriter.GetInstance(doc, fs)) { doc.Open(); for (int i = 1; i <= 5; i++) { doc.NewPage(); doc.Add(new Paragraph(String.Format("This is page {0}", i))); } doc.Close(); } } } }
ب) افزودن Watermark به فایل PDF تهیه شده
private static void addWatermark(string pdfFile, string watermarkedFile, string watermarkText) { FontFactory.Register("c:\\windows\\fonts\\tahoma.ttf"); var tahoma = FontFactory.GetFont("Tahoma", BaseFont.IDENTITY_H, 40, Font.NORMAL, BaseColor.BLACK); var reader = new PdfReader(pdfFile); using (var fileStream = new FileStream(watermarkedFile, FileMode.Create, FileAccess.Write, FileShare.None)) { using (var stamper = new PdfStamper(reader, fileStream)) { int pageCount = reader.NumberOfPages; for (int i = 1; i <= pageCount; i++) { var rect = reader.GetPageSize(i); var cb = stamper.GetUnderContent(i); var gState = new PdfGState(); gState.FillOpacity = 0.25f; cb.SetGState(gState); ColumnText.ShowTextAligned( canvas: cb, alignment: Element.ALIGN_CENTER, phrase: new Phrase(watermarkText, tahoma), x: rect.Width / 2, y: rect.Height / 2, rotation: 45f, runDirection: PdfWriter.RUN_DIRECTION_LTR, arabicOptions: 0); } } } }
در اینجا توسط PdfReader، فایل موجود گشوده میشود. به این ترتیب میتوان به تک تک صفحات این فایل دسترسی یافت. از PdfStamper برای نوشتن در این فایل باز شده استفاده میکنیم. متد stamper.GetUnderContent، لایه زیرین متن صفحات را در اختیار ما قرار میدهد. اگر علاقمند به نوشتن بر روی لایه رویی متون هستید از متد stamper.GetOverContent استفاده کنید. در اینجا از PdfGState برای مشخص سازی میزان شفافیت متن درحال نمایش، با مقدار دهی FillOpacity آن استفاده شده است. نهایتا از متد ColumnText.ShowTextAligned برای نمایش متن مورد نظر در مکان و زاویهای مشخص استفاده میکنیم. این متد با زبان فارسی سازگاری دارد و run direction آن قابل تنظیم است.
ج) آشنایی با ساختار سطح پایین Watermark اضافه شده به صفحات
تقریبا اکثر Watermarkهایی که بر روی صفحات PDF درج میشوند، نیمه شفاف هستند تا بتوان متن زیر آنها را مطالعه کرد. این شفافیت همانطور که ذکر شد توسط مقدار دهی شیء PdfGState حاصل میشود. اگر فایل PDF تولیدی در قسمت ب را توسط برنامه iText Rups باز کنیم، به شکل زیر خواهیم رسید:
هدف ما این است که به شیء PdfGState موجود در هر صفحه، دسترسی یافته و مقدار FillOpacity آنرا صفر کنیم. به این ترتیب این Watermark، کاملا شفاف یا نامرئی خواهد شد. در PDF نهایی، چیزی به نام شیء PdfGState وجود ندارد، بلکه با یک سری Dictionary و Array سر و کار داریم.
همانطور که در شکل فوق ملاحظه میکنید، برای رسیدن به Gstateها باید مراحل زیر طی شوند:
1- فایل PDF گشوده شده و سپس به هر صفحه دسترسی یافت.
2- نیاز است RESOURCES صفحه جاری استخراج شوند.
3- در این منابع، باید EXTGSTATE را که همان PdfGStateها هستند، بیابیم.
4- سپس مقدار ca این EXTGSTATE یافت شده را به صفر مقدار دهی کنیم.
private static void removeWatermark(string watermarkedFile, string unwatermarkedFile) { PdfReader.unethicalreading = true; PdfReader reader = new PdfReader(watermarkedFile); reader.RemoveUnusedObjects(); int pageCount = reader.NumberOfPages; for (int i = 1; i <= pageCount; i++) { var page = reader.GetPageN(i); PdfDictionary resources = page.GetAsDict(PdfName.RESOURCES); PdfDictionary extGStates = resources.GetAsDict(PdfName.EXTGSTATE); if (extGStates == null) continue; foreach (PdfName name in extGStates.Keys) { var obj = extGStates.Get(name); PdfDictionary extGStateObject = (PdfDictionary)PdfReader.GetPdfObject(obj); var stateNumber = extGStateObject.Get(PdfName.ca_); if (stateNumber == null) continue; var caNumber = (PdfNumber)PdfReader.GetPdfObject(stateNumber); if (caNumber.FloatValue != 1f) { extGStateObject.Remove(PdfName.ca_); //با تنظیم مقدار به صفر، نامرئی خواهد شد extGStateObject.Put(PdfName.ca_, new PdfNumber(0f)); } } } using (FileStream fs = new FileStream(unwatermarkedFile, FileMode.Create, FileAccess.Write, FileShare.None)) { using (PdfStamper stamper = new PdfStamper(reader, fs)) { stamper.SetFullCompression(); stamper.Close(); } } }
در این متد، watermarkedFile فایلی است که باید watermark آن نامرئی شود و unwatermarkedFile فایل تولیدی حاصل است.
که اینجا دکمهها از سمت راست به چپ، عملیات افزودن، عدم انتخاب، ویرایش و حذف را انجام میدهند. کدهای HTML این پنل را در ادامه مشاهده میکنید:
<div id="CrudPanel" class="row treeview-panel" > <div class="col-lg-7 pull-right"> <input type="text" id="txtLocationTitle" class="form-control" /> </div> <div class="col-lg-5 pull-left" style="text-align: left;"> <button data-toggle="tooltip" data-placement="left" title="افزودن" id="btnAddLocation" class="btn btn-sm btn-success"> <i class="fa fa-plus"></i> </button> <button data-toggle="tooltip" data-placement="left" title="عدم انتخاب" id="btnUnSelect" class="btn btn-sm btn-info"> <i class="fa fa-square-o"></i> </button> <button data-toggle="tooltip" data-placement="left" title="ویرایش" id="btnEditLocation" class="btn btn-sm btn-warning"> <i class="fa fa-pencil"></i> </button> <button data-toggle="tooltip" data-placement="left" title="حذف" id="btnDeleteLocation" class="btn btn-sm btn-danger"> <i class="fa fa-times"></i> </button> </div> </div>
و قطعه کد ذیل مربوط به پنل ویرایش است که در ابتدای کار کلاس hide به آن انتساب داده شده و پنهان میشود:
<div id="EditPanel" class="row edit hide treeview-panel"> <div class="col-lg-7 pull-right"> <input type="text" id="txtLocationEditTitle" class="form-control" /> </div> <div class="col-lg-5 pull-left" style="text-align: left"> <input type="button" value="ویرایش" id="btnEditPanelLocation" data-code="" data-parentId="" class="btn btn-sm btn-success" /> <input type="button" value="انصراف" id="btnCancle" class="btn btn-sm btn-info" /> </div> </div>
در آخر این تکه کد نیز مربوط به KendoUI TreeView است:
<div class="col-lg-6 k-rtl treeview-style"> @(Html.Kendo() .TreeView() .Name("treeview") .DataTextField("Title") .DragAndDrop(false) .DataSource(dataSource => dataSource .Model(model => model.Id("Id")) .Read(read => read.Action(MVC.Admin.Location.ActionNames.GetAllAssetGroupTree, MVC.Admin.Location.Name))) ) </div>
یک نکته
- کلاس k-rtl مربوط به خود treeview میباشد و با این کلاس، درخت ما راست به چپ میشود.
در ادامه cssهای مربوط به کلاسهای treeview-style ،hide و treeview-panel بررسی خواهند شد:
.treeview-style { min-height: 86px; max-height: 300px; overflow: scroll; overflow-x: hidden; position: relative; } .treeview-panel { background-color: #eee; padding: 25px 0 25px 0; } .hide { display: none; }
تا اینجای مقاله، کدهای Html و Css موجود را بررسی کردیم. حالا سراغ قسمت اصلی خواهیم رفت. یعنی عملیات CRUD.
لازم به ذکر است در ابتدای قسمت script باید این چند خط کد نوشته شود:
var treeview = null; $(window).load(function () { treeview = $("#treeview").data("kendoTreeView"); });
در اینجا بعد از بارگذاری کامل صفحه، درخت مورد نظر ما ساخته خواهد شد و میتوان به متغیر treeview در تمام قسمت script دسترسی داشت.
پیاده سازی عملیات افزودن:
$(document).on('click', '#btnAddLocation', function () { var title = $('#txtLocationTitle').val(); var selectedNodeId = null; var selectedNode = treeview.select(); if (selectedNode.length == 0) { selectedNode = null; } else { selectedNodeId = treeview.dataItem(selectedNode).id;// گرفتن آی دی گره انتخاب شده } $.ajax({ url: '@Url.Action(MVC.Admin.Location.CreateByAjax())', type: 'POST', data: { Title: title, ParentId: selectedNodeId }, success: function (data) { debugger; showMessage(data.message, data.notificationType); if (data.result) treeview.dataSource.read(); }, error: function () { showMessage('لطفا مجددا تلاش نمایید', 'warning'); } }); });
توضیحات: مقدار گره جدید را خوانده و در متغیر title قرار میدهیم. گره انتخاب شده را توسط این خط
var selectedNode = treeview.select();
می گیریم و سپس در ادامه بررسی خواهیم کرد تا اگر گرهای انتخاب نشده باشد، به کاربر پیغامی را نشان دهد؛ در غیر این صورت توسط ajax، مقادیر مورد نظر، به اکشن ما در LocationController ارسال میشوند:
[HttpPost] public virtual ActionResult CreateByAjax(AddLocationViewModel locationViewModel) { if (ModelState.IsNotValid()) return JsonResult(false, "عنوان نباید خالی و یا کمتر از دو کاراکتر باشد.", NotificationType.Error); var result = _locationService.Add(locationViewModel);//سرویس مورد نظر برای اضافه کردن به دیتابیس switch (result) { case AddStatus.AddSuccessful: _uow.SaveChanges(); return JsonResult(true, Messages.SaveSuccessfull, NotificationType.Success); case AddStatus.Faild: return JsonResult(false, Messages.SaveFailed, NotificationType.Error); case AddStatus.Exists: return JsonResult(false, Messages.DataExists, NotificationType.Warning); default: return JsonResult(false, Messages.SaveFailed, NotificationType.Error); } }
public virtual JsonResult JsonResult(bool result, string message, string notificationType) { return Json(new { result = result, message = message, notificationType = notificationType }, JsonRequestBehavior.AllowGet); }
اکشن JsonResult که مقادیر نتیجه، پیغام و نوع اطلاع رسانی را میگیرد و یک آبجکت از نوع json را به تابع success ایجکس، ارسال میکند.
public class AddLocationViewModel { [DisplayName("عنوان")] [Required(ErrorMessage ="لطفا عنوان گروه را وارد نمایید"),MinLength(2,ErrorMessage ="طول عنوان خیلی کوتاه میباشد ")] public string Title { get; set; } [DisplayName("گروه پدر")] public Guid? ParentId { get; set; } }
این کلاس viewModel ما میباشد.
public enum AddStatus { AddSuccessful, Faild, Exists }
و این مورد هم کلاس AddStatus از نوع enum.
public class Messages { #region Fields public const string SaveSuccessfull = "اطلاعات با موفقیت ذخیره شد"; public const string SaveFailed = "خطا در ثبت اطلاعات"; public const string DeleteMessage = "کابر گرامی ، آیا از حذف کردن این رکورد مطمئن هستید ؟"; public const string DeleteSuccessfull = "اطلاعات با موفقیت حذف شد"; public const string DeleteFailed = "خطا در حذف اطلاعات ، لطفا مجددا تلاش نمایید"; public const string DeleteHasInclude = "کاربر گرامی ، رکورد مورد نظر هم اکنون در بانک اطلاعاتی سیستم در حال استفاده توسط منابع دیگر میباشد"; public const string NotFoundData = "اطلاعات یافت نشد"; public const string NoAttachmentSelect = "تصویری انتخاب نشده است"; public const string DataExists = "اطلاعات وارد شده در بانک اطلاعاتی موجود میباشد"; public const string DeletedRowHasIncluded = "کاربر گرامی ، رکوردی که قصد حذف آن را دارید هم اکنون در بانک اطلاعاتی سیستم ، توسط سایر بخشها در حال استفاده میباشد"; #endregion }
و این موارد هم مقادیر ثابت فیلدهای مورد استفادهی ما در کلاس Message.
پیاده سازی عملیات حذف
به طور اختصار، عملیات حذف را توضیح میدهم تا به قسمت اصلی مقاله یعنی ویرایش بپردازیم:
$(document).on('click', '#btnDeleteLocation', function () { var selectedNode = treeview.select(); var currentNode = treeview.dataItem(selectedNode); if (selectedNode.length == 0) { showMessage('گزینه ای انتخاب نشده است. لطفا یک گزینه انتخاب نمایید', 'warning'); } else { var selectedNodeId = treeview.dataItem(selectedNode).id; if (currentNode.hasChildren) { var title = 'کاربر گرامی ، با حذف شدن این گره، تمام زیر شاخههای آن حذف میشود. آیا مطمئن هستید ؟ '; DeleteConfirm(selectedNodeId, '@Url.Action(MVC.Admin.Location.DeleteByAjax())', title); } else { $.ajax({ url: '@Url.Action(MVC.Admin.Location.DeleteByAjax())', type: 'POST', data: { id: selectedNodeId }, success: function (data) { debugger; showMessage(data.message, data.notificationType); if (data.result) treeview.remove(selectedNode); }, error: function () { showMessage('لطفا مجددا تلاش نمایید', 'warning'); } }); } } });
این مورد نیز همانند عملیات افزودن عمل میکند. یعنی ابتدا چک میکند که آیا گرهای انتخاب شده است یا خیر؟ و اگر گره انتخابی ما دارای فرزند باشد، به کاربر پیغامی را نشان میدهد و میگوید «گره مورد نظر، دارای فرزند است. آیا مایل به حذف تمام فرزندان آن هستید؟» مانند تصویر زیر:
در نهایت چه گره انتخابی دارای فرزند باشد و چه نباشد، به یک مسیر مشترک ارسال میشوند:
public virtual ActionResult DeleteByAjax(Guid id) { var result = _locationService.Delete(id); switch (result) { case DeleteStatus.Successfull: _uow.SaveChanges(); return DeleteJsonResult(true, Messages.DeleteSuccessfull, NotificationType.Success); case DeleteStatus.NotFound: return DeleteJsonResult(false, Messages.NotFoundData, NotificationType.Error); case DeleteStatus.Failed: return DeleteJsonResult(false, Messages.DeleteFailed, NotificationType.Error); case DeleteStatus.ThisRowHasIncluded: return DeleteJsonResult(false, Messages.DeletedRowHasIncluded, NotificationType.Warning); default: return DeleteJsonResult(false, Messages.DeleteFailed, NotificationType.Error); } }
در سرویس مورد نظر ما یعنی Delete، اگه گرهای دارای فرزند باشد، تمام فرزندان آن را حذف میکند. حتی فرزندان فرزندان آن را:
public DeleteStatus Delete(Guid id) { var model = GetAsModel(id); if (model == null) return DeleteStatus.NotFound; if (!CanDelete(model)) return DeleteStatus.ThisRowHasIncluded; _uow.MarkAsSoftDelete(model, _userManager.GetCurrentUserId()); if (model.Children.Any()) DeleteChildren(model); return DeleteStatus.Successfull; }
private void DeleteChildren(Location model) { foreach (var item in model.Children) { _uow.MarkAsSoftDelete(item, _userManager.GetCurrentUserId()); if (item.Children.Any()) DeleteChildren(item); } }
public class Location:BaseEntity,ISoftDelete { public string Title { get; set; } public Location Parent { get; set; } public Guid? ParentId { get; set; } public bool IsDeleted { get; set; } public virtual ICollection<Location> Children { get; set; } }
و این هم مدل Location که سمت سرور از مدل استفاده میکنیم.
پیاده سازی عملیات ویرایش
حالا به قسمت اصلی مقاله رسیدیم. در اینجا قرار است گرهای را انتخاب نماییم و با زدن دکمه ویرایش و باز شدن پنل آن، آن را ویرایش کنیم. با زدن دکمه ویرایش، کدهای زیر اجرا میشوند:
// Open Edit Panel $(document).on('click', '#btnEditLocation', function () { debugger; var selectedNode = treeview.select(); var currentNode = treeview.dataItem(selectedNode);// با استفاده از این خط، گره انتخاب شده جاری را میگیریم. if (selectedNode.length == 0) { //این شرط به ما میگوید اگر گره ای انتخاب نشده بود پیغامی به کاربر نمایش بده showMessage('گزینه ای انتخاب نشده است. لطفا یک گزینه انتخاب نمایید', 'warning'); } else { var selectedNodeCode = treeview.dataItem(selectedNode).Code; var selectedNodeTitle = treeview.dataItem(selectedNode).Title; var selectedNodeParentId = treeview.dataItem(selectedNode).ParentId; // آی دی یا کد، عنوان و آی دی پدر گره انتخاب شده را با استفاده از این سه خط در اختیار میگیریم $('#CrudPanel').toggleClass('hide'); //المنت کرادپنل که در حال حاضر کاربر آن را میبیند، با این خط کد، پنهان میشود $('#EditPanel').toggleClass('hide'); //المنت ادیت پنل که در حال حاضر از دید کاربر پنهان است، قابل نمایش میشود $("#txtLocationEditTitle").val(selectedNodeTitle); //عنوان گره ای که میخواهیم آن را ویرایش کنیم در تکست باکس مورد نظر قرار میگیرد $("#txtLocationEditTitle").focusTextToEnd(); // با استفاده از این پلاگین، کرسر ماوس در انتهای مقدار دیفالت تکست باکس قرار میگیرد $("#btnEditPanelLocation").attr('data-code', selectedNodeCode); $("#btnEditPanelLocation").attr('data-parentId', selectedNodeParentId == null ? '' : selectedNodeParentId); //مقادیر پرنت آی دی و کد را در دیتا اتریبیوتهای موجود در المنت خودمان قرار میدهیم // Disable clicking in treeview $("#treeview").children().bind('click', function () { return false; }); } }); (function ($) { $.fn.focusTextToEnd = function () { this.focus(); var $thisVal = this.val(); this.val('').val($thisVal); return this; } }(jQuery));
کد زیر باعث میشود تا زمانیکه پنل ویرایش باز است، کاربر نتواند هیچ کلیکی را در عناصر داخل درخت ما، داشته باشد.
$("#treeview").children().bind('click', function () { return false; });
و در نهایت با زدن دکمه ویرایش، پنل ویرایش ما به صورت زیر باز میشود:
همانطور که در تصویر بالا مشاهده میکنید، با انتخاب ساختمان مرکزی و زدن دکمه ویرایش، پنل CRUD ما پنهان و پنل ویرایش ظاهر میگردد. همچنین عنوان گره انتخابی به عنوان پیش فرض تکست باکس ما تنظیم میشود و کاربر نمیتواند گره دیگری را انتخاب کند؛ به شرط آنکه این پنل ویرایش بسته شود.
با تغییر عنوان تکست باکس و زدن دکمهی ویرایش، رویداد زیر رخ میدهد:
// Edit tree node $(document).on('click', '#btnEditPanelLocation', function () { debugger; var code = $("#btnEditPanelLocation").attr('data-code'); var parentId = $("#btnEditPanelLocation").attr('data-parentId'); var title = $("#txtLocationEditTitle").val().trim(); $.ajax({ url: '@Url.Action(MVC.Admin.Location.EditByAjax())', type: 'POST', data: { Code: code, Title: title, ParentId: parentId.length === 0 ? null : parentId }, success: function (data) { debugger; showMessage(data.message, data.notificationType); if (data.result) { treeview.dataSource.read(); CloseEditPanel(); } }, error: function () { showMessage('لطفا مجددا تلاش نمایید', 'warning'); } }); });
[HttpPost] public virtual ActionResult EditByAjax(EditLocationViewModel editLocationViewModel) { if (ModelState.IsNotValid()) return JsonResult(false,"عنوان نباید خالی و یا کمتر از دو کاراکتر باشد.", NotificationType.Error); var result = _locationService.Edit(editLocationViewModel); switch (result) { case EditStatus.Successful: _uow.SaveChanges(); return JsonResult(true, Messages.SaveSuccessfull, NotificationType.Success); case EditStatus.NotFound: return JsonResult(false, Messages.NotFoundData, NotificationType.Error); case EditStatus.Faild: return JsonResult(false, Messages.SaveFailed, NotificationType.Error); case EditStatus.Exists: return JsonResult(false, Messages.DataExists, NotificationType.Warning); default: return JsonResult(false, Messages.SaveFailed, NotificationType.Error); } }
تابع CloseEditPanel بعد از اتمام ویرایش هر گره و یا با زدن دکمه انصراف در شکل بالا، فراخوانی میشود که کد آن به شکل زیر است:
function CloseEditPanel() { $('#CrudPanel').toggleClass('hide'); //پنل کراد ما که در حال حاضر از دید کاربر پنهان است با این خط ظاهر میگردد $('#EditPanel').toggleClass('hide'); //پنل ویرایش ما که در حال حاضر کاربر آن را میبیند، پنهان میشود از دید کاربر $("#txtLocationEditTitle").val(''); //مقدار تکست باکس خالی میشود $("#btnEditPanelLocation").attr('data-code', ''); $("#btnEditPanelLocation").attr('data-parentId', ''); //دیتا اتریبیوتهای ما که مقادیر کد و آی دی والد در آن قرار گرفته نیز خالی میشود // Enable clicking in treeview $("#treeview").children().unbind('click').bind('click', function () { return true; }); //اگر یادتان باشد با یک خط کد به کاربر اجازه ندادیم که با باز شدن پنل ویرایش، گره دیگری را انتخاب نمایی. حالا این خط کد عکس کد قبلیست و به کاربر اجازه میدهد در المنت مورد نظر کلیک کند }
// Cancle edit Node tree $(document).on('click', '#btnCancle', function () { CloseEditPanel(); });
$(document).on('click', '#btnUnSelect', function () { //رویداد عدم انتخاب treeview.select(null); });
EF Code First #12
using System; using System.ComponentModel.DataAnnotations; namespace Test { [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] public class CompareAttribute : ValidationAttribute { public CompareAttribute(string originalProperty, string confirmProperty) { OriginalProperty = originalProperty; ConfirmProperty = confirmProperty; } public string ConfirmProperty { get; private set; } public string OriginalProperty { get; private set; } protected override ValidationResult IsValid(object value, ValidationContext ctx) { if (value == null) return new ValidationResult("لطفا فیلدها را تکمیل نمائید"); var confirmProperty = ctx.ObjectType.GetProperty(ConfirmProperty); if (confirmProperty == null) throw new InvalidOperationException(string.Format("لطفا فیلد {0} را تعریف نمائید", ConfirmProperty)); var confirmValue = confirmProperty.GetValue(ctx.ObjectInstance, null) as string; if (string.IsNullOrWhiteSpace(confirmValue)) return new ValidationResult(string.Format("لطفا فیلد {0} را تکمیل نمائید", ConfirmProperty)); var originalProperty = ctx.ObjectType.GetProperty(OriginalProperty); if (originalProperty == null) throw new InvalidOperationException(string.Format("لطفا فیلد {0} را تعریف نمائید", OriginalProperty)); var originalValue = originalProperty.GetValue(ctx.ObjectInstance, null) as string; if (string.IsNullOrWhiteSpace(originalValue)) return new ValidationResult(string.Format("لطفا فیلد {0} را تکمیل نمائید", OriginalProperty)); return originalValue == confirmValue ? ValidationResult.Success : new ValidationResult("مقادیر وارد شده یکسان نیستند"); } } }
<button data-perm="true" data-controller="c" data-action="r" data-method="post" data-type="disable">Test 1</button> <button data-perm="true" data-controller="c" data-action="t" data-method="post">Test 2</button> <button data-perm="false" data-controller="c" data-action="r" data-method="post">Test 3</button> <a data-perm="true" data-controller="c" data-action="m" data-method="post">Test 4</a> <a data-perm="false" data-controller="c" data-action="m" data-method="post">Test 5</a> <a data-perm="true" data-controller="c" data-action="t" data-method="post">Test 6</a>
ویژگی | توضیحات |
perm | آیا نیاز به اعتبارسنجی دارد یا خیر؟ در صورتی که المانی مقدار Perm آن با مقدار true پر گردد، اعتبارسنجی روی آن اعمال خواهد شد. |
controller | نام کنترلری که به آن دسترسی دارد. |
action | نام اکشنی که به آن در کنترلر ذکر شده دسترسی دارد. |
method | در صورتیکه دسترسی get و post و ... هر یک متفاوت باشد. |
type | نحوه برخورد با المان غیرمجاز. در صورتیکه با disable مقداردهی شود، المان غیرفعال و در غیر اینصورت، از روی صفحه حذف میشود. |
سپس یک کلاس جدید ساخته و با ارث بری از ActionFilterAttribute، کار ساخت فیلتر را آغاز میکنیم:
public class AuthorizePage: ActionFilterAttribute { private HtmlTextWriter _htmlTextWriter; private StringWriter _stringWriter; private StringBuilder _stringBuilder; private HttpWriter _output; IAuthorization _auth; public override void OnActionExecuting(ActionExecutingContext filterContext) { _stringBuilder = new StringBuilder(); _stringWriter = new StringWriter(_stringBuilder); _htmlTextWriter = new HtmlTextWriter(_stringWriter); _output = (HttpWriter) filterContext.RequestContext.HttpContext.Response.Output; filterContext.RequestContext.HttpContext.Response.Output = _htmlTextWriter; _auth = new Auth(); } public override void OnResultExecuted(ResultExecutedContext filterContext) { var response = _stringBuilder.ToString(); response = AuthorizeTags(response); _output.Write(response); } public string AuthorizeTags(string response) { var doc = GetHtmlDocument(response); var nodes=doc.DocumentNode.SelectNodes("//*[@data-perm]"); if (nodes == null) return response; foreach(var node in nodes) { var dataPermission = node.Attributes["data-perm"]; if(!dataPermission.Value.TryBooleanParse()) { continue; } var controller = node.Attributes["data-controller"].Value; var action = node.Attributes["data-action"].Value; var method = node.Attributes["data-method"].Value; var access=_auth.Authorize(HttpContext.Current.User.Identity.Name , controller, action, method); if (access) continue; var removeElm = true; var type = node.Attributes["data-type"]?.Value; if (type!=null && type.ToLower()== "disable") { removeElm = false; } if(removeElm) { node.Remove(); continue; } node.Attributes.Add("disabled", "true"); } return doc.DocumentNode.OuterHtml; } private HtmlDocument GetHtmlDocument(string htmlContent) { var doc = new HtmlDocument { OptionOutputAsXml = true, OptionDefaultStreamEncoding = Encoding.UTF8 }; doc.LoadHtml(htmlContent); return doc; } }
public interface IAuthorization { bool Authorize(string userId, string controller, string action, string method); }
ادامه کد در متد OnResultExecuted قرار دارد و متد اصلی کار ما میباشد. این متد بعد از صدور خروجی از اکشن، صدا زده شده اجرا میشود و شامل خروجی اکشن میباشد. خروجی اکشن را به متدی به نام AuthorizeResponse داده و با استفاده از بسته htmlagilitypack که یک HTML Parser میباشد، کدهای HTML را تحلیل میکنیم. قاعده فیلترسازی المانها در این کتابخانه بر اساس قواعد تعریف شده در XPath میباشد. بر اساس این قاعده ما گفتیم هر نوع تگی که دارای ویژگی data-perm میباشد، باید به عنوان گرههای فیلتر شده برگشت داده شود. سپس مقادیر نام کنترلر و اکشن و ... از المان دریافت شده و با استفاده از اینترفیسی که ما اینجا تعریف کردهایم، بررسی میکنیم که آیا این کاربر به این موارد دسترسی دارد یا خیر. در صورتیکه پاسخ برگشتی، از عدم اعتبار کاربر بگوید، گره مورد نظر حذف و یا در صورتیکه ویژگی data-type وجود داشته و مقدارش برابر disable باشد، آن المان غیرفعال خواهد شد. در نهایت کد تولیدی سند را به رشته تبدیل کرده و جایگزین خروجی فعلی میکنیم.
protected void Application_Start() { GlobalFilters.Filters.Add(new AuthorizePage()); }
try { // some code to check ... } catch (InvalidOperationException ex) { // do your handling for invalid operation ... } catch (IOException ex) { // do your handling for IO error ... }
catch (SomeException ex) when (someConditionIsMet) { // Your handler logic }
علاوه بر این در اینجا میتوان چندین بدنهی catch مجزا را به ازای یک نوع استثنای مشخص به همراه whenهای متفاوتی نیز تعریف کرد و از این لحاظ محدودیتی وجود ندارد. فقط در این حالت باید به تقدم و تاخرها دقت داشت. برای نمونه در مثال ذیل، ترکیب چندین شرط متفاوت را بر اساس یک نوع مشخص استثناء، مشاهده میکنید. در اینجا اگر برای نمونه شرط ذکر شدهی در قسمت when مربوط به catch اولی صادق باشد، همینجا کار خاتمه مییابد و سایر catchها بررسی نمیشوند:
catch (SomeDependencyException ex) when (condition1 && condition2) { } catch (SomeDependencyException ex) when (condition1) { } catch (SomeDependencyException ex) { }
catch (SomeDependencyException ex) { } catch (SomeDependencyException ex) when (condition1 && condition2) { } catch (SomeDependencyException ex) when (condition1) { }
لاگ کردن استثناءها در C# 6 بدون مدیریت آنها
به مثال ذیل دقت کنید:
try { DoSomethingThatMightFail(s); } catch (Exception ex) when (Log(ex, "An error occurred")) { // this catch block will never be reached } ... static bool Log(Exception ex, string message, params object[] args) { Debug.Print(message, args); return false; }
تفاوت C# 6 - Exception Filtering با if/else نوشتن در بدنهی catch چیست؟
تا اینجا به این نتیجه رسیدیم که کدهای if/else دار داخل بدنهی catch کدهای قدیمی را مانند کد ذیل:
try { var request = WebRequest.Create("http://www.google.coom/"); var response = request.GetResponse(); } catch (WebException we) { if (we.Status == WebExceptionStatus.NameResolutionFailure) { //handle DNS error return; } if (we.Status == WebExceptionStatus.ConnectFailure) { //handle connection error return; } throw; }
try { var request = WebRequest.Create("http://www.google.coom/"); var response = request.GetResponse(); } catch (WebException we) when (we.Status == WebExceptionStatus.NameResolutionFailure) { //Handle NameResolutionFailure Separately } catch (WebException we) when (we.Status == WebExceptionStatus.ConnectFailure) { //Handle ConnectFailure Separately }
به این معنا که exception filters سبب Stack unwinding نمیشوند. با هربار ورود به بدنهی catch، اصطلاحا عملیات Stack unwinding صورت میگیرد. یعنی اطلاعات stack مربوط به متدهای پیش از فراخوانی متدی که سبب بروز استثناء شدهاست، از بین میروند. به این ترتیب تشخیص مقادیر متغیرهایی که سبب بروز این استثناء شدهاند نیز میسر نخواهد بود و دیگر نمیتوان با قطعیت عنوان کرد که چه مقادیری و چه اطلاعاتی سبب بروز این مشکل شدهاند. اما در حالت exception filters در قسمت when آن هنوز وارد بدنهی catch نشدهایم. در اینجا دسترسی کاملی به اطلاعات stack جاری و مقادیر متغیرهای محلی که سبب بروز این استثناء شدهاند وجود دارد.
تفاوت stack با stack trace چیست؟ stack قطعهای از حافظهاست که اطلاعاتی در مورد نحوهی فراخوانی متدها، آدرس بازگشتی آنها، آرگومان و همچنین متغیرهای محلی آنها را دارا است. اما stack trace تنها یک رشتهاست و بیانگر نام متدهایی است که هم اکنون بر روی stack قرار دارند. احتمالا پیشتر خوانده بودید که فراخوانی throw داخل بدنهی catch سبب حفظ stack trace میشود و اگر throw ex صورت گیرد، این اطلاعات از دست میروند و بازنویسی میشوند. اما در C# 6 امکان حفظ کل اطلاعات stack به همراه exception filtering میسر شدهاست.
inject$ در AngularJs
تعریف کلی آن به صورت زیر است:
$watch(watchExpression, listener, objectEquality)
»Listener : با تغییر در مقدار watchExpression اگر مقدار قبلی این عبارت با مقدار فعلی آن برابر نباشد این تابع فراخوانی میشود.
» objectEquality : به صورت پیش فرض Angular مقادیر مورد نظر برای تغییرات را فقط از نظر Reference Equal بودن چک میکند. اگر بخواهیم که Angular به صورت عمقی و درختی مقادیر ابجکتها را بررسی کند مقدار این پارامتر باید true شود.
app.controller('MainCtrl', function($scope) { $scope.foo = "Foo"; $scope.world = "World"; });
Hello, {{ World }}
و به عنوان نکته آخر، در Angular نسخه 1.1.4 تابعی به نام watchCollection اضافه شده است که برای ردیابی تغییرات یک مجموعه مورد استفاده قرار میگیرد.
قسمت قبل به IIS7 اختصاص داشت که شاید برای خیلیها کاربرد نداشته باشد خصوصا اینکه برنامه نویسها ترجیح میدهند به روشهایی روی بیاورند که کمتر نیاز به دخالت مدیر سرور داشته باشد؛ یا زمانیکه سایت شما بر روی یک هاست اینترنتی قرار گرفته است عملا شاید دسترسی خاصی به تنظیمات IIS نداشته باشید (مگر اینکه یک هاست اختصاصی را تهیه کنید).
برای IIS6 و ماقبل از آن و حتی بعد از آن!، حداقل دو روش برای کش کردن اطلاعات استاتیک وجود دارد:
الف) استفاده از web resources معرفی شده در ASP.Net 2.0 به بعد
در مورد نحوهی تعریف و بکارگیری web resources میتوان به مقاله "تبدیل پلاگینهای jQuery به کنترلهای ASP.Net" رجوع کرد.
همانطور که در شکل فوق نیز ملاحظه میکنید، هدر مربوط به مدت زمان منقضی شدن کش سمت کلاینت یک web resource توسط موتور ASP.Net به صورت خودکار به سال 2010 تنظیم شده است و این مقدار خالی نیست.
ب) افزودن این هدر به صورت دستی
برای این منظور باید در نحوهی ارائه فایلهای استاتیک دخالت کنیم و اینکار را با استفاده از یک generic handler میتوان انجام داد.
کد این generic handler میتواند به صورت زیر باشد:
using System;
using System.IO;
using System.Web;
using System.Web.Services;
using System.Reflection;
namespace test1
{
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class cache : IHttpHandler
{
private static void cacheIt(TimeSpan duration)
{
HttpCachePolicy cache = HttpContext.Current.Response.Cache;
FieldInfo maxAgeField = cache.GetType().GetField("_maxAge", BindingFlags.Instance | BindingFlags.NonPublic);
maxAgeField.SetValue(cache, duration);
cache.SetCacheability(HttpCacheability.Public);
cache.SetExpires(DateTime.Now.Add(duration));
cache.SetMaxAge(duration);
cache.AppendCacheExtension("must-revalidate, proxy-revalidate");
}
public void ProcessRequest(HttpContext context)
{
string file = context.Request.QueryString["file"];
if (string.IsNullOrEmpty(file))
{
return;
}
string contetType = context.Request.QueryString["contetType"];
if (string.IsNullOrEmpty(contetType))
{
return;
}
context.Response.Write(File.ReadAllText(context.Server.MapPath(file)));
//Set the content type
context.Response.ContentType = contetType;
// Cache the resource for 30 Days
cacheIt(TimeSpan.FromDays(30));
}
public bool IsReusable
{
get
{
return false;
}
}
}
}
این generic handler دو کوئری استرینگ را دریافت میکند؛ file جهت دریافت نام فایل و contetType جهت مشخص سازی نوع محتوایی که باید سرو شود؛ مثلا جاوا اسکریپت یا استایل شیت و امثال آن. سپس زمانیکه محتوا را Response.Write میکند، هدر مربوط به کش شدن آنرا نیز به 30 روز تنظیم مینماید.
تابع مربوط به کش کردن اطلاعات از مقاله ASP.NET Ajax Under-the-hood Secrets استخراج شد.
روش استفاده در مورد فایلهای CSS
بجای تعریف یک فایل CSS در صفحه، به صورت استاندارد، اکنون تعریف متداول را به صورت زیر اصلاح کنید:
<link type="text/css" href="cache.ashx?v=1&file=site.css&contetType=text/css" rel="Stylesheet" />
روش استفاده در مورد فایلهای JS
<script type="text/javascript" src="cache.ashx?v=1&file=js/jquery-1.3.2.min.js&contetType=application/x-javascript"></script>
CoffeeScript #3
Syntax
Object & Array
برای تعریف Object در CoffeeScript میتوان دقیقا مانند جاوااسکریپت عمل کرد؛ با یک جفت براکت و ساختار کلید / مقدار. البته همانند تابع، نوشتن براکت اختیاری است. در واقع، شما میتوانید از تورفتگی و هر کلید/مقدار، در خط جدید به جای کاما استفاده کنید:
object1 = {one: 1, two: 2} # Without braces object2 = one: 1, two: 2 # Using new lines instead of commas object3 = one: 1 two: 2 User.create(name: "Vahid Mohammad Taheri")
array1 = [1, 2, 3] array2 = [ 1 2 3 ] array3 = [1,2,3,]
Flow control
طبق قاعدهای که برای نوشتن پرانتز در قبل گفته شد (پرانتز اختیاری است)، در دستورات if و else نیز چنین است:
if true == true "We're ok" if true != true then "Vahid" # برابر است با: # (1 > 0) ? "Yes" : "No!" if 1 > 0 then "Yes" else "No!"
CoffeeScript از اپراتورهای شرطی (:?) پشتیبانی نمی کند و به جای آن از if / else استفاده کنید.
CoffeeScript نیز همانند Ruby امکان نوشتن بدنه شرط را به صورت پسوندی ایجاد کرده است.
alert "It's cold!" if 1 < 5
if not true then "Vahid"
unless true "Vahid"
if true is 1 "OK!"
if true isnt true alert "OK!"
الحاق رشته ها
CoffeeScript امکان الحاق رشتهها را با استفاده از روش الحاق رشتهها در Ruby فراهم کرده است. برای انجام این عمل از {}# در داخل " " استفاده کنید که در داخل براکت میتوانید از دستورات مختلف استفاده کنید. برای مثال:favorite_color = "Blue. No, yel..." question = "Sam: What... is your favorite color? Ben: #{favorite_color} Sam: Wrong! "
var favorite_color, question; favorite_color = "Blue. No, yel..."; question = "Sam: What... is your favorite color? Ben: " + favorite_color + " Sam: Wrong!";
<input type="text" ng-model="newItemTitle"> <button type="button" ng-click="add()">افزودن</button> <ul> <li ng-repeat="item in items">{{item.title}}</li> </ul> <div ng-show="showMSG()">شما بیش از ۱۰ استان ثبت کرده اید</div>
$scope.newItemTitle=''; $scope.add=function(){ $scope.items.push({ title:$scope.newItemTitle }); } $scope.items=[ {title:'اردبیل'}, {title:'تهران'}, {title:'اصفهان'}, {title:'شیراز'}, {title:'مشهد'}, ]; $scope.showMSG=function(){ return $scope.items.length>10; }
ng-show="showMSG()" ng-if="showMSG()" ng-hide="showMSG()==false" ng-class="{'red',getResult()}" ng-style="{'width':getWidth()}"
<div ng-show="items.length>10">شما بیش از ۱۰ استان ثبت کرده اید</div>
$scope.maxItem=false'; $scope.add=function(){ $scope.items.push({ title:$scope.newItemTitle }); $scope.maxItem=$scope.items.length>10; }
<div ng-show="maxItem">شما بیش از ۱۰ استان ثبت کرده اید</div>