پیاده سازی یک معماری لایه ای در MVC

دوشنبه 22 آذر 1395

در این مقاله می خواهیم برنامه ای با استفاده از نوع خاصی از معماری پیاده سازی کنیم که لایه های آن دارای حداقل وابستگی باشند ، به طوری که برنامه برای استفاده در مقیاس های بزرگ کاملا مورد قبول و مفید باشد.

پیاده سازی یک معماری لایه ای در MVC

انتخاب معماری مناسب برای یک برنامه تحت وب، یک مسئله ی بسیار مهم است، به خصوص اگر این برنامه در مقیاس وسیعی مورد استفاده قرار بگیرد. استفاده از قالب پیش فرض Visual Studio ، یعنی همان ASP.NET MVC Web Application ، و اضافه کردن کنترلر ها با Scaffolding و ساخت صفحات و داده ها تنها در چند دقیقه مطمئنا امکانات بی نظیری برای توسعه دهندگان محسوب می شوند، اما باید این را در نظر داشته باشیم که استفاده از این روش، همیشه انتخاب درستی نیست . معماری و روشی که برای توسعه برنامه تان انتخاب می کنید، بر مواردی نظیر قابلیت نگهداری، استفاده مجدد از برنامه و قابلیت تست برنامه تاثیر به سزایی دارند. در این مقاله ، می آموزیم که چطور می توانیم یک برنامه در اختیار داشته باشیم که قسمت های مختلف آن نظیر لایه های  Data Access, Business و  Presentation به درستی تعریف شده باشند و کمترین وابستگی بین اجزا در برنامه وجود داشته باشد. برای این کار، ما از الگوها و فریم ورک های متعددی استفاده می کنیم که برخی از آن ها در زیر آورده شده اند:

Entity Framework Code First development

Generic Repository Pattern

Dependency Injection using Autofac framework

Automapper

معماری که ما می خواهیم آن را در این برنامه پیاده سازی کنیم، به صورت زیر است:

بیایید ساخت برنامه را شروع کنیم. فرض کنید که می خواهیم یک برنامه تحت وب به نام Store بسازیم. برای این کار یک solution خالی ایجاد کرده و آن را  نامگذاری نمایید. 

مدل ها 

یک class Library با نام Store.Modelبه Solution خود اضافه کنید. این کتابخانه ، محلی است که ما همه ی شی های برنامه را در آن نگهداری خواهیم کرد. Entity Framework برای ساخت پایگاه داده از این مدل ها استفاده خواهد کرد. اما در این پروژه قصد نداریم از DataAnnotations attribute ها برای پیکربندی Code First استفاده کنیم. در عوض، ما همه ی پیکربندی های مربوط به Code First را در کلاس هایی با استفاده از Fluent API انجام خواهیم داد. یک پوشه به نام Model ایجاد کنید و دو کلاس زیر را به آن اضافه نمایید. 

Gadget.cs

public class Gadget
    {
        public int GadgetID { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public decimal Price { get; set; }
        public string Image { get; set; }
 
        public int CategoryID { get; set; }
        public Category Category { get; set; }
    }

Category.cs

public class Category
    {
        public int CategoryID { get; set; }
        public string Name { get; set; }
        public DateTime? DateCreated { get; set; }
        public DateTime? DateUpdated { get; set; }
 
        public virtual List<Gadget> Gadgets { get; set; }
 
        public Category()
        {
            DateCreated = DateTime.Now;
        }
    }

ما در این مقاله ترجیح دادیم از فضای نام Store.Model برای این کلاس ها استفاده کنیم. 

Data Access Layer و Repository ها

هدف این لایه ، دسترسی مستقیم به پایگاه داده است. در حقیقت، تنها این لایه مسئول برقراری ارتباط با پایگاه داده است. اگر یک لایه نیاز به ارتباط با پایگاه داده داشته باشد، ما این کار را از طریق تعریف کلاس هایی (Repository ها) انجام خواهیم داد. برای این کار نیاز داریم تا یک پروژه ی class Library به نام Store.Data ایجاد کنیم و refrence آن را به پروژه قبلی اضافه نماییم.با استفاده از Nuget Package Manager  ، می توانید Entity Framework را به پروژه تان اضافه کنید. اولین کاری که می خواهیم انجام بدهیم، تعریف Entity Type Configuration ها برای شی هایمان است. یک پوشه به نام Configuration ایجاد کنید که دارای دو کلاس زیر باشد. این دو کلاس از کلاس EntityTypeConfiguration ارث بری خواهند کرد. 

GadgetConfiguration.cs

public class GadgetConfiguration: EntityTypeConfiguration<Gadget>
    {
        public GadgetConfiguration()
        {
            ToTable("Gadgets");
            Property(g => g.Name).IsRequired().HasMaxLength(50);
            Property(g => g.Price).IsRequired().HasPrecision(8, 2);
            Property(g => g.CategoryID).IsRequired();
        }

CategoryConfiguration.cs

public class CategoryConfiguration : EntityTypeConfiguration<Category>
    {
        public CategoryConfiguration()
        {
            ToTable("Categories");
            Property(c => c.Name).IsRequired().HasMaxLength(50);
        }
    }

هبچ پیکربندی خاصی نیست که نیاز به توضیح داشته باشد، تنها نکته این است که بدانیم شی ها را در جای مناسب خودشان قرار بدهیم. مرحله بعدی ایجاد کلاس DbContext است که مسئول دسترسی به پایگاه داده است. کلاس زیر را در root (مسیر اصلی ) پروژه جاری ایجاد کنید. 

StoreEntities.cs

public class StoreEntities : DbContext
    {
        public StoreEntities() : base("StoreEntities") { }
 
        public DbSet<Gadget> Gadgets { get; set; }
        public DbSet<Category> Categories { get; set; }
 
        public virtual void Commit()
        {
            base.SaveChanges();
        }
 
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Configurations.Add(new GadgetConfiguration());
            modelBuilder.Configurations.Add(new CategoryConfiguration());
        }
    }

ما می خواهیم زمانی که اولین بار، برنامه اجرا شد، پایگاه داده با  داده هایی پر شود، برای این کار کلاس زیر را نیز در root پروژه ایجاد نمایید.

 StoreSeedData

public class StoreSeedData : DropCreateDatabaseIfModelChanges<StoreEntities>
    {
        protected override void Seed(StoreEntities context)
        {
            GetCategories().ForEach(c => context.Categories.Add(c));
            GetGadgets().ForEach(g => context.Gadgets.Add(g));
 
            context.Commit();
        }
 
        private static List<Category> GetCategories()
        {
            return new List<Category>
            {
                new Category {
                    Name = "Tablets"
                },
                new Category {
                    Name = "Laptops"
                },
                new Category {
                    Name = "Mobiles"
                }
            };
        }
 
        private static List<Gadget> GetGadgets()
        {
            return new List<Gadget>
            {
                new Gadget {
                    Name = "ProntoTec 7",
                    Description = "Android 4.4 KitKat Tablet PC, Cortex A8 1.2 GHz Dual Core Processor,512MB / 4GB,Dual Camera,G-Sensor (Black)",
                    CategoryID = 1,
                    Price = 46.99m,
                    Image = "prontotec.jpg"
                },
                new Gadget {
                    Name = "Samsung Galaxy",
                    Description = "Android 4.4 Kit Kat OS, 1.2 GHz quad-core processor",
                    CategoryID = 1,
                    Price = 120.95m,
                    Image= "samsung-galaxy.jpg"
                },
                new Gadget {
                    Name = "NeuTab® N7 Pro 7",
                    Description = "NeuTab N7 Pro tablet features the amazing powerful, Quad Core processor performs approximately Double multitasking running speed, and is more reliable than ever",
                    CategoryID = 1,
                    Price = 59.99m,
                    Image= "neutab.jpg"
                },
                new Gadget {
                    Name = "Dragon Touch® Y88X 7",
                    Description = "Dragon Touch Y88X tablet featuring the incredible powerful Allwinner Quad Core A33, up to four times faster CPU, ensures faster multitasking speed than ever. With the super-portable size, you get a robust power in a device that can be taken everywhere",
                    CategoryID = 1,
                    Price = 54.99m,
                    Image= "dragon-touch.jpg"
                },
                new Gadget {
                    Name = "Alldaymall A88X 7",
                    Description = "This Alldaymall tablet featuring the incredible powerful Allwinner Quad Core A33, up to four times faster CPU, ensures faster multitasking speed than ever. With the super-portable size, you get a robust power in a device that can be taken everywhere",
                    CategoryID = 1,
                    Price = 47.99m,
                    Image= "Alldaymall.jpg"
                },
                new Gadget {
                    Name = "ASUS MeMO",
                    Description = "Pad 7 ME170CX-A1-BK 7-Inch 16GB Tablet. Dual-Core Intel Atom Z2520 1.2GHz CPU",
                    CategoryID = 1,
                    Price = 94.99m,
                    Image= "asus-memo.jpg"
                },
                // Code ommitted 
            };
        }
    }

حالا بیایید قسمت اصلی پروژه را ایجاد کنیم. یک پوشه به نام Infrastructure بسازید . برای این که بتوانیم از الگوی Repository به روش درست استفاده کنیم، نیاز داریم تا یک Infrastructure خوب (زیربنای مناسب) برای آن بسازیم. همه شی ها از طریق inteface های تزریق شده در طول برنامه در دسترس خواهند بود. و اولین نمونه ای که به آن نیاز داریم، یک نمونه از StoreEntities است. بنابراین بیایید یک factory Interface بسازیم که مسئول ایجاد این نمونه از کلاس باشد. یک Interface به نام IDbFactory داخل پوشه ی Infrastructure بسازید. 

public interface IDbFactory : IDisposable
    {
        StoreEntities Init();
    }

می توانید ببینید که این Interface از IDisposable ارث بری می کند، بنابراین هر کلاسی که این Interface را implement کند، باید IDisposable  را نیز implement  کند. برای انجام این کار به شیوه ی درست ، یک کلاس Disposable ایجاد کنید که از  IDisposable interface اث بری می کند. بنابراین هر کلاسی که از IDbFactory interface ارث بری کند، ملزم می شود که از این کلاس نیز ارث بری کند. 

Disposable.cs

public class Disposable : IDisposable
    {
        private bool isDisposed;
 
        ~Disposable()
        {
            Dispose(false);
        }
 
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        private void Dispose(bool disposing)
        {
            if (!isDisposed && disposing)
            {
                DisposeCore();
            }
 
            isDisposed = true;
        }
 
        // Ovveride this to dispose custom objects
        protected virtual void DisposeCore()
        {
        }
    }

حالا بخش implement شده را در زیر می بینیم:

DbFactory.cs

public class DbFactory : Disposable, IDbFactory
    {
        StoreEntities dbContext;
 
        public StoreEntities Init()
        {
            return dbContext ?? (dbContext = new StoreEntities());
        }
 
        protected override void DisposeCore()
        {
            if (dbContext != null)
                dbContext.Dispose();
        }
    }

حالا زمان ایجاد Generic Interface ای به نام IRepository است که ما در آن عملیات مورد نظر و پیش فرض مان را تعریف می کنیم. ما در زیر برخی از عملیات پرکاربرد را آورده ایم، ولی شما می توانید با توجه به نیاز هایتان، آن ها را گسترش بدهید.

 IRepository.cs

public interface IRepository<T> where T : class
    {
        // Marks an entity as new
        void Add(T entity);
        // Marks an entity as modified
        void Update(T entity);
        // Marks an entity to be removed
        void Delete(T entity);
        void Delete(Expression<Func<T, bool>> where);
        // Get an entity by int id
        T GetById(int id);
        // Get an entity using delegate
        T Get(Expression<Func<T, bool>> where);
        // Gets all entities of type T
        IEnumerable<T> GetAll();
        // Gets entities using delegate
        IEnumerable<T> GetMany(Expression<Func<T, bool>> where);
    }

به خط هایی که به صورت کامنت قرار داده شده اند، دقت کنید. آن ها همان عملیات CRUD هستند. این موضوع به این معنی است که زمانی که یک repository implentation یک موجودیت را اضافه، ویرایش و یا حذف می کند، در همان لحظه اطلاعات به پایگاه داده فرستاده نمی شوند(اطلاعات در همان لحظه در پایگاه داده تغییر نخواهند کرد. در عوض، فراخوانی کننده (service layer) مسئول ارسال دستور تغییر اطلاعات در پایگاه داده است. که این کار را از طریق یک نمونه ی تزریق شده IUnitOfWOrk انجام می دهد. برای این کار ما از الگویی به نام UnitOfWork استفاده می کنیم. سپس دو فایل زیر را به پوشه ی Infrastructure  اضافه کنید. 

IUnitOfWork.cs

public interface IUnitOfWork
    {
        void Commit();
    }

UnitOfWork.cs

public class UnitOfWork : IUnitOfWork
    {
        private readonly IDbFactory dbFactory;
        private StoreEntities dbContext;
 
        public UnitOfWork(IDbFactory dbFactory)
        {
            this.dbFactory = dbFactory;
        }
 
        public StoreEntities DbContext
        {
            get { return dbContext ?? (dbContext = dbFactory.Init()); }
        }
 
        public void Commit()
        {
            DbContext.Commit();
        }
    } 

به همان روشی که ما از کلاس Disposable استفاده کردیم، می خواهیم از یک کلاس abstract  استفاده کنیم که یک پیاده سازی مجازی از اینترفیس IRepository دارد. این کلاس base (پایه) از تمام Repository ها ارث بری می کند بنابراین از اینترفیس IRepository  نیز implement خواهد کرد. کلاس زیر را اضافه کنید:

RepositoryBase.cs

public abstract class RepositoryBase<T> where T : class
    {
        #region Properties
        private StoreEntities dataContext;
        private readonly IDbSet<T> dbSet;
 
        protected IDbFactory DbFactory
        {
            get;
            private set;
        }
 
        protected StoreEntities DbContext
        {
            get { return dataContext ?? (dataContext = DbFactory.Init()); }
        }
        #endregion
 
        protected RepositoryBase(IDbFactory dbFactory)
        {
            DbFactory = dbFactory;
            dbSet = DbContext.Set<T>();
        }
 
        #region Implementation
        public virtual void Add(T entity)
        {
            dbSet.Add(entity);
        }
 
        public virtual void Update(T entity)
        {
            dbSet.Attach(entity);
            dataContext.Entry(entity).State = EntityState.Modified;
        }
 
        public virtual void Delete(T entity)
        {
            dbSet.Remove(entity);
        }
 
        public virtual void Delete(Expression<Func<T, bool>> where)
        {
            IEnumerable<T> objects = dbSet.Where<T>(where).AsEnumerable();
            foreach (T obj in objects)
                dbSet.Remove(obj);
        }
 
        public virtual T GetById(int id)
        {
            return dbSet.Find(id);
        }
 
        public virtual IEnumerable<T> GetAll()
        {
            return dbSet.ToList();
        }
 
        public virtual IEnumerable<T> GetMany(Expression<Func<T, bool>> where)
        {
            return dbSet.Where(where).ToList();
        }
 
        public T Get(Expression<Func<T, bool>> where)
        {
            return dbSet.Where(where).FirstOrDefault<T>();
        }
 
        #endregion
     
    }

به دلیل این که implementation ها از نوع virtual است، هر Repository می توانید عملیات را طبق نیازش override  کند. حالا بیایید به سراغ repository ها برویم. برای این کار، یک پوشه به نام Repositories ایجاد کنید و دو کلاس زیر را به آن اضافه کنید.

GadgetRepository.cs

public class GadgetRepository : RepositoryBase<Gadget>, IGadgetRepository
    {
        public GadgetRepository(IDbFactory dbFactory)
            : base(dbFactory) { }
    }
 
    public interface IGadgetRepository : IRepository<Gadget>
    {
 
    }

CategoryRepository.cs

public class CategoryRepository : RepositoryBase<Category>, ICategoryRepository
    {
        public CategoryRepository(IDbFactory dbFactory)
            : base(dbFactory) { }
 
        public Category GetCategoryByName(string categoryName)
        {
            var category = this.DbContext.Categories.Where(c => c.Name == categoryName).FirstOrDefault();
 
            return category;
        }
 
        public override void Update(Category entity)
        {
            entity.DateUpdated = DateTime.Now;
            base.Update(entity);
        }
    }
 
    public interface ICategoryRepository : IRepository<Category>
    {
        Category GetCategoryByName(string categoryName);
    }

می توانید ببینید که GadgetRepositoryهمه ی عملیات پیش فرض را با استفاده از رفتارهای پیش فرض پشتیبانی می کند و این خیلی خوب است. از سوی دیگر، شما ممکن است ببینید که یک Repository خاص نیاز به توسعه ی عملیات ها و یا override  کردن مقادیر پیش فرض داشته باشد. روند کار به این صورت است که شما یک Repository برای هر کدام از Model classها ایجاد می کنید، بنابراین هر repository از نوع T ، مسئول دستکاری یک DBSet خاص از طریق  DbContext.Set<T> است. کار ما با لایه ی Data Access Layer تمام شد، بنابراین می توانیم به بخش بعد برویم. 

لایه Service

نیاز دارید تا در کنترلرها چه عملیاتی انجام شود؟ Business logic در کدام بخش باید implement شود؟ درست حدس زدید، پیاده سازی این بخش در این لایه انجام می شود. یک پروژه ی class library جدید به نام Store.Service  اضافه کنید. به یاد داشته باشید که باید Refrence آن را به پروژه های دیگر اضافه کنید. توجه داشته باشید که نیازی به نصب Entity Framework در این لایه نداریم. زیرا هر دسترسی ای که لازم باشد از طریق  repository های ایجاد شده در مرحله ی قبل ، انجام می شود. بنابراین اولین سرویس خودمان را ایجاد می کنیم:

GadgetService.cs

// operations you want to expose
    public interface IGadgetService
    {
        IEnumerable<Gadget> GetGadgets();
        IEnumerable<Gadget> GetCategoryGadgets(string categoryName, string gadgetName = null);
        Gadget GetGadget(int id);
        void CreateGadget(Gadget gadget);
        void SaveGadget();
    }
 
    public class GadgetService : IGadgetService
    {
        private readonly IGadgetRepository gadgetsRepository;
        private readonly ICategoryRepository categoryRepository;
        private readonly IUnitOfWork unitOfWork;
 
        public GadgetService(IGadgetRepository gadgetsRepository, ICategoryRepository categoryRepository, IUnitOfWork unitOfWork)
        {
            this.gadgetsRepository = gadgetsRepository;
            this.categoryRepository = categoryRepository;
            this.unitOfWork = unitOfWork;
        }
 
        #region IGadgetService Members
 
        public IEnumerable<Gadget> GetGadgets()
        {
            var gadgets = gadgetsRepository.GetAll();
            return gadgets;
        }
 
        public IEnumerable<Gadget> GetCategoryGadgets(string categoryName, string gadgetName = null)
        {
            var category = categoryRepository.GetCategoryByName(categoryName);
            return category.Gadgets.Where(g => g.Name.ToLower().Contains(gadgetName.ToLower().Trim()));
        }
 
        public Gadget GetGadget(int id)
        {
            var gadget = gadgetsRepository.GetById(id);
            return gadget;
        }
 
        public void CreateGadget(Gadget gadget)
        {
            gadgetsRepository.Add(gadget);
        }
 
        public void SaveGadget()
        {
            unitOfWork.Commit();
        }
 
        #endregion
     
    }

اگر می خواستیم از یک شی gadget  در این کلاس استفاده کنیم، به کدهای زیر نیاز داشتیم:

// init a gadget object..
gadgetService.CreateGadget(gadget);
gadgetService.SaveGadget();

repositpry های لازم از طریق constructor ها تزریق خواهند شد. این کار از طریق Dependency Injection که ما آن را در start up با استفاده از فریم ورکAutofac تنظیم می کنیم، انجام خواهد شد. 

CategoryService.cs

/ operations you want to expose
   public interface ICategoryService
   {
       IEnumerable<Category> GetCategories(string name = null);
       Category GetCategory(int id);
       Category GetCategory(string name);
       void CreateCategory(Category category);
       void SaveCategory();
   }
 
   public class CategoryService : ICategoryService
   {
       private readonly ICategoryRepository categorysRepository;
       private readonly IUnitOfWork unitOfWork;
 
       public CategoryService(ICategoryRepository categorysRepository, IUnitOfWork unitOfWork)
       {
           this.categorysRepository = categorysRepository;
           this.unitOfWork = unitOfWork;
       }
 
       #region ICategoryService Members
 
       public IEnumerable<Category> GetCategories(string name = null)
       {
           if (string.IsNullOrEmpty(name))
               return categorysRepository.GetAll();
           else
               return categorysRepository.GetAll().Where(c => c.Name == name);
       }
 
       public Category GetCategory(int id)
       {
           var category = categorysRepository.GetById(id);
           return category;
       }
 
       public Category GetCategory(string name)
       {
           var category = categorysRepository.GetCategoryByName(name);
           return category;
       }
 
       public void CreateCategory(Category category)
       {
           categorysRepository.Add(category);
       }
 
       public void SaveCategory()
       {
           unitOfWork.Commit();
       }
 
       #endregion
   }

کار ما با لایه سرویس نیز به اتمام رسید. 

Presentation Layer

یک  ASP.NET Web Application جدید به نام Store.Web ایجاد نمایید. برای این پروژه از قالب Empty استفاده کنید و گزینه ی MVC  را نیز علامت بزنید. ما نیاز داریم تا refrence همه پروژه های قبل را به این پروژه بدهیم و همچنین باید Entity Framework را نیز از طریق Nuget Packages نصب کنیم. شاید این سوال برایتان پیش آمده باشد که آیا ما می خواهیم از Query های Entity  در این پروژه استفاده کنیم؟ پاسخ این سوالف منفی است. ما نیاز به برخی از فضای نام های این فریم ورک داریم تا بتوانیم پیکربندی های مربوط به پایگاه داده را در برنامه مان(مانند database Initializer) انجام بدهیم. حالا فایل Global.asax.cs را باز کنید و تکه کد زیر را در برای ایجاد seed که در پروژه ی Store.Data  ایجاد کرده ایم، وارد کنید. 

Glbal.asax.cs

protected void Application_Start()
        {
            // Init database
            System.Data.Entity.Database.SetInitializer(new StoreSeedData());
 
            AreaRegistration.RegisterAllAreas();
            RouteConfig.RegisterRoutes(RouteTable.Routes);
        }

همچنین ما نیاز داریم تا یک رشته اتصال برای ایجاد پایگاه داده مان ، بنویسیم. برای این کار، تکه کد زیر را در فایل  Web.config اضافه کنید و آن را مطابق با تنظیمات سیستم خودتان تغییر بدهید. 

<connectionStrings>
  <add name="StoreEntities" connectionString="Data Source=(localdb)\v11.0;Initial Catalog=StoreDb;Integrated Security=True" providerName="System.Data.SqlClient" />
</connectionStrings>

حالا زمان آن فرا رسیده است که از همه ی مواردی که تا به حال ساخته ایم، استفاده کنیم. در این مرحله برای این که برنامه به درستی کار کند، نیاز داریم تا Dependency Injectionرا برای برنامه تنظیم کنیم که ما در این مقاله از Autofac برای این کار استفاده می کنیم. همچنین مطمئن شوید که Autofac ASP.NET MVC 5 Integration را از طریق Nuget نصب کرده باشید. 

یک فایل Bootstrapper.c در پوشه ی Start_App ایجاد کنید و کدهای زیر را در آن وارد نمایید. 

Bootstrapper.cs

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
	
public static void Run()
        {
            SetAutofacContainer();
        }
 
        private static void SetAutofacContainer()
        {
            var builder = new ContainerBuilder();
            builder.RegisterControllers(Assembly.GetExecutingAssembly());
            builder.RegisterType<UnitOfWork>().As<IUnitOfWork>().InstancePerRequest();
            builder.RegisterType<DbFactory>().As<IDbFactory>().InstancePerRequest();
 
            // Repositories
            builder.RegisterAssemblyTypes(typeof(GadgetRepository).Assembly)
                .Where(t => t.Name.EndsWith("Repository"))
                .AsImplementedInterfaces().InstancePerRequest();
            // Services
            builder.RegisterAssemblyTypes(typeof(GadgetService).Assembly)
               .Where(t => t.Name.EndsWith("Service"))
               .AsImplementedInterfaces().InstancePerRequest();
 
            IContainer container = builder.Build();
            DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
        }
    }

حالا اولین کاری است که باید انجام بدهیم تعریف ViewModel ها و تنظیم AutoMapper برای نگاشت داده ها است. دومین کار تنظیم CSS Bootstrap در برنامه است. در اینجا روش کار ما کمی متفاوت است.

CSS Bootstrap

زمانی که شما Bootstrap را به پروژه اضافه می کنید، سه پوشه به نام های css, fonts  و js به پروژه شما اضافه خواهند شد. در پوشه ی css فایل bootstrap.css را دانلود و اضافه کنید. در پوشه ی fonts همه موارد داخل پوشه ی fonts را قرار بدهید و در پوشه ی js فقط فایل bootstrap.js را جایگذاری کنید. ما می خواهیم از Bundling and Minification برای bootstrap استفاده کنیم . مطمئن شوید که Microsoft ASP.NET Web Optimization Framework را از طریق Nuget نصب کرده باشید.

زمانی که نصب به پایان رسید، یک کلاس جدید به نام BundleConfig در پوشه ی App_Start به صورت زیر ایجاد نمایید:

BundleConfig.cs

public class BundleConfig
    {
        public static void RegisterBundles(BundleCollection bundles)
        {
            bundles.Add(new ScriptBundle("~/bootstrap/js").Include("~/js/bootstrap.js", "~/js/site.js"));
            bundles.Add(new StyleBundle("~/bootstrap/css").Include("~/css/bootstrap.css", "~/css/site.css"));
 
            BundleTable.EnableOptimizations = true;
        }
    }

حالا تکه کدهای زیر را برای استفاده از bundling and minication در فایل Global.asax.c وارد می کنیم:

Global.asax.cs

protected void Application_Start()
        {
            // Init database
            System.Data.Entity.Database.SetInitializer(new StoreSeedData());
 
            AreaRegistration.RegisterAllAreas();
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);
 
            // Autofac and Automapper configurations
            Bootstrapper.Run();
        }

