مطالب
سفارشی سازی Header و Footer در PdfReport
صورت مساله:
- می‌خواهیم footer پیش فرض PdfReport را که تاریخ را در یک سمت، و شماره صفحه را در سمتی دیگر نمایش می‌دهد، به عبارت «صفحه x از n» تغییر دهیم.
- می‌خواهیم در Header گزارش بجای Header پیش فرض PdfReport یکی از قالب‌های PDF تهیه شده توسط Open Office را نمایش دهیم (و یا هر ساختار دیگری را).

تمام اجزای PdfReport جهت امکان اعمال تغییرات کلی و توسعه آن‌ها طراحی شده‌اند؛ قالب‌ها، هدر، فوتر، منابع داده، قالب‌های نمایش سلول‌ها، تعریف توابع تجمعی سفارشی و غیره. جهت سهولت کار، به ازای هر یک از این موارد، پیاده سازی‌های پیش فرضی در PdfReport قرار دارند، امکان اگر مورد رضایت شما نیستند ... از بنیان تغییرشان دهید! (و همچنین اگر مورد جالبی را پیاده سازی کردید، می‌توانید به عنوان یک وصله جدید ارائه دهید تا به پروژه اضافه شود)
ضمنا این مطالب سفارشی سازی نیاز به آشنایی با ساختار iTextSharp را نیز دارند؛ در حد ایجاد یک جدول ساده باید با iTextSharp آشنا باشید.

مدل‌های مورد استفاده:
namespace PdfReportSamples.Models
{
    public class Task
    {
        public int Id { set; get; }
        public string Name { set; get; }
        public int PercentCompleted { set; get; }
        public bool IsActive { set; get; }
        public User Assignee { set; get; }
    }
}

using System;

namespace PdfReportSamples.Models
{
    public class User
    {
        public int Id { set; get; }
        public string Name { set; get; }
        public string LastName { set; get; }
        public long Balance { set; get; }
        public DateTime RegisterDate { set; get; }
    }
}
توسط این مدل‌ها قصد داریم تعدادی فعالیت (Task) را که به تعدادی کاربر انتساب یافته است، نمایش دهیم. همچنین نمایش مقادیر خواص تو در تو  نیز در اینجا مد نظر است؛ برای مثال ستونی مانند این:
 column.PropertyName<Task>(x => x.Assignee.Name) 
کدهای کامل مثال را در ادامه ملاحظه خواهید نمود:
using System;
using System.Collections.Generic;
using System.Drawing;
using PdfReportSamples.Models;
using PdfRpt.Core.Contracts;
using PdfRpt.FluentInterface;

namespace PdfReportSamples.CustomHeaderFooter
{
    public class CustomHeaderFooterPdfReport
    {
        readonly CustomHeader _customHeader = new CustomHeader();
        public IPdfReportData CreatePdfReport()
        {
            return new PdfReport().DocumentPreferences(doc =>
            {
                doc.RunDirection(PdfRunDirection.LeftToRight);
                doc.Orientation(PageOrientation.Portrait);
                doc.PageSize(PdfPageSize.A4);
                doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" });
            })
            .DefaultFonts(fonts =>
            {
                fonts.Path(Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.ttf",
                                  Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf");
            })
            .PagesFooter(footer =>
            {
                footer.CustomFooter(new CustomFooter(footer.PdfFont, PdfRunDirection.LeftToRight));
            })
            .PagesHeader(header =>
            {
                header.CustomHeader(_customHeader);
            })
            .MainTableTemplate(template =>
            {
                template.BasicTemplate(BasicTemplate.SilverTemplate);
            })
            .MainTablePreferences(table =>
            {
                table.ColumnsWidthsType(TableColumnWidthType.Relative);
                table.MultipleColumnsPerPage(new MultipleColumnsPerPage
                {
                    ColumnsGap = 22,
                    ColumnsPerPage = 2,
                    ColumnsWidth = 250,
                    IsRightToLeft = false,
                    TopMargin = 7
                });
            })
            .MainTableDataSource(dataSource =>
            {
                var rows = new List<Task>();
                var rnd = new Random();
                for (int i = 1; i < 210; i++)
                {
                    rows.Add(new Task
                    {
                        Assignee = new User
                        {
                            Id = i,
                            Name = "user-" + i
                        },
                        IsActive = rnd.Next(0, 2) == 1 ? true : false,
                        Name = "task-" + i
                    });
                }
                dataSource.StronglyTypedList(rows);
            })
            .MainTableColumns(columns =>
            {
                columns.AddColumn(column =>
                {
                    column.PropertyName("rowNo");
                    column.IsRowNumber(true);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(0);
                    column.Width(1);
                    column.HeaderCell("#");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<Task>(x => x.Name);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(1);
                    column.Width(3);
                    column.HeaderCell("Task Name");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<Task>(x => x.Assignee.Name); // nested property support
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(2);
                    column.Width(3);
                    column.HeaderCell("Assignee");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<Task>(x => x.IsActive);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(3);
                    column.Width(2);
                    column.HeaderCell("Active");
                    column.ColumnItemsTemplate(template =>
                    {
                        template.Checkmark(checkmarkFillColor: Color.Green, crossSignFillColor: Color.DarkRed);
                    });
                });
            })
            .MainTableEvents(events =>
            {
                events.DataSourceIsEmpty(message: "There is no data available to display.");
            })
            .Export(export =>
            {
                export.ToExcel();
            })
            .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\CustomHeaderFooterPdfReportSample.pdf"));
        }
    }
}

به همراه Header سفارشی:
using System.Collections.Generic;
using iTextSharp.text;
using iTextSharp.text.pdf;
using PdfRpt.Core.Contracts;
using PdfRpt.Core.Helper;

namespace PdfReportSamples.CustomHeaderFooter
{
    public class CustomHeader : IPageHeader
    {
        public PdfPTable RenderingGroupHeader(Document pdfDoc, PdfWriter pdfWriter, IList<CellData> rowdata, IList<SummaryCellData> summaryData)
        {
            return null;
        }

        Image _image;
        public PdfPTable RenderingReportHeader(Document pdfDoc, PdfWriter pdfWriter, IList<SummaryCellData> summaryData)
        {
            if (_image == null) //cache is empty
            {
                var templatePath = AppPath.ApplicationPath + "\\data\\PdfHeaderTemplate.pdf";
                _image = PdfImageHelper.GetITextSharpImageFromPdfTemplate(pdfWriter, templatePath);
            }

            var table = new PdfPTable(1);
            var cell = new PdfPCell(_image, true) { Border = 0 };
            table.AddCell(cell);
            return table;
        }
    }
}

و Footer سفارشی استفاده شده:
using System.Collections.Generic;
using iTextSharp.text;
using iTextSharp.text.pdf;
using PdfRpt.Core.Contracts;

namespace PdfReportSamples.CustomHeaderFooter
{
    public class CustomFooter : IPageFooter
    {
        PdfContentByte _pdfContentByte;
        readonly IPdfFont _pdfRptFont;
        readonly Font _font;
        readonly PdfRunDirection _direction;
        PdfTemplate _template;

        public CustomFooter(IPdfFont pdfRptFont, PdfRunDirection direction)
        {
            _direction = direction;
            _pdfRptFont = pdfRptFont;
            _font = _pdfRptFont.Fonts[0];
        }

        public void ClosingDocument(PdfWriter writer, Document document, IList<SummaryCellData> columnCellsSummaryData)
        {
            _template.BeginText();
            _template.SetFontAndSize(_pdfRptFont.Fonts[0].BaseFont, 8);
            _template.SetTextMatrix(0, 0);
            _template.ShowText((writer.PageNumber - 1).ToString());
            _template.EndText();
        }

        public void PageFinished(PdfWriter writer, Document document, IList<SummaryCellData> columnCellsSummaryData)
        {
            var pageSize = document.PageSize;
            var text = "Page " + writer.PageNumber + " / ";
            var textLen = _font.BaseFont.GetWidthPoint(text, _font.Size);
            var center = (pageSize.Left + pageSize.Right) / 2;
            var align = _direction == PdfRunDirection.RightToLeft ? Element.ALIGN_RIGHT : Element.ALIGN_LEFT;

            ColumnText.ShowTextAligned(
                        canvas: _pdfContentByte,
                        alignment: align,
                        phrase: new Phrase(text, _font),
                        x: center,
                        y: pageSize.GetBottom(25),
                        rotation: 0,
                        runDirection: (int)_direction,
                        arabicOptions: 0);

            var x = _direction == PdfRunDirection.RightToLeft ? center - textLen : center + textLen;
            _pdfContentByte.AddTemplate(_template, x, pageSize.GetBottom(25));
        }

        public void DocumentOpened(PdfWriter writer, IList<SummaryCellData> columnCellsSummaryData)
        {
            _pdfContentByte = writer.DirectContent;
            _template = _pdfContentByte.CreateTemplate(50, 50);
        }
    }
}

البته لازم به ذکر است که تمام این کدها به پوشه Samples سورس پروژه نیز جهت سهولت دسترسی، اضافه شده‌اند .

توضیحات:

برای پیاده سازی Header و Footer سفارشی در PdfReport نیاز خواهید داشت تا دو اینترفیس IPageHeader و IPageFooter را پیاده سازی کنید.
ساختار IPageHeader را در ذیل ملاحظه می‌کنید:
using System.Collections.Generic;
using iTextSharp.text;
using iTextSharp.text.pdf;

namespace PdfRpt.Core.Contracts
{
    public interface IPageHeader
    {
        PdfPTable RenderingGroupHeader(Document pdfDoc, PdfWriter pdfWriter, IList<CellData> newGroupInfo, IList<SummaryCellData> summaryData);

        PdfPTable RenderingReportHeader(Document pdfDoc, PdfWriter pdfWriter, IList<SummaryCellData> summaryData);
    }
}

RenderingGroupHeader مرتبط است به مباحث گروه بندی اطلاعات و گزارشات master-detail که در قسمت‌های بعد به آن‌ها اشاره خواهد شد. چون در اینجا به آن نیازی نداشتیم، تنها کافی است متد متناظر با آن، null بر گرداند که در کلاس CustomHeader فوق قابل مشاهده است.
متد RenderingReportHeader به ازای تولید هر صفحه جدید، فراخوانی خواهد شد. به عبارتی می‌توانید در صفحات مختلف، هدرهای مختلفی را نمایش دهید.
خروجی هر دو متد در اینجا یک جدول از نوع PdfPTable است. بنابراین هر نوع ساختار دلخواهی را که علاقمند هستید به شکل یک PdfPTable ایجاد کرده و بازگشت دهید. این جدول در هدر صفحات ظاهر خواهد شد.
برای نمونه در کلاس CustomHeader، یک قالب تهیه شده توسط Open Office توسط متد توکار PdfImageHelper.GetITextSharpImageFromPdfTemplate دریافت و تبدیل به تصویر می‌شود. این تصویر از نوع تصاویر قابل درک توسط iTextSharp است و نه اینکه واقعا تبدیل به یک تصویر معمولی مثلا از نوع bmp شود. سپس این تصویر، در یک ردیف از جدولی قرار داده شده و این جدول بازگشت داده می‌شود.
در کل یا توسط کار با PdfPTable می‌توانید یک هدر غیرپیش فرض را طراحی کنید و یا می‌توانید توسط ابزارهای بصری مانند Open Office یک قالب خاص را برای آن تهیه کرده و به روشی که ذکر شد و کدهای آن‌را ملاحظه می‌کنید، بارگذاری و استفاده کنید. این قالب‌ها در مسیر Bin\Data سورس‌های پروژه قرار داده شده‌اند.

ساختار IPageFooter به صورت زیر است:
using iTextSharp.text;
using iTextSharp.text.pdf;
using System.Collections.Generic;

namespace PdfRpt.Core.Contracts
{
    public interface IPageFooter
    {
        void DocumentOpened(PdfWriter writer, IList<SummaryCellData> columnCellsSummaryData);

        void PageFinished(PdfWriter writer, Document document, IList<SummaryCellData> columnCellsSummaryData);

        void ClosingDocument(PdfWriter writer, Document document, IList<SummaryCellData> columnCellsSummaryData);
    }
}

برای طراحی یک Footer سفارشی کافی است اینترفیس فوق را پیاده سازی کنید که نمونه‌ای از آن‌را در کدهای کلاس CustomFooter ملاحظه می‌نمائید.
متد DocumentOpened، با وهله سازی شیء Document فراخوانی می‌شود.
متد PageFinished هر بار پیش از اتمام کار صفحه جاری و افزوده شدن آن به Document فراخوانی می‌گردد.
متد ClosingDocument، در زمان بسته شدن شیء Document فراخوانی خواهد شد.

اگر به امضای این متدها دقت کنید، شیء PdfWriter در اختیار شما قرار گرفته است که توسط آن می‌توان مستقیما بر روی فایل PDF، محتوایی را قرار داد. شیء Document نیز در دسترس است. مثلا توسط آن می‌توان اندازه دقیق صفحه را بدست آورد.
به علاوه پارامتر columnCellsSummaryData نیز امکان دسترسی به مقادیر ردیف‌های قبلی را در اختیار شما قرار می‌دهد. برای مثال اگر نیاز دارید تا بر اساس مقادیر ستون‌ها و ردیف‌های قبلی، محاسباتی را انجام داده و در پایین صفحات درج کنید، به این ترتیب دسترسی کاملی به آن‌ها، خواهید داشت.

استفاده از این کلاس‌های سفارشی نیز همواره به شکل زیر خواهد بود:
readonly CustomHeader _customHeader = new CustomHeader();
//...
.PagesFooter(footer =>
{
   footer.CustomFooter(new CustomFooter(footer.PdfFont, PdfRunDirection.LeftToRight));
})
.PagesHeader(header =>
{
  header.CustomHeader(_customHeader);
})
کلا در PdfReport هر جایی متدی به نام CustomXYZ را مشاهده کردید، این متد یک اینترفیس را دریافت می‌کند. به عبارتی این امکان را خواهید داشت تا از متدهای پیش فرض مهیا صرفنظر کرده و مطابق نیاز، نسبت به پیاده سازی و استفاده از وهله جدیدی از این اینترفیس تعریف شده، اقدام کنید.
مطالب
ASP.NET MVC #10

آشنایی با روش‌های مختلف ارسال اطلاعات یک درخواست به کنترلر

تا اینجا با روش‌های مختلف ارسال اطلاعات از یک کنترلر به View متناظر آن آشنا شدیم. اما حالت عکس آن چطور؟ مثلا در ASP.NET Web forms، دوبار بر روی یک دکمه کلیک می‌کردیم و در روال رویدادگردان کلیک آن، همانند برنامه‌های ویندوزی، دسترسی به اطلاعات اشیاء قرار گرفته بر روی فرم را داشتیم. در ASP.NET MVC که کلا مفهوم Events را حذف کرده و وب را همانگونه که هست ارائه می‌دهد و به علاوه کنترلرهای آن، ارجاع مستقیمی را به هیچکدام از اشیاء بصری در خود ندارند (برای مثال کنترلر و متدی در آن نمی‌دانند که الان بر روی View آن، یک گرید قرار دارد یا یک دکمه یا اصلا هیچی)، چگونه می‌توان اطلاعاتی را از کاربر دریافت کرد؟
در اینجا حداقل سه روش برای دریافت اطلاعات از کاربر وجود دارد:
الف) استفاده از اشیاء Context مانند HttpContext، Request، RouteData و غیره
ب) به کمک پارامترهای اکشن متدها
ج) با استفاده از ویژگی جدیدی به نام Data Model Binding

یک مثال کاربردی
قصد داریم یک صفحه لاگین ساده را طراحی کنیم تا بتوانیم هر سه حالت ذکر شده فوق را در عمل بررسی نمائیم. بحث HTML Helpers استاندارد ASP.NET MVC را هم که در قسمت قبل شروع کردیم، لابلای توضیحات قسمت جاری و قسمت‌های بعدی با مثال‌های کاربردی دنبال خواهند شد.
بنابراین یک پروژه جدید خالی ASP.NET MVC را شروع کرده و مدلی را به نام Account با محتوای زیر به پوشه Models برنامه اضافه کنید:

namespace MvcApplication6.Models
{
public class Account
{
public string Name { get; set; }
public string Password { get; set; }
}
}

یک کنترلر جدید را هم به نام LoginController به پوشه کنترلرهای برنامه اضافه کنید. بر روی متد Index پیش فرض آن کلیک راست نمائید و یک View خالی را اضافه نمائید.
در ادامه به فایل Global.asax.cs مراجعه کرده و نام کنترلر پیش‌فرض را به Login تغییر دهید تا به محض شروع برنامه در VS.NET، صفحه لاگین ظاهر شود.
کدهای کامل کنترلر لاگین را در ادامه ملاحظه می‌کنید:

using System.Web.Mvc;
using MvcApplication6.Models;

namespace MvcApplication6.Controllers
{
public class LoginController : Controller
{
[HttpGet]
public ActionResult Index()
{
return View(); //Shows the login page
}

[HttpPost]
public ActionResult LoginResult()
{
string name = Request.Form["name"];
string password = Request.Form["password"];

if (name == "Vahid" && password == "123")
ViewBag.Message = "Succeeded";
else
ViewBag.Message = "Failed";

return View("Result");
}

[HttpPost]
[ActionName("LoginResultWithParams")]
public ActionResult LoginResult(string name, string password)
{
if (name == "Vahid" && password == "123")
ViewBag.Message = "Succeeded";
else
ViewBag.Message = "Failed";

return View("Result");
}

[HttpPost]
public ActionResult Login(Account account)
{
if (account.Name == "Vahid" && account.Password == "123")
ViewBag.Message = "Succeeded";
else
ViewBag.Message = "Failed";

return View("Result");
}
}
}

همچنین Viewهای متناظر با این کنترلر هم به شرح زیر هستند:
فایل index.cshtml به نحو زیر تعریف خواهد شد:

@model MvcApplication6.Models.Account
@{
ViewBag.Title = "Index";
}
<h2>
Login</h2>
@using (Html.BeginForm(actionName: "LoginResult", controllerName: "Login"))
{
<fieldset>
<legend>Test LoginResult()</legend>
<p>
Name: @Html.TextBoxFor(m => m.Name)</p>
<p>
Password: @Html.PasswordFor(m => m.Password)</p>
<input type="submit" value="Login" />
</fieldset>
}
@using (Html.BeginForm(actionName: "LoginResultWithParams", controllerName: "Login"))
{
<fieldset>
<legend>Test LoginResult(string name, string password)</legend>
<p>
Name: @Html.TextBoxFor(m => m.Name)</p>
<p>
Password: @Html.PasswordFor(m => m.Password)</p>
<input type="submit" value="Login" />
</fieldset>
}
@using (Html.BeginForm(actionName: "Login", controllerName: "Login"))
{
<fieldset>
<legend>Test Login(Account acc)</legend>
<p>
Name: @Html.TextBoxFor(m => m.Name)</p>
<p>
Password: @Html.PasswordFor(m => m.Password)</p>
<input type="submit" value="Login" />
</fieldset>
}

و فایل result.cshtml هم محتوای زیر را دارد:

@{
ViewBag.Title = "Result";
}
<fieldset>
<legend>Login Result</legend>
<p>
@ViewBag.Message</p>
</fieldset>

توضیحاتی در مورد View لاگین برنامه:
در View صفحه لاگین سه فرم را مشاهده می‌کنید. در برنامه‌های ASP.NET Web forms در هر صفحه، تنها یک فرم را می‌توان تعریف کرد؛ اما در ASP.NET MVC این محدودیت برداشته شده است.
تعریف یک فرم هم با متد کمکی Html.BeginForm انجام می‌شود. در اینجا برای مثال می‌شود یک فرم را به کنترلری خاص و متدی مشخص در آن نگاشت نمائیم.
از عبارت using هم برای درج خودکار تگ بسته شدن فرم، در حین dispose شیء MvcForm کمک گرفته شده است.
برای نمونه خروجی HTML اولین فرم تعریف شده به صورت زیر است:

<form action="/Login/LoginResult" method="post">   
<fieldset>
<legend>Test LoginResult()</legend>
<p>
Name: <input id="Name" name="Name" type="text" value="" /></p>
<p>
Password: <input id="Password" name="Password" type="password" /></p>
<input type="submit" value="Login" />
</fieldset>
</form>

توسط متدهای کمکی Html.TextBoxFor و Html.PasswordFor یک TextBox و یک PasswordBox به صفحه اضافه می‌شوند، اما این For آن‌ها و همچنین lambda expression ایی که بکارگرفته شده برای چیست؟
متدهای کمکی Html.TextBox و Html.Password از نگارش‌های اولیه ASP.NET MVC وجود داشتند. این متدها نام خاصیت‌ها و پارامترهایی را که قرار است به آن‌ها بایند شوند، به صورت رشته می‌پذیرند. اما با توجه به اینکه در اینجا می‌توان یک strongly typed view را تعریف کرد،‌ تیم ASP.NET MVC بهتر دیده است که این رشته‌ها را حذف کرده و از قابلیتی به نام Static reflection استفاده کند (^ و ^).

با این توضیحات، اطلاعات سه فرم تعریف شده در View لاگین برنامه، به سه متد متفاوت قرار گرفته در کنترلری به نام Login ارسال خواهند شد. همچنین با توجه به مشخص بودن نوع model که در ابتدای فایل تعریف شده، خاصیت‌هایی را که قرار است اطلاعات ارسالی به آن‌ها بایند شوند نیز به نحو strongly typed تعریف شده‌اند و تحت نظر کامپایلر خواهند بود.


توضیحاتی در مورد نحوه عملکرد کنترلر لاگین برنامه:

در این کنترلر صرفنظر از محتوای متدهای آن‌ها، دو نکته جدید را می‌توان مشاهده کرد. استفاده از ویژگی‌های HttpPost، HttpGet و ActionName. در اینجا به کمک ویژگی‌های HttpGet و HttpPost در مورد نحوه دسترسی به این متدها، محدودیت قائل شده‌ایم. به این معنا که تنها در حالت Post است که متد LoginResult در دسترس خواهد بود و اگر شخصی نام این متدها را مستقیما در مرورگر وارد کند (یا همان HttpGet پیش فرض که نیازی هم به ذکر صریح آن نیست)، با پیغام «یافت نشد» مواجه می‌گردد.
البته در نگارش‌های اولیه ASP.NET MVC از ویژگی دیگری به نام AcceptVerbs برای مشخص سازی نوع محدودیت فراخوانی یک اکشن متد استفاده می‌شد که هنوز هم معتبر است. برای مثال:

[AcceptVerbs(HttpVerbs.Get)]

یک نکته امنیتی:
همیشه متدهای Delete خود را به HttpPost محدود کنید. به این علت که ممکن است در طی مثلا یک ایمیل، آدرسی به شکل http://localhost/blog/delete/10 برای شما ارسال شود و همچنین سشن کار با قسمت مدیریتی بلاگ شما نیز در همان حال فعال باشد. URL ایی به این شکل، در حالت پیش فرض، محدودیت اجرایی HttpGet را دارد. بنابراین احتمال اجرا شدن آن بالا است. اما زمانیکه متد delete را به HttpPost محدود کردید، دیگر این نوع حملات جواب نخواهند داد و حتما نیاز خواهد بود تا اطلاعاتی به سرور Post شود و نه یک Get ساده (مثلا کلیک بر روی یک لینک معمولی)، کار حذف را انجام دهد.


توسط ActionName می‌توان نام دیگری را صرفنظر از نام متد تعریف شده در کنترلر، به آن متد انتساب داد که توسط فریم ورک در حین پردازش نهایی مورد استفاده قرار خواهد گرفت. برای مثال در اینجا به متد LoginResult دوم، نام LoginResultWithParams را انتساب داده‌ایم که در فرم دوم تعریف شده در View لاگین برنامه مورد استفاده قرار گرفته است.
وجود این ActionName هم در مثال فوق ضروری است. از آنجائیکه دو متد هم نام را معرفی کرده‌ایم و فریم ورک نمی‌داند که کدامیک را باید پردازش کند. در این حالت (بدون وجود ActionName معرفی شده)، برنامه با خطای زیر مواجه می‌گردد:

The current request for action 'LoginResult' on controller type 'LoginController' is ambiguous between the following action methods:
System.Web.Mvc.ActionResult LoginResult() on type MvcApplication6.Controllers.LoginController
System.Web.Mvc.ActionResult LoginResult(System.String, System.String) on type MvcApplication6.Controllers.LoginController

برای اینکه بتوانید نحوه نگاشت فرم‌ها به متدها را بهتر درک کنید، بر روی چهار return View موجود در کنترلر لاگین برنامه، چهار breakpoint را تعریف کنید. سپس برنامه را در حالت دیباگ اجرا نمائید و تک تک فرم‌ها را یکبار با کلیک بر روی دکمه لاگین، به سرور ارسال نمائید.


بررسی سه روش دریافت اطلاعات از کاربر در ASP.NET MVC

الف) استفاده از اشیاء Context

در ویژوال استودیو، در کنترلر لاگین برنامه، بر روی کلمه Controller کلیک راست کرده و گزینه Go to definition را انتخاب کنید. در اینجا بهتر می‌توان به خواصی که در یک کنترلر به آن‌ها دسترسی داریم، نگاهی انداخت:

public HttpContextBase HttpContext { get; }
public HttpRequestBase Request { get; }
public HttpResponseBase Response { get; }
public RouteData RouteData { get; }

در بین این خواص و اشیاء مهیا، Request و RouteData بیشتر مد نظر ما هستند. در مورد RouteData در قسمت ششم این سری، توضیحاتی ارائه شد. اگر مجددا Go to definition مربوط به HttpRequestBase خاصیت Request را بررسی کنیم، موارد ذیل جالب توجه خواهند بود:

public virtual NameValueCollection QueryString { get; } // GET variables
public NameValueCollection Form { get; } // POST variables
public HttpCookieCollection Cookies { get; }
public NameValueCollection Headers { get; }
public string HttpMethod { get; }

توسط خاصیت Form شیء Request می‌توان به مقادیر ارسالی به سرور در یک کنترلر دسترسی یافت که نمونه‌ای از آن‌را در اولین متد LoginResult می‌توانید مشاهده کنید. این روش در ASP.NET Web forms هم کار می‌کند. جهت اطلاع این روش با ASP کلاسیک دهه نود هم سازگار است!
البته این روش آنچنان مرسوم نیست؛ چون NameValueCollection مورد استفاده، ایندکسی عددی یا رشته‌ای را می‌پذیرد که هر دو با پیشرفت‌هایی که در زبان‌های دات نتی صورت گرفته‌اند، دیگر آنچنان مطلوب و روش مرجح به حساب نمی‌آیند. اما ... هنوز هم قابل استفاده است.
به علاوه اگر دقت کرده باشید در اینجا HttpContextBase داریم بجای HttpContext. تمام این کلاس‌های پایه هم به جهت سهولت انجام آزمون‌های واحد در ASP.NET MVC ایجاد شده‌اند. کار کردن مستقیم با HttpContext مشکل بوده و نیاز به شبیه سازی فرآیندهای رخ داده در یک وب سرور را دارد. اما این کلاس‌های پایه جدید، مشکلات یاد شده را به همراه ندارند.


ب) استفاده از پارامترهای اکشن متدها

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

Request.Form
Request.QueryString
Request.Files
RouteData.Values

برای نمونه در متدی که با نام LoginResultWithParams مشخص شده، چون نام‌های دو پارامتر آن، با نام‌های بکارگرفته شده در Html.TextBoxFor و Html.PasswordFor یکی هستند، با مقادیر ارسالی آن‌ها مقدار دهی شده و سپس در متد قابل استفاده خواهند بود. در پشت صحنه هم از همان رکوردهای موجود در Request.Form (یا سایر موارد ذکر شده)، استفاده می‌شود. در اینجا هر رکورد مثلا مجموعه Request.Form، کلیدی مساوی نام ارسالی به سرور را داشته و مقدار آن هم، مقداری است که کاربر وارد کرده است.
اگر همانندی یافت نشد، آن پارامتر با نال مقدار دهی می‌گردد. بنابراین اگر برای مثال یک پارامتر از نوع int را معرفی کرده باشید و چون نوع int، نال نمی‌پذیرد، یک استثناء بروز خواهد کرد. برای حل این مشکل هم می‌توان از Nullable types استفاده نمود (مثلا بجای int id نوشت int? id تا مشکلی جهت انتساب مقدار نال وجود نداشته باشد).
همچنین باید دقت داشت که این بررسی تطابق‌های بین نام عناصر HTML و نام پارامترهای متدها، case insensitive است و به کوچکی و بزرگی حروف حساس نیست. برای مثال، پارامتر معرفی شده در متد LoginResult مساوی string name است، اما نام خاصیت تعریف شده در کلاس Account مساوی Name بود.


ج) استفاده از ویژگی جدیدی به نام Data Model Binding

در ASP.NET MVC چون می‌توان با یک Strongly typed view کار کرد، خود فریم ورک این قابلیت را دارد که اطلاعات ارسالی یکی فرم را به صورت خودکار به یک وهله از یک شیء نگاشت کند. در اینجا model binder وارد عمل می‌شود، مقادیر ارسالی را استخراج کرده (اطلاعات دریافتی از Form یا کوئری استرینگ‌ها یا اطلاعات مسیریابی و غیره) و به خاصیت‌های یک شیء نگاشت می‌کند. بدیهی است در اینجا این خواص باید عمومی باشند و هم نام عناصر HTML ارسالی به سرور. همچنین model binder پیش فرض ASP.NET MVC را نیز می‌توان کاملا تعویض کرد و محدود به استفاده از model binder توکار آن نیستیم.
وجود این Model binder، کار با ORMها را بسیار لذت بخش می‌کند؛ از آنجائیکه خود فریم ورک ASP.NET MVC می‌تواند عناصر شیءایی را که قرار است به بانک اطلاعاتی اضافه شود، یا در آن به روز شود، به صورت خودکار ایجاد کرده یا به روز رسانی نماید.
نحوه کار با model binder را در متد Login کنترلر فوق می‌توانید مشاهده کنید. بر روی return View آن یک breakpoint قرار دهید. فرم سوم را به سرور ارسال کنید و سپس در VS.NET خواص شیء ساخته شده را در حین دیباگ برنامه، بررسی نمائید.
بنابراین تفاوتی نمی‌کند که از چندین پارامتر استفاده کنید یا اینکه کلا یک شیء را به عنوان پارامتر معرفی نمائید. فریم ورک سعی می‌کند اندکی هوش به خرج داده و مقادیر ارسالی به سرور را به پارامترهای تعریفی، حتی به خواص اشیاء این پارامترهای تعریف شده، نگاشت کند.

در ASP.NET MVC سه نوع Model binder وجود دارند:
1) Model binder پیش فرض که توضیحات آن به همراه مثالی ارائه شد.
2) Form collection model binder که در ادامه توضیحات آن‌را مشاهده خواهید نمود.
3) HTTP posted file base model binder که توضیحات آن به قسمت بعدی موکول می‌شود.

یک نکته:
اولین متد LoginResult کنترلر را به نحو زیر نیز می‌توان بازنویسی کرد:
[HttpPost]
[ActionName("LoginResultWithFormCollection")]
public ActionResult LoginResult(FormCollection collection)
{
string name = collection["name"];
string password = collection["password"];

if (name == "Vahid" && password == "123")
ViewBag.Message = "Succeeded";
else
ViewBag.Message = "Failed";

return View("Result");
}

در اینجا FormCollection به صورت خودکار بر اساس مقادیر ارسالی به سرور توسط فریم ورک تشکیل می‌شود (FormCollection هم یک نوع model binder ساده است) و اساسا یک NameValueCollection می‌باشد.
بدیهی است در این حالت باید نگاشت مقادیر دریافتی، به متغیرهای متناظر با آن‌ها، دستی انجام شود (مانند مثال فوق) یا اینکه می‌توان از متد UpdateModel کلاس Controller هم استفاده کرد:

[HttpPost]
public ActionResult LoginResultUpdateFormCollection(FormCollection collection)
{
var account = new Account();
this.UpdateModel(account, collection.ToValueProvider());

if (account.Name == "Vahid" && account.Password == "123")
ViewBag.Message = "Succeeded";
else
ViewBag.Message = "Failed";

return View("Result");
}

متد توکار UpdateModel، به صورت خودکار اطلاعات FormCollection دریافتی را به شیء مورد نظر، نگاشت می‌کند.
همچنین باید عنوان کرد که متد UpdateModel، در پشت صحنه از اطلاعات Model binder پیش فرض و هر نوع Model binder سفارشی که ایجاد کنیم استفاده می‌کند. به این ترتیب زمانیکه از این متد استفاده می‌کنیم، اصلا نیازی به استفاده از FormCollection نیست و متد بدون آرگومان زیر هم به خوبی کار خواهد کرد:

[HttpPost]
public ActionResult LoginResultUpdateModel()
{
var account = new Account();
this.UpdateModel(account);

if (account.Name == "Vahid" && account.Password == "123")
ViewBag.Message = "Succeeded";
else
ViewBag.Message = "Failed";

return View("Result");
}

استفاده از model binderها همینجا به پایان نمی‌رسد. نکات تکمیلی آن‌ها در قسمت بعدی بررسی خواهند شد.

مطالب
ساخت یک برنامه ساده‌ی جاوااسکریپتی با استفاده از الگوی MVC

در این مقاله قصد داریم با استفاده از جاوااسکریپت خالص، یک برنامه‌ی ساده را با الگوی MVC انجام دهیم. این برنامه، عملیات CRUD را پیاده سازی میکند و تنها به سه فایل index.html , script.js , style.css  نیاز دارد و از هیچ کتابخانه یا فریم ورک دیگری در آن استفاده نمیکنیم.

در الگوی MVC
  • M مخفف  Model می‌باشد و کار مدیریت داده‌ها را بر عهده دارد.
  • V مخفف View  می‌باشد و وظیفه‌ی نمایش داده‌ها به کاربر را بر عهده دارد.
  • C  مخفف  Controller می‌باشد و پل ارتباطی بین Model و  View می‌باشد و مدیریت درخواست‌ها را بر عهده دارد.
در برنامه‌ی جاری همه چیز با جاوا اسکریپت هندل میشود و فایل  index.html  فقط دارای یک المنت با آیدی مشخصی است. کد زیر ساختار فایل  index.html می‌باشد:
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>الگوی MVC در جاوااسکریپت</title>
    <link rel="stylesheet" href="style.css">
</head>

<body>

    <div id="root"></div>

    <script src="script.js"></script>
</body>

</html>
فایل  style.css  آن نیز دارای دستورات ساده‌ای است و یا میتوان از Normalize.css به همراه استایل دلخواه استفاده کرد و یا از فریم ورکهای مطرح دیگر استفاده نمود. کدهای فایل sytle.css آن نیز به شکل زیر خواهد بود:
*,
*::before,
*::after {
  box-sizing: border-box
}

html {
  color: #444;
}

#root {
  max-width: 450px;
  margin: 2rem auto;
  padding: 0 1rem;
}

form {
  display: flex;
  margin-bottom: 2rem;
}

[type="text"],
button {
  display: inline-block;
  -webkit-appearance: none;
  padding: .5rem 1rem;
  border: 2px solid #ccc;
  border-radius: 4px;
}

button {
  cursor: pointer;
  background: #007bff;
  color: white;
  border: 2px solid #007bff;
  margin: 0 .5rem;
}

[type="text"] {
  width: 100%;
}

[type="text"]:active,
[type="text"]:focus {
  outline: 0;
  border: 2px solid #007bff;
}

[type="checkbox"] {
  margin-right: 1rem;
}

h1 {
  color: #222;
}

ul {
  padding: 0;
}

li {
  display: flex;
  align-items: center;
  padding: 1rem;
  margin-bottom: 1rem;
  background: #f4f4f4;
  border-radius: 4px;
}

li span {
  display: inline-block;
  padding: .5rem;
  width: 250px;
  border-radius: 4px;
  border: 2px solid transparent;
}

li span:hover {
  background: rgba(179, 215, 255, 0.52);
}

li span:focus {
  outline: 0;
  border: 2px solid #007bff;
  background: rgba(179, 207, 255, 0.52)
}


فایلهای HTML و CSS را برای شروع کار آماده نمودیم و از این پس با فایل  script.js، ادامه کار را پیش می‌بریم. برای جداسازی هر قسمت از اجزای MVC، کلاسی خاص را تدارک می‌بینیم. پس سه کلاس خواهیم داشت به‌نام‌های  Model , View , Controller و در سازنده کلاس کنترلر، دو شی از View و  Model را بعنوان ورودی دریافت میکنیم. همانطور که پیش‌تر توضیح داده شد، قسمت Controller، پل ارتباطی بین View و Model می‌باشد. کد فایل  script.js را به شکل زیر تغییر میدهیم:
class Model {
    constructor() {}
}

class View {
    constructor() {}
}

class Controller {
    constructor(model, view) {
        this.model = model
        this.view = view
    }
}


const app = new Controller(new Model(), new View())

در ادامه کار در کلاس Model شروع به کدنویسی میکنیم و متدهای مد نظر را برای عملیات CRUD، در آن اضافه میکنیم. چهار تابع را به کلاس Model به‌نامهای addTodo  ، editTodo  ، deleteTodo ، toggleTodo اضافه میکنیم. در کد زیر، در بالای هر تابع، توضیحی در مورد عملکرد تابع ذکر شده است:
class Model {
    constructor() {
        // یک آرایه از اطلاعات پیش فرض 
        this.todos = [{
                id: 1,
                text: 'Run a marathon',
                complete: false
            },
            {
                id: 2,
                text: 'Plant a garden',
                complete: false
            },
        ]
    }

    // متدی برای افزودن آیتم جدید به آرایه
    addTodo(todoText) {
        const todo = {
            id: this.todos.length > 0 ? this.todos[this.todos.length - 1].id + 1 : 1,
            text: todoText,
            complete: false,
        }

        this.todos.push(todo)
    }

    // متدی برای بروزسانی آیتم مورد نظر
    editTodo(id, updatedText) {
        this.todos = this.todos.map(todo =>
            todo.id === id ? {
                id: todo.id,
                text: updatedText,
                complete: todo.complete
            } : todo
        )
    }

    // انجام میدهد filter با استفاده از متد id  تابعی که عملیات حذف را بوسیله فیلد   
    deleteTodo(id) {
        this.todos = this.todos.filter(todo => todo.id !== id)
    }

    //  متدی که در آن مشخص میکنیم کار مد نظرانجام شده یا خیر 
    toggleTodo(id) {
        this.todos = this.todos.map(todo =>
            todo.id === id ? {
                id: todo.id,
                text: todo.text, 
                complete: !todo.complete
            } : todo
        )
    }
}

میتوانیم برای تست و نحوه عملکرد آن با استفاده از شیء app،  با دستور زیر، آیتمی را به آرایه اضافه کنیم و در کنسول آن را نمایش دهیم:
app.model.addTodo('Take a nap')
console.log(app.model.todos)

در حال حاضر با هر بار reload  شدن صفحه، فقط اطلاعات پیش فرض، درون آرایه todos قرار میگیرد؛ ولی در ادامه آن را در local storage ذخیره میکنیم.

برای ساختن قسمت View، از جاوااسکریپت استفاده میکنیم و DOM را تغییر میدهیم. البته اینکار را بدون استفاده از JSX و یا یک templating language انجام خواهیم داد. قسمت‌های دیگر برنامه مانند Controller و Model نباید درگیر تغییرات DOM یا CSS یا عناصر HTML باشند و تمام این موارد توسط View هندل میشود. کد View به نحو زیر خواهد بود:

class View {
    constructor() {}

    // ایجاد یک المنت با کلاسهای استایل دلخواه
    createElement(tag, className) {
        const element = document.createElement(tag)
        if (className) element.classList.add(className)

        return element
    }

    // DOM انتخاب و گرفتن آیتمی خاص از 
    getElement(selector) {
        const element = document.querySelector(selector)

        return element
    }
}

سپس قسمت سازنده کلاس View را تغییر میدهیم و تمام المنت‌های مورد نیاز را در آن ایجاد میکنیم:

  • ارجاعی به المنتی با آی‌دی root
  • تگ h1 برای عنوان
  • یک form، input  و دکمه‌ای برای افزودن آیتمی جدید به آرایه‌ی todos
  • یک المنت ul برای نمایش آیتم‌های  todos
سپس کلاس  View به شکل زیر خواهد بود:
    constructor() {
        // root ارجاعی به المنتی با آیدی
        this.app = this.getElement('#root')

        // عنوان برنامه
        this.title = this.createElement('h1')
        this.title.textContent = 'Todos'

        // فرم ، اینپوت ورودی و دکمه
        this.form = this.createElement('form')

        this.input = this.createElement('input')
        this.input.type = 'text'
        this.input.placeholder = 'Add todo'
        this.input.name = 'todo'

        this.submitButton = this.createElement('button')
        this.submitButton.textContent = 'Submit'

        // برای نمایش عناط آرایه یا همان لیست کارها
        this.todoList = this.createElement('ul', 'todo-list')

        // افزودن اینپوت ورودی و دکمه به فرم
        this.form.append(this.input, this.submitButton)

        // ایجاد شده است app که اینجا ارجاعی به آن بنام  root اضافه کردن تمام آیتمهای بالا در المنتی با آیدی 
        this.app.append(this.title, this.form, this.todoList)
    }

در قسمت View، دو تابع هم برای getter و setter داریم که از underscore در اول نام آنها استفاده شده که نشان دهنده این است، توابع از خارج از کلاس در دسترس نیستند (شبیه private  در سی شارپ؛ البته این یک قرارداد هست یا convention)
get _todoText() {
  return this.input.value
}

_resetInput() {
  this.input.value = ''
}

در ادامه این کلاس، یک تابع دیگر هم برای نمایش آرایه داریم که هر زمان عناصر آن تغییر کردند، بتواند نمایش به‌روز اطلاعات را نشان دهد:
displayTodos(todos){
 //...
}

متد displayTodos یک المنت ul و li‌هایی را به تعداد عناصر todos ایجاد میکند و آنها را نمایش میدهد. هر زمانکه تغییراتی مانند اضافه شدن، حذف و ویرایش در todos صورت گیرد، این متد دوباره فراخوانی میشود و لیست جدید را نمایش میدهد. محتوای متد dispayTodos به شکل زیر خواهد بود:

  displayTodos(todos) {
    // حذف تمام نودها
    while (this.todoList.firstChild) {
      this.todoList.removeChild(this.todoList.firstChild)
    }

    // اگر هیچ آیتمی در آرایه نبود این پاراگراف با متن پیش فرض نمایش داده میشود
    if (todos.length === 0) {
      const p = this.createElement('p')
      p.textContent = 'Nothing to do! Add a task?'
      this.todoList.append(p)
    } else {
      // وعناصرمربوطه را ایجاد میکند liاگه درون آرایه آیتمی قرار دارد پس به ازای آن یک عنصر 
      todos.forEach(todo => {
        const li = this.createElement('li')
        li.id = todo.id

        const checkbox = this.createElement('input')
        checkbox.type = 'checkbox'
        checkbox.checked = todo.complete

        const span = this.createElement('span')
        span.contentEditable = true
        span.classList.add('editable')

        if (todo.complete) {
          const strike = this.createElement('s')
          strike.textContent = todo.text
          span.append(strike)
        } else {
          span.textContent = todo.text
        }

        const deleteButton = this.createElement('button', 'delete')
        deleteButton.textContent = 'Delete'
        li.append(checkbox, span, deleteButton)

        // نود ایجاد شده به لیست اضافه میکند
        this.todoList.append(li)
      })
    }

    // برای خطایابی و نمایش در کنسول
    console.log(todos)
  }


در نهایت قسمت Controller را که پل ارتباطی بین View و Model می‌باشد، کامل میکنیم. اولین تغییراتی که در کلاس Controller ایجاد میکنیم، استفاده از متد displayTodos در سازنده‌ی این کلاس می‌باشد و با هر بار تغییر این متد، دوباره فراخوانی میشود:

class Controller {
  constructor(model, view) {
    this.model = model
    this.view = view

    // نمایش اطلاعات پیش فرض
    this.onTodoListChanged(this.model.todos)
  }

  onTodoListChanged = todos => {
    this.view.displayTodos(todos)
  }
}


چهار تابعی را که در قسمت Model ایجاد نمودیم و کار ویرایش، حذف، افزودن و اتمام کار را انجام میدادند، در کلاس کنترلر آنها را هندل میکنیم و زمانیکه کاربر دکمه‌ای را برای افزودن یا تیک حذف آیتمی، زد، تابع مربوطه توسط کنترلر در Model فراخوانی شود:

handleAddTodo = todoText => {
  this.model.addTodo(todoText)
}

handleEditTodo = (id, todoText) => {
  this.model.editTodo(id, todoText)
}

handleDeleteTodo = id => {
  this.model.deleteTodo(id)
}

handleToggleTodo = id => {
  this.model.toggleTodo(id)
}

چون کنترلر نمیتواند بصورت مستقیم فراخوانی شود و این توابع باید درون DOM تنظیم شوند تا به ازای رخدادهایی همچون click و change، فراخوانی شوند. پس از این توابع در قسمت View استفاده میکنیم و به کلاس View، موارد زیر را اضافه میکنیم:

bindAddTodo(handler) {
  this.form.addEventListener('submit', event => {
    event.preventDefault()

    if (this._todoText) {
      handler(this._todoText)
      this._resetInput()
    }
  })
}

bindDeleteTodo(handler) {
  this.todoList.addEventListener('click', event => {
    if (event.target.className === 'delete') {
      const id = parseInt(event.target.parentElement.id)

      handler(id)
    }
  })
}

bindToggleTodo(handler) {
  this.todoList.addEventListener('change', event => {
    if (event.target.type === 'checkbox') {
      const id = parseInt(event.target.parentElement.id)

      handler(id)
    }
  })
}


برای bind کردن این متدها در کلاس Controller، کدهای زیر را اضافه میکنیم:

this.view.bindAddTodo(this.handleAddTodo)
this.view.bindDeleteTodo(this.handleDeleteTodo)
this.view.bindToggleTodo(this.handleToggleTodo)


برای ذخیره اطلاعات در local storage، در سازنده کلاس Model، کد زیر را اضافه میکنیم:

 this.todos = JSON.parse(localStorage.getItem('todos')) || []

متد دیگری هم در کلاس Model برای به‌روز رسانی مقادیر local storage قرار میدهیم:

_commit(todos) {
  this.onTodoListChanged(todos)
  localStorage.setItem('todos', JSON.stringify(todos))
}

متدی هم برای تغییراتی که هر زمان بر روی todos  اتفاق می‌افتد، فراخوانی شود:

deleteTodo(id) {
  this.todos = this.todos.filter(todo => todo.id !== id)

  this._commit(this.todos)
}
در پایان میتوانید سورس کد مقاله جاری را از اینجا دانلود نمایید.
این مقاله صرفا جهت آشنایی و نمونه کدی از پیاده سازی الگوی  MVC  در جاوااسکریپت می‌باشد.
مطالب
EF Code First #4

آشنایی با Code first migrations

ویژگی Code first migrations برای اولین بار در EF 4.3 ارائه شد و هدف آن سهولت هماهنگ سازی کلاس‌های مدل برنامه با بانک اطلاعاتی است؛ به صورت خودکار یا با تنظیمات دقیق دستی.

همانطور که در قسمت‌های قبل نیز به آن اشاره شد، تا پیش از EF 4.3، پنج روال جهت آغاز به کار با بانک اطلاعاتی در EF code first وجود داشت و دارد:
1) در اولین بار اجرای برنامه، در صورتیکه بانک اطلاعاتی اشاره شده در رشته اتصالی وجود خارجی نداشته باشد، نسبت به ایجاد خودکار آن اقدام می‌گردد. اینکار پس از وهله سازی اولین DbContext و همچنین صدور یک کوئری به بانک اطلاعاتی انجام خواهد شد.
2) DropCreateDatabaseAlways : همواره پس از شروع برنامه، ابتدا بانک اطلاعاتی را drop کرده و سپس نمونه جدیدی را ایجاد می‌کند.
3) DropCreateDatabaseIfModelChanges : اگر EF Code first تشخیص دهد که تعاریف مدل‌های شما با بانک اطلاعاتی مشخص شده توسط رشته اتصالی، هماهنگ نیست، آن‌را drop کرده و نمونه جدیدی را تولید می‌کند.
4) با مقدار دهی پارامتر متد System.Data.Entity.Database.SetInitializer به نال، می‌توان فرآیند آغاز خودکار بانک اطلاعاتی را غیرفعال کرد. در این حالت شخص می‌تواند تغییرات انجام شده در کلاس‌های مدل برنامه را به صورت دستی به بانک اطلاعاتی اعمال کند.
5) می‌توان با پیاده سازی اینترفیس IDatabaseInitializer، یک آغاز کننده بانک اطلاعاتی سفارشی را نیز تولید کرد.

اکثر این روش‌ها در حین توسعه یک برنامه یا خصوصا جهت سهولت انجام آزمون‌های خودکار بسیار مناسب هستند، اما به درد محیط کاری نمی‌خورند؛ زیرا drop یک بانک اطلاعاتی به معنای از دست دادن تمام اطلاعات ثبت شده در آن است. برای رفع این مشکل مهم، مفهومی به نام «Migrations» در EF 4.3 ارائه شده است تا بتوان بانک اطلاعاتی را بدون تخریب آن، بر اساس اطلاعات تغییر کرده‌ی کلاس‌های مدل برنامه، تغییر داد. البته بدیهی است زمانیکه توسط NuGet نسبت به دریافت و نصب EF اقدام می‌شود، همواره آخرین نگارش پایدار که حاوی اطلاعات و فایل‌های مورد نیاز جهت کار با «Migrations» است را نیز دریافت خواهیم کرد.


تنظیمات ابتدایی Code first migrations

در اینجا قصد داریم همان مثال قسمت قبل را ادامه دهیم. در آن مثال از یک نمونه سفارشی سازی شده DropCreateDatabaseAlways استفاده شد.
نیاز است از منوی Tools در ویژوال استودیو، گزینه‌ Library package manager آن، گزینه package manager console را انتخاب کرد تا کنسول پاورشل NuGet ظاهر شود.
اطلاعات مرتبط با پاورشل EF، به صورت خودکار توسط NuGet نصب می‌شود. برای مثال جهت مشاهده آن‌ها به مسیر packages\EntityFramework.4.3.1\tools در کنار پوشه پروژه خود مراجعه نمائید.
در ادامه در پایین صفحه، زمانیکه کنسول پاورشل NuGet ظاهر می‌شود، ابتدا باید دقت داشت که قرار است فرامین را بر روی چه پروژه‌ای اجرا کنیم. برای مثال اگر تعاریف DbContext را به یک اسمبلی و پروژه class library مجزا انتقال داده‌اید، گزینه Default project را در این قسمت باید به این پروژه مجزا، تغییر دهید.
سپس در خط فرمان پاور شل، دستور enable-migrations را وارد کرده و دکمه enter را فشار دهید.
پس از اجرای این دستور، یک سری اتفاقات رخ خواهد داد:
الف) پوشه‌ای به نام Migrations به پروژه پیش فرض مشخص شده در کنسول پاورشل، اضافه می‌شود.
ب) دو کلاس جدید نیز در آن پوشه تعریف خواهند شد به نام‌های Configuration.cs و یک نام خودکار مانند number_InitialCreate.cs
ج) در کنسول پاور شل، پیغام زیر ظاهر می‌گردد:
Detected database created with a database initializer. Scaffolded migration '201205050805256_InitialCreate' 
corresponding to current database schema. To use an automatic migration instead, delete the Migrations
folder and re-run Enable-Migrations specifying the -EnableAutomaticMigrations parameter.

با توجه به اینکه در مثال قسمت سوم، از آغاز کننده سفارشی سازی شده DropCreateDatabaseAlways استفاده شده بود، اطلاعات آن در جدول سیستمی dbo.__MigrationHistory در بانک اطلاعاتی برنامه موجود است (تصویری از آن‌را در قسمت اول این سری مشاهده کردید). سپس با توجه به ساختار بانک اطلاعاتی جاری، دو کلاس خودکار زیر را ایجاد کرده است:

namespace EF_Sample02.Migrations
{
using System;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Linq;

internal sealed class Configuration : DbMigrationsConfiguration<EF_Sample02.Sample2Context>
{
public Configuration()
{
AutomaticMigrationsEnabled = false;
}

protected override void Seed(EF_Sample02.Sample2Context context)
{
// This method will be called after migrating to the latest version.

// You can use the DbSet<T>.AddOrUpdate() helper extension method
// to avoid creating duplicate seed data. E.g.
//
// context.People.AddOrUpdate(
// p => p.FullName,
// new Person { FullName = "Andrew Peters" },
// new Person { FullName = "Brice Lambson" },
// new Person { FullName = "Rowan Miller" }
// );
//
}
}
}

namespace EF_Sample02.Migrations
{
using System.Data.Entity.Migrations;

public partial class InitialCreate : DbMigration
{
public override void Up()
{
CreateTable(
"Users",
c => new
{
Id = c.Int(nullable: false, identity: true),
Name = c.String(),
LastName = c.String(),
Email = c.String(),
Description = c.String(),
Photo = c.Binary(),
RowVersion = c.Binary(nullable: false, fixedLength: true, timestamp: true, storeType: "rowversion"),
Interests_Interest1 = c.String(maxLength: 450),
Interests_Interest2 = c.String(maxLength: 450),
AddDate = c.DateTime(nullable: false),
})
.PrimaryKey(t => t.Id);

CreateTable(
"Projects",
c => new
{
Id = c.Int(nullable: false, identity: true),
Title = c.String(maxLength: 50),
Description = c.String(),
RowVesrion = c.Binary(nullable: false, fixedLength: true, timestamp: true, storeType: "rowversion"),
AddDate = c.DateTime(nullable: false),
AdminUser_Id = c.Int(),
})
.PrimaryKey(t => t.Id)
.ForeignKey("Users", t => t.AdminUser_Id)
.Index(t => t.AdminUser_Id);

}

public override void Down()
{
DropIndex("Projects", new[] { "AdminUser_Id" });
DropForeignKey("Projects", "AdminUser_Id", "Users");
DropTable("Projects");
DropTable("Users");
}
}
}


در این کلاس خودکار، نحوه ایجاد جداول بانک اطلاعاتی تعریف شده‌اند. در متد تحریف شده Up، کار ایجاد بانک اطلاعاتی و در متد تحریف شده Down، دستورات حذف جداول و قیود ذکر شده‌اند.
به علاوه اینبار متد Seed را در کلاس مشتق شده از DbMigrationsConfiguration، می‌توان تحریف و مقدار دهی کرد.
علاوه بر این‌ها جدول سیستمی dbo.__MigrationHistory نیز با اطلاعات جاری مقدار دهی می‌گردد.


فعال سازی گزینه‌های مهاجرت خودکار

برای استفاده از این کلاس‌ها، ابتدا به فایل Configuration.cs مراجعه کرده و خاصیت AutomaticMigrationsEnabled را true‌ کنید:

internal sealed class Configuration : DbMigrationsConfiguration<EF_Sample02.Sample2Context>
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
}

پس از آن EF به صورت خودکار کار استفاده و مدیریت «Migrations» را عهده‌دار خواهد شد. البته برای این منظور باید نوع آغاز کننده بانک اطلاعاتی را از DropCreateDatabaseAlways قبلی به نمونه جدید MigrateDatabaseToLatestVersion نیز تغییر دهیم:
//Database.SetInitializer(new Sample2DbInitializer());
Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample2Context, Migrations.Configuration>());

یک نکته:
کلاس Migrations.Configuration که باید در حین وهله سازی از MigrateDatabaseToLatestVersion قید شود (همانند کدهای فوق)، از نوع internal sealed معرفی شده است. بنابراین اگر این کلاس را در یک اسمبلی جداگانه قرار داده‌اید، نیاز است فایل را ویرایش کرده و internal sealed آن‌را به public تغییر دهید.

روش دیگر معرفی کلاس‌های Context و Migrations.Configuration، حذف متد Database.SetInitializer و استفاده از فایل app.config یا web.config است به نحو زیر ( در اینجا حرف ` اصطلاحا back tick نام دارد. فشردن دکمه ~ در حین تایپ انگلیسی):

<entityFramework>
<contexts>
<context type="EF_Sample02.Sample2Context, EF_Sample02">
<databaseInitializer
type="System.Data.Entity.MigrateDatabaseToLatestVersion`2[[EF_Sample02.Sample2Context, EF_Sample02],
[EF_Sample02.Migrations.Configuration, EF_Sample02]], EntityFramework"
/>
</context>
</contexts>
</entityFramework>

آزمودن ویژگی مهاجرت خودکار

اکنون برای آزمایش این موارد، یک خاصیت دلخواه را به کلاس Project به نام public string SomeProp اضافه کنید. سپس برنامه را اجرا نمائید.
در ادامه به بانک اطلاعاتی مراجعه کرده و فیلدهای جدول Projects را بررسی کنید:

CREATE TABLE [dbo].[Projects](
---...
[SomeProp] [nvarchar](max) NULL,
---...

بله. اینبار فیلد SomeProp بدون از دست رفتن اطلاعات و drop بانک اطلاعاتی، به جدول پروژه‌ها اضافه شده است.


عکس العمل ویژگی مهاجرت خودکار در مقابل از دست رفتن اطلاعات

در ادامه، خاصیت public string SomeProp را که در قسمت قبل به کلاس پروژه اضافه کردیم، حذف کنید. اکنون مجددا برنامه را اجرا نمائید. برنامه بلافاصله با استثنای زیر متوقف خواهد شد:

Automatic migration was not applied because it would result in data loss.

از آنجائیکه حذف یک خاصیت مساوی است با حذف یک ستون در جدول بانک اطلاعاتی، امکان از دست رفتن اطلاعات در این بین بسیار زیاد است. بنابراین ویژگی مهاجرت خودکار دیگر اعمال نخواهد شد و این مورد به نوعی یک محافظت خودکار است که درنظر گرفته شده است.
البته در EF Code first این مساله را نیز می‌توان کنترل نمود. به کلاس Configuration اضافه شده توسط پاورشل مراجعه کرده و خاصیت AutomaticMigrationDataLossAllowed را به true تنظیم کنید:

internal sealed class Configuration : DbMigrationsConfiguration<EF_Sample02.Sample2Context>
{
public Configuration()
{
this.AutomaticMigrationsEnabled = true;
this.AutomaticMigrationDataLossAllowed = true;
}

این تغییر به این معنا است که خودمان صریحا مجوز حذف یک ستون و اطلاعات مرتبط به آن‌را صادر کرده‌ایم.
پس از این تغییر، مجددا برنامه را اجرا کنید. ستون SomeProp به صورت خودکار حذف خواهد شد، اما اطلاعات رکوردهای موجود تغییری نخواهند کرد.


استفاده از Code first migrations بر روی یک بانک اطلاعاتی موجود

تفاوت یک دیتابیس موجود با بانک اطلاعاتی تولید شده توسط EF Code first در نبود جدول سیستمی dbo.__MigrationHistory است.
به این ترتیب زمانیکه فرمان enable-migrations را در یک پروژه EF code first متصل به بانک اطلاعاتی قدیمی موجود اجرا می‌کنیم، پوشه Migration در آن ایجاد خواهد شد اما تنها حاوی فایل Configuration.cs است و نه فایلی شبیه به number_InitialCreate.cs .
بنابراین نیاز است به صورت صریح به EF اعلام کنیم که نیاز است تا جدول سیستمی dbo.__MigrationHistory و فایل number_InitialCreate.cs را نیز تولید کند. برای این منظور کافی است دستور زیر را در خط فرمان پاورشل NuGet پس از فراخوانی enable-migrations اولیه، اجرا کنیم:
add-migration Initial -IgnoreChanges

با بکارگیری پارامتر IgnoreChanges، متد Up در فایل number_InitialCreate.cs تولید نخواهد شد. به این ترتیب نگران نخواهیم بود که در اولین بار اجرای برنامه، تعاریف دیتابیس موجود ممکن است اندکی تغییر کند.
سپس دستور زیر را جهت به روز رسانی جدول سیستمی dbo.__MigrationHistory اجرا کنید:
update-database

پس از آن جهت سوئیچ به مهاجرت خودکار، خاصیت AutomaticMigrationsEnabled = true را در فایل Configuration.cs همانند قبل مقدار دهی کنید.


مشاهده دستوارت SQL به روز رسانی بانک اطلاعاتی

اگر علاقمند هستید که دستورات T-SQL به روز رسانی بانک اطلاعاتی را نیز مشاهده کنید، دستور Update-Database را با پارامتر Verbose آغاز نمائید:
Update-Database -Verbose

و اگر تنها نیاز به مشاهده اسکریپت تولیدی بدون اجرای آن‌ها بر روی بانک اطلاعاتی مدنظر است، از پارامتر Script باید استفاده کرد:
update-database -Script



نکته‌ای در مورد جدول سیستمی dbo.__MigrationHistory

تنها دلیلی که این جدول در SQL Server البته (ونه برای مثال در SQL Server CE) به صورت سیستمی معرفی می‌شود این است که «جلوی چشم نباشد»! به این ترتیب در SQL Server management studio در بین سایر جداول معمولی بانک اطلاعاتی قرار نمی‌گیرد. اما برای EF تفاوتی نمی‌کند که این جدول سیستمی است یا خیر.
همین سیستمی بودن آن ممکن است بر اساس سطح دسترسی کاربر اتصالی به بانک اطلاعاتی مساله ساز شود. برای نمونه ممکن است schema کاربر متصل dbo نباشد. همینجا است که کار به روز رسانی این جدول متوقف خواهد شد.
بنابراین اگر قصد داشتید خواص سیستمی آن‌را لغو کنید، تنها کافی است دستورات T-SQL زیر را در SQL Server اجرا نمائید:

SELECT * INTO [TempMigrationHistory]
FROM [__MigrationHistory]
DROP TABLE [__MigrationHistory]
EXEC sp_rename [TempMigrationHistory], [__MigrationHistory]


ساده سازی پروسه مهاجرت خودکار

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

using System;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Data.Entity.Migrations.Infrastructure;
using System.IO;

namespace EF_Sample02
{
public class Configuration<T> : DbMigrationsConfiguration<T> where T : DbContext
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
AutomaticMigrationDataLossAllowed = true;
}
}

public class SimpleDbMigrations
{
public static void UpdateDatabaseSchema<T>(string SQLScriptPath = "script.sql") where T : DbContext
{
var configuration = new Configuration<T>();
var dbMigrator = new DbMigrator(configuration);
saveToFile(SQLScriptPath, dbMigrator);
dbMigrator.Update();
}

private static void saveToFile(string SQLScriptPath, DbMigrator dbMigrator)
{
if (string.IsNullOrWhiteSpace(SQLScriptPath)) return;

var scriptor = new MigratorScriptingDecorator(dbMigrator);
var script = scriptor.ScriptUpdate(sourceMigration: null, targetMigration: null);
File.WriteAllText(SQLScriptPath, script);
Console.WriteLine(script);
}
}
}

سپس برای استفاده از آن خواهیم داشت:

SimpleDbMigrations.UpdateDatabaseSchema<Sample2Context>();

در این کلاس ذخیره سازی اسکریپت تولیدی جهت به روز رسانی بانک اطلاعاتی جاری در یک فایل نیز درنظر گرفته شده است.



تا اینجا مهاجرت خودکار را بررسی کردیم. در قسمت بعدی Code-Based Migrations را ادامه خواهیم داد.
مطالب
کش کردن اطلاعات غیر پویا در ASP.Net - قسمت دوم

قسمت قبل به IIS7‌ اختصاص داشت که شاید برای خیلی‌ها کاربرد نداشته باشد خصوصا اینکه برنامه نویس‌ها ترجیح می‌دهند به روش‌هایی روی بیاورند که کمتر نیاز به دخالت مدیر سرور داشته باشد؛ یا زمانیکه سایت شما بر روی یک هاست اینترنتی قرار گرفته است عملا شاید دسترسی خاصی به تنظیمات IIS نداشته باشید (مگر اینکه یک هاست اختصاصی را تهیه کنید).
برای IIS6 و ماقبل از آن و حتی بعد از آن!، حداقل دو روش برای کش کردن اطلاعات استاتیک وجود دارد:

الف) استفاده از web resources معرفی شده در ASP.Net 2.0 به بعد
در مورد نحوه‌ی تعریف و بکارگیری web resources می‌توان به مقاله "تبدیل پلاگین‌های jQuery‌ به کنترل‌های ASP.Net" رجوع کرد.


همانطور که در شکل فوق نیز ملاحظه می‌کنید، هدر مربوط به مدت زمان منقضی شدن کش سمت کلاینت یک web resource توسط موتور ASP.Net به صورت خودکار به سال 2010 تنظیم شده است و این مقدار خالی نیست.

ب) افزودن این هدر به صورت دستی

برای این منظور باید در نحوه‌ی ارائه فایل‌های استاتیک دخالت کنیم و این‌کار را با استفاده از یک generic handler می‌توان انجام داد.


کد این generic handler می‌تواند به صورت زیر باشد:

using System;
using System.IO;
using System.Web;
using System.Web.Services;
using System.Reflection;

namespace test1
{
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class cache : IHttpHandler
{

private static void cacheIt(TimeSpan duration)
{
HttpCachePolicy cache = HttpContext.Current.Response.Cache;

FieldInfo maxAgeField = cache.GetType().GetField("_maxAge", BindingFlags.Instance | BindingFlags.NonPublic);
maxAgeField.SetValue(cache, duration);

cache.SetCacheability(HttpCacheability.Public);
cache.SetExpires(DateTime.Now.Add(duration));
cache.SetMaxAge(duration);
cache.AppendCacheExtension("must-revalidate, proxy-revalidate");
}

public void ProcessRequest(HttpContext context)
{
string file = context.Request.QueryString["file"];
if (string.IsNullOrEmpty(file))
{
return;
}

string contetType = context.Request.QueryString["contetType"];
if (string.IsNullOrEmpty(contetType))
{
return;
}

context.Response.Write(File.ReadAllText(context.Server.MapPath(file)));

//Set the content type
context.Response.ContentType = contetType;

// Cache the resource for 30 Days
cacheIt(TimeSpan.FromDays(30));
}

public bool IsReusable
{
get
{
return false;
}
}
}
}
توضیحات:
این generic handler دو کوئری استرینگ را دریافت می‌کند؛ file جهت دریافت نام فایل و contetType جهت مشخص سازی نوع محتوایی که باید سرو شود؛ مثلا جاوا اسکریپت یا استایل شیت و امثال آن. سپس زمانیکه محتوا را Response.Write می‌کند، هدر مربوط به کش شدن آن‌را نیز به 30 روز تنظیم می‌نماید.
تابع مربوط به کش کردن اطلاعات از مقاله ASP.NET Ajax Under-the-hood Secrets استخراج شد.

روش استفاده در مورد فایل‌های CSS
بجای تعریف یک فایل CSS در صفحه، به صورت استاندارد، اکنون تعریف متداول را به صورت زیر اصلاح کنید:

<link type="text/css" href="cache.ashx?v=1&file=site.css&contetType=text/css" rel="Stylesheet" />
هر زمانیکه که فایل site.css درخواست می‌شود، باید از فیلتر ما عبور کند و سپس ارائه گردد. در این حین، هدر مربوط به مدت زمان کش شدن سمت کلاینت به آن اضافه می‌شود. از کوئری استرینگ مربوط v هم جهت به روز رسانی‌های بعدی استفاده می‌شود تا اگر تغییری را اعمال کردیم، کلاینت حتما با توجه به آدرس جدید، محتویات جدید را یکبار دیگر دریافت کند. (مرورگر آدرس‌های مشابه را در صورتیکه هدر مربوط به کش شدن آن‌ها تنظیم شده باشد، از کش خواهد خواند و کاری به آخرین تغییرات شما در سرور ندارد)

روش استفاده در مورد فایل‌های JS
<script type="text/javascript" src="cache.ashx?v=1&file=js/jquery-1.3.2.min.js&contetType=application/x-javascript"></script>
اکنون اگر سایت را مجددا با افزونه YSlow بررسی کنیم، می‌توان این هدر جدید را مشاهده کرد:



اشتراک‌ها
مدیریت همه ریسورس ها در یک پنجره

با استفاده از افزونه بالا به جای مدیریت هر فایل ریسورس به صورت تکی میتوانید همه ریسورس‌ها را در یک پنجره و به صورت همزمان مدیریت کنید

مدیریت همه ریسورس ها در یک پنجره
مسیرراه‌ها
+AngularJS 2.0
شروع به کار با AngularJS 2.0 و TypeScript
فرم‌های مبتنی بر قالب‌ها در Angular
Angular Animation
آپلود فایل
Angular Material 6x
  • ابزارهای توسعه
مطالب
بررسی ابزار SQL Server Profiler

مقدمه

Profiler یک ابزار گرافیکی برای ردیابی و نظارت بر کارآئی SQL Server است. امکان ردیابی اطلاعاتی در خصوص رویدادهای مختلف و ثبت این داده‌ها در یک فایل (با پسوند trc) یا جدول برای تحلیل‌های آتی نیز وجود دارد. برای اجرای این ابزار مراحل زیر را انجام دهید:

Start > Programs> Microsoft SQL Server > Performance Tools> SQL Server Profiler
و یا در محیط  Management Studio از منوی Tools گزینه SQL Server Profiler را انتخاب نمائید.


1- اصطلاحات

1-1- رویداد (Event):

یک رویداد، کاری است که توسط موتور بانک اطلاعاتی (Database Engine) انجام می‌شود. برای مثال هر یک از موارد زیر یک رویداد هستند.
-  متصل شدن کاربران (login connections) قطع شدن ارتباط یک login
-  اجرای دستورات T-SQL، شروع و پایان اجرای یک رویه، شروع و پایان یک دستور در طول اجرای یک رویه، اجرای رویه‌های دور Remote Procedure Call
-  باز شدن یک Cursor
-  بررسی و کنترل مجوزهای امنیتی

1-2- کلاس رویداد (Event Class):

برای بکارگیری رویدادها در Profiler، از یک Event Class استفاده می‌کنیم. یک Event Class رویدادی است که قابلیت ردیابی دارد. برای مثال بررسی ورود و اتصال کاربران با استفاده از کلاس Audit Login قابل پیاده سازی است. هر یک از موارد زیر یک Event Class هستند.
-  SQL:BatchCompleted
-  Audit Login
-  Audit Logout
-  Lock: Acquired
-  Lock: Released

1-3- گروه رویداد (Event Category):

یک گروه رویداد شامل رویدادهایی است که به صورت مفهومی دسته بندی شده اند. برای مثال، کلیه رویدادهای مربوط به قفل‌ها از جمله Lock: Acquired (بدست آوردن قفل) و Lock: Released (رها کردن قفل) در گروه رویداد Locks قرار  دارند.

1-4- ستون داده ای (Data Column):

یک ستون داده ای، خصوصیت و جزئیات یک رویداد را شامل می‌شود. برای مثال در یک Trace که رویدادهای Lock: Acquired را نظارت می‌کند، ستون Binary Data شامل شناسه (ID) یک صفحه و یا یک سطر قفل شده است و یا اینکه ستون Duration مدت زمان اجرای یک رویه را نمایش می‌دهد.

1-5- الگو (Template):

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

1-6- ردیاب (Trace):

یک Trace داده‌ها را براساس رویدادهای انتخاب شده، جمع آوری می‌کند. امکان اجرای بلافاصله یک Trace برای جمع آوری اطلاعات با توجه به رویدادهای انتخاب شده و ذخیره کردن آن برای اجرای آتی وجود دارد.

1-7- فیلتر (Filter):

هنگامی که یک Trace یا الگو ایجاد می‌شود، امکان تعریف شرایطی برای فیلتر کردن داده‌های جمع آوری شده نیز وجود دارد. این کار باعث کاهش حجم داده‌های گزارش شده می‌شود. برای مثال اطلاعات مربوط به یک کاربر خاص جمع آوری می‌شود و یا اینکه رشد یک بانک اطلاعاتی مشخص بررسی می‌شود.


2- انتخاب الگو (Profiler Trace Templates)

از آنجائیکه اصولاً انتخاب Eventهای مناسب، کار سخت و تخصصی می‌باشد برای راحتی کار تعدادی Template‌های آماده وجود دارد، برای مثال TSQL_Duration تاکیدش روی مدت انجام کار است و یا SP_Counts در مواردی که بخواهیم رویه‌های ذخیره شده را بهینه کنیم استفاده می‌شود در جدول زیر به شرح هر یک پرداخته شده است:
 الگو  هدف 
 Blank   ایجاد یک Trace کلی 
 SP_Counts   ثبت اجرای هر رویه ذخیره شده برای تشخیص اینکه هر رویه چند بار اجرا شده است 
 Standard   ثبت آمارهای کارائی برای هر رویه ذخیره شده و Query‌های عادی SQL که اجرا می‌شوند و عملیات ورود و خروج هر Login (پیش فرض) 
 TSQL   ثبت یک لیست از همه رویه‌های ذخیره شده و Query‌های عادی SQL که اجرا می‌شوند ولی آمارهای کارائی را شامل نمی‌شود 
 TSQL_Duration   ثبت مدت زمان اجرای هر رویه ذخیره شده و هر Query عادی SQL 
 TSQL_Grouped   ثبت تمام  login‌ها و logout‌ها در طول اجرای رویه‌های ذخیره شده و هر Query عادی SQL، شامل اطلاعاتی برای شناسائی برنامه و کاربری که درخواست را اجرا می‌کند 
 TSQL_Locks   ثبت اطلاعات انسداد (blocking) و بن بست (deadlock) از قبیل blocked processes، deadlock chains، deadlock graphs,... . این الگو همچنین درخواست‌های تمام رویه‌های ذخیره شده و تمامی دستورات هر رویه و  هر Query عادی SQL را دریافت می‌کند 
 TSQL_Replay   ثبت اجرای رویه‌های ذخیره شده و Query‌های SQL در یک SQL Instance و  مهیا کردن امکان اجرای دوباره عملیات در سیستمی دیگر 
 TSQL_SPs   ثبت کارائی برای Query‌های SQL، رویه‌های ذخیره شده و تمامی دستورات درون یک رویه ذخیره شده و نیز عملیات ورود و خروج هر Login 
 Tuning   ثبت اطلاعات کارائی برای Query‌های عادی SQL و رویه‌های ذخیره شده و یا تمامی دستورات درون یک رویه ذخیره شده 

3- انتخاب رویداد (SQL Trace Event Groups)

