نظرات مطالب
OpenCVSharp #6
من از کدهای زیر استفاده کردم و در نهایت این خطا را در خط Application.Run(new Form1());  گرفتم
An unhandled exception of type 'System.Reflection.TargetInvocationException' occurred in mscorlib.dll

Additional information: Exception has been thrown by the target of an invocation.
کدهایی که در برنامه نوشتم:
private void button1_Click(object sender, EventArgs e)
        {
            if (_worker != null && _worker.IsBusy)
            {
                return;
            }

            _worker = new BackgroundWorker
            {
                WorkerReportsProgress = true,
                WorkerSupportsCancellation = true
            };
            _worker.DoWork += workerDoWork;
            _worker.ProgressChanged += workerProgressChanged;
            _worker.RunWorkerCompleted += workerRunWorkerCompleted;
            _worker.RunWorkerAsync();
        }
private void workerDoWork(object sender, DoWorkEventArgs e)
        {
            //var interval = (int)(1000 / _capture.Fps);
            Image image;
            while ((image = _capture.QueryFrame().ToBitmap()) != null &&
                    _worker != null && !_worker.CancellationPending)
            {
                _worker.ReportProgress(0, image);
                //Thread.Sleep(interval);

                Thread.Sleep(10);
            }
        }
        private void workerProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            var image = e.UserState as Image;
            if (image == null) return;

            //Cv.Not(image, image);
            //_pictureBoxIpl1.RefreshIplImage(image);
            //_pictureBoxIpl1.Image=image;

            _pictureBoxIpl1.Invoke(new EventHandler(delegate
            {
                _pictureBoxIpl1.Image = image;
            }));

            
        }

        private void workerRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            _worker.Dispose();
            _worker = null;
        }
نظرات مطالب
EF Code First #12
به این ترتیب باید پیاده سازی بشه. یک حالت عمومی است و به کلاس و شیء خاصی گره نخورده:
using System;
using System.ComponentModel.DataAnnotations;
namespace Test
{
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public class CompareAttribute : ValidationAttribute
    {
        public CompareAttribute(string originalProperty, string confirmProperty)
        {
            OriginalProperty = originalProperty;
            ConfirmProperty = confirmProperty;
        }
        public string ConfirmProperty { get; private set; }
        public string OriginalProperty { get; private set; }
        protected override ValidationResult IsValid(object value, ValidationContext ctx)
        {
            if (value == null)
                return new ValidationResult("لطفا فیلدها را تکمیل نمائید");
            var confirmProperty = ctx.ObjectType.GetProperty(ConfirmProperty);
            if (confirmProperty == null)
                throw new InvalidOperationException(string.Format("لطفا فیلد {0} را تعریف نمائید", ConfirmProperty));
            var confirmValue = confirmProperty.GetValue(ctx.ObjectInstance, null) as string;
            if (string.IsNullOrWhiteSpace(confirmValue))
                return new ValidationResult(string.Format("لطفا فیلد {0} را تکمیل نمائید", ConfirmProperty));
            var originalProperty = ctx.ObjectType.GetProperty(OriginalProperty);
            if (originalProperty == null)
                throw new InvalidOperationException(string.Format("لطفا فیلد {0} را تعریف نمائید", OriginalProperty));
            var originalValue = originalProperty.GetValue(ctx.ObjectInstance, null) as string;
            if (string.IsNullOrWhiteSpace(originalValue))
                return new ValidationResult(string.Format("لطفا فیلد {0} را تکمیل نمائید", OriginalProperty));
            return originalValue == confirmValue ? ValidationResult.Success : new ValidationResult("مقادیر وارد شده یکسان نیستند");
        }
    }
}

مطالب
کاهش پیچیدگی؛ قسمت اول: الگوی مورد خاص (Special Case Pattern)
مهمترین دستاورد  الگوی شیء نال ( Null Object Pattern ) این است که جریان کنترل (branch ) برای شاخه مثبت و منفی یکسان است و هیچگونه انشعاب شرطی بر اساس آزمون‌های null وجود ندارد. شی‌ءهای حقیقی دارای یک سری از رفتار‌ها هستند؛ ولی Null Object معمولا هیچ کاری را انجام نمی‌دهد. 

Null Object دارای هیچگونه اطلاعاتی نیست. اگر ما یک برنامه تجارت داشتیم که در آن درخواست خرید، Null Object را برگرداند، در واقع تمام سر نخ‌هایی را که چرا عملیات با شکست مواجه شد‌ه‌است، کنار می‌گذاریم. آیا این مبلغ در حساب کاربری کاربر کافی نیست و یا آیا آیتمی موجود نیست؟ 

در این مقاله حالت‌هایی را که الگوی طراحی Null Object، قادر به تشخیص آنها نیست را به وسیله الگوی طراحی Special case رفع می‌کنیم.


 الگوی طراحی Special Case 

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

بجای برگشت دادن شیء Null در تمام این موارد، ما می‌توانیم نتیجه را اصلاح کنیم و اساسا هر بار یک شیء مختلف را بازگردانیم. اینها هنوز هم نوعی از اشیاء Null هستند؛ ولی اینبار دارای معانی هستند. یکی از انها برای «حساب کاربری ناکافی» است، یکی دیگر برای «سایت در دست تعمیر و نگهداری» است و یا یکی دیگر از آنها «موجود نبودن در انبار» خواهد بود. 

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


نمونه‌ای از پیاده سازی موارد خاص

این بخشی از رابط کاربری است که توسط سرویس‌های برنامه کاربردی اجرا می‌شود:

public interface IApplicationServices
{
    ...
    IReceiptViewModel LoggedInUserPurchase(string itemName);
}

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

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

       1)  سایت در دست تعمیر و نگهداری باشد.

       2)  کاربر ثبت نشده و یا فعال نیست.

       3) آیتمی موجود نیست و یا وجود ندارد.

       4)  موجودی کاربر کم باشد.

 برای هر یک از این موارد یک کلاس خاص را ایجاد می‌کنیم که رابط کاربری IReceiptViewModel را پیاده سازی خواهد کرد. 
public class DownForMaintenance: IReceiptViewModel
{
}
این ویو مدل نشان دهنده این می‌باشد که سایت در دست تعمیر و نگهداری است. در حال حاضر هیچ اطلاعات اضافی همراه آن نیست؛ اما بعدها می‌توانیم بعضی از ویژگی‌های آن را اضافه کنیم. برای مثال زمان برآورد که سایت چه زمانی دوباره باز می‌شود. 
public class InvalidUser: IReceiptViewModel
{
    public string UserName { get; private set; }

    public InvalidUser(string userName)
    {
        this.UserName = userName;
    }
}
این کلاس برای مواقعی که کاربر وجود نداشته باشد و یا غیر فعال باشد، مورد استفاده قرار می‌گیرد. اینبار شیء مورد خاص (Special case) دارای اطلاعات اضافی مانند نام کاربری که خرید آن شکست خورده است، می‌باشد. 
توجه داشته باشید موارد خاص InvalidUser زمانی تولید می‌شوند که حالت DownForMaintenance را با موفقیت گذرانده باشیم. این دقیقا همان لحظه‌ای است که برنامه ما می‌داند کاربر وارد سیستم شده‌است. 
این قاعده کلی در الگوی طراحی مورد خاص ( Special case ) است. همانطور که از طریق منطق دامنه ( Domain Logic ) پیشرفت می‌کنید، اطلاعات بیشتری جمع آوری می‌شود که هر کدام از آنها برای تولید یک مورد مورد خاص با ارزش‌تر، مورد استفاده قرار می‌گیرند.
public class OutOfStock: IReceiptViewModel
{
    public string UserName { get; private set; }
    public string ItemName { get; private set; }

    public OutOfStock(string userName, string itemName)
    {
        this.UserName = userName;
        this.ItemName = itemName;
    }
}
این ویو مدل نشان دهنده این است که آیتمی موجود نیست. اینبار ما نام کاربری و نام آیتم (معتبر) را می‌دانیم. این شیء شامل اطلاعاتی است که آیتم مشخص شده توسط کاربر ثبت شده موجود در انبار موجود نیست. 
public class InsufficientFunds: IReceiptViewModel
{
    public string UserName { get; private set; }
    public decimal Amount { get; private set; }
    public string ItemName { get; private set; }

    public InsufficientFunds(string userName, decimal amount, string itemName)
    {
        this.UserName = userName;
        this.Amount = amount;
        this.ItemName = itemName;
    }
}
در نهایت، این مدل اطلاعاتی را ارائه می‌دهد که کاربری خاص، موجودی کافی برای پرداخت اقلام درخواستی را ندارد. بازگشت یک مورد خاص ( Special case) بجای Null Object ساده که حاوی اطلاعات اضافی نیست، به ما اجازه می‌دهد که اطلاعات قابل تامل‌تری را به کاربر بازگشت دهیم.

مثال استفاده از موارد خاص ( Special case)
با این حال ما تعدادی از کلاس‌های مورد خاص را پیاده سازی کرده‌ایم که هر کدام برای سناریوهای منفی در برنامه است. در هر موردی که ما شیء null یا Null را استفاده می‌کردیم وآن را بازگشت می‌دادیم، اکنون دیگر فقط مورد خاص را ایجاد و باز می‌گردانیم.
public class ApplicationServices: IApplicationServices
{
    ...
    public IReceiptViewModel LoggedInUserPurchase(string itemName)
    {
        if (IsDownForMaintenance())
            return new DownForMaintenance();
        return this.domain.Purchase(Session.LoggedInUserName, itemName);
    }

    private bool IsDownForMaintenance()
    {
        return File.Exists("maintenance.lock");
    }
}
این پیاده سازی سرویس نرم افزاری است که می‌تواند برای موارد تعمیر و نگهداری در مواردی که برنامه در دست نیست، بازگرداننده شود. در غیر این صورت سرویس برنامه، سرویس  دامین را فراخوانی کرده و شیءای را که آنجا تولید می‌شود، بازگشت می‌دهد. 
در داخل سرویس دامنه، مسائل ممکن است بسیار پیچیده‌تر باشند که در اینجا پیاده سازی شده است:
public class DomainServices: IDomainServices
{
    public IReceiptViewModel Purchase(string userName, string itemName)
    {
        User user = this.userRepository.Find(userName);
        if (user == null)
            return new InvalidUser(userName);

        Account account = this.accountRepository.FindByUser(user);
        return this.Purchase(user, account, itemName);
    }

    private IReceiptViewModel Purchase(User user, Account account, string itemName)
    {
        Product item = this.productRepository.Find(itemName);
        if (item == null)
            return new OutOfStock(user.UserName, itemName);

        ReceiptDto receipt = user.Purchase(item);
        MoneyTransaction transaction = account.Withdraw(receipt.Price);
        if (transaction == null)
            return new InsufficientFunds(user.UserName, receipt.Price, itemName);

        return receipt;
    }
}
در این مورد، اگر در سرویس‌های دامنه، چیزی اشتباه برآورده شود، موارد خاصی را باز می‌گردانند. فقط در پایان اجرا، اگر همه چیز خوب پیش رود، سرویس دامنه یک شیء واقعی را بازگشت خواهد داد.
به این ترتیب زمانیکه کاربر درخواست خریدی را می‌کند، خدمات برنامه و دامنه، اطلاعات و اتفاقات لازم را به لایه نمایش ارسال می‌کنند. اگر خرید با خطا رو به رو شد، لایه نمایش یک شیء را که حاوی تمام داده‌های موجود در مورد دلایل شکست است، دریافت می‌کند. اگر خرید با موفقیت باشد، لایه نمایش یک شیء را دریافت خواهد کرد که قادر به نمایش آن به کاربر خواهد بود.

نتیجه گیری
الگوی طراحی مورد خاص، مکمل ایده الگوی Null Object است. در بسیاری از موارد، Null Object واقعا قابل اجرا نیست؛ زیرا اطلاعاتی را درباره آنکه چرا هیچ خاصیت خاصی تولید نمی‌شود، ارائه نمی‌دهد. special case شیء خاصی است که اطلاعات بیشتری را ارائه می‌دهد.
نتیجه این پیچیدگی در کد این است که تماس گیرنده مجبور به انجام if-then-else بر اساس اینکه آیا شیء null یا غیر null است، دیگر نیست. رفرنس‌ها همیشه غیر Null خواهند بود. تنها تفاوت قابل ملاحظه‌ای که بین الگوهای Null Object و Special case وجود دارد این است که الگوی خاص ( Special Case) رفتارهای پیچیده‌تری را از خود نشان می‌دهد. از این رو، الگوی مورد خاص را می‌توان به سناریوهای پیچیده‌تری اعمال کرد که تماس گیرنده را از منطق if-then-else محافظت می‌کند. 
بازخوردهای پروژه‌ها
ارسال پارامترهای اختیاری به دستور Sql و مشکل با دستور is null
با سلام
برای تهیه گزارشی به صورت پویا و با چندین پارامتر اختیاری (ارسال پارامتر با مقدار null) همچنین query تولید شده است.
Select [VoucherRows].[Description] AS [Description],[VoucherRows].[Creditor] AS [Creditor],[VoucherRows].[Debtor] AS [Debtor],[Vouchers].[Number] AS [Number],[Vouchers].[SubmitDate] AS [SubmitDate] 
From [VoucherRows] AS [VoucherRows],[Vouchers] AS [Vouchers] 
 Where [VoucherRows].[VoucherId] IS Null OR  [Vouchers].[Id]  =  [VoucherRows].[VoucherId]  
And @p1 IS Null OR  [Vouchers].[Number]  >=  @p1  
And @p2 IS Null OR  [Vouchers].[Number]  <=  @p2  And @p3 IS Null OR  [Vouchers].[Description]  Like  @p3   
 Order By [Vouchers].[SubmitDate] DESC,[Vouchers].[Number] ASC
و هنگام اجرای خطای 
//The specified argument value for the function is not valid. [ Argument # = 1,Name of function(if known) = isnull ]
را دارم. پس از جستجو این فروم پیشنهاد تغییر DbType  به  SqlDbType را داده است،
 آیا این راه حل با توجه به استفاده از   GenericDataReader و مدنظر بودن عمومیت آن در تضاد نیست؟
اگه این راه حل درسته باید کد داخل کتابخانه pdfreport تغییر کند؟
مطالب
دستکاری کردن عملیات Sort در SQL Server
گاهی اوقات لازم می‌باشد، در زمان Sort نمودن یک ستون، تمایل داشته باشیم Range خاصی از مقادیر آن ستون در ابتدا قرار گیرد، و عملیات Sort پس از آن Range، اعمال گردد. برای انجام چنین کاری می‌توانیداز روش زیر استفاده نمایید:
برای درک مطلب مثالی می‌زنیم:
در ابتدا Script زیر را اجرا نمایید، که شامل یک جدول و درج چند رکورد در آن می‌باشد:
Create Table TestSort
(ID  int identity(1,1),
Name nvarchar(30),
Color nvarchar(15)
)
درج رکورد:
Insert into TestSort (Name,color)
Values  ('Adjustable Race',null)
       ,('Bearing Ball',null)
       ,('Headset Ball Bearings',null)
       ,('LL Crankarm','Black')
       ,('ML Crankarm','Black')
       ,('Chainring','Black')
       ,('Front Derailleur Cage','Silver')
       ,('Front Derailleur Linkage','Silver')
       ,('Lock Ring','Silver')
       ,('HL Road Frame - Red, 62','Red')
       ,('HL Road Frame - Red, 48','Red')
       ,('LL Road Frame - Red, 44','Red')
       ,('Full-Finger Gloves, M','RED')
       ,('Road-550-W Yellow, 38','Yellow')
       ,('Road-550-W Yellow, 40','Yellow')
       ,('Road-550-W Yellow, 42','Yellow')
       ,('Classic Vest, S','Blue')
       ,('Classic Vest, M','Blue')
       ,('Classic Vest, L','Blue')
در جدول TestSort ستونی به نام Color داریم، که نام رنگها در آن درج شده است، رنگهایی همچون  Black ، Silver،Blue،Yellow و Red
درابتدا روی ستون Color بصورت نرمال Sort صعودی انجام می‌دهیم:
Select t.ID,t.Name,t.Color from TestSort as t order by t.Color
خروجی:

مطابق شکل،زمانی که Sort بصورت صعودی است، رکوردهایی را که ستون Color آنها دارای مقدار Null می‌باشند، در ابتدای جدول قرار گرفته اند.
در ادامه می‌خواهیم، عملیات Sort ی را روی ستون Color انجام دهیم، بطوریکه تمامی رکوردهایی که مقدار ستون Color شان Red است، در ابتدای جدول قرار گیرد، و پس از آن عملیات سورت روی رنگهای دیگر اعمال شود.
برای انجام چنین کاری کافیست Script زیر را اجرا نمایید:
Select t.ID,t.Name,t.Color from TestSort as t 
Where t.color is not null
order by  Case t.Color
When 'Red' Then Null
Else t.color 
End;
خروجی:

چطور اینکار انجام شد:
اگر بهScript ذکر شده دقت نمایید، در قسمت Order by اشاره کردیم، تمام مقادیر Red در ستون Color به Null تغییر کنند، بنابراین SQL Server، در ابتدا مقادیر Red را یافته آنها را به Null تغییر و سپس عملیات سورت را انجام می‌دهد،
برای درک بیشتر عملیاتی را که SQL Server پشت صحنه انجام می‌دهد با Script  زیر قابل شبیه سازی میباشد:
Select * into Simulation from 
(Select t.ID,t.Name,t.Color,
Case t.Color
When 'Red' Then Null
Else t.color 
End RedNull
from TestSort as t 
Where t.color is not null) A
سپس روی ستون RedNull از جدول Simulation سورت انجام می‌دهیم:
Select * from Simulation order by Rednull
خروجی:

مطابق شکل،پشت صحنه SQL Server چنین کاری را انجام می‌دهد، و در زمان نمایش ستون RedNull پنهان یا حذف می‌گردد، و ستون Color، Name و ID نمایش داده می‌شود.
امیدوارم مفید واقع شده باشد.
مطالب
رسم گراف

کتابخانه‌های زیادی برای رسم گراف وجود دارند منجمله mxGraph که برای استفاده غیرتجاری رایگان و سورس باز است. mxGraph نگارش‌های PHP ، Java‌ و JavaScript ایی نیز دارد که به همراه بسته مربوطه ارائه می‌شوند.
پس از دریافت آن، در فولدری به نام dotnet می‌توانید سورس کتابخانه مربوط به دات نت فریم ورک آن‌را دریافت کنید.
فایل پروژه‌ی VS.Net را در آن فولدر نخواهید یافت. حتی آن‌را کامپایل هم نکرده‌اند. (احتمالا به این دلیل که کسی نپرسد این پروژه با چه محصولی تولید شده و آیا لایسنس استفاده از آن را دارید یا خیر. این هم یک روش است ...)
برای کامپایل آن، یک پروژه library جدید را در VS.Net آغاز کرده و پوشه‌های موجود در پوشه‌ی dotnet را به آن افزوده و سپس آن‌را کامپایل کنید تا فایل mxGraph.dll تولید شود.

یک مثال ساده از نحوه‌ی استفاده‌ی آن به صورت زیر است که فایل test.png را تولید خواهد کرد.
using System;
using System.Drawing;
using System.Windows.Forms;
using com.mxgraph;
using System.Drawing.Imaging;

void Test1()
{
// Creates graph with model
mxGraph graph = new mxGraph();
Object parent = graph.GetDefaultParent();

// Adds cells into the graph
graph.Model.BeginUpdate();
try
{
Object v1 = graph.InsertVertex(parent, null, "سلام", 20, 20, 80, 30, "strokeColor=#FFCF8A;fillColor=#FFCF8A;gradientColor=white;fontBold=true;fontFamily=tahoma;rounded=true;shadow=true;shape=ellipse");
Object v2 = graph.InsertVertex(parent, null, "!دنیای ظالم", 200, 150, 80, 30, "rounded=true;shadow=true;fontFamily=tahoma");
Object e1 = graph.InsertEdge(parent, null, "e1", v1, v2, "fontFamily=tahoma");
}
finally
{
graph.Model.EndUpdate();
}

mxCellRenderer.CreateImage(graph, null, 1,
Color.White, true, null).Save("test.png", ImageFormat.Png);
}




و یا اگر قصد داشته باشید که از آن در ASP.Net استفاده کنید، یک generic handler را به پروژه خود افزوده (مثلا ImageHandler.ashx) و کد آن‌را برای مثال به صورت زیر تغییر دهید:

using System;
using System.Web;
using com.mxgraph;
using System.Drawing;
using System.Web.Services;
using System.IO;
using System.Drawing.Imaging;

namespace test
{
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class ImageHandler : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
// Creates graph with model
mxGraph graph = new mxGraph();
Object parent = graph.GetDefaultParent();

// Adds cells into the graph
graph.Model.BeginUpdate();
try
{
Object v1 = graph.InsertVertex(parent, null, "سلام", 20, 20, 80, 30, "strokeColor=#FFCF8A;fillColor=#FFCF8A;gradientColor=white;fontBold=true;fontFamily=tahoma;rounded=true;shadow=true;shape=ellipse");
Object v2 = graph.InsertVertex(parent, null, "!دنیای ظالم", 200, 150, 80, 30, "rounded=true;shadow=true;fontFamily=tahoma");
Object e1 = graph.InsertEdge(parent, null, "e1", v1, v2, "fontFamily=tahoma");
}
finally
{
graph.Model.EndUpdate();
}

Image image = mxCellRenderer.CreateImage(graph, null, 1, Color.White, true, null);

// Render BitMap Stream Back To Client
MemoryStream memStream = new MemoryStream();
image.Save(memStream, ImageFormat.Png);

memStream.WriteTo(context.Response.OutputStream);
}

public bool IsReusable
{
get
{
return false;
}
}
}

}
اکنون نحوه استفاده از این handler در یک صفحه وب به صورت زیر است:
<img src="ImageHandler.ashx" />

مطالب
پیاده سازی عملیات 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);
    });
مطالب
شروع به کار با EF Core 1.0 - قسمت 6 - تعیین نوع‌های داده و ویژگی‌های آن‌ها
یکی از مهم‌ترین قسمت‌های مدل سازی موجودیت‌ها، تعیین نوع‌های صحیح ستون‌ها و همچنین تعیین اندازه‌ی مناسبی برای آن‌ها است؛ به همراه تعیین اجباری بودن یا نبودن مقدار دهی آن‌ها.

تعیین اجباری بودن یا نبودن ستون‌ها در EF Core

به صورت پیش فرض در EF Core، هر نوع CLR ایی که نال پذیر باشد، به صورت یک ستون اختیاری در نظر گرفته می‌شود؛ مانند:
 string, int?, byte[]
و هر ستونی که نوع CLR آن نال پذیر نباشد، مقدار دهی آن در EF Core اجباری است؛ مانند:
 int, decimal, bool, DateTime
همچنین باید دقت داشت که حتی اگر در تنظیمات نگاشت‌های برنامه به صورت اختیاری تعریف شوند، باز هم EF Core آن‌ها را اجباری درنظر می‌گیرد.

برای لغو اختیاری بودن یک خاصیت نال پذیر می‌توان از ویژگی Required استفاده کرد:
 [Required]
public string Url { get; set; }
نوع string نال پذیر است. برای لغو این وضعیت می‌توان از ویژگی Required استفاده کرد که در سمت بانک اطلاعاتی نیز به not null ترجمه می‌شود.
و یا معادل Fluent API آن با استفاده از ذکر متد IsRequired است:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
   modelBuilder.Entity<Blog>()
              .Property(b => b.Url)
              .IsRequired();
}
با توجه به این توضیحات، نیازی نیست در بالای یک خاصیت از نوع int، ویژگی Required را ذکر کرد. چون int نال پذیر نیست، مقدار دهی آن اجباری است.


کار با رشته‌ها در EF Core

ذکر یک خاصیت رشته‌ای به این صورت:
public string FirstName { get; set; }
به معنای نال پذیر بودن این ستون است (چون Required تعریف نشده‌است) و همچنین نوع و طول آن در SQL Server به nvarchar max تنظیم می‌شود. این تنظیم طول هرچند در مورد SQL Server صادق است، اما ممکن است در SQL Server CE به nvarchar 4000 تفسیر شود (و این مشکل را به همراه داشته باشد که چرا نمی‌توان متون طولانی را در آن ثبت کرد). به عبارتی عدم ذکر دقیق طول یک خاصیت رشته‌ای، در پروایدرهای مختلف، ممکن است معانی مختلفی را به همراه داشته باشد. بنابراین نیاز است طول خواص رشته‌ای حتما ذکر شوند تا در تمام بانک‌های اطلاعاتی با دقت کامل و بدون حدس و گمان تنظیم گردند.
  [StringLength(450)]
  public string FirstName { get; set; }

  [MaxLength(450)]
  public string LastName { get; set; }

  [MaxLength]
  public string Address { get; set; }
برای تعیین طول دقیق یک فیلد رشته‌ای، می‌توان از ویژگی‌های StringLength و یا MaxLength با ذکر اندازه‌ای استفاده کرد.
برای تعیین صریح یک فیلد رشته‌ای به حداکثر مقدار آن بهتر است ویژگی MaxLength را بدون ذکر پارامتری قید کرد. این مورد جهت سازگاری با بانک‌های اطلاعاتی مختلف ضروری است.
معادل این تنظیمات با روش Fluent API به صورت زیر است:
برای تعیین صریح طول یک فیلد رشته‌ای:
modelBuilder.Entity<Person>()
   .Property(x => x.Address)
   .HasMaxLength(450);
و برای تعیین صریح nvarchar max بودن آن فیلد:
modelBuilder.Entity<Person>()
   .Property(x => x.Address)
   .HasColumnType("nvarchar(max)");
حالت پیش فرض EF Core، کار با رشته‌های یونیکد است. یعنی تمام فیلدهای فوق به nvarchar تفسیر می‌شوند و این n ایی که در ابتدا ذکر شده‌است به معنای یونیکد بودن آن است. اگر می‌خواهید این پیش‌فرض تعیین نوع یونیکد را تغییر دهید، می‌توان از ویژگی Column استفاده کرد:
   [Column(TypeName = "varchar")]
  [MaxLength]
  public string Address { get; set; }
البته اگر اطلاعاتی را که با آن کار می‌کنید چندزبانی و یونیکد هستند، بهتر است این مورد را تغییر ندهید.

نکته‌ای در مورد تغییر نوع خواص: اگر از متد HasColumnType و یا ویژگی Column به نحو فوق استفاده کردید، نیاز است طول رشته را صریحا مشخص کنید. در غیر اینصورت در حین migration خطای ذیل را دریافت خواهید کرد:
 Data type 'varchar' is not supported in this form. Either specify the length explicitly in the type name, for example as 'varchar(16)',
or remove the data type and use APIs such as HasMaxLength to allow EF choose the data type.
در اینجا عنوان می‌کند که اگر مقصود شما varchar max است، ویژگی MaxLength را حذف کرده و تنها بنویسید:
   [Column(TypeName = "varchar(max)")]

نکته‌ای در مورد ایندکس‌ها: در قسمت قبل عنوان شد که می‌توان بر روی خواص، ایندکس منحصربفرد اعمال کرد. در مورد رشته‌ها در SQL Server، اگر طول فیلد مدنظر حداکثر تا 900 بایت باشد، یک چنین کاری را می‌توان انجام داد. البته این محدودیت 900 بایتی تا SQL Server 2014 وجود دارد. این سقف در SQL Server 2016 به 1700 بایت افزایش یافته‌است (900bytes for a clustered index. 1,700 for a nonclustered index). بنابراین چون نوع پیش فرض ستون‌های رشته‌ای، یونیکد و nvarchar درنظر گرفته می‌شود، حداکثر طول امنی را که می‌توان برای آن تعریف کرد، مساوی 450 است (نصف 900 بایت). به همین جهت ذکر ایندکس منحصربفرد بر روی یک ستون رشته‌ای، باید به همراه ذکر اجباری حداکثر طول مساوی 450 آن باشد.


کار با اعداد در EF Core

کلاس نمونه‌ای را با ساختار ذیل درنظر بگیرید:
    public class Person 
    {
        public int Id { set; get; }

        public DateTime? DateAdded { set; get; }

        public DateTime? DateUpdated { set; get; }

        [StringLength(450)]
        public string FirstName { get; set; }

        [MaxLength(450)]
        public string LastName { get; set; }

        //[Column(TypeName = "varchar")]
        [MaxLength]
        public string Address { get; set; }


        //bit
        public bool IsActive { get; set; }

        //tiny Int
        public byte Age { get; set; }

        //small Int
        public short Short { get; set; }

        //int
        public int Int32 { get; set; }

        //Big int
        public long Long { get; set; }
    }
پس از اعمال مهاجرت‌ها و به روز رسانی ساختار بانک اطلاعاتی، به ساختار ذیل خواهیم رسید:


همانطور که ملاحظه می‌کنید، نوع bool دات نت به نوع bit در SQL Server، نوع long به bigint، نوع short به smallint، نوع int به int و نوع byte به tinyint نگاشت شده‌اند.


نکته‌ای در مورد اعداد اعشاری: توصیه شده‌است در تعاریف موجودیت‌های خود بهتر است از نوع‌های float و یا double استفاده نکنید. برای کار با اعداد اعشاری از نوع decimal استفاده نمائید تا بتوانید از قابلیت مقایسه‌ی دقیق آن‌ها استفاده کنید. اطلاعات بیشتر: «روش صحیح مقایسه دو عدد اعشاری با هم»


کار با تاریخ در EF Core

اگر به تصویر فوق دقت کنید، نوع DateTime دات نت به datetime2 در سمت SQL Server ترجمه شده‌است:
 CREATE TABLE [dbo].[Persons](
 [DateAdded] [datetime2](7) NULL,
 [DateUpdated] [datetime2](7) NULL,
اگر در داده‌های خود نیازی به زمان ندارید، می‌توان این نوع پیش فرض را با ویژگی Column که پیشتر بحث شد، به date تغییر داد.
اطلاعات بیشتر: «کنترل نوع‌های داده با استفاده از EF در SQL Server»

به علاوه در دات نت نوع DateTime از نوع value type است. بنابراین همانطور که در ابتدای بحث نیز عنوان شد، مقدار دهی آن اجباری است؛ مگر آنکه آن‌را نال پذیر تعریف کنید.


کار با مباحث همزمانی در EF Core

EF Core به صورت پیش فرض، فرض می‌کند رکوردی را که با آن در حال کار هستید، توسط هیچ کاربر دیگری در شبکه تغییر نیافته‌است و تغییرات شما را در حین فراخوانی متد SaveChanges ذخیره می‌کند. اگر علاقمند هستید که EF Core در صورت تغییر مقدار خاصیت خاصی توسط سایر کاربران، این مساله را با صدور استثنایی به شما اطلاع رسانی کند، از ویژگی ConcurrencyCheck
 [ConcurrencyCheck]
public string Name { set; get; }
و یا متد IsConcurrencyToken حالت Fluent API استفاده نمائید:
modelBuilder.Entity<Person>()
    .Property(p => p.Name)
    .IsConcurrencyToken();
در این حالت کوئری به روز رسانی، علاوه بر فیلد Id رکورد، حاوی فیلد Name نیز خواهد بود (در حین تشکیل شرط یافتن رکورد) و اگر در بین فاصله‌ی یافتن شخص و به روز رسانی نام او، شخص دیگری این‌کار را انجام داده باشد، این به روز رسانی موفقیت آمیز نبوده و استثنایی صادر می‌شود.

اگر علاقمند هستید که تمام فیلدهای جدول تحت نظر قرارگیرند، می‌توان از روش ویژه‌ای به نام Timestamp/row version استفاده کرد:
 [Timestamp]
 public byte[] Timestamp { get; set; }
با معادل Fluent API ذیل:
modelBuilder.Entity<Blog>()
   .Property(p => p.Timestamp)
   .ValueGeneratedOnAddOrUpdate()
   .IsConcurrencyToken();
در مورد ValueGeneratedOnAddOrUpdate در قسمت قبل بحث کردیم. فیلد TimeStamp نیز جزو فیلدهای ویژه‌ای است که SQL Server به صورت خودکار قادر است آن‌را مقدار دهی کند و زمانیکه ValueGeneratedOnAddOrUpdate قید می‌شود، یعنی این فیلد همواره با فراخوانی متد SaveChanges، به صورت خودکار مقدار دهی خواهد شد (و نیازی نیست تا توسط برنامه مقدار دهی شود).
در این حالت در حین به روز رسانی یک چنین رکوردی، اگر از زمان کوئری آن (یافتن رکورد) و ذخیره سازی آن، شخص دیگری آن‌را تغییر داده باشد، به علت عدم تطابق Timestamp ها، عملیات به روز رسانی باشکست روبرو شده و یک استثناء صادر می‌شود.
مطالب
بررسی کارآیی کوئری‌ها در SQL Server - قسمت دوم - جمع آوری اطلاعات آماری کوئری‌ها توسط Extended Events
همانطور که در قسمت قبل نیز بررسی کردیم، Management Studio برای جمع آوری اطلاعات آماری کوئری‌های زنده بسیار مفید است؛ اما تهیه‌ی آن دستی است. باید کوئری را اجرا کرد و سپس مراحلی را طی نمود تا به نتایج آماری حاصل از کوئری‌ها رسید و همچنین دست آخر باید از نتایج آن نیز یک خروجی دستی را تهیه کرد. روش دیگری نیز برای جمع آوری اطلاعات آماری کوئری‌ها در SQL Server توسط Extended Events/Trace وجود دارد که به ازای هر کوئری، قابل استخراج است. علاوه بر آن می‌توان از Dynamic management objects و یا Query store نیز استفاده کرد. این دو برخلاف Extended Events/Trace، اطلاعات تجمعی گروهی از کوئری‌ها را بازگشت می‌دهند. همچنین در اینجا performance monitor نیز می‌تواند مورد استفاده قرار گیرد؛ اما محدوده‌ی دید آن کل بانک اطلاعاتی است.


Extended Events/Trace

Extended Events، زیر ساخت مدیریت رخ‌دادها در SQL Server است. برای مثال در نگارش 2016 آن بیش‌از 300 رخ‌داد در SQL Server تعریف شده‌اند و زمانیکه در مورد اجرای کوئری‌ها بحث می‌کنیم، این رخ‌دادها بیشتر مدنظر ما هستند:
sql_statement_completed
sp_statement_completed
rpc_completed
sql_batch_completed
کار آن‌ها دریافت اطلاعاتی در مورد logical reads، میزان مصرف CPU، مدت زمان اجرای کوئری‌ها و امثال آن‌ها است. در این بین، دو مورد اول بیش از همه مورد استفاده قرار می‌گیرند.
علاوه بر این‌ها، رخ‌دادهای بسط یافته‌ی زیر را نیز می‌توان مورد استفاده قرار داد:
query_post_compilation_showplan
query_post_execution_showplan
query_pre_execution_showplan
اما به علت هزینه‌بر بودن تولید execution plan به ازای هر کوئری، آنچنان مورد استفاده قرار نمی‌گیرند.


استفاده از Extended Events برای جمع آوری اطلاعات آماری کوئری‌ها

برای آزمایش نحوه‌ی کار با Extended Events، ابتدا رویه‌ی ذخیره شده‌ی زیر را ایجاد می‌کنیم:
USE [WideWorldImporters];
GO

DROP PROCEDURE IF EXISTS [Application].[usp_GetCountryInfo];
GO

CREATE PROCEDURE [Application].[usp_GetCountryInfo]
    @Country_Name NVARCHAR(60)
AS

SELECT *
FROM [Application].[Countries] [c]
    JOIN [Application].[StateProvinces] [s]
    ON [s].[CountryID] = [c].[CountryID]
WHERE [c].[CountryName] = @Country_Name;
GO
این کوئری شبیه به کوئری‌است که در قسمت قبل مورد استفاده قرار گرفت؛ با این تفاوت که به همراه یک * SELECT است که استفاده‌ی از آن توصیه نمی‌شود و در اینجا بیشتر جهت بررسی کارآیی این کوئری، تعریف شده‌است.
سپس یک سشن Extended Events سفارشی را به صورت زیر ایجاد می‌کنیم:
/*
Create XE session to capture sql_statement_completed
and sp_statement_completed
*/
IF EXISTS (
SELECT *
FROM sys.server_event_sessions
WHERE [name] = 'QueryPerf')
BEGIN
    DROP EVENT SESSION [QueryPerf] ON SERVER;
END
GO

CREATE EVENT SESSION [QueryPerf] 
ON SERVER 
ADD EVENT sqlserver.sp_statement_completed(WHERE ([duration]>(1000))),
ADD EVENT sqlserver.sql_statement_completed(WHERE ([duration]>(1000)))
ADD TARGET package0.event_file(SET filename=N'C:\Temp\QueryPerf\test.xel',max_file_size=(256))
WITH (
  MAX_MEMORY=16384 KB,EVENT_RETENTION_MODE=ALLOW_SINGLE_EVENT_LOSS,
  MAX_DISPATCH_LATENCY=5 SECONDS,MAX_EVENT_SIZE=0 KB,
  MEMORY_PARTITION_MODE=NONE,TRACK_CAUSALITY=OFF,STARTUP_STATE=OFF);
GO
در این سشن، رخ‌دادهای sp_statement_completed و sql_statement_completed مورد استفاده قرار گرفته‌اند. هر کدام نیز بر اساس مدت زمان اجرای کوئری، فیلتر شده‌اند. در اینجا عدد 1000، یعنی یک میلی ثانیه که عدد بسیار کوچکی است؛ اما برای دمو، مفید است. نتیجه‌ی عملیات نیز در مسیر C:\Temp\QueryPerf ذخیره خواهد شد.

سپس نیاز است تا این سشن را که QueryPerf نام دارد، در قسمت management->extended events، اجرا و آغاز کرد:


در ادامه ابتدا بر روی بانک اطلاعاتی WideWorldImporters، کلیک راست کرده و یک پنجره‌ی new query جدید را ایجاد می‌کنیم:
WHILE 1 = 1
BEGIN
   EXECUTE [Application].[usp_GetCountryInfo] N'United States';
END
در این پنجره با یک حلقه‌ی بی‌پایان، رویه‌ی ذخیره شده‌ای را که ایجاد کردیم، بارها و بارها اجرا خواهیم کرد (نکته‌ی «عدم نمایش ردیف‌های بازگشت داده شده‌ی توسط کوئری در حین جمع آوری اطلاعات آماری» قسمت قبل را هم مدنظر داشته باشید).

سپس مجددا یک پنجره‌ی new query دیگر را باز می‌کنیم:
WHILE 1 = 1
BEGIN
    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';
END
این کوئری شبیه به رویه‌ی ذخیره شده‌ای است که ایجاد کردیم؛ اما یک کوئری Ad Hoc و غیر پارامتری می‌باشد.

کوئری‌های هر دو پنجره را به صورت مجزایی اجرا کنید. سپس در قسمت management->extended events، بر روی سشن QueryPerf کلیک راست کرده و گزینه‌ی View live data را انتخاب کنید:


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

یک نکته: در اینجا در قسمت Details، اگر بر روی هر ردیف کلیک کنید، امکان انتخاب و نمایش آن در لیست بالای صفحه توسط گزینه‌ی Show Column in table وجود دارد.

در آخر در قسمت management->extended events، بر روی سشن QueryPerf کلیک راست کرده و گزینه‌ی Stop Session را انتخاب کنید. اکنون اگر به پوشه‌ی C:\Temp\QueryPerf مراجعه کنید، فایل xel حاوی اطلاعات این گزارش را نیز می‌توانید مشاهده نمائید (به ازای هربار اجرای این سشن، یک فایل جدید را تولید می‌کند).


 این فایل توسط Management Studio قابل گشودن و بررسی است و دقیقا همان نمای گزارش live data را به همراه دارد.
مطالب
پروسیجرها و شنود پارامترها در SQL Server
در اس کیو ال سرور 2016، قابلیت غیر فعال نمودن parameter sniffing در سطح بانک اطلاعاتی مهیا شده است. اما چرا؟


قبل از پاسخگویی به سؤال بالا، به یک سری مقدمات نیاز است:

وقتی یک کوئری به اس کیو ال ارسال می‌شود، چه اتفاقی رخ می‌دهد؟
وقتی یک کوئری ارسال می‌شود، تعدادی از پروسس‌ها بر روی کوئری شروع به فعالیت‌هایی مانند مهیا نمودن داده‌های بازگشتی، یا ذخیره سازی و ... می‌کنند.
 پروسس‌ها به دو دسته زیر تقسیم می‌شوند:
  1. پروسس‌هایی که در relational engine رخ می‌دهند
  2. پروسس‌هایی که در storage engine رخ می‌دهند

در relational engine، هر کوئری pars شده و سپس بوسیله query optimizer پردازش و پلن اجرایی (execution plan) آن که بفرمت باینری است، ایجاد می‌شود و به storage engine ارسال می‌گردد. در storage engine پروسس‌هایی مانند قفل گذاری، نگهداری ایندکس‌ها و تراکنش‌ها رخ می‌دهد. هنگامیکه اس کیو ال سرور کوئری را دریافت می‌نمایند، آن را بلافاصله به relational engine ارسال می‌کند. سپس نحو (syntax) آن بررسی می‌شود؛ این عمل  query parsing نامیده می‌شود. خروجی عملیات پارسر، یک ساختار درختی (query tree) است. این ساختار درختی مشخص کننده مراحل لازم جهت اجرای کوئری ارائه شده می‌باشد.
اگر یک کوئری شامل DML نباشد (مانند ساخت جدول)، علمیات بهبود برروی آن صورت نخواهد گرفت. ولی در صورتیکه کوئری ارسالی، DML باشد، درخت اشاره شده در بالا به algebrizer فرستاده می‌شود که وظیفه آن تفسیر و بررسی کلیه نام اشیاء، جداول و ستون‌های اشاره شده در متن کوئری است. فرآیند algebrizer بسیار مهم و حیاتی است؛ بدلیل اینکه در کوئری ممکن است اشاره کننده‌هایی به اشیایی باشند که در بانک اطلاعاتی موجود نیست. خروجی algebrizer یک query processor tree باینری است که به بهبود دهنده کوئری ارسال می‌گردد. 

معرفی Query Optimizer (بهبود دهنده پرس و جو)

بهبود دهنده، بهترین مسیر اجرای کوئری را مشخص می‌کند. این بهبود دهنده است که مشخص می‌کند که اطلاعات بوسیله ایندکس دریافت شوند، یا اینکه از چه اتصالی استفاده شود و الی آخر. این تصمیمات براساس محاسبات هزینه‌های (میزان پردازش لازم cpu و I/O) پلن اجرایی صورت خواهد پذیرفت. بهمین دلیل به پلن cost-based نیز شناخته می‌شود.
هنگامیکه کوئری ساده‌ای مانند دریافت اطلاعات از یک جدول، که بر روی آن ایندکس گذاری انجام نشده‌است، ارسال شود، بهبود دهنده بجای مشخص نمودن یک پلن مناسب بهینه، از یک پلن ساده (trivial) استفاده می‌کند. ولی برعکس در صورتیکه کوئری trivial نباشد (یعنی مثلا کوئری به گونه‌ای باشد که از ایندکس‌ها به شکل صحیحی استفاده شده باشند)، بهبود دهنده یک پلن مناسب را براساس اطلاعات آماری مهیا شده در اس کیو ال سرور، تولید و انتخاب می‌نماید.

اطلاعات آماری از ستون‌ها و ایندکس‌ها جمع آوری می‌شود. این اطلاعات شامل نحوه توزیع داده، یکتایی و انتخاب شوندگی است. این اطلاعات توسط یک histogram ارائه می‌شود. اگر اطلاعات آماری برای یک ستون و یا ایندکس وجود داشته باشد، بهبود دهنده از آن‌ها برای محاسبات خود استفاده خواهد کرد. اطلاعات آماری بصورت خودکار برای تمام ایندکس‌ها و یا هر ستونی که بشود بر روی آن‌ها where یا join نوشت، فراهم خواهد شد.
بهبود دهنده با مقایسه پلن‌ها براساس بررسی تفاوت‌های انواع joinها، چیدمان مجدد ترتیب join و بررسی ایندکس‌های مختلف و سایر فعالیت‌های دیگر، پلن مناسب را انتخاب و از آن استفاده می‌کند. در طی هر کدام از فعالیت‌های اشاره شده، زمان اجرای آن‌ها نیز تخمین زده (estimated cost) خواهد شد و در پایان، زمان کل تخمینی بدست خواهد آمد و بهبود دهنده از این زمان برای انتخاب پلن مناسب بهره خواهد برد. باید توجه داشت که این زمان تقریبی است. زمانیکه بهبود دهنده پلن اجرایی انتخاب می‌کند، یک actual plan را ایجاد و در حافظه ذخیره می‌شود؛ بنام plan cache. البته درصورتیکه پلن مشابه و بهینه‌تری وجود نداشته باشد. 

استفاده مجدد از پلن ها

تولید پلن هزینه بر است. به‌همین دلیل اس کیوال سرور اقدام به ذخیره سازی و نگهداری آن‌ها می‌کند تا بتواند از آن‌ها مجددا استفاده نماید؛ البته تا جایی که مقدور باشد. هنگامیکه آن‌ها تولید می‌شوند، در قسمتی از حافظه بنام plan cache ذخیره می‌شوند. به این عمل procedure cache نیز گفته می‌شود.

هنگامیکه کوئری به سرور ارسال می‌شود، بوسیله بهبود دهنده، یک estimated plan ایجاد خواهد شد و قبل از اینکه به storage engine ارسال شود، بهبود دهنده estimated plan را با actual execution planهای موجود در plan cache مقایسه می‌کند. در صورتیکه یک actual plan را مطابق با estimated plan پیدا نماید، از آن مجدد استفاده خواهد کرد. این استفاده مجدد به عدم تحمیل سربار اضافه‌ای به سرور جهت کوئری‌های بزرگ و پیچیده که در زمان واحد، هزاران بار اجرا خواهند شد، منجر می‌شود.
هر پلن فقط یکبار در حافظه ذخیره خواهد شد. ولی در مواقعی با تشخیص بهبود دهنده و هزینه پلن، یک کوئری می‌تواند پلن دیگری نیز داشته باشد. بنابراین پلن دوم نیز با مجموعه عملیاتی متفاوت، جهت اجرای موازی (parallel execution) برای یک کوئری ایجاد و در حافظه ذخیره می‌شود.
پلن‌های اجرایی برای همیشه در حافظه باقی نخواهند ماند. پلن‌های اجرایی دارای طول عمری طبق فرمول حاصل ضرب هزینه، در تعداد دفعات می‌باشند. مثلاً پلنی با هزینه 10 و تعداد دفعات اجرای 5، طول عمر 50 را خواهد داشت. پروسس lazywriter که یک پروسس داخلی است وظیفه آزاد سازی تمام انواع کش‌ها، از جمله پلن کش را دارد. این پروسس در بازه‌های مشخص، تمام اشیاء درون حافظه را بررسی کرده و یک واحد از طول عمر آن‌ها می‌کاهد.
در موارد زیر، یک پلن از حافظه پاک خواهد شد:
1. به حافظه بیشتری نیاز باشد
2. طول عمر پلن صفر شده باشد 

حال فرض کنید شما یک پروسیجر یا یک کوئری پارامتری دارید (پارامتر ورودی: شناسه سفارش یا نال) که کلیه محصولات سفارش داده شده یا محصولات یک سفارش خاص را نمایش می‌دهد. هنگامی که SQL Server optimizer پلن این کوئری را ایجاد می‌کند و یا آن را کامپایل می‌کند، به پارامترهای ورودی این پروسیجر گوش می‌دهد (نال یا یک شناسه سفارش). optimizer بوسیله column statistics از تعداد رکوردهایی که بازگشت داده می‌شود، برآوردی می‌کند (مثلا 40 رکورد). سپس یک پلن مناسب را انتخاب می‌کند و آن را برای اجرا ارسال می‌کند و پلن را ذخیره می‌نماید.
جمله آخر، معمولا باعث ایجاد مشکل می‌شوند.
اگر optimizer تکست کوئری مشابهی را مشاهده نماید، ولی با پارامترهای متفاوت، به کش پلن مراجعه کرده و اگر در آن جا قرار داشت، از آن مجددا استفاده می‌نماید. این استفاده مجدد خوب است؛ اما  درصورتیکه پارامتر ارسالی نال باشد چه اتفاقی رخ می‌دهد؟ جدول سفارشات محصول بسیار حجیم است و متاسفانه از پلنی که برای بازگشت 40 رکورد قبلا ایجاد شده، برای بازگشت این حجم بالای از رکوردها استفاده می‌شود که این کشنده است.
هیچ تضمینی وجود ندارد که از وقوع این اتفاق جلوگیری نمایید؛ اما می‌توانید در هنگام توسعه، پروسیجر را شناسایی و نسبت به رفع آنها اقدام نمایید. ابتدا کش پلن را خالی نمایید و سپس پروسیجر را با مقادیر متفاوت، اجرا نمایید. در صورتیکه پلن‌های متفاوتی مشاهده نمودید، این یک علامت هشدار است و می‌بایست نسبت به رفع آن‌ها اقدام فوری نمایید.