یکی از سوالات معمول درباره ASP.Net نحوه ارنتقال اطلاعات بین صفحات است. در ابتدا شاید این کار یکی از وظایف عادی و روزمره هر برنامه نویسی به نظر برسد، اما در ادامه خواهید دید که این بحث یکی از موارد مهم و عمیق در ASP.Net میباشد. راه های مختلفی برای این کار وجود دارد، و انتخاب بهترین راه حل در زمان مناسب نیاز به شناخت و تجربه زیادی دارد.
به طور کل انتقال و نگه داری اصلاعات در دو سطح مختلف انجام میشود.
1- سطح سرور
2- سطح کلاینت
از دسته اول میتوان به Application State، cache، Session State و context را نام برد و از گروه دوم Query String، View Stateو Cookie پرکاربرد ترین هستند.
در قسمت های بعدی شما با راه های مختلف و زمان کاربرد هر یک از گزینه های بالا آشنا خواهید شد.
Application State:
HttpApplicationState یکی از اشیایی است که از زمان asp کلاسیک امکان استفاده از آن را، برای کارهایی نظیر ذخیره سازی متغیرهای سراسری داریم. برای مثال اگر شما یک DataSet داشته باشید که در قسمت های مختلف برنامه نیاز به استفاده از آن پیدا کنید، به سادگی میتوانید این شی را در Application State ذخیره کنید و در صورت نیاز این شی را از آن قسمت بازیابی کنید.
بازیابی اطلاعات از Application State کار ساده ای بوده و بیش از یک خط کد نیاز ندارد. نمونه زیر نحوه این کار را به شما نمایش میدهد.
به طور کل انتقال و نگه داری اصلاعات در دو سطح مختلف انجام میشود.
1- سطح سرور
2- سطح کلاینت
از دسته اول میتوان به Application State، cache، Session State و context را نام برد و از گروه دوم Query String، View Stateو Cookie پرکاربرد ترین هستند.
در قسمت های بعدی شما با راه های مختلف و زمان کاربرد هر یک از گزینه های بالا آشنا خواهید شد.
Application State:
HttpApplicationState یکی از اشیایی است که از زمان asp کلاسیک امکان استفاده از آن را، برای کارهایی نظیر ذخیره سازی متغیرهای سراسری داریم. برای مثال اگر شما یک DataSet داشته باشید که در قسمت های مختلف برنامه نیاز به استفاده از آن پیدا کنید، به سادگی میتوانید این شی را در Application State ذخیره کنید و در صورت نیاز این شی را از آن قسمت بازیابی کنید.
بازیابی اطلاعات از Application State کار ساده ای بوده و بیش از یک خط کد نیاز ندارد. نمونه زیر نحوه این کار را به شما نمایش میدهد.
VB.NET
Dim ds As DataSet = CType(Application("MyDataSet"), DataSet)
//C#
DataSet Source = (DataSet)(Application["MyDataSet"]);
تغییراتی که در اشیایی که در Application ذخیره شده اند میدهیم، به صورت خودکار مجددا در Application ذخیره نخواهد شد. به همین دلیل با هربار تغییر در شی خوانده شده از Application باید مراحل ذخیره سازی را مجددا تکرار کنیم تا تغییرات اعمال گردد.
همانطور که ممکن است از نام این شی حدس زده باشید. درسترسی به شی Application تنها در حوزه برنامه جاری امکان پذیر است. برای مثال اگر شما دو وب ساید مختلف روی سرور خود داشته باشید برنامه 2 امکان دستری به شی Application در برنامه 1 را نخواهد داشت.
Cache Object:
درباره Application State و کارایی آن شکی نیست. اما در این قسمت شما با شی دیگر آشنا خواهید شد که کارایی آن از بسیاری جهات شبیه به Application State است. این شی HttpCache نام دارد.
مانند HttpApplicationState شی HttpCache نیز محل مناسبی جهت ذخیره سازی اطلاعات عمومی و نسبتا پایدار برنامه میباشد.
با کمی توجه به نحوه ذخیره سازی و بازیابی اطلاعات در HttpCache متوجه خواهیم شد که این کار بسیار شبیه به شی application است. با این تفاوت که به علت اینکه مدیریت دسترسی همزمان در داخل این شی پیاده سازی شده است، دیگر نیازی به قفل کردن و باز کردن قفل در کار کردن با این شی نیست.
همانطور که ممکن است از نام این شی حدس زده باشید. درسترسی به شی Application تنها در حوزه برنامه جاری امکان پذیر است. برای مثال اگر شما دو وب ساید مختلف روی سرور خود داشته باشید برنامه 2 امکان دستری به شی Application در برنامه 1 را نخواهد داشت.
Cache Object:
درباره Application State و کارایی آن شکی نیست. اما در این قسمت شما با شی دیگر آشنا خواهید شد که کارایی آن از بسیاری جهات شبیه به Application State است. این شی HttpCache نام دارد.
مانند HttpApplicationState شی HttpCache نیز محل مناسبی جهت ذخیره سازی اطلاعات عمومی و نسبتا پایدار برنامه میباشد.
با کمی توجه به نحوه ذخیره سازی و بازیابی اطلاعات در HttpCache متوجه خواهیم شد که این کار بسیار شبیه به شی application است. با این تفاوت که به علت اینکه مدیریت دسترسی همزمان در داخل این شی پیاده سازی شده است، دیگر نیازی به قفل کردن و باز کردن قفل در کار کردن با این شی نیست.
'VB.NET
Cache("MyGlobalValue") = TextBox1.Text 'Store
Dim s As String = Cache("MyGlobalValue").ToString 'Retrieve
//C#
Cache["MyGlobalValue"] = TextBox1.Text; //Store
string s = Cache["MyGlobalValue"].ToString(); //Retrieve
شی HttpCache علی رغم شباهت هایی که با شی application دارد قابلیت هایی به آن افزوده است که کارایی و مقیاس پذیری این شی را به طور قابل توجهی افزایش داده است.
این شی تکنیک های ذخیره سازی هوشمندی را پیاده سازی کرده است. برای مثال در صورتی که حافظه برنامه رو به کاهش بگذارد، این شی به صورت خودکار، اشیایی ذخیره شده در خود را که به ندرت نیاز به دسترسی به آنها میباشد، حذف خواهد کرد. خوشبختانه امکان تعیین حق تقدم برای اشیای ذخیره شده در Cache به صورت دلخواه وجود دارد. به همین خاطر میتوانیم تعیین کنیم که اشیایی که برای ما از اهمیت بیشتری برخوردار هستند دیرتر از Cache خارج شوند. به کمک CacheItemRemovedCallback هنگامی که آیتمی از شی Cache حذف میشود با خبر شوید. در شی Cache شما این امکان را دارید تا تعیین کنید اشیا چگونه و برای چه مدت زمانی در Cache ذخیره شوند. برای مثال شما میتوانید تعیین کنید که یک شی که در Cache ذخیره شده است بعد از 1 ساعت منقضی شود و از Cache حذف گردد.
در مثال بعدی مشاهده خواهید کرد که چگونه یک شی در Cache ذخیره میشود. با این تفاوت که در این مثال ما زمانی 20 دقیقه ای برای Cache در نظر میگیریم و بعد از این مدت زمان شی ما از Cache حذف خواهد شد.
این شی تکنیک های ذخیره سازی هوشمندی را پیاده سازی کرده است. برای مثال در صورتی که حافظه برنامه رو به کاهش بگذارد، این شی به صورت خودکار، اشیایی ذخیره شده در خود را که به ندرت نیاز به دسترسی به آنها میباشد، حذف خواهد کرد. خوشبختانه امکان تعیین حق تقدم برای اشیای ذخیره شده در Cache به صورت دلخواه وجود دارد. به همین خاطر میتوانیم تعیین کنیم که اشیایی که برای ما از اهمیت بیشتری برخوردار هستند دیرتر از Cache خارج شوند. به کمک CacheItemRemovedCallback هنگامی که آیتمی از شی Cache حذف میشود با خبر شوید. در شی Cache شما این امکان را دارید تا تعیین کنید اشیا چگونه و برای چه مدت زمانی در Cache ذخیره شوند. برای مثال شما میتوانید تعیین کنید که یک شی که در Cache ذخیره شده است بعد از 1 ساعت منقضی شود و از Cache حذف گردد.
در مثال بعدی مشاهده خواهید کرد که چگونه یک شی در Cache ذخیره میشود. با این تفاوت که در این مثال ما زمانی 20 دقیقه ای برای Cache در نظر میگیریم و بعد از این مدت زمان شی ما از Cache حذف خواهد شد.
'VB.NET
Cache.Insert("MyGlobalValue", TextBox1.Text, _
Nothing, System.Web.Caching.Cache.NoAbsoluteExpiration, _
New TimeSpan(0, 20, 0))
//C#
Cache.Insert("MyGlobalValue", TextBox1.Text,
null, System.Web.Caching.Cache.NoAbsoluteExpiration,
new TimeSpan(0, 20, 0));
به جز زمان، Cache میتواند به موارد دیگری نیز حساس باشد و با تغییر آن موارد، شی ذخیره شده در Cache منقضی شود. مثلا ممکن است داده های ذخیره شده از فایلی خوانده شده باشد. این امکان وجود دارد که Cache به آن فایل حساس باشد و با تغییر آن فایل Cache نیز منقضی شده و اصلاعات آن حذف شود. Cache میتواند حتی به یک Cache دیگر وابسته باشد و با تغییر Cache والد، Cache فرزند منقضی شود. شی CacheDependency کلید انجام موارد بالا است. در ضمن کلاس SqlCacheDependency نیز وجود دارد که به کمک آن میتوانیم یک سری اطلاعات ذخیره شده در cache را هنگامی که اطلاعات خاصی در بانک داده ما تغییر کرد، حذف کنیم.
مانند شی application حوزه دسترسی به cache نیز برنامه جاری میباشد.
Session State :
همانطور که مشاهده کردید، دو گزینه قبل محل مناسبی برای ذخیره سازی اصلاعات کلی و عمومی بودند، در مقابل Session محلی است برای نگه داری اصلاعات مخصوص یک کاربر. نحوه استفاده از این گزینه نیز بسیار ساده و آسان است.
مانند شی application حوزه دسترسی به cache نیز برنامه جاری میباشد.
Session State :
همانطور که مشاهده کردید، دو گزینه قبل محل مناسبی برای ذخیره سازی اصلاعات کلی و عمومی بودند، در مقابل Session محلی است برای نگه داری اصلاعات مخصوص یک کاربر. نحوه استفاده از این گزینه نیز بسیار ساده و آسان است.
'VB.NET
Session("UserName") = TextBox1.Text 'Store
Dim s As String = Session("UserName").ToString 'Retrieve
//C#
Session["UserName"] = TextBox1.Text; //Store
string s = Session["UserName"].ToString();//Retrieve
دسترسی به شی ذخیره شده در session تنها در برنامه جاری و تنها برای کاربر جاری میباشد. به گفته دیگر اگر دو کاربر وارد سایت شما شوند، و کد بالا در سایت شما وجود داشته باشد، نام کاربری هر یک از آنها از textbox خوانده شده و در شی Session ذخیره میشود. نام کاربری آنها کاملا جدا ذخیره میشود و آنها هرگز نام کاربری یکدیگر را مشاهده نخواهند کرد.
همانطور که مشاهده میکنید استفاده از session بسیار ساده است. اما در استفاده از از شی بسیار دقت کنید. زیرا این شی نیز مانند موارد قبلی از حافظه سرور جهت ذخیره سازی استفاده میکند و در صورت استفاده بیش از حد از این شی ممکن است با مشکل حافظه مواجه شوید. با این تفاوت که به خاطر وابستگی این شی به کاربران، سرعت کم شدن حافظه به وسیله این شی بسیار بیشتر از گزینه های قبلی است. فرض کنید در هرکدام از گزینه ها 10 متغیر را ذخیره کنید و سایت شما 100 بازدید کننده داشته باشد. برای application و cache در مجموع 20 متغیر ذخیره خواهد شد. اما برای session چون به کاربران وابستگی دارد 100 * 10 = 1000 متغیر ذخیره خواهد شد. خوشبختانه در Asp.net این امکان فراهم شده است تا تعیین کنیم Session در کجا ذخیره گردد. به طور پیش فرض روی حافظه سرور جاری میباشد. اما با انجام تنظیماتی در web.config میتوانیم تعیین کنیم که در Sql server یا در سرور دیگری که مخصوص مدیریت session ها است، این کار انجام شود.
Context:
یکی از راه هایی که به ندرت برای انتقال اطلاعات بین صفحات استفاده میشود، contextاست. به همراه هر صفحه که در asp.net درایم یک نمونه از شی context نیز وجود خواهد داشت. به خاطر اینکه شی صفحه مدت زمان خیلی کوتاهی در سرور ایجاد میشود و بعد از ارسال آن برای کاربر، شی صفحه از حافظه حذف میگردد، اشیایی که در شی Context ذخیره میگردد، مدت زمان کوتاهی عمر خواهند کرد. در بسیاری موارد این شی محل مناسبی برای ذخیره سازی اصلاعات میباشد به خاطر اینکه اطلاعات به سرعت و به طور خودکار از حافظه حذف میگرددند.
همانطور که این شی در طول حیات یک صفحه در دسترس است، این شی هنگام انتقال از صفحه ای به صفحه دیگر نیز در دسترس خواهد بود. در نمونه کد زیر داده در شی context در صفحه اول ذخیره میشود و بعد از انتقال به صفحه دوم به این داده ها دسترسی پیدا میکنیم.
همانطور که مشاهده میکنید استفاده از session بسیار ساده است. اما در استفاده از از شی بسیار دقت کنید. زیرا این شی نیز مانند موارد قبلی از حافظه سرور جهت ذخیره سازی استفاده میکند و در صورت استفاده بیش از حد از این شی ممکن است با مشکل حافظه مواجه شوید. با این تفاوت که به خاطر وابستگی این شی به کاربران، سرعت کم شدن حافظه به وسیله این شی بسیار بیشتر از گزینه های قبلی است. فرض کنید در هرکدام از گزینه ها 10 متغیر را ذخیره کنید و سایت شما 100 بازدید کننده داشته باشد. برای application و cache در مجموع 20 متغیر ذخیره خواهد شد. اما برای session چون به کاربران وابستگی دارد 100 * 10 = 1000 متغیر ذخیره خواهد شد. خوشبختانه در Asp.net این امکان فراهم شده است تا تعیین کنیم Session در کجا ذخیره گردد. به طور پیش فرض روی حافظه سرور جاری میباشد. اما با انجام تنظیماتی در web.config میتوانیم تعیین کنیم که در Sql server یا در سرور دیگری که مخصوص مدیریت session ها است، این کار انجام شود.
Context:
یکی از راه هایی که به ندرت برای انتقال اطلاعات بین صفحات استفاده میشود، contextاست. به همراه هر صفحه که در asp.net درایم یک نمونه از شی context نیز وجود خواهد داشت. به خاطر اینکه شی صفحه مدت زمان خیلی کوتاهی در سرور ایجاد میشود و بعد از ارسال آن برای کاربر، شی صفحه از حافظه حذف میگردد، اشیایی که در شی Context ذخیره میگردد، مدت زمان کوتاهی عمر خواهند کرد. در بسیاری موارد این شی محل مناسبی برای ذخیره سازی اصلاعات میباشد به خاطر اینکه اطلاعات به سرعت و به طور خودکار از حافظه حذف میگرددند.
همانطور که این شی در طول حیات یک صفحه در دسترس است، این شی هنگام انتقال از صفحه ای به صفحه دیگر نیز در دسترس خواهد بود. در نمونه کد زیر داده در شی context در صفحه اول ذخیره میشود و بعد از انتقال به صفحه دوم به این داده ها دسترسی پیدا میکنیم.
'VB.NET
Context.Items("UserName") = TextBox1.Text 'Page1
Server.Transfer("Page2.aspx") 'Page1
Dim s As String = Context.Items("UserName").ToString 'Page2
//C#
Context.Items["UserName"] = TextBox1.Text; //Page1
Server.Transfer("Page2.aspx"); //Page1
string s = Context.Items["UserName"].ToString(); //Page2
به یاد داشته باشید که برای انتقال بین صفحات از Server.Transfer باید استفاده کنید. به خاطر اینکه اگر از Response.Redirect استفاده کنید، به واسطه رفت و برگشتی که بین سرور و کلاینت ایجاد میشود، شی contextاست مربوط به صفحه اول از بین خواهد رفت.
غیر از موارد بالا context در جاهای دیگری نیز میتواند کاربرد داشته باشد. برای مثال اگر شما در صفحه خود شی خاصی را استفاده کنید، در آن شی امکان استفاده از application یا session را ندارید. در این موارد به راحتی مورد دلخواه خود را از طریق context به شی دلخواه خود ارسال میکنید.
غیر از موارد بالا context در جاهای دیگری نیز میتواند کاربرد داشته باشد. برای مثال اگر شما در صفحه خود شی خاصی را استفاده کنید، در آن شی امکان استفاده از application یا session را ندارید. در این موارد به راحتی مورد دلخواه خود را از طریق context به شی دلخواه خود ارسال میکنید.
تا اینجا درباره نحوه انتقال اطلاعات در سمت سرور، و نقاط قوت و ضعف هر روش بحث کردیم. از این پس شیوه انتقال اطلاعات را در سمت کلاینت مورد بررسی قرار خواهیم داد.
ViewState
این شی برای نگه داری و انتقال اطلاعات در طی postbackهای مختلف در یک صفحه کاربرد دارد. در واقع شما نمیتوانید از این امکان برای انتقال اطلاعات از صفحه ای به صفحه دیگر استفاده کنید و صرفا جهت نگه داری اصلاعات در صفحه جاری کاربرد دارد.
نحوه کار با این امکان بسیار ساده است، با کمی دقت متوجه میشویم نحوه استفاده از این امکان بسیار شبیه به Session است.
ViewState
این شی برای نگه داری و انتقال اطلاعات در طی postbackهای مختلف در یک صفحه کاربرد دارد. در واقع شما نمیتوانید از این امکان برای انتقال اطلاعات از صفحه ای به صفحه دیگر استفاده کنید و صرفا جهت نگه داری اصلاعات در صفحه جاری کاربرد دارد.
نحوه کار با این امکان بسیار ساده است، با کمی دقت متوجه میشویم نحوه استفاده از این امکان بسیار شبیه به Session است.
'VB.NET
ViewState("PageValue") = TextBox1.Text 'Store
Dim s As String = ViewState("PageValue").ToString 'Retrieve
//C#
ViewState["PageValue"] = TextBox1.Text; //Store
string s = ViewState["PageValue"].ToString(); //Retrieve
در این روش به جای اینکه اطلاعات در سمت سرور و در حافظه سرور نگه داری شوند، اطلاعات کد گذاری شده و همراه html به سمت کاربر ارسال میگردد. در صورتی که بعد از بارگذاری صفحه ایتم view source را انتخاب کنید، مشاهده خواهید کرد که تگ زیر به html اضافه شده است که وظیفه آن نگه داری ViewState است.
<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE"
value="/wEPDwUKMTkwNjc4NTIwMWRkv1e5TcWOq4qnwyDuryos=" />
بعد از اینکه یک request به سمت سرور ارسال شد، سرور این مقدار به هم ریخته را خوانده مجددا از حالت کدگذاری شده خارج میکند و از مقادیر ذخیره شده در آن استفاده میکند.
از آنجا که این اصلاعات تنها کد گذاری شده اند، قابلیت دیکد کردن آن برای افرادی که قصد این کار را داشته باشند وجود دارد. پس در استفاده از این امکان باید دقت کنیم که، اطلاعات مهمی را در این حالت ذخیره سازی نمکنیم.
نکته قابل توجه دیگر اینکه، به دلیل انتقال این اصلاعات بین کلاینت و سرور، این روش از پهنای باند استفاده میکند، به همین دلیل باید دقت کافی داشته باشیم که داده های زیادی رد این حالت ارسال نشود.
QueryString
ارسال اطلاعات به وسیله query string یکی از روش های پر کاربرد و قدیمی در ارسال اطلاعات است. در این حالت مقداری که نیاز به ارسال آن داریم بعد از علامت سوال پشت سر URL قرار میگیرد.
استفاده از این اکان نیز بسیار ساده است. تنها کافی است، مقدار مورد نظر خود را بعد از URL اضافه کنید.
از آنجا که این اصلاعات تنها کد گذاری شده اند، قابلیت دیکد کردن آن برای افرادی که قصد این کار را داشته باشند وجود دارد. پس در استفاده از این امکان باید دقت کنیم که، اطلاعات مهمی را در این حالت ذخیره سازی نمکنیم.
نکته قابل توجه دیگر اینکه، به دلیل انتقال این اصلاعات بین کلاینت و سرور، این روش از پهنای باند استفاده میکند، به همین دلیل باید دقت کافی داشته باشیم که داده های زیادی رد این حالت ارسال نشود.
QueryString
ارسال اطلاعات به وسیله query string یکی از روش های پر کاربرد و قدیمی در ارسال اطلاعات است. در این حالت مقداری که نیاز به ارسال آن داریم بعد از علامت سوال پشت سر URL قرار میگیرد.
استفاده از این اکان نیز بسیار ساده است. تنها کافی است، مقدار مورد نظر خود را بعد از URL اضافه کنید.
'VB.NET
Dim s as string = Server.UrlEncode(TextBox1.Text) 'Page1
Response.Redirect("Page2.aspx?UserName=" & s) 'Page1
Dim s2 As String = Request.QueryString("UserName") 'Page2
//C#
string s = Server.UrlEncode(TextBox1.Text); //Page1
Response.Redirect("Page2.aspx?UserName=" + s); //Page1
string s2 = Request.QueryString["UserName"]; //Page2
به خاطر نحوه نمایش این اطلاعات در Address bar امنیت انتقال اطلاعات در این روش بسیار پایین است و اگر در استفاده از این امکان نکات امنیتی را در نظر نگیریم، به سادگی در این روش مورد حمله قرار خواهیم گرفت.
در ضمن به خاطر ساختار متنی این روش، تنها اطلاعات ساده را به کمک این روش میتوانیم انقال دهیم.
Cookies:
کوکی ها، فایل های متنی کوچکی هستند (4096 بایت) که از سمت سرور ارسال میگردند و در کلاینت ذخیره میشوند. در هر بار درخواستی که به سرور ارسال میشود، تمام کوکی های مرتبط با درخواست به طور خودکار به سمت سرور ارسال میشوند و مجددا به سمت کلاینت باز میگردند.
نحوه استفاده از کوکی ها به شکل زیر میباشد:
در ضمن به خاطر ساختار متنی این روش، تنها اطلاعات ساده را به کمک این روش میتوانیم انقال دهیم.
Cookies:
کوکی ها، فایل های متنی کوچکی هستند (4096 بایت) که از سمت سرور ارسال میگردند و در کلاینت ذخیره میشوند. در هر بار درخواستی که به سرور ارسال میشود، تمام کوکی های مرتبط با درخواست به طور خودکار به سمت سرور ارسال میشوند و مجددا به سمت کلاینت باز میگردند.
نحوه استفاده از کوکی ها به شکل زیر میباشد:
'VB.NET
Response.Cookies("myval").Value = TextBox1.Text 'Store
Response.Cookies("myval").Value 'Retrieve
//C#
Response.Cookies["myval"].Value = TextBox1.Text; //Store
Response.Cookies["myval"].Value; //Retrieve
کوکی ها امکانات تنظیماتی خوبی در اختیار برنامه نویس قرار میدهند مانند زمان از بین رفتن و یا ذخیره چند مقدار در یک کوکی.
این اطلاعات در هارد کاربر ذخیره میشوند و این امکان وجود دارد که توسط کاربر تغییر داده شوند، به همین دلیل هنگام استفاده از این امکان باید دقت کنیم و اطلاعات مهم را در کوکی ها ذخیره سازی نکنیم.
نکته شایان ذکر دیگر اینکه، این احتمال وجود دارد که کاربری امکان کوکی را در سیستم خود از کار انداخته باشد، پس در استفاده از این روش همیشه باید دقت کنیم که راه حل جایگزینی نیز برای چنین شرایطی در نظر داشته باشیم.
این اطلاعات در هارد کاربر ذخیره میشوند و این امکان وجود دارد که توسط کاربر تغییر داده شوند، به همین دلیل هنگام استفاده از این امکان باید دقت کنیم و اطلاعات مهم را در کوکی ها ذخیره سازی نکنیم.
نکته شایان ذکر دیگر اینکه، این احتمال وجود دارد که کاربری امکان کوکی را در سیستم خود از کار انداخته باشد، پس در استفاده از این روش همیشه باید دقت کنیم که راه حل جایگزینی نیز برای چنین شرایطی در نظر داشته باشیم.
پ ن: این مطلب رو چند وقت پیش نوشته بودم، یکی از دوستان سوالی پرسید، گفتم شاید بد نباشه مطلب رو دوباره اینجا منتشر کنم.
1 نظرات:
متشکرمم بسیار سودمند بود .
ارسال یک نظر