• 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;
        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

    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{
        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.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 了,如下:

    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 的方法,但是,我相信,大家已经会自己实现了,很简单的,这里就不再赘述了。

  • 相关阅读:
    网络攻击技术:SQL Injection(sql注入)
    人生若只如初见,何事秋风悲画扇
    TCPClient、TCPListener的用法
    C#中时间的Ticks属性
    string.Empty、" "、null 三者之间的区别
    telnet的使用解析
    public DataTable ExecuteQuery(string sql) 这段话具体意思
    C#中三层架构UI、BLL、DAL、Model详解(送给自学的初学者)
    数据库三层架构
    获取SQL Server数据库的表结构的做法
  • 原文地址:https://www.cnblogs.com/luminji/p/3860791.html
Copyright © 2020-2023  润新知