مطالب
شروع کار با Dart - قسمت 2
لطفا قسمت اول را در اینجا مطالعه بفرمائید

گام سوم: افزودن یک button
در این مرحله یک button را به صفحه html اضافه می‌کنیم. button زمانی فعال می‌شود که هیچ متنی در فیلد input موجود نباشد. زمانی که کاربر بر روی دکمه کلیک می‌کند نام Meysam Khoshbakht را در کادر قرمز رنگ می‌نویسد.
تگ <button> را بصورت زیر در زیر فیلد input ایجاد کنید
...
<div>
  <div>
    <input type="text" id="inputName" maxlength="15">
  </div>
  <div>
    <button id="generateButton">Aye! Gimme a name!</button>
  </div>
</div>
...
در زیر دستور import و بصورت top-level متغیر زیر را تعریف کنید تا یک ButtonElement در داخل آن قرار دهیم.
import 'dart:html';

ButtonElement genButton;
توضیحات
- ButtonElement یکی از انواع المنت‌های DOM می‌باشد که در کتابخانه dart:html قرار دارد
- اگر متغیری مقداردهی نشده باشد بصورت پیش فرض با null مقداردهی می‌گردد
به منظور مدیریت رویداد کلیک button کد زیر را به تابع main اضافه می‌کنیم
void main() {
  querySelector('#inputName').onInput.listen(updateBadge);
  genButton = querySelector('#generateButton');
  genButton.onClick.listen(generateBadge);
}
جهت تغییر محتوای کادر قرمز رنگ تابع top-level زیر را به piratebadge.dart اضافه می‌کنیم
...

void setBadgeName(String newName) {
  querySelector('#badgeName').text = newName;
}
جهت مدیریت رویداد کلیک button تابع زیر را بصورت top-level اضافه می‌کنیم
...

void generateBadge(Event e) {
  setBadgeName('Meysam Khoshbakht');
}
همانطور که در کدهای فوق مشاهده می‌کنید، با فشردن button تابع generateBadge فراخوانی میشود و این تابع نیز با فراخوانی تابع setBadgeName محتوای badge یا کادر قرمز رنگ را تغییر می‌دهد. همچنین می‌توانیم کد موجود در updateBadge مربوط به رویداد input فیلد input را بصورت زیر تغییر دهیم
void updateBadge(Event e) {
  String inputName = (e.target as InputElement).value;
  setBadgeName(inputName);
}
جهت بررسی پر بودن فیلد input می‌توانیم از یک if-else بصورت زیر استفاده کنیم که با استفاده از توابع رشته ای پر بودن فیلد را بررسی می‌کند.
void updateBadge(Event e) {
  String inputName = (e.target as InputElement).value;
  setBadgeName(inputName);
  if (inputName.trim().isEmpty) {
    // To do: add some code here.
  } else {
    // To do: add some code here.
  }
}
توضیحات
- کلاس String شامل توابع و ویژگی‌های مفیدی برای کار با رشته‌ها می‌باشد. مثل trim که فواصل خالی ابتدا و انتهای رشته را حذف می‌کند و isEmpty که بررسی می‌کند رشته خالی است یا خیر.
- کلاس String در کتابخانه dart:core قرار دارد که بصورت خودکار در تمامی برنامه‌های دارت import می‌شود
حال جهت مدیریت وضعیت فعال یا غیر فعال بودن button کد زیر را می‌نویسیم
void updateBadge(Event e) {
  String inputName = (e.target as InputElement).value;
  setBadgeName(inputName);
  if (inputName.trim().isEmpty) {
    genButton..disabled = false
             ..text = 'Aye! Gimme a name!';
  } else {
    genButton..disabled = true
             ..text = 'Arrr! Write yer name!';
  }
}
توضیحات
- عملگر cascade یا آبشاری (..)، به شما اجازه می‌دهد تا چندین عملیات را بر روی اعضای یک شی انجام دهیم. اگر به کد دقت کرده باشید با یک بار ذکر نام متغیر genButton ویژگی‌های disabled و text را مقدار دهی نمودیم که موجب تسریع و کاهش حجم کد نویسی می‌گردد.
همانند گام اول برنامه را اجرا کنید و نتیجه را مشاهده نمایید. با تایپ کردن در فیلد input و خالی کردن آن وضعیت button را بررسی کنید. همچنین با کلیک بر روی button نام درج شده در badge را مشاهده کنید.
 

گام چهارم: ایجاد کلاس PirateName

در این مرحله فقط کد مربوط به فایل dart را تغییر میدهیم. ابتدا کلاس PirateName را ایجاد می‌کنیم. با ایجاد نمونه ای از این کلاس، یک نام بصورت تصادفی انتخاب می‌شود و یا نامی بصورت اختیاری از طریق سازنده انتخاب می‌گردد.

نخست کتابخانه dart:math را به ابتدای فایل dart اضافه کنید

import 'dart:html';

import 'dart:math' show Random;

توضیحات

- با استفاده از کلمه کلیدی show، شما می‌توانید فقط کلاسها، توابع و یا ویژگی‌های مورد نیازتان را import کنید.

- کلاس Random یک عدد تصادفی را تولید می‌کند

در انتهای فایل کلاس زیر را تعریف کنید

...

class PirateName {
}

در داخل کلاس یک شی از کلاس Random ایجاد کنید

class PirateName {
  static final Random indexGen = new Random();
}

توضیحات

- با استفاده از static یک فیلد را در سطح کلاس تعریف می‌کنیم که بین تمامی نمونه‌های ایجاد شده از کلاس مشترک می‌باشد

- متغیرهای final فقط خواندنی می‌باشند و غیر قابل تغییر هستند.

- با استفاده از new می‌توانیم سازنده ای را فراخوانی نموده و نمونه ای را از کلاس ایجاد کنیم

دو فیلد دیگر از نوع String و با نام‌های _firstName و _appelation به کلاس اضافه می‌کنیم

class PirateName {
  static final Random indexGen = new Random();
  String _firstName;
  String _appellation;
}

متغیرهای خصوصی با (_) تعریف می‌شوند. Dart کلمه کلیدی private را ندارد.

دو لیست static به کلاس فوق اضافه می‌کنیم که شامل لیستی از name و appelation می‌باشد که می‌خواهیم آیتمی را بصورت تصادفی از آنها انتخاب کنیم.

class PirateName {
  ...
  static final List names = [
    'Anne', 'Mary', 'Jack', 'Morgan', 'Roger',
    'Bill', 'Ragnar', 'Ed', 'John', 'Jane' ];
  static final List appellations = [
    'Jackal', 'King', 'Red', 'Stalwart', 'Axe',
    'Young', 'Brave', 'Eager', 'Wily', 'Zesty'];
}

کلاس List می‌تواند شامل مجموعه ای از آیتم‌ها می‌باشد که در Dart تعریف شده است.

سازنده ای را بصورت زیر به کلاس اضافه می‌کنیم

class PirateName {
  ...
  PirateName({String firstName, String appellation}) {
    if (firstName == null) {
      _firstName = names[indexGen.nextInt(names.length)];
    } else {
      _firstName = firstName;
    }
    if (appellation == null) {
      _appellation = appellations[indexGen.nextInt(appellations.length)];
    } else {
      _appellation = appellation;
    }
  }
}

توضیحات

- سازنده تابعی همنام کلاس می‌باشد

- پارامترهایی که در {} تعریف می‌شوند اختیاری و Named Parameter می‌باشند. Named Parameter‌ها پارمترهایی هستند که جهت مقداردهی به آنها در زمان فراخوانی، از نام آنها استفاده می‌شود.

- تابع nextInt() یک عدد صحیح تصادفی جدید را تولید می‌کند.

- جهت دسترسی به عناصر لیست از [] و شماره‌ی خانه‌ی لیست استفاده می‌کنیم.

- ویژگی length تعداد آیتم‌های موجود در لیست را بر می‌گرداند.

در این مرحله یک getter برای دسترسی به pirate name ایجاد می‌کنیم

class PirateName {
  ...
  String get pirateName =>
    _firstName.isEmpty ? '' : '$_firstName the $_appellation';
}

توضیحات

- Getter‌ها متدهای خاصی جهت دسترسی به یک ویژگی به منظور خواندن مقدار آنها می‌باشند.

- عملگر سه گانه :? دستور میانبر عبارت شرطی if-else می‌باشد

- $ یک کاراکتر ویژه برای رشته‌های موجود در Dart می‌باشد و می‌تواند محتوای یک متغیر یا ویژگی را در رشته قرار دهد. در رشته '$_firstName the $_appellation' محتوای دو ویژگی _firstName و _appellation در رشته قرار گرفته و نمایش می‌یابند.

- عبارت (=> expr;) یک دستور میانبر برای { return expr; } می‌باشد.

تابع setBadgeName را بصورت زیر تغییر دهید تا یک پارامتر از نوع کلاس PirateName را به عنوان پارامتر ورودی دریافت نموده و با استفاده از Getter مربوط به ویژگی pirateName، مقدار آن را در badge name نمایش دهد.

void setBadgeName(PirateName newName) {
  querySelector('#badgeName').text = newName.pirateName;
}

تابع updateBadge را بصورت زیر تغییر دهید تا یک نمونه از کلاس PirateName را با توجه به مقدار ورودی کاربر در فیلد input تولید نموده و تابع setBadgeName رافراخوانی نماید. همانطور که در کد مشاهده می‌کنید پارامتر ورودی اختیاری firstName در زمان فراخوانی با ذکر نام پارامتر قبل از مقدار ارسالی نوشته شده است. این همان قابلیت Named Parameter می‌باشد.

void updateBadge(Event e) {
  String inputName = (e.target as InputElement).value;
  
  setBadgeName(new PirateName(firstName: inputName));
  ...
}

تابع generateBadge را بصورت زیر تغییر دهید تا به جای نام ثابت Meysam Khoshbakht، از کلاس PirateName به منظور ایجاد نام استفاده کند. همانطور که در کد می‌بینید، سازنده‌ی بدون پارامتر کلاس PirateName فراخوانی شده است.

void generateBadge(Event e) {
  setBadgeName(new PirateName());
}

همانند گام سوم برنامه را اجرا کنید و نتیجه را مشاهده نمایید.

نظرات مطالب
ASP.NET Web API - قسمت سوم
در Rest  قابلیتی بنام  Syndication Feed Fromatter  وجود دارد در Web API چطور؟
نظرات مطالب
فعال سازی عملیات CRUD در Kendo UI Grid
نکات مطلب «فرمت کردن اطلاعات نمایش داده شده به کمک Kendo UI Grid» را در مورد کار با قالب‌ها مطالعه کنید.
روش اول:
columns: [
                { field: "units", title: "Units" },
                { field: "price", title: "Price" },
                { field: null, title: "Extended Price", template: '#= units * price #' }
            ]
روش دوم؛ یک ستون قالب دار با این فرمت اضافه می‌کنید: 
#=calculateField(data)#
که متد آن برای نمونه به این ترتیب محاسبه شود:
//Passed data contains current row model
function calculateField(data) {
  return data.f1 + " " + data.f2;
}
مطالب
توزیع کلاس های اندرویدی با استفاده از Gradle قسمت دوم
در مقاله قبل در مورد اینکه در پشت صحنه‌ی سیستم توزیع گریدل چه اتفاقاتی در حال رخ دادن است، توضیح دادیم. در این نوشتار سعی داریم به عنوان مثال کلاسی به اسم AndroidBreadCrumb را به این سرورها آپلود کنیم.

سوال: چگونه این فایل را در Jcenter آپلود کنیم؟
فرآیندی که در این نوشتار قصد داریم دنبال شود شامل مراحل زیر است:
ابتدا کتابخانه‌ی خودمان را روی جی سنتر قرار داده و در صورتیکه علاقه داشته باشیم، آن را به mavenCentral هم انتقال می‌دهیم.

ابتدا نیاز است در سایت bintray ثبت نام کنید و با حساب جدید وارد شوید و گزینه‌ی maven را انتخاب کنید.

سپس روی گزینه‌ی Add New Package کلیک کنید تا یک پکیج جدید را ایجاد کنیم.

در صفحه‌ای که باز می‌شود، اطلاعات مربوط به این پکیج را وارد کنید که عموما شامل نام پکیج، مجوز آن، کلمات کلیدی، لینک گزارش باگ و .. می‌شود. در انتخاب نام پکیج، قانون اجباری یا خاصی وجود ندارد؛ ولی توصیه می‌شود که از حروف کوچک و - استفاده گردد. بعد از پرکردن فیلدهای الزامی، وارد صفحه‌ی جزئیات پکیج می‌شوید که در آن فیلدهای اضافه‌تری نیز وجود دارند که میتوانید در صورت تمایل آن‌ها را پر کنید. همچنین در بالای صفحه لینک به صفحه‌ی اختصاصی این پکیج نیز وجود دارد که در زیر عبارت Edit Package قرار گرفته است.

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


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

سوال: چگونه این فایل را در SonaType آپلود کنیم؟ 
گام اول: ابتدا باید در سایت ثبت نام کنید. پس به این صفحه رفته و ثبت نام کنید. سپس در یک مرحله‌ی غیرمنطقی باید یک issue توسط سیستم JIRA ایجاد کنید. برای همین گزینه‌ی Creare را در بالای صفحه بزنید. اطلاعات زیر را به ترتیب پر کنید:
Project: Community Support - Open Source Project Repository Hosting

Issue Type: New Project

Summary: مثلا نام پروژه خودتان را بنویسید

یک نام پکیج که سعی کنید کتابخانه‌های هم خانواده این اشتراک را داشته باشند که در یک گروه قرار بگیرند
Group Id: AndroidBreadCrumb.Plus

آدرس جایی که پروژه قرار دارد
Project URL: https://github.com/yeganehaym/AndroidBreadCrumb

//آدرس سیستم کنترل نسخه
SCM url: https://github.com/yeganehaym/AndroidBreadCrumb
بقیه‌ی موارد الزامی نیست. کادر را تایید کنید. در این مرحله باید مدتی منتظر بمانید تا این مخزن را برای شما تایید کنند که طبق مستندات حدود یک هفته‌ای طول می‌کشد. بعد از اینکار باید نام کاربری اکانت خود را در پروفایل سایت Bintray  در برگه‌ی Accounts  قسمت sonatype oss account وارد کنید و پروفایل را آپدیت کنید.

فعال سازی امضای خودکار در Bintray

همانطور که در ابتدای مقاله گفتیم، می‌خواهیم کتابخانه‌ی خود را از طریق jcenter به maven ارسال کنیم. برای همین نیاز داریم که ابتدا کتابخانه‌ی خود را امضا کنیم. برای اینکار باید از طریق GPG یک کلید بسازیم. ساخت کلید به این شیوه، قبلا در مقاله‌ی «ساخت کلیدهای امنیتی با GnuPG» توضیح داده شد و از تکرار آن خودداری می‌کنیم. تنها به ذکر این نکته بسنده می‌کنیم که شما باید یک کلید ساخته و آن را به سرور کلیدها ارسال کنید و سپس کلید متنی عمومی و خصوصی آن را در پروفایل bintray برگه‌ی GPG Signing درج کنید.


به عنوان آخرین کار باید امضای خودکار را فعال کنید. بنابراین نیاز است که به صفحه‌ی اصلی رفته و بر روی maven کلیک کنید و گزینه‌ی Edit را انتخاب کنید. در صفحه‌ی باز شده، تیک GPG sign uploaded files automatically را بزنید.

این تنظیم از این پس بر روی تمامی کتابخانه‌ها اعمال می‌شود.


سوال : چگونه پروژه‌ی اندرویدی خودم را کامپایل کنم؟


فایل  build.gradle پروژه را باز کنید و پلاگین bintray را به آن معرفی کنید:
 dependencies {
        classpath 'com.android.tools.build:gradle:1.2.2'
        classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.2'
        classpath 'com.github.dcendents:android-maven-plugin:1.2'
    }
بسیار مهم است که نسخه‌ی gradle build tools از نسخه‌ی 1.1.2 به بعد باشد. چون که در نسخه‌های پایین‌تر، یک باگ بسیار حیاتی یافت شده، که از این نسخه به بعد رفع گردیده است. حال فایل local.properties را باز کنید و اطلاعات زیر را وارد کنید:
bintray.user=YOUR_BINTRAY_USERNAME
bintray.apikey=YOUR_BINTRAY_API_KEY
bintray.gpg.password=YOUR_GPG_PASSWORD
نام کاربری که برای اکانت bintray انتخاب کردید را بنویسید. برای ApiKey در قسمت Edit Profile برگه‌ی Api Key در خط دوم می‌توانید آن را ببینید و کلمه‌ی عبور GPG هم همان عبارتی است که در انتهای ساخت کلید دوبار از شما پرسیده شده است.  (در صورتی که اقدامات لازم برای mavenCentral را انجام داده اید خط سوم را  وارد کنید)

در مرحله‌ی بعدی خطوط زیر را بعد از 'Apply Plugin 'com.android.library اضافه کنید و اطلاعاتی که در bintray وارد کرده‌اید را در اینجا وارد کنید:
apply plugin: 'com.android.library'

ext {
    bintrayRepo = 'maven'
    bintrayName = 'AndroidBreadCrumb'

    publishedGroupId = 'com.plus'
    libraryName = 'AndroidBreadCrumb'
    artifact = 'AndroidBreadCrumb'

    libraryDescription = 'create breadcrumb on android to show a path to user and let user to jump on them'

    siteUrl = 'https://github.com/yeganehaym/AndroidBreadCrumb'
    gitUrl = 'https://github.com/yeganehaym/AndroidBreadCrumb'

    libraryVersion = '1.0'

    developerId = 'yeganehaym'
    developerName = 'ali yeganeh.m'
    developerEmail = 'yeganehaym@gmail.com'

    licenseName = 'The Apache Software License, Version 2.0'
    licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
    allLicenses = ["Apache-2.0"]
}
بعد از آن هم در انتهای فایل گریدل ماژول، دو خط زیر را که شامل اسکریپت‌هایی برای راحتی کار است، معرفی کنید:
apply from: 'https://raw.githubusercontent.com/nuuneoi/JCenter/master/installv1.gradle'
apply from: 'https://raw.githubusercontent.com/nuuneoi/JCenter/master/bintrayv1.gradle'
با اطلاعاتی که بالا تعریف کردیم، کاربر باید شناسه‌ای به شکل زیر در گریدل به کار ببرد تا به کتابخانه‌ی ما دسترسی داشته باشد:
compile 'com.plus:AndroidBreadCrumb:1.0'

آپلود فایل‌ها به مخزن
برای آپلود فایل‌های ماژول به مخزن، ابتدا ترمینال اندروید استودیو را باز کنید و گام‌های زیر را به ترتیب انجام بدهید:
گام اول: با ارسال دستور زیر از صحت کدها و منابع مطمئن می‌شویم:
gradlew install
در صورتیکه خطایی نباشد، پیام موفقیت آمیزی را به شما نمایش می‌دهد:
BUILD SUCCESSFUL
حالا باید فایل‌ها را به سمت سرور ارسال کنیم:
gradlew bintrayUpload
در صورتی که عملیات موفقیت آمیز باشد پیام زیر نمایش می‌یابد:
SUCCESSFUL

حال صفحه‌ی اختصاصی پکیج‌تان را چک کنید. می‌بینید که قسمت‌هایی از آن تغییر کرده‌است و قسمت نسخه‌، به روز شده است:



و قسمت فایل‌ها هم دیگر خالی نیست:




با اینکه کتابخانه‌ی ما روی maven قرار گرفت، ولی هنوز نمی‌توان آن را توسط jcenter استفاده کرد و باید bintray maven را با jcenter هماهنگ نماییم. در حال حاضر استفاده از این کتابخانه بدون سینک به شکل زیر است:

گریدل پروژه
     maven{
            url 'https://dl.bintray.com/yeganehaym/maven'
        }

گریدل ماژول

dependencies {
    compile 'com.plus:AndroidbreadCrumb:1.0'
}
آدرس اولی را می‌توانید با نگاه در صفحه‌ی اختصاصی پکیج ببینید که لینک آن در جلوی نام پکیج وجود دارد و قابلیت کپی کردن لینک هم وجود دارد. اگر روی آن کلیک کنید، می‌توانید مسیر را در صفحه‌ی باز شده ببینید. البته راه مستقیم‌تر اینکه به جای yeganehaym در لینک بالا، نام کاربری خودتان را جایگزین کنید.

برای افزودن کتابخانه‌ی خود به سیستم jcenter با کلیک بر روی گزینه‌ی Add to jcenter می‌توانید به تیم jcenter درخواست دهید که آن را تایید کنند که بعد از درخواست حدود سه ساعت طول می‌کشد تا پاسخ شما را بدهند.




به این ترتیب دیگر نیازی به تعریف یک url به maven نخواهد بود.

برای دیدن این کتابخانه در صفحه jcenter به ترتیب شناسه‌های Group_ID.Artifact.version را دنبال کنید، یعنی برای ما می‌شود:

com/plus/androidbreadcrumb/1.0
نکته اول : این نکته را به یاد داشته باشید که عمل سینک تنها یکبار اتفاق می‌افتد و در زمان‌های بعد، هر تغییری مثل حذف، به روز آوری و ... مستقیما در jcenter بعد از چند دقیقه اعمال می‌شود.

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


ارسال کتابخانه به mavenCentra
 در این مرحله قصد داریم که این کتابخانه را بر روی mavenCentral هم داشته باشیم. اگر قصدش را ندارید از اینجا به بعد را نیازی نیست انجام بدهید و برای اینکار لازم است همه‌ی مراحل بالا انجام گرفته باشد.
قبل از اینکه این عمل ارسال انجام گیرد، باید دو عمل زیر از قبل صورت گرفته باشند:
  1. پکیج شما در jcenter تایید شده باشد.
  2. با مخزن شما در sonatype موافقت شده باشد.

در صورتیکه دو مرحله‌ی بالا صورت گرفته باشند، در صفحه‌ی پکیج اختصاصی، بر روی گزینه‌ی mavenCentral کلیک کنید:

پس از آن باید نام کاربری و کلمه‌ی عبورتان را در SonaType، وارد کنید و گزینه‌ی sync را بفشارید:

در صورتیکه پیام موفقیت در سینک را بدهد، پکیج شما منتقل شده‌است. در غیر این صورت خطای آن را اعلام می‌کند و باید برای رفع آن تلاش کنید تا خطاها از بین بروند. برای اینکه بتوانید این پکیج را در لیست mavenCentral ببینید، مثل همان چیزی که در بالاتر گفته شد، شناسه‌ی گریدل را دنبال کنید.

مطالب
Persist ، Load و Bookmark در Workflow

در خیلی از مواقع workflow‌ها به مرحله‌ای می‌رسند که احتیاج به دستوری از بیرون از فرآیند دارند. در هنگام انتظار، اگر به هر دلیلی workflow از حافظه حذف شود، امکان ادامه فرآیند وجود ندارد. اما می‌توان با Persist (ذخیره) کردن آن، در زمان انتظار و فراخوانی مجدد آن در هنگام نیاز، این ریسک را برطرف نمود.

قصد دارم با این مثال، طریقه persist شدن یک workflow در زمانیکه نیاز به انتظار برای تایید دارد و فراخوانی آن از همان نقطه پس از تایید مربوطه را توضیح دهم.

ساختار اینترفیس کاربری ما WPF می‌باشد. پس در ابتدا یک پروژه از نوع WPF ایجاد می‌کنیم. اسم solution  را PersistWF و اسم Project را PersistWF.UI انتخاب می‌کنیم.

در پروژه  UI نام فایل MainWindow.xaml  را به AddRequest.xaml تغییر می‌دهیم. همچنین اسم کلاس مربوطه را در codebehind 

همین طور مقدار StartupUri را هم در app.xaml اصلاح می‌کنیم

StartupUri="AddRequest.xaml"

Reference ‌های زیر رو هم به پروژه اضافه می‌کنیم 

•System.Activities
•System.Activities.DurableInstancing
•System.Configuration
•System.Data.Linq
•System.Runtime.DurableInstancing
•System.ServiceModel
•System.ServiceModel.Activities
•System.Workflow.ComponentModel
•System.Runtime.DurableInstancing
•System.Activities.DurableInstancing

قرار است کاربری ثبت نام کند، در فرایند ثبت، منتظر تایید یکی از مدیران قرار می‌گیرد. مدیر، لیست کاربران جدید را می‌بینید، یک کاربر را انتخاب می‌کند؛ مقادیر لازم را وارد می‌کند و سپس پروسه تایید را انجام می‌دهد که فراخوانی فرآیند مربوطه از همان قسمتی‌است که منتظر تایید مانده است.

برای Persist کردن workflow از کلاس SqlWorkflowInstanceStore   استفاده می‌کنم. این شی به connection ای به یک دیتابیس با یک ساختار معین احتیاج دارد. خوشبختانه اسکریپت‌های مورد نیاز این ساختار در پوشه [Drive]:\Windows\Microsoft.NET\Framework\v4.0.30319\SQL\en وجود دارند. دو اسکریپت با نام‌های SqlWorkflowInstanceStoreSchema و SqlWorkflowInstanceStoreLogic باید به ترتیب در دیتابیس اجرا شوند.

من یک دیتابیس با نام PersistWF ایجاد می‌کنم و اسکریپت‌ها را بر روی آن اجرا می‌کنم. یک جدول هم برای نگهداری کاربران ثبت شده در همین دیتابیس ایجاد می‌کنم.

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

XAML زیر، ساختار فرم AddRequest می‌باشد که قرار است نقش UI برنامه را ایفا کند. آن را با XAML‌های پیش فرض عوض کنید. 

<Window x:Class="PersistWF.UI.AddRequest"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="520" Width="550" Loaded="Window_Loaded">
    <Grid MinWidth="300" MinHeight="100" Width="514">
        <Label Height="30" Margin="5,10,10,10" Name="lblName"  VerticalAlignment="Top" HorizontalAlignment="Left" Width="90"  HorizontalContentAlignment="Right">Name:</Label>
        <Label Height="30" Margin="270,10,10,10" Name="lblPhone"  VerticalAlignment="Top" HorizontalAlignment="Left" Width="90"  HorizontalContentAlignment="Right">Phone Number:</Label>
        <Label Height="30" Margin="5,40,10,10" Name="lblEmail"  VerticalAlignment="Top" HorizontalAlignment="Left" Width="90"  HorizontalContentAlignment="Right">Email:</Label>
        <TextBox Height="25" Margin="100,10,10,10" Name="txtName"  VerticalAlignment="Top" HorizontalAlignment="Left" Width="170" />
        <TextBox Height="25" Margin="365,10,10,10" Name="txtPhone"  VerticalAlignment="Top" HorizontalAlignment="Left" Width="100" />
        <TextBox Height="25" Margin="100,40,10,10" Name="txtEmail"  VerticalAlignment="Top" HorizontalAlignment="Left" Width="300" />
        <Button Height="23" Margin="100,86,0,0" Name="brnRegister"  VerticalAlignment="Top" HorizontalAlignment="Left" Width="70"  Click="brnRegister_Click">Register</Button>
        <ListView x:Name="lstUsers" Margin="10,125,10,10" Height="145"  VerticalAlignment="Top" ItemsSource="{Binding}"  HorizontalContentAlignment="Center"  SelectionChanged="lstUsers_SelectionChanged" >
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="Current User" Width="480">
                        <GridViewColumn.CellTemplate>
                            <DataTemplate>
                                <StackPanel Orientation="Horizontal">
                                    <TextBlock Text="{Binding Name}"  Width="110"/>
                                    <TextBlock Text="{Binding Phone}"  Width="70"/>
                                    <TextBlock Text="{Binding Email}"  Width="130"/>
                                    <TextBlock Text="{Binding Status}"  Width="70"/>
                                    <TextBlock Text="{Binding AcceptedBy}"  Width="100"/>
                                </StackPanel>
                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>
                </GridView>
            </ListView.View>
        </ListView>
        <Label Height="37" HorizontalAlignment="Stretch" Margin="10,272,5,10"  Name="lblSelectedNotes" VerticalAlignment="Top" Visibility="Hidden" />
        <Label Height="30" Margin="10,0,0,140" Name="lblAgent"  VerticalAlignment="Bottom" HorizontalAlignment="Left" Width="40"  HorizontalContentAlignment="Left" Visibility="Hidden">Admin Name:</Label>
        <TextBox Height="25" Margin="60,0,0,140" Name="txtAcceptedBy"  VerticalAlignment="Bottom" HorizontalAlignment="Left" Width="190"  Visibility="Hidden" />
        <Button Height="25" Margin="270,0,0,140" Name="btnAccept"  VerticalAlignment="Bottom" HorizontalAlignment="Left" Width="90"  Click="btnAccept_Click" Visibility="Hidden">Accept</Button>
        <Label Height="27" HorizontalAlignment="Left" Margin="10,0,0,110"  Name="lblEvent" VerticalAlignment="Bottom" Width="76">Event Log</Label>
        <ListBox Margin="12,0,5,12" Name="lstEvents" Height="100"  VerticalAlignment="Bottom" FontStretch="Condensed" FontSize="10" />
    </Grid>
