2017年4月8日 星期六

將資料存放到 Seesion, 客製化 ModelBinder 來存取

假設有一個購物網站有一個CartController控制器使用Session存取Cart物件的實例
public class CartController : Controller
{
    private IProductRepository repository;
 
    public CartController(IProductRepository repo)
    {
        this.repository = repo;
    }
 
    public RedirectToRouteResult AddToCart(int productId, string returnUrl)
    {
        Product product = repository.Products.FirstOrDefault(p => p.ProductId == productId);
        if (product != null)
        {
            GetCart().AddItem(product, 1);
        }
        return RedirectToAction("Index"new { returnUrl });
    }
 
    public RedirectToRouteResult RemoveFromCart(int productId, string returnUrl)
    {
        Product product = repository.Products.FirstOrDefault(p => p.ProductId == productId);
        if (product != null)
        {
            GetCart().RemoveItem(product);
        }
        return RedirectToAction("Index"new { returnUrl });
    }
 
    private Cart GetCart()
    {
        Cart cart = (Cart)Session["Cart"];
        if (cart == null)
        {
            cart = new Cart();
            Session["Cart"= cart;
        }
        return cart;
    }
}

現在要自定義一個 ModelBinder 來獲取 Session 數據中的 Cart 實例,通過實作 System.Web.Mvc.IModelBinder 介面
public class CartModelBinder : IModelBinder
{
    private const string sessionKey = "Cart";
 
    public object BindModel(
        ControllerContext controllerContext,
        ModelBindingContext bindingContext)
    {
        // 通過 session 取得 Cart
        Cart cart = null;
        if (controllerContext.HttpContext.Session != null)
        {
            cart = (Cart)controllerContext.HttpContext.Session[sessionKey];
        }
 
        // 若 session 中沒有 Cart, 則創建一個
        if (cart == null)
        {
            cart = new Cart();
            if (controllerContext.HttpContext.Session != null)
            {
                controllerContext.HttpContext.Session[sessionKey] = cart;
            }
        }
        return cart;
    }
}

需要告訴 MVC 框架,使用 CartModelBinder 來創建 Cart 實例,在Global.asax 的 Application_Start 方法中設置
public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();
        RouteConfig.RegisterRoutes(RouteTable.Routes);
 
        // 告訴 MVC 框架使用 CartModelBinder 類來創建 Cart 實例
        ModelBinders.Binders.Add(typeof(Cart), new CartModelBinder());
    }
}

現在更新 CartController,刪除 GetCart 方法,依靠 CartModelBinder  為 CartController 提供 Cart 物件
public class CartController : Controller
{
    private IProductRepository repository;
 
    public CartController(IProductRepository repo)
    {
        this.repository = repo;
    }
 
    public RedirectToRouteResult AddToCart(Cart cart, int productId, string returnUrl)
    {
        Product product = repository.Products.FirstOrDefault(p => p.ProductId == productId);
        if (product != null)
        {
            cart.AddItem(product, 1);
        }
        return RedirectToAction("Index"new { returnUrl });
    }
 
    public RedirectToRouteResult RemoveFromCart(Cart cart, int productId, string returnUrl)
    {
        Product product = repository.Products.FirstOrDefault(p => p.ProductId == productId);
        if (product != null)
        {
            cart.RemoveItem(product);
        }
        return RedirectToAction("Index"new { returnUrl });
    }
}

用來創建 Cart 物件與 CartController 的邏輯方離開來了,這樣開發者能夠修改存取 Cart 物件的方法而不需要修改 CartController,可以方便對 CartController 做單元測試而不需要模仿大量的 ASP.NET 通道。
[TestMethod]
public void Can_Add_To_Cart()
{
    // Arrange
    Mock<IProductRepository> mock = new Mock<IProductRepository>();
    mock.Setup(m => m.Products).Returns(new Product[]
    {
        new Product{Id=1,Name="P1",Category = "Apples"},
    }.AsQueryable());
 
    Cart cart = new Cart();
 
    CartController target = new CartController(mock.Object);
 
    // Act
    target.AddToCart(cart, 1null);
 
    // Asert
    Assert.AreEqual(1, cart.Products.Count(), 1);
    Assert.AreEqual(1, cart.Products.ToArray()[0].Id);
}

沒有留言: