public abstract class myabstractclass { public abstract string dosomething( string input ); public double round( double number , int decimals ) { return math.round( number , decimals ); } }
روش اول:
در این روش ابتدا باید کلاسی نوشت تا کلاس abstract بالا رو پیاده سازی کنه:
public class mynewclass : myabstractclass { public override string dosomething( string input ) { return input; } }
[testclass] public class mytest { [testmethod] public void testround() { mynewclass mynewclass = new mynewclass(); var result = mynewclass.round( 5.55 , 1 ); assert.areequal( 5.6 , result ); } }
البته روش بالا خیلی مورد پسند من نیست.
در روش دوم که من خیلی بیشتر بهش علاقه دارم دیگه نیازی به استفاده از یک کلاس دوم برای پیاده سازی کلاس abstract نیست. بلکه در این روش از ابزار rhinomocks برای این کار استفاده میکنیم . استفاده از rhino mocks به چندین روش امکان پذیره که امروز 2 روش اونو براتون توضیح میدم:
در روش اول از mockrepository استفاده میکنیم و در روش دوم از روش aaa یا arrange-act-assert
استفاده از mockrepository :
ابتدا کدهای مربوطه رو مینویسم:
[testmethod] public void testwithmockrepository() { var mockrepository = new rhino.mocks.mockrepository(); var mock = mockrepository.partialmock<myabstractclass>(); using ( mockrepository.record() ) { expect.call( mock.dosomething( arg<string>.is.anything ) ).return( "hi..." ).repeat.once(); } using ( mockrepository.playback() ) { assert.areequal( "hi..." , mock.dosomething( "salam" ) ); } }
در مورد expect.call باید بگم که از این کلاس برای شبیه سازی رفتار یک متد استفاده میشه (مثلا در مواقعی که یک متد برای انجام عملیات باید به دیتا بیس وصل شده و یک query را اجرا کنه) برای اینکه در تست، از این عملیات صرف نظر بشه از mockها استفاده کرده و رفتار متد رو به روش بالا شبیه سازی میکنیم. البته کار کردن با rhino mocks به صورت بالا دیگه از مد افتاده و جدیدا از روش aaa استفاده میشه که اونو در پایین توضیح میدم:
[testmethod] public void testwithaaa() { var mock = mockrepository.generatepartialmock<myabstractclass>(); mock.expect( x => x.dosomething( arg<string>.is.anything ) ).return( "hi..." ).repeat.once();//arange var result = mock.dosomething( "salam" );//act assert.areequal( "hi..." , result );//assert }
[testmethod] public void testwithaaa() { var mock = mockrepository.generatepartialmock<myabstractclass>(); mock.expect( x => x.dosomething( arg<string>.is.anything ) ).return( "hi..." ).repeat.once();//arange var result = mock.dosomething( "salam" );//act var result2 = mock.dosomething( "bye" );//act assert.areequal( "hi..." , result );//assert }
خطای آن هم واضح داره میگه که expected#1 هستش در حالی که actual#2 (تعداد دفعات حقیقی از دفعات مورد انتظار بیشتره)
توی پستهای بعدی (اگه وقت بشه) حتما در مورد rhino mocks بیشتر توضیح میدم
روشهای متفاوت ایجاد توالی (sequence) در Rx
الف) استفاده از متدهای Factory
1) Observable.Create
نمونهای از استفاده از آنرا در مطلب «معرفی Reactive extensions» مشاهده کردید.
var query = Enumerable.Range(1, 5).Select(number => number); var observableQuery = query.ToObservable(); var observer = Observer.Create<int>(onNext: number => Console.WriteLine(number)); observableQuery.Subscribe(observer);
البته در این مثال فقط delegate مربوط به onNext را ملاحظه میکند. توسط سایر overloadهای آن امکان ذکر delegateهای OnError/OnCompleted نیز وجود دارد.
2) Observable.Return
برای ایجاد یک خروجی Observable از یک مقدار مشخص، میتوان از متد جنریک Observable.Return استفاده کرد. برای مثال:
var observableValue1 = Observable.Return("Value"); var observableValue2 = Observable.Return(2);
public static IObservable<T> Return<T>(T value) { return Observable.Create<T>(o => { o.OnNext(value); o.OnCompleted(); return Disposable.Empty; }); }
var observableValue1 = Observable.Return<string>("Value"); var observableValue2 = Observable.Return<int>(2);
3) Observable.Empty
برای بازگشت یک توالی خالی که تنها کار اطلاع رسانی onCompleted را انجام میدهد.
var emptyObservable = Observable.Empty<string>();
public static IObservable<T> Empty<T>() { return Observable.Create<T>(o => { o.OnCompleted(); return Disposable.Empty; }); }
4) Observable.Never
برای بازگشت یک توالی بدون قابلیت اطلاع رسانی و notification
var neverObservable = Observable.Never<string>();
public static IObservable<T> Never<T>() { return Observable.Create<T>(o => { return Disposable.Empty; }); }
5) Observable.Throw
برای ایجاد یک توالی که صرفا کار اطلاع رسانی OnError را توسط استثنای معرفی شده به آن انجام میدهد.
var throwObservable = Observable.Throw<string>(new Exception());
public static IObservable<T> Throws<T>(Exception exception) { return Observable.Create<T>(o => { o.OnError(exception); return Disposable.Empty; }); }
6) توسط Observable.Range
به سادگی میتوان بازهی Observable ایی را ایجاد کرد:
var range = Observable.Range(10, 15); range.Subscribe(Console.WriteLine, () => Console.WriteLine("Completed"));
7) Observable.Generate
اگر بخواهیم عملیات Observable.Range را پیاده سازی کنیم، میتوان از متد Observable.Generate استفاده کرد:
public static IObservable<int> Range(int start, int count) { var max = start + count; return Observable.Generate( initialState: start, condition: value => value < max, iterate: value => value + 1, resultSelector: value => value); }
8) Observable.Interval
عموما از انواع و اقسام تایمرهای موجود در دات نت مانند System.Timers.Timer ، System.Threading.Timer و System.Windows.Threading.DispatcherTimer برای ایجاد یک توالی از رخدادها استفاده میشود. تمام اینها را به سادگی میتوان توسط متد Observable.Interval، که قابل انتقال به تمام پلتفرمهایی است که Rx برای آنها تهیه شدهاست، جایگزین کرد:
var interval = Observable.Interval(period: TimeSpan.FromMilliseconds(250)); interval.Subscribe(Console.WriteLine, () => Console.WriteLine("completed"));
Overload دوم این متد، امکان معرفی scheduler و اجرای بر روی تردی دیگر را نیز میسر میکند.
9) Observable.Timer
تفاوت Observable.Timer با Observable.Interval در مفهوم پارامتر ارسالی به آنها است:
var timer = Observable.Timer(dueTime: TimeSpan.FromSeconds(1)); timer.Subscribe(Console.WriteLine, () => Console.WriteLine("completed"));
خروجی Observable.Interval مثال زده شده به نحو زیر است و خاتمهای ندارد:
0
1
2
3
4
5
اما خروجی Observable.Timer به نحو ذیل بوده و پس از یک ثانیه، خاتمه مییابد:
0
completed
متد Observable.Timer دارای هفت overload متفاوت است که توسط آنها dueTime (مدت زمان صبر کردن تا تولید اولین خروجی)، period (کار Observable.Timer را به صورت متوالی در بازهی زمانی مشخص شده تکرار میکند) و scheduler (تعیین ترد اجرایی عملیات) قابل مقدار دهی هستند.
اگر میخواهید Observable.Timer بلافاصله شروع به کار کند، مقدار dueTime آنرا مساوی TimeSpan.Zero قرار دهید. به این ترتیب یک Observable.Interval را به وجود آوردهاید که بلافاصله شروع به کار کرده است و تا مدت زمان مشخص شدهای جهت اجرای اولین callback خود صبر نمیکند.
ب) تبدیلگرهایی که خروجی IObservable ایجاد میکنند
برای تبدیل مدلهای برنامه نویسی Async قدیمی دات نت مانند APM، رخدادها و امثال آن به معادلهای Rx، متدهای الحاقی خاصی تهیه شدهاند.
1) تبدیل delegates به معادل Observable
متد Observable.Start، امکان تبدیل یک Func یا Action زمانبر را به یک توالی observable میسر میکند. در این حالت به صورت پیش فرض، پردازش عملیات بر روی یکی از تردهای ThreadPool انجام میشود.
static void StartAction() { var start = Observable.Start(() => { Console.Write("Observable.Start"); for (int i = 0; i < 10; i++) { Thread.Sleep(100); Console.Write("."); } }); start.Subscribe( onNext: unit => Console.WriteLine("published"), onCompleted: () => Console.WriteLine("completed")); } static void StartFunc() { var start = Observable.Start(() => { Console.Write("Observable.Start"); for (int i = 0; i < 10; i++) { Thread.Sleep(100); Console.Write("."); } return "value"; }); start.Subscribe( onNext: Console.WriteLine, onCompleted: () => Console.WriteLine("completed")); }
زمانیکه از Func استفاده میشود، تابع یک خروجی را ارائه داده و سپس توالی خاتمه مییابد. اگر از Action استفاده شود، نوع Observable بازگشت داده شده از نوع Unit است که در برنامه نویسی functional معادل void است و هدف از آن مشخص سازی پایان عملیات Action میباشد. Unit دارای مقداری نبوده و صرفا سبب اجرای اطلاع رسانی OnNext میشود.
تفاوت مهم Observable.Start و Observable.Return در این است که Observable.Start مقدار تابع را به صورت تنبل (lazily) پردازش میکند، اما Observable.Return پردازش حریصانهای (eagrly) را به همراه خواهد داشت. به این ترتیب Observable.Start بسیار شبیه به یک Task (پردازشهای غیرهمزمان) عمل میکند.
در اینجا شاید این سؤال مطرح شود که استفاده از قابلیتهای Async سیشارپ 5 برای اینگونه کارها مناسب است یا Rx؟ قابلیتهای Async بیشتر به اعمال مخصوص IO bound مانند کار با شبکه، دریافت فایل از اینترنت، کار با یک بانک اطلاعاتی خارج از مرزهای سیستم، مرتبط میشوند؛ اما اعمال CPU bound مانند محاسبات سنگین حاصل از توالیهای observable را به خوبی میتوان توسط Rx مدیریت کرد.
2) تبدیل Events به معادل Observable
دات نت از روزهای اول خود به همراه یک event driven programming model بودهاست. Rx متدهایی را برای دریافت یک رخداد و تبدیل آن به یک توالی Observable ارائه دادهاست. برای نمونه ObservableCollection زیر را درنظر بگیرید
var items = new System.Collections.ObjectModel.ObservableCollection<string> { "Item1", "Item2", "Item3" };
items.CollectionChanged += (sender, ea) => { if (ea.Action == NotifyCollectionChangedAction.Remove) { foreach (var oldItem in ea.OldItems.Cast<string>()) { Console.WriteLine("Removed {0}", oldItem); } } };
var removals = Observable.FromEventPattern<NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventArgs> ( addHandler: handler => items.CollectionChanged += handler, removeHandler: handler => items.CollectionChanged -= handler ) .Where(e => e.EventArgs.Action == NotifyCollectionChangedAction.Remove) .SelectMany(c => c.EventArgs.OldItems.Cast<string>()); var disposable = removals.Subscribe(onNext: item => Console.WriteLine("Removed {0}", item));
items.Remove("Item1");
3) تبدیل Task به معادل Observable
متد ToObservable واقع در فضای نام System.Reactive.Threading.Tasks را بر روی یک Task نیز میتوان فراخوانی کرد:
var task = Task.Factory.StartNew(() => "Test"); var source = task.ToObservable(); source.Subscribe(Console.WriteLine, () => Console.WriteLine("completed"));
4) تبدیل IEnumerable به معادل Observable
با این مورد تاکنون آشنا شدهاید. فقط کافی است متد ToObservable را بر روی یک IEnumerable، جهت تهیه خروجی Observable فراخوانی کرد.
5) تبدیل APM به معادل Observable
APM یا Asynchronous programming model، همان روش کار با متدهای Async با نامهای BeginXXX و EndXXX است که از نگارشهای آغازین دات نت به همراه آن بودهاند. کار کردن با آن مشکل است و مدیریت آن به همراه پراکندگیهای بسیاری جهت کار با callbacks آن است. برای تبدیل این نوع روش برنامه نویسی به روش Rx نیز متدهایی پیش بینی شدهاست؛ مانند Observable.FromAsyncPattern.
یک نکته
کتابخانهای به نام Rxx بسیاری از این محصور کنندهها را تهیه کردهاست:
http://Rxx.codeplex.com
ابتدا بستهی نیوگت آنرا نصب کنید:
PM> Install-Package Rxx
using (new FileStream("file.txt", FileMode.Open) .ReadToEndObservable() .Subscribe(x => Console.WriteLine(x.Length))) { Console.ReadKey(); }
پَرباد چیست؟
آنچه که شما در این مطلب یاد خواهید گرفت:
- طریقه نصب
- ایجاد صورتحساب و ارسال کاربر به درگاه پرداخت
- تایید صورتحساب
- مردود کردن صورتحساب قبل از انتقال وجه از مشتری به فروشنده
- برگشت وجه به حساب مشتری پس از تأیید صورتحساب
- درگاه مجازی پرداخت (برای تست وب اپلیکیشن، بدون داشتن حساب واقعی در درگاههای بانکی)
- تنظیمات
- ذخیره سازی اطلاعات پرداخت
طریقه نصب
PM> Install-Package Parbad
PM> Install-Package Parbad.MVC5
ایجاد صورتحساب و ارسال کاربر به درگاه پرداخت
var invoice = new Invoice( [Order Number], [Amount], [Verify URL]);
var invoice = new Invoice(1, 30000, "http://www.mywebsite.com/payment/verify" );
var result = Payment.Request(Gateways.Mellat, invoice);
if (result.Status == RequestResultStatus.Success) { // این متد، کاربر را به سمت وب سایت درگاه پرداخت هدایت میکند result.Process(Context); } else { // در صورت تمایل میتوانید پیغام مورد نظر از درگاه پرداخت را نمایش دهید var msg = result.Message; }
در وب سایتهای MVC میتوانید به روش زیر عمل کنید
if (result.Status == RequestResultStatus.Success) { // کاربر را به سمت وب سایت درگاه پرداخت هدایت میکند return new RequestActionResult(result); } else { return View("Error"); }
تأیید صورتحساب
var result = Payment.Verify(System.Web.HttpContext.Current);
if(result.Status == VerifyResultStatus.Success) { // کاربر، صورتحساب را پرداخت کرده است و شما میتوانید ادامه عملیات خرید را انجام دهید } else { // کاربر بنا به دلایلی صورتحساب را پرداخت نکرده است // شما همچنین میتوانید علت را در قالب یک پیام از پراپرتی پیام مشاهده کنید // بنابراین شما میتوانید این صورتحساب را در پایگاه داده خود مردود اعلام کنید }
مردود کردن صورتحساب قبل از انتقال وجه از مشتری به فروشنده
همانطور که در تصویر میبینید، در هنگام بازگشت مشتری به وب سایت شما و تأیید کردن صورتحساب، شما میتوانید اطلاعات تراکنش مورد نظر را که شامل، درگاه پرداخت بانکی، شماره سفارش و شماره رجوع است را دریافت کنید و سپس با استفاده از این اطلاعات، پایگاه داده خود را بررسی کرده و در صورت لزوم، متد Cancel را فراخوانی کنید. به این ترتیب به درگاه بانکی، هیچگونه تأییدیه ای اعلام نمیشود و این بدان معناست که اگر وجهی به حساب فروشگاه واریز شده باشد، پس از چند دقیقه (معمولا ۱۵ دقیقه) به حساب مشتری برگشت داده خواهد شد.
برگشت وجه به حساب مشتری پس از تأیید صورتحساب
var refundResult = Payment.Refund(new RefundInvoice([Order Number], [Amount]));
درگاه مجازی پرداخت
var result = Payment.Request(Gateways.ParbadVirtualGateway, invoice);
در نتیجه در هنگام هدایت کاربر به درگاه پرداخت، کاربر به درگاه مجازی هدایت خواهد شد.
<system.webServer> <handlers> <add name="ParbadGatewayPage" verb="*" path="Parbad.axd" type="Parbad.Web.Gateway.ParbadVirtualGatewayHandler" /> </handlers> </system.webServer>
ParbadConfiguration.Gateways.ConfigureParbadVirtualGateway(new ParbadVirtualGatewayConfiguration("Parbad.axd"));
تنظیمات پَرباد
public class Global : HttpApplication { void Application_Start(object sender, EventArgs e) { // configurations } }
public class Startup { public void Configuration(IAppBuilder app) { // configurations } }
تنظیمات درگاههای پرداخت
تنظیمات ذخیره سازی اطلاعات پرداخت
ParbadConfiguration.Storage = new SqlServerStorage("Connection String", "MyPaymentTableName");
public class MyStorage : Storage { // Implement methods here... }
ParbadConfiguration.Storage = new MyStorage();
در صورتیکه هر گونه پیشنهاد یا انتقاد نسبت به کارکرد این سیستم دارید، صمیمانه منتظر شنیدن آن در راستای توسعه این سیستم هستم.همچنین در صورت تمایل به توسعه آن، میتوانید آن را در گیت هاب دنبال کنید و یا لطفا پیشنهادات، بحثها و نظرات خود را در صفحه مخصوص این پروژه ارسال کنید.با تشکر.
Serialization #2
مطابق آنچه در قسمت قبل گفته شد برای آنکه بتوان از مدل News برای سریالیکردن استفاده کرد، باید آن را به شکل ذیل پیادهسازی کرد:
[DataContract] public class News { [DataMember] public int Id; [DataMember] public string Body; [DataMember] public DateTime NewsDate; }
با Override کردن [DataContract]به صورت [("DataContract(Name=”MyCustomNews] میتوان نام ریشه XML فایل را به MyCustomNews تغییر داد. همچنین با Override کردن [DataMember] بصورت [("DataMember(Name=”MyCustomFieldName] میشود به هر فیلدی عنوان دلخواهی داد و همچنین با تعیین عبارت NameSpace به صورت [("DataContract(Name = "MyCustomNews", Namespace = "http://www.my.com] میشود فضای نام را تغییر داد که با این تغییرات، خروجی زیر حاصل میشود:
<?xml version="1.0" encoding="utf-8"?> <MyCustomNews xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.my.com"> <Body>NewsBody</Body> <MyCustomFieldName>111</MyCustomFieldName> <NewsDate>2012-10-04T00:00:00</NewsDate> </MyCustomNews>
ویژگی [DataMember] هم ازفیلدها و هم از propertyها، پشتیبانی میکند، خواه عمومی باشند یا خصوصی و نوع فیلد یا Property میتواند به یکی از اشکال زیر باشد:
- انواع اولیه .
- انواع DateTime ،TimeSpan، Guid ،Uri و انواع Enum
- انواع پوچ پذیر هر کدام از موارد بالا
- نوع byte[]
- انواع تعریف شده توسط کاربر که توسط صفت [DataContract] محصور شدهاند.
- هر نوع IEnumerable
- هر نوعی که با صفت [Serializable] محصور شود و یا اینترفیس ISerializable را پیاده سازی کند.
- هر نوعی که اینترفیس IXmlSerializble را پیاده سازی نماید.
تعیین فرمت باینری برای سریالیکردن:
برای سریالی کنندههای DataContractSerializer و NetDataContractSerializer میتوان به روش زیر فرمت خروجی را به شکل فرمت باینری درآورد که خروجی آن تاحد زیادی کوچکتر و کم حجمتر میشود:
var s = new MemoryStream(); using (XmlDictionaryWriter w=XmlDictionaryWriter.CreateBinaryWriter(s)) { ds.WriteObject(w,news); }
و برای Deserialize کردن آن به شیوه زیر عمل میکنیم:
var s2 = new MemoryStream(s.ToArray()); News deserializednews; using (XmlDictionaryReader r=XmlDictionaryReader.CreateBinaryReader(s2,XmlDictionaryReaderQuotas.Max)) { deserializednews = (News)ds.ReadObject(r); }
که در آن از ویژگی Max کلاس XmlDictionaryReaderQuotas برای به دست آوردن حداکثر سهمیه فضای دیسک مربوط به XmlDictionaryReaders استفاده میشود.
شاید بعضی از سایتها را دیده باشید که در حین ثبت نام، پس از وارد کردن یک نام کاربری و سپس مشغول شدن به پر کردن فیلد کلمهی عبور، در قسمت نام کاربری شروع به جستجو در مورد آزاد بودن نام کاربری درخواستی میکنند یا نمونهای دیگر، فرم پرداخت الکترونیکی بانک سامان. پس از اینکه شماره قبض را وارد کردید، بلافاصله بدون ریفرش صفحه به شما پیغام میدهد که این شماره معتبر است یا خیر. امروز قصد داریم این قابلیت را با استفاده از کتابخانهی Ajax مجموعه jQuery در ASP.Net پیاده سازی کنیم (بدون استفاده از ASP.Net Ajax مایکروسافت).
ابتدا سورس کامل را ملاحظه نمائید:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="AjaxTest.aspx.cs" Inherits="testWebForms87.AjaxTest" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>jQuery Ajax Text</title>
<script src="jquery.min.js" type="text/javascript"></script>
<script type="text/javascript">
$(document).ready(function() {
$("#<%= TextBox1.ClientID %>").blur(function(event) {
$.ajax({
type: "POST",
url: "AjaxTest.aspx/IsUserAvailable",
data: "{'username': '" + $('#<%= TextBox1.ClientID %>').val() + "'}",
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function(msg) {
$('#valid').html("<img src='ajaxImages/waiting.gif' alt='لطفا کمی تامل کنید'>");
var delay = function() {
AjaxSucceeded(msg);
};
setTimeout(delay, 2000); //remove this
},
error: AjaxFailed
});
});
});
function AjaxSucceeded(result) {
if (result.d == true)
$('#msg').html("<img src='ajaxImages/available.gif' alt='نام کاربری درخواستی موجود است'>");
else
$('#msg').html("<img src='ajaxImages/taken.gif' alt='متاسفانه نام کاربری مورد نظر پیشتر دریافت شدهاست'>");
}
function AjaxFailed(result) {
alert(result.status + ' ' + result.statusText);
}
</script>
</head>
<body>
<form id="form1" runat="server">
<div>
user name:
<asp:TextBox ID="TextBox1" runat="server"></asp:TextBox>
<span id="msg"></span>
<br />
pass:
<asp:TextBox ID="TextBox2" TextMode="Password" runat="server"></asp:TextBox>
</div>
<!-- preload -->
<div style="display: none">
<img src="ajaxImages/available.gif" alt="available" />
<img src="ajaxImages/taken.gif" alt="taken" />
<img src="ajaxImages/waiting.gif" alt="waiting" />
</div>
</form>
</body>
</html>
using System;
using System.Web.Services;
namespace testWebForms87
{
public partial class AjaxTest : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
}
[WebMethod]
public static bool IsUserAvailable(string username)
{
// این مورد را با خواندن اطلاعات از دیتابیس میشود تعویض کرد
return username != "test";
}
}
}
همانطور که ملاحظه میکنید صفحهی ASP.Net ما بسیار ساده است و از دو تکست باکس استاندارد تشکیل میشود، به همراه تصاویر مربوط به Ajax که یک سری تصاویر ساده چرخان معروف منتظر بمانید ، یافت شد یا موجود نیست میباشند. این تصاویر در یک div مخفی (display: none) در صفحه قرار گرفتهاند و در هنگام بارگذاری صفحه، اینها نیز بارگذاری شده و حاضر و آماده خواهند بود. بنابراین هنگام استفاده از آنها، کاربر تاخیری را مشاهده نخواهد کرد. همچنین یک span با id مساوی msg را هم پس از تکست باکس اضافه کردهایم تا تصاویر مربوط به رخدادهای Ajax را با استفاده از تواناییهای jQuery به آن اضافه کنیم.
اسکریپت Ajax ما با دراختیار گرفتن روال رخداد گردان blur شیء textBox1 شروع میشود. همانطور که در مقالات پیشین سایت نیز ذکر شد، روش صحیح دریافت ID یک کنترل ASP.Net در کدهای سمت کلاینت جاوا اسکریپتی، بر اساس خاصیت ClientID آن است که در اولین سطر کدهای ما مشخص است (زیرا در ASP.Net نام و ID یک کنترل در هنگام رندر شدن به همراه ID کنترلهای دربرگیرنده آن نیز خواهد بود، بنابراین بهتر است این مورد را داینامیک کرد).
کار بررسی موجود بودن نام کاربری (یا مثلا یک شماره قبض و امثال آن) توسط WebMethod ایی به نام IsUserAvailable در code behind صفحه انجام میشود که پیاده سازی آنرا ملاحظه میکنید. بدیهی است در این مثال ساده، تنها نام کاربری از پیش رزرو شده، کلمهی test است و در یک کد واقعی این مورد با مقایسهی نام کاربری با اطلاعات موجود در دیتابیس باید صورت گیرد (و حملات تزریق اس کیوال را هم فراموش نکنید. برای رهایی از آنها "حتما" باید از پارامترهای ADO.Net استفاده کرد و گرنه کد شما مستعد به این نوع حملات خواهد بود).
سؤال: چرا از web method استفاده شد و همچنین چرا این متد static است؟
زمانیکه یک متد با کلمه کلیدی static مشخص میشود حالت state less پیدا میکند یعنی مستقل از وهلهی کلاس عمل میکند. در این حالت نیازی به ارسال ViewState نبوده (بنابراین در کوئری مورد نظر ما بسیار بهینه و سبک عمل میکنند) و همچنین نیازی به ایجاد یک وهلهای از کلاس صفحهی ما نیز نخواهد بود. برای توضیحات بیشتر به این مقاله مراجعه نمائید. (به صورت خلاصه، دلیل اصلی، کارآیی بالا و بهینه بودن این روش در این مساله ویژه است و در ASP.Net Ajax مایکروسافت به صورت گستردهای در پشت صحنه مورد استفاده قرار میگیرد)
استفاده از ویژگی WebMethod عملکرد صفحهی ما را شبیه به یک وب سرویس خواهد کرد و امکان دسترسی به آن در متدهای استاندارد POST به صورت ارسال دیتا به آدرس WebService.asmx/WebMethodName خواهد بود. یک مثال ساده و عملی
بررسی تابع Ajax بکار رفته:
این تابع هنگام فراخوانی رخداد blur تکستباکس ما (مطابق کد فوق) فراخوانی میشود. ساختار سادهای دارد که به شرح زیر است:
type: "POST"
url: "AjaxTest.aspx/IsUserAvailable"
data: "{'username': '" + $('#<%= TextBox1.ClientID %>').val() + "'}",
contentType: "application/json; charset=utf-8",
dataType: "json",
<xx yy="nn"></xx>
{ "xx": {"yy":"nn"} }
success: function(msg)
error: AjaxFailed
در این مثال برای نمایش بهتر عملیات، یک وقفهی 2 ثانیهای توسط setTimeout ایجاد شده و بدیهی است در یک مثال واقعی باید آنرا حذف نمود.
نکته: با استفاده از افزونهی فایرباگ فایرفاکس، میتوان جزئیات این عملیات را بهتر مشاهده نمود:
اصل چهارم: Starve for loosely coupled designs
"به دنبال طراحی با اتصال سست بین اجزا باش"
اتصال بین اجزای برنامه نویسی باعث سختتر شدن مدیریت تغییرات میشود؛ چرا که با تغییر یک بخش، بخشهای متصل نیز دچار مشکل خواهند شد. اتصالها از لحاظ نوع قدرت متفاوتند و اساسا سیستمی بدون اتصال وجود ندارد. لذا باید به دنبال یک طراحی با کمترین میزان قدرت اتصال یا همان سست اتصال باشیم.
تا به اینجا، اصلهای دوم و سوم ما را در کاهش وابستگی و اتصال قوی کمک کردهاند. استفاده از واسطها، باعث کاهش وابستگی به نوع پیاده سازی میشود. استفاده از ترکیب نیز به نوعی باعث از بین رفتن وابستگی قوی بین کلاسهای فرزند و کلاس والد میشود و با روشی دیگر (استفاده از شیء در برگرفته شده برای پیاده سازی وظیفهی تغییر کننده) وظایف را در کلاسها پیاده سازی میکند. در زیر نمونهی اتصال قوی و نتیجهی آن را میبینیم:
public class StrongCoupledConcreteA { public string GenerateString(string s) { return s + " from" + this.GetType().ToString(); } } public class StrongCoupledConcreteB { public void GenerateString(ref string s) { s += " from" + this.GetType().ToString(); } } public class Printer { bool condition; public Printer(bool cond) { condition = cond; } public void SetCondition(bool value) { condition = value; } public void Print() { string result; string input = " this message is"; if (condition) { var stringGenerator = new StrongCoupledConcreteA(); result = stringGenerator.GenerateString(input); } else { var stringGenerator = new StrongCoupledConcreteB(); result = input; stringGenerator.GenerateString(ref result); } Console.WriteLine(result); } } public class Context { Printer printer; public void DoWork() { printer = new Printer(true); printer.Print(); printer.SetCondition(false); printer.Print(); } }
حال کد بازنویسی شده را با آن مقایسه کنید:
public interface IStringGenerator { string GenerateString(string s); } public class LooslyCoupledConcreteA : IStringGenerator { public string GenerateString(string s) { return s + " from " + this.GetType().ToString(); } } public class LooslyCoupledConcreteB : IStringGenerator { public string GenerateString(string s) { return s + " from " + this.GetType().ToString(); } } public class Printer { bool condition; public Printer(bool cond) { condition = cond; } public void SetCondition(bool value) { condition = value; } public void Print() { string result; string input = " this message is"; IStringGenerator generator; if (condition) { generator = new LooslyCoupledConcreteA(); } else { generator = new LooslyCoupledConcreteB(); } result = generator.GenerateString(input); Console.WriteLine(result); } }
با کمی دقت مشاهده میکنیم که در کلاسهای strongly coupled با اینکه هدف هر دو کلاس تولید یک رشته است، ولی عدم وجود پروتکل باعث شده است نحوهی گرفتن ورودی و برگرداندن خروجی متفاوت شود و در نتیجه نیازمند به اضافه کردن پیچیدگی در کلاس فراخوانی کنندهی آنها میشویم. این در حالی است که در روش loosely coupled با ایجاد یک پروتکل (واسط IStringGenerator ) این پیچیدگی از بین رفته است. در اینجا نوع اتصال (وابستگی) از جنس اتصال (وابستگی) قوی به تعریف (prototype) و شاید به نوعی نحوهی پیاده سازی متد میباشد.
SOLID Principles *
پنج اصل بعدی به اصول SOLID معروف هستند.
S: Single Responsibility
O: Open/Closed
L: Liskov’s Substitution
I: Interface Segregation
D: Dependency Injection
اصل پنجم: Single responsibility
"به دنبال ماژولهای تک مسئولیتی باش"
در این قسمت مقصود از مسئولیت، «دلیلی است که کلاس باید تغییر کند» بدین معنا که اگر کلاسی با چند دلیل متفاوت مجبور به تغییر شود، آن کلاس چند مسئولیتی است. کلاسهای چند مسئولیتی عموما کد حجیمی دارند؛ نام آنها تعریف دقیقی را از مسئولیتشان ارائه نمیدهد و با عنوانی بسیار کلی نامگذاری میشوند و اشکال زدایی آنها بسیار طاقت فرساست. از طرفی، چند مسئولیتی بودن یک کلاس، باعث از بین رفتن مزایای توارث میشود. مثلا فرض کنید دو مسئولیت A,B در واسطی بیان میشوند که به یکدیگر مرتبط نبوده و مستقلند. برای مسئولیت A دو پیاده سازی و برای مسئولیت B، سه پیاده سازی در نظر گرفته شده است و جمعا برای پشتیبانی از تمامی حالات باید شش کلاس پیاده ساز، در نظر گرفته شود که توارث را سخت و بی معنی میکند زیرا قابلیت استفاده مجدد را از توارث سلب کرده است. با این وجود عملا رعایت همچین نکتهای در دنیای واقعی کار سختی است.
مثال زیر این مشکل را بیان میدارد:
// single responsibility principle - bad example interface IEmail { void SetSender(string sender); void SetReceiver(string receiver); void SetContent(string content); } class Email : IEmail { public void SetSender(string sender) { throw new NotImplementedException(); } public void SetReceiver(string receiver) { throw new NotImplementedException(); } public void SetContent(string content) { throw new NotImplementedException(); } }
در این مثال کلاس Email دارای دو مسئولیت (دلیل برای تغییر) است: الف- نحوه مقداردهی فرستنده و گیرنده براساس پروتکلهای مختلف مانند IMAP, POP3 ، بدین معنا که با تغییر پروتکل نیاز به تغییر پیاده سازی خواهیم شد. ب- تعریف محتوای پیام، بدین معنا که برای پشتیبانی از محتوای html, xml نیاز به تغییر کلاس Email داریم.
با تغییر طراحی خواهیم داشت:
// single responsibility principle - good example public interface IMessage { void SetSender(string sender); void SetReceiver(string receiver); void SetContent(IContent content); } public interface IContent { string GetAsString(); // used for serialization } public class Email : IMessage { public void SetSender(string sender) { throw new NotImplementedException(); } public void SetReceiver(string receiver) { throw new NotImplementedException(); } public void SetContent(IContent content) { throw new NotImplementedException(); } }
در اینجا واسط IContent مسئولیت پشتیبانی از xml, html را
خواهد داشت و نیازی به تغییر کلاس Email برای
پشتیبانی از این فرمتهای محتوای پیام را نخواهیم داشت.
اصل ششم: Open for
extension, close for modification : Open/Closed Principle
"پذیرای توسعه و
بازدارنده از تغییر هر آنچه که هست، باش"
ا ین اصل میگوید طراحی باید به گونهای باشد که با
اضافه شدن یک ویژگی، کدهای قبلی تغییری نکنند و فقط کدهای جدید برای پیاده سازی
ویژگی جدید نوشته شوند.
public class AreaCalculator { public double Area(object[] shapes) { double area = 0; foreach (var shape in shapes) { if (shape is Square) { Square square = (Square)shape; area += Math.Sqrt(square.Height); } if (shape is Triangle) { Triangle triangle = (Triangle)shape; double TotalHalf = (triangle.FirstSide + triangle.SecondSide + triangle.ThirdSide) / 2; area += Math.Sqrt(TotalHalf * (TotalHalf - triangle.FirstSide) * (TotalHalf - triangle.SecondSide) * (TotalHalf - triangle.ThirdSide)); } if (shape is Circle) { Circle circle = (Circle)shape; area += circle.Radius * circle.Radius * Math.PI; } } return area; } } public class Square { public double Height { get; set; } } public class Circle { public double Radius { get; set; } } public class Triangle { public double FirstSide { get; set; } public double SecondSide { get; set; } public double ThirdSide { get; set; } }
در اینجا کلاس AreaCalculator برای محاسبه مساحت تمام اشیاء ورودی، مساحت تک تک اشیاء را محاسبه میکند و نتیجه را برمیگرداند. در این مثال با اضافه شدن شکل هندسی جدید، باید کد این کلاس تغییر کند که با اصل Open/Closed مغایر است. برای بهبود این کد طراحی زیر پیشنهاد شده است:
public class AreaCalculator { public double Area(Shape[] shapes) { double area = 0; foreach (var shape in shapes) { area += shape.Area(); } return area; } } public abstract class Shape { public abstract double Area(); } public class Square : Shape { public double Height { get { return _height; } } private double _height; public Square(double Height) { _height = Height; } public override double Area() { return Math.Sqrt(_height); } } public class Circle : Shape { public double Radius { get { return _radius; } } private double _radius; public Circle(double Radius) { _radius = Radius; } public override double Area() { return _radius * _radius * Math.PI; } } public class Triangle : Shape { public double FirstSide { get { return _firstSide; } } public double SecondSide { get { return _secondSide; } } public double ThirdSide { get { return _thirdSide; } } private double _firstSide; private double _secondSide; private double _thirdSide; public Triangle(double FirstSide, double SecondSide, double ThirdSide) { _firstSide = FirstSide; _secondSide = SecondSide; _thirdSide = ThirdSide; } public override double Area() { double TotalHalf = (_firstSide + _secondSide + _thirdSide) / 2; return Math.Sqrt(TotalHalf * (TotalHalf - _firstSide) * (TotalHalf - _secondSide) * (TotalHalf - _thirdSide)); } }
در این طراحی، پیچیدگی محاسبه مساحت هر شکل به کلاس آن شکل منتقل شده است و با اضافه شدن شکل جدید نیازی به تغییر کلاس AreaCalculator نداریم.
در مقالهی بعدی به سه اصل دیگر اصول SOLID خواهم پرداخت.
بررسی Semantic Search و FTS Table-valued functions
توابع Predicates مختص به FTS مانند Contains و Freetext، تنها ردیفهای متناظر با جستجوی انجام شده را باز میگردانند و رتبهای به نتایج جستجو اعمال نمیگردد. برای مثال، مشخص نیست اولین ردیف بازگشت داده شده بهترین تطابق را با جستجوی انجام شده دارد یا بدترین نتیجهی ممکن است. برای رفع این مشکل FTS table-valued functions معرفی شدهاند. حاصل اینها یک جدول با دو ستون است. ستون اول کلید متناظر با جدول تطابق یافته بوده و ستون دوم، Rank نام دارد که بیانگر میزان مفید بودن و درجهی اعتبار ردیف بازگشت داده شدهاست.
Semantic Search نیز به کمک سه table-valued functions پیاده سازی میشود. همچنین باید دقت داشت که تمام زبانهای پشتیبانی شده توسط FTS در حالت Semantic Search پشتیبانی نمیشوند. برای بررسی این مورد، دو کوئری ذیل را اجرا نمائید:
-- Full text Languages SELECT * FROM sys.fulltext_languages ORDER BY name; -- Semantic Search Languages SELECT * FROM sys.fulltext_semantic_languages ORDER BY name; GO
بررسی table-valued functions مختص به FTS
دو متد ویژهی CONTAINSTABLE و FREETEXTTABLE خروجی از نوع جدول دارند؛ با ستونهایی به نامهای key و rank. اگر قسمت ایجاد کاتالوگ FTS و ایندکس آنرا بخاطر داشته باشید، در حین ایجاد ایندکس FTS میبایستی KEY INDEX PK_Documents را نیز ذکر کرد. کاربرد آن در همین table-valued functions است.
مقدار rank، عددی است بین 0 و 1000 که هر چقدر مقدار آن بیشتر باشد، یعنی نتیجهای نزدیکتر، به عبارت جستجو شده، یافت گردیدهاست. باید دقت داشت که این عدد فقط در زمینهی یک کوئری معنا پیدا میکند و مقایسهی rank دو کوئری مختلف با هم، بیمعنا است.
عملکرد CONTAINSTABLE بسیار شبیه به متد Contains است با این تفاوت که قابلیتهای بیشتری دارد. برای مثال در اینجا میتوان برای قسمتی از جستجو، وزن و اهمیت بیشتری را قائل شد و این حالت تنها زمانی معنا پیدا میکند که خروجی جستجو، دارای rank باشد.
متد FREETEXTTABLE نیز بسیار شبیه به FREETEXT عمل کرده و نسبت به CONTAINSTABLE بسیار سادهتر است. برای نمونه امکان تعریف وزن، formsof، near و غیره در اینجا وجود ندارد. به علاوه عملگرهای منطقی مانند and و or نیز در اینجا کاربردی نداشته و صرفا یک noise word درنظر گرفته میشوند.
چند مثال جهت بررسی عملکرد دو متد CONTAINSTABLE و FREETEXTTABLE
استفاده از متد CONTAINSTABLE
-- Rank with CONTAINSTABLE SELECT D.id, D.title, CT.[RANK], D.docexcerpt FROM CONTAINSTABLE(dbo.Documents, docexcerpt, N'data OR level') AS CT INNER JOIN dbo.Documents AS D ON CT.[KEY] = D.id ORDER BY CT.[RANK] DESC;
این متد ابتدا نام جدول مورد بررسی را دریافت میکند. سپس ستونی که باید جستجو بر روی آن انجام شود و در ادامه عبارت جستجو شونده، مشخص میگردد. اگر این متد را به تنهایی اجرا کنیم:
SELECT * FROM CONTAINSTABLE(dbo.Documents, docexcerpt, N'data OR level')
استفاده از متد FREETEXTTABLE
-- Rank with FREETEXTTABLE SELECT D.id, D.title, FT.[RANK], D.docexcerpt FROM FREETEXTTABLE (dbo.Documents, docexcerpt, N'data level') AS FT INNER JOIN dbo.Documents AS D ON FT.[KEY] = D.id ORDER BY FT.[RANK] DESC;
در اینجا اگر نیاز باشد تا تعداد نتایج را شبیه به کوئریهای top n محدود نمود، میتوان از پارامتر عددی بعدی که برای نمونه به 2 تنظیم شدهاست، استفاده کرد:
-- Rank with FREETEXTTABLE and top_n_by_rank SELECT D.id, D.title, FT.[RANK], D.docexcerpt FROM FREETEXTTABLE (dbo.Documents, docexcerpt, N'data level', 2) AS FT INNER JOIN dbo.Documents AS D ON FT.[KEY] = D.id ORDER BY FT.[RANK] DESC;
تعیین وزن و اهمیت کلمات در حال جستجو
-- Weighted terms SELECT D.id, D.title, CT.[RANK], D.docexcerpt FROM CONTAINSTABLE (dbo.Documents, docexcerpt, N'ISABOUT(data weight(0.8), level weight(0.2))') AS CT INNER JOIN dbo.Documents AS D ON CT.[KEY] = D.id ORDER BY CT.[RANK] DESC;
انجام جستجوهای Proximity
-- Proximity term SELECT D.id, D.title, CT.[RANK] FROM CONTAINSTABLE (dbo.Documents, doccontent, N'NEAR((data, row), 30)') AS CT INNER JOIN dbo.Documents AS D ON CT.[KEY] = D.id ORDER BY CT.[RANK] DESC; GO
بررسی Semantic Search key valued functions
متد SEMANTICKEYPHRASETABLE کار بازگشت واژههای کلیدی آنالیز شده توسط FTS را انجام داده و جدولی حاوی 4 ستون را باز میگرداند. این چهار ستون عبارتند از:
- column_id: شماره ستون واژه کلیدی یافت شدهاست. تفسیر آن نیاز به استفاده از تابع سیستمی COL_NAME دارد (مانند مثال زیر).
- document_key: متناظر است با کلید اصلی جدولی که بر روی آن کوئری گرفته میشود.
- keyphrase: همان واژه کلیدی است.
- score: رتبهی واژه کلیدی است در بین سایر واژههایی که بازگشت داده شده و عددی است بین صفر تا یک.
مثالی از آنرا در ادامه ملاحظه میکنید:
-- Top 100 semantic key phrases SELECT TOP (100) D.id, D.title, SKT.column_id, COL_NAME(OBJECT_ID(N'dbo.Documents'), SKT.column_id) AS column_name, SKT.document_key, SKT.keyphrase, SKT.score FROM SEMANTICKEYPHRASETABLE (dbo.Documents, doccontent) AS SKT INNER JOIN dbo.Documents AS D ON SKT.document_key = D.id ORDER BY SKT.score DESC;
در متد جدولی SEMANTICKEYPHRASETABLE، ابتدا جدول مورد نظر و سپس ستونی که نیاز است واژههای کلیدی آنالیز شدهی آن بازگشت داده شوند، قید میگردند. document_key آن به تنهایی شاید مفید نباشد. به همین جهت join شدهاست به جدول اصلی، تا بتوان رکوردهای متناظر را نیز بهتر تشخیص داد.
به این ترتیب مهمترین واژههای کلیدی ستون doccontent را به همراه درجهی اهمیت و رتبهی آنها، میتوان گزارش گرفت.
متد SEMANTICSIMILARITYTABLE برای یافتن سندهای مشابه با یک سند مشخص بکار میروند؛ چیزی شبیه به گزارش «مقالات مشابه مطلب جاری» در بسیاری از سایتهای ارائهی محتوا. ستونهای خروجی آن عبارتند از:
- source_column_id: شماره ستون منبع انجام کوئری.
- matched_column_id: شماره ستون سند مشابه یافت شده.
- matched_document_key: متناظر است با کلید اصلی جدولی که بر روی آن کوئری گرفته میشود.
- score: رتبهی نسبی سند مشابه یافت شده.
-- Documents that are similar to document 1 SELECT S.source_document_title, SST.matched_document_key, D.title AS matched_document_title, SST.score FROM (SEMANTICSIMILARITYTABLE (dbo.Documents, doccontent, 1) AS SST INNER JOIN dbo.Documents AS D ON SST.matched_document_key = D.id) CROSS JOIN (SELECT title FROM dbo.Documents WHERE id=1) AS S(source_document_title) ORDER BY SST.score DESC;
در این کوئری، اسناد مشابه با سند شماره 1 یافت شدهاند. مبنای جستجو نیز ستون doccontent، جدول dbo.Documents است. از join بر روی matched_document_key و id جدول اصلی، مشخصات سند یافت شده را میتوان استخراج کرد. کار CROSS JOIN تعریف شده، صرفا افزودن یک ستون مشخص به نتیجهی خروجی کوئری است.
همانطور که در تصویر مشخص است، سند شماره 4 بسیار شبیه است به سند شماره 1. در ادامه قصد داریم بررسی کنیم که علت این شباهت چه بودهاست؟
متد SEMANTICSIMILARITYDETAILSTABLE واژههای کلیدی مهم مشترک بین دو سند را بازگشت میدهد (سند منبع و سند مقصد). به این ترتیب میتوان دریافت، چه واژههای کلیدی سبب شدهاند تا این دو سند به هم شبیه باشند. ستونهای خروجی آن عبارتند از:
- keyphrase: واژهی کلیدی
- score: رتبهی نسبی واژهی کلیدی
-- Key phrases that are common across two documents SELECT SSDT.keyphrase, SSDT.score FROM SEMANTICSIMILARITYDETAILSTABLE (dbo.Documents, doccontent, 1, doccontent, 4) AS SSDT ORDER BY SSDT.score DESC;
در کوئری فوق قصد داریم بررسی کنیم چه واژههای کلیدی، سبب مشابهت سندهای شماره 1 و 4 شدهاند و بین آنها مشترک میباشند.
اگر چک لیستهای SEO وب سایت ها را مشاهده کنیم، میتوانیم آنها را در دو دستهی کلی بهینه سازی درونی و برونی وب سایت در نظر بگیریم:
Off-Page Optimization یا برونی ، که بیشتر بر دوش مشاوران سئو و خود مدیران وب سایت است.(link building ، فعالیت در شبکه اجتماعی و ...)
و اما در حوزه On-Page Optimization یا درونی که بخشهای مهمی از آن وظیفهی مابرنامه نویسها است.(H1 Tag ، URL Naming ، Meta Tags ، عنوان صفحه و ...)
[البته عامل درونی بهینه سازی محتوا (Content Optimization) که مهمترین عامل در الگوریتمهای نسل جدید موتورهای جستجو و همچنین الگوریتم جدید گوگل (و +) به حساب میآید بر عهده مشاوران سئو و خود مدیران وب سایت میباشد]
در ادامه به ارائه چند راهکار جهت بهینه سازی برنامههای وب ASP.NET مان برای موتورهای جستجو میپردازیم:
1.متدی برای ایجاد عنوان سایت
private const string SeparatorTitle = " - "; private const int MaxLenghtTitle = 60; public static string GeneratePageTitle(params string[] crumbs) { var title = ""; for (int i = 0; i < crumbs.Length; i++) { title += string.Format ( "{0}{1}", crumbs[i], (i < crumbs.Length - 1) ? SeparatorTitle : string.Empty ); } title = title.Substring(0, title.Length <= MaxLenghtTitle ? title.Length : MaxLenghtTitle).Trim(); return title; }
- MaxLenghtTitle پیشنهادی برای عنوان سایت 60 میباشد.
2.متدی برای ایجاد متاتگ صفحات سایت
public enum CacheControlType { [Description("public")] _public, [Description("private")] _private, [Description("no-cache")] _nocache, [Description("no-store")] _nostore }
private const int MaxLenghtTitle = 60; private const int MaxLenghtDescription = 170; private const string FaviconPath = "~/cdn/ui/favicon.ico"; public static string GenerateMetaTag(string title, string description, bool allowIndexPage, bool allowFollowLinks, string author = "", string lastmodified = "", string expires = "never", string language = "fa", CacheControlType cacheControlType = CacheControlType._private) { title = title.Substring(0, title.Length <= MaxLenghtTitle ? title.Length : MaxLenghtTitle).Trim(); description = description.Substring(0, description.Length <= MaxLenghtDescription ? description.Length : MaxLenghtDescription).Trim(); var meta = ""; meta += string.Format("<title>{0}</title>\n", title); meta += string.Format("<link rel=\"shortcut icon\" href=\"{0}\"/>\n", FaviconPath); meta += string.Format("<meta http-equiv=\"content-language\" content=\"{0}\"/>\n", language); meta += string.Format("<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\"/>\n"); meta += string.Format("<meta charset=\"utf-8\"/>\n"); meta += string.Format("<meta name=\"description\" content=\"{0}\"/>\n", description); meta += string.Format("<meta http-equiv=\"Cache-control\" content=\"{0}\"/>\n", EnumExtensions.EnumHelper<CacheControlType>.GetEnumDescription(cacheControlType.ToString())); meta += string.Format("<meta name=\"robots\" content=\"{0}, {1}\" />\n", allowIndexPage ? "index" : "noindex", allowFollowLinks ? "follow" : "nofollow"); meta += string.Format("<meta name=\"expires\" content=\"{0}\"/>\n", expires); if (!string.IsNullOrEmpty(lastmodified)) meta += string.Format("<meta name=\"last-modified\" content=\"{0}\"/>\n", lastmodified); if (!string.IsNullOrEmpty(author)) meta += string.Format("<meta name=\"author\" content=\"{0}\"/>\n", author); //------------------------------------Google & Bing Doesn't Use Meta Keywords ... //meta += string.Format("<meta name=\"keywords\" content=\"{0}\"/>\n", keywords); return meta; }
-
MaxLenghtDescription پیشنهادی برای متاتگ توضیح سایت 170 می باشد.
- آشنایی با متاتگها (Meta tags) و کاربرد آنها در صفحات وب (HTML)
- برای کاربرد allowIndexPage و allowFollowLinks هم میتوانید به لینک بالا و بررسی متاتگ robots بپردازید.
- با توجه به اهمیت شبکههای اجتماعی متاتگهای شبکههای اجتماعی (+ و +) را هم نباید از قلم انداخت.
- برای دریافت Description نوع سفارشی CacheControlType از پروژه متدهای الحاقی علیرضا اسم رام استفاده کردم.
3.متدی برای ایجاد Slug ( اسلاگ آدرسی با مفهوم برای بکار بردن در URL ها است که دوستدار موتورهای جستجو میباشد)
private const int MaxLenghtSlug = 45; public static string GenerateSlug(string title) { var slug = RemoveAccent(title).ToLower(); slug = Regex.Replace(slug, @"[^a-z0-9-\u0600-\u06FF]", "-"); slug = Regex.Replace(slug, @"\s+", "-").Trim(); slug = Regex.Replace(slug, @"-+", "-"); slug = slug.Substring(0, slug.Length <= MaxLenghtSlug ? slug.Length : MaxLenghtSlug).Trim(); return slug; } private static string RemoveAccent(string text) { var bytes = Encoding.GetEncoding("UTF-8").GetBytes(text); return Encoding.UTF8.GetString(bytes); }
- MaxLenghtSlug پیشنهادی برای عنوان سایت 45 میباشد.
نمونه ای از کاربرد توابع :
Head.InnerHtml = SEO.GenerateMetaTag ( title: SEO.GeneratePageTitle(".NET Tips", "آرشیو مطالب", "ASP.NET MVC #1"), description: "چرا ASP.NET MVC با وجود فریم ورک پختهای به نام ASP.NET web forms، اولین سؤالی که حین سوئیچ به ASP.NET MVC مطرح میشود این است: «برای چی؟». بنابراین تا به این سؤال پاسخ داده نشود، هر نوع بحث فنی در این مورد بی فایده است.", allowIndexPage: true, allowFollowLinks: true, author: "وحید نصیری", cacheControlType: SEO.CacheControlType._private );
<title>.NET Tips - آرشیو مطالب - ASP.NET MVC #1</title> <link rel="shortcut icon" href="../../cdn/images/ui/favicon.ico"/> <meta http-equiv="content-language" content="fa"/> <meta http-equiv="content-type" content="text/html; charset=utf-8"/> <meta charset="utf-8"/> <meta name="description" content="چرا ASP.NET MVC ؟با وجود فریم ورک پختهای به نام ASP.NET web forms، اولین سؤالی که حین سوئیچ به ASP.NET MVC مطرح میشود این است: «برای چی؟». بن ..."/> <meta http-equiv="Cache-control" content="private"/> <meta name="robots" content="index, follow" /> <meta name="expires" content="never"/> <meta name="author" content="وحید نصیری"/>