مطالب
MVVM و امکان استفاده از یک وهله از ViewModel جهت چند View مرتبط

عموما هنگام طراحی یک View، خیلی زود به حجم انبوهی از کدهای XAML خواهیم رسید. در ادامه بررسی خواهیم کرد که چطور می‌توان یک View را به چندین View خرد کرد، بدون اینکه نیازی باشد تا از چندین ViewModel (یا همان code behind عاری از ارجاعات بصری سابق قرار گرفته در یک پروژه جدای دیگر) استفاده شود و تمام این View های خرد شده هم تنها از یک وهله از ViewModel ایی خاص استفاده کنند و با اطلاعاتی یکپارچه سروکار داشته باشند؛ یا در عمل یکپارچه کار کنند.
این مشکل از جایی شروع می‌شود که مثلا خرد کردن یک user control به چند یوزر کنترل، یعنی کار کردن با چند وهله از اشیایی متفاوت. هر چند نهایتا تمام این‌ها قرار است در یک صفحه در کنار هم قرار گیرند اما در عمل از هم کاملا مجزا هستند و اگر به ازای هر کدام یکبار ViewModel را وهله سازی کنیم، به مشکل برخواهیم خورد؛ چون هر وهله نسبت به وهله‌ای دیگر ایزوله است. اگر در یکی Name مثلا Test بود در دیگری ممکن است مقدار پیش فرض نال را داشته باشد؛ چون با چند وهله از یک کلاس، در یک فرم نهایی سروکار خواهیم داشت.

ابتدا Model و ViewModel ساده زیر را در نظر بگیرید:
using System.ComponentModel;

namespace SplittingViewsInMvvm.Models
{
public class GuiModel : INotifyPropertyChanged
{
string _name;
public string Name
{
get { return _name; }
set
{
_name = value;
raisePropertyChanged("Name");
}
}

string _lastName;
public string LastName
{
get { return _lastName; }
set
{
_lastName = value;
raisePropertyChanged("LastName");
}
}

#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
void raisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler == null) return;
handler(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
}

using SplittingViewsInMvvm.Models;

namespace SplittingViewsInMvvm.ViewModels
{
public class MainViewModel
{
public GuiModel GuiModelData { set; get; }

public MainViewModel()
{
GuiModelData = new GuiModel();
GuiModelData.Name = "Name";
GuiModelData.LastName = "LastName";
}
}
}

سپس View زیر هم از این اطلاعات استفاده خواهد کرد:

<UserControl x:Class="SplittingViewsInMvvm.Views.Main"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
xmlns:VM="clr-namespace:SplittingViewsInMvvm.ViewModels"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<VM:MainViewModel x:Key="vmMainViewModel" />
</UserControl.Resources>
<StackPanel DataContext="{Binding Source={StaticResource vmMainViewModel}}">
<GroupBox Margin="2" Header="Group 1">
<TextBlock Text="{Binding GuiModelData.Name}" />
</GroupBox>
<GroupBox Margin="2" Header="Group 2">
<TextBlock Text="{Binding GuiModelData.LastName}" />
</GroupBox>
</StackPanel>
</UserControl>

اکنون فرض کنید که می‌خواهیم Group 1 و Group 2 را جهت مدیریت ساده‌تر View اصلی در دو user control مجزا قرار دهیم؛ مثلا:

<UserControl x:Class="SplittingViewsInMvvm.Views.Group1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<GroupBox Margin="2" Header="Group 1">
<TextBlock Text="{Binding GuiModelData.Name}" />
</GroupBox>
</Grid>
</UserControl>
و
<UserControl x:Class="SplittingViewsInMvvm.Views.Group2"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<GroupBox Margin="2" Header="Group 2">
<TextBlock Text="{Binding GuiModelData.LastName}" />
</GroupBox>
</Grid>
</UserControl>

اکنون اگر این دو را مجددا در همان View اصلی ساده شده قبلی قرار دهیم (بدون اینکه در هر user control به صورت جداگانه data context را تنظیم کنیم):
<UserControl x:Class="SplittingViewsInMvvm.Views.Main"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
xmlns:V="clr-namespace:SplittingViewsInMvvm.Views"
xmlns:VM="clr-namespace:SplittingViewsInMvvm.ViewModels"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<VM:MainViewModel x:Key="vmMainViewModel" />
</UserControl.Resources>
<StackPanel DataContext="{Binding Source={StaticResource vmMainViewModel}}">
<V:Group1 />
<V:Group2 />
</StackPanel>
</UserControl>

باز هم .... برنامه همانند سابق کار خواهد کرد و ViewModel وهله سازی شده در user control فوق به صورت یکسانی در اختیار هر دو View اضافه شده قرار می‌گیرد و نهایتا یک View یکپارچه را در زمان اجرا می‌توان مورد استفاده قرار داد. علت هم بر می‌گردد به مقدار دهی خودکار DataContext هر View اضافه شده به بالاترین DataContext موجود در Visual tree که ذکر آن الزامی نیست:

<UserControl x:Class="SplittingViewsInMvvm.Views.Main"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
xmlns:V="clr-namespace:SplittingViewsInMvvm.Views"
xmlns:VM="clr-namespace:SplittingViewsInMvvm.ViewModels"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<VM:MainViewModel x:Key="vmMainViewModel" />
</UserControl.Resources>
<StackPanel DataContext="{Binding Source={StaticResource vmMainViewModel}}">
<V:Group1 DataContext="{Binding}" />
<V:Group2 DataContext="{Binding}"/>
</StackPanel>
</UserControl>


بنابراین به صورت خلاصه زمانیکه از MVVM استفاده ‌می‌کنید لازم نیست کار خاصی را جهت خرد کردن یک View به چند Sub View انجام دهید! فقط این‌ها را در چند User control جدا کنید و بعد مجددا به کمک فضای نامی که تعریف خواهد (مثلا V در اینجا) در همان View اصلی تعریف کنید. بدون هیچ تغییر خاصی باز هم برنامه همانند سابق کار خواهد کرد.


نظرات مطالب
بررسی وجود نام کاربر با استفاده از jQuery Ajax در ASP.Net
سلام
بله. برنامه reflector معروف که مد نظر هست. اسمبلی برنامه فوق را با آن باز کردم و زبان انتخابی هم VB . حاصل به صورت زیر نمایش داده شد:
Public Shared Function IsUserAvailable

یک سری کار سورس باز دیدم در CodePlex در مورد jQuery Ajax که تلاش‌هایی برای ساده‌تر کردنش کردند. بد نیست برای تکمیل کار یک سری هم به آن‌ها هم بزنید. برای ایده گرفتن خوب است.
مطالب دوره‌ها
نگاهی به SignalR Clients
در قسمت قبل موفق به ایجاد اولین Hub خود شدیم. در ادامه، برای تکمیل برنامه نیاز است تا کلاینتی را نیز برای آن تهیه کنیم.
مصرف کنندگان یک Hub می‌توانند انواع و اقسام برنامه‌های کلاینت مانند jQuery Clients و یا حتی یک برنامه کنسول ساده باشند و همچنین Hubهای دیگر نیز قابلیت استفاده از این امکانات Hubهای موجود را دارند. تیم SignalR امکان استفاده از Hubهای آن‌را در برنامه‌های دات نت 4 به بعد، برنامه‌های WinRT، ویندوز فون 8، سیلورلایت 5، jQuery و همچنین برنامه‌های CPP نیز مهیا کرده‌اند. به علاوه گروه‌های مختلف نیز با توجه به سورس باز بودن این مجموعه، کلاینت‌های iOS Native، iOS via Mono و Android via Mono را نیز به این لیست اضافه کرده‌اند.


بررسی کلاینت‌های jQuery

با توجه به پروتکل مبتنی بر JSON سیگنال‌آر، استفاده از آن در کتابخانه‌های جاوا اسکریپتی همانند jQuery نیز به سادگی مهیا است. برای نصب آن نیاز است در کنسول پاور شل نوگت، دستور زیر را صادر کنید:
 PM> Install-Package Microsoft.AspNet.SignalR.JS
برای نمونه به solution پروژه قبل، یک برنامه وب خالی دیگر را اضافه کرده و سپس دستور فوق را بر روی آن اجرا نمائید. در این حالت فقط باید دقت داشت که فرامین بر روی کدام پروژه اجرا می‌شوند:


با استفاده از افزونه SignalR jQuery، به دو طریق می‌توان به یک Hub اتصال برقرار کرد:
الف) استفاده از فایل proxy تولید شده آن (این فایل، در زمان اجرای برنامه تولید می‌شود و یا امکان استفاده از آن به کمک ابزارهای کمکی نیز وجود دارد)
نمونه‌ای از آن‌را در قسمت قبل ملاحظه کردید؛ همان فایل تولید شده در مسیر /signalr/hubs برنامه. به نوعی به آن Service contract نیز گفته می‌شود (ارائه متادیتا و قراردادهای کار با یک سرویس Hub). این فایل همانطور که عنوان شد به صورت پویا در زمان اجرای برنامه ایجاد می‌شود.
امکان تولید آن توسط برنامه کمکی signalr.exe نیز وجود دارد؛ برای دریافت آن می‌توان از طریق NuGet اقدام کرد (بسته Microsoft.AspNet.SignalR.Utils) که نهایتا در پوشه packages قرار خواهد گرفت. نحوه استفاده از آن نیز به صورت زیر است:
 Signalr.exe ghp http://localhost/
در این دستور ghp مخفف generate hub proxy است و نهایتا فایلی را به نام server.js تولید می‌کند.

ب) بدون استفاده از فایل proxy و به کمک روش late binding (انقیاد دیر هنگام)

برای کار با یک Hub از طریق jQuery مراحل ذیل باید طی شوند:
1) ارجاعی به Hub باید مشخص شود.
2) روال‌های رخدادگردان تنظیم گردند.
3) اتصال به Hub برقرار گردد.
4) متدی فراخوانی شود.

در اینجا باید دقت داشت که امکانات Hub به صورت خواص
 $.connection
در سمت کلاینت جی‌کوئری، در دسترس خواهند بود. برای مثال:
 $.connection.chatHub
و نام‌های بکارگرفته شده در اینجا مطابق روش‌های متداول نام گذاری در جاوا اسکریپت، camel case هستند.

خوب، تا اینجا فرض بر این است که یک پروژه خالی ASP.NET را آغاز و سپس فرمان نصب Microsoft.AspNet.SignalR.JS را نیز همانطور که عنوان شد، صادر کرده‌اید. در ادامه یک فایل ساده html را به نام chat.htm، به این پروژه جدید اضافه کنید (برای استفاده از کتابخانه جاوا اسکریپتی SignalR الزامی به استفاده از صفحات کامل پروژه‌های وب نیست).
<!DOCTYPE>
<html>
<head>
    <title></title>
    <script src="Scripts/jquery-1.6.4.min.js" type="text/javascript"></script>
    <script src="Scripts/jquery.signalR-1.0.1.min.js" type="text/javascript"></script>
    <script src="http://localhost:1072/signalr/hubs" type="text/javascript"></script>
</head>
<body>
    <div>
        <input id="txtMsg" type="text" /><input id="send" type="button" value="send msg" />
        <ul id="messages">
        </ul>
    </div>
    <script type="text/javascript">
        $(function () {
            var chat;
            $.connection.hub.logging = true; //اطلاعات بیشتری را در جاوا اسکریپت کنسول مرورگر لاگ می‌کند
            chat = $.connection.chat; //این نام مستعار پیشتر توسط ویژگی نام هاب تنظیم شده است
            chat.client.hello = function (message) {
                //متدی که در اینجا تعریف شده دقیقا مطابق نام متد پویایی است که در هاب تعریف شده است
                //به این ترتیب سرور می‌تواند کلاینت را فراخوانی کند
                $("#messages").append("<li>" + message + "</li>");
            };
            $.connection.hub.start(/*{ transport: 'longPolling' }*/); // فاز اولیه ارتباط را آغاز می‌کند

            $("#send").click(function () {
                // Hub's `SendMessage` should be camel case here
                chat.server.sendMessage($("#txtMsg").val());
            });
        });
    </script>
</body>
</html>
کدهای آن‌را به نحو فوق تغییر دهید.
توضیحات:
همانطور که ملاحظه می‌کنید ابتدا ارجاعاتی به jquery و jquery.signalR-1.0.1.min.js اضافه شده‌اند. سپس نیاز است مسیر دقیق فایل پروکسی هاب خود را نیز مشخص کنیم. اینکار با تعریف مسیر signalr/hubs انجام شده است.
<script src="http://localhost:1072/signalr/hubs" type="text/javascript"></script>
در ادامه توسط تنظیم connection.hub.logging سبب خواهیم شد تا اطلاعات بیشتری در javascript console مرورگر لاگ شود.
سپس ارجاعی به هاب تعریف شده، تعریف گردیده است. اگر از قسمت قبل به خاطر داشته باشید، توسط ویژگی HubName، نام chat را برگزیدیم. بنابراین connection.chat ذکر شده دقیقا به این هاب اشاره می‌کند.
سپس سطر chat.client.hello مقدار دهی شده است. متد hello، متدی dynamic و تعریف شده در سمت هاب برنامه است. به این ترتیب می‌توان به پیام‌های رسیده از طرف سرور گوش فرا داد. در اینجا، این پیام‌ها، به li ایی با id مساوی messages اضافه می‌شوند.
سپس توسط فراخوانی متد connection.hub.start، فاز negotiation شروع می‌شود. در اینجا حتی می‌توان نوع transport را نیز صریحا انتخاب کرد که نمونه‌ای از آن را به صورت کامنت شده جهت آشنایی با نحوه تعریف آن مشاهده می‌کنید. مقادیر قابل استفاده در آن به شرح زیر هستند:
 - webSockets
- forverFrame
- serverSentEvents
- longPolling
سپس به رویدادهای کلیک دکمه send گوش فرا داده و در این حین، اطلاعات TextBox ایی با id مساوی txtMsg را به متد SendMessage هاب خود ارسال می‌کنیم. همانطور که پیشتر نیز عنوان شد، در سمت کلاینت، تعریف متد SendMessage باید camel case باشد.

اکنون به صورت جداگانه یکبار برنامه hub را در مرورگر باز کنید. سپس بر روی فایل chat.htm کلیک راست کرده و گزینه مشاهده آن را در مرورگر نیز انتخاب نمائید (گزینه View in browser منوی کلیک راست).

خوب! پروژه کار نمی‌کند! برای اینکه مشکلات را بهتر بتوانید مشاهده کنید نیاز است به JavaScript Console مرورگر خود مراجعه نمائید. برای مثال در مرورگر کروم دکمه F12 را فشرده و برگه Console آن‌را باز کنید. در اینجا اعلام می‌کند که فاز negotiation قابل انجام نیست؛ چون مسیر پیش فرضی را که انتخاب کرده است، همین مسیر پروژه دومی است که اضافه کرده‌ایم (کلاینت ما در پروژه دوم قرار دارد و نه در همان پروژه اول هاب).
برای اینکه مسیر دقیق hub را در این حالت مشخص کنیم، سطر زیر را به ابتدای کدهای جاوا اسکریپتی فوق اضافه نمائید:
 $.connection.hub.url = 'http://localhost:1072/signalr'; //چون در یک پروژه دیگر قرار داریم
اکنون اگر مجدا سعی کنید، باز هم برنامه کار نمی‌کند و پیام می‌دهد که امکان دسترسی به این سرویس از خارج از دومین آن میسر نیست. برای اینکه این مجوز را صادر کنیم نیاز است تنظیمات مسیریابی پروژه هاب را به نحو ذیل ویرایش نمائیم:
using System;
using System.Web;
using System.Web.Routing;
using Microsoft.AspNet.SignalR;

namespace SignalR02
{
    public class Global : HttpApplication
    {
        protected void Application_Start(object sender, EventArgs e)
        {
            // Register the default hubs route: ~/signalr
            RouteTable.Routes.MapHubs(new HubConfiguration
            {
                EnableCrossDomain = true
            });
        }
    }
}
با تنظیم EnableCrossDomain به true اینبار فاز‌های آغاز ارتباط با سرور برقرار می‌شوند:
 SignalR: Auto detected cross domain url. jquery.signalR-1.0.1.min.js:10
SignalR: Negotiating with 'http://localhost:1072/signalr/negotiate'. jquery.signalR-1.0.1.min.js:10
SignalR: SignalR: Initializing long polling connection with server. jquery.signalR-1.0.1.min.js:10
SignalR: Attempting to connect to 'http://localhost:1072/signalr/connect?transport=longPolling&connectionToken…NRh72omzsPkKqhKw2&connectionData=%5B%7B%22name%22%3A%22chat%22%7D%5D&tid=3' using longPolling. jquery.signalR-1.0.1.min.js:10
SignalR: Longpolling connected jquery.signalR-1.0.1.min.js:10
مطابق این لاگ‌ها ابتدا فاز negotiation انجام می‌شود. سپس حالت long polling را به صورت خودکار انتخاب می‌کند.

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

یک نکته:
اگر از ویندوز 8 (یعنی IIS8) و VS 2012 استفاده می‌کنید، برای استفاده از حالت Web socket، ابتدا فایل وب کانفیگ برنامه را باز کرده و در قسمت httpRunTime، مقدار ویژگی targetFramework را بر روی 4.5 تنظیم کنید. اینبار اگر مراحل negotiation را بررسی کنید در همان مرحله اول برقراری اتصال، از روش Web socket استفاده گردیده است.


تمرین 1
به پروژه ساده و ابتدایی فوق یک تکست باکس دیگر به نام Room را اضافه کنید؛ به همراه دکمه join. سپس نکات قسمت قبل را در مورد الحاق به یک گروه و سپس ارسال پیام به اعضای گروه را پیاده سازی نمائید. (تمام نکات آن با مطلب فوق پوشش داده شده است و در اینجا باید صرفا فراخوانی متدهای عمومی دیگری در سمت هاب، صورت گیرد)

تمرین 2
در انتهای قسمت دوم به نحوه ارسال پیام از یک هاب به هابی دیگر اشاره شد. این MonitorHub را ایجاد کرده و همچنین یک کلاینت جاوا اسکریپتی را نیز برای آن تهیه کنید تا بتوان اتصال و قطع اتصال کلیه کاربران سیستم را مانیتور و مشاهده کرد.


پیاده سازی کلاینت jQuery بدون استفاده از کلاس Proxy

در مثال قبل، از پروکسی پویای مهیای در آدرس signalr/hubs استفاده کردیم. در اینجا قصد داریم، بدون استفاده از آن نیز کار برپایی کلاینت را بررسی کنیم.
بنابراین یک فایل جدید html را مثلا به نام chat_np.html به پروژه دوم برنامه اضافه کنید. سپس محتویات آن‌را به نحو زیر تغییر دهید:
<!DOCTYPE>
<html>
<head>
    <title></title>
    <script src="Scripts/jquery-1.6.4.min.js" type="text/javascript"></script>
    <script src="Scripts/jquery.signalR-1.0.1.min.js" type="text/javascript"></script>
</head>
<body>
    <div>
        <input id="txtMsg" type="text" /><input id="send" type="button" value="send msg" />
        <ul id="messages">
        </ul>
    </div>
    <script type="text/javascript">
        $(function () {                        
            $.connection.hub.logging = true; //اطلاعات بیشتری را در جاوا اسکریپت کنسول مرورگر لاگ می‌کند

            var connection = $.hubConnection();
            connection.url = 'http://localhost:1072/signalr'; //چون در یک پروژه دیگر قرار داریم
            var proxy = connection.createHubProxy('chat');

            proxy.on('hello', function (message) {
                //متدی که در اینجا تعریف شده دقیقا مطابق نام متد پویایی است که در هاب تعریف شده است
                //به این ترتیب سرور می‌تواند کلاینت را فراخوانی کند
                $("#messages").append("<li>" + message + "</li>");
            });

            $("#send").click(function () {
                // Hub's `SendMessage` should be camel case here
                proxy.invoke('sendMessage', $("#txtMsg").val());
            });

            connection.start();
        });
    </script>
</body>
</html>
در اینجا سطر مرتبط با تعریف مسیر اسکریپت‌های پویای signalr/hubs را دیگر در ابتدای فایل مشاهده نمی‌کنید. کار تشکیل proxy اینبار از طریق کدنویسی صورت گرفته است. پس از ایجاد پروکسی، برای گوش فرا دادن به متدهای فراخوانی شده از طرف سرور از متد proxy.on و نام متد فراخوانی شده سمت سرور استفاده می‌کنیم و یا برای ارسال اطلاعات به سرور از متد proxy.invoke به همراه نام متد سمت سرور استفاده خواهد شد.


کلاینت‌های دات نتی SignalR
تا کنون Solution ما حاوی یک پروژه Hub و یک پروژه وب کلاینت جی‌کوئری است. به همین Solution، یک پروژه کلاینت کنسول ویندوزی را نیز اضافه کنید.
سپس در خط فرمان پاور شل نوگت دستور زیر را صادر نمائید تا فایل‌های مورد نیاز به پروژه کنسول اضافه شوند:
 PM> Install-Package Microsoft.AspNet.SignalR.Client
در اینجا نیز باید دقت داشت تا دستور بر روی default project صحیحی اجرا شود (حالت پیش فرض، اولین پروژه موجود در solution است).
پس از نصب آن اگر به پوشه packages مراجعه کنید، نگارش‌های مختلف آن‌را مخصوص سیلورلایت، دات نت‌های 4 و 4.5، WinRT و ویندوز فون8 نیز می‌توانید در پوشه Microsoft.AspNet.SignalR.Client ملاحظه نمائید. البته در ابتدای نصب، انتخاب نگارش مناسب، بر اساس نوع پروژه جاری به صورت خودکار صورت می‌گیرد.
مدل برنامه نویسی آن نیز بسیار شبیه است به حالت عدم استفاده از پروکسی در حین استفاده از jQuery که در قسمت قبل بررسی گردید و شامل این مراحل است:
1) یک وهله از شیء HubConnection را ایجاد کنید.
2) پروکسی مورد نیاز را جهت اتصال به Hub از طریق متد CreateProxy تهیه کنید.
3) رویدادگردان‌ها را همانند نمونه کدهای جاوا اسکریپتی قسمت قبل، توسط متد On تعریف کنید.
4) به کمک متد Start، اتصال را آغاز نمائید.
5) متدها را به کمک متد Invoke فراخوانی نمائید.

using System;
using Microsoft.AspNet.SignalR.Client.Hubs;

namespace SignalR02.WinClient
{
    class Program
    {
        static void Main(string[] args)
        {
            var hubConnection = new HubConnection(url: "http://localhost:1072/signalr");
            var chat = hubConnection.CreateHubProxy(hubName: "chat");

            chat.On<string>("hello", msg => {
                Console.WriteLine(msg);
            });

            hubConnection.Start().Wait();

            chat.Invoke<string>("sendMessage", "Hello!");

            Console.WriteLine("Press a key to terminate the client...");
            Console.Read();
        }
    }
}
نمونه‌ای از این پیاده سازی را در کدهای فوق ملاحظه می‌کنید که از لحاظ طراحی آنچنان تفاوتی با نمونه ذهنی جاوا اسکریپتی ندارد.

نکته مهم
کلیه فراخوانی‌هایی که در اینجا ملاحظه می‌کنید غیرهمزمان هستند.
به همین جهت پس از متد Start، متد Wait ذکر شده‌است تا در این برنامه ساده، پس از برقراری کامل اتصال، کار invoke صورت گیرد و یا زمانیکه callback تعریف شده توسط متد chat.On فراخوانی می‌شود نیز این فراخوانی غیرهمزمان است و خصوصا اگر نیاز است رابط کاربری برنامه را در این بین به روز کنید باید به نکات به روز رسانی رابط کاربری از طریق یک ترد دیگر دقت داشت.
مطالب
آشنایی با M.A.F - قسمت دوم

قسمت قبل بیشتر آشنایی با یک سری از اصطلاحات مرتبط با فریم ورک MAF بود و همچنین نحوه‌ی کلی استفاده از آن. در این قسمت یک مثال ساده را با آن پیاده سازی خواهیم کرد و فرض قسمت دوم بر این است که افزونه‌ی Visual Studio Pipeline Builder را نیز نصب کرده‌اید.

یک نکته پیش از شروع:
- اگر افزونه‌ی Visual Studio Pipeline Builder پس از نصب به منوی Tools اضافه نشده است، یک پوشه‌ی جدید را به نام Addins در مسیر Documents\Visual Studio 2008 ایجاد کرده و سپس فایل‌های آن‌را در این مسیر کپی کنید.

ساختار اولیه یک پروژه MAF

- پروژ‌ه‌هایی که از MAF استفاده می‌کنند، نیاز به ارجاعاتی به دو اسمبلی استاندارد System.AddIn.dll و System.AddIn.Contract.dll دارند (مطابق شکل زیر):



- ساختار آغازین یک پروژه MAF از سه پروژه تشکیل می‌شود که توسط افزونه‌ی Visual Studio Pipeline Builder به 7 پروژه بسط خواهد یافت.
این سه پروژه استاندارد آغازین شامل موارد زیر هستند:



- هاست: همان برنامه‌ی اصلی که قرار است از افزونه استفاده کند.
- قرار داد: نحو‌ه‌ی تعامل هاست و افزونه در این پروژه تعریف می‌شود. (یک پروژه از نوع class library)
- افزونه: کار پیاده سازی قرار داد را عهده دار خواهد شد. (یک پروژه از نوع class library)

- همچنین مرسوم است جهت مدیریت بهتر خروجی‌های حاصل شده یک پوشه Output را نیز به این solution اضافه کنند:



اکنون با توجه به این محل خروجی، به خواص Build سه پروژه موجود مراجعه کرده و مسیر Build را اندکی اصلاح خواهیم کرد (هر سه مورد بهتر است اصلاح شوند)، برای مثال:



نکته‌ی مهم هم اینجا است که خروجی host باید به ریشه این پوشه تنظیم شود و سایر پروژه‌ها هر کدام خروجی خاص خود را در پوشه‌ای داخل این ریشه باید ایجاد کنند.



تا اینجا قالب اصلی کار آماده شده است. قرارداد ما هم به شکل زیر است (ویژگی AddInContract آن نیز نباید فراموش شود):

using System.AddIn.Pipeline;
using System.AddIn.Contract;

namespace CalculatorConract
{
[AddInContract]
public interface ICalculatorContract : IContract
{
double Operate(string operation, double a, double b);
}
}

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

ایجاد pipeline

اگر قسمت قبل را مطالعه کرده باشید، یک راه حل مبتنی بر MAF از 7 پروژه تشکیل می‌شود که عمده‌ترین خاصیت آن‌ها مقاوم کردن سیستم در مقابل تغییرات نگارش قرارداد است. در این حالت اگر قرار داد تغییر کند، نه هاست و نه افزونه‌ی قدیمی، نیازی به تغییر در کدهای خود نخواهند داشت و این پروژه‌های میانی هستند که کار وفق دادن (adapters) نهایی را برعهده می‌گیرند.


برای ایجاد خودکار View ها و همچنین Adapters ، از افزونه‌ی Visual Studio Pipeline Builder که پیشتر معرفی شد استفاده خواهیم کرد.



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


پس از ایجاد این پروژه‌ها، نیاز به اصلاحات مختصری در مورد نام اسمبلی و فضای نام هر کدام می‌باشد؛ زیرا به صورت پیش فرض هر کدام به نام template نامگذاری شده‌اند:



پیاده سازی افزونه

قالب کاری استفاده از این فریم ورک آماده است. اکنون نوبت به پیاده سازی یک افزونه می‌باشد. به پروژه AddIn مراجعه کرده و ارجاعی را به اسمبلی AddInView خواهیم افزود. به این صورت افزونه‌ی ما به صورت مستقیم با قرارداد سروکار نداشته و ارتباطات، در راستای همان pipeline تعریف شده، جهت مقاوم شدن در برابر تغییرات صورت می‌گیرد:
using System;
using CalculatorConract.AddInViews;
using System.AddIn;

namespace CalculatorAddIn
{
[AddIn]
public class MyCalculatorAddIn : ICalculator
{
public double Operate(string operation, double a, double b)
{
throw new NotImplementedException();
}
}
}

در اینجا افزونه‌ی ما باید اینترفیس ICalculator مربوط به AddInView را پیاده سازی نماید که برای مثال خواهیم داشت:

using System;
using CalculatorConract.AddInViews;
using System.AddIn;

namespace CalculatorAddIn
{
[AddIn("افزونه یک", Description = "توضیحات", Publisher = "نویسنده", Version = "نگارش یک")]
public class MyCalculatorAddIn : ICalculator
{
public double Operate(string operation, double a, double b)
{
switch (operation)
{
case "+":
return a + b;
case "-":
return a - b;
case "*":
return a * b;
default:
throw new NotSupportedException("عملیات مورد نظر توسط این افزونه پشتیبانی نمی‌شود");
}
}
}
}

همانطور که در قسمت قبل نیز ذکر شد، این کلاس باید با ویژگی AddIn مزین شود که توسط آن می‌توان توضیحاتی در مورد نام ، نویسنده و نگارش افزونه ارائه داد.


استفاده از افزونه‌ی تولید شده

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

نکته‌ی مهم!
در هر دو ارجاع به HostView و یا AddInView باید خاصیت Copy to local به false تنظیم شود، در غیر اینصورت افزونه‌ی شما بارگذاری نخواهد شد.



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

using System;
using System.AddIn.Hosting;
using CalculatorConract.HostViews;

namespace Calculator
{
class Program
{
private static ICalculator _calculator;

static void doOperation()
{
Console.WriteLine("1+2: {0}", _calculator.Operate("+", 1, 2));
}

static void Main(string[] args)
{
//مسیر پوشه ریشه مربوطه به خط لوله افزونه‌ها
string path = Environment.CurrentDirectory;

//مشخص سازی مسیر خواندن و کش کردن افزونه‌ها
AddInStore.Update(path);

//یافتن افزونه‌هایی سازگار با شرایط قرارداد پروژه
//در اینجا هیچ افزونه‌ای بارگذاری نمی‌شود
var addIns = AddInStore.FindAddIns(typeof(ICalculator), path);

//اگر افزونه‌ای یافت شد
if (addIns.Count > 0)
{
var addIn = addIns[0]; //استفاده از اولین افزونه
Console.WriteLine("1st addIn: {0}", addIn.Name);

//فعال سازی افزونه و همچنین مشخص سازی سطح دسترسی آن
_calculator = addIn.Activate<ICalculator>(AddInSecurityLevel.Intranet);

//یک نمونه از استفاده آن
doOperation();
}

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}

چند نکته جالب توجه در مورد قابلیت‌های ارائه شده:
- مدیریت load و unload پویا
- امکان تعریف سطح دسترسی و ویژگی‌های امنیتی اجرای یک افزونه
- امکان ایزوله سازی پروسه اجرای افزونه از هاست (در ادامه توضیح داده خواهد شد)
- مقاوم بودن پروژه به نگارش‌های مختلف قرارداد


اجرای افزونه در یک پروسه مجزا

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

//فعال سازی افزونه و همچنین مشخص سازی سطح دسترسی آن
//همچنین جدا سازی پروسه اجرایی افزونه از هاست
_calculator = addIn.Activate<ICalculator>(
new AddInProcess(),
AddInSecurityLevel.Intranet);

در این حالت اگر پس از فعال شدن افزونه، یک break point قرار دهیم و به task manager ویندوز مراجعه نمائیم، پروسه‌ی مجزای افزونه قابل مشاهده است.



برای مطالعه بیشتر + ، + ، + و +

مطالب
آموزش PouchDB : قسمت اول (معرفی)

آموزش PouchDB : معرفی

هدف این مقاله بر این است که شما را با دیتابیس PouchDB  آشنا سازد .

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

فهرست مطالب :

  • بخش اول : معرفی PouchDB
  • شروع به کار با PouchDB
  • نحوه استفاده از API ها
  • سوالات متداول در مورد PouchDB
  • خطاهای احتمالی
  • پروژه‌ها و پلاگین های PouchDB


PouchDB یک دیتابیس NoSQL می‌باشد که به وسیله Javascript نوشته شده و هدف آن این است که برنامه نویس‌ها بتوانند برنامه‌هایی را توسعه و ارائه کنند که بتواند هم به صورت آفلاین و هم آنلاین سرویس دهی داشته باشند.
برنامه اطلاعات خودش را به صورت آفلاین ذخیره می‌کند و کاربر می‌تواند زمانیکه به اینترنت متصل نیست، از آنها استفاده کند. اما به محض اتصال به اینترنت، دیتابیس خودش را با دیتابیس آنلاین همگام (Sync) می‌کند. اینجاست که قدرت اصلی PouchDB مشخص می‌شود.
بزرگترین برتری  PouchDB همین است. دیتابیسی است که به صورت توکار قابلیت‌های همگام سازی را دارا می‌باشد و به صورت اتوماتیک این کار را انجام می‌دهد.
PouchDB یک پروژه‌ی اوپن سورس است که توسط  این افراد به روز می‌شود. البته باید گفت که PouchDB  از CouchDB الهام گرفته شده است. اگر شما هم قصد همکاری در این پروژه را دارید بهتر است که  راهنمای همکاری  را مطالعه کنید .



پشتیبانی مرورگرها

PouchDB پیش زمینه‌های مختلفی دارد که به آن این امکان را می‌دهد تا روی همه مرورگر‌ها و صد البته روی NodeJs کار کند. از IndexedDB بر روی Firefox/Chrome/Opera/IE و WebSql بر روی Safari و همچنین LevelDB بر روی NodeJs استفاده می‌کند.
در حال حاظر PouchDB  بر روی مرورگرهای زیر تست شده است:
  • فایرفاکس 12 و بالاتر
  • گوگل کروم 19 و بالاتر
  • اپرا 12 و بالاتر
  • سافاری 5 و بالاتر
  • اینترنت اکسپلورر 10 و بالاتر
  • NodeJs 0.10 و بالاتر
  • و به صورت شگفت انگیزی در Apache Cordova
برای اطلاعات بیشتر در مورد مرورگرهایی که IndexdDB و WebSql را پشتیبانی می‌کنند به لینک‌های زیر مراجعه کنید:
نکته : در صورتی که برنامه شما نیاز دارد تا از اینترنت اکسپلورر نسخه پایینتر از 10 استفاده کند می‌توانید از دیتابیس‌های آنلاین استفاده کنید، که البته دیگر قابلیت استفاده آفلاین را نخواهد داشت.



وضعیت فعلی PouchDB

PouchDB برای مرورگر، فعلا در وضعیت بتا به سر می‌برد و به صورت فعالی در حال گذراندن تست هایی می‌باشد تا باگ‌های آن برطرف شود و به صورت پایدار (Stable ) ارائه گردد. البته فقط ممکن است که شما باگی را در قسمت Api‌ها پیدا کنید که البته Api‌ها هم در حال حاضر پایدار هستند و گزارشی مبنی بر باگ در آن‌ها موجود نیست. اگر هم باگی پیدا بشود شما می‌توانید PouchDB  را بدون ریسک از دست رفتن اطلاعات آپگرید کنید.
PouchDB برای NodeJs فعلا در وضعیت آلفا است و آپگرید کردن ممکن است به اطلاعات شما آسیب بزند. البته با آپدیت به صورت دستی خطری شما را تهدید نخواهد کرد .



نحوه‌ی نصب PouchDB

PouchDB به صورت یک کتابخانه‌ی کوچک و جمع و جور طراحی شده است تا بتواند همه نیاز‌ها را برطرف و روی همه نوع Device اعم از موبایل، تبلت، مرورگر و کلا هر چیزی که جاوا اسکریپت را ساپورت می‌کند کار خود را به خوبی انجام بدهد.
برای استفاده از PouchDB میبایست این فایل را با حجم فوق العاده 97 کیلوبایت دانلود کنید  و آن را به یک صفحه وب اضافه کنید :
<script src="pouchdb-2.1.0.min.js"></script>

آخرین نسخه و بهترین نسخه : pouchdb-2.1.0.min.js
برای اطلاع از آخرین آپدیتها و نسخه‌ها به این صفحه در گیت هاب مراجعه کنید .
برای کسانی هم که از NodeJS استفاده می‌کنند نحوه نصب به این صورت است :
$ npm install pouchdb

مطالب
شروع به کار با DNTFrameworkCore - قسمت 3 - پیاده‌سازی سرویس‌های موجودیت‌ها
در قسمت قبل سناریوهای مختلف مرتبط با طراحی موجودیت‌های سیستم را بررسی کردیم. در این قسمت به طراحی DTO‌های متناظر با موجودیت‌ها به همراه اعتبارسنج‌های مرتبط و در نهایت به پیاده سازی سرویس‌های CRUD آنها خواهیم پرداخت. 
قراردادها، مفاهیم و نکات اولیه
  1. برخلاف بسیاری از طراحی‌های موجود، بر فراز هر موجودیت اصلی (منظور AggregateRoot) باید یک DTO که از این پس با عنوان Model از آنها یاد خواهیم کرد، تعریف شود. 
  2. هیچ تراکنشی برای موجودیت‌های فرعی یا همان Detailها نخواهیم داشت. این موجودیت‌ها در تراکنش موجودیت اصلی مرتبط به آن مدیریت خواهند شد.
  3. هر Commandای که قرار است مرتبط با یک موجودیت اصلی در سیستم انجام پذیرد، باید از منطق تجاری آن موجودیت عبور کند و نباید با دور زدن منطق تجاری، از طرق مختلف تغییراتی بر آن موجودیت اعمال شود. (موضوع مهمی که در ادامه مطلب جاری تشریح خواهد شد)
  4. ویوهای مختلفی از یک موجودیت می‌توان انتظار داشت که ویو پیش‌فرض آن در CrudService تدارک دیده شده است. برای سایر موارد نیاز است در سرویس مرتبط، متدهای Read مختلفی را پیاده‌سازی کنید.
  5. با اعمال اصل CQS، متدهای ثبت و ویرایش در کلاس سرویس پایه CRUD، بعد از انجام عملیات مربوطه، Id و RowVersion مدل ورودی و هچنین Id و TrackingState موجودیت‌های فرعی وابسته، مقداردهی خواهند شد و نیاز به انجام یک Query دیگر و بازگشت آن به عنوان خروجی متدها نبوده است. به همین دلیل خروجی این متدها صرفا Result ای می‌باشد که نشان از امکان Failure بودن انجام آنها می‌باشد که با اصل مذکور در تضاد نمی‌باشد.
  6. ورودی متدهای Read شما که در اکثر موارد نیاز به مهیا کردن خروجی صفحه‌بندی شده دارند، باید از نوع PagedQueryModel و یا اگر همچنین نیاز به جستجوی پویا براساس فیلدهایی موجود در ReadModel مرتبط دارید، باید از نوع FilteredPagedQueryModel باشد. متدهای الحاقی برای اعمال خودکار این صفحه‌‌بندی و جستجوی پویا در نظر گرفته شده است. همچنین خروجی آنها در اکثر موارد از نوع IPagedQueryResult خواهد بود. اگر نیاز است تا جستجوی خاصی داشته باشید که خصوصیتی متناظر با آن فیلد در مدل Read وجود ندارد، لازم است تا از این QueryModel‌های مطرح شده، ارث‌بری کرده و خصوصیت اضافی مدنظر خود را تعریف کنید. بدیهی است که اعمال جستجوی این موارد خاص به عهده توسعه دهنده می‌باشد.
  7. عملیات ثبت، ویرایش و حذف، برای کار بر روی لیستی از وهله‌های Model، طراحی شده‌اند. این موضوع در بسیاری از دومین‌ها قابلیت مورد توجهی می‌باشد. 
  8. رخداد متناظر با عملیات CUD مرتبط با هر موجودیت اصلی، به عنوان یکسری نقاط قابل گسترش (Extensibility Point) در اختیار سایر بخش‌های سیستم می‌باشد. این رخدادها درون تراکنش جاری Raise خواهند شد؛ از این جهت امکان اعمال یکسری Rule جدید از سمت سایر موءلفه‌های سیستم موجود می‌باشد.
  9. برخلاف بسیاری از طراحی‌های موجود، قصد ایجاد لایه انتزاعی برفراز EF Core  به منظور رسیدن به Persistence Ignorance را ندارم. بنابراین امروز بسته DNTFrameworkCore.EntityFramework آن آماده می‌باشد. اگر توسعه دهنده‌ای قصد یکپارچه کردن این زیرساخت را با سایر ORMها یا Micro ORMها داشته باشد، می‌تواند Pull Request خود را ارسال کند.
  10. خبر خوب اینکه هیچ وابستگی به AutoMapper به منظور نگاشت مابین موجودیت‌ها و مدل‌های متناظر آنها، در این زیرساخت وجود ندارد. با پیاده سازی متدهای MapToModel و MapToEntity می‌توانید از کتابخانه Mapper مورد نظر خودتان استفاده کنید؛ یا به صورت دستی این کار را انجام دهید. بعد از چند سال استفاده از AutoMapper، این روزها خیلی اعتقادی به استفاده از آن ندارم.
  11. هیچ وابستگی به FluentValidation به منظور اعتبارسنجی ورودی متدها یا پیاده‌سازی قواعد تجاری، در این زیرساخت وجود ندارد. شما امکان استفاده از Attributeهای اعتبارسنجی توکار، پیاده سازی IValidatableObject توسط مدل یا در موارد خاص به منظور پیاده سازی قواعد تجاری پیچیده، پیاده سازی IModelValidator را دارید. با این حال برای یکپارچگی با این کتابخانه محبوب، می‌توانید بسته نیوگت DNTFrameworkCore.FluentValidation را نصب کرده و استفاده کنید.
  12. با اعمال الگوی Template Method در پیاده سازی سرویس CRUD پایه، از طریق تعدادی متد با پیشوندهای Before و After متناظر با عملیات CUD می‌توانید در فرآیند انجام آنها نیز دخالت داشته باشید؛ به عنوان مثال: BeforeEditAsync یا AfterCreateAsync
  13. باتوجه به اینکه در فرآیند انجام متدهای CUD، یکسری Event هم Raise خواهند شد و همچنین در خیلی از موراد شاید نیاز به فراخوانی SaveChange مرتبط با UnitOfWork جاری باشد، لذا مطمئن‌ترین راه حل برای این قضیه و حفظ ثبات سیستم، همان استفاده از تراکنش محیطی می‌باشد. از این جهت متدهای مذکور با TransactionAttribute نیز تزئین شده‌اند که برای فعال سازی این مکانیزم نیاز است تا TransactionInterceptor مربوطه را به سیستم معرفی کنید.
  14. ValidationInterceptor موجود در زیرساخت، در صورتیکه خروجی متد از نوع Result باشد، خطاهای ممکن را در قالب یک شی Result بازگشت خواهد داد؛ در غیر این صورت یک استثنای ValidationException پرتاب می‌شود که این مورد هم توسط GlobalExceptionFilter مدیریت خواهند شد و در قالب یک BadRequest به کلاینت ارسال خواهد شد.
  15. در سناریوهای Master-Detail، قرارداد این است که Detailها به همراه Master متناظر واکشی خواهند شد و در زمان ثبت و یا ویرایش هم همه آنها به همراه Master متناظر خود به سرور ارسال خواهند شد. 
نکته مهم:  همانطور که اشاره شد، در سناریوهای Master-Detail باید تمامی Detailها به سمت سرور ارسال شوند. در این صورت سناریویی را در نظر بگیرید که قرار است کاربر در front-office سیستم امکان حذف یک قلم از اقلام فاکتور را داشته باشد؛ این درحالی است که در back-office و در منطق تجاری اصلی، ما جایی برای حذف یک تک قلم ندیده‌ایم و کلا منطق و قواعد تجاری حاکم بر فاکتور را زیر سوال می‌برد. چرا که ممکن است یکسری قواعد تجاری متناسب با دومین، بر روی لیست اقلام یک فاکتور در زمان ذخیره سازی وجود داشته باشند که با حذف یک تک قلم از یک مسیر فرعی، کل فاکتور را در حالت نامعتبری برای ذخیره سازی‌های بعدی قرار دهد. در این موارد باید API شما یک DTO سفارشی را دریافت کند که شامل شناسه قلم فاکتور و شناسه فاکتور می‌باشد. سپس با استفاده از شناسه فاکتور و سرویس متناظر، آن را واکشی کرده و از لیست قلم‌های InvoiceModel، آن قلم را با TrackingState.Deleted علامت‌گذاری کنید. همچنین باید توجه داشته باشید که برروی فیلدهای موجود در جداول مرتبط با موجودیت‌های Detail، محدودیت‌های دیتابیسی از جمله Unique Constraint و ... را اعمال نکنید؛ مگر اینکه میدانید و دقیقا مطمئن باشید عملیات حذف اقلام، قبل از عملیات ثبت اقلام جدید رخ می‎دهد (این موضوع نیاز به توضیح و شبیه سازی شرایط خاص آن را دارد که در صورت نیاز می‌توان در مطلب جدایی به آن پرداخت).
‌پیاده سازی و بررسی تعدادی سرویس فرضی
برای شروع لازم است بسته‌های نیوگت زیر را نصب کنید:
PM> Install-Package DNTFrameworkCore -Version 1.0.0
PM> Install-Package DNTFrameworkCore.EntityFramework -Version 1.0.0

مثال اول: پیاده‌سازی سرویس یک موجودیت ساده بدون نیاز به ReadModel 
گام اول: طراحی Model متناظر
[LocalizationResource(Name = "SharedResource", Location = "DNTFrameworkCore.TestAPI")]
public class BlogModel : MasterModel<int>, IValidatableObject
{
    public string Title { get; set; }

    [MaxLength(50, ErrorMessage = "Maximum length is 50")]
    public string Url { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (Title == "BlogTitle")
        {
            yield return new ValidationResult("IValidatableObject Message", new[] {nameof(Title)});
        }
    }
}
مدل متناظر با موجودیت‌های اصلی باید از کلاس جنریک MasterModel ارث‌بری کرده باشد. همانطور که ملاحظه می‌کنید، برای نشان دادن مکانیزم اعتبارسنجی، از DataAnnotationها و IValidatableObject استفاده شده‌است. LocalizationResource برای مشخص کردن نام و محل فایل Resource متناظر برای خواندن پیغام‌های اعتبارسنجی استفاده می‌شود. این مورد برای سناریوهای ماژولار و کامپوننت محور بیشتر می‌تواند مدنظر باشد. 
گام دوم: پیاده‌سازی اعتبارسنج مستقل
در صورت نیاز به اعتبارسنجی پیچیده برای مدل متناظر، می‌توانید با استفاده از دو روش زیر به این هدف برسید:
1- استفاده از کتابخانه DNTFrameworkCore.FluentValidation
public class BlogValidator : FluentModelValidator<BlogModel>
{
    public BlogValidator(IMessageLocalizer localizer)
    {
        RuleFor(b => b.Title).NotEmpty()
            .WithMessage(localizer["Blog.Fields.Title.Required"]);
    }
}
2- پیاده‌سازی IModelValidator یا ارث‌بری از کلاس ModelValidator پایه
public class BlogValidator : ModelValidator<BlogkModel>
{
    public override IEnumerable<ModelValidationResult> Validate(BlogModel model)
    {
        yield return new ModelValidationResult(nameof(BlogkModel.Title), "Validation from IModelValidator");
    }
}

گام سوم: پیاده‌سازی سرویس متناظر
public interface IBlogService : ICrudService<int, BlogModel>
{
}
پیاده سازی واسط بالا
public class BlogService : CrudService<Blog, int, BlogModel>, IBlogService
{
    public BlogService(CrudServiceDependency dependency) : base(dependency)
    {
    }

    protected override IQueryable<BlogModel> BuildReadQuery(FilteredPagedQueryModel model)
    {
        return EntitySet.AsNoTracking().Select(b => new BlogModel
            {Id = b.Id, RowVersion = b.RowVersion, Url = b.Url, Title = b.Title});
    }

    protected override Blog MapToEntity(BlogModel model)
    {
        return new Blog
        {
            Id = model.Id,
            RowVersion = model.RowVersion,
            Url = model.Url,
            Title = model.Title,
            NormalizedTitle = model.Title.ToUpperInvariant() //todo: normalize based on your requirement 
        };
    }

    protected override BlogModel MapToModel(Blog entity)
    {
        return new BlogModel
        {
            Id = entity.Id,
            RowVersion = entity.RowVersion,
            Url = entity.Url,
            Title = entity.Title
        };
    }
}
برای این چنین موجودیت‌هایی، بازنویسی همین 3 متد کفایت می‌کند؛ دو متد MapToModel و MapToEntity برای نگاشت مابین مدل و موجودیت مورد نظر و متد BuildReadQuery نیز برای تعیین نحوه ساخت کوئری ReadPagedListAsync پیش‌فرض موجود در CrudService به عنوان متد Read پیش‌فرض این موجودیت. باکمترین مقدار کدنویسی و با کیفیت قابل قبول، عملیات CRUD یک موجودیت ساده، تکمیل شد. 
مثال دوم: پیاده سازی سرویس یک موجودیت ساده با ReadModel و  FilteredPagedQueryModel متمایز
گام اول: طراحی Model متناظر
[LocalizationResource(Name = "SharedResource", Location = "DNTFrameworkCore.TestAPI")]
public class TaskModel : MasterModel<int>, IValidatableObject
{
    public string Title { get; set; }

    [MaxLength(50, ErrorMessage = "Validation from DataAnnotations")]
    public string Number { get; set; }

    public string Description { get; set; }
    public TaskState State { get; set; } = TaskState.Todo;

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (Title == "IValidatableObject")
        {
            yield return new ValidationResult("Validation from IValidatableObject");
        }
    }
}
public class TaskReadModel : MasterModel<int>
{
    public string Title { get; set; }
    public string Number { get; set; }
    public TaskState State { get; set; } = TaskState.Todo;
    public DateTimeOffset CreationDateTime { get; set; }
    public string CreatorUserDisplayName { get; set; }
}
به عنوان مثال خصوصیاتی برای نمایش داریم که در زمان ثبت و ویرایش، انتظار دریافت آنها را از کاربر نیز نداریم. 
گام دوم: پیاده‌سازی اعتبارسنج  مستقل 
public class TaskValidator : ModelValidator<TaskModel>
{
    public override IEnumerable<ModelValidationResult> Validate(TaskModel model)
    {
        if (!Enum.IsDefined(typeof(TaskState), model.State))
        {
            yield return new ModelValidationResult(nameof(TaskModel.State), "Validation from IModelValidator");
        }
    }
}
 گام سوم: پیاده‌سازی سرویس متناظر
public interface ITaskService : ICrudService<int, TaskReadModel, TaskModel, TaskFilteredPagedQueryModel>
{
}
همانطور که ملاحظه می‌کنید، از ICrudService استفاده شده است که امکان تعیین نوع پارامتر جنریک TReadModel و TFilteredPagedQueryModel را هم دارد.
مدل جستجو و صفحه‌بندی سفارشی 
public class TaskFilteredPagedQueryModel : FilteredPagedQueryModel
{
    public TaskState? State { get; set; }
}


پیاده سازی واسط ITaskService با استفاده از AutoMapper

public class TaskService : CrudService<Task, int, TaskReadModel, TaskModel, TaskFilteredPagedQueryModel>,
  ITaskService
{
    private readonly IMapper _mapper;

    public TaskService(CrudServiceDependency dependency, IMapper mapper) : base(dependency)
    {
        _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
    }

    protected override IQueryable<TaskReadModel> BuildReadQuery(TaskFilteredPagedQueryModel model)
    {
        return EntitySet.AsNoTracking()
                    .WhereIf(model.State.HasValue, t => t.State == model.State)
                    .ProjectTo<TaskReadModel>(_mapper.ConfigurationProvider);
    }

    protected override Task MapToEntity(TaskModel model)
    {
        return _mapper.Map<Task>(model);
    }

    protected override TaskModel MapToModel(Task entity)
    {
        return _mapper.Map<TaskModel>(entity);
    }
}

به عنوان مثال در کلاس بالا برای نگاشت مابین مدل و موجودیت، از واسط IMapper کتابخانه AutoMapper استفاده شده‌است و همچنین عملیات جستجوی سفارشی در همان متد BuildReadQuery برای تولید کوئری متد Read پیش‌فرض، قابل ملاحظه می‌باشد.

مثال سوم: پیاده‌سازی سرویس یک موجودیت اصلی به همراه تعدادی موجودیت فرعی وابسته (سناریوهای Master-Detail) 

گام اول: طراحی Modelهای متناظر

