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="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">&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="#">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;
    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="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="@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">&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="#">Continue shopping</a></div>
            <div class="align right"><a class="button next" href="#">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="@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">&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="#">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">&nbsp;</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>
}

現在,效果如下:

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="@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 = $.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">&nbsp;</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 的方法,但是,我相信,大家已經會自己實現了,很簡單的,這里就不再贅述了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM