اشتراکها
کلاس ساده زیر را در نظر بگیرید:
using System.Collections.Generic;
namespace testWinForms87
{
class CDbgDisplay
{
public struct Person
{
public string Name;
public int Id;
}
public static List<Person> GetData()
{
List<Person> data = new List<Person>();
for (int i = 0; i < 40; i++)
data.Add(new Person { Name = "P" + i, Id = i });
return data;
}
}
}
همانطور که مشاهده میکنید، خروجی پیش فرض آنچنان دلپذیر نیست. به ازای هر کدام از 40 موردی که در این لیست قرار دارد، یکبار باید آن آیتم مورد نظر را انتخاب کرد، بر روی علامت + کنار آن کلیک نمود و سپس محتوای آنرا مشاهده کرد.
برای سفارشی سازی خروجی دیباگر ویژوال استودیو میتوان از ویژگی DebuggerDisplay استفاده کرد. سطر زیر را به بالای ساختار person اضافه کنید:
[DebuggerDisplay("Name:{Name},Id={Id}")]
اکنون یکبار دیگر بر روی data یک break point قرار داده و نتیجه را ملاحظه نمائید (شکل زیر):
بهتر شد؛ نه؟!
در اینجا یک رشته را با محتوای فیلدهای ساختار Person ایجاد کردیم و سپس خروجی پیش فرض دیباگر VS.Net را با آن جایگزین نمودیم. ویژوال استودیو محتوای عبارت داخل {} را با مقدار آن فیلد جایگزین خواهد کرد.
یکی از مهمترین بخشهای هر برنامه، بخش ذخیره و بازیابی دیتا است. برای ذخیره سازی از طریق وب و مرورگر، راههای مختلف زیادی چون webStorage ,Indexed DB ,Sqlite ,NeDB, و ... وجود دارند.
Sqlite دیتابیس مناسبی برای برنامههای چندسکویی است و عموما به عنوان اولین گزینه استفاده میشود. برای کار با این دیتابیس، ما از ماژول sql.js که یکی از ماژولهای معروف در جاوااسکریپت است، استفاده میکنیم. برای نصب آن از طریق npm، به شکل زیر اقدام میکنیم:
Sqlite دیتابیس مناسبی برای برنامههای چندسکویی است و عموما به عنوان اولین گزینه استفاده میشود. برای کار با این دیتابیس، ما از ماژول sql.js که یکی از ماژولهای معروف در جاوااسکریپت است، استفاده میکنیم. برای نصب آن از طریق npm، به شکل زیر اقدام میکنیم:
npm install sql.js --save
const{app,BrowserWindow}=require("electron"); let win; function onLoad() { win=new BrowserWindow({ width:800, height:600 }); win.loadURL(`file://${__dirname}/index.html`); } app.on("ready",onLoad());
ابتدا باید بررسی کنیم که آیا دیتابیس از قبل موجود
است یا خیر و اگر موجود نبود، برای اولین بار آن را بسازیم. برای بررسی وجود
یک فایل نیز میتوانیم از چند دستور مختلف استفاده کنیم. path.exists و fs.exists، دو عدد از آنها میباشند که هر دوی آنان به صورت
غیرهمزمان هستند و پارمتر اولشان نام فایل، به همراه مسیر (یا تنها مسیر) است و
پارامتر دوم هم یک تابع callback است که به عنوان پارامتر، جواب را بر
میگرداند. برای استفاده از حالت همزمان، عبارت Sync را به انتهای نام متدها
اضافه کنید. نحوه استفاده از آن به شکل زیر است:
var path=require("path"); path.exists('filepath",(status)=> { .... }); var status=path.existsSync("file"); //=============================== var fs=require("fs"); fs.exists('filepath",(status)=> { .... }); var status=path.existsSync("file");
fs.stat('foo.txt', function(err, stat) { if(err == null) { console.log('فایل موجوده'); } else if(err.code == 'ENOENT') { // فایل وجود نداره fs.writeFile('log.txt', 'Some log\n'); } else { //خطای دیگری رخ داده است } });
try { stats = fs.statSync(path); console.log("File exists."); } catch (e) { console.log("File does not exist."); }
const fs = require('fs'); const sql = require('sql.js'); dbPath = './mydb.sqlite'; dbExists=false; try { dbExists = fs.statSync(dbPath); } catch (e) { } if(!dbExists) { //create Database var sqlStr=fs.readFileSync("./sql.txt"); var db = new sql.Database(); db.run(String(sqlStr)); //write to disk var data=db.export(); var buffer=new Buffer(data); fs.writeFileSync(dbPath,buffer); } else{ var buffer = fs.readFileSync(dbPath); var db = new sql.Database(buffer); }
CREATE TABLE numbers ( id INT PRIMARY KEY UNIQUE NOT NULL, fname VARCHAR (20) NOT NULL, lname VARCHAR (30) NOT NULL, number VARCHAR (15) NOT NULL ); insert into numbers values(1,'ali','yeganeh','03111223344'); insert into numbers values(2,'xxx','yyy','45454555');
سپس در خطوط بعدی دیتابیس جدیدی را ایجاد میکنیم و دستور sql را با متد run اجرا میکنیم. دستوراتی که متد run اجرا میکند، شامل خروجی نیستند. پس دستوراتی را که نیاز به خروجی ندارند، به این متد بسپارید. سپس از این دیتابیس، یک شیء خروجی را دریافت میکنم و با بافر کردن، آن را در یک فایل ذخیره میکنیم. در صورتی هم که از قبل دیتابیس وجود داشته باشد، بافر خوانده شده را مستقیما به سازنده Database میدهیم.
بعد از آن نیاز است تا دیتابیس در دسترس Render Processها قرار بگیرد که در مقاله "شیوه کدنویسی در الکترون " در مورد global صحبت کردهایم و نحوه استفاده از آن را فرا گرفتیم:
بعد از آن نیاز است تا دیتابیس در دسترس Render Processها قرار بگیرد که در مقاله "شیوه کدنویسی در الکترون " در مورد global صحبت کردهایم و نحوه استفاده از آن را فرا گرفتیم:
global.db=db;
در پایان اجرای برنامه لازم است که دیتابیس توسط دستور close بسته شود. سپس کد زیر را در رویداد windows-all-closed مینویسیم:
app.on('window-all-closed', () => { db.close(); if (process.platform !== 'darwin') { app.quit(); } });
در این کد گفتهایم که موقعی که تمام پنجرههای برنامه بسته شدند، دیتابیس را نیز ببند.
(چند مورد خارج از بحث): کد بعدی که مورد استفاده قرار گرفته است و در مقالات قبلی در مورد آن صحبت نکردهایم این است که در سیستمهای مک، وضعیت به این قرار است که اگر شما برنامه را ببیندید، آن برنامه بسته نشده و در پس زمینه فعال است و میتوانید آن را از طریق dock اطراف صفحه، مجددا فعال کنید. ولی با نوشتن کد بالا، ما این وضعیت را اعلام کردهایم که اگر تمامی پنجرهها بسته شدند، کل برنامه را ببند.
همچنین بسیار خوب است که کد زیر را هم همیشه اضافه کنید:
(چند مورد خارج از بحث): کد بعدی که مورد استفاده قرار گرفته است و در مقالات قبلی در مورد آن صحبت نکردهایم این است که در سیستمهای مک، وضعیت به این قرار است که اگر شما برنامه را ببیندید، آن برنامه بسته نشده و در پس زمینه فعال است و میتوانید آن را از طریق dock اطراف صفحه، مجددا فعال کنید. ولی با نوشتن کد بالا، ما این وضعیت را اعلام کردهایم که اگر تمامی پنجرهها بسته شدند، کل برنامه را ببند.
همچنین بسیار خوب است که کد زیر را هم همیشه اضافه کنید:
win.on('closed', () => { win = null; });
موقعی که پنجره مربوطه بسته شود، متغیری که به پنجره اشاره میکند، در حافظه میماند. پس بهتر است که این مقدار حافظه را رها کنید تا Garbage Collector اقدام به حذف آن در حافظه کند.
پس اگر این کد را نوشتید، وضعیت سیستم عامل مک را به خاطر داشته باشید و مجبور هستید کد زیر را نیز اضافه کنید:
پس اگر این کد را نوشتید، وضعیت سیستم عامل مک را به خاطر داشته باشید و مجبور هستید کد زیر را نیز اضافه کنید:
app.on('activate', () => { if (win === null) { createWindow(); } });
بعد از اینکه دیتابیس را به شیء global دادیم، در صفحه html کد زیر را وارد میکنیم:
<html> <head> <script src="./jquery.min.js"></script> <link href="./bootstrap-3.3.6-dist/css/bootstrap.min.css" rel="stylesheet"></link> <meta charset="utf-8"> <title></title> <script> const {remote}=require("electron"); let db=remote.getGlobal("db"); </script> </head> <body> <table id="people" class="table table-hover table-striped"> <th> <tr> <td>First Name</td> <td>last Name</td> <td>Phone Number</td> </tr> </th> <tbody> </tbody> </table> </body> </html>
$(document).ready(()=> { //show data var tableBody=$("#people"); db.each("select * from numbers",(row)=>{ let rowTemplate=`<tr><td>${row.fname}</td><td>${row.lname}</td><td>${row.number}</td></tr>`; tableBody.append(rowTemplate); });
متد each برای مواقعی مناسب است که شما تعدادی سطر را برای بازگشت دارید و callback آن، به ازای هر سطر اجرا میشود و با استفاده از یک template string سطر سازی را انجام داده و آن را با استفاده از توابع جی کوئری به انتهای جدول اضافه میکنیم.
حال وقت آن رسیده است که خروجی کار را ببینیم. پس کد npm start را اجرا میکنیم. همانطور که میبینید خروجی به راحتی نمایش داده میشود. در مقاله بعدی بیشتر در این مورد صحبت میکنیم.
حال وقت آن رسیده است که خروجی کار را ببینیم. پس کد npm start را اجرا میکنیم. همانطور که میبینید خروجی به راحتی نمایش داده میشود. در مقاله بعدی بیشتر در این مورد صحبت میکنیم.
نظرات مطالب
هزینه استفاده از دات نت فریم ورک چقدر است؟
کسی که نسخهی ultimate ویژوال استودیو رو نصب کرده اما از ابزارهای unit testing ، UML Modeling projects ، بررسی معماری پروژه جاری و امثال آن استفاده نمیکند، همانند کسی است که Photoshop را نصب کرده اما دارد با آن عکس Resize میکند! :)
نظرات مطالب
هزینه استفاده از دات نت فریم ورک چقدر است؟
کسی که نسخهی ultimate ویژوال استودیو رو نصب کرده اما از ابزارهای unit testing ، UML Modeling projects ، بررسی معماری پروژه جاری و امثال آن استفاده نمیکند، همانند کسی است که Photoshop را نصب کرده اما دارد با آن عکس Resize میکند! :)
مطالب
آموزش Prism #2
در پست قبلی توضیح کلی درباره فریم ورک Prism داده شد. در این بخش قصد داریم آموزشهای داده شده در پست قبلی را با هم در یک مثال مشاهده کنیم. در پروژههای ماژولار طراحی و ایجاد زیر ساخت قوی برای مدیریت ماژولها بسیار مهم است. Prism فریم ورکی است که فقط چارچوب و قواعد اصول طراحی این گونه پروژهها را در اختیار ما قرار میدهد. در پروژههای ماژولار هر ماژول باید در یک اسمبلی جدا قرار داشته باشد که ساختار پیاده سازی آن میتواند کاملا متفاوت با پیاده سازی سایر ماژولها باشد.
برای شروع باید فایلهای اسمبلی Prism رو دانلود کنید(لینک دانلود). تشریح پروژه:
میخواهیم برنامه ای بنویسیم که دارای سه ماژول زیر است.:
- ماژول Navigator : برای انتخاب و Switch کردن بین ماژولها استفاده میشود؛
- ماژول طبقه بندی کتابها : لیست طبقه بندی کتابها را به ما نمایش میدهد؛
- ماژول لیست کتابها : عناوین کتابها به همراه نویسنده و کد کتاب را به ما نمایش میدهد.
ابتدا یک پروژه WPF در Vs.Net ایجاد کنید(در اینجا من نام آن را FirstPrismSample گذاشتم). قصد داریم یک صفحه طراحی کنیم که دو ماژول مختلف در آن لود شود. ابتدا باید Shell پروژه رو طراحی کنیم. یک Window جدید به نام Shell بسازید و کد زیر را در آن کپی کنید.
<Window x:Class="FirstPrismSample.Shell" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:com="http://www.codeplex.com/CompositeWPF" Title="Prism Sample By Masoud Pakdel" Height="400" Width="600" WindowStartupLocation="CenterScreen"> <DockPanel> <ContentControl com:RegionManager.RegionName="WorkspaceRegion" Width="400"/> <ContentControl com:RegionManager.RegionName="NavigatorRegion" DockPanel.Dock="Left" Width="200" /> </DockPanel> </Window>
#پروژه Common
قبل از هر چیز یک پروژه Common میسازیم و مشترکات بین ماژولها رو در آن قرار میدهیم(این پروژه باید به تمام ماژولها رفرنس داده شود). این مشترکات شامل :
- کلاس پایه ViewModel
- کلاس ViewRequestEvent
- کلاس ModuleService
کد کلاس ViewModelBase که فقط اینترفیس INotifyPropertyChanged رو پیاده سازی کرده است:
using System.ComponentModel; namespace FirstPrismSample.Common { public abstract class ViewModelBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void RaisePropertyChangedEvent( string propertyName ) { if ( PropertyChanged != null ) { PropertyChangedEventArgs e = new PropertyChangedEventArgs( propertyName ); PropertyChanged( this, e ); } } } }
using Microsoft.Practices.Composite.Presentation.Events; namespace FirstPrismSample.Common.Events { public class ViewRequestedEvent : CompositePresentationEvent<string> { } }
در طراحی و توسعه پروژههای ماژولار نکته ای که باید به آن دقت کنید این است که ماژولهای پروژه نباید به هم وابستگی مستقیم داشته باشند در عین حال ماژولها باید بتوانند با هم در ارتباط باشند. CPE یا CompositePresentationEventدقیقا برای این منظور به وجود آمده است. CPE که در این جا طراحی کردم فقط کلاسی است که از CompositePresentationEventارث برده است و دلیل آن که به صورت string generic استفاده شده است این است که میخواهیم در هر درخواست نام ماژول درخواستی را داشته باشیم و به همین دلیل نام آن را ViewRequestedEvent گذاشتم.
توضیح درباره EventAggregator
EventAggregator یا به اختصار EA مکانیزمی است در پروژهای ماژولار برای اینکه در Composite UIها بتوانیم بین کامپوننتها ارتباط برقرار کنیم. استفاده از EA وابستگی بین ماژولها را از بین خواهد برد. برنامه نویسانی که با MVVM Light آشنایی دارند از قابلیت Messaging موجود در این فریم ورک برای ارتباط بین View و ViewModel استفاده میکنند. در Prism این عملیات توسط EA انجام میشود. یعنی برای ارتباط با Viewها باید از EA تعبیه شده در Prism استفاده کنیم. در ادامه مطلب، چگونگی استفاده از EA را خواهید آموخت.
اینترفیس IModuleService که فقط شامل یک متد است:توضیح درباره EventAggregator
EventAggregator یا به اختصار EA مکانیزمی است در پروژهای ماژولار برای اینکه در Composite UIها بتوانیم بین کامپوننتها ارتباط برقرار کنیم. استفاده از EA وابستگی بین ماژولها را از بین خواهد برد. برنامه نویسانی که با MVVM Light آشنایی دارند از قابلیت Messaging موجود در این فریم ورک برای ارتباط بین View و ViewModel استفاده میکنند. در Prism این عملیات توسط EA انجام میشود. یعنی برای ارتباط با Viewها باید از EA تعبیه شده در Prism استفاده کنیم. در ادامه مطلب، چگونگی استفاده از EA را خواهید آموخت.
namespace FirstPrismSample .Common { public interface IModuleServices { void ActivateView(string viewName); } }
using Microsoft.Practices.Composite.Regions; using Microsoft.Practices.Unity; namespace FirstPrismSample.Common { public class ModuleServices : IModuleServices { private readonly IUnityContainer m_Container; public ModuleServices(IUnityContainer container) { m_Container = container; } public void ActivateView(string viewName) { var regionManager = m_Container.Resolve<IRegionManager>(); // غیر فعال کردن ویو IRegion workspaceRegion = regionManager.Regions["WorkspaceRegion"]; var views = workspaceRegion.Views; foreach (var view in views) { workspaceRegion.Deactivate(view); } //فعال کردن ویو انتخاب شده var viewToActivate = regionManager.Regions["WorkspaceRegion"].GetView(viewName); regionManager.Regions["WorkspaceRegion"].Activate(viewToActivate); } } }
*نکته: در هر ماژول ارجاع به اسمبلیهای Prism مورد نیاز است.
#ماژول طبقه بندی کتاب ها:
برای شروع یک Class Library جدید به نام ModuleCategory به پروژه اضافه کنید. یک UserControl به نام CategoryView بسازید و کدهای زیر را در آن کپی کنید.
<UserControl x:Class="FirstPrismSample.ModuleCategory.CategoryView " xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Background="LightGray" FlowDirection="RightToLeft" FontFamily="Tahoma"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <TextBlock Text=" طبقه بندی ها"/> <ListView Grid.Row="1" Margin="10" Name="lvCategory"> <ListView.View> <GridView> <GridViewColumn Header="کد" Width="50" /> <GridViewColumn Header="عنوان" Width="200" /> </GridView> </ListView.View> </ListView> </Grid> </UserControl>
using Microsoft.Practices.Composite.Events; using Microsoft.Practices.Composite.Modularity; using Microsoft.Practices.Composite.Regions; using Microsoft.Practices.Unity; using FirstPrismSample.Common; using FirstPrismSample.Common.Events; using Microsoft.Practices.Composite.Presentation.Events; namespace FirstPrismSample.ModuleCategory { [Module(ModuleName = "ModuleCategory")] public class CategoryModule : IModule { private readonly IUnityContainer m_Container; private readonly string moduleName = "ModuleCategory"; public CategoryModule(IUnityContainer container) { m_Container = container; } ~CategoryModule() { var eventAggregator = m_Container.Resolve<IEventAggregator>(); var viewRequestedEvent = eventAggregator.GetEvent<ViewRequestedEvent>(); viewRequestedEvent.Unsubscribe(ViewRequestedEventHandler); } public void Initialize() { var regionManager = m_Container.Resolve<IRegionManager>(); regionManager.Regions["WorkspaceRegion"].Add(new CategoryView(), moduleName); var eventAggregator = m_Container.Resolve<IEventAggregator>(); var viewRequestedEvent = eventAggregator.GetEvent<ViewRequestedEvent>(); viewRequestedEvent.Subscribe(this.ViewRequestedEventHandler, true); } public void ViewRequestedEventHandler(string moduleName) { if (this.moduleName != moduleName) return; var moduleServices = m_Container.Resolve<IModuleServices>(); moduleServices.ActivateView(moduleName); } } }
*ModuleAttribute استفاده شده در بالای کلاس برای تعیین نام ماژول استفاده میشود. این Attribute دارای دو خاصیت دیگر هم است :
- OnDemand : برای تعیین اینکه ماژول باید به صورت OnDemand (بنا به درخواست) لود شود.
- StartupLoaded : برای تعیین اینکه ماژول به عنوان ماژول اول پروزه لود شود.(البته این گزینه Obsolute شده است)
*برای تعریف ماژول کلاس مورد نظر حتما باید اینترفیس IModule را پیاده سازی کند. این اینترفیس فقط شامل یک متد است به نام Initialize.
*در این پروژه چون Viewهای برنامه صرفا جهت نمایش هستند در نتیجه نیاز به ایجاد ViewModel برای آنها نیست. در پروژههای اجرایی حتما برای هر View باید ViewModel متناظر با آن تهیه شود.
توضیح درباره متد Initialize
در این متد ابتدا با استفاده از Container موجود RegionManager را به دست میآوریم. با استفاده از RegionManager میتونیم یک CompositeUI طراحی کنیم. در فایل Shell مشاهده کردید که یک صفحه به دو ناحیه تقسیم شد و به هر ناحیه هم یک نام اختصاص دادیم. دستور زیر به یک ناحیه اشاره خواهد داشت:
regionManager.Regions["WorkspaceRegion"]
در خط بعد با استفاده از EA یا Event Aggregator توانستیم CPE را بدست بیاوریم. متد Subscribe در کلاس CPE یک ارجاع قوی به delegate مورد نظر ایجاد میکند(پارامتر دوم این متد که از نوع boolean است) که به این معنی است که این delegate هیچ گاه توسط GC جمع آوری نخواهد شد. در نتیجه، قبل از اینکه ماژول بسته شود باید به صورت دستی این کار را انجام دهیم که مخرب را برای همین ایجاد کردیم. اگر به کدهای مخرب دقت کنید میبینید که با استفاده از EA توانستیم ViewRequestEventHandler را Unsubscribe کنیم به دلیل اینکه از ارجاع قوی با strong Reference در متد Subscribe استفاده شده است.
دستور moduleService.ActiveateView ماژول مورد نظر را در region مورد نظر هاست خواهد کرد.#ماژول لیست کتاب ها:
ابتدا یک Class Library به نام ModuleBook بسازید و همانند ماژول قبلی نیاز به یک Window و یک کلاس داریم:
BookWindow که کاملا مشابه به CategoryView است.
<UserControl x:Class="FirstPrismSample.ModuleBook.BookView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Background="LightGray" FontFamily="Tahoma" FlowDirection="RightToLeft"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <TextBlock Text="لیست کتاب ها"/> <ListView Grid.Row="1" Margin="10" Name="lvBook"> <ListView.View> <GridView> <GridViewColumn Header="کد" Width="50" /> <GridViewColumn Header="عنوان" Width="200" /> <GridViewColumn Header="نویسنده" Width="150" /> </GridView> </ListView.View> </ListView> </Grid> </UserControl>
کلاس BookModule که پیاده سازی و توضیحات آن کاملا مشابه به CategoryModule میباشد.
using Microsoft.Practices.Composite.Events; using Microsoft.Practices.Composite.Modularity; using Microsoft.Practices.Composite.Presentation.Events; using Microsoft.Practices.Composite.Regions; using Microsoft.Practices.Unity; using FirstPrismSample.Common; using FirstPrismSample.Common.Events; namespace FirstPrismSample.ModuleBook { [Module(ModuleName = "moduleBook")] public class BookModule : IModule { private readonly IUnityContainer m_Container; private readonly string moduleName = "ModuleBook"; public BookModule(IUnityContainer container) { m_Container = container; } ~BookModule() { var eventAggregator = m_Container.Resolve<IEventAggregator>(); var viewRequestedEvent = eventAggregator.GetEvent<ViewRequestedEvent>(); viewRequestedEvent.Unsubscribe(ViewRequestedEventHandler); } public void Initialize() { var regionManager = m_Container.Resolve<IRegionManager>(); var view = new BookView(); regionManager.Regions["WorkspaceRegion"].Add(view, moduleName); regionManager.Regions["WorkspaceRegion"].Deactivate(view); var eventAggregator = m_Container.Resolve<IEventAggregator>(); var viewRequestedEvent = eventAggregator.GetEvent<ViewRequestedEvent>(); viewRequestedEvent.Subscribe(this.ViewRequestedEventHandler, true); } public void ViewRequestedEventHandler(string moduleName) { if (this.moduleName != moduleName) return; var moduleServices = m_Container.Resolve<IModuleServices>(); moduleServices.ActivateView(m_WorkspaceBName); } } }
برای این ماژول هم ابتدا View مورد نظر را ایجاد میکنیم:
<UserControl x:Class="FirstPrismSample.ModuleNavigator.NavigatorView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" > <Grid> <StackPanel VerticalAlignment="Center"> <TextBlock Text="انتخاب ماژول" Foreground="Green" HorizontalAlignment="Center" VerticalAlignment="Center" FontFamily="Tahoma" FontSize="24" FontWeight="Bold" /> <Button Command="{Binding ShowModuleCategory}" Margin="5" Width="125">طبقه بندی کتاب ها</Button> <Button Command="{Binding ShowModuleBook}" Margin="5" Width="125">لیست کتاب ها</Button> </StackPanel> </Grid> </UserControl>
public interface INavigatorViewModel { ICommand ShowModuleCategory { get; set; } ICommand ShowModuleBook { get; set; } string ActiveWorkspace { get; set; } IUnityContainer Container { get; set; } event PropertyChangedEventHandler PropertyChanged; }
*خاصیت ActiveWorkspace برای تعیین workspace فعال تعریف شده است.
حال به پیاده سازی مثال بالا میپردازیم:
public class NavigatorViewModel : ViewModelBase, INavigatorViewModel { public NavigatorViewModel(IUnityContainer container) { this.Initialize(container); } public ICommand ShowModuleCategory { get; set; } public ICommand ShowModuleBook { get; set; } public string ActiveWorkspace { get; set; } public IUnityContainer Container { get; set; } private void Initialize(IUnityContainer container) { this.Container = container; this.ShowModuleCategory = new ShowModuleCategoryCommand(this); this.ShowModuleBook = new ShowModuleBookCommand(this); this.ActiveWorkspace = "ModuleCategory"; } }
تنها نکته مهم در کلاس بالا متد Initialize است که دو Command مورد نظر را پیاده سازی کرده است. ماژول پیش فرض هم ماژول طبقه بندی کتابها یا ModuleCategory در نظر گرفته شده است. همان طور که میبینید پیاده سازی Commandها بالا توسط دو کلاس ShowModuleCategoryCommand و ShowModuleBookCommand انجام شده که در زیر کدهای آنها را میبینید.
#کد کلاس ShowModuleCategoryCommand public class ShowModuleCategoryCommand : ICommand { private readonly NavigatorViewModel viewModel; private const string workspaceName = "ModuleCategory"; public ShowModuleCategoryCommand(NavigatorViewModel viewModel) { this.viewModel = viewModel; } public bool CanExecute(object parameter) { return viewModel.ActiveWorkspace != workspaceName; } public event EventHandler CanExecuteChanged { add { CommandManager.RequerySuggested += value; } remove { CommandManager.RequerySuggested -= value; } } public void Execute(object parameter) { CommandServices.ShowWorkspace(workspaceName, viewModel); } }
public class ShowModuleBookCommand : ICommand { private readonly NavigatorViewModel viewModel; private readonly string workspaceName = "ModuleBook"; public ShowModuleBookCommand( NavigatorViewModel viewModel ) { this.viewModel = viewModel; } public bool CanExecute( object parameter ) { return viewModel.ActiveWorkspace != workspaceName; } public event EventHandler CanExecuteChanged { add { CommandManager.RequerySuggested += value; } remove { CommandManager.RequerySuggested -= value; } } public void Execute( object parameter ) { CommandServices.ShowWorkspace( workspaceName , viewModel ); } }
با توجه به این که فرض است با متدهای Execute و CanExecute و CanExecuteChanged آشنایی دارید از توضیح این مطالب خودداری خواهم کرد. فقط کلاس CommandServices در متد Execute دارای متدی به نام ShowWorkspace است که کدهای زیر را شامل میشود:
public static void ShowWorkspace(string workspaceName, INavigatorViewModel viewModel) { var eventAggregator = viewModel.Container.Resolve<IEventAggregator>(); var viewRequestedEvent = eventAggregator.GetEvent<ViewRequestedEvent>(); viewRequestedEvent.Publish(workspaceName); viewModel.ActiveWorkspace = workspaceName; }
عدم وابستگی ماژول ها
همان طور که میبینید ماژولهای پروژه به هم Reference داده نشده اند حتی هیچ Reference هم به پروژه اصلی یعنی جایی که فایل App.xaml قرار دارد، داده نشده است ولی در عین حال باید با هم در ارتباط باشند. برای حل این مسئله این ماژولها باید در فولدر bin پروژه اصلی خود را کپی کنند. بهترین روش استفاده از Pre-Post Build Event خود VS.Net است. برای این کار از پنجره Project Properties وارد برگه Build Events شوید و از قسمت Post Build Event Command Line استفاده کنید و کد زیر را در آن کپی نمایید:
xcopy "$(TargetDir)FirstPrismSample.ModuleBook.dll" "$(SolutionDir)FirstPrismSample\bin\$(ConfigurationName)\Modules\" /Y
مانند:
مراحل بالا برای هر ماژول باید تکرار شود(ModuleNavigation , ModuleBook , ModuleCategory). بعد از Rebuild پروژه در فولدر bin پروژه اصلی یک فولدر به نام Module ایجاد میشود که اسمبلی هر ماژول در آن کپی خواهد شد.
ایجاد Bootstrapper
حال نوبت به Bootstrapper میرسد(در پست قبلی در باره مفهوم Bootstrapper شرح داده شد). در پروژه اصلی یعنی جایی که فایل App.xaml قرار دارد کلاس زیر را ایجاد کنید.
public class Bootstrapper : UnityBootstrapper { protected override void ConfigureContainer() { base.ConfigureContainer(); Container.RegisterType<IModuleServices, ModuleServices>(); } protected override DependencyObject CreateShell() { var shell = new Shell(); shell.Show(); return shell; } protected override IModuleCatalog GetModuleCatalog() { var catalog = new DirectoryModuleCatalog(); catalog.ModulePath = @".\Modules"; return catalog; } }
متد ConfigureContainer برای تزریق وابستگی به وسیله UnityContainer استفاده میشود. در این متد باید تمامی Registrationهای مورد نیاز برای DI را انجام دهید. نکته مهم این است که عملیات وهله سازی و Initialization برای Container در متد base کلاس UnityBootstrapper انجام خواهد شد پس همیشه باید متد base این کلاس در ابتدای این متد فراخوانی شود در غیر این صورت با خطا متوقف خواهید شد.
متد CreateShell برای ایجاد و وهله سازی از Shell پروژه استفاده میشود. در این جا یک وهله از Shell Window برگشت داده میشود.
متد GetModuleCatalog برای تعیین مسیر ماژولها در پروژه کاربرد دارد. در این متد با استفاده از خاصیت ModulePath کلاس DirectoryModuleCatalog تعیین کرده ایم که ماژولهای پروژه در فولدر Modules موجود در bin اصلی پروژه قرار دارد. اگر به دستورات کپی در Post Build Event قسمت قبل توجه کنید میبینید که دستور ساخت فولدر وجود دارد.
متد GetModuleCatalog برای تعیین مسیر ماژولها در پروژه کاربرد دارد. در این متد با استفاده از خاصیت ModulePath کلاس DirectoryModuleCatalog تعیین کرده ایم که ماژولهای پروژه در فولدر Modules موجود در bin اصلی پروژه قرار دارد. اگر به دستورات کپی در Post Build Event قسمت قبل توجه کنید میبینید که دستور ساخت فولدر وجود دارد.
"$(SolutionDir)FirstPrismSample\bin\$(ConfigurationName)\Modules\" /Y
*نکته: اگر استفاده از این روش برای شناسایی ماژولها توسط Bootstrapper را
چندان جالب نمیدانید میتونید از MEF استفاده کنید که اسمبلی ماژولهای پروژه را به راحتی شناسایی میکند و در اختیار Bootsrtapper قرار میدهد(از آن جا در مستندات مربوط به Prism، بیشتر به استفاده از MEF تاکید شده است من هم در
پستهای بعدی، مثالها را با MEF پیاده سازی خواهم کرد)
در پایان باید فایل App.xaml را تغییر دهید به گونه ای که متد Run در کلاس Bootstapper ابتدا اجرا شود.
public partial class App : Application { protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); var bootstrapper = new Bootstrapper(); bootstrapper.Run(); } }
اجرای پروژه:
بعد از اجرا، با انتخاب ماژول مورد نظر اطلاعات ماژول در Workspace Content Control لود خواهد شد.
ادامه دارد...
در مقاله قبل توضیح دادیم که وظیفه httphandler رندر و پردازش خروجی یک درخواست هست؛ حالا در این مقاله قصد داریم که مفهوم httphandler را بیشتر بررسی کنیم.
HttpHandler
برای تهیهی یک httphandler، باید کلاسی را بر اساس اینترفیس IHttpHandler پیاده سازی کنیم و بعدا آن را در web.config برنامه معرفی کنیم. برای پیاده سازی این اینترفیس، به یک متد به اسم ProcessRequest با یک پارامتر از نوع HttpContext و یک پراپرتی به اسم IsReusable نیاز داریم که مقدار برگشتی این پراپرتی را false بگذارید؛ بعدا خواهم گفت چرا اینکار را میکنیم. نحوهی پیادهسازی یک 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>
خروجی نهایی باید تنها این متن باشد: Every Page has a some text like this
گزینه Type که نام کلاس میباشد و اگر کلاس داخل یک فضای نام قرار گرفته باشد، باید اینطور نوشت : namespace.ClassName |
گزینه verb شامل مقادیری چون Get,Post,Head,Putو Delete میباشد و httphandler را فقط برای این نوع درخواستها اجرا میکند و در صورتیکه بخواهید چندتا از آنها را استفاده کنید، با , از هم جدا میشوند. مثلا Get,post و درصورتیکه همهی گزینهها را بخواهید علامت * را میتوان استفاده کرد. |
گزینهی path این امکان را به شما میدهد که مسیر و نوع فایلهایی را که قصد دارید روی آنها فقط اجرا شود، مشخص کنید و ما در قطعه کد بالا گفتهایم که تنها روی فایلهایی با پسوند aspx اجرا شود و چون مسیری هم ذکر نکردیم برای همهی مسیرها قابل اجراست. یکی از مزیتهای دادن پسوند این است که میتوانید پسوندهای اختصاصی داشته باشید. مثلا پسوند RSS برای فیدهای وب سایتتان. بسیاری از برنامه نویسان به جای استفاده از صفحات aspx از ashx استفاده میکنند که به مراتب سبکتر از aspx هست و شامل بخش ui نمیشود و نتیجه خروجی آن بر اساس کدی که مینویسید مشخص میشود که میتواند صفحه متنی یا عکس یا xml یا ... باشد. در اینجا در مورد ساخت صفحات ashx توضیح داده شده است. |
IHttpHandlerFactory
کار این اینترفیس پیاده سازی یک کلاس است که خروجی آن یک کلاس از نوع IHttpHandler هست. اگر دقت کنید در مثالهای قبلی ما برای معرفی یک هندلر در وب کانفیگ یک سری path را به آن میدادیم و برای نمونه aspx.* را معرفی میکردیم؛ یعنی این هندلر را بر روی همهی فایلهای aspx اجرا کن و اگر دو یا چند هندلر در وب کانفیگ معرفی کنیم و برای همه مسیر aspx را قرار بدهیم، یعنی همه این هندلرها باید روی صفحات aspx اجرا گردند ولی در httphandlerfactory، ما چند هندلر داریم و میخواهیم فقط یکی از آنها بر روی صفحات aspx انجام بگیرد، پس ما یک هندلرفکتوری را برای صفحات aspx معرفی میکنیم و در حین اجرا تصمیم میگیریم که کدام هندلر را ارسال کنیم.
اجازه بدهید نوشتن این نوع کلاس را آغاز کنیم،ابتدا دو هندلر به نامهای httphandler1 و httphandler2 مینویسیم :
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) { } }
نحوهی تعریف factory در وب کانفیگ مانند قبل است و فقط باید در Type به جای نام هندلر نام فکتوری را نوشت. برنامه را اجرا کنید تا نتیجه آن را ببینیم:
تصویر زیر نتیجه صدا زده شدن فایل default.aspx است:
تصویر زیر نتیجه صدا زده شدن فایل Tours_List.aspx است:
AsyncHttpHandlers
برای اینکه کار این اینترفیس را درک کنید بهتر هست اینجا را مطالعه کنید. در اینجا به خوبی تفاوت متدهای همزمان و غیرهمزمان توضیح داده شده است.
متن زیر خلاصهترین و بهترین توضیح برای این پرسش است، چرا غیرهمزمان؟
موقعی که اینترفیس 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) { }
پراپرتی ISResuable هم موقعی که true برگشت بدهد، باعث میشود pooling فعال شده و این هندلر در حافظه باقی بماند و تمامی درخواستها از طریق همین یک نمونه اجرا شوند.
به زبان سادهتر، این پراپرتی میگوید اگر چندین درخواست از طرف کلاینتها برسد، توسط یک نمونه یا instance از هندلر پردازش خواهند شد؛ چون به طور پیش فرض موقعی که تمام درخواستهای از pipeline بگذرند، هندلرها توسط httpapplication در یک لیست بازیافت قرار گرفته و همهی آنها با null مقداردهی میشوند تا از حافظه پاک شوند ولی اگر این پراپرتی true برگرداند، هندلر مربوطه نال نشده و برای پاسخگویی به درخواستهای بعدی در حافظه خواهد ماند.
مهمترین مزیت این گزینه، این میباشد که کاآیی سیستم را بالا میبرد و اشیا کمتری به GC پاس میشوند. ولی یک عیب هم دارد که این تردهایی که ایجاد میکند، امنیت کمتری دارند و باید توسط برنامه نویس این امنیت بالاتر رود. این پراپرتی را در مواقعی که با هندلرهای همزمان کار میکنید برابر با false بگذارید چون این گزینه بیشتر بر روی هندلرهای غیرهمزمان اثر دارد و هم اینکه بعضیها توصیه میکنند که false بگذارید چون GC مدیریت خوبی در مورد هندلرها دارد و هم این که ارزش یافتن باگ در کد را ندارد.
بر میگردیم سراغ کد نویسی هندلر غیر همزمان. در آخرین قطعه کد نوشته شده، ما دو متد دیگر را پیاده سازی کردیم که یکی از آنها BeginProcessRequest است و خروجی آن کلاسی است که از اینترفیس IAsyncResult ارث بری کرده است. پس یک کلاس با ارث بری از این اینترفیس مینویسیم و در این کلاس نیاز است که 4 پراپرتی را پیاده سازی کنیم که این کلاس به شکل زیر در خواهد آمد:
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; }
همانطور که میبینید موارد موجود در متد BeginProcessRequest را تحویل میگیریم تا اطلاعات درخواستی مربوطه را داشته باشیم و مقدار _Completed را هم برابر با false قرار میدهیم. سپس نوبت این میرسد که ما درخواست را در صف pool قرار دهیم. برای همین تکه کد زیر را اضافه میکنیم:
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); }
نهایتا کل این کلاس را در متد BeginProcessRequest صدا بزنید:
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); }
آشنایی با فایل ASHX
در مطالب بالاتر به فایلهای Ashx اشاره کردیم. این فایل به نام Generic Web Handler شناخته میشوند و میتوانید با Add New Item این نوع فایلها را اضافه کنید. این فایل شامل هیچ UI ایی نمیباشد و فقط شامل بخش کد میباشد. برای همین نسبت به aspx سبکتر بوده و شامل یک directive به اسم WebHandler@ است.
مایکروسافت در MSDN نوشته است که httphandlerها در واقع فرآیندهایی هستند (به این فرایندها بیشتر End Point میگویند) که در پاسخ به درخواستهای رسیده شده توسط asp.net application اجرا میشوند و بیشترین درخواست هایی هم که میرسد از نوع صفحات Aspx میباشد و موقعی که کاربری درخواست صفحهی aspx میکند هندلرهای مربوط به page اجرا میشوند.
در متن بالا به خوبی روشن هست که ashx به دلیل نداشتن UI، تعداد کمتری از handlerها را در مسیر Pipeline قرار میدهند و اجرای آنها سریعتر است. غیر از این دو هندلر aspx و ashx، هندلر توکار دیگری چون asmx که مختص وب سرویس هست و axd مربوط به اعمال trace نیز وجود دارند.
در این لینک که در بالاتر هم درج شده بود یک نمونه هندلر برای نمایش تصویر نوشته است. اگر تصاویرتان را بدین صورت اجرا کنید میتوان جلوی درخواستهای رسیده از وب سایتهای دیگر را سد کرد. برای مثال یک نفر مطالب شما را کپی میکند و در داخل وبلاگ یا وب سایتش میگذارد و شما در اینجا درخواستهای رسیده خارج از وب سایت خود را لغو خواهید کرد و تصاویر کپی شده نمایش داده نخواهند شد.