بازخوردهای دوره
آشنایی با AOP Interceptors
باید از قابلیت scan در StructureMap استفاده کنید:
            ObjectFactory.Initialize(x =>
            {
                var dynamicProxy = new ProxyGenerator();
                x.Scan(scanner =>
                    {
                        scanner.AssemblyContainingType<IMyType>(); // نحوه یافتن اسمبلی لایه سرویس

                        // Connect `IName` interface to 'Name' class automatically
                        scanner.WithDefaultConventions()
                               .OnAddedPluginTypes(plugin => plugin.EnrichWith(target =>
                                    dynamicProxy.CreateInterfaceProxyWithTargetInterface(target.GetType().GetInterfaces().First(),
                                                                 target.GetType().GetInterfaces(), target,
                                                                 new LoggingInterceptor())
                               ));
                    });
            });
- در این حالت AssemblyContainingType مشخص می‌کند که کدام اسمبلی باید اسکن شود.
- WithDefaultConventions یعنی هرجایی IName داشتیم را به صورت خودکار به Name متصل کن. (روال پیش فرض سیم کشی اینترفیس‌ها و کلاس‌ها برای وهله سازی)
- OnAddedPluginTypes یک Callback هست که زمان انجام اولیه تنظیمات به ازای هر type یافت شده فراخوانی می‌شود. در اینجا می‌شود با استفاده از EnrichWith و ProxyGenerator کار اتصال کلاس Interceptor را انجام داد.
مطالب
افزونه جی کوئری RowAdder
دیروز در یک برنامه میخواستم کاربر بتواند لیست مواد مصرفی یک کارخانه را ایجاد کند که نیاز بود کاربر بتواند از هر سطر به تعداد نامحدود ایجاد کند و برای انتخاب هر یک از مواد به همراه جزئیات آن یک سطر به لیست اضافه شود. برای اینکار میتوانیم با استفاده از فناوری جی کوئری اینکار را انجام دهیم ولی بهتر بود که این مورد به یک افزونه تبدیل میشد تا در دفعات بعدی بسیار راحت‌تر باشیم. جهت آشنایی با پلاگین نویسی بهتر هست این مقالات (+) را مطالعه فرمایید.

نحوه استفاده
نحوه استفاده از آن بسیار راحت است و در دموی html همراه آن به طور ساده در سه مثال توضیح داده شده است. ابتدا از این آدرس کتابخانه آن را دریافت کنید. این کتابخانه شامل یک فایل js که شامل کدهای پلاگین است، یک فایل css جهت تغییر استایل کدهایی است که پلاگین تولید میکند که اسامی آن دقیقا مشخص می‌کند که هر کلاس متعلق به چه بخشی است.

گام اول:
فایل‌های مورد نظر را بعد از صدا زدن کتابخانه‌ی جی کوئری صدا بزنید.
<link type="text/css" href="css/RowAdder.css" rel="stylesheet" />
    <script src="js/RowAdder.js" type="text/javascript"></script>


گام دوم :
 در تکه کدهای html، کدی را که قرار است در هر سطر تکرار شود، داخل یک div قرار داده و نامی مثل row-sample را برای آن قرار دهید (فعلا حتما این نام باشد)، بعدها پلاگین، کدهای داخل این تگ div را به عنوان هر سطر خواهد شناخت:
<div id="row-sample">
    <form style="margin: 0; padding: 0;">
        Name:<input type="text"/>
        <input type="radio" name="Gender" value="male" checked="checked">Male
        <input type="radio" name="Gender" value="female">Female
    </form>
</div>


گام سوم:
 سپس یک div دیگر ایجاد کنید و نامی مثل mypanel را به آن بدهید تا سطرهایی که ایجاد می‌شوند داخل این div قرار بگیرند.
<div id="mypanel"></div>

گام چهارم:
در بخش head یک تگ اسکریپت باز کرده و کدهای زیر را به آن اضافه می‌کنیم. این کد باعث می‌شود که پلاگین فعال شود.
<script>
$(document).ready(function() {
$("#mypanel").RowAdder();
});
</script>
گام پنجم:
 یک دکمه جهت افزودن سطر به صفحه اضافه می‌کنیم
<button id="addanotherform">Add New Form</button>

و در قسمت تگ اسکریپت هم کد زیر را اضافه می‌کنیم:
$("#addanotherform").on('click', function() {
                $("#mypanel").RowAdder('add');
            });

حال از صفحه تست می‌گیریم: با هر بار کلیک بر روی دکمه‌ی Add New Form یک سطر جدید ایجاد می‌گردد.


در تصویر بالا دکمه‌های دیگر هم دیده می‌شوند که به دیگر متدهای آن اشاره دارد:

جهت مخفی سازی:
 $("#mypanel").RowAdder('hide');

چهت نمایش:
$("#mypanel").RowAdder('show');

جهت افزودن سطر با کد:
$("#mypanel").RowAdder('add');

جهت دریافت تعداد سطرهای ایجاد شده:
$("#mypanel").RowAdder('count')


جهت دریافت کدهای یک سطر در اندیس x

$("#mypanel").RowAdder('content', 3)

جهت حذف یک سطر با اندیس x
$("#mypanel").RowAdder('remove', 3);

همانطور که با صدا زدن اولین متد پلاگین متوجه شدید و نتیجه‌ی آن را در دمو دیدید، این پلاگین از پیش فرض‌هایی جهت راه اندازی اولیه استفاده می‌کند که این پیش فرض‌ها عبارتند از تگ row-sample که بدون معرفی رسمی، آن را شناسایی کرد. همچنین ممکن است بخواهید عبارت Remove را با کلمه‌ی فارسی «حذف» جایگزین نمایید. برای اینکار می‌توانید پلاگین را به شکل زیر به کار ببرید:
    $("#mypanel").RowAdder({
                sample: '#my-custom-sample',
                type: 'text',
                value:'حذف'
        });

تغییر اولین پیش فرض، تغییر نام تگ row-sample به my-custom-sample بود و در مرحله‌ی بعد هم نام فارسی حذف را جایگزین remove کردیم. عبارت type به طور پیش فرض بر روی text قرار دارد که اجباری به ذکر آن در کد بالا نبود. ولی اگر دوست دارید که به جای نمایش عبارت حذف، از یک آیکن یا تصویر استفاده کنید، کد را به شکل زیر تغییر دهید:
  $("#mypanel").RowAdder({
                type: 'image',
                value: 'images/remove.png'
            });
در خطوط بالا عبارت type با image مقدار دهی شد و به پلاگین می‌گوید که به جای متن، از تصویر استفاده کن. همچنین value را به جای متن با آدرس تصویر مقداردهی کرده‌ایم و نتیجه را می‌توانید در دموی قرار گرفته در گیت هاب ببینید.

فایل RowAdder.css
در بردارنده هر سطر
.each-section {
    margin: 20px;
    padding: 5px;
}

جهت استایل بندی لینک چه تصویر و چه متن
.remove-link {
    color:#999;
     text-decoration: none;
}

a:hover.remove-link {
   color:#802727;
}
جهت تغییر استایل بر روی خود تصویر
.remove-image {
    
}

آشنایی با کد پلاگین
(function ($) {
    
    var settings = null;
  $.fn.RowAdder = function (method) {
    
            // call methods
            if (methods[method]) {
                return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
            } else if (typeof method === 'object' || !method) {
                return methods.init.apply(this, arguments);
            } else {
                $.error('Method ' + method + ' does not exist on jQuery.RowAdder');
            }
      

    };
})(jQuery);
در قسمت دوم آموزش پلاگین نویسی برای جی کوئری، متدها به طور واضح توضیح داده شده‌اند. این کدها وظیفه دارند متدهایی را که کاربر درخواست داده است، شناسایی و به همراه آرگومان‌های آن به سمت توابعی که به هر نام متد اختصاص داده‌ایم، ارسال کنند. در صورتیکه متدی با آرگومان‌های ناهماهنگی ارسال شوند، پیام خطایی ارسال می‌گردد و در صورتیکه تعریف نشود، به طور مستقیم init را صدا می‌زند. متغیر settings هم بعدا با تنظیمات پیش فرض پر می‌شود.

متدها
//methods
    var methods = {
        init: function (options) {
            //default-settings
             settings = $.extend({
                'sample': '#row-sample',
                'type': 'text',
                'value': 'Remove'
             }, options);
             this.attr('data-sample', settings.sample);
             this.attr('data-type', settings.type);
             this.attr('data-value', settings.value);
            Do(this);
        },
        show: function () {
            this.css("display", "inline");
        },
        hide: function () {
            this.css("display", "none");
        },
        add: function () {
            Do(this);
        },
        remove: function (index) {
            console.log(index);
           this.find(".each-section")[index].remove();
        },
        content: function (index) {
            return this.find(".each-section")[index];
        },
        count: function (index) {
            return this.find(".each-section").size();
        }
    };
متد init تنظیمات پیش فرض را دریافت می‌نماید و سپس بر روی المانی که پلاگین روی آن واقع شده‌است، مقادیر را ذخیره می‌کند تا در آینده با صدا زدن متدهای دیگر آن را استفاده نماید. کلمه‌ی this در واقع به تگی اشاره می‌کند که پلاگین روی آن اعمال شده است که در مثال‌های بالا mypanel نام داشت. متد Do تابع اصلی ما را در بر دارد که کدهای اصلی پلاگین را شامل می‌شود. مابقی متدها در واقع  جست و جویی بر المان‌ها هستند.

تابع Do
    function Do(panelDiv) {

        settings.sample = panelDiv.data('sample');
        settings.type = panelDiv.data('type');
        settings.value = panelDiv.data('value');
        //find sample code
        var rowsample = $(settings.sample);
        rowsample.css("display", "none");
        var sample = rowsample.html();


        var i = panelDiv.find(".each-section").size();
        //add html details to create a correct template
        var sectionDiv = $('<div />', { "class": 'each-section', 'id': 'section'+i });
        var image = $("<img />", { "src": settings.value,"class":"remove-image" });
        var link = $("<a />", { "text": settings.value,"class":"remove-link" });
        //remove event for remove selected form

        //create new form
        sectionDiv.html(sample);

        link.on('click', function (e) {

            e.preventDefault();
            var $this = $(this);
            $this.closest(".each-section").remove();
        });

        if (i > 0) {
            if (settings.type == 'image') {
                link.text('');
                link.append(image);

            }
            sectionDiv.append(link);
        }

        //add new created form on document
        panelDiv.append(sectionDiv);
       
    }
آرگومان داده شده، در واقع همان this هست که به این تابع ارسال شده است. در اولین گام تنظیمات ذخیره شده را که قبلا ذخیره کرده‌ایم، واکشی می‌کنیم. سپس تگ row-sample یا هر نامی را که به آن اختصاص داده شده است، می‌یابیم و محتوای آن را به شکل html در قالب string بیرون می‌کشیم. این کد html در واقع نمونه‌ای است که قرار است در سطر تکرار شود. البته تگ نمونه فقط برای نمونه به کار می‌رود و نیازی نیست روی صفحه نمایش داده شود؛ پس آن را مخفی می‌کنیم. از آنجا که ممکن است این سطری که ایجاد می‌شود، سطر اول نباشد و قبلا هم سطرهایی توسط همین متد ایجاد شده‌اند، بررسی می‌کنیم چند تگ با کلاس each-section داریم. اگر بیشتر از صفر باشد یعنی قبلا سطرهایی ایجاد شده است. در غیر اینصورت این اولین سطر ماست. اولین سطر توسط init صدا زده می‌شود و مابقی توسط متد add انجام می‌گیرد.
        settings.sample = panelDiv.data('sample');
        settings.type = panelDiv.data('type');
        settings.value = panelDiv.data('value');
        //find sample code
        var rowsample = $(settings.sample);
        rowsample.css("display", "none");
        var sample = rowsample.html();


        var i = panelDiv.find(".each-section").size();
در خطوط بعدی یک سری متغیر داریم که برای هر کدام یک قالب تگ div با کلاس‌های مختلف می‌سازیم. sectionDiv یک تگ  div  با کلاس each-section است که هر سطر را به طور کامل در خود قرار می‌دهد. link، جهت ساخت لینک حذف با کلاس remove-link به کار می‌رود. image هم یک تگ image می‌سازد تا اگر کاربر درخواست 'type:'image را داد، به جای لینک متنی حذف، از تصویر استفاده شود.
        //add html details to create a correct template
        var sectionDiv = $('<div />', { "class": 'each-section', 'id': 'section'+i });
        var image = $("<img />", { "src": settings.value,"class":"remove-image" });
        var link = $("<a />", { "text": settings.value,"class":"remove-link" });

در خط بعدی محتویات نمونه را داخل تگ sectiondiv قرار می‌دهیم:
//create new form
        sectionDiv.html(sample);

بعد از آن برای رویداد کلیک لینک حذف، کد زیر را وارد می‌کنیم:
   link.on('click', function (e) {

            e.preventDefault();
            var $this = $(this);
            $this.closest(".each-section").remove();
        });
متد closest در جی کوئری این وظیفه را دارد تا به سمت تگ‌های والد تگ this حرکت کند و با برخوردن با اولین تگ والد با کلاس each-section، آن تگ والد را بازگرداند و سپس متد remove را روی آن اجرا کند تا آن تگ به همراه تمام فرزندانش حذف شوند.

اولین شرط زیر بررسی می‌کند که آیا این سطری که ایجاد شده است سطر دوم به بعد است یا خیر؟ اگر آری پس باید دکمه‌ی حذف را به همراه داشته باشد. در صورتیکه سطر دوم به بعد باشد، وارد آن می‌شود. حالا بررسی می‌کند که کاربر برای دکمه‌ی حذف، درخواست لینک تصویری یا لینک متنی داده است و لینک مناسب را ساخته و آن را به انتهای sectionDiv اضافه می‌کند.
   if (i > 0) {
            if (settings.type == 'image') {
                link.text('');
                link.append(image);

            }
            sectionDiv.append(link);
        }

در انتها کل تگ sectionDiv را به تگ داده شده اضافه می‌کنیم تا به کاربر نمایش داده شود.
//add new created form on document
        panelDiv.append(sectionDiv);
نظرات مطالب
فعال سازی عملیات CRUD در Kendo UI Grid
مثال فوق را (KendoUI06) اگر برای ASP.NET MVC بازنویسی کنیم به کدهای ذیل خواهیم رسید:
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Mvc;
using Kendo.DynamicLinq;
using KendoUI06Mvc.Models;
using Newtonsoft.Json;

namespace KendoUI06Mvc.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View(); // shows the page.
        }

        [HttpDelete]
        public ActionResult DeleteProduct(int id)
        {
            var item = ProductDataSource.LatestProducts.FirstOrDefault(x => x.Id == id);
            if (item == null)
                return new HttpNotFoundResult();

            ProductDataSource.LatestProducts.Remove(item);

            return Json(item);
        }

        [HttpGet]
        public ActionResult GetProducts()
        {
            var request = JsonConvert.DeserializeObject<DataSourceRequest>(
               this.Request.Url.ParseQueryString().GetKey(0)
            );

            var list = ProductDataSource.LatestProducts;
            return Json(list.AsQueryable()
                       .ToDataSourceResult(request.Take, request.Skip, request.Sort, request.Filter),
                       JsonRequestBehavior.AllowGet);
        }

        [HttpPost]
        public ActionResult PostProduct(Product product)
        {
            if (!ModelState.IsValid)
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);

            var id = 1;
            var lastItem = ProductDataSource.LatestProducts.LastOrDefault();
            if (lastItem != null)
            {
                id = lastItem.Id + 1;
            }
            product.Id = id;
            ProductDataSource.LatestProducts.Add(product);

            // گرید آی دی جدید را به این صورت دریافت می‌کند
            return Json(new DataSourceResult { Data = new[] { product } });
        }

        [HttpPut] // Add it to fix this error: The requested resource does not support http method 'PUT'
        public ActionResult UpdateProduct(int id, Product product)
        {
            var item = ProductDataSource.LatestProducts
                                        .Select(
                                            (prod, index) =>
                                                new
                                                {
                                                    Item = prod,
                                                    Index = index
                                                })
                                        .FirstOrDefault(x => x.Item.Id == id);
            if (item == null)
                return new HttpNotFoundResult();


            if (!ModelState.IsValid || id != product.Id)
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);

            ProductDataSource.LatestProducts[item.Index] = product;

            //Return HttpStatusCode.OK
            return new HttpStatusCodeResult(HttpStatusCode.OK);
        }
    }
}
در این حالت View برنامه فقط جهت ذکر آدرس‌های جدید باید اصلاح شود و نیاز به تغییر دیگری ندارد:
        var productsDataSource = new kendo.data.DataSource({
            transport: {
                read: {
                    url: "@Url.Action("GetProducts","Home")",
                    dataType: "json",
                    contentType: 'application/json; charset=utf-8',
                    type: 'GET'
                },
                create: {
                    url: "@Url.Action("PostProduct","Home")",
                    contentType: 'application/json; charset=utf-8',
                    type: "POST"
                },
                update: {
                    url: function (product) {
                        return "@Url.Action("UpdateProduct","Home")/" + product.Id;
                        },
                        contentType: 'application/json; charset=utf-8',
                        type: "PUT"
                    },
                    destroy: {
                        url: function (product) {
                            return "@Url.Action("DeleteProduct","Home")/" + product.Id;
                        },
                        contentType: 'application/json; charset=utf-8',
                        type: "DELETE"
                    },
                    parameterMap: function (options) {
                        return kendo.stringify(options);
                    }
                },
مطالب
آشنایی با ساختار IIS قسمت سیزدهم
در مبحث قبلی گفتیم که ویرایش تنظیمات لاگ‌ها از طریق IIS یا ویرایش مستقیم فایل‌های کانفیگ میسر است. در این مقاله که قسمت پایانی مبحث لاگ هاست، در مورد ویرایش فایلهای کانفیگ صحبت می‌کنیم؛ همچنین استفاده از دستورات appcmd برای ویرایش و نهایتا کد نویسی در زبان سی شارپ و جاوااسکریپت.
تنظیمات لاگ سایت‌ها در فایل applicationhost در آدرس زیر قرار دارد:
C:\Windows\System32\inetsrv\config\applicationHost.config
برای هر تگ سایت، یک تگ <logfile> وجود دارد که ویژگی‌های Attributes آن، نوع ثبت لاگ را مشخص می‌کنند و می‌توانید مستقیما در اینجا به ویرایش بپردازید. البته ویرایش فایل کانفیگ از طریق IIS به طور مستقیم هم امکان پذیر است. برای این منظور در IIS سرور را انتخاب و از بین ماژول‌های قسمت management گزینه‌ی Configuration Editor را انتخاب کنید. در قسمت Section گزینه‌ی System.applicationhost را باز کرده و از زیر مجموعه‌های آن گزینه‌ی Site را برگزینید. در تنظیمات باز شده، گزینه collection را انتخاب کنید تا در انتهای سطر، دکمه‌ی ... پدیدار گردد. روی آن کلیک کنید تا محیطی ویرایشی باز گردد که به شما اجازه‌ی افزودن و ویرایش خصوصیت‌ها را می‌دهد. برای ویرایش لاگ‌ها باید خصوصیت logfile را باز کنید. اگر قسمت قبلی را مطالعه کرده باشید، باید بسیاری از این خصوصیت‌ها و مقادیر را بشناسید.
خصوصیات دیگری را هم مشاهده خواهید کرد که شاید قبلا ندیده‌اید که البته بستگی به ورژن IIS  شما دارد؛ مثلا خصوصیت‌های maxLogLineLength و flushByEntryCountW3Clog از IIS8.5 اضافه شده اند.

جدول خصوصیت ها
 خصوصیت توضیح 
 customLogPluginClsid   یک پارامتر رشته‌ای اختیاری که در آن، آی دی کلاس یا کلاس‌هایی نوشته می‌شود که برای custom logging نوشته شده‌اند و این گزینه ترتیب اجرای آن‌ها را تعیین می‌کند.
 directory   اختیاری است. محل ذخیره‌ی لاگ فایل‌ها را مشخص می‌کند و در صورتیکه ذکر نشود، همان مسیر پیش فرض است.
 enabled   اختیاری است. فعال بودن سیستم لاگ برای آن سایت را مشخص می‌کند. مقدار پیش فرض آن true است.
 flushByEntryCountW3CLog   این مقدار مشخص می‌کند چند رخداد باید اتفاق بیفتد تا عمل ذخیره سازی لاگ صورت گیرد. اگر بعد از هر رخداد عمل ثبت لاگ انجام شود، سرعت ثبت لاگ‌ها بالا می‌رود؛ ولی باعث استفاده‌ی مداوم از منابع و همچنین درخواست ثبت اطلاعات را روی دیسک خواهد داد و تاوان آن با زیاد شدن عملیات روی دیسک، پرداخته خواهد شد. ولی در حالتیکه چند رخداد را نگهداری  سپس دسته‌ای ثبت کند، باعث افزایش کارآیی و راندمان سرور خواهد شد. در صورتیکه سرور به مشکلات لحظه‌ای برخورد می‌کند مقدار آن را کاهش دهید. مقدار پیش فرض 0 است. یعنی اینکه ثبت، بعد از 64000 لاگ خواهد بود.
 localTimeRollover   نحوه‌ی نامگذاری فایل‌های لاگ را مشخص می‌کند که مقدار بولین گرفته و اختیاری است. به طور پیش فرض مقدار false دارد.
 logExtFileFlags   این گزینه در حالتی به کارتان می‌آید که فرمت W3C را برای ثبت لاگ‌ها انتخاب کرده باشید و در اینجا مشخص می‌کنید که چه فیلدهایی باید در لاگ باشند و اگر بیش از یکی بود میتوان با ، (کاما) از هم جدایشان کرد.
 logFormat  نوع فرمت ذخیره سازی لاگ‌ها
 logSiteId  اختیاری است و مقدار پیش فرض آن true است. بدین معنا که کد یا شماره‌ی سایت هم در لاگ خواهد بود و این در حالتی است که گزارش در سطح سرور باشد. در غیر این صورت اگر هر سایت، جداگانه لاگی برای خود داشته باشد، ذکر نمی‌گردد.
 logTargetW3C   اختیاری است و و مقدار file و *ETW را می‌گیرد که به طور پیش فرض روی File تنظیم است. در این حالت فایل لاگ‌ها در یک فایل متنی توسط http.sys ذخیره می‌شود. ولی موقعیکه از ETW استفاده می‌شود، http.sys با استفاده از iislogprovider داده‌ها را به سمت ETW ارسال میکند که منجر به اجرای سرویس Logsvc شده که از داده‌ها کوئری گرفته و آن‌ها را مستقیما از پروسه‌های کارگر جمع آوری و به سمت فایل لاگ ارسال می‌کند. همچنین انتخاب این دو گزینه نیز ممکن است.
 maxLogLineLength  حداکثر تعداد خطی که یک لاگ میتواند داشته باشد تا اینکه بتوانید در مصرف دیسک سخت صرفه جویی کنید و بیشتر کاربرد آن برای لاگ‌های کاستوم است. این عدد باید از نوع Uint باشد و اختیاری است و از 2 تا 65536 مقدار میپذیرد که مقدار پیش فرض آن 65536 می‌باشد.
 period   همان مبحث زمان بندی در مورد ایجاد فایل‌های لاگ است که در مقاله‌ی پیشین برسی کردیم و مقادیر Dialy,Hourly,monthlyو weekly را می‌پذیرد. همچنین maxsize هم هست؛ موقعی که لاگ به نهایت حجمی که برای آن تعیین کردیم میرسد.
 truncateSize   اختیاری است و مقدار آن از نوع int64 است. حداکثر حجم یک فایل لاگ را مشخص می‌کند تا اگر period روی maxsize تنظیم شده بود، حداکثر حجم را میتوان از اینجا تعیین نمود. در مقاله پیشین در این باره صحبت کردیم؛ حداقل عدد برای آن 1,048,576 است و اگر کمتر از آن بنویسید، سیستم همین عدد 1,048,576 را در نظر خواهد گرفت. مقدار پیش فرض آن 20971520 می باشد.
* ETW یا  Event Tracing Windows، سیستم و یا نرم افزاری برای عیب یابی و نظارت برای کامپوننت‌های ویندوزی است و یکی از استفاده کننده‌هایش IIS است  که از ویندوز 2000 به بعد اضافه شده‌است. برای قطع کردن این ماژول در IIS هم میتوانید  قسمت هفتم  را بررسی نمایید و دنیال ماژول TracingModule  بگردید. این ماژول به صورت Real time به ثبت رخدادهای IIS می‌پردازد.

به غیر از خصوصات بالا، خصوصیت customFields نیز از IIS 8.5 (به بعد) در دسترس است. اگر قصد دارید به غیر از فیلدهای W3c فیلدهای اختصاصی دیگری نیز داشته باشید، میتوان از این گزینه استفاده کرد. این فیلدهای کاستوم می‌توانند اطلاعاتشان را از request header ، response header و server variables دریافت کنند. این ویژگی تنها در فرمت W3C و در سطح سایت قابل انجام است. موقعی که یک فایل لاگ شامل فیلدهای اختصاصی شود، به انتها نام فایل X_ اضافه میگردد تا نشان دهد شامل یک فیلد اختصاصی یا کاستوم است. نحوه تعریف آن در فایل applicationhost به شکل زیر است:
<system.applicationHost>
   <sites>
      <siteDefaults>
         <logFile logFormat="W3C"
            directory="%SystemDrive%\inetpub\logs\LogFiles"
            enabled="true">
            <customFields>
               <clear/>
               <add logFieldName="ContosoField" sourceName="ContosoSource"
                  sourceType="ServerVariable" />
            </customFields>
         </logFile>
      </siteDefaults>
   </sites>
</system.applicationHost>

تغییر تنظمیات لاگ با Appcmd
appcmd.exe set config -section:system.applicationHost/sites /siteDefaults.logFile.enabled:"True" /commit:apphost
appcmd.exe set config -section:system.applicationHost/sites /siteDefaults.logFile.logFormat:"W3C" /commit:apphost
appcmd.exe set config -section:system.applicationHost/sites /siteDefaults.logFile.directory:"%SystemDrive%\inetpub\logs\LogFiles" /commit:apphost

تنظمیات تگ لاگ با برنامه نویسی و اسکریپت نویسی
هچنین با رفرنس Microsoft.web.administration در پروژه‌های دات نتی خود میتوانید امکان ویرایش تنظیمات را در برنامه‌های خود نیز داشته باشید:
using System;
using System.Text;
using Microsoft.Web.Administration;

internal static class Sample
{
   private static void Main()
   {
      using (ServerManager serverManager = new ServerManager())
      {
         Configuration config = serverManager.GetApplicationHostConfiguration();
         ConfigurationSection sitesSection = config.GetSection("system.applicationHost/sites");
         ConfigurationElement siteDefaultsElement = sitesSection.GetChildElement("siteDefaults");

         ConfigurationElement logFileElement = siteDefaultsElement.GetChildElement("logFile");
         logFileElement["logFormat"] = @"W3C";
         logFileElement["directory"] = @"%SystemDrive%\inetpub\logs\LogFiles";
         logFileElement["enabled"] = true;

         serverManager.CommitChanges();
      }
   }
}

با استفاده از اسکریپت نویسی توسط جاوااسکریپت و وی بی اسکریپت هم نیز این امکان مهیاست:
var adminManager = new ActiveXObject('Microsoft.ApplicationHost.WritableAdminManager');
adminManager.CommitPath = "MACHINE/WEBROOT/APPHOST";
var sitesSection = adminManager.GetAdminSection("system.applicationHost/sites", "MACHINE/WEBROOT/APPHOST");
var siteDefaultsElement = sitesSection.ChildElements.Item("siteDefaults");

var logFileElement = siteDefaultsElement.ChildElements.Item("logFile");
logFileElement.Properties.Item("logFormat").Value = "W3C";
logFileElement.Properties.Item("directory").Value = "%SystemDrive%\\inetpub\\logs\\LogFiles";
logFileElement.Properties.Item("enabled").Value = true;

adminManager.CommitChanges();

FTP Logging
برای اطمینان از نصب Ftp logging موقع نصب، باید از مورد زیر مطمئن باشید:

IIS را باز کنید و در لیست درختی، سرور را انتخاب کنید. در قسمت FTP میتوانید گزینه‌ی Ftp logging را ببینید. تنظیمات این قسمت هم دقیقا همانند قسمت logging میباشد و همان موارد برای آن هم صدق می‌کند.


بررسی تگ آن در applicationhost

تگ این نوع لاگ در فایل applicationhost در زیر مجموعه‌ی تگ <site> به شکل زیر نوشته می‌شود:

<system.ftpServer>
   <log centralLogFileMode="Central">
      <centralLogFile enabled="true" />
   </log>
</system.ftpServer>

گزینه centralLogFileMode  دو مقدار central و site را می‌پذیرد. اگر گزینه‌ی central انتخاب شود، یعنی همه‌ی لاگ‌ها را داخل یک فایل در سطح سرور ثبت کن ولی اگر گزینه‌ی site انتخاب شده باشد، لاگ هر سایت در یک فایل ثبت خواهد شد.

گزینه‌ی logInUTF8  یک خصوصیت اختیاری است که مقدار پیش فرض آن true میباشد. در این حالت باید تمامی رشته‌ها به انکدینگ UTF-8 تبدیل شوند.

همانطور که می‌بینید تگ log در بالا یک تگ فرزند هم به اسم centralLogFile دارد که همان خصوصیات جدول بالا در آن مهیاست.


دسترسی به تنظیمات این قسمت توسط دستور Appcmd: 

appcmd.exe set config -section:system.ftpServer/log /centralLogFileMode:"Central" /commit:apphost

appcmd.exe set config -section:system.ftpServer/log /centralLogFile.enabled:"True" /commit:apphost


دسترسی به تنظیمات این قسمت توسط دات نت:

using System;
using System.Text;
using Microsoft.Web.Administration;

internal static class Sample
{
   private static void Main()
   {
      using (ServerManager serverManager = new ServerManager())
      {
         Configuration config = serverManager.GetApplicationHostConfiguration();

         ConfigurationSection logSection = config.GetSection("system.ftpServer/log");
         logSection["centralLogFileMode"] = @"Central";

         ConfigurationElement centralLogFileElement = logSection.GetChildElement("centralLogFile");
         centralLogFileElement["enabled"] = true;

         serverManager.CommitChanges();
      }
   }
}


دسترسی به تنظیمات این قسمت توسط Javascript: 

var adminManager = new ActiveXObject('Microsoft.ApplicationHost.WritableAdminManager');
adminManager.CommitPath = "MACHINE/WEBROOT/APPHOST";

var logSection = adminManager.GetAdminSection("system.ftpServer/log", "MACHINE/WEBROOT/APPHOST");
logSection.Properties.Item("centralLogFileMode").Value = "Central";

var centralLogFileElement = logSection.ChildElements.Item("centralLogFile");
centralLogFileElement.Properties.Item("enabled").Value = true;

adminManager.CommitChanges();
نظرات مطالب
به دست آوردن اطلاعات کد اجراکننده یک متد
اضافه شدن ویژگی CallerArgumentExpression به C#10.0

ویژگی [CallerArgumentExpression] امکان دریافت آرگومان ارسالی به یک متد را به صورت رشته‌ای میسر می‌کند. برای مثال:
public static void LogExpression<T>(
    T value, 
    [CallerArgumentExpression("value")] string expression = null)
{
    Console.WriteLine($"{expression}: {value}");
}
با ورودی زیر:
var person = new Person("Vahid", "N.");
LogExpression(person.FirstName);
چنین خروجی را تولید می‌کند:
person.FirstName: Vahid

پارامتری که توسط ویژگی [CallerArgumentExpression] معرفی می‌شود، اختیاری بوده و به صورت خودکار توسط کامپایلر مقدار دهی می‌شود. یعنی کامپایلر فراخوانی فوق را به صورت زیر انجام می‌دهد:
LogExpression(person.FirstName, "person.FirstName");
وجود یک چنین قابلیتی، نویسندگان کتابخانه‌ها را از بکارگیری <<Expression<Func<T‌ها یا همان استاتیک ریفلکشن، رهایی می‌بخشد.

یک نمونه کاربرد دیگر آن در بررسی نال بودن مقدار پارامترهای ارسالی است که می‌توان آن‌ها را به صورت زیر خلاصه کرد:
public static void EnsureArgumentIsNotNull<T>(
   T value, 
   [CallerArgumentExpression("value")] string expression = null)
{
    if (value is null)
        throw new ArgumentNullException(expression);
}

public static void Foo(string name)
{
    EnsureArgumentIsNotNull(name); // if name is null, throws ArgumentNullException: "Value cannot be null. (Parameter 'name')"
    ...
}
پیشتر می‌بایستی با استفاده از nameof، نام پارامتر را مشخص کرد. در اینجا کامپایلر قادر است این مقدار را مشخص کند و دیگر نیازی به استفاده از روش زیر نیست:
    if (name is null)
    {
        throw new ArgumentNullException(nameof(name));
    }
البته NET 6.0. به همراه متد جدید زیر که از قابلیت فوق استفاده می‌کند، هست:
ArgumentNullException.ThrowIfNull(name);
و متد ThrowIfNull آن به صورت زیر تعریف شده‌است:
public static void ThrowIfNull(
    [NotNull] object? argument,
    [CallerArgumentExpression("argument")] string? paramName = null)

سؤال: چرا آرگومان اول این متد، هم nullable تعریف شده‌است و هم با ویژگی NotNull مزین گشته‌است؟
nullable بودن آن از این جهت است که ممکن است مقدار ارسالی به آن نال باشد. ویژگی NotNull آن به کامپایلر اعلام می‌کند که اگر این متد با موفقیت به پایان رسید، در سطرهای پس از آن، مقدار این شیء دیگر نال نیست و نیازی نیست تا به استفاده کنند اعلام کند که باید مراقب نال بودن آن باشد.
نظرات مطالب
طراحی گردش کاری با استفاده از State machines - قسمت دوم
- آیا چنین انتقال هایی به شکل زیر کار اصولی می‌باشد؟ 

به عنوان مثال اگر Trigger1 همان رویداد Save ما بوده که با توجه به Domain ای که در آن قرار داریم انجام آن در حالت‌های مشخص شده رویداد معتبری باشد، ماشین حالت بالا صحیح می‌باشد یا اینکه باید با یک رویداد دیگری به حالت State1 برگشته و دوباره انتقال به حالت State2 را انجام دهیم. البته متوجه هستم که به ازای تک تک رویدادها همیشه و نه لزوما باید دکمه متناظری را باتوجه به حالت فعلی شیء مورد، توسط کاربر قابل مشاهده باشد. با توجه به اینکه اگر کاربر نهایی به ازای تک تک رویدادهای موجود، لزوما دکمه‌های متناظری را در فرم مشاهده نکند و نیاز باشد یک دکمه Shortcut مانندی قرار بگیرد که احتمال دارد باعث چندین انتقال شود، Fire کردن چندین رویداد در پشت صحنه، کار اصولی می‌باشد؟

- نحوه استفاده از ماشین حالت ایجاد شده در لایه Application/ServiceLayer به چه شکل خواهد بود؟ حدس و سعی بنده به شکل زیر می‌باشد:
 public InvoiceStateMachine(InvoiceModel model)
        {
            _stateMachine =
                new StateMachine<InvoiceStatus, Trigger>(() => model.Status, state => model.Status = state);
سپس تعریف فیلدی به شکل زیر به منظور داشتن یک رویداد Parameterised، در دل ماشین حالت ایجاد شده:
public Func<InvoiceModel, Task> OnSaveAsync = null;
private readonly StateMachine<InvoiceStatus, Trigger>.TriggerWithParameters<InvoiceModel> _saveTrigger;

کانفیگ آن به شکل زیر:
 _stateMachine.Configure(InvoiceStatus.Pending)
                .OnEntryFromAsync(_saveTrigger, async invoice => await OnSaveAsync(invoice))
سپس در InvoiceService به شکل زیر عمل کرده:
 private void InitializeStateMachine(InvoiceModel model)
        {
            _stateMachine = new InvoiceStateMachine(model)
            {
                OnSaveAsync = async invoiceModel =>
                {
                    var result = model.IsNew() ? await CreateAsync(model) : await EditAsync(model);
                    if (!result.Succeeded)
                        throw new BusinessRuleValidationException(result.Message);
                }
            };
        }

و در نهایت متدی که از بیرون فراخوانی خواهد شد:
[Transactional]
public async Task<Result> SaveAsync(InvoiceModel model)
{
    try
    {
        InitializeStateMachine(model);
        await _stateMachine.TryFireSaveAsync(model);
        return Success();
    }
    catch (BusinessRuleValidationException e)
    {
        return Failed(e.Message);
    }
}

برای انتقال خطاهای ایجاد شده در زمان انتقال، راه حلی به غیر از صدور یک استثناء مشخص و گرفتن آن و ساخت خروجی مورد نظر متد سرویس به ذهنم نمی‌رسد.
مطالب
استفاده از ویجت آپلود KendoUI بصورت پاپ آپ
پیشنیاز:
- بررسی ویجت Kendo UI File Upload

در مطلب قبل جزئیات استفاده از ویجت آپلود فریمورک قدرتمند Kendo UI عنوان شدند. در این مطلب قصد داریم طریقه‌ی استفاده از آن را به صورت پاپ آپ، در ویجت گرید Kendo بررسی کنیم.
مدل زیر را در نظر بگیرید:
var product = {
    ProductId: 1001,
    ProductName: "Product 1001",
    Available: true,
    Filename: "Image02.png"
};
و برای ایجاد یک دیتاسورس سمت کاربر برای گرید، یک منبع داده را با استفاده از مدل بالا تولید میکنیم:
        var productCount = 100;
        var products = [];

        function datasourceFilling() {
            for (var i = 0; i < productCount; i++) {
                var product = {
                    ProductId: i,
                    ProductName: "Product " + i + " Name",
                    Available: i % 2 == 0 ? true: false,
                    Filename: i % 2 == 0 ? "Image01.png" : "Image02.png"
                };
                products.push(product);
            }
        }
سپس برای نمایش در گرید، سیم پیچی‌های لازم را انجام میدهیم:
        function makekendoGrid() {

            $("#grid").kendoGrid({
                dataSource: {
                    data: products,
                    schema: {
                        model:
                        {
                            //id: "ProductId",
                            fields:
                            {
                                ProductId: { editable: false, nullable: true },
                                ProductName: { validation: { required: true } },
                                Available: { type: "boolean" },
                                ImageName: { type: "string", editable: false },
                                Filename: { type: "string", validation: { required: true } }
                            }
                        }
                    },
                    pageSize: 20
                },
                height: 550,
                scrollable: true,
                sortable: true,
                filterable: true,
                pageable: {
                    input: true,
                    numeric: false
                },
                editable: {
                    mode: "popup",
                },
                columns: [
                    { field: "ProductName", title: "Product Name" },
                    { field: "Available", width: "100px", template: '<input type="checkbox" #= Available ? checked="checked" : "" # disabled="disabled" ></input>' },
                    { field: "ImageName", width: "150px", template: "<img src='/img/#=Filename#' alt='#=Filename #' Title='#=Filename #' height='80' width='80'/>" },
                    { field: "Filename", width: "100px", editor: fileEditor },
                    { command: ["edit"], title: "&nbsp;", width: "200px" }
                ]
            });

            var grid = $('#grid').data('kendoGrid');
            grid.hideColumn(3);
        }
همانطور که میبینید در ستون‌های گرید، یک ستون ImageName نیز اضافه شده‌است که به‌صورت تمپلیت تصویر محصول را نمایش میدهد. برای خود فیلد نیز از یک تمپلیت ادیتور که در ادامه تعریف خواهیم کرد استفاده کرده‌ایم:
        function fileEditor(container, options) {

            $('<input type="file" name="file"/>')
                .appendTo(container)
                .kendoUpload({
                    multiple: false,
                    async: {
                        saveUrl: "@Url.Action("Save", "Home")",
                        removeUrl: "@Url.Action("Remove", "Home")",
                        autoUpload: true,
                    },
                    upload: function (e) {
                        alert("upload");
                        e.data = { Id: options.model.Id };
                    },
                    success: function (e) {
                        alert("success");
                        options.model.set("ImageName", e.response.ImageUrl);
                    },
                    error: function (e) {
                        alert("error");
                        alert("Failed to upload " + e.files.length + " files " + e.XMLHttpRequest.status + " " + e.XMLHttpRequest.responseText);
                    }
                });
        }
برای آپلود فایل و حذف آن نیز اکشن‌های لازم را در سمت سرور مینویسم:
        [System.Web.Mvc.HttpPost]
        public virtual ActionResult Save(HttpPostedFileBase file)
        {
            var exName = Path.GetExtension(file.FileName);
            var totalFileName = System.Guid.NewGuid().ToString().ToLower().Replace("-", "") + exName;
            var physicalPath = Path.Combine(Server.MapPath("/img"), totalFileName);
            file.SaveAs(physicalPath);
            return Json(new { ImageUrl = totalFileName }, "text/plain");
        }

        [System.Web.Mvc.HttpPost]
        public virtual ContentResult Remove(string fileName)
        {
            if (fileName != null)
            {
                var physicalPath = Path.Combine(Server.MapPath("/img"), fileName);
                System.IO.File.Delete(physicalPath);
            }

            // Return an empty string to signify success
            return Content("");
        }

حاصل کار بصورت تصویر زیر نمایش داده شده است:

اشتراک‌ها
مسیر راه Entity Framework Core 2.1

New features
- Lazy loading
- Parameters in entity constructors
- Value conversions
- LINQ GroupBy translation
- Data Seeding
- Query types
- Include for derived types
- System.Transactions support 

مسیر راه Entity Framework Core 2.1
نظرات مطالب
معرفی پروژه فروشگاهی Iris Store
 تخفیف در بخش کالاهای مشابه  نمایش داده نمی‌شود. زیرا در ایندکس لوسین لیست تخفیف‌ها وارد نشده است. لیست تخفیف‌ها به ویو  ارسال می‌شود و در آنجا بررسی می‌شود که تخفیف فعال وجود دارد یا خیر! 

راه حل بنده
کنترلر:
            // در این قسمت discounts
            // در کالاهای مشابه اضافه نشده است
            var serchResult = LuceneIndex.GetMoreLikeThisProjectItems(id)
                    .Where(item => item.Category == "کالا‌ها").Skip(1).Take(8).ToList();

            List<ProductWidgetViewModel> productToDel = new List<ProductWidgetViewModel>();
            if (serchResult.Count > 0)
            {
                foreach (var product in serchResult)
                {
                    if (product.Discount > 0)
                    {
                        var resul = await _productService.GetProductDiscount(product.Id);
                        // محصول در دیتابیس وجود نداشته
                        if (resul == null)
                        {
                            productToDel.Add(product);
                            //error
                            //serchResult.Remove(product);
                        }
                        else if (resul.Discount != 0)
                        {
                            product.Discounts = new List<ProductPageDiscountWidgetViewModel>();
                            product.Discounts.Add(resul);
                        }
                    }
                }
            }

            foreach (var product in productToDel)
            {
                serchResult.Remove(product);
            }
            ViewData["SimilarProducts"] = serchResult;

سرویس:
 public async Task<ProductPageDiscountWidgetViewModel> GetProductDiscount(int productId)
        {
            var product = await _products.FirstOrDefaultAsync(p => p.Id == productId);
            //by SYA
            // در ایندکس هست اما در دیتابیس نیست product == null

            if (product == null)
            {
                return null;
            }
            else if (product.Discounts == null)
            {
                return new ProductPageDiscountWidgetViewModel { Discount = 0 };
            }
            //_mappingEngine.Map<ProductDiscount, ProductPageDiscountWidgetViewModel>(
            //    product.Discounts.OrderByDescending(p => p.EndDate).FirstOrDefault());
            foreach (var dic in product.Discounts.OrderByDescending(p => p.StartDate).ToList())
            {
                if (dic.Discount > 0 && dic.EndDate >= DateTimeExtentionService.NowIranZone())
                {
                    return _mappingEngine.Map<ProductDiscount, ProductPageDiscountWidgetViewModel>(dic);
                }
            }
            return new ProductPageDiscountWidgetViewModel { Discount = 0 };
        }

در موقع نوشتن کد حالتی را در نظر گرفتم که کالا در ایندکس لوسین وجود داشته باشد اما در دیتابیس نه!


نظرات مطالب
بررسی تغییرات Blazor 8x - قسمت چهارم - معرفی فرم‌های جدید تعاملی

یک نکته‌ی تکمیلی: روش طراحی binding دو طرفه در Blazor SSR

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

[Parameter] public T? Value { set; get; }

[Parameter] public EventCallback<T?> ValueChanged { get; set; }

[Parameter] public Expression<Func<T?>> ValueExpression { get; set; } = default!;

اگر این خواص را به کامپوننت‌های توکار خود Blazor متصل کنیم (مانند InputBox آن و مابقی آن‌ها)، نیازی به کدنویسی بیشتری ندارند و کار می‌کنند. اما اگر قرار است از یک input ساده‌ی Html ای استفاده کنیم، نیاز است ValueChanged آن‌را اینبار در متد OnInitialized فراخوانی کنیم؛ چون در زمان post-back به سرور است که مقدار آن در اختیار مصرف کننده‌ی کامپوننت قرار می‌گیرد. این مورد، مهم‌ترین تفاوت نحوه‌ی طراحی binding دوطرفه در Blazor SSR با مابقی حالات و نگارش‌های Blazor است.

بررسی وقوع post-back به سرور به دو روش زیر میسر است:

الف) بررسی کنیم که آیا HttpPost ای رخ‌داده‌است؟ سپس در همین لحظه، متد ValueChanged.InvokeAsync را فراخوانی کنیم:

[CascadingParameter] internal HttpContext HttpContext { set; get; } = null!;

protected override void OnInitialized()
{
   base.OnInitialized();

   if (HttpContext.IsPostRequest())
   {
      SetValueCallback(Value);
   }
}

private void SetValueCallback(string value)
{
   if (!ValueChanged.HasDelegate)
   {
      return;
   }

   _ = ValueChanged.InvokeAsync(value);
}

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

ب) می‌توان در قسمت OnInitialized، بررسی کرد که آیا درخواست جاری به همراه اطلاعات یک فرم ارسال شده‌ی به سمت سرور است یا خیر؟ روش کار به صورت زیر است:

protected override void OnInitialized()
{
   base.OnInitialized();

   if (HttpContext.Request.HasFormContentType &&
       HttpContext.Request.Form.TryGetValue(ValueField.HtmlFieldName, out var data))
   {
      SetValueCallback(data.ToString());
   }
}

در اینجا از ValueField.HtmlFieldName که در نکته‌ی قبلی معرفی BlazorHtmlField به آن اشاره شد، جهت یافتن نام واقعی فیلد ورودی، استفاده شده‌است.