نظرات اشتراک‌ها
نکاتی که باید جهت بررسی یکسان بودن دو URL بررسی کرد
موارد مهم این نکات رو اگر تبدیل به یک متد کمکی کنیم، کلاس زیر بدست خواهد آمد:
using System;
using System.Web;

namespace UrlNormalizationTest
{
    public static class UrlNormalization
    {
        public static bool AreTheSameUrls(this string url1, string url2)
        {
            url1 = url1.NormalizeUrl();
            url2 = url2.NormalizeUrl();
            return url1.Equals(url2);
        }

        public static bool AreTheSameUrls(this Uri uri1, Uri uri2)
        {
            var url1 = uri1.NormalizeUrl();
            var url2 = uri2.NormalizeUrl();
            return url1.Equals(url2);
        }

        public static string[] DefaultDirectoryIndexes = new[]
            {
                "default.asp",
                "default.aspx",
                "index.htm",
                "index.html",
                "index.php"
            };

        public static string NormalizeUrl(this Uri uri)
        {
            var url = urlToLower(uri);
            url = limitProtocols(url);
            url = removeDefaultDirectoryIndexes(url);
            url = removeTheFragment(url);
            url = removeDuplicateSlashes(url);
            url = addWww(url);
            url = removeFeedburnerPart(url);
            return removeTrailingSlashAndEmptyQuery(url);
        }

        public static string NormalizeUrl(this string url)
        {
            return NormalizeUrl(new Uri(url));
        }

        private static string removeFeedburnerPart(string url)
        {
            var idx = url.IndexOf("utm_source=", StringComparison.Ordinal);
            return idx == -1 ? url : url.Substring(0, idx - 1);
        }

        private static string addWww(string url)
        {
            if (new Uri(url).Host.Split('.').Length == 2 && !url.Contains("://www."))
            {
                return url.Replace("://", "://www.");
            }
            return url;
        }

        private static string removeDuplicateSlashes(string url)
        {
            var path = new Uri(url).AbsolutePath;
            return path.Contains("//") ? url.Replace(path, path.Replace("//", "/")) : url;
        }

        private static string limitProtocols(string url)
        {
            return new Uri(url).Scheme == "https" ? url.Replace("https://", "http://") : url;
        }

        private static string removeTheFragment(string url)
        {
            var fragment = new Uri(url).Fragment;
            return string.IsNullOrWhiteSpace(fragment) ? url : url.Replace(fragment, string.Empty);
        }

        private static string urlToLower(Uri uri)
        {
            return HttpUtility.UrlDecode(uri.AbsoluteUri.ToLowerInvariant());
        }

        private static string removeTrailingSlashAndEmptyQuery(string url)
        {
            return url
                    .TrimEnd(new[] { '?' })
                    .TrimEnd(new[] { '/' });
        }

        private static string removeDefaultDirectoryIndexes(string url)
        {
            foreach (var index in DefaultDirectoryIndexes)
            {
                if (url.EndsWith(index))
                {
                    url = url.TrimEnd(index.ToCharArray());
                    break;
                }
            }
            return url;
        }
    }
}
با این تست‌ها جهت بررسی آن:
using NUnit.Framework;
using UrlNormalizationTest;

namespace UrlNormalization.Tests
{
    [TestFixture]
    public class UnitTests
    {
        [Test]
        public void Test1ConvertingTheSchemeAndHostToLowercase()
        {
            var url1 = "HTTP://www.Example.com/".NormalizeUrl();
            var url2 = "http://www.example.com/".NormalizeUrl();

            Assert.AreEqual(url1, url2);
        }

        [Test]
        public void Test2CapitalizingLettersInEscapeSequences()
        {
            var url1 = "http://www.example.com/a%c2%b1b".NormalizeUrl();
            var url2 = "http://www.example.com/a%C2%B1b".NormalizeUrl();

            Assert.AreEqual(url1, url2);
        }

        [Test]
        public void Test3DecodingPercentEncodedOctetsOfUnreservedCharacters()
        {
            var url1 = "http://www.example.com/%7Eusername/".NormalizeUrl();
            var url2 = "http://www.example.com/~username/".NormalizeUrl();

            Assert.AreEqual(url1, url2);
        }

        [Test]
        public void Test4RemovingTheDefaultPort()
        {
            var url1 = "http://www.example.com:80/bar.html".NormalizeUrl();
            var url2 = "http://www.example.com/bar.html".NormalizeUrl();

            Assert.AreEqual(url1, url2);
        }

        [Test]
        public void Test5AddingTrailing()
        {
            var url1 = "http://www.example.com/alice".NormalizeUrl();
            var url2 = "http://www.example.com/alice/?".NormalizeUrl();

            Assert.AreEqual(url1, url2);
        }

        [Test]
        public void Test6RemovingDotSegments()
        {
            var url1 = "http://www.example.com/../a/b/../c/./d.html".NormalizeUrl();
            var url2 = "http://www.example.com/a/c/d.html".NormalizeUrl();

            Assert.AreEqual(url1, url2);
        }

        [Test]
        public void Test7RemovingDirectoryIndex1()
        {
            var url1 = "http://www.example.com/default.asp".NormalizeUrl();
            var url2 = "http://www.example.com/".NormalizeUrl();

            Assert.AreEqual(url1, url2);
        }

        [Test]
        public void Test7RemovingDirectoryIndex2()
        {
            var url1 = "http://www.example.com/default.asp?id=1".NormalizeUrl();
            var url2 = "http://www.example.com/default.asp?id=1".NormalizeUrl();

            Assert.AreEqual(url1, url2);
        }

        [Test]
        public void Test7RemovingDirectoryIndex3()
        {
            var url1 = "http://www.example.com/a/index.html".NormalizeUrl();
            var url2 = "http://www.example.com/a/".NormalizeUrl();

            Assert.AreEqual(url1, url2);
        }

        [Test]
        public void Test8RemovingTheFragment()
        {
            var url1 = "http://www.example.com/bar.html#section1".NormalizeUrl();
            var url2 = "http://www.example.com/bar.html".NormalizeUrl();

            Assert.AreEqual(url1, url2);
        }

        [Test]
        public void Test9LimitingProtocols()
        {
            var url1 = "https://www.example.com/".NormalizeUrl();
            var url2 = "http://www.example.com/".NormalizeUrl();

            Assert.AreEqual(url1, url2);
        }

        [Test]
        public void Test10RemovingDuplicateSlashes()
        {
            var url1 = "http://www.example.com/foo//bar.html".NormalizeUrl();
            var url2 = "http://www.example.com/foo/bar.html".NormalizeUrl();

            Assert.AreEqual(url1, url2);
        }

        [Test]
        public void Test11AddWww()
        {
            var url1 = "http://example.com/".NormalizeUrl();
            var url2 = "http://www.example.com".NormalizeUrl();

            Assert.AreEqual(url1, url2);
        }

        [Test]
        public void Test12RemoveFeedburnerPart()
        {
            var url1 = "http://site.net/2013/02/firefox-19-released/?utm_source=rss&utm_medium=rss&utm_campaign=firefox-19-released".NormalizeUrl();
            var url2 = "http://site.net/2013/02/firefox-19-released".NormalizeUrl();

            Assert.AreEqual(url1, url2);
        }
    }
}
نظرات مطالب
ASP.NET MVC #21
با درود؛ من با استفاده از متد jQuery.Ajax  و درخواست از یک کنترلر برای نمایش اطلاعات از دیتابیس به روش زیر عمل کردم
     <script type="text/javascript">
 
        $(function () {
            getData();
        });
 
        function getData() {
        var $tbl = $('#tblEmployee');
            $.ajax({
            url: 'Home/EmployeeInfoData',
            type: 'Post',
                datatype: 'json',
                success: function (data) {
                    if (data.length > 0) {
                        $tbl.empty();
                        $tbl.append(' <tr><th>ID</th><th>Name</th><th>Family</th></tr>');
                        var rows = [];
                        for (var i = 0; i < data.length; i++) {
                            rows.push(' <tr><td>' + data[i].Id + '</td><td>' + data[i].Name + '</td><td>' + data[i].Family + '</td></tr>');
                        }
                        $tbl.append(rows.join(''));
                    }
                }
            });
        }
    </script>

و کنترلر مربوط
[HttpPost]        
        public ActionResult EmployeeInfoData()
        {

 InfoEmployee mp = new InfoEmployee();
             var names = mp.GetData();
         return Json(names);
        }
و سوال اینکه وقتی از Return View استفاده کردم هیچ رکوردی بازگردانده نشد و با یک صفحه سفید مواجه شدم و باید حتما از Return Json استفاده کنم تا اطلاعات درخواستی نمایش داده بشه؟ آیا حتما باید از Return Json استفاده کرد ؟ و یا در کد نویسی من جایی اشکال هست ؟
بازخوردهای دوره
مدیریت نگاشت ConnectionIdها در SignalR به کاربران واقعی سیستم
واقعا سپاسگذارم . ولی مشکلم و حل نمیکنه این موارد.
یکم گیج و سردرگم شدم.
ببینید من یه هاب ایجاد کردم به شکل زیر
 public class User
    {
        public string UserName { get; set; }
        public bool IsRole { get; set; }
        public HashSet<string> ConnectionIds { get; set; }
    }

    [Authorize]
    [HubName("userActivityHub")]
    public class UserActivityHub : Hub
    {
        private static readonly ConcurrentDictionary<string, User> Users = new ConcurrentDictionary<string, User>();
     

        public void AdminJoin()
        {
            Groups.Add(Context.ConnectionId, "admins");
        }

        public void Join()
        {
            var userName = Context.User.Identity.Name;
            var connectionId = Context.ConnectionId;
            var isAdmin = Context.User.IsInRole("Admin");

            var user = Users.GetOrAdd(userName, _ => new User
            {
                UserName = userName,
                IsRole = isAdmin,
                ConnectionIds = new HashSet<string>()
            });

            if (user.IsRole == true)
            {
                Groups.Add(user.ConnectionIds.ToString(), "admins");
            }
            else
            {
                lock (user.ConnectionIds)
                {
                    user.ConnectionIds.Add(connectionId);
                }
                Clients.Group("admins").showUserCount(Users.Count(a => a.Value.IsRole != true));
            }

        }

        public void GetUserCount()
        {
            Clients.Group("admins").showUserCount(Users.Count(a => a.Value.IsRole != true));
        }

        public override System.Threading.Tasks.Task OnDisconnected(bool stopCalled)
        {
            if (stopCalled)
            {
                var userName = Context.User.Identity.Name;
                var connectionId = Context.ConnectionId;

                User user;
                Users.TryGetValue(userName, out user);
                if (user != null)
                {
                    lock (user.ConnectionIds)
                    {
                        user.ConnectionIds.RemoveWhere(cid => cid.Equals(connectionId));
                        if (!user.ConnectionIds.Any())
                        {
                            User removeUser;
                            Users.TryRemove(userName, out removeUser);
                        }
                    }
                }
                return Clients.Group("admins").showUserCount(Users.Count(a => a.Value.IsRole != true));
            }
            else
            {
                return base.OnDisconnected(false);
            }
        }
    }

اینک کد ویو Index
 <script type="text/javascript">
        var userHub = $.connection.userActivityHub;
        $.connection.hub.logging = true;
        $.connection.hub.start().done(function() {
            userHub.server.join();
        });

        $(function() {
            window.onbeforeunload = function() {
                $.connection.hub.stop();
            };
        });
    </script>

اینم ویو Index مربوط به Area Admin 
    <script type="text/javascript">
        var userHub = $.connection.userActivityHub;
        userHub.client.showUserCount = function (message) {
            $('#userOnlineCount').html(message);
        };
        $.connection.hub.start().done(function() {
            userHub.server.adminJoin().done(function() {
                userHub.server.getUserCount();
            });
        });
    </script>
وقتی کاربر لاگین میکنه یه کانتر تو ویو ادمین دارم که بهش اضافه میشه و برای لاگین کاربران درست کار میکنه.
مشکلم اینه که وقتی کاربر دکمه خروج و میزنه یا مرورگر رو میبنده رویداد OnDisconnected اجرا نمیشه! در نتیجه کانتر موجود در ویو ادمین هم کم نمیشه.
اینم کد دکمه خروج که البته تو یه پارشال ویو وجود داره
<script>
    $('a.btn.btn-danger.btn-block').click(function(e) {
        e.preventDefault();
        $('#logoutForm').submit();
        $.connection.userActivityHub.connection.stop();
    });
    $(function() {
        window.onbeforeunload = function(e) {
            $.connection.hub.stop();
        };
    });

</script>
نمیدونم مشکل کجاست! آیا باید برای دکمه خروج هم یه متد تو هاب ایجاد کنم یا همون Ondisconnected کافیه!
واقعا ممنون از زحمات شما.
مطالب
عمومی سازی الگوریتم‌ها با استفاده از Reflection
در این مقاله قصد داریم عملیات Reflection را بیشتر در انجام ساده‌تر عملیات ببینیم. عملیاتی که به همراه کار اضافه، تکراری و خسته کننده است و با استفاده از Reflection این کارها حذف شده و تعداد خطوط هم پایین می‌آید. حتی گاها ممکن است موجب استفاده‌ی مجدد از کدها شود که همگی این عوامل موجب بالا رفتن امتیاز Refactoring می‌شوند.
در مثال‌های زیر مجموعه‌ای از Reflection‌های ساده و کاملا کاربردی است که من با آن‌ها روبرو شده ام.


کوتاه سازی کدهای نمایش یک View در ASP.NET MVC با Reflection 

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

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

ساختار اطلاعاتی تصویر فوق به شرح زیر است:
 <div>
                              <div>
                                  <div>
                                      <p><span>First Name </span>: Jonathan</p>
                                  </div>
   </div>
                          </div>
که به دو فایل پارشال تقسیم شده است Bio_ و BioRow_ که محتویات هر پارشال هم به شرح زیر است:

BioRow_
@model System.Web.UI.WebControls.ListItem


<div>
    <p><span>@Model.Text </span>: @Model.Value</p>
</div>
در پارشال بالا ورودی از نوع listItem است که یک متن دارد و یک مقدار.(شاید به نظر شبیه حالت جفت کلید و مقدار باشد ولی در این کلاس خبری از کلید نیست).
پارشال پایینی هم دربرگیرنده‌ی پارشال بالاست که قرار است چندین و چند بار پارشال بالا در خودش نمایش دهد.
Bio_
@using System.Web.UI.WebControls
@using ZekrWepApp.Filters
@model ZekrModel.Admin

    <div>
        <h1>Bio Graph</h1>
        <div>
            
            @{
                ListItemCollection collection = GetCustomProperties.Get(Model,exclude:new string[]{"Poems","Id"});
                foreach (var item in collection)
                {
                    Html.RenderPartial(MVC.Shared.Views._BioRow, item);
                }
            }

                    </div>
    </div>
پارشال بالا یک مدل از کلاس Admin را می‌پذیرد که قرار است اطلاعات شخصی مدیر را نمایش دهد. در ابتدا متدی از یک کلاس ایستا وجود دارد که کدهای Reflection درون آن قرار دارند که یک مجموعه از ListItem‌ها را بر می‌گرداند و سپس با یک حلقه، پارشال BioRow_ را صدا می‌زند.

کد درون این کلاس ایستا را بررسی می‌کنیم؛ این کلاس دو متد دارد یکی عمومی و دیگری خصوصی است:
  public class GetCustomProperties
    {
        private static PropertyInfo[] getObjectsInfos(object obj,string[] inclue,string[] exclude )
        {
            var list = obj.GetType().GetProperties();

            PropertyInfo[] outputPropertyInfos = null;

            if (inclue != null)
            {           
                return list.Where(propertyInfo => inclue.Contains(propertyInfo.Name)).ToArray();
            }
            if (exclude != null)
            {
                return list.Where(propertyInfo => !exclude.Contains(propertyInfo.Name)).ToArray();
            }
            return list;
        }
    }
کد بالا که یک کد خصوصی است، سه پارامتر را می‌پذیرد. اولی مدل یا کلاسی است که به آن پاس کرده‌ایم. دو پارامتر بعدی اختیاری است و در کد پارشال بالا Exclude را تعریف کرده ایم و تنهای یکی از دو پارامتر بالا هم باید مورد استفاده قرار بگیرند و Include ارجحیت دارد. وظیفه‌ی این دو پارمتر این است که آرایه ای از رشته‌ها را دریافت می‌کنند که نام پراپرتی‌ها در آن‌ها ذکر شده است. آرایه Include می‌گوید که فقط این پراپرتی‌ها را برگردان ولی اگر دوست دارید همه‌ی پارامترها را نمایش دهید و تنها یکی یا چندتا از آن‌ها را حذف کنید، از آرایه Exclude استفاده کنید. در صورتی که این دو آرایه خالی باشند، همه‌ی پراپرتی‌ها بازگشت داده می‌شوند و در صورتی که یکی از آن‌ها وارد شده باشد، طبق دستورات Linq بالا بررسی می‌کند که (Include) آیا اسامی مشترکی بین آن‌ها وجود دارد یا خیر؟ اگر وجود دارد آن را در لیست قرار داده و بر می‌گرداند و در حالت Exclude این مقایسه به صورت برعکس انجام می‌گیرد و باید لیستی برگردانده شود که اسامی، نکته مشترکی نداشته باشند.

متد عمومی که در این کلاس قرار دارد به شرح زیر است:
 public static ListItemCollection Get(object obj,string[] inclue=null,string[] exclude=null)
        {
            var propertyInfos = getObjectsInfos(obj, inclue, exclude);
            if (propertyInfos == null) throw new ArgumentNullException("propertyInfos is null");

            var collection = new ListItemCollection();

            foreach (PropertyInfo propertyInfo in propertyInfos)
            {


                string name = propertyInfo.Name;

                foreach (Attribute attribute in propertyInfo.GetCustomAttributes(true))
                {
                    DisplayAttribute displayAttribute = attribute as DisplayAttribute;

                    if (displayAttribute != null)
                    {
                        name = displayAttribute.Name;
                        break;
                    }                  
                }

                string value = "";
                object objvalue = propertyInfo.GetValue(obj);
                if (objvalue != null) value = objvalue.ToString();

                collection.Add(new ListItem(name,value));
            }
            return collection;
        }
این متد سه پارامتر را از کاربر دریافت و به سمت متد خصوصی ارسال می‌کند. موقعی‌که پراپرتی‌ها بازگشت داده می‌شوند، دو قسمت آن مهم است؛ یکی عنوان پراپرتی و دیگری مقدار پراپرتی. از آن جا که نام پراپرتی‌ها طبق سلیقه‌ی برنامه نویس و با حروف انگلیسی نوشته می‌شوند، در صورتی که برنامه نویس از متادیتای Display در مدل بهره برده باشد، به جای نام پراپرتی مقداری را که به متادیتای Display داده‌ایم، بر می‌گردانیم.

کد بالا پراپرتی‌ها را دریافت و یک به یک متادیتاهای آن را بررسی کرده و در صورتی که از متادیتای Display استفاده کرده باشند، مقدار آن را جایگزین نام پراپرتی خواهد کرد. در مورد مقدار هم از آنجا که اگر پراپرتی با Null پر شده باشد، تبدیل به رشته‌ای با پیام خطای روبرو خواهد شد. در نتیجه بهتر است یک شرط احتیاط هم روی آن پیاده شود. در آخر هم از متن و مقدار، یک آیتم ساخته و درون Collection اضافه می‌کنیم و بعد از اینکه همه پراپرتی‌ها بررسی شدند، Collection را بر می‌گردانیم.
 [Display(Name = "نام کاربری")]
public string UserName { get; set; }

کد کامل کلاس:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Web;
using System.Web.Mvc.Html;
using System.Web.UI.WebControls;
using Links;

namespace ZekrWepApp.Filters
{
    
    public class GetCustomProperties
    {
        public static ListItemCollection Get(object obj,string[] inclue=null,string[] exclude=null)
        {
            var propertyInfos = getObjectsInfos(obj, inclue, exclude);
            if (propertyInfos == null) throw new ArgumentNullException("propertyInfos is null");

            var collection = new ListItemCollection();

            foreach (PropertyInfo propertyInfo in propertyInfos)
            {
                string name = propertyInfo.Name;
                foreach (Attribute attribute in propertyInfo.GetCustomAttributes(true))
                {
                    DisplayAttribute displayAttribute = attribute as DisplayAttribute;

                    if (displayAttribute != null)
                    {
                        name = displayAttribute.Name;
                        break;
                    }                  
                }

                string value = "";
                object objvalue = propertyInfo.GetValue(obj);
                if (objvalue != null) value = objvalue.ToString();

                collection.Add(new ListItem(name,value));
            }
            return collection;
        }
        private static PropertyInfo[] getObjectsInfos(object obj,string[] include,string[] exclude )
        {
            var list = obj.GetType().GetProperties();

            PropertyInfo[] outputPropertyInfos = null;

            if (include != null)
            {           
                return list.Where(propertyInfo => include.Contains(propertyInfo.Name)).ToArray();
            }
            if (exclude != null)
            {
                return list.Where(propertyInfo => !exclude.Contains(propertyInfo.Name)).ToArray();
            }
            return list;
        }  
    }   
}

لیستی از پارامترها با Reflection


مورد بعدی که ساده‌تر بوده و از کد بالا مختصرتر هم هست، این است که قرار بود برای یک درگاه، یک سری اطلاعات را با متد Post ارسال کنم که نحوه‌ی ارسال اطلاعات به شکل زیر بود:
amount=1000&orderId=452&Pid=xxx&....
کد زیر را من جهت ساخت قالب‌های این چنینی استفاده می‌کنم:

using System;
using System.Collections.Generic;
using System.Linq;

namespace Utils
{
    public class QueryStringParametersList
    {
        private string Symbol = "&";
        private List<KeyValuePair<string, string>> list { get; set; }

        public QueryStringParametersList()
        {
            list = new List<KeyValuePair<string, string>>();
        }
        public QueryStringParametersList(string symbol)
        {
            Symbol = symbol;
           list = new List<KeyValuePair<string, string>>(); 
        }

        public int Size
        {
            get { return list.Count; }
        }
        public void Add(string key, string value)
        {
            list.Add(new KeyValuePair<string, string>(key, value));
        }

        public string GetQueryStringPostfix()
        {
            return string.Join(Symbol, list.Select(p => Uri.EscapeDataString(p.Key) + "=" + Uri.EscapeDataString(p.Value)));
        }
    }
}



یک متغیر به نام symbol دارد و در صورتی در شرایط متفاوت، قصد چسپاندن چیزی را به یکدیگر با علامتی خاص داشته باشید، این تابع می‌تواند کاربرد داشته باشد. این متد از یک لیست کلید و مقدار استفاده کرده و پارامترهایی را که به آن پاس می‌شود، نگهداری و سپس توسط متد GetQueryStringPostfix آن‌ها را با یکدیگر الحاق کرده و در قالب یک رشته بر می‌گرداند.
کاربرد Reflection در اینجا این است که من باید دوبار به شکل زیر، دو نوع اطلاعات متفاوت را پست کنم. یکی موقع ارسال به درگاه و دیگری موقع بازگشت از درگاه.
QueryStringParametersList queryparamsList = new QueryStringParametersList();

            ueryparamsList.Add("consumer_key", requestPayment.Consumer_Key);
            queryparamsList.Add("amount", requestPayment.Amount.ToString());
            queryparamsList.Add("callback", requestPayment.Callback);
            queryparamsList.Add("description", requestPayment.Description);
            queryparamsList.Add("email", requestPayment.Email);
            queryparamsList.Add("mobile", requestPayment.Mobile);
            queryparamsList.Add("name", requestPayment.Name);
            queryparamsList.Add("irderid", requestPayment.OrderId.ToString());

ولی با استفاده از کد Reflection که در بالاتر عنوان شد، باید نام و مقدار پراپرتی را گرفته و در یک حلقه آن‌ها را اضافه کنیم، بدین شکل:
   private QueryStringParametersList ReadParams(object obj)
        {
            PropertyInfo[] propertyInfos = obj.GetType().GetProperties();

            QueryStringParametersList queryparamsList = new QueryStringParametersList();
            for (int i = 0; i < propertyInfos.Count(); i++)
            {
                queryparamsList.Add(propertyInfos[i].Name.ToLower(),propertyInfos[i].GetValue(obj).ToString() );              
            }
            return queryparamsList;
        }
در کد بالا هر بار پراپرتی‌های کلاس را خوانده و نام و مقدار آن‌ها را گرفته و به کلاس QueryString اضافه می‌کنیم. پارامتر ورودی این متد به این خاطر object در نظر گرفته شده است که تا هر کلاسی را بتوانیم به آن پاس کنیم که خودم در همین کلاس درگاه، دو کلاس را به آن پاس کردم.
مطالب دوره‌ها
مثال - نمایش بلادرنگ تعداد کاربران آنلاین توسط SignalR
راه حل‌های زیادی برای محاسبه و نمایش تعداد کاربران آنلاین یک برنامه وب وجود دارند و عموما مبتنی بر کار با متغیرهای سشن یا Application و امثال آن هستند. این روش‌ها عموما دقیق نبوده و خصوصا قسمت قطع اتصال کاربر را نمی‌توانند دقیقا تشخیص دهند. به همین جهت نیاز به یک تایمر دارند که مثلا اگر در 5 دقیقه قبل، کاربری درخواست مشاهده آدرسی را به سرور ارسال نکرده بود، از لیست کاربران آنلاین حذف شود.
در ادامه بجای این روش‌ها، از SignalR برای محاسبه تعداد کاربران آنلاین و همچنین به روز رسانی بلادرنگ این عدد در سمت کاربر، استفاده خواهیم کرد.

تشخیص اتصال و قطع اتصال کاربران در SignalR

زیر ساخت‌های کلاس Hub موجود در SignalR، دارای متدهای ردیابی اتصال (OnConnected)، قطع اتصال (OnDisconnected) و یا برقراری مجدد اتصال کاربران (OnReconnected) هستند. با بازنویسی این متدها می‌توان به تخمین بسیار دقیقی از تعداد کاربران آنلاین یک سایت رسید.


پیشنیازهای بحث
پیشنیازهای این بحث با مطلب «مثال - نمایش درصد پیشرفت عملیات توسط SignalR» یکی است. برای مثال نحوه دریافت وابستگی‌ها، تنظیمات فایل global.asax و افزودن اسکریپت‌ها، تفاوتی با مثال یاد شده ندارند.


تعریف هاب کاربران آنلاین برنامه

using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.SignalR;

namespace SignalR05.Common
{
    public class OnlineUsersHub : Hub
    {
        public static readonly ConcurrentDictionary<string, string> OnlineUsers = new ConcurrentDictionary<string, string>();

        public void UpdateUsersOnlineCount()
        {
            // آی پی معرف یک کاربر است
            // اما کانکشن آی دی معرف یک برگه جدید در مرورگر او است
            // هر کاربر می‌تواند چندین برگه را به یک سایت گشوده یا ببندد
            var ipsCount = OnlineUsers.Select(x => x.Value).Distinct().Count();
            this.Clients.All.updateUsersOnlineCount(ipsCount);
        }

        /// <summary>
        /// اگر کاربران اعتبار سنجی شده‌اند بهتر است از
        /// this.Context.User.Identity.Name
        /// بجای آی پی استفاده شود
        /// </summary>        
        protected string GetUserIpAddress()
        {
            object environment;
            if (!Context.Request.Items.TryGetValue("owin.environment", out environment))
                return null;

            object serverRemoteIpAddress;
            if (!((IDictionary<string, object>)environment).TryGetValue("server.RemoteIpAddress", out serverRemoteIpAddress))
                return null;

            return serverRemoteIpAddress.ToString();
        }

        public override Task OnConnected()
        {
            var ip = GetUserIpAddress();
            OnlineUsers.TryAdd(this.Context.ConnectionId, ip);
            UpdateUsersOnlineCount();

            return base.OnConnected();
        }

        public override Task OnReconnected()
        {
            var ip = GetUserIpAddress();
            OnlineUsers.TryAdd(this.Context.ConnectionId, ip);
            UpdateUsersOnlineCount();

            return base.OnReconnected();
        }

        public override Task OnDisconnected()
        {
            // در این حالت ممکن است مرورگر کاملا بسته شده باشد
            // یا حتی صرفا یک برگه مرورگر از چندین برگه متصل به سایت بسته شده باشند
            string ip;
            OnlineUsers.TryRemove(this.Context.ConnectionId, out ip);
            UpdateUsersOnlineCount();

            return base.OnDisconnected();
        }
    }
}
کدهای کامل هاب شمارش کاربران آنلاین را در اینجا ملاحظه می‌کنید؛ به همراه نکته‌ی نحوه‌ی دریافت IP کاربر متصل شده به سایت، در یک هاب. کار افزودن یا حذف این کاربران به ConcurrentDictionary تعریف شده، در روال‌های بازنویسی شده اتصال، قطع اتصال و اتصال مجدد یک کاربر، انجام شده است.
در اینجا، هم به IP کاربر و هم به ConnectionId او نیاز است. از این جهت که هر ConnectionId، معرف یک برگه جدید باز شده در مرورگر کاربر است. اگر صرفا IPها را پردازش کنیم، با بسته شدن یکی از چندین برگه مرورگر او که اکنون به سایت متصل هستند، آمار او را از دست خواهیم داد. این کاربر هنوز چندین برگه باز دیگر را دارد که با سایت در ارتباط هستند، اما چون IP او را از لیست حذف کرده‌ایم (در نتیجه بسته شدن یکی از برگه‌ها)، آمار کلی شخص را نیز از دست خواهیم داد. بنابراین هر دوی IP و ConnectionIdها باید پردازش شوند.
اگر برنامه شما دارای اعتبارسنجی است (یک صفحه لاگین دارد)، بهتر است بجای IP از this.Context.User.Identity.Name استفاده کنید.


کدهای سمت کلاینت نمایش آمار کاربران

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
    <script src="Scripts/jquery-1.6.4.min.js" type="text/javascript"></script>
    <script src="Scripts/jquery.signalR-1.1.3.min.js" type="text/javascript"></script>
    <script type="text/javascript" src='<%= ResolveClientUrl("~/signalr/hubs") %>'></script>
</head>
<body>
    <form id="form1" runat="server">
    online users count: <span id="usersCount"></span>
    </form>
    <script type="text/javascript">
        $(function () {
            $.connection.hub.logging = true;
            var onlineUsersHub = $.connection.onlineUsersHub;
            onlineUsersHub.client.updateUsersOnlineCount = function (count) {
                $('#usersCount').text(count);
            };
            $.connection.hub.start();
        });
    </script>
</body>
</html>
با توجه به اینکه در هاب تعریف شده، متد پویای updateUsersOnlineCount، آمار تعداد کاربران متصل را (تعداد آی پی‌های منحصربفرد متصل را) به کلاینت‌ها ارسال می‌کند، بنابراین در سمت کلاینت نیز با تعریف callback ایی به همین نام، می‌توان این آمار دریافتی را به کاربران سایت نمایش داد. آماری که به صورت خودکار با کم و زیاد شدن کاربران به روز شده و نیازی نیست کاربر به صورت دستی، صفحه را به روز کند.


کدهای کامل این مثال را از اینجا نیز می‌توانید دریافت کنید:
SignalR05.zip
 
مطالب
آشنایی با ساختار IIS قسمت هشتم
پس از بررسی مفاهیم، بهتر هست وارد یک کار عملی شویم. مثال مورد نظر، یک مثال از وب سایت شرکت مایکروسافت است که هنگام نمایش تصاویر، بر حسب پیکربندی موجود، یک پرچسب یا تگی را در گوشه‌ای از تصویر درج می‌کند. البته تصویر را ذخیره نمی‌کنیم و تگ را بر روی تصویر اصلی قرار نمی‌دهیم. تنها هنگام نمایش به کاربر، روی response خروجی آن را درج می‌کنیم.
قبلا ما در این مقاله به بررسی httpandler پرداخته‌ایم، ولی بهتر هست در این مثال کمی حالت پیشرفته‌تر آن‌را بررسی کنیم.
ابتدا اجازه دهید کمی قابلیت‌های فایل کانفیگ IIS را گسترش دهیم.
مسیر زیر را باز کنید:
%windir%\system32\inetsrv\config\schema
یک فایل xml را با نام  imagecopyright.xml ساخته و تگ‌های زیر را داخلش قرار دهید:
احتمال زیاد دسترسی برای ویرایش این دایرکتوری به خاطر مراتب امنیتی با مشکل برخواهید خورد برای ویرایش این نکته امنیتی از اینجا یا به خصوص از اینجا  کمک بگیرید.
<configSchema> 
 
     <sectionSchema name="system.webServer/imageCopyright">  
         <attribute name="enabled" type="bool" defaultValue="false" />  
         <attribute name="message" type="string" defaultValue="Your Copyright Message" /> 
        <attribute name="color" type="string" defaultValue="Red"/> 
   </sectionSchema>
 </configSchema>
با این کار ما یک شِما یا اسکیما را ایجاد کردیم که دارای سه خصوصیت زیر است:
  • enabled: آیا این هندلر فعال باشد یا خیر.
  • message: پیامی که باید به عنوان تگ درج شود.
  • color: رنگ متن که به طور پیش فرض قرمز رنگ است.
به هر کدام از تگ‌های بالا یک مقدار پیش فرض داده ایم تا اگر مقداردهی نشدند، ماژول طبق مقادیر پیش فرض کار خود را انجام هد.
بعد از نوشتن شما، لازم هست که آن را در فایل applicationhost.config نیز به عنوان یک section جدید در زیر مجموعه system.webserver معرفی کنیم:
<configSections> 

...
   <sectionGroup name="system.webServer">  
        <section name="imageCopyright"  overrideModeDefault="Allow"/> 
...    
   </sectionGroup>
</configSections>
تعریف کد بالا به شما اجازه میدهد تا در زیر مجموعه تگ system.webserver، برای هندلر خود تگ تعریف کنید. در کد بالا، شمای خود را بر اساس نام فایل مشخص می‌کنیم و خصوصیت overrideModeDefault، یک قفل گذار امنیتی برای تغییر محتواست. در صورتی که allow باشد هر کسی در هر مرحله‌ی دسترسی در سیستم و در هر فضای نامی، در فایل‌های وب کانفیگ می‌تواند به مقادیر این section دسترسی یافته و آن‌ها را تغییر دهد. ولی اگر با Deny مقدادهی شده باشد، مقادیر قفل شده و هیچ دسترسی برای تغییر آن‌ها وجود ندارد.
در مثال زیر ما به ماژول windows Authentication اجازه می‌دهیم که هر کاربری در هر سطح دسترسی به این section دسترسی داشته باشد؛ از تمامی سایت‌ها یا اپلیکشین‌ها یا virtual directories موجود در سیستم و در بعضی موارد این گزینه باعث افزایش ریسک امنیتی می‌گردد.
<section name="windowsAuthentication" overrideModeDefault="Allow" />
در کد زیر اینبار ما دسترسی را بستیم و در تعاریف دامنه‌های دسترسی، دسترسی را فقط برای سطح مدیریت سایت AdministratorSite باز گذاشته‌ایم:
 <location path="AdministratorSite" overrideMode="Allow">  
   <security> 
            <authentication> 
                     <providers>  
                <windowsAuthentication enabled="false"> 
                     </providers> 
                        <add value="Negotiate" /> 
                        <add value="NTLM" /> 
 </location> 
                </windowsAuthentication> 
            </authentication> 
    </security>
برای خارج نشدن بیش از اندازه از بحث، به ادامه تعریف هندلر  می‌پردازیم. بعد از معرفی یک section برای هندلر خود، میتوانیم به راحتی تگ آن را در قسمت system.webserver تعریف کنیم. این کار می‌تواند از طریق فایل web.config سایت یا applicationhost.config صورت بگیرد یا میتواند از طریق ویرایش دستی یا خط فرمان appcmd معرفی شود؛ ولی در کل باید به صورت زیر تعریف شود:
 <system.webServer>  
     <imageCopyright /> 
 </system.webServer>
در کد بالا این تگ تنها معرفی شده است؛ ولی مقادیر آن پیش فرض می‌باشند. در صورتی که بخواهید مقادیر آن را تغییر دهید کد به شکل زیر تغییر می‌کند:
 <system.webServer>   
 <imageCopyright enabled="true" message="an example of www.dotnettips.info" color="Blue" />  
 </system.webServer>
در صورتی که میخواهید از خط فرمان کمک بگیرید به این شکل بنویسید:
%windir%\system32\inetsrv\appcmd set config -section:system.webServer/imageCopyright /color:yellow /message:"Dotnettips.info" /enabled:true
برای اطمینان از این که دستور شما اجرا شده است یا خیر، یک کوئری یا لیست از تگ مورد نظر در system.webserver بگیرید:
%windir%\system32\inetsrv\appcmd list config -section:system.webServer/imageCopyright
در این مرحله یک دایرکتوری برای پروژه تصاویر ایجاد کنید و در این مثال ما فقط تصاویر jpg را ذخیره می‌کنیم و در هنگام درج تگ، تصاویر jpg را هندل می‌کنیم؛ برای مثال ما:
c:\inetpub\mypictures
در این مرحله دایرکتوری ایجاد شده را به عنوان یک application معرفی می‌کنیم:
%windir%\system32\inetsrv\appcmd add app -site.name:"Default Web Site" -path:/mypictures -physicalPath:%systemdrive%\inetpub\mypictures
و برای آن ماژول DirectoryBrowse را فعال می‌کنیم. برای اطلاعات بیشتر به مقاله قبلی که به تشریح وظایف ماژول‌ها پرداختیم رجوع کنید. فقط به این نکته اشاره کنم که اگر کاربر آدرس localhost/mypictures را درخواست کند، فایل‌های این قسمت را برای ما لیست می‌کند. برای فعال سازی، کد زیر را فعال می‌کنیم:
%windir%\system32\inetsrv\appcmd set config "Default Web Site/mypictures"  -section:directoryBrowse -enabled:true
حال زمان این رسیده است تا کد نوشته و فایل cs آن را در مسیر زیر ذخیره کنیم:
c:\inetpub\mypictures\App_Code\imagecopyrighthandler.cs
هندل مورد نظر در زبان سی شارپ :
#region Using directives
using System;
using System.Web;
using System.Drawing;
using System.Drawing.Imaging;
using Microsoft.Web.Administration;
#endregion
  
namespace IIS7Demos
{
    public class imageCopyrightHandler : IHttpHandler
    {
        public void ProcessRequest(HttpContext context)
        {
            ConfigurationSection imageCopyrightHandlerSection = 
                WebConfigurationManager.GetSection("system.webServer/imageCopyright");
  
            HandleImage(    context,
                            (bool)imageCopyrightHandlerSection.Attributes["enabled"].Value,
                            (string)imageCopyrightHandlerSection.Attributes["message"].Value,
                            (string)imageCopyrightHandlerSection.Attributes["color"].Value                            
                        );
        }
  
        void HandleImage(   HttpContext context,
                            bool enabled,
                            string copyrightText,
                            string color
                        )           
        {
            try
            {
                string strPath = context.Request.PhysicalPath;
                if (enabled)
                {
                    Bitmap bitmap = new Bitmap(strPath);
                    // add copyright message
                    Graphics g = Graphics.FromImage(bitmap);
                    Font f = new Font("Arial", 50, GraphicsUnit.Pixel);
                    SolidBrush sb = new SolidBrush(Color.FromName(color));
                    g.DrawString(   copyrightText,
                                    f,
                                    sb,
                                    5,
                                    bitmap.Height - f.Height - 5
                                );
                    f.Dispose();
                    g.Dispose();
                    // slow, but good looking resize for large images
                    context.Response.ContentType = "image/jpeg";
                    bitmap.Save(
                                        context.Response.OutputStream,
                                        System.Drawing.Imaging.ImageFormat.Jpeg
                                     );
                    bitmap.Dispose();
                }
                else
                {
                    context.Response.WriteFile(strPath);
                }
            }
            catch (Exception e)
            {
                context.Response.Write(e.Message);
            }
        }
  
        public bool IsReusable
        {
            get { return true; }
        }
    }
}
در خط WebConfigurationManager.GetSection، در صورتیکه تگ imagecopyright تعریف شده باشد، همه اطلاعات این تگ را از فایل کانفیگ بیرون کشیده و داخل شیء imageCopyrightHandlerSection از نوع ConfigurationSection قرار می‌دهیم. سپس اطلاعات هر سه گزینه را خوانده و به همراه context (اطلاعات درخواست) به تابع handleimage که ما آن را نوشته ایم ارسال می‌کنیم. کار این تابع درج تگ می‌باشد.
در خطوط اولیه تابع، ما آدرس فیزیکی منبع درخواست شده را به دست آورده و در صورتیکه مقدار گزینه enable با true مقدار دهی شده باشد، آن را به شی bitmap نسبت می‌دهیم و با استفاده از دیگر کلاس‌های گرافیکی، تگ مورد نظر را با متن و رنگ مشخص شده ایجاد می‌کنیم. در نهایت شیء bitmap را ذخیره و نوع خروجی response را از نوع image/jpeg تعریف می‌کنیم تا مرورگر بداند که خروجی ما یک تصویر است. ولی در صورتی که enabled با false مقداردهی شده باشد، همان تصویر اصلی را بدون درج تگ ارسال می‌کنیم.
فضای نام Microsoft.Web.Administration برای اجرای خود نیاز دارد تا اسمبلی آن رفرنس شود. برای اینکار به درون دایرکتوری mypictures رفته و در داخل فایل web.config که بعد از تبدیل این دایرکتوری به اپلیکیشن ایجاد شده بنویسید:
 <system.web>  
     <compilation>  
       <assemblies>  
         <add assembly="Microsoft.Web.Administration, Version=7.0.0.0,   
 Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"/> 
      </assemblies>
    </compilation>
 </system.web>
در صورتی که کلاس خود را کامپایل کنید می‌توانید آن را داخل پوشه‌ی Bin به جای App_Code قرار دهید و نیاز به رفرنس کرده اسمبلی Microsoft.Web.Administration نیز ندارید.
در آخرین مرحله فقط باید به IIS بگویید که تنها فایل‌های jpg را برای این هندلر، هندل کن. این کار را از طریق خط فرمان نجام می‌دهیم:
appcmd set config "Default Web Site/mypictures/" -section:handlers  /+[name='JPGimageCopyrightHandler',path='*.jpg',verb='GET',type='IIS7Demos.imageCopyrightHandler']
هندلر مورد نظر تنها برای این اپلیکیشن و در مسیر mypicture فعال شده و در قسمت name، یک نام اختیاری بدون فاصله و unique بر می‌گزینیم. در قسمت path نوع فایل‌هایی را که نیاز به هندل هست، مشخص کردیم و در قسمت verb گفته‌ایم که تنها برای درخواست‌های نوع GET، هندلر را اجرا کن و در قسمت type هم که اگر  مقاله httphandler را خوانده باشید می‌دانید که به معرفی هندلر می‌پردازیم؛ اولی نام فضای نام هست و بعد از . نام کلاس، که در اینجا می‌شود : 
'IIS7Demos.imageCopyrightHandler 
الان همه چیز برای اجرا آماده است و فقط یک مورد برای احتیاط الزامی است و آن هم این است که پروسه‌های کارگر، ممکن است از قبل در حال اجرا بوده باشند و هنوز شمای جدید ما را شناسایی نکرده باشند، برای همین باید آن‌ها را با تنظیمات جدیدمان آشنا کنیم تا احیانا برایمان استثناء صادر نشود:
appcmd recycle AppPool DefaultAppPool
کارمان تمام شده ، چند تصویر داخل دایرکتوری قرار داده و درخواست  تصاویر موجود را بدهید تا تگ را ببینید:

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

ACID چیست؟

ACID از 4 قاعده تشکیل شده است (Atomic, Consistent, Isolated, and Durable) که با کنار هم قرار دادن آن‌ها یک تراکنش مفهوم پیدا می‌کند:

الف) Atomic: به معنای همه یا هیچ
اگر تراکنشی از چندین تغییر تشکیل می‌شود، همه‌ی آن‌ها باید با موفقیت انجام شوند، یا اینکه هیچکدام از تغییرات نباید فرصت اعمال نهایی را بیابند.
برای مثال انتقال مبلغ X را از یک حساب، به حسابی دیگر درنظر بگیرید. در این حالت X ریال از حساب شخص کسر و X ریال به حساب شخص دیگری واریز خواهد شد. اگر موجودی حساب شخص، دارای X ریال نباشد، نباید مبلغی از این حساب کسر شود. مرحله اول شکست خورده است؛ بنابراین کل عملیات لغو می‌شود. همچنین اگر حساب دریافت کننده بسته شده باشد نیز نباید مبلغی از حساب اول کسر گردد و در این حالت نیز کل تراکنش باید برگشت بخورد.

ب) Consistent یا یکپارچه
در اینجا consistency علاوه بر اعمال قیود، به معنای اطلاعاتی است که بلافاصله پس از پایان تراکنشی از سیستم قابل دریافت و خواندن است.

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

د) Durable: ماندگار
اگر سیستم پایان تراکنشی را اعلام می‌کند، این مورد به معنای 100 درصد نوشته شدن اطلاعات در سخت دیسک باید باشد.


مراحل چهارگانه ACID در RavenDB به چه نحوی وجود دارند؟

RavebDB از هر دو نوع تراکنش‌های implicit و explicit پشتیبانی می‌کند. Implicit به این معنا است که در حین استفاده معمول از RavenDB (و بدون انجام تنظیمات خاصی)، به صورت خودکار مفهوم تراکنش‌ها وجود داشته و اعمال می‌شوند. برای نمونه به متد ذیل توجه نمائید:
public void TransferMoney(string fromAccountNumber, string toAccountNumber, decimal amount) 
{
   using(var session = Store.OpenSession()) 
   {
         session.Advanced.UseOptimisticConcurrency = true;

         var fromAccount = session.Load<Account>("Accounts/" + fromAccountNumber);
         var toAccount = session.Load<Account>("Accounts/" + toAccountNumber);

         fromAccount.Balance -= amount;
         toAccount.Balance += amount;

         session.SaveChanges();
   }
}
در این متد مراحل ذیل رخ می‌دهند:
- از document store ایی که پیشتر تدارک دیده شده، جهت بازکردن یک سشن استفاده شده است.
- به سشن صراحتا عنوان شده است که از Optimistic Concurrency استفاده کند. در این حالت RavenDB اطمینان حاصل می‌کند که اکانت‌های بارگذاری شده توسط متدهای Load، تا زمان فراخوانی SaveChanges تغییر پیدا نکرده‌اند (و در غیراینصورت یک استثناء را صادر می‌کند).
- دو اکانت بر اساس Id آن‌ها از بانک اطلاعاتی واکشی می‌شوند.
- موجودی یکی تقلیل یافته و موجودی دیگر، افزایش می‌یابد.
- متد SaveChanges بر روی شی‌ء سشن فراخوانی شده است. تا زمانیکه این متد فراخوانی نشده است، کلیه تغییرات در حافظه نگهداری می‌شوند و به سرور ارسال نخواهند شد. فراخوانی آن سبب کامل شدن تراکنش و ارسال اطلاعات به سرور می‌گردد.
بنابراین شیء سشن بیانگر یک atomic transaction ماندگار و محصور شده است (سه جزء ACID تاکنون محقق شده‌اند). محصور شده بودن آن به این معنا است که:
الف) هر تغییری که در سشن اعمال می‌شود، تا پیش از فراخوانی متد SaveChanges از دید سایر تراکنش‌ها مخفی است.
ب) اگر دو تراکنش همزمان رخ دهند، تغییرات هیچکدام بر روی دیگری اثری ندارد.

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


عاقبت یک دست شدن یا eventual consistency

درک Consistency مفهوم ACID در RavenDB بسیار مهم است و عدم آشنایی با نحوه عملکرد آن می‌تواند مشکل‌ساز شود. در دنیای بانک‌های اطلاعاتی رابطه‌ای، برنامه نویس‌ها به «immediate consistency» عادت دارند (یکپارچگی آنی). به این معنا که هرگونه تغییری در بانک اطلاعاتی، پس از پایان تراکنش، بلافاصله در اختیار کلیه خوانندگان سیستم قرار می‌گیرد. در RavenDB و خصوصا دنیای NoSQL، این یکپارچگی آنی دنیای رابطه‌ای، به «eventual consistency» تبدیل می‌شود (عاقبت یک‌دست شدن). عاقبت یک دست شدن در RavenDB به این معنا است که اگر تغییری به یک سند اعمال گردیده و ذخیره شود؛ کوئری انجام شده بر روی این اطلاعات تغییر یافته ممکن است «stale data» باز گرداند. واژه stale در RavenDB به این معنا است که هنوز اطلاعاتی در دیتابیس موجود هستند که جهت تکمیل ایندکس‌ها پردازش نشده‌اند. به این مورد در قسمت بررسی ایندکس‌ها در RavenDB اشاره شد.
در RavenDB یک سری تردهای پشت صحنه، مدام مشغول به کار هستند و بدون کند کردن عملیات سیستم، کار ایندکس کردن اطلاعات را انجام می‌دهند. هر زمانیکه اطلاعاتی را ذخیره می‌کنیم، بلافاصله این تردها تغییرات را تشخیص داده و ایندکس‌ها را به روز رسانی می‌کنند. همچنین باید درنظر داشت که RavenDB جزو معدود بانک‌های اطلاعاتی است که خودش را بر اساس نحوه استفاده شما ایندکس می‌کند! (نمونه‌ای از آن‌را در قسمت ایندکس‌های پویای حاصل از کوئری‌های LINQ پیشتر مشاهده کرده‌اید)

نکته مهم
در RavenDB اگر از کوئری‌های LINQ استفاده کنیم، ممکن است به علت اینکه هنوز تردهای پشت صحنه‌ی ایندکس سازی اطلاعات، کارشان تمام نشده است، تمام اطلاعات یا آخرین اطلاعات را دریافت نکنیم (که به آن stale data گفته می‌شود). هر آنچه که ایندکس شده است دریافت می‌گردد (مفهوم عاقبت یک دست شدن ایندکس‌ها). اما اگر نیاز به یکپارچگی آنی داشتیم، متد Load یک سشن، مستقیما به بانک اطلاعاتی مراجعه می‌کند و اطلاعات بازگشت داده شده توسط آن هیچگاه احتمال stale بودن را ندارند.
بنابراین برای نمایش اطلاعات یا گزارشگیری، از کوئری‌های LINQ استفاده کنید. RavenDB خودش را بر اساس کوئری شما ایندکس خواهد کرد و نهایتا به کوئری‌هایی فوق العاده سریعی در طول کارکرد سیستم خواهیم رسید. اما در صفحه ویرایش اطلاعات بهتر است از متد Load استفاده گردد تا نیاز به مفهوم immediate consistency یا یکپارچگی آنی برآورده شود.


تنظیمات خاص کار با ایندکس سازها برای انتظار جهت اتمام کار آن‌ها

عنوان شد که اگر ایندکس سازهای پشت صحنه هنوز کارشان تمام نشده است، در حین کوئری گرفتن، هر آنچه که ایندکس شده بازگشت داده می‌شود.
در اینجا می‌توان به RavenDB گفت که تا چه زمانی می‌تواند یک کوئری را جهت دریافت اطلاعات نهایی به تاخیر بیندازد. برای اینکار باید اندکی کوئری‌های LINQ آن‌را سفارشی سازی کنیم:
RavenQueryStatistics stats;
var results = session.Query<Product>()
    .Statistics(out stats)
    .Where(x => x.Price > 10)
    .ToArray();
 
if (stats.IsStale)
{
    // Results are known to be stale
}
توسط امکانات آماری کوئری‌های LINQ در RavenDB مطابق کدهای فوق، می‌توان دریافت که آیا اطلاعات دریافت شده stale است یا خیر.
همچنین زمان انتظار تا پایان کار ایندکس ساز را نیز توسط متد Customize به نحو ذیل می‌توان تنظیم کرد:
RavenQueryStatistics stats;
var results = session.Query<Product>()
    .Statistics(out stats)
    .Where(x => x.Price > 10)
    .Customize(x => x.WaitForNonStaleResults(TimeSpan.FromSeconds(5)))
    .ToArray();
به علاوه می‌توان کلیه کوئری‌های یک documentStore را وارد به صبر کردن تا پایان کار ایندکس سازی کرد (متد Customize پیش فرضی را با WaitForNonStaleResultsAsOfLastWrite مقدار دهی و اعمال می‌کند):
 documentStore.Conventions.DefaultQueryingConsistency = ConsistencyOptions.QueryYourWrites;
این مورد در برنامه‌های وب توصیه نمی‌شود چون کل سیستم در حین آغاز کار با آن بر اساس یک documentStore سینگلتون باید کار کند و همین مساله صبر کردن‌ها، با بالا رفتن حجم اطلاعات و تعداد کاربران، پاسخ دهی سیستم را تحت تاثیر قرار خواهد داد. به علاوه این تنظیم خاص بر روی کوئری‌های پیشرفته Map/Reduce کار نمی‌کند. در این نوع کوئری‌های ویژه، برای صبر کردن تا پایان کار ایندکس شدن، می‌توان از روش زیر استفاده کرد:
while (documentStore.DatabaseCommands.GetStatistics().StaleIndexes.Length != 0)
{
    Thread.Sleep(10);
}

مقابله با تداخلات همزمانی

با تنظیم session.Advanced.UseOptimisticConcurrency = true، اگر سندی که در حال ویرایش است، در این حین توسط کاربر دیگری تغییر کرده باشد، استثنای ConcurrencyException صادر خواهد شد. همچنین این استثناء در صورتیکه شخصی قصد بازنویسی سند موجودی را داشته باشد نیز صادر خواهد شد (شخصی بخواهد سندی را با ID سند موجودی ذخیره کند). اگر از optimistic concurrency استفاده نشود، آخرین ترد نویسنده یا به روز کننده اطلاعات، برنده خواهد شد و اطلاعات نهایی موجود در بانک اطلاعاتی متعلق به او و حاصل بازنویسی آن ترد است.
 optimistic concurrency به زبان ساده به معنای به خاطر سپردن شماره نگارش یک سند است، زمانیکه آن‌را بارگذاری می‌کنیم و سپس ارسال آن به سرور، زمانیکه قصد ذخیره آن‌را داریم. در SQL Server اینکار توسط RowVersion انجام می‌شود. در بانک‌های اطلاعاتی سندگرا چون تمایل به استفاده از HTTP در آن‌ها زیاد است (مانند RavenDB) از مکانیزمی به نام E-Tag برای این منظور کمک گرفته می‌شود. هر زمانیکه تغییری به یک سند اعمال می‌شود، E-Tag آن  به صورت خودکار افزایش خواهد یافت.
برای مثال فرض کنید کاربری سندی را با E-Tag مساوی 2 بارگذاری کرده است. قبل از اینکه این کاربر در صفحه ویرایش اطلاعات کارش با این سند خاتمه یابد، کاربر دیگری در شبکه، این سند را ویرایش کرده است و اکنون E-Tag آن مثلا مساوی 6 است. در این زمان اگر کاربر یک سعی به ذخیره سازی اطلاعات نماید، چون E-Tag سند او با E-Tag سند موجود در سرور دیگر یکی نیست، با استثنای ConcurrencyException متوقف خواهد شد.



مشکل! در برنامه‌های بدون حالت وب، چون پس از نمایش صفحه ویرایش اطلاعات، سشن RavenDB نیز بلافاصله Dispose خواهد شد، این E-Tag را از دست خواهیم داد. همچنین باید دقت داشت که سشن RavenDB به هیچ عنوان نباید در طول عمر یک برنامه باز نگهداشته شود و برای طول عمری کوتاه طراحی شده است. راه حلی که برای آن درنظر گرفته شده است، ذخیره سازی این E-Tag در بار اول دریافت آن از سشن می‌باشد. برای این منظور تنها کافی است خاصیتی را به نام Etag با ویژگی JsonIgnore (که سبب عدم ذخیره سازی آن در بانک اطلاعاتی خواهد شد) تعریف کنیم:
public class Person
{
    public string Id { get; set; }

    [JsonIgnore]
    public Guid? Etag { get; set; }

    public string Name { get; set; }
}
اکنون زمانیکه سندی را از بانک اطلاعاتی دریافت می‌کنیم، با استفاده از متد session.Advanced.GetEtagFor، می‌توان این Etag واقعی را دریافت کرد و ذخیره نمود:
public Person Get(string id)
{
    var person = session.Load<Person>(id);
    person.Etag = session.Advanced.GetEtagFor(person);
    return person;
}
و برای استفاده از آن ابتدا باید UseOptimisticConcurrency به true تنظیم شده و سپس در متد Store این Etag دریافتی از سرور را مشخص نمائیم:
public void Update(Person person)
{
    session.Advanced.UseOptimisticConcurrency = true;
    session.Store(person, person.Etag, person.Id);
    session.SaveChanges();
    person.Etag = session.Advanced.GetEtagFor(person);
}


تراکنش‌های صریح

همانطور که عنوان شد، به صورت ضمنی کلیه سشن‌ها، یک واحد کار را تشکیل داده و با پایان آن‌ها، تراکنش خاتمه می‌یابد. اگر به هر علتی قصد تغییر این رفتار ضمنی پیش فرض را دارید، امکان تعریف صریح تراکنش‌های نیز وجود دارد:
using (var transaction = new TransactionScope())
{
   using (var session1 = store.OpenSession())
   {
     session1.Store(new Account());
     session1.SaveChanges();
   }

   using (var session2 = store.OpenSession())
   {
     session2.Store(new Account());
     session2.SaveChanges();
   }

   transaction.Complete();
}
باید دقت داشت که پایان یک تراکنش، یک non-blocking asynchronous call است و مباحث stale data که پیشتر در مورد آن بحث شد، برقرار هستند.
مطالب دوره‌ها
استفاده از RavenDB در ASP.NET MVC به همراه تزریق وابستگی‌ها
جهت تکمیل مباحث این دوره می‌توان به نحوه مدیریت سشن‌ها و document store بانک اطلاعاتی RavenDB با استفاده از یک IoC Container مانند StructureMap در ASP.NET MVC پرداخت. اصول کلی آن به تمام فناوری‌های دات نتی دیگر مانند وب فرم‌ها، WPF و غیره نیز قابل بسط است. تنها پیشنیاز آن مطالعه «کامل» دوره «بررسی مفاهیم معکوس سازی وابستگی‌ها و ابزارهای مرتبط با آن » می‌باشد.


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


ابزارها و پیشنیازهای لازم
ابتدا یک برنامه جدید ASP.NET MVC را آغاز کنید. سپس ارجاعات لازم را به کلاینت RavenDB، سرور درون پروسه‌ای آن (RavenDB.Embedded) و همچنین StructureMap با استفاده از نیوگت، اضافه نمائید:
 PM> Install-Package RavenDB.Client
PM> Install-Package RavenDB.Embedded -Pre
PM> Install-Package structuremap

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

این مثال، به همراه فایل‌های باینری ارجاعات یاد شده، نیست (جهت کاهش حجم 100 مگابایتی آن). برای بازیابی آن‌ها می‌توانید به مطلبی در اینباره در سایت مراجعه کنید.
این پروژه از چهار قسمت مطابق شکل زیر تشکیل شده است:


الف) لایه سرویس‌های برنامه
using RavenDB25Mvc4Sample.Models;
using System.Collections.Generic;

namespace RavenDB25Mvc4Sample.Services.Contracts
{
    public interface IUsersService
    {
        User AddUser(User user);
        IList<User> GetUsers(int page, int count = 20);
    }
}

using System.Collections.Generic;
using System.Linq;
using Raven.Client;
using RavenDB25Mvc4Sample.Models;
using RavenDB25Mvc4Sample.Services.Contracts;

namespace RavenDB25Mvc4Sample.Services
{
    public class UsersService : IUsersService
    {
        private readonly IDocumentStore _documentStore;
        private readonly IDocumentSession _documentSession;
        public UsersService(IDocumentStore documentStore, IDocumentSession documentSession)
        {
            _documentStore = documentStore;
            _documentSession = documentSession;
        }

        public User AddUser(User user)
        {
            _documentSession.Store(user);
            return user;
        }

        public IList<User> GetUsers(int page, int count = 20)
        {
            return _documentSession.Query<User>()
                                   .Skip(page * count)
                                   .Take(count)
                                   .ToList();
        }

        //todo: سایر متدهای مورد نیاز در اینجا

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

ب) لایه Infrastructure برنامه
در این لایه کدهای اتصالات IoC Container مورد استفاده قرار می‌گیرند. کدهایی که به برنامه جاری وابسته‌اند، اما حالت عمومی و مشترکی ندارند تا در سایر پروژه‌های مشابه استفاده شوند.
using Raven.Client;
using Raven.Client.Embedded;
using RavenDB25Mvc4Sample.Services;
using RavenDB25Mvc4Sample.Services.Contracts;
using StructureMap;

namespace RavenDB25Mvc4Sample.Infrastructure
{
    public static class IoCConfig
    {
        public static void ApplicationStart()
        {
            ObjectFactory.Initialize(x =>
            {
                // داکیومنت استور سینگلتون تعریف شده چون باید در طول عمر برنامه زنده نگه داشته شود
                x.ForSingletonOf<IDocumentStore>().Use(() =>
                       {
                           return new EmbeddableDocumentStore
                           {
                               DataDirectory = "App_Data"
                           }.Initialize();
                       });

                // سشن در برنامه وب هیبرید تعریف شده تا در طول عمر یک درخواست زنده نگه داشته شود
                // در برنامه‌های ویندوزی حالت هیبرید را حذف کنید
                x.For<IDocumentSession>().HybridHttpOrThreadLocalScoped().Use(context =>
                    {
                        return context.GetInstance<IDocumentStore>().OpenSession();
                    });

                // اتصالات لایه سرویس در اینجا
                x.For<IUsersService>().Use<UsersService>();
                // ...
            });
        }

        public static void ApplicationEndRequest()
        {
            ObjectFactory.ReleaseAndDisposeAllHttpScopedObjects();
        }
    }
}
تعاریف اتصالات StructureMap را در اینجا ملاحظه می‌کنید.
IDocumentStore و IDocumentSession، دو اینترفیس تعریف شده در کلاینت RavenDB هستند. اولی کار اتصال به بانک اطلاعاتی را مدیریت خواهد کرد و دومی کار مدیریت الگوی واحد کار را انجام می‌دهد. IDocumentStore به صورت Singleton تعریف شده است؛ چون باید در طول عمر برنامه زنده نگه داشته شود. اما IDocumentStore در ابتدای هر درخواست رسیده، وهله سازی شده و سپس در پایان هر درخواست در متد ApplicationEndRequest به صورت خودکار Dispose خواهد شد.
اگر به فایل Global.asax.cs پروژه وب برنامه مراجعه کنید، نحوه استفاده از این کلاس را مشاهده خواهید کرد:
using System;
using System.Globalization;
using System.Web.Mvc;
using System.Web.Routing;
using RavenDB25Mvc4Sample.Infrastructure;
using StructureMap;

namespace RavenDB25Mvc4Sample
{
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            IoCConfig.ApplicationStart();

            AreaRegistration.RegisterAllAreas();

            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);

            //Set current Controller factory as StructureMapControllerFactory  
            ControllerBuilder.Current.SetControllerFactory(new StructureMapControllerFactory());
        }

        protected void Application_EndRequest(object sender, EventArgs e)
        {
            IoCConfig.ApplicationEndRequest();
        }
    }

    public class StructureMapControllerFactory : DefaultControllerFactory
    {
        protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
        {
            if (controllerType == null)
                throw new InvalidOperationException(string.Format("Page not found: {0}", 
                    requestContext.HttpContext.Request.Url.AbsoluteUri.ToString(CultureInfo.InvariantCulture)));
            return ObjectFactory.GetInstance(controllerType) as Controller;
        }
    }
}
در ابتدای کار برنامه، متد IoCConfig.ApplicationStart جهت برقراری اتصالات، فراخوانی می‌شود. در پایان هر درخواست نیز شیء سشن جاری تخریب خواهد شد. همچنین کلاس StructureMapControllerFactory نیز جهت وهله سازی خودکار کنترلرهای برنامه به همراه تزریق وابستگی‌های مورد نیاز، تعریف گشته است.

ج) استفاده از کلاس‌های لایه سرویس در کنترلرهای برنامه

using System.Web.Mvc;
using Raven.Client;
using RavenDB25Mvc4Sample.Models;
using RavenDB25Mvc4Sample.Services.Contracts;

namespace RavenDB25Mvc4Sample.Controllers
{
    public class HomeController : Controller
    {
        private readonly IDocumentSession _documentSession;
        private readonly IUsersService _usersService;
        public HomeController(IDocumentSession documentSession, IUsersService usersService)
        {
            _documentSession = documentSession;
            _usersService = usersService;
        }

        [HttpGet]
        public ActionResult Index()
        {
            return View(); //نمایش صفحه ثبت
        }

        [HttpPost]
        public ActionResult Index(User user)
        {
            if (this.ModelState.IsValid)
            {
                _usersService.AddUser(user);
                _documentSession.SaveChanges();

                return RedirectToAction("Index");
            }
            return View(user);
        }
    }
}
پس از این مقدمات و طراحی اولیه، استفاده از کلاس‌های لایه سرویس در کنترل‌ها، ساده خواهند بود. تنها کافی است اینترفیس‌های مورد نیاز را از طریق روش تزریق در سازنده کلاس‌ها تعریف کنیم. سایر مسایل وهله سازی آن خودکار خواهند بود.
مطالب
نمایش اخطارها و پیام‌های بوت استرپ به کمک TempData در ASP.NET MVC
در بوت استرپ برای نمایش اعلانی به کاربر، از کلاس alert می‌توان استفاده کرد. برای نمایش این اعلان کافی است محتوای خود را درون یک div با کلاس alert قرار دهیم: 
<div class="alert">
    نمایش اعلانات
</div>

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

همچنین اگر مایل بودید می‌توانید با افزودن یک دکمه با کلاس close و ویژگی data-dismiss مساوی alert، امکان بستن پیام را در اختیار کاربر قرار دهید: 

<div class="alert alert-warning alert-dismissable">
  <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
  نمایش اعلان
</div>

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

ابتدا کلاس‌های زیر را تعریف می‌کنیم:

کلاس Alert

public class Alert
{
        public const string TempDataKey = "TempDataAlerts";
        public string AlertStyle { get; set; }
        public string Message { get; set; }
        public bool Dismissable { get; set; }
}

در کلاس فوق خصوصیت یک alert را تعریف کرده‌ایم (از خاصیت TempDataKey جهت پاس دادن alert‌ها به view استفاده می‌کنیم).

  کلاس AlertStyles
public class AlertStyles
{
        public const string Success = "success";
        public const string Information = "info";
        public const string Warning = "warning";
        public const string Danger = "danger";
}
کلاس فوق نیز جهت نگهداری اسامی کلاس‌های alert، مورد استفاده قرار می‌گیرد. قدم بعدی، استفاده از کلاس‌های فوق و انتقال alerts توسط TempData به داخل viewها می‌باشد. برای جلوگیری از زیاد شدن حجم کدهای تکراری داخل کنترلرها و همچنین به عنوان یک Best practice، یک کنترلر Base را برای اینکار تعریف می‌کنیم و متدهای موردنیاز را داخل آن می‌نویسیم:
public class BaseController : Controller
{
        public void Success(string message, bool dismissable = false)
        {
            AddAlert(AlertStyles.Success, message, dismissable);
        }

        public void Information(string message, bool dismissable = false)
        {
            AddAlert(AlertStyles.Information, message, dismissable);
        }

        public void Warning(string message, bool dismissable = false)
        {
            AddAlert(AlertStyles.Warning, message, dismissable);
        }

        public void Danger(string message, bool dismissable = false)
        {
            AddAlert(AlertStyles.Danger, message, dismissable);
        }

        private void AddAlert(string alertStyle, string message, bool dismissable)
        {
            var alerts = TempData.ContainsKey(Alert.TempDataKey)
                ? (List<Alert>)TempData[Alert.TempDataKey]
                : new List<Alert>();

            alerts.Add(new Alert
            {
                AlertStyle = alertStyle,
                Message = message,
                Dismissable = dismissable
            });

            TempData[Alert.TempDataKey] = alerts;
        }
}
از متدهایی که به صورت عمومی تعریف شده‌اند جهت ارسال پیام به view استفاده می‌کنیم. متد AddAlert نیز جهت ایجاد لیستی از پیام‌ها(Alert) مورداستفاده قرار می‌گیرد؛ زیرا ممکن است بخواهید هم زمان از چندین متد عمومی فوق استفاده کنید، یعنی چندین پیام را به کاربر نمایش دهید. 

نکته: در کد فوق از TempData جهت پاس دادن شیء alerts استفاده کرده‌ایم. TempData به صورت short-lived عمل می‌کند به دو دلیل: 1- بلافاصله بعد از خوانده شدن، حذف خواهد شد. 2- پس از پایان درخواست از بین خواهد رفت. از TempData جهت پاس دادن داده‌ها از درخواست فعلی به درخواست بعدی (redirect از یک صفحه به صفحه دیگر) استفاده می‌شود. یعنی در زمان redirect سعی می‌کند داده‌های بین redirectها را در خود نگه دارد. اگر از ViewBag و ViewData استفاده می‌کردیم داده‌های داخل آنها بلافاصله بعد از redirect شدن null می‌شدند.
به طور مثال اکشن متد زیر را در نظر بگیرید:
public ActionResult Index()
        {
            var userInfo = new
            {
                Name = "Sirwan",
                LastName = "Afifi",
            };
 
            ViewData["User"] = userInfo;
            ViewBag.User = userInfo;
            TempData["User"] = userInfo;

            return RedirectToAction("About");
        }
view :
@{
    ViewBag.Title = "About";
}

<h1>Tempdata</h1><p>@TempData["User"]</p>
<h1>ViewData</h1><p>@ViewData["User"]</p>
<h1>ViewBag</h1><p>@ViewBag.User</p>
اگر کد فوق را تست کنید خواهید دید که در خروجی تنها اطلاعات داخل TempData نمایش داده می‌شود.
معمولاً برای ارسال داده‌های خطاها از TempData استفاده می‌شود.

اکنون در هر کنترلری که می‌خواهید پیامی را به صورت alert، پس از ثبت اطلاعات به کاربر نمایش دهید، باید از کنترلر BaseController  ارث‌بری کنید:
public class NewsController : BaseController
{
readonly INewsService _newsService;
        readonly IUnitOfWork _uow;
        public NewsController(INewsService newsService, IUnitOfWork uow)
        {
            _newsService = newsService;
            _uow = uow;
        }
        [HttpPost]
        [ValidateAntiForgeryToken]
        [ValidateInput(false)]
        public ActionResult Create(News news)
        {
            if (ModelState.IsValid)
            {
                _newsService.AddNews(news);
                _uow.SaveChanges();
                Success(string.Format("خبر با عنوان  <b>{0}</b> با موفقیت ذخیره گردید!", news.Title), true);
                return RedirectToAction("Index");
            }
            Danger("خطا در هنگام ثبت اطلاعات ");
            return View(news);
        }
        [HttpPost]
        public ActionResult Delete(int id)
        {
            _newsService.DeleteNewsById(id);
            _uow.SaveChanges();
            Danger("اطلاعات مورد نظر با موفقیت حذف گردید!", true);
            return RedirectToAction("Index");
        }
}

نمایش پیام‌ها 
برای نمایش پیام‌ها یک partial view با نام _Alerts در مسیر Views\Shared ایجاد می‌کنیم: 
@{
    var alerts = TempData.ContainsKey(Alert.TempDataKey)
                ? (List<Alert>)TempData[Alert.TempDataKey]
                : new List<Alert>();

    if (alerts.Any())
    {
        <hr />
    }

    foreach (var alert in alerts)
    {
        var dismissableClass = alert.Dismissable ? "alert-dismissable" : null;
        <div class="alert alert-@alert.AlertStyle @dismissableClass">
            @if (alert.Dismissable)
            {
                <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
            }
            @Html.Raw(alert.Message)
        </div>
    }
}
در کد فوق، ابتدا شیء alerts را از TempData دریافت کرده‌ایم و توسط یک حلقه foreach، داخل آن به ازای هر آیتم، آن را پیمایش می‌کنیم و در نهایت کد html متناظر با هر alert را در خروجی نمایش می‌دهیم.
اکنون جهت استفاده از partial view فوق در جایی که می‌خواهید پیام نمایش داده شود partial view فوق را فراخوانی کنید (به عنوان مثال داخل فایل Layout): 
<div>
                    @{ Html.RenderPartial("_Alerts"); }
                    @RenderBody()
</div>

مطالب
Angular Material 6x - قسمت هفتم - کار با انواع قالب‌ها
در این قسمت می‌خواهیم روش تغییر رنگ‌های قالب‌های پیش‌فرض Angular Material را به همراه تغییر پویای آن‌ها در زمان اجرا، بررسی کنیم. همچنین Angular Material از راست به چپ نیز به خوبی پشتیبانی می‌کند که مثالی از آن‌را در ادامه بررسی خواهیم کرد.


بررسی ساختار یک قالب Angular Material

قالب، مجموعه‌ای از رنگ‌ها است که به کامپوننت‌های Angular Material اعمال می‌شود. هر قالب از چندین جعبه‌رنگ یا palette تشکیل می‌شود:
- primary palette: به صورت گسترده‌ای در تمام کامپوننت‌ها مورد استفاده‌است.
- accent palette: به المان‌های تعاملی انتساب داده می‌شود.
- warn palette: برای نمایش خطاها و اخطارها بکار می‌رود.
- foreground palette: برای متون و آیکن‌ها استفاده می‌شود.
- background palette: برای پس‌زمینه‌ی المان‌ها بکار می‌رود.

روش انتخاب این جعبه رنگ‌ها نیز به صورت زیر است:
<mat-card>
  Main Theme:
  <button mat-raised-button color="primary">
    Primary
  </button>
  <button mat-raised-button color="accent">
    Accent
  </button>
  <button mat-raised-button color="warn">
    Warning
  </button>
</mat-card>
در Angular Material تمام قالب‌ها استاتیک بوده و در زمان کامپایل برنامه به صورت خودکار به آن اضافه می‌شوند. به همین جهت برنامه نیازی به تشکیل این اجزا و کامپایل یک قالب را در زمان آغاز آن ندارد.
همانطور که در قسمت اول این سری نیز بررسی کردیم، بسته‌ی Angular Material به همراه چندین قالب از پیش طراحی شده‌است (قالب‌های از پیش آماده‌ی متریال را در پوشه‌ی node_modules\@angular\material\prebuilt-themes می‌توانید مشاهده کنید) و در حین اجرای برنامه تنها یکی از آن‌ها که در فایل styles.css ذکر شده‌است، مورد استفاده قرار می‌گیرد.
اگر نیاز به سفارشی سازی بیشتری وجود داشته باشد، می‌توان قالب‌های ویژه‌ی خود را نیز طراحی کرد. این قالب جدید باید mat-core() sass mixin را import کند که حاوی تمام شیوه‌نامه‌های مشترک بین کامپوننت‌ها است. این مورد باید تنها یکبار به کل برنامه الحاق شود تا حجم آن‌را بیش از اندازه زیاد نکند. سپس این قالب سفارشی، جعبه رنگ‌های خاص خودش را معرفی می‌کند. در ادامه این جعبه رنگ‌ها توسط توابع mat-light-theme و یا mat-dark-theme ترکیب شده و مورد استفاده قرار می‌گیرند. سپس این قالب را include خواهیم کرد. به این ترتیب یک قالب سفارشی Angular Material، چنین طرحی را دارد:
@import '~@angular/material/theming';
@include mat-core();

$candy-app-primary: mat-palette($mat-indigo);
$candy-app-accent: mat-palette($mat-pink, A200, A100, A400);
$candy-app-warn: mat-palette($mat-red);

$candy-app-theme: mat-light-theme($candy-app-primary, $candy-app-accent, $candy-app-warn);

@include angular-material-theme($candy-app-theme);
اعداد و ارقامی را که در اینجا ملاحظه می‌کنید، در سیستم رنگ‌های طراحی متریال به darker hue و lighter hue تفسیر می‌شوند. همچنین امکان سفارشی سازی تایپوگرافی آن نیز وجود دارد.
ذکر جعبه رنگ اخطار در اینجا اختیاری است و اگر ذکر نشود به قرمز تنظیم خواهد شد.

ایجاد یک قالب سفارشی جدید Angular Material

برای ایجاد یک قالب سفارشی نیاز است از فایل‌های sass استفاده کرد. بنابراین بهترین روش ایجاد برنامه‌های Angular Material در ابتدای کار، ذکر صریح نوع style مورد استفاده به sass است:
 ng new MyProjectName --style=sass
اگر اینکار را انجام نداده‌ایم و حالت پیش‌فرض پروژه همان css است، مهم نیست. می‌توان فایل قالب سفارشی را در یک فایل با پسوند custom.theme.scss نیز در پوشه‌ی src قرار داد و سپس آن‌را در فایل angular.json مشخص کرد تا به صورت css کامپایل شده و مورد استفاده قرار گیرد:
"styles": [
   "node_modules/material-design-icons/iconfont/material-icons.css",
   "src/styles.css",
   "src/custom.theme.scss"
],
بدیهی است اگر از ابتدا style=sass را تنظیم کرده بودیم، نیازی به ایجاد این فایل اضافی نبود و همان styles.scss اصلی را می‌شد ویرایش کرد و در این حالت فایل angular.json بدون تغییر باقی می‌ماند.
پس از افزودن و تنظیم فایل custom.theme.scss، به فایل styles.css مراجعه کرده و قالب فعلی را به صورت comment در می‌آوریم:
/* @import "~@angular/material/prebuilt-themes/indigo-pink.css"; */

body {
  margin: 0;
}
سپس فایل src\custom.theme.scss را به صورت زیر تکمیل می‌کنیم:
@import '~@angular/material/theming';
@include mat-core();

$my-app-primary: mat-palette($mat-blue-grey);
$my-app-accent:  mat-palette($mat-pink, 500, 900, A100);
$my-app-warn:    mat-palette($mat-deep-orange);

$my-app-theme: mat-light-theme($my-app-primary, $my-app-accent, $my-app-warn);

@include angular-material-theme($my-app-theme);

.alternate-theme {
  $alternate-primary: mat-palette($mat-light-blue);
  $alternate-accent:  mat-palette($mat-yellow, 400);

  $alternate-theme: mat-light-theme($alternate-primary, $alternate-accent);

  @include angular-material-theme($alternate-theme);
}
که در اینجا شامل مراحل import فایل‌های پایه‌ی Angular Material، تعریف جعبه رنگ جدید، ترکیب آن‌ها و در نهایت include آن‌ها می‌باشد.

در اینجا روش تعریف یک قالب دوم (alternate-theme) را نیز مشاهده می‌کنید. علت تعریف قالب دوم در همین فایل جاری، کاهش حجم نهایی برنامه است. از این جهت که اگر alternate-themeها را در فایل‌های scss دیگری قرار دهیم، مجبور به import تعاریف اولیه‌ی قالب‌های Angular Material در هرکدام به صورت جداگانه‌ای خواهیم بود که حجم قابل ملاحظه‌ای را به خود اختصاص می‌دهند. به همین جهت قالب‌های دیگر را نیز در همینجا به صورت کلاس‌های ثانویه تعریف خواهیم کرد.
 در این حالت روش استفاده‌ی از این قالب ثانویه به صورت زیر می‌باشد:
<mat-card class="alternate-theme">
  Alternate Theme:
  <button mat-raised-button color="primary">
    Primary
  </button>
  <button mat-raised-button color="accent">
    Accent
  </button>
  <button mat-raised-button color="warn">
    Warning
  </button>
</mat-card>

پس از افزودن فایل src\custom.theme.scss به برنامه، اگر آن‌را اجرا کنیم به خروجی زیر خواهیم رسید:




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

قصد داریم به منوی برنامه که اکنون گزینه‌ی new contact را به همراه دارد، گزینه‌ی toggle theme را هم جهت تغییر پویای قالب اصلی برنامه اضافه کنیم. به همین جهت فایل toolbar.component.html را گشوده و به صورت زیر تغییر می‌دهیم:
  <mat-menu #menu="matMenu">
    <button mat-menu-item (click)="openAddContactDialog()">New Contact</button>
    <button mat-menu-item (click)="toggleTheme.emit()">Toggle theme</button>
  </mat-menu>
در اینجا دکمه‌ای به منو اضافه شده‌است که سبب صدور رخدادی به والد آن یا همان sidenav خواهد شد. علت اینجا است که تغییر قالب را در sidenav، که در برگیرنده‌ی router-outlet است، می‌توان به کل برنامه اعمال کرد.
بنابراین جهت تبادل اطلاعات بین toolbar و sidenav از یک رخ‌داد استفاده خواهیم کرد. برای این منظور فایل toolbar.component.ts را گشوده و این رخ‌داد را به آن اضافه می‌کنیم:
export class ToolbarComponent implements OnInit {

  @Output() toggleTheme = new EventEmitter<void>();
پس از این تعریف، به sidenav.component.html مراجعه کرده و به این رخ‌داد گوش فرا می‌دهیم:
<app-toolbar (toggleTheme)="toggleTheme()" (toggleSidenav)="sidenav.toggle()"></app-toolbar>
متد toggleTheme را نیز به صورت زیر به sidenav.component.ts اضافه می‌کنیم:
export class SidenavComponent {

  isAlternateTheme = false;

  toggleTheme() {
    this.isAlternateTheme = !this.isAlternateTheme;
  }
}
در اینجا یک خاصیت عمومی boolean را با کلیک بر روی گزینه‌ی منوی Toggle theme، به true و یا false تنظیم می‌کنیم. اکنون از این مقدار جهت تغییر css قالب sidenav استفاده خواهیم کرد:
<mat-sidenav-container fxLayout="row" class="app-sidenav-container" fxFill 
   [class.alternate-theme]="isAlternateTheme">
به این ترتیب اگر مقدار isAlternateTheme مساوی true باشد، کلاس alternate-theme به قالب sidenav به صورت پویا اعمال خواهد شد و برعکس. در تصویر زیر نمونه‌ای از تغییر پویای قالب برنامه را مشاهده می‌کنید:




افزودن پشتیبانی از راست به چپ به قالب برنامه

اگر به mat-sidenav-container ویژگی dir=rtl را اضافه کنیم، قالب برنامه راست به چپ خواهد شد. در ادامه می‌خواهیم شبیه به حالت تغییر پویای قالب سایت، گزینه‌ای را به منوی برنامه جهت تغییر جهت برنامه نیز اضافه کنیم. برای این منظور به قالب toolbar.component.html مراجعه کرده و گزینه‌ی Toggle dir را به آن اضافه می‌کنیم:
  <mat-menu #menu="matMenu">
    <button mat-menu-item (click)="openAddContactDialog()">New Contact</button>
    <button mat-menu-item (click)="toggleTheme.emit()">Toggle theme</button>
    <button mat-menu-item (click)="toggleDir.emit()">Toggle dir</button>
  </mat-menu>
سپس این رخ‌داد را که قرار است در نهایت به sidenav منتقل شود، به صورت زیر به toolbar.component.ts اضافه می‌کنیم:
export class ToolbarComponent implements OnInit {

  @Output() toggleDir = new EventEmitter<void>();
اکنون در sidenav.component.html به این ر‌خ‌داد گوش فرا خواهیم داد:
<app-toolbar (toggleDir)="toggleDir()" 
(toggleTheme)="toggleTheme()" 
(toggleSidenav)="sidenav.toggle()"></app-toolbar>
متد toggleDir در sidenav.component.ts به صورت زیر پیاده سازی می‌شود:
export class SidenavComponent implements OnInit, OnDestroy {

  dir = "ltr";

  toggleDir() {
    this.dir = this.dir === "ltr" ? "rtl" : "ltr";
  }
}
و در نهایت این جهت را به mat-sidenav-container در فایل sidenav.component.html اعمال می‌کنیم:
<mat-sidenav-container fxLayout="row" class="app-sidenav-container" fxFill [dir]="dir"
  [class.alternate-theme]="isAlternateTheme">
در تصویر زیر نمونه‌ای از تغییر پویای جهت برنامه را مشاهده می‌کنید:




کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: MaterialAngularClient-06.zip
برای اجرای آن:
الف) ابتدا به پوشه‌ی src\MaterialAngularClient وارد شده و فایل‌های restore.bat و ng-build-dev.bat را اجرا کنید.
ب) سپس به پوشه‌ی src\MaterialAspNetCoreBackend\MaterialAspNetCoreBackend.WebApp وارد شده و فایل‌های restore.bat و dotnet_run.bat را اجرا کنید.
اکنون برنامه در آدرس https://localhost:5001 قابل دسترسی است.