<style> :any-link { color: red; } </style> <a href="page1.htm">Link 1</a>
Selector | نسخه CSS | |||||
No | No | No | No | No | :any-link | 4 |
<style> :local-link { color: red; } </style> <a href="http://www.example.com/article/">Link 1</a> <a href="http://www.example.com/article">Link 2</a> <a href="http://www.example.com/article/">Link 3</a> <a href="http://www.example.com/article/12">Link 4</a>
Selector | نسخه CSS | |||||
No | No | No | No | No | :local-link | 4 |
<style> :local-link { color: red; } :local-link(0) { text-decoration: none; } :local-link(1) { text-decoration: overline; } :local-link(2) { background: blue; } </style> <a href="http://www.example.com">Link 1</a> <a href="http://www.example.com/2016">Link 2</a> <a href="http://www.example.com/2016/01">Link 3</a> <a href="http://www.example.com/2016/01/">Link 4</a> <a href="http://www.example.com/2016/01/02">Link 5</a> <a href="https://www.example.com/2016/01/">Link 6</a> <a href="http://example.com/2016/01">Link 7</a>
Selector | نسخه CSS | |||||
No | No | No | No | No | :local-link(n) | 4 |
Selector | نسخه CSS | |||||
No | No | No | No | No | :active-drop-target | 4 |
Selector | نسخه CSS | |||||
No | No | No | No | No | :valid-drop-target | 4 |
Selector | نسخه CSS | |||||
No | No | No | No | No | :invalid-drop-target | 4 |
<style> :active-drop-target { background: blue; } :valid-drop-target { border: 1px solid green; } :invalid-drop-target { border: 1px solid red; } .container { width: 200px; height: 200px; } </style> <div class="container"></div> <img src="image.jpg" draggable="true"/>
Selector | نسخه CSS | |||||
No | No | No | No | No | :current | 4 |
Selector | نسخه CSS | |||||
No | No | No | No | No | :past | 4 |
Selector | نسخه CSS | |||||
No | No | No | No | No | :future | 4 |
<style> :past(div,p) { background: red; } :current(div,p) { background: green; } :future(div,p) { background: yellow; } </style>
<style> :placeholder-shown { color: gray; } </style> <input placeholder="Enter Username"/> <input placeholder="Enter Password"/>
Selector | نسخه CSS | |||||
-webkit- | -wekit- | -ms- | -moz- | -webkit- | :placeholder-shown | 4 |
| | | | | | |
| | | | | | |
بعضی از قسمتهای صفحه مانند هدر، منوی راهبری، فوتر امثال آن، عموما در تمام صفحات سایت، به یک شکل نمایش داده میشوند. جهت کاهش اینگونه کدهای تکراری، در برنامهی ASP.NET Web Forms، مفهوم Master Page و در برنامههای MVC، مفهوم layout page ارائه شد که قسمتهای مشترک UI را در آن قرار میدهند تا دیگر نیازی نباشد به ازای هر صفحه، آنها را تکرار کرد. Layout در برنامههای Blazor نیز چنین عملکردی را دارد. از لحاظ فنی، Layout نیز یک کامپوننت Blazor محسوب میشود. برای مثال فایل پیشفرض Shared\MainLayout.razor با یک چنین ساختاری:
@inherits LayoutComponentBase <div class="sidebar"> <NavMenu /> </div> <div class="main"> <div class="content px-4"> @Body </div> </div>
- سپس با استفاده از Body@، سبب درج محتوای کامپوننت در حال رندر، در صفحه میشود.
DefaultLayout تعریف شده، در فایل آغازین App.razor به تمام کامپوننتهای برنامه اعمال میشود. اما اگر نیاز باشد کامپوننت خاصی از layout دیگری استفاده کند، میتوان از دایرکتیو layout برای بازنویسی آن، استفاده کرد:
@page "/episodes" @layout DoctorWhoLayout <h1>Component 2</h1>
نکته 2: اگر برای کامپوننتهای خود فایل code-behind تهیه میکنید، دایرکتیو layout@ به ویژگی Layout قرار گرفتهی بر روی کلاس کامپوننت ترجمه میشود:
[Layout(typeof(MainLayout))]
حتی میتوان یک layout خاص را به پوشهای از کامپوننتها اعمال کرد. برای این منظور در ریشهی این پوشه، فایل Imports.razor_ را قرار داده و سپس دایرکتیو layout@ را به آن اضافه کنید. فایل ویژهی Imports.razor_ را میتوان به هر پوشهای از کامپوننتهای برنامه اضافه کرد.
تذکر! دایرکتیو layout@ را در بالاترین فایل Imports.razor_ که در ریشهی پروژه قرار دارد، درج نکنید؛ چون سبب بروز یک حلقهی بینهایت خواهد شد. همانطور که عنوان شد، بالاترین سطح تعریف layout پیشفرض، در فایل App.razor انجام میشود.
$("#dialog").dialog({ autoOpen: false, open: function (event, ui) { $(this).load(url); }) }); <div id="dialog" title="Basic dialog"> <p>Dialog box</p> </div>
طرحبندی صفحات وب با بوت استرپ 4 - قسمت سوم
<div class="clearfix"> <span class="float-left">Float left</span> <span class="float-right">Float right</span> </div>
<div class="col-lg-6 d-flex justify-content-end">
<ul class="nav"> <li><a href class="nav-link">Link</a></li> <li><a href class="nav-link">Link</a></li> <li class="ml-auto"><a href class="nav-link">Right</a></li> </ul>
متغیرها در ES 6
واژهی کلیدی let
تاکنون به کمک واژهی کلیدی var امکان تعریف متغیرها در جاوا اسکریپت مهیا بودند. برای نمونه در مثال زیر، متغیر x داخل بدنهی if با استفاده از var تعریف شدهاست:
var doWork = function(flag){ if(flag){ var x = 3; } return x; };
زمانیکه از var استفاده میشود، برای یک متغیر دو نوع میدان دید را میتوان متصور شد:
- اگر خارج از بدنهی تابع تعریف شود، این متغیر عمومی خواهد بود.
- اگر داخل بدنهی تابع تعریف شود، میدان دید آن محدود به همان بدنهی تابع میشود. در این حالت چیزی به نام block scope بیمفهوم است. در متد doWork فوق، هرچند متغیر x داخل بدنهی بلاک if تعریف شدهاست، اما این x در کل بدنهی تابع در دسترس است و نه صرفا داخل بلاک if. این مورد تا پیش از ES 6 منشاء بسیاری از باگها بودهاست.
بنابراین در اینجا چون x تعریف شده، میدان دیدی در سطح متد دارد، return x معتبر بوده و در حالت دریافت پارامتر true، مقدار 3 را بر میگرداند و در حالت false هم همچنان مقداری را دریافت خواهیم کرد و این مقدار undefined است (اما پیام خطای عدم دسترسی به x را دریافت نمیکنیم).
به این رفتار اصطلاحا hoisting میگویند. در این حالت موتور جاوا اسکریپت، تمام متغیرهای تعریف شدهی توسط var را به صورت ضمنی به ابتدای تعریف متد منتقل کرده و آنها را در آنجا تعریف میکند. به همین جهت است که return x تعریف شدهی در انتهای متد، قابلیت دسترسی به x داخل بدنهی if را دارد.
در ES 6 برای رفع این مشکل، واژهی کلیدی جدیدی به نام let معرفی شدهاست و هدف آن مهیا کردن block scoping تعریف متغیرها است:
var doWork = function(flag){ if(flag){ let x = 3; } return x; };
بله. همانطور که مشاهده میکنید، اینبار میدان دید x به if block تعریف شدهی در آن محدود گشته و دیگر خارج از آن مفهومی ندارد و تعریف نشدهاست. به همین جهت زمانیکه به return x میرسیم، پیام تعریف نشده بودن x را دریافت خواهیم کرد. برای اینکه قطعه کد فوق کار کند، نیاز است return x را به داخل بدنهی قطعهی if تعریف شده، انتقال داد.
این block scoping مهیا شدهی توسط let، با حلقهی for نیز کار میکند:
var doWork = function(){ for(let i = 0; i< 10; i++){ } /* return i won't work */ return 0; };
یک نکته
مفهوم block scoping با تعریف {} معنا پیدا میکند. بنابراین میتوانید یک قطعهی دلخواه را با تعریف {} نیز مشخص کنید:
و یا در مثال ذیل چندین قطعهی تو در تو را مشاهده میکنید:
let outer = 'I am so eccentric!' { let inner = 'I play with neighbors in my block and the sewers' { let innermost = 'I only play with neighbors in my block' } // accessing innermost here would throw } // accessing inner here would throw // accessing innermost here would throw
نمونهی دیگر آن تعریف یک متد داخل یک بلاک است:
{ let _nested = 'secret' function nested () { return _nested } } console.log(nested())
در ES 6 نمیتوان به متغیرهای تعریف شدهی توسط let داخل یک بلاک، در خارج از آن دسترسی یافت. اگر میخواهید سطح دسترسی به متد را افزایش دهید، نیاز است به شکل ذیل عمل کنید و متد را خارج از بدنهی بلاک با سطح دسترسی بیشتری تعریف نمائید:
var nested; { let _nested = 'secret' nested = function () { return _nested } } console.log(nested()) // <- 'secret'
واژهی کلیدی const
در ES 6 برای ایجاد و مقدار دهی متغیرهای فقط خواندنی، واژهی کلیدی const افزوده شدهاست. در اینجا const نیز مانند let دارای block scoping است.
doWork = function() { const value = 10; value = 11; return value; }
در ES 6، انتساب یک مقدار به یک const، پس از تعریف آن، منجر به بروز خطای syntax error خواهد شد. همچنین تعریف مجدد آن نیز چنین خطایی را سبب خواهد شد.
یک نکته
هر چند const سبب read only شدن یک متغیر میشود، اما آنرا immutable نمیکند:
const items = { people: ['you', 'me'] } items.people.push('test') console.log(items)
همانطور که مشاهده میکنید، هنوز هم میتوان به شیء تعریف شده، آیتمی را اضافه کرد (در اینجا test به آرایهی people اضافه شدهاست).
آشنایی با مفهوم shadowing
همان مثال ابتدای بحث را در نظر بگیرید:
var doWork = function(flag){ if(flag){ let x = 10; var x = 3; return x; } };
let x = 10; var doWork = function(flag){ if(flag){ var x = 3; return x; } };
مثال ذکر شده، با مثال ذیل که یک بلاک را توسط {} ایجاد کردهایم، یکی است:
let x = 10; { let x = 3; console.log(x); } console.log(x);
در اینجا نیز ابتدا مقدار 3 که مرتبط با بلاک داخلی است چاپ خواهد شد و سپس مقدار 10 که مرتبط است به بلاک خارجیتر.
در دات نت فریم ورک امکان کامپایل پویای یک قطعه کد دریافت شده از یک رشته، توسط فضای نام CodeDom مهیا است که قدرت قابل توجهی را در اختیار برنامه نویس قرار میدهد.
مثال یک:
رشته زیر را کامپایل کرده و تبدیل به یک فایل exe کنید:
string source =
@"
namespace Foo
{
public class Bar
{
static void Main(string[] args)
{
Bar.SayHello();
}
public static void SayHello()
{
System.Console.WriteLine(""Hello World"");
}
}
}
";
using System;
using System.Collections.Generic;
//دو فضای نامی که برای این منظور اضافه شدهاند
using Microsoft.CSharp;
using System.CodeDom.Compiler;
namespace compilerTest
{
class Program
{
static void compileIt1()
{
//سورس کد ما جهت کامپایل
string source =
@"
namespace Foo
{
public class Bar
{
static void Main(string[] args)
{
Bar.SayHello();
}
public static void SayHello()
{
System.Console.WriteLine(""Hello World"");
}
}
}
";
//تعیین نگارش کامپایلر مورد استفاده
Dictionary<string, string> providerOptions = new Dictionary<string, string>
{
{"CompilerVersion", "v3.5"}
};
//تعیین اینکه کد ما سی شارپ است
CSharpCodeProvider provider = new CSharpCodeProvider(providerOptions);
//تعیین اینکه خروجی یک فایل اجرایی است بعلاوه مشخص سازی محل ذخیره سازی فایل نهایی
CompilerParameters compilerParams = new CompilerParameters
{
OutputAssembly = "D:\\Foo.EXE",
GenerateExecutable = true
};
//عملیات کامپایل در اینجا صورت میگیرد
CompilerResults results = provider.CompileAssemblyFromSource(compilerParams, source);
//اگر خطایی وجود داشته باشد نمایش داده خواهد شد
Console.WriteLine("Number of Errors: {0}", results.Errors.Count);
foreach (CompilerError err in results.Errors)
{
Console.WriteLine("ERROR {0}", err.ErrorText);
}
}
static void Main(string[] args)
{
compileIt1();
Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
کد مورد نظر را به صورت یک فایل dll کامپایل کنید.
برای این منظور تمامی مراحل مانند قبل است فقط GenerateExecutable ذکر شده به false تنظیم شده و نام خروجی نیز به foo.dll باید تنظیم شود.
مثال 3:
کد مورد نظر را در حافظه کامپایل کرده (خروجی dll یا exe نمیخواهیم)، سپس متد SayHello آن را به صورت پویا فراخوانی نموده و خروجی را نمایش دهید.
در این حالت روش کار همانند مثال 1 است با این تفاوت که GenerateInMemory = true و GenerateExecutable = false تنظیم میشوند. همچنین جهت دسترسی به متد کلاس ذکر شده، از قابلیتهای ریفلکشن موجود در دات نت فریم ورک استفاده خواهد شد.
using System;
using System.Collections.Generic;
using Microsoft.CSharp;
using System.CodeDom.Compiler;
using System.Reflection;
namespace compilerTest
{
class Program
{
static void compileIt2()
{
//سورس کد ما جهت کامپایل
string source =
@"
namespace Foo
{
public class Bar
{
static void Main(string[] args)
{
Bar.SayHello();
}
public static void SayHello()
{
System.Console.WriteLine(""Hello World"");
}
}
}
";
//تعیین نگارش کامپایلر مورد استفاده
Dictionary<string, string> providerOptions = new Dictionary<string, string>
{
{"CompilerVersion", "v3.5"}
};
//تعیین اینکه کد ما سی شارپ است
CSharpCodeProvider provider = new CSharpCodeProvider(providerOptions);
//نحوه تعیین مشخص سازی کامپایل در حافظه
CompilerParameters compilerParams = new CompilerParameters
{
GenerateInMemory = true,
GenerateExecutable = false
};
//عملیات کامپایل در اینجا صورت میگیرد
CompilerResults results = provider.CompileAssemblyFromSource(compilerParams, source);
// اگر خطایی در کامپایل وجود نداشت متد دلخواه را فراخوانی میکنیم
if (results.Errors.Count == 0)
{
//استفاده از ریفلکشن برای دسترسی به متد و فراخوانی آن
Type type = results.CompiledAssembly.GetType("Foo.Bar");
MethodInfo method = type.GetMethod("SayHello");
method.Invoke(null, null);
}
}
static void Main(string[] args)
{
compileIt2();
Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
مثال:
اگر رشته سورس ما به صورت زیر بوده و از اسمبلی System.Drawing.Dll نیز کمک گرفته باشد،
string source =
@"
namespace Foo
{
public class Bar
{
static void Main(string[] args)
{
Bar.SayHello();
}
public static void SayHello()
{
System.Console.WriteLine(""Hello World"");
var r = new System.Drawing.Rectangle(0,0,100,100);
System.Console.WriteLine(r);
}
}
}
";
Number of Errors: 1
ERROR The type or namespace name 'Drawing' does not exist in the namespace 'System' (are you missing an assembly reference?)
برای رفع این مشکل و معرفی این اسمبلی، سطر زیر باید پس از تعریف compilerParams اضافه شود.
compilerParams.ReferencedAssemblies.Add("System.Drawing.Dll");
نمونهای دیگر از این دست، استفاده از LINQ میباشد. در این حالت اسمبلی System.Core.Dll نیز به روش ذکر شده باید معرفی گردد تا مشکلی در کامپایل کد رخ ندهد.
کاربردها:
1- استفاده در ابزارهای تولید کد (برای مثال در برنامه Linqer از این قابلیت استفاده میشود)
2- استفادههای امنیتی (ایجاد روشهای تولید یک سریال به صورت پویا و کامپایل پویای کد مربوطه در حافظهای محافظت شده)
3- استفاده جهت مقاصد محاسباتی پیشرفته
4- دادن اجازهی کد نویسی به کاربران برنامهی خود (شبیه به سیستمهای ماکرو و اسکریپت نویسی موجود)
و ...
RxJS اکنون جزئی از پروژههای گوگل است
توسعه دهندهی اصلی RxJS یا همان Ben Lesh اکنون به گوگل پیوستهاست و جزو تیم Angular است. بنابراین در آینده شاهد یکپارچگی بهتر این دو با هم خواهیم بود. البته RxJS هنوز هم به عنوان یک پروژهی مستقل از Angular مدیریت خواهد شد.
آشنایی با تغییرات RxJS 5.5 جهت مهاجرت به RxJS 6.0 ضروری است
در مطلب «کاهش حجم قابل ملاحظهی برنامههای Angular با استفاده از RxJS 5.5» با pipe-able operators آشنا شدیم و این موارد پایههای مهاجرت به RxJS 6.0 هستند. بنابراین پیش از مطالعهی ادامهی بحث نیاز است این مطلب را به خوبی مطالعه و بررسی کنید.
تغییر رفتار خطاهای مدیریت نشده در RxJS 6.0
تا پیش از RxJS 6.0 اگر خطای مدیریت نشدهای رخ میداد، این خطا به صورت synchronous به فراخوان صادر میشد. این رفتار در نگارش 6 تغییر کرده و صدور آن اینبار asynchronous شدهاست.
برای مثال یک چنین کدی تا پیش از RxJS 6.0 کار میکرد:
try { source$.subscribe(nextFn, undefined, completeFn); } catch (err) { handleError(err); }
برای اصلاح این کد در نگارش 6، همان پارامتر دوم متد را مقدار دهی کنید و try/catch را در صورت وجود حذف نمائید.
تغییرات مهم importها در RxJS 6.0
همانطور که در مطلب «کاهش حجم قابل ملاحظهی برنامههای Angular با استفاده از RxJS 5.5» نیز بررسی کردیم، تا نگارش 5 این کتابخانه، importها به صورت زیر بودند:
import 'rxjs/add/operator/map';
import { map } from 'rxjs/operators';
import { timer } from 'rxjs/observable/timer'; import { of } from 'rxjs/observable/of'; import { from } from 'rxjs/observable/from'; import { range } from 'rxjs/observable/range';
import { interval, of } from 'rxjs'; import { filter, mergeMap, scan } from 'rxjs/operators';
البته RxJS 6.0 در کل به همراه 4 گروه کلی importها است که در زیر مشاهده میکنید (در اینجا مواردی که کمتر در برنامههای Angular به صورت مستقیم استفاده میشوند مانند ajax آن و یا webSocket هم قابل مشاهده هستند):
rxjs rxjs/operators rxjs/testing rxjs/webSocket rxjs/ajax
مواردی که از RxJS 6.0 حذف شدهاند
برای کاهش حجم کتابخانهی RxJS و همچنین جلوگیری از بکارگیری متدهایی که نمیبایستی خارج از کدهای اصلی خود RxJS استفاده شوند، تعداد زیادی از متدهای قدیمی آن و روشهای کار پیشین با RxJS حذف شدهاند. برای مثال شما در RxJS 5.5 میتوانید برای کار با عملگر of، یا آنرا از مسیر rxjs/add/observable/of دریافت کنید (همان روش وصله کردن تا پیش از RxJS 5.5) و یا آنرا از مسیر rxjs/observable/of به روش مخصوص ES 6.0 به برنامه اضافه کنید و یا حتی امکان دریافت آن از مسیر rxjs/observable/fromArray نیز میسر است.
در RxJS 6.0 تمام اینها حذف شدهاند و فقط روش زیر باقی ماندهاست:
import { of } from 'rxjs';
معرفی بستهی rxjs-compat
در مطلب «ارتقاء به Angular 6: بررسی تغییرات Angular CLI» روش ارتقاء وابستگیهای پروژه به نگارش 6 را بررسی کردیم. یکی از مراحل آن اجرای دستور زیر بود:
ng update rxjs
پس از آن اگر پروژه را کامپایل کنید، پر خواهد بود از خطاهای rxjs، مانند:
ERROR in node_modules/ng2-slim-loading-bar/src/slim-loading-bar.service.d.ts(1,10): error TS2305: Module '"/node_modules/rxjs/Observable"' has no exported member 'Observable'.
برای رفع این مشکل و ارائهی راهحلی کوتاه مدت، بستهای به نام rxjs-compat ارائه شدهاست که سبب هدایت تعاریف قدیمی به تعاریف جدید میشود و به این ترتیب کدهای کتابخانهی ثالث، بدون مشکل با نگارش 6 نیز قابل استفاده خواهند بود.
برای نصب آن نیاز است دستور زیر را صادر کنید:
npm i rxjs-compat --save
البته دقت داشته باشید از rxjs-compat به عنوان یک راه حل موقت باید استفاده کرد و نیاز است ابتدا کدهای خود را به روش pipe-able operators بازنویسی کنید و مسیرهای importها را اصلاح کنید و در آخر بستههای جدید وابستگیهای ثالث را که از RxJS6 استفاده میکنند، نصب نمائید. در نهایت rxjs-compat را حذف کنید.
خودکار سازی اصلاح importها در برنامههای پیشین، جهت مهاجرت به RxJS 6.0
با توجه به این تغییرات و حذف و اضافه شدنها در نگارش 6، تقریبا دیگر هیچکدام از importهای قبلی شما کار نمیکنند! و اصلاح آنها نیاز به زمان زیادی خواهد داشت. به همین جهت تیم RxJS ابزاری را طراحی کردهاند که با اجرای آن بر روی پروژه، به صورت خودکار تمام importهای قبلی را به نگارش جدید تبدیل میکند. برای اینکار ابتدا ابزار rxjs-tslint را نصب کنید:
npm i -g rxjs-tslint
{ "rulesDirectory": [ "node_modules/rxjs-tslint" ], "rules": { "rxjs-collapse-imports": true, "rxjs-pipeable-operators-only": true, "rxjs-no-static-observable-methods": true, "rxjs-proper-imports": true } }
rxjs-5-to-6-migrate -p src/tsconfig.app.json
البته توصیه شدهاست این ابزار را بیش از یکبار نیاز است اجرا کنید.
خلاصهی روش مهاجرت به RxJS 6x
ابتدا آخرین نگارش rxjs را نصب کنید:
ng update rxjs
npm i rxjs-compat --save
npm i -g rxjs-tslint rxjs-5-to-6-migrate -p src/tsconfig.app.json
یافتن معادلهای جدید دستورات قدیمی
در حین تبدیل کدهای قدیمی به جدید نیاز خواهید داشت تا معادلها را بیابید. برای این منظور به مستندات رسمی این مهاجرت مراجعه کنید:
https://github.com/ReactiveX/rxjs/blob/master/MIGRATION.md
برای مثال در اینجا مشاهده خواهید کرد که معادل Observable.throw حذف شده، اکنون throwError است و همینطور برای مابقی.
یک مثال واقعی تغییر یافته
مخزن کد تمام مثالهای سایت جاری که پیشتر منتشر شدهاند، به نسخهی 6 ارتقاء داده شد. ریز تغییرات RxJS 6.0 آنها را در اینجا میتوانید مشاهده کنید.
در قسمت :
//فقط موارد جدید به تگها و ارتباطات موجود اضافه میشوند foreach (var tag in data.Tags) { if (!listOfActualTagNames.Contains(tag.ToLowerInvariant().Trim())) { _tag.Add(new Tag { Title = tag.Trim() }); } } _uow.SaveChanges(); // ثبت موارد جدید
فقط تگها در جدول خودشون ذخیره میشن و ارتباطی با پست مربوطه ایجاد نمیشه.
آیا نباید به کد زیر تغییر داد؟
if (!listOfActualTagNames.Contains(tag.ToLowerInvariant().Trim())) { var tags = new Tag { Title = tag.Trim() }; _tag.Add(tags); posts.Tags.Add(tags); }
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 خواهیم پرداخت.