Compare commits

...

52 Commits

Author SHA1 Message Date
Jason Zhu fe196ca256 10.2.2.1 Helping the Controller Select a View; Listing 10-5 Updating the Menu Action Method in the NavController.cs File 2021-09-22 22:59:14 +10:00
Jason Zhu b03c3d3e77 10.2.2 Creating a Responsive Product List; Listing 10-3 Creating a Product List in _Layout.cshtml File 2021-09-22 22:46:58 +10:00
Jason Zhu 17c5e9e391 10.2.1 Creating a Responsive Header; Listing 10-2. Adding Responsive Content to the `Summary.cshtml` File 2021-09-22 22:36:56 +10:00
Jason Zhu fa611c9e3b 10.2.1 Creating a Responsive Header; Listing 10-1 Adding Responsive Content to the _Layout.cshtml file 2021-09-22 22:14:15 +10:00
Jason Zhu dfd6b230f6 9.3.7 Displaying a Summary Page; Listing 10-1 Adding Responsive Content to the _Layout.cshtml File 2021-09-22 20:51:13 +10:00
Jason Zhu e899f54af3 Fixed location of email file sent location 2021-09-22 16:43:28 +10:00
Jason Zhu ffce55b6a9 9.3.6 Displaying Validation Errors; Listing 9-21 Adding a Link Element in the _Layout.cshtml File 2021-09-22 16:25:16 +10:00
Jason Zhu ba2f003e49 9.3.6 Displaying Validation Errors; Listing 9-20 The Contents of the ErrorStyles.css File 2021-09-22 16:24:00 +10:00
Jason Zhu f4146a6c0f 9.3.6 Displaying Validation Errors; Listing 9-19 Adding a Validation Summary to the Checkout.cshtml File 2021-09-22 16:16:18 +10:00
Jason Zhu 6838555dd2 9.3.5 Completing the Cart Controller; UNIT TEST: ORDER PROCESSING 2021-09-22 16:14:07 +10:00
Jason Zhu 23f23641e5 9.3.5 Completing the Cart Controller; Listing 9-18 Completing the Controller in the CartController.cs File 2021-09-22 15:20:39 +10:00
Jason Zhu b8a2c0043d 9.3.4 Registering the Implementation; Listing 9-17 Application Settings ini the Web.config File 2021-09-22 15:10:08 +10:00
Jason Zhu ca3c72f560 9.3.4 Registering the Implementation; Listing 9-16 Adding Ninject Bindings for IOrderProcessor to the NinjectDependencyResolver.cs File 2021-09-22 15:01:30 +10:00
Jason Zhu 27f0d26168 9.3.3.2 Implementing the Interface; Listing 9-15 The Contents of the EmailOrderProcessor.cs File 2021-09-22 14:51:34 +10:00
Jason Zhu 021dd4d937 9.3.3.1 Defining the Interface; Listing 9-14 The Contents of the IOrderProcessor.cs File 2021-09-22 14:28:44 +10:00
Jason Zhu 41f564e967 9.3.2 Adding the Checkout Process; Listing 9-13 Applying the Display attribute to the ShippingDetails.cs File 2021-09-22 14:20:20 +10:00
Jason Zhu 9491c62113 9.3.2 Adding the Checkout Process; Listing 9-12 Reducing Duplication in the Checkout.cshtml File 2021-09-22 14:19:19 +10:00
Jason Zhu 4090de140a 9.3.2 Adding the Checkout Process; Listing 9-11 The Contents of the Checkout.cshtml File 2021-09-22 11:42:47 +10:00
Jason Zhu 5eb2302b52 9.3.2 Adding the Checkout Process; Listing 9-10 The Checkout Action Method in the CartController.cs File 2021-09-22 11:40:11 +10:00
Jason Zhu 5e83bfa72f 9.3.2 Adding the Checkout Process; Listing 9-9 Adding the Checkout Now Button to the Index.cshtml File 2021-09-22 11:37:26 +10:00
Jason Zhu 35419556fe 9.3.1 Extending the Domain Model; Listing 9-8 The Contents of the ShippingDetails.cs File 2021-09-22 11:34:57 +10:00
Jason Zhu 4027f863cc 9.2.2 Adding the Cart Summary; Listing 9-7 Adding the Summary Partial View to the _Layout.cshtml File 2021-09-22 11:22:41 +10:00
Jason Zhu 57c4f167ca 9.2.2 Adding the Cart Summary; Listing 9-6 The Contents of the Summary.cshtml File 2021-09-22 11:19:34 +10:00
Jason Zhu d17ea37a0d 9.2.2 Adding the Cart Summary; Listing 9-5 Adding the Summary Method to the CartController.cs File 2021-09-22 11:15:51 +10:00
Jason Zhu 5636a37f58 9.2.1 Removing Items from the Cart; Listing 9-4 Introducing a Remove Button to the Index.cshtml File 2021-09-22 11:15:00 +10:00
Jason Zhu a8154783e7 9.1.1 Creating a Custom Model Binder; UNIT TEST: THE CART CONTROLLER 2021-09-18 23:30:33 +10:00
Jason Zhu 78b4ea3640 9.1.1 Creating a Custom Model Binder; Listing 9-3 Relying on the Model Binder in the CartController.cs File 2021-09-18 23:15:05 +10:00
Jason Zhu 767b09def1 9.1.1 Creating a Custom Model Binder; Listing 9-2 Registering the CartModelBinder Class in the Global.asax.cs File 2021-09-18 22:28:20 +10:00
Jason Zhu 1bb96ab788 9.1.1 Creating a Custom Model Binder; Listing 9-1. The Contents of the CartModelBinder.cs File 2021-09-18 22:26:21 +10:00
Jason Zhu 8dff8292b3 8.2 Building the Shopping Cart; Listing 8-17. The Contents of the Index.cshtml File 2021-09-18 18:34:56 +10:00
Jason Zhu b64c59264b 8.2 Building the Shopping Cart; Listing 8-16 The Index Action Method in the CartController.cs File 2021-09-18 18:25:55 +10:00
Jason Zhu 4436c60e79 8.2 Building the Shopping Cart; Listing 8-15. The Contents of the CartIndexViewModel.cs File 2021-09-18 18:22:36 +10:00
Jason Zhu a24fe02bbd 8.2.3 Implementing the Cart Controller; Listing 8-14 The Contents of the CartController.cs File 2021-09-13 00:23:59 +10:00
Jason Zhu 905ecd6969 8.2.2 Adding the Add to Cart Buttons; Listing 8-13. Adding the Buttons to the ProductSummary.cshtml File View 2021-09-13 00:16:06 +10:00
Jason Zhu 697f6017dd 8.2.1 Defining the Cart Entity; UNIT TEST: TESTING THE CART 2021-09-13 00:11:39 +10:00
Jason Zhu 2b1fee6172 8.2.1 Defining the Cart Entity; Listing 8-12 The Cart and CartLine Classes in the Cart.cs File 2021-09-09 00:02:29 +10:00
Jason Zhu 62e5982ef8 8.1.4 Correcting the Page Count; UNIT TEST: CATEGORY PRODUCT COUNTS 2021-09-08 23:55:32 +10:00
Jason Zhu 8f39a1ae1c 8.1.4 Correcting the Page Count; Listing 8-11 Creating the Category-Aware Pagination Data in the ProductController.cs File 2021-09-08 23:43:01 +10:00
Jason Zhu 59e5f471db 8.1.3.4 Highlighting the Current Category; Listing 8-10. Highlighting the Selected Category in the Menu.cshtml File 2021-09-08 23:41:08 +10:00
Jason Zhu 6cc688fc9d 8.1.3.4 Highlighting the Current Category; UNIT TEST: REPORTING THE SELECTED CATEGORY 2021-09-08 23:24:09 +10:00
Jason Zhu 32fa22777d 8.1.3.4 Highlighting the Current Category; Listing 8-9 Using the View Bag Feature in the NavController.cs File 2021-09-08 23:11:00 +10:00
Jason Zhu 4eaf23bc64 8.1.3.3 Creating the View; Listing 8-8 The Contents of the Menu.cshtml File 2021-09-08 23:08:58 +10:00
Jason Zhu c55dd5f133 8.1.3.2 Generating Category Lists; UNIT TEST: GENERATING THE CATEGORY LIST 2021-09-08 23:00:27 +10:00
Jason Zhu d240d7d569 8.1.3.2 Generating Category Lists; Listing 8-7 Implementing the Menu Method in the NavController.cs File 2021-09-08 22:33:17 +10:00
Jason Zhu 0bd06ef01c 8.1.3.1 Creating the Navigation Controller; Listing 8-6 Adding the RenderAction Call to the _Layout.cshtml File 2021-09-08 22:15:59 +10:00
Jason Zhu 5b697839e7 8.1.3.1 Creating the Navigation Controller; Listing 8-5 Adding The Menu Action Method to the NavController.cs File 2021-09-08 22:12:51 +10:00
Jason Zhu 56838b589e 8.1.2 Refining the URL Scheme; Listing 8-4. Adding Category Information to the Pagination Links in the List.cshtml File 2021-09-08 22:09:48 +10:00
Jason Zhu f2b64d578f 8.1.2 Refining the URL Scheme; Listing 8-3. The New URL Scheme in the RouteConfig.cs File 2021-09-08 22:08:59 +10:00
Jason Zhu a353aacadc 8.1.1 Filtering the Product List; UNIT TEST: CATEGORY FILTERING 2021-09-08 21:37:23 +10:00
Jason Zhu 7659a7b282 8.1.1 Filtering the Product List; UNIT TEST: UPDATING EXISTING UNIT TESTS 2021-09-08 21:27:53 +10:00
Jason Zhu 4b8ce0c014 8.1.1 Filtering the Product List; Listing 8-2. Adding Category Support to the List Action Method in the ProductController.cs File 2021-09-08 21:25:39 +10:00
Jason Zhu 0086a9a75f 8.1.1 Filtering the Product List; Listing 8-1. Enhancing the ProductsListViewModel.cs File 2021-09-08 21:22:41 +10:00
28 changed files with 1043 additions and 29 deletions

View File

@ -0,0 +1,14 @@
using SportsStore.Domain.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SportsStore.Domain.Abstract
{
public interface IOrderProcessor
{
void ProcessOrder(Cart cart, ShippingDetails shippingDetails);
}
}

View File

@ -0,0 +1,93 @@
using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Mail;
using System.Text;
using System.Threading.Tasks;
namespace SportsStore.Domain.Concrete
{
public class EmailSettings
{
public string MailToAddress = "orders@example.com";
public string MailFromAddress = "sportsstore@example.com";
public bool UseSsl = true;
public string Username = "MySmtpUsername";
public string Password = "MySmtpPassword";
public string Servername = "smtp.example.com";
public int ServerPort = 587;
public bool WriteAsFile = false;
public string FileLocation = @"C:\Users\jason.zhu";
}
public class EmailOrderProcessor : IOrderProcessor
{
private EmailSettings emailSettings;
public EmailOrderProcessor(EmailSettings settings)
{
emailSettings = settings;
}
public void ProcessOrder(Cart cart, ShippingDetails shippingInfo)
{
using (var smtpClient = new SmtpClient())
{
smtpClient.EnableSsl = emailSettings.UseSsl;
smtpClient.Host = emailSettings.Servername;
smtpClient.Port = emailSettings.ServerPort;
smtpClient.UseDefaultCredentials = false;
smtpClient.Credentials = new NetworkCredential(
emailSettings.Username,
emailSettings.Password);
if (emailSettings.WriteAsFile)
{
smtpClient.DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory;
smtpClient.PickupDirectoryLocation = emailSettings.FileLocation;
smtpClient.EnableSsl = false;
}
StringBuilder body = new StringBuilder()
.AppendLine("A new order has been submitted")
.AppendLine("---")
.AppendLine("Items:");
foreach (var line in cart.Lines)
{
var subtotal = line.Product.Price * line.Quantity;
body.AppendFormat("{0} x {1} (subtotal: {2:c}", line.Quantity, line.Product.Name, subtotal);
}
body.AppendFormat("Total order value: {0:c}", cart.ComputeTotalValue())
.AppendLine("---")
.AppendLine("Ship to:")
.AppendLine(shippingInfo.Name)
.AppendLine(shippingInfo.Line1)
.AppendLine(shippingInfo.Line2 ?? "")
.AppendLine(shippingInfo.Line3 ?? "")
.AppendLine(shippingInfo.City)
.AppendLine(shippingInfo.State ?? "")
.AppendLine(shippingInfo.Country)
.AppendLine(shippingInfo.Zip)
.AppendLine("---")
.AppendFormat("Gift wrap: {0}", shippingInfo.GiftWrap ? "Yes" : "No");
MailMessage mailMessage = new MailMessage(
emailSettings.MailFromAddress, // From
emailSettings.MailToAddress, // To
"New order submitted!", // Subject
body.ToString()); // Body
if (emailSettings.WriteAsFile)
{
mailMessage.BodyEncoding = Encoding.ASCII;
}
smtpClient.Send(mailMessage);
}
}
}
}

View File

@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SportsStore.Domain.Entities
{
public class Cart
{
private List<CartLine> lineCollection = new List<CartLine>();
public void AddItem(Product product, int quantity)
{
CartLine line = lineCollection
.Where(p => p.Product.ProductID == product.ProductID)
.FirstOrDefault();
if (line == null)
{
lineCollection.Add(new CartLine
{
Product = product,
Quantity = quantity
});
}
else
{
line.Quantity += quantity;
}
}
public void RemoveLine(Product product)
{
lineCollection.RemoveAll(l => l.Product.ProductID == product.ProductID);
}
public decimal ComputeTotalValue()
{
return lineCollection.Sum(e => e.Product.Price * e.Quantity);
}
public void Clear()
{
lineCollection.Clear();
}
public IEnumerable<CartLine> Lines
{
get { return lineCollection; }
}
}
public class CartLine
{
public Product Product { get; set; }
public int Quantity { get; set; }
}
}

View File

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SportsStore.Domain.Entities
{
public class ShippingDetails
{
[Required(ErrorMessage = "Please enter a name")]
public string Name { get; set; }
[Required(ErrorMessage = "Please enter the first address line")]
[Display(Name = "Line 1")]
public string Line1 { get; set; }
[Display(Name = "Line 2")]
public string Line2 { get; set; }
[Display(Name = "Line 3")]
public string Line3 { get; set; }
[Required(ErrorMessage = "Please enter a city name")]
public string City { get; set; }
[Required(ErrorMessage = "Please enter a state name")]
public string State { get; set; }
public string Zip { get; set; }
[Required(ErrorMessage = "Please enter a country name")]
public string Country { get; set; }
public bool GiftWrap { get; set; }
}
}

View File