توجه داشته باشید که ما تابع Bootstrapper.Run() را فراخوانی کرده ایم که تنظیمات Autofac را انجام بدهد. این تابع، همچنین تنظیمات Automapper را نیز انجام خواهد داد. ما به یک Layout  برای برنامه مان احتیاج داریم، بنابراین به یک پوشه در پوشه ی Views  به نام Shared  ایجاد کنید و یک فایل از نوع MVC 5 Layout Page به نام _Layout.cshtml ایجاد نمایید. 

_Layout.cshtml

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>@ViewBag.Title</title>
    <!-- Bootstrap -->
    @Styles.Render("~/bootstrap/css")
    <!--[if lt IE 9]>
    <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/
    html5shiv.js"></script>
    <script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/
    respond.min.js"></script>
    <![endif]-->
</head>
<body>
    <nav id="myNavbar" class="navbar navbar-default navbar-inverse navbar-fixed-top" role="navigation">
        <!-- Brand and toggle get grouped for better mobile display -->
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#navbarCollapse">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                @Html.ActionLink("Store", "Index", "Home", new { }, new { @class = "navbar-brand" })
            </div>
            <!-- Collect the nav links, forms, and other content for toggling -->
            <div class="collapse navbar-collapse" id="navbarCollapse">
                <ul class="nav navbar-nav">
                    <li class="active">
                        @Html.ActionLink("Tablets", "Index", "Home", new { category = "Tablets" }, null)
                    </li>
                    <li class="active">
                        @Html.ActionLink("Laptops", "Index", "Home", new { category = "Laptops" }, null)
                    </li>
                    <li class="active">
                        @Html.ActionLink("Mobiles", "Index", "Home", new { category = "Mobiles" }, null)
                    </li>
                </ul>
            </div>
    </nav>
    @RenderBody()
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
    @Scripts.Render("~/bootstrap/js")
</body>
</html>

اگر با پیغام خطایی مواجه شدید که Razor syntax قادر به شناسایی نیست، باید کدهای زیر را در فایل web.config موجود در پوشه ی Views  اضافه کنید. 

web.config

<namespaces>
        <add namespace="System.Web.Mvc" />
        <add namespace="System.Web.Mvc.Ajax" />
        <add namespace="System.Web.Mvc.Html" />
        <add namespace="System.Web.Routing" />
        <add namespace="Store.Web" />
        <add namespace="System.Web.Optimization" />
      </namespaces>

Automapper

در یک برنامه واقعی، احتمالا ما شی هایی داریم که حاوی Property های زیادی هستند ولی ما می خواهیم فقط بخشی از آن ها را نمایش بدهیم و یا در هنگام ارسال اطلاعات به سرور، ما نیاز داریم تا فقط برخی از Property ها را بفرستیم. برای این گونه موارد از شی های ViewModel استفاده می کنیم و آن ها را به جای شی های واقعی مورد استفاده قرار می دهیم. برای این کار ابتدا مطمئن شوید که Auto Mapper را از طریق Nuget نصب کرده باشید. 

یک پوشه ی جدید به نام ViewModels با کلاس های زیر ایجاد نمایید.

GadgetViewModel.cs

