مطالب
بالا بردن سرعت بارگذاری اولیه EF Code first با تعداد مدل‌های زیاد
EF Code first هربار در حین آغاز اجرای برنامه و اولین کوئری که به بانک اطلاعاتی ارسال می‌کند، کار تشخیص روابط بین کلاس‌ها و همچنین نگاشت آن‌ها را به بانک اطلاعاتی، انجام می‌دهد. این مورد شاید با تعداد کم کلاس‌ها آنچنان به نظر نرسد، اما اگر تعداد کلاس‌های شما به بالای 200 عدد رسید، زمان آغاز برنامه آزار دهنده خواهد شد. راه حلی برای این مساله وجود دارد به نام ایجاد Viewهای متناظر با نگاشت‌ها و سپس کامپایل آن به عنوان جزئی از برنامه، که در ادامه نحوه انجام این‌کار را مرور خواهیم کرد.

بررسی ساختار pre-generated views

برای کامپایل نگاشت‌های EF در خود برنامه (بجای تولید پویای هربار آن‌ها)، ابتدا باید فایل edmx متناظر با مدل‌ها و روابط بین آن‌ها تشکیل شود:
    var ms = new MemoryStream();
    using (var writer = XmlWriter.Create(ms))
    {
        EdmxWriter.WriteEdmx(new Context(), writer);
    }
پس از اینکه edmx تشکیل شد، باید از ساختار فشرده آن سه جزء زیر را استخراج کرد:
الف) ssdl : storageModels
ب) csdl : conceptualModels
ج) msl : mappings

اینکار را به صورت زیر می‌توان انجام داد:
    var xDoc = XDocument.Load(ms);

    var ssdl = xDoc.Descendants("{http://schemas.microsoft.com/ado/2009/02/edm/ssdl}Schema").Single();
    var csdl = xDoc.Descendants("{http://schemas.microsoft.com/ado/2008/09/edm}Schema").Single();
    var msl = xDoc.Descendants("{http://schemas.microsoft.com/ado/2008/09/mapping/cs}Mapping").Single();
پس از آن باید محتوای این سه جزء را توسط متد Save هر کدام، در فایل‌های xml ایی ذخیره کرد و توسط ابزاری به نام EdmGen.exe که جزئی از  ویژوال استودیو است، فایل Context.Views.cs را تولید، به برنامه اضافه و سپس کامپایل کرد:
 EdmGen.exe /mode:ViewGeneration /incsdl:Context.csdl  /inmsl:Context.msl /inssdl:Context.ssdl /outviews:Context.Views.cs
بهتر است این پروسه هر بار که قرار است ارائه نهایی برنامه صورت گیرد، انجام شود.

علاوه بر این‌ها اگر علاقمند باشید که کار فایل EdmGen را شبیه سازی کنید، کلاس زیر این‌کار را انجام داده و قادر است خروجی vb یا cs متناظری را نیز تولید کند:
using System;
using System.Data.Entity;
using System.Data.Entity.Design;
using System.Data.Entity.Infrastructure;
using System.Data.Mapping;
using System.Data.Metadata.Edm;
using System.IO;
using System.Linq;
using System.Xml;
using System.Xml.Linq;

namespace EfUtils
{
    public static class PreGeneratedViewsWriter
    {
        public static void CreatePreGeneratedViewsFile(
                this DbContext contextInstance,
                LanguageOption language = LanguageOption.GenerateCSharpCode,
                string viewsFile = "Context.Views.cs",
                string edmxFile = "context.edmx",
                string ssdlFile = "context.ssdl.xml",
                string csdlFile = "context.csdl.xml",
                string mslFile = "context.msl.xml")
        {
            using (var contextViewsMemoryStream = new MemoryStream())
            {
                using (var edmxMemoryStream = new MemoryStream())
                {
                    var edmx = createEdmx(contextInstance, edmxFile, edmxMemoryStream);
                    var mappingItemCollection = createMappingItemCollection(ssdlFile, csdlFile, mslFile, edmx);
                    generateViews(language, viewsFile, contextViewsMemoryStream, mappingItemCollection);
                }
            }
        }

        private static void generateViews(LanguageOption language, string viewsFile, MemoryStream contextViewsMemoryStream, StorageMappingItemCollection mappingItemCollection)
        {
            var viewGenerator = new EntityViewGenerator // It's defined in System.Data.Entity.Design.dll
            {
                LanguageOption = language
            };
            using (var streamWriter = new StreamWriter(contextViewsMemoryStream))
            {
                var errors = viewGenerator.GenerateViews(mappingItemCollection, streamWriter).ToList();

                if (errors.Any())
                    throw new InvalidOperationException(errors.First().Message);

                contextViewsMemoryStream.Position = 0;
                using (var reader = new StreamReader(contextViewsMemoryStream))
                {
                    var codeData = reader.ReadToEnd();
                    File.WriteAllText(viewsFile, codeData);
                }
            }
        }

        private static StorageMappingItemCollection createMappingItemCollection(string ssdlFile, string csdlFile, string mslFile, XDocument edmx)
        {
            var ssdl = edmx.Descendants("{http://schemas.microsoft.com/ado/2009/02/edm/ssdl}Schema").Single();
            ssdl.Save(ssdlFile);
            var storeItemCollection = new StoreItemCollection(new[] { ssdl.CreateReader() });

            var csdl = edmx.Descendants("{http://schemas.microsoft.com/ado/2008/09/edm}Schema").Single();
            csdl.Save(csdlFile);
            var edmItemCollection = new EdmItemCollection(new[] { csdl.CreateReader() });

            var msl = edmx.Descendants("{http://schemas.microsoft.com/ado/2008/09/mapping/cs}Mapping").Single();
            msl.Save(mslFile);

            var mappingItemCollection = new StorageMappingItemCollection(edmItemCollection, storeItemCollection, new[] { msl.CreateReader() });
            return mappingItemCollection;
        }

        private static XDocument createEdmx(DbContext contextInstance, string edmxFile, MemoryStream edmxMemoryStream)
        {
            var settings = new XmlWriterSettings { Indent = true };
            using (var writer = XmlWriter.Create(edmxMemoryStream, settings))
            {
                EdmxWriter.WriteEdmx(contextInstance, writer);
            }
            File.WriteAllBytes(edmxFile, edmxMemoryStream.ToArray());

            edmxMemoryStream.Position = 0;
            var edmx = XDocument.Load(edmxMemoryStream);
            return edmx;
        }
    }
}
در اینجا همان مراحلی که عنوان شد، تکرار می‌شود. فایل edmx متناظر با وهله‌ای از DbContext برنامه، تولید شده و سه جزء آن استخراج می‌شوند. سپس این موارد به EntityViewGenerator موجود در اسمبلی System.Data.Entity.Design.dll ارسال شده و کد نهایی متناظر قابل کامپایل در برنامه تولید می‌گردد.
پس از تولید فایل Context.Views.cs یا Context.Views.vb، آن‌را به پروژه اضافه کنید.
اینبار نحوه استفاده از آن باید به صورت زیر باشد:
 Database.SetInitializer<MyContext>(null);
 از این جهت که تمام اطلاعات لازم جهت آغاز کار، در فایل تولیدی Context.Views وجود دارد و اکنون جزئی از فایل اجرایی برنامه است و نیازی به تکرار ساخت مجدد پویای آن نیست.

مرجع:
Entity Framework Code First View Generation Templates On Visual Studio Code Gallery

مطالب
استفاده از Full text search توسط Entity Framework
پیشنیاز مطلب:
پشتیبانی از Full Text Search در SQL Server

Full Text Search یا به اختصار FTS یکی از قابلیت‌های SQL Server جهت جستجوی پیشرفته در متون میباشد. این قابلیت تا کنون در EF 6.1.1 ایجاد نشده است.
در ادامه پیاده سازی از FTS در EF را مشاهده مینمایید.
جهت ایجاد قابلیت FTS از متد Contains در Linq استفاده شده است.
ابتدا متد‌های الحاقی جهت اعمال دستورات FREETEXT و CONTAINS اضافه میشود.سپس دستورات تولیدی EF را قبل از اجرا بر روی بانک اطلاعاتی توسط امکان Command Interception به دستورات FTS تغییر میدهیم.
همانطور که میدانید دستور Contains در Linq توسط EF به دستور LIKE تبدیل میشود. به جهت اینکه ممکن است بخواهیم از دستور LIKE نیز استفاده کنیم یک پیشوند به مقادیری که میخواهیم به دستورات FTS تبدیل شوند اضافه مینماییم.
جهت استفاده از Fts در EF کلاس‌های زیر را ایجاد نمایید.
 
کلاس FullTextPrefixes :
/// <summary>
    /// 
    /// </summary>
    public static class FullTextPrefixes
    {
        /// <summary>
        /// 
        /// </summary>
        public const string ContainsPrefix = "-CONTAINS-";

        /// <summary>
        /// 
        /// </summary>
        public const string FreetextPrefix = "-FREETEXT-";

        /// <summary>
        /// 
        /// </summary>
        /// <param name="searchTerm"></param>
        /// <returns></returns>
        public static string Contains(string searchTerm)
        {
            return string.Format("({0}{1})", ContainsPrefix, searchTerm);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="searchTerm"></param>
        /// <returns></returns>
        public static string Freetext(string searchTerm)
        {
            return string.Format("({0}{1})", FreetextPrefix, searchTerm);
        }

    }

کلاس جاری جهت علامت گذاری دستورات FTS میباشد و توسط متد‌های الحاقی و کلاس FtsInterceptor استفاده میگردد.

کلاس FullTextSearchExtensions :
public static class FullTextSearchExtensions
    {

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <param name="source"></param>
        /// <param name="expression"></param>
        /// <param name="searchTerm"></param>
        /// <returns></returns>
        public static IQueryable<TEntity> FreeTextSearch<TEntity>(this IQueryable<TEntity> source, Expression<Func<TEntity, object>> expression, string searchTerm) where TEntity : class
        {
            return FreeTextSearchImp(source, expression, FullTextPrefixes.Freetext(searchTerm));
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <param name="source"></param>
        /// <param name="expression"></param>
        /// <param name="searchTerm"></param>
        /// <returns></returns>
        public static IQueryable<TEntity> ContainsSearch<TEntity>(this IQueryable<TEntity> source, Expression<Func<TEntity, object>> expression, string searchTerm) where TEntity : class
        {
            return FreeTextSearchImp(source, expression, FullTextPrefixes.Contains(searchTerm));
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <param name="source"></param>
        /// <param name="expression"></param>
        /// <param name="searchTerm"></param>
        /// <returns></returns>
        private static IQueryable<TEntity> FreeTextSearchImp<TEntity>(this IQueryable<TEntity> source, Expression<Func<TEntity, object>> expression, string searchTerm)
        {
            if (String.IsNullOrEmpty(searchTerm))
            {
                return source;
            }

            // The below represents the following lamda:
            // source.Where(x => x.[property].Contains(searchTerm))

            //Create expression to represent x.[property].Contains(searchTerm)
            //var searchTermExpression = Expression.Constant(searchTerm);
            var searchTermExpression = Expression.Property(Expression.Constant(new { Value = searchTerm }), "Value");
            var checkContainsExpression = Expression.Call(expression.Body, typeof(string).GetMethod("Contains"), searchTermExpression);

            //Join not null and contains expressions

            var methodCallExpression = Expression.Call(typeof(Queryable),
                                                       "Where",
                                                       new[] { source.ElementType },
                                                       source.Expression,
                                                       Expression.Lambda<Func<TEntity, bool>>(checkContainsExpression, expression.Parameters));

            return source.Provider.CreateQuery<TEntity>(methodCallExpression);
        }

در این کلاس متدهای الحاقی جهت اعمال قابلیت Fts ایجاد شده است.
متد FreeTextSearch جهت استفاده از دستور FREETEXT  استفاده میشود.در این متد پیشوند -FREETEXT- جهت علامت گذاری این دستور به ابتدای مقدار جستجو اضافه میشود. این متد دارای دو پارامتر میباشد ، اولی ستونی که میخواهیم بر روی آن جستجوی FTS انجام دهیم و دومی عبارت جستجو.
متد ContainsSearch جهت استفاده از دستور CONTAINS استفاده میشود.در این متد پیشوند -CONTAINS- جهت علامت گذاری این دستور به ابتدای مقدار جستجو اضافه میشود. این متد دارای دو پارامتر میباشد ، اولی ستونی که میخواهیم بر روی آن جستجوی FTS انجام دهیم و دومی عبارت جستجو. 
متد FreeTextSearchImp جهت ایجاد دستور Contains در Linq میباشد.
 
کلاس FtsInterceptor :
 public class FtsInterceptor : IDbCommandInterceptor
    {

        /// <summary>
        /// 
        /// </summary>
        /// <param name="command"></param>
        /// <param name="interceptionContext"></param>
        public void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
        {
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="command"></param>
        /// <param name="interceptionContext"></param>
        public void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
        {
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="command"></param>
        /// <param name="interceptionContext"></param>
        public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {
            RewriteFullTextQuery(command);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="command"></param>
        /// <param name="interceptionContext"></param>
        public void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="command"></param>
        /// <param name="interceptionContext"></param>
        public void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
        {
            RewriteFullTextQuery(command);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="command"></param>
        /// <param name="interceptionContext"></param>
        public void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
        {
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="cmd"></param>
        public static void RewriteFullTextQuery(DbCommand cmd)
        {
            var text = cmd.CommandText;
            for (var i = 0; i < cmd.Parameters.Count; i++)
            {
                var parameter = cmd.Parameters[i];
                if (
                    !parameter.DbType.In(DbType.String, DbType.AnsiString, DbType.StringFixedLength,
                        DbType.AnsiStringFixedLength)) continue;
                if (parameter.Value == DBNull.Value)
                    continue;
                var value = (string)parameter.Value;
                if (value.IndexOf(FullTextPrefixes.ContainsPrefix, StringComparison.Ordinal) >= 0)
                {
                    parameter.Size = 4096;
                    parameter.DbType = DbType.AnsiStringFixedLength;
                    value = value.Replace(FullTextPrefixes.ContainsPrefix, ""); // remove prefix we added n linq query
                    value = value.Substring(1, value.Length - 2); // remove %% escaping by linq translator from string.Contains to sql LIKE
                    parameter.Value = value;
                    cmd.CommandText = Regex.Replace(text,
                        string.Format(
                            @"\[(\w*)\].\[(\w*)\]\s*LIKE\s*@{0}\s?(?:ESCAPE N?'~')", parameter.ParameterName),
                        string.Format(@"CONTAINS([$1].[$2], @{0})", parameter.ParameterName));
                    if (text == cmd.CommandText)
                        throw new Exception("FTS was not replaced on: " + text);
                    text = cmd.CommandText;
                }
                else if (value.IndexOf(FullTextPrefixes.FreetextPrefix, StringComparison.Ordinal) >= 0)
                {
                    parameter.Size = 4096;
                    parameter.DbType = DbType.AnsiStringFixedLength;
                    value = value.Replace(FullTextPrefixes.FreetextPrefix, ""); // remove prefix we added n linq query
                    value = value.Substring(1, value.Length - 2); // remove %% escaping by linq translator from string.Contains to sql LIKE
                    parameter.Value = value;
                    cmd.CommandText = Regex.Replace(text,
                        string.Format(
                            @"\[(\w*)\].\[(\w*)\]\s*LIKE\s*@{0}\s?(?:ESCAPE N?'~')", parameter.ParameterName),
                        string.Format(@"FREETEXT([$1].[$2], @{0})", parameter.ParameterName));
                    if (text == cmd.CommandText)
                        throw new Exception("FTS was not replaced on: " + text);
                    text = cmd.CommandText;
                }
            }
        }

    }

در این کلاس دستوراتی را که توسط متد‌های الحاقی جهت امکان Fts علامت گذاری شده بودند را یافته و دستور LIKE را با دستورات  CONTAINSو FREETEXT جایگزین میکنیم.

در ادامه برنامه ای جهت استفاده از این امکان به همراه دستورات تولیدی آنرا مشاهده مینمایید.
public class Note
    {
        /// <summary>
        /// 
        /// </summary>
        public int Id { get; set; }

        /// <summary>
        /// 
        /// </summary>
        public string NoteText { get; set; }
    }

  public class FtsSampleContext : DbContext
    {
        /// <summary>
        /// 
        /// </summary>
        public DbSet<Note> Notes { get; set; }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="modelBuilder"></param>
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Configurations.Add(new NoteMap());
        }
    }

/// <summary>
    /// 
    /// </summary>
    class Program
    {
        /// <summary>
        /// 
        /// </summary>
        /// <param name="args"></param>
        static void Main(string[] args)
        {
            DbInterception.Add(new FtsInterceptor());
            const string searchTerm = "john";
            using (var db = new FtsSampleContext())
            {
                var result1 = db.Notes.FreeTextSearch(a => a.NoteText, searchTerm).ToList();
                //SQL Server Profiler result ===>>>
                //exec sp_executesql N'SELECT 
                //    [Extent1].[Id] AS [Id], 
                //    [Extent1].[NoteText] AS [NoteText]
                //    FROM [dbo].[Notes] AS [Extent1]
                //    WHERE FREETEXT([Extent1].[NoteText], @p__linq__0)',N'@p__linq__0 
                //char(4096)',@p__linq__0='(john)'
                var result2 = db.Notes.ContainsSearch(a => a.NoteText, searchTerm).ToList();
                //SQL Server Profiler result ===>>>
                //exec sp_executesql N'SELECT 
                //    [Extent1].[Id] AS [Id], 
                //    [Extent1].[NoteText] AS [NoteText]
                //    FROM [dbo].[Notes] AS [Extent1]
                //    WHERE CONTAINS([Extent1].[NoteText], @p__linq__0)',N'@p__linq__0 
                //char(4096)',@p__linq__0='(john)'
            }
            Console.ReadKey();
        }
    }

ابتدا کلاس FtsInterceptor را به EF معرفی مینماییم. سپس از دو متد الحاقی مذکور استفاده مینماییم. خروجی هر دو متد توسط SQL Server Profiler در زیر هر متد مشاهده مینمایید.
تمامی کدها و مثال مربوطه در آدرس https://effts.codeplex.com قرار گرفته است.
منبع:
Full Text Search in Entity Framework 6
مطالب
استفاده از افزونه Typeahead مجموعه Twitter Bootstrap در ASP.NET MVC
با تعدادی از کامپوننت‌های Bootstrap در مطلب «نگاهی به اجزای تعاملی Twitter Bootstrap» آشنا شدید. یکی دیگر از این افزونه‌ها، Typeahead نام دارد که در حقیقت نوعی Autocomplete text box است. در ادامه قصد داریم نحوه استفاده از آن‌را در ASP.NET MVC بررسی کنیم.

استفاده‌ی استاتیک از افزونه Typeahead

منظور از استفاده‌ی استاتیک، مشخص بودن آرایه عناصر و هچنین درج آن به صورت html encoded در صفحه است. برای این منظور، کنترلر برنامه چنین شکلی را خواهد داشت:
using System.Web.Mvc;
using System.Web.Script.Serialization;

namespace Mvc4TwitterBootStrapTest.Controllers
{
    public class HomeController : Controller
    {
        [HttpGet]
        public ActionResult Index()
        {
            var array = new[]
                {
                     "Afghanistan",
                     "Albania",
                     "Algeria",
                     "American Samoa",
                     "Andorra",
                     "Angola",
                     "Anguilla",
                     "Antarctica",
                     "Antigua and/or Barbuda"
                };
            ViewBag.JsonString = new JavaScriptSerializer().Serialize(array);
            return View();
        }
    }
}
در اینجا یک آرایه با تعداد عناصر مشخص، تبدیل به رشته JSON معادل آن شده و توسط ViewBag.JsonString به View ارسال می‌شود.
View متناظر با آن به نحو ذیل با مشخص سازی نوع data-provide (تا به کتابخانه‌ی جاوا اسکریپتی همراه bootstrap اعلام کند از چه افزونه‌ای در اینجا قرار است استفاده شود)، منبع داده data-source و حداکثر تعداد آیتم ظاهر شونده data-items، می‌تواند طراحی شود:
@{
    ViewBag.Title = "Index";    
}
<h2>
    Typeahead</h2>
@Html.TextBox("search", null, htmlAttributes:
                              new
                              {
                                  autocomplete = "off",
                                  data_provide = "typeahead",
                                  data_items = 8,
                                  data_source = @ViewBag.JsonString
                              })

به این ترتیب، یک چنین خروجی در صفحه درج می‌شود:
<input autocomplete="off" data-items="8" data-provide="typeahead" 
data-source="[&quot;Afghanistan&quot;,&quot;Albania&quot;,&quot;Algeria&quot;,&quot;American Samoa&quot;,&quot;Andorra&quot;,&quot;Angola&quot;,&quot;Anguilla&quot;,&quot;Antarctica&quot;,&quot;Antigua and/or Barbuda&quot;]" 
id="search" name="search" type="text" value="" />
همانطور که ملاحظه می‌کنید دقیقا data-source تهیه شده مطابق نیاز خاص این افزونه، html encoded است. به علاوه هر جایی در htmlAttributes صفحه از under line استفاده شده، در این سمت به صورت خودکار به - ترجمه گردیده است.
اگر هم بخواهیم برای آن یک Html Helper درست کنیم، می‌توان به نحو ذیل عمل کرد:
        public static MvcHtmlString TypeaheadFor<TModel, TValue>(
                this HtmlHelper<TModel> htmlHelper,
                Expression<Func<TModel, TValue>> expression,
                IEnumerable<string> source,
                int items = 8)
        {
            var jsonString = new JavaScriptSerializer().Serialize(source);
            return htmlHelper.TextBoxFor(
                expression,
                new
                {
                    autocomplete = "off",
                    data_provide = "typeahead",
                    data_items = items,
                    data_source = jsonString
                }
            );
        }


استفاده پویا و Ajax ایی از افزونه Typeahead

اگر بخواهیم data-source را به صورت پویا، هربار از بانک اطلاعاتی دریافت و ارائه دهیم، نیاز به کمی اسکریپت نویسی خواهد بود:
using System;
using System.Linq;
using System.Web.Mvc;
using System.Web.Script.Serialization;

namespace Mvc4TwitterBootStrapTest.Controllers
{
    public class HomeController : Controller
    {
        [HttpGet]
        public JsonResult GetNames(string term)
        {
            var array = new[]
                {
                     "Afghanistan",
                     "Albania",
                     "Algeria",
                     "American Samoa",
                     "Andorra",
                     "Angola",
                     "Anguilla",
                     "Antarctica",
                     "Antigua and/or Barbuda"
                };

            var results = array.Where(n =>
                n.StartsWith(term, StringComparison.OrdinalIgnoreCase));

            return Json(results.ToArray(), JsonRequestBehavior.AllowGet);
        }
    }
}
در این حالت، کدهای اکشن متدی که یک عبارت، یا قسمتی از آن را از طریق پارامتر term دریافت و خروجی JSON مناسبی را ارائه می‌کند، همانند متد GetNames فوق خواهد بود.
سپس در تعاریف View، قسمت data-source مرتبط با TextBox حذف و از طریق فراخوانی مستقیم کدهای افزونه typeahead مقدار دهی می‌گردد:
@{
    ViewBag.Title = "Index";
    var url = Url.Action("GetNames", "Home");
}
<h2>
    Typeahead</h2>
@Html.TextBox("search", null, htmlAttributes:
                              new
                              {
                                  autocomplete = "off",
                                  data_provide = "typeahead",
                                  data_items = 8
                              })
@section JavaScript
{
    <script type="text/javascript">
            $(function () {
                $('#search').typeahead({
                    source: function (term, process) {
                        return $.getJSON('@url', { term: term }, function (data) { return process(data); });
                    }
                });
            });
    </script>
}
در اینجا توسط متد getJSON کتابخانه jQuery، مقدار عبارت وارد شده در TextBox جستجو، به آدرس اکشن متد GetNames ارسال و سپس حاصل به source افزونه typeahead انتساب داده می‌شود.
مطالب
5# آموزش سیستم مدیریت کد Git : استفاده به صورت محلی
در قسمت قبل با چگونگی نصب و راه‌اندازی git آشنا شدیم، در ادامه با نحوه‌ی استفاده از git به صورت local آشنا خواهیم شد.

 در ابتدای کار نیاز است تا repository خود را ایجاد کنیم. بدین منظور از طریق محیط command prompt به آدرس پوشه مورد نظر رفته و دستور git init را اجرا می‌کنیم. این کار سبب می‌شود تا پوشه git. در داخل فولدر جاری ایجاد شود. این پوشه در واقع همان repository و پوشه جاری، همان working tree ما خواهند بود. حال با استفاده از یک ادیتور نظیر notepad یک فایل متنی جدید را با نام readme1.txt در پوشه ایجاد کنید (توجه کنید در working tree، نه در پوشه git.؛ محتویات این پوشه جز در مورد برخی فایل‌ها نباید توسط کاربر تغییر کند)
 اکنون دستور زیر را اجرا کنید:
 git status
 همانطور که می‌بینید git نشان می‌دهد فایلی در working tree وجود دارد که تغییرات آن دنبال نمی‌شود:


 برای آن‌که این فایل را در repository ذخیره کنیم همانطور که قبلا گفته شد باید ابتدا آن‌را به index اضافه کنیم این کار با استفاده از دستور زیر انجام می‌شود:
 git add readme1.txt
 حال اگر مجددا دستور status را اجرا کنید می‌بینید که فایل به index یا همان stage اضافه شده‌است.

 اما توجه کنید که کار در این‌جا تمام نشده است برای آن‌که فایل در repository ذخیره شود باید از دستور commit استفاده کرد:
 
git commit
 بعد از اجرای این دستور، git ادیتور پیش‌فرضی را که در پیکربندی قبلا تعیین کردید باز می‌کند تا شما بتوانید توضیحاتی درباره commit خود بنویسید. از این توضیحات بعدا می‌توان به عنوان راهنمایی جهت دنبال کردن تغییرات فایل‌ها استفاده نمود. می‌توان از دستور زیر به منظور اجرای commit و نوشتن پیام آن به صورت همزمان استفاده نمود:
 
git commit -m “commit descriptions”
 بعد از اجرای دستور commit در صورتی‌که دستور status را اجرا نمایید خواهید دید که stage خالی شده و فایل readme1 در repository ذخیره شده است. در بعضی موارد می‌خواهیم چند فایل را همزمان به index اضافه کنیم در این مواقع می‌توان از دستور زیر استفاده کرد:
 git add . 
دستور فوق تمامی فایل‏های تغییر کرده و یا جدیدا اضافه شده در پوشه جاری را به stage اضافه می‏ کند. فایل readme1.txt را باز کرده و در آن تغییری دلخواه را ایجاد کنید. با اجرای دستور status می‌بینید که git به شما نشان می‌دهد فایلی تغییر یافته است. بنابراین برای ثبت تغییرات باید فایل را به stage اضافه کرد. برای اضافه کردن فایل‌های آپدیت شده، علاوه بر دستور add که در بالا گفته شد از دستور زیر نیز می‌توان استفاده کرد:
 git add -u
 سپس دستور commit را اجرا کنید تا تغییرات در repository ثبت شود. با استفاده از دستور زیر می‌توان از دستورات commit، یک log تهیه کرد:
 git log 
همانطور که در شکل زیر می‌بینید، ما دارای دو دستور commit هستیم که هر کدام از این commit‌ها توسط یک کد SHA-1 منحصر به فرد مشخص شده است

 اگر می‌خواهید مشاهده تعداد commitهای ثبت شده را در دستور log محدود کنید از دستورات زیر می‌توانید استفاده کنید:
git log --until [date]
git log --since [date]
git log -[number] 

 چگونگی حذف فایل‌ها:
 تا اینجا با نحوه چگونگی ایجاد فایل‌های جدید و یا ویرایش فایل‌های قدیمی آشنا شدید. برای حذف یک فایل می‌توان به دو صورت عمل کرد:
1) ابتدا فایل را را مستقیما حذف نموده، سپس با استفاده از دستور زیر ابتدا فایل حذف شده را به stage آورده و سپس آن را commit می‌کنیم:
git rm [filename] 
2) دستور فوق را نوشته و سپس آن را commit می‌کنیم. در این حالت خود git مدیریت حذف فایل را به عهده می‌گیرد و آن را حذف می‌کند.
 
چگونگی تغییر نام و یا جابجایی یک فایل:
برای تغییر نام و جابجایی یک فایل نیز مانند حذف، دو روش وجود دارد:
 ۱) ابتدا فایل مورد نظر را تغییر نام داده و یا جابجا می‌کنیم. در این حالت اگر status بگیریم خواهیم دید که git به ما می‌گوید فایلی با نام قبلی حذف شده و فایلی با نام جدید اضافه شده است. یعنی git تشخیص نمی‌دهد که این دو فایل یکی هستند و تنها تغییر نام داده شده است. اما به محض آن‌که فایل اول را با دستور rm حذف و فایل دوم را با دستور add اضافه کنیم، git متوجه می‌شود که این دو فایل در واقع یک فایل تغییر نام یافته هستند. البته در صورتی‌که حداقل ۵۰ درصد فایل دوم با فایل اول شباهت داشته باشد، بعد از انجام عملیات فوق از دستور commit استفاده می‌کنیم.
 ۲) در این روش از دستور زیر استفاده کرده و سپس commit را انجام می‌دهیم:
git mv [firstname][secondname] 
در ادامه مثالی را برای هر دو روش مشاهده خواهید کرد:
روش اول :


روش دوم :

 
مطالب
تاریخ شمسی با Extension Method برای DateTime
برای تبدیل تاریخ میلادی به شمسی راه‌های زیادی وجود داره که خوب این هم یک نوع از اونها هست.
برای این کار با استفاده از Extension Method برای کلاس DateTime تاریخ رو تبدیل می‌کنیم.
برای فرمت‌های‌های نمایشی تاریخ هم می‌بایست به صورت متن به Method بدهید تا تاریخ رو به اون صورتی که می‌خواهید نمایش بدهد، برای نوشتن فرمت‌ها هم می‌توانید از این لینک استفاده کنید.
خوب برای این کار ابتدا یک کلاس با نام PersianDateExtensionMethods  بسازید و تمام کد زیر رو جایگزین کد‌های کلاس کنید:
using System;
using System.Globalization;
using System.Reflection;

namespace System 
{
    public static class PersianDateExtensionMethods
    {
        private static CultureInfo _Culture;
        public static CultureInfo GetPersianCulture()
        {
            if (_Culture == null)
            {
                _Culture = new CultureInfo("fa-IR");
                DateTimeFormatInfo formatInfo = _Culture.DateTimeFormat;
                formatInfo.AbbreviatedDayNames = new[] { "ی", "د", "س", "چ", "پ", "ج", "ش" };
                formatInfo.DayNames = new[] { "یکشنبه", "دوشنبه", "سه شنبه", "چهار شنبه", "پنجشنبه", "جمعه", "شنبه" };
                var monthNames = new[]
                {
                    "فروردین", "اردیبهشت", "خرداد", "تیر", "مرداد", "شهریور", "مهر", "آبان", "آذر", "دی", "بهمن",
                    "اسفند",
                    ""
                };
                formatInfo.AbbreviatedMonthNames =
                    formatInfo.MonthNames =
                    formatInfo.MonthGenitiveNames = formatInfo.AbbreviatedMonthGenitiveNames = monthNames;
                formatInfo.AMDesignator = "ق.ظ";
                formatInfo.PMDesignator = "ب.ظ";
                formatInfo.ShortDatePattern = "yyyy/MM/dd";
                formatInfo.LongDatePattern = "dddd, dd MMMM,yyyy";
                formatInfo.FirstDayOfWeek = DayOfWeek.Saturday;
                System.Globalization.Calendar cal = new PersianCalendar();

                FieldInfo fieldInfo = _Culture.GetType().GetField("calendar", BindingFlags.NonPublic | BindingFlags.Instance);
                if (fieldInfo != null)
                    fieldInfo.SetValue(_Culture, cal);

                FieldInfo info = formatInfo.GetType().GetField("calendar", BindingFlags.NonPublic | BindingFlags.Instance);
                if (info != null)
                    info.SetValue(formatInfo, cal);

                _Culture.NumberFormat.NumberDecimalSeparator = "/";
                _Culture.NumberFormat.DigitSubstitution = DigitShapes.NativeNational;
                _Culture.NumberFormat.NumberNegativePattern = 0;
            }
            return _Culture;
        }

        public static string ToPeString(this DateTime date,string format = "yyyy/MM/dd")
        {
            return date.ToString(format,GetPersianCulture());
        }
    }
}

در کد بالا ما بااستفاده از متد GetPersianCulture یک کالچر برای تاریخ شمسی و فرمت‌های اون می‌سازیم و در متد ToPeString از اون استفاده و به متد ToString از کلاس DateTime پاس می‌دهیم.
چند مثال هم برای استفاده از متد ToPeString :
var date1 = DateTime.Now.ToPeString("yyyy/MM/dd");
var date2 = DateTime.Now.ToPeString("dddd, dd MMMM,yyyy");

//Output:
//1391/12/13
//یکشنبه, 13 اسفند,1391

نکته : با استفاده از Culture ای که در کلاس بالا در متد GetPersianCulture ساخته می‌شود امکانش هست که خود کلاس DateTime رو به شمسی تبدیل کرد، برای این کار باید به صورت زیر عمل کرد:
Thread.CurrentThread.CurrentCulture = Thread.CurrentThread.CurrentUICulture =  PersianDateExtensionMethods.GetPersianCulture();

var d1 = DateTime.Now.ToString();
//Output : 1391/12/13 11:25:44 ب.ظ

یک نکته‌ی تکمیلی: کتابخانه‌ی «DNTPersianUtils.Core» به همراه نکات این مطلب است؛ مانند:
Thread.CurrentThread.CurrentCulture = PersianCulture.Instance;
مطالب
اعتبارسنجی سفارشی سمت کاربر و سمت سرور در jqGrid
همانطور که در مطلب «فعال سازی و پردازش صفحات پویای افزودن، ویرایش و حذف رکوردهای jqGrid در ASP.NET MVC» نیز ذکر شد، خاصیت editrules یک ستون، برای مباحث اعتبارسنجی اطلاعات ورودی توسط کاربر پیش بینی شده‌است. برای مثال اگر required: true در آن تنظیم شود، کاربر مجبور به تکمیل این سلول خاص خواهد بود. در اینجا خواصی مانند number و integer از نوع bool هستند، minValue و maxValue از نوع عددی، email, url, date, time از نوع bool و custom قابل تنظیم است. در ادامه نحوه‌ی اعمال اعتبارسنجی‌های سفارشی سمت سرور و همچنین سمت کلاینت را بررسی خواهیم کرد.


مدل برنامه و نیازمندی‌های اعتبارسنجی آن

namespace jqGrid08.Models
{
    public class User
    {
        public int Id { set; get; }

        public string Name { set; get; }

        public string Email { set; get; }

        public string Password { set; get; }

        public string SiteUrl { set; get; }
    }
}
مدل کاربر فوق را در نظر بگیرید. در حین ورود اطلاعات نیاز است:
- نام کاربر به صورت اجباری وارد شود و همچنین بین 3 تا 40 حرف باشد.
- همچنین نام کاربر نباید بر اساس اطلاعات موجود در بانک اطلاعاتی، تکراری وارد شود.
- ورود ایمیل شخص اجباری است؛ به علاوه فرمت آن نیز باید با یک ایمیل واقعی تطابق داشته باشد.
- ایمیل وارد شده‌ی یک کاربر جدید نیز نباید تکراری بوده و پیشتر توسط کاربر دیگری وارد شده باشد.
- ورود کلمه‌ی عبور در حالت ثبت اطلاعات اجباری است؛ اما در حالت ویرایش اطلاعات خیر (از کلمه‌ی عبور موجود در این حالت استفاده خواهد شد).
- ورود آدرس سایت کاربر اجباری بوده و همچنین فرمت آدرس وارد شده نیز باید معتبر باشد.


اعتبار سنجی سمت سرور و سمت کلاینت نام کاربر

                colModel: [
                    {
                        name: '@(StronglyTyped.PropertyName<User>(x => x.Name))',
                        index: '@(StronglyTyped.PropertyName<User>(x => x.Name))',
                        align: 'right', width: 150,
                        editable: true, edittype: 'text',
                        editoptions: {
                            maxlength: 40
                        },
                        editrules: {
                            required: true,
                            custom: true,
                            custom_func: function (value, colname) {
                                if (!value)
                                    return [false, "لطفا نامی را وارد کنید"];

                                if (value.length < 3 || value.length > 40)
                                    return [false, colname + " باید بین 3 تا 40 حرف باشد"];
                                else
                                    return [true, ""];
                            }
                        }
                    },
                ],
با تنظیم required: true، کار تنظیم ورود اجباری نام کاربر به پایان می‌رسد. اما نیاز است این نام بین 3 تا 40 حرف باشد. بنابراین نیاز است سمت کاربر بتوان اطلاعات وارد شده توسط کاربر را دریافت کرده و سپس طول آن‌را بررسی نمود. این‌کار، توسط مقدار دهی خاصیت custom به true و سپس تعریف متدی برای custom_func قابل انجام است.
خروجی این متد یک آرایه دو عضوی است. اگر عضو اول آن true باشد، یعنی اعتبارسنجی موفقیت آمیز بوده‌است؛ اگر خیر، عضو دوم آرایه، پیامی است که به کاربر نمایش داده خواهد شد.
تا اینجا کار اعتبارسنجی سمت کاربر به پایان می‌رسد. اما نیاز است در سمت سرور نیز بررسی شود که آیا نام وارد شده تکراری است یا خیر. برای این منظور تنها کافی است رویداد afterSubmit حالت‌های Add و Edit را بررسی کنیم:
            $('#list').jqGrid({
                // ...
            }).navGrid(
                '#pager',
                //enabling buttons
                { add: true, del: true, edit: true, search: false },
                //edit option
                {
                    afterSubmit: showServerSideErrors
                },
                //add options
                {
                    afterSubmit: showServerSideErrors
                },
                //delete options
                {
                });
        });

        function showServerSideErrors(response, postdata) {
            var result = $.parseJSON(response.responseText);
            if (result.success === false) {
                //نمایش خطای اعتبار سنجی سمت سرور پس از ویرایش یا افزودن
                //و همچنین جلوگیری از ثبت نهایی فرم
                return [false, result.message, result.id];
            }

            return [true, "", result.id];
        }
شیء response، حاوی اطلاعات بازگشت داده شده از طرف سرور است. برای مثال یک چنین خروجی JSON ایی را در حالت‌های شکست اعتبارسنجی بازگشت می‌دهیم:
        [HttpPost]
        public ActionResult AddUser(User postData)
        {
            //todo: Add user to repository
            if (postData == null)
                return Json(new { success = false, message = "اطلاعات دریافتی خالی است" }, JsonRequestBehavior.AllowGet);

            if (_usersInMemoryDataSource.Any(
                    user => user.Name.Equals(postData.Name, StringComparison.InvariantCultureIgnoreCase)))
            {
                return Json(new { success = false, message = "نام کاربر تکراری است" }, JsonRequestBehavior.AllowGet);
            }

            if (_usersInMemoryDataSource.Any(
                    user => user.Email.Equals(postData.Email, StringComparison.InvariantCultureIgnoreCase)))
            {
                return Json(new { success = false, message = "آدرس ایمیل کاربر تکراری است" }, JsonRequestBehavior.AllowGet);
            }

            postData.Id = _usersInMemoryDataSource.LastOrDefault() == null ? 1 : _usersInMemoryDataSource.Last().Id + 1;
            _usersInMemoryDataSource.Add(postData);

            return Json(new { id = postData.Id, success = true }, JsonRequestBehavior.AllowGet);
        }
در سمت کلاینت در روال رویدادگردان afterSubmit می‌توان با آنالیز response و سپس استخراج فیلدهای JSON آن، وضعیت success و همچنین پیام‌های بازگشت داده شده را بررسی کرد.
خروجی روال رویدادگردان afterSubmit نیز بسیار شبیه است به حالت اعتبارسنجی سفارشی یک ستون. اگر عضو اول آرایه بازگشت داده شده توسط آن false باشد، یعنی اعتبارسنجی سمت سرور، با شکست مواجه شده و در این حالت از عضو دوم آرایه برای نمایش پیام خطای بازگشت داده شده از طرف سرور استفاده خواهد شد.



اعتبار سنجی ایمیل کاربر

                colModel: [
                    {
                        name: '@(StronglyTyped.PropertyName<User>(x => x.Email))',
                        index: '@(StronglyTyped.PropertyName<User>(x => x.Email))',
                        align: 'center', width: 150,
                        editable: true, edittype: 'text',
                        editoptions: {
                            maxlength: 250,
                            dir: 'ltr'
                        },
                        editrules: {
                            required: true,
                            email: true
                        },
                        formatter: 'email'
                    },
                ],
با تنظیم required: true، کار تنظیم ورود اجباری ایمیل کاربر به پایان می‌رسد. همچنین با تنظیم email: true، به صورت خودکار فرمت ایمیل وارد شده نیز بررسی می‌شود.
مطابق نیازمندی‌های اعتبارسنجی پروژه، ایمیل وارد شده نیز نباید تکراری باشد. این مورد نیز توسط خروجی روال رویدادگردان afterSubmit که پیشتر توضیح داده شده، مدیریت می‌شود.



اعتبار سنجی کلمه عبور کاربر

                colModel: [
                    {
                        name: '@(StronglyTyped.PropertyName<User>(x => x.Password))',
                        index: '@(StronglyTyped.PropertyName<User>(x => x.Password))',
                        align: 'center', width: 70,
                        editable: true, edittype: 'password',
                        editoptions: {
                            maxlength: 10,
                            dir: 'ltr'
                        },
                        editrules: {
                            //required: true ---> در این حالت خاص قابل استفاده نیست
                            //در حالت ویرایش رکورد، ورود کلمه عبور اختیاری است
                            //در حالت افزودن رکورد، ورود کلمه عبور اجباری است
                        }
                    },
                ],
حالت بررسی اعتبارسنجی کلمه‌ی عبور در اینجا، حالت ویژه‌ای است. نیاز است در حالت ثبت اطلاعات اجباری باشد اما در حالت ویرایش خیر. بنابراین نمی‌توان از خاصیت required: true استفاده کرد؛ چون به هر دو حالت ویرایش و ثبت اطلاعات به صورت یکسان اعمال می‌شود.
برای این منظور تنها کافی است از روال رویدادگردان beforeSubmit استفاده کرد:
            $('#list').jqGrid({
                // ...
            }).navGrid(
                '#pager',
                //enabling buttons
                { add: true, del: true, edit: true, search: false },
                //edit option
                {
                    /*, beforeSubmit: function (posdata, obj) {
                        //در حالت ویرایش رکورد، ورود کلمه عبور اختیاری است
                        return [true, ""];
                    }*/
                },
                //add options
                {
                    beforeSubmit: function (postdata, obj) {
                        //در حالت افزودن رکورد، ورود کلمه عبور اجباری است
                        if (postdata.Password == null || postdata.Password == "" || postdata.Password == undefined)
                            return [false, "لطفا کلمه عبور را وارد کنید"];
                        return [true, ""];
                    }
                },
                //delete options
                {
                });
        });
چون می‌خواهیم تنها حالت Add را تحت کنترل قرار دهیم، رویدادگردان beforeSubmit آن‌را مقدار دهی کرده‌ایم. توسط postdata کلیه اطلاعات قابل ارسال به سرور به صورت یک شیء جاوا اسکریپتی یا JSON در اختیار ما است. سپس با بررسی برای مثال postdata.Password می‌توان در مورد مقدار کلمه‌ی عبور تصمیم گیری کرد. در اینجا نیز خروجی متد باید یک آرایه دو عضوی باشد تا در صورت false بودن اولین عضو آن، پیام سفارشی اعتبارسنجی خاصی را بتوان به کاربر نمایش داد.



اعتبار سنجی آدرس سایت کاربر

                colModel: [
                    {
                        name: '@(StronglyTyped.PropertyName<User>(x => x.SiteUrl))',
                        index: '@(StronglyTyped.PropertyName<User>(x => x.SiteUrl))',
                        align: 'center', width: 150,
                        editable: true, edittype: 'text',
                        editoptions: {
                            maxlength: 1000,
                            dir: 'ltr'
                        },
                        editrules: {
                            required: true,
                            url: true
                        },
                        formatter: function (cellvalue, options, rowObject) {
                            return "<a href='" + cellvalue + "' >" + cellvalue + "</a>";
                        },
                        unformat: function (cellvalue, options, cell) {
                            return $('a', cell).attr('href');
                        }
                    },
                ],
با تنظیم required: true، کار تنظیم ورود اجباری آدرس سایت کاربر به پایان می‌رسد. همچنین با تنظیم url: true، به صورت خودکار فرمت URL وارد شده نیز بررسی می‌شود.



کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید
jqGrid08.zip
نظرات مطالب
SQL Antipattern #2
نیازی به استفاده از Id نیست. مسیر زیر را در نظر بگیرید:
/// Example: "00001.00042.00005".
مسیر بالا متناظر با نودی در درخت می‌باشد که در عمق 2 بوده و فرزند 5 ام مربوط به نود 00001.00042 می‌باشد. اگر نیاز باشد فرزند جدیدی به نود 00001.00042 اضافه شود، باید ابتدا مسیر آخرین فرزند آن یعنی الگوی بالایی واکشی شده و سپس مسیر جدیدی برای نود جدید به شکل زیر تشکیل شود:
/// Example: "00001.00042.00006".
دقیقا مشابه به کاری می‌باشد که نوع داده hierarchyid موجود در Sql Server انجام می‌دهد. با این روش دقیقا مشخص می‌باشد که نود x در چه مکانی قرار داد.

مدیریت واحدهای سازمانی
یکسری متد کمکی هم برای مدیریت فیلد Path در نظر گرفته شده است.
    public class OrganizationalUnit : TrackableEntity<User>, IHasRowVersion, IPassivable
    {
        #region Constants

        /// <summary>
        /// Maximum depth of an UO hierarchy.
        /// </summary>
        public const int MaxDepth = 16;

        /// <summary>
        /// Length of a code unit between dots.
        /// </summary>
        public const int PathUnitLength = 5;

        /// <summary>
        /// Maximum length of the <see cref="Path"/> property.
        /// </summary>
        public const int MaxPathLength = MaxDepth * (PathUnitLength + 1) - 1;

        public const char HierarchicalDisplayNameSeperator = '»';

        #endregion

        #region Properties

        public string Name { get; set; }
        public string NormalizedName { get; set; }
        public string HierarchicalDisplayName { get; set; }
        /// <summary>
        /// Hierarchical Path of this organization unit.
        /// Example: "00001.00042.00005".
        /// It's changeable if OU hierarch is changed.
        /// </summary>
        public string Path { get; set; }
        public bool IsActive { get; set; } = true;
        public byte[] RowVersion { get; set; }

        #endregion

        #region Navigation Properties

        public OrganizationalUnit Parent { get; set; }
        public long? ParentId { get; set; }
        public ICollection<OrganizationalUnit> Children { get; set; } = new HashSet<OrganizationalUnit>();
        public ICollection<UserOrganizationalUnit> UserOrganizationalUnits { get; set; } =
            new HashSet<UserOrganizationalUnit>();

        #endregion

        #region Public Methods

        /// <summary>
        /// Creates path for given numbers.
        /// Example: if numbers are 4,2 then returns "00004.00002";
        /// </summary>
        /// <param name="numbers">Numbers</param>
        public static string CreatePath(params int[] numbers)
        {
            if (numbers.IsNullOrEmpty())
            {
                return null;
            }

            return numbers.Select(number => number.ToString(new string('0', PathUnitLength))).JoinAsString(".");
        }

        /// <summary>
        /// Appends a child path to a parent path. 
        /// Example: if parentPath = "00001", childPath = "00042" then returns "00001.00042".
        /// </summary>
        /// <param name="parentPath">Parent path. Can be null or empty if parent is a root.</param>
        /// <param name="childPath">Child path.</param>
        public static string AppendPath(string parentPath, string childPath)
        {
            if (childPath.IsNullOrEmpty())
            {
                throw new ArgumentNullException(nameof(childPath), "childPath can not be null or empty.");
            }

            if (parentPath.IsNullOrEmpty())
            {
                return childPath;
            }

            return parentPath + "." + childPath;
        }

        /// <summary>
        /// Gets relative path to the parent.
        /// Example: if path = "00019.00055.00001" and parentPath = "00019" then returns "00055.00001".
        /// </summary>
        /// <param name="path">The path.</param>
        /// <param name="parentPath">The parent path.</param>
        public static string GetRelativePath(string path, string parentPath)
        {
            if (path.IsNullOrEmpty())
            {
                throw new ArgumentNullException(nameof(path), "Path can not be null or empty.");
            }

            if (parentPath.IsNullOrEmpty())
            {
                return path;
            }

            if (path.Length == parentPath.Length)
            {
                return null;
            }

            return path.Substring(parentPath.Length + 1);
        }

        /// <summary>
        /// Calculates next path for given path.
        /// Example: if code = "00019.00055.00001" returns "00019.00055.00002".
        /// </summary>
        /// <param name="path">The path.</param>
        public static string CalculateNextPath(string path)
        {
            if (path.IsNullOrEmpty())
            {
                throw new ArgumentNullException(nameof(path), "Path can not be null or empty.");
            }

            var parentPath = GetParentPath(path);
            var lastUnitPath = GetLastUnitPath(path);

            return AppendPath(parentPath, CreatePath(Convert.ToInt32(lastUnitPath) + 1));
        }

        /// <summary>
        /// Gets the last unit path.
        /// Example: if path = "00019.00055.00001" returns "00001".
        /// </summary>
        /// <param name="path">The path.</param>
        public static string GetLastUnitPath(string path)
        {
            if (path.IsNullOrEmpty())
            {
                throw new ArgumentNullException(nameof(path), "Path can not be null or empty.");
            }

            var splittedPath = path.Split('.');
            return splittedPath[splittedPath.Length - 1];
        }

        /// <summary>
        /// Gets parent path.
        /// Example: if path = "00019.00055.00001" returns "00019.00055".
        /// </summary>
        /// <param name="path">The path.</param>
        public static string GetParentPath(string path)
        {
            if (path.IsNullOrEmpty())
            {
                throw new ArgumentNullException(nameof(path), "Path can not be null or empty.");
            }

            var splittedPath = path.Split('.');
            if (splittedPath.Length == 1)
            {
                return null;
            }

            return splittedPath.Take(splittedPath.Length - 1).JoinAsString(".");
        }

        #endregion
    }

البته یک ویو نمایشی برای حالت درختی هم بهتر است داشته باشید.


یکسری متد DomainService

       public virtual async Task<string> GetNextChildPathAsync(long? parentId)
        {
            var lastChild = await GetLastChildOrNullAsync(parentId).ConfigureAwait(false);
            if (lastChild == null)
            {
                var parentPath = parentId != null ? await GetPathAsync(parentId.Value).ConfigureAwait(false) : null;
                return OrganizationalUnit.AppendPath(parentPath, OrganizationalUnit.CreatePath(1));
            }

            return OrganizationalUnit.CalculateNextPath(lastChild.Path);
        }

        public async Task<string> GetNextChildHierarchicalDisplayNameAsync(string name, long? parentId)
        {
            var parent = parentId != null
                ? await _organizationalUnits.SingleOrDefaultAsync(a => a.Id == parentId.Value).ConfigureAwait(false)
                : null;

            return parent == null
                ? name
                : $"{parent.HierarchicalDisplayName} {OrganizationalUnit.HierarchicalDisplayNameSeperator} {name}";
        }

        public virtual async Task<OrganizationalUnit> GetLastChildOrNullAsync(long? parentId)
        {
            return await _organizationalUnits.OrderByDescending(c => c.Path)
                .FirstOrDefaultAsync(ou => ou.ParentId == parentId).ConfigureAwait(false);
        }

        public virtual async Task<string> GetPathAsync(long id)
        {
            Guard.ArgumentNotZero(id, nameof(id));
            var organizationalUnit = await _organizationalUnits.SingleOrDefaultAsync(ou => ou.Id == id).ConfigureAwait(false);
            if (organizationalUnit == null)
            {
                throw new KeyNotFoundException();
            }
            return organizationalUnit.Path;
        }

        public async Task<List<OrganizationalUnit>> FindChildrenAsync(long? parentId, bool recursive = false)
        {
            if (!recursive)
            {
                return await _organizationalUnits.Where(ou => ou.ParentId == parentId).ToListAsync().ConfigureAwait(false);
            }

            if (!parentId.HasValue)
            {
                return await _organizationalUnits.ToListAsync().ConfigureAwait(false);
            }

            var path = await GetPathAsync(parentId.Value).ConfigureAwait(false);

            return await _organizationalUnits.Where(
                ou => ou.Path.StartsWith(path) && ou.Id != parentId.Value).ToListAsync().ConfigureAwait(false);
        }

        public virtual async Task MoveAsync(long id, long? parentId)
        {
            Guard.ArgumentNotZero(id, nameof(id));
            var organizationalUnit = await _organizationalUnits.SingleOrDefaultAsync(ou => ou.Id == id).ConfigureAwait(false);
            if (organizationalUnit == null || organizationalUnit.ParentId == parentId)
            {
                return;
            }

            //Should find children before Path change
            var children = await FindChildrenAsync(id, true).ConfigureAwait(false);

            //Store old Path of OU
            var oldPath = organizationalUnit.Path;

            //Move OU
            organizationalUnit.Path = await GetNextChildPathAsync(parentId).ConfigureAwait(false);
            organizationalUnit.ParentId = parentId;

            //Update Children Paths
            foreach (var child in children)
            {
                child.Path = OrganizationalUnit.AppendPath(organizationalUnit.Path, OrganizationalUnit.GetRelativePath(child.Path, oldPath));
            }
        }



مطالب
پیاده سازی authorization به روش AOP به کمک کتابخانه های SNAP و StructureMap
همانطور که پیشتر در این مقاله بحث شده است، بوسیله AOP می‌توان قابلیت‌هایی که قسمت عمده‌ای از برنامه را تحت پوشش قرار می‌دهند، کپسوله کرد. یکی از قابلیت‌هایی که در بخشهای مختلف یک سیستم نرم‌افزاری مورد نیاز است، Authorization یا اعتبارسنجی‌ست. در ادامه به بررسی یک پیاده‌سازی به این روش می‌پردازیم.
 
کتابخانه SNAP
کتابخانه SNAP به گفته سازنده آن، با یکپارچه‌سازی AOP با IoC Container‌های محبوب، برنامه‌نویسی به این سبک را ساده می‌کند. این کتابخانه هم اکنون علاوه بر structureMap از IoC Providerهای Autofac, Ninject, LinFu و Castle Windsor نیز پشتیبانی میکند. 
دریافت SNAP.StructureMap 
برای دریافت آن نیاز است دستور پاورشل ذیل را در کنسول نیوگت ویژوال استودیو اجرا کنید:
PM> Install-Package snap.structuremap
پس از اجرای دستور فوق، کتابخانه SNAP.StructureMap که در زمان نگارش این مطلب نسخه 1.8.0 آن موجود است به همراه کلیه نیازمندی‌های آن که شامل موارد زیر می‌باشد نصب خواهد شد.
StructureMap (≥ 2.6.4.1)
CommonServiceLocator.StructureMapAdapter (≥ 1.1.0.3)
SNAP (≥ 1.8)
fasterflect (≥ 2.1.2)
Castle.Core (≥ 3.1.0)
CommonServiceLocator (≥ 1.0)

تنظیمات SNAP 

از آنجا که تنظیمات SNAP همانند تنظیمات StructureMap تنها باید یک بار اجرا شود، بهترین جا برای آن در یک برنامه وب، Application_Start فایل Global.asax است.
namespace Framework.UI.Asp
{
    public class Global : HttpApplication
    {
        void Application_Start(object sender, EventArgs e)
        {
            initSnap();
            initStructureMap();
        }

        private static void initSnap()
        {
            SnapConfiguration.For<StructureMapAspectContainer>(c =>
            {
                // Tell Snap to intercept types under the "Framework.ServiceLayer..." namespace.
                c.IncludeNamespace("Framework.ServiceLayer.*");
                // Register a custom interceptor (a.k.a. an aspect).
                c.Bind<Framework.ServiceLayer.Aspects.AuthorizationInterceptor>()
                .To<Framework.ServiceLayer.Aspects.AuthorizationAttribute>();
            });
        }

        void Application_EndRequest(object sender, EventArgs e)
        {
            ObjectFactory.ReleaseAndDisposeAllHttpScopedObjects();
        }

        private static void initStructureMap()
        {
            var thread = StructureMap.Pipeline.Lifecycles.GetLifecycle(InstanceScope.HttpSession);
            ObjectFactory.Configure(x =>
            {
                x.For<IUserManager>().Use<EFUserManager>();
                x.For<IAuthorizationManager>().LifecycleIs(thread)
                 .Use<EFAuthorizationManager>().Named("_AuthorizationManager");                
                x.For<Framework.DataLayer.IUnitOfWork>()
                 .Use<Framework.DataLayer.Context>();

                x.SetAllProperties(y =>
                {
                    y.OfType<IUserManager>();
                    y.OfType<Framework.DataLayer.IUnitOfWork>();
                    y.OfType<Framework.Common.Web.IPageHelpers>();
                });
            });
        }
    }
}


بخش اعظم کدهای فوق در مقاله‌های «استفاده از StructureMap به عنوان یک IoC Container» و «تزریق خودکار وابستگی‌ها در برنامه‌های ASP.NET Web forms» شرح داده شده‌اند، تنها بخش جدید متد ()initSnap است، که خط اول آن به snap می‌گوید همه کلاس‌هایی که در فضای نام Framework.ServiceLayer و زیرمجموعه‌های آن هستند را پوشش دهد. خط دوم نیز کلاس AuthorizationInterceptor را به عنوان مرجعی برای handle کردن AuthorizationAttribute معرفی می‌کند.
در ادامه به بررسی کلاس AuthorizationInterceptor می‌پردازیم.
namespace Framework.ServiceLayer.Aspects
{
    public class AuthorizationInterceptor : MethodInterceptor
    {
        public override void InterceptMethod(IInvocation invocation, MethodBase method, Attribute attribute)
        {
            var AuthManager = StructureMap.ObjectFactory
.GetInstance<Framework.ServiceLayer.UserManager.IAuthorizationManager>();
            var FullName = GetMethodFullName(method);
            if (!AuthManager.IsActionAuthorized(FullName))
                throw new Common.Exceptions.UnauthorizedAccessException("");

            invocation.Proceed(); // the underlying method call
        }

        private static string GetMethodFullName(MethodBase method)
        {
            var TypeName = (((System.Reflection.MemberInfo)(method)).DeclaringType).FullName;
            return TypeName + "." + method.Name;
        }
    }

    public class AuthorizationAttribute : MethodInterceptAttribute
    { }
کلاس مذکور از کلاس MethodInterceptor کتابخانه snap ارث بری کرده و متد InterceptMethod را تحریف میکند. این متد، کار اجرای متد اصلی ای که با این Aspect تزئین شده را بر عهده دارد. بنابراین می‌توان پیش از اجرای متد اصلی،  اعتبارسنجی را انجام داد.
 
کلاس MethodInterceptor   
کلاس MethodInterceptor  شامل چندین متد دیگر نیز هست که میتوان برای سایر مقاصد از جمله مدیریت خطا و Event logging از آنها استفاده کرد.
namespace Snap
{
    public abstract class MethodInterceptor : IAttributeInterceptor, IInterceptor, IHideBaseTypes
    {
        protected MethodInterceptor();

        public int Order { get; set; }
        public Type TargetAttribute { get; set; }

        public virtual void AfterInvocation();
        public virtual void BeforeInvocation();
        public void Intercept(IInvocation invocation);
        public abstract void InterceptMethod(IInvocation invocation, MethodBase method, Attribute attribute);
        public bool ShouldIntercept(IInvocation invocation);
    }
}
 

یک نکته  

نکته مهمی که در اینجا پیش می‌آید این است که برای اعتبارسنجی، کد کاربری شخصی که لاگین کرده، باید به طریقی در اختیار متد ()IsActionAuthorized قرار بگیرد. برای این کار می‌توان در یک HttpMudole به عنوان مثال همان ماژولی که برای تسهیل در کار تزریق خودکار وابستگی‌ها در سطح فرم‌ها استفاده می‌شود، با استفاده از امکانات structureMap به وهله‌ی ایجاد شده از AuthorizationManager (که با کمک structureMap با طول عمر InstanceScope.HttpSession ساخته شده است) دسترسی پیدا کرده و خاصیت مربوطه را مقداردهی کرد.
        private void Application_PreRequestHandlerExecute(object source, EventArgs e)
        {
            var page = HttpContext.Current.Handler as BasePage; // The Page handler
            if (page == null)
                return;

            WireUpThePage(page);
            WireUpAllUserControls(page);

            var UsrCod = HttpContext.Current.Session["UsrCod"];
            if (UsrCod != null)
            {
                var _AuthorizationManager = ObjectFactory
                    .GetNamedInstance<Framework.ServiceLayer.UserManager.IAuthorizationManager>("_AuthorizationManager");

                ((Framework.ServiceLayer.UserManager.EFAuthorizationManager)_AuthorizationManager)
                    .AuditUserId = UsrCod.ToString();
            }
        }
 
روش استفاده
نحوه استفاده از Aspect تعریف شده در کد زیر قابل مشاهده است:
namespace Framework.ServiceLayer.UserManager
{
    public class EFUserManager : IUserManager
    {
        IUnitOfWork _uow;
        IDbSet<User> _users;

        public EFUserManager(IUnitOfWork uow)
        {
            _uow = uow;
            _users = _uow.Set<User>();
        }

        [Framework.ServiceLayer.Aspects.Authorization]
        public List<User> GetAll()
        {
            return _users.ToList<User>();
        }
    }
}

مطالب
کار با بانک‌های اطلاعاتی مختلف در PdfReport
تعدادی از منابع داده پیش فرض PdfReport جهت کار مستقیم با بانک‌های اطلاعاتی مختلف، کوئری نوشتن و نمایش نتایج آن‌ها طراحی شده‌اند.
در این بین با توجه به اینکه دات نت پشتیبانی توکاری از SQL Server دارد، اتصال و استفاده از توانمندی‌های آن نیاز به کتابخانه جانبی خاصی ندارد. اما برای کار با بانک‌های اطلاعاتی دیگر نیاز خواهد بود تا پروایدر ADO.NET آن‌ها را تهیه و به برنامه اضافه کنیم.
چهار نمونه از منابع داده پیش فرضی که در متد MainTableDataSource قابل تعریف هستند به شرح زیر می‌باشند:
public void SqlDataReader(string connectionString, string sql, params object[] parametersValues)

//.mdb or .accdb files
public void AccessDataReader(string filePath, string password, string sql, params object[] parametersValues)

public void OdbcDataReader(string connectionString, string sql, params object[] parametersValues)
SqlDataReader برای کار با بانک‌های اطلاعاتی SQL Server بهینه سازی شده است.
AccessDataReader قابلیت اتصال به بانک‌های اطلاعاتی اکسس جدید (فایل‌های accdb) و اکسس قدیم (فایل‌های mdb) را دارد.
OdbcDataReader یک پروایدر عمومی است که از روز اول دات نت به همراه آن بوده است. برای مثال جهت اتصال به بانک‌های اطلاعاتی فاکس‌پرو می‌تواند مورد استفاده قرار گیرد.
اما ... برای مابقی بانک‌های اطلاعاتی چطور؟
برای سایر بانک‌های اطلاعاتی، منبع داده عمومی زیر تدارک دیده شده است:
public void GenericDataReader(string providerName, string connectionString, string sql, params object[] parametersValues)
تنها تفاوت آن با نمونه‌های قبل، ذکر providerName آن است. برای مثال جهت اتصال به SQLite ابتدا پروایدر مخصوص ADO.NET آن‌را دریافت و به پروژه خود اضافه نمائید. سپس پارامتر providerName فوق را با "System.Data.SQLite" مقدار دهی کنید.

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

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

using System;
using PdfRpt.Core.Contracts;
using PdfRpt.Core.Helper;
using PdfRpt.FluentInterface;

namespace PdfReportSamples.SQLiteDataReader
{
    public class SQLiteDataReaderPdfReport
    {
        public IPdfReportData CreatePdfReport()
        {
            return new PdfReport().DocumentPreferences(doc =>
            {
                doc.RunDirection(PdfRunDirection.RightToLeft);
                doc.Orientation(PageOrientation.Portrait);
                doc.PageSize(PdfPageSize.A4);
                doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" });
            })
            .DefaultFonts(fonts =>
            {
                fonts.Path(AppPath.ApplicationPath + "\\fonts\\irsans.ttf",
                                  Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf");
            })
            .PagesFooter(footer =>
            {
                footer.DefaultFooter(DateTime.Now.ToString("MM/dd/yyyy"));
            })
            .PagesHeader(header =>
            {
                header.DefaultHeader(defaultHeader =>
                {
                    defaultHeader.RunDirection(PdfRunDirection.RightToLeft);
                    defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png");
                    defaultHeader.Message("گزارش جدید ما");
                });
            })
            .MainTableTemplate(template =>
            {
                template.BasicTemplate(BasicTemplate.SilverTemplate);
            })
            .MainTablePreferences(table =>
            {
                table.ColumnsWidthsType(TableColumnWidthType.Relative);
                table.NumberOfDataRowsPerPage(5);
            })
            .MainTableDataSource(dataSource =>
            {
                dataSource.GenericDataReader(
                    providerName: "System.Data.SQLite",
                    connectionString: "Data Source=" + AppPath.ApplicationPath + "\\data\\blogs.sqlite",
                    sql: @"SELECT [url], [name], [NumberOfPosts], [AddDate]
                               FROM [tblBlogs]
                               WHERE [NumberOfPosts]>=@p1",
                    parametersValues: new object[] { 10 }
                );
            })
            .MainTableSummarySettings(summarySettings =>
            {
                summarySettings.OverallSummarySettings("جمع کل");
                summarySettings.PreviousPageSummarySettings("نقل از صفحه قبل");
                summarySettings.PageSummarySettings("جمع صفحه");
            })
            .MainTableColumns(columns =>
            {
                columns.AddColumn(column =>
                {
                    column.PropertyName("rowNo");
                    column.IsRowNumber(true);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(0);
                    column.Width(1);
                    column.HeaderCell("ردیف");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName("url");
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(1);
                    column.Width(2);
                    column.HeaderCell("آدرس");
                    column.ColumnItemsTemplate(template =>
                    {
                        template.Hyperlink(foreColor: System.Drawing.Color.Blue, fontUnderline: true);
                    });
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName("name");
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(2);
                    column.Width(2);
                    column.HeaderCell("نام");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName("NumberOfPosts");
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(3);
                    column.Width(2);
                    column.HeaderCell("تعداد مطلب");
                    column.ColumnItemsTemplate(template =>
                    {
                        template.TextBlock();
                        template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                    });
                    column.AggregateFunction(aggregateFunction =>
                    {
                        aggregateFunction.NumericAggregateFunction(AggregateFunction.Sum);
                        aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                    });
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName("AddDate");
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(4);
                    column.Width(2);
                    column.HeaderCell("تاریخ ثبت");
                    column.ColumnItemsTemplate(template =>
                    {
                        template.TextBlock();
                        template.DisplayFormatFormula(obj => obj == null ? string.Empty : PersianDate.ToPersianDateTime((DateTime)obj) /*((DateTime)obj).ToString("dd/MM/yyyy HH:mm")*/);
                    });
                });
            })
            .MainTableEvents(events =>
            {
                events.DataSourceIsEmpty(message: "There is no data available to display.");
            })
            .Export(export =>
            {
                export.ToExcel();
            })
            .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\RptSqlDataReaderSample.pdf"));
        }
    }
}

توضیحات:

- در مثال فوق نحوه استفاده از یک بانک اطلاعاتی SQLite را ملاحظه می‌کنید. این بانک اطلاعاتی نمونه در پوشه bin\data سورس به روز شده پروژه موجود است.
                dataSource.GenericDataReader(
                    providerName: "System.Data.SQLite",
                    connectionString: "Data Source=" + AppPath.ApplicationPath + "\\data\\blogs.sqlite",
                    sql: @"SELECT [url], [name], [NumberOfPosts], [AddDate]
                               FROM [tblBlogs]
                               WHERE [NumberOfPosts]>=@p1",
                    parametersValues: new object[] { 10 }
                );
فرض بر این است که فایل‌های System.Data.SQLite.dll و SQLite.Interop.dll را از سایت SQLite دریافت کرده و سپس ارجاعی را به اسمبلی System.Data.SQLite.dll به پروژه خود افزوده‌اید.
در مرحله بعد به کمک GenericDataReader می‌توان به این پروایدر دسترسی یافت. همانطور که ملاحظه می‌کنید یک کوئری پارامتری با مقدار پارامتر مساوی 10 جهت تهیه گزارش، تعریف شده است.
همچنین باید دقت داشت که اگر پروژه جاری شما مبتنی بر دات نت 4 است، نیاز خواهید داشت چند سطر زیر را به فایل config برنامه اضافه نمائید تا با SQLite مشکلی نداشته باشد:
<?xml version="1.0"?>
   <configuration>
      <startup useLegacyV2RuntimeActivationPolicy="true">
           <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
      </startup>
   </configuration>                  
- مرحله بعد نوبت به معرفی ستون‌های گزارش است. هر ستون، معادل یک فیلد معرفی شده در کوئری SQL ارسال شده به GenericDataReader خواهد بود (کوچکی و بزرگی حروف باید در اینجا رعایت شوند).
- در حین معرفی ستون AddDate، نحوه نمایش و تبدیل تاریخ دریافتی که با فرمت DateTime است را به تاریخ شمسی ملاحظه می‌کنید. متد PersianDate.ToPersianDateTime در فضای نام PdfRpt.Core.Helper قرار دارد. توسط DisplayFormatFormula، فرصت خواهید داشت مقدار متناظر با سلول در حال رندر را پیش از نمایش، به هر نحو دلخواهی فرمت کنید.
- در ستون url از قالب نمایشی پیش فرض Hyperlink، برای نمایش اطلاعات فیلد جاری به صورت یک لینک قابل کلیک استفاده شده است.

یک نکته:
ذکر قسمت MainTableColumns و تمام تعاریف مرتبط با آن در PdfReports اختیاری است. به این معنا که می‌توانید قسمت گزارش سازی و تعاریف گزارشات برنامه خود را پویا کنید (شبیه به حالت auto generate columns در گرید‌های معروف). کوئری‌های SQL متناظر با گزارشات را در بانک اطلاعاتی ذخیره کنید و به گزارش ساز فوق ارسال نمائید. حاصل یک گزارش جدید است.

مطالب
breeze js به همراه ایجاد سایت آگهی قسمت سوم
در مطلب قبلی، پیش نیازهای مربوطه را نصب کردیم. در این قسمت به ساخت صفحات ورود و خروج، ثبت نام کاربران و تغییر رمز عبور خواهیم پرداخت.
در اینجا ما از Account Controller پیش فرض Asp.net Mvc استفاده میکنیم که متدهای مورد استفاده ما در آن قرار دارد و به BreezeController مزین شده است.
 [BreezeController]
    public class AccountController : ApiController
    {
      ...
    }