@ -72,10 +72,14 @@
<Reference Include="System.Xml" /> <Reference Include="System.Xml" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Abstract\IOrderProcessor.cs" />
<Compile Include="Abstract\IProductRepository.cs" /> <Compile Include="Abstract\IProductRepository.cs" />
<Compile Include="Concrete\EFDbContext.cs" /> <Compile Include="Concrete\EFDbContext.cs" />
<Compile Include="Concrete\EFProductRepository.cs" /> <Compile Include="Concrete\EFProductRepository.cs" />
<Compile Include="Concrete\EmailOrderProcessor.cs" />
<Compile Include="Entities\Cart.cs" />
<Compile Include="Entities\Product.cs" /> <Compile Include="Entities\Product.cs" />
<Compile Include="Entities\ShippingDetails.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -0,0 +1,271 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using SportsStore.Domain.Entities;
using System;
using System.Linq;
using System.Security.AccessControl;
using System.Web.Mvc;
using Moq;
using SportsStore.Domain.Abstract;
using SportsStore.WebUI.Controllers;
using SportsStore.WebUI.Models;
namespace SportsStore.UnitTests
{
[TestClass]
public class CartTests
{
[TestMethod]
public void Can_Add_New_Lines()
{
// Arrange - create some test products
Product p1 = new Product { ProductID = 1, Name = "P1" };
Product p2 = new Product { ProductID = 2, Name = "P2" };
// Arrange - create a new cart
Cart target = new Cart();
// Act
target.AddItem(p1, 1);
target.AddItem(p2, 1);
CartLine[] results = target.Lines.ToArray();
// Assert
Assert.AreEqual(results.Length, 2);
Assert.AreEqual(results[0].Product, p1);
Assert.AreEqual(results[1].Product, p2);
}
[TestMethod]
public void Can_Add_Quantity_For_Existing_Lines()
{
// Arrange - create some test products
Product p1 = new Product { ProductID = 1, Name = "P1" };
Product p2 = new Product { ProductID = 2, Name = "P2" };
// Arrange - create a new cart
Cart target = new Cart();
// Act
target.AddItem(p1, 1);
target.AddItem(p2, 1);
target.AddItem(p1, 10);
CartLine[] results = target.Lines.OrderBy(c => c.Product.ProductID).ToArray();
// Assert
Assert.AreEqual(results.Length, 2);
Assert.AreEqual(results[0].Quantity, 11);
Assert.AreEqual(results[1].Quantity, 1);
}
[TestMethod]
public void Can_Remove_Line()
{
// Arrange - create some test products
Product p1 = new Product { ProductID = 1, Name = "P1" };
Product p2 = new Product { ProductID = 2, Name = "P2" };
Product p3 = new Product { ProductID = 3, Name = "P3" };
// Arrange - create a new cart
Cart target = new Cart();
// Arrange - add some products to the cart
target.AddItem(p1, 1);
target.AddItem(p2, 3);
target.AddItem(p3, 5);
target.AddItem(p2, 1);
// Act
target.RemoveLine(p2);
// Assert
Assert.AreEqual(target.Lines.Where(c => c.Product == p2).Count(), 0);
Assert.AreEqual(target.Lines.Count(), 2);
}
[TestMethod]
public void Calculate_Cart_Total()
{
// Arrange - create some test products
Product p1 = new Product { ProductID = 1, Name = "P1", Price = 100M };
Product p2 = new Product { ProductID = 2, Name = "P2", Price = 50M };
// Arrange - create a new cart
Cart target = new Cart();
// Act
target.AddItem(p1, 1);
target.AddItem(p2, 1);
target.AddItem(p1, 3);
decimal result = target.ComputeTotalValue();
// Assert
Assert.AreEqual(result, 450M);
}
[TestMethod]
public void Can_Clear_Contents()
{
// Arrange - create some test products
Product p1 = new Product { ProductID = 1, Name = "P1", Price = 100M };
Product p2 = new Product { ProductID = 2, Name = "P2", Price = 50M };
// Arrange - create a new cart
Cart target = new Cart();
// Arrange - add some items
target.AddItem(p1, 1);
target.AddItem(p2, 1);
// Act - reset the cart
target.Clear();
// Assert
Assert.AreEqual(target.Lines.Count(), 0);
}
[TestMethod]
public void Can_Add_To_Cart()
{
// Arrange - create the mock repository
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new Product[]
{
new Product { ProductID = 1, Name = "P1", Category = "Apples" },
}.AsQueryable());
// Arrange - create a Cart
Cart cart = new Cart();
// Arrange - create the controller
CartController target = new CartController(mock.Object, null);
// Act - add a product to the cart
target.AddToCart(cart, 1, null);
// Assert
Assert.AreEqual(cart.Lines.Count(), 1);
Assert.AreEqual(cart.Lines.ToArray()[0].Product.ProductID, 1);
}
[TestMethod]
public void Adding_Product_To_Cart_Goes_To_Cart_Screen()
{
// Arrange - create the mock repository
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new Product[]
{
new Product { ProductID = 1, Name = "P1", Category = "Apples" },
}.AsQueryable());
// Arrange - create a Cart
Cart cart = new Cart();
// Arrange - create the controller
CartController target = new CartController(mock.Object, null);
// Act - add a product to the
RedirectToRouteResult result = target.AddToCart(cart, 2, "myUrl");
// Assert
Assert.AreEqual(result.RouteValues["action"], "Index");
Assert.AreEqual(result.RouteValues["returnUrl"], "myUrl");
}
[TestMethod]
public void Can_View_Cart_Contents()
{
// Arrange - create a Cart
Cart cart = new Cart();
// Arrange - create the controller
CartController target = new CartController(null, null);
// Act - call the Index action methdo
CartIndexViewModel result = (CartIndexViewModel)target.Index(cart, "myUrl").ViewData.Model;
// Assert
Assert.AreSame(result.Cart, cart);
Assert.AreEqual(result.ReturnUrl, "myUrl");
}
[TestMethod]
public void Cannot_Checkout_Empty_Cart()
{
// Arrange - create a mock order processor
Mock<IOrderProcessor> mock = new Mock<IOrderProcessor>();
// Arrange - create an empty cart
Cart cart = new Cart();
// Arrange - create shipping details
ShippingDetails shippingDetails = new ShippingDetails();
// Arrange - create an instance of the controller
CartController target = new CartController(null, mock.Object);
// Act
ViewResult result = target.Checkout(cart, shippingDetails);
// Assert - check that the order hasn't been passed on to the processor
mock.Verify(m => m.ProcessOrder(It.IsAny<Cart>(), It.IsAny<ShippingDetails>()), Times.Never());
// Assert - check that method is returning the default view
Assert.AreEqual("", result.ViewName);
// Assert - check that I am passing an invalid model to the view
Assert.AreEqual(false, result.ViewData.ModelState.IsValid);
}
[TestMethod]
public void Cannot_Checkout_Invalid_ShippingDetails()
{
// Arrange - create a mock order processor
Mock<IOrderProcessor> mock = new Mock<IOrderProcessor>();
// Arrange - create a cart with an item
Cart cart = new Cart();
cart.AddItem(new Product(), 1);
// Arrange - create an instance of the controller
CartController target = new CartController(null, mock.Object);
// use null to inject repo, as we don't need it
// Arrange - add an error to the model
target.ModelState.AddModelError("error", "error");
// Act - try to checkout
ViewResult result = target.Checkout(cart, new ShippingDetails());
// Assert - check that the order hasn't been passed on to the processor
mock.Verify(m => m.ProcessOrder(It.IsAny<Cart>(), It.IsAny<ShippingDetails>()),
Times.Never());
// Assert - check that the method is returning the default view
Assert.AreEqual("", result.ViewName);
// Assert - check that I am passing an invalid model to the view
Assert.AreEqual(false, result.ViewData.ModelState.IsValid);
}
[TestMethod]
public void Can_Checkout_And_Submit_Order()
{
// Arrange - create a mock order processor
Mock<IOrderProcessor> mock = new Mock<IOrderProcessor>();
// Arrange - create a cart with an item
Cart cart = new Cart();
cart.AddItem(new Product(), 1);
// Arrange - create an instance of the controller
CartController target = new CartController(null, mock.Object);
// Act - try to checkout
ViewResult result = target.Checkout(cart, new ShippingDetails());
// Assert - check that the order has been passed on to the processor
mock.Verify(m => m.ProcessOrder(It.IsAny<Cart>(), It.IsAny<ShippingDetails>()),
Times.Once());
// Assert - check that the method is returning the Completed view
Assert.AreEqual("Completed", result.ViewName);
// Assert - check that I am passing a valid model to the view
Assert.AreEqual(true, result.ViewData.ModelState.IsValid);
}
}
}

View File

@ -90,6 +90,7 @@
<Compile Include="App_Start\NinjectWebCommon.cs" /> <Compile Include="App_Start\NinjectWebCommon.cs" />
<Compile Include="UnitTest1.cs" /> <Compile Include="UnitTest1.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="CartTests.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="app.config" /> <None Include="app.config" />

View File

@ -31,7 +31,7 @@ namespace SportsStore.UnitTests
ProductController controller = new ProductController(mock.Object); ProductController controller = new ProductController(mock.Object);
controller.PageSize = 3; controller.PageSize = 3;
// Act // Act
ProductsListViewModel result = (ProductsListViewModel)controller.List(2).Model; ProductsListViewModel result = (ProductsListViewModel)controller.List(null, 2).Model;
// Assert // Assert
Product[] prodArray = result.Products.ToArray(); Product[] prodArray = result.Products.ToArray();
Assert.IsTrue(prodArray.Length == 2); Assert.IsTrue(prodArray.Length == 2);
@ -72,12 +72,13 @@ namespace SportsStore.UnitTests
// Arrange // Arrange
Mock<IProductRepository> mock = new Mock<IProductRepository>(); Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new Product[] { mock.Setup(m => m.Products).Returns(new Product[]
new Product {ProductID = 1, Name = "P1"}, {
new Product {ProductID = 2, Name = "P2"}, new Product { ProductID = 1, Name = "P1" },
new Product {ProductID = 3, Name = "P3"}, new Product { ProductID = 2, Name = "P2" },
new Product {ProductID = 4, Name = "P4"}, new Product { ProductID = 3, Name = "P3" },
new Product {ProductID = 5, Name = "P5"} new Product { ProductID = 4, Name = "P4" },
new Product { ProductID = 5, Name = "P5" }
}); });
// Arrange // Arrange
@ -85,7 +86,7 @@ namespace SportsStore.UnitTests
controller.PageSize = 3; controller.PageSize = 3;
// Act // Act
ProductsListViewModel result = (ProductsListViewModel)controller.List(2).Model; ProductsListViewModel result = (ProductsListViewModel)controller.List(null, 2).Model;
// Assert // Assert
PagingInfo pageInfo = result.PagingInfo; PagingInfo pageInfo = result.PagingInfo;
@ -94,5 +95,120 @@ namespace SportsStore.UnitTests
Assert.AreEqual(pageInfo.TotalItems, 5); Assert.AreEqual(pageInfo.TotalItems, 5);
Assert.AreEqual(pageInfo.TotalPages, 2); Assert.AreEqual(pageInfo.TotalPages, 2);
} }
[TestMethod]
public void Can_Filter_Products()
{
// Arrange
// - create the mock repository
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new Product[]
{
new Product { ProductID = 1, Name = "P1", Category = "Cat1" },
new Product { ProductID = 2, Name = "P2", Category = "Cat2" },
new Product { ProductID = 3, Name = "P3", Category = "Cat1" },
new Product { ProductID = 4, Name = "P4", Category = "Cat2" },
new Product { ProductID = 5, Name = "P5", Category = "Cat3" }
});
// Arrange - create a controller and make the page size 3 items
ProductController controller = new ProductController(mock.Object);
controller.PageSize = 3;
// Action
Product[] result = ((ProductsListViewModel)controller.List("Cat2", 1).Model)
.Products.ToArray();
// Assert
Assert.AreEqual(result.Length, 2);
Assert.IsTrue(result[0].Name == "P2" && result[0].Category == "Cat2");
Assert.IsTrue(result[1].Name == "P4" && result[1].Category == "Cat2");
}
[TestMethod]
public void Can_Create_Categories()
{
// Arrange
// - create the mock repository
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new Product[]
{
new Product { ProductID = 1, Name = "P1", Category = "Apples" },
new Product { ProductID = 2, Name = "P2", Category = "Apples" },
new Product { ProductID = 3, Name = "P3", Category = "Plums" },
new Product { ProductID = 4, Name = "P4", Category = "Oranges" },
});
// Arrange - create the controller
NavController target = new NavController(mock.Object);
// Act = get the set of categories
string[] results = ((IEnumerable<string>)target.Menu().Model).ToArray();
// Assert
Assert.AreEqual(results.Length, 3);
Assert.AreEqual(results[0], "Apples");
Assert.AreEqual(results[1], "Oranges");
Assert.AreEqual(results[2], "Plums");
}
[TestMethod]
public void Indicates_Selected_Category()
{
// Arrange
// - create the mock repository
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new Product[]
{
new Product { ProductID = 1, Name = "P1", Category = "Apples" },
new Product { ProductID = 4, Name = "P2", Category = "Oranges" },
});
// Arrange - create the controller
NavController target = new NavController(mock.Object);
// Arrange - define the category to selected
string categoryToSelected = "Apples";
// Action
string result = target.Menu(categoryToSelected).ViewBag.SelectedCategory;
// Assert
Assert.AreEqual(categoryToSelected, result);
}
[TestMethod]
public void Generate_Category_Specific_Product_Count()
{
// Arrange
// - create the mock repository
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new Product[]
{
new Product { ProductID = 1, Name = "P1", Category = "Cat1" },
new Product { ProductID = 2, Name = "P2", Category = "Cat2" },
new Product { ProductID = 3, Name = "P3", Category = "Cat1" },
new Product { ProductID = 4, Name = "P4", Category = "Cat2" },
new Product { ProductID = 5, Name = "P5", Category = "Cat3" }
});
// Arrange - create a controller and make the page size 3 items
ProductController target = new ProductController(mock.Object);
target.PageSize = 3;
// Action - test the product counts for different categories
int res1 = ((ProductsListViewModel)target
.List("Cat1").Model).PagingInfo.TotalItems;
int res2 = ((ProductsListViewModel)target
.List("Cat2").Model).PagingInfo.TotalItems;
int res3 = ((ProductsListViewModel)target
.List("Cat3").Model).PagingInfo.TotalItems;
int resAll = ((ProductsListViewModel)target
.List(null).Model).PagingInfo.TotalItems;
// Assert
Assert.AreEqual(res1, 2);
Assert.AreEqual(res2, 2);
Assert.AreEqual(res3, 1);
Assert.AreEqual(resAll, 5);
}
} }
} }

View File

