تهیه یک بانک اطلاعاتی نمونه
برای نمایش امکانات کار با روش Database first، نیاز است یک بانک اطلاعاتی را به صورت مستقل و متداولی ایجاد کنیم. به همین جهت اسکریپت SQL ذیل را توسط Management studio اجرا کنید تا بانک اطلاعاتی BloggingCore2016، به همراه دو جدول به هم وابسته، در آن ایجاد شوند:
CREATE DATABASE [BloggingCore2016] GO USE [BloggingCore2016] GO CREATE TABLE [Blog] ( [BlogId] int NOT NULL IDENTITY, [Url] nvarchar(max) NOT NULL, CONSTRAINT [PK_Blog] PRIMARY KEY ([BlogId]) ); GO CREATE TABLE [Post] ( [PostId] int NOT NULL IDENTITY, [BlogId] int NOT NULL, [Content] nvarchar(max), [Title] nvarchar(max), CONSTRAINT [PK_Post] PRIMARY KEY ([PostId]), CONSTRAINT [FK_Post_Blog_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blog] ([BlogId]) ON DELETE CASCADE ); GO INSERT INTO [Blog] (Url) VALUES ('https://www.dntips.ir/'), ('http://blogs.msdn.com/dotnet'), ('http://blogs.msdn.com/webdev'), ('http://blogs.msdn.com/visualstudio') GO
پیشنیازهای مهندسی معکوس ساختار بانک اطلاعاتی در EF Core
در قسمت اول در حین بررسی «برپایی تنظیمات اولیهی EF Core 1.0 در یک برنامهی ASP.NET Core 1.0»، چهار مدخل جدید را به فایل project.json برنامه اضافه کردیم. مدخل جدید Microsoft.EntityFrameworkCore.Tools که به قسمت tools آن اضافه شد، پیشنیاز اصلی کار با EF Core Migrations است. همچنین وجود مدخل Microsoft.EntityFrameworkCore.SqlServer.Design برای تدارک امکانات مهندسی معکوس ساختار یک بانک اطلاعاتی SQL Server ضروری است.
تبدیل ساختار دیتابیس BloggingCore2016 به کدهای معادل EF Core آن
پس از فعال سازی ابزارهای خط فرمان EF Core، به پوشهی اصلی پروژه مراجعه کرده، کلید shift را نگه دارید. سپس کلیک راست کرده و گزینهی Open command window here را انتخاب کنید تا خط فرمان از این پوشه آغاز شود. در ادامه دستور ذیل را صادر کنید:
dotnet ef dbcontext scaffold "Data Source=(local);Initial Catalog=BloggingCore2016;Integrated Security = true" Microsoft.EntityFrameworkCore.SqlServer -o Entities --context MyDBDataContext --verbose
اجرا این دستور سبب اتصال به رشتهی اتصالی ذکر شده که به بانک اطلاعاتی BloggingCore2016 اشاره میکند، میشود. سپس پروایدر مدنظر ذکر شدهاست. سوئیچ o محل درج فایلهای نهایی را مشخص میکند. برای مثال در اینجا فایلهای نهایی مهندسی معکوس شده در پوشهی Entities درج میشوند (تصویر فوق). همچنین در اینجا امکان ذکر فایل context تولیدی نیز وجود دارد. اگر علاقمند باشید تا تمام ریز جزئیات این عملیات را نیز مشاهده کنید، میتوانید پارامتر اختیاری verbose را نیز به انتهای دستور اضافه نمائید.
بقیه مراحل کار با این فایلهای تولید شده، با نکاتی که تاکنون عنوان شدهاند یکی است. برای مثال اگر میخواهید رشتهی اتصالی پیش فرض را از این Context تولید شده خارج کنید:
public partial class MyDBDataContext : DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"Data Source=(local);Initial Catalog=BloggingCore2016;Integrated Security = true"); }
بررسی پارامترهای دیگر ابزار مهندسی معکوس به Code First
اگر دستور dotnet ef dbcontext scaffold --help را صادر کنیم، خروجی راهنمای ذیل را میتوان مشاهده کرد:
Usage: dotnet ef dbcontext scaffold [arguments] [options] Arguments: [connection] The connection string of the database [provider] The provider to use. For example, Microsoft.EntityFrameworkCore.SqlServer Options: -a|--data-annotations Use DataAnnotation attributes to configure the model where possible. If omitted, the output code will use only the fluent API. -c|--context <name> Name of the generated DbContext class. -f|--force Force scaffolding to overwrite existing files. Otherwise, the code will only proceed if no output files would be overwritten. -o|--output-dir <path> Directory of the project where the classes should be output. If omitted, the top-level project directory is used. --schema <schema> Selects a schema for which to generate classes. -t|--table <schema.table> Selects a table for which to generate classes. -e|--environment <environment> The environment to use. If omitted, "Development" is used. -h|--help Show help information -v|--verbose Enable verbose output
- حالت پیش فرض تنظیمات روابط مدلها در این روش، حالت استفاده از Fluent API است. اگر میخواهید آنرا به حالت استفادهی از Data Annotations تغییر دهید، پارامتر a- و یا data-annotations-- را در دستور نهایی ذکر کنید.
- حالت پیش فرض تولید فایلهای نهایی این روش، عدم بازنویسی فایلهای موجود است. اگر میخواهید پس از تغییر بانک اطلاعاتی، مجددا این فایلها را از صفر تولید کنید، پارامتر f- و یا force- را در دستور نهایی ذکر کنید.
بنابراین اگر میخواهید هربار فایلهای نهایی را بازنویسی کنید و همچنین روش کار با Data Annotations را ترجیح میدهید، دستور نهایی، شکل زیر را پیدا خواهد کرد:
dotnet ef dbcontext scaffold "Data Source=(local);Initial Catalog=BloggingCore2016;Integrated Security = true" Microsoft.EntityFrameworkCore.SqlServer -o Entities --context MyDBDataContext --verbose --force --data-annotations
کار با یک بانک اطلاعاتی موجود، با روش مهاجرتهای Code First
فرض کنید میخواهید از یک بانک اطلاعاتی از پیش موجود EF 6.x (یا هر بانک اطلاعاتی از پیش موجود دیگری)، به روش پیش فرض EF Core استفاده کنید. برای این منظور:
- ابتدا جدول migration history قدیمی آنرا حذف کنید؛ چون ساختار آن با EF Core یکی نیست.
- سپس با استفاده از دستور dotnet ef dbcontext scaffold فوق، معادل کلاسها، روابط و Context سازگار با EF Core آنرا تولید کنید.
- در ادامه رشتهی اتصالی پیش فرض آنرا از کلاس Context تولیدی خارج کرده و از یکی از روشهای مطرح شدهی در مطلب «شروع به کار با EF Core 1.0 - قسمت 1 - برپایی تنظیمات اولیه» استفاده کنید.
- سپس نیاز است این Context جدید را توسط متد services.AddDbContext به لیست سرویسهای برنامه اضافه کنید. این مورد نیز در قسمت اول بررسی شدهاست.
- مرحلهی بعد، افزودن جدول __EFMigrationsHistory جدید EF Core، به این بانک اطلاعاتی است. برای این منظور به روش متداول فعال کردن مهاجرتها، دستور ذیل را صادر کنید:
dotnet ef migrations add InitialDatabase
using Microsoft.EntityFrameworkCore.Migrations; namespace Core1RtmEmptyTest.DataLayer.Migrations { public partial class InitialDatabase : Migration { protected override void Up(MigrationBuilder migrationBuilder) { } protected override void Down(MigrationBuilder migrationBuilder) { } } }
سپس دستور به روز رسانی بانک اطلاعاتی را صادر کنید:
dotnet ef database update
پس از این مرحله، روش کار، Code first خواهد بود. برای مثال خاصیتی را به کلاسی اضافه میکنید و سپس دو دستور ذیل را صادر خواهید کرد که در آن v2 یک نام دلخواه است:
dotnet ef migrations add v2 dotnet ef database update
- برتری های شیرپوینت 2010 نسبت به شیرپوینت 2007 – لیست ها | عارف حکیمی | www.parnianportal.com
- پندهای جوئل اسپالسکی به دانشجویان کامپیوتر | (Afshar Mohebbi) | blog.afsharm.com
- سیر تکاملی Delegate ها - قسمت سوم، Lambda Expression | www.persiadevelopers.com
- مجموعه وبلاگ های یادگیری الکترونیکی در Google Reader | پرهام | www.elb.ir
- Introducing the Microsoft “Roslyn” CTP | Visual Studio Blog | blogs.msdn.com
- Introducing the Microsoft “Roslyn” CTP | CSharpFAQ | blogs.msdn.com
- Microsoft Announces Windows Management Framework 3.0 CTP #1 | Jeff Martin | www.infoq.com
- Mono 2.12 Roadmap | Jonathan Allen | www.infoq.com
- NHibernate Designer 2 | Phillip | www.philliphaydon.com
- Roslyn CTP | Kirill Osenkov - MSFT | blogs.msdn.com
- Roslyn CTP is available | Patrick Smacchia | codebetter.com
- Roslyn CTP Now Available | S.Somasegar | blogs.msdn.com
- Roslyn Syntax Visualizers | Visual Studio Blog | blogs.msdn.com
- The Roslyn Preview Is Now Available | Eric Lippert | blogs.msdn.com
- Windows Server 8 | Bill Laing, Manlio Vecchiet, Max Herrmann, Mike Neil | channel9.msdn.com
- آیا میدانید چطور میتوان تعداد اتصالها به اس کیوال سرور را محدود کرد؟ | beyondrelational.com
- بهینه سازیهای SIMD کامپایلر سی++ ویژوال استودیو 11 | blogs.microsoft.co.il
- ترجیح اوراکل بیشتر به جاوای نگارش سازمانی است تا پشتیبانی دسکتاپ | rss.slashdot.org
- مطابق قوانین کانادا لینک دادن اشکالی ندارد! | rss.slashdot.org
- نحوه استفاده از Stack Exchange API | channel9.msdn.com
- Create Database
- Delete Database
- Migrate Database
- Seed Database
services.AddParbad() .ConfigureDatabase(builder => { // Choose your database provider (SQL Server, MySql, Sqlite, etc.) builder.Use.... }) .ConfigureDatabaseInitializer(builder => { builder.UseInitializer(async context => { await context.Database.EnsureDeletedAsync(); // OR await context.Database.EnsureCreatedAsync(); // OR await context.Database.MigrateAsync(); }); });
services.AddParbad() .ConfigureDatabase(builder => { // Choose your Entity Framework provider (SQL Server, MySql, Sqlite, etc.) builder.Use.... }) .ConfigureDatabaseInitializer(builder => { builder.CreateDatabase(); // OR builder.DeleteAndCreateDatabase(); // OR builder.CreateAndMigrateDatabase(); });
services.AddParbad() .ConfigureDatabase(builder => { // SQL Server builder.UseSqlServer("Connection String", options => options.UseParbadMigrations()); }) .ConfigureDatabaseInitializer(builder => { builder.CreateAndMigrateDatabase(); });
builder.UseSqlServer("Connection String", options => options.MigrationsAssembly("Parbad"));
نمونه مثالها را همچنین میتوانید در صفحه GitHub پروژه مشاهده کنید.
متدی تحت عنوان ValidateEmail را تصور کنید. این متد از حیث بازگشت نتیجه به عنوان خروجی میتواند به اشکال مختلفی پیاده سازی شود که در ادامه مشاهده میکنیم:
متد ValidateEmail با خروجی Boolean
public bool ValidateEmail(string email) { var valid = true; if (string.IsNullOrWhiteSpace(email)) { valid = false; } var isValidFormat = true;//todo: using RegularExpression if (!isValidFormat) { valid = false; } var isRealDoamin = true;//todo: Code here that confirms whether domain exists. if (!isRealDoamin) { valid = false; } return valid; }
همانطور که در تکه کد زیر مشخص میباشد، استفاده کننده از متد بالا، امکان بررسی خروجی آن را در قالب یک شرط خواهد داشت و علاوه بر اینکه پیاده سازی آن ساده میباشد، خوانایی کد را نیز بالا میبرد؛ ولی با این حال نمیتوان متوجه شد مشکل اصلی آدرس ایمیل ارسالی به عنوان آرگومان، دقیقا چیست.
var email = "email@example.com"; var isValid = ValidateEmail(email); if(isValid) { //do something }
متد ValidateEmail با صدور استثناء
public void ValidateEmail(string email) { if (string.IsNullOrWhiteSpace(email)) throw new ArgumentNullException(nameof(email)); var isValidFormat = true;//todo: using RegularExpression if (!isValidFormat) throw new ArgumentException("email is not in a correct format"); var isRealDoamin = true;//todo: Code here that confirms whether domain exists. if (!isRealDoamin) throw new ArgumentException("email does not include a valid domain.") }
روش بالا هم جواب میدهد ولی بهتر است کلاس Exception سفارشی به عنوان مثال ValidationException برای این قضیه در نظر گرفته شود تا بتوان وهلههای صادر شده از این نوع را در لایههای بالاتر مدیریت کرد.
متد ValidateEmail با چندین خروجی
برای این منظور چندین راه حل پیش رو داریم.
با استفاده از پارامتر out:
public bool ValidateEmail(string email, out string message) { var valid = true; message = string.Empty; if (string.IsNullOrWhiteSpace(email)) { valid = false; message = "email is null."; } if (valid) { var isValidFormat = true;//todo: using RegularExpression if (!isValidFormat) { valid = false; message = "email is not in a correct format"; } } if (valid) { var isRealDoamin = true;//todo: Code here that confirms whether domain exists. if (!isRealDoamin) { valid = false; message = "email does not include a valid domain."; } } return valid; }
var email = "email@example.com"; var isValid = ValidateEmail(email, out string message); if (isValid) { //do something }
Tuple<bool, List<string>> result = Tuple.Create<bool, List<string>>(true, new List<string>());
public class OperationResult { public bool Success { get; set; } public IList<string> Messages { get; } = new List<string>(); public void AddMessage(string message) { Messages.Add(message); } }
public OperationResult ValidateEmail(string email) { var result = new OperationResult(); if (string.IsNullOrWhiteSpace(email)) { result.Success = false; result.AddMessage("email is null."); } if (result.Success) { var isValidFormat = true;//todo: using RegularExpression if (!isValidFormat) { result.Success = false; result.AddMessage("email is not in a correct format"); } } if (result.Success) { var isRealDoamin = true;//todo: Code here that confirms whether domain exists. if (!isRealDoamin) { result.Success = false; result.AddMessage("email does not include a valid domain."); } } return result; }
این بار خروجی متد مذکور از نوع OperationResult ای میباشد که هم موفقیت آمیز بودن یا عدم آن را مشخص میکند و همچنین امکان دسترسی به لیست پیغامهای مرتبط با اعتبارسنجیهای انجام شده، وجود دارد.
استفاده از Exception برای نمایش پیغام برای کاربر نهایی
با صدور یک استثناء و مدیریت سراسری آن در بالاترین (خارجی ترین) لایه و نمایش پیغام مرتبط با آن به کاربر نهایی، میتوان از آن به عنوان ابزاری برای ارسال هر نوع پیغامی به کاربر نهایی استفاده کرد. اگر قوانین تجاری با موفقیت برآورده نشدهاند یا لازم است به هر دلیلی یک پیغام مرتبط با یک اعتبارسنجی تجاری را برای کاربر نمایش دهید، این روش بسیار کارساز میباشد و با یکبار وقت گذاشتن برای توسعه زیرساخت برای این موضوع به عنوان یک Cross Cutting Concern تحت عنوان Exception Management آزادی عمل زیادی در ادامه توسعه سیستم خود خواهید داشت.
به عنوان مثال داشتن یک کلاس Exception سفارشی تحت عنوان UserFriendlyException در این راستا یک الزام میباشد.
[Serializable] public class UserFriendlyException : Exception { public string Details { get; private set; } public int Code { get; set; } public UserFriendlyException() { } public UserFriendlyException(SerializationInfo serializationInfo, StreamingContext context) : base(serializationInfo, context) { } public UserFriendlyException(string message) : base(message) { } public UserFriendlyException(int code, string message) : this(message) { Code = code; } public UserFriendlyException(string message, string details) : this(message) { Details = details; } public UserFriendlyException(int code, string message, string details) : this(message, details) { Code = code; } public UserFriendlyException(string message, Exception innerException) : base(message, innerException) { } public UserFriendlyException(string message, string details, Exception innerException) : this(message, innerException) { Details = details; } }
و همچنین لازم است در بالاترین لایه سیستم خود به عنوان مثال برای یک پروژه ASP.NET MVC یا ASP.NET Core MVC میتوان یک ExceptionFilter سفارشی نیز تهیه کرد که هم به صورت سراسری استثناءهای سفارشی شما را مدیریت کند و همچنین خروجی مناسب Json برای استفاده در سمت کلاینت را نیز مهیا کند. به عنوان مثال برای درخواستهای Ajax ای لازم است در سمت کلاینت نیز پاسخهای رسیده از سمت سرور به صورت سراسری مدیریت شوند و برای سایر درخواستها همان نمایش صفحات خطای پیغام مرتبط با استثناء رخ داده شده کفایت میکند.
یک مدل پیشنهادی برای تهیه خروجی مناسب برای ارسال جزئیات استثنا رخ داده در درخواستهای Ajax ای
[Serializable] public class MvcAjaxResponse : MvcAjaxResponse<object> { public MvcAjaxResponse() { } public MvcAjaxResponse(bool success) : base(success) { } public MvcAjaxResponse(object result) : base(result) { } public MvcAjaxResponse(ErrorInfo error, bool unAuthorizedRequest = false) : base(error, unAuthorizedRequest) { } } [Serializable] public class MvcAjaxResponse<TResult> : MvcAjaxResponseBase { public MvcAjaxResponse(TResult result) { Result = result; Success = true; } public MvcAjaxResponse() { Success = true; } public MvcAjaxResponse(bool success) { Success = success; } public MvcAjaxResponse(ErrorInfo error, bool unAuthorizedRequest = false) { Error = error; UnAuthorizedRequest = unAuthorizedRequest; Success = false; } /// <summary> /// The actual result object of AJAX request. /// It is set if <see cref="MvcAjaxResponseBase.Success" /> is true. /// </summary> public TResult Result { get; set; } } public class MvcAjaxResponseBase { public string TargetUrl { get; set; } public bool Success { get; set; } public ErrorInfo Error { get; set; } public bool UnAuthorizedRequest { get; set; } public bool __mvc { get; } = true; }
[Serializable] public class ErrorInfo { public int Code { get; set; } public string Message { get; set; } public string Detail { get; set; } public Dictionary<string, string> ValidationErrors { get; set; } public ErrorInfo() { } public ErrorInfo(string message) { Message = message; } public ErrorInfo(int code) { Code = code; } public ErrorInfo(int code, string message) : this(message) { Code = code; } public ErrorInfo(string message, string details) : this(message) { Detail = details; } public ErrorInfo(int code, string message, string details) : this(message, details) { Code = code; } }
public async Task CheckIsDeactiveAsync(long id) { if (await _organizationalUnits.AnyAsync(a => a.Id == id && !a.IsActive).ConfigureAwait(false)) throw new UserFriendlyException("واحد سازمانی جاری غیرفعال میباشد."); }
روش نام گذاری متدهایی که امکان بازگشت خروجی Null را دارند
public User GetById(long id);
[Serializable] public class EntityNotFoundException : Exception { public Type EntityType { get; set; } public object Id { get; set; } public EntityNotFoundException() { } public EntityNotFoundException(string message) : base(message) { } public EntityNotFoundException(string message, Exception innerException) : base(message, innerException) { } public EntityNotFoundException(SerializationInfo serializationInfo, StreamingContext context) : base(serializationInfo, context) { } public EntityNotFoundException(Type entityType, object id) : this(entityType, id, null) { } public EntityNotFoundException(Type entityType, object id, Exception innerException) : base($"There is no such an entity. Entity type: {entityType.FullName}, id: {id}", innerException) { EntityType = entityType; Id = id; } }
یک مثال واقعی
public async Task<UserOrganizationalUnitInfo> GetCurrentOrganizationalUnitInfoOrNullAsync(long userId) { return (await _setting.GetSettingValueForUserAsync( UserSettingNames.CurrentOrganizationalUnitInfo, userId).ConfigureAwait(false)) .FromJsonString<UserOrganizationalUnitInfo>(); }
- آیا اطلاعات، یک بار نوشته خواهند شد و به کرّات واکشی میشوند؟ (مانند پرفایل کاربران یا اطلاعات یک کالا در یک فروشگاه اینترنتی)
Cache-Aside
میتوان این روش را یکی از متداولترین و یا آشناترین روشهای caching دانست و شاید حداقل یک بار، کارکردن با آن را تجربه کردهایم.
نحوهی کار به این صورت میباشد که:
1- برنامه ابتدا cache را بررسی میکند میکند و اگر اطلاعات مورد نظر در cache یافت شود، اطلاعات به کاربر برگشت داده میشوند.
2- اگر اطلاعات مورد نظر در cache یافت نشود، برنامه همان درخواست را به دیتابیس میفرستد و اطلاعات را به کاربر برمیگرداند؛ همچنین موظف است اطلاعات دریافتی از دیتابیس را در cache ذخیره کند تا در دفعات بعدی آنرا از cache، واکشی کند.
public object GetMyEntity(int key) { // Try to get the entity from the cache. var value = cache.StringGet(key); if (value == null) // Cache miss { // If there's a cache miss, get the entity from the original store and cache it. value = db.StringGet(key); // Avoid caching a null value. if (value != null) { // Put the item in the cache with a custom expiration time that cache.StringSetAsync(key, JsonConvert.SerializeObject(value)); } } return value; }
Read-Through Cache
در این حالت دیتابیس و cache به صورت پشت سر هم (in-line) قرار دارند و نحوه کار به این صورت است که همیشه درخواستها در ابتدا به cache ارسال میشوند. تنها تفاوت این روش با روش قبل این است که برنامهی ما همیشه با cache صحبت میکند.
مزایا و معایب
Write-Through Cache
در این حالت اطلاعات ابتدا در cache ذخیره خواهند شد و بعد از آن در دیتابیس قرار خواهند گرفت. همچنین همانند روش Read-Through، برنامه همیشه با cache صحبت میکند.
این روش همهی مزایای روش Read-Through را دارد به علاوه رفع معایب آن از جمله:
- ناسازگاری دادهها نمیتوانند اتفاق بیفتند؛ زیرا اطلاعات همیشه ابتدا در cache نوشته خواهند شد و بعد در دیتابیس؛ به همین خاطر اطلاعات در هر دو نسخه یکسان هستند.
- بعد از درج اطلاعات جدید، نیازی به warming نیست. به این دلیل که در ابتدا در cache ذخیره خواهند شد.
یکپارچه سازی CKEditor با Lightbox
آموزش Knockout.Js #3
Computed Observables
Computed Observablesها به واقع خواصی هستندکه از ترکیب چند خاصیت دیگر به دست میآیند یا برای به دست آوردن مقادیر آنها باید یک سری محاسبات را انجام داد. برای مثال به ViewModel زیر دقت کنید:
var personViewModel = { firstName: ko.observable("Masoud"), lastName: ko.observable("Pakdel") this.fullName = ko.computed(function() { return this.firstName() + " " + this.lastName(); }, this); };
<span data-bind='text: fullName'></span>
function Product(name, price) { this.name = ko.observable(name); this.price = ko.observable(price); }
this.shoppingCart = ko.observableArray([ new Product("Beer", 10.99), new Product("Brats", 7.99), new Product("Buns", 1.49) ]);
<table> <thead><tr> <th>Product</th> <th>Price</th> </tr></thead> <tbody data-bind='foreach: shoppingCart'> <tr> <td data-bind='text: name'></td> <td data-bind='text: price'></td> </tr> </tbody> </table>
حال نیاز به یک button داریم تا با کلیک با بر روی آن یک product جدید به لیست اضافه خواهد شد.
<button data-bind='click: addProduct'>Add Beer</button>
this.addProduct = function() { this.shoppingCart.push(new Product("More Beer", 10.99)); };
تا اینجا کدهای ViewModel به صورت زیر خواهد بود:
function PersonViewModel() { this.firstName = ko.observable("John"); this.lastName = ko.observable("Smith"); this.checkout = function () { alert("Trying to checkout"); }; this.fullName = ko.computed(function(){ return this.firstName() + " " + this.lastName(); }, this); this.shoppingCart = ko.observableArray([ new Product("Beer", 10.99), new Product("Brats", 7.99), new Product("Buns", 1.49) ]); this.addProduct = function () { this.shoppingCart.push(new Product("More beer", 10.99)); }; };
در این مرحله قصد داریم که یک button نیز برای حذف آیتم از لیست ایجاد کنیم. در ابتدا یک تایع جدید به نام removeProduct به صورت زیر ایجاد خواهیم کرد:
this.removeProduct = function(product) { self.shoppingCart.remove(product); };
function PersonViewModel() { var self = this;
<tr> <td data-bind='text: name'></td> <td data-bind='text: price'></td> <td><button data-bind='click: $root.removeProduct'>Remove</button></td> </tr>
دستور remove در لیست باعث حذف کامل آیتم از لیست خواهد شد و در خیلی موارد این مورد برای ما خوشایند نیست زیرا حذف یک آیتم از لیست باید در سمت سرور نیز انجام شود نه صرفا در سمت کلاینت، در نتیجه میتوانیم از دستور destroy استفاده کنیم. استفاده از این دستور باعث خواهد شد که عنصر مورد نظر در لیست نمایش داده نشود ولی به صورت واقعی از لیست حذف نشده است(این کار را با تغییر در مقدار خاصیت destroy_ هر عنصر انجام میدهد)
ادامه دارد...
دریافت سورس مثال
چرا از آنگولار به ری اکت + ری داکس سوئیچ کردم!
نتیجه گیری: یک جمع بر اساس «همدلی» و «همراهی» میتوانند رشد کنند و یا ... «جمع» نامیده نمیشوند.