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


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


معرفی Event Call Back

در این قسمت قصد داریم به کامپوننت Pages\LearnBlazor\LearnBlazor‍Components\IndividualRoom.razor که در مثال این سری تکمیل کردیم، یک checkbox انتخاب را نیز اضافه کنیم تا کاربرها بتوانند در زمان نمایش لیست اتاق‌ها در کامپوننت Pages\LearnBlazor\DemoHotel.razor، بیش از یک اتاق را انتخاب کنند.
برای این منظور در ابتدا به کامپوننت DemoHotel مراجعه کرده و فیلد SelectedRooms را در آن تعریف کرده و ذیل عنوان Hotel Rooms نمایش می‌دهیم:
@page "/demoHotel"

        <div class="col-12">
            <h4 class="text-info">Hotel Rooms</h4>
            <span>Rooms Selected - @SelectedRooms</span>
        </div>
        @foreach (var room in Rooms)
        {
            <IndividualRoom Room="room"></IndividualRoom>
        }

@code{

    int SelectedRooms;

    void RoomSelectionCounterChanged(bool isRoomSelected)
    {
        if (isRoomSelected)
        {
            SelectedRooms++;
        }
        else
        {
            SelectedRooms--;
        }
    }
    // ...
}
همچنین متد RoomSelectionCounterChanged را هم برای تغییر مقدار SelectedRooms تعریف کرده‌ایم. اما این متد به تنهایی کار نمی‌کند و باید بتوان آن‌را به کامپوننت فرزند <IndividualRoom Room="room"></IndividualRoom> انتقال داد تا در زمان انتخاب آن اتاق (و یا عدم انتخاب آن) در کامپوننت IndividualRoom، پارامتر bool isRoomSelected بر اساس وضعیت checkbox آن دریافت شده و در نتیجه مقدار جاری SelectedRooms، یک واحد کم یا زیاد شود. بنابراین نیاز است تا بتوان اشاره‌گری از یک متد کامپوننت سطح بالا را به یک کامپوننت سطح پایین و فرزند آن انتقال داد. اینکار در Blazor توسط EventCallback‌ها انجام می‌شود.
در ادامه به کامپوننت IndividualRoom.razor مراجعه کرده و کدهای آن را به صورت زیر تغییر می‌دهیم:
<input type="checkbox" @onchange="RoomCheckBoxSelectionChanged" />

@code
{
    //...

    [Parameter]
    public EventCallback<bool> OnRoomCheckBoxSelection { get; set; }

    async Task RoomCheckBoxSelectionChanged(ChangeEventArgs e)
    {
        await OnRoomCheckBoxSelection.InvokeAsync((bool)e.Value);
    }
}
ابتدا یک checkbox را جهت فراهم آوردن امکان انتخاب یک اتاق اضافه کرده‌ایم. سپس رخ‌داد onchange آن‌را به متد RoomCheckBoxSelectionChanged متصل کرده‌ایم. نوع پارامتر این متد را با نزدیک کردن اشاره‌گر ماوس به onchange@ چک باکس می‌توان مشاهده کرد:


تا اینجا فقط یک رخ‌داد را به یک متد، در همان کامپوننت متصل کرده‌ایم. هربار که checkbox تعریف شده انتخاب شود، متد رویدادگردان RoomCheckBoxSelectionChanged اجرا می‌شود. مرحله‌ی بعد، انتقال اطلاعات آن، به کامپوننت والد است که اینکار توسط پارامتر OnRoomCheckBoxSelection صورت می‌گیرد. کار آن، انتقال وضعیت checkbox، به متد RoomSelectionCounterChanged کامپوننت والد است.
بنابراین در اینجا نیاز است تا بتوان ارجاعی از این متد کامپوننت والد را به کامپوننت فرزند ارسال کرد که EventCallback تعریف شده‌ی به صورت پارامتر، چنین هدفی را برآورده می‌کند. با پارامتر تعریف شدن آن، می‌توان OnRoomCheckBoxSelection را به صورت زیر، به هر المان تعریف کننده‌ی کامپوننت IndividualRoom در کامپوننت DemoHotel اضافه کرد:
@foreach (var room in Rooms)
{
    <IndividualRoom OnRoomCheckBoxSelection="RoomSelectionCounterChanged" Room="room"></IndividualRoom>
}
در این تعریف، پارامتر Room، یک پارامتر ورودی است و پارامتر OnRoomCheckBoxSelection، به نوعی یک پارامتر خروجی است که با اتصال به متدی در همین کامپوننت، امکان دریافت اطلاعات و رویدادها را از یک کامپوننت سطح پایین‌تر پیدا می‌کند.

بنابراین به صورت خلاصه، هر زمانیکه یک checkbox در کامپوننت IndividualRoom انتخاب می‌شود، در نتیجه‌ی آن متد منتسب به EventCallback ارسالی به آن کامپوننت نیز فراخوانی می‌گردد که اینکار، سبب اجرای کدی در کامپوننت والد خواهد شد.


یک تمرین: انتقال رویداد انتخاب شدن یک div به کامپوننت والد

در بخش 2، در انتهای مطلب، لیست امکانات رفاهی هتل را هم نمایش دادیم. در اینجا می‌خواهیم اگر کاربری بر روی خروجی کامپوننت یکی از امکانات موجود کلیک کرد (کلیک بر روی div آن)، نام آن ویژگی در کامپوننت والد نمایش داده شود.
برای این منظور کامپوننت Pages\LearnBlazor\LearnBlazor‍Components\IndividualAmenity.razor را به صورت زیر تغییر می‌دهیم:
<div class="bg-light border p-2 col-5 offset-1 mt-2"
    @onclick="args => AmenitySelectionChanged(args, Amenity.Name)">
    <h4 class="text-secondary">Amenity - @Amenity.Id</h4>
    @Amenity.Name<br />
    @Amenity.Description<br />
</div>

@code
{
    [Parameter]
    public BlazorAmenity Amenity { get; set; }

    [Parameter]
    public EventCallback<string> OnAmenitySelection { get; set; }

    protected async Task AmenitySelectionChanged(MouseEventArgs e, string name)
    {
        await OnAmenitySelection.InvokeAsync(name);
    }
}
هدف از این مثال، آشنایی با نحوه‌ی تغییر امضای متد منتسب به رویداد onclick@ است. چون نوع پارامتر متد متناظر با آن، از نوع MouseEventArgs است (<EventCallback<MouseEventArgs) و نه از نوع ChangeEventArgs مثال قبلی که به همراه خاصیت Value مفیدی بود:


در اینجا نیاز خواهیم داشت تا اطلاعات رشته‌ای نام Amenity جاری را پس از کلیک بر روی div، به کامپوننت والد انتقال دهیم و MouseEventArgs فقط به همراه اطلاعات مختصات محل قرارگیری اشاره‌گر ماوس است. در یک چنین حالتی می‌توان با استفاده از anonymous method زیر، امضای متد منتسب به آن‌را تغییر داد:
@onclick="args => AmenitySelectionChanged(args, Amenity.Name)"
اکنون با هر بار کلیک بر روی div، نام Amenity جاری از طریق EventCallback تعریف شده، به سمت کامپوننت والد ارسال می‌شود. بنابراین مرحله‌ی بعدی، مراجعه به کامپوننت DemoHotel.razor است و استفاده از پارامتر جدید OnAmenitySelection:
@page "/demoHotel"

    @foreach (var amenity in AmenitiesList)
    {
      <IndividualAmenity OnAmenitySelection="AmenitySelectionChanged" Amenity="amenity"></IndividualAmenity>
    }

    <div class="col-12">
        <p class="text-secondary"> Selected Amenity : @SelectedAmenity </p>
    </div>

@code{

    string SelectedAmenity = "";

    void AmenitySelectionChanged(string amenity)
    {
        SelectedAmenity = amenity;
    }
}
ابتدا پارامتر جدید OnAmenitySelection کامپوننت IndividualAmenity به متد AmenitySelectionChanged همین کامپوننت متصل می‌شود. امضای آن بر اساس نوع پارامتر <EventCallback<string کامپوننت IndividualAmenity تعیین شده‌است.
سپس این پارامتر رشته‌ای دریافتی، به فیلد جدید SelectedAmenity انتساب داده می‌شود که پس از پایان این متد و رویداد، سبب درج آن در زیر حلقه‌ی نمایش AmenitiesList خواهد شد:



کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-07.zip
مطالب
WF:Windows Workflow #۶
در این قسمت به تکمیل مثالی که در قسمت قبل زده شد پرداخته می‌شود و همچنین کنترل‌های Foreach , Try Catch نیز بررسی خواهند شد.
در ابتدا دو کلاس به نام‌های ItemInfo و OutOfStockException  را به برنامه اضافه می‌کنیم. کلاس اول برای ذخیره سازی مشخصات هر سفارش و کلاس دیگر برای مدیریت خطا‌ها می‌باشد.
public class ItemInfo
    {
        public string ItemCode { get; set; } 
        public string Description { get; set; } 
        public decimal Price { get; set; }
    }

public class OutOfStockException : Exception
    {
        public OutOfStockException() 
            : base() 
        { 

        }

        public OutOfStockException(string message) 
            : base(message) 
        { 

        }
    }
در Workflow مورد نظر که به نام OrderWF.xaml می‌باشد٬ پس از کنترل Assign که برای صفر کردن مقدار متغیر TotalAmount از آن استفاده می‌شود٬ یک کنترل ForEach را به Flow جاری اضافه می‌کنیم. این کنترل دارای دو خاصیت به نام‌های Type Arguments و Values می‌باشد. اولین خاصیت که مقدار پیش فرض آن، مقدار عددی Int32 است٬ برای مشخص کردن نوع متغییر حلقه و دیگری برای مشخص کردن نوع منبع داده حلقه تعریف شده‌اند.

همانطور که در شکل بالا مشخص می‌باشد٬ Type Arguments حلقه برابر با کلاس OrderItem می‌باشد. Values هم برابر با OrderInfo.Items است. از این جهت نوع حلقه  را از جنس کلاس OrderItem مشخص کرد‌‌ه‌ایم تا کنترل بر روی مقادیر Items اجرا شود (لیستی از کلاس مورد نظر).
حال همانند شکل بالا، در قسمت Body کنترل ForEach، یک کنترل Sequence را ایجاد کرده و سپس برای اینکه کنترل LookupItem را ایجاد کنیم٬ ابتدا باید یک Code Activity را به پروژه اضافه کنیم. به همین منظور پروژه جاری را انتخاب کرده و یک Code Activity به آن اضافه و نام آن را LookupItem  می‌گذاریم. سپس کد زیر را به آن اضافه می‌کنیم:
 public sealed class LookupItem : CodeActivity
    {
        // Define an activity input argument of type string
        public InArgument<string> ItemCode { get; set; }         
        public OutArgument<ItemInfo> Item { get; set; }

        // If your activity returns a value, derive from CodeActivity<TResult>
        // and return the value from the Execute method.
        protected override void Execute(CodeActivityContext context)
        {
            // Obtain the runtime value of the Text input argument
            ItemInfo i = new ItemInfo();             
            i.ItemCode = context.GetValue<string>(ItemCode);
            switch (i.ItemCode)
            {

                case "12345": 
                    i.Description = "Widget"; 
                    i.Price = (decimal)10.0; 
                    break;
                case "12346": 
                    i.Description = "Gadget"; 
                    i.Price = (decimal)15.0; 
                    break;
                case "12347": 
                    i.Description = "Super Gadget"; 
                    i.Price = (decimal)25.0; break;
            }
     
            context.SetValue(this.Item, i);
            
        }
    }
در این کد، دو متغییر تعریف شده‌اند؛ یکی از نوع رشته بوده و از طریق آن، دستور Switch تصمیم می‌گیرد که کلاس ItemInfo را با چه مقادیری پرکند. متغییر دیگر از نوع کلاس  ItemInfo می‌باشد و برای گرفتن مقدار کلاس از دستور Switch تعریف شده است.
حال برای اینکه بتوانیم از Code Activity مورد نظر استفاده کنیم٬ ابتدا باید پروژه را یکبار Build کنیم. اکنون در قسمت Toolbox یک٬ Tab ایی به نام پروژه ایجاد شده است و در آن یک کنترل به نام  LookupItem  موجود می‌باشد. آن را گرفته و به درون Sequence انتقال می‌دهیم.
سپس برای مقدار دادن به متغیر‌های تعریف شده در Code Activity، کنترل LookupItem را انتخاب کرده و در قسمت Properties به خصوصیت ItemCode، کد زیر را اضافه می‌کنیم:
item.ItemCode

نکته
: از کلاس Code Activity برای ارسال و دریافت مقادیر به درون Workflow استفاده می‌شود.

 Try Catch 
از این کنترل برای مدیریت خطا‌ها استفاده می‌شود.
ابتدا یک کنترل Try Catch را به Workflow اضافه کرده، مانند شکل زیر:

در بدنه Try می‌توان از کنترل‌های مورد نظر استفاده کنیم و همانطور که در شکل بالا مشخص است٬ از کنترل Throw برای ایجاد خطا استفاده شده‌است. کنترل جاری را انتخاب کرده و از قسمت Properties در خاصیت Exception کد زیر را اضافه می‌کنیم:
new OutOfStockException("Item Code"+item.ItemCode)
این  دستور باعث ایجاد یک خطا از نوع کلاس OutOfStockException می‌شود. برای کنترل خطای مورد نظر در قسمت Catches مانند شکل زیر عمل می‌کنیم.

مطالب
چگونگی گزارشگیری از Business Objects مانند List توسط StimulSoft

می‌خواهیم از یک لیست در گزارش خود استفاده کنیم؛ بطور مثال وقتی در LINQ  از دستور ToList استفاده می‌کنیم و می‌خواهیم آنرا بصورت مستقیم به Stimul بفرستیم. فرض بر این است که شما DLLهای Stimul را به پروژه اضافه کرده اید و آماده گزارشگیری هستید.

مثلا مدلی در Entity FrameWork با نام base_CenterType 

public class base_CenterType
    {
        public int ID { get; set; }
        public string Title { get; set; }
        public string Dsc { get; set; }
     }

و متدی بصورت ذیل:

public IList<base_CenterType> GetAll()
        {
            return _base_CenterType.ToList();
        }

طراحی گزارش برای این لیست به این صورت است:

1- اضافه کردن StiWebReport به فرم به نام StiWebReport1

2- با کلیک بر روی فلش سمت راست و بالای StiWebReport1 و انتخاب Design Report، وارد قسمت طراحی می‌شویم:

3- با راست کلیک بر روی Business Object و انتخاب New Business Object  پنجره مربوطه باز میشود:

4- بعد از زدن OK پنجره زیر باز خواهد شد که باید در کادر Name نام Business Object را انتخاب کنیم که برای خوانایی بهتر است همان نام کلاس را برای آن انتخاب کنیم. چون Category  نداریم پس باید کادر آن خالی بماند. 

در قسمت Columns باید ستون‌های هم نام و هم نوع با خواص کلاس base_CenterType  را ایجاد کنیم. 

و نهایتا Business Objectی به نام base_CenterType با سه ستون ایجاد خواهد شد. 

  حال می‌توانید ستون‌های مورد نظر را در گزارش بکار ببرید.

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

StiReport mainreport = new StiReport();            
mainreport.RegBusinessObject("base_CenterType", base_CenterTypeService.GetAll());
mainreport.Load("C:\\StiWebReport2.mrt");
mainreport.Show();
مطالب
NHibernate 3.0 و خواص تنبل (lazy properties)!

احتمالا مطلب "دات نت 4 و کلاس Lazy" را پیشتر مطالعه کرده‌اید. هر چند NHibernate 3.0 بر اساس دات نت فریم ورک 3 و نیم تهیه شده، اما شبیه به این مفهوم را در مورد بارگذاری به تاخیر افتاده‌ی مقادیر خواص یک کلاس که به ندرت مورد استفاده قرار می‌گیرند، پیاده سازی کرده است. Lazy را در اینجا تنبل، به تعویق افتاده، با تاخیر و شبیه به آن می‌توان ترجمه کرد؛ خواص معوقه!
برای مثال فرض کنید یکی از خواص کلاس مورد استفاده، متن، تصویر یا فایلی حجیم است. در مکانی هم که قرار است وهله‌ای از این کلاس مورد استفاده قرار گیرد نیازی به این اطلاعات حجیم نیست؛ با سایر خواص آن سر و کار داریم و نیازی به اشغال حافظه و منابع سیستم در این مورد خاص نیست.

سؤال: چگونه آن‌را تعریف کنیم؟
اگر از NHibernate سنتی استفاده می‌کنید (یا به عبارتی فایل‌های hbm.xml را دستی تهیه می‌کنید)، ویژگی Lazy را به صورت زیر می‌توان مشخص کرد:
<property name="Text" lazy="true"/>
اگر از Fluent NHibernate استفاده می‌کنید (و فایل‌های hbm.xml به صورت خودکار از کلاس‌های شما تهیه خواهند شد)، روش کار به صورت زیر است (فراخوانی متد LazyLoad روی خاصیت مورد نظر):
public class Post
{
public virtual int Id { set; get; }
public virtual string PostText { set; get; }
}

public class PostMappings : ClassMap<Post>
{
public PostMappings()
{
Id(p => p.Id, "PostId").GeneratedBy.Identity();
Map(p => p.PostText).LazyLoad();
//…
Table("Posts");
}
}
در این حالت در پشت صحنه در مورد خاصیت PostText چنین نگاشتی تعریف خواهد شد:
<property name="PostText" type="System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" lazy="true" />

سؤال: چه زمانی نباید از این روش استفاده کرد؟
فرض کنید در شرایطی دیگر نیاز است تا اطلاعات تمام رکوردهای جدول مذکور به همراه مقدار PostText نمایش داده شود. در این حالت بسته به تعداد رکوردها، ممکن است هزاران هزار کوئری به دیتابیس ایجاد شود که مطلوب نیست (به ازای هربار دسترسی به خاصیت PostText یک کوئری تولید می‌شود).

البته امکان لغو موقت این روش تنها در حین استفاده از HQL (یکی دیگر از روش‌های دسترسی به داده‌ها در NHibernate) میسر است. اطلاعات بیشتر: (+)

مطالب
آشنایی با NHibernate - قسمت ششم

آشنایی با Automapping در فریم ورک Fluent NHibernate

اگر قسمت‌های قبل را دنبال کرده باشید، احتمالا به پروسه طولانی ساخت نگاشت‌ها توجه کرده‌اید. با کمک فریم ورک Fluent NHibernate می‌توان پروسه نگاشت domain model خود را به data model متناظر آن به صورت خودکار نیز انجام داد و قسمت عمده‌ای از کار به این صورت حذف خواهد شد. (این مورد یکی از تفاوت‌های مهم NHibernate با نمونه‌های مشابهی است که مایکروسافت تا تاریخ نگارش این مقاله ارائه داده است. برای مثال در نگار‌ش‌های فعلی LINQ to SQL یا Entity framework ، اول دیتابیس مطرح است و بعد ساخت کد از روی آن، در حالیکه در اینجا ابتدا کد و طراحی سیستم مطرح است و بعد نگاشت آن به سیستم داده‌ای و دیتابیس)

امروز قصد داریم یک سیستم ساده ثبت خبر را از صفر با NHibernate پیاده سازی کنیم و همچنین مروری داشته باشیم بر قسمت‌های قبلی.

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

ابتدا یک پروژه کنسول جدید را به نام NHSample2 آغاز کنید. سپس ارجاعاتی را به اسمبلی‌های زیر به آن اضافه نمائید:
FluentNHibernate.dll
NHibernate.dll
NHibernate.ByteCode.Castle.dll
NHibernate.Linq.dll
و ارجاعی به اسمبلی استاندارد System.Data.Services.dll دات نت فریم ورک سه و نیم

سپس پوشه‌ای را به نام Domain به این پروژه اضافه نمائید (کلیک راست روی نام پروژه در VS.Net و سپس مراجعه به منوی Add->New folder). در این پوشه تعاریف موجودیت‌های برنامه را قرار خواهیم داد. سه کلاس جدید Category ، User و News را در این پوشه ایجاد نمائید. محتویات این سه کلاس به شرح زیر هستند:

namespace NHSample2.Domain
{
public class User
{
public virtual int Id { get; set; }
public virtual string UserName { get; set; }
public virtual string Password { get; set; }
}
}


namespace NHSample2.Domain
{
public class Category
{
public virtual int Id { get; set; }
public virtual string CategoryName { get; set; }
}
}


using System;

namespace NHSample2.Domain
{
public class News
{
public virtual Guid Id { get; set; }
public virtual string Subject { get; set; }
public virtual string NewsText { get; set; }
public virtual DateTime DateEntered { get; set; }
public virtual Category Category { get; set; }
public virtual User User { get; set; }
}
}
همانطور که در قسمت‌های قبل نیز ذکر شد، تمام خواص پابلیک کلاس‌های Domain ما به صورت virtual تعریف شده‌اند تا lazy loading را در NHibernate فعال سازیم. در حالت lazy loading ، اطلاعات تنها زمانیکه به آن‌ها نیاز باشد بارگذاری خواهند شد. این مورد در حالتیکه نیاز به نمایش اطلاعات تنها یک شیء وجود داشته باشد بسیار مطلوب می‌باشد، یا هنگام ثبت و به روز رسانی اطلاعات نیز یکی از بهترین روش‌ها است. اما زمانیکه با لیستی از اطلاعات سروکار داشته باشیم باعث کاهش افت کارآیی خواهد شد زیرا برای مثال نمایش آن‌ها سبب خواهد شد که 100 ها کوئری دیگر جهت دریافت اطلاعات هر رکورد در حال نمایش اجرا شود (مفهوم دسترسی به اطلاعات تنها در صورت نیاز به آن‌ها). Lazy loading و eager loading (همانند مثال‌های قبلی) هر دو در NHibernate به سادگی قابل تنظیم هستند (برای مثال LINQ to SQL به صورت پیش فرض همواره lazy load است و تا این تاریخ راه استانداردی برای امکان تغییر و تنظیم این مورد پیش بینی نشده است).

اکنون کلاس جدید Config را به برنامه اضافه نمائید:

using FluentNHibernate.Automapping;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using NHibernate.Cfg;
using NHibernate.Tool.hbm2ddl;

namespace NHSample2
{
class Config
{
public static Configuration GenerateMapping(IPersistenceConfigurer dbType)
{
var cfg = dbType.ConfigureProperties(new Configuration());

new AutoPersistenceModel()
.Where(x => x.Namespace.EndsWith("Domain"))
.AddEntityAssembly(typeof(NHSample2.Domain.News).Assembly).Configure(cfg);

return cfg;
}

public static void GenerateDbScript(Configuration config, string filePath)
{
bool script = true;//فقط اسکریپت دیتابیس تولید گردد
bool export = false;//نیازی نیست بر روی دیتابیس هم اجرا شود
new SchemaExport(config).SetOutputFile(filePath).Create(script, export);
}

public static void BuildDbSchema(Configuration config)
{
bool script = false;//آیا خروجی در کنسول هم نمایش داده شود
bool export = true;//آیا بر روی دیتابیس هم اجرا شود
bool drop = false;//آیا اطلاعات موجود دراپ شوند
new SchemaExport(config).Execute(script, export, drop);
}

public static void CreateSQL2008DbPlusScript(string connectionString, string filePath)
{
Configuration cfg =
GenerateMapping(
MsSqlConfiguration
.MsSql2008
.ConnectionString(connectionString)
.ShowSql()
);
GenerateDbScript(cfg, filePath);
BuildDbSchema(cfg);
}

public static ISessionFactory CreateSessionFactory(IPersistenceConfigurer dbType)
{
return
Fluently.Configure().Database(dbType)
.Mappings(m => m.AutoMappings
.Add(
new AutoPersistenceModel()
.Where(x => x.Namespace.EndsWith("Domain"))
.AddEntityAssembly(typeof(NHSample2.Domain.News).Assembly))
)
.BuildSessionFactory();
}
}
}

در متد GenerateMapping از قابلیت Automapping موجود در فریم ورک Fluent Nhibernate استفاده شده است (بدون نوشتن حتی یک سطر جهت تعریف این نگاشت‌ها). این متد نوع دیتابیس مورد نظر را جهت ساخت تنظیمات خود دریافت می‌کند. سپس با کمک کلاس AutoPersistenceModel این فریم ورک، به صورت خودکار از اسمبلی برنامه نگاشت‌های لازم را به کلاس‌های موجود در پوشه Domain ما اضافه می‌کند (مرسوم است که این پوشه در یک پروژه Class library مجزا تعریف شود که در این برنامه جهت سهولت کار در خود برنامه قرار گرفته است). قسمت Where ذکر شده به این جهت معرفی گردیده است تا Fluent Nhibernate برای تمامی کلاس‌های موجود در اسمبلی جاری، سعی در تعریف نگاشت‌های لازم نکند. این نگاشت‌ها تنها به کلاس‌های موجود در پوشه دومین ما محدود شده‌اند.
سه متد بعدی آن، جهت ایجاد اسکریپت دیتابیس از روی این نگاشت‌های تعریف شده و سپس اجرای این اسکریپت بر روی دیتابیس جاری معرفی شده، تهیه شده‌اند. برای مثال CreateSQL2008DbPlusScript یک مثال ساده از استفاده دو متد قبلی جهت ایجاد اسکریپت و دیتابیس متناظر اس کیوال سرور 2008 بر اساس نگاشت‌های برنامه است.
با متد CreateSessionFactory در قسمت‌های قبل آشنا شده‌اید. تنها تفاوت آن در این قسمت، استفاده از کلاس AutoPersistenceModel جهت تولید خودکار نگاشت‌ها است.

در ادامه دیتابیس متناظر با موجودیت‌های برنامه را ایجاد خواهیم کرد:

using System;

namespace NHSample2
{
class Program
{
static void Main(string[] args)
{
Config.CreateSQL2008DbPlusScript(
"Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true",
"db.sql");

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}

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



همچنین اسکریپت تولید شده آن، صرفنظر از عبارات drop اولیه، به صورت زیر است:

create table [Category] (
Id INT IDENTITY NOT NULL,
CategoryName NVARCHAR(255) null,
primary key (Id)
)

create table [User] (
Id INT IDENTITY NOT NULL,
UserName NVARCHAR(255) null,
Password NVARCHAR(255) null,
primary key (Id)
)

create table [News] (
Id UNIQUEIDENTIFIER not null,
Subject NVARCHAR(255) null,
NewsText NVARCHAR(255) null,
DateEntered DATETIME null,
Category_id INT null,
User_id INT null,
primary key (Id)
)

alter table [News]
add constraint FKE660F9E1C9CF79
foreign key (Category_id)
references [Category]

alter table [News]
add constraint FKE660F95C1A3C92
foreign key (User_id)

references [User]

اکنون یک سری گروه خبری، کاربر و خبر را به دیتابیس خواهیم افزود:

using System;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using NHSample2.Domain;

namespace NHSample2
{
class Program
{
static void Main(string[] args)
{
using (ISessionFactory sessionFactory = Config.CreateSessionFactory(
MsSqlConfiguration
.MsSql2008
.ConnectionString("Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true")
.ShowSql()
))
{
using (ISession session = sessionFactory.OpenSession())
{
using (ITransaction transaction = session.BeginTransaction())
{
//با توجه به کلیدهای خارجی تعریف شده ابتدا باید گروه‌ها را اضافه کرد
Category ca = new Category() { CategoryName = "Sport" };
session.Save(ca);
Category ca2 = new Category() { CategoryName = "IT" };
session.Save(ca2);
Category ca3 = new Category() { CategoryName = "Business" };
session.Save(ca3);

//سپس یک کاربر را به دیتابیس اضافه می‌کنیم
User u = new User() { Password = "123$5@1", UserName = "VahidNasiri" };
session.Save(u);

//اکنون می‌توان یک خبر جدید را ثبت کرد

News news = new News()
{
Category = ca,
User = u,
DateEntered = DateTime.Now,
Id = Guid.NewGuid(),
NewsText = "متن خبر جدید",
Subject = "عنوانی دلخواه"
};
session.Save(news);

transaction.Commit(); //پایان تراکنش
}
}
}

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
جهت بررسی انجام عملیات ثبت هم می‌توان به دیتابیس مراجعه کرد، برای مثال:



و یا می‌توان از LINQ استفاده کرد:
برای مثال کاربر VahidNasiri تعریف شده را یافته، اطلاعات آن‌را نمایش دهید؛ سپس نام او را به Vahid ویرایش کرده و دیتابیس را به روز کنید.

برای اینکه کوئری‌های LINQ ما شبیه به LINQ to SQL شوند، کلاس NewsContext را به صورت ذیل تشکیل می‌دهیم. این کلاس از کلاس پایه NHibernateContext مشتق شده و سپس به ازای تمام موجودیت‌های برنامه، یک متد از نوع IOrderedQueryable را تشکیل خواهیم داد.

using System.Linq;
using NHibernate;
using NHibernate.Linq;
using NHSample2.Domain;

namespace NHSample2
{
class NewsContext : NHibernateContext
{
public NewsContext(ISession session)
: base(session)
{ }

public IOrderedQueryable<News> News
{
get { return Session.Linq<News>(); }
}

public IOrderedQueryable<Category> Categories
{
get { return Session.Linq<Category>(); }
}

public IOrderedQueryable<User> Users
{
get { return Session.Linq<User>(); }
}
}
}
اکنون جهت یافتن کاربر و به روز رسانی اطلاعات او در دیتابیس خواهیم داشت:

using System;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using System.Linq;
using NHSample2.Domain;

namespace NHSample2
{
class Program
{
static void Main(string[] args)
{
using (ISessionFactory sessionFactory = Config.CreateSessionFactory(
MsSqlConfiguration
.MsSql2008
.ConnectionString("Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true")
.ShowSql()
))
{
using (ISession session = sessionFactory.OpenSession())
{
using (ITransaction transaction = session.BeginTransaction())
{
using (NewsContext db = new NewsContext(session))
{
var query = from x in db.Users
where x.UserName == "VahidNasiri"
select x;

//اگر چیزی یافت شد
if (query.Any())
{
User vahid = query.First();
//نمایش اطلاعات کاربر
Console.WriteLine("Id: {0}, UserName: {0}", vahid.Id, vahid.UserName);
//به روز رسانی نام کاربر
vahid.UserName = "Vahid";
session.Update(vahid);

transaction.Commit(); //پایان تراکنش
}
}
}
}
}

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
مباحث تکمیلی AutoMapping

اگر به اسکریپت دیتابیس تولید شده دقت کرده باشید، عملیات AutoMapping یک سری پیش فرض‌هایی را اعمال کرده است. برای مثال فیلد Id را از نوع identity و به صورت کلید تعریف کرده، یا رشته‌ها را به صورت nvarchar با طول 255 ایجاد نموده است. امکان سفارشی سازی این موارد نیز وجود دارد.

مثال:

using FluentNHibernate.Conventions.Helpers;

public static Configuration GenerateMapping(IPersistenceConfigurer dbType)
{
var cfg = dbType.ConfigureProperties(new Configuration());

new AutoPersistenceModel()
.Conventions.Add()
.Where(x => x.Namespace.EndsWith("Domain"))
.Conventions.Add(
PrimaryKey.Name.Is(x => "ID"),
DefaultLazy.Always(),
ForeignKey.EndsWith("ID"),
Table.Is(t => "tbl" + t.EntityType.Name)
)
.AddEntityAssembly(typeof(NHSample2.Domain.News).Assembly)
.Configure(cfg);

return cfg;
}

تابع GenerateMapping معرفی شده را اینجا با قسمت Conventions.Add تکمیل کرده‌ایم. به این صورت دقیقا مشخص شده است که فیلدهایی با نام ID باید primary key در نظر گرفته شوند، همواره lazy loading صورت گیرد و نام کلید خارجی به ID ختم شود. همچنین نام جداول با tbl شروع گردد.
روش دیگری نیز برای معرفی این قرار دادها و پیش فرض‌ها وجود دارد. فرض کنید می‌خواهیم طول رشته پیش فرض را از 255 به 500 تغییر دهیم. برای اینکار باید اینترفیس IPropertyConvention را پیاده سازی کرد:

using FluentNHibernate.Conventions;
using FluentNHibernate.Conventions.Instances;

namespace NHSample2.Conventions
{
class MyStringLengthConvention : IPropertyConvention
{
public void Apply(IPropertyInstance instance)
{
instance.Length(500);
}
}
}
سپس نحوه‌ی معرفی آن به صورت زیر خواهد بود:

public static Configuration GenerateMapping(IPersistenceConfigurer dbType)
{
var cfg = dbType.ConfigureProperties(new Configuration());

new AutoPersistenceModel()
.Conventions.Add()
.Where(x => x.Namespace.EndsWith("Domain"))
.Conventions.Add<MyStringLengthConvention>()
.AddEntityAssembly(typeof(NHSample2.Domain.News).Assembly)
.Configure(cfg);

return cfg;
}

نکته:
اگر برای یافتن اطلاعات بیشتر در این مورد در وب جستجو کنید، اکثر مثال‌هایی را که مشاهده خواهید کرد بر اساس نگارش بتای fluent NHibernate هستند و هیچکدام با نگارش نهایی این فریم ورک کار نمی‌کنند. در نگارش رسمی نهایی ارائه شده، تغییرات بسیاری صورت گرفته که آن‌ها را در این آدرس می‌توان مشاهده کرد.

دریافت سورس برنامه قسمت ششم


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

نظرات مطالب
Defensive Programming - بازگشت نتایج قابل پیش بینی توسط متدها
public class Result
{
    public bool Success { get; private set; }
    public string Error { get; private set; }
    public bool Failure { /* … */ }
    protected Result(bool success, string error) { /* … */ }
    public static Result Fail(string message) { /* … */ }
    public static Result<T> Ok<T>(T value) {  /* … */ }
}

public class Result<T> : Result
{
    public T Value { get; set; }
    protected internal Result(T value, bool success, string error)
        : base(success, error)
    {
        /* … */
    }
}

مطالب
یکی کردن اسمبلی‌های یک پروژه‌ی WPF
فرض کنید پروژه‌ی WPF شما از چندین پروژه‌ی ‍Class library و اسمبلی‌های جانبی دیگر، تشکیل شده‌است. اکنون نیاز است جهت سهولت توزیع آن، تمام این فایل‌ها را با هم یکی کرده و تبدیل به یک فایل EXE نهایی کنیم. مایکروسافت ابزاری را به نام ILMerge، برای یک چنین کارهایی تدارک دیده‌است؛ اما این برنامه با WPF سازگار نیست. در ادامه قصد داریم اسمبلی‌های جانبی را تبدیل به منابع مدفون شده در فایل EXE برنامه کرده و سپس آن‌ها را در اولین بار اجرای برنامه، به صورت خودکار بارگذاری و در برنامه مورد استفاده قرار دهیم.

یک مثال جهت بازتولید کدهای این مطلب
الف) یک پروژه‌ی WPF جدید را به نام MergeAssembliesIntoWPF ایجاد کنید.
ب) یک پروژه‌ی Class library جدید را به نام MergeAssembliesIntoWPF.ViewModels به این Solution اضافه کنید. از آن برای تعریف ViewModelهای برنامه استفاده خواهیم کرد.
برای نمونه کلاس ذیل را به آن اضافه کنید:
namespace MergeAssembliesIntoWPF.ViewModels
{
    public class ViewModel1
    {
        public string Data { set; get; }

        public ViewModel1()
        {
            Data = "Test";
        }
    }
}
ج) یک پروژه‌ی WPF User control library را نیز به نام MergeAssembliesIntoWPF.Shell به این Solution اضافه کنید. از آن برای تعریف Viewهای برنامه کمک خواهیم گرفت.
به این پروژه ارجاعی را به اسمبلی قسمت (ب) اضافه نموده و برای نمونه User control ذیل را به نام View1.xaml به آن اضافه نمائید:
<UserControl x:Class="MergeAssembliesIntoWPF.Shell.View1"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             xmlns:VM="clr-namespace:MergeAssembliesIntoWPF.ViewModels;assembly=MergeAssembliesIntoWPF.ViewModels"
             d:DesignHeight="300" d:DesignWidth="300">
    <UserControl.Resources>
        <VM:ViewModel1 x:Key="ViewModel1" />
    </UserControl.Resources>
    <Grid DataContext="{Binding Source={StaticResource ViewModel1}}">
        <TextBlock Text="{Binding Data}" />
    </Grid>
</UserControl>
در پروژه اصلی Solution (قسمت الف)، ارجاعاتی را به دو اسمبلی قسمت‌های ب و ج اضافه کنید. سپس MainWindow.xaml آن‌را به نحو ذیل تغییر داده و برنامه را اجرا کنید:
<Window x:Class="MergeAssembliesIntoWPF.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:V="clr-namespace:MergeAssembliesIntoWPF.Shell;assembly=MergeAssembliesIntoWPF.Shell"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <V:View1 x:Key="View1" />
    </Window.Resources>
    <Grid>
        <V:View1 />
    </Grid>
</Window>
تا اینجا باید متن Test در پنجره اصلی برنامه ظاهر شود.


ب) مدفون کردن خودکار اسمبلی‌های جانبی برنامه در فایل EXE آن
فایل csproj پروژه اصلی را خارج از VS.NET باز کنید. در انتهای آن سطر ذیل قابل مشاهده است:
 <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
پس از این سطر، چند سطر ذیل را اضافه کنید:
  <Target Name="AfterResolveReferences">
    <ItemGroup>
      <EmbeddedResource Include="@(ReferenceCopyLocalPaths)" Condition="'%(ReferenceCopyLocalPaths.Extension)' == '.dll'">
        <LogicalName>%(ReferenceCopyLocalPaths.DestinationSubDirectory)%(ReferenceCopyLocalPaths.Filename)%(ReferenceCopyLocalPaths.Extension)</LogicalName>
      </EmbeddedResource>
    </ItemGroup>
  </Target>
این task جدید MSBuild سبب خواهد شد تا با هر بار Build برنامه، اسمبلی‌هایی که در ارجاعات برنامه دارای خاصیت Copy local مساوی true هستند، به صورت خودکار به صورت یک resource جدید در فایل exe برنامه مدفون شوند. عموما ارجاعاتی که دستی اضافه می‌شوند، مانند دو اسمبلی یاد شده در ابتدای بحث، دارای خاصیت Copy local=true نیز هستند.
پس از این تغییر نیاز است یکبار پروژه را بسته و مجددا باز کنید. اکنون پروژه را build کنید و جهت اطمینان بیشتر آن‌را برای مثال توسط ILSpy مورد بررسی قرار دهید:


همانطور که مشاهده می‌کنید، دو اسمبلی مورد استفاده در برنامه به صورت خودکار در قسمت منابع فایل EXE مدفون شده‌اند.
اگر به مسیر LogicalName تنظیمات فوق دقت کنید، DestinationSubDirectory نیز ذکر شده‌است. علت این است که بسیاری از اسمبلی‌های بومی سازی شده WPF با نام‌هایی یکسان اما در پوشه‌هایی مانند fa، fr و امثال آن ذخیره می‌شوند. به همین جهت نیاز است بین این‌ها تمایز قائل شد.


ج) بارگذاری خودکار اسمبلی‌ها در AppDomain برنامه

تا اینجا اسمبلی‌های جانبی را در فایل EXE مدفون کرده‌ایم. اکنون نوبت به بارگذاری آن‌ها در AppDomain برنامه است. برای اینکار نیاز است تا روال رخدادگردان AppDomain.CurrentDomain.AssemblyResolve را تحت نظر قرار داده و اسمبلی‌هایی را که برنامه درخواست می‌کند، در همینجا از منابع خوانده و به AppDomain اضافه کرد.
انجام اینکار در برنامه‌های WinForms ساده‌است. فقط کافی است به متد Program.Main برنامه مراجعه کرده و تعریف یاد شده را به ابتدای متد Main اضافه کرد. اما در WPF هرچند فایل App.xaml.cs به نظر نقطه‌ی آغازین برنامه است، اما در واقع اینطور نیست. برای نمونه، پوشه‌ی obj\Debug برنامه را گشوده و فایل App.g.i.cs آن‌را بررسی کنید. در اینجا می‌توانید همان رویه شبیه به برنامه‌های WinForm را در متد Program.Main آن، مشاهده کنید. بنابراین نیاز است کنترل این مساله را راسا در دست بگیریم:
using System;
using System.Globalization;
using System.Reflection;

namespace MergeAssembliesIntoWPF
{
    public class Program
    {
        [STAThreadAttribute]
        public static void Main()
        {
            AppDomain.CurrentDomain.AssemblyResolve += OnResolveAssembly;
            App.Main();
        }

