精通 ASP.NET MVC 4 学习笔记(一)

时间:2022-12-31 20:01:48

这里记录着从 P132 到 P192 的内容。水分很足,大部分是书上的代码,我只加了一些基于我自己的理解的能帮助初学者看懂的注释,并且把书中的部分内容做了一些的拓展。

建立数据层

设置 DI 容器

/// <summary>
    /// 设置 DI 容器
    /// </summary>
    /// <seealso cref="System.Web.Mvc.DefaultControllerFactory" />
    public class NinjectControllerFactory : DefaultControllerFactory
    {
        private IKernel ninjectKernel;

        //添加依赖绑定规则
        private void AddBindings()
        {
            //put bindings here

            //将所有对 IProductsRepository 接口的实现替换为对 EFProductRepository 类的实现!!!
            ninjectKernel.Bind<IProductsRepository>().To<EFProductRepository>();
        }

        //必要的
        public NinjectControllerFactory()
        {
            ninjectKernel = new StandardKernel();
            AddBindings();
        }

        protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
        {
            return controllerType == null ? null : (IController)ninjectKernel.Get(controllerType);
        }
    }

这个NinjectControllerFactory是一个继承于DefaultControllerFactory的自定义的依赖性解释器,这样,我们的就可以手动的控制查找控制器的方式。这里,我们把所有对 IProductsRepository 接口的实现替换为对 EFProductRepository 类的实现。
然后,在我们的应用中的 Global.asax.cs 文件中的Application_Start方法里面注册一下自定义的依懒性解析器:

ControllerBuilder.Current.SetControllerFactory(new NinjectControllerFactory());

现在,我们接着看看与IProductsRepository相关的东西,首先是这个这个接口的内容:

/// <summary>
/// 通过存储库模式实现持久化逻辑与域模型实体的分离。
/// </summary>
public interface IProductsRepository
{
    IQueryable<Product> Products { get; }
}

Products 属性存着来自数据库的 Product 表内容。

使用 EF 连接到数据库

然后我们再看看EFProductRepository的内容。

/// <summary>
/// 通过实现 IProductsRepository 接口实现的存储库类。
/// </summary>
/// <seealso cref="SportsStore.Domain.Abstract.IProductsRepository" />
public class EFProductRepository : IProductsRepository
{
    /// <summary>
    /// 用来定义从实体对象到数据库的映射。
    /// </summary>
    private EFDbContext context = new EFDbContext();

    public IQueryable<Product> Products
    {
        get { return context.Products; }
    }
}

这就是存储库类,他实现了IProductsRepository接口,而且用了一个EFDbContext实例,便于 Entity Framework 接收数据库的数据,然后通过Products属性以只读的方式访问数据库实体。

然后让我们看看EFDbContext的内容:

public class EFDbContext : DbContext
{
    /// <summary>
    /// 表示用 Product 模型类型来表示 Products 表中的每一行。
    /// </summary>
    /// <value>
    /// Products 表中所有行的集合。
    /// </value>
    public DbSet<Product> Products { get; set; }
}

它继承于DbContext,定义了从实体对象到数据库的映射。除此之外,他还提供了如下的服务:

  • 跟踪已经检索到的实体对象,如果再次查询该对象,就直接从对象上下文中提取它。
  • 保存实体的状态信息。可以获得已添加、修改和删除对象的信息。
  • 更新对象上下文中的实体,把改变的内容写入底层的存储器中。

然后他还有一个DataSet属性,DataSet表示上下文中给定类型的所有实体的集合或可从数据库中查询的给定类型的所有实体的集合,基本上就是一个存在于内存的数据库,其中包含了所有表、关系和约束。

仅仅是这么做是不够的,我们还需要告诉 Entity Framework 如何连接到数据库,我们要在 Web.config 文件中添加一条连接字符串,就像在 WebForms 网站中做的一样:

<connectionStrings>
    <add name="EFDbContext" connectionString="Data Source=DESKTOP-M31J37E;Initial Catalog=SportsStore;Integrated Security=True" providerName="System.Data.SqlClient" />
 </connectionStrings>

添加控制器

ProductController

首先是书中最先添加的 ProductController

public class ProductController : Controller
    {
        private IProductsRepository repository;
        public int PageSize = 4;

        public ProductController(IProductsRepository productReposotory)
        {
            this.repository = productReposotory;
        }

        /// <summary>
        /// 显示指定目录、指定页面的商品。
        /// </summary>
        /// <param name="category">The category.</param>
        /// <param name="page">The page.</param>
        /// <returns>返回一个 ProductsListViewModel 模型的视图</returns>
        public ViewResult List(string category, int page = 1)
        {
            ProductsListViewModel model = new ProductsListViewModel
            {
                Products = repository.Products
                .Where(p => category == null || p.Category == category)
                .OrderBy(p => p.ProductID)
                .Skip((page - 1) * PageSize)
                .Take(PageSize),
                PagingInformation = new PagingInfo
                {
                    CurrentPage = page,
                    ItemsPerPage = PageSize,
                    TotalItems = category == null ? repository.Products.Count() :
                        repository.Products.Where(e => e.Category == category).Count()
                },
                CurrentCategory = category
            };
            return View(model);
        }
    }

这里需要关注的是这个控制器的构造函数,他需要一个商品存储库类型的参数,这个参数将由我们之前设置的 DI 容器自动注入,也就是一个被映射有数据库内容的EFProductRepository对象,这样,我们的控制器就可以获取到数据库的内容了。
然后List方法中,创建了一个ProductsListViewModel,其中包含了要被显示出来的商品的集合、页面的分页信息跟当前显示的分类信息。下面是这个视图模型的具体内容:

public class ProductsListViewModel
    {
        public IEnumerable<Product> Products { get; set; }
        public PagingInfo PagingInformation { get; set; }
        public string CurrentCategory { get; set; }
    }

由于这个模型与业务中的域模型并没有什么关系,所以他被定义在 WebUI.cs 中。

接下来是NavController,用来给用户提供一个分类显示商品的方法,在页面中的体现就是导航栏。
为了实现这个功能,我们使用了 MVC 框架中的“子动作”,这依赖于一个叫做“RenderAction”的Html辅助器的方法,他可以让用户在当前视图中包含任意一个动作方法的输出(不是以iframe的形式)。
下面是这个控制器的内容:

public class NavController : Controller
    {
        private IProductsRepository respository;

        public NavController(IProductsRepository repo)
        {
            respository = repo;
        }

        public PartialViewResult Menu(string category = null)
        {
            ViewBag.SelectedCategory = category;
            IEnumerable<string> categories = respository.Products
                .Select(x => x.Category)
                .Distinct()
                .OrderBy(x => x);
            return PartialView(categories);
        }
    }

这个控制器的构造函数也是跟ProductController.List的构造函数一样,由 DI 来为他们注入参数。
Menu方法返回一个分部视图,这样可以让视图片段跨视图重用,有助于减少重复,分部视图在渲染的时候只是生成 HTML 片段。

为了能够完整的把 MVC 的思想体现出来,接下来将会介绍与上面两个控制器相关的视图。

视图

List

@model SportsStore.WebUI.Models.ProductsListViewModel

@{
    ViewBag.Title = "Products";
}

@foreach (var p in Model.Products)
{
    Html.RenderPartial("ProductSummary", p);
}

<div class="pager">
    @Html.PageLinks(Model.PagingInformation, x => Url.Action("List", new { page = x, category = Model.CurrentCategory }))
</div>

这是一个强类型的视图,与List方法使用一致的模型。
首先我们需要关注的是第9行的代码,这里使用了RenderPartialHTML辅助器,它会直接将内容写入当前页面,不返回任何值。在这里,我们使用了参数为(视图名,模型)的重载。其中ProductSummary是一个分部视图,其中的内容暂且按下不表。

