مطالب دورهها
طراحی روابط و ارجاعات در RavenDB
در قسمتهای قبل، با پیش زمینهی ذهنی طراحی مدلهای RavenDB به همراه اصول مقدماتی کوئری نویسی آن آشنا شدیم. در این قسمت قصد داریم معادلهای روابط موجود در بانکهای اطلاعاتی رابطهای را در RavenDB و مطابق ذهنیت غیر رابطهای آن، مدلسازی کنیم و مثالهای بیشتری را بررسی نمائیم.
مدیریت روابط در RavenDB
یکی از اصول طراحی مدلها در RavenDB، مستقل بودن اسناد یا documents است. به این ترتیب کلیه اطلاعاتی که یک سند نیاز دارد، داخل همان سند ذخیره میشوند (به این نوع شیء، Root Aggregate هم گفته میشود). اما این اصل سبب نخواهد شد تا نتوان یا نباید ارتباطی را بین اسناد تعریف کرد. بنابراین سؤال مهم اینجا است که چه اطلاعات مرتبطی باید داخل یک سند ذخیره شوند و چه اطلاعاتی باید به سند دیگری ارجاع داده شوند. برای پاسخ به این سؤال سه روش ذیل را باید مدنظر داشت:
الف) Denormalized references
فرض کنید در دنیای رابطهای دو جدول سفارش و مشتری را دارید. در این حالت، جدول سفارش تنها شماره آی دی اطلاعات مشتری را از جدول مشتری یا کاربران سیستم، در خود ذخیره خواهد کرد. به این ترتیب از تکرار اطلاعات مشتری در جدول سفارشات جلوگیری میگردد. اما اگر اطلاعات پرکاربرد مشتری را در داخل جدول سفارش قرار دهیم به آن denormalized reference گفته میشود.
ایجاد denormalized reference یکی از روشهای مرسوم در دنیای NoSQL و RavenDB است؛ خصوصا جهت سهولت نمایش اطلاعات. به این ترتیب ارجاع به سندهای دیگر کمتر شده و ترافیک شبکه نیز کاهش مییابد. برای مثال در اینجا نام و آدرس مشتری را داخل سند ثبت شده قرار میدهیم و از سایر اطلاعات او (که اهمیت نمایشی ندارند) مانند کلمه عبور و امثال آن صرفنظر خواهیم کرد.
اینجا است که یک سری از سؤالات مطرح خواهند شد مانند : «اگر آدرس مشتری تغییر کرد، چطور؟»
بنابراین بهترین حالت استفاده از روش denormalized references محدود خواهد شد به موارد ذیل:
الف) قید اطلاعاتی که به ندرت تغییر میکنند. برای مثال نام یک شخص یا نام یک کشور، استان یا شهر.
ب) ثبت اطلاعات تکراری که در طول زمان تغییر میکنند، اما باید تاریخچهی آنها حفظ شوند. برای مثال اگر آدرس مشتری تغییر کرده است، واقعا اجناس سندهای قبلی او، صرفنظر از آدرس جدیدی که اعلام کرده است، به آدرس قبلی او ارسال شدهاند و این تاریخچه باید در سیستم حفظ شوند.
ج) اطلاعاتی که ممکن است بعدها حذف شوند؛ اما نیاز است سابقه اسناد قبلی تخریب نشوند. برای مثال کارخانهای را درنظر بگیرید که امسال یک سری چینی خاص را تولید میکند و میفروشد. سال بعد خط تولید خود را عوض کرده و سری اجناس دیگری را شروع به تولید و فروش خواهد کرد. در بانکهای اطلاعاتی رابطهای نمیتوان اجناسی را که در جداول دیگر ارجاع دارند، به این سادگیها حذف کرد. در اینجا باید از روشهایی مانند تعریف فیلد بیتی IsDeleted برای مخفی کردن ظاهری رکوردهای موجود کمک گرفت. اما در دنیای رابطهای، اطلاعات مهم محصول را در سند اصلی ثبت کنید. بعد هر زمانیکه نیازی به محصول نبود، کلا تعریف آنرا حذف نمائید.
ب) Includes
Includes در RavenDB برای پوشش مشکلات denormalization ارائه شده است. در اینجا بجای اینکه یک شیء کپی اطلاعات پرکاربرد شیءایی دیگر را در خود ذخیره کند، تنها ارجاعی (یک Id رشتهای) از آن شیء را در سند مرتبط ذخیره خواهد کرد.
برای نمونه در کلاس Order شاهد یک Id رشتهای ارجاع دهنده به کلاس Customer هستیم. هرگاه که نیاز به بارگذاری اطلاعات شیء Order به همراه کل اطلاعات مشتری او تنها در یک رفت و برگشت به بانک اطلاعاتی باشد، میتوان از متد الحاقی Include مختص RavenDB استفاده کرد:
همانطور که مشاهده میکنید، با ذکر متد Include، اعلام کردهایم که مایل هستیم تا اطلاعات سند مشتری متناظر را نیز داشته باشیم. در این حالت در Load بعدی که بر اساس Id مشتری انجام شده، دیگر رفت و برگشتی به سرور انجام نشده و اطلاعات مشتری از کش سشن جاری که پیشتر با فراخوانی Include مقدار دهی شده است، دریافت میگردد.
حتی میتوان چند سند مرتبط را با هم بارگذاری کرد؛ با حداقل رفت و برگشت به سرور:
همچنین امکان استفاده از متد Include در LINQ API نیز پیش بینی شده است. برای این منظور باید از متد Customize استفاده کرد:
Includeهای یک به چند
اکنون فرض کنید به کلاس سفارش، آرایه تامین کنندهها نیز افزوده شده است (رابطه یک به چند):
بارگذاری یکباره روابط یک به چند نیز با Include میسر است:
Includeهای چند سطحی
در اینجا کلاس سفارشی را در نظر بگیرید که دارای خاصیت ارجاع دهنده نیز هست. این خاصیت به شکل یک کلاس تعریف شده است و نه به شکل یک آی دی رشتهای:
متد Include امکان ارجاع به خواص تو در تو را نیز دارد:
همچنین این متد با مجموعهها نیز کار میکند. برای مثال اگر تعریف متد LineItem به صورت زیر باشد:
برای بارگذاری یکباره اسناد مرتبط میتوان به روش ذیل عمل کرد:
و به صورت خلاصه برای باگذاری اسناد مرتبط، دیگر از دو کوئری پشت سر هم ذیل استفاده نکنید:
این دو کوئری یعنی دوبار رفت و برگشت به سرور. با استفاده از Include میتوان تعداد رفت و برگشتها و همچنین ترافیک شبکه را کاهش داد. به علاوه سرعت کار نیز افزایش خواهد یافت.
ج) تفاوت بین Reference و Relationship
برای درک اینکه آیا اطلاعات یک شیء مرتبط را بهتر است داخل شیء اصلی (Aggregate rooe) ذخیره کرد یا خیر، باید مفاهیم ارجاع و ارتباط را بررسی کنیم.
اگر به مثال سفارش و مشتری دقت کنیم، یک سفارش را بدون مشتری نیز میتوان تکمیل کرد. برای مثال بسیاری از فروشگاهها به همین نحو عمل میکنند و اگر شماره Id مشتری را به سندی اضافه میکنیم، صرفا جهت این است که بدانیم این سند متعلق به شخص دیگری نیست. بنابراین «ارجاعی» به کاربر در جدول سفارش میتواند وجود داشته باشد.
اکنون اقلام سفارش را درنظر بگیرید. هر آیتم سفارش تنها با بودن آن سفارش خاص است که معنا پیدا میکنند و نه بدون آن. این آیتم میتواند ارجاعی به محصول مرتبط داشته باشد. اینجا است که میگوییم اقلام سند با سفارش «در ارتباط» هستند؛ اما یک سند ارجاعی دارد به مشتری.
از این دو مفهوم برای تشخیص تشکیل Root Aggregate استفاده میشود. به این ترتیب تشخیص دادهایم اقلام سند، Root Aggregate را تشکیل میدهند؛ بنابراین ذخیره سازی تمام آنها داخل یک سند RavenDB معنا پیدا میکند.
چند مثال برای درک بهتر نحوه طراحی اسناد در RavenDB
الف) Stackoverflow
صفحه نمایش یک سؤال و پاسخهای آن و همچنین رایهای هر آیتم را درنظر بگیرید. در اینجا کاربران همزمانی ممکن است به یک سؤال رای بدهند، پاسخهایی را ارائه دهند و یا کاربر اصلی، سؤال خویش را ویرایش کند. به این ترتیب با قرار دادن کلیه آیتمهای این سند داخل آن، به مشکلات همزمانی برخواهیم خورد. برای مثال واقعا نمیخواهیم که به علت افزوده شدن یک پاسخ، کل سند قفل شود.
بنابراین ذخیره سازی سؤال در یک سند و ذخیره سازی لیست پاسخها در سندی دیگر، طراحی بهتری خواهد بود.
ب) سبد خرید و آیتمهای آن
زمانیکه کاربری مشغول به خرید آنلاین از سایتی میشود، لیست اقلام انتخابی او یک سفارش را تشکیل داده و به تنهایی معنا پیدا نمیکنند. به همین جهت ذخیره سازی اقلام سفارش به صورت یک Root aggregate در اینجا مفهوم داشته و متداول است.
ج) یک بلاگ و کامنتهای آن
در اینجا نیز کاربران، مجزای از مطلب اصلی ارسال شده ممکن است نظرات خود را ویرایش کنند یا اینکه بخواهیم نظرات را جداگانه لیست کنیم. بنابراین این دو (مطالب و نظرات) موضوعاتی جداگانه بوده و نیازی نیست به صورت یک Root aggregate تعریف شوند.
بنابراین در حین طراحی اسناد NoSQL باید به اعمال و «محدودههای تراکنشی» انجام شده دقت داشت تا اینکه صرفا عنوان شود این یک رابطه یک به چند یا چند به چند است.
مدیریت روابط در RavenDB
یکی از اصول طراحی مدلها در RavenDB، مستقل بودن اسناد یا documents است. به این ترتیب کلیه اطلاعاتی که یک سند نیاز دارد، داخل همان سند ذخیره میشوند (به این نوع شیء، Root Aggregate هم گفته میشود). اما این اصل سبب نخواهد شد تا نتوان یا نباید ارتباطی را بین اسناد تعریف کرد. بنابراین سؤال مهم اینجا است که چه اطلاعات مرتبطی باید داخل یک سند ذخیره شوند و چه اطلاعاتی باید به سند دیگری ارجاع داده شوند. برای پاسخ به این سؤال سه روش ذیل را باید مدنظر داشت:
الف) Denormalized references
فرض کنید در دنیای رابطهای دو جدول سفارش و مشتری را دارید. در این حالت، جدول سفارش تنها شماره آی دی اطلاعات مشتری را از جدول مشتری یا کاربران سیستم، در خود ذخیره خواهد کرد. به این ترتیب از تکرار اطلاعات مشتری در جدول سفارشات جلوگیری میگردد. اما اگر اطلاعات پرکاربرد مشتری را در داخل جدول سفارش قرار دهیم به آن denormalized reference گفته میشود.
ایجاد denormalized reference یکی از روشهای مرسوم در دنیای NoSQL و RavenDB است؛ خصوصا جهت سهولت نمایش اطلاعات. به این ترتیب ارجاع به سندهای دیگر کمتر شده و ترافیک شبکه نیز کاهش مییابد. برای مثال در اینجا نام و آدرس مشتری را داخل سند ثبت شده قرار میدهیم و از سایر اطلاعات او (که اهمیت نمایشی ندارند) مانند کلمه عبور و امثال آن صرفنظر خواهیم کرد.
اینجا است که یک سری از سؤالات مطرح خواهند شد مانند : «اگر آدرس مشتری تغییر کرد، چطور؟»
بنابراین بهترین حالت استفاده از روش denormalized references محدود خواهد شد به موارد ذیل:
الف) قید اطلاعاتی که به ندرت تغییر میکنند. برای مثال نام یک شخص یا نام یک کشور، استان یا شهر.
ب) ثبت اطلاعات تکراری که در طول زمان تغییر میکنند، اما باید تاریخچهی آنها حفظ شوند. برای مثال اگر آدرس مشتری تغییر کرده است، واقعا اجناس سندهای قبلی او، صرفنظر از آدرس جدیدی که اعلام کرده است، به آدرس قبلی او ارسال شدهاند و این تاریخچه باید در سیستم حفظ شوند.
ج) اطلاعاتی که ممکن است بعدها حذف شوند؛ اما نیاز است سابقه اسناد قبلی تخریب نشوند. برای مثال کارخانهای را درنظر بگیرید که امسال یک سری چینی خاص را تولید میکند و میفروشد. سال بعد خط تولید خود را عوض کرده و سری اجناس دیگری را شروع به تولید و فروش خواهد کرد. در بانکهای اطلاعاتی رابطهای نمیتوان اجناسی را که در جداول دیگر ارجاع دارند، به این سادگیها حذف کرد. در اینجا باید از روشهایی مانند تعریف فیلد بیتی IsDeleted برای مخفی کردن ظاهری رکوردهای موجود کمک گرفت. اما در دنیای رابطهای، اطلاعات مهم محصول را در سند اصلی ثبت کنید. بعد هر زمانیکه نیازی به محصول نبود، کلا تعریف آنرا حذف نمائید.
ب) Includes
Includes در RavenDB برای پوشش مشکلات denormalization ارائه شده است. در اینجا بجای اینکه یک شیء کپی اطلاعات پرکاربرد شیءایی دیگر را در خود ذخیره کند، تنها ارجاعی (یک Id رشتهای) از آن شیء را در سند مرتبط ذخیره خواهد کرد.
public class Order { public string CustomerId { get; set; } public LineItem[] LineItems { get; set; } public double TotalPrice { get; set; } } public class Customer { public string Name { get; set; } public string Address { get; set; } public short Age { get; set; } public string HashedPassword { get; set; } }
var order = session.Include<Order>(x => x.CustomerId) .Load("orders/1234"); // این کوئری از کش سشن خوانده میشود و کاری به سرور ندارد var cust = session.Load<Customer>(order.CustomerId);
حتی میتوان چند سند مرتبط را با هم بارگذاری کرد؛ با حداقل رفت و برگشت به سرور:
var orders = session.Include<Order>(x => x.CustomerId) .Load("orders/1234", "orders/4321"); foreach (var order in orders) { // این کوئریها سمت کلاینت هستند و به سرور ارسال نمیشوند var cust = session.Load<Customer>(order.CustomerId); }
var orders = session.Query<Order>() .Customize(x => x.Include<Order>(o => o.CustomerId)) .Where(x => x.TotalPrice > 100) .ToList(); foreach (var order in orders) { // این کوئریها سمت کلاینت اجرا میشوند var cust = session.Load<Customer>(order.CustomerId); }
Includeهای یک به چند
اکنون فرض کنید به کلاس سفارش، آرایه تامین کنندهها نیز افزوده شده است (رابطه یک به چند):
public class Order { public string CustomerId { get; set; } public string[] SupplierIds { get; set; } public LineItem[] LineItems { get; set; } public double TotalPrice { get; set; } }
var orders = session.Include<Order>(x => x.SupplierIds) .Load("orders/1234", "orders/4321"); foreach (var order in orders) { foreach (var supplierId in order.SupplierIds) { // از کش سشن خوانده میشود var supp = session.Load<Supplier>(supplierId); } }
Includeهای چند سطحی
در اینجا کلاس سفارشی را در نظر بگیرید که دارای خاصیت ارجاع دهنده نیز هست. این خاصیت به شکل یک کلاس تعریف شده است و نه به شکل یک آی دی رشتهای:
public class Order { public string CustomerId { get; set; } public string[] SupplierIds { get; set; } public Referral Refferal { get; set; } public LineItem[] LineItems { get; set; } public double TotalPrice { get; set; } } public class Referral { public string CustomerId { get; set; } public double CommissionPercentage { get; set; } }
var order = session.Include<Order>(x => x.Refferal.CustomerId) .Load("orders/1234"); // از کش سشن خوانده میشود var referrer = session.Load<Customer>(order.Refferal.CustomerId);
public class LineItem { public string ProductId { get; set; } public string Name { get; set; } public int Quantity { get; set; } public double Price { get; set; } }
var order = session.Include<Order>(x => x.LineItems.Select(li => li.ProductId)) .Load("orders/1234"); foreach (var lineItem in order.LineItems) { // از کش سمت کلاینت خوانده میشود var product = session.Load<Product>(lineItem.ProductId); }
و به صورت خلاصه برای باگذاری اسناد مرتبط، دیگر از دو کوئری پشت سر هم ذیل استفاده نکنید:
var order = session.Load<Order>("orders/1"); var customer = session.Load<Customer>(order.CustomerId);
ج) تفاوت بین Reference و Relationship
برای درک اینکه آیا اطلاعات یک شیء مرتبط را بهتر است داخل شیء اصلی (Aggregate rooe) ذخیره کرد یا خیر، باید مفاهیم ارجاع و ارتباط را بررسی کنیم.
اگر به مثال سفارش و مشتری دقت کنیم، یک سفارش را بدون مشتری نیز میتوان تکمیل کرد. برای مثال بسیاری از فروشگاهها به همین نحو عمل میکنند و اگر شماره Id مشتری را به سندی اضافه میکنیم، صرفا جهت این است که بدانیم این سند متعلق به شخص دیگری نیست. بنابراین «ارجاعی» به کاربر در جدول سفارش میتواند وجود داشته باشد.
اکنون اقلام سفارش را درنظر بگیرید. هر آیتم سفارش تنها با بودن آن سفارش خاص است که معنا پیدا میکنند و نه بدون آن. این آیتم میتواند ارجاعی به محصول مرتبط داشته باشد. اینجا است که میگوییم اقلام سند با سفارش «در ارتباط» هستند؛ اما یک سند ارجاعی دارد به مشتری.
از این دو مفهوم برای تشخیص تشکیل Root Aggregate استفاده میشود. به این ترتیب تشخیص دادهایم اقلام سند، Root Aggregate را تشکیل میدهند؛ بنابراین ذخیره سازی تمام آنها داخل یک سند RavenDB معنا پیدا میکند.
چند مثال برای درک بهتر نحوه طراحی اسناد در RavenDB
الف) Stackoverflow
صفحه نمایش یک سؤال و پاسخهای آن و همچنین رایهای هر آیتم را درنظر بگیرید. در اینجا کاربران همزمانی ممکن است به یک سؤال رای بدهند، پاسخهایی را ارائه دهند و یا کاربر اصلی، سؤال خویش را ویرایش کند. به این ترتیب با قرار دادن کلیه آیتمهای این سند داخل آن، به مشکلات همزمانی برخواهیم خورد. برای مثال واقعا نمیخواهیم که به علت افزوده شدن یک پاسخ، کل سند قفل شود.
بنابراین ذخیره سازی سؤال در یک سند و ذخیره سازی لیست پاسخها در سندی دیگر، طراحی بهتری خواهد بود.
ب) سبد خرید و آیتمهای آن
زمانیکه کاربری مشغول به خرید آنلاین از سایتی میشود، لیست اقلام انتخابی او یک سفارش را تشکیل داده و به تنهایی معنا پیدا نمیکنند. به همین جهت ذخیره سازی اقلام سفارش به صورت یک Root aggregate در اینجا مفهوم داشته و متداول است.
ج) یک بلاگ و کامنتهای آن
در اینجا نیز کاربران، مجزای از مطلب اصلی ارسال شده ممکن است نظرات خود را ویرایش کنند یا اینکه بخواهیم نظرات را جداگانه لیست کنیم. بنابراین این دو (مطالب و نظرات) موضوعاتی جداگانه بوده و نیازی نیست به صورت یک Root aggregate تعریف شوند.
بنابراین در حین طراحی اسناد NoSQL باید به اعمال و «محدودههای تراکنشی» انجام شده دقت داشت تا اینکه صرفا عنوان شود این یک رابطه یک به چند یا چند به چند است.
Ajax.BeginForm در ASP.NET MVC از jQuery Ajax برای ارسال مقادیر فرم، به سرور استفاده میکند. در این بین اگر یکی از عناصر فرم، المان ارسال فایل به سرور باشد، مقدار دریافتی در سمت سرور نال خواهد بود. مشکل اینجا است که نمیتوان به کمک Ajax معمولی (یا به عبارتی XMLHttpRequest) فایلی را به سرور ارسال کرد. یا باید از سیلورلایت یا فلش استفاده نمود و یا از مرورگرهایی که XMLHttpRequest Level 2 را پشتیبانی میکنند (از IE 10 به بعد مثلا) که امکان Ajax upload توکار به همراه گزارش درصد آپلود را بدون نیاز به فلش یا سیلورلایت، دارند.
در این بین راه حل دیگری نیز وجود دارد که با تمام مرورگرها سازگار است؛ اما تنها گزارش درصد آپلود را توسط آن نخواهیم داشت. در اینجا به صورت پویا یک IFrame مخفی در صفحه تشکیل میشود، مقادیر معمولی فرم (تمام المانها، منهای file) به صورت Ajax ایی به سرور ارسال خواهند شد. المان file آن در این IFrame مخفی، به صورت معمولی به سرور Postback میشود. البته کاربر در این بین چیزی را مشاهده یا احساس نخواهد کرد و تمام عملیات از دیدگاه او Ajax ایی به نظر میرسد. برای انجام اینکار تنها کافی است از افزونهی AjaxFileUpload استفاده کنیم که در ادامه نحوهی استفاده از آنرا بررسی خواهیم کرد.
پیشنیازها
در ادامه فرض بر این است که افزونهی AjaxFileUpload را دریافت کرده و به فایل Layout برنامه افزودهاید:
مدل، کنترلر و View برنامه
مدل برنامه مشخصات یک محصول است:
کنترلر آن از سه متد تشکیل شدهاست:
Index اول، کار نمایش صفحهی ارسال اطلاعات را انجام خواهد داد.
Index دوم کار پردازش Ajax ایی اطلاعات ارسالی به سرور را به عهده دارد. HttpPost آن Ajax ایی است.
متد UploadFiles، کار پردازش اطلاعات ارسالی از طرف IFrame مخفی را انجام میدهد. HttpPost آن معمولی است.
و کدهای View این مثال نیز به شرح زیر است:
فرمی که توسط Ajax.BeginForm تشکیل شدهاست، یک فرم معمولی Ajax ایی است و نکتهی جدیدی ندارد. تنها در آن یک المان ارسال فایل قرار گرفتهاست و همچنین Id آنرا نیز جهت استفاده توسط jQuery مشخص کردهایم.
در ادامه نحوهی فعال سازی ajaxFileUpload را دقیقا در زمان submit فرم، مشاهده میکنید. در اینجا url آن به اکشن متدی که اطلاعات المان file را باید دریافت کند، اشاره میکند. fileElementId آن مساوی Id المان فایل فرم Ajax ایی صفحهاست. از قسمت data جهت ارسال اطلاعات اضافهتری به اکشن متد UploadFiles استفاده میشود. سایر قسمتهای آن نیز مشخص هستند. اگر عملیات موفقیت آمیز بود، success آن و اگر خیر، error آن اجرا میشوند.
فقط باید دقت داشت که content type دریافتی توسط آن باید text/html باشد، که این مورد در اکشن متدهای کنترلر مشخص هستند.
به این ترتیب دیگر کاربر نیازی ندارد ابتدا یکبار بر روی دکمهی دومی کلیک کرده و فایل را ارسال کند و سپس بار دیگر بر روی دکمهی submit فرم کلیک نماید. هر دو کار توسط یک دکمه انجام میشوند.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید
MVCAjaxFormUpload.zip
در این بین راه حل دیگری نیز وجود دارد که با تمام مرورگرها سازگار است؛ اما تنها گزارش درصد آپلود را توسط آن نخواهیم داشت. در اینجا به صورت پویا یک IFrame مخفی در صفحه تشکیل میشود، مقادیر معمولی فرم (تمام المانها، منهای file) به صورت Ajax ایی به سرور ارسال خواهند شد. المان file آن در این IFrame مخفی، به صورت معمولی به سرور Postback میشود. البته کاربر در این بین چیزی را مشاهده یا احساس نخواهد کرد و تمام عملیات از دیدگاه او Ajax ایی به نظر میرسد. برای انجام اینکار تنها کافی است از افزونهی AjaxFileUpload استفاده کنیم که در ادامه نحوهی استفاده از آنرا بررسی خواهیم کرد.
پیشنیازها
در ادامه فرض بر این است که افزونهی AjaxFileUpload را دریافت کرده و به فایل Layout برنامه افزودهاید:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>@ViewBag.Title - My ASP.NET Application</title> <link href="~/Content/Site.css" rel="stylesheet" type="text/css" /> </head> <body> <div> @RenderBody() </div> <script src="~/Scripts/jquery-1.11.1.min.js"></script> <script src="~/Scripts/jquery.unobtrusive-ajax.js"></script> <script src="~/Scripts/ajaxfileupload.js"></script> @RenderSection("Scripts", required: false) </body> </html>
مدل، کنترلر و View برنامه
مدل برنامه مشخصات یک محصول است:
namespace MVCAjaxFormUpload.Models { public class Product { public int Id { set; get; } public string Name { set; get; } } }
کنترلر آن از سه متد تشکیل شدهاست:
using System.Threading; using System.Web; using System.Web.Mvc; using MVCAjaxFormUpload.Models; namespace MVCAjaxFormUpload.Controllers { public class HomeController : Controller { public ActionResult Index() { return View(); } [HttpPost] public ActionResult Index(Product product) { var isAjax = this.Request.IsAjaxRequest(); return Json(new { result = "ok" }, JsonRequestBehavior.AllowGet); } [HttpPost] public ActionResult UploadFiles(HttpPostedFileBase image1, int id) { var isAjax = this.Request.IsAjaxRequest(); Thread.Sleep(3000); //شبیه سازی عملیات طولانی return Json(new { FileName = "/Uploads/filename.ext" }, "text/html", JsonRequestBehavior.AllowGet); } } }
Index دوم کار پردازش Ajax ایی اطلاعات ارسالی به سرور را به عهده دارد. HttpPost آن Ajax ایی است.
متد UploadFiles، کار پردازش اطلاعات ارسالی از طرف IFrame مخفی را انجام میدهد. HttpPost آن معمولی است.
و کدهای View این مثال نیز به شرح زیر است:
@model MVCAjaxFormUpload.Models.Product @{ ViewBag.Title = "Index"; } <h2>Ajax Form Upload</h2> @using (Ajax.BeginForm(actionName: "Index", controllerName: "Home", ajaxOptions: new AjaxOptions { HttpMethod = "POST" }, routeValues: null, htmlAttributes: new { id = "uploadForm" })) { <label>Name:</label> @Html.TextBoxFor(model => model.Name) <br /> <label>Image:</label> <br /> <input type="file" name="Image1" id="Image1" /> <br /> <input type="submit" value="Submit" /> <img id="loading" src="~/Content/Images/loading.gif" style="display:none;"> } @section Scripts { <script type="text/javascript"> $(function () { $('#uploadForm').submit(function () { $("#loading").show(); $.ajaxFileUpload({ url: "@Url.Action("UploadFiles", "Home")", // مسیری که باید فایل به آن ارسال شود secureuri: false, fileElementId: 'Image1', // آی دی المان ورودی فایل dataType: 'json', data: { id: 1, data: 'test' }, // اطلاعات اضافی در صورت نیاز success: function (data, status) { $("#loading").hide(); if (typeof (data.FileName) != 'undefined') { alert(data.FileName); } }, error: function (data, status, e) { $("#loading").hide(); alert(e); } }); }); }); </script> }
در ادامه نحوهی فعال سازی ajaxFileUpload را دقیقا در زمان submit فرم، مشاهده میکنید. در اینجا url آن به اکشن متدی که اطلاعات المان file را باید دریافت کند، اشاره میکند. fileElementId آن مساوی Id المان فایل فرم Ajax ایی صفحهاست. از قسمت data جهت ارسال اطلاعات اضافهتری به اکشن متد UploadFiles استفاده میشود. سایر قسمتهای آن نیز مشخص هستند. اگر عملیات موفقیت آمیز بود، success آن و اگر خیر، error آن اجرا میشوند.
فقط باید دقت داشت که content type دریافتی توسط آن باید text/html باشد، که این مورد در اکشن متدهای کنترلر مشخص هستند.
به این ترتیب دیگر کاربر نیازی ندارد ابتدا یکبار بر روی دکمهی دومی کلیک کرده و فایل را ارسال کند و سپس بار دیگر بر روی دکمهی submit فرم کلیک نماید. هر دو کار توسط یک دکمه انجام میشوند.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید
MVCAjaxFormUpload.zip
اشتراکها
یک سیستم گرید مبتنی بر ویژگی FlexBox
اشتراکها
سیستم عامل Firefox
من قصد دارم در قالب چند مطلب برخی از مفاهیم پایه و مهم برنامه نویسی را که پیش نیازی برای درک اکثر مطالب موجود در وب سایت است به زبان ساده بیان کنم تا دایره افرادی که میتوانند از مطالب ارزشمند این وب سایت استفاده کنند وسعت بیشتری پیدا کند. لازم به توضیح است از آنجا که علاقه ندارم اینجا تبدیل به نسخه فارسی MSDN یا کتاب آنلاین آموزش برنامه نویسی شود این سری آموزشها بیشتر شامل مفاهیم کلیدی خواهند بود.
این مطلب به عنوان اولین بخش از این سری مطالب منتشر میشود.
هدف این نوشته بررسی جزییات برنامه نویسی در رابطه با کلاس و شیء نیست. بلکه دریافتن چگونگی شکل گرفتن ایده شیء گرایی و علت مفید بودن آن است.
در سمت راست بخشی از نقشه یک ساختمان و در سمت چپ ساختمان ساخته شده بر اساس این نقشه را میبینید. ساختمان همان شیء است. و نقشه ساختمان کلاس آن است چراکه امکان ایجاد اشیائی که تحت عنوان ساختمان طبقه بندی (کلاس بندی) میشوند را فراهم میکند. به همین سادگی. کلاسها طرح اولیه، نقشه یا قالبی هستند که جزییات یک شی را توصیف میکنند.
حتماً با من موافق هستید اگر بگویم:
سوال: کلاس یا نقشه ایجاد گاو چیست؟ اگر از من بپرسید خواهم گفت طرح اولیه گاو هم ممکن است وجود داشته باشد البته در اختیار خداوند و با سطح دسترسی ملکوت!
اتومبیل، تلویزیون و ... همگی مثال هایی از اشیاء پیرامون ما در دنیای واقعی هستند که حتماً میتوانید کلاس یا نقشه ایجاد آنها را نیز بدست آورید و یا ویژگیها و کارکردهای آنها را برشمارید.
برای نوشتن برنامه جهت حل یک مسئله بزرگ باید بتوان آن مسئله را به بخشهای کوچکتری تقسیم نمود. در این رابطه مفهوم شیء و کلاس با همان کیفیتی که در محیط پیرامون ما وجود دارد به صورت مناسبی امکان تقسیم یه مسئله بزرگ به بخشهای کوچکتر را فراهم میکند. و سبب میشود هماهنگی و تقارن و تناظر خاصی بین اشیاء برنامه و دنیای واقعی بوجود آید که یکی از مزایای اصلی روش شیء گراست.
از آنجا که در یک برنامه اصولاً همه چیز و همه مفاهیم در قالب کدها و دستورات برنامه معنا دارد، کلاس و شیء نیز چیزی بیش از قطعاتی کد نیستند. قطعه کد هایی که بسته بندی شده اند تا تمام کار مربوط به هدفی که برای آنها در نظر گرفته شده است را انجام دهند.
همان طور که در هر زبان برنامه نویسی دستوراتی برای کارهای مختلف مانند تعریف یک متغیر یا ایجاد یک حلقه و ... در نظر گرفته شده است، در زبانهای برنامه نویسی شیء گرا نیز دستوراتی وجود دارد تا بتوان قطعه کدی را بر اساس مفهوم کلاس بسته بندی کرد.
به طور مثال قطعه کد زیر را در زبان برنامه نویسی سی شارپ در نظر بگیرید.
در این قطعه کد با استفاده از کلمه کلیدی class در زبان سی شارپ کلاسی ایجاد شده است که دارای دو ویژگی نام و سن و دو رفتار راه رفتن و دویدن است.
این کلاس به چه دردی میخورد؟ کجا میتوانیم از این کلاس استفاده کنیم؟
پاسخ این است که این کلاس ممکن است برای ما هیچ سودی نداشته باشد و هیچ کجا نتوانیم از آن استفاده کنیم. اما بیایید فرض کنیم برنامه نویسی هستیم که قصد داریم یک بازی فوتبال بنویسیم. به جای آنکه قطعات کد مجزایی برای هر یک از بازیکنان و کنترل رفتار و ویژگیهای آنان بنویسیم با اندکی تفکر به این نکته پی میبریم که همه بازیکنان مشترکات بسیاری دارند و به عبارتی در یک گروه یا کلاس قابل دسته بندی هستند. پس سعی میکنیم نقشه یا قالبی برای بازیکنها ایجاد کنیم که دربردارنده ویژگیها و رفتارهای آنها باشد.
همان طور که در نقشه ساختمان نمیتوانیم زندگی کنیم این کلاس هم هنوز آماده انجام کارهای واقعی نیست. چراکه برخی مقادیر هنوز برای آن تنظیم نشده است. مانند نام بازیکن و سن و ....
و همان طور که برای سکونت لازم است ابتدا یک ساختمان از روی نقشه ساختمان بسازیم برای استفاده واقعی از کلاس یاد شده نیز باید از روی آن شیء بسازیم. به این فرآیند وهله سازی یا نمونه سازی نیز میگویند. یک زبان برنامه نویسی شیء گرا دستوراتی را برای وهله سازی نیز در نظر گرفته است. در C# کلمه کلیدی new این وظیفه را به عهده دارد.
وقتی فرآیند وهله سازی صورت میگیرد یک نمونه یا شیء از آن کلاس در حافظه ساخته میشود که در حقیقت میتوانید آنرا همان کدهای کلاس تصور کنید با این تفاوت که مقداردهیهای لازم صورت گرفته است. به دلیل تعیین مقادیر لازم، حال شیء تولید شده میتواند به خوبی اعمال پیش بینی شده را انجام دهد. توجه نمایید در اینجا پیاده سازی داخلی رفتار دویدن و اینکه مثلاً در هنگام فراخوانی آن چه کدی باید اجرا شود تا تصویر یک بازیکن در حال دویدن در بازی نمایش یابد مد نظر و موضوع بحث ما نیست. بحث ما چگونگی سازماندهی کدها توسط مفهوم کلاس و شیء است. همان طور که مشاهده میکنید ما تمام جزییات بازیکنها را یکبار در کلاس پیاده سازی کرده ایم اما به تعداد دلخواه میتوانیم از روی آن بازیکنهای مختلف را ایجاد کنیم. همچنین به راحتی رفتار دویدن یک بازیکن را فراخوانی میکنیم بدون آنکه پیاده سازی کامل آن در اختیار و جلوی چشم ما باشد.
تمام آنچه که بازیکن برای انجام امور مربوط به خود نیاز دارد در کلاس بازیکن کپسوله میشود. بدیهی است در یک برنامه واقعی ویژگیها و رفتارهای بسیار بیشتری باید برای کلاس بازیکن در نظر گرفته شود. مانند پاس دادن، شوت زدن و غیره.
به این ترتیب ما برای هر برنامه میتوانیم مسئله اصلی را به تعدادی مسئله کوچکتر تقسیم کنیم و وظیفه حل هر یک از مسائل کوچک را به یک شیء واگذار کنیم. و بر اساس اشیاء تشخیص داده شده کلاسهای مربوطه را بنویسیم. برنامه نویسی شیء گرا سبب میشود تا مسئله توسط تعدادی شیء که دارای نمونههای متناظری در دنیای واقعی هستند حل شود که این امر زیبایی و خوانایی و قابلیت نگهداری و توسعه برنامه را بهبود میدهد.
احتمالاً تاکنون متوجه شده اید که برای نگهداری ویژگیهای اشیاء از متغیرها و برای پیاده سازی رفتارها یا کارکردهای اشیاء از توابع استفاده میکنیم.
با توجه به این که هدف این مطلب بررسی مفهوم شیء گرائی بود و نه جزییات برنامه نویسی، بنابراین بیان برخی مفاهیم در این رابطه را که بیشتر در مهندسی نرم افزار معنا دارند تا در دنیای واقعی در مطالب بعدی بررسی میکنیم.
این مطلب به عنوان اولین بخش از این سری مطالب منتشر میشود.
هدف این نوشته بررسی جزییات برنامه نویسی در رابطه با کلاس و شیء نیست. بلکه دریافتن چگونگی شکل گرفتن ایده شیء گرایی و علت مفید بودن آن است.
مشاهده مفاهیم شیء گرایی در پیرامون خود
حتماً در دنیای برنامه نویسی شیء گرا بارها با کلمات کلاس و شیء روبرو شده اید. درک صحیح از این مفاهیم بسیار مهم و البته بسیار ساده است. کار را با یک مثال شروع میکنیم. به تصویر زیر نگاه کنید.در سمت راست بخشی از نقشه یک ساختمان و در سمت چپ ساختمان ساخته شده بر اساس این نقشه را میبینید. ساختمان همان شیء است. و نقشه ساختمان کلاس آن است چراکه امکان ایجاد اشیائی که تحت عنوان ساختمان طبقه بندی (کلاس بندی) میشوند را فراهم میکند. به همین سادگی. کلاسها طرح اولیه، نقشه یا قالبی هستند که جزییات یک شی را توصیف میکنند.
حتماً با من موافق هستید اگر بگویم:
- در نقشه ساختمان نمیتوانید زندگی کنید اما در خود ساختمان میتوانید.
- از روی یک نقشه میتوان به تعداد دلخواه ساختمان ساخت.
- هنگامی که در یک ساختمان زندگی میکنید نیازی نیست تا دقیقاً بدانید چگونه ساخته شده و مثلاً سیم کشی یا لوله کشیهای آن چگونه است! تنها کافیست بدانید برای روشن شدن لامپ باید کلید آن را بزنید.
- ساختمان دارای ویژگی هایی مانند متراژ، ضخامت دیوار، تعداد پنجره و ابعاد هر یک و ... است که در هنگام ساخت و بر اساس اطلاعات موجود در نقشه تعیین شده اند.
- ساختمان دارای کارکرد هایی است. مانند بالا و پایین رفتن آسانسور و یا باز و بسته شدن درب پارکینگ. هر یک از این کارکردها نیز بر اساس اطلاعات موجود در نقشه پیاده سازی و ساخته شده اند.
- ساختمان تمام اجزای لازم برای اینکه از آن بتوانیم استفاده کنیم و به عبارتی در آن بتوانیم زندگی کنیم را در خود دارد.
سوال: کلاس یا نقشه ایجاد گاو چیست؟ اگر از من بپرسید خواهم گفت طرح اولیه گاو هم ممکن است وجود داشته باشد البته در اختیار خداوند و با سطح دسترسی ملکوت!
اتومبیل، تلویزیون و ... همگی مثال هایی از اشیاء پیرامون ما در دنیای واقعی هستند که حتماً میتوانید کلاس یا نقشه ایجاد آنها را نیز بدست آورید و یا ویژگیها و کارکردهای آنها را برشمارید.
مفاهیم شیء گرایی در مهندسی نرم افزار
مفاهیمی که تاکنون در مورد دنیای واقعی مرور کردیم همان چیزی است که در دنیای برنامه نویسی ـ به عقیده من دنیای واقعیتر از دنیای واقعی ـ با آن سر و کار داریم. علت این امر آن است که اصولاً ایده روش برنامه نویسی شیء گرا با مشاهده محیط پیرامون ما به وجود آمده است.برای نوشتن برنامه جهت حل یک مسئله بزرگ باید بتوان آن مسئله را به بخشهای کوچکتری تقسیم نمود. در این رابطه مفهوم شیء و کلاس با همان کیفیتی که در محیط پیرامون ما وجود دارد به صورت مناسبی امکان تقسیم یه مسئله بزرگ به بخشهای کوچکتر را فراهم میکند. و سبب میشود هماهنگی و تقارن و تناظر خاصی بین اشیاء برنامه و دنیای واقعی بوجود آید که یکی از مزایای اصلی روش شیء گراست.
از آنجا که در یک برنامه اصولاً همه چیز و همه مفاهیم در قالب کدها و دستورات برنامه معنا دارد، کلاس و شیء نیز چیزی بیش از قطعاتی کد نیستند. قطعه کد هایی که بسته بندی شده اند تا تمام کار مربوط به هدفی که برای آنها در نظر گرفته شده است را انجام دهند.
همان طور که در هر زبان برنامه نویسی دستوراتی برای کارهای مختلف مانند تعریف یک متغیر یا ایجاد یک حلقه و ... در نظر گرفته شده است، در زبانهای برنامه نویسی شیء گرا نیز دستوراتی وجود دارد تا بتوان قطعه کدی را بر اساس مفهوم کلاس بسته بندی کرد.
به طور مثال قطعه کد زیر را در زبان برنامه نویسی سی شارپ در نظر بگیرید.
class Player { public string Name; public int Age; public void Walk() { // کدهای مربوط به پیاده سازی راه رفتن } public void Run() { // کدهای مربوط به پیاده سازی دویدن } }
این کلاس به چه دردی میخورد؟ کجا میتوانیم از این کلاس استفاده کنیم؟
پاسخ این است که این کلاس ممکن است برای ما هیچ سودی نداشته باشد و هیچ کجا نتوانیم از آن استفاده کنیم. اما بیایید فرض کنیم برنامه نویسی هستیم که قصد داریم یک بازی فوتبال بنویسیم. به جای آنکه قطعات کد مجزایی برای هر یک از بازیکنان و کنترل رفتار و ویژگیهای آنان بنویسیم با اندکی تفکر به این نکته پی میبریم که همه بازیکنان مشترکات بسیاری دارند و به عبارتی در یک گروه یا کلاس قابل دسته بندی هستند. پس سعی میکنیم نقشه یا قالبی برای بازیکنها ایجاد کنیم که دربردارنده ویژگیها و رفتارهای آنها باشد.
همان طور که در نقشه ساختمان نمیتوانیم زندگی کنیم این کلاس هم هنوز آماده انجام کارهای واقعی نیست. چراکه برخی مقادیر هنوز برای آن تنظیم نشده است. مانند نام بازیکن و سن و ....
و همان طور که برای سکونت لازم است ابتدا یک ساختمان از روی نقشه ساختمان بسازیم برای استفاده واقعی از کلاس یاد شده نیز باید از روی آن شیء بسازیم. به این فرآیند وهله سازی یا نمونه سازی نیز میگویند. یک زبان برنامه نویسی شیء گرا دستوراتی را برای وهله سازی نیز در نظر گرفته است. در C# کلمه کلیدی new این وظیفه را به عهده دارد.
Player objPlayer = new Player(); objPlayer.Name = “Ali Karimi”; objPlayer.Age = 30; objPlayer.Run();
تمام آنچه که بازیکن برای انجام امور مربوط به خود نیاز دارد در کلاس بازیکن کپسوله میشود. بدیهی است در یک برنامه واقعی ویژگیها و رفتارهای بسیار بیشتری باید برای کلاس بازیکن در نظر گرفته شود. مانند پاس دادن، شوت زدن و غیره.
به این ترتیب ما برای هر برنامه میتوانیم مسئله اصلی را به تعدادی مسئله کوچکتر تقسیم کنیم و وظیفه حل هر یک از مسائل کوچک را به یک شیء واگذار کنیم. و بر اساس اشیاء تشخیص داده شده کلاسهای مربوطه را بنویسیم. برنامه نویسی شیء گرا سبب میشود تا مسئله توسط تعدادی شیء که دارای نمونههای متناظری در دنیای واقعی هستند حل شود که این امر زیبایی و خوانایی و قابلیت نگهداری و توسعه برنامه را بهبود میدهد.
احتمالاً تاکنون متوجه شده اید که برای نگهداری ویژگیهای اشیاء از متغیرها و برای پیاده سازی رفتارها یا کارکردهای اشیاء از توابع استفاده میکنیم.
با توجه به این که هدف این مطلب بررسی مفهوم شیء گرائی بود و نه جزییات برنامه نویسی، بنابراین بیان برخی مفاهیم در این رابطه را که بیشتر در مهندسی نرم افزار معنا دارند تا در دنیای واقعی در مطالب بعدی بررسی میکنیم.
پیشنیاز
نحوه ذخیره شدن متن در فایلهای PDF
حتما نیاز است پیشنیاز فوق را یکبار مطالعه کنید تا علت خروجیهای متفاوتی را که در ادامه ملاحظه خواهید نمود، بهتر مشخص شوند. همچنین فایل PDF ایی که مورد بررسی قرار خواهد گرفت، همان فایلی است که توسط متد writePdf ذکر شده در پیشنیاز تهیه شده است.
دو کلاس متفاوت برای استخراج متن از فایلهای PDF در iTextSharp وجود دارند:
الف) SimpleTextExtractionStrategy
مثال فوق، متن موجود در تمام صفحات یک فایل PDF را در فایلهای txt جداگانهای ثبت میکند. برای نمونه اگر از PDF پیشنیاز یاد شده استفاده کنیم، خروجی آن به نحو زیر خواهد بود:
علت آن نیز پیشتر بررسی گردید. متن، در این فایل ویژه در مختصات خاصی ترسیم شده است. حاصل از دیدگاه خواننده نهایی بسیار خوانا است؛ اما خروجی hello world متنی جالبی از آن استخراج نمیشود. SimpleTextExtractionStrategy دقیقا بر اساس همان عملگرهای Tj و همچنین منابع صفحه، عبارات را یافته و سر هم میکند.
ب) LocationTextExtractionStrategy
همان مثال قبل را درنظر بگیرید، اینبار به شکل زیر:
کلاس LocationTextExtractionStrategy هوشمندتر عمل کرده و بر اساس عملگرهای هندسی یک فایل PDF، سعی میکند جملات و حروف را کنار هم قرار دهد و در نهایت خروجی متنی بهتری را تولید کند. برای نمونه اینبار خروجی متنی حاصل به صورت زیر خواهد بود:
این خروجی با آنچه که در صفحه نمایش داده میشود تطابق دارد.
استخراج متون فارسی از فایلهای PDF توسط iTextSharp
روشهای فوق با PDFهای فارسی هم کار میکنند اما خروجی حاصل آن مفهوم نیست و نیاز به پردازش ثانوی دارد. ابتدا مثال زیر را درنظر بگیرید:
از متد فوق، برای تولید یک فایل PDF که متنی فارسی را نمایش میدهد استفاده خواهیم کرد. اگر متد readPdf2 را که به همراه LocationTextExtractionStrategy تعریف شده است، بر روی فایل حاصل فراخوانی کنیم، خروجی آن به صورت زیر خواهد بود:
برای تبدیل آن به یونیکد خواهیم داشت:
اکنون خروجی ثبت شده در فایل متنی حاصل به صورت زیر است:
دقیقا به همان نحوی است که iTextSharp و اکثر تولید کنندههای PDF فارسی از آن استفاده میکنند و اصطلاحا چرخاندن حروف یا تولید Glyph mirrors صورت میگیرد. روشهای زیادی برای چرخاندن حروف وجود دارند. در ادامه از روشی استفاده خواهیم کرد که خود ویندوز در کارهای داخلیاش از آن استفاده میکند:
از کلاس فوق در هر برنامهای که راست به چپ را به نحو صحیحی پشتیبانی نمیکند، میتوان استفاده کرد؛ خصوصا برنامههای گرافیکی.
در اینجا برای اصلاح متد readPdf2 خواهیم داشت:
اگر خروجی متد اصلاح شده فوق را بررسی کنیم، دقیقا به «تست میشود» خواهیم رسید.
سؤال: آیا این روش با تمام PDFهای فارسی کار میکند؟
پاسخ: خیر! همانطور که در پیشنیاز مطلب جاری عنوان شد، در یک حالت خاص، PDF writer میتواند شماره Glyphها را کاملا عوض کرده و در فایل PDF نهایی ثبت کند. خروجی حاصل در برنامه Adobe reader خوانا است، چون نمایش را بر اساس اطلاعات هندسی Glyphها انجام میدهد؛ اما خروجی متنی آن به نوعی obfuscated است چون مثلا حرف A آن به کاراکتر مرسوم دیگری نگاشت شده است.
نحوه ذخیره شدن متن در فایلهای PDF
حتما نیاز است پیشنیاز فوق را یکبار مطالعه کنید تا علت خروجیهای متفاوتی را که در ادامه ملاحظه خواهید نمود، بهتر مشخص شوند. همچنین فایل PDF ایی که مورد بررسی قرار خواهد گرفت، همان فایلی است که توسط متد writePdf ذکر شده در پیشنیاز تهیه شده است.
دو کلاس متفاوت برای استخراج متن از فایلهای PDF در iTextSharp وجود دارند:
الف) SimpleTextExtractionStrategy
using System.Diagnostics; using System.IO; using iTextSharp.text; using iTextSharp.text.pdf; using iTextSharp.text.pdf.parser; namespace TestReaders { class Program { private static void readPdf1() { var reader = new PdfReader("test.pdf"); int intPageNum = reader.NumberOfPages; for (int i = 1; i <= intPageNum; i++) { var text = PdfTextExtractor.GetTextFromPage(reader, i, new SimpleTextExtractionStrategy()); File.WriteAllText("page-" + i + "-text.txt", text); } reader.Close(); } static void Main(string[] args) { readPdf1(); } } }
Test ld Wor llo He Hello People
ب) LocationTextExtractionStrategy
همان مثال قبل را درنظر بگیرید، اینبار به شکل زیر:
private static void readPdf2() { var reader = new PdfReader("test.pdf"); int intPageNum = reader.NumberOfPages; for (int i = 1; i <= intPageNum; i++) { var text = PdfTextExtractor.GetTextFromPage(reader, i, new LocationTextExtractionStrategy()); File.WriteAllText("page-" + i + "-text.txt", text); } reader.Close(); }
Test Hello World Hello People
استخراج متون فارسی از فایلهای PDF توسط iTextSharp
روشهای فوق با PDFهای فارسی هم کار میکنند اما خروجی حاصل آن مفهوم نیست و نیاز به پردازش ثانوی دارد. ابتدا مثال زیر را درنظر بگیرید:
static void writePdf2() { using (var document = new Document(PageSize.A4)) { var writer = PdfWriter.GetInstance(document, new FileStream("test.pdf", FileMode.Create)); document.Open(); FontFactory.Register("c:\\windows\\fonts\\tahoma.ttf"); var tahoma = FontFactory.GetFont("tahoma", BaseFont.IDENTITY_H); ColumnText.ShowTextAligned( canvas: writer.DirectContent, alignment: Element.ALIGN_CENTER, phrase: new Phrase("تست میشود", tahoma), x: 100, y: 100, rotation: 0, runDirection: PdfWriter.RUN_DIRECTION_RTL, arabicOptions: 0); } Process.Start("test.pdf"); }
ﺩﻮﺷﻲﻣ ﺖﺴﺗ
private static void readPdf2() { var reader = new PdfReader("test.pdf"); int intPageNum = reader.NumberOfPages; for (int i = 1; i <= intPageNum; i++) { var text = PdfTextExtractor.GetTextFromPage(reader, i, new LocationTextExtractionStrategy()); text = Encoding.UTF8.GetString(Encoding.UTF8.GetBytes(text)); File.WriteAllText("page-" + i + "-text.txt", text, Encoding.UTF8); } reader.Close(); }
ﺩﻮﺷﻲﻣ ﺖﺴﺗ
using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Runtime.InteropServices; using System.Security; namespace TestReaders { [SuppressUnmanagedCodeSecurity] class GdiMethods { [DllImport("GDI32.dll")] public static extern bool DeleteObject(IntPtr hgdiobj); [DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true)] public static extern uint GetCharacterPlacement(IntPtr hdc, string lpString, int nCount, int nMaxExtent, [In, Out] ref GcpResults lpResults, uint dwFlags); [DllImport("GDI32.dll")] public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj); } [StructLayout(LayoutKind.Sequential)] struct GcpResults { public uint lStructSize; [MarshalAs(UnmanagedType.LPTStr)] public string lpOutString; public IntPtr lpOrder; public IntPtr lpDx; public IntPtr lpCaretPos; public IntPtr lpClass; public IntPtr lpGlyphs; public uint nGlyphs; public int nMaxFit; } public class UnicodeCharacterPlacement { const int GcpReorder = 0x0002; GCHandle _caretPosHandle; GCHandle _classHandle; GCHandle _dxHandle; GCHandle _glyphsHandle; GCHandle _orderHandle; public Font Font { set; get; } public string Apply(string lines) { if (string.IsNullOrWhiteSpace(lines)) return string.Empty; return Apply(lines.Split('\n')).Aggregate((s1, s2) => s1 + s2); } public IEnumerable<string> Apply(IEnumerable<string> lines) { if (Font == null) throw new ArgumentNullException("Font is null."); if (!hasUnicodeText(lines)) return lines; var graphics = Graphics.FromHwnd(IntPtr.Zero); var hdc = graphics.GetHdc(); try { var font = (Font)Font.Clone(); var hFont = font.ToHfont(); var fontObject = GdiMethods.SelectObject(hdc, hFont); try { var results = new List<string>(); foreach (var line in lines) results.Add(modifyCharactersPlacement(line, hdc)); return results; } finally { GdiMethods.DeleteObject(fontObject); GdiMethods.DeleteObject(hFont); font.Dispose(); } } finally { graphics.ReleaseHdc(hdc); graphics.Dispose(); } } void freeResources() { _orderHandle.Free(); _dxHandle.Free(); _caretPosHandle.Free(); _classHandle.Free(); _glyphsHandle.Free(); } static bool hasUnicodeText(IEnumerable<string> lines) { return lines.Any(line => line.Any(chr => chr >= '\u00FF')); } void initializeResources(int textLength) { _orderHandle = GCHandle.Alloc(new int[textLength], GCHandleType.Pinned); _dxHandle = GCHandle.Alloc(new int[textLength], GCHandleType.Pinned); _caretPosHandle = GCHandle.Alloc(new int[textLength], GCHandleType.Pinned); _classHandle = GCHandle.Alloc(new byte[textLength], GCHandleType.Pinned); _glyphsHandle = GCHandle.Alloc(new short[textLength], GCHandleType.Pinned); } string modifyCharactersPlacement(string text, IntPtr hdc) { var textLength = text.Length; initializeResources(textLength); try { var gcpResult = new GcpResults { lStructSize = (uint)Marshal.SizeOf(typeof(GcpResults)), lpOutString = new String('\0', textLength), lpOrder = _orderHandle.AddrOfPinnedObject(), lpDx = _dxHandle.AddrOfPinnedObject(), lpCaretPos = _caretPosHandle.AddrOfPinnedObject(), lpClass = _classHandle.AddrOfPinnedObject(), lpGlyphs = _glyphsHandle.AddrOfPinnedObject(), nGlyphs = (uint)textLength, nMaxFit = 0 }; var result = GdiMethods.GetCharacterPlacement(hdc, text, textLength, 0, ref gcpResult, GcpReorder); return result != 0 ? gcpResult.lpOutString : text; } finally { freeResources(); } } } }
در اینجا برای اصلاح متد readPdf2 خواهیم داشت:
private static void readPdf2() { var reader = new PdfReader("test.pdf"); int intPageNum = reader.NumberOfPages; for (int i = 1; i <= intPageNum; i++) { var text = PdfTextExtractor.GetTextFromPage(reader, i, new LocationTextExtractionStrategy()); text = Encoding.UTF8.GetString(Encoding.UTF8.GetBytes(text)); text = new UnicodeCharacterPlacement { Font = new System.Drawing.Font("Tahoma", 12) }.Apply(text); File.WriteAllText("page-" + i + "-text.txt", text, Encoding.UTF8); } reader.Close(); }
سؤال: آیا این روش با تمام PDFهای فارسی کار میکند؟
پاسخ: خیر! همانطور که در پیشنیاز مطلب جاری عنوان شد، در یک حالت خاص، PDF writer میتواند شماره Glyphها را کاملا عوض کرده و در فایل PDF نهایی ثبت کند. خروجی حاصل در برنامه Adobe reader خوانا است، چون نمایش را بر اساس اطلاعات هندسی Glyphها انجام میدهد؛ اما خروجی متنی آن به نوعی obfuscated است چون مثلا حرف A آن به کاراکتر مرسوم دیگری نگاشت شده است.
مطالب
Html Encoding
.
.
مقدمه
در دنیای وب دو انکدینگ معروف داریم: Url Encoding و Html Encoding. در هر کدام از این انکدینگها یک عملیات کلی صورت میگیرد: تبدیل کاراکترهای غیرمجاز به عبارات معادل مجاز.
Url Encoding همانطور که از نامش پیداست روشی برای کدکردن Url هاست. مثل عبارت کدشده زیر:
Hello%20world%20,%20hi
درواقع کاراکتر مشخصکننده رشتهای که Url Encoding احتمالا در آن اعمال شده است، همان کاراکتر % است. بحث درباره این نوع انکدینگ کمی مفصل است که خود مطلب جداگانهای میطلبد. (اطلاعات بیشتر)
Html Encoding نیز با توجه به نامش برای انکدینگ عبارات HTML استفاده میشود. مثلا عبارت زیر را درنظر بگیرید:
<html>encoding</html>
این عبارت پس از اعمال عملیات Html Encoding به صورت زیر در خواهد آمد:
<html>encoding</html>
میبینید که در اینجا کاراکترهای > و < به صورت عبارات ;lt& و ;gt& در آمدهاند. شرح کاملی درباره این عبارات معادل (که اصطلاحا به آنها character entity میگویند) در اینجا آورده شده است.
در حالت کلی Html Encoding شامل کدکردن 5 کاراکتر زیر است:
.
کاراکتر | عبارت معادل | توضیحات |
> | > | |
< | < | |
" | " | |
' | ' | یا ;apos& به غیر از IE |
& | & |
نکته: در برخی استانداردها (بیشتر برای XML) برای کاراکتر ' از عبارت ;apos& استفاده میشود. این عبارت جایگزین به غیر از IE در بقیه مرورگرها درست کار میکند.
این کاراکترها درواقع از عناصر اصلی تشکیلدهنده ساختار Html هستند، بنابراین وجود آنها درون یک متن میتواند در روند رندر صفحات html اختلال ایجاد کند. بنابراین با استفاده از Html Encoding و تبدیل این کاراکترها به معادلشان (عباراتی که مرورگرها آنها را میشناسند)، میتوان از نمایش درست این کاراکترها مطمئن شد. البته یکی دیگر از دلایل مهم اعمال این انکدینگ، افزایش امنیت و جلوگیری از حملات XSS است.
فرمت این عبارات معادل به صورت ;entity_name& است. به کل این عبارت اصطلاحا Character Entity گفته میشود. این عبارات با کاراکتر & شروع شده و به یک کاراکتر ; ختم میشوند. کلمه میان این دو کاراکتر نیز عبارت جایگزین (یا همان entity name) هر یک از این کاراکترهاست که در لینک بالا به همراه بسیاری دیگر از کاراکترها اشاره شده است (^).
روش دیگری نیز برای کدکردن کاراکترها با فرمت ;entity_number#& وجود دارد. این entity_number درواقع کد کاراکتر مربوطه در جدول کاراکترست جاری مرورگر است. معمولا این کدها منطبق بر جدول ASCII هستند. برای کاراکترهای خارج از جدول اسکی هم از سایر جداول (مثلا یونیکد) استفاده میشود. عملیات انکدینگ برای کاراکترهای با کد 160 تا 255 (براساس استاندارد ISO-8859-1) با این روش انجام میشود (^). اطلاعات بیشتر راجع به این کدها در اینجا آورده شده است.
خوشبختانه در سمت سرور، در داتنت روشهای گوناگون و قابل اطمینانی برای اعمال این انکدینگ وجود دارد. اما متاسفانه در سمت کلاینت چنین امکاناتی اصلا فراهم نیست و برنامه نویسان خود باید دست به کار شوند. ازآنجاکه امروزه قسمتهای بیشتری از اپلیکیشنهای تحت وب در سمت کلاینت پیاده میشوند و کتابخانههای سمت کلاینت روز به روز پرطرفدارتر میشوند وجود نمونههای مشابه از این متدها در سمت کلاینت میتواند بسیار مفید باشد.
بنابراین تمرکز اصلی ادامه این مطلب بیشتر بر نحوه اعمال این انکدینگ در سمت کلاینت با استفاده از زبان جاوا اسکریپت است.
.
.
Html Encoding در داتنت
در داتنت متدهای متعددی برای اعمال Html Encoding وجود دارد. برخی از آنها صرفا برای اسناد HTML طراحی شدهاند و برخی دیگر یک پیادهسازی کلی دارند و بعضی نیز برای فایلهای XML ارائه شدهاند. این متدها عبارتند از:
- متد System.Security.SecurityElement.Escape: این متد بیشتر برای اعمال این انکدینگ در XML بهکار میرود. در این متد 5 کاراکتر اشاره شده در بالا به عبارات معادل انکد میشوند. البته برای کاراکتر ' از عبارت ;apos& استفاده میشود.
- متدهای موجود در System.Net.WebUtility: متدهای HtmlEncode و HtmlDecode موجود در این کلاس عملیات انکدینگ را انجام میدهند. این کلاس از داتنت 4 اضافه شده است.
- متدهای کلاس System.Web.HttpUtility: در این کلاس از متدهای موجود در کلاس System.Web.Util.HttpEncoder استفاده میشود. در پیادهسازی پیشفرض، متدهای این کلاس از متدهای موجود در کلاس WebUtility استفاده میکنند. البته میتوان با فراهم کردن یک Encoder سفارشی و تنظیم آن در فایل کانفیگ (خاصیت encoderType در قسمت HttpRuntime) این رفتار را تغییر داد. دلیل اصلی جابجایی مکان پیادهسازی این متدها از دات نت 4 به بعد نیز به همین دلیل است. (اطلاعات بیشتر ^ و ^).
- متدهای موجود در System.Web.HttpServerUtility: متدهای HtmlEncode و HtmlDecode موجود در این کلاس مستقیما از متدهای موجود در کلاس HttpUtility استفاده میکنند. خاصیت Server موجود در HttpContext یا در کلاس Page از نوع این کلاس است.
- متدهای موجود در کلاس System.Web.Security.AntiXss.AntiXssEncoder: این کلاس از دات نت 4.5 اضافه شده است. همانطور که از نام این کلاس بر میآید، از HttpEncoder مشتق شده است که در متدهای مرتبط با html encoding تغییراتی در آن اعمال شده است. متدهای این کلاس برای امنیت بیشتر به جای استفاده از Black List از یک White List استفاده میکنند.
درحال حاضر بهترین گزینه موجود برای عملیات انکدینگ، متدهای موجود در کلاس WebUtility هستند. ازآنجاکه این کلاس در فضای System.Net و در کتابخانه System.dll قرار دارد (کتابخانهای که معمولا برای تمام برنامههای داتنتی نیاز است)، بنابراین بارگذاری آن در برنامه نیز بار اضافی بر حافظه تحمیل نمیکند.
پیادهسازی عملیات HtmlEncode کار سختی نیست. مثلا میتوان برای سادگی از متد Replace استفاده کرد. اما برای رشتههای طولانی این متد کارایی مناسبی ندارد. به همین دلیل در تمام پیادهسازیها، معمولا از یک حلقه بر روی تمام کاراکترهای رشته موردنظر برای یافتن کاراکترهای غیرمجاز استفاده میشود. در کدهای متدهای موجود، برای افزایش سرعت حتی از اشارهگر و کدهای unsafe نیز استفاده شده است.
برای افزایش کارایی در تولید رشته نهایی تبدیلشده، بهتر است از یک StringBuilder استفاده شود. در پیادهسازیهای متدهای بالا برای اینکار معمولا از یک TextWriter استفاده میشود. TextWriterهای موجود از کلاس StrigBuilder برای دستکاری رشتهها استفاده میکنند.
صرفا جهت آشنایی بیشتر، پیادهسازی خلاصهشده متد HtmlEncode در کلاس WebUtility در زیر آورده شده است:
public static unsafe void HtmlEncode(string value, TextWriter output) { int index = IndexOfHtmlEncodingChars(value, 0); if (index == -1) { output.Write(value); return; } int cch = value.Length - index; fixed (char* str = value) { char* pch = str; while (index-- > 0) { output.Write(*pch++); } while (cch-- > 0) { char ch = *pch++; if (ch <= '>') { switch (ch) { case '<': output.Write("<"); break; case '>': output.Write(">"); break; case '"': output.Write("""); break; case '\'': output.Write("'"); break; case '&': output.Write("&"); break; default: output.Write(ch); break; } } else if (ch >= 160 && ch < 256) { // The seemingly arbitrary 160 comes from RFC output.Write("&#"); output.Write(((int)ch).ToString(NumberFormatInfo.InvariantInfo)); output.Write(';'); } else { output.Write(ch); } } } } private static unsafe int IndexOfHtmlEncodingChars(string s, int startPos) { int cch = s.Length - startPos; fixed (char* str = s) { for (char* pch = &str[startPos]; cch > 0; pch++, cch--) { char ch = *pch; if (ch <= '>') { switch (ch) { case '<': case '>': case '"': case '\'': case '&': return s.Length - cch; } } else if (ch >= 160 && ch < 256) { return s.Length - cch; } } } return -1; }
در ابتدا بررسی میشود که آیا اصلا متن ورودی حاوی کاراکترهای غیرمجاز است یا خیر. درصورت عدم وجود چنین کاراکترهایی، کار متد با برگشت خود متن ورودی پایان مییابد. درغیراینصورت عملیات انکدینگ آغاز میشود.
همانطور که میبینید عملیات انکدینگ برای 5 کاراکتر اشاره شده به صورت جداگانه انجام میشود و برای کاراکترهای با کد 160 تا 255 (با توجه به توضیحات موجود در مقدمه) نیز با استاندارد ;code#& عملیات تبدیل انجام میشود.در سمت دیگر، پیادهسازی بهینه متد HtmlDecode چندان ساده نیست. چون به جای یافتن یک کاراکتر غیرمجاز باید به دنبال عبارات چند کاراکتری معادل گشت که کاری نسبتا پیچیده است.
اطلاعات و پیادهسازی نسبتا کاملی درباره Html Encoding در سمت سرور در اینجا قابل مشاهده است.
نکته: درصورت نیاز به کدکردن سایر کاراکترها (مثلا کاراکترهای یونیکد) پیادهسازیهای موجود کارا نخواهند بود. بنابراین باید encoder سفارشی خود را تهیه کنید. مثلا میتوانید شرط دوم در بررسی کد کاراکترها را بردارید (منظور قسمت ch < 256) که در اینصورت متد شما محدوده وسیعی را پوشش میدهد. اما دقت کنید که با این تغییر متدی سفارشی برای عملیات decode نیز باید تهیه کنید!
.
.
Html Encoding در جاوا اسکریپت
برای انجام عملیات Url Encoding در جاوا اسکریپت چند متد توکار وجود دارد، که فرایند کلی عملیات همه آنها تقریبا یکسان است. اما متاسفانه برای انجام عملیات Html Encoding متدی در جاوا اسکریپت وجود ندارد. بنابراین متدهای مربوطه باید توسط خود برنامهنویسان پیادهسازی شوند.
یک روش برای اینکار استفاده از لیست اشارهشده در بالا و انجام عملیات replace برای تمام این کاراکترهاست (5 کاراکتر اصلی و درصورت نیاز سایر کاراکترها). این کار میتواند کمی سخت باشد و درواقع پیادهسازی چنین متدی نسبتا مشکل نیز هست (مخصوصا عملیات decode).
اما خوشبختانه امکانی در اسناد html وجود دارد که این کار (مخصوصا Decode کردن) را آسان میکند.
این روش جالب برای انجام عملیات Html Encoding در جاوا اسکریپت، استفاده از یک قابلیت توکار در مرورگرهاست. عناصر DOM (مانند div) دو خاصیت innerText و innerHTML دارند که مرورگرها با توجه به مقادیر تنظیمشده برای هر یک، عملیات coding و decoding مربوطه را به صورت کاملا خودکار انجام داده و مقدار خاصیت دیگر را بهروزرسانی میکنند (دقت کنید که در این دو پراپرتی، کلمه HTML کاملا با حروف بزرگ است، برخلاف Text که تنها حرف اول آن بزرگ است).
برای روشنتر شدن موضوع به مثال زیر برای عملیات encode توجه کنید:
<div id="log"></div> <script type="text/javascript"> var element = document.getElementById('log'); element.innerText = '<html> encoding </html>'; console.log(element.innerHTML); </script>
<html> encoding </html>
عکس این عملیات یعنی decoding نیز با استفاده از کدی مثل زیر امکانپذیر است:
<div id="log"> </div> <script type="text/javascript"> var element = document.getElementById('log'); element.innerHTML = "<html> encoding </html>"; console.log(element.innerText); </script>
<html> encoding </html>
..
.
متد htmlEncode
برای پیادهسازی این متد برای حالت استفاده مستقیم داریم:
String.htmlEncode = function (s) { var el = document.createElement("div"); el.innerText = s || ''; return el.innerHTML; };
در اینجا با استفاده از متد createElement شی document یک المان DOM (در اینجا div) ایجاد شده و سپس با توجه به توضیحات بالا خاصیت innerText آن به مقدار ورودی تنظیم میشود. استفاده از عبارت '' || s در اینجا برای جلوگیری از برگشت عبارات ناخواسته (مثل undefined یا null) برای ورودیهای غیرمجاز است. درنهایت خاصیت innerHTML این المان به عنوان رشته انکدشده برگشت داده میشود.
console.log(String.htmlEncode("<html>")); //result: <html>
و برای حالت استفاده از خاصیت prototype داریم:
String.prototype.htmlEncode = function () { var el = document.createElement("div"); el.innerText = this.toString(); return el.innerHTML; };
console.log("<html>".htmlEncode()); //result: <html>
.
.
متد htmlDecode
با استفاده از مطالب اشارهشده در بالا، پیادهسازی این متد به صورت زیر است:
String.htmlDecode = function (s) { var el = document.createElement("div"); el.innerHTML = s || ''; return el.innerText; };
String.prototype.htmlDecode = function () { var el = document.createElement("div"); el.innerHTML = this.toString(); return el.innerText; };
نحوه استفاده از این متدها هم به صورت زیر است:
console.log(String.htmlDecode("<html>")); console.log("<html>".htmlDecode());
.
.
پیادهسازی با استفاده از jQuery
درصورت در دسترس بودن کتابخانه jQuery، کار پیادهسازی این متدها بسیار سادهتر خواهد شد. برای اینکار میتوان از متدهای زیر استفاده کرد:
.
- متد htmlEncode:
String.htmlEncode = function (s) { return $('<div/>').text(value).html(); }; String.prototype.htmlEncode = function () { return $('<div/>').text(this.toString()).html(); };
- متد htmlDecode:
String.htmlDecode = function (s) { return $('<div/>').html(s).text(); }; String.prototype.htmlDecode = function () { return $('<div/>').html(this.toString()).text(); };
.
.
نکات پایانی
1. با اینکه به نظر میرسد در متدهای ارائه شده در بالا، بین نسخههای معمولی و نسخه مخصوص jQuery تفاوتی وجود ندارد اما تست زیر نشان میدهد که نکات ریزی باعث بهوجود آمدن برخی تفاوتها میشود. رشته زیر را درنظر بگیرید:
var value = "a \n b";
با استفاده از متد htmlEncode معمولی نشان داده شده در بالا، عبارت انکدشده رشته فوق به صورت زیر خواهد بود:
"a <br> b"
میبینید که به صورت هوشمندانهای! مقدار n\ به تگ <br> انکد شده است. اما اگر با استفاده از متد نوشته شده با jQuery سعی به انکدکردن این رشته کنیم، میبینیم که مقدار n\ بدین صورت انکد نمیشود! حال کدام روش درست و استاندارد است؟
در ابتدای این مطلب هم اشاره شده بود که Html Encoding برای کدکردن یکسری کاراکتر غیرمجاز در متون موجود در صفحات HTML بکار میرود و معمولا همان 5 کاراکتر اشارهشده در بالا به عنوان کاراکترهای اصلی غیرمجاز به حساب میآیند. کاراکتر n\ از این نوع کاراکترها محسوب نمیشود. همچنین ازآنجاکه عملیات عکس این تبدیل در Decode مربوطه صورت نمیگیرد، تبدیل این کاراکتر به معادلش در html اصلا کاری منطقی نیست و باعث خراب شدن متن موردنظر میشود.
با استفاده از متدهای HtmlEncode موجود در کلاسهای دات نت (WebUtility و HtmlUtility که در بالا به آنها اشاره شده بود) عملیات انکدینگ برای این رشته تکرار شد و نتیجه حاصله نشان داد که عبارت n\ در خروجی این متدها نیز انکد نمیشود. بنابراین متد نوشته شده با استفاده از jQuery خروجیهای استانداردتری ارائه میدهد.
با کمی تحقیق و بررسی کدهای jQuery مشخص شد که دلیل این تفاوت، در استفاده از متد createTextNode از شی document در متد ()text است. بنابراین برای بهبود متد htmlEncode اولیه داریم:
String.htmlEncode = function (s) { var el = document.createElement("div"); var txt = document.createTextNode(s); el.appendChild(txt); return el.innerHTML; };
.
.
2. نکته مهم دیگری که باید بدان توجه داشت برقراری اصل مهم زیر در عملیات انکدینگ است:
String.htmlDecode(String.htmlEncode(myString)) === myString;
var myString = "<HTML>"; String.htmlDecode(String.htmlEncode(myString)) === myString; // result: true // -------------------------------------------------------------------------- myString = "<اچ تی ام ال>"; String.htmlDecode(String.htmlEncode(myString)) === myString; // result: true
myString = "a \r b"; String.htmlDecode(String.htmlEncode(myString)) === myString; // result: false
پس از کمی تحقیق و بررسی بیشتر مشخص شد که مرورگرها در تبدیل کاراکترها، کاراکتر carriage return (یا CR یا همان r\ با کد اسکی 13 یا 0D) را تبدیل به کاراکتر line feed (یا LF یا n\ با کد اسکی 10 یا 0A) میکنند. برای آزمایش این نکته میتوانید از سه خط زیر استفاده کنید:
console.log(escape(String.htmlDecode('\r'))); // result: %0A : it is url encode of character '\n' console.log(escape(String.htmlDecode('\n'))); // result: %0A console.log(escape(String.htmlDecode('\r\n'))); // result: %0A
با بررسی بیشتر مشخص شد که این تبدیل به محض مقداردهی به یکی از خاصیتهای یک عنصر DOM صورت میگیرد. برای مثال کد زیر را در مرورگرهای مختلف امتحان کنید:
var el = document.createElement('div'); el.innerText = '\r'; console.log(escape(el.innerText)); // result: %0A el.innerHTML = '\r'; console.log(escape(el.innerHTML)); // result: %0A console.log(escape('\r')); // result: %0D
با بررسی هایی که من کردم دلیل و یا راهحلی برای این مشکل پیدا نکردم!
بنابراین در استفاده از این متدها باید این نکته را مدنظر قرار داد. ازآنجاکه این مشکل حالتی به خصوص دارد نمیتوان راهحلی کلی برای آن ارائه داد. پس برای موقعیتهای گوناگون با توجه به زوایای روشنشده از این مشکل باید به دنبال راهحل مناسب بود.
البته ممکن است این اشکال درمورد کاراکترهای دیگری هم وجود داشته باشد که من به آن برخورد نکرده باشم (با درنظر گرفتن تفاوت میان مرورگرهای مختلف ممکن است پیچیدهتر هم باشد).
نکته: ازآنجاکه برای رفع این مشکل، پیادهسازی متد htmlDecode به این کاملی، با عدم استفاده از ویژگی پراپرتیهای innerHTML و innerText، کاری نسبتا سخت و پیچیده و طولانی است، بنابراین در بیشتر حالات میتوان از این مشکل صرفنظر کرد! به همین دلیل در اینجا نیز متد دیگری برای رفع این مشکل ارائه نمیشود!
.
.
3. یک مشکل دیگر که این متدها دارند این است که متاسفانه در متد htmlEncode، از 5 کاراکتر معروف بالا، کاراکترهای ' و " در این متدها اصلا تبدیل نمیشوند. همچنین سایر کاراکترهای عنواندار یا کاراکترهای خارج از جدول ASCII (مثلا کاراکترهای با کد 160 تا 255 یا کاراکترهای یونیکد) نیز که معمولا انکد میشوند در این متد تغییری نمیکنند و به همان صورت برگشت داده میشوند.
هرچند متد htmlDecode نشان داده شده در این مطلب، بهدرستی تمامی عبارات معادل (حتی عبارات معادل غیر از 5 کاراکتر نشان داده شده در بالا با هر دو استاندارد ;character-entity& و ;code#&) را تبدیل کرده و کاراکتر درست را برمیگرداند.
برای اصلاح این مشکل میتوان متد htmlEncode را کاملا به صورت دستی و مستقیم نوشت و اعمال انکدینگهای موردنیاز را با استفاده یک حلقه روی تمام کاراکترها متن موردنظر انجام داد. چیزی شبیه به کد زیر:
String.htmlEncode = function (text) { text = text || ''; var encoded = ''; for (var i = 0; i < text.length; i++) { var c = text[i]; switch (c) { case '<': encoded += '<'; break; case '>': encoded += '>'; break; case '&': encoded += '&'; break; case '"': encoded += '"'; break; case "'": encoded += '''; break; default: // the upper limit can be removed to support more chars... var code = c.charCodeAt(); if (code >= 160 & code < 256) encoded += '&#' + code + ';'; else encoded += c; } } return encoded; };
.
.
کتابخانههای موجود
هرچند توضیحات ارائه شده در این مطلب کافی هستند، اما صرفا برای آشنایی با سایر کتابخانههای موجود، روشهای استفادهشده در آنها و نقایص و مزایای آنها این قسمت اضافه شده است.
.
Prototype: این کتابخانه شامل مجموعهای از متدهای کمکی برای راحتی کار در سمت کلاینت است. برای عملیات html encoding دو متد escapeHTML و unescapeHTML دارد که به صورت زیر پیاده شدهاند:
function escapeHTML() { return this.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); } function unescapeHTML() { return this.stripTags().replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&'); }
همانطور که میبینید در این متدها از replace استفاده شده است که برای متنهای طولانی کندتر از روشهای نشان دادهشده در این مطلب است. همچنین عملیات انکد و دیکد را تنها برای 3 کاراکتر < و > و & انجام میدهد که نقص بزرگی محسوب میشود.
.
jQuery.string: این پلاگین حاوی چند متد برای کار با رشتههاست که یکی از این متدها با نام htmlspecialchars مخصوص عملیات انکدینگ است. در این متد تنها همان 5 کاراکتر اصلی تبدیل میشوند. متاسفانه متدی برای decode در این پلاگین وجود ندارد. پیادهسازی خلاصهشده این کتابخانه تنها برای نمایش نحوه عملکرد متد فوق به صورت زیر است:
var andExp = /&/g, htmlExp = [/(<|>|")/g, /(<|>|')/g, /(<|>|'|")/g], htmlCharMap = { '<': '<', '>': '>', "'": ''', '"': '"' }, htmlReplace = function (all, $1) { return htmlCharMap[$1]; }; $.extend({ // convert special html characters htmlspecialchars: function (string, quot) { return string.replace(andExp, '&').replace(htmlExp[quot || 0], htmlReplace); } });
$.htmlspecialchars("<div>");
string.$: پلاگین دیگری برای jQuery که عملیات مربوط به رشتهها را دربر دارد. در این پلاگین برای عملیات انکدینگ دو متد escapeHTML و unescapeHTML به صورت زیر تعریف شدهاند:
this.escapeHTML = function (s) { this.str = this.s(s) .split('&').join('&') .split('<').join('<') .split('>').join('>'); return this; }; this.unescapeHTML = function (s) { this.str = this.stripTags(this.s(s)).str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); return this; };
همانطور که میبنید در متد encode این پلاگین از یک روش جالب اما به نسبت ناکارآمد در رشتههای طولانی، برای استخراج کاراکترهای غیرمجاز استفاده شده است. در این متدها هم تنها 3 کاراکتر & و < و > انکد و دیکد میشوند.
.
encoder.js: کتابخانه نسبتا کاملی برای عملیات انکدینگ رشتهها در سمت کلاینت. این کتابخانه علاوه بر encode و decode رشتهها متدهایی برای تبدیل html entityها به فرمت عددیشان و برعکس، حذف کاراکترهای یونیکد، بررسی اینکه رشته ورودی شامل کاراکترهای انکد شده است، جلوگیری از انکدینک مجدد یک رشته و ... نیز دارد.
. .
htmlEncode: این متد پیادهسازی کاملی برای اجرای عملیات Html Encode دارد و محدوده وسیعی از کاراکترها را نیز تبدیل میکند. مشاهده عملیات موجود در این متد برای آشنایی با مطالب ظریفتر پیشنهاد میشود.
.
.
منابع برای مطالعه بیشتر
در مطلب «روش بازگشت به قالبهای کلاسیک پروژهها در دات نت 6» مشاهده کردیم که قالب پیشفرض یک برنامهی کنسول دات نت 6، چنین فایل Program.cs ای را تولید میکند:
که در حقیقت همان اجبار به استفادهی از سبک «Top Level Programs» ارائه شدهی در C# 9.0 است. اما اگر به همین دو سطر هم دقت کنید، یک تفاوت مهم را با نمونهی C# 9.0 دارد و آن هم عدم ذکر عبارت using System در ابتدای آن است. علت اینجا است که فایل csproj پیشفرض پروژههای مبتنی بر NET 6.0.، دو تغییر مهم دیگر را هم دارند:
الف) فعال بودن nullable reference types که در C# 8.0 ارائه شد.
ب) فعال بودن ImplicitUsings که مختص به C# 10.0 است.
بررسی مفهوم global using directives در C# 10.0
هدف اصلی از وجود Using directives در زبان #C که از نگارش 1 آن در دسترس هستند، خلاصه نویسی نام طولانی اشیاء و متدها است. برای مثال نام اصلی متد Console.WriteLine به صورت System.Console.WriteLine است که با درج فضای نام System در ابتدای فایل، میتوان از ذکر مجدد آن جلوگیری کرد. از این دست میتوان به نوع System.Collections.Generic.List نیز اشاره کرد که کمتر کسی علاقمند است تا این نام طولانی را تایپ کند. به همین جهت با استفاده از یک using directive متناظر با فضای نام System.Collections.Generic، ذکر نام این نوع، به List خلاصه میشود.
طراحی دات نت 6 مبتنی بر سبک minimalism است! برای نمونه خلاصه کردن نزدیک به 10 سطر فایل Program.cs کلاسیک، به تنها یک سطر که به همراه ذکر using System در ابتدای آن هم نیست. در C# 10.0 دیگر نیازی نیست تا برای مثال ذکر using System را در دهها و یا صدها فایل، بارها و بارها تکرار کرد. برای اینکار تنها کافی است یکبار آنرا به صورت global تعریف کنیم و پس از آن دیگر نیازی به ذکر آن در کل پروژه نیست:
میتوان این سطر را در ابتدای یک تک فایل cs. قرار داد و ذکر آن به معنای الحاق خودکار آن، در ابتدای تک تک فایلهای cs. برنامه است.
چند نکته:
- امکان ترکیب global usingها و usingها معمولی در یک فایل هست.
- امکان تعریف global usingهای استاتیک نیز پیشبینی شدهاست:
که برای نمونه در این حالت بجای ذکر Console.WriteLine، تنها ذکر نام متد WriteLine در سراسر برنامه کفایت میکند.
مفهوم جدید implicit global using directives در C# 10.0 و به کمک NET SDK 6.0.
تا اینجا دریافتیم که میتوان دایرکتیوهای سراسری using را در برنامه به صورت دستی تعریف و استفاده کرد. اما ... پروژهی کنسولی که به صورت پیشفرض توسط NET SDK 6.0. ایجاد میشود، به همراه هیچ global using ای نیست. این مورد توسط تنظیم زیر که جزئی از NET SDK 6.0. است، فعال میشود:
زمانیکه ImplicitUsings را در فایل csproj برنامه فعال میکنیم، یعنی قرار است از یکسری global usingهای از پیش تعریف شدهی توسط SDK استفاده کنیم. بنابراین «global using directives» جزئی از ویژگیهای جدید C# 10.0 است اما « implicit global using directives» تنها یک لطف ارائه شدهی توسط NET SDK. است. برای یافتن لیست آنها، پروژه را build کرده و سپس به پوشهی obj\Debug\net6.0 مراجعه کنید. در اینجا به دنبال فایلی مانند MyProjectName. GlobalUsings.g.cs بگردید. محتویات آن به صورت زیر است:
اینها همان global using هایی هستند که با فعالسازی تنظیم ImplicitUsings در فایل csproj، به صورت خودکار توسط NET SDK. تولید و به برنامه الحاق میشوند.
البته این فایل ویژه به ازای نوعهای پروژههای مختلف، محتوای متفاوتی را دارد. برای مثال در برنامههای ASP.NET Core، چنین محتوای پیشفرضی را پیدا میکند:
این تعاریف در اصل در پوشهی C:\Program Files\dotnet\sdk\6.0.100-rc.2.21505.57\Sdks\Microsoft.NET.Sdk\targets و در فایل Microsoft.NET.GenerateGlobalUsings.targets آن قرار دارند.
روش حذف و یا اضافهی global usingهای پیشفرض
اگر به هر دلیلی نمیخواهید تعدادی از global usingهای پیشفرض به همراه گزینهی ImplicitUsings استفاده کنید، میتوانید آنها را در فایل csproj به صورت زیر، Remove و یا حتی موارد جدیدی را Include کنید:
یکی از کاربردهای این قابلیت، تولید کتابخانههای multi-target است که میتوان توسط Conditionها، فضاهای نامی را که نباید برای target خاصی include کرد، مشخص نمود:
// See https://aka.ms/new-console-template for more information Console.WriteLine("Hello, World!");
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net6.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> </Project>
ب) فعال بودن ImplicitUsings که مختص به C# 10.0 است.
بررسی مفهوم global using directives در C# 10.0
هدف اصلی از وجود Using directives در زبان #C که از نگارش 1 آن در دسترس هستند، خلاصه نویسی نام طولانی اشیاء و متدها است. برای مثال نام اصلی متد Console.WriteLine به صورت System.Console.WriteLine است که با درج فضای نام System در ابتدای فایل، میتوان از ذکر مجدد آن جلوگیری کرد. از این دست میتوان به نوع System.Collections.Generic.List نیز اشاره کرد که کمتر کسی علاقمند است تا این نام طولانی را تایپ کند. به همین جهت با استفاده از یک using directive متناظر با فضای نام System.Collections.Generic، ذکر نام این نوع، به List خلاصه میشود.
طراحی دات نت 6 مبتنی بر سبک minimalism است! برای نمونه خلاصه کردن نزدیک به 10 سطر فایل Program.cs کلاسیک، به تنها یک سطر که به همراه ذکر using System در ابتدای آن هم نیست. در C# 10.0 دیگر نیازی نیست تا برای مثال ذکر using System را در دهها و یا صدها فایل، بارها و بارها تکرار کرد. برای اینکار تنها کافی است یکبار آنرا به صورت global تعریف کنیم و پس از آن دیگر نیازی به ذکر آن در کل پروژه نیست:
global using System;
چند نکته:
- امکان ترکیب global usingها و usingها معمولی در یک فایل هست.
- امکان تعریف global usingهای استاتیک نیز پیشبینی شدهاست:
global using static System.Console;
مفهوم جدید implicit global using directives در C# 10.0 و به کمک NET SDK 6.0.
تا اینجا دریافتیم که میتوان دایرکتیوهای سراسری using را در برنامه به صورت دستی تعریف و استفاده کرد. اما ... پروژهی کنسولی که به صورت پیشفرض توسط NET SDK 6.0. ایجاد میشود، به همراه هیچ global using ای نیست. این مورد توسط تنظیم زیر که جزئی از NET SDK 6.0. است، فعال میشود:
<ImplicitUsings>enable</ImplicitUsings>
// <auto-generated/> global using global::System; global using global::System.Collections.Generic; global using global::System.IO; global using global::System.Linq; global using global::System.Net.Http; global using global::System.Threading; global using global::System.Threading.Tasks;
البته این فایل ویژه به ازای نوعهای پروژههای مختلف، محتوای متفاوتی را دارد. برای مثال در برنامههای ASP.NET Core، چنین محتوای پیشفرضی را پیدا میکند:
// <autogenerated /> global using global::System; global using global::System.Collections.Generic; global using global::System.IO; global using global::System.Linq; global using global::System.Net.Http; global using global::System.Threading; global using global::System.Threading.Tasks; global using global::System.Net.Http.Json; global using global::Microsoft.AspNetCore.Builder; global using global::Microsoft.AspNetCore.Hosting; global using global::Microsoft.AspNetCore.Http; global using global::Microsoft.AspNetCore.Routing; global using global::Microsoft.Extensions.Configuration; global using global::Microsoft.Extensions.DependencyInjection; global using global::Microsoft.Extensions.Hosting; global using global::Microsoft.Extensions.Logging;
روش حذف و یا اضافهی global usingهای پیشفرض
اگر به هر دلیلی نمیخواهید تعدادی از global usingهای پیشفرض به همراه گزینهی ImplicitUsings استفاده کنید، میتوانید آنها را در فایل csproj به صورت زیر، Remove و یا حتی موارد جدیدی را Include کنید:
<ItemGroup> <Import Remove="System.Threading" /> <Import Include="Microsoft.Extensions.Logging" /> </ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net472'"> </ItemGroup>
نظرات اشتراکها
چرا از آنگولار به ری اکت + ری داکس سوئیچ کردم!
- فسلفه React مبتنی بر مخلوط کردن جاوا اسکریپت و HTML با هم هست در فایلهای JSX (نوشتن HTML با کدهای جاوا اسکریپت). به این صورت شما مزیتهای ذاتی HTML و CSS را یکجا از دست میدید؛ چون دیگه نمیتونید HTML جدا یا CSS جدای از جاوا اسکریپت را داشته باشین. در حالیکه در Angular این دو یا این سه (TypeScript، HTML و CSS) از هم جدا هستند که مزیت آن دسترسی به انواع ادیتورهایی هست که بدون اینکه برای Angular نوشته شده باشند، در همان بدو معرفی آن، با آن سازگار هستند که سادگی توسعه را به همراه داره. شاید تولید کامپوننتهای ساده React تولید شده با کدهای جاوا اسکریپتی ساده باشه، اما کمی که حجم آن بیشتر شد، کنترل و مدیریت این مخلوط، سختتر و سختتر میشه و به علاوه مخلوط کردن کدهای یک فریم ورک با HTML و CSS خیلی شبیه به PHP کلاسیک و یا ASP کلاسیک هست و این روزها کسی را پیدا نمیکنید که برای پروژههای واقعی حتی از PHP در حالت کلاسیک آن بدون یک فریم ورک جانبی استفاده کنه. در Angular از همان بدو امر مباحث طراحی ماژولها، کامپوننتها و جدا سازی کدها به صورت ذاتی طراحی شدهاند.
- مزیت کار کردن با TypeScript در مقایسه با ES6 خالص در React، امکان دسترسی به کامپایل آفلاین هست و مباحث پیشرفتهی کامپایلر مانند tree-shaking (حذف کدهای مرده) و AOT (a head of time compilation) که سبب میشن هم حجم نهایی کمتری تولید شود و هم پیش از اجرای برنامه در مرورگر و سپس یافتن باگهای احتمالی در زمان اجرا، پیش از موعد و توسط کامپایلر این باگها گزارش شوند. اگر قصد داشته باشید به یک چنین کیفیت و بررسی کدی در React برسید، باید تعداد آزمونهای واحد قابل توجهی را داشته باشین تا بتونید یافتن مشکلاتی را که کامپایلر TypeScript گوشزد میکند، شبیه سازی کنید. همچنین شما در TypeScript میتونید به تمام امکانات پیشرفتهی زبان جاوا اسکریپت (حتی پس از ES6) دسترسی داشته باشید، اما کد نهایی جاوا اسکریپتی تولید شدهی توسط آنرا برای ES5 که تمام مرورگرها از آن پشتیبانی میکنند، تولید کنید که این هم خودش یک مزیت مهم هست. بنابراین TypeScript فقط یک static type checker ساده نیست.
- اینکه Angular یک فریمورک هست به خودی خودش یک مزیت مهم هست نسبت به React که یک کتابخانه است و اجزای آن باید از منابع مختلفی تهیه شوند. فریم ورک یعنی به روز رسانیهای منظم تمام اجزای آن توسط خود تیم Angular و سازگاری کامل و یکدست هر جزء با نگارش فعلی یا همان آخرین نگارش موجود. اگر با دنیای وابستگیهای ثالث در یک پروژهی واقعی کار کرده باشید به خوبی میدونید که هر چقدر تعداد آنها کمتر باشند، نگهداری طولانی مدت آن پروژه آسانتر میشود؛ چون روزی ممکن است آن کتابخانهی ثالث دیگر توسعه پیدا نکند، یا منسوخ شود یا دیرتر از آخرین نگارش ارائه شده به روز رسانی شود. مزیت داشتن یک فریم ورک یکدست، درگیر نشدن با این مسایل است؛ خصوصا اینکه عموما کتابخانههای ثالث کیفیتشون در حد کتابخانهی اصلی نیست و اینکه مثلا خود تیم Angular ماژول روتر، اعتبارسنجی یا فرمهای اون رو توسعه میده، قطعا کیفیتشون از کتابخانههای ثالث دیگه بهتر هست.
- در مورد سرعت و کارآیی و حتی مصرف حافظه، مطابق یک benchmarck خیلی معتبر، وضعیت Angular اندکی بهتر از React است؛ هرچند در کل از این لحاظ به هم نزدیک هستند.
- این مباحث انحصاری شدن و اینها هم در مورد محصولات سورس باز، زیاد مفهومی ندارند و بیشتر یکسری شعار ایدئولوژیک هست توسط کسانیکه حتی تغییر رفتار این شرکتها را هم دنبال نمیکنند و منابع و ماخذی رو که مطالعه کردن مربوط به یک دهه قبل هست.
- مزیت کار کردن با TypeScript در مقایسه با ES6 خالص در React، امکان دسترسی به کامپایل آفلاین هست و مباحث پیشرفتهی کامپایلر مانند tree-shaking (حذف کدهای مرده) و AOT (a head of time compilation) که سبب میشن هم حجم نهایی کمتری تولید شود و هم پیش از اجرای برنامه در مرورگر و سپس یافتن باگهای احتمالی در زمان اجرا، پیش از موعد و توسط کامپایلر این باگها گزارش شوند. اگر قصد داشته باشید به یک چنین کیفیت و بررسی کدی در React برسید، باید تعداد آزمونهای واحد قابل توجهی را داشته باشین تا بتونید یافتن مشکلاتی را که کامپایلر TypeScript گوشزد میکند، شبیه سازی کنید. همچنین شما در TypeScript میتونید به تمام امکانات پیشرفتهی زبان جاوا اسکریپت (حتی پس از ES6) دسترسی داشته باشید، اما کد نهایی جاوا اسکریپتی تولید شدهی توسط آنرا برای ES5 که تمام مرورگرها از آن پشتیبانی میکنند، تولید کنید که این هم خودش یک مزیت مهم هست. بنابراین TypeScript فقط یک static type checker ساده نیست.
- اینکه Angular یک فریمورک هست به خودی خودش یک مزیت مهم هست نسبت به React که یک کتابخانه است و اجزای آن باید از منابع مختلفی تهیه شوند. فریم ورک یعنی به روز رسانیهای منظم تمام اجزای آن توسط خود تیم Angular و سازگاری کامل و یکدست هر جزء با نگارش فعلی یا همان آخرین نگارش موجود. اگر با دنیای وابستگیهای ثالث در یک پروژهی واقعی کار کرده باشید به خوبی میدونید که هر چقدر تعداد آنها کمتر باشند، نگهداری طولانی مدت آن پروژه آسانتر میشود؛ چون روزی ممکن است آن کتابخانهی ثالث دیگر توسعه پیدا نکند، یا منسوخ شود یا دیرتر از آخرین نگارش ارائه شده به روز رسانی شود. مزیت داشتن یک فریم ورک یکدست، درگیر نشدن با این مسایل است؛ خصوصا اینکه عموما کتابخانههای ثالث کیفیتشون در حد کتابخانهی اصلی نیست و اینکه مثلا خود تیم Angular ماژول روتر، اعتبارسنجی یا فرمهای اون رو توسعه میده، قطعا کیفیتشون از کتابخانههای ثالث دیگه بهتر هست.
- در مورد سرعت و کارآیی و حتی مصرف حافظه، مطابق یک benchmarck خیلی معتبر، وضعیت Angular اندکی بهتر از React است؛ هرچند در کل از این لحاظ به هم نزدیک هستند.
- این مباحث انحصاری شدن و اینها هم در مورد محصولات سورس باز، زیاد مفهومی ندارند و بیشتر یکسری شعار ایدئولوژیک هست توسط کسانیکه حتی تغییر رفتار این شرکتها را هم دنبال نمیکنند و منابع و ماخذی رو که مطالعه کردن مربوط به یک دهه قبل هست.