نظرات مطالب
روش صحیح تعریف قلم در iTextSharp
- اگر هدف استفاده از فونت هست که برآورده شده.
- و ... بله. فونتش استاندارد نیست. این فونت را به همین روشی که گفتم باید استفاده کنید تا کار کند؛ فقط از مسیر فایل ttf دار بجای پارامترهای اصلی استفاده کنید و نه از نام آن. فونت استاندارد برای PDF چند سال قبل تولید شده؛ در اینجا.
مطالب
بازنویسی رویدادها در jQuery
فرض کنید می‌خواهید برای تمام دکمه‌های حذف، در کل پروژه، قبل از انجام عمل اصلی، یک confirm را به کاربر نشان دهید تا اگر کاربر بر روی کلید تایید، کلیک نمود، عمل مورد نظر انجام شود. برای چنین کاری در یک layout اصلی (و یا یک فایل js ) کدی شبیه به قطعه کد زیر را نوشته‌ایم:
$(document).ready(function () {
    $(document).on('click', '.confirm', function () {
        alert("Clicked Me !");
    });
});

حال فرض کنید در یکی از صفحات  قصد داریم اگر بر دکمه‌ای که قبلا برای آن رویدادی نوشته‌ایم  (منظور کد‌های بالا می‌باشد)، کلیک شد، یکسری عملیات دیگر، جدای از آن عملیات انجام شود. برای اینکار در صفحه مربوطه، کدی شبیه زیر را نوشته‌ایم :
$(document).ready(function () {
    $(document).on('click', '.confirm', function () {
        alert("Clicked Me On nested Page !");
    });
});

اگر پروژه را اجرا نمایید و بر روی دکمه مربوطه کلیک نماییم، هر دو Event نوشته شده، اجرا خواهند شد. در چنین حالتی تکلیف چیست و چکار باید کرد؟ جواب: selector را تغییر دهیم :خیر.
برای این کار می‌توان رویداد کلیک را برای تگ مورد نظر با استفاده از متد off بازنویسی کنیم:
$(document).off('click', '.confirm');

- روش‌هایی دیگر برای انجام این کار:
$(".confirm").removeAttr("onclick");
$( ".confim").unbind( "click" );
مطالب
تبدیل برنامه‌های کنسول ویندوز به سرویس ویندوز ان تی
در ویژوال استودیو، قالب پروژه ایجاد سرویس‌های ویندوز ان تی از پیش تدارک دیده شده است؛ اما کار کردن با آن ساده نیست به علاوه امکان دیباگ این نوع سرویس‌ها نیز به صورت پیش فرض درنظر گرفته نشده است و نیاز به تمهیدات و نکات خاصی دارد. جهت سهولت ایجاد سرویس‌های ویندوز ان تی، کتابخانه‌ای به نام TopShelf ایجاد شده است که یک برنامه ویندوزی را به سادگی تبدیل به یک سرویس ویندوز ان تی می‌کند. در ادامه جزئیات نحوه استفاده از آن‌را مرور خواهیم کرد.

الف) دریافت TopShelf
TopShelf یک کتابخانه سورس باز است و علاوه بر آن، آخرین فایل‌های باینری آن‌را از طریق نیوگت نیز می‌توان دریافت کرد:
 PM> Install-Package Topshelf


ب) فعال سازی TopShelf
یک برنامه ساده کنسول را ایجاد کنید. سپس با استفاده از نیوگت و اجرای فرمان فوق، ارجاعی را به اسمبلی TopShelf اضافه نمائید.
using Topshelf;

namespace MyService
{
    class Program
    {
        static void Main(string[] args)
        {
            HostFactory.Run(config =>
            {
                config.Service(settings => new TestService());
                config.EnableServiceRecovery(recovery => recovery.RestartService(delayInMinutes: 1));
                config.EnableShutdown();
                config.EnablePauseAndContinue();
                config.SetDescription("MyService Desc.");
                config.SetDisplayName("MyService");
                config.SetServiceName("MyService");
                config.RunAsLocalSystem();
            });
        }
    }
}
کدهای آغازین کار با TopShelf همین چندسطر فوق هستند. در آن وهله‌ای از کلاس سرویس مشتق شده از ServiceControl را دریافت کرده و سپس نام سرویس و سطح دسترسی اجرای آن مشخص می‌شوند. EnableServiceRecovery مربوط به حالتی است که سرویس کرش کرده است و ویندوز این قابلیت را دارد تا یک سرویس را به صورت خودکار راه اندازی مجدد کنند.

using Topshelf;
using Topshelf.Logging;

namespace MyService
{
    public class TestService : ServiceControl
    {
        static readonly LogWriter _log = HostLogger.Get<TestService>();

        public bool Start(HostControl hostControl)
        {
            _log.Info("TestService Starting...");

            return true;
        }

        public bool Stop(HostControl hostControl)
        {
            _log.Info("TestService Stopped");

            return true;
        }

        public bool Pause(HostControl hostControl)
        {
            _log.Info("TestService Paused");

            return true;
        }

        public bool Continue(HostControl hostControl)
        {
            _log.Info("TestService Continued");

            return true;
        }
    }
}
در اینجا امضای کلی کلاس سرویس را مشاهده می‌کند که می‌تواند شامل چهار متد استاندارد آغاز، پایان، مکث و ادامه باشد.


ج) نصب TopShelf

در همین حالت اگر برنامه را اجرا کنید، سرویس ویندوز ان تی تهیه شده، شروع به کار خواهد کرد (مزیت مهم آن نسبت به قالب توکار تهیه سرویس‌های ویندوز در ویژوال استودیو).
برای نصب این سرویس تنها کافی است در خط فرمان با دسترسی مدیریتی، دستور نصب your_exe install و یا عزل your_exe uninstall صادر شوند.
مطالب
یکپارچه کردن ELMAH با WCF RIA Services

پیشتر در مورد ELMAH مطلبی را منتشر کرده بودم و اگر برنامه نویس ASP.NET هستید و با ELMAH آشنایی ندارید،‌ جدا نیمی از عمر کاری شما بر فنا است!
هاست پیش فرض یک WCF RIA Service هم یک برنامه‌ی ASP.NET است. بنابراین کلیه‌ی خطاهای رخ داده در سمت سرور را باید بتوان به نحوی لاگ کرد تا بعدا با مطالعه‌ی آن‌ها اطلاعات ارزشمندی را از نقایص برنامه در عمل و پیش از گوشزد شدن آن‌ها توسط کاربران، دریافت، بررسی و رفع کرد.
کلیه خطاها را لاگ می‌کنم تا:
- بدانم معنای جمله‌ی "برنامه کار نمی‌کنه" چی هست.
- بدون روبرو شدن با کاربران یا حتی سؤال و جوابی از آن‌ها بدانم دقیقا مشکل از کجا ناشی شده.
- بدانم رفتارهای عمومی کاربران که منجر به بروز خطا می‌شوند کدام‌ها هستند.
- بدانم در کدامیک از قسمت‌های برنامه تعیین اعتبار ورودی کاربران یا انجام نشده یا ضعیف و ناکافی است.
- بدانم زمانیکه دوستی (!) قصد پایین آوردن برنامه را با تزریق SQL داشته، دقیقا چه چیزی را وارد کرده، در کجا و چه زمانی؟
- بتوانم Remote worker خوبی باشم.

ELMAH هم برای لاگ کردن خطاهای مدیریت نشده‌ی یک برنامه‌ی ASP.NET ایجاد شده است. بنابراین باید بتوان این دو (WCF RIA Services و ELMAH) را به نحوی با هم سازگار کرد. برای اینکار نیاز است تا یک مدیریت کننده‌ی خطای سفارشی را با پیاده سازی اینترفیس IErrorHandler تهیه کنیم (تا خطاهای مدیریت نشده‌ی حاصل را به سمت ELMAH هدایت کند) و سپس آن‌را به کمک یک ویژگی یا Attribute به DomainService خود جهت لاگ کردن خطاها اعمال نمائیم. روش تعریف این Attribute را در کدهای بعد ملاحظه خواهید نمود (در اینجا نیاز است تا دو ارجاع را به اسمبلی‌های Elmah.dll که دریافت کرده‌اید و اسمبلی استاندارد System.ServiceModel نیز به پروژه اضافه نمائید):

//add a reference to "Elmah.dll"
using System;
using System.ServiceModel.Channels;
using System.ServiceModel.Dispatcher;
using System.Web;

namespace ElmahWcf
{
public class HttpErrorHandler : IErrorHandler
{
#region IErrorHandler Members
public bool HandleError(Exception error)
{
return false;
}

public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
{
if (error == null)
return;

if (HttpContext.Current == null) //In case we run outside of IIS
return;

Elmah.ErrorSignal.FromCurrentContext().Raise(error);
}
#endregion
}
}

//add a ref to "System.ServiceModel" assembly
using System;
using System.Collections.ObjectModel;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;

namespace ElmahWcf
{
public class ServiceErrorBehaviorAttribute : Attribute, IServiceBehavior
{
Type errorHandlerType;
public ServiceErrorBehaviorAttribute(Type errorHandlerType)
{
this.errorHandlerType = errorHandlerType;
}

#region IServiceBehavior Members

public void AddBindingParameters(
ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints,
BindingParameterCollection bindingParameters)
{ }

public void ApplyDispatchBehavior(
ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase)
{
IErrorHandler errorHandler;
errorHandler = (IErrorHandler)Activator.CreateInstance(errorHandlerType);
foreach (ChannelDispatcherBase cdb in serviceHostBase.ChannelDispatchers)
{
ChannelDispatcher cd = cdb as ChannelDispatcher;
cd.ErrorHandlers.Add(errorHandler);
}
}

public void Validate(
ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase)
{ }
#endregion
}
}
اکنون پس از تعریف ویژگی ServiceErrorBehavior، نوبت به اعمال آن می‌رسد. به فایل DomainService خود مراجعه کرده و یک سطر زیر را به آن اضافه نمائید:
    [ServiceErrorBehavior(typeof(HttpErrorHandler))] //Integrating with ELMAH
[EnableClientAccess()]
public partial class MyDomainService : LinqToEntitiesDomainService<myEntities>

در ادامه نحوه‌ی افزودن تعاریف متناظر با ELMAH به Web.Config برنامه ذکر شده است. این تعاریف برای IIS6 و 7 به بعد هم تکمیل گردیده است. خطاها هم به صورت فایل‌های XML در پوشه‌ای به نام Errors که به ریشه‌ی سایت اضافه خواهید نمود (یا هر پوشه‌ی دلخواه دیگری)، لاگ می‌شوند.
به نظر من این روش، از ذخیره سازی اطلاعات لاگ‌ها در دیتابیس بهتر است. چون اساسا زمانیکه خطایی رخ می‌دهد شاید مشکل اصلی همان ارتباط با دیتابیس باشد.
قسمت ارسال خطاها به صورت ایمیل نیز comment شده است که در صورت نیاز می‌توان آن‌را فعال نمود:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<sectionGroup name="elmah">
<section name="security" requirePermission="false" type="Elmah.SecuritySectionHandler, Elmah"/>
<section name="errorLog" requirePermission="false" type="Elmah.ErrorLogSectionHandler, Elmah" />
<section name="errorMail" requirePermission="false" type="Elmah.ErrorMailSectionHandler, Elmah" />
<section name="errorFilter" requirePermission="false" type="Elmah.ErrorFilterSectionHandler, Elmah"/>
<section name="errorTweet" requirePermission="false" type="Elmah.ErrorTweetSectionHandler, Elmah"/>
</sectionGroup>
</configSections>

<elmah>
<security allowRemoteAccess="1" />
<errorLog type="Elmah.XmlFileErrorLog, Elmah" logPath="~/Errors" />
<!-- <errorMail
from="errors@site.net"
to="nasiri@site.net"
subject="prj-error"
async="true"
smtpPort="25"
smtpServer="mail.site.net"
noYsod="true" /> -->
</elmah>

<system.webServer>
<modules runAllManagedModulesForAllRequests="true">
<add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah"/>
<add name="DomainServiceModule"
preCondition="managedHandler"
type="System.ServiceModel.DomainServices.Hosting.DomainServiceHttpModule, System.ServiceModel.DomainServices.Hosting, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
</modules>
<validation validateIntegratedModeConfiguration="false" />
<handlers>
<add name="Elmah" verb="POST,GET,HEAD" path="myelmah.axd" type="Elmah.ErrorLogPageFactory, Elmah" />
</handlers>
</system.webServer>
<system.web>
<globalization
requestEncoding="utf-8"
responseEncoding="utf-8"
/>
<authentication mode="Forms">
<!--one month ticket-->
<forms name=".403AuthV"
cookieless="UseCookies"
slidingExpiration="true"
protection="All"
path="/"
timeout="43200" />
</authentication>
<httpHandlers>
<add verb="POST,GET,HEAD" path="myelmah.axd" type="Elmah.ErrorLogPageFactory, Elmah" />
</httpHandlers>
<httpModules>
<add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah"/>
<add name="DomainServiceModule"
type="System.ServiceModel.DomainServices.Hosting.DomainServiceHttpModule, System.ServiceModel.DomainServices.Hosting, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
</httpModules>
<compilation debug="true" targetFramework="4.0">
<assemblies>
<add assembly="System.Data.Entity, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
</assemblies>
</compilation>
</system.web>
<connectionStrings>
</connectionStrings>
<system.serviceModel>
<serviceHostingEnvironment
aspNetCompatibilityEnabled="true"
multipleSiteBindingsEnabled="true" />
</system.serviceModel>
</configuration>
اکنون برای مثال به یکی از متدهای DomainService خود سطر زیر را اضافه کرده و برنامه را آزمایش کنید:
throw new Exception("This is an ELMAH test");

سپس به آدرس http://localhost/myelmah.axd مراجعه نموده و اطلاعات لاگ شده حاصل را بررسی کنید:


این روش با WCF Services های متداول هم کار می‌کند. فقط در این سرویس‌ها باید aspNetCompatibilityEnabled مطابق تگ‌های ذکر شده‌ی system.serviceModel فوق در web.config لحاظ شوند (این مورد به صورت پیش فرض در WCF RIA Services وجود دارد). همچنین ویژگی زیر نیز باید به سرویس شما اضافه گردد:
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]

منابع مورد استفاده:
Integrating ELMAH for a WCF Service
Making WCF and ELMAH play nice together
Getting ELMAH to work with WCF services



پ.ن.
اگر به خطاهای ASP.NET دقت کرده باشید که به yellow screen of death هم مشهور هستند (در مقابل صفحات آبی ویندوز!)، ابتدای آن خیلی بزرگ نوشته شده Server Error و سپس ادامه‌ی خطا. همین مورد دقیقا یادم هست که هر بار سبب بازخواست مدیران شبکه بجای برنامه نویس‌ها می‌شد! (احتمالا این هم یک نوع بدجنسی تیم ASP.NET برای گرفتن حال ادمین‌های شبکه است! و گرنه مثلا می‌توانستند همان ابتدا بنویسند program/application error بجای server error)

مطالب
خواندنی‌ها روز 22 فروردین

سایت iplocationtools بانک اطلاعاتی موقعیت مکانی IP های خود را که ماهیانه نیز به روز خواهد شد، به رایگان برای دریافت قرار داده است. فرمت تعاریف جداول و عبارت insert آن مطابق دستورات بانک اطلاعاتی MySQL است.
تنها بازیگر شاخص یازده سال قبل در زمینه توسعه برنامه‌های مبتنی بر QT ، برنامه KDevelop بوده است. این وضعیت با ارائه Qt Creator‌ بهبود یافته است ...
  • شرکت Red gate که پیشتر برنامه‌ی معروف Reflector را در جهت اهداف تبلیغاتی خود تصاحب کرده بود، اکنون در ادامه‌ی همین سیاست، کتاب Illustrated C# 2008 را نیز به رایگان ارائه داده است.
  • سایت DelphiFeeds.com با طراحی جدید خود چند روزی است که متحول شده است. سایتی شبیه به سایت مهندس خودمان که بعد از عید معلوم نیست چه بلایی سرش آمده است و جای خالی آن احساس می‌شود، هر چند سایت IDevCenter نیز همین کار را به صورت تخصصی‌تری ارائه می‌دهد.
  • Microsoft MED-V 1.0 در جهت توسعه‌ی اهداف مجازی سازی مایکروسافت ارائه شد.

مطالب
نوشتن افزونه برای مرورگرها: قسمت دوم : کروم
در مقاله پیشین ما ظاهر افزونه را طراحی و یک سری از قابلیت‌های افزونه را معرفی کردیم. در این قسمت قصد داریم پردازش پس زمینه افزونه یعنی خواندن RSS و اعلام به روز آوری سایت را مورد بررسی قرار دهیم و یک سری قابلیت هایی که گوگل در اختیار ما قرار داده است.

خواندن RSS توسط APIهای گوگل
گوگل در تعدادی از زمینه‌ها و سرویس‌های خودش apiهایی را ارائه کرده است که یکی از آن ها خواندن فید است و ما از آن برای خواندن RSS یا اتم وب سایت کمک می‌گیریم. روند کار بدین صورت است که ابتدا ما بررسی می‌کنیم کاربر چه مقادیری را ثبت کرده است و افزونه قرار است چه بخش هایی از وب سایت را بررسی نماید. در این حین، صفحه پس زمینه شروع به کار کرده و در هر سیکل زمانی مشخص شده بررسی می‌کند که آخرین بار چه زمانی RSS به روز شده است. اگر از تاریخ قبلی بزرگتر باشد، پس سایت به روز شده است و تاریخ جدید را برای دفعات آینده جایگزین تاریخ قبلی کرده و یک پیام را به صورت نوتیفیکیشن جهت اعلام به روز رسانی جدید در آن بخش به کاربر نشان می‌دهد.
 اجازه دهید کدها را کمی شکیل‌تر کنیم. من از فایل زیر که یک فایل جاوااسکریپتی است برای نگه داشتن مقادیر بهره می‌برم تا اگر روزی خواستم یکی از آن‌ها را تغییر دهم راحت باشم و در همه جا نیاز به تغییر مجدد نداشته نباشم. نام فایل را (const.js) به خاطر ثابت بودن آن‌ها انتخاب کرده‌ام.
  //برای ذخیره مقادیر از ساختار نام و مقدار استفاده می‌کنیم که نام‌ها را اینجا ثبت کرده ام
var Variables={
 posts:"posts",
 postsComments:"postsComments",
 shares:"shares",
 sharesComments:"sharesComments",
}

//برای ذخیره زمان آخرین تغییر سایت برای هر یک از مطالب به صورت جداگانه نیاز به یک ساختار نام و مقدار است که نام‌ها را در اینجا ذخیره کرده ام
var DateContainer={
 posts:"dtposts",
 postsComments:"dtpostsComments",
 shares:"dtshares",
 sharesComments:"dtsharesComments",
 interval:"interval"
}
 