    public class UserModel : MasterModel
    {
        public string UserName { get; set; }
        public string DisplayName { get; set; }
        public string Password { get; set; }
        public bool IsActive { get; set; }
        public ICollection<UserRoleModel> Roles { get; set; } = new HashSet<UserRoleModel>();
        public ICollection<PermissionModel> Permissions { get; set; } = new HashSet<PermissionModel>();
        public ICollection<PermissionModel> IgnoredPermissions { get; set; } = new HashSet<PermissionModel>();
    }

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

public class PermissionModel : DetailModel<int>
{
    public string Name { get; set; }
}

به عنوان مثال PermissionModel بالا از DetailModel جنریک‌ای ارث‌بری کرده است که دارای Id و TrackingState نیز می‌باشد. 

public class UserRoleModel : DetailModel<int>
{
    public long RoleId { get; set; }
}

شاید در نگاه اول برای گروه‌های کاربری یک کاربر کافی بود تا یک لیست ساده از long را از کلاینت دریافت کنیم. در این صورت نیاز است تا برای تمام موجودیت‎های سیستم که چنین شرایط مشابهی را دارند، عملیات ثبت، ویرایش و حذف متناظر با تک تک Detailها را دستی مدیریت کنید. روش فعلی خصوصا برای سناریوهای منفصل به مانند پروژه‌های تحت وب، پیشنهاد می‌شود.

گام دوم: پیاده سازی اعتبارسنج مستقل

public class UserValidator : FluentModelValidator<UserModel>
{
    private readonly IUnitOfWork _uow;

    public UserValidator(IUnitOfWork uow, IMessageLocalizer localizer)
    {
        _uow = uow ?? throw new ArgumentNullException(nameof(uow));

        RuleFor(m => m.DisplayName).NotEmpty()
            .WithMessage(localizer["User.Fields.DisplayName.Required"])
            .MinimumLength(3)
            .WithMessage(localizer["User.Fields.DisplayName.MinimumLength"])
            .MaximumLength(User.MaxDisplayNameLength)
            .WithMessage(localizer["User.Fields.DisplayName.MaximumLength"])
            .Matches(@"^[\u0600-\u06FF,\u0590-\u05FF,0-9\s]*$")
            .WithMessage(localizer["User.Fields.DisplayName.RegularExpression"])
            .DependentRules(() =>
            {
                RuleFor(m => m).Must(model =>
                     !CheckDuplicateDisplayName(model.DisplayName, model.Id))
                    .WithMessage(localizer["User.Fields.DisplayName.Unique"])
                    .OverridePropertyName(nameof(UserModel.DisplayName));
            });

        RuleFor(m => m.UserName).NotEmpty()
            .WithMessage(localizer["User.Fields.UserName.Required"])
            .MinimumLength(3)
            .WithMessage(localizer["User.Fields.UserName.MinimumLength"])
            .MaximumLength(User.MaxUserNameLength)
            .WithMessage(localizer["User.Fields.UserName.MaximumLength"])
            .Matches("^[a-zA-Z0-9_]*$")
            .WithMessage(localizer["User.Fields.UserName.RegularExpression"])
            .DependentRules(() =>
            {
                RuleFor(m => m).Must(model =>
                     !CheckDuplicateUserName(model.UserName, model.Id))
                    .WithMessage(localizer["User.Fields.UserName.Unique"])
                    .OverridePropertyName(nameof(UserModel.UserName));
            });

        RuleFor(m => m.Password).NotEmpty()
            .WithMessage(localizer["User.Fields.Password.Required"])
            .When(m => m.IsNew, ApplyConditionTo.CurrentValidator)
            .MinimumLength(6)
            .WithMessage(localizer["User.Fields.Password.MinimumLength"])
            .MaximumLength(User.MaxPasswordLength)
            .WithMessage(localizer["User.Fields.Password.MaximumLength"]);

        RuleFor(m => m).Must(model => !CheckDuplicateRoles(model))
            .WithMessage(localizer["User.Fields.Roles.Unique"])
            .When(m => m.Roles != null && m.Roles.Any(r => !r.IsDeleted));
    }

    private bool CheckDuplicateUserName(string userName, long id)
    {
        var normalizedUserName = userName.ToUpperInvariant();
        return _uow.Set<User>().Any(u => u.NormalizedUserName == normalizedUserName && u.Id != id);
    }

    private bool CheckDuplicateDisplayName(string displayName, long id)
    {
        var normalizedDisplayName = displayName.NormalizePersianTitle();
        return _uow.Set<User>().Any(u => u.NormalizedDisplayName == normalizedDisplayName && u.Id != id);
    }

    private bool CheckDuplicateRoles(UserModel model)
    {
        var roles = model.Roles.Where(a => !a.IsDeleted);
        return roles.GroupBy(r => r.RoleId).Any(r => r.Count() > 1);
    }
}

به عنوان مثال در این اعتبارسنج بالا، قواعدی از جمله بررسی تکراری بودن نام‌کاربری و از این دست اعتبارسنجی‌ها نیز انجام شده است. نکته حائز اهمیت آن متد CheckDuplicateRoles می‌باشد:

private bool CheckDuplicateRoles(UserModel model)
{
    var roles = model.Roles.Where(a => !a.IsDeleted);
    return roles.GroupBy(r => r.RoleId).Any(r => r.Count() > 1);
}

با توجه به «نکته مهم» ابتدای بحث، model.Roles، شامل تمام گروه‌های کاربری متصل شده به کاربر می‌باشند که در این لیست برخی از آنها با TrackingState.Deleted، برخی دیگر با TrackingState.Added و ... علامت‌گذاری شده‌اند. لذا برای بررسی یکتایی و عدم تکرار در این سناریوها نیاز به اجری پرس‌و‌جویی بر روی دیتابیس نمی‌باشد. بدین منظور، با اعمال یک شرط، گروه‌های حذف شده را از بررسی خارج کرده‌ایم؛ چرا که آنها بعد از عبور از منطق تجاری، حذف خواهند شد. 


گام سوم: پیاده‌سازی سرویس متناظر

public interface IUserService : ICrudService<long, UserReadModel, UserModel>
{
}
public class UserService : CrudService<User, long, UserReadModel, UserModel>, IUserService
{
    private readonly IUserManager _manager;

    public UserService(CrudServiceDependency dependency, IUserManager manager) : base(dependency)
    {
        _manager = manager ?? throw new ArgumentNullException(nameof(manager));
    }

    protected override IQueryable<User> BuildFindQuery()
    {
        return base.BuildFindQuery()
            .Include(u => u.Roles)
            .Include(u => u.Permissions);
    }

    protected override IQueryable<UserReadModel> BuildReadQuery(FilteredPagedQueryModel model)
    {
        return EntitySet.AsNoTracking().Select(u => new UserReadModel
        {
            Id = u.Id,
            RowVersion = u.RowVersion,
            IsActive = u.IsActive,
            UserName = u.UserName,
            DisplayName = u.DisplayName,
            LastLoggedInDateTime = u.LastLoggedInDateTime
        });
    }

    protected override User MapToEntity(UserModel model)
    {
        return new User
        {
            Id = model.Id,
            RowVersion = model.RowVersion,
            IsActive = model.IsActive,
            DisplayName = model.DisplayName,
            UserName = model.UserName,
            NormalizedUserName = model.UserName.ToUpperInvariant(),
            NormalizedDisplayName = model.DisplayName.NormalizePersianTitle(),
            Roles = model.Roles.Select(r => new UserRole
                {Id = r.Id, RoleId = r.RoleId, TrackingState = r.TrackingState}).ToList(),
            Permissions = model.Permissions.Select(p => new UserPermission
            {
                Id = p.Id,
                TrackingState = p.TrackingState,
                IsGranted = true,
                Name = p.Name
            }).Union(model.IgnoredPermissions.Select(p => new UserPermission
            {
                Id = p.Id,
                TrackingState = p.TrackingState,
                IsGranted = false,
                Name = p.Name
            })).ToList()
        };
    }

    protected override UserModel MapToModel(User entity)
    {
        return new UserModel
        {
            Id = entity.Id,
            RowVersion = entity.RowVersion,
            IsActive = entity.IsActive,
            DisplayName = entity.DisplayName,
            UserName = entity.UserName,
            Roles = entity.Roles.Select(r => new UserRoleModel
                {Id = r.Id, RoleId = r.RoleId, TrackingState = r.TrackingState}).ToList(),
            Permissions = entity.Permissions.Where(p => p.IsGranted).Select(p => new PermissionModel
            {
                Id = p.Id,
                TrackingState = p.TrackingState,
                Name = p.Name
            }).ToList(),
            IgnoredPermissions = entity.Permissions.Where(p => !p.IsGranted).Select(p => new PermissionModel
            {
                Id = p.Id,
                TrackingState = p.TrackingState,
                Name = p.Name
            }).ToList()
        };
    }

    protected override Task BeforeSaveAsync(IReadOnlyList<User> entities, List<UserModel> models)
    {
        ApplyPasswordHash(entities, models);
        ApplySerialNumber(entities, models);
        return base.BeforeSaveAsync(entities, models);
    }

