مطالب
آموزش MDX Query - قسمت سوم - نصب Adventure Work DW و تهیه ی پایگاه داده ی Multidimensional Database توسط SSAS

برای ادامه دادن این سری از مقالات آموزش MDX Query نیاز می‌باشد که پایگاه داده‌ی Advwnture Work DW را نصب کرده و سپس توسط SSAS عمل Deploy را انجام دهیم تا پایگاه داده‌ی Multidimensional Database توسط SSAS ساخته شود .

در ابتدا می‌بایست فایل نصب پایگاه داده ی  Advwnture Work را دانلود نمایید برای این منظور به آدرس زیر رفته و فایل AdventureWorks2008R2_SR1.exe  را دانلود نمایید .

  http://www.general-files.biz/download/gs4ac37d18h17i0/AdventureWorks2008R2_SR1.exe.html

یا به آدرس زیر مراجعه کنید

https://msftdbprodsamples.codeplex.com/releases/view/59211 

نیاز می‌باشد قبل از شروع به نصب نرم افزار SQL Server Management Studio را ببندید.

سپس مراحل زیر را انجام دهید.

1. فایل AdventureWorks2008R2_SR1.exe را اجرا نمایید. 

2.  کمی صبر کنید تا صفحه‌ی زیر نمایش داده شود. و گزینه‌ی I Accept … را انتخاب نماید و دکمه‌ی Next را بزنید. 

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

4. در صورتی که از ویندوز‌های Server 2003, XP , Win7 استفاده کنید صفحه‌ی زیر را خواهید دید. در این صفحه ابتدا Instance مربوط به SQL سرور خود را انتخاب نماید (در صورت داشتن چندین Instance روی سرور پایگاه داده) سپس مسیر نصب فایل‌های Sample را مشخص نمایید (بعدا از همین مسیر اقدام به Deploy کردن پایگاه داده‌ی Multidimensional خواهیم کرد) و پیش فرض‌ها را بپذیرید و دکمه‌ی Install را بزنید. 

5. کمی صبر کنید تا نصب انجام گردد. و در انتها کلید Finish را بزنید. 

پس از مراحل بالا (به جز ویندوز 8) با باز کردن نرم افزار SQL Server Management Studio و اتصال به سرویس Database Engine در قسمت Database تصویر زیر را خواهید دید (البته امکان دارد شما از قبل دارای پایگاه داده‌های شخصی بوده باشید که بنابر این آنها نیز در لیست شما وجود خواهند داشت)  

همچنین شما می‌توانید از پنجره‌ی Object Explorer در قسمت Connect اقدام به اتصال به سرویس SSAS نموده . 

و در پنجره‌ی باز شده Server Name را انتخاب نمایید (با توجه به اینکه شما در حال حاضر می‌خواهید به SSAS موجود در سیستم Local متصل شوید ، بنابر این انتخاب سرور Local با وارد کردن کاراکتر (.) انجام می‌شود.) 

بعد از اتصال شکل زیر را خواهید داشت و در شاخه‌ی Database همچنان هیچ Multidimensional Database ی نخواهید داشت.(بعد از عمل Deploy که در ادامه آموزش داده خواهد شد پایگاه داده‌ی Multidimensional ساخته می‌شود.) 

 

تنها روشی که تاکنون برای  نصب پایگاه داده‌ی Adventure Work DW برروی ویندوز 8 یافته ام (البته کمی غیر حرفه ای می‌باشد.) به صورت زیر می‌باشد.

فایل بالا را ( AdventureWorks2008R2_SR1.exe ) روی سیستم عامل‌های ( Server 2003,XP,Win 7 ) نصب کرده (به عنوان یک سیستم عامل واسط) و سپس سرویس Database Engine را Stop کرده و فایل‌های پایگاه داده را به سیستم عامل ویندوز 8 انتقال داده و به صورت دستی Restore کنیم.

مراحل ایجاد پایگاه داده‌ی Multidimensional در ویندوز‌های مختلف ، یکسان می‌باشد.

 

بعد از نصب پایگاه داده‌ی Adventure Work DW باید به شاخه‌ی نصب Sample بروید (همان مسیری که در مراحل نصب وارد کردیم و البته آدرس پیش فرض آن C:\Program Files\Microsoft Sql Server\100\Tools\Sample می‌باشد.)

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

سپس به زیر شاخه‌ی \ AdventureWorks 2008R2 Analysis Services Project\enterprise بروید و فایل Adventure Works.sln را با Visual Studio 2010 باز کنید.

احتمال دارد که نیاز باشد روی کل شاخه‌ی enterprise در قسمت Security کاربر جاری را Add کنید و به آن دسترسی Full Control بدهید تا عملیات Convert این پروژه به درستی انجام شود.

پس از باز کردن پروژه در Visual Studio 2010 صفحه ای مطابق تصویر زیر در پنجره‌ی Solution Explorer خواهید دید. 

به هیچ عنوان نگران ساختار این پروژه نباشید ، زیرا در مقاله‌های آیند شرح کاملی در این خصوص کار با Business Intelligence Management Studio خواهم داد. فعلا هدف ما ایجاد پایگاه داده‌ی Multidimensional  می باشد.

 

برای ساخت پایگاه داده‌ی Multidimensional مراحل زیر را دنبال نمایید.

1. در ابتدا روی پروژه کلیک راست کرده و گزینه‌ی Properties را انتخاب نمایید. 

2. در قسمت Configuration Properties  منوی Deployment را انتخاب کرده و اطمینان حاصل کنید که سرور شما LocalHost و نام پایگاه داده شما Adventure Works DW 2008R2  باشد. 

3. سپس روی Adventure Works.ds کلیک راست کنید تا تنظیمات Connection String  به DW را انجام دهیم. مطابق شکل زیر 

4. سپس در پنجره‌ی باز شده دکمه‌ی Edit  را بزنید . 

5. و در صفحه باز شده تنظیمات زیر را مطابق تصویر زیر انجام دهید. دقت داشته باشید که تغییرات را از بالا به پایین باید انجام دهید و قبل از زدن دکمه‌ی OK حتما Test Connection را بزنید تا از صحت تنظیمات مطمعا شوید. 

