پیاده سازی اعمال زمان بر در وب
پیش نیاز این مقاله :
- تسلط به HTML
- نحوه ارسال ایمیل
- تسلط به برنامه نویسی شی گرا
- تسلط به جاوا اسکریپت و jQuery
- آشنایی با Delegate و Thread
خیلی وقت ها نیاز به انجام یک سری عملیات زمان بر در یک صفحه وب داریم ، به عنوان مثال ارسال تعداد زیادی ایمیل ، مشکلی که این اعمال برای ما به وجود می آورند به دو بخش تقسیم می شود :
- بعد از چند دقیقه ممکن است صفحه TimeOut شود
- کاربر متوجه نمی شود که چه مقدار از عملیات انجام شده و چه مقدار باقی مانده است
اگر شما تا کنون برنامه نویسی تحت ویندوز انجام داده باشید مطمئنا با مفهوم Thread آشنا هستید ، یکی از روش های عملی برای انجام اینگونه عملیات زمانبر استفاده از Thread است بدین معنی که انجام آن عملیات را به دست یک Thread دیگر می سپاریم تا Thread جاری صفحه درگیر این عملیات نشود .
استفاده از Thread چندین روش دارد که ما در این مقاله از یک Delegate به صورت Asynchronous (غیر همزمان) استفاده می کنیم .
قبل از هرچیز فرض کنید تابع زیر قرار است برای ما داخل یک حلقه ایمیل ارسال کند :
public static ArrayList SendMail(string subject, string[] to, string body, int Delay, int Period)
{
ArrayList Failds = new ArrayList();
int Myperiod = Period;
for (int i = 0; i < to.Length; i++)
{
try
{
SmtpClient MyMail = new SmtpClient();
MailMessage MyMsg = new MailMessage();
MyMsg.To.Add(new MailAddress(to[i]));
MyMsg.Subject = subject;
MyMsg.SubjectEncoding = Encoding.UTF8;
MyMsg.IsBodyHtml = true;
MyMsg.BodyEncoding = Encoding.UTF8;
MyMsg.Body = body;
MyMail.Send(MyMsg);
}
catch
{
// ذخیره ایمیل های ارسال نشده
Failds.Add(to[i]);
}
// تاخیر بین ارسال ها
if (i == Myperiod)
{
System.Threading.Thread.Sleep(Delay);
Myperiod += Period;
}
// ذخیره تعداد ایمیل های ارسال شده در کش
HttpRuntime.Cache["count"] = Convert.ToInt32(HttpRuntime.Cache["count"]) + 1;
}
return Failds;
}
تنظیمات میل سرور را هم داخل وب کانفیگ قرار میدهیم :
<system.net>
<mailSettings>
<smtp from="ali@how2learnasp.net" deliveryMethod="Network">
<network host="mail.how2learnasp.net" port="25" userName="ali@how2learnasp.net" password="12345" defaultCredentials="false"/>
</smtp>
</mailSettings>
</system.net>
قبل از هرچیز یک فرم ارسال ایمیل ساده بسازید :
<body style="font-family: Tahoma; font-size: 11px">
<form id="form1" runat="server">
<div id="myForm" dir="rtl">
<table>
<tr>
<td>
عنوان :
</td>
<td>
<asp:TextBox ID="txtSubject" runat="server"></asp:TextBox>
</td>
</tr>
<tr>
<td>
متن :
</td>
<td>
<asp:TextBox ID="txtBody" runat="server" TextMode="MultiLine"></asp:TextBox>
</td>
</tr>
<tr>
<td>
ایمیل ها :
</td>
<td>
<asp:TextBox ID="txtReceivers" runat="server" TextMode="MultiLine"></asp:TextBox>
</td>
</tr>
<tr>
<td colspan="2">
<asp:Button ID="btnSend" runat="server" Text="ارسال" OnClick="btnSend_Click" />
</td>
</tr>
<tr>
<td>
</td>
<td>
</td>
</tr>
</table>
</div>
<div id="myStatus" dir="rtl">
<div id="status">
</div>
<br />
<div style="width: 100px; background-color: red; color: White">
<div id="prog" style="background-color: Blue;">
0%
</div>
</div>
</div>
</form>
</body>
همانطور که ملاحظه می کنید من چند تگ div برای نمایش تعداد ارسال شده ها و همچنین درصد پیشرفت ارسال قرار دادم .
سپس فضا نام های زیر را به صفحه Default.aspx.cs که قرار است درخواست ارسال ایمیل از اینجا صادر شود اضافه نمایید :
using System.Collections;
using System.Runtime.Remoting.Messaging;
اکنون یک Delegate متناظر با این متد تعریف می کنیم :
private delegate ArrayList SendEmailDelegate(string subject, string[] to, string body, int Delay, int Period);
و سپس در رویداد کلیک شدن دکمه ارسال این متد را به صورت Async فراخوانی می کنیم :
protected void btnSend_Click(object sender, EventArgs e)
{
Cache["count"] = 0;
string[] receivers = txtReceivers.Text.Split(new char[] { '\n' });
SendEmailDelegate sendIt = new SendEmailDelegate(MailClass.SendMail);
IAsyncResult ar = sendIt.BeginInvoke(txtSubject.Text, receivers, txtBody.Text, 5000, 2, new AsyncCallback(SendCallback), null);
Session["result"] = ar;
}
همانطور که ملاحظه میکنید در متد فوق بین هر 2 ارسال 5 ثانیه تاخیر قرار دادیم ، یک Cache به نام count تعریف شده و مقدار 0 در آن قرار گرفته است ، این شی برای این است که بفهمیم چند عدد ارسال انجام شده است ، مقدار این شی را درمتد SendMail افزایش می دهیم (به قطعه کد اول دقت بفرمایید) ، دلیل استفاده از Cache این است که نمیتوانیم از Session در کلاس MailClass استفاده کنیم .
همچنین اگر دقت بفرمایید بنده از متد Split برای جداکردن ایمیل ها استفاده کردم ، همانطور که میدانید کاراکتر اینتر n\ هست ، بنابراین ایمیل ها را با اینتر از هم جدا می کنیم . البته میتوانید چند کاراکتر را با هم در نظر بگیرید ، مثلا اگر بخواهید کاراکتر ',' را هم اضافه کنید داریم :
string[] receivers = txtReceivers.Text.Split(new char[] { '\n',',' });
بنابراین الان میتوانید ایمیل ها را هم با اینتر و هم با ویرگول جدا نمایید .
همچنین دقت بفرمایید اگر از این صفحه چندین کاربر مختلف استفاده می کنند باید نام کش را به آیدی کاربر الصاق کنید تا هم پوشانی ایجاد نشود :
Cache["count" + User.Identity.Name] = 0;
و در کلاس SendMail هم به همین ترتیب :
HttpRuntime.Cache["count" + HttpContext.Current.User.Identity.Name] = Convert.ToInt32(HttpRuntime.Cache["count" + HttpContext.Current.User.Identity.Name]) + 1;
سپس یک Session به نام result هم تعریف کردیم که مقدار خروجی BeginInvoke در آن ذخیره می شود ، از این شی هم برای پی بردن به اینکه آیا عملیات پایان یافته یا نه استفاده خواهد شد . دلیل استفاده از Session این است که ما در یک صفحه دیگر نتیجه را مرتبا چک می کنیم .
و متد SendCallback که در متد فوق به کار رفته است :
private void SendCallback(IAsyncResult ar)
{
Session["result"] = ar;
AsyncResult aResult = (AsyncResult)ar;
SendEmailDelegate sendIt = (SendEmailDelegate)aResult.AsyncDelegate;
ArrayList retval = sendIt.EndInvoke(ar);
Session["failds"] = retval;
}
هنگامی که عملیات به پایان برسد تابع SendCallBack فراخوانی خواهد شد و خروجی تابع را در Session قرار می دهیم .
اکنون میخواهیم به کاربر نشان دهیم که چه مقدار از ارسال انجام شده ، برای اینکار ابتدا یک صفحه به نام QueryResult از نوع ashx یا Generic Handler به پروژه اضافه کنید ، سپس فضا نام های زیر را اضافه نمایید :
using System.Runtime.Remoting.Messaging;
using System.Web.SessionState;
و برای شناسایی Session ها در این کلاس ، آن را از واسط IReadOnlySessionState مشتق نموده و سپس به صورت زیر نتیجه را چک می کنیم :
<%@ WebHandler Language="C#" Class="QueryResult" %>
using System;
using System.Web;
using System.Runtime.Remoting.Messaging;
using System.Web.SessionState;
using System.Collections;
public class QueryResult : IHttpHandler, IReadOnlySessionState
{
public void ProcessRequest(HttpContext context)
{
context.Response.ContentType = "text/plain";
context.Response.Clear();
if (context.Session["result"] != null)
{
AsyncResult ar = (AsyncResult)context.Session["result"];
if (ar.IsCompleted)
{
context.Session.Remove("result");
ArrayList failds = (ArrayList)context.Session["failds"];
context.Response.Write("1:" + failds.Count.ToString());
}
else
{
context.Response.Write("0:" + context.Cache["count"].ToString());
}
}
else
{
context.Response.Write("-1");
}
context.Response.End();
}
public bool IsReusable
{
get
{
return false;
}
}
}
این صفحه فقط وظیفه دارد که ضعیت را چاپ کند ، اگر سیستم در حال ارسال باشد ابتدا عدد 0 و پس از : تعداد ارسال شده چاپ می شود و در صورت اتمام ارسال عدد 1 و سپس تعداد ارسال نشده ها چاپ می گردد .
حال از این صفحه توسط jQuery AJAX در همان صفحه ارسال Query میگیریم ، بنابراین کتابخانه jQuery و توابع زیر را به همان صفحه ای که فرم ارسال ایمیل را قرار دادید اضافه نمایید :
<head runat="server">
<script src="jquery.js" type="text/javascript"></script>
<script>
var timeout;
var w = 0;
var count = 0;
function start(icount) {
count = icount;
$("#status").html('در حال ارسال...');
timeout = setTimeout("checkStatus();", 50);
}
function checkStatus() {
$.ajax({
type: "POST",
url: "QueryResult.ashx",
success: function(content) {
var str = content;
var items = str.split(":");
if (items[0] == 1) {
clearTimeout(timeout);
$("#status").html(" پیام های شما با موفقیت ارسال شد ");
$("#prog").css("width", 100 + "px");
$("#prog").html(100 + "%");
}
else {
w = parseInt(items[1]) * parseFloat((100 / count));
$("#status").html(items[1] + " پیام ارسال شد ");
$("#prog").css("width", w + "px");
$("#prog").html(parseInt(w) + "%");
setTimeout("checkStatus();", 50);
}
},
error: function() {
alert('error');
}
});
}
</script>
</head>
همانطور که از توابع فوق مشخص است توسط jQuery Ajax هر 50 میلی ثانیه یکبار صفحه QueryResult.ashx فراخوانی شده و مقادیر چاپ شده بررسی و نتایج به کاربر نشان داده می شود .
اکنون کد زیر را به رویداد Page_load اضافه کنید که پس از کلیک بر روی دکمه ارسال تابع جاوا اسکریپت فوق اجرا شود :
if (IsPostBack)
Page.ClientScript.RegisterStartupScript(this.GetType(), "onclick", "start(" + txtReceivers.Text.Split(new char[] { '\n' }).Length.ToString() + ");", true);
این روش کاملا بر اساس تجربیات شخصی بدست آمده و هیچ منبع خاصی ندارد .
فایل ضمیمه پروژه را از اینجا دانلود نمایید .