مطالب
معرفی و راهنمایی جهت انتخاب پلتفرم‌های توسعه‌ی رابط کاربری جدید مایکروسافت
اگر به تکنولوژی‌های شرکت مایکروسافت علاقمند باشید و اخبار آن را دنبال کرده باشید قطعا در جریان هستید که علاوه بر تکنولوژی‌های قدیمی (WPF, UWP, Xamarin) تکنولوژی‌های جدیدی (Project Reunion, Maui, WinUI, Uno, Xaml Island) نیز بصورت همزمان در حال توسعه هستند. اکثر این تکنولوژی‌ها شبیه و نزدیک به هم هستند و برای کسی که تازه کار باشد ممکن است دچار سردرگمی شود و چون بصورت همزمان در حال توسعه می‌باشند ممکن سوالاتی برای شما پیش بیاید. در این مطلب هر کدام از این تکنولوژی‌ها را معرفی کرده و در انتخاب صحیح به شما کمک خواهیم کرد.

ساخت برنامه با WPF
به کمک تکنولوژی WPF میتوانیم نرم افزارهای دسکتاپ را توسعه دهیم. WPF همچنان پشتیبانی میشود و در سال‌های اخیر بصورت متن باز نیز منتشر شده‌است. اگر نیاز دارید که برنامه شما در ویندوز‌های 7 تا ویندوز 11 اجرا شود، میتوانید از WPF استفاده کنید. لازم به ذکر است که برنامه‌های WPF به عنوان Win32 یا Desktop نیز شناخته میشوند.

ساخت برنامه با UWP
UWP بعد از WPF و با انتشار ویندوز 10 معرفی شد. علت انتشار، هماهنگی برنامه‌ها با سیستم عامل ویندوز 10 و امنیت بیشتر بود. به‌طور فنی برنامه‌ای که بصورت UWP ساخته میشود، همان WPF است؛ با این تفاوت که داخل SandBox اجرا میشود و با محیط خارج ارتباطی ندارد. بدلیل مسائل امنیتی، بسیاری از کارهای ساده و مهم در UWP غیرممکن (البته راه حل‌هایی نیز وجود دارد) می‌باشد و نیاز به دسترسی کاربر دارد. به عنوان مثال، API‌های system.Io.File یا Process قابل استفاده نمی‌باشند.
نرم افزارهایی که با UWP ساخته میشوند فقط بر روی ویندوز 10 به بالا قابلیت اجرایی دارند و توزیع آن از طریق استور مایکروسافت امکان پذیر است. در صورت نیاز به توزیع دستی (فایل نصبی)، توسعه دهنده باید فایل نصبی را بصورت دیجیتال، امضاء کند که دردسر‌های خودش را دارد.

ساخت برنامه با Xamarin
اگر نیاز دارید که برای سیستم عامل اندروید و مک برنامه بنویسید، زامارین میتواند به شما کمک کند.

ساخت برنامه با WinUI
بعد از معرفی UWP نیاز به یک فریمورک رابط کاربری قوی جهت جذب کاربران به سمت UWP احساس شد. در نتیجه مایکروسافت فریمورک WinUI را ایجاد کرد. WinUI در 2 نسخه در حال توسعه می‌باشد:

WinUI 2.X
این نسخه از WinUI فقط قابلیت استفاده در برنامه‌های مبتنی بر UWP را دارد. اخیرا نسخه 2.6 آن منتشر شده که شامل تغییرات بصری عظیمی می‌باشد. لازم به ذکر است که ویندوز 11 که اخیرا معرفی شد، بر پایه WinUI 2.6 ایجاد شده است.

WinUI 3.X
این نسخه از WinUI قابلیت استفاده در پلتفرم‌های دیگر را محیا می‌کند و هم اکنون بصورت پیش نمایش است و بر پایه WinUI 2.5 می‌باشد. در 3 ماهه آخر سال 2021 تمامی استایل‌ها بر پایه نسخه 2.6 خواهد بود.
 

پلتفرم Uno
پلتفرم اونو توسط مایکروسافت ایجاد نشده، اما توسط آن پشتیبانی میشود. شما به کمک پلتفرم اونو میتوانید به کمک WinUI 3، برنامه‌های خود را در ویندوز 7 (به کمک موتور رندر Skia ) تا ویندوز 11، لینوکس (به کمک Skia)، مک و حتی موبایل اجرا کنید.

پلتفرم Maui
مائویی در واقع نسل بعدی زامارین می‌باشد و بصورت تک پروژه‌ای ایجاد شده‌است. در زامارین شما برای هر پلتفرم (ویندوز، اندروید، مک و...) یک پروژه جداگانه داشتید، اما در مائویی فقط یک پروژه واحد وجود دارد. پس اگر نیاز دارید که برای گوشی‌های همراه برنامه نویسی کنید، میتوانید از مائویی استفاده کنید. لازم به ذکر است به کمک مائویی میتوانید برای لینوکس و مک هم برنامه ایجاد کنید. اما بدلیل وجود WinUI در سایر پلتفرم‌ها، بهتر است از مائویی فقط برای ایجاد برنامه‌های موبایل استفاده کنید.
 

پلتفرم Project Reunion
اخیرا نام این پروژه به Windows App SDK تغییر یافته‌است. به کمک این پروژه میتوانید از WinUI 3 در برنامه‌های WPF و سایر تکنولوژی‌های Desktop استفاده کنید و کل برنامه خود را مدرن کنید. لازم به ذکر است که برنامه‌های ساخته شده توسط Reunion فقط در ویندوز 10 به بالا اجرا میشوند. در حال حاضر جهت اجرای برنامه نیاز هست که برنامه بصورت MSIX پکیج بشود. در نسخه 1.0 که تا چند ماه آینده منتشر خواهد شد، نیازی به پکیج کردن نخواهد بود.

پلتفرم Xaml Island
این پلتفرم در واقع پلی است که میتوانید از کنترل‌های UWP یا WinUI در برنامه‌های دسکتاپ (WPF) استفاده کنید. تفاوت این پلتفرم با Reunion در این است که شما فقط میتوانید بخشی از برنامه خود را مدرن کنید و قسمت‌های مدرن شده در ویندوز‌های پایین‌تر از ویندوز 10، کار نخواهند کرد. اما در Reunion تمام بخش‌های برنامه شما مدرن خواهد شد.

سخن آخر اینکه اگر نیاز به برنامه‌های موبایل دارید، بهتر است از مائویی استفاده کنید بدلیل اینکه:
  • نسل بعدی زامارین است
  • از دات نت 6 به بالا استفاده می‌کند
  • روان، سریع و انعطاف پذیر است
  • خطاهای بسیار کمتری دارد
  • مخصوص موبایل طراحی شده است
  • تجربه بیشتری نسبت به سایر پلتفرم‌ها دارد

اگر نیاز به اجرای برنامه بصورت کراس پلتفرم دارید (ویندوز/لینوکس/مک) بهتر است از Uno استفاده کنید بدلیل اینکه:
  • مخصوص کراس پلتفرم طراحی شده
  • از WinUI 3 استفاده می‌کند
  • برای ویندوز 10 به بالا از تکنولوژی UWP و برای ویندوز 7 و لینوکس از Skia استفاده می‌کند

اگر نیاز دارید برنامه شما فقط در ویندوز 10 به بالا اجرا شود بهتر است از Project Reunion استفاده کنید بدلیل اینکه:
  • از WinUI 3 استفاده می‌کند
  • تمام ویژگی‌های UWP را دارد
  • محدودیت‌های UWP را ندارد
  • بصورت Full Trust اجرا میشود
  • پیچیدگی‌های UWP را ندارد
  • از پلتفرم WPF برای اجرا استفاده می‌کند

اگر نیاز دارید که برنامه شما در ویندوز 7 به بالا اجرا شود و در ویندوز 10 ظاهر مدرن‌تری به خود بگیرد بهتر است از Xaml Island استفاده کنید بدلیل اینکه:
  • فقط بخشی از برنامه را مدرن می‌کند
  • قسمت‌های مدرن شده در نسخه‌های قبل ویندوز 10 اجرا نمیشود

بازخوردهای دوره
آشنایی با مدل برنامه نویسی TAP
استفاده از async به معنای خالی کردن ترد جاری کدهای مدیریت شده‌ی دات نت است و انجام سایر کارهای برنامه و صبر کردن برای دریافت پاسخی است که در سمت کدهای مدیریت شده نیازی به پردازش و محاسبه ندارد.
برای مثال در حالت کار با یک دیتابیس، این موتور بانک اطلاعاتی است که کوئری رسیده را پردازش می‌کند و برنامه‌ی ما صرفا درخواستی را به آن ارائه داده است. به این ترتیب در اینجا استفاده از async برای خالی کردن ترد جاری و صبر کردن جهت دریافت نتیجه‌ی اطلاعات از سرور مفید است و میزان پاسخ‌دهی برنامه را بالا می‌برد.
بنابراین استفاده از async در سمت کدهای دات نتی، تاثیری بر روی عملکرد یک بانک اطلاعاتی ندارد. فقط در سمت کدهای ما است که برنامه تا رسیدن و محاسبه‌ی درخواست توسط بانک اطلاعاتی، هنگ نمی‌کند.
در این حالت اگر برنامه‌ی شما ویندوزی است، ترد UI آن آزاد شده و برنامه مدام در حال هنگ به نظر نمی‌رسد. اگر برنامه‌ی وب است، ترد جاری آن آزاد شده و thread pool برنامه می‌تواند از این ترد آزاد شده، برای پردازش سایر درخواست‌های رسیده توسط کاربران استفاده کند. به این ترتیب بازدهی و اصطلاحا throughput سرور افزایش پیدا می‌کند.
در حال حاضر تمام APIهای جدید مایکروسافت نسخه‌ی async را هم اضافه کرده‌اند. برای مثال اگر از EF استفاده می‌کنید، از نسخه‌ی 6 آن به بعد، متدهایی مانند ToListAsync برای کوئری گرفتن معمولی غیرهمزمان و SaveChangesAsync برای ذخیره سازی اطلاعات به صورت غیرهمزمان، اضافه شده‌اند. یک مثال کامل در این مورد در اینجا
البته بدیهی است تمام ORMهای دات نتی در سطح پایین خودشان از ADO.NET استفاده می‌کنند. ADO.NET نیز Async API سازگار با دات نت 4.5 به بعد را مدتی است که اضافه کرده‌است. برای مثال متدهایی مانند ExecuteReaderAsync ، GetFieldValueAsync و ExecuteNonQueryAsync و امثال آن به زیر ساخت ADO.NET اضافه شده‌اند. اطلاعات بیشتر
مطالب
لینک‌های هفته‌ی آخر بهمن

وبلاگ‌ها ، سایت‌ها و مقالات ایرانی (داخل و خارج از ایران)

امنیت

Visual Studio

ASP. Net

طراحی و توسعه وب

اس‌کیوال سرور

سی شارپ

عمومی دات نت

مسایل اجتماعی و انسانی برنامه نویسی

کتاب‌های رایگان جدید

متفرقه
مطالب
چند نکته کوتاه و عمومی در مورد قبل و بعد از نصب ویندوز 7

- فایل ISO ایی که از سایت‌های این دور و اطراف قابل دریافت است، یک DVD از نوع bootable به شما ارائه نخواهد داد. بنابراین نیاز است تا این قابلیت را فراهم کرد. خود مایکروسافت برنامه سورس بازی را در آدرس زیر جهت تهیه یک bootable DVD یا حتی bootable flash drive از روی فایل ISO دریافت شده ارائه داده است:


- نیاز است تا کلیه افزونه‌ها، کلمات عبور ذخیره شده و غیره‌ی فایرفاکس را بتوان به سیستم جدید انتقال داد. ابزار رایگان و سورس باز MozBackup این‌کار را به بهترین نحو ممکن میسر می‌سازد: +

- پس از نصب، نیاز به صفحه کلید فارسی مرغوب می‌باشد : + و +

- پس از نصب ویندوز 7 ، درایو ویندوز قبلی موجود شما ناپدید شده است! نحوه‌ی رفع مشکل : +

- هر چند در پیدا کردن درایورها از اینترنت مشکلی ندارد، اما بد نیست یکبار هم سیستم را با ابزار رایگانی جهت به روز بودن درایورها بررسی کرد: +

- اگر هنگام کار با Virtual PC مایکروسافت هنگام اتصال به VPN با خطای 721 متوقف شدید، نگران نباشید. با انتخاب گزینه‌ی run as admin این مشکل برطرف می‌شود.

- تنظیم DNS جدید گوگل نیز توصیه می‌شود: +

و یک گجت تقویم فارسی هم کفایت می‌کند : + و یا +

مطالب
آموزش زبان Rust - قسمت 2 - نصب Rust و ایجاد یک پروژه‌ی جدید
نصب Rust

برای نصب rust، متناسب با سیستم عامل خود، ابتدا وارد سایت rustup  شوید و فایل دانلود متناسب با سیستم عامل مورد نظرتان را دانلود و نصب کنید.

Cargo  چیست و چه کاربردی دارد؟

Cargo همراه با زبان برنامه نویسی Rust گنجانده شده‌، همزمان نصب می‌شود و برای ایجاد، ساخت و مدیریت پروژه‌های Rust استفاده می‌گردد. این یک رابط سطح بالا برای کار با کدهای Rust را ارائه می‌دهد که شروع به کار با Rust و مدیریت پروژه‌های خود را برای توسعه دهندگان آسان‌تر می‌کند.
Cargo سیستم ساخت و package manager مخصوص زبان برنامه نویسی Rust است. ابزاری است که به توسعه دهندگان Rust کمک می‌کند تا پروژه‌های خود را با خودکارسازی کارهایی مانند کامپایل کد، مدیریت وابستگی‌ها، اجرای آزمایش‌ها و ایجاد بسته‌های قابل توزیع، مدیریت کنند.

برخی از ویژگی‌های Cargo

Dependency management: برنامه Cargo می‌تواند به‌طور خودکار وابستگی‌های پروژه‌های Rust را دانلود کرده، بسازد و مدیریت کند. این باعث می‌شود توسعه دهندگان به راحتی کتابخانه‌ها و ماژول‌های جدیدی را به پروژه‌های خود اضافه کنند.

Building and testing: برنامه Cargo می‌تواند پروژه‌های Rust را بسازد و test‌ها را به صورت خودکار اجرا کند. همچنین گزینه‌هایی را برای ساختن ساخت‌های بهینه یا اشکال زدایی فراهم می‌کند.

Packaging: برنامه Cargo می‌تواند بسته‌های قابل توزیعی را مانند tarballs یا بسته‌های باینری را برای پروژه‌های Rust ایجاد کند.

Customization: برنامه Cargo به توسعه دهندگان اجازه می‌دهد تا فرآیند ساخت برنامه‌ی خود را با تعیین گزینه‌های ساخت مختلف، در فایل پیکربندی Cargo.toml سفارشی کنند. به‌طور کلی Cargo توسعه و مدیریت پروژه‌های Rust را با ارائه یک ابزار کارآمد برای خودکارسازی بسیاری از وظایف توسعه رایج، ساده می‌کند.


ایجاد اولین پروژه‌ی Rust

بعد از نصب rust برای ایجاد یک پروژه جدید، terminal سیستم عامل را باز کنید و دستور زیر را در آن وارد کنید:
cargo new  project_name
هنگامیکه یک پروژه‌ی جدید Rust را با استفاده از نام پروژه‌ی مورد نظر ایجاد می‌کنید، یک دایرکتوری با نام داده شده، حاوی فایل‌ها و دایرکتوری‌های زیر ایجاد می‌گردد:
Cargo.toml : این فایل manifest پروژه است که در آن نام پروژه، نسخه، وابستگی‌ها و سایر ابرداده‌ها را مشخص می‌کنید. فایل Cargo.toml حاوی یک metadata درباره پروژه است؛ مانند نام، نسخه، نویسندگان و وابستگی‌های آن. در اینجا مثالی از شکل ظاهری یک فایل Cargo.toml آورده شده است:
[package]
name = "my-project"
version = "0.1.0"
authors = ["John Doe <johndoe@example.com>"]

[dependencies]
serde = "1.0"
serde_json = "1.0"
بخش [package] حاوی یک metadata در مورد خود پروژه، از جمله نام، نسخه، نویسندگان و جزئیات دیگر است.
بخش [dependencies] وابستگی‌های پروژه را فهرست می‌کند. در این مثال، پروژه به پکیج‌های serde و serde_json بستگی دارد که برای serialization و deserialization داده‌ها استفاده می‌شوند.
src directory: این دایرکتوری حاوی کد منبع پروژه شما است. به طور پیش فرض، شامل یک فایل main.rs است که نقطه ورود برنامه شما است.
target directory: این فهرست شامل فایل‌های باینری کامپایل شده تولید شده توسط کامپایلر Rust می‌باشد.

هنگامی که cargo build یا cargo run را اجرا می‌کنید، Cargo به طور خودکار یک دایرکتوری target/debug را برای ذخیره‌ی فایل‌های باینری کامپایل شده ایجاد می‌کند. اگر cargo build --release را اجرا کنید، Cargo بجای آن، یک دایرکتوری target/release را ایجاد می‌کند. بعلاوه، اگر هنگام ایجاد پروژه‌ی خود از نسخه‌ی خاصی از Rust (مانند نسخه‌ی 2018) استفاده کنید، Cargo یک فیلد نسخه را در فایل Cargo.toml شما برای تعیین نسخه، اضافه می‌کند. 
مطالب
توسعه برنامه‌های Cross Platform با Xamarin Forms & Bit Framework - قسمت نهم
در قسمت‌های قبل یاد گرفتید که چطور View و View Model را در متد RegisterTypes در App.xaml.cs به یکدیگر وصل کرده و به آن‌ها یک اسم دهید و با Navigation Service آن‌ها را در حالت‌های مختلف مثل Navigation Page و Master Detail و Popup و ... باز کنید و بین صفحات جابجا شوید.
در View‌ها دو مورد را به صورت ابتدایی توضیح دادیم، Binding و Command. در این قسمت وارد جزئیات می‌شویم.

Command قطعه‌ای کد در View Model است (عموما یک متد CSharp ای) که در View می‌تواند مورد استفاده قرار گیرد. برای این که کلیک بر روی یک دکمه، متد مربوطه در View Model را اجرا کند، آن را درون یک BitDelegateCommand قرار دادیم و به Command دکمه Bind کردیم.
می‌دانیم که Button، بجز کلیک، دارای Event‌های دیگری نیز هست. علاوه بر این، ممکن است بجای کلیک دکمه، بخواهیم زمانیکه روی یک عکس عمل Swipe انجام می‌شود، کاری در سمت View Model انجام شود.
برای حل این مشکل از Event To Command و Gesture Recognizer استفاده می‌کنیم. مورد اول همانطور که از نام‌اش پیداست، وظیفه آن اتصال بر قرار کردن بین یک Event و یک Command است و دومی نیز برای مسائلی چون Swipe - Tap - Pan - Pinch و ... بکار می‌رود.
در کد پایین که می‌تواند در HelloWorldView.xaml در پروژه XamApp نیز تست شود، یکبار Command دکمه را Bind کرده‌ایم و یکبار Clicked Event آن را، که البته نتیجه یکی است! به جای نام Clicked می‌توان نام هر یک از Event‌‌ها را نوشت. همچنین این کار بجز Button، بر روی باقی کنترل‌ها نیز کار می‌کند.
        <Button Command="{Binding IncreaseStepsCountCommand}" Text="Button with command binding" />
        <Button Text="Button with event to command">
            <Button.Behaviors>
                <prismBehaviors:EventToCommandBehavior Command="{Binding IncreaseStepsCountCommand}" EventName="Clicked" />
            </Button.Behaviors>
        </Button>
همانطور که می‌بینید، EventToCommandBehavior که توسط Prism ارائه شده‌است، به Behavior‌های Button اضافه شده‌است. هر Behavior، رفتاری خاص را به کنترل‌های شما اضافه می‌کند و امکان نوشتن Custom behavior نیز وجود دارد که از موضوع این بحث خارج است.

prismBehaviors در بالای فایل Xaml به شکل زیر تعریف شده است:
xmlns:prismBehaviors="clr-namespace:Prism.Behaviors;assembly=Prism.Forms"
این عمل معادل using نوشتن در بالای فایل‌های CSharp است و می‌گوید که EventToCommandBehavior در کدام namespace و dll است. وقتی این کار را می‌کنید، می‌توانید در Xaml به تمامی کلاس‌های آن namespace دسترسی داشته باشید.

در همان مثال HelloWorldView.xaml، یک Label بود که تعداد زده شدن دکمه را نشان می‌داد. اگر بخواهیم وقتی روی آن Label به سمت چپ Swipe می‌کنیم نیز Command مربوطه اجرا شود، خواهیم داشت:
        <Label Text="{Binding StepsCount, StringFormat='{}Button tapped {0} times!'}">
            <Label.GestureRecognizers>
                <SwipeGestureRecognizer Command="{Binding IncreaseStepsCountCommand}" Direction="Left" />
            </Label.GestureRecognizers>
        </Label>
در صورتیکه قصد تست بر روی Windows-UWP را دارید و صفحه نمایش لمسی ندارید، مطابق با آموزش قسمت Windows همین سری آموزشی، بر روی Simulator خروجی بگیرید.

در مورد Command، مطالبی چون Command Parameter و Event Args Parameter Path و Event Args Converter باقی می‌مانند که در قسمت‌های بعدی به آن‌ها نیز خواهیم پرداخت. ولی به صورت کلی در نظر داشته باشید که جای منطق برنامه، در سمت View Model است و اگر در سناریویی نمی‌دانستید که چگونه در واکنش به تغییری در View، در سمت View Model کاری کنید، می‌توانید همینجا سوال خود را بپرسید.

Binding نیز همان طور که تا الان متوجه شده‌اید، باعث اتصال دو چیز به یک دیگر می‌شود. وقتی می‌نویسیم:
<Label Text="{Binding StepsCount, StringFormat='{}Button tapped {0} times!'}" />
داریم Text را که یک Property در Label در View است، به StepsCount که یک Property در View Model است، وصل می‌کنیم.
هر Binding یک Source دارد. به صورت پیش فرض، چون داریم از Prism استفاده می‌کنیم، تمامی Binding‌ها در View به صورت پیش فرض به View Model اشاره می‌کنند؛ مگر اینکه چیز دیگری بگوییم. برای مثال، اگر بخواهیم یک Label داشته باشیم که متن یک Entry را نمایش می‌دهد، هیچ‌یک از این دو در View Model نیستند. پس در این Binding خاص، باید سورس را نیز مشخص کنیم:
        <Entry x:Name="MyEntry" />
        <Label Text="{Binding Text, Source={x:Reference MyEntry}}" />
