مطالب
Blazor 5x - قسمت 17 - کار با فرم‌ها - بخش 5 - آپلود تصاویر
از زمان Blazor 5x، پشتیبانی توکار از آپلود فایل‌ها، به آن اضافه شده‌است و پیش از آن می‌بایستی از کامپوننت‌های ثالث استفاده می‌شد. در این قسمت نحوه‌ی استفاده از کامپوننت آپلود فایل‌های Blazor را بررسی می‌کنیم. همچنین یک نمونه مثال، از فرم‌های master-details را نیز با هم مرور خواهیم کرد.



افزودن فیلد آپلود تصاویر، به فرم ثبت اطلاعات یک اتاق

در ادامه به کامپوننت Pages\HotelRoom\HotelRoomUpsert.razor که تا این قسمت آن‌را تکمیل کرده‌ایم مراجعه کرده و فیلد جدید InputFile را ذیل قسمت ثبت توضیحات، اضافه می‌کنیم:
<div class="form-group">
    <InputFile OnChange="HandleImageUpload" multiple></InputFile>
</div>

@code
{
    private async Task HandleImageUpload(InputFileChangeEventArgs args)
    {

    }
}
- ذکر ویژگی multiple در اینجا سبب می‌شود تا بتوان بیش از یک فایل را هربار انتخاب و آپلود کرد.
- در این کامپوننت، رویداد OnChange، پس از تغییر مجموعه‌ی فایل‌های اضافه شده‌ی به آن، فراخوانی می‌شود و آرگومانی از نوع InputFileChangeEventArgs را دریافت می‌کند.


افزودن لیست فایل‌های انتخابی به HotelRoomDTO

تا اینجا اگر به BlazorServer.Models\HotelRoomDTO.cs مراجعه کنیم (کلاسی که مدل UI فرم ثبت اطلاعات اتاق را فراهم می‌کند)، امکان افزودن لیست تصاویر انتخابی به آن وجود ندارد. به همین جهت در این کلاس، تغییر زیر را اعمال می‌کنیم:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace BlazorServer.Models
{
    public class HotelRoomDTO
    {
        // ... 
        public virtual ICollection<HotelRoomImageDTO> HotelRoomImages { get; set; } = new List<HotelRoomImageDTO>();
    }
}
HotelRoomImageDTO را در قسمت قبل اضافه کردیم. متناظر با ICollection فوق، چنین خاصیتی در موجودیت HotelRoom که از نوع <ICollection<HotelRoomImage است نیز تعریف شده‌است تا بتوان به ازای هر اتاق، مشخصات تعدادی تصویر را در بانک اطلاعاتی ذخیره کرد.


تکمیل متد رویدادگردان HandleImageUpload

در ادامه، لیست فایل‌ها‌ی انتخاب شده‌ی توسط کاربر را دریافت کرده و آن‌ها را آپلود می‌کنیم:
@inject IHotelRoomService HotelRoomService
@inject NavigationManager NavigationManager
@inject IJSRuntime JsRuntime
@inject IFileUploadService FileUploadService
@inject IWebHostEnvironment WebHostEnvironment

@code
{
    // ...

    private async Task HandleImageUpload(InputFileChangeEventArgs args)
    {
        var files = args.GetMultipleFiles(maximumFileCount: 5);
        if (args.FileCount == 0 || files.Count == 0)
        {
            return;
        }

        var allowedExtensions = new List<string> { ".jpg", ".png", ".jpeg" };
        if(!files.Any(file => allowedExtensions.Contains(Path.GetExtension(file.Name), StringComparer.OrdinalIgnoreCase)))
        {
            await JsRuntime.ToastrError("Please select .jpg/.jpeg/.png files only.");
            return;
        }

        foreach (var file in files)
        {
            var uploadedImageUrl = await FileUploadService.UploadFileAsync(file, WebHostEnvironment.WebRootPath, "Uploads");
            HotelRoomModel.HotelRoomImages.Add(new HotelRoomImageDTO { RoomImageUrl = uploadedImageUrl });
        }
    }
}
- در اینجا نیاز به تزریق چند سرویس جدید هست؛ مانند IFileUploadService که در قسمت قبل تکمیل کردیم و سرویس توکار IWebHostEnvironment. به همین جهت به فایل BlazorServer.App\_Imports.razor مراجعه کرده و فضاهای نام متناظر زیر را اضافه می‌کنیم:
@using Microsoft.AspNetCore.Hosting
@using System.Linq
@using System.IO
برای مثال سرویس IWebHostEnvironment که از آن برای دسترسی به WebRootPath یا محل قرارگیری پوشه‌ی wwwroot استفاده می‌کنیم، در فضای نام Microsoft.AspNetCore.Hosting قرار دارد و یا متد Path.GetExtension در فضای نام System.IO و متد الحاقی Contains با دو پارامتر استفاده شده، در فضای نام System.Linq قرار دارند.
- متد ()args.GetMultipleFiles، امکان دسترسی به فایل‌های انتخابی توسط کاربر را میسر می‌کند که خروجی آن از نوع <IReadOnlyList<IBrowserFile است. در قسمت قبل، سرویس آپلود فایل‌هایی را که تکمیل کردیم، امکان آپلود یک IBrowserFile را به سرور میسر می‌کند. اگر متد ()GetMultipleFiles را بدون پارامتری فراخوانی کنیم، حداکثر 10 فایل را قبول می‌کند و اگر تعداد بیشتری انتخاب شده باشد، یک استثناء را صادر خواهد کرد.
- سپس بر اساس پسوند فایل‌های دریافتی، آن‌ها را صرفا به فایل‌های تصویری محدود کرده‌ایم.
- در آخر، لیست فایل‌های دریافتی را یکی یکی به سرور آپلود کرده و Url دسترسی به آن‌ها را به لیست HotelRoomImages اضافه می‌کنیم. فایل‌های آپلود شده در پوشه‌ی BlazorServer.App\wwwroot\Uploads قابل مشاهده هستند.


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


در ادامه می‌خواهیم پس از آپلود فایل‌ها، آن‌ها را در ذیل کامپوننت InputFile نمایش دهیم. برای اینکار در ابتدا به فایل wwwroot\css\site.css مراجعه کرده و شیوه نامه‌ی نمایش تصاویر و عناوین آن‌ها را اضافه می‌کنیم:
.room-image {
  display: block;
  width: 100%;
  height: 150px;
  background-size: cover !important;
  border: 3px solid green;
  position: relative;
}

.room-image-title {
  position: absolute;
  top: 0;
  right: 0;
  background-color: green;
  color: white;
  padding: 0px 6px;
  display: inline-block;
}
سپس بر روی لیست HotelRoomModel.HotelRoomImages که در متد HandleImageUpload آن‌را تکمیل کردیم، حلقه‌ای را ایجاد کرده و تصاویر را بر اساس RoomImageUrl آن‌ها، نمایش می‌دهیم:
<div class="form-group">
    <InputFile OnChange="HandleImageUpload" multiple></InputFile>
    <div class="row">
    @if (HotelRoomModel.HotelRoomImages.Count > 0)
    {
        var serial = 1;
        foreach (var roomImage in HotelRoomModel.HotelRoomImages)
        {
            <div class="col-md-2 mt-3">
                <div class="room-image" style="background: url('@roomImage.RoomImageUrl') 50% 50%; ">
                   <span class="room-image-title">@serial</span>
                </div>
                <button type="button" class="btn btn-outline-danger btn-block mt-4">Delete</button>
            </div>
            serial++;
        }
    }
    </div>
</div>

ذخیره سازی اطلاعات تصاویر آپلودی یک اتاق در بانک اطلاعاتی

تا اینجا موفق شدیم تصاویر انتخابی کاربر را آپلود کرده و همچنین لیست آن‌ها را نیز نمایش دهیم. در ادامه نیاز است تا این اطلاعات را در بانک اطلاعاتی ثبت کنیم. به همین جهت ابتدا سرویس IHotelRoomImageService را که در قسمت قبل تکمیل کردیم، به کامپوننت جاری تزریق می‌کنیم و سپس با استفاده از متد CreateHotelRoomImageAsync، رکوردهای تصویر متناظر با اتاق ثبت شده را اضافه می‌کنیم:
// ...
@inject IHotelRoomImageService HotelRoomImageService


@code
{
    // ...

    private async Task AddHotelRoomImageAsync(HotelRoomDTO roomDto)
    {
        foreach (var imageDto in HotelRoomModel.HotelRoomImages)
        {
            imageDto.RoomId = roomDto.Id;
            await HotelRoomImageService.CreateHotelRoomImageAsync(imageDto);
        }
    }
}
در حین آپلود فایل‌ها، فقط خاصیت RoomImageUrl را مقدار دهی کردیم:
HotelRoomModel.HotelRoomImages.Add(new HotelRoomImageDTO { RoomImageUrl = uploadedImageUrl });
در اینجا RoomId هر imageDto را نیز بر اساس Id واقعی اتاق ثبت شده‌ی جاری، تکمیل کرده و سپس آن‌را به CreateHotelRoomImageAsync ارسال می‌کنیم.

محل فراخوانی AddHotelRoomImageAsync فوق، در متد HandleHotelRoomUpsert است که در قسمت‌های قبل تکمیل کردیم. در اینجا پس از ثبت اطلاعات اتاق در بانک اطلاعاتی است که به Id آن دسترسی پیدا می‌کنیم:
private async Task HandleHotelRoomUpsert()
    {
       // ...

       // Create Mode
       var createdRoomDto = await HotelRoomService.CreateHotelRoomAsync(HotelRoomModel);
       await AddHotelRoomImageAsync(createdRoomDto);
       await JsRuntime.ToastrSuccess($"The `{HotelRoomModel.Name}` created successfully.");

       // ... 
    }
اکنون اگر اطلاعات اتاق جدیدی را تکمیل کرده و تصاویری را نیز به آن انتساب دهیم، با کلیک بر روی دکمه‌ی ثبت، ابتدا اطلاعات این اتاق در بانک اطلاعاتی ثبت شده و Id آن به‌دست می‌آید، سپس رکوردهای تصویر آن جداگانه ذخیره خواهند شد.

یک نکته: در انتهای بحث خواهیم دید که اینکار غیرضروری است و با وجود رابطه‌ی one-to-many تعریف شده‌ی توسط EF-Core، اگر لیست HotelRoomImages موجودیت اتاق تعریف شده و در حال ثبت نیز مقدار دهی شده باشد، به صورت خودکار جزئی از این رابطه و تنها در یک رفت و برگشت، ثبت می‌شود. یعنی همان متد CreateHotelRoomAsync، قابلیت ثبت خودکار اطلاعات خاصیت HotelRoomImages موجودیت اتاق را نیز دارا است.


نمایش تصاویر یک اتاق، در حالت ویرایش رکورد آن

تا اینجا فقط حالت ثبت یک رکورد جدید را پوشش دادیم. در این حالت اگر به لیست اتاق‌های ثبت شده مراجعه کرده و بر روی دکمه‌ی edit یکی از آن‌ها کلیک کنیم، به صفحه‌ی ویرایش رکورد منتقل خواهیم شد؛ اما این صفحه، فاقد اطلاعات تصاویر منتسب به آن رکورد است.
علت اینجا است که در حین ویرایش اطلاعات، در متد OnInitializedAsync، هرچند اطلاعات یک اتاق را از بانک اطلاعاتی دریافت کرده و آن‌را تبدیل به Dto آن می‌کنیم که سبب نمایش جزئیات هر خاصیت در فیلد متصل به آن در فرم جاری می‌شود:
    protected override async Task OnInitializedAsync()
    {
        if (Id.HasValue)
        {
            // Update Mode
            Title = "Update";
            HotelRoomModel = await HotelRoomService.GetHotelRoomAsync(Id.Value);
        }
        // ...
    }
اما چون یک رابطه‌ی one-to-many بین اتاق و تصاویر آن برقرار است، نیاز است این رابطه را از طریق eager-loading و فراخوانی متد Include، واکشی کنیم تا اینبار زمانیکه GetHotelRoomAsync فراخوانی می‌شود، به همراه اطلاعات navigation property لیست تصاویر اتاق (HotelRoomImages) نیز باشد.
بنابراین به فایل BlazorServer\BlazorServer.Services\HotelRoomService.cs مراجعه کرده و تغییرات زیر را اعمال می‌کنیم:
namespace BlazorServer.Services
{
    public class HotelRoomService : IHotelRoomService
    {
        // ...
 
        public IAsyncEnumerable<HotelRoomDTO> GetAllHotelRoomsAsync()
        {
            return _dbContext.HotelRooms
                        .Include(x => x.HotelRoomImages)
                        .ProjectTo<HotelRoomDTO>(_mapperConfiguration)
                        .AsAsyncEnumerable();
        }

        public Task<HotelRoomDTO> GetHotelRoomAsync(int roomId)
        {
            return _dbContext.HotelRooms
                            .Include(x => x.HotelRoomImages)
                            .ProjectTo<HotelRoomDTO>(_mapperConfiguration)
                            .FirstOrDefaultAsync(x => x.Id == roomId);
        }
    }
}
در اینجا تنها تغییری که صورت گرفته، استفاده از متد Include(x => x.HotelRoomImages) است؛ تا هنگامیکه اطلاعات یک اتاق را واکشی می‌کنیم، به صورت خودکار اطلاعات تصاویر مرتبط به آن نیز واکشی گردد و سپس توسط AutoMapper، به Dto آن انتساب داده شود (یعنی انتساب HotelRoomImages موجودیت اتاق، به همین خاصیت در DTO آن). این انتساب، سبب به روز رسانی خودکار UI نیز می‌شود. یعنی برای نمایش تصاویر مرتبط با یک اتاق، همان کدهای قبلی که پیشتر داشتیم، هنوز هم کار می‌کنند.


افزودن تصاویر جدید، در حین ویرایش یک رکورد

