‫نمودار گرافیکی با استفاده از ASP.NET

همیشه دیده بودم هر کسی می‌خواهد در صفحات ASP.NET از نمودارهای گرافیکی و چارت‌ها استفاده کند مستقیماً به سراغ کنترل‌های سنگین و گران قیمتی مثل Dundas، تلریک یا ComponentOne می‌رود. همیشه به خودم می‌گفتم برای کارهای گرافیکی خیلی سبک حتماً کنترل‌های سبک و رایگانی هم وجود دارد.

اخیراً به یک نمودار گرافیکی نیاز پیدا کردم و متوجه شدم خود مایکروسافت کامپوننتی را برای این موضوع منتشر کرده به اسم ASP.NET Charting Control. این کامپوننت که با ASP.NET 3.5 هم کار می‌کند هیچ اجباری به نصب یا تغییر IIS سرور ندارد و جان می‌دهد برای سرورهای اشتراکی. من از این کامپوننت برای بخش آمار کارویس استفاده کردم. برای دیدن این نمونه به بخش آمار کارویس و برای کسب اطلاعات بیشتر راجع به خود کامپوننت، نمونه‌های آن یا نحوه دانلود آن به اینجا مراجعه کنید.

‫Custom Paging بهینه و غیر Declarative در DataGrid

اگر بخواهید از الگوی MVP در برنامه‌های ASP.NET استفاده کنید بایستی بتوانید همه چیز را به CodeBehind انتقال دهید. این یعنی declarative و binding تقریباً تعطیل. یکی از امکانات مهمی که این وسط از دست می‌رود، امکان Paging بهینه کنترل GridView از طریق CodeBeind است. منظور از Paging بهینه فقط نمایش pager در قسمت پایین کنترل نیست، بلکه منظور این است که فقط رکوردهای مورد نیاز برای صفحه جاری (معمولاً ۱۰ تا ۲۰ تا) از دیتابیس فراخوانی شود. کنترل GridView از طریق DataSourceهای موجود در markup مثل ObjectDataSource یا SqlDataSource می‌تواند Paging بهینه را ارائه دهد. اما از طریق CodeBehind، یعنی handle کردن دستی event مربوط به Paging فقط یک Paging غیر بهینه ارائه می‌دهد. چون در CodeBehind راهی برای اعلام تعداد رکوردها به GridView وجود ندارد (+).

می‌شود به جای GridView از کنترل DataGrid استفاده کرد. DataGrid امکان Paging بهینه از داخل Code Behind را به خوبی پشتیبانی کرده و می‌توان آن را با استفاده از الگوی MVP به کار گرفت. برای این کار کافیست از eventهای SortCommand و PageIndexChanged و پراپرتی‌های VirtualItemCount و CurrentPageIndex استفاده نمایید. لطفاً نمونه زیر را ببینید.

‫DataBinding در WPF

به عنوان یک برنامه‌نویس ASP.NET عادت داشتم هر جا که اطلاعات تغییر می‌کند. متود DataBind()‎ را از کنترل‌هایی مثل GridView فراخوانی کنم. در WPF هم می‌خواستم همان کارها را بکنم اما به کمی مشکل برخوردم. در WPF باید کارهای زیر انجام شود:

۱- یکی از کنترل‌های گرید در WPF که امکانات Binding خوبی دارد کنترل DataGrid است. من هم از همین کنترل شروع می‌کنم.

۲- مقدار AutoGenerateColumns را مشابه ASP.NET برابر True قرار می‌دهم.

۳- اطلاعاتی که می‌خواهم با آنها کار کنم را به ItemSource (به جای DataSource) معرفی می‌کنم.

۴- متودی به اسم DataBind وجود ندارد. برای Refresh شدن اطلاعات باید دقت کنیم که مدل Data Binding در WPF چیزی به اسم Observer pattern است. در این الگو خود data به کنترل DataGrid می‌گویند که چه وقتی اطلاعات آنها تغییر کرده و باید اطلاعات Refresh شوند.

۵- برای پیاده‌سازی عملیات Refresh باید اطلاعاتی که قرار است به کنترل‌هایی داده‌ای Bind شوند، اینترفیس INotifyCollectionChanged را پیاده‌سازی کرده باشند. به این ترتیب این وظیفه خود منبع اطلاعاتی است که به استفاده کنندگانش اعلام کند که اطلاعات تغییر یافته.

۶- برای راحتی کار می‌توان از Collectionهای ویژه‌ای مثل BindingList استفاده کرد.

۷- نمی‌دانم این روش Data Binding در WPF خوب است یا بد. اما ظاهراً حق انتخاب دیگری وجود ندارد.

‫مشکل با EnableSEOPaging

یکی از امکانات کنترل گرید تلریک یعنی RadGrid امکان SEO friendly بودن مکانیزم Paging آن می‌باشد. به این معنی که هر کدام از Pageهای گرید، URL مختص به خودشان را دارند. به عبارت دیگر با وارد کردن URL صفحه می‌توان به همان Page متناظر رسید. این امکان در کنترل استاندارد GridView وجود ندارد.

متاسفانه یکی از مشکلاتی که EnableSEOPaging ایجاد می‌کند پاک شدن دیگر کنترل‌های موجود در صفحه به هنگام رفت و آمد بین Pageهای مختلف گرید است. یعنی اگر از صفحه ۱ به صفحه ۲ بروید همه اطلاعات داخل دیگر کنترل‌های صفحه از بین می‌روند. البته این رفتار به نوعی منطقی است و از طبیعت آن ناشی می‌شود.

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

‫اندازه گیری سرعت صفحات ASP.NET

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

۱- یکی از دقیق‌ترین ابزارها برای اندازه‌گیری سرعت صفحات ASP.NET استفاده از Performance Monitor در ویندوز ۲۰۰۸ است.

با استفاده از این ابزار می‌توانید بفمهید اجرای صفحه مورد نظر شما چقدر طول می‌کشد و در کدام یک از مراحل اجرای کد دات‌نت قرار دارد. سه تا از counterهای مفید عبارتند از

  • ‭% Time in Jit (.Net CLR Jit)
  • ‭Request Execution Time (ASP.NET Apps)
  • ‭Requests/Sec (ASP.NET Apps)

۲- ابزار دیگری که بدون دسترسی به سرور هم قابل استفاده است و بیشتر روی مراحل مختلف ساخت شی Page متمرکز است، عبارت است از ابزار Trace.

برای فعال کردن این امکان در ASP.NET باید تنظیمات زیر را در مدخل system.web در web.config قرار دهید:


۳- حواستان باشد زمان مورد نیاز برای JIT را به حساب کندی برنامه نگذارید. dllهای دات‌نت به زبان IL هستند و برای اجرا نیاز دارند به کد ماشین ترجمه شوند. انجام این کار به عهده JIT هست. وقتی که یک برنامه ASP.NET را در IIS قرار می‌دهید، با اولین درخواست برای هر صفحه، کد آن توسط JIT تبدیل به کد ماشین می‌شود. این تبدیل فقط یک بار انجام خواهد شد و برای دفعات بعد cache خواهد شد.

‫یک مثال عملی از Decoupling

من قبلاً هم راجع به Decoupling یا افتراق در نرم‌افزار خیلی چیزها می‌دانستم و همیشه عاشق مزایای آن بودم و دوست داشتم هر کدی که می‌نویسم Decoupling آن در سطح بالایی باشد. اما همان طور که همه می‌دانند یاد گرفتن یک ایده کار خیلی سختی نیست ولی عملی کردن آن چرا.

امروز موقعیت خوبی در یک برنامه ASP.NET پیش آمد که هم مزایای واقعی Decoupling (افتراق) را درک کنم و هم یک مثال ساده از آن اجرا کنم. یکی از امکاناتی که برای قسمت بندی بخش‌های مختلف برنامه و امکان Code Reuse در ASP.NET فراهم شده است امکان User Control است. برنامه‌نویس می‌تواند یک User Control نوشته و آن را در چند جای مختلف استفاده کند. یا حتی صرفاً از آن برای کاهش پیچیدگی استفاده کند. یکی از کارهای رایجی که خودم به شخصه در User Controlها انجام می‌دهم ارتباط با کنترل والد یا همان کنترلی است که کنترل مورد نظر من در آن قرار دارد. در موردی که من امروز با آن برخورد کردم User Control به Parent خود دسترسی پیدا کرده و از طریق FindControl یکی از دکمه‌های آن را Disable/Enable می‌کرد. این موضوع به غیر نازیبایی و ناخوانایی که ایجاد کرده بود باعث شده بود وقتی از این کنترل در جای دیگری استفاده می‌کردم خطا به وجود بیاید چون آن دکمه دیگر در اینجا وجود نداشت.

اعمال Decoupling (افتراق) در اینجا خیلی ساده بود. در User Control مورد بحث یک Event تعریف کردم و Disable/Enable کردن دکمه مورد نظر را به خود ماژول استفاده کننده سپردم. این ماژول فقط در Event تعریف شده مشترک می‌شد و Disable/Enable کردن دکمه را خودش انجام می‌داد. ماژول‌های دیگری هم که از User Control من استفاده می‌کردند هم صرفاً از همین Event استفاده می‌کنند. در واقع دیگر لازم نیست که User Control مورد نظر اطلاعی از وضعیت ماژول‌های والد خود داشته باشد و این یعنی Decoupling (افتراق).

راه حل های ساده

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

ASP.NET Automatic DataBinding

ASP.NET مکانیزم خودکاری برای data binding صفحات دارد. در این مکانیزم همه کنترل‌های data aware صفحه شناسایی شده و توسط موتور ASP.NET با ترتیب معینی که ظاهراً قابل عوض کردن هم نیست data bind می‌شوند. این مکانیزم در بیشتر اوقات خیلی مفید و کار راحت کن است. اما این موضوع وقتی که تعداد کنترل‌های data aware از حد معینی بیشتر می‌شود مشکل زا خواهد شد. یکی از مثال‌های معروف این مشکل وقتی است که یک DropDownList و یک GridView در صفحه قرار دارند. DropDownList به ObjectDataSourceی به نام ods1 و GridView هم به ObjectDataSourceی به نام ods2 وصل هستند. فرض کنید یکی از پارامترهای ods2 به SelectedValue مربوط به DropDownList وصل است. در این حالت بعضی وقت‌ها دچار این مشکل می‌شویم که GridView زودتر از DropDownList مورد DataBinding قرار می‌گیرد. در نتیجه SelectedValue مربوط به DropDownList همیشه خالی بوده و مقدارش نامعتبر خواهد بود.

راه حلی که من همیشه استفاده می‌کردم پاک کردن صورت مسئله بود. یعنی هر نوع DataSource را از صفحه برداشته و خودم عملیات خواندن/نوشتن اطلاعات از/به دیتابیس را به طور دستی بر اساس eventهای مختلف کنترل‌های موجود انجام می‌دادم. نا گفته پیداست که این کار چه زحماتی ایجاد می‌کرده و چه قدر خطا ساز بوده است.

راه حل ساده‌ای که تازگی‌ها پیدا کرده‌ام این است که Data Sourceها و کلیه مکانیزم‌های مربوطه را در صفحه نگه داشته و به اصطلاح کاملاً به صورت declarative programming عمل کنم. اما برای رفع مشکل ناهماهنگی eventها یا اصطلاحاً Event Taming، مکانیزم Data Binding خودکار ASP.NET را از کار انداخته و خودم عملیات Binding یعنی صرفاً معرفی DataSourceها به کنترل‌ها و فراخوانی DataBind کنترل‌ها را به طور دستی از طریق Code Behind انجام می‌دهم. البته از کار انداختن مکانیزم خودکار Data Binding در صفحات ASP.NET کار چندان سختی نیست. فقط کافی‌ست خصیصه DataSourceID را از تمام کنترل‌های صفحه برداشته و بعداً خودتان آنها را از طریق code behind تنظیم کنید. جهت کسب اطلات بیشتر به این پیوند مراجعه کنید.

AutoComplete TextBox

AutoComplete TextBox امکانی است که از طریق آژاکس به صفحات وب اضافه می‌شود. با کمک این امکان وقتی کاربر چند حرف اول کلمه مورد نظرش را در TextBox تایپ می‌کند، تعدادی از موارد مشابه هم به وی نشان داده می‌شود. این موارد مشابه از طریق فراخوانی یک وب سرویس و به شیوه آژاکس به دست client می‌رسند. اجرای چنین راه حلی مستلزم نوشتن یک وب سرویس و استفاده از کنترل‌های خاصی مثل AutoCompleteExtender از کتابخانه Ajax Control Toolkit می‌باشد.

اما اگر از کنترل‌های Telerik استفاده می‌کنید و حجم دیتا خیلی زیاد نیست، یک راه حل ساده‌تر وجود دارد. این راه ساده‌تر استفاده از کنترل استاندارد RadComboBox و تنظیم خاصیت‌های AllowCustomText و Filter آن است. به این ترتیب بدون استفاده از هیچ نوع وب سرویس و هیچ نوع آژاکسی می‌توان به کاربر قبولاند که این Combo خاصیت AutoComplete دارد. البته اگر از Telerik هم استفاده نمی‌کنید، فکر می‌کنم بتوان کنترل DropDownList از ASP.NET را طوری دستکاری کرد که از آن بشود چنین استفاده‌ای کرد.

