public class ModelStateFeatureFilter : IAsyncActionFilter { public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { var state = context.ModelState; // store state ... await next(); } }
در این مقاله قصد داریم با استفاده از جاوااسکریپت خالص، یک برنامهی ساده را با الگوی MVC انجام دهیم. این برنامه، عملیات CRUD را پیاده سازی میکند و تنها به سه فایل index.html , script.js , style.css نیاز دارد و از هیچ کتابخانه یا فریم ورک دیگری در آن استفاده نمیکنیم.
- M مخفف Model میباشد و کار مدیریت دادهها را بر عهده دارد.
- V مخفف View میباشد و وظیفهی نمایش دادهها به کاربر را بر عهده دارد.
- C مخفف Controller میباشد و پل ارتباطی بین Model و View میباشد و مدیریت درخواستها را بر عهده دارد.
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>الگوی MVC در جاوااسکریپت</title> <link rel="stylesheet" href="style.css"> </head> <body> <div id="root"></div> <script src="script.js"></script> </body> </html>
*, *::before, *::after { box-sizing: border-box } html { color: #444; } #root { max-width: 450px; margin: 2rem auto; padding: 0 1rem; } form { display: flex; margin-bottom: 2rem; } [type="text"], button { display: inline-block; -webkit-appearance: none; padding: .5rem 1rem; border: 2px solid #ccc; border-radius: 4px; } button { cursor: pointer; background: #007bff; color: white; border: 2px solid #007bff; margin: 0 .5rem; } [type="text"] { width: 100%; } [type="text"]:active, [type="text"]:focus { outline: 0; border: 2px solid #007bff; } [type="checkbox"] { margin-right: 1rem; } h1 { color: #222; } ul { padding: 0; } li { display: flex; align-items: center; padding: 1rem; margin-bottom: 1rem; background: #f4f4f4; border-radius: 4px; } li span { display: inline-block; padding: .5rem; width: 250px; border-radius: 4px; border: 2px solid transparent; } li span:hover { background: rgba(179, 215, 255, 0.52); } li span:focus { outline: 0; border: 2px solid #007bff; background: rgba(179, 207, 255, 0.52) }
class Model { constructor() {} } class View { constructor() {} } class Controller { constructor(model, view) { this.model = model this.view = view } } const app = new Controller(new Model(), new View())
class Model { constructor() { // یک آرایه از اطلاعات پیش فرض this.todos = [{ id: 1, text: 'Run a marathon', complete: false }, { id: 2, text: 'Plant a garden', complete: false }, ] } // متدی برای افزودن آیتم جدید به آرایه addTodo(todoText) { const todo = { id: this.todos.length > 0 ? this.todos[this.todos.length - 1].id + 1 : 1, text: todoText, complete: false, } this.todos.push(todo) } // متدی برای بروزسانی آیتم مورد نظر editTodo(id, updatedText) { this.todos = this.todos.map(todo => todo.id === id ? { id: todo.id, text: updatedText, complete: todo.complete } : todo ) } // انجام میدهد filter با استفاده از متد id تابعی که عملیات حذف را بوسیله فیلد deleteTodo(id) { this.todos = this.todos.filter(todo => todo.id !== id) } // متدی که در آن مشخص میکنیم کار مد نظرانجام شده یا خیر toggleTodo(id) { this.todos = this.todos.map(todo => todo.id === id ? { id: todo.id, text: todo.text, complete: !todo.complete } : todo ) } }
app.model.addTodo('Take a nap') console.log(app.model.todos)
در حال حاضر با هر بار reload شدن صفحه، فقط اطلاعات پیش فرض، درون آرایه todos قرار میگیرد؛ ولی در ادامه آن را در local storage ذخیره میکنیم.
برای ساختن قسمت View، از جاوااسکریپت استفاده میکنیم و DOM را تغییر میدهیم. البته اینکار را بدون استفاده از JSX و یا یک templating language انجام خواهیم داد. قسمتهای دیگر برنامه مانند Controller و Model نباید درگیر تغییرات DOM یا CSS یا عناصر HTML باشند و تمام این موارد توسط View هندل میشود. کد View به نحو زیر خواهد بود:
class View { constructor() {} // ایجاد یک المنت با کلاسهای استایل دلخواه createElement(tag, className) { const element = document.createElement(tag) if (className) element.classList.add(className) return element } // DOM انتخاب و گرفتن آیتمی خاص از getElement(selector) { const element = document.querySelector(selector) return element } }
سپس قسمت سازنده کلاس View را تغییر میدهیم و تمام المنتهای مورد نیاز را در آن ایجاد میکنیم:
- ارجاعی به المنتی با آیدی root
- تگ h1 برای عنوان
- یک form، input و دکمهای برای افزودن آیتمی جدید به آرایهی todos
- یک المنت ul برای نمایش آیتمهای todos
constructor() { // root ارجاعی به المنتی با آیدی this.app = this.getElement('#root') // عنوان برنامه this.title = this.createElement('h1') this.title.textContent = 'Todos' // فرم ، اینپوت ورودی و دکمه this.form = this.createElement('form') this.input = this.createElement('input') this.input.type = 'text' this.input.placeholder = 'Add todo' this.input.name = 'todo' this.submitButton = this.createElement('button') this.submitButton.textContent = 'Submit' // برای نمایش عناط آرایه یا همان لیست کارها this.todoList = this.createElement('ul', 'todo-list') // افزودن اینپوت ورودی و دکمه به فرم this.form.append(this.input, this.submitButton) // ایجاد شده است app که اینجا ارجاعی به آن بنام root اضافه کردن تمام آیتمهای بالا در المنتی با آیدی this.app.append(this.title, this.form, this.todoList) }
get _todoText() { return this.input.value } _resetInput() { this.input.value = '' }
displayTodos(todos){ //... }
متد displayTodos یک المنت ul و liهایی را به تعداد عناصر todos ایجاد میکند و آنها را نمایش میدهد. هر زمانکه تغییراتی مانند اضافه شدن، حذف و ویرایش در todos صورت گیرد، این متد دوباره فراخوانی میشود و لیست جدید را نمایش میدهد. محتوای متد dispayTodos به شکل زیر خواهد بود:
displayTodos(todos) { // حذف تمام نودها while (this.todoList.firstChild) { this.todoList.removeChild(this.todoList.firstChild) } // اگر هیچ آیتمی در آرایه نبود این پاراگراف با متن پیش فرض نمایش داده میشود if (todos.length === 0) { const p = this.createElement('p') p.textContent = 'Nothing to do! Add a task?' this.todoList.append(p) } else { // وعناصرمربوطه را ایجاد میکند liاگه درون آرایه آیتمی قرار دارد پس به ازای آن یک عنصر todos.forEach(todo => { const li = this.createElement('li') li.id = todo.id const checkbox = this.createElement('input') checkbox.type = 'checkbox' checkbox.checked = todo.complete const span = this.createElement('span') span.contentEditable = true span.classList.add('editable') if (todo.complete) { const strike = this.createElement('s') strike.textContent = todo.text span.append(strike) } else { span.textContent = todo.text } const deleteButton = this.createElement('button', 'delete') deleteButton.textContent = 'Delete' li.append(checkbox, span, deleteButton) // نود ایجاد شده به لیست اضافه میکند this.todoList.append(li) }) } // برای خطایابی و نمایش در کنسول console.log(todos) }
در نهایت قسمت Controller را که پل ارتباطی بین View و Model میباشد، کامل میکنیم. اولین تغییراتی که در کلاس Controller ایجاد میکنیم، استفاده از متد displayTodos در سازندهی این کلاس میباشد و با هر بار تغییر این متد، دوباره فراخوانی میشود:
class Controller { constructor(model, view) { this.model = model this.view = view // نمایش اطلاعات پیش فرض this.onTodoListChanged(this.model.todos) } onTodoListChanged = todos => { this.view.displayTodos(todos) } }
چهار تابعی را که در قسمت Model ایجاد نمودیم و کار ویرایش، حذف، افزودن و اتمام کار را انجام میدادند، در کلاس کنترلر آنها را هندل میکنیم و زمانیکه کاربر دکمهای را برای افزودن یا تیک حذف آیتمی، زد، تابع مربوطه توسط کنترلر در Model فراخوانی شود:
handleAddTodo = todoText => { this.model.addTodo(todoText) } handleEditTodo = (id, todoText) => { this.model.editTodo(id, todoText) } handleDeleteTodo = id => { this.model.deleteTodo(id) } handleToggleTodo = id => { this.model.toggleTodo(id) }
چون کنترلر نمیتواند بصورت مستقیم فراخوانی شود و این توابع باید درون DOM تنظیم شوند تا به ازای رخدادهایی همچون click و change، فراخوانی شوند. پس از این توابع در قسمت View استفاده میکنیم و به کلاس View، موارد زیر را اضافه میکنیم:
bindAddTodo(handler) { this.form.addEventListener('submit', event => { event.preventDefault() if (this._todoText) { handler(this._todoText) this._resetInput() } }) } bindDeleteTodo(handler) { this.todoList.addEventListener('click', event => { if (event.target.className === 'delete') { const id = parseInt(event.target.parentElement.id) handler(id) } }) } bindToggleTodo(handler) { this.todoList.addEventListener('change', event => { if (event.target.type === 'checkbox') { const id = parseInt(event.target.parentElement.id) handler(id) } }) }
برای bind کردن این متدها در کلاس Controller، کدهای زیر را اضافه میکنیم:
this.view.bindAddTodo(this.handleAddTodo) this.view.bindDeleteTodo(this.handleDeleteTodo) this.view.bindToggleTodo(this.handleToggleTodo)
برای ذخیره اطلاعات در local storage، در سازنده کلاس Model، کد زیر را اضافه میکنیم:
this.todos = JSON.parse(localStorage.getItem('todos')) || []
متد دیگری هم در کلاس Model برای بهروز رسانی مقادیر local storage قرار میدهیم:
_commit(todos) { this.onTodoListChanged(todos) localStorage.setItem('todos', JSON.stringify(todos)) }
متدی هم برای تغییراتی که هر زمان بر روی todos اتفاق میافتد، فراخوانی شود:
deleteTodo(id) { this.todos = this.todos.filter(todo => todo.id !== id) this._commit(this.todos) }
این مقاله صرفا جهت آشنایی و نمونه کدی از پیاده سازی الگوی MVC در جاوااسکریپت میباشد.
این فرم ورک متشکل است از ترکیب Knockout, Sammy, and RequireJS تهیه شده توسط آیزنبرگ که احتمالا فریم ورک MVVM ایشون رو قبلا دیدید.
همانطور که قول داده بودم، به اصول GRASP میپردازیم.
اصول GRASP-General Responsibility Assignment
Software Principles
این اصول به بررسی نحوه تقسیم وظایف بین کلاسها و مشارکت اشیاء برای به انجام رساندن یک مسئولیت میپردازند. اینکه هر کلاس در ساختار نرم افزار چه وظیفهای دارد و چگونه با کلاسهای دیگر مشارکت میکند تا یک عملکرد به سیستم اضافه گردد. این اصول به چند بخش تقسیم میشوند:
- کنترلر ( Controller )
- ایجاد کننده ( Creator )
- انسجام قوی ( High Cohesion )
- واسطه گری ( Indirection )
- دانای اطلاعات ( Information Expert )
- اتصال ضعیف ( Low Coupling )
- چند ریختی ( Polymorphism )
- حفاظت از تاثیر تغییرات ( Protected Variations )
- مصنوع خالص ( Pure Fabrication )
Controller
این الگو بیان میکند که مسئولیت پاسخ به رویدادهای (Events ) یک سناریوی محدود مانند یک مورد کاربردی ( Use Case ) باید به عهده یک کلاس غیر UI باشد. کنترلر باید کارهایی را که نیاز است در پاسخ رویداد انجام شود، به دیگران بسپرد و نتایج را طبق درخواست رویداد بازگرداند. در اصل، کنترلر دریافت کننده رویداد، راهنمای مسیر پردازش برای پاسخ به رویداد و در نهایت برگرداننده پاسخ به سمت مبداء رویداد است. در زیر مثالی را میبینیم که رویداد اتفاق افتاده توسط واسط گرافیکی به سمت یک handler (که متدی است با ورودیِ فرستنده و آرگمانهای مورد نیاز) در کنترلر فرستاده میشود. این روش event handling، در نمونههای وب فرم و ویندوز فرم دیده میشود. به صورتی خود کلاسهای .Net وظیفه Event Raising از سمت UI با کلیک روی دکمه را انجام میدهد:
public class UserController { protected void OnClickCreate(object sender, EventArgs e) { // call validation services // call create user services } }
در مثال بعد عملیات مربوط به User در یک WebApiController پاسخ داده میشود. در اینجا به جای استفاده از Event Raising برای کنترل کردن رویداد، از فراخوانی یک متد در کنترلر توسط درخواست HttpPost انجام میگیرد. در اینجا نیاز است که در سمت کلاینت درخواستی را ارسال کنیم:
public class UserWebApiController { [HttpPost] public HttpResponseMessage Create(UserViewModel user) { // call validation services // call create user services } }
Creator :
این اصل میگوید شیء ای میتواند یک شیء دیگر را بسازد ( instantiate ) که: (اگر کلاس B بخواهد کلاس A را instantiate کند)
- کلاس B شیء از کلاس A را در خود داشته باشد؛
- یا اطلاعات کافی برای instantiate کردن از A را داشته باشد؛
- یا به صورت نزدیک با A در ارتباط باشد؛
- یا بخواهد شیء A را ذخیره کند.
از آنجایی که این اصل بدیهی به نظر میرسد، با مثال نقض، درک بهتری را نسبت به آن میتوان پیدا کرد:
// سازنده public class B { public static A CreateA(string name, string lastName, string job) { return new A() { Name =name, LastName = lastName, Job = job }; } } // ایجاد شونده public class A { public string Name { get; set; } public string LastName { get; set; } public string Job { get; set; } } public class Context { public void Main() { var name = "Rasoul"; var lastName = "Abbasi"; var job = "Developer"; var obj = B.CreateA(name, lastName, job); } }
و اما چرا این مثال، اصل Creator را نقض میکند. در مثال میبینید که کلاس B، یک شیء از نوع A را در متد Main کلاس Context ایجاد میکند. کلاس B فقط یک متد برای تولید A دارد و در عملیات تولید A هیچ منطق خاصی را پیاده سازی نمیکند.کلاس B شیء ای را از کلاس A ، در خود ندارد، با آن ارتباط نزدیک ندارد و آنرا ذخیره نمیکند. با اینکه کلاس B اطلاعات کافی را برای تولید A از ورودی میگیرد، ولی این کلاس Context است که اطلاعات کافی را ارسال مینماید. اگر در کلاس B منطقی اضافه بر instance گیریِ ساده وجود داشت (مانند بررسی صحت و اعتبار سنجی)، میتوانستیم بگوییم کلاس B از یک مجموعه عملیات instance گیری با خبر است که کلاس Context نباید از آن خبر داشته باشد. لذا اکنون هیچ دلیلی وجود ندارد که وظیفه تولید A را در Context انجام ندهیم و این مسئولیت را به کلاس B منتقل کنیم. این مورد ممکن است در ذهن شما با الگوی Factory تناقض داشته باشد. ولی نکته اصلی در الگو Factory انجام عملیات instance گیری با توجه به منطق برنامه است؛ یعنی وظیفهای که کلاس Context نباید از آن خبر داشته باشد را به کلاس Factory منتقل میکنیم. در غیر اینصورت ایجاد کلاس Factory بی معنا خواهد بود (مگر به عنوان افزایش انعطاف پذیری معماری که بتوان به راحتی نوع پیاده سازی یک واسط را تغییر داد).
High Cohesion :
این اصل اشاره به یکی از اصول اساسی طراحی نرم افزار دارد. انسجام واحدهای نرم افزاری باعث افزایش خوانایی، سهولت اشکال زدایی، قابلیت نگهداری و کاهش تاثیر زنجیرهای تغییرات میشود. طبق این اصل، مسئولیتهای هر واحد باید مرتبط باشد. لذا اجزایی کوچک با مسئولیتهای منسجم و متمرکز بهتر از اجزایی بزرگ با مسئولیتهای پراکنده است. اگر واحدهای سازنده نرم افزار انسجام ضعیفی داشته باشند، درک همکاریها، استفاده مجدد آنها، نگه داری نرم افزار و پاسخ به تغییرات سختتر خواهد شد.
در مثال زیر نقض این اصل را مشاهده میکنیم:
class Controller { public void CreateProduct(string name, int categoryId) { } public void EditProduct(int id, string name) { } public void DeleteProduct(int id) { } public void CreateCategory(string name) { } public void EditCategory(int id, string name) { } public void DeleteCategory(int id) { } }
همانطور که میبینید، کلاس
کنترلر ما، مسئولیت مدیریت Product و Category را بر عهده دارد. بزرگ شدن این کلاس، باعث سختتر شدن
خواندن کد و رفع اشکال میگردد. با جداسازی کنترلر مربوط به Product از Category میتوان انسجام را بالا برد.
Indirection :
این اصل بیان میکند که با تعریف یک واسط بین دو مولفه نرم افزاری میتوان میزان اتصال نرم افزار را کاهش داد. بدین ترتیب وظیفه هماهنگی ارتباط دو مؤلفه، به عهده این واسط خواهد بود و نیازی نیست دادههای ورودی و خروجی دو مؤلفه، هماهنگ باشند. در اینجا واسط، از وابستگی بین دو مؤلفه با پنهان کردن ضوابط هر مؤلفه از دیگری و ایجاد وابستگی ضعیف خود با دو مؤلفه، باعث کاهش اتصال کلی طراحی میگردد.
الگوهای Adapter و Delegate و همچنین نقش کنترلر در الگوی معماری MVC از این اصل پیروی میکنند.
class SenderA { public Mediator mediator { get; } public SenderA() { mediator = new Mediator(); } public void Send(string message, string reciever) { mediator.Send(message, reciever); } } class SenderB { public Mediator mediator { get; } public SenderB() { mediator = new Mediator(); } public void Send(string message) { } } public class RecieverA { public void DoAction(string message) { // انجام عملیات بر اساس پیغام دریافت شده switch (message) { case "create": break; case "delete": break; default: break; } } } public class RecieverB { public void DoAction(string message) { // انجام عملیات بر اساس پیغام دریافت شده switch (message) { case "edit": break; case "rollback": break; default: break; } } } class Mediator { internal void Send(string message, string reciever) { switch (reciever) { case "A": var recieverObjA = new RecieverA(); recieverObjA.DoAction(message); break; case "B": var recieverObjB = new RecieverB(); recieverObjB.DoAction(message); break; default: break; } } } class IndirectionContext { public void Main() { var senderA = new SenderA(); senderA.Send("rollback", "B"); var senderB = new SenderA(); senderB.Send("create", "A"); } }
در این مثال کلاس Mediator به عنوان واسط ارتباطی بین کلاسهای Sender و Receiver قرار گرفته و نقش تحویل پیغام را دارد.
در مقاله بعدی، به بررسی سایر اصول GRASP خواهم پرداخت.
مدیریت بهینهی سشن فکتوری
ساخت یک شیء SessionFactory بسیار پر هزینه و زمانبر است. به همین جهت لازم است که این شیء یکبار حین آغاز برنامه ایجاد شده و سپس در پایان کار برنامه تخریب شود. انجام اینکار در برنامههای معمولی ویندوزی (WinForms ،WPF و ...)، ساده است اما در محیط Stateless وب و برنامههای ASP.Net ، نیاز به راه حلی ویژه وجود خواهد داشت و تمرکز اصلی این مقاله حول مدیریت صحیح سشن فکتوری در برنامههای ASP.Net است.
برای پیاده سازی شیء سشن فکتوری به صورتی که یکبار در طول برنامه ایجاد شود و بارها مورد استفاده قرار گیرد باید از یکی از الگوهای معروف طراحی برنامه نویسی شیء گرا به نام Singleton Pattern استفاده کرد. پیاده سازی نمونهی thread safe آن که در برنامههای ذاتا چند ریسمانی وب و همچنین برنامههای معمولی ویندوزی میتواند مورد استفاده قرار گیرد، در آدرس ذیل قابل مشاهده است:
از پنجمین روش ذکر شده در این مقاله جهت ایجاد یک lazy, lock-free, thread-safe singleton استفاده خواهیم کرد.
بررسی مدل برنامه
در این مدل ساده ما یک یا چند پارکینگ داریم که در هر پارکینگ یک یا چند خودرو میتوانند پارک شوند.
یک برنامه ASP.Net را آغاز کرده و ارجاعاتی را به اسمبلیهای زیر به آن اضافه نمائید:
FluentNHibernate.dll
NHibernate.dll
NHibernate.ByteCode.Castle.dll
NHibernate.Linq.dll
و همچنین ارجاعی به اسمبلی استاندارد System.Data.Services.dll دات نت فریم ورک سه و نیم
تصویر نهایی پروژه ما به شکل زیر خواهد بود:
پروژه ما دارای یک پوشه domain ، تعریف کننده موجودیتهای برنامه جهت تهیه نگاشتهای لازم از روی آنها است. سپس یک پوشه جدید را به نام NHSessionManager به آن جهت ایجاد یک Http module مدیریت کننده سشنهای NHibernate در برنامه اضافه خواهیم کرد.
ساختار دومین برنامه (مطابق کلاس دیاگرام فوق):
namespace NHSample3.Domain
{
public class Car
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual string Color { get; set; }
}
}
using System.Collections.Generic;
namespace NHSample3.Domain
{
public class Parking
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual string Location { get; set; }
public virtual IList<Car> Cars { get; set; }
public Parking()
{
Cars = new List<Car>();
}
}
}
در این قسمت قصد داریم Http Module ایی را جهت مدیریت سشنهای NHibernate ایجاد نمائیم.
در ابتدا کلاس Config را در پوشه مدیریت سشن NHibernate با محتویات زیر ایجاد کنید:
using FluentNHibernate.Automapping;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHibernate.Tool.hbm2ddl;
namespace NHSessionManager
{
public class Config
{
public static FluentConfiguration GetConfig()
{
return
Fluently.Configure()
.Database(
MsSqlConfiguration
.MsSql2008
.ConnectionString(x => x.FromConnectionStringWithKey("DbConnectionString"))
)
.ExposeConfiguration(
x => x.SetProperty("current_session_context_class", "managed_web")
)
.Mappings(
m => m.AutoMappings.Add(
new AutoPersistenceModel()
.Where(x => x.Namespace.EndsWith("Domain"))
.AddEntityAssembly(typeof(NHSample3.Domain.Car).Assembly))
);
}
public static void CreateDb()
{
bool script = false;//آیا خروجی در کنسول هم نمایش داده شود
bool export = true;//آیا بر روی دیتابیس هم اجرا شود
bool dropTables = false;//آیا جداول موجود دراپ شوند
new SchemaExport(GetConfig().BuildConfiguration()).Execute(script, export, dropTables);
}
}
}
دو نکته جدید در متد GetConfig وجود دارد:
الف) استفاده از متد FromConnectionStringWithKey ، بجای تعریف مستقیم کانکشن استرینگ در متد مذکور که روشی است توصیه شده. به این صورت فایل وب کانفیگ ما باید دارای تعریف کلید مشخص شده در متد GetConfig به نام DbConnectionString باشد:
<connectionStrings>
<!--NHSessionManager-->
<add name="DbConnectionString"
connectionString="Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true" />
</connectionStrings>
در اینجا به AutoMapper خواهیم گفت که قصد داریم از امکانات مدیریت سشن مخصوص وب فریم ورک NHibernate استفاده کنیم. فریم ورک NHibernate دارای کلاسی است به نام NHibernate.Context.ManagedWebSessionContext که جهت مدیریت سشنهای خود در پروژههای وب ASP.Net پیش بینی کرده است و از این متد در Http module ایی که ایجاد خواهیم کرد جهت ردگیری سشن جاری آن کمک خواهیم گرفت.
اگر متد CreateDb را فراخوانی کنیم، جداول نگاشت شده به کلاسهای پوشه دومین برنامه، به صورت خودکار ایجاد خواهند شد که دیتابیس دیاگرام آن به صورت زیر میباشد:
سپس کلاس SingletonCore را جهت تهیه تنها و تنها یک وهله از شیء سشن فکتوری در کل برنامه ایجاد خواهیم کرد (همانطور که عنوان شده، ایده پیاده سازی این کلاس thread safe ، از مقاله معرفی شده در ابتدای بحث گرفته شده است):
using NHibernate;
namespace NHSessionManager
{
/// <summary>
/// lazy, lock-free, thread-safe singleton
/// </summary>
public class SingletonCore
{
private readonly ISessionFactory _sessionFactory;
SingletonCore()
{
_sessionFactory = Config.GetConfig().BuildSessionFactory();
}
public static SingletonCore Instance
{
get
{
return Nested.instance;
}
}
public static ISession GetCurrentSession()
{
return Instance._sessionFactory.GetCurrentSession();
}
public static ISessionFactory SessionFactory
{
get { return Instance._sessionFactory; }
}
class Nested
{
// Explicit static constructor to tell C# compiler
// not to mark type as beforefieldinit
static Nested()
{
}
internal static readonly SingletonCore instance = new SingletonCore();
}
}
}
using System;
using System.Web;
using NHibernate;
using NHibernate.Context;
namespace NHSessionManager
{
public class SessionModule : IHttpModule
{
public void Dispose()
{ }
public void Init(HttpApplication context)
{
if (context == null)
throw new ArgumentNullException("context");
context.BeginRequest += Application_BeginRequest;
context.EndRequest += Application_EndRequest;
}
private void Application_BeginRequest(object sender, EventArgs e)
{
ISession session = SingletonCore.SessionFactory.OpenSession();
ManagedWebSessionContext.Bind(HttpContext.Current, session);
session.BeginTransaction();
}
private void Application_EndRequest(object sender, EventArgs e)
{
ISession session = ManagedWebSessionContext.Unbind(
HttpContext.Current, SingletonCore.SessionFactory);
if (session == null) return;
try
{
if (session.Transaction != null &&
!session.Transaction.WasCommitted &&
!session.Transaction.WasRolledBack)
{
session.Transaction.Commit();
}
else
{
session.Flush();
}
}
catch (Exception)
{
session.Transaction.Rollback();
}
finally
{
if (session != null && session.IsOpen)
{
session.Close();
session.Dispose();
}
}
}
}
}
در متد Application_BeginRequest ، در ابتدای هر درخواست یک سشن جدید ایجاد و به مدیریت سشن وب NHibernate بایند میشود، همچنین یک تراکنش نیز آغاز میگردد. سپس در پایان درخواست، این انقیاد فسخ شده و تراکنش کامل میشود، همچنین کار پاکسازی اشیاء نیز صورت خواهد گرفت.
با توجه به این موارد، دیگر نیازی به ذکر using جهت dispose کردن سشن جاری در کدهای ما نخواهد بود، زیرا در پایان هر درخواست اینکار به صورت خودکار صورت میگیرد. همچنین نیازی به ذکر تراکنش نیز نمیباشد، چون مدیریت آنرا خودکار کردهایم.
جهت استفاده از این Http module تهیه شده باید چند سطر زیر را به وب کانفیگ برنامه اضافه کرد:
<httpModules>
<!--NHSessionManager-->
<add name="SessionModule" type="NHSessionManager.SessionModule"/>
</httpModules>
اکنون مثالی از نحوهی استفاده از امکانات فراهم شده فوق به صورت زیر میتواند باشد:
ابتدا کلاس ParkingContext را جهت مدیریت مطلوبتر LINQ to NHibernate تشکیل میدهیم.
using System.Linq;
using NHibernate;
using NHibernate.Linq;
using NHSample3.Domain;
namespace NHSample3
{
public class ParkingContext : NHibernateContext
{
public ParkingContext(ISession session)
: base(session)
{ }
public IOrderedQueryable<Car> Cars
{
get { return Session.Linq<Car>(); }
}
public IOrderedQueryable<Parking> Parkings
{
get { return Session.Linq<Parking>(); }
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using NHibernate;
using NHSample3.Domain;
using NHSessionManager;
namespace NHSample3
{
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
//ایجاد دیتابیس در صورت نیاز
//Config.CreateDb();
//ثبت یک سری رکورد در دیتابیس
ISession session = SingletonCore.GetCurrentSession();
Car car1 = new Car() { Name = "رنو", Color = "مشکلی" };
session.Save(car1);
Car car2 = new Car() { Name = "پژو", Color = "سفید" };
session.Save(car2);
Parking parking1 = new Parking()
{
Location = "آدرس پارکینگ مورد نظر",
Name = "پارکینگ یک",
Cars = new List<Car> { car1, car2 }
};
session.Save(parking1);
//نمایش حاصل در یک گرید ویوو
ParkingContext db = new ParkingContext(session);
var query = from x in db.Cars select new { CarName = x.Name, CarColor = x.Color };
GridView1.DataSource = query.ToList();
GridView1.DataBind();
}
}
}
در برنامههای ویندوزی مانند WinForms ، WPF و غیره، تا زمانیکه یک فرم باز باشد، کل فرم و اشیاء مرتبط با آن به یکباره تخریب نخواهند شد، اما در یک برنامه ASP.Net جهت حفظ منابع سرور در یک محیط چند کاربره، پس از پایان نمایش یک صفحه وب، اثری از آثار اشیاء تعریف شده در کدهای آن صفحه در سرور وجود نداشته و همگی بلافاصله تخریب میشوند. به همین جهت بحثهای ویژه state management در ASP.Net در اینباره مطرح است و مدیریت ویژهای باید روی آن صورت گیرد که در قسمت قبل مطرح شد.
از بحث فوق، تنها استفاده از کلاسهای Config و SingletonCore ، جهت استفاده و مدیریت بهینهی سشن فکتوری در برنامههای ویندوزی کفایت میکنند.
دریافت سورس برنامه قسمت هفتم
ادامه دارد ....
معرفی WPF Extended toolkit
پیشنیازهای کار با کامپوننت ng2-file-upload
برای شروع به کار با کامپوننت ng2-file-upload، ابتدا نیاز است بستهی npm آنرا نصب کرد:
>npm install ng2-file-upload --save
همچنین یک کامپوننت آزمایشی را هم به برنامه (دقیقا همان مثال مطلب قبلی) جهت اعمال آن اضافه میکنیم:
>ng g c UploadFile/ng2-file-upload-test
پس از آن نیاز است به ماژولی که این کامپوننت جدید در آن قرار دارد، مدخل FileUploadModule کامپوننت ng2-file-upload را افزود:
import { FileUploadModule } from "ng2-file-upload"; @NgModule({ imports: [ FileUploadModule ]
تکمیل Ng2FileUploadTestComponent جهت اعمال ng2-file-upload
اکنون به کلاس کامپوننت جدیدی که ایجاد کردیم، مراجعه کرده و تغییرات ذیل را اعمال میکنیم:
import { FileUploader, FileUploaderOptions } from "ng2-file-upload"; import { Ticket } from "./../ticket"; export class Ng2FileUploadTestComponent implements OnInit { fileUploader: FileUploader; model = new Ticket();
وهله سازی از کامپوننت ng2-file-upload و انجام تنظیمات اولیهی آن
پس از تعریف خاصیت عمومی fileUploader، اکنون نوبت به وهله سازی آن است:
this.fileUploader = new FileUploader( <FileUploaderOptions>{ url: "api/SimpleUpload/SaveTicket", headers: [ { name: "X-XSRF-TOKEN", value: this.getCookie("XSRF-TOKEN") }, { name: "Accept", value: "application/json" } ], isHTML5: true, // allowedMimeType: ["image/jpeg", "image/png", "application/pdf", "application/msword", "application/zip"] allowedFileType: [ "application", "image", "video", "audio", "pdf", "compress", "doc", "xls", "ppt" ], removeAfterUpload: true, autoUpload: false, maxFileSize: 10 * 1024 * 1024 } );
- اگر برنامه از نکات anti-forgery token استفاده میکند، این کامپوننت برخلاف روش مطرح شدهی در مطلب مشابه قبلی، هیچ هدری را به سمت سرور ارسال نمیکند. بنابراین نیاز است کوکی مرتبط را خودمان یافته و سپس به لیست هدرها اضافه کنیم. در اینجا روش استخراج یک کوکی را توسط کدهای جاوا اسکریپتی مشاهده میکنید:
getCookie(name: string): string { const value = "; " + document.cookie; const parts = value.split("; " + name + "="); if (parts.length === 2) { return decodeURIComponent(parts.pop().split(";").shift()); } }
- برای محدود سازی فایلهای ارسالی توسط این کامپوننت، دو روش وجود دارد:
الف) مشخص سازی مقدار خاصیت allowedMimeType
همانطور که مشاهده میکنید، در اینجا باید mime type فایلهای مجاز را مشخص کرد.
ب) مشخص سازی مقدار خاصیت allowedFileType
برخلاف تصور، در اینجا از پسوند فایلها استفاده نمیکند و از یک لیست از پیش مشخص که نمونهای از آنرا در اینجا مشاهده میکنید، کمک گرفته میشود. بنابراین اگر برای مثال تنها نیاز به ارسال تصاویر بود، مقدار image را نگه داشته و مابقی را از لیست حذف کنید.
- removeAfterUpload به این معنا است که آیا لیست نهایی که نمایش داده میشود، پس از آپلود باقی بماند یا خیر؟
- توسط خاصیت maxFileSize میتوان حداکثر اندازهی قابل قبول فایلهای ارسالی را مشخص کرد.
مدیریت رخدادهای کامپوننت ng2-file-upload
اکنون که وهلهای از این کامپوننت ساخته شدهاست، میتوان رخدادهای آنرا نیز مدیریت کرد. برای مثال:
الف) نحوهی ارسال اطلاعات اضافی به همراه یک فایل به سمت سرور
this.fileUploader.onBuildItemForm = (fileItem, form) => { for (const key in this.model) { if (this.model.hasOwnProperty(key)) { form.append(key, this.model[key]); } } };
ب) اطلاع یافتن از رخداد خاتمهی کار
رخداد onCompleteAll پس از ارسال تمام فایلها به سمت سرور فراخوانی میشود:
this.fileUploader.onCompleteAll = () => { // clear the form // this.model = new Ticket(); };
ج) در حین وهله سازی fileUploader، تعدادی محدودیت نیز قابل اعمال هستند. این محدودیتها سبب نمایش هیچگونه پیام خطایی نمیشوند. فقط زمانیکه کاربر فایلی را انتخاب میکند، این فایل در لیست ظاهر نمیشود. اگر علاقمند به مدیریت این وضعیت باشید، میتوان از رخداد onWhenAddingFileFailed استفاده کرد:
this.fileUploader.onWhenAddingFileFailed = (item, filter, options) => { // msg: `You can't select ${item.name} file because of the ${filter.name} filter.` };
د) اگر ارسال فایلی به سمت سرور با شکست مواجه شود، در رخدادگردان onErrorItem میتوان به نام این فایل و اطلاعات بیشتری که از سمت سرور دریافت شدهاست، دسترسی یافت:
this.fileUploader.onErrorItem = (fileItem, response, status, headers) => { // };
ه) اگر از سمت سرور اطلاعات JSON مانندی یا هر اطلاعات دیگری به سمت کلاینت پس از آپلود ارسال میشود، این اطلاعات را میتوان در رخدادگردان onSuccessItem دریافت کرد:
this.fileUploader.onSuccessItem = (item, response, status, headers) => { if (response) { const ticket = JSON.parse(response); console.log(`ticket:`, ticket); } };
ارسال نهایی فرم و فایلها به سمت سرور
در پایان، با فراخوانی متد uploadAll شیء fileUploader جاری، میتوان اطلاعات فرم و تمام فایلهای آنرا به سمت سرور ارسال کرد:
submitForm(form: NgForm) { this.fileUploader.uploadAll(); // NOTE: Upload multiple files in one request -> https://github.com/valor-software/ng2-file-upload/issues/671 }
کدهای کامل کامپوننت ng2-file-upload-test.component.ts را در اینجا میتوانید مشاهده کنید.
تکمیل قالب کامپوننت Ng2FileUploadTestComponent
اکنون که کار تکمیل کامپوننت آزمایشی ارسال فایلها به سمت سرور به پایان رسید، نوبت به تکمیل قالب آن است.
افزودن فیلد اضافی توضیحات به فرم
<div class="container"> <h3>Support Form(ng2-file-upload)</h3> <form #form="ngForm" (submit)="submitForm(form)" novalidate> <div class="form-group" [class.has-error]="description.invalid && description.touched"> <label class="control-label">Description</label> <input #description="ngModel" required type="text" class="form-control" name="description" [(ngModel)]="model.description"> <div *ngIf="description.invalid && description.touched"> <div class="alert alert-danger" *ngIf="description.errors.required"> description is required. </div> </div> </div>
تعریف ویژهی فیلد ارسال فایلها به سمت سرور
<div class="form-group"> <label class="control-label">Screenshot(s)</label> <input required type="file" multiple ng2FileSelect [uploader]="fileUploader" class="form-control" name="screenshot"> </div>
ذکر ویژگی استاندارد multiple را نیز در اینجا مشاهده میکنید. وجود آن سبب خواهد شد تا کاربر بتواند چندین فایل را با هم انتخاب کند. اگر نیازی به ارسال چندین فایل نیست، این ویژگی را حذف کنید.
نمایش لیست فایلها و نمایش درصد پیشرفت آپلود آنها
جدولی را که در تصویر ابتدای بحث مشاهده کردید، به صورت ذیل شکل میگیرد (کدهای آن در همان صفحهی توضیحات کامپوننت نیز موجود هستند):
<div style="margin-bottom: 10px" *ngIf="fileUploader.queue.length"> <h3>Upload queue</h3> <p>Queue length: {{ fileUploader?.queue?.length }}</p> <table class="table"> <thead> <tr> <th width="50%">Name</th> <th>Size</th> <th>Progress</th> <th>Status</th> <th>Actions</th> </tr> </thead> <tbody> <tr *ngFor="let item of fileUploader.queue"> <td><strong>{{ item?.file?.name }}</strong></td> <td nowrap>{{ item?.file?.size/1024/1024 | number:'.2' }} MB</td> <td> <div class="progress" style="margin-bottom: 0;"> <div class="progress-bar" role="progressbar" [ngStyle]="{ 'width': item.progress + '%' }"></div> </div> </td> <td class="text-center"> <span *ngIf="item.isError"><i class="glyphicon glyphicon-remove"></i></span> </td> <td nowrap> <button type="button" class="btn btn-danger btn-xs" (click)="item.remove()"> <span class="glyphicon glyphicon-trash"></span> Remove </button> </td> </tr> </tbody> </table> <div> <div> Queue progress: <div class="progress"> <div class="progress-bar" role="progressbar" [ngStyle]="{ 'width': fileUploader.progress + '%' }"></div> </div> </div> <button type="button" class="btn btn-danger btn-s" (click)="fileUploader.clearQueue()" [disabled]="!fileUploader.queue.length"> <span class="glyphicon glyphicon-trash"></span> Remove all </button> </div> </div>
در اینجا از progress-bar بوت استرپ برای نمایش درصد آپلود فایلها استفاده شدهاست:
<div class="progress" style="margin-bottom: 0;"> <div class="progress-bar" role="progressbar" [ngStyle]="{ 'width': item.progress + '%' }"></div> </div>
غیرفعال کردن دکمهی ارسال، در صورت عدم انتخاب یک فایل
<button class="btn btn-primary" [disabled]="form.invalid || !fileUploader.queue.length" type="submit">Submit</button> </form>
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
در این قسمت، یک مثال ساده، بدون استفاده از فریم ورکهای متداول M-V-VM بررسی شده است. در قسمتهای بعدی با یک سری از فریم ورکهای موجود آشنا خواهیم شد.
فهرست مطالب:
فصل 2- معرفی مثالی مقدماتی از پیاده سازی الگوی M-V-VM در WPF
- مقدمه
- ساختار پوشههای یک برنامهی MVVM
- معرفی برنامهی فصل
- مدل برنامه
- View برنامه
- ViewModel برنامه
دریافت قسمت دوم
دریافت مثال قسمت دوم
نمایش پیامها و اخطارهای یک برنامهی Angular توسط ng2-toasty
در مطلب «ایجاد Drop Down Listهای آبشاری در Angular» در قسمت دریافت اطلاعات drop down دوم از سرور، اگر کاربر مجددا گروه را بر روی حالت «لطفا گروهی را انتخاب کنید ...» قرار دهد، مقدار categoryId به undefined تغییر میکند:
fetchProducts(categoryId?: number) { console.log(categoryId); this.products = []; if (categoryId === undefined || categoryId.toString() === "undefined") { return; }
پیشنیازهای کار با کامپوننت Angular2 Toasty توسط یک برنامهی Angular CLI
برای کار با کامپوننت Angular2 Toasty، ابتدا از طریق خط فرمان به پوشهی ریشهی برنامه وارد شده و سپس دستور ذیل را صادر میکنیم:
> npm install ng2-toasty --save
یک نکته: اگر در حین اجرای این دستور به خطای ذیل برخوردید:
npm ERR! Error: EPERM: operation not permitted, rename
پس از آن نیاز است یکی از شیوهنامههایی را که در تصویر فوق ملاحظه میکنید، در فایل angular-cli.json. مشخص کنیم:
"styles": [ "../node_modules/bootstrap/dist/css/bootstrap.min.css", "../node_modules/ng2-toasty/bundles/style-bootstrap.css", "styles.css" ],
سپس باید به فایل src\app\app.module.ts مراجعه کرد و ماژول این کامپوننت را معرفی نمود:
import { ToastyModule } from "ng2-toasty"; @NgModule({ imports: [ BrowserModule, ToastyModule.forRoot(),
همچنین در همین قسمت، به فایل قالب src\app\app.component.html مراجعه کرده و selector tag این کامپوننت را در ابتدای آن تعریف میکنیم:
<ng2-toasty [position]="'top-right'"></ng2-toasty>
نمایش یک پیام خطا توسط ToastyService
اکنون که کار برپایی کامپوننت Angular2 Toasty به پایان رسید، کار کردن با آن به سادگی تزریق سرویس آن به سازندهی یک کامپوننت و فراخوانی متدهای info، success ، wait ، error و warning آن است:
import { ToastyService, ToastOptions } from "ng2-toasty"; export class ProductGroupComponent implements OnInit { constructor( private productItemsService: ProductItemsService, private toastyService: ToastyService) { } fetchProducts(categoryId?: number) { console.log(categoryId); this.products = []; if (categoryId === undefined || categoryId.toString() === "undefined") { this.toastyService.error(<ToastOptions>{ title: "Error!", msg: "Please select a category.", theme: "bootstrap", showClose: true, timeout: 5000 }); return; }
- سپس ToastyService به سازندهی کلاس کامپوننت مدنظر تزریق شدهاست تا بتوان از امکانات آن استفاده کرد.
- در ادامه، فراخوانی متد this.toastyService.error سبب نمایش اخطار قرمز رنگی میشود که تصویر آنرا در ابتدای مطلب جاری مشاهده کردید.
- علت ذکر <ToastOptions> در اینجا این است که وجود آن سبب خواهد شد تا intellisense در VSCode فعال شود و پس از آن بتوان تمام گزینههای این متد و تنظیمات را بدون مراجعهی به مستندات آن از طریق intellisense یافت و درج کرد:
مدیریت سراسری خطاهای مدیریت نشده، در یک برنامهی Angular
در برنامههای Angular از این دست کدها بسیار مشاهده میشوند:
this.productItemsService.getCategories().subscribe( data => { this.categories = data; }, err => console.log("get error: ", err) );
به همین منظور کلاس جدیدی را به صورت ذیل در پوشهی src\app اضافه میکنیم:
> ng g cl app.error-handler
installing class create src\app\app.error-handler.ts
import { ErrorHandler } from "@angular/core"; export class AppErrorHandler implements ErrorHandler { handleError(error: any): void { console.log("Error:", error); } }
اکنون نیاز است این ErrorHandler سفارشی را بجای نمونهی اصلی به برنامه معرفی کنیم. برای این منظور به فایل src\app\app.module.ts مراجعه کرده و تغییرات ذیل را اعمال میکنیم:
import { NgModule, ErrorHandler } from "@angular/core"; import { AppErrorHandler } from "./app.error-handler"; @NgModule({ providers: [ { provide: ErrorHandler, useClass: AppErrorHandler } ]
اکنون برای آزمایش آن، در کدهای سمت سرور مطلب «ایجاد Drop Down Listهای آبشاری در Angular»، یک استثنای عمدی را قرار میدهیم:
[HttpGet("[action]/{categoryId:int}")] public async Task<IActionResult> GetProducts(int categoryId) { throw new Exception();
برای اینکه AppErrorHandler، مورد استفاده قرار گیرد، قسمت err دریافت لیست محصولات را نیز حذف میکنیم (تا تبدیل به یک استثنای مدیریت نشده شود):
this.productItemsService.getProducts(categoryId).subscribe( data => { this.products = data; this.isLoadingProducts = false; }// , // err => { // console.log("get error: ", err); // this.isLoadingProducts = false; // } );
افزودن ToastyService به AppErrorHandler
در ادامه میخواهیم بجای console.log از ToastyService برای نمایش خطاهای مدیریت نشدهی برنامه در کلاس AppErrorHandler استفاده کنیم:
import { ToastyService, ToastOptions } from "ng2-toasty"; import { ErrorHandler } from "@angular/core"; export class AppErrorHandler implements ErrorHandler { constructor(private toastyService: ToastyService) { } handleError(error: any): void { // console.log("Error:", error); this.toastyService.error(<ToastOptions>{ title: "Error!", msg: "Fatal error!", theme: "bootstrap", showClose: true, timeout: 5000 }); } }
مشکل اول! اکنون اگر برنامه را اجرا کنیم، در کنسول developer tools چنین خطایی ظاهر میشود:
Uncaught Error: Can't resolve all parameters for AppErrorHandler: (?).
import { ErrorHandler, Inject } from "@angular/core"; export class AppErrorHandler implements ErrorHandler { constructor( @Inject(ToastyService) private toastyService: ToastyService ) { }
مشکل دوم! اینبار برنامه را اجرا کنید. سپس گروهی را انتخاب نمائید. مشاهده میکنید که خطایی نمایش داده نشد؛ هرچند در کنسول developer tools میتوان اثری از آن را مشاهده کرد. مجددا گروه دیگری را انتخاب کنید، در این بار دوم است که خطای ارائه شدهی توسط this.toastyService.error ظاهر میشود. توضیح آن نیاز به بررسی مفهومی به نام Zones در Angular دارد.
مفهوم Zones در Angular
زمانیکه متد this.toastyService.error در یک کامپوننت برنامه مورد استفاده قرار گرفت، به خوبی کار میکرد و در همان بار اول فراخوانی، پیام را نمایش میداد. اما با انتقال آن به کلاسAppErrorHandler ، این قابلیت از کار افتاد. علت اینجا است که زمینهی اجرایی این قطعه کد، اکنون خارج از Zone یا ناحیهی Angular است و به همین دلیل متوجه تغییرات آن نمیشود. Zone زمینهی اجرایی اعمال async است و اگر به فایل package.json یک برنامهی Angular دقت کنید، بستهی zone.js، یکی از وابستگیهای همراه آن است.
تغییرات حالت برنامه، توسط یکی از اعمال ذیل رخ میدهند:
الف) بروز رخدادهایی مانند کلیک، ورود اطلاعات و یا ارسال فرم
ب) اعمال Ajax ایی
ج) استفاده از Timers مانند استفاده از setTimeout و setInterval
هر سه مورد یاد شده از نوع async بوده و زمانیکه رخ میدهند، حالت برنامه را تغییر خواهند داد. Angular نیز تنها به این موارد علاقمند بوده و به آنها در جهت به روز رسانی رابط کاربری برنامه واکنش نشان میدهد.
برای مثال this.toastyService.error دارای خاصیتی است به نام timeout: 5000 که در آن، مورد «ج» فوق رخ میدهد؛ یعنی یک Timer پس از 5 ثانیه سبب بسته شدن آن خواهد شد. به همین جهت است که اگر پیش از پایان این 5 ثانیه مجددا درخواست واکشی لیست محصولات یک گروه را بدهیم، خطای مربوطه مشاهده میشود. چون Angular زمینهی اجرایی لازم را فراهم کرده (یا همان Zone در اینجا) و مجبور به واکنش به عملیات async از نوع Timer است.
برای دسترسی به امکانات کتابخانهی zone.js، میتوان از طریق تزریق سرویس آن به نام NgZone به سازندهی کلاس شروع کرد:
import { ToastyService, ToastOptions } from "ng2-toasty"; import { ErrorHandler, Inject, NgZone } from "@angular/core"; import { LocationStrategy, PathLocationStrategy } from "@angular/common"; export class AppErrorHandler implements ErrorHandler { constructor( @Inject(NgZone) private ngZone: NgZone, @Inject(ToastyService) private toastyService: ToastyService, @Inject(LocationStrategy) private locationProvider: LocationStrategy ) { } handleError(error: any): void { // console.log("Error:", error); const url = this.locationProvider instanceof PathLocationStrategy ? this.locationProvider.path() : ""; const message = error.message ? error.message : error.toString(); this.ngZone.run(() => { this.toastyService.error(<ToastOptions>{ title: "Error!", msg: `URL:${url} \n ERROR:${message}`, theme: "bootstrap", showClose: true, timeout: 5000 }); }); // IMPORTANT: Rethrow the error otherwise it gets swallowed // throw error; } }
چند نکته
1- اگر میخواهید علاوه بر رخدادگردانی سراسری خطاها، این خطاها را به محل اصلی آنها نیز انتشار دهید، نیاز است سطر throw error را در انتهای متد handleError نیز ذکر کنید. در غیر اینصورت، کار در همینجا به پایان خواهد رسید و این خطاها دیگر منتشر نمیشوند.
2- روش دریافت URL جاری صفحه را نیز در اینجا مشاهده میکنید. این اطلاعات میتوانند جهت ارسال به سرور برای ثبت و بررسیهای بعدی مفید باشند.
3- مقدار new Error().stack معادل stack trace جاری است و تقریبا در تمام مرورگرهای جدید پشتیبانی میشود.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-template-driven-forms-lab-07.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس به ریشهی پروژه وارد شده و دو پنجرهی کنسول مجزا را باز کنید. در اولی دستورات
>npm install >ng build --watch
>dotnet restore >dotnet watch run
HttpHandler
public class MyHttpHandler : IHttpHandler { public void ProcessRequest(HttpContext context) { } public bool IsReusable { get { return false; } } }
public class MyHttpHandler : IHttpHandler { public void ProcessRequest(HttpContext context) { HttpResponse response = context.Response; HttpRequest request = context.Request; response.Write("Every Page has a some text like this"); } public bool IsReusable { get { return false; } } }
<system.web> <httpHandlers> <add verb="*" path="*.aspx" type="MyHttpHandler"/> </httpHandlers> </system.web>
<configuration> <system.web> <httpHandlers> <add name="myhttphandler" verb="*" path="*.aspx" type="MyHttpHandler"/> </httpHandlers> </system.web> </configuration>
گزینه Type که نام کلاس میباشد و اگر کلاس داخل یک فضای نام قرار گرفته باشد، باید اینطور نوشت : namespace.ClassName |
گزینه verb شامل مقادیری چون Get,Post,Head,Putو Delete میباشد و httphandler را فقط برای این نوع درخواستها اجرا میکند و در صورتیکه بخواهید چندتا از آنها را استفاده کنید، با , از هم جدا میشوند. مثلا Get,post و درصورتیکه همهی گزینهها را بخواهید علامت * را میتوان استفاده کرد. |
گزینهی path این امکان را به شما میدهد که مسیر و نوع فایلهایی را که قصد دارید روی آنها فقط اجرا شود، مشخص کنید و ما در قطعه کد بالا گفتهایم که تنها روی فایلهایی با پسوند aspx اجرا شود و چون مسیری هم ذکر نکردیم برای همهی مسیرها قابل اجراست. یکی از مزیتهای دادن پسوند این است که میتوانید پسوندهای اختصاصی داشته باشید. مثلا پسوند RSS برای فیدهای وب سایتتان. بسیاری از برنامه نویسان به جای استفاده از صفحات aspx از ashx استفاده میکنند که به مراتب سبکتر از aspx هست و شامل بخش ui نمیشود و نتیجه خروجی آن بر اساس کدی که مینویسید مشخص میشود که میتواند صفحه متنی یا عکس یا xml یا ... باشد. در اینجا در مورد ساخت صفحات ashx توضیح داده شده است. |
public class MyHttpHandler1 :IHttpHandler { public void ProcessRequest(HttpContext context) { HttpResponse response = context.Response; response.Write("this is httphandler1"); } public bool IsReusable { get { return false; } } } public class MyHttpHandler2 : IHttpHandler { public void ProcessRequest(HttpContext context) { HttpResponse response = context.Response; response.Write("this is httphandler2"); } public bool IsReusable { get { return false; } } }
public class MyFactory : IHttpHandlerFactory { public IHttpHandler GetHandler(HttpContext context, string requestType, string url, string pathTrasnlated) { } public void ReleaseHandler(IHttpHandler handler) { } }
Context | یک شی از کلاس httpcontext که دسترسی ما را برای اشیاء سروری چون response,request,session و... فراهم میکند. |
RequestType | مشخص میکند که درخواست صفحه به چه صورتی است. این گزینه برای مواردی است که verb بیش از یک مورد را حمایت میکند. برای مثال دوست دارید یک هندلر را برای درخواستهای Get ارسال کنید و هندلر دیگر را برای درخواستهای نوع Post |
URL | مسیر مجازی virtual Path صفحه صدا زده شده |
PathTranslated | مسیر فیزیکی صفحه درخواست کننده را ارسال میکند. |
public IHttpHandler GetHandler(HttpContext context, string requestType, string url, string pathTrasnlated) { string handlername = "MyHttpHandler1"; if(url.Substring(url.LastIndexOf("/")+1).StartsWith("t")) { handlername = "MyHttpHandler2"; } try { return (IHttpHandler) Activator.CreateInstance(Type.GetType(handlername)); } catch (Exception e) { throw new HttpException("Error: " + handlername, e); } } public void ReleaseHandler(IHttpHandler handler) { } }
موقعی که اینترفیس IHttpAsyncHandler را ارث بری کنید (این اینترفیس نیز از IHttpHandler ارث بری کرده است و دو متد اضافهتر دارد)، باید دو متد دیگر را نیز پیاده سازی کنید:در اعمالی که disk I/O و یا network I/O دارند، پردازش موازی و اعمال async به شدت مقیاس پذیری سیستم را بالا میبرند. به این ترتیب worker thread جاری (که تعداد آنها محدود است)، سریعتر آزاد شده و به worker pool بازگشت داده میشود تا بتواند به یک درخواست دیگر رسیده سرویس دهد. در این حالت میتوان با منابع کمتری، درخواستهای بیشتری را پردازش کرد.
public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback callback, object obj) { } public void EndProcessRequest(IAsyncResult result) { }
public class AsynchOperation : IAsyncResult { private bool _completed; private Object _state; private AsyncCallback _callback; private HttpContext _context; bool IAsyncResult.IsCompleted { get { return _completed; } } WaitHandle IAsyncResult.AsyncWaitHandle { get { return null; } } Object IAsyncResult.AsyncState { get { return _state; } } bool IAsyncResult.CompletedSynchronously { get { return false; } } }
public AsynchOperation(AsyncCallback callback, HttpContext context, Object state) { _callback = callback; _context = context; _state = state; _completed = false; }
public void StartAsyncWork() { ThreadPool.QueueUserWorkItem(new WaitCallback(StartAsyncTask),null); }
private void StartAsyncTask(Object workItemState) { _context.Response.Write("<p>Completion IsThreadPoolThread is " + Thread.CurrentThread.IsThreadPoolThread + "</p>\r\n"); _context.Response.Write("Hello World from Async Handler!"); _completed = true; _callback(this); }
context.Response.Write("<p>Begin IsThreadPoolThread is " + Thread.CurrentThread.IsThreadPoolThread + "</p>\r\n"); AsynchOperation asynch = new AsynchOperation(callback, context, obj); asynch.StartAsyncWork(); return asynch;
public class MyHttpHandler : IHttpAsyncHandler { public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback callback, object obj) { context.Response.Write("<p>Begin IsThreadPoolThread is " + Thread.CurrentThread.IsThreadPoolThread + "</p>\r\n"); AsynchOperation asynch = new AsynchOperation(callback, context, obj); asynch.StartAsyncWork(); return asynch; } public void EndProcessRequest(IAsyncResult result) { } public void ProcessRequest(HttpContext context) { throw new InvalidOperationException(); } public bool IsReusable { get { return false; } } } public class AsynchOperation : IAsyncResult { private bool _completed; private Object _state; private AsyncCallback _callback; private HttpContext _context; bool IAsyncResult.IsCompleted { get { return _completed; } } WaitHandle IAsyncResult.AsyncWaitHandle { get { return null; } } Object IAsyncResult.AsyncState { get { return _state; } } bool IAsyncResult.CompletedSynchronously { get { return false; } } public AsynchOperation(AsyncCallback callback, HttpContext context, Object state) { _callback = callback; _context = context; _state = state; _completed = false; } public void StartAsyncWork() { ThreadPool.QueueUserWorkItem(new WaitCallback(StartAsyncTask),null); } private void StartAsyncTask(Object workItemState) { _context.Response.Write("<p>Completion IsThreadPoolThread is " + Thread.CurrentThread.IsThreadPoolThread + "</p>\r\n"); _context.Response.Write("Hello World from Async Handler!"); _completed = true; _callback(this); }
مایکروسافت در MSDN نوشته است که httphandlerها در واقع فرآیندهایی هستند (به این فرایندها بیشتر End Point میگویند) که در پاسخ به درخواستهای رسیده شده توسط asp.net application اجرا میشوند و بیشترین درخواست هایی هم که میرسد از نوع صفحات Aspx میباشد و موقعی که کاربری درخواست صفحهی aspx میکند هندلرهای مربوط به page اجرا میشوند.