Boostrap Ladda یک پلاگین برای نمایش دکمه هایی است که پس از کلیک باید به حالت loading تبدیل شوند. این پلاگین در فرمهای ایجاد و یا ویرایش و یا صفحات جستجو بسیار مفید هستند. Angular-Ladda نسخه همگام سازی شده این پلاگین با AngularJs است که به سادگی میتواند به برنامه تک صفحه ای شما اضافه شود.
در این پست و با استفاده از سری پستهای آقای مهندس یوسف نژاد در این زمینه، یک Attribute جهت هماهنگ سازی با سیستم اعتبار سنجی ASP.NET MVC در سمت سرور و سمت کلاینت (با استفاده از jQuery Validation) بررسی خواهد شد.
قبل از شروع مطالعه سری پستهای MVC و Entity Framework الزامی است.
برای انجام این کار ابتدا مدل زیر را در برنامه خود ایجاد میکنیم.
using System;
public class SampleModel
{
public DateTime StartDate { get; set; }
public string Data { get; set; }
public int Id { get; set; }
}
using System; using System.ComponentModel.DataAnnotations; public class SampleModel { [Required(ErrorMessage = "Start date is required")] public DateTime StartDate { get; set; } [Required(ErrorMessage = "Data is required")] public string Data { get; set; } public int Id { get; set; } }
@model SampleModel @{ ViewBag.Title = "Index"; Layout = "~/Views/Shared/_Layout.cshtml"; } <section> <header> <h3>SampleModel</h3> </header> @Html.ValidationSummary(true, null, new { @class = "alert alert-error alert-block" }) @using (Html.BeginForm("SaveData", "Sample", FormMethod.Post)) { <p> @Html.LabelFor(x => x.StartDate) @Html.TextBoxFor(x => x.StartDate) @Html.ValidationMessageFor(x => x.StartDate) </p> <p> @Html.LabelFor(x => x.Data) @Html.TextBoxFor(x => x.Data) @Html.ValidationMessageFor(x => x.Data) </p> <input type="submit" value="Save"/> } </section>
public class SampleController : Controller { // // GET: /Sample/ public ActionResult Index() { return View(); } public ActionResult SaveData(SampleModel item) { if (ModelState.IsValid) { //save data } else { ModelState.AddModelError("","لطفا خطاهای زیر را برطرف نمایید"); RedirectToAction("Index", item); } return View("Index"); } }
تا اینجای کار روال عادی همیشگی است. اما برای طراحی Attribute دلخواه جهت اعتبار سنجی (مثلا برای مجبور کردن کاربر به وارد کردن یک فیلد با پیام دلخواه و زبان دلخواه) باید یک کلاس جدید تعریف کرده و از کلاس RequiredAttribute ارث ببرد. در پارامتر ورودی این کلاس جهت کار با Resourceهای ثابت در نظر گرفته شده است اما برای اینکه فیلد دلخواه را از دیتابیس بخواند این روش جوابگو نیست. برای انجام آن باید کلاس RequiredAttribute بازنویسی شود.
کلاس طراحی شده باید به صورت زیر باشد:
public class VegaRequiredAttribute : RequiredAttribute, IClientValidatable { #region Fields (2) private readonly string _resourceId; private String _resourceString = String.Empty; #endregion Fields #region Constructors (1) public VegaRequiredAttribute(string resourceId) { _resourceId = resourceId; ErrorMessage = _resourceId; AllowEmptyStrings = true; } #endregion Constructors #region Properties (1) public new String ErrorMessage { get { return _resourceString; } set { _resourceString = GetMessageFromResource(value); } } #endregion Properties #region Methods (2) // Public Methods (1) public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context) { yield return new ModelClientValidationRule { ErrorMessage = GetMessageFromResource(_resourceId), ValidationType = "required" }; } // Private Methods (1) private string GetMessageFromResource(string resourceId) { var errorMessage = HttpContext.GetGlobalResourceObject(_resourceId, "Yes") as string; return errorMessage ?? ErrorMessage; } #endregion Methods }
1- ابتدا دستور
HttpContext.GetGlobalResourceObject(_resourceId, "Yes") as string;
که عنوان کلید Resource را از سازنده کلاس گرفته (کد اقای یوسف نژاد) رشته معادل آن را از دیتابیس بازیابی میکند.
2- ارث بری از اینترفیس IClientValidatable، در صورتی که از این اینترفیس ارث بری نداشته باشیم. Validator طراحی شده در طرف کلاینت کار نمیکند. بلکه کاربر با کلیک بروی دکمه مورد نظر دادهها را به سمت سرور ارسال میکند. در صورت وجود خطا در پست بک خطا نمایش داده خواهد شد. اما با ارث بری از این اینترفیس و پیاده سازی متد GetClientValidationRules میتوان تعریف کرد که در طرف کلاینت با استفاده از Unobtrusive jQuery پیام خطای مورد نظر به کنترل ورودی مورد نظر (مانند تکست باکس) اعمال میشود. مثلا در این مثال خصوصیت data-val-required به input هایی که قبلا در مدل ما Reqired تعریف شده اند اعمال میشود.
حال در مدل تعریف شده میتوان به جای Required میتوان از VegaRequiredAttribute مانند زیر استفاده کرد. (همراه با نام کلید مورد نظر در دیتابیس)
public class SampleModel { [VegaRequired("RequiredMessage")] public DateTime StartDate { get; set; } [VegaRequired("RequiredMessage")] public string Data { get; set; } public int Id { get; set; } }
با اجرای برنامه اینبار خروجی به شکل زیر خواهد بود.
جهت فعال ساری اعتبار سنجی سمت کلاینت ابتدا باید اسکریپتهای زیر به صفحه اضافه شود.
<script src="@Url.Content("~/Scripts/jquery-1.9.1.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
<appSettings> <add key="ClientValidationEnabled" value="true" /> <add key="UnobtrusiveJavaScriptEnabled" value="true"/> </appSettings>
<script type="text/javascript"> jQuery.validator.addMethod('required', function (value, element, params) { if (value == null | value == "") { return false; } else { return true; } }, ''); jQuery.validator.unobtrusive.adapters.add('required', {}, function (options) { options.rules['required'] = true; options.messages['required'] = options.message; }); </script>
موفق و موید باشید.
منابع ^ و ^ و ^ و ^
برپایی پروژههای مورد نیاز
ابتدا یک پوشهی جدید را مانند DownloadFilesSample، ایجاد کرده و در داخل آن دستور زیر را اجرا میکنیم:
> dotnet new react
سپس در این پوشه، پوشهی ClientApp پیشفرض آنرا حذف میکنیم؛ چون کمی قدیمی است. همچنین فایلهای کنترلر و سرویس آب و هوای پیشفرض آنرا به همراه پوشهی صفحات Razor آن، حذف میکنیم.
به علاوه بجای تنظیم پیش فرض زیر در فایل کلاس آغازین برنامه:
spa.UseReactDevelopmentServer(npmScript: "start");
spa.UseProxyToSpaDevelopmentServer("http://localhost:3000");
اکنون در ریشهی پروژهی ASP.NET Core ایجاد شده، دستور زیر را صادر میکنیم تا پروژهی کلاینت React را با فرمت جدید آن ایجاد کند:
> create-react-app clientapp
> cd clientapp > npm install --save bootstrap axios
- برای استفاده از شیوهنامههای بوت استرپ، بستهی bootstrap نیز در اینجا نصب میشود که برای افزودن فایل bootstrap.css آن به پروژهی React خود، ابتدای فایل clientapp\src\index.js را به نحو زیر ویرایش خواهیم کرد:
import "bootstrap/dist/css/bootstrap.css";
- برای دریافت فایلها از سمت سرور، از کتابخانهی معروف axios استفاده خواهیم کرد.
کدهای سمت سرور دریافت فایلهای پویا
در اینجا کدهای سمت سرور برنامه، یک فایل PDF ساده را بازگشت میدهند. این محتوای باینری میتواند حاصل اجرای یک گزارش اکسل، PDF و یا کلا هر نوع فایلی باشد:
using Microsoft.AspNetCore.Mvc; namespace DownloadFilesSample.Controllers { [Route("api/[controller]")] public class ReportsController : Controller { [HttpGet("[action]")] public IActionResult GetPdfReport() { return File(virtualPath: "~/app_data/sample.pdf", contentType: "application/pdf", fileDownloadName: "sample.pdf"); } } }
روش دریافت محتوای باینری در برنامههای React
برای دریافت یک محتوای باینری از سرور توسط axios مانند تصاویر، فایلهای PDF و اکسل و غیره، مهمترین نکته، تنظیم ویژگی responseType آن به blob است:
const getResults = async () => { const { headers, data } = await axios.get(apiUrl, { responseType: "blob" }); }
ساخت URL برای دسترسی به اطلاعات باینری
تمام مرورگرهای جدید از ایجاد URL برای اشیاء Blob دریافتی از سمت سرور، توسط متد توکار URL.createObjectURL پشتیبانی میکنند. این متد، شیء URL را از شیء window جاری دریافت میکند و سپس اطلاعات باینری را دریافت کرده و آدرسی را جهت دسترسی موقت به آن تولید میکند. حاصل آن، یک URL ویژهاست مانند blob:https://localhost:5001/03edcadf-89fd-48b9-8a4a-e9acf09afd67 که گشودن آن در مرورگر، یا سبب نمایش آن تصویر و یا دریافت مستقیم فایل خواهد شد.
در ادامه کدهای تبدیل blob دریافت شدهی از سرور را به این URL ویژه، مشاهده میکنید:
import axios from "axios"; import React, { useEffect, useState } from "react"; export default function DisplayPdf() { const apiUrl = "https://localhost:5001/api/Reports/GetPdfReport"; const [blobInfo, setBlobInfo] = useState({ blobUrl: "", fileName: "" }); useEffect(() => { getResults(); }, []); const getResults = async () => { try { const { headers, data } = await axios.get(apiUrl, { responseType: "blob" }); console.log("headers", headers); const pdfBlobUrl = window.URL.createObjectURL(data); console.log("pdfBlobUrl", pdfBlobUrl); const fileName = headers["content-disposition"] .split(";") .find(n => n.includes("filename=")) .replace("filename=", "") .trim(); console.log("filename", fileName); setBlobInfo({ blobUrl: pdfBlobUrl, fileName: fileName }); } catch (error) { console.log(error); } };
- توسط useEffect Hook و بدون ذکر وابستگی خاصی در آن، سبب شبیه سازی رویداد componentDidUpdate شدهایم. به این معنا که متد getResults فراخوانی شدهی در آن، پس از رندر کامپوننت در DOM فراخوانی میشود و بهترین محلی است که از آن میتوان برای ارسال درخواستهای Ajaxای به سمت سرور و دریافت اطلاعات از backend، استفاده کرد و سپس setState را با اطلاعات جدید فراخوانی نمود. معادل setState در اینجا نیز، همان شیء حالتی است که توسط useState Hook و متد setBlobInfo آن تعریف کردهایم.
- پس از دریافت headers و data از سرور، با استفاده از متد createObjectURL، آنرا تبدیل به یک blob URL کردهایم.
- همچنین در سمت سرور، پارامتر fileDownloadName را نیز تنظیم کردهایم. این نام در سمت کلاینت، توسط هدری با کلید content-disposition ظاهر میشود:
ontent-disposition: "attachment; filename=sample.pdf; filename*=UTF-8''sample.pdf"
- اکنون که نام فایل و URL دسترسی به دادهی فایل باینری دریافتی از سرور را استخراج و ایجاد کردهایم. با فراخوانی متد setBlobInfo، سبب تنظیم متغیر حالت blobInfo خواهیم شد. این مورد، رندر مجدد UI را سبب شده و توسط آن میتوان برای مثال فایل PDF دریافتی را نمایش داد.
نمایش فایل PDF دریافتی از سرور، به همراه دکمههای دریافت، چاپ و بازکردن آن در برگهای جدید
در ادامه کدهای کامل قسمت رندر این کامپوننت را مشاهده میکنید:
import axios from "axios"; import React, { useEffect, useState } from "react"; export default function DisplayPdf() { // ... const { blobUrl } = blobInfo; return ( <> <h1>Display PDF Files</h1> <button className="btn btn-info" onClick={handlePrintPdf}> Print PDF </button> <button className="btn btn-primary ml-2" onClick={handleShowPdfInNewTab}> Show PDF in a new tab </button> <button className="btn btn-success ml-2" onClick={handleDownloadPdf}> Download PDF </button> <section className="card mb-5 mt-3"> <div className="card-header"> <h4>using iframe</h4> </div> <div className="card-body"> <iframe title="PDF Report" width="100%" height="600" src={blobUrl} type="application/pdf" ></iframe> </div> </section> <section className="card mb-5"> <div className="card-header"> <h4>using object</h4> </div> <div className="card-body"> <object data={blobUrl} aria-label="PDF Report" type="application/pdf" width="100%" height="100%" ></object> </div> </section> <section className="card mb-5"> <div className="card-header"> <h4>using embed</h4> </div> <div className="card-body"> <embed aria-label="PDF Report" src={blobUrl} type="application/pdf" width="100%" height="100%" ></embed> </div> </section> </> ); }
در اینجا با انتساب مستقیم blob URL ایجاد شده، به خواص src و یا data اشیائی مانند iframe ،object و یا embed، میتوان سبب نمایش فایل pdf دریافتی از سرور شد. این نمایش نیز توسط قابلیتهای توکار مرورگر صورت میگیرد و نیاز به نصب افزونهی خاصی را ندارد.
در ادامه کدهای مرتبط با سه دکمهی چاپ، دریافت و بازکردن فایل دریافتی از سرور را مشاهده میکنید.
مدیریت دکمهی چاپ PDF
پس از اینکه به blobUrl دسترسی یافتیم، اکنون میتوان یک iframe مخفی را ایجاد کرد، سپس src آنرا به این آدرس ویژه تنظیم نمود و در آخر متد print آنرا فراخوانی کرد که سبب نمایش خودکار دیالوگ چاپ مرورگر میشود:
const handlePrintPdf = () => { const { blobUrl } = blobInfo; if (!blobUrl) { throw new Error("pdfBlobUrl is null"); } const iframe = document.createElement("iframe"); iframe.style.display = "none"; iframe.src = blobUrl; document.body.appendChild(iframe); if (iframe.contentWindow) { iframe.contentWindow.print(); } };
مدیریت دکمهی نمایش فایل PDF در یک برگهی جدید
اگر علاقمند بودید تا این فایل PDF را به صورت تمام صفحه و در برگهای جدید نمایش دهید، میتوان از متد window.open استفاده کرد:
const handleShowPdfInNewTab = () => { const { blobUrl } = blobInfo; if (!blobUrl) { throw new Error("pdfBlobUrl is null"); } window.open(blobUrl); };
مدیریت دکمهی دریافت فایل PDF
بجای نمایش فایل PDF میتوان دکمهای را بر روی صفحه قرار داد که با کلیک بر روی آن، این فایل توسط مرورگر به صورت متداولی جهت دریافت به کاربر ارائه شود:
const handleDownloadPdf = () => { const { blobUrl, fileName } = blobInfo; if (!blobUrl) { throw new Error("pdfBlobUrl is null"); } const anchor = document.createElement("a"); anchor.style.display = "none"; anchor.href = blobUrl; anchor.download = fileName; document.body.appendChild(anchor); anchor.click(); };
اگر خواستید عملیات axios.get و دریافت فایل، با هم یکی شوند، میتوان متد handleDownloadPdf را پس از پایان کار await axios.get، فراخوانی کرد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: DownloadFilesSample.zip
برای اجرای آن، پس از صدور فرمان dotnet restore که سبب بازیابی وابستگیهای سمت کلاینت نیز میشود، ابتدا به پوشهی clientapp مراجعه کرده و فایل run.cmd را اجرا کنید. با اینکار react development server بر روی پورت 3000 شروع به کار میکند. سپس به پوشهی اصلی برنامهی ASP.NET Core بازگشته و فایل dotnet_run.bat را اجرا کنید. این اجرا سبب راه اندازی وب سرور برنامه و همچنین ارائهی برنامهی React بر روی پورت 5001 میشود.
برای طراحی گزارش شما میتوانید به سه روش این کار را انجام دهید.
1- طراحی در برنامه طراح گزارش
2- طراحی از داخل ویژوال استودیو
3- طراحی گزارش در زمان اجرا
برای شروع شما میتوانید نسخه آزمایشی این گزارشساز را دریافت کنید. تنها محدودیت این نسخه نمایش عبارت Demo در چاپ میباشد.
برنامه Designer را اجرا کنید. در صورتی که برای اولین بار است این برنامه را اجرا میکنید ابتدا باید رابط کاربری خود را انتخاب نمایید. نوار ابزار سمت چپ تمامی ابزارهای پرکاربرد طراحی گزارش را در اختیارتان قرار میدهد. ابزارهایی که در این بخش درباره آنها توضیح داده خواهد شد عبارتند از:
Header, Footer, Data, Page Header, Page Footer, Report Title, Report Summery
*به ابزارهای بالا Band گفته میشود.
Header , Footer :
همانطور که از نامشان پیداست در قسمت بالا و پایین بخشی از گزارش قرار میگیرند که برای استفاده در بالا و پایین بند Data میباشد. به عنوان مثال بند Header مناسب طراحی سرستونهای یک جدول میباشد و بند Footer هم جهت نمایش اطلاعات انتهایی یک جدول. ولی شما میتوانید با تنظیم خصوصیات هر بند رفتار و نمایش آنها را به طور کل تغییر دهید. نکته مثبت این گزارشساز این است که شما میتوانید بیش از یک واحد از هر بند را بر روی صفحه طراح خود قرار دهید، به عنوان مثال شما میتوانید دو بند Header داشته باشید که یکی در صفحات زوج و دیگری در صفحات فرد نمایش داده شود.
Data :
این بند جهت نمایش اطلاعات از منبع دادهها میباشد. به این معنا که به ازای هر سطر از دادهها یک بار این بخش نمایش داده میشود. تعداد دفعات نمایش این بند محدود به تعداد سطرهای منبع داده و یا اندازه صفحه و همچنین خصوصیت محدوده نمایش سطرها در یک صفحه میباشد.
Page Header , Page Footer :
این دو بند با توجه به نامشان جهت نمایش در بالا و پایین هر صفحه از گزارش میباشد. البته باز هم یادآور میشوم که با تغییر در خصوصیاتشان میتوانید رفتار و نحوه نمایش آنها را تغییر دهید.
Report Title :
این بند فقط در ابتدای گزارش نمایش داده خواهد شد.
Report Summery :
این بند بلافاصله بعد از اتمام گزارش نمایش داده خواهد شد.
مثال :
برای شروع در Designer یک گزارش جدید از نوع Blank Report ایجاد نمایید. سپس در پنل Dictionary بر روی New Item کلیک کرده و گزینه XML Data را انتخاب نمایید. با توجه به محل نصب گزارشساز وارد مسیر …\Bin\Data شده و فایلهای Demo.xsd و Demo.xml را برای قسمتهای مربوطه انتخاب نمایید. یک بار دیگر بر رو New Item کلیک کرده و گزینه New Data Source را انتخاب نمایید، از لیست ظاهر شده کانکشنی را که ایجاد کردهاید را انتخاب نمایید؛ نتیجه کار تا اینجا باید به صورت زبر باشد.
جدول Product را دراگ کرده و بر روی صفحه طراحی گزارش رها کنید. فرم Data ظاهر میشود این فرم را مطابق تصویر زیر تنظیم نمایید.
حال بر روی صفحه طراحی گزارش بندهای Header, Data, Footer مشاهده میشود؛ حال شما میتوانید با کلیک بر روی سربرگ Preview خروجی گزارش را ببینید.
توابع :
این گزارشساز دارای توابع بسیاری است که اکثر نیازهای شما را برطرف میکند به عنوان مثال تابع تبدیل عدد به حروف به زبان فارسی. همچنین شما میتوانید توابع خاص خود را ساخته و به صورت رفرنس به گزارش اضافه نمایید.
در این بخش ما از توابع موجود در گزارش استفاده خواهیم کرد. برای شروع بر روی کامپوننت Text در بند Footer زیر ستون UnitPrice دابل کلیک کرده تا فرم TextEditor ظاهر شود. سربرگ Summery را انتخاب نمایید. مطابق اطلاعات زیر بخشها را تنظیم نمایید.
Summery Function: Sum
Data Band: DataProducts
Data Column: Products.UnitPrice
حال بر روی سربرگ Preview
کلیک نمایید تا خروجی گزارش را ببینید. جمع ستون UnitPrice
فقط در صفحه آخر نمایش داده خواهد شد. اگر بخواهید جمع ستون در پایین هر صفحه
نمایش داده شود ابتدا باید خصوصیت Print on All Pages بند Footer به True
ست شود. سپس بر روی کامپوننت Text در بند Footer،
دابل کلیک نمایید و در فرم TextEditor
سربرگ Summery
تیک Running Total
را به حالت انتخاب شده در بیاورید، حال خروجی گزارش را ببینید، جمع در انتهای هر
صفحه ظاهر میشود.
متغیرها :
در این گزارش ساز دو نوع متغیر وجود دارد؛ نرمال و سیستمی. نوع سیستمی شامل متغیرهایی میشود که کاربرد مشخصی در تهیه گزارش دارند، مثل شماره صفحه، شماره ردیف، عنوان گزارش و ...
برای مثال شما میتوانید متغیر سیستمی Line را برای روی صفحه طراحی دراگ کنید. دو کامپوننت Text بر روی صفحه ایجاد میشود. اولی با محتوای Line و دومی با محتوای {Line}. اولی را به بند Header و دومی را به بند Data منتقل کنید و سپس خروجی گزارش را مشاهده نمایید، حال گزارش شما دارای شماره ردیف است.
متغیرهای نرمال تقریبا همانند متغیرهایی هستند که همه روزه شما در برنامههای خود از آنها استفاده میکنید. با کلیک بر روی New Item گزینه New Variable را انتخاب نمایید و نوع متغیر را Decimal انتخاب نمایید، سپس متغیر ایجاد شده را دراگ کرده و بروی صفحه طراحی قرار دهید و مشابه متغیر Line عمل کرده و کامپوننتهای Text را در بندهای مناسب قرار دهید. سپس بند Data بر روی صفحه طراحی را انتخاب نمایید، در پنل Properties بر روی Eventes کلیک کرده سپس در رویداد Rendering کد زیر را وارد نمایید.
Variable1 += Products.UnitPrice
حال در خروجی گزارش میتوانید مقادیر محاسبه
شده را ببینید. توجه داشته باشید که شما میتوانید در رویدادهای این گزارشساز به
زبان VB
و C#
برنامه نویسی کنید و محدود به یک خط کد نمیباشید.
شما میتوانید گزارش ساخته شده را به صورتهای مختلف ذخیره کنید از جمله کد C# و یا یک اپلیکیشن قابل اجرا.
تغییرات مورد نیاز سمت کلاینت جهت فعال سازی جستجو در jqGrid
در سمت کلاینت، در حین تعریف ستونها، ابتدا باید توسط مقدار دهی خاصیت search، ستونهای مشارکت کنندهی در حین جستجو را مشخص کرد:
colModel: [ { name: 'Name', index: 'Name', align: 'right', width: 200, search: true, stype: 'text', searchoptions: { sopt: searchOptions } }, { name: 'Supplier.Id', index: 'Supplier.Id', align: 'right', width: 200, search: true, stype: 'select', edittype: 'select', searchOperators: true, searchoptions: { sopt: searchOptions, dataUrl: '@Url.Action("SuppliersSelect","Home")' } } ],
- stype، روش مقایسهی مقادیر را مشخص میکند. مقدار پیش فرض آن text است و مقادیری مانند int، integer، float، number، numeric، date و datetime را میپذیرد.
- searchoptions برای تنظیم جزئیات نحوهی جستجوی بر روی فیلدها بکار میرود. توسط sopt میتوان آرایهای با مقادیر ذیل را مقدار دهی کرد:
var searchOptions = ['eq', 'ne', 'lt', 'le', 'gt', 'ge', 'bw', 'bn', 'in', 'ni', 'ew', 'en', 'cn', 'nc'];
البته باید دقت داشت که آرایه فوق کاملترین حالت ممکن است و ضرورتی ندارد تمام حالات را برای یک فیلد تعریف کرد. چون برای مثال جستجو cn یا contains برای مقادیر رشتهای معنا دارد و نه سایر حالات.
- searchoptions گزینههای دیگری را نیز میتواند شامل شود. برای مثال در حین نمایش جستجوی داخل ردیفی یا صفحهی دیالوگ مخصوص آن، قصد داریم فیلد نام تولید کننده را توسط یک drop down نمایش دهیم و نه یک text box پیش فرض. برای این منظور dataUrl مقدار دهی شدهاست.
SuppliersSelect آن به اکشن متد ذیل اشاره میکند که لیست تولید کنندگان را با فرمت لیستی از SelectListItemها به یک partial view تولید کنندهی دراپ داون ارسال میکند:
public ActionResult SuppliersSelect() { var list = ProductDataSource.LatestProducts; var suppliers = list.Select(x => new SelectListItem { Text = x.Supplier.CompanyName, Value = x.Supplier.Id.ToString(CultureInfo.InvariantCulture) }).ToList(); return PartialView("_SelectPartial", suppliers); }
@model IList<SelectListItem> @Html.DropDownList("srch", Model)
- اگر دقت کرده باشید، نام فیلد تولید کننده با Supplier.Id مقدار دهی شدهاست. علت اینجا است که در زمان استفاده از drop down، مقدار Id آیتم انتخابی، به سرور ارسال میشود. به همین جهت کار کردن پویا با Supplier.Id سادهتر خواهد بود.
- برای نمایش دیالوگ و یا نوار ابزار توکار جستجو، میتوان دکمههایی را به فوتر گرید اضافه کرد:
$(document).ready(function () { $('#list').jqGrid({ // ... مانند قبل و توضیحات فوق }) .jqGrid('navGrid', '#pager', { add: false, edit: false, del: false }, {}, // default settings for edit {}, // default settings for add {}, // delete instead that del:false we need this { // search options multipleSearch: true, closeOnEscape: true, closeAfterSearch: true, ignoreCase: true }) .jqGrid('navButtonAdd', "#pager", { caption: "نوار ابزار جستجو", title: "Search Toolbar", buttonicon: 'ui-icon-search', onClickButton: function () { toolbarSearching(); } }); }); function toolbarSearching() { $('#list').filterToolbar({ groupOp: 'OR', defaultSearch: "cn", autosearch: true, searchOnEnter: true, searchOperators: true, // فعال سازی منوی اپراتورها stringResult : true // وجود این سطر سبب میشود تا اپراتورها به سرور ارسال شوند }); }; function singleSearching() { $('#list').searchGrid({ closeAfterSearch: true }); }; function advancedSearching() { $('#list').searchGrid({ multipleSearch: true, closeAfterSearch: true }); };
- با استفاده از متد jqGrid و پارامتر navGrid، در ناحیهی pager گرید، تنظیمات جستجو را فعال کردهایم.
multipleSearch به معنای امکان جستجوی همزمان بر روی بیش از یک فیلد است. closeOnEscape سبب بسته شدن صفحهی دیالوگ جستجو با فشردن دکمهی ESC میشود. اگر closeAfterSearch به true تنظیم نشود، صفحهی دیالوگ جستجو پس از جستجو، در صفحه باقی مانده و بسته نخواهد شد.
- این دکمهی جستجو، جزو موارد توکار jqGrid است. اگر قصد داشته باشیم یک دکمهی سفارشی دیگر را نیز اضافه کنیم، مجددا از متد jqGrid با پارامتر navButtonAdd در ناحیهی pager استفاده خواهیم کرد. کلیک بر روی آن سبب اجرای متد toolbarSearching میشود.
در اینجا حداقل سه نوع جستجو را میتوان فعال کرد:
- filterToolbar که سبب نمایش نوار ابزار جستجو، دقیقا بالای ستونهای جدول میشود.
- searchGrid که صفحهی دیالوگ مستقلی را جهت جستجو به صورت پویا تولید میکند. اگر خاصیت multipleSearch آن به true تنظیم نشود، این جستجو هربار تنها بر روی یک فیلد قابل انجام خواهد بود و برعکس.
- در حالت جستجوی نوار ابزاری، اگر خواص searchOperators و stringResult به true تنظیم شوند، مانند تصویر ذیل، به ازای هر ستون میتوان از عملگرهای مختلفی استفاده کرد. در غیراینصورت جستجوی انجام شده بر اساس groupOp و defaultSearch پیش فرض انجام میشود. یعنی And یا Or تمام موارد تنها در حالت مثلا contains یا تساوی و امثال آن و نه حالت پیشرفتهی انتخاب عملگرها توسط کاربر.
یک نکته
اگر میخواهید صفحهی جستجو در وسط صفحه ظاهر شود، میتوانید از تنظیمات CSS ذیل استفاده کنید:
/* align center search popup in jqgrid */ .ui-jqdialog { display: none; width: 300px; position: absolute; padding: .2em; font-size: 11px; overflow: visible; left: 30% !important; top: 40% !important; }
پردازش سمت سرور جستجوی پویای jqGrid
کدهای سمت سرور، با کدهای استفاده از dynamic LINQ مایکروسافت یکی است. با این تفاوت که اینبار قسمت where این کوئری نیز پویا میباشد. پیشتر قسمت order by را پویا پردازش کرده بودیم. برای ساخت where پویا که در dynamic LINQ به خوبی پشتیبانی میشود، باید ابتدا ساختار اطلاعات ارسالی به سرور را آنالیز کنیم:
//single field search //_search=true&nd=1403935889318&rows=10&page=1&sidx=Id&sord=asc&searchField=Id&searchString=4444&searchOper=eq&filters= //multi-field search //_search=true&nd=1403935941367&rows=10&page=1&sidx=Id&sord=asc&filters=%7B%22groupOp%22%3A%22AND%22%2C%22rules%22%3A%5B%7B%22field%22%3A%22Id%22%2C%22op%22%3A%22eq%22%2C%22data%22%3A%2244%22%7D%2C%7B%22field%22%3A%22SupplierID%22%2C%22op%22%3A%22eq%22%2C%22data%22%3A%221%22%7D%5D%7D&searchField=&searchString=&searchOper= // filters -> {"groupOp":"AND","rules":[{"field":"All","op":"cn","data":"fffff"},{"field":"Price","op":"bn","data":"ffff"}]} //toolbar search //_search=true&nd=1403935593036&rows=10&page=1&sidx=Id&sord=asc&Id=2&Name=333&SupplierID=1&CategoryID=1&Price=44
- در تمام این حالات پارامتر _search مساوی true است (تفاوت آن با درخواست اطلاعات معمولی).
- در حالت جستجوی نوار ابزاری، اگر گزینههای searchOperators و stringResult به true تنظیم نشوند، حالت toolbar search فوق را شاهد خواهیم بود. در غیراینصورت به حالت multi-field search سوئیچ میشود.
- در حالت جستجوی تک فیلدی توسط صفحه دیالوگ جستجوی jqGrid، فیلد در حال جستجو توسط searchField و مقدار آن توسط searchString به سرور ارسال شدهاند. مابقی پارامترها نال هستند.
- در حالت جستجوی چند فیلدی توسط صفحه دیالوگ جستجوی jqGrid، اینبار filters مقدار دهی شدهاست و سایر پارامترها نال هستند. مقدار filters ارسالی، در حقیقت یک شیء JSON است با ساختار کلی ذیل:
{ "groupOp": "AND", "groups" : [ { "groupOp": "OR", "rules": [ { "field": "name", "op": "eq", "data": "England" }, { "field": "id", "op": "le", "data": "5"} ] } ], "rules": [ { "field": "name", "op": "eq", "data": "Romania" }, { "field": "id", "op": "le", "data": "1"} ] }
public class SearchFilter { public string groupOp { set; get; } public List<SearchGroup> groups { set; get; } public List<SearchRule> rules { set; get; } } public class SearchRule { public string field { set; get; } public string op { set; get; } public string data { set; get; } public override string ToString() { return string.Format("'{0}' {1} '{2}'", field, op, data); } } public class SearchGroup { public string groupOp { set; get; } public List<SearchRule> rules { set; get; } }
اگر این موارد را کنار هم قرار دهیم، به متدی عمومی ApplyFilter با امضای ذیل خواهیم رسید.
public IQueryable<T> ApplyFilter<T>(IQueryable<T> query, bool _search, string searchField, string searchString, string searchOper, string filters, NameValueCollection form)
پس از آن، تغییراتی که در کدهای متد GetProducts باید اعمال شوند به صورت ذیل است:
[HttpPost] public ActionResult GetProducts(string sidx, string sord, int page, int rows, bool _search, string searchField, string searchString, string searchOper, string filters) { var list = ProductDataSource.LatestProducts; var pageIndex = page - 1; var pageSize = rows; var totalRecords = list.Count; var totalPages = (int)Math.Ceiling(totalRecords / (float)pageSize); var productsQuery = list.AsQueryable(); productsQuery = new JqGridSearch().ApplyFilter(productsQuery, _search, searchField, searchString, searchOper, filters, this.Request.Form); var productsList = productsQuery.OrderBy(sidx + " " + sord) .Skip(pageIndex * pageSize) .Take(pageSize) .ToList(); var productsData = new JqGridData { Total = totalPages, Page = page, Records = totalRecords, Rows = (productsList.Select(product => new JqGridRowData { Id = product.Id, RowCells = new List<string> { product.Id.ToString(CultureInfo.InvariantCulture), product.Name, product.Supplier.CompanyName, product.Category.Name, product.Price.ToString(CultureInfo.InvariantCulture) } })).ToArray() }; return Json(productsData, JsonRequestBehavior.AllowGet); }
- نوع متد به HttpPost تغییر کردهاست. این مورد برای ارسال اطلاعات حجیم جستجوها به سرور ضروری است و بهتر است از حالت Get استفاده نشود.
این حالت در سمت کلاینت نیز باید تنظیم شود:
$('#list').jqGrid({ //url access method type mtype: 'POST',
productsQuery = new JqGridSearch().ApplyFilter(productsQuery, _search, searchField, searchString, searchOper, filters, this.Request.Form);
برای مطالعه بیشتر
جستجوی تک فیلدی
جستجوی نوار ابزاری
جستجوی چند فیلدی
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید
jqGrid03.zip
در اینجا React عنوان میکند که هر عنصر از لیستی را که در حال نمایش آن هستید، باید به همراه یک key، ارائه دهید. اما ... این key چیست؟
زمانیکه حالت کامپوننتی تغییر میکند (شیء یا اشیایی که به عناصر UI متصل هستند، تغییر میکنند)، React، درخت جدیدی از اشیایی را که باید رندر شوند، تولید میکند. اکنون React باید محاسبه کند که چه عناصری نسبت به درخت فعلی که در حال نمایش است، تغییر کردهاند تا فقط آنها را به DOM اصلی اعمال کند؛ تا این تغییرات به کاربر نمایش داده شوند و ... این کار را هم به خوبی انجام میدهد. پس مشکل با لیستها چیست که نیاز به key دارند؟
فرض کنید رندر لیستی، خروجی زیر را تولید میکند:
<li>Item 1</li> <li>Item 2</li>
<li>Item 1</li> <li>Item 2</li> <li>Item 3</li>
<li>Item 0</li> // <= New item <li>Item 1</li> <li>Item 2</li> <li>Item 3</li>
راهحل React برای تشخیص منحصربفرد بودن المانهای یک لیست و یا آرایه، استفاده از خاصیت key است:
<li key={0}>Item 0</li> // <= New item <li key={1}>Item 1</li> <li key={2}>Item 2</li> <li key={3}>Item 3</li>
این نکات چه ربطی به Blazor دارند؟!
واقعیت این است که Blazor، همان نسخهی مایکروسافتی React است (!) و این خاصیت key، در Blazor نیز تحت عنوان key directive@ وجود دارد و دقیقا مفهوم آن نیز با توضیحاتی که در مورد React داده شد، یکی است.
زمانیکه Blazor صفحهای را رندر میکند، ابتدا یک DOM مشخصی را تولید خواهد کد. سپس با تغییر State یک کامپوننت، DOM جدیدی را محاسبه کرده و آنرا با DOM فعلی مقایسه میکند و در نهایت diff تولیدی را به DOM موجود، در جهت نمایش تغییرات، اعمال خواهد کرد.
بنابراین الگوریتم diff باید اضافات، به روز رسانیها و حذفهای صورت گرفتهی در UI را تشخیص داده و فقط قسمتهای تغییر یافته را جهت به روز رسانی بهینهی UI، به آن اعمال کند. این الگوریتم diff به صورت پیشفرض از ایندکس المانها برای مقایسهی آنها استفاده میکند. هرچند این روش در بسیاری از حالات، بهینهترین روش است، اما در مورد لیستها خیر؛ که توضیحات آنرا با مثالی در مورد React، در ابتدای بحث بررسی کردیم. برای مثال اگر شیءای به انتهای لیست اضافه شود، هر المانی را که پس از این ایندکس قرار گرفته باشد، تغییر یافته درنظر گرفته و آنرا مجددا رندر میکند. به همین جهت است که اگر المانی به ابتدای یک لیست اضافه شود، اینبار کل لیست را مجددا رندر میکند (چون تمام ایندکسهای اشیاء موجود در لیست، تغییر کردهاند)؛ صرفنظر از اینکه عناصری از این لیست، پیشتر در UI رندر شدهاند و نیازی به رندر مجدد، ندارند.
یک مثال: بررسی نحوهی رندر لیستی از اشیاء در Blazor
در اینجا کدهای کامل کامپوننتی را مشاهده میکنید که یک لیست ساده را در ابتدا رندر کرده و هر بار که بر روی دکمهی افزودن یک شخص کلیک میشود، شخص جدیدی را به ابتدای لیست اضافه میکند:
@page "/" <button class="btn btn-primary" @onclick="addPerson">Add Person</button> <ul class="mt-5"> @foreach (var person in People) { <li>@person.Id, @person.Name</li> } </ul> @code{ List<Person> People = new() { new() { Id = 1, Name = "User 1" }, new() { Id = 2, Name = "User 2" }, }; int LastId = 2; void addPerson() { People.Insert(0, new() { Id = ++LastId, Name = $"User {LastId}" }); } class Person { public int Id { set; get; } public string Name { set; get; } } }
همانطور که مشاهده میکنید، در مثال فوق هر بار کلیک بر روی دکمهی افزودن یک شخص جدید، به همراه رندر تمام المانهای لیست است (محتوای تمام liها یکبار صورتی شده و بعد به حالت عادی در میآیند).
تغییر الگوریتم diff محاسبهی تغییرات UI
الگوریتم پیشفرض diff، از ایندکسهای عناصر برای یافتن تغییرات، استفاده میکند. با استفاده از دایرکتیو ویژهی key@ میتوان ایندکسهای پیشفرض را با مقادیری منحصربفرد، بازنویسی کرد، تا اینبار Blazor دقیقا بداند که کدام آیتم، جدید است و کدامها نیازی به رندر مجدد ندارند:
<ul class="mt-5"> @foreach (var person in People) { <li @key="person.Id">@person.Id, @person.Name</li> } </ul>
اگر به تصویر فوق دقت کنید، اینبار فقط li جدیدی که اضافه شدهاست، ابتدا با رنگ صورتی نمایش داده میشود و محتوای داخل سایر li ها، دست نخورده باقی ماندهاست؛ یعنی مجددا رندر نشدهاند.
ابتدا یک پروژه Asp.Net MVC ایجاد کنید. در فولدر scripts تمام فایلهای جاوااسکریپ پروژه قرار خواهند داشت. اگر قصد داشته باشیم که فایلهای جاوااسکریپی سایر فریم ورکها را استفاده نماییم (مثل backbone.js و ExtJs و...) برای طبقه بندی بهتر فایل ها، بهتر است که یک فولدر با نامی مشخص بسازیم و فایلهای مورد نیاز را در آن قرار دهیم. البته اگر از nuget برای نصب این فریم ورکها استفاده نمایید عموما این کار انجام خواهد شد.
حال با استفاده از Package Manager Console و اجرای دستور زیر، اقدام به نصب requireJs کنید
PM> Install-package requireJs
یک فولدر به نام MyFiles در فولدر Scripts بسازید و فایلهای purchase.js و product.js و credits.js در پروژه قبل را در آن کپی نمایید. کد فایلهای پروژه قبل به صورت زیر بوده است:
purchase.js
define(["credits","products"], function(credits,products) { console.log("Function : purchaseProduct"); return { purchaseProduct: function() { var credit = credits.getCredits(); if(credit > 0){ products.reserveProduct(); alert('purchase done');' return true;
} alert('purchase cancel'); return false; } } });
products.js
define(function(products) { return { reserveProduct: function() { console.log("Function : reserveProduct"); return true; } } });
define(function() { console.log("Function : getCredits"); return { getCredits: function() { var credits = "100"; return credits; } } });
برای قدم بعدی، در متد RegisterBundles فایل bundleConfig پروژه دستور زیر را وارد نمایید:
bundles.Add( new ScriptBundle( "~/bundles/require" ).Include( "~/Scripts/require.js" ) );
حال برای استفاده و لود ماژول purchase در انتهای فایل Index فولدر Home تغییرات زیر را اعمال نمایید:
@section scripts { <script type="text/javascript"> require(['purchase'], function (purchase) { purchase.purchaseProduct(); }); </script> }
namespace BlazorServer.Services { public interface IHotelRoomService { Task<HotelRoomDTO> CreateHotelRoomAsync(HotelRoomDTO hotelRoomDTO); Task<int> DeleteHotelRoomAsync(int roomId); IAsyncEnumerable<HotelRoomDTO> GetAllHotelRoomsAsync(); Task<HotelRoomDTO> GetHotelRoomAsync(int roomId); Task<HotelRoomDTO> IsRoomUniqueAsync(string name); Task<HotelRoomDTO> UpdateHotelRoomAsync(int roomId, HotelRoomDTO hotelRoomDTO); } }
تعریف کامپوننتهای ابتدایی نمایش لیست اتاقها و ثبت و ویرایش آنها
در ابتدا کامپوننتهای خالی نمایش لیست اتاقها و همچنین فرم خالی ثبت و ویرایش آنها را به همراه مسیریابیهای مرتبط، ایجاد میکنیم. به همین جهت ابتدا داخل پوشهی Pages، پوشهی جدید HotelRoom را ایجاد کرده و فایل جدید HotelRoomList.razor را با محتوای ابتدایی زیر، به آن اضافه میکنیم.
@page "/hotel-room" <div class="row mt-4"> <div class="col-8"> <h4 class="card-title text-info">Hotel Rooms</h4> </div> <div class="col-3 offset-1"> <NavLink href="hotel-room/create" class="btn btn-info">Add New Room</NavLink> </div> </div> @code { }
<li class="nav-item px-3"> <NavLink class="nav-link" href="hotel-room"> <span class="oi oi-list-rich" aria-hidden="true"></span> Hotel Rooms </NavLink> </li>
تا اینجا صفحهی ابتدایی نمایش لیست اتاقها، به همراه یک دکمهی افزودن اتاق جدید نیز هست. به همین جهت فایل جدید Pages\HotelRoom\HotelRoomUpsert.razor را به همراه مسیریابی hotel-room/create/ برای تعریف کامپوننت ابتدایی ثبت و ویرایش اطلاعات اتاقها، اضافه میکنیم:
@page "/hotel-room/create" <h3>HotelRoomUpsert</h3> @code { }
- NavLink تعریف شدهی در کامپوننت نمایش لیست اتاقها، به مسیریابی کامپوننت فوق اشاره میکند.
ایجاد فرم ثبت یک اتاق جدید
برای ثبت یک اتاق جدید نیاز است به مدل UI آن که همان HotelRoomDTO تعریف شدهی در قسمت قبل است، دسترسی داشت. به همین جهت در پروژهی BlazorServer.App، ارجاعی را به پروژهی BlazorServer.Models.csproj اضافه میکنیم:
<Project Sdk="Microsoft.NET.Sdk.Web"> <ItemGroup> <ProjectReference Include="..\BlazorServer.Models\BlazorServer.Models.csproj" /> </ItemGroup> </Project>
@using BlazorServer.Models
@page "/hotel-room/create" <div class="row mt-2 mb-5"> <h3 class="card-title text-info mb-3 ml-3">@Title Hotel Room</h3> <div class="col-md-12"> <div class="card"> <div class="card-body"> <EditForm Model="HotelRoomModel"> <div class="form-group"> <label>Name</label> <InputText @bind-Value="HotelRoomModel.Name" class="form-control"></InputText> </div> </EditForm> </div> </div> </div> </div> @code { private HotelRoomDTO HotelRoomModel = new HotelRoomDTO(); private string Title = "Create"; }
- در برنامههای Blazor، کامپوننت ویژهی EditForm را بجای تگ استاندارد form، مورد استفاده قرار میدهیم.
- این کامپوننت، مدل فرم را از فیلد HotelRoomModel که در قسمت کدها تعریف کردیم، دریافت میکند. کار آن تامین اطلاعات فیلدهای فرم است.
- سپس در EditForm تعریف شده، بجای المان استاندارد input، از کامپوننت InputText برای دریافت اطلاعات متنی استفاده میشود. با bind-value@ در قسمت چهارم این سری بیشتر آشنا شدیم و کار آن two-way data binding است. در اینجا هر اطلاعاتی که وارد میشود، سبب به روز رسانی خودکار مقدار خاصیت HotelRoomModel.Name میشود و برعکس.
یک نکته: در قسمت قبل، مدل UI را از نوع رکورد C# 9.0 و init only تعریف کردیم. رکوردها، با EditForm و two-way databinding آن سازگاری ندارند (bind-value@ در اینجا) و بیشتر برای کنترلرهای برنامههای Web API که یکبار قرار است کار وهله سازی آنها در زمان دریافت اطلاعات از کاربر صورت گیرد، مناسب هستند و نه با فرمهای پویای Blazor. به همین جهت به پروژهی BlazorServer.Models مراجعه کرده و نوع آنها را به کلاس و initها را به set معمولی تغییر میدهیم تا در فرمهای Blazor هم قابل استفاده شوند.
تا اینجا کامپوننت ثبت اطلاعات یک اتاق جدید، چنین شکلی را پیدا کردهاست:
تکمیل سایر فیلدهای فرم ورود اطلاعات اتاق
پس از تعریف فیلد ورود اطلاعات نام اتاق، سایر فیلدهای متناظر با HotelRoomDTO را نیز به صورت زیر به EditForm تعریف شده اضافه میکنیم که در اینجا از InputNumber برای دریافت اطلاعات عددی و از InputTextArea، برای دریافت اطلاعات متنی چندسطری استفاده شدهاست:
<EditForm Model="HotelRoomModel"> <div class="form-group"> <label>Name</label> <InputText @bind-Value="HotelRoomModel.Name" class="form-control"></InputText> </div> <div class="form-group"> <label>Occupancy</label> <InputNumber @bind-Value="HotelRoomModel.Occupancy" class="form-control"></InputNumber> </div> <div class="form-group"> <label>Rate</label> <InputNumber @bind-Value="HotelRoomModel.RegularRate" class="form-control"></InputNumber> </div> <div class="form-group"> <label>Sq ft.</label> <InputText @bind-Value="HotelRoomModel.SqFt" class="form-control"></InputText> </div> <div class="form-group"> <label>Details</label> <InputTextArea @bind-Value="HotelRoomModel.Details" class="form-control"></InputTextArea> </div> <div class="form-group"> <button class="btn btn-primary">@Title Room</button> <NavLink href="hotel-room" class="btn btn-secondary">Back to Index</NavLink> </div> </EditForm>
تعریف اعتبارسنجیهای فیلدهای یک فرم Blazor
در حین تعریف یک فرم، برای واکنش نشان دادن به دکمهی submit، میتوان رویداد OnSubmit را به کامپوننت EditForm اضافه کرد که سبب فراخوانی متدی در قسمت کدهای کامپوننت جاری خواهد شد؛ مانند فراخوانی متد HandleHotelRoomUpsert در مثال زیر:
<EditForm Model="HotelRoomModel" OnSubmit="HandleHotelRoomUpsert"> </EditForm> @code { private HotelRoomDTO HotelRoomModel = new HotelRoomDTO(); private async Task HandleHotelRoomUpsert() { } }
اگر این مورد، مدنظر نیست، میتوان بجای OnSubmit، از رویداد OnValidSubmit استفاده کرد. در این حالت اگر اعتبارسنجی مدل فرم با شکست مواجه شود، دیگر متد HandleHotelRoomUpsert فراخوانی نخواهد شد. همچنین در این حالت میتوان خطاهای اعتبارسنجی را نیز در فرم نمایش داد:
<EditForm Model="HotelRoomModel" OnValidSubmit="HandleHotelRoomUpsert"> <DataAnnotationsValidator /> @*<ValidationSummary />*@ <div class="form-group"> <label>Name</label> <InputText @bind-Value="HotelRoomModel.Name" class="form-control"></InputText> <ValidationMessage For="()=>HotelRoomModel.Name"></ValidationMessage> </div> <div class="form-group"> <label>Occupancy</label> <InputNumber @bind-Value="HotelRoomModel.Occupancy" class="form-control"></InputNumber> <ValidationMessage For="()=>HotelRoomModel.Occupancy"></ValidationMessage> </div> <div class="form-group"> <label>Rate</label> <InputNumber @bind-Value="HotelRoomModel.RegularRate" class="form-control"></InputNumber> <ValidationMessage For="()=>HotelRoomModel.RegularRate"></ValidationMessage> </div>
- کامپوننت DataAnnotationsValidator، اعتبارسنجی مبتنی بر data annotations را مانند [Required]، در دامنهی دید یک EditForm فعال میکند.
- اگر خواستیم تمام خطاهای اعتبارسنجی را به صورت خلاصهای در بالای فرم نمایش دهیم، میتوان از کامپوننت ValidationSummary استفاده کرد.
- و یا اگر خواستیم خطاها را به صورت اختصاصیتری ذیل هر تکستباکس نمایش دهیم، میتوان از کامپوننت ValidationMessage کمک گرفت. خاصیت For آن از نوع <Expression<System.Func تعریف شدهاست که اجازهی تعریف strongly typed نام خاصیت در حال اعتبارسنجی را به صورتی که مشاهده میکنید، میسر میکند.
ثبت اولین اتاق هتل
در ادامه میخواهیم روال رویدادگردان HandleHotelRoomUpsert را مدیریت کنیم. به همین جهت نیاز به کار با سرویس IHotelRoomService ابتدای بحث خواهد بود. بنابراین در ابتدا به فایل BlazorServer.App\_Imports.razor مراجعه کرده و فضای نام سرویسهای برنامه را اضافه میکنیم:
@using BlazorServer.Services
@page "/hotel-room/create" @inject IHotelRoomService HotelRoomService @inject NavigationManager NavigationManager @code { private HotelRoomDTO HotelRoomModel = new HotelRoomDTO(); private string Title = "Create"; private async Task HandleHotelRoomUpsert() { var roomDetailsByName = await HotelRoomService.IsRoomUniqueAsync(HotelRoomModel.Name); if (roomDetailsByName != null) { //there is a duplicate room. show an error msg. return; } var createdResult = await HotelRoomService.CreateHotelRoomAsync(HotelRoomModel); NavigationManager.NavigateTo("hotel-room"); } }
اگر پیشتر با ASP.NET Web Forms کار کرده باشید (اولین روش توسعهی برنامههای وب در دنیای دات نت)، مدل برنامه نویسی Blazor Server، بسیار شبیه به کار با وب فرمها است؛ البته بر اساس آخرین تغییرات دنیای دانت نت مانند برنامه نویسی async، کار با سرویسها، تزریق وابستگیهای توکار و غیره.
نمایش لیست اتاقهای ثبت شده
تا اینجا موفق شدیم اطلاعات یک مدل اعتبارسنجی شده را در بانک اطلاعاتی ثبت کنیم. مرحلهی بعد، نمایش لیست اطلاعات ثبت شدهی در بانک اطلاعاتی است. بنابراین به کامپوننت HotelRoomList.razor مراجعه کرده و آنرا به صورت زیر تکمیل میکنیم:
@page "/hotel-room" @inject IHotelRoomService HotelRoomService <div class="row mt-4"> <div class="col-8"> <h4 class="card-title text-info">Hotel Rooms</h4> </div> <div class="col-3 offset-1"> <NavLink href="hotel-room/create" class="btn btn-info">Add New Room</NavLink> </div> </div> <div class="row mt-4"> <div class="col-12"> <table class="table table-bordered table-hover"> <thead> <tr> <th>Name</th> <th>Occupancy</th> <th>Rate</th> <th> Sqft </th> <th> </th> </tr> </thead> <tbody> @if (HotelRooms.Any()) { foreach (var room in HotelRooms) { <tr> <td>@room.Name</td> <td>@room.Occupancy</td> <td>@room.RegularRate.ToString("c")</td> <td>@room.SqFt</td> <td></td> </tr> } } else { <tr> <td colspan="5">No records found</td> </tr> } </tbody> </table> </div> </div> @code { private List<HotelRoomDTO> HotelRooms = new List<HotelRoomDTO>(); protected override async Task OnInitializedAsync() { await foreach(var room in HotelRoomService.GetAllHotelRoomsAsync()) { HotelRooms.Add(room); } } }
- متد GetAllHotelRoomsAsync، لیست اتاقهای ثبت شده را بازگشت میدهد. البته خروجی آن از نوع <IAsyncEnumerable<HotelRoomDTO است که از زمان C# 8.0 ارائه شد و روش کار با آن اندکی متفاوت است. IAsyncEnumerableها را باید توسط await foreach پردازش کرد.
- همانطور که در مطلب بررسی چرخهی حیات کامپوننتها نیز عنوان شد، متدهای رویدادگران OnInitialized و نمونهی async آن برای دریافت اطلاعات از سرویسها طراحی شدهاند که در اینجا نمونهای از آنرا مشاهده میکنید.
- پس از تشکیل لیست اتاقها، حلقهی foreach (var room in HotelRooms) تعریف شده، ردیفهای آنرا در UI نمایش میدهد.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-14.zip
<button onclick="$('#list')[0].toggleToolbar()">نمایش/حذف تولبار</button>