‫رکورد اضافی در NHibernate

من مشکلی با NHibernate دارم که نمی‌دانم آیا بقیه هم این مشکل را با NHibernate یا دیگر ORMها یا حتی ADO دارند یا نه. البته اصل این مشکل در صفحات ASP.NET Webform وجود دارد. مشکل این است که وقتی می‌خواهم یک آیتم را در دیتابیس ذخیره کنم یا حتی وقتی می‌خواهم یک رکورد را روی صفحه نمایش دهم مجبور می‌شوم یک یا چند رکورد اضافی را هم از دیتابیس بخوانم.

به عنوان مثال یک صفحه ASP.NET را به شکل زیر تصور کنید:





protected void btnSave_Click(object sender, EventArgs e)
{
Company company = new Company()
{
City = City.Find(drpCity.SelectedValue),
Address = txtAddress.Text
};

company.Save();
}


protected void Search()
{
IList result = MyCustomSearch.SearchCompanies(
City.Find(drpCity.SelectedValue), txtAddress.Text);

//display results in GridView
}

در صفحه بالا، همه Cityها به طور خودکار به drpCity بایند می‌شوند. سپس با کلیک روی دکمه btnSave یک Company با کمک اطلاعات txtAddress و drpCity ذخیره می‌شود. البته کد بالا با Castle ActiveRecord نوشته شده ولی اصل مشکل با دیگر ORMها همان است. همان طور در متود btnSave_Click مشاهده می‌کنید من مجبور هستم برای ذخیره آبجکت Company یک بار به دیتابیس مراجعه کرده و City مورد نظر را از آن فراخوانی نمایم. در متود Search هم همین اتفاق می‌افتد چون متود SearchCompanies پارامتری از نوع City نیاز دارد.

مدتی است که با خودم فکر می‌کنم اگر بتوان این مراجعات بی‌مورد به دیتابیس را حذف کرد، به performance بسیار بهتری می‌رسیم. به همین دلیل به دنبال راه‌هایی هستم که نیاز به load مجدد object را از بین ببرد. در اولین قدم می‌توان همه‌ی متودهایی را که به عنوان ورودی به کل object نیاز دارند را پیدا کرده و آنها را طوری تغییر دهیم که صرفاً بتوانند با ID آبجکت مورد نظر کار کنند. مثلاً متود زیر را در نظر بگیرید:

IList SearchCompanies(City city, string address)
{
var query = from company in ActiveRecordLinq.AsQueryable()
where City == city
select company;

//...
}

این متود را به راحتی می‌توان طوری تغییر داد که به جای City از CityID استفاده کند:

IList SearchCompanies(long cityID, string address)
{
var query = from company in ActiveRecordLinq.AsQueryable()
where City.ID == cityID
select company;

//...
}

Web Development Server and IIS

I am using “ASP.NET Development Server” with Visual Studio 2010 Ultimate x64 on a Windows Server 2008 R2 x64 machine. Development Server is my primary development and debug my ASP.NET applications. After my work is done, the website moves to an IIS 7.5 on a Windows Server 2008 R2 x64 (same machine) with “DefaultAppPool”.

I was used to think that an application’s behavior is as same as in both “ASP.NET Development Server” and “IIS”. Both recently realized they are not as same as I think. I found 3 situations that they behave differently:

1. Server.MapPath behaved differently between them. Path that returned from one was different from other one. More detail available here.

2. Object comparison in LINQ-to-Object queries with comparing only objects themselves is not possible in IIS, this comparison should be done with comparing object’s Ids.

3. Our data access layer is implemented via Castle ActiveRecord. In an arbitrary method properties of an entity was updating without calling its .Save() method. Despite I agree this is a bug itself, the method was working properly in ASP.NET Development Server but was not working in IIS.

I’m not the only person that think ASP.NET Development Server and IIS behaves differently. Other people like this think like me.

At this moment I’m curios about How Application Pool affects IIS and what is Application Pool of ASP.NET Development Server. Additionally Pipeline mode may be a cause of many of this problems. Pipeline mode in IIS 6 and below was classic that means they was using ISAPI but in IIS 7 and above a new Pipeline has been emerged. This new Pipeline is named “Integrated” and do not use ISAPI.