public class GadgetViewModel
    {
        public int GadgetID { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public decimal Price { get; set; }
        public string Image { get; set; }
 
        public int CategoryID { get; set; }
    }

CategoryViewModel.cs

public class CategoryViewModel
    {
        public int CategoryID { get; set; }
        public string Name { get; set; }
 
        public List<GadgetViewModel> Gadgets { get; set; }
    }

GadgetFormViewModel.cs

public class GadgetFormViewModel
    {
        public HttpPostedFileBase File { get; set; }
        public string GadgetTitle { get; set; }
        public string GadgetDescription { get; set; }
        public decimal GadgetPrice { get; set; }
        public int GadgetCategory { get; set; }
    }

اگر کلاس های View Model  شما Property هایی همنام با شی های اصلی داشته باشند، AutoMapper آن ها را تشخیص می دهد و آن ها را به صورت پیش فرض مانند همان شی ها مدیریت خواهد کرد. در غیر اینصورت، باید عملیات نگاشت را به صورت دستی انجام بدهید. ما می توانیم از کلمه ی Form قبل از واژه ViewModel استفاده کنیم که به این ترتیب نشان داده شود این View Model از طریق عنصر Formبه سمت سرور فرستاده می شود. حالا بیایید پیکربندی های مربوط به نگاشت ها را انجام بدهیم. یک پوشه به نام Mappings اضافه کنید و کلاس زیر را در آن وارد نمایید. 

AutoMapperConfiguration.cs

public class AutoMapperConfiguration
    {
        public static void Configure()
        {
            Mapper.Initialize(x =>
            {
                x.AddProfile<DomainToViewModelMappingProfile>();
                x.AddProfile<ViewModelToDomainMappingProfile>();
            });
        }
    }

ما هنوز Profile مورد نیاز را ایجاد نکرده ایم. نکته ای که در اینجا مورد توجه است این است که ما می توانیم چندین AutoMapper Profile ایجاد کنیم و سپس آن ها را به تابع Mapper.Initialize اضافه کنیم. ما در این مقاله دو profile تعریف می کنیم. دو کلاس زیر را به همان پوشه اضافه نمایید. 

DomainToViewModelMappingProfile.cs

public class DomainToViewModelMappingProfile : Profile
    {
        public override string ProfileName
        {
            get { return "DomainToViewModelMappings"; }
        }
 
        protected override void Configure()
        {
            Mapper.CreateMap<Category,CategoryViewModel>();
            Mapper.CreateMap<Gadget, GadgetViewModel>();
        }
    }

ViewModelToDomainMappingProfile.cs

public class ViewModelToDomainMappingProfile : Profile
    {
        public override string ProfileName
        {
            get { return "ViewModelToDomainMappings"; }
        }
 
        protected override void Configure()
        {
            Mapper.CreateMap<GadgetFormViewModel, Gadget>()
                .ForMember(g => g.Name, map => map.MapFrom(vm => vm.GadgetTitle))
                .ForMember(g => g.Description, map => map.MapFrom(vm => vm.GadgetDescription))
                .ForMember(g => g.Price, map => map.MapFrom(vm => vm.GadgetPrice))
                .ForMember(g => g.Image, map => map.MapFrom(vm => vm.File.FileName))
                .ForMember(g => g.CategoryID, map => map.MapFrom(vm => vm.GadgetCategory));
        }
    }

برای نگاشت GadgetFormViewModel -> Gadget نیاز به تنظیمات دستی داریم که طریقه آن در بالا گفته شد. تنها نکته ای که برای تمام کردن کار با Automapper  لازم است رعایت کنیم، اضافه کد خط زیر به کلاسBootstrapper  است.

 Bootsrapper.cs

public static class Bootstrapper
    {
        public static void Run()
        {
            SetAutofacContainer();
            //Configure AutoMapper
            AutoMapperConfiguration.Configure();
        }
// Code ommitted 

Controller ها و View ها

ما تقریبا کارمان به اتمام رسیده است. یک MVC Controller جدید به نام HomeController ایجاد کنید و کدهای زیر را در داخل آن بنویسید. 

HomeController.cs

public class HomeController : Controller
    {
        private readonly ICategoryService categoryService;
        private readonly IGadgetService gadgetService;
 
        public HomeController(ICategoryService categoryService, IGadgetService gadgetService)
        {
            this.categoryService = categoryService;
            this.gadgetService = gadgetService;
        }
 
        // GET: Home
        public ActionResult Index(string category = null)
        {
            IEnumerable<CategoryViewModel> viewModelGadgets;
            IEnumerable<Category> categories;
 
            categories = categoryService.GetCategories(category).ToList();
 
            viewModelGadgets = Mapper.Map<IEnumerable<Category>, IEnumerable<CategoryViewModel>>(categories);
            return View(viewModelGadgets);
        }
    }

service  ها از طریق controller  برای هر درخواست ، تزریق می شوند و داده های آن ها قبل از ارسال به سمت Client ، به View model ها نگاشت می شوند. بر روی  Index action کلیک راست نمایید و یک view به نام Index  با کدهای زیر ایجاد کنید. 

Views/Home/Index.cshtml

@model IEnumerable<Store.Web.ViewModels.CategoryViewModel>
 
@{
    ViewBag.Title = "Store";
    Layout = "~/Views/Shared/_Layout.cshtml";
}
 
<p>
 
</p>
<div class="container">
    <div class="jumbotron">
 
        @foreach (var item in Model)
        {
            <div class="panel panel-default">
                <div class="panel-heading">
                    @*@Html.DisplayFor(modelItem => item.Name)*@
                    @Html.ActionLink("View all " + item.Name, "Index", new { category = item.Name }, new { @class = "pull-right" })
                    @using (Html.BeginForm("Filter", "Home", new { category = item.Name }, FormMethod.Post, new { @class = "navbar-form" }))
                    {
                        @Html.TextBox("gadgetName", null, new { @class = "form-control", placeholder = "Search in " + item.Name })
                    }
 
 
                </div>
                @foreach (var gadget in item.Gadgets)
                {
                    @Html.Partial("Gadget", gadget)
                }
                <div class="panel-footer">
                    @using (Html.BeginForm("Create", "Home", FormMethod.Post,
                            new { enctype = "multipart/form-data", @class = "form-inline" }))
                    {
                        @Html.Hidden("GadgetCategory", item.CategoryID)
                        <div class="form-group">
                            <label class="sr-only" for="file">File</label>
                            <input type="file" class="form-control" name="file" placeholder="Select picture..">
                        </div>
                        <div class="form-group">
                            <label class="sr-only" for="gadgetTitle">Title</label>
                            <input type="text" class="form-control" name="gadgetTitle" placeholder="Title">
                        </div>
                        <div class="form-group">
                            <label class="sr-only" for="gadgetName">Price</label>
                            <input type="number" class="form-control" name="gadgetPrice" placeholder="Price">
                        </div>
                        <div class="form-group">
                            <label class="sr-only" for="gadgetName">Description</label>
                            <input type="text" class="form-control" name="gadgetDescription" placeholder="Description">
                        </div>
                        <button type="submit" class="btn btn-primary">Upload</button>
                    }
                </div>
            </div>
        }
 
    </div>
 
</div>

دو نکته باید در اینجا مورد توجه قرار بگیرند،اولی این است که باید یک Partial View برای نمایش شی Gadget View Model بسازیم و دومین نکته درباره ی نام های عناصر استفاده شده در کنترل Form است. در زیر پوشه ی Shared  یک Partial View برای نمایش GadgetViewModel ایجاد می کنیم.

Views/Shared/Gadget.cshtml

@model Store.Web.ViewModels.GadgetViewModel
 
<div class="panel-body">
    <div class="media">
        <a class="pull-left" href="#">
            <img class="media-object" src="../../images/@Model.Image" />
        </a>
        <div class="media-body">
            <h3 class="media-heading">
                @Model.Name
            </h3>
            <p>@Model.Description</p>
        </div>
    </div>
</div>

در صفحه ی Index.cshtml ما از توابع search و  filter  و Create gadget نیز استفاده کرده ایم. برای استفاده از آن ها باید Action methode های زیر را درون HomeController اضافه کنید.

HomeController.cs

public ActionResult Filter(string category, string gadgetName)
        {
            IEnumerable<GadgetViewModel> viewModelGadgets;
            IEnumerable<Gadget> gadgets;
 
            gadgets = gadgetService.GetCategoryGadgets(category, gadgetName);
 
            viewModelGadgets = Mapper.Map<IEnumerable<Gadget>, IEnumerable<GadgetViewModel>>(gadgets);
 
            return View(viewModelGadgets);
        }
 
        [HttpPost]
        public ActionResult Create(GadgetFormViewModel newGadget)
        {
            if (newGadget != null && newGadget.File != null)
            {
                var gadget = Mapper.Map<GadgetFormViewModel, Gadget>(newGadget);
                gadgetService.CreateGadget(gadget);
 
                string gadgetPicture = System.IO.Path.GetFileName(newGadget.File.FileName);
                string path = System.IO.Path.Combine(Server.MapPath("~/images/"), gadgetPicture);
                newGadget.File.SaveAs(path);
 
                gadgetService.SaveGadget();
            }
 
            var category = categoryService.GetCategory(newGadget.GadgetCategory);
            return RedirectToAction("Index", new { category = category.Name });
        }

در این مرحله ما نیاز داریم تا یک صفحه ی Filter ایجاد کنیم. برای این کار بر روی Filter action کلیک راست کنید و View زیر را ایجاد کنید. 

Home/Views/Filter.cshtml

@model IEnumerable<Store.Web.ViewModels.GadgetViewModel>
 
@{
    ViewBag.Title = "Filter";
    Layout = "~/Views/Shared/_Layout.cshtml";
}
 
<div class="container">
    <div class="jumbotron">
 
        @foreach (var item in Model)
        {
            <div class="panel panel-default">
                <div class="panel-heading">
                    @Html.Label(item.Name)
                </div>
                @Html.Partial("Gadget", item)
            </div>
        }
 
    </div>
</div>

شما می توانید Gadget  ها را بر اساس یک نوع خاص دسته بندی و یا جستجو کنید. کار ما به اتمام رسید،

آموزش asp.net mvc

فایل های ضمیمه

برنامه نویسان

نویسنده 3355 مقاله در برنامه نویسان

کاربرانی که از نویسنده این مقاله تشکر کرده اند

در صورتی که در رابطه با این مقاله سوالی دارید، در تاپیک های انجمن مطرح کنید