Quantcast
Channel: ‫فید گروه mvc .NET Tips
Viewing all 316 articles
Browse latest View live

‫جلوگیری از ارسال Spam در ASP.NET MVC

$
0
0
در هر وب‌سایتی که فرمی برای ارسال اطلاعات به سرور موجود باشد، آن وب سایت مستعد ارسال اسپم و بمباران درخواست‌های متعدد خواهد بود. در برخی موارد استفاده از کپچا می‌تواند راه خوبی برای جلوگیری از ارسال‌های مکرر و مخرب باشد، ولی گاهی اوقات سناریوی ما به شکلی است که امکان استفاده از کپچا، به عنوان یک مکانیزم امنیتی مقدور نیست.
اگر شما یک فرم تماس با ما داشته باشید استفاده از کپچا یک مکانیزم امنیتی معقول می‌باشد و همچنین اگر فرمی جهت ارسال پست داشته باشید. اما در برخی مواقع مانند فرمهای ارسال کامنت، پاسخ، چت و ... امکان استفاده از این روش وجود ندارد و باید به فکر راه حلی مناسب برای مقابل با درخواست‌های مخرب باشیم.
اگر شما هم به دنبال تامین امنیت سایت خود هستید و دوست ندارید که وب سایت شما (به دلیل کمبود پهنای باند یا ارسال مطالب نامربوط که گاهی اوقات به صدها هزار مورد می‌رسد) از دسترس خارج شود این آموزش را دنبال کنید.
برای این منظور ما از یک ActionFilter برای امضای ActionMethodهایی استفاده می‌کنیم که باید با ارسالهای متعدد از سوی یک کاربر مقابله کنند. این ActionFilter  باید قابلیت تنظیم حداقل زمان بین درخواستها را داشته باشد و اگر درخواستی در زمانی کمتر از مدت مجاز تعیین شده برسد، به نحوی مطلوبی به آن رسیدگی کند.
پس از آن ما نیازمند مکانیزمی هستیم تا درخواست‌های رسیده‌ی از سوی هرکاربر را به شکلی کاملا خاص و یکتا شناسایی کند. راه حلی که قرار است در این ActionFilter  از آن استفاده کنم به شرح زیر است:
ما به دنبال آن هستیم که یک شناسه‌ی منحصر به فرد را برای هر درخواست ایجاد کنیم. لذا از اطلاعات شیئ Request جاری برای این منظور استفاده می‌کنیم.
1) IP درخواست جاری (قابل بازیابی از هدر HTTP_X_FORWARDED_FOR یا REMOTE_ADDR)
2) مشخصات مرورگر کاربر (قابل بازیابی از هدر USER_AGENT)
3) آدرس درخواست جاری (برای اینکه شناسه‌ی تولیدی کاملا یکتا باشد، هرچند می‌توانید آن را حذف کنید)

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

مرحله بعد پیاده سازی مکانیزمی برای نگهداری این اطلاعات و بازیابی آن‌ها در هر درخواست است. ما برای این منظور از سیستم Cache استفاده می‌کنیم؛ هرچند راه حل‌های بهتری هم وجود دارند.
بنابراین پس از ایجاد شناسه یکتای درخواست، آن را در Cache قرار می‌دهیم و زمان انقضای آن را هم پارامتری که ابتدای کار گفتم قرار می‌دهیم. سپس در هر درخواست Cache را برای این مقدار یکتا جستجو می‌کنیم. اگر شناسه پیدا شود، یعنی در کمتر از زمان تعیین شده، درخواست مجددی از سوی کاربر صورت گرفته است و اگر شناسه در Cache موجود نباشد، یعنی درخواست رسیده در زمان معقولی صادر شده است.
باید توجه داشته باشید که تعیین زمان بین هر درخواست به ازای هر ActionMethod خواهد بود و نباید آنقدر زیاد باشد که عملا کاربر را محدود کنیم. برای مثال در یک سیستم چت، زمان معقول بین هر درخواست 5 ثانیه است و در یک سیستم ارسال نظر یا پاسخ، 10 ثانیه. در هر حال بسته به نظر شما این زمان می‌تواند قابل تغییر باشد. حتی می‌توانید کاربر را مجبور کنید که در روز فقط یک دیدگاه ارسال کند!

قبل از پیاده سازی سناریوی فوق، در مورد نقش گزینه‌ی سوم در شناسه‌ی درخواست، لازم است توضیحاتی بدهم. با استفاده از این خصوصیت (یعنی آدرس درخواست جاری) شدت سختگیری ما کمتر می‌شود. زیرا به ازای هر آدرس، شناسه‌ی تولیدی متفاوت خواهد بود. اگر فرد مهاجم، برنامه‌ای را که با آن اسپم می‌کند، طوری طراحی کرده باشد که مرتبا درخواست‌ها را به آدرس‌های متفاوتی ارسال کند، مکانیزم ما کمتر با آن مقابله خواهد کرد.
برای مثال فرد مهاجم می‌تواند در یک حلقه، ابتدا درخواستی را به AddComment بدهد، بعد AddReply و بعد SendMessage. پس همانطور که می‌بینید اگر از پارامتر سوم استفاده کنید، عملا قدرت مکانیزم ما به یک سوم کاهش می‌یابد.
نکته‌ی دیگری که قابل ذکر است اینست که این روش راهی برای تشخیص زمان بین درخواست‌های صورت گرفته از کاربر است و به تنهایی نمی‌تواند امنیت کامل را برای مقابله با اسپم‌ها، مهیا کند و باید به فکر مکانیزم دیگری برای مقابله با کاربری که درخواست‌های نامعقولی در مدت زمان کمی می‌فرستد پیاده کنیم (پیاده سازی مکانیزم تکمیلی را در آینده شرح خواهم داد).
اکنون نوبت پیاده سازی سناریوی ماست. ابتدا یک کلاس ایجاد کنید و آن را از ActionFilterAttribute مشتق کنید و کدهای زیر را وارد کنید:
using System;
using System.Linq;
using System.Web.Mvc;
using System.Security.Cryptography;
using System.Text;
using System.Web.Caching;

namespace Parsnet.Core
{
    public class StopSpamAttribute : ActionFilterAttribute
    {
        // حداقل زمان مجاز بین درخواست‌ها برحسب ثانیه
        public int DelayRequest = 10;

        // پیام خطایی که در صورت رسیدن درخواست غیرمجاز باید صادر کنیم
        public string ErrorMessage = "درخواست‌های شما در مدت زمان معقولی صورت نگرفته است.";

        //خصوصیتی برای تعیین اینکه آدرس درخواست هم به شناسه یکتا افزوده شود یا خیر
        public bool AddAddress = true;


        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            // درسترسی به شئی درخواست
            var request = filterContext.HttpContext.Request;

            // دسترسی به شیئ کش
            var cache = filterContext.HttpContext.Cache;

            // کاربر IP بدست آوردن
            var IP = request.ServerVariables["HTTP_X_FORWARDED_FOR"] ?? request.UserHostAddress;

            // مشخصات مرورگر
            var browser = request.UserAgent;

            // در اینجا آدرس درخواست جاری را تعیین می‌کنیم
            var targetInfo = (this.AddAddress) ? (request.RawUrl + request.QueryString) : "";

            // شناسه یکتای درخواست
            var Uniquely = String.Concat(IP, browser, targetInfo);


            //در اینجا با کمک هش یک امضا از شناسه‌ی درخواست ایجاد می‌کنیم
            var hashValue = string.Join("", MD5.Create().ComputeHash(Encoding.ASCII.GetBytes(Uniquely)).Select(s => s.ToString("x2")));

            // ابتدا چک می‌کنیم که آیا شناسه‌ی یکتای درخواست در کش موجود نباشد
            if (cache[hashValue] != null)
            {
                // یک خطا اضافه می‌کنیم ModelState اگر موجود بود یعنی کمتر از زمان موردنظر درخواست مجددی صورت گرفته و به
                filterContext.Controller.ViewData.ModelState.AddModelError("ExcessiveRequests", ErrorMessage);
            }
            else
            {
                // اگر موجود نبود یعنی درخواست با زمانی بیشتر از مقداری که تعیین کرده‌ایم انجام شده
                // پس شناسه درخواست جدید را با پارامتر زمانی که تعیین کرده بودیم به شیئ کش اضافه می‌کنیم
                cache.Add(hashValue, true, null, DateTime.Now.AddSeconds(DelayRequest), Cache.NoSlidingExpiration, CacheItemPriority.Default, null);
            }

            base.OnActionExecuting(filterContext);
        }
    }
}
و حال برای استفاده از این مکانیزم امنیتی ActionMethod مورد نظر را با آن امضا می‌کنیم:
[HttpPost]
        [StopSpam(DelayRequest = 5)]
        [ValidateAntiForgeryToken]
        public virtual async Task<ActionResult> SendFile(HttpPostedFileBase file, int userid = 0)
        { 
        }

[HttpPost]
        [StopSpam(DelayRequest = 30, ErrorMessage = "زمان لازم بین ارسال هر مطلب 30 ثانیه است")]
        [ValidateAntiForgeryToken]
        public virtual async Task<ActionResult> InsertPost(NewPostModel model)
        {
        }

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

به توجه به دیدگاه‌های مطرح شده اصلاحاتی در کلاس صورت گرفت و قابلیتی به آن اضافه گردید که بتوان مکانیزم اعتبارسنجی را کنترل کرد.
برای این منظور خصوصیتی به این ActionFilter افزوده شد تا هنگامیکه داده‌های فرم معتبر نباشند و در واقع هنوز چیزی ثبت نشده است این مکانیزم را بتوان کنترل کرد. خصوصیت CheckResult باعث میشود تا اگر داده‌های مدل ما در اعتبارسنجی، معتبر نبودند کلید افزوده شده به کش را حذف تا کاربر بتواند مجدد فرم را ارسال کند. مقدار آن به طور پیش فرض true است و اگر برابر false قرار بگیرد تا اتمام زمان تعیین شده در مکانیزم ما، کاربر امکان ارسال مجدد فرم را ندارد.
همچنین باید بعد از اتمام عملیات در صورت عدم موفقیت آمیز بودن آن به ViewBag یک خصوصیت به نام ExecuteResult اضافه کنید و مقدار آن را برابر false قرار دهید. تا کلید از کش حذف گردد.
نحوه استفاده آن هم به شکل زیر می‌باشد:
        [HttpPost]
        [StopSpam(AddAddress = true, DelayRequest = 20)]
        [ValidateAntiForgeryToken]
        public Task<ActionResult> InsertPost(NewPostModel model)
        {
            if (ModelState.IsValid)
            {
                var newPost = dbContext.InsertPost(model);
                if (newPost != null)
                {
                    ViewBag.ExecuteResult = true;
                }
            }

            if (ModelState.IsValidField("ExcessiveRequests") == true)
{
ViewBag.ExecuteResult = false;
}
return View(); }

فایل ضمیمه را می‌توانید از زیر دانلود کنید:
StopSpamAttribute.rar

‫تکنیک‌های ایجاد سایت‌های چند زبانه

$
0
0
امروزه چند زبانه بودن سایت‌ها، از اهمیت بالایی برخوردار شده است و هر سایتی که نیاز داشته باشد در سایر نقاط جهان شناخته شود و کاربران مناطق مختلف، به راحتی از آن استفاده کنند، سایت‌های خود را بر پایه‌ی چندین زبان ایجاد می‌کنند. در این نوشتار سعی داریم بر این موضوع بررسی اجمالی داشته باشیم و نکات زیر را بررسی نماییم.

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

طراحی مدل دیتابیس

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

مزایا:

  1. پیاده سازی آسان

معایب:

  1. در این روش با زیاد شدن هر زبان، تعداد ستون‌ها افزایش می‌یابد که باعث می‌شود طراحی مناسبی نداشته باشد.
  2. در ضمن این مورد باید توسط برنامه نویس مرتبا اضافه گردد یا اینکه برنامه نویس این امکان را در سیستم قرار دهد که مدیر سایت بتواند در پشت صحنه کوئری افزودن ستون را ایجاد کند که باید جدول مرتبا مورد alter گرفتن قرار بگیرد.
  3. ممکن است همیشه برای هر زبانی مطلبی قرار نگیرد و این مورد باعث می‌شود بی جهت فضایی برای آن در نظر گرفته شود.

پی نوشت: با اینکه امروزه بحث فیلدهای sparse Column وجود دارد ولی این فیلد‌ها در هر شرایطی مورد استفاده قرار نمی‌گیرند وبیشتر متعلق به زمانی است که می‌دانیم آن فیلد به شدت کم مورد استفاده قرار می‌گیرد.

پی نوشت دوم : در صورتی که فیلد شما مانند متن مقاله که عموما از نوع داده (varchar(max است استفاده می‌کنید و در صورتی که زبان مورد استفاده قرار نگیرد در خیلی از اوقات بی جهت فیلد‌های Blob ساخته اید که بهینه سازی آن را نیز باید در نظر بگیرید.


در مرحله‌ی بعدبرای رفع مشکلات بالا یک جدول از زبان‌ها، مانند جدول زیر را ایجاد می‌کنیم:
 
 ID کد
 Language زبان
 ISO کد دو رقمی آن زبان
 Flag پرچم آن کشور
بعد از آن هر مقاله برای یک زبان ایجاد خواهد شد؛ چیزی مانند تصویر زیر:

مزایا:

  1. پیاده سازی آسان

معایب:

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


سومین راه حل این است که سه جدول ایجاد کنیم:

یک. جدول زبان‌ها (که بالاتر ایجاد شده بود)

دو . جدول نام مقاله به همراه اطلاعات پایه و فیلدها بی نیاز به چند زبانه بودن

سه : یک جدول که هر دو ستون آن کدهای کلید دو جدول بالا را دارند و فیلدهای چند زبانه در آن وجود دارند.

جدول پایه

 ID کد
 Name نام مقاله
 CreationDate تاریخ ایجاد
 Writer نویسنده
 Visibilty وضعیت نمایش
 جدول مقالات
 LanguageCodeکد زبان
 ArticleID کد مقاله
 CreationDate تاریخ ایجاد
 Visibility وضعیت نمایش مقاله
Title
عنوان مقاله
ContentText
متن مقاله

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

مزایا:

  1. گزارش گیری آسان برای هر دسته مقاله با زبان‌های مختلف و ارتباط و یکپارچگی
  2. آسان در افزودن زبان.

معایب:

  1. ایجاد کوئری‌های پیچیده‌تر و جوین دار که به نسبت روش‌های قبلی کوئری‌ها پیچیده‌تر شده اند.
  2. کدنویسی زیادتر.

استفاده از ساختارهای XML یا JSON برای ذخیره سازی اطلاعات چند زبانه مانند ساختارهای زیر:

XML
<Articles><Article>
this is english text</Article><Article>
این یک متن فارسی است
</Article></Articles>

یا 
<Articles><en-us>
this is english text</en-us><fa-ir>
این یک متن فارسی است
</fa-ir></Articles>
JSON
"Articles":["en-us':{"title":"this is english text","content":" english content"},"fa-ir":{"title":"متن فارسی","content":"محتوای فارسی"}]
ازSQL Server 2005 به بعد از نوع داده xml پشتیبانی می‌شود و در نسخه‌ی 2016 آن نیز پشتیبانی از Json اضافه شده است که حتی شامل اندیکس‌های اختصاصی هم برای این دو نوع می‌باشد.
از مزایای این روشذخیره‌ی همه داده‌ها در یک ستون و یک جدول است و نیازی به ستون‌های اضافه یا جداول اضافه نیست ولی معایب این روش استفاده از کوئری‌های پیچیده‌تر جهت ارتباط و خواندن است.

استفاده از بانک‌های اطلاعاتی NO SQL
در این بانک‌ها دیگر درگیر تعداد ستون‌ها و جنس آن‌ها نیستیم و میتوانیم برای هر مقاله یا محصول، هر تعداد زبان و یا فیلد را که می‌خواهیم، در نظر بگیریم و اضافه کنیم. برای آشنایی بیشتر با این نوع بانک‌ها و انواع آن، مقالات مربوط به nosql را در سایت دنبال کنید.

نکاتی که در یک سایت چند زبانه باید به آن‌ها توجه کرد.

یک . زبان آن صفحه را معرفی کنید: این کار هم به موتورهای جست و جو برای ثبت سایت شما کمک می‌کند و هم برای معلولین که از ابزارهای صفحه خوان استفاده می‌کنند، کمک بزرگی است.در این روش، صفحه خوان‌ها و دستگاه‌های خط بریل که زبان صفحه را تشخیص نمی‌دهند با خواندن کد زبان می‌توانند زبان صفحه را تشخیص دهند.با استفاده از خط زیر میتوانید زبان اصلی صفحه‌ی خود را تنظیم نمایید:
<html lang="en">

اگر از XHTML استفاده می‌کنید خاصیت زیر را فراموش نکنید. دریافت W3C Validation بدون آن امکان پذیر نخواهد بود.
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
با تغییر زبان هر صفحه، باید تنظیم زبان آن تغییر یابد:



دو. چند زبانه بودن صفحه: در بالا یاد گرفتیم که چگونه زبان اصلی صفحه را تنظیم کنیم، ولی گاهی اوقات صفحه به غیر از زبان اصلی، شامل زبان‌های دیگر هم می‌شود؛ مثل نقل قول‌ها یا موارد دیگر. برای این‌کار می‌توانید از خصوصیت lang که در اکثر تگ‌ها پشتیبانی می‌شود، استفاده کنید. مثال پایین یک نقل قول فرانسوی است که ما آن را به خصوصیت lang، جهت تایید زبانش مزین کرده‌ایم:
<blockquote lang=”fr”><p>Le plus grand faible des hommes, c'est l'amour qu'ils ont de la vie.</p></blockquote>

سه. لینک ها:اگر دارید در صفحه‌ای لینک به جایی می‌دهید که متفاوت از زبان شماست، حتما باید زبان صفحه یا سایت مقصد را مشخص کنید. مثلا لینک زیر برای صفحه‌ای است که از یک زبان غیر فرانسوی به یک صفحه‌ی با زبان فرانسوی هدایت می‌شود:
<a href="" hreflang="fr">French</a>

همچنین اگر متن لینک شما هم به زبان فرانسوی باشد خیلی خوب می‌شود که آن را هم بیان کنید و از خاصیت lang و هم hreflang همزمان استفاده کنید:
<a href="" hreflang="fr">Francais</a>

چهار. جهت صفحه: به طور پیش فرض شما این مورد را به خاطر طراحی ظاهر صفحه رعایت می‌کنید ولی خالی از لطف نیست که بگوییم جهت زبان را هم با خصوصیت dir  که مقدار پیش فرضش راست به چپ هست، ذکر کنید. هر چند که عموما وب سایت‌های چند زبانه شامل یک قالب دیگر به صورت راست چین یا چپ چین هم هستند که بر اساس زبان انتخاب شده، قالب خود را تغییر خواهند داد.

پنج. انکودینگ صفحه را مشخص کنید: برای اینکه نحوه‌ی رمزگذاری و رمزگشایی حروف و نمادها مشخص گردد، باید انکودینگ تنظیم شود و حتی برای بعضی از موتورهای جست و جو که ممکن است با وب سایت شما به مشکل بر بخورند. امروزه بیشتر از صفحات یونیکد استفاده می‌شود که سطح وسیعی از کاراکترها را پشتیبانی می‌کند.
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">

HTML5
<meta charset="UTF-8">

شش. اندازه‌ی فونت:
موقعی که یک سایت چند زبانه را طراحی می‌کنید این نکته خیلی مهم هست که بدانید اندازه فونت‌های زبان پیش فرض، برای باقی زبان‌ها مناسب نیستند. به عنوان مثال ممکن است اندازه فونتی برای زبان‌های انگلیسی، فرانسوی و آلمانی مناسب باشد ولی برای زبان‌های فارسی و عربی و چینی و ... مناسب نباشد و خواندن آن سخت شود. به همین جهت یکی از راه‌های حل این مشکل استفاده از قالب css است که وابسته به خصوصیت lang ای است که شما برای صفحه و هر المان یا تگی که از این خصوصیت استفاده می‌کند، تعیین کرده‌اید.
:lang(en) {

font-size: 85%;

font-family: arial, verdana, sans-serif;

}

:lang(zh) {

font-size: 125%;

font-family: helvetica, verdana, sans-serif;

}

خط زیر تعیین میکند که از استایل اول استفاده شود:
<html lang="en">
و خط زیر تعیین می‌کند که از استایل دوم استفاده شود:
<html lang="zh">
البته این کد بالا در مرورگرهای فایرفاکس، اپرا و IE8 به بالا پاسخ می‌دهد. برای سایر مروگرها چون کروم و نسخه‌های پیشین IE باید از شیوه‌ی زیر بهره ببرید:
<body class="english"> or <body class="chinese">

و استایل:
.english {

font-size: 85%;

font-family: arial, verdana, sans-serif;

}

.chinese {

font-size: 125%;

font-family: helvetica, verdana, sans-serif;

}
در این شیوه برای تگ مربوطه یک کلاس با نام آن زبان ایجاد کرده که محتوای آن تنظیمات قلم آن زبان می‌باشد.

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

هشت : زمان را نیز تغییر دهید: یکی از مواردی که در کمتر سایت چند زبانه‌ای به چشم می‌خورد و به نظر بنده می‌تواند بسیار مهم باشد این است که time zone منطقه‌ی هر زبان را بدانید. به عنوان مثال برای مقاله‌ی خود، تاریخ ایجاد را به صورت UTC ذخیره کنید و سپس نمایش را بر اساس زبان یا حتی بهتر و دقیق‌تر از طریق IP کشور مربوطه به دست آورید. برای کاربران ثبت نام شده این تاریخ می‌تواند دقیق‌تر باشد همانند انجمن‌های وی بولتین.


شیوه‌های تشخیص زبان سایت

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

دومین راه، استفاده از IP کاربر مراجعه کننده است تا بر اساس آن زبان مورد نظر را انتخاب کنید.

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

پی نوشت: فراموش نگردد که امکان تغییر زبان همیشه برای کاربر مهیا باشد و طوری نباشد که کاربر در آینده نتواند زبان سایت را تغییر دهد؛ حتی اگر تشخیص خودکار سایت برای زبان فعال باشد.

پی نوشت: در روش‌های بالا بهتر است همان مرتبه‌ی اول اطلاعات را در یک کوکی ذخیره کنید تا مراحل پیگیری راحت‌تر و آسان‌تر شود.


پلاگین‌ها و ابزارهای مدیریت زبان

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

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

پلاگین بعدی International Telephone Input است که  پیاده سازی پلاگین بالا می‌باشد. برای مواردی مفید است که شما نیاز دارید کد تلفنی کشوری را انتخاب کنید.

در مقاله‌های زیر که در سایت جاری است در مورد Globalization و به خصوص استفاده از ریسورس‌ها مطالب خوبی بیان شده است:

قسمت بیست و دوم آموزش MVC که مبحث Globalization را دنبال می‌کند.

قسمت اول از شش قسمت مباحث Globalization  که دنباله‌ی آن را می‌توانید در مقاله‌ی خودش دنبال کنید.

    ‫توسعه سیستم مدیریت محتوای DNTCms - قسمت اول

    $
    0
    0
    قصد داریم طی یک سری مقالات به توسعه یک سیستم مدیریت محتوا بپردازیم. مسلما فاصله‌ی زمانی بین انتشار مقالات این سری، کمی زیاد خواهد بود. ولی سعی خواهیم کرد تا قدم به قدم و با تحلیل و توضیح کافی هر بخش به این هدف برسیم.
    همکاران این قسمت:
    پیشنیاز‌ها:
    در زیر بخش هایی که برای این سیستم تا به امروز در نظر گرفتیم (مسلما با ارائه ایده‌ها و بازخورد‌های دوستان این امکانات دستخوش تغییر خواهند شد) ، به شرح زیر میباشد:
    • انجمن
    • ارتباط دوستی
    • سیستم ترفیع رتبه
    • Themeable
    • سیستم Following
    • صفحات داینامیک
    • سیستم پیام رسانی
    • امکان ساخت گروه‌های شخصی برای انتشار مطالب خود (توسط کاربران) با اعمال دسترسی مختلف
    • پیغام خصوصی
    • وبلاگ
    • نظرسنجی ها
    • مدیریت کاربران با دسترسی‌ها داینامیک
    • اخبار
    • آگهی ها
    در این قسمت مدل‌های مربوط به بخش وبلاگ را بررسی میکنیم. ابتدا قصد داشتیم تا امکان نگارش یک پست توسط چندین کاربر را به طور همزمان، در سیستم قرار دهیم و ایده کار هم که توسط یکی دوستان (محمد شریفی) پیشنهاد شد به صورت زیر بود:
    ابتدا کاربری به عنوان ایجاد کننده اصلی پست، بخش‌ها مختلفی را برای یک پست در نظر بگیرد و هر قسمت را به یک همکار نسبت دهد و با اتمام تمام بخش‌ها و اعلام آن توسط همکاران، کاربر اصلی ایجاد کننده پست بتواند بخش‌ها را باهم ادغام کند و برای آماده انتشار نهایی باشد.
    ولی خب به نظر بنده الان سیستم‌های ارتباطی قدرتمندتری هم هست که همکاران در مورد نگارش یک پست به صورت مشارکتی بپردازند و صرفا در مرحله‌ی انتشار، این کاربران به عنوان کاربران همکار به پست منتشر شده نسبت داده شوند تا در زیر پست مشخصات کامل آنها قابل مشاهد باشد (راه حلی که پیاده سازی خواهیم کرد).
    مدل‌های در نظر گرفته شده برای سیستم وبلاگ به صورت زیر می‌باشد:

    مخزن برچسب ها
    /// <summary>
        /// Represents the lable 
        /// </summary>
        public class Tag
        {
            #region Ctor
            /// <summary>
            /// Create one instance of <see cref="Tag"/>
            /// </summary>
            public Tag()
            {
                Id = SequentialGuidGenerator.NewSequentialGuid();
            }
            #endregion
    
            #region Properties
            /// <summary>
            /// sets or gets Tag's identifier
            /// </summary>
            public virtual Guid  Id { get; set; }
            /// <summary>
            /// sets or gets Tag's name
            /// </summary>
            public virtual string Name { get; set; }
            #endregion
    
            #region NavigationProperties
            /// <summary>
            /// sets or gets Tag's posts
            /// </summary>
            public virtual ICollection<BlogPost> BlogPosts { get; set; }
            #endregion
        }
    کلاس بالا مشخص کننده‌ی مخزن برچسب‌های سیستم ما خواهد بود. جدول حاصل از این مدل با جداول سایر بخش‌ها که نیاز به داشتن برچسب خواهند داشت، ارتباط چند به چند خواهد داشت. برای این منظور همانطور که مشخص است در قسمت NavigationProperties یک لیست از BlogPost معرفی شده است و در ادامه خواهیم دید که لیستی از کلاس Tag هم در کلاس BlogPost معرفی خواهد شد.

    مدل پیش نویس ها
      /// <summary>
        /// Represents the Post's Draft
        /// </summary>
        public  class BlogDraft
        {
            #region Ctor
            /// <summary>
            /// create one instance of <see cref="BlogDraft"/>
            /// </summary>
            public  BlogDraft()
            {
                Id = SequentialGuidGenerator.NewSequentialGuid();
            }
            #endregion
    
            #region Properties
            /// <summary>
            /// gets or sets Id of post's draft
            /// </summary>
            public virtual  Guid Id { get; set; }
            /// <summary>
            /// gets or sets body of post's draft
            /// </summary>
            public virtual  string Body { get; set; }
            /// <summary>
            /// gets or set title of post's draft
            /// </summary>
            public virtual  string Title { get; set; }
            /// <summary>
            /// gets or sets tags of post's draft that seperated using ','
            /// </summary>
            public virtual  string TagNames { get; set; }
            /// <summary>
            /// gets or sets value indicating whether this draft is ready to publish
            /// </summary>
            public virtual  bool IsReadyForPublish { get; set; }
            /// <summary>
            /// ges ro sets DateTime that this draft added
            /// </summary>
            public virtual  DateTime CreatedOn { get; set; }
            /// <summary>
            /// gets or sets information of User-Agent
            /// </summary>
            public virtual string Agent { get; set; }
            /// <summary>
            /// gets or sets date that this draft publish as ready
            /// </summary>
            public virtual DateTime? ReadyForPublishOn { get; set; }
            #endregion
    
            #region NavigationProperties
            /// <summary>
            /// gets or sets Id of user that he is owner of this draft
            /// </summary>
            public virtual  long OwnerId { get; set; }
            /// <summary>
            /// gets or sets user that he is owner of this draft
            /// </summary>
            public virtual  User Owner { get; set; }
            #endregion
        }
    کلاس فوق برای ذخیره سازی پست‌های پیشنویس کاربران در نظر گرفته شده است. شاید بهتر بود این پیش نویس‌ها هم در همان مدل پستی که در ادامه مشاهده می‌کنید ادغام شوند. ولی این یک تصمیم شخصی برای این کار بوده و برای سیستی بزرگی که امکان دارد حذف و درج پیشنویس‌ها زیاد باشد و فرض بر اینکه long هم برای آی دی جواب گو نخواهد بود و نیاز است از Guid ای که به صورت متوالی افزایش میابد به منظور جلوگیری از Fragmentation، استفاده کرد. بقیه فیلد‌ها هم مشخص هستند و نیازی به توضیح اضافی نخواهد بود. مسلما هر کاربر می‌تواند چندین پیشنویس داشته باشد. لذا ارتباط یک به چند مابین کاربر و پیشنویس پست، خواهیم داشت.
    مدل امتیاز دهی کاربران
    /// <summary>
        /// Section of Rating
        /// </summary>
        public enum RatingSection
        {
            News,
            Announcement,
            ForumTopic,
            BlogComment,
            NewsComment,
            PollComment,
            AnnouncementComment,
            ForumPost,
            ...
        }
    
    
        /// <summary>
        /// Represents Rating Record regard by section type for Rating System
        /// </summary>
        public class UserRating
        {
            #region Ctor
            /// <summary>
            /// Create one instance of <see cref="UserRating"/>
            /// </summary>
            public UserRating()
            {
                Id = SequentialGuidGenerator.NewSequentialGuid();
            }
            #endregion
    
            #region Properties
            /// <summary>
            /// gets or sets Id of Rating Record
            /// </summary>
            public virtual Guid Id { get; set; }
            /// <summary>
            /// gets or sets value of rate
            /// </summary>
            public virtual double RatingValue { get; set; }
            /// <summary>
            /// gets or sets Section's Id 
            /// </summary>
            public virtual long SectionId { get; set; }
            /// <summary>
            /// gets or sets Section 
            /// </summary>
            public virtual RatingSection Section { get; set; }
            #endregion
    
            #region Navigation Properties
            /// <summary>
            /// gets or sets user that rate one section
            /// </summary>
            public virtual User Rater { get; set; }
            /// <summary>
            /// gets or sets Rater Id that rate one section
            /// </summary>
            public virtual long RaterId { get; set; }
    
            #endregion
        }
    نوع داده‌ی شمارشی RatingSection مشخص کننده‌ی بخش‌های مختلف سیستم می‌باشد که نیاز به امتیاز دهی دارند. چون سیستم ما یک سیستم بسته می‌باشد و برای امتیاز دهی نیاز به احراز هویت خواهد بود، لذا باید از امتیاز دادن بیش از یک بار برای هر بخش جلوگیری کنیم. برای این کار می‌توان بین تمام بخش‌های موجود ارتباط‌های چند به چند در نظر گرفت. برای مثال یک ارتباط چند به چند بین کاربر و نظرات که مشخص کننده‌ی این است که یک کاربر می‌تواند به چند نظر امتیاز دهد و هر نظر هم میتواند چندین امتیاز دهنده داشته باشد ولی تعداد جداول بالا خواهد رفت و کار آن هم زیاد خواهد شد. برای این منظور میتوان یک جدول به مانند UserRating در نظر گرفت که صرفا با جدول کاربر ما یک ارتباط یک به چند دارد و با استفاده از خصوصیت RatingSection موجود در این کلاس وخصوصیت SectionId و همچنین در نظر گرفتن یک کلید منحصر به فرد بر روی خصوصیت‌های RatingSection ، RaterId و SectionId، از امتیاز دادن چند باره‌ی کاربر به یک بخش جلوگیری کرد.
    /// <summary>
        /// Represent the rating as ComplexType
        /// </summary>
        [ComplexType]
        public class Rating
        {
            /// <summary>
            /// sets or gets total of rating
            /// </summary>
            public virtual double? TotalRating { get; set; }
            /// <summary>
            /// sets or gets rater's count
            /// </summary>
            public virtual long? RatersCount { get; set; }
            /// <summary>
            /// sets or gets average of rating
            /// </summary>
            public virtual double? AverageRating { get; set; }
        }
    کلاس بالا یک ComplexType می‌باشد که در نهایت به هیچ جدولی مپ نخواهد شد و صرفا به منظور کپسوله کردن یکسری فیلد مورد استفاده قرار گرفته است و از این کلاس می‌توان در هر بخش که لازم است امتیاز دهی داشته باشیم، به عنوان یک خصوصیت، استفاده کرد.

    کلاس پایه‌ی محتوا
    /// <summary>
        /// Represents a base class for every content in system
        /// </summary>
        public abstract class BaseContent
        {
            #region Properties
    
            /// <summary>
            /// get or set identifier of record
            /// </summary>
            public virtual long Id { get; set; }
            /// <summary>
            /// gets or sets date of publishing content
            /// </summary>
            public virtual DateTime PublishedOn { get; set; }
            /// <summary>
            /// gets or sets Last Update's Date
            /// </summary>
            public virtual DateTime ModifiedOn { get; set; }
            /// <summary>
            /// gets or sets the blog pot body
            /// </summary>
            public virtual string Body { get; set; }
            /// <summary>
            /// gets or sets the content title
            /// </summary>
            public virtual string Title { get; set; }
            /// <summary>
            /// gets or sets value  indicating Custom Slug
            /// </summary>
            public virtual string SlugUrl { get; set; }
            /// <summary>
            /// gets or sets meta title for seo
            /// </summary>
            public virtual string MetaTitle { get; set; }
            /// <summary>
            /// gets or sets meta keywords for seo
            /// </summary>
            public virtual string MetaKeywords { get; set; }
            /// <summary>
            /// gets or sets meta description of the content
            /// </summary>
            public virtual string MetaDescription { get; set; }
            /// <summary>
            /// gets or sets 
            /// </summary>
            public virtual string FocusKeyword { get; set; }
            /// <summary>
            /// gets or sets value indicating whether the content use CanonicalUrl
            /// </summary>
            public virtual bool UseCanonicalUrl { get; set; }
            /// <summary>
            /// gets or sets CanonicalUrl That the Post Point to it
            /// </summary>
            public virtual string CanonicalUrl { get; set; }
            /// <summary>
            /// gets or sets value indicating whether the content user no Follow for Seo
            /// </summary>
            public virtual bool UseNoFollow { get; set; }
            /// <summary>
            /// gets or sets value indicating whether the content user no Index for Seo
            /// </summary>
            public virtual bool UseNoIndex { get; set; }
            /// <summary>
            /// gets or sets value indicating whether the content in sitemap
            /// </summary>
            public virtual bool IsInSitemap { get; set; }
            /// <summary>
            /// gets or sets a value indicating whether the content comments are allowed 
            /// </summary>
            public virtual bool AllowComments { get; set; }
            /// <summary>
            /// gets or sets a value indicating whether the content comments are allowed for anonymouses 
            /// </summary>
            public virtual bool AllowCommentForAnonymous { get; set; }
            /// <summary>
            /// gets or sets  viewed count by rss
            /// </summary>
            public virtual long ViewCountByRss { get; set; }
            /// <summary>
            /// gets or sets viewed count 
            /// </summary>
            public virtual long ViewCount { get; set; }
            /// <summary>
            /// Gets or sets the total number of  comments
            /// <remarks>The same as if we run Item.Comments.where(a=>a.Status==Status.Approved).Count()
            /// We use this property for performance optimization (no SQL command executed)
            /// </remarks>
            /// </summary>
            public virtual int ApprovedCommentsCount { get; set; }
            /// <summary>
            /// Gets or sets the total number of  comments
            /// <remarks>The same as if we run Item.Comments.where(a=>a.Status==Status.UnApproved).Count()
            /// We use this property for performance optimization (no SQL command executed)</remarks></summary>
            public virtual int UnApprovedCommentsCount { get; set; }
            /// <summary>
            /// gets or sets value  indicating whether the content is logical deleted or hidden
            /// </summary>
            public virtual bool IsDeleted { get; set; }
            /// <summary>
            /// gets or sets rating complex instance
            /// </summary>
            public virtual Rating Rating { get; set; }
            /// <summary>
            /// gets or sets value indicating whether the content show with rssFeed
            /// </summary>
            public virtual bool ShowWithRss { get; set; }
            /// <summary>
            /// gets or sets value indicating maximum days count that users can send comment
            /// </summary>
            public virtual int DaysCountForSupportComment { get; set; }
            /// <summary>
            /// gets or sets information of User-Agent
            /// </summary>
            public virtual string Agent { get; set; }
            /// <summary>
            /// gets or sets icon name with size 200*200 px for snippet 
            /// </summary>
            public virtual string SocialSnippetIconName { get; set; }
            /// <summary>
            /// gets or sets title for snippet
            /// </summary>
            public virtual string SocialSnippetTitle { get; set; }
            /// <summary>
            /// gets or sets description for snippet
            /// </summary>
            public virtual string SocialSnippetDescription { get; set; }
            /// <summary>
            /// gets or sets body of content's comment
            /// </summary>
            public virtual byte[] RowVersion { get; set; }
            /// <summary>
            /// gets or sets name of tags seperated by comma that assosiated with this content fo increase performance
            /// </summary>
            public virtual string TagNames { get; set; }
            /// <summary>
            /// gets or sets counter for Content's report
            /// </summary>
            public virtual int ReportsCount { get; set; }
            #endregion
    
            #region NavigationProperties
    
            /// <summary>
            /// get or set user that create this record
            /// </summary>
            public virtual User Author { get; set; }
    
            /// <summary>
            /// gets or sets Id of user that create this record
            /// </summary>
            public virtual long AuthorId { get; set; }
            /// <summary>
            /// get or set  the tags integrated with content
            /// </summary>
            public virtual ICollection<Tag> Tags { get; set; }
            #endregion
    
        }

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

    نکته‌ای که وجود دارد فیلد‌های ApprovedCommentsCount  UnApprovedCommentsCount و TagNames می‌باشند که هنگام درج نظر جدید باید تعداد نظرات ذخیره شده را  ویرایش کنیم و هنگام ویرایش خود پست یا خبر با ... و یا حتی ویرایش خود تگ یا حذف آن تگ باید TagNames که لیست برچسب‌های محتوا را به صورت جدا شده با (,) از هم دیگر می‌باشد، ویرایش کنیم (جای بحث دارد).

    مشخص است که هر یک از مطالب منتشر شده در بخش‌های وبلاگ، اخبار، نظرسنجی و آگهی‌ها، یک کابر ایجاد کننده (Author نامیده‌ایم) خواهد داشت و هر کاربر هم می‌تواند چندین مطلب را ایجاد کند. لذا رابطه‌ی یک به چند بین تمام این بخش‌ها مذکور و کاربر ایجاد خواهد شد.

    مدل LinkBack

     /// <summary>
        /// Represents link for implemention linkback
        /// </summary>
        public class LinkBack
        {
    
            #region Ctor
            /// <summary>
            /// create one instance of <see cref="LinkBack"/>
            /// </summary>
            public LinkBack()
            {
                CreatedOn = DateTime.Now;
                Id = SequentialGuidGenerator.NewSequentialGuid();
            }
            #endregion
    
            #region Properties
            /// <summary>
            /// gets or sets link's Id
            /// </summary>
            public virtual Guid Id { get; set; }
            /// <summary>
            /// gets or sets text for show Link
            /// </summary>
            public virtual string Title { get; set; }
            /// <summary>
            /// gets or sets link's address 
            /// </summary>
            public virtual string Url { get; set; }
            /// <summary>
            /// gets or set value indicating whether this link is internal o external
            /// </summary>
            public virtual LinkBackType Type { get; set; }
            /// <summary>
            /// gets or sets date that this record is added
            /// </summary>
            public virtual DateTime CreatedOn { get; set; }
            #endregion
    
            #region NavigationProperties
            /// <summary>
            /// gets or sets Post that associated
            /// </summary>
            public virtual BlogPost Post { get; set; }
            /// <summary>
            /// gets or sets id of Post that associated
            /// </summary>
            public virtual long PostId { get; set; }
            #endregion
        }
    
    
     /// <summary>
        /// represents Type of ReferrerLinks
        /// </summary>
        public enum LinkBackType
        {
            /// <summary>
            /// Internal link
            /// </summary>
            Internal,
            /// <summary>
            /// External Link
            /// </summary>
            External
        }

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

    مدل پست ها

      /// <summary> 
        /// Represents a blog post
        /// </summary>
        public  class BlogPost : BaseContent
        {
            #region Ctor
            /// <summary>
            /// Create one Instance of <see cref="BlogPost"/>
            /// </summary>
            public BlogPost()
            {
                Rating = new Rating();
                PublishedOn = DateTime.Now;
            }
            #endregion
    
           #region Properties
            /// <summary>
            /// gets or sets Status of LinkBack Notifications
            /// </summary>
            public virtual LinkBackStatus LinkBackStatus { get; set; }
            #endregion
    
            #region NavigationProperties
    
            /// <summary>
            /// get or set  blog post's Reviews
            /// </summary>
            public virtual ICollection<BlogComment> Comments { get; set; }
            /// <summary>
            /// get or set collection of links that reference to this blog post
            /// </summary>
            public virtual ICollection<LinkBack> LinkBacks { get; set; }
            /// <summary>
            /// get or set Collection of Users that Contribute on this post
            /// </summary>
            public virtual ICollection<User> Contributors  { get; set; }
            #endregion
        }
    
    
      /// <summary>
        /// Represents Status for ReferrerLinks
        /// </summary>
        public enum  LinkBackStatus
        {
            [Display(Name ="غیرفعال")]
            Disable,
            [Display(Name = "فعال")]
            Enable,
            [Display(Name = "لینک‌ها داخلی")]
            JustInternal,
            [Display(Name = "لینک‌ها خارجی")]
            JustExternal
        }
     کلاس بالا نشان دهنده‌ی مدل پست‌های ما در سیستم می‌باشد که بیشتر خصوصیات خود را از کلاس پایه‌ی BaseContent به ارث برده و لازم به تکرار آنها نیست. به علاوه یکسری خصوصیات دیگر، از جلمه‌ی آنها یک لیست از کلاس LinkBack، لیستی از کاربران مشارکت کننده به منظور نمایش مشخصات آنها در زیر پست و لیستی از کلاس BlogComment که نشان دهنده‌ی نظرات پست می‌باشد، هم خواهد داشت. خصوصیتی از نوع داده‌ی LinkBackStatus هم صرفا به منظور تنظیمات مدیریتی در نظر گرفته شده است. 

    کلاس پایه نظرات
      /// <summary>
        /// Represents a base class for every comment in system 
        /// </summary>
    
        public abstract class BaseComment
        {
            #region Properties
            /// <summary>
            /// get or set identifier of record
            /// </summary>
            public virtual long Id { get; set; }
            /// <summary>
            /// gets or sets date of creation 
            /// </summary>
            public virtual DateTime CreatedOn { get; set; }
            /// <summary>
            /// gets or sets displayName of this comment's Creator if  he/she is Anonymous
            /// </summary>
            public virtual string CreatorDisplayName { get; set; }
            /// <summary>
            /// gets or sets body of blog post's comment
            /// </summary>
            public virtual string Body { get; set; }
            /// <summary>
            /// gets or sets body of blog post's comment
            /// </summary>
            public virtual Rating Rating { get; set; }
            /// <summary>
            /// gets or sets informations of agent
            /// </summary>
            public virtual string UserAgent { get; set; }
            /// <summary>
            /// gets or sets siteUrl of Creator if he/she is Anonymous
            /// </summary>
            public virtual string SiteUrl { get; set; }
            /// <summary>
            /// gets or sets Email of Creator if he/she is anonymous
            /// </summary>
            public virtual string Email { get; set; }
            /// <summary>
            /// gets or sets status of comment
            /// </summary>
            public virtual CommentStatus Status { get; set; }
            /// <summary>
            /// gets or sets Ip Address of Creator
            /// </summary>
            public virtual string CreatorIp { get; set; }
            /// <summary>
            /// gets or sets datetime that is modified
            /// </summary>
            public virtual DateTime? ModifiedOn { get; set; }
            /// <summary>
            /// gets or sets counter for report this comment
            /// </summary>
            public virtual int ReportsCount { get; set; }
            #endregion
    
            #region NavigationProperties
    
            /// <summary>
            /// get or set user that create this record
            /// </summary>
            public virtual User Creator { get; set; }
    
            /// <summary>
            /// get or set Id of user that create this record
            /// </summary>
            public virtual long? CreatorId { get; set; }
            #endregion
        }
    
    public enum CommentStatus
        {
            /* 0 - approved, 1 - pending, 2 - spam, -1 - trash */
            [Display(Name = "تأیید شده")]
            Approved = 0,
            [Display(Name = "در انتظار بررسی")]
            Pending = 1,
            [Display(Name = "جفنگ")]
            Spam = 2,
            [Display(Name = "زباله دان")]
            Trash = -1
        }
    به مانند BaseContent که برای کپسوله کردن خصوصیات تکراری و نه برای اعمال ارث بری TPH یا TPT، کلاس BaseComment را در نظر گرفته‌ایم. نکته‌ی مهم این است که هر نظری یک درج کننده دارد. ولی اگر فیلد AllowCommentForAnonymous مربوط به مطلب True باشد، این امکان وجود خواهد داشت تا کاربران احراز هویت نشده نیز نظر ارسال کنند. لذا CreatorId در کلاس BaseComment به صورت Nullable در نظر گرفته شده است و یک سری خصوصیت‌ها از قبیل SiteUrl ، CreatorDisplayName ، Email برای کاربران احراز هویت نشده در نظر گرفته شده است. نوع شمارشی CommentStatus نیز برای اعمال مدیریتی در نظر گرفته شده است.
    مدل نظرات پست ها
    /// <summary>
        /// Represents a blog post's comment
        /// </summary>
        public class BlogComment : BaseComment
        {
            #region Ctor
            /// <summary>
            /// Create One Instance for <see cref="BlogComment"/>
            /// </summary>
            public BlogComment()
            {
                Rating = new Rating();
                CreatedOn = DateTime.Now;
            }
            #endregion
    
            #region NavigationProperties
            /// <summary>
            /// gets or sets BlogComment's identifier for Replying and impelemention self referencing
            /// </summary>
            public virtual long? ReplyId { get; set; }
            /// <summary>
            /// gets or sets blog's comment for Replying and impelemention self referencing
            /// </summary>
            public virtual BlogComment Reply { get; set; }
            /// <summary>
            /// get or set collection of blog's comment for Replying and impelemention self referencing
            /// </summary>
            public virtual ICollection<BlogComment> Children { get; set; }
            /// <summary>
            /// gets or sets post that this comment sent to it
            /// </summary>
            public virtual BlogPost Post { get; set; }
            /// <summary>
            /// gets or sets post'Id that this comment sent to it
            /// </summary>
            public virtual long PostId { get; set; }
            #endregion
        }
    کلاس بالا مشخص کننده‌ی نظرات پست‌های وبلاگ می‌باشند که بیشتر خصوصیت‌های خود را از کلاس پایه‌ی BaseComment به ارث برده است و همچنین ساختار سلسله مراتبی آن نیز  قابل مشاهده است. 
    برای سیستم Report یا همان گزارش خطا هم سیستمی شبیه به امتیاز دهی ستاره‌ای در نظر گرفته ایم. برای اینکه مقاله خیلی طولانی شد، بهتر است در مقاله‌ای جدا کار را ادامه داد.
    نتیجه این قسمت

    ‫توسعه سیستم مدیریت محتوای DNTCms - قسمت دوم

    $
    0
    0

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

    مدل گزارش دهی

        /// <summary>    
        /// Repersents a Report template for every cms section
        /// </summary>
        public class Report
        {
            #region Ctor
            /// <summary>
            /// Create one instance for <see cref="Report"/>
            /// </summary>
            public Report()
            {
                ReportedOn = DateTime.Now;
                Id = SequentialGuidGenerator.NewSequentialGuid();
            }
            #endregion
    
            #region Properties
            /// <summary>
            /// gets or sets identifier for Report
            /// </summary>
            public virtual Guid Id { get; set; }
            /// <summary>
            /// gets or sets reason of report
            /// </summary>
            public virtual string Reason { get; set; }
            /// <summary>
            /// gets or sets section that is reported
            /// </summary>
            public virtual ReportSection Section { get; set; }
            /// <summary>
            /// gets or sets sectionid that is reported
            /// </summary>
            public virtual long SectionId { get; set; }
            /// <summary>
            /// gets or sets type of report
            /// </summary>
            public virtual ReportType Type{ get; set; }
            /// <summary>
            /// gets or sets report's datetime
            /// </summary>
            public virtual DateTime ReportedOn { get; set; }
            /// <summary>
            /// indicate this report is read by admin
            /// </summary>
            public virtual bool IsRead { get; set; }
            #endregion
    
            #region NavigationProperties
            /// <summary>
            /// gets or sets id of user that is reporter
            /// </summary>
            public virtual long ReporterId { get; set; }
            /// <summary>
            /// gets or sets id of user that is reporter
            /// </summary>
            public virtual User Reporter { get; set; }
            #endregion
        }
    
    /// <summary>
        /// Represents Report Section
        /// </summary>
       public  enum  ReportSection
        {
            News,
            Poll,
            Announcement,
            ForumTopic,
            BlogComment,
            BlogPost,
            NewsComment,
            PollComment,
            AnnouncementComment,
            ForumPost,
            User,
          ...
        }
    
    /// <summary>
        /// Represents Type of Report
        /// </summary>
        public enum  ReportType
        {
            Spam,
            Abuse,
            Advertising,
           ...
        }

    قصد داریم در این سیستم به کاربران خاصی دسترسی گزارش دادن در بخش‌های مختلف را بدهیم. این دسترسی‌ها در بخش تنظیمات سیستم قابل تغییر خواهند بود (برای مثال براساس امتیاز ، براساس تعداد پست و ... ) . این امکان می‌تواند برای مدیریت سیستم مفید باشد.
    برای سیستم گزارش دهی به مانند سیستم امتیاز دهی عمل خواهیم کرد. در کلاس Report، خصوصیت ReportSection  از نوع داده‌ی شمارشی می‌باشد که در بالا تعریف آن نیز آماده است و مشخص کننده‌ی بخش‌هایی می‌باشد که لازم است امکان گزارش دهی داشته باشند. خصوصیت Type هم که از نوع شمارشی ReportType می‌باشد، مشخص کننده‌ی نوع گزارشی است که داده شده است. 
    علاوه بر نوع گزارش، می‌توان دلیل گزارش را هم ذخیره کرد که برای این منظور خصوصیت Reason در نظر گرفته شده‌است. خصوصیت IsRead هم برای مدیریت این گزارشات در پنل مدیریت در نظر گرفته شده است. اگر در مقاله‌ی قبل دقت کرده باشید، متوجه وجود خصوصیتی به نام ReportsCount در کلاس BaseContent و  BaseComment خواهید شد که برای نشان دادن تعداد گزارش‌هایی است که برای آن مطلب یا نظر داده شده است، استفاده می‌شود.

    کلاس پایه فایل‌های ضمیمه

     /// <summary>
        /// Represents a base class for every attachment
        /// </summary>
        public abstract class BaseAttachment
        {
            #region Ctor
    
            public BaseAttachment()
            {
                Id = SequentialGuidGenerator.NewSequentialGuid();
                AttachedOn = DateTime.Now;
            }
            #endregion
    
            #region Properties
            /// <summary>
            /// sets or gets identifier for attachment
            /// </summary>
            public virtual Guid Id { get; set; }
            /// <summary>
            /// sets or gets name for attachment
            /// </summary>
            public virtual string FileName { get; set; }
            /// <summary>
            /// sets or gets type of attachment
            /// </summary>
            public virtual string ContentType { get; set; }
            /// <summary>
            /// sets or gets size of attachment
            /// </summary>
            public virtual long Size { get; set; }
            /// <summary>
            /// sets or gets Extention of attachment
            /// </summary>
            public virtual string Extension { get; set; }
            /// <summary>
            /// sets or gets bytes of data
            /// </summary>
            //public byte[] Data { get; set; }
            /// <summary>
            /// sets or gets Creation Date
            /// </summary>
            public virtual DateTime AttachedOn { get; set; }
            /// <summary>
            /// gets or sets counts of download this file
            /// </summary>
            public virtual long DownloadsCount { get; set; }
            /// <summary>
            /// gets or sets datetime that is modified
            /// </summary>
            public virtual DateTime ModifiedOn { get; set; }
            /// <summary>
            /// gets or sets section that this file attached there
            /// </summary>
            public virtual AttachmentSection Section { get; set; }
            /// <summary>
            /// gets or sets information of user agent 
            /// </summary>
            public virtual string Agent { get; set; }
            #endregion
    
            #region NavigationProperties
            /// <summary>
            /// sets or gets identifier of attachment's owner
            /// </summary>
            public virtual long OwnerId { get; set; }
            /// <summary>
            /// sets or gets identifier of attachment's owner
            /// </summary>
            public virtual User Owner { get; set; }
            #endregion
        }
    
    
    
        public enum  AttachmentSection
        {
            News,
            Announcement,
            ForumTopic,
            Conversation,
            BlogComment,
            NewsComment,
            PollComment,
            AnnouncementComment,
            ForumPost,
            BlogPost,
            Group,
            ...
        }

    کلاس بالا اکثر خصوصیات لازم برای مدل Attachment ما را در خود دارد. قصد داریم ازارث بری TPHبرای مدیریت فایل‌های ضمیمه استفاده کنیم. در سیستم بسته‌ی ما، تنها کاربران احراز هویت شده می‌توانند فایل ضمیمه کنند و برای همین منظور OwnerId را که همان ارسال کننده‌ی فایل می‌باشد، به صورت Nullable در نظر نگرفته‌ایم.
    یک سری از مشخصات که نیاز به توضیح اضافی ندارند، ولی خصوصیت AttachmentSection که از نوع شمارشی AttachmentSection است، برای دسترسی راحت کاربر به فایل‌های ارسالی خود در پنل کاربری در نظر گرفته شده است. برای بخش‌های (وبلاگ - اخبار - نظرسنجی‌ها - آگهی‌ها - انجمن)  که نیاز به Privacy خاصی نیست و احراز هویت کفایت می‌کند، مدل زیر را در نظر گرفته ایم:

    مدل فایل‌های ضمیمه عمومی

     /// <summary>
        /// Repersent the attachment for file
        /// </summary>
        public class Attachment : BaseAttachment
        {
        }
      مدل بالا صرفا برای بخش‌های مذکور کفایت خواهد کرد. در ادامه مقالات، برای بخش‌هایی مانند پیغام خصوصی، گروه‌هایی که کاربران ایجاد می‌کنند، برای انتشار تجربیات خود و هر بخشی که اضافه شود و نیاز به Privacy داشته باشد، نیاز خواهند بود تا مدل Attachment آنها با خود بخش هم در ارتباط باشد و تمام خصوصیت آنها که اکثرا کلید خارجی خواهند بود به صورت Nullable تعریف شوند.
    مدل اخبار
     /// <summary>
        /// Represents one news item 
        /// </summary>
        public class NewsItem : BaseContent
        {
            #region Ctor
            /// <summary>
            /// create one instance of <see cref="NewsItem"/>
            /// </summary>
            public NewsItem()
            {
                Rating = new Rating();
                PublishedOn = DateTime.Now;
            }
            #endregion
    
            #region Properties
            /// <summary>
            /// indicating that this news show on sidebar
            /// </summary>
            public virtual bool ShowOnSideBar { get; set; }
            /// <summary>
            /// indicate this NewsItem is approved by admin if NewsItem.Moderate==true
            /// </summary>
            public virtual bool IsApproved { get; set; }
    
            #endregion
    
            #region NavigationProperties
    
            /// <summary>
            /// gets or sets  newsitem's Reviews
            /// </summary>
            public ICollection<NewsComment> Comments { get; set; }
    
            #endregion
        }

                      کلاس بالا نشان دهنده‌ی اشتراک‌های ما خواهند بود. این مدل ما هم از کلاس پایه‌ی BaseContent بحث شده در مقاله‌ی قبل، ارث بری کرده و علاوه بر آن دو خصوصیت دیگر تحت عنوان IsApproved برای اعمال مدیریتی در نظر گرفته شده است (اگر در بخش تنظیمات سیستم اخبار، مدیریت تصمیم گرفته باشد تا اخبار جدید به اشتراک گذاشته شده با تأیید مدیریتی منتشر شوند) و خصوصیت ShowOnSideBar هم به عنوان یک تنظیم مدیریتی برای خبر خاصی در نظر گرفته شده که لازم است به صورت sticky در سایدبار نمایش داده شود.
    برای اخبار نیز امکان ارسال نظر خواهیم داشت که برای این منظور لیستی از مدل زیر (NewsComment) در مدل بالا تعریف شده است .

    مدل نظرات اخبار 

     public class NewsComment : BaseComment
        {
            #region Ctor
            public NewsComment()
            {
                Rating = new Rating();
                CreatedOn = DateTime.Now;
    
            }
            #endregion
    
            #region NavigationProperties
    
            /// <summary>
            /// gets or sets body of blog NewsItem's comment
            /// </summary>
            public virtual long? ReplyId { get; set; }
            /// <summary>
            /// gets or sets body of blog NewsItem's comment
            /// </summary>
            public virtual NewsComment Reply { get; set; }
            /// <summary>
            /// gets or sets body of blog NewsItem's comment
            /// </summary>
            public virtual ICollection<NewsComment> Children { get; set; }
            /// <summary>
            /// gets or sets NewsItem that this comment sent to it
            /// </summary>
            public virtual NewsItem NewsItem { get; set; }
            /// <summary>
            /// gets or sets NewsItem'Id that this comment sent to it
            /// </summary>
            public virtual long NewsItemId { get; set; }
            #endregion
        }

                               مدل بالا نشان دهنده‌ی نظرات داده شده‌ی برای اخبار می‌باشند که از کلاس BaseComment بحث شده در مقاله‌ی قبل ارث بری کرده و ساختار درختی آن نیز مشخص است و همچنین برای اعمال ارتباط یک به چند نیز خصوصیتی تحت عنوان NewsItem  با کلید NewsItemId در این کلاس در نظر گرفته شده است.

    مدل‌های پیغام خصوصی
    /// <summary>
        /// Indicate one conversation
        /// </summary>
        public class Conversation
        {
            #region Ctor
            /// <summary>
            /// create one instance of <see cref="Conversation"/>
            /// </summary>
            public Conversation()
            {
                Id = SequentialGuidGenerator.NewSequentialGuid();
                SentOn = DateTime.Now;
            }
            #endregion
    
            #region Properties
            /// <summary>
            /// gets or sets identifier of record
            /// </summary>
            public virtual Guid Id { get; set; }
            /// <summary>
            /// represents this conversaion is seen
            /// </summary>
            public virtual bool IsRead { get; set; }
            /// <summary>
            /// gets or sets subject of this conversation
            /// </summary>
            public virtual string Subject { get; set; }
            /// <summary>
            /// gets or sets Date that this record added
            /// </summary>
            public virtual DateTime SentOn { get; set; }
            /// <summary>
            /// indicate this record deleted by sender
            /// </summary>
            public virtual bool DeletedBySender { get; set; }
            /// <summary>
            /// indicate this record deleted by receiver
            /// </summary>
            public virtual bool DeletedByReceiver { get; set; }
            /// <summary>
            /// gets or sets Messagescount that Unread  by sender of this conversation
            /// </summary>
            public virtual int UnReadSenderMessagesCount { get; set; }
            /// <summary>
            /// gets or sets Messagescount that Unread  by receiver of this conversation
            /// </summary>
            public virtual int UnReadReceiverMessagesCount { get; set; }
            /// <summary>
            /// gets or sets Messagescount of this conversation for increase performance
            /// </summary>
            public virtual int MessagesCount { get; set; }
            #endregion
    
            #region NavigationProperties
            /// <summary>
            /// gets or sets if of  user that start this conversation
            /// </summary>
            public virtual long SenderId { get; set; }
            /// <summary>
            /// gets or sets user that start this conversation
            /// </summary>
            public virtual User Sender { get; set; }
            /// <summary>
            /// gets or sets id of  user that is recipient
            /// </summary>
            public virtual long ReceiverId { get; set; }
            /// <summary>
            /// gets or sets   user that is recipient
            /// </summary>
            public virtual User Receiver { get; set; }
            /// <summary>
            /// get or set Messages of this conversation
            /// </summary>
            public virtual ICollection<ConversationReply> Messages { get; set; }
            /// <summary>
            /// get or set Attachments that attached in this conversation
            /// </summary>
            public virtual ICollection<ConversationAttachment> Attachments { get; set; }
            #endregion

    مدل بالا نشان دهنده‌ی گفتگوی بین دو کاربر می‌باشد. هر گفتگو امکان دارد با موضوع خاصی ایجاد شود و مسلما یک کاربر به‌عنوان دریافت کننده و کاربر دیگری بعنوان ارسال کننده خواهد بود. برای این منظور خصوصیات Receiver و Sender که از نوع User هستند را در این کلاس در نظر گرفته‌ایم.
    خصوصیات DeletedBySender و DeletedByReceiver هم برای این در نظر گفته شده‌اند که اگر یک طرف این گفتگو خواهان حذف آن باشد، برای آن کاربر حذف نرم انجام دهیم و فعلا برای کاربر مقابل قابل دسترسی باشد.
    UnReadSenderMessagesCount و UnReadReceiverMessagesCount هم برای بالا بردن کارآیی سیستم در نظر گفته شده‌اند و در واقع تعداد پیغام‌های خوانده نشده در یک گفتگو به صورت متمایز برای هر دو طرف، ذخیره می‌شود. هر گفتگو شامل یکسری پیغام رد و بدل شده خواهد بود که بدین منظور لیستی از ConversationReply‌ها را در مدل بالا تعریف کرده‌ایم.
    در هر گفتگو یکسری فایل هم ممکن است ضمیمه شود ، برای این منظور هم یک لیستی از کلاس ConversationAttachment در مدل گفتگو تعریف شده است که در ادامه پیاده سازی کلاس ConversationAttachment را هم خواهیم دید.   
    مدل  ConversationReply به شکل زیر می‌باشد:

      /// <summary>
        /// Represents One Reply to Conversation
        /// </summary>
        public class ConversationReply
        {
            #region Ctor
            /// <summary>
            /// create one instance of <see cref="ConversationReply"/>
            /// </summary>
            public ConversationReply()
            {
                Id = SequentialGuidGenerator.NewSequentialGuid();
                SentOn = DateTime.Now;
            }
            #endregion
    
            #region Properties
            /// <summary>
            /// gets or sets identifier of record
            /// </summary>
            public virtual Guid Id { get; set; }
            /// <summary>
            /// represents this conversaionReply is seen
            /// </summary>
            public virtual bool IsRead { get; set; }
            /// <summary>
            /// gets or sets body of this conversationReply
            /// </summary>
            public virtual string Body { get; set; }
            /// <summary>
            /// gets or sets Date that this record added
            /// </summary>
            public virtual DateTime SentOn { get; set; }
            #endregion
    
            #region NavigationProperties
            /// <summary>
            /// gets or sets  Parent's Id Of this ConversationReply
            /// </summary>
            public virtual Guid? ParentId { get; set; }
            /// <summary>
            /// gets or sets Parent Of this ConversationReply
            /// </summary>
            public virtual ConversationReply Parent { get; set; }
            /// <summary>
            /// get or set Children Of this ConversationReply
            /// </summary>
            public virtual ICollection<ConversationReply> Children { get; set; }
            /// <summary>
            /// gets or sets if of  user that start this conversationReply
            /// </summary>
            public virtual long SenderId { get; set; }
            /// <summary>
            /// gets or sets user that start this conversationReply
            /// </summary>
            public virtual User Sender { get; set; }
            /// <summary>
            /// gets or sets Conversation that this message sent in it 
            /// </summary>
            public virtual Conversation Conversation{ get; set; }
            /// <summary>
            /// gets or sets Id of Conversation that this message sent in it 
            /// </summary>
            public virtual Guid ConversationId { get; set; }
            #endregion
        }

    مدل بالا نشان دهنده‌ی پیغام‌های داده شده در یک گفتگو با موضوعی خاص می‌باشد. ساختار درختی آن هم برای ایجاد امکان جواب دهی برای پیغام‌ها در نظر گرفته شده است (الزامی نیست). هر پیغام در یک گفتگو ارسال شده و یک ارسال کننده نیز دارد که برای این منظور به ترتیب دو خصوصیت Conversation از نوع کلاس Conversation و Sender از نوع User در نظر گرفته‌ایم.  
    با توجه به وجود Privacy در گفتگو نیاز است تا مدل فایل ضمیمه بخش گفتگو‌ها به شکل زیر باشد:

    /// <summary>
        /// Represents the attachment That attached in Conversation
        /// </summary>
        public class ConversationAttachment : BaseAttachment
        {
            #region NavigationProperties
    
            public virtual Conversation Conversation { get; set; }
            public virtual Guid? ConversationId { get; set; }
            #endregion
        }

    همانطور که کمی بالاتر بحث شد، قصد اعمال ارث بری TPHرا برای مدیریت فایل‌های ضمیمه داریم. برای این منظور مدل بالا نیز از کلاس BaseAttachment ارث بری کرده و دو خصوصیت اضافه هم برای اعمال ارتباط یک به چند با گفتگو خواهد داشت. توجه کنید که ConversationId به صورت Nullable تعریف شده‌است.

    نتیجه این قسمت

    ‫توسعه سیستم مدیریت محتوای DNTCms - قسمت سوم

    $
    0
    0

    در این قسمت به پیاده سازی و توضیح مدل‌های انجمن خواهیم پرداخت. قبل از شروع پیشنهاد می‌کنم مقالات قبلیرا مطالعه کنید.
    همکاران این قسمت:
    سلمان معروفی 
    سید مجتبی حسینی 
    پیشنیاز این قسمت:
    مقالات SQL Antipattern 

    سعی کردیم چندین پروژه‌ی سورس باز را هم بررسی کنیم و در نهایت کاملترین و بهترین روش را پیاده سازی کنیم. NForum ، MyBB ، MVCForum، بخش CMS مربوط به SmartStore و ساختار دیتابیس StackOverFlow ازجمله‌ی آنها هستند.

    ساختار انجمن‌ها اغلب به شکل سلسله مراتبی می‌باشد و این مورد در دسته بندی آنها خیلی مفید خواهد بود. صرف اینکه بتوان برای این مورد یک مدل خود ارجاع در نظر گرفت کاری خاصی ندارد. ولی مشکل از آنجا شروع می‌شود که بخواهیم برای انجمن هایمان مدیرانی هم تعیین کنیم یا فقط تا عمق مشخصی را واکشی کنیم و خیلی چالش برانگیزتر از اینها، اگر لازم باشد دسترسی‌های مدیران یک انجمن قابلیت اعمال بر زیرشاخه‌ها را داشته باشد و در مقابل زیرشاخه‌ها هم بتوانند از این ارث بری ممانعت کنند و از این نوع چالش‌های شیرین دیگر.
    مدل انجمن
      /// <summary>
        /// Represents the Forum
        /// </summary>
        public class Forum
        {
    
            #region Properties
            /// <summary>
            /// gets or sets Id that Identify Forum
            /// </summary>
            public virtual long Id { get; set; }
            /// <summary>
            /// gets or sets Forum's title
            /// </summary>
            public virtual string Title { get; set; }
            /// <summary>
            /// gets or sets Description of forum
            /// </summary>
            public virtual string Description { get; set; }
            /// <summary>
            /// gets or sets value indicating Custom Slug
            /// </summary>
            public virtual string SlugUrl { get; set; }
            /// <summary>
            /// gets or sets order for display forum
            /// </summary>
            public virtual long DisplayOrder { get; set; }
            /// <summary>
            /// Indicating This Forum is Active or Not
            /// </summary>
            public virtual bool IsActive { get; set; }
            /// <summary>
            /// Indicating This Forum is Close or Not
            /// </summary>
            public virtual bool IsClose { get; set; }
            /// <summary>
            /// Indicating This Forum is Private or Not
            /// </summary>
            public virtual bool IsPrivate { get; set; }
            /// <summary>
            /// sets or gets password for login to Private forums
            /// </summary>
            public virtual string PasswordHash { get; set; }
            /// <summary>
            /// sets or gets depth of forum in tree structure of forums
            /// </summary>
            public virtual int Depth { get; set; }
            /// <summary>
            /// sets or gets Count of  posts That they are Approved
            /// </summary>
            public virtual long ApprovedPostsCount { get; set; }
            /// <summary>
            /// sets or gets Count of  topics That they are Approved
            /// </summary>
            public virtual long ApprovedTopicsCount { get; set; }
            /// <summary>
            /// Gets or sets the id of last topic
            /// </summary>
            public virtual long LastTopicId { get; set; }
            /// <summary>
            /// gets or sets date of creation of last topic
            /// </summary>
            public virtual DateTime? LastTopicCreatedOn { get; set; }
            /// <summary>
            /// gets or sets title of last topic
            /// </summary>
            public virtual string LastTopicTitle { get; set; }
            /// <summary>
            /// gets or sets creator of last topic
            /// </summary>
            public virtual string LastTopicCreator { get; set; }
            /// <summary>
            /// gets or sets id of creator that create last topic
            /// </summary>
            public virtual long LastTopicCreatorId { get; set; }
            /// <summary>
            /// Indicate in this Forum Moderate Topics Before Display 
            /// </summary>
            public virtual bool ModerateTopics { get; set; }
            /// <summary>
            /// Indicate in this Forum Moderate Posts Before Dipslay
            /// </summary>
            public virtual bool ModeratePosts { get; set; }
            /// <summary>
            /// gets or sets Count of posts that they are UnApproved
            /// </summary>
            public virtual long UnApprovedPostsCount { get; set; }
            /// <summary>
            /// gets or sets Count of topics that they are UnApproved
            /// </summary>
            public virtual long UnApprovedTopicsCount { get; set; }
            /// <summary>
            /// gets or sets Rowversion
            /// </summary>
            public virtual byte[] RowVersion { get; set; }
            /// <summary>
            /// gets or sets icon name with size 200*200 px for snippet 
            /// </summary>
            public virtual string SocialSnippetIconName { get; set; }
            /// <summary>
            /// gets or sets title for snippet
            /// </summary>
            public virtual string SocialSnippetTitle { get; set; }
            /// <summary>
            /// gets or sets description for snippet
            /// </summary>
            public virtual string SocialSnippetDescription { get; set; }
            /// <summary>
            /// gets or sets path for tree structure antipattern (1/3/4/23)
            /// </summary>
            public virtual string Path { get; set; }
            /// <summary>
            /// Indicate this forum inherit moderators from parent forum
            /// </summary>
            public virtual bool IsModeratorsInherited { get; set; }
            /// <summary>
            /// gets or set datetime that Last Post is Created In this forum. used for ForumTracking 
            /// </summary>
            public virtual DateTime? LastPostCreatedOn { get; set; }
            #endregion
    
            #region NavigationProperties
            /// <summary>
            /// sets or gets identifier forum's parent
            /// </summary>
            public virtual long? ParentId { get; set; }
            /// <summary>
            /// sets or gets forum's parent
            /// </summary>
            public virtual Forum Parent { get; set; }
            /// <summary>
            /// sets or gets sub forums of forum
            /// </summary>
            public virtual ICollection<Forum> Children { get; set; }
            /// <summary>
            /// set or get topics of forum
            /// </summary>
            public virtual ICollection<ForumTopic> Topics { get; set; }
            /// <summary>
            /// get or set moderators of this forum
            /// </summary>
            public virtual ICollection<ForumModerator> Moderators { get; set; }
            /// <summary>
            /// get or set Subscriptions List 
            /// </summary>
            public virtual ICollection<User> Subscribers { get; set; }
            /// <summary>
            /// get or set Announcements Collection of this Forum
            /// </summary>
            public virtual ICollection<ForumAnnouncement> Announcements { get; set; }
            /// <summary>
            /// get or set Trackers List Of this Forum
            /// </summary>
            public virtual ICollection<ForumTracker> Trackers  { get; set; }
            /// <summary>
            /// get or set Posts List that Posted in this forum for increase Performance for get Posts Count 
            /// </summary>
            public virtual ICollection<ForumPost> Posts  { get; set; }
            /// <summary>
            /// get or set 
            /// </summary>
            public virtual ICollection<ForumTopicTracker> TopicTrackers  { get; set; }
            #endregion
    مدل بالا نشان دهنده‌ی ساختار انجمن‌های ما می‌باشد. خصوصیت هایی که نیاز به توضیح دارند به شکل زیر می‌باشند:
    1. IsActive : مشخص کننده‌ی این است که در این انجمن امکان ارسال تاپیک و پست وجود دارد و در صورت false بودن این خصوصیت، بر تمام زیر انجمن‌ها هم اعمال خواهد شد و برای زمانی مفید است که میخواهیم برای مدتی به هر دلیل خاصی امکان ارسال تاپیک و پست را برای انجمن خاصی، ندهیم. 
    2. IsColsed : خصوصت اولی که مطرح شد اگر مقدار false بگیرد، همچنان کاربران می‌توانند تایپک‌ها و پست‌های قبلی را مشاهده و مطالعه کنند. ولی با مقدار دهی این خصوصیت با مقدار false، امکان کلیه‌ی فعالیت‌ها و مشاهده‌ای را از محتوای این انجمن و زیر انجمن‌های آن نخواهیم داشت.
    3. IsPrivate : برای مواقعی که لازم است برای انجمن خاصی کلمه‌ی عبور در نظر بگیریم تا افراد خاص که کلمه‌ی عبور آن را دارند بتوانند در آن انجمن فعالیت کنند، در نظر گرفته شده است.
    4. ApprovedPostsCount , UnApprovedPostsCount,ApprovedTopicsCount,UnApprovedTopicsCount : برای بالا بردن کارآیی سیستم به مانند مدل‌های قبل در نظر گرفته شده‌اند.
    5. LastTopicId, LastTopicTitle , LastTopicCreator , LastTopicCreatorId , LastTopicCreatedOn: همچنین برای افزایش کارآیی سیستم و نمایش به عنوان قسمتی از مشخصات قابل مشاهده از هر انجمن، در نظر گرفته شده‌اند.
    6. Depth : برای نشان دادن عمق گره در درخت استفاده می‌شود که هنگام درج انجمن، این مورد از نتیجه‌ی جمع عمق پدر انتخاب شده و یک، به دست خواهد آمد. این مورد هنگام واکشی برای مثال 4 سطح اول برای نمایش آنها در صفحه‌ی اول انجمن به صورت سلسله مراتبی خیلی مفید خواهد بود.
    7. Path : برای استفاده از SQL Antipattern شمارش مسیر در نظر گرفته شده است. این مورد جزء Best Practice‌‌ها می‌تواند باشد. چون هم با استفاده از ساختار خود ارجاع، درخت خود را داریم و با این Antipattern کوئری‌های مربوط به درخت خیلی راحت خواهد بود.
    8. IsModeratorsInherited : اگر لازم است مدیران انجمن، پدر را به عنوان مدیر خود قبول کنند، این خصوصیت مقدار true خواهد گرفت.
    9. Subscribers : هر انجمنی می‌تواند یکسری مشترک نیز داشته باشد (به منظور اطلاع رسانی با درج یک تاپیک جدید در خود انجمن یا زیر انجمن‌های آن) .
    10. Posts : به منظور افزایش کارایی هنگام محاسبه تعداد پست‌های ارسالی در یک انجمن  ، در نظر گرفته شده است.
    11. TopicTrackers : در مقاله بعد توضیح داده خواهد شد.
    12. LastPostCreatedOn : به منظور استفاده از آن برای سیستم Tracking انجمن‌ها استفاده خواهد شد . 
    ساختار درختی آن هم قابل مشاهده بوده  و نیاز به توضیح خاضی ندارد. در هر انجمن ما، یکسری تاپیک مطرح خواهد شد و برای این منظور لیستی از ForumTopic را در این کلاس معرفی کرده‌ایم. 
    علاوه بر اینها انجمن‌های ما مدیرانی هم خواهند داشت که برای این منظور نیز لیستی از ForumModerator را در مدل بالا تعریف کرده‌ایم. همچنین تصمیم گرفتیم امکانی را برای سیستم انجمن در نظر بگیرم تا اگر لازم بود یک سری اعلان در بالای انجمن‌ها نشان داده شوند و در بخش مدیریت بتوان این امکان را هندل کرد؛ که لیستی  است از ForumAnnouncement‌های مدل بالا.

    مدل مدیران انجمن
     /// <summary>
        /// Represents The Moderator For Forum
        /// </summary>
        public class ForumModerator
        {
            #region NavigationProperties
            /// <summary>
            /// gets or sets Forum
            /// </summary>
            public virtual Forum Forum { get; set; }
            /// <summary>
            /// gets or sets identifier of forum
            /// </summary>
            public virtual long ForumId { get; set; }
            /// <summary>
            /// gets or sets user that moderate forum
            /// </summary>
            public virtual User Moderator { get; set; }
            /// <summary>
            /// gets or sets id of user that moderate forum
            /// </summary>
            public virtual long ModeratorId { get; set; }
            /// <summary>
            /// gets or sets permission of user that moderate forum
            /// </summary>
            public virtual ForumModeratorPermissions Permissions { get; set; }
            /// <summary>
            /// indicate moderator's permissions in this forum apply with
            /// </summary>
            public virtual bool ApplyChildren { get; set; }
            #endregion
        }
    
        [Flags]
        public enum  ForumModeratorPermissions
        {
            CanEditPosts=1,
            CanDeletePosts=2,
            CanManageTopics=4,
            CanOpenCloseTopics=8,
           ...
        }
    این مدل نشان می‌دهد که کاربر x به عنوان مدیر انجمن y می‌باشد و یکسری دسترسی‌ها را نیز در این انجمن خواهد داشت.
    1. ApplyChildren : برای اعمال دسترسی‌های مدیریتی کاربر x به زیر انجمن‌های انجمن y البته اگر خصوصیت IsModeratorsInherited زیر انجمن‌های مورد نظر با مقدار true مقدار دهی شده باشد.
    2. Permissions : از نوع ForumModeratorPermissions و نگهدارنده دسترسی‌های کاربر x به عنوان مدیر انجمن y، می‌باشد.
    3. نکته : برای این مدل آی دی در نظر گرفته نشده است و از کلید مرکب متشکل از ForumId و ModeratorId استفاده خواهیم کرد. 
      مدل اعلان‌های انجمن
          /// <summary>
          /// Represents the Announcement that shown Top Of Forums
          /// </summary>
          public class ForumAnnouncement
          {
              #region Ctor
              /// <summary>
              /// create one instance of <see cref="ForumAnnouncement"/>
              /// </summary>
              public ForumAnnouncement()
              {
                  Id = SequentialGuidGenerator.NewSequentialGuid();
                 CreatedOn = DateTime.Now;
              }
              #endregion
      
              #region Properties
              /// <summary>
              /// gets or sets Identifier 
              /// </summary>
              public virtual Guid Id { get; set; }
              /// <summary>
              /// gets or sets DateTime That this Announcement Will be Shown
              /// </summary>
              public virtual DateTime StartOn { get; set; }
              /// <summary>
              /// gets or sets DateTime That this Announcement Will be Finished 
              /// </summary>
              public virtual DateTime? ExpireOn { get; set; }
              /// <summary>
              /// gets or sets Content of this Announcement
              /// </summary>
              public virtual string Message { get; set; }
              /// <summary>
              /// Indicate this Announcement Will be shown on Children Forums
              /// </summary>
              public virtual bool ApplyChildren { get; set; }
              /// <summary>
              /// gets or sets datetime that this record created
              /// </summary>
              public virtual DateTime CreatedOn { get; set; }
              #endregion
      
              #region NavigationProperties
              /// <summary>
              /// gets or sets Forum that associated With this Announcement
              /// </summary>
              public virtual Forum Forum { get; set; }
              /// <summary>
              /// gets or sets Identifier of Forum that associated With this Announcement
              /// </summary>
              public virtual long ForumId { get; set; }
              #endregion
          }
      مدل بالا نشان دهنده‌ی اعلان‌هایی است که می‌توان با امکان تنظیم زمان آغاز و اتمام، آنها را در صفحات انجمن‌ها نمایش داد. این امکان هم از لحاظ مدیریتی می‌تواند مفید باشد و هم اگر لازم شد، تبلیغی انجام شود. برای اعمال رابطه‌ی یک به چند، یک خصوصیت از نوع Forum با همراه ForumId را در مدل بالا تعریف کرده‌ایم.
      1. ApplyChildren : برای مشخص کردین نمایش این اعلان در زیر انجمن‌های انجمن مورد نظر
      2. ExpireOn : به این دلیل نال پذیر در نظر گرفته شده است که اگر لازم بود، در زمان مشخصی به پایان نرسد و با null مقدار دهی شود.
      کلاس پایه AuditBaseEntity 
       /// <summary>
          /// Represents a base class for AuditLog
          /// </summary>
          public abstract class AuditBaseEntity
          {
              #region Properties
              /// <summary>
              /// sets or gets identifier
              /// </summary>
              public virtual long Id { get; set; }
              /// <summary>
              /// gets or sets datetime that is created
              /// </summary>
              public  virtual DateTime CreatedOn { get; set; }
              /// <summary>
              /// gets or sets datetime that is modified
              /// </summary>
              public virtual DateTime? LastModifiedOn { get; set; }
              /// <summary>
              /// gets or sets reason of Last Update for increase performance
              /// </summary>
              public virtual string LastModifyReason { get; set; }
              /// <summary>
              /// gets or sets displayName of Last Modifier  for increase performance
              /// </summary>
              public virtual string LastModifier{ get; set; }
              /// <summary>
              /// indicate this entity is Locked for Modify
              /// </summary>
              public virtual bool ModifyLocked { get; set; }
              /// <summary>
              /// gets or sets rowversion for synchronization problem
              /// </summary>
              public virtual byte[] RowVersion { get; set; }
              /// <summary>
              /// gets or sets count of this content's Updates
              /// </summary>
              public virtual int ModifyCount { get; set; }
              #endregion
      
              #region NavigationProperties
              /// <summary>
              /// gets or sets creator of this record
              /// </summary>
              public virtual User Creator { get; set; }
              /// <summary>
              /// gets or sets creator's Id of this record
              /// </summary>
              public virtual long CreatorId { get; set; }
              #endregion
          }
      این کلاس به منظور کپسوله کردن یکسری فیلد تکراری برای مدل‌هایی که نیاز است آخرین تغییر دهنده و زمان آن را ذخیره کنند، در نظر گرفته شده است و هدف از آن هیچ گونه اعمال ارث بری TPH یا TPT هم نیست.
      ModifyLocked : برای زمانی مفید است که مدیریت امکان ویرایش یک مطلب را به صورت دستی غیرفعال میکند.
      مدل تاپیک ها
        /// <summary>
          /// Represents the Topic in the Forums
          /// </summary>
          public class ForumTopic 
          {
              #region Ctor
              /// <summary>
              /// create one instance of <see cref="ForumTopic"/>
              /// </summary>
              public ForumTopic()
              {
                  CreatedOn = DateTime.Now;
              }
              #endregion
      
              #region Properties
              /// <summary>
              /// sets or gets identifier
              /// </summary>
              public virtual long Id { get; set; }
              /// <summary>
              /// gets or sets datetime that is created
              /// </summary>
              public virtual DateTime CreatedOn { get; set; }
              /// <summary>
              /// gets or sets Title Of this topic
              /// </summary>
              public virtual string Title { get; set; }
              /// <summary>
              /// gets or sets name of tags that assosiated with 
              /// this content fo increase performance
              /// </summary>
              public virtual string TagNames { get; set; }
              /// <summary>
              /// indicate this topic is Sticky and will be shown top of forum
              /// </summary>
              public virtual bool IsSticky { get; set; }
              /// <summary>
              /// indicate this topic is closed
              /// </summary>
              public virtual bool IsClosed { get; set; }
              /// <summary>
              /// gets or sets identifier of  last post in this topic
              /// </summary>
              public virtual long LastPostId { get; set; }
              /// <summary>
              /// gets or sets identifier of Last user that post in this topic
              /// </summary>
              public virtual long LastPosterId { get; set; }
              /// <summary>
              /// gets or sets title of last Post in this topic
              /// </summary>
              public virtual string LastPostTitle { get; set; }
              /// <summary>
              /// gets or sets displayName of user that create lastpost in this topic
              /// </summary>
              public virtual string LastPoster { get; set; }
              /// <summary>
              /// gets or sets datetime that last post posted in this topic
              /// </summary>
              public virtual DateTime? LastPostCreatedOn { get; set; }
              /// <summary>
              /// indicate this topic is approved
              /// </summary>
              public virtual bool IsApproved { get; set; }
              /// <summary>
              /// indicate this topic is type of Announcements and shown in Annoucements sections
              /// </summary>
              public virtual bool IsAnnouncement { get; set; }
              /// <summary>
              /// gets or sets viewed count 
              /// </summary>
              public virtual long ViewCount { get; set; }
              /// <summary>
              /// gets or sets count of posts that they are approved
              /// </summary>
              public virtual int ApprovedPostsCount { get; set; }
              /// <summary>
              /// gets or sets count of posts that they are Unapproved
              /// </summary>
              public virtual int UnApprovedPostsCount { get; set; }
              /// <summary>
              /// gets or sets specifications of this topic's rating
              /// </summary>
              public virtual Rating Rating { get; set; }
              /// <summary>
              /// gets or sets datetime that this topic closed
              /// </summary>
              public virtual DateTime? ClosedOn { get; set; }
              /// <summary>
              /// gets or sets reason that this topic colsed
              /// </summary>
              public virtual string ClosedReason { get; set; }
              /// <summary>
              /// gets or sets count of reports
              /// </summary>
              public virtual int ReportsCount { get; set; }
              /// <summary>
              /// indicate the posts of this topic should be Moderate Before Dipslay
              /// </summary>
              public virtual bool ModeratePosts { get; set; }
              /// <summary>
              /// gets or sets Level of this topic
              /// </summary>
              public virtual ForumTopicLevel Level { get; set; }
              /// <summary>
              /// gets or sets type of this topic
              /// </summary>
              public virtual ForumTopicType Type { get; set; }
              #endregion
      
              #region NavigationProperties
              /// <summary>
              /// gets or sets Collection of tags that associated with this topic
              /// </summary>
              public virtual ICollection<Tag> Tags { get; set; }
              /// <summary>
              /// gets or sets forum
              /// </summary>
              public virtual Forum Forum { get; set; }
              /// <summary>
              /// gets or sets identifier of Forum
              /// </summary>
              public virtual long ForumId { get; set; }
              /// <summary>
              /// gets or sets Posts Of this topic
              /// </summary>
              public virtual ICollection<ForumPost> Posts { get; set; }
              /// <summary>
              /// get or set Subscriptions List 
              /// </summary>
              public virtual ICollection<User> Subscribers { get; set; }
              /// <summary>
              /// get or set Trackkers list of this Topic
              /// </summary>
              public virtual ICollection<ForumTopicTracker> Trackers { get; set; }
              /// <summary>
              /// gets or sets creator of this record
              /// </summary>
              public virtual User Creator { get; set; }
              /// <summary>
              /// gets or sets creator's Id of this record
              /// </summary>
              public virtual long CreatorId { get; set; }
              #endregion
          }
      
       public enum ForumTopicType
          {
              Non,
              Tutorial,
              Conversation,
              Question,
              News,
              Article
          }
      
          public enum ForumTopicLevel
          {
              Professional,
              Intermediate,
              Beginner
          }
      مدل بالا مشخص کننده‌ی تاپیک‌های انجمن می‌باشد. خصوصیاتی که نیاز به توضیح دارند:
      1. LastPostId , LastPosterId, LastPoster  , LastPostTitle  ,LastPostCreatedOn: برای افزایش کارآیی سیستم در نظر گرفته شده‌اند.
      2. ModeratePosts : اگر لازم است پست‌های یک تاپیک خاص، قبل از نمایش مدیریت شوند، با true مقدار دهی خواهد شد.
      3. Tags : لیستی از برچسب‌ها که برای اعمال رابطه‌ی چند به چند با مدل برچسب‌های معرفی شده‌ی در مقاله اول، در نظر گرفته شده است.
      4. Posts : در هر تاپیکی یک سری پست به عنوان جواب‌های آن ارسال خواهد شد.
      5. Subscribers  : به مانند انجمن‌ها، تاپیک‌های ما هم می‌توانند یک سری مشترک داشته باشند، تا از تغییرات این تاپیک مطلع شوند .
      6. Trackers : مربوط به سیستم Tracking تاپیک میباشد. و در مقاله بعد توضیح داده خواهد شد.

       حتما لازم خواهد بود تاریخچه‌ی تغییرات برای  پست‌های ارسالی ذخیره شوند؛ در مقاله‌ی بعدی به این موضوع هم خواهیم پرداخت.
      نتیجه این قسمت

      ‫توسعه سیستم مدیریت محتوای DNTCms - قسمت چهارم

      $
      0
      0

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

      مدل پست‌های انجمن

       /// <summary>
          /// Represents The Post of Forum
          /// </summary>
          public class ForumPost : AuditBaseEntity
          {
              #region Ctor
              /// <summary>
              /// create one instance of <see cref="ForumPost"/>
              /// </summary>
              public ForumPost()
              {
                  CreatedOn = DateTime.Now;
              }
              #endregion
      
              #region Properties
              /// <summary>
              /// gets or sets body of this post
              /// </summary>
              public virtual string Body { get; set; }
              /// <summary>
              /// gets or sets Count of this post's reports
              /// </summary>
              public virtual int ReportsCount { get; set; }
              /// <summary>
              /// gets or sets information of User-Agent
              /// </summary>
              public virtual string Agent { get; set; }
              /// <summary>
              /// gets or sets rating values 
              /// <remarks>is a complex type</remarks>
              /// </summary>
              public virtual Rating Rating { get; set; }
              /// <summary>
              /// gets or sets author's ip address
              /// </summary>
              public virtual string CreatorIp { get; set; }
              /// <summary>
              /// gets or sets status of this post
              /// </summary>
              public virtual ForumPostStatus Status { get; set; }
              #endregion
      
              #region NavigationProperties
              /// <summary>
              /// gets or sets ParentPost of this post
              /// </summary>
              public virtual ForumPost Reply { get; set; }
              /// <summary>
              /// gets or sets ParentPost's Id of this post
              /// </summary>
              public virtual long? ReplyId { get; set; }
              /// <summary>
              /// gets or sets 
              /// </summary>
              public virtual ICollection<ForumPost> Children { get; set; }
              /// <summary>
              /// gets or sets Topic That Associated with this Post
              /// </summary>
              public virtual ForumTopic Topic { get; set; }
              /// <summary>
              /// gets or sets Id of Topic That Associated with this Post
              /// </summary>
              public virtual long TopicId { get; set; }
              /// <summary>
              /// get or sets  Histories of this Post's Updates
              /// </summary>
              public virtual ICollection<ForumPostHistory> Histories { get; set; }
              /// <summary>
              /// gets or sets Forum that this post created in it . used for retrive posts count 
              /// </summary>
              public virtual Forum Forum { get; set; }
              /// <summary>
              /// gets or sets id of Forum that this post created in it . used for retrive posts count 
              /// </summary>
              public virtual long ForumId { get; set; }
              #endregion
          }
      
       public enum ForumPostStatus 
          {
              /* 0 - approved, 1 - pending, 2 - spam, -1 - trash */
              [Display(Name = "تأیید شده")]
              Approved = 0,
              [Display(Name = "در انتظار بررسی")]
              Pending = 1,
              [Display(Name = "جفنگ")]
              Spam = 2,
              [Display(Name = "زباله دان")]
              Trash = -1
          }

      مدل بالا مشخص کننده‌ی پست‌هایی که در پاسخ به تاپیک‌ها ارسال می‌شوند، می‌باشد. ساختار درختی آن به منظور امکان پاسخ به پست‌ها در نظر گرفته شده است. در هر تاپیک چندین پست ارسال می‌شود که اولین پست ارسال شده، همان محتوای اصلی تاپیک می‌باشد. بدین منظور خصوصیت Topic را در مدل بالا تعریف کرده‌ایم. برای این پست‌های ارسالی امکان امتیاز دهی و اخطار دادن نیز خواهیم داشت که به ترتیب خصوصیات Rating و ReportsCount  (بحث شده در مقالات قبل) را در مدل بالا تعریف کرده‌ایم. خصوصیت Status به منظور اعمال مدیریتی در نظر گرفته شده است که از نوع ForumPostStatus می‌باشد و در بالا تعریف آن نیز آمده است.

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

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

      مدل تاریخچه‌ی تغییرات پست

       /// <summary>
          /// Represents History Of Post's Updates
          /// </summary>
          public class ForumPostHistory 
          {
              #region Ctor
              /// <summary>
              /// create one instance of <see cref="ForumPostHistory"/>
              /// </summary>
              public ForumPostHistory()
              {
                  Id = SequentialGuidGenerator.NewSequentialGuid();
                  CreatedOn = DateTime.Now;
              }
              #endregion
      
              #region Properties
              /// <summary>
              /// gets or sets Identifier of this history
              /// </summary>
              public virtual Guid Id { get; set; }
              /// <summary>
              /// gets or sets Reason of  update
              /// </summary>
              public virtual string Reason { get; set; }
              /// <summary>
              /// gets or sets DateTime that this record added
              /// </summary>
              public virtual DateTime CreatedOn { get; set; }
              /// <summary>
              /// gets or sets body of this post
              /// </summary>
              public virtual string Body { get; set; }
              #endregion
              #region NavigationProperties
              /// <summary>
              /// gets or sets Post
              /// </summary>
              public virtual ForumPost Post { get; set; }
              /// <summary>
              /// gets or sets Id Of Post
              /// </summary>
              public virtual long PostId { get; set; }
              /// <summary>
              /// gets or sets User that modified this Record
              /// </summary>
              public virtual User Modifier { get; set; }
              /// <summary>
              /// gets or sets if of User that modified this Record
              /// </summary>
              public virtual long ModifierId { get; set; }
              #endregion
          }

      اگر خصوصیت ModifyLocked مربوط به مدل ForumPost که آن را از کلاس پایه AuditBaseEntity به ارث برده است، دارای مقدار true باشد، این امکان وجود خواهد داشت تا بتوان پست مورد نظر را ویرایش کرده و اطلاعات قبلی، در قالب یک رکورد در جدول حاصل از مدل بالا ثبت شوند.

      • Reason : دلیل این ویرایش به عمل آماده 
      • Body : محتوای پست یا تاپیک
      • Modifier : کاربر انجام دهنده‌ی این ویرایش 
      • CreatedOn : زمانی که این ویرایش انجام شده است

      مدل ردیابی انجمن ها

      در خیلی از انجمن‌ها حتما متوجه شده‌اید که لینک برخی از انجمن‌ها یا تاپیک‌های درج شده‌ی در آنها برای شما bold شده نشان داده می‌شود. در واقع، هدف مطلع کردن شما از اینکه در حال حاضر یکسری تاپیک یا پست در انجمن ثبت شده است که شما آنها را مشاهده نکرده‌اید.
       public class ForumTracker
          {
              #region Ctor
              /// <summary>
              /// create one instance of <see cref="ForumTracker"/>
              /// </summary>
              public ForumTracker()
              {
                  LastMarkedOn = DateTime.Now;
              }
      
              #endregion
      
              #region Properties
              /// <summary>
              /// gets or sets DateTime Of Las Visit by User
              /// </summary>
              public virtual DateTime LastMarkedOn { get; set; }
      
              #endregion
      
              #region NavigationProperties
              /// <summary>
              /// gets or sets Forum that Tracked
              /// </summary>
              public virtual Forum Forum { get; set; }
              /// <summary>
              /// gets or sets Id of Forum tath Tracked
              /// </summary>
              public virtual long ForumId { get; set; }
              /// <summary>
              /// gets or sets User that tracked The forum
              /// </summary>
              public virtual User Tracker { get; set; }
              /// <summary>
              /// gets or sets Id Of User that Tracked the forum
              /// </summary>
              public virtual long TrackerId { get; set; }
              #endregion
          }
      
      
         public class ForumTopicTracker
            {
              #region Ctor
              /// <summary>
              /// create one instance of <see cref="ForumTopicTracker"/>
              /// </summary>
              public ForumTopicTracker()
              {
                  LastVisitedOn = DateTime.Now;
              }
      
              #endregion
      
              #region Properties
              /// <summary>
              /// gets or sets DateTime Of Las Visit by User
              /// </summary>
              public virtual DateTime LastVisitedOn { get; set; }
      
              #endregion
      
              #region NavigationProperties
              /// <summary>
              /// gets or sets topc that Tracked
              /// </summary>
              public virtual ForumTopic Topic { get; set; }
              /// <summary>
              /// gets or sets Id of topic that Tracked
              /// </summary>
              public virtual long TopicId { get; set; }
              /// <summary>
              /// gets or sets User that tracked The topic
              /// </summary>
              public virtual User Tracker { get; set; }
              /// <summary>
              /// gets or sets Id Of User that Tracked the topic
              /// </summary>
              public virtual long TrackerId { get; set; }
              /// <summary>
              /// gets or sets Forum 
              /// </summary>
              public virtual Forum Forum { get; set; }
              /// <summary>
              /// gets or sets Identifier of Forum . used for delete 
              /// </summary>
              public virtual long ForumId { get; set; }
      
              #endregion
            }
      در سیستم، برای کاربران احراز هویت شده، این امکان را مهیا ساخته‌ایم تا انجمن‌ها و تایپک‌هایی که پست جدید ارسال شده دارند و توسط کاربر خوانده نشده است، به نحوی متمایز نشان داده شوند. 
      برای این منظور از دو مدل پیاده سازی شده‌ی در بالا و یک خصوصیت از نوع تاریخ تحت عنوان LastMarkedOn در مدل User، استفاده خواهیم کرد. در واقع از LastMarkedOn مدل User، برای نگه داری آخرین تاریخی استفاده می‌شود که کاربر تمام انجمن‌ها را خوانده شده علامت گذاری کرده است. در این صورت می‌توان تمام رکورد‌های ذخیره شده‌ی در جداول ForumTrackers و ForumTopicTrackers را که قبل از این تاریخ هستند، حذف کرد. از LastMarkedOn مدل ForumTracker هم برای نگهداری تاریخی استفاده می‌شود که یک انجمن خاص را خوانده شده علامت گذاری کرده است و همچنین می‌توان تمام رکورد‌های مربوط به آن انجمن را در جدول ForumTopicTrackers حذف کرد.

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

      البته نیاز است هنگام واکشی انجمن‌ها و تاپیک‌ها، یکسری بررسی‌هایی را بر اساس این جداول انجام داد که تشریح این بررسی‌ها را  قصد دارم هنگام پیاده سازی سیستم انجام دهم. 

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

      مدل سیستم نظرسنجی

      public class Poll : BaseContent
          {
              #region Ctor
              /// <summary>
              /// create one instance of <see cref="Poll"/>
              /// </summary>
              public Poll()
              {
                  Rating = new Rating();
                  PublishedOn = DateTime.Now;
              }
              #endregion
      
              #region Properties
              /// <summary>
              /// gets or set Date that this Poll will Expire
              /// </summary>
              public virtual DateTime? ExpireOn { get; set; }
              /// <summary>
              ///indicating this poll allow to select multi item
              /// </summary>
              public virtual bool IsMultiSelect { get; set; }
              /// <summary>
              /// gets or sets Count of this poll's votes 
              /// </summary>
              public virtual long VotesCount { get; set; }
              /// <summary>
              /// indicate this Poll is approved by admin if Poll.Moderate==true
              /// </summary>
              public virtual bool IsApproved { get; set; }
      
              #endregion
      
              #region NavigationProperties
              /// <summary>
              /// get or set comments of this poll
              /// </summary>
              public virtual ICollection<PollComment> Comments { get; set; }
              /// <summary>
              /// get or set Options Of Poll For selection
              /// </summary>
              public virtual ICollection<PollOption> Options { get; set; }
              /// <summary>
              /// get or set Users List That vote for this poll
              /// </summary>
              public virtual ICollection<User> Voters { get; set; }
              #endregion
          }
      مدل بالا مشخص کننده‌ی نظرسنجی‌های سیستم ما می‌باشد. این مدل نیز از کلاس پایه مطرح شده در مقاله اولارث بری کرده است و علاوه بر آن یکسری خصوصیت دیگر را به شرح زیر دارد:
      • ExpireOn : زمان اتمام فرصت رای دهی که اگر نال باشد در آن صورت زمان انقضا نخواهد داشت.
      • IsMultiSelect : اگر انتخاب چندگزینه‌ای مجاز باشد، این خصوصیت، با مقدار true مقدار دهی می‌شود.
      • VotesCount : به منظور افزایش کارآیی در نظر گرفته شده است و تعداد کل رای‌های داده شده‌ی به نظرسنجی را در بر می‌گیرد.
      • Voters : برای جلوگیری از رای دهی چند باره‌ی کاربر به یک نظرسنجی، یک ارتباط چند به چند بین کاربر و نظرسنجی برقرار کرده‌ایم. هر کاربر به چند نظر سنجی می‌تواند پاسخ دهد و به هر نظرسنجی توسط چندین کاربر رای داده می‌شود.
      • PollOptions : هر نظر سنجی تعدادی گزینه‌ی انتخابی هم خواهد داشت که برای همین منظور و اعمال ارتباط یک به چند بین نظرسنجی و گزینه‌های انتخابی، لیستی از PollOption را در مدل بالا تعریف کرده‌ایم.

        مدل گزینه‌های نظرسنجی

         public class PollOption
            {
                #region Properties
                /// <summary>
                /// gets or sets identifier of this polloption
                /// </summary>
                public virtual long Id { get; set; }
                /// <summary>
                /// gets or sets Title of this polloption
                /// </summary>
                public virtual string Title { get; set; }
                /// <summary>
                /// gets or sets count of votes 
                /// </summary>
                public virtual long VotesCount { get; set; }
                /// <summary>
                /// gets or sets Description of this Option for more details
                /// </summary>
                public virtual string Description { get; set; }
                #endregion
        
                #region NavigationProperties
                /// <summary>
                /// gets or sets the poll that assosiated with this Polloption
                /// </summary>
                public virtual Poll Poll { get; set; }
                /// <summary>
                /// gets or sets the id of poll that assosiated with this Polloption
                /// </summary>
                public virtual long PollId { get; set; }
                #endregion
            }
        مدل بالا نشان دهنده‌ی گزینه‌های انتخابی در هر نظر سنجی می‌باشد. خصوصیت Poll و به دنبال آن PollId به منظور اعمال ارتباط یک به چند بین نظرسنجی و گزینه‌ها در نظر گرفته شده‌اند. 
        • Title: عنوان گزینه‌ی مورد نظر
        • Description: توضیح بیشتر برای گزینه‌ی مورد نظر
        • VotesCount: تعداد باری که یک گزینه در نظر سنجی انتخاب شده است.
        در این سیستم نیازی نیست که بدانیم چه کاربرانی در یک نظر سنجی کدام گزینه را انتخاب کرده‌اند و لذا مدل بالا برای کار ما کافی است.

        مدل نظرات سیستم نظرسنجی

         public class PollComment : BaseComment
            {
                #region Ctor
                public PollComment()
                {
                    CreatedOn = DateTime.Now;
                    Rating = new Rating();
                }
                #endregion
        
                #region NavigationProperties
        
                /// <summary>
                /// gets or sets body of blog poll's comment
                /// </summary>
                public virtual long? ReplyId { get; set; }
                /// <summary>
                /// gets or sets body of blog poll's comment
                /// </summary>
                public virtual PollComment Reply { get; set; }
                /// <summary>
                /// gets or sets body of blog poll's comment
                /// </summary>
                public virtual ICollection<PollComment> Children { get; set; }
                /// <summary>
                /// gets or sets poll that this comment sent to it
                /// </summary>
                public virtual Poll Poll { get; set; }
                /// <summary>
                /// gets or sets poll'Id that this comment sent to it
                /// </summary>
                public virtual long PollId { get; set; }
                #endregion
            }
        مدل بالا نیز از کلاس پایه‌ی BaseComment مورد بحث در مقاله‌ی اول  ارث بری کرده است و ساختار درختی آن نیز مشخص است و همچنین یک ارتباط یک به چند بین نظرسنجی‌ها و نظرات وجود خواهد داشت که برای این منظور خصوصیت Poll را در مدل بالا تعریف کرده‌ایم.

        در مقاله‌ی بعد به بررسی سیستم پیام رسانی و همچنین بخشی از سیستم تحت عنوان Collections (امکان ساخت گروه‌های شخصی برای انتشار مطالب خود (توسط کاربران) با اعمال دسترسی‌های مختلف) خواهیم پرداخت.

        نتیجه تا این قسمت


          ‫توسعه سیستم مدیریت محتوای DNTCms - قسمت پنجم

          $
          0
          0
          در این قسمت به بررسی بخش Collections (امکان ساخت گروه‌های شخصی برای انتشار مطالب خود (توسط کاربران) با اعمال دسترسی‌های مختلف) ، بخش آگهی‌ها، سیستم لاگ عملیات کاربران و مدل‌های سیستمی می‌پردازیم.
          در مدل‌های سیستم، یک تغییر کلی به منظور نگهداری آخرین تغییر دهنده و آخرین تاریخ تغییر در رکورد‌ها، ایجاد شده است. کلاس پایه‌ی زیر به منظور کپسوله کردن یکسری خصوصیات تکراری در نظر گرفته شده است.
            public abstract class BaseEntity
              {
                  #region Properties
                  /// <summary>
                  /// gets or sets Identifier of this Entity
                  /// </summary>
                  public virtual long Id { get; set; }
                  /// <summary>
                  /// gets or sets date that this entity was created
                  /// </summary>
                  public virtual DateTime CreatedOn { get; set; }
                  /// <summary>
                  /// gets or sets Date that this entity was updated
                  /// </summary>
                  public virtual DateTime ModifiedOn { get; set; }
                  /// <summary>
                  /// indicate this entity is Locked for Modify
                  /// </summary>
                  public virtual bool ModifyLocked { get; set; }
                  /// <summary>
                  /// gets or sets date that this entity repoted last time
                  /// </summary>
                  public virtual DateTime? ReportedOn { get; set; }
                  /// <summary>
                  /// gets or sets counter for Content's report
                  /// </summary>
                  public virtual int ReportsCount { get; set; }
                  /// <summary>
                  /// gets or sets TimeStamp for prevent concurrency Problems
                  /// </summary>
                  public virtual byte[] RowVersion { get; set; }
                  #endregion
          
                  #region NavigationProperties
                  /// <summary>
                  /// gets ro sets User that Modify this entity
                  /// </summary>
                  public virtual User ModifiedBy { get; set; }
                  /// <summary>
                  /// gets ro sets Id of  User that modify this entity
                  /// </summary>
                  public virtual long? ModifiedById { get; set; }
                  /// <summary>
                  /// gets ro sets User that Create this entity
                  /// </summary>
                  public virtual User CreatedBy { get; set; }
                  /// <summary>
                  /// gets ro sets User that Create this entity
                  /// </summary>
                  public virtual long CreatedById { get; set; }
                  #endregion
              }
          با توجه به امکان تغییر نام کاربری توسط کاربر در سیستم، نگه داری صرفا نام کاربری آخرین تغییر دهنده، مفید نخواهد بود. شبیه به این کار را در سیستم Decisionنیز می‌توانید مشاهده کنید. خصوصیاتی که نیاز به توضیح دارند :
          • ReportedOn : نگهداری آخرین تاریخ اخطار 
          • ModifyLocked : به منظور ممانعت از ویرایش 
          • CreatedBy,CreatedById : به منظور ایجاد ارتباط یک به چند بین کاربر و سایر موجودیت‌ها (به عنوان ایجاد کننده)
          • ModifiedBy , ModifiedById : به منظور ایجاد ارتباط یک به چند بین کاربر و سایر موجودیت‌ها (به عنوان آخرین تغییر دهنده)

          مدل کلکسیون (کالکشن ،Collection)

          بخش Collections می‌تواند برای کاربران امکان انتشار مطالب گروهبندی شده را بر اساس موضوع‌های مختلف، مهیا کند. کاربران بعد از کسب امتیازات لازم با استفاده از مهیا کردن محتوای وبلاگ یا فعالیت‌های آنها در انجمن و ... می‌توانند دسترسی لازم را برای ساخت Collections‌‌های خود، داشته باشند.  
           /// <summary>
              /// Represents the Collection for group posts by topic 
              /// </summary>
              public class Collection : GuidBaseEntity
              {
                  #region Properties
                  /// <summary>
                  /// gets or sets name of collection
                  /// </summary>
                  public virtual string Name { get; set; }
                  /// <summary>
                  /// gets or sets Alternative SlugUrl
                  /// </summary>
                  public virtual string SlugUrl { get; set; }
                  /// <summary>
                  /// gets or sets some description of group
                  /// </summary>
                  public virtual string Description { get; set; }
                  /// <summary>
                  /// gets or sets description that indicate how to pay 
                  /// </summary>
                  public virtual string HowToPay { get; set; }
                  /// <summary>
                  /// gets or sets Visibility Type 
                  /// </summary>
                  public virtual CollectionVisibility Visibility { get; set; }
                  /// <summary>
                  /// gets or sets color of Collection's Cover
                  /// </summary>
                  public virtual string Color { get; set; }
                  /// <summary>
                  /// gets or sets Name of Image that used as Cover
                  /// </summary>
                  public virtual string Photo { get; set; }
                  /// <summary>
                  /// gets or sets name of tags seperated by comma that assosiated with this content fo increase performance
                  /// </summary>
                  public virtual string TagNames { get; set; }
                  /// <summary>
                  /// indicate this collection is active or not
                  /// </summary>
                  public virtual bool IsActive { get; set; }
                  #endregion
          
                  #region  NavigationProperties
                 
                  /// <summary>
                  /// get or set collection of attachments that attached in this group
                  /// </summary>
                  public virtual ICollection<CollectionAttachment> Attachments { get; set; }
                  /// <summary>
                  /// get or set tags of collection
                  /// </summary>
                  public virtual ICollection<Tag> Tags { get; set; }
                  /// <summary>
                  /// get or set List Of Posts that Associated with this Collection
                  /// </summary>
                  public virtual ICollection<CollectionPost> Posts { get; set; }
                  /// <summary>
                  /// get or set Users that they are Member of this collection if visibility is Custom
                  /// </summary>
                  public virtual ICollection<User> Memebers { get; set; }
                  #endregion
              }
             public enum  CollectionVisibility
              {
                  Friends,
                  OnlyMe,
                  Public,
                  NotFree,
                  Custom
              }
          مدل بالا نشان دهنده‌ی کلکسیون‌های ایجاد شده‌ی توسط کاربران می‌باشد. خصوصیاتی که نیاز به توضیح دارند:
          • Visibility : برای اعمال محدودیت دسترسی به یک کلکسیون در نظر گرفته شده است. از نوع، نوع داده‌ی شمارشی CollectionVisibility پیاده سازی شده‌ی دربالا، می‌باشد. حالت Custom برای زمانی است که نیاز است صرفا یک سری از کاربران بدون هزینه‌، دسترسی برای مطالعه‌ی این کلکسیون داشته باشند. حالت NotFree هم برای زمانی است که کاربرانی که هزینه‌ی مورد نظر را پرداخت کرده باشند، به عنوان عضو به این کلکسیون می‌توانند دسترسی داشته باشند.
          • Members : به منظور اعمال ارتباط چند به چند بین مدل کاربر و مدل کلکسیون، در نظر گرفته شده است و زمانی این لیست اعضا خالی نیست که حالت Visibility با NotFree یا Custom مقدار دهی شده باشد.
          • Tags : برای یافتن راحت‌تر کلکسیون‌های مورد نظر کاربران، یک ارتباط چند به چند بین کلکسیون‌ها و مخزن برچسب مطرح شده‌ی در مقاله اول، در نظر گرفته شده است. 
          • TagNames : برای افزایش کارآیی سیستم در نظر گرفته شده است و نام برچسب‌های در ارتباط با کلکسیون را در خود به صورت جدا شده با (,) نگهداری می‌کند.
          • Posts : لیست پست‌هایی است که توسط مدیر کلکسیون در آن ارسال می‌شود. لذا یک ارتباط یک به چند بین کلکسیون‌ها و CollectionPost در نظر گرفته شده است.
          • Attachments : لیست فایل‌هایی است که در کلکسیون بارگذاری شده‌اند (در ادامه پیاده سازی خواهد شد).
          • Owner , OwnerId : هر کلکسیون نیز یک صاحب خواهد داشت. بدین منظور یک ارتباط یک به چند بین کاربر و کلکسیون برقرار شده است.
          • HowToPay : توضیحاتی در مورد نحوه‌ی پرداخت هزینه (در صورتی که Visibility این کلکسیون NotFree باشد).

          مدل پست‌های کلکسیون ها

           public class CollectionPost : GuidBaseEntity
              {
                  #region Properties
                  /// <summary>
                  /// indicate this post should be pin
                  /// </summary>
                  public virtual bool IsPin { get; set; }
                  /// <summary>
                  /// gets or sets the blog pot body
                  /// </summary>
                  public virtual string Body { get; set; }
                  /// <summary>
                  /// gets or sets the content title
                  /// </summary>
                  public virtual string Title { get; set; }
                  /// <summary>
                  /// gets or sets value  indicating Custom Slug
                  /// </summary>
                  public virtual string SlugAltUrl { get; set; }
                  /// <summary>
                  /// gets or sets a value indicating whether the content comments are allowed 
                  /// </summary>
                  public virtual bool AllowComments { get; set; }
                  /// <summary>
                  /// indicate comments should be approved before display
                  /// </summary>
                  public virtual bool ModerateComments { get; set; }
                  /// <summary>
                  /// gets or sets viewed count 
                  /// </summary>
                  public virtual long ViewCount { get; set; }
                  /// <summary>
                  /// Gets or sets the total number of  approved comments
                  /// <remarks>The same as if we run Item.Comments.Count()
                  /// We use this property for performance optimization (no SQL command executed)
                  /// </remarks>
                  /// </summary>
                  public virtual int ApprovedCommentsCount { get; set; }
                  /// <summary>
                  /// Gets or sets the total number of  unapproved comments
                  /// <remarks>The same as if we run Item.Comments.Count()
                  /// We use this property for performance optimization (no SQL command executed)
                  /// </remarks>
                  /// </summary>
                  public virtual int UnApprovedCommentsCount { get; set; }
                  /// <summary>
                  /// gets or sets rating complex instance
                  /// </summary>
                  public virtual Rating Rating { get; set; }
                  /// <summary>
                  /// gets or sets information of User-Agent
                  /// </summary>
                  public virtual string Agent { get; set; }
                  /// <summary>
                  /// indicate users can share this post
                  /// </summary>
                  public virtual bool IsEnableForShare { get; set; }
                  #endregion
          
                  #region NavigationProperties
                  /// <summary>
                  /// get or set comments of this post
                  /// </summary>
                  public virtual ICollection<CollectionComment> Comments { get; set; }
                  /// <summary>
                  /// gets or sets collection that associated with this post
                  /// </summary>
                  public virtual Collection Collection { get; set; }
                  /// <summary>
                  /// gets or sets id of collection that associated with this post
                  /// </summary>
                  public virtual Guid CollectionId { get; set; }
                  #endregion
              }
          مدل بالا نشان دهنده‌ی پست‌های ارسالی در کلکسیون‌ها می‌باشد. خصوصیاتی که نیاز به توضیح دارند:
          • IsEnableForShare : اگر با مقدار True مقدار دهی شده باشد ، امکان به اشتراک گذاری آن درشبکه‌های اجتماعی وجود خواهد داشت. 
          • Comments : اگر مقدار AllowComments مربوط به پست ارسالی true باشد، در آن صورت امکان نظر دهی به پست  هم امکان پذیر خواهد بود. برای برقراری ارتباط یک به چند بین مدل پست کلکسیون و نظرات کلکسیون، این لیست در مدل بالا گنجانده شده است.
          • ModerateComments : اگر برای پست خاصی، با مقدار true مقدار دهی شده باشد، نظرات آن پست قبل از نمایش باید توسط مدیر آن کلکسیون تأیید شوند.
          • Author , AuthorId : در واقع ارسال کننده‌ی تمام پست‌ها، همان صاحب کلکسیون می‌باشد. ولی برای راحتی واکشی لیست پست‌های ارسالی کاربر مورد نظر یک ارتباط یک به چند بین کاربر و پست‌های ارسالی را در کلکسیون اعمال کرده‌ایم.
          • Collection , CollectionId : کلید خارجی ما در دیتابیس ایجاد شده خواهد بود که نشان دهنده‌ی ارتباط یک به چند بین کلکسیون و پست‌ها می‌باشد.
          • IsPin : اگر لازم است پستی به عنوان اولین پست در کلکسیون نمایش داده شود، این خصوصیت برای آن true خواهد بود.
          • ApprovedCommentsCount , UnApprovedCommentsCount: برای افزایش کارآیی سیستم در نظر گرفته شده است و هنگام درج نظر جدید یا حذف نظر، ویرایش خواهند شد. 

          مدل نظرات ارسالی در کلکسیون ها

           public class CollectionComment 
              {
                  #region Ctor
                  public CollectionComment()
                  {
                      Id = SequentialGuidGenerator.NewSequentialGuid();
                      CreatedOn = DateTime.Now;
          
                  }
                  #endregion
          
                  #region Properties
                  /// <summary>
                  /// get or set identifier of record
                  /// </summary>
                  public virtual Guid Id { get; set; }
                  /// <summary>
                  /// gets or sets date of creation 
                  /// </summary>
                  public virtual DateTime CreatedOn { get; set; }
                  /// <summary>
                  /// gets or sets body of blog post's comment
                  /// </summary>
                  public virtual string Body { get; set; }
                  /// <summary>
                  /// gets or sets body of blog post's comment
                  /// </summary>
                  public virtual Rating Rating { get; set; }
                  /// <summary>
                  /// gets or sets informations of agent
                  /// </summary>
                  public virtual string UserAgent { get; set; }
                  /// <summary>
                  /// indicate this comment is Approved 
                  /// </summary>
                  public virtual bool IsApproved { get; set; }
                  /// <summary>
                  /// gets or sets Ip Address of Creator
                  /// </summary>
                  public virtual string CreatorIp { get; set; }
                  /// <summary>
                  /// gets or sets datetime that is modified
                  /// </summary>
                  public virtual DateTime? ModifiedOn { get; set; }
                  /// <summary>
                  /// gets or sets counter for report this comment
                  /// </summary>
                  public virtual int ReportsCount { get; set; }
                  /// <summary>
                  /// indicate this entity is Locked for Modify
                  /// </summary>
                  public virtual bool ModifyLocked { get; set; }
                  /// <summary>
                  /// gets or sets date that this entity repoted last time
                  /// </summary>
                  public virtual DateTime? ReportedOn { get; set; }
                  #endregion
          
                  #region NavigationProperties
                  /// <summary>
                  /// gets or sets post that this comment added it
                  /// </summary>
                  public virtual CollectionPost Post { get; set; }
                  /// <summary>
                  /// gets or sets id of post that this comment added it
                  /// </summary>
                  public virtual Guid PostId { get; set; }
                  /// <summary>
                  /// get or set user that create this record
                  /// </summary>
                  public virtual User Creator { get; set; }
                  /// <summary>
                  /// get or set Id of user that create this record
                  /// </summary>
                  public virtual long CreatorId { get; set; }
                  /// <summary>
                  /// gets or sets CollectionComment's identifier for Replying and impelemention self referencing
                  /// </summary>
                  public virtual Guid? ReplyId { get; set; }
                  /// <summary>
                  /// gets or sets Collection's comment for Replying and impelemention self referencing
                  /// </summary>
                  public virtual CollectionComment Reply { get; set; }
                  /// <summary>
                  /// get or set collection of Collection's comment for Replying and impelemention self referencing
                  /// </summary>
                  public virtual ICollection<CollectionComment> Children { get; set; }
                  #endregion
              }

          مدل بالا نشان دهنده‌ی نظرات ارسالی برای پست‌های کلکسیون‌ها می‌باشد. صرفا کاربران عضو سیستم این اجازه را در صورتی خواهند داشت که برای پست مورد نظر خصوصیت AllowComments با مقدار true مقدار دهی شده باشد
          حالت درختی آن مشخص است. برای اعمال ارتباط یک به چند بین پست‌ها و نظرات، از CollectionPost و CollectionPostId استفاده خواهد شد.

          • IsApproved : برای زمانی استفاده خواهد شد که خصوصیت ModerateComments پست مورد نظر با مقدار true مقدار دهی شده باشد. 
          • ReportsCount : به مانند بخش‌های قبل، تعداد اخطار‌های داده شده‌ی برای یک نظر را نشان خواهد داد. 
          • Creator,CreatorId : ارسال کننده‌ی نظر می‌باشد و برای ایجاد ارتباط یک به چند بین کاربر و نظرات کلکسیون‌ها در نظر گرفته شده‌اند. 
          • ReportedOn : نگه داری آخرین تاریخ اخطار 
          • ModifyLocked : به منظور ممانعت از ویرایش

          مدل فایل‌های ضمیمه کلکسیون ها

          public class CollectionAttachment : BaseAttachment
              {
                  #region NavigationProperties
                  /// <summary>
                  /// gets or sets Collection  that this file attached 
                  /// </summary>
                  public virtual Collection Collection { get; set; }
                  /// <summary>
                  /// gets or sets Id of Collection  that this file attached
                  /// </summary>
                  public virtual Guid? CollectionId { get; set; }
                  #endregion
              }
          اگر یادتان باشد در مقاله‌ی دوم در مورد نحوه‌ی مدیریت فایل‌ها بحث شد و در نتیجه تصمیم گرفته شد که از ارث بری TPH استفاده کنیم. مدل بالا نیز یکی از SubClass‌های ما خواهد بود. با توجه به اینکه شاید Privacy فایل‌های ارسالی یک گروه مهم باشد (در صورت خصوصی بودن یا پولی بودن مطالعه‌ی کلکسیون)  نیاز شد مدل بالا را نیز داشته باشیم. برای اعمال ارتباط یک به چند بین مدل بالا و مدل کلکسیون، از خصوصیت‌های Colllection , CollectionId استفاده خواهیم کرد. دقت کنید که لازم است CollectionId به صورت نال پذیر درنظر گرفته شود.

          مدل آگهی ها

          /// <summary>
              /// Represents Announcement For Announcement Section
              /// </summary>
              public class Announcement : BaseContent
              {
                  #region Properties
                  /// <summary>
                  /// gets or sets Date that this Announcement will Expire
                  /// </summary>
                  public virtual DateTime? ExpireOn { get; set; }
                  /// <summary>
                  /// indicate this accouncement is approved by admin if announcementSetting.Moderate==true
                  /// </summary>
                  public virtual bool IsApproved { get; set; }
                  #endregion
          
                  #region NavigationProperties
                  /// <summary>
                  /// get or set Collection of Comments for this Announcement
                  /// </summary>
                  public virtual ICollection<AnnouncementComment> Comments { get; set; }
          
                  #endregion
              }
          مدل بالا نشان دهنده‌ی آگهی‌ها سیستم خواهد بود. همان طور که مشخص است، این مدل نیز از کلاس پایه BaseContent ارث بری کرده و علاوه بر آن یکسری خصوصیت دیگر را به شرح زیر دارد:
          • ExpireOn : زمان انقضای آگهی 
          • IsApproved : به منظور اعمال مدیریتی در نظر گرفته شده است
          • Comments : اگر امکان ارسال نظرات برای آگهی از بخش تنظیمات فعال باشد، این لیست نظرات ما را نگه داری خواهد کرد. لذا یک رابطه‌ی یک به چند بین نظرات و آگهی‌ها خواهد بود.

          مدل نظرات آگهی ها

          /// <summary>
              /// Repersents Comment For Announcement
              /// </summary>
              public class AnnouncementComment : BaseComment
              {
                  #region NavigationProperties
                  /// <summary>
                  /// gets or sets body of announcement's comment
                  /// </summary>
                  public virtual long? ReplyId { get; set; }
                  /// <summary>
                  /// gets or sets body of announcement's comment
                  /// </summary>
                  public virtual AnnouncementComment Reply { get; set; }
                  /// <summary>
                  /// gets or sets body of announcement's comment
                  /// </summary>
                  public virtual ICollection<AnnouncementComment> Children { get; set; }
                  /// <summary>
                  /// gets or sets announcement that this comment sent to it
                  /// </summary>
                  public virtual Announcement Announcement { get; set; }
                  /// <summary>
                  /// gets or sets announcement'Id that this comment sent to it
                  /// </summary>
                  public virtual long AnnouncementId { get; set; }
                  #endregion
              }
          مدل بالا  نشان دهنده‌ی نظرات ارسالی برای آگهی‌ها می‌باشد. از کلاس پایه‌ی مطرح شده‌ی در مقاله‌ی اولارث بری کرده و علاوه بر آن ساختار درختی آن مشخص است و همچنین برای برقراری ارتباط یک به چند با مدل آگهی‌ها، خصوصیات Announcement  , AnnouncementId در نظر گرفته شده‌اند.

          مدل سیستم لاگ عملیات کاربران

          /// <summary>
              /// Represent The Operation's log
              /// </summary>
              public class AuditLog
              {
                  #region Ctor
                  /// <summary>
                  /// 
                  /// </summary>
                  public AuditLog()
                  {
                      Id = SequentialGuidGenerator.NewSequentialGuid();
                      OperatedOn = DateTime.Now;
                  }
                  #endregion
          
                  #region Properties
                  /// <summary>
                  /// sets or gets identifier of AuditLog
                  /// </summary>
                  public virtual Guid Id { get; set; }
                  /// <summary>
                  /// sets or gets description of Log
                  /// </summary>
                  public virtual string Description { get; set; }
                  /// <summary>
                  /// sets or gets when log is operated
                  /// </summary>
                  public virtual DateTime OperatedOn { get; set; }
                  /// <summary>
                  /// sets or gets log's section
                  /// </summary>
                  public virtual AuditSection Section { get; set; }
                  #endregion
          
                  #region NavigationProperties
                  /// <summary>
                  /// sets or gets log's creator
                  /// </summary>
                  public virtual User OperatedBy { get; set; }
                  /// <summary>
                  /// sets or gets identifier of log's creator
                  /// </summary>
                  public virtual long OperatedById { get; set; }
                  #endregion
              }
          
            public enum AuditSection
              {
                  Blog,
                  News,
                  Forum,
                  ...
              }
          مدل بالا نگهدارنده زمان اکشن انجام شده، توسط کاربری که انجام شده و یه سری توضیحات کلی می‌باشد. به منظور اعمال ارتباط یک به چند مابین کاربر و مدل بالا، خصوصیات OperatedBy و OperatedById را در نظر گرفته‌ایم. خصوصیت Section که از نوع، نوع داده شمارشی AuditSection می‌باشد، می‌تواند برای جداسازی این لاگ‌های ذخیره شده، مفید باشد.

          مدل دامین‌های ممنوع

          در پنل مدیریت امکانی را خواهیم داشت تا یکسری از دامین‌های مد نظر را که نمی‌خواهیم در محتوای سیستم، آدرس صفحات آنها دیده شوند و همچنین برای عدم ارسال و دریافت پینگ بک‌ها لحاظ شوند، ثبت کنیم.
           /// <summary>
              /// Represents Domain that is banned
              /// </summary>
              public class BannedDomain
              {
                  #region Propertie
                  /// <summary>
                  /// gets or sets identifier of Domain
                  /// </summary>
                  public virtual Guid Id { get; set; }
                  /// <summary>
                  /// gets or sets DomainName
                  /// </summary>
                  public virtual string Name { get; set; }
                  /// <summary>
                  /// gets or sets Date that this record added
                  /// </summary>
                  public virtual DateTime BannedOn { get; set; }
                  #endregion
              }

          مدل کلمات ممنوع 

           /// <summary>
              /// Represents the banned words
              /// </summary>
              public class BannedWord
              {
                  #region Ctor
                  /// <summary>
                  /// Create one instance of <see cref="BannedWord"/>
                  /// </summary>
                  public BannedWord()
                  {
                      Id = SequentialGuidGenerator.NewSequentialGuid();
                  }
                  #endregion
          
                  #region Properties
                  /// <summary>
                  /// gets or sets identifier of BannedWord
                  /// </summary>
                  public Guid Id { get; set; }
                  /// <summary>
                  /// gets or sets Bad word
                  /// </summary>
                  public string BadWord { get; set; }
                  /// <summary>
                  /// gets or sets Good replaceword
                  /// </summary>
                  public string GoodWord { get; set; }
                  /// <summary>
                  /// indicating that this Word is spam
                  /// </summary>
                  public bool IsStopWord { get; set; }
                  #endregion
          
              }
          مدل بالا نشان دهنده‌ی کلماتی است که لازم است در متن مطالب سیستم حذف یا با کلمات جدید جایگزین شوند.
          • BadWord : کلمه مورد نظر که قرار است Ban شود.
          • IsStopWord : اگر لازم نیست جایگزینی برای کلمه استفاده شود و فقط لازم است حذف گردد، مقدار این خصوصیت true خواهد بود.
          • GoodWord : کلمه جایگزین 

          مدل تنظیمات سیستم

           /// <summary>
              /// Represent The CMS setting
              /// </summary>
              public class Setting
              {
                  #region Properties
                  /// <summary>
                  /// sets or gets name of setting
                  /// </summary>
                  public virtual string Name { get; set; }
                  /// <summary>
                  /// sets or gets value of setting
                  /// </summary>
                  public virtual string Value { get; set; }
                  /// <summary>
                  /// sets or gets Type of setting
                  /// </summary>
                  public virtual string Type { get; set; }
                  #endregion
              }
          مدل بالا کل تنظیمات سیستم را در قالب Key , Value نگهداری خواهد کرد. برای توضیحات این قسمت می‌توانید به مقاله‌ی "ذخیره تنظیمات متغیر مربوط به یک وب اپلیکیشن ASP.NET MVC با استفاده از EF" رجوع کنید.
          پ.ن:در مقاله‌ی بعد با پیاده سازی مدل‌های مدیریت کاربران، سیستم پیام رسانی، سیستم ترفیع رتبه و ارتباط دوستی، DomainClasses به اتمام خواهد رسید.

          نتیجه‌ی این قسمت 

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

          ‫توسعه سیستم مدیریت محتوای DNTCms - قسمت ششم

          $
          0
          0
          در این قسمت مدل‌های باقی مانده‌ی از بخش‌هایی را که در مقاله اول مطرح شدند، به اتمام می‌رسانیم. همچنین با بازخوردهایی که در مقالات قبل گرفتیم، در این قسمت تغییرات ایجاد شده‌ی در مدل‌های قسمت‌های قبل را نیز مطرح خواهیم کرد.

          مدل‌های AuditLog (اصلاحیه)و ActivityLog

          باید توجه داشت که اگر سیستم AuditLog، جزئیات بیشتری را در بر بگیرد، می‌توان از آن به عنوان History هم یاد کرد. در قسمت چهارم برای پست‌های انجمن یک جدول جدا هم به منظور ذخیره سازی تاریخچه‌ی تغییرات، در نظر گرفتیم. فرض کنید که یک سری از جداول دیگر هم نیازمند این امکان باشند! راه حل چیست؟
          1. استفاده از جداول جدا برای هر کدام از جداول به صورتیکه یک ارتباط یک به چند مابین آنها برقرار است. از این جداول تحت عنوان HistoryTable یاد می‌شود.
          2. استفاده از یک جدول برای نگهداری تاریخچه‌ی تغییرات جداولی که نیازمند این امکان هستند. 
          در زیر پیاده سازی از روش دوم رو مشاهده میکنید.
            /// <summary>
              /// Represent The Operation's log
              /// </summary>
              public class AuditLog
              {
                  #region Ctor
                  /// <summary>
                  /// Create One Instance Of <see cref="AuditLog"/>
                  /// </summary>
                  public AuditLog()
                  {
                      Id = SequentialGuidGenerator.NewSequentialGuid();
                      OperatedOn = DateTime.Now;
                  }
                  #endregion
          
                  #region Properties
                  /// <summary>
                  /// sets or gets identifier of AuditLog
                  /// </summary>
                  public virtual Guid Id { get; set; }
                  /// <summary>
                  /// gets or sets Type of  Modification(create,softDelet,Delete,update)
                  /// </summary>
                  public virtual AuditAction Action { get; set; }
                  /// <summary>
                  /// sets or gets description of Log
                  /// </summary>
                  public virtual string Description { get; set; }
                  /// <summary>
                  /// sets or gets when log is operated
                  /// </summary>
                  public virtual DateTime OperatedOn { get; set; }
                  /// <summary>
                  /// sets or gets Type Of Entity 
                  /// </summary>
                  public virtual string Entity { get; set; }
                  /// <summary>
                  /// gets or sets  Old value of  Properties before modification
                  /// </summary>
                  public virtual string XmlOldValue { get; set; }
                  /// <summary>
                  /// gets or sets XML Base OldValue of Properties (NotMapped)
                  /// </summary>
                  public virtual XElement XmlOldValueWrapper
                  {
                      get { return XElement.Parse(XmlOldValue); }
                      set { XmlOldValue = value.ToString(); }
                  }
                  /// <summary>
                  /// gets or sets new value of  Properties after modification
                  /// </summary>
                  public virtual string XmlNewValue { get; set; }
                  /// <summary>
                  /// gets or sets XML Base NewValue of Properties (NotMapped)
                  /// </summary>
                  public virtual XElement XmlNewValueWrapper
                  {
                      get { return XElement.Parse(XmlNewValue); }
                      set { XmlNewValue = value.ToString(); }
                  }
                  /// <summary>
                  /// gets or sets Identifier Of Entity
                  /// </summary>
                  public virtual string EntityId { get; set; }
                  /// <summary>
                  /// gets or sets user agent information
                  /// </summary>
                  public virtual string Agent { get; set; }
                  /// <summary>
                  /// gets or sets user's ip address
                  /// </summary>
                  public virtual string OperantIp { get; set; }
                  #endregion
          
                  #region NavigationProperties
                  /// <summary>
                  /// sets or gets log's creator
                  /// </summary>
                  public virtual User Operant { get; set; }
                  /// <summary>
                  /// sets or gets identifier of log's creator
                  /// </summary>
                  public virtual long OperantId { get; set; }
                  #endregion
              }
            public enum AuditAction
              {
                  Create,
                  Update,
                  Delete,
                  SoftDelete,
              }

          خصوصیاتی که نیاز به توضیح خواهند داشت:
          • Action : از نوع AdutiAction است و برای مشخص کردن نوع عملیاتی که انجام شده است، می‌باشد.
          • Description : اگر نیاز باشد توضیحاتی اضافی ثبت شوند، از این خصوصیت استفاده می‌شود.
          • Entity : مشخص کننده‌ی نام مدل خواهد بود. شاید بهتر بود از یک Enum استفاده می‌شد. ولی این سیستم به احتمال زیاد قرار است افزونه پذیر باشد و استفاده از Enum، یعنی محدودیت و این امکان وجود نخواهد داشت که سایر افزونه‌ها بتوانند از مدل بالا استفاده کنند. برا ی مثال BlogPost , NewsItem , ForumPost , ...
          • EntitytId : آی دی رکوردی است که تاریخچه‌ی آن ثبت شده است. از آنجائیکه بعضی از موجودیت‌ها دارای آی دی از نوع long و برخی دیگر Guid ، لذا ذخیره‌ی رشته‌ای آن مفید خواهد بود.
          • XmlOldValue : در برگیرنده‌ی مقدار (قبل از اعمال تغییرات) خصوصیاتی است که لازم است از یک موجودیت مشخص، در قالب XML رشته‌ای ذخیره شوند.
          • XmlNewValue : در برگیرنده‌ی مقدار (بعد از تغییرات) خصوصیاتی است که لازم است از یک موجودیت مشخص، در قالب XML رشته‌ای ذخیره شوند.
          • Operant, OperantId: برای برقراری ارتباط یک به چند مابین مدل کاربر و مدل بالا در نظر گرفته شده‌اند که به عنوان انجام دهنده‌ی این تغییرات بوده است.
          با استفاده از مدل بالا می‌توان متوجه شد که کاربر x چه خصوصیاتی از موجودیت y را تغییر داده است و این خصوصیات قبل از تغییر چه مقدارهایی داشته‌اند.
            /// <summary>
              /// Represents Activity Log record
              /// </summary>
              public class ActivityLog
              {
                  #region Ctor
                  /// <summary>
                  /// Create one instance of <see cref="ActivityLog"/>
                  /// </summary>
                  public ActivityLog()
                  {
                      Id = SequentialGuidGenerator.NewSequentialGuid();
                      OperatedOn=DateTime.Now;
                  }
                  #endregion
          
                  #region Properties
                  /// <summary>
                  /// gets or sets identifier 
                  /// </summary>
                  public virtual Guid Id { get; set; }
                  /// <summary>
                  /// gets or sets the comment of this activity
                  /// </summary>
                  public virtual string Comment { get; set; }
                  /// <summary>
                  /// gets or sets the date that this activity was done
                  /// </summary>
                  public virtual DateTime OperatedOn { get; set; }
                  /// <summary>
                  /// gets or sets the page url . 
                  /// </summary>
                  public virtual string Url { get; set; }
                  /// <summary>
                  /// gets or sets the title of page if Url is Not null
                  /// </summary>
                  public virtual string Title { get; set; }
                  /// <summary>
                  /// gets or sets user agent information
                  /// </summary>
                  public virtual string Agent { get; set; }
                  /// <summary>
                  /// gets or sets user's ip address
                  /// </summary>
                  public virtual string OperantIp { get; set; }
                  #endregion
          
                  #region NavigationProperties
                  /// <summary>
                  /// gets or sets the type of this activity
                  /// </summary>
                  public virtual ActivityLogType Type{ get; set; }
                  /// <summary>
                  /// gets or sets the  type's id of this activity
                  /// </summary>
                  public virtual Guid TypeId { get; set; }
                  /// <summary>
                  /// gets or sets User that done this activity
                  /// </summary>
                  public virtual User Operant { get; set; }
                  /// <summary>
                  /// gets or sets Id of User that done this activity
                  /// </summary>
                  public virtual long OperantId { get; set; }
                  #endregion
              }
          
             /// <summary>
              /// Represents Activity Log Type Record
              /// </summary>
              public class ActivityLogType
              {
                  #region Ctor
                  /// <summary>
                  /// Create one Instance of <see cref="ActivityLogType"/>
                  /// </summary>
                  public ActivityLogType()
                  {
                      Id = SequentialGuidGenerator.NewSequentialGuid();
                  }
                  #endregion
          
                  #region Properties
                  /// <summary>
                  /// gets or sets identifier 
                  /// </summary>
                  public virtual Guid Id { get; set; }
                  /// <summary>
                  /// gets or sets the system name
                  /// </summary>
                  public virtual string Name{ get; set; }
                  /// <summary>
                  /// gets or sets the display name
                  /// </summary>
                  public virtual string DisplayName { get; set; }
                  /// <summary>
                  /// gets or sets the description 
                  /// </summary>
                  public virtual string Description { get; set; }
                  /// <summary>
                  /// indicate this log type is enable for logging
                  /// </summary>
                  public virtual bool IsEnabled { get; set; }
                  #endregion
              }
          مدل‌های بالا هم برای ثبت لاگ فعالیت‌های کاربران در سیستم در نظر گرفته شده است . برای مثال اگر بخش آخرین تغییرات سایت جاریرا هم مشاهده کنید، یک همچین سیستمی را هم دارد. این لاگ‌ها برای ردیابی عملکرد کاربران در سیستم مفید خواهد بود.
          • Comment : توضیحات کوتاهی از اکشنی که کاربر انجام داده است.
          • Url : آدرس صفحه‌ای که این عملیات در آنجا انجام شده است. این خصوصیت نال‌پذیر می‌باشد.
          • Title : عنوان صفحه‌ای که این عملیات در آنجا انجام شده است؛ اگر Url نال نباشد.
          • Operant , OperantId : برای برقراری ارتباط یک به چند بین کاربر و مدل فعالیت‌ها در نظر گرفته شده‌اند.
          • Type : از نوع ActivityLogType پیاده سازی شده در بالا می‌باشد. با استفاده از مدل ActivityLogType می‌توان مثلا لاگ فعالیت مربوط به بخش اخبار را غیر فعال کند یا بالعکس و از این موارد.
          خصوصیات مدل ActivityLogType :
          • Name : نام سیستمی آن است. برای مثال : Login ، NewsComment ، NewsItem و ...
          • IsEnabled : نشان دهنده‌ی این است که این نوع لاگ فعال است یا خیر.

            کلاس پایه تمام مدل‌ها (اصلاحیه)

            /// <summary>
                /// Represents the  entity
                /// </summary>
                /// <typeparam name="TForeignKey">type of user's Id that can be long or long? </typeparam>
                public abstract class Entity<TForeignKey>
                {
                    #region Properties
                    /// <summary>
                    /// gets or sets date that this entity was created
                    /// </summary>
                    public virtual DateTime CreatedOn { get; set; }
                    /// <summary>
                    /// gets or sets Date that this entity was updated
                    /// </summary>
                    public virtual DateTime ModifiedOn { get; set; }
                    /// <summary>
                    /// gets or sets IP Address of Creator
                    /// </summary>
                    public virtual string CreatorIp { get; set; }
                    /// <summary>
                    /// gets or set IP Address of Modifier
                    /// </summary>
                    public virtual string ModifierIp { get; set; }
                    /// <summary>
                    /// indicate this entity is Locked for Modify
                    /// </summary>
                    public virtual bool ModifyLocked { get; set; }
                    /// <summary>
                    /// indicate this entity is deleted softly
                    /// </summary>
                    public virtual bool IsDeleted { get; set; }
                    /// <summary>
                    /// gets or sets information of user agent of modifier
                    /// </summary>
                    public virtual string ModifierAgent { get; set; }
                    /// <summary>
                    /// gets or sets information of user agent of Creator
                    /// </summary>
                    public virtual string CreatorAgent { get; set; }
                    /// <summary>
                    /// gets or sets date that this entity repoted last time
                    /// </summary>
                    public virtual DateTime? ReportedOn { get; set; }
                    /// <summary>
                    /// gets or sets counter for Content's report
                    /// </summary>
                    public virtual int ReportsCount { get; set; }
                    /// <summary>
                    /// gets or sets count of Modification Default is 1
                    /// </summary>
                    public virtual int Version { get; set; }
                    /// <summary>
                    /// gets or sets action (create,update,softDelete) 
                    /// </summary>
                    public virtual AuditAction Action { get; set; }
                    /// <summary>
                    /// gets or sets TimeStamp for prevent concurrency Problems
                    /// </summary>
                    public virtual byte[] RowVersion { get; set; }
                    #endregion
            
                    #region NavigationProperties
                    /// <summary>
                    /// gets ro sets User that Modify this entity
                    /// </summary>
                    public virtual User ModifiedBy { get; set; }
                    /// <summary>
                    /// gets ro sets Id of  User that modify this entity
                    /// </summary>
                    public virtual TForeignKey ModifiedById { get; set; }
                    /// <summary>
                    /// gets ro sets User that Create this entity
                    /// </summary>
                    public virtual User CreatedBy { get; set; }
                    /// <summary>
                    /// gets ro sets User that Create this entity
                    /// </summary>
                    public virtual TForeignKey CreatedById { get; set; }
                    #endregion
                }
            
              /// <summary>
                /// Represents the base Entity
                /// </summary>
                /// <typeparam name="TKey">type of Id</typeparam>
                /// <typeparam name="TForeignKey">type of User's Id that can be long or long?</typeparam>
                public abstract class BaseEntity<TKey,TForeignKey> : Entity<TForeignKey>
                {
                    #region Properties
                    /// <summary>
                    /// gets or sets Identifier of this Entity
                    /// </summary>
                    public virtual TKey Id { get; set; }
                    #endregion
                }
            از دو کلاس معرفی شده‌ی در بالا برای کپسوله کردن یکسری خصوصیات تکراری استفاده شده است. البته با بهبودهایی نسبت به مقاله‌ی قبل که با مشاهده‌ی خصوصیات آنها قابل فهم خواهد بود. در برخی از مدل‌ها، برای مثال نظرات وبلاگ امکان ارسال نظر برای افراد Anonymous هم وجود داشت؛ لذا CreatedById امکان نال بودن را هم داشت. به همین دلیل برای کاهش کدها، کلاس‌های بالا را به صورت جنریک تعریف کردیم. فایل EDMX نهایی که در انتهای مقاله ضمیمه شده، برای درک تغییرات اعمال شده مفید خواهد.

            مدل سیستم آگاه سازی

            /// <summary>
                /// Represents the Notification Record
                /// </summary>
                public class Notification
                {
                    #region Ctor
                    /// <summary>
                    /// create one instance of <see cref="Notification"/>
                    /// </summary>
                    public Notification()
                    {
                        Id = SequentialGuidGenerator.NewSequentialGuid();
                        ReceivedOn = DateTime.Now;
                    }
                    #endregion
            
                    #region Properties
                    /// <summary>
                    /// gets or sets identifier
                    /// </summary>
                    public virtual Guid Id { get; set; }
                    /// <summary>
                    /// indicate that this notification is read by owner
                    /// </summary>
                    public virtual bool IsRead { get; set; }
                    /// <summary>
                    /// gets or sets notification's text body
                    /// </summary>
                    public virtual string Message { get; set; }
                    /// <summary>
                    /// gets or sets page url that this notification is related with it
                    /// </summary>
                    public virtual string Url { get; set; }
                    /// <summary>
                    /// gets or sets date that this Notification Received
                    /// </summary>
                    public virtual DateTime ReceivedOn { get; set; }
                    /// <summary>
                    /// gets or sets the type of notification
                    /// </summary>
                    public virtual NotificationType Type { get; set; }
                    #endregion
            
                    #region NavigationProperties
                    /// <summary>
                    /// gets or sets the id of user that is owner of this notification
                    /// </summary>
                    public virtual long OwnerId { get; set; }
                    /// <summary>
                    /// gets or sets the user that is owner of this notification
                    /// </summary>
                    public virtual User Owner { get; set; }
                    #endregion
                }
            
                public enum  NotificationType
                {
                    NewConversation,
                    NewConversationReply,
                    ...
                }
            در این سیستم برای اطلاع رسانی کاربر، علاوه بر ارسال ایمیل، بحث اطلاع رسانی RealTime را هم خواهیم داشت. اطلاع رسانی‌هایی که توسط کاربر خوانده نشده باشند، در جدول حاصل از مدل Notification ذخیره خواهند شد. خصوصیاتی که نیاز به توضیح دارند:
            • IsRead : مشخص کننده‌ی این است که یک اطلاع رسانی خوانده شده است یا خیر. در آخر هر روز اطلاع رسانی‌هایی که دارای خصوصیت IsRead با مقدار true هستند، حذف خواهند شد.
            • Url : برای مواردی که لازم است کاربر با کلیک بر روی آن به صفحه‌ی خاصی هدایت شود.
            • OwnerId, Owner : برای برقراری ارتباط یک به چند بین کاربر و مدل Notification در نظر گرفته شده‌اند.
            • Type : از نوع NotificationType و مشخص کننده‌ی نوع اطلاع رسانی می‌باشد .

              مدل Observation

               public class Observation
                  {
                      #region Ctor
                      /// <summary>
                      /// create one instance of <see cref="Observation"/>
                      /// </summary>
                      public Observation()
                      {
                          LastObservedOn = DateTime.Now;
                          Id = SequentialGuidGenerator.NewSequentialGuid();
                      }
                      #endregion
              
                      #region Properties
                      public virtual Guid Id { get; set; }
                      /// <summary>
                      /// gets or sets datetime of last visit 
                      /// </summary>
                      public virtual DateTime LastObservedOn { get; set; }
                      /// <summary>
                      /// gets or sets Id Of section That user is  observing the entity
                      /// </summary>
                      public virtual string SectionId { get; set; }
                      /// <summary>
                      /// gets or sets  section That user is  observing in it
                      /// </summary>
                      public virtual string Section { get; set; }
                      #endregion
              
                      #region NavigationProperites
                      /// <summary>
                      /// gets or sets user that observed the entity
                      /// </summary>
                      public virtual User Observer { get; set; }
                      /// <summary>
                      /// gets or sets identifier of user that observed the entity
                      /// </summary>
                      public virtual long ObserverId { get; set; }
                      #endregion
                  }
              این مدل برای ایجاد امکانی به منظور واکشی لیست افردای که در حال مشاهده‌ی یک بخش خاص هستند، مفید است. فرض کنید در یک انجمن قصد دارید لیست افردای را که در حال مشاهده‌ی آن هستند، در پایین صفحه نمایش دهید. برای تاپیک‌ها هم همین امکان لازم است. لذا مدل بالا مختص مدل خاصی نیست و برای هر بخشی می‌توان از آن استفاده کرد. 
              فرض کنیم کاربری قصد هدایت به یک تاپیک را دارد. لذا هنگام هدایت شدن لازم است رکوردی در جدول حاصل از مدل بالا ثبت شود که کاربر x در بخش Topic، در تاریخ d، تاپیک به شماره‌ی y را مشاهده کرد. در صفحه‌ی مشاهده‌ی تاپیک می‌توان لیست افرادی را که قبل از مدت زمان مشخصی تاپیک را مشاهده کرده اند، نمایش داد. این رکورد‌ها را هم با تعریف یک Task می‌توان در بازه‌های زمانی مشخصی حذف کرد.

                  مدل صفحات داینامیک

                  /// <summary>
                      /// represents one custom page
                      /// </summary>
                      public class Page : BaseEntity<long, long>
                      {
                          #region Properties
                          /// <summary>
                          /// gets or sets the blog pot body
                          /// </summary>
                          public virtual string Body { get; set; }
                          /// <summary>
                          /// gets or sets the content title
                          /// </summary>
                          public virtual string Title { get; set; }
                          /// <summary>
                          /// gets or sets value  indicating Custom Slug
                          /// </summary>
                          public virtual string SlugUrl { get; set; }
                          /// <summary>
                          /// gets or sets meta title for seo
                          /// </summary>
                          public virtual string MetaTitle { get; set; }
                          /// <summary>
                          /// gets or sets meta keywords for seo
                          /// </summary>
                          public virtual string MetaKeywords { get; set; }
                          /// <summary>
                          /// gets or sets meta description of the content
                          /// </summary>
                          public virtual string MetaDescription { get; set; }
                          /// <summary>
                          /// gets or sets 
                          /// </summary>
                          public virtual string FocusKeyword { get; set; }
                          /// <summary>
                          /// gets or sets value indicating whether the content use CanonicalUrl
                          /// </summary>
                          public virtual bool UseCanonicalUrl { get; set; }
                          /// <summary>
                          /// gets or sets CanonicalUrl That the Post Point to it
                          /// </summary>
                          public virtual string CanonicalUrl { get; set; }
                          /// <summary>
                          /// gets or sets value indicating whether the content user no Follow for Seo
                          /// </summary>
                          public virtual bool UseNoFollow { get; set; }
                          /// <summary>
                          /// gets or sets value indicating whether the content user no Index for Seo
                          /// </summary>
                          public virtual bool UseNoIndex { get; set; }
                          /// <summary>
                          /// gets or sets value indicating whether the content in sitemap
                          /// </summary>
                          public virtual bool IsInSitemap { get; set; }
                          /// <summary>
                          /// gets or sets title for snippet
                          /// </summary>
                          public string SocialSnippetTitle { get; set; }
                          /// <summary>
                          /// gets or sets description for snippet
                          /// </summary>
                          public string SocialSnippetDescription { get; set; }
                          /// <summary>
                          /// gets or sets section's type that this page show on
                          /// </summary>
                          public virtual ShowPageSection Section { get; set; }
                          /// <summary>
                          /// indicate this page has not any body
                          /// </summary>
                          public virtual bool IsCategory { get; set; }
                          /// <summary>
                          /// gets or sets order for display forum
                          /// </summary>
                          public virtual int DisplayOrder { get; set; }
                  
                          #endregion
                  
                          #region NavigationProeprties
                          /// <summary>
                          /// gets or sets Parent of this page
                          /// </summary>
                          public virtual Page Parent { get; set; }
                          /// <summary>
                          /// gets or sets parent'id of this page
                          /// </summary>
                          public virtual long? ParentId { get; set; }
                          /// <summary>
                          /// get or set collection of page that they are children of this page
                          /// </summary>
                          public virtual ICollection<Page> Children { get; set; }
                          #endregion
                      }
                  
                    public enum ShowPageSection
                      {
                          Menu,
                          Footer,
                          SideBar
                      }
                  مدل بالا مشخص کننده‌ی صفحاتی است که مدیر می‌تواند در پنل مدیریتی آنها را برای استفاده‌های خاصی تعریف کند. حالت درختی آن مشخص است. یکسری از خصوصیات مربوط به محتوای صفحه و همچنین تنظیمات سئو برای آن در نظر گرفته شده است که بیشتر آنها در مقالات قبل توضیح داده شده‌اند. خصوصیت Section از نوع ShowPageSection و برای مشخص کردن امکان نمایش صفحه‌ی مورد نظر در نظر گرفته شده‌است. همچنین این مدل بالا از کلاس پایه‌ی مطرح شده‌ی در اول مقاله، ارث بری کرده است که امکان ردیابی تغییرات آن را مهیا می‌کند.
                    خوب! حجم مقاله زیاد شده است و تا اینجا کافی خواهد بود ؛ بر خلاف تصور بنده، یک مقاله‌ی دیگر نیز برای اتمام بحث لازم میباشد.

                  نتیجه‌ی تا این قسمت


                  ‫استفاده از GZip توکار IISهای جدید و تنظیمات مرتبط با آن‌ها

                  $
                  0
                  0
                  یکی از نقش‌های IISهای جدید (از نگارش 7 به بعد) که در ویندوز سرورهای قابل نصب است، نقش Performance است و ذیل آن دو نقش فشرده سازی استاتیک و پویا قابل انتخاب است. اگر این نقش‌ها بر روی سرور نصب باشند، دیگر نیازی به استفاده از HTTP Moduleهای متداول فشرده سازی صفحات وب نیست. برای استفاده‌ی از آن تنها کافی است کمی web.config را ویرایش کرد و ... گفته شده‌است که کار می‌کند! اما پس از اعمال تنظیمات، اگر به هدرهای خروجی Response صفحه در ابزارهای web developer مرورگرها دقت کنید، خبری از encoding جدیدی به نام gzip نیست (Content-Encoding: gzip) و به نظر اعمال نمی‌شود. در ادامه بررسی خواهیم کرد که چرا اینگونه است.


                  فعال سازی GZip توکار IIS

                  تنظیمات پیش فرض فعال سازی ماژول توکار GZip وب سرورهای جدید شامل دو مرحله است:
                  الف) تنظیمات سرور جهت فعال سازی فشرده سازی
                  بر روی ویندوزهای سرور، پس از مراجعه به Administrative Tools -> Server Manager و گشودن Roles آن، ذیل قسمت Web Server که در اینجا IIS است، نیاز است نقش جدیدی به نام Performance اضافه شود و مطابق تصویر، هر دو گزینه‌ی فشرده سازی استاتیک و پویا انتخاب گردد.


                  بنابراین اولین قدم برای عیب یابی کار نکردن GZip توکار IIS، از این مرحله شروع می‌شود که آیا اصلا ماژول مربوطه نصب هست یا خیر؟

                  ب) تنظیمات برنامه جهت فعال سازی ماژول GZip
                  پس از اطمینان از نصب ماژول توکار فشرده سازی صفحات وب IIS در سمت تنظیمات سرور، اکنون باید چند سطر ذیل را به Web.Config برنامه اضافه کرد:
                  <system.webServer><httpCompression directory="%SystemDrive%\inetpub\temp\IIS Temporary Compressed Files"><scheme name="gzip" dll="%Windir%\system32\inetsrv\gzip.dll" staticCompressionLevel="9" /><dynamicTypes><add mimeType="text/*" enabled="true" /><add mimeType="message/*" enabled="true" /><add mimeType="application/x-javascript" enabled="true" /><add mimeType="application/javascript" enabled="true" /><add mimeType="application/json" enabled="true" /><add mimeType="application/json; charset=utf-8" enabled="true" /><add mimeType="application/atom+xml" enabled="true" /><add mimeType="application/xaml+xml" enabled="true" /><add mimeType="*/*" enabled="false" /></dynamicTypes><staticTypes><add mimeType="text/*" enabled="true" /><add mimeType="message/*" enabled="true" /><add mimeType="application/x-javascript" enabled="true" /><add mimeType="application/javascript" enabled="true" /><add mimeType="application/json" enabled="true" /><add mimeType="application/json; charset=utf-8" enabled="true" /><add mimeType="application/atom+xml" enabled="true" /><add mimeType="application/xaml+xml" enabled="true" /><add mimeType="*/*" enabled="false" /></staticTypes></httpCompression><urlCompression doStaticCompression="true" doDynamicCompression="true" /></system.webServer>
                  در اینجا تنظیمات مخصوص نحوه‌ی فعال سازی فشرده سازی توکار صفحات پویا و فایل‌های استاتیک را مشاهده می‌کنید. در این تنظیمات محل قرارگیری فایل‌های موقتی فشرده شده‌ی توسط این ماژول و همچنین mime typeهای مدنظر جهت فشرده سازی، ذکر شده‌اند. با این تنظیمات، تنها mime typeهایی که به صورت صریح ذکر شده‌اند فشرده خواهند شد و از سایر mime types صرفنظر می‌شود.
                  این تنظیماتی است که در اکثر سایت‌ها نیز یافت می‌شود. ذکر آن‌ها اجباری است و پس از اعمال، اگر برنامه را اجرا کنید ... چیزی فشرده نمی‌شود! علت اصلی را باید در تنظیماتی یافت که مخصوص سرور است و در اینجا ذکر نشده‌اند.


                  تنظیمات مخصوص آستانه‌ی فشرده سازی صفحات

                  علت اصلی عدم مشاهده‌ی هدر gzip، در Response برنامه، به frequent hit threshold تنظیم شده‌ی در IIS بر می‌گردد. مقدار آن به 2 درخواست در طی 10 ثانیه تنظیم شده‌است. یعنی اگر به صفحه‌ای در طی 10 ثانیه دو درخواست نرسد، فشرده نخواهد شد. این تنظیم را می‌توان با مراجعه‌ی به configuration editor نود اصلی سرور وب در IIS manager، ویرایش کرد:



                  برای نمونه در تصویر فوق، این آستانه به یک درخواست در طی 10 ساعت تنظیم شده‌است. این عدد سبب خواهد شد تا تمامی درخواست‌های رسیده حتما فشرده سازی شوند.
                  این تنظیم معادل یک سطر ذیل در فایل web.config است. اما چون قسمت system.webServer/serverRuntime در تنظیمات سرور قفل شده‌است، هیچ تاثیری نخواهد داشت و حتما باید در سمت سرور و توسط IIS manager اعمال شود:
                  <system.webServer><serverRuntime frequentHitThreshold="1" frequentHitTimePeriod="10:00:00" /></system.webServer>
                  برای آزاد سازی این تنظیمات نیاز است دستور ذیل بر روی سرور اجرا شود. پس از آن کاربران برنامه‌های وب می‌توانند از تنظیمات وب کانفیگ خاص خود استفاده کنند:
                   C:\Windows\System32\inetsrv\appcmd.exe unlock config /section:system.webServer/serverRuntime

                  یک نکته
                  اگر از سرورهای پس از 2008 استفاده می‌کنید، گزینه‌ی staticCompressionIgnoreHitFrequency نیز به تنظیمات serverRuntime اضافه شده‌است که با تنظیم آن به true، از این حد آستانه، برای فایل‌های استاتیک صرفنظر خواهد شد.


                  تنظیمات مخصوص اندازه‌ی فایل‌هایی که باید فشرده سازی شوند

                  تنها حد آستانه‌ی درخواست صفحات وب نیست که بر روی فشرده سازی یا عدم آن ثاثیرگذار است. در اینجا میزان CPU Usage سیستم و یا حتی اندازه‌ی Response خروجی نیز مهم هستند که نمونه‌ای از تنظیمات آن‌را در شکل ذیل مشاهده می‌کنید:


                  در اینجا با تنظیم minFileSizeForComp به 1024، اعلام شده‌است که حجم‌هایی کمتر از یک کیلوبایت، فشرده سازی نشوند (مقدار پیش فرض آن، بیش از این عدد است).
                  البته این عدد را به شکل زیر نیز می‌توان به تنظیمات httpCompression وب کانفیگ اضافه کرد:
                  <httpCompression directory="%SystemDrive%\inetpub\temp\IIS Temporary Compressed Files" minFileSizeForComp="1024">

                  پس از اعمال این تنظیمات نیاز است یکبار IIS را نیز ری استارت کرد.


                  نتیجه گیری

                  اگر پس از فعال سازی GZip وب سرور، خروجی برنامه فشرده سازی نشد (Content-Encoding: gzip)، علت اینجا است که هنوز 2 درخواست مورد نیاز، در طی 10 ثانیه به سمت سرور ارسال نشده‌اند و تنظیمات پیش فرض این ماژول، جهت حداقل مصرف CPU و فشار بر روی سرور است.

                  ‫فیلترها در MVC

                  $
                  0
                  0
                  هنگامی که درخواستی به سرور ارسال می‌گردد، به کنترلر و اکشن مربوطه جهت پاسخگویی هدایت می‌شود. خب شاید مواقعی شما نیاز داشته باشید قبل یا بعد از اجرای اکشن متدی، کدی اجرا گردد. به‌همین جهت در MVC قابلیتی بنام Filter ارائه گردید.
                  فیلتر، یک کلاس سفارشی است که شما می‌توانید منطق برنامه را جهت اجرا، قبل یا بعد از اجرای یک اکشن متد، در آن پیاده سازی نمایید. فیلترها می‌توانند به یک اکشن متد و یا کنترلری منتسب شوند که در ادامه با این روشها آشنا خواهید شد.

                  در لیست زیر انواع فیلترها و اینترفیس‌هایی که باید توسط کلاس سفارشی شما پیاده سازی شوند، معرفی شده است.

                   نوعتوضیح
                   فیلتر توکار
                   اینترفیس
                   Authorization
                  انجام عملیات احراز هویت و سطح دسترسی، قبل از اجرای کد اکشن متد  
                   [Authorize] و [RequireHttps]  
                   IAuthorizationFilter 
                   Action
                  اجرای کدهایی قبل از اجرای کدهای اکشن متد 
                    IActionFilter 
                   Result
                  اجرای کدهایی قبل یا بعد از تولید ویو (View result) 
                   [OutputCache]   IResultFilter 
                  Exception
                  اجرای کدهایی در صورت وجود استثنای مدیریت نشده 
                  [HandleError] 
                  IExceptionFilter
                  مثال: هنگامی که خطایی در حین اجرای اکشن متدی رخ می‌دهد، فیلتر توکار MVC بنام HandleErrorاجرا می‌شود. این فیلتر توکار فایل Error.cshtml را که در فولدر Shared قرار دارد، رندر می‌کند و نمایش می‌دهد.
                  در تکه کد زیر نحوه‌ی استفاده از این فیلتر را مشاهده می‌کنید:
                  [HandleError]
                  public class HomeController : Controller
                  {
                      public ActionResult Index()
                      {
                          //throw exception for demo
                          throw new Exception("This is unhandled exception");
                          return View();
                      }
                  
                      public ActionResult About()
                      {
                          return View();
                      }
                  
                      public ActionResult Contact()
                      {
                          return View();
                      }        
                  }

                  نکته: فیلترهای اعمال شده‌ی به یک کنترلر، به تمام اکشن متدهای آن نیز اعمال می‌گردند. 

                  در کد بالا خصیصه‌ی HandleError به HomeController اعمال شده است. بنابراین در صورت بروز خطایی در هر کدام از اکشن‌ها، صفحه‌ی Error.cshtml نمایش داده خواهد شد و در تظر داشته باشید که خطاها توسط try-catch هندل نشده‌اند.
                  باید جهت عملکرد صحیح فیلتر توکار HandleErrorAttribute، مقدار customErrors در قسمت System.web فایل web.config مساوی on باشد.
                  <customErrors mode="On" />


                  مهیا کنندگان فیلترها

                  بصورت پیش فرض MVC از سه طریق زیر فیلترها را جهت استفاده‌ی در برنامه فراهم می‌کند:

                  1. خصیصه‌ی GlobalFilters.Filters برای فیلترهای سراسری
                  2. کلاس FilterAttributeFilterProvider برای فیلترهای خصیصه‌ای
                  3. کلاس ControllerInstanceFilterProvider جهت افزودن کنترلر به یک وهله از FilterProviderCollection

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

                  ترتیب اجرای فیلترها

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

                  1. فیلترهای Authorization
                  2. فیلترهای Action
                  3. فیلترهای Result یا Response
                  4. فیلترهای Exception

                  فیلترها براساس ترتیب اشاره شده‌ی در بالا اجرا خواهند شد. در صورتیکه چند فیلتر از یک نوع استفاده شود، جهت تقدم و تاخر در اجرا، از خاصیت Order استفاده خواهد شد. بعنوان مثال در کد زیر بدلیل خاصیت Order=1 ابتدا AuthorizationFilterB  و سپس AuthorizationFilterA اجرا می‌شود.

                  [AuthorizationFilterA(Order=2)]
                  [AuthorizationFilterB(Order=1)]
                  public ActionResult Index()
                  {          
                      return View();
                  }
                  علاوه بر خاصیت Order، مقدار Scopeنیز سطح سومی از ترتیب اجرای فیلترها می‌باشد. مقادیر Scope بشرح زیر است:
                  public enum FilterScope
                  {
                      First = 0,
                      Global = 10,
                      Controller = 20,
                      Action = 30,
                      Last = 100,
                  }
                  این خصیصه‌ی فیلترها در محل بکار گیری آنها مقدار دهی می‌شود. در صورتیکه فیلتری بصورت سراسری رجیستر شود، Scope آن برابر 10 و در سطح کنترلر، برابر 20 خواهد بود و الی آخر.

                  نکته: مقدار Scope فیلترهای Authorization برابر 0 و فیلترهای Exception برابر 100 می‌باشد.

                  ایجاد فیلتر سفارشی

                  روش اول:پیاده سازی اینترفیس یکی از انواع فیلترها و ارث بری از کلاس FilterAttribute

                  در این روش متدهایی که باید پیاده سازی شوند متفاوت خواهد بود. به همین جهت متدهای هر نوع بشرح زیر معرفی می‌شود:

                  • IAuthorizationFilter
                  // Called when authorization is required
                  void OnAuthorization(AuthorizationContext filterContext)
                  • IActionFilter
                  // Called after the action method executes
                  void OnActionExecuted(ActionExecutedContext filterContext)
                  
                  // Called before an action method executes
                  void OnActionExecuting(ActionExecutingContext filterContext)
                  • IResultFilter
                  // Called after an action result executes
                  void OnResultExecuted(ResultExecutedContext filterContext)
                  
                  // Called before an action result executes
                  void OnResultExecuting(ResultExecutedContext filterContext)
                  • IException
                  // Called when an exception occurs
                  void OnException(ExceptionContext filterContext)

                  یادآوری: همانطور که در ابتدای مقاله اشاره شد، فیلترها قبل یا بعد از اجرای اکشن متدها فراخوانی خواهند شد. بنابراین به کامنت بالای متد فیلترها دقت داشته باشید.

                  مثال: پیاده سازی اینترفیس IExceptionFilter و ارث بری از کلاس FilterAttribute جهت تهیه‌ی فیلتری سفارشی از نوع Exception

                  class CustomErrorHandler : FilterAttribute, IExceptionFilter
                  {
                      public override void IExceptionFilter.OnException(ExceptionContext filterContext)
                      {
                          Log(filterContext.Exception);
                  
                          base.OnException(filterContext);
                      }
                  
                      private void Log(Exception exception)
                      {
                          //log exception here..
                      }
                  }

                  روش دوم:
                  ارث بری از ActionFilterAttribute
                  کلاس abstract فوق دارای چهار متد زیر جهت تحریف است. همانطور که مشاهده می‌کنید این کلاس علاوه بر دو متد OnActionExecuted و OnActionExecuting دارای دو متد دیگر OnResultExecuting و OnResultExecuted که به‌ترتیب قبل و بعد خروجی (Result) اکشن متد اجرا می‌شوند، نیز می‌باشد. این نوع فیلترها عموما جنبه‌ی استفاده عمومی داشته و می‌توان از آنها جهت logging ،caching و یا authorization استفاده کرد.
                  // Called by MVC after the action method executes
                  void OnActionExecuted(ActionExecutedContext filterContext)
                  
                  // Called by MVC before the action method executes
                  void OnActionExecuted(ActionExecutedContext filterContext)
                  
                  // Called by MVC after the action result executes
                  void OnResultExecuted(ResultExecutedContext filterContext)
                  
                  // Called by MVC before the action result executes
                  void OnResultExecuting(ResultExecutingContext filterContext)

                  مثال: کلاس LogAttribute که از کلاس ActionFilterAttribute ارث بری کرده است، عملیات قبل و بعد از اجرای اکشن متد را لاگ می‌کند.
                  public class LogAttribute : ActionFilterAttribute
                  {
                      public override void OnActionExecuted(ActionExecutedContext filterContext)
                      {
                          Log("OnActionExecuted", filterContext.RouteData); 
                      }
                  
                      public override void OnActionExecuting(ActionExecutingContext filterContext)
                      {
                          Log("OnActionExecuting", filterContext.RouteData);      
                      }
                  
                      public override void OnResultExecuted(ResultExecutedContext filterContext)
                      {
                          Log("OnResultExecuted", filterContext.RouteData);      
                      }
                  
                      public override void OnResultExecuting(ResultExecutingContext filterContext)
                      {
                          Log("OnResultExecuting ", filterContext.RouteData);      
                      }
                  
                      private void Log(string methodName, RouteData routeData)
                      {
                          var controllerName = routeData.Values["controller"];
                          var actionName = routeData.Values["action"];
                          var message = String.Format("{0}- controller:{1} action:{2}", methodName, 
                                                                                      controllerName, 
                                                                                      actionName);
                          Debug.WriteLine(message);
                      }
                  }

                  روش سوم:
                  پیاده سازی داخل کنترلر
                  کلاس Controller  می‌تواند هر یک از اینترفیس‌های فیلترها را پیاده سازی نماید. به عبارت دیگر در هر کلاس کنترلر می‌توانید متدهای زیر را تحریف نمایید.
                  • OnAuthorization ^
                  • OnException ^
                  • OnActionExecuting ^
                  • OnActionExecuted ^
                  • OnResultExecuting ^
                  • OnResultExecuted ^


                  روش چهارم:ارث بری از کلاس فیلترهای توکار و مهیای در MVC و تحریف متدهای آن 
                  در کد زیر با تحریف و سفارشی سازی متد OnException مخصوص فیلتر توکار HandleError، قابلیت‌های آن افزایش یافته است:

                  class CustomErrorHandler : HandleErrorAttribute
                  {
                      public override void OnException(ExceptionContext filterContext)
                      {
                          Log(filterContext.Exception);
                  
                          base.OnException(filterContext);
                      }
                  
                      private void Log(Exception exception)
                      {
                          //log exception here..
                      }
                  }


                  رجیستر فیلترها

                  • سراسری:

                  درصورتی که قصد داشته باشید فیلتری بصورت سراسری و در کل برنامه فعال گردد باید آن را در رویداد Application_Start فایل Global.asax.cs بوسیله‌ی متد RegisterGlobalFilters کلاس FiterConfig رجیستر نمایید. بعد از آن فیلتر به کلیه‌ی کنترلرها و اکشن متدها اعمال می‌گردد.

                  public class MvcApplication : System.Web.HttpApplication
                  {
                      protected void Application_Start()
                      {
                            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
                      }
                  }
                  
                  // FilterConfig.cs located in App_Start folder 
                  public class FilterConfig
                  {
                      public static void RegisterGlobalFilters(GlobalFilterCollection filters)
                      {
                          filters.Add(new HandleErrorAttribute());
                  
                          // add your new custom filters
                          filters.Add(new LogAttribute()); 
                          filters.Add(new CustomErrorHandler());
                       }
                  }

                  در کد بالا فیلتر توکار HandleError و البته فیلترهای سفارشی دیگری نیز به صورت سراسری به تمام اکشن متدهای کنترلرها اعمال گردیده است.

                  • کنترلر: در صورتی که فقط بخواهید یک فیلتر به کل اکشن‌های یک کنترلر اعمال گردد. همانند آنچه که در مثال ابتدایی بدان اشاره شد.
                  [HandleError]
                  public class HomeController : Controller
                    • اکشن متد: اعمال یک فیلتر به یک اکشن متد خاص کنترلر. در کد زیر فیلتر HandleError فقط به اکشن متد Index کنترلر Home اعمال خواهد شد.
                    public class HomeController : Controller
                    {
                        [HandleError]
                        public ActionResult Index()
                        {
                            return View();
                        }
                    }

                      ‫نحوه‌ی شناسایی مرورگر Edge در برنامه‌های ASP.NET

                      $
                      0
                      0
                       قطعه کد زیر در برنامه‌های ASP.NET، نام مرورگر کاربر و همچنین شماره نگارش آن‌را باز می‌گرداند:
                       var browser = Request.Browser.Browser + " " + Request.Browser.Version;

                      برای مثال با فایرفاکس، چنین خروجی را دارد:


                      اما ... با مرورگر جدید Edge مایکروسافت، خروجی کروم را مشاهده خواهیم کرد:


                      از این جهت که user agent این مرورگر، چنین شکلی را دارد و ختم به edge است:
                      Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.10240

                      برای رفع این مشکل، نیاز است فایل جدیدی را به مجموعه‌ی «browser definition files» دات نت اضافه کنیم. این فایل‌ها عموما در مسیر زیر یافت می‌شوند:
                      <windir>\Microsoft.NET\Framework\<ver>\CONFIG\Browsers
                      برای نمونه مسیر ذیل را برای مشاهده‌ی فایل‌های مرورگرهای موجود، بررسی کنید:
                       C:\Windows\Microsoft.NET\Framework\v4.0.30319\Config\Browsers
                      در این بین اثری از تعریف مرورگر edge نیست. برای حل این مشکل، الزاما نیازی نیست تا فایل مرورگر جدیدی را به پوشه‌ی فوق اضافه کنیم. می‌توان تعریف این فایل را در پوشه‌ی استانداردی به نام App_Browsers نیز در ریشه‌ی پروژه، قرار داد:


                      با این محتویات:
                      <browsers><browser id="Edge" parentID="Chrome"><identification><userAgent match="Edge/(?'version'(?'major'\d+)(?'minor'\.\d+))" /></identification><capabilities><capability name="browser" value="Edge" /><capability name="version" value="${version}" /><capability name="majorversion" value="${major}" /><capability name="minorversion" value="${minor}" /></capabilities></browser></browsers>
                      در اینجا user agent مرورگر کاربر دریافت شده و اگر ختم به Edge بود، نام و شماره نگارش صحیح آن، دریافت خواهد شد.
                      اکنون پس از این تنظیمات، برنامه (تفاوتی نمی‌کند که وب فرم باشد یا MVC) اطلاعات صحیحی را نمایش می‌دهد:

                      ‫نمایش ای‌جکسی یک partial view در popover بوت استرپ 3

                      $
                      0
                      0
                      فرض کنید بخواهیم نمایش رای دهنده‌های یک مطلب را با popoverبوت استرپ 3 نمایش دهیم:

                      Popover بوت استرپ برای کار با منابع remote طراحی نشده‌است و نیاز است توابع API آن‌را به همراه jQuery Ajax ترکیب کرد تا به تصویر فوق رسید.


                      مرحله‌ی اول: اکشن متدی که یک partial view را باز می‌گرداند

                      فرض کنید اکشن متدی که لیست کاربران رای داده‌ی به یک مطلب را باز می‌گرداند، چنین شکلی را دارد:
                              public ActionResult RenderResults(string param1)
                              {
                                  var users = new[]
                                  {
                                      new User{ Id = 1, Name = "Test 1", Rating = 3},
                                      new User{ Id = 2, Name = "Test 2", Rating = 4},
                                      new User{ Id = 3, Name = "Test 3", Rating = 5}
                                  };
                                  return PartialView("_RenderResults", model: users);
                              }
                      در اینجا لیستی دریافت و سپس به partial view ارسال می‌شود. در ادامه این Partial view نیز به صورت ذیل اطلاعات را رندر می‌کند:
                      @using RemotePopOver.Models
                      @model IList<User><ul id="ratings1" data-title="Ratings" class="list-unstyled">
                          @foreach (var user in Model)
                          {<li>
                                  @user.Name<span class="badge pull-right">@user.Rating</span></li>
                          }</ul>
                      در اینجا از یک ویژگی سفارشی data-* به نام data-title نیز استفاده کرده‌ایم. متنی که در آن قرار می‌گیرد، به صورت عنوان popover ظاهر خواهد شد.


                      مرحله‌ی دوم: دریافت اطلاعات partial view با استفاده از jQuery Ajax و سپس درج آن در یک popover

                      می‌خواهیم با حرکت ماوس بر روی دکمه‌ی سفارشی ذیل، یک popover ظاهر شده و محتوای خودش را از اکشن متد فوق تامین کند.
                      <span id="remotePopover1"
                            aria-hidden="true"
                            data-param1="test"
                            data-popover-content-url="@Url.Action("RenderResults", "Home")"
                            class="glyphicon glyphicon-info-sign btn btn-info"></span>
                      در این مثال، چند ویژگی سفارشی data-* دیگر را نیز تعریف کرده‌ایم تا نیازی به تعریف سراسری متغیرهای جاوا اسکریپتی نباشد. برای مثال data-param1 یک پارامتر دلخواه است و data-popover-content-url به آدرس اکشن متدی اشاره می‌کند که قرار است partial view مدنظر را رندر کند.
                      در ادامه نحوه‌ی استفاده‌ی از این ویژگی‌ها را در jQuery Ajax مشاهده می‌کنید:
                      @section Scripts
                      {<script type="text/javascript">
                              $(document).ready(function () {
                                  $('body').on('mouseenter', 'span[data-popover-content-url]', function () {
                                      var el = $(this);
                                      $.ajax({
                                          type: "POST",
                                          url: $(this).data("popover-content-url"),
                                          data: JSON.stringify({ param1: $(this).data("param1") }),
                                          contentType: "application/json; charset=utf-8",
                                          dataType: "json",
                                          // controller is returning a simple text, not json
                                          complete: function (xhr, status) {
                                              var data = xhr.responseText;
                                              if (status === 'error' || !data) {
                                                  el.popover({
                                                      content: 'Error connecting server!',
                                                      trigger: 'focus',
                                                      html: true,
                                                      container: 'body',
                                                      placement: 'auto',
                                                      title: 'Error!'
                                                  }).popover('show');
                                              }
                                              else {
                                                  el.popover({
                                                      content: data,
                                                      trigger: 'focus',
                                                      html: true,
                                                      container: 'body',
                                                      placement: 'auto',
                                                      title: $('<html />').html(data).find('#ratings1:first').data('title')
                                                  }).popover('show');
                                              }
                                          }
                                      });
                                  }).on('mouseleave', 'span[data-popover-content-url]', function () {
                                      $(this).popover('hide');
                                  });
                              });</script>
                      }
                      در این مثال تمام المان‌هایی که دارای data-popover-content-url هستند، تحت نظر قرار می‌گیرند. سپس اگر ماوس به محدوده‌ی آن‌ها وارد شد، مقدار ("this).data("popover-content-url)$ مساوی آدرسی است که قرار است اطلاعات را از سرور دریافت کند. به همین جهت از آن برای استفاده‌ی در متد ajax کمک گرفته شده‌است.
                      خروجی partial view به صورت json نیست. بنابراین باید اطلاعات نهایی آن‌را در callback ویژه‌ی complete دریافت کرد. مقدار data دریافتی، معادل اطلاعات رندر شده‌ی partial view است. به همین جهت آن‌را به خاصیت content متد popver ارسال می‌کنیم. همچنین چون خروجی patrtial view به همراه html است، نیاز است خاصیت html متد popover نیز به true تنظیم شود. در خاصیت title، نحوه‌ی دسترسی به مقدار data-title تنظیم شده‌ی در partial view را مشاهده می‌کنید.


                      کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید:
                      RemotePopOver.zip

                      ‫معرفی برنامه انتخاب واحد

                      $
                      0
                      0
                      حتما همه‌ی شما با فرآیند انتخاب واحد دانشگاه‌ها آشنا هستید. معمولا دانشجویان سعی می‌کنند، دروسی را انتخاب کنند تا در حداقل تعداد روز‌های هفته، بیشترین تعداد واحد ممکن را بگیرند. اما این کار وقتی که تعداد دروس و اساتید زیاد باشد، مشکل است و باید وقت زیادی  را صرف آن کرد. در نتیجه تصمیم گرفتم تا برنامه‌ای را بنویسم که با گرفتن لیست دروس مورد نظر، تمامی برنامه‌های هفتگی ممکن را نمایش دهد.
                         
                      فناوری‌های استفاده شده:
                        
                      - ASP.NET MVC برای سرور
                      - AngularJS برای کلاینت
                         
                      الگوریتم پیاده سازی شده:
                        
                      در مورد الگوریتم نیز باید بگویم که من از همان ابتدا ساده‌ترین راه، یعنی تست کردن تمامی حالات ممکن را پیاده سازی کردم. متاسفانه این روش منابع زیادی را مصرف  می‌کند و زمان بر است. البته قطعا می‌توان همین روش را با بهینه سازی‌هایی از جمله انتخاب ساختمان داده‌های مناسب‌تر و تغییراتی در الگوریتم، کاراتر کرد.
                      همچنین پس از پایان کار، با کمی تحقیق فهمیدم که برای حل این مساله، راه سر راست و ساده ای وجود ندارد. ولی قطعا می‌توان با استفاده از الگوریتم‌های خاصی، راه حل بهتر و بهینه‌تری را پیاده سازی کرد که هدف از به اشتراک گذاری این برنامه همین مساله هست.

                      برای مثال لینک‌های زیر به توضیح راه‌حل‌هایی برای مسائل مشابه پرداخته‌اند:  
                       
                      دریافت سورس کد
                      برای دریافت سورس کد برنامه به لینک زیر مراجعه کنید:
                         
                      تصاویری از برنامه:
                         

                       
                       

                        

                      ‫کتابخانه GMap.Net

                      $
                      0
                      0
                      نقشه گوگل در حال حاضر یکی از محبوب‌ترین و کاملترین نقشه‌های جهان است و امکانات خوبی هم دارد. در این راستا بسیاری از مردم سعی در استفاده از این نقشه‌ها و امکانات آن‌ها دارند. به همین دلیل گوگل در بسته‌های api خود نیز این مورد را گنجانده است. ولی استفاده از این api مستلزم نوشتن کدهای جاوا اسکرپیتی و شناخت توابع و ثابت‌های api گوگل است. اما در هر صورت این مستندات مورد مطالعه قرار می‌گیرند.

                      سال گذشته بود که به بررسی کتابخانه‌های موجود برای دات نت که به ساخت نقشه‌های گوگل (+ ) می‌پردازند پرداختم. ولی مشکلی که وجود داشت، همه آن‌ها در نهایت یک تصویر jpeg تحویل می‌دادند. ولی من می‌خواستم نقشه‌ی من زنده و واکنشگرا باشد تا کاربر بتواند روی آن حرکت کند، زوم کند، مارکر‌ها را جابجا کند و امکانات دیگری که در این نقشه در دسترس است را داشته باشد. برای همین شروع به ساخت یک class library کردم تا کاربر بتواند در محیط سی شارپ، تنظیمات را با اسامی قابل شناخت و یک intellisense قدرتمند بنویسد و در نهایت بر اساس اطلاعات کاربر، این کدها به صورت جاوا اسکریپت تولید شود. می‌توانید سورس نهایی کتابخانه‌ی GMap.Netرا در گیت هاب، به همراه یک پروژه‌ی نمونه ببینید.

                      پروژه‌ی وابسته این کتابخانه،  MS Ajax Minifierجهت کم حجم کردن کدهای جاوا اسکریپت است. در مورد این کتابخانهدر سایت جاری بحث شده است.
                      برای نصب این کتابخانه می‌توانید از طریق دستور زیر در Nuget عمل کنید:
                      Install-Package GMap.Net

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

                      برای شروع نیاز است که یک نمونه از کلاس GoogleMapApi را ایجاد کنید. بعد از آن با استفاده از خصوصیت SetLocation، مختصات مرکز نقشه را تنظیم نمایید. سپس با استفاده از خصوصیات دیگر نیز می‌توانید نقشه را تنظیم نمایید. تعدادی از این خصوصیات مثل SetZoomVisibility هستند که با استفاده از آن می‌توانید تنظیمات زوم را روی نقشه پیاده سازی کنید. البته فعال کردن این گزینه به تنهایی کافی نیست و باید از طریق خصوصیت ZoomControlOption پیکربندی کنترل زوم را نیز اینجام دهید که این پیکربندی شامل موقعیت قرارگیری کنترل‌های زوم و اندازه‌ی دکمه‌ها می‌باشد و مابقی تنظیمات هم بدین شکل هستند:

                      به غیر از تنظیمات نقشه، Overlayهای زیر در این کلاس پشتیبانی می‌شوند:
                      عنوان
                      توضیحات
                       Marker یک نشانه گذار که برای مشخص کردن یک محل بر روی نقشه به کار می‌رود. این علامت گذار شامل خصوصیت‌هایی چون نقطه‌ی قرارگیری، آیکن، عنوان و انیمیشنی برای نحوه‌ی نمایش آن می‌باشد. همچنین شامل یک خصوصیت دیگر از نوع InfoWindow است که به شما امکان نمایش یک پنجره‌ی توضیحات را نیز بر روی مارکر می‌دهد. این محتوا می‌تواند به صورت HTML نمایش یابد.
                       Circle در صورتیکه بخواهید ناحیه‌ای دایره‌ای شکل را بر روی نقشه مشخص کنید، کاربرد دارد. با دادن نقطه‌ی مرکزی و شعاع می‌توانید دایره را ترسیم کنید. همچنین شامل خصوصیات ظاهری چون رنگ داخل و حاشیه‌ها و میزان شفافیت نیز می‌باشد.
                       Rectangle به رسم یک مستطیل می‌پردازد و تنها لازم است دو مختصات را به آن بدهید و بر اساس این دو نقطه، ناحیه‌ی مستطیلی شکل ترسیم می‌گردد. در صورتیکه نقاط بیشتری را به آن اضافه کنید، فقط دوتای اولی در نظر گرفته می‌شوند. این گزینه شامل خصوصیات ظاهری نیز می‌گردد.
                       Polyline برای ترسیم مسیرها به صورت چند ضلعی به کار می‌رود و الزامی به بستن مسیرها نیست. دارای خصوصیات ظاهری نیز می‌باشد.
                       
                      polygon
                      کاملا شبیه Ployline است؛ با این تفاوت که یک چند ضلعی بسته است و می‌تواند داخل آن با رنگ پر باشد. برای بستن این چند ضلعی لازم نیست تا کاری انجام دهید. خود کلاس، نقطه‌ی اول و آخر را به هم وصل می‌کند.

                      خصوصیات آیتم‌های بالا، شامل موارد زیر می‌شود:

                       نام خصوصیت
                      توضیحات
                       Id در سازنده‌ی هر کدام به طور اجباری قرار گرفته است. این id برای زمانی است که بخواهید با استفاده از جاوااسکرپیت با آن ارتباط برقرار کنید.
                       Editable با فعال کردن این خاصیت، به کاربر این اجازه را می‌دهید که بتواند روی Overlay ویرایش انجام دهد.
                       StrokeWeight ضخامت لبه‌ها را مشخص می‌کند.
                       StrokeColor رنگ لبه را مشخص می‌کند.
                       StrokeOpacity میزان شفافیت لبه را بین 0 تا 1 مشخص می‌کند.
                       FillColor بعضی از المان‌ها مانند چند ضلعی‌های بسته و مستطیل که ناحیه‌ی داخلی دارند، شامل این گزینه هستند و رنگ داخل این ناحیه را مشخص می‌کنند.
                       FillOpacity میزان شفافیت خصوصیت بالا را از 0 تا 1 مشخص می‌کند.
                       Points با استفاده از این خاصیت می‌توانید مختصات را با استفاده از کلاس Location به آن اضافه کنید. برای دایره خصوصیت Point وجود دارد.
                       Radius برای دایره کاربرد دارد. با مقدار نوع Int می‌توانید شعاع آن را مقدار دهی کنید.
                      کد زیر و تصویر زیر نمونه‌ای از کاربرد این کلاس است:
                      public class MiladTower
                          {
                              public GoogleMapApi TestMarker()
                              {
                                  var map=new GoogleMapApi(true);
                                  var location = new Location(35.7448416, 51.3753212);
                                  map.SetLocation(location);
                                  map.SetZoom(17);
                                  map.SetMapType(MapTypes.ROADMAP);
                                  map.SetBackgroundColor(Color.Aqua);
                                  map.ZoomControlVisibilty(true);
                                  map.ZoomOptions = new zoomControlOptions()
                                  {
                                      Position = Position.TOP_LEFT,
                                      ZoomStyle = ZoomStyle.SMALL
                                  };
                      
                                  Marker marker=new Marker("mymarker1");
                                  marker.InfoWindow=new InfoWindow("iw1")
                                  {
                                      Content = "<b>Milad Tower</b><i>in Tehran</i><br/>Milad Tower is the highest tower in iran,many people and tourists visit it each year, but it's so expensive that i cant afford it as iranian citizen<br/>please see more info at  <a href=\"https://en.wikipedia.org/wiki/Milad_Tower\"><img width='16px' height='16px' src='https://en.wikipedia.org/favicon.ico'/>wikipedia</a>"
                                  };
                                  marker.MarkerPoint = location;
                                  map.Markers.Add(marker);
                      
                                  var circle=new CircleMarker("mymarker2");
                                  circle.FillColor = Color.Green;
                                  circle.FillOpacity = 0.6f;
                                  circle.StrokeColor = Color.Red;
                                  circle.StrokeOpacity = 0.8f;
                                  circle.Point = location;
                                  circle.Radius = 30;
                                  circle.Editable = true;
                                  circle.StrokeWeight = 3;
                                  map.Circles.Add(circle);
                      
                                  Rectangle rect=new Rectangle("rect1");
                                  rect.FillColor = Color.Black;
                                  rect.FillOpacity = 0.4f;
                                  rect.Points.Add(new Location(35.74728723483808, 51.37550354003906));
                                  rect.Points.Add(new Location(35.74668641224311, 51.376715898513794));
                                  map.Rectangles.Add(rect);
                      
                                  Polyline polyline=new Polyline("poly1");
                                  polyline.Points.Add(new Location(35.74457043569041, 51.373915672302246));
                                  polyline.Points.Add(new Location(35.74470976097927, 51.37359380722046));
                                  polyline.Points.Add(new Location(35.744378863020074, 51.37337923049927));
                                  polyline.StrokeColor = Color.Blue;
                                  polyline.StrokeWeight = 2;
                                  map.Polylines.Add(polyline);
                      
                                  Polygon polygon=new Polygon("poly2");
                                  polygon.Points.Add(new Location(35.746494844665094, 51.374655961990356));
                                  polygon.Points.Add(new Location(35.74635552250061, 51.37283205986023));
                                  polygon.Points.Add(new Location(35.74598109297522, 51.372681856155396));
                                  polygon.Points.Add(new Location(35.7454934611854, 51.37361526489258));
                                  polygon.FillColor = Color.Black;
                                  polygon.FillOpacity = 0.5f;
                                  polygon.StrokeColor = Color.Gray;
                                  polygon.StrokeWeight = 1;
                                  map.Polygons.Add(polygon);
                      
                                  return map;
                              }
                          }
                      بعد از ایجاد چنین کلاسی نیاز است تا آن را در ویوو ترسیم کنیم. ابتدا یک div برای ناحیه‌ی نقشه ایجاد می‌کنیم و سپس خروجی متد ShowMapForMVC را در ناحیه‌ی Head صفحه چاپ می‌کنیم. این خروجی به طور خودکار یک MVCHTMLString را بر می‌گرداند. در صورتیکه از وب فرم استفاده می‌کنید، می‌توانید از گزینه‌ی ShowMap استفاده کنید.
                      @section javascript
                      {
                          @{
                              var map = new MiladTower().TestMarker();
                              @map.ShowMapForMvc("mapdiv")
                          }
                      }<br/><br/><div id="mapdiv" style="width:600px;height:600px;"></div>

                      در نهایت نقشه‌ی زیر نمایش داده می‌شود:


                      کم حجم کردن کدها
                      در صورتیکه به سورس صفحه نگاهی بیندازید، می‌بینید که کدهای جاوا اسکریپت، داخل صفحه نوشته شده‌اند. اگر بخواهید برای کم حجم‌تر شدن کد، عملیات minify را انجام دهید، با true شدن خصوصیت minified با استفاده از کتابخانه‌ی وابسته‌اش (MS Ajax Minifier)  اینکار را انجام می‌دهد.

                      انتقال کدها به یک فایل خارجی
                      بسیاری از ما برای نوشتن کدهای جاوا اسکریپت، از یک فایل خارجی استفاده می‌کنیم. برای داشتن این قابلیت می‌توانید به جای ShowMapForMVC متد CallJs را صدا بزنید تا کتابخانه api گوگل را صدا بزند و سپس در یک اکشن متد، متد GiveJustJS را صدا بزنید وطبق مقاله‌ی موجوددر سایت جاری محتوای آن را برگردانید و به عنوان یک فایل JS به این اکشن متد لینک بدهید. کدهای زیر به شما نحوه‌ی این کار را نشان می‌دهند:
                      ابتدا در یک اکشن متد، کد زیر را وارد می‌کنیم:
                          public ActionResult MiladJs()
                              {
                                  var output = new MiladTower().TestMarker().GiveJustJs("mapdiv");
                                  Response.ContentType = "text/javascript";
                                  return Content(output);
                              }

                      بعد از آن در ویووی مربوطه کد زیر را داریم:
                      @section javascript
                      {
                          @{
                              var map = new MiladTower().TestMarker();
                              @map.CallJs()<script type="text/javascript" src="@Url.Action("MiladJs","Home")"></script>
                          }   
                      }<br/><br/><div id="mapdiv" style="width:600px;height:600px;"></div>
                      بدین ترتیب کدهای شما داخل یک فایل خارجی قرار می‌گیرند.


                      نحوه‌ی کارکرد این کتابخانه
                      برای آشنایی با نحوه‌ی کارکرد آن باید بدانید که اساس کار این کتابخانه string interpolationاست. این کتابخانه کلاسی را به صورت Partial دارد که بین چندین فایل تقسیم شده است و هر یک از فایل‌ها، با نام محتوای آن نامگذاری شده‌اند. Public methodsمتدهای عمومی، private methodsمتدهای خصوصی، Constants یا ثابت‌ها که حاوی تمام دستورات جاوا اسکریپتی هستند و در نهایت خود کلاس اصلی GoogleMapApi که حاوی کدهای اجرایی و تشکیل کد جاوا اسکریپت می‌باشد. در کنار کلاس اصلی، کلاس‌های Overlay هم قرار دارند که شامل اطلاعات اشیاء روی نقشه‌ها هستند؛ مثل مارکرها و چندضلعی‌ها و ... و در نهایت یک سری کلاس و نوع شمارشی Enumشامل خصوصیت‌هایی که برای تنظیمات و پیکربندی نقشه به کار می‌روند.
                      کلاس GoogleMapApi برای ایجاد کدها، داده‌هایی را که برای هر کلاس در نظر گرفته‌اید، با استفاده از interpolation و ثابت‌های حاوی کد جاوا اسکریپت، در یک رشته‌ی جدید قرار می‌دهند و این رشته‌ها با اتصال درست در موقعیت خود، کد نهایی جاوا اسکریپت را تولید می‌کنند.

                      ‫معرفی DNTBreadCrumb

                      $
                      0
                      0
                      سال نو مبارک! با آرزوی بهترین‌ها برای تمام همراهان سایت.

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


                      پس از بررسی نمونه‌های bread crumbs موجود، مشکلی که اکثر آن‌ها داشتند یا استفاده از سشن جهت تشکیل لیست آیتم‌ها (سشن در سایت جاری غیرفعال است) و یا بیش از اندازه پیچیده بودن آن‌ها بود. به همین جهت یک نمونه‌ی ساده‌تر و سبک‌تر تهیه شد که در ذیل نحوه‌ی نصب و استفاده‌ی آن‌را بررسی خواهیم کرد.


                      نصب DNTBreadCrumb

                      برای نصب این bread crumb مبتنی بر بوت استرپ 3، تنها کافی است دستور ذیل را در کنسول پاورشل نیوگت صادر کنید:
                       PM> install-package DNTBreadCrumb


                      تنظیمات اولیه‌ی DNTBreadCrumb

                      پس از نصب، علاوه بر فایل اسمبلی DNTBreadCrumb، فایل جدید Views\Shared\_BreadCrumb.cshtml نیز به پروژه‌ی شما اضافه می‌شود. این فایل، لیست نهایی آیتم‌های تنظیم شده‌ی توسط اکشن متدها را به صورت یک bread crumb رندر می‌کند. مزیت کار کردن با فایل‌های cshtml (بجای HTML Helperها)، امکان سفارشی سازی نهایی آن‌ها توسط استفاده کننده‌است.
                      بنابراین برای نمایش لیست bread crumb تنها کافی است یک سطر ذیل را به فایل layout برنامه اضافه کنید:
                       @{Html.RenderPartial("_BreadCrumb");}


                      طراحی یک bread crumb سه سطحی

                      اگر به فایل Views\Shared\_BreadCrumb.cshtmlمراجعه کنید، مشاهده خواهید کرد که سطح اول bread crumb یا همان نمایش Home، به صورت پیش فرض قرار داده شده‌است و در اینجا اگر می‌خواهید نام دیگری را بجای Home (مثلا خانه) تنظیم کنید، به سادگی قابل انجام است.
                      دو سطح بعدی یک bread crumb، نام کنترلر و سپس نام اکشن متد خواهند بود:
                          [BreadCrumb(Title = "News Root", UseDefaultRouteUrl = true, RemoveAllDefaultRouteValues = true,
                              Order = 0, GlyphIcon = "glyphicon glyphicon-link")]
                          public class NewsController : Controller
                          {
                              [BreadCrumb(Title = "Main index", Order = 1)]
                              public ActionResult Index(string id)
                              {
                                  if (!string.IsNullOrWhiteSpace(id))
                                  {
                                      this.SetCurrentBreadCrumbTitle(id);
                                  }
                      
                                  return View();
                              }
                      در این مثال، از ویژگی جدید BreadCrumb بر روی کنترلر و سپس یک اکشن متد مدنظر، استفاده شده‌است.
                      کار با تنظیم Title یا همان عناوینی که در لینک‌های bread crumb ظاهر می‌شوند، شروع خواهد شد. سپس اگر علاقمند بودید، می‌توانید یک گلیف آیکن را نیز در اینجا مشخص کنید تا در bread crumb نهایی، کنار عنوان مشخص شده، رندر شود.
                      هر ویژگی BreadCrumb دارای خاصیت Url نیز هست. اما با توجه به اینکه می‌توان از طریق مسیریابی‌های پیش فرض، این آدرس‌ها را پیدا کرد، نیازی به ذکر آن‌ها نیست. برای مثال تنظیم UseDefaultRouteUrl در BreadCrumb یک کنترلر، مقدار Url مرتبط با آن‌را به صورت خودکار از مسیریابی پیش فرض آن دریافت و محاسبه می‌کند. خاصیت RemoveAllDefaultRouteValues به این معنا است که اگر در اکشن متد index، مقدار id تنظیم شده بود، نیازی نیست تا حین تشکیل آدرس ریشه‌ی کنترلر، این مقدار نیز لحاظ شود.

                      و ... همین مقدار تنظیم، برای کار با این سیستم کافی است.


                      موارد تکمیلی

                      - نیاز است عنوان bread crumb به صورت پویا تنظیم شود. چگونه این‌کار را انجام دهیم؟
                      برای اینکار می‌توانید از متد الحاقی SetCurrentBreadCrumbTitle استفاده کنید. برای نمونه تصویر ابتدای مطلب نیز به همین ترتیب تولید شده‌است. در اینجا عنوان پویای مقاله، توسط متد SetCurrentBreadCrumbTitle بجای Title پیش فرض bread crumb تنظیم شده‌است.

                      - چگونه می‌توان بیش از سه سطح را تعریف کرد؟
                      برای تعریف بیش از سه سطح پیش فرض خانه/کنترلر/اکشن متد، می‌توانید از متد الحاقی AddBreadCrumb استفاده کنید:
                              [BreadCrumb(Title = "News Archive", Order = 2)]
                              public ActionResult Archive(int? id)
                              {
                                  if (id != null)
                                  {
                                      this.SetCurrentBreadCrumbTitle(string.Format("News item {0}", id.Value));
                                      this.AddBreadCrumb(new BreadCrumb
                                      {
                                          Title = "News Archive",
                                          Order = 1,
                                          Url = Url.Action("Archive", "News", routeValues: new { id = "" })
                                      });
                                  }
                      
                                  return View();
                              }
                      در اینجا به هر تعدادی که نیاز است می‌توانید AddBreadCrumb را انجام دهید. فقط باید دقت داشت که تقدم و تاخر این‌ها بر اساس خاصیت Order انجام می‌شود. بنابراین اگر پس از رندر شدن مشاهده کردید که لینک تولیدی، پس یا پیش از آیتم مدنظر شما است، فقط کافی است Orderها را صحیح مقدار دهی کنید.


                      سورس کامل مثال‌های مطرح شده‌ی در این مطلب را در پروژه‌ی MVCBreadCrumbTestمی‌توانید مشاهده کنید.


                      ‫طراحی یک ماژول IpBlocker در ASP.NET MVC

                      $
                      0
                      0
                      همانطور که میدانید وب سایت‌های اینترنتی در معرض انواع و اقسام حملات قرار دارند و یکی از این حملات Dosاست. در این نوشتار میخواهیم تکه کدی را ارائه دهیم، تا این نوع حملات را دفع نماید. همانطور که میدانید یک درخواست Http باید از ماژول‌های مختلفی عبور نماید تا به یک Http Handler برسد.
                      ابتدا باید یک Enum تعریف کنیم تا نوع درخواست کاربر را مشخص کند. مثلا 100 درخواست ابتدایی را به عنوان FirstVisite در نظر گرفته و اگر تعداد درخواستها از 100 گذشت، در دسته Revisit قرار میگیرند و ... . البته این بستگی به شما دارد که مقادیر را چقدر در نظر بگیرید؛ اما دقت کنید.
                      private enum VisiteType
                              {
                                  FirstVisite = 2,
                                  Reviste = 4,
                              }
                      در مرحله بعدی باید آدرس Ip بیننده سایت را بدست آورده وچک کنیم که این آدرس Ip در کش موجود هست یا خیر. اگر موجود بود مقدار متغیر hits را افزایش میدهیم و چک میکنیم که متغیر با کدام یک از مقادیر Enum برابری میکند و آن را در کش سیستم ذخیره میکنیم.
                      if (httpContext.Cache[ipAddress] != null)
                                      {
                                          hits++;
                      
                                          if (hits == (int)VisiteType.FirstVisite)
                                          {
                                              httpContext.Cache.Insert(key: ipAddress,
                                                 value: hits,
                                                 dependencies: null,
                                                 absoluteExpiration: DateTime.UtcNow.AddSeconds(1),
                                                 slidingExpiration: Cache.NoSlidingExpiration);
                                              return;
                                          }
                      در تکه کد بالا چون درخواست به حد معین و معقولی رسیده است، آدرس بعد از یک ثانیه از کش حذف میشود. اما اگر درخواستهای غیرمعقولی به سرور ارسال شود، باید پاسخ را قطع و آدرس Ip را در کش ذخیره و این آدرس Ip باید به مدت معینی در کش مانده و بلاک شود.
                      using System;
                      using System.Net;
                      using System.Web;
                      using System.Web.Caching;
                      
                      namespace IpBlocker
                      {
                          public class IpBlocker : IHttpModule
                          {
                              private int hits = 0;
                              /// <summary>
                              /// Define enum to specify visite type
                              /// </summary>
                              private enum VisiteType
                              {
                                  FirstVisite = 2,
                                  Reviste = 4,
                              }
                              public void Init(HttpApplication context)
                              {
                                  context.BeginRequest += OnBeginRequest;
                              }
                              public void Dispose()
                              {
                      
                              }
                      
                              public void OnBeginRequest(object sender, EventArgs e)
                              {
                                  var httpApplication = sender as HttpApplication;
                                  var httpContext = httpApplication?.Context;
                                  ProcessRequest(httpApplication, httpContext);
                              }
                      
                              private void ProcessRequest(HttpApplication application, HttpContext httpContext)
                              {
                                  //Checke if browser is a search engine web crawler
                                  if (httpContext.Request.Browser.Crawler)
                                      return;
                      
                                  var ipAddress = application.Context.Request.UserHostAddress;
                      
                                  if (httpContext.Cache[ipAddress] == null)
                                  {
                                      // Reset hits for new request after blocking ip
                                      if (hits > 0)
                                          hits = 0;
                      
                                      hits++;
                      
                                      httpContext.Cache.Insert(key: ipAddress,
                                         value: hits,
                                         dependencies: null,
                                         absoluteExpiration: DateTime.UtcNow.AddSeconds(1),
                                         slidingExpiration: Cache.NoSlidingExpiration);
                      
                                      return;
                                  }          
                      
                                  else
                                  {
                                      if (httpContext.Cache[ipAddress] != null)
                                      {
                                          hits++;
                      
                                          if (hits == (int)VisiteType.FirstVisite)
                                          {
                                              httpContext.Cache.Insert(key: ipAddress,
                                                 value: hits,
                                                 dependencies: null,
                                                 absoluteExpiration: DateTime.UtcNow.AddSeconds(1),
                                                 slidingExpiration: Cache.NoSlidingExpiration);
                                              return;
                                          }
                      
                                          if (hits == (int)VisiteType.Reviste)
                                          {
                      
                                              httpContext.Cache.Insert(key: ipAddress,
                                                 value: hits,
                                                 dependencies: null,
                                                 absoluteExpiration: DateTime.UtcNow.AddSeconds(1),
                                                 slidingExpiration: Cache.NoSlidingExpiration);
                                              return;
                                          }
                                          if (hits > (int)VisiteType.Reviste)
                                          {
                                              httpContext.Cache.Insert(key: ipAddress,
                                                  value: hits,
                                                  dependencies: null,
                                                  absoluteExpiration: DateTime.UtcNow.AddMinutes(1),
                                                  slidingExpiration: Cache.NoSlidingExpiration);
                      
                                              httpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
                                              httpContext.Response.SuppressContent = true;
                                              httpContext.Response.End();
                                          }
                                      }
                                  }
                              }
                          }
                      }
                      در کد بالا محدودیت زمانی یک دقیقه در نظر گرفته شده است.

                      ‫پیاده سازی Conventional UI در ASP.NET MVC

                      $
                      0
                      0
                      بعد از مدتی کار کردن با فریمورک ASP.NET MVC، شاید ایجاد یک فریمورک شخصی برپایه آن، یکی از باید‌ها برای شما باشد. در این راستا، نظم بخشیدن به ویوها برای جلوگیری از تکرار یکسری کد که اکثرا مورد استفاده قرار میگیرند، نجات بخش خواهد بود.
                      به تصویر زیر که حاصل از ویو مربوط به ویرایش یک Issue است، توجه فرمایید:

                      آیا به این نتیجه رسیدید که اصل DRY  را نقض کرده‌ایم؟ بله همین طور است. تکرار کلاس‌های css مربوط به بوت استرپ، تکرار هلپرهای توکار ASP.NET MVC بارها و بارها، خوانایی کد را پایین میارود و در برخی موارد هم خسته کننده خواهد بود. اگر با مباحث مربوط به EditorTemplate‌ها قبلا آشنا شده باشید، خیلی سریع عنوان خواهید کرد که بهتر است از این امکان بهره برد؛ بله درست است. برای این منظور در مسیر Views/Shared/EditorTemplates، فایل cshtml. همنام با نوع داده مد نظر را ایجاد میکنیم.

                      String.cshtml

                      @model string
                          @Html.TextBox("",ViewData.TemplateInfo.FormattedModelValue,
                          new { @class="form-control",placeholder=ViewData.ModelMetadata.Watermark})

                      Enum.cshtml

                      @model Enum
                      @Html.EnumDropDownListFor(m => Model, new { @class = "form-control" })

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

                      این نتیجه امیدوار کننده است ولی  بازهم یکسری از کدها بی دلیل تکرار شده‌اند. هلپرهای زیر نیز میتوانند در کاهش کدها به کمک ما برسند :

                      public static class BootstrapHelpers
                          {
                              public static IHtmlString BootstrapLabelFor<TModel,TProp>(
                                  this HtmlHelper<TModel> helper,
                                  Expression<Func<TModel,TProp>> property)
                              {
                                  return helper.LabelFor(property, new
                                  {
                                      @class = "col-md-2 control-label"
                                  });
                              }
                              public static IHtmlString BootstrapLabel(
                                  this HtmlHelper helper,
                                  string propertyName)
                              {
                                  return helper.Label(propertyName, new
                                  {
                                      @class = "col-md-2 control-label"
                                  });
                              }
                          }

                      از کلاس بالا برای عدم تکرار کلاس‌های بوت استرپ مربوط به Label، استفاده میشود .

                      حال دوباره نتیجه را مشاهده کنید:

                      خیلی عالی؛ توانستیم از تکرار یکسری از کلاس‌های بوت استرپ خلاص شویم. اما در ادامه با استفاده از یک Object Template به عنوان EditorTemplate برای نوع داده‌های Complex، کار را تمام خواهیم کرد.

                       EditorTemplate‌های تعریف شده در بالا، صرفا برای نوع داده‌های خاصی مورد استفاده قرار خواهند گرفت؛ ولی پیاده سازی یک EditorTemplate جنریک که حتی از ویومدل‌های موجود در پروژه نیز پشتیابی کند، به شکل زیر خواهد بود.

                      Object.cshtml

                      @model dynamic
                      
                      @foreach (var prop in ViewData.ModelMetadata.Properties
                                                  .Where(p => p.ShowForEdit))
                      {
                          if (prop.TemplateHint == "HiddenInput")
                          {
                              @Html.Hidden(prop.PropertyName)
                          }
                          else
                          {<div class="form-group">
                                  @Html.BootstrapLabel(prop.PropertyName)<div class="col-md-10">
                                      @Html.Editor(prop.PropertyName)
                                      @Html.ValidationMessage(prop.PropertyName)</div></div>
                          }
                      }

                      با استفاده از ViewData.ModelMetadata میتوان به خصوصیات مدل مربوط به ویو دسترسی پیدا کرد که در بالا با استفاده از همین خصوصیت به تمام پراپرتی‌های مدل دسترسی پیدا کرده و مقداری کد تکراری باقی مانده را هم در اینجا کپسوله کردیم.

                      حال کافی است به شکل زیر عمل کنیم:

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

                      ‫معرفی پروژه فروشگاهی Iris Store

                      $
                      0
                      0
                      پروژه IrisStore، یک سیستم فروشگاهی متن باز برای راه اندازی فروشگاه‌های اینترنتی کوچک است که سورس آن را می‌توانید از آدرس زیر دریافت کنید:
                       
                      https://github.com/MehdiSaeedifar/IrisStore
                       
                      همچنین نمونه‌ی آنلاین آن‌را می‌توانید در فروشگاه آیریسمشاهده کنید.
                       

                      در ادامه برخی از قابلیت‌های این سیستم را مشاهده می‌کنید:
                       

                      جست و جو با قابلیت دسته بندی نتایج

                       
                      به هنگام جست و جو، لیستی از موارد پیشنهادی به صورت دسته بندی شده نمایش داده می‌شود.



                      جست و جوی پیشرفته کالا‌ها
                       
                      جست و جو بر اساس قیمت، گروه، کلمات کلیدی و مرتب سازی نتایج انجام می‌گیرد. همچنین نتایج جست و جو بدون رفرش شدن صفحه و به صورت AJAX ای به همراه تغییر URL صفحه صورت می‌گیرد.



                      نمایش نمودار تغییرات قیمت
                       
                      امکان نمایش نمودار تغییرات قیمت کالا در بازه‌ی زمانی نیز پیش بینی شده است.

                         
                      ویرایش اطلاعات به صورت inline
                       
                      امکان ویرایش قیمت و تاریخ به صورت inline وجود دارد.



                        

                      مدیریت تصاویر کالا

                       
                      در این قسمت امکان آپلود همزمان چندین فایل به همراه پیش نمایش آن‌ها وجود دارد. همچنین امکان کشیدن و رها کردن برای تغییر ترتیب چیدمان عکس‌ها نیز مهیا است.( تصویر اول به عنوان کاور کالا در نظر گرفته می‌شود.)


                        

                      قابلیت‌های دیگر:

                       
                      - مدیریت تصاویر اسلایدشو و تغییر ترتیب آن‌ها از طریق کشیدن و رها کردن (drag & drop)
                      - تعریف برگه و تغییر ترتیب نمایش آن‌ها از طریق کشیدن و رها کردن
                      - امکان ارسال پست
                      - تعریف دسته بندی
                      - مدیریت کاربران
                      - تعریف تنظیمات سایت
                      - نمایش کالا و پست‌های مشابه

                      کارهایی که باید انجام شود:

                       
                      - پیاده سازی سبد خرید و خرید آنلاین
                       

                      تصویر پنل مدیریت

                       

                      تصویر صفحه‌ی اصلی:



                      همچنین به راحتی می‌توان با طراحی قالب جدیدی، از این سیستم برای کاری غیر از فروشگاه اینترنتی استفاده کرد؛ سایت‌های زیر نمونه‌های آنلاین دیگری از این سیستم هستند:

                      - http://www.petrapars.ir
                      - http://www.ava-tarh.ir
                        
                      در نهایت فهرستی از کتاب خانه‌ها و فناوری‌های استفاده شده و همچنین مقالات مرتبط با این پروژه را قرار داده‌ام.

                      کتابخانه‌ها و فریم ورک‌های سمت سرور:

                       فناوری یا کتابخانه   توضیحات  
                      مقالات مرتبط
                       ASP.NET MVC 5.x
                       فریم ورک و موتور اصلی سایت
                      -ASP.NET MVC
                      -How to handle repeating form fields in ASP MVC
                      -How to dynamically (via AJAX) add new items to a bound list model, in ASP MVC.NET  
                       Entity Framework 6.x
                       فریم ورک دسترسی به داده
                      -Entity framework code-first
                      -Update One-to-Many Entity using DBContext 
                      -مدیریت اطلاعات وابسته به زمان در بانک‌های اطلاعاتی رابطه‌ای
                      EFSecondLevelCache
                      کش سطح دوم EF 6
                       -بازنویسی سطح دوم کش برای Entity framework 6 
                       AutoMapper
                       نگاشت اطلاعات یک شی به شی دیگر به صورت خودکار  - دوره AutoMapper
                      - خودکارسازی فرآیند نگاشت اشیاء در AutoMapper 
                       StructureMap
                       تزریق وابستگی‌ها
                      -EF Code First #12 
                       MvcCheckBoxList
                       اضافه کردن CheckBoxList  به HtmlHelper 

                       DNTScheduler
                       برای انجام کارهای زمان بندی شده
                      -انجام کارهای زمانبندی شده در برنامه‌های ASP.NET توسط DNT Scheduler
                       Lucene.Net
                       موتور جستجوی سایت  -جستجوی سریع و پیشرفته با لوسین Lucene.net 
                       AspNet.Identity
                       سیستم مدیریت کاربران
                      -اعمال تزریق وابستگی‌ها به مثال رسمی ASP.NET Identity 
                       ELMAH.MVC
                       کتابخانه ثبت وقایع و خطا‌های سیستم  -معرفی ELMAH
                       PagedList
                       نمایش اطلاعات به صورت صفحه بندی شده

                      PersianDateTime
                      جایگزینی است برای System.DateTime برای تاریخ‌های شمسی
                      -PersianDateTime جایگزینی برای System.DateTime
                      T4MVC
                      تعاریف Strongly typed مسیرها 
                      -T4MVC : یکی از الزامات مدیریت پروژه‌های ASP.NET MVC
                      Dynamic LINQ
                      نوشتن کوئری‌های LINQ به صورت رشته ای
                      -انتخاب پویای فیلد‌ها در LINQ
                      -فعال سازی و پردازش جستجوی پویای jqGrid در ASP.NET MVC 

                      کتابخانه‌های جاوا اسکریپتی سمت کلاینت:
                       
                       فناوری یا کتابخانه  
                        توضیحات     مقالات مرتبط 
                       jQuery کتاب خانه‌ی پایه جاوا اسکرپتی سایت
                       -آموزش (jQuery) جی کوئری
                      -آموزش JQuery Plugin و مباحث پیشرفته جی کوئری 
                       
                       jQuery UI ویجت‌های رابط کاربری
                      - نمایش رکوردها به ترتیب اولویت به کمک jQuery UI sortable در ASP.NET MVC
                      - jQuery UI Sortable
                      -Categorized search result with jQuery UI Autocomplete
                      - jQuery UI Slider
                      -rtl jQuery UI Slider
                      -jquery UI Sortable with table and tr width 
                      jQuery Validationاعتبار سنجی سمت کلاینت
                      -مشکل اعتبار سنجی jQuery validator در Bootstrap tabs
                      -نمایش خطاهای اعتبارسنجی سمت کاربر ASP.NET MVC به شکل Popover به کمک Twitter bootstrap
                      toastrنمایش پیام و اطلاع رسانی

                      PersianDatePickerیک DatePicker شمسی کم حجم 
                      -PersianDatePicker یک DatePicker شمسی به زبان JavaScript که از تاریخ سرور استفاده می‌کند
                      CKEDITORادیتور متن
                      -استفاده از ادیتور CKEditor در صفحات ASP.NET
                      -یکپارچه سازی CKEditor با Lightbox
                      Roxy Filemanمدیریت فایل ها  -افزونه مدیریت فایل‌های رایگان Roxy FileMan برای TinyMce و CkEditor  
                      Magnific Popupنمایش عکس‌ها به صورت پاپ آپ

                      Select2تغییر شکل drop down list‌ها برای انتخاب گزینه‌ها

                      jqGrid v4.6نمایش اطلاعات در قالب جدول
                      - آموزش jqGrid
                      Bootstrap Star Ratingامتیاز دهی ستاره ای
                      -پیاده سازی امتیاز دهی ستاره‌ای به مطالب به کمک jQuery در ASP.NET MVC
                      jQuery File Upload Pluginآپلود فایل به صورت AJAX ای

                      HIGHCHARTSنمایش نمودار

                      jQuery Number Pluginبرای فرمت کردن اعداد

                      X-editableویرایش اطلاعات به صورت inline
                      -قابل ویرایش کننده‌ی فوق العاده x-editable ؛ قسمت اول 
                      bootstrap-confirmationنمایش فرم تایید در قالب popover

                      PathJS برای تغییر URL صفحه برای اعمال Ajax ای 
                      -پیاده سازی دکمه «بیشتر» یا «اسکرول نامحدود» به کمک jQuery در ASP.NET MVC 

                      فریمورک‌های CSS:
                       
                      فناوری یا کتابخانه
                       توضیحات  
                       مقالات مرتبط  
                       Bootstrap 3.x 
                       فریم ورک پایه ای css سایت
                       - Bootstrap 3 RTL Theme
                      - Twitter Bootstrap
                      -سازگارسازی کلاس‌های اعتبارسنجی Twitter Bootstrap 3 با فرم‌های ASP.NET MVC 
                      -ساخت قالب‌های نمایشی و ادیتور دکمه سه وضعیتی سازگار با Twitter bootstrap در ASP.NET MVC
                      -نمایش اخطارها و پیام‌های بوت استرپ به کمک TempData در ASP.NET MVC
                       AdminLTE 
                       قالب مدیریت سایت
                       - نسخه راستچین شده AdminLTE 2.2.1
                      Animate.css  انیمیشن‌های css3 سایت

                      Font Awesome  پک آیکون‌های برداری

                      Awesome Bootstrap Checkbox  زیبا سازی چک باکس ها

                      فونت فارسی وزیر  قلم فارسی


                       

                      ‫ایجاد Self-Signed Certificate در IIS Express

                      $
                      0
                      0
                      در حال نوشتن یک برنامه‌ی ویندوزی بودم که نیاز به یک وب سرویس داشت و اتصال باید از طریق HTTPS انجام می‌گرفت. پروژه‌ی وب سرویس را تنظیم کردم تا SSL را هم پشتیبانی کند (تنظیمات انجام شد). وقتی می‌خواستم روی یک سیستم دیگر، پروژه را در ویژوال استودیو باز و اجرا کنم، با پیام خطای «عدم وجود ارتباط امن» در هنگام برقراری ارتباط با وب سرویس مواجه شدم.
                      که بعد از بررسی به راه حل‌های زیر رسیدم:

                      راه حل اول

                      بعد از اجرای وب سرویس و باز کردن آدرس آن به صورت HTTPS در مرورگر، پیام مبنی بر عدم اعتبار گواهی HTTPS را در آدرس وارد شده، مشاهده می‌کنیم. (Untrusted certificate) (که نسبت به مرورگر استفاده شده، این پیام متفاوت است و من در اینجا از مرورگر IE استفاده می‌کنم)

                      1. بر روی Certificate error در نوار آدرس، کلیک کرده و View certificates را انتخاب می‌کنیم.
                      2. وقتی پنجره Certificateباز شد بر روی دکمه Install Certificateکلیک کرده و پنجره Certificate Import Wizard باز شده و Next را می‌زنیم و Place all certificates in the following store را انتخاب می‌کنیم و بر روی دکمه Browse کلیک می‌کنیم.
                      3. از پنجره باز شده Trusted Root Certification Authorities را انتخاب می‌کنیم و بر روی دکمه OK، کلیک می‌کنیم.
                      4. سپس Nextرا می‌زنیم و در پایان بر روی دکمه Finishکلیک می‌کنیم.
                      5. پس از اتمام Wizard، پنجره Security Warning به شما نمایش داده می‌شود که باید بر روی Yesآن کلیک کنید، بعد از تایید، پیام .The import was successfulبه شما نمایش داده می‌شود.


                      راه حل دوم

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

                      1. بازکردن پنجره Run و وارد کردن دستور mmc و زدن دکمه OK.
                      2. اضافه کردن Snap-in
                      • انتخاب Add/Remove Snap-in  از منوی File
                      • انتخاب Certificates  از لیست سمت چپ و انتخاب دکمه Add
                      • در پنجره Certificates Snap-ins انتخاب گزینه Computer account و انتخاب دکمه Next
                      • انتخاب Local computer  و کلیک بر روی دکمه Finish
                      • انتخاب دکمه OK
                    • استخراج IIS Express certificate از computer’s personal store
                      • در قسمت Console Root ، بخش Certificates (Local Computer)، سپس قسمت Personalو انتخاب Certificates.
                      • انتخاب گواهی با مشخصات زیر:
                        • "Issued to = "localhost
                        • "Issued by = "localhost
                        • "Friendly Name = "IIS Express Development Certificate
                      • انتخاب گزینه Exportاز زیرمنوی All Tasksدر منوی Action
                      • پنجره Certificate Export Wizard باز شده و انتخاب دکمه Next
                      • انتخاب No, do not export the private key و انتخاب دکمه Next
                      • انتخاب DER encoded binary X.509 (.CER) و انتخاب دکمه Next
                      • انتخاب مسیر ذخیره فایل گواهی تصدیق مجوز و انتخاب دکمه Next
                      • انتخاب دکمه Finish برای انجام عملیات Export و مشاهده پیام موفقیت
                    • وارد کردن IIS Express certificate به computer’s Trusted Root Certification Authorities store
                      1. در قسمت Console Root ، بخش Certificates (Local Computer)، سپس قسمت Trusted Root Certification Authorities و انتخاب Certificates.
                      2. انتخاب گزینه Import از زیرمنوی All Tasks در منوی Action
                      3. پنجره Certificate Export Wizard باز شده و انتخاب دکمه Next
                      4. انتخاب مسیر فایل ذخیره شده در مرحله قبل و انتخاب دکمه Next
                      5. انتخاب Place all certificates in the following store و در قسمت Certificate store ، انتخاب بخش Trusted Root Certification Authorities و انتخاب دکمه Next
                      6. انتخاب دکمه Finish برای انجام عملیات Import و مشاهده پیام موفقیت و مشاهده گواهی تصدیق مجوز با نام localhost در لیست Trusted Root Certification Authorities


                      راه حل سوم

                      با استفاده از Developer Command Promptنیز می‌توان این کار را انجام داد.

                      1. با اجرای دستور زیر و دریافت فایل خروجی
                        makecert -r -n "CN=localhost" -b 01/01/2000 -e 01/01/2099 -eku 1.3.6.1.5.5.7.3.1 -sv localhost.pvk localhost.cer
                        
                        cert2spc localhost.cer localhost.spc
                        
                        pvk2pfx -pvk localhost.pvk -spc localhost.spc -pfx localhost.pfx
                      2. اجرای فایل localhost.pfx و وقتی پنجره Certificate Import Wizard باز شد، Next را می‌زنیم.
                      3. نام فایل انتخاب شده را در این قسمت مشاهده می‌کنیم و Next را می‌زنیم.
                      4. در صورت داشتن کلمه عبور، آن را وارد کرده (که در اینجا کلمه عبوری را تعریف نکرده‌ایم) و Next را می‌زنیم.
                      5. صفحه Place all certificates in the following store را انتخاب می‌کنیم و بر روی دکمه Browse کلیک می‌کنیم.
                      6. از پنجره باز شده، Trusted Root Certification Authorities را انتخاب می‌کنیم و بر روی دکمه OK، کلیک می‌کنیم.
                      7. سپس Nextرا می‌زنیم و در پایان بر روی دکمه Finishکلیک می‌کنیم.
                      8. پس از اتمام Wizard، پنجره Security Warning به شما نمایش داده می‌شود که باید بر روی Yesکلیک کنید. بعد از تایید، پیام .The import was successfulبه شما نمایش داده می‌شود.

                      نکته:در صورتی که بخواهید برنامه شما (windows form) بتواند به سرور از طریق HTTPS اتصال پیدا کند، باید این فایل pfx بر روی هر کلاینت نصب شده باشد. شما می‌توانید با اجرای دستور زیر در ابتدای فایل program.cs این کار را انجام دهید.

                      var cert = new X509Certificate2( Properties.Resources.localhost );
                      
                      var store = new X509Store( StoreName.AuthRoot, StoreLocation.LocalMachine );
                      store.Open(OpenFlags.ReadWrite);
                      store.Add(cert);
                      store.Close();
                      در اینجا من فایل localhost را به Resource برنامه اضافه کردم.

                      ‫پشتیبانی آنلاین سایت با SignalR ،ASP.NET MVC و AngularJS

                      $
                      0
                      0
                        پشتیبانی آنلاین سایت، روشی مناسب برای افزایش سطح تماس مشتریان با فروشندگان، برای جلوگیری از اتلاف وقت در برقراری تماس میباشد.
                      قصد داریم در این بخش پشتیبانی آنلاین سایت را با استفاده از AngularJs /Asp.Net Mvc / Signalrتهیه کنیم.
                      امکانات این برنامه:
                      * امکان مکالمه متنی به همراه ارسال شکلک
                      * امکان انتقال مکالمه
                      * مشاهده آرشیو گفتگوها
                      * امکان ارسال فایل (بزودی)
                      * امکان ذخیره گفتگو و ارسال گفتگو به ایمیل  (بزودی)
                      * امکان ارسال تیکت در صورت آفلاین بودن کارشناسان (بزودی) 
                      * رعایت مسائل امنیتی(بزودی)

                      مراحل نحوه اجرای برنامه:
                      1-  باز کردن دو tab، یکی برای کارشناس یکی  برای مشتری .
                      2-  تعدادی کارشناس تعریف شده است که با کلیک بر روی هر کدام وارد پنل کارشناس خواهیم شد.
                      3- شروع مکالمه توسط مشتری با کلیک بر روی chatbox پایین صفحه (سمت راست پایین).
                      4- شروع کردن مکالمه توسط کارشناس. 
                      5- ادامه،خاتمه یا انتقال مکالمه توسط کارشناس.

                      نصب کتابخانه‌های زیر:
                      //client
                      Install-Package angularjs 
                      Install-Package angular-strap 
                      Install-Package Microsoft.AspNet.SignalR.JS 
                      install-package AngularJs.SignalR.Hub 
                      Install-Package jQuery.TimeAgo
                      Install-Package FontAwesome
                      Install-Package toastr
                      Install-Package Twitter.Bootstrap.RTL 
                      bower install angular-smilies  
                      
                      //server
                      Install-Package Newtonsoft.Json
                      Install-Package Microsoft.AspNet.SignalR 
                      Install-Package EntityFramework

                      گام‌های برنامه:
                      1-ایجاد جداول 
                      جدول Message: هر پیام دارای فرستنده و گیرنده‌ای، به همراه زمان ارسال میباشد.
                      جدول Session: شامل لیستی از پیام‌ها به همراه ارجاعی به خود (استفاده هنگام انتقال مکالمه )
                       public partial class Message
                          {
                              public int Id { get; set; }
                              public string Sender { get; set; }
                              public string Receiver { get; set; }
                              public string Body { get; set; }
                              public DateTimeOffset? CreationTime { get; set; }
                              public int? SessionId { get; set; }
                              public virtual Session Session { get; set; }
                          }
                          public partial class Session
                          {
                              public Session()
                              {
                                 Messages = new List<Message>();
                                 Sessions = new List<Session>();
                              }
                              public int Id { get; set; }
                              public string AgentName { get; set; }
                              public string CustomerName { get; set; }
                              public DateTime CreatedDateTime { get; set; }
                              public int? ParentId { get; set; }
                              public virtual Session Parent { get; set; }
                              public virtual ICollection<Message> Messages { get; set; }
                              public virtual ICollection<Session> Sessions { get; set; }
                          }

                      2- ایجاد ویو مدلهای زیر
                          public class UserInformation
                          {
                              public string ConnectionId { get; set; }
                              public bool IsOnline { get; set; }
                              public string UserName { get; set; }
                          }
                          public class ChatSessionVm
                          {
                              public string Key { get; set; }
                              public List<string> Value { get; set; }
                          }
                          public class AgentViewModel
                          {
                              public int Id { get; set; }
                              public string CustomerName { get; set; }
                              public int Lenght { get; set; }
                              public DateTimeOffset? Date { get; set; }
                          }

                      3- ایجاد Hub در سرور
                       [HubName("chatHub")]
                          public class ChatHub : Microsoft.AspNet.SignalR.Hub
                          {
                          }

                      4- فراخوانی chathub توسط کلاینت:برای آشنایی با سرویس  hubکلیک نمایید.

                      listeners متدهای سمت کلاینت
                      methods آرایه ای از متدهای سمت سرور

                       $scope.myHub = new hub("chatHub", {
                        listeners: {},
                        methods: []
                      })
                      در صورت موفقیت آمیز بودن اتصال به هاب، متد init سمت سرور فراخوانی میشود و وضعیت آنلاین بودن کارشناسان برای کلاینت مشخص میشود.
                       $scope.myHub.promise.done(function () {
                           $scope.myHub.init();
                           $scope.myHub.promise.done(function () { });
                        });
                       public void Init()
                              {
                                  _chatSessions = _chatSessions ?? (_chatSessions = new List<ChatSessionVm>());
                                  _agents = _agents ?? (_agents = new ConcurrentDictionary<string, UserInformation>());
                                  Clients.Caller.onlineStatus(_agents.Count(x => x.Value.IsOnline) > 0);
                              }

                      5-وضعیت کارشناسان :
                      در صورت آنلاین بودن کارشناسان: ارسال اولین پیام و تقاضای شروع مکالمه
                      در صورت آفلاین بودن کارشناسان:ارسال تیکت(بزودی)
                      اگر برای اولین بار  پیامی را ارسال میکنید، برای شما session ایی ایجاد نشده است. در اینصورت مکان تقاضای مشتری از سایت http://ipinfo.io دریافت شده و به سرور ارسال می‌گردد و  متد logvist سرور، تقاضای شروع مکالمه مشتری را به اطلاع  تمام کارشناسان میرساند و وضعیت chatbox را تغییر میدهد.
                      اگر session برای مشتری تعریف شده باشد، مکالمه مشتری با کارشناس مربوطه انجام میگردد.
                       $scope.requestChat = function (msg) {
                                      if (!defaultCustomerUserName) {
                                          //گرفتن کاربر لاگین شده
                                          //ما از آرایه تصادفی استفاده میکنیم
                                          var nameDefaultArray = [
                                              'حسین', 'حسن', 'علی', 'عباس', 'زهرا', 'سمیه'
                                          ];
                                          defaultCustomerUserName=nameDefaultArray[Math.floor(Math.random() * nameDefaultArray.length)];
                                      }
                                      var userName = defaultCustomerUserName;
                                      if (!$scope.chatId) {
                                          $scope.chatId = sessionStorage.getItem(chatKey);
                                          $http.get("http://ipinfo.io")
                                            .success(function (response) {
                                                $scope.myHub.logVisit(response.city, response.country, msg, userName);
                                            }).error(function (e, status, headers, config) {
                                                $scope.myHub.logVisit("Tehran", "Ir", msg, userName)
                                            });
                                          $scope.myHub.requestChat(msg);
                                          $scope.chatTitle = $scope.options.waitingForOperator;
                                          $scope.pendingRequestChat = true;
                                      } else {
                                          $scope.myHub.clientSendMessage(msg, userName);
                                      };
                                      $scope.message = "";
                                  };

                      6-مشاهده تقاضای مکالمه کاربران  توسط کارشناسان
                      کارشناسان در صورت تمایل، شروع به مکالمه با کاربر مینمایند و مکالمه آغاز میگردد.با شروع مکالمه توسط کارشناس، متد acceptRequestChat  سرور فراخوانی میشود.
                       پیام‌های مناسب برای کارشناس مربوطه، برای مشتری و تمام کارشناسان (به تمام کارشناسان اطلاع داده می‌شود که مشتری با چه کارشناسی در حال مکالمه میباشد) ارسال میگردد و مقادیر مربوطه در دیتابیس ذخیره میگردد.
                      public void AcceptRequestChat(string customerConnectionId, string body, string userName)
                              {
                                  var agent = FindAgent(Context.ConnectionId);
                                  var session = _chatSessions.FirstOrDefault(item => item.Key.Equals(agent.Key));
                                  if (session == null)
                                  {
                                      _chatSessions.Add(new ChatSessionVm
                                      {
                                          Key = agent.Key,
                                          Value = new List<string> { customerConnectionId }
                                      });
                                  }
                                  else
                                  {
                                      session.Value.Add(customerConnectionId);
                                  }
                                  Clients.Client(Context.ConnectionId).agentChat(customerConnectionId, body, userName);
                                  Clients.Client(customerConnectionId).clientChat(customerConnectionId, agent.Value.UserName);
                                  foreach (var item in _agents.Where(item => item.Value.IsOnline))
                                  {
                                      Clients.Client(item.Value.ConnectionId).refreshChatWith(agent.Value.UserName, customerConnectionId);
                                  }
                             var session = _db.Sessions.Add(new Session
                                  {
                                      AgentName = agent.Key,
                                      CustomerName = userName,
                                      CreatedDateTime = DateTime.Now
                                  });
                                  _db.SaveChanges();
                      
                                  var message = new Message
                                  {
                                      CreationTime = DateTime.Now,
                                      Sender = agent.Key,
                                      Receiver = userName,
                                      body=body,
                                      Session = session
                                  };
                                  _db.Messages.Add(message);
                                  _db.SaveChanges();
                              }
                      7-خاتمه مکالمه توسط کارشناس یا مشتری امکان پذیر میباشد:
                      متد closeChat  سرور فراخوانی میگردد. پیام مناسبی به مشتری و تمام کارشناسان ارسال میگردد.
                      public void CloseChat(string id)
                              {
                                  var findAgent = FindAgent(Context.ConnectionId);
                                  var session = _chatSessions.FirstOrDefault(item => item.Value.Contains(id));
                                  if (session == null) return;
                                  Clients.Client(id).clientAddMessage(findAgent.Key, "مکالمه شما با کارشناس مربوطه به اتمام رسیده است");
                      
                                  foreach (var agent in _agents)
                                  {
                                      Clients.Client(agent.Value.ConnectionId).refreshLeaveChat(agent.Value.UserName, id);
                                  }
                                  _chatSessions.Remove(session);
                              }

                      8-انتقال مکالمه مشتری به کارشناسی دیگر
                      مکالمه از کارشناس فعلی گرفته شده و به کارشناس جدید داده می‌شود؛ به همراه ارسال پیام‌های مناسب به طرف‌های مربوطه
                         public void EngageVisitor(string newAgentId, string cumtomerId, string customerName,string clientSessionId)
                              {
                                  #region remove session of current agent
                                  var currentAgent = FindAgent(Context.ConnectionId);
                                  var currentSession = _chatSessions.FirstOrDefault(item => item.Value.Contains(cumtomerId));
                                  if (currentSession != null)
                                  {
                                      _chatSessions.Remove(currentSession);
                                  }
                                  #endregion
                      
                                  #region add  session to new agent
                                  var newAgent = FindAgent(newAgentId);
                                  var newSession = _chatSessions.FirstOrDefault(item => item.Key.Equals(newAgent.Key));
                                  if (newSession == null)
                                  {
                                      _chatSessions.Add(new ChatSessionVm
                                      {
                                          Key = newAgent.Key,
                                          Value = new List<string> { cumtomerId }
                                      });
                                  }
                                  else
                                  {
                                      newSession.Value.Add(cumtomerId);
                                  }
                                  #endregion
                      
                                  Clients.Client(currentAgent.Value.ConnectionId).addMessage(cumtomerId, newAgent.Key,
                                      "ادامه مکالمه به کارشناس  " + newAgent.Key + "مقابل  منتقل شد");
                                  Clients.Client(newAgentId).addMessage(cumtomerId, currentAgent.Key,
                                      "لطفا مکالمه را ادامه دهید.با تشکر");
                      
                                  Clients.Client(cumtomerId).clientAddMessage(newAgent.Value.UserName,
                                      "مکالمه شما با کارشناس زیر برقرار گردید" + newAgent.Key);
                      
                                  var session = _db.Sessions.FirstOrDefault
                                      (item => item.AgentName.Equals(currentAgent.Value.UserName)&& item.CustomerName.Equals(customerName));
                                  if (session != null)
                                  {
                                      var sessionId = session.Id;
                                      var messages = _db.Messages.Where(item => item.Session.Id.Equals(sessionId));
                                      var result = JsonConvert.SerializeObject(messages, new Formatting(), _settings);
                                      Clients.Client(newAgentId).visitorSwitchConversation
                                          (Context.ConnectionId, customerName, result, clientSessionId);
                                  }
                                  foreach (var item in _agents.Where(item => item.Value.IsOnline))
                                  {
                                      Clients.Client(item.Value.ConnectionId).refreshChatWith(newAgent.Value.UserName, cumtomerId);
                                  }
                                  _db.Sessions.Add(new Session
                                  {
                                      AgentName = newAgent.Key,
                                      CustomerName = customerName,
                                      CreatedDateTime = DateTime.Now,
                                      Parent = _db.Sessions.Where(item => item.AgentName.Equals(currentAgent.Key)&& item.CustomerName.Equals(customerName)).OrderByDescending(item => item.Id).FirstOrDefault()
                                  });
                                  _db.SaveChanges();
                              }
                      از آنجاییکه اسم متدها کاملا گویا میباشد، به نظر نیازی به توضیح بیشتری ندارند.
                      فایل کامل  app.js 
                      var app = angular.module("app", ["SignalR", 'ngRoute', 'ngAnimate', 'ngSanitize', 'mgcrea.ngStrap', 'angular-smilies']); 
                      
                      app.config(["$routeProvider", "$provide", "$httpProvider", "$locationProvider",
                              function ($routeProvider, $provide, $httpProvider, $locationProvider) {
                                  $routeProvider.
                                     when('/', { templateUrl: 'app/views/home.html', controller: "HomeCtrl" }).
                                     when('/agent', { templateUrl: 'app/views/agent.html', controller: "ChatCtrl" })
                                      .otherwise({
                                          redirectTo: "/"
                                      });;
                              }]);
                      app.controller("HomeCtrl", ["$scope", function ($scope) {
                          $scope.title = "home";
                      }])
                      app.controller("ChatCtrl", ["$scope", "Hub", "$location", "$http", "$rootScope",
                          function ($scope, hub, $location, $http, $rootScope) {
                              if (!$scope.myHub) {
                                  var chatKey = "angular-signalr";
                                  var defaultCustomerUserName = null;
                                  function getid(id) {
                                      var find = false;
                                      var position = null;
                                      angular.forEach($scope.chatConversation, function (index, i) {
                                          if (index.id === id && !find) {
                                              find = true;
                                              position = i;
                                              return;
                                          }
                                      });
                                      return position;
                                  }
                                  function apply() {
                                      $scope.$apply();
                                  }
                                  $scope.boxheader = function () {
                                      var height = 0;
                                      $("#chat-box").slideToggle('slow', function () {
                                          if ($("#chat-box-header").css("bottom") === "0px") {
                                              height = $("#chat-box").height() + 20;
                                          } else {
                                              height = 0;
                                          }
                                          $("#chat-box-header").css("bottom", height);
                                      });
                                  };
                                  var init = function () {
                                      $scope.agent = {
                                          id: "", name: "", isOnline: false
                                      };
                                      $rootScope.msg = "";
                                      $scope.alarmStatus = false;
                                      $scope.options = {
                                          offlineTitle: "آفلاین",
                                          onlineTitle: "آنلاین",
                                          waitingForOperator: "لطفا منتظر بمانید تا به اپراتور وصل شوید",
                                          emailSent: "ایمیل ارسال گردید",
                                          emailFailed: "متاسفانه ایمیل ارسال نگردید",
                                          logOut: "خروج",
                                          setting: "تنظیمات",
                                          conversion: "آرشیو",
                                          edit: "ویرایش",
                                          alarm: "قطع/وصل کردن صدا",
                                          complete: "تکمیل",
                                          pending: "منتظر ماندن",
                                          reject: "عدم پذیرش",
                                          lock: "آنلاین شدن",
                                          unlock: "آفلاین شدن",
                                          alarmOn: "روشن",
                                          alarmOff: "خاموش",
                                          upload: "آپلود"
                                      };
                                      $scope.chatConversation = [];
                                      $scope.chatSessions = [];
                                      $scope.customerVisit = [];
                      
                                      $scope.agentClientMsgs = [];
                                      $scope.clientAgentMsg = [];
                                  }();
                      //تعریف هاب به همراه متدهای آن
                                  $scope.myHub = new hub("chatHub", {
                                      listeners: {
                                          "clientChat": function (id, agentName) {
                                              $scope.clientAgentMsg.push({ name: agentName, msg: "با سلام در خدمت میباشم" });
                                              $scope.chatTitle = "کارشناس: " + agentName;
                                              $scope.pendingRequestChat = false;
                                              sessionStorage.setItem(chatKey, id);
                                          }, "agentChat": function (id, firstComment, customerName) {
                                              var date = new Date();
                                              var position = getid(id);
                                              if (position > 0) {
                                                  $scope.chatSessions[position].length = $scope.chatConversation[position].length + 1;
                                                  $scope.chatSessions[position].date = date.toISOString();
                                                  return;
                                              }
                                              else {
                                                  $scope.chatConversation.push({
                                                      id: id,
                                                      sessions: [{
                                                          name: customerName,
                                                          msg: firstComment,
                                                          date: date
                                                      }],
                                                      agentName: $scope.agent.name,
                                                      customerName: customerName,
                                                      dateStartChat: date.getHours() + ":" + date.getMinutes(),
                                                  });
                                                  $scope.chatSessions.push({
                                                      id: id,
                                                      length: 1,
                                                      userName: customerName,
                                                      date: date.toISOString()
                                                  });
                                              }
                                              sessionStorage.setItem(chatKey, id);
                                              apply();
                                          }, 
                      //برروز رسانی لیست برای کارشناسان
                      "refreshChatWith": function (agentName, customerConnectionId) {
                                              angular.forEach($scope.customerVisit, function (index, i) {
                                                  if (index.connectionId === customerConnectionId) {
                                                      $scope.customerVisit[i].chatWith = agentName;
                                                  }
                                              });
                                              apply();
                                          },
                      //برروز رسانی لیست برای کارشناسان
                       "refreshLeaveChat": function (agentName, customerConnectionId) {
                                              angular.forEach($scope.customerVisit, function (index, i) {
                                                  if (index.connectionId === customerConnectionId) {
                                                      $scope.customerVisit[i].chatWith =agentName + "---" + "  به مکالمه خاتمه داده است ";
                                                  }
                                              });
                                              apply();
                                          }
                      //وضعیت آنلاین بودن کارشناسان
                                          , "onlineStatus": function (state) {
                                              if (state) {
                                                  $scope.chatTitle = $scope.options.onlineTitle;
                                                  $scope.hasOnline = true;
                                                  $scope.hasOffline = false;
                                              } else {
                                                  $scope.chatTitle = $scope.options.offlineTitle;
                                                  $scope.hasOffline = true;
                                                  $scope.hasOnline = false;
                                              }
                                              $scope.$apply()
                                          }, "loginResult": function (status, id, name) {
                                              if (status) {
                                                  $scope.agent.id = id;
                                                  $scope.agent.name = name;
                                                  $scope.agent.isOnline = true;
                                                  $scope.userIsLogin = $scope.agent;
                                                  $scope.$apply(function () {
                                                      $location.path("/agent");
                                                  });
                                              } else {
                                                  $scope.agent = null;
                                                  toastr.error("کارشناسی با این مشخصات وجود ندارد");
                                                  return;
                                              }
                                          }, "newVisit": function (userName, city, country, chatWith, connectionId, firstComment) {
                                              var exist = false;
                                              angular.forEach($scope.customerVisit, function (index) {
                                                  if (index.connectionId === connectionId) {
                                                      exist = true;
                                                      return;
                                                  }
                                              });
                                              if (!exist) {
                                                  var date = new Date();
                                                  $scope.customerVisit.unshift({
                                                      userName: userName,
                                                      date: date,
                                                      city: city,
                                                      country: country,
                                                      chatWith: chatWith,
                                                      connectionId: connectionId,
                                                      firstComment: firstComment
                                                  });
                                                  if ($scope.alarmStatus) {
                                                      var snd = new Audio("/App/assets/sounds/Sedna.ogg");
                                                      snd.play();
                                                  }
                                                  toastr.success("تقاضای جدید دریافت گردید");
                                                  apply();
                                              }
                                          }, "addMessage": function (id, from, value) {
                                              if ($scope.alarmStatus) {
                                                  var snd = new Audio("/App/assets/sounds/newmsg.mp3");
                                                  snd.play();
                                              }
                                              $scope.agentUserMsgs = [];
                                              var date = new Date();
                                              var position = getid(id);
                                              if ($scope.chatConversation.length > 0 && position != null) {
                                                  $scope.chatConversation[position].sessions.push({ name: from, msg: value, date: date });
                                              }
                                              var item = $scope.chatConversation[position];
                                              if (item) {
                                                  angular.forEach(item.sessions, function (index) {
                                                      $scope.agentUserMsgs.push({ name: index.name, msg: index.msg, date: date });
                                                  });
                                                  $scope.chatSessions[position].length = $scope.chatSessions[position].length + 1;
                                              }
                                              apply();
                                          }, "clientAddMessage": function (id, from) {
                                              if ($scope.alarmStatus) {
                                                  var snd = new Audio("/App/assets/sounds/newmsg.mp3");
                                                  snd.play();
                                              }
                                              $scope.clientAgentMsg.push({ name: id, msg: from });
                                              apply();
                                          }, "visitorSwitchConversation": function (id, customerName, sessions, sessionId) {
                                              sessions = JSON.parse(sessions);
                                              var date = new Date();
                                              var sessionList = [];
                                              angular.forEach(sessions, function (index) {
                                                  sessionList.push({
                                                      name: index.sender,
                                                      msg: index.body,
                                                      date: index.creationTime
                                                  });
                                              });
                                              $scope.chatConversation.push({
                                                  id: sessionId,
                                                  sessions: sessionList,
                                                  customerName: customerName,
                                                  dateStartChat: date.getHours() + ":" + date.getMinutes(),
                                                  agentName: $scope.agent.name
                                              });
                                              $scope.chatSessions.push({
                                                  id: sessionId,
                                                  length: sessions.length,
                                                  date: date
                                              });
                                          }, "receiveTicket": function (items) {
                                              angular.forEach(JSON.parse(items), function (index) {
                                                  $scope.ticketList = [];
                                                  $scope.ticketList.push(index);
                                              });
                                          }, 
                      //آرشیو گفته گوهای کارشناس
                      "receiveHistory": function (items) {
                                              $scope.agentHistory = [];
                                              angular.forEach(JSON.parse(items), function (index) {
                                                  $scope.agentHistory.push(index);
                                              });
                                              apply();
                                          }, 
                      //جزییات آرشیو گفتگوها
                      "detailsHistory": function (items) {
                                              $scope.historyMsg = [];
                                              angular.forEach(JSON.parse(items), function (index) {
                                                  $scope.historyMsg.push({ name: index.sender, msg: index.body, date: index.creationTime });
                                              });
                                              $("#detailsAgentHistory").modal();
                                              apply();
                                          }, 
                      //لیست کارشناسان آنلاین
                      "agentList": function (items) {
                                              $scope.agentList = [];
                                              angular.forEach(items, function (index) {
                                                  if ($scope.agent.name != index.Key) {
                                                      $scope.agentList.push({ name: index.Key, id: index.Value.ConnectionId });
                                                  }
                                              });
                                              $("#agentList").modal();
                                              apply();
                                          }
                                      },
                                      methods: ["agentConnect", "sendTicket", "requestChat", "clientSendMessage", "closeChat", "init", "logVisit",
                                          "agentChangeStatus", "engageVisitor", "agentSendMessage", "transfer", "leaveChat", "acceptRequestChat",
                                          "leaveChat", "detailsSessoinMessage", "showAgentList", "getAgentHistoryChat"
                                      ], errorHandler: function (error) {
                                          console.error(error);
                                      }
                                  });
                                  $scope.myHub.promise.done(function () {
                                      $scope.myHub.init();
                                      $scope.myHub.promise.done(function () { });
                                  });
                      
                                  $scope.LeaveChat = function () {
                                      $scope.myHub.LeaveChat();
                                  };
                                  $scope.loginAgent = function (userName) {
                                      // username :security user username from agent role
                                      if (userName == "hossein" || userName == "ali") {
                                          $scope.myHub.promise.done(function () {
                                              $scope.myHub.agentConnect(userName).then(function (result) {
                                                  $scope.agent.name = userName;
                                                  $scope.agent.isOnline = true;
                                              });
                                          });
                                      }
                                  };
                                  $scope.requestChat = function (msg) {
                                      if (!defaultCustomerUserName) {
                                          //گرفتن کاربر لاگین شده
                                          //ما از آرایه تصادفی استفاده میکنیم
                                          var nameDefaultArray = [
                                              'حسین', 'حسن', 'علی', 'عباس', 'زهرا', 'سمیه'
                                          ];
                                          defaultCustomerUserName=nameDefaultArray[Math.floor(Math.random() * nameDefaultArray.length)];
                                      }
                                      var userName = defaultCustomerUserName;
                                      if (!$scope.chatId) {
                                          $scope.chatId = sessionStorage.getItem(chatKey);
                                          $http.get("http://ipinfo.io")
                                            .success(function (response) {
                                                $scope.myHub.logVisit(response.city, response.country, msg, userName);
                                            }).error(function (e, status, headers, config) {
                                                $scope.myHub.logVisit("Tehran", "Ir", msg, userName)
                                            });
                                          $scope.myHub.requestChat(msg);
                                          $scope.chatTitle = $scope.options.waitingForOperator;
                                          $scope.pendingRequestChat = true;
                                      } else {
                                          $scope.myHub.clientSendMessage(msg, userName);
                                      };
                                      $scope.message = "";
                                  };
                                  $scope.acceptRequestChat = function (customerConnectionId, firstComment, customerName) {
                                      $scope.myHub.acceptRequestChat(customerConnectionId, firstComment, customerName);
                                  };
                                  $scope.changeAgentStatus = function () {
                                      $scope.agent.isOnline = !$scope.agent.isOnline;
                                      $scope.myHub.agentChangeStatus($scope.agent.isOnline);
                                  };
                                  $scope.detailsChat = function (chatId, userName) {
                                      $scope.agentUserMsgs = [];
                                      angular.forEach($scope.chatConversation, function (index) {
                                          if (index.id === chatId) {
                                              $scope.dateStartChat = index.dateStartChat;
                                              angular.forEach(index.sessions, function (value) {
                                                  $scope.agentUserMsgs.push({ name: value.name, msg: value.msg, date: value.date });
                                              });
                                          };
                                      });
                                      $scope.agentChatWithUser = chatId;
                                      $scope.customerName = userName;
                                      $("#agentUserChat").modal();
                                  };
                                  $scope.ticket = {
                                      submit: function () {
                                          var name = $scope.ticket.name;
                                          var email = $scope.ticket.email;
                                          var comment = $scope.ticket.comment;
                                          $scope.myHub.sendTicket(name, email, comment);
                                      }
                                  };
                                  $scope.showHistory = function () {
                                      $scope.myHub.getAgentHistoryChat($scope.agent.name);
                                  };
                                  $scope.detailsChatHistory = function (id) {
                                      $scope.myHub.detailsSessoinMessage(id, $scope.agent.id);
                                  };
                                  $scope.agentMsgToUser = function (msg) {
                                      var chatId = $scope.agentChatWithUser;
                                      var customerName = $scope.customerName;
                                      if (!customerName) {
                                          angular.forEach($scope.customerVisit, function (index) {
                                              if (index.connectionId == chatId) {
                                                  customerName = index.userName;
                                              }
                                          });
                                      }
                                      if (chatId !== "" && msg !== "") {
                                          $scope.myHub.agentSendMessage(chatId, msg, customerName);
                                      }
                                      //not bind to scope.msg! not correctly work
                                      $scope.msg = "";
                                      $("#post-msg").val("");
                                  };
                                  $scope.closeChat = function (chatId) {
                                      var item = $scope.chatConversation[getid(chatId)];
                                      $scope.myHub.closeChat(chatId);
                                  };
                                  $scope.engageVisitor = function (newAgentId) {
                                      var customerId = $scope.customerId;
                                      var customerName = $scope.customerName;
                                      var clientSessionId = $scope.clientSessionId;
                                      $scope.myHub.engageVisitor(newAgentId, customerId, customerName, clientSessionId);
                                      $("[data-dismiss=modal]").trigger({ type: "click" });
                                  };
                                  $scope.selectVisitor = function (customerId, customerName, clientSessionId) {
                                      $scope.customerId = customerId;
                                      $scope.customerName = customerName;
                                      $scope.clientSessionId = clientSessionId;
                                      $scope.myHub.showAgentList();
                                  };
                                  $scope.setClass = function (item) {
                                      if (item === "من")
                                          return "question";
                                      else
                                          return "response";
                                  };
                                  $scope.setdirectionClass = function (item) {
                                      if (item === $scope.agent.name)
                                          return { "float": "left" };
                                      else
                                          return { "float": "right" };
                                  };
                                  $scope.setArrowClass = function (item) {
                                      if (item === $scope.agent.name)
                                          return "left-arrow";
                                      else
                                          return "right-arrow";
                                  };
                                  $scope.setAlarm = function () {
                                  $scope.alarmStatus = !$scope.alarmStatus;
                                  };
                              }
                          }]);
                      app.directive("showtab", function () {
                          return {
                              link: function (scope, element, attrs) {
                                  element.click(function (e) {
                                      e.preventDefault();
                                      $(element).addClass("active");
                                      $(element).tab("show");
                                  });
                              }
                          };
                      });
                      //زمان ارسال پیام
                      app.directive("timeAgo", function ($q) {
                          return {
                              restrict: "AE",
                              scope: false,
                              link: function (scope, element, attrs) {
                                  jQuery.timeago.settings.strings =
                                  {
                                      prefixAgo: null,
                                      prefixFromNow: null,
                                      suffixAgo: "پیش",
                                      suffixFromNow: "از حالا",
                                      seconds: "کمتر از یک دقیقه",
                                      minute: "در حدود یک دقیقه",
                                      minutes: "%d دقیقه",
                                      hour: "حدود یگ ساعت",
                                      hours: "حدود %d ساعت ",
                                      day: "یک روز",
                                      days: "%d روز",
                                      month: "حدود یک ماه",
                                      months: "%d ماه",
                                      year: "حدود یک سال",
                                      years: "%d سال",
                                      wordSeparator: " ",
                                      numbers: []
                                  }
                                  var parsedDate = $q.defer();
                                  parsedDate.promise.then(function () {
                                      jQuery(element).timeago();
                                  });
                                  attrs.$observe("title", function (newValue) {
                                      parsedDate.resolve(newValue);
                                  });
                              }
                          };
                      });
                      فایل chathub.cs
                      برای آشنایی بیشتر مقاله نگاهی به SignalR Hubs   مفید خواهد بود.
                         [HubName("chatHub")]
                          public class ChatHub : Microsoft.AspNet.SignalR.Hub
                          {
                              private readonly ApplicationDbContext _db = new ApplicationDbContext();
                              private static ConcurrentDictionary<string, UserInformation> _agents;
                              private static List<ChatSessionVm> _chatSessions;
                              private readonly JsonSerializerSettings _settings = new JsonSerializerSettings
                              {
                                  ContractResolver = new CamelCasePropertyNamesContractResolver(),
                                  ReferenceLoopHandling = ReferenceLoopHandling.Ignore
                              };
                              public void Init()
                              {
                                  _chatSessions = _chatSessions ?? (_chatSessions = new List<ChatSessionVm>());
                                  _agents = _agents ?? (_agents = new ConcurrentDictionary<string, UserInformation>());
                                  Clients.Caller.onlineStatus(_agents.Count(x => x.Value.IsOnline) > 0);
                              }
                              public void AgentConnect(string userName)
                              {
                                  //ما برای ساده کردن مقایسه ساده ای انجام دادیم فقط کاربر حسین یا علی میتواند کارشناس باشد
                                  if (userName == "hossein" || userName == "ali")
                                  {
                                      var agent = new UserInformation();
                                      if (_agents.Any(item => item.Key == userName))
                                      {
                                          agent = _agents[userName];
                                          agent.ConnectionId = Context.ConnectionId;
                                      }
                                      else
                                      {
                                          agent.ConnectionId = Context.ConnectionId;
                                          agent.UserName = userName;
                                          agent.IsOnline = true;
                                          _agents.TryAdd(userName, agent);
                      
                                      }
                                      Clients.Caller.loginResult(true, agent.ConnectionId, agent.UserName);
                                      Clients.All.onlineStatus(_agents.Count(x => x.Value.IsOnline) > 0);
                                  }
                                  else
                                  {
                                      Clients.Caller.loginResult(false, null, null);
                                  }
                              }
                              public void AgentChangeStatus(bool status)
                              {
                                  var agent = _agents.FirstOrDefault(x => x.Value.ConnectionId == Context.ConnectionId).Value;
                                  if (agent == null) return;
                                  agent.IsOnline = status;
                                  Clients.All.onlineStatus(_agents.Count(x => x.Value.IsOnline) > 0);
                              }
                              public void LogVisit(string city, string country, string firstComment, string userName)
                              {
                                  foreach (var agent in _agents)
                                  {
                                      Clients.Client(agent.Value.ConnectionId).newVisit(userName, city, country, null, Context.ConnectionId, firstComment);
                                  }
                              }
                              public void AcceptRequestChat(string customerConnectionId, string body, string userName)
                              {
                                  var agent = FindAgent(Context.ConnectionId);
                                  var session = _chatSessions.FirstOrDefault(item => item.Key.Equals(agent.Key));
                                  if (session == null)
                                  {
                                      _chatSessions.Add(new ChatSessionVm
                                      {
                                          Key = agent.Key,
                                          Value = new List<string> { customerConnectionId }
                                      });
                                  }
                                  else
                                  {
                                      session.Value.Add(customerConnectionId);
                                  }
                                  Clients.Client(Context.ConnectionId).agentChat(customerConnectionId, body, userName);
                                  Clients.Client(customerConnectionId).clientChat(customerConnectionId, agent.Value.UserName);
                                  foreach (var item in _agents.Where(item => item.Value.IsOnline))
                                  {
                                      Clients.Client(item.Value.ConnectionId).refreshChatWith(agent.Value.UserName, customerConnectionId);
                                  }
                                  _db.Sessions.Add(new Session
                                  {
                                      AgentName = agent.Key,
                                      CustomerName = userName,
                                      CreatedDateTime = DateTime.Now
                                  });
                                  _db.SaveChanges();
                      
                                  var message = new Message
                                  {
                                      CreationTime = DateTime.Now,
                                      Sender = agent.Key,
                                      Receiver = userName,
                                      Body = body,
                                      //ConnectionId = _agents.FirstOrDefault(item => item.Value.UserName == userName).Key,
                                      Session = _db.Sessions.OrderByDescending(item => item.Id)
                                      .FirstOrDefault(item => item.AgentName.Equals(agent.Key) && item.CustomerName.Equals(userName))
                                  };
                                  _db.Messages.Add(message);
                                  _db.SaveChanges();
                              }
                              public void GetAgentHistoryChat(string userName)
                              {
                                  var dic = new Dictionary<int, int>();
                                  var lenght = 0;
                                  var chats = _db.Sessions.OrderBy(item => item.Id).Include(item => item.Parent)
                                      .Where(item => item.AgentName.Equals(userName)).ToList();
                      
                                  foreach (var session in chats)
                                  {
                                      Result(session, ref lenght);
                                      dic.Add(session.Id, lenght);
                                      lenght = 0;
                                  }
                                  if (!chats.Any()) return;
                      
                                  var historyResult = chats.Select(item => new AgentViewModel
                                  {
                                      Id = item.Id,
                                      CustomerName = item.CustomerName,
                                      Date = item.CreatedDateTime,
                                      Lenght = dic.Any(di => di.Key.Equals(item.Id)) ? dic.FirstOrDefault(di => di.Key.Equals(item.Id)).Value : 0,
                                  }).OrderByDescending(item => item.Id).ToList();
                                  Clients.Caller.receiveHistory(JsonConvert.SerializeObject(historyResult, new Formatting(), _settings));
                              }
                              public void DetailsSessoinMessage(int sessionId, string agentId)
                              {
                                  var session = _db.Sessions.FirstOrDefault(item => item.Id.Equals(sessionId));
                                  if (session == null) return;
                                  var list = new List<Message>();
                                  GetAllMessages(session, list);
                                  var result = JsonConvert.SerializeObject(list.OrderBy(item => item.Id), new Formatting(), _settings);
                                  Clients.Client(Context.ConnectionId).detailsHistory(result);
                              }
                              public void ClientSendMessage(string body, string userName)
                              {
                                  var session = _chatSessions.FirstOrDefault(item => item.Value.Contains(Context.ConnectionId));
                                  if (session == null || session.Key == null) return;
                                  var agentId = _agents.FirstOrDefault(item => item.Key.Equals(session.Key)).Value.ConnectionId;
                                  Clients.Caller.clientAddMessage("من", body);
                                  Clients.Client(agentId).addMessage(Context.ConnectionId, userName, body);
                                  var message = new Message
                                  {
                                      Sender = FindAgent(agentId).Key,
                                      Receiver = userName,
                                      Body = body,
                                      CreationTime = DateTime.Now,
                                      Session = FindSession(userName, FindAgent(agentId).Key)
                                  };
                                  _db.Messages.Add(message);
                                  _db.SaveChanges();
                              }
                              public void AgentSendMessage(string id, string body, string userName)
                              {
                                  var agent = FindAgent(Context.ConnectionId);
                                  Clients.Caller.addMessage(id, agent.Value.UserName, body);
                                  Clients.Client(id).clientAddMessage(agent.Value.UserName, body);
                                  var message = new Message
                                  {
                                      Sender = agent.Key,
                                      Receiver = userName,
                                      Body = body,
                                      Session = FindSession(agent.Key, userName),
                                      CreationTime = DateTime.Now
                                  };
                                  _db.Messages.Add(message);
                                  _db.SaveChanges();
                              }
                              public void CloseChat(string id)
                              {
                                  var findAgent = FindAgent(Context.ConnectionId);
                                  var session = _chatSessions.FirstOrDefault(item => item.Value.Contains(id));
                                  if (session == null) return;
                                  Clients.Client(id).clientAddMessage(findAgent.Key, "مکالمه شما با کارشناس مربوطه به اتمام رسیده است");
                      
                                  foreach (var agent in _agents)
                                  {
                                      Clients.Client(agent.Value.ConnectionId).refreshLeaveChat(agent.Value.UserName, id);
                                  }
                                  _chatSessions.Remove(session);
                              }
                              public void RequestChat(string message)
                              {
                                  Clients.Caller.clientAddMessage("من", message);
                              }
                              public void EngageVisitor(string newAgentId, string cumtomerId, string customerName,string clientSessionId)
                              {
                                  #region remove session of current agent
                                  var currentAgent = FindAgent(Context.ConnectionId);
                                  var currentSession = _chatSessions.FirstOrDefault(item => item.Value.Contains(cumtomerId));
                                  if (currentSession != null)
                                  {
                                      _chatSessions.Remove(currentSession);
                                  }
                                  #endregion
                      
                                  #region add  session to new agent
                                  var newAgent = FindAgent(newAgentId);
                                  var newSession = _chatSessions.FirstOrDefault(item => item.Key.Equals(newAgent.Key));
                                  if (newSession == null)
                                  {
                                      _chatSessions.Add(new ChatSessionVm
                                      {
                                          Key = newAgent.Key,
                                          Value = new List<string> { cumtomerId }
                                      });
                                  }
                                  else
                                  {
                                      newSession.Value.Add(cumtomerId);
                                  }
                                  #endregion
                      
                                  Clients.Client(currentAgent.Value.ConnectionId).addMessage(cumtomerId, newAgent.Key,
                                      "ادامه مکالمه به کارشناس  " + newAgent.Key + "مقابل  منتقل شد");
                                  Clients.Client(newAgentId).addMessage(cumtomerId, currentAgent.Key,
                                      "لطفا مکالمه را ادامه دهید.با تشکر");
                      
                                  Clients.Client(cumtomerId).clientAddMessage(newAgent.Value.UserName,
                                      "مکالمه شما با کارشناس زیر برقرار گردید" + newAgent.Key);
                      
                                  var session = _db.Sessions.FirstOrDefault
                                      (item => item.AgentName.Equals(currentAgent.Value.UserName)&& item.CustomerName.Equals(customerName));
                                  if (session != null)
                                  {
                                      var sessionId = session.Id;
                                      var messages = _db.Messages.Where(item => item.Session.Id.Equals(sessionId));
                                      var result = JsonConvert.SerializeObject(messages, new Formatting(), _settings);
                                      Clients.Client(newAgentId).visitorSwitchConversation
                                          (Context.ConnectionId, customerName, result, clientSessionId);
                                  }
                                  foreach (var item in _agents.Where(item => item.Value.IsOnline))
                                  {
                                      Clients.Client(item.Value.ConnectionId).refreshChatWith(newAgent.Value.UserName, cumtomerId);
                                  }
                                  _db.Sessions.Add(new Session
                                  {
                                      AgentName = newAgent.Key,
                                      CustomerName = customerName,
                                      CreatedDateTime = DateTime.Now,
                                      Parent = _db.Sessions.Where(item => item.AgentName.Equals(currentAgent.Key)&& item.CustomerName.Equals(customerName)).OrderByDescending(item => item.Id).FirstOrDefault()
                                  });
                                  _db.SaveChanges();
                              }
                              public void ShowAgentList()
                              {
                                  Clients.Caller.agentList(_agents.ToList());
                              }
                              public override Task OnDisconnected(bool stopCalled)
                              {
                                  var id = Context.ConnectionId;
                                  var isAgent = _agents != null && _agents.Any(item => item.Value.ConnectionId.Equals(id));
                                  if (isAgent)
                                  {
                                      UserInformation agent;
                                      var currentAgentConnectionId = FindAgent(id).Key;
                                      if (currentAgentConnectionId == null)
                                          return base.OnDisconnected(stopCalled);
                                      if (_chatSessions.Any())
                                      {
                                          var sessions = _chatSessions.FirstOrDefault(item => item.Key.Equals(currentAgentConnectionId));
                                          //اطلاع دادن به تمام کاربرانی که در حال مکالمه با کارشناس هستند
                                          if (sessions != null)
                                          {
                                              var result = sessions.Value.ToList();
                                              for (var i = 0; i < result.Count(); i++)
                                              {
                                                  var localId = result[i];
                                                  Clients.Client(localId).clientAddMessage(currentAgentConnectionId, "ارتباط شما با مشاور مورد نظر قطع شده است");
                                              }
                                          }
                                      }
                                      _agents.TryRemove(currentAgentConnectionId, out agent);
                                      Clients.All.onlineStatus(_agents.Count(x => x.Value.IsOnline) > 0);
                                      Clients.Client(id).loginResult(false, null, null);
                                  }
                                  else
                                  {
                                      if (_chatSessions == null ||
                                          !_chatSessions.Any(item => item.Value.Contains(id)&& _agents == null))
                                          return base.OnDisconnected(stopCalled);
                      
                                      var session = _chatSessions.FirstOrDefault(item => item.Value.Contains(id));
                                      if (session == null)
                                          return base.OnDisconnected(stopCalled);
                      
                                      var agentName = session.Key;
                                      var agent = _agents.FirstOrDefault(item => item.Key.Equals(agentName));
                                      if (agent.Key != null)
                                      {
                                          Clients.Client(agent.Value.ConnectionId).addMessage(id, "کاربر", "اتصال با کاربر قطع شده است");
                                      }
                                  }
                                  return base.OnDisconnected(stopCalled);
                              }
                      
                      
                              private KeyValuePair<string, UserInformation> FindAgent(string connectionId)
                              {
                                  return _agents.FirstOrDefault(item => item.Value.ConnectionId.Equals(connectionId));
                              }
                              private Session FindSession(string key, string userName)
                              {
                                  return _db.Sessions.Where(item => item.AgentName.Equals(key) && item.CustomerName.Equals(userName))
                                      .OrderByDescending(item => item.Id).FirstOrDefault();
                              }
                              private static void Result(Session parent, ref int lenght)
                              {
                                  while (true)
                                  {
                                      if (parent == null)
                                          return;
                                      lenght += parent.Messages.Count();
                                      parent = parent.Parent;
                                  }
                              }
                              private static List<Message> GetAllMessages(Session node, List<Message> list)
                              {
                                  if (node == null) return null;
                                  list.AddRange(node.Messages);
                                  if (node.Parent != null)
                                  {
                                      GetAllMessages(node.Parent, list);
                                  }
                                  return null;
                              }
                          }
                      فایل agent.html
                      <div><div><h2>خوش آمدید
                                  <span ng-bind="agent.name"></span><a ng-click="changeAgentStatus()"><i ng-if="changeStatus==null"
                                         data-placement="bottom"
                                         data-trigger="hover "
                                         bs-tooltip="options.lock"></i><i ng-if="changeStatus==true"
                                         data-placement="bottom"
                                         data-trigger="hover"
                                         bs-tooltip="options.unlock"></i></a></h2><div style="float: left"><a ng-click="setAlarm()"><i ng-show="alarmStatus"
                                         data-placement="bottom"
                                         data-trigger="hover "
                                         bs-tooltip="options.alarmOn"></i><i ng-show="!alarmStatus"
                                         data-placement="bottom"
                                         data-trigger="hover "
                                         bs-tooltip="options.alarmOff"></i></a><!--<a data-placement="bottom"
                                     data-trigger="hover "
                                     bs-tooltip="options.conversion" ng-click="showHistory()"><i></i></a>--><a data-placement="bottom"
                                     data-trigger="hover "
                                     bs-tooltip="options.edit"><i></i><span></span></a><a data-placement="bottom"
                                     data-trigger="hover "
                                     bs-tooltip="options.setting"><i></i></a><a data-placement="bottom"
                                     data-trigger="hover "
                                     bs-tooltip="options.signOut" ng-click="LeaveChat()"><i></i><span></span></a></div></div><div><div><div id="chat-content"><div><ul><li><a showtab href="#online-list">آنلاین</a></li><li><a ng-click="showHistory()" showtab href="#conversation">آرشیو گفتگوها</a></li></ul><div><div id="online-list"><div><h2><i></i><span></span><span>نمایش آنلاین مراجعه ها</span></h2></div><div><div id="agent-chat"><div id="real-time-visits"><table id="current-visits"><thead><tr><th>نام کاربر</th><th>زمان اولین تقاضا</th><th>منطقه</th><th>پاسخ</th></tr></thead><tbody><tr id="{{item.connectionId}}" ng-animate="animate" ng-repeat="item in customerVisit "><td ng-bind="item.userName"></td><td><span time-ago title="{{item.date}}"></span></td><td><span ng-bind="item.country"></span> /<span ng-bind="item.city"> </span></td><td><a style="cursor: pointer" ng-if="item.chatWith== null"
                                                                                 ng-click="acceptRequestChat(item.connectionId,item.firstComment,item.userName)">شروع مکالمه
                                                                              </a><span ng-if="item.chatWith ">وضعیت:
                                                                                  <span>در حال مکالمه با</span><span ng-bind="item.chatWith"></span><a ng-show="item.chatWith==agent.name"
                                                                                     ng-click="selectVisitor(item.connectionId,item.userName,item.connectionId)">انتقال مکالمه
                                                                                  </a></span><ul ng-repeat="session in chatSessions track by $index" style="padding:0px;"><li ng-if="session.id==item.connectionId" id="{{session.id}}"><div><p>تاریخ شروع مکالمه:
                                                                                              <span time-ago title="{{session.date}}"></span></p><p>تعداد پیام ها:
                                                                                              <span ng-bind="session.length"></span></p></div><p><a ng-click="detailsChat(session.id,session.userName)">جزییات </a><a ng-click="closeChat(session.id)">خاتمه عملیات
                                                                                          </a></p></li></ul></td></tr></tbody></table></div></div></div></div><div id="conversation"><div><h2><i></i><span></span><span>آرشیو گفتگوهای </span>
                                                          {{agent.name}}</h2></div><div><div><table id="current-visits"><thead><tr><th>شناسه مشتری</th><th>نام مشتری</th><th>تعداد محاوره ها</th><th>تاریخ</th><th>جزئیات</th></tr></thead><tbody><tr ng-repeat="item in agentHistory track by $index"><td ng-bind="item.id"></td><td ng-bind="item.customerName"></td><th ng-bind="item.lenght"></th><td><span time-ago title="{{item.date}}"></span></td><th><ang-click="detailsChatHistory(item.id)" >مشاهده جزییات گفتگو</a></th></tr></tbody></table></div></div></div></div></div></div></div></div><div id="detailsAgentHistory" tabindex="-1" role="dialog" aria-labelledby="cmdLabel" aria-hidden="true"><div><div><div><div><button type="button" data-dismiss="modal" aria-hidden="true">×</button></div><h2><span></span>تاریخچه گفتگو
                                          </h2></div><div><div style="display: block"><ul ng-repeat="item in historyMsg"><li><span ng-bind="item.name" ng-style="setdirectionClass(item.name)"></span><span ng-style="setdirectionClass(item.name)"><span ng-class="setArrowClass(item.name)"></span><span time-ago title="{{item.date}}"></span><span><p ng-bind-html="item.msg | smilies"></p></span></span></li></ul></div></div></div></div></div><div id="agentList" tabindex="-1" role="dialog" aria-labelledby="cmdLabel" aria-hidden="true"><div><div><div><div><button type="button" data-dismiss="modal" aria-hidden="true">×</button></div><h2><span></span>لیست تمام کارشناسان
                                          </h2></div><div><div style="display: block;"><div ng-show="agentList.length==0">کارشناس آنلاینی وجود ندارد
                                              </div><ul ng-repeat="item in agentList"><li><span><a ng-click="engageVisitor(item.id)">{{item.name}}</a></span></li></ul></div></div></div></div></div><div id="agentUserChat"  tabindex="-1" role="dialog" aria-labelledby="cmdLabel" aria-hidden="true"><div><div><div><div><button type="button" data-dismiss="modal" aria-hidden="true">×</button></div><h2><span></span>گفتگو
                                          </h2></div><div><div><div><div style="display: block;"><label>شروع چت در </label>:<span ng-bind="dateStartChat"></span><ul><li ng-repeat="item in agentUserMsgs"><span ng-bind="item.name" ng-style="setdirectionClass(item.name)"></span><span ng-style="setdirectionClass(item.name)"><span ng-class="setArrowClass(item.name)"></span><span time-ago title="{{item.date}}"></span><span><p ng-bind-html="item.msg | smilies"></p></span></span></li></ul><div><div><textarea id="post-msg" ng-model="msg" placeholder="متن خود را وارد نمایید" style="overflow: hidden; word-wrap: break-word; resize: horizontal; height: 80px; max-width: 100%"></textarea><span smilies-selector="msg" smilies-placement="right" smilies-title="Smilies"></span></div><div style="text-align: center; margin-top: 5px"><button ng-click="agentMsgToUser(msg)">ارسال</button></div></div></div></div></div></div></div></div></div></div>
                      فایل index.cshtml
                      <html ng-app="app"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Live Support</title><link href="~/Content/bootstrap-rtl.css" rel="stylesheet" /><link href="~/Scripts/smilies/angular-smilies-embed.css" rel="stylesheet" /><link href="~/Content/font-awesome.css" rel="stylesheet" /><link href="~/Content/toastr.css" rel="stylesheet" /><link href="~/Content/liveSupport.css" rel="stylesheet" /><script src="~/Scripts/jquery-1.10.2.js"></script><script src="~/Scripts/toastr.js"></script><script src="~/Scripts/jquery.timeago.js"></script><script src="~/Scripts/angular.js"></script><script src="~/Scripts/angular-animate.js"></script><script src="~/Scripts/angular-sanitize.js"></script><script src="~/Scripts/angular-route.js"></script><script src="~/Scripts/angular-strap.js"></script><script src="~/Scripts/angular-strap.tpl.js"></script><script src="~/Scripts/smilies/angular-smilies.js"></script><script src="~/Scripts/jquery.signalR-2.2.0.js"></script><script src="~/Scripts/angular-signalr-hub.js"></script><script src="~/app/app.js"></script>
                          @Scripts.Render("~/bundles/bootstrap")</head><body ng-controller="ChatCtrl"><div ng-view></div><div id="chat-box-header" ng-click="boxheader()">
                              {{chatTitle}}</div><div id="chat-box"><div ng-show="hasOnline"><div id="style-1" style="min-height:100px;"><div ng-repeat="item in clientAgentMsg  track by $index"><span ng-class="setClass(item.name)">
                                              {{item.name}}</span><br /><p ng-bind-html="item.msg | smilies"></p></div></div><div><label>پیام</label><div style="text-align: left; clear: both"><a data-placement="top"
                                             data-trigger="hover "
                                             bs-tooltip="options.alarm" ng-click="alarm()"><i></i></a><a data-placement="top"
                                             data-trigger="hover "
                                             bs-tooltip="options.signOut" href="signOut()"><i></i><span></span></a><a data-placement="top"
                                             data-trigger="hover "
                                             bs-tooltip="options.upload" href="fileupload()"><span><i></i></span></a></div><div><textarea style="height: 150px; max-height: 160px;" ng-model="message" placeholder=" متن خود را وارد نمایید"></textarea><span smilies-selector="message" smilies-placement="right" smilies-title="Smilies"></span></div></div><div style="text-align: center"><button type="button" ng-disabled="pendingRequestChat" ng-click="requestChat(message)">ارسال </button></div></div><div ng-show="hasOffline"><div><form name="Ticket" id="form1"><fieldset><div><label>نام</label><input name="email"
                                                         ng-model="ticket.name"></div><div><label>ایمیل</label><input name="email"
                                                         ng-model="ticket.email"></div><div><label>پیام</label></div><div><textarea ng-model="ticket.comment" placeholder="متن خود را وارد نمایید"></textarea><span smilies-selector="ticket.comment" smilies-placement="right" smilies-title="Smilies"></span></div></fieldset><div style="text-align: center"><button type="button"
                                                      ng-click="ticket.submit(ticket)">ارسال
                                              </button></div></form></div></div></div></body></html>
                      LiveSupport.zip 

                      نکات تکمیلی :
                      نگاشت  Hub‌ها به برنامه در مسیر ("signalr/")در فایل  ConfigureAuth.Cs 
                        app.MapSignalR();

                      Viewing all 316 articles
                      Browse latest View live