این باعث می‌شود که Text، که یک Property در Label است، به Text، که یک Property در کنترلی با نام MyEntry است، وصل شود.

x:DataType
Binding‌ها به صورت پیش فرض، در زمان اجرا و با کمک Reflection کار می‌کنند؛ این مهم دو مسئله را ایجاد می‌کند:
1- اگر در View، نام Property مد نظر در View Model را به اشتباه بنویسید، تا زمانیکه برنامه را اجرا نکنید و وارد آن فرم نشوید، متوجه مشکل دار بودن ماجرا نمی‌شوید و وقتی در View Model نام یک Property را عوض می‌کنید، در سمت View در هنگام Build خطایی نمی‌گیرید.
2- Reflection چیز خوبی است و برخی سناریوها بدون آن خیلی سخت پیاده سازی می‌شوند؛ ولی Reflection سربار کارآیی دارد و بهتر است در صورت امکان از آن پرهیز شود.
برای حل این مشکل در Xamarin Forms امکانی به نام x:DataType معرفی شده‌است که در View می‌گویید که Source چه کلاسی است، که قاعدتا View Model است. برای این کار داریم:
<ContentPage
...
    xmlns:vm="clr-namespace:XamApp.ViewModels"
    x:DataType="vm:HelloWorldViewModel">
با این روش، هر دو مشکل فوق حل می‌شوند و هم خطای زمان Build برای Binding‌های اشتباه خواهیم داشت و هم بدون Reflection از Binding‌ها استفاده می‌کنیم که بهبود سرعت برنامه را در بر دارد.

INotifyPropertyChanged
در مثال XamApp، وقتی روی دکمه کلیک می‌کنیم، مقدار StepsCount را که یک Property در View Model است، یکی یکی افزایش می‌دهیم. این Property در View به Label بایند شده‌است. وقتی مقداری در View Model که سورس Binding ما محسوب می‌شود رخ می‌دهد، View چگونه خبر دار می‌شود؟
هر کلاسی که بخواهد در Binding به عنوان Source استفاده شود (منجمله View Model)، می‌تواند یک interface را پیاده سازی کند؛ به نام INotifyPropertyChanged. همچنین تعریف یک Property نیز باید به این شکل باشد:
        private int _StepsCount;
        public int StepsCount
        {
            get => _StepsCount;
            set => SetProperty(ref _StepsCount, value);
        }
طبیعتا این کد که در سناریوهایی می‌تواند حتی پیچیده‌تر شود، جالب نیست و برای همین ما از  PropertyChanged.Fody استفاده کرده‌ایم که در زمان Build، خودش همه مسائل را مدیریت می‌کند و کد فوق به سادگی می‌تواند به صورت زیر نوشته شود:
public int StepsCount { get; set; }
به صفحه GitHub این کتابخانه مراجعه کنید و با سایر امکانات فوق العاده‌ی آن آشنا شوید!
در Binding، موارد دیگری چون Value Converters و Relative Source نیز وجود دارند که در ادامه با آنها آشنا می‌شویم. اگر در Binding نیز مشکلی دارید، می‌توانید همینجا نیز بپرسید.
مطالب
آشنایی با NHibernate - قسمت دهم

آشنایی با کتابخانه NHibernate Validator

پروژه جدیدی به پروژه NHibernate Contrib در سایت سورس فورج اضافه شده است به نام NHibernate Validator که از آدرس زیر قابل دریافت است:


این پروژه که توسط Dario Quintana توسعه یافته است، امکان اعتبار سنجی اطلاعات را پیش از افزوده شدن آن‌ها به دیتابیس به دو صورت دستی و یا خودکار و یکپارچه با NHibernate فراهم می‌سازد؛ که امروز قصد بررسی آن‌را داریم.

کامپایل پروژه اعتبار سنجی NHibernate

پس از دریافت آخرین نگارش موجود کتابخانه NHibernate Validator از سایت سورس فورج، فایل پروژه آن‌را در VS.Net گشوده و یکبار آن‌را کامپایل نمائید تا فایل اسمبلی NHibernate.Validator.dll حاصل گردد.

بررسی مدل برنامه

در این مدل ساده، تعدادی پزشک داریم و تعدادی بیمار. در سیستم ما هر بیمار تنها توسط یک پزشک مورد معاینه قرار خواهد گرفت. رابطه آن‌ها را در کلاس دیاگرام زیر می‌توان مشاهده نمود:


به این صورت پوشه دومین برنامه از کلاس‌های زیر تشکیل خواهد شد:

namespace NHSample5.Domain
{
public class Patient
{
public virtual int Id { get; set; }
public virtual string FirstName { get; set; }
public virtual string LastName { get; set; }
}
}

using System.Collections.Generic;

namespace NHSample5.Domain
{
public class Doctor
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual IList<Patient> Patients { get; set; }

public Doctor()
{
Patients = new List<Patient>();
}
}
}
برنامه این قسمت از نوع کنسول با ارجاعاتی به اسمبلی‌های FluentNHibernate.dll ،log4net.dll ،NHibernate.dll ، NHibernate.ByteCode.Castle.dll ،NHibernate.Linq.dll ،NHibernate.Validator.dll و System.Data.Services.dll است.

ساختار کلی این پروژه را در شکل زیر مشاهده می‌کنید:


اطلاعات این برنامه بر مبنای NHRepository و NHSessionManager ایی است که در قسمت‌های قبل توسعه دادیم و پیشنیاز ضروری مطالعه آن می‌باشند (سورس پیوست شده شامل نمونه تکمیل شده این موارد نیز هست). همچنین از قسمت ایجاد دیتابیس از روی مدل نیز صرفنظر می‌شود و همانند قسمت‌های قبل است.