6. سپس دو بار دکمه‌ی OK را در دوصفحه کلیک کنید. (بعد از این مراحل شما آماده‌ی Deploy کردن می‌باشد)

7. در ابتدا پروژه را Build نمایید ( CTRl + Shift + B )  و اطمینان حاصل کنید که Build  با موفقیت انجام می‌شود.

8. در انتها برروی نام پروژه کلیک راست نمایید و گزینه‌ی Deploy را انتخاب نمایید. فرایند Deploy کردن می‌تواند کمی زمان بر باشد بنابر این شکیبا باشید و در انتها پیام Deployment Completed Successfully را دریافت خواهید کرد. 

9. حال به SQL Server Management Studio بروید و به سرویس SSAS کانکت شوید . در قسمت DataBase یک پایگاه داده با نام Adventure Works DW 2008R2 مشاهده خواهید کرد . 

 

به شما تبریک می‌گویم اینک شما یک پایگاه داده‌ی Multidimensional را ساخته اید .

در مقاله‌ی بعدی توضیحاتی در خصوص BIMS (Business Intelligence Management Studio) خواهم داد و همچنین اولین MDX Query  را خواهیم نوشت.

 

مطالب
بررسی کارآیی کوئری‌ها در SQL Server - قسمت پنجم - خواندن Query Plans
برای هر کوئری که به SQL Server ارسال می‌شود، یک Plan تولید خواهد شد. این عملیات نیز توسط بخش Query Optimizer آغاز می‌گردد. به آن می‌توان همانند فریم‌ورکی که درون SQL Server قرار گرفته و کارش یافتن یک Query Plan مناسب مخصوص کوئری رسیده‌است، نگاه کرد. ابتدا عملیات Parsing صورت می‌گیرد. توسط آن Syntax کوئری رسیده بررسی شده و صحت آن تائید می‌گردد. پس از آن یک Parser tree تولید می‌شود که نمای درونی آن کوئری است. سپس فاز Binding رخ می‌دهد که در آن بررسی می‌شود که آیا تمام اشیاء موجود درخواستی توسط کوئری وجود داشته و توسط کاربر قابل دسترسی هستند. خروجی این فاز یک Query Tree است که به فاز بهینه سازی ارسال می‌شود. یک Query Tree به همراه اعمالی منطقی است. این اعمال منطقی توصیف رخ‌دادهایی می‌باشند که قرار است اتفاق بیفتند؛ مانند خواندن اطلاعات از یک جدول، مرتب سازی اطلاعات، ایجاد جوین و غیره. سپس بهینه ساز، این اعمال منطقی را تبدیل به اعمال فیزیکی می‌کند. برای مثال خواندن اطلاعات از یک جدول، تبدیل به یک Index seek می‌شود. یک جوین تبدیل به یک حلقه‌ی تو در تو می‌شود. در آخر این اعمال فیزیکی در کنار هم قرار گرفته و Query Plan را تشکیل می‌دهند و ما به عنوان یک توسعه دهنده می‌توانیم با بررسی این Plan دریابیم که SQL Server با کوئری رسیده، چگونه برخورد کرده و قرار است چگونه آن‌را اجرا کند.


Plan چیست؟



در اینجا Plan کوئری ساده‌ای را مشاهده می‌کنید. کار آن انتخاب نام، نام خانوادگی و آدرس ایمیل افرادی است که نام خانوادگی آن‌ها با Whit شروع می‌شود و بر روی دو جدول که با هم جوین شده‌اند عمل می‌کند.
اولین موردی را که باید در یک Plan به آن دقت کرد، عملگرهای آن است که شامل select، nested loop، index seek و clustered index seek می‌باشند. index seek بر روی جدول اشخاص و clustered index seek بر روی جدول ایمیل‌ها صورت می‌گیرد. nested loop بیانگر جوین بین جداول است. این عملگرها بیانگر اعمال فیزیکی هستند که رخ داده‌اند.
همچنین تعدادی پیکان (arrow) را هم مشاهده می‌کنید که بیانگر جهت سیلان داده‌ها است. اطلاعات از طریق index seek و clustered index seek به nested loop می‌رسند و در نهایت به عملگر select ارائه خواهند شد.
در این تصویر، هزینه‌های تخمینی مرتبط با هر عملگر نیز قابل مشاهده‌است که نسبت به کل کوئری محاسبه شده‌اند. این هزینه، بدون واحد است و به معنای میزان زمان و یا CPU صرف شده‌ی برای انجام عمل خاصی نیست و صرفا برای مقایسه‌ی هزینه‌ی نسبی عملگرها در کل یک Plan کاربرد دارد. باید دقت داشت که هزینه‌های نمایش داده شده‌ی در یک Plan، همیشه تخمینی هستند. در قسمت‌های قبل در مورد نحوه‌ی دریافت estimated plan و actual plan بحث کردیم. هیچگاه چیزی به نام Actual cost در یک Actual plan وجود ندارد و همیشه تخمینی است. روش محاسبه‌ی آن‌ها توسط الگوریتم‌های بهینه ساز است و مستقل از سخت افزار مورد استفاده.

در یک پلن، مدت زمان انجام یک کوئری، میزان I/O ، locks و wait statistics قابل مشاهده نیستند. البته اگر از SQL Server 2016 به بعد استفاده می‌کنید و یک Actual plan را محاسبه کرده‌اید، مدت زمان انجام یک کوئری و میزان I/O نیز در Plan قابل مشاهده‌اند.


از چه جهتی باید یک Plan را خواند؟

اگر هدف، بررسی «سیلان کنترل» است (Control flow)، باید یک Plan را از «چپ به راست» خواند. یعنی از عملگر select شروع می‌کنیم که کوئری ما را کنترل می‌کند. سپس به nested loop می‌رسیم که نام و نام خانوادگی را از جدول اشخاص دریافت می‌کند. این nested loop نیز با کمک ایندکس‌های تعریف شده، شرط کوئری را بر آورده می‌کند.
اما جهت «سیلان اطلاعات» در یک Plan از «راست به چپ» است (Data flow). اطلاعات از طریق index seekها به حلقه و سپس select می‌رسند.


