Service Layer
نقش لایهی سرویس این است که به عنوان یک مدخل ورودی به برنامه کاربردی عمل کند. در برخی مواقع این لایه را به عنوان لایهی Facade نیز میشناسند. این لایه، دادهها را در قالب یک نوع داده ای قوی (Strongly Typed) به نام View Model، برای لایهی Presentation فراهم میکند. کلاس View Model یک Strongly Typed محسوب میشود که نماهای خاصی از دادهها را که متفاوت از دید یا نمای تجاری آن است، بصورت بهینه ارائه مینماید. در مورد الگوی View Model در مباحث بعدی بیشتر صحبت خواهم کرد.
الگوی Facade یک Interface ساده را به منظور کنترل دسترسی به مجموعه ای از Interfaceها و زیر سیستمهای پیچیده ارائه میکند. در مباحث بعدی در مورد آن بیشتر صحبت خواهم کرد.
کلاسی با نام ProductViewModel را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:
public class ProductViewModel { Public int ProductId {get; set;} public string Name { get; set; } public string Rrp { get; set; } public string SellingPrice { get; set; } public string Discount { get; set; } public string Savings { get; set; } }
برای اینکه کلاینت با لایهی سرویس در تعامل باشد باید از الگوی Request/Response Message استفاده کنیم. بخش Request توسط کلاینت تغذیه میشود و پارامترهای مورد نیاز را فراهم میکند. کلاسی با نام ProductListRequest را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:
using SoCPatterns.Layered.Model; namespace SoCPatterns.Layered.Service { public class ProductListRequest { public CustomerType CustomerType { get; set; } } }
در شی Response نیز بررسی میکنیم که درخواست به درستی انجام شده باشد، دادههای مورد نیاز را برای کلاینت فراهم میکنیم و همچنین در صورت عدم اجرای صحیح درخواست، پیام مناسب را به کلاینت ارسال مینماییم. کلاسی با نام ProductListResponse را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:
public class ProductListResponse { public bool Success { get; set; } public string Message { get; set; } public IList<ProductViewModel> Products { get; set; } }
به منظور تبدیل موجودیت Product به ProductViewModel، به دو متد نیاز داریم، یکی برای تبدیل یک Product و دیگری برای تبدیل لیستی از Product. شما میتوانید این دو متد را به کلاس Product موجود در Domain Model اضافه نمایید، اما این متدها نیاز واقعی منطق تجاری نمیباشند. بنابراین بهترین انتخاب، استفاده از Extension Methodها میباشد که باید برای کلاس Product و در لایهی سرویس ایجاد نمایید. کلاسی با نام ProductMapperExtensionMethods را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:
public static class ProductMapperExtensionMethods { public static ProductViewModel ConvertToProductViewModel(this Model.Product product) { ProductViewModel productViewModel = new ProductViewModel(); productViewModel.ProductId = product.Id; productViewModel.Name = product.Name; productViewModel.RRP = String.Format(“{0:C}”, product.Price.RRP); productViewModel.SellingPrice = String.Format(“{0:C}”, product.Price.SellingPrice); if (product.Price.Discount > 0) productViewModel.Discount = String.Format(“{0:C}”, product.Price.Discount); if (product.Price.Savings < 1 && product.Price.Savings > 0) productViewModel.Savings = product.Price.Savings.ToString(“#%”); return productViewModel; } public static IList<ProductViewModel> ConvertToProductListViewModel( this IList<Model.Product> products) { IList<ProductViewModel> productViewModels = new List<ProductViewModel>(); foreach(Model.Product p in products) { productViewModels.Add(p.ConvertToProductViewModel()); } return productViewModels; } }
حال کلاس ProductService را جهت تعامل با کلاس سرویس موجود در Domain Model و به منظور برگرداندن لیستی از محصولات و تبدیل آن به لیستی از ProductViewModel، ایجاد مینماییم. کلاسی با نام ProductService را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:
public class ProductService { private Model.ProductService _productService; public ProductService(Model.ProductService ProductService) { _productService = ProductService; } public ProductListResponse GetAllProductsFor( ProductListRequest productListRequest) { ProductListResponse productListResponse = new ProductListResponse(); try { IList<Model.Product> productEntities = _productService.GetAllProductsFor(productListRequest.CustomerType); productListResponse.Products = productEntities.ConvertToProductListViewModel(); productListResponse.Success = true; } catch (Exception ex) { // Log the exception… productListResponse.Success = false; // Return a friendly error message productListResponse.Message = ex.Message; } return productListResponse; } }
کلاس Service تمامی خطاها را دریافت نموده و پس از مدیریت خطا، پیغامی مناسب را به کلاینت ارسال میکند. همچنین این لایه محل مناسبی برای Log کردن خطاها میباشد. در اینجا کد نویسی لایه سرویس به پایان رسید و در ادامه به کدنویسی Data Layer میپردازیم.
Data Layer
برای ذخیره سازی محصولات، یک بانک اطلاعاتی با نام Shop01 ایجاد کنید که شامل جدولی به نام Product با ساختار زیر باشد:
برای اینکه کدهای بانک اطلاعاتی را سریعتر تولید کنیم از روش Linq to SQL در Data Layer استفاده میکنم. برای این منظور یک Data Context برای Linq to SQL به این لایه اضافه میکنیم. بر روی پروژه SoCPatterns.Layered.Repository کلیک راست نمایید و گزینه Add > New Item را انتخاب کنید. در پنجره ظاهر شده و از سمت چپ گزینه Data و سپس از سمت راست گزینه Linq to SQL Classes را انتخاب نموده و نام آن را Shop.dbml تعیین نمایید.
از طریق پنجره Server Explorer به پایگاه داده مورد نظر متصل شوید و با عمل Drag & Drop جدول Product را به بخش Design کشیده و رها نمایید.
اگر به یاد داشته باشید، در لایه Model برای برقراری ارتباط با پایگاه داده از یک Interface به نام IProductRepository استفاده نمودیم. حال باید این Interface را پیاده سازی نماییم. کلاسی با نام ProductRepository را با کد زیر به پروژه SoCPatterns.Layered.Repository اضافه کنید:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using SoCPatterns.Layered.Model; namespace SoCPatterns.Layered.Repository { public class ProductRepository : IProductRepository { public IList<Model.Product> FindAll() { var products = from p in new ShopDataContext().Products select new Model.Product { Id = p.ProductId, Name = p.ProductName, Price = new Model.Price(p.Rrp, p.SellingPrice) }; return products.ToList(); } } }
در متد FindAll، با استفاده از دستورات Linq to SQL، لیست تمامی محصولات را برگرداندیم. کدنویسی لایهی Data هم به پایان رسید و در ادامه به کدنویسی لایهی Presentation و UI میپردازیم.
Presentation Layer
به منظور جداسازی منطق نمایش (Presentation) از رابط کاربری (User Interface)، از الگوی Model View Presenter یا همان MVP استفاده میکنیم که در مباحث بعدی با جزئیات بیشتری در مورد آن صحبت خواهم کرد. یک Interface با نام IProductListView را با کد زیر به پروژه SoCPatterns.Layered.Presentation اضافه کنید:
using SoCPatterns.Layered.Service; public interface IProductListView { void Display(IList<ProductViewModel> Products); Model.CustomerType CustomerType { get; } string ErrorMessage { set; } }
این Interface توسط Web Formهای ASP.NET و یا Win Formها باید پیاده سازی شوند. کار با Interfaceها موجب میشود تا تست Viewها به راحتی انجام شوند. کلاسی با نام ProductListPresenter را با کد زیر به پروژه SoCPatterns.Layered.Presentation اضافه کنید:
using SoCPatterns.Layered.Service; namespace SoCPatterns.Layered.Presentation { public class ProductListPresenter { private IProductListView _productListView; private Service.ProductService _productService; public ProductListPresenter(IProductListView ProductListView, Service.ProductService ProductService) { _productService = ProductService; _productListView = ProductListView; } public void Display() { ProductListRequest productListRequest = new ProductListRequest(); productListRequest.CustomerType = _productListView.CustomerType; ProductListResponse productResponse = _productService.GetAllProductsFor(productListRequest); if (productResponse.Success) { _productListView.Display(productResponse.Products); } else { _productListView.ErrorMessage = productResponse.Message; } } } }
کلاس Presenter وظیفهی واکشی داده ها، مدیریت رویدادها و بروزرسانی UI را دارد. در اینجا کدنویسی لایهی Presentation به پایان رسیده است. از مزایای وجود لایهی Presentation این است که تست نویسی مربوط به نمایش دادهها و تعامل بین کاربر و سیستم به سهولت انجام میشود بدون آنکه نگران دشواری Unit Test نویسی Web Formها باشید. حال میتوانید کد نویسی مربوط به UI را انجام دهید که در ادامه به کد نویسی در Win Forms و Web Forms خواهیم پرداخت.
خطا و راهنمایی
Error1Cannot create an instance of "Browser"....\Samples\DemosBrowser\MainWindow.xaml99DemosBrowser
Could not find a part of the path 'C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\Pdf\AccountingBalanceSample.pdf'. at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath) at System.IO.FileStream.Init(String path, FileMode mode, FileAccess access, Int32 rights, Boolean useRights, FileShare share, Int32 bufferSize, FileOptions options, SECURITY_ATTRIBUTES secAttrs, String msgPath, Boolean bFromProxy, Boolean useLongPath, Boolean checkHost) at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share) at PdfRpt.FluentInterface.DataTemplate.get_PdfStreamOutput() in ...\Lib\FluentInterface\DataTemplate.cs:line 605 at PdfRpt.PdfReportDocument.createPdf() in ...\Lib\PdfReportDocument.cs:line 119 at PdfRpt.PdfReportDocument.<runInReleaseMode>b__0(Document pdfDisposable) in ...\Lib\PdfReportDocument.cs:line 113 at PdfRpt.Core.Helper.Guard.SafeUsingBlock[TDisposable,T](TDisposable disposable, Action`1 action, Func`2 unwrapper) in ...\Lib\Core\Helper\Guard.cs:line 91 at PdfRpt.Core.Helper.Guard.SafeUsingBlock[TDisposable](TDisposable disposable, Action`1 action) in ...\Lib\Core\Helper\Guard.cs:line 58 at PdfRpt.PdfReportDocument.runInReleaseMode() in ...\Lib\PdfReportDocument.cs:line 105 at PdfRpt.PdfReportDocument.GeneratePdf(Boolean debugMode) in ...\Lib\PdfReportDocument.cs:line 87 at PdfRpt.FluentInterface.PdfReport.Generate(Action`1 pdfRptFileBuilder, Boolean debugMode) in ...\Lib\FluentInterface\PdfReport.cs:line 90 at PdfReportSamples.AccountingBalanceColumn.AccountingBalanceColumnPdfReport.CreatePdfReport() in ...\Samples\PdfReportSamples\AccountingBalanceColumn\AccountingBalanceColumnPdfReport.cs:line 17
Warning4Could not resolve this reference. Could not locate the assembly "System.Windows.Controls.Toolkit, Version=4.0.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35". Check to make sure the assembly exists on disk. If this reference is required by your code, you may get compilation errors.SlPdf
Warning5Could not resolve this reference. Could not locate the assembly "System.Windows.Controls.Toolkit.Internals, Version=4.0.5.0, Culture=neutral, PublicKeyToken=2c5c654d367bf4a7". Check to make sure the assembly exists on disk. If this reference is required by your code, you may get compilation errors.SlPdf
Error1The type or namespace name 'BusyIndicator' could not be found (are you missing a using directive or an assembly reference?)...\Samples\SlPdf\SlPdf\obj\Debug\MainPage.g.i.cs3818SlPdf
Warning2The variable 'ex' is declared but never used...\Samples\DemosBrowser\Converters\ShellThumbnailConverter.cs4030DemosBrowser
EF Code First #5
در قسمت قبل خاصیت AutomaticMigrationsEnabled را در کلاس Configuration به true تنظیم کردیم. به این ترتیب، عملیات ساده شده، اما یک سری از قابلیتهای ردیابی تغییرات را از دست خواهیم داد و این عملیات، صرفا یک عملیات رو به جلو خواهد بود.
اگر AutomaticMigrationsEnabled را مجددا به false تنظیم کنیم و هربار به کمک دستوارت Add-Migration و Update-Database تغییرات مدلها را به بانک اطلاعاتی اعمال نمائیم، علاوه بر تشکیل تاریخچه این تغییرات در برنامه، امکان بازگشت به عقب و لغو تغییرات صورت گرفته نیز مهیا میگردد.
هدف قرار دادن مرحلهای خاص یا لغو آن
به همان پروژه قسمت قبل مراجعه نمائید. در کلاس Configuration آن، خاصیت AutomaticMigrationsEnabled را به false تنظیم کنید. سپس یک خاصیت جدید را به کلاس Project اضافه نموده و برنامه را اجرا نمائید. بلافاصله خطای زیر را دریافت خواهیم کرد:
Unable to update database to match the current model because there are pending changes and
automatic migration is disabled. Either write the pending model changes to a code-based migration
or enable automatic migration. Set DbMigrationsConfiguration.AutomaticMigrationsEnabled to true
to enable automatic migration.
EF تشخیص داده است که کلاس مدل برنامه، با بانک اطلاعاتی تطابق ندارد و همچنین ویژگی مهاجرت خودکار نیز فعال نیست. بنابراین اعمال code-based migration را توصیه کرده است.
برای این منظور به کنسول پاورشل NuGet مراجعه نمائید (منوی Tools در ویژوال استودیو، گزینه Library package manager آن و سپس انتخاب گزینه package manager console). در ادامه فرمان add-m را نوشته و دکمه tab را فشار دهید. یک منوی Auto Complete ظاهر خواهد شد که از آن میتوان فرمان add-migration را انتخاب نمود. در اینجا یک نام را هم نیاز است وارد کرد؛ برای مثال:
Add-Migration AddSomeProp2ToProject
به این ترتیب کلاس زیر را به صورت خودکار تولید خواهد کرد:
namespace EF_Sample02.Migrations
{
using System.Data.Entity.Migrations;
public partial class AddSomeProp2ToProject : DbMigration
{
public override void Up()
{
AddColumn("Projects", "SomeProp", c => c.String());
AddColumn("Projects", "SomeProp2", c => c.String());
}
public override void Down()
{
DropColumn("Projects", "SomeProp2");
DropColumn("Projects", "SomeProp");
}
}
}
مدلهای برنامه را با بانک اطلاعاتی تطابق داده و دریافته است که هنوز دو خاصیت در اینجا به بانک اطلاعاتی اضافه نشدهاند.
از متد Up برای اعمال تغییرات و از متد Down برای بازگشت به قبل استفاده میگردد. نام فایل این کلاس هم طبق معمول چیزی است شبیه به timeStamp_AddSomeProp2ToProject.cs .
در ادامه نیاز است این تغییرات به بانک اطلاعاتی اعمال شوند. به همین منظور دستور زیر را در کنسول پاورشل وارد نمائید:
Update-Database -Verbose
پارامتر Verbose آن سبب خواهد شد تا جزئیات عملیات به صورت مفصل گزارش داده شود که شامل دستورات ALTER TABLE نیز هست:
Using NuGet project 'EF_Sample02'.
Using StartUp project 'EF_Sample02'.
Target database is: 'testdb2012' (DataSource: (local), Provider: System.Data.SqlClient, Origin: Configuration).
Applying explicit migrations: [201205061835024_AddSomeProp2ToProject].
Applying explicit migration: 201205061835024_AddSomeProp2ToProject.
ALTER TABLE [Projects] ADD [SomeProp] [nvarchar](max)
ALTER TABLE [Projects] ADD [SomeProp2] [nvarchar](max)
[Inserting migration history record]
اکنون مجددا یک خاصیت دیگر را مثلا به نام public string SomeProp3، به کلاس Project اضافه نمائید.
سپس همین روال باید مجددا تکرار شود. دستورات زیر را در کنسول پاورشل NuGet اجرا نمائید:
Add-Migration AddSomeProp3ToProject
Update-Database -Verbose
اینبار نیز یک کلاس جدید به نام AddSomeProp3ToProject به پروژه اضافه خواهد شد و سپس بر اساس آن، امکان به روز رسانی بانک اطلاعاتی میسر میگردد.
در ادامه برای مثال به این نتیجه رسیدهایم که نیازی به خاصیت public string SomeProp3 اضافه شده، نبوده است. روش متداول، باز هم مانند سابق است. ابتدا خاصیت را از کلاس Project حذف خواهیم کرد و سپس دو دستور Add-Migration و Update-Database را اجرا خواهیم نمود.
اما با توجه به اینکه مهاجرت خودکار را غیرفعال کردهایم و هربار با فراخوانی دستور Add-Migration یک کلاس جدید، با متدهای Up و Down به پروژه، جهت نگهداری سوابق عملیات اضافه میشوند، میتوان دستور Update-Database را جهت فراخوانی متد Down صرفا یک مرحله موجود نیز فراخوانی نمود.
نکته:
اگر علاقمند باشید که راهنمای مفصل پارامترهای دستور Update-Database را مشاهده کنید، تنها کافی است دستور زیر را در کنسول پاورشل اجرا نمائید:
get-help update-database -detailed
به عنوان نمونه اگر در حین فراخوانی دستور Update-Database احتمال از دست رفتن اطلاعات باشد، عملیات متوقف میشود. برای وادار کردن پروسه به انجام تغییرات بر روی بانک اطلاعاتی میتوان از پارامتر Force در اینجا استفاده کرد.
در ادامه برای اینکه دستور Update-Database تنها یک مرحله مشخص را که سابقه آن در برنامه موجود است، هدف قرار دهد، باید از پارامتر TargetMigration به همراه نام کلاس مرتبط استفاده کرد:
Update-Database -TargetMigration:"AddSomeProp2ToProject" -Verbose
اگر دقت کرده باشید در اینجا AddSomeProp2ToProject بجای AddSomeProp3ToProject بکارگرفته شده است. اگر یک مرحله قبل را هدف قرار دهیم، متد Down را اجرا خواهد کرد:
Using NuGet project 'EF_Sample02'.
Using StartUp project 'EF_Sample02'.
Target database is: 'testdb2012' (DataSource: (local), Provider: System.Data.SqlClient, Origin: Configuration).
Reverting migrations: [201205061845485_AddSomeProp3ToProject].
Reverting explicit migration: 201205061845485_AddSomeProp3ToProject.
DECLARE @var0 nvarchar(128)
SELECT @var0 = name
FROM sys.default_constraints
WHERE parent_object_id = object_id(N'Projects')
AND col_name(parent_object_id, parent_column_id) = 'SomeProp3';
IF @var0 IS NOT NULL
EXECUTE('ALTER TABLE [Projects] DROP CONSTRAINT ' + @var0)
ALTER TABLE [Projects] DROP COLUMN [SomeProp3]
[Deleting migration history record]
همانطور که ملاحظه میکنید در اینجا عملیات حذف ستون SomeProp3 انجام شده است. البته این خاصیت به صورت خودکار از کدهای برنامه (کلاس Project در این مثال) حذف نمیشود و فرض بر این است که پیشتر اینکار را انجام دادهاید.
سفارشی سازی کلاسهای مهاجرت
تمام کلاسهای خودکار مهاجرت تولید شده توسط پاورشل، از کلاس DbMigration ارث بری میکنند. در این کلاس امکانات قابل توجهی مانند AddColumn، AddForeignKey، AddPrimaryKey، AlterColumn، CreateIndex و امثال آن وجود دارند که در تمام کلاسهای مشتق شده از آن، قابل استفاده هستند. حتی متد Sql نیز در آن پیش بینی شده است که در صورت نیاز به اجرای دستوارت خام SQL، میتوان از آن استفاده کرد.
برای مثال فرض کنید مجددا همان خاصیت public string SomeProp3 را به کلاس Project اضافه کردهایم. اما اینبار نیاز است حین تشکیل این فیلد در بانک اطلاعاتی، یک مقدار پیش فرض نیز برای آن درنظر گرفته شود که در صورت نال بودن مقدار خاصیت آن در برنامه، به صورت خودکار توسط بانک اطلاعاتی مقدار دهی گردد:
namespace EF_Sample02.Migrations
{
using System.Data.Entity.Migrations;
public partial class AddSomeProp3ToProject : DbMigration
{
public override void Up()
{
AddColumn("Projects", "SomeProp3", c => c.String(defaultValue: "some data"));
Sql("Update Projects set SomeProp3=N'some data'");
}
public override void Down()
{
DropColumn("Projects", "SomeProp3");
}
}
}
متد String در اینجا چنین امضایی دارد:
public ColumnModel String(bool? nullable = null, int? maxLength = null, bool? fixedLength = null,
bool? isMaxLength = null, bool? unicode = null, string defaultValue = null, string defaultValueSql = null,
string name = null, string storeType = null)
که برای نمونه در اینجا پارامتر defaultValue آنرا در کلاس AddSomeProp3ToProject مقدار دهی کردهایم.
برای اعمال این تغییرات تنها کافی است دستور Update-Database -Verbose اجرا گردد. اینبار خروجی SQL اجرا شده آن به نحو زیر است که شامل مقدار پیش فرض نیز شده است:
ALTER TABLE [Projects] ADD [SomeProp3] [nvarchar](max) DEFAULT 'some data'
تعیین مقدار پیش فرض، زمانیکه یک فیلد not null تعریف شدهاست نیز میتواند مفید باشد. همچنین در اینجا امکان اجرای دستورات مستقیم SQL نیز وجود دارد که نمونهای از آنرا در متد Up فوق مشاهده میکنید.
افزودن رکوردهای پیش فرض در حین به روز رسانی بانک اطلاعاتی
در قسمتهای قبل با متد Seed که به همراه آغاز کنندههای بانک اطلاعاتی EF ارائه شدهاند، جهت افزودن رکوردهای اولیه و پیش فرض به بانک اطلاعاتی آشنا شدید. در اینجا نیز با تحریف متد Seed در کلاس Configuration، چنین امری میسر است:
namespace EF_Sample02.Migrations
{
using System;
using System.Data.Entity.Migrations;
internal sealed class Configuration : DbMigrationsConfiguration<EF_Sample02.Sample2Context>
{
public Configuration()
{
this.AutomaticMigrationsEnabled = false;
this.AutomaticMigrationDataLossAllowed = true;
}
protected override void Seed(EF_Sample02.Sample2Context context)
{
context.Users.AddOrUpdate(
a => a.Name,
new Models.User { Name = "Vahid", AddDate = DateTime.Now },
new Models.User { Name = "Test", AddDate = DateTime.Now });
}
}
}
متد AddOrUpdate در EF 4.3 اضافه شده است. این متد ابتدا بررسی میکند که آیا رکورد مورد نظر در بانک اطلاعاتی وجود دارد یا خیر. اگر خیر، آنرا اضافه خواهد کرد در غیراینصورت، نمونه موجود را به روز رسانی میکند. اولین پارامتر آن، identifierExpression نام دارد. توسط آن مشخص میشود که بر اساس چه خاصیتی باید در مورد update یا add تصمیمگیری شود. دراینجا اگر نیاز به ذکر بیش از یک خاصیت وجود داشت، از anonymously type object میتوان کمک گرفت new { p.Name, p.LastName } .
تولید اسکریپت به روز رسانی بانک اطلاعاتی
بهترین کار و امنترین روش حین انجام این نوع به روز رسانیها، تهیه اسکریپت SQL فرامینی است که باید بر روی بانک اطلاعاتی اجرا شوند. سپس میتوان این دستورات و اسکریپت نهایی را دستی هم اجرا کرد (که روش متداولتری است در محیط کاری).
برای اینکار تنها کافی است دستور زیر را در کنسول پاورشل اجرا نمائیم:
Update-Database -Verbose -Script
پس از اجرای این دستور، یک فایل اسکریپت با پسوند sql تولید شده و بلافاصله در ویژوال استودیو جهت مرور نیز گشوده خواهد شد. برای نمونه محتوای آن برای افزودن خاصیت جدید SomeProp5 به صورت زیر است:
ALTER TABLE [Projects] ADD [SomeProp5] [nvarchar](max)
INSERT INTO [__MigrationHistory] ([MigrationId], [CreatedOn], [Model], [ProductVersion]) VALUES
('201205060852004_AutomaticMigration', '2012-05-06T08:52:00.937Z', 0x1F8B0800000............ '4.3.1')
همانطور که ملاحظه میکنید، در یک مرحله، جدول پروژهها را به روز خواهد کرد و در مرحله بعد، سابقه آنرا در جدول __MigrationHistory ثبت میکند.
یک نکته:
اگر دستور فوق را بر روی برنامهای که با بانک اطلاعاتی هماهنگ است اجرا کنیم، خروجی را مشاهده نخواهیم کرد. برای این منظور میتوان مرحله خاصی را توسط پارامتر SourceMigration هدف گیری کرد:
Update-Database -Verbose -Script -SourceMigration:"stepName"
استفاده از DB Migrations در عمل
البته این یک روش پیشنهادی و امن است:
الف) در ابتدای اجرا برنامه، پارامتر ورودی متد System.Data.Entity.Database.SetInitializer را به نال تنظیم کنید تا برنامه تغییری را بر روی بانک اطلاعاتی اعمال نکند.
ب) توسط دستور enable-migrations، فایلهای اولیه DB Migration را ایجاد کنید. پیش فرضهای آن را نیز تغییر ندهید.
ج) هر بار که کلاسهای مدل برنامه تغییر کردند و پس از آن نیاز به به روز رسانی ساختار بانک اطلاعاتی وجود داشت دو دستور زیر را اجرا کنید:
Add-Migration AddSomePropToProject
Update-Database -Verbose -Script
به این ترتیب سابقه تغییرات در برنامه نگهداری شده و همچنین بدون اجرای دستورات بر روی بانک اطلاعاتی، اسکریپت نهایی اعمال تغییرات تولید میگردد.
د) اسکریپت تولید شده را بررسی کرده و پس از تائید و افزودن به سورس کنترل، به صورت دستی بر روی بانک اطلاعاتی اجرا کنید (مثلا توسط management studio).
public class Page { public int Id { get; set; } public string Path { get; set; } public string Content { get; set; } }
public class CustomVirtualPathProvider : VirtualPathProvider { public override bool FileExists(string virtualPath) { return base.FileExists(virtualPath) || FileExistsInDatabase(virtualPath); } public override VirtualFile GetFile(string virtualPath) { return base.FileExists(virtualPath) ? base.GetFile(virtualPath) : new CustomVirtualFile(virtualPath, GetFileFromDatabase(virtualPath)); } private bool FileExistsInDatabase(string virtualPath) { virtualPath = virtualPath.Replace("~", ""); return new DatabaseContext().Pages.Any(v => v.Path == virtualPath); } private byte[] GetFileFromDatabase(string virtualPath) { virtualPath = virtualPath.Replace("~", ""); return Encoding.UTF8.GetBytes(new DatabaseContext().Pages.First(v => v.Path == virtualPath).Content); } }
public class CustomVirtualFile : VirtualFile { private readonly byte[] _content; public CustomVirtualFile(string virtualPath, byte[] content) : base(virtualPath) { _content = content; } public override Stream Open() { return new MemoryStream(_content); } }
حال نوبت ثبت کلاس CustomVirtualPathProvider جهت استفادهی خودکار از آن میباشد. برای این کار در تابع Application_Start موجود در فایل Global.asax.cs دستور زیر را اضافه مینماییم:
protected void Application_Start() { HostingEnvironment.RegisterVirtualPathProvider(new CustomVirtualPathProvider()); //... }
SASS #2
متغیرها (Variables)
متغیرها در SASS با استفاده از $ در ابتدای نام آن، به عنوان یک مقدار مورد استفادهی در CSS تعریف میشوند. شما در SASS میتوانید متغیرهایی را برای margin ،font-size و یا padding و غیره، تعریف کنید. استفاده از متغیرها این امکان را به شما میدهد که خیلی راحتتر از styleهای تعریف شده، مجدد استفاده کنید.
شما 6 نوع مختلف متغیر را میتوانید با استفاده از SASS بکار ببرید.
- Strings (مثال: ;"myString: "your text here$ )
- Numbers (مثال: ;myNum: 16px$)
- Colors (مثال: ;myColor: aqua$)
- Booleans (مثال: ;myBool: true$)
- Lists (مثال: ;myItemList: 1px solid red$)
- Nulls (مثال: ;myVar: null$)
برای مثالی از استفادهی از این متغیرها، یک فایل را با نام styles.scss ایجاد کرده و کدهای زیر را در آن وارد کنید:
$myColor: #FFF726; $myBackColor: #2B14FF; $myString: "I Love "; $myFontSize: 13px; $myMargin: 0px auto; $myWidth: 300px; h1 { color: $myColor; margin: 0; padding: 0; } h1:before{ content: $myString; } #container { width: $myWidth; margin: $myMargin; background-color:$myBackColor; text-align:center; }
ریاضی (Math)
برخلاف SASS ،CSS به ما امکان استفاده از عبارات ریاضی را میدهد. عملگرهای جمع + ، تفریق - ، تقسیم / ، ضرب * ، باقیمانده % ، مساوی == ، نامساوی =! را SASS پشتیبانی میکند. در هنگام استفاده از عبارات ریاضی چند نکته وجود دارد که باید رعایت کنید:
نکته1: چون علامت / در CSS به عنوان یک کوتاه کننده استفاده میشود مانند font: 14px/16px، در صورتیکه بخواهید عمل تقسیم را بر روی مقدار ثابتی انجام دهید باید آنها را درون پرانتر قرار دهید.
$fontDiff: (14px/16px);
$container-width: 100% - 20px;
حال میخواهیم براساس عرض container، ستونهای پویایی را ایجاد کنیم:
$container-width: 100%; .container { width: $container-width; } .col-4 { width: $container-width / 4; }
.container { width: 100%; } .col-4 { width: 25%; }
مشاهدهی پیاده سازی مثال بالا اینجا.
توابع (Functions)
یکی از بهترین قسمتهای SASS، توابع پیاده سازی شدهی آن است. شما میتوانید لیست بزرگی از توابع SASS را در اینجا مشاهده کنید.برای نمونه برخی از توابع مربوط به کار با رنگها را توضیح میدهیم:
- (darken ($color, $amount: این تابع برای تیرهتر کردن یک کد رنگ میباشد. شما برای استفادهی از این تابع باید دو مقدار را به آرگومانهای ورودی این تابع که به ترتیب کد رنگ و میزان تیرهتر شدن آن به صورت درصد از %0 تا %100 میباشند، ارسال کنید و خروجی آن کد رنگ تولید شده است. توجه داشته باشید نوع سیستم رنگی ارسال شده به عنوان پارامتر color$، خروجی این تابع نیز همان نوع میباشد.
darken(hsl(25, 100%, 80%), 30%) => hsl(25, 100%, 50%) darken(#800, 20%) => #200
- (lighten ($color, $amount: این تابع برای روشنتر کردن یک رنگ میباشد و دقیقا برعکس تابع darken عمل میکند.
lighten(hsl(0, 0%, 0%), 30%) => hsl(0, 0, 30) lighten(#800, 20%) => #e00
- (alpha ($color) / opacity($color: با استفاده از این دو تابع میتوانید میزان شفافیت/کدری را مشخص کنید.
- (mix ($color1, $color2, $weight:50% : با استفاده از این تابع
میتوانید دو رنگ را با هم ترکیب کنید. مقدار پیش فرض آرگومان weight$
برابر %50 میباشد و تعیین آن اختیاری است. محدودهی پذیرش مقدار weight$
هرچه به %0 نزدیکتر باشد، باعث نزدیکتر بودن رنگ خروجی به رنگ دوم و
هرچه به %100 نزدیکتر باشد رنگ خروجی به رنگ اول نزدیکتر میشود. توجه:
میزان شفافیت/کدری را نیز میتواند تشخیص دهد.
mix(#f00, #00f) => #7f007f mix(#f00, #00f, 25%) => #3f00bf mix(rgba(255, 0, 0, 0.5), #00f) => rgba(63, 0, 191, 0.75)
تو در تو (Nesting)
SASS امکان تعریف استایلهای تو در تو را به شما میدهد که باعث خواناتر شدن استایلهای نوشته شده میشود. به عنوان مثال به کد CSS زیر توجه کنید:
#container { width: 500px; margin: 0 auto; } #container p { font-family: Arial; font-size: 13px; } #container h1 { font-family: Tahoma; font-size: 15px; } #container h2 { font-family: Helvetica; font-size: 14px; }
$myFontsize1: 13px; $myFontsize2: 18px; $myFontsize3: 25px; $myWidth: 500px; $myMargin: 0px auto; #container { width: $myWidth; margin: $myMargin; p { font-family: Arial; font-size: $myFontsize1; } h1 { font-family: Tahoma; font-size: $myFontsize3; } h2 { font-family: Helvetica; font-size: $myFontsize2; } }
در صورتیکه نیاز به دسترسی به والد داشته باشید کافیست از علامت & استفاده کنید.
a.myAnchor { color: blue; &:hover { text-decoration: underline; } &:visited { color: purple; } }
at-root@
قبل استایلی که میخواهید به صورت تو در تو تعریف نشود، استفاده کنید..first-component { .text { font-size: 1.4em; } .button { font-size: 1.7em; } .second-component { .text { font-size: 1.2em; } .button { font-size: 1.4em; } } }
.first-component .text { font-size: 1.4em; } .first-component .button { font-size: 1.7em; } .first-component .second-component .text { font-size: 1.2em; } .first-component .second-component .button { font-size: 1.4em; }
at-root@
به صورت زیر میشود:.first-component .text { font-size: 1.4em; } .first-component .button { font-size: 1.7em; } .second-component .text { font-size: 1.2em; } .second-component .button { font-size: 1.4em; }
دیت پیکر فارسی
تلاشهای همه افراد هم خوب و ستودنی است ولی به نظر خود من این دیت پیکر امکانات کمی داره
دیت پیکر
http://stefangabos.ro/jquery/zebra-datepicker/
خیلی عالی هست و با کتابخانه PersianCalendar که در جاوا اسکریپت نوشته شده به سادگی فارسی میشه و خیلی خوب هم کار میکنه حالا باید این نسخه ای که تاریخ فارسی داره رو بذارم روی وب
تبدیلش به کامپوننت هم کار زیاد سختی نیست
کار با فرمها در بوت استرپ 3
نحوه ارتقاء فرمهای بوت استرپ 2 به 3
تمام این تغییرات در بوت استرپ 3، جهت پیاده سازی ایده mobile-first بودن آن است. برای مثال فرمهای افقی بوت استرپ 3 با کوچک شدن اندازه صفحه، به صورت خودکار واکنش نشان داده و تبدیل به فرمهای معمولی که اجزای آن به صورت یک stack عمودی قرار گرفتهاند، میشوند.
اکنون اگر فرمهایی را دارید که در برنامههای پیشین خود از بوت استرپ 2 استفاده کردهاند، نیاز است تغییرات ذیل را به آنها اعمال کنید تا با سیستم جدید بوت استرپ 3 سازگار شوند:
- کلاس control-group را به کلاس form-group تبدیل کنید.
- form-search حذف شده است. آنرا با form-inline جایگزین کنید.
- دیگر نیازی به استفاده از input-block-level نیست؛ از آنجائیکه به صورت پیش فرض کلیه inputها دارای عرض 100 درصد هستند.
- help-inline حذف شده است. آنرا با help-block جایگزین کنید.
- عرض ستونها را در فرمهای افقی، برچسبها و کنترلها مشخص کنید.
- کلاس controls حذف شده است.
- کلاس form-control را به inputها و selectها اضافه کنید.
- checkboxها و radioها باید در یک div محصور شوند.
- کلاسهای radio.inline و checkbox.inline باید با inline جایگزین شوند.
- کلاسهای input-small به input-sm و input-large به input-lg تبدیل شدهاند.
- کلاسهای input-prepend با input-group و input-append با input-group جایگزین شدهاند.
- کلاس alert-error حذف شدهاست. بجای آن میشود از alert-warning استفاده کرد.
- کلاس alert-block را با alert جایگزین کنید.
ایجاد اولین فرم افقی با بوت استرپ 3
فرض کنید که قصد داریم یک چنین فرم افقی را توسط امکانات بوت استرپ 3 ایجاد کنیم:
همانطور که ملاحظه میکنید، با کوچک شدن اندازه صفحه، این فرم نیز تغییر شکل میدهد:
کدهای کامل این فرم را در ادامه ملاحظه میکنید:
<div class="container"> <h4 class="alert alert-info"> فرمهای بوت استرپ 3</h4> <div class="row"> <article class="registrationform"> <h2> فرم ثبت نام</h2> <form class="registration form-horizontal" action="#"> <fieldset id="personalinfo"> <legend>اطلاعات شخصی</legend> <section class="row"> <label class="col col-lg-4 control-label" for="myname"> نام</label> <div class="controls"> <input class="col col-lg-8" type="text" name="myname" id="myname" autofocus placeholder="نام و نام خانوادگی" required> </div> <!-- controls --> </section><!-- row --> <section class="row"> <label class="col col-lg-4 control-label" for="companyname"> نام شرکت</label> <div class="controls"> <input class="col col-lg-8" type="text" name="companybname" id="companyname" /> </div> <!-- controls --> </section><!-- row --> <section class="row"> <label class="col col-lg-4 control-label" for="myemail"> ایمیل</label> <div class="controls"> <input class="col col-lg-8" type="email" name="myemail" id="myemail" required autocomplete="off" /> </div> <!-- controls --> </section><!-- row --> </fieldset> <!-- personal info --> <fieldset id="otherinfo"> <legend>سایر اطلاعات</legend> <section class="row"> <label class="col col-lg-4 control-label"> نوع درخواست</label> <div class="controls col col-lg-8"> <label class="radio"> <input type="radio" name="requesttype" value="question" /> سؤال </label> <label class="radio"> <input type="radio" name="requesttype" value="comment" /> انتقاد </label> </div> <!-- controls --> </section><!-- row --> <section class="row"> <label class="col col-lg-4 control-label"> خبرنامه</label> <div class="controls col col-lg-8"> <label class="checkbox"> <input type="checkbox" id="subscribe" name="subscribe" checked value="yes" /> آیا مایل به دریافت ایمیلهای خبرنامه ما هستید؟ </label> </div> <!-- controls --> </section><!-- row --> <section class="row"> <label class="col col-lg-4 control-label" for="reference"> چطور از وجود سایت ما آگاه شدید؟</label> <div class="controls col col-lg-8"> <select name="reference" id="reference"> <option>لطفا انتخاب کنید...</option> <option value="friend">از طریق یک دوست</option> <option value="facebook">Facebook</option> <option value="twitter">Twitter</option> </select> </div> <!-- controls --> </section><!-- row --> </fieldset> <button class="btn" type="submit"> ارسال</button> </form> </article> </div> <!-- end row --> </div> <!-- /container -->
- باید درنظر داشت که اگر هیچگونه فرمتی را به فرمهای بوت استرپ 3 اعمال نکنیم، به صورت پیش فرض فرمت دهی شده و تبدیل به فرمهای عمودی شکیلی میشوند که شاید از دیدگاه خیلیها مناسب بوده و نیاز به تغییرات خاصی نداشته باشند.
- برای تبدیل این فرم عمودی پیش فرض، به فرمهای افقی دو ستونه، نیاز است یک سری کلاس بوت استرپ 3 را به المانهای آن اضافه کنیم. برای این منظور ابتدا کلاس form-horizontal را به تگ فرم اضافه میکنیم.
- هر سطر فرم، در یک المان section با کلاس row قرار خواهد گرفت.
- اکنون هر سطر، از یک برچسب به همراه یک یا چند المان تشکیل خواهد شد. در هر سطر، کنترلها در یک div با کلاس controls قرار میگیرند.
- برای اینکه برچسبهای هر ردیف با کنترلها و المانهای آن ردیف، تراز شوند، تنها کافی است به آنها کلاس control-label را اضافه کنیم.
در ادامه تمام این مراحل را باید به ازای هر سطر فرم تکرار کنیم.
- زمانیکه به radio buttons یا check boxes میرسیم، باید به چند نکته دقت داشت:
الف) حین کار با radio buttons، علاوه بر برچسب آن سطر که با label مشخص میشود، هر radio button نیز باید داخل یک label با کلاس radio محصور شود.
ب) تمام radio buttons یک سطر نیز باید در یک div ایی با کلاس controls محصور شوند.
این نکته در مورد check boxes نیز صادق است.
با رعایت همین چند نکته ساده میتوان به یک طراحی دو ستونی خودکار واکنشگرا رسید.
اصلاح قالب ایجاد فرمهای خودکار ASP.NET MVC بر اساس بوت استرپ 3
مطلب «ویرایش قالب پیش فرض Add View در ASP.NET MVC برای سازگار سازی آن با Twitter bootstrap» جهت بوت استرپ 2 تهیه شده بود. فایل نهایی ویرایش شده آنرا با توجه به توضیحات مطلب جاری برای بوت استرپ 3 از پیوست انتهای بحث دریافت کنید و برای استفاده از آن فقط کافی است آنرا در مسیر CodeTemplates\AddView\CSHTML\CreateBootstrap3Form.tt ریشه پروژه جاری خود کپی و به پروژه اضافه کنید تا در صفحه دیالوگ Add view ظاهر شود (خاصیت custom tool آنرا هم خالی کنید).
در مورد اعتبارسنجیهای فرمها چطور؟
اصلاح مطالبی مانند «اعمال کلاسهای ویژه اعتبارسنجی Twitter bootstrap به فرمهای ASP.NET MVC» جهت کار با فرمهای بوت استرپ 3 بسیار ساده است. از این جهت که در کدهای آن فقط باید نام کلاسهای CSS قدیمی به جدید ویرایش شوند. مابقی کدها یکسان است. مثلا نام کلاس control-group شده است form-group (همان توضیحات ابتدای بحث جاری). کلاسهای error شدهاند has-error و success شده است has-success.
فایلهای نهایی این قسمت را از اینجا نیز میتوانید دریافت کنید:
bs3-sample05.zip
با استفاده از سرویس توکار IWebHostEnvironment و متد ()IsDevelopment آن، میتوان تشخیص داد که اکنون برنامه در حالت توسعه اجرا میشود یا توزیع. بر این اساس اگر کامپوننت DevOnly.razor را به کمک یک RenderFragment تشکیل دهیم، میتوان if بررسی کردن این متد را تبدیل به یک کامپوننت با استفادهی مجدد کرد:
@using Microsoft.Extensions.Hosting @inject Microsoft.AspNetCore.Hosting.IWebHostEnvironment env @if (isDevelopment) { @ChildContent } @code { [Parameter] public RenderFragment ChildContent { get; set; } bool isDevelopment = false; protected override void OnInitialized() { isDevelopment = env.IsDevelopment(); } }
<DevOnly> <p>Show some debugging info here!</p> </DevOnly>