اشتراکها
مطالب
SignalR
چند وقتی هست که در کنار بدنه اصلی داتنت فریمورک چندین کتابخونه به صورت متنباز در حال توسعه هستند. این مورد در ASP.NET بیشتر فعاله و مثلا دو کتابخونه SignalR و WebApi توسط خود مایکروسافت توسعه داده میشه.
SignalR همونطور که در سایت بسیار خلاصه و مفید یک صفحهای! خودش توضیح داده شده (^) یک کتابخونه برای توسعه برنامههای وب «زمان واقعی»! (real-time web) است:
Async library for .NET to help build real-time, multi-user interactive web applications.
برنامههای زمان واقعی به صورت خلاصه و ساده بهصورت زیر تعریف میشن (^):
The real-time web is a set of technologies and practices that enable users to receive information as soon as it is published by its authors, rather than requiring that they or their software check a source periodically for updates.
یعنی کاربر سیستم ما بدون نیاز به ارسال درخواستی صریح! برای دریافت آخرین اطلاعات به روز شده در سرور، در برنامه کلاینتش از این تغییرات آگاه بشه. مثلا برنامههایی که برای نمایش نمودارهای آماری دادهها استفاده میشه (بورس، قیمت ارز و طلا و ...) و یا مهمترین مثالش میتونه برنامه «چت» باشه. متاسفانه پروتوکل HTTP مورد استفاده در وب محدودیتهایی برای پیادهسازی این گونه برنامهها داره. روشهای گوناگونی برای پیادهسازی برنامههای زمان واقعی در وب وجود داره که کتابخونه SignalR فعلا از موارد زیر استفاده میکنه:
- تکنولوژی جدید WebSocket (^) که خوشبختانه پشتیبانی کاملی از اون در دات نت 4.5 (چهار نقطه پنج! نه چهار و نیم!) وجود داره. اما تمام مرورگرها و تمام وب سرورها از این تکنولوژی پشتیبانی نمیکنند و تنها برخی نسخههای جدید قابلیت استفاده از آخرین ورژن WebSocket رو دارند که میشه به کروم 16 به بالا و فایرفاکس 11 به بالا و اینترنت اکسپلورر 10 اشاره کرد (برای استفاده از این تکنولوژی در ویندوز نیاز به IIS 8.0 است که متاسفانه فقط در ویندوز 8.0 موجوده):
Chrome 16, Firefox 11 and Internet Explorer 10 are currently the only browsers supporting the latest specification (RFC 6455). - یه روش دیگه Server-sent Events نام داره که دادههای جدید رو به فرم رویدادهای DOM به سمت کلاینت میفرسته(^).
- روش دیگهای که موجوده به Forever Frame معروفه که در این روش یک iframe مخفی درون کد html مسئول تبادل دادههاست. این iframe مخفی بهصورت یک بلاک Chunked (^) به سمت کلاینت فرستاده میشه. این iframe که مسئول رندر دادههای جدید در سمت کلاینت هست ارتباط خودش رو با سرور تا ابد! (برای همین بهش forever میگن) حفظ میکنه. هر وقت رویدادی سمت سرور رخ میده با استفاده از این روش دادهها بهصورت تگهای script به این فریم مخفی فرستاده میشوند و چون مرورگرها محتوای html رو به صورت افزایشی (incrementally) رندر میکنن بنابراین این اسکریپتها بهترتیب زمان دریافت اجرا میشوند. (البته ظاهرا عبارت forever frame در صنعت عکاسی! معروفتره بنابراین در جستجو در زمینه این روش ممکنه کمی مشکل داشته باشین) (^).
- روش آخر که در کتابخونه SignalR ازش استفاده میشه long-polling نام داره. در روش polling معمولی پس از ارسال درخواست توسط کلاینت، سرور بلافاصله نتیجه حاصله رو به سمت کلاینت میفرسته و ارتباط قطع میشه. بنابراین برای دادههای جدید درخواست جدیدی باید به سمت سرور فرستاده بشه که تکرار این روش باعث افزایش شدید بار بر روی سرور و کاهش کارآمدی اون میشه. اما در روش long-polling پس از برقراری ارتباط کلاینت با سرور این ارتباط تا مدت زمان معینی (که توسط یه مقدار تایم اوت مشخص میشه و مقدار پیشفرضش 2 دقیقه است) برقرار میمونه. بنابراین کلاینت میتونه بدون ایجاد مشکلی در کارایی، دادههای جدید رو از سرور دریافت کنه. به این روش در برنامهنویسی وب اصطلاحا برنامهنویسی کامت (Comet Programming) میگن (^ ^).
(البته روشهای دیگری هم برای پیادهسازی برنامههای زمان اجرا وجود داره مثل کتابخونه node.js که جستجوی بیشتر به خوانندگان واگذار میشه)
SignalR برای برقراری ارتباط ابتدا بررسی میکنه که آیا هر دو سمت سرور و کلاینت قابلیت پشتیبانی از WebSocket رو دارند. در غیراینصورت سراغ روش Server-sent Events میره. اگر باز هم موفق نشد سعی به برقراری ارتباط با روش forever frame میکنه و اگر باز هم موفق نشد در آخر سراغ long-polling میره.
با استفاده از SignalR شما میتونین از سرور، متدهایی رو در سمت کلاینت فراخونی کنین. یعنی درواقع با استفاده از کدهای سی شارپ میشه متدهای جاوااسکریپت سمت کلاینت رو صدا زد!
بطور خلاصه در این کتابخونه دو کلاس پایه وجود داره:
- کلاس سطح پایین PersistentConnection
- کلاس سطح بالای Hub
علت این نامگذاری به این دلیله که کلاس سطح پایین پیادهسازی پیچیدهتر و تنظیمات بیشتری نیاز داره اما امکانات بیشتری هم در اختیار برنامهنویس قرار میده.
خوب پس از این مقدمه نسبتا طولانی برای دیدن یک مثال ساده میتونین با استفاده از نوگت (Nuget) مثال زیر رو نصب و اجرا کنین (اگه تا حالا از نوگت استفاده نکردین قویا پیشنهاد میکنم که کار رو با دریافتش از اینجا آغاز کنین) :
PM> Install-Package SignalR.Sample
پس از کامل شدن نصب این مثال اون رو اجرا کنین. این یک مثال فرضی ساده از برنامه نمایش ارزش آنلاین سهام برخی شرکتهاست. میتونین این برنامه رو همزمان در چند مرورگر اجرا کنین و نتیجه رو مشاهده کنین.
حالا میریم سراغ یک مثال ساده. میخوایم یک برنامه چت ساده بنویسیم. ابتدا یک برنامه وب اپلیکیشن خالی رو ایجاد کرده و با استفاده از دستور زیر در خط فرمان نوگت، کتابخونه SignalR رو نصب کنین:
PM> Install-Package SignalR
پس از کامل شدن نصب این کتابخونه، ریفرنسهای زیر به برنامه اضافه میشن:
Microsoft.Web.Infrastructure Newtonsoft.Json SignalR SignalR.Hosting.AspNet SignalR.Hosting.Common
برای کسب اطلاعات مختصر و مفید از تمام اجزای این کتابخونه به اینجا مراجعه کنین.
همچنین اسکریپتهای زیر به پوشه Scripts اضافه میشن (این نسخهها مربوط به زمان نگارش این مطلب است):
jquery-1.6.4.js jquery.signalR-0.5.1.js
بعد یک کلاس با نام SimpleChat به برنامه اضافه و محتوای زیر رو در اون وارد کنین:
using SignalR.Hubs; namespace SimpleChatWithSignalR { public class SimpleChat : Hub { public void SendMessage(string message) { Clients.reciveMessage(message); } } }
دقت کنین که این کلاس از کلاس Hub مشتق شده و همچنین خاصیت Clients از نوع dynamic است. (در مورد جزئیات این کتابخونه در قسمتهای بعدی توضیحات مفصلتری داده میشه)
سپس یک فرم به برنامه اضافه کرده و محتوای زیر رو در اون اضافه کنین:
<input type="text" id="msg" /> <input type="button" value="Send" id="send" /><br /> <textarea id='messages' readonly="true" style="height: 200px; width: 200px;"></textarea> <script src="Scripts/jquery-1.6.4.min.js" type="text/javascript"></script> <script src="Scripts/jquery.signalR-0.5.1.min.js" type="text/javascript"></script> <script src="signalr/hubs" type="text/javascript"></script> <script type="text/javascript"> var chat = $.connection.simpleChat; chat.reciveMessage = function (msg) { $('#messages').val($('#messages').val() + "-" + msg + "\r\n"); }; $.connection.hub.start(); $('#send').click(function () { chat.sendMessage($('#msg').val()); }); </script>
همونطور که میبینین برنامه چت ما آماده شد! حالا برنامه رو اجرا کنین و با استفاده از دو مرورگر مختلف نتیجه رو مشاهده کنین.
نکته کلیدی کار SignalR در خط زیر نهفته است:
<script src="signalr/hubs" type="text/javascript"></script>
اگر محتوای آدرس فوق رو دریافت کنین میبینین که موتور این کتابخانه تمامی متدهای موردنیاز در سمت کلاینت رو با استفاده از کدهای جاوااسکریپت تولید کرده. البته در این کد تولیدی از نامگذاری camel Casing استفاده میشه، بنابراین متد SendMessage در سمت سرور بهصورت sendMessage در سمت کلاینت در دسترسه.
امیدوارم تا اینجا تونسته باشم علاقه شما به استفاده از این کتابخونه رو جلب کرده باشم. در قسمتهای بعد موارد پیشرفتهتر این کتابخونه معرفی میشه.
اگه علاقهمند باشین میتونین از این ویکی اطلاعات بیشتری بدست بیارین.
به روز رسانی
در دورهای به نام SignalR در سایت، به روز شدهای این مباحث را میتوانید مطالعه کنید.
به روز رسانی
در دورهای به نام SignalR در سایت، به روز شدهای این مباحث را میتوانید مطالعه کنید.
مطالب
Cookie - قسمت دوم
کوکی در جاوا اسکریپت
همانطور که در قسمت قبل اشاره کوتاهی شد، مدیریت کوکیهای در دسترس در وضعیت جاری، در جاوا اسکریپت ازطریق پراپرتی cookie از شی document امکانپذیر است. این پراپرتی کاری همانند هدرهای Set-Cookie و Cookie (که در قسمت قبل درباره آنها بحث شد) انجام میدهد. این پراپرتی یک مورد کاملا استثنایی و نسبتا عجیب در زبان جاوا اسکریپت است. در نگاه اول ظاهرا document.cookie از نوع رشته است، اما قضیه کاملا فرق میکند. برای روشن شدن مطلب به ادامه بحث توجه کنید.
افزودن کوکی
- برای افزودن یا ویرایش یک کوکی باید از ساختاری مانند ساختار هدر Set-Cookie که چیزی شبیه به عبارت زیر است، پیروی کرد:
document.cookie = "name=value; expires=date; domain=theDomain; path=thePath; secure";
نکته: با توجه به توضیحاتی که در قسمت قبل ارائه شد، بدیهی است که امکان ثبت یک کوکی با فلگ HttpOnly در جاوا اسکریپت وجود ندارد!
اجرای دستوری شبیه با ساختار نشان داده شده در بالا، موجب حذف کوکیهای قبلی نمیشود. از این دستور برای ایجاد یک کوکی و یا ویرایش یک کوکی موجود استفاده میشود. کوکیهای ایجادشده با این روش تفاوتی با کوکیهای ایجادشده توسط هدر Set-Cookie ندارند و همانند آنها در درخواستهای بعدی با توجه به خواص تنظیم شده، به سمت سرور ارسال خواهند شد.
همانطور که مشاهده میکنید خاصیتهای کوکی به صورت جفتهای نام-مقدار درون یک رشته به document.cookie نسبت داده میشوند. این خاصیتها توسط یک کاراکتر ; از یکدیگر جدا میشوند. شرح ساختار فوق در زیر آورده شده است:
1. همیشه اولین جفتِ نام-مقدار همانند مثال بالا باید «عنوان و مقدار» کوکی را مشخص سازد. این قسمت تنها عضو اجباری ساختار فوق است.
2. سپس یک سمیکالن و یک فاصله
3. تاریخ انقضا (expires) یا حداکثر طول عمر کوکی (max-age)
4. سپس یک سمیکالن و یک فاصله
5. دمین و یا مسیر مربوط به کوکی
6. سپس یک سمیکالن و یک فاصله
7. سایر خواص چون Secure
نکته: این ساختار عجیب معرفی شده را عینا رعایت کنید. بقیه کار توسط مرورگر انجام خواهد شد.
نکته: قسمتهای مختلف این ساختار case-sensitive نیست، البته بهجز نام کوکی که کاملا case-sensitive است.
مثلا برای ثبت یک کوکی با عنوان myCookie و مقدار myValue و دمین d.com و مسیر test و طول عمر 5 روزه باید از دستور زیر استفاده کرد:
document.cookie = 'myCookie=myValue; max-age=432000; domain=d.com; path=/test';
خواندن کوکی
- برای خواندن کوکیها تنها کافی است مقدار پراپرتی document.cookie بررسی شود. با اینکه از دستور نشان داده شده در بالا اینگونه برمی آید که پراپرتی document.cookie به رشته معرفی شده مقداردهی شده است، اما به محض خواندن این پراپرتی چیزی شبیه به عبارت زیر برگردانده میشود:
myCookie=myValue
از بقیه خواص اثری نیست! این رفتار به دلیل حفط امنیت کوکیها در تمام مرورگرها رعایت میشود.
- برای ثبت کوکی دیگری در وضعیت جاری کافی است یکبار دیگر دستور بالا را برای کوکی جدید به کار ببریم. مثلا به صورت زیر:
document.cookie = 'mySecondCookie=mySecondValue; path=/'
اینار یک کوکی سشنی بدون دمین و با مقدار / برای مسیر کوکی ثبت میشود! در این حالت کوکی قبلی دوباره نویسی و یا حذف نمیشود و تنها یک کوکی جدید به لیست کوکیهای مرورگر اضافه میشود! این رفتار عجیب از ویژگیهای جالب document.cookie است.
- اگر مقدار document.cookie در این حالت خوانده شود مقدار زیر برگشت داده میشود:
myCookie=myValue; mySecondCookie=mySecondValue
باز هم خبری از سایر خاصیتها نیست. ولی همانطور که میبینید کوکی دوم به لیست کوکیهای مرورگر اضافه شده است.
نکته: عبارت برگشت داده شده از پراپرتی document.cookie همانند مقداری است که در هدر Cookie هر درخواست توسط مرورگر گنجانده میشود، یعنی جفت نام-مقدار کوکیها به همراه یک ; و یک فاصله بین مقادیر هر کوکی. بنابراین برای بدست آوردن مقدار یک کوکی یکسری عملیات جهت Parse کردن دادههای آن نیاز است!
متدها
امروزه کتابخانههای متعددی با استفاده از زبان جاوا اسکریپت برای برنامه نویسی سمت کلاینت وجود دارد که بیشتر آنها قابلیتهایی برای کار با کوکیها نیز دارند. ازجمله میتوان به jQuery و YUI اشاره کرد. پلاگین مخصوص کوکیها در jquery در اینجا بحث شده است. برای کسب اطلاعات بیشتر درباره قابلیتهای کار با کوکی در YUI نیز به اینجا مراجعه کنید. مطالب زیر صرفا برای روشن شدن بحث ارائه میشوند. بدیهی است که برای کارهای عملی بهتر است از کتابخانههای موجود استفاده شود.
با توجه به اطلاعات بالا از متدهای زیر میتوان برای خواندن، افزودن و حذف کوکیها استفاده کرد.
نکته: متدهای زیر از ترکیب چندین ریفرنس مختلف بدست آمده است. هرچند برای موارد خاصتر میتوانند بیشتر سفارشی شوند.
افزودن و یا ویرایش کوکی
function setCookie(data, value) { if (typeof data === "string") { data = { name: data, value: value }; }; if (!data.name) throw "Cookie's name can not be null."; var cookie = escape(data.name) + "=" + escape(data.value); var expDate = null; if (data.expDays) { expDate = new Date(); expDate.setDate(expDate.getDate() + data.expDays); } else if (data.expYear && data.expMonth && data.expDay) { expDate = new Date(data.expYear, data.expMonth, data.expDay); } else if (data.expires) { expDate = data.expires; } else if (data.maxAge) { expDate = new Date(); expDate.setSeconds(expDate.getSeconds() + data.maxAge); } if (expDate != null) cookie += "; expires=" + expDate.toGMTString(); if (data.domain) cookie += "; domain=" + escape(data.domain); if (data.path) cookie += "; path=" + escape(data.path); if (data.secure) cookie += "; secure"; document.cookie = cookie; return document.cookie; }
در کد فوق برای انکد کردن رشتههای مورد استفاده از متد escape استفاده شده است. برای آشنایی با این متد به اینجا مراجعه کنید.
همچنین کار کردن با نوع داده تاریخ در جاوا اسکریپت کمی متفاوت است. بنابراین برای آشنایی بیشتر با این نوع داده به اینجا رجوع کنید.
نکته: در متد بالا بدلیل عدم پشتیبانی از خاصیت max-age در نسخههای قدیمی اینترنت اکسپلورر (نسخه 8 و قبل از آن) تنها از خاصیت expires استفاده شده است.
نحوه استفاده از متد بالا به صورت زیر است:
setCookie('cookie1', 'Value1'); setCookie({name:'cookie1', value:'Value1'}); setCookie({name:'cookie2', value:'Value2', expDays:10}); setCookie({name:'cookie3', value:'Value3', expires:new Date()}); setCookie({name:'cookie4', value:'Value4', expYear:2013, expMonth:0, expDay:13}); setCookie({name:'cookie3', value:'Value3', maxAge:365*24*60*60}); setCookie({name:'cookie5', value:'Value5', domain:'d.net', path:'/'}); setCookie({name:'cookie6', value:'Value6', secure:true}); setCookie({name:'cookie7', value:'Value7', expDays:100, domain:'dd.com', path:'/employee', secure:true});
حذف کوکی
همانطور که در قسمت قبل هم اشاره شد، برای حذف یک کوکی، کافی است تا تاریخ انقضای آن به تاریخی در گذشته مقداردهی شود. بنابراین برای اینکار میتوان از متد زیر استفاده کرد:
function delCookie(data) { if (typeof data === "string") { data = { name: data }; }; data.expDays = -1; return setCookie(data); }
delCookie('myCookie'); delCookie({ name: 'myCookie', domain: 'd.com', path: '/test' });
خواندن کوکی
برای خواندن مقدار یک کوکی میتوان از متد زیر استفاده کرد:
function getCookie(name) { var cookies = document.cookie.split(";"); for (var i = 0; i < cookies.length; i++) { var cookie = cookies[i].split("="); if (cookie[0].trim() == escape(name)) { return unescape(cookie[1].trim()); } } return null; }
برای آشنایی با متد unescape که در بالا از آن استفاده شده است به اینجا مراجعه کنید. در متد فوق از متد trim زیر استفاده شده است:
String.prototype.trim = function () { return this.replace(/^\s+|\s+$/g, ""); };
String.prototype.trimStart = function () { return this.replace(/^\s+/, ""); }; String.prototype.trimEnd = function () { return this.replace(/\s+$/, ""); };
روش استفاده شده برای خواندن مقادیر کوکیها در متد بالا بسیار ساده و ابتدایی است و صرفا برای آشنایی با نحوه Parse کردن رشته برگشت داده شده توسط document.cookie ارائه شده است. روشهای مناسبتر و مطمئنتر با یک جستجوی ساده در دسترس هستند. البته همانطور که قبلا هم اشاره شد، استفاه از کتابخانههای موجود راهحل بهتری است.
همچنین ازآنجاکه مقدار یک کوکی میتواند شامل کاراکتر = نیز باشد، بنابراین قسمت return متد فوق را میتوان به صورت زیر تغییر داد:
cookie.shift(1); return unescape(cookie.join('=').trim());
نکته: با توجه به مطالب ارائه شده در قسمت قبل بدست آوردن مقادیر کوکیها کمی پیچیدهتر از دیگر عملیاتهاست. ازآنجاکه راه مستقیمی با استفاده از جاوا اسکریپت برای یافتن سایر خواص کوکی وجود ندارد، بنابراین بدست آوردن مقدار دقیق کوکی موردنظر ممکن است غیرممکن باشد! (با توجه به اینکه کوکیهای متفاوت میتوانند نامهای یکسانی داشته باشند).
با توجه به نکته بالا، حال اگر با یک نام بخصوص، چندین کوکی ثبت شده باشد (با خواص متفاوت)، یکی از راهحلها این است که آرایهای از مقادیر این کوکیهای همنام برگشت داده شود. بنابراین متد فوق را میتوان به صورت زیر تکمیل کرد:
function getCookie(name) { var foundCookies = []; var cookies = document.cookie.split(";"); for (var i = 0; i < cookies.length; i++) { var cookie = cookies[i].split("="); if (cookie[0].trim() == escape(name) && cookie.length >= 2) { cookie.shift(1); foundCookies.push(unescape(cookie.join('=').trim())); } } return foundCookies.length > 1 ? foundCookies : foundCookies.length == 1 ? foundCookies[0] : null; }
خلاصهای از نحوه استفاده از متدهای بالا در IE8 (برای نمایش اجرای درست در مرورگری قدیمی!) در تصویر زیر نشان داده شده است:
نکته: کار توسعه این متدها را میتوان برای پشتیبانی از SubCookieها نیز ادامه داد، اما به دلیل دورشدن از مبحث اصلی، این موضوع در این مطلب ارائه نمیشود (درباره این نوع از کوکیها در قسمت قبل شرح کوتاهی داده شده است). اگر علاقهمند و نیازمند به این نوع کوکیها هستید، کتابخانه YUI پشتیبانی کاملی از آنها ارائه میکند.
در قسمت بعدی به نکات کار با کوکی در ASP.NET میپردازیم.
منابع:
برای توسعه سریع استفاده از Intellisense برای جاوا اسکریت در محیط ویژوال استودیو، که نحوه بهبود آن در این آدرس اشاره شده است، امری مهم و ضروری است
JavaScript is an important technology for development on many
different platforms, including web, mobile app, and server programming.
In Visual Studio 2013 we already support IntelliSense, Go to Definition,
colorization, and formatting of JavaScript source, along with several
other features. We’ve carried these forward into Visual Studio 2015 and we’ve improved the experience for JavaScript developers by focusing on three key areas:
- Improving the development experience when using popular JavaScript libraries
- Adding support for new JavaScript ECMAScript 2015 (also known as ES2015 and formerly ES6) language and web browser APIs
- Increasing your productivity in complex JavaScript code bases
نظرات مطالب
ASP.NET Web API - قسمت اول
دقت داشته باشید که Web API عرضه نشده تا WCF رو منسوخ کنه. برنامه هایی که صرفاً از بستر پروتوکل HTTP به عنوان یک سرویس برای رد و بدل کردن دادهها استفاده میکنند، بهتره که از این به بعد از Web API استفاده کنند. ضمن سادگی و مفاهیم آشنای ASP.NET MVC، روش یکپارچه ای برای ایجاد وب سرویسهای HTTP نیز به وجود اومده که مشکلات استفاده از WCF رو از بین میبره. WCF ذاتاً برای پیغامهای SOAP محور طراحی شده و به کار گرفتن اون برای وب سرویسهای HTTP یا به زور خوراندن HTTP به اون بی معنیه. در WCF راههای مختلفی برای ایجاد وب سرویسهای HTTP وجود داره که باعث گمراهی و سردرگمی توسعه گر میشه و حتی فریمورکهای مختلفی مانند OpenRasta و ServiceStack نیز بدین منظور وجود دارند. بنابراین پشتیبانی WCF از HTTP به یک پروژهی دیگه تحت نام ASP.NET Web API منتقل شده و WCF Web API دیگه پشتیبانی نمیشه. کمی تغییر نام و کمی جابجایی مفاهیم دراینجا صورت گرفته.
WCF همچنان قدرتمنده و نباید Web API به هیچ وجه به عنوان جایگزینی برای اون تصور بشه. ایجاد بسترهایی برای ارتباطات دو طرفه یا صفی از پیغامها یا سویچ بین کانالها در هنگام فعال نبودن یک کانال، اینها همه از قابلیتهایی هست که Web API هرگز جایگزینی برای اونها نخواهد بود و مختص WCF هستند.
- اگر احتمالا پروژهی پیوست را اجرا کرده باشید، مشاهده کردید که پس از لاگین، یک refresh کامل صورت میگیرد و مشکل خاصی از این لحاظ وجود ندارد (چندین بار هم آزمایش شد).
- در پروژهی سفارشی و خاص خودتان نیاز است هدرهای ارسالی به سمت سرور را در برگهی network ابزارهای توسعه دهندگان مرورگر، بررسی کنید. محتوای آن را همانند روشی که در نکتهی «نگاهی به محتوای JSON Web Token تولیدی » عنوان شد، دقیقا بررسی کنید که آیا به همراه claims مدنظر شما هست یا خیر؟ سمت سرور فقط بر اساس این محتوا هست که سشنی را ایجاد میکند. اگر این محتوا ناقص باشد، اطلاعات مدنظر هم قابل استخراج نخواهند بود. همچنین آیا سمت سرور عملیات اعتبارسنجی ثانویهای هم صورت میگیرد و آیا ساختار JWT ارسالی، اطلاعات مدنظر آنرا تامین میکند یا خیر. اینها را باید مرحله به مرحله دیباگ کنید.
- همچنین طول عمرهای توکنهای تولیدی را هم باید مدنظر داشته باشید. در مثال جاری، این عدد در فایل appsettings.json به 20 دقیقه تنظیم شده:
"BearerTokens": { "Key": "This is my shared key, not so secret, secret!", "Issuer": "https://localhost:5001/", "Audience": "Any", "AccessTokenExpirationMinutes": 20 }
Kendo UI به همراه یک ویجت وب مخصوص ارسال فایلها به سرور نیز هست. این ویجت قابلیت ارسال چندین فایل با هم را به صورت Ajax ایی دارا است و همچنین کاربران میتوانند فایلها را با کشیدن و رها کردن بر روی آن، به لیست فایلهای قابل ارسال اضافه کنند.
ارسال فایل Ajax ایی آن توسط HTML5 File API صورت میگیرد که در تمام مرورگرهای جدید پشتیبانی خوبی از آن وجود دارد. در مرورگرهای قدیمیتر، به صورت خودکار همان حالت متداول ارسال همزمان فایلها را فعال میکند (یا همان post back معمولی).
فعال سازی مقدماتی kendoUpload
ابتداییترین حالت کار با kendoUpload، فعال سازی حالت post back معمولی است؛ به شرح زیر:
در این حالت صرفا input با نوع file، با ظاهری سازگار با سایر کنترلهای Kendo UI به نظر میرسد و عملیات ارسال فایل، همانند قبل به همراه یک post back است. این روش برای حالتی مفید است که بخواهید یک فایل را به همراه سایر عناصر فرم در طی یک مرحله به سمت سرور ارسال کنید.
فعال سازی حالت ارسال فایل Ajax ایی kendoUpload
برای فعال سازی ارسال Ajax ایی فایلها در Kendo UI نیاز است خاصیت async آنرا به نحو ذیل مقدار دهی کرد:
در اینجا دو آدرس ذخیره سازی فایلها و همچنین حذف آنها را مشاهده میکنید. امضای این دو اکشن متد در ASP.NET MVC به صورت ذیل هستند:
در هر دو حالت، لیستی از فایلها توسط kendoUpload به سمت سرور ارسال میشوند. در حالت Save، محتوای این فایلها جهت ذخیره سازی بر روی سرور در دسترس خواهد بود. در حالت Remove، صرفا نام این فایلها برای حذف از سرور، توسط کاربر ارسال میشوند.
دو دکمهی حذف با کارکردهای متفاوت در ویجت kendoUpload وجود دارند. در ابتدای کار، پیش از ارسال فایلها به سرور:
کلیک بر روی دکمهی حذف در این حالت، صرفا فایلی را از لیست سمت کاربر حذف میکند.
پس از ارسال فایلها به سرور:
اما پس از پایان عملیات ارسال، اگر کاربر بر روی دکمهی حذف کلیک کند، توسط آدرس مشخص شده توسط خاصیت removeUrl، نام فایلهای مورد نظر، برای حذف از سرور ارسال میشوند.
چند نکتهی تکمیلی
- تنظیم خاصیت autoUpload به true سبب میشود تا پس از انتخاب فایلها توسط کاربر، بلافاصله و به صورت خودکار عملیات ارسال فایلها به سرور آغاز شوند. اگر به false تنظیم شود، دکمهی ارسال فایلها در پایین لیست نمایش داده خواهد شد.
- شاید علاقمند باشید تا removeVerb را به DELETE تغییر دهید؛ بجای POST. به همین منظور میتوان خاصیت removeVerb در اینجا مقدار دهی کرد.
- با تنظیم خاصیت multiple به true، کاربر قادر خواهد شد تا توسط صفحهی دیالوگ انتخاب فایلها، قابلیت انتخاب بیش از یک فایل را داشته باشد.
- showFileList نمایش لیست فایلها را سبب میشود.
تعیین پسوند فایلهای صفحهی انتخاب فایلها
هنگامیکه کاربر بر روی دکمهی انتخاب فایلها برای ارسال کلیک میکند، در صفحهی دیالوگ باز شده میتوان پسوندهای پیش فرض مجاز را نیز تعیین کرد.
برای این منظور تنها کافی است ویژگی accept را به input از نوع فایل اضافه کرد. چند مثال در این مورد:
نمایش متن کشیدن و رها کردن، بومی سازی برچسبها و نمایش راست به چپ
همانطور که در تصاویر فوق ملاحظه میکنید، نمایش این ویجت راست به چپ و پیامهای آن نیز ترجمه شدهاند.
برای راست به چپ سازی آن مانند قبل تنها کافی است input مرتبط، در یک div با کلاس k-rtl محصور شود:
برای بومی سازی پیامهای آن میتوان مانند مثال ذیل، خاصیت localization را مقدار دهی کرد:
به علاوه متن dropFilesHere به صورت پیش فرض نامرئی است. برای نمایش آن نیاز است CSS موجود را بازنویسی کرد تا em مرتبط مرئی شود:
تغییر قالب نمایش لیست فایلها
لیست فایلها در ویجت kendoUpload دارای یک قالب پیش فرض است که امکان بازنویسی کامل آن وجود دارد. ابتدا نیاز است یک kendo-template را بر این منظور تدارک دید:
و سپس برای استفاده از آن خواهیم داشت:
در این قالب، مقدار size هر فایل نیز در کنار نام آن نمایش داده میشود.
رخدادهای ارسال فایلها
افزونهی kendoUpload در حالت ارسال Ajax ایی فایلها، رخدادهایی مانند شروع به ارسال، موفقیت، پایان، درصد ارسال فایلها و امثال آنرا نیز به همراه دارد که لیست کامل آنها را در ذیل مشاهده میکنید:
ارسال متادیتای اضافی به همراه فایلهای ارسالی
فرض کنید میخواهید به همراه فایلهای ارسالی به سرور، پارامتر codeId را نیز ارسال کنید. برای این منظور باید خاصیت e.data رویداد upload را به نحو ذیل مقدار دهی کرد:
سپس در سمت سرور، امضای متد Save بر اساس پارامترهای تعریف شده در سمت کاربر، به نحو ذیل تغییر میکند:
فعال سازی ارسال batch
اگر در متد Save سمت سرور یک break point قرار دهید، مشاهده خواهید کرد که به ازای هر فایل موجود در لیست در سمت کاربر، یکبار متد Save فراخوانی میشود و عملا متد Save، لیستی از فایلها را در طی یک فراخوانی دریافت نمیکند. برای فعال سازی این قابلیت تنها کافی است خاصیت batch را به true تنظیم کنیم:
به این ترتیب دیگر لیست فایلها به صورت مجزا در سمت کاربر نمایش داده نمیشود و تمام آنها با یک کاما از هم جدا خواهند شد. همچنین دیگر شاهد نمایش درصد پیشرفت تکی فایلها نیز نخواهیم بود و اینبار درصد پیشرفت کل batch گزارش میشود.
در یک چنین حالتی باید دقت داشت که تنظیم maxRequestLength در web.config برنامه الزامی است؛ زیرا به صورت پیش فرض محدودیت 4 مگابایتی ارسال فایلها توسط ASP.NET اعمال میشود:
ارسال فایل Ajax ایی آن توسط HTML5 File API صورت میگیرد که در تمام مرورگرهای جدید پشتیبانی خوبی از آن وجود دارد. در مرورگرهای قدیمیتر، به صورت خودکار همان حالت متداول ارسال همزمان فایلها را فعال میکند (یا همان post back معمولی).
فعال سازی مقدماتی kendoUpload
ابتداییترین حالت کار با kendoUpload، فعال سازی حالت post back معمولی است؛ به شرح زیر:
<form method="post" action="submit" enctype="multipart/form-data"> <div> <input name="files" id="files" type="file" /> <input type="submit" value="Submit" class="k-button" /> </div> </form> <script> $(document).ready(function() { $("#files").kendoUpload(); }); </script>
فعال سازی حالت ارسال فایل Ajax ایی kendoUpload
برای فعال سازی ارسال Ajax ایی فایلها در Kendo UI نیاز است خاصیت async آنرا به نحو ذیل مقدار دهی کرد:
<script type="text/javascript"> $(function () { $("#files").kendoUpload({ name: "files", async: { // async configuration saveUrl: "@Url.Action("Save", "Home")", // the url to save a file is '/save' removeUrl: "@Url.Action("Remove", "Home")", // the url to remove a file is '/remove' autoUpload: false, // automatically upload files once selected removeVerb: 'POST' }, multiple: true, showFileList: true }); }); </script>
[HttpPost] public ActionResult Save(IEnumerable<HttpPostedFileBase> files) { if (files != null) { // ... // Process the files and save them // ... } // Return an empty string to signify success return Content(""); } [HttpPost] public ContentResult Remove(string[] fileNames) { if (fileNames != null) { foreach (var fullName in fileNames) { // ... // delete the files // ... } } // Return an empty string to signify success return Content(""); }
دو دکمهی حذف با کارکردهای متفاوت در ویجت kendoUpload وجود دارند. در ابتدای کار، پیش از ارسال فایلها به سرور:
کلیک بر روی دکمهی حذف در این حالت، صرفا فایلی را از لیست سمت کاربر حذف میکند.
پس از ارسال فایلها به سرور:
اما پس از پایان عملیات ارسال، اگر کاربر بر روی دکمهی حذف کلیک کند، توسط آدرس مشخص شده توسط خاصیت removeUrl، نام فایلهای مورد نظر، برای حذف از سرور ارسال میشوند.
چند نکتهی تکمیلی
- تنظیم خاصیت autoUpload به true سبب میشود تا پس از انتخاب فایلها توسط کاربر، بلافاصله و به صورت خودکار عملیات ارسال فایلها به سرور آغاز شوند. اگر به false تنظیم شود، دکمهی ارسال فایلها در پایین لیست نمایش داده خواهد شد.
- شاید علاقمند باشید تا removeVerb را به DELETE تغییر دهید؛ بجای POST. به همین منظور میتوان خاصیت removeVerb در اینجا مقدار دهی کرد.
- با تنظیم خاصیت multiple به true، کاربر قادر خواهد شد تا توسط صفحهی دیالوگ انتخاب فایلها، قابلیت انتخاب بیش از یک فایل را داشته باشد.
- showFileList نمایش لیست فایلها را سبب میشود.
تعیین پسوند فایلهای صفحهی انتخاب فایلها
هنگامیکه کاربر بر روی دکمهی انتخاب فایلها برای ارسال کلیک میکند، در صفحهی دیالوگ باز شده میتوان پسوندهای پیش فرض مجاز را نیز تعیین کرد.
برای این منظور تنها کافی است ویژگی accept را به input از نوع فایل اضافه کرد. چند مثال در این مورد:
<!-- Content Type with wildcard. All Images --> <input type="file" id="demoFile" title="Select file" accept="image/*" /> <!-- List of file extensions --> <input type="file" id="demoFile" title="Select file" accept=".jpg,.png,.gif" /> <!-- Any combination of the above --> <input type="file" id="demoFile" title="Select file" accept="audio/*,application/pdf,.png" />
نمایش متن کشیدن و رها کردن، بومی سازی برچسبها و نمایش راست به چپ
همانطور که در تصاویر فوق ملاحظه میکنید، نمایش این ویجت راست به چپ و پیامهای آن نیز ترجمه شدهاند.
برای راست به چپ سازی آن مانند قبل تنها کافی است input مرتبط، در یک div با کلاس k-rtl محصور شود:
<div class="k-rtl k-header"> <input name="files" id="files" type="file" /> </div>
<script type="text/javascript"> $(function () { $("#files").kendoUpload({ name: "files", async: { //... }, //... localization: { select: 'انتخاب فایلها برای ارسال', remove: 'حذف فایل', retry: 'سعی مجدد', headerStatusUploading: 'در حال ارسال فایلها', headerStatusUploaded: 'پایان ارسال', cancel: "لغو", uploadSelectedFiles: "ارسال فایلها", dropFilesHere: "فایلها را برای ارسال، کشیده و در اینجا رها کنید", statusUploading: "در حال ارسال", statusUploaded: "ارسال شد", statusWarning: "اخطار", statusFailed: "خطا در ارسال" } }); }); </script>
<style type="text/css"> div.k-dropzone { border: 1px solid #c5c5c5; /* For Default; Different for each theme */ } div.k-dropzone em { visibility: visible; } </style>
تغییر قالب نمایش لیست فایلها
لیست فایلها در ویجت kendoUpload دارای یک قالب پیش فرض است که امکان بازنویسی کامل آن وجود دارد. ابتدا نیاز است یک kendo-template را بر این منظور تدارک دید:
<script id="fileListTemplate" type="text/x-kendo-template"> <li class='k-file'> <span class='k-progress'></span> <span class='k-icon'></span> <span class='k-filename' title='#=name#'>#=name# (#=size# bytes)</span> <strong class='k-upload-status'></strong> </li> </script>
<script type="text/javascript"> $(function () { $("#files").kendoUpload({ name: "files", async: { // ... }, // ... template: kendo.template($('#fileListTemplate').html()), // ... }); }); </script>
رخدادهای ارسال فایلها
افزونهی kendoUpload در حالت ارسال Ajax ایی فایلها، رخدادهایی مانند شروع به ارسال، موفقیت، پایان، درصد ارسال فایلها و امثال آنرا نیز به همراه دارد که لیست کامل آنها را در ذیل مشاهده میکنید:
<script type="text/javascript"> $(function () { $("#files").kendoUpload({ name: "files", async: { // async configuration //... }, //... localization: { }, cancel: function () { console.log('Cancel Event.'); }, complete: function () { console.log('Complete Event.'); }, error: function () { console.log('Error uploading file.'); }, progress: function (e) { console.log('Uploading file ' + e.percentComplete); }, remove: function () { console.log('File removed.'); }, select: function () { console.log('File selected.'); }, success: function () { console.log('Upload successful.'); }, upload: function (e) { console.log('Upload started.'); } }); }); </script>
ارسال متادیتای اضافی به همراه فایلهای ارسالی
فرض کنید میخواهید به همراه فایلهای ارسالی به سرور، پارامتر codeId را نیز ارسال کنید. برای این منظور باید خاصیت e.data رویداد upload را به نحو ذیل مقدار دهی کرد:
<script type="text/javascript"> $(function () { $("#files").kendoUpload({ name: "files", async: { //... }, //... localization: { }, upload: function (e) { console.log('Upload started.'); // Sending metadata to the save action e.data = { codeId: "1234567", param2: 12 //, ... }; } }); }); </script>
[HttpPost] public ActionResult Save(IEnumerable<HttpPostedFileBase> files, string codeId)
فعال سازی ارسال batch
اگر در متد Save سمت سرور یک break point قرار دهید، مشاهده خواهید کرد که به ازای هر فایل موجود در لیست در سمت کاربر، یکبار متد Save فراخوانی میشود و عملا متد Save، لیستی از فایلها را در طی یک فراخوانی دریافت نمیکند. برای فعال سازی این قابلیت تنها کافی است خاصیت batch را به true تنظیم کنیم:
<script type="text/javascript"> $(function () { $("#files").kendoUpload({ name: "files", async: { // .... batch: true }, }); }); </script>
در یک چنین حالتی باید دقت داشت که تنظیم maxRequestLength در web.config برنامه الزامی است؛ زیرا به صورت پیش فرض محدودیت 4 مگابایتی ارسال فایلها توسط ASP.NET اعمال میشود:
<?xml version="1.0" encoding="utf-8"?> <configuration> <system.web> <!-- The request length is in kilobytes, execution timeout is in seconds --> <httpRuntime maxRequestLength="10240" executionTimeout="120" /> </system.web> <system.webServer> <security> <requestFiltering> <!-- The content length is in bytes --> <requestLimits maxAllowedContentLength="10485760"/> </requestFiltering> </security> </system.webServer> </configuration>
نظرات مطالب
OpenCVSharp #6
2 نکته و یک تجربه کوچک درباره نمایش ویدیو با خواندن اطلاعات از WebCam :
-اول اینکه اگر خواستید لیست از وب کمهای سیستم تون داشته باشید از کد زیر استفاده کنید (البته برای استفاده از آن به DirectShow.Net dll نیاز دارید)
-دوم اینکه برای نسبت دادن وب کم به CvCapture از متد CvCapture.FromCamera(cameraIndex) استفاده میکنیم :
این رو هم بگم که همین روش رو با بکارگیری محصور کننده Emgu انجام دادم و سرعت پایینتری نسبت به OpenCvSharp داشت.
و یک سوال : چرا در حین کار با وب کم مقدار خروجی capture.Fps یا همان frames per second مقدار صفر را بر میگرداند؟
-اول اینکه اگر خواستید لیست از وب کمهای سیستم تون داشته باشید از کد زیر استفاده کنید (البته برای استفاده از آن به DirectShow.Net dll نیاز دارید)
private void LoadCameras() { List<string> data = new List<string>(); List<KeyValuePair<int, string>> ListCamerasData = new List<KeyValuePair<int, string>>(); //-> Find systems cameras with DirectShow.Net dll DsDevice[] _SystemCamereas = DsDevice.GetDevicesOfCat(FilterCategory.VideoInputDevice); int _DeviceIndex = 0; foreach (DirectShowLib.DsDevice _Camera in _SystemCamereas) { ListCamerasData.Add(new KeyValuePair<int, string>(_DeviceIndex, _Camera.Name)); data.Add(_Camera.Name); _DeviceIndex++; } CameraList.ItemsSource = data; }
using (CvCapture capture = CvCapture.FromCamera(cameraIndex)) { //var interval = (int)(1000 / capture.Fps); IplImage image; while (_worker != null && !_worker.CancellationPending) { if ((image = capture.QueryFrame()) != null) { _worker.ReportProgress(0, image); Thread.Sleep(10); } } }
این رو هم بگم که همین روش رو با بکارگیری محصور کننده Emgu انجام دادم و سرعت پایینتری نسبت به OpenCvSharp داشت.
و یک سوال : چرا در حین کار با وب کم مقدار خروجی capture.Fps یا همان frames per second مقدار صفر را بر میگرداند؟
در گریدی که تا به اینجا طراحی کردیم، اگر قرار باشد بجای جدول فیلمها، جدول مشتریها نمایش داده شود، چکار باید کرد؟ با پیاده سازی فعلی، باید کل تعاریف MoviesTable را در کامپوننت دیگری مانند CustomersTable تکرار کنیم. به همین جهت برای پویاسازی تعاریف ستونها نیاز است این قسمت را از جدول اصلی جدا کرده و به کامپوننت مستقلی مانند tableHeader منتقل کنیم.
ایجاد کامپوننت جدید tableHeader
برای پویاسازی تعاریف ستونها و همچنین کم کردن مسئولیتهای کامپوننت MoviesTable، فایل جدید src\components\common\tableHeader.jsx را ایجاد میکنیم تا در برگیرندهی کامپوننت جدید TableHeader شود. پس از ایجاد این فایل، با استفاده از میانبرهای imrc و cc، ساختار ابتدایی کامپوننت TableHeader را تشکیل میدهیم. سپس به کامپوننت MoviesTable بازگشته و متد raiseSort آنرا cut و به اینجا منتقل میکنیم. همچنین نیاز است کل thead جدول فیلمها را نیز به اینجا منتقل کنیم. اما چون میخواهیم این تعاریف پویا باشند، باید امکان تعریف پویای ستونها را نیز به آن اضافه کنیم. بنابراین اینترفیس این کامپوننت به صورت زیر است:
- ورودیهای آن: آرایهی ستونهای جدول و همچنین شیء sortColumn و رخداد onSort که در متد raiseSort استفاده میشوند.
با این توضیحات، کامپوننت TableHeader چنین شکلی را پیدا میکند:
در ابتدای آن، متد raiseSort را از کامپوننت MoviesTable به اینجا منتقل کردهایم.
سپس در متد رندر آن، بر اساس آرایهی columns که از props این کامپوننت دریافت خواهد شد، لیست thهای هدر را به صورت پویا رندر میکنیم. در اینجا ساختار مورد نیاز شیء column را نیز مشاهده میکنید. نیاز است یک برچسب نمایش داده شود و همچنین برای اینکه this.raiseSort نیز بتواند مجددا کار کند، نیاز است نام خاصیتی که قرار است مرتب سازی بر اساس آن انجام شود نیز مشخص باشد. بنابراین تا اینجا شیء column باید دارای دو خاصیت label و path باشد.
پس از تعریف ابتدایی کامپوننت TableHeader، به کامپوننت MoviesTable بازگشته و شروع به استفادهی از آن میکنیم:
در ادامه باید آرایهی columns را که به صورت props به کامپوننت TableHeader ارسال میشود، تعریف و مقدار دهی کنیم که تشکیل شدهاست از اشیایی با خواص path و label:
در اینجا دو شیء خالی را نیز در انتهای لیست مشاهده میکنید که به thهای خالی مانند نمایش Like و دکمهی Delete اشاره میکنند.
اکنون میتوان کل تعریف thead موجود در این کامپوننت را به طور کامل با کامپوننت TableHeader ای که import کردیم، جایگزین کنیم:
در اینجا ویژگیهای مورد نیاز جهت تامین props کامپوننت TableHeader نیز ذکر شدهاند. this.columns را که در همین کامپوننت تعریف کردیم، sortColumn و onSort هم جزو props ارسالی به کامپوننت جاری هستند.
در این حالت اگر برنامه را اجرا کنید، بدون مشکل خروجی نهایی را رندر میکند؛ اما در کنسول توسعه دهندگان مرورگر یک چنین خطایی را نیز لاگ خواهد کرد:
در حین تعریف رندر لیست thها در کامپوننت TableHeader، ذکر ویژگی key را فراموش کردهایم. البته در اینجا میتوان از column.path بهعنوان key استفاده کرد، اما چون در آرایهی ستونها دو شیء خالی را نیز در انتهای لیست داریم، بهتر است برای اینها یک id را نیز تعریف کردیم تا بتوان آنها را به صورت منحصربفردی شناسایی کرد:
سپس متد رندر کامپوننت TableHeader را جهت درج key به روز رسانی میکنیم:
دراینجا اگر column.path مقدار دهی شده بود، از آن استفاده میشود، در غیراینصورت از مقدار column.key، به عنوان مقدار ویژگی خاصیت key هر المان th، استفاده خواهد شد.
استخراج TableBody از جدول کامپوننت MoviesTable
اکنون با استخراج TableHeader از کامپوننت MoviesTable، به همان مشکل مخلوط بودن درجهی abstractions رسیدهایم. از یک طرف با یک abstraction سطح بالا مانند TableHeader در این کامپوننت سر و کار داریم و از طرف دیگر، نمایش تمام جزئیات درونی رندر جدول نیز پیش روی ما است. همچنین رندر ستونهای آن نیز پویا نیست و هنوز بر اساس خاصیت this.columns تعریف شده، واکنش نشان نمیدهد. به همین جهت tbody این جدول را نیز به یک کامپوننت مستقل تبدیل میکنیم. برای این منظور فایل جدید src\components\common\tableBody.jsx را اضافه میکنیم. سپس با استفاده از میانبرهای imrc و cc، ساختار ابتدایی کامپوننت TableBody را تشکیل میدهیم.
این کامپوننت قرار است آرایهای از اشیاء را دریافت و ردیفهایی را بر اساس آنها رندر کند. به همین جهت این آرایه را از props و با نام data دریافت میکنیم. نام data به عمد انتخاب شدهاست، تا بیانگر عمومی بودن آن باشد؛ بجای استفاده از نام ویژهی آرایهی movies، در این مثال خاص.
تا اینجا ساختار ابتدایی کامپوننت TableBody را مشاهده میکنید که هدف آن، رندر پویای قسمت tbody جدول است. این کامپوننت ابتدا نیاز دارد تا data را از props دریافت کند و بر اساس آن، لیست trها را رندر کند. سپس هر tr نیز از چندین td تشکیل میشود. به همین جهت به لیست دومی به نام columns، برای رندر پویای tdها نیاز است.
رندر محتویات هر سلول جدول به صورت پویا
در این مرحله میخواهیم محتویات tdها را رندر کنیم و حالت فعلی آنها یک چنین شکلی را داشته و در آن ارجاع مستقیمی به شیء movie و خواص آن وجود دارد:
به علاوه این tdها به رندر دکمهی Like و Delete که المانهای سفارشی نیز محسوب میشوند، ختم شدهاند.
برای رندر خواص اشیاء آرایهی ارسالی به کامپوننت TableBody، میتوان از روش [] برای دسترسی به مقادیر خواص استفاده کرد که سبب رندر پویای این مقادیر میشود:
مشکل! روش item[column.path] با خاصیتی مانند "genre.name" که یک خاصیت تو در تو است، کار نمیکند. به همین جهت نیاز به متد زیر، برای انجام اینکار است:
بنابراین تا اینجا روش رندر مقدار هر خاصیت به صورت زیر تغییر میکند:
این تغییر میتواند 4 ستون اول را بدون مشکل رندر کند. اما برای مثال در ستون پنجم، کامپوننت Like قرار گرفتهاست. برای نمایش آن باید چکار کرد؟
همانطور که در ابتدای این سری نیز بررسی کردیم، عبارات JSX در نهایت به اشیاء خالص جاوا اسکریپتی ترجمه میشوند. این ویژگی در حین تعریف المانهای سفارشی مانند کامپوننت Like نیز صادق است. به همین جهت در آرایهی columns که تعاریف ستونهای جدول را به همراه دارد، میتوان یک خاصیت جدید را تعریف و به آن عبارات JSX را انتساب داد. بنابراین تعاریف tdهای Like و Delete را به طور کامل cut کرده و به خاصیت جدید content این دو شیء خالی انتهای لیست آرایهی columns انتساب میدهیم:
البته در اینجا جهت مقدار دهی اشیایی مانند movie، بجای استفادهی مستقیم از یک React element، از یک arrow function استفاده کردهایم تا movie را دریافت کند و یک المان React را بازگشت دهد. همچنین پیشتر از متغیرهای onLike و onDelete در کدهای tdها استفاده کرده بودیم که در ابتدای متد رندر تعریف شده بودند؛ اما زمانیکه این قطعات کد را به خاصیت content منتقل میکنیم، دیگر شناسایی نمیشوند. بنابراین در اینجا برای دسترسی به آنها، مستقیما از props استفاده میشود.
مرحلهی بعد، مراجعه به کامپوننت tableBody و استفاده از خاصیت جدید content، جهت رندر محتوای آن است. در اینجا در متد renderCell بررسی میکنیم اگر ستونی دارای خاصیت content باشد، آن content را رندر میکنیم. در غیراینصورت از همان getPropValue متداول استفاده خواهد شد:
- در متد renderCell، فراخوانی column.content(item) با توجه به function بودن content تعریف شدهی در آرایهی columns، در حقیقیت یک عبارت JSX را بازگشت میدهد که در خروجیهای متدهای React مجاز است و در نهایت تبدیل به المانهای خالص جاوا اسکریپتی در DOM مجازی React و در نهایت DOM اصلی مرورگر میشوند.
- همچنین در اینجا یک createKey را نیز مشاهده میکنید. المانهای هر Array.map نوشته شده، نیاز به یک ویژگی key مقدار دهی شده دارند که در دو قسمت trها و همچنین tdها تعریف شدهاست. در فرمول آن جائیکه از || استفاده شده، اگر ستونی دارای path بود، مقدار آن درج میشود، اما اگر مانند دو ستون آخر صرفا key تعریف شده بود، وجود || سبب میشود تا column.key درنظر گرفته شود و مشکلی رخ ندهد.
- علت تعریف دو متد مجزای renderCell و createKey هم کم شدن بار if/elseها، در بین کدهای درج شدهی در ردیفهای جدول است.
اکنون به کامپوننت MoviesTable مراجعه کرده و کل tbody آنرا حذف و با المان کامپوننت TableBody، جایگزین میکنیم:
تا اینجا اگر این تغییرات را ذخیره کرده و برنامه را مجددا در مرورگر بارگذاری کنیم، باید به همان خروجی قبلی برسیم؛ که اینبار تعاریف ستونهای آن پویا شدهاست.
اضافه کردن آیکن مرتب سازی اطلاعات به سر ستونهای جدول
در کامپوننت tableHeader، کار رندر thها انجام میشود. در اینجا پس از نام سرستون، میخواهیم آیکن نمایش صعودی و یا نزولی بودن روش مرتب سازی جاری را نمایش دهیم. برای این منظور، ابتدا متد renderSortIcon را به این کامپوننت اضافه میکنیم:
این متد، شیء column در حال رندر را دریافت کرده و بر اساس sortColumn دریافتی از props و همچنین صعودی و یا نزولی بودن روش مرتب سازی، یکی از آیکنهای font-awesome را به صورت یک المان جدید رندر میکند. اگر این column در حال رندر، با sortColumn تعیین شده یکی نبود، آیکنی رندر نمیشود (با بازگشت نال، هیچ چیزی رندر نخواهد شد).
و سپس در متد رندر کامپوننت tableHeader، این متد را در کنار label آن ستون درج خواهیم کرد:
پس از ذخیره سازی تغییرات و بارگذاری مجدد برنامه در مرورگر، خروجی آنرا برای نمونه به صورت یک آیکن مثلثی شکل، در کنار عنوان Title میتوان مشاهده کرد:
استخراج کل Table از جدول کامپوننت MoviesTable
در حال حاضر اگر به پیاده سازی کامپوننت MoviesTable دقت کنیم، یک تگ table به همراه دو کامپوننت TableHeader و TableBody در آن درج شدهاند. با این طراحی، اگر قصد استفادهی از این امکانات را در جای دیگری داشته باشیم، باید دقیقا همین قطعه کد را تکرار کنیم. به همین جهت کل تگ table این کامپوننت را استخراج کرده و به کامپوننت جدیدی منتقل میکنیم. به همین جهت فایل جدید src\components\common\table.jsx را ایجاد کرده و با استفاده از میانبرهای imrc و cc، ساختار ابتدایی کامپوننت Table را تشکیل میدهیم. سپس کل تگ table کامپوننت MoviesTable را cut کرده و به متد رندر کامپوننت جدید Table منتقل میکنیم. سپس اولین قدم برای سازگار کردن این محتوا با یک کامپوننت جدید، افزودن importهای زیر است:
سپس باید تمام ویژگیهای استفاده شدهی در این المان منتقل شده را از طریق props دریافت کرد که انجام اینکار را در سطر اول متد رندر مشاهده میکنید:
با این تغییرات به یک کامپوننت سادهی با قابلیت استفادهی مجدد رسیدهایم. اکنون المان آنرا در کامپوننت MoviesTable، در جای تگ قبلی table قرار میدهیم:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-14.zip
ایجاد کامپوننت جدید tableHeader
برای پویاسازی تعاریف ستونها و همچنین کم کردن مسئولیتهای کامپوننت MoviesTable، فایل جدید src\components\common\tableHeader.jsx را ایجاد میکنیم تا در برگیرندهی کامپوننت جدید TableHeader شود. پس از ایجاد این فایل، با استفاده از میانبرهای imrc و cc، ساختار ابتدایی کامپوننت TableHeader را تشکیل میدهیم. سپس به کامپوننت MoviesTable بازگشته و متد raiseSort آنرا cut و به اینجا منتقل میکنیم. همچنین نیاز است کل thead جدول فیلمها را نیز به اینجا منتقل کنیم. اما چون میخواهیم این تعاریف پویا باشند، باید امکان تعریف پویای ستونها را نیز به آن اضافه کنیم. بنابراین اینترفیس این کامپوننت به صورت زیر است:
- ورودیهای آن: آرایهی ستونهای جدول و همچنین شیء sortColumn و رخداد onSort که در متد raiseSort استفاده میشوند.
با این توضیحات، کامپوننت TableHeader چنین شکلی را پیدا میکند:
import React, { Component } from "react"; class TableHeader extends Component { raiseSort = path => { console.log("raiseSort", path); const sortColumn = { ...this.props.sortColumn }; if (sortColumn.path === path) { sortColumn.order = sortColumn.order === "asc" ? "desc" : "asc"; } else { sortColumn.path = path; sortColumn.order = "asc"; } this.props.onSort(sortColumn); }; render() { return ( <thead> <tr> {this.props.columns.map(column => ( <th onClick={() => this.raiseSort(column.path)}>{column.label}</th> ))} </tr> </thead> ); } } export default TableHeader;
سپس در متد رندر آن، بر اساس آرایهی columns که از props این کامپوننت دریافت خواهد شد، لیست thهای هدر را به صورت پویا رندر میکنیم. در اینجا ساختار مورد نیاز شیء column را نیز مشاهده میکنید. نیاز است یک برچسب نمایش داده شود و همچنین برای اینکه this.raiseSort نیز بتواند مجددا کار کند، نیاز است نام خاصیتی که قرار است مرتب سازی بر اساس آن انجام شود نیز مشخص باشد. بنابراین تا اینجا شیء column باید دارای دو خاصیت label و path باشد.
پس از تعریف ابتدایی کامپوننت TableHeader، به کامپوننت MoviesTable بازگشته و شروع به استفادهی از آن میکنیم:
import TableHeader from "./common/tableHeader";
در ادامه باید آرایهی columns را که به صورت props به کامپوننت TableHeader ارسال میشود، تعریف و مقدار دهی کنیم که تشکیل شدهاست از اشیایی با خواص path و label:
columns = [ { path: "title", label: "Title" }, { path: "genre.name", label: "Genre" }, { path: "numberInStock", label: "Stock" }, { path: "dailyRentalRate", label: "Rate" }, {}, {} ];
اکنون میتوان کل تعریف thead موجود در این کامپوننت را به طور کامل با کامپوننت TableHeader ای که import کردیم، جایگزین کنیم:
render() { const { movies, onDelete, onLike, onSort, sortColumn } = this.props; return ( <table className="table"> <TableHeader columns={this.columns} sortColumn={sortColumn} onSort={onSort} /> <tbody>
در این حالت اگر برنامه را اجرا کنید، بدون مشکل خروجی نهایی را رندر میکند؛ اما در کنسول توسعه دهندگان مرورگر یک چنین خطایی را نیز لاگ خواهد کرد:
index.js:1375 Warning: Each child in a list should have a unique "key" prop. Check the render method of `TableHeader`. See https://fb.me/react-warning-keys for more information.
class MoviesTable extends Component { columns = [ { path: "title", label: "Title" }, { path: "genre.name", label: "Genre" }, { path: "numberInStock", label: "Stock" }, { path: "dailyRentalRate", label: "Rate" }, { key: "like" }, { key: "delete" } ];
render() { return ( <thead> <tr> {this.props.columns.map(column => ( <th key={column.path || column.key} style={{ cursor: "pointer" }} onClick={() => this.raiseSort(column.path)} > {column.label} </th> ))} </tr> </thead> );
استخراج TableBody از جدول کامپوننت MoviesTable
اکنون با استخراج TableHeader از کامپوننت MoviesTable، به همان مشکل مخلوط بودن درجهی abstractions رسیدهایم. از یک طرف با یک abstraction سطح بالا مانند TableHeader در این کامپوننت سر و کار داریم و از طرف دیگر، نمایش تمام جزئیات درونی رندر جدول نیز پیش روی ما است. همچنین رندر ستونهای آن نیز پویا نیست و هنوز بر اساس خاصیت this.columns تعریف شده، واکنش نشان نمیدهد. به همین جهت tbody این جدول را نیز به یک کامپوننت مستقل تبدیل میکنیم. برای این منظور فایل جدید src\components\common\tableBody.jsx را اضافه میکنیم. سپس با استفاده از میانبرهای imrc و cc، ساختار ابتدایی کامپوننت TableBody را تشکیل میدهیم.
این کامپوننت قرار است آرایهای از اشیاء را دریافت و ردیفهایی را بر اساس آنها رندر کند. به همین جهت این آرایه را از props و با نام data دریافت میکنیم. نام data به عمد انتخاب شدهاست، تا بیانگر عمومی بودن آن باشد؛ بجای استفاده از نام ویژهی آرایهی movies، در این مثال خاص.
import React, { Component } from "react"; class TableBody extends Component { render() { const { data, columns } = this.props; return ( <tbody> {data.map(item => ( <tr> {columns.map(column => ( <td></td> ))} </tr> ))} </tbody> ); } } export default TableBody;
رندر محتویات هر سلول جدول به صورت پویا
در این مرحله میخواهیم محتویات tdها را رندر کنیم و حالت فعلی آنها یک چنین شکلی را داشته و در آن ارجاع مستقیمی به شیء movie و خواص آن وجود دارد:
{movies.map(movie => ( <tr key={movie._id}> <td>{movie.title}</td>
برای رندر خواص اشیاء آرایهی ارسالی به کامپوننت TableBody، میتوان از روش [] برای دسترسی به مقادیر خواص استفاده کرد که سبب رندر پویای این مقادیر میشود:
<td>{item[column.path]}</td>
getPropValue(obj, path) { if (!path) { return obj; } const properties = path.split("."); return this.getPropValue(obj[properties.shift()], properties.join(".")); }
<td>{getPropValue(item, column.path)}</td>
همانطور که در ابتدای این سری نیز بررسی کردیم، عبارات JSX در نهایت به اشیاء خالص جاوا اسکریپتی ترجمه میشوند. این ویژگی در حین تعریف المانهای سفارشی مانند کامپوننت Like نیز صادق است. به همین جهت در آرایهی columns که تعاریف ستونهای جدول را به همراه دارد، میتوان یک خاصیت جدید را تعریف و به آن عبارات JSX را انتساب داد. بنابراین تعاریف tdهای Like و Delete را به طور کامل cut کرده و به خاصیت جدید content این دو شیء خالی انتهای لیست آرایهی columns انتساب میدهیم:
class MoviesTable extends Component { columns = [ { path: "title", label: "Title" }, { path: "genre.name", label: "Genre" }, { path: "numberInStock", label: "Stock" }, { path: "dailyRentalRate", label: "Rate" }, { key: "like", content: movie => ( <Like liked={movie.liked} onClick={() => this.props.onLike(movie)} /> ) }, { key: "delete", content: movie => ( <button onClick={() => this.props.onDelete(movie)} className="btn btn-danger btn-sm" > Delete </button> ) } ];
مرحلهی بعد، مراجعه به کامپوننت tableBody و استفاده از خاصیت جدید content، جهت رندر محتوای آن است. در اینجا در متد renderCell بررسی میکنیم اگر ستونی دارای خاصیت content باشد، آن content را رندر میکنیم. در غیراینصورت از همان getPropValue متداول استفاده خواهد شد:
renderCell = (item, column) => { if (column.content) { return column.content(item); } return this.getPropValue(item, column.path); }; createKey = (item, column) => { return item._id + (column.path || column.key); }; render() { const { data, columns } = this.props; return ( <tbody> {data.map(item => ( <tr key={item._id}> {columns.map(column => ( <td key={this.createKey(item, column)}> {this.renderCell(item, column)} </td> ))} </tr> ))} </tbody> ); }
- همچنین در اینجا یک createKey را نیز مشاهده میکنید. المانهای هر Array.map نوشته شده، نیاز به یک ویژگی key مقدار دهی شده دارند که در دو قسمت trها و همچنین tdها تعریف شدهاست. در فرمول آن جائیکه از || استفاده شده، اگر ستونی دارای path بود، مقدار آن درج میشود، اما اگر مانند دو ستون آخر صرفا key تعریف شده بود، وجود || سبب میشود تا column.key درنظر گرفته شود و مشکلی رخ ندهد.
- علت تعریف دو متد مجزای renderCell و createKey هم کم شدن بار if/elseها، در بین کدهای درج شدهی در ردیفهای جدول است.
اکنون به کامپوننت MoviesTable مراجعه کرده و کل tbody آنرا حذف و با المان کامپوننت TableBody، جایگزین میکنیم:
//... import TableBody from "./common/tableBody"; //... class MoviesTable extends Component { // ... render() { const { movies, onSort, sortColumn } = this.props; return ( <table className="table"> <TableHeader columns={this.columns} sortColumn={sortColumn} onSort={onSort} /> <TableBody columns={this.columns} data={movies} /> </table> ); } }
اضافه کردن آیکن مرتب سازی اطلاعات به سر ستونهای جدول
در کامپوننت tableHeader، کار رندر thها انجام میشود. در اینجا پس از نام سرستون، میخواهیم آیکن نمایش صعودی و یا نزولی بودن روش مرتب سازی جاری را نمایش دهیم. برای این منظور، ابتدا متد renderSortIcon را به این کامپوننت اضافه میکنیم:
renderSortIcon = column => { const { sortColumn } = this.props; if (column.path !== sortColumn.path) { return null; } if (sortColumn.order === "asc") { return <i className="fa fa-sort-asc" />; } return <i className="fa fa-sort-desc" />; };
و سپس در متد رندر کامپوننت tableHeader، این متد را در کنار label آن ستون درج خواهیم کرد:
{column.label} {this.renderSortIcon(column)}
استخراج کل Table از جدول کامپوننت MoviesTable
در حال حاضر اگر به پیاده سازی کامپوننت MoviesTable دقت کنیم، یک تگ table به همراه دو کامپوننت TableHeader و TableBody در آن درج شدهاند. با این طراحی، اگر قصد استفادهی از این امکانات را در جای دیگری داشته باشیم، باید دقیقا همین قطعه کد را تکرار کنیم. به همین جهت کل تگ table این کامپوننت را استخراج کرده و به کامپوننت جدیدی منتقل میکنیم. به همین جهت فایل جدید src\components\common\table.jsx را ایجاد کرده و با استفاده از میانبرهای imrc و cc، ساختار ابتدایی کامپوننت Table را تشکیل میدهیم. سپس کل تگ table کامپوننت MoviesTable را cut کرده و به متد رندر کامپوننت جدید Table منتقل میکنیم. سپس اولین قدم برای سازگار کردن این محتوا با یک کامپوننت جدید، افزودن importهای زیر است:
import TableBody from "./tableBody"; import TableHeader from "./tableHeader";
import TableBody from "./tableBody"; import TableHeader from "./tableHeader"; class Table extends Component { render() { const { columns, sortColumn, onSort, data } = this.props; return ( <table className="table"> <TableHeader columns={columns} sortColumn={sortColumn} onSort={onSort} /> <TableBody columns={columns} data={data} /> </table> ); } } export default Table;
//... import Table from "./common/table"; class MoviesTable extends Component { //... render() { const { movies, onSort, sortColumn } = this.props; return ( <Table columns={this.columns} sortColumn={sortColumn} onSort={onSort} data={movies} /> ); } }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-14.zip