چگونه یک Query Plan را شروع به بررسی کنیم؟

ابتدا در management studio از منوی Query، گزینه‌ی Include actual execution plan را انتخاب می‌کنیم. سپس کوئری زیر را اجرا می‌کنیم:
USE [WideWorldImporters];
GO

SELECT
    [s].[StateProvinceName],
    [s].[SalesTerritory],
    [s].[LatestRecordedPopulation],
    [s].[StateProvinceCode]
FROM [Application].[Countries] [c]
    JOIN [Application].[StateProvinces] [s]
    ON [s].[CountryID] = [c].[CountryID]
WHERE [c].[CountryName] = 'United States';
GO
نتیجه‌ی آن تولید Query Plan زیر است:


در اینجا چهار عملگر select، nested loop، clustered index seek و clustered index scan مشاهده می‌شوند. شاید اینطور به نظر برسد که در این Plan، ابتدا clustered index scan و clustered index seek انجام می‌شوند و سپس به nested loop می‌رسیم (اگر Plan را بر اساس سیلان داده، از راست به چپ بخوانیم)؛ اما اینطور نیست. عملگرها در اینجا در حقیقت یک سری iterator هستند که با دریافت ردیف‌های مرتبط، بلافاصله آن‌ها را به nested loop ارسال می‌کنند. این nested loop نیز ردیف‌هایی را که با جوین انجام شده تطابق دارند، به سمت select ارسال می‌کند.
اگر به تصویر دقت کنید هر کدام از ایندکس‌ها به یک جدول اشاره می‌کنند که نام آن بالای عدد هزینه درج شده‌است. برای مشاهده نام کامل شیء متناظر با آن، می‌توان اشاره‌گر ماوس را بر روی ایندکس حرکت داد و به اطلاعات قسمت Object دقت کرد:


و یا اگر اطلاعات کاملتری از این popup را نیاز داشتید، عملگر مدنظر را انتخاب کرده و سپس دکمه‌ی F4 را فشار دهید:



در برگه‌ی خواص ظاهر شده می‌توان ریز جزئیات تمام اطلاعات مرتبط با عملگر انتخاب شده را مشاهده کرد. برای مثال در اینجا حتی اطلاعات Logical reads را بدون روشن کردن SET STATISTICS IO ON می‌توان مشاهده کرد:


همچنین با توجه به انتخاب گزینه‌ی Include actual execution plan، تعداد ردیف‌های بازگشت داده شده‌ی واقعی و تخمینی، با هدایت اشاره‌گر ماوس بر روی یکی از اشیاء مرتبط با بررسی ایندکس‌ها، قابل مشاهده هستند:


گزارش این تعداد ردیف‌ها، با حرکت اشاره‌گر ماوس، بر روی پیکان‌های منتهی به nested loop و یا select نیز قابل مشاهده هستند:


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

اکنون در ادامه سعی می‌کنیم توسط DMO's، این Plan را از Plan cache دریافت کنیم:
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT [cp].[size_in_bytes],
    [cp].[cacheobjtype],
    [cp].[objtype],
    [cp].[plan_handle],
    [dest].[text],
    [plan].[query_plan]
FROM [sys].[dm_exec_cached_plans] [cp]
CROSS APPLY [sys].[dm_exec_sql_text]([cp].[plan_handle]) [dest]
CROSS APPLY [sys].[dm_exec_query_plan]([cp].[plan_handle]) [plan]
WHERE [dest].[text] LIKE '%StateProvinces%'
OPTION(MAXDOP
1,
RECOMPILE);
ستون آخر این کوئری به query_plan اشاره می‌کند که در management studio به صورت یک لینک قابل کلیک ظاهر می‌شود. اگر بر روی آن کلیک کنیم، به تصویر زیر خواهیم رسید:


همانطور که مشاهده می‌کنید، اینبار تنها اطلاعات تخمینی در این Plan ظاهر شده‌اند؛ چون اطلاعات آن از کش خوانده شده‌است. همچنین در اینجا اطلاعات I/O مانند حالت Actual Plan، در برگه‌ی خواص عملگرهای این Plan، قابل مشاهده نیستند.


نگاهی به اطلاعات XML ای یک Plan

اگر کوئری زیر را با فرض انتخاب Include actual execution plan در منوی Query اجرا کنیم:
SELECT
    [o].[OrderID],
    [ol].[OrderLineID],
    [o].[OrderDate],
    [o].[CustomerID],
    [ol].[Quantity],
    [ol].[UnitPrice]
FROM [Sales].[Orders] [o]
    JOIN [Sales].[OrderLines] [ol]
    ON [o].[OrderID] = [ol].[OrderID];
GO
به این Plan خواهیم رسید که نوع بررسی ایندکس‌ها و جوین آن متفاوت است:


در اینجا با کلیک راست بر روی Plan، می‌توان گزینه‌ی Show Execution Plan XML را نیز انتخاب کرد. گاهی از اوقات کار کردن با این اطلاعات، به صورت XML ای ساده‌تر است و فرمت آن از هر نگارش به نگارش دیگر SQL Server می‌تواند متفاوت باشد.
برای مثال اگر در برگه‌ی نمایش این اطلاعات، دکمه‌های ctrl+f را فشرده و به دنبال runtime بگردیم، خیلی سریعتر می‌توان به اطلاعات I/O ،CPU و تعداد ردیف‌های بازگشت داده شده، رسید.


و یا حتی اطلاعات wait statistics را نیز می‌توان به سادگی در اینجا مشاهده کرد تا مشخص شود چرا یک کوئری خوب عمل نمی‌کند:



اجرای چند کوئری با هم و بررسی Query Plan آن‌ها

اگر دو کوئری زیر را با فرض انتخاب Include actual execution plan در منوی Query با هم اجرا کنیم:
USE [WideWorldImporters];
GO