تعریف اعتبار سنجی دومین با کمک ویژگی‌ها (attributes)

فرض کنید می‌خواهیم بر روی طول نام و نام خانوادگی بیمار محدودیت قرار داده و آن‌ها را با کمک کتابخانه NHibernate Validator ، اعتبار سنجی کنیم. برای این منظور ابتدا فضای نام NHibernate.Validator.Constraints به کلاس بیمار اضافه شده و سپس با کمک ویژگی‌هایی که در این کتابخانه تعریف شده‌اند می‌توان قیود خود را به خواص کلاس تعریف شده اعمال نمود که نمونه‌ای از آن را مشاهده می‌نمائید:

using NHibernate.Validator.Constraints;

namespace NHSample5.Domain
{
public class Patient
{
public virtual int Id { get; set; }

[Length(Min = 3, Max = 20,Message="طول نام باید بین 3 و 20 کاراکتر باشد")]
public virtual string FirstName { get; set; }

[Length(Min = 3, Max = 60, Message = "طول نام خانوادگی باید بین 3 و 60 کاراکتر باشد")]
public virtual string LastName { get; set; }
}
}
اعمال این قیود از این جهت مهم هستند که نباید وقت برنامه و سیستم را با دریافت خطای نهایی از دیتابیس تلف کرد. آیا بهتر نیست قبل از اینکه اطلاعات به دیتابیس وارد شوند و رفت و برگشتی در شبکه صورت گیرد، مشخص گردد که این فیلد حتما نباید خالی باشد یا طول آن باید دارای شرایط خاصی باشد و امثال آن؟

مثالی دیگر:
جهت اجباری کردن و همچنین اعمال Regular expressions برای اعتبار سنجی یک فیلد می‌توان دو ویژگی زیر را به بالای آن فیلد مورد نظر افزود:

[NotNull]
[Pattern(Regex = "[A-Za-z0-9]+")]

تعریف اعتبار سنجی با کمک کلاس ValidationDef

راه دوم تعریف اعتبار سنجی، کمک گرفتن از کلاس ValidationDef این کتابخانه و استفاده از روش fluent configuration است. برای این منظور، پوشه جدیدی را به برنامه به نام Validation اضافه خواهیم کرد و سپس دو کلاس DoctorDef و PatientDef را به آن به صورت زیر خواهیم افزود:

using NHibernate.Validator.Cfg.Loquacious;
using NHSample5.Domain;

namespace NHSample5.Validation
{
public class DoctorDef : ValidationDef<Doctor>
{
public DoctorDef()
{
Define(x => x.Name).LengthBetween(3, 50);
Define(x => x.Patients).NotNullableAndNotEmpty();
}
}
}

using NHSample5.Domain;
using NHibernate.Validator.Cfg.Loquacious;

namespace NHSample5.Validation
{
public class PatientDef : ValidationDef<Patient>
{
public PatientDef()
{
Define(x => x.FirstName)
.LengthBetween(3, 20)
.WithMessage("طول نام باید بین 3 و 20 کاراکتر باشد");

Define(x => x.LastName)
.LengthBetween(3, 60)
.WithMessage("طول نام خانوادگی باید بین 3 و 60 کاراکتر باشد");
}
}
}

استفاده از قیودات تعریف شده به صورت دستی

می‌توان از این کتابخانه اعتبار سنجی به صورت مستقیم نیز اضافه کرد. روش انجام آن‌را در متد زیر مشاهده می‌نمائید.

/// <summary>
/// استفاده از اعتبار سنجی ویژه به صورت مستقیم
/// در صورت استفاده از ویژگی‌ها
/// </summary>
static void WithoutConfiguringTheEngine()
{
//تعریف یک بیمار غیر معتبر
var patient1 = new Patient() { FirstName = "V", LastName = "N" };
var ve = new ValidatorEngine();
var invalidValues = ve.Validate(patient1);
if (invalidValues.Length == 0)
{
Console.WriteLine("patient1 is valid.");
}
else
{
Console.WriteLine("patient1 is NOT valid!");
//نمایش پیغام‌های تعریف شده مربوط به هر فیلد
foreach (var invalidValue in invalidValues)
{
Console.WriteLine(
"{0}: {1}",
invalidValue.PropertyName,
invalidValue.Message);
}
}

//تعریف یک بیمار معتبر بر اساس قیودات اعمالی
var patient2 = new Patient() { FirstName = "وحید", LastName = "نصیری" };
if (ve.IsValid(patient2))
{
Console.WriteLine("patient2 is valid.");
}
else
{
Console.WriteLine("patient2 is NOT valid!");
}
}
ابتدا شیء ValidatorEngine تعریف شده و سپس متد Validate آن بر روی شیء بیماری غیر معتبر فراخوانی می‌گردد. در صورتیکه این عتبار سنجی با موفقیت روبر نشود، خروجی این متد آرایه‌ای خواهد بود از فیلدهای غیرمعتبر به همراه پیغام‌هایی که برای آن‌ها تعریف کرده‌ایم. یا می‌توان به سادگی همانند بیمار شماره دو، تنها از متد IsValid آن نیز استفاده کرد.

در اینجا اگر سعی در اعتبار سنجی یک پزشک نمائیم، نتیجه‌ای حاصل نخواهد شد زیرا هنگام استفاده از کلاس ValidationDef، باید نگاشت لازم به این قیودات را نیز دقیقا مشخص نمود تا مورد استفاده قرار گیرد که نحوه‌ی انجام این عملیات را در متد زیر می‌توان مشاهده نمود.

public static ValidatorEngine GetFluentlyConfiguredEngine()
{
var vtor = new ValidatorEngine();
var configuration = new FluentConfiguration();
configuration
.Register(
Assembly
.GetExecutingAssembly()
.GetTypes()
.Where(t => t.Namespace.Equals("NHSample5.Validation"))
.ValidationDefinitions()
)
.SetDefaultValidatorMode(ValidatorMode.UseExternal);
vtor.Configure(configuration);
return vtor;
}