رویداد‌ها در 21 گروه رویداد دسته بندی می‌شوند که در جدول زیر لیست شده اند:
 گروه رویداد  هدف 
 Broker  13 رویداد برای واسطه سرویس (Service Broker) 
 CLR   1 رویداد برای بارگذاری اسمبلی‌های CLR (Common Language Runtime) 
 Cursors   7 رویداد برای ایجاد، دستیابی و در اختیار گرفتن Cursor 
 Database   6 رویداد برای رشد/کاهش  (grow/shrink) فایل های  Data/Log همچنین تغییرات حالت انعکاس (Mirroring) 
 Deprecation   2 رویداد برای آگاه کردن وضعیت نابسامان درون یک SQL Instance 
 Errors and
Warnings 
 16 رویداد برای خطاها، هشدارها و پیغام‌های اطلاعاتی که ثبت شده است 
 Full Text   3  رویداد برای پیگیری یک شاخص متنی کامل 
 Locks   9 رویداد برای بدست آوردن، رها کردن قفل و بن بست (Deadlock) 
 OLEDB   5 رویداد برای درخواست‌های توزیع شده و RPC (اجرای رویه‌های دور) 
 Objects   3 رویداد برای وقتی که یک شی ایجاد، تغییر یا حذف می‌شود 
 Performance   14 رویداد برای ثبت نقشه درخواست‌ها (Query Plan) برای استفاده نقشه راهنما (Plan Guide) به منظور بهینه سازی کارائی درخواست ها،  همچنین این گروه رویداد در خواست‌های متنی کامل (full text) را ثبت می‌کند 
 Progress Report   10 رویداد برای ایجاد Online Index 
 Query
Notifications 
 4 رویداد برای سرویس اطلاع رسان (Notification Service) 
 Scans   2 رویداد برای وقتی که یک جدول یا شاخص، پویش می‌شود 
 Security Audit   44 رویداد برای وقتی که مجوزی استفاده شود، جابجائی هویتی رخ دهد، تنظیمات امنیتی اشیائی تغییر کند،یک  SQL Instance  شروع و متوقف شود و یک  Database جایگزین شود یا از آن پشتیبان گرفته شود 
 Server  3 رویداد برای Mount Tape، تغییر کردن حافظه سرور و بستن یک فایل Trace 
 Sessions   3 رویداد برای وقتی که Connection‌ها موجود هستند و یک Trace فعال می‌شود، همچنین یک Trigger  و یک تابع دسته بندی(classification functions) مربوط به مدیریت منابع(resource governor) رخ دهد 
 Stored Procedures   12 رویداد برای اجرای یک رویه ذخیره شده و دستورات درون آن ، کامپایل مجدد و استفاده از حافظه نهانی (Cache) 
 Transactions   13 رویداد برای شروع، ذخیره ، تائید و لغو یک تراکنش 
 TSQL   9  رویداد برای اجرای Query‌های SQL و جستجوهای XQUERY (در داده‌های XML)  
 User Configurable   10 رویداد که شما می‌توانید پیکربندی کنید 
به طور معمول بیشتر از گروه رویدادهای Locks، Performance، Security Audit، Stored Procedures و TSQL استفاده می‌شود.


4- انتخاب ستون‌های داده ای ( Data Columns)

اگرچه می‌توان همه‌ی 64 ستون داده ای ممکن را برای ردیابی انتخاب کرد ولیکن داده‌های Trace شما زمانی مفید خواهند بود که اطلاعات ضروری را ثبت کرده باشید. برای مثال شماره ترتیب تراکنش‌ها را،  برای یک رویداد RPC:Completed می‌توانید برگردانید، اما همه رویه‌های ذخیره شده مقادیر را تغییر نمی‌دهند بنابراین شماره ترتیب تراکنش‌ها فضای بیهوده ای را مصرف می‌کند. بعلاوه همه ستون‌های داده ای برای تمامی رویداد‌های Trace معتبر نیستند. برای مثال Read ، Write ،CPU و Duration برای رویداد‌های RPC:Starting و SQL:BatchStarting معتبر نیستند.
ApplicationName، NTUserName، LoginName، ClientProcessID، SPID، HostName، LoginSID، NTDomainName و SessionLoginName ، مشخص می‌کنند چه کسی و از چه منشاء دستور را اجرا کرده است.
ستون SessionLoginName معمولاً نام Login ای که از آن برای متصل شدن به SQL Instance استفاده شده است را نشان می‌دهد. در حالیکه ستون LoginName نام کاربری را که دستور را اجرا می‌کند نشان می‌دهد (EXECUTE AS). ستون ApplicationName خالی است مگر اینکه در ConnectionString برنامه کاربردیمان این خصوصیت (Property) مقداردهی شده باشد. ستون StartTime و EndTime زمان سرحدی برای هر رویداد را ثبت می‌کند این ستون‌ها بویژه در هنگامی که به عملیات Correlate  نیاز دارید مفید هستند.


5- بررسی چند سناریو نمونه

•  یافتن درخواست هائی (Queries) که بدترین کارایی را دارا هستند.

برای ردیابی درخواست‌های ناکارا، از رویداد RPC:Completed از دسته Stored Procedure و رویداد SQL:BatchCompleted از دسته TSQL استفاده می‌شود.

•  نظارت بر کارایی رویه ها

برای ردیابی کارائی رویه ها، از رویدادهای SP:Starting، SP:Completed، SP:StmtCompleted و SP:StmtStaring از کلاس Stored Procedure و رویدادهای SQL:BatchStarting ، SQL:BatchCompleted از کلاس TSQL استفاده می‌شود.

•  نظارت بر اجرای دستورات T-SQL توسط هر کاربر

برای ردیابی دستوراتی که توسط یک کاربر خاص اجرا می‌شود، نیاز به ایجاد یک Trace برای نظارت بر رویدادهای کلاس‌های Sessions، ExistingConnection و TSQL داریم همچنین لازم است نام کاربر در قسمت فیلتر  و با استفاده از DBUserName مشخص شود.

•  اجرا دوباره ردیاب (Trace Replay)

این الگو  معمولاً برای debugging استفاده می‌شود برای این منظور  از الگوی Replay استفاده می‌شود. در ضمن امکان اجرای دوباره عملیات در سیستمی دیگر با استفاده از این الگو مهیا می‌شود.

•  ابزار Tuning Advisor (راهنمای تنظیم کارائی)

این ابزاری برای تحلیل کارائی یک یا چند بانک اطلاعاتی و تاثیر عملکرد آنها بر بار کاری (Workload) سرویس دهنده است. یک بار کاری مجموعه ای از دستورات T-SQL است که روی بانک اطلاعاتی اجرا می‌شود. بعد از تحلیل تاثیر بارکاری بر بانک اطلاعاتی، Tuning Advisor توصیه هائی برای اضافه کردن، حذف و یا تغییر طراحی فیزیکی ساختار بانک اطلاعاتی ارائه می‌دهد این تغییرات ساختاری شامل پیشنهاد برای تغییر ساختاری موارد Clustered Indexes، Nonclustered Indexes، Indexed View و Partitioning است.
برای ایجاد بارکاری می‌توان از یک ردیاب تهیه شده در SQL Profiler استفاده کرد برای این منظور از الگوی Tuning استفاده می‌شود و یا رویدادهای RPC:Completed، SQL:BatchCompleted و SP:StmtCompleted را ردیابی نمائید.

•  ترکیب ابزارهای نظارتی (Correlating Performance and Monitoring Data)

یک Trace برای ثبت اطلاعاتی که در یک SQL Instance رخ می‌دهد، استفاده می‌شود. System Monitor  برای ثبت شمارنده‌های کارائی(performance counters) استفاده می‌شود و همچنین از منابع سخت افزاری و اجزای دیگر که روی سرور اجرا می‌شوند، تصاویری فراهم می‌کند. توجه شود که در مورد  Correlating یک فایل ردیاب (trace file) و یک Counter Log (ابزار Performance )، ستون داده ای StartTime و EndTime باید انتخاب شود، برای این کار از منوی File گزینه Import Performance Data انتخاب می‌شود.

•  جستجوی علت رخ دادن یک بن بست

برای ردیابی علت رخ دادن یک بن بست، از رویدادهای RPC:Starting، SQLBatchStarting از دسته Stored Procedure و رویدادهای Deadlock graph، Lock:Deadlock و Lock:Deadlock Chain از دسته Locks استفاده می‌شود. ( در صورتی که نیاز به یک ارائه گرافیکی دارید از  Deadlock graph استفاده نمائید، خروجی مطابق تصویر زیر می‌شود).


5-1- ایجاد یک Trace

1-  Profiler را اجرا کنید از منوی File گزینه New Trace را انتخاب کنید و به SQL Instance مورد نظرتان متصل شوید.
2-  مطابق تصویر زیر برای Trace یک نام و الگو و تنظیمات ذخیره سازی فایل را مشخص کنید.


3-  بر روی قسمت Events Selection کلیک نمائید.
4-  مطابق تصویر زیر رویداد‌ها و کلاس رویداد‌ها را انتخاب کنید، ستون‌های TextData، NTUserName، LoginName، CPU،Reads،Writes، Duration، SPID، StartTime، EndTime، BinaryData، DataBaseName، ServerName و ObjectName را انتخاب کنید.

5-  روی Column Filters کلیک کنید و مطابق تصویر زیر برای DatabaseName فیلتری تنظیم کنید.


6-  روی Run کلیک کنید. تعدادی Query و رویه ذخیره شده مرتبط با پایگاه داده AdventureWorks اجرا کنید .


5-2- ایجاد یک Counter Log

برای ایجاد یک Counter Log  مراحل زیر  را انجام دهید:
1-  ابزار Performance را اجرا کنید (برای این کار عبارتPerfMon را در قسمت Run بنویسید).
2-  در قسمت Counter Logs یک log ایجاد کنید.
3-  روی Add Counters کلیک کرده و مطابق تصویر موارد زیر را انتخاب کنید.
Select counters from list 
Performance Object 
 Output Queue Length  Network Interface 
 % Processor Time  Processor 
 Processor Queue Length  System 
 Buffer Manager:Page life expectancy  SQLServer 
 
4-  روی Ok کلیک کنید تا Counter Log ذخیره شود سپس روی آن راست کلیک کرده و آنرا Start کنید.


5-3- ترکیب ابزارهای نظارتی (Correlating SQL Trace and System Monitor Data)

1-  Profiler را اجرا کنید از منوی File گزینه Open و سپس Trace File را انتخاب کنید فایل trc را که در گام اول ایجاد کردید، باز نمائید.
2-  از منوی File گزینه Import Performance Data را انتخاب کنید و فایل counter log  را که در مرحله قبل ایجاد کردید  انتخاب کنید.



نکته: اطلاعات فایل trc را می‌توان درون یک جدول وارد کرد، بدین ترتیب می‌توان آنالیز بیشتری داشت به عنوان مثال دستورات زیر این عمل را انجام می‌دهند.


 SELECT * INTO dbo.BaselineTrace
FROM fn_trace_gettable(' c:\performance baseline.trc ', default);
با اجرای دستور زیر جدولی با نام  BaselineTrace ایجاد و محتویات Trace مان (performance baseline.trc) در آن درج می‌گردد.