همانطور که اطلاع دارید، کنترلهای Calendar و DatePicker در WPF، از تقویمهای مختلف پشتیبانی نمیکنند و نمونههایی که در سطح اینترنت موجود است، ظاهر و استایل مناسبی ندارند. بنابر این تصمیم گرفتم تا خودم دست به کار شوم و این کمبود را حل کنم. نتیجهی آن شد کتابخانهی PersianToolkit که بصورت استاندارد تقویم شمسی را به کنترلهای Calendar و DatePicker اضافه میکند و استایلهای زیبایی را هم به همراه خود دارد. این کتابخانه شامل تمام مناسبتهای شمسی، قمری و میلادی بوده و امکان نمایش روزهای تعطیل را نیز داراست.
همچنین پرشین تولکیت شامل توابع قدرتمندی جهت دریافت تاریخ بصورت شمسی، قمری و میلادی است. لازم به ذکر است که توابع محاسبه تاریخ قمری بسیار دقیق بوده و خطای بسیار کمی دارد. علاوه بر موارد گفته شده، پرشین تولکیت شامل استایلها و براشهای پیشفرض متنوعی میباشد که کاربر میتواند با توجه به سلیقهی خویش، رنگ بندی تقویم را تغییر دهد.
نحوهی نصب و استفاده
ابتدا پرشین تولکیت را از نیوگت نصب کنید:
سپس منابع برنامه را در فایل App.xaml بارگزاری کنید:
پرشین تولکیت شامل 3 پوسته پیشفرض است که میتوانید به راحتی در زمان اجرا، پوسته برنامه را تغییر دهید. قبل از آن نیاز است تا تابع تغییر پوسته را پیاده سازی کنید:
ورودی تابع از نوع SkinType میباشد که شامل Dark, Violet و Default است:
تغییر تقویم به حالت میلادی یا شمسی
برای تغییر نوع تقویم میتوانید از کلاس ConfigHelper استفاده کنید (به صورت پیشفرض تقویم شمسی فعال است و نیازی به تغییر تقویم به شمسی نیست):
گزینههای موجود
رنگها : روز جاری، روز انتخاب شده، روزهای تعطیل، کادر مناسبتها
شما میتوانید براحتی رنگ روز فعلی، روز انتخاب شده و روزهای تعطیل را تغییر دهید:
بصورت پیشفرض رنگهای زیر موجود است و با نوشتن آنها میتوانید از رنگهای پیشفرض استفاده کنید:
PrimaryBrush
SuccessBrush
InfoBrush
DangerBrush
WarningBrush
AccentBrush
LabelDefault
LabelPrimary
LabelSuccess
LabelInfo
LabelDanger
LabelWarning
همچنین پرشین تولکیت شامل توابع قدرتمندی جهت دریافت تاریخ بصورت شمسی، قمری و میلادی است. لازم به ذکر است که توابع محاسبه تاریخ قمری بسیار دقیق بوده و خطای بسیار کمی دارد. علاوه بر موارد گفته شده، پرشین تولکیت شامل استایلها و براشهای پیشفرض متنوعی میباشد که کاربر میتواند با توجه به سلیقهی خویش، رنگ بندی تقویم را تغییر دهد.
بروزرسانی:
نمونه مثال کامل به گیتهاب پروژه اضافه شد
کنترل تاریخ زمان به این مجموعه اضافه شد
پرشین تولکیت در 3 استایل مختلف موجود است که بصورت Runtime هم قابلیت تغییر استایل را دارد (روشن، تاریک، بنفش):
نحوهی نصب و استفاده
ابتدا پرشین تولکیت را از نیوگت نصب کنید:
Install-Package PersianToolkit
<ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="pack://application:,,,/PersianToolkit;component/Themes/SkinDefault.xaml"/> <ResourceDictionary Source="pack://application:,,,/PersianToolkit;component/Themes/Theme.xaml"/> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </ResourceDictionary.MergedDictionaries> </ResourceDictionary>
در هر کجا که نیاز به تقویم دارید ابتدا فضای نام برنامه را فراخوانی کنید سپس از اجزای آن استفاده کنید
تغییر پوسته برنامهxmlns:pc="http://github.com/ghost1372/PersianToolkit"<pc:Calendar/><pc:DatePicker/>
پرشین تولکیت شامل 3 پوسته پیشفرض است که میتوانید به راحتی در زمان اجرا، پوسته برنامه را تغییر دهید. قبل از آن نیاز است تا تابع تغییر پوسته را پیاده سازی کنید:
internal void UpdateSkin(SkinType skin) { ResourceDictionary skins0 = Resources.MergedDictionaries[0]; skins0.MergedDictionaries.Clear(); skins0.MergedDictionaries.Add(ResourceHelper.GetSkin(skin)); skins0.MergedDictionaries.Add(new ResourceDictionary { Source = new Uri("pack://application:,,,/PersianToolkit;component/Themes/Theme.xaml") }); Current.MainWindow?.OnApplyTemplate(); }
UpdateSkin(SkinType.Violet);
تغییر تقویم به حالت میلادی یا شمسی
برای تغییر نوع تقویم میتوانید از کلاس ConfigHelper استفاده کنید (به صورت پیشفرض تقویم شمسی فعال است و نیازی به تغییر تقویم به شمسی نیست):
ConfigHelper.Instance.SetLanguage(ConfigHelper.Language.Persian);
نکته: فقط یکبار آن هم در زمان شروع شدن برنامه میتوانید نوع تقویم را تغییر دهید. در زمان اجرا نمیتوانید نوع تقویم را تغییر دهید.
ShowHoliday :مناسبتها
جهت نمایش مناسبتها و روزهای تعطیل بر روی تقویم، میتوانید آن را فعال کنید:<pc:Calendar pc:Holiday.ShowHoliday="True"/>
رنگها : روز جاری، روز انتخاب شده، روزهای تعطیل، کادر مناسبتها
شما میتوانید براحتی رنگ روز فعلی، روز انتخاب شده و روزهای تعطیل را تغییر دهید:
<pc:Calendar pc:ColorStyle.HolidayDayBrush="{DynamicResource SuccessBrush}" pc:ColorStyle.SelectedDateBrush="{DynamicResource WarningBrush}" pc:ColorStyle.TodayDateBrush="{DynamicResource InfoBrush}"/>
PrimaryBrush
SuccessBrush
InfoBrush
DangerBrush
WarningBrush
AccentBrush
برای تغییر رنگ کادر عنوان مناسبتها باید از پراپرتی HolidayContentStyle استفاده کنید. دقت کنید که این پراپرتی ورودی از نوع Style را برای کنترل Label دریافت میکند. بصورت پیشفرض استایلهای زیر موجود است:
<pc:Calendar pc:ColorStyle.HolidayContentStyle="{StaticResource LabelPrimary}"/>
LabelPrimary
LabelSuccess
LabelInfo
LabelDanger
LabelWarning
تغییر رنگها و استایل به وسیله کدهای سی شارپ
شما میتوانید رنگها و استایلهای موجود را با سی شارپ هم تغییر دهید برای این منظور وارد کلاس ColorStyle شوید و پراپرتی موردنظر را انتخاب کنید و مقدار دلخواه را تنظیم کنید. برای سادگی کار، استایلها و رنگهای پیشفرض توسط کلاس ResourceHelper و ResourceBrushToken یا ResourceHolidayContentStyleToken قابل دسترسی هستند. ColorStyle.SetHolidayDayBrush(pc, ResourceHelper.GetResource<Brush>(ResourceBrushToken.SuccessBrush)); ColorStyle.SetHolidayContentStyle(pc, ResourceHelper.GetResource<Style>(ResourceHolidayContentStyleToken.LabelPrimary));
اشتراکها
راهنمای ارتقاء به Angular 5
نظرات مطالب
Bundling and Minifying Inline Css and Js
با تشکر از این نکته جدید.
به نظر من اگر فشرده سازی Response فعال باشه اصلا نیازی به حذف فواصل خالی در HTML نهایی نیست. چون حداقل کاری رو که الگوریتمهای فشرده سازی خوب انجام میدن، مدیریت فضاهای خالی است.
شاید بد نباشه به صورت یک کار تحقیقی بررسی بشه که اگر فشرده سازی رو فعال کردیم چند درصد روی حجم دریافتی تاثیر داره. اگر روش حذف فضاهای خالی رو بدون فشرده سازی اعمال کردیم، چند درصد فرقش هست.
اولین کاری را که میتوان پس از نصب Angular CLI انجام داد، ایجاد یک برنامهی جدید است و نمونهای از آنرا در قسمت قبل بررسی کردیم. در ادامه میخواهیم به پارامترهای بیشتر مرتبط با آن و همچنین نحوهی سفارشی سازی ایجاد برنامههای جدید بپردازیم.
ایجاد برنامههای جدید توسط Angular CLI
دستور خط فرمان ابتدایی ایجاد یک برنامهی جدید توسط Angular CLI به صورت ذیل است
در اینجا ng همان Angular CLI است. new عملی است که قرار است رخ دهد و my-app یک نام دلخواه میباشد.
پس از اجرای این دستور، برنامهی جدید ایجاد شده، در پوشهی جدید my-app قرار میگیرد.
گزینهی دیگر این دستور، استفاده از پرچم dry-run است:
کار این پرچم صرفا گزارش دادن جزئیات عملیات ng new است؛ بدون اینکه فایلی را تولید کند. به این ترتیب میتوان برآوردی را از فایلهای تولید شدهی توسط فرامین ng، پیش از تولید واقعی آنها، مشاهده کرد. برای مثال:
همانطور که مشاهده میکنید، در ابتدای کار پیامی را مبنی بر عدم نوشته شدن این فایلها بر روی فایل سیستم، ارائه دادهاست.
گزینهی دیگر دستور ng new را که در قسمت قبل ملاحظه کردید:
کار پرچم skip-install عدم فراخوانی خودکار دستور npm install است که سبب خواهد شد، برنامه بدون نصب وابستگیهای npm آن، با سرعت هرچه بیشتر، ایجاد شود. از این گزینه میتوان جهت مشاهدهی ساختار فایلهای تولیدی استفاده کرد و در نهایت در این حالت باید دستور npm install را به صورت دستی فراخوانی کرد. پرچم dry-run نیز skip-install را به صورت ضمنی به همراه دارد.
برای مشاهدهی سایر پرچمهای مرتبط با دستور ng new میتوان از پرچم help استفاده کرد:
بررسی فایل angular-cli.json.
فایل angular-cli.json. حاوی تنظیمات Angular CLI است.
در ابتدای این فایل، نام برنامهی جدید را مشاهده میکنید. این نام، همانی است که توسط دستور ng new my-app تعیین گردید.
سپس محل پوشهی source برنامه و همچنین خروجی نهایی آن، مشخص میشوند:
یکی از تنظیمات مهم این فایل، مقدار prefix است:
از این مقدار برای تنظیم مقدار prefix تمام کامپوننتها و دایرکتیوهای تولیدی توسط Angular CLI استفاده میشود. برای مثال اگر به فایل src\app\app.component.ts دقت کنید:
نام selector آن با app- شروع شدهاست که این app، از مقدار prefix فایل angular-cli.json. دریافت شدهاست.
تغییر این مقدار صرفا بر روی کامپوننتهای جدید تولید شدهی توسط Angular CLI تاثیرگذار خواهند بود. اگر میخواهید در ابتدای کار تولید یک برنامه، این مقدار را مشخص کنید، میتوان از پرچم prefix استفاده کرد و در صورت عدم ذکر آن، مقدار پیش فرض app برای آن درنظر گرفته میشود:
عدم ایجاد مخزن Git به همراه ng new
با صدور فرمان ng new، کار ایجاد یک مخزن Git نیز به صورت خودکار انجام خواهد شد. برای نمونه اگر خواستید برنامهای را بدون نصب وابستگیها، بدون ایجاد تستها و بدون ایجاد مخزن git آن تولید کنید، میتوان از دستور ذیل استفاده کرد:
استفادهی از sass بجای css توسط Angular CLI
سیستم Build همراه با Angular CLI مبتنی بر webpack است و به خوبی قابلیت پردازش فایلهای sass را نیز دارا است. اگر خواستید حالت پیش فرض تولید فایلهای css این ابزار را که در فایل angular-cli.json. نمونهای از آن ذکر شدهاست، به همراه فایلهایی مانند app.component.css، به sass تغییر دهید:
تنها کافی است پرچم style را با sass مقدار دهی کرد که حالت پیش فرض آن css است:
با ذکر این پرچم، تغییرات فایل angular-cli.json به صورت ذیل خواهند بود:
و حتی کامپوننت src\app\app.component.ts نیز به همراه شیوهنامهای از نوع sass که در حین ارائه نهایی توسط webpack به صورت خودکار پردازش میشود (بدون نیاز به تنظیمات اضافهتری)، مقدار دهی شدهاست:
و یا حتی اگر علاقمند به استفادهی از less باشید نیز میتوان پرچم style less-- را استفاده نمود.
انجام تنظیمات مسیریابی پیش فرض پروژه جدید توسط Angular CLI
حالت پیش فرض تولید برنامههای جدید Angular CLI به همراه تنظیمات مسیریابی آن نیست. اگر علاقمند هستید تا مبحث مسیریابی را خلاصه کرده و به سرعت تنظیمات ابتدایی مسیریابی را توسط این ابزار تولید کنید، میتوان پرچم routing را نیز در اینجا ذکر کرد:
در این حالت اگر به پوشهی src\app مراجعه کنید، فایل جدید app-routing.module.ts را نیز مشاهده خواهید کرد که AppRoutingModule پیش فرضی در آن تنظیم شدهاست و آمادهی استفاده میباشد.
همچنین فایل app.module.ts را نیز اندکی تغییر داده و این AppRoutingModule جدید را نیز به آن معرفی کردهاست.
به این ترتیب ارتباطات ابتدایی مورد نیاز سیستم مسیریابی برقرار شده و قابل استفادهاست. بنابراین ذکر پرچم routing میتواند یکی از پرچمهای اصلی ایجاد برنامههای جدید مبتنی بر Angular CLI باشد.
اجرای ابتدایی یک برنامهی مبتنی بر Angular CLI
پس از انتخاب پرچمهای مناسب جهت ایجاد یک پروژهی جدید مبتنی بر Angular CLI و همچنین نصب وابستگیهای آنها و یا عدم ذکر پرچم skip-install، اکنون نوبت به اجرای این پروژهاست. به همین جهت از طریق خط فرمان به ریشهی پوشهی برنامهی جدید ایجاد شده، وارد شوید. سپس دستور ذیل را صادر کنید:
در اینجا o- به معنای open است؛ یا گشودن آن در یک مرورگر. به این ترتیب کار کامپایل برنامه صورت گرفته و توسط مرورگر پیشفرض سیستم به صورت خودکار باز خواهد شد. آدرس پیش فرض آن نیز به صورت ذیل است:
نکتهی جالب این وب سرور در این است که تغییرات شما را به صورت خودکار دنبال کرده و بلافاصله ارائه میدهد. برای مثال فایل src\app\app.component.html را گشوده و به صورت ذیل تغییر دهید:
پس از ذخیرهی آن، بلافاصله خروجی نهایی را در مرورگر خواهید دید.
تغییر پیش فرضهای عمومی Angular CLI
تا اینجا مشاهده کردیم که اگر بخواهیم مقدار prefix پیش فرض را که به app تنظیم شدهاست به myCompany تغییر دهیم، یا میتوان از پرچم prefix در ابتدای کار فراخوانی دستور ng new استفاده کرد و یا میتوان فایل angular-cli.json. را نیز دستی ویرایش نمود. برای تغییر عمومی و سراسری مقدار پیش فرض app میتوان از دستور ng set استفاده کرد:
دستور اول فایل angular-cli.json. پروژهی جاری را ویرایش میکند و دستور دوم، فایل angular-cli.json سراسری Angular CLI را مقدار دهی خواهد کرد (با توجه به سوئیچ g- آن). البته بدیهی است ویرایشگرهای امروزی به خوبی قابلیت ویرایش فایلهای json را ارائه میدهند و شاید نیازی به استفادهی از این دستورات،حداقل برای اعمال تغییرات محلی نباشد.
و یا اگر بخواهید نوع شیوهنامهی مورد استفاده را ویرایش کنید، میتوان از یکی از دو دستور ذیل استفاده کرد (اولی محلی است و دومی عمومی):
اجرای امکانات Linting پروژههای مبتنی بر Angular CLI
برای بررسی کیفیت کدهای نوشته شده، میتوان از امکانات Linting استفاده کرد. برای این منظور تنها کافی است دستور ذیل را در ریشهی پروژه اجرا نمود:
برای مشاهدهی تمام گزینههای آن دستور زیر را صادر کنید:
اگر علاقمند هستید تا خروجی این ابزار، با رنگهای بهتری نمایش داده شوند، میتوان از دستور ذیل استفاده کرد:
به علاوه این ابزار قابلیت رفع مشکلات را با اعمال تغییراتی در کدهای موجود نیز به همراه دارد:
برای این منظور میتوان از پرچم fix آن استفاده کرد.
یک مثال: فایل src\app\app.component.ts را باز کنید و به عمد تعدادی مشکل را در آن ایجاد نمائید. برای نمونه دو سطر ابتدایی آنرا به صورت ذیل تغییر دهید:
اکنون اگر ng lint را اجرا کنیم، به خروجی ذیل خواهیم رسید:
عنوان میکند که بهتر است number به صورت یک const تعریف شود و همچنین یک سمیکالن در سطر اول فراموش شدهاست.
اکنون اگر دستور ng lint --fix را فراخوانی کنیم، تغییرات ذیل به فایل src\app\app.component.ts اعمال خواهند شد:
به صورت خودکار، به سطر اول، یک سمیکالن را اضافه کرده و همچنین سطر دوم را نیز به const تبدیل کردهاست.
ایجاد برنامههای جدید توسط Angular CLI
دستور خط فرمان ابتدایی ایجاد یک برنامهی جدید توسط Angular CLI به صورت ذیل است
> ng new my-app
پس از اجرای این دستور، برنامهی جدید ایجاد شده، در پوشهی جدید my-app قرار میگیرد.
گزینهی دیگر این دستور، استفاده از پرچم dry-run است:
> ng new my-app --dry-run
>ng new my-app --dry-run installing ng You specified the dry-run flag, so no changes will be written. create .editorconfig create README.md create src\app\app.component.css create src\app\app.component.html . . . Project 'my-app' successfully created.
گزینهی دیگر دستور ng new را که در قسمت قبل ملاحظه کردید:
> ng new my-app --skip-install
برای مشاهدهی سایر پرچمهای مرتبط با دستور ng new میتوان از پرچم help استفاده کرد:
> ng new --help
بررسی فایل angular-cli.json.
فایل angular-cli.json. حاوی تنظیمات Angular CLI است.
در ابتدای این فایل، نام برنامهی جدید را مشاهده میکنید. این نام، همانی است که توسط دستور ng new my-app تعیین گردید.
"project": { "name": "my-app" },
"apps": [ { "root": "src", "outDir": "dist",
یکی از تنظیمات مهم این فایل، مقدار prefix است:
"prefix": "app",
@Component({ selector: 'app-root',
تغییر این مقدار صرفا بر روی کامپوننتهای جدید تولید شدهی توسط Angular CLI تاثیرگذار خواهند بود. اگر میخواهید در ابتدای کار تولید یک برنامه، این مقدار را مشخص کنید، میتوان از پرچم prefix استفاده کرد و در صورت عدم ذکر آن، مقدار پیش فرض app برای آن درنظر گرفته میشود:
> ng new my-project --skip-install --prefix myCompany
عدم ایجاد مخزن Git به همراه ng new
با صدور فرمان ng new، کار ایجاد یک مخزن Git نیز به صورت خودکار انجام خواهد شد. برای نمونه اگر خواستید برنامهای را بدون نصب وابستگیها، بدون ایجاد تستها و بدون ایجاد مخزن git آن تولید کنید، میتوان از دستور ذیل استفاده کرد:
> ng new my-project --skip-install --skip-git --skip-tests --skip-commit
استفادهی از sass بجای css توسط Angular CLI
سیستم Build همراه با Angular CLI مبتنی بر webpack است و به خوبی قابلیت پردازش فایلهای sass را نیز دارا است. اگر خواستید حالت پیش فرض تولید فایلهای css این ابزار را که در فایل angular-cli.json. نمونهای از آن ذکر شدهاست، به همراه فایلهایی مانند app.component.css، به sass تغییر دهید:
"styles": [ "styles.css" ], "defaults": { "styleExt": "css", "component": {} }
> ng new my-project --skip-install --style sass
"styles": [ "styles.sass" ], "defaults": { "styleExt": "sass", "component": {} }
@Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.sass'] })
انجام تنظیمات مسیریابی پیش فرض پروژه جدید توسط Angular CLI
حالت پیش فرض تولید برنامههای جدید Angular CLI به همراه تنظیمات مسیریابی آن نیست. اگر علاقمند هستید تا مبحث مسیریابی را خلاصه کرده و به سرعت تنظیمات ابتدایی مسیریابی را توسط این ابزار تولید کنید، میتوان پرچم routing را نیز در اینجا ذکر کرد:
> ng new my-project --skip-install --routing
const routes: Routes = [ { path: '', children: [] } ];
imports: [ BrowserModule, FormsModule, HttpModule, AppRoutingModule ],
اجرای ابتدایی یک برنامهی مبتنی بر Angular CLI
پس از انتخاب پرچمهای مناسب جهت ایجاد یک پروژهی جدید مبتنی بر Angular CLI و همچنین نصب وابستگیهای آنها و یا عدم ذکر پرچم skip-install، اکنون نوبت به اجرای این پروژهاست. به همین جهت از طریق خط فرمان به ریشهی پوشهی برنامهی جدید ایجاد شده، وارد شوید. سپس دستور ذیل را صادر کنید:
>ng serve -o
http://localhost:4200/
نکتهی جالب این وب سرور در این است که تغییرات شما را به صورت خودکار دنبال کرده و بلافاصله ارائه میدهد. برای مثال فایل src\app\app.component.html را گشوده و به صورت ذیل تغییر دهید:
<h1> Test {{title}} </h1>
تغییر پیش فرضهای عمومی Angular CLI
تا اینجا مشاهده کردیم که اگر بخواهیم مقدار prefix پیش فرض را که به app تنظیم شدهاست به myCompany تغییر دهیم، یا میتوان از پرچم prefix در ابتدای کار فراخوانی دستور ng new استفاده کرد و یا میتوان فایل angular-cli.json. را نیز دستی ویرایش نمود. برای تغییر عمومی و سراسری مقدار پیش فرض app میتوان از دستور ng set استفاده کرد:
>ng set apps.prefix myCompany >ng set apps.prefix myCompany -g
و یا اگر بخواهید نوع شیوهنامهی مورد استفاده را ویرایش کنید، میتوان از یکی از دو دستور ذیل استفاده کرد (اولی محلی است و دومی عمومی):
>ng set defaults.styleExt sass >ng set defaults.styleExt sass -g
اجرای امکانات Linting پروژههای مبتنی بر Angular CLI
برای بررسی کیفیت کدهای نوشته شده، میتوان از امکانات Linting استفاده کرد. برای این منظور تنها کافی است دستور ذیل را در ریشهی پروژه اجرا نمود:
> ng lint
> ng lint --help
> ng lint --format stylish
>ng lint --fix
یک مثال: فایل src\app\app.component.ts را باز کنید و به عمد تعدادی مشکل را در آن ایجاد نمائید. برای نمونه دو سطر ابتدایی آنرا به صورت ذیل تغییر دهید:
import { Component } from '@angular/core' let number = 10;
>ng lint --format stylish /src/app/app.component.ts[3, 5]: Identifier 'number' is never reassigned; use 'const' instead of 'let'. /src/app/app.component.ts[1, 42]: Missing semicolon Lint errors found in the listed files.
اکنون اگر دستور ng lint --fix را فراخوانی کنیم، تغییرات ذیل به فایل src\app\app.component.ts اعمال خواهند شد:
import { Component } from '@angular/core'; const number = 10;
مطالب
MVC Scaffolding #3
شاید کیفیت کدهای تولیدی یا کدهای View حاصل از MVC Scaffolding مورد تائید شما نباشد. در این قسمت به نحوه تغییر و سفارشی سازی این موارد خواهیم پرداخت.
آشنایی با ساختار اصلی MVC Scaffolding
پس از نصب MVC Scaffolding از طریق NuGet به پوشه Packages مراجعه نمائید. در اینجا پوشههای MvcScaffolding، T4Scaffolding و T4Scaffolding.Core ساختار اصلی این بسته را تشکیل میدهند. برای نمونه اگر پوشه T4Scaffolding\tools را باز کنیم، شاهد تعدادی فایل ps1 خواهیم بود که همان فایلهای پاورشل هستند. مطابق طراحی NuGet، همواره فایلی با نام init.ps1 در ابتدا اجرا خواهد شد. همچنین در اینجا پوشههای T4Scaffolding\tools\EFRepository و T4Scaffolding\tools\EFDbContext نیز قرار دارند که حاوی قالبهای اولیه کدهای مرتبط با الگوی مخزن و DbContext تولیدی میباشند.
در پوشه MvcScaffolding\tools، ساختار قالبهای پیش فرض تولید Viewها و کنترلرهای تولیدی قرار دارند. در اینجا به ازای هر مورد، دو نگارش vb و cs قابل مشاهده است.
سفارشی سازی قالبهای پیش فرض Viewهای MVC Scaffolding
برای سفارشی سازی قالبهای پیش فرض از دستور کلی زیر استفاده میشود:
مانند دستور زیر:
در اینجا View نام یک Scaffolder است و Index نام قالبی در آن.
اگر دستور فوق را اجرا کنیم، فایل جدیدی به نام CodeTemplates\Scaffolders\MvcScaffolding.RazorView\Index.cs.t4 به پروژه جاری اضافه میشود. از این پس کلیه فرامین اجرایی، از نسخه محلی فوق بجای نمونههای پیش فرض استفاده خواهند کرد.
در ادامه قصد داریم اندکی این قالب پیش فرض را جهت اعمال ویژگی DisplayName به هدر جدول تولیدی نمایش اطلاعات Tasks تغییر دهیم. در کلاس Task، خاصیت زمان موعود با ویژگی DisplayName مزین شده است. این نام نمایشی حین تولید فرمهای ثبت و ویرایش اطلاعات بکار گرفته میشود، اما در زمان تولید جدول اطلاعات ثبت شده، به هدر جدول اعمال نمیگردد.
برای تغییر و بهبود این مساله، فایل Index.cs.t4 را که پیشتر به پروژه اضافه کردیم باز کنید. کلاس ModelProperty را یافته و خاصیت جدید DisplayName را به آن اضافه کنید:
در حالت پیش فرض فقط از خاصیت Name برای تولید هدر جدول در ابتدای فایل t4 در حال ویرایش استفاده میشود. در پایان فایل t4 جاری، متد زیر را اضافه کنید:
در اینجا بررسی میشود که آیا ویژگی DisplayNameAttribute بر روی خاصیت در حال بررسی وجود دارد یا خیر. اگر خیر از نام خاصیت استفاده خواهد شد و اگر بلی، مقدار ویژگی نام نمایشی استخراج شده و بازگشت داده میشود.
اکنون برای اعمال متد GetDisplayName، متد GetEligibleProperties را یافته و به نحو زیر تغییر دهید:
در اینجا خاصیت DisplayName به لیست خروجی اضافه شده است.
اکنون قسمت هدر جدول تولیدی را در ابتدای فایل t4 یافته و به نحو زیر تغییر میدهیم تا از DisplayName استفاده کند:
در ادامه برای آزمایش تغییرات فوق، دستور ذیل را صادر میکنیم:
پس از اجرای دستور، به فایل Views\Tasks\Index.cshtml مراجعه نمائید. اینبار هدر خودکار تولیدی از Due Date بجای DueDate استفاده کرده است.
سفارشی سازی قالبهای پیش فرض کنترلرهای MVC Scaffolding
در ادامه قصد داریم کدهای الگوی مخزن تهیه شده را اندکی تغییر دهیم. برای مثال با توجه به اینکه از تزریق وابستگیها استفاده خواهیم کرد، نیازی به سازنده اولیه پیش فرض کنترلر که در بالای آن ذکر شده «در صورت استفاده از یک DI این مورد را حذف کنید»، نداریم. برای این منظور دستور زیر را اجرا کنید:
در اینجا قصد ویرایش قالب پیش فرض کنترلرهای تشکیل شده با استفاده از الگوی مخزن را داریم. نام ControllerWithRepository از فایل ControllerWithRepository.cs.t4 موجود در پوشه packages\MvcScaffolding\tools\Controller گرفته شده است.
به این ترتیب فایل جدید CodeTemplates\Scaffolders\MvcScaffolding.Controller\ControllerWithRepository.cs.t4 به پروژه جاری اضافه خواهد شد. در این فایل چند سطر ذیل را یافته و سپس حذف کنید:
برای آزمایش آن دستور زیر را صادر نمائید:
چون تنها قصد تغییر کنترلر را داریم از پارامتر ForceMode با مقدار ControllerOnly استفاده شده است.
یا اگر نیاز به تغییر کدهای الگوی مخزن مورد استفاده است میتوان از دستور ذیل استفاده کرد:
به این ترتیب فایل جدید CodeTemplates\Scaffolders\EFRepository\EFRepositoryTemplate.cs.t4 جهت ویرایش به پروژه جاری اضافه خواهد شد.
لیست Scaffolderهای مهیا با دستور Get-Scaffolder قابل مشاهده است.
آشنایی با ساختار اصلی MVC Scaffolding
پس از نصب MVC Scaffolding از طریق NuGet به پوشه Packages مراجعه نمائید. در اینجا پوشههای MvcScaffolding، T4Scaffolding و T4Scaffolding.Core ساختار اصلی این بسته را تشکیل میدهند. برای نمونه اگر پوشه T4Scaffolding\tools را باز کنیم، شاهد تعدادی فایل ps1 خواهیم بود که همان فایلهای پاورشل هستند. مطابق طراحی NuGet، همواره فایلی با نام init.ps1 در ابتدا اجرا خواهد شد. همچنین در اینجا پوشههای T4Scaffolding\tools\EFRepository و T4Scaffolding\tools\EFDbContext نیز قرار دارند که حاوی قالبهای اولیه کدهای مرتبط با الگوی مخزن و DbContext تولیدی میباشند.
در پوشه MvcScaffolding\tools، ساختار قالبهای پیش فرض تولید Viewها و کنترلرهای تولیدی قرار دارند. در اینجا به ازای هر مورد، دو نگارش vb و cs قابل مشاهده است.
سفارشی سازی قالبهای پیش فرض Viewهای MVC Scaffolding
برای سفارشی سازی قالبهای پیش فرض از دستور کلی زیر استفاده میشود:
Scaffold CustomTemplate Name Template
Scaffold CustomTemplate View Index
اگر دستور فوق را اجرا کنیم، فایل جدیدی به نام CodeTemplates\Scaffolders\MvcScaffolding.RazorView\Index.cs.t4 به پروژه جاری اضافه میشود. از این پس کلیه فرامین اجرایی، از نسخه محلی فوق بجای نمونههای پیش فرض استفاده خواهند کرد.
در ادامه قصد داریم اندکی این قالب پیش فرض را جهت اعمال ویژگی DisplayName به هدر جدول تولیدی نمایش اطلاعات Tasks تغییر دهیم. در کلاس Task، خاصیت زمان موعود با ویژگی DisplayName مزین شده است. این نام نمایشی حین تولید فرمهای ثبت و ویرایش اطلاعات بکار گرفته میشود، اما در زمان تولید جدول اطلاعات ثبت شده، به هدر جدول اعمال نمیگردد.
[DisplayName("Due Date")] public DateTime? DueDate { set; get; }
// Describes the information about a property on the model class ModelProperty { public string Name { get; set; } public string DisplayName { get; set; } public string ValueExpression { get; set; } public EnvDTE.CodeTypeRef Type { get; set; } public bool IsPrimaryKey { get; set; } public bool IsForeignKey { get; set; } public bool IsReadOnly { get; set; } }
static string GetDisplayName(EnvDTE.CodeProperty prop) { var displayAttr = prop.Attributes.OfType<EnvDTE80.CodeAttribute2>().Where(x => x.FullName == typeof(System.ComponentModel.DisplayNameAttribute).FullName).FirstOrDefault(); if(displayAttr == null) { return prop.Name; } return displayAttr.Value.Replace("\"",""); }
اکنون برای اعمال متد GetDisplayName، متد GetEligibleProperties را یافته و به نحو زیر تغییر دهید:
results.Add(new ModelProperty { Name = prop.Name, DisplayName = GetDisplayName(prop), ValueExpression = "Model." + prop.Name, Type = prop.Type, IsPrimaryKey = Model.PrimaryKeyName == prop.Name, IsForeignKey = ParentRelations.Any(x => x.RelationProperty == prop), IsReadOnly = !prop.IsWriteable() });
اکنون قسمت هدر جدول تولیدی را در ابتدای فایل t4 یافته و به نحو زیر تغییر میدهیم تا از DisplayName استفاده کند:
<# List<ModelProperty> properties = GetModelProperties(Model.ViewDataType, true); foreach (ModelProperty property in properties) { if (!property.IsPrimaryKey && !property.IsForeignKey) { #> <th> <#= property.DisplayName #> </th> <# } } #>
PM> Scaffold Controller -ModelType Task -ControllerName TasksController -DbContextType TasksDbContext -Repository -Force
سفارشی سازی قالبهای پیش فرض کنترلرهای MVC Scaffolding
در ادامه قصد داریم کدهای الگوی مخزن تهیه شده را اندکی تغییر دهیم. برای مثال با توجه به اینکه از تزریق وابستگیها استفاده خواهیم کرد، نیازی به سازنده اولیه پیش فرض کنترلر که در بالای آن ذکر شده «در صورت استفاده از یک DI این مورد را حذف کنید»، نداریم. برای این منظور دستور زیر را اجرا کنید:
PM> Scaffold CustomTemplate Controller ControllerWithRepository
به این ترتیب فایل جدید CodeTemplates\Scaffolders\MvcScaffolding.Controller\ControllerWithRepository.cs.t4 به پروژه جاری اضافه خواهد شد. در این فایل چند سطر ذیل را یافته و سپس حذف کنید:
// If you are using Dependency Injection, you can delete the following constructor public <#= Model.ControllerName #>() : this(<#= String.Join(", ", Repositories.Values.Select(x => "new " + x.RepositoryTypeName + "()")) #>) { }
PM> Scaffold Controller -ModelType Task -ControllerName TasksController -DbContextType TasksDbContext -Repository -Force -ForceMode ControllerOnly
یا اگر نیاز به تغییر کدهای الگوی مخزن مورد استفاده است میتوان از دستور ذیل استفاده کرد:
Scaffold CustomScaffolder EFRepository
لیست Scaffolderهای مهیا با دستور Get-Scaffolder قابل مشاهده است.
مطالب دورهها
خطا ها و مدیریت خطا (Exception Handling)
مدیریت خطا در #F شبیه به الگوی try catch finally در #C است. برای تعریف خطا از کلمه کلیدی exception استفاده میکنیم و
یک نام رو به اون اختصاص میدهیم و میتونیم به صورت اختیاری یک نوع داده رو
هم برای این خطا با استفاده از کلمه کلیدی of تعیین کنیم.
با استفاده از دستور raise میتونیم یک exception رو پرتاب کنیم.(به دلیل
اینکه در دات نت از دستور throw به معنی پرتاب کردن استفاده میکنیم این جا
نیز از همین لغت استفاده کردم کما اینکه در #F دستور raise جایگزین throw
شده است). البته در جاهایی که قصد ما از پرتاب exception فقط متوقف کردن
عملیات و نمایش یک خطا است میتونیم از دستور failwith به همراه یک پیغام نیز استفاده کنیم.(یک نمونه از آن را در فصلهای قبلی مشاهده کردید)
ساختار کلی try catch finally در #F به صورت زیر است.(تنها تفاوت در کلمه with به جای catch است)
یا به صورت
*نکته مهم: در #F شما اجازه استفاده از finally رو به همراه with ندارید.به همین دلیل من این ساختارو به دو صورت بالا نوشتم.
یک مثال از try with:
در کد با در هر خط توضیحات لازم داده شده است. نکته قابل ذکر این است که در #C زمانی که قصد داشته باشیم یک استثنا جدید ایجاد کنیم باید کلاسی جدیدی که از کلاس System.Exception ارث برده باشد(یا هر کلاس دیگری که خود از این System.Exception ارث برده است) ایجاد کنیم و کدهای مورد نظر رو در اون قرار بدیم. ولی در اینجا (در قسمتی که رنگ آن متفاوت است) به راحتی توانستیم یک استثنا جدید بر اساس نیاز بسازیم.
یک مثال از try finally :
عملکرد finally در #F دقیقا مشابه با عملکرد finally در #C است. یعنی دستورات بلوک finally همواره (چه استثنا رخ بدهد و چه رخ ندهد) اجرا خواهد شد.
*توجه : برنامه نویسانی که قبلا با OCaml کدنویسی کرده اند هنگام برنامه نویسی #F از raise کردنهای زیاد و بی مورد استثناها خودداری کنند. به دلیل نوع معماری CLR پرتاب کردن استثنا و مدیریت آن کمی هزینه بر است (بیشتر از زبان Ocaml). البته این مسئله در زبانهای تحت دات نت نیز مطرح است کما اینکه در #C نیز مدیریت استثناها رو در بالاترین لایه انجام میدهیم و از catch کردن بی مورد استثنائات در لایههای زیرین خودداری میکنیم.
یک مثال از الگوی Matching در try with
exception myError of int
ساختار کلی try catch finally در #F به صورت زیر است.(تنها تفاوت در کلمه with به جای catch است)
try // try code here with //catch statement here
try // try code here finally //finally statement here
یک مثال از try with:
exception WrongSecond of int//یک exception تعریف میکنیم let primes = [ 2; 3; 5; 7; 11; 13; 17; 19; 23; 29; 31; 37; 41; 43; 47; 53; 59 ] // یک تابع برای تست اینکه آیا ثانیه الان در لیست prime وجود دارد یا نه let testSecond() = try let currentSecond = System.DateTime.Now.Second in // شرط برای اینکه مشخص شود که ثانیه در لیست است یا خیر if List.exists (fun x -> x = currentSecond) primes then // اگر بود یک خطا تولید میشود failwith "A prime second" else // اگر نیود یک استثنا از نوع wrongSecond پرتاب میشود raise (WrongSecond currentSecond) with // catch کردن استثناها WrongSecond x -> printf "The current was %i, which is not prime" x
در کد با در هر خط توضیحات لازم داده شده است. نکته قابل ذکر این است که در #C زمانی که قصد داشته باشیم یک استثنا جدید ایجاد کنیم باید کلاسی جدیدی که از کلاس System.Exception ارث برده باشد(یا هر کلاس دیگری که خود از این System.Exception ارث برده است) ایجاد کنیم و کدهای مورد نظر رو در اون قرار بدیم. ولی در اینجا (در قسمتی که رنگ آن متفاوت است) به راحتی توانستیم یک استثنا جدید بر اساس نیاز بسازیم.
یک مثال از try finally :
// تابعی برای نوشتن فایل let writeToFile() = //ابتدا فایل به صورت متنی ساخته میشود let file = System.IO.File.CreateText("test.txt") try // متن مورد نظر در فایل نوشته میشود file.WriteLine("Hello F# users") finally //فایل مورد نظر بسته میشود.این دستور حتی اگر در هنگام نوشتن فایل استثنا هم رخ بدهد اجرا خواهد شد file.Dispose()
*توجه : برنامه نویسانی که قبلا با OCaml کدنویسی کرده اند هنگام برنامه نویسی #F از raise کردنهای زیاد و بی مورد استثناها خودداری کنند. به دلیل نوع معماری CLR پرتاب کردن استثنا و مدیریت آن کمی هزینه بر است (بیشتر از زبان Ocaml). البته این مسئله در زبانهای تحت دات نت نیز مطرح است کما اینکه در #C نیز مدیریت استثناها رو در بالاترین لایه انجام میدهیم و از catch کردن بی مورد استثنائات در لایههای زیرین خودداری میکنیم.
یک مثال از الگوی Matching در try with
let getNumber msg = printf msg; try int32(System.Console.ReadLine()) with | :? System.FormatException -> -1 | :? System.OverflowException -> System.Int32.MinValue | :? System.ArgumentNullException -> 0
طراحی API برنامه توسط Actionها
روش مرسوم طراحی Fluent interfaces، جهت ارائه روش ساخت اشیاء مسطح به کاربران بسیار مناسب هستند. اما اگر سعی در تهیه API عمومی برای کار با اشیاء چند سطحی مانند معرفی فایلهای XML توسط کلاسهای سی شارپ کنیم، اینبار Fluent interfaces آنچنان قابل استفاده نخواهند بود و نمیتوان این نوع اشیاء را به شکل روانی با کنار هم قرار دادن زنجیر وار متدها تولید کرد. برای حل این مشکل روش طراحی خاصی در نگارشهای اخیر NHibernate معرفی شده است به نام loquacious interface که این روزها در بسیاری از APIهای جدید شاهد استفاده از آن هستیم و در ادامه با پشت صحنه و طرز تفکری که در حین ساخت این نوع API وجود دارد آشنا خواهیم شد.
در ابتدا کلاسهای مدل زیر را در نظر بگیرید که قرار است توسط آنها ساختار یک جدول از کاربر دریافت شود:
در روش طراحی loquacious interface به ازای هر کلاس مدل، یک کلاس سازنده ایجاد خواهد شد. اگر در کلاس جاری، خاصیتی از نوع کلاس یا لیست باشد، برای آن نیز کلاس سازنده خاصی درنظر گرفته میشود و این روند ادامه پیدا میکند تا به خواصی از انواع ابتدایی مانند int و string برسیم:
نقطه آغازین API ایی که در اختیار استفاده کنندگان قرار میگیرد با متد CreateTable ایی شروع میشود که ساخت شیء جدول را به ظاهر توسط یک Action به استفاده کننده واگذار کرده است، اما توسط کلاس TableCreator او را مقید و راهنمایی میکند که چگونه باید اینکار را انجام دهد.
همچنین در بدنه متد CreateTable، نکته نحوه دریافت خروجی از Action ایی که به ظاهر خروجی خاصی را بر نمیگرداند نیز قابل مشاهده است.
همانطور که عنوان شد کلاسهای xyzCreator تا رسیدن به خواص معمولی و ابتدایی پیش میروند. برای مثال در سطح اول چون خاصیت عرض از نوع float است، صرفا با یک متد معمولی دریافت میشود. دو خاصیت دیگر نیاز به Creator دارند تا در سطحی دیگر برای آنها سازندههای سادهتری را طراحی کنیم.
همچنین باید دقت داشت که در این طراحی تمام متدها از نوع void هستند. اگر قرار است خاصیتی را بین خود رد و بدل کنند، این خاصیت به صورت internal تعریف میشود تا در خارج از کتابخانه قابل دسترسی نباشد و در intellisense ظاهر نشود.
مرحله بعد، ایجاد دو کلاس HeaderCreator و CellsCreator است تا کلاس TableCreator تکمیل گردد:
نحوه ایجاد کلاسهای Builder و یا Creator این روش بسیار ساده و مشخص است:
مقدار هر خاصیت معمولی توسط یک متد ساده void دریافت خواهد شد.
هر خاصیتی که اندکی پیچیدگی داشته باشد، نیاز به یک Creator جدید خواهد داشت.
کار هر Creator بازگشت دادن مقدار یک شیء است یا نهایتا ساخت یک لیست از یک شیء. این مقدار از طریق یک خاصیت internal بازگشت داده میشود.
البته عموما بجای معرفی مستقیم کلاسهای Creator از یک اینترفیس معادل آنها استفاده میشود. سپس کلاس Creator را internal تعریف میکنند تا خارج از کتابخانه قابل دسترسی نباشد و استفاده کننده نهایی فقط با توجه به متدهای void تعریف شده در interface کار تعریف اشیاء را انجام خواهد داد.
در نهایت، مثال تکمیل شده ما به شکل زیر خواهد بود:
نحوه استفاده از این طراحی نیز جالب توجه است:
این نوع طراحی مزیتهای زیادی را به همراه دارد:
الف) ساده سازی طراحی اشیاء چند سطحی و تو در تو
ب) امکان درنظر گرفتن مقادیر پیش فرض برای خواص
ج) سادهتر سازی تعاریف لیستها
د) استفاده کنندگان در حین استفاده نهایی و تعریف اشیاء به سادگی میتوانند کدنویسی کنند (مثلا سلولها را با یک حلقه اضافه کنند).
ه) امکان بهتر استفاده از امکانات Intellisense. برای مثال فرض کنید یکی از خاصیتهایی که قرار است برای آن Creator درست کنید یک interface را میپذیرد. همچنین در برنامه خود چندین پیاده سازی کمکی از آن نیز وجود دارد. یک روش این است که مستندات قابل توجهی را تهیه کنید تا این امکانات توکار را گوشزد کند؛ روش دیگر استفاده از طراحی فوق است. در اینجا در کلاس Creator ایجاد شده چون امکان معرفی متد وجود دارد، میتوان امکانات توکار را توسط این متدها نیز معرفی کرد و به این ترتیب Intellisense تبدیل به راهنمای اصلی کتابخانه شما خواهد شد.
روش مرسوم طراحی Fluent interfaces، جهت ارائه روش ساخت اشیاء مسطح به کاربران بسیار مناسب هستند. اما اگر سعی در تهیه API عمومی برای کار با اشیاء چند سطحی مانند معرفی فایلهای XML توسط کلاسهای سی شارپ کنیم، اینبار Fluent interfaces آنچنان قابل استفاده نخواهند بود و نمیتوان این نوع اشیاء را به شکل روانی با کنار هم قرار دادن زنجیر وار متدها تولید کرد. برای حل این مشکل روش طراحی خاصی در نگارشهای اخیر NHibernate معرفی شده است به نام loquacious interface که این روزها در بسیاری از APIهای جدید شاهد استفاده از آن هستیم و در ادامه با پشت صحنه و طرز تفکری که در حین ساخت این نوع API وجود دارد آشنا خواهیم شد.
در ابتدا کلاسهای مدل زیر را در نظر بگیرید که قرار است توسط آنها ساختار یک جدول از کاربر دریافت شود:
using System; using System.Collections.Generic; namespace Test { public class Table { public Header Header { set; get; } public IList<Cell> Cells { set; get; } public float Width { set; get; } } public class Header { public string Title { set; get; } public DateTime Date { set; get; } public IList<Cell> Cells { set; get; } } public class Cell { public string Caption { set; get; } public float Width { set; get; } } }
using System; using System.Collections.Generic; namespace Test { public class TableApi { public Table CreateTable(Action<TableCreator> action) { var creator = new TableCreator(); action(creator); return creator.TheTable; } } public class TableCreator { readonly Table _theTable = new Table(); internal Table TheTable { get { return _theTable; } } public void Width(float value) { _theTable.Width = value; } public void AddHeader(Action<HeaderCreator> action) { _theTable.Header = ... } public void AddCells(Action<CellsCreator> action) { _theTable.Cells = ... } } }
همچنین در بدنه متد CreateTable، نکته نحوه دریافت خروجی از Action ایی که به ظاهر خروجی خاصی را بر نمیگرداند نیز قابل مشاهده است.
همانطور که عنوان شد کلاسهای xyzCreator تا رسیدن به خواص معمولی و ابتدایی پیش میروند. برای مثال در سطح اول چون خاصیت عرض از نوع float است، صرفا با یک متد معمولی دریافت میشود. دو خاصیت دیگر نیاز به Creator دارند تا در سطحی دیگر برای آنها سازندههای سادهتری را طراحی کنیم.
همچنین باید دقت داشت که در این طراحی تمام متدها از نوع void هستند. اگر قرار است خاصیتی را بین خود رد و بدل کنند، این خاصیت به صورت internal تعریف میشود تا در خارج از کتابخانه قابل دسترسی نباشد و در intellisense ظاهر نشود.
مرحله بعد، ایجاد دو کلاس HeaderCreator و CellsCreator است تا کلاس TableCreator تکمیل گردد:
using System; using System.Collections.Generic; namespace Test { public class CellsCreator { readonly IList<Cell> _cells = new List<Cell>(); internal IList<Cell> Cells { get { return _cells; } } public void AddCell(string caption, float width) { _cells.Add(new Cell { Caption = caption, Width = width }); } } public class HeaderCreator { readonly Header _header = new Header(); internal Header Header { get { return _header; } } public void Title(string title) { _header.Title = title; } public void Date(DateTime value) { _header.Date = value; } public void AddCells(Action<CellsCreator> action) { var creator = new CellsCreator(); action(creator); _header.Cells = creator.Cells; } } }
مقدار هر خاصیت معمولی توسط یک متد ساده void دریافت خواهد شد.
هر خاصیتی که اندکی پیچیدگی داشته باشد، نیاز به یک Creator جدید خواهد داشت.
کار هر Creator بازگشت دادن مقدار یک شیء است یا نهایتا ساخت یک لیست از یک شیء. این مقدار از طریق یک خاصیت internal بازگشت داده میشود.
البته عموما بجای معرفی مستقیم کلاسهای Creator از یک اینترفیس معادل آنها استفاده میشود. سپس کلاس Creator را internal تعریف میکنند تا خارج از کتابخانه قابل دسترسی نباشد و استفاده کننده نهایی فقط با توجه به متدهای void تعریف شده در interface کار تعریف اشیاء را انجام خواهد داد.
در نهایت، مثال تکمیل شده ما به شکل زیر خواهد بود:
using System; using System.Collections.Generic; namespace Test { public class TableCreator { readonly Table _theTable = new Table(); internal Table TheTable { get { return _theTable; } } public void Width(float value) { _theTable.Width = value; } public void AddHeader(Action<HeaderCreator> action) { var creator = new HeaderCreator(); action(creator); _theTable.Header = creator.Header; } public void AddCells(Action<CellsCreator> action) { var creator = new CellsCreator(); action(creator); _theTable.Cells = creator.Cells; } } public class CellsCreator { readonly IList<Cell> _cells = new List<Cell>(); internal IList<Cell> Cells { get { return _cells; } } public void AddCell(string caption, float width) { _cells.Add(new Cell { Caption = caption, Width = width }); } } public class HeaderCreator { readonly Header _header = new Header(); internal Header Header { get { return _header; } } public void Title(string title) { _header.Title = title; } public void Date(DateTime value) { _header.Date = value; } public void AddCells(Action<CellsCreator> action) { var creator = new CellsCreator(); action(creator); _header.Cells = creator.Cells; } } }
var data = new TableApi().CreateTable(table => { table.Width(1); table.AddHeader(header=> { header.Title("new rpt"); header.Date(DateTime.Now); header.AddCells(cells=> { cells.AddCell("cell 1", 1); cells.AddCell("cell 2", 2); }); }); table.AddCells(tableCells=> { tableCells.AddCell("c 1", 1); tableCells.AddCell("c 2", 2); }); });
این نوع طراحی مزیتهای زیادی را به همراه دارد:
الف) ساده سازی طراحی اشیاء چند سطحی و تو در تو
ب) امکان درنظر گرفتن مقادیر پیش فرض برای خواص
ج) سادهتر سازی تعاریف لیستها
د) استفاده کنندگان در حین استفاده نهایی و تعریف اشیاء به سادگی میتوانند کدنویسی کنند (مثلا سلولها را با یک حلقه اضافه کنند).
ه) امکان بهتر استفاده از امکانات Intellisense. برای مثال فرض کنید یکی از خاصیتهایی که قرار است برای آن Creator درست کنید یک interface را میپذیرد. همچنین در برنامه خود چندین پیاده سازی کمکی از آن نیز وجود دارد. یک روش این است که مستندات قابل توجهی را تهیه کنید تا این امکانات توکار را گوشزد کند؛ روش دیگر استفاده از طراحی فوق است. در اینجا در کلاس Creator ایجاد شده چون امکان معرفی متد وجود دارد، میتوان امکانات توکار را توسط این متدها نیز معرفی کرد و به این ترتیب Intellisense تبدیل به راهنمای اصلی کتابخانه شما خواهد شد.
بازخوردهای پروژهها
نمایش متن در زیر جدول مربوط به هر گروه
با سلام.
اطلاعات من بر اساس یک فیلد گروهبندی میشوند و هر گروه هم ممکن است از یک صفحه تجاوز کند. میخواهم در زیر جدول به ازای هر گروه یک سری توضیحات درج کنم. با تشکر.
نگارش ابتدایی «iTextSharp.LGPLv2.Core » بر اساس کدهای اولیهی iTextSharp بود که مستقیما از جاوا به سیشارپ ترجمه شده بود. این کدها پر بودند از ساختارهای دادهای مانند Hashtable و ArrayList که مرتبط هستند با روزهای آغازین ارائهی دات نت 1؛ پیش از ارائهی Generics. برای مثال نوع Hashtable، همانند ساختار دادهی Dictionary عمل میکند، اما جنریک نیست؛ یعنی شبیه به <Dictionary<object, object عمل میکند و برای کار با آن، باید مدام از تبدیل نوعهای دادهها (یا همان boxing) از نوع object، به نوع دادهی مدنظر، استفاده کرد که این تبدیل نوعها، همیشه به همراه کاهش کارآیی هم هستند. به علاوه در حین کار با Hashtable، اگر کلیدی در مجموعهی آن وجود نداشته باشد، فقط نال را بازگشت میدهد، اما Dictionary، یک استثنای یافت نشدن کلید را صادر میکند. بنابراین فرض کنید که با هزاران سطر کد استفاده کنندهی از Hashtable طرف هستید که اگر آنها را تبدیل به Dictionaryهای جنریک متناسبی کنید تا کارآیی برنامه بهبود یابد، تمام موارد استفادهی از آنهارا نیز باید به همراه TryGetValueها کنید تا از شر استثنای یافت نشدن کلید درخواستی، در امان باشید. در این مطلب روش مواجه شدن با یک چنین حالتی را با حداقل تغییر در کدها بررسی خواهیم کرد.
ممنوع کردن استفادهی از ساختارهای دادهی غیرجنریک
قدم اول مواجه شدن با یک چنین کدهای قدیمی، ممنوع کردن استفادهی از ساختارهای دادهی غیرجنریک و الزام به تبدیل آنها به نوعهای جدید است. برای این منظور میتوان از Microsoft.CodeAnalysis.BannedApiAnalyzers استفاده کرد که توضیحات بیشتر آنرا در مطلب «غنی سازی کامپایلر C# 9.0 با افزونهها» پیشتر بررسی کردهایم. به صورت خلاصه، ابتدا بستهی نیوگت آنرا به صورت یک آنالایزر جدید به فایل csproj. برنامه معرفی میکنیم:
همچنین در اینجا نیاز است یک فایل متنی BannedSymbols.txt را نیز به آن معرفی کرد؛ برای مثال با این محتوا:
این تنظیمات سبب خواهند شد تا اگر در کدهای ما، ساختارهای دادهی غیرجنریکی در حال استفاده بودند، با یک اخطار ظاهر شوند و جهت سختگیری بیشتر، روش تبدیل اخطارها به خطاها را نیز در مطلب «غنی سازی کامپایلر C# 9.0 با افزونهها» بررسی کردهایم تا مجبور به اصلاح آنها شویم.
پیشنهاد یک دیکشنری کم دردسرتر!
برای نمونه پس از تنظیمات فوق، مجبور به تغییر تمام hash tableها به دیکشنریهای جدید جنریک خواهیم شد؛ اما ... اگر اینکار را انجام دهیم، برنامهای که تا پیش از این بدون مشکل کار میکرد، اکنون با استثناهای متعدد یافت نشدن کلیدها، خاتمه پیدا میکند! چون دیگر دیکشنریهای جدید، همانند hash tableهای قدیمی، در صورت عدم وجود کلیدی، نال را بازگشت نمیدهند.
برای رفع این مشکل و اصلاح انبوهی از کدها با حداقل تغییرات و عدم تکرار TryGetValueها در همهجا، میتوان دسترسی به ایندکسهای یک دیکشنری استاندارد دات نت را به صورت زیر با ارثبری از آن، بازنویسی کرد:
همانطور که مشاهده میکنید، اگر بجای Dictionary، از NullValueDictionary پیشنهادی استفاده کنیم، دیگر نیازی نیست تا هزاران TryGetValue را در سراسر کدهای برنامه، تکرار و پراکنده کنیم و با حداقل تغییرات میتوان معادل بهتری را بجای Hashtable قدیمی داشت.
ممنوع کردن استفادهی از ساختارهای دادهی غیرجنریک
قدم اول مواجه شدن با یک چنین کدهای قدیمی، ممنوع کردن استفادهی از ساختارهای دادهی غیرجنریک و الزام به تبدیل آنها به نوعهای جدید است. برای این منظور میتوان از Microsoft.CodeAnalysis.BannedApiAnalyzers استفاده کرد که توضیحات بیشتر آنرا در مطلب «غنی سازی کامپایلر C# 9.0 با افزونهها» پیشتر بررسی کردهایم. به صورت خلاصه، ابتدا بستهی نیوگت آنرا به صورت یک آنالایزر جدید به فایل csproj. برنامه معرفی میکنیم:
<Project Sdk="Microsoft.NET.Sdk"> <ItemGroup> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> </ItemGroup> <ItemGroup> <AdditionalFiles Include="$(MSBuildThisFileDirectory)BannedSymbols.txt" Link="Properties/BannedSymbols.txt"/> </ItemGroup> </Project>
# https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.BannedApiAnalyzers/BannedApiAnalyzers.Help.md T:System.Collections.ICollection;Don't use a non-generic data structure. T:System.Collections.Hashtable;Don't use a non-generic data structure. T:System.Collections.ArrayList;Don't use a non-generic data structure. T:System.Collections.SortedList;Don't use a non-generic data structure. T:System.Collections.Stack;Don't use a non-generic data structure. T:System.Collections.Queue;Don't use a non-generic data structure.
پیشنهاد یک دیکشنری کم دردسرتر!
برای نمونه پس از تنظیمات فوق، مجبور به تغییر تمام hash tableها به دیکشنریهای جدید جنریک خواهیم شد؛ اما ... اگر اینکار را انجام دهیم، برنامهای که تا پیش از این بدون مشکل کار میکرد، اکنون با استثناهای متعدد یافت نشدن کلیدها، خاتمه پیدا میکند! چون دیگر دیکشنریهای جدید، همانند hash tableهای قدیمی، در صورت عدم وجود کلیدی، نال را بازگشت نمیدهند.
برای رفع این مشکل و اصلاح انبوهی از کدها با حداقل تغییرات و عدم تکرار TryGetValueها در همهجا، میتوان دسترسی به ایندکسهای یک دیکشنری استاندارد دات نت را به صورت زیر با ارثبری از آن، بازنویسی کرد:
/// <summary> /// This custom IDictionary doesn't throw a KeyNotFoundException while accessing its value by a given key /// </summary> public interface INullValueDictionary<TKey, TValue> : IDictionary<TKey, TValue> { new TValue this[TKey key] { get; set; } } /// <summary> /// This custom IDictionary doesn't throw a KeyNotFoundException while accessing its value by a given key /// </summary> public class NullValueDictionary<TKey, TValue> : Dictionary<TKey, TValue>, INullValueDictionary<TKey, TValue> { public new TValue this[TKey key] { get => TryGetValue(key, out var val) ? val : default; set => base[key] = value; } }