Push Notification

در این مقاله نشان می دهیم که چگونه با استفاده از SignalR یک سیستم push notification برای اطلاع رسانی به کلاینت ها و کاربران متصل پیاده سازی کنیم که زمانی که تغییری در دیتابیس روی سرور ایجاد شد، به کاربران اطلاع دهیم.

امروزه بیشتر اپلیکیشن ها چند کاربره هستند، که چند کاربر به طور همزمان وظایف خود را انجام می دهند.

اما مشکل این است که هر کاربری یک سری تغییرات اعمال می کند ( به طور مثال، یک سفارش جدید ثبت می نماید) سایر کاربران اطلاعی از آن ندارند، مگر اینکه درخواستی توسط کاربر برای جستجوی به روز رسانی ها وارد شود.

بنابراین، ما باید روشی داشته باشیم که اگر تغییری در سرور اتفاق افتاد بدون refresh کردن صفحه یا به روز رسانی صفحه وب به همه کلاینت های متصل اطلاع رسانی کنیم. این محدوده ای است که در آن asp.net SignalR وارد می شود.

برای پیاده سازی “سیستم push notification با signalR asp.net MVC” فقط کافی است گام های زیر را دنبال کنید:

در این مقاله از ویژوال استودیوی ۲۰۱۳ استفاده می کنیم.

گام اول: ایجاد پروژه جدید

از منوی File گزینه New و سپس Project و بعد از آن ASP.NET Web Application (زیرشاخه Web) را انتخاب می کنیم.

نام اپلیکیشن را وارد کرده و OK می کنیم.

Empty template را انتخاب کرده و زیر گزینه “Add folders and core references for” گزینه MVC را تیک می زنیم و سپس OK می کنیم.

گام دوم: اضافه کردن یک دیتابیس SQL Server

برای این کار از SQL server management studio استفاده می کنیم.

بنابراین بعد از باز کردن SQL Server management studio، نوع سرور را SQL Server Database Engine انتخاب کرده و گزینه connect را می زنیم.

روی Database راست کلیک کرده و گزینه New Database را انتخاب کرده و در پنجره New Database، نامی برای انتخاب می کنیم و OK می کنیم.

گام سوم: ایجاد جدول برای ذخیره داده ها

در لیست کناری سمت چپ صفحه، دیتابیس موردنظر را پیدا کرده و آن را باز می کنیم و روی گزینه Tables راست کلیک می کنیم و New Table را انتخاب می کنیم. جدولی با عنوان Contacts ایجاد کرده و ذخیره می کنیم.

گام چهارم: فعال کردن Service Broker روی دیتابیس

اسکریپت زیر را روی دیتابیسی که جدول را در آن ساختیم، اجرا می کنیم بنابراین SQL Server از این به بعد شروع به اطلاع رسانی اپلیکیشن .NET می کند که متوجه تغییرات این جدول شود.


ALTER DATABASE [yourdatabasename] SET ENABLE_BROKER WITH ROLLBACK IMMEDIATE ;

گام پنجم: اضافه کردن Entity Data Model

به پنجره Solution Explorer در ویژوال استودیو رفته و روی نام پروژه راست کلیک می کنیم، گزینه Add، سپس New item و بعد از آن ADO.net Entity Data Model که زیرشاخه data است را انتخاب می کنیم.

نامی برای آن انتخاب کرده و گزینه Add را می زنیم.

در پنجره Entity Data Model Wizard بازشده گزینه Generate from database را انتخاب کرده و Next را می زنیم، در مرحله بعدی data connection و سپس دیتابیس موردنظر را انتخاب کرده و next را می زنیم، جدول های دیتابیس موردنظر را انتخاب کرده و Model Namespace را انتخاب کرده و Finish را می زنیم.

گام ششم: نصب SignalR NuGet Package

در پنجره Solution Explorer روی References راست کلیک کرده و گزینه Manage NuGetPackages… را انتخاب کرده و SignalR را جستجو و سپس نصب می کنیم و درنهایت close را می زنیم.

