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

برای این کار روش استاندارد و مستقیمی وجود ندارد؛ اما  با استفاده از بعضی از روشها مانند ارسال عکس مخفی، برخی تگ‌هایی که یک آدرس را فراخوانی می‌کنند (مانند bgsound)، کلیک روی لینک و یا ترکیبی از چند روش می‌توان تا حدود زیادی به اهداف فوق رسید.

روشی که بررسی خواهیم کرد، مبتنی بر ارسال عکس بوده و برای شروع، به یک عکس بسیار کوچک (1X1) با حجم کم و غیرقابل دید (Transparent) نیاز داریم که می‌توانید یکی از آنها را از این آدرس انتخاب کنید. در این روش به همراه ایمیل، این عکس را ارسال خواهیم کرد (در تگ img). عکس باید بگونه‌ای باشد که ظاهر ایمیل را تحت تأثیر قرار ندهد و آدرس عکس باید طوری تنظیم شود که به برنامه ما یک درخواست را ارسال کند و برنامه با توجه به درخواست ارسال شده، عملیات لازم را انجام دهد. 

مثالی از عکسی که باید ارسال شود: 

<img src='http://www.example.com/1x1.gif' style='width:1px;height:1px;'>

برای شروع یک برنامه‌ی ASP.NET Core را ایجاد کرده و مراحل ارسال ایمیل را طی نمایید:
private bool SendEmail(string mail_to, string mail_subject, string mail_body)
{
    bool result = false;
    try
    {
      SmtpClient client = new SmtpClient("smtp.gmail.com");
      client.UseDefaultCredentials = false;
      client.EnableSsl = true;
      client.Port = 587;
      client.Credentials = new NetworkCredential("from@mail.com", "your_gmail_password");
      MailMessage mailMessage = new MailMessage();       mailMessage.From = new MailAddress("from@mail.com");       mailMessage.To.Add(mail_to);       mailMessage.Body = mail_body;       mailMessage.Subject = mail_subject;       client.Send(mailMessage);       result = true;      }      catch (Exception ex)      {       result = false;      }      return result; }
public IActionResult SendEmailWithTransparentImage()
{
   var email_body = "your_email_body";            
   var imageUrl = Url.Action("ImageRequestFromEmail", "Home", new {user_id=12345}, protocol: Url.ActionContext.HttpContext.Request.Scheme);            
   var imageTag = $"<img src='{imageUrl}' style='width:1px;height:1px;'>";            
   email_body += imageTag;
   var result = SendEmail("to@email.com", "test image", email_body);
   return View(result);
}

سپس اکشن متد پاسخ به درخواست عکس از طرف سرویس دهنده ایمیل را ایجاد نمایید:
[Route("1x1.gif")]
public IActionResult ImageResponse()
{
   //درخواست عکس ارسال شده و اینجا باید عملیات دلخواه را انجام دهیم
   var emailOpenDate = DateTime.Now;
            
   //سپس عکس را در جواب درخواست ارسال میکنیم
   byte[] imagegBytes = Convert.FromBase64String("R0lGODlhAQABAAAAACH5BAEAAAAALAAAAAABAAEAAAI=");
   return File(imagegBytes, "image/gif");
}
اگر اکشن SendEmailWithTransparentImage فراخوانی شود، یک ایمیل که حاوی عکس مورد نظر است، ارسال می‌شود. کاربر با باز کردن ایمیل باعث می‌شود سرویس دهنده ایمیل، درخواست خواندن عکس را به برنامه شما داده و اکشنی را که برای پاسخ به درخواست ارسال عکس نوشته شده، فراخوانی کند. از این طریق متوجه خواهیم شد که ایمیل توسط کاربر باز و مشاهده شده‌است.
بدیهی است می‌توان یک سری کوئری استرینگ را برای بهتر شدن فرآیند، به آدرس عکس مورد نظر اضافه کرد:
<img src='http://www.example.com/1x1.gif?user_id=12345' style='width:1px;height:1px;'>

در این صورت باید تغییراتی را در کدها لحاظ کنیم:
در اکشن SendEmailWithTransparentImage  :
var imageUrl = Url.Action("ImageResponse, "Home", new {user_id=12345},protocol: Url.ActionContext.HttpContext.Request.Scheme);
و امضای اکشن ImageResponse را به‌صورت زیر تغییر می‌دهیم:
public IActionResult ImageResponse(int user_id)

استفاده از عکس‌های کوچک به علت سربار کم و پشتیبانی مرورگرها، بسیار مرسوم بوده و بسیاری از آنالیزورهای سایت مانند google analytics، سایت‌های ارسال انبوه ایمیل و ...  برای رهگیری رفتار کاربران از این روش استفاده می‌کنند. 
برای مثال اگر در همین سایت Developer Tools مرورگر را باز کنید و صفحه را رفرش کنید در تب Network، با کمی دقت عکسی با کوئری استرینگ پیچیده خواهید یافت که مربوط به google analytics است و تقریبا تمام اطلاعات مورد نیاز خود در رابطه با رفتار کاربران در سایت جاری را با استفاده از این روش جمع آوری می‌کند.
نظرات مطالب
متدی برای بررسی صحت کد ملی وارد شده
با سلام
اینم واسه شناسه ملی اشخاص حقوقی منیع
public bool IsValidIranianLegalCode(string input)
        {
            //input has 11 digits that all of them are not equal
            if (!Regex.IsMatch(input, @"^(?!(\d)\1{10})\d{11}$"))
                return false;

            var check = Convert.ToInt32(input.Substring(10, 1));
            int dec = Convert.ToInt32(input.Substring(9, 1)) + 2;
            int[] Coef = new int[10] { 29, 27, 23, 19, 17, 29, 27, 23, 19, 17 };

            var sum = Enumerable.Range(0, 10)
                          .Select(x => (Convert.ToInt32(input.Substring(x, 1)) + dec) * Coef[x])
                          .Sum() % 11;

            return sum == check;
        }

اشتراک‌ها
کتابخانه‌ی CalcBinding برای WPF
Before (Automatic inverse example):

<TextBox Text = "{Binding Path=A, Conveter={x:StaticResource MyMathConverter}">

public class MyMathConverter : IValueConverter
{
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
          var source = (int)value;
          return Math.Sin(source*2)-5;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {        
          var res = Double.Parse(value);
          return (int)(Math.Asin(res + 5) / 2);
        }
}

After:

<TextBox Text = "{c:Binding 'Math.Sin(A*2)-5'}">
کتابخانه‌ی CalcBinding برای WPF
نظرات مطالب
صفحه بندی و مرتب سازی خودکار اطلاعات به کمک jqGrid در ASP.NET MVC
با سلام
اطلاعات رو با entity خوندم توی لایه سرویس و با Automapper توی ویومدل بایندکردم و خروجی رو توسط یک اکشن به jqgrid بر میگردونم
مشکل اینجاست که زمان جستجو به مشکل میخورم
مثلا دو تا جدول استان و شهر دارم
که فیلدهاشون Id , Name هستن
این دوتا جدول رو join می‌کنم و داخل یه ویومدل میریزم با این اسم ها:
StateId
StateName
CityId
CityName
چون خاصیت index  توی گریدویو به این شکل هست
 index: ' (StronglyTyped.PropertyName<StateCityViewModel>(x => x.CityName))',
زمانی که میخوام روی این فیلد جستجو بزنم دچار خطا میشه که میگه این فیلد رو نمیشناسه که راست هم میگه
ممنون میشم پیشنهادی برای این موضوع بدید
کد دریافت اطلاعات لایه سرویس هم اینه
public async Task<DataGridViewModel<StateCityViewModel>> GetDataGridSource(string orderBy, JqGridRequest request, NameValueCollection form, DateTimeType dateTimeType,
            int page, int pageSize)
        {
            var query = _stateCity.AsQueryable();
            
            query = new JqGridSearch(request, form, dateTimeType).ApplyFilter(query);

            var resultsToSkip = page * pageSize;

            var dataGridModel = new DataGridViewModel<StateCityViewModel>
            {
                Records = await query.AsQueryable().OrderBy(orderBy)
                    .Skip(() => resultsToSkip)
                    .Take(pageSize).ProjectTo<StateCityViewModel>(null, _mappingEngine).ToListAsync(),

                TotalCount = await query.AsQueryable().OrderBy(orderBy).CountAsync()
            };

            return dataGridModel;
        }



نظرات مطالب
ارسال انواع بی نام (Anonymous) بازگشتی توسط Entity framework به توابع خارجی
خواهش میکنم قابل نداشت . دوست عزیز این مطلب واقعا پیچیده و تخصصی هستش من یک نمونه واستون نوشتم که کد رو براتون میزارم ولی برای فهم کاملش نیاز به اشنایی عمقی با ساختار دات نت و کار با رفلکشن داره که توضیحش در چند خط نمیگنجه بحثه یک کتاب کامله. این نمونه کد که نوشتم دقیقا همون چیزی هست که درخواست توضیحش رو دادید .
        public static List<T> CreateGenericListFromAnonymous<T>(object obj, T example)
        {
            var newquery = new List<T>();
            var constructor = typeof(T).GetConstructors(
            System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance
            ).OrderBy(c => c.GetParameters().Length).First();
            foreach (var item in ((IEnumerable<object>)obj))
            {
                var mapobj = new object[example.GetType().GetProperties().Count()];
                int counter = 0;
                foreach (var itemmap in example.GetType().GetProperties())
                {

                    object value = item.GetType().GetProperty(itemmap.Name).GetValue(item, null);
                    Type t = itemmap.PropertyType;
                    mapobj[counter] = Convert.ChangeType(value, t);
                    counter++;
                }
                newquery.Add((T)constructor.Invoke(mapobj));
            }

            return newquery;
        }


var context = new Models.EntitiesConnection();
var query = context.Posts.Select(pst => new
                              { 
                                   id = pst.id, 
                                   Title = pst.Title,
                                    Likes = pst.Likes,
                                    Unlikes = pst.Unlikes
                             }).OrderByDescending(c => c.id);

object test_custom_casting = CreateGenericListFromAnonymous(query.ToList(), new { id = 0, Title = string.Empty });
 همینطور که میبینید نتیجه‌ی بازگشتی از یک query مرجع یک list شامل فقط و فقط فیلدهایی هست که به صورت inline توسط انواع بی نام مشخص شده ! امیدوارم مفید واقع شده باشه . یا حق
مطالب
Query Options در پروتکل OData
در قسمت قبل  با OData به صورت مختصر آشنا شدیم. در این قسمت به امکانات توکار OData و جزئیات query options پرداخته و همچنین قابلیت‌های امنیتی این پروتکل را بررسی مینماییم.
در قسمت قبلی، config مربوط به OData و همچنین Controller و Crud مربوط به آن entity پیاده سازی شد. در این قسمت ابتدا سه موجودیت را به نام‌های Product ، Category و همچنین Supplier، به صورت زیر تعریف مینماییم:

به این صورت مدل‌های خود را تعریف کرده و طبق مقاله‌ی قبلی، Controller‌های هر یک را پیاده سازی نمایید:

public class Supplier
{
    [Key]
    public string Key {get; set; }
    public string Name { get; set; }
}
public class Category
{
    public int ID { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Product> Products { get; set; }
}

public class Product
{
    public int ID { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }

    [ForeignKey("Category")]
    public int CategoryId { get; set; }
    public Category Category { get; set; }

    [ForeignKey("Supplier")]
    public string SupplierId { get; set; }
    public virtual Supplier Supplier { get; set; }
}

پکیج Microsoft.AspNet.OData به تازگی ورژن 6 آن به صورت رسمی منتشر شده و شامل تغییراتی نسبت به نسخه‌ی قبلی آن است. اولین نکته‌ی حائز اهمیت، Config آن است که به صورت زیر تغییر کرده و باید Option‌های مورد نیاز، کانفیگ شوند. در این نسخه DI نیز به Odata اضافه شده است:

public static void Register(HttpConfiguration config)
        {
            ODataModelBuilder odataModelBuilder = new ODataConventionModelBuilder();

            var product = odataModelBuilder.EntitySet<Product>("Products");
            var category = odataModelBuilder.EntitySet<Category>("Categories");
            var supplier = odataModelBuilder.EntitySet<Supplier>("Suppliers");

            var edmModel = odataModelBuilder.GetEdmModel();

            supplier.EntityType.Ignore(c => c.Name);

            config.Select(System.Web.OData.Query.QueryOptionSetting.Allowed);
            config.MaxTop(25);
            config.OrderBy(System.Web.OData.Query.QueryOptionSetting.Allowed);
            config.Count(System.Web.OData.Query.QueryOptionSetting.Allowed);
            config.Expand(System.Web.OData.Query.QueryOptionSetting.Allowed);
            config.Filter(System.Web.OData.Query.QueryOptionSetting.Allowed);
            config.Count(System.Web.OData.Query.QueryOptionSetting.Allowed);

            //config.MapODataServiceRoute("ODataRoute", "odata", edmModel); // کانفیگ به صورت معمولی
           config.MapODataServiceRoute("ODataRoute", "odata",
                builder =>
                {
                    builder.AddService(ServiceLifetime.Singleton, sp => edmModel);
                    builder.AddService<IEnumerable<IODataRoutingConvention>>(ServiceLifetime.Singleton, sp => ODataRoutingConventions.CreateDefault());
                });
}
باید همه‌ی Query option‌هایی را که به آنها نیاز داریم، معرفی نماییم و فرض کنید که ProductsController و متد Get آن بدین صورت پیاده سازی شده باشد:
[EnableQuery]
        public IQueryable<Product> Get()
        {
            return new List<Product>
            {
                new Product { Id = 1, Name = "name 1", Price = 11, Category = new Category {Id =1, Name = "Cat1" } },
                new Product { Id = 2, Name = "name 2", Price = 12, Category = new Category {Id =2, Name = "Cat2" } },
                new Product { Id = 3, Name = "name 3", Price = 13, Category = new Category {Id =3, Name = "Cat3" } },
                new Product { Id = 4, Name = "name 4", Price = 14, Category = new Category {Id =4, Name = "Cat4" } },
            }.AsQueryable(); ;
        }
در این پروتکل به صورت توکار، optionهای زیر قابل استفاده است:

 Description Option 
 بسط دادن موجودیت مرتبط  $expand
 فیلتر کردن نتیجه، بر اساس شرط‌های Boolean ی  $filter
 فرمان به سرور که تعداد رکورد‌های بازگشتی را نیز نمایش دهد(مناسب برای پیاده سازی server-side pagging )  $count
 مرتب کردن نتیجه‌ی بازگشتی  $orderby
 select زدن روی پراپرتی‌های درخواستی  $select
 پرش کردن از اولین رکورد به اندازه‌ی n عدد  $skip
 فقط بازگرداندن n رکورد اول  $top
کوئری‌های زیر را در نظر بگیرید:

 در کوئری اول، فقط فیلد‌های Id,Name از Products برگشت داده خواهند شد و در کوئری دوم، از 2 رکورد اول، صرفنظر می‌شود و از بقیه‌ی آنها، فقط 3 رکورد بازگشت داده میشود:
/odata/Products?$select=Id,Name
/odata/Products?$top=3&$skip=2
/odata/Products?$count=true
در پاسخ کوئری فوق، تعداد رکورد‌های بازگشتی نیز نمایش داده میشوند:
{@odata.context: "http://localhost:4516/odata/$metadata#Products", @odata.count: 4,…}
@odata.context:"http://localhost:4516/odata/$metadata#Products"
@odata.count:4
value:[{Id: 1, Name: "name 1", Price: 11, SupplierId: 0, CategoryId: 0},…]
0:{Id: 1, Name: "name 1", Price: 11, SupplierId: 0, CategoryId: 0}
1:{Id: 2, Name: "name 2", Price: 12, SupplierId: 0, CategoryId: 0}
2:{Id: 3, Name: "name 3", Price: 13, SupplierId: 0, CategoryId: 0}
3:{Id: 4, Name: "name 4", Price: 14, SupplierId: 0, CategoryId: 0}
در response، این مقادیر به همراه تعداد رکورد بازگشتی، نمایش داده میشوند که برای پیاده سازی paging مناسب است.
/odata/Products?$filter=Id eq 1
در کوئری فوق eq مخفف equal و به معنای برابر است و بجای آن میتوان از gt به معنای بزرگتر و lt به معنای کوچکتر نیز استفاده کرد:
/odata/Products?$filter=Id gt 1 and Id lt 3
/odata/Products?$orderby=Id desc
در کوئری فوق نیز به صورت واضح، بر روی فیلد Id، مرتب سازی به صورت نزولی خواهد بود و در صورت وجود نداشتن کلمه کلیدی desc، به صورت صعودی خواهد بود.
بسط دادن موجودیت‌های دیگر نیز بدین شکل زیر میباشد:
/odata/Products?$expand=Category
برای اینکه چندین موجودیت دیگر نیز بسط داده شوند، اینگونه رفتار مینماییم:
/odata/Products?$expand=Category,Supplier
برای اینکه به صورت عمیق به موجودیت‌های دیگر بسط داده شود، بصورت زیر:
/odata/Categories(1)?$expand=Products/Supplier
و برای اینکه حداکثر تعداد رکورد بازگشتی را مشخص نماییم:
[EnableQuery(PageSize = 10)]


محدود کردن Query Options
به صورت زیر میتوانیم فقط option‌های دلخواه را فراخوانی نماییم. مثلا در اینجا فقط اجازه‌ی Skip و Top داده شده است و بطور مثال Select قابل فراخوانی نیست:
[EnableQuery (AllowedQueryOptions= AllowedQueryOptions.Skip | AllowedQueryOptions.Top)]
برای اینکه فقط اجازه‌ی logical function زیر را بدهیم (فقط eq):
[EnableQuery(AllowedLogicalOperators=AllowedLogicalOperators.Equal)]
برای اینکه Property خاصی در edm قابلیت نمایش نباشد، در config بطور مثال Price را ignore مینماییم:
var product = odataModelBuilder.EntitySet<Product>("Products");
product.EntityType.Ignore(e => e.Price);
برای اینکه فقط بر روی فیلد‌های خاصی بتوان از orderby استفاده نمود:
[EnableQuery(AllowedOrderByProperties = "Id,Name")]
یک option به نام value$ برای بازگرداندن تنها آن رکورد مورد نظر، به صورت مجزا میباشد. برای اینکار بطور مثال متد زیر را به کنترلر خود اضافه کنید:
public System.Web.Http.IHttpActionResult GetName(int key)
        {
            Product product = Get().Single(c => c.Id == key);            
            return Ok(product.Name);
        }
/odata/Products(1)/Name/$value
و حاصل کوئری فوق، مقداری بطور مثال برابر زیر خواهد بود و نه به صورت convention پاسخ‌های OData، فرمت بازگشتی "text/plain" خواهد بود و نه json:
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
DataServiceVersion: 3.0
Content-Length: 3

Ali

Attribute Convention
هایی هم برای اعتبارسنجی پراپرتی‌ها موجود است که نام آن‌ها واضح تعریف شده‌اند:
 Description  Attribute
 اجازه‌ی فیلتر زدن بر روی آن پراپرتی داده نخواهد شد  NotFilterable
 اجازه‌ی مرتب کردن بر روی آن پراپرتی داده نخواهد شد  NotSortable
 اجازه‌ی select زدن بر روی آن پراپرتی داده نمیشود  NotNavigable
 اجازه‌ی شمارش دهی بر روی آن Collection داده نمیشود  NotCountable
 اجازه‌ی بسط دادن آن Collection داده نمیشود  NotExpandable

و همچنین [AutoExpand] به صورت اتوماتیک آن موجودیت مورد نظر را بسط میدهد.

بطور مثال کد‌های زیر را در مدل خود میتوانید مشاهده نمائید:

public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }

        [NotFilterable, NotSortable]
        public decimal Price { get; set; }

        [ForeignKey(nameof(SupplierId))]
        [NotNavigable]
        public virtual Supplier Supplier { get; set; }
        public int SupplierId { get; set; }


        [ForeignKey(nameof(CategoryId))]
        public virtual Category Category { get; set; }
        public int CategoryId { get; set; }

        [NotExpandable]
        public virtual ICollection<TestEntity> TestEnities { get; set; }
    }

فرض کنید پراپرتی زیر را به مدل خود اضافه کرده اید

public DateTimeOffset CreatedOn { get; set; }

حال کوئری زیر را برای فیلتر زدن، بر روی آن در اختیار داریم:

/odata/Products?$filter=year(CreatedOn) eq 2016

در اینجا فقط Product هایی بازگردانده میشوند که در سال 2016 ثبت شده‌اند:

/odata/Products?$filter=CreatedOn lt cast(2017-04-01T04:11:31%2B08:00,Edm.DateTimeOffset)

کوئری فوق تاریخ مورد نظر را Cast کرده و همه‌ی Product هایی را که قبل از این تاریخ ثبت شده‌اند، باز می‌گرداند.

Nested Filter In Expand

/odata/Categories?$expand=Products($filter=Id gt 1 and Id lt 5)

همه‌ی Category‌ها به علاوه بسط دادن Product هایشان، در صورتیکه Id آنها بیشتر از 1 باشد

و یا حتی بر روی موجودیت بسط داده شده، select زده شود:

/odata/Categories?$expand=Products($select=Id,Name)


Custom Attribute

ضمنا به سادگی میتوان اتریبیوت سفارشی نوشت:

public class MyEnableQueryAttribute : EnableQueryAttribute
{
    public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
    {
       // Don't apply Skip and Top.
       var ignoreQueryOptions = AllowedQueryOptions.Skip | AllowedQueryOptions.Top;
       return queryOptions.ApplyTo(queryable, ignoreQueryOptions);
    }
}

روی هر متدی از کنترلر خود که اتریبیوت [MyEnableQuery] را قرار دهید، دیگر قابلیت Skip, Top را نخواهد داشت.

Dependency Injection در آخرین نسخه‌ی OData اضافه شده است. بطور پیشفرض OData بصورت case-sensitive رفتار میکند. برای تغییر دادن آن در نسخه‌های قدیمی، Extension Methodی به نام EnableCaseSensitive وجود داشت. اما در نسخه‌ی جدید شما میتوانید پیاده سازی خاص خود را از هر کدام از بخش‌های OData داشته باشید و با استفاده از تزریق وابستگی، آن را به config برنامه‌ی خود اضافه کنید؛ برای مثال:

 public class CaseInsensitiveResolver : ODataUriResolver
    {
        private bool _enableCaseInsensitive;

        public override bool EnableCaseInsensitive
        {
            get { return true; }
            set { _enableCaseInsensitive = value; }
        }
    }

اینجا پیاده سازی از ODataUriResolver انجام شده و متد EnableCaseInsensitive به صورت جدیدی override و در حالت default مقدار true را برمیگرداند.

حال به صورت زیر آن را می‌توان به وابستگی‌های config برنامه، اضافه نمود:

config.MapODataServiceRoute("ODataRoute", "odata",
                builder =>
                {
                    builder.AddService(ServiceLifetime.Singleton, sp => edmModel);
                    builder.AddService<IEnumerable<IODataRoutingConvention>>(ServiceLifetime.Singleton, sp => ODataRoutingConventions.CreateDefault());
                    builder.AddService<ODataUriResolver>(ServiceLifetime.Singleton, sp => new CaseInsensitiveResolver()); // how enable case sensitive
                });

در قسمت بعدی به Action‌ها و Function‌ها در OData میپردازیم.

مطالب
تولید SiteMap استاندارد و ایجاد یک ActionResult اختصاصی برای Return کردن SiteMap تولید شده
یکی از item‌های مهم در بهینه سازی SEO یک وب‌سایت وجود یک SiteMap استاندارد متشکل از لینک‌های موجود در سایت هست که در وب‌سایت‌های داینامیک معمولا این لینک‌ها بر اساس داده‌های موجود در بانک اطلاعاتی ایجاد میشه. برای مثال مطالب، اخبار و ....
در اینجا بنده قبلا یک کلاس برای تولید SiteMap آماده کردم که در پروژه‌های خودم ازش استفاده میکنم. توسط این کلاس میتونید به صورت داینامیک SiteMap وب‌سایت مبتنی بر ASP.NET MVC خودتون رو ایجاد کنید.

برای آشنایی با ساختار یک SiteMap استاندارد میتونید به لینک رسمی روبرو مراجعه کنید : http://www.sitemaps.org/de/protocol.html 

بنده کلاس‌های زیر رو بر مبنای لینک مذکور در سایت رسمی SiteMaps تولید کردم.  بعد از تولید SiteMap نیاز دارید که اون رو مثلا به عنوان خروجی یک ActionResult بازگردونید. برای این کار هم یک کلاس با نام XmlResult  مشتق شده از ActionResult آماده سازی کردم که کلاس تولید شده SiteMap رو Serialize میکنه و به عنوان نتیجه‌ی یک Action باز می‌گردونه .
using System;
using System.Collections;
using System.Web.Mvc;
using System.Xml.Serialization;

namespace Neoox.Core.SeoTools
{
    [XmlRoot("urlset", Namespace = "http://www.sitemaps.org/schemas/sitemap/0.9")]
    public class Sitemap
    {
        private ArrayList map;

        public Sitemap()
        {
            map = new ArrayList();
        }

        [XmlElement("url")]
        public Location[] Locations
        {
            get
            {
                Location[] items = new Location[map.Count];
                map.CopyTo(items);
                return items;
            }
            set
            {
                if (value == null)
                    return;
                Location[] items = (Location[])value;
                map.Clear();
                foreach (Location item in items)
                    map.Add(item);
            }
        }

        public int Add(Location item)
        {
            return map.Add(item);
        }
    }

    public class Location
    {
        public enum eChangeFrequency
        {
            always,
            hourly,
            daily,
            weekly,
            monthly,
            yearly,
            never
        }

        [XmlElement("loc")]
        public string Url { get; set; }

        [XmlElement("changefreq")]
        public eChangeFrequency? ChangeFrequency { get; set; }
        public bool ShouldSerializeChangeFrequency() { return ChangeFrequency.HasValue; }

        [XmlElement("lastmod")]
        public DateTime? LastModified { get; set; }
        public bool ShouldSerializeLastModified() { return LastModified.HasValue; }

        [XmlElement("priority")]
        public double? Priority { get; set; }
        public bool ShouldSerializePriority() { return Priority.HasValue; }
    }

    public class XmlResult : ActionResult
    {
        private object objectToSerialize;

        public XmlResult(object objectToSerialize)
        {
            this.objectToSerialize = objectToSerialize;
        }

        public object ObjectToSerialize
        {
            get { return this.objectToSerialize; }
        }

