مقدمهای بر زبان Go
EF Code First #11
1- Domain Layer یا Model Layer: با استفاده از Code First فقط Entityها تعریف می شود (بدون Mapping و Data Annotation)
2- DAL: با استفاده از DbContext و Fluent API موارد مرتبط با Mapping و Data Annotation و ساخت DbSet ها تعریف می شود.
3- Facade DAL: با استفاده از WCF یا هر تکنیک سرویس گرایی دیگر یک لایه سطح بالا بر روی DAL کشیده می شود تا در BLL تنها از آن استفاده شود و عملا لایه های BLL و UI از EF هیچ اطلاعی نخواهند داشت.
4- BLL: کلاس های مربوط به برنامه، Rule ها و Infrastructureها
5- UI: شامل پروژه ASP.NET MVC، WPF و یا برنامه های موبایلی
آیا این معماری درک درستی از مطالب ارائه شده توسط شما است؟
و سوال دوم اینکه در مورد برنامه هایی با منطق MVC و یا MVVM لایه مدل تقریبا با این معماری چیزی نخواهد بود و تنها ویو مدل ها به چشم خواهند خورد. آیا این مورد هم درست است؟
شمسی ساز تاریخ اکسپلورر ویندوز
یا اگر لیست فایلها را بررسی میکردیم، ستون تاریخ آنها نیز قابل درک بود
و یا اگر به خواص یک فایل مراجعه میکردیم، تاریخ ایجاد آن نیز شمسی بود
- با استفاده از برنامه سورس باز ExplorerPCal.exe میتوانید به این قابلیتها دسترسی پیدا کنید.
- این برنامه برای اجرا نیاز به دات نت فریم ورک 4 دارد. بنابراین بر روی ویندوزهای XP SP3 به بعد قابل اجرا است. ضمنا برنامه ExplorerPCal.exe با هر دو نگارش 32 بیتی و 64 بیتی ویندوز سازگار است.
- برای اجرای آن تنها کافی است فایل «ExplorerPCal.exe» را اجرا کنید. همچنین بهتر است برنامه را در درایو C کپی نکنید.
- برای حذف آن از سیستم، نیاز است پوشهی مربوطه را حذف نمائید. در این حال اگر ویندوز پیام در حال استفاده بودن فایلی را میدهد، ویندوز را یکبار ریاستارت کنید و پس از آن فایلهای باقیمانده را بدون مشکل میتوانید حذف نمائید.
- برای ویندوز 10 به این مطلب مراجعه کنید.
- Kernel
- ماشین مجازی Dalvik
- application framework
- applications
هسته اندروید (kernel) چیست؟
اندروید بر روی هسته لینوکس نسخه 2.6 به بالاتر اجرا میشود. هسته، اولین و نخستین لایهی نرم افزاری در اندروید است که با سخت افزار به صورت کاملا مستقیم در تعامل میباشد. تغییرات در هسته و یا ساختن دوبارهی آن چیزی نیست که لازم باشد از ابتدا به ساکن و مجددا توسط یک برنامه نویس انجام شود! فقط سازندگان سخت افزار قادر به تغییرات درون هسته هستند و این یک امر غیرطبیعی و ناممکن برای برنامه نویسان خواهد بود؛ چرا که یک برنامه نویس تنها قادر است تا لایههای نرم افزاری را تغییر دهد نه سخت افزار آن را و این مورد تنها بهعهدهی افرادی است که با سخت افزار اندروید در ارتباط هستند.
کتابخانه ها
اجزای کتابخانهها بعنوان یک لایه مترجم بین هسته و فریم ورکها عمل میکنند. این کتابخانهها و اجزای درونی آنها با زبان C و ++C نوشته شدهاند؛ اما از طریق یک API در زبان جاوا در اختیار توسعه دهندگان قرار میگیرند تا به سهولت قابل استفاده باشند. برنامه نویسان و توسعه دهندگان میتوانند از طریق فریم ورکهای زبان جاوا برای دسترسی مستقیم به کتابخانههای اصلی C و ++C استفاده کنند.
برخی از کتابخانههای اصلی عبارتند از:
- LibWebCore
- Media libraries
- Graphics libraries
ماشین مجازی Dalvik چیست؟
در اصل آقای Dan Bornstein ماشین مجازی Dalvik را نوشت. ماشین مجازی دالویک برای اجرای برنامهها بر روی دستگاههای مختلف با منابع بسیار محدود نوشته شده بود و این یکی از اولویتهای این ماشین بود تا بتواند با منابع محدود بر روی دستگاه در تعامل باشد و برنامه را اجرا نماید. به طور معمول ماشین دالویک برای تلفنهای همراهای استفاده میشد که توان پردازشی پایین، عمر باتری کم و همچنین حافظهی اندکی داشتند. ماشین مجازی دالویک فایلهای با پسوند dex. را اجرا میکند. این فایلها با استفاده از کلاسهای جاوا کامپایل شدهاند که با در نظر گرفتن نوع کلاس class. یا jar. انجام میشوند و فایلهای class. را به یک ثابت (constant) مشترک و هماهنگ تبدیل مینمایند. ابزار DX، که در Android SDK موجود است، این تبدیل را انجام میدهد.
به تصویر زیر توجه کنید:
به تصویر زیر توجه کنید؛ پس از تبدیل، فایلهای dex. به طرز قابل توجهی کوچکتر و کم حجمتر میشوند!
Application Framework چیست؟
اپلیکیشن فریم ورک، یکی از بلاکهای مهم ساختن سیستم نهایی میباشد. این فریم ورکها مجموعهای از خدمات را درون سیستم برای برنامهنویس یا توسعه دهندگان جهت نوشتن یک برنامه بدون مشکل فراهم میکنند. بهعنوان یک توسعه دهنده شما کد را مینویسید و فقط از APIها برای تکمیل آنها در زبان جاوا استفاده میکنید. از طریق یک API در زبان جاوا قادر خواهید بود تا به هسته و کتابخانههای C و ++C دسترسی داشته باشید.
بعنوان نمونه : کد زیر به شما نحوه پخش یک فایل ویدئویی را نشان میدهد
package com.example.android.apis.media; import com.example.android.apis.R; import android.app.Activity; import android.os.Bundle; import android.widget.MediaController; import android.widget.Toast; import android.widget.VideoView; public class VideoViewDemo extends Activity { /** * TODO: Set the path variable to a streaming video URL or a local media * file path. */ private String path = ""; private VideoView mVideoView; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.videoview); mVideoView = (VideoView) findViewById(R.id.surface_view); if (path == "") { // Tell the user to provide a media file URL/path. Toast.makeText( VideoViewDemo.this, "Please edit VideoViewDemo Activity, and set path" + " variable to your media file URL/path", Toast.LENGTH_LONG).show(); } else { /* * Alternatively,for streaming media you can use * mVideoView.setVideoURI(Uri.parse(URLstring)); */ mVideoView.setVideoPath(path); mVideoView.setMediaController(new MediaController(this)); mVideoView.requestFocus(); } } }
نحوهی رندر لیستی از اشیاء در کامپوننتهای React
فرض کنید میخواهیم لیستی از تگها را رندر کنیم. برای این منظور ابتدا دادههای مرتبط را به خاصیت state کامپوننت، اضافه میکنیم:
class Counter extends Component { state = { count: 0, tags: ["tag 1", "tag 2", "tag 3"] };
در مطلب «React 16x - قسمت 3 - بررسی پیشنیازهای جاوا اسکریپتی - بخش 2» در مورد متد Array.map بحث شد. در اینجا میتوان توسط متد map، هر المان آرایهی تگها را به یک المان React تبدیل و سپس رندر کرد:
class Counter extends Component { state = { count: 0, tags: ["tag 1", "tag 2", "tag 3"] }; render() { return ( <div> <span className={this.getBadgeClasses()}>{this.formatCount()}</span> <button className="btn btn-secondary btn-sm">Increment</button> <ul> {this.state.tags.map(tag => ( <li>{tag}</li> ))} </ul> </div> ); }
هرچند اکنون لیستی از تگها در مرورگر رندر شدهاند، اما در کنسول توسعه دهندگان مرورگر، یک اخطار نیز درج شدهاست. علت اینجا است که React نیاز دارد تا بتواند هر آیتم رندر شده را به صورت منحصربفردی شناسایی کند. هدف این است که بتواند در صورت تغییر state هر المان در DOM مجازی خودش، خیلی سریع تشخیص دهد که چه چیزی تغییر کرده و فقط کدام قسمت خاص را باید در DOM اصلی، درج و به روز رسانی کند. برای رفع این مشکل، ویژگی key را به هر المان li در کدهای فوق اضافه میکنیم:
<li key={tag}>{tag}</li>
رندر شرطی عناصر در کامپوننتهای React
در اینجا میخواهیم اگر تگی وجود نداشت، پیام متناسبی ارائه شود؛ در غیراینصورت لیست تگها همانند قبل نمایش داده شود (رندر شرطی یا conditional rendering). برای انجام اینکار در React، برخلاف Angular، دارای دایرکتیوهای ساختاری if/else نیستیم؛ چون همانطور که عنوان شد، JSX یک templating engine نیست. به همین جهت برای رندر شرطی المانها در React، باید از همان جاوا اسکریپت خالص کمک بگیریم:
renderTags() { if (this.state.tags.length === 0) { return <p>There are no tags!</p>; } return ( <ul> {this.state.tags.map(tag => ( <li key={tag}>{tag}</li> ))} </ul> ); }
render() { return ( <div> <span className={this.getBadgeClasses()}>{this.formatCount()}</span> <button className="btn btn-secondary btn-sm">Increment</button> {this.renderTags()} </div> ); }
state = { count: 0, tags: [] };
روش دوم حل این نوع مسالهها، استفاده از روش زیر است؛ در این حالت خاص، فقط یک if را داریم، بدون وجود قسمت else:
{this.state.tags.length === 0 && "Please create a new tag!"}
اما این روش چگونه کار میکند؟! در اینجا && را به دو مقدار مشخص اعمال کردهایم. یکی حاصل یک مقایسه است و دیگری یک مقدار رشتهای مشخص. در جاوا اسکریپت برخلاف سایر زبانهای برنامه نویسی، میتوان && را بین دو مقدار غیر Boolean نیز اعمال کرد. در جاوا اسکریپت، یک رشتهی خالی به false تعبیر میشود و اگر تنها دارای یک حرف باشد، true درنظر گرفته میشود. برای نمونه در ترکیب 'true && 'Hi، هر دو قسمت به true تفسیر میشوند. در این حالت موتور جاوا اسکریپت، دومین عبارت (آخرین عبارت && شده) را بازگشت میدهد. همچنین در جاوا اسکریپت عدد صفر به false تفسیر میشود. بنابراین ترکیب true && 'Hi' && 1 مقدار 1 را بازگشت میدهد؛ چون عدد 1 هم از دیدگاه جاوا اسکریپت به true تفسیر خواهد شد.
مدیریت رخدادها در React
همانطور که در تصویر فوق نیز مشاهده میکنید، رخدادهای استاندارد DOM، دارای خواص معادل React ای نیز هستند. برای مثال زمانیکه مینویسیم onClick، دقیقا متناظر است با یک خاصیت المان React در عبارات JSX. بنابراین این نامها حساس به کوچکی و بزرگی حروف نیز هستند.
روش تعریف متدهای رخدادگردان در اینجا، با ذکر فعل handle شروع میشود:
handleIncrement() { console.log("Increment clicked!"); }
<button onClick={this.handleIncrement} className="btn btn-secondary btn-sm" > Increment </button>
اکنون اگر این فایل را ذخیره کرده و خروجی را در مرورگر بررسی کنیم، با هربار کلیک بر روی دکمهی Increment، یک console.log صورت میگیرد.
در ادامه میخواهیم در این رخدادگردان، مقدار this.state.count را افزایش دهیم. برای این منظور ابتدا مقدار this.state.count را به نحو زیر لاگ میکنیم:
handleIncrement() { console.log("Increment clicked!", this.state.count); }
bind مجدد شیء this در رخدادگردانهای React
در مورد this و bind مجدد آن در مطلب «React 16x - قسمت 2 - بررسی پیشنیازهای جاوا اسکریپتی - بخش 1» مفصل بحث کردیم و در اینجا میخواهیم از نتایج آن استفاده کنیم.
همانطور که مشاهده کردید، در متد رویدادگران handleIncrement، به شیء this دسترسی نداریم. چرا؟ چون this در جاوا اسکریپت نسبت به سایر زبانهای برنامه نویسی، متفاوت رفتار میکند. بسته به اینکه یک متد یا تابع، چگونه فراخوانی میشود، this میتواند اشیاء متفاوتی را بازگشت دهد. اگر تابعی به عنوان یک متد و جزئی از یک شیء فراخوانی شود، this در این حالت همواره ارجاعی را به آن شیء باز میگرداند. اما اگر آن تابع به صورت متکی به خود فراخوانی شد، به صورت پیشفرض ارجاعی را به شیء سراسری window مرورگر، بازگشت میدهد و اگر strict mode فعال باشد، تنها undefined را بازگشت میدهد. به همین جهت است که در اینجا خطای undefined بودن this را دریافت میکنیم.
یک روش حل این مشکل که پیشتر نیز در مورد آن توضیح دادیم، استفاده از متد bind است:
constructor() { super(); console.log("constructor", this); this.handleIncrement = this.handleIncrement.bind(this); }
اکنون اگر برنامه را اجرا کنید، با کلیک بر روی دکمهی Increment، بجای this.state.count لاگ شده، مقدار آن که صفر است، در کنسول توسعه دهندههای مرورگر ظاهر میشود.
این یک روش است که کار میکند؛ اما کمی طولانی است و به ازای هر روال رویدادگردانی باید دقیقا به همین نحو تکرار شود. روش دیگر، تبدیل متد handleIncrement به یک arrow function است و همانطور که در قسمت دوم این سری نیز بررسی کردیم، arrow functionها، this شیء جاری را بازنویسی نمیکنند؛ بلکه آنرا به ارث میبرند. بنابراین ابتدا کدهای سازندهی فوق را حذف میکنیم (چون دیگر نیازی به آنها نیست) و سپس متد handleIncrement سابق را به صورت زیر، تبدیل به یک arrow function میکنیم:
handleIncrement = () => { console.log("Increment clicked!", this.state.count); }
به روز رسانی state در کامپوننتهای React
اکنون که در روال رویدادگردان handleIncrement به شیء this و سپس مقدار this.state.count آن دسترسی پیدا کردهایم، میخواهیم با هربار کلیک بر روی این دکمه، یک واحد مقدار آنرا افزایش داده و در UI نمایش دهیم.
در React، خواص شیء state را جهت نمایش آنها در UI، مستقیما تغییر نمیدهیم. به عبارت دیگر نوشتن یک چنین کدی در React برای به روز رسانی UI، مرسوم نیست:
handleIncrement = () => { this.state.count++; };
در کدهای فوق هرچند با کلیک بر روی دکمهی Increment، مقدار count افزایش یافتهاست، اما React از وقوع این تغییرات مطلع نیست. به همین جهت است که هیچ تغییری را در UI برنامه مشاهده نمیکنید.
با اجرای قطعه کد فوق، یک چنین اخطاری نیز در کنسول توسعه دهندگان مرورگر ظاهر میشود:
Line 33:5: Do not mutate state directly. Use setState() react/no-direct-mutation-state
برای رفع این مشکل باید از یکی از متدهای به ارث برده شدهی از کلاس پایهی Component، به نام setState استفاده کرد. به این ترتیب به React اعلام میکنیم که state تغییر کردهاست (فعالسازی Change Detection، فقط در صورت نیاز). سپس React شروع به محاسبهی تغییرات کرده و در نتیجه قسمتهای متناظری از UI را برای هماهنگ سازی DOM مجازی خودش با DOM اصلی، به روز رسانی میکند.
زمانیکه از متد setState استفاده میکنیم، شیءای را باید به صورت یک پارامتر به آن ارسال کنیم. در این حالت مقادیر آن یا به خاصیت state جاری اضافه میشوند و یا در صورت از پیش موجود بودن، همان خواص را بازنویسی میکنند:
handleIncrement = () => { this.setState({ count: this.state.count + 1 }); };
در این مرحله، فایل جاری را ذخیره کرده و پس از بارگذاری مجدد برنامه در مرورگر، بر روی دکمهی Increment کلیک کنید. اینبار ... کار میکند! چون React از تغییرات مطلع شدهاست:
وقتی state تغییر میکند، چه اتفاقاتی رخ میدهند؟
با فراخوانی متد this.setState، به React اعلام میکنیم که state یک کامپوننت قرار است تغییر کند. سپس React فراخوانی مجدد متد Render را در صف اجرایی خودش قرار میدهد تا در زمانی در آینده، اجرا شود؛ این فراخوانی async است. کار متد render، بازگشت یک المان جدید React است. در اینجا DOM مجازی React از چند المان، به صورت یک div و دو فرزند دکمه و span تشکیل شدهاست. در این حالت یک DOM مجازی قدیمی نیز از قبل (پیش از اجرای مجدد متد render) وجود دارد. در این لحظه، React این دو DOM مجازی را کنار هم قرار میدهد و محاسبه میکند که در اینجا دقیقا کدام المانها نسبت به قبل تغییر کردهاند. برای نمونه در اینجا تشخیص میدهد که span است که تغییر کرده، چون مقدار count، توسط آن نمایش داده میشود. در این حالت از کل DOM اصلی، تنها همان span تغییر کرده را به روز رسانی میکند و نه کل DOM را (و نه اعمال مجدد کل المانهای حاصل از متد render را).
این مورد را میتوان به نحو زیر آزمایش و مشاهده کرد:
در مرورگر بر روی المان span که شمارهها را نمایش میدهد، کلیک راست کرده و گزینهی inspect را انتخاب کنید. سپس بر روی دکمهی Increment کلیک نمائید. مرورگر قسمتی را که به روز میشود، با رنگی مشخص و متمایز، به صورت لحظهای نمایش میدهد:
ارسال پارامترها به متدهای رویدادگردان
تا اینجا متد handleIncrement، بدون پارامتر تعریف شدهاست. فرض کنید در یک برنامهی واقعی قرار است با کلیک بر روی این دکمه، id یک محصول را نیز به handleIncrement، منتقل و ارسال کنیم. اما در onClick={this.handleIncrement} تعریف شده، یک ارجاع را به متد handleIncrement داریم. بنابراین برای حل این مساله نمیتوان از روشی مانند onClick={this.handleIncrement(1)} استفاده کرد که در آن عدد فرضی 1 به صورت آرگومان متد handleIncrement ذکر شدهاست.
یک روش حل این مساله، تعریف متد دومی است که متد handleIncrement پارامتر دار را فراخوانی میکند:
doHandleIncrement = () => { this.handleIncrement({ id: 1, name: "Product 1" }); };
handleIncrement = product => { console.log(product); this.setState({ count: this.state.count + 1 }); };
هرچند این روش کار میکند، اما بیش از اندازه طولانی شدهاست. راه حل بهتر، استفاده از یک inline function است:
onClick={() => this.handleIncrement({ id: 1, name: "Product 1" })}
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-04-part02.zip
قسمت دوم آشنایی با Refactoring به معرفی روش «استخراج متدها» اختصاص دارد. این نوع Refactoring بسیار ساده بوده و مزایای بسیاری را به همراه دارد؛ منجمله:
- بالا بردن خوانایی کد؛ از این جهت که منطق طولانی یک متد به متدهای کوچکتری با نامهای مفهوم شکسته میشود.
- به این ترتیب نیاز به مستند سازی کدها نیز بسیار کاهش خواهد یافت. بنابراین در یک متد، هر جایی که نیاز به نوشتن کامنت وجود داشت، یعنی باید همینجا آن قسمت را جدا کرده و در متد دیگری که نام آن، همان خلاصه کامنت مورد نظر است، قرار داد.
- این نوع جدا سازی منطقهای پیاده سازی قسمتهای مختلف یک متد، در آینده نگهداری کد نهایی را نیز سادهتر کرده و انجام تغییرات بر روی آن را نیز تسهیل میبخشد؛ زیرا اینبار بجای هراس از دستکاری یک متد طولانی، با چند متد کوچک و مشخص سروکار داریم.
برای نمونه به مثال زیر دقت کنید:
using System.Collections.Generic;
namespace Refactoring.Day2.ExtractMethod.Before
{
public class Receipt
{
private IList<decimal> Discounts { get; set; }
private IList<decimal> ItemTotals { get; set; }
public decimal CalculateGrandTotal()
{
// Calculate SubTotal
decimal subTotal = 0m;
foreach (decimal itemTotal in ItemTotals)
subTotal += itemTotal;
// Calculate Discounts
if (Discounts.Count > 0)
{
foreach (decimal discount in Discounts)
subTotal -= discount;
}
// Calculate Tax
decimal tax = subTotal * 0.065m;
subTotal += tax;
return subTotal;
}
}
}
همانطور که از کامنتهای داخل متد CalculateGrandTotal مشخص است، این متد سه کار مختلف را انجام میدهد؛ جمع اعداد، اعمال تخفیف، اعمال مالیات و نهایتا یک نتیجه را باز میگرداند. بنابراین بهتر است هر عمل را به یک متد جداگانه و مشخص منتقل کرده و کامنتهای ذکر شده را نیز حذف کنیم. نام یک متد باید به اندازهی کافی مشخص و مفهوم باشد و آنچنان نیازی به مستندات خاصی نداشته باشد:
using System.Collections.Generic;
namespace Refactoring.Day2.ExtractMethod.After
{
public class Receipt
{
private IList<decimal> Discounts { get; set; }
private IList<decimal> ItemTotals { get; set; }
public decimal CalculateGrandTotal()
{
decimal subTotal = CalculateSubTotal();
subTotal = CalculateDiscounts(subTotal);
subTotal = CalculateTax(subTotal);
return subTotal;
}
private decimal CalculateTax(decimal subTotal)
{
decimal tax = subTotal * 0.065m;
subTotal += tax;
return subTotal;
}
private decimal CalculateDiscounts(decimal subTotal)
{
if (Discounts.Count > 0)
{
foreach (decimal discount in Discounts)
subTotal -= discount;
}
return subTotal;
}
private decimal CalculateSubTotal()
{
decimal subTotal = 0m;
foreach (decimal itemTotal in ItemTotals)
subTotal += itemTotal;
return subTotal;
}
}
}
بهتر شد! عملکرد کد نهایی، تغییری نکرده اما کیفیت کد ما بهبود یافته است (همان مفهوم و معنای Refactoring). خوانایی کد افزایش یافته است. نیاز به کامنت نویسی به شدت کاهش پیدا کرده و از همه مهمتر، اعمال مختلف، در متدهای خاص آنها قرار گرفتهاند.
به همین جهت اگر حین کد نویسی، به یک متد طولانی برخوردید (این مورد بسیار شایع است)، در ابتدا حداقل کاری را که جهت بهبود کیفیت آن میتوانید انجام دهید، «استخراج متدها» است.
ابزارهای کمکی جهت پیاده سازی روش «استخراج متدها»:
- ابزار Refactoring توکار ویژوال استودیو پس از انتخاب یک قطعه کد و سپس کلیک راست و انتخاب گزینهی Refactor->Extract method، این عملیات را به خوبی میتواند مدیریت کند و در وقت شما صرفه جویی خواهد کرد.
- افزونههای ReSharper و همچنین CodeRush نیز چنین قابلیتی را ارائه میدهند؛ البته توانمندیهای آنها از ابزار توکار یاد شده بیشتر است. برای مثال اگر در میانه کد شما جایی return وجود داشته باشد، گزینهی Extract method ویژوال استودیو کار نخواهد کرد. اما سایر ابزارهای یاده شده به خوبی از پس این موارد و سایر موارد پیشرفتهتر بر میآیند.
نتیجه گیری:
نوشتن کامنت، داخل بدنهی یک متد مزموم است؛ حداقل به دو دلیل:
- ابزارهای خودکار مستند سازی از روی کامنتهای نوشته شده، از این نوع کامنتها صرفنظر خواهند کرد و در کتابخانهی شما مدفون خواهند شد (یک کار بیحاصل).
- وجود کامنت در داخل بدنهی یک متد، نمود آشکار ضعف شما در کپسوله سازی منطق مرتبط با آن قسمت است.
و ... «لطفا» این نوع پیاده سازیها را خارج از فایل code behind هر نوع برنامهی winform/wpf/asp.net و غیره قرار دهید. تا حد امکان سعی کنید این مکانها، استفاده کنندهی «نهایی» منطقهای پیاده سازی شده توسط کلاسهای دیگر باشند؛ نه اینکه خودشان محل اصلی قرارگیری و ابتدای تعریف منطقهای مورد نیاز قسمتهای مختلف همان فرم مورد نظر باشند. «لطفا» یک فرم درست نکنید با 3000 سطر کد که در قسمت code behind آن قرار گرفتهاند. code behind را محل «نهایی» ارائه کار قرار دهید؛ نه نقطهی آغاز تعریف منطقهای پیاده سازی کار. این برنامه نویسی چندلایه که از آن صحبت میشود، فقط مرتبط با کار با بانکهای اطلاعاتی نیست. در همین مثال، کدهای فرم برنامه، باید نقطهی نهایی نمایش عملیات محاسبه مالیات باشند؛ نه اینکه همانجا دوستانه یک قسمت مالیات حساب شود، یک قسمت تخفیف، یک قسمت جمع بزند، همانجا هم نمایش بدهد! بعد از یک هفته میبینید که code behind فرم در حال انفجار است! شده 3000 سطر! بعد هم سؤال میپرسید که چرا اینقدر میل به «بازنویسی» سیستم این اطراف زیاد است! برنامه نویس حاضر است کل کار را از صفر بنویسد، بجای اینکه با این شاهکار بخواهد سرو کله بزند! هر چند یکی از روشهای برخورد با این نوع کدها جهت کاهش هراس نگهداری آنها، شروع به Refactoring است.
مطلب زیر چکیدهای است از کتاب Framework Design Guidelines در مورد سربارگذاری توابع
الف) از یک نوع خروجی استفاده کنید
مثال زیر را در نظر بگیرید:
public User[] GetGroupMembers(int groupId)
public List<User> GetGroupMembers(string groupName)
مشکل اینجا است که هنگام فراخوانی هر کدام، نحوهی استفاده متفاوت خواهد بود. بنابراین باید از این نوع سربارگذاری پرهیز کرد.
ب) نامهای آرگومانهای توابع سربارگذاری شده شما باید یکسان باشند
در مثال زیر، از اولین آرگومان جهت دریافت شناسهی یک گروه استفاده میشود:
public List<User> GetGroupMembers(int groupId)
public List<User> GetGroupMembers(int id, int pageIndex, int pageSize)
ج) ترتیب آرگومانها را در توابع سربارگذاری شده حفظ و رعایت نمائید
لطفا به مثال زیر دقت کنید:
public List<User> GetGroupMembers(int groupId)
public List<User> GetGroupMembers(int pageIndex, int pageSize, int groupId)
د) از call forwarding استفاده کنید
جهت توضیح بهتر call forwarding به مثال زیر دقت نمائید:
public List<User> GetGroupMembers(int groupId)
{
return GetGroupMembers(groupId, 0, 10);
}
public List<User> GetGroupMembers(int groupId, int pageIndex, int pageSize)
{
return GetGroupMembers(groupId, pageIndex, pageSize, SortOrder.Ascending);
}
public List<User> GetGroupMembers(int groupId, int pageIndex, int pageSize, SortOrder sortOrder)
{
var query = new GroupQuery();
query.GroupID = groupId;
query.PageIndex = pageIndex;
query.PageSize = pageSize;
query.SortOrder = sortOrder;
return GetGroupMembers(query);
}
public List<User> GetGroupMembers(GroupQuery groupQuery)
{
// Actual implementation to get group members goes here
}
ه) زیاده روی نکنید!
آخرین موردی که توصیه شده این است که در تهیه توابع سربارگذاری شده زیاده روی نکنید. در این نوع موارد باید قانون 80/20 را در نظر گرفت. 2 تا 3 تابع سربارگذاری شده ارائه دهید که 80 درصد کار را انجام میدهند و سپس یک تابع سربارگذاری شدهی دیگر ارائه دهید که 20 درصد باقیمانده را پوشش دهد.
پ.ن.
در سی شارپ 4 ، با معرفی optional parameters ، شاید کمتر به سربارگذاری نیاز باشد.
پیشنیازها
«بررسی روش آپلود فایلها در ASP.NET Core»
«ارسال فایل و تصویر به همراه دادههای دیگر از طریق jQuery Ajax »
- در مطلب اول، روش دریافت فایلها از کلاینت، در سمت سرور و ذخیره سازی آنها در یک برنامهی ASP.NET Core بررسی شدهاست که کلیات آن در اینجا نیز صادق است.
- در مطلب دوم، روش کار با FormData استاندارد بررسی شدهاست. هرچند در مطلب جاری از jQuery استفاده نمیشود، اما نکات نحوهی کار با شیء FormData استاندارد، در اینجا نیز یکی است.
برپایی پروژههای مورد نیاز
ابتدا یک پوشهی جدید مانند UploadFilesSample را ایجاد کرده و در داخل آن دستور زیر را اجرا میکنیم:
dotnet new react
سپس در این پوشه، پوشهی ClientApp پیشفرض آنرا حذف میکنیم؛ چون کمی قدیمی است. همچنین فایلهای کنترلر و سرویس آب و هوای پیشفرض آنرا به همراه پوشهی صفحات Razor آن، حذف و پوشهی خالی wwwroot را نیز به آن اضافه میکنیم.
همچنین بجای تنظیم پیش فرض زیر در فایل کلاس آغازین برنامه:
spa.UseReactDevelopmentServer(npmScript: "start");
spa.UseProxyToSpaDevelopmentServer("http://localhost:3000");
اکنون در ریشهی پروژهی ASP.NET Core ایجاد شده، دستور زیر را صادر میکنیم تا پروژهی کلاینت React را با فرمت جدید آن ایجاد کند:
> create-react-app clientapp
> cd clientapp > npm install --save bootstrap axios react-toastify
- برای استفاده از شیوهنامههای بوت استرپ، بستهی bootstrap نیز در اینجا نصب میشود که برای افزودن فایل bootstrap.css آن به پروژهی React خود، ابتدای فایل clientapp\src\index.js را به نحو زیر ویرایش خواهیم کرد:
import "bootstrap/dist/css/bootstrap.css";
- برای نمایش پیامهای برنامه از کامپوننت react-toastify استفاده میکنیم که پس از نصب آن، با مراجعه به فایل app.js نیاز است importهای لازم آنرا اضافه کنیم:
import { ToastContainer } from "react-toastify"; import "react-toastify/dist/ReactToastify.css";
render() { return ( <React.Fragment> <ToastContainer />
ایجاد کامپوننت React فرم ارسال فایلها به سمت سرور
پس از این مقدمات، فایل جدید clientapp\src\components\UploadFileSimple.jsx را ایجاد کرده و به صورت زیر تکمیل میکنیم:
import React, { useState } from "react"; import axios from "axios"; import { toast } from "react-toastify"; export default function UploadFileSimple() { const [description, setDescription] = useState(""); const [selectedFile1, setSelectedFile1] = useState(); const [selectedFile2, setSelectedFile2] = useState(); return ( <form> <fieldset className="form-group"> <legend>Support Form</legend> <div className="form-group row"> <label className="form-control-label" htmlFor="description"> Description </label> <input type="text" className="form-control" name="description" onChange={event => setDescription(event.target.value)} value={description} /> </div> <div className="form-group row"> <label className="form-control-label" htmlFor="file1"> File 1 </label> <input type="file" className="form-control" name="file1" onChange={event => setSelectedFile1(event.target.files[0])} /> </div> <div className="form-group row"> <label className="form-control-label" htmlFor="file2"> File 2 </label> <input type="file" className="form-control" name="file2" onChange={event => setSelectedFile2(event.target.files[0])} /> </div> <div className="form-group row"> <button className="btn btn-primary" type="submit" > Submit </button> </div> </fieldset> </form> ); }
- توسط آن یک textbox به همراه دو فیلد ارسال فایل، به فرم اضافه شدهاند.
- مرحلهی بعد، دسترسی به فایلهای انتخابی کاربر و همچنین مقدار توضیحات وارد شدهاست. به همین جهت با استفاده از useState Hook، روش دریافت و تنظیم این مقادیر را مشخص کردهایم:
const [description, setDescription] = useState(""); const [selectedFile1, setSelectedFile1] = useState(); const [selectedFile2, setSelectedFile2] = useState();
- پس از طراحی state این فرم، مرحلهی بعدی، استفاده از متدهای set تمام useStateهای فوق است. برای مثال در مورد یک textbox معمولی، میتوان آنرا به صورت inline تعریف کرد و با هر بار تغییری در محتوای آن، این رخداد را به متد setDescription ارسال نمود تا مقدار وارد شده را به متغیر حالت description انتساب دهد:
<input type="text" className="form-control" name="description" onChange={event => setDescription(event.target.value)} value={description} />
<input type="file" className="form-control" name="file1" onChange={event => setSelectedFile1(event.target.files[0])} />
تشکیل مدل ارسال دادهها به سمت سرور
در فرمهای معمولی، عموما دادهها به صورت یک شیء JSON به سمت سرور ارسال میشوند؛ اما در اینجا وضع متفاوت است و به همراه توضیحات وارد شده، دو فایل باینری نیز وجود دارند.
در حالت ارسال متداول فرمهایی که به همراه المانهای دریافت فایل هستند، ابتدا یک ویژگی enctype با مقدار multipart/form-data به المان فرم اضافه میشود و سپس این فرم به سادگی قابلیت post-back به سمت سرور را پیدا میکند:
<form enctype="multipart/form-data" action="/upload" method="post"> <input id="file-input" type="file" /> </form>
let file = document.getElementById("file-input").files[0]; let formData = new FormData(); formData.append("file", file); fetch('/upload/image', {method: "POST", body: formData});
در یک برنامهی React نیز باید دقیقا چنین مراحلی طی شوند. تا اینجا کار دسترسی به مقدار files[0] و تشکیل متغیرهای حالت فرم را انجام دادهایم. در مرحلهی بعد، شیء FormData را تشکیل خواهیم داد:
// ... export default function UploadFileSimple() { // ... const handleSubmit = async event => { event.preventDefault(); const formData = new FormData(); formData.append("description", description); formData.append("file1", selectedFile1); formData.append("file2", selectedFile2); toast.success("Form has been submitted successfully!"); setDescription(""); }; return ( <form onSubmit={handleSubmit}> </form> ); }
ارسال مدل دادههای فرم React به سمت سرور
پس از تشکیل شیء FormData در متد مدیریت کنندهی handleSubmit، اکنون با استفاده از کتابخانهی axios، کار ارسال این اطلاعات را به سمت سرور انجام خواهیم داد:
// ... export default function UploadFileSimple() { const apiUrl = "https://localhost:5001/api/SimpleUpload/SaveTicket"; // ... const [isUploading, setIsUploading] = useState(false); const handleSubmit = async event => { event.preventDefault(); const formData = new FormData(); formData.append("description", description); formData.append("file1", selectedFile1); formData.append("file2", selectedFile2); try { setIsUploading(true); const { data } = await axios.post(apiUrl, formData, { headers: { "Content-Type": "multipart/form-data" }} }); toast.success("Form has been submitted successfully!"); console.log("uploadResult", data); setIsUploading(false); setDescription(""); } catch (error) { setIsUploading(false); toast.error(error); } }; return ( // ... ); }
در قطعه کد فوق، متغیر جدید حالت isLoading را نیز مشاهده میکنید. از آن میتوان برای فعال و غیرفعال کردن دکمهی submit فرم در زمان ارسال اطلاعات به سمت سرور، استفاده کرد:
<button disabled={ isUploading } className="btn btn-primary" type="submit" > Submit </button>
اعتبارسنجی سمت کلاینت فایلهای ارسالی به سمت سرور
در اینجا شاید نیاز باشد نوع و یا اندازهی فایلهای انتخابی توسط کاربر را تعیین اعتبار کرد. به همین جهت متدی را برای اینکار به صورت زیر تهیه میکنیم:
const isFileValid = selectedFile => { if (!selectedFile) { // toast.error("Please select a file."); return false; } const allowedMimeTypes = [ "image/png", "image/jpeg", "image/gif", "image/svg+xml" ]; if (!allowedMimeTypes.includes(selectedFile.type)) { toast.error(`Invalid file type: ${selectedFile.type}`); return false; } const maxFileSize = 1024 * 500; const fileSize = selectedFile.size; if (fileSize > maxFileSize) { toast.error( `File size ${(fileSize / 1024).toFixed( 2 )} KB must be less than ${maxFileSize / 1024} KB` ); return false; } return true; };
اکنون برای استفادهی از این متد دو راه وجود دارد:
الف) استفاده از آن در متد مدیریت کنندهی submit اطلاعات:
const handleSubmit = async event => { event.preventDefault(); if (!isFileValid(selectedFile1) || !isFileValid(selectedFile2)) { return; }
ب) استفادهی از آن جهت غیرفعال کردن دکمهی submit:
<button disabled={ isUploading || !isFileValid(selectedFile1) || !isFileValid(selectedFile2) } className="btn btn-primary" type="submit" > Submit </button>
نمایش درصد پیشرفت آپلود فایلها
کتابخانهی axios، امکان دسترسی به میزان اطلاعات آپلود شدهی به سمت سرور را به صورت یک رخداد فراهم کردهاست که در ادامه از آن برای نمایش درصد پیشرفت آپلود فایلها استفاده میکنیم:
const startTime = Date.now(); const { data } = await axios.post(apiUrl, formData, { headers: { "Content-Type": "multipart/form-data" }, onUploadProgress: progressEvent => { const { loaded, total } = progressEvent; const timeElapsed = Date.now() - startTime; const uploadSpeed = loaded / (timeElapsed / 1000); setUploadProgress({ queueProgress: Math.round((loaded / total) * 100), uploadTimeRemaining: Math.ceil((total - loaded) / uploadSpeed), uploadTimeElapsed: Math.ceil(timeElapsed / 1000), uploadSpeed: (uploadSpeed / 1024).toFixed(2) }); } });
const [uploadProgress, setUploadProgress] = useState({ queueProgress: 0, uploadTimeRemaining: 0, uploadTimeElapsed: 0, uploadSpeed: 0 });
const showUploadProgress = () => { const { queueProgress, uploadTimeRemaining, uploadTimeElapsed, uploadSpeed } = uploadProgress; if (queueProgress <= 0) { return <></>; } return ( <table className="table"> <thead> <tr> <th width="15%">Event</th> <th>Status</th> </tr> </thead> <tbody> <tr> <td> <strong>Elapsed time</strong> </td> <td>{uploadTimeElapsed} second(s)</td> </tr> <tr> <td> <strong>Remaining time</strong> </td> <td>{uploadTimeRemaining} second(s)</td> </tr> <tr> <td> <strong>Upload speed</strong> </td> <td>{uploadSpeed} KB/s</td> </tr> <tr> <td> <strong>Queue progress</strong> </td> <td> <div className="progress-bar progress-bar-info progress-bar-striped" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow={queueProgress} style={{ width: queueProgress + "%" }} > {queueProgress}% </div> </td> </tr> </tbody> </table> ); };
{showUploadProgress()}
در اینجا از کامپوننت progress-bar خود بوت استرپ برای نمایش درصد آپلود فایلها استفاده شدهاست. اگر style آنرا هر بار با مقدار جدید queueProgress به روز رسانی کنیم، سبب نمایش پویای این progress-bar خواهد شد.
یک نکته: اگر میخواهید درصد پیشرفت آپلود را در حالت آزمایش local بهتر مشاهده کنید، دربرگهی network، سرعت را بر روی 3G تنظیم کنید (مانند تصویر ابتدای بحث)؛ در غیراینصورت همان ابتدای کار به علت بالا بودن سرعت ارسال فایلها، 100 درصد را مشاهده خواهید کرد.
دریافت فرم React درخواست پشتیبانی، در سمت سرور و ذخیرهی فایلهای آن
بر اساس نحوهی تشکیل FormData سمت کلاینت:
const formData = new FormData(); formData.append("description", description); formData.append("file1", selectedFile1); formData.append("file2", selectedFile2);
using Microsoft.AspNetCore.Http; namespace UploadFilesSample.Models { public class Ticket { public int Id { set; get; } public string Description { set; get; } public IFormFile File1 { set; get; } public IFormFile File2 { set; get; } } }
پس از آن کنترلر ذخیره سازی اطلاعات Ticket را مشاهده میکنید:
using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using UploadFilesSample.Models; namespace UploadFilesSample.Controllers { [Route("api/[controller]")] [ApiController] public class SimpleUploadController : Controller { private readonly IWebHostEnvironment _environment; public SimpleUploadController(IWebHostEnvironment environment) { _environment = environment; } [HttpPost("[action]")] public async Task<IActionResult> SaveTicket([FromForm]Ticket ticket) { var file1Path = await saveFileAsync(ticket.File1); var file2Path = await saveFileAsync(ticket.File2); //TODO: save the ticket ... get id return Created("", new { id = 1001 }); } private async Task<string> saveFileAsync(IFormFile file) { const string uploadsFolder = "uploads"; var uploadsRootFolder = Path.Combine(_environment.WebRootPath, "uploads"); if (!Directory.Exists(uploadsRootFolder)) { Directory.CreateDirectory(uploadsRootFolder); } //TODO: Do security checks ...! if (file == null || file.Length == 0) { return string.Empty; } var filePath = Path.Combine(uploadsRootFolder, file.FileName); using (var fileStream = new FileStream(filePath, FileMode.Create)) { await file.CopyToAsync(fileStream); } return $"/{uploadsFolder}/{file.Name}"; } } }
- تزریق IWebHostEnvironment در سازندهی کلاس کنترلر، سبب میشود تا از طریق خاصیت WebRootPath آن، به wwwroot دسترسی پیدا کنیم و فایلهای نهایی را در آنجا ذخیره سازی کنیم.
- همانطور که ملاحظه میکنید، هنوز هم model binding کار کرده و میتوان شیء Ticket را به نحو متداولی دریافت کرد:
public async Task<IActionResult> SaveTicket([FromForm]Ticket ticket)
const { data } = await axios.post(apiUrl, formData, { headers: { "Content-Type": "multipart/form-data" }} });
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: UploadFilesSample.zip
برای اجرای آن، پس از صدور فرمان dotnet restore که سبب بازیابی وابستگیهای سمت کلاینت نیز میشود، ابتدا به پوشهی clientapp مراجعه کرده و فایل run.cmd را اجرا کنید. با اینکار react development server بر روی پورت 3000 شروع به کار میکند. سپس به پوشهی اصلی برنامهی ASP.NET Core بازگشت شده و فایل dotnet_run.bat را اجرا کنید. این اجرا سبب راه اندازی وب سرور برنامه و همچنین ارائهی برنامهی React بر روی پورت 5001 میشود.