var formData = new FormData(); formData.append('Organizations', $('#Organizations').val()); formData.append('OrgNameInReports', $('#OrgNameInReports').val()); formData.append('CenterTypes', $('#CenterTypes').val()); formData.append('Users', $('#Users').val()); formData.append('Comments', $('#Comments').val()); formData.append('UserTypes', $('#UserTypes').val()); jQuery.each($('#profile-img')[0].files, function (i, file) { formData.append('picture-' + i, file); }); $.ajax({ url: 'SettingPages/OrgManagmentService.asmx/SaveNewOrgManager', type: 'POST', data: formData, dataType: 'json', contentType: false, processData: false, beforeSend: function () { Preloader(); }, complete: function (data) { RemovePreloader(); }, success: function (data, status) { data = JSON.stringify(data); console.log(data); }, error: function (data, status,e) { alert(status); } });
var image = document.createElement("img"); image.setAttribute("src", "logo.png"); React.createElement("img", { src : "logo.png" });
Virtual DOM
تفاوت در ساخت تگهای HTML به صورت مجازی بین JavaScript و React این است که React وضعیت تگهایی را که میسازد دنبال میکند. برای مثال فرض کنید نام سه محصول را در یک تگ < ul > نشان دادهایم. React وضعیت اصلی این تگ را که به مرورگر فرستاده، در حافظه دارد و همچنین در اثر تغییر منبع دادهای که برای < ul > مشخص کردهایم (که میتواند ورود اطلاعات به صورت Ajax باشد (مثلا اضافه شدن یک محصول جدید)) وضعیت جدیدی را برای تگ < ul > در حافظه ایجاد میکند. با وجود دو وضعیت برای یک تگ در حافظه، React میتواند تفاوت بین آنها را تشخیص داده و تگ را به روز کند. به این حالت عملکرد React ، اصطلاحا Virtual DOM میگویند.
React رابط کاربری را به صورت یک مدل میبیند و این مدل را با توجه به وضعیت اصلی آن در حافظه دوباره میسازد. برای React مهم نیست که ماهیت تغییر چیست. فقط وضعیتها را مثل دو عکس میبیند و میفهمد که آیا چیزی عوض شدهاست یا نه. دیالوگ React با مرورگر اینطور است: ای تگ < ul > این لیست را نشان بده (لیستی با سه محصول)، و بعد میگوید: ای تگ < ul > این لیست را نشان بده (لیستی با چهار محصول)!
کامپوننتهای React
<a href = “http://google.com”> <img src=”google.png”/> </a> // Components <clickableimage/> <linkimage/>
در کد بالا، بخش اول واضح است. عکسی که قابلیت کلیک شدن را دارد. حال فرض کنید یکی از کامپوننتهای <clickableimage/> یا <linkimage/>، همان تصویر قابل کلیک را ایجاد کنند. با نام گذاری واضح کامپوننتها، خوانایی برنامه بهتر میشود. یعنی میدانیم هر کامپوننت چه کاری را برای ما انجام میدهد. با این تصور که اگر تگهای زیاد و طولانی را در بخش رابط کاربری داریم، ارزش استفاده از کامپوننتهای React مشخص میشود.
قابلیت استفاده مجدد
در React کامپوننتها برای اساس توابع ساخته میشوند. یعنی وقتی یک کامپوننت را صدا بزنیم، در واقع یک تابع را اجرا میکنیم. در نتیجه کامپوننتها رفتار توابع را دارند؛ ورودی میگیرند و خروجی که یک DOM مجازی است را تحویل میدهند. اگر تابعی که مسئول ساخت کامپوننت است وابستگی به توابع یا متغیرهای بیرونی نداشته باشد، میتواند در جای دیگری از برنامه یا برنامهای دیگر مجددا استفاده شود. کد زیر نشان میدهد که چطور کامپوننتهای React ساخته میشوند.var ClickableImage = function(props) { return ( <a href={props.href}> <img src={props.src} /> </a> ); }; ReactDOM.render( <ClickableImage href="http://google.com" src="logo.png" />, document.getElementById("targetDivId"));
describe('myApp', function() { var scope; beforeEach(angular.mock.module('myApp')); beforeEach(angular.mock.inject(function($rootScope) { scope = $rootScope.$new(); }); it('...') });
describe('Remote tests', function() { var $httpBackend, $rootScope, myService; beforeEach(inject( function(_$httpBackend_, _$rootScope_, _myService_) { $httpBackend = _$httpBackend_; $rootScope = _$rootScope_; myService = _myService_; })); it('should make a request to the backend', function() { $httpBackend.expect('GET', '/v1/api/current_user') .respond(200, {userId: 123}); myService.getCurrentUser(); $httpBackend.flush(); }); });
»httpBackend$.expectGet
»httpBackend$.expectPost
»httpBackend$.expectDelete
»httpBackend$.expectJson
»httpBackend$.expectHead
»httpBackend$.expectPatch
Flush کردن سرویس httpBackend$ در پایان تست نیز برای همین مبحث async اجرا شدن سرویسهای http$backend است.
روش اول: دریافت اطلاعات سمت سرور به کمک درخواستهای Ajax
استفاده از Ajax یکی از روشهای کلاسیک دریافت اطلاعات سمت سرور در کدهای جاوا اسکریپتی است.
<script type="text/javascript"> var products = []; $(function() { $.getJSON("/home/products", function(response) { products = response.products; }); }); </script>
- مزایا: استفاده از Ajax، روشی بسیار متداول و شناخته شدهاست و به کمک انواع و اقسام روشهای بازگشت JSON از سرور، میتوان با آن کار کرد.
- معایب: درخواست Ajax، صرفا پس از بارگذاری اولیهی صفحه به سمت سرور ارسال خواهد شد و در این بین، کاربر وقفهای را مشاهده خواهد کرد. همچنین در اینجا بجای یک درخواست از سرور، حداقل دو درخواست باید ارسال شوند؛ یکی برای بارگذاری صفحهی اصلی و دیگری برای دریافت اطلاعات Ajax ایی از سرور به صورت غیرهمزمان.
روش دوم: دریافت اطلاعات از یک فایل جاوا اسکریپتی خارجی
اطلاعات سمت کاربر را از یک فایل جاوا اسکریپتی خارجی الحاق شدهی به صفحهی جاری نیز میتوان تهیه کرد:
<script src="/file.js"></script>
این روش نیز تقریبا مانند حالت یک درخواست Ajax ایی کار میکند و اطلاعات مورد نیاز را در طی یک درخواست جداگانه، پس از بارگذاری صفحهی اصلی، از سرور دریافت خواهد کرد. البته در حالت کار با Ajax، میتوان در طی یک callback، نتیجه را دریافت کرد و سپس عکس العمل نشان داد؛ اما در اینجا callback ایی وجود ندارد.
روش سوم: استفاده از SignalR
در SignalR ابتدا سعی میشود تا با استفاده از Web Sockets ارتباطی ماندگار بین کلاینت و سرور برقرار شود و سپس در این حالت، سرور میتواند مدام اطلاعاتی، مانند تغییرات دادههای خود را به سمت کاربر، جهت نمایش و یا محاسبات خاص خود ارسال کند. اگر حالت Web Socket میسر نباشد (توسط سرور یا کلاینت پشتیبانی نشود)، به حالتهای دیگری مانند server events, forever frames, long polling سوئیچ خواهد کرد. اطلاعات بیشتر
روش چهارم: قرار دادن اطلاعات سمت سرور در کدهای HTML صفحه
روش متداول دیگری جهت تامین اطلاعات جاوا اسکریپتی سمت کاربر، قرار دادن آنها در ویژگیهای data-* ارائه شده در HTML5 است.
<ul> @foreach (var product in products) { <li id="product@product.Id" data-rank="@product.Rank">@product.Name</li> } </ul>
اکنون برای دسترسی به مقدار data-rank سطری مانند product1، در کدهای جاوا اسکریپتی صفحه میتوان نوشت:
<script type="text/javascript"> var product1Rank = $("#product1").data("rank"); </script>
روش پنجم: قرار دادن اطلاعات سمت سرور در کدهای جاوا اسکریپتی صفحه
این روش همانند روش چهارم است، با این تفاوت که اینبار اطلاعات مورد نیاز، مستقیما به یک متغیر جاوا اسکریپتی انتساب داده شدهاست:
<script type="text/javascript"> var product1Name = "@product1.Name"; </script>
روش ششم: انتساب یک شیء دات نتی به یک متغیر جاوا اسکریپتی
این روش همانند روش پنجم است، با این تفاوت که اینبار قصد داریم بجای یک مقدار ثابت رشتهای یا عددی، برای مثال، آرایهای از اشیاء را به یک متغیر جاوا اسکریپتی انتساب دهیم. در اینجا ابتدا اطلاعات مورد نظر را به فرمت JSON تبدیل میکنیم:
//سمت سرور [HttpGet] public ActionResult Index() { var array = new[] { "Afghanistan", "Albania", "Algeria", "American Samoa", "Andorra", "Angola", "Anguilla", "Antarctica", "Antigua and/or Barbuda" }; ViewBag.JsonString = new JavaScriptSerializer().Serialize(array); return View(); }
//سمت کلاینت <script type="text/javascript"> var jsonArray = @Html.Raw(@ViewBag.JsonString); </script>
و یا اینکار را به صورت خلاصه به شکل زیر نیز میتوان در سمت کاربر انجام داد:
<script type="text/javascript"> var model = @Html.Raw(Json.Encode(Model)); // your js code here </script>
آموزش Knockout.Js #4
Click Binding روشی است برای اضافه کردن یک گرداننده رویداد در زمانی که قصد داریم یک تابع جاوااسکریپتی را در هنگام کلیک بر روی المان مورد نظر فراخوانی کنیم. از این مقید سازی عموما در عناصر button و input و تگ a استفاده میشود. اما در حقیقت در تمام عناصر غیر پنهان صفحه مورد استفاده قرار میگیرد.
<div> Number Of Clicks <span data-bind="text: numberOfClicks"></span> times <button data-bind="click: clickMe">Click me</button> </div> <script type="text/javascript"> var viewModel = { numberOfClicks : ko.observable(0), clickMe: function() { var previousCount = this.numberOfClicks(); this.numberOfClicks(previousCount + 1); } }; </script>
*نکته اول: اگر قصد داشته باشیم که عنصر جاری در viewModel را به گرداننده رویداد پاس دهیم چه باید کرد؟
هنگام فراخوانی رویدادها، KO به صورت پیش فرض مقدار جاری مدل را به عنوان اولین پارامتر به این گرداننده پاس میدهد. این روش مخصوصا در هنگامی که قصد اجرای عملیاتی خاص بر روی تک تک عناصر یک مجموعه را داشته باشید(مثل حلقه foreach) بسیار مفید خواهد بود.
<ul data-bind="foreach: places"> <li> <span data-bind="text: $data"></span> <button data-bind="click: $parent.removePlace">Remove</button> </li> </ul> <script type="text/javascript"> function MyViewModel() { var self = this; self.places = ko.observableArray(['Tehran', 'Esfahan', 'Shiraz']); self.removePlace = function(place) { self.places.remove(place) } } ko.applyBindings(new MyViewModel()); </script>
همان طور که پست قبل توضیح داده شد؛ برای اینکه بتوانیم از یک viewModel به مجموعه از عناصر در یک حلقه foreach مقید کنیم امکان استفاده از اشاره گر this میسر نیست. در نتیجه بهتر است در ابتدای viewModel مقدار این اشاره گر را در یک متغیر معمولی (در اینجا به نام self است) ذخیره کنیم و از این پس این متغیر را برای اشاره به عناصر viewModel به کار بریم. در اینجا self به عنواتن یک alias برای this خواهد بود.
*نکته دوم: دسترسی به عنصر رویداد
در بعضی مواقع نیاز است در حین فراخوانی رویداد ،عنصر رویداد DOM به عنوان فرستنده در اختیار تابع گرداننده قرار گیرد. خبر خوش این است که KO به صورت پیش فرض این عنصر را نیز به عنوان پارامتر دوم به توابع گرداننده رویداد پاس میدهد. برای مثال:
<button data-bind="click: myFunction"> Click me </button> <script type="text/javascript"> var viewModel = { myFunction: function(data, event) { if (event.shiftKey) { } else { } } }; ko.applyBindings(viewModel); </script>
*نکته سوم: به صورت پیش فرض KO از اجرای عملیات پیش فرض رویدادها جلوگیری به عمل میآورد. این به این معنی است که اگر برای رویداد کلیک تگ a بک تابع گرداننده تعریف کرده باشید، بعد از کلیک بر روی این المان؛ مرورگر فقط این تابع تعریف شده توسط شما را فراخوانی خواهد کرد و دیگر عملیات راهبری به صفحه مورد نظر در خاصیت href صورت نخواهد گرفت. اگر به هر دلیلی قصد داشته باشیم که این رفتار صورت نگیرد کافیست در انتهای تابع گرداننده رویداد مقدار true برگشت داده شود.
*نکته چهارم: مفهوم clickBubble
ابتدا به کد زیر دقت کنید:
<div data-bind="click: myDivHandler"> <button data-bind="click: myButtonHandler"> Click me </button> </div>
<div data-bind="click: myDivHandler"> <button data-bind="click: myButtonHandler, clickBubble: false"> Click me </button> </div>
نصب Swashbuckle (سوواَش باکِل)
اگر عبارت Swashbuckle.AspNetCore را در سایت NuGet جستجو کنیم، چندین بستهی مختلف مرتبط با آنرا خواهیم یافت. ما در این بین، بیشتر به این بستهها علاقمندیم:
- Swashbuckle.AspNetCore.Swagger: کار آن ارائهی خروجی OpenAPI تولیدی بر اساس ASP.NET Core API برنامهی ما، به صورت یک JSON Endpoint است.
- Swashbuckle.AspNetCore.SwaggerGen: کار آن ساخت Swagger document objects است؛ یا همان OpenAPI Specification.
عموما این دو بسته را با هم جهت ارائهی OpenAPI Specification استفاده میکنند.
- Swashbuckle.AspNetCore.SwaggerUI: این بسته، نگارش جایگذاری شدهی (embedded) ابزار swagger-UI را به همراه دارد. کار آن، ارائهی یک UI خودکار، بر اساس OpenAPI Specification است که از آن برای آزمایش API نیز میتوان استفاده کرد.
یک نکته: اگر صرفا بستهی Swashbuckle.AspNetCore را نصب کنیم، هر سه بستهی فوق را با هم دریافت خواهیم کرد و اگر از Visual Studio برای نصب آنها استفاده میکنید، انتخاب گزینهی Include prerelease را فراموش نکنید؛ از این جهت که قصد داریم از نگارش 5 آنها استفاده کنیم. چون این نگارش است که از OpenAPI 3x، پشتیبانی میکند. خلاصهی این موارد، افزودن PackageReference زیر به فایل پروژهی OpenAPISwaggerDoc.Web.csproj است و سپس اجرای دستور dotnet restore:
<Project Sdk="Microsoft.NET.Sdk.Web"> <ItemGroup> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc2" /> </ItemGroup> </Project>
تنظیم میانافزار Swashbuckle
پس از افزودن ارجاعی به Swashbuckle.AspNetCore، اکنون نوبت انجام تنظیمات میانافزارهای آن است. برای این منظور ابتدا به کلاس Startup و متد ConfigureServices آن مراجعه میکنیم:
namespace OpenAPISwaggerDoc.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { // ... services.AddSwaggerGen(setupAction => { setupAction.SwaggerDoc( name: "LibraryOpenAPISpecification", info: new Microsoft.OpenApi.Models.OpenApiInfo() { Title = "Library API", Version = "1", Description = "Through this API you can access authors and their books.", Contact = new Microsoft.OpenApi.Models.OpenApiContact() { Email = "name@site.com", Name = "DNT", Url = new Uri("https://www.dntips.ir") }, License = new Microsoft.OpenApi.Models.OpenApiLicense() { Name = "MIT License", Url = new Uri("https://opensource.org/licenses/MIT") } }); }); }
اکنون در متد Configure، میانافزار آنرا خواهیم افزود:
namespace OpenAPISwaggerDoc.Web { public class Startup { public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // ... app.UseHttpsRedirection(); app.UseSwagger(); // ... }
تا اینجا اگر برنامه را اجرا کنید، میتوان OpenAPI Specification تولیدی را در آدرس زیر یافت:
https://localhost:5001/swagger/LibraryOpenAPISpecification/swagger.json
در این آدرس، LibraryOpenAPISpecification، همان نامی است که در قسمت setupAction.SwaggerDoc تنظیم کردیم.
نگاهی به OpenAPI Specification تولیدی
در ابتدای swagger.json تولیدی، همانطور که در تصویر فوق نیز مشخص است، همان مشخصات ذکر شدهی در قسمت info متد setupAction.SwaggerDoc، قابل مشاهدهاست. سپس لیست مسیرهای این API مشخص شدهاند:
اینها مسیرهایی هستند که توسط دو کنترلر کتابها و نویسندگان برنامهی Web API ما عمومی شدهاند. در اینجا مقابل هر مسیر، تعداد آیتمهای متناظری نیز ذکر شدهاند. این موارد مرتبط هستند با HTTP methods پشتیبانی شده:
که هر کدام به همراه نام متدها و پارامترهای متناظر با آنها نیز میشوند. به علاوه نوع responseهای پشتیبانی شدهی توسط این متدها نیز ذکر شدهاند. هر کدام از خروجیها نیز نوع مشخصی دارند که توسط قسمت components -> schemas تصاویر فوق، جزئیات دقیق آنها بر اساس نوع مدلهای متناظر، استخراج و ارائه شدهاند.
مشکل: نوع Response تولیدی در OpenAPI Specification صحیح نیست
اگر به جزئیات مسیر /api/authors/{authorId} دقت کنیم، نوع response آنرا صرفا 200 یا Ok ذکر کردهاست؛ در حالیکه GetAuthor تعریف شده، حالت NotFound را نیز دارد:
[HttpGet("{authorId}")] public async Task<ActionResult<Author>> GetAuthor(Guid authorId) { var authorFromRepo = await _authorsService.GetAuthorAsync(authorId); if (authorFromRepo == null) { return NotFound(); } return Ok(_mapper.Map<Author>(authorFromRepo)); }
افزودن و راه اندازی Swagger UI
در ادامه میخواهیم یک رابط کاربری خودکار را بر اساس OpenAPI Specification تولیدی، ایجاد کنیم:
namespace OpenAPISwaggerDoc.Web { public class Startup { public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // ... app.UseHttpsRedirection(); app.UseSwagger(); app.UseSwaggerUI(setupAction => { setupAction.SwaggerEndpoint( "/swagger/LibraryOpenAPISpecification/swagger.json", "Library API"); }); // ... }
پس از این تنظیم اگر آدرس https://localhost:5001/swagger/index.html را در مرورگر باز کنیم، چنین خروجی قابل مشاهده خواهد بود:
و اگر بر روی هر کدام کلیک کنیم، ریز جزئیات آنها بر اساس OpenAPI Specification ای که بررسی کردیم، تولید شدهاست (از پارامترها تا نوع خروجی):
اکنون اگر بر روی دکمهی try it out آن نیز کلیک کنید، در همینجا میتوان این API را آزمایش کرد. برای مثال Controls Accept header را بر روی application/json قرار داده و سپس بر روی دکمهی execute که پس از کلیک بر روی دکمهی try it out ظاهر شدهاست، کلیک کنید تا بتوان خروجی Web API را مشاهده کرد.
در انتهای این صفحه، در قسمت schemas آن، مشخصات مدلهای بازگشت داده شدهی توسط Web API نیز ذکر شدهاند:
یک نکته: تغییر آدرس https://localhost:5001/swagger/index.html به ریشهی سایت
اگر علاقمند باشید تا زمانیکه برای اولین بار آدرس ریشهی سایت را در مسیر https://localhost:5001 باز میکنید، Swagger UI نمایان شود، میتوانید تنظیم RoutePrefix زیر را اضافه کنید:
app.UseSwaggerUI(setupAction => { setupAction.SwaggerEndpoint( "/swagger/LibraryOpenAPISpecification/swagger.json", "Library API"); setupAction.RoutePrefix = ""; });
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: OpenAPISwaggerDoc-02.zip
در قسمت بعد، به بهبود و غنی سازی جزئیات OpenAPI Specification تولیدی خواهیم پرداخت.
فرض کنید تعیین اعتبار یکی از فیلدهای فرم نیاز به انجام محاسباتی در سمت سرور دارد و اینکار را میخواهیم با استفاده از jQuery Ajax انجام دهیم. مشکلی که در اینجا وجود دارد، این است که A در Ajax به معنای asynchronous است. یعنی زمانیکه کاربر دکمه submit را فشرد، دیگر برنامه منتظر این نخواهد شد که پاسخ کامل دریافت شود ، سایر پردازشها صورت گیرد و سپس فرم را به سرور ارسال نماید (شبیه به ایجاد یک ترد جدید در برنامههای ویندوزی). مثال زیر را در نظر بگیرید:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="TestCustomValidation.aspx.cs"
Inherits="TestJQueryAjax.TestCustomValodation" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
<script src="js/jquery.js" type="text/javascript"></script>
<script type="text/javascript">
function validate() {
var number1 = $("#<%=txtNumber1.ClientID %>").val();
var number2 = $("#<%=txtNumber2.ClientID %>").val();
var result = false;
$.ajax({
type: "POST",
url: 'AjaxSrv.asmx/ValidateIt',
data: '{"number1":' + number1 + ',"number2":' + number2 + '}',
contentType: "application/json; charset=utf-8",
dataType: "json",
success:
function(msg) {
if (msg.d) {
result = true;
alert('بسیار خوب');
}
else {
result = false;
alert('دوباره سعی کنید');
}
},
error:
function(XMLHttpRequest, textStatus, errorThrown) {
result = false;
alert("خطایی رخ داده است");
}
});
//debugger;
return result;
}
</script>
</head>
<body>
<form id="form1" runat="server">
<div>
number 1 :
<asp:TextBox runat="server" ID="txtNumber1" />
<br />
number 2 :
<asp:TextBox runat="server" ID="txtNumber2" />
<br />
<asp:Button ID="btnSubmit" Text="Submit" UseSubmitBehavior="false" runat="server"
OnClientClick="if(!validate()){ return false;}" OnClick="btnSubmitClick" />
</div>
</form>
</body>
</html>
این مثال یک نوع اعتبار سنجی سفارشی را در حین submit با استفاده از وب سرویس زیر انجام میدهد (حاصلضرب دو عدد دریافتی را بررسی میکند که باید مساوی 10 باشند. البته هدف از این مثال ساده، آشنایی با نحوهی انجام این نوع عملیات است که میتواند شامل کار با دیتابیس و غیره هم باشد. و گرنه بدیهی است این بررسی را با دو سطر کد جاوا اسکریپتی نیز میشد انجام داد):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Services;
using System.Web.Script.Services;
namespace TestJQueryAjax
{
/// <summary>
/// Summary description for AjaxSrv
/// </summary>
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.ComponentModel.ToolboxItem(false)]
[ScriptService]
public class AjaxSrv : System.Web.Services.WebService
{
[WebMethod(EnableSession = true)]
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
public bool ValidateIt(int number1, int number2)
{
return number1 * number2 == 10;
}
}
}
راه حل چیست؟
راه حلهای فضایی بسیاری را در وب در این مورد میتوان پیدا کرد؛ اما راه حل استاندارد آن در این حالت ویژه، استفاده از Ajax در حالت غیرهمزمان است. یعنی این فریم ورک Ajax را وادار کنیم که تا پایان عملیات مورد نظر، منتظر بماند و سپس فرم را ارسال کند. برای این منظور تنها کافی است یک سطر زیر را پیش از فراخوانی تابع Ajax ، اضافه و فراخوانی نمائیم:
$.ajaxSetup({async: false}) ;
UseSubmitBehavior دکمه ما را به شکل زیر رندر میکند (دکمه به یک button معمولی (بجای حالت submit) تبدیل شده و سپس یک doPostBack را اضافه خواهد کرد):
<input id="btnSubmit" type="button" onclick="if(!validate()){ return false;};__doPostBack('btnSubmit','')" value="Submit" name="btnSubmit"/>
کدهای سمت سرور دریافت فایل PDF
using Microsoft.AspNetCore.Mvc; namespace BlazorWasmShowBinaryFiles.Server.Controllers { [ApiController] [Route("api/[controller]")] public class ReportsController : ControllerBase { [HttpGet("[action]")] public IActionResult GetPdfReport() { //TODO: create the `sample.pdf` report file on the server return File(virtualPath: "~/app_data/sample.pdf", contentType: "application/pdf", fileDownloadName: "sample.pdf"); } } }
ساخت URL برای دسترسی به اطلاعات باینری
تمام مرورگرهای جدید از ایجاد URL برای اشیاء Blob دریافتی از سمت سرور، توسط متد توکار URL.createObjectURL پشتیبانی میکنند. این متد، شیء URL را از شیء window جاری دریافت میکند و سپس اطلاعات باینری را دریافت کرده و آدرسی را جهت دسترسی موقت به آن تولید میکند. حاصل آن، یک URL ویژهاست مانند blob:https://localhost:5001/03edcadf-89fd-48b9-8a4a-e9acf09afd67 که گشودن آن در مرورگر، یا سبب نمایش آن تصویر و یا دریافت مستقیم فایل خواهد شد.
در برنامههای Blazor نیاز است اینکار را توسط JS Interop آن انجام داد؛ از این جهت که API تولید یک Blob URL، صرفا توسط کدهای جاوا اسکریپتی قابل دسترسی است. به همین جهت فایل جدید Client\wwwroot\site.js را با محتوای زیر ایجاد کرده و همچنین مدخل آنرا در به انتهای فایل Client\wwwroot\index.html، پیش از بسته شدن تگ body، اضافه میکنیم:
window.JsBinaryFilesUtils = { createBlobUrl: function (byteArray, contentType) { // The byte array in .NET is encoded to base64 string when it passes to JavaScript. const numArray = atob(byteArray) .split("") .map((c) => c.charCodeAt(0)); const uint8Array = new Uint8Array(numArray); const blob = new Blob([uint8Array], { type: contentType }); return URL.createObjectURL(blob); }, downloadFromUrl: function (fileName, url) { const anchor = document.createElement("a"); anchor.style.display = "none"; anchor.href = url; anchor.download = fileName; document.body.appendChild(anchor); anchor.click(); document.body.removeChild(anchor); }, downloadBlazorByteArray: function (fileName, byteArray, contentType) { const blobUrl = this.createBlobUrl(byteArray, contentType); this.downloadFromUrl(fileName, blobUrl); URL.revokeObjectURL(blobUrl); }, printFromUrl: function (url) { const iframe = document.createElement("iframe"); iframe.style.display = "none"; iframe.src = url; document.body.appendChild(iframe); if (iframe.contentWindow) { iframe.contentWindow.print(); } }, printBlazorByteArray: function (byteArray, contentType) { const blobUrl = this.createBlobUrl(byteArray, contentType); this.printFromUrl(blobUrl); URL.revokeObjectURL(blobUrl); }, showUrlInNewTab: function (url) { window.open(url); }, showBlazorByteArrayInNewTab: function (byteArray, contentType) { const blobUrl = this.createBlobUrl(byteArray, contentType); this.showUrlInNewTab(blobUrl); URL.revokeObjectURL(blobUrl); }, };
- زمانیکه در برنامههای Blazor با استفاده از متد ()HttpClient.GetByteArrayAsync آرایهای از بایتهای یک فایل باینری را دریافت میکنیم، ارسال آن به کدهای جاوااسکریپتی به صورت یک رشتهی base64 شده صورت میگیرد (JS Interop اینکار را به صورت خودکار انجام میدهد). به همین جهت در متد createBlobUrl روش تبدیل این رشتهی base64 دریافتی را به آرایهای از بایتها، سپس به یک Blob و در آخر به یک Blob URL، مشاهده میکنید. این Blob Url اکنون آدرس موقتی دسترسی به آرایهای از بایتهای دریافتی توسط مرورگر است. به همین جهت میتوان از آن به عنوان src بسیاری از اشیاء HTML استفاده کرد.
- متد downloadFromUrl، کار دریافت یک Url و سپس دانلود خودکار آنرا انجام میدهد. اگر به یک anchor استاندارد HTML، ویژگی download را نیز اضافه کنیم، با کلیک بر روی آن، بجای گشوده شدن این Url، مرورگر آنرا دریافت خواهد کرد. متد downloadFromUrl کار ساخت لینک و تنظیم ویژگیهای آن و سپس کلیک بر روی آنرا به صورت خودکار انجام میدهد. از متد downloadFromUrl زمانی استفاده کنید که منبع مدنظر، محافظت شده نباشد و Url آن به سادگی در مرورگر قابل گشودن باشد.
- متد downloadBlazorByteArray همان کار متد downloadFromUrl را انجام میدهد؛ با این تفاوت که Url مورد نیاز توسط متد downloadFromUrl را از طریق یک Blob Url تامین میکند.
- متد printFromUrl که جهت دسترسی به منابع محافظت نشده طراحی شدهاست، Url یک منبع را دریافت کرده، آنرا به یک iframe اضافه میکند و سپس متد print را بر روی این iframe به صورت خودکار فراخوانی خواهد کرد تا سبب ظاهر شدن صفحهی پیشنمایش چاپ شود.
- printBlazorByteArray همان کار متد printFromUrl را انجام میدهد؛ با این تفاوت که Url مورد نیاز توسط متد printFromUrl را از طریق یک Blob Url تامین میکند.
تهیهی متدهایی الحاقی جهت کار سادهتر با JsBinaryFilesUtils
پس از تهیهی JsBinaryFilesUtils فوق، میتوان با استفاده از کلاس زیر که به همراه متدهایی الحاقی جهت دسترسی به امکانات آن است، کار با متدهای دریافت، نمایش و چاپ فایلهای باینری را سادهتر کرد و از تکرار کدها جلوگیری نمود:
using System.Threading.Tasks; using Microsoft.JSInterop; namespace BlazorWasmShowBinaryFiles.Client.Utils { public static class JsBinaryFilesUtils { public static ValueTask<string> CreateBlobUrlAsync( this IJSRuntime JSRuntime, byte[] byteArray, string contentType) { return JSRuntime.InvokeAsync<string>("JsBinaryFilesUtils.createBlobUrl", byteArray, contentType); } public static ValueTask DownloadFromUrlAsync(this IJSRuntime JSRuntime, string fileName, string url) { return JSRuntime.InvokeVoidAsync("JsBinaryFilesUtils.downloadFromUrl", fileName, url); } public static ValueTask DownloadBlazorByteArrayAsync( this IJSRuntime JSRuntime, string fileName, byte[] byteArray, string contentType) { return JSRuntime.InvokeVoidAsync("JsBinaryFilesUtils.downloadBlazorByteArray", fileName, byteArray, contentType); } public static ValueTask PrintFromUrlAsync(this IJSRuntime JSRuntime, string url) { return JSRuntime.InvokeVoidAsync("JsBinaryFilesUtils.printFromUrl", url); } public static ValueTask PrintBlazorByteArrayAsync( this IJSRuntime JSRuntime, byte[] byteArray, string contentType) { return JSRuntime.InvokeVoidAsync("JsBinaryFilesUtils.printBlazorByteArray", byteArray, contentType); } public static ValueTask ShowUrlInNewTabAsync(this IJSRuntime JSRuntime, string url) { return JSRuntime.InvokeVoidAsync("JsBinaryFilesUtils.showUrlInNewTab", url); } public static ValueTask ShowBlazorByteArrayInNewTabAsync( this IJSRuntime JSRuntime, byte[] byteArray, string contentType) { return JSRuntime.InvokeVoidAsync("JsBinaryFilesUtils.showBlazorByteArrayInNewTab", byteArray, contentType); } } }
اصلاح Content Security Policy سمت سرور جهت ارائهی محتوای blob
پس از دریافت فایل PDF به صورت یک blob، با استفاده از متد URL.createObjectURL میتوان آدرس موقت محلی را برای دسترسی به آن تولید کرد و یک چنین آدرسهایی به صورت blob:http تولید میشوند. در این حالت در Content Security Policy سمت سرور، نیاز است امکان دسترسی به تصاویر و همچنین اشیاء از نوع blob را نیز آزاد معرفی کنید:
img-src 'self' data: blob: default-src 'self' blob: object-src 'self' blob:
نمایش فایل PDF دریافتی از سرور، به همراه دکمههای دریافت، چاپ و نمایش آن در صفحهی جاری
در ادامه کدهای کامل مرتبط با تصویری را که در ابتدای بحث مشاهده کردید، ملاحظه میکنید:
@page "/" @using BlazorWasmShowBinaryFiles.Client.Utils @inject IJSRuntime JSRuntime @inject HttpClient HttpClient <h1>Display PDF Files</h1> <button class="btn btn-info" @onclick="handlePrintPdf">Print PDF</button> <button class="btn btn-primary ml-2" @onclick="handleShowPdf">Show PDF</button> <button class="btn btn-success ml-2" @onclick="handleDownloadPdf">Download PDF</button> @if(!string.IsNullOrWhiteSpace(PdfBlobUrl)) { <section class="card mb-5 mt-3"> <div class="card-header"> <h4>using iframe</h4> </div> <div class="card-body"> <iframe title="PDF Report" width="100%" height="600" src="@PdfBlobUrl" type="@PdfContentType"></iframe> </div> </section> <section class="card mb-5"> <div class="card-header"> <h4>using object</h4> </div> <div class="card-body"> <object data="@PdfBlobUrl" aria-label="PDF Report" type="@PdfContentType" width="100%" height="100%"></object> </div> </section> <section class="card mb-5"> <div class="card-header"> <h4>using embed</h4> </div> <div class="card-body"> <embed aria-label="PDF Report" src="@PdfBlobUrl" type="@PdfContentType" width="100%" height="100%"> </div> </section> } @code { private const string ReportUrl = "/api/Reports/GetPdfReport"; private const string PdfContentType = "application/pdf"; private string PdfBlobUrl; private async Task handlePrintPdf() { // Note: Using the `HttpClient` is useful for accessing the protected API's by JWT's (non cookie-based authorization). // Otherwise just use the `PrintFromUrlAsync` method. var byteArray = await HttpClient.GetByteArrayAsync(ReportUrl); await JSRuntime.PrintBlazorByteArrayAsync(byteArray, PdfContentType); } private async Task handleDownloadPdf() { // Note: Using the `HttpClient` is useful for accessing the protected API's by JWT's (non cookie-based authorization). // Otherwise just use the `DownloadFromUrlAsync` method. var byteArray = await HttpClient.GetByteArrayAsync(ReportUrl); await JSRuntime.DownloadBlazorByteArrayAsync("report.pdf", byteArray, PdfContentType); } private async Task handleShowPdf() { // Note: Using the `HttpClient` is useful for accessing the protected API's by JWT's (non cookie-based authorization). // Otherwise just use the `ReportUrl` as the `src` of the `iframe` directly. var byteArray = await HttpClient.GetByteArrayAsync(ReportUrl); PdfBlobUrl = await JSRuntime.CreateBlobUrlAsync(byteArray, PdfContentType); } // Tips: // 1- How do I enable/disable the built-in pdf viewer of FireFox // https://support.mozilla.org/en-US/kb/disable-built-pdf-viewer-and-use-another-viewer // 2- How to configure browsers to use the Adobe PDF plug-in to open PDF files // https://helpx.adobe.com/acrobat/kb/pdf-browser-plugin-configuration.html // https://helpx.adobe.com/acrobat/using/display-pdf-in-browser.html // 3- Microsoft Edge is gaining new PDF reader features within the Windows 10 Fall Creator’s Update (version 1709). }
- پس از تهیهی JsBinaryFilesUtils و متدهای الحاقی متناظر با آن، اکنون تنها کافی است با استفاده از متد ()HttpClient.GetByteArrayAsync، فایل PDF ارائه شدهی توسط یک اکشن متد را به صورت آرایهای از بایتها دریافت و سپس به متدهای چاپ (PrintBlazorByteArrayAsync) و دریافت (DownloadBlazorByteArrayAsync) آن ارسال کنیم.
- در مورد نمایش آرایهای از بایتهای دریافتی، وضعیت کمی متفاوت است. ابتدا باید توسط متد CreateBlobUrlAsync، آدرس موقتی این آرایه را در مرورگر تولید کرد و سپس این آدرس را برای مثال به src یک iframe انتساب دهیم تا PDF را با استفاده از امکانات توکار مرورگر، نمایش دهد.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: BlazorWasmShowBinaryFiles.zip
using System; namespace CS9Features { class Program { static void Main(string[] args) { Console.WriteLine("Hello World!"); } } }
تبدیل قالب پیشفرض برنامههای کنسول به یک Top level program
در C# 9.0 میتوان تمام سطرهای فوق را به دو سطر زیر تقلیل داد و خلاصه کرد:
using System; Console.WriteLine("Hello World!");
روش استفاده از متدهای async در Top level programs
زمانیکه نقطهی آغازین برنامه را تبدیل به یک top level program کردیم، دیگر دسترسی مستقیمی را به متد Main نداریم تا آنرا async Task دار معرفی کنیم و پس از آن بتوانیم به سادگی با متدهای async کار کنیم. برای رفع این مشکل، کامپایلر فقط کافی است یک await را در قطعه کد شما پیدا کند. خودش به صورت خودکار متد Main غیرهمزمانی را جهت اجرای کدها، تشکیل میدهد. به همین جهت برای کار با کدهای async در اینجا، نیاز به تنظیم خاصی نیست و قطعه کد زیر که در آن متد MyMethodAsync را اجرا میکند، بدون مشکل کامپایل و اجرا خواهد شد:
using System; using System.Threading.Tasks; await MyMethodAsync(); Console.WriteLine("Hello World!"); static async Task MyMethodAsync() { await Task.Yield(); }
روش دسترسی به args در Top level programs
همانطور که در قطعه کد ابتدایی این مطلب مشخص است، متد Main به همراه پارامتر string[] args نیز هست. اما اکنون در Top level programs که فاقد متد Main هستند، چگونه میتوان به این آرگومانهای ارسالی توسط کاربر دسترسی یافت؟
پاسخ: پارامتر args نیز هنوز در اینجا قابل دسترسی است؛ فقط به ظاهر مخفی است:
using System; Console.WriteLine(args[0]);
بعضی از برنامههای کنسول در انتهای متد Main خود برای مثال return 0 و یا return 1 را دارند؛ که اولی به معنای موفقیت عملیات و دومی به معنای شکست عملیات است. در top level programs نیز میتوان این returnها را در انتهای کار قید کرد:
using System; Console.WriteLine($"Hello world!"); return 1;
// <Program>$ using System; using System.Runtime.CompilerServices; [CompilerGenerated] internal static class <Program>$ { private static int <Main>$(string[] args) { Console.WriteLine("Hello world!"); return 1; } }
امکان تعریف کلاسها و متدها در Top level programs
در تک فایل program.cs برنامه، در حین کار با Top level programs محدودیتی از لحاظ تعریف متدها، کلاسها و غیره نیست؛ یک مثال:
using System; var greeter = new Greeter(); var helloTeacher = greeter.Greet("teacher"); var helloStudents = SayHello("students"); Console.WriteLine(helloTeacher); Console.WriteLine(helloStudents); static string SayHello(string name) { return "Hello, " + name; } public class Greeter { public string Greet(string name) { return "Hello, " + name; } }
Top-level statements must precede namespace and type declarations. [CS9Features]csharp(CS8803)
سطوح دسترسی به کلاسها و متدهای تعریف شدهی در Top level programs
اگر قطعه کد مثال قبل را کامپایل کنیم، نمونهی دیکامپایل شدهی آن به صورت زیر است:
using System; using System.Runtime.CompilerServices; [CompilerGenerated] internal static class <Program>$ { private static void <Main>$(string[] args) { Greeter greeter = new Greeter(); string helloTeacher = greeter.Greet("teacher"); string helloStudents = SayHello("students"); Console.WriteLine(helloTeacher); Console.WriteLine(helloStudents); static string SayHello(string name) { return "Hello, " + name; } } }
نوع متدهای تعریف شدهی در Top level programs
مثال زیر را که یک top level program است، درنظر بگیرید:
using System; Foo(); var x = 3; int result = AddToX(4); Console.WriteLine(result); static void Foo() { Console.WriteLine("Foo"); } int AddToX(int y) { return x + y; }
پاسخ: متدهایی که در top level programs تعریف میشوند در حقیقت از نوع local functions هستند که در ابتدا در C# 7.0 معرفی شدند و سپس در C# 8.0 امکان تعریف نمونههای static آنها نیز میسر شد.
قطعه کد فوق در اصل به صورت زیر کامپایل میشود که متدهای AddToX و Foo در آن داخل متد Main تشکیل شده، به صورت local function تعریف شدهاند:
// <Program>$ using System; using System.Runtime.CompilerServices; [CompilerGenerated] internal static class <Program>$ { private static void <Main>$(string[] args) { Foo(); int x = 3; int result = AddToX(4); Console.WriteLine(result); int AddToX(int y) { return x + y; } static void Foo() { Console.WriteLine("Foo"); } } }
برپایی پیشنیازها
در اینجا نیز از همان برنامهای که در قسمت 30، برای بررسی مثالهای React hooks ایجاد کردیم، استفاده خواهیم کرد. فقط در آن، کتابخانهی Axios را نیز نصب میکنید. به همین جهت در ریشهی پروژهی React این قسمت، دستور زیر را در خط فرمان صادر کنید:
> npm install --save axios
در ادامه میخواهیم در برنامهی React خود، لیست مطالب برنامهی backend را از سرور دریافت کرده و نمایش دهیم. همچنین یک search box را به همراه دکمههای search و clear نیز به آن اضافه کنیم.
دریافت اطلاعات اولیه از سرور، درون useEffect Hook
پس از نصب پیشنیازها و راه اندازی برنامهی backend، در ابتدا فایل src\config.json را جهت درج مشخصات آدرس REST Api آن، ایجاد میکنیم:
{ "apiUrl": "https://localhost:5001/api" }
import React from "react"; import {apiUrl} from "../../config.json"; export default function App() { return <></>; }
import App from "./components/part03/Search";
import axios from "axios"; import React, { useEffect, useState } from "react"; import { apiUrl } from "../../config.json"; export default function App() { useEffect(() => { axios .get(apiUrl + "/posts/search?query=") .then(response => console.log(response.data)); }); return <></>; }
اکنون میخواهیم این اطلاعات دریافتی را در برنامهی خود نیز نمایش دهیم. به همین جهت نیاز است تا response.data را درون state کامپوننت جاری قرار داده و در حین رندر کامپوننت، با تشکیل حلقهای بر روی آن، اطلاعات نهایی را نمایش دهیم. بنابراین نیاز به useState Hook خواهیم داشت که ابتدا آنرا import کرده و سپس آنرا تعریف و در قسمت then، فراخوانی میکنیم:
import axios from "axios"; import React, { useEffect, useState } from "react"; import { apiUrl } from "../../config.json"; export default function App() { const [results, setResults] = useState([]); useEffect(() => { axios.get(apiUrl + "/posts/search?query=").then(response => { console.log(response.data); setResults(response.data); }); });
همانطور که مشاهده میکنید، یک حلقهی بی پایان در اینجا رخ دادهاست! برای پایان آن، مجبور خواهیم شد ابتدا کنسول اجرایی برنامهی React را به صورت دستی خاتمه داده و سپس مرورگر را نیز refresh کنیم تا این حلقه، خاتمه پیدا کند.
علت این مشکل را در قسمت 30 بررسی کردیم؛ effect method تابع useEffect (همان متد در برگیرندهی قطعه کدهای axios.get در اینجا)، پس از هربار رندر کامپوننت، یکبار دیگر نیز اجرا میشود. یعنی این متد، هر دو حالت componentDidMount و componentDidUpdate کامپوننتهای کلاسی را با هم پوشش میدهد و چون در اینجا setState را با فراخوانی متد setResults داریم، یعنی درخواست رندر مجدد کامپوننت انجام شدهاست و پس از آن، مجددا effect method فراخوانی میشود و ... این حلقه هیچگاه خاتمه نخواهد یافت. به همین جهت مرورگر و برنامه، هر دو با هم هنگ میکنند!
در این برنامه فعلا میخواهیم که فقط در حالت componentDidMount، کار درخواست اطلاعات از backend صورت گیرد. به همین جهت پارامتر دوم متد useEffect را با یک آرایهی خالی مقدار دهی میکنیم:
useEffect(() => { // ... }, []);
//... export default function App() { // ... return ( <> <table className="table"> <thead> <tr> <th>Title</th> </tr> </thead> <tbody> {results.map(post => ( <tr key={post.id}> <td>{post.title}</td> </tr> ))} </tbody> </table> </> ); }
استفاده ازAsync/Await برای دریافت اطلاعات، درون یک useEffect Hook
اکنون میخواهیم درون effect method یک useEffect Hook، روش قدیمی استفادهی از callbackها و متد then را برای دریافت اطلاعات، با روش جدیدتر async/await که در قسمت 23 آنرا بیشتر بررسی کردیم، جایگزین کنیم.
useEffect(async () => { const { data } = await axios.get(apiUrl + "/posts/search?query="); console.log(data); setResults(data); }, []);
Warning: An effect function must not return anything besides a function, which is used for clean-up. It looks like you wrote useEffect(async () => ...) or returned a Promise.
برای رفع این مشکل، روش توصیه شده، ایجاد یک تابع مجزای async و سپس فراخوانی آن درون effect function است:
useEffect(() => { getResults(); }, []); const getResults = async () => { const { data } = await axios.get(apiUrl + "/posts/search?query="); console.log(data); setResults(data); };
پیاده سازی componentDidUpdate با یک useEffect Hook، جهت انجام جستجوهای پویا
تا اینجا با اضافه کردن پارامتر دومی به متد useEffect، رویداد componentDidUpdate آنرا از کار انداختیم، تا برنامه با هربار فراخوانی setState و اجرای مجدد effect function، در یک حلقهی بینهایت وارد نشود. اکنون این سؤال مطرح میشود که اگر یک textbox را برای جستجوی در عناوین نمایش داده شده، در بالای جدول آن قرار دهیم، نیاز است با هربار تغییر ورودی آن، کار فراخوانی مجدد effect function صورت گیرد، تا بتوان نتایج جدیدتری را از سرور دریافت و به کاربر نشان داد؛ این مشکل را چگونه باید حل کرد؟
برای دریافت عبارت وارد شدهی توسط کاربر و جستجو بر اساس آن، ابتدا متغیر state و متد تنظیم آنرا با استفاده از useState Hook و یک مقدار اولیهی دلخواه تنظیم میکنیم:
export default function App() { // ... const [query, setQuery] = useState("Title");
<input type="text" name="query" className="form-control my-3" placeholder="Search..." onChange={event => setQuery(event.target.value)} value={query} />
اکنون که متغیر query دارای مقدار شدهاست، میتوان از آن در متد axios.get، به نحو زیر و با ارسال یک کوئری استرینگ به سمت سرور، استفاده کرد:
const { data } = await axios.get( `${apiUrl}/posts/search?query=${encodeURIComponent(query)}` );
تا اینجا اگر برنامه را ذخیره کرده و اجرا کنید، با تایپ در textbox جستجو، تغییری در نتایج حاصل نمیشود؛ چون effect function تعریف شده که سبب اجرای مجدد axios.get میشود، طوری تنظیم شدهاست که فقط یکبار، آنهم پس از رندر اولیهی کامپوننت، اجرا شود. برای رفع این مشکل، با مقدار دهی آرایهای که به عنوان پارامتر دوم متد useEffect تعریف شده، میتوان اجرای مجدد effect function آنرا وابستهی به تغییرات متغیر query در state کامپوننت کرد:
useEffect(() => { getResults(); }, [query]);
دریافت اطلاعات جستجو، تنها با ارسال اطلاعات یک فرم به سمت سرور
تا اینجا کاربر با هر حرفی که درون textbox جستجو وارد میکند، یک کوئری، به سمت سرور ارسال خواهد شد. برای کاهش آن میتوان یک دکمهی جستجو را در کنار این textbox قرار داد تا تنها پس از کلیک بر روی آن، این جستجو صورت گیرد.
برای پیاده سازی این قابلیت، ابتدا وابستگی به query را از متد useEffect حذف میکنیم، تا دیگر با تغییر اطلاعات textbox، متد callback آن اجرا نشود (پارامتر دوم آنرا مجددا به یک آرایهی خالی تنظیم میکنیم). سپس یک دکمه را که از نوع button است و رویداد onClick آن به getResults اشاره میکند، در بالای جدول نتایج مطالب، قرار میدهیم:
<button className="btn btn-primary" type="button" onClick={getResults} > Search </button>
<form onSubmit={handleSearch}> <div className="input-group my-3"> <label htmlFor="query" className="form-control-label sr-only"></label> <input type="text" id="query" name="query" className="form-control" placeholder="Search ..." onChange={event => setQuery(event.target.value)} value={query} /> <div className="input-group-append"> <button className="btn btn-primary" type="submit"> Search </button> </div> </div> </form>
const handleSearch = event => { event.preventDefault(); getResults(); };
افزودن قابلیت پاک کردن textbox جستجو و معرفی useRef Hook
در ادامه میخواهیم یک دکمهی جدید را در کنار دکمهی Search، اضافه کنیم تا با کلیک کاربر بر روی آن، نه فقط محتوای وارد شدهی در textbox پاک شود، بلکه focus نیز به آن منتقل گردد. برای پاک کردن textbox، فقط کافی است متد setQuery را با یک رشتهی خالی ارسالی به آن فراخوانی کنیم. اما برای انتقال focus به textbox، نیاز به داشتن ارجاع مستقیمی به آن المان وجود دارد که با مفهوم آن در قسمت 18 آشنا شدیم: «برای دسترسی به یک المان DOM در React، باید یک reference را به آن نسبت داد. برای این منظور یک خاصیت جدید را در سطح کلاس کامپوننت ایجاد کرده و آنرا با React.RefObject مقدار دهی اولیه کرده و سپس ویژگی ref المان مدنظر را به این RefObject تنظیم میکنیم». برای انجام یک چنین کاری در اینجا، Hook ویژهای به نام useRef معرفی شدهاست. بنابراین برای پیاده سازی این نیازمندیها، ابتدا دکمهی Clear را در کنار دکمهی Search قرار میدهیم:
<button type="button" onClick={handleClearSearch} className="btn btn-info" > Clear </button>
import React, { useEffect, useRef, useState } from "react"; // ... export default function App() { // ... const searchInputRef = useRef(); const handleClearSearch = () => { setQuery(""); searchInputRef.current.focus(); };
البته این searchInputRef برای اینکه دقیقا به textbox تعریف شده اشاره کند، باید آنرا به ویژگی ref المان، انتساب داد:
<input type="text" id="query" name="query" className="form-control" placeholder="Search ..." onChange={event => setQuery(event.target.value)} value={query} ref={searchInputRef} />
نمایش «لطفا منتظر بمانید» در حین دریافت اطلاعات از سرور
البته در اینجا با هر بار کلیک بر روی دکمهی جستجو، نتیجهی نهایی به سرعت نمایش داده میشود؛ اما اگر سرعت اتصال کاربر کمتر باشد، با یک وقفه این امر رخ میدهد. به همین جهت بهتر است یک پیام «لطفا منتظر بمانید» را در این حین به او نمایش دهیم. به همین جهت در ابتدا state مرتبطی را به کامپوننت اضافه میکنیم:
const [loading, setLoading] = useState(false);
const getResults = async () => { setLoading(true); const { data } = await axios.get( `${apiUrl}/posts/search?query=${encodeURIComponent(query)}` ); console.log(data); setResults(data); setLoading(false); };
{loading ? ( <div className="alert alert-info">Loading results...</div> ) : ( <table className="table"> <thead> <tr> <th>Title</th> </tr> </thead> <tbody> {results.map(post => ( <tr key={post.id}> <td>{post.title}</td> </tr> ))} </tbody> </table> )}
برای آزمایش آن میتوان سرعت اتصال را در برگهی شبکهی ابزارهای توسعه دهندگان مرورگر، تغییر داد:
مدیریت خطاها در حین اعمال async
آخرین امکانی را که به این مطلب اضافه خواهیم کرد، مدیریت خطاهای اعمال async است که با try/catch صورت میگیرد:
// ... export default function App() { // ... const [error, setError] = useState(null); // ... const getResults = async () => { setLoading(true); try { const { data } = await axios.get( `${apiUrl}/posts/search?query=${encodeURIComponent(query)}` ); console.log(data); setResults(data); } catch (err) { setError(err); } setLoading(false); };
{error && <div className="alert alert-warning">{error.message}</div>}
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-30-part-03-frontend.zip و sample-30-part-03-backend.zip