在这一部分,我们继续完善我们的购物车,我们要做以下一些事情:
1:完成 shoppingcart.cshtml;
2:让用户可以更新数量及从购物车删除商品;
3:创建一个 widget,在上面可以看到商品数量,并且能链接到购物车;
同时,我们会接触到以下技术点:
1:熟悉 IContentManager.GetItemMetadata;
2:通过 IResourceManifestProvider 来包含 resources;
3:使用 KnockoutJS and jQuery,并且应用 MVVM。
一:完善 shoppingcart.cshtml
@{
Style.Require("TMinji.Shop.ShoppingCart");
}
<article class="shoppingcart">
<table>
<thead>
<tr>
<td>Article</td>
<td class="numeric">Quantity</td>
<td class="numeric">Price</td>
<td></td>
</tr>
</thead>
<tbody>
@for (var i = 0; i < 5; i++) {
<tr>
<td>Product title</td>
<td class="numeric"><input type="number" value="1" /></td>
<td class="numeric">$9.99</td>
<td><a class="icon delete" href="#"></a></td>
</tr>
}</tbody>
<tfoot>
<tr class="separator"><td colspan="4"> </td></tr>
<tr>
<td class="numeric label" colspan="2">VAT (19%):</td>
<td class="numeric">$9.99</td>
<td></td>
</tr>
<tr>
<td class="numeric label" colspan="2">Total:</td>
<td class="numeric">$9.99</td>
<td></td>
</tr>
</tfoot>
</table>
<footer>
<div class="group">
<div class="align left"><a class="button" href="#">Continue shopping</a></div>
<div class="align right"><a class="button next" href="#">Proceed to checkout</a></div>
</div>
</footer>
</article>
在上面代码的第一行,我们看到了 Style,这是 Orchard.Mvc.ViewEngines.Razor.WebViewPage<T> 这个类的一个属性,Require 方法参数指定了资源的名字,该资源我们需要 resource manifest 来进行定义,而这个 resource manifest 实际就是一个类型,它实现了 IManifestResourceProvider 接口,如下:
using Orchard.UI.Resources;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace TMinji.Shop
{
public class ResourceManifest : IResourceManifestProvider
{
public void BuildManifests(ResourceManifestBuilder builder)
{
// Create and add a new manifest
var manifest = builder.Add();// Define a "common" style sheet
manifest.DefineStyle("TMinji.Shop.Common").SetUrl("common.css");// Define the "shoppingcart" style sheet
manifest.DefineStyle("TMinji.Shop.ShoppingCart").SetUrl("shoppingcart.css").SetDependencies("TMinji.Shop.Common");
}
}}
现在,创建 Styles 文件夹,并创建 common.css:
.group .align.left {
float: left;
}
.group .align.right {
float: right;
}
.icon {
display: inline-block;
16px;
height: 16px;
background: url("../images/sprites.png");
}
.icon.edit {
background-position: -8px -40px;
}
.icon.edit:hover {
background-position: -40px -40px;
}
.icon.delete {
background-position: -8px -7px;
}
.icon.delete:hover {
background-position: -39px -7px;
}
以及 shoppingcart.css:
article.shoppingcart {
500px;
}
article.shoppingcart table {
100%;
}
article.shoppingcart td {
padding: 7px 3px 4px 4px;
}
article.shoppingcart table thead td {
background: #f6f6f6;
font-weight: bold;
}
article.shoppingcart table tfoot tr.separator td {
border-bottom: 1px solid #ccc;
}
article.shoppingcart table tfoot td {
font-weight: bold;
}
article.shoppingcart footer {
margin-top: 20px;
}
article.shoppingcart td.numeric {
75px;
text-align: right;
}
article.shoppingcart td.numeric input {
50px;
}
现在,我们创建 css 中使用到的图片,让我们创建 Images 文件夹,并添加 sprites.png,
注意哦,如果这个时候我们运行带来,会看到 css 并没有呈现出来,这是因为,orchard 接管了所有文件的 handle,我们需要在 Images 和 Styles 文件下放置 web.config,让 Orchard 不要处理 static 文件,如下:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appSettings>
<add key="webpages:Enabled" value="false" />
</appSettings>
<system.web>
<httpHandlers>
<!-- iis6 - for any request in this location, return via managed static file handler -->
<add path="*" verb="*" type="System.Web.StaticFileHandler" />
</httpHandlers>
</system.web>
<system.webServer>
<handlers accessPolicy="Script,Read">
<!--
iis7 - for any request to a file exists on disk, return it via native http module.
accessPolicy 'Script' is to allow for a managed 404 page.
-->
<add name="StaticFile" path="*" verb="*" modules="StaticFileModule" preCondition="integratedMode" resourceType="File" requireAccess="Read" />
</handlers>
</system.webServer>
</configuration>
现在,看到效果:
现在,我们修改控制器,如下:
using Orchard;
using System;
using Orchard.Mvc;
using Orchard.Themes;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web.Mvc;
using TMinji.Shop.Services;
using TMinji.Shop.Models;
using Orchard.ContentManagement;namespace TMinji.Shop.Controllers
{
public class ShoppingCartController : Controller
{private readonly IShoppingCart _shoppingCart;
private readonly IOrchardServices _services;public ShoppingCartController(IShoppingCart shoppingCart, IOrchardServices services)
{
_shoppingCart = shoppingCart;
_services = services;
}[HttpPost]
public ActionResult Add(int id)
{
// Add the specified content id to the shopping cart with a quantity of 1.
_shoppingCart.Add(id, 1);// Redirect the user to the Index action (yet to be created)
return RedirectToAction("Index");
}[Themed]
public ActionResult Index()
{
// Create a new shape using the "New" property of IOrchardServices.
var shape = _services.New.ShoppingCart();//// Create a LINQ query that projects all items in the shoppingcart into shapes
//var query = from item in _shoppingCart.Items
// let product = _shoppingCart.GetProduct(item.ProductId)
// select _services.New.ShoppingCartItem(
// Product: product,
// Quantity: item.Quantity
// );
// Get a list of all product IDs from the shopping cart
var ids = _shoppingCart.Items.Select(x => x.ProductId).ToList();// Load all product parts by the list of IDs
var productParts = _services.ContentManager.GetMany<ProductPart>(ids, VersionOptions.Latest, QueryHints.Empty).ToArray();// Create a LINQ query that projects all items in the shoppingcart into shapes
var query = from item in _shoppingCart.Items
from productPart in productParts
where productPart.Id == item.ProductId
select _services.New.ShoppingCartItem(
Product: productPart,
Quantity: item.Quantity
);// Execute the LINQ query and store the results on a property of the shape
shape.Products = query.ToList();// Store the grand total, sub total and VAT of the shopping cart in a property on the shape
shape.Total = _shoppingCart.Total();
shape.Subtotal = _shoppingCart.Subtotal();
shape.Vat = _shoppingCart.Vat();// Return a ShapeResult
return new ShapeResult(this, shape);}
}
}
理论上,就可以在前台展示数据了。
当然,上面代码不完美,因为把业务逻辑放到控制器方法了,所以,我们不妨先重构一下,首先,增加实体类 ProductQuantity:
public sealed class ProductQuantity
{
public ProductPart ProductPart { get; set; }
public int Quantity { get; set; }
}
其次,修改我们的服务接口,增加一个 GetProducts 方法,如下:
public interface IShoppingCart : IDependency
{
IEnumerable<ShoppingCartItem> Items { get; }
void Add(int productId, int quantity = 1);
void Remove(int productId);
ProductPart GetProduct(int productId);
IEnumerable<ProductQuantity> GetProducts();
decimal Subtotal();
decimal Vat();
decimal Total();
int ItemCount();
}
然后,实现之:
public IEnumerable<ProductQuantity> GetProducts()
{
// Get a list of all product IDs from the shopping cart
var ids = Items.Select(x => x.ProductId).ToList();// Load all product parts by the list of IDs
var productParts = _contentManager.GetMany<ProductPart>(ids, VersionOptions.Latest, QueryHints.Empty).ToArray();// Create a LINQ query that projects all items in the shoppingcart into shapes
var query = from item in Items
from productPart in productParts
where productPart.Id == item.ProductId
select new ProductQuantity
{
ProductPart = productPart,
Quantity = item.Quantity
};return query;
}
修改控制器方法,如下:
[Themed]
public ActionResult Index()
{// Create a new shape using the "New" property of IOrchardServices.
var shape = _services.New.ShoppingCart(
Products: _shoppingCart.GetProducts().ToList(),
Total: _shoppingCart.Total(),
Subtotal: _shoppingCart.Subtotal(),
Vat: _shoppingCart.Vat()
);// Return a ShapeResult
return new ShapeResult(this, shape);
}
然后,前台代码改为:
@using Orchard.ContentManagement
@using Orchard.Core.Title.Models
@using TMinji.Shop.Models
@{
Style.Require("TMinji.Shop.ShoppingCart");
var items = (IList<ProductQuantity>)Model.Products;
var subtotal = (decimal)Model.Subtotal;
var vat = (decimal)Model.Vat;
var total = (decimal)Model.Total;
}<article class="shoppingcart">
<table>
<thead>
<tr>
<td>Article</td>
<td class="numeric">Quantity</td>
<td class="numeric">Price</td>
<td></td>
</tr>
</thead>
<tbody>
@foreach (var item in items)
{
var product = item.ProductPart;
var titlePart = product.As<TitlePart>();
var title = titlePart != null ? titlePart.Title : "(no TitlePart attached)";
var quantity = item.Quantity;
<tr>
<td>@title</td>
<td class="numeric"><input type="number" value="@quantity" /></td>
<td class="numeric">@product.UnitPrice.ToString("c")</td>
<td class="action"><a class="icon delete" href="#"></a></td>
</tr>
}
</tbody>
<tfoot>
<tr class="separator"><td colspan="4"> </td></tr>
<tr>
<td class="numeric label" colspan="2">Subtotal:</td>
<td class="numeric">@subtotal.ToString("c")</td>
<td></td>
</tr>
<tr>
<td class="numeric label" colspan="2">VAT (19%):</td>
<td class="numeric">@vat.ToString("c")</td>
<td></td>
</tr>
<tr>
<td class="numeric label" colspan="2">Total:</td>
<td class="numeric">@total.ToString("c")</td>
<td></td>
</tr>
</tfoot>
</table>
<footer>
<div class="group">
<div class="align left"><a class="button" href="#">Continue shopping</a></div>
<div class="align right"><a class="button next" href="#">Proceed to checkout</a></div>
</div>
</footer>
</article>
然后,我们就实现了这样的功能:
二:关于 ItemMetadata
我们在前台代码中看到了这样的代码:
var titlePart = product.As<TitlePart>();
var title = titlePart != null ? titlePart.Title : "(no TitlePart attached)";
As 方法把一个 ContentPart 转型为了另一个 ContentPart。我们知道,所有的 Content 都是是由 ContentPart 组成的,在代码上,我们可以通过 content.ContentItem 这个属性得到它们。现在,我们来查看 As 方法:
public static T As<T>(this IContent content) where T : IContent {
return content == null ? default(T) : (T)content.ContentItem.Get(typeof(T));
}
我们就理解了,如果我们创建 Product 这个 Content 的时候,没有 attached TitlePart,则我们就应该提示 “no TitlePart attached”。
但是,其实我们有更好的方法来得到 content title,那就是使用 IContentManager 的 GetItemMetadata 方法。我们可以查看 TitlePartHandler:
using Orchard.ContentManagement;
using Orchard.ContentManagement.Aspects;
using Orchard.ContentManagement.Handlers;
using Orchard.Core.Title.Models;
using Orchard.Data;namespace Orchard.Core.Title.Handlers {
public class TitlePartHandler : ContentHandler {public TitlePartHandler(IRepository<TitlePartRecord> repository) {
Filters.Add(StorageFilter.For(repository));
OnIndexing<ITitleAspect>((context, part) => context.DocumentIndex.Add("title", part.Title).RemoveTags().Analyze());
}protected override void GetItemMetadata(GetContentItemMetadataContext context) {
var part = context.ContentItem.As<ITitleAspect>();if (part != null) {
context.Metadata.DisplayText = part.Title;
}
}
}
}
它有方法 GetItemMetadata,当我们将某个 Part 转型为 TitlePart 的时候,我们就会得到 TitlePart 的 Title。
现在,我们修改控制器方法:
[Themed]
public ActionResult Index()
{// Create a new shape using the "New" property of IOrchardServices.
//var shape = _services.New.ShoppingCart(
// Products: _shoppingCart.GetProducts().ToList(),
// Total: _shoppingCart.Total(),
// Subtotal: _shoppingCart.Subtotal(),
// Vat: _shoppingCart.Vat()
//);
var shape = _services.New.ShoppingCart(
Products: _shoppingCart.GetProducts().Select(p => _services.New.ShoppingCartItem(
ProductPart: p.ProductPart,
Quantity: p.Quantity,
Title: _services.ContentManager.GetItemMetadata(p.ProductPart).DisplayText)
).ToList(),
Total: _shoppingCart.Total(),
Subtotal: _shoppingCart.Subtotal(),
Vat: _shoppingCart.Vat()
);// Return a ShapeResult
return new ShapeResult(this, shape);
}
注意了,在这里,我们干了一件事情:我们通过 GetItemMetadata 来得到 title。好的,这个时候修改前台为:
@using Orchard.ContentManagement
@using Orchard.Core.Title.Models
@using TMinji.Shop.Models
@{
Style.Require("TMinji.Shop.ShoppingCart");
var items = (IList<dynamic>)Model.Products;
var subtotal = (decimal)Model.Subtotal;
var vat = (decimal)Model.Vat;
var total = (decimal)Model.Total;
}
<article class="shoppingcart">
<table>
<thead>
<tr>
<td>Article</td>
<td class="numeric">Quantity</td>
<td class="numeric">Price</td>
<td></td>
</tr>
</thead>
<tbody>
@foreach (var item in items)
{
var product = item.ProductPart;
var title = item.Title;
var quantity = item.Quantity;
<tr>
<td>@title</td>
<td class="numeric"><input type="number" value="@quantity" /></td>
<td class="numeric">@product.UnitPrice.ToString("c")</td>
<td class="action"><a class="icon delete" href="#"></a></td>
</tr>
}
</tbody>
<tfoot>
<tr class="separator"><td colspan="4"> </td></tr>
<tr>
<td class="numeric label" colspan="2">Subtotal:</td>
<td class="numeric">@subtotal.ToString("c")</td>
<td></td>
</tr>
<tr>
<td class="numeric label" colspan="2">VAT (19%):</td>
<td class="numeric">@vat.ToString("c")</td>
<td></td>
</tr>
<tr>
<td class="numeric label" colspan="2">Total:</td>
<td class="numeric">@total.ToString("c")</td>
<td></td>
</tr>
</tfoot>
</table>
<footer>
<div class="group">
<div class="align left"><a class="button" href="#">Continue shopping</a></div>
<div class="align right"><a class="button next" href="#">Proceed to checkout</a></div>
</div>
</footer>
</article>
这就是最终的前台。
三:更新 和 删除 购物车产品
首先,需要修改前台,让它变成一个表单:
@using Orchard.ContentManagement
@using Orchard.Core.Title.Models
@using TMinji.Shop.Models
@{
Style.Require("TMinji.Shop.ShoppingCart");
var items = (IList<dynamic>)Model.Products;
var subtotal = (decimal)Model.Subtotal;
var vat = (decimal)Model.Vat;
var total = (decimal)Model.Total;
}
<article class="shoppingcart">
@using (Html.BeginFormAntiForgeryPost(Url.Action("Update", "ShoppingCart", new { area = "TMinji.Shop" })))
{
<table>
<thead>
<tr>
<td>Article</td>
<td class="numeric">Unit Price</td>
<td class="numeric">Quantity</td>
<td class="numeric">Total Price</td>
<td class="action"></td>
</tr>
</thead>
<tbody>
@for (var i = 0; i < items.Count; i++)
{
var item = items[i];
var product = (ProductPart)item.ProductPart;
var title = item.Title ?? "(no routepart attached)";
var quantity = (int)item.Quantity;
var unitPrice = product.UnitPrice;
var totalPrice = quantity * unitPrice;
<tr>
<td>@title</td>
<td class="numeric">@unitPrice.ToString("c")</td>
<td class="numeric">
<input name="@string.Format("items[{0}].ProductId", i)" type="hidden" value="@product.Id" />
<input name="@string.Format("items[{0}].IsRemoved", i)" type="hidden" value="false" />
<input name="@string.Format("items[{0}].Quantity", i)" type="number" value="@quantity" />
</td>
<td class="numeric">@totalPrice.ToString("c")</td>
<td class="action"><a class="icon delete" href="#"></a></td>
</tr>
}</tbody>
<tfoot>
<tr><td colspan="5"> </td></tr>
<tr class="separator">
<td class="update" colspan="5"><button name="command" value="Update" type="submit">Update</button></td>
</tr>
<tr>
<td class="numeric label" colspan="3">Subtotal:</td>
<td class="numeric">@subtotal.ToString("c")</td>
<td></td>
</tr>
<tr>
<td class="numeric label" colspan="3">VAT (19%):</td>
<td class="numeric">@vat.ToString("c")</td>
<td></td>
</tr>
<tr>
<td class="numeric label" colspan="3">Total:</td>
<td class="numeric">@total.ToString("c")</td>
<td></td>
</tr>
</tfoot>
</table>
<footer>
<div class="group">
<div class="align left"><button type="submit" name="command" value="ContinueShopping">Continue shopping</button></div>
<div class="align right"><button type="submit" name="command" value="Checkout">Proceed to checkout</button></div>
</div>
</footer>
}
</article>
然后,增加控制器方法:
public ActionResult Update(string command, UpdateShoppingCartItemViewModel[] items)
{// Loop through each posted item
foreach (var item in items)
{
// Select the shopping cart item by posted product ID
var shoppingCartItem = _shoppingCart.Items.SingleOrDefault(x => x.ProductId == item.ProductId);
if (shoppingCartItem != null)
{
// Update the quantity of the shoppingcart item. If IsRemoved == true, set the quantity to 0
shoppingCartItem.Quantity = item.IsRemoved ? 0 : item.Quantity < 0 ? 0 : item.Quantity;
}
}// Update the shopping cart so that items with 0 quantity will be removed
_shoppingCart.UpdateItems();// Return an action result based on the specified command
switch (command)
{
case "Checkout":
break;
case "ContinueShopping":
break;
case "Update":
break;
}// Return to Index if no command was specified
return RedirectToAction("Index");
}
同时,创建一个实体类 UpdateShoppingCartItemViewModel:
public class UpdateShoppingCartItemViewModel
{
public decimal ProductId { get; set; }
public bool IsRemoved { get; set; }
public int Quantity { get; set; }
}
然后,实现 Services/ShoppingCart.cs 中的 UpdateItems:
public void UpdateItems()
{
ItemsInternal.RemoveAll(x => x.Quantity == 0);
}
3.1 加入 JQuery
首先,我们需要修改 ResourceManifest,如下:
public class ResourceManifest : IResourceManifestProvider
{
public void BuildManifests(ResourceManifestBuilder builder)
{
// Create and add a new manifest
var manifest = builder.Add();// Define a "common" style sheet
manifest.DefineStyle("TMinji.Shop.Common").SetUrl("common.css");// Define the "shoppingcart" style sheet
manifest.DefineStyle("TMinji.Shop.ShoppingCart").SetUrl("shoppingcart.css").SetDependencies("TMinji.Shop.Common");manifest.DefineScript("jQuery").SetUrl("jquery-1.9.1.min.js", "jquery-1.9.1.js").SetVersion("1.9.1");
}
}
然后,修改 Module.txt,
name: tminji.shop
antiforgery: enabled
author: tminji.com
website: http://www.tminji.com
version: 1.0.0
orchardversion: 1.0.0
description: The tminji.com module is a shopping module.
Dependencies: Orchard.Projections, Orchard.Forms, Orchard.jQuery
features:
shop:
Description: shopping module.
Category: ASample
再然后,增加 Scripts 文件夹,添加 shoppingcart.js:
(function ($) {
$(".shoppingcart a.icon.delete").click(function (e) {
var $button = $(this);
var $tr = $button.parents("tr:first");
var $isRemoved = $("input[name$='IsRemoved']", $tr).val("true");
var $form = $button.parents("form");$form.submit();
e.preventDefault();
});})(jQuery);
现在,为了让模版找到这个 js,需要修正 ResourceManifest,如下:
public class ResourceManifest : IResourceManifestProvider
{
public void BuildManifests(ResourceManifestBuilder builder)
{
// Create and add a new manifest
var manifest = builder.Add();// Define a "common" style sheet
manifest.DefineStyle("TMinji.Shop.Common").SetUrl("common.css");// Define the "shoppingcart" style sheet
manifest.DefineStyle("TMinji.Shop.ShoppingCart").SetUrl("shoppingcart.css").SetDependencies("TMinji.Shop.Common");//manifest.DefineScript("jQuery").SetUrl("jquery-1.9.1.min.js", "jquery-1.9.1.js").SetVersion("1.9.1");
// Define the "shoppingcart" script and set a dependency on the "jQuery" resource
manifest.DefineScript("Skywalker.Webshop.ShoppingCart").SetUrl("shoppingcart.js").SetDependencies("jQuery");}
}
再一次,我们需要修改 Views/ShoppingCart.cshtml:
@{
Style.Require("TMinji.Shop.ShoppingCart");
Script.Require("TMinji.Shop.ShoppingCart").AtHead();
如果没有 AtHead,则 js 文件会加在 </body> 后。
现在,稍稍再修正下前台:
@using Orchard.ContentManagement
@using Orchard.Core.Title.Models
@using TMinji.Shop.Models
@{
Style.Require("TMinji.Shop.ShoppingCart");
Script.Require("TMinji.Shop.ShoppingCart").AtHead();var items = (IList<dynamic>)Model.Products;
var subtotal = (decimal)Model.Subtotal;
var vat = (decimal)Model.Vat;
var total = (decimal)Model.Total;
}
@if (!items.Any())
{
<p>You don't have any items in your shopping cart.</p>
<a class="button" href="#">Continue shopping</a> }
else
{
<article class="shoppingcart">
。。。
</article>
}
现在,效果如下:
当然,update 也已经可用了。
四:添加 Widget
首先,我们需要增加 Models/ShoppingCartWidgetPart.cs:
public class ShoppingCartWidgetPart : ContentPart
{
}
有了 part,我们还需要 Drivers/ShoppingCartWidgetPartDriver.cs:
using Orchard.ContentManagement.Drivers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TMinji.Shop.Models;
using TMinji.Shop.Services;namespace TMinji.Shop.Drivers
{
public class ShoppingCartWidgetPartDriver : ContentPartDriver<ShoppingCartWidgetPart>
{
private readonly IShoppingCart _shoppingCart;public ShoppingCartWidgetPartDriver(IShoppingCart shoppingCart)
{
_shoppingCart = shoppingCart;
}protected override DriverResult Display(ShoppingCartWidgetPart part, string displayType, dynamic shapeHelper)
{
return ContentShape("Parts_ShoppingCartWidget", () => shapeHelper.Parts_ShoppingCartWidget(
ItemCount: _shoppingCart.ItemCount(),
TotalAmount: _shoppingCart.Total()
));
}
}}
然后,修改 Placement.info:
<Placement>
<Place Parts_Product_Edit="Content:1" />
<Place Parts_Product="Content:0" />
<Place Parts_Product_AddButton="Content:after" />
<Place Parts_ShoppingCartWidget="Content:0" />
</Placement>
然后,修改 Migrations,我们要添加该 widget:
public int UpdateFrom2()
{
// Define a new content type called "ShoppingCartWidget"
ContentDefinitionManager.AlterTypeDefinition("ShoppingCartWidget", type => type
// Attach the "ShoppingCartWidgetPart"
.WithPart("ShoppingCartWidgetPart")
// In order to turn this content type into a widget, it needs the WidgetPart
.WithPart("WidgetPart")
// It also needs a setting called "Stereotype" to be set to "Widget"
.WithSetting("Stereotype", "Widget")
);return 3;
}
现在,添加 Views/Parts/ShoppingCartWidget.cshtml:
@using Orchard.ContentManagement
@using Orchard.Core.Title.Models
@using TMinji.Shop.Models@{
Style.Require("TMinji.Shop.ShoppingCart");
var itemCount = (int)Model.ItemCount;
var totalAmount = (decimal)Model.TotalAmount;
}
<article>
<span class="label">Items:</span> <span class="value">@itemCount</span><br />
<span class="label">Amount:</span> <span class="value">@totalAmount.ToString("c")</span><br />
<div class="group">
<div class="align right">
<a href="@Url.Action("Index", "ShoppingCart", new { area = "TMinji.Shop" })">View shoppingcart</a>
</div>
</div>
</article>
再添加 Styles/shoppingcartwidget.css:
article.widget-shopping-cart-widget header h1{
background: #f6f6f6;
font-weight: bold;
line-height: 24px;
margin: 0;
padding: 0 5px 0 5px;
}
article.widget-shopping-cart-widget article {
padding: 5px;
border: 1px dotted #ccc;
line-height: 20px;
}
article.widget-shopping-cart-widget article span.label{
60px;
font-style: italic;
color: #aaa;
display: inline-block;
}
再次更新 ResourceManifest:
public void BuildManifests(ResourceManifestBuilder builder)
{
// Create and add a new manifest
var manifest = builder.Add();// Define a "common" style sheet
manifest.DefineStyle("TMinji.Shop.Common").SetUrl("common.css");// Define the "shoppingcart" style sheet
manifest.DefineStyle("TMinji.Shop.ShoppingCart").SetUrl("shoppingcart.css").SetDependencies("TMinji.Shop.Common");manifest.DefineStyle("TMinji.Shop.ShoppingCartWidget").SetUrl("shoppingcartwidget.css").SetDependencies("Webshop.Common");
//manifest.DefineScript("jQuery").SetUrl("jquery-1.9.1.min.js", "jquery-1.9.1.js").SetVersion("1.9.1");
// Define the "shoppingcart" script and set a dependency on the "jQuery" resource
manifest.DefineScript("TMinji.Shop.ShoppingCart").SetUrl("shoppingcart.js").SetDependencies("jQuery");}
好了,来到后台,就可以:
结果,点击之后报错:
更详细的日志在:Orchard.WebApp_DataLogs,好吧,看了日志,大概就是 ShoppingCartWidget 还必须依赖于 CommonPart,然后,继续 Migrations:
public int UpdateFrom3()
{
// Update the ShoppingCartWidget so that it has a CommonPart attached, which is required for widgets (it's generally a good idea to have this part attached)
ContentDefinitionManager.AlterTypeDefinition("ShoppingCartWidget", type => type
.WithPart("CommonPart")
);return 4;
}
然后,再次运行,就可以添加 Widget 了,如下:
然后,前台的效果就是:
五:让前端支持 MVVM
直接 install KnockoutJS 和 LinqJS
装完之后,要确保 Enable。
当然,作为程序员的我们,还需要把它们引入到自己的解决方案中来,注意,修改 target framework 为 4.5(如果你的 orchard 是 4.5 的话)。
修改 ResourceManifest.cs:
public void BuildManifests(ResourceManifestBuilder builder)
{
// Create and add a new manifest
var manifest = builder.Add();// Define a "common" style sheet
manifest.DefineStyle("TMinji.Shop.Common").SetUrl("common.css");// Define the "shoppingcart" style sheet
manifest.DefineStyle("TMinji.Shop.ShoppingCart").SetUrl("shoppingcart.css").SetDependencies("TMinji.Shop.Common");manifest.DefineStyle("TMinji.Shop.ShoppingCartWidget").SetUrl("shoppingcartwidget.css").SetDependencies("Webshop.Common");
//manifest.DefineScript("jQuery").SetUrl("jquery-1.9.1.min.js", "jquery-1.9.1.js").SetVersion("1.9.1");
// Define the "shoppingcart" script and set a dependency on the "jQuery" resource
//manifest.DefineScript("TMinji.Shop.ShoppingCart").SetUrl("shoppingcart.js").SetDependencies("jQuery");
manifest.DefineScript("Minji.Shop.ShoppingCart").SetUrl("shoppingcart.js").SetDependencies("jQuery", "jQuery_LinqJs", "ko");
}
修改 Module.txt:
name: tminji.shop
antiforgery: enabled
author: tminji.com
website: http://www.tminji.com
version: 1.0.0
orchardversion: 1.0.0
description: The tminji.com module is a shopping module.
Dependencies: Orchard.Projections, Orchard.Forms, Orchard.jQuery, Orchard.jQuery, AIM.LinqJs, Orchard.Knockout
features:
shop:
Description: shopping module.
Category: ASample
然后,修改 shoppingcart.js:
(function ($) {
$(".shoppingcart a.icon.delete").live("click", function (e) {
e.preventDefault();// Check if the clicked button is generated by KO. If so, we simply remove the item from the model and return.
var shoppingCartItem = ko.dataFor(this);if (shoppingCartItem != null) {
shoppingCartItem.remove();
return;
}// If we got here, the clicked button was not created by KO (which should only happen if we disabled KO).
var $button = $(this);
var $tr = $button.parents("tr:first");
var $isRemoved = $("input[name$='IsRemoved']", $tr).val("true");
var $form = $button.parents("form");$form.submit();
});
/***************************************************** * ShoppingCartItem class ******************************************************/
var ShoppingCartItem = function (data) {this.id = data.id;
this.title = data.title;
this.unitPrice = data.unitPrice;
this.quantity = ko.observable(data.quantity);this.total = ko.dependentObservable(function () {
return this.unitPrice * parseInt(this.quantity());
}, this);this.remove = function () {
shoppingCart.items.remove(this);
saveChanges();
};this.quantity.subscribe(function (value) {
saveChanges();
});this.index = ko.dependentObservable(function () {
return shoppingCart.items.indexOf(this);
}, this);
};/***************************************************** * ShoppingCart (viewmodel) ******************************************************/
var shoppingCart = {
items: ko.observableArray()
};shoppingCart.calculateSubtotal = ko.dependentObservable(function () {
return $.Enumerable.From(this.items()).Sum(function (x) { return x.total(); });
}, shoppingCart);shoppingCart.itemCount = ko.dependentObservable(function () {
return $.Enumerable.From(this.items()).Sum(function (x) { return parseInt(x.quantity()); });
}, shoppingCart);shoppingCart.hasItems = ko.dependentObservable(function () { return this.items().length > 0; }, shoppingCart);
shoppingCart.calculateVat = function () { return this.calculateSubtotal() * 0.19; };
shoppingCart.calculateTotal = function () { return this.calculateSubtotal() + this.calculateVat(); };/***************************************************** * SaveChanges ******************************************************/
var saveChanges = function () {
var data = $.Enumerable.From(shoppingCart.items()).Select(function (x) { return { productId: x.id, quantity: x.quantity() }; }).ToArray();
var url = $("article.shoppingcart").data("update-shoppingcart-url");
var config = {
url: url,
type: "POST",
data: data ? JSON.stringify(data) : null,
dataType: "json",
contentType: "application/json; charset=utf-8"
};
$.ajax(config);
};/***************************************************** * Initialization ******************************************************/
if ($("article.shoppingcart").length > 0) {
$.ajaxSetup({ cache: false });
ko.applyBindings(shoppingCart);
var dataUrl = $("article.shoppingcart").data("load-shoppingcart-url");// Clear any existing table rows.
$("article.shoppingcart tbody").empty();// Hide the "Update" button, as we will auto update the quantities using AJAX.
$("button[value='Update']").hide();$.getJSON(dataUrl, function (data) {
for (var i = 0; i < data.items.length; i++) {
var item = data.items[i];
shoppingCart.items.push(new ShoppingCartItem(item));
}
});
}})(jQuery);
现在,再次修正 ShoppingCart.cshtml:
@using Orchard.ContentManagement
@using Orchard.Core.Title.Models
@using TMinji.Shop.Models
@{
Style.Require("TMinji.Shop.ShoppingCart");
Script.Require("TMinji.Shop.ShoppingCart").AtHead();var items = (IList<dynamic>)Model.Products;
var subtotal = (decimal)Model.Subtotal;
var vat = (decimal)Model.Vat;
var total = (decimal)Model.Total;
}
@if (!items.Any())
{
<p>You don't have any items in your shopping cart.</p>
<a class="button" href="#">Continue shopping</a> }
else
{
<div data-bind="visible: !hasItems()">
<p>You don't have any items in your shopping cart.</p>
<a class="button" href="#">Continue shopping</a>
</div><div data-bind="visible: hasItems()">
<article class="shoppingcart" data-load-shoppingcart-url="@Url.Action("GetItems", "ShoppingCart", new { area = "TMinji.Shop" })" data-update-shoppingcart-url="@Url.Action("Update", "ShoppingCart", new { area = "TMinji.Shop" })">
@using (Html.BeginFormAntiForgeryPost(Url.Action("Update", "ShoppingCart", new { area = "TMinji.Shop" })))
{
<table>
<thead>
<tr>
<td>Article</td>
<td class="numeric">Unit Price</td>
<td class="numeric">Quantity</td>
<td class="numeric">Total Price</td>
<td class="action"></td>
</tr>
</thead>
<tbody data-bind='template: {name: "itemTemplate", foreach: items}'>
@for (var i = 0; i < items.Count; i++)
{
var item = items[i];
var product = (ProductPart)item.ProductPart;
var title = item.Title ?? "(no routepart attached)";
var quantity = (int)item.Quantity;
var unitPrice = product.UnitPrice;
var totalPrice = quantity * unitPrice;
<tr>
<td>@title</td>
<td class="numeric">@unitPrice.ToString("c")</td>
<td class="numeric">
<input name="@string.Format("items[{0}].ProductId", i)" type="hidden" value="@product.Id" />
<input name="@string.Format("items[{0}].IsRemoved", i)" type="hidden" value="false" />
<input name="@string.Format("items[{0}].Quantity", i)" type="number" value="@quantity" />
</td>
<td class="numeric">@totalPrice.ToString("c")</td>
<td class="action"><a class="icon delete postback" href="#"></a></td>
</tr>
}</tbody>
<tfoot>
<tr><td colspan="5"> </td></tr>
<tr class="separator">
<td class="update" colspan="5"><button name="command" value="Update" type="submit">Update</button></td>
</tr>
<tr>
<td class="numeric label" colspan="3">Subtotal:</td>
<td class="numeric"><span data-bind="text: calculateSubtotal()">@subtotal.ToString("c")</span></td>
<td></td>
</tr>
<tr>
<td class="numeric label" colspan="3">VAT (19%):</td>
<td class="numeric"><span data-bind="text: calculateVat()">@vat.ToString("c")</span></td>
<td></td>
</tr>
<tr>
<td class="numeric label" colspan="3">Total:</td>
<td class="numeric"><span data-bind="text: calculateTotal()">@total.ToString("c")</span></td>
<td></td>
</tr>
</tfoot>
</table>
<footer>
<div class="group">
<div class="align left"><button type="submit" name="command" value="ContinueShopping">Continue shopping</button></div>
<div class="align right"><button type="submit" name="command" value="Checkout">Proceed to checkout</button></div>
</div>
</footer>
}
</article><script type="text/html" id="itemTemplate">
<tr>
<td><span data-bind="text: title"></span></td>
<td class="numeric"><span data-bind="text: unitPrice"></span></td>
<td class="numeric">
<input data-bind="attr: { name: 'items[' + index() + '].ProductId'}, value: id" type="hidden" />
<input data-bind="attr: { name: 'items[' + index() + '].Quantity'}, value: quantity" type="number" />
</td>
<td class="numeric"><span data-bind="text: total()"></span></td>
<td><a class="icon delete" href="#"></a></td>
</tr>
</script>
</div>}
修正 ShoppingCartWidget.cshtml:
@using Orchard.ContentManagement
@using Orchard.Core.Title.Models
@using TMinji.Shop.Models@{
Style.Require("TMinji.Shop.ShoppingCart");
var itemCount = (int)Model.ItemCount;
var totalAmount = (decimal)Model.TotalAmount;
}
<article>
<span class="label">Items:</span> <span class="value" data-bind="text: itemCount()">@itemCount</span><br />
<span class="label">Amount:</span> <span class="value" data-bind="text: calculateTotal()">@totalAmount.ToString("c")</span><br />
<div class="group">
<div class="align right">
<a href="@Url.Action("Index", "ShoppingCart", new { area = "TMinji.Shop" })">View shoppingcart</a>
</div>
</div>
</article>
修改控制器 ShoppingCartController:
public ActionResult GetItems()
{
var products = _shoppingCart.GetProducts();var json = new
{
items = (from item in products
select new
{
id = item.ProductPart.Id,
title = _services.ContentManager.GetItemMetadata(item.ProductPart).DisplayText ?? "(No TitlePart attached)",
unitPrice = item.ProductPart.UnitPrice,
quantity = item.Quantity
}).ToArray()
};return Json(json, JsonRequestBehavior.AllowGet);
}private void UpdateShoppingCart(IEnumerable<UpdateShoppingCartItemViewModel> items)
{_shoppingCart.Clear();
if (items == null)
return;_shoppingCart.AddRange(items
.Where(item => !item.IsRemoved)
.Select(item => new ShoppingCartItem(item.ProductId, item.Quantity < 0 ? 0 : item.Quantity))
);_shoppingCart.UpdateItems();
}
OK,运行一下你的代码吧。
提醒一下哦,上面的代码中还需要我们实现一些 IShoppingCart 的方法,但是,我相信,大家已经会自己实现了,很简单的,这里就不再赘述了。