SELECT
    [CustomerID],
    [TransactionAmount]
FROM [Sales].[CustomerTransactions]
WHERE [CustomerID] = 1056;
GO


SELECT
    [o].[OrderID],
    [ol].[OrderLineID],
    [o].[OrderDate],
    [o].[CustomerID],
    [ol].[Quantity],
    [ol].[UnitPrice]
FROM [Sales].[Orders] [o]
    JOIN [Sales].[OrderLines] [ol]
    ON [o].[OrderID] = [ol].[OrderID];
GO
به این Plan خواهیم رسید که نکته‌ی مهم آن، هزینه‌ی انجام کوئری‌ها است:


هزینه‌ی اولین کوئری نسبت به کل batch جاری، 10 درصد است و هزینه‌ی دومین کوئری، 90 درصد. بنابراین اگر چندین کوئری را با هم اجرا کنیم، به این صورت می‌توان هزینه‌ی هر کدام را نسبت به کل عملیات، تخمین بزنیم. در هر کوئری نیز هزینه‌هایی درج شده‌اند که صرفا متعلق به همان کوئری هستند. برای مثال در اولین کوئری، key lookup سنگین‌ترین عملگر کل کوئری است.
مطالب
استفاده از modal dialogs مجموعه Twitter Bootstrap برای گرفتن تائید از کاربر
یکی دیگر از اجزای تعاملی Twitter Bootstrap صفحات modal هستند. صفحات modal بر روی صفحه جاری ظاهر شده و کنترل آن‌را در دست می‌گیرند و تا زمانیکه این صفحه ویژه بسته نشود، امکان استفاده از صفحه زیرین، وجود نخواهد داشت. برای استفاده از این امکان ویژه، ابتدا باید یک لینک یا دکمه‌ای، جهت فراخوانی اسکریپت‌های صفحات modal در صفحه تدارک دیده شود.
برای طراحی یک صفحه modal چهار div باید اضافه شوند. بیرونی‌ترین div باید دارای کلاس modal مجموعه Bootstrap باشد. می‌توان به کلاس modal در اینجا کلاس‌های hide fade را هم برای نمونه اضافه کرد. در این حالت، نمایش و بسته شدن صفحه modal به همراه پویانمایی ویژه‌ای خواهد بود.
داخل این div، سه div با کلاس‌های modal-header برای نمایش هدر، modal-body برای نمایش محتوایی در این صفحه modal و modal-footer برای تدارک محتوای footer این صفحه، قرار خواهند گرفت.
در این بین هر لینکی با ویژگی data-dismiss=modal، سبب بسته شدن خودکار صفحه باز شده خواهد شد.

افزونه bootstrapModalConfirm

اگر نکات یاد شده را بخواهیم کپسوله کنیم، می‌توان یک افزونه جدید جی‌کوئری را با نام فایل jquery.bootstrap-modal-confirm.js برای این منظور تدارک دید:
// <![CDATA[
(function ($) {
    $.bootstrapModalConfirm = function (options) {
        var defaults = {
            caption: 'تائید عملیات',
            body: 'آیا عملیات درخواستی اجرا شود؟',
            onConfirm: null,
            confirmText: 'تائید',
            closeText: 'انصراف'
        };
        var options = $.extend(defaults, options);

        var confirmContainer = "#confirmContainer";
        var html = '<div class="modal hide fade" id="confirmContainer">' +
                   '<div class="modal-header">' +
                        '<a class="close" data-dismiss="modal">&times;</a>'
                        + '<h5>' + options.caption + '</h5></div>' +
                   '<div class="modal-body">'
                        + options.body + '</div>' +
                   '<div class="modal-footer">'
                        + '<a href="#" class="btn btn-success" id="confirmBtn">' + options.confirmText + '</a>'
                    + '<a href="#" class="btn" data-dismiss="modal">' + options.closeText + '</a></div></div>';

        $(confirmContainer).remove();
        $(html).appendTo('body');
        $(confirmContainer).modal('show');

        $('#confirmBtn', confirmContainer).click(function () {
            if (options.onConfirm)
                options.onConfirm();
            $(confirmContainer).modal('hide');           
        });
    };
})(jQuery);
// ]]>
در اینجا به صورت پویا div یک صفحه modal ایجاد و دو دکمه تائید و لغو به همراه نمایش یک هدر و همچنین محتوایی به کاربر به صفحه اضافه می‌شود. سپس افزونه modal در حالت show، روی این div فراخوانی می‌گردد. در اینجا اگر کاربر بر روی دکمه تائید کلیک کرد، یک callback به نام onConfirm فراخوانی می‌گردد.


مثالی از استفاده از افزونه bootstrapModalConfirm

@{
    ViewBag.Title = "Index";
}
<h2>
    Index</h2>
<a href="#" class="btn btn-danger" id="deleteBtn">حذف رکورد</a>

@section JavaScript
{
    <script type="text/javascript">
        $(function () {
            $("#deleteBtn").click(function (e) {
                e.preventDefault(); //می‌خواهیم لینک به صورت معمول عمل نکند
                $.bootstrapModalConfirm({
                                    caption: 'تائید عملیات',
                                    body: 'آیا عملیات درخواستی اجرا شود؟',
                                    onConfirm: function () {
                                        alert('در حال انجام عملیات'); 
                                    },
                                    confirmText: 'تائید',
                                    closeText: 'انصراف'
                });
            });
        });
    </script>
}
در مثال فوق، اگر کاربر بر روی لینک حذف رکورد، که به صورت یک دکمه مزین شده است کلیک کند، در صورت تائید، قسمت onConfirm اجرا خواهد شد.


مطالب دوره‌ها
عیب یابی و دیباگ برنامه‌های SignalR
1) برنامه SignalR در IE کار نمی‌کند.
پردازشگر json، در نگارش‌های اخیر IE به آن اضافه شده است. برای رفع این مشکل در نگارش‌های قدیمی، نیاز است از اسکریپت کمکی http://nuget.org/List/Packages/json2 استفاده نمائید. همچنین مرورگر IE را نیز باید وادار ساخت تا بر اساس آخرین موتور پردازشی خود کار کند:
 <meta http-equiv="X-UA-Compatible" content="IE=edge" />

2) هنگام فراخوانی مسیر signalr/hubs پیغام 404 (یافت نشد) دریافت می‌شود.
برای رفع این مشکل ابتدا اطمینان حاصل کنید که تنظیمات مسیریابی تعریف شده در فایل global.asax.cs موجود هستند.
در ادامه اطمینان حاصل نمائید مسیر اسکریپت‌های signalr/hubs به درستی تعریف شده‌اند:
 <script type="text/javascript" src="/signalr/hubs"></script>
برای مثال در برنامه‌های MVC و وب فرم‌ها تعریف صحیح باید به شکل زیر باشد:
 MVC:
<script type="text/javascript" src="@Url.Content("~/signalr/hubs")"></script>

Web forms:
<script type="text/javascript" src='<%= ResolveClientUrl("~/signalr/hubs") %>'></script>
همچنین وجود تنظیمات ذیل را در فایل وب کانفیگ برنامه نیز بررسی کنید:
<configuration>
   <system.webServer>
        <modules runAllManagedModulesForAllRequests="true">
        </modules>
    </system.webServer>
</configuration>

3) متدهای سمت کلاینت من فراخوانی نمی‌شوند.
بهترین راه برای مشاهده ریز جرئیات خطاها، ذکر سطر ذیل در کدهای سمت کلاینت جاوا اسکریپتی برنامه است:
 $.connection.hub.logging = true;
و سپس مراجعه به کنسول جاوا اسکریپت مرورگر برای بررسی خطاهای لاگ شده.

4) خطای «Connection must be started before data can be sent» را دریافت می‌کنم.
همانطور که در قسمت قبل عنوان شد، کلیه فراخوانی‌های SignalR از نوع غیرهمزمان هستند. بنابراین باید با استفاده از callback و زمان فراخوانی آن‌ها که عموما پس از برقراری اتصال رخ می‌دهد، نسبت به انجام امور دلخواه اقدام کرد.
               var connection = $.hubConnection('http://localhost:8081/');
               proxy = connection.createProxy('collectionhub')
               connection.start()
                    .done(function () {
                        proxy.invoke('subscribe', 'Product');
                        $('#messages').append('<li>invoked subscribe</li>');
                    })
                    .fail(function () { alert("Could not Connect!"); });
همانطور که در این مثال مشاهده می‌کنید، سطر proxy.invoke در یک callback فراخوانی شده است و نه بلافاصله در سطری پس از connection.start. هر زمان که اتصال به نحو موفقیت آمیزی برقرار شد، آنگاه متد subscribe در سمت سرور فراخوانی می‌گردد.
در حالت استفاده بدون پروکسی نیز چنین callbackهایی قابل تعریف هستند:
$.connection.hub.start()
                .done(function() {
                    myHub.server.SomeFunction(SomeParam) //e.g. a login or init
                         .done(connectionReady); 
                })
                .fail(function() {
                    alert("Could not Connect!");
                 });

5) بعد از 10 اتصال به IIS، برنامه متوقف می‌شود.
این مورد، محدودیت ذاتی IIS 7 نصب شده بر روی ویندوز 7 است. بهتر است از یک IIS کامل موجود در ویندوزهای سرور استفاده کنید. در این سرورها عدد پیش فرض تنظیم شده 5000 اتصال است که در صورت نیاز با استفاده از دستور زیر قابل تغییر است:
 appcmd.exe set config /section:system.webserver/serverRuntime /appConcurrentRequestLimit:100000
به علاوه ASP.NET نیز محدودیت 5000 اتصال به ازای هر CPU را دارد. برای تغییر آن باید به مسیر ذیل مراجعه
  %windir%\Microsoft.NET\Framework\v4.0.30319\aspnet.config
و سپس مقدار maxConcurrentRequestsPerCPU را تنظیم کرد:
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
    <runtime>
        <legacyUnhandledExceptionPolicy enabled="false" />
        <legacyImpersonationPolicy enabled="true"/>
        <alwaysFlowImpersonationPolicy enabled="false"/>
        <SymbolReadingPolicy enabled="1" />
        <shadowCopyVerifyByTimestamp enabled="true"/>
    </runtime>
    <startup useLegacyV2RuntimeActivationPolicy="true" />
    <system.web>
        <applicationPool maxConcurrentRequestsPerCPU="20000" />
    </system.web>
</configuration>
به علاوه ASP.NET پس از رد شدن از حد maxConcurrentRequestsPerCPU، درخواست‌ها را در صف قرار می‌دهد. این مورد نیز قابل تنظیم است. ابتدا به مسیر ذیل مراجعه کرده
 %windir%\Microsoft.NET\Framework\v4.0.30319\Config\machine.config
و سپس در صورت نیاز و لزوم، مقدار requestQueueLimit را تغییر دهید:
 <processModel autoConfig="false" requestQueueLimit="250000" />
مطالب
پیاده سازی عملیات CRUD در Kendo UI Treeview یک پروژه‌ی ASP.NET MVC
در این مقاله می‌خواهیم عملیات CRUD را بر روی Telerik kendo treeview  در یک پروژه‌ی ASP.NET MVC پیاده سازی کنیم. شکل کلی این پروژه به صورت زیر می‌باشد:


که اینجا دکمه‌ها از سمت راست به چپ، عملیات افزودن، عدم انتخاب، ویرایش و حذف را انجام می‌دهند. کدهای HTML این پنل را در ادامه مشاهده می‌کنید:

<div id="CrudPanel" class="row treeview-panel" >
      <div class="col-lg-7 pull-right">
           <input type="text" id="txtLocationTitle" class="form-control" />
      </div>
      <div class="col-lg-5 pull-left" style="text-align: left;">
           <button data-toggle="tooltip" data-placement="left" title="افزودن" id="btnAddLocation" class="btn btn-sm btn-success">
                <i class="fa fa-plus"></i>
           </button>
           <button data-toggle="tooltip" data-placement="left" title="عدم انتخاب" id="btnUnSelect" class="btn btn-sm btn-info">
                <i class="fa fa-square-o"></i>
           </button>
           <button data-toggle="tooltip" data-placement="left" title="ویرایش" id="btnEditLocation" class="btn btn-sm btn-warning">
                <i class="fa fa-pencil"></i>
           </button>
           <button data-toggle="tooltip" data-placement="left" title="حذف" id="btnDeleteLocation" class="btn btn-sm btn-danger">
                <i class="fa fa-times"></i>
           </button>
      </div>
</div>


و قطعه کد ذیل مربوط به پنل ویرایش است که در ابتدای کار کلاس hide به آن انتساب داده شده و پنهان می‌شود:

<div id="EditPanel" class="row edit hide treeview-panel">
     <div class="col-lg-7 pull-right">
          <input type="text" id="txtLocationEditTitle" class="form-control" />
     </div>
     <div class="col-lg-5 pull-left" style="text-align: left">
          <input type="button" value="ویرایش" id="btnEditPanelLocation" data-code="" data-parentId="" class="btn btn-sm btn-success" />
          <input type="button" value="انصراف" id="btnCancle" class="btn btn-sm btn-info" />
     </div>
</div>


در آخر این تکه کد نیز مربوط به KendoUI TreeView است:

 <div class="col-lg-6 k-rtl treeview-style">
                    @(Html.Kendo()
                          .TreeView()
                          .Name("treeview")
                          .DataTextField("Title")
                          .DragAndDrop(false)
                          .DataSource(dataSource => dataSource
                          .Model(model => model.Id("Id"))
                          .Read(read => read.Action(MVC.Admin.Location.ActionNames.GetAllAssetGroupTree, MVC.Admin.Location.Name)))
                    )
                </div>


یک نکته

- کلاس k-rtl مربوط به خود treeview می‌باشد و با این کلاس، درخت ما راست به چپ می‌شود.


در ادامه css‌های مربوط به کلاس‌های treeview-style ،hide و treeview-panel بررسی خواهند شد:

.treeview-style {
    min-height: 86px;
    max-height: 300px;
    overflow: scroll;
    overflow-x: hidden;
    position: relative;
}
.treeview-panel {
    background-color: #eee;
    padding: 25px 0 25px 0;
}
.hide {
    display: none;
}


تا اینجای مقاله، کدهای Html و Css موجود را بررسی کردیم. حالا سراغ قسمت اصلی خواهیم رفت. یعنی عملیات CRUD.


لازم به ذکر است در ابتدای قسمت script  باید این چند خط کد نوشته شود:

 var treeview = null;
    $(window).load(function () {
        treeview = $("#treeview").data("kendoTreeView");
    });

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


پیاده سازی عملیات افزودن: 

 $(document).on('click', '#btnAddLocation', function () {
        var title = $('#txtLocationTitle').val();
        var selectedNodeId = null;
        var selectedNode = treeview.select();
        if (selectedNode.length == 0) {
            selectedNode = null;
        }
        else {
            selectedNodeId = treeview.dataItem(selectedNode).id;// گرفتن آی دی گره انتخاب شده
        }
        $.ajax({
            url: '@Url.Action(MVC.Admin.Location.CreateByAjax())',
            type: 'POST',
            data: { Title: title, ParentId: selectedNodeId },
            success: function (data) {
                debugger;
                showMessage(data.message, data.notificationType);
                if (data.result)
                    treeview.dataSource.read();
            },
            error: function () {
                showMessage('لطفا مجددا تلاش نمایید', 'warning');
            }
        });

    });

توضیحات: مقدار گره جدید را خوانده و در متغیر title قرار می‌دهیم. گره انتخاب شده را توسط این خط

var selectedNode = treeview.select();

می گیریم و سپس در ادامه بررسی خواهیم کرد تا اگر گره‌ای انتخاب نشده باشد، به کاربر پیغامی را نشان دهد؛ در غیر این صورت توسط ajax، مقادیر مورد نظر، به اکشن ما در LocationController ارسال می‌شوند:

 [HttpPost]
        public virtual ActionResult CreateByAjax(AddLocationViewModel locationViewModel)
        {
            if (ModelState.IsNotValid())
                return JsonResult(false, "عنوان نباید خالی و یا کمتر از دو کاراکتر باشد.", NotificationType.Error);
            var result = _locationService.Add(locationViewModel);//سرویس مورد نظر برای اضافه کردن به دیتابیس
            switch (result)
            {
                case AddStatus.AddSuccessful:
                    _uow.SaveChanges();
                    return JsonResult(true, Messages.SaveSuccessfull, NotificationType.Success);
                case AddStatus.Faild:
                    return JsonResult(false, Messages.SaveFailed, NotificationType.Error);
                case AddStatus.Exists:
                    return JsonResult(false, Messages.DataExists, NotificationType.Warning);
                default:
                    return JsonResult(false, Messages.SaveFailed, NotificationType.Error);
            }
        }


   public virtual JsonResult JsonResult(bool result, string message, string notificationType)
        {
            return Json(new { result = result, message = message, notificationType = notificationType }, JsonRequestBehavior.AllowGet);
        }

اکشن JsonResult  که مقادیر نتیجه، پیغام و نوع اطلاع رسانی را می‌گیرد و یک آبجکت از نوع json را به تابع success ای‌جکس، ارسال می‌کند.


 public class AddLocationViewModel
    {
        [DisplayName("عنوان")]
        [Required(ErrorMessage ="لطفا عنوان گروه را وارد نمایید"),MinLength(2,ErrorMessage ="طول عنوان خیلی کوتاه می‌باشد ")]
        public string Title { get; set; }
        [DisplayName("گروه پدر")]
        public Guid? ParentId { get; set; }

    }

این کلاس viewModel ما می‌باشد.


  public enum AddStatus
    {
        AddSuccessful,
        Faild,
        Exists
    }

و این مورد هم کلاس AddStatus از نوع enum.


  public class Messages
    {
        #region  Fields

        public const string SaveSuccessfull = "اطلاعات با موفقیت ذخیره شد";
        public const string SaveFailed = "خطا در ثبت اطلاعات";
        public const string DeleteMessage = "کابر گرامی ، آیا از حذف کردن این رکورد مطمئن هستید ؟";
        public const string DeleteSuccessfull = "اطلاعات با موفقیت حذف شد";
        public const string DeleteFailed = "خطا در حذف اطلاعات ، لطفا مجددا تلاش نمایید";
        public const string DeleteHasInclude = "کاربر گرامی ، رکورد مورد نظر هم اکنون در بانک اطلاعاتی سیستم در حال استفاده توسط منابع دیگر می‌باشد";
        public const string NotFoundData = "اطلاعات یافت نشد";
        public const string NoAttachmentSelect = "تصویری انتخاب نشده است";
        public const string DataExists = "اطلاعات وارد شده در بانک اطلاعاتی موجود می‌باشد";
        public const string DeletedRowHasIncluded = "کاربر گرامی ، رکوردی که قصد حذف آن را دارید هم اکنون در بانک اطلاعاتی سیستم ، توسط سایر بخش‌ها در حال استفاده می‌باشد";
        
        #endregion
    }

و این موارد هم مقادیر ثابت فیلد‌های مورد استفاده‌ی ما در کلاس Message.


پیاده سازی عملیات حذف

به طور اختصار، عملیات حذف را توضیح می‌دهم تا به قسمت اصلی مقاله یعنی ویرایش بپردازیم:

$(document).on('click', '#btnDeleteLocation', function () {
        var selectedNode = treeview.select();
        var currentNode = treeview.dataItem(selectedNode);
        if (selectedNode.length == 0) {
            showMessage('گزینه ای انتخاب نشده است. لطفا یک گزینه انتخاب نمایید', 'warning');
        } else {
            var selectedNodeId = treeview.dataItem(selectedNode).id;
            if (currentNode.hasChildren) {
                var title = 'کاربر گرامی ، با حذف شدن این گره، تمام زیر شاخه‌های آن حذف می‌شود. آیا مطمئن هستید ؟ ';
                DeleteConfirm(selectedNodeId, '@Url.Action(MVC.Admin.Location.DeleteByAjax())', title);
            } else {
                $.ajax({
                    url: '@Url.Action(MVC.Admin.Location.DeleteByAjax())',
                    type: 'POST',
                    data: { id: selectedNodeId },
                    success: function (data) {
                        debugger;
                        showMessage(data.message, data.notificationType);
                        if (data.result)
                            treeview.remove(selectedNode);
                    },
                    error: function () {
                        showMessage('لطفا مجددا تلاش نمایید', 'warning');
                    }
                });
            }
        }
    });

این مورد نیز همانند عملیات افزودن عمل می‌کند. یعنی ابتدا چک می‌کند که آیا گره‌ای انتخاب شده است یا خیر؟ و اگر گره انتخابی ما دارای فرزند باشد، به کاربر پیغامی را نشان می‌دهد و می‌گوید «گره مورد نظر، دارای فرزند است. آیا مایل به حذف تمام فرزندان آن هستید؟» مانند تصویر زیر:



در نهایت چه گره انتخابی دارای فرزند باشد و چه نباشد، به یک مسیر مشترک ارسال می‌شوند:

  public virtual ActionResult DeleteByAjax(Guid id)
        {
            var result = _locationService.Delete(id);
            switch (result)
            {
                case DeleteStatus.Successfull:
                    _uow.SaveChanges();
                    return DeleteJsonResult(true, Messages.DeleteSuccessfull, NotificationType.Success);
                case DeleteStatus.NotFound:
                    return DeleteJsonResult(false, Messages.NotFoundData, NotificationType.Error);
                case DeleteStatus.Failed:
                    return DeleteJsonResult(false, Messages.DeleteFailed, NotificationType.Error);
                case DeleteStatus.ThisRowHasIncluded:
                    return DeleteJsonResult(false, Messages.DeletedRowHasIncluded, NotificationType.Warning);
                default:
                    return DeleteJsonResult(false, Messages.DeleteFailed, NotificationType.Error);
            }
        }


در سرویس مورد نظر ما یعنی Delete، اگه گره‌ای دارای فرزند باشد، تمام فرزندان آن را حذف می‌کند. حتی فرزندان فرزندان آن را:

  public DeleteStatus Delete(Guid id)
        {
            var model = GetAsModel(id);
            if (model == null) return DeleteStatus.NotFound;
            if (!CanDelete(model)) return DeleteStatus.ThisRowHasIncluded;
            _uow.MarkAsSoftDelete(model, _userManager.GetCurrentUserId());

            if (model.Children.Any())
                DeleteChildren(model);
            return DeleteStatus.Successfull;
        }


  private void DeleteChildren(Location model)
        {
            foreach (var item in model.Children)
            {
                _uow.MarkAsSoftDelete(item, _userManager.GetCurrentUserId());
                if (item.Children.Any())
                    DeleteChildren(item);
            }
        }


  public class Location:BaseEntity,ISoftDelete
    {
        public string Title { get; set; }
        public Location Parent { get; set; }
        public Guid? ParentId { get; set; }
        public bool IsDeleted { get; set; }

        public virtual ICollection<Location> Children { get; set; }
}

 و این هم مدل Location که سمت سرور از مدل استفاده می‌کنیم.


پیاده سازی عملیات ویرایش

