ثبت برنامهی خود در گوگل و انجام تنظیمات آن
اولین کاری که برای استفاده از نگارش سوم Google Analytics API باید صورت گیرد، ثبت برنامهی خود در Google Developer Console است. برای این منظور ابتدا به آدرس ذیل وارد شوید:
سپس بر روی دکمهی Create project کلیک کنید. نام دلخواهی را وارد کرده و در ادامه بر روی دکمهی Create کلیک نمائید تا پروفایل این پروژه ایجاد شود.
تنها نکتهی مهم این قسمت، بخاطر سپردن نام پروژه است. زیرا از آن جهت اتصال به API گوگل استفاده خواهد شد.
پس از ایجاد پروژه، به صفحهی آن وارد شوید و از منوی سمت چپ صفحه، گزینهی Credentials را انتخاب کنید. در ادامه در صفحهی باز شده، بر روی دکمهی Create new client id کلیک نمائید.
در صفحهی باز شده، گزینهی Service account را انتخاب کنید. اگر سایر گزینهها را انتخاب نمائید، کاربری که قرار است از API استفاده کند، باید بتواند توسط مرورگر نصب شدهی بر روی کامپیوتر اجرا کنندهی برنامه، یکبار به گوگل لاگین نماید که این مورد مطلوب برنامههای وب و همچنین سرویسها نیست.
در اینجا ابتدا یک فایل مجوز p12 را به صورت خودکار دریافت خواهید کرد و همچنین پس از ایجاد client id، نیاز است، ایمیل آنرا جایی یادداشت نمائید:
از این ایمیل و همچنین فایل p12 ارائه شده، جهت لاگین به سرور استفاده خواهد شد.
همچنین نیاز است تا به برگهی APIs پروژهی ایجاد شده رجوع کرد و گزینهی Analytics API آنرا فعال نمود:
تا اینجا کار ثبت و فعال سازی برنامهی خود در گوگل به پایان میرسد.
دادن دسترسی به Client ID ثبت شده در برنامهی Google Analytics
پس از اینکه Client ID سرویس خود را ثبت کردید، نیاز است به اکانت Google Analytics خود وارد شوید. سپس در منوی آن، گزینهی Admin را پیدا کرده و به آن قسمت، وارد شوید:
در ادامه به گزینهی User management آن وارد شده و به ایمیل Client ID ایجاد شده در قسمت قبل، دسترسی خواندن و آنالیز را اعطاء کنید:
در صورت عدم رعایت این مساله، کلاینت API، قادر به دسترسی به Google Analytics نخواهد بود.
استفاده از نگارش سوم Google Analytics API در دات نت
قسمت مهم کار، تنظیمات فوق است که در صورت عدم رعایت آنها، شاید نصف روزی را مشغول به دیباگ برنامه شوید. در ادامه نیاز است پیشنیازهای دسترسی به نگارش سوم Google Analytics API را نصب کنیم. برای این منظور، سه بستهی نیوگت ذیل را توسط کنسول پاورشل نیوگت، به برنامه اضافه کنید:
PM> Install-Package Google.Apis PM> Install-Package Google.Apis.auth PM> Install-Package Google.Apis.Analytics.v3
using System; using System.Linq; using System.Security.Cryptography.X509Certificates; using Google.Apis.Analytics.v3; using Google.Apis.Analytics.v3.Data; using Google.Apis.Auth.OAuth2; using Google.Apis.Services; namespace GoogleAnalyticsAPIv3Tests { public class AnalyticsQueryParameters { public DateTime Start { set; get; } public DateTime End { set; get; } public string Dimensions { set; get; } public string Filters { set; get; } public string Metrics { set; get; } } public class AnalyticsAuthentication { public Uri SiteUrl { set; get; } public string ApplicationName { set; get; } public string ServiceAccountEmail { set; get; } public string KeyFilePath { set; get; } public string KeyFilePassword { set; get; } public AnalyticsAuthentication() { KeyFilePassword = "notasecret"; } } public class GoogleAnalyticsApiV3 { public AnalyticsAuthentication Authentication { set; get; } public AnalyticsQueryParameters QueryParameters { set; get; } public GaData GetData() { var service = createAnalyticsService(); var profile = getProfile(service); var query = service.Data.Ga.Get("ga:" + profile.Id, QueryParameters.Start.ToString("yyyy-MM-dd"), QueryParameters.End.ToString("yyyy-MM-dd"), QueryParameters.Metrics); query.Dimensions = QueryParameters.Dimensions; query.Filters = QueryParameters.Filters; query.SamplingLevel = DataResource.GaResource.GetRequest.SamplingLevelEnum.HIGHERPRECISION; return query.Execute(); } private AnalyticsService createAnalyticsService() { var certificate = new X509Certificate2(Authentication.KeyFilePath, Authentication.KeyFilePassword, X509KeyStorageFlags.Exportable); var credential = new ServiceAccountCredential( new ServiceAccountCredential.Initializer(Authentication.ServiceAccountEmail) { Scopes = new[] { AnalyticsService.Scope.AnalyticsReadonly } }.FromCertificate(certificate)); return new AnalyticsService(new BaseClientService.Initializer { HttpClientInitializer = credential, ApplicationName = Authentication.ApplicationName }); } private Profile getProfile(AnalyticsService service) { var accountListRequest = service.Management.Accounts.List(); var accountList = accountListRequest.Execute(); var site = Authentication.SiteUrl.Host.ToLowerInvariant(); var account = accountList.Items.FirstOrDefault(x => x.Name.ToLowerInvariant().Contains(site)); var webPropertyListRequest = service.Management.Webproperties.List(account.Id); var webPropertyList = webPropertyListRequest.Execute(); var sitePropertyList = webPropertyList.Items.FirstOrDefault(a => a.Name.ToLowerInvariant().Contains(site)); var profileListRequest = service.Management.Profiles.List(account.Id, sitePropertyList.Id); var profileList = profileListRequest.Execute(); return profileList.Items.FirstOrDefault(a => a.Name.ToLowerInvariant().Contains(site)); } } }
مثالی از نحوه استفاده از کلاس GoogleAnalyticsApiV3
در ادامه یک برنامهی کنسول را ملاحظه میکنید که از کلاس GoogleAnalyticsApiV3 استفاده میکند:
using System; using System.Collections.Generic; using System.Linq; namespace GoogleAnalyticsAPIv3Tests { class Program { static void Main(string[] args) { var statistics = new GoogleAnalyticsApiV3 { Authentication = new AnalyticsAuthentication { ApplicationName = "My Project", KeyFilePath = "811e1d9976cd516b55-privatekey.p12", ServiceAccountEmail = "10152bng4j3mq@developer.gserviceaccount.com", SiteUrl = new Uri("https://www.dntips.ir/") }, QueryParameters = new AnalyticsQueryParameters { Start = DateTime.Now.AddDays(-7), End = DateTime.Now, Dimensions = "ga:date", Filters = null, Metrics = "ga:users,ga:sessions,ga:pageviews" } }.GetData(); foreach (var result in statistics.TotalsForAllResults) { Console.WriteLine(result.Key + " -> total:" + result.Value); } Console.WriteLine(); foreach (var row in statistics.ColumnHeaders) { Console.Write(row.Name + "\t"); } Console.WriteLine(); foreach (var row in statistics.Rows) { var rowItems = (List<string>)row; Console.WriteLine(rowItems.Aggregate((s1, s2) => s1 + "\t" + s2)); } Console.ReadLine(); } } }
چند نکته
ApplicationName همان نام پروژهای است که ابتدای کار، در گوگل ایجاد کردیم.
KeyFilePath مسیر فایل مجوز p12 ایی است که گوگل در حین ایجاد اکانت سرویس، در اختیار ما قرار میدهد.
ServiceAccountEmail آدرس ایمیل اکانت سرویس است که در قسمت ادمین Google Analytics به آن دسترسی دادیم.
SiteUrl آدرس سایت شما است که هم اکنون در Google Analytics دارای یک اکانت و پروفایل ثبت شدهاست.
توسط AnalyticsQueryParameters میتوان نحوهی کوئری گرفتن از Google Analytics را مشخص کرد. تاریخ شروع و پایان گزارش گیری در آن مشخص هستند. در مورد پارامترهایی مانند Dimensions و Metrics بهتر است به مرجع کامل آن در گوگل مراجعه نمائید:
Dimensions & Metrics Reference
برای نمونه در مثال فوق، تعداد کاربران، سشنهای آن و همچنین تعداد بار مشاهدهی صفحات، گزارشگیری میشود.
برای مطالعه بیشتر
Using Google APIs in Windows Store Apps
How To Use Google Analytics From C# With OAuth
Google Analytic’s API v3 with C#
.NET Library for Accessing and Querying Google Analytics V3 via Service Account
Google OAuth2 C#
در مطلب جاری قصد داریم با نحوه ارائه یک UI کاربر پسند برای این منظور آشنا شویم و سؤال مهم هم این است: «چگونه میتوان کار کاربر را در حین وارد کردن تعدادی از برچسبهای مرتبط با یک مطلب سادهتر کرد؟». برای این منظور یکی از راه حلهایی که در بسیاری از سایتها مرسوم شده است، استفاده از افزونههایی مانند jQuery TagIt میباشد که در ادامه با نحوه استفاده از آن در ASP.NET MVC آشنا خواهیم شد.
پیشنیازها:
دریافت افزونه TagIt
همچنین دریافت jQuery UI (افزونه TagIt برای نمایش لیست Auto Complete آیتمها از jQuery UI در پشت صحنه استفاده میکند)
<head> <title>@ViewBag.Title</title> <link href="@Url.Content("~/Content/TagIt/jquery-ui-1.8.23.custom.css")" rel="stylesheet" type="text/css" /> <link href="@Url.Content("~/Content/TagIt/tagit-simple-blue.css")" rel="stylesheet" type="text/css" /> <link href="@Url.Content("Content/Site.css")" rel="stylesheet" type="text/css" /> <script src="@Url.Content("~/Scripts/jquery-1.9.1.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Content/TagIt/jquery-ui-1.8.23.custom.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Content/TagIt/tagit.js")" type="text/javascript"></script> @RenderSection("JavaScript", required: false) </head>
آشنایی با مدل برنامه
using System.ComponentModel; using System.ComponentModel.DataAnnotations; namespace jQueryMvcSample04.Models { public class BlogPostViewModel { [DisplayName("عنوان"), Required(ErrorMessage = "*")] public string Title { set; get; } [DisplayName("متن"), Required(ErrorMessage = "*")] public string Body { set; get; } /// <summary> /// آرایهای محدود از برچسبهای این مطلب خاص به صورت جیسون که پیشتر ثبت شده است /// هدف استفاده در حین ویرایش مطلب /// </summary> public string InitialTags { set; get; } /// <summary> /// آرایهای جیسونی از تمام برچسبهای موجود در سیستم /// هدف نمایش منوی انتخاب برچسبها از لیست /// </summary> public string TagsSource { set; get; } /// <summary> /// آرایهای از برچسبهای وارد شده توسط کاربر در حین ثبت مطلب /// </summary> [DisplayName("برچسبها"), Required(ErrorMessage = "*")] public string[] Tags { set; get; } public int? Id { set; get; } } }
افزونه TagIt برای نمایش اطلاعات خود به دو منبع داده نیاز دارد:
الف) TagsSource : لیستی است به فرمت JSON، از هر آنچه که در سیستم پیشتر به عنوان یک برچسب ثبت شده است. از این لیست برای نمایش منوی خودکار انتخاب آیتمها استفاده میشود.
ب) InitialTags : لیستی است به فرمت JSON، از تمام برچسبهای مرتبط با یک مطلب. از این اطلاعات در حین ویرایش یک مطلب استفاده خواهد شد.
در این ViewModel یک خاصیت دیگر به شکل آرایه، به نام Tags تعریف شده است که لیست برچسبهای وارد شده توسط کاربر را دریافت خواهد کرد.
معرفی کنترلر برنامه
using System.Web.Mvc; using jQueryMvcSample04.Extensions; using jQueryMvcSample04.Models; namespace jQueryMvcSample04.Controllers { public class HomeController : Controller { [HttpGet] public ActionResult Index(int? id) { //در ابتدای کار تمام تگهای موجود در سیستم از بانک اطلاعاتی دریافت خواهند شد //از این تگها برای تشکیل منوی انتخاب برچسبها استفاده میشود var tagsSource = new[] { "C#", "C++", "C", "ASP.NET", "MVC" }.ToJson(); //همچنین صرفا برچسبهای مطلب جاری که پیشتر ثبت شدهاند نیز باید از بانک اطلاعاتی دریافت گردند //از این برچسبها برای ویرایش یک مطلب موجود استفاده خواهد شد var init = new[] { "ASP.NET" }.ToJson(); var model = new BlogPostViewModel { TagsSource = tagsSource, InitialTags = init, Id = id }; return View(model); } [HttpPost] public ActionResult Index(BlogPostViewModel data) { if (this.ModelState.IsValid) { //todo: save data // ... return RedirectToAction(actionName: "index", controllerName: "home"); } //در صورت بروز خطا مجددا اطلاعات موجود نمایش داده خواهند شد data.TagsSource = new[] { "C#", "C++", "C", "ASP.NET", "MVC" }.ToJson(); data.InitialTags = data.Tags.ToJson(); return View(data); } } }
با توجه به توضیحاتی که ارائه شد، کنترلر برنامه ساختار واضحتری را یافته است. در اولین بار نمایش صفحه، لیست منبع داده تگها و همچنین تگهای مرتبط با یک مطلب (در صورت وجود) به View ارائه خواهند شد.
از همین ViewModel، در عملیات Post نیز استفاده گردیده و اطلاعات دریافت میگردد.
تعریف متد الحاقی ToJson مورد استفاده را نیز در ادامه ملاحظه مینمائید:
using System.Linq; using System.Web.Script.Serialization; namespace jQueryMvcSample04.Extensions { public static class JsonExt { public static string ToJson(this string[] initialTags) { if (initialTags == null || !initialTags.Any()) return "[]"; else return new JavaScriptSerializer().Serialize(initialTags); } } }
و مرحله آخر تعریف View متناظر است
@model jQueryMvcSample04.Models.BlogPostViewModel @{ ViewBag.Title = "Index"; } @using (Html.BeginForm()) { @Html.ValidationSummary(true) <fieldset> <legend>ثبت مطلب جدید</legend> @Html.HiddenFor(model => model.Id) <div class="editor-label"> @Html.LabelFor(model => model.Title) </div> <div class="editor-field"> @Html.EditorFor(model => model.Title) @Html.ValidationMessageFor(model => model.Title) </div> <div class="editor-label"> @Html.LabelFor(model => model.Body) </div> <div class="editor-field"> @Html.EditorFor(model => model.Body) @Html.ValidationMessageFor(model => model.Body) </div> <div class="editor-label"> @Html.LabelFor(model => model.Tags) </div> <div class="editor-field"> <ul id="tagsList" dir="ltr" name="Tags"> </ul> @Html.ValidationMessageFor(model => model.Tags) </div> <p> <input type="submit" value="Create" /> </p> </fieldset> } @section JavaScript { <script type="text/javascript"> $(document).ready(function () { var tagsSource = @Html.Raw(Model.TagsSource); $('#tagsList').tagit({ tagSource: tagsSource, select: true, triggerKeys: ['enter', 'comma', 'tab'], initialTags: @Html.Raw(Model.InitialTags) }); }); </script> }
الف) برای نمایش افزونه TagIt از یک ul با id مساوی tagsList استفاده شده است.
ب) خواص اضافی موجود در ViewModel که اطلاعات JSON ایی مورد نیاز را بازگشت میدهند در قسمت اسکریپت صفحه مورد استفاده قرار گرفتهاند. در اینجا نیاز است از Html.Raw استفاده شود تا اطلاعات مرتبط با JSON اشتباها encode نشده و قابل استفاده باشند.
دریافت مثال و پروژه کامل این قسمت
jQueryMvcSample04.zip
میخواهیم از یک لیست در گزارش خود استفاده کنیم؛ بطور مثال وقتی در LINQ از دستور ToList استفاده میکنیم و میخواهیم آنرا بصورت مستقیم به Stimul بفرستیم. فرض بر این است که شما DLLهای Stimul را به پروژه اضافه کرده اید و آماده گزارشگیری هستید.
مثلا مدلی در Entity FrameWork با نام base_CenterType
public class base_CenterType { public int ID { get; set; } public string Title { get; set; } public string Dsc { get; set; } }
و متدی بصورت ذیل:
public IList<base_CenterType> GetAll() { return _base_CenterType.ToList(); }
طراحی گزارش برای این لیست به این صورت است:
1- اضافه کردن StiWebReport به فرم به نام StiWebReport1
2- با کلیک بر روی فلش سمت راست و بالای StiWebReport1 و انتخاب Design Report، وارد قسمت طراحی میشویم:
3- با راست کلیک بر روی Business Object و انتخاب New Business Object پنجره مربوطه باز میشود:
4- بعد از زدن OK پنجره زیر باز خواهد شد که باید در کادر Name نام Business Object را انتخاب کنیم که برای خوانایی بهتر است همان نام کلاس را برای آن انتخاب کنیم. چون Category نداریم پس باید کادر آن خالی بماند.
در قسمت Columns باید ستونهای هم نام و هم نوع با خواص کلاس base_CenterType را ایجاد کنیم.
و نهایتا Business Objectی به نام base_CenterType با سه ستون ایجاد خواهد شد.
حال میتوانید ستونهای مورد نظر را در گزارش بکار ببرید.
با فرض اینکه گزارش را طراحی کرده و آنرا در ریشه درایو C ذخیره کردهاید، از قطعه کد زیر برای ارسال لیست به گزارش و نمایش آن استفاده میکنیم.
StiReport mainreport = new StiReport(); mainreport.RegBusinessObject("base_CenterType", base_CenterTypeService.GetAll()); mainreport.Load("C:\\StiWebReport2.mrt"); mainreport.Show();
جهت اینکار یک پروژه از نوع class library ایجاد کنید. فایل class1.cs را که به طور پیش فرض ایجاد میشود، حذف کنید و رفرنسهای Microsoft.Web.Management.dll و Microsoft.Web.Administration.dll را از مسیر زیر اضافه کنید:
\Windows\system32\inetsrv
در مرحله بعدی در تب Build Events کد زیر را در بخش Post-build event command line اضافه کنید. این کد باعث میشود بعد از هر بار کامپایل پروژه، به طور خودکار در GAC ثبت شود:
call "%VS80COMNTOOLS%\vsvars32.bat" > NULL gacutil.exe /if "$(TargetPath)"
نکته:در صورتی که از VS2005 استفاده میکنید در تب Debug در قسمت Start External Program مسیر زیر را قرار بدهید. اینکار برای تست و دیباگینگ پروژه به شما کمک خواهد کرد. این تنظیم شامل نسخههای اکسپرس نمیشود.\windows\system32\inetsrv\inetmgr.exe
ساخت یک Module Provider
رابطهای کاربری IIS همانند هسته و کل سیستمش، ماژولار و قابل خصوصی سازی است. رابط کاربری، مجموعهای از ماژول هایی است که میتوان آنها را حذف یا جایگزین کرد. تگ ورودی یا معرفی برای هر UI یک module provider است. خیلی خودمانی، تگ ماژول پروایدر به معرفی یک UI در IIS میپردازد. لیستی از module providerها را میتوان در فایل زیر در تگ بخش <modules> پیدا کرد.
%windir%\system32\inetsrv\Administration.config
در اولین گام یک کلاس را به اسم imageCopyrightUIModuleProvider.cs ایجاد کرده و سپس آنرا به کد زیر، تغییر میدهیم. کد زیر با استفاده از ModuleDefinition یک نام به تگ Module Provider داده و کلاس imageCopyrightUI را که بعدا تعریف میکنیم، به عنوان مدخل entry رابط کاربری معرفی کرده:
using System; using System.Security; using Microsoft.Web.Management.Server; namespace IIS7Demos { class imageCopyrightUIProvider : ModuleProvider { public override Type ServiceType { get { return null; } } public override ModuleDefinition GetModuleDefinition(IManagementContext context) { return new ModuleDefinition(Name, typeof(imageCopyrightUI).AssemblyQualifiedName); } public override bool SupportsScope(ManagementScope scope) { return true; } } }
با ارث بری از کلاس module provider، سه متد بازنویسی میشوند که یکی از آن ها SupportsScope هست که میدان عمل پروایدر را مشخص میکند، مانند اینکه این پرواید در چه میدانی باید کار کند که میتواند سه گزینهی server,site,application باشد. در کد زیر مثلا میدان عمل application انتخاب شده است ولی در کد بالا با برگشت مستقیم true، همهی میدان را جهت پشتیبانی از این پروایدر اعلام کردیم.
public override bool SupportsScope(ManagementScope scope) { return (scope == ManagementScope.Application) ; }
حالا که پروایدر (معرف رابط کاربری به IIS) تامین شده، نیاز است قلب کار یعنی ماژول معرفی گردد. اصلیترین متدی که باید از اینترفیس ماژول پیاده سازی شود متد initialize است. این متد جایی است که تمام عملیات در آن رخ میدهد. در کلاس زیر imageCopyrightUI ما به معرفی مدخل entry رابط کاربری میپردازیم. در سازندههای این متد، پارامترهای نام، صفحه رابط کاربری وتوضیحی در مورد آن است. تصویر کوچک و بزرگ جهت آیکن سازی (در صورت عدم تعریف آیکن، چرخ دنده نمایش داده میشود) و توصیفهای بلندتر را نیز شامل میشود.
internal class imageCopyrightUI : Module { protected override void Initialize(IServiceProvider serviceProvider, ModuleInfo moduleInfo) { base.Initialize(serviceProvider, moduleInfo); IControlPanel controlPanel = (IControlPanel)GetService(typeof(IControlPanel)); ModulePageInfo modulePageInfo = new ModulePageInfo(this, typeof(imageCopyrightUIPage), "Image Copyright", "Image Copyright",Resource1.Visual_Studio_2012,Resource1.Visual_Studio_2012); controlPanel.RegisterPage(modulePageInfo); } }
شیء ControlPanel مکانی است که قرار است آیکن ماژول نمایش داده شود. شکل زیر به خوبی نام همه قسمتها را بر اساس نام کلاس و اینترفیس آنها دسته بندی کرده است:
پس با تعریف این کلاس جدید ما روی صفحهی کنترل پنل IIS، یک آیکن ساخته و صفحهی رابط کاربری را به نام imageCopyrightUIPage، در آن ریجستر میکنیم. این کلاس را پایینتر شرح دادهایم. ولی قبل از آن اجازه بدهید تا انواع کلاس هایی را که برای ساخت صفحه کاربرد دارند، بررسی نماییم. در این مثال ما با استفاده از پایهایترین کلاس، سادهترین نوع صفحه ممکن را خواهیم ساخت. 4 کلاس برای ساخت یک صفحه وجود دارند که بسته به سناریوی کاری، شما یکی را انتخاب میکنید.
ModulePage | شامل اساسیترین متدها و سورسها شده و هیچگونه رابط کاری ویژهای را در اختیار شما قرار نمیدهد. تنها یک صفحهی خام به شما میدهد که میتوانید از آن استفاده کرده یا حتی با ارث بری از آن، کلاسهای جدیدتری را برای ساخت صفحات مختلف و ویژهتر بسازید. در حال حاضر که هیچ کدام از ویژگیهای IIS فعلی از این کلاس برای ساخت رابط کاربری استفاده نکردهاند. |
ModuleDialogPage | یک صفحه شبیه به دیالوگ را ایجاد میکند و شامل دکمههای Apply و Cancel میشود به همراه یک سری متدهای اضافیتر که اجازهی override کردن آنها را دارید. همچنین یک سری از کارهایی چون refresh و از این دست عملیات خودکار را نیز انجام میدهد. از نمونه رابطهایی که از این صفحات استفاده میکنند میتوان machine key و management service را اسم برد. |
ModulePropertiesPage | این صفحه یک رابط کاربری را شبیه پنجره property که در ویژوال استادیو وجود دارد، در دسترس شما قرار میدهد. تمام عناصر آن در یک حالت گرید grid لیست میشوند. از نمونههای موجود میتوان به CGI,ASP.Net Compilation اشاره کرد. |
ModuleListPage | این کلاس برای مواقعی کاربرد دارد که شما قرار است لیستی از آیتمها را نشان دهید. در این صفحه شما یک ListView دارید که میتوانید عملیات جست و جو، گروه بندی و نحوهی نمایش لیست را روی آن اعمال کنید. |
public sealed class imageCopyrightUIPage : ModulePage { public string message; public bool featureenabled; public string color; ComboBox _colCombo = new ComboBox(); TextBox _msgTB = new TextBox(); CheckBox _enabledCB = new CheckBox(); public imageCopyrightUIPage() { this.Initialize(); } void Initialize() { Label crlabel = new Label(); crlabel.Left = 50; crlabel.Top = 100; crlabel.AutoSize = true; crlabel.Text = "Enable Image Copyright:"; _enabledCB.Text = ""; _enabledCB.Left = 200; _enabledCB.Top = 100; _enabledCB.AutoSize = true; Label msglabel = new Label(); msglabel.Left = 150; msglabel.Top = 130; msglabel.AutoSize = true; msglabel.Text = "Message:"; _msgTB.Left = 200; _msgTB.Top = 130; _msgTB.Width = 200; _msgTB.Height = 50; Label collabel = new Label(); collabel.Left = 160; collabel.Top = 160; collabel.AutoSize = true; collabel.Text = "Color:"; _colCombo.Left = 200; _colCombo.Top = 160; _colCombo.Width = 50; _colCombo.Height = 90; _colCombo.Items.Add((object)"Yellow"); _colCombo.Items.Add((object)"Blue"); _colCombo.Items.Add((object)"Red"); _colCombo.Items.Add((object)"White"); Button apply = new Button(); apply.Text = "Apply"; apply.Click += new EventHandler(this.applyClick); apply.Left = 200; apply.AutoSize = true; apply.Top = 250; Controls.Add(crlabel); Controls.Add(_enabledCB); Controls.Add(collabel); Controls.Add(_colCombo); Controls.Add(msglabel); Controls.Add(_msgTB); Controls.Add(apply); } public void ReadConfig() { try { ServerManager mgr; ConfigurationSection section; mgr = new ServerManager(); Configuration config = mgr.GetWebConfiguration( Connection.ConfigurationPath.SiteName, Connection.ConfigurationPath.ApplicationPath + Connection.ConfigurationPath.FolderPath); section = config.GetSection("system.webServer/imageCopyright"); color = (string)section.GetAttribute("color").Value; message = (string)section.GetAttribute("message").Value; featureenabled = (bool)section.GetAttribute("enabled").Value; } catch { } } void UpdateUI() { _enabledCB.Checked = featureenabled; int n = _colCombo.FindString(color, 0); _colCombo.SelectedIndex = n; _msgTB.Text = message; } protected override void OnActivated(bool initialActivation) { base.OnActivated(initialActivation); if (initialActivation) { ReadConfig(); UpdateUI(); } } private void applyClick(Object sender, EventArgs e) { try { UpdateVariables(); ServerManager mgr; ConfigurationSection section; mgr = new ServerManager(); Configuration config = mgr.GetWebConfiguration ( Connection.ConfigurationPath.SiteName, Connection.ConfigurationPath.ApplicationPath + Connection.ConfigurationPath.FolderPath ); section = config.GetSection("system.webServer/imageCopyright"); section.GetAttribute("color").Value = (object)color; section.GetAttribute("message").Value = (object)message; section.GetAttribute("enabled").Value = (object)featureenabled; mgr.CommitChanges(); } catch { } } public void UpdateVariables() { featureenabled = _enabledCB.Checked; color = _colCombo.Text; message = _msgTB.Text; } }
mgr.CommitChanges();
%vs110comntools%\vsvars32.bat
GACUTIL /l ClassLibrary1
<add name="imageCopyrightUI" type="ClassLibrary1.imageCopyrightUIProvider, ClassLibrary1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=d0b3b3b2aa8ea14b"/>
%windir%\system32\inetsrv\config\administration.config
از آنجا که این مقاله طولانی شده است، باقی موارد ویرایشی روی این UI را در مقاله بعدی بررسی خواهیم کرد.
@using (Ajax.BeginForm("Upload", "Attachment", FormMethod.Post, new AjaxOptions { HttpMethod = "POST", }, new { encType = "multipart/form-data", id = "attach-form" })) { @Html.AntiForgeryToken() @Html.TextBoxFor(m => m.FirstName }) <input type="file" name="Files" data-buttonText="انتخاب تصویر"> <button type="submit">ارسال</button> }
var formData = new FormData(); $('form').submit(function() { var action = $(this).attr('action'); var formData = new FormData($(this).get(0)); $.ajax({ type: "POST", dataType: "json", url: action, data: formData, processData: false, contentType: false, success: function(data) { //... } success: function(data) { //... } }); return false; });
public class MyModel
{
public string FirstName{ get; set; }
public IEnumerable<HttpPostedFileBase> Files { get; set; }
}
[AjaxOnly] [HttpPost] [ValidateAntiForgeryToken] public ActionResult Upload(MyModel model) { if (!ModelState.IsValid) return //.... if(model.Files != null) foreach (var file in model.Files) if (file != null && file.ContentLength > 0) { // .... } }
mysite.com/reporting
using System.ComponentModel.DataAnnotations; namespace SampleProject.Models { public class Channel { public string ChannelTitle { get; set; } [Required] public string ChannelUrl { get; set; } } }
using System.Collections.Generic; namespace SampleProject.Models { public static class ChannelDataSource { static ChannelDataSource() => Channels = new List<Channel>(); public static List<Channel> Channels { get; private set; } public static void Add(Channel channel) => Channels.Add(channel); } }
using SampleProject.Models; using System.Linq; using System.Web.Mvc; namespace SampleProject.Controllers { public class ChannelController : Controller { // GET: Channel public ActionResult Index() { var channels = ChannelDataSource.Channels; return View(channels); } public ActionResult Channel(string channelUrl) { if (string.IsNullOrWhiteSpace(channelUrl)) { return new HttpNotFoundResult("channel not found!"); } var channel = ChannelDataSource.Channels.SingleOrDefault(ch => ch.ChannelUrl == channelUrl.ToLower()); if (channel == null) { return new HttpNotFoundResult("channel not found!"); } return View(channel); } public ActionResult Create() => View(); [HttpPost] [ValidateAntiForgeryToken] public ActionResult Create(Channel channel) { if (!ModelState.IsValid) { ModelState.AddModelError(string.Empty, "Please check your inputs!"); return View(channel); } ChannelDataSource.Add(channel); TempData["Message"] = "Channel added successfully!"; return RedirectToAction(nameof(Index)); } } }
using System.Web.Mvc; using System.Web.Routing; namespace SampleProject { public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( name: "ChannelUrls", url: "{channelurl}", defaults: new { controller = "Channel", action = "Channel", id = UrlParameter.Optional } ); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Channel", action = "Index", id = UrlParameter.Optional } ); } } }
using System.Collections.Generic; using System.Linq; using System.Web.Routing; namespace SampleProject.Models { public static class Utility { public static List<string> GetAllAreaNames() { var areaNames = RouteTable.Routes.OfType<Route>() .Where(d => d.DataTokens != null) .Where(d=> d.DataTokens.ContainsKey("area")) .Select(r => r.DataTokens["area"].ToString().ToLower()) .ToList(); return areaNames; } } }
using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Web.Mvc; using SampleProject.Models; namespace SampleProject.CustomValidators { public class CheckForAreaExisting : ValidationAttribute, IClientValidatable { public List<string> AreaNames { get { return Utility.GetAllAreaNames(); } } public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context) { var rule = new ModelClientValidationRule { ValidationType = "checkforareaexisting", ErrorMessage = FormatErrorMessage(metadata.GetDisplayName()) }; rule.ValidationParameters.Add("areanames", string.Join(",", AreaNames)); yield return rule; } public override bool IsValid(object value) { if (value != null) { return Utility.GetAllAreaNames() .SingleOrDefault(area => area == value.ToString().ToLower()) == null; } return true; } } }
jQuery.validator.addMethod("checkforareaexisting", function (value, element, param) { var isValueOneOfTheAreaNames = $.inArray(value.toLowerCase(), param.areaNames) === -1; return isValueOneOfTheAreaNames; }); $.validator.unobtrusive.adapters.add('checkforareaexisting', ['areanames'], function (options) { options.rules['checkforareaexisting'] = { areaNames: options.params.areanames.split(',') }; options.messages['checkforareaexisting'] = options.message; });
<!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>_Layout</title> <style> </style> </head> <body> <div> @RenderBody() </div> <script src="~/Scripts/Jquery.js"></script> <script src="~/Scripts/jquery.validate.min.js"></script> <script src="~/Scripts/jquery.validate.unobtrusive.min.js"></script> <script src="~/Scripts/CustomValidation.js"></script> </body> </html>
using SampleProject.CustomValidators; using System.ComponentModel.DataAnnotations; namespace SampleProject.Models { public class Channel { public string ChannelTitle { get; set; } [Required] [CheckForAreaExisting(ErrorMessage = "You can't use this url for your channel!")] public string ChannelUrl { get; set; } } }
[HttpPost] public ActionResult CheckForAreaExisting(string channelUrl) { var isValueOneOfTheAreaNames = Utility.GetAllAreaNames() .SingleOrDefault(area => area == channelUrl.ToLower()) == null; return Json(isValueOneOfTheAreaNames); }
using System.ComponentModel.DataAnnotations; using System.Web.Mvc; namespace SampleProject.Models { public class Channel { public string ChannelTitle { get; set; } [Required] [Remote("CheckForAreaExisting", "Channel", ErrorMessage = "You can't use this url for your channel!", HttpMethod = "Post")] public string ChannelUrl { get; set; } } }
در ادامه، تعاریف سایر موجودیتهای سیستم ثبت سفارشات و نگاشت آنها را بررسی خواهیم کرد.
کلاس Product تعریف شده در فایل جدید Product.cs در پوشه domain برنامه:
namespace NHSample1.Domain
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal UnitPrice { get; set; }
public bool Discontinued { get; set; }
}
}
using FluentNHibernate.Mapping;
using NHSample1.Domain;
namespace NHSample1.Mappings
{
public class ProductMapping : ClassMap<Product>
{
public ProductMapping()
{
Not.LazyLoad();
Id(p => p.Id).GeneratedBy.HiLo("1000");
Map(p => p.Name).Length(50).Not.Nullable();
Map(p => p.UnitPrice).Not.Nullable();
Map(p => p.Discontinued).Not.Nullable();
}
}
}
آزمون واحد بررسی این نگاشت نیز همانند مثال قبلی است.
کلاس ProductMapping_Fixture را در فایل جدید ProductMapping_Fixture.cs به پروژه UnitTests خود (که ارجاعات آنرا در قسمت قبل مشخص کردیم) خواهیم افزود:
using NUnit.Framework;
using FluentNHibernate.Testing;
using NHSample1.Domain;
namespace UnitTests
{
[TestFixture]
public class ProductMapping_Fixture : FixtureBase
{
[Test]
public void can_correctly_map_product()
{
new PersistenceSpecification<Product>(Session)
.CheckProperty(p => p.Id, 1001)
.CheckProperty(p => p.Name, "Apples")
.CheckProperty(p => p.UnitPrice, 10.45m)
.CheckProperty(p => p.Discontinued, true)
.VerifyTheMappings();
}
}
}
ProductMapping_Fixture.can_correctly_map_product : Passed
NHibernate: select next_hi from hibernate_unique_key
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 2, @p1 = 1
NHibernate: INSERT INTO "Product" (Name, UnitPrice, Discontinued, Id) VALUES (@p0, @p1, @p2, @p3);@p0 = 'Apples', @p1 = 10.45, @p2 = True, @p3 = 1001
NHibernate: SELECT product0_.Id as Id1_0_, product0_.Name as Name1_0_, product0_.UnitPrice as UnitPrice1_0_, product0_.Discontinued as Disconti4_1_0_ FROM "Product" product0_ WHERE product0_.Id=@p0;@p0 = 1001
در ادامه تعریف کلاس کارمند، نگاشت و آزمون واحد آن به صورت زیر خواهند بود:
using System;
namespace NHSample1.Domain
{
public class Employee
{
public int Id { set; get; }
public string LastName { get; set; }
public string FirstName { get; set; }
}
}
using NHSample1.Domain;
using FluentNHibernate.Mapping;
namespace NHSample1.Mappings
{
public class EmployeeMapping : ClassMap<Employee>
{
public EmployeeMapping()
{
Not.LazyLoad();
Id(e => e.Id).GeneratedBy.Assigned();
Map(e => e.LastName).Length(50);
Map(e => e.FirstName).Length(50);
}
}
}
using NUnit.Framework;
using NHSample1.Domain;
using FluentNHibernate.Testing;
namespace UnitTests
{
[TestFixture]
public class EmployeeMapping_Fixture : FixtureBase
{
[Test]
public void can_correctly_map_employee()
{
new PersistenceSpecification<Employee>(Session)
.CheckProperty(p => p.Id, 1001)
.CheckProperty(p => p.FirstName, "name1")
.CheckProperty(p => p.LastName, "lname1")
.VerifyTheMappings();
}
}
}
NHibernate: select next_hi from hibernate_unique_key
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 2, @p1 = 1
NHibernate: INSERT INTO "Employee" (LastName, FirstName, Id) VALUES (@p0, @p1, @p2);@p0 = 'lname1', @p1 = 'name1', @p2 = 1001
NHibernate: SELECT employee0_.Id as Id4_0_, employee0_.LastName as LastName4_0_, employee0_.FirstName as FirstName4_0_ FROM "Employee" employee0_ WHERE employee0_.Id=@p0;@p0 = 1001
همانطور که ملاحظه میکنید، این آزمونهای واحد 4 مرحله را در یک سطر انجام میدهند:
الف) ایجاد یک وهله از کلاس Employee
ب) ثبت اطلاعات کارمند در دیتابیس
ج) دریافت اطلاعات کارمند در وهلهای جدید از شیء Employee
د) و در پایان بررسی میکند که آیا شیء جدید ایجاد شده با شیء اولیه مطابقت دارد یا خیر
اکنون در ادامه پیاده سازی سیستم ثبت سفارشات، به قسمت جالب این مدل میرسیم. قسمتی که در آن ارتباطات اشیاء و روابط one-to-many تعریف خواهند شد. تعاریف کلاسهای OrderItem و OrderItemMapping را به صورت زیر در نظر بگیرید:
کلاس OrderItem تعریف شده در فایل جدید OrderItem.cs واقع شده در پوشه domain پروژه:
که در آن هر سفارش (order) دقیقا از یک محصول (product) تشکیل میشود و هر محصول میتواند در سفارشات متعدد و مختلفی درخواست شود.
namespace NHSample1.Domain
{
public class OrderItem
{
public int Id { get; set; }
public int Quantity { get; set; }
public Product Product { get; set; }
}
}
using FluentNHibernate.Mapping;
using NHSample1.Domain;
namespace NHSample1.Mappings
{
public class OrderItemMapping : ClassMap<OrderItem>
{
public OrderItemMapping()
{
Not.LazyLoad();
Id(oi => oi.Id).GeneratedBy.Assigned();
Map(oi => oi.Quantity).Not.Nullable();
References(oi => oi.Product).Not.Nullable();
}
}
}
نکتهی دیگر مهم آن این مورد است که Id در اینجا به صورت یک کلید تعریف نشده است. یک آیتم سفارش داده شده، موجودیت به حساب نیامده و فقط یک شیء مقداری (value object) است و به خودی خود امکان وجود ندارد. هر وهله از آن تنها توسط یک سفارش قابل تعریف است. بنابراین id در اینجا فقط به عنوان یک index میتواند مورد استفاده قرار گیرد و فقط توسط شیء Order زمانیکه یک OrderItem به آن اضافه میشود، مقدار دهی خواهد شد.
اگر برای این نگاشت نیز آزمون واحد تهیه کنیم، به صورت زیر خواهد بود:
using NUnit.Framework;
using NHSample1.Domain;
using FluentNHibernate.Testing;
namespace UnitTests
{
[TestFixture]
public class OrderItemMapping_Fixture : FixtureBase
{
[Test]
public void can_correctly_map_order_item()
{
var product = new Product
{
Name = "Apples",
UnitPrice = 4.5m,
Discontinued = true
};
new PersistenceSpecification<OrderItem>(Session)
.CheckProperty(p => p.Id, 1)
.CheckProperty(p => p.Quantity, 5)
.CheckReference(p => p.Product, product)
.VerifyTheMappings();
}
}
}
مشکل! این آزمون واحد با شکست مواجه خواهد شد، زیرا هنوز مشخص نکردهایم که دو شیء Product را که در قسمت CheckReference فوق برای این منظور معرفی کردهایم، چگونه باید با هم مقایسه کرد. در مورد مقایسه نوعهای اولیه و اصلی مانند int و string و امثال آن مشکلی نیست، اما باید منطق مقایسه سایر اشیاء سفارشی خود را با پیاده سازی اینترفیس IEqualityComparer دقیقا مشخص سازیم:
using System.Collections;
using NHSample1.Domain;
namespace UnitTests
{
public class CustomEqualityComparer : IEqualityComparer
{
public bool Equals(object x, object y)
{
if (ReferenceEquals(x, y)) return true;
if (x == null || y == null) return false;
if (x is Product && y is Product)
return (x as Product).Id == (y as Product).Id;
if (x is Customer && y is Customer)
return (x as Customer).Id == (y as Customer).Id;
if (x is Employee && y is Employee)
return (x as Employee).Id == (y as Employee).Id;
if (x is OrderItem && y is OrderItem)
return (x as OrderItem).Id == (y as OrderItem).Id;
return x.Equals(y);
}
public int GetHashCode(object obj)
{
//شاید وقتی دیگر
return obj.GetHashCode();
}
}
}
سپس برای بکار گیری این کلاس جدید، سطر مربوط به استفاده از PersistenceSpecification به صورت زیر تغییر خواهد کرد:
new PersistenceSpecification<OrderItem>(Session, new CustomEqualityComparer())
پس از این تغییرات و مشخص سازی نحوهی مقایسه دو شیء سفارشی، آزمون واحد ما پاس شده و خروجی SQL تولید شده آن به صورت زیر میباشد:
NHibernate: select next_hi from hibernate_unique_key
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 2, @p1 = 1
NHibernate: INSERT INTO "Product" (Name, UnitPrice, Discontinued, Id) VALUES (@p0, @p1, @p2, @p3);@p0 = 'Apples', @p1 = 4.5, @p2 = True, @p3 = 1001
NHibernate: INSERT INTO "OrderItem" (Quantity, Product_id, Id) VALUES (@p0, @p1, @p2);@p0 = 5, @p1 = 1001, @p2 = 1
NHibernate: SELECT orderitem0_.Id as Id0_1_, orderitem0_.Quantity as Quantity0_1_, orderitem0_.Product_id as Product3_0_1_, product1_.Id as Id3_0_, product1_.Name as Name3_0_, product1_.UnitPrice as UnitPrice3_0_, product1_.Discontinued as Disconti4_3_0_ FROM "OrderItem" orderitem0_ inner join "Product" product1_ on orderitem0_.Product_id=product1_.Id WHERE orderitem0_.Id=@p0;@p0 = 1
قسمت پایانی کار تعاریف کلاسهای نگاشت، مربوط به کلاس Order است که در ادامه بررسی خواهد شد.
using System;
using System.Collections.Generic;
namespace NHSample1.Domain
{
public class Order
{
public int Id { set; get; }
public DateTime OrderDate { get; set; }
public Employee Employee { get; set; }
public Customer Customer { get; set; }
public IList<OrderItem> OrderItems { get; set; }
}
}
using NHSample1.Domain;
using FluentNHibernate.Mapping;
namespace NHSample1.Mappings
{
public class OrderMapping : ClassMap<Order>
{
public OrderMapping()
{
Not.LazyLoad();
Id(o => o.Id).GeneratedBy.GuidComb();
Map(o => o.OrderDate).Not.Nullable();
References(o => o.Employee).Not.Nullable();
References(o => o.Customer).Not.Nullable();
HasMany(o => o.OrderItems)
.AsList(index => index.Column("ListIndex").Type<int>());
}
}
}
قسمت جدید آن HasMany است که جهت تعریف رابطه one-to-many بکار گرفته شده است. یک سفارش رابطه many-to-one با یک مشتری و همچنین کارمندی که این رکورد را ثبت میکند، دارد. در اینجا مجموعه آیتمهای یک سفارش به صورت یک لیست بازگشت داده میشود و ایندکس آن به ستونی به نام ListIndex در یک جدول دیتابیس نگاشت خواهد شد. نوع این ستون، int میباشد.
using System;
using System.Collections.Generic;
using NUnit.Framework;
using NHSample1.Domain;
using FluentNHibernate.Testing;
namespace UnitTests
{
[TestFixture]
public class OrderMapping_Fixture : FixtureBase
{
[Test]
public void can_correctly_map_an_order()
{
{
var product1 =
new Product
{
Name = "Apples",
UnitPrice = 4.5m,
Discontinued = true
};
var product2 =
new Product
{
Name = "Pears",
UnitPrice = 3.5m,
Discontinued = false
};
Session.Save(product1);
Session.Save(product2);
var items = new List<OrderItem>
{
new OrderItem
{
Id = 1,
Quantity = 100,
Product = product1
},
new OrderItem
{
Id = 2,
Quantity = 200,
Product = product2
}
};
var customer = new Customer
{
FirstName = "Vahid",
LastName = "Nasiri",
AddressLine1 = "Addr1",
AddressLine2 = "Addr2",
PostalCode = "1234",
City = "Tehran",
CountryCode = "IR"
};
var employee =
new Employee
{
FirstName = "name1",
LastName = "lname1"
};
var order = new Order
{
Customer = customer,
Employee = employee,
OrderDate = DateTime.Today,
OrderItems = items
};
new PersistenceSpecification<Order>(Session, new CustomEqualityComparer())
.CheckProperty(o => o.OrderDate, order.OrderDate)
.CheckReference(o => o.Customer, order.Customer)
.CheckReference(o => o.Employee, order.Employee)
.CheckList(o => o.OrderItems, order.OrderItems)
.VerifyTheMappings();
}
}
}
}
متد آزمون واحد فوق کمی طولانی است؛ زیرا در آن باید تعاریف انواع و اقسام اشیاء مورد استفاده را مشخص نمود (و ارزش کار نیز دقیقا در همینجا مشخص میشود که بجای SQL نوشتن، با اشیایی که توسط کامپایلر تحت نظر هستند سر و کار داریم).
تنها نکته جدید آن استفاده از CheckList برای بررسی IList تعریف شده در قسمت قبل است.
خروجی SQL این آزمون واحد پس از اجرا و موفقیت آن به صورت زیر است:
OrderMapping_Fixture.can_correctly_map_an_order : Passed
NHibernate: select next_hi from hibernate_unique_key
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 2, @p1 = 1
NHibernate: select next_hi from hibernate_unique_key
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 3, @p1 = 2
NHibernate: INSERT INTO "Product" (Name, UnitPrice, Discontinued, Id) VALUES (@p0, @p1, @p2, @p3);@p0 = 'Apples', @p1 = 4.5, @p2 = True, @p3 = 1001
NHibernate: INSERT INTO "Product" (Name, UnitPrice, Discontinued, Id) VALUES (@p0, @p1, @p2, @p3);@p0 = 'Pears', @p1 = 3.5, @p2 = False, @p3 = 1002
NHibernate: INSERT INTO "Customer" (FirstName, LastName, AddressLine1, AddressLine2, PostalCode, City, CountryCode, Id) VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7);@p0 = 'Vahid', @p1 = 'Nasiri', @p2 = 'Addr1', @p3 = 'Addr2', @p4 = '1234', @p5 = 'Tehran', @p6 = 'IR', @p7 = 2002
NHibernate: select next_hi from hibernate_unique_key
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 4, @p1 = 3
NHibernate: INSERT INTO "Employee" (LastName, FirstName, Id) VALUES (@p0, @p1, @p2);@p0 = 'lname1', @p1 = 'name1', @p2 = 3003
NHibernate: INSERT INTO "OrderItem" (Quantity, Product_id, Id) VALUES (@p0, @p1, @p2);@p0 = 100, @p1 = 1001, @p2 = 1
NHibernate: INSERT INTO "OrderItem" (Quantity, Product_id, Id) VALUES (@p0, @p1, @p2);@p0 = 200, @p1 = 1002, @p2 = 2
NHibernate: INSERT INTO "Order" (OrderDate, Employee_id, Customer_id, Id) VALUES (@p0, @p1, @p2, @p3);@p0 = 2009/10/10 12:00:00 ق.ظ, @p1 = 3003, @p2 = 2002, @p3 = 0
NHibernate: UPDATE "OrderItem" SET Order_id = @p0, ListIndex = @p1 WHERE Id = @p2;@p0 = 0, @p1 = 0, @p2 = 1
NHibernate: UPDATE "OrderItem" SET Order_id = @p0, ListIndex = @p1 WHERE Id = @p2;@p0 = 0, @p1 = 1, @p2 = 2
NHibernate: SELECT order0_.Id as Id1_2_, order0_.OrderDate as OrderDate1_2_, order0_.Employee_id as Employee3_1_2_, order0_.Customer_id as Customer4_1_2_, employee1_.Id as Id4_0_, employee1_.LastName as LastName4_0_, employee1_.FirstName as FirstName4_0_, customer2_.Id as Id2_1_, customer2_.FirstName as FirstName2_1_, customer2_.LastName as LastName2_1_, customer2_.AddressLine1 as AddressL4_2_1_, customer2_.AddressLine2 as AddressL5_2_1_, customer2_.PostalCode as PostalCode2_1_, customer2_.City as City2_1_, customer2_.CountryCode as CountryC8_2_1_ FROM "Order" order0_ inner join "Employee" employee1_ on order0_.Employee_id=employee1_.Id inner join "Customer" customer2_ on order0_.Customer_id=customer2_.Id WHERE order0_.Id=@p0;@p0 = 0
NHibernate: SELECT orderitems0_.Order_id as Order4_2_, orderitems0_.Id as Id2_, orderitems0_.ListIndex as ListIndex2_, orderitems0_.Id as Id0_1_, orderitems0_.Quantity as Quantity0_1_, orderitems0_.Product_id as Product3_0_1_, product1_.Id as Id3_0_, product1_.Name as Name3_0_, product1_.UnitPrice as UnitPrice3_0_, product1_.Discontinued as Disconti4_3_0_ FROM "OrderItem" orderitems0_ inner join "Product" product1_ on orderitems0_.Product_id=product1_.Id WHERE orderitems0_.Order_id=@p0;@p0 = 0
تا اینجای کار تعاریف اشیاء ، نگاشت آنها و همچنین بررسی صحت این نگاشتها به پایان میرسد.
نکته:
دیتابیس برنامه را جهت آزمونهای واحد برنامه، از نوع SQLite ساخته شده در حافظه مشخص کردیم. اگر علاقمند باشید که database schema تولید شده توسط NHibernate را مشاهده نمائید، در متد SetupContext کلاس FixtureBase که در قسمت قبل معرفی شد، سطر آخر را به صورت زیر تغییر دهید، تا اسکریپت دیتابیس نیز به صورت خودکار در خروجی اس کیوال آزمون واحد لحاظ شود (پارامتر دوم آن مشخص میکند که schema ساخته شده، نمایش داده شود یا خیر):
SessionSource.BuildSchema(Session, true);
drop table if exists "OrderItem"
drop table if exists "Order"
drop table if exists "Customer"
drop table if exists "Product"
drop table if exists "Employee"
drop table if exists hibernate_unique_key
create table "OrderItem" (
Id INTEGER not null,
Quantity INTEGER not null,
Product_id INTEGER not null,
Order_id INTEGER,
ListIndex INTEGER,
primary key (Id)
)
create table "Order" (
Id INTEGER not null,
OrderDate DATETIME not null,
Employee_id INTEGER not null,
Customer_id INTEGER not null,
primary key (Id)
)
create table "Customer" (
Id INTEGER not null,
FirstName TEXT not null,
LastName TEXT not null,
AddressLine1 TEXT not null,
AddressLine2 TEXT,
PostalCode TEXT not null,
City TEXT not null,
CountryCode TEXT not null,
primary key (Id)
)
create table "Product" (
Id INTEGER not null,
Name TEXT not null,
UnitPrice NUMERIC not null,
Discontinued INTEGER not null,
primary key (Id)
)
create table "Employee" (
Id INTEGER not null,
LastName TEXT,
FirstName TEXT,
primary key (Id)
)
create table hibernate_unique_key (
next_hi INTEGER
)
برای اینکه از دیتابیس اس کیوال سرور استفاده کنیم، در همان متد SetupContext کلاس مذکور، سطر اول را به صورت زیر تغییر دهید (نوع دیتابیس اس کیوال سرور 2008 مشخص شده و سپس رشته اتصالی به دیتابیس ذکر گردیده است):
var cfg = Fluently.Configure().Database(
// SQLiteConfiguration.Standard.ShowSql().InMemory
MsSqlConfiguration
.MsSql2008
.ShowSql()
.ConnectionString("Data Source=(local);Initial Catalog=testdb2009;Integrated Security = true")
);
if exists (select 1 from sys.objects where object_id = OBJECT_ID(N'[FK3EF88858466CFBF7]') AND parent_object_id = OBJECT_ID('[OrderItem]'))
alter table [OrderItem] drop constraint FK3EF88858466CFBF7
if exists (select 1 from sys.objects where object_id = OBJECT_ID(N'[FK3EF888589F32DE52]') AND parent_object_id = OBJECT_ID('[OrderItem]'))
alter table [OrderItem] drop constraint FK3EF888589F32DE52
if exists (select 1 from sys.objects where object_id = OBJECT_ID(N'[FK3117099B1EBA72BC]') AND parent_object_id = OBJECT_ID('[Order]'))
alter table [Order] drop constraint FK3117099B1EBA72BC
if exists (select 1 from sys.objects where object_id = OBJECT_ID(N'[FK3117099BB2F9593A]') AND parent_object_id = OBJECT_ID('[Order]'))
alter table [Order] drop constraint FK3117099BB2F9593A
if exists (select * from dbo.sysobjects where id = object_id(N'[OrderItem]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table [OrderItem]
if exists (select * from dbo.sysobjects where id = object_id(N'[Order]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table [Order]
if exists (select * from dbo.sysobjects where id = object_id(N'[Customer]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table [Customer]
if exists (select * from dbo.sysobjects where id = object_id(N'[Product]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table [Product]
if exists (select * from dbo.sysobjects where id = object_id(N'[Employee]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table [Employee]
if exists (select * from dbo.sysobjects where id = object_id(N'hibernate_unique_key') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table hibernate_unique_key
create table [OrderItem] (
Id INT not null,
Quantity INT not null,
Product_id INT not null,
Order_id INT null,
ListIndex INT null,
primary key (Id)
)
create table [Order] (
Id INT not null,
OrderDate DATETIME not null,
Employee_id INT not null,
Customer_id INT not null,
primary key (Id)
)
create table [Customer] (
Id INT not null,
FirstName NVARCHAR(50) not null,
LastName NVARCHAR(50) not null,
AddressLine1 NVARCHAR(50) not null,
AddressLine2 NVARCHAR(50) null,
PostalCode NVARCHAR(10) not null,
City NVARCHAR(50) not null,
CountryCode NVARCHAR(2) not null,
primary key (Id)
)
create table [Product] (
Id INT not null,
Name NVARCHAR(50) not null,
UnitPrice DECIMAL(19,5) not null,
Discontinued BIT not null,
primary key (Id)
)
create table [Employee] (
Id INT not null,
LastName NVARCHAR(50) null,
FirstName NVARCHAR(50) null,
primary key (Id)
)
alter table [OrderItem]
add constraint FK3EF88858466CFBF7
foreign key (Product_id)
references [Product]
alter table [OrderItem]
add constraint FK3EF888589F32DE52
foreign key (Order_id)
references [Order]
alter table [Order]
add constraint FK3117099B1EBA72BC
foreign key (Employee_id)
references [Employee]
alter table [Order]
add constraint FK3117099BB2F9593A
foreign key (Customer_id)
references [Customer]
create table hibernate_unique_key (
next_hi INT
)
الف) جداول مطابق نام کلاسهای ما تولید شدهاند.
ب) نام فیلدها دقیقا مطابق نام خواص کلاسهای ما تشکیل شدهاند.
ج) Id ها به صورت primary key تعریف شدهاند (از آنجائیکه ما در هنگام تعریف نگاشتها، آنها را از نوع identity مشخص کرده بودیم).
د) رشتهها به نوع nvarchar با اندازه 50 نگاشت شدهاند.
ه) کلیدهای خارجی بر اساس نام جدول با پسوند _id تشکیل شدهاند.
ادامه دارد ...
Service Layer
نقش لایهی سرویس این است که به عنوان یک مدخل ورودی به برنامه کاربردی عمل کند. در برخی مواقع این لایه را به عنوان لایهی Facade نیز میشناسند. این لایه، دادهها را در قالب یک نوع داده ای قوی (Strongly Typed) به نام View Model، برای لایهی Presentation فراهم میکند. کلاس View Model یک Strongly Typed محسوب میشود که نماهای خاصی از دادهها را که متفاوت از دید یا نمای تجاری آن است، بصورت بهینه ارائه مینماید. در مورد الگوی View Model در مباحث بعدی بیشتر صحبت خواهم کرد.
الگوی Facade یک Interface ساده را به منظور کنترل دسترسی به مجموعه ای از Interfaceها و زیر سیستمهای پیچیده ارائه میکند. در مباحث بعدی در مورد آن بیشتر صحبت خواهم کرد.
کلاسی با نام ProductViewModel را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:
public class ProductViewModel { Public int ProductId {get; set;} public string Name { get; set; } public string Rrp { get; set; } public string SellingPrice { get; set; } public string Discount { get; set; } public string Savings { get; set; } }
برای اینکه کلاینت با لایهی سرویس در تعامل باشد باید از الگوی Request/Response Message استفاده کنیم. بخش Request توسط کلاینت تغذیه میشود و پارامترهای مورد نیاز را فراهم میکند. کلاسی با نام ProductListRequest را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:
using SoCPatterns.Layered.Model; namespace SoCPatterns.Layered.Service { public class ProductListRequest { public CustomerType CustomerType { get; set; } } }
در شی Response نیز بررسی میکنیم که درخواست به درستی انجام شده باشد، دادههای مورد نیاز را برای کلاینت فراهم میکنیم و همچنین در صورت عدم اجرای صحیح درخواست، پیام مناسب را به کلاینت ارسال مینماییم. کلاسی با نام ProductListResponse را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:
public class ProductListResponse { public bool Success { get; set; } public string Message { get; set; } public IList<ProductViewModel> Products { get; set; } }
به منظور تبدیل موجودیت Product به ProductViewModel، به دو متد نیاز داریم، یکی برای تبدیل یک Product و دیگری برای تبدیل لیستی از Product. شما میتوانید این دو متد را به کلاس Product موجود در Domain Model اضافه نمایید، اما این متدها نیاز واقعی منطق تجاری نمیباشند. بنابراین بهترین انتخاب، استفاده از Extension Methodها میباشد که باید برای کلاس Product و در لایهی سرویس ایجاد نمایید. کلاسی با نام ProductMapperExtensionMethods را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:
public static class ProductMapperExtensionMethods { public static ProductViewModel ConvertToProductViewModel(this Model.Product product) { ProductViewModel productViewModel = new ProductViewModel(); productViewModel.ProductId = product.Id; productViewModel.Name = product.Name; productViewModel.RRP = String.Format(“{0:C}”, product.Price.RRP); productViewModel.SellingPrice = String.Format(“{0:C}”, product.Price.SellingPrice); if (product.Price.Discount > 0) productViewModel.Discount = String.Format(“{0:C}”, product.Price.Discount); if (product.Price.Savings < 1 && product.Price.Savings > 0) productViewModel.Savings = product.Price.Savings.ToString(“#%”); return productViewModel; } public static IList<ProductViewModel> ConvertToProductListViewModel( this IList<Model.Product> products) { IList<ProductViewModel> productViewModels = new List<ProductViewModel>(); foreach(Model.Product p in products) { productViewModels.Add(p.ConvertToProductViewModel()); } return productViewModels; } }
حال کلاس ProductService را جهت تعامل با کلاس سرویس موجود در Domain Model و به منظور برگرداندن لیستی از محصولات و تبدیل آن به لیستی از ProductViewModel، ایجاد مینماییم. کلاسی با نام ProductService را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:
public class ProductService { private Model.ProductService _productService; public ProductService(Model.ProductService ProductService) { _productService = ProductService; } public ProductListResponse GetAllProductsFor( ProductListRequest productListRequest) { ProductListResponse productListResponse = new ProductListResponse(); try { IList<Model.Product> productEntities = _productService.GetAllProductsFor(productListRequest.CustomerType); productListResponse.Products = productEntities.ConvertToProductListViewModel(); productListResponse.Success = true; } catch (Exception ex) { // Log the exception… productListResponse.Success = false; // Return a friendly error message productListResponse.Message = ex.Message; } return productListResponse; } }
کلاس Service تمامی خطاها را دریافت نموده و پس از مدیریت خطا، پیغامی مناسب را به کلاینت ارسال میکند. همچنین این لایه محل مناسبی برای Log کردن خطاها میباشد. در اینجا کد نویسی لایه سرویس به پایان رسید و در ادامه به کدنویسی Data Layer میپردازیم.
Data Layer
برای ذخیره سازی محصولات، یک بانک اطلاعاتی با نام Shop01 ایجاد کنید که شامل جدولی به نام Product با ساختار زیر باشد:
برای اینکه کدهای بانک اطلاعاتی را سریعتر تولید کنیم از روش Linq to SQL در Data Layer استفاده میکنم. برای این منظور یک Data Context برای Linq to SQL به این لایه اضافه میکنیم. بر روی پروژه SoCPatterns.Layered.Repository کلیک راست نمایید و گزینه Add > New Item را انتخاب کنید. در پنجره ظاهر شده و از سمت چپ گزینه Data و سپس از سمت راست گزینه Linq to SQL Classes را انتخاب نموده و نام آن را Shop.dbml تعیین نمایید.
از طریق پنجره Server Explorer به پایگاه داده مورد نظر متصل شوید و با عمل Drag & Drop جدول Product را به بخش Design کشیده و رها نمایید.
اگر به یاد داشته باشید، در لایه Model برای برقراری ارتباط با پایگاه داده از یک Interface به نام IProductRepository استفاده نمودیم. حال باید این Interface را پیاده سازی نماییم. کلاسی با نام ProductRepository را با کد زیر به پروژه SoCPatterns.Layered.Repository اضافه کنید:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using SoCPatterns.Layered.Model; namespace SoCPatterns.Layered.Repository { public class ProductRepository : IProductRepository { public IList<Model.Product> FindAll() { var products = from p in new ShopDataContext().Products select new Model.Product { Id = p.ProductId, Name = p.ProductName, Price = new Model.Price(p.Rrp, p.SellingPrice) }; return products.ToList(); } } }
در متد FindAll، با استفاده از دستورات Linq to SQL، لیست تمامی محصولات را برگرداندیم. کدنویسی لایهی Data هم به پایان رسید و در ادامه به کدنویسی لایهی Presentation و UI میپردازیم.
Presentation Layer
به منظور جداسازی منطق نمایش (Presentation) از رابط کاربری (User Interface)، از الگوی Model View Presenter یا همان MVP استفاده میکنیم که در مباحث بعدی با جزئیات بیشتری در مورد آن صحبت خواهم کرد. یک Interface با نام IProductListView را با کد زیر به پروژه SoCPatterns.Layered.Presentation اضافه کنید:
using SoCPatterns.Layered.Service; public interface IProductListView { void Display(IList<ProductViewModel> Products); Model.CustomerType CustomerType { get; } string ErrorMessage { set; } }
این Interface توسط Web Formهای ASP.NET و یا Win Formها باید پیاده سازی شوند. کار با Interfaceها موجب میشود تا تست Viewها به راحتی انجام شوند. کلاسی با نام ProductListPresenter را با کد زیر به پروژه SoCPatterns.Layered.Presentation اضافه کنید:
using SoCPatterns.Layered.Service; namespace SoCPatterns.Layered.Presentation { public class ProductListPresenter { private IProductListView _productListView; private Service.ProductService _productService; public ProductListPresenter(IProductListView ProductListView, Service.ProductService ProductService) { _productService = ProductService; _productListView = ProductListView; } public void Display() { ProductListRequest productListRequest = new ProductListRequest(); productListRequest.CustomerType = _productListView.CustomerType; ProductListResponse productResponse = _productService.GetAllProductsFor(productListRequest); if (productResponse.Success) { _productListView.Display(productResponse.Products); } else { _productListView.ErrorMessage = productResponse.Message; } } } }
کلاس Presenter وظیفهی واکشی داده ها، مدیریت رویدادها و بروزرسانی UI را دارد. در اینجا کدنویسی لایهی Presentation به پایان رسیده است. از مزایای وجود لایهی Presentation این است که تست نویسی مربوط به نمایش دادهها و تعامل بین کاربر و سیستم به سهولت انجام میشود بدون آنکه نگران دشواری Unit Test نویسی Web Formها باشید. حال میتوانید کد نویسی مربوط به UI را انجام دهید که در ادامه به کد نویسی در Win Forms و Web Forms خواهیم پرداخت.
در مقالهی قبل توانستیم یک سری
از مدلهای مربوط به وبلاگ را آماده کنیم. در ادامه به تکمیل آن و همچین
آغاز تهیهی مدلهای مربوط به اخبار و پیغام خصوصی میپردازیم.
همکاران این قسمت:
سلمان معروفی
مدل گزارش دهی
/// <summary> /// Repersents a Report template for every cms section /// </summary> public class Report { #region Ctor /// <summary> /// Create one instance for <see cref="Report"/> /// </summary> public Report() { ReportedOn = DateTime.Now; Id = SequentialGuidGenerator.NewSequentialGuid(); } #endregion #region Properties /// <summary> /// gets or sets identifier for Report /// </summary> public virtual Guid Id { get; set; } /// <summary> /// gets or sets reason of report /// </summary> public virtual string Reason { get; set; } /// <summary> /// gets or sets section that is reported /// </summary> public virtual ReportSection Section { get; set; } /// <summary> /// gets or sets sectionid that is reported /// </summary> public virtual long SectionId { get; set; } /// <summary> /// gets or sets type of report /// </summary> public virtual ReportType Type{ get; set; } /// <summary> /// gets or sets report's datetime /// </summary> public virtual DateTime ReportedOn { get; set; } /// <summary> /// indicate this report is read by admin /// </summary> public virtual bool IsRead { get; set; } #endregion #region NavigationProperties /// <summary> /// gets or sets id of user that is reporter /// </summary> public virtual long ReporterId { get; set; } /// <summary> /// gets or sets id of user that is reporter /// </summary> public virtual User Reporter { get; set; } #endregion } /// <summary> /// Represents Report Section /// </summary> public enum ReportSection { News, Poll, Announcement, ForumTopic, BlogComment, BlogPost, NewsComment, PollComment, AnnouncementComment, ForumPost, User, ... } /// <summary> /// Represents Type of Report /// </summary> public enum ReportType { Spam, Abuse, Advertising, ... }
قصد داریم در این سیستم به کاربران خاصی دسترسی گزارش دادن در بخشهای مختلف را بدهیم. این دسترسیها در بخش تنظیمات سیستم قابل تغییر خواهند بود (برای مثال براساس امتیاز ، براساس تعداد پست و ... ) . این امکان میتواند برای مدیریت سیستم مفید باشد.
برای سیستم گزارش دهی به مانند سیستم امتیاز دهی عمل خواهیم کرد. در کلاس Report، خصوصیت ReportSection از نوع دادهی شمارشی میباشد که در بالا تعریف آن نیز آماده است و مشخص کنندهی بخشهایی میباشد که لازم است امکان گزارش دهی داشته باشند. خصوصیت Type هم که از نوع شمارشی ReportType میباشد، مشخص کنندهی نوع گزارشی است که داده شده است.
علاوه بر نوع گزارش، میتوان دلیل گزارش را هم ذخیره کرد که برای این منظور خصوصیت Reason در نظر گرفته شدهاست. خصوصیت IsRead هم برای مدیریت این گزارشات در پنل مدیریت در نظر گرفته شده است. اگر در مقالهی قبل دقت کرده باشید، متوجه وجود خصوصیتی به نام ReportsCount در کلاس BaseContent و BaseComment خواهید شد که برای نشان دادن تعداد گزارشهایی است که برای آن مطلب یا نظر داده شده است، استفاده میشود.
کلاس پایه فایلهای ضمیمه
/// <summary> /// Represents a base class for every attachment /// </summary> public abstract class BaseAttachment { #region Ctor public BaseAttachment() { Id = SequentialGuidGenerator.NewSequentialGuid(); AttachedOn = DateTime.Now; } #endregion #region Properties /// <summary> /// sets or gets identifier for attachment /// </summary> public virtual Guid Id { get; set; } /// <summary> /// sets or gets name for attachment /// </summary> public virtual string FileName { get; set; } /// <summary> /// sets or gets type of attachment /// </summary> public virtual string ContentType { get; set; } /// <summary> /// sets or gets size of attachment /// </summary> public virtual long Size { get; set; } /// <summary> /// sets or gets Extention of attachment /// </summary> public virtual string Extension { get; set; } /// <summary> /// sets or gets bytes of data /// </summary> //public byte[] Data { get; set; } /// <summary> /// sets or gets Creation Date /// </summary> public virtual DateTime AttachedOn { get; set; } /// <summary> /// gets or sets counts of download this file /// </summary> public virtual long DownloadsCount { get; set; } /// <summary> /// gets or sets datetime that is modified /// </summary> public virtual DateTime ModifiedOn { get; set; } /// <summary> /// gets or sets section that this file attached there /// </summary> public virtual AttachmentSection Section { get; set; } /// <summary> /// gets or sets information of user agent /// </summary> public virtual string Agent { get; set; } #endregion #region NavigationProperties /// <summary> /// sets or gets identifier of attachment's owner /// </summary> public virtual long OwnerId { get; set; } /// <summary> /// sets or gets identifier of attachment's owner /// </summary> public virtual User Owner { get; set; } #endregion } public enum AttachmentSection { News, Announcement, ForumTopic, Conversation, BlogComment, NewsComment, PollComment, AnnouncementComment, ForumPost, BlogPost, Group, ... }
کلاس بالا اکثر خصوصیات لازم برای مدل Attachment ما را در خود دارد. قصد داریم از ارث بری TPH برای مدیریت فایلهای ضمیمه استفاده کنیم. در سیستم بستهی ما، تنها کاربران احراز هویت شده میتوانند فایل ضمیمه کنند و برای همین منظور OwnerId را که همان ارسال کنندهی فایل میباشد، به صورت Nullable در نظر نگرفتهایم.
یک سری از مشخصات که نیاز به توضیح اضافی ندارند، ولی خصوصیت AttachmentSection که از نوع شمارشی AttachmentSection است، برای دسترسی راحت کاربر به فایلهای ارسالی خود در پنل کاربری در نظر گرفته شده است. برای بخشهای (وبلاگ - اخبار - نظرسنجیها - آگهیها - انجمن) که نیاز به Privacy خاصی نیست و احراز هویت کفایت میکند، مدل زیر را در نظر گرفته ایم:
مدل فایلهای ضمیمه عمومی
/// <summary> /// Repersent the attachment for file /// </summary> public class Attachment : BaseAttachment { }
/// <summary> /// Represents one news item /// </summary> public class NewsItem : BaseContent { #region Ctor /// <summary> /// create one instance of <see cref="NewsItem"/> /// </summary> public NewsItem() { Rating = new Rating(); PublishedOn = DateTime.Now; } #endregion #region Properties /// <summary> /// indicating that this news show on sidebar /// </summary> public virtual bool ShowOnSideBar { get; set; } /// <summary> /// indicate this NewsItem is approved by admin if NewsItem.Moderate==true /// </summary> public virtual bool IsApproved { get; set; } #endregion #region NavigationProperties /// <summary> /// gets or sets newsitem's Reviews /// </summary> public ICollection<NewsComment> Comments { get; set; } #endregion }
کلاس بالا نشان دهندهی اشتراکهای ما خواهند بود. این مدل ما هم از کلاس پایهی BaseContent بحث شده در مقالهی قبل، ارث بری کرده و علاوه بر آن دو خصوصیت دیگر تحت عنوان IsApproved برای اعمال مدیریتی در نظر گرفته شده است (اگر در بخش تنظیمات سیستم اخبار، مدیریت تصمیم گرفته باشد تا اخبار جدید به اشتراک گذاشته شده با تأیید مدیریتی منتشر شوند) و خصوصیت ShowOnSideBar هم به عنوان یک تنظیم مدیریتی برای خبر خاصی در نظر گرفته شده که لازم است به صورت sticky در سایدبار نمایش داده شود.
برای اخبار نیز امکان ارسال نظر خواهیم داشت که برای این منظور لیستی از مدل زیر (NewsComment) در مدل بالا تعریف شده است .
مدل نظرات اخبار
public class NewsComment : BaseComment { #region Ctor public NewsComment() { Rating = new Rating(); CreatedOn = DateTime.Now; } #endregion #region NavigationProperties /// <summary> /// gets or sets body of blog NewsItem's comment /// </summary> public virtual long? ReplyId { get; set; } /// <summary> /// gets or sets body of blog NewsItem's comment /// </summary> public virtual NewsComment Reply { get; set; } /// <summary> /// gets or sets body of blog NewsItem's comment /// </summary> public virtual ICollection<NewsComment> Children { get; set; } /// <summary> /// gets or sets NewsItem that this comment sent to it /// </summary> public virtual NewsItem NewsItem { get; set; } /// <summary> /// gets or sets NewsItem'Id that this comment sent to it /// </summary> public virtual long NewsItemId { get; set; } #endregion }
/// <summary> /// Indicate one conversation /// </summary> public class Conversation { #region Ctor /// <summary> /// create one instance of <see cref="Conversation"/> /// </summary> public Conversation() { Id = SequentialGuidGenerator.NewSequentialGuid(); SentOn = DateTime.Now; } #endregion #region Properties /// <summary> /// gets or sets identifier of record /// </summary> public virtual Guid Id { get; set; } /// <summary> /// represents this conversaion is seen /// </summary> public virtual bool IsRead { get; set; } /// <summary> /// gets or sets subject of this conversation /// </summary> public virtual string Subject { get; set; } /// <summary> /// gets or sets Date that this record added /// </summary> public virtual DateTime SentOn { get; set; } /// <summary> /// indicate this record deleted by sender /// </summary> public virtual bool DeletedBySender { get; set; } /// <summary> /// indicate this record deleted by receiver /// </summary> public virtual bool DeletedByReceiver { get; set; } /// <summary> /// gets or sets Messagescount that Unread by sender of this conversation /// </summary> public virtual int UnReadSenderMessagesCount { get; set; } /// <summary> /// gets or sets Messagescount that Unread by receiver of this conversation /// </summary> public virtual int UnReadReceiverMessagesCount { get; set; } /// <summary> /// gets or sets Messagescount of this conversation for increase performance /// </summary> public virtual int MessagesCount { get; set; } #endregion #region NavigationProperties /// <summary> /// gets or sets if of user that start this conversation /// </summary> public virtual long SenderId { get; set; } /// <summary> /// gets or sets user that start this conversation /// </summary> public virtual User Sender { get; set; } /// <summary> /// gets or sets id of user that is recipient /// </summary> public virtual long ReceiverId { get; set; } /// <summary> /// gets or sets user that is recipient /// </summary> public virtual User Receiver { get; set; } /// <summary> /// get or set Messages of this conversation /// </summary> public virtual ICollection<ConversationReply> Messages { get; set; } /// <summary> /// get or set Attachments that attached in this conversation /// </summary> public virtual ICollection<ConversationAttachment> Attachments { get; set; } #endregion
مدل بالا نشان دهندهی گفتگوی بین دو کاربر میباشد. هر گفتگو امکان دارد با موضوع خاصی ایجاد شود و مسلما یک کاربر بهعنوان دریافت کننده و کاربر دیگری بعنوان ارسال کننده خواهد بود. برای این منظور خصوصیات Receiver و Sender که از نوع User هستند را در این کلاس در نظر گرفتهایم.
خصوصیات DeletedBySender و DeletedByReceiver هم برای این در نظر گفته شدهاند که اگر یک طرف این گفتگو خواهان حذف آن باشد، برای آن کاربر حذف نرم انجام دهیم و فعلا برای کاربر مقابل قابل دسترسی باشد.
UnReadSenderMessagesCount و UnReadReceiverMessagesCount هم برای بالا بردن کارآیی سیستم در نظر گفته شدهاند و در واقع تعداد پیغامهای خوانده نشده در یک گفتگو به صورت متمایز برای هر دو طرف، ذخیره میشود. هر گفتگو شامل یکسری پیغام رد و بدل شده خواهد بود که بدین منظور لیستی از ConversationReplyها را در مدل بالا تعریف کردهایم.
در هر گفتگو یکسری فایل هم ممکن است ضمیمه شود ، برای این منظور هم یک لیستی از کلاس ConversationAttachment در مدل گفتگو تعریف شده است که در ادامه پیاده سازی کلاس ConversationAttachment را هم خواهیم دید.
مدل ConversationReply به شکل زیر میباشد:
/// <summary> /// Represents One Reply to Conversation /// </summary> public class ConversationReply { #region Ctor /// <summary> /// create one instance of <see cref="ConversationReply"/> /// </summary> public ConversationReply() { Id = SequentialGuidGenerator.NewSequentialGuid(); SentOn = DateTime.Now; } #endregion #region Properties /// <summary> /// gets or sets identifier of record /// </summary> public virtual Guid Id { get; set; } /// <summary> /// represents this conversaionReply is seen /// </summary> public virtual bool IsRead { get; set; } /// <summary> /// gets or sets body of this conversationReply /// </summary> public virtual string Body { get; set; } /// <summary> /// gets or sets Date that this record added /// </summary> public virtual DateTime SentOn { get; set; } #endregion #region NavigationProperties /// <summary> /// gets or sets Parent's Id Of this ConversationReply /// </summary> public virtual Guid? ParentId { get; set; } /// <summary> /// gets or sets Parent Of this ConversationReply /// </summary> public virtual ConversationReply Parent { get; set; } /// <summary> /// get or set Children Of this ConversationReply /// </summary> public virtual ICollection<ConversationReply> Children { get; set; } /// <summary> /// gets or sets if of user that start this conversationReply /// </summary> public virtual long SenderId { get; set; } /// <summary> /// gets or sets user that start this conversationReply /// </summary> public virtual User Sender { get; set; } /// <summary> /// gets or sets Conversation that this message sent in it /// </summary> public virtual Conversation Conversation{ get; set; } /// <summary> /// gets or sets Id of Conversation that this message sent in it /// </summary> public virtual Guid ConversationId { get; set; } #endregion }
مدل بالا نشان دهندهی پیغامهای داده شده در یک گفتگو با موضوعی خاص میباشد. ساختار درختی آن هم برای ایجاد امکان جواب دهی برای پیغامها در نظر گرفته شده است (الزامی نیست). هر پیغام در یک گفتگو ارسال شده و یک ارسال کننده نیز دارد که برای این منظور به ترتیب دو خصوصیت Conversation از نوع کلاس Conversation و Sender از نوع User در نظر گرفتهایم.
با توجه به وجود Privacy در گفتگو نیاز است تا مدل فایل ضمیمه بخش گفتگوها به شکل زیر باشد:
/// <summary> /// Represents the attachment That attached in Conversation /// </summary> public class ConversationAttachment : BaseAttachment { #region NavigationProperties public virtual Conversation Conversation { get; set; } public virtual Guid? ConversationId { get; set; } #endregion }
همانطور که کمی بالاتر بحث شد، قصد اعمال ارث بری TPH را برای مدیریت فایلهای ضمیمه داریم. برای این منظور مدل بالا نیز از کلاس BaseAttachment ارث بری کرده و دو خصوصیت اضافه هم برای اعمال ارتباط یک به چند با گفتگو خواهد داشت. توجه کنید که ConversationId به صورت Nullable تعریف شدهاست.
نتیجه این قسمت