public StoreDbContext() : base("name=StoreDb") { }
[MaxLength(2, ErrorMessage = "طول فیلد بیش از حد مجاز است")]
ASP.NET MVC #21
آشنایی با الگوی MVP
EF Code First #12
من چند روز پیش مطلبی رو راجع به ساخت attribute برای compare validation در روش code first عنوان کردم(البته در asp.net web form).که در واقع اصلیترین کاربردش همون کاربرد معروف مقایسه رمز عبور و تکرار رمز عبور هست. یه سری کارایی در این زمینه انجام دادم و خواستم در مورد مشکل مربوطه و در کل ، صحیح یا غلط بودن این روش کمکم کنید.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = true, Inherited = true)] public class CompareAttribute : ValidationAttribute { public CompareAttribute(string originalProperty, string confirmProperty) { OriginalProperty = originalProperty; ConfirmProperty = confirmProperty; } public string ConfirmProperty { get; private set; } public string OriginalProperty { get; private set; } public override bool IsValid(object value) { PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(value); return String.Equals(((User)value).Password, ((User)value).RepeatPassword); } }
//[CompareAttribute("Password", "RepeatPassword", ErrorMessage = "Not Compare!")] public class User { public Int64 UserId { get; set; }
[Required(ErrorMessageResourceName = "Password", ErrorMessageResourceType = typeof(ErrorMessageResource))] public String Password { get; set; }
[NotMapped] [Required(ErrorMessageResourceName = "RepeatPassword", ErrorMessageResourceType = typeof(ErrorMessageResource))] //[CompareAttribute("Password","RepeatPassword" , ErrorMessage = "Not Compare!")] public String RepeatPassword { get; set; } }
وقتی اونو بالای کلاس User میزارم کار میکنه.ولی خب این یه کار خیلی زشتیه! چون به ازای همه propertyها اجرا میشه.ولی وقتی اونو تو جای اصلیش که همون بالای repeat password هست میزارم کار نمیکنه.
یه جورایی مشکل از اینه که نمیشه انگار مقدار property رو به این سمت پاس داد.ولی در حالتی که بالای کلاس میزارمش چون خود کلاس رو پاس میده ، طبیعتاٌ
اسم propertyها رو هم پیدا میکنه.
ممنون میشم راهنماییم کنید .
بررسی اجزای اولین آزمایش با Postman
آزمونهای Postman، عموما یک سری Assertion هستند؛ به این معنا که در آنها، مقداری از Response دریافتی از سرور را با مقداری مشخص و مورد انتظار، مقایسه میکنیم:
pm.test("Status code is 200", function () { pm.response.to.have.status(200); });
چند نکته:
- آزمونهای Postman، با متد pm.test شروع میشوند. هدف از آن، نوشتن بدنهی یک آزمون متشکل از چندین Assertion است. اولین پارامتر آن، نام رشتهای آزمون است. پارامتر دوم آن، یک callback function است که پس از پایان درخواست جاری، اجرا میشود.
- روش اجرای آزمونها در اینجا non-blocking است. یعنی آزمونهای نوشته شده، به موازات هم اجرا شده و نتایج و حتی خطاهای آنها بر روی یکدیگر تاثیرگذار نیستند.
- برای یک Response و اجزای مختلف آن، میتوان چندین آزمون را نوشت و هر آزمون میتواند چندین Assertion را داشته باشد.
- در Postman، آزمونها تنها پس از پایان اجرای درخواستها، اجرا میشوند.
- به شیء pm.response در اینجا، response assertion API میگویند. توسط آن میتوان به اجزای مختلف response مانند status code، هدرها و یا بدنهی بازگشتی از سمت سرور، دسترسی یافت و برای آنها یک Assertion را نوشت.
لیستی از بررسیهای متداول، در حین نوشتن آزمونهای Postman
تا اینجا روش بررسی status code را در حین نوشتن آزمونهای Postman بررسی کردیم. در جدول زیر، مهمترین حالاتی را که جهت بررسی خروجی یک API میتوان مدنظر داشت، برشمرده شدهاند:
نوع بررسی | Response assertions |
بررسی مقدار status code دریافتی از سرور با مقدار مورد انتظار | pm.response.to.have.status(200); |
آیا status code دریافتی، معادل یکی از مقادیر مشخص شدهاست؟ | pm.expect(pm.response.code).to.be.oneOf([201,202]); |
آیا status name دریافتی از سرور، معادل عبارت مشخصی است؟ | response.to.have.status("Created"); |
مقایسهی مقدار responseTime با مقدار مورد انتظار | pm.expect(pm.response.responseTime).to.be.below(200); |
آیا هدر تنظیم شدهی توسط response، دارای کلید مورد انتظار است؟ | pm.response.to.have.header("X-Cache"); |
آیا هدر تنظیم شدهی توسط response، دارای کلید و مقدار مشخصی است؟ | pm.response.to.have.header("X-Cache", "HIT"); |
آیا نام یکی از کوکیهای تنظیم شدهی توسط response، معادل مقدار مورد انتظار است؟ | pm.expect(pm.cookies.has("sessionId")).to.be.true; |
آیا نام و مقدار یکی از کوکیهای تنظیم شدهی توسط response، معادل مقادیر مشخصی هستند؟ | pm.expect(pm.cookies.get("sessionId")).to.equal("abcb9s"); |
آیا بدنهی response، دقیقا معادل مقدار مشخص است؟ | pm.response.to.have.body("OK"); |
آیا بدنهی response، حاوی مقدار مشخصی است؟ | pm.expect(pm.response.text()).to.include("Order placed."); |
یک نکته: در رابط کاربری Postman، زمانیکه برگهی Tests را انتخاب میکنیم، کنار آن لیستی از code snippets نیز قرار دارند که با کلیک بر روی آنها، میتوان حالت عمومی اکثر موارد فوق را به صورت خودکار تولید کرد:
روش بررسی اجزای خروجی با فرمت JSON از سرور
فرض کنید API شما یک چنین خروجی JSON ای را بازگشت میدهد:
{ "id": 12, "name": "DNT", "isDeleted": false, "prefs": { "comments": "members", "voting": "disabled" } }
pm.test("Your test name", function () { var jsonData = pm.response.json(); pm.expect(jsonData.name).to.eql("DNT"); pm.expect(jsonData.prefs.voting).to.eql("disabled"); pm.expect(jsonData.isDeleted).to.eql(false); });
ساماندهی بهتر آزمونهای نوشته شده
البته میتوان تمام Response assertions مدنظر را داخل یک callback function نیز قرار داد، اما بهتر است هر کدام را و یا گروهی از آنها را که به هم مرتبط هستند، توسط یک pm.test جدید تعریف کرد تا بتوان به ساماندهی بهتر رسید و همچنین زمانیکه این آزمونها بررسی میشوند، گزارش بهتری را نیز مشاهده نمود. به همین جهت برای نمونه میتوان آزمایش فوق را به دو آزمایش مجزا تبدیل کرد که در یکی ایجاد مطلب جدید و در دیگری، ویژگیهای آن مطلب بررسی شدهاند:
pm.test("Post should be created", function () { var jsonData = pm.response.json(); pm.expect(jsonData.name).to.eql("DNT"); pm.expect(jsonData.isDeleted).to.eql(false); }); pm.test("Post's voting feature should be disabled", function () { var jsonData = pm.response.json(); pm.expect(jsonData.prefs.voting).to.eql("disabled"); });
ساده سازی و همچنین بهبود کارآیی آزمونهای نوشته شده
چون در اینجا چندینبار از ()var jsonData = pm.response.json داخل هر آزمایش استفاده شدهاست و در عمل یک شیء response نیز بیشتر وجود ندارد، میتوان جهت کاهش این تکرار و بهبود کارآیی آزمونهای نوشته شده، آنرا به صورت یک ثابت، به پیش از تمام آزمایشها منتقل کرد:
const jsonData = pm.response.json(); pm.test("Post should be created", function () { pm.expect(jsonData.name).to.eql("DNT"); pm.expect(jsonData.isDeleted).to.eql(false); }); pm.test("Post's voting feature should be disabled", function () { pm.expect(jsonData.prefs.voting).to.eql("disabled"); });
- Grid Panel
- Stack Panel
- Dock Panel
- Wrap Panel
- Canvas Panel
StackPanel
<StackPanel> <TextBlock Margin="10" FontSize="20">How do you like your coffee?</TextBlock> <Button Margin="10">Black</Button> <Button Margin="10">With milk</Button> <Button Margin="10">Latte machiato</Button> <Button Margin="10">Chappuchino</Button> </StackPanel>
نکتهی مهم اینکه میتوانید در اینجا از یک nested layout هم استفاده کنید بدین صورت که یک layout را داخل یک layout دیگر قرار دهید. کد زیر ترکیب دو stack panel را به صورت افقی و عمودی به ما نشان میدهد:
<StackPanel Orientation="Vertical"> <!-- Vertical is the default --> <Label Background="Red">Red 1</Label> <Label Background="LightGreen">Green 1</Label> <StackPanel Orientation="Horizontal"> <Label Background="Red">Red 2</Label> <Label Background="LightGreen">Green 2</Label> <Label Background="LightBlue">Blue 2</Label> <Label Background="Yellow">Yellow 2</Label> <Label Background="Orange">Orange 2</Label> </StackPanel> <Label Background="LightBlue">Blue 1</Label> <Label Background="Yellow">Yellow 1</Label> <Label Background="Orange">Orange 1</Label> </StackPanel>
Dock Panel
احتمالا به خاطر نامش، نحوه کارش را حدس زده اید. این پنل، اشیاء موجود را در 4 جهت و مرکز میچسباند. مشخص نمودن جهت چسبیده شدن هر کنترل توسط خاصیت DockPanel.Dock صورت میگیرد و مقدار Left، مقدار پیش فرض است. در صورتی که بخواهید المانی را در مرکز بچسبانید باید آن را به عنوان آخرین المان معرفی کرده و در Dock Panel مقدار خاصیت LastChildFill را با True برابر کنید.
<DockPanel LastChildFill="True"> <Button Content="Dock=Top" DockPanel.Dock="Top"/> <Button Content="Dock=Bottom" DockPanel.Dock="Bottom"/> <Button Content="Dock=Left"/> <Button Content="Dock=Right" DockPanel.Dock="Right"/> <Button Content="LastChildFill=True"/> </DockPanel>
به نحوهی تعریف خاصیت DockPanel.Dock دقت کنید به این نوع خاصیتها، Attached Dependency Property (شاید در فارسی بتوانیم خاصیتهای وابستگی متصل صدا بزنیم) میگویند. این خاصیتها نوع خاصی از خاصیتهای وابستگی هستند که به شما اجازه میدهند مقداری را به شیءایی نسبت دهید که آن شیء چیزی در مورد آن نمیداند. بهترین مثال در مورد این ویژگی، پنلها هستند که یکی از موارد استفادهی از آن را در بالا میبینید. هر پنل میتواند تا بی نهایت المان فرزند داشته باشد که هر المان باید خواصش توسط پنل مشخص گردد. ولی اگر پنل ما تعداد زیادی فرزند داشته باشد، نوشتن خواص هر کدام از فرزندها داخل تگ پنل، کاری غیر ممکن است. اینجاست که این نوع خاصیتها خودشان را نشان میدهند. پس به این نحو مقادیر، داخل کنترل هر تگ تعریف میشود ولی توسط پنل مورد استفاده قرار میگیرد. نحوهی نوشتن این نوع خاصیت: ابتدا یک پیشوند از نوع تگ پنل را در ابتدا آورده و سپس بعد از .(نقطه) نام خاصیت را ذکر میکنیم.
نحوهی تعریف این نوع خاصیتها در یک کلاس به صورت زیر است که برای شیء یا پنل canvas میباشد:
public static readonly DependencyProperty TopProperty = DependencyProperty.RegisterAttached("Top", typeof(double), typeof(Canvas), new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.Inherits)); public static void SetTop(UIElement element, double value) { element.SetValue(TopProperty, value); } public static double GetTop(UIElement element) { return (double)element.GetValue(TopProperty); }
<Button Content="Dock=Top" DockPanel.Dock="Top"/> <Button Content="Dock=Bottom" DockPanel.Dock="Bottom"/> <Button Content="Dock=Left"/> <Button Content="Dock=Left2"/> <Button Content="Right2" DockPanel.Dock="Right"/> <Button Content="Dock=Right" DockPanel.Dock="Right"/> <Button Content="LastChildFill=True"/>
Wrap Panel
این پنل بسیار شبیه StackPanel هست ولی مثل آن اشیاء را در یک سطر یا ستون ادامه نمیدهد؛ بلکه با رسیدن به انتهای پنجره، سطر یا ستون جدیدی را آغاز میکند. در stack panel با پایان پنجره، ادامه اشیا آن قابل مشاهده نبود ولی در این شیء با اتمام و رسیدن به لبهی پنجره، اشیاء در سر جدید (افقی) یا ستون جدید (عمودی) نمایش داده میشوند. این پنلها میتوانند در ساخت تبها و نوار ابزار استفاده شوند.
Canvas Panel
پایهایترین layout موجود در WPF است. موقعیت قرارگیری المانهای فرزندش بر اساس نقاط تعیین شده است.این پنل بیشتر برای رسم اشکال و گرافیک دو بعدی مناسب است و اصلا برای قرارگیری کنترلهای WPF روی آن توصیه نمیشود و مشکل winformها در آن رخ خواهد داد.
شروع ترسیم یک شکل دو بعدی روی آن بر اساس دوتا از چهار "خاصیتهای وابستگی متصل" صورت میگیرد که به شرح زیر هستند:
- Canvas.LEFT
- Canvas.RIGHT
- Canvas.TOP
- Canvas.BOTTOM
نمونه از کد نوشته شده آن به صورت زیر است:
<Canvas> <Rectangle Canvas.Left="40" Canvas.Top="31" Width="63" Height="41" Fill="Blue" /> <Ellipse Canvas.Left="130" Canvas.Top="79" Width="58" Height="58" Fill="Blue" /> <Path Canvas.Left="61" Canvas.Top="28" Width="133" Height="98" Fill="Blue" Stretch="Fill" Data="M61,125 L193,28"/> </Canvas>
ترتیب قرارگیری اشکال روی هم در canvas به ترتیبی انجام میگیرد که در XAML نوشته اید ولی میتوان با استفاده از خاصیت Canvas.ZIndex این ترتیب را تغییر داد.
<Canvas> <Ellipse Fill="Green" Width="60" Height="60" Canvas.Left="30" Canvas.Top="20" Canvas.ZIndex="1"/> <Ellipse Fill="Blue" Width="60" Height="60" Canvas.Left="60" Canvas.Top="40"/> </Canvas>
ViewBox
نمونهی کد زیر را تست کنید تا تفاوت بین دو Button را ببینید:
<StackPanel Orientation="Vertical"> <Button Content="Test" /> <Viewbox Stretch="Uniform"> <Button Content="Test" /> </Viewbox> </StackPanel>
در بخش دوم Layoutها مبحث گرید و ساخت Layout اختصاصی و تعدادی از خاصیتها را بررسی خواهیم کرد.
فرآیند داده کاوی در Microsoft SQL Server (بخش یک)
مقدمه
بطور کلی داده کاوی به دو قسمت زیر تقسیم میشود:
1- اهداف توصیفی (Descriptive Goal): بدنبال یافتن الگوها و روابط بین دادهها هستیم، بدین ترتیب مدلی برای توصیف بهتر دادهها بدست خواهد آمد.
2- اهداف پیش بینانه (Predictive Goal): بدنبال انجام پیش بینی با استفاده از الگوها و مدلهای فوق هستیم.
همچنین مراحل اجرای یک پروژه داده کاوی شامل مراحل زیر است:
1- تحلیل: مهمترین فعالیت در این فاز، فهم عمیق مسئله و شناخت درست مسئله و شناسائی مفاهیم کلیدی (Key Concept) در مسئله است.
2- طراحی: مهمترین فعالیت این فاز، فرموله کردن مسئله با استفاده از مفاهیم کلیدی است.
3- پیاده سازی/ نگهداری و بهبود
مراحل کاری داده کاوی بر اساس استاندارد CRISP-DM
محصول
مشترک شرکتهای SPSS, Teradata, NCR و دایملر- کرایسلر است و یک فرآیند
استاندارد Cross-Industry برای داده کاوی است که به طور گسترده ای استفاده
میشود. مراحل کاری در این مدل به شش فاز اصلی به شرح زیر تقسیم میشوند:
1. درک پروژه و فهم حوزه کاربرد (Business Understanding):
به طور صریح و آشکار اهداف و نیازمندیها مشخص میشود. ترجمه اهداف و
محدودیت آن در قاعده سازی، تعریف مسئله داده کاوی و مهیا کردن استراتژی
اولیه برای نائل شدن به اهداف در این مرحله تعریف می شود.
2. انتخاب دادهها (Data Understanding):
این مرحله شامل جمع آوری دادهها برای استفاده از تحلیل اکتشافی و مشخص
کردن اطلاعات اولیه برای ارزیابی دادههای با کیفیت و انتخاب دادههای
مفید و مورد نیاز میباشد.
3. آماده سازی دادهها (Data Preparation):
آماده کردن دادههای اولیه خام به دادههای نهایی، این دادها در کلیه
مراحل بعدی استفاده میشود و از این نظر این مرحله تحلیل و تلاش بیشتری را
میطلبد. انتخاب عناصر و شناسههای تحلیل شده را برای کاوش دادهها
اختصاص میدهیم و با تمیز کردن دادههای خام آن را برای ابزارهای مدل سازی
آماده می کنیم.
4. مدل سازی (Modeling):
با انتخاب و به کار بستن تکنیکهای مدل سازی مناسب و روش داده کاوی معین
نتایج مدل سازی را بهینه می کنیم، که در صورت نیاز میتوانیم با برگشت به
عقب تحلیل مدل سازی را بهینهتر نماییم.
5. ارزیابی (Evaluation):
مشخص کردن اینکه آیا مدل انتخابی، ما را به اهدافمان که در اولین مرحله
تعیین کردیم، می رساند. اتخاذ تصمیم راجع به استفاده از نتایج داده کاوی
برای اعتبارسنجی نیز در این مرحله انجام می شود.
6. استقرار (Deployment):
استفاده کردن از مدل ایجاد شده، برای مثال میتواند تولید یک گزارش ساده از
خروجیها را نام برد، و برای یک مثال پیچیده تکمیل کردن پردازش داده کاوی
موازی در سایر حوزهها میباشد، که این الگوها به یک دانش مفید و قابل
استفاده تبدیل میشوند و پس از بهبود آنها، الگوهایی که کارا محسوب می
شوند در یک سیستم اجرایی به کار گرفته خواهند شد.
مراحل کاری داده کاوی در بستر تکنولوژی Microsoft
داده کاوی غالباً به عنوان فرآیند استخراج اطلاعات، الگوها و روندهای موجود در مجموعه ی عظیمی از دادهها یاد می شود. این الگوها و روندها را می توان به عنوان یک مدل کاوشی تعریف نمود. به بیانی دیگر ایجاد یک مدل کاوشی بخشی از فرآیند بزرگتری است که در برگیرنده ی همه مراحل؛ از تعریف مسئله که مدل حل خواهد نمود تا اجرای مدل در محیطهای کاری است. می توان این فرآیند را با استفاده از 6 مرحله اساسی زیر تعریف نمود:
باید در نظر داشت که تهیه یک مدل داده کاوی، فرآیندی چرخشی، پویا و تکرار پذیر می باشد و ممکن است هر یک از این مراحل آن قدر تکرار شود، تا مدل مناسبی تهیه گردد.
- تعریف مسئله (Defining the Problem):
تعریف روشنی از مشکل و مسئله کسب و کار است. این مرحله شامل تجزیه و تحلیل نیازمندیهای کسب وکار، تعریف دامنه مشکل، تعریف معیارهایی که با آن مدلها ارزیابی خواهد شد و تعریف هدف نهایی پروژه ی داده کاوی است.
- آماده سازی دادهها (Preparing Data):
یکپارچه سازی و پالایش داده هایی است که در مرحله ی تعریف مسئله فرآیند معین شده است. SSIS حاوی تمامی ابزارهای ملزوم برای تکمیل این مرحله میباشد.
- بررسی دادهها (Exploring Data):
به منظور تصمیم گیریهای مناسب در هنگام تهیه مدل، می بایست دادهها را درک نمود و پس از آن می توان تصمیم گیری در مورد وجود دادههای مخدوش در مجموعه داده و در نهایت استراتژی مناسب برای رفع این مشکلات اتخاذ نمود. Data Source view Designer موجود در BIDS حاوی ابزارهای جامعی برای بررسی و شناخت دادهها شامل محاسبه ارقام حداقل و حداکثر، محاسبه میانگین و انحراف معیار و بررسی توزیع دادهها می باشد.
- تهیه مدل ها (Building Models):
پیش از تهیه مدل باید، دادهها را به دو دسته ی دادههای آموزشی و اعتبارسنجی (آزمایشی) تقسیم نمود. از دادههای آموزشی برای تهیه مدل و از دادههای اعتبارسنجی برای آزمایش صحت مدل با ایجاد سوالاتی در مورد صحت پیش بینیها استفاده نمود. پس از تعریف ساختار کاوشی، می بایست به پردازش مدل پرداخته شود و ساختارهای خالی با الگوهایی که مدل را توصیف می نمایند، پُر شوند. این مرحله با عنوان آموزش مدل شناخته می شود.
- بررسی و ارزیابی مدلها (Exploring and Validating Models):
این مرحله شامل بررسی مدلهای ایجاد شده به منظور آزمودن کارایی آنهاست. می توان مدلها را با ابزارهای موجود در Designer از جمله نمودار صعود و یا ماتریس دسته بندی بررسی نمود.
- اجرا و بروزرسانی مدلها (Deploying and Updating Models):
این مرحله شامل اجرای مدل هایی است که بهترین کارائی را در یک محیط عملیاتی داشته اند. پس از استقرار مدلهای کاوشی در یک محیط عملیاتی می توان از این مدلها برای پیش بینی هایی بهره گرفت.
مراحل سه گانه موجود در ساخت یک مدل کاوش
- ایجاد ساختار کاوشی (Mining Structures): تعریف یک ساختار کاوشی شامل، تعیین تعداد ستونهای ورودی، تعداد ستونهای قابل پیش بینی و الگوریتم وابسته به آن میباشد. ساختار کاوشی یک ساختار داده ای است که محدوده ی داده هایی را که از روی آنها مدلهای کاوش ساخته می شود را تعریف می نماید.
- آموزش مدل (Model Training): یک مدل کاوشی، الگوریتمهای کاوش را به داده هایی که ساختار کاوش ارائه می نماید، اعمال می کند. به بیان دیگر استفاده و کاربرد هر ستون و الگوریتمی که برای ساخت مدل استفاده می شود را تعریف می کند، پس شامل داده منبع اصلی نیست، بلکه شامل اطلاعاتی است که توسط الگوریتم کشف می شود. به آموزش مدل، پردازش مدل نیز گفته میشود و زمانی که یک مدل پردازش می شود داده هایی که توسط ساختار کاوش تعریف شده اند، از طریق الگوریتمهای داده کاوی انتخابی منتقل می شوند، الگوریتم؛ الگوها و روندها را جستجو می کند و در ادامه این اطلاعات در مدل ذخیره می شوند. از این رو پس از یادگیری و آموزش مدل، الگوهای بدست آمده در مدل کاوش ذخیره می شوند.
- پیش بینی مدل (Prediction): غالباً مهمترین مرحله و هدف نهایی در پروژههای داده کاوی است. پیش بینی به کشف اطلاعات ناشناخته با استفاده از الگوهای یافته شده از سوابق دادهها اشاره دارد. در پیش بینی به یک مدل کاوشی آموزش دیده و یک مجموعه داده ی جدید نیاز است. و در طول پیش بینی موتور داده کاوی، قواعد بدست آمده در مرحله یادگیری را در مورد مجموعه داده ی جدید بکار می برد و نتایج پیش بینی را به هر Case ورودی تخصیص می دهد.
استفاده از Ember Data با Local Storage
برای کار با HTML 5 local storage نیاز به Ember Data Local Storage Adapter نیز هست که در قسمت اول این سری، آدرس دریافت آن معرفی شد. این فایلها نیز در پوشهی Scripts\Libs برنامه کپی خواهند شد.
در ادامه به فایل Scripts\App\store.js که در قسمت قبل جهت تعریف دو آرایه ثابت مطالب و نظرات اضافه شد، مراجعه کرده و محتوای فعلی آنرا با کدهای زیر جایگزین کنید:
Blogger.ApplicationSerializer = DS.LSSerializer.extend(); Blogger.ApplicationAdapter = DS.LSAdapter.extend();
در ادامه با توجه به حذف دو آرایهی posts و comments که پیشتر در فایل store.js تعریف شده بودند، نیاز است مدلهای متناظری را جهت تعریف خواص آنها، به برنامه اضافه کنیم. اینکار را با افزودن دو فایل جدید comment.js و post.js به پوشهی Scripts\Models انجام خواهیم داد.
محتوای فایل Scripts\Models\post.js :
Blogger.Post = DS.Model.extend({ title: DS.attr(), body: DS.attr() });
Blogger.Comment = DS.Model.extend({ text: DS.attr() });
<script src="Scripts/Models/post.js" type="text/javascript"></script> <script src="Scripts/Models/comment.js" type="text/javascript"></script>
برای تعاریف مدلها در Ember data مرسوم است که نام مدلها، اسامی جمع نباشند. سپس با ایجاد وهلهای از DS.Model.extend یک مدل ember data را تعریف خواهیم کرد. در این مدل، خواص هر شیء را مشخص کرده و مقدار آنها همیشه ()DS.attr خواهد بود. این نکته را در دو مدل Post و Comment مشاهده میکنید. اگر دقت کنید به هر دو مدل، خاصیت id اضافه نشدهاست. این خاصیت به صورت خودکار توسط Ember data تنظیم میشود.
اکنون نیاز است برنامه را جهت استفاده از این مدلهای جدید به روز کرد. برای این منظور فایل Scripts\Routes\posts.js را گشوده و مدل آنرا به نحو ذیل ویرایش کنید:
Blogger.PostsRoute = Ember.Route.extend({ //controllerName: 'posts', // مقدار پیش فرض است و نیازی به ذکر آن نیست //renderTemplare: function () { // this.render('posts'); // مقدار پیش فرض است و نیازی به ذکر آن نیست //}, model: function () { return this.store.find('post'); } });
به همین ترتیب فایل Scripts\Routes\recent-comments.js را نیز جهت استفاده از data store ویرایش خواهیم کرد:
Blogger.RecentCommentsRoute = Ember.Route.extend({ model: function () { return this.store.find('comment'); } });
Blogger.PostRoute = Ember.Route.extend({ model: function (params) { return this.store.find('post', params.post_id); } });
افزودن امکان ثبت یک مطلب جدید
تا اینجا اگر برنامه را اجرا کنید، برنامه بدون خطا بارگذاری خواهد شد اما فعلا رکوردی را برای نمایش ندارد. در ادامه، برنامه را جهت افزودن مطالب جدید توسعه خواهیم داد. برای اینکار ابتدا به فایل Scripts\App\router.js مراجعه کرده و سپس مسیریابی جدید new-post را تعریف خواهیم کرد:
Blogger.Router.map(function () { this.resource('posts', { path: '/' }); this.resource('about'); this.resource('contact', function () { this.resource('email'); this.resource('phone'); }); this.resource('recent-comments'); this.resource('post', { path: 'posts/:post_id' }); this.resource('new-post'); });
<h2>Ember.js blog</h2> <ul> {{#each post in arrangedContent}} <li>{{#link-to 'post' post.id}}{{post.title}}{{/link-to}}</li> {{/each}} </ul> <a href="#" class="btn btn-primary" {{action 'sortByTitle' }}>Sort by title</a> {{#link-to 'new-post' classNames="btn btn-success"}}New Post{{/link-to}}
برای تعریف عناصر نمایشی این مسیریابی، فایل جدید قالب Scripts\Templates\new-post.hbs را با محتوای زیر اضافه کنید:
<h1>New post</h1> <form> <div class="form-group"> <label for="title">Title</label> {{input value=title id="title" class="form-control"}} </div> <div class="form-group"> <label for="body">Body</label> {{textarea value=body id="body" class="form-control" rows="5"}} </div> <button class="btn btn-primary" {{action 'save'}}>Save</button> </form>
پس از آن نیاز است نام فایل قالب new-post را به template loader برنامه در فایل index.html اضافه کرد:
<script type="text/javascript"> EmberHandlebarsLoader.loadTemplates([ 'posts', 'about', 'application', 'contact', 'email', 'phone', 'recent-comments', 'post', 'new-post' ]); </script>
Blogger.NewPostController = Ember.Controller.extend({ actions: { save: function () { var newPost = this.store.createRecord('post', { title: this.get('title'), body: this.get('body') }); newPost.save(); this.transitionToRoute('posts'); } } });
<script src="Scripts/Controllers/new-post.js" type="text/javascript"></script>
در اینجا کنترلر جدید NewPostController را مشاهده میکنید. از این جهت که برای دسترسی به خواص مدل تغییر کرده، از متد this.get استفاده شدهاست، نیازی نیست حتما از یک ObjectController مانند قسمت قبل استفاده کرد و Controller معمولی نیز برای اینکار کافی است.
آرگومان اول this.store.createRecord نام مدل است و آرگومان دوم آن، وهلهای که قرار است به آن اضافه شود. همچنین باید دقت داشت که برای تنظیم یک خاصیت، از متد this.set و برای دریافت مقدار یک خاصیت تغییر کرده از this.get به همراه نام خاصیت مورد نظر استفاده میشود و نباید مستقیما برای مثال از this.title استفاده کرد.
this.store.createRecord صرفا یک شیء جدید (ember data object) را ایجاد میکند. برای ذخیره سازی نهایی آن باید متد save آنرا فراخوانی کرد (پیاده سازی الگوی active record است). به این ترتیب این شیء در local storage ذخیره خواهد شد.
پس از ذخیرهی مطلب جدید، از متد this.transitionToRoute استفاده شدهاست. این متد، برنامه را به صورت خودکار به صفحهی متناظر با مسیریابی posts هدایت میکند.
اکنون برنامه را اجرا کنید. بر روی دکمهی سبز رنگ new post در صفحهی اول کلیک کرده و یک مطلب جدید را تعریف کنید. بلافاصله عنوان و لینک متناظر با این مطلب را در صفحهی اول سایت مشاهده خواهید کرد.
همچنین اگر برنامه را مجددا بارگذاری کنید، این مطالب هنوز قابل مشاهده هستند؛ زیرا در local storage مرورگر ذخیره شدهاند.
در اینجا اگر به لینکهای تولید شده دقت کنید، id آنها عددی نیست. این روشی است که local storage با آن کار میکند.
افزودن امکان حذف یک مطلب به سایت
برای حذف یک مطلب، دکمهی حذف را به انتهای قالب Scripts\Templates\post.hbs اضافه خواهیم کرد:
<h2>{{title}}</h2> {{#if isEditing}} <form> <div class="form-group"> <label for="title">Title</label> {{input value=title id="title" class="form-control"}} </div> <div class="form-group"> <label for="body">Body</label> {{textarea value=body id="body" class="form-control" rows="5"}} </div> <button class="btn btn-primary" {{action 'save' }}>Save</button> </form> {{else}} <p>{{body}}</p> <button class="btn btn-primary" {{action 'edit' }}>Edit</button> <button class="btn btn-danger" {{action 'delete' }}>Delete</button> {{/if}}
سپس کنترلر Scripts\Controllers\post.js را جهت مدیریت اکشن جدید delete به نحو ذیل تکمیل میکنیم:
Blogger.PostController = Ember.ObjectController.extend({ isEditing: false, actions: { edit: function () { this.set('isEditing', true); }, save: function () { var post = this.get('model'); post.save(); this.set('isEditing', false); }, delete: function () { if (confirm('Do you want to delete this post?')) { this.get('model').destroyRecord(); this.transitionToRoute('posts'); } } } });
متد save نیز در اینجا بهبود یافتهاست. ابتدا مدل جاری دریافت شده و سپس متد save بر روی آن فراخوانی میشود. به این ترتیب اطلاعات از حافظه به local storage نیز منتقل خواهند شد.
ثبت و نمایش نظرات به همراه تنظیمات روابط اشیاء در Ember Data
در ادامه قصد داریم امکان افزودن نظرات را به مطالب، به همراه نمایش آنها در ذیل هر مطلب، پیاده سازی کنیم. برای اینکار نیاز است رابطهی بین یک مطلب و نظرات مرتبط با آنرا در مدل ember data مشخص کنیم. به همین جهت فایل Scripts\Models\post.js را گشوده و تغییرات ذیل را به آن اعمال کنید:
Blogger.Post = DS.Model.extend({ title: DS.attr(), body: DS.attr(), comments: DS.hasMany('comment', { async: true }) });
همچنین نیاز است یک سر دیگر رابطه را نیز مشخص کرد. برای این منظور فایل Scripts\Models\comment.js را گشوده و به نحو ذیل تکمیل کنید:
Blogger.Comment = DS.Model.extend({ text: DS.attr(), post: DS.belongsTo('post', { async: true }) });
در ادامه نیاز است بتوان تعدادی نظر را ثبت کرد. به همین جهت با تعریف مسیریابی آن شروع میکنیم. این مسیریابی تعریف شده در فایل Scripts\App\router.js نیز باید تو در تو باشد؛ زیرا قسمت ثبت نظر (new-comment) دقیقا داخل همان صفحهی نمایش یک مطلب ظاهر میشود:
Blogger.Router.map(function () { this.resource('posts', { path: '/' }); this.resource('about'); this.resource('contact', function () { this.resource('email'); this.resource('phone'); }); this.resource('recent-comments'); this.resource('post', { path: 'posts/:post_id' }, function () { this.resource('new-comment'); }); this.resource('new-post'); });
<h2>{{title}}</h2> {{#if isEditing}} <form> <div class="form-group"> <label for="title">Title</label> {{input value=title id="title" class="form-control"}} </div> <div class="form-group"> <label for="body">Body</label> {{textarea value=body id="body" class="form-control" rows="5"}} </div> <button class="btn btn-primary" {{action 'save' }}>Save</button> </form> {{else}} <p>{{body}}</p> <button class="btn btn-primary" {{action 'edit' }}>Edit</button> <button class="btn btn-danger" {{action 'delete' }}>Delete</button> {{/if}} <h2>Comments</h2> {{#each comment in comments}} <p> {{comment.text}} </p> {{/each}} <p>{{#link-to 'new-comment' this class="btn btn-success"}}New comment{{/link-to}}</p> {{outlet}}
در انتهای قالب نیز یک {{outlet}} اضافه شدهاست. کار آن نمایش قالب ارسال یک نظر جدید، پس از کلیک بر روی لینک New Comment میباشد. این قالب را با افزودن فایل Scripts\Templates\new-comment.hbs با محتوای ذیل ایجاد خواهیم کرد:
<h2>New comment</h2> <form> <div class="form-group"> <label for="text">Your thoughts:</label> {{textarea value=text id="text" class="form-control" rows="5"}} </div> <button class="btn btn-primary" {{action "save"}}>Add your comment</button> </form>
<script type="text/javascript"> EmberHandlebarsLoader.loadTemplates([ 'posts', 'about', 'application', 'contact', 'email', 'phone', 'recent-comments', 'post', 'new-post', 'new-comment' ]); </script>
Blogger.NewCommentController = Ember.ObjectController.extend({ needs: ['post'], actions: { save: function () { var comment = this.store.createRecord('comment', { text: this.get('text') }); comment.save(); var post = this.get('controllers.post.model'); post.get('comments').pushObject(comment); post.save(); this.transitionToRoute('post', post.id); } } });
<script src="Scripts/Controllers/new-comment.js" type="text/javascript"></script>
قسمت ذخیره سازی comment جدید با ذخیره سازی یک post جدید که پیشتر بررسی کردیم، تفاوتی ندارد. از متد this.store.createRecord جهت معرفی وهلهای جدید از comment استفاده و سپس متد save آن، برای ثبت نهایی فراخوانی شدهاست.
در ادامه باید این نظر جدید را به post متناظر با آن مرتبط کنیم. برای اینکار نیاز است تا به مدل کنترلر post دسترسی داشته باشیم. به همین جهت خاصیت needs را به تعاریف کنترلر جاری به همراه نام کنترلر مورد نیاز، اضافه کردهایم. به این ترتیب میتوان توسط متد this.get و پارامتر controllers.post.model در کنترلر NewComment به اطلاعات کنترلر post دسترسی یافت. سپس خاصیت comments شیء post جاری را یافته و مقدار آنرا به comment جدیدی که ثبت کردیم، تنظیم میکنیم. در ادامه با فراخوانی متد save، کار تنظیم ارتباطات یک مطلب و نظرهای جدید آن به پایان میرسد.
در آخر با فراخوانی متد transitionToRoute به مطلبی که نظر جدیدی برای آن ارسال شدهاست باز میگردیم.
همانطور که در این تصویر نیز مشاهده میکنید، اطلاعات ذخیره شده در local storage را توسط افزونهی Ember Inspector نیز میتوان مشاهده کرد.
افزودن دکمهی حذف به لیست نظرات ارسالی
برای افزودن دکمهی حذف، به قالب Scripts\Templates\post.hbs مراجعه کرده و قسمتی را که لیست نظرات را نمایش میدهد، به نحو ذیل تکمیل میکنیم:
{{#each comment in comments}} <p> {{comment.text}} <button class="btn btn-xs btn-danger" {{action 'delete' }}>delete</button> </p> {{/each}}
Blogger.CommentController = Ember.ObjectController.extend({ needs: ['post'], actions: { delete: function () { if (confirm('Do you want to delete this comment?')) { var comment = this.get('model'); comment.deleteRecord(); comment.save(); var post = this.get('controllers.post.model'); post.get('comments').removeObject(comment); post.save(); } } } });
<script src="Scripts/Controllers/comment.js" type="text/javascript"></script>
در این حالت اگر برنامه را اجرا کنید، پیام «Do you want to delete this post» را مشاهده خواهید کرد بجای پیام «Do you want to delete this comment». علت اینجا است که قالب post به صورت پیش فرض به کنترلر post متصل است و نه کنترلر comment. برای رفع این مشکل تنها کافی است از itemController به نحو ذیل استفاده کنیم:
{{#each comment in comments itemController="comment"}} <p> {{comment.text}} <button class="btn btn-xs btn-danger" {{action 'delete' }}>delete</button> </p> {{/each}}
در کنترلر Comment روش دیگری را برای حذف یک رکورد مشاهده میکنید. میتوان ابتدا متد deleteRecord را بر روی مدل فراخوانی کرد و سپس آنرا save نمود تا نهایی شود. همچنین در اینجا نیاز است نظر حذف شده را از سر دیگر رابطه نیز حذف کرد. روش دسترسی به post جاری در این حالت، همانند توضیحات NewCommentController است که پیشتر بحث شد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید:
EmberJS03_04.zip