    private void ApplySerialNumber(IEnumerable<User> entities, IReadOnlyList<UserModel> models)
    {
        var i = 0;
        foreach (var entity in entities)
        {
            var model = models[i++];

            if (model.IsNew || !model.IsActive || !model.Password.IsEmpty() ||
                model.Roles.Any(a => a.IsNew || a.IsDeleted) ||
                model.IgnoredPermissions.Any(p => p.IsDeleted || p.IsNew) ||
                model.Permissions.Any(p => p.IsDeleted || p.IsNew))
            {
                entity.SerialNumber = _manager.NewSerialNumber();
            }
            else
            {
                //prevent include SerialNumber in update query
                UnitOfWork.Entry(entity).Property(a => a.SerialNumber).IsModified = false;
            }
        }
    }

    private void ApplyPasswordHash(IEnumerable<User> entities, IReadOnlyList<UserModel> models)
    {
        var i = 0;
        foreach (var entity in entities)
        {
            var model = models[i++];
            if (model.IsNew || !model.Password.IsEmpty())
            {
                entity.PasswordHash = _manager.HashPassword(model.Password);
            }
            else
            {
                //prevent include PasswordHash in update query
                UnitOfWork.Entry(entity).Property(a => a.PasswordHash).IsModified = false;
            }
        }
    }
}

در سناریوهای Master-Detail نیاز است متد دیگری تحت عنوان BuildFindQuery را نیز بازنویسی کنید. این متد برای بقیه حالات نیاز به بازنویسی نداشت؛ چرا که یک تک موجودیت واکشی می‌شد و خبری از موجودیت‌های Detail نبود. در اینجا لازم است تا روش تولید کوئری FindAsyn رو بازنویسی کنیم تا جزئیات دیگری را نیز واکشی کنیم. به عنوان مثال در اینجا Roles و Permissions کاربر نیز Include شده‌اند.

نکته: بازنویسی BuildFindQuery را شاید بتوان با روش‌های دیگری هم مانند تزئین موجودیت‌های وابسته با یک DetailOfAttribute و مشخص کردن نوع موجودیت اصلی، نیز جایگزین کرد.

متدهای MapToModel و MapToEntity هم به مانند قبل پیاده‌سازی شده‌اند. موضوع دیگری که در برخی از سناریوها پیش خواهد آمد، مربوط است به خصوصیتی که در زمان ثبت ضروری می‌باشد، ولی در زمان ویرایش اگر مقدار داشت باید با اطلاعات موجود در دیتابیس جایگزین شود؛ مانند Password و SerialNumber در موجودیت کاربر. برای این حالت می‌توان از متد BeforeSaveAsync بهره برد؛ به عنوان مثال برای SerialNumber:

private void ApplySerialNumber(IEnumerable<User> entities, IReadOnlyList<UserModel> models)
{
    var i = 0;
    foreach (var entity in entities)
    {
        var model = models[i++];

        if (model.IsNew || !model.IsActive || !model.Password.IsEmpty() ||
            model.Roles.Any(a => a.IsNew || a.IsDeleted) ||
            model.IgnoredPermissions.Any(p => p.IsDeleted || p.IsNew) ||
            model.Permissions.Any(p => p.IsDeleted || p.IsNew))
        {
            entity.SerialNumber = _manager.NewSerialNumber();
        }
        else
        {
            //prevent include SerialNumber in update query
            UnitOfWork.Entry(entity).Property(a => a.SerialNumber).IsModified = false;
        }
    }
}

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

نکته: متد BeforeSaveAsync دقیقا بعد از ردیابی شدن وهله‌های موجودیت توسط Context برنامه و دقیقا قبل از UnitOfWork.SaveChange فراخوانی خواهد شد.


برای بررسی بیشتر، پیشنهاد می‌کنم پروژه DNTFrameworkCore.TestAPI موجود در مخزن این زیرساخت را بازبینی کنید.
پاسخ به بازخورد‌های پروژه‌ها
استفاده از progressBar در درون یک CustomCellTemplate
نه به صورت مستقیم.
 سورس آن در دسترس هست. در متد public void CellRendered کار نقاشی داخل یک سلول انجام می‌شود. اگر در سلول سفارشی خودتون برای مثال یک جدول قرار دادید و می‌خواهید در یکی از سلول‌های آن این نقاشی را اعمال کنید، باید خاصیت CellEvent آن‌را مقدار دهی کرده و سپس در public void CellLayout رخدادگردان آن درصد پیشرفت را نمایش دهید. یک مثال در مورد پیاده سازی CellEvent سفارشی.
مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت دوم - ایجاد ساختار اولیه‌ی مثال این سری
در این قسمت قصد داریم ساختار مقدماتی مثال این سری را که لیستی از تصاویر آپلود شده‌ی توسط کاربران مختلف را نمایش می‌دهد، بدون افزودن مباحث امنیتی و سطوح دسترسی کاربران وارد شده‌ی به سیستم، تشکیل دهیم. در قسمت‌های بعدی، به تدریج آن‌را با قابلیت‌های مختلف IdentityServer 4x یکپارچه خواهیم کرد. در اینجا فرض بر این است که حداقل SDK نگارش 2.1.401 را پیشتر نصب کرده‌اید.


بررسی ساختار WebAPI مقدماتی مثال این سری


این پروژه‌ی مقدماتی که هنوز قسمت‌های اعتبارسنجی به آن اضافه نشده‌اند، از دو قسمت WebApi و MvcClient تشکیل می‌شود.
کار قسمت WebApi، ارائه‌ی یک Restful-API برای کار با گالری تصاویر است. برای اجرای آن وارد پوشه‌ی src\WebApi\ImageGallery.WebApi.WebApp شده و ابتدا فایل restore.bat و سپس dotnet_run.bat را اجرا کنید.
در این حالت برنامه بر روی پورت 7001 در دسترس خواهد بود:


این پورت نیز در فایل Properties\launchSettings.json تنظیم شده‌است تا با پورت کلاینت MVC تهیه شده، تداخل نکند.
کار این سرویس، ارائه‌ی ImagesController است که توسط آن می‌توان لیستی از تصاویر موجود، اطلاعات یک تصویر و همچنین کار ثبت، ویرایش و حذف تصاویر را انجام داد.
در این Solution، رکوردهای تصاویری که در بانک اطلاعاتی ذخیره می‌شوند، یک چنین ساختاری را دارند:
using System;
using System.ComponentModel.DataAnnotations;

namespace ImageGallery.WebApi.DomainClasses
{
    public class Image
    {
        [Key]
        public Guid Id { get; set; }

        [Required]
        [MaxLength(150)]
        public string Title { get; set; }

        [Required]
        [MaxLength(200)]
        public string FileName { get; set; }

        [Required]
        [MaxLength(50)]
        public string OwnerId { get; set; }
    }
}
همانطور که ملاحظه می‌کنید در اینجا OwnerId نیز در نظر گرفته شده‌است تا بتوان پس از اعمال مباحث اعتبارسنجی، تصاویر را از کاربری خاص دریافت و همچنین صرفا تصاویر متعلق به او را به آن کاربر خاص نمایش داد.
در این قسمت توسط کلاس ImageConfiguration کار مقدار دهی اولیه‌ی بانک اطلاعاتی به کمک متد HasData مربوط به EF Core 2.1 صورت گرفته‌است و به این ترتیب می‌توان برنامه را برای نمایش مقدماتی جاری، بدون داشتن سیستم اعتبارسنجی و مفاهیم کاربران، نمایش داد.
این قسمت از Solution، به نحو زیر تشکیل شده‌است:
- ImageGallery.WebApi.DataLayer
در اینجا کار تشکیل DbContext برنامه و همچنین مقدار دهی اولیه‌ی بانک اطلاعاتی و تنظیمات Migrations قرار گرفته‌اند.
- ImageGallery.WebApi.DomainClasses
در این پروژه کلاس‌های موجودیت‌های متناظر با جداول بانک اطلاعاتی قرار می‌گیرند.
- ImageGallery.WebApi.Mappings
این پروژه کار تهیه نگاشت‌های AutoMapper برنامه را انجام می‌دهد؛ نگاشت‌هایی بین Models و DomainClasses که در ImagesController از آن‌ها استفاده می‌شود.
- ImageGallery.WebApi.Models
در این پروژه همان DTO's پروژه قرار گرفته‌اند. جهت رعایت مسایل امنیتی نباید کلاس موجودیت Image فوق را مستقیما در معرض دید API عمومی قرار داد. به همین جهت تعدادی Model در اینجا تعریف شده‌اند که هم برای ثبت، ویرایش و حذف اطلاعات بکار می‌روند و هم جهت گزارشگیری از رکوردهای موجود جدول تصاویر.
- ImageGallery.WebApi.Services
در این پروژه کار با DbContext انجام شده و توسط آن اطلاعات تصاویر به بانک اطلاعاتی اضافه شده و یا واکشی می‌شوند.
- ImageGallery.WebApi.WebApp
این پروژه، همان پروژه‌ی اصلی است که سایر قسمت‌های یاد شده را در کنار هم قرار داده و به صورت یک Restful-API ارائه می‌دهد.



بررسی ساختار MvcClient مقدماتی مثال این سری

پس از اجرای WebAPI و آماده بودن آن جهت استفاده، اکنون یک کلاینت ASP.NET Core MVC را جهت کار با امکانات ImagesController آن، تدارک دیده‌ایم.
برای اجرای آن وارد پوشه‌ی src\MvcClient\ImageGallery.MvcClient.WebApp شده و ابتدا فایل restore.bat و سپس dotnet_run.bat را اجرا کنید.
در این حالت برنامه بر روی پورت 5001 در دسترس خواهد بود:


این پورت نیز در فایل Properties\launchSettings.json تنظیم شده‌است.


در اینجا نمایی از اجرای این برنامه را مشاهده می‌کنید که لیستی از تصاویر را توسط GalleryController، از سرویس ImagesController مربوط به WebAPI، دریافت کرده و سپس نمایش می‌دهد. در این لیست تصاویر تمام کاربران با هم نمایش داده شده‌اند و هنوز امکان فیلتر کردن آن‌ها بر اساس کاربران وارد شده‌ی به سیستم را نداریم که در قسمت‌های بعدی آن‌ها را تکمیل خواهیم کرد.

این قسمت از Solution به نحو زیر تشکیل شده‌است:
- ImageGallery.MvcClient.Services
در اینجا یک Typed HTTP Client مخصوص NET Core 2.1. را تهیه کرده‌ایم. این سرویس جهت دسترسی به آدرس https://localhost:7001 که WebAPI برنامه در آن قرار دارد، تشکیل شده‌است. روش ثبت مخصوص آن‌را نیز در فایل آغازین پروژه‌ی MvcClient.WebApp توسط متد services.AddHttpClient ملاحظه می‌کنید.
- ImageGallery.MvcClient.ViewModels
مدل‌های متناظر با ساختار Viewهای Razor برنامه‌ی وب، در اینجا قرار می‌گیرند.
- ImageGallery.MvcClient.WebApp
این پروژه، همان پروژه‌ی اصلی است که سایر قسمت‌های یاد شده را در کنار هم قرار داده و به صورت یک برنامه‌ی MVC قابل مرور در مرورگر، ارائه می‌دهد.

کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
برای اجرای آن ابتدا باید پروژه‌ی WebApi.WebApp را اجرا کنید و سپس پروژه‌ی MvcClient.WebApp.
مطالب
مرتب‌سازی، فیلتر کردن و صفحه‌بندی اطلاعات در ASP.NET Core

مقدمه

اگر با Apiها کار کرده باشید احتمالاً با این چالش که گاهی نیاز است منابعی (Resources) که به کاربر ارسال می‌شوند مرتب (Sort)، بر اساس درخواست کاربر فیلتر (Filter) و در صفحه‌بندی (Paging) مشخصی تحویل داده شوند، برخورد کرده‌اید. این نیاز خصوصاً در پاسخ (Response) با روش GET از استاندارد HTTP مشهود است. در این مطلب به معرفی کتابخانه‌ای می‌پردازیم که با استفاده از آن می‌توان عملیات فوق را پیاده‌سازی نمود. Sieve یک چارچوب (Framework) ساده، تمیز و قابل توسعه برای NET Core. است. در زمان نگارش این مقاله ویرایش ۲.۱.۳ از این کتابخانه در دسترس است و همانگونه که اشاره شد، این کتابخانه منبع باز (Open Source) بوده و می‌توانید آن را از مخزن گیت‌هاب در این پیوند دریافت نمایید.
  
سیستمی با یک موجودیت به نام "پست" (Post) مفروض است. با استفاده از کتابخانه Sieve عملیات مرتب‌سازی، فیلتر و صفحه‌بندی را هنگام درخواست (GET) تمامی پست‌ها اعمال خواهیم کرد.
// Post

public int Id { get; set; }

public string Title { get; set; }

public int LikeCount { get; set; }

public int CommentCount { get; set; }

public DateTimeOffset DateCreated { get; set; } = DateTimeOffset.UtcNow;

۱. نصب کتابخانه

ابتدا لازم است از طریق Package Manager Console و اجرای دستور فوق اقدام به نصب این کتابخانه نمایید:  Install-Package Sieve -Version 2.1.3

۲. اضافه کردن سرویس‌

در فایل Startup.cs سرویس SieveProcessor را تزریق کنید:
services.AddScoped<SieveProcessor>();
 

۳. تعیین ویژگی‌هایی از کلاس برای اعمال مرتب‌سازی و فیلتر

باید ویژگی‌هایی (Properties) از کلاس را که می‌خواهید اعمال مرتب‌سازی و فیلتر بر روی آن‌ها انجام شوند، مشخص کنید. به دو روش این امر ممکن است:
 

۱.۳. از طریق اضافه کردن صفت (Attribute) به ویژگی‌ها

تنها ویژگی‌هایی از کلاس که دارای صفت [(Sieve(CanSort = true, CanFilter = true] باشند، می‌توانند مرتب و یا فیلتر شوند (می‌توان تنها از یکی از آن‌ها نیز استفاده نمود).
لذا کلاس پست به صورت زیر ویرایش می‌شود:
// Post

public int Id { get; set; }

[Sieve(CanFilter = true, CanSort = true)]
public string Title { get; set; }

[Sieve(CanFilter = true, CanSort = true)]
public int LikeCount { get; set; }

[Sieve(CanFilter = true, CanSort = true)]
public int CommentCount { get; set; }

[Sieve(CanFilter = true, CanSort = true, Name = "created")]
public DateTimeOffset DateCreated { get; set; } = DateTimeOffset.UtcNow;

 

۲.۳. از طریق Fluent API

برای استفاده از این روش، ابتدا کلاسی را ایجاد و از کلاس SieveProcessor مشتق کنید. سپس تابع MapProperties موجود در کلاس والد را override کنید.
// ApplicationSieveProcessor

public class ApplicationSieveProcessor : SieveProcessor
{
    public ApplicationSieveProcessor(
        IOptions<SieveOptions> options, 
        ISieveCustomSortMethods customSortMethods, 
        ISieveCustomFilterMethods customFilterMethods) 
        : base(options, customSortMethods, customFilterMethods)
    {
    }

    protected override SievePropertyMapper MapProperties(SievePropertyMapper mapper)
    {
        mapper.Property<Post>(p => p.Title)
            .CanFilter()
            .HasName("a_different_query_name_here");

        mapper.Property<Post>(p => p.CommentCount)
            .CanSort();

        mapper.Property<Post>(p => p.DateCreated)
            .CanSort()
            .CanFilter()
            .HasName("created_on");

        return mapper;
    }
}
حال باید کلاس جدید را تزریق نمایید:
services.AddScoped<ISieveProcessor, ApplicationSieveProcessor>();
در هر دو روش پارامتری دیگر با نام "Name" نیز وجود دارد که می‌توانید با استفاده از آن برای هر ویژگی، نامی غیر از نام اصلی آن را اتخاذ نمایید.
 

۴. دریافت پرس‌و‌جوهای (Queries) مرتب/فیلتر/صفحه‌بندی با اضافه کردن SieveModel به کنترلر (Controller)

برای دریافت پرس‌و‌جوهای مرتب/فیلتر/صفحه‌بندی، SieveModel را به اکشنی (Action) که پست‌ها را برگشت می‌دهد به عنوان پارامتر اضافه کنید. سپس با فراخوانی تابع Apply با استفاده از تزریق SieveProcessor در کنترلر خود، پرس‌و‌جوها را به منابع خود اعمال کنید.
[HttpGet]
public JsonResult GetPosts(SieveModel sieveModel) 
{
    var result = _dbContext.Posts;
    result = _sieveProcessor.Apply(sieveModel, result);
    return Json(result.ToList());
}
توجه داشته باشید مقادیر پرس‌و‌جوها اختیاری است و هر کدام می‌توانند به تنهایی و یا با هم مورد استفاده قرار گیرند.
 

۵. ارسال درخواست

با تمام موارد گفته شده، اکنون می‌توانید درخواستی را برای دریافت (GET) شامل پرس‌و‌جوهای مرتب/فیلتر/صفحه‌بندی ارسال نمایید. برای مثال:
GET /GetPosts

?sorts=     LikeCount,CommentCount,-created         // sort by likes, then comments, then descendingly by date created 
&filters=   LikeCount>10,Title@=awesome title,      // filter to posts with more than 10 likes, and a title that contains the phrase "awesome title"
&page=      1                                       // get the first page...
&pageSize=  10                                      // ...which contains 10 posts
sorts= LikeCount,CommentCount,-created ?: مرتب‌سازی بر اساس تعداد محبوبیت، سپس تعداد نظرات و در آخر تاریخ ثبت پست به صورت نزولی.
filters= LikeCount>10,Title@=awesome title &: فیلتر بر اساس پست‌هایی که تعداد محبوبیت آن‌ها بیش از ۱۰ است و در عنوان خود شامل عبارت "awesome title" می‌باشند.
page=1 &: صفحه اول ...
pageSize=10 &: ... که شامل ۱۰ پست است.

به صورت رسمی‌تر:
sorts: فهرست دستورالعمل‌هایی شامل نام ویژگی‌هایی است که مرتب‌سازی بر روی آن‌ها اعمال می‌شود و از طریق کاما (,) از یکدیگر تمایز داده می‌شوند. با اضافه کردن - قبل از نام ویژگی، آن را به صورت نزولی مرتب نمایید.
  • filters: دستورالعمل‌های جدا شده توسط کاما (,) به صورت {Name}{Operator}{Value} که در آن:
    • {Name} نام ویژگی‌ای است که صفت Sieve بر روی آن تعریف شده و یا نام سفارشی‌ای است که کاربر تعیین کرده است.
      • همچنین می‌توانید بیش از یک نام (برای یای منطقی (OR)) در درون جفت پرانتز باز و بسته و جداکننده یای منطقی (|) داشته باشید. برای مثال: LikeCount|CommentCount)>10) مشخص می‌کند مقدار LikeCount و یا CommentCount بیش از ۱۰ باشد.
    • {Operator} یکی از عملگرهای ممکن است.
    • {Value} مقداری است که در عمل فیلتر مورد استفاده قرار می‌گیرد.
  • page: شماره صفحه‌ای است که برگشت داده می‌شود.
  • pageSize: تعداد مواردی است که در هر صفحه برگردانده خواهد شد.
 

۶. عملگرها (Operators)

عملگر
توضیحات
عملگر
توضیحات
== برابر =@  شامل
=! مخالف
=_  شروع شود با
< بزرگ‌تر
*=@  شامل (حساس به حروف)*
> کوچک‌تر
*=_  شروع شود با (حساس به حروف)
=< بزرگ‌تر مساوی
*==  برابر (حساس به حروف)
=> کوچک‌تر مساوی

 
* حساس به بزرگی و کوچکی حروف

۷. پیکربندی

برای پیکربندی شامل مواردی چون حساس به بزرگ و کوچک بودن نام ویژگی، تعداد صفحات پیشفرض، حداکثر تعداد صفحه مجاز و نحوه برخورد با نام ویژگی ناموجود در ویژگی‌های کلاس ابتدا قطعه زیر را به appsettings اضافه کنید.
{
    "Sieve": {
        "CaseSensitive": "boolean: should property names be case-sensitive? Defaults to false",
        "DefaultPageSize": "int number: optional number to fallback to when no page argument is given. Set <=0 to disable paging if no pageSize is specified (default).",
        "MaxPageSize": "int number: maximum allowed page size. Set <=0 to make infinite (default)",
        "ThrowExceptions": "boolean: should Sieve throw exceptions instead of silently failing? Defaults to false"
    }
}

 سپس سرویس فوق را در Startup.cs اضافه کنید:
services.Configure<SieveOptions>(Configuration.GetSection("Sieve"));