نکته : فقط در Vs.Net با نسخههای Ultimate و Premium میتونید از Code UI Test استفاده کنید که البته به دلیل اینکه در ایران پیدا کردن نسخههای دیگر Vs.Net به غیر از Ultimate سختتر است به طور قطع این محدودیت برای برنامه نویسان ما وجود نخواهد داشت. برای اینکه از نسخه Vs.Net خود اطمینان حاصل کنید از منوی Help گزینه About Microsoft Visual Studio رو انتخاب کنید. پنجره ای به شکل زیر مشاهده خواهید کرد که در آن مشخصات کامل Vs.Net ذکر شده است.
در این مرحله قصد داریم برای فرم زیر Unit Test طراحی کنیم. پروژه به صورت زیر است:
کاملا واضح است که در این فرم دو عدد به عنوان ورودی دریافت میشود و بعد از کلیک بر روی CalculateSum نتیجه در textbox سوم نمایش داده میشود. برای تست عملکرد صحیح فرم بالا ایتدا به Solution مورد نظر از منوی test Project یک Coded UI Test Project اضافه میکنیم. به دلیل اینکه این قبلا در این Solution پروژه تست از نوع Coded UI Test نبود بلافاصله یک پنجره نمایش داده میشود. مطمئن شوید گزینه اول انتخاب شده و بعد بر روی Ok کلیک کنید.(گزینه اول به معنی است که قصد داریم عملیات مورد نظر بر روی UI را رکورد کنیم و گزینه دوم به معنی است که قصد داریم از عملیات رکورد شده قبلی استفاده کنیم). یک کلاس به نام CodeUITest1 به همراه یک متد تست به نام CodedUITestMethod1 ساخته میشود. اولین چیزی که جلب توجه میکند این است که این کلاس به جای TestClassAttribute دارای نشان CodeUITestAttrbiute است. در گوشه سمت راست Vs.Net خود یک پنجره کوچک به نام UI Map Test Builder مانند شکل زیر خواهید دید.دکمه قرمز رنگ به نام Record Button است و عملیات تست را رکورد خواهد کرد. دکمه دایره ای به رنگ مشکی برای تعیین Assertion به کار میرود. و در نهایت گزینه آخر کدهای مورد نظر مراحل قبل را به صورت خودکار تولید خواهد کرد.
#روش کار
روش کار به این صورت است که ابتدا شما مراحل تست خود را شبیه سازی خواهید کرد و بعد از آن Test Builder مراحل تست شما را به صورت کامل به صورت کدهای قابل فهم تولید خواهد کرد. (دقیقا شبیه به ایجاد UnitTest به روش Arrange/Act/Assert است با این تفاوت که این مراحل توسط UI Map رکورد شده و نیازی به کد نویسی ندارد). در پایان باید یک Data Driven Coded UI Test طراحی کنید تا بتوانید از این مراحل رکورد استفاده نمایید.
#چگونگی شبیه سازی :
پروژه را اجرا نمایید. زمانی که فرم مورد نظر ظاهر شد بر روی گزینه Record در TestBuilder کلیک کنید. عملیات ذخیره سازی شروع شده است. در نتیجه به فرم مربوطه رفته و در Textbox اول مقدار 10 و در textbox دوم مقدار 5 را وارد نمایید. با کلیک بر روی دکمه CalculateSum مقدار 15 نمایش داده خواهد شد. از برنامه خارج شوید و بعد بر روی گزینه Generate Code در TestBuilder کلیک کنید با از کلیدهای ترکیبی Alt + G استفاده نمایید.(اگر در این مرحله، از برنامه خارج نشده باشید با خطا مواجه خواهید شد.) در پنجره نمایش داده شده یک نام به متد اختصاص دهید. عملیات تولید کد شروع خواهد شد. بعد کدی مشابه زیر را در متد مربوطه مشاهده خواهید کرد.
[TestMethod] public void CodedUITestMethod1() { this.UIMap.CalculateSum(); this.UIMap.txtSecondValueMustBe10(); }
public void CodedUITestMethod1 ()
{
#region Variable Declarations
WinEdit uITxtFirstNumberEdit = this.UIدوعددصحیحواردنماییدWindow.UITxtFirstNumberWindow.UITxtFirstNumberEdit;
WinEdit uITxtSecondNumberEdit = this.UIدوعددصحیحواردنماییدWindow.UITxtSecondNumberWindow.UITxtSecondNumberEdit;
WinButton uICalculateSumButton = this.UIدوعددصحیحواردنماییدWindow.UICalculateSumWindow.UICalculateSumButton;
#endregion
// Type '10' in 'txtFirstNumber' text box
uITxtFirstNumberEdit.Text = this.CalculateSumParams.UITxtFirstNumberEditText;
// Type '{Tab}' in 'txtFirstNumber' text box
Keyboard.SendKeys(uITxtFirstNumberEdit, this.CalculateSumParams.UITxtFirstNumberEditSendKeys, ModifierKeys.None);
// Type '10' in 'txtSecondNumber' text box
uITxtSecondNumberEdit.Text = this.CalculateSumParams.UITxtSecondNumberEditText;
// Click 'Calculate Sum' button
Mouse.Click(uICalculateSumButton, new Point(83, 12));
// Type '10' in 'txtFirstNumber' text box
uITxtFirstNumberEdit.Text = this.CalculateSumParams.UITxtFirstNumberEditText1;
// Type '{Tab}' in 'txtFirstNumber' text box
Keyboard.SendKeys(uITxtFirstNumberEdit, this.CalculateSumParams.UITxtFirstNumberEditSendKeys1, ModifierKeys.None);
// Type '10' in 'txtSecondNumber' text box
uITxtSecondNumberEdit.Text = this.CalculateSumParams.UITxtSecondNumberEditText1;
// Type '{Tab}' in 'txtSecondNumber' text box
Keyboard.SendKeys(uITxtSecondNumberEdit, this.CalculateSumParams.UITxtSecondNumberEditSendKeys, ModifierKeys.None);
// Click 'Calculate Sum' button
Mouse.Click(uICalculateSumButton, new Point(49, 11));
// Type '10' in 'txtFirstNumber' text box
uITxtFirstNumberEdit.Text = this.CalculateSumParams.UITxtFirstNumberEditText2;
// Type '{Tab}' in 'txtFirstNumber' text box
Keyboard.SendKeys(uITxtFirstNumberEdit, this.CalculateSumParams.UITxtFirstNumberEditSendKeys2, ModifierKeys.None);
// Type '5' in 'txtSecondNumber' text box
uITxtSecondNumberEdit.Text = this.CalculateSumParams.UITxtSecondNumberEditText2;
// Type '{Tab}' in 'txtSecondNumber' text box
Keyboard.SendKeys(uITxtSecondNumberEdit, this.CalculateSumParams.UITxtSecondNumberEditSendKeys1, ModifierKeys.None);
// Click 'Calculate Sum' button
Mouse.Click(uICalculateSumButton, new Point(74, 16));
}
چگونگی ایجاد Assertion
اگر به کد متد تست CodedUITestMethod1 در بالا دقت کنید یک متد به صورت this.UIMap.txtSecondValueMustBe10 فراخوانی شده است. این در واقع یک Assertion است که در هنگام عملیات رکورد ایجاد کردم و به این معنی است که مقدار TextBox دوم حتما باید 10 باشد. حال روش تولید Assertionها را بررسی خواهیم کرد.
بعد از شروع شدن مرحله رکورد اگر قصد دارید برای یک کنترل خاص Assert بنویسید، دکمه assertion (به رنگ مشکی و به صورت دایره است) را بر روی کنترل مورد نظر drag&drop کنید. یک border آبی برای کنترل مورد نظر ایجاد خواهد شد:
به محض اتمام عملیات drag&drop منوی زیر ظاهر خواهد شد:
از گزینه Add Assertion استفاده کنید و برای کنترل مورد نظر یک assert بنویسید. در شکل زیر یک assert برای textbox دوم نوشتم به صورتی که مقدار آن باید با 5 برابر باشد.از گزینه آخر برای نمایش پیغام مورد نظر خودتون در هنگامی که aseert با شکست مواجه میشود استفاده کنید.
کد تولید شده زیر برای عملیات assert بالا است:
public void txtSecondValueMustBe10() { #region Variable Declarations WinEdit uITxtSecondNumberEdit = this.UIدوعددصحیحواردنماییدWindow.UITxtSecondNumberWindow.UITxtSecondNumberEdit; #endregion // Verify that the 'ControlType' property of 'txtSecondNumber' text box equals '10' Assert.AreEqual(this.txtSecondValueMustBe10ExpectedValues.UITxtSecondNumberEditControlType, uITxtSecondNumberEdit.ControlType.ToString()); }
مرحله اول انجام شد. برای تست این مراحل باید یک Data DrivenTest بسازید که در پست بعدی به صورت کامل شرح داده خواهد شد.
CREATE TABLE [dbo].[Employee] ( [EmpId] INT NOT NULL, [FirstName] VARCHAR (20) NOT NULL, [LastName] VARCHAR (20) NOT NULL, [City] VARCHAR (20) NOT NULL, PRIMARY KEY CLUSTERED ([EmpId] ASC) );
<asp:BoundField DataField="FirstName" HeaderText="First Name" />
<asp:TemplateField> <ItemTemplate> <asp:CheckBox ID="chkDel" runat="server" /> </ItemTemplate> </asp:TemplateField>
<asp:Button ID="btnDeleteRecord" runat="server" OnClick="btnDeleteRecord_Click" Text="Delete" />
function DeleteConfirm() { var Ans = confirm("Do you want to Delete Selected Employee Record?"); if (Ans) { return true; } else { return false; } }
protected void Page_Load(object sender, EventArgs e) { if(!IsPostBack) { //Displaying the Data showData(); //Adding an Attribute to Server Control(i.e. btnDeleteRecord) btnDeleteRecord.Attributes.Add("onclick", "javascript:return DeleteConfirm()"); } }
//Method for Displaying Data protected void showData() { DataTable dt = new DataTable(); SqlConnection con = new SqlConnection(cs); SqlDataAdapter adapt = new SqlDataAdapter("select * from Employee",con); con.Open(); adapt.Fill(dt); con.Close(); GridView1.DataSource = dt; GridView1.DataBind(); }
protected void DeleteRecord(int empid) { SqlConnection con = new SqlConnection(cs); SqlCommand com = new SqlCommand("delete from Employee where EmpId=@ID",con); com.Parameters.AddWithValue("@ID",empid); con.Open(); com.ExecuteNonQuery(); con.Close(); }
protected void btnDeleteRecord_Click(object sender, EventArgs e) { foreach (GridViewRow grow in GridView1.Rows) { //Searching CheckBox("chkDel") in an individual row of Grid CheckBox chkdel = (CheckBox)grow.FindControl("chkDel"); //If CheckBox is checked than delete the record with particular empid if(chkdel.Checked) { int empid = Convert.ToInt32(grow.Cells[1].Text); DeleteRecord(empid); } } //Displaying the Data in GridView showData(); }
Mvc File Manager
Admin (Full access) FileManager_Read(readonly access) FileManager_Write(Creat Folder & upload file) FileManager_Change(Move & Rename) FileManager_Delete(Delete file and Folder
آموزش Code Contracts
همان طور که مشاهده میکنید با استفاده از تعریف Contract قبل از اجرای برنامه برای ما مشخص خواهد شد مقدار پیش فرض 0 برای متغیر d درست نیست در واقع اصلا این کد کامپایل نمیشود.
به این نکته نیز توجه داشته باشید که با تمام مزیت هایی که Code Contracts در اختیار ما قرار میدهد، زمان کامپایل پروژه را به شدت افزایش خواهد داد به طوری که در یک Solution نسبتا بزرگ آزار دهنده است.
CSS isolation در ASP.NET Core 6
کتابخانه جایگزین آنرا افزونه XMLWorker معرفی کردهاند که توانایی پردازش CSS و HTML بهتر و کاملتری را نسبت به HTMLWorker ارائه میدهد. این کتابخانه نیز همانند HTMLWorker پشتیبانی توکاری از متون راست به چپ و یونیکد فارسی، ندارد و نیاز است برای نمایش صحیح متون فارسی در آن، نکات خاصی را اعمال نمود که در ادامه بحث آنها را مرور خواهیم کرد.
ابتدا برای دریافت آخرین نگارشهای iTextSharp و افزونه XMLWorker آن به آدرسهای ذیل مراجعه نمائید:
تهیه یک UnicodeFontProvider
Encoding پیش فرض قلمها در XMLWorker مساوی BaseFont.CP1252 است؛ که از حروف یونیکد پشتیبانی نمیکند. برای رفع این نقیصه نیاز است یک منبع تامین قلم سفارشی را برای آن ایجاد نمود:
public class UnicodeFontProvider : FontFactoryImp { static UnicodeFontProvider() { // روش صحیح تعریف فونت var systemRoot = Environment.GetEnvironmentVariable("SystemRoot"); FontFactory.Register(Path.Combine(systemRoot, "fonts\\tahoma.ttf")); // ثبت سایر فونتها در اینجا //FontFactory.Register(Path.Combine(Environment.CurrentDirectory, "fonts\\irsans.ttf")); } public override Font GetFont(string fontname, string encoding, bool embedded, float size, int style, BaseColor color, bool cached) { if (string.IsNullOrWhiteSpace(fontname)) return new Font(Font.FontFamily.UNDEFINED, size, style, color); return FontFactory.GetFont(fontname, BaseFont.IDENTITY_H, BaseFont.EMBEDDED, size, style, color); } }
مابقی مسایل آن خودکار خواهد بود و هر زمانیکه نیاز به قلم خاصی از طرف XMLWorker وجود داشت، به متد GetFont فوق مراجعه کرده و اینبار قلمی با BaseFont.IDENTITY_H را دریافت میکند. IDENTITY_H در استاندارد PDF، جهت مشخص ساختن encoding قلمهایی با پشتیبانی از یونیکد بکار میرود.
تهیه منبع تصاویر
در XMLWorker اگر تصاویر با http شروع نشوند (دریافت تصاویر وب آن خودکار است)، آن تصاویر را از مسیری که توسط پیاده سازی کلاس AbstractImageProvider مشخص خواهد شد، دریافت میکند که نمونهای از پیاده سازی آنرا در ذیل مشاهده میکنید:
public class ImageProvider : AbstractImageProvider { public override string GetImageRootPath() { var path = Environment.GetFolderPath(Environment.SpecialFolder.MyPictures); return path + "\\"; // مهم است که این مسیر به بک اسلش ختم شود تا درست کار کند } }
نحوه تعریف یک فایل CSS خارجی
public static class XMLWorkerUtils { /// <summary> /// نحوه تعریف یک فایل سی اس اس خارجی /// </summary> public static ICssFile GetCssFile(string filePath) { using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { return XMLWorkerHelper.GetCSS(stream); } } }
تبدیل المانهای HTML پردازش شده به یک لیست PDF ایی
تهیه مقدمات فارسی سازی و نمایش راست به چپ اطلاعات در کتابخانه XMLWorker از اینجا شروع میشود. در حالت پیش فرض کار آن، المانهای HTML به صورت خودکار Parse شده و به صفحه اضافه میشوند. به همین دلیل دیگر فرصت اعمال خواص RTL به المانهای پردازش شده دیگر وجود نخواهد داشت و به صورت توکار نیز این مسایل درنظر گرفته نمیشود. به همین دلیل نیاز است که در حین پردازش المانهای HTML و تبدیل آنها به معادل المانهای PDF، بتوان آنها را جمع آوری کرد که نحوه انجام آنرا با پیاده سازی اینترفیس IElementHandler در ذیل مشاهده میکنید:
/// <summary> /// معادل پی دی افی المانهای اچ تی ام ال را جمع آوری میکند /// </summary> public class ElementsCollector : IElementHandler { private readonly Paragraph _paragraph; public ElementsCollector() { _paragraph = new Paragraph { Alignment = Element.ALIGN_LEFT // سبب میشود تا در حالت راست به چپ از سمت راست صفحه شروع شود }; } /// <summary> /// این پاراگراف حاوی کلیه المانهای متن است /// </summary> public Paragraph Paragraph { get { return _paragraph; } } /// <summary> /// بجای اینکه خود کتابخانه اصلی کار افزودن المانها را به صفحات انجام دهد /// قصد داریم آنها را ابتدا جمع آوری کرده و سپس به صورت راست به چپ به صفحات نهایی اضافه کنیم /// </summary> /// <param name="htmlElement"></param> public void Add(IWritable htmlElement) { var writableElement = htmlElement as WritableElement; if (writableElement == null) return; foreach (var element in writableElement.Elements()) { fixNestedTablesRunDirection(element); _paragraph.Add(element); } } /// <summary> /// نیاز است سلولهای جداول تو در توی پی دی اف نیز راست به چپ شوند /// </summary> private void fixNestedTablesRunDirection(IElement element) { var table = element as PdfPTable; if (table == null) return; table.RunDirection = PdfWriter.RUN_DIRECTION_RTL; foreach (var row in table.Rows) { foreach (var cell in row.GetCells()) { cell.RunDirection = PdfWriter.RUN_DIRECTION_RTL; foreach (var item in cell.CompositeElements) { fixNestedTablesRunDirection(item); } } } } }
یک مثال کامل از نحوه کنار هم قرار دادن پیشنیازهای تهیه شده
خوب؛ تا اینجا یک سری پیشنیاز را تهیه کردیم، اما XMLWorker از وجود آنها بیخبر است. برای معرفی آنها باید به نحو ذیل عمل کرد:
using (var pdfDoc = new Document(PageSize.A4)) { var pdfWriter = PdfWriter.GetInstance(pdfDoc, new FileStream("test.pdf", FileMode.Create)); pdfWriter.RgbTransparencyBlending = true; pdfDoc.Open(); var html = @"<span style='color:blue; font-family:tahoma;'><b>آزمایش</b></span> کتابخانه <i>iTextSharp</i> <u>جهت بررسی فارسی نویسی</u> <table style='color:blue; font-family:tahoma;' border='1'><tr><td>eeمتن</td></tr></table> <code>This is a code!</code> <br/> <img src='av-13489.jpg' /> "; var cssResolver = new StyleAttrCSSResolver(); // cssResolver.AddCss(XMLWorkerUtils.GetCssFile(@"c:\path\pdf.css")); cssResolver.AddCss(@"code { padding: 2px 4px; color: #d14; white-space: nowrap; background-color: #f7f7f9; border: 1px solid #e1e1e8; }", "utf-8", true); // کار جمع آوری المانهای ترجمه شده به المانهای پی دی اف را انجام میدهد var elementsHandler = new ElementsCollector(); var htmlContext = new HtmlPipelineContext(new CssAppliersImpl(new UnicodeFontProvider())); htmlContext.SetImageProvider(new ImageProvider()); htmlContext.CharSet(Encoding.UTF8); htmlContext.SetAcceptUnknown(true).AutoBookmark(true).SetTagFactory(Tags.GetHtmlTagProcessorFactory()); var pipeline = new CssResolverPipeline(cssResolver, new HtmlPipeline(htmlContext, new ElementHandlerPipeline(elementsHandler, null))); var worker = new XMLWorker(pipeline, parseHtml: true); var parser = new XMLParser(); parser.AddListener(worker); parser.Parse(new StringReader(html)); // با هندلر سفارشی که تهیه کردیم تمام المانهای اچ تی ام ال به المانهای پی دی اف تبدیل شدند // الان تنها کافی کافی است تا اینها را در یک جدول راست به چپ محصور کنیم تا درست نمایش داده شوند var mainTable = new PdfPTable(1) { WidthPercentage = 100, RunDirection = PdfWriter.RUN_DIRECTION_RTL }; var cell = new PdfPCell { Border = 0, RunDirection = PdfWriter.RUN_DIRECTION_RTL, HorizontalAlignment = Element.ALIGN_LEFT }; cell.AddElement(elementsHandler.Paragraph); mainTable.AddCell(cell); pdfDoc.Add(mainTable); } Process.Start("test.pdf");
UnicodeFontProvider باید به HtmlPipelineContext شناسانده شود.
ImageProvider توسط متد SetImageProvider به HtmlPipelineContext معرفی میشود.
ElementsCollector سفارشی ما در قسمت CssResolverPipeline باید به سیستم تزریق شود.
پس از آن XMLWorker را وادار میکنیم تا HTML را Parse کرده و معادل المانهای PDF ایی آنرا تهیه کند؛ اما آنها را به صورت خودکار به صفحات فایل PDF نهایی اضافه نکند. در این بین ElementsCollector ما این المانها را جمع آوری کرده و در نهایت، پاراگراف کلی حاصل از آنرا به یک جدول با RUN_DIRECTION_RTL اضافه میکنیم. حاصل آن نمایش صحیح متون فارسی است.
کدهای مثال فوق را از آدرس ذیل نیز میتوانید دریافت کنید:
XMLWorkerRTLsample.cs
به روز رسانی
کلیه نکات مطلب فوق را به همراه بهبودهای مطرح شده در نظرات آن، در پروژهی ذیل میتوانید به صورت یکجا دریافت و بررسی کنید:
XMLWorkerRTLsample.zip
تمرین 1 - ایجاد فرم ثبت نام
میخواهیم به برنامه، فرم ثبت نام را که حاوی سه فیلد نام کاربری، کلمهی عبور و نام است، اضافه کنیم. نام کاربری باید از نوع ایمیل باشد. بنابراین اعتبارسنجی مرتبطی نیز باید برای این فیلد تعریف شود. کلمهی عبور وارد شده باید حداقل 5 حرف باشد. همچنین تا زمانیکه اعتبارسنجی فرم تکمیل نشدهاست، باید دکمهی submit فرم، غیرفعال باقی بماند. لینک ورود به این فرم نیز باید به منوی راهبری سایت اضافه شود.
برای حل این تمرین، فایل جدید registerForm.jsx را در پوشهی components ایجاد میکنیم و سپس توسط میانبرهای imrc و cc در VSCode، ساختار ابتدایی کامپوننت RegisterForm را ایجاد کرده و سپس آنرا به صورت زیر تکمیل میکنیم:
- ابتدا در فایل app.js، پس از import ماژول آن:
import RegisterForm from "./components/registerForm";
<Route path="/register" component={RegisterForm} />
<NavLink className="nav-item nav-link" to="/register"> Register </NavLink>
import Joi from "@hapi/joi"; import React from "react"; import Form from "./common/form"; class RegisterForm extends Form { state = { data: { username: "", password: "", name: "" }, errors: {} }; schema = { username: Joi.string() .required() .email({ minDomainSegments: 2, tlds: { allow: ["com", "net"] } }) .label("Username"), password: Joi.string() .required() .min(5) .label("Password"), name: Joi.string() .required() .label("Name") }; doSubmit = () => { // Call the server console.log("Submitted"); }; render() { return ( <div> <h1>Register</h1> <form onSubmit={this.handleSubmit}> {this.renderInput("username", "Username")} {this.renderInput("password", "Password", "password")} {this.renderInput("name", "Name")} {this.renderButton("Register")} </form> </div> ); } } export default RegisterForm;
- سپس state این کامپوننت را با شیءای حاوی دو خاصیت data و error، مقدار دهی اولیه میکنیم. خواص متناظر با المانهای فرم را نیز به صورت یک شیء، به خاصیت data انتساب دادهایم.
- پس از آن، خاصیت schema تعریف شدهاست؛ تا قواعد اعتبارسنجی تک تک فیلدهای فرم را به کمک کتابخانهی Joi، مطابق نیازمندیهایی که در ابتدای تعریف این تمرین مشخص کردیم، ایجاد کند.
- در ادامه، متد doSubmit را ملاحظه میکنید. این متد پس از کلیک بر روی دکمهی Register و پس از اعتبارسنجی موفقیت آمیز فرم، به صورت خودکار فراخوانی میشود.
- در آخر، تعریف فرم ثبتنام را مشاهده میکنید که نکات آنرا در قسمت قبل، با معرفی کامپوننت Form و افزودن متدهای کمکی رندر input و button به آن، بررسی کردیم و در کل با نکات بررسی شدهی در فرم لاگینی که تا به اینجا ایجاد کردیم، تفاوتی ندارد.
تمرین 2- ایجاد فرم ثبت و یا ویرایش یک فیلم
فرم جدید ثبت و ویرایش یک فیلم، نکات بیشتری را به همراه دارد. در اینجا میخواهیم در بالای لیست نمایش فیلمها، یک دکمهی new movie را اضافه کنیم تا با کلیک بر روی آن، به فرم ثبت و ویرایش فیلمها هدایت شویم. این فرم، از فیلدهای یک عنوان متنی، انتخاب ژانر از یک drop down list، تعداد موجود (بین 1 و 100) و امتیاز (بین صفر تا 10) تشکیل شدهاست. همچنین تا زمانیکه اعتبارسنجی فرم تکمیل نشدهاست، دکمهی submit فرم باید غیرفعال باقی بماند. پس از ذخیره شدن این فیلم (در لیست درون حافظهای برنامه)، با مراجعهی به لیست فیلمها و انتخاب آن از لیست (با کلیک بر روی لینک آن)، باید مجددا به همین فرم، در حالت ویرایش این رکورد هدایت شویم. به علاوه اگر در بالای صفحه یک id اشتباه وارد شد، باید صفحهی «پیدا نشد» نمایش داده شود.
کامپوننت MovieForm و مسیریابی آنرا در قسمت 17، تعریف و اضافه کردیم. برای تعریف لینکی به آن، به کامپوننت movies مراجعه کرده و بالای متنی که تعداد کل آیتمهای موجود در بانک اطلاعاتی را نمایش میدهد، المان زیر را اضافه میکنیم:
import { Link } from "react-router-dom"; // ... <div className="col"> <Link to="/movies/new" className="btn btn-primary" style={{ marginBottom: 20 }} > New Movie </Link> <p>Showing {totalCount} movies in the database.</p>
سپس به کامپوننت src\components\movieForm.jsx که پیشتر آنرا اضافه کرده بودیم، مراجعه کرده و به صورت زیر آنرا تکمیل میکنیم:
import Joi from "@hapi/joi"; import React from "react"; import { getGenres } from "../services/fakeGenreService"; import { getMovie, saveMovie } from "../services/fakeMovieService"; import Form from "./common/form"; class MovieForm extends Form { state = { data: { title: "", genreId: "", numberInStock: "", dailyRentalRate: "" }, genres: [], errors: {} };
- سپس این کامپوننت نیز از کامپوننت Form ارث بری میکند تا به امکانات ویژهی آن دسترسی پیدا کند.
- در ادامه در خاصیت state، طبق روالی که در کامپوننت فرم درنظر گرفتهایم، دو خاصیت data و errors باید حضور داشته باشند. در خاصیت data، شیءای که نام خاصیتهای آن با فیلدهای فرم تطابق دارد، ذکر شدهاند. در اینجا برای ذخیره سازی اطلاعات انتخاب شدهی از drop down list مرتبط با ژانرهای سینمایی، از خاصیت genreId استفاده میشود؛ این تنها اطلاعاتی است که از کل آیتمهای یک drop down list نیاز داریم. آرایهی genres که آیتمهای این drop down list را مقدار دهی میکند، در روال componentDidMount، از سرویس مرتبطی دریافت و مقدار دهی خواهد شد.
در ادامهی کدهای کامپوننت MovieForm، کدهای schema اعتبارسنجی شیء data را ملاحظه میکنید:
schema = { _id: Joi.string(), title: Joi.string() .required() .label("Title"), genreId: Joi.string() .required() .label("Genre"), numberInStock: Joi.number() .required() .min(0) .max(100) .label("Number in Stock"), dailyRentalRate: Joi.number() .required() .min(0) .max(10) .label("Daily Rental Rate") };
اکنون به مرحلهی componentDidMount میرسیم:
componentDidMount() { const genres = getGenres(); this.setState({ genres }); const movieId = this.props.match.params.id; if (movieId === "new") return; const movie = getMovie(movieId); if (!movie) return this.props.history.replace("/not-found"); this.setState({ data: this.mapToViewModel(movie) }); }
- پس از آن، نحوهی دریافت پارامتر id مسیریابی رسیده را ملاحظه میکنید. این id اگر به "new" تنظیم شده بود، یعنی قرار است، اطلاعات جدیدی ثبت شوند. بنابراین متد جاری را خاتمه میدهیم (چون کار ادامهی این متد، مقدار دهی اولیهی تمام فیلدهای فرم، بر اساس اطلاعات شیء دریافت شدهی از سرویس فیلمها است). در غیراینصورت (و با مشخص بودن id)، با استفاده از این id و متد getMovie سرویس src\services\fakeMovieService.js، سعی خواهیم کرد تا اطلاعات شیء movie متناظری را دریافت کنیم. اگر خروجی این متد null بود، یعنی id وارد شده معتبر نیست. به همین جهت کاربر را به صفحهی not-found هدایت میکنیم. اگر دقت کنید در اینجا بجای متد push، از متد replace استفاده کردهایم. چون اگر از متد push استفاده میکردیم و کاربر بر روی دکمهی back مرورگر کلیک میکرد، دوباره به همین صفحه، با id غیرمعتبر قبلی وارد میشد و یک حلقهی بیپایان رخ میداد. همچنین به return ای هم که به همراه متد replace استفاده شده، دقت کنید. کار redirect به یک صفحهی دیگر، به معنای عدم اجرای کدهای پس از آن نیست. بنابراین اگر میخواهیم کار این متد با redirect، به پایان برسد، ذکر return الزامی است.
- در پایان این متد، خاصیت data موجود در state را به روز رسانی میکنیم؛ تا سبب رندر فرم، با اطلاعات شیء movie یافت شده گردد و چون ساختار شیء movie دریافت شدهی از سرویس، با ساختار data تعریف شدهی در state یکی نیست، نیاز به نگاشت این دو به هم، توسط متد سفارشی mapToViewModel زیر است:
mapToViewModel(movie) { return { _id: movie._id, title: movie.title, genreId: movie.genre._id, numberInStock: movie.numberInStock, dailyRentalRate: movie.dailyRentalRate }; }
در ادامهی کدهای کامپوننت فرم فیلمها، به متد doSubmit میرسیم:
doSubmit = () => { saveMovie(this.state.data); this.props.history.push("/movies"); };
در انتهای این کامپوننت نیز به متد رندر آن میرسیم:
render() { return ( <div> <h1>Movie Form</h1> <form onSubmit={this.handleSubmit}> {this.renderInput("title", "Title")} {this.renderSelect("genreId", "Genre", this.state.genres)} {this.renderInput("numberInStock", "Number in Stock", "number")} {this.renderInput("dailyRentalRate", "Rate")} {this.renderButton("Save")} </form> </div> ); }
برای تعریف متد جدید renderSelect به این صورت عمل میکنیم:
- ابتدا فایل جدید src\components\common\select.jsx را ایجاد کرده و سپس آنرا جهت نمایش یک drop down list، ویرایش میکنیم:
import React from "react"; const Select = ({ name, label, options, error, ...rest }) => { return ( <div className="form-group"> <label htmlFor={name}>{label}</label> <select name={name} id={name} {...rest} className="form-control"> <option value="" /> {options.map(option => ( <option key={option._id} value={option._id}> {option.name} </option> ))} </select> {error && <div className="alert alert-danger">{error}</div>} </div> ); }; export default Select;
- پس از آن به کامپوننت src\components\common\form.jsx مراجعه کرده و متد رندر آنرا اضافه میکنیم:
import Select from "./select"; // ... class Form extends Component { // ... renderSelect(name, label, options) { const { data, errors } = this.state; return ( <Select name={name} value={data[name]} label={label} options={options} onChange={this.handleChange} error={errors[name]} /> ); } }
تمرین 3- جستجوی در لیست فیلمها
میخواهیم در بالای لیست نمایش فیلمها، یک search box را قرار دهیم تا توسط آن بتوان بر اساس عنوان وارد شده، در فیلمهای موجود جستجو کرد. همچنین این جستجو قرار است کلی بوده و حتی در صورت انتخاب ژانر خاصی از منوی کنار صفحه، باید در کل اطلاعات موجود جستجو کند. به علاوه اگر کاربر ژانری را انتخاب کرد، این text box باید خالی شود.
برای اینکار ابتدا فایل جدید src\components\searchBox.jsx را ایجاد کرده و به صورت زیر آنرا تکمیل میکنیم:
import React from "react"; const SearchBox = ({ value, onChange }) => { return ( <input type="text" name="query" className="form-control my-3" placeholder="Search..." value={value} onChange={e => onChange(e.currentTarget.value)} /> ); }; export default SearchBox;
سپس به کامپوننت movies مراجعه کرده و آنرا ذیل متن نمایش تعداد رکوردها، درج میکنیم:
<p>Showing {totalCount} movies in the database.</p> <SearchBox value={searchQuery} onChange={this.handleSearch} />
import SearchBox from "./searchBox"; //... class Movies extends Component { state = { //... selectedGenre: {}, searchQuery: "" }; handleSearch = query => { this.setState({ searchQuery: query, selectedGenre: null, currentPage: 1 }); }; handleGenreSelect = genre => { console.log("handleGenreSelect", genre); this.setState({ selectedGenre: genre, searchQuery: "", currentPage: 1 }); };
متد handleGenreSelect را نیز اندکی تغییر دادهایم تا اگر گروهی انتخاب شد، مقدار searchQuery را خالی کند. اگر در اینجا searchQuery را به نال تنظیم میکردیم، controlled component جعبهی جستجو، تبدیل به کامپوننت کنترل نشدهای میشد و در این حالت، React، اخطار تبدیل بین این دو را صادر میکرد.
در آخر، ابتدای متد getPageData هم جهت اعمال searchQuery، به صورت زیر تغییر میکند:
getPagedData() { const { pageSize, currentPage, selectedGenre, movies: allMovies, sortColumn, searchQuery } = this.state; let filteredMovies = allMovies; if (searchQuery) { filteredMovies = allMovies.filter(m => m.title.toLowerCase().startsWith(searchQuery.toLowerCase()) ); } else if (selectedGenre && selectedGenre._id) { filteredMovies = allMovies.filter(m => m.genre._id === selectedGenre._id); }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-21.zip