另一个需要关注的地方是第13行我们自定义的Html辅助器,他的内容如下:

public static class PagingHelpers
    {
        /// <summary>
        /// Build "a" tag to navigate from page to others.
        /// </summary>
        /// <param name="html">The HTML Helper.</param>
        /// <param name="pageInfo">The page information.</param>
        /// <param name="pageUrl">The delegate that can create page URL for paging.</param>
        /// <returns>The Html string.</returns>
        public static MvcHtmlString PageLinks(this HtmlHelper html, PagingInfo pageInfo, Func<int, string> pageUrl)
        {
            StringBuilder result = new StringBuilder();
            for (int i = 1; i <= pageInfo.TotalPages; i++)
            {
                TagBuilder tag = new TagBuilder("a");
                tag.MergeAttribute("href", pageUrl(i));
                tag.InnerHtml = i.ToString();
                if (i == pageInfo.CurrentPage)
                {
                    tag.AddCssClass("selected");
                }
                result.Append(tag.ToString());
            }
            return MvcHtmlString.Create(result.ToString());
        }
    }

PageLinks是一个拓展方法,他接受一个页面信息参数跟一个用来生成 Url 的函数参数用来生成 HTML 代码。这个函数参数调用的是Url.Action方法,他可以根据动作方法与数据模型生成指定的 Url 。

这是与NavController.Menu方法相关的视图,他的内容如下:

@model IEnumerable<string>

@Html.ActionLink("Home", "List", "Product")

@foreach (var link in Model)
{
    @Html.RouteLink(link, new
{
    controller = "Product",
    action = "List",
    category = link,
    page = 1
},
    new { @class = link == ViewBag.SelectedCategory ? "selected" : null }
)
}

这也是一个强类型视图,不过在创建这个视图的时候,VS2015并不能让我直接从弹出的对话框里面指定我想要的模型,所以我是先创建空模型的视图,然后手动在源码里面添加模型的。这个模型里面包含着所有分类的名称,然后使用RouteLink方法生成对应的a标签,其中,第一个参数是a标签的内容,第二个参数是包含有路由信息的属性,通过检查对象的属性,利用反射检索参数。该对象通常是使用对象初始值设定项语法创建的。第三个参数是一个包含要添加给标签属性的对象,在这里,我们使用@class来给标签元素添加一个 class 。

路由

至此,我们的网站已经有了最基本的功能,但是,页面的 url 却很不美观,这里,我们就需要修改 RouteConfig.cs 里面的内容来产生美观的 url 。修改后的内容如下:

public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute
            (
                name: null,
                url: "",
                defaults: new { controller = "Product", action = "List", category = (string)null, page = 1 }
            );
            routes.MapRoute
            (
                name: null,
                url: "Page{page}",
                defaults: new { Controller = "Product", action = "List", category = (string)null },
                constraints: new { page = @"\d+" }
            );
            routes.MapRoute
            (
                name: null,
                url: "{category}",
                defaults: new { Controller = "Product", action = "List", page = 1 }
            );

            routes.MapRoute
            (
                name: null,
                url: "{category}/Page{page}",
                defaults: new { Controller = "Product", action = "List" },
                constraints: new { page = @"\d+" }//约束
            );

            routes.MapRoute(null, "{controller}/{action}");
        }
    }

这里面,主要需要注意的是 MapRoute 方法的调用。路由的是按照他定义的顺序来运用的,如果改变这种顺序会出现奇怪的效果。
现在我来解释一下这个方法的几个参数的含义:

  • name:表示路由的名称,目前我感觉木啥卵用
  • url:表示路由的格式,大括号内的内容是用来匹配实际 url 的
  • defaults:一个包含了控制器,动作方法的对象,动作方法参数的对象
  • constrains:用来约束上面查询字符串的正则表达式

具体是如何实现的,等我去研究一下。。。