        public override void ExecuteResult(ControllerContext context)
        {
            if (this.objectToSerialize != null)
            {
                context.HttpContext.Response.Clear();
                var xs = new System.Xml.Serialization.XmlSerializer(this.objectToSerialize.GetType());
                context.HttpContext.Response.ContentType = "text/xml";
                xs.Serialize(context.HttpContext.Response.Output, this.objectToSerialize);
            }
        }
    }
}
 و اما نحوه استفاده از این کلاس‌ها هم خیلی سادست. به مثال زیر توجه کنید ... فقط این نکته رو در نظر داشته باشید که item هایی که به sitemap اضافه میشه واکشی شده از بانک اطلاعاتی هست، در این مثال چون خواستم ساده توضیح داده بشه نحوه استفاده از این کلاس‌ها، این داده‌ها به صورت static در نظر گرفته شد ولی شما میتونید داده‌ها رو بر اساس ساختار بانک اطلاعاتی خودتون واکشی کرده و به SiteMap اضافه کنید تا یک SiteMap کاملا پویا و Dynamic داشته باشید...
        public ActionResult Sitemap()
        {
            Sitemap sm = new Sitemap();
            sm.Add(new Location()
            {
                Url = string.Format("http://www.TechnoDesign.ir/Articles/{0}/{1}", 1, "SEO-in-ASP.NET-MVC"),
                LastModified = DateTime.UtcNow,
                Priority = 0.5D
            });
            return new XmlResult(sm);
        }
مطالب
بهینه سازی کوئری‌های LINQ - بخش اول
یکی از جذاب‌ترین لحظات کار با LINQ و EF زمانی است که به خاطر افزایش حجم دیتا، کوئری خود را بازنگری کرده و آن را بهینه می‌کنید.

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

برای نمونه دو Entity زیر را در مدل EF خود داریم:
public class User
{
   public int ID { get; set; }
   public string Name { get; set; }
   public int Age { get; set; }
}

public class Login
{
   public int ID { get; set; }
   public DateTime Date { get; set; }
   public int UserID { get; set; }
   public User User { get; set; }
}
موجودیت User، اطلاعات کاربر و موجودیت Login، اطلاعات مربوط به لوگین‌های هر کاربر را نگه می‌دارد. برای تست، یک دیتاست را به صورت تصادفی تولید کردیم که حاوی 1200 کاربر و 21000 لوگین هست.

برای تولید اطلاعات تصادفی می‌توان از کد زیر در LINQPad استفاده کرد:
int usersCount = 1200;
Random rnd = new Random();
for(int i=0; i<usersCount; i++)
{
   Users.Add(new User()
     {
       Name = $"User {i + 1}",
       Age = rnd.Next(10, i + 10) / 10
     });
}

SaveChanges();

$"Users: {Users.Count()}".Dump();

var usersID = Users.Select(x => x.ID).ToArray();

int loginsCount  = 20000;

for(int i=0; i<loginsCount; i++)
{
    Logins.Add(new Login()
    {
        UserID = usersID[rnd.Next(0, usersID.Length - 1)],
        Date = DateTime.Now.AddDays(rnd.Next(0, i))
    });

    if(i % 1000 == 0)
   {
      SaveChanges();
      $"Save {i + 1}".Dump();
   }
}

SaveChanges();
$"Logins: {Logins.Count()}".Dump();

$"Users: {Users.Count()}".Dump();
$"Logins: {Logins.Count()}".Dump();

Users: 1200
Logins: 21000

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

در سناریوهای این سبکی، باید خیلی با دقت عمل کرد و از تمام اطلاعات موجود استفاده کرد. اطلاعاتی که در اینجا برای ما مفید است، تعداد نسبی رکوردهای جداول دیتابیس است. مثلا در حال حاضر تعداد رکوردهای Logins تقریبا 17 برابر Users است و در آینده هم رشد Logins چند برابر Users خواهد بود. از طرفی در صورت مسئله، اطلاعات هر کاربر را می‌خواهیم، که به سادگی یک SELECT است. ولی بخش سنگین‌تر کوئری، محاسبه‌ی تعداد لوگین‌ها و تاریخ آخرین لوگین‌های هر فرد است که باز هم به جدول Logins بر می‌گردد.

روش اول:

راه حل اولی که به ذهن می‌رسد، JOIN کردن این دو جدول و محاسبه موارد لازم از ترکیب این دو جدول است:
var data =
(
   from u in Users
   join x in Logins on u.ID equals x.UserID into g
   from x in g.DefaultIfEmpty()
   select new
     {
        UserID = u.ID,
        Name = u.Name,
        Age = u.Age,
        Date = x.Date
     }
);

var result =
(
   from d in data
   group d by d.UserID into g
   select new
   {
       UserID = g.Key,
       Name = g.FirstOrDefault().Name,
       LoginsCount = g.Count(x => x.Date != null),
       LastLogin = g.Max(x => (DateTime?) x.Date) ?? null
   }
);
کد SQL تولید شده‌ی در این روش، ترکیبی از 11 دستور SELECT تو در تو و 4 دستور LEFT OUTER JOIN است که ممکن است در حجم اطلاعات بیشتر، کوئری را با کندی همراه کند. نکته‌ی جالب توجه اینست که دستور group by ما در خروجی ظاهر نشده است و تبدیل به دستور SELECT تو در تو شده است که مورد انتظار ما نبوده است.

Generated SQL
SELECT 
    [Project7].[ID] AS [ID], 
    [Project7].[C2] AS [C1], 
    [Project7].[C3] AS [C2], 
    [Project7].[C1] AS [C3]
    FROM ( SELECT 
        [Project6].[ID] AS [ID], 
        CASE WHEN ([Project6].[C3] IS NULL) THEN CAST(NULL AS datetime2) ELSE [Project6].[C4] END AS [C1], 
        [Project6].[C1] AS [C2], 
        [Project6].[C2] AS [C3]
        FROM ( SELECT 
            [Project5].[ID] AS [ID], 
            [Project5].[C1] AS [C1], 
            [Project5].[C2] AS [C2], 
            [Project5].[C3] AS [C3], 
            (SELECT 
                MAX( CAST( [Extent9].[Date] AS datetime2)) AS [A1]
                FROM  [dbo].[Users] AS [Extent8]
                LEFT OUTER JOIN [dbo].[Logins] AS [Extent9] ON [Extent8].[ID] = [Extent9].[UserID]
                WHERE [Project5].[ID] = [Extent8].[ID]) AS [C4]
            FROM ( SELECT 
                [Project4].[ID] AS [ID], 
                [Project4].[C1] AS [C1], 
                [Project4].[C2] AS [C2], 
                (SELECT 
                    MAX( CAST( [Extent7].[Date] AS datetime2)) AS [A1]
                    FROM  [dbo].[Users] AS [Extent6]
                    LEFT OUTER JOIN [dbo].[Logins] AS [Extent7] ON [Extent6].[ID] = [Extent7].[UserID]
                    WHERE [Project4].[ID] = [Extent6].[ID]) AS [C3]
                FROM ( SELECT 
                    [Project3].[ID] AS [ID], 
                    [Project3].[C1] AS [C1], 
                    (SELECT 
                        COUNT(1) AS [A1]
                        FROM [dbo].[Logins] AS [Extent5]
                        WHERE [Project3].[ID] = [Extent5].[UserID]) AS [C2]
                    FROM ( SELECT 
                        [Distinct1].[ID] AS [ID], 
                        (SELECT TOP (1) 
                            [Extent3].[Name] AS [Name]
                            FROM  [dbo].[Users] AS [Extent3]
                            LEFT OUTER JOIN [dbo].[Logins] AS [Extent4] ON [Extent3].[ID] = [Extent4].[UserID]
                            WHERE [Distinct1].[ID] = [Extent3].[ID]) AS [C1]
                        FROM ( SELECT DISTINCT 
                            [Extent1].[ID] AS [ID]
                            FROM  [dbo].[Users] AS [Extent1]
                            LEFT OUTER JOIN [dbo].[Logins] AS [Extent2] ON [Extent1].[ID] = [Extent2].[UserID]
                        )  AS [Distinct1]
                    )  AS [Project3]
                )  AS [Project4]
            )  AS [Project5]
        )  AS [Project6]
    )  AS [Project7]
    ORDER BY [Project7].[C3] ASC, [Project7].[ID] ASC

روش دوم:
روش دوم اینست که داده‌های سنگین‌تر (اطلاعات Logins) را ابتدا محاسبه کرده و سپس JOIN را انجام دهیم:
var data =
(
  from x in Logins
  group x by x.UserID into g
  orderby g.Key descending
  select new
  {
    UserID = g.Key,
    LoginsCount = g.Count(),
    LastLogin = g.Max(d => d.Date)
  }
);

var result =
(
  from u in Users
  join d in data on u.ID equals d.UserID into g
  from d in g.DefaultIfEmpty()
  select new
  {
    UserID = u.ID,
    LoginsCount = d != null ? d.LoginsCount : 0,
    LastLogin = d != null ? (DateTime?)d.LastLogin : null
  }
);
در روش دوم، ابتدا فقط به Logins کوئری می‌زنیم و برای محاسبه‌ی تعداد لوگین و آخرین لوگین، از Group By استفاده می‌کنیم. استفاده از این دستور باعث می‌شود که محاسبه‌ی سنگین ما در سریعترین حالت ممکن توسط  SQL انجام شود. در مرحله‌ی بعد، این اطلاعات را با جدول Users از طریق LEFT OUTER JOIN ترکیب می‌کنیم. علت استفاده از DefaultIfEmpty بدین سبب است که برخی از کاربران ممکن است تاکنون لوگینی را انجام نداده باشند؛ در نتیجه باید تعداد صفر و تاریخ null برای آنها نمایش داده شود.

اکنون اگر کد SQL روش دوم را بررسی کنیم خواهیم دید که تنها 2 دستور SELECT ، یک LEFT OUTER JOIN به همراه یک GROUP BY تولید شده است که با توجه به ماهیت مسئله و ساختار دیتای ما، این دستورات منطقی‌ترین و بهینه‌ترین دستورات ممکن به نظر می‌رسد.

Generated SQL
SELECT 
    [Project1].[ID] AS [ID], 
    [Project1].[C1] AS [C1], 
    [Project1].[C2] AS [C2]
    FROM ( SELECT 
        [Extent1].[ID] AS [ID], 
        CASE WHEN ([GroupBy1].[K1] IS NOT NULL) THEN [GroupBy1].[A1] ELSE 0 END AS [C1], 
        CASE WHEN ([GroupBy1].[K1] IS NOT NULL) THEN  CAST( [GroupBy1].[A2] AS datetime2) END AS [C2]
        FROM  [dbo].[Users] AS [Extent1]
        LEFT OUTER JOIN  (SELECT 
            [Extent2].[UserID] AS [K1], 
            COUNT(1) AS [A1], 
            MAX([Extent2].[Date]) AS [A2]
            FROM [dbo].[Logins] AS [Extent2]
            GROUP BY [Extent2].[UserID] ) AS [GroupBy1] ON [Extent1].[ID] = [GroupBy1].[K1]
    )  AS [Project1]
    ORDER BY [Project1].[C1] ASC, [Project1].[ID] ASC
پس، همواره کد SQL دستورات LINQ خود را یا از طریق SQL Profiler یا برنامه‌ای مثل LINQPad حتما تست کنید و کوئری خود را در مقابل حجم زیاد اطلاعات هم بررسی کنید. چرا که LINQ به علت سادگی و قدرتی که دارد، گاهی شما را به اشتباه می‌اندازد و باعث می‌شود شما کوئری ای بزنید که جواب شما را می‌دهد، ولی فقط برای حجم کم دیتای کنونی بهینه است و در صورت افزایش رکوردها، یا خیلی کند می‌شود یا کلا شما را با  Timeout مواجه می‌کند.
مطالب
آشنایی با OWIN و بررسی نقش آن در ASP.NET Core
در این مطلب می‌خواهیم نگاهی به قسمت‌های کلیدی OWIN و همچنین پروژه‌ی Katana بیندازیم و در نهایت نیز نقش OWIN را در ASP.NET Core بررسی خواهیم کرد.



OWIN چیست؟

همانطور که می‌دانید OWIN یک specification است که استانداری را بین وب‌سرور و وب‌اپلیکیشن‌ها تعریف کرده است. در واقع OWIN یکسری لایه‌ی انتزاعی را جهت ایجاد اپلیکیشن‌هایی که نحوه‌ی میزبانی آنها اهمیتی ندارد، تعریف خواهد کرد. به صورت خلاصه توسط این لایه‌ی انتزاعی می‌توانیم وابستگی بین وب‌سرور و وب‌اپلیکیشن را حذف کنیم. در این specification منظور از وب‌سرور یک delegate و همچنین یک دیکشنری است. در واقع هدف این است که وقتی درخواستی به این وب‌سرور ارسال شد، درخواست به قسمت‌های کوچکی تقسیم‌بندی شده و درون این دیکشنری قرار خواهند گرفت (این دیکشنری حاوی کلیدهای از پیش‌تعریف شده‌ای است که توسط OWIN تعریف شده‌اند). سپس این دیکشنری از طریق یک application function به درون pipeline ارسال خواهد شد و از یکسری middleware عبور خواهد کرد. در اینحالت می‌توانیم کنترلی را بر روی درخواست‌های وارده و صادره داشته باشیم. ایده‌ی middleware خیلی شبیه به HTTP moduleها در IIS است؛ اما تفاوت آن این است که middlewareها وابستگی‌ایی به IIS ندارند و همچنین مبتنی بر رویداد نیستند. هر middleware بعد از انجام تغییرات بر روی درخواست، تا زمان رسیدن دیکشنری به آخرین middleware، آن را به middleware بعدی ارسال خواهد کرد. در این حین می‌توانیم به response streams اطلاعاتی را append کنیم. وقتی دیکشنری از تمامی middlewareها عبور کرد، سرور مطلع خواهد شد و نتیجه را به کلاینت ارسال می‌کند.


استاندارد OWIN تعدادی کلید را درون یک دیکشنری تعریف کرده است که بعد از ورود به هر middleware مقداردهی خواهند شد. این کلیدها را می‌توانیم در دو دسته‌ی Request و Response بررسی کنیم.

کلیدهای مربوط به Request

ضروری؟

نام کلید

مقدار

بله

"owin.RequestBody"

یک Stream همراه با request body. اگر body برای request وجود نداشته باشد، Stream.Null به عنوان placeholder قابل استفاده است.

بله

"owin.RequestHeaders"

یک دیکشنری به صورت IDictionary<string, string[]> از هدرهای درخواست.

بله

"owin.RequestMethod"

رشته‌ایی حاوی نوع فعل متد HTTP مربوط به درخواست (مانند GET and POST )

بله

"owin.RequestPath"

path درخواست شده به صورت string

بله

"owin.RequestPathBase"

قسمتی از path درخواست به صورت string

بله

"owin.RequestProtocol"

نام و نسخه‌ی پروتکل (مانند HTTP/1.0 or HTTP/1.1 )

بله

"owin.RequestQueryString"

رشته‌ای حاوی query string ؛ بدون علامت ? (مانند foo=bar&baz=quux )

بله

"owin.RequestScheme"

رشته‌ایی حاوی URL scheme استفاده شده در درخواست (مانند HTTP or HTTPS )



کلیدهای مربوط به Response

ضروری؟

نام کلید

مقدار

بله

"owin.ResponseBody"

یک Stream جهت نوشتن response body در خروجی

بله

"owin.ResponseHeaders"

یک دیکشنری به صورت IDictionary<string, string[]> از هدرهای response

خیر

"owin.ResponseStatusCode"

یک عدد صحیح حاوی کد وضعیت HTTP response ؛ حالت پیش‌فرض 200 است.

خیر

"owin.ResponseReasonPhrase"

یک رشته حاوی reason phrase مربوط به status code ؛ اگر خالی باشد در نتیجه سرور بهتر است آن را مقداردهی کند.

خیر

"owin.ResponseProtocol"

یک رشته حاوی نام و نسخه‌ی پروتکل (مانند HTTP/1.0 or HTTP/1.1 )؛ اگر خالی باشد؛ “owin.RequestProtocol” به عنوان مقدار پیش‌فرض در نظر گرفته خواهد شد.


Katana
پروژه‌ی Katana یک پیاده‌سازی از استاندارد OWIN است که توسط مایکروسافت ایجاد شده است. مایکروسافت علاوه بر پیاده‌سازی OWIN، یکسری قابلیت دیگر را نیز به آن اضافه کرده است. برای شروع کار با Katana یک پروژه خالی از نوع ASP.NET Web Application را ایجاد کنید. در ادامه لازم است پکیج Microsoft.Owin.Host.SystemWeb را نیز نصب کنیم. همراه با نصب این پکیج، دو وابستگی دیگر نیز نصب خواهند شد؛ زیرا پیاده‌سازی OWIN درون پکیج Microsoft.Owin قرار دارد:
<package id="Microsoft.Owin" version="3.0.1" targetFramework="net461" />
<package id="Microsoft.Owin.Host.SystemWeb" version="3.0.1" targetFramework="net461" />
<package id="Owin" version="1.0" targetFramework="net461" />
در ادامه نیاز به یک نقطه‌ی شروع برای اپلیکیشن‌مان داریم. طبق convention باید یک فایل را با نام Startup.cs با محتویات زیر ایجاد کنیم:
using Owin;
namespace SimpleOwinWebApp
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {

        } 
    }
}
توسط IAppBuilder می‌توانیم middlewareها را به pipeline اضافه کنیم:
using Owin;
namespace SimpleOwinWebApp
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.Use(async (ctx, next) =>
            {
                await ctx.Response.WriteAsync("Hello");
            });
        } 
    }
توسط متد Use، یک middleware را به صورت inline تعریف کرده‌ایم. متد Use یک delegate را از ورودی دریافت خواهد کرد و امضای آن به اینصورت است:
Func<IOwinContext, Func<Task>, Task> handler

IOwinContext در واقع یک wrapper برای environment dictionaryایی است که در ابتدا به آن اشاره کردیم. در مثال قبل، از پراپرتی Response، جهت ارسال خروجی به کلاینت استفاده شده است. این پراپرتی در واقع معادل کلید owin.ResponseBody درون دیکشنری است. اما در اینجا به صورت strongly-typed و ساده به آن دسترسی داریم؛ هر چند که امکان کار با دیکشنری خام نیز وجود دارد. به عنوان مثال معادل مثال قبل بدون استفاده از پراپرتی Response، اینچنین خواهد بود:
app.Use(async (ctx, next) =>
{
   var response = ctx.Environment["owin.ResponseBody"] as Stream;
   using (var writer = new StreamWriter(response))
   {
      await writer.WriteAsync("Hello");
   }
});
اکنون اگر پروژه را اجرا کنید، با وارد کردن هر آدرسی، پیام Hello درون مرورگر برایتان نمایش داده خواهد شد:


به هر تعداد middleware که خواستید می‌توانید به pipeline اضافه کنید؛ اما باید دقت داشته باشید که ترتیب قرار دادن آنها اهمیت دارد.

Self-hosting OWIN
در مثال قبلی، اپلیکیشن توسط IIS Express اجرا می‌شد. برای میزبانی درون یک کنسول اپلیکیشن، ابتدا یک پروژه‌ی Console Application را ایجاد کرده و پکیج Microsoft.Owin.SelfHost را نصب کنید. سپس کلاس Startup موردنظرتان را ایجاد کرده و در نهایت درون متد Main، کار راه‌اندازی سرور را انجام خواهیم داد:
using System;
using Microsoft.Owin.Hosting;

namespace SimpleOwinConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            using (WebApp.Start<Startup>("http://localhost:12345"))
            {
                Console.WriteLine("Listening to port 12345");
                Console.WriteLine("Press Enter to end...");
                Console.ReadLine();
            }
        }
    }
}

OWIN در ASP.NET Core
ASP.NET Core دارای مفهومی با عنوان pipeline است. این pipeline خیلی شبیه به OWIN است اما OWIN نیست؛ بلکه عملکرد آن شبیه به OWIN است. به عنوان مثال اینبار به جای دیکشنری، شیء HttpContext را داریم. در ادامه یک پروژه‌ی ASP.NET Core Web Application از نوع Empty را شروع خواهیم کرد. اگر دقت کنید اینبار برای کلاس Startup باید دو متد را پیاده‌سازی کنیم:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace SimpleOwinCoreApp
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.Run(async (context) =>
            {
                await context.Response.WriteAsync("Hello World!");
            });
        }
    }
}

متد Configure: همانطور که در ابتدای مطلب مشاهده کردید این متد قبلاً در پروژه‌های مبتنی بر کاتانا Configuration نام داشت؛ همچنین به جای IAppBuilder اینبار IApplicationBuilder را داریم. مزیت ASP.NET Core این است که در هر جایی از اپلیکیشن می‌توانیم از سیستم DI توکار آن استفاده کنیم؛ در نتیجه علاوه بر IApplicationBuilder وابستگی‌های دیگری مانند IHostingEnvironment و ILoggerFactory را نیز می‌توانیم تزریق کنیم.
متد ConfigureServices: در اینجا می‌توانیم سرویس‌های موردنیاز درون اپلیکیشن را برای IoC ریجستر کنیم.
در کد فوق استفاده از متد Use به معنای آخرین نقطه در pipeline است. یعنی جایی که response برگردانده خواهد شد و چیزی بعد از آن اجرا نخواهد شد؛ در واقع ارجاعی به middleware بعدی وجود ندارد.

ایجاد یک Middleware جدید
تا اینجا تمامی کدها را به صورت inline نوشتیم. اما اگر بخواهیم middlewareمان قابلیت استفاده‌ی مجدد داشته باشد می‌توانیم تعاریف آن را به یک کلاس با ساختار زیر منتقل نمائیم:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;

namespace SimpleOwinCoreApp.Middlewares
{
    public class SimpleMiddleware
    {
        private readonly RequestDelegate _next;

        public SimpleMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task Invoke(HttpContext ctx)
        {
            // قبل از فراخوانی میان‌افزار بعدی

            await ctx.Response.WriteAsync("Hello DNT!");

            await _next(ctx);

            // بعد از فراخوانی میان‌افزار بعدی
        }
    }
}

درون متد Invoke بعد از پردازش درخواست باید متد middleware بعدی را همراه با context جاری فراخوانی کنیم. در نتیجه قبل و بعد از فراخوانی middleware بعدی فرصت این را خواهیم داشت تا درخواست را پردازش کنیم. در نهایت برای استفاده از middleware فوق می‌توانیم از متد الحاقی UseMiddleware استفاده کنیم:
app.UseMiddleware<SimpleMiddleware>();

استفاده از middlewareهای مبتنی بر Katana در ASP.NET Core
middlewareهایی را که برای Katana نوشته‌اید، درون یک اپلیکیشن ASP.NET Core نیز قابل استفاده هستند. برای اینکار با مراجعه به فایل project.json می‌توانید پکیج زیر را نصب کنید:
"Microsoft.AspNetCore.Owin": "1.0.0"
سپس درون متد Configure می‌توانید Owin را به pipeline اضافه کرده و middleware خود را ریجستر کنید:
app.UseOwin(pipeline =>
{
pipeline(next => new MyKatanaBasedMiddleware(next).Invoke)
});

مثال تکمیلی:
در ادامه می‌خواهیم ماژول مطرح شده در این مطلب  را به صورت یک middleware با قابلیت پذیرفتن تنظیمات، نوشته و سپس درون pipeline استفاده کنیم. برای شروع یک کلاس با نام IpBlockerMiddleware با محتویات زیر ایجاد خواهیم کرد:
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;

namespace SimpleOwinAspNetCore.Middleware
{
    public class IpBlockerMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly IpBlockerOptions _options;

        public IpBlockerMiddleware(RequestDelegate next, IpBlockerOptions options)
        {
            _next = next;
            _options = options;
        }

        public async Task Invoke(HttpContext context)
        {
            var ipAddress = context.Request.Host.Host;
            if (IsBlockedIpAddress(ipAddress))
            {
                context.Response.StatusCode = 403;
                await context.Response.WriteAsync("Forbidden : The server understood the request, but It is refusing to fulfill it.");
                return;
            }
            await _next.Invoke(context);
        }

        private bool IsBlockedIpAddress(string ipAddress)
        {
            return _options.Ips.Any(ip => ip == ipAddress);
        }
    }
}
در کدهای فوق لیست Ipها از پراپرتی Ips درون کلاس IpBlockerOptions دریافت خواهد شد:
using System.Collections.Generic;

namespace SimpleOwinAspNetCore.Middleware
{
    public class IpBlockerOptions
    {
        public IpBlockerOptions()
        {
            Ips = new[] { "192.168.1.1" };
        }
        public IList<string> Ips { get; set; }
    }
}
همچنین برای استفاده راحت‌تر از middleware، یک متد الحاقی را برای آن ایجاد کرده‌ایم و سپس پراپرتی Ips را توسط اینترفیس IConfigurationRoot دریافت کرده‌ایم:
using System.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;

namespace SimpleOwinAspNetCore.Middleware
{
    public static class IpBlockerExtensions
    {
        public static IApplicationBuilder UseIpBlocker(this IApplicationBuilder builder, IConfigurationRoot configuration, IpBlockerOptions options = null)
        {
            return builder.UseMiddleware<IpBlockerMiddleware>(options ?? new IpBlockerOptions
            {
                Ips = configuration.GetSection("block_list").GetChildren().Select(p => p.Value).ToArray()
            });
        }
    }
}
قبلاً در رابطه با فایل‌های کانفیگ مطلبی را مطالعه کرده‌اید؛ در نتیجه نیازی به توضیح اضافه‌تری ندارد. تنها کاری که در اینجا انجام شده است، دریافت محتویات کلید block_list از فایل کانفیگ است. 
محتویات فایل blockedIps.json:
{
  "block_list": [
    "192.168.1.1",
    "localhost",
    "127.0.0.1",
    "172.16.132.151"
  ]
}

برای خواندن فایل فوق در برنامه نیز خواهیم داشت:
public IConfigurationRoot Configuration { set; get; }

public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("blockedIps.json");
Configuration = builder.Build();
}
در نهایت برای استفاده از middleware فوق خواهیم داشت:
app.UseIpBlocker(Configuration);
اکنون هر درخواستی که با آدرس‌های تعیین شده درون فایل blockedIps.json وارد pipeline شود، امکان استفاده‌ی از سایت را نخواهد داشت.

کدهای این مطلب را می‌توانید از اینجا دریافت کنید.
مطالب
پیاده سازی عملیات 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);
    });