</Window>

اگر همه چیز مرتب باشد؛ ساختار فرم شما باید به این شکل باشد 

اکثر workflow‌ها از activity معروف  WrteLine استفاده می‌کنند که برای نمایش یک رشته به کار می‌رود. ما هم در workflow مثالمان از این Activity استفاده می‌کنیم. اما برای اینکه مقادیری که توسط این Activity ایجاد می‌شوند در کادر event log فرم خودمان نمایش داده شود؛ احتیاج داریم که یک TextWriter سفارشی برای خودمان ایجاد کنیم. اما قبل از آن یک کلاس static در پروژه ایجاد می‌کنیم که بتوانیم در هر قسمتی، به فرم دسترسی داشته باشیم.

کلاسی را با نام ApplicationInterface به پروژه اضافه کرده و یک  Property استاتیک از جنس فرم AddRequest هم برای آن تعریف می‌کنیم:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PersistWF.UI
{
    public static class ApplicationInterface
    {
        public static AddRequest _app { get; set; }
    }
}

به Constructor کلاس موجود در فایل AddRequest.xaml.cs  این خط کد رو اضافه می‌کنم

        public AddRequest()
        {
            InitializeComponent();
            ApplicationInterface._app = this;
        }
این دو متد را هم به این کلاس اضافه می‌کنیم  
private void AddEvent(string szText)
        {
            lstEvents.Items.Add(szText);
        }
        public ListBox GetEventListBox()
        {
            return this.lstEvents;
        }

متد اول برای اضافه کردن یک event Log و متد دوم هم که کنسول لاگ را در اختیار درخواست کننده‌اش قرار می‌دهد.

و حالا کلاس TextWriter سفارشی‌امان را می‌نویسیم. یک کلاس به نام ListBoxTextWriter به پروژه اضافه می‌کنیم که از TextWriter مشتق می‌شود و محتویات آن‌را در زیر می‌بینید: 

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls;

namespace PersistWF.UI
{
    public class ListBoxTextWriter : TextWriter 
    { 
         const string textClosed = "This TextWriter must be opened before use"; 
         private Encoding _encoding; 
         private bool _isOpen = false; 
         private ListBox _listBox; 
         public ListBoxTextWriter() 
         { 
             // Get the static list box 
             _listBox = ApplicationInterface._app.GetEventListBox(); 
             if (_listBox != null) 
             _isOpen = true; 
         } 
         public ListBoxTextWriter(ListBox listBox) 
         { 
             this._listBox = listBox; 
             this._isOpen = true; 
         } 
         public override Encoding Encoding 
         { 
             get 
             { 
                if (_encoding == null) 
                { 
                    _encoding = new UnicodeEncoding(false, false); 
                } 
                return _encoding; 
             } 
         }
         public override void Close()
         {
             this.Dispose(true);
         }
         protected override void Dispose(bool disposing)
         {
             this._isOpen = false;
             base.Dispose(disposing);
         }
         public override void Write(char value)
         {
             if (!this._isOpen)
                 throw new ApplicationException(textClosed); ;
             this._listBox.Dispatcher.BeginInvoke(new Action(() => this._listBox.Items.Add(value.ToString())));
         }
         public override void Write(string value)
         {
             if (!this._isOpen)
                 throw new ApplicationException(textClosed); 
             if (value != null)
                 this._listBox.Dispatcher.BeginInvoke(new Action(() => this._listBox.Items.Add(value)));
         }
         public override void Write(char[] buffer, int index, int count)
         {
             String toAdd = "";
             if (!this._isOpen)
                 throw new ApplicationException(textClosed); ;
             if (buffer == null || index < 0 || count < 0)
                 throw new ArgumentOutOfRangeException("buffer");
             if ((buffer.Length - index) < count)
                 throw new ArgumentException("The buffer is too small");
             for (int i = 0; i < count; i++)
                 toAdd += buffer[i];
             this._listBox.Dispatcher.BeginInvoke(new Action(() => this._listBox.Items.Add(toAdd)));
         }
    }
}

همان طور که می‌بینید کلاس ListBoxTextWriter از کلاس abstract  TextWriter  مشتق شده و پیاده سازی از متد Write را فراهم می‌کند تا یک رشته را به کنترل ListBox اضافه کنه. (البته سه تا از این متد‌ها را Override می‌کنیم تا بتوانیم یک رشته، یک کاراکتر و یا آرایه ای از کاراکتر‌ها را به ListBox اضافه کنیم) در constructor  پیشفرض از کلاس ApplicationInterface استفاده کردیم تا بتوانیم کنترل lstEvents را از فرم اصلی برنامه به دست بیاوریم. برای Add کردن از Dispatcher و متد BeginInvoke مرتبط با آن استفاده کردیم . این کار، متد را قادر می‌سازد حتی وقتی‌که از یک thread متفاوت فراخوانی می‌شود، کار کند.

حالا می‌توانیم از این کلاس، به عنوان مقدار خاصیت TextWriter برای WriteLine استفاده کنیم.

به کلاس ApplicationInterface برگردیم تا متد زیر را هم به آن اضافه کنیم 

public static void AddEvent(String status)
        {
            if (_app != null)
            {
                new ListBoxTextWriter(_app.GetEventListBox()).WriteLine(status);
            }
        }

این هم از constructor دومی استفاده می‌کنه برای معرفی ListBox.

برای ارتباط با دیتابیس از LINQ to SQL استفاده می‌کنیم تا User رو ذخیره و بازیابی کنیم. به پروژه یک آیتم از نوع LINQ to SQL با نام UserData.dbml اضافه می‌کنیم. به دیتابیس متصل شده و جدول User رو به محیط Design می‌کشیم. در ادامه برای شی کلاس SQLWorkflowInstanceStore هم از همین Connectionstring استفاده می‌کنیم. 

برای ایجاد workflow مورد نظر، به دو Activity سفارشی احتیاج داریم که باید خودمان ایجاد نماییم. یک پوشه با نام Activities به پروژه اضافه می‌کنم تا کلاس‌های مورد نظر را آن‌جا ایجاد کنیم.

1. یک Activity برای ایجاد User

این Activity تعدادی پارامتر از نوع InArgument دارد که توسط آن‌ها یک Instance از کلاس User ایجاد می‌کند و در حقیقت آن را به دیتابیس می‌فرستد و دخیره می‌کند. Connectionstring را هم می‌شود توسط یک آرگومان ورودی دیگر مقدار دهی کرد. یک آرگومان خروجی هم برای این Activity در نظر می‌گیریم تا User ایجاد شده را برگردانیم. روی پوشه‌ی Activities کلیک راست می‌کنیم و Add - NewItem را انتخاب می‌کنیم. از لیست workflow‌ها Template مربوط به CodeActivity را انتخاب کرده و یک CodeActivity با نام CreateUser ایجاد می‌کنیم 

محتویات این کلاس را هم مانند زیر کامل می‌کنیم 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Activities;

namespace PersistWF.UI.Activities
{

    public sealed class CreateUser : CodeActivity
    {


        public InArgument<string> Name { get; set; }
        public InArgument<string> Email { get; set; }
        public InArgument<string> Phone { get; set; }
        public InArgument<string> ConnectionString { get; set; }

        public OutArgument<User> User { get; set; }

        protected override void Execute(CodeActivityContext context)
        {
            // ایجاد کاربر
            User user = new User();
            user.Email = Email.Get(context);
            user.Name = Name.Get(context);
            user.Phone = Phone.Get(context);
            user.Status = "New";
     user.WorkflowID = context.WorkflowInstanceId;
            UserDataDataContext db = new UserDataDataContext(ConnectionString.Get(context));
            db.Users.InsertOnSubmit(user);
            db.SubmitChanges();
            User.Set(context, user);
        }
    }
}

متد Execute، توسط مقادیری که به عنوان پارامتر دریافت شده، یک شی از کلاس User ایجاد می‌کند و به کمک DataContext آن‌را در دیتابیس دخیره کرده و در آخر User ذخیره شده را در اختیار پارامتر خروجی قرار می‌دهد.

1. یک Activity برای انتظار دریافت تایید

این Activity قرار است Workflow را Idle کند تا زمانیکه مدیر دستور تایید را با فراخوانی مجدد workflow از این همین قسمت صادر نماید.

این Activity باید از NativeActivity مشتق شده و برای اینکه workflow را وادرا به معلق شدن کند کافی‌است خاصیت CanInduceIdle را با مقدار برگشتی true , override کنیم.

مثل قسمت قبل یک CodeActivity ایجاد می‌کنیم. اینبار با نام WaitForAccept که محتویاتش را هم مانند زیر تغییر می‌دهیم. 

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Activities;
using System.Workflow.ComponentModel;

namespace PersistWF.UI.Activities
{

    public sealed class WaitForAccept<T> : NativeActivity<T>
    {
        public WaitForAccept()
            :base()
        {

        }
        public string BookmarkName { get; set; }
        public OutArgument<T> Input { get; set; } 

        protected override void Execute(NativeActivityContext context)
        {
            context.CreateBookmark(BookmarkName, new BookmarkCallback(this.Continue));
        }

        private void Continue(NativeActivityContext context, Bookmark bookmark, object value)
        {
            Input.Set(context, (T)value); 
        }
        protected override bool CanInduceIdle
        {
            get
            {
                return true;
            }
        }
    }
}
این کلاس را generic نوشتم تا به جای User بشود هر پارامتر دیگه‌ای را به آن ارسال کرد. در واقع وقتی workflow به این Activity می‌رسد، Idle می‌شود. این activity  یک bookmark هم ایجاد می‌کند. ما وقتی workflow را با این bookmark فراخوانی کنیم؛ workflow از همینجا ادامه می‌یابد. فراخوانیbookmark می‌تواند همراه با وارد کردن یک  object باشد. متد Continue آن object را به آرگومان خروجی می‌دهد تا مسیر workflow را طی کند.
ما User  هایی را که به این نقطه رسیدنْ نمایش می‌دهیم. مدیر اونها را دیده و با مقدار دهی فیلد AcceptedBy، آن User را از اینجا به workflow می‌فرستد و ما user وارد شده را در ادامه‌ی فرآیند Accept می‌کنیم.
 
برای ایجاد workflow هم می‌توانید از designer استفاده کنید و هم می‌توانید کد مربوط به workflow را پیاده سازی کنید.

برای پیاده سازی از طریق کد، یک کلاس با نام UserWF ایجاد می‌کنیم و محتویات workflow را مانند زیر پیاده سازی خواهیم کرد:

using PersistWF.UI.Activities;
using System;
using System.Activities;
using System.Activities.Statements;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PersistWF.UI
{
     public sealed class UserWF : Activity
    {
        public InArgument<string> Name { get; set; }
        public InArgument<string> Email { get; set; }
        public InArgument<string> Phone { get; set; }
        public InArgument<string> ConnectionString { get; set; }
        public InArgument<TextWriter> Writer { get; set; }

        public UserWF()
        {
            Variable<User> User = new Variable<User> { Name = "User" };
            this.Implementation = () => new Sequence
            {
                DisplayName = "EnterUser",
                Variables = { User },
                Activities = { 
                     new CreateUser   //  1. ایجاد کاربر با ورود پارامتر‌های ورودی  
                    {
                        ConnectionString = new InArgument<string>(c=> ConnectionString.Get(c)),
                        Email = new InArgument<string>(c=> Email.Get(c)),
                        Name = new InArgument<string>(c=> Name.Get(c)),
                        Phone = new InArgument<string>(c=> Phone.Get(c)),
                        User = new OutArgument<User>(c=> User.Get(c))
                    },
                    new WriteLine // 2. لاگ مربوط به دخیره کاربر
                    {
                        TextWriter = new InArgument<TextWriter>(c=> Writer.Get(c)),
                        Text = new InArgument<string>(c=> string.Format("User {0} Registered and waiting for Accept", Name.Get(c) ) )
                    },
                    new InvokeMethod 
                     { 
                         TargetType = typeof(ApplicationInterface),  // 3. برای به روزرسانی لیست کاربران ثبت شده در نمایش فرم
                         MethodName = "NewUser", 
                         Parameters = 
                         { 
                            new InArgument<User>(env => User.Get(env)) 
                         } 
                     }, 
                     new WaitForAccept<User>  // 4. اینجا فرایند متوقف می‌شود و منتظر تایید مدیر می‌ماند
                     {  
                        BookmarkName = "GetAcceptes",
                        Input = new OutArgument<User>(env => User.Get(env))
                     },
                     new WriteLine // 5. لاگ مربوط به تایید شدن کاربر
                     {
                         TextWriter = new InArgument<TextWriter>(c=> Writer.Get(c)),
                         Text = new InArgument<string>(c=> string.Format("User {0} Accepter by {1}",Name.Get(c),User.Get(c).AcceptedBy))
                     }
                }

            };

        }

    }
}

اگر بخوایم از Designer استفاده کنیم.  فرایندمان چیزی شبیه شکل زیر خواهد بود 

به Application بر می‌گردیم تا آن را پیاده سازی کنیم. ابتدا به app.config که اتوماتیک ایجاد شده رفته تا اسم Connectionstring  رو به UserGenerator تغییر دهیم. محتویات درون app.config به شکل زیر است. 

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <configSections>
    </configSections>
    <connectionStrings>
        <add name="UserGenerator"
            connectionString="Data Source=.;Initial Catalog=PersistWF;Integrated Security=True"
            providerName="System.Data.SqlClient" />
    </connectionStrings>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
    </startup>
</configuration>

در کلاس AddRequest کد زیر را اضافه می‌کنم. برای نگهداری مقدار connectionstring 

private string _connectionString = "";

همچنین کد‌های زیر را به رویداد Load فرم اضافه می‌کنم تا مقدار ConnectionString را از Config بخوانم: 

Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
            ConnectionStringsSection css = (ConnectionStringsSection)config.GetSection("connectionStrings");
            _connectionString =  css.ConnectionStrings["UserGenerator"].ConnectionString;

خط زیر را هم به کلاس AddRequest اضافه نمایید.  

private InstanceStore _instanceStore;

این ارجاعیه  به کلاس InstanceStore که برای Persist و Load کردن workflow از آن استفاده می‌کنیم و کد‌های زیر را هم به رویداد Load فرم اضافه می‌کنیم.  

_instanceStore = new SqlWorkflowInstanceStore(_connectionString);
            InstanceView view = _instanceStore.Execute(_instanceStore.CreateInstanceHandle(), new CreateWorkflowOwnerCommand(), TimeSpan.FromSeconds(30));
            _instanceStore.DefaultInstanceOwner = view.InstanceOwner;

InstanceStore یک کلاس abstract  می باشد که همه‌ی Provider‌های مربوط به persistence از آن مشتق می‌شوند. در این پروژه من از کلاس SqlWorkflowInstanceStore استفاده کردم تا workflow‌ها را در دیتابیسSQL Server ذخیره کنم.

برای ایجاد یک Request مقادیر را از فرم دریافت کرده، یک User ایجاد می‌کنیم و آن را در فرآیند به جریان می‌اندازیم. این کار را در رویداد کلیک دکمه Register انجام می‌دهیم 

private void brnRegister_Click(object sender, RoutedEventArgs e)
        {
            Dictionary<string, object> parameters = new Dictionary<string, object>();
            parameters.Add("Name", txtName.Text);
            parameters.Add("Phone", txtPhone.Text);
            parameters.Add("Email", txtEmail.Text);
            parameters.Add("ConnectionString", _connectionString);
            parameters.Add("Writer", new ListBoxTextWriter(lstEvents));
            WorkflowApplication i = new WorkflowApplication
            (new UserWF(), parameters);
            // Setup persistence 
            i.InstanceStore = _instanceStore;
            i.PersistableIdle = (waiea) => PersistableIdleAction.Unload;
            i.Run(); 
        }

پارامتر‌های ورودی را از روی فرم مقدار دهی می‌کنیم. یک شی از کلاس WorkflowApplication ایجاد می‌کنیم. خاصیت InstanceStore آن را با Store ای که ایجاد کردیم مقدار دهی می‌کنیم. توسط رویداد PersistableIdle فرآیند رو مجبور می‌کنیم به Persist شدن و Unload شدن.

و سپس فرایند را اجرا می‌کنم.

اگر یادتان باشد، در فرآیند، از یک InvoceMethod استفاده کردیم. متد مورد نظر را هم در کلاس ApplicationInterface.cs ایجاد می‌کنیم. 

public static void NewUser(User l)
        {
            if (_app != null)
                _app.AddNewUser(l);
        }

همین طور که می‌بینید، یک متد هم در کلاس AddRequest ایجاد می‌شود؛ با این محتوا 

public void AddNewUser(User l)
        {
            this.lstUsers.Dispatcher.BeginInvoke(new Action(() => this.lstUsers.Items.Add(l)));
        }

این متد فقط یک کاربر را به لیست کاربران اضافه می‌کند. این لیست همه کاربران را نشان می‌دهد. توسط رویداد SelectionChanged این کنترل، کاربر انتخاب شده را بررسی کرده در صورتی که کاربر جدید باشد، امکان تایید شدن را برایش فراهم می‌کنیم؛ که نمایش دکمه تایید است. 

private void lstUsers_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (lstUsers.SelectedIndex >= 0)
            {
                User l = (User)lstUsers.Items[lstUsers.SelectedIndex];
                lblSelectedNotes.Visibility = Visibility.Visible;
                if (l.Status == "New")
                {
                    lblAgent.Visibility = Visibility.Visible;
                    txtAcceptedBy.Visibility = Visibility.Visible;
                    btnAccept.Visibility = Visibility.Visible;
                }
                else
                {
                    lblAgent.Visibility = Visibility.Hidden;
                    txtAcceptedBy.Visibility = Visibility.Hidden;
                    btnAccept.Visibility = Visibility.Hidden;
                }
            }
            else
            {
                lblSelectedNotes.Content = "";
                lblSelectedNotes.Visibility = Visibility.Hidden;
                lblAgent.Visibility = Visibility.Hidden;
                txtAcceptedBy.Visibility = Visibility.Hidden;
                btnAccept.Visibility = Visibility.Hidden;
            } 
        }

و برای رویداد کلیک دکمه تایید کاربر : 

private void btnAccept_Click(object sender, RoutedEventArgs e)
        {
            if (lstUsers.SelectedIndex >= 0) 
             { 
                 User u = (User)lstUsers.Items[lstUsers.SelectedIndex]; 
                 Guid id = u.WorkflowID.Value;
                 UserDataDataContext dc = new UserDataDataContext(_connectionString); 
                 dc.Refresh(RefreshMode.OverwriteCurrentValues, dc.Users);
                 u = dc.Users.SingleOrDefault<User>(x => x.WorkflowID == id); 
                 if (u != null) 
                 { 
                     u.AcceptedBy = txtAcceptedBy.Text; 
                     u.Status = "Assigned"; 
                     dc.SubmitChanges();
                     // Clear the input 
                     txtAcceptedBy.Text = "";
                 }
                 // Update the grid 
                 lstUsers.Items[lstUsers.SelectedIndex] = u;
                 lstUsers.Items.Refresh();
                 WorkflowApplication i = new WorkflowApplication(new UserWF());
                 i.InstanceStore = _instanceStore;
                 i.PersistableIdle = (waiea) => PersistableIdleAction.Unload;
                 i.Load(id);
                 try
                 {
                     i.ResumeBookmark("GetAcceptes", u);
                 }
                 catch (Exception e2)
                 {
                     AddEvent(e2.Message);
                 }
             } 
        }

کاربر را انتخاب می‌کنم مقادیرش را تنظیم می‌کنیم. آن را دخیره کرده و workflow را از روی guid مربوط به آن که قبلا در فرآیند به Entity دادیم، Load می‌کنیم و همانطور که می‌بینید توسط متد ResumeBookmark فرآیند رو از جایی که می‌خواهیم ادامه می‌دهیم. البته می‌توان تایید کاربر را هم در خود فرآیند انجام داد و چون نوشتن Activity  مرتبط با آن تقریبا تکراری است با اجازه‌ی شما من اون رو ننوشتم و زحمتش با خودتونه.

حالا فقط مانده‌است که همه کاربران را در ابتدای نمایش فرم از دیتابیس فراخوانی کنیم و در لیست نمایش دهیم:

private void LoadExistingLeads()
        {
            UserDataDataContext dc = new UserDataDataContext(_connectionString);
            dc.Refresh(RefreshMode.OverwriteCurrentValues, dc.Users);
            IEnumerable<User> q = dc.Users;
            foreach (User u in q)
            {
                AddNewUser(u);
            }
        }

و فراخوانی این متد را به انتهای رویداد Load صفحه واگذار می‌کنیم.

پروژه رو اجرا کرده و یک کاربر را اضافه می‌کنم. همانطور که می‌دانید این کاربر در فرآیند ایجاد و در دیتابیس ذخیره می‌شود

برنامه را می‌بندم و دوباره اجرا می‌کنم. کاربر را انتخاب می‌کنم و یک نام برای admin انتخاب و آن را تایید می‌کنم. فرآیند را از bookmark مورد نظر اجرا کرده و به پایان می‌رسد. با بسته شدن برنامه، فرایند Idle و Unload می‌شود و ذخیره آن در sqlserver صورت می‌گیرد. 

مطالب
ایجاد نقشه سایت (Site Map) داینامیک
همان طور که می‌دانید نقشه‌ی سایت علاوه بر استفاده از MetaTag‌ها و Url Routing‌ها و ... یکی از نکات اصلی برای سایت شماست که در نتایج گوگل برای جستجو کنندگان نمایش داده شود .

در این مقاله من قصد دارم که به شما چگونگی ایجاد و کار با یک SiteMap  داینامیک را آموزش دهم .
منظور از SiteMap داینامیک این است که به ازای هر مطلبی که شما در سایت تان ایجاد ، ویرایش یا حذف می‌کنید چنانچه دارای صفحه ای در سایت باشد SiteMap تغییر می‌کند .

ساختار یک SiteMap به صورت زیر است :

<?xml version="1.0" encoding="utf-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
<loc>Url Page 1</loc>
<lastmod>2014-02-20</lastmod>
<changefreq>always</changefreq>
<priority>1</priority>
  </url>
  <url>
<loc>Url Page 2</loc>
<lastmod>2014-02-20</lastmod>
<changefreq>always</changefreq>
<priority>1</priority>
  </url>
</urlset>
به ازای هر مطلبی که به سایتتان اضافه می‌کنید چنانچه آن مطلب جهت نمایش دارای Url باشد ، باید یک تگ Url  به SiteMap اضافه شود.
تگ loc موجود در تگ url آدرس صفحه را مشخص می‌کند .
تگ lastmod تاریخ اضافه کردن یا آخرین ویرایش را نمایش می‌دهد .
تگ changefreq که دوره‌ی بروز رسانی صفحه را مشخص می‌کند .
تگ priority الویت صفحه را مشخص می‌کند .

که من در کد نویسی تگ changefreq  را always  و تگ priority را  1 قرار دادم.

در فایل ضمیمه یک کلاس به اسم updateSiteMap.cs وجود دارد که تابع آن شامل دو پارامتر ورودی مانند زیر است :

public void UpdateSiteMap(string Addr,string NewOpr)
پارامتر Addr که آدرس صفحه ای است که شما می‌خواهید به SiteMap اضافه شود .
پارامتر NewOpr که می‌تواند شامل یکی از سه مقدار زیر باشد  :add ، edit ، delete

اگر پارامتر NewOpr دارای مقدار add باشد یعنی مقدار موجود در پارامتر Addr را بهSiteMap اضافه کن . یعنی یک تگ url  یه SiteMap اضافه کن .
اگر پارامتر NewOpr دارای مقدار edit باشد یعنی مقدار موجود در تگ <lastmod> را ویرایش کن . یعنی تاریخ آخرین بروزرسانی تغییر می‌کند .
اگر پارامتر NewOpr دارای مقدار delete باشد یعنی تگ url ایی که محتوای تگ loc آن برابر است با مقدار موجود در پارامتر Addr را از SiteMap حذف کن.

این بخش از کد موجود در فایل ضمیمه uupdateSiteMaop.cs  قسمت edit و delete  نقشه‌ی سایت را انجام می‌دهد .
 if (NewOpr != "add")
{
XmlElement xmlElement = xmlDoc.DocumentElement;
if (xmlElement.ChildNodes != null)
{
  foreach (XmlElement myElement in xmlDoc.DocumentElement)
  {
if (myElement.ChildNodes[0].InnerText == Addr)
{
  if (NewOpr != "delete")
myElement.ChildNodes[1].InnerText = DateTime.Now.ToString("yyyy-MM-dd");
  else
myElement.ParentNode.RemoveChild(myElement);
break;
}
  }
}
}

و بخش else دستور بالا قسمت Add  را انجام می‌دهد .یعنی کدهای زیر:
 else
{
string ns="http://www.sitemaps.org/schemas/sitemap/0.9";
XmlNode url = xmlDoc.CreateNode(XmlNodeType.Element, "url",ns );
XmlNode loc = xmlDoc.CreateNode(XmlNodeType.Element, "loc", ns);
XmlNode lastmod = xmlDoc.CreateNode(XmlNodeType.Element, "lastmod", ns);
XmlNode changefreq = xmlDoc.CreateNode(XmlNodeType.Element, "changefreq", ns);
XmlNode priority = xmlDoc.CreateNode(XmlNodeType.Element, "priority", ns);
loc.InnerText = Addr;
lastmod.InnerText = DateTime.Now.ToString("yyyy-MM-dd");
changefreq.InnerText = "always";
priority.InnerText = "1";
url.AppendChild(loc);
url.AppendChild(lastmod);
url.AppendChild(changefreq);
url.AppendChild(priority);
xmlDoc.DocumentElement.AppendChild(url);
}
اگر اطلاعاتی را به جدول اضافه می‌کنید و می‌خواهید Url صفحه‌ی مربوط به آن اطلاعات برای شما در SiteMap اضافه شود  بعد از ذخیره شدن اطلاعات در جدول بلافاصله کد زیر را اضافه می‌کنید :
//
Add Info In Table
//
updateSiteMap updateSiteMap = new updateSiteMap();
updateSiteMap.UpdateSiteMap("Url Page", "add");

برای قسمت ویرایش هم پس از آنکه اطلاعات را ویرایش کردید چنانچه برای آن اطلاعات صفحه ای را در SiteMap درج کرده اید کد زیر را می‌نویسید :
 updateSiteMap updateSiteMap = new updateSiteMap();
updateSiteMap.UpdateSiteMap("Url Page", "edit");

برای قسمت حذف هم اگر شما اطلاعاتی را از جدول حذف می‌کنید چنانچه برای آن اطلاعات صفحه ای در SiteMap درج کرده اید کد زیر را می‌نویسید :
  updateSiteMap updateSiteMap = new updateSiteMap();
updateSiteMap.UpdateSiteMap("Url Page", "delete");

موفق باشید .


 
Files.zip
مطالب دوره‌ها
پیاده سازی امتیاز دهی ستاره‌ای به مطالب به کمک jQuery در ASP.NET MVC
در این قسمت قصد داریم با نحوه پیاده سازی امتیاز دهی ستاره‌ای به مطالب، که نمونه‌ای از آن‌را در سایت جاری در قسمت‌های مختلف آن مشاهده می‌کنید، آشنا شویم.


مدل برنامه

در ابتدای کار نیاز است تا ساختاری را جهت ارائه لیستی از مطالب که دارای گزینه امتیاز دهی می‌باشند، تهیه کنیم:
namespace jQueryMvcSample03.Models
{
    public class BlogPost
    {
        public int Id { set; get; }
        public string Title { set; get; }
        public string Body { set; get; }

        /// <summary>
        /// اطلاعات رای گیری یک مطلب به صورت یک خاصیت تو در تو یا پیچیده
        /// </summary>
        public Rating Rating { set; get; }

        public BlogPost()
        {
            Rating = new Rating();
        }
    }
}

namespace jQueryMvcSample03.Models
{
    //[ComplexType]
    public class Rating
    {
        public double? TotalRating { get; set; }
        public int? TotalRaters { get; set; }
        public double? AverageRating { get; set; }
    }
}
اگر با EF Code first آشنا باشید، خاصیت Rating تعریف شده در اینجا می‌تواند از نوع ComplexType تعریف شود که شامل جمع امتیازهای داده شده، تعداد کل رای دهنده‌ها و همچنین میانگین امتیازهای حاصل است.


منبع داده فرضی برنامه

using System.Collections.Generic;
using System.Linq;
using jQueryMvcSample03.Models;

namespace jQueryMvcSample03.DataSource
{
    /// <summary>
    /// منبع داده فرضی
    /// </summary>
    public static class BlogPostDataSource
    {
        private static IList<BlogPost> _cachedItems;
        /// <summary>
        /// با توجه به استاتیک بودن سازنده کلاس، تهیه کش، پیش از سایر فراخوانی‌ها صورت خواهد گرفت
        /// باید دقت داشت که این فقط یک مثال است و چنین کشی به معنای
        /// تهیه یک لیست برای تمام کاربران سایت است
        /// </summary>
        static BlogPostDataSource()
        {
            _cachedItems = createBlogPostsInMemoryDataSource();
        }

        /// <summary>
        /// هدف صرفا تهیه یک منبع داده آزمایشی ساده تشکیل شده در حافظه است
        /// </summary>        
        private static IList<BlogPost> createBlogPostsInMemoryDataSource()
        {
            var results = new List<BlogPost>();
            for (int i = 1; i < 30; i++)
            {
                results.Add(new BlogPost { Id = i, Title = "عنوان " + i, Body = "متن ... متن ... متن " + i, Rating = new Rating { TotalRaters = i + 1, AverageRating = 3.5 } });
            }
            return results;
        }

        /// <summary>
        /// پارامترهای شماره صفحه و تعداد رکورد به ازای یک صفحه برای صفحه بندی نیاز هستند
        /// شماره صفحه از یک شروع می‌شود
        /// </summary>
        public static IList<BlogPost> GetLatestBlogPosts(int pageNumber, int recordsPerPage = 4)
        {
            var skipRecords = pageNumber * recordsPerPage;
            return _cachedItems
                        .OrderByDescending(x => x.Id)
                        .Skip(skipRecords)
                        .Take(recordsPerPage)
                        .ToList();
        }
    }
}
در این مثال نیز از یک منبع داده فرضی تشکیل شده در حافظه استفاده خواهیم کرد تا امکان اجرای پروژه پیوستی را بدون نیاز به بانک اطلاعاتی خاصی و بدون نیاز به مقدمات برپایی آن، به سادگی داشته باشید.
در این منبع داده ابتدا لیستی از مطالب تهیه شده و سپس کش می‌شوند. در ادامه توسط متد GetLatestBlogPosts بازه‌ای از این اطلاعات قابل بازیابی خواهند بود که برای استفاده در حالات صفحه بندی اطلاعات بهینه سازی شده است.


آشنایی با طراحی افزونه jQuery Star Rating

افزودن CSS نمایش امتیازها در ذیل هر مطلب

/* star rating system */
.post_rating
{
direction: ltr;
}
.rating
{
text-indent: -99999px;
overflow: hidden;
background-repeat: no-repeat;
display: inline-block;
width: 8px;
height: 16px;
}
.rating.stars
{
background-image: url('Images/star_rating.png');
}
.rating.stars.active
{
cursor: pointer;
}
.star-left_off
{
background-position: -0px -0px;
}
.star-left_on
{
background-position: -16px -0px;
}
.star-right_off
{
background-position: -8px -0px;
}
.star-right_on
{
background-position: -24px -0px;
}
برای نمایش ستاره‌ها و کار با تصویر Images/star_rating.png (که در پروژه پیوست قرار دارد) ابتدا نیاز است CSS فوق را به پروژه خود اضافه نمائید.

افزودن افزونه jQuery Star rating

// <![CDATA[
(function ($) {
    $.fn.StarRating = function (options) {
        var defaults = {            
            ratingStarsSpan: '.rating.stars',
            postInfoUrl: '/',
            loginUrl: '/login',
            errorHandler: null,
            completeHandler: null,
            onlyOneTimeHandler: null
        };
        var options = $.extend(defaults, options);

        return this.each(function () {
            var ratingStars = $(this);

            $(ratingStars).unbind('mouseover');
            $(ratingStars).mouseover(function () {
                var span = $(this).parent("span");
                var newRating = $(this).attr("value");
                setRating(span, newRating);
            });

            $(ratingStars).unbind('mouseout');
            $(ratingStars).mouseout(function () {
                var span = $(this).parent("span");
                var rating = span.attr("rating");
                setRating(span, rating);
            });

            $(ratingStars).unbind('click');
            $(ratingStars).click(function () {
                var span = $(this).parent("span");
                var newRating = $(this).attr("value");
                var text = span.children("span");
                var pID = span.attr("post");
                var type = span.attr("sectiontype");
                postData({ postID: pID, rating: newRating, sectionType: type });
                span.attr("rating", newRating);
                setRating(span, newRating);
            });

            function setRating(span, rating) {
                span.find(options.ratingStarsSpan).each(function () {
                    var value = parseFloat($(this).attr("value"));
                    var imgSrc = $(this).attr("class");
                    if (value <= rating)
                        $(this).attr("class", imgSrc.replace("_off", "_on"));
                    else
                        $(this).attr("class", imgSrc.replace("_on", "_off"));
                });
            }

            function postData(dataJsonArray) {
                $.ajax({
                    type: "POST",
                    url: options.postInfoUrl,
                    data: JSON.stringify(dataJsonArray),
                    contentType: "application/json; charset=utf-8",
                    dataType: "json",
                    complete: function (xhr, status) {
                        var data = xhr.responseText;
                        if (xhr.status == 403) {
                            window.location = options.loginUrl;
                        }
                        else if (status === 'error' || !data) {
                            if (options.errorHandler)
                                options.errorHandler(this);
                        }
                        else if (data == "nok") {
                            if (options.onlyOneTimeHandler)
                                options.onlyOneTimeHandler(this);
                        }
                        else {
                            if (options.completeHandler)
                                options.completeHandler(this);
                        }
                    }
                });
            }
        });
    };
})(jQuery);
// ]]>
اطلاعات فوق، فایل jquery.StarRating.js را تشکیل می‌دهند که باید به پروژه اضافه گردند.
کاری که این افزونه انجام می‌دهد ردیابی حرکت ماوس بر روی ستاره‌های نمایش داده شده و سپس ارسال سه پارامتر ذیل به اکشن متدی که توسط پارامتر postInfoUrl مشخص می‌گردد، پس از کلیک کاربر می‌باشد:
 { postID: pID, rating: newRating, sectionType: type }
همانطور که ملاحظه می‌کنید به ازای هر قطعه رای گیری که به صفحه اضافه می‌شود، Id مطلب، رای داده شده و نام قسمت جاری، به اکشن متدی خاص ارسال خواهند گردید. sectionType از این جهت اضافه گردیده است تا بتوانید با بیش از یک جدول کار کنید و از این افزونه در قسمت‌های مختلف سایت به سادگی بتوانید استفاده نمائید.
در اینجا از errorHandler برای نمایش خطاها، از completeHandler برای نمایش تشکر به کاربر و از onlyOneTimeHandler برای نمایش اخطار مثلا «یکبار بیشتر مجاز نیستید به ازای یک مطلب رای دهید»، می‌توان استفاده کرد.

بنابراین تا اینجا فایل layout برنامه تقریبا چنین مداخلی را خواهد داشت:
<head>
    <title>@ViewBag.Title</title>    
    <link href="@Url.Content("Content/starRating.css")" rel="stylesheet" type="text/css" />
    <link href="@Url.Content("Content/Site.css")" rel="stylesheet" type="text/css" />
    <script src="@Url.Content("~/Scripts/jquery-1.9.1.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.StarRating.js")" type="text/javascript"></script>
    @RenderSection("JavaScript", required: false)
</head>

طراحی یک HTML helper برای نمایش ستاره‌های امتیاز دهی

ابتدا پوشه استاندارد app_code را به پروژه اضافه کرده و سپس فایلی را به نام StarRatingHelper.cshtml، با محتوای ذیل به آن اضافه نمائید:
@using System.Globalization
@helper AddStarRating(int postId,
                      double? average = 0, int? postRatingsCount = 0, string type = "BlogPost",
                      string tooltip = "لطفا جهت رای دادن کلیک نمائید")
    {
        string actIt = "active ";
        if (!average.HasValue) { average = 0; }
        if (!postRatingsCount.HasValue) { postRatingsCount = 0; }
    
    <span class='postRating' rating='@average' post='@postId' title='@tooltip' sectiontype='@type'>
        @for (double i = .5; i <= 5.0; i = i + .5)
        {
            string left;
            if (i <= average)
            {
                left = (i * 2) % 2 == 1 ? "left_on" : "right_on";
            }
            else
            {
                left = (i * 2) % 2 == 1 ? "left_off" : "right_off";
            }
            <span class='rating stars @(actIt)star-@left' value='@i'></span>
        }
        &nbsp;
        @if (postRatingsCount > 0)
        {
            var ratingInfo = string.Format(CultureInfo.InvariantCulture, "امتیاز {0:0.00} از 5 توسط {1} نفر", average, postRatingsCount);
            <span>@ratingInfo</span>                
        }
        else
        {
            <span></span>
        }
    </span>
}
از این Html helper برای تشکیل ساختار نمایش قطعه امتیاز دهی به یک مطلب استفاده خواهیم کرد که توسط افزونه جی‌کوئری فوق ردیابی می‌شود.


کنترلر ذخیره سازی اطلاعات دریافتی برنامه

using System.Web.Mvc;
using System.Web.UI;
using jQueryMvcSample03.DataSource;
using jQueryMvcSample03.Security;

namespace jQueryMvcSample03.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            var postsList = BlogPostDataSource.GetLatestBlogPosts(pageNumber: 0);
            return View(postsList); //نمایش صفحه اصلی
        }


        [HttpPost]
        [AjaxOnly]
        [OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
        public ActionResult SaveRatings(int? postId, double? rating, string sectionType)
        {
            if (postId == null || rating == null || string.IsNullOrWhiteSpace(sectionType))
                return Content(null); //اعلام بروز خطا

            if (!this.HttpContext.CanUserVoteBasedOnCookies(postId.Value, sectionType))
                return Content("nok"); //اعلام فقط یکبار مجاز هستید رای دهید

            switch (sectionType) //قسمت‌های مختلف سایت که در جداول مختلفی قرار دارند نیز می‌توانند گزینه امتیاز دهی داشته باشند
            {
                case "BlogPost":
                    //الان شماره مطلب و رای ارسالی را داریم که می‌توان نسبت به ذخیره آن اقدام کرد
                    //مثلا
                    //_blogPostsService.SaveRating(postId.Value, rating.Value);
                    break;

                //... سایر قسمت‌های دیگر سایت

                default:
                    return Content(null); //اعلام بروز خطا
            }

            return Content("ok"); //اعلام موفقیت آمیز بودن ثبت اطلاعات
        }

        [HttpGet]
        public ActionResult Post(int? id)
        {
            if (id == null)
                return Redirect("/");

            //todo: show the content here
            return Content("Post " + id.Value);
        }
    }
}
در اینجا کنترلری را که کار پردازش کلیک کاربر را بر روی امتیازی خاص انجام می‌دهد، ملاحظه می‌کنید.
امضای اکشن متد SaveRatings دقیقا بر اساس سه پارامتر ارسالی توسط jquery.StarRating.js که پیشتر توضیح داده شد، تعیین گردیده است. در این متد ابتدا بررسی می‌شود که آیا اطلاعاتی دریافت شده است یا خیر. اگر خیر، null را بازگشت خواهد داد. سپس توسط متد CanUserVoteBasedOnCookies بررسی می‌شود که آیا کاربر می‌تواند (خصوصا مجددا) رای دهد یا خیر. این افزونه برای رای دهی کاربران وارد نشده به سیستم نیز مناسب است. به همین جهت از کوکی‌ها برای ثبت اطلاعات رای دادن کاربران استفاده گردیده است. پیاده سازی متد CanUserVoteBasedOnCookies را در ادامه ملاحظه خواهید نمود.
در ادامه در متد SaveRatings، یک switch تشکیل شده است تا بر اساس نام قسمت مرتبط به رای گیری، اطلاعات را بتوان به سرویس خاصی در برنامه هدایت کرد. مثلا اطلاعات قسمت مطالب به سرویس مطالب و قسمت نظرات به سرویس نظرات هدایت شوند.


متدهایی برای کار با کوکی‌ها در ASP.NET MVC

using System;
using System.Web;

namespace jQueryMvcSample03.Security
{
    public static class CookieHelper
    {
        public static bool CanUserVoteBasedOnCookies(this HttpContextBase httpContext, int postId, string sectionType)
        {
            string key = sectionType + "-" + postId;
            var value = httpContext.GetCookieValue(key);
            if (string.IsNullOrWhiteSpace(value))
            {
                httpContext.AddCookie(key, key);
                return true;
            }
            return false;
        }

        public static void AddCookie(this HttpContextBase httpContextBase, string cookieName, string value)
        {
            httpContextBase.AddCookie(cookieName, value, DateTime.Now.AddDays(30));
        }

        public static void AddCookie(this HttpContextBase httpContextBase, string cookieName, string value, DateTime expires)
        {
            var cookie = new HttpCookie(cookieName)
            {
                Expires = expires,
                Value = httpContextBase.Server.UrlEncode(value) // For Cookies and Unicode characters
            };
            httpContextBase.Response.Cookies.Add(cookie);
        }

