در ادامهی مطلب قبلی، نکاتی دیگر را جهت افزایش کارآیی سیستمهای مبتنی بر EF اشاره خواهیم کرد:
عدم استفاده از کوئریهای کلی
فرض کنید در یک فرم جستجو، 4 تکست باکس FirstName, LastName, City و PostalZipCode برای عملیات جستجو در نظر گرفته شده است و کاربر میتواند بر اساس آنها جستجو را انجام دهد.
var searchModel = new Pupil { FirstName = "Ben", LastName = null, City = null, PostalZipCode = null }; List<Pupil> pupils = db.Pupils.Where(p => (searchModel.FirstName == null || p.FirstName == searchModel.FirstName) && (searchModel.LastName == null || p.LastName == searchModel.LastName) && (searchModel.City == null || p.LastName == searchModel.City) && (searchModel.PostalZipCode == null || p.PostalZipCode == searchModel.PostalZipCode)) .Take(100) .ToList()
USE [EFSchoolSystem] DECLARE @p__linq__0 NVarChar(4000) SET @p__linq__0 = 'Ben' DECLARE @p__linq__1 NVarChar(4000) SET @p__linq__1 = 'Ben' DECLARE @p__linq__2 NVarChar(4000) SET @p__linq__2 = '' DECLARE @p__linq__3 NVarChar(4000) SET @p__linq__3 = '' DECLARE @p__linq__4 NVarChar(4000) SET @p__linq__4 = '' DECLARE @p__linq__5 NVarChar(4000) SET @p__linq__5 = '' DECLARE @p__linq__6 NVarChar(4000) SET @p__linq__6 = '' DECLARE @p__linq__7 NVarChar(4000) SET @p__linq__7 = '' -- Executed query SELECT TOP (100) [Extent1].[PupilId] AS [PupilId] , [Extent1].[FirstName] AS [FirstName] , [Extent1].[LastName] AS [LastName] , [Extent1].[Address1] AS [Address1] , [Extent1].[Adderss2] AS [Adderss2] , [Extent1].[PostalZipCode] AS [PostalZipCode] , [Extent1].[City] AS [City] , [Extent1].[PhoneNumber] AS [PhoneNumber] , [Extent1].[SchoolId] AS [SchoolId] , [Extent1].[Picture] AS [Picture] FROM [dbo].[Pupils] AS [Extent1] WHERE (@p__linq__0 IS NULL OR [Extent1].[FirstName] = @p__linq__1) AND (@p__linq__2 IS NULL OR [Extent1].[LastName] = @p__linq__3) AND (@p__linq__4 IS NULL OR [Extent1].[LastName] = @p__linq__5) AND (@p__linq__6 IS NULL OR [Extent1].[PostalZipCode] = @p__linq__7)
مشکلی که در این دسته از کوئریهای عمومی ایجاد میگردد آن است که ممکن است پلنی که برای یک گروه از پارامترهای ورودی مناسب باشد (جستجو بر اساس نام) برای سایر پارامترهای ورودی نامناسب باشد. تصور کنید تمام دانش آموزان، در شهر نیویورک یا بوستون زندگی میکنند. بنابراین این ستون از تنوع انتخاب کمتری برخوردار است در مقایسه با ستون نام خانوادگی و فرض کنید پلن، براساس پارامتر شهر ایجاد شده است. بنابراین ایجاد این پلن برای سایر پارامترها از کارآیی کافی برخوردار نیست. این مشکل با نام Bad Parameter Sniffing شناخته میشود و دربارهی Parameter Sniffing در اینجا به تفصیل اشاره شده است.
این مشکل زمانی بیشتر مشکل ساز خواهد شد که 99 درصد دانش آموزان در شهر نیویورک و فقط 1 درصد آنها در شهر بوستون زندگی میکنند و پلن ایجاد شده بر اساس پارامتر شهر بوستون باشد.
راه حل اول:
برای حل این مشکل تنها یک راه حل خاص وجود ندارد و باید براساس شرایط برنامه، کوئری از حالت عمومی خارج گردد؛ مانند زیر:
if (searchModel.City == null) { pupils = db.Pupils.Where(p => (searchModel.FirstName == null || p.FirstName == searchModel.FirstName) && (searchModel.LastName == null || p.LastName == searchModel.LastName) && (searchModel.PostalZipCode == null || p.PostalZipCode == searchModel.PostalZipCode)) .Take(100) .ToList(); } else { pupils = db.Pupils.Where(p => (searchModel.FirstName == null || p.FirstName == searchModel.FirstName) && (searchModel.LastName == null || p.LastName == searchModel.LastName) && (searchModel.City == null || p.LastName == searchModel.City) && (searchModel.PostalZipCode == null || p.PostalZipCode == searchModel.PostalZipCode)) .Take(100) .ToList(); }
راه حل دوم:
کامپایل مجدد پلن در اجرای هر کوئری، اما این راه حل سرباری را تحمیل میکند. بدین منظور مترجم زیر را ایجاد کنید:
public class RecompileDbCommandInterceptor : IDbCommandInterceptor { public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext) { if(!command.CommandText.EndsWith(" option(recompile)")) { command.CommandText += " option(recompile)"; } } //and implement other interface members }
var interceptor = new RecompileDbCommandInterceptor(); DbInterception.Add(interceptor); var pupils = db.Pupils.Where(p => p.City = city).ToList(); DbInterception.Remove(interceptor);
راه حل سوم:
استفاده از اجرای به تعویق افتاده به شکل زیر است:
var result = db.Pupils.AsQueryable(); if(searchModel.FirstName != null ) result = result.Where(p => p.FirstName == searchModel.FirstName); if(searchModel.LastName != null ) result = result.Where(p => p.LastName == searchModel.LastName); if(searchModel.PostalZipCode != null ) result = result.Where(p => p.PostalZipCode == searchModel.PostalZipCode); if(searchModel.City != null ) result = result.Where(p => p.City == searchModel.City);
افزونگی کشِ پلن
استفادهی مجدد از پلن بدلیل عدم ایجاد مجدد آن در زمان اجرای هر کوئری، بسیار خوب است. برای استفادهی مجدد از پلن، باید دستورات ارسالی یکسان باشند؛ مانند کوئریهای پارامتریک. در EF هنگامیکه از متغیرها استفاده کنید، کوئریها پارامتریک تولید میکند؛ اما یک استثناء وجود دارد: ()Skip و ()Take
2 متد فوق بیشتر جهت صفحه بندی استفاده میشوند:
var schools = db.Schools .OrderBy(s => s.PostalZipCode) .Skip(model.Page * model.ResultsPerPage) .Take(model.ResultsPerPage) .ToList();
حال اگر قصد دریافت اطلاعات صفحهی 500 را داشته باشید، مقادیر کوئری بعدی بترتیب 100 و 50000 خواهد بود و بجای مقادیر تصویر بالا 100 و 50000 قرار داده میشوند و کوئری متفاوتی با پلن متفاوتی ایجاد خواهد شد و اس کیو ال پلن کوئری قبلی را مورد استفاده قرار نخواهد داد و با اجرای کوئری دوم، پلن متفاوتی ایجاد خواهد کرد که این باعث ایجاد افزونگی پلنها خواهد شد و همانگونه که قبلا اشاره شد ایجاد پلن جدید هزینه بر است.
نکته: جهت مشاهده پلنهای کش شده در اس کیو ال، دستور زیر اجرا کنید:
SELECT text, query_plan FROM sys.dm_exec_cached_plans CROSS APPLY sys.dm_exec_query_plan(plan_handle) CROSS APPLY sys.dm_exec_sql_text(plan_handle)
- صدمه به کارآیی؛ هربار EF یک کوئری و اس کیو ال یک پلن جدید را ایجاد میکنند.
- افزایش اشغال حافظه؛ کش شدن کوئریها توسط EF سمت کلاینت و کش شدن پلنها در اس کیو ال سرور (کش بی رویهی پلنها باعث حذف سایر پلنهای مورد استفاده بدلیل محدودیت حافظه میشود که امکان بروز اختلال در کارآیی را بههمراه خواهد داشت.)
علت بروز مشکل:
هنگامیکه یک مقدار int، به متدهای ()Skip و ()Take ارسال میشود، EF نمیتواند تشخیص دهد این مقدار ارسالی ثابت (absolute) مانند (100)Take است یا توسط یک متغیر مانند (متغیر)Take تولید شده است. به همین خاطر EF مقدار ارسال شده را پارامتریک در نظر نمیگیرد.
راه حل:
در EF6 پیاده سازی دیگری برای متدهای ()Skip و ()Take ارائه شده است که برای حل مشکل فوق میتوان به کار گرفت، این پیاده سازی امکان دریافت lambada بجای int را دارد که باعث ایجاد کوئریهای پارامتریک خواهد شد.
int resultsToSkip = model.Page * model.ResultsPerPage; var schools = db.Schools .OrderBy(s => s.PostalZipCode) .Skip(() => resultsToSkip) //must pre-calculate this value .Take(() => model.ResultsPerPage) .ToList();
همانطور که مشاهده میکنید این بار EF کوئری پارامتریک ایجاد و ارسال کرده است.
برای شروع کار با Backload ابتدا به قسمت Nutget میرویم که در مسیر زیر است :
Backload. A Proffesional Full Featured Asp.Net FileUpload Controller *
پس از نصب دو مورد بالا ، موارد زیر را که در دو عکس پایین میبینید، به پروژهی شما اضافه خواهند شد:
*نکاتی که باید بدانید:
عکس هایی که شما آپلود میکنید در پوشهی Files موجود در ریشهی پروژهی شما قرار میگیرند این پوشه بوسیلهی خود ابزار Backload ساخته میشود.
چنانچه بخواهید در پوشهی Files پوشهای ایجاد کنید، ابتدا View آنرا باز کنید. این View در پروژه، در مسیر Views/ BackloadDemo/Index قرار دارد .
در داخل تگ form یک Hidden Field با نام objectContext اضافه میکنید. Value ایی که شما به این Hidden Field نسبت میدهید، نام پوشهی شما در پوشهی Files خواهد بود. مانند تصویر زیر: در اینجا پوشهای با نام 2014-02 در پوشهی Files وقتی که فایلی را باFile Upload آپلود میکنیم، ایجاد میشود.
چنانچه بخواهید در پوشهای که خودتان ایجاد کردید (که در این مثال 2014-02 میباشد) پوشههای متعدد دیگری هم داشته باشید Hidden Field ای با نام uploadContext ایجاد میکنید؛ مانند تصویر زیر :
اکنون اگر فایل جدیدی را آپلود کنید در مسیر
ذخیره میشود . یعنی بین نام هر پوشه از سمی کولن ; در Value استفاده میکنید.
تا اینجا ما میتوانیم بوسیلهی ابزار Backload عکسها را آپلود ، حذف و مسیر آپلود عکسها را تغییر دهیم.اگر توجه کرده باشید دفعات بعد که پروژه را اجرا میکنید عکسهای موجود در پوشه، در گرید ویو File Upload به شما نمایش داده خواهد شد. حال اگر بخوایم عکسهای موجود در پوشهی دیگری را نمایش دهیم باید چه کار کنیم؟!
گاهی اوقات نیاز داریم که محتویات پوشهای خاص را در گرید ویو File Upload مان نمایش دهیم. برای این کار شما باید کنترلر FileUploadController که در فایل ضمیمه در آموزش هست را در پوشهی Controller پروژهی خود کپی کنید .اگر شما این کنترلر را باز کنید مواردی مانند کلمه کلیدی Task و async را مشاهده خواهید کرد. این موارد در .Net Framework 4 شناسایی نمیشود. برای همین در ابتدای آموزش تاکید کردم که .Net Framework 4.5 و یا بالاتر را برای پروژهی خود انتخاب کنید .
در تابع Handler_GetFilesRequestStarted در این کنترلر باید مشخص کنید که فایلهای موجود در کدام مسیر را برای شما نمایش دهد؛ آن هم با استفاده از دستور زیر :
اکنون قبل از آنکه پروژه را اجرا کنید فایل Backload.Demo.js که در مسیر Scripts/Fileupload هست را باز کنید و url موجود در آنرا مانند عکس زیر تغیییر دهید :
حالا پروژه را اجرا کنید. خواهید دید تمامی فایلهای موجود در مسیری را که شما مشخص کردهاید، برایتان نمایش خواهد داد.
چنانچه بخواهید تعداد مثلا 5 فایل برای شما در گرید ویو مربوط به FileUpload نمایش داده شود، به تابع handler_GetFilesRequestFinished میروید. متغیر limit مشخص میکند که 5 فایل نمایش داده شود. میتوانید این عدد را به دلخواه خود تغییر دهید.
نکتهی بسیار مهم دیگری که باید به آن توجه شود مربوط به Hidden Field نام پوشهها میباشد. بار دیگر پروژه را اجرا کنید. با استفاده از ابزاری مثل FireBug کدهای html صفحهی خود را ببینید. Hidden Field ایی با نام objectContext را پیدا کنید و Value آنرا به test تغییر دهید. فایلی را آپلود کنید حالا به پوشهی Files موجود در ریشهی پروژه بروید. میبینید که پوشهای با نام testایجاد شده و فایلی هم که آپلود کردید در آن قرار دارد که این یک اشکال است. برای اینکه جلوی این گونه کارها را بگیریم به تابع handler_StoreFileRequestStartedAsync میرویم و کد زیر را مینویسیم :
دستور e.Context.PipelineControl.ExecutePipeline = false; باعث میشود که اجرای تابع متوقف شود.
فایل ضمیمه :FileUploadController-462d551688cf48c68cb55343ac5464f3.zip
برای مشاهده مثالهای دیگری دربارهی Backload به این لینک بروید.
موفق باشید.
این نوشتار اولین آموزش من در این سایت میباشد و جا دارد از دوست خوبم "محبوبه قربانی" که باعث شد من با MVC آشنا شوم تشکر کنم.
بر اساس جستجوهایی که انجام دادهام، CHM پشتیبانی کاملی را از یونیکد انجام نمیدهد (مشکل جستجو و همچنین ایندکس کردن).
اما با ترفندی میتوان این مساله را حل کرد و آن هم تبدیل encoding فایلها به عربی است (windows-1256). در این حالت هم جستجو کار میکند و هم عنوان صفحات هنگام جستجو در لیست موارد یاد شده درست نمایش داده میشود و صفحه add to favorites نیز مشکلی در نمایش عنوانهای صفحهها نخواهد داشت. روش کار به شرح زیر است:
الف) encoding تمام فایلهای html خود را به صورت زیر تغییر دهید (از utf-8 به windows-1256):
<meta content="text/html; charset=Windows-1256" http-equiv="Content-Type">
using System.IO;
using System.Text;
public static void SaveAs1256(string fileName)
{
string content = File.ReadAllText(fileName);
File.WriteAllText(fileName, content, Encoding.GetEncoding("windows-1256"));
}
ج) اصلاح فایل hhp پروژه خود
فایل hhp مربوط به html help work shop را باز کنید. (همان فایل پروژه ساخت راهنما)
اگر مثال قبل را دنبال کرده باشید، محتوای فایل آن چیزی شبیه به خطوط زیر خواهد بود:
[OPTIONS]
Compatibility=1.1 or later
Compiled file=test.chm
Contents file=Table of Contents.hhc
Default Window=win1
Default topic=page1.html
Display compile progress=No
Full-text search=Yes
Index file=Index.hhk
Language=0x429 Farsi
Title=راهنمای یک
[WINDOWS]
win1=,"Table of Contents.hhc","Index.hhk","page1.html","page1.html",,,,,0x3420,,0x304e,,,,,,2,,0
[FILES]
page1.html
page2.html
[INFOTYPES]
به قسمت options چند سطر زیر را اضافه کنید: (زبان فارسی و فونت تاهومای عربی)
Default Font=Tahoma,8,178
Language=0x429 Farsi
محض نمونه، کل وبلاگ جاری را به یک فایل chm تبدیل کردهام که آنرا از آدرس زیر میتوانید دریافت نمائید:
دریافت فایل
برای آزمایش، یک عبارت فارسی را در آن جستجو نمائید.
پ.ن.
این راه حلی است که به نظر من رسیده و جواب داده. اگر شما با encoding های دیگر هم جواب گرفتهاید (مشکل جستجوی فارسی حل شده) لطفا پیغام بگذارید. با تشکر.
TempData["PaymentMethodsWithshippingMethods"]= await _paymentMethodService.FindAllWithShippingMethodsAsync();
var paymentMethodsWithshippingMethods = TempData .Where(x => x.Key.Contains("PaymentMethodsWithshippingMethods")) .Select(x => new { value = (PaymentMethodViewModel)x.Value }) .Select(x => new PaymentMethodViewModel { PaymentMethodId = x.value.PaymentMethodId, Type = x.value.Type, ShippingMethods = x.value.ShippingMethods.Select(y => new ShippingMethodViewModel { ShippingMethodId = y.ShippingMethodId, DiscountPrice = y.DiscountPrice, ProductPrice = y.ProductPrice, Tax = y.Tax, Type = y.Type, Cost = y.Cost, }).ToList() }).ToList();
Unable to cast object of type 'System.Collections.Generic.List`1[MeMarketShop.ViewModel.PaymentMethod.PaymentMethodViewModel]' to type 'MeMarketShop.ViewModel.PaymentMethod.PaymentMethodViewModel'.
ASP.NET MVC #8
معرفی HTML Helpers
یک HTML Helper تنها یک متد است که رشتهای را بر میگرداند و این رشته میتواند حاوی هر نوع محتوای دلخواهی باشد. برای مثال میتوان از HTML Helpers برای رندر تگهای HTML، مانند img و input استفاده کرد. یا به کمک HTML Helpers میتوان ساختارهای پیچیدهتری مانند نمایش لیستی از اطلاعات دریافت شده از بانک اطلاعاتی را پیاده سازی کرد. به این ترتیب حجم کدهای تکراری تولید رابط کاربری در Viewهای برنامههای ASP.NET MVC به شدت کاهش خواهد یافت، به همراه قابلیت استفاده مجدد از متدهای الحاقی HTML Helpers در برنامههای دیگر.
HTML Helpers در ASP.NET MVC معادل کنترلهای ASP.NET Web forms هستند اما نسبت به آنها بسیار سبکتر میباشند؛ برای مثال به همراه ViewState و همچنین Event model نیستند.
ASP.NET MVC به همراه تعدادی متد HTML Helper توکار است و برای دسترسی به آنها شیء Html که وهلهای از کلاس توکار HtmlHelper میباشد، در تمام Viewها قابل استفاده است.
نحوه ایجاد یک HTML Helper سفارشی
از دات نت سه و نیم به بعد امکان توسعه اشیاء توکار فریم ورک، به کمک متدهای الحاقی (extension methods) میسر شده است. برای نوشتن یک HTML Helper نیز باید همین شیوه عمل کرد و کلاس HtmlHelper را توسعه داد. در ادامه قصد داریم یک HTML Helper را جهت رندر تگ label در صفحه ایجاد کنیم. برای این منظور پوشهی جدیدی به نام Helper را به پروژه اضافه نمائید (جهت نظم بیشتر). سپس کلاس زیر را به آن اضافه کنید:
using System;
using System.Web.Mvc;
namespace MvcApplication4.Helpers
{
public static class LabelExtensions
{
public static string MyLabel(this HtmlHelper helper, string target, string text)
{
return string.Format("<label for='{0}'>{1}</label>", target, text);
}
}
}
همانطور که ملاحظه میکنید متد Label به شکل یک متد الحاقی توسعه دهنده کلاس HtmlHelper که تنها یک رشته را بر میگرداند، تعریف شده است. اکنون برای استفاده از این متد در View دلخواهی خواهیم داشت:
@using MvcApplication4.Helpers
@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
@Html.MyLabel("firstName", "First Name:")
ابتدا فضای نام مرتبط با متد الحاقی باید پیوست شود و سپس از طریق شیء Html میتوان به این متد الحاقی دسترسی پیدا کرد. اگر برنامه را اجرا کنید، این خروجی را مشاهده خواهیم کرد. چرا؟
Index
<label for='firstName'>First Name:</label>
علت این است که Razor، اطلاعات را Html encoded به مرورگر تحویل میدهد. برای تغییر این رویه باید اندکی متد الحاقی تعریف شده را تغییر داد:
using System.Web.Mvc;
namespace MvcApplication4.Helpers
{
public static class LabelExtensions
{
public static MvcHtmlString MyLabel(this HtmlHelper helper, string target, string text)
{
return MvcHtmlString.Create(string.Format("<label for='{0}'>{1}</label>", target, text));
}
}
}
تنها تغییر صورت گرفته، استفاده از MvcHtmlString بجای string معمولی است تا Razor آنرا encode نکند.
تعریف HTML Helpers سفارشی به صورت عمومی:
میتوان فضای نام MvcApplication4.Helpers این مثال را عمومی کرد. یعنی بجای اینکه بخواهیم در هر View آنرا ابتدا تعریف کنیم، یکبار آنرا همانند تعاریف اصلی یک برنامه ASP.NET MVC، عمومی معرفی میکنیم. برای این منظور فایل web.config موجود در پوشه Views را باز کنید (و نه فایل web.config قرار گرفته در ریشه اصلی برنامه). سپس فضای نام مورد نظر را در قسمت namespaces صفحات اضافه نمائید:
<pages pageBaseType="System.Web.Mvc.WebViewPage">
<namespaces>
<add namespace="System.Web.Mvc" />
<add namespace="System.Web.Mvc.Ajax" />
<add namespace="System.Web.Mvc.Html" />
<add namespace="System.Web.Routing" />
<add namespace="MvcApplication4.Helpers"/>
</namespaces>
به این ترتیب متدهای الحاقی تعریف شده در فضای نام MvcApplication4.Helpers، در تمام Viewهای برنامه در دسترس خواهند بود.
استفاده از کلاس TagBuilder برای تولید HTML Helpers سفارشی:
using System.Web.Mvc;
namespace MvcApplication4.Helpers
{
public static class LabelExtensions
{
public static MvcHtmlString MyNewLabel(this HtmlHelper helper, string target, string text)
{
var labelTag = new TagBuilder("label");
labelTag.MergeAttribute("for", target);
labelTag.InnerHtml = text;
return MvcHtmlString.Create(labelTag.ToString());
}
}
}
در فضای نام System.Web.Mvc، کلاسی وجود دارد به نام TagBuilder که کار تولید تگهای HTML، مقدار دهی ویژگیها و خواص آنها را بسیار ساده میکند و روش توصیه شدهای است برای تولید متدهای HTML Helper. یک نمونه از کاربرد آنرا برای بازنویسی متد MyLabel ذکر شده در اینجا ملاحظه میکنید.
شبیه به همین کلاس، کلاس دیگری به نام HtmlTextWriter در فضای نام System.Web.UI برای انجام اینگونه کارها وجود دارد.
نوشتن HTML Helpers ویژه، به کمک امکانات Razor
نوع دیگری از این متدهای کمکی، Declarative HTML Helpers نام دارند. از این جهت هم Declarative نامیده شدهاند که مستقیما درون فایلهای cshtml یا vbhtml به کمک امکانات Razor قابل تعریف هستند. تولید این نوع متدهای کمکی به این شکل نسبت به مثلا روش TagBuilder سادهتر است، چون توسط Razor به سادگی و به نحو طبیعیتری میتوان تگهای HTML و کدهای مورد نظر را با هم ترکیب کرد (این رفتار طبیعی و روان، یکی از اهداف Razor است).
به عنوان مثال، تعاریف همان کلاسهای Product و Products قسمت قبل (قسمت هفتم) را در نظر بگیرید. با همان کنترلر و View ایی که ذکر شد.
سپس برای تعریف این نوع خاص از HTML Helpers/Razor Helpers باید به این نحو عمل کرد:
الف) در ریشه پروژه یا سایت، پوشهی جدیدی به نام App_Code ایجاد کنید (دقیقا به همین نام. این پوشه، جزو پوشههای ویژه ASP.NET است).
ب) بر روی این پوشه کلیک راست کرده و گزینه Add|New Item را انتخاب کنید.
ج) در صفحه باز شده، MVC 3 Partial Page/Razor را یافته و مثلا نام ProductsList.cshtml را وارد کرده و این فایل را اضافه کنید.
د) محتوای این فایل جدید را به نحو زیر تغییر دهید:
@using MvcApplication4.Models
@helper GetProductsList(List<Product> products)
{
<ul>
@foreach (var item in products)
{
<li>@item.Name ($@item.Price)</li>
}
</ul>
}
در اینجا نحوه تعریف یک helper method مخصوص Razor را مشاهده میکنید که با کلمه @helper شروع شده است. مابقی آن هم ترکیب آشنای code و markup هستند که به کمک امکانات Razor به این شکل روان میسر شده است.
اکنون اگر Viewایی بخواهد از این اطلاعات استفاده کند تنها کافی است به نحو زیر عمل نماید:
@model List<MvcApplication4.Models.Product>
@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
@ProductsList.GetProductsList(@Model)
ابتدا نام فایل ذکر شده بعد نام متد کمکی تعریف شده در آن. Model هم در اینجا به لیستی از محصولات اشاره میکند.
همچنین چون در پوشه app_code قرار گرفته، تمام Viewها به اطلاعات آن دسترسی خواهند داشت. علت هم این است که ASP.NET به صورت خودکار محتوای این پوشه ویژه را همواره کامپایل میکند و در اختیار برنامه قرار میدهد.
به علاوه در این فایل ProductsList.cshtml، باز هم میتوان متدهای helper دیگری را اضافه کرد و از این بابت محدودیتی ندارد. همچنین میتوان این متد helper را مستقیما داخل یک View هم تعریف کرد. بدیهی است در این حالت قابلیت استفاده مجدد از آنرا به همراه داشتن Viewهایی تمیز و کم حجم، از دست خواهیم داد.
جهت تکمیل بحث
Turn your Razor helpers into reusable libraries
در قسمت قبل، در حین بررسی رفتار جزیرههای تعاملی Blazor Server، نکتهی زیر را هم دربارهی راهبری صفحات SSR مرور کردیم:
« اگر دقت کنید، جابجایی بین صفحات، با استفاده از fetch انجام شده؛ یعنی با اینکه این صفحات در اصل static HTML خالص هستند، اما ... کار full reload صفحه مانند ASP.NET Web forms قدیمی انجام نمیشود (و یا حتی برنامههای MVC و Razor pages) و نمایش صفحات، Ajax ای است و با fetch استاندارد آن صورت میگیرد تا هنوز هم حس و حال SPA بودن برنامه حفظ شود. همچنین اطلاعات DOM کل صفحه را هم بهروز رسانی نمیکند؛ فقط موارد تغییر یافته در اینجا به روز رسانی خواهند شد.»
در این قسمت، نکات تکمیلی این قابلیت را که به آن enhanced navigation هم گفته میشود، بررسی میکنیم.
روش غیرفعال کردن راهبری بهبودیافته برای بعضی از لینکها
ویژگی راهبری بهبودیافته فقط در حین هدایت بین صفحات مختلف یک برنامهی Blazor 8x SSR، فعال است. اگر در این بین، کاربری به یک صفحهی غیر بلیزری هدایت شود، راهبری بهبود یافته شکست خورده و سعی میکند حالت full document load را پیاده سازی و اجرا کند. مشکل اینجاست که در این حالت دو درخواست ارسال میشود: ابتدا حالت راهبری بهبودیافته فعال میشود و در ادامه پس از شکست این راهبری، هدایت مستقیم صورت میگیرد. برای رفع این مشکل میتوان ویژگی جدید data-enhance-nav را با مقدار false، به لینکهای خارجی مدنظر اضافه کرد تا برای این حالتها دیگر ویژگی راهبری بهبودیافته فعال نشود:
<a href="/not-blazor" data-enhance-nav="false">A non-Blazor page</a>
فعالسازی مدیریت بهبودیافتهی فرمهای SSR
در قسمت چهارم این سری با فرمهای جدید SSR مخصوص Blazor 8x آشنا شدیم. این فرمها هم میتوانند از امکانات راهبری بهبود یافته استفاده کنند (یعنی مدیریت ارسال آن، توسط fetch API انجام شده و به روز رسانی قسمتهای تغییریافتهی صفحه را Ajax ای انجام دهند)؛ برای نمونه اینبار همانند تصویر زیر، از fetch استاندارد برای ارسال اطلاعات به سمت سرور کمک گرفته میشود (یعنی عملیات Ajax ای شده؛ بجای یک post-back معمولی):
اما ... این قابلیت به صورت پیشفرض در فرمهای تعاملی SSR غیرفعال است. چون همانطور که عنوان شد، اگر مقصد این فرم، یک آدرس غیربلیزری باشد، دوبار ارسال فرم صورت خواهد گرفت؛ یکبار با استفاده از fetch API و بار دیگر پس از شکست، به صورت معمولی. اما اگر مطمئن هستید که endpoint این فرم، قطعا یک کامپوننت بلیزری است، بهتر است این قابلیت را در یک چنین فرمهایی نیز به صورت زیر فعال کنید:
<form method="post" @onsubmit="() => submitted = true" @formname="name" data-enhance> <AntiforgeryToken /> <InputText @bind-Value="Name" /> <button>Submit</button> </form> @if (submitted) { <p>Hello @Name!</p> } @code { bool submitted; [SupplyParameterFromForm] public string Name { get; set; } = ""; }
<EditForm method="post" Model="NewCustomer" OnValidSubmit="() => submitted = true" FormName="customer" Enhance> <DataAnnotationsValidator /> <ValidationSummary/> <p> <label> Name: <InputText @bind-Value="NewCustomer.Name" /> </label> </p> <button>Submit</button> </EditForm> @if (submitted) { <p id="pass">Hello @NewCustomer.Name!</p> } @code { bool submitted = false; [SupplyParameterFromForm] public Customer? NewCustomer { get; set; } protected override void OnInitialized() { NewCustomer ??= new(); } public class Customer { [StringLength(3, ErrorMessage = "Name is too long")] public string? Name { get; set; } } }
نکتهی مهم: در این حالت فرض بر این است که هیچگونه هدایتی به یک Non-Blazor endpoint صورت نمیگیرید؛ وگرنه با یک خطا مواجه خواهید شد.
غیرفعال کردن راهبری بهبودیافته برای قسمتی از صفحه
اگر با استفاده از جاواسکریپت و خارج از کدهای بیلزر، اطلاعات DOM را بهروز رسانی میکنید، ویژگی راهبری بهبودیافته، از آن آگاهی نداشته و به صورت خودکار تمام تغییرات شما را بازنویسی میکند. به همین جهت اگر نیاز است قسمتی از صفحه را که مستقیما توسط کدهای جاواسکریپتی تغییر میدهید، از بهروز رسانیهای این قابلیت مصون نگهدارید، میتوانید ویژگی جدید data-permanent را به آن قسمت اضافه کنید:
<div data-permanent> Leave me alone! I've been modified dynamically. </div>
امکان آگاه شدن از بروز راهبری بهبودیافته در کدهای جاواسکریپتی
اگر به هردلیلی در کدهای جاواسکریپتی خودنیاز به آگاه شدن از وقوع یک هدایت بهبودیافته را دارید (برای مثال جهت بازنویسی تغییرات ایجاد شدهی توسط آن)، میتوانید به نحو زیر، مشترک رخدادهای آن شوید:
<script> Blazor.addEventListener('enhancedload', () => { console.log('enhanced load event occurred'); }); </script>
ویژگی جدید Named Element Routing در Blazor 8x
Blazor 8x از ویژگی مسیریابی سمت کلاینت به کمک تعریف URL fragments پشتیبانی میکند. به این صورت رسیدن (اسکرول) به یک قسمت از صفحهای طولانی، بسیار ساده میشود.
برای مثال المان h2 با id مساوی targetElement را درنظر بگیرید:
<div class="border border-info rounded bg-info" style="height:500px"></div> <h2 id="targetElement">Target H2 heading</h2> <p>Content!</p>
<a href="/counter#targetElement"> <NavLink href="/counter#targetElement"> Navigation.NavigateTo("/counter#targetElement");
معرفی متد جدید Refresh در Blazor 8x
در Blazor 8x، امکان بارگذاری مجدد صفحه با فراخوانی متد جدید NavigationManager.Refresh(bool forceLoad = false) میسر شدهاست. این متد در حالت پیشفرض از قابلیت راهبری بهبودیافته برای به روز رسانی صفحه استفاده میکند؛ مگر اینکه اینکار میسر نباشد. اگر آنرا با پارامتر true فراخوانی کنید، full-page reload رخ خواهد داد.
همین اتفاق در مورد متد Navigation.NavigateTo نیز رخدادهاست. این متد نیز در Blazor 8x به صورت پیشفرض بر اساس قابلیت راهبری بهبود یافته کار میکند؛ مگر اینکه اینکار میسر نباشد و یا پارامتر forceLoad آنرا به true مقدار دهی کنید.
من هم مشکل شمارو داشتم و کدهای فایل ajaxfileupload.js رو بررسی کردم و به این تکه کد برخورد کردم:
$("#" + formElementId + " > input[type='file']").each(function () { var oldElement = $(this); var newElement = jQuery(oldElement).clone(); jQuery(oldElement).attr("id", fileId); jQuery(oldElement).before(newElement); jQuery(oldElement).appendTo(form); });
تقویم شمسی رسپانسیو برای بوت استراپ
$(".from-date").pDatepicker({ format: "YYYY/MM/DD dddd", autoClose: true, onSelect: function (unixDate) { console.log(unixDate); $("#start").val(unixDate); return this; }, navigator: { enabled: true, text: { btnNextText: ">", btnPrevText: "<" }, }, });
- صفحه بندی و مرتب سازی خودکار اطلاعات به کمک jqGrid در ASP.NET MVC
- فعال سازی و پردازش جستجوی پویای jqGrid در ASP.NET MVC
- سفارشی سازی عناصر صفحات پویای افزودن و ویرایش رکوردهای jqGrid در ASP.NET MVC
- آشنایی با کتابخانهی PDF Report
اضافه کردن دکمهی خروجی به jqGrid
برای تهیه خروجی از jqGrid نیاز است بدانیم، اکنون در چه صفحهای از اطلاعات قرار داریم؟ بر روی چه ستونی، مرتب سازی صورت گرفتهاست؟ بر روی کدام فیلدها با چه مقادیری جستجو انجام شدهاست؟ تا ... بتوانیم بر این مبنا، منبع دادهی موجود را فیلتر کرده و لیست نهایی را تبدیل به گزارش کنیم. گزارشی که دقیقا با اطلاعاتی که کاربر در صفحه مشاهده میکند، تطابق داشته باشد.
خوشبختانه تمام این سؤالات توسط متد توکار excelExport در سمت سرور قابل دریافت است:
@section Scripts { <script type="text/javascript"> $(document).ready(function () { $('#list').jqGrid({ caption: "آزمایش ششم", // مانند قبل }).navGrid( // مانند قبل }).jqGrid('navButtonAdd', '#pager', { caption: "", buttonicon: "ui-icon-print", title: "خروجی پی دی اف", onClickButton: function () { $("#list").jqGrid('excelExport', { url: '@Url.Action("GetProducts", "Home")' }); } }); }); </script> }
در اینجا توسط متد navButtonAdd یک دکمهی جدید را اضافه کردهایم که کلیک بر روی آن سبب فراخوانی متد excelExport و ارسال اطلاعات گزارش به url تنظیم شدهاست. باید دقت داشت که این اطلاعات از طریق Http Get به سرور ارسال میشوند و دقیقا اجزای آن همان اجزای جستجوی پویای jqGrid است:
public ActionResult GetProducts(string sidx, string sord, int page, int rows, bool _search, string searchField, string searchString, string searchOper, string filters, string oper)
البته چون تعداد این پارامترها بیش از اندازه شدهاست، بهتر است آنها را تبدیل به یک کلاس کرد:
namespace jqGrid06.Models { public class JqGridRequest { public string sidx { set; get; } public string sord { set; get; } public int page { set; get; } public int rows { set; get; } public bool _search { set; get; } public string searchField { set; get; } public string searchString { set; get; } public string searchOper { set; get; } public string filters { set; get; } public string oper { set; get; } } }
public ActionResult GetProducts(JqGridRequest request) { var list = ProductDataSource.LatestProducts; var pageIndex = request.page - 1; var pageSize = request.rows; var totalRecords = list.Count; var totalPages = (int)Math.Ceiling(totalRecords / (float)pageSize); var productsQuery = list.AsQueryable(); productsQuery = new JqGridSearch().ApplyFilter(productsQuery, request, this.Request.Form); productsQuery = productsQuery.OrderBy(request.sidx + " " + request.sord); if (string.IsNullOrWhiteSpace(request.oper)) { productsQuery = productsQuery .Skip(pageIndex * pageSize) .Take(pageSize); } else if (request.oper == "excel") { productsQuery = productsQuery .Skip(pageIndex * pageSize); } var productsList = productsQuery.ToList(); if (!string.IsNullOrWhiteSpace(request.oper) && request.oper == "excel") { new ProductsPdfReport().CreatePdfReport(productsList); } var productsData = new JqGridData { Total = totalPages, Page = request.page, Records = totalRecords, Rows = (productsList.Select(product => new JqGridRowData { Id = product.Id, RowCells = new List<string> { product.Id.ToString(CultureInfo.InvariantCulture), product.Name, product.AddDate.ToPersianDate(), product.Price.ToString(CultureInfo.InvariantCulture) } })).ToArray() }; return Json(productsData, JsonRequestBehavior.AllowGet); }
توضیحات:
اکثر قسمتهای این متد با متدی که در مطلب «فعال سازی و پردازش جستجوی پویای jqGrid در ASP.NET MVC» مشاهده کردید یکی است؛ برای مثال order by آن با استفاده از کتابخانهی Dynamic LINQ به صورت پویا عمل میکند و متد ApplyFilter، کار تهیه where پویا را انجام میدهد.
فقط در اینجا بررسی و پردازش پارامتر oper نیز اضافه شدهاست. اگر این پارامتر مقدار دهی شده باشد، یعنی نیاز است کل اطلاعات را واکشی کرد؛ زیرا میخواهیم گزارش گیری کنیم و نه اینکه صرفا اطلاعات یک صفحه را به کاربر بازگشت دهیم. همچنین در اینجا List نهایی فیلتر شده به یک گزارش Pdf Report ارسال میشود. این گزارش چون نهایتا اطلاعات را در مرورگر کاربر Flush میکند، کار به اجرای سایر قسمتها نخواهد رسید و همینجا گزارش نهایی تهیه میشود.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید
jqGrid06.7z