حالا به قسمت اصلی مقاله رسیدیم. در اینجا قرار است گره‌ای را انتخاب نماییم و با زدن دکمه ویرایش و باز شدن پنل آن، آن را ویرایش کنیم. با زدن دکمه ویرایش، کدهای زیر اجرا می‌شوند:

    // Open Edit Panel
    $(document).on('click', '#btnEditLocation', function () {
        debugger;
        var selectedNode = treeview.select();
        var currentNode = treeview.dataItem(selectedNode);// با استفاده از این خط، گره انتخاب شده جاری را می‌گیریم.


        if (selectedNode.length == 0) {
//این شرط به ما می‌گوید اگر گره ای انتخاب نشده بود پیغامی به کاربر نمایش بده
            showMessage('گزینه ای انتخاب نشده است. لطفا یک گزینه انتخاب نمایید', 'warning');
        } else {
            var selectedNodeCode = treeview.dataItem(selectedNode).Code;
            var selectedNodeTitle = treeview.dataItem(selectedNode).Title;
            var selectedNodeParentId = treeview.dataItem(selectedNode).ParentId;
// آی دی یا کد، عنوان و آی دی پدر گره انتخاب شده را با استفاده از این سه خط در اختیار می‌گیریم
            $('#CrudPanel').toggleClass('hide'); //المنت کرادپنل که در حال حاضر کاربر آن را می‌بیند، با این خط کد، پنهان می‌شود
            $('#EditPanel').toggleClass('hide'); //المنت ادیت پنل که در حال حاضر از دید کاربر پنهان است، قابل نمایش می‌شود

            $("#txtLocationEditTitle").val(selectedNodeTitle);
//عنوان گره ای که می‌خواهیم آن را ویرایش کنیم در تکست باکس مورد نظر قرار می‌گیرد
            $("#txtLocationEditTitle").focusTextToEnd();
// با استفاده از این پلاگین، کرسر ماوس در انتهای مقدار دیفالت تکست باکس قرار می‌گیرد
            $("#btnEditPanelLocation").attr('data-code', selectedNodeCode);
            $("#btnEditPanelLocation").attr('data-parentId', selectedNodeParentId == null ? '' : selectedNodeParentId);
//مقادیر پرنت آی دی و کد را در دیتا اتریبیوت‌های موجود در المنت خودمان قرار می‌دهیم
            // Disable clicking in treeview
            $("#treeview").children().bind('click', function () { return false; });
        }
    });

  (function ($) {
        $.fn.focusTextToEnd = function () {
            this.focus();
            var $thisVal = this.val();
            this.val('').val($thisVal);
            return this;
        }
    }(jQuery));

کد زیر باعث می‌شود تا زمانیکه پنل ویرایش باز است، کاربر نتواند هیچ کلیکی را در عناصر داخل درخت ما، داشته باشد.

            $("#treeview").children().bind('click', function () { return false; });


و در نهایت با زدن دکمه ویرایش، پنل ویرایش ما به صورت زیر باز می‌شود:


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

با تغییر عنوان تکست باکس و زدن دکمه‌ی ویرایش، رویداد زیر رخ می‌دهد:

  // Edit tree node
    $(document).on('click', '#btnEditPanelLocation', function () {
        debugger;
        var code = $("#btnEditPanelLocation").attr('data-code');
        var parentId = $("#btnEditPanelLocation").attr('data-parentId');
        var title = $("#txtLocationEditTitle").val().trim();
        $.ajax({
            url: '@Url.Action(MVC.Admin.Location.EditByAjax())',
            type: 'POST',
            data: { Code: code, Title: title, ParentId: parentId.length === 0 ? null : parentId },
            success: function (data) {
                debugger;
                showMessage(data.message, data.notificationType);
                if (data.result) {
                    treeview.dataSource.read();
                    CloseEditPanel();
                }
            },
            error: function () {
                showMessage('لطفا مجددا تلاش نمایید', 'warning');
            }
        });
    });


  [HttpPost]
        public virtual ActionResult EditByAjax(EditLocationViewModel editLocationViewModel)
        {

            if (ModelState.IsNotValid())
                return JsonResult(false,"عنوان نباید خالی و یا کمتر از دو کاراکتر باشد.", NotificationType.Error);
            var result = _locationService.Edit(editLocationViewModel);
            switch (result)
            {
                case EditStatus.Successful:
                    _uow.SaveChanges();
                    return JsonResult(true, Messages.SaveSuccessfull, NotificationType.Success);
                case EditStatus.NotFound:
                    return JsonResult(false, Messages.NotFoundData, NotificationType.Error);
                case EditStatus.Faild:
                    return JsonResult(false, Messages.SaveFailed, NotificationType.Error);
                case EditStatus.Exists:
                    return JsonResult(false, Messages.DataExists, NotificationType.Warning);
                default:
                    return JsonResult(false, Messages.SaveFailed, NotificationType.Error);
            }
        }


تابع CloseEditPanel  بعد از اتمام ویرایش هر گره و یا با زدن دکمه انصراف در شکل بالا، فراخوانی می‌شود که کد آن به شکل زیر است:

  function CloseEditPanel() {
        $('#CrudPanel').toggleClass('hide');
//پنل کراد ما که در حال حاضر از دید کاربر پنهان است با این خط ظاهر می‌گردد
        $('#EditPanel').toggleClass('hide');
//پنل ویرایش ما که در حال حاضر کاربر آن را می‌بیند، پنهان می‌شود از دید کاربر
        $("#txtLocationEditTitle").val('');
//مقدار تکست باکس خالی می‌شود
        $("#btnEditPanelLocation").attr('data-code', '');
        $("#btnEditPanelLocation").attr('data-parentId', '');
//دیتا اتریبیوت‌های ما که مقادیر کد و آی دی والد در آن قرار گرفته نیز خالی می‌شود
        // Enable clicking in treeview
        $("#treeview").children().unbind('click').bind('click', function () { return true; });
//اگر یادتان باشد با یک خط کد به کاربر اجازه ندادیم که با باز شدن پنل ویرایش، گره دیگری را انتخاب نمایی. حالا این خط کد عکس کد قبلیست و به کاربر اجازه می‌دهد در المنت مورد نظر کلیک کند
    }


   // Cancle edit Node tree
    $(document).on('click', '#btnCancle', function () {
        CloseEditPanel();
    });
  $(document).on('click', '#btnUnSelect', function () {
//رویداد عدم انتخاب
        treeview.select(null);
    });
نظرات اشتراک‌ها
FireFox 18 و مصرف بالای CPU
من از نرم افزار Firefox Ultimate Optimizer در کنارش استفاده می‌کنم ظاهرا که مشکلی ندارم