صورت مساله:
تعدادی تصویر داریم، میخواهیم اینها را تبدیل به فایل PDF کنیم به این شرط که هر تصویر در یک صفحه مجزا قرار داده شود.
در ادامه برای این منظور از کتابخانهی iTextSharp استفاده خواهیم کرد.
iTextSharp چیست؟
iTextSharp کتابخانهی سورس باز و معروفی جهت تولید فایلهای PDF ، توسط برنامههای مبتنی بر دات نت است. آن را از آدرس زیر میتوان دریافت کرد:
کتابخانه iTextSharp نیز جزو کتابخانههایی است که از جاوا به دات نت تبدیل شدهاند. نام کتابخانه اصلی iText است و اگر کمی جستجو کنید میتوانید کتاب 617 صفحهای iText in Action از انتشارات MANNING را در این مورد نیز بیابید. هر چند این کتاب برای برنامه نویسهای جاوا نوشته شده اما نام کلاسها و متدها در iTextSharp تفاوتی با iText اصلی ندارند و مطالب آن برای برنامه نویسهای دات نت هم قابل استفاده است.
مجوز استفاده از iTextSharp کدام است؟
مجوز این کتابخانه GNU Affero General Public License است. به این معنا که شما موظفید، تغییری در قسمت تهیه کننده خواص فایل PDF تولیدی که به صورت خودکار به نام کتابخانه تنظیم میشود، ندهید. اگر میخواهید این قسمت را تغییر دهید باید هزینه کنید. همچنین با توجه به اینکه این مجوز، GPL است یعنی زمانیکه از آن استفاده کردید باید کار خود را به صورت سورس باز ارائه دهید؛ درست خوندید! بله! مثل مجوز استفاده از نگارش عمومی و رایگان MySQL و اگر نمیخواهید اینکار را انجام دهید، در اینجا تاکید شده که باید کتابخانه را خریداری کنید.
نحوه استفاده از کتابخانه iTextSharp
در ابتدا کد تبدیل تصاویر به فایل PDF را در ذیل مشاهده خواهید کرد. فرض بر این است که ارجاعی را به اسمبلی itextsharp.dll اضافه کردهاید:
using System.Collections.Generic;
using System.Drawing.Imaging;
using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;
namespace iTextSharpTests
{
public class ImageToPdf
{
public iTextSharp.text.Rectangle PdfPageSize { set; get; }
public ImageFormat ImageCompressionFormat { set; get; }
public bool FitImagesToPage { set; get; }
public void ExportToPdf(IList<string> imageFilesPath, string outPdfPath)
{
using (var pdfDoc = new Document(PdfPageSize))
{
PdfWriter.GetInstance(pdfDoc, new FileStream(outPdfPath, FileMode.Create));
pdfDoc.Open();
foreach (var file in imageFilesPath)
{
var pngImg = iTextSharp.text.Image.GetInstance(file);
if (FitImagesToPage)
{
pngImg.ScaleAbsolute(pdfDoc.PageSize.Width, pdfDoc.PageSize.Height);
}
pngImg.SetAbsolutePosition(0, 0);
//add to page
pdfDoc.Add(pngImg);
//start a new page
pdfDoc.NewPage();
}
}
}
}
}
استفاده از کتابخانهی iTextSharp همیشه شامل 5 مرحله است. ابتدا شیء Document ایجاد میشود. سپس وهلهای از PdfWriter ساخته شده و Document جهت نوشتن در آن گشوده خواهد شد. در طی یک سری مرحله محتویات مورد نظر به Document اضافه شده و نهایتا این شیء بسته خواهد شد. البته در اینجا چون کلاس Document اینترفیس IDisposable را پیاده سازی کرده، بهترین روش استفاده از آن بکارگیری واژه کلیدی using جهت مدیریت منابع آن است. به این ترتیب کامپایلر به صورت خودکار قطعه try/finally مرتبط را جهت پاکسازی منابع، تشکیل خواهد داد.
اندازه صفحات توسط سازندهی شیء Document مشخص خواهند شد. این شیء از نوع iTextSharp.text.Rectangle است؛ اما مقدار دهی آن توسط کلاس iTextSharp.text.PageSize صورت میگیرد که انواع و اقسام اندازه صفحات استاندارد در آن تعریف شدهاند.
متد iTextSharp.text.Image.GetInstance که در این مثال جهت دریافت اطلاعات تصاویر مورد استفاده قرار گرفت، 15 overload دارد که از آدرس مستقیم یک فایل تا استریم مربوطه تا Uri یک آدرس وب را نیز میپذیرد و از این لحاظ بسیار غنی است.
مثالی در مورد نحوه استفاده از کلاس فوق:
using System.Collections.Generic;
using System.Drawing.Imaging;
namespace iTextSharpTests
{
class Program
{
static void Main(string[] args)
{
new ImageToPdf
{
FitImagesToPage = true,
ImageCompressionFormat = ImageFormat.Jpeg,
PdfPageSize = iTextSharp.text.PageSize.A4
}.ExportToPdf(
imageFilesPath: new List<string>
{
@"D:\3.jpg",
@"D:\4.jpg"
},
outPdfPath: @"D:\tst.pdf"
);
}
}
}
public static readonly ConcurrentDictionary<string, string> OnlineUsers = new ConcurrentDictionary<string, string>();
+ کمی بالاتر در نظرات هم توضیح دادم. اگر پارامتر stopCalled متد OnDisconnected مساوی true باشد، یعنی کاربر «به هر نحوی» به سرور در مورد قطع اتصالش اطلاع رسانی کردهاست. اگر مقدار آن false باشد، یعنی SignalR این متد را به علت عدم فعالیت کاربر در طی 35 ثانیهی قبل فراخوانی کردهاست. بنابراین این دو مورد را باید مدیریت کنید (اطلاعات رسانیهای سمت سرور و سمت کاربر).
- در مورد اطلاع رسانیهای ویژه هم مطلب در گروه SignalR سایت داریم.
یکی از مواردی که در تمام برنامههای فارسی "باید" رعایت شود (مهم نیست به چه زبانی یا چه سکویی باشد یا چه بانک اطلاعاتی مورد استفاده است)، بحث اصلاح "ی" و "ک" دریافتی از کاربر و یکسان سازی آنها میباشد. به عبارتی برنامهی فارسی که اصلاح خودکار این دو مورد را لحاظ نکرده باشد دیر یا زود به مشکلات حادی برخورد خواهد کرد و "ناقص" است : اطلاعات بیشتر ؛ برای مثال شاید دوست نداشته باشید که دو کامران در سایت شما ثبت نام کرده باشند؛ یکی با ک فارسی و یکی با ک عربی! به علاوه همین کامران امروز میتواند لاگین کند و فردا با یک کامپیوتر دیگر و صفحه کلیدی دیگر پشت درب خواهد ماند. در حالیکه از دید این کامران، کلمه کامران همان کامران است!
بنابراین در دو قسمت "باید" این یکسان سازی صورت گیرد:
الف) پیش از ثبت اطلاعات در بانک اطلاعاتی (تا با دو کامران ثبت شده در بانک اطلاعاتی مواجه نشوید)
ب) پیش از جستجو (تا کامران روزی دیگر با صفحه کلیدی دیگر بتواند به برنامه وارد شود)
راه حل یکسان سازی هم شاید به نظر این باشد: رخداد فشرده شدن کلید را کنترل کنید و سپس جایگزینی را انجام دهید (مثلا ی عربی را با ی فارسی جایگزین کنید). این روش چند ایراد دارد:
الف) Silverlight به دلایل امنیتی اصلا چنین اجازهای را به شما نمیدهد! (تا نتوان کلیدی را جعل کرد)
ب) همیشه با یک TextBox ساده سر و کار نداریم. کنترلهای دیگری هم هستند که امکان ورود اطلاعات در آنها وجود دارد و آن وقت باید برای تمام آنها کد نوشت. ظاهر کدهای برنامه در این حالت در حجم بالا، اصلا جالب نخواهد بود و ضمنا ممکن است یک یا چند مورد فراموش شوند.
راه بهتر این است که دقیقا حین ثبت اطلاعات یا جستجوی اطلاعات در لایهای که تمام ثبتها یا اعمال کار با بانک اطلاعاتی برنامه به آنجا منتقل میشود، کار یکسان سازی صورت گیرد. به این صورت کار یکپارچه سازی یکبار باید انجام شود اما تاثیرش را بر روی کل برنامه خواهد گذاشت، بدون اینکه هرجایی که امکان ورود اطلاعات هست روالهای رخداد گردان هم حضور داشته باشند.
در مورد مقدمات WCF RIA Services که درSilverlight و ASP.NET کاربرد دارد میتوانید به این مطلب مراجعه کنید: +
جهت تکمیل این بحث متدی تهیه شده که کار یکسان سازی ی و ک دریافتی از کاربر را حین ثبت توسط امکانات WCF RIA Services انجام میدهد (دقیقا پیش از فراخوانی متد SubmitChanges باید بکارگرفته شود):
namespace SilverlightTests.RiaYeKe
{
public static class PersianHelper
{
public static string ApplyUnifiedYeKe(this string data)
{
if (string.IsNullOrEmpty(data)) return data;
return data.Replace("ی", "ی").Replace("ک", "ک");
}
}
}
using System.Linq;
using System.Windows.Controls;
using System.Reflection;
using System.ServiceModel.DomainServices.Client;
namespace SilverlightTests.RiaYeKe
{
public class RIAHelper
{
/// <summary>
/// یک دست سازی ی و ک در عبارات ثبت شده در بانک اطلاعاتی پیش از ورود به آن
/// این متد باید پیش از فراخوانی متد
/// SubmitChanges
/// استفاده شود
/// </summary>
/// <param name="dds"></param>
public static void ApplyCorrectYeKe(DomainDataSource dds)
{
if (dds == null)
return;
if (dds.DataView.TotalItemCount <= 0)
return;
//پیدا کردن موجودیتهای تغییر کرده
var changedEntities = dds.DomainContext.EntityContainer.GetChanges().Where(
c => c.EntityState == EntityState.Modified ||
c.EntityState == EntityState.New);
foreach (var entity in changedEntities)
{
//یافتن خواص این موجودیتها
var propertyInfos = entity.GetType().GetProperties(
BindingFlags.Public | BindingFlags.Instance
);
foreach (var propertyInfo in propertyInfos)
{
//اگر این خاصیت رشتهای است ی و ک آن را استاندارد کن
if (propertyInfo.PropertyType != typeof (string)) continue;
var propName = propertyInfo.Name;
var val = new PropertyReflector().GetValue(entity, propName);
if (val == null) continue;
new PropertyReflector().SetValue(
entity,
propName,
val.ToString().ApplyUnifiedYeKe());
}
}
}
}
}
توضیحات:
از آنجائیکه حین فراخوانی متد SubmitChanges فقط موجودیتهای تغییر کرده جهت ثبت ارسال میشوند، ابتدا این موارد یافت شده و سپس خواص عمومی تک تک این اشیاء توسط عملیات Reflection بررسی میگردند. اگر خاصیت مورد بررسی از نوع رشتهای بود، یکبار این یک دست سازی اطلاعات ی و ک دریافتی صورت خواهد گرفت (و از آنجائیکه این تعداد همیشه محدود است عملیات Reflection سربار خاصی نخواهد داشت).
اگر در کدهای خود از DomainDataSource استفاده نمیکنید باز هم تفاوتی نمیکند. متد ApplyCorrectYeKe را از قسمت DomainContext.EntityContainer به بعد دنبال کنید.
اکنون تنها مورد باقیمانده بحث جستجو است که با اعمال متد ApplyUnifiedYeKe به مقدار ورودی متد جستجوی خود، مشکل حل خواهد شد.
کلاس PropertyReflector بکارگرفته شده هم از اینجا به عاریت گرفته شد.
دریافت کدهای این بحث
- به نظر با آخرین نگارش jQuery سازگار نیست. به صفحه خطای مرورگر مراجعه کنید تا خطای شناخته نشدن نوع مرورگر قابل مشاهده باشد. با نگارشهای پایینتر مشکلی ندارد.
var image = document.createElement("img"); image.setAttribute("src", "logo.png"); React.createElement("img", { src : "logo.png" });
Virtual DOM
تفاوت در ساخت تگهای HTML به صورت مجازی بین JavaScript و React این است که React وضعیت تگهایی را که میسازد دنبال میکند. برای مثال فرض کنید نام سه محصول را در یک تگ < ul > نشان دادهایم. React وضعیت اصلی این تگ را که به مرورگر فرستاده، در حافظه دارد و همچنین در اثر تغییر منبع دادهای که برای < ul > مشخص کردهایم (که میتواند ورود اطلاعات به صورت Ajax باشد (مثلا اضافه شدن یک محصول جدید)) وضعیت جدیدی را برای تگ < ul > در حافظه ایجاد میکند. با وجود دو وضعیت برای یک تگ در حافظه، React میتواند تفاوت بین آنها را تشخیص داده و تگ را به روز کند. به این حالت عملکرد React ، اصطلاحا Virtual DOM میگویند.
React رابط کاربری را به صورت یک مدل میبیند و این مدل را با توجه به وضعیت اصلی آن در حافظه دوباره میسازد. برای React مهم نیست که ماهیت تغییر چیست. فقط وضعیتها را مثل دو عکس میبیند و میفهمد که آیا چیزی عوض شدهاست یا نه. دیالوگ React با مرورگر اینطور است: ای تگ < ul > این لیست را نشان بده (لیستی با سه محصول)، و بعد میگوید: ای تگ < ul > این لیست را نشان بده (لیستی با چهار محصول)!
کامپوننتهای React
<a href = “http://google.com”> <img src=”google.png”/> </a> // Components <clickableimage/> <linkimage/>
در کد بالا، بخش اول واضح است. عکسی که قابلیت کلیک شدن را دارد. حال فرض کنید یکی از کامپوننتهای <clickableimage/> یا <linkimage/>، همان تصویر قابل کلیک را ایجاد کنند. با نام گذاری واضح کامپوننتها، خوانایی برنامه بهتر میشود. یعنی میدانیم هر کامپوننت چه کاری را برای ما انجام میدهد. با این تصور که اگر تگهای زیاد و طولانی را در بخش رابط کاربری داریم، ارزش استفاده از کامپوننتهای React مشخص میشود.
قابلیت استفاده مجدد
در React کامپوننتها برای اساس توابع ساخته میشوند. یعنی وقتی یک کامپوننت را صدا بزنیم، در واقع یک تابع را اجرا میکنیم. در نتیجه کامپوننتها رفتار توابع را دارند؛ ورودی میگیرند و خروجی که یک DOM مجازی است را تحویل میدهند. اگر تابعی که مسئول ساخت کامپوننت است وابستگی به توابع یا متغیرهای بیرونی نداشته باشد، میتواند در جای دیگری از برنامه یا برنامهای دیگر مجددا استفاده شود. کد زیر نشان میدهد که چطور کامپوننتهای React ساخته میشوند.var ClickableImage = function(props) { return ( <a href={props.href}> <img src={props.src} /> </a> ); }; ReactDOM.render( <ClickableImage href="http://google.com" src="logo.png" />, document.getElementById("targetDivId"));
OpenCVSharp #4
فرض کنید قصد داریم یک چنین مثال زبان C را که در مورد کار با فیلترها در OpenCV است، به نمونهی دات نتی آن تبدیل کنیم:
#include <cv.h> #include <highgui.h> #include <stdio.h> int main (int argc, char **argv) { IplImage *src_img = 0, *dst_img; float data[] = { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }; CvMat kernel = cvMat (1, 21, CV_32F, data); if (argc >= 2) src_img = cvLoadImage (argv[1], CV_LOAD_IMAGE_ANYDEPTH | CV_LOAD_IMAGE_ANYCOLOR); if (src_img == 0) exit (-1); dst_img = cvCreateImage (cvGetSize (src_img), src_img->depth, src_img->nChannels); cvNormalize (&kernel, &kernel, 1.0, 0, CV_L1); cvFilter2D (src_img, dst_img, &kernel, cvPoint (0, 0)); cvNamedWindow ("Filter2D", CV_WINDOW_AUTOSIZE); cvShowImage ("Filter2D", dst_img); cvWaitKey (0); cvDestroyWindow ("Filter2D"); cvReleaseImage (&src_img); cvReleaseImage (&dst_img); return 0; }
using (var src = new IplImage(@"..\..\Images\Penguin.Png", LoadMode.AnyDepth | LoadMode.AnyColor)) using (var dst = new IplImage(src.Size, src.Depth, src.NChannels)) { float[] data = { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }; var kernel = new CvMat(rows: 1, cols: 21, type: MatrixType.F32C1, elements: data); Cv.Normalize(src: kernel, dst: kernel, a: 1.0, b: 0, normType: NormType.L1); Cv.Filter2D(src, dst, kernel, anchor: new CvPoint(0, 0)); using (new CvWindow("src", image: src)) using (new CvWindow("dst", image: dst)) { Cv.WaitKey(0); } }
در قسمتهای قبلی در مورد بارگذاری تصاویر، تهیهی یک Clone از آن و همچنین ساخت یک پنجره به روشهای مختلف و رها سازی خودکار منابع مرتبط، بیشتر بحث شد. در اینجا تصویر اصلی با همان عمق و وضوح تغییر نیافتهی آن بارگذاری میشود.
کار cvMat، آغاز یک ماتریس OpenCV است. پارامترهای آن، تعداد ردیفها، ستونها، نوع دادهی المانها و دادههای مرتبط را مشخص میکنند.
در این مثال آرایهی data، یک فیلتر را تعریف میکند که در اینجا، حالت یک بردار را دارد تا یک ماتریس. برای تبدیل آن به ماتریس، از شیء CvMat استفاده خواهد شد که آنرا تبدیل به ماتریسی با یک ردیف و 21 ستون خواهد کرد.
در اینجا از نام کرنل استفاده شدهاست. کرنل در OpenCV به معنای ماتریسی از دادهها با یک نقطهی anchor (لنگر) است. این لنگر به صورت پیش فرض در میانهی ماتریس قرار دارد (نقطهی 1- , 1- ).
مرحلهی بعد، نرمال سازی این فیلتر است. تاثیر نرمال سازی اطلاعات را به این نحو میتوان نمایش داد:
double sum = 0; foreach (var item in data) { sum += Math.Abs(item); } Console.WriteLine(sum); // => .999999970197678
اگر مرحلهی نرمال سازی اطلاعات را حذف کنیم، تصویر نهایی حاصل، چنین شکلی را پیدا میکند:
زیرا عملیات تغییر اندازهی اطلاعات بردار صورت نگرفتهاست و دادههای آن مطلوب متد cvFilter2D نیست.
و مرحلهی آخر، اجرای این بردار نرمال شده خطی، بر روی تصویر اصلی به کمک متد cvFilter2D است. این متد، تصویر مبدا را پس از تبدیلات ماتریسی، به تصویر مقصد تبدیل میکند. فرمول ریاضی اعمال شدهی در اینجا برای محاسبهی نقاط تصویر خروجی به صورت زیر است:
فیلترهای توکار OpenCV
علاوه بر امکان طراحی فیلترهای سفارشی خطی مانند مثال فوق، کتابخانهی OpenCV دارای تعدادی فیلتر توکار نیز میباشد که نمونهای از آنرا در مثال ذیل میتوانید مشاهده کنید:
using (var src = new IplImage(@"..\..\Images\Car.jpg", LoadMode.AnyDepth | LoadMode.AnyColor)) { using (var dst = new IplImage(src.Size, src.Depth, src.NChannels)) { using (new CvWindow("src", image: src)) { Cv.Erode(src, dst); using (new CvWindow("Erode", image: dst)) { Cv.Dilate(src, dst); using (new CvWindow("Dilate", image: dst)) { Cv.Not(src, dst); using (new CvWindow("Invert", image: dst)) { Cv.WaitKey(0); } } } } } }
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید.
1- چگونه Areaهای استاندارد را تبدیل به یک افزونهی مجزا و منتقل شدهی به یک اسمبلی دیگر کنیم.
2- چگونه ساختار پایهای را جهت تامین نیازهای هر افزونه جهت تزریق وابستگیها تا ثبت مسیریابیها و امثال آن تدارک ببینیم.
3- چگونه فایلهای CSS ، JS و همچنین تصاویر ثابت هر افزونه را داخل اسمبلی آن قرار دهیم تا دیگر نیازی به ارائهی مجزای آنها نباشد.
4- چگونه Entity Framework Code-First را با این طراحی یکپارچه کرده و از آن جهت یافتن خودکار مدلها و موجودیتهای خاص هر افزونه استفاده کنیم؛ به همراه مباحث Migrations خودکار و همچنین پیاده سازی الگوی واحد کار.
در مطلب جاری، موارد اول و دوم بررسی خواهند شد. پیشنیازهای آن مطالب ذیل هستند:
الف) منظور از یک Area چیست؟
ب) توزیع پروژههای ASP.NET MVC بدون ارائه فایلهای View آن
ج) آشنایی با تزریق وابستگیها در ASP.NET MVC و همچنین اصول طراحی یک سیستم افزونه پذیر به کمک StructureMap
د) آشنایی با رخدادهای Build
تبدیل یک Area به یک افزونهی مستقل
روشهای زیادی برای خارج کردن Areaهای استاندارد ASP.NET MVC از یک پروژه و قرار دادن آنها در اسمبلیهای دیگر وجود دارند؛ اما در حال حاضر تنها روشی که نگهداری میشود و همچنین اعضای آن همان اعضای تیم نیوگت و ASP.NET MVC هستند، همان روش استفاده از Razor Generator است.
بنابراین ساختار ابتدایی پروژهی افزونه پذیر ما به صورت ذیل خواهد بود:
1) ابتدا افزونهی Razor Generator را نصب کنید.
2) سپس یک پروژهی معمولی ASP.NET MVC را آغاز کنید. در این سری نام MvcPluginMasterApp برای آن در نظر گرفته شدهاست.
3) در ادامه یک پروژهی معمولی دیگر ASP.NET MVC را نیز به پروژهی جاری اضافه کنید. برای مثال نام آن در اینجا MvcPluginMasterApp.Plugin1 تنظیم شدهاست.
4) به پروژهی MvcPluginMasterApp.Plugin1 یک Area جدید و معمولی را به نام NewsArea اضافه کنید.
5) از پروژهی افزونه، تمام پوشههای غیر Area را حذف کنید. پوشههای Controllers و Models و Views حذف خواهند شد. همچنین فایل global.asax آنرا نیز حذف کنید. هر افزونه، کنترلرها و Viewهای خود را از طریق Area مرتبط دریافت میکند و در این حالت دیگر نیازی به پوشههای Controllers و Models و Views واقع شده در ریشهی اصلی پروژهی افزونه نیست.
6) در ادامه کنسول پاور شل نیوگت را باز کرده و دستور ذیل را صادر کنید:
PM> Install-Package RazorGenerator.Mvc
همانطور که در تصویر نیز مشخص شدهاست، برای اجرای دستور نصب RazorGenerator.Mvc نیاز است هربار پروژهی پیش فرض را تغییر دهید.
7) اکنون پس از نصب RazorGenerator.Mvc، نوبت به اجرای آن بر روی هر دو پروژهی اصلی و افزونه است:
PM> Enable-RazorGenerator
همچنین هربار که View جدیدی اضافه میشود نیز باید اینکار را تکرار کنید یا اینکه مطابق شکل زیر، به خواص View جدید مراجعه کرده و Custom tool آنرا به صورت دستی به RazorGenerator تنظیم نمائید. دستور Enable-RazorGenerator اینکار را به صورت خودکار انجام میدهد.
تا اینجا موفق شدیم Viewهای افزونه را داخل فایل dll آن مدفون کنیم. به این ترتیب با کپی کردن افزونه به پوشهی bin پروژهی اصلی، دیگر نیازی به ارائهی فایلهای View آن نیست و تمام اطلاعات کنترلرها، مدلها و Viewها به صورت یکجا از فایل dll افزونهی ارائه شده خوانده میشوند.
کپی کردن خودکار افزونه به پوشهی Bin پروژهی اصلی
پس از اینکه ساختار اصلی کار شکل گرفت، هربار پس از کامپایل افزونه (یا افزونهها)، نیاز است فایلهای پوشهی bin آنرا به پوشهی bin پروژهی اصلی کپی کنیم (پروژهی اصلی در این حالت هیچ ارجاع مستقیمی را به افزونهی جدید نخواهد داشت). برای خودکار سازی این کار، به خواص پروژهی افزونه مراجعه کرده و قسمت Build events آنرا به نحو ذیل تنظیم کنید:
در اینجا دستور ذیل در قسمت Post-build event نوشته شده است:
Copy "$(ProjectDir)$(OutDir)$(TargetName).*" "$(SolutionDir)MvcPluginMasterApp\bin\"
تنظیم فضاهای نام کلیه مسیریابیهای پروژه
در همین حالت اگر پروژه را اجرا کنید، موتور ASP.NET MVC به صورت خودکار اطلاعات افزونهی کپی شده به پوشهی bin را دریافت و به Application domain جاری اعمال میکند؛ برای اینکار نیازی به کد نویسی اضافهتری نیست و خودکار است. برای آزمایش آن فقط کافی است یک break point را داخل کلاس RazorGeneratorMvcStart افزونه قرار دهید.
اما ... پس از اجرا، بلافاصله پیام تداخل فضاهای نام را دریافت میکنید. خطاهای حاصل عنوان میکند که در App domain جاری، دو کنترلر Home وجود دارند؛ یکی در پروژهی اصلی و دیگری در پروژهی افزونه و مشخص نیست که مسیریابیها باید به کدامیک ختم شوند.
برای رفع این مشکل، به فایل NewsAreaAreaRegistration.cs پروژهی افزونه مراجعه کرده و مسیریابی آنرا به نحو ذیل تکمیل کنید تا فضای نام اختصاصی این Area صریحا مشخص گردد.
using System.Web.Mvc; namespace MvcPluginMasterApp.Plugin1.Areas.NewsArea { public class NewsAreaAreaRegistration : AreaRegistration { public override string AreaName { get { return "NewsArea"; } } public override void RegisterArea(AreaRegistrationContext context) { context.MapRoute( "NewsArea_default", "NewsArea/{controller}/{action}/{id}", // تکمیل نام کنترلر پیش فرض new { controller = "Home", action = "Index", id = UrlParameter.Optional }, // مشخص کردن فضای نام مرتبط جهت جلوگیری از تداخل با سایر قسمتهای برنامه namespaces: new[] { string.Format("{0}.Controllers", this.GetType().Namespace) } ); } } }
using System.Web.Mvc; using System.Web.Routing; namespace MvcPluginMasterApp { public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }, // مشخص کردن فضای نام مرتبط جهت جلوگیری از تداخل با سایر قسمتهای برنامه namespaces: new[] { string.Format("{0}.Controllers", typeof(RouteConfig).Namespace) } ); } } }
طراحی قرارداد پایه افزونهها
تا اینجا با نحوهی تشکیل ساختار هر پروژهی افزونه آشنا شدیم. اما هر افزونه در آینده نیاز به مواردی مانند منوی اختصاصی در منوی اصلی سایت، تنظیمات مسیریابی اختصاصی، تنظیمات EF و امثال آن نیز خواهد داشت. به همین منظور، یک پروژهی class library جدید را به نام MvcPluginMasterApp.PluginsBase آغاز کنید.
سپس قرار داد IPlugin را به نحو ذیل به آن اضافه نمائید:
using System; using System.Reflection; using System.Web.Optimization; using System.Web.Routing; using StructureMap; namespace MvcPluginMasterApp.PluginsBase { public interface IPlugin { EfBootstrapper GetEfBootstrapper(); MenuItem GetMenuItem(RequestContext requestContext); void RegisterBundles(BundleCollection bundles); void RegisterRoutes(RouteCollection routes); void RegisterServices(IContainer container); } public class EfBootstrapper { /// <summary> /// Assemblies containing EntityTypeConfiguration classes. /// </summary> public Assembly[] ConfigurationsAssemblies { get; set; } /// <summary> /// Domain classes. /// </summary> public Type[] DomainEntities { get; set; } /// <summary> /// Custom Seed method. /// </summary> //public Action<IUnitOfWork> DatabaseSeeder { get; set; } } public class MenuItem { public string Name { set; get; } public string Url { set; get; } } }
PM> install-package EntityFramework PM> install-package Microsoft.AspNet.Web.Optimization PM> install-package structuremap.web
توضیحات قرار داد IPlugin
از این پس هر افزونه باید دارای کلاسی باشد که از اینترفیس IPlugin مشتق میشود. برای مثال فعلا کلاس ذیل را به افزونهی پروژه اضافه نمائید:
using System.Web.Mvc; using System.Web.Optimization; using System.Web.Routing; using MvcPluginMasterApp.PluginsBase; using StructureMap; namespace MvcPluginMasterApp.Plugin1 { public class Plugin1 : IPlugin { public EfBootstrapper GetEfBootstrapper() { return null; } public MenuItem GetMenuItem(RequestContext requestContext) { return new MenuItem { Name = "Plugin 1", Url = new UrlHelper(requestContext).Action("Index", "Home", new { area = "NewsArea" }) }; } public void RegisterBundles(BundleCollection bundles) { //todo: ... } public void RegisterRoutes(RouteCollection routes) { //todo: add custom routes. } public void RegisterServices(IContainer container) { // todo: add custom services. container.Configure(cfg => { //cfg.For<INewsService>().Use<EfNewsService>(); }); } } }
برای اینکه هر افزونه در منوی اصلی ظاهر شود، نیاز به یک نام، به همراه آدرسی به صفحهی اصلی آن خواهد داشت. به همین جهت در متد GetMenuItem نحوهی ساخت آدرسی را به اکشن متد Index کنترلر Home واقع در Areaایی به نام NewsArea، مشاهده میکنید.
بارگذاری و تشخیص خودکار افزونهها
پس از اینکه هر افزونه دارای کلاسی مشتق شده از قرارداد IPlugin شد، نیاز است آنها را به صورت خودکار یافته و سپس پردازش کنیم. اینکار را به کتابخانهی StructureMap واگذار خواهیم کرد. برای این منظور پروژهی جدیدی را به نام MvcPluginMasterApp.IoCConfig آغاز کرده و سپس تنظیمات آنرا به نحو ذیل تغییر دهید:
using System; using System.IO; using System.Threading; using System.Web; using MvcPluginMasterApp.PluginsBase; using StructureMap; using StructureMap.Graph; namespace MvcPluginMasterApp.IoCConfig { public static class SmObjectFactory { private static readonly Lazy<Container> _containerBuilder = new Lazy<Container>(defaultContainer, LazyThreadSafetyMode.ExecutionAndPublication); public static IContainer Container { get { return _containerBuilder.Value; } } private static Container defaultContainer() { return new Container(cfg => { cfg.Scan(scanner => { scanner.AssembliesFromPath( path: Path.Combine(HttpRuntime.AppDomainAppPath, "bin"), // یک اسمبلی نباید دوبار بارگذاری شود assemblyFilter: assembly => { return !assembly.FullName.Equals(typeof(IPlugin).Assembly.FullName); }); scanner.WithDefaultConventions(); //Connects 'IName' interface to 'Name' class automatically. scanner.AddAllTypesOf<IPlugin>().NameBy(item => item.FullName); }); }); } } }
PM> install-package EntityFramework PM> install-package structuremap.web
کاری که در کلاس SmObjectFactory انجام شده، بسیار ساده است. مسیر پوشهی Bin پروژهی اصلی به structuremap معرفی شدهاست. سپس به آن گفتهایم که تنها اسمبلیهایی را که دارای اینترفیس IPlugin هستند، به صورت خودکار بارگذاری کن. در ادامه تمام نوعهای IPlugin را نیز به صورت خودکار یافته و در مخزن تنظیمات خود، اضافه کن.
تامین نیازهای مسیریابی و Bundling هر افزونه به صورت خودکار
در ادامه به پروژهی اصلی مراجعه کرده و در پوشهی App_Start آن کلاس ذیل را اضافه کنید:
using System.Linq; using System.Web.Optimization; using System.Web.Routing; using MvcPluginMasterApp; using MvcPluginMasterApp.IoCConfig; using MvcPluginMasterApp.PluginsBase; [assembly: WebActivatorEx.PostApplicationStartMethod(typeof(PluginsStart), "Start")] namespace MvcPluginMasterApp { public static class PluginsStart { public static void Start() { var plugins = SmObjectFactory.Container.GetAllInstances<IPlugin>().ToList(); foreach (var plugin in plugins) { plugin.RegisterServices(SmObjectFactory.Container); plugin.RegisterRoutes(RouteTable.Routes); plugin.RegisterBundles(BundleTable.Bundles); } } } }
دراینجا با استفاده از کتابخانهای به نام WebActivatorEx (که باز هم توسط نویسندگان اصلی Razor Generator تهیه شدهاست)، یک متد PostApplicationStartMethod سفارشی را تعریف کردهایم.
مزیت استفاده از اینکار این است که فایل Global.asax.cs برنامه شلوغ نخواهد شد. در غیر اینصورت باید تمام این کدها را در انتهای متد Application_Start قرار میدادیم.
در اینجا با استفاده از structuremap، تمام افزونههای موجود به صورت خودکار بررسی شده و سپس پیشنیازهای مسیریابی و Bundling و همچنین تنظیمات IoC Container مورد نیاز آنها به هر افزونه به صورت مستقل، تزریق خواهد شد.
اضافه کردن منوهای خودکار افزونهها به پروژهی اصلی
پس از اینکه کار پردازش اولیهی IPluginها به پایان رسید، اکنون نوبت به نمایش آدرس اختصاصی هر افزونه در منوی اصلی سایت است. برای این منظور فایل جدیدی را به نام PluginsMenu.cshtml_، در پوشهی shared پروژهی اصلی اضافه کنید؛ با این محتوا:
@using MvcPluginMasterApp.IoCConfig @using MvcPluginMasterApp.PluginsBase @{ var plugins = SmObjectFactory.Container.GetAllInstances<IPlugin>().ToList(); } @foreach (var plugin in plugins) { var menuItem = plugin.GetMenuItem(this.Request.RequestContext); <li> <a href="@menuItem.Url">@menuItem.Name</a> </li> }
سپس به فایل Layout.cshtml_ پروژهی اصلی مراجعه و توسط فراخوانی Html.RenderPartial، آنرا در بین سایر آیتمهای منوی اصلی اضافه میکنیم:
<div class="navbar navbar-inverse navbar-fixed-top"> <div class="container"> <div class="navbar-header"> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse"> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> @Html.ActionLink("MvcPlugin Master App", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" }) </div> <div class="navbar-collapse collapse"> <ul class="nav navbar-nav"> <li>@Html.ActionLink("Master App/Home", "Index", "Home", new {area = ""}, null)</li> @{ Html.RenderPartial("_PluginsMenu"); } </ul> </div> </div> </div>
بنابراین به صورت خلاصه
1) هر افزونه، یک پروژهی کامل ASP.NET MVC است که پوشههای ریشهی اصلی آن حذف شدهاند و اطلاعات آن توسط یک Area جدید تامین میشوند.
2) تنظیم فضای نام مسیریابیهای تمام پروژهها را فراموش نکنید. در غیر اینصورت شاهد تداخل پردازش کنترلرهای هم نام خواهید بود.
3) جهت سهولت کار، میتوان فایلهای bin هر افزونه را توسط رخداد post-build، به پوشهی bin پروژهی اصلی کپی کرد.
4) Viewهای هر افزونه توسط Razor Generator در فایل dll آن مدفون خواهند شد.
5) هر افزونه باید دارای کلاسی باشد که اینترفیس IPlugin را پیاده سازی میکند. از این اینترفیس برای ثبت اطلاعات هر افزونه یا دریافت اطلاعات سفارشی از آن کمک میگیریم.
6) با استفاده از استراکچرمپ و قرارداد IPlugin، منوهای هر افزونه را به صورت خودکار یافته و سپس به فایل layout اصلی اضافه میکنیم.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید:
MvcPluginMasterApp-Part1.zip
روش غیرفعال کردن موقتی ReSharper
<asp:ScriptManager ID="ScriptManager" runat="server" ScriptMode="Release"> <CompositeScript> <Scripts> <asp:ScriptReference Path="~/Scripts/jquery-1.7.2.min.js" /> <asp:ScriptReference Path="~/Scripts/jquery.ui.core.min.js" /> <asp:ScriptReference Path="~/Scripts/jquery.ui.widget.min.js" /> <asp:ScriptReference Path="~/Scripts/jquery.ui.mouse.min.js" /> <asp:ScriptReference Path="~/Scripts/jquery.ui.sortable.min.js" /> </Scripts> </CompositeScript> </asp:ScriptManager>
علاوه بر آن حتما خاصیت ScriptMode کنترل ScriptManager را بر روی Release تنظیم نمایید تا از حداکثر کارایی و کش فایلها استفاده نمایید
دسترسی به ScriptManager در صفحات دیگر و الصاق فایلهای خاص
برخی از فایلهای Javascript فقط در صفحات خاصی استفاده شده اند و لازم نیست در هر صفحه بارگذاری شود برای این کار کافی است فایل مورد نظر را در صفحه خاص به ScriptManager اضافه نمایید. البته ابتدا لازم است به ScriptManager دسترسی داشته باشیم. کد زیر نحوه دسترسی به آن را نمایش داده است:
ScriptManager scriptManager = ScriptManager.GetCurrent(this.Page); scriptManager.CompositeScript.Scripts.Add(new ScriptReference("~/Scripts/jquery.json.min.js"));