FluentConfiguration آن مجزا است از نمونه مشابه کتابخانه Fluent NHibernate و نباید با آن اشتباه گرفته شود (در فضای نام NHibernate.Validator.Cfg.Loquacious تعریف شده است).
در این متد کلاس‌های قرار گرفته در پوشه Validation برنامه که دارای فضای نام NHSample5.Validation هستند، به عنوان کلاس‌هایی که باید اطلاعات لازم مربوط به اعتبار سنجی را از آنان دریافت کرد معرفی شده‌اند.
همچنین ValidatorMode نیز به صورت External تعریف شده و منظور از External در اینجا هر چیزی بجز استفاده از روش بکارگیری attributes است (علاوه بر امکان تعریف این قیودات در یک پروژه class library مجزا و مشخص ساختن اسمبلی آن در اینجا).

اکنون جهت دسترسی به این موتور اعتبار سنجی تنظیم شده می‌توان به صورت زیر عمل کرد:

/// <summary>
/// استفاده از اعتبار سنجی ویژه به صورت مستقیم
/// در صورت تعریف آن‌ها با کمک
/// ValidationDef
/// </summary>
static void WithConfiguringTheEngine()
{
var ve2 = VeConfig.GetFluentlyConfiguredEngine();
var doctor1 = new Doctor() { Name = "S" };
if (ve2.IsValid(doctor1))
{
Console.WriteLine("doctor1 is valid.");
}
else
{
Console.WriteLine("doctor1 is NOT valid!");
}

var patient1 = new Patient() { FirstName = "وحید", LastName = "نصیری" };
if (ve2.IsValid(patient1))
{
Console.WriteLine("patient1 is valid.");
}
else
{
Console.WriteLine("patient1 is NOT valid!");
}

var doctor2 = new Doctor() { Name = "شمس", Patients = new List<Patient>() { patient1 } };
if (ve2.IsValid(doctor2))
{
Console.WriteLine("doctor2 is valid.");
}
else
{
Console.WriteLine("doctor2 is NOT valid!");
}
}

نکته مهم:
فراخوانی GetFluentlyConfiguredEngine نیز باید یکبار در طول برنامه صورت گرفته و سپس حاصل آن بارها مورد استفاده قرار گیرد. بنابراین نحوه‌ی صحیح دسترسی به آن باید حتما از طریق الگوی Singleton که در قسمت‌های قبل در مورد آن بحث شد، انجام شود.


استفاده از قیودات تعریف شده و سیستم اعتبار سنجی به صورت یکپارچه با NHibernate

کتابخانه NHibernate Validator زمانیکه با NHibernate یکپارچه گردد دو رخداد PreInsert و PreUpdate آن‌را به صورت خودکار تحت نظر قرار داده و پیش از اینکه اطلاعات ثبت و یا به روز شوند، ابتدا کار اعتبار سنجی خود را انجام داده و اگر اعتبار سنجی مورد نظر با شکست مواجه شود، با ایجاد یک exception از ادامه برنامه جلوگیری می‌کند. در این حالت استثنای حاصل شده از نوع InvalidStateException خواهد بود.

برای انجام این مرحله یکپارچه سازی ابتدا متد BuildIntegratedFluentlyConfiguredEngine را به شکل زیر باید فراخوانی نمائیم:

/// <summary>
/// از این کانفیگ برای آغاز سشن فکتوری باید کمک گرفته شود
/// </summary>
/// <param name="nhConfiguration"></param>
public static void BuildIntegratedFluentlyConfiguredEngine(ref Configuration nhConfiguration)
{
var vtor = new ValidatorEngine();
var configuration = new FluentConfiguration();
configuration
.Register(
Assembly
.GetExecutingAssembly()
.GetTypes()
.Where(t => t.Namespace.Equals("NHSample5.Validation"))
.ValidationDefinitions()
)
.SetDefaultValidatorMode(ValidatorMode.UseExternal)
.IntegrateWithNHibernate
.ApplyingDDLConstraints()
.And
.RegisteringListeners();
vtor.Configure(configuration);

//Registering of Listeners and DDL-applying here
ValidatorInitializer.Initialize(nhConfiguration, vtor);
}
این متد کار دریافت Configuration مرتبط با NHibernate را جهت اعمال تنظیمات اعتبار سنجی به آن انجام می‌دهد. سپس از nhConfiguration تغییر یافته در این متد جهت ایجاد سشن فکتوری استفاده خواهیم کرد (در غیر اینصورت سشن فکتوری درکی از اعتبار سنجی‌های تعریف شده نخواهد داشت). اگر قسمت‌های قبل را مطالعه کرده باشید، کلاس SingletonCore را جهت مدیریت بهینه‌ی سشن فکتوری به خاطر دارید. این کلاس اکنون باید به شکل زیر وصله شود:

SingletonCore()
{
Configuration cfg = DbConfig.GetConfig().BuildConfiguration();
VeConfig.BuildIntegratedFluentlyConfiguredEngine(ref cfg);
//با همان کانفیگ تنظیم شده برای اعتبار سنجی باید کار شروع شود
_sessionFactory = cfg.BuildSessionFactory();
}

از این لحظه به بعد، نیاز به فراخوانی متدهای Validate و یا IsValid نبوده و کار اعتبار سنجی به صورت خودکار و یکپارچه با NHibernate انجام می‌شود. لطفا به مثال زیر دقت بفرمائید:

/// <summary>
/// استفاده از اعتبار سنجی یکپارچه و خودکار
/// </summary>
static void tryToSaveInvalidPatient()
{
using (Repository<Patient> repo = new Repository<Patient>())
{
try
{
var patient1 = new Patient() { FirstName = "V", LastName = "N" };
repo.Save(patient1);
}
catch (InvalidStateException ex)
{
Console.WriteLine("Validation failed!");
foreach (var invalidValue in ex.GetInvalidValues())
Console.WriteLine(
"{0}: {1}",
invalidValue.PropertyName,
invalidValue.Message);
log4net.LogManager.GetLogger("NHibernate.SQL").Error(ex);
}
}
}

/// <summary>
/// استفاده از اعتبار سنجی یکپارچه و خودکار
/// </summary>
static void tryToSaveValidPatient()
{
using (Repository<Patient> repo = new Repository<Patient>())
{
var patient1 = new Patient() { FirstName = "Vahid", LastName = "Nasiri" };
repo.Save(patient1);
}
}
در اینجا از کلاس Repository که در قسمت‌های قبل توسعه دادیم، استفاده شده است. در متد tryToSaveInvalidPatient ، بدلیل استفاده از تعریف بیماری غیرمعتبر، پیش از انجام عملیات ثبت، استثنایی حاصل شده و پیش از هرگونه رفت و برگشتی به دیتابیس، سیستم از بروز این مشکل مطلع خواهد شد. همچنین پیغام‌هایی را که هنگام تعریف قیودات مشخص کرده بودیم را نیز توسط آرایه ex.GetInvalidValues می‌توان دریافت کرد.

نکته:
اگر کار ساخت database schema را با کمک کانفیگ تنظیم شده توسط کتابخانه اعتبار سنجی آغاز کنیم، طول فیلدها دقیقا مطابق با حداکثر طول مشخص شده در قسمت تعاریف قیود هر یک از فیلدها تشکیل می‌گردد (حاصل از اعمال متد ApplyingDDLConstraints در متد BuildIntegratedFluentlyConfiguredEngine ذکر شده می‌باشد).

public static void CreateValidDb()
{
bool script = false;//آیا خروجی در کنسول هم نمایش داده شود
bool export = true;//آیا بر روی دیتابیس هم اجرا شود
bool dropTables = false;//آیا جداول موجود دراپ شوند

Configuration cfg = DbConfig.GetConfig().BuildConfiguration();
VeConfig.BuildIntegratedFluentlyConfiguredEngine(ref cfg);
//با همان کانفیگ تنظیم شده برای اعتبار سنجی باید کار شروع شود

new SchemaExport(cfg).Execute(script, export, dropTables);
}


دریافت سورس کامل قسمت دهم


مطالب دوره‌ها
نکته‌ای در مورد مدیریت طول عمر اشیاء در حالت HybridHttpOrThreadLocalScoped در برنامه‌های دسکتاپ
اگر به فایل IocConfig.cs پروژه دقت کرده باشید، مدیریت طول عمر واحد کار به صورت معمولی و متداولی تعریف شده است:
 cfg.For<IUnitOfWork>().Use(() => new MyWpfFrameworkContext());
و در اینجا از حالت HybridHttpOrThreadLocalScoped که در برنامه‌های وب نیز مورد استفاده قرار می‌گیرد، استفاده نشده است. چرا؟
 ThreadLocal - A single instance will be created for each requesting thread. Caches the instances with ThreadLocalStorage.
Hybrid - Uses HttpContext storage if it exists, otherwise uses ThreadLocal storage
تعاریف رسمی مرتبط با Thread local و حالت Hypbrid را در اینجا ملاحظه می‌کنید. در حالت Thread local از یک وهله در طی طول عمر یک ترد استفاده می‌شود. حالت Hybrid مشخص می‌کند که اگر برنامه وب بود، از HttpContext برای مدیریت این طول عمر استفاده کن و اگر برنامه دسکتاپ بود از ThreadLocal storage.
خوب، مشکل کجا است؟ در یک برنامه دسکتاپ اگر ترد جدیدی را ایجاد نکنیم، کل برنامه در طول مدتی که در حال اجرا است در یک ترد اصلی اجرا می‌شود. در نتیجه استفاده از حالت HybridHttpOrThreadLocalScoped در برنامه‌های دسکتاپ، بسیار شبیه به حالت Singleton است. به این ترتیب با بسته شدن یک پنجره یا هدایت به صفحه‌ای دیگر، Context برنامه و EF رها نخواهند شد (چون تضمین شده است که یک وهله از این شیء در طول عمر ترد جاری وجود داشته باشد). نتیجه آن روبرو شدن با خطاهایی است که ردیابی و دیباگ بسیار مشکلی دارند. برای مثال چندی قبل در سایت مطرح شده بود که چرا در یک برنامه WinForms خطای زیر را دریافت می‌کنم:
 An object with the same key already exists in the ObjectStateManager
علت پس از چند سؤال جواب، به مدیریت طول عمر UOW مرتبط شد. چون از حالت HybridHttpOrThreadLocalScoped استفاده شده بوده، در صفحه‌ای، شیءایی به Context اضافه شده. پس از مراجعه به صفحه‌ای دیگر، مجددا سعی در اضافه کردن همین شیء گردیده است (با این تصور که با بسته شدن صفحه قبلی، Context هم از بین رفته است). بلافاصله خطای ذکر شده، توسط EF گزارش گردیده است؛ چون هنوز Context باز است و از بین نرفته است.

در برنامه‌های وب چطور؟
در برنامه‌های وب، به ازای هر درخواست رسیده، یک ترد جدید ایجاد می‌شود. به همین جهت استفاده از حالت HybridHttpOrThreadLocalScoped برای داشتن تنها یک Context در طول یک درخواست ضروری است.

نتیجه گیری
در برنامه‌های دسکتاپ خود اگر از ترد استفاده نمی‌کنید، از حالت HybridHttpOrThreadLocalScoped برای مدیریت طول عمر UOW استفاده نکنید.