البته در کنار گذاشتن جی کوئری این نکته باید مدنظر باشه که تعدادی زیادی کتابخانه جی کوئری داریم.
در ادامه قسمت قبلی
قصد داریم دو کنترل دیگر را نیز بایند کنیم؛ ولی از آنجا که مقادیر آنها
رشتهای یا عددی نیست و مقداری متفاوت هست، از مبحثی به نام ValueConverter
استفاده خواهیم کرد.
Value Converter چیست؟
موقعی که شما قصد بایند کردن دو نوع داده متفاوت را به هم دارید، نیاز
به یک کد واسط پیدا میکنید تا این کد واسط مقادیر شما را از مبدا دریافت
کرده و تبدیل به نوعی کند که مقصد بتواند از آن استفاده کند یا بلعکس.
ValueConverter نام کلاسی است که از یک اینترفیس به نام IValueConverter
ارث بری کرده است و شامل دو متد تبدیل نوع از مبدا به مقصد Convert و دیگری
از مقصد به مبدا ConvertBack میشود که خیلی کمتر پیاده سازی میشود.
پیاده سازی یک کلاس مبدل سه مرحله دارد:
در مرحله اول، نحوه پیاده سازی کلاس ValueConverter به شکل زیر است:
پیاده سازی یک کلاس مبدل سه مرحله دارد:
- مرحله اول :ساخت کلاس ValueConverter
- مرحله دوم : تعریف آن به عنوان یک منبع یا ریسورس
- مرحله سوم : استفاده از آن در عملیات بایند کردن Binding
در مرحله اول، نحوه پیاده سازی کلاس ValueConverter به شکل زیر است:
public class BoolToVisibilityConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { // Do the conversion from bool to visibility } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { // Do the conversion from visibility to bool } }
در متد تبدیل باید مقداری را که کنترل نیاز دارد، بر اساس مقادیر کلاس، ایجاد و بازگشت دهید.
در مرحلهی دوم نحوه تعریف مبدل ما در پنجره XAML به صورت زیر میباشد:
<Window x:Class="test.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:نامی دلخواه="clr-namespace:فضای نامی که کلاس مبدل در آن قرار دارد" > <Window.Resources> <نام دلخواهی که در بالا تعریف کرده اید: ClassNameنام کلاس x:Key="کلید این آیتم در ریسورس"/> </Window.Resources>
کلمههای کلیدی xmlns که مخفف XML NameSpace هستند جهت تعریف دسترسی به فضاهای نام طراحی شدهاند و طبق تعریف مایکروسافت
همان مفهوم تگهای تعریف فضای نام در XML را دارند که توسط مایکروسافت توسعه
یافتهاند. این تعریفها در تگ ریشه (در اینجا window) تعریف میشوند.
دو فضای نام اولی که به طور پیش فرض در همه جای پروژه قرار دارند، اشاره به فریم
ورک WPF دارند. کلمهی کلیدی x در خط شماره سه، نام دلخواهی است که دسترسی ما
را به خصوصیات یا تعاریف XAML موجود در sdk باز میکند؛ مثلا استفاده از
خصوصیاتی چون x:key یا x:class را به همراه دارد.
پس الان باید خط چهارم برای ما روشن باشد؛ فضای نام جدیدی را در برنامه خودمان ایجاد کردهایم که این تگ به آن اشاره میکند و نام دلخواهی هم برای اشاره به این فضای نام برایش در نظر گرفتهایم. هر موقع در برنامه این نام دلخواه تعیین شده قرار گیرد، یعنی اشاره به این فضای نام که در قسمت Window.resource خط هشتم تعریف شده است.
در خط هشتم، یک ریسورس (منبع) را به برنامه معرفی کردهایم:
ریسورسها برای ذخیره سازی دادهها در سطح یک کنترل، سطح محلی در یک پنجره، یا سطح عمومی در کل پروژه به کار میروند. محدودیتی در ذخیره دادهها وجود ندارد و هر چقدر که دوست دارید میتوانید داده به آن پاس کنید. این دادهها میتوانند یک سری اطلاعات ذخیره شده در یک ساختار ساده تا یک ساختار سلسه مراتبی از کنترلها باشند. ریسورسها به شما این اجازه را میدهند تا دادهها را در یک مکان ذخیره کرده و آنها را در یک یا چندجا مورد استفاده قرار دهید.
از آن جا که مباحث ریسورسها را در یک مقالهی جداگانه بررسی میکنیم، فقط به ذکر نکات بالا جهت کد فعلی بسنده خواهیم کرد و ادامهی آن را در یک مقاله دیگر مورد بررسی قرار میدهیم.
هر ریسورس دارای یک نام یا یک کلید است که با خصوصیت x:key تعریف میشود.
ریسورس بالا یک کلاس را که در فضای نام دلخواهی قرار دارد، تعریف میکند و یک کلید هم به آن انتساب میدهد.
مرحلهی سوم معرفی ریسورس به عملیات Binding است:
پس الان باید خط چهارم برای ما روشن باشد؛ فضای نام جدیدی را در برنامه خودمان ایجاد کردهایم که این تگ به آن اشاره میکند و نام دلخواهی هم برای اشاره به این فضای نام برایش در نظر گرفتهایم. هر موقع در برنامه این نام دلخواه تعیین شده قرار گیرد، یعنی اشاره به این فضای نام که در قسمت Window.resource خط هشتم تعریف شده است.
در خط هشتم، یک ریسورس (منبع) را به برنامه معرفی کردهایم:
ریسورسها برای ذخیره سازی دادهها در سطح یک کنترل، سطح محلی در یک پنجره، یا سطح عمومی در کل پروژه به کار میروند. محدودیتی در ذخیره دادهها وجود ندارد و هر چقدر که دوست دارید میتوانید داده به آن پاس کنید. این دادهها میتوانند یک سری اطلاعات ذخیره شده در یک ساختار ساده تا یک ساختار سلسه مراتبی از کنترلها باشند. ریسورسها به شما این اجازه را میدهند تا دادهها را در یک مکان ذخیره کرده و آنها را در یک یا چندجا مورد استفاده قرار دهید.
از آن جا که مباحث ریسورسها را در یک مقالهی جداگانه بررسی میکنیم، فقط به ذکر نکات بالا جهت کد فعلی بسنده خواهیم کرد و ادامهی آن را در یک مقاله دیگر مورد بررسی قرار میدهیم.
هر ریسورس دارای یک نام یا یک کلید است که با خصوصیت x:key تعریف میشود.
ریسورس بالا یک کلاس را که در فضای نام دلخواهی قرار دارد، تعریف میکند و یک کلید هم به آن انتساب میدهد.
مرحلهی سوم معرفی ریسورس به عملیات Binding است:
{Binding نام پراپرتی کلاس, Converter={StaticResource کلید آیتم مربوطه در ریسورس}, ConverterParameter=پارامتری که به کلاس مبدل پاس میشود}
بخش اول دقیقا همان چیزی است که در قسمت قبلی
یاد گرفتیم. معرفی پراپرتی که باید عمل بایند به آن صورت گیرد. قسمت
بعدی معرفی مبدل است و از آن جا که تابع مبدل ما در یک منبع است، اینگونه
مینویسیم: {} را باز کرده و ابتدا کلمه StaticResource فاصله و سپس کلید
ریسورس که تابع را از ریسورس فراخوانی کند و قسمت بعدی هم پاس کردن یک
پارامتر به تابع مبدل است.
حال که با اصول نوشتار آشنا شدیم کار را آغاز میکنیم.
قصد داریم یک مبدل برای فیلد جنیست درست کنیم. از آنجا که این فیلد Boolean است و خصوصیت IsChecked یک RadioButton هم Boolean است، میتوان یک ارتباط مستقیم را ایجاد کرد. ولی مشکل در اینجا هست که True برای مذکر است و false برای مؤنث. در نتیجه تنها Radiobutton مربوطه به جنس مذکر به این حالت پاسخ میدهد و از آنجا که برای جنس مونث false در نظر گرفته شده است، انتخاب آن هم false خواهد بود. پس باید در مبدل، مقداری که کنترل میخواهد را شناسایی کرده و اگر مقدار با آن برابر بود، True را بازگردانیم. مقداری که هر کنترل درخواست میکند را از طریق پارامتر به تابع مبدل ارسال میکنیم. radiobutton مذکر، مقدار True را به عنوان پارامتر ارسال میکند و radiobutton مونث هم مقدار false را به عنوان پارامتر ارسال میکند. اگر تابع مبدل را ببینید، این مقدارها با پارامترها همخوانی دارند، True در غیر این صورت false بر میگرداند.
مرحله اول تعریف کلاس ValueConveter:
قصد داریم یک مبدل برای فیلد جنیست درست کنیم. از آنجا که این فیلد Boolean است و خصوصیت IsChecked یک RadioButton هم Boolean است، میتوان یک ارتباط مستقیم را ایجاد کرد. ولی مشکل در اینجا هست که True برای مذکر است و false برای مؤنث. در نتیجه تنها Radiobutton مربوطه به جنس مذکر به این حالت پاسخ میدهد و از آنجا که برای جنس مونث false در نظر گرفته شده است، انتخاب آن هم false خواهد بود. پس باید در مبدل، مقداری که کنترل میخواهد را شناسایی کرده و اگر مقدار با آن برابر بود، True را بازگردانیم. مقداری که هر کنترل درخواست میکند را از طریق پارامتر به تابع مبدل ارسال میکنیم. radiobutton مذکر، مقدار True را به عنوان پارامتر ارسال میکند و radiobutton مونث هم مقدار false را به عنوان پارامتر ارسال میکند. اگر تابع مبدل را ببینید، این مقدارها با پارامترها همخوانی دارند، True در غیر این صورت false بر میگرداند.
مرحله اول تعریف کلاس ValueConveter:
using System; using System.Globalization; using System.Windows; using System.Windows.Data; namespace test.ValueConverters { public class GenderConverter: IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { var ParameterString = parameter as string; if (ParameterString == null) return DependencyProperty.UnsetValue; bool bparam; bool test = bool.TryParse(parameter.ToString(), out bparam); if (test) { return ((bool)value).Equals(bparam); } return DependencyProperty.UnsetValue; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } } }
برای تعریف این مبدل در محیط XAML به صورت زیر اقدام میکنیم:
<Window x:Class="test.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:valueConverters="clr-namespace:test.ValueConverters" Title="MainWindow" Height="511.851" Width="525"> <Window.Resources> <valueConverters:GenderConverter x:Key="GenderConverter"/> </Window.Resources> </window>
نکته: در صورتی که بعد از تعریف ریسورس با خطای زیر روبرو شدید و محیط طراحی Design را از دست دادید یکبار پروژه را بیلد کنید تا مشکل حل شود.
The name "GenderConverter" does not exist in the namespace "clr-namespace:test.ValueConverters".
اکنون در عملیات بایندینگ دو کنترل اینگونه مینویسیم:
<RadioButton GroupName="Gender" IsChecked="{Binding Gender, Converter={StaticResource GenderConverter}, ConverterParameter=True}" Name="RdoMale" >Male</RadioButton> <RadioButton GroupName="Gender" IsChecked="{Binding Gender, Converter={StaticResource GenderConverter}, ConverterParameter=False}" Name="RdoFemale" Margin="0 5 0 0" >Female</RadioButton>
حال برنامه را اجرا کرده و نتیجه را ببینید. برای تست
بهتر میتوانید جنسیت فرد را در منبع داده تغییر دهید. همچنین از آنجا که
این مقدار برای جنس مذکر نیازی به بررسی و تبدیل ندارد میتوانید عملیات
بایند را عادی بنویسید:
<RadioButton GroupName="Gender" IsChecked="{Binding Gender}" Name="RdoMale" >Male</RadioButton>
هر کنترل مقداری از یک enum را میپذیرد که میتواند آن مقدار را با استفاده
از پارامتر، به تابع مبدل ارسال کند و سپس تابع چک میکند که آیا چنین
مقداری در enum مدنظر یافت میشود یا خیر؛ این کد الان کار میکندو واقعا
هم کد درستی است و هیچ مشکلی هم ندارد. ولی برای یک لحظه تصور کنید پنجره
شما شامل چهار خصوصیت enum دار است. یعنی الان باید 4 تابع مبدل بنویسید.
پس باید کد را بازنویسی کنیم تا به هر enum که مدنظر است پاسخ دهد؛ به این
ترتیب تنها یک تابع مبدل مینویسیم.
جهت یادآوری نگاهی به کلاس برنامه میاندازیم:
جهت یادآوری نگاهی به کلاس برنامه میاندازیم:
public enum FieldOfWork { Actor=0, Director=1, Producer=2 } public class Person : INotifyPropertyChanged { public bool Gender { get; set; } public string ImageName { get; set; } public string Country { get; set; } public DateTime Date { get; set; } public FieldOfWork FieldOfWork { get; set; } private string _name; public string Name { get { return _name; } set { _name = value; OnPropertyChanged(); } }
public FieldOfWork FieldOfWork { get; set; }
کد کلاس مبدل را به صورت زیر مینویسیم:
using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Data; namespace test.ValueConverters { public class EnumConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { var ParameterString = parameter as string; if (ParameterString == null) return DependencyProperty.UnsetValue; if (Enum.IsDefined(value.GetType(), value) == false) return DependencyProperty.UnsetValue; object paramvalue = Enum.Parse(value.GetType(), ParameterString); return paramvalue.Equals(value); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } } }
کد قسمت ریسورس را با کلاس جدید به روز میکنیم:
<Window.Resources> <valueConverters:GenderConverter x:Key="GenderConverter"/> <valueConverters:EnumConverter x:Key="EnumConverter"></valueConverters:EnumConverter> </Window.Resources>
<CheckBox Name="ChkActor" IsChecked="{Binding FieldOfWork, Converter={StaticResource EnumConverter}, ConverterParameter=Actor}" >Actor/Actress</CheckBox> <CheckBox Name="ChkDirector" IsChecked="{Binding FieldOfWork, Converter={StaticResource EnumConverter}, ConverterParameter=Director}" >Director</CheckBox> <CheckBox Name="ChkProducer" IsChecked="{Binding FieldOfWork, Converter={StaticResource EnumConverter}, ConverterParameter=Producer}" >Producer</CheckBox>
public static Person GetPerson() { return new Person() { Name = "Leo", Gender =true, ImageName ="man.jpg", Country = "Italy", FieldOfWork = test.FieldOfWork.Actor, Date = DateTime.Now.AddDays(-3) }; }
برنامه را اجرا کنید تا نتیجه کار را ببینید. باید چک باکس Actor تیک خورده باشد. میتوانید منبع داده را تغییر داده تا نتیجه کار را ببینید.
بگذارید فیلد FieldOfWork را به حالت قبلی یعنی IList برگردانیم. در بسیاری از اوقات ما چند گزینه از یک Enum را انتخاب میکنیم، مثل داشتن چند سطح دسترسی یا چند سمت کاری و ...
کلاس را به همراه کد GetPerson به شکل زیر تغییر میدهیم:
public enum FieldOfWork { Actor=0, Director=1, Producer=2 } public class Person : INotifyPropertyChanged { public bool Gender { get; set; } public string ImageName { get; set; } public string Country { get; set; } public DateTime Date { get; set; } public IList<FieldOfWork> FieldOfWork { get; set; } private string _name; public string Name { get { return _name; } set { _name = value; OnPropertyChanged(); } } public static Person GetPerson() { return new Person { Name = "Leo", Gender = true, ImageName = "man.jpg", Country = "Italy", FieldOfWork = new FieldOfWork[] { test.FieldOfWork.Actor, test.FieldOfWork.Producer }, Date = DateTime.Now.AddDays(-3) }; } }
دو Enum بازیگر و تهیه کننده را انتخاب کردهایم، پس در زمان اجرا باید این دو گزینه انتخاب شوند.
کد مبدل را به صورت زیر مینویسیم:
using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Data; namespace test.ValueConverters { public class EnumList : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { var ParameterString = parameter as string; if (ParameterString == null) return DependencyProperty.UnsetValue; var enumlist= (IList) value; if(enumlist==null && enumlist.Count<1) return DependencyProperty.UnsetValue; if (Enum.IsDefined(enumlist[0].GetType(), ParameterString) == false) return DependencyProperty.UnsetValue; /* foreach (var item in enumlist) { object paramvalue = Enum.Parse(item.GetType(), ParameterString); bool result = item.Equals(paramvalue); if (result) return true; } return false; */ return (from object item in enumlist let paramvalue = Enum.Parse(item.GetType(), ParameterString) select item.Equals(paramvalue)).Any(result => result); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } } }
در حلقهای که به شکل توضیح درآمده، همه آیتمهای مربوطه در لیست را بررسی کرده و اگر آیتمی برابر پارامتر باشد، True بر میگرداند و در صورتی که حلقه به اتمام برسد و آیتم پیدا نشود، مقدار False را برمیگرداند. این حلقه از آن جهت به شکل توضیح درآمده است که کد Linq آن در زیر نوشته شده است.
تعریف کلاس بالا در ریسورس:
<Window.Resources> <valueConverters:GenderConverter x:Key="GenderConverter"/> <valueConverters:EnumConverter x:Key="EnumConverter"></valueConverters:EnumConverter> <valueConverters:EnumList x:Key="EnumList"></valueConverters:EnumList> </Window.Resources>
<CheckBox Name="ChkActor" IsChecked="{Binding FieldOfWork, Converter={StaticResource EnumList}, ConverterParameter=Actor}" >Actor/Actress</CheckBox> <CheckBox Name="ChkDirector" IsChecked="{Binding FieldOfWork, Converter={StaticResource EnumList}, ConverterParameter=Director}" >Director</CheckBox> <CheckBox Name="ChkProducer" IsChecked="{Binding FieldOfWork, Converter={StaticResource EnumList}, ConverterParameter=Producer}" >Producer</CheckBox>
نتیجهی کار باید به شکل زیر باشد:
فیلد جنسیت و زمینه کاری از تابع مبدل به دست آمده است.
بدیهی است خروجیهای بالا برای کنترل هایی است که مقدار Boolean را میپذیرند و برای سایر کنترلها باید با کمی تغییر در کد و نوع برگشتی که تحویل خروجی متد مبدل میشود، دهید.دانلود فایلهای این قسمت
ASP.NET Core به همراه یک فریم ورک توکار ثبت وقایع (Logging) ارائه شدهی توسط تزریق وابستگیها است که به صورت پیش فرض نیز فعال است.
این تصویر را پیشتر در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 6 - سرویسها و تزریق وابستگیها» مشاهده کردهاید. در اینجا لیست سرویسهایی را مشاهده میکنید که به صورت پیش فرض، ثبت شدهاند و فعال هستند و ILogger و ILoggerFactory نیز جزئی از آنها هستند. بنابراین نیازی به فعال سازی آنها نیست؛ اما برای استفادهی از آنها نیاز به انجام یک سری تنظیمات است.
پیاده سازی ثبت وقایع در ASP.NET Core
اولین قدم کار با فریم ورک ثبت وقایع ASP.NET Core، معرفی ILoggerFactory به متد Configure کلاس آغازین برنامه است:
متد Configure امضای مشخصی را ندارد و در اینجا به هر تعداد سرویسی که نیاز باشد، میتوان اینترفیسهای آنها را جهت تزریق وابستگیهای متناظر توسط IoC Containser توکار ASP.NET Core، معرفی کرد. در اینجا برای تنظیم ویژگیهای سرویس ثبت وقایع، تزریق وابستگی ILoggerFactory صورت گرفتهاست.
سطر اول متد، تنظیمات ثبت وقایع را از خاصیت Logging فایل appsettings.json برنامه میخواند (در مورد خاصیت Configuration، در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایلهای config» بیشتر بحث شد) و لاگ کردن ویژهی در کنسول NET Core. را فعال میکند:
در مورد Log Level و یا سطوح ثبت وقایع، در ادامهی مطلب بحث خواهد شد.
و سطر دوم سبب نمایش اطلاعات لاگ شده در کنسول دیباگ ویژوال استودیو میشود.
متد AddDebug برای شناسایی، نیاز به افزودن وابستگیهای ذیل در فایل project.json برنامه را دارد:
پس از این تنظیمات، برنامه را اجرا کنید.
در اینجا میتوانید ریز وقایعی را که توسط ASP.NET Core لاگ شدهاست، مشاهده کنید. برای مثال چه درخواستی صورت گرفتهاست و چقدر اجرای آن زمانبردهاست.
این فعال سازی مرتبط است به متد AddDebug که اضافه شد. اگر میخواهید خروجی AddConsole را هم مشاهده کنید، از طریق خط فرمان، به پوشهی اصلی پروژه وارد شده و سپس دستور dotnet run را اجرا کنید:
دستور dotnet run سبب راه اندازی وب سرور برنامه بر روی پورت 5000 شدهاست که در تصویر نیز مشخص است.
بنابراین اینبار برای دسترسی به برنامه باید مسیر http://localhost:5000 را در مرورگر خود طی کنید. در اینجا نیز میتوان حالتهای مختلف اطلاعات لاگ شده را مشاهده کرد و تمام اینها مرتبط هستند به ذکر متد AddConsole .
کار با سرویس ثبت وقایع ASP.NET Core از طریق تزریق وابستگیها
برای کار با سرویس ثبت وقایع توکار ASP.NET Core در قسمتهای مختلف برنامه، میتوان از ترزیق وابستگی ILogger آن استفاده کرد:
در این کنترلر، وابستگی اینترفیس ILogger با پارامتری از نوع کنترلر جاری به سازندهی کلاس تزریق شدهاست. علت ذکر این پارامتر جنریک این است که ILoggerFactory بداند چگونه باید متد CreateLogger خود را در پشت صحنه وهله سازی کند.
سپس با توجه به اینکه این سرویس جزو سرویسهای از پیش ثبت شدهی ASP.NET Core است، امکانات آن بدون نیاز به تنظیمات بیشتری در دسترس است. برای مثال از متد LogInformation آن در اکشن متد Hello استفاده شدهاست و خروجی عبارت لاگ شدهی آنرا در اینجا میتوانید مشاهده کنید:
سطوح مختلف ثبت وقایع
اینترفیس ILogger به همراه متدهای مختلفی است؛ مانند LogError، LogDebug و غیره. معانی آنها به شرح زیر هستند:
Debug (1): ثبت واقعهای است با بیشترین حد جزئیات ممکن که عموما شامل اطلاعات حساسی نیز میباشد. بنابراین نباید در حالت ارائهی نهایی برنامه فعال شود.
(2) Verbose: ثبت وقایعی مفصل، جهت بررسی مشکلات در حین توسعهی برنامه. تنها باید حاوی اطلاعاتی برای دیباگ برنامه باشند.
(3) Information: عموما برای ردیابی قسمتهای مختلف برنامه مورد استفاده قرار میگیرند.
(4) Warning: جهت ثبت واقعهای نامطلوب در سیستم بکار میرود و سبب قطع اجرای برنامه نمیشود.
(5) Errors: مشکلات برنامه را که سبب قطع سرویس دهی آن شدهاند را ثبت میکند. هدف آن ثبت مشکلات واحد جاری است و نه کل برنامه.
Critical (6): هدف آن ثبت مشکلات بحرانی کل سیستم است که سبب از کار افتادن آن شدهاند.
برای مثال در حین تنظیم متد AddDebug که سبب نمایش اطلاعات لاگ شده در کنسول دیباگ ویژوال استودیو میشود، میتوان حداقل سطح ثبت وقایع را نیز ذکر کرد:
این حداقل مرتبط است با اعدادی که در کنار سطوح فوق ملاحظه میکنید. برای مثال اگر حداقل سطح ثبت وقایع به Information تنظیم شود، چون سطح آن 3 است، دیگر سطوح پایینتر از آن لاگ نخواهند شد. اهمیت این مساله در اینجا است که اگر صرفا نیاز به اطلاعات Critical داشتیم، نیازی نیست تا با انبوهی از اطلاعات لاگ شده سر و کار داشته باشیم و به این ترتیب میتوان حجم اطلاعات نمایش داده شده را کاهش داد.
البته ترتیب واقعی این سطوح را در enum مرتبط با آنها بهتر میتوان مشاهده کرد:
یک نکته: زمانیکه متد AddDebug را بدون پارامتر فراخوانی میکنید، حداقل سطح ثبت وقایع آن به Information تنظیم شدهاست. یعنی در این لاگ، خبری از اطلاعات Debug نخواهد بود (چون سطح دیباگ پایینتر است از Information). بنابراین اگر میخواهید این اطلاعات را هم مشاهده کنید باید پارامتر minLevel آنرا به LogLevel.Debug تنظیم نمائید.
امکان استفادهی از پروایدرهای ثبت وقایع ثالث
تا اینجا، دو نمونه از پروایدرهای توکار ثبت وقایع ASP.NET Core را بررسی کردیم. اگر نیاز به ثبت این اطلاعات با فرمتهای مختلف و یا در بانک اطلاعاتی وجود دارد، میتوان به تامین کنندههای ثالثی که قابلیت کار با ILoggerFactory را دارند نیز مراجعه کرد. برای مثال:
- elmah.io - provider for the elmah.io service
- Loggr - provider for the Loggr service
- NLog - provider for the NLog library
- Serilog - provider for the Serilog library
این تصویر را پیشتر در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 6 - سرویسها و تزریق وابستگیها» مشاهده کردهاید. در اینجا لیست سرویسهایی را مشاهده میکنید که به صورت پیش فرض، ثبت شدهاند و فعال هستند و ILogger و ILoggerFactory نیز جزئی از آنها هستند. بنابراین نیازی به فعال سازی آنها نیست؛ اما برای استفادهی از آنها نیاز به انجام یک سری تنظیمات است.
پیاده سازی ثبت وقایع در ASP.NET Core
اولین قدم کار با فریم ورک ثبت وقایع ASP.NET Core، معرفی ILoggerFactory به متد Configure کلاس آغازین برنامه است:
public void Configure(ILoggerFactory loggerFactory, IApplicationBuilder app, IHostingEnvironment env) { loggerFactory.AddConsole(Configuration.GetSection("Logging")); loggerFactory.AddDebug();
سطر اول متد، تنظیمات ثبت وقایع را از خاصیت Logging فایل appsettings.json برنامه میخواند (در مورد خاصیت Configuration، در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایلهای config» بیشتر بحث شد) و لاگ کردن ویژهی در کنسول NET Core. را فعال میکند:
{ "Logging": { "IncludeScopes": false, "LogLevel": { "Default": "Debug", "System": "Information", "Microsoft": "Information" } } }
و سطر دوم سبب نمایش اطلاعات لاگ شده در کنسول دیباگ ویژوال استودیو میشود.
متد AddDebug برای شناسایی، نیاز به افزودن وابستگیهای ذیل در فایل project.json برنامه را دارد:
{ "dependencies": { //same as before "Microsoft.Extensions.Logging": "1.0.0", "Microsoft.Extensions.Logging.Console": "1.0.0", "Microsoft.Extensions.Logging.Debug": "1.0.0" } }
در اینجا میتوانید ریز وقایعی را که توسط ASP.NET Core لاگ شدهاست، مشاهده کنید. برای مثال چه درخواستی صورت گرفتهاست و چقدر اجرای آن زمانبردهاست.
این فعال سازی مرتبط است به متد AddDebug که اضافه شد. اگر میخواهید خروجی AddConsole را هم مشاهده کنید، از طریق خط فرمان، به پوشهی اصلی پروژه وارد شده و سپس دستور dotnet run را اجرا کنید:
دستور dotnet run سبب راه اندازی وب سرور برنامه بر روی پورت 5000 شدهاست که در تصویر نیز مشخص است.
بنابراین اینبار برای دسترسی به برنامه باید مسیر http://localhost:5000 را در مرورگر خود طی کنید. در اینجا نیز میتوان حالتهای مختلف اطلاعات لاگ شده را مشاهده کرد و تمام اینها مرتبط هستند به ذکر متد AddConsole .
کار با سرویس ثبت وقایع ASP.NET Core از طریق تزریق وابستگیها
برای کار با سرویس ثبت وقایع توکار ASP.NET Core در قسمتهای مختلف برنامه، میتوان از ترزیق وابستگی ILogger آن استفاده کرد:
[Route("[controller]")] public class AboutController : Controller { private readonly ILogger<AboutController> _logger; public AboutController(ILogger<AboutController> logger) { _logger = logger; } [Route("")] public ActionResult Hello() { _logger.LogInformation("Running Hello"); return Content("Hello from DNT!"); }
سپس با توجه به اینکه این سرویس جزو سرویسهای از پیش ثبت شدهی ASP.NET Core است، امکانات آن بدون نیاز به تنظیمات بیشتری در دسترس است. برای مثال از متد LogInformation آن در اکشن متد Hello استفاده شدهاست و خروجی عبارت لاگ شدهی آنرا در اینجا میتوانید مشاهده کنید:
سطوح مختلف ثبت وقایع
اینترفیس ILogger به همراه متدهای مختلفی است؛ مانند LogError، LogDebug و غیره. معانی آنها به شرح زیر هستند:
Debug (1): ثبت واقعهای است با بیشترین حد جزئیات ممکن که عموما شامل اطلاعات حساسی نیز میباشد. بنابراین نباید در حالت ارائهی نهایی برنامه فعال شود.
(2) Verbose: ثبت وقایعی مفصل، جهت بررسی مشکلات در حین توسعهی برنامه. تنها باید حاوی اطلاعاتی برای دیباگ برنامه باشند.
(3) Information: عموما برای ردیابی قسمتهای مختلف برنامه مورد استفاده قرار میگیرند.
(4) Warning: جهت ثبت واقعهای نامطلوب در سیستم بکار میرود و سبب قطع اجرای برنامه نمیشود.
(5) Errors: مشکلات برنامه را که سبب قطع سرویس دهی آن شدهاند را ثبت میکند. هدف آن ثبت مشکلات واحد جاری است و نه کل برنامه.
Critical (6): هدف آن ثبت مشکلات بحرانی کل سیستم است که سبب از کار افتادن آن شدهاند.
برای مثال در حین تنظیم متد AddDebug که سبب نمایش اطلاعات لاگ شده در کنسول دیباگ ویژوال استودیو میشود، میتوان حداقل سطح ثبت وقایع را نیز ذکر کرد:
loggerFactory.AddDebug(minLevel: LogLevel.Information);
البته ترتیب واقعی این سطوح را در enum مرتبط با آنها بهتر میتوان مشاهده کرد:
public enum LogLevel { Trace, Debug, Information, Warning, Error, Critical, None, }
یک نکته: زمانیکه متد AddDebug را بدون پارامتر فراخوانی میکنید، حداقل سطح ثبت وقایع آن به Information تنظیم شدهاست. یعنی در این لاگ، خبری از اطلاعات Debug نخواهد بود (چون سطح دیباگ پایینتر است از Information). بنابراین اگر میخواهید این اطلاعات را هم مشاهده کنید باید پارامتر minLevel آنرا به LogLevel.Debug تنظیم نمائید.
امکان استفادهی از پروایدرهای ثبت وقایع ثالث
تا اینجا، دو نمونه از پروایدرهای توکار ثبت وقایع ASP.NET Core را بررسی کردیم. اگر نیاز به ثبت این اطلاعات با فرمتهای مختلف و یا در بانک اطلاعاتی وجود دارد، میتوان به تامین کنندههای ثالثی که قابلیت کار با ILoggerFactory را دارند نیز مراجعه کرد. برای مثال:
- elmah.io - provider for the elmah.io service
- Loggr - provider for the Loggr service
- NLog - provider for the NLog library
- Serilog - provider for the Serilog library
پاسخ به بازخوردهای پروژهها
عدم سازگاری با EF
مطابق شکل lazy loading فعال است در حین کار با روابط (order.OrderProductVariants) تعیین شده (این DynamicProxies که قابل مشاهده است).
- میتونید این lazy loading رو با تنظیم DbContext.Configuration.ProxyCreationEnabled = false غیرفعال کنید (کلا و سراسری).
- یا میتونید زمانیکه کوئری تهیه میکنید، از متد AsNoTracking استفاده کنید (()from p in context.Products.AsNoTracking).
+ همچنین زمانیکه کوئری تهیه میشود میتونید از متد Include برای eager loading بجای lazy loading استفاده کنید. (روش استاندارد و متداول برای تهیه گزارشات؛ از این جهت که eager loading، رفت و برگشت به بانک اطلاعاتی را کاهش میدهد و همچنین ذکر AsNoTracking مصرف حافظه برنامه را در حین تهیه گزارشات فقط خواندنی، کم میکند)
- میتونید این lazy loading رو با تنظیم DbContext.Configuration.ProxyCreationEnabled = false غیرفعال کنید (کلا و سراسری).
- یا میتونید زمانیکه کوئری تهیه میکنید، از متد AsNoTracking استفاده کنید (()from p in context.Products.AsNoTracking).
+ همچنین زمانیکه کوئری تهیه میشود میتونید از متد Include برای eager loading بجای lazy loading استفاده کنید. (روش استاندارد و متداول برای تهیه گزارشات؛ از این جهت که eager loading، رفت و برگشت به بانک اطلاعاتی را کاهش میدهد و همچنین ذکر AsNoTracking مصرف حافظه برنامه را در حین تهیه گزارشات فقط خواندنی، کم میکند)
context.Customers.Include(c => c.Orders).AsNoTracking()
با سلام و تشکر. این برنامه پیغام میدهد List وجود ندارد.
لطفا راهنمایی بفرمایید من چطور میتوانم نام شیتها را بدست بیاورم؟
SELECT * FROM [List$]
نظرات مطالب
بازگردانی پایگاه داده بدون فایل لاگ
این اسکریپت رو پیدا کردم، منتهی اجراش که میکنم زمان باقی مونده رو اعلام نمیکنه.
در ضمن دیتابیس روی درایو D هست و فضای چند ده گیگ خالی داره. حجم فایل ldf هم تا حدود 400 مگ هست. وقتی هم یک کوئری بخوام روی دیتابیس اجرا کنم خطای زیر رو میده:
DECLARE @DBName VARCHAR(64) = 'databasename' DECLARE @ErrorLog AS TABLE([LogDate] CHAR(24), [ProcessInfo] VARCHAR(64), [TEXT] VARCHAR(MAX)) INSERT INTO @ErrorLog EXEC sys.xp_readerrorlog 0, 1, 'Recovery of database', @DBName SELECT TOP 5 [LogDate] ,SUBSTRING([TEXT], CHARINDEX(') is ', [TEXT]) + 4,CHARINDEX(' complete (', [TEXT]) - CHARINDEX(') is ', [TEXT]) - 4) AS PercentComplete ,CAST(SUBSTRING([TEXT], CHARINDEX('approximately', [TEXT]) + 13,CHARINDEX(' seconds remain', [TEXT]) - CHARINDEX('approximately', [TEXT]) - 13) AS FLOAT)/60.0 AS MinutesRemaining ,CAST(SUBSTRING([TEXT], CHARINDEX('approximately', [TEXT]) + 13,CHARINDEX(' seconds remain', [TEXT]) - CHARINDEX('approximately', [TEXT]) - 13) AS FLOAT)/60.0/60.0 AS HoursRemaining ,[TEXT] FROM @ErrorLog ORDER BY [LogDate] DESC
در ضمن دیتابیس روی درایو D هست و فضای چند ده گیگ خالی داره. حجم فایل ldf هم تا حدود 400 مگ هست. وقتی هم یک کوئری بخوام روی دیتابیس اجرا کنم خطای زیر رو میده:
Database 'Manager' is being recovered. Waiting until recovery is finished.
React برخلاف Angular، دارای قابلیتهای توکار مسیریابی نیست و تنها کاری را که انجام میدهد همان رندر View است که تا اینجا بررسی کردیم. به همین جهت در این قسمت ابتدا یک برنامهی عمومی و ساده را با نصب کتابخانهی ثالثی برای توضیح مفاهیم مسیریابی، شامل کار با پارامترهای مسیریابی، کوئری استرینگها، هدایت کاربران به صفحات دیگر، مدیریت صفحات یافت نشده و مسیریابی تو در تو، بررسی میکنیم. سپس به عنوان تمرین، همان برنامهی طراحی گریدی را که تا قسمت 14 تکمیل کردیم، با معرفی مسیریابی بهبود خواهیم بخشید.
برپایی پیشنیازها
در اینجا برای بررسی مسیریابی، یک پروژهی جدید React را ایجاد میکنیم.
در ادامه توئیتر بوت استرپ 4 را نیز نصب میکنیم. برای این منظور پس از باز کردن پوشهی اصلی برنامه توسط VSCode، دکمههای ctrl+` را فشرده (ctrl+back-tick) و دستور زیر را در ترمینال ظاهر شده وارد کنید:
سپس برای افزودن فایل bootstrap.css به پروژهی React خود، ابتدای فایل index.js را به نحو زیر ویرایش خواهیم کرد:
این import به صورت خودکار توسط webpack ای که در پشت صحنه کار bundling & minification برنامه را انجام میدهد، مورد استفاده قرار میگیرد.
همچنین کتابخانهی ثالث بسیار معروف react-router-dom را نیز نصب میکنیم:
نگارش dom آن مخصوص کار با مرورگر است و نگارش native آن (react-router-native)، مخصوص React Native و تولید برنامههای موبایل میباشد که مبحث دیگری است.
افزودن مسیریابی به برنامه
پس از نصب کتابخانهی react-router-dom، برای افزودن آن به برنامه و فعالسازی مسیریابی، به فایل index.js مراجعه کرده و import آنرا به ابتدای فایل اضافه میکنیم:
سپس کامپوننت App را داخل BrowserRouter، محصور میکنیم:
کار BrowserRouter، محصور سازی مدیریت تاریخچهی مرور صفحات در مرورگر و انتقال آن به درخت کامپوننتهای React است. به این ترتیب در هر قسمتی از درخت کامپوننتهای برنامه میتوان از History object مرورگر استفاده کرد.
ثبت و معرفی مسیریابیها
در ادامه باید مسیریابیهای خود را ثبت کنیم؛ به این معنا که بر اساس URL خاصی، چه کامپوننتی باید رندر شود. به همین جهت پوشهی جدید src\components را ایجاد کرده و کامپوننت src\components\navbar.jsx را که یک کامپوننت تابعی بدون حالت است، در آن تعریف میکنیم:
کار آن نمایش منوی بالای صفحه است.
سپس به فایل app.js مراجعه کرده و ساختار آنرا به صورت زیر، جهت درج این NavBar، ویرایش میکنیم تا سبب رندر و نمایش منوی راهبری در مرورگر شود:
در ادامه در کامپوننت App، یک container را اضافه میکنیم که قرار است در آن بر اساس URL رسیده، محتوای کامپوننت خاصی رندر شود. به همین جهت کامپوننت Route را در اینجا قرار میدهیم و در آن یک یا چند Route را ثبت میکنیم:
Route نیز یک کامپوننت است؛ همانند تمام کامپوننتهایی که تاکنون تعریف کردیم و دارای چند ویژگی است که به صورت props به آن منتقل میشوند. برای نمونه خاصیت path آن به مسیر products/ در مرورگر اشاره میکند و سبب رندر کامپوننت جدید Products که در بالای این ماژول نیز import شده، میشود. در اینجا سه مسیریابی دیگر را نیز ثبت کردهایم که کامپوننتهای جدید متناظر با آنها به صورت زیر تعریف میشوند:
کامپوننت جدید src\components\products.jsx جهت رندر لیست آرایهی اشیاء product:
کامپوننت بدون حالت تابعی src\components\home.jsx با این محتوا:
کامپوننت بدون حالت تابعی src\components\posts.jsx با این محتوا:
کامپوننت بدون حالت تابعی src\components\admin\dashboard.jsx در پوشهی جدید admin با این محتوا:
تا اینجا اگر برنامه را اجرا کنیم، در اولین بار نمایش آن، شاهد رندر کامپوننت Home خواهیم بود. اما اگر در همین حالت بر روی لیست products، در منوی بالای صفحه کلیک کنیم، هم کامپوننت products و هم کامپونت home، هر دو با هم رندر شدهاند. یک چنین رفتاری را در سایر صفحات نیز میتوان مشاهده کرد:
معرفی کامپوننت Switch
الگوریتم تطابق کامپوننت Route، ابتدا بررسی میکند که آیا برای مثال URL ای با path مساوی products/ شروع شدهاست؟ اگر اینطور است، کامپوننت متناظر با آن را که برای نمونه در اینجا Products است، رندر میکند. این حالت جهت مسیری مانند products/new/ نیز صدق میکند؛ چون این URL نیز با products/ شروع شدهاست. همچنین این تطابقگر، مسیر ثبت شدهی برای کامپوننت Home را نیز چون با / شروع شدهاست و جزء ابتدایی مسیر products/ است هم رندر میکند. به همین جهت است که وقتی مسیر products/ را درخواست میدهیم، در صفحه دو کامپوننت products و home، با هم رندر میشوند.
یک روش حل این مشکل، استفاده از ویژگی exact است:
به این ترتیب اگر مسیر درخواستی دقیقا مساوی / بود، کامپوننت Home را رندر خواهد کرد. با این تغییر، با مراجعهی به آدرس products/، دیگر رندر کامپوننت home را شاهد نخواهیم بود:
راه دوم رفع این مشکل، استفاده از کامپوننت Switch است. به همین جهت ابتدا این کامپوننت را import میکنیم:
سپس تمام Routeهای تعریف شده را داخل Switch محصور خواهیم کرد:
Switch اولین مسیریابی را که با URL داده شده تطابق داشته باشد، رندر میکند. همچنین در اینجا دیگر نیازی به ذکر ویژگی exact نیز وجود ندارد. بنابراین با استفاده از Switch اگر مسیر داده شده، products/ باشد، مسیریابی تعریف شدهی با آن یافت میشود که در اینجا اولین Route تعریف شدهاست. سپس کار رندر کامپوننت آنرا انجام داده و از مابقی مسیریابیهای تعریف شده، صرفنظر میکند.
بنابراین هنگام کار با Switch، ترتیب مسیریابیهای تعریف شده مهم است و باید از یک مسیریابی ویژه شروع شده و به یک مسیریابی عمومی مانند / ختم شود.
معرفی کامپوننت Link
تا اینجا اگر برنامه را اجرا کرده باشید و پیشتر سابقهی کار با برنامههای SPA یا Single page applications را داشته باشید، یک مشکل دیگر را نیز احساس کردهاید: سیستم مسیریابی که تا کنون تعریف کردهایم، به صورت SPA عمل نمیکند. یعنی به ازای هربار کلیک بر روی لینکهای منوی راهبری سایت، یکبار دیگر به طور کامل برنامه از صفر بارگذاری مجدد میشود و تمام اسکریپتهای آن مجددا از سرور دریافت شده و رندر خواهند شد. این مورد را در برگهی network ابزارهای توسعه دهندگان مرورگر خود بهتر میتوانید مشاهده کنید. به ازای هر درخواست نمایش کامپوننتی، تعدادی درخواست HTTP به سمت سرور ارسال میشوند که برای دریافت صفحهی index و bundle.js برنامه هستند. اما در برنامههای SPA، مانند جمیل، با هربار کلیک بر روی لینکی، شاهد ریفرش و بارگذاری مجدد کل آن صفحه نیستیم و تنها اطلاعات موجود در قسمت container به روز میشوند.
یک نکته: در اینجا ممکن است دو درخواست websocket و info را نیز مشاهده کنید. این دو مرتبط به hot module reloading هستند که با ذخیرهی برنامه در ادیتور VSCode، بلافاصله سبب به روز رسانی و ریفرش برنامه در مرورگر میشوند.
برای رفع مشکل SPA نبودن برنامه، باید به کامپوننت NavBar مراجعه کرده و تمام anchorهای استاندارد تعریف شدهی در آنرا با کامپوننت Link جایگزین کنیم:
در اینجا ابتدا کامپوننت Link را در ابتدای ماژول، import کردیم. سپس تمام anchorها را یافته و تبدیل به کامپوننت Link نمودیم. همچنین href آنها را نیز به ویژگی to تغییر دادیم.
با این تغییرات اگر برنامه را اجرا کنیم، اینبار با کلیک بر روی هر لینک، دیگر شاهد بارگذاری کامل صفحه در مرورگر نخواهیم بود؛ بلکه تنها قسمت container ای که کامپوننت Route مسیریابی در آن درج شدهاست، به روز رسانی میشود و این عملیات نیز بسیار سریع است؛ از این جهت که محتوای این کامپوننتها از همان bundle.js حاوی تمام کدهای برنامه تامین میشود و این فایل تنها یکبار در آغاز برنامه از سرور خوانده شده و سپس توسط مرورگر پردازش میشود. بنابراین در برنامههای SPA، برخلاف برنامههای وب معمولی، هربار که کاربر آدرس متفاوتی را انتخاب میکند، بارگذاری مجدد برنامه و خوانده شدن محتوای متناظر از سرور صورت نمیگیرد؛ این محتوا هم اکنون در bundle.js برنامه مهیا است و قابلیت استفادهی آنی را دارد.
اما کامپوننت Link چگونه کار میکند؟
کامپوننت لینک در نهایت همان anchorهای استاندارد را رندر میکند؛ اما به هر کدام یک onClick را نیز اضافه میکند که سبب جلوگیری از رفتار پیشفرض anchor میشود. به همین جهت مرورگر درخواست اضافهای را به سمت سرور ارسال نمیکند. در اینجا مدیریت کنندهی onClick، تنها Url بالای صفحه را در مرورگر تغییر میدهد. اکنون که Url تغییر کردهاست، یکی از مسیریابیهای تعریف شده، با این Url تطابق یافته و سپس کامپوننت متناظر با آنرا رندر میکند.
بررسی Route props
اگر بر روی لینک نمایش products در منوی راهبری سایت کلیک کرده و سپس به خروجی افزونهی react developer tools دقت کنیم (تصویر فوق)، مشاهده میکنیم که این کامپوننت هم اکنون تعدادی خاصیت را به صورت props در اختیار دارد؛ مانند history (امکان هدایت کاربر را به صفحهای دیگر دارد)، location (آدرس جاری برنامه) و match (اطلاعاتی در مورد الگوریتم تطابق مسیر). کار تنظیم این props، توسط کامپوننت Route ای که کار ثبت مسیریابیها را انجام میدهد، صورت میگیرد. به عبارتی کامپوننت Route، محصور کنندهی کامپوننتی است که آنرا به عنوان پارامتر، دریافت و در صورت تطابق با مسیر جاری، آنرا رندر میکند. همچنین در این بین کار تزریق خواص props یاد شده را نیز انجام میدهد.
ارسال props سفارشی در حین مسیریابی به کامپوننتها
همانطور که بررسی کردیم، کامپوننت Route، حداقل سه خاصیت props را به کامپوننتهایی که رندر میکند، تزریق خواهد کرد. اما در اینجا برای تزریق خواص سفارشی چگونه باید عمل کرد؟
در حین کار با کامپوننت Route، برای ارسال props اضافی، بجای استفاده از ویژگی component آن، باید از ویژگی render استفاده کرد:
در اینجا کار با تعریف یک arrow function شروع میشود که در نهایت المان کامپوننت مدنظر را همانند روش متداولی که برای تعریف تمام کامپوننتهای React و تنظیم ویژگیهای آنها استفاده میشود، بازگشت میدهد که تاثیر آنرا در خروجی افزونهی react developer tools بهتر میتوان مشاهده کرد:
البته اگر به تصویر فوق دقت کنید، سایر خواص پیشینی که تزریق شده بودند مانند history، location و match، دیگر در اینجا حضور ندارند. برای رفع این مشکل باید تعریف arrow function انجام شده را به صورت زیر تغییر داد:
ابتدا پارامتر arrow function را به همان props تنظیم میکنیم. سپس با استفاده از spread operator، این props را در المان JSX تعریف شده، گسترده و تزریق میکنیم؛ با این خروجی:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-15-part-01.zip
برپایی پیشنیازها
در اینجا برای بررسی مسیریابی، یک پروژهی جدید React را ایجاد میکنیم.
> create-react-app sample-15 > cd sample-15 > npm start
> npm install --save bootstrap
import "bootstrap/dist/css/bootstrap.css";
همچنین کتابخانهی ثالث بسیار معروف react-router-dom را نیز نصب میکنیم:
> npm i react-router-dom --save
افزودن مسیریابی به برنامه
پس از نصب کتابخانهی react-router-dom، برای افزودن آن به برنامه و فعالسازی مسیریابی، به فایل index.js مراجعه کرده و import آنرا به ابتدای فایل اضافه میکنیم:
import { BrowserRouter } from "react-router-dom";
ReactDOM.render( <BrowserRouter> <App /> </BrowserRouter>, document.getElementById("root") );
ثبت و معرفی مسیریابیها
در ادامه باید مسیریابیهای خود را ثبت کنیم؛ به این معنا که بر اساس URL خاصی، چه کامپوننتی باید رندر شود. به همین جهت پوشهی جدید src\components را ایجاد کرده و کامپوننت src\components\navbar.jsx را که یک کامپوننت تابعی بدون حالت است، در آن تعریف میکنیم:
import React from "react"; const NavBar = () => { return ( <nav className="navbar bg-dark navbar-dark navbar-expand-sm"> <div className="navbar-nav"> <a className="nav-item nav-link" href="/"> Home </a> <a className="nav-item nav-link" href="/products"> Products </a> <a className="nav-item nav-link" href="/posts/2018/06"> Posts </a> <a className="nav-item nav-link" href="/admin"> Admin </a> </div> </nav> ); }; export default NavBar;
سپس به فایل app.js مراجعه کرده و ساختار آنرا به صورت زیر، جهت درج این NavBar، ویرایش میکنیم تا سبب رندر و نمایش منوی راهبری در مرورگر شود:
import "./App.css"; import React, { Component } from "react"; import NavBar from "./components/navbar"; class App extends Component { render() { return ( <div> <NavBar /> </div> ); } } export default App;
import "./App.css"; import React, { Component } from "react"; import { Route } from "react-router-dom"; import Dashboard from "./components/admin/dashboard"; import Home from "./components/home"; import NavBar from "./components/navbar"; import Posts from "./components/posts"; import Products from "./components/products"; class App extends Component { render() { return ( <div> <NavBar /> <div className="container"> <Route path="/products" component={Products} /> <Route path="/posts" component={Posts} /> <Route path="/admin" component={Dashboard} /> <Route path="/" component={Home} /> </div> </div> ); } } export default App;
کامپوننت جدید src\components\products.jsx جهت رندر لیست آرایهی اشیاء product:
import React, { Component } from "react"; class Products extends Component { state = { products: [ { id: 1, name: "Product 1" }, { id: 2, name: "Product 2" }, { id: 3, name: "Product 3" } ] }; render() { return ( <div> <h1>Products</h1> <ul> {this.state.products.map(product => ( <li key={product.id}> <a href={`/products/${product.id}`}>{product.name}</a> </li> ))} </ul> </div> ); } } export default Products;
کامپوننت بدون حالت تابعی src\components\home.jsx با این محتوا:
import React from "react"; const Home = () => { return <h1>Home</h1>; }; export default Home;
کامپوننت بدون حالت تابعی src\components\posts.jsx با این محتوا:
import React from "react"; const Posts = () => { return ( <div> <h1>Posts</h1> Year: , Month: </div> ); }; export default Posts;
کامپوننت بدون حالت تابعی src\components\admin\dashboard.jsx در پوشهی جدید admin با این محتوا:
import React from "react"; const Dashboard = ({ match }) => { return ( <div> <h1>Admin Dashboard</h1> </div> ); }; export default Dashboard;
معرفی کامپوننت Switch
<div className="container"> <Route path="/products" component={Products} /> <Route path="/posts" component={Posts} /> <Route path="/admin" component={Dashboard} /> <Route path="/" component={Home} /> </div>
یک روش حل این مشکل، استفاده از ویژگی exact است:
<Route path="/" exact component={Home} />
راه دوم رفع این مشکل، استفاده از کامپوننت Switch است. به همین جهت ابتدا این کامپوننت را import میکنیم:
import { Route, Switch } from "react-router-dom";
class App extends Component { render() { return ( <div> <NavBar /> <div className="container"> <Switch> <Route path="/products" component={Products} /> <Route path="/posts" component={Posts} /> <Route path="/admin" component={Dashboard} /> <Route path="/" component={Home} /> </Switch> </div> </div> ); } }
بنابراین هنگام کار با Switch، ترتیب مسیریابیهای تعریف شده مهم است و باید از یک مسیریابی ویژه شروع شده و به یک مسیریابی عمومی مانند / ختم شود.
معرفی کامپوننت Link
تا اینجا اگر برنامه را اجرا کرده باشید و پیشتر سابقهی کار با برنامههای SPA یا Single page applications را داشته باشید، یک مشکل دیگر را نیز احساس کردهاید: سیستم مسیریابی که تا کنون تعریف کردهایم، به صورت SPA عمل نمیکند. یعنی به ازای هربار کلیک بر روی لینکهای منوی راهبری سایت، یکبار دیگر به طور کامل برنامه از صفر بارگذاری مجدد میشود و تمام اسکریپتهای آن مجددا از سرور دریافت شده و رندر خواهند شد. این مورد را در برگهی network ابزارهای توسعه دهندگان مرورگر خود بهتر میتوانید مشاهده کنید. به ازای هر درخواست نمایش کامپوننتی، تعدادی درخواست HTTP به سمت سرور ارسال میشوند که برای دریافت صفحهی index و bundle.js برنامه هستند. اما در برنامههای SPA، مانند جمیل، با هربار کلیک بر روی لینکی، شاهد ریفرش و بارگذاری مجدد کل آن صفحه نیستیم و تنها اطلاعات موجود در قسمت container به روز میشوند.
یک نکته: در اینجا ممکن است دو درخواست websocket و info را نیز مشاهده کنید. این دو مرتبط به hot module reloading هستند که با ذخیرهی برنامه در ادیتور VSCode، بلافاصله سبب به روز رسانی و ریفرش برنامه در مرورگر میشوند.
برای رفع مشکل SPA نبودن برنامه، باید به کامپوننت NavBar مراجعه کرده و تمام anchorهای استاندارد تعریف شدهی در آنرا با کامپوننت Link جایگزین کنیم:
import React from "react"; import { Link } from "react-router-dom"; const NavBar = () => { return ( <nav className="navbar bg-dark navbar-dark navbar-expand-sm"> <div className="navbar-nav"> <Link className="nav-item nav-link" to="/"> Home </Link> <Link className="nav-item nav-link" to="/products"> Products </Link> <Link className="nav-item nav-link" to="/posts/2018/06"> Posts </Link> <Link className="nav-item nav-link" to="/admin"> Admin </Link> </div> </nav> ); }; export default NavBar;
با این تغییرات اگر برنامه را اجرا کنیم، اینبار با کلیک بر روی هر لینک، دیگر شاهد بارگذاری کامل صفحه در مرورگر نخواهیم بود؛ بلکه تنها قسمت container ای که کامپوننت Route مسیریابی در آن درج شدهاست، به روز رسانی میشود و این عملیات نیز بسیار سریع است؛ از این جهت که محتوای این کامپوننتها از همان bundle.js حاوی تمام کدهای برنامه تامین میشود و این فایل تنها یکبار در آغاز برنامه از سرور خوانده شده و سپس توسط مرورگر پردازش میشود. بنابراین در برنامههای SPA، برخلاف برنامههای وب معمولی، هربار که کاربر آدرس متفاوتی را انتخاب میکند، بارگذاری مجدد برنامه و خوانده شدن محتوای متناظر از سرور صورت نمیگیرد؛ این محتوا هم اکنون در bundle.js برنامه مهیا است و قابلیت استفادهی آنی را دارد.
اما کامپوننت Link چگونه کار میکند؟
کامپوننت لینک در نهایت همان anchorهای استاندارد را رندر میکند؛ اما به هر کدام یک onClick را نیز اضافه میکند که سبب جلوگیری از رفتار پیشفرض anchor میشود. به همین جهت مرورگر درخواست اضافهای را به سمت سرور ارسال نمیکند. در اینجا مدیریت کنندهی onClick، تنها Url بالای صفحه را در مرورگر تغییر میدهد. اکنون که Url تغییر کردهاست، یکی از مسیریابیهای تعریف شده، با این Url تطابق یافته و سپس کامپوننت متناظر با آنرا رندر میکند.
بررسی Route props
اگر بر روی لینک نمایش products در منوی راهبری سایت کلیک کرده و سپس به خروجی افزونهی react developer tools دقت کنیم (تصویر فوق)، مشاهده میکنیم که این کامپوننت هم اکنون تعدادی خاصیت را به صورت props در اختیار دارد؛ مانند history (امکان هدایت کاربر را به صفحهای دیگر دارد)، location (آدرس جاری برنامه) و match (اطلاعاتی در مورد الگوریتم تطابق مسیر). کار تنظیم این props، توسط کامپوننت Route ای که کار ثبت مسیریابیها را انجام میدهد، صورت میگیرد. به عبارتی کامپوننت Route، محصور کنندهی کامپوننتی است که آنرا به عنوان پارامتر، دریافت و در صورت تطابق با مسیر جاری، آنرا رندر میکند. همچنین در این بین کار تزریق خواص props یاد شده را نیز انجام میدهد.
ارسال props سفارشی در حین مسیریابی به کامپوننتها
همانطور که بررسی کردیم، کامپوننت Route، حداقل سه خاصیت props را به کامپوننتهایی که رندر میکند، تزریق خواهد کرد. اما در اینجا برای تزریق خواص سفارشی چگونه باید عمل کرد؟
در حین کار با کامپوننت Route، برای ارسال props اضافی، بجای استفاده از ویژگی component آن، باید از ویژگی render استفاده کرد:
<Route path="/products" render={() => <Products param1="123" param2="456" />} />
البته اگر به تصویر فوق دقت کنید، سایر خواص پیشینی که تزریق شده بودند مانند history، location و match، دیگر در اینجا حضور ندارند. برای رفع این مشکل باید تعریف arrow function انجام شده را به صورت زیر تغییر داد:
<Route path="/products" render={props => ( <Products param1="123" param2="456" {...props} /> )} />
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-15-part-01.zip
بازخوردهای دوره
صفحات مودال در بوت استرپ 3
در حالت لوکال پارشال ویوو به فرم مدال بدرستی تزریق میشه ولی در حالت پابلیش شده این تزریق پارشال ویوو انجام نمیشه ، و محتوای مودال خالیه.چرایی این حالت رو نمیدونم
همانطور که میدانید GridView جزء جداناپذیر از اکثر پروژههای برنامه نویسان ASP.NET Web forms میباشد. اکثرا روشی که در میان برنامه نویسان بیشتر استفاده میشود، قرار دادن یک دکمه/لینک در هر ردیف از GridView برای حذف رکورد مورد نظر میباشد. در این مقاله قصد دارم روشی را ارائه کنم تا کاربر قادر باشد هر تعداد رکورد را که مدنظر دارد، انتخاب کرده و با فشردن دکمه "حذف" رکوردهای انتخاب شده را حذف کند.
1- یک GridView به صفحه افزوده و خاصیت AutoGenerateColumns آن را برابر False قرار دهید .
3- برای قرار دادن کنترلهای Asp.net که در اینجا منظور CheckBox میباشد میبایست از TemplateField و قرار دادن تگ ItemTemplate درون آن، به صورت زیر استفاده نمایید :
و بعد از تگ GridView دکمهای را برای حذف موارد انتخابی در فرم قرار دهید :
برای نمایش یک پیغام به کاربر به منظور Confirm کردن دستور حذف در سمت کلاینت، قطعه کد Javascript زیر را قرار میدهیم:
و در رویداد Page_Load کدهای زیر را جهت نمایش مقادیر در GridView و افزودن تابع فوق به دکمه، قبل از حذف رکوردها میافزاییم :
ابتدا تابع DeleteRecode را به صورت زیر پیاده سازی میکنیم :
و اما بخش مهم مربوط به رویداد دکمه میباشد. در هنگام کلیک بر روی دکمه باید تمامی رکوردهای GridView را چک و تمامی رکوردهایی را که CheckBox آنها تیک خورده است گرفته و ID رکورد مورد نظر را به تابع DeleteRecode فرستاد و در پایان برای اعمال تغییرات، متد ShowDate را فراخوانی و GridView را مجددا Bind میکنیم.
برای درک بهتر، ابتدا جدولی به اسم "Emploee" را در SQL Server با مشخصات زیر ساخته :
CREATE TABLE [dbo].[Employee] ( [EmpId] INT NOT NULL, [FirstName] VARCHAR (20) NOT NULL, [LastName] VARCHAR (20) NOT NULL, [City] VARCHAR (20) NOT NULL, PRIMARY KEY CLUSTERED ([EmpId] ASC) );
2- فیلدهایی را که قصد نمایش آنها در GridView را دارید به صورت زیر به GridView بیفزایید :
<asp:BoundField DataField="FirstName" HeaderText="First Name" />
<asp:TemplateField> <ItemTemplate> <asp:CheckBox ID="chkDel" runat="server" /> </ItemTemplate> </asp:TemplateField>
<asp:Button ID="btnDeleteRecord" runat="server" OnClick="btnDeleteRecord_Click" Text="Delete" />
function DeleteConfirm() { var Ans = confirm("Do you want to Delete Selected Employee Record?"); if (Ans) { return true; } else { return false; } }
protected void Page_Load(object sender, EventArgs e) { if(!IsPostBack) { //Displaying the Data showData(); //Adding an Attribute to Server Control(i.e. btnDeleteRecord) btnDeleteRecord.Attributes.Add("onclick", "javascript:return DeleteConfirm()"); } }
//Method for Displaying Data protected void showData() { DataTable dt = new DataTable(); SqlConnection con = new SqlConnection(cs); SqlDataAdapter adapt = new SqlDataAdapter("select * from Employee",con); con.Open(); adapt.Fill(dt); con.Close(); GridView1.DataSource = dt; GridView1.DataBind(); }
که یک پارمتر را از ورودی دریافت میکند که ID رکورد انتخاب شده میباشد و با استفاده از ID، رکورد مورد نظر را حذف میکنیم :
protected void DeleteRecord(int empid) { SqlConnection con = new SqlConnection(cs); SqlCommand com = new SqlCommand("delete from Employee where EmpId=@ID",con); com.Parameters.AddWithValue("@ID",empid); con.Open(); com.ExecuteNonQuery(); con.Close(); }
protected void btnDeleteRecord_Click(object sender, EventArgs e) { foreach (GridViewRow grow in GridView1.Rows) { //Searching CheckBox("chkDel") in an individual row of Grid CheckBox chkdel = (CheckBox)grow.FindControl("chkDel"); //If CheckBox is checked than delete the record with particular empid if(chkdel.Checked) { int empid = Convert.ToInt32(grow.Cells[1].Text); DeleteRecord(empid); } } //Displaying the Data in GridView showData(); }
Second Level Cache In NHibernate 4
همان طور که میدانیم کش در NHibernate در دو سطح قابل انجام میباشد:
- کش سطح اول که همان اطلاعات سشن، در تراکنش جاری هست و با اتمام تراکنش، محتویات آن خالی میگردد. این سطح همیشه فعال میباشد و در این بخش قصد پرداختن به آن را نداریم.
- کش سطح دوم که بین همهی تراکنشها مشترک و پایدار میباشد. این مورد به طور پیش فرض فعال نمیباشد و میبایستی از طریق کانفیگ برنامه فعال گردد.
جهت پیاده سازی باید قسمتهای ذیل را در کانفیگ مربوط به NHibernate اضافه نمود:
پیاده سازی Caching در NHibernate در سه مرحله قابل اعمال میباشد :
- کش در سطح Load موجودیتهای مستقل
- کش در سطح Load موجودیتهای وابسته Bag , List , Set , …
- کش در سطح Query ها
Providerهای مختلفی برای اعمال و پیاده سازی آن وجود دارند که معروفترین آنها SysCache بوده و ما هم از همان استفاده مینماییم.
- مدت زمان پیش فرض کش سطح دوم، ۵ دقیقه میباشد و در صورت نیاز به تغییر آن، باید تگ مربوط به SysCache را تنظیم نمود. محدودیتی در تعریف تعداد متفاوتی از زمانهای خالی شدن کش وجود ندارد و مدت زمان آن بر حسب ثانیه مشخص میگردد. نحوهی تخصیص زمان انقضای کش به هر مورد بدین شکل صورت میگیرد که region مربوطه در آن معرفی میگردد.
جهت اعمال کش در سطح Load موجودیتهای مستقل، علاوه بر کانفیگ اصلی، میبایستی کدهای زیر را به Mapping موجودیت اضافه نمود مانند :
این مورد برای موجودیتهای وابسته هم نیز صادق است؛ به شکل کد زیر:
ویژگی usage نیز با مقادیر زیر قابل تنظیم است:
- read-only : این مورد جهت موجودیتهایی مناسب است که امکان بروزرسانی آنها توسط کاربر وجود ندارد. این مورد بهترین کارآیی را دارد.
- read-write : این مورد جهت موجودیتهایی بکار میرود که امکان بروزرسانی آنها توسط کاربر وجود دارد. این مورد کارآیی پایینتری دارد.
- nonstrict-read-write : این مورد جهت موجودیتهایی مناسب میباشد که امکان بروزرسانی آنها توسط کاربر وجود دارد؛ اما امکان همزمان بروز کردن آنها توسط چندین کاربر وجود نداشته باشد. این مورد در قیاس، کارآیی بهتر و بهینهتری نسبت به مورد قبل دارد.
جهت اعمال کش در کوئریها نیز باید مراحل خاص خودش را انجام داد. به عنوان مثال برای یک کوئری Linq به شکل زیر خواهیم داشت:
در واقع کد اضافه شده به کوئری بالا، قابل کش بودن کوئری را مشخص مینماید و مدت زمان کش شدن آن نیز از طریق کانفیگ مربوطه مشخص میگردد. این نکته را هم درنظر داشته باشید که کش در سطح کوئری برای کوئریهایی که دقیقا مثل هم هستند اعمال میگردد و با افزوده یا کاسته شدن یک شرط جدید به کوئری، مجددا کوئری سمت پایگاه داده ارسال میگردد.
در انتها لینکهای زیر هم جهت مطالعه بیشتر پیشنهاد میگردند:
http://www.nhforge.org/doc/nh/en/index.html#performance-cache-readonly
http://nhforge.org/blogs/nhibernate/archive/2009/02/09/quickly-setting-up-and-using-nhibernate-s-second-level-cache.aspx
http://www.klopfenstein.net/lorenz.aspx/using-syscache-as-secondary-cache-in-nhibernate
http://stackoverflow.com/questions/1837651/hibernate-cache-strategy
همان طور که میدانیم کش در NHibernate در دو سطح قابل انجام میباشد:
- کش سطح اول که همان اطلاعات سشن، در تراکنش جاری هست و با اتمام تراکنش، محتویات آن خالی میگردد. این سطح همیشه فعال میباشد و در این بخش قصد پرداختن به آن را نداریم.
- کش سطح دوم که بین همهی تراکنشها مشترک و پایدار میباشد. این مورد به طور پیش فرض فعال نمیباشد و میبایستی از طریق کانفیگ برنامه فعال گردد.
جهت پیاده سازی باید قسمتهای ذیل را در کانفیگ مربوط به NHibernate اضافه نمود:
<?xml version="1.0" encoding="utf-8"?> <configuration> <configSections> <section name="hibernate-configuration" type="NHibernate.Cfg.ConfigurationSectionHandler, NHibernate"/> <section name="syscache" type="NHibernate.Caches.SysCache.SysCacheSectionHandler, NHibernate.Caches.SysCache" requirePermission="false"/> </configSections> <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2" > <session-factory> <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property> <property name="dialect">NHibernate.Dialect.MsSql2005Dialect</property> <property name="connection.driver_class">NHibernate.Driver.SqlClientDriver</property> <property name="connection.connection_string_name">LocalSqlServer</property> <property name="show_sql">false</property> <property name="hbm2ddl.keywords">none</property> <property name="cache.use_second_level_cache">true</property> <property name="cache.use_query_cache" >true</property> <property name="cache.provider_class">NHibernate.Caches.SysCache.SysCacheProvider, NHibernate.Caches.SysCache</property> </session-factory> </hibernate-configuration> <syscache> <cache region="LongExpire" expiration="3600" priority="5"/> <cache region="ShortExpire" expiration="600" priority="3"/> </syscache> </configuration>
پیاده سازی Caching در NHibernate در سه مرحله قابل اعمال میباشد :
- کش در سطح Load موجودیتهای مستقل
- کش در سطح Load موجودیتهای وابسته Bag , List , Set , …
- کش در سطح Query ها
Providerهای مختلفی برای اعمال و پیاده سازی آن وجود دارند که معروفترین آنها SysCache بوده و ما هم از همان استفاده مینماییم.
- مدت زمان پیش فرض کش سطح دوم، ۵ دقیقه میباشد و در صورت نیاز به تغییر آن، باید تگ مربوط به SysCache را تنظیم نمود. محدودیتی در تعریف تعداد متفاوتی از زمانهای خالی شدن کش وجود ندارد و مدت زمان آن بر حسب ثانیه مشخص میگردد. نحوهی تخصیص زمان انقضای کش به هر مورد بدین شکل صورت میگیرد که region مربوطه در آن معرفی میگردد.
جهت اعمال کش در سطح Load موجودیتهای مستقل، علاوه بر کانفیگ اصلی، میبایستی کدهای زیر را به Mapping موجودیت اضافه نمود مانند :
<?xml version="1.0" encoding="utf-8" ?> <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="Core.Domain" namespace="Core.Domain.Model"> <class name="Organization" table="Core_Enterprise_Organization"> <cache usage="nonstrict-read-write" region="ShortExpire"/> <id name="Id" > <generator/> </id> <version name="Version"/> <property name="Title" not-null="true" unique="true"/> <property name="Code" not-null="true" unique="true"/> </class> </hibernate-mapping>
<?xml version="1.0" encoding="utf-8" ?> <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="Core.Domain" namespace="Core.Domain.Model"> <class name="Party" table="Core_Enterprise_Party"> <id name="Id" > <generator /> </id> <version name="Version"/> <property name="Username" unique="true"/> <property name="DisplayName" not-null="true"/> <bag name="PartyGroups" inverse="true" table="Core_Enterprise_PartyGroup" cascade="all-delete-orphan"> <cache usage="nonstrict-read-write" region="ShortExpire"/> <key column="Party_id_fk"/> <one-to-many/> </bag> </class> </hibernate-mapping>
ویژگی usage نیز با مقادیر زیر قابل تنظیم است:
- read-only : این مورد جهت موجودیتهایی مناسب است که امکان بروزرسانی آنها توسط کاربر وجود ندارد. این مورد بهترین کارآیی را دارد.
- read-write : این مورد جهت موجودیتهایی بکار میرود که امکان بروزرسانی آنها توسط کاربر وجود دارد. این مورد کارآیی پایینتری دارد.
- nonstrict-read-write : این مورد جهت موجودیتهایی مناسب میباشد که امکان بروزرسانی آنها توسط کاربر وجود دارد؛ اما امکان همزمان بروز کردن آنها توسط چندین کاربر وجود نداشته باشد. این مورد در قیاس، کارآیی بهتر و بهینهتری نسبت به مورد قبل دارد.
جهت اعمال کش در کوئریها نیز باید مراحل خاص خودش را انجام داد. به عنوان مثال برای یک کوئری Linq به شکل زیر خواهیم داشت:
public IList<Organization> Search(QueryOrganizationDto dto) { var q = SessionInstance.Query<Organization>(); if (!String.IsNullOrEmpty(dto.Title)) q = q.Where(x => x.Title.Contains(dto.Title)); if (!String.IsNullOrEmpty(dto.Code)) q = q.Where(x => x.Code.Contains(dto.Code)); q = q.OrderBy(x => x.Title); q = q.CacheRegion("ShortExpire").Cacheable(); return q.ToList(); }
در انتها لینکهای زیر هم جهت مطالعه بیشتر پیشنهاد میگردند:
http://www.nhforge.org/doc/nh/en/index.html#performance-cache-readonly
http://nhforge.org/blogs/nhibernate/archive/2009/02/09/quickly-setting-up-and-using-nhibernate-s-second-level-cache.aspx
http://www.klopfenstein.net/lorenz.aspx/using-syscache-as-secondary-cache-in-nhibernate
http://stackoverflow.com/questions/1837651/hibernate-cache-strategy