//برای نمایش پیام‌ها به کاربر
var Messages={
SettingsSaved:"تنظیمات ذخیره شد",
SiteUpdated:"سایت به روز شد",
PostsUpdated:"مطلب ارسالی جدید به سایت اضافه شد",
CommentsUpdated:"نظری جدیدی در مورد مطالب سایت ارسال شد",
SharesUpdated:"اشتراک جدید به سایت ارسال شد",
SharesCommentsUpdated:"نظری برای اشتراک‌های سایت اضافه شد"
}
//لینک‌های فید سایت
var Links={
 postUrl:"https://www.dntips.ir/feeds/posts",
 posts_commentsUrl:"https://www.dntips.ir/feeds/comments",
 sharesUrl:"https://www.dntips.ir/feed/news",
 shares_CommentsUrl:"https://www.dntips.ir/feed/newscomments"
}
//لینک صفحات سایت
var WebLinks={
Home:"https://www.dntips.ir",
 postUrl:"https://www.dntips.ir/postsarchive",
 posts_commentsUrl:"https://www.dntips.ir/commentsarchive",
 sharesUrl:"https://www.dntips.ir/newsarchive",
 shares_CommentsUrl:"https://www.dntips.ir/newsarchive/comments"
}
موقعی که اولین بار افزونه نصب می‌شود، باید مقادیر پیش فرضی وجود داشته باشند که یکی از آن‌ها مربوط به مقدار سیکل زمانی است (هر چند وقت یکبار فید را چک کند) و دیگری ذخیره مقادیر پیش فرض رابط کاربری که قسمت پیشین درست کردیم؛ پروسه پس زمینه برای کار خود به آن‌ها نیاز دارد و بعدی هم تاریخ نصب افزونه است برای اینکه تاریخ آخرین تغییر سایت را با آن مقایسه کند که البته با اولین به روزرسانی تاریخ فید جای آن را می‌گیرد. جهت انجام اینکار یک فایل init.js ایجاد کرده‌ام که قرار است بعد از نصب افزونه، مقادیر پیش فرض بالا را ذخیره کنیم.
chrome.runtime.onInstalled.addListener(function(details) {
var now=String(new Date());

var params={};
params[Variables.posts]=true;
params[Variables.postsComments]=false;
params[Variables.shares]=false;
params[Variables.sharesComments]=false;

params[DateContainer.interval]=1;

params[DateContainer.posts]=now;
params[DateContainer.postsComments]=now;
params[DateContainer.shares]=now;
params[DateContainer.sharesComments]=now;

 chrome.storage.local.set(params, function() {
  if(chrome.runtime.lastError)
   {
       /* error */
       console.log(chrome.runtime.lastError.message);
       return;
   }
        });
});
chrome.runtime شامل رویدادهایی چون onInstalled ، onStartup ، onSuspend و ... است که مربوطه به وضعیت اجرایی افزونه میشود. آنچه ما اضافه کردیم یک listener برای زمانی است که افزونه نصب شده است و در آن مقادیر پیش فرض ذخیره می‌شوند. اگر خوب دقت کنید می‌بینید که روش دخیره سازی ما در اینجا کمی متفاوت از مقاله پیشین هست و شاید پیش خودتان بگویید که احتمالا به دلیل زیباتر شدن کد اینگونه نوشته شده است ولی مهمترین دلیل این نوع نوشتار این است که متغیرهای بین {} آنچنان فرقی با خود string نمی‌کنند یعنی کد زیر:
chrome.storage.local.set('mykey':myvalue,....
با کد زیر برابر است:
chrome.storage.local.set(mykey:myvalue,...
پس اگر مقداری را داخل متغیر بگذاریم آن مقدار حساب نمی‌شود؛ بلکه کلید نام متغیر خواهد شد.
 برای معرفی این دو فایل const.js و init.js به manifest.json می‌توانید به صورت زیر عمل کنید:
"background": {
    "scripts": ["const.js","init.js"]
}
در این حالت خود اکستنشن در زمان نصب یک فایل html درست کرده و این دو فایل js را در آن صدا میزند که البته خود ما هم می‌توانیم اینکار را مستقیما انجام دهیم. مزیت اینکه ما خودمان مسقیما این کار را انجام دهیم این است که در صورتی که فایل‌های js ما زیاد شوند، فایل manifest.jason زیادی شلوغ شده و شکل زشتی پیدا می‌کند و بهتر است این فایل را تا آنجا که می‌توانیم خلاصه نگه داریم. البته روش بالا برای دو یا سه تا فایل js بسیار خوب است ولی اگر به فرض بشود 10 تا یا بیشتر بهتر است یک فایل جداگانه شود و من به همین علت فایل background.htm را درست کرده و به صورت زیر تعریف کرده‌ام:
نکته:نمی توان در تعریف بک گراند هم فایل اسکریپت معرفی کرد و هم فایل html
"background": {
    "page": "background.htm"
}
background.htm
<html>
  <head>
  <script type="text/javascript" src="const.js"></script>
    <script type="text/javascript" src="https://www.google.com/jsapi"></script>
    <script type="text/javascript" src="init.js"></script>
<script type="text/javascript" src="omnibox.js"></script>
<script type="text/javascript" src="rssreader.js"></script>
<script type="text/javascript" src="contextmenus.js"></script>
  </head>
  <body>
  </body>
</html>
لینک‌های بالا به ترتیب معرفی ثابت‌ها، لینک api گوگل که بعدا بررسی می‌شود، فایل init.js برای ذخیره مقادیر پیش فرض، فایل ominibox که در مقاله پیشین در مورد آن صحبت کردیم و فایل rssreader.js که جهت خواندن rss در پایینتر در موردش بحث می‌کنیم و فایل contextmenus که این را هم در مطلب پیشین توضیح دادیم.
جهت خواندن فید سایت ما از Google API استفاده می‌کنیم؛ اینکار دو دلیل دارد:
  1. کدنویسی راحت‌تر و خلاصه‌تر برای خواندن RSS
  2. استفاده اجباری از یک پروکسی به خاطر Content Security Policy و حتی CORS
قبل از اینکه manifst به ورژن 2 برسد ما اجازه داشتیم کدهای جاوااسکریپت به صورت inline در فایل‌های html بنویسیم و یا اینکه از منابع و آدرس‌های خارجی استفاده کنیم برای مثال یک فایل jquery بر روی وب سایت jquery ؛ ولی از ورژن 2 به بعد، گوگل سیاست امنیت محتوا Content Security Policy را که سورس و سند اصلی آن در اینجا قرار دارد، به سیستم Extension خود افزود تا از حملاتی قبیل XSS و یا تغییر منبع راه دور به عنوان یک malware جلوگیری کند. پس ما از این به بعد نه اجازه داشتیم inline بنویسیم و نه اجازه داشتیم فایل jquery را از روی سرورهای سایت سازنده صدا بزنیم. پس برای حل این مشکل، ابتدا مثل همیشه یک فایل js را در فایل html معرفی می‌کردیم و برای حل مشکل دوم باید منابع را به صورت محلی استفاده می‌کردیم؛ یعنی فایل jquery را داخل دایرکتوری extension قرار می‌دادیم.
برای حل مشکل مشکل صدا زدن فایل‌های راه دور ما از Relaxing the Default Policy  استفاده می‌کنیم که به ما یک لیست سفید ارائه می‌کند و در این لیست سفید دو نکته‌ی مهم به چشم میخورد که یکی از آن این است که استفاده از آدرس هایی با پروتکل Https و آدرس لوکال local host/127.0.0.1 بلا مانع است و از آنجا که api گوگل یک آدرس Https است، می‌توانیم به راحتی از API آن استفاده کنیم. فقط نیاز است تا خط زیر را به manifest.json اضافه کنیم تا این استثناء را برای ما در نظر بگیرد.
"content_security_policy": "script-src 'self' https://*.google.com; object-src 'self'"
در اینجا استفاده از هر نوع subdomain در سایت گوگل بلامانع اعلام می‌شود.
بنابراین آدرس زیر به background.htm اضافه می‌شود:
 <script type="text/javascript" src="https://www.google.com/jsapi"></script>

استفاده از این Api در rssreader.js
فایل rssreader.js را به background.htm اضافه می‌کنیم و در آن کد زیر را می‌نویسیم:
google.load("feeds", "1");
google.setOnLoadCallback(alarmManager);
آدرسی که ما از گوگل درخواست کردیم فقط مختص خواندن فید نیست؛ تمامی apiهای جاوااسکریپتی در آن قرار دارند و ما تنها نیاز داریم قسمتی از آن لود شود. پس اولین خط از دستور بالا بارگذاری بخش مورد نیاز ما را به عهده دارد. در مورد این دستور این صفحه را مشاهده کنید.
در خط دوم ما تابع خودمان را به آن معرفی می‌کنیم تا وقتی که گوگل لودش تمام شد این تابع را اجرا کند تا قبل از لود ما از توابع آن استفاده نکنیم و خطای undefined دریافت نکنیم. تابعی که ما از آن خواستیم اجرا کند alarmManager نام دارد و قرار است یک آلارم و یک سیکل زمانی را ایجاد کرده و در هر دوره، فید را بخواند. کد تابع مدنظر به شرح زیر است:
function alarmManager()
{
chrome.storage.local.get(DateContainer.interval,function ( items) {
period_time==items[DateContainer.interval];
chrome.alarms.create('RssInterval', {periodInMinutes: period_time});
});


chrome.alarms.onAlarm.addListener(function (alarm) {
console.log(alarm);
    if (alarm.name == 'RssInterval') {

var boolposts,boolpostsComments,boolshares,boolsharesComments;
chrome.storage.local.get([Variables.posts,Variables.postsComments,Variables.shares,Variables.sharesComments],function ( items) {
boolposts=items[Variables.posts];
boolpostsComments=items[Variables.postsComments];
boolshares=items[Variables.shares];
boolsharesComments=items[Variables.sharesComments];


chrome.storage.local.get([DateContainer.posts,DateContainer.postsComments,DateContainer.shares,DateContainer.sharesComments],function ( items) {

var Vposts=new Date(items[DateContainer.posts]);
var VpostsComments=new Date(items[DateContainer.postsComments]);
var Vshares=new Date(items[DateContainer.shares]);
var VsharesComments=new Date(items[DateContainer.sharesComments]);

if(boolposts){var result=RssReader(Links.postUrl,Vposts,DateContainer.posts,Messages.PostsUpdated);}
if(boolpostsComments){var result=RssReader(Links.posts_commentsUrl,VpostsComments,DateContainer.postsComments,Messages.CommentsUpdated); }
if(boolshares){var result=RssReader(Links.sharesUrl,Vshares,DateContainer.shares,Messages.SharesUpdated);}
if(boolsharesComments){var result=RssReader(Links.shares_CommentsUrl,VsharesComments,DateContainer.sharesComments,Messages.SharesCommentsUpdated);}

});
});
    }
});
}
خطوط اول تابع alarmManager وظیفه‌ی خواندن مقدار interval را که در init.js ذخیره کرده‌ایم، دارند که به طور پیش فرض 10 ذخیره شده است تا تایمر یا آلارم خود را بر اساس آن بسازیم. در خط chrome.alarms.create یک آلارم با نام rssinterval می‌سازد و قرار است هر 10 دقیقه وظایفی که بر دوشش گذاشته می‌شود را اجرا کند (استفاده از api جهت دسترسی به آلارم نیاز به مجوز "alarms" دارد). وظایفش از طریق یک listener که بر روی رویداد chrome.alarms.onAlarm  گذاشته شده است مشخص می‌شود. در خط بعدی مشخص می‌شود که این رویداد به خاطر چه آلارمی صدا زده شده است. البته از آنجا که ما یک آلارم داریم، نیاز چندانی به این کد نیست. ولی اگر پروژه شما حداقل دو آلارم داشته باشد نیاز است مشخص شود که کدام آلارم باعث صدا زدن این رویداد شده است. در مرحله بعد مشخص می‌کنیم که کاربر قصد بررسی چه قسمت‌هایی از سایت را داشته است و در تابع callback آن هم تاریخ آخرین تغییرات هر بخش را می‌خوانیم و در متغیری نگه داری می‌کنیم. هر کدام را جداگانه چک کرده و تابع RssReader را برای هر کدام صدا می‌زنیم. این تابع 4 پارامتر دارد:
  1. آدرس فیدی که قرار است از روی آن بخواند
  2. آخرین به روزسانی که از سایت داشته متعلق به چه تاریخی است.
  3. نام کلید ذخیره سازی تاریخ آخرین تغییر سایت که اگر بررسی شد و مشخص شد سایت به روز شده است، تاریخ جدید را روی آن ذخیره کنیم.
  4. در صورتی که سایت به روز شده باشد نیاز است پیامی را برای کاربر نمایش دهیم که این پیام را در اینجا قرار می‌دهیم.
کد تابع rssreader
function RssReader(URL,lastupdate,datecontainer,Message) {
            var feed = new google.feeds.Feed(URL);
            feed.setResultFormat(google.feeds.Feed.XML_FORMAT);
                    feed.load(function (result) {
if(result!=null)
{
var strRssUpdate = result.xmlDocument.firstChild.firstChild.childNodes[5].textContent;
var RssUpdate=new Date(strRssUpdate);

if(RssUpdate>lastupdate)
{
SaveDateAndShowMessage(datecontainer,strRssUpdate,Message)
}
}
});
}
در خط اول فید توسط گوگل خوانده میشود، در خط بعدی ما به گوگل میگوییم که فید خوانده شده را چگونه به ما تحویل دهد که ما قالب xml را خواسته ایم و در خط بعدی اطلاعات را در متغیری به اسم result قرار میدهد که در یک تابع برگشتی آن را در اختیار ما میگذارد. از آن جا که ما قرار است تگ lastBuildDate را بخوانیم که پنجمین تگ اولین گره در اولین گره به حساب می‌آید، خط زیر این دسترسی را برای ما فراهم می‌کند و چون تگ ما در یک مکان ثابت است با همین تکه کد، دسترسی مستقیمی به آن داریم:
var strRssUpdate = result.xmlDocument.firstChild.firstChild.childNodes[5].textContent;
مرحله بعد تاریخ را که در قالب رشته‌ای است، تبدیل به تاریخ کرده و با lastupdate یعنی آخرین تغییر قبلی مقایسه می‌کنیم و اگر تاریخ برگرفته از فید بزرگتر بود، یعنی سایت به روز شده است و تابع SaveDateAndShowMessage را صدا می‌زنیم که وظیفه ذخیره سازی تاریخ جدید و ایجاد notification را به عهده دارد و سه پارامتر کلید ذخیره سازی و مقدار آن و پیام را به آن پاس می‌کنیم.

کد تابع SaveDateAndShowMesage
function SaveDateAndShowMessage(DateField,DateValue,Message)
{
var params={
}
params[DateField]=DateValue;

chrome.storage.local.set( params,function(){

var options={
  type: "basic",
   title: Messages.SiteUpdated,
   message: Message,
   iconUrl: "icon.png"
}
chrome.notifications.create("",options,function(){
chrome.notifications.onClicked.addListener(function(){
chrome.tabs.create({'url': WebLinks.Home}, function(tab) {
});
});
});
});
}
خطوط اول مربوط به ذخیره تاریخ است و دومین نکته نحوه‌ی ساخت نوتیفکیشن است. اجرای یک notification  نیاز به مجوز "notifications " دارد که مجوز آن در manifest به شرح زیر است:
"permissions": [
    "storage",
     "tabs",
 "alarms",
 "notifications"
  ]
در خطوط بالا سایر مجوزهایی که در طول این دوره به کار اضافه شده است را هم می‌بینید.
برای ساخت نوتیفکیشن از کد chrome.notifications.create استفاده می‌کنیم که پارامتر اول آن کد یکتا یا همان ID جهت ساخت نوتیفیکیشن هست که میتوان خالی گذاشت و دومی تنظیمات ساخت آن است؛ از قبیل عنوان و آیکن و ... که در بالا به اسم options معرفی کرده ایم و در آگومان دوم آن را معرفی کرده ایم و آرگومان سوم هم یک تابع callback است که نوشتن آن اجباری است. options شامل عنوان، پیام، آیکن و نوع notification می‌باشد که در اینجا basic انتخاب کرده‌ایم. برای دسترسی به دیگر خصوصیت‌های options به اینجا و برای داشتن notification‌های زیباتر به عنوان rich notification به اینجا مراجعه کنید. برای اینکه این امکان باشد که کاربر با کلیک روی notification به سایت هدایت شود باید در تابع callback مربوط به notifications.create این کد اضافه گردد که در صورت کلیک یک تب جدید با آدرس سایت ساخته شود:
chrome.notifications.create("",options,function(){
chrome.notifications.onClicked.addListener(function(){
chrome.tabs.create({'url': WebLinks.Home}, function(tab) {
});});
});

نکته مهم:  پیشتر معرفی آیکن به صورت بالا کفایت میکرد ولی بعد از این باگ  کد زیر هم باید جداگانه به manifest اضافه شود:
"web_accessible_resources": [
    "icon.png"
  ]


خوب؛ کار افزونه تمام شده است ولی اجازه دهید این بار امکانات افزونه را بسط دهیم:
من می‌خواهم برای افزونه نیز قسمت تنظیمات داشته باشم. برای دسترسی به options میتوان از قسمت مدیریت افزونه‌ها در مرورگر یا حتی با راست کلیک روی آیکن browser action عمل کرد. در اصل این قسمت برای تنظیمات افزونه است ولی ما به خاطر آموزش و هم اینکه افزونه ما UI خاصی نداشت تنظیمات را از طریق browser action پیاده سازی کردیم و گرنه در صورتی که افزونه شما شامل UI خاصی مثلا نمایش فید مطالب باشد، بهترین مکان تنظیمات، options است. برای تعریف options در manifest.json به روش زیر اقدام کنید:
"options_page": "popup.html"
همان صفحه popup را در این بخش نشان میدهم و اینبار یک کار اضافه‌تر دیگر که نیاز به آموزش ندارد اضافه کردن input  با Type=number است که برای تغییر interval به کار می‌رود و نحوه ذخیره و بازیابی آن را در طول دوره یاد گرفته اید.

جایزگزینی صفحات یا 
 Override Pages
بعضی صفحات مانند بوک مارک و تاریخچه فعالیت‌ها History و همینطور newtab را می‌توانید جایگزین کنید. البته یک اکستنشن میتواند فقط یکی از صفحات را جایگزین کند. برای تعیین جایگزین در manifest اینگونه عمل می‌کنیم:
"chrome_url_overrides": {
    "newtab": "newtab.htm"
  }

ایجاد یک تب اختصاصی در Developer Tools
تکه کدی که باید manifest اضافه شود:
"devtools_page": "devtools.htm"
شاید فکر کنید کد بالا الان شامل مباحث ui و ... می‌شود و بعد به مرورگر اعمال خواهد شد؛ در صورتی که اینگونه نیست و نیاز دارد چند خط کدی نوشته شود. ولی مسئله اینست که کد بالا تنها صفحات html را پشتیبانی می‌کند و مستقیما نمی‌تواند فایل js را بخواند. پس صفحه بالا را ساخته و کد زیر را داخلش می‌گذاریم:
<script src="devtools.js"></script>
فایل devtools.js هم شامل کد زیر می‌شود:
chrome.devtools.panels.create(
    "Dotnettips Updater Tools", 
    "icon.png", 
    "devtoolsui.htm",
    function(panel) {
    }
);
خط chrome.devtools.panels.create یک پنل یا همان تب را ساخته و در پارامترهای بالا به ترتیب عنوان، آیکن و صفحه‌ای که باید در آن رندر شود را دریافت می‌کند و پس از ایجاد یک callback اجرا می‌شود. اطلاعات بیشتر

APIها
برای دیدن لیست کاملی از API‌ها می‌توانید به مستندات آن رجوع کنید و این مورد را به یاد داشته باشید که ممکن است بعضی api‌ها در بعضی موارد پاسخ ندهند. به عنوان مثال در content scripts نمی‌توانید به chrome.devtools.panels دسترسی داشته باشید یا اینکه در DeveloperTools  دسترسی به DOM میسر نیست. پس این مورد را به خاطر داشته باشید. همچنین بعضی api‌ها از نسخه‌ی خاصی به بعد اضافه شده‌اند مثلا همین مثال قبلی devtools از نسخه 18 به بعد اضافه شده است و به این معنی است با خیال راحت می‌توانید از آن استفاده کنید. یا آلارم‌ها از نسخه 22 به بعد اضافه شده‌اند. البته خوشبختانه امروزه با دسترسی آسانتر به اینترنت و آپدیت خودکار مرورگرها این مشکلات دیگر آن چنان رخ نمی‌دهند.

Messaging
همانطور که در بالا اشاره شد شما نمی‌توانید بعضی از apiها را در بعضی جاها استفاده کنید. برای حل این مشکل می‌توان از messaging استفاده کرد که دو نوع تبادلات پیغامی داریم:
  1. One-Time Requests یا درخواست‌های تک مرتبه‌ای
  2. Long-Lived Connections یا اتصالات بلند مدت یا مصر

درخواست‌های تک مرتبه ای
  این درخواست‌ها همانطور که از نامش پیداست تنها یک مرتبه رخ می‌دهد؛ درخواست را ارسال کرده و منتظر پاسخ می‌ماند. به عنوان مثال به کد زیر که در content script است دقت کنید:
window.addEventListener("load", function() {
    chrome.extension.sendMessage({
        type: "dom-loaded", 
        data: {
            myProperty   : "value" 
        }
    });
}, true);
کد بالا یک ارسال کننده پیام است. موقعی که سایتی باز می‌شود، یک کلید با مقدارش را ارسال می‌کند و کد زیر در background گوش می‌ایستد تا اگر درخواستی آمد آن را دریافت کند:
chrome.extension.onMessage.addListener(function(request, sender, sendResponse) {
    switch(request.type) {
        case "dom-loaded":
            alert(request.data.myProperty   ); 
        break;
    }
    return true;
});

اتصالات بلند مدت یا مصر
اگر نیاز به یک کانال ارتباطی مصر و همیشگی دارید کد‌ها را به شکل زیر تغییر دهید
contentscripts
var port = chrome.runtime.connect({name: "my-channel"});
port.postMessage({myProperty: "value"});
port.onMessage.addListener(function(msg) {
    // do some stuff here
});

background
chrome.runtime.onConnect.addListener(function(port) {
    if(port.name == "my-channel"){
        port.onMessage.addListener(function(msg) {
            // do some stuff here
        });
    }
});

نمونه کد
نمونه کدهایی که در سایت گوگل موجود هست می‌توانند کمک بسیاری خوبی باشند ولی اینگونه که پیداست اکثر مثال‌ها مربوط به نسخه‌ی یک manifest است که دیگر توسط مرورگرها پشتیبانی نمی‌شوند و مشکلاتی چون اسکریپت inline و CSP که در بالا اشاره کردیم را دارند و گوگل کدها را به روز نکرده است.

دیباگ کردن و پک کردن فایل‌ها برای تبدیل به فایل افزونه Debugging and packing
برای دیباگ کردن کد‌ها می‌توان از دو نمونه console.log و alert برای گرفتن خروجی استفاده کرد و همچنین ابزار  Chrome Apps & Extensions Developer Tool هم نسبتا امکانات خوبی دارد که البته میتوان از آن برای پک کردن اکستنشن نهایی هم استفاده کرد. برای پک کردن روی گزینه pack کلیک کرده و در کادر باز شده گزینه‌ی pack را بزنید. برای شما دو نوع فایل را در مسیر والد دایرکتوری extension نوشته شده درست خواهد کرد که یکی پسوند crx دارد که می‌شود همان فایل نهایی افزونه و دیگری هم پسوند pem دارد که یک کلید اختصاصی است و باید برای آپدیت‌های آینده افزونه آن را نگاه دارید. در صورتی که افزونه را تغییر دادید و خواستید آن را به روز رسانی کنید موقعی که اولین گزینه pack را می‌زنید و صفحه باز می‌شود قبل از اینکه دومین گزینه pack را بزنید، از شما می‌خواهد اگر دارید عملیات به روز رسانی را انجام می‌دهید، کلید اختصاصی آن را وارد نمایید و بعد از آن گزینه pack را بزنید:


آپلود نهایی کار در Google web store

برای آپلود نهایی کار به google web store که در آن تمامی برنامه‌ها و افزونه‌های کروم قرار دارند بروید. سمت راست آیکن تنظیمات را بزنید و گزینه developer dashboard را انتخاب کنید تا صفحه‌ی آپلود کار برای شما باز شود. دایرکتوری محتویات اکستنشن را zip کرده و آپلود نمایید. توجه داشته باشید که محتویات و سورس خود را باید آپلود کنید نه فایل crx را. بعد از آپلود موفقیت آمیز، صفحه‌ای ظاهر می‌شود که از شما آیکن افزونه را در اندازه 128 پیکسل میخواهد بعلاوه توضیحاتی در مورد افزونه، قیمت گذاری که به طور پیش فرض به صورت رایگان تنظیم شده است، لینک وب سایت مرتبط، لینک محل پرسش و پاسخ برای افزونه، اگر لینک یوتیوبی در مورد افزونه دارید، یک شات تصویری از افزونه و همینطور چند تصویر برای اسلایدشو سازی که در همان صفحه استاندارد آن‌ها را توضیح می‌دهد و در نهایت گزینه‌ی جالب‌تر هم اینکه اکستنشن شما برای چه مناطقی تهیه شده است که متاسفانه ایران را ندیدم که می‌توان همه موارد را انتخاب کرد. به خصوص در مورد ایران که آی پی‌ها هم صحیح نیست، انتخاب ایران چنان تاثیری ندارد و در نهایت گزینه‌ی publish را می‌زنید که متاسفانه بعد از این صفحه درخواست می‌کند برای اولین بار باید 5 دلار آمریکا پرداخت شود که برای بسیاری از ما این گزینه ممکن نیست.

سورس پروژه را می‌توانید از اینجا ببینید و خود افزونه را از اینجا دریافت کنید.

 
مطالب
رویه های ذخیره شده خوب یا بد؟!

استفاده یا عدم استفاده از یک تکنولوژی یا ابزار خاص، به پارامترهای مختلفی از جمله ابعاد پروژه، مهارت و دانش اعضای تیم، ماهیت پروژه، پلتفرم اجرا، بودجه‌ی پروژه، مهلت تکمیل پروژه و تعداد نفرات تیم بستگی دارد. بنابراین واضح است پیچیدن یک نسخه‌ی خاص، برای همه‌ی سناریو‌ها امکان پذیر نیست؛ اما شرایطی وجود دارد که استفاده یا عدم استفاده از این ابزارهای تکنولوژیک منطقی‌تر مینمایند.

Stored Procedure (که از این به بعد برای ایجاز، SP نوشته خواهد شد) هم از قاعده فوق مستثنی نیست و در صورت انتخاب صحیح میتواند به ارائه‌ی محصول نهایی با کیفیت‌تری در زمان کوتاه‌تری کمک کند و در صورت انتخاب ناآگاهانه ممکن است باعث شکست یک پروژه (بخصوص در بلند مدت) شود.


تاریخچه

SQL توسط شرکت IBM در اوایل دهه 70 میلادی ایجاد شد. با اوج گرفتن زبان‌های رویه‌ای، SQL هم چندان از این قافله عقب نماند که منجر به پذیرش SP به عنوان یک استاندارد، در دهه 90 میلادی و پیاده سازی تدریجی آن توسط غول‌های سازنده دیتابیس شد (رجوع فرمایید به ^ و ^). این فاصله 20 ساله باعث غنی‌تر شدن SQL شد و وجود SP - به معنی انتقال مدل برنامه نویسی رویه‌ای به SQL - بخشی از مشکلات قبلی کار با کوئری‌های پشت سر هم و خام را حل کرد. از سال 2000 میلادی به بعد، ORM‌های قدرتمندی از جمله  Hibernate  و پیاده سازی‌های مختلفی از Active Record  و Entity Framework متولد شدند. بنابر این تقدم و تاخّرهای زمانی، بدیهی است اغلب مزایای SP نسبت به Raw SQL Query و اغلب معایب آن نسبت به ORM‌ها باشد. 

بنظر میرسد برای پاسخ به سوال اصلی این مطلب، ناگزیر به مقایسه SP با رقبای دیرینه‌اش هستیم. با برشمردن معایب و مزایای SP میتوان به نتیجه‌ی منطقی‌تری رسید. البته باید در نظر داشت صرف استفاده از SP به معنای بهره‌مند شدن از مزایای آن و صرف استفاده نکردن از آن هم بهره‌مندی از رقبای آن نیست. چگونگی استفاده یک ابزار، مهمتر از خود ابزار است.


معایب SP

- دستورات Alter Table ، Add Column و Drop Column  به این سادگی‌ها هم نیستند؛ ممکن است به یکی از جداول دیتابیس دو ستون اضافه یا از آن حذف شوند. مجبوریم تمامی SP‌ها را بخصوص Insert و Update متناظر با جدول را تغییر دهیم که این تغییرات ممکن است بصورت زنجیره‌وار به سایر SP‌ها هم سرایت کند. حال شرایطی را در نظر بگیرید که تعداد SP‌های شما به چند ده و یا حتی به چند صد عدد و بیشتر، رسیده باشد که این به معنی زحمت بیشتر و تغییرات پر هزینه‌تر است.

- احتمال کند شدن ماشین سرویس دهنده در اثر اجرای تعداد زیادی SP ؛ چناچه بخش زیادی از منطق برنامه از طریق SP اجرا شود، سرور دیتابیس موظف به اجرای آنهاست. اما در صورتیکه منطق، در کد برنامه قرار داشته باشد، امکان توزیع آن بر روی سرور‌های مجزا و یا حتی ماشین کلاینت وجود خواهد داشت. امروزه اکثر کلاینت‌ها به دیتابیس‌های سبک و سریعی مجهز شده‌اند. بنابراین در صورت امکان چرا بار پردازشی را به عهده آنها نگذاریم؟! 

- یکپارچگی کمتر؛ تقریبا همه اپلیکیشن‌ها نیازمند ارتباط با سایر سیستم‌ها هستند. اگر بخش‌های زیادی از منطق برنامه درون SP مخفی شده باشند، این نقطه تلاقی بین سیستمی، احتمالا درون خود دیتابیس قرار میگیرد و این به معنی ایجاد SP های بیشتر، افزودن پارامتر‌های بیشتر، توسعه SPهای قبلی و بطور خلاصه اعمال تغییرات بیشتر، که منتج به قابلیت نگهداری کمترخواهد شد.

- انعطاف پذیری کمتر؛ در یک شرایط ایده آل، عملکرد اپلیکیشن، مستقل از دیتابیس است. اگر نیاز به تغییر دیتابیس، مثلا از اوراکل به Microsoft SQL Server وجود داشته باشد، نیاز به بازنویسی و انتقال فانکشن‌ها و SP ها محتمل است و از آنجائیکه که با وجود استانداردها، دیتابیس‌های مختلف، معمولا در Syntax دستورات، تفاوت‌های فاحشی دارند، هر چه کد بیشتری در SP ها باشد، نیاز به انتقال و تبدیل بیشتری وجود دارد. 

- عدم وجود بازخورد مناسب؛ بسیاری از اوقات در صورت بروز اشکالی در حین اجرای یک SP، فقط با یک متن ساده بصورت Table has no rows   و یا  error مواجه میشویم. چنین خطاهایی هنگام دیباگ اصلا خوشایند نیستند. MS SQL در این بین بازخورد‌های مناسبی را ارائه میکند. اگر تجربه کار با سایر دیتابیس‌ها را داشته باشید، اهمیت بازخورد‌های مناسب، ملموس‌تر خواهد بود.

- کد نویسی سخت‌تر؛ نوشتن کد SQL  معمولا در همان IDE  اپلیکیشن انجام نمیشود. جابجایی مداوم بین دو IDE ، دیباگ و کد نویسی از طریق دو اینترفیس مجزا، اصلا ایده‌ال نیست. 

- SP  منطق را بیش از حد پنهان میکند؛ حتی با دانستن نام صحیح یک SP، باز هم تصویری از پارامتر‌های ارسالی به آن و نتیجه برگشتی نخواهیم داشت. نمیدانیم نتیجه حاصل از اجرای SP ما مقداری را برمیگرداند یا خیر؟ در صورت وجود برگشتی، یک Cursor است یا یک مقدار؟ اگر Cursor است شامل چه ستون‌هایی است؟

- SP نمیتواند یک شیء را به عنوان آرگومان بپذیرد؛ بنابراین احتمال کثیف شدن کد به مرور افزایش پیدا میکند و بدتراز آن، در صورت ارسال اشتباه یک پارامتر، یا عدم  تطابق تعداد پارامتر‌ها، مجبور به بررسی تمام آنها بصورت دستی هستیم. برای مثال دو قطعه کد زیر را با هم مقایسه کنید:

INSERT INTO User_Table(Id,Username,Password,FirstName,SureName,PhoneNumber,x,Email)
VALUES (1,'VahidN','123456','Vahid','Nasiri','09120000000','vahid_xxx@example.com')

و معادل آن در یک ORM  فرضی:

public void Insert(User user)
{
  _users.Insert(user);
  db.Save();
}

به‌وضوح قطعه کد sql، قبل از خوب یا بد بودن، زشت است. همچنین پارامتر x آن که فرضاً به تازگی اضافه شده، مقداری را دریافت نکرده و باعث بروز خطا خواهد شد.

- نبود Query Chaining؛ یکی از ویژگی‌های جذاب ORM‌‌های امروزی، امکان تشکیل یک کوئری با قابلیت خوانایی بالا و افزودن شرط‌های بیشتر از طریق  الگوی builder است. قطعه کد زیر یک SP برای جستجوی داینامیک نام و نام خانوادگی در یک جدول فرضی به اسم Users است:

public ICollection<User> GetUsers(string firstName,string lastName,Func<User, bool> orderBy)
{
    var query = _users.where(u => u.LastName.StartsWith(lastName));
    query = query.where(u => u.FirstName.StartsWith(firstName));
    query = query.OrderBy(orderBy);
    return  query.ToList();
}

در مقایسه با معادل SP آن:

CREATE PROCEDURE DynamicWhere 
    @LastName varchar(50) = null,
    @FirstName varchar(50) = null,
    @Orderby varchar(50) = null
AS
BEGIN
    DECLARE @where nvarchar(max)
    SELECT @where = '1 = 1'
 
    IF @LastName IS NOT NULL
        SELECT @Where = @Where + " AND A.LastName LIKE @LastName + '%'"
 
    IF @FirstName IS NOT NULL
        SELECT @Where = @Where + " AND A.FirstName LIKE @FirstName + '%'"
 
    DECLARE @orderBySql nvarchar(max)
    SELECT @orderBySql = CASE
        WHEN @OrderBy = "LastName" THEN "A.LastName"
        ELSE @OrderBy = "FirstName" THEN "A.FirstName"
    END
 
    DECLARE @sql nvarchar(max)
    SELECT @sql = "
    SELECT A.Id , A.AccountNoId, A.LastName, A.FirstName, A.PostingDt, 
    A.BillingAmount
    FROM Users 
    WHERE " + @where + " 
    ORDER BY " + @orderBySql
 
    exec sp_executesql @sql,  N'@LastName varchar(50), @FirstName varchar(50)
        @LastName, @FirstName
END

حاجت به گفتن نیست که قطعه کد اول چقدر خواناتر، انعطاف پذیرتر، خلاصه‌تر و قابل نگهداری‌تر است.

- نداشتن امکانات زبان‌های مدرن؛ زبان‌ها و IDE‌های مدرن، امکانات قابل توجهی را برای نگهداری بهتر، انعطاف پذیری بیشتر، مقیاس پذیری بالاتر، تست پذیری دقیق‌تر و... ارائه میکنند. به عنوان مثال:

  • شیءگرایی و امکانات آن که در SP موجود نیست و در مورد قبلی معایب، به آن مختصرا اشاره شد. در نظر بگیرید اگر SQL زبانی شیء گرا بود و مجهز به ارث بری و کپسوله سازی بود، چقدر قابلیت نگهداری آن بالاتر میرفت و حجم کد‌های نوشته شده میتوانست کمتر باشند.
  • نداشتن Lazy Loading که باعث مصرف زیاد حافظه میشود.
  • نداشتن intellisense حین فراخوانی‌ها.
  • نداشتن Navigation Property که باعث join نویسی‌های زیاد خواهد شد.
  • SQL در مقایسه با یک زبان مدرن ناقص بنظر میرسد و این نوشتن کد آن را سخت‌تر میکند.‌
  • نداشتن امکان تغییر منطقی نام جداول و ستون ها
  • مدیریت تراکنش‌ها بصورت دستی، حال آنکه با الگوی Unit Of Work  این مشکل در یک ORM قدرتمند مثل EF حل شده است.


- زمان بر بودن نوشتن SP؛ گاهی نوشتن یک تابع در یک ORM یا بعضا نوشتن یک کوئری SQL کوتاه در یک رشته متنی، ساده‌تر از نوشتن کد SP است. آیا برای هر وظیفه کوچک در دیتابیس، نوشتن یک SP ضروری است؟


مزایای SP :

- کمتر کردن Round Trips در شبکه و متعاقبا کاهش ترافیک شبکه؛ اگر از یک فراخوانی استفاده کنیم، کاهش Round Trip‌ها تاثیر چندانی نخواهد داشت. همچنین ارسال یک کوئری کامل، نسبت به ارسال فقط اسم SP و پارامتر‌های آن، پهنای باند بیشتری اِشغال میکند. البته در یک شبکه با سرعت قابل قبول، بعید است این دو مزیت محسوس باشند؛ اما به هر حال برای موارد خاص، دو مزیت محسوب میشوند. نکته دیگر آنکه بدلیل Pre-Compiled بودن SP‌ها و همچنین کَش شدن Execution Plan آنها، اندکی با سرعت بالاتری اجرا میشوند.

- امکان چک کردن سینتکس قبل از اجرای آن؛ در مقایسه با Raw Query مزیت محسوب میشود.

امکان به اشتراک گذاری کد؛ برای پروژه‌هایی که چندین اپلیکیشن با چندین زبان برنامه نویسی مختلف در حال تهیه هستند و نیازمند دسترسی مستقیم به داده‌ها با سرعت به نسبت بالاتری هستند، SP  میتواند یک راه حل ایده آل محسوب شود. بجای پیاده سازی منطق برنامه در هر اپلیکیشن بصورت جداگانه و زحمت کدنویسی هرکدام، میتوان از SP  استفاده کرد. هرچند امروزه معمولا برای حل این مشکل، API های مشترک معماری Restful  ارجحیت دارد. 

- کمک به ایجاد یک پَک؛ در یک زیر سیستم با نیازمندی مشخص که اعمال تغییرات در آن محتمل نمیباشد نیز SP میتواند یک گزینه مناسب به حساب آید. مثلا یک سیستم Membership را در نظر بگیرید که در پروژه‌های مختلف شما مورد استفاده قرار خواهد گرفت. برای مثال میشود یک سیستم Membership  سفارشی را با امکان  Hash  پسورد و  رمز کردن داده‌های حساس،  به کمک SP و Function ‌های مناسب فراهم کرد و در واقع بین Application Login  و Data Logic تمایز قائل شد. شخصا معماری Restful را به این روش هم ترجیح میدهم. 

بهرمند شدن از امکانات بومی SQL ؛ به عنوان نمونه برای ترانهاده کردن خروجی یک کوئری میتوان از فانکشن  Pivot  استفاده کرد. یا فانکشن‌های تحلیلی  Lead  و  Lag  (لینک مستندات اوراکل این دو فانکشن به ترتیب در ^ و ^ ) که بنظر نمیرسد هنوز معادل مستقیمی درORM  ها  داشته باشند. 

تسلط و کنترل بیشتر و دقیقتر بر کوئری نهایی؛ گفته میشود SP و عبارات SQL در دیتابیس، حکم assembly را در سایر زبان‌ها دارند. بنابراین با SP میتوان عبارات SQL و نحوه اجرای آن را در دیتابیس، بطور کامل تحت فرمان داشت. این در حالی است که هر یک از ORM‌ها دستورات زبان برنامه نویسی مبداء را به یک عبارت SQL ترجمه میکنند که این عبارت چندان تحت کنترل برنامه نویس نیست و بیشتر به مدل کاری ORM بستگی دارد. 

امکان join بین دو یا چند دیتابیس مجزا؛ حال آنکه امکان join بین دو Context در ORM ‌ها وجود ندارد. بعلاوه اگر دو دیتابیس مدنظر ما روی دو سرور مجزا باشند، با SP و  کانفیگ Linked Server  کماکان میشود کوئری join  دار نوشت.

برای عملیات‌های Batch مناسب‌تر است؛ در مقام مقایسه با ORM ‌ها که با تکنیک‌های مختلفی سعی در افزایش سرعت عملیات Batch، بخصوص Insert و Update را دارند، SP  با سرعت قابل قبول‌تری اجرا میشود.

عدم نیاز به یادگیری سینتکس و ابزاری جدید؛ موارد بسیاری وجود دارند که فرصت یادگیری تکنولوژی جدیدی مثل یک ORM و یا SQL Bulk و حتی کتابخانه‌های ثالث مبتنی بر این ابزارها  وجود ندارند و ممکن است مجبور شوید برای باقی ماندن در بازار رقابتی، از دانسته‌های قبلی خود استفاده کنید .

تخصصی‌تر کردن وظایف؛ برنامه نویس‌های دیتابیس به صورت تخصصی اقدام به تحلیل روابط و ایندکس‌ها میکنند، دیتابیس را ایجاد و نرمال سازی مینمایند، SP های متناسب را میسازند و به بهترین شکل Optimize و در آخر تست میکنند.

- امنیت به نسبت بالاتر؛ میتوان مجوز اجرای SP را به یک کاربر اعطا کرد، بدون آنکه مجوز دسترسی به جداول مورد استفاده در آن SP را داد. همچنین نسبت به کوئری‌های پارامتری نشده، SQL ارجیحت دارند چون احتمال آسیب پذیری در مقابل SQL Injection را کمتر میکنند.


نتیجه‌گیری

اگرچه SP ها برای پردازش داده‌ها آنقدر هم که در وبلاگ‌ها میخوانیم بد نیستند، اما سوء استفاده از آن، مشکلات عدیده‌ای را ایجاد خواهد کرد. با توجه به روند تغییرات تکنولوژی‌های دسترسی به داده‌ها و معماری‌های مدرن بنظر میرسد SP در بهترین حالت، ابزار مناسبی برای انجام عملیات CRUD است و نه بیشتر؛ مگر در مواردی خاص که به تشخیص شما نیاز به استفاده بیشتر از آن وجود داشته باشد.

مطالب
تغییرات مهم مقایسه‌‌ی رشته‌ها در NET 5.0.
با توجه به ماهیت چندسکویی NET 5.، در اکثر سیستم‌های ویندوزی، سرویس بومی سازی، بر اساس استاندارد NLS کار می‌کند، اما در سیستم‌های لینوکسی و مبتنی بر یونیکس، این استاندارد از نوع ICU است (و وجود و تنظیم آن‌ها خارج از NET. و توسط سیستم عامل مدیریت می‌شود). جهت یک‌دست سازی این دو نوع سیستم بومی سازی در دات نت، از نگارش 5 آن به بعد، استاندارد ICU که به صورت گسترده‌تری مورد پذیرش قرار گرفته‌است، استاندارد بومی سازی پیش‌فرض دات نت درنظر گرفته می‌شود؛ مگر اینکه سیستم عاملی آن‌را پشتیبانی نکند.


کدام نگارش از ویندوز، از ICU پشتیبانی می‌کند؟

تمام ویندوزهای پس از Windows 10 May 2019 Update، به همراه icu.dll، به عنوان جزء استاندارد سیستم عامل هستند. بنابراین دات نت 5 و نگارش‌های پس از آن، در این سیستم عامل‌ها، از سرویس بومی سازی ICU استفاده خواهند کرد؛ اما اگر از نگارش‌های پیشین ویندوز استفاده می‌کنید، به اجبار به سیستم NLS سوئیچ خواهد شد.


تاثیر ICU بر برنامه‌های دات نت 5 به بعد

قطعه کد زیر را درنظر بگیرید:
string s = "Hello\r\nworld!";
int idx = s.IndexOf("\n");
Console.WriteLine(idx);
در نگارش‌های پیش از 5 دات نت، خروجی کدهای فوق، عدد 6 است؛ اما ... اما ... (!) از زمان دات نت 5 به بعد، خروجی آن «منهای یک» است! البته به شرطی که آخرین به روز رسانی ویندوز 10 را نصب کرده باشید؛ یعنی حداقل  Windows 10 May 2019 Update را داشته باشید.


حالت «پیش‌فرض» جستجو و مقایسه‌ی رشته‌ها در دات نت 5 به بعد، یک مقایسه‌ی مبتنی بر «دستورات زبانی» بر اساس فرهنگ تنظیم شده‌ی در Thread جاری برنامه‌است (یا همان System.Threading.Thread.CurrentThread.CurrentCulture).


چرا متدهای کار بر روی رشته‌ها در دات نت 5 به بعد، نسبت به نگارش‌های قبلی متفاوت عمل می‌کنند؟

زمانیکه متدی مانند IndexOf فراخوانی می‌شود، هدف عمده‌ی برنامه‌نویس‌ها، یک جستجوی Ordinal است (یعنی مقایسه‌ی کاراکتر به کاراکتر؛ بدون درنظر گرفتن نکات زبانی و بومی)؛ اما فراموش می‌کنند که این متدها دارای پارامتر دومی هم هستند که از نوع StringComparison است و سال‌ها است که توصیه می‌شود این پارامتر را هم به صورت صریحی مقدار دهی کنید تا هدف خود را از نوع جستجو دقیقا مشخص نمائید. از زمان دات نت 5 به بعد، اگر این پارامتر را مشخص نکنید، جستجوی صورت گرفته یک رفتار culture-specific را خواهد داشت و نه Ordinal.  از این لحاظ مقایسه‌ی رشته‌ها توسط استانداردهای ICU و NLS، بر اساس پیاده سازی‌های مختلف زبان‌شناسی، خروجی‌های یکسانی را ارائه نمی‌دهند و به همین جهت است که اینبار خروجی منهای یک را دریافت می‌کنیم.

یک نکته: خروجی قطعه کد فوق در سیستم‌های لینوکسی که از .NET Core 2x - 3x. هم استفاده می‌کنند، دقیقا منهای یک است؛ چون پیش‌فرض بومی سازی آن‌ها نیز ICU است.


چگونه می‌توان به همان حالت پیشین مقایسه‌ی رشته‌ها در NET. بازگشت؟

مایکروسافت بسته‌ی نیوگت Microsoft.CodeAnalysis.FxCopAnalyzers را جهت گوشزد کردن نکته‌ی ذکر صریح StringComparison، به روز رسانی کرده‌است. بنابراین بهتر است تا آن‌را به پروژه‌ی خود اضافه کنید. در این حالت اخطارهای مناسبی را جهت یافتن قسمت‌های مشکل‌دار برنامه‌ی خود دریافت می‌کنید. برای مثال برای اینکه در قطعه کد فوق به همان پاسخ متداول 6 برسیم، تنها کافی‌است پارامتر دوم StringComparison را ذکر کنیم:
int idx = s.IndexOf("\n", StringComparison.Ordinal);

و یا حتی می‌توانید فایل csproj پروژه‌ی خود را ویرایش کرده و یک سطر زیر را به آن اضافه کنید:
<ItemGroup>
   <RuntimeHostConfigurationOption Include="System.Globalization.UseNls" Value="true" />
</ItemGroup>
در این حالت کل برنامه‌ی شما بدون هیچ تغییری مانند قبل کار کرده و از سیستم NLS استفاده می‌شود.



کدام متدهای کار با رشته‌ها در دات نت 5، تحت تاثیر این تغییرات قرار گرفته‌اند؟

اگر از متدهای زیر در برنامه‌های خود استفاده می‌کنید، نکته‌ی ذکر پارامتر StringComparison.Ordinal را فراموش نکنید:
System.String.Compare
System.String.EndsWith
System.String.IndexOf
System.String.StartsWith
System.String.ToLower
System.String.ToLowerInvariant
System.String.ToUpper
System.String.ToUpperInvariant
System.Globalization.TextInfo (most members)
System.Globalization.CompareInfo (most members)
System.Array.Sort (when sorting arrays of strings)
System.Collections.Generic.List<T>.Sort() (when the list elements are strings)
System.Collections.Generic.SortedDictionary<TKey,TValue> (when the keys are strings)
System.Collections.Generic.SortedList<TKey,TValue> (when the keys are strings)
System.Collections.Generic.SortedSet<T> (when the set contains strings)


سؤال: اگر متدی پارامتر دوم StringComparison را نداشت چطور؟
اگر به ماخذ «Behavior changes when comparing strings on .NET 5» مراجعه کنید، در انتهای آن جدولی را ارائه داده که دو سطر اول آن، به صورت زیر است:
API                Default behavior       Remarks
string.Compare     CurrentCulture
در این جدول، هر متدی که رفتار پیش‌فرض آن از نوع CurrentCulture است، تحت تاثیر قرار گرفته‌است و متدی مانند string.Contains که رفتار پیش‌فرض آن Ordinal است، از این تغییرات مصون است و نیازی به تغییری ندارد.


برای مطالعه‌ی بیشتر:
Behavior changes when comparing strings on .NET 5+
.NET globalization and ICU.
Globalization breaking changes
بحث و گفتگویی در این مورد