به این صورت مدلهای خود را تعریف کرده و طبق مقالهی قبلی، Controllerهای هر یک را پیاده سازی نمایید:
public class Supplier { [Key] public string Key {get; set; } public string Name { get; set; } } public class Category { public int ID { get; set; } public string Name { get; set; } public virtual ICollection<Product> Products { get; set; } } public class Product { public int ID { get; set; } public string Name { get; set; } public decimal Price { get; set; } [ForeignKey("Category")] public int CategoryId { get; set; } public Category Category { get; set; } [ForeignKey("Supplier")] public string SupplierId { get; set; } public virtual Supplier Supplier { get; set; } }
پکیج Microsoft.AspNet.OData به تازگی ورژن 6 آن به صورت رسمی منتشر شده و شامل تغییراتی نسبت به نسخهی قبلی آن است. اولین نکتهی حائز اهمیت، Config آن است که به صورت زیر تغییر کرده و باید Optionهای مورد نیاز، کانفیگ شوند. در این نسخه DI نیز به Odata اضافه شده است:
public static void Register(HttpConfiguration config) { ODataModelBuilder odataModelBuilder = new ODataConventionModelBuilder(); var product = odataModelBuilder.EntitySet<Product>("Products"); var category = odataModelBuilder.EntitySet<Category>("Categories"); var supplier = odataModelBuilder.EntitySet<Supplier>("Suppliers"); var edmModel = odataModelBuilder.GetEdmModel(); supplier.EntityType.Ignore(c => c.Name); config.Select(System.Web.OData.Query.QueryOptionSetting.Allowed); config.MaxTop(25); config.OrderBy(System.Web.OData.Query.QueryOptionSetting.Allowed); config.Count(System.Web.OData.Query.QueryOptionSetting.Allowed); config.Expand(System.Web.OData.Query.QueryOptionSetting.Allowed); config.Filter(System.Web.OData.Query.QueryOptionSetting.Allowed); config.Count(System.Web.OData.Query.QueryOptionSetting.Allowed); //config.MapODataServiceRoute("ODataRoute", "odata", edmModel); // کانفیگ به صورت معمولی config.MapODataServiceRoute("ODataRoute", "odata", builder => { builder.AddService(ServiceLifetime.Singleton, sp => edmModel); builder.AddService<IEnumerable<IODataRoutingConvention>>(ServiceLifetime.Singleton, sp => ODataRoutingConventions.CreateDefault()); }); }
[EnableQuery] public IQueryable<Product> Get() { return new List<Product> { new Product { Id = 1, Name = "name 1", Price = 11, Category = new Category {Id =1, Name = "Cat1" } }, new Product { Id = 2, Name = "name 2", Price = 12, Category = new Category {Id =2, Name = "Cat2" } }, new Product { Id = 3, Name = "name 3", Price = 13, Category = new Category {Id =3, Name = "Cat3" } }, new Product { Id = 4, Name = "name 4", Price = 14, Category = new Category {Id =4, Name = "Cat4" } }, }.AsQueryable(); ; }
Description | Option |
بسط دادن موجودیت مرتبط | $expand |
فیلتر کردن نتیجه، بر اساس شرطهای Boolean ی | $filter |
فرمان به سرور که تعداد رکوردهای بازگشتی را نیز نمایش دهد(مناسب برای پیاده سازی server-side pagging ) | $count |
مرتب کردن نتیجهی بازگشتی | $orderby |
select زدن روی پراپرتیهای درخواستی | $select |
پرش کردن از اولین رکورد به اندازهی n عدد | $skip |
فقط بازگرداندن n رکورد اول | $top |
/odata/Products?$select=Id,Name
/odata/Products?$top=3&$skip=2
/odata/Products?$count=true
{@odata.context: "http://localhost:4516/odata/$metadata#Products", @odata.count: 4,…} @odata.context:"http://localhost:4516/odata/$metadata#Products" @odata.count:4 value:[{Id: 1, Name: "name 1", Price: 11, SupplierId: 0, CategoryId: 0},…] 0:{Id: 1, Name: "name 1", Price: 11, SupplierId: 0, CategoryId: 0} 1:{Id: 2, Name: "name 2", Price: 12, SupplierId: 0, CategoryId: 0} 2:{Id: 3, Name: "name 3", Price: 13, SupplierId: 0, CategoryId: 0} 3:{Id: 4, Name: "name 4", Price: 14, SupplierId: 0, CategoryId: 0}
/odata/Products?$filter=Id eq 1
/odata/Products?$filter=Id gt 1 and Id lt 3
/odata/Products?$orderby=Id desc
/odata/Products?$expand=Category
/odata/Products?$expand=Category,Supplier
/odata/Categories(1)?$expand=Products/Supplier
[EnableQuery(PageSize = 10)]
محدود کردن Query Options
[EnableQuery (AllowedQueryOptions= AllowedQueryOptions.Skip | AllowedQueryOptions.Top)]
[EnableQuery(AllowedLogicalOperators=AllowedLogicalOperators.Equal)]
var product = odataModelBuilder.EntitySet<Product>("Products"); product.EntityType.Ignore(e => e.Price);
[EnableQuery(AllowedOrderByProperties = "Id,Name")]
public System.Web.Http.IHttpActionResult GetName(int key) { Product product = Get().Single(c => c.Id == key); return Ok(product.Name); }
/odata/Products(1)/Name/$value
HTTP/1.1 200 OK Content-Type: text/plain; charset=utf-8 DataServiceVersion: 3.0 Content-Length: 3 Ali
Attribute Convention هایی هم برای اعتبارسنجی پراپرتیها موجود است که نام آنها واضح تعریف شدهاند:
Description | Attribute |
اجازهی فیلتر زدن بر روی آن پراپرتی داده نخواهد شد | NotFilterable |
اجازهی مرتب کردن بر روی آن پراپرتی داده نخواهد شد | NotSortable |
اجازهی select زدن بر روی آن پراپرتی داده نمیشود | NotNavigable |
اجازهی شمارش دهی بر روی آن Collection داده نمیشود | NotCountable |
اجازهی بسط دادن آن Collection داده نمیشود | NotExpandable |
و همچنین [AutoExpand] به صورت اتوماتیک آن موجودیت مورد نظر را بسط میدهد.
بطور مثال کدهای زیر را در مدل خود میتوانید مشاهده نمائید:
public class Product { public int Id { get; set; } public string Name { get; set; } [NotFilterable, NotSortable] public decimal Price { get; set; } [ForeignKey(nameof(SupplierId))] [NotNavigable] public virtual Supplier Supplier { get; set; } public int SupplierId { get; set; } [ForeignKey(nameof(CategoryId))] public virtual Category Category { get; set; } public int CategoryId { get; set; } [NotExpandable] public virtual ICollection<TestEntity> TestEnities { get; set; } }
فرض کنید پراپرتی زیر را به مدل خود اضافه کرده اید
public DateTimeOffset CreatedOn { get; set; }
حال کوئری زیر را برای فیلتر زدن، بر روی آن در اختیار داریم:
/odata/Products?$filter=year(CreatedOn) eq 2016
در اینجا فقط Product هایی بازگردانده میشوند که در سال 2016 ثبت شدهاند:
/odata/Products?$filter=CreatedOn lt cast(2017-04-01T04:11:31%2B08:00,Edm.DateTimeOffset)
کوئری فوق تاریخ مورد نظر را Cast کرده و همهی Product هایی را که قبل از این تاریخ ثبت شدهاند، باز میگرداند.
Nested Filter In Expand
/odata/Categories?$expand=Products($filter=Id gt 1 and Id lt 5)
همهی Categoryها به علاوه بسط دادن Product هایشان، در صورتیکه Id آنها بیشتر از 1 باشد
و یا حتی بر روی موجودیت بسط داده شده، select زده شود:
/odata/Categories?$expand=Products($select=Id,Name)
Custom Attribute
ضمنا به سادگی میتوان اتریبیوت سفارشی نوشت:
public class MyEnableQueryAttribute : EnableQueryAttribute { public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions) { // Don't apply Skip and Top. var ignoreQueryOptions = AllowedQueryOptions.Skip | AllowedQueryOptions.Top; return queryOptions.ApplyTo(queryable, ignoreQueryOptions); } }
روی هر متدی از کنترلر خود که اتریبیوت [MyEnableQuery] را قرار دهید، دیگر قابلیت Skip, Top را نخواهد داشت.
Dependency Injection در آخرین نسخهی OData اضافه شده است. بطور پیشفرض OData بصورت case-sensitive رفتار میکند. برای تغییر دادن آن در نسخههای قدیمی، Extension Methodی به نام EnableCaseSensitive وجود داشت. اما در نسخهی جدید شما میتوانید پیاده سازی خاص خود را از هر کدام از بخشهای OData داشته باشید و با استفاده از تزریق وابستگی، آن را به config برنامهی خود اضافه کنید؛ برای مثال:
public class CaseInsensitiveResolver : ODataUriResolver { private bool _enableCaseInsensitive; public override bool EnableCaseInsensitive { get { return true; } set { _enableCaseInsensitive = value; } } }
اینجا پیاده سازی از ODataUriResolver انجام شده و متد EnableCaseInsensitive به صورت جدیدی override و در حالت default مقدار true را برمیگرداند.
حال به صورت زیر آن را میتوان به وابستگیهای config برنامه، اضافه نمود:
config.MapODataServiceRoute("ODataRoute", "odata", builder => { builder.AddService(ServiceLifetime.Singleton, sp => edmModel); builder.AddService<IEnumerable<IODataRoutingConvention>>(ServiceLifetime.Singleton, sp => ODataRoutingConventions.CreateDefault()); builder.AddService<ODataUriResolver>(ServiceLifetime.Singleton, sp => new CaseInsensitiveResolver()); // how enable case sensitive });
در قسمت بعدی به Actionها و Functionها در OData میپردازیم.
EF Code First #12
- چون از لایه سرویس استفاده نکرده کل منطق کار رو برده داخل کنترلرها.
- از تزریق وابستگیها استفاده نکرده، بنابراین برخلاف شکلی که در ابتدای کار گذاشته، کنترلرهاش رو نمیتونه مستقل از یک سری کلاس کاملا مشخص، تست کنه.
- الگوی مخزنی که ارائه داده در این مثال ساده کار میکنه اما اگر قرار باشه با چند موجودیت کار کرد و نتیجه رو ترکیب، کارآیی خوبی نداره چون خیلی از قابلیتهای ذاتی EF مثل کوئریهای به تاخیر افتاده (deferred LINQ queries) در اینجا قابل پیاده سازی نیست. اگر هم بخوان این رو اضافه کنن باید به لایه مخزن خروجی IQueryable اضافه کنن که به یک طراحی نشتی دار خواهند رسید چون انتهای کار با خروجی IQueryable کاملا باز باقی میماند (نمونهاش متد Get ایی است که طراحی کرده).
یا یکی دیگر از اهداف ظاهری لایه مخزن، امکان تعویض آن در صورت نیاز است و مثلا کوچ به یک ORM دیگر. دنیای واقعی: include ایی که اینجا تعریف شده فقط در EF وجود خارجی دارد یا یک سری از نکات دیگر بکار گرفته شده در این الگوی مخزن. (در قسمت 11 سری EF سایت بحث شده)
- در مثالی که زده باگ امنیتی وجود دارد. متد Updateاش به دلیل عدم استفاده از ViewModel آسیب پذیر هست. (در این مورد در سری MVC بحث شده)
ASP.NET MVC #6
ولی به نظر من توی mvc قضیه برعکسه. وقتی قرار واسط کاربر طراحی بشه ، و از روش ترجیح داده شده strongly type view استفاده بشه ( که واقعا جالبه و کار باهاش راحت) ، طراح حتی باید مدل ها رو بشناسه، یا حتی در روش باز هم توصیه شده ، کلاس های سی شارپی به عنوان viewModel تعریف کنه و view رو با اونها تشکیل بده. در حالیکه در مرحله کدنویسی دیگه به عناصر صفحه وابستگی وجود نداره.
منظورم اینه که توی هرکدوم به نظرم وابستگی وجود داره، ولی تو مراحل متفاوت.اینطوره؟
کلیدهای مربوط به Request
ضروری؟ | نام کلید | مقدار |
بله | "owin.RequestBody" | یک Stream همراه با request body. اگر body برای request وجود نداشته باشد، Stream.Null به عنوان placeholder قابل استفاده است. |
بله | "owin.RequestHeaders" | یک دیکشنری به صورت IDictionary<string, string[]> از هدرهای درخواست. |
بله | "owin.RequestMethod" | رشتهایی حاوی نوع فعل متد HTTP مربوط به درخواست (مانند GET and POST ) |
بله | "owin.RequestPath" | path درخواست شده به صورت string |
بله | "owin.RequestPathBase" | قسمتی از path درخواست به صورت string |
بله | "owin.RequestProtocol" | نام و نسخهی پروتکل (مانند HTTP/1.0 or HTTP/1.1 ) |
بله | "owin.RequestQueryString" | رشتهای حاوی query string ؛ بدون علامت ? (مانند foo=bar&baz=quux ) |
بله | "owin.RequestScheme" | رشتهایی حاوی URL scheme استفاده شده در درخواست (مانند HTTP or HTTPS ) |
ضروری؟ | نام کلید | مقدار |
بله | "owin.ResponseBody" | یک Stream جهت نوشتن response body در خروجی |
بله | "owin.ResponseHeaders" | یک دیکشنری به صورت IDictionary<string, string[]> از هدرهای response |
خیر | "owin.ResponseStatusCode" | یک عدد صحیح حاوی کد وضعیت HTTP response ؛ حالت پیشفرض 200 است. |
خیر | "owin.ResponseReasonPhrase" | یک رشته حاوی reason phrase مربوط به status code ؛ اگر خالی باشد در نتیجه سرور بهتر است آن را مقداردهی کند. |
خیر | "owin.ResponseProtocol" | یک رشته حاوی نام و نسخهی پروتکل (مانند HTTP/1.0 or HTTP/1.1 )؛ اگر خالی باشد؛ “owin.RequestProtocol” به عنوان مقدار پیشفرض در نظر گرفته خواهد شد. |
<package id="Microsoft.Owin" version="3.0.1" targetFramework="net461" /> <package id="Microsoft.Owin.Host.SystemWeb" version="3.0.1" targetFramework="net461" /> <package id="Owin" version="1.0" targetFramework="net461" />
using Owin; namespace SimpleOwinWebApp { public class Startup { public void Configuration(IAppBuilder app) { } } }
using Owin; namespace SimpleOwinWebApp { public class Startup { public void Configuration(IAppBuilder app) { app.Use(async (ctx, next) => { await ctx.Response.WriteAsync("Hello"); }); } }
Func<IOwinContext, Func<Task>, Task> handler
app.Use(async (ctx, next) => { var response = ctx.Environment["owin.ResponseBody"] as Stream; using (var writer = new StreamWriter(response)) { await writer.WriteAsync("Hello"); } });
using System; using Microsoft.Owin.Hosting; namespace SimpleOwinConsoleApp { class Program { static void Main(string[] args) { using (WebApp.Start<Startup>("http://localhost:12345")) { Console.WriteLine("Listening to port 12345"); Console.WriteLine("Press Enter to end..."); Console.ReadLine(); } } } }
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace SimpleOwinCoreApp { public class Startup { public void ConfigureServices(IServiceCollection services) { } public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { loggerFactory.AddConsole(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.Run(async (context) => { await context.Response.WriteAsync("Hello World!"); }); } } }
using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace SimpleOwinCoreApp.Middlewares { public class SimpleMiddleware { private readonly RequestDelegate _next; public SimpleMiddleware(RequestDelegate next) { _next = next; } public async Task Invoke(HttpContext ctx) { // قبل از فراخوانی میانافزار بعدی await ctx.Response.WriteAsync("Hello DNT!"); await _next(ctx); // بعد از فراخوانی میانافزار بعدی } } }
app.UseMiddleware<SimpleMiddleware>();
"Microsoft.AspNetCore.Owin": "1.0.0"
app.UseOwin(pipeline => { pipeline(next => new MyKatanaBasedMiddleware(next).Invoke) });
using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace SimpleOwinAspNetCore.Middleware { public class IpBlockerMiddleware { private readonly RequestDelegate _next; private readonly IpBlockerOptions _options; public IpBlockerMiddleware(RequestDelegate next, IpBlockerOptions options) { _next = next; _options = options; } public async Task Invoke(HttpContext context) { var ipAddress = context.Request.Host.Host; if (IsBlockedIpAddress(ipAddress)) { context.Response.StatusCode = 403; await context.Response.WriteAsync("Forbidden : The server understood the request, but It is refusing to fulfill it."); return; } await _next.Invoke(context); } private bool IsBlockedIpAddress(string ipAddress) { return _options.Ips.Any(ip => ip == ipAddress); } } }
using System.Collections.Generic; namespace SimpleOwinAspNetCore.Middleware { public class IpBlockerOptions { public IpBlockerOptions() { Ips = new[] { "192.168.1.1" }; } public IList<string> Ips { get; set; } } }
using System.Linq; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; namespace SimpleOwinAspNetCore.Middleware { public static class IpBlockerExtensions { public static IApplicationBuilder UseIpBlocker(this IApplicationBuilder builder, IConfigurationRoot configuration, IpBlockerOptions options = null) { return builder.UseMiddleware<IpBlockerMiddleware>(options ?? new IpBlockerOptions { Ips = configuration.GetSection("block_list").GetChildren().Select(p => p.Value).ToArray() }); } } }
{ "block_list": [ "192.168.1.1", "localhost", "127.0.0.1", "172.16.132.151" ] }
public IConfigurationRoot Configuration { set; get; } public Startup(IHostingEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("blockedIps.json"); Configuration = builder.Build(); }
app.UseIpBlocker(Configuration);
using System.Collections.Generic; using System.Linq; using System.ServiceModel; using System.Text; using System.Threading.Tasks; namespace MyNewsWCFLibrary { [ServiceContract] interface IMyNewsService { [OperationContract] List<tblNews> GetAllNews(); [OperationContract] tblNews GetNews(int tblNewsId); [OperationContract] int AddNews(tblNews News); [OperationContract] bool EditNews(tblNews News); [OperationContract] bool DeleteNews(int tblNewsId); [OperationContract] int AddCategory(tblCategory News); [OperationContract] bool DeleteCategory(int tblCategoryId); [OperationContract] List<tblCategory> GetAllCategory(); } }
کلاسی به نام MyNewsService با ارثبری از IMyNewsService ایجاد میشود. زیر حرف I از IMyNewsService یک خط دیده میشود که با کلیک روی آن برابر با شکل زیر عمل کنید:
ملاحظه خواهید کرد که کلیهی متدها برابر با Interface ساخته خواهد شد. اکنون همانند شکل روی نشان هرم شکلی که هنگامی که روی نام کلاس کلیک میکنید، در سمت چپ نشان داده میشود کلیک کنید و گزینه Move to another file to match type name را انتخاب کنید:
به صورت خودکار محتوای این کلاس به یک فایل دیگر انتقال مییابد. اکنون هر کدام از متدها را به شکل دلخواه ویرایش میکنیم. من کد کلاس را اینگونه تغییر دادم:
using System; using System.Collections.Generic; using System.Data.Entity; using System.Linq; namespace MyNewsWCFLibrary { class MyNewsService : IMyNewsService { private dbMyNewsEntities dbMyNews = new dbMyNewsEntities(); public List<tblNews> GetAllNews() { return dbMyNews.tblNews.Where(p => p.IsDeleted == false).ToList(); } public tblNews GetNews(int tblNewsId) { return dbMyNews.tblNews.FirstOrDefault(p => p.tblNewsId == tblNewsId); } public int AddNews(tblNews News) { dbMyNews.tblNews.Add(News); dbMyNews.SaveChanges(); return News.tblNewsId; } public bool EditNews(tblNews News) { try { dbMyNews.Entry(News).State = EntityState.Modified; dbMyNews.SaveChanges(); return true; } catch (Exception exp) { return false; } } public bool DeleteNews(int tblNewsId) { try { tblNews News = dbMyNews.tblNews.FirstOrDefault(p => p.tblNewsId == tblNewsId); News.IsDeleted = true; dbMyNews.SaveChanges(); return true; } catch (Exception exp) { return false; } } public int AddCategory(tblCategory Category) { dbMyNews.tblCategory.Add(Category); dbMyNews.SaveChanges(); return Category.tblCategoryId; } public bool DeleteCategory(int tblCategoryId) { try { tblCategory Category = dbMyNews.tblCategory.FirstOrDefault(p => p.tblCategoryId == tblCategoryId); Category.IsDeleted = true; dbMyNews.SaveChanges(); return true; } catch (Exception exp) { return false; } } public List<tblCategory> GetAllCategory() { return dbMyNews.tblCategory.Where(p => p.IsDeleted == false).ToList(); } } }
ولی شما ممکن است دربارهی حذف، دوست داشته باشید رکوردها از پایگاه داده حذف شوند و نه اینکه با یک فیلد بولی آنها را مدیریت کنید. در این صورت کد شما میتواند اینگونه نوشته شود:
using System; using System.Collections.Generic; using System.Data.Entity; using System.Linq; namespace MyNewsWCFLibrary { class MyNewsService : IMyNewsService { private dbMyNewsEntities dbMyNews = new dbMyNewsEntities(); public List<tblNews> GetAllNews() { return dbMyNews.tblNews.ToList(); } public tblNews GetNews(int tblNewsId) { return dbMyNews.tblNews.FirstOrDefault(p => p.tblNewsId == tblNewsId); } public int AddNews(tblNews News) { dbMyNews.tblNews.Add(News); dbMyNews.SaveChanges(); return News.tblNewsId; } public bool EditNews(tblNews News) { try { dbMyNews.Entry(News).State = EntityState.Modified; dbMyNews.SaveChanges(); return true; } catch (Exception exp) { return false; } } public bool DeleteNews(tblNews News) { try { dbMyNews.tblNews.Remove(News); dbMyNews.SaveChanges(); return true; } catch (Exception exp) { return false; } } public int AddCategory(tblCategory Category) { dbMyNews.tblCategory.Add(Category); dbMyNews.SaveChanges(); return Category.tblCategoryId; } public bool DeleteCategory(tblCategory Category) { try { dbMyNews.tblCategory.Remove(Category); dbMyNews.SaveChanges(); return true; } catch (Exception exp) { return false; } } public List<tblCategory> GetAllCategory() { return dbMyNews.tblCategory.ToList(); } } }
البته باید در نظر داشته باشید که در صورت هر گونه تغییر در پارامترهای ورودی، لایهی Interface نیز باید تغییر کند. گونهی دیگر نوشتن متد حذف خبر میتواند به صورت زیر باشد:
public bool DeleteNews(int tblNewsId) { try { tblNews News = dbMyNews.tblNews.FirstOrDefault(p => p.tblNewsId == tblNewsId); dbMyNews.tblNews.Remove(News); dbMyNews.SaveChanges(); return true; } catch (Exception exp) { return false; } }
در بخش 5 دربارهی تغییرات App.Config خواهم نوشت.
EF Code First #12
نهایتا اطلاعات ViewModel در حالت Post، به اطلاعات Model انتساب داده میشه. یا دستی و یا مثلا توسط AutoMapper ؛ در این حالت هم هر طور که راحت هستید عمل کنید. قانون یا روش بهتری برای این نوع انتسابها وجود نداره.
ASP.NET MVC #11
به هیچ عنوان هم از یک مدل (domain model) به عنوان مدل یک View استفاده نمیکنم خصوصا به دلایل امنیتی که نباید کل model را در معرض دید قرار داد. برای مثال اگر یک صفحه فقط به نام کاربری و پسورد نیاز دارد، این view به یک viewmodel به نام UserInfoViewModel با دو فیلد یاد شده نیاز دارد و نه بیشتر.
به طور خلاصه برای مثال اگر از EF Code first استفاده میکنید، کلاسهای مدل آن نباید مستقیما در MVC استفاده شوند. این M در MVC به معنای مدلی مخصوص View است و نه کلاسهای اصلی دومین شما.
ASP.NET MVC #11
من اینطور فهمیده بودم که viewModel فقط برای strongly type بودن view هستش و هیچ منطقی قرار نیست در اون پیاده بشه. بنابراین اگه چند بخش مختلف مثل اخبار، نفرات حاضر و ... بوسیله اون نمایش داده بشه مشکلی ایجاد نمی کنه. چون ViewModel به فرض فقط لیستی از خبرها رو نگهداری میکنه و پر کردن اون لیست وظیفه اش نیست.
ولی مثل اینکه دوستان نظر دیگه ای دارن.
ویدیوهای آموزشی MVVM
یک سری ویدیوی رایگان آموزشی MVVM از مایکروسافت و همچنین شرکت Infragistics در دسترس هستند که جهت سهولت، لیست آنها را ادامه میتوانید مشاهده نمائید: