首页 > 代码库 > Orchard模块开发全接触5:深度改造前台第二部分

Orchard模块开发全接触5:深度改造前台第二部分

在这一部分,我们继续完善我们的购物车,我们要做以下一些事情:

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="http://www.mamicode.com/1" /></td>
                <td class="numeric">$9.99</td>
                <td><a class="icon delete" href="http://www.mamicode.com/#"></a></td>
            </tr>
            }

        </tbody>
        <tfoot>
            <tr class="separator"><td colspan="4">&nbsp;</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="http://www.mamicode.com/#">Continue shopping</a></div>
            <div class="align right"><a class="button next" href="http://www.mamicode.com/#">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;
    width: 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 {
    width: 500px;
}
article.shoppingcart table {
    width: 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 {
    width: 75px;
    text-align: right;
}
article.shoppingcart td.numeric input {
    width: 50px;
}

现在,我们创建 css 中使用到的图片,让我们创建 Images 文件夹,并添加 sprites.png

sprites

注意哦,如果这个时候我们运行带来,会看到 css 并没有呈现出来,这是因为,orchard 接管了所有文件的 handle,我们需要在 Images 和 Styles 文件下放置 web.config,让 Orchard 不要处理 static 文件,如下:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <appSettings>
    <add key="webpages:Enabled" value="http://www.mamicode.com/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>

现在,看到效果:

image

现在,我们修改控制器,如下:

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="http://www.mamicode.com/@quantity" /></td>
                    <td class="numeric">@product.UnitPrice.ToString("c")</td>
                    <td class="action"><a class="icon delete" href="http://www.mamicode.com/#"></a></td>
                </tr>
            }
        </tbody>
        <tfoot>
            <tr class="separator"><td colspan="4">&nbsp;</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="http://www.mamicode.com/#">Continue shopping</a></div>
            <div class="align right"><a class="button next" href="http://www.mamicode.com/#">Proceed to checkout</a></div>
        </div>
    </footer>
</article>

然后,我们就实现了这样的功能:

image

 

二:关于 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,那就是使用 IContentManagerGetItemMetadata 方法。我们可以查看 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="http://www.mamicode.com/@quantity" /></td>
                    <td class="numeric">@product.UnitPrice.ToString("c")</td>
                    <td class="action"><a class="icon delete" href="http://www.mamicode.com/#"></a></td>
                </tr>
            }
        </tbody>
        <tfoot>
            <tr class="separator"><td colspan="4">&nbsp;</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="http://www.mamicode.com/#">Continue shopping</a></div>
            <div class="align right"><a class="button next" href="http://www.mamicode.com/#">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="http://www.mamicode.com/@product.Id" />
                            <input name="@string.Format("items[{0}].IsRemoved", i)" type="hidden" value="http://www.mamicode.com/false" />
                            <input name="@string.Format("items[{0}].Quantity", i)" type="number" value="http://www.mamicode.com/@quantity" />
                        </td>
                        <td class="numeric">@totalPrice.ToString("c")</td>
                        <td class="action"><a class="icon delete" href="http://www.mamicode.com/#"></a></td>
                    </tr>
                }

            </tbody>
            <tfoot>
                <tr><td colspan="5">&nbsp;</td></tr>
                <tr class="separator">
                    <td class="update" colspan="5"><button name="command" value="http://www.mamicode.com/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="http://www.mamicode.com/ContinueShopping">Continue shopping</button></div>
                <div class="align right"><button type="submit" name="command" value="http://www.mamicode.com/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="http://www.mamicode.com/#">Continue shopping</a> }
else
{
    <article class="shoppingcart">
        。。。
    </article>
}

现在,效果如下:

image

当然,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="http://www.mamicode.com/@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{
    width: 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");

}

好了,来到后台,就可以:

image

image

结果,点击之后报错:

image

更详细的日志在:Orchard.Web\App_Data\Logs,好吧,看了日志,大概就是 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 了,如下:

image

然后,前台的效果就是:

image

 

五:让前端支持 MVVM

直接 install KnockoutJS 和 LinqJS

image

image

装完之后,要确保 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 = http://www.mamicode.com/$.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=http://www.mamicode.com/‘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="http://www.mamicode.com/#">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="http://www.mamicode.com/#">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="http://www.mamicode.com/@product.Id" />
                                    <input name="@string.Format("items[{0}].IsRemoved", i)" type="hidden" value="http://www.mamicode.com/false" />
                                    <input name="@string.Format("items[{0}].Quantity", i)" type="number" value="http://www.mamicode.com/@quantity" />
                                </td>
                                <td class="numeric">@totalPrice.ToString("c")</td>
                                <td class="action"><a class="icon delete postback" href="http://www.mamicode.com/#"></a></td>
                            </tr>
                        }

                    </tbody>
                    <tfoot>
                        <tr><td colspan="5">&nbsp;</td></tr>
                        <tr class="separator">
                            <td class="update" colspan="5"><button name="command" value="http://www.mamicode.com/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="http://www.mamicode.com/ContinueShopping">Continue shopping</button></div>
                        <div class="align right"><button type="submit" name="command" value="http://www.mamicode.com/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="http://www.mamicode.com/#"></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="http://www.mamicode.com/@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 的方法,但是,我相信,大家已经会自己实现了,很简单的,这里就不再赘述了。