        public static string GetCookieValue(this HttpContextBase httpContext, string cookieName)
        {
            var cookie = httpContext.Request.Cookies[cookieName];
            if (cookie == null)
                return string.Empty; //cookie doesn't exist

            // For Cookies and Unicode characters
            return httpContext.Server.UrlDecode(cookie.Value);
        }
    }
}
در اینجا یک سری متد الحاقی را ملاحظه می‌کنید که برای ثبت اطلاعات رای داده شده یک کاربر بر اساس Id مطلب و نام قسمت متناظر با آن در یک کوکی طراحی شده‌اند. بدیهی است اگر تمام قسمت‌های برنامه شما محافظت شده هستند و کاربران حتما نیاز است ابتدا به سیستم لاگین نمایند، می‌توانید این قسمت را حذف کرده و اطلاعات postId و SectionType را به ازای هر کاربر، جداگانه در بانک اطلاعاتی ثبت و بازیابی نمائید (دقیق‌ترین حالت ممکن؛ البته برای سیستمی بسته که حتما تمام قسمت‌های آن نیاز به اعتبار سنجی دارند).


پیشنهادی در مورد نحوه ذخیره سازی اطلاعات دریافتی

using jQueryMvcSample03.Models;

namespace jQueryMvcSample03.DataSource
{
    public interface IBlogPostsService
    {
        void SaveRating(int postId, double rating);
    }

    public class SampleService : IBlogPostsService
    {
        /// <summary>
        /// یک نمونه از متد ذخیره سازی اطلاعات پیشنهادی
        /// فقط برای ایده گرفتن
        /// بدیهی است محل قرارگیری اصلی آن در لایه سرویس برنامه شما خواهد بود
        /// </summary>
        public void SaveRating(int postId, double rating)
        {
            BlogPost post = null;
            //post = _blogCtx.Find(postId); // بر اساس شماره مطلب، مطلب یافت شده و فیلدهای آن تنظیم می‌شوند
            if (post == null) return;

            if (!post.Rating.TotalRaters.HasValue) post.Rating.TotalRaters = 0;
            if (!post.Rating.TotalRating.HasValue) post.Rating.TotalRating = 0;
            if (!post.Rating.AverageRating.HasValue) post.Rating.AverageRating = 0;

            post.Rating.TotalRaters++;
            post.Rating.TotalRating += rating;
            post.Rating.AverageRating = post.Rating.TotalRating / post.Rating.TotalRaters;

            // todo: call save changes at the end.
        }
    }
}
همانطور که عنوان شد، سه داده Id مطلب، رای داده شده و نام قسمت متناظر به اکشن متد ارسال می‌شود. از نام قسمت، برای انتخاب سرویس ذخیره سازی اطلاعات استفاده خواهیم کرد. این سرویس می‌تواند شامل متدی به نام SaveRating، همانند کدهای فوق باشد که Id مطلب و عدد رای حاصل به آن ارسال می‌گردند. ابتدا بر اساس این Id، مطلب متناظر یافت شده و سپس اطلاعات Rating آن به روز خواهد شد. در پایان هم ذخیره سازی اطلاعات باید صورت گیرد.



Viewهای برنامه

قسمت پایانی کار ما در اینجا تهیه دو View است:
الف) یک Partial view که لیست مطالب را به همراه گزینه رای دهی به آن‌ها رندر می‌کند.
ب) View کاملی که از این Partial View استفاده کرده و همچنین افزونه jquery.StarRating.js را فراخوانی می‌کند.
@using System.Text.RegularExpressions
@model IList<jQueryMvcSample03.Models.BlogPost>
<ul>
    @foreach (var item in Model)
    {
        <li>
            <fieldset>
            <legend>مطلب @item.Id</legend>
                <h5>
                    @Html.ActionLink(linkText: item.Title,
                                 actionName: "Post",
                                 controllerName: "Home",
                                 routeValues: new { id = item.Id },
                                 htmlAttributes: null)
                </h5>
                @item.Body
                <div class="post_rating">
                    @Html.Raw(Regex.Replace(@StarRatingHelper.AddStarRating(item.Id, item.Rating.AverageRating, item.Rating.TotalRaters, "BlogPost").ToHtmlString(), @">\s+<", "><"))
                </div>
            </fieldset>
        </li>
    }
</ul>
کدهای _ItemsList.cshtml را در اینجا ملاحظه می‌کند که در آن نحوه فراخوانی متد کمکی StarRatingHelper.AddStarRating ذکر شده است.
اگر به کدهای آن دقت کنید از Regex.Replace برای حذف فاصله‌های خالی و خطوط جدید بین تگ‌ها استفاده گردیده است. اگر اینکار انجام نشود، نیمه‌های ستاره‌های نمایش داده شده، با فاصله از یکدیگر رندر می‌شوند که صورت خوشایندی ندارد.

و نهایتا View ایی که از این اطلاعات استفاده می‌کنید ساختار زیر را خواهد داشت:
@model IList<jQueryMvcSample03.Models.BlogPost>
@{
    ViewBag.Title = "Index";
    var postInfoUrl = Url.Action(actionName: "SaveRatings", controllerName: "Home");
}
<h2>
    سیستم امتیاز دهی</h2>
@{ Html.RenderPartial("_ItemsList", Model); }
@section JavaScript
{
    <script type="text/javascript">
        $(document).ready(function () {
            $(".rating.stars.active").StarRating({
                ratingStarsSpan: '.rating.stars',
                postInfoUrl: '@postInfoUrl',
                loginUrl: '/login',
                errorHandler: function () {
                    alert('خطایی رخ داده است');
                },
                completeHandler: function () {
                    alert('با تشکر! رای شما با موفقیت ثبت شد');
                },
                onlyOneTimeHandler: function () {
                    alert('فقط یکبار می‌توانید به ازای هر مطلب رای دهید');
                }
            });
        });
    </script>
}
در این View لیستی از مطالب دریافت و به partial view طراحی شده برای نمایش ارسال می‌شود. سپس افزونه StarRating نیز تنظیم و به صفحه اضافه خواهد گردید. نکته مهم آن تعیین صحیح اکشن متدی است که قرار است اطلاعات را دریافت کند و نحوه مقدار دهی آن‌را توسط متغیر postInfoUrl مشاهده می‌کنید.

دریافت کدها و پروژه کامل این قسمت
jQueryMvcSample03.zip
مطالب
چندین Submit در یک Html Form و انتساب Action های مجزا به هر یک از Submit ها در MVC
تا به حال به این نکته برخورد کردید که برای یک فرم Html نیاز به چندین Submit داشته باشید که هر کدوم یک Action مجزا داشته باشن و یک کار متفاوت انجام بدن ؟
برای مثال فرمی داریم که داده‌های وارد شده در ان باید به دو صورت برای یک کاربر ارسال بشن یا از طریق پیامک یا از طریق ایمیل (این فقط یک مثال پیش فرض هست) و .... در حالت عادی ما در یک فرم نمیتونیم دو عدد Submit  داشته باشیم که هر کدوم به یک Action جدا بسط داده بشه خب راه حل چیه ؟ شاید با خودتون بگید خب دو input از نوع radio قرار میدیم و در یک اکشن کنترل میکنیم که کدوم یکی انتخاب شده و عملیات رو با اون معیار انجام میدیم ... به نظرتون زیباتر نیست برای هر عملیات که ممکن باشه هر کدوم کاملا روال کاری متفاوتی داشته باشه یک Action وجود داشته باشه ؟ در این صورت خوانایی کد خیلی بالاتر میره و Unit Test هر Action کاملا مشخص هست که قراره چه فرایندی رو مورد تست قرار بده و مجبور نیستیم چندین حالت رو با عبارات شرطی از هم جدا کنیم و همه چی قاطی بشه با هم ... من در کل با امکاناتی که C# و MVC در اختیارم قرار میده حاظر نیستم تن به کد نویسی به صورت کلاسیک و قاطی پاتی بدم سعی میکنم با مطالعه‌ی سورس MVC بهترین حالت رو انتخاب کنم شما چطور ؟ معلومه که همه همینو میخوان پس بریم سر اصل مطلب .
قطعه کد Html و Razor ساده‌ی زیر رو در نظر بگیرید برای View :
@model Models.MyModel
@{
    Layout = null;
}
<!DOCTYPE html>
<html>
<head>
    <title>ViewPage1</title>
</head>
<body>
    <div>
        @using (Html.BeginForm("SendMessage", "Home", FormMethod.Post))
        {

            @Html.LabelFor(x => x.Name); 
            @Html.TextBoxFor(x => x.Name);

            <input type="submit" value="ارسال توسط پیامک" name="Send_sms" />
            <input type="submit" value="ارسال توسط ایمیل" name="Send_email" />
        }
    </div>
</body>
</html>

خب ما دو تا Submit داریم . یکم اگه شیطنت کنید و مقادیر ارسال شده بعد از submit این فرم رو توسط ابزارهای مانیتورینگ بررسی کنید میبینید که روی هر کدوم از Submit‌ها که کلیک میشه داده ای با نام اون که در خاصیت name اون و مقدار موجود در value اون همراه اون فرم به سرور ارسال میشه و اون یکی Submit از این اتفاق بی نصیب میمونه ... خب ما هم استفاده‌ی لازم رو از این موضوع شیرین میبریم و با یک تکنیک تهاجمی از این موضوع برای رسیدن به هدفمون استفاده میکنیم .

این هم کلاس Model ماست :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel;

namespace Models
{
    public class MyModel
    {
        [DisplayName("نام خود را وارد کنید :")]
        public string Name { get; set; }
    }
}
 و اما یک نکته‌ی دیگه . توجه داشته باشید که ما در قسمت View نام Action رو در فرم, SendMessage مشخص کردیم . ولی ... اصلا در واقع همچین اکشنی وجود نداره ! پس چرا ما همچین نامی رو واسه اکشن فرم گذاشتیم !؟
دلیل اینه که ما قصد داریم با یک ActionNameSelectorAttribute درخواست کاربر رو شکار کنیم و اون رو به اکشن دلخواه ارجاع بدیم ... جالبه نه ؟ ولی چه جوری ... کلاس زیر رو بهش دقت مضاعف کنید و در پروژتون ایجادش کنید :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Reflection;

namespace ActionHandlers
{
    public class SendMessageHandlerAttribute : ActionNameSelectorAttribute
    {
        public override bool IsValidName(ControllerContext controllerContext, string actionName, MethodInfo methodInfo)
        {
            if (actionName.Equals(methodInfo.Name, StringComparison.InvariantCultureIgnoreCase))
                return true;

            if (!actionName.Equals("SendMessage", StringComparison.InvariantCultureIgnoreCase))
                return false;

            var request = controllerContext.RequestContext.HttpContext.Request;
            return request[methodInfo.Name] != null;
        }
    }
}
 خب حالا بخش Controller رو بهش دقت کنید که ما در اون دو اکشن رو با نام هایی که برای هر Submit مشخص کردیم مینویسیم و ActionNameSelectorAttribute نوشته شده رو به اونها بسط میدیم. 
    [SendMessageHandler ]
        [HttpPost]
        public ContentResult Send_sms(MyModel mdl)
        {
            /// Do something ...
           return string.Empty ;
        }

        [SendMessageHandler ]
        [HttpPost]
        public ContentResult Send_email(MyModel mdl)
        {
           /// Do something ...
          return  string.Empty; 
        }
 خب حالا بعد از کلیک بر روی هر Submit اکشن متناظر با اون اجرا میشه . بعد از ارسال درخواست به سرور MVC در بین اکشن‌های موجود در Controller مشخص شده به دنبال اکشن معین شده میگرده و وقتی به اکشن‌های ما میرسه میبینه عجب ! اون دوتا ActionNameSelectorAttribute سفارشی دارن پس میره ببینه چه خبره اونجا که ما با یک حرکت تهاجمی بررسی میکنیم که اگه  نام اکشن مشخص شده در فرم با نام اکشن در حال بررسی مساوی بود که همینو اجرا کن ( یعنی ما میتونی اکشنی با نام SendMessage هم داشته باشیم ) . اگه نام اکشن مشخص شده در فرم اون نامی نبود که ما میخوایم که کلا بیخیال هندل کردن اکشن میشیم میزاریم خود MVC تصمیم بگیره . و در اخر بررسی میکنیم که ایا در درخواست جاری مقداری با نام اکشن در حال بررسی وجود داره !؟ اگه داشت یعنی همون Submit که ما میخوایم وصل بشه به این اکشن کلیک شده پس اکشن در حال بررسی رو بسط میدیم به درخواست ارسال شده ... به همین سادگی ...

پیروز و موفق باشید . 
مطالب
EF Code First #3

بررسی تعاریف نگاشت‌ها به کمک متادیتا در EF Code first

در قسمت قبل مروری سطحی داشتیم بر امکانات مهیای جهت تعاریف نگاشت‌ها در EF Code first. در این قسمت، حالت استفاده از متادیتا یا همان data annotations را با جزئیات بیشتری بررسی خواهیم کرد.
برای این منظور پروژه کنسول جدیدی را آغاز نمائید. همچنین به کمک NuGet، ارجاعات لازم را به اسمبلی EF، اضافه کنید. در ادامه مدل‌های زیر را به پروژه اضافه نمائید؛ یک شخص که تعدادی پروژه منتسب می‌تواند داشته باشد:

using System;
using System.Collections.Generic;

namespace EF_Sample02.Models
{
public class User
{
public int Id { set; get; }
public DateTime AddDate { set; get; }
public string Name { set; get; }
public string LastName { set; get; }
public string Email { set; get; }
public string Description { set; get; }
public byte[] Photo { set; get; }
public IList<Project> Projects { set; get; }
}
}

using System;

namespace EF_Sample02.Models
{
public class Project
{
public int Id { set; get; }
public DateTime AddDate { set; get; }
public string Title { set; get; }
public string Description { set; get; }
public virtual User User { set; get; }
}
}

به خاصیت public virtual User User در کلاس Project اصطلاحا Navigation property هم گفته می‌شود.
دو کلاس زیر را نیز جهت تعریف کلاس Context که بیانگر کلاس‌های شرکت کننده در تشکیل بانک اطلاعاتی هستند و همچنین کلاس آغاز کننده بانک اطلاعاتی سفارشی را به همراه تعدادی رکورد پیش فرض مشخص می‌کنند، به پروژه اضافه نمائید.

using System;
using System.Collections.Generic;
using System.Data.Entity;
using EF_Sample02.Models;

namespace EF_Sample02
{
public class Sample2Context : DbContext
{
public DbSet<User> Users { set; get; }
public DbSet<Project> Projects { set; get; }
}

public class Sample2DbInitializer : DropCreateDatabaseAlways<Sample2Context>
{
protected override void Seed(Sample2Context context)
{
context.Users.Add(new User
{
AddDate = DateTime.Now,
Name = "Vahid",
LastName = "N.",
Email = "name@site.com",
Description = "-",
Projects = new List<Project>
{
new Project
{
Title = "Project 1",
AddDate = DateTime.Now.AddDays(-10),
Description = "..."
}
}
});

base.Seed(context);
}
}
}

به علاوه در فایل کانفیگ برنامه، تنظیمات رشته اتصالی را نیز اضافه نمائید:

<connectionStrings>
<add
name="Sample2Context"
connectionString="Data Source=(local);Initial Catalog=testdb2012;Integrated Security = true"
providerName="System.Data.SqlClient"
/>
</connectionStrings>

همانطور که ملاحظه می‌کنید، در اینجا name به نام کلاس مشتق شده از DbContext اشاره می‌کند (یکی از قراردادهای توکار EF Code first است).

یک نکته:
مرسوم است کلاس‌های مدل را در یک class library جداگانه اضافه کنند به نام DomainClasses و کلاس‌های مرتبط با DbContext را در پروژه class library دیگری به نام DataLayer. هیچکدام از این پروژه‌ها نیازی به فایل کانفیگ و تنظیمات رشته اتصالی ندارند؛ زیرا اطلاعات لازم را از فایل کانفیگ پروژه اصلی که این دو پروژه class library را به خود الحاق کرده، دریافت می‌کنند. دو پروژه class library اضافه شده تنها باید ارجاعاتی را به اسمبلی‌های EF و data annotations داشته باشند.

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

using System;
using System.Data.Entity;

namespace EF_Sample02
{
class Program
{
static void Main(string[] args)
{
Database.SetInitializer(new Sample2DbInitializer());
using (var db = new Sample2Context())
{
var project1 = db.Projects.Find(1);
Console.WriteLine(project1.Title);
}
}
}
}

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

CREATE TABLE [dbo].[Users](
[Id] [int] IDENTITY(1,1) NOT NULL,
[AddDate] [datetime] NOT NULL,
[Name] [nvarchar](max) NULL,
[LastName] [nvarchar](max) NULL,
[Email] [nvarchar](max) NULL,
[Description] [nvarchar](max) NULL,
[Photo] [varbinary](max) NULL,
CONSTRAINT [PK_Users] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF,
IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]


CREATE TABLE [dbo].[Projects](
[Id] [int] IDENTITY(1,1) NOT NULL,
[AddDate] [datetime] NOT NULL,
[Title] [nvarchar](max) NULL,
[Description] [nvarchar](max) NULL,
[User_Id] [int] NULL,
CONSTRAINT [PK_Projects] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF,
IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

ALTER TABLE [dbo].[Projects] WITH CHECK ADD CONSTRAINT [FK_Projects_Users_User_Id] FOREIGN KEY([User_Id])
REFERENCES [dbo].[Users] ([Id])
GO

ALTER TABLE [dbo].[Projects] CHECK CONSTRAINT [FK_Projects_Users_User_Id]
GO

توضیحاتی در مورد ساختار فوق، جهت یادآوری مباحث دو قسمت قبل:
- خواصی با نام Id تبدیل به primary key و identity field شده‌اند.
- نام جداول، همان نام خواص تعریف شده در کلاس Context است.
- تمام رشته‌ها به nvarchar از نوع max نگاشت شده‌اند و null پذیر می‌باشند.
- خاصیت تصویر که با آرایه‌ای از بایت‌ها تعریف شده به varbinary از نوع max نگاشت شده است.
- بر اساس ارتباط بین کلاس‌ها فیلد User_Id در جدول Projects اضافه شده است که توسط قیدی به نام FK_Projects_Users_User_Id، جهت تعریف کلید خارجی عمل می‌کند. این نام گذاری پیش فرض هم بر اساس نام خواص در دو کلاس انجام می‌شود.
- schema پیش فرض بکارگرفته شده، dbo است.
- null پذیری پیش فرض فیلدها بر اساس اصول زبان مورد استفاده تعیین شده است. برای مثال در سی شارپ، نوع int نال پذیر نیست یا نوع DateTime نیز به همین ترتیب یک value type است. بنابراین در اینجا این دو نوع به صورت not null تعریف شده‌اند (صرفنظر از اینکه در SQL Server هر دو نوع یاد شده، null پذیر هم می‌توانند باشند). بدیهی است امکان تعریف nullable types نیز وجود دارد.


مروری بر انواع متادیتای قابل استفاده در EF Code first

1) Key
همانطور که ملاحظه کردید اگر نام خاصیتی Id یا ClassName+Id باشد، به صورت خودکار به عنوان primary key جدول، مورد استفاده قرار خواهد گرفت. این یک قرارداد توکار است.
اگر یک چنین خاصیتی با نام‌های ذکر شده در کلاس وجود نداشته باشد، می‌توان با مزین سازی خاصیتی مفروض با ویژگی Key که در فضای نام System.ComponentModel.DataAnnotations قرار دارد، آن‌را به عنوان Primary key معرفی نمود. برای مثال:

public class Project
{
[Key]
public int ThisIsMyPrimaryKey { set; get; }

و ضمنا باید دقت داشت که حین کار با ORMs فرقی نمی‌کند EF باشد یا سایر فریم ورک‌های دیگر، داشتن یک key جهت عملکرد صحیح فریم ورک، ضروری است. بر اساس یک Key است که Entity معنا پیدا می‌کند.


2) Required
ویژگی Required که در فضای نام System.ComponentModel.DataAnnotations تعریف شده است، سبب خواهد شد یک خاصیت به صورت not null در بانک اطلاعاتی تعریف شود. همچنین در مباحث اعتبارسنجی برنامه، پیش از ارسال اطلاعات به سرور نیز نقش خواهد داشت. در صورت نال بودن خاصیتی که با ویژگی Required مزین شده است، یک استثنای اعتبارسنجی پیش از ذخیره سازی اطلاعات در بانک اطلاعاتی صادر می‌گردد. این ویژگی علاوه بر EF Code first در ASP.NET MVC نیز به نحو یکسانی تاثیرگذار است.


3) MaxLength و MinLength
این دو ویژگی نیز در فضای نام System.ComponentModel.DataAnnotations قرار دارند (اما در اسمبلی EntityFramework.dll تعریف شده‌اند و جزو اسمبلی‌ پایه System.ComponentModel.DataAnnotations.dll نیستند). در ذیل نمونه‌ای از تعریف این‌ها را مشاهده می‌کنید. همچنین باید درنظر داشت که روش دیگر تعریف متادیتا، ترکیب آن‌ها در یک سطر نیز می‌باشد. یعنی الزامی ندارد در هر سطر یک متادیتا را تعریف کرد:

[MaxLength(50, ErrorMessage = "حداکثر 50 حرف"), MinLength(4, ErrorMessage = "حداقل 4 حرف")]
public string Title { set; get; }

ویژگی MaxLength بر روی طول فیلد تعریف شده در بانک اطلاعاتی تاثیر دارد. برای مثال در اینجا فیلد Title از نوع nvarchar با طول 30 تعریف خواهد شد.
ویژگی MinLength در بانک اطلاعاتی معنایی ندارد.
هر دوی این ویژگی‌ها در پروسه اعتبار سنجی اطلاعات مدل دریافتی تاثیر دارند. برای مثال در اینجا اگر طول عنوان کمتر از 4 حرف باشد، یک استثنای اعتبارسنجی صادر خواهد شد.

ویژگی دیگری نیز به نام StringLength وجود دارد که جهت تعیین حداکثر طول رشته‌ها به کار می‌رود. این ویژگی سازگاری بیشتر با ASP.NET MVC‌ دارد از این جهت که Client side validation آن‌را نیز فعال می‌کند.


4) Table و Column
این دو ویژگی نیز در فضای نام System.ComponentModel.DataAnnotations قرار دارند، اما در اسمبلی EntityFramework.dll تعریف شده‌اند. بنابراین اگر تعاریف مدل‌های شما در پروژه Class library جداگانه‌ای قراردارند، نیاز خواهد بود تا ارجاعی را به اسمبلی EntityFramework.dll نیز داشته باشند.
اگر از نام پیش فرض جداول تشکیل شده خرسند نیستید، ویژگی Table را بر روی یک کلاس قرار داده و نام دیگری را تعریف کنید. همچنین اگر Schema کاربری رشته اتصالی به بانک اطلاعاتی شما dbo نیست، باید آن‌را در اینجا صریحا ذکر کنید تا کوئری‌های تشکیل شده به درستی بر روی بانک اطلاعاتی اجرا گردند:

[Table("tblProject", Schema="guest")]
public class Project

توسط ویژگی Column سه خاصیت یک فیلد بانک اطلاعاتی را می‌توان تعیین کرد:

[Column("DateStarted", Order = 4, TypeName = "date")]
public DateTime AddDate { set; get; }

به صورت پیش فرض، خاصیت فوق با همین نام AddDate در بانک اطلاعاتی ظاهر می‌گردد. اگر برای مثال قرار است از یک بانک اطلاعاتی قدیمی استفاده شود یا قرار نیست از شیوه نامگذاری خواص در سی شارپ در یک بانک اطلاعاتی پیروی شود، توسط ویژگی Column می‌توان این تعاریف را سفارشی نمود.
توسط پارامتر Order آن که از صفر شروع می‌شود، ترتیب قرارگیری فیلدها در حین تشکیل یک جدول مشخص می‌گردد.
اگر نیاز است نوع فیلد تشکیل شده را نیز سفارشی سازی نمائید، می‌توان از پارامتر TypeName استفاده کرد. برای مثال در اینجا علاقمندیم از نوع date مهیا در SQL Server 2008 استفاده کنیم و نه از نوع datetime پیش فرض آن.

نکته‌ای در مورد Order:
Order پیش فرض تمام خواصی که قرار است به بانک اطلاعاتی نگاشت شوند، به int.MaxValue تنظیم شده‌اند. به این معنا که تنظیم فوق با Order=4 سبب خواهد شد تا این فیلد، پیش از تمام فیلدهای دیگر قرار گیرد. بنابراین نیاز است Order اولین خاصیت تعریف شده را به صفر تنظیم نمود. (البته اگر واقعا نیاز به تنظیم دستی Order داشتید)


نکاتی در مورد تنظیمات ارث بری در حالت استفاده از متادیتا:
حداقل سه حالت ارث بری را در EF code first می‌توان تعریف و مدیریت کرد:
الف) Table per Hierarchy - TPH
حالت پیش فرض است. نیازی به هیچگونه تنظیمی ندارد. معنای آن این است که «لطفا تمام اطلاعات کلاس‌هایی را که از هم ارث بری کرده‌اند در یک جدول بانک اطلاعاتی قرار بده». فرض کنید یک کلاس پایه شخص را دارید که کلاس‌های بازیکن و مربی از آن ارث بری می‌کنند. زمانیکه کلاس پایه شخص توسط DbSet در کلاس مشتق شده از DbContext در معرض استفاده EF قرار می‌گیرد، بدون نیاز به هیچ تنظیمی، تمام این سه کلاس، تبدیل به یک جدول شخص در بانک اطلاعاتی خواهند شد. یعنی یک table به ازای سلسله مراتبی (Hierarchy) که تعریف شده.
ب) Table per Type - TPT
به این معنا است که به ازای هر نوع، باید یک جدول تشکیل شود. به عبارتی در مثال قبل، یک جدول برای شخص، یک جدول برای مربی و یک جدول برای بازیکن تشکیل خواهد شد. دو جدول مربی و بازیکن با یک کلید خارجی به جدول شخص مرتبط می‌شوند. تنها تنظیمی که در اینجا نیاز است، قرار دادن ویژگی Table بر روی نام کلاس‌های بازیکن و مربی است. به این ترتیب حالت پیش فرض الف (TPH) اعمال نخواهد شد.
ج) Table per Concrete Type - TPC
در این حالت فقط دو جدول برای بازیکن و مربی تشکیل می‌شوند و جدولی برای شخص تشکیل نخواهد شد. خواص کلاس شخص، در هر دو جدول مربی و بازیکن به صورت جداگانه‌ای تکرار خواهد شد. تنظیم این مورد نیاز به استفاده از Fluent API دارد.

توضیحات بیشتر این موارد به همراه مثال، موکول خواهد شد به مباحث استفاده از Fluent API که برای تعریف تنظیمات پیشرفته نگاشت‌ها طراحی شده است. استفاده از متادیتا تنها قسمت کوچکی از توانایی‌های Fluent API را شامل می‌شود.



5) ConcurrencyCheck و Timestamp
هر دوی این ویژگی‌ها در فضای نام System.ComponentModel.DataAnnotations و اسمبلی به همین نام تعریف شده‌اند.
در EF Code first دو راه برای مدیریت مسایل همزمانی وجود دارد:
[ConcurrencyCheck]
public string Name { set; get; }

[Timestamp]
public byte[] RowVersion { set; get; }

زمانیکه از ویژگی ConcurrencyCheck استفاده می‌شود، تغییر خاصی در سمت بانک اطلاعاتی صورت نخواهد گرفت، اما در برنامه، کوئری‌های update و delete ایی که توسط EF صادر می‌شوند، اینبار اندکی متفاوت خواهند بود. برای مثال برنامه جاری را به نحو زیر تغییر دهید:

using System;
using System.Data.Entity;

namespace EF_Sample02
{
class Program
{
static void Main(string[] args)
{
Database.SetInitializer(new Sample2DbInitializer());
using (var db = new Sample2Context())
{
//update
var user = db.Users.Find(1);
user.Name = "User name 1";
db.SaveChanges();
}
}
}
}

متد Find بر اساس primary key عمل می‌کند. به این ترتیب، اول رکورد یافت شده و سپس نام آن‌ تغییر کرده و در ادامه، اطلاعات ذخیره خواهند شد.
اکنون اگر توسط SQL Server Profiler کوئری update حاصل را بررسی کنیم، به نحو زیر خواهد بود:

exec sp_executesql N'update [dbo].[Users]
set [Name] = @0
where (([Id] = @1) and ([Name] = @2))
',N'@0 nvarchar(max) ,@1 int,@2 nvarchar(max) ',@0=N'User name 1',@1=1,@2=N'Vahid'

همانطور که ملاحظه می‌کنید، برای به روز رسانی فقط از primary key جهت یافتن رکورد استفاده نکرده، بلکه فیلد Name را نیز دخالت داده است. از این جهت که مطمئن شود در این بین، رکوردی که در حال به روز رسانی آن هستیم، توسط کاربر دیگری در شبکه تغییر نکرده باشد و اگر در این بین تغییری رخ داده باشد، یک استثناء صادر خواهد شد.
همین رفتار در مورد delete نیز وجود دارد:
//delete
var user = db.Users.Find(1);
db.Users.Remove(user);
db.SaveChanges();
که خروجی آن به صورت زیر است:

exec sp_executesql N'delete [dbo].[Users]
where (([Id] = @0) and ([Name] = @1))',N'@0 int,@1 nvarchar(max) ',@0=1,@1=N'Vahid'

در اینجا نیز به علت مزین بودن خاصیت Name به ویژگی ConcurrencyCheck، فقط همان رکوردی که یافت شده باید حذف شود و نه نمونه تغییر یافته آن توسط کاربری دیگر در شبکه.
البته در این مثال شاید این پروسه تنها چند میلی ثانیه به نظر برسد. اما در برنامه‌ای با رابط کاربری، شخصی ممکن است اطلاعات یک رکورد را در یک صفحه دریافت کرده و 5 دقیقه بعد بر روی دکمه save کلیک کند. در این بین ممکن است شخص دیگری در شبکه همین رکورد را تغییر داده باشد. بنابراین اطلاعاتی را که شخص مشاهده می‌کند، فاقد اعتبار شده‌اند.

ConcurrencyCheck را بر روی هر فیلدی می‌توان بکاربرد، اما ویژگی Timestamp کاربرد مشخص و محدودی دارد. باید به خاصیتی از نوع byte array اعمال شود (که نمونه‌ای از آن‌را در بالا در خاصیت public byte[] RowVersion مشاهده نمودید). علاوه بر آن، این ویژگی بر روی بانک اطلاعاتی نیز تاثیر دارد (نوع فیلد را در SQL Server تبدیل به timestamp می‌کند و نه از نوع varbinary مانند فیلد تصویر). SQL Server با این نوع فیلد به خوبی آشنا است و قابلیت مقدار دهی خودکار آن‌را دارد. بنابراین نیازی نیست در حین تشکیل اشیاء در برنامه، قید شود.
پس از آن، این فیلد مقدار دهی شده به صورت خودکار توسط بانک اطلاعاتی، در تمام updateها و deleteهای EF Code first حضور خواهد داشت:

exec sp_executesql N'delete [dbo].[Users]
where ((([Id] = @0) and ([Name] = @1)) and ([RowVersion] = @2))',N'@0 int,@1 nvarchar(max) ,
@2 binary(8)',@0=1,@1=N'Vahid',@2=0x00000000000007D1

از این جهت که اطمینان حاصل شود، واقعا مشغول به روز رسانی یا حذف رکوردی هستیم که در ابتدای عملیات از بانک اطلاعاتی دریافت کرده‌ایم. اگر در این بین RowVesrion تغییر کرده باشد، یعنی کاربر دیگری در شبکه این رکورد را تغییر داده و ما در حال حاضر مشغول به کار با رکوردی غیرمعتبر هستیم.
بنابراین استفاده از Timestamp را می‌توان به عنوان یکی از best practices طراحی برنامه‌های چند کاربره ASP.NET درنظر داشت.


6) NotMapped و DatabaseGenerated
این دو ویژگی نیز در فضای نام System.ComponentModel.DataAnnotations قرار دارند، اما در اسمبلی EntityFramework.dll تعریف شده‌اند.
به کمک ویژگی DatabaseGenerated، مشخص خواهیم کرد که این فیلد قرار است توسط بانک اطلاعاتی تولید شود. برای مثال خواصی از نوع public int Id به صورت خودکار به فیلدهایی از نوع identity که توسط بانک اطلاعاتی تولید می‌شوند، نگاشت خواهند شد و نیازی نیست تا به صورت صریح از ویژگی DatabaseGenerated جهت مزین سازی آن‌ها کمک گرفت. البته اگر علاقمند نیستید که primary key شما از نوع identity باشد، می‌توانید از گزینه DatabaseGeneratedOption.None استفاده نمائید:
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Id { set; get; }

DatabaseGeneratedOption در اینجا یک enum است که به نحو زیر تعریف شده است:

public enum DatabaseGeneratedOption
{
None = 0,
Identity = 1,
Computed = 2
}

تا اینجا حالت‌های None و Identity آن، بحث شدند.
در SQL Server امکان تعریف فیلدهای محاسباتی و Computed با T-SQL نویسی نیز وجود دارد. این نوع فیلدها در هربار insert یا update یک رکورد، به صورت خودکار توسط بانک اطلاعاتی مقدار دهی می‌شوند. بنابراین اگر قرار است خاصیتی به این نوع فیلدها در SQL Server نگاشت شود، می‌توان از گزینه DatabaseGeneratedOption.Computed استفاده کرد.
یا اگر برای فیلدی در بانک اطلاعاتی default value تعریف کرده‌اید، مثلا برای فیلد date متد getdate توکار SQL Server را به عنوان پیش فرض درنظر گرفته‌اید و قرار هم نیست توسط برنامه مقدار دهی شود، باز هم می‌توان آن‌را از نوع DatabaseGeneratedOption.Computed تعریف کرد.
البته باید درنظر داشت که اگر خاصیت DateTime تعریف شده در اینجا به همین نحو بکاربرده شود، اگر مقداری برای آن در حین تعریف یک وهله جدید از کلاس User درکدهای برنامه درنظر گرفته نشود، یک مقدار پیش فرض حداقل به آن انتساب داده خواهد شد (چون value type است). بنابراین نیاز است این خاصیت را از نوع nullable تعریف کرد (public DateTime? AddDate).

همچنین اگر یک خاصیت محاسباتی در کلاسی به صورت ReadOnly تعریف شده است (توسط کدهای مثلا سی شارپ یا وی بی):

[NotMapped]
public string FullName
{
get { return Name + " " + LastName; }
}

بدیهی است نیازی نیست تا آن‌را به یک فیلد بانک اطلاعاتی نگاشت کرد. این نوع خواص را با ویژگی NotMapped می‌توان مزین کرد.
همچنین باید دقت داشت در این حالت، از این نوع خواص دیگر نمی‌توان در کوئری‌های EF استفاده کرد. چون نهایتا این کوئری‌ها قرار هستند به عبارات SQL ترجمه شوند و چنین فیلدی در جدول بانک اطلاعاتی وجود ندارد. البته بدیهی است امکان تهیه کوئری LINQ to Objects (کوئری از اطلاعات درون حافظه) همیشه مهیا است و اهمیتی ندارد که این خاصیت درون بانک اطلاعاتی معادلی دارد یا خیر.


7) ComplexType
ComplexType یا Component mapping مربوط به حالتی است که شما یک سری خواص را در یک کلاس تعریف می‌کنید، اما قصد ندارید این‌ها واقعا تبدیل به یک جدول مجزا (به همراه کلید خارجی) در بانک اطلاعاتی شوند. می‌خواهید این خواص دقیقا در همان جدول اصلی کنار مابقی خواص قرار گیرند؛ اما در طرف کدهای ما به شکل یک کلاس مجزا تعریف و مدیریت شوند.
یک مثال:
کلاس زیر را به همراه ویژگی ComplexType به برنامه مطلب جاری اضافه نمائید:

using System.ComponentModel.DataAnnotations;

namespace EF_Sample02.Models
{
[ComplexType]
public class InterestComponent
{
[MaxLength(450, ErrorMessage = "حداکثر 450 حرف")]
public string Interest1 { get; set; }

[MaxLength(450, ErrorMessage = "حداکثر 450 حرف")]
public string Interest2 { get; set; }
}
}

سپس خاصیت زیر را نیز به کلاس User اضافه کنید:

public InterestComponent Interests { set; get; }

همانطور که ملاحظه می‌کنید کلاس InterestComponent فاقد Id است؛ بنابراین هدف از آن تعریف یک Entity نیست و قرار هم نیست در کلاس مشتق شده از DbContext تعریف شود. از آن صرفا جهت نظم بخشیدن به یک سری خاصیت مرتبط و هم‌خانواده استفاده شده است (مثلا آدرس یک، آدرس 2، تا آدرس 10 یک شخص، یا تلفن یک تلفن 2 یا موبایل 10 یک شخص).
اکنون اگر پروژه را اجرا نمائیم، ساختار جدول کاربر به نحو زیر تغییر خواهد کرد:

CREATE TABLE [dbo].[Users](
---...
[Interests_Interest1] [nvarchar](450) NULL,
[Interests_Interest2] [nvarchar](450) NULL,
---...

در اینجا خواص کلاس InterestComponent، داخل همان کلاس User تعریف شده‌اند و نه در یک جدول مجزا. تنها در سمت کدهای ما است که مدیریت آن‌ها منطقی‌تر شده‌اند.

یک نکته:
یکی از الگوهایی که حین تشکیل مدل‌های برنامه عموما مورد استفاده قرار می‌گیرد، null object pattern نام دارد. برای مثال:

namespace EF_Sample02.Models
{
public class User
{
public InterestComponent Interests { set; get; }
public User()
{
Interests = new InterestComponent();
}
}
}

در اینجا در سازنده کلاس User، به خاصیت Interests وهله‌ای از کلاس InterestComponent نسبت داده شده است. به این ترتیب دیگر در کدهای برنامه مدام نیازی نخواهد بود تا بررسی شود که آیا Interests نال است یا خیر. همچنین استفاده از این الگو حین کار با یک ComplexType ضروری است؛ زیرا EF امکان ثبت رکورد جاری را در صورت نال بودن خاصیت Interests (صرفنظر از اینکه خواص آن مقدار دهی شده‌اند یا خیر) نخواهد داد.


8) ForeignKey
این ویژگی نیز در فضای نام System.ComponentModel.DataAnnotations قرار دارد، اما در اسمبلی EntityFramework.dll تعریف شده‌است.
اگر از قراردادهای پیش فرض نامگذاری کلیدهای خارجی در EF Code first خرسند نیستید، می‌توانید توسط ویژگی ForeignKey، نامگذاری مورد نظر خود را اعمال نمائید. باید دقت داشت که ویژگی ForeignKey را باید به یک Reference property اعمال کرد. همچنین در این حالت، کلید خارجی را با یک value type نیز می‌توان نمایش داد:
[ForeignKey("FK_User_Id")]
public virtual User User { set; get; }
public int FK_User_Id { set; get; }

در اینجا فیلد اضافی دوم FK_User_Id به جدول Project اضافه نخواهد شد (چون توسط ویژگی ForeignKey تعریف شده است و فقط یکبار تعریف می‌شود). اما در این حالت نیز وجود Reference property ضروری است.


9) InverseProperty
این ویژگی نیز در فضای نام System.ComponentModel.DataAnnotations قرار دارد، اما در اسمبلی EntityFramework.dll تعریف شده‌است.
از ویژگی InverseProperty برای تعریف روابط دو طرفه استفاده می‌شود.
برای مثال دو کلاس زیر را درنظر بگیرید:
public class Book
{
public int ID {get; set;}
public string Title {get; set;}

[InverseProperty("Books")]
public Author Author {get; set;}
}

public class Author
{
public int ID {get; set;}
public string Name {get; set;}

[InverseProperty("Author")]
public virtual ICollection<Book> Books {get; set;}
}

این دو کلاس همانند کلاس‌های User و Project فوق هستند. ذکر ویژگی InverseProperty برای مشخص سازی ارتباطات بین این دو غیرضروری است و قراردادهای توکار EF Code first یک چنین مواردی را به خوبی مدیریت می‌کنند.
اما اکنون مثال زیر را درنظر بگیرید:
public class Book
{
public int ID {get; set;}
public string Title {get; set;}

public Author FirstAuthor {get; set;}
public Author SecondAuthor {get; set;}
}

public class Author
{
public int ID {get; set;}
public string Name {get; set;}

public virtual ICollection<Book> BooksAsFirstAuthor {get; set;}
public virtual ICollection<Book> BooksAsSecondAuthor {get; set;}
}

این مثال ویژه‌ای است از کتابخانه‌ای که کتاب‌های آن، تنها توسط دو نویسنده نوشته‌ شده‌اند. اگر برنامه را بر اساس این دو کلاس اجرا کنیم، EF Code first قادر نخواهد بود تشخیص دهد، روابط کدام به کدام هستند و در جدول Books چهار کلید خارجی را ایجاد می‌کند. برای مدیریت این مساله و تعین ابتدا و انتهای روابط می‌توان از ویژگی InverseProperty کمک گرفت:

public class Book
{
public int ID {get; set;}
public string Title {get; set;}

[InverseProperty("BooksAsFirstAuthor")]
public Author FirstAuthor {get; set;}
[InverseProperty("BooksAsSecondAuthor")]
public Author SecondAuthor {get; set;}
}

public class Author
{
public int ID {get; set;}
public string Name {get; set;}

[InverseProperty("FirstAuthor")]
public virtual ICollection<Book> BooksAsFirstAuthor {get; set;}
[InverseProperty("SecondAuthor")]
public virtual ICollection<Book> BooksAsSecondAuthor {get; set;}
}

اینبار اگر برنامه را اجرا کنیم، بین این دو جدول تنها دو رابطه تشکیل خواهد شد و نه چهار رابطه؛ چون EF اکنون می‌داند که ابتدا و انتهای روابط کجا است. همچنین ذکر ویژگی InverseProperty در یک سر رابطه کفایت می‌کند و نیازی به ذکر آن در طرف دوم نیست.