پس از نمایش لیست تصاویر منتسب به یک اتاق در حال ویرایش، اکنون می‌خواهیم در همین حالت اگر کاربر تصویر جدیدی را انتخاب کرد، این تصویر را نیز به لیست تصاویر ثبت شده‌ی در بانک اطلاعاتی اضافه کنیم. برای اینکار نیز به متد HandleHotelRoomUpsert مراجعه کرده و از متد AddHotelRoomImageAsync در قسمت به روز رسانی آن استفاده می‌کنیم:
private async Task HandleHotelRoomUpsert()
{
   //...

   // Update Mode
   var updatedRoomDto = await HotelRoomService.UpdateHotelRoomAsync(HotelRoomModel.Id, HotelRoomModel);
   await AddHotelRoomImageAsync(updatedRoomDto);
   await JsRuntime.ToastrSuccess($"The `{HotelRoomModel.Name}` updated successfully.");

   //...
}
مشکل! اگر از این روش استفاده کنیم، هربار به روز رسانی اطلاعات یک جدول، به همراه ثبت رکوردهای تکراری نمایش داده شده‌ی در حالت ویرایش هم خواهند بود. برای مثال فرض کنید سه تصویر را به یک اتاق انتساب داده‌اید. در حالت ویرایش، ابتدا این سه تصویر نمایش داده می‌شوند. بنابراین در لیست HotelRoomModel.HotelRoomImages وجود خواهند داشت. اکنون کاربر دو تصویر جدید دیگر را هم به این لیست اضافه می‌کند. در زمان ثبت، در متد AddHotelRoomImageAsync، بررسی نمی‌کنیم که این تصویر اضافه شده، جدید است یا خیر  و یا همان سه تصویر ابتدای کار نمایش فرم در حالت ویرایش هستند. به همین جهت رکوردها، تکراری ثبت می‌شوند.
برای رفع این مشکل می‌توان در متد AddHotelRoomImageAsync، جدید بودن یک تصویر را بر اساس RoomId آن بررسی کرد. اگر این RoomId مساوی صفر بود، یعنی تازه به لیست اضافه شده‌است و حاصل بارگذاری اولیه‌ی فرم ویرایش اطلاعات نیست:
    private async Task AddHotelRoomImageAsync(HotelRoomDTO roomDto)
    {
        foreach (var imageDto in HotelRoomModel.HotelRoomImages.Where(x => x.RoomId == 0))
        {
            imageDto.RoomId = roomDto.Id;
            await HotelRoomImageService.CreateHotelRoomImageAsync(imageDto);
        }
    }
در قسمت بعد، کدهای حذف اطلاعات اتاق‌ها و تصاویر مرتبط با هر کدام را نیز تکمیل خواهیم کرد.


یک نکته: متد AddHotelRoomImageAsync اضافی است!

چون از AutoMapper استفاده می‌کنیم، در ابتدای متد ثبت یک اتاق، کار نگاشت DTO، به موجودیت متناظر با آن انجام می‌شود:
public async Task<HotelRoomDTO> CreateHotelRoomAsync(HotelRoomDTO hotelRoomDTO)
{
   var hotelRoom = _mapper.Map<HotelRoom>(hotelRoomDTO);
یعنی در اینجا چون خاصیت مجموعه‌ای HotelRoomImages موجود در HotelRoomDTO با نمونه‌ی مشابه آن در HotelRoom هم نام است، به صورت خودکار توسط AutoMapper به آن انتساب داده می‌شود و چون رابطه‌ی one-to-many در EF-Core تنظیم شده، همینقدر که hotelRoom حاصل، به همراه HotelRoomImages از پیش مقدار مقدار دهی شده‌است، به صورت خودکار آن‌ها را جزئی از اطلاعات همین اتاق ثبت می‌کند.
مقدار دهی RoomId یک تصویر، در اینجا غیرضروری است؛ چون RoomId و Room، به عنوان کلید خارجی این رابطه تعریف شده‌اند که در اینجا Room یک تصویر، دقیقا همین اتاق در حال ثبت است و EF Core در حین ثبت نهایی، آن‌را به صورت خودکار در تمام تصاویر مرتبط نیز مقدار دهی می‌کند.
یعنی نیازی به چندین بار رفت و برگشت تعریف شده‌ی در متد AddHotelRoomImageAsync نیست و اساسا نیازی به آن نیست؛ نه برای ثبت و نه برای ویرایش اطلاعات!


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-17.zip
مطالب
EF Code First #7

مدیریت روابط بین جداول در EF Code first به کمک Fluent API

EF Code first بجای اتلاف وقت شما با نوشتن فایل‌های XML تهیه نگاشت‌ها یا تنظیم آن‌ها با کد، رویه Convention over configuration را پیشنهاد می‌دهد. همین رویه، جهت مدیریت روابط بین جداول نیز برقرار است. روابط one-to-one، one-to-many، many-to-many و موارد دیگر را بدون یک سطر تنظیم اضافی، صرفا بر اساس یک سری قراردادهای توکار می‌تواند تشخیص داده و اعمال کند. عموما زمانی نیاز به تنظیمات دستی وجود خواهد داشت که قراردادهای توکار رعایت نشوند و یا برای مثال قرار است با یک بانک اطلاعاتی قدیمی از پیش موجود کار کنیم.


مفاهیمی به نام‌های Principal و Dependent

در EF Code first از یک سری واژه‌های خاص جهت بیان ابتدا و انتهای روابط استفاده شده است که عدم آشنایی با آن‌ها درک خطاهای حاصل را مشکل می‌کند:
الف) Principal : طرفی از رابطه است که ابتدا در بانک اطلاعاتی ذخیره خواهد شد.
ب) Dependent : طرفی از رابطه است که پس از ثبت Principal در بانک اطلاعاتی ذخیره می‌شود.
Principal می‌تواند بدون نیاز به Dependent وجود داشته باشد. وجود Dependent بدون Principal ممکن نیست زیرا ارتباط بین این دو توسط یک کلید خارجی تعریف می‌شود.


کدهای مثال مدیریت روابط بین جداول

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

using System.Collections.Generic;

namespace EF_Sample35.Models
{
public class Customer
{
public int Id { set; get; }
public string FirstName { set; get; }
public string LastName { set; get; }

public virtual AlimentaryHabits AlimentaryHabits { set; get; }
public virtual ICollection<CustomerAlias> Aliases { get; set; }
public virtual ICollection<Role> Roles { get; set; }
public virtual Address Address { get; set; }
}
}

namespace EF_Sample35.Models
{
public class CustomerAlias
{
public int Id { get; set; }
public string Aka { get; set; }

public virtual Customer Customer { get; set; }
}
}

using System.Collections.Generic;

namespace EF_Sample35.Models
{
public class Role
{
public int Id { set; get; }
public string Name { set; get; }

public virtual ICollection<Customer> Customers { set; get; }
}
}

namespace EF_Sample35.Models
{
public class AlimentaryHabits
{
public int Id { get; set; }
public bool LikesPasta { get; set; }
public bool LikesPizza { get; set; }
public int AverageDailyCalories { get; set; }

public virtual Customer Customer { get; set; }
}
}

using System.Collections.Generic;

namespace EF_Sample35.Models
{
public class Address
{
public int Id { set; get; }
public string City { set; get; }
public string StreetAddress { set; get; }
public string PostalCode { set; get; }

public virtual ICollection<Customer> Customers { set; get; }
}
}



همچنین تعاریف نگاشت‌های برنامه نیز مطابق کد‌های زیر است:

using System.Data.Entity.ModelConfiguration;
using EF_Sample35.Models;

namespace EF_Sample35.Mappings
{
public class CustomerAliasConfig : EntityTypeConfiguration<CustomerAlias>
{
public CustomerAliasConfig()
{
// one-to-many
this.HasRequired(x => x.Customer)
.WithMany(x => x.Aliases)
.WillCascadeOnDelete();
}
}
}

using System.Data.Entity.ModelConfiguration;
using EF_Sample35.Models;

namespace EF_Sample35.Mappings
{
public class CustomerConfig : EntityTypeConfiguration<Customer>
{
public CustomerConfig()
{
// one-to-one
this.HasOptional(x => x.AlimentaryHabits)
.WithRequired(x => x.Customer)
.WillCascadeOnDelete();

// many-to-many
this.HasMany(p => p.Roles)
.WithMany(t => t.Customers)
.Map(mc =>
{
mc.ToTable("RolesJoinCustomers");
mc.MapLeftKey("RoleId");
mc.MapRightKey("CustomerId");
});

// many-to-one
this.HasOptional(x => x.Address)
.WithMany(x => x.Customers)
.WillCascadeOnDelete();
}
}
}


به همراه Context زیر:

using System.Data.Entity;
using System.Data.Entity.Migrations;
using EF_Sample35.Mappings;
using EF_Sample35.Models;

namespace EF_Sample35.DataLayer
{
public class Sample35Context : DbContext
{
public DbSet<AlimentaryHabits> AlimentaryHabits { set; get; }
public DbSet<Customer> Customers { set; get; }

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new CustomerConfig());
modelBuilder.Configurations.Add(new CustomerAliasConfig());

base.OnModelCreating(modelBuilder);
}
}

public class Configuration : DbMigrationsConfiguration<Sample35Context>
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
AutomaticMigrationDataLossAllowed = true;
}

protected override void Seed(Sample35Context context)
{
base.Seed(context);
}
}
}


که نهایتا منجر به تولید چنین ساختاری در بانک اطلاعاتی می‌گردد:



توضیحات کامل کدهای فوق:

تنظیمات روابط one-to-one و یا one-to-zero

زمانیکه رابطه‌ای 0..1 و یا 1..1 است، مطابق قراردادهای توکار EF Code first تنها کافی است یک navigation property را که بیانگر ارجاعی است به شیء دیگر، تعریف کنیم (در هر دو طرف رابطه).
برای مثال در مدل‌های فوق یک مشتری که در حین ثبت اطلاعات اصلی او، «ممکن است» اطلاعات جانبی دیگری (AlimentaryHabits) نیز از او تنها در طی یک رکورد، دریافت شود. قصد هم نداریم یک ComplexType را تعریف کنیم. نیاز است جدول AlimentaryHabits جداگانه وجود داشته باشد.

namespace EF_Sample35.Models
{
public class Customer
{
// ...
public virtual AlimentaryHabits AlimentaryHabits { set; get; }
}
}

namespace EF_Sample35.Models
{
public class AlimentaryHabits
{
// ...
public virtual Customer Customer { get; set; }
}
}

در اینجا خواص virtual تعریف شده در دو طرف رابطه، به EF خواهد گفت که رابطه‌ای، 1:1 برقرار است. در این حالت اگر برنامه را اجرا کنیم، به خطای زیر برخواهیم خورد:

Unable to determine the principal end of an association between 
the types 'EF_Sample35.Models.Customer' and 'EF_Sample35.Models.AlimentaryHabits'.
The principal end of this association must be explicitly configured using either
the relationship fluent API or data annotations.

EF تشخیص داده است که رابطه 1:1 برقرار است؛ اما با قاطعیت نمی‌تواند طرف Principal را تعیین کند. بنابراین باید اندکی به او کمک کرد:

using System.Data.Entity.ModelConfiguration;
using EF_Sample35.Models;

namespace EF_Sample35.Mappings
{
public class CustomerConfig : EntityTypeConfiguration<Customer>
{
public CustomerConfig()
{
// one-to-one
this.HasOptional(x => x.AlimentaryHabits)
.WithRequired(x => x.Customer)
.WillCascadeOnDelete();
}
}
}


همانطور که ملاحظه می‌کنید در اینجا توسط متد WithRequired طرف Principal و توسط متد HasOptional، طرف Dependent تعیین شده است. به این ترتیب EF می‌توان یک رابطه 1:1 را تشکیل دهید.
توسط متد WillCascadeOnDelete هم مشخص می‌کنیم که اگر Principal حذف شد، لطفا Dependent را به صورت خودکار حذف کن.

توضیحات ساختار جداول تشکیل شده:
هر دو جدول با همان خواص اصلی که در دو کلاس وجود دارند، تشکیل شده‌اند.
فیلد Id جدول AlimentaryHabits اینبار دیگر Identity نیست. اگر به تعریف قید FK_AlimentaryHabits_Customers_Id دقت کنیم، در اینجا مشخص است که فیلد Id جدول AlimentaryHabits، به فیلد Id جدول مشتری‌ها متصل شده است (یعنی در آن واحد هم primary key است و هم foreign key). به همین جهت به این روش one-to-one association with shared primary key هم گفته می‌شود (کلید اصلی جدول مشتری با جدول AlimentaryHabits به اشتراک گذاشته شده است).


تنظیمات روابط one-to-many

برای مثال همان مشتری فوق را درنظر بگیرید که دارای تعدادی نام مستعار است:

using System.Collections.Generic;

namespace EF_Sample35.Models
{
public class Customer
{
// ...
public virtual ICollection<CustomerAlias> Aliases { get; set; }
}
}

namespace EF_Sample35.Models
{
public class CustomerAlias
{
// ...
public virtual Customer Customer { get; set; }
}
}

همین میزان تنظیم کفایت می‌کند و نیازی به استفاده از Fluent API برای معرفی روابط نیست.
در طرف Principal، یک مجموعه یا لیستی از Dependent وجود دارد. در Dependent هم یک navigation property معرف طرف Principal اضافه شده است.
جدول CustomerAlias اضافه شده، توسط یک کلید خارجی به جدول مشتری مرتبط می‌شود.

سؤال: اگر در اینجا نیز بخواهیم CascadeOnDelete را اعمال کنیم، چه باید کرد؟
پاسخ: جهت سفارشی سازی نحوه تعاریف روابط حتما نیاز به استفاده از Fluent API به نحو زیر می‌باشد:

using System.Data.Entity.ModelConfiguration;
using EF_Sample35.Models;

namespace EF_Sample35.Mappings
{
public class CustomerAliasConfig : EntityTypeConfiguration<CustomerAlias>
{
public CustomerAliasConfig()
{
// one-to-many
this.HasRequired(x => x.Customer)
.WithMany(x => x.Aliases)
.WillCascadeOnDelete();
}
}
}

اینکار را باید در کلاس تنظیمات CustomerAlias انجام داد تا بتوان Principal را توسط متد HasRequired به Customer و سپس dependent را به کمک متد WithMany مشخص کرد. در ادامه می‌توان متد WillCascadeOnDelete یا هر تنظیم سفارشی دیگری را نیز اعمال نمود.
متد HasRequired سبب خواهد شد فیلد Customer_Id، به صورت not null در سمت بانک اطلاعاتی تعریف شود؛ متد HasOptional عکس آن است.


تنظیمات روابط many-to-many

برای تنظیم روابط many-to-many تنها کافی است دو سر رابطه ارجاعاتی را به یکدیگر توسط یک لیست یا مجموعه داشته باشند:

using System.Collections.Generic;

namespace EF_Sample35.Models
{
public class Role
{
// ...
public virtual ICollection<Customer> Customers { set; get; }
}
}