@ -5,6 +5,7 @@ using System.Web;
using System.Web.Mvc; using System.Web.Mvc;
using System.Web.Routing; using System.Web.Routing;
using System.Web.UI; using System.Web.UI;
using Ninject.Infrastructure.Language;
namespace SportsStore.WebUI namespace SportsStore.WebUI
{ {
@ -14,17 +15,49 @@ namespace SportsStore.WebUI
{ {
routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute( routes.MapRoute(null,
name: null, "",
url: "Page{page}", new
defaults: new { Controller = "Product", action = "List"} {
); controller = "Product", action = "List",
category = (string)null, page = 1
});
routes.MapRoute( routes.MapRoute(null,
name: "Default", "Page{page}",
url: "{controller}/{action}/{id}", new
defaults: new { controller = "Product", action = "List", id = UrlParameter.Optional } {
); controller = "Product",
action = "List",
category = (string)null
},
new
{
page = @"\d+"
});
routes.MapRoute(null,
"{category}",
new
{
controller = "Product",
action = "List",
page = 1
});
routes.MapRoute(null,
"{category}/Page{page}",
new
{
controller = "Product",
action = "List"
},
new
{
page = @"\d+"
});
routes.MapRoute(null, "{controller}/{action}");
} }
} }
} }

View File

@ -0,0 +1,21 @@
.field-validation-error     {
color: #f00;
}
.field-validation-valid     {
display: none;
}
.input-validation-error     {
border: 1px solid #f00;
background-color: #fee;
}
.validation-summary-errors {
font-weight: bold;
color: #f00;
}
.validation-summary-valid   {
display: none;
}

View File

@ -0,0 +1,88 @@
using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using SportsStore.WebUI.Models;
namespace SportsStore.WebUI.Controllers
{
public class CartController : Controller
{
private IProductRepository repository;
private IOrderProcessor orderProcessor;
public CartController(IProductRepository repo, IOrderProcessor proc)
{
repository = repo;
orderProcessor = proc;
}
public ViewResult Index(Cart cart, string returnUrl)
{
return View(new CartIndexViewModel
{
Cart = cart,
ReturnUrl = returnUrl
});
}
// GET: Cart
public RedirectToRouteResult AddToCart(Cart cart, int productId, string returnUrl)
{
Product product = repository.Products
.FirstOrDefault(p => p.ProductID == productId);
if (product != null)
{
cart.AddItem(product, 1);
}
return RedirectToAction("Index", new { returnUrl });
}
public RedirectToRouteResult RemoveFromCart(Cart cart, int productId, string returnUrl)
{
Product product = repository.Products
.FirstOrDefault(p => p.ProductID == productId);
if (product != null)
{
cart.RemoveLine(product);
}
return RedirectToAction("Index", new { returnUrl });
}
public PartialViewResult Summary(Cart cart)
{
return PartialView(cart);
}
// GET
public ViewResult Checkout()
{
return View(new ShippingDetails());
}
// POST
[HttpPost]
public ViewResult Checkout(Cart cart, ShippingDetails shippingDetails)
{
if (cart.Lines.Count() == 0)
{
ModelState.AddModelError("", "Sorry, your cart is empty");
}
if (ModelState.IsValid)
{
orderProcessor.ProcessOrder(cart, shippingDetails);
cart.Clear();
return View("Completed");
}
else
{
return View(shippingDetails);
}
}
}
}

View File

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using SportsStore.Domain.Abstract;
namespace SportsStore.WebUI.Controllers
{
public class NavController : Controller
{
private IProductRepository repository;
public NavController(IProductRepository repo)
{
repository = repo;
}
public PartialViewResult Menu(string category = null,
bool horizontalLayout = false)
{
ViewBag.SelectedCategory = category;
IEnumerable<string> categories = repository.Products
.Select(x => x.Category)
.Distinct()
.OrderBy(x => x);
string viewName = horizontalLayout ? "MenuHorizontal" : "Menu";
return PartialView(viewName, categories);
}
}
}

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Web; using System.Web;
using System.Web.Mvc; using System.Web.Mvc;
@ -18,11 +19,12 @@ namespace SportsStore.WebUI.Controllers
this.repository = productRepository; this.repository = productRepository;
} }
public ViewResult List(int page = 1) public ViewResult List(string category, int page = 1)
{ {
ProductsListViewModel model = new ProductsListViewModel ProductsListViewModel model = new ProductsListViewModel
{ {
Products = repository.Products Products = repository.Products
.Where(p => category == null || p.Category == category)
.OrderBy(p => p.ProductID) .OrderBy(p => p.ProductID)
.Skip((page - 1) * PageSize) .Skip((page - 1) * PageSize)
.Take(PageSize), .Take(PageSize),
@ -30,8 +32,11 @@ namespace SportsStore.WebUI.Controllers
{ {
CurrentPage = page, CurrentPage = page,
ItemsPerPage = PageSize, ItemsPerPage = PageSize,
TotalItems = repository.Products.Count() TotalItems = category == null ?
} repository.Products.Count() :
repository.Products.Where(e => e.Category == category).Count()
},
CurrentCategory = category
}; };
return View(model); return View(model);
} }

View File

@ -4,6 +4,8 @@ using System.Linq;
using System.Web; using System.Web;
using System.Web.Mvc; using System.Web.Mvc;
using System.Web.Routing; using System.Web.Routing;
using SportsStore.Domain.Entities;
using SportsStore.WebUI.Infrastructure.Binders;
namespace SportsStore.WebUI namespace SportsStore.WebUI
{ {
@ -13,6 +15,7 @@ namespace SportsStore.WebUI
{ {
AreaRegistration.RegisterAllAreas(); AreaRegistration.RegisterAllAreas();
RouteConfig.RegisterRoutes(RouteTable.Routes); RouteConfig.RegisterRoutes(RouteTable.Routes);
ModelBinders.Binders.Add(typeof(Cart), new CartModelBinder());
} }
} }
} }

View File

@ -0,0 +1,34 @@
using System.Web.Mvc;
using SportsStore.Domain.Entities;
namespace SportsStore.WebUI.Infrastructure.Binders
{
public class CartModelBinder : IModelBinder
{
private const string sessionKey = "Cart";
public object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
// get the Cart from the session
Cart cart = null;
if (controllerContext.HttpContext.Session != null)
{
cart = (Cart)controllerContext.HttpContext.Session[sessionKey];
}
// create the Cart if there wasn't one in the session data
if (cart == null)
{
cart = new Cart();
if (controllerContext.HttpContext.Session != null)
{
controllerContext.HttpContext.Session[sessionKey] = cart;
}
}
// return the cart
return cart;
}
}
}

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Configuration;
using System.Linq; using System.Linq;
using System.Web; using System.Web;
using System.Web.Mvc; using System.Web.Mvc;
@ -35,8 +36,14 @@ namespace SportsStore.WebUI.Infrastructure
private void AddBindings() private void AddBindings()
{ {
kernel.Bind<IProductRepository>().To<EFProductRepository>(); kernel.Bind<IProductRepository>().To<EFProductRepository>();
EmailSettings emailSettings = new EmailSettings
{
WriteAsFile = bool.Parse(ConfigurationManager.AppSettings["Email.WriteAsFile"] ?? "false")
};
kernel.Bind<IOrderProcessor>().To<EmailOrderProcessor>()
.WithConstructorArgument("settings", emailSettings);
} }
} }
} }

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using SportsStore.Domain.Entities;
namespace SportsStore.WebUI.Models
{
public class CartIndexViewModel
{
public Cart Cart { get; set; }
public string ReturnUrl { get; set; }
}
}

View File

@ -10,5 +10,6 @@ namespace SportsStore.WebUI.Models
{ {
public IEnumerable<Product> Products { get; set; } public IEnumerable<Product> Products { get; set; }
public PagingInfo PagingInfo { get; set; } public PagingInfo PagingInfo { get; set; }
public string CurrentCategory { get; set; }
} }
} }

View File

@ -114,6 +114,7 @@
<Content Include="Content\bootstrap-theme.min.css" /> <Content Include="Content\bootstrap-theme.min.css" />
<Content Include="Content\bootstrap.css" /> <Content Include="Content\bootstrap.css" />
<Content Include="Content\bootstrap.min.css" /> <Content Include="Content\bootstrap.min.css" />
<Content Include="Content\ErrorStyles.css" />
<Content Include="Content\Site.css" /> <Content Include="Content\Site.css" />
<Content Include="fonts\glyphicons-halflings-regular.svg" /> <Content Include="fonts\glyphicons-halflings-regular.svg" />
<Content Include="Global.asax" /> <Content Include="Global.asax" />
@ -130,12 +131,16 @@
<ItemGroup> <ItemGroup>
<Compile Include="App_Start\NinjectWebCommon.cs" /> <Compile Include="App_Start\NinjectWebCommon.cs" />
<Compile Include="App_Start\RouteConfig.cs" /> <Compile Include="App_Start\RouteConfig.cs" />
<Compile Include="Controllers\CartController.cs" />
<Compile Include="Controllers\NavController.cs" />
<Compile Include="Controllers\ProductController.cs" /> <Compile Include="Controllers\ProductController.cs" />
<Compile Include="Global.asax.cs"> <Compile Include="Global.asax.cs">
<DependentUpon>Global.asax</DependentUpon> <DependentUpon>Global.asax</DependentUpon>
</Compile> </Compile>
<Compile Include="HtmlHelpers\PagingHelpers.cs" /> <Compile Include="HtmlHelpers\PagingHelpers.cs" />
<Compile Include="Infrastructure\Binders\CartModelBinder.cs" />
<Compile Include="Infrastructure\NinjectDependencyResolver.cs" /> <Compile Include="Infrastructure\NinjectDependencyResolver.cs" />
<Compile Include="Models\CartIndexViewModel.cs" />
<Compile Include="Models\PagingInfo.cs" /> <Compile Include="Models\PagingInfo.cs" />
<Compile Include="Models\ProductsListViewModel.cs" /> <Compile Include="Models\ProductsListViewModel.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
@ -148,6 +153,11 @@
<Content Include="Views\Product\List.cshtml" /> <Content Include="Views\Product\List.cshtml" />
<Content Include="Scripts\jquery-1.9.0.min.map" /> <Content Include="Scripts\jquery-1.9.0.min.map" />
<Content Include="Views\Shared\ProductSummary.cshtml" /> <Content Include="Views\Shared\ProductSummary.cshtml" />
<Content Include="Views\Nav\Menu.cshtml" />
<Content Include="Views\Cart\Index.cshtml" />
<Content Include="Views\Cart\Summary.cshtml" />
<Content Include="Views\Cart\Checkout.cshtml" />
<Content Include="Views\Cart\Completed.cshtml" />
<None Include="Web.Debug.config"> <None Include="Web.Debug.config">
<DependentUpon>Web.config</DependentUpon> <DependentUpon>Web.config</DependentUpon>
</None> </None>

View File

@ -0,0 +1,42 @@
@model SportsStore.Domain.Entities.ShippingDetails
@{
ViewBag.Title = "SportStore: Checkout";
}
<h2>Check out now</h2>
<p>Please enter your details, and we'll ship your goods right now</p>
@using (Html.BeginForm())
{
@Html.ValidationSummary()
<h3>Ship to</h3>
<div class="form-group">
<label>Name:</label>
@Html.TextBoxFor(x => x.Name, new { @class = "form-control" })
</div>
<h3>Address</h3>
foreach (var property in ViewData.ModelMetadata.Properties)
{
if (property.PropertyName != "Name" && property.PropertyName != "GiftWrap")
{
<div class="form-group">
<label>@(property.DisplayName ?? property.PropertyName)</label>
@Html.TextBox(property.PropertyName, null, new { @class = "form-control" })
</div>
}
}
<h3>Options</h3>
<div class="checkbox">
<label>
@Html.EditorFor(x => x.GiftWrap)
Gift wrap these items
</label>
</div>
<div class="text-center">
<input class="btn btn-primary" type="submit" value="Complete order" />
</div>
}

View File

@ -0,0 +1,6 @@
@{
ViewBag.Title = "SportsStore: Order Submitted";
}
<h2>Thanks!</h2>
Thanks for placing your order. We'll ship your goods as soon as possible.

View File

@ -0,0 +1,59 @@
@model SportsStore.WebUI.Models.CartIndexViewModel
@{
ViewBag.Title = "Sports Store: Your Cart";
}
@*Add style tag of cart table*@
<style>
#cartTable td { vertical-align: middle; }
</style>
<h2>Your cart</h2>
@*Add id*@
<table id="cartTable" class="table">
<thead>
<tr>
<th>Quantity</th>
<th>Item</th>
<th class="text-right">Price</th>
<th class="text-right">Subtotal</th>
</tr>
</thead>
<tbody>
@foreach (var line in Model.Cart.Lines) {
<tr>
<td class="text-center">@line.Quantity</td>
<td class="text-left">@line.Product.Name</td>
<td class="text-right">@line.Product.Price.ToString("c")</td>
<td class="text-right">
@((line.Quantity * line.Product.Price).ToString("c"))
</td>
@*Add button*@
<td>
@using (Html.BeginForm("RemoveFromCart", "Cart"))
{
@Html.Hidden("ProductId", line.Product.ProductID)
@Html.HiddenFor(x => x.ReturnUrl)
<input class="btn btn-sm btn-warning"
type="submit" value="Remove" />
}
</td>
</tr>
}
</tbody>
<tfoot>
<tr>
<td colspan="3" class="text-right">Total:</td>
<td class="text-right">
@Model.Cart.ComputeTotalValue().ToString("c")
</td>
</tr>
</tfoot>
</table>
<div class="text-center">
<a class="btn btn-primary" href="@Model.ReturnUrl">Continue shopping</a>
@Html.ActionLink("Checkout now", "Checkout", null, new { @class = "btn btn-primary" })
</div>

View File

@ -0,0 +1,20 @@
@model SportsStore.Domain.Entities.Cart
<div class="navbar-right hidden-xs">
@Html.ActionLink("Checkout", "Index", "Cart",
new { returnUrl = Request.Url.PathAndQuery },
new { @class = "btn btn-default navbar-btn" } )
</div>
<div class="navbar-right visible-xs">
<a href=@Url.Action("Index", "Cart", new { returnUrl = Request.Url.PathAndQuery })
class="btn btn-default navbar-btn">
<span class="glyphicon glyphicon-shopping-cart"></span>
</a>
</div>
<div class="navbar-text navbar-right">
<b class="hidden-xs">Your cart:</b>
@Model.Lines.Sum(x => x.Quantity) item(s),
@Model.ComputeTotalValue().ToString("c")
</div>

View File

@ -0,0 +1,21 @@
@using System.ServiceModel.Syndication
@model IEnumerable<string>
@Html.ActionLink("Home", "List", "Product", null,
new { @class = "btn btn-block btn-default btn-lg" })
@foreach (var link in Model)
{
@Html.RouteLink(link,
new
{
controller = "Product",
action = "List",
category = link,
page = 1
},
new
{
@class = "btn btn-block btn-default btn-lg" + (link == ViewBag.SelectedCategory ? " btn-primary" : "")
})
}

View File

@ -10,6 +10,7 @@
@Html.Partial("ProductSummary", p) @Html.Partial("ProductSummary", p)
} }
<div class="pager"> <div class="btn-group pull-right">
@Html.PageLinks(Model.PagingInfo, x => Url.Action("List", new { page = x})) @Html.PageLinks(Model.PagingInfo, x => Url.Action("List",
new { page = x, category = Model.CurrentCategory }))
</div> </div>

View File

@ -5,5 +5,14 @@
<strong>@Model.Name</strong> <strong>@Model.Name</strong>
<span class="pull-right label label-primary">@Model.Price.ToString("c")</span> <span class="pull-right label label-primary">@Model.Price.ToString("c")</span>
</h3> </h3>
@using (Html.BeginForm("AddToCart", "Cart"))
{
<div class="pull-right">
@Html.HiddenFor(x => x.ProductID)
@Html.Hidden("returnUrl", Request.Url.PathAndQuery)
<input type="submit" class="btn btn-success" value="Add to cart" />
</div>
}
<span class="lead">@Model.Description</span> <span class="lead">@Model.Description</span>
</div> </div>

View File

@ -5,17 +5,29 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="~/Content/bootstrap.css" rel="stylesheet"/> <link href="~/Content/bootstrap.css" rel="stylesheet"/>
<link href="~/Content/bootstrap-theme.css" rel="stylesheet"/> <link href="~/Content/bootstrap-theme.css" rel="stylesheet"/>
<link href="~/Content/ErrorStyles.css" rel="stylesheet"/>
<title>@ViewBag.Title</title> <title>@ViewBag.Title</title>
<style>
.navbar-right {
float: right !important;
margin-right: 15px; margin-left: 15px;
}
</style>
</head> </head>
<body> <body>
<div class="navbar navbar-inverse" role="navigation"> <div class="navbar navbar-inverse" role="navigation">
<a class="navbar-brand" href="#">SPORTS STORE</a> <a class="navbar-brand" href="#">
<span class="hidden-xs">SPORTS STORE</span>
<span class="visible-xs">SPORTS</span>
<span class="visible-xs">STORE</span>
</a>
@Html.Action("Summary", "Cart")
</div> </div>
<div class="row panel"> <div class="row panel">
<div id="categoreis" class="col-xs-3"> <div class="col-sm-3 hidden-xs">
Put something useful here later @Html.Action("Menu", "Nav")
</div> </div>
<div class="col-xs-8"> <div class="col-xs-12 col-sm-8">
@RenderBody() @RenderBody()
</div> </div>
</div> </div>

View File

@ -16,6 +16,7 @@
<add key="webpages:Enabled" value="false" /> <add key="webpages:Enabled" value="false" />
<add key="ClientValidationEnabled" value="true" /> <add key="ClientValidationEnabled" value="true" />
<add key="UnobtrusiveJavaScriptEnabled" value="true" /> <add key="UnobtrusiveJavaScriptEnabled" value="true" />
<add key="Email.WriteAsFile" value="true"/>
</appSettings> </appSettings>
<system.web> <system.web>
<compilation debug="true" targetFramework="4.5.1" /> <compilation debug="true" targetFramework="4.5.1" />