مطالب
Blazor 5x - قسمت نهم - مبانی Blazor - بخش 6 - ساده سازی تعاریف ویژگی‌های المان‌ها و انتقال پارامترها به چندین زیر سطح
بررسی ویژگی Attribute Splatting

برای تعریف المان‌های فرم‌ها نیاز است ویژگی‌های قابل توجهی را مانند placeholder ،required ،maxlength و غیره، تعریف کرد که در صورت زیاد بودن تعداد المان‌های یک فرم، مدیریت تعریف این ویژگی‌ها مشکل می‌شود. به همین جهت قابلیت ویژه‌ای مخصوص اینکار به نام Attribute Splatting در Blazor درنظر گرفته شده‌است. برای توضیح آن، ابتدا کامپوننت والد Pages\LearnBlazor\AttributeSplatting.razor و کامپوننت فرزند Pages\LearnBlazor\LearnBlazor‍Components\AttributeSplattingChild.razor را ایجاد می‌کنیم.
در کامپوننت فرزند یا همان AttributeSplattingChild، یک المان را به همراه تعدادی ویژگی تعریف شده مشاهده می‌کنید:
<div>
    <h4 class="text-primary pt-3">Attribute Splatting Child Component</h4>

    <input id="roomName"
        placeholder="@Placeholder"
        required="@Required"
        maxlength="@MaxLength"
        class="form-control" />
</div>

@code {
    [Parameter]
    public string Placeholder { get; set; } = "Initial Text";

    [Parameter]
    public string Required { get; set; } = "required";

    [Parameter]
    public string MaxLength { get; set; } = "10";
}
و کامپوننت والد و یا همان AttributeSplatting.razor، از آن به صورت زیر استفاده می‌کند:
@page "/AttributeSplatting"

<h1>Attribute Splatting</h1>

<AttributeSplattingChild
    Placeholder="Enter the Room Name From Parent"
    MaxLength="5">
</AttributeSplattingChild>
روش ارسال پارامترها را به کامپوننت‌های فرزند، در قسمت پنجم این سری بررسی کردیم. تنها نکته‌ی جدید آن، تعریف مقادیر پیش‌فرض پارامترها در کامپوننت فرزند است. برای مثال در حین تعریف المان AttributeSplattingChild در کامپوننت والد، پارامتر Required مقدار دهی نشده‌است. در این حالت، مقدار پیش‌فرض درج شده‌ی در کامپوننت فرزند، مورد استفاده قرار می‌گیرد؛ وگرنه مقادیر تنظیم شده‌ی توسط کامپوننت والد، حق تقدم بالاتری نسبت به مقادیر پیش‌فرض خواهند داشت.

مشکل! کامپوننت AttributeSplattingChild که فقط به همراه یک المان است، تا این لحظه نیاز به تعریف سه پارامتر جدید را جهت تامین ویژگی‌های آن المان داشته‌است. اگر تعداد این المان‌ها افزایش پیدا کرد، آیا راه بهتری برای مدیریت تعداد بالای ویژگی‌های مورد نیاز وجود دارد؟
پاسخ: در یک چنین حالتی می‌توان ویژگی‌های هر المان را توسط پارامتری از نوع Dictionary مدیریت کرد؛ بجای تعریف تک تک آن‌ها به صورت خواصی مجزا. به این قابلیت، Attribute Splatting می‌گویند.
در این حالت تمام کدهای AttributeSplattingChild.razor به صورت زیر خلاصه می‌شوند:
<div>
    <h4 class="text-primary pt-3">Attribute Splatting Child Component</h4>

    <input id="roomName" @attributes="InputAttributes" class="form-control" />
</div>

@code {
    [Parameter]
    public Dictionary<string, object> InputAttributes { get; set; } = new Dictionary<string, object>
    {
        { "required" , "required"},
        { "placeholder", "Initial Text"},
        { "maxlength", 10}
    };
}
در اینجا با استفاده از دایرکتیو جدید attributes@ می‌توان لیستی از key/value‌های ویژگی‌های یک المان را به صورت یک دیکشنری دریافت کرد و دیگر نیازی نیست تا تک تک آن‌ها را تبدیل به یک پارامتر و خاصیت عمومی مجزا کرد. در این حالت مقادیری که در سمت کامپوننت فرزند تعریف می‌شوند، به عنوان مقادیر اولیه‌ی قابل بازنویسی توسط کامپوننت والد، درنظر گرفته خواهند شد (مانند مثال پارامتر Required که عنوان شد).
و همچنین در ادامه کامپوننت والد یا AttributeSplatting.razor نیز به صورت زیر تغییر می‌کند:
@page "/AttributeSplatting"

<h1>Attribute Splatting</h1>

<AttributeSplattingChild InputAttributes="InputAttributesFromParent"></AttributeSplattingChild>

@code{
    Dictionary<string, object> InputAttributesFromParent = new Dictionary<string, object>
    {
        { "required" , "required"},
        { "placeholder", "Enter the Room Name From Parent"},
        { "maxlength", 5}
    };
}
با توجه به اینکه پارامتر InputAttributes، یک شیء دیکشنری را دریافت می‌کند، فیلد آن‌را در قسمت کدهای کامپوننت جاری تعریف کرده و مورد استفاده قرار می‌دهیم. در این حالت هر مقداری که در سمت والد تنظیم شود، حق تقدم بیشتری نسبت به مقدار پیش‌فرض ویژگی‌های تنظیم شده‌ی در کامپوننت فرزند خواهد داشت.



ساده سازی روش تعریف key/value‌های شیء دیکشنری Attribute Splatting

تا اینجا موفق شدیم تعداد قابل ملاحظه‌ای از پارامترهای عمومی یک کامپوننت را تنها توسط یک شیء Dictionary مدیریت کنیم. همچنین همانطور که ملاحظه می‌کنید، هم Dictionary سمت کامپوننت فرزند و هم سمت کامپوننت والد، نیاز به مقدار دهی اولیه‌ای را دارند. این مقدار دهی اولیه را می‌توان به نحو دیگری نیز در حین استفاده‌ی از قابلیت Attribute Splatting، انجام داد:
<div>
    <h4 class="text-primary pt-3">Attribute Splatting Child Component</h4>

    <input id="roomName" @attributes="InputAttributes" placeholder="Initial Text" class="form-control" />
</div>

@code {
    [Parameter(CaptureUnmatchedValues = true)]
    public Dictionary<string, object> InputAttributes { get; set; } = new Dictionary<string, object>();
}
در اینجا مقادیر اولیه‌ی دیکشنری تعریف شده را حذف کرده‌ایم و بجای آن‌ها، این مقادیر اولیه را به صورت ویژگی‌های متداول یک المان HTML ای تعریف کرده‌ایم؛ مانند placeholder تعریف شده. برای اینکه یک چنین ویژگی‌هایی به عنوان key/valueهای دیکشنری تعریف شده قابل استفاده باشند، تنها کافی است خاصیت CaptureUnmatchedValues ویژگی پارامتر را به true تنظیم کرد. در اینجا Unmatched Values، همان ویژگی‌هایی هستند که در حین تعریف یک المان اضافه شده‌اند (مانند placeholder در مثال فوق) اما در حین مقدار دهی اولیه‌ی دیکشنری، تعریف نشده‌اند و یا تمام پارامترهای عمومی دیگری که در اینجا ذکر و تعریف نشده‌اند. بنابراین تنها یک CaptureUnmatchedValues = true را در سطح یک کامپوننت می‌توان تعریف کرد.

پس از این تغییر، کامپوننت والد هم به صورت زیر خلاصه می‌شود و دیگر نیازی به تعریف و مقدار دهی InputAttributes و یا تعریف مجزای یک دیکشنری را ندارد. در اینجا هر ویژگی که به المان نسبت داده شود، به عنوان Unmatched Values تفسیر شده و مورد استفاده قرار می‌گیرد.
@page "/AttributeSplatting"

<h1>Attribute Splatting</h1>

<AttributeSplattingChild placeholder="Placeholder default"></AttributeSplattingChild>


اگر به تصویر فوق دقت کنید، هرچند در کامپوننت والد مقدار placeholder، به متن دیگری تنظیم شده، اما متن تنظیم شده‌ی در کامپوننت فرزند، تقدم بیشتری پیدا کرده و نمایش داده شده‌است. علت اینجا است که محل قرارگیری آن در مثال فوق، در سمت راست دایرکتیو attributes@ است. اگر آن‌را در سمت چپ attributes@ قرار دهیم، حق تقدم attributes@ بیشتر شده و مقدار تنظیم شده‌ی در سمت کامپوننت والد، بجای placeholder اولیه‌ی تعریف شده‌ی در اینجا مورد استفاده قرار می‌گیرد:
<input id="roomName" placeholder="Initial Text" @attributes="InputAttributes" class="form-control" />


روش انتقال پارامترها به چندین زیر سطح

در قسمت قبل، ParentComponent.razor و ChildComponent.razor را تعریف و تکمیل کردیم. هدف از آ‌ن‌ها، بررسی ویژگی Render Fragment‌ها بود. در ادامه‌ی آن، یک زیر کامپوننت دیگر را نیز به نام Pages\LearnBlazor\LearnBlazor‍Components\GrandChildComponent.razor اضافه می‌کنیم. هدف این است که کامپوننت Parent، کامپوننت Child را فراخوانی کند و کامپوننت Child، کامپوننت GrandChild را تا یک سلسله مراتب از کامپوننت‌ها را تشکیل دهیم.
محتوای GrandChildComponent را هم بسیار ساده نگه می‌داریم، تا پارامتری رشته‌ای را دریافت کرده و نمایش دهد:
<div class="row">
    <h4 class="text-primary pl-4 pt-2 col-12">Grand Child Component</h4>
    <br />
    <p> There is a message - @MessageForGrandChild </p>
</div>

@code {
    [Parameter]
    public string MessageForGrandChild { get; set; }
}
در ChildComponent، کامپوننت GrandChild را به نحو زیر فراخوانی کرده و پارامتری را به آن ارسال می‌کنیم:
<div class="mt-2">
    <GrandChildComponent MessageForGrandChild="@MessageForGrandChild"></GrandChildComponent>
</div>


@code {
    [Parameter]
    public string MessageForGrandChild { get; set; }

   // ...
}
و اکنون در بالاترین سطح این سلسه مراتبی که مشاهده می‌کنید یعنی کامپوننت Parent، این پیام MessageForGrandChild را مقدار دهی خواهیم کرد تا توسط GrandChildComponent نمایش داده شود:
<ChildComponent
    MessageForGrandChild="This is a message from Grand Parent"
    Title="This is the second child component">
    <p><b>@MessageText</b></p>
</ChildComponent>
همانطور که مشاهده می‌کنید، انتقال متداول یک پارامتر، از بالاترین سطح سلسه مراتب کامپوننت‌ها به پایین‌ترین سطح موجود، نیاز به مقدار قابل ملاحظه‌ای کد تکراری را دارد. همچنین برای نمونه پارامتر انتقالی تعریف شده‌ی در کامپوننت Child، اصلا در آن کامپوننت استفاده نمی‌شود و هدف از آن، متصل کردن یک سطح بالاتر، به یک سطح پایین‌تر است.
بنابراین اکنون این سؤال مطرح می‌شود که آیا می‌توان پارامتری را در همان کامپوننت Parent تعریف کرد که توسط کامپوننت GrandChild قابل شناسایی و استفاده باشد، بدون اینکه کامپوننت Child را در این بین تغییر دهیم؟
پاسخ: بله. برای اینکار ویژگی‌های CascadingValue و CascadingParameter در Blazor پیش بینی شده‌اند.
در ابتدا، پارامتر MessageForGrandChild کامپوننت Child حذف کرده و سپس آن‌را توسط کامپوننت توکار CascadingValue محصور می‌کنیم. در اینجا نیاز است مقدار انتقالی را نیز مشخص کنیم:
<CascadingValue Value="@MessageForGrandChild">
    <ChildComponent        
        Title="This is the second child component">
        <p><b>@MessageText</b></p>
    </ChildComponent>
</CascadingValue>

@code {
    string MessageForGrandChild = "This is a message from Grand Parent";
پس از این تعریف، به کامپوننت Child مراجعه کرده و پارامتر MessageForGrandChild آن‌را حذف می‌کنیم؛ چون دیگر نیازی به آن نیست. همچنین در این کامپوننت، فراخوانی GrandChildComponent نیز به صورت زیر خلاصه شده و دیگر نیازی به ذکر پارامتر انتقالی MessageForGrandChild حذف شده را ندارد:
<GrandChildComponent></GrandChildComponent>
در آخر به کامپوننت GrandChild مراجعه کرده و اینبار پارامتر مورد استفاده‌ی در آن‌را با ویژگی جدید CascadingParameter مزین می‌کنیم:
[CascadingParameter]
public string MessageForGrandChild { get; set; }


چند نکته:
- در اینجا نوع CascadingParameter تعریف شده، باید با نوع Value کامپوننت CascadingValue، در بالاترین سطح سلسله مراتب کامپوننت‌ها، یکی باشد.
- نام CascadingParameter تعریف شده مهم نیست. فقط نوع آن مهم است.
- تمام کامپوننت‌های موجود و پوشش داده شده‌ی در سلسله مراتب جاری، قابلیت تعریف CascadingParameter ای مانند مثال فوق را دارند و این تعریف، محدود به پایین‌ترین سطح موجود نیست. برای مثال در اینجا در کامپوننت Child هم در صورت نیاز می‌توان همین CascadingParameter را تعریف و استفاده کرد.


روش تعریف پارامترهای آبشاری نام‌دار

تا اینجا روش انتقال یک پارامتر را از بالاترین سطح، به پایین‌ترین سطح سلسله مراتب کامپوننت‌های تعریف شده، بررسی کردیم. اکنون شاید این سؤال مطرح شود که اگر خواستیم بیش از یک پارامتر را بین اجزای این سلسله، به اشتراک بگذاریم چه باید کرد؟
در این حالت می‌توان پارامتر جدید را توسط یک کامپوننت CascadingValue تو در تو، به صورت زیر معرفی کرد؛ که اینبار نامدار نیز هست:
<CascadingValue Value="@MessageForGrandChild" Name="MessageFromGrandParent">
    <CascadingValue Value="@Number" Name="GrandParentsNumber">
        <ChildComponent
            Title="This is the second child component">
            <p><b>@MessageText</b></p>
        </ChildComponent>
    </CascadingValue>
</CascadingValue>

@code {
    string MessageForGrandChild = "This is a message from Grand Parent";
    int Number = 7;
برای نمونه در این مثال، عدد 7 نیز قرار است در اختیار سلسله مراتب شروع شده‌ی از کامپوننت جاری، قرار گیرد. به همین جهت یک CascadingValue تو در توی مختص آن نیز تعریف شده‌است که اینبار نامش GrandParentsNumber است.

پس از این تغییر، GrandChildComponent، این پارامترهای نامدار را از طریق ذکر صریح خاصیت Name ویژگی CascadingParameter، دریافت می‌کند:
<div class="row">
    <h4 class="text-primary pl-4 pt-2 col-12">Grand Child Component</h4>
    <br />
    There is a message: @Message
    <br />
    GrandParentsNumber: @Number
</div>

@code {
    [CascadingParameter(Name = "MessageFromGrandParent")]
    public string Message { get; set; }

    [CascadingParameter(Name = "GrandParentsNumber")]
    public int Number { get; set; }
}


یک نکته: چون نوع پارامترهای ارسالی یکی نیست، الزامی به ذکر نام آن‌ها نبود. در این حالت بر اساس نوع پارامترهای آبشاری، عملیات اتصال مقادیر صورت می‌گیرد. اما اگر نوع هر دو را برای مثال رشته‌ای تعریف می‌کردیم، مقدار Number، بر روی مقدار MessageForGrandChild بازنویسی می‌شد. یعنی در UI، هر دو پارامتر هم نوع، یک مقدار را نمایش می‌دادند که در حقیقت مقدار پایین‌ترین CascadingValue تعریف شده‌است. بنابراین ذکر نام پارامترهای آبشاری، روشی‌است جهت تمایز قائل شدن بین پارامترهای هم نوع.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-09.zip
مطالب
مدیریت سراسری خطاها در یک برنامه‌ی Angular
در این مطلب قصد داریم پیام‌ها و اخطارهای برنامه را توسط کامپوننت Angular2 Toasty نمایش داده و همچنین برای کاهش میزان تکرار قسمت‌های نمایش خطا در برنامه، کار مدیریت متمرکز و سراسری آن‌ها را نیز انجام دهیم.


نمایش پیام‌ها و اخطارهای یک برنامه‌ی Angular توسط ng2-toasty

در مطلب «ایجاد Drop Down List‌های آبشاری در Angular» در قسمت دریافت اطلاعات drop down دوم از سرور، اگر کاربر مجددا گروه را بر روی حالت «لطفا گروهی را انتخاب کنید ...» قرار دهد، مقدار categoryId به undefined تغییر می‌کند:
  fetchProducts(categoryId?: number) {
    console.log(categoryId);

    this.products = [];

    if (categoryId === undefined || categoryId.toString() === "undefined") {
      return;
    }
در اینجا می‌خواهیم توسط کامپوننت Angular2 Toasty، پیام متناسبی را نمایش دهیم:



پیشنیازهای کار با کامپوننت Angular2 Toasty توسط یک برنامه‌ی Angular CLI

برای کار با کامپوننت Angular2 Toasty، ابتدا از طریق خط فرمان به پوشه‌ی ریشه‌ی برنامه وارد شده و سپس دستور ذیل را صادر می‌کنیم:
> npm install ng2-toasty --save
اینکار سبب خواهد شد تا این کامپوننت در پوشه‌ی node_modules\ng2-toasty نصب شده و همچنین فایل package.json نیز جهت درج مدخل آن به روز رسانی شود:


یک نکته: اگر در حین اجرای این دستور به خطای ذیل برخوردید:
 npm ERR! Error: EPERM: operation not permitted, rename
چون VSCode پوشه‌ی node_modules را تحت نظر قرار می‌دهد، ممکن است یک سری اعمال npm مجوز اجرا را پیدا نکنند. بنابراین ابتدا VSCode را بسته و مجددا دستور npm را اجرا کنید.

پس از آن نیاز است یکی از شیوه‌نامه‌هایی را که در تصویر فوق ملاحظه می‌کنید، در فایل angular-cli.json. مشخص کنیم:
"styles": [
    "../node_modules/bootstrap/dist/css/bootstrap.min.css",
    "../node_modules/ng2-toasty/bundles/style-bootstrap.css",
    "styles.css"
],
که برای نمونه در اینجا، شیوه‌نامه‌ی بوت استرپ آن انتخاب شده‌است.

سپس باید به فایل src\app\app.module.ts مراجعه کرد و ماژول این کامپوننت را معرفی نمود:
import { ToastyModule } from "ng2-toasty";

@NgModule({
  imports: [
    BrowserModule,
    ToastyModule.forRoot(),

همچنین در همین قسمت، به فایل قالب src\app\app.component.html مراجعه کرده و selector tag این کامپوننت را در ابتدای آن تعریف می‌کنیم:
 <ng2-toasty [position]="'top-right'"></ng2-toasty>
در اینجا با استفاده از property binding و تعیین مقدار رشته‌ای top-right، محل نمایش اعلانات برنامه را مشخص می‌کنیم. مقدارهای ممکن آن شامل bottom-right، bottom-left، top-right، top-left، top-center، bottom-center، center-center هستند. برای مثال اگر می‌خواهید آن‌را در میانه‌ی صفحه نمایش دهید، مقدار center-center را انتخاب کنید. همچنین باید دقت داشت که این مقدار باید درون '' قرار گیرد تا مشخص شود که رشته‌ای به خاصیت position انتساب داده شده‌است و این مقدار یک خاصیت عمومی تعریف شده‌ی در کامپوننت متناظر با قالب، نیست.


نمایش یک پیام خطا توسط ToastyService

اکنون که کار برپایی کامپوننت Angular2 Toasty به پایان رسید، کار کردن با آن به سادگی تزریق سرویس آن به سازنده‌ی یک کامپوننت و فراخوانی متدهای info، success ، wait ، error و warning آن است:
import { ToastyService, ToastOptions } from "ng2-toasty";

export class ProductGroupComponent implements OnInit {

  constructor(
    private productItemsService: ProductItemsService,
    private toastyService: ToastyService) { }

  fetchProducts(categoryId?: number) {
    console.log(categoryId);

    this.products = [];

    if (categoryId === undefined || categoryId.toString() === "undefined") {
      this.toastyService.error(<ToastOptions>{
        title: "Error!",
        msg: "Please select a category.",
        theme: "bootstrap",
        showClose: true,
        timeout: 5000
      });
      return;
    }
- در اینجا در ابتدا ماژول‌های مورد نیاز import شده‌اند.
- سپس ToastyService به سازنده‌ی کلاس کامپوننت مدنظر تزریق شده‌است تا بتوان از امکانات آن استفاده کرد.
- در ادامه، فراخوانی متد this.toastyService.error سبب نمایش اخطار قرمز رنگی می‌شود که تصویر آن‌را در ابتدای مطلب جاری مشاهده کردید.
- علت ذکر <ToastOptions> در اینجا این است که وجود آن سبب خواهد شد تا intellisense در VSCode فعال شود و پس از آن بتوان تمام گزینه‌های این متد و تنظیمات را بدون مراجعه‌ی به مستندات آن از طریق intellisense یافت و درج کرد:



مدیریت سراسری خطاهای مدیریت نشده، در یک برنامه‌ی Angular

در برنامه‌های Angular از این دست کدها بسیار مشاهده می‌شوند:
    this.productItemsService.getCategories().subscribe(
      data => {
        this.categories = data;
      },
      err => console.log("get error: ", err)
    );
تا اینجا قسمت err یا بروز خطا را با console.log مدیریت کرده‌ایم. در این حالت کاربر ممکن است 10 بار بر روی دکمه‌ای کلیک کند یا صفحه‌ای را بارگذاری کند و دست آخر متوجه نشود که مشکل کار چیست. به همین جهت می‌توان خطاها را نیز توسط ToastyService نمایش داد تا کاربران دقیقا متوجه بروز مشکل رخ داده شوند. اما ... به این ترتیب تکرار کد زیادی را خواهیم داشت و باید به ازای تمام این موارد، یکبار this.toastyService.error را فراخوانی کنیم. برای مدیریت بهتر یک چنین سناریویی در Angular، کلاس و سرویس توکاری به نام ErrorHandler وجود دارد. در هر قسمتی از برنامه‌ی Angular که استثنایی مدیریت نشده رخ دهد، ابتدا از این کلاس رد شده و سپس به برنامه انتشار پیدا می‌کند. بنابراین می‌توان یک ErrorHandler سفارشی را با ارث بری از آن تهیه کرد و سپس بجای سرویس توکار اصلی، به برنامه معرفی و از آن استفاده نمود. به این ترتیب می‌توان یک Global Error Interceptor را طراحی نمود.
به همین منظور کلاس جدیدی را به صورت ذیل در پوشه‌ی src\app اضافه می‌کنیم:
> ng g cl app.error-handler
با این خروجی
 installing class
  create src\app\app.error-handler.ts
سپس این کلاس را به نحو ذیل تکمیل خواهیم کرد:
import { ErrorHandler } from "@angular/core";

export class AppErrorHandler implements ErrorHandler {

  handleError(error: any): void {
    console.log("Error:", error);
  }
}
کلاس جدید AppErrorHandler از کلاس پایه ErrorHandler ارث بری می‌کند. بنابراین import آن‌را در ابتدای کار مشاهده می‌کنید. سپس باید متد handleError آن‌را با امضایی که مشاهده می‌کنید، پیاده سازی کنیم. فعلا با استفاده از console.log این خطا را در کنسول developer tools نمایش می‌دهیم.

اکنون نیاز است این ErrorHandler سفارشی را بجای نمونه‌ی اصلی به برنامه معرفی کنیم. برای این منظور به فایل src\app\app.module.ts مراجعه کرده و تغییرات ذیل را اعمال می‌کنیم:
import { NgModule, ErrorHandler } from "@angular/core";
import { AppErrorHandler } from "./app.error-handler";

@NgModule({
  providers: [
    { provide: ErrorHandler, useClass: AppErrorHandler }
  ]
ابتدا ErrorHandler به لیست imports اضافه شده‌است و همچنین محل تامین AppErrorHandler نیز مشخص گردیده‌است. سپس در قسمت providers ماژول جاری، از تعریف خاصی که ملاحظه می‌کنید، استفاده خواهد شد. به این ترتیب به Angular اعلام می‌کنیم، هرگاه نیازی به وهله‌ای از کلاس توکار ErrorHandler بود، وهله‌ای از کلاس سفارشی AppErrorHandler را مورد استفاده قرار بده.

اکنون برای آزمایش آن، در کدهای سمت سرور مطلب «ایجاد Drop Down List‌های آبشاری در Angular»، یک استثنای عمدی را قرار می‌دهیم:
[HttpGet("[action]/{categoryId:int}")]
public async Task<IActionResult> GetProducts(int categoryId)
{
   throw new Exception();
به این ترتیب هر زمانیکه گروهی انتخاب شد، دریافت محصولات آن گروه با خطا مواجه می‌شود.
برای اینکه AppErrorHandler، مورد استفاده قرار گیرد، قسمت err دریافت لیست محصولات را نیز حذف می‌کنیم (تا تبدیل به یک استثنای مدیریت نشده شود):
    this.productItemsService.getProducts(categoryId).subscribe(
      data => {
        this.products = data;
        this.isLoadingProducts = false;
      }// ,
      // err => {
      //   console.log("get error: ", err);
      //   this.isLoadingProducts = false;
      // }
    );
اکنون اگر برنامه را اجرا کنیم، چنین پیامی، در کنسول developer tools ظاهر می‌شود و مشخص است از فایل AppErrorHandler صادر شده‌است:



افزودن ToastyService به AppErrorHandler

در ادامه می‌خواهیم بجای console.log از ToastyService برای نمایش خطاهای مدیریت نشده‌ی برنامه در کلاس AppErrorHandler استفاده کنیم:
import { ToastyService, ToastOptions } from "ng2-toasty";
import { ErrorHandler } from "@angular/core";

export class AppErrorHandler implements ErrorHandler {

  constructor(private toastyService: ToastyService) {
  }

  handleError(error: any): void {
    // console.log("Error:", error);
    this.toastyService.error(<ToastOptions>{
      title: "Error!",
      msg: "Fatal error!",
      theme: "bootstrap",
      showClose: true,
      timeout: 5000
    });
  }
}
به همین منظور سرویس آن‌را به سازنده‌ی کلاس AppErrorHandler تزریق کرده و سپس از آن به نحو متداولی در متد handleError استفاده می‌کنیم. به این ترتیب بجای ده‌ها و یا صدها قسمت مدیریت err=>this.toastyService.error در برنامه، تنها یک مورد مدیریت مرکزی را خواهیم داشت.

مشکل اول! اکنون اگر برنامه را اجرا کنیم، در کنسول developer tools چنین خطایی ظاهر می‌شود:
 Uncaught Error: Can't resolve all parameters for AppErrorHandler: (?).
به این معنا که Angular قادر نیست وهله‌ای از AppErrorHandler را ایجاد کند؛ چون نمی‌داند که چگونه باید پارامتر سازنده‌ی ToastyService را وهله سازی و تزریق نماید. علت اینجا است که کار آغاز کلاس ویژه‌ی ErrorHandler سراسری، پیش از کار بارگذاری ماژول مرتبط با ToastyService انجام می‌شود. به همین جهت، این مورد جزو معدود مواردی است که باید به صورت دستی تزریق شود:
import { ErrorHandler, Inject } from "@angular/core";

export class AppErrorHandler implements ErrorHandler {

  constructor(
    @Inject(ToastyService) private toastyService: ToastyService
  ) {
  }
در اینجا توسط Inject decorator، کار تزریق دستی ToastyService انجام خواهد شد. اکنون اگر برنامه را مجدد اجرا کنیم، خطای قبلی برطرف شده‌؛ یعنی کلاس AppErrorHandler با موفقیت وهله سازی شده‌است.

مشکل دوم! اینبار برنامه را اجرا کنید. سپس گروهی را انتخاب نمائید. مشاهده می‌کنید که خطایی نمایش داده نشد؛ هرچند در کنسول developer tools می‌توان اثری از آن را مشاهده کرد. مجددا گروه دیگری را انتخاب کنید، در این بار دوم است که خطای ارائه شده‌ی توسط this.toastyService.error ظاهر می‌شود. توضیح آن نیاز به بررسی مفهومی به نام Zones در Angular دارد.


مفهوم Zones در Angular

زمانیکه متد this.toastyService.error در یک کامپوننت برنامه مورد استفاده قرار گرفت، به خوبی کار می‌کرد و در همان بار اول فراخوانی، پیام را نمایش می‌داد. اما با انتقال آن به کلاسAppErrorHandler ، این قابلیت از کار افتاد. علت اینجا است که زمینه‌ی اجرایی این قطعه کد، اکنون خارج از Zone یا ناحیه‌ی Angular است و به همین دلیل متوجه تغییرات آن نمی‌شود. Zone زمینه‌ی اجرایی اعمال async است و اگر به فایل package.json یک برنامه‌ی Angular دقت کنید، بسته‌ی zone.js، یکی از وابستگی‌های همراه آن است.
تغییرات حالت برنامه، توسط یکی از اعمال ذیل رخ می‌دهند:
الف) بروز رخ‌دادهایی مانند کلیک، ورود اطلاعات و یا ارسال فرم
ب) اعمال Ajax ایی
ج) استفاده از Timers مانند استفاده از setTimeout و  setInterval

هر سه مورد یاد شده از نوع async بوده و زمانیکه رخ می‌دهند، حالت برنامه را تغییر خواهند داد. Angular نیز تنها به این موارد علاقمند بوده و به آ‌ن‌ها در جهت به روز رسانی رابط کاربری برنامه واکنش نشان می‌دهد.
برای مثال this.toastyService.error دارای خاصیتی است به نام timeout: 5000 که در آن، مورد «ج» فوق رخ می‌دهد؛ یعنی یک Timer پس از 5 ثانیه سبب بسته شدن آن خواهد شد. به همین جهت است که اگر پیش از پایان این 5 ثانیه مجددا درخواست واکشی لیست محصولات یک گروه را بدهیم، خطای مربوطه مشاهده می‌شود. چون Angular زمینه‌ی اجرایی لازم را فراهم کرده (یا همان Zone در اینجا) و مجبور به واکنش به عملیات async از نوع Timer است.

برای دسترسی به امکانات کتابخانه‌ی zone.js، می‌توان از طریق تزریق سرویس آن به نام NgZone به سازنده‌ی کلاس شروع کرد:
import { ToastyService, ToastOptions } from "ng2-toasty";
import { ErrorHandler, Inject, NgZone } from "@angular/core";
import { LocationStrategy, PathLocationStrategy } from "@angular/common";

export class AppErrorHandler implements ErrorHandler {

  constructor(
    @Inject(NgZone) private ngZone: NgZone,
    @Inject(ToastyService) private toastyService: ToastyService,
    @Inject(LocationStrategy) private locationProvider: LocationStrategy
  ) {
  }

  handleError(error: any): void {
    // console.log("Error:", error);

    const url = this.locationProvider instanceof PathLocationStrategy ? this.locationProvider.path() : "";
    const message = error.message ? error.message : error.toString();
    this.ngZone.run(() => {
      this.toastyService.error(<ToastOptions>{
        title: "Error!",
        msg: `URL:${url} \n ERROR:${message}`,
        theme: "bootstrap",
        showClose: true,
        timeout: 5000
      });
    });

    // IMPORTANT: Rethrow the error otherwise it gets swallowed
    // throw error;
  }
}
در اینجا فراخوانی this.ngZone.run سبب می‌شود تا درخواست نمایش خطای رخ‌داده وارد Angular Zone شده و بلافاصله سبب نمایش آن گردد:
 


چند نکته
1- اگر می‌خواهید علاوه بر رخ‌دادگردانی سراسری خطاها، این خطاها را به محل اصلی آن‌ها نیز انتشار دهید، نیاز است سطر throw error را در انتهای متد handleError نیز ذکر کنید. در غیر اینصورت، کار در همینجا به پایان خواهد رسید و این خطاها دیگر منتشر نمی‌شوند.
2- روش دریافت URL جاری صفحه را نیز در اینجا مشاهده می‌کنید. این اطلاعات می‌توانند جهت ارسال به سرور برای ثبت و بررسی‌های بعدی مفید باشند.
3- مقدار new Error().stack معادل stack trace جاری است و تقریبا در تمام مرورگرهای جدید پشتیبانی می‌شود.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-template-driven-forms-lab-07.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس به ریشه‌ی پروژه وارد شده و دو پنجره‌ی کنسول مجزا را باز کنید. در اولی دستورات
>npm install
>ng build --watch
و در دومی دستورات ذیل را اجرا کنید:
>dotnet restore
>dotnet watch run
اکنون می‌توانید برنامه را در آدرس http://localhost:5000 مشاهده و اجرا کنید.
مطالب
ckeditor در برابر tinymce
این دو ادیتور  یعنی CKEditor و TinyMCE  دو تا از محبوب‌ترین ادیتورهای موجود تحت وب هستند که به صورت متن باز ارائه می‌شوند و از لحاظ قدرت و کارآیی در رده بالایی قرار دارند. همچنین مستندات و api‌های خوبی هم در مورد آنها موجود است؛ ولی در هنگام استفاده خیلی‌ها شاید این سؤال را داشته باشند که کدام ادیتور را انتخاب کنند؟ حتی اگر هر دو ادیتور امکاناتی بیش از نیاز ما را فراهم کنند، باز هم انسان در پی بهترین هاست. 
در این مطلب به مزایا و معایب اشاره نشده و قصد تخریب ادیتور خاصی را نداریم؛ بلکه خصوصیات آن‌ها بررسی شده و در نهایت توسعه دهنده می‌تواند بر اساس این‌ها، ابزار درستی را برای کارش، انتخاب کند.

رابط کاربری یا user interface
ckeditor داری رابط کاربری است که برای همه ادیتورها به صورت پیش فرض وجود دارد و این حالت برای خیلی از کاربرها شناخته شده هست.

در tinymce رابط کاربری به غیر از نوار ابزار میتواند شامل منو هم باشد؛ با اینکه استفاده از منو آنچنان در ادیتور مورد استقبال قرار نمی‌گیرد ولی ممکن است بعضی‌ها این ویژگی را دوست داشته باشند:

برای حذف منو در این ادیتور یا مشخص کردن چه عناصری نمایش داده شود، این کد در سایت سازنده نوشته شده:
// Disable all menus
tinymce.init({
    menubar : false
});

// Configure different order
tinymce.init({ 
    menubar: "tools table format view insert edit"
});

پلاگین‌ها Plugins
وقتی بحث پلاگین پیش کشیده می‌شود، انتخابی بین ساده بودن و اختصاصی بودن پیش خواهد آمد. به نظر می‌رسد پلاگین‌های ck در حالت عادی بیشتر از tiny هستند و امکانات بیشتری را نسبت به tiny ارائه می‌کنند؛ برای مثال به دیالوگ درج تصویر در دو شکل زیر نگاه کنید: در ck شما امکانات بیشتری برای درج یک تصویر دارید تا tiny

آپلود تصویر در ckeditor

CKEditor

tinymce

Tinymce

در نسخه‌های پیشین tiny این دیالوگ‌ها به صورت پنجره‌های popup باز می‌شدند، ولی در تمامی نسخه‌های ck بسیاری از این پنجره‌ها بدین صورت باز نشده و سفارشی هستند. درست است که در ظاهر مثل هم باز می‌شوند ولی مکانیزم و روش باز شدن آن‌ها با یکدیگر متفاوت است و در واقع ck با روشی هوشمندانه و با استفاده از کدهای جاوا اسکریپت و ترکیب لایه‌های html این روش را پیاده سازی می‌کند.

شاید بگویید خوب چه فرقی می‌کند که از چه روشی برای پیاده سازی استفاده شود؟ این مورد از دو جنبه مورد بررسی قرار می‌گیرد:

1 - اگر به مرورگرهایی چون IE دقت کرده باشید، می‌بینید که موقعی‌که یک popup باز می‌شود، به عنوان یک پنجره‌ی جدید باز می‌شود و باعث شلوغ شدن صفحه می‌گردد و بیشتر موجب آزار کاربر می‌گردد.
2- ساخت واقعی یک صفحه popup منجر شدن به ایجاد یک پنجره ای جدید و طی شدن فرآیندهای مربوط به بارگذاری و رندر شدن یک صفحه‌ی جدید می‌باشد؛ ولی در مورد پنجره‌های سفارشی این فرآیندها صورت نمی‌گیرد. این مورد بر روی بسیاری از هاست‌ها خود را به وضوح نمایش می‌دهد و ck در صرفه جویی از زمان، به مراتب بهتر عمل می‌کند. 


از نسخه tinymce 4 به بعد مکانیزم سفارشی بودن دیالوگ‌ها بر روی این ادیتور نیز صورت گرفته است.

کپی و پیست Copy - Paste

بسیاری از کاربران گاهی اوقات مطالب خود را از جایی یا از یک صفحه‌ی وب دیگر به ادیتور کپی می‌کنند. برای مثال در اینجا صفحه‌ی گوگل را به داخل ادیتورها کپی می‌کنیم و به نظر میرسد ckeditor در این مواقع رفتار بهتری نسبت به tiny از خود نشان می‌دهد.

در تاینی لوگوی گوگل دقیقا در وسط قرار گرفته و بعضی موارد خاص مانند فرم‌ها به خوبی نمایش داده می‌شوند؛ ولی در ck شما متن تمیزتری دارید و طوری نشان داده می‌شود که یک css کاربرپسند به آن اضافه شده است.

ckeditor

Ckeditor


انعطاف پذیری Flexibility

هر دو ادیتور در این زمینه به خوبی طراحی شده‌اند و با استفاده از جاوا اسکریپت می‌توان آن‌ها را به سادگی توسعه داد و امکانات دلخواهتان را به آن اضافه کنید ولی در کل ckeditor در  زمینه apiهای برنامه نویسی، پیشروتر از رقیبش tinymce است.

حجم یا سایز ادیتورها Size

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

حالا شما با توجه به موارد بالا می‌توانید ادیتور خود را انتخاب کنید.

در اینجا هم می‌توانید فایل‌های مربوط به زبان را نیز دانلود کنید که هر دو از زیان فارسی پشتیبانی می‌کنند.

فایل‌های زبان TinyMCE 

فایل‌های زبان CKeditor 



منابع:

http://www.krizalys.com/article/ckeditor-vs-tinymce  

http://www.tinymce.com/wiki.php/Configuration:menubar

پاسخ به بازخورد‌های پروژه‌ها
Custom Membership
پیشنهاد می‌کنم اگر نیاز به تنظیم دسترسی در سطح اکشن رو دارید ، یک Attribute سفارشی بنویسید و اون رو بصورت سراسری به همه کنترلرها بدید.

بعد توسط Attribute می‌تونین قبل از اجرای هر اکشن ، کنترل دسترسی رو انجام بدین و اجازه اجرا شدن رو به اکشن بدید یا ندید.

در پروژه‌های ساده‌تر اگر دوست دارید همه چی رو کنترل کنین ، می‌تونید یک Role Provider سفارشی بنویسید، اگر نه می‌تونین از Identity بهره ببرید
مطالب
فایل‌های chm و مشکل فارسی - قسمت اول

همانطور که مطلع هستید از ویندوز ویستا به بعد، فرمت قدیمی فایل‌های راهنمای ویندوز (فایل‌های hlp) منسوخ شده تلقی می‌شود و فرمت پیشنهادی، chm است. نرم افزارهای زیادی برای تهیه فایل‌های compiled html help یا همان chm های معروف وجود دارند که معروفترین آن‌ها برنامه‌ی Help & Manual است.
اما تمام این برنامه‌ها در حقیقت پوسته‌ای هستند برای برنامه‌ی رایگان html help work shop مایکروسافت و در نهایت از کامپایلر آن استفاده می‌کنند. بنابراین چرا از برنامه‌ی رایگان اصلی استفاده نشود؟

اگر تا به حال با html help work shop کار نکرده‌اید، در ادامه مروری سریع بر آن خواهیم داشت:

الف) درست کردن فایل‌های صفحات راهنما
برنامه‌ی Help & Manual ایی که معرفی شد و تمام نمونه‌های مشابه آن، تنها کار مهمی را که انجام می‌دهند این است که شما را از یک html editor بی‌نیاز می‌کنند. بنابراین زمانیکه می‌خواهیم از برنامه‌ی اصلی html help work shop استفاده کنیم نیاز به یک html editor خارجی برای تهیه فایل‌های راهنما خواهیم داشت. مثلا مرحوم front page یا نگارش جدید آن به نام Microsoft expression web یا Dreamweaver یا Aptana یا حتی notepad !
در اینجا تنها مشخص کردن نوع encoding نمایش صفحه برای صحیح نمایش داده شدن متون فارسی کافی است. اما این تمام ماجرا نیست.

ب) کامپایل کردن فایل‌های راهنمای ایجاد شده
برای این منظور بجای آپلود بیش از 30 تصویر جهت توضیحات قدم به قدم نحوه‌ی انجام این‌کار، یک فایل ویدیویی درست کرده‌ام که آن‌‌را از آدرس زیر می‌توانید دریافت کنید.
دریافت فایل

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

در قسمت دوم، نحوه‌ی رفع این دو مشکل (مشکل جستجوی عبارات فارسی و مشکل عنوان‌های به هم ریخته) را بررسی خواهیم کرد.

ادامه دارد ...

مطالب
راهنمای تغییر بخش احراز هویت و اعتبارسنجی کاربران سیستم مدیریت محتوای IRIS به ASP.NET Identity – بخش اول
سیستم مدیریت محتوای IRIS از سیستم‌های اعتبار سنجی و مدیریت کاربران رایج نظیر ASP.NET Membership و یا ASP.NET Simple Membership استفاده نمی‌کند و از یک سیستم احراز هویت سفارشی شده مبتنی بر FormsAuthentication بهره می‌برد. زمانیکه در حال نوشتن پروژه‌ی IRIS بودم هنوز ASP.NET Identity معرفی نشده بود و به دلیل مشکلاتی که سیستم‌های قدیمی ذکر شده داشت، یک سیستم اعتبار سنجی کاربران سفارشی شده را در پروژه پیاده سازی کردم.
برای اینکه با معایب سیستم‌های مدیریت کاربران پیشین و مزایای ASP.NET Identity آشنا شوید، مقاله زیر می‌تواند شروع خیلی خوبی باشد:


به این نکته نیز اشاره کنم که  هنوز هم می‌توان از FormsAuthentication مبتنی بر OWIN استفاده کرد؛ ولی چیزی که برای من بیشتر اهمیت دارد خود سیستم Identity هست، نه نحوه‌ی ورود و خروج به سایت و تولید کوکی اعتبارسنجی.
باید اعتراف کرد که سیستم جدید مدیریت کاربران مایکروسافت خیلی خوب طراحی شده است و اشکالات سیستم‌های پیشین خود را ندارد. به راحتی می‌توان آن را توسعه داد و یا قسمتی از آن را تغییر داد و به جرات می‌توان گفت که پایه و اساس هر سیستم اعتبار سنجی و مدیریت کاربری را که در نظر داشته باشید، به خوبی پیاده سازی کرده است.
در ادامه قصد دارم، چگونگی مهاجرت از سیستم فعلی به سیستم Identity را بدون از دست دادن اطلاعات فعلی شرح دهم.

رفع باگ ثبت کاربرهای تکراری نسخه‌ی کنونی

قبل از این که سراغ پیاده سازی Identity برویم، ابتدا باید یک باگ مهم را در نسخه‌ی قبلی، برطرف نماییم. نسخه‌ی کنونی مدیریت کاربران اجازه‌ی ثبت کاربر با ایمیل و یا نام کاربری تکراری را نمی‌دهد. جلوگیری از ثبت نام کاربر جدید با ایمیل یا نام کاربری تکراری از طریق کدهای زیر صورت گرفته است؛ اما در عمل، همیشه هم درست کار نمی‌کند.
        public AddUserStatus Add(User user)
        {
            if (ExistsByEmail(user.Email))
                return AddUserStatus.EmailExist;
            if (ExistsByUserName(user.UserName))
                return AddUserStatus.UserNameExist;

            _users.Add(user);
            return AddUserStatus.AddingUserSuccessfully;
        }
شاید الان با خود بگویید که چرا برای فیلدهای UserName و Email ایندکس منحصر به فرد تعریف نشده است؟ دلیلش این بوده که در زمان نگارش پروژه، Entity Framework پشتیبانی پیش فرضی از تعریف ایندکس نداشت و نوشتن همین شرط‌ها کافی به نظر می‌رسید. باز هم ممکن است بگویید که مسائل همزمانی چگونه مدیریت شده است و اگر دو کاربر مختلف در یک لحظه، نام کاربری یکسانی را انتخاب کنند، سیستم چگونه از ثبت دو کاربر مختلف با نام کاربری یکسان ممانعت می‌کند؟ 
جواب این است که ممانعتی نمی‌کند و دو کاربر با نام‌های کاربری یکسان ثبت می‌شوند؛ اما من برای وبسایت خودم که تعداد کاربرانش محدود بود این سناریو را محتمل نمی‌دانستم و کد خاصی برای جلوگیری از این اتفاق پیاده سازی نکرده بودم.
با این حال، در حال حاضر نزدیک به 20 کاربر تکراری در دیتابیسی که این سیستم استفاده می‌کند ثبت شده است. اما واقعا آیا دو کاربر مختلف اطلاعات یکسانی وارد کرده‌اند؟
 دلیل رخ دادن این اتفاق این است که کاربری که در حال ثبت نام در سایت است، وقتی که بر روی دکمه‌ی ثبت نام کلیک می‌کند و اطلاعات به سرور ارسال می‌شوند، در سمت سرور بعد از رد شدن از شرطهای تکراری نبودن UserName و Email، قبل از رسیدن به متد SaveChanges برای ذخیره شدن اطلاعات کاربر جدید در دیتابیس، وقفه ای در ترد این درخواست به وجود می‌آید. کاربر که احساس می‌کند اتفاقی رخ نداده است، دوباره بر روی دکمه‌ی ثبت نام کلیک می‌کند و  همان اطلاعات قبلی به سرور ارسال می‌شود و این درخواست نیز دوباره شرط‌های تکراری نبودن اطلاعات را با موفقیت رد می‌کند( چون هنوز SaveChanges درخواست اول فراخوانی نشده است) و این بار SaveChanges درخواست دوم با موفقیت فراخوانی می‌شود و کاربر ثبت می‌شود. در نهایت هم ترد درخواست اول به ادامه‌ی کار خود می‌پردازد و SaveChanges درخواست اول نیز فراخوانی می‌شود و خیلی راحت دو کاربر با اطلاعات یکسان ثبت می‌شود. این سناریو را در ویژوال استادیو با قرار دادن یک break point قبل از فراخوانی متد SaveChanges می‌توانید شبیه سازی کنید.
احتمالا این سناریو با مباحث همزمانی در سیستم عامل و context switch‌های بین ترد‌ها مرتبط است و این context switch‌ها  بین درخواست‌ها و atomic نبودن روند چک کردن اطلاعات و ثبت آن ها، سبب بروز چنین مشکلی میشود.
برای رفع این مشکل می‌توان از غیر فعال کردن یک دکمه در حین انجام پردازش‌های سمت سرور استفاده کرد تا کاربر بی حوصله، نتواند چندین بار بر روی یک دکمه کلیک کند و یا راه حل اصولی‌تر این است که ایندکس منحصر به فرد برای فیلدهای مورد نظر تعریف کنیم.
به طور پیش فرض در ASP.NET Identity برای فیلدهای UserName و Email ایندکس منحصر به فرد تعریف شده است. اما مشکل این است که به دلیل وجود کاربرانی با Email و UserName تکراری در دیتابیس کنونی، امکان تعریف Index منحصر به فرد وجود ندارد و پیش از انجام هر کاری باید این ناهنجاری را در دیتابیس برطرف نماییم.
   
به شخصه معمولا برای انجام کارهایی از این دست، یک کنترلر در برنامه خود تعریف می‌کنم و در آنجا کارهای لازم را انجام می‌دهم.
در اینجا من برای حذف کاربران با اطلاعات تکراری، یک کنترلر به نام Migration و اکشن متدی به نام RemoveDuplicateUsers تدارک دیدم.
using System.Linq;
using System.Web.Mvc;
using Iris.Datalayer.Context;

namespace Iris.Web.Controllers
{
    public class MigrationController : Controller
    {
        public ActionResult RemoveDuplicateUsers()
        {
            var db = new IrisDbContext();

            var lstDuplicateUserGroup = db.Users
                                               .GroupBy(u => u.UserName)
                                               .Where(g => g.Count() > 1)
                                               .ToList();

            foreach (var duplicateUserGroup in lstDuplicateUserGroup)
            {
                foreach (var user in duplicateUserGroup.Skip(1).Where(user => user.UserMetaData != null))
                {
                    db.UserMetaDatas.Remove(user.UserMetaData);
                }
                
                db.Users.RemoveRange(duplicateUserGroup.Skip(1));
            }

            db.SaveChanges();

            return new EmptyResult();
        }
    }
}
در اینجا کاربران بر اساس نام کاربری گروه بندی می‌شوند و گروه‌هایی که بیش از یک عضو داشته باشند، یعنی کاربران آن گروه دارای نام کاربری یکسان هستند و به غیر از کاربر اول گروه، بقیه باید حذف شوند. البته این را متذکر شوم که منطق وبسایت من به این شکل بوده است و اگر منطق کدهای شما فرق می‌کند، مطابق با منطق خودتان این کدها را تغییر دهید.

تذکر: اینجا شاید بگویید که چرا cascade delete برای UserMetaData فعال نیست و بخواهید که آن را اصلاح کنید. پیشنهاد من این است که اکنون از هدف اصلی منحرف نشوید و تمام تمرکز خود را بر روی انتقال به ASP Identity با حداقل تغییرات بگذارید. این گونه نباشد که در اواسط کار با خود بگویید که بد نیست حالا فلان کتابخانه را آپدیت کنم یا این تغییر را بدهم بهتر می‌شود. سعی کنید با حداقل تغییرات رو به جلو حرکت کنید؛ آپدیت کردن کتابخانه‌ها را هم بعدا می‌شود انجام داد.
   

 مقایسه ساختار جداول دیتابیس کاربران IRIS با ASP.NET Identity

ساختار جداول ASP.NET Identity به شکل زیر است:

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

همان طور که مشخص است در هر دو سیستم، بین ساختار جداول و رابطه‌ی بین آن‌ها شباهت‌ها و تفاوت هایی وجود دارد. سیستم Identity دو جدول بیشتر از IRIS دارد و برای جداولی که در سیستم کنونی وجود ندارند نیاز به انجام کاری نیست و به هنگام پیاده سازی Identity، این جداول به صورت خودکار به دیتابیس اضافه خواهند شد.

دو جدول مشترک در این دو سیستم، جداول Users و Roles هستندکه نحوه‌ی ارتباطشان با یکدیگر متفاوت است. در Iris بین User و Role رابطه‌ی یک به چند وجود دارد ولی در Identity، رابطه‌ی بین این دو جدول چند به چند است و جدول واسط بین آن‌ها نیز UserRoles نام دارد.

از آن جایی که من قصد دارم در سیستم جدید هم رابطه‌ی بین کاربر و نقش چند به چند باشد، به پیش فرض‌های Identity کاری ندارم. به رابطه‌ی کنونی یک به چند کاربر و نقشش نیز دست نمی‌گذارم تا در انتها با یک کوئری از دیتابیس، اطلاعات نقش‌های کاربران را به جدول جدیدش منتقل کنم.

جدولی که در هر دو سیستم مشترک است و هسته‌ی آن‌ها را تشکیل می‌دهد، جدول Users است. اگر دقت کنید می‌بینید که این جدول در هر دو سیستم، دارای یک سری فیلد مشترک است که دقیقا هم نام هستند مثل Id، UserName و Email؛ پس این فیلد‌ها از نظر کاربرد در هر دو سیستم یکسان هستند و مشکلی ایجاد نمی‌کنند.

یک سری فیلد هم در جدول User در سیستم IRIS هست که در Identity نیست و بلعکس. با این فیلد‌ها نیز کاری نداریم چون در هر دو سیستم کار مخصوص به خود را انجام می‌دهند و تداخلی در کار یکدیگر ایجاد نمی‌کنند.

اما فیلدی که برای ذخیره سازی پسورد در هر دو سیستم استفاده می‌شود دارای نام‌های متفاوتی است. در Iris این فیلد Password نام دارد و در Identity نامش PasswordHash است.

برای اینکه در سیستم کنونی، نام  فیلد Password جدول User را به PasswordHash تغییر دهیم قدم‌های زیر را بر می‌داریم:

وارد پروژه‌ی DomainClasses شده و کلاس User را باز کنید. سپس نام خاصیت Password را به PasswordHash تغییر دهید.  پس از این تغییر بلافاصله یک گزینه زیر آن نمایان می‌شود که می‌خواهد در تمام جاهایی که از این نام استفاده شده است را به نام جدید تغییر دهد؛ آن را انتخاب کرده تا همه جا Password به PasswordHash تغییر کند.

برای این که این تغییر نام بر روی دیتابیس نیز اعمال شود باید از Migration استفاده کرد. در اینجا من از مهاجرت دستی که بر اساس کد هست استفاده می‌کنم تا هم بتوانم کدهای مهاجرت را پیش از اعمال بررسی و هم تاریخچه‌ای از تغییرات را ثبت کنم.

برای این کار، Package Manager Console را باز کرده و از نوار بالایی آن، پروژه پیش فرض را بر روی DataLayer قرار دهید. سپس در کنسول، دستور زیر را وارد کنید:

Add-Migration Rename_PasswordToPasswordHash_User

اگر وارد پوشه Migrations پروژه DataLayer خود شوید، باید کلاسی با نامی شبیه به 201510090808056_Rename_PasswordToPasswordHash_User ببینید. اگر آن را باز کنید کدهای زیر را خواهید دید: 

  public partial class Rename_PasswordToPasswordHash_User : DbMigration
    {
        public override void Up()
        {
            AddColumn("dbo.Users", "PasswordHash", c => c.String(nullable: false, maxLength: 200));
            DropColumn("dbo.Users", "Password");
        }
        
        public override void Down()
        {
            AddColumn("dbo.Users", "Password", c => c.String(nullable: false, maxLength: 200));
            DropColumn("dbo.Users", "PasswordHash");
        }
    }

بدیهی هست که این کدها عمل حذف ستون Password را انجام میدهند که سبب از دست رفتن اطلاعات می‌شود. کد‌های فوق را به شکل زیر ویرایش کنید تا تنها سبب تغییر نام ستون Password به PasswordHash شود.

    public partial class Rename_PasswordToPasswordHash_User : DbMigration
    {
        public override void Up()
        {
            RenameColumn("dbo.Users", "Password", "PasswordHash");
        }
        
        public override void Down()
        {
            RenameColumn("dbo.Users", "PasswordHash", "Password");
        }
    }

سپس باز در کنسول دستور Update-Database  را وارد کنید تا تغییرات بر روی دیتابیس اعمال شود.

دلیل اینکه این قسمت را مفصل بیان کردم این بود که می‌خواستم در مهاجرت از سیستم اعتبارسنجی خودتان به ASP.NET Identity دید بهتری داشته باشید.

تا به این جای کار فقط پایگاه داده سیستم کنونی را برای مهاجرت آماده کردیم و هنوز ASP.NET Identity را وارد پروژه نکردیم. در بخش‌های بعدی Identity را نصب کرده و تغییرات لازم را هم انجام می‌دهیم.

مطالب
Blazor 5x - قسمت 28 - برنامه‌ی Blazor WASM - نمایش لیست اطلاعات دریافتی از Web API
در قسمت قبل، سرویس و کامپوننت دریافت اطلاعات اتاق‌ها را از Web API برنامه، تکمیل کردیم. در این قسمت با استفاده از اطلاعات مهیا شده، UI آن‌را نیز تکمیل خواهیم کرد.


نمایش منتظر بمانید در حین بارگذاری اولیه‌ی کامپوننت

کامپوننت‌هایی که قرار است اطلاعات را از یک Web API دریافت کنند، مدتی باید منتظر بمانند تا عملیات رفت و برگشت به سرور، تکمیل شود. در این بین می‌توان یک loading را به کاربر نمایش داد:
@page "/hotel/rooms"

@if (Rooms is not null && Rooms.Any())
{

}
else
{
    <div style="position:fixed;top:50%;left:50%;margin-top:-50px;margin-left:-100px;">
        <img src="images/loader.gif" />
    </div>
}

@code {
    IEnumerable<HotelRoomDTO> Rooms = new List<HotelRoomDTO>();
    // ... 
}
- فیلد Rooms را در قسمت قبل، در متد LoadRooms، از Web API دریافت و مقدار دهی کردیم. تا زمان تکمیل عملیات این متد، فیلد Rooms، فاقد عضوی است؛ بنابراین قسمت else شرط فوق اجرا می‌شود که یک loading را نمایش خواهد داد. مابقی UI برنامه در قسمت if آن قرار می‌گیرد.
- هر زمانیکه کار روال رویدادگردان OnInitializedAsync به پایان برسد (که شامل اجرای متد LoadRooms نیز هست)، سبب فراخوانی خودکار StateHasChanged می‌شود. این فراخوانی، UI را مجددا رندر می‌کند. به همین جهت است که پس از پایان کار، محتوای if، رندر خواهد شد.
- از این loading سفارشی که در میانه‌ی صفحه نمایش داده می‌شود، می‌توان در فایل wwwroot\index.html نیز بجای loading پیش‌فرض آن استفاده کرد:
  <body>
    <div id="app">
      <div
        style="
          position: fixed;
          top: 50%;
          left: 50%;
          margin-top: -50px;
          margin-left: -100px;
        "
      >
        <img src="images/ajax-loader.gif" />
      </div>
    </div>

افزودن خواصی جدید به HotelRoomDTO

می‌خواهیم به کاربر امکان تغییر تعداد روزهای اقامت را بدهیم. این انتخاب باید در لیست اتاق‌های نمایش داده شده، با تغییر تعداد روزهای اقامت (TotalDays) و هزینه‌ی جدید متناظر با آن (TotalAmount)، منعکس شود. به همین جهت این خواص را به HotelRoomDTO، اضافه می‌کنیم:
namespace BlazorServer.Models
{
    public class HotelRoomDTO
    {
        // ...

        public int TotalDays { get; set; }
        public decimal TotalAmount { get; set; }
    }
}
محاسبات مربوط به این خواص را هم می‌توان در همان کامپوننت HotelRooms.razor، پس از بارگذاری لیست اتاق‌ها از Web API، انجام داد:
@code
{
     HomeVM HomeModel = new HomeVM();
    // ...

    private async Task LoadRoomsAsync()
    {
        Rooms = await HotelRoomService.GetHotelRoomsAsync(HomeModel.StartDate, HomeModel.EndDate);
        foreach (var room in Rooms)
        {
            room.TotalAmount = room.RegularRate * HomeModel.NoOfNights;
            room.TotalDays = HomeModel.NoOfNights;
        }
    }
}


افزودن امکان تغییر تعداد روزهای اقامت در همان صفحه‌ی نمایش لیست اتاق‌ها


همانطور که در تصویر فوق هم مشاهده می‌کنید، می‌خواهیم در این صفحه نیز کاربر بتواند زمان شروع اقامت و مدت مدنظر را تغییر دهد. به همین جهت، HomeModel ای را که در قسمت قبل از Local Storage دریافت کردیم، به فرم زیر متصل می‌کنیم تا اجزای آن در این فرم، نمایش داده شده و قابل تغییر شوند:
@if (Rooms is not null && Rooms.Any())
{
    <EditForm Model="HomeModel" OnValidSubmit="SaveBookingInfo" class="bg-light">
        <div class="pt-3 pb-2 px-5 mx-1 mx-md-0 bg-secondary">
            <DataAnnotationsValidator />
            <div class="row px-3 mx-3">
                <div class="col-6 col-md-4">
                    <div class="form-group">
                        <label class="text-warning">Check in Date</label>
                        <InputDate @bind-Value="HomeModel.StartDate" class="form-control" />
                    </div>
                </div>
                <div class="col-6 col-md-4">
                    <div class="form-group">
                        <label class="text-warning">Check Out Date</label>
                        <input @bind="HomeModel.EndDate" disabled="disabled"
                            readonly="readonly" type="date" class="form-control" />
                    </div>
                </div>
                <div class=" col-4 col-md-2">
                    <div class="form-group">
                        <label class="text-warning">No. of nights</label>
                        <select class="form-control" @bind="HomeModel.NoOfNights">
                            <option value="Select" selected disabled="disabled">(Select No. Of Nights)</option>
                            @for (var i = 1; i <= 10; i++)
                            {
                                <option value="@i">@i</option>
                            }
                        </select>
                    </div>
                </div>

                <div class="col-8 col-md-2">
                    <div class="form-group" style="margin-top: 1.9rem !important;">
                        @if (IsProcessing)
                        {
                            <button class="btn btn-success btn-block form-control">
                                <i class="fa fa-spin fa-spinner"></i>Processing...
                            </button>
                        }
                        else
                        {
                            <input type="submit" value="Update" class="btn btn-success btn-block form-control" />
                        }
                    </div>
                </div>
            </div>
        </div>
    </EditForm>
نکته‌ی مهم این فرم، مدیریت قسمت کلیک بر روی دکمه‌ی Update است که سبب فراخوانی روال رویدادگران OnValidSubmit می‌شود:
@code {
    bool IsProcessing;

    // ...

    private async Task SaveBookingInfo()
    {
        IsProcessing = true;
        HomeModel.EndDate = HomeModel.StartDate.AddDays(HomeModel.NoOfNights);
        await LocalStorage.SetItemAsync(ConstantKeys.LocalInitialBooking, HomeModel);
        await LoadRoomsAsync();
        IsProcessing = false;
    }
}
در ابتدای عملیات، فیلد جدید IsProcessing را به true تنظیم می‌کنیم. این مورد سبب می‌شود تا برچسب دکمه‌ی Update به Processing... تغییر کند. سپس فیلد محاسباتی EndDate را بر اساس اطلاعات جدید فرم، به روز رسانی می‌کنیم. در ادامه، مجددا این اطلاعات را در Local Storage ذخیره سازی کرده و کار LoadRoomsAsync را انجام می‌دهیم که به همراه آن، خواص جدید تعداد روزها و هزینه‌ی اقامت نیز مجددا محاسبه می‌شوند. در آخر برچسب دکمه‌ی Update را به حالت اول باز می‌گردانیم.

سؤال: زمانیکه IsProcessing به true تنظیم می‌شود که هنوز کار متد رویدادگردان SaveBookingInfo به پایان نرسیده‌است و فراخوانی خودکار StateHasChanged در پایان متدهای رویدادگردان صورت می‌گیرد. پس چطور است که سبب رندر مجدد UI و تغییر برچسب دکمه‌ی Update می‌شود؟
پاسخ به این سؤال را در قسمت 6 این سری با بررسی چرخه‌ی حیات کامپوننت‌ها، مشاهده کردیم:
«البته متدهای رویدادگردان async، دوبار سبب فراخوانی ضمنی StateHasChanged می‌شوند؛ یکبار زمانیکه قسمت sync متد به پایان می‌رسد (در این مثال یعنی تا قبل از اولین await نوشته شده) و یکبار هم زمانیکه کار فراخوانی کلی متد به پایان خواهد رسید»


نمایش لیست اتاق‌ها


نمایش لیست اتاق‌ها مطابق تصویر فوق، دو قسمت اصلی را دارد:
الف) نمایش لیست تصاویر منتسب به یک اتاق، توسط کامپوننت carousel بوت استرپ
@foreach (var room in Rooms)
{
            <div class="row p-2 my-3 " style="border-radius:20px; border: 1px solid gray">
                <div class="col-12 col-lg-3 col-md-4">
                    <div id="carouselExampleIndicators_@room.Id"
                        class="carousel slide mb-4 m-md-3 m-0 pt-3 pt-md-0"
                        data-ride="carousel">
                        <ol class="carousel-indicators">
                            @{
                                int imageIndex = 0;
                                int innerImageIndex = 0;
                            }
                            @foreach (var image in room.HotelRoomImages)
                            {
                                if (imageIndex == 0)
                                {
                                    <li data-target="#carouselExampleIndicators_@room.Id"
                                        data-slide-to="@imageIndex" class="active"></li>

                                }
                                else
                                {
                                    <li data-target="#carouselExampleIndicators_@room.Id"
                                        data-slide-to="@imageIndex"></li>
                                }
                                imageIndex++;
                            }
                        </ol>
                        <div class="carousel-inner">
                            @foreach (var image in room.HotelRoomImages)
                            {
                                var imageUrl = $"{ImagesBaseAddress}/{image.RoomImageUrl}";
                                if (innerImageIndex == 0)
                                {
                                    <div class="carousel-item active">
                                        <img class="d-block w-100" style="border-radius:20px;"
                                            src="@imageUrl" alt="First slide">
                                    </div>
                                }
                                else
                                {
                                    <div class="carousel-item">
                                        <img class="d-block w-100" style="border-radius:20px;"
                                            src="@imageUrl" alt="First slide">
                                    </div>
                                }

                                innerImageIndex++;
                            }
                        </div>
                        <a class="carousel-control-prev" href="#carouselExampleIndicators_@room.Id"
                            role="button" data-slide="prev">
                            <span class="carousel-control-prev-icon" aria-hidden="true"></span>
                            <span class="sr-only">Previous</span>
                        </a>
                        <a class="carousel-control-next" href="#carouselExampleIndicators_@room.Id"
                            role="button" data-slide="next">
                            <span class="carousel-control-next-icon" aria-hidden="true"></span>
                            <span class="sr-only">Next</span>
                        </a>
                    </div>
                </div>
}
- هرچند این قطعه کد، طولانی به نظر می‌رسد اما قسمت‌های مختلف آن صرفا بر اساس مستندات سایت بوت استرپ، جهت تشکیل ساختار ابتدایی و استاندارد کامپوننت carousel، تهیه شده‌اند.
- سپس در حلقه‌ای که برای نمایش لیست اتاق‌ها تهیه کرده‌ایم، قسمت‌های مختلف carousel را تکمیل می‌کنیم که در اینجا نیاز به ایندکس تصاویر، لیست تصاویر و یک Id منحصربفرد برای این carousel خاص را دارد تا بتوان چندین وهله از آن‌را در صفحه قرار داد که این id را بر اساس Id اتاق مشخص کرد‌ه‌ایم.

دو نکته:
- در این مثال برای تعریف لینک به تصاویر، کد زیر را مشاهده می‌کنید:
var imageUrl = $"{ImagesBaseAddress}/{image.RoomImageUrl}";
و این ImagesBaseAddress، به صورت زیر تعریف شده که همان آدرس برنامه‌ی blazor server ای است که مشخصات اتاق‌ها و تصاویر را ثبت می‌کند:
@code {
   string ImagesBaseAddress = "https://localhost:5006";
بنابراین اگر می‌خواهید تصاویر را هم مشاهده کنید، باید برنامه‌ی مجزای blazor server این سری نیز در حال اجرا باشد.
- کامپوننت carousel برای اجرا، نیاز به فایل lib/bootstrap/dist/js/bootstrap.bundle.min.js را نیز دارد. به همین جهت مدخل اسکریپت آن‌را باید به فایل wwwroot\index.html اضافه کرد.

ب) نمایش جزئیات نام و هزینه‌ی اتاق
قسمت دوم حلقه‌ی foreach نمایش لیست اتاق‌ها، جهت نمایش جزئیات هر اتاق تعریف شده‌است:
@foreach (var room in Rooms)
{
                <div class="col-12 col-lg-9 col-md-8">
                    <div class="row pt-3">
                        <div class="col-12 col-lg-8">
                            <p class="card-title text-warning" style="font-size:xx-large">@room.Name</p>
                            <p class="card-text">
                                @((MarkupString)room.Details)
                            </p>
                        </div>
                        <div class="col-12 col-lg-4">
                            <div class="row pb-3 pt-2">
                                <div class="col-12 col-lg-11 offset-lg-1">
                                    <a href="@($"hotel/room-details/{room.Id}")" class="btn btn-success btn-block">Book</a>
                                </div>
                            </div>
                            <div class="row ">
                                <div class="col-12 pb-5">
                                    <span class="float-right">
                                        <span class="float-right">Occupancy : @room.Occupancy adults </span><br />
                                        <span class="float-right pt-1">Room Size : @room.SqFt sqft</span><br />
                                        <h4 class="text-warning font-weight-bold pt-4">
                                            <span style="border-bottom:1px solid #ff6a00">
                                                @room.TotalAmount.ToString("#,#.00;(#,#.00#)")
                                            </span>
                                        </h4>
                                        <span class="float-right">Cost for  @room.TotalDays nights</span>
                                    </span>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
}
- در این مثال از MarkupString استفاده شده تا بتوان یک محتوای HTML ای را در صفحه نمایش داد.
- هر اتاق نمایش داده شده، لینکی را به صفحه‌ی خاص خودش نیز دارد که آن‌را در قسمت بعدی تکمیل می‌کنیم.
- در اینجا TotalAmount و TotalDays محاسباتی و قابل تغییر بر اساس انتخاب کاربر نیز درج شده‌اند.


یک تمرین: در برنامه‌ی Blazor Server، سرویسی را جهت درج مشخصات امکانات رفاهی هتل تهیه کردیم. این امکانات رفاهی را از طریق Web API برنامه دریافت و سپس در برنامه‌ی سمت کلاینت نمایش دهید.
بنابراین تکمیل این تمرین شامل تهیه‌ی موارد زیر است که کدنویسی آن، با دو قسمت اخیر این سری دقیقا یکی است و نکته‌ی جدیدی را به همراه ندارد (و کدهای کامل آن را از انتهای بحث می‌توانید دریافت کنید):
- تهیه‌ی HotelAmenityController در پروژه‌ی Web API که به کمک IAmenityService، لیست امکانات رفاهی را بازگشت می‌دهد.
- تهیه‌ی ‍ClientHotelAmenityService در پروژه‌ی WASM که همانند ClientHotelRoomService قسمت قبل ، از Web API، لیست HotelAmenityDTO‌ها را دریافت می‌کند.
- ثبت سرویس جدید ‍ClientHotelAmenityService در Program.cs.
- در آخر حلقه‌ای را بر روی لیست HotelAmenityDTO دریافتی از ClientHotelRoomService در کامپوننت Index.razor تشکیل داده و آن‌ها را نمایش می‌دهیم.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-28.zip
نظرات مطالب
چگونه نرم افزارهای تحت وب سریعتری داشته باشیم؟ قسمت سوم
1. اکثر کنترل‌های ASP.NET WebForm قابلیت bind به DataSet را دارد. اگر از آنها در این مدل استفاده نمایید کار صفحه بندی به عهده کنترل+DataSet می‌افتد.
2. صفحه بندی مجازی یعنی شما به کنترل می‌گویید کل اطلاعات شما مثلا 51 صفحه است، الان صفحه 4 را نمایش می‌دهی این هم اطلاعات صفحه 4. بعد وقتی کاربر بر روی گزینه صفحه بعد کلیک کرد، به کنترل می‌گویید کل اطلاعات 51 صفحه است، الان صفحه 5 را نمایش می‌دهی و این هم اطلاعات آن.
برای مثال می‌توانی به این مثال   در DataTables مراجعه کنید
نظرات مطالب
نحوه استفاده از ViewModel در ASP.NET MVC
سلاام ...
من تاجاییکه میتونستم چیزهایی که درباره viemodel  و automapper توی سایت بود رو خوندم ولی چیزی که میخواستم را پیدا نکردم شاید هم درک درستی ازش نداشتم در کل سوال سوال من از شما دوستان اینه : 
یک viewmodel دارم که از چندین جدول توی db ایجاد شده که در زیر نوشتمش : 
public class ResumeViewModel
    {
        public ResumeViewModel()
        {
            
        }
        public ResumeViewModel(IEnumerable<Resume> resum, IEnumerable<Work_Experience_Job_Seeker> workExperienceJobSeekerOfViewModel, IEnumerable<Job_Expertises> jobExpertisesOfViewModel, IEnumerable<Degrees_Work_Experience_Required> degreesWorkExperienceRequiredOfViewModel, IEnumerable<Specialized_Course> specializedCourseOfViewModel, IEnumerable<Book_Published> bookPublishedOfViewModel, IEnumerable<Basic_Table> basicTable)
        {
            ResumeOfViewModel = resum;
            WorkExperienceJobSeekerOfViewModel = workExperienceJobSeekerOfViewModel;
            JobExpertisesOfViewModel = jobExpertisesOfViewModel;
            DegreesWorkExperienceRequiredOfViewModel = degreesWorkExperienceRequiredOfViewModel;
            SpecializedCourseOfViewModel = specializedCourseOfViewModel;
            BookPublishedOfViewModel = bookPublishedOfViewModel;
            BasicTable = basicTable;
        }
        public IEnumerable<Resume> ResumeOfViewModel { get; set; }
        public IEnumerable<Work_Experience_Job_Seeker> WorkExperienceJobSeekerOfViewModel { get; set; }
        public IEnumerable<Job_Expertises> JobExpertisesOfViewModel { get; set; }
        public IEnumerable<Degrees_Work_Experience_Required> DegreesWorkExperienceRequiredOfViewModel { get; set; }
        public IEnumerable<Specialized_Course> SpecializedCourseOfViewModel { get; set; }
        public IEnumerable<Book_Published> BookPublishedOfViewModel { get; set; }
        public IEnumerable<Basic_Table> BasicTable { get; set; }

        public int NumberForm { get; set; } // EditResumes.chtml & ShowResumes.chtml  ===> baraye select kardan formha :D 

        }
و این viewmodel رو توی متد Edit Resume  استفاده کردم که متد get  بصورت زیر ::
        [HttpGet]
        public ActionResult EditResumes(int id)
        {
            var contex = new Final_My_ProjectEntities2();
            var res1 = contex.Resumes.Where(rec => rec.Resume_ID == id);
            var res2 = contex.Work_Experience_Job_Seeker.Where(rec => rec.Resume_ID == id).ToList();
            var res3 = contex.Job_Expertises.Where(rec => rec.Resume_ID == id).ToList();
            var res4 = contex.Degrees_Work_Experience_Required.Where(rec => rec.Resume_ID == id).ToList();
            var res5 = contex.Specialized_Course.Where(rec => rec.Resume_ID == id).ToList();
            var res6 = contex.Book_Published.Where(rec => rec.Resume_ID == id).ToList();
            var res12 = contex.Basic_Table.ToList();
            var viewModel = new ResumeViewModel(res1, res2, res3, res4, res5, res6,res12);
            var items = new SelectList(
                 new[]
                    {
                        new {Value = "1", Text = "فرم مهارت ها"},
                        new {Value = "2", Text = "فرم کتاب/مقالات منتشر شده"},
                        new {Value = "3", Text = "فرم سابقه کاری"},
                        new {Value = "4", Text = "فرم دوره‌های تخصصی گذرانده"},
                        new {Value = "5", Text = "فرم تخصص‌های شغلی"},
                        new {Value = "6", Text = "فرم مدارک تحصیلی"}
                    },
             "Value", "Text");

            ViewBag.Form = new SelectList(items, "Value", "Text");

            var res7 = contex.Basic_Table.Where(rec => rec.Domain == "MilitaryStatus").ToList();
            ViewBag.MilitaryStatus = new SelectList(res7, "Value", "Meaning",res1);

            var res8 = contex.Basic_Table.Where(rec => rec.Domain == "Sex");
            ViewBag.Sex = new SelectList(res8, "Value", "Meaning", res1);

            var res9 = contex.Basic_Table.Where(rec => rec.Domain == "MartialStatus").ToList();
            ViewBag.MartialStatus = new SelectList(res9, "Value", "Meaning", res1);

            var res10 = contex.Basic_Table.Where(rec => rec.Domain == "Degree").ToList();
            ViewBag.Degree = new SelectList(res10, "Value", "Meaning");

            var res11 = contex.Basic_Table.Where(rec => rec.Domain == "Ability").ToList();
            ViewBag.Ability = new SelectList(res11, "Value", "Meaning");


            return View(viewModel);
        }
و view  این متد بصورت زیر هست البته قسمتی از آن :: 
@model Final_My_Project.ViewModels.ResumeViewModel

@{
    ViewBag.Title = "ویرایش رزومه";
    ViewBag.PartOne = "فرم مهارت ها";
    ViewBag.PartTwo = "فرم کتاب/مقالات منتشر شده";
    ViewBag.Part3 = "فرم سابقه کاری";
    ViewBag.Part4 = "فرم دوره‌های تخصصی گذرانده";
    ViewBag.Part5 = "فرم تخصص‌های شغلی";
    ViewBag.Part6 = "فرم مدارک تحصیلی";

}
<h2 style="font-family:  Arial;">@ViewBag.Title</h2><br/>
   <script type="text/javascript">
       $(function () {
           $('#Gender').change(function () {
               var selectKind = $(this).find('option:selected').text();
               var divMilitary;
               if (selectKind == "زن") {
                   divMilitary = $('#Military');
                   divMilitary.hide();
                   divMilitary.css('display', 'none');
               }
               else if (selectKind == "مرد") {
                   divMilitary = $('#Military');
                   divMilitary.show();
                   divMilitary.css('display', 'block');

               }
           });
       });
</script>

<script type="text/javascript">
    $(function () {
        $('#SelectForm').change(function () {
            var selectFrom = $(this).find('option:selected').text();

            if (selectFrom == "فرم مهارت ها") {
                $('#PartOne').show();
                $('#PartOne').css('display', 'block');
                $('#PartTwo').hide();
                $('#PartTwo').css('display', 'none');
                $('#Part3').hide();
                $('#Part3').css('display', 'none');
                $('#Part4').hide();
                $('#Part4').css('display', 'none');
                $('#Part5').hide();
                $('#Part5').css('display', 'none');
                $('#Part6').hide();
                $('#Part6').css('display', 'none');


            }
            if (selectFrom == "فرم کتاب/مقالات منتشر شده") {
                $('#PartTwo').show();
                $('#PartTwo').css('display', 'block');
                $('#PartOne').show();
                $('#PartOne').css('display', 'none');
                $('#Part3').hide();
                $('#Part3').css('display', 'none');
                $('#Part4').hide();
                $('#Part4').css('display', 'none');
                $('#Part5').hide();
                $('#Part5').css('display', 'none');
                $('#Part6').hide();
                $('#Part6').css('display', 'none');

            }
            if (selectFrom == "فرم سابقه کاری") {

                $('#Part3').show();
                $('#Part3').css('display', 'block');
                $('#PartTwo').hide();
                $('#PartTwo').css('display', 'none');
                $('#PartOne').show();
                $('#PartOne').css('display', 'none');
                $('#Part4').hide();
                $('#Part4').css('display', 'none');
                $('#Part5').hide();
                $('#Part5').css('display', 'none');
                $('#Part6').hide();
                $('#Part6').css('display', 'none');


            }
            if (selectFrom == "فرم دوره‌های تخصصی گذرانده") {

                $('#Part4').show();
                $('#Part4').css('display', 'block');
                $('#PartTwo').hide();
                $('#PartTwo').css('display', 'none');
                $('#PartOne').show();
                $('#PartOne').css('display', 'none');
                $('#Part3').hide();
                $('#Part3').css('display', 'none');
                $('#Part5').hide();
                $('#Part5').css('display', 'none');
                $('#Part6').hide();
                $('#Part6').css('display', 'none');

            }
            if (selectFrom == "فرم تخصص‌های شغلی") {

                $('#Part5').show();
                $('#Part5').css('display', 'block');
                $('#PartTwo').hide();
                $('#PartTwo').css('display', 'none');
                $('#PartOne').show();
                $('#PartOne').css('display', 'none');
                $('#Part3').hide();
                $('#Part3').css('display', 'none');
                $('#Part4').hide();
                $('#Part4').css('display', 'none');
                $('#Part6').hide();
                $('#Part6').css('display', 'none');

            }

            if (selectFrom == "فرم مدارک تحصیلی") {

                $('#Part6').show();
                $('#Part6').css('display', 'block');
                $('#Part5').show();
                $('#Part5').css('display', 'none');
                $('#PartTwo').hide();
                $('#PartTwo').css('display', 'none');
                $('#PartOne').show();
                $('#PartOne').css('display', 'none');
                $('#Part3').hide();
                $('#Part3').css('display', 'none');
                $('#Part4').hide();
                $('#Part4').css('display', 'none');
            }

        });
    });

</script>

@Html.DropDownListFor(m=>m.NumberForm, (SelectList)ViewBag.Form, new { id = "SelectForm" })


@using (Html.BeginForm())
{
    @Html.ValidationSummary(true)

        <div id="PartOne" >
    <h3 style="font-family: Arial; color: #008080; font-weight: bold; ">@ViewBag.PartOne</h3><br/>
    @foreach (var item in Model.ResumeOfViewModel)
    {
        <table dir="rtl">
            <tr>
                <td>
                    @Html.Label("عنوان رزومه")
                </td>
                <td>
                    @Html.TextBoxFor(model =>item.Title_Of_Resume  , new {@class = "text", style = "width:100 px"})
                    @Html.ValidationMessageFor(model => item.Title_Of_Resume)
                </td>
            </tr>
            <tr>
                <td>
        <div id="Gender" >
        @Html.Label("نوع جنسیت")
         @Html.DropDownList("نوع جنسیت", new SelectList(ViewBag.Sex, 
                       "Value", "Text", item.Sex_ID == 0 ? 0 : item.Sex_ID))
        @Html.ValidationMessageFor(model => item.Sex_ID)
        </div>
    </td>

                <td>
                    <div >
                        @Html.Label("وضعیت تاهل")
         @Html.DropDownList("وضعیت تاهل", new SelectList(ViewBag.MartialStatus, 
                       "Value", "Text", item.Martial_Status_ID == 0 ? 0 : item.Martial_Status_ID))
                        @Html.ValidationMessageFor(model => item.Martial_Status_ID)
                    </div>

                </td>
            </tr>

            <tr id="Military" style="display: none;">
                <td>
                    @Html.Label("وضعیت نظام وظیفه")
                </td>
                <td>
         @Html.DropDownList("وضعیت نظام وظیفه", new SelectList(ViewBag.MilitaryStatus,
                           "Value", "Text", item.Military_Status_ID == 0 ? 0 : item.Military_Status_ID), new { id = "Gender" })
                    @Html.ValidationMessageFor(model => item.Military_Status_ID)
                </td>

            </tr>

            <tr>
                <td>
                    @Html.Label("آشنایی با رایانه")
                </td>
                <td>
                    @Html.DropDownListFor(model => item.Knowledge_Of_Computers_ID, (SelectList)ViewBag.Ability)
                    @Html.ValidationMessageFor(model => item.Knowledge_Of_Computers_ID)
                </td>
            </tr>

            <tr>
                <td>
                    @Html.Label("آشنایی با امور اداری و دفتری")
                </td>
                <td>
                    @Html.DropDownListFor(model => item.Knowledge_Administrative_and_Clerical_ID, (SelectList) ViewBag.Ability)
                    @Html.ValidationMessageFor(model => item.Knowledge_Administrative_and_Clerical_ID)
                </td>
            </tr>

            <tr>
                <td>
                    @Html.Label("آشنایی با زبان انگلیسی")
                </td>
                <td>
                    @Html.DropDownListFor(model => item.Knowledge_Of_English_ID, (SelectList) ViewBag.Ability)
                    @Html.ValidationMessageFor(model => item.Knowledge_Of_English_ID)
                </td>
            </tr>

            <tr>
                <td>
                    @Html.Label("آشنایی با زبان عربی")
                </td>
                <td>
                    @Html.DropDownListFor(model => item.Knowledge_Of_Arabic_ID, (SelectList) ViewBag.Ability)
                    @Html.ValidationMessageFor(model => item.Knowledge_Of_Arabic_ID)
                </td>
            </tr>

            <tr>
                <td>
                    @Html.Label("آشنایی با ماکروسافت آفیس")
                </td>
                <td>
                    @Html.DropDownListFor(model =>item.Knowledge_Of_Office_ID, (SelectList) ViewBag.Ability)
                    @Html.ValidationMessageFor(model => item.Knowledge_Of_Office_ID)
                </td>
            </tr>

            <tr>
                <td>  
                    @Html.Label("آشنایی با امور مالی و حسابداری")
                </td>
                <td>
                    @Html.DropDownListFor(model =>item.Knowledge_Of_Finance_ID, (SelectList) ViewBag.Ability)
                    @Html.ValidationMessageFor(model => item.Knowledge_Of_Finance_ID)
                </td>
            </tr>

            <tr>
                <td>
                    @Html.Label("آشنایی با مدیریت")
                </td>
                <td>
                    @Html.DropDownListFor(model =>item.Knowledge_Of_Manage_ID, (SelectList) ViewBag.Ability)
                    @Html.ValidationMessageFor(model =>item.Knowledge_Of_Manage_ID)
                </td>
            </tr>

            <tr>
                <td>
                    @Html.Label("گواهینامه رانندگی پایه یک")

                </td>
                <td>
                    @Html.DropDownListFor(model =>item.Driving_license_One_ID, (SelectList) ViewBag.Ability)
                    @Html.ValidationMessageFor(model =>item.Driving_license_One_ID)
                </td>
            </tr>

            <tr>
                <td>
                    @Html.Label("گواهینامه رانندگی پایه دو")
                </td>
                <td>
                    @Html.DropDownListFor(model =>item.Driving_license_Two_ID, (SelectList) ViewBag.Ability)
                    @Html.ValidationMessageFor(model =>item.Driving_license_Two_ID)
                </td>
            </tr>

            <tr>
                <td>
                    @Html.Label("گواهینامه رانندگی پایه موتورسیکلت")
                </td>
                <td>
                    @Html.DropDownListFor(model =>item.Certificate_Motor_ID, (SelectList) ViewBag.Ability)
                    @Html.ValidationMessageFor(model =>item.Certificate_Motor_ID)
                </td>
            </tr>

            <tr>
                <td>
                    @Html.Label("ماشین شخصی")
                </td>
                <td>
                    @Html.DropDownListFor(model =>item.Personal_Vehicle_ID, (SelectList) ViewBag.Ability)
                    @Html.ValidationMessageFor(model =>item.Personal_Vehicle_ID)
                </td>
            </tr>

            <tr>
                <td>
                    @Html.Label("روابط عمومی")
                </td>
                <td>
                    @Html.DropDownListFor(model =>item.Public_Relationship_ID, (SelectList) ViewBag.Ability)
                    @Html.ValidationMessageFor(model =>item.Public_Relationship_ID)
                </td>
            </tr>

            <tr>
                <td>
                    @Html.Label("دیگر توانایی ها")

                </td>
                <td>
                    @Html.EditorFor(model =>item.Etc_Ability)
                    @Html.ValidationMessageFor(model =>item.Etc_Ability)
                </td>
            </tr>

        </table>
    }
        </div>

    
        <p>
            <input type="submit" value="Save" onclick="return confirm('از ثبت اطلاعات مطمئن هستید؟')" />
        </p>
}

<div>
    @Html.ActionLink("بازگشت به مدیریت رزومه ها", "ManageOfResumes")
</div>

@section scripts {
    @Scripts.Render("~/bundles/jqueryval")
}
و مشکل اینجاست که بعد از ثبت اطلاعات وقتی به متد post میره مقدارش null  هستش ... درحالیکه فقط در صورت edit  اینجوریه وقتی از همین viewmodel  برای مشاهده رزومه که فقط گزارشگیریه استفاده میکنم نتیجه را میبینم ولی اینجا نه !
متد پست بصورت زیر هستش ... اگر میدونید چطور و چی کار کنم که این درست شه ممنون میشم ... چون دیگه نمیدونم تو متد پست چی بنویسم ... منتظر جوابم ... که چرا null میده و اینکه تو متد پست چطور اینارو ذخیره کنم توی db .../؟
        [HttpPost]
        public ActionResult EditResumes(ResumeViewModel model) // model null mishe ! CHERAA??!
        {
            var contex = new Final_My_ProjectEntities2();
            try
            {
                if (ModelState.IsValid)
                {

                    // Code... che cody?
                    contex.SaveChanges();
                }

            }
            catch (Exception)
            {

                ViewBag.wrong = "لطفا داده‌های ورودی را بررسی نمایید";
            }
            return View(model);

        }