و یا می توان آن را از طریق package manager console نیز نصب کرد.

به منوی Tools در بالای پنجره رفته، Library Package Manager را انتخاب کرده و Package Manager Console را باز می کنیم. از دستور زیر برای نصب SignalR استفاده می شود.

  1. PM> Install-Package Microsoft.AspNet.SignalR

گام هفتم: اضافه کردن یک فایل Owin startup

برای فعال سازی SignalR در برنامه، یک کلاس Owin startup به آن اضافه می کنیم.

به پنجره Solution Explorer رفته روی نام پروژه راست کلیک کرده و گزینه Add و پس از آن OWIN Startup را انتخاب می کنیم و نام کلاس را Startup.cs می گذاریم و Add را می زنیم.

کد زیر را در کلاس  Startup.cs می نویسیم:


using System;

using System.Threading.Tasks;

using Microsoft.Owin;

using Owin;

[assembly: OwinStartup(typeof(PushNotification.Startup))]

namespace PushNotification

{

public class Startup

{

public void Configuration(IAppBuilder app)

{

app.MapSignalR();

}

}

}

گام هشتم: اضافه کردن یک کلاس SignalR hub

حال یک کلاس SignalR Hub ایجاد می کنیم که امکان فراخوانی متدهای جاوااسکریپت سمت کلاینت را از سمت سرور فراهم می سازد.

در این برنامه، ما از این امکان برای نمایش نوتیفیکیشن استفاده می کنیم.

SignalR برای ارتباط بین کلاینت و سرور از اشیای “Hub” استفاده می کند.

در پنجره Solution Explorer روی نام پروژه راست کلیک کرده گزینه Add و سپس New item… را زده و سپس SignalR Hub class را انتخاب کرده و نام آن را NotificationHub.cs می گذاریم و Add را می زنیم.

محتوای زیر را در فایل قرار می دهیم.


using System;

using System.Collections.Generic;

using System.Linq;

using System.Web;

using Microsoft.AspNet.SignalR;

namespace PushNotification

{

public class NotificationHub : Hub

{
//Nothing required here

//public void Hello()

//{

//    Clients.All.hello();

//}
}
}

همان طور که مشاهده می کنید، کلاس NotificationHub.cs یک کلاس خالی است. این کلاس را همینطور خالی باقی می گذاریم و سپس در جایی دیگر از آن استفاده خواهیم کرد.

Push Notification

گام نهم: اضافه کردن connection  string به فایل web.config

فایل web.config که در شاخه اصلی اپلیکیشن قرار دارد باز کرده و element را پیدا می کنیم. Connection string زیر را به آن اضافه می کنیم.


<add name="sqlConString" connectionString="data source=YourSqlDatasourceName;initial catalog=DatabaseName;;user id=YourSqlUserID;password=YourSqlPassword;"/>

گام دهم: کلاس دیگری برای ثبت نوتیفیکیشن برای تغییرات اعمال شده در دیتابیس

در این کلاس، می خواهیم یک SQL dependency ایجاد کنیم که زمانی که داده های دیتابیس تغییر می کنند برنامه ی ما متوجه شود.

موارد زیر را داخل کلاس قرار می دهیم…

  1. RegisterNotification : یک متد void برای ثبت نوتیفیکیشن (خط ۱۴ تا ۳۶)
  2. SqlDependency_OnChange : رویداد SqlDependency onChange، زمانی اجرا می شود که دستور SQL تخصیص داده شده به آن نتیجه متفاوتی تولید کند (خط ۳۸ تا ۵۱)
  3. GetContacts : این متد برای برگرداندن تغییرات انجام شده روی سرور است، در اینجا داده های contactهای جدیدی که وارد کردیم برگردانده می شود (خط ۵۳ تا ۵۹)

NotificationComponent.cs

using System;

using System.Collections.Generic;

using System.Configuration;

using System.Linq;

using System.Web;

using System.Data.SqlClient;

using Microsoft.AspNet.SignalR;

namespace PushNotification
{
public class NotificationComponent
{
//Here we will add a function for register notification (will add sql dependency)

public void RegisterNotification(DateTime currentTime)
{
string conStr = ConfigurationManager.ConnectionStrings["sqlConString"].ConnectionString;

string sqlCommand = @"SELECT [ContactID],[ContactName],[ContactNo] from [dbo].[Contacts] where [AddedOn] > @AddedOn";

//you can notice here I have added table name like this [dbo].[Contacts] with [dbo], its mendatory when you use Sql Dependency

using (SqlConnection con = new SqlConnection(conStr))

{

SqlCommand cmd = new SqlCommand(sqlCommand, con);

cmd.Parameters.AddWithValue("@AddedOn", currentTime);

if (con.State != System.Data.ConnectionState.Open)

{

con.Open();

}

cmd.Notification = null;

SqlDependency sqlDep = new SqlDependency(cmd);

sqlDep.OnChange += sqlDep_OnChange;

//we must have to execute the command here

using (SqlDataReader reader = cmd.ExecuteReader())

{

// nothing need to add here now

}

}

}

void sqlDep_OnChange(object sender, SqlNotificationEventArgs e)

{

//or you can also check => if (e.Info == SqlNotificationInfo.Insert) , if you want notification only for inserted record

if (e.Type == SqlNotificationType.Change)

{

SqlDependency sqlDep = sender as SqlDependency;

sqlDep.OnChange -= sqlDep_OnChange;

//from here we will send notification message to client

var notificationHub = GlobalHost.ConnectionManager.GetHubContext<NotificationHub>();

notificationHub.Clients.All.notify("اضافه شد");

//re-register notification

RegisterNotification(DateTime.Now);

}

}

public List<Contact> GetContacts(DateTime afterDate)

{

using (MyPushNotificationEntities dc = new MyPushNotificationEntities())

{

return dc.Contacts.Where(a => a.AddedOn > afterDate).OrderByDescending(a => a.AddedOn).ToList();

}

}

}

}

گام یازدهم: ایجاد یک MVC Controller

به پنجره Solution Explorer رفته و روی فولدر Controllers راست کلیک کرده و گزینه Add و سپس Controller را انتخاب می کنیم و نامی برای آن انتخاب می کنیم و گزینه Template “empty MVC Controller” را انتخاب کرده و Add می کنیم.

در اینجا کنترلری به نام “HomeController” ایجاد کرده ایم.

گام دوازدهم: اضافه کردن action جدید به کنترلر

در اینجا Index action را به کنترلر Home اضافه کردیم. کد زیر را برای این کار می نویسیم.


public ActionResult Index()

{

return View();

}

گام سیزدهم:

روی Action Method راست کلیک کرده (در اینجا روی Index Action) و Add View را انتخاب می کنیم و سپس نام View را وارد کرده و View Engine (Razor) و سپس Add را انتخاب می کنیم.


@{

ViewBag.Title = "Index";

}


<h2>Index</h2>


گام چهاردهم:

اضافه کردن یک action method دیگر به کنترلر برای واکشی داده های contact


public JsonResult GetNotificationContacts()

{

var notificationRegisterTime = Session["LastUpdated"] != null ? Convert.ToDateTime(Session["LastUpdated"]) : DateTime.Now;

NotificationComponent NC = new NotificationComponent();

var list = NC.GetContacts(notificationRegisterTime);

//update session here for get only new added contacts (notification)

Session["LastUpdate"] = DateTime.Now;

return new JsonResult { Data = list, JsonRequestBehavior = JsonRequestBehavior.AllowGet };

}

گام پانزدهم:

تغییر فایل : Layout.cshtml برای نمایش نوتیفیکیشن

در اینجا برای نمایش نوتیفیکیشن مقداری کد HTML، CSS و JS اضافه کردیم.

کدهای زرد رنگی که مشاهده می کنید مربوط به این قسمت هستند.

  • خط ۱۶ تا ۲۰ : کد  HTML اضافه شده برای نمایش آیکون نوتیفیکشن در گوشه راست بالای صفحه
  • خط ۴۴ تا ۵۰: کدهای کتابخانه های jquery، SignalR و CSS
  • خط ۵۲ تا ۹۸: کد CSS اضافه شده که ظاهر آیکون نوتیفیکیشن را بهبود بخشیده
  • خط ۱۰۰ تا ۱۵۹: کد JS اضافه شده برای نمایش/پنهان کردن نوتیفیکیشن ها، به روزرسانی تعداد نوتیفیکیشن و شروع notification  hub
<!DOCTYPE html>

<html>

<head>

<meta charset="utf-8" />

<meta name="viewport" content="width=device-width, initial-scale=1.0">

<title>@ViewBag.Title - My ASP.NET Application</title>

<link href="~/Content/Site.css" rel="stylesheet" type="text/css" />

<link href="~/Content/bootstrap.min.css" rel="stylesheet" />

<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cscript%20src%3D%22~%2FScripts%2Fmodernizr-2.6.2.js%22%3E%3C%2Fscript%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="script" title="script" />

</head>

<body>


<div class="navbar navbar-inverse navbar-fixed-top">


<div class="container">


<div class="navbar-header">

<span class="noti glyphicon glyphicon-bell"><span class="count"></span></span>


<div class="noti-content">


<div class="noti-top-arrow"></div>



<ul id="notiContent"></ul>


</div>


<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">

<span class="icon-bar"></span>

<span class="icon-bar"></span>

<span class="icon-bar"></span>

</button>

@Html.ActionLink("Application name", "Index", "Home", null, new { @class = "navbar-brand" })

</div>



<div class="navbar-collapse collapse">


<ul class="nav navbar-nav">
</ul>


</div>


</div>


</div>



<div class="container body-content">

@RenderBody()


<hr />



<footer>



&copy; @DateTime.Now.Year - My ASP.NET Application


</footer>


</div>


@* Add Jquery Library *@

<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cscript%20src%3D%22~%2FScripts%2Fjquery-2.2.3.min.js%22%3E%3C%2Fscript%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="script" title="script" />

<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cscript%20src%3D%22~%2FScripts%2Fjquery.signalR-2.2.0.min.js%22%3E%3C%2Fscript%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="script" title="script" />

<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cscript%20src%3D%22%2Fsignalr%2Fhubs%22%3E%3C%2Fscript%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="script" title="script" />

<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cscript%20src%3D%22~%2FScripts%2Fbootstrap.min.js%22%3E%3C%2Fscript%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="script" title="script" />

@* Add css  *@

<link href="~/Content/bootstrap.min.css" rel="stylesheet" />


<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cstyle%20type%3D%22text%2Fcss%22%3E%0A%0A%2F*Added%20css%20for%20design%20notification%20area%2C%20you%20can%20design%20by%20your%20self*%2F%0A%0A%2F*%20COPY%20css%20content%20from%20youtube%20video%20description*%2F%0A%0A.noti-content%7B%0A%0Aposition%3Afixed%3B%0A%0Aright%3A100px%3B%0A%0Abackground%3A%23e5e5e5%3B%0A%0Aborder-radius%3A4px%3B%0A%0Atop%3A47px%3B%0A%0Awidth%3A250px%3B%0A%0Adisplay%3Anone%3B%0A%0Aborder%3A%201px%20solid%20%239E988B%3B%0A%0A%7D%0A%0Aul%23notiContent%7B%0A%0Amax-height%3A200px%3B%0A%0Aoverflow%3Aauto%3B%0A%0Apadding%3A0px%3B%0A%0Amargin%3A0px%3B%0A%0Apadding-left%3A20px%3B%0A%0A%7D%0A%0Aul%23notiContent%20li%20%7B%0A%0Amargin%3A3px%3B%0A%0Apadding%3A6px%3B%0A%0Abackground%3A%23fff%3B%0A%0A%7D%0A%0A.noti-top-arrow%7B%0A%0Aborder-color%3Atransparent%3B%0A%0Aborder-bottom-color%3A%23F5DEB3%3B%0A%0Aborder-style%3Adashed%20dashed%20solid%3B%0A%0Aborder-width%3A%200%208.5px%208.5px%3B%0A%0Aposition%3Aabsolute%3B%0A%0Aright%3A32px%3B%0A%0Atop%3A-8px%3B%0A%0A%7D%0A%0Aspan.noti%7B%0A%0Acolor%3A%23FF2323%3B%0A%0Amargin%3A15px%3B%0A%0Aposition%3Afixed%3B%0A%0Aright%3A100px%3B%0A%0Afont-size%3A18px%3B%0A%0Acursor%3Apointer%3B%0A%0A%7D%0A%0Aspan.count%7B%0A%0Aposition%3Arelative%3B%0A%0Atop%3A-3px%3B%0A%0A%7D%0A%0A%3C%2Fstyle%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="style" title="style" />




@* Add jquery code for Get Notification & setup signalr *@

<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cscript%20type%3D%22text%2Fjavascript%22%3E%0A%0A%24(function%20()%20%7B%0A%0A%2F%2F%20Click%20on%20notification%20icon%20for%20show%20notification%0A%0A%24('span.noti').click(function%20(e)%20%7B%0A%0Ae.stopPropagation()%3B%0A%0A%24('.noti-content').show()%3B%0A%0Avar%20count%20%3D%200%3B%0A%0Acount%20%3D%20parseInt(%24('span.count').html())%20%7C%7C%200%3B%0A%0A%2F%2Fonly%20load%20notification%20if%20not%20already%20loaded%0A%0Aif%20(count%20%3E%200)%20%7B%0A%0AupdateNotification()%3B%0A%0A%7D%0A%0A%24('span.count'%2C%20this).html('%26nbsp%3B')%3B%0A%0A%7D)%0A%0A%2F%2F%20hide%20notifications%0A%0A%24('html').click(function%20()%20%7B%0A%0A%24('.noti-content').hide()%3B%0A%0A%7D)%0A%0A%2F%2F%20update%20notification%0A%0Afunction%20updateNotification()%20%7B%0A%0A%24('%23notiContent').empty()%3B%0A%0A%24('%23notiContent').append(%24('%3C%2Fp%3E%0A%3Cli%3E%D8%A8%D8%A7%D8%B1%DA%AF%D8%B0%D8%A7%D8%B1%DB%8C...%3C%2Fli%3E%0A%3Cp%3E'))%3B%0A%0A%24.ajax(%7B%0A%0Atype%3A%20'GET'%2C%0A%0Aurl%3A%20'%2Fhome%2FGetNotificationContacts'%2C%0A%0Asuccess%3A%20function%20(response)%20%7B%0A%0A%24('%23notiContent').empty()%3B%0A%0Aif%20(response.length%C2%A0%20%3D%3D%200)%20%7B%0A%0A%24('%23notiContent').append(%24('%3C%2Fp%3E%0A%3Cli%3E%D9%87%DB%8C%DA%86%20%D8%AF%D8%A7%D8%AF%D9%87%20%D8%A7%DB%8C%20%D9%85%D9%88%D8%AC%D9%88%D8%AF%20%D9%86%DB%8C%D8%B3%D8%AA!%3C%2Fli%3E%0A%3Cp%3E'))%3B%0A%0A%7D%0A%0A%24.each(response%2C%20function%20(index%2C%20value)%20%7B%0A%0A%24('%23notiContent').append(%24('%3C%2Fp%3E%0A%3Cli%3E%D9%85%D8%AE%D8%A7%D8%B7%D8%A8%20%D8%AC%D8%AF%DB%8C%D8%AF%20%C2%A0%3A%20'%20%2B%20value.ContactName%20%2B%20'%20('%20%2B%20value.ContactNo%20%2B%20')%20%D8%A7%D8%B6%D8%A7%D9%81%D9%87%20%D8%B4%D8%AF%D9%87%3C%2Fli%3E%0A%3Cp%3E'))%3B%0A%0A%7D)%3B%0A%0A%7D%2C%0A%0Aerror%3A%20function%20(error)%20%7B%0A%0Aconsole.log(error)%3B%0A%0A%7D%0A%0A%7D)%0A%0A%7D%0A%0A%2F%2F%20update%20notification%20count%0A%0Afunction%20updateNotificationCount()%20%7B%0A%0Avar%20count%20%3D%200%3B%0A%0Acount%20%3D%20parseInt(%24('span.count').html())%20%7C%7C%200%3B%0A%0Acount%2B%2B%3B%0A%0A%24('span.count').html(count)%3B%0A%0A%7D%0A%0A%2F%2F%20signalr%20js%20code%20for%20start%20hub%20and%20send%20receive%20notification%0A%0Avar%20notificationHub%20%3D%20%24.connection.notificationHub%3B%0A%0A%24.connection.hub.start().done(function%20()%20%7B%0A%0Aconsole.log('Notification%20hub%20started')%3B%0A%0A%7D)%3B%0A%0A%2F%2Fsignalr%20method%20for%20push%20server%20message%20to%20client%0A%0AnotificationHub.client.notify%20%3D%20function%20(message)%20%7B%0A%0Aif%20(message%20%26%26%20message.toLowerCase()%20%3D%3D%20%22%D8%A7%D8%B6%D8%A7%D9%81%D9%87%20%D8%B4%D8%AF%22)%20%7B%0A%0AupdateNotificationCount()%3B%0A%0A%7D%0A%0A%7D%0A%0A%7D)%0A%0A%3C%2Fscript%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="script" title="script" />

</body>

</html>

گام شانزدهم:

تغییر فایل global.asax.cs برای شروع و توقف SQL dependency

در اینجا کدی را برای آغاز و پایان SQL dependency به فایل global.asax.cs اضافه کردیم.

کدهایی که به رنگ زرد نشان داده شده اند.

  • خط ۱۲: connection string را از فایل web.config می گیرد.
  • خط ۱۸: SQL dependency را درون رویداد Application_Start شروع می کند.
  • خط ۲۱ تا ۲۷: ثبت نوتیفیکیشن
  • خط ۳۰ تا ۳۴: SQL dependency را درون رویداد Application_End متوقف می کند.

using System;

using System.Configuration;

using System.Data.SqlClient;

using System.Web;

using System.Web.Mvc;

using System.Web.Routing;

namespace PushNotification

{

public class MvcApplication : System.Web.HttpApplication

{

string con = ConfigurationManager.ConnectionStrings["sqlConString"].ConnectionString;

protected void Application_Start()

{

AreaRegistration.RegisterAllAreas();

RouteConfig.RegisterRoutes(RouteTable.Routes);

//here in Application Start we will start Sql Dependency

SqlDependency.Start(con);

}

protected void Session_Start(object sender, EventArgs e)

{

NotificationComponent NC = new NotificationComponent();

var currentTime = DateTime.Now;

HttpContext.Current.Session["LastUpdated"] = currentTime;

NC.RegisterNotification(currentTime);

}

protected void Application_End()

{

//here we will stop Sql Dependency

SqlDependency.Stop(con);

}

}

}

گام آخر: اجرای برنامه

 

فاطمه زکایی

فاطمه زکایی هستم. فارغ التحصیل کارشناسی مهندسی نرم افزار، مدت سه سال هست که در زمینه توسعه اپلیکیشن های تحت وب و اندروید و همچنین تولید محتوای تخصصی برنامه نویسی تحت وب و اندروید در مجموعه mspsoft در خدمت شما هستم.

نوشته‌های مرتبط

دیدگاه‌ها

*
*

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