 اینترفیس IAuthService:
module Interfaces {
    export interface IAuthService {
        user: Models.IUserToken
        getUserInfo(accessToken);
        login(data);
        logOut();
        register(data);
        changePassword(data);
        accessToken(accessToken, data);
    }
}

سرویس AuthService 
پیاده سازی اینترفیس IAuthService را برعهده دارد. در سازنده آن، وابستگی‌های آن مقداردهی شده‌است و همچنین تنظیمات manager را انجام داده‌ایم.
متد accessToken: وظیفه ارسال توکن  را به سرور و همچنین نگهداری آن‌را در local storage، برعهده دارد.
متد getUserInfo: اطلاعات کاربر لاگین شده را  از سرور دریافت مینماید.
متد login: فرمت مورد قبول سرور به نحو زیر میباشد. در صورت موفقیت آمیز بودن، توکن را به متد accessToken  پاس میدهیم و آبجکت user را با مقادیر دریافتی پر می‌نماییم.
"grant_type=password & username=myusername & password=mypassword";
برای فراخوانی متدهای post، همانطوری که در مطلب ارسال کوئری‌های پست به آن اشاره شده‌است، عمل می‌نماییم. در ابتدا فایل breeze.ajaxpost.js را اضافه می‌کنیم سپس در فایل breeze.angular  قطعه کد زیر را در متد useNgHttp اضافه می‌کنیم.
var ajaxAdapter = breeze.config.getAdapterInstance("ajax");
breeze.ajaxpost(ajaxAdapter);
با این تنظیمات میتوانیم توسط withParameters، متدهای post را فراخوانی کنیم. 
.withParameters({
                    $method: 'POST',
                    $encoding: 'JSON',
                    $data: newData
                }
متد logOut: خروج از برنامه را برای ما انجام می‌دهد و درصورت موفقیت آمیز بودن، به صفحه اصلی هدایت می‌شود.
متد register: ثبت نام کاربران را بر عهده دارد.
متد changePassword: تغییر رمز عبور کاربران را برعهده دارد.
module AdApps {
    var securityUrls = {
        site: '/',
        login: '/token',
        logout: 'logout',
        register: 'register',
        userInfo: 'getUserInfo',
        changePassword: 'changePassword',
    }
    export class AuthService implements Interfaces.IAuthService {
        private manager: breeze.EntityManager;

        constructor(
            private _breeze: typeof breeze,
            private $http: ng.IHttpProvider,
            private toaster: ngtoaster.IToasterService,
            private $location: ng.ILocationService) {

            var dataService = new _breeze.DataService({
                serviceName: "/breeze/Account",
                hasServerMetadata: false
            });
            var metadataStore = new _breeze.MetadataStore({
                namingConvention: _breeze.NamingConvention.camelCase
            });
            this.manager = new _breeze.EntityManager({
                dataService: dataService,
                metadataStore: metadataStore,
                saveOptions: new _breeze.SaveOptions({
                    allowConcurrentSaves: true, tag: [{}]
                })
            });
        }

        user: Models.IUserToken;
        accessToken(accessToken, data): string {
            if (accessToken === 'clear') {
                localStorage.removeItem('accessToken');
                delete this.$http.defaults.headers.common.Authorization;
            }
            else {
                window.localStorage.setItem("accessToken", accessToken);
                this.$http.defaults.headers.common.Authorization = 'Bearer ' + accessToken;
            }
            return accessToken;
        }
        getUserInfo(): ng.IPromise<any> {
            var query = this._breeze.EntityQuery.from(securityUrls.userInfo);
            return this.manager.executeQuery(query).then(data => {
                return data.results[0];
            });
        }
        login(data: any): ng.IPromise<any> {
            var newData = "grant_type=password&username=" + data.userName + "&password=" + data.password;
            var query = this._breeze.EntityQuery.from(securityUrls.login)
                .withParameters({
                    $method: 'POST',
                    $encoding: 'JSON',
                    $data: newData
                });
            return this.manager.executeQuery(query).then(data => {
                var self = this;
                var result = data.results[0] as any;
                self.accessToken(result.access_token, data.results[0]);
                self.user = <Models.IUserToken>{};
                self.user = <Models.IUserToken>result;
                return result;
            });
        }
        logOut(): ng.IPromise<any> {
            var query = this._breeze.EntityQuery.from(securityUrls.logout)
                .withParameters({
                    $method: 'POST',
                    $encoding: 'JSON',
                });
            return this.manager.executeQuery(query).then(data => {
                this.user = null;
                this.accessToken('clear', null);
                this.$location.path("/");
            });
        }
        register(data: Object): ng.IPromise<any> {
            var query = this._breeze.EntityQuery.from(securityUrls.register)
                .withParameters({
                    $method: 'POST',
                    $encoding: 'JSON',
                    $data: data
                });
            return this.manager.executeQuery(query).then(data => { });
        }
        changePassword(data: Object): ng.IPromise<any> {
            var query = this._breeze.EntityQuery.from(securityUrls.changePassword)
                .withParameters({
                    $method: 'POST',
                    $encoding: 'JSON',
                    $data: data
                });
            return this.manager.executeQuery(query).then(data => { });
        }
    }
}

سرویس HttpInterceptor 
: رهگیری و پیگیری کردن نتیجه درخواست‌های http را بر عهده دارد.
درrequest : توکن امنیتی را به هدر درخواست‌ها اضافه میکنیم.
در response : در صورت موفقیت درخواست http، پیغام مناسبی را نمایش میدهیم.
در responseError : در صورت عدم موفقیت درخواست http، پیغام مناسبی را نمایش میدهیم.
module AdApps {
    export class HttpInterceptor {
        private static _toaster: ngtoaster.IToasterService;
        private static _$q: ng.IQService;
        constructor(
            private $q: ng.IQService,
            private toaster: ngtoaster.IToasterService,
            private $location: ng.ILocationService) {
            HttpInterceptor._toaster = toaster;
            HttpInterceptor._$q = $q;
        }
        request(config): string {
            config.headers = config.headers || {};
            var authData = window.localStorage.getItem("accessToken");
            if (authData) {
                config.headers.Authorization = "Bearer " + authData;
            }
            return config;
        };
        response(response): ng.IPromise<any> {
            if (response.data && response.data.message && response.status === 200) {
                HttpInterceptor._toaster.success(response.data.message)
            }
            return HttpInterceptor._$q.resolve(response);
        };
        responseError(response): ng.IPromise<any> {
            var self = this;
            var data = response.data;
            var title = "خطا";
            var messages = [];
            if (data) {
                if (data.error) {
                    title = data.error;
                }
                if (data.message) {
                    messages.push(data.message);
                }
                if (data.Message) {
                    messages.push(data.Message);
                }
                if (data.ModelState) {
                    angular.forEach(data.ModelState, function (errors, key) {
                        if (key.substr(0, 1) != "$")
                        { messages.push(errors); }

                    });
                }
                if (data.exceptionMessage) {
                    messages.push(data.exceptionMessage);
                }
                if (data.ExceptionMessage) {
                    messages.push(data.ExceptionMessage);
                }
                if (data.error_description) {
                    messages.push(data.error_description);
                }
                if (messages.length > 0) {
                    HttpInterceptor._toaster.error(title, messages.join("<br/>"));
                }
                if (response.status === "401") {
                    self.$location.path("/ورود");
                }
            }
            return HttpInterceptor._$q.reject(response);
        }
    }
}

معرفی کردن مسیرهای ورود، ثبت نام و تغییر رمز عبور به انگولار
module AdApps {
    class SecurityCtrl {
        constructor(private $scope: Interfaces.IAuthScope, private authService: AuthService) {
            $scope.authService = authService;
            if (window.localStorage.getItem("accessToken") != null) {
                authService.getUserInfo().then(function (data) {
                    $scope.authService.user = data;
                });
            }
            $scope.logOut = function () {
                return authService.logOut().then(function () { });
            }
        }
    }
    define(["angularAmd", "angular", "factory/AuthService", "factory/httpInterceptor"], (angularAmd, ng) => {
        angularAmd = angularAmd.__proto__;
        var app = ng.module("AngularTypeScript", ['ngRoute', 'breeze.angular', 'toaster']);
        var viewPath = "app/views/";
        var controllerPath = "app/controller/";
        app.config(['$routeProvider', '$httpProvider', function ($routeProvider, $httpProvider) {
            $httpProvider.interceptors.push("HttpInterceptor");
            $routeProvider
                .when("/", angularAmd.route({
                    templateUrl: viewPath + "home.html",
                    controllerUrl: controllerPath + "home.js"
                }))
                .when("/login", angularAmd.route({
                    templateUrl: viewPath + "login.html",
                    controllerUrl: controllerPath + "login.js"
                }))
                .when("/register", angularAmd.route({
                    templateUrl: viewPath + "register.html",
                    controllerUrl: controllerPath + "register.js"
                }))
                .when("/changePassword", angularAmd.route({
                    templateUrl: viewPath + "change-password.html",
                    controllerUrl: controllerPath + "changePassword.js"
                }))
                .otherwise({ redirectTo: '/' });
        }
        ]);
        app.service('AuthService', ['breeze', '$http', 'toaster', '$location', AuthService]);
        app.service("HttpInterceptor", ["$q", "toaster", "$location", HttpInterceptor]);
        app.controller('SecurityCtrl', ['$scope', 'AuthService', SecurityCtrl]);
        return angularAmd.bootstrap(app);
    });
}

ایجاد کنترلر .login.ts و ارسال سرویس‌های لازم به کلاس LoginCtrl
در صورت صحیح بودن نام کاربری و رمز عبور به صفحه اصلی هدایت خواهد شد.
module AdApps {
    define(['app'], function (app) {
        app.controller('LoginCtrl', ["$scope", "AuthService", "$location", LoginCtrl]);
    });
   export  class LoginCtrl {
        constructor($scope: Interfaces.ILoginScope, authService: AuthService, $location: ng.ILocationService) {
           $scope.submit = function () {
               authService.login(angular.copy($scope.form))
                    .then(function (data) {
                        this.$location.path("/");
                    })
            };
        }
    }
}

ایجاد login.html
<div  ng-controller="LoginCtrl">
    <div>
        <i></i>
        <span>ورود</span>
        <div>
            <div>
            </div>
        </div>
    </div>
    <div>
        <div>
            <div>
                <form name="Form" id="form1">
                    <fieldset>
                        <div>
                            <div>
                                <input
                                       name="username"
                                       ng-model="form.userName"
                                       placeholder="نام کاربری"
                                       required>
                                <span>
                                    <i></i>
                                </span>
                            </div>
                        </div>
                        <div>
                            <div>
                                <input name="password"
                                       type="password"
                                       ng-model="form.password"
                                       placeholder="{{'Password'}}"
                                       validator="required">
                                <span>
                                    <i></i>
                                </span>
                            </div>
                        </div>

                    </fieldset>
                    <div>
                        <button type="submit" ng-click="submit()">ورود</button>
                    </div>
                </form>
            </div>
        </div>
    </div>
</div>
فایل‌های مربوط به ثبت نام و تغییر رمز عبور، مشابه لاگین می‌باشد و از ذکر آن خودداری می‌نماییم و فایل‌های مربوطه در پروژه قرار دارند.
با تغییرات بالا، فایل main.ts دارای محتویات زیر میباشد:
requirejs.config({
    paths: {
        "app": "app",
        "angularAmd": "/Scripts/angularAmd",
        "angular": "/Scripts/angular",
        "breezeAjaxpost": "/Scripts/breeze/breeze.ajaxpost",
        "breeze": "/Scripts/breeze/breeze.debug",
        "breezeAngular": "/Scripts/breeze/breeze.angular",
        "bootstrap": "/Scripts/bootstrap",
        "angularRoute": "/Scripts/angular-route",
        "jquery": "/Scripts/jquery-2.2.2",
        "entityManagerService": "factory/entityManagerService",
        "toaster": "/Scripts/toaster",
    },
    waitSeconds: 0,
    shim: {
        "angular": { exports: "angular" },
        "angularRoute": { deps: ["angular"] },
        "bootstrap": { deps: ["jquery"] },
        "breeze": { deps: ["jquery"] },
        "breezeAngular": {
            deps: ["angular", "breeze"]
        },
        "toaster": { deps: ["angular"] },
        "app": {
            deps: ["bootstrap", "angularRoute", "toaster", "breezeAngular", "breezeAjaxpost"]
        }
    }
});
require(["app"]);
فایل پروژه :AngularTypeScript.zip
در قسمت‌های بعدی به ثبت و نمایش آگهی در سایت خواهیم پرداخت.