using System.Collections.Generic;

namespace EF_Sample35.Models
{
public class Customer
{
// ...
public virtual ICollection<Role> Roles { get; set; }
}
}

همانطور که مشاهده می‌کنید، یک مشتری می‌تواند چندین نقش داشته باشد و هر نقش می‌تواند به چندین مشتری منتسب شود.
اگر برنامه را به این ترتیب اجرا کنیم، به صورت خودکار یک رابطه many-to-many تشکیل خواهد شد (بدون نیاز به تنظیمات نگاشت‌های آن). نکته جالب آن تشکیل خودکار جدول ارتباط دهنده واسط یا اصطلاحا join-table می‌باشد:

CREATE TABLE [dbo].[RolesJoinCustomers](
[RoleId] [int] NOT NULL,
[CustomerId] [int] NOT NULL,
)

سؤال: نام‌های خودکار استفاده شده را می‌خواهیم تغییر دهیم. چکار باید کرد؟
پاسخ: اگر بانک اطلاعاتی برای بار اول است که توسط این روش تولید می‌شود شاید این پیش فرض‌ها اهمیتی نداشته باشد و نسبتا هم مناسب هستند. اما اگر قرار باشد از یک بانک اطلاعاتی موجود که امکان تغییر نام فیلدها و جداول آن وجود ندارد استفاده کنیم، نیاز به سفارشی سازی تعاریف نگاشت‌ها به کمک Fluent API خواهیم داشت:

using System.Data.Entity.ModelConfiguration;
using EF_Sample35.Models;

namespace EF_Sample35.Mappings
{
public class CustomerConfig : EntityTypeConfiguration<Customer>
{
public CustomerConfig()
{
// many-to-many
this.HasMany(p => p.Roles)
.WithMany(t => t.Customers)
.Map(mc =>
{
mc.ToTable("RolesJoinCustomers");
mc.MapLeftKey("RoleId");
mc.MapRightKey("CustomerId");
});
}
}
}


تنظیمات روابط many-to-one

در تکمیل مدل‌های مثال جاری، به دو کلاس زیر خواهیم رسید. در اینجا تنها در کلاس مشتری است که ارجاعی به کلاس آدرس او وجود دارد. در کلاس آدرس، یک navigation property همانند حالت 1:1 تعریف نشده است:

namespace EF_Sample35.Models
{
public class Address
{
public int Id { set; get; }
public string City { set; get; }
public string StreetAddress { set; get; }
public string PostalCode { set; get; }
}
}

using System.Collections.Generic;

namespace EF_Sample35.Models
{
public class Customer
{
// …
public virtual Address Address { get; set; }
}
}

این رابطه توسط EF Code first به صورت خودکار به یک رابطه many-to-one تفسیر خواهد شد و نیازی به تنظیمات خاصی ندارد.
زمانیکه جداول برنامه تشکیل شوند، جدول Addresses موجودیتی مستقل خواهد داشت و جدول مشتری با یک فیلد به نام Address_Id به جدول آدرس‌ها متصل می‌گردد. این فیلد نال پذیر است؛ به عبارتی ذکر آدرس مشتری الزامی نیست.
اگر نیاز بود این تعاریف نیز توسط Fluent API سفارشی شوند، باید خاصیت public virtual ICollection<Customer> Customers به کلاس Address نیز اضافه شود تا بتوان رابطه زیر را توسط کدهای برنامه تعریف کرد:

using System.Data.Entity.ModelConfiguration;
using EF_Sample35.Models;

namespace EF_Sample35.Mappings
{
public class CustomerConfig : EntityTypeConfiguration<Customer>
{
public CustomerConfig()
{
// many-to-one
this.HasOptional(x => x.Address)
.WithMany(x => x.Customers)
.WillCascadeOnDelete();
}
}
}

متد HasOptional سبب می‌شود تا فیلد Address_Id اضافه شده به جدول مشتری‌ها، null پذیر شود.

مطالب
React 16x - قسمت 31 - React Hooks - بخش 2 - مقایسه حالت‌های مختلف مدیریت حالت با useState Hook
در قسمت قبل، با useState Hook آشنا شدیم. همچنین چندین مثال را در مورد نحوه‌ی تعریف تکی و یا چندتایی آن در یک کامپوننت تابعی، با انواع و اقسام داده‌های مختلف، بررسی کردیم؛ اما بهتر است از کدام حالت استفاده شود؟ آیا بهتر است به ازای هر خاصیت state، یکبار useState Hook جدیدی را تعریف کنیم و یا بهتر است همانند کامپوننت‌های کلاسی، یک شیء کامل را به همراه چندین خاصیت، به یک تک useState Hook معرفی کنیم؟


پیاده سازی یک فرم لاگین با استفاده از چندین useState Hook

در ابتدا، یک مثال کاربردی‌تر را به کمک useState Hook‌ها پیاده سازی می‌کنیم. در اینجا هر المان فرم را به یک useState Hook مجزا، متصل کرده‌ایم. کدهای کامل این کامپوننت را در ادامه مشاهده می‌کنید:
import React, { useState } from "react";