        private static Assembly OnResolveAssembly(object sender, ResolveEventArgs args)
        {
            var executingAssembly = Assembly.GetExecutingAssembly();
            var assemblyName = new AssemblyName(args.Name);

            var path = assemblyName.Name + ".dll";
            if (assemblyName.CultureInfo.Equals(CultureInfo.InvariantCulture) == false)
            {
                path = String.Format(@"{0}\{1}", assemblyName.CultureInfo, path);
            }

            using (var stream = executingAssembly.GetManifestResourceStream(path))
            {
                if (stream == null)
                    return null;

                var assemblyRawBytes = new byte[stream.Length];
                stream.Read(assemblyRawBytes, 0, assemblyRawBytes.Length);
                return Assembly.Load(assemblyRawBytes);
            }
        }
    }
}
کلاس Program را با تعاریف فوق به پروژه خود اضافه نمائید. در اینجا Program.Main مورد نیاز خود را تدارک دیده‌ایم. کار آن مدیریت روال رخدادگردان AppDomain.CurrentDomain.AssemblyResolve برنامه پیش از شروع به هر کاری است. در روال رخداد گردان OnResolveAssembly، برنامه اعلام می‌کند که به چه اسمبلی خاصی نیاز دارد. ما آن‌را از قسمت منابع خوانده و سپس توسط متد Assembly.Load آن‌را در AppDomain برنامه بارگذاری می‌کنیم.
پس از اینکه کلاس فوق را اضافه کردید، نیاز است کلاس Program اضافه شده را به عنوان Startup object برنامه نیز معرفی کنید:

انجام اینکار ضروری است؛ در غیراینصورت با متد Main موجود در فایل App.g.i.cs تداخل می‌کند.
اکنون برای آزمایش برنامه، یکبار آن‌را Build کرده و بجز فایل Exe، مابقی فایل‌های موجود در پوشه‌ی bin را حذف کنید. سپس برنامه را خارج از VS.NET اجرا کنید. کار می‌کند!
MergeAssembliesIntoWPF.zip
 
نظرات مطالب
آشنایی با مفاهیم نوع داده Enum و توسعه آن - قسمت دوم
وقتی کلاس Description در   فضای نام  System.ComponentModel وجود داره دلیلی نداره  کلاس مشابه ای تعریف کنیم.
بخاطر اینکه مصرف کننده محض نباشیم یک متد الحاقی به نام ()GetEnumList اضافه کردم که لیست اعضای یک Enum  رو برای استفاده در کمبو باکس و ... بر می‌گرودنه :
ابتدا کلاس زیر به کلاس ExtensionMethodCls اضافه می‌کنیم :
 public class EnumObject
        {
            public Enum ValueMember { get; set; }
            public int intValueMember
            {
                get { return int.Parse(ValueMember.ToString("D")); }
            }
            public string stringValueMember
            {
                get { return ValueMember.ToString(""); }
            }
            public string DisplayMember
            {
                get { return ValueMember.GetDescription(); }
            }
        }
و متد الحاقی زیر رو برای گرفتن لیست تعریف می‌کنیم:
      public static List<EnumObject> GetEnumList(this Enum enu)
        {
            List<EnumObject> li = new List<EnumObject>();
            foreach (var item in enu.GetType().GetEnumValues())
            {
                li.Add(new EnumObject { ValueMember = (Enum)item });
            }
            return li;
        }
نحوه استفاده  :
          comboBox1.DataSource = Grade.VeryGood.GetEnumList();
          comboBox1.DisplayMember = "DisplayMember";
          comboBox1.ValueMember = "ValueMember";
همون طوری که در کد بالا می‌بینید برای گرفتن لیست مجبور شدیم یکی از اعضای enum  رو انتخاب کنیم (Grade.VeryGood.GetEnumList()) شاید انتخاب یکی از اعضا و بعد درخواست لیست اعضا رو کردن کار قشنگی نباشه به همین دلیل متد زیر رو تعریف کردیم :
        public static List<EnumObject> EnumToList<T>()
        {
            Type enumType = typeof(T);
            if (enumType.BaseType != typeof(Enum))
                throw new ArgumentException("T must be of type System.Enum");

            List<EnumObject> li = new List<EnumObject>();
            foreach (var item in enumType.GetEnumValues())
            {
                li.Add(new EnumObject { ValueMember = (Enum)item });
            }
            return li;
        }
نحوه استفاده :
    comboBox1.DataSource =ExtensionMethodCls.EnumToList<Grade>();
          comboBox1.DisplayMember = "DisplayMember";
          comboBox1.ValueMember = "ValueMember";
کد کامل :
 public static class ExtensionMethodCls
    {
        public class EnumObject
        {
            public Enum ValueMember { get; set; }
            public int intValueMember
            {
                get { return int.Parse(ValueMember.ToString("D")); }
            }
            public string stringValueMember
            {
                get { return ValueMember.ToString(""); }
            }
            public string DisplayMember
            {
                get { return ValueMember.GetDescription(); }
            }
        }
 
    

        public static List<EnumObject> EnumToList<T>()
        {
            Type enumType = typeof(T);
            if (enumType.BaseType != typeof(Enum))
                throw new ArgumentException("T must be of type System.Enum");

            List<EnumObject> li = new List<EnumObject>();
            foreach (var item in enumType.GetEnumValues())
            {
                li.Add(new EnumObject { ValueMember = (Enum)item });
            }
            return li;
        }

        public static List<EnumObject> GetEnumList(this Enum enu)
        {
            List<EnumObject> li = new List<EnumObject>();
            foreach (var item in enu.GetType().GetEnumValues())
            {
                li.Add(new EnumObject { ValueMember = (Enum)item });
            }
            return li;
        }

        public static string GetDescription(this Enum enu)
        {

            Type type = enu.GetType();

            MemberInfo[] memInfo = type.GetMember(enu.ToString());

            if (memInfo != null && memInfo.Length > 0)
            {

                object[] attrs = memInfo[0].GetCustomAttributes(typeof(DescriptionAttribute), false);

                if (attrs != null && attrs.Length > 0)
                    return ((DescriptionAttribute)attrs[0]).Description;
            }

            return enu.ToString();

        }
    }
مطالب
MVVM و امکان استفاده از یک وهله از ViewModel جهت چند View مرتبط

عموما هنگام طراحی یک View، خیلی زود به حجم انبوهی از کدهای XAML خواهیم رسید. در ادامه بررسی خواهیم کرد که چطور می‌توان یک View را به چندین View خرد کرد، بدون اینکه نیازی باشد تا از چندین ViewModel (یا همان code behind عاری از ارجاعات بصری سابق قرار گرفته در یک پروژه جدای دیگر) استفاده شود و تمام این View های خرد شده هم تنها از یک وهله از ViewModel ایی خاص استفاده کنند و با اطلاعاتی یکپارچه سروکار داشته باشند؛ یا در عمل یکپارچه کار کنند.
این مشکل از جایی شروع می‌شود که مثلا خرد کردن یک user control به چند یوزر کنترل، یعنی کار کردن با چند وهله از اشیایی متفاوت. هر چند نهایتا تمام این‌ها قرار است در یک صفحه در کنار هم قرار گیرند اما در عمل از هم کاملا مجزا هستند و اگر به ازای هر کدام یکبار ViewModel را وهله سازی کنیم، به مشکل برخواهیم خورد؛ چون هر وهله نسبت به وهله‌ای دیگر ایزوله است. اگر در یکی Name مثلا Test بود در دیگری ممکن است مقدار پیش فرض نال را داشته باشد؛ چون با چند وهله از یک کلاس، در یک فرم نهایی سروکار خواهیم داشت.

ابتدا Model و ViewModel ساده زیر را در نظر بگیرید:
using System.ComponentModel;

namespace SplittingViewsInMvvm.Models
{
public class GuiModel : INotifyPropertyChanged
{
string _name;
public string Name
{
get { return _name; }
set
{
_name = value;
raisePropertyChanged("Name");
}
}

string _lastName;
public string LastName
{
get { return _lastName; }
set
{
_lastName = value;
raisePropertyChanged("LastName");
}
}

#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
void raisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler == null) return;
handler(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
}

using SplittingViewsInMvvm.Models;

namespace SplittingViewsInMvvm.ViewModels
{
public class MainViewModel
{
public GuiModel GuiModelData { set; get; }

public MainViewModel()
{
GuiModelData = new GuiModel();
GuiModelData.Name = "Name";
GuiModelData.LastName = "LastName";
}
}
}

سپس View زیر هم از این اطلاعات استفاده خواهد کرد:

<UserControl x:Class="SplittingViewsInMvvm.Views.Main"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
xmlns:VM="clr-namespace:SplittingViewsInMvvm.ViewModels"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<VM:MainViewModel x:Key="vmMainViewModel" />
</UserControl.Resources>
<StackPanel DataContext="{Binding Source={StaticResource vmMainViewModel}}">
<GroupBox Margin="2" Header="Group 1">
<TextBlock Text="{Binding GuiModelData.Name}" />
</GroupBox>
<GroupBox Margin="2" Header="Group 2">
<TextBlock Text="{Binding GuiModelData.LastName}" />
</GroupBox>
</StackPanel>
</UserControl>

اکنون فرض کنید که می‌خواهیم Group 1 و Group 2 را جهت مدیریت ساده‌تر View اصلی در دو user control مجزا قرار دهیم؛ مثلا:

<UserControl x:Class="SplittingViewsInMvvm.Views.Group1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<GroupBox Margin="2" Header="Group 1">
<TextBlock Text="{Binding GuiModelData.Name}" />
</GroupBox>
</Grid>
</UserControl>
و
<UserControl x:Class="SplittingViewsInMvvm.Views.Group2"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<GroupBox Margin="2" Header="Group 2">
<TextBlock Text="{Binding GuiModelData.LastName}" />
</GroupBox>
</Grid>
</UserControl>

اکنون اگر این دو را مجددا در همان View اصلی ساده شده قبلی قرار دهیم (بدون اینکه در هر user control به صورت جداگانه data context را تنظیم کنیم):
<UserControl x:Class="SplittingViewsInMvvm.Views.Main"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
xmlns:V="clr-namespace:SplittingViewsInMvvm.Views"
xmlns:VM="clr-namespace:SplittingViewsInMvvm.ViewModels"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<VM:MainViewModel x:Key="vmMainViewModel" />
</UserControl.Resources>
<StackPanel DataContext="{Binding Source={StaticResource vmMainViewModel}}">
<V:Group1 />
<V:Group2 />
</StackPanel>
</UserControl>

باز هم .... برنامه همانند سابق کار خواهد کرد و ViewModel وهله سازی شده در user control فوق به صورت یکسانی در اختیار هر دو View اضافه شده قرار می‌گیرد و نهایتا یک View یکپارچه را در زمان اجرا می‌توان مورد استفاده قرار داد. علت هم بر می‌گردد به مقدار دهی خودکار DataContext هر View اضافه شده به بالاترین DataContext موجود در Visual tree که ذکر آن الزامی نیست:

<UserControl x:Class="SplittingViewsInMvvm.Views.Main"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
xmlns:V="clr-namespace:SplittingViewsInMvvm.Views"
xmlns:VM="clr-namespace:SplittingViewsInMvvm.ViewModels"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<VM:MainViewModel x:Key="vmMainViewModel" />
</UserControl.Resources>
<StackPanel DataContext="{Binding Source={StaticResource vmMainViewModel}}">
<V:Group1 DataContext="{Binding}" />
<V:Group2 DataContext="{Binding}"/>
</StackPanel>
</UserControl>


بنابراین به صورت خلاصه زمانیکه از MVVM استفاده ‌می‌کنید لازم نیست کار خاصی را جهت خرد کردن یک View به چند Sub View انجام دهید! فقط این‌ها را در چند User control جدا کنید و بعد مجددا به کمک فضای نامی که تعریف خواهد (مثلا V در اینجا) در همان View اصلی تعریف کنید. بدون هیچ تغییر خاصی باز هم برنامه همانند سابق کار خواهد کرد.


مطالب
ASP.NET MVC #22

تهیه سایت‌های چند زبانه و بومی سازی نمایش اطلاعات در ASP.NET MVC

زمانیکه دات نت فریم ورک نیاز به انجام اعمال حساس به مسایل بومی را داشته باشد،‌ ابتدا به مقادیر تنظیم شده دو خاصیت زیر دقت می‌کند:
الف) System.Threading.Thread.CurrentThread.CurrentCulture
بر این اساس دات نت می‌تواند تشخیص دهد که برای مثال خروجی متد DateTime.Now.ToString در کانادا و آمریکا باید با هم تفاوت داشته باشند. مثلا در آمریکا ابتدا ماه، سپس روز و در آخر سال نمایش داده می‌شود و در کانادا ابتدا سال، بعد ماه و در آخر روز نمایش داده خواهد شد. یا نمونه‌ی دیگری از این دست می‌تواند نحوه نمایش علامت واحد پولی کشورها باشد.
ب) System.Threading.Thread.CurrentThread.CurrentUICulture
مقدار CurrentUICulture بر روی بارگذاری فایل‌های مخصوصی به نام Resource، تاثیر گذار است.

این خواص را یا به صورت دستی می‌توان تنظیم کرد و یا ASP.NET، این اطلاعات را از هدر Accept-Language دریافتی از مرورگر کاربر به صورت خودکار مقدار دهی می‌کند. البته برای این منظور نیاز است یک سطر زیر را به فایل وب کانفیگ برنامه اضافه کرد:

<system.web>
<globalization culture="auto" uiCulture="auto" />

یا اگر نیاز باشد تا برنامه را ملزم به نمایش اطلاعات Resource مرتبط با فرهنگ بومی خاصی کرد نیز می‌توان در همین قسمت مقادیر culture و uiCulture را دستی تنظیم نمود و یا اگر همانند برنامه‌هایی که چند لینک را بالای صفحه نمایش می‌دهند که برای مثال به نگارش‌های فارسی/عربی/انگلیسی اشاره می‌کند، اینکار را با کد نویسی نیز می‌توان انجام داد:

System.Threading.Thread.CurrentThread.CurrentCulture =
System.Globalization.CultureInfo.CreateSpecificCulture("fa");


جهت آزمایش این مطلب، ابتدا تنظیم globalization فوق را به فایل وب کانفیگ برنامه اضافه کنید. سپس به مسیر زیر در IE مراجعه کنید:

IE -> Tools -> Intenet options -> Genarl tab -> Languages

در اینجا می‌توان هدر Accept-Language را مقدار دهی کرد. برای نمونه اگر مقدار زبان پیش فرض را به فرانسه تنظیم کنیم (به عنوان اولین زبان تعریف شده در لیست) و سپس سعی در نمایش مقدار decimal زیر را داشته باشیم:

string.Format("{0:C}", 10.5M)

اگر زبان پیش فرض، انگلیسی آمریکایی باشد، $ نمایش داده خواهد شد و اگر زبان به فرانسه تنظیم شود، یورو در کنار عدد مبلغ نمایش داده می‌شود.
تا اینجا تنها با تنظیم culture=auto به این نتیجه رسیده‌ایم. اما سایر قسمت‌های صفحه چطور؟ برای مثال برچسب‌های نمایش داده شده را چگونه می‌توان به صورت خودکار بر اساس Accept-Language مرجح کاربر تنظیم کرد؟ خوشبختانه در دات نت، زیر ساخت مدیریت برنامه‌های چند زبانه به صورت توکار وجود دارد که در ادامه به بررسی آن خواهیم پرداخت.


آشنایی با ساختار فایل‌های Resource


فایل‌های Resource یا منبع، در حقیقت فایل‌هایی هستند مبتنی بر XML با پسوند resx و هدف آن‌ها ذخیره سازی رشته‌های متناظر با فرهنگ‌های مختلف می‌باشد و برای استفاده از آن‌ها حداقل یک فایل منبع پیش فرض باید تعریف شود. برای نمونه فایل mydata.resx را در نظر بگیرید. برای ایجاد فایل منبع اسپانیایی متناظر، باید فایلی را به نام mydata.es.resx تولید کرد. البته نوع فرهنگ مورد استفاده را کاملتر نیز می‌توان ذکر کرد برای مثال mydata.es-mex.resx جهت فرهنگ اسپانیایی مکزیکی بکارگرفته خواهد شد، یا mydata.fr-ca.resx به فرانسوی کانادایی اشاره می‌کند. سپس مدیریت منابع دات نت فریم ورک بر اساس مقدار CurrentUICulture جاری، اطلاعات فایل متناظری را بارگذاری خواهد کرد. اگر فایل متناظری وجود نداشت، از اطلاعات همان فایل پیش فرض استفاده می‌گردد.
حین تهیه برنامه‌ها نیازی نیست تا مستقیما با فایل‌های XML منابع کار کرد. زمانیکه اولین فایل منبع تولید می‌شود، به همراه آن یک فایل cs یا vb نیز ایجاد خواهد شد که امکان دسترسی به کلیدهای تعریف شده در فایل‌های XML را به صورت strongly typed میسر می‌کند. این فایل‌های خودکار، تنها برای فایل پیش فرض mydata.resx تولید می‌شوند،‌از این جهت که تعاریف اطلاعات سایر فرهنگ‌های متناظر نیز باید با همان کلیدهای فایل پیش فرض آغاز شوند. تنها «مقادیر» کلیدهای تعریف شده در کلاس‌های منبع متفاوت هستند.
اگر به خواص فایل‌های resx در VS.NET دقت کنیم، نوع Build action آن‌ها به embedded resource تنظیم شده است.


مثالی جهت بررسی استفاده از فایل‌های Resource

یک پروژه جدید خالی ASP.NET MVC را آغاز کنید. فایل وب کانفیگ آن‌را ویرایش کرده و تنظیمات globalization ابتدای بحث را به آن اضافه کنید. سپس مدل، کنترلر و View متناظر با متد Index آن‌را با محتوای زیر به پروژه اضافه نمائید:

namespace MvcApplication19.Models
{
public class Employee
{
public int Id { set; get; }
public string Name { set; get; }
}
}

using System.Web.Mvc;
using MvcApplication19.Models;

namespace MvcApplication19.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
var employee = new Employee { Name = "Name 1" };
return View(employee);
}
}
}

@model MvcApplication19.Models.Employee
@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
<fieldset>
<legend>Employee</legend>
<div class="display-label">
Name
</div>
<div class="display-field">
@Html.DisplayFor(model => model.Name)
</div>
</fieldset>
<fieldset>
<legend>Employee Info</legend>
@Html.DisplayForModel()
</fieldset>

قصد داریم در View فوق بر اساس uiCulture کاربر مراجعه کننده به سایت، برچسب Name را مقدار دهی کنیم. اگر کاربری از ایران مراجعه کند، «نام کارمند» نمایش داده شود و سایر کاربران، «Employee Name» را مشاهده کنند. همچنین این تغییرات باید بر روی متد Html.DisplayForModel نیز تاثیرگذار باشد.
برای این منظور بر روی پوشه Views/Home که محل قرارگیری فایل Index.cshtml فوق است کلیک راست کرده و گزینه Add|New Item را انتخاب کنید. سپس در صفحه ظاهر شده، گزینه «Resources file» را انتخاب کرده و برای مثال نام Index_cshtml.resx را وارد کنید.
به این ترتیب اولین فایل منبع مرتبط با View جاری که فایل پیش فرض نیز می‌باشد ایجاد خواهد شد. این فایل، به همراه فایل Index_cshtml.Designer.cs تولید می‌شود. سپس همین مراحل را طی کنید، اما اینبار نام Index_cshtml.fa.resx را حین افزودن فایل منبع وارد نمائید که برای تعریف اطلاعات بومی ایران مورد استفاده قرار خواهد گرفت. فایل دومی که اضافه شده است، فاقد فایل cs همراه می‌باشد.
اکنون فایل Index_cshtml.resx را در VS.NET باز کنید. از بالای صفحه، به کمک گزینه Access modifier، سطح دسترسی متدهای فایل cs همراه آن‌را به public تغییر دهید. پیش فرض آن internal است که برای کار ما مفید نیست. از این جهت که امکان دسترسی به متدهای استاتیک تعریف شده در فایل خودکار Index_cshtml.Designer.cs را در View های برنامه، نخواهیم داشت. سپس دو جفت «نام-مقدار» را در فایل resx وارد کنید. مثلا نام را Name و مقدار آن‌را «Employee Name» و سپس نام دیگر را NameIsNotRight و مقدار آن‌را «Name is required» وارد نمائید.
در ادامه فایل Index_cshtml.fa.resx را باز کنید. در اینجا نیز دو جفت «نام-مقدار» متناظر با فایل پیش فرض منبع را باید وارد کرد. کلیدها یا نام‌ها یکی است اما قسمت مقدار اینبار باید فارسی وارد شود. مثلا نام را Name و مقدار آن‌را «نام کارمند» وارد نمائید. سپس کلید یا نام NameIsNotRight و مقدار «لطفا نام را وارد نمائید» را تنظیم نمائید.
تا اینجا کار تهیه فایل‌های منبع متناظر با View جاری به پایان می‌رسد.
در ادامه با کمک فایل Index_cshtml.Designer.cs که هربار پس از تغییر فایل resx متناظر آن به صورت خودکار توسط VS.NET تولید و به روز می‌شود، می‌توان به کلیدها یا نام‌هایی که تعریف کرده‌ایم، در قسمت‌های مختلف برنامه دست یافت. برای نمونه تعریف کلید Name در این فایل به نحو زیر است:

namespace MvcApplication19.Views.Home {
public class Index_cshtml {
public static string Name {
get {
return ResourceManager.GetString("Name", resourceCulture);
}
}
}
}

بنابراین برای استفاده از آن در هر View ایی تنها کافی است بنویسیم:

@MvcApplication19.Views.Home.Index_cshtml.Name

به این ترتیب بر اساس تنظیمات محلی کاربر، اطلاعات به صورت خودکار از فایل‌های Index_cshtml.fa.resx فارسی یا فایل پیش فرض Index_cshtml.resx، دریافت می‌گردد.
علاوه بر امکان دسترسی مستقیم به کلیدهای تعریف شده در فایل‌های منبع، امکان استفاده از آن‌ها توسط data annotations نیز میسر است. در این حالت می‌توان مثلا پیغام‌های اعتبار سنجی را بومی کرد یا حین استفاده از متد Html.DisplayForModel، بر روی برچسب نمایش داده شده خودکار، تاثیر گذار بود. برای اینکار باید اندکی مدل برنامه را ویرایش کرد:

using System.ComponentModel.DataAnnotations;

namespace MvcApplication19.Models
{
public class Employee
{
[ScaffoldColumn(false)]
public int Id { set; get; }

[Display(ResourceType = typeof(MvcApplication19.Views.Home.Index_cshtml),
Name = "Name")]
[Required(ErrorMessageResourceType = typeof(MvcApplication19.Views.Home.Index_cshtml),
ErrorMessageResourceName = "NameIsNotRight")]
public string Name { set; get; }
}
}

همانطور که ملاحظه می‌کنید، حین تعریف ویژگی‌های Display یا Required، امکان تعریف نام کلاس متناظر با فایل resx خاصی وجود دارد. به علاوه ErrorMessageResourceName به نام یک کلید در این فایل و یا پارامتر Name ویژگی Display نیز به نام کلیدی در فایل منبع مشخص شده، اشاره می‌کنند. این اطلاعات توسط متدهای Html.DisplayForModel، Html.ValidationMessageFor، Html.LabelFor و امثال آن به صورت خودکار مورد استفاده قرار خواهند گرفت.


نکته‌ای در مورد کش کردن اطلاعات
در این مثال اگر فیلتر OutputCache را بر روی متد Index تعریف کنیم، حتما نیاز است به هدر Accept-Language نیز دقت داشت. در غیراینصورت تمام کاربران، صرفنظر از تنظیمات بومی آن‌ها، یک صفحه را مشاهده خواهند کرد:

[OutputCache(Duration = 60, VaryByHeader = "Accept-Language")]
public ActionResult Index()