public class Image { public int Id { get; set; } public byte[] Data { get; set; } }
بالاخره گوگل کار تهیه API مخصوص ابزار Analytics خود را به پایان رساند و اکنون برنامه نویسها میتوانند همانند سایر سرویسهای گوگل از این ابزار گزارشگیری نمایند.
خلاصه کاربردی این API ، دو صفحه تعاریف پروتکل (+) و ریز مواردی (+) است که میتوان گزارشگیری نمود.
هنوز کتابخانه google-gdata جهت استفاده از این API به روز رسانی نشده است؛ بنابراین در این مقاله سعی خواهیم کرد نحوه کار با این API را از صفر بازنویسی کنیم.
مطابق صفحه تعاریف پروتکل، سه روش اعتبارسنجی جهت دریافت اطلاعات API معرفی شده است که در اینجا از روش ClientLogin که مرسومتر است استفاده خواهیم کرد.
مطابق مثالی که در آن صفحه قرار دارد، اطلاعاتی شبیه به اطلاعات زیر را باید ارسال و دریافت کنیم:
POST /accounts/ClientLogin HTTP/1.1
User-Agent: curl/7.15.1 (i486-pc-linux-gnu) libcurl/7.15.1
OpenSSL/0.9.8a zlib/1.2.3 libidn/0.5.18
Host: www.google.com
Accept: */*
Content-Length: 103
Content-Type: application/x-www-form-urlencoded
accountType=GOOGLE&Email=userName@google.com&Passwd=myPasswrd&source=curl-tester-1.0&service=analytics
HTTP/1.1 200 OK
Content-Type: text/plain
Cache-control: no-cache
Pragma: no-cache
Date: Mon, 02 Jun 2008 22:08:51 GMT
Content-Length: 497
SID=DQ...
LSID=DQAA...
Auth=DQAAAG8...
string getSecurityToken()
{
if (string.IsNullOrEmpty(Email))
throw new NullReferenceException("Email is required!");
if (string.IsNullOrEmpty(Password))
throw new NullReferenceException("Password is required!");
WebRequest request = WebRequest.Create("https://www.google.com/accounts/ClientLogin");
request.Method = "POST";
string postData = "accountType=GOOGLE&Email=" + Email + "&Passwd=" + Password + "&service=analytics&source=vahid-testapp-1.0";
byte[] byteArray = Encoding.ASCII.GetBytes(postData);
request.ContentType = "application/x-www-form-urlencoded";
request.ContentLength = byteArray.Length;
using (Stream dataSt = request.GetRequestStream())
{
dataSt.Write(byteArray, 0, byteArray.Length);
}
string auth = string.Empty;
using (WebResponse response = request.GetResponse())
{
using (Stream dataStream = response.GetResponseStream())
{
using (StreamReader reader = new StreamReader(dataStream))
{
string responseFromServer = reader.ReadToEnd().Trim();
string[] tokens = responseFromServer.Split('\n');
foreach (string token in tokens)
{
if (token.StartsWith("SID="))
continue;
if (token.StartsWith("LSID="))
continue;
if (token.StartsWith("Auth="))
{
auth = token.Substring(5);
}
else
{
throw new AuthenticationException("Error authenticating Google user " + Email);
}
}
}
}
}
return auth;
}
همانطور که ملاحظه میکنید به آدرس https://www.google.com/accounts/ClientLogin ، اطلاعات postData با متد POST ارسال شده (دقیقا مطابق توضیحات گوگل) و سپس از پاسخ دریافتی، مقدار نشانه Auth را جدا نموده و در ادامه عملیات استفاده خواهیم کرد. وجود این نشانه در پاسخ دریافتی به معنای موفقیت آمیز بودن اعتبار سنجی ما است و مقدار آن در طول کل عملیات باید نگهداری شده و مورد استفاده مجدد قرار گیرد.
سپس مطابق ادامه توضیحات API گوگل باید لیست پروفایلهایی را که ایجاد کردهایم پیدا نمائیم:
string getAvailableProfiles(string authToken)
{
return fetchPage("https://www.google.com/analytics/feeds/accounts/default", authToken);
}
متد fetchPage را از پیوست این مقاله میتوانید دریافت نمائید. خروجی یک فایل xml است که با انواع و اقسام روشهای موجود قابل آنالیز است، از کتابخانههای XML دات نت گرفته تا Linq to xml و یا روش serialization که من روش آخر را ترجیح میدهم.
مرحله بعد، ساخت URL زیر و دریافت مجدد اطلاعات مربوطه است:
string url = string.Format("https://www.google.com/analytics/feeds/data?ids={0}&metrics=ga:pageviews&start-date={1}&end-date={2}", id, from, to);
return fetchPage(url, auth);
فایلهای کلاسهای مورد استفاده را از اینجا دریافت نمائید.
مثالی در مورد نحوه استفاده از آن:
CGoogleAnalytics cga = new CGoogleAnalytics
{
Email = "username@gmail.com",
Password = "password",
From = DateTime.Now.Subtract(TimeSpan.FromDays(1)),
To = DateTime.Now.Subtract(TimeSpan.FromDays(1))
};
List<CGoogleAnalytics.SitePagePreviews> pagePreviews =
cga.GetTotalNumberOfPageViews();
foreach (var list in pagePreviews)
{
//string site = list.Site;
//int pw = list.PagePreviews;
}
اگر با MVC کار کرده باشید حتما با ModelBinding آن آشنا هستید؛ DefaultModelBinder توکار آن که در اکثر مواقع، باری زیادی را از روی دوش برنامه نویسان بر میدارد و کار را برای آنان راحتتر میکند.
اما در بعضی مواقع این مدل بایندر پیش فرض ممکن است پاسخگوی نیاز ما در
بایند کردن یک خصوصیت از یک مدل خاص نباشد، برای همین ما نیاز داریم که کمی آن را سفارشی سازی کنیم.
برای این کار ما دو راه داریم:
1) یک مدل بایندر جدید را با پیاده سازی IModelBinder تهیه کنیم. (در این حالت ما مجبوریم که مدل بایندر را از ابتدا جهت بایند
کردن کلیه مقادیر شی مدل خود، بازنویسی کنیم و در واقع امکان انتساب آنرا در سطح فقط یک خصوصیت نداریم.) (نحوه پیاده سازی قبلا در اینجا مطرح شده)
2) ModelBinder پیش فرض را جهت پاسخگویی به نیازمان توسعه دهیم. (که در این مطلب قصد آموزشش را داریم.)
فرض کنید که میخواهید بر اساس یک Enum در صفحه، یک DropDownFor معادل را قرار بدید که به طور خودکار رشته انتخاب شده را به یک خصوصیت مدل که از نوع بایت هست بایند بکند.
@Html.DropDownListFor(model => model.AccountType, new SelectList(Enum.GetNames(typeof(Enums.AccountType))))
public enum AccountType : byte { مدیر = 0, کاربر_حقیقی = 1, کاربر_حقوقی = 2, }
namespace MvcApplication1.Models { [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] public abstract class PropertyBindAttribute : Attribute { public abstract bool BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor); } public class ExtendedModelBinder : DefaultModelBinder { protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor) { if (propertyDescriptor.Attributes.OfType<PropertyBindAttribute>().Any()) { var modelBindAttr = propertyDescriptor.Attributes.OfType<PropertyBindAttribute>().FirstOrDefault(); if (modelBindAttr.BindProperty(controllerContext, bindingContext, propertyDescriptor)) return; } base.BindProperty(controllerContext, bindingContext, propertyDescriptor); } } }
در کد بالا ما تمام کلاس هایی را که از PropertyBindAttribute مشتق شده باشند را به DefaultModelBinder اضافه میکنیم. این کد فقط یک بار نوشته میشود و از این به بعد هر بایندر سفارشی که بسازیم به بایندر پیشفرض اضافه خواهد شد.
public class AccountTypeBindAttribute : PropertyBindAttribute { public override bool BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor) { if (propertyDescriptor.PropertyType == typeof(byte)) { HttpRequestBase request = controllerContext.HttpContext.Request; byte accountType = (byte)Enum.Parse(typeof(Enums.AccountType), request.Form["AccountType"]); propertyDescriptor.SetValue(bindingContext.Model, accountType); return true; } return false; } }
[AccountTypeBindAttribute] public byte AccountType { get; set; }
ModelBinders.Binders.DefaultBinder = new ExtendedModelBinder();
The field نوع کاربر : must be a number.
@Html.DropDownListFor(model => model.AccountType, new SelectList(Enum.GetNames(typeof(Enums.AccountType))),new Dictionary<string, object>() {{ "data-val", "false" }})
یکی از مواردی که در تمام برنامههای فارسی "باید" رعایت شود (مهم نیست به چه زبانی یا چه سکویی باشد یا چه بانک اطلاعاتی مورد استفاده است)، بحث اصلاح "ی" و "ک" دریافتی از کاربر و یکسان سازی آنها میباشد. به عبارتی برنامهی فارسی که اصلاح خودکار این دو مورد را لحاظ نکرده باشد دیر یا زود به مشکلات حادی برخورد خواهد کرد و "ناقص" است : اطلاعات بیشتر ؛ برای مثال شاید دوست نداشته باشید که دو کامران در سایت شما ثبت نام کرده باشند؛ یکی با ک فارسی و یکی با ک عربی! به علاوه همین کامران امروز میتواند لاگین کند و فردا با یک کامپیوتر دیگر و صفحه کلیدی دیگر پشت درب خواهد ماند. در حالیکه از دید این کامران، کلمه کامران همان کامران است!
بنابراین در دو قسمت "باید" این یکسان سازی صورت گیرد:
الف) پیش از ثبت اطلاعات در بانک اطلاعاتی (تا با دو کامران ثبت شده در بانک اطلاعاتی مواجه نشوید)
ب) پیش از جستجو (تا کامران روزی دیگر با صفحه کلیدی دیگر بتواند به برنامه وارد شود)
راه حل یکسان سازی هم شاید به نظر این باشد: رخداد فشرده شدن کلید را کنترل کنید و سپس جایگزینی را انجام دهید (مثلا ی عربی را با ی فارسی جایگزین کنید). این روش چند ایراد دارد:
الف) Silverlight به دلایل امنیتی اصلا چنین اجازهای را به شما نمیدهد! (تا نتوان کلیدی را جعل کرد)
ب) همیشه با یک TextBox ساده سر و کار نداریم. کنترلهای دیگری هم هستند که امکان ورود اطلاعات در آنها وجود دارد و آن وقت باید برای تمام آنها کد نوشت. ظاهر کدهای برنامه در این حالت در حجم بالا، اصلا جالب نخواهد بود و ضمنا ممکن است یک یا چند مورد فراموش شوند.
راه بهتر این است که دقیقا حین ثبت اطلاعات یا جستجوی اطلاعات در لایهای که تمام ثبتها یا اعمال کار با بانک اطلاعاتی برنامه به آنجا منتقل میشود، کار یکسان سازی صورت گیرد. به این صورت کار یکپارچه سازی یکبار باید انجام شود اما تاثیرش را بر روی کل برنامه خواهد گذاشت، بدون اینکه هرجایی که امکان ورود اطلاعات هست روالهای رخداد گردان هم حضور داشته باشند.
در مورد مقدمات WCF RIA Services که درSilverlight و ASP.NET کاربرد دارد میتوانید به این مطلب مراجعه کنید: +
جهت تکمیل این بحث متدی تهیه شده که کار یکسان سازی ی و ک دریافتی از کاربر را حین ثبت توسط امکانات WCF RIA Services انجام میدهد (دقیقا پیش از فراخوانی متد SubmitChanges باید بکارگرفته شود):
namespace SilverlightTests.RiaYeKe
{
public static class PersianHelper
{
public static string ApplyUnifiedYeKe(this string data)
{
if (string.IsNullOrEmpty(data)) return data;
return data.Replace("ی", "ی").Replace("ک", "ک");
}
}
}
using System.Linq;
using System.Windows.Controls;
using System.Reflection;
using System.ServiceModel.DomainServices.Client;
namespace SilverlightTests.RiaYeKe
{
public class RIAHelper
{
/// <summary>
/// یک دست سازی ی و ک در عبارات ثبت شده در بانک اطلاعاتی پیش از ورود به آن
/// این متد باید پیش از فراخوانی متد
/// SubmitChanges
/// استفاده شود
/// </summary>
/// <param name="dds"></param>
public static void ApplyCorrectYeKe(DomainDataSource dds)
{
if (dds == null)
return;
if (dds.DataView.TotalItemCount <= 0)
return;
//پیدا کردن موجودیتهای تغییر کرده
var changedEntities = dds.DomainContext.EntityContainer.GetChanges().Where(
c => c.EntityState == EntityState.Modified ||
c.EntityState == EntityState.New);
foreach (var entity in changedEntities)
{
//یافتن خواص این موجودیتها
var propertyInfos = entity.GetType().GetProperties(
BindingFlags.Public | BindingFlags.Instance
);
foreach (var propertyInfo in propertyInfos)
{
//اگر این خاصیت رشتهای است ی و ک آن را استاندارد کن
if (propertyInfo.PropertyType != typeof (string)) continue;
var propName = propertyInfo.Name;
var val = new PropertyReflector().GetValue(entity, propName);
if (val == null) continue;
new PropertyReflector().SetValue(
entity,
propName,
val.ToString().ApplyUnifiedYeKe());
}
}
}
}
}
توضیحات:
از آنجائیکه حین فراخوانی متد SubmitChanges فقط موجودیتهای تغییر کرده جهت ثبت ارسال میشوند، ابتدا این موارد یافت شده و سپس خواص عمومی تک تک این اشیاء توسط عملیات Reflection بررسی میگردند. اگر خاصیت مورد بررسی از نوع رشتهای بود، یکبار این یک دست سازی اطلاعات ی و ک دریافتی صورت خواهد گرفت (و از آنجائیکه این تعداد همیشه محدود است عملیات Reflection سربار خاصی نخواهد داشت).
اگر در کدهای خود از DomainDataSource استفاده نمیکنید باز هم تفاوتی نمیکند. متد ApplyCorrectYeKe را از قسمت DomainContext.EntityContainer به بعد دنبال کنید.
اکنون تنها مورد باقیمانده بحث جستجو است که با اعمال متد ApplyUnifiedYeKe به مقدار ورودی متد جستجوی خود، مشکل حل خواهد شد.
کلاس PropertyReflector بکارگرفته شده هم از اینجا به عاریت گرفته شد.
دریافت کدهای این بحث
ساختار درختی
اصطلاحات درخت
در شکل بالا دایرههایی برای هر بخش از اطلاعت کشیده شده و ارتباط هر کدام از آنها از طریق یک خط برقرار شده است. اعداد داخل هر دایره تکراری نیست و همه منحصر به فرد هستند. پس وقتی از اعداد اسم ببریم متوجه میشویم که در مورد چه چیزی صحبت میکنیم.
در شکل بالا به هر یک از دایرهها یک گره Node میگویند و به هر خط ارتباط دهنده بین گرهها لبه Edge گفته میشود. گرههای 19 و 21 و 14 زیر گرههای گره 7 محسوب میشوند. گرههایی که به صورت مستقیم به زیر گرههای خودشان اشاره میکنند را گرههای والد Parent میگویند و زیرگرههای 7 را گرههای فرزند ChildNodes. پس با این حساب میتوانیم بگوییم گرههای 1 و 12 و 31 را هم فرزند گره 19 هستند و گره 19 والد آن هاست. همچنین گرههای یک والد را مثل 19 و 21 و 14 که والد مشترک دارند، گرههای خواهر و برادر یا حتی همنژاد Sibling میگوییم. همچنین ارتباط بین گره 7 و گرههای سطح دوم و الی آخر یعنی 1 و 12 و 31 و 23 و 6 را که والد بودن آن به صورت غیر مستقیم است را جد یا ancestor مینامیم و نوهها و نتیجههای آنها را نسل descendants.
ریشه Root: به گرهای میگوییم که هیچ والدی ندارد و خودش در واقع اولین والد محسوب میشود؛ مثل گره 7.
برگ Leaf: به گرههایی که هیچ فرزندی ندارند، برگ میگوییم. مثال گرههای 1 و12 و 31 و 23 و 6
گرههای داخلی Internal Nodes: گره هایی که نه برگ هستند و نه ریشه. یعنی حداقل یک فرزند دارند و خودشان یک گره فرزند محسوب میشوند؛ مثل گرههای 19 و 14.
مسیر Path: راه رسیدن از یک گره به گره دیگر را مسیر میگویند. مثلا گرههای 1 و 19 و 7 و 21 به ترتیب یک مسیر را تشکیل میدهند ولی گرههای 1 و 19 و 23 از آن جا که هیچ جور اتصالی بین آنها نیست، مسیری را تشکیل نمیدهند.
طول مسیر Length of Path: به تعداد لبههای یک مسیر، طول مسیر میگویند که میتوان از تعداد گرهها -1 نیز آن را به دست آورد. برای نمونه : مسیر 1 و19 و 7 و 21 طول مسیرشان 3 هست.
عمق Depth: طول مسیر یک گره از ریشه تا آن گره را عمق درخت میگویند. عمق یک ریشه همیشه صفر است و برای مثال در درخت بالا، گره 19 در عمق یک است و برای گره 23 عمق آن 2 خواهد بود.
تعریف خود درخت Tree: درخت یک ساختار داده برگشتی recursive است که شامل گرهها و لبهها، برای اتصال گرهها به یکدیگر است.
جملات زیر در مورد درخت صدق میکند:
- هر گره میتواند فرزند نداشته باشد یا به هر تعداد که میخواهد فرزند داشته باشد.
- هر گره یک والد دارد و تنها گرهای که والد ندارد، گره ریشه است (البته اگر درخت خالی باشد هیچ گره ای وجود ندارد).
- همه گرهها از ریشه قابل دسترسی هستند و برای دسترسی به گره مورد نظر باید از ریشه تا آن گره، مسیری را طی کرد.
برای پیاده سازی یک درخت، از دو کلاس یکی جهت ساخت گره که حاوی اطلاعات است <TreeNode<T و دیگری جهت ایجاد درخت اصلی به همراه کلیه متدها و خاصیت هایش <Tree<T کمک میگیریم.
public class TreeNode<T> { // شامل مقدار گره است private T value; // مشخص میکند که آیا گره والد دارد یا خیر private bool hasParent; // در صورت داشتن فرزند ، لیست فرزندان را شامل میشود private List<TreeNode<T>> children; /// <summary>سازنده کلاس </summary> /// <param name="value">مقدار گره</param> public TreeNode(T value) { if (value == null) { throw new ArgumentNullException( "Cannot insert null value!"); } this.value = value; this.children = new List<TreeNode<T>>(); } /// <summary>خاصیتی جهت مقداردهی گره</summary> public T Value { get { return this.value; } set { this.value = value; } } /// <summary>تعداد گرههای فرزند را بر میگرداند</summary> public int ChildrenCount { get { return this.children.Count; } } /// <summary>به گره یک فرزند اضافه میکند</summary> /// <param name="child">آرگومان این متد یک گره است که قرار است به فرزندی گره فعلی در آید</param> public void AddChild(TreeNode<T> child) { if (child == null) { throw new ArgumentNullException( "Cannot insert null value!"); } if (child.hasParent) { throw new ArgumentException( "The node already has a parent!"); } child.hasParent = true; this.children.Add(child); } /// <summary> /// گره ای که اندیس آن داده شده است بازگردانده میشود /// </summary> /// <param name="index">اندیس گره</param> /// <returns>گره بازگشتی</returns> public TreeNode<T> GetChild(int index) { return this.children[index]; } } /// <summary>این کلاس ساختار درخت را به کمک کلاس گرهها که در بالا تعریف کردیم میسازد</summary> /// <typeparam name="T">نوع مقادیری که قرار است داخل درخت ذخیره شوند</typeparam> public class Tree<T> { // گره ریشه private TreeNode<T> root; /// <summary>سازنده کلاس</summary> /// <param name="value">مقدار گره اول که همان ریشه میشود</param> public Tree(T value) { if (value == null) { throw new ArgumentNullException( "Cannot insert null value!"); } this.root = new TreeNode<T>(value); } /// <summary>سازنده دیگر برای کلاس درخت</summary> /// <param name="value">مقدار گره ریشه مثل سازنده اول</param> /// <param name="children">آرایه ای از گرهها که فرزند گره ریشه میشوند</param> public Tree(T value, params Tree<T>[] children) : this(value) { foreach (Tree<T> child in children) { this.root.AddChild(child.root); } } /// <summary> /// ریشه را بر میگرداند ، اگر ریشه ای نباشد نال بر میگرداند /// </summary> public TreeNode<T> Root { get { return this.root; } } /// <summary>پیمودن عرضی و نمایش درخت با الگوریتم دی اف اس </summary> /// <param name="root">ریشه (گره ابتدایی) درختی که قرار است پیمایش از آن شروع شود</param> /// <param name="spaces">یک کاراکتر جهت جداسازی مقادیر هر گره</param> private void PrintDFS(TreeNode<T> root, string spaces) { if (this.root == null) { return; } Console.WriteLine(spaces + root.Value); TreeNode<T> child = null; for (int i = 0; i < root.ChildrenCount; i++) { child = root.GetChild(i); PrintDFS(child, spaces + " "); } } /// <summary>متد پیمایش درخت به صورت عمومی که تابع خصوصی که در بالا توضیح دادیم را صدا میزند</summary> public void TraverseDFS() { this.PrintDFS(this.root, string.Empty); } } /// <summary> /// کد استفاده از ساختار درخت /// </summary> public static class TreeExample { static void Main() { // Create the tree from the sample Tree<int> tree = new Tree<int>(7, new Tree<int>(19, new Tree<int>(1), new Tree<int>(12), new Tree<int>(31)), new Tree<int>(21), new Tree<int>(14, new Tree<int>(23), new Tree<int>(6)) ); // پیمایش درخت با الگوریتم دی اف اس یا عمقی tree.TraverseDFS(); // خروجی // 7 // 19 // 1 // 12 // 31 // 21 // 14 // 23 // 6 } }
پیمایش درخت به روش عمقی (DFS (Depth First Search
هدف از پیمایش درخت ملاقات یا بازبینی (تهیه لیستی از همه گرههای یک درخت) تنها یکبار هر گره در درخت است. برای این کار الگوریتمهای زیادی وجود دارند که ما در این مقاله تنها دو روش DFS و BFS را بررسی میکنیم.
روش DFS: هر گرهای که به تابع بالا بدهید، آن گره برای پیمایش، گره ریشه حساب خواهد شد و پیمایش از آن آغاز میگردد. در الگوریتم DFS روش پیمایش بدین گونه است که ما از گره ریشه آغاز کرده و گره ریشه را ملاقات میکنیم. سپس گرههای فرزندش را به دست میآوریم و یکی از گرهها را انتخاب کرده و دوباره همین مورد را رویش انجام میدهیم تا نهایتا به یک برگ برسیم. وقتی که به برگی میرسیم یک مرحله به بالا برگشته و این کار را آنقدر تکرار میکنیم تا همهی گرههای آن ریشه یا درخت پیمایش شده باشند.
همین درخت را در نظر بگیرید:
پیمایش درخت را از گره 7 آغاز میکنیم و آن را به عنوان ریشه در نظر میگیریم. حتی میتوانیم پیمایش را از گره مثلا 19 آغاز کنیم و آن را برای پیمایش ریشه در نظر بگیریم ولی ما از همان 7 پیمایش را آغاز میکنیم:
ابتدا گره 7 ملاقات شده و آن را مینویسیم. سپس فرزندانش را بررسی میکنیم که سه فرزند دارد. یکی از فرزندان مثل گره 19 را انتخاب کرده و آن را ملاقات میکنیم (با هر بار ملاقات آن را چاپ میکنیم) سپس فرزندان آن را بررسی میکنیم و یکی از گرهها را انتخاب میکنیم و ملاقاتش میکنیم؛ برای مثال گره 1. از آن جا که گره یک، برگ است و فرزندی ندارد یک مرحله به سمت بالا برمیگردیم و برگهای 12 و 31 را هم ملاقات میکنیم. حالا همهی فرزندان گره 19 را بررسی کردیم، بر میگردیم یک مرحله به سمت بالا و گره 21 را ملاقات میکنیم و از آنجا که گره 21 برگ است و فرزندی ندارد به بالا باز میگردیم و بعد گره 14 و فرزندانش 23 و 6 هم بررسی میشوند. پس ترتیب چاپ ما اینگونه میشود:
7-19-1-12-31-21-14-23-6
پیمایش درخت به روش (BFS (Breadth First Search
در این روش (پیمایش سطحی) گره والد ملاقات شده و سپس همه گرههای فرزندش ملاقات میشوند. بعد از آن یک گره انتخاب شده و همین پیمایش مجددا روی آن انجام میشود تا آن سطح کاملا پیمایش شده باشد. سپس به همین مرحله برگشته و فرزند بعدی را پیمایش میکنیم و الی آخر. نمونهی این پیمایش روی درخت بالا به صورت زیر نمایش داده میشود:
7-19-21-14-1-12-31-23-6
اگر خوب دقت کنید میبینید که پیمایش سطحی است و هر سطح به ترتیب ملاقات میشود. به این الگوریتم، پیمایش موجی هم میگویند. دلیل آن هم این است که مثل سنگی میماند که شما برای ایجاد موج روی دریاچه پرتاب میکنید.
برای این پیمایش از صف کمک گرفته میشود که مراحل زیر روی صف صورت میگیرد:
- ریشه وارد صف Q میشود.
- دو مرحله زیر مرتبا تکرار میشوند:
- اولین گره صف به نام V را از Q در یافت میکنیم و آن را چاپ میکنیم.
- فرزندان گره V را به صف اضافه میکنیم.
namespace BlazorServer.Services { public interface IHotelRoomService { Task<HotelRoomDTO> CreateHotelRoomAsync(HotelRoomDTO hotelRoomDTO); Task<int> DeleteHotelRoomAsync(int roomId); IAsyncEnumerable<HotelRoomDTO> GetAllHotelRoomsAsync(); Task<HotelRoomDTO> GetHotelRoomAsync(int roomId); Task<HotelRoomDTO> IsRoomUniqueAsync(string name); Task<HotelRoomDTO> UpdateHotelRoomAsync(int roomId, HotelRoomDTO hotelRoomDTO); } }
تعریف کامپوننتهای ابتدایی نمایش لیست اتاقها و ثبت و ویرایش آنها
در ابتدا کامپوننتهای خالی نمایش لیست اتاقها و همچنین فرم خالی ثبت و ویرایش آنها را به همراه مسیریابیهای مرتبط، ایجاد میکنیم. به همین جهت ابتدا داخل پوشهی Pages، پوشهی جدید HotelRoom را ایجاد کرده و فایل جدید HotelRoomList.razor را با محتوای ابتدایی زیر، به آن اضافه میکنیم.
@page "/hotel-room" <div class="row mt-4"> <div class="col-8"> <h4 class="card-title text-info">Hotel Rooms</h4> </div> <div class="col-3 offset-1"> <NavLink href="hotel-room/create" class="btn btn-info">Add New Room</NavLink> </div> </div> @code { }
<li class="nav-item px-3"> <NavLink class="nav-link" href="hotel-room"> <span class="oi oi-list-rich" aria-hidden="true"></span> Hotel Rooms </NavLink> </li>
تا اینجا صفحهی ابتدایی نمایش لیست اتاقها، به همراه یک دکمهی افزودن اتاق جدید نیز هست. به همین جهت فایل جدید Pages\HotelRoom\HotelRoomUpsert.razor را به همراه مسیریابی hotel-room/create/ برای تعریف کامپوننت ابتدایی ثبت و ویرایش اطلاعات اتاقها، اضافه میکنیم:
@page "/hotel-room/create" <h3>HotelRoomUpsert</h3> @code { }
- NavLink تعریف شدهی در کامپوننت نمایش لیست اتاقها، به مسیریابی کامپوننت فوق اشاره میکند.
ایجاد فرم ثبت یک اتاق جدید
برای ثبت یک اتاق جدید نیاز است به مدل UI آن که همان HotelRoomDTO تعریف شدهی در قسمت قبل است، دسترسی داشت. به همین جهت در پروژهی BlazorServer.App، ارجاعی را به پروژهی BlazorServer.Models.csproj اضافه میکنیم:
<Project Sdk="Microsoft.NET.Sdk.Web"> <ItemGroup> <ProjectReference Include="..\BlazorServer.Models\BlazorServer.Models.csproj" /> </ItemGroup> </Project>
@using BlazorServer.Models
@page "/hotel-room/create" <div class="row mt-2 mb-5"> <h3 class="card-title text-info mb-3 ml-3">@Title Hotel Room</h3> <div class="col-md-12"> <div class="card"> <div class="card-body"> <EditForm Model="HotelRoomModel"> <div class="form-group"> <label>Name</label> <InputText @bind-Value="HotelRoomModel.Name" class="form-control"></InputText> </div> </EditForm> </div> </div> </div> </div> @code { private HotelRoomDTO HotelRoomModel = new HotelRoomDTO(); private string Title = "Create"; }
- در برنامههای Blazor، کامپوننت ویژهی EditForm را بجای تگ استاندارد form، مورد استفاده قرار میدهیم.
- این کامپوننت، مدل فرم را از فیلد HotelRoomModel که در قسمت کدها تعریف کردیم، دریافت میکند. کار آن تامین اطلاعات فیلدهای فرم است.
- سپس در EditForm تعریف شده، بجای المان استاندارد input، از کامپوننت InputText برای دریافت اطلاعات متنی استفاده میشود. با bind-value@ در قسمت چهارم این سری بیشتر آشنا شدیم و کار آن two-way data binding است. در اینجا هر اطلاعاتی که وارد میشود، سبب به روز رسانی خودکار مقدار خاصیت HotelRoomModel.Name میشود و برعکس.
یک نکته: در قسمت قبل، مدل UI را از نوع رکورد C# 9.0 و init only تعریف کردیم. رکوردها، با EditForm و two-way databinding آن سازگاری ندارند (bind-value@ در اینجا) و بیشتر برای کنترلرهای برنامههای Web API که یکبار قرار است کار وهله سازی آنها در زمان دریافت اطلاعات از کاربر صورت گیرد، مناسب هستند و نه با فرمهای پویای Blazor. به همین جهت به پروژهی BlazorServer.Models مراجعه کرده و نوع آنها را به کلاس و initها را به set معمولی تغییر میدهیم تا در فرمهای Blazor هم قابل استفاده شوند.
تا اینجا کامپوننت ثبت اطلاعات یک اتاق جدید، چنین شکلی را پیدا کردهاست:
تکمیل سایر فیلدهای فرم ورود اطلاعات اتاق
پس از تعریف فیلد ورود اطلاعات نام اتاق، سایر فیلدهای متناظر با HotelRoomDTO را نیز به صورت زیر به EditForm تعریف شده اضافه میکنیم که در اینجا از InputNumber برای دریافت اطلاعات عددی و از InputTextArea، برای دریافت اطلاعات متنی چندسطری استفاده شدهاست:
<EditForm Model="HotelRoomModel"> <div class="form-group"> <label>Name</label> <InputText @bind-Value="HotelRoomModel.Name" class="form-control"></InputText> </div> <div class="form-group"> <label>Occupancy</label> <InputNumber @bind-Value="HotelRoomModel.Occupancy" class="form-control"></InputNumber> </div> <div class="form-group"> <label>Rate</label> <InputNumber @bind-Value="HotelRoomModel.RegularRate" class="form-control"></InputNumber> </div> <div class="form-group"> <label>Sq ft.</label> <InputText @bind-Value="HotelRoomModel.SqFt" class="form-control"></InputText> </div> <div class="form-group"> <label>Details</label> <InputTextArea @bind-Value="HotelRoomModel.Details" class="form-control"></InputTextArea> </div> <div class="form-group"> <button class="btn btn-primary">@Title Room</button> <NavLink href="hotel-room" class="btn btn-secondary">Back to Index</NavLink> </div> </EditForm>
تعریف اعتبارسنجیهای فیلدهای یک فرم Blazor
در حین تعریف یک فرم، برای واکنش نشان دادن به دکمهی submit، میتوان رویداد OnSubmit را به کامپوننت EditForm اضافه کرد که سبب فراخوانی متدی در قسمت کدهای کامپوننت جاری خواهد شد؛ مانند فراخوانی متد HandleHotelRoomUpsert در مثال زیر:
<EditForm Model="HotelRoomModel" OnSubmit="HandleHotelRoomUpsert"> </EditForm> @code { private HotelRoomDTO HotelRoomModel = new HotelRoomDTO(); private async Task HandleHotelRoomUpsert() { } }
اگر این مورد، مدنظر نیست، میتوان بجای OnSubmit، از رویداد OnValidSubmit استفاده کرد. در این حالت اگر اعتبارسنجی مدل فرم با شکست مواجه شود، دیگر متد HandleHotelRoomUpsert فراخوانی نخواهد شد. همچنین در این حالت میتوان خطاهای اعتبارسنجی را نیز در فرم نمایش داد:
<EditForm Model="HotelRoomModel" OnValidSubmit="HandleHotelRoomUpsert"> <DataAnnotationsValidator /> @*<ValidationSummary />*@ <div class="form-group"> <label>Name</label> <InputText @bind-Value="HotelRoomModel.Name" class="form-control"></InputText> <ValidationMessage For="()=>HotelRoomModel.Name"></ValidationMessage> </div> <div class="form-group"> <label>Occupancy</label> <InputNumber @bind-Value="HotelRoomModel.Occupancy" class="form-control"></InputNumber> <ValidationMessage For="()=>HotelRoomModel.Occupancy"></ValidationMessage> </div> <div class="form-group"> <label>Rate</label> <InputNumber @bind-Value="HotelRoomModel.RegularRate" class="form-control"></InputNumber> <ValidationMessage For="()=>HotelRoomModel.RegularRate"></ValidationMessage> </div>
- کامپوننت DataAnnotationsValidator، اعتبارسنجی مبتنی بر data annotations را مانند [Required]، در دامنهی دید یک EditForm فعال میکند.
- اگر خواستیم تمام خطاهای اعتبارسنجی را به صورت خلاصهای در بالای فرم نمایش دهیم، میتوان از کامپوننت ValidationSummary استفاده کرد.
- و یا اگر خواستیم خطاها را به صورت اختصاصیتری ذیل هر تکستباکس نمایش دهیم، میتوان از کامپوننت ValidationMessage کمک گرفت. خاصیت For آن از نوع <Expression<System.Func تعریف شدهاست که اجازهی تعریف strongly typed نام خاصیت در حال اعتبارسنجی را به صورتی که مشاهده میکنید، میسر میکند.
ثبت اولین اتاق هتل
در ادامه میخواهیم روال رویدادگردان HandleHotelRoomUpsert را مدیریت کنیم. به همین جهت نیاز به کار با سرویس IHotelRoomService ابتدای بحث خواهد بود. بنابراین در ابتدا به فایل BlazorServer.App\_Imports.razor مراجعه کرده و فضای نام سرویسهای برنامه را اضافه میکنیم:
@using BlazorServer.Services
@page "/hotel-room/create" @inject IHotelRoomService HotelRoomService @inject NavigationManager NavigationManager @code { private HotelRoomDTO HotelRoomModel = new HotelRoomDTO(); private string Title = "Create"; private async Task HandleHotelRoomUpsert() { var roomDetailsByName = await HotelRoomService.IsRoomUniqueAsync(HotelRoomModel.Name); if (roomDetailsByName != null) { //there is a duplicate room. show an error msg. return; } var createdResult = await HotelRoomService.CreateHotelRoomAsync(HotelRoomModel); NavigationManager.NavigateTo("hotel-room"); } }
اگر پیشتر با ASP.NET Web Forms کار کرده باشید (اولین روش توسعهی برنامههای وب در دنیای دات نت)، مدل برنامه نویسی Blazor Server، بسیار شبیه به کار با وب فرمها است؛ البته بر اساس آخرین تغییرات دنیای دانت نت مانند برنامه نویسی async، کار با سرویسها، تزریق وابستگیهای توکار و غیره.
نمایش لیست اتاقهای ثبت شده
تا اینجا موفق شدیم اطلاعات یک مدل اعتبارسنجی شده را در بانک اطلاعاتی ثبت کنیم. مرحلهی بعد، نمایش لیست اطلاعات ثبت شدهی در بانک اطلاعاتی است. بنابراین به کامپوننت HotelRoomList.razor مراجعه کرده و آنرا به صورت زیر تکمیل میکنیم:
@page "/hotel-room" @inject IHotelRoomService HotelRoomService <div class="row mt-4"> <div class="col-8"> <h4 class="card-title text-info">Hotel Rooms</h4> </div> <div class="col-3 offset-1"> <NavLink href="hotel-room/create" class="btn btn-info">Add New Room</NavLink> </div> </div> <div class="row mt-4"> <div class="col-12"> <table class="table table-bordered table-hover"> <thead> <tr> <th>Name</th> <th>Occupancy</th> <th>Rate</th> <th> Sqft </th> <th> </th> </tr> </thead> <tbody> @if (HotelRooms.Any()) { foreach (var room in HotelRooms) { <tr> <td>@room.Name</td> <td>@room.Occupancy</td> <td>@room.RegularRate.ToString("c")</td> <td>@room.SqFt</td> <td></td> </tr> } } else { <tr> <td colspan="5">No records found</td> </tr> } </tbody> </table> </div> </div> @code { private List<HotelRoomDTO> HotelRooms = new List<HotelRoomDTO>(); protected override async Task OnInitializedAsync() { await foreach(var room in HotelRoomService.GetAllHotelRoomsAsync()) { HotelRooms.Add(room); } } }
- متد GetAllHotelRoomsAsync، لیست اتاقهای ثبت شده را بازگشت میدهد. البته خروجی آن از نوع <IAsyncEnumerable<HotelRoomDTO است که از زمان C# 8.0 ارائه شد و روش کار با آن اندکی متفاوت است. IAsyncEnumerableها را باید توسط await foreach پردازش کرد.
- همانطور که در مطلب بررسی چرخهی حیات کامپوننتها نیز عنوان شد، متدهای رویدادگران OnInitialized و نمونهی async آن برای دریافت اطلاعات از سرویسها طراحی شدهاند که در اینجا نمونهای از آنرا مشاهده میکنید.
- پس از تشکیل لیست اتاقها، حلقهی foreach (var room in HotelRooms) تعریف شده، ردیفهای آنرا در UI نمایش میدهد.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-14.zip
در ادامه بجای این روشها، از SignalR برای محاسبه تعداد کاربران آنلاین و همچنین به روز رسانی بلادرنگ این عدد در سمت کاربر، استفاده خواهیم کرد.
تشخیص اتصال و قطع اتصال کاربران در SignalR
زیر ساختهای کلاس Hub موجود در SignalR، دارای متدهای ردیابی اتصال (OnConnected)، قطع اتصال (OnDisconnected) و یا برقراری مجدد اتصال کاربران (OnReconnected) هستند. با بازنویسی این متدها میتوان به تخمین بسیار دقیقی از تعداد کاربران آنلاین یک سایت رسید.
پیشنیازهای بحث
پیشنیازهای این بحث با مطلب «مثال - نمایش درصد پیشرفت عملیات توسط SignalR» یکی است. برای مثال نحوه دریافت وابستگیها، تنظیمات فایل global.asax و افزودن اسکریپتها، تفاوتی با مثال یاد شده ندارند.
تعریف هاب کاربران آنلاین برنامه
using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNet.SignalR; namespace SignalR05.Common { public class OnlineUsersHub : Hub { public static readonly ConcurrentDictionary<string, string> OnlineUsers = new ConcurrentDictionary<string, string>(); public void UpdateUsersOnlineCount() { // آی پی معرف یک کاربر است // اما کانکشن آی دی معرف یک برگه جدید در مرورگر او است // هر کاربر میتواند چندین برگه را به یک سایت گشوده یا ببندد var ipsCount = OnlineUsers.Select(x => x.Value).Distinct().Count(); this.Clients.All.updateUsersOnlineCount(ipsCount); } /// <summary> /// اگر کاربران اعتبار سنجی شدهاند بهتر است از /// this.Context.User.Identity.Name /// بجای آی پی استفاده شود /// </summary> protected string GetUserIpAddress() { object environment; if (!Context.Request.Items.TryGetValue("owin.environment", out environment)) return null; object serverRemoteIpAddress; if (!((IDictionary<string, object>)environment).TryGetValue("server.RemoteIpAddress", out serverRemoteIpAddress)) return null; return serverRemoteIpAddress.ToString(); } public override Task OnConnected() { var ip = GetUserIpAddress(); OnlineUsers.TryAdd(this.Context.ConnectionId, ip); UpdateUsersOnlineCount(); return base.OnConnected(); } public override Task OnReconnected() { var ip = GetUserIpAddress(); OnlineUsers.TryAdd(this.Context.ConnectionId, ip); UpdateUsersOnlineCount(); return base.OnReconnected(); } public override Task OnDisconnected() { // در این حالت ممکن است مرورگر کاملا بسته شده باشد // یا حتی صرفا یک برگه مرورگر از چندین برگه متصل به سایت بسته شده باشند string ip; OnlineUsers.TryRemove(this.Context.ConnectionId, out ip); UpdateUsersOnlineCount(); return base.OnDisconnected(); } } }
در اینجا، هم به IP کاربر و هم به ConnectionId او نیاز است. از این جهت که هر ConnectionId، معرف یک برگه جدید باز شده در مرورگر کاربر است. اگر صرفا IPها را پردازش کنیم، با بسته شدن یکی از چندین برگه مرورگر او که اکنون به سایت متصل هستند، آمار او را از دست خواهیم داد. این کاربر هنوز چندین برگه باز دیگر را دارد که با سایت در ارتباط هستند، اما چون IP او را از لیست حذف کردهایم (در نتیجه بسته شدن یکی از برگهها)، آمار کلی شخص را نیز از دست خواهیم داد. بنابراین هر دوی IP و ConnectionIdها باید پردازش شوند.
اگر برنامه شما دارای اعتبارسنجی است (یک صفحه لاگین دارد)، بهتر است بجای IP از this.Context.User.Identity.Name استفاده کنید.
کدهای سمت کلاینت نمایش آمار کاربران
<html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title></title> <script src="Scripts/jquery-1.6.4.min.js" type="text/javascript"></script> <script src="Scripts/jquery.signalR-1.1.3.min.js" type="text/javascript"></script> <script type="text/javascript" src='<%= ResolveClientUrl("~/signalr/hubs") %>'></script> </head> <body> <form id="form1" runat="server"> online users count: <span id="usersCount"></span> </form> <script type="text/javascript"> $(function () { $.connection.hub.logging = true; var onlineUsersHub = $.connection.onlineUsersHub; onlineUsersHub.client.updateUsersOnlineCount = function (count) { $('#usersCount').text(count); }; $.connection.hub.start(); }); </script> </body> </html>
کدهای کامل این مثال را از اینجا نیز میتوانید دریافت کنید:
SignalR05.zip
WebStorage: قسمت دوم
window.localStorage window.sessionStorage
if(typeof(Storage) !== "undefined") { // Code for localStorage/sessionStorage. } else { // Sorry! No Web Storage support.. }
localStorage.setItem("lastname", "Smith"); //======================== localStorage.getItem("lastname");
var a=localStorage.lastname;
//ذخیره مقدار store.set('username', 'marcus') //بازیابی مقدار store.get('username') //حذف مقدار store.remove('username') //حذف تمامی مقادیر ذخیره شده store.clear() //ذخیره ساختار store.set('user', { name: 'marcus', likes: 'javascript' }) //بازیابی ساختار به شکل قبلی var user = store.get('user') alert(user.name + ' likes ' + user.likes) //تغییر مستقیم مقدار قبلی store.getAll().user.name == 'marcus' //بازخوانی تمام مقادیر ذخیر شده توسط یک حلقه store.forEach(function(key, val) { console.log(key, '==', val) })
<script src="store.min.js"></script> <script> init() function init() { if (!store.enabled) { alert('Local storage is not supported by your browser. Please disable "Private Mode", or upgrade to a modern browser.') return } var user = store.get('user') // ... and so on ... } </script>
var storeWithExpiration = { // دریافت کلید و مقدار و زمان انقضا به میلی ثانیه set: function(key, val, exp) { //ایجاد زمان فعلی جهت ثبت تاریخ ایجاد store.set(key, { val:val, exp:exp, time:new Date().getTime() }) }, get: function(key) { var info = store.get(key) //در صورتی که کلید داده شده مقداری نداشته باشد نال را بر میگردانیم if (!info) { return null } //تاریخ فعلی را منهای تاریخ ثبت شده کرده و در صورتی که //از مقدار میلی ثاینه بیشتر باشد یعنی منقضی شده و نال بر میگرداند if (new Date().getTime() - info.time > info.exp) { return null } return info.val } } // استفاده عملی از کد بالا // استفاده از تایمر جهت نمایش واکشی دادهها قبل از نقضا و بعد از انقضا storeWithExpiration.set('foo', 'bar', 1000) setTimeout(function() { console.log(storeWithExpiration.get('foo')) }, 500) // -> "bar" setTimeout(function() { console.log(storeWithExpiration.get('foo')) }, 1500) // -> null
CrossStorageHub.init([ {origin: /\.example.com$/, allow: ['get']}, {origin: /:\/\/(www\.)?example.com$/, allow: ['get', 'set', 'del']} ]);
valid.example.com
invalid.example.com.malicious.com
{ 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET,PUT,POST,DELETE', 'Access-Control-Allow-Headers': 'X-Requested-With', 'Content-Security-Policy': "default-src 'unsafe-inline' *", 'X-Content-Security-Policy': "default-src 'unsafe-inline' *", 'X-WebKit-CSP': "default-src 'unsafe-inline' *", }
<script type="text/javascript" src="~/Scripts/cross-storage/hub.js"></script> <script> CrossStorageHub.init([ {origin: /.*localhost:300\d$/, allow: ['get', 'set', 'del']} ]); </script>
var storage = new CrossStorageClient('http://localhost:3000/example/hub.html'); var setKeys = function () { return storage.set('key1', 'foo').then(function() { return storage.set('key2', 'bar'); }); };
storage.onConnect().then(function() { return storage.set('key', {foo: 'bar'}); }).then(function() { return storage.set('expiringKey', 'foobar', 10000); });
storage.onConnect().then(function() { return storage.get('key1'); }).then(function(res) { return storage.get('key1', 'key2', 'key3'); }).then(function(res) { // ... });
storage.onConnect() .then(function() { return storage.get('key1', 'key2'); }) .then(function(res) { console.log(res); // ['foo', 'bar'] })['catch'](function(err) { console.log(err); });
<script src="https://s3.amazonaws.com/es6-promises/promise-1.0.0.min.js"></script>
var request = indexedDB.open("library"); request.onupgradeneeded = function() { // The database did not previously exist, so create object stores and indexes. var db = request.result; var store = db.createObjectStore("books", {keyPath: "isbn"}); var titleIndex = store.createIndex("by_title", "title", {unique: true}); var authorIndex = store.createIndex("by_author", "author"); // Populate with initial data. store.put({title: "Quarry Memories", author: "Fred", isbn: 123456}); store.put({title: "Water Buffaloes", author: "Fred", isbn: 234567}); store.put({title: "Bedrock Nights", author: "Barney", isbn: 345678}); }; request.onsuccess = function() { db = request.result; };
var tx = db.transaction("books", "readwrite"); var store = tx.objectStore("books"); store.put({title: "Quarry Memories", author: "Fred", isbn: 123456}); store.put({title: "Water Buffaloes", author: "Fred", isbn: 234567}); store.put({title: "Bedrock Nights", author: "Barney", isbn: 345678}); tx.oncomplete = function() { // All requests have succeeded and the transaction has committed. };
var tx = db.transaction("books", "readonly"); var store = tx.objectStore("books"); var index = store.index("by_author"); var request = index.openCursor(IDBKeyRange.only("Fred")); request.onsuccess = function() { var cursor = request.result; if (cursor) { // Called for each matching record. report(cursor.value.isbn, cursor.value.title, cursor.value.author); cursor.continue(); } else { // No more matching records. report(null); } };
*یکی از پیش نیازهای این پست مطالعه این دو مطلب (^ ) و (^ ) میباشد.
فرض میکنیم در دیتابیس مورد نظر یک Store به همراه یک جدول به صورت زیر داریم:
[Entity] public interface IBook { [Identifier] string Id { get; } string Title { get; set; } string Isbn { get; set; } }
بعد از انخاب گزینه بالا یک فایل با پسوند tt به پروژه اضافه خواهد شد که وظیفه آن جستجو در اسمبلی مورد نظر و پیدا کردن تمام اینترفیس هایی که دارای EntityAttribute هستند و همچنین ایجاد کلاسهای متناظر جهت پیاده سازی اینترفیسهای بالا است. در نتیجه ساختار پروژه تا این جا به صورت زیر خواهد شد.
واضح است که فایلی به نام Book به عنوان پیاده سازی مدل IBook به عنوان زیر مجموعه فایل DatabaseContext.tt به پروژه اضافه شده است.
تا اینجا برای استفاده از Context مورد نظر باید به صورت زیر عمل نمود:
DatabaseContext context = new DatabaseContext(); context.Books.Add(new Book());
public interface IUnitOfWork { BrightstarEntitySet<T> Set<T>() where TEntity : class; void DeleteObject(object obj); void SaveChanges(); }
نکته: برای حذف یک آبجکت از Store، باید از متد DeleteObject تعبیه شده در Context استفاده نماییم. در نتیجه متد مورد نظر نیز در اینترفیس بالا در نظر گرفته شده است.
استفاده از IOC Container جهت رجیستر کردن IUnitOfWrok
در این قدم باید IUnitOfWork را در یک IOC container رجیستر کرده تا در جای مناسب عملیات وهله سازی از آن میسر باشد. من در اینجا از Castle Windsor Container استفاده کردم. کلاس زیر این کار را برای ما انجام خواهد داد:
public class DependencyResolver { public static void Resolve(IWindsorContainer container) { var context = new DatabaseContext("type=embedded;storesdirectory=c:\brightstar;storename=test "); container.Register(Component.For<IUnitOfWork>().Instance(context).LifestyleTransient()); } }
public class BookService { public BookService(IUnitOfWork unitOfWork) { UnitOfWork = unitOfWork; } public IUnitOfWork UnitOfWork { get; private set; } public IList<IBook> GetAll() { return UnitOfWork.Set<IBook>().ToList(); } public void Add() { UnitOfWork.Set<IBook>().Add(new Book()); } public void Remove(IBook entity) { UnitOfWork.DeleteObject(entity); } }
نکته: در حال حاضر امکان جداسازی مدلهای برنامه (تعاریف اینترفیس) در قالب یک پروژه دیگر(نظیر مدل CodeFirst در EF) در B*Db امکان پذیر نیست.
نکته : برای اضافه کردن آیتم جدید به Store نیاز به وهله سازی از اینترفیس IBook داریم. کلاس Book ساخته شده توسط DatabaseContext.tt در عملیات Insert و update کاربرد خواهد داشت.