export default function Login() {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [user, setUser] = useState(null);

  const handleSubmit = event => {
    event.preventDefault();

    const userData = {
      username,
      password
    };
    setUser(userData);

    setUsername("");
    setPassword("");
  };

  return (
    <>
      <h2 className="mt-3">Login</h2>
      <form onSubmit={handleSubmit}>
        <div className="form-group">
          <label htmlFor="username">Username</label>
          <input
            type="text"
            name="username"
            id="username"
            onChange={event => setUsername(event.target.value)}
            value={username}
            className="form-control"
          />
        </div>
        <div className="form-group">
          <label htmlFor="password">Password</label>
          <input
            type="password"
            name="password"
            id="password"
            onChange={event => setPassword(event.target.value)}
            value={password}
            className="form-control"
          />
        </div>
        <button type="submit">Submit</button>
      </form>

      {user && JSON.stringify(user, null, 2)}
    </>
  );
}
توضیحات:
- اگر دقت کرده باشید، اینبار این کامپوننت تابعی را به صورت متداول ()function Login تعریف کرده‌ایم. مزیت یک چنین تعریفی، امکان export در محل آن می‌باشد:
export default function Login() {
و دیگر برخلاف حالت استفاده‌ی از arrow function‌ها برای تعریف کامپوننت‌های تابعی، نیازی نیست تا این export را جداگانه در این ماژول درج کرد.
به علاوه وجود واژه‌ی default در اینجا سبب می‌شود که برای import آن، بتوان از هر نام دلخواهی استفاده کرد و در اینجا اجباری به استفاده‌ی از نام Login وجود ندارد که نمونه‌ی استفاده‌ی از آن در فایل index.js، می‌تواند به صورت زیر باشد:
import App from "./components/part02/Login";
- همانطور که در قسمت قبل نیز بررسی کردیم، useState Hook‌ها را با هر نوع داده‌ی دلخواهی می‌توان مقدار دهی اولیه کرد؛ برای مثال با یک int و یا یک object. همچنین الزامی هم به تعریف فقط یک useState Hook وجود ندارد و هر قسمتی از state را می‌توان توسط یک useState Hook مجزا، تعریف و مدیریت کرد.
- فرم لاگین تعریف شده، از یک فیلد نام کاربری و یک فیلد کلمه‌ی عبور تشکیل شده‌است.
- اکنون می‌خواهیم اطلاعات دریافت شده‌ی از کاربر را در state کامپوننت جاری منعکس کنیم. به همین جهت، کار با import متد useState شروع می‌شود. سپس به ازای هر فیلد در فرم، یک state مجزا را تعریف می‌کنیم:
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
- اکنون برای به روز رسانی مقادیر درج شده‌ی در state‌های تعریف شده بر اساس اطلاعات وارد شده‌ی توسط کاربر، از رویداد onChange استفاده می‌کنیم؛ برای مثال:
<input type="text" name="username" id="username"
       onChange={event => setUsername(event.target.value)}
       value={username}
        className="form-control"
/>
در اینجا تابع مدیریت کننده‌ی رویداد onChange، به صورت inline تعریف شده‌است. پیشتر اگر با کامپوننت‌های کلاسی می‌خواستیم اینکار را انجام دهیم، نیاز به clone شیء state، دسترسی به خاصیت متناظر با نام فیلد تعریف شده‌ی در آن به صورت پویا، به روز رسانی آن و در آخر به روز رسانی state با مقدار جدید شیء state می‌بود. اما در اینجا نیازی به دانستن نام المان و یا نام خاصیتی نیست.
- پس از به روز رسانی state، می‌خواهیم در حین submit فرم، این اطلاعات را برای مثال به صورت یک شیء، به سمت سرور ارسال کنیم. به همین جهت نیاز است رویداد onSubmit فرم را  مدیریت کرد. در این متد ابتدا از post back معمول آن به سمت سرور جلوگیری می‌شود و سپس بر اساس متغیرهای تعریف شده‌ی در state، یک شیء را ایجاد کرده‌ایم:
  const handleSubmit = event => {
    event.preventDefault();

    const userData = {
      username,
      password
    };
    setUser(userData);

    setUsername("");
    setPassword("");
  };
همچنین چون در پایین فرم نیز می‌خواهیم این اطلاعات را به صورت JSON نمایش دهیم:
{user && JSON.stringify(user, null, 2)}
 یک state مجزا را هم برای این شیء تعریف:
const [user, setUser] = useState(null);
 و در handleSubmit، به روز رسانی کرده‌ایم.

- دو سطر بعدی را که در انتهای handleSubmit مشاهده می‌کنید، روشی است برای خالی کردن المان‌های فرم، پس از ارسال اطلاعات فرم، برای مثال به backend server. البته این حالت فقط برای حالتی نیاز است که فرم قرار نباشد به آدرس دیگری Redirect شود. برای خالی کردن المان‌های فرم، المان‌های آن‌را باید تبدیل به controlled elements کرد که اینکار با مقدار دهی value آن‌ها توسط value={username} صورت گرفته‌است. به این ترتیب محتوای این المان‌ها با اطلاعاتی که در state داریم، قابل کنترل می‌شوند.


پیاده سازی فرم ثبت نام با استفاده از تنها یک useState Hook

مثال دوم این مطلب نیز در مورد مدیریت المان‌های یک فرم توسط useState Hook است؛ با این تفاوت که در اینجا تنها یک شیء، کل state را تشکیل می‌دهد. کدهای کامل این مثال را در ادامه مشاهده می‌کنید:
import React, { useState } from "react";

const initialFormState = {
  username: "",
  email: "",
  password: ""
};

export default function Register() {
  const [form, setForm] = useState(initialFormState);
  const [user, setUser] = useState(null);

  const handleChange = event => {
    setForm({
      ...form,
      [event.target.name]: event.target.value
    });
  };

  const handleSubmit = event => {
    event.preventDefault();

    setUser(form);
    setForm(initialFormState);
  };

  return (
    <>
      <h2 className="mt-3">Register</h2>
      <form onSubmit={handleSubmit}>
        <div className="form-group">
          <label htmlFor="username">Username</label>
          <input
            type="text"
            name="username"
            id="username"
            onChange={handleChange}
            value={form.username}
            className="form-control"
          />
        </div>
        <div className="form-group">
          <label htmlFor="email">Email</label>
          <input
            type="email"
            name="email"
            id="email"
            onChange={handleChange}
            value={form.email}
            className="form-control"
          />
        </div>
        <div className="form-group">
          <label htmlFor="password">Password</label>
          <input
            type="password"
            name="password"
            id="password"
            onChange={handleChange}
            value={form.password}
            className="form-control"
          />
        </div>
        <button type="submit" className="btn btn-primary">
          Submit
        </button>
      </form>

      {user && JSON.stringify(user, null, 2)}
    </>
  );
}
توضیحات:
- فرم ثبت نام فوق از سه فیلد نام کاربری، ایمیل و کلمه‌ی عبور تشکیل شده‌است.
- اینبار نحوه‌ی تشکیل state مرتبط با این سه فیلد را بسیار شبیه به حالت مدیریت state در کامپوننت‌های کلاسی، تعریف کرده‌ایم؛ که تنها با یک تک شیء، انجام می‌شود و نام آن‌را form در نظر گرفته‌ایم:
const [form, setForm] = useState({ username: "",  email: "", password: ""});
- اکنون باید راهی را بیابیم تا این خواص شیء form را بر اساس ورودی‌های کاربر، به روز رسانی کنیم. به همین جهت رویداد onChange این ورودی را به متغیر handleChange که متد منتسب به آن، این تغییرات را ردیابی می‌کند، متصل می‌کنیم:
<input type="text" name="username" id="username"
       onChange={handleChange} value={form.username}
       className="form-control" />
متد رویدادگردان منتسب به handleChange نیز به صورت زیر تعریف می‌شود:
  const handleChange = event => {
    setForm({
      ...form,
      [event.target.name]: event.target.value
    });
  };
این متد بر اساس name المان‌های ورودی عمل می‌کند (در مثال اول این قسمت، نیازی به دانستن نام المان‌ها نبود). زمانیکه یک شیء را به صورت [event.target.name]: event.target.value تعریف می‌کنیم، یعنی قرار است نام خاصیت این شیء را به صورت پویا تعریف کنیم و مقدار آن نیز از target.value شیء رویداد رسیده، تامین می‌شود. سپس این شیء جدید، با فراخوانی متد setForm، سبب به روز رسانی شیء form موجود در state می‌شود.
- علت وجود spread operator تعریف شده‌ی در اینجا یعنی form...، این است که در حالت استفاده‌ی از useState، برخلاف حالت کار با کامپوننت‌های کلاسی، خواص اضافه شده‌ی به state، به شیء نهایی به صورت خودکار اضافه نمی‌شوند و باید کار یکی سازی را توسط spread operator انجام داد. برای مثال فرض کنید که کاربر، فیلد نام کاربری را ابتدا ثبت می‌کند. بنابراین در این لحظه، شیء ارسالی به setForm، فقط دارای خاصیت username خواهد شد. اکنون اگر در ادامه، کاربر فیلد ایمیل را تکمیل کند، اینبار فقط خاصیت ایمیل در این شیء قرار خواهد گرفت (یا مقدار قبلی را به روز رسانی می‌کند) و از سایر خواص صرفنظر می‌شود؛ مگر اینکه توسط spread operator، سایر خواص پیشین موجود در شیء form را نیز در اینجا لحاظ کنیم، تا اطلاعاتی را از دست نداده باشیم.
بنابراین به صورت خلاصه در روش سنتی کار با کامپوننت‌های کلاسی، فراخوانی متد this.setState کار merge خواص را انجام می‌دهد؛ اما در اینجا فقط کار replace صورت می‌گیرد و باید کار merge خواص یک شیء را به صورت دستی و توسط یک spread operator انجام دهیم. البته در قسمت قبل چون تمام خواص شیء تعریف شده‌ی در state را با هم به روز رسانی می‌کردیم:
    setMousePosition({
      x: event.pageX,
      y: event.pageY
    });
نیازی به تعریف spread operator نبود؛ اما در مثال جاری، هربار فقط یک خاصیت به روز رسانی می‌شود.

- سایر فیلدهای فرم نیز به همین روش onChange={handleChange}، به متد رویدادگردان فوق متصل می‌شوند.
- در پایان برای مدیریت رخ‌داد ارسال فرم، handleSubmit را به صورت زیر تعریف کرده‌ایم:
  const handleSubmit = event => {
    event.preventDefault();

    setUser(form);
    setForm(initialFormState);
  };
در اینجا برخلاف مثال اول، دیگر نیازی به تشکیل دستی یک شیء جدید برای ارسال به سرور وجود ندارد و هم اکنون اطلاعات کل شیء form، در اختیار برنامه است.
- همچنین چون در پایین فرم نیز می‌خواهیم این اطلاعات را به صورت JSON نمایش دهیم:
{user && JSON.stringify(user, null, 2)}
 یک state مجزا را هم برای این شیء تعریف:
const [user, setUser] = useState(null);
 و در handleSubmit، آن‌را با فراخوانی متد setUser، به روز رسانی کرده‌ایم.
- برای پاک کردن المان‌های فرم، پس از submit آن، ابتدا نیاز است این المان‌ها را تبدیل به controlled elements کرد که اینکار با مقدار دهی value آن‌ها توسط برای مثال  value={form.username} صورت گرفته‌است. به این ترتیب محتوای این المان‌ها با اطلاعاتی که در state داریم، قابل کنترل می‌شوند. اکنون اگر setForm را با یک شیء خالی مقدار دهی کنیم، به صورت خودکار المان‌های فرم را پاک می‌کند. برای اینکار بجای تعریف شیء موجود در state به صورت inline:
const [form, setForm] = useState({ username: "",  email: "", password: ""});
می‌توان آن‌را خارج از تابع کامپوننت قرار داد:
const initialFormState = {
  username: "",
  email: "",
  password: ""
};

export default function Register() {
  const [form, setForm] = useState(initialFormState);
و سپس آن‌را به عنوان مقدار اولیه، به صورت setForm(initialFormState)، فراخوانی کرد؛ تا سبب پاک شدن المان‌های فرم شود.


مقایسه‌ی روش‌های مختلف مدیریت state توسط useState Hook

همانطور که مشاهده کردید، با useState Hook، به انعطاف پذیری بیشتری برای مدیریت حالت، نسبت به روش سنتی کامپوننت‌های کلاسی رسیده‌ایم. در حالت تعریف یک useState به ازای هر فیلد، روش تعریف رویدادگردان‌ها و همچنین تبدیل المان‌ها به المان‌های کنترل شده، نسبت به روش تعریف تنها یک useState به ازای کل فرم، ساده‌تر و قابل درک‌تر است. اما زمانیکه نیاز به پاک کردن المان‌های فرم باشد، روش کار کردن با یک تک شیء، ساده‌تر است. درکل بهتر است برای خواص غیرمرتبط state، به ازای هر کدام، یک useState را تعریف کرد و برای یک فرم، همان روش قرار دادن اطلاعات تمام المان‌ها در یک شیء، برای کار با فرم‌های طولانی‌تر، سریع‌تر و قابلیت مدیریت ساده‌تری را به همراه دارد.

کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-30-part-02.zip
نظرات مطالب
افزودن و اعتبارسنجی خودکار Anti-Forgery Tokens در برنامه‌های Angular مبتنی بر ASP.NET Core
وقتی که یک فرم با متد POST به سمت API ارسال میشه خطای 400 Bad request می‌گیرم.
ولی اگر AutoValidateAntiforgeryTokenAttribute در قسمت سرویس startup رو غیر فعال می‌کنم مشکل پیش نمیاد.
درصورتی که برای فرم لاگین با همچین مشکلی برخورد نمی‌کنم!
نظرات مطالب
رفع مشکل postBack در Master Page و دریافت خطای Operation is not valid due to the current state of the object
مشکل وب فرم‌ها اینه که کل صفحه رو یک فرم درنظر می‌گیره. بنابراین موقع postback به سرور تمام صفحه رو ارسال می‌کنه که ممکنه سنگین باشه. در MVC می‌تونید در یک صفحه چندتا فرم داشته باشید با محدوده‌های عملکرد کوچک‌تر.
نظرات مطالب
روش نامگذاری Smurf ایی!
مشکل اینجاست که در پروژه ای، کلاسهایی هم اسم کلاسهای دات نت داشته باشیم. همیشه باید فضای نام را در ابتدای کلاسها نوشت.
مثلا فرض کنید کنترلهای TextBox و Button و ... را در یک پروژه وب فرم یا ویندوز فرم سفارشی کرده باشیم. در این حالت چکار باید بکنیم؟
نظرات مطالب
سفارشی سازی عناصر صفحات پویای افزودن و ویرایش رکوردهای jqGrid در ASP.NET MVC
من از گرید شما استفاده کردم ولی متأسفانه وقتی تعداد سطرها زیاد باشه و صفحه اسکرول پیدا کنه، برای حذف و یا تغییرات عناصر انتهایی، فرم تغییرات در قسمت بالای صفحه ظاهر میشه که نمود چندان جالبی نداره
برای رفع این ایراد چه میشه کرد؟
نظرات مطالب
MVC Scaffolding #2

می‌خواستم بدونم در حالت One-to-many امکان استفاده از Html.EditForModel وجود دارد؟ یعنی میتوان بدون استفاده از UiHint ویا امثال اون فرم اتوماتیک ساخته شود و فیلدهای Dropdownlist را ایجاد کند چرا که در حالت عادی View به صورت EditForModel ساخته نشده و عناصر جدول وابسته به صورت لیست به View پاس داده می‌شود.

مطالب دوره‌ها
بررسی جزئیات تزریق وابستگی‌ها در قالب پروژه WPF Framework
در قالب طراحی شده، نه در کدهای Viewهای اضافه شده و نه در ViewModelها، اثری از کدهای مرتبط با تزریق وابستگی‌ها و یا حتی وهله سازی ViewModel مرتبط با یک View مشاهده نمی‌شود. در ادامه قصد داریم جزئیات پیاده سازی آن‌را مرور کنیم.

مدیریت خودکار وهله سازی ViewModelها

اگر به فایل MVVM\ViewModelFactory.cs قرار گرفته در پروژه Common مراجعه کنید، کدهای کلاسی که کار وهله سازی ViewModelها را انجام می‌دهد، مشاهده خواهید کرد:
using System.Windows;
using StructureMap;

namespace WpfFramework1999.Common.MVVM
{
    /// <summary>
    /// Stitches together a view and its view-model
    /// </summary>
    public class ViewModelFactory
    {
        private readonly FrameworkElement _control;

        /// <summary>
        /// سازنده کلاس تزریق وابستگی‌ها به ویوو مدل و وهله سازی آن
        /// </summary>
        /// <param name="control">وهله‌ای از شیءایی که باید کار تزریق وابستگی‌ها در آن انجام شود</param>
        public ViewModelFactory(FrameworkElement control)
        {
            _control = control;
        }

        /// <summary>
        /// وهله متناظر با ویوو مدل
        /// </summary>
        public IViewModel ViewModelInstance { get; private set; }

        /// <summary>
        /// کار تزریق خودکار وابستگی‌ها و وهله سازی ویوو مدل مرتبط انجام خواهد شد
        /// </summary>        
        public void WireUp()
        {
            var viewName = _control.GetType().Name;
            var viewModelName = string.Concat(viewName, "ViewModel"); //قرار داد نامگذاری ما است

            if (!_control.IsLoaded)
            {
                _control.Loaded += (s, e) =>
                {
                    setDataContext(viewModelName);
                };
            }
            else
            {
                setDataContext(viewModelName);
            }
        }

        private void setDataContext(string viewModelName)
        {
            //کار تزریق خودکار وابستگی‌ها و وهله سازی ویوو مدل مرتبط انجام خواهد شد
            ViewModelInstance = ObjectFactory.TryGetInstance<IViewModel>(viewModelName);
            if (ViewModelInstance == null) // این صفحه ویوو مدل ندارد
                return;

            _control.DataContext = ViewModelInstance;
        }
    }
}
در این کلاس، یک وهله از صفحه‌ای که توسط کاربر درخواست شده‌است، در سازنده کلاس دریافت گردیده و سپس در متد WireUp، بر اساس قرارداد نامگذاری که پیشتر نیز عنوان شد، ViewModel متناظر با نام View از IoC Container استخراج و وهله سازی می‌گردد. سپس این وهله به DataContext صفحه انتساب داده می‌شود.
چند سؤال مهم:
- IoC Container از کجا می‌داند که ViewModelها در کجا قرار دارند؟
- این کلاس ViewModelFactory چگونه به وهله‌ای از یک صفحه درخواستی توسط کاربر دسترسی پیدا می‌کند و در کجا؟


IoC Container از کجا می‌داند که ViewModelها در کجا قرار دارند؟

اگر بحث سری جاری را از ابتدا دنبال کرده باشید، عنوان شد که ViewModelها را در این قالب، باید مشتق شده از کلاس پایه‌ای به نام BaseViewModel تهیه کنیم. برای مثال:
/// <summary>
/// ویوو مدل افزودن و مدیریت کاربران
/// </summary>
public class AddNewUserViewModel : BaseViewModel
این کلاس پایه که در فایل MVVM\BaseViewModel.cs پروژه Common قرار دارد، به نحو زیر آغاز شده است:
/// <summary>
/// کلاس پایه ویوو مدل‌های برنامه که جهت علامتگذاری آن‌ها برای سیم کشی‌های تزریق وابستگی‌های برنامه نیز استفاده می‌شود
/// </summary>
public abstract class BaseViewModel : DataErrorInfoBase, INotifyPropertyChanged, IViewModel
اگر دقت کنید در اینجا اینترفیس IViewModel نیز ذکر شده است. این اینترفیس برای علامتگذاری ViewModelها و یافتن خودکار آن‌ها توسط IoC Container مورد استفاده درنظر گرفته شده است. اگر به فایل Core\IocConfig.cs پروژه Infrastructure مراجعه کنید، چنین تنظیمی را در آن مشاهده خواهید نمود:
// Add all types that implement IView into the container,
// and name each specific type by the short type name.
scan.AddAllTypesOf<IViewModel>().NameBy(type => type.Name);
به این ترتیب StructureMap با اسکن اسمبلی Infrastructure کلیه کلاس‌های پیاده سازی کننده IViewModel را یافته و سپس آن‌ها را بر اساس نام متناظری که دارند، ذخیره می‌کند. با این تنظیم، اکنون در کلاس ViewModelFactory یک چنین کدی کار خواهد کرد:
 //کار تزریق خودکار وابستگی‌ها و وهله سازی ویوو مدل مرتبط انجام خواهد شد
ViewModelInstance = ObjectFactory.TryGetInstance<IViewModel>(viewModelName);


کلاس ViewModelFactory چگونه به وهله‌ای از یک صفحه درخواستی توسط کاربر دسترسی پیدا می‌کند و در کجا؟

در اینجا قسمتی از کدهای فایل Core\FrameFactory.cs قرار گرفته در پروژه Infrastructure را ملاحظه می‌کنید:
namespace WpfFramework.Infrastructure.Core
{
    /// <summary>
    /// ایجاد یک کنترل فریم سفارشی که قابلیت تزریق وابستگی‌ها را به صورت خودکار دارد
    /// به همراه اعمال مسایل راهبری برنامه که از منوی اصلی دریافت می‌شوند
    /// </summary>
    public class FrameFactory : Frame
    {
        /// <summary>
        /// در اینجا می‌شود به وهله‌ای از صفحه‌ای که قرار است اضافه گردد دسترسی یافت
        /// </summary>
        protected override void OnContentChanged(object oldContent, object newContent)
        {
            base.OnContentChanged(oldContent, newContent);

            var newPage = newContent as FrameworkElement;
            if (newPage == null)
                return;

            _currentViewModelFactory = new ViewModelFactory(newPage);
            _currentViewModelFactory.WireUp(); //کار تزریق وابستگی‌ها و وهله سازی ویوو مدل مرتبط انجام خواهد شد
        }
    }
}
در این کلاس، یک Frame سفارشی را طراحی کرده‌ایم؛ از این جهت که بتوان متد OnContentChanged آن‌را تحریف کرد. در این متد، newContent دقیقا وهله‌ای از صفحه جدیدی است که توسط کاربر درخواست شده‌است. خوب ... این وهله را داریم، بنابراین تنها کافی است آن‌را به کلاس ViewModelFactory ارسال کنیم و متد WireUp آن‌را بر روی وهله کلاس صفحه درخواستی فراخوانی نمائیم. به این ترتیب، صفحه‌ای نمایش داده خواهد شد که DataContext آن با وهله‌ای از ViewModel متناظر مقدار دهی شده‌است. از این جهت که این وهله سازی توسط IoC Container صورت می‌گیرد، کلیه وابستگی‌های تعریف شده در سازنده کلاس ViewModel نیز به صورت خودکار وهله سازی و مقدار دهی خواهند شد.

نهایتا فراخوانی متد IocConfig.Init، در فایل App.xaml.cs پروژه ریشه، در آغاز برنامه قرار گرفته است.
نظرات مطالب
بهینه سازی برنامه‌های وب ASP.NET برای موتورهای جستجو (SEO)
بله صابر جان حتما همین طور است (برای مثال بیان شده است).
خودم به این صورت استفاده می‌کنم :
<head>
    <asp:Literal runat="server" ID="litHead"></asp:Literal>
....
</head>
و در قسمت code-behind
litHead.Text = SEO.GenerateMetaTag(...)
پ ن : چرا از کنترل Literal استفاده شده و از Label استفاده نشده ؟ به این دلیل که خروجی متن رندر شده Label توسط تگ <span> یا <label> احاطه می‌شود. (+)