Exercise Files
This commit is contained in:
485
Ch07/07_02_End/Website/Features/Account/AccountController.cs
Normal file
485
Ch07/07_02_End/Website/Features/Account/AccountController.cs
Normal file
@ -0,0 +1,485 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using System.Web.Mvc;
|
||||
using Microsoft.AspNet.Identity;
|
||||
using Microsoft.AspNet.Identity.Owin;
|
||||
using Microsoft.Owin.Security;
|
||||
using HPlusSports.Models;
|
||||
|
||||
namespace HPlusSports.Controllers
|
||||
{
|
||||
[Authorize]
|
||||
public class AccountController : Controller
|
||||
{
|
||||
private ApplicationSignInManager _signInManager;
|
||||
private ApplicationUserManager _userManager;
|
||||
|
||||
public AccountController()
|
||||
{
|
||||
}
|
||||
|
||||
public AccountController(ApplicationUserManager userManager, ApplicationSignInManager signInManager)
|
||||
{
|
||||
UserManager = userManager;
|
||||
SignInManager = signInManager;
|
||||
}
|
||||
|
||||
public ApplicationSignInManager SignInManager
|
||||
{
|
||||
get
|
||||
{
|
||||
return _signInManager ?? HttpContext.GetOwinContext().Get<ApplicationSignInManager>();
|
||||
}
|
||||
private set
|
||||
{
|
||||
_signInManager = value;
|
||||
}
|
||||
}
|
||||
|
||||
public ApplicationUserManager UserManager
|
||||
{
|
||||
get
|
||||
{
|
||||
return _userManager ?? HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
|
||||
}
|
||||
private set
|
||||
{
|
||||
_userManager = value;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// GET: /Account/Login
|
||||
[AllowAnonymous]
|
||||
public ActionResult Login(string returnUrl)
|
||||
{
|
||||
ViewBag.ReturnUrl = returnUrl;
|
||||
return View();
|
||||
}
|
||||
|
||||
//
|
||||
// POST: /Account/Login
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
// This doesn't count login failures towards account lockout
|
||||
// To enable password failures to trigger account lockout, change to shouldLockout: true
|
||||
var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);
|
||||
switch (result)
|
||||
{
|
||||
case SignInStatus.Success:
|
||||
return RedirectToLocal(returnUrl);
|
||||
case SignInStatus.LockedOut:
|
||||
return View("Lockout");
|
||||
case SignInStatus.RequiresVerification:
|
||||
return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
|
||||
case SignInStatus.Failure:
|
||||
default:
|
||||
ModelState.AddModelError("", "Invalid login attempt.");
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// GET: /Account/VerifyCode
|
||||
[AllowAnonymous]
|
||||
public async Task<ActionResult> VerifyCode(string provider, string returnUrl, bool rememberMe)
|
||||
{
|
||||
// Require that the user has already logged in via username/password or external login
|
||||
if (!await SignInManager.HasBeenVerifiedAsync())
|
||||
{
|
||||
return View("Error");
|
||||
}
|
||||
return View(new VerifyCodeViewModel { Provider = provider, ReturnUrl = returnUrl, RememberMe = rememberMe });
|
||||
}
|
||||
|
||||
//
|
||||
// POST: /Account/VerifyCode
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<ActionResult> VerifyCode(VerifyCodeViewModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
// The following code protects for brute force attacks against the two factor codes.
|
||||
// If a user enters incorrect codes for a specified amount of time then the user account
|
||||
// will be locked out for a specified amount of time.
|
||||
// You can configure the account lockout settings in IdentityConfig
|
||||
var result = await SignInManager.TwoFactorSignInAsync(model.Provider, model.Code, isPersistent: model.RememberMe, rememberBrowser: model.RememberBrowser);
|
||||
switch (result)
|
||||
{
|
||||
case SignInStatus.Success:
|
||||
return RedirectToLocal(model.ReturnUrl);
|
||||
case SignInStatus.LockedOut:
|
||||
return View("Lockout");
|
||||
case SignInStatus.Failure:
|
||||
default:
|
||||
ModelState.AddModelError("", "Invalid code.");
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// GET: /Account/Register
|
||||
[AllowAnonymous]
|
||||
public ActionResult Register()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
//
|
||||
// POST: /Account/Register
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<ActionResult> Register(RegisterViewModel model)
|
||||
{
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
|
||||
var result = await UserManager.CreateAsync(user, model.Password);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
|
||||
|
||||
// For more information on how to enable account confirmation and password reset please visit https://go.microsoft.com/fwlink/?LinkID=320771
|
||||
// Send an email with this link
|
||||
// string code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
|
||||
// var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);
|
||||
// await UserManager.SendEmailAsync(user.Id, "Confirm your account", "Please confirm your account by clicking <a href=\"" + callbackUrl + "\">here</a>");
|
||||
|
||||
return RedirectToAction("Index", "Home");
|
||||
}
|
||||
AddErrors(result);
|
||||
}
|
||||
|
||||
// If we got this far, something failed, redisplay form
|
||||
return View(model);
|
||||
}
|
||||
|
||||
//
|
||||
// GET: /Account/ConfirmEmail
|
||||
[AllowAnonymous]
|
||||
public async Task<ActionResult> ConfirmEmail(string userId, string code)
|
||||
{
|
||||
if (userId == null || code == null)
|
||||
{
|
||||
return View("Error");
|
||||
}
|
||||
var result = await UserManager.ConfirmEmailAsync(userId, code);
|
||||
return View(result.Succeeded ? "ConfirmEmail" : "Error");
|
||||
}
|
||||
|
||||
//
|
||||
// GET: /Account/ForgotPassword
|
||||
[AllowAnonymous]
|
||||
public ActionResult ForgotPassword()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
//
|
||||
// POST: /Account/ForgotPassword
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<ActionResult> ForgotPassword(ForgotPasswordViewModel model)
|
||||
{
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
var user = await UserManager.FindByNameAsync(model.Email);
|
||||
if (user == null || !(await UserManager.IsEmailConfirmedAsync(user.Id)))
|
||||
{
|
||||
// Don't reveal that the user does not exist or is not confirmed
|
||||
return View("ForgotPasswordConfirmation");
|
||||
}
|
||||
|
||||
// For more information on how to enable account confirmation and password reset please visit https://go.microsoft.com/fwlink/?LinkID=320771
|
||||
// Send an email with this link
|
||||
// string code = await UserManager.GeneratePasswordResetTokenAsync(user.Id);
|
||||
// var callbackUrl = Url.Action("ResetPassword", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);
|
||||
// await UserManager.SendEmailAsync(user.Id, "Reset Password", "Please reset your password by clicking <a href=\"" + callbackUrl + "\">here</a>");
|
||||
// return RedirectToAction("ForgotPasswordConfirmation", "Account");
|
||||
}
|
||||
|
||||
// If we got this far, something failed, redisplay form
|
||||
return View(model);
|
||||
}
|
||||
|
||||
//
|
||||
// GET: /Account/ForgotPasswordConfirmation
|
||||
[AllowAnonymous]
|
||||
public ActionResult ForgotPasswordConfirmation()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
//
|
||||
// GET: /Account/ResetPassword
|
||||
[AllowAnonymous]
|
||||
public ActionResult ResetPassword(string code)
|
||||
{
|
||||
return code == null ? View("Error") : View();
|
||||
}
|
||||
|
||||
//
|
||||
// POST: /Account/ResetPassword
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<ActionResult> ResetPassword(ResetPasswordViewModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
var user = await UserManager.FindByNameAsync(model.Email);
|
||||
if (user == null)
|
||||
{
|
||||
// Don't reveal that the user does not exist
|
||||
return RedirectToAction("ResetPasswordConfirmation", "Account");
|
||||
}
|
||||
var result = await UserManager.ResetPasswordAsync(user.Id, model.Code, model.Password);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
return RedirectToAction("ResetPasswordConfirmation", "Account");
|
||||
}
|
||||
AddErrors(result);
|
||||
return View();
|
||||
}
|
||||
|
||||
//
|
||||
// GET: /Account/ResetPasswordConfirmation
|
||||
[AllowAnonymous]
|
||||
public ActionResult ResetPasswordConfirmation()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
//
|
||||
// POST: /Account/ExternalLogin
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public ActionResult ExternalLogin(string provider, string returnUrl)
|
||||
{
|
||||
// Request a redirect to the external login provider
|
||||
return new ChallengeResult(provider, Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl }));
|
||||
}
|
||||
|
||||
//
|
||||
// GET: /Account/SendCode
|
||||
[AllowAnonymous]
|
||||
public async Task<ActionResult> SendCode(string returnUrl, bool rememberMe)
|
||||
{
|
||||
var userId = await SignInManager.GetVerifiedUserIdAsync();
|
||||
if (userId == null)
|
||||
{
|
||||
return View("Error");
|
||||
}
|
||||
var userFactors = await UserManager.GetValidTwoFactorProvidersAsync(userId);
|
||||
var factorOptions = userFactors.Select(purpose => new SelectListItem { Text = purpose, Value = purpose }).ToList();
|
||||
return View(new SendCodeViewModel { Providers = factorOptions, ReturnUrl = returnUrl, RememberMe = rememberMe });
|
||||
}
|
||||
|
||||
//
|
||||
// POST: /Account/SendCode
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<ActionResult> SendCode(SendCodeViewModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
// Generate the token and send it
|
||||
if (!await SignInManager.SendTwoFactorCodeAsync(model.SelectedProvider))
|
||||
{
|
||||
return View("Error");
|
||||
}
|
||||
return RedirectToAction("VerifyCode", new { Provider = model.SelectedProvider, ReturnUrl = model.ReturnUrl, RememberMe = model.RememberMe });
|
||||
}
|
||||
|
||||
//
|
||||
// GET: /Account/ExternalLoginCallback
|
||||
[AllowAnonymous]
|
||||
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
|
||||
{
|
||||
var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
|
||||
if (loginInfo == null)
|
||||
{
|
||||
return RedirectToAction("Login");
|
||||
}
|
||||
|
||||
// Sign in the user with this external login provider if the user already has a login
|
||||
var result = await SignInManager.ExternalSignInAsync(loginInfo, isPersistent: false);
|
||||
switch (result)
|
||||
{
|
||||
case SignInStatus.Success:
|
||||
return RedirectToLocal(returnUrl);
|
||||
case SignInStatus.LockedOut:
|
||||
return View("Lockout");
|
||||
case SignInStatus.RequiresVerification:
|
||||
return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = false });
|
||||
case SignInStatus.Failure:
|
||||
default:
|
||||
// If the user does not have an account, then prompt the user to create an account
|
||||
ViewBag.ReturnUrl = returnUrl;
|
||||
ViewBag.LoginProvider = loginInfo.Login.LoginProvider;
|
||||
return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = loginInfo.Email });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// POST: /Account/ExternalLoginConfirmation
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<ActionResult> ExternalLoginConfirmation(ExternalLoginConfirmationViewModel model, string returnUrl)
|
||||
{
|
||||
if (User.Identity.IsAuthenticated)
|
||||
{
|
||||
return RedirectToAction("Index", "Manage");
|
||||
}
|
||||
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
// Get the information about the user from the external login provider
|
||||
var info = await AuthenticationManager.GetExternalLoginInfoAsync();
|
||||
if (info == null)
|
||||
{
|
||||
return View("ExternalLoginFailure");
|
||||
}
|
||||
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
|
||||
var result = await UserManager.CreateAsync(user);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
result = await UserManager.AddLoginAsync(user.Id, info.Login);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
}
|
||||
AddErrors(result);
|
||||
}
|
||||
|
||||
ViewBag.ReturnUrl = returnUrl;
|
||||
return View(model);
|
||||
}
|
||||
|
||||
//
|
||||
// POST: /Account/LogOff
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public ActionResult LogOff()
|
||||
{
|
||||
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
|
||||
return RedirectToAction("Index", "Home");
|
||||
}
|
||||
|
||||
//
|
||||
// GET: /Account/ExternalLoginFailure
|
||||
[AllowAnonymous]
|
||||
public ActionResult ExternalLoginFailure()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
if (_userManager != null)
|
||||
{
|
||||
_userManager.Dispose();
|
||||
_userManager = null;
|
||||
}
|
||||
|
||||
if (_signInManager != null)
|
||||
{
|
||||
_signInManager.Dispose();
|
||||
_signInManager = null;
|
||||
}
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Helpers
|
||||
// Used for XSRF protection when adding external logins
|
||||
private const string XsrfKey = "XsrfId";
|
||||
|
||||
private IAuthenticationManager AuthenticationManager
|
||||
{
|
||||
get
|
||||
{
|
||||
return HttpContext.GetOwinContext().Authentication;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddErrors(IdentityResult result)
|
||||
{
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
ModelState.AddModelError("", error);
|
||||
}
|
||||
}
|
||||
|
||||
private ActionResult RedirectToLocal(string returnUrl)
|
||||
{
|
||||
if (Url.IsLocalUrl(returnUrl))
|
||||
{
|
||||
return Redirect(returnUrl);
|
||||
}
|
||||
return RedirectToAction("Index", "Home");
|
||||
}
|
||||
|
||||
internal class ChallengeResult : HttpUnauthorizedResult
|
||||
{
|
||||
public ChallengeResult(string provider, string redirectUri)
|
||||
: this(provider, redirectUri, null)
|
||||
{
|
||||
}
|
||||
|
||||
public ChallengeResult(string provider, string redirectUri, string userId)
|
||||
{
|
||||
LoginProvider = provider;
|
||||
RedirectUri = redirectUri;
|
||||
UserId = userId;
|
||||
}
|
||||
|
||||
public string LoginProvider { get; set; }
|
||||
public string RedirectUri { get; set; }
|
||||
public string UserId { get; set; }
|
||||
|
||||
public override void ExecuteResult(ControllerContext context)
|
||||
{
|
||||
var properties = new AuthenticationProperties { RedirectUri = RedirectUri };
|
||||
if (UserId != null)
|
||||
{
|
||||
properties.Dictionary[XsrfKey] = UserId;
|
||||
}
|
||||
context.HttpContext.GetOwinContext().Authentication.Challenge(properties, LoginProvider);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
111
Ch07/07_02_End/Website/Features/Account/AccountViewModels.cs
Normal file
111
Ch07/07_02_End/Website/Features/Account/AccountViewModels.cs
Normal file
@ -0,0 +1,111 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace HPlusSports.Models
|
||||
{
|
||||
public class ExternalLoginConfirmationViewModel
|
||||
{
|
||||
[Required]
|
||||
[Display(Name = "Email")]
|
||||
public string Email { get; set; }
|
||||
}
|
||||
|
||||
public class ExternalLoginListViewModel
|
||||
{
|
||||
public string ReturnUrl { get; set; }
|
||||
}
|
||||
|
||||
public class SendCodeViewModel
|
||||
{
|
||||
public string SelectedProvider { get; set; }
|
||||
public ICollection<System.Web.Mvc.SelectListItem> Providers { get; set; }
|
||||
public string ReturnUrl { get; set; }
|
||||
public bool RememberMe { get; set; }
|
||||
}
|
||||
|
||||
public class VerifyCodeViewModel
|
||||
{
|
||||
[Required]
|
||||
public string Provider { get; set; }
|
||||
|
||||
[Required]
|
||||
[Display(Name = "Code")]
|
||||
public string Code { get; set; }
|
||||
public string ReturnUrl { get; set; }
|
||||
|
||||
[Display(Name = "Remember this browser?")]
|
||||
public bool RememberBrowser { get; set; }
|
||||
|
||||
public bool RememberMe { get; set; }
|
||||
}
|
||||
|
||||
public class ForgotViewModel
|
||||
{
|
||||
[Required]
|
||||
[Display(Name = "Email")]
|
||||
public string Email { get; set; }
|
||||
}
|
||||
|
||||
public class LoginViewModel
|
||||
{
|
||||
[Required]
|
||||
[Display(Name = "Username")]
|
||||
public string Email { get; set; }
|
||||
|
||||
[Required]
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "Password")]
|
||||
public string Password { get; set; }
|
||||
|
||||
[Display(Name = "Remember me?")]
|
||||
public bool RememberMe { get; set; }
|
||||
}
|
||||
|
||||
public class RegisterViewModel
|
||||
{
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
[Display(Name = "Email")]
|
||||
public string Email { get; set; }
|
||||
|
||||
[Required]
|
||||
[StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "Password")]
|
||||
public string Password { get; set; }
|
||||
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "Confirm password")]
|
||||
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
|
||||
public string ConfirmPassword { get; set; }
|
||||
}
|
||||
|
||||
public class ResetPasswordViewModel
|
||||
{
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
[Display(Name = "Email")]
|
||||
public string Email { get; set; }
|
||||
|
||||
[Required]
|
||||
[StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "Password")]
|
||||
public string Password { get; set; }
|
||||
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "Confirm password")]
|
||||
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
|
||||
public string ConfirmPassword { get; set; }
|
||||
|
||||
public string Code { get; set; }
|
||||
}
|
||||
|
||||
public class ForgotPasswordViewModel
|
||||
{
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
[Display(Name = "Email")]
|
||||
public string Email { get; set; }
|
||||
}
|
||||
}
|
||||
10
Ch07/07_02_End/Website/Features/Account/ConfirmEmail.cshtml
Normal file
10
Ch07/07_02_End/Website/Features/Account/ConfirmEmail.cshtml
Normal file
@ -0,0 +1,10 @@
|
||||
@{
|
||||
ViewBag.Title = "Confirm Email";
|
||||
}
|
||||
|
||||
<h2>@ViewBag.Title.</h2>
|
||||
<div>
|
||||
<p>
|
||||
Thank you for confirming your email. Please @Html.ActionLink("Click here to Log in", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" })
|
||||
</p>
|
||||
</div>
|
||||
@ -0,0 +1,36 @@
|
||||
@model HPlusSports.Models.ExternalLoginConfirmationViewModel
|
||||
@{
|
||||
ViewBag.Title = "Register";
|
||||
}
|
||||
<h2>@ViewBag.Title.</h2>
|
||||
<h3>Associate your @ViewBag.LoginProvider account.</h3>
|
||||
|
||||
@using (Html.BeginForm("ExternalLoginConfirmation", "Account", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
|
||||
{
|
||||
@Html.AntiForgeryToken()
|
||||
|
||||
<h4>Association Form</h4>
|
||||
<hr />
|
||||
@Html.ValidationSummary(true, "", new { @class = "text-danger" })
|
||||
<p class="text-info">
|
||||
You've successfully authenticated with <strong>@ViewBag.LoginProvider</strong>.
|
||||
Please enter a user name for this site below and click the Register button to finish
|
||||
logging in.
|
||||
</p>
|
||||
<div class="form-group">
|
||||
@Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })
|
||||
<div class="col-md-10">
|
||||
@Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
|
||||
@Html.ValidationMessageFor(m => m.Email, "", new { @class = "text-danger" })
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-md-offset-2 col-md-10">
|
||||
<input type="submit" class="btn btn-default" value="Register" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
@Scripts.Render("~/bundles/jqueryval")
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
@{
|
||||
ViewBag.Title = "Login Failure";
|
||||
}
|
||||
|
||||
<hgroup>
|
||||
<h2>@ViewBag.Title.</h2>
|
||||
<h3 class="text-danger">Unsuccessful login with service.</h3>
|
||||
</hgroup>
|
||||
@ -0,0 +1,29 @@
|
||||
@model HPlusSports.Models.ForgotPasswordViewModel
|
||||
@{
|
||||
ViewBag.Title = "Forgot your password?";
|
||||
}
|
||||
|
||||
<h2>@ViewBag.Title.</h2>
|
||||
|
||||
@using (Html.BeginForm("ForgotPassword", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
|
||||
{
|
||||
@Html.AntiForgeryToken()
|
||||
<h4>Enter your email.</h4>
|
||||
<hr />
|
||||
@Html.ValidationSummary("", new { @class = "text-danger" })
|
||||
<div class="form-group">
|
||||
@Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })
|
||||
<div class="col-md-10">
|
||||
@Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-md-offset-2 col-md-10">
|
||||
<input type="submit" class="btn btn-default" value="Email Link" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
@Scripts.Render("~/bundles/jqueryval")
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
@{
|
||||
ViewBag.Title = "Forgot Password Confirmation";
|
||||
}
|
||||
|
||||
<hgroup class="title">
|
||||
<h1>@ViewBag.Title.</h1>
|
||||
</hgroup>
|
||||
<div>
|
||||
<p>
|
||||
Please check your email to reset your password.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
47
Ch07/07_02_End/Website/Features/Account/IdentityModels.cs
Normal file
47
Ch07/07_02_End/Website/Features/Account/IdentityModels.cs
Normal file
@ -0,0 +1,47 @@
|
||||
using System.Data.Entity;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Identity;
|
||||
using Microsoft.AspNet.Identity.EntityFramework;
|
||||
|
||||
namespace HPlusSports.Models
|
||||
{
|
||||
public static class UserRoles
|
||||
{
|
||||
public const string Admin = "Admin";
|
||||
public const string ProductAdmin = "ProductAdmin";
|
||||
}
|
||||
|
||||
// You can add profile data for the user by adding more properties to your ApplicationUser class, please visit https://go.microsoft.com/fwlink/?LinkID=317594 to learn more.
|
||||
public class ApplicationUser : IdentityUser
|
||||
{
|
||||
public ApplicationUser() : base()
|
||||
{
|
||||
}
|
||||
|
||||
public ApplicationUser(string username) : base(username)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
|
||||
{
|
||||
// Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
|
||||
var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
|
||||
// Add custom user claims here
|
||||
return userIdentity;
|
||||
}
|
||||
}
|
||||
|
||||
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
|
||||
{
|
||||
public ApplicationDbContext()
|
||||
: base("HPlusSports_Identity", throwIfV1Schema: false)
|
||||
{
|
||||
}
|
||||
|
||||
public static ApplicationDbContext Create()
|
||||
{
|
||||
return new ApplicationDbContext();
|
||||
}
|
||||
}
|
||||
}
|
||||
53
Ch07/07_02_End/Website/Features/Account/Login.cshtml
Normal file
53
Ch07/07_02_End/Website/Features/Account/Login.cshtml
Normal file
@ -0,0 +1,53 @@
|
||||
@using HPlusSports.Models
|
||||
@model LoginViewModel
|
||||
@{
|
||||
ViewBag.Title = "";
|
||||
}
|
||||
|
||||
<h2>User Login</h2>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<section id="loginForm">
|
||||
@using (Html.BeginForm("Login", "Account", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
|
||||
{
|
||||
@Html.AntiForgeryToken()
|
||||
<hr />
|
||||
@Html.ValidationSummary(true, "", new { @class = "text-danger" })
|
||||
<div class="form-group">
|
||||
@Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })
|
||||
<div class="col-md-10">
|
||||
@Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
|
||||
@Html.ValidationMessageFor(m => m.Email, "", new { @class = "text-danger" })
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" })
|
||||
<div class="col-md-10">
|
||||
@Html.PasswordFor(m => m.Password, new { @class = "form-control" })
|
||||
@Html.ValidationMessageFor(m => m.Password, "", new { @class = "text-danger" })
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-md-offset-2 col-md-10">
|
||||
<div class="checkbox">
|
||||
@Html.CheckBoxFor(m => m.RememberMe)
|
||||
@Html.LabelFor(m => m.RememberMe)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-md-offset-2 col-md-10">
|
||||
<input type="submit" value="Log in" class="btn btn-default" />
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
@Html.ActionLink("Register as a new user", "Register")
|
||||
</p>
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@Scripts.Render("~/bundles/jqueryval")
|
||||
}
|
||||
86
Ch07/07_02_End/Website/Features/Account/ManageViewModels.cs
Normal file
86
Ch07/07_02_End/Website/Features/Account/ManageViewModels.cs
Normal file
@ -0,0 +1,86 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.AspNet.Identity;
|
||||
using Microsoft.Owin.Security;
|
||||
|
||||
namespace HPlusSports.Models
|
||||
{
|
||||
public class IndexViewModel
|
||||
{
|
||||
public bool HasPassword { get; set; }
|
||||
public IList<UserLoginInfo> Logins { get; set; }
|
||||
public string PhoneNumber { get; set; }
|
||||
public bool TwoFactor { get; set; }
|
||||
public bool BrowserRemembered { get; set; }
|
||||
}
|
||||
|
||||
public class ManageLoginsViewModel
|
||||
{
|
||||
public IList<UserLoginInfo> CurrentLogins { get; set; }
|
||||
public IList<AuthenticationDescription> OtherLogins { get; set; }
|
||||
}
|
||||
|
||||
public class FactorViewModel
|
||||
{
|
||||
public string Purpose { get; set; }
|
||||
}
|
||||
|
||||
public class SetPasswordViewModel
|
||||
{
|
||||
[Required]
|
||||
[StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "New password")]
|
||||
public string NewPassword { get; set; }
|
||||
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "Confirm new password")]
|
||||
[Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
|
||||
public string ConfirmPassword { get; set; }
|
||||
}
|
||||
|
||||
public class ChangePasswordViewModel
|
||||
{
|
||||
[Required]
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "Current password")]
|
||||
public string OldPassword { get; set; }
|
||||
|
||||
[Required]
|
||||
[StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "New password")]
|
||||
public string NewPassword { get; set; }
|
||||
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "Confirm new password")]
|
||||
[Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
|
||||
public string ConfirmPassword { get; set; }
|
||||
}
|
||||
|
||||
public class AddPhoneNumberViewModel
|
||||
{
|
||||
[Required]
|
||||
[Phone]
|
||||
[Display(Name = "Phone Number")]
|
||||
public string Number { get; set; }
|
||||
}
|
||||
|
||||
public class VerifyPhoneNumberViewModel
|
||||
{
|
||||
[Required]
|
||||
[Display(Name = "Code")]
|
||||
public string Code { get; set; }
|
||||
|
||||
[Required]
|
||||
[Phone]
|
||||
[Display(Name = "Phone Number")]
|
||||
public string PhoneNumber { get; set; }
|
||||
}
|
||||
|
||||
public class ConfigureTwoFactorViewModel
|
||||
{
|
||||
public string SelectedProvider { get; set; }
|
||||
public ICollection<System.Web.Mvc.SelectListItem> Providers { get; set; }
|
||||
}
|
||||
}
|
||||
41
Ch07/07_02_End/Website/Features/Account/Register.cshtml
Normal file
41
Ch07/07_02_End/Website/Features/Account/Register.cshtml
Normal file
@ -0,0 +1,41 @@
|
||||
@model HPlusSports.Models.RegisterViewModel
|
||||
@{
|
||||
ViewBag.Title = "Register";
|
||||
}
|
||||
|
||||
<h2>@ViewBag.Title.</h2>
|
||||
|
||||
@using (Html.BeginForm("Register", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
|
||||
{
|
||||
@Html.AntiForgeryToken()
|
||||
<h4>Create a new account.</h4>
|
||||
<hr />
|
||||
@Html.ValidationSummary("", new { @class = "text-danger" })
|
||||
<div class="form-group">
|
||||
@Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })
|
||||
<div class="col-md-10">
|
||||
@Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" })
|
||||
<div class="col-md-10">
|
||||
@Html.PasswordFor(m => m.Password, new { @class = "form-control" })
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" })
|
||||
<div class="col-md-10">
|
||||
@Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" })
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-md-offset-2 col-md-10">
|
||||
<input type="submit" class="btn btn-default" value="Register" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
@Scripts.Render("~/bundles/jqueryval")
|
||||
}
|
||||
42
Ch07/07_02_End/Website/Features/Account/ResetPassword.cshtml
Normal file
42
Ch07/07_02_End/Website/Features/Account/ResetPassword.cshtml
Normal file
@ -0,0 +1,42 @@
|
||||
@model HPlusSports.Models.ResetPasswordViewModel
|
||||
@{
|
||||
ViewBag.Title = "Reset password";
|
||||
}
|
||||
|
||||
<h2>@ViewBag.Title.</h2>
|
||||
|
||||
@using (Html.BeginForm("ResetPassword", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
|
||||
{
|
||||
@Html.AntiForgeryToken()
|
||||
<h4>Reset your password.</h4>
|
||||
<hr />
|
||||
@Html.ValidationSummary("", new { @class = "text-danger" })
|
||||
@Html.HiddenFor(model => model.Code)
|
||||
<div class="form-group">
|
||||
@Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })
|
||||
<div class="col-md-10">
|
||||
@Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" })
|
||||
<div class="col-md-10">
|
||||
@Html.PasswordFor(m => m.Password, new { @class = "form-control" })
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" })
|
||||
<div class="col-md-10">
|
||||
@Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" })
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-md-offset-2 col-md-10">
|
||||
<input type="submit" class="btn btn-default" value="Reset" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
@Scripts.Render("~/bundles/jqueryval")
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
@{
|
||||
ViewBag.Title = "Reset password confirmation";
|
||||
}
|
||||
|
||||
<hgroup class="title">
|
||||
<h1>@ViewBag.Title.</h1>
|
||||
</hgroup>
|
||||
<div>
|
||||
<p>
|
||||
Your password has been reset. Please @Html.ActionLink("click here to log in", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" })
|
||||
</p>
|
||||
</div>
|
||||
24
Ch07/07_02_End/Website/Features/Account/SendCode.cshtml
Normal file
24
Ch07/07_02_End/Website/Features/Account/SendCode.cshtml
Normal file
@ -0,0 +1,24 @@
|
||||
@model HPlusSports.Models.SendCodeViewModel
|
||||
@{
|
||||
ViewBag.Title = "Send";
|
||||
}
|
||||
|
||||
<h2>@ViewBag.Title.</h2>
|
||||
|
||||
@using (Html.BeginForm("SendCode", "Account", new { ReturnUrl = Model.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" })) {
|
||||
@Html.AntiForgeryToken()
|
||||
@Html.Hidden("rememberMe", @Model.RememberMe)
|
||||
<h4>Send verification code</h4>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
Select Two-Factor Authentication Provider:
|
||||
@Html.DropDownListFor(model => model.SelectedProvider, Model.Providers)
|
||||
<input type="submit" value="Submit" class="btn btn-default" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
@Scripts.Render("~/bundles/jqueryval")
|
||||
}
|
||||
38
Ch07/07_02_End/Website/Features/Account/VerifyCode.cshtml
Normal file
38
Ch07/07_02_End/Website/Features/Account/VerifyCode.cshtml
Normal file
@ -0,0 +1,38 @@
|
||||
@model HPlusSports.Models.VerifyCodeViewModel
|
||||
@{
|
||||
ViewBag.Title = "Verify";
|
||||
}
|
||||
|
||||
<h2>@ViewBag.Title.</h2>
|
||||
|
||||
@using (Html.BeginForm("VerifyCode", "Account", new { ReturnUrl = Model.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" })) {
|
||||
@Html.AntiForgeryToken()
|
||||
@Html.Hidden("provider", @Model.Provider)
|
||||
@Html.Hidden("rememberMe", @Model.RememberMe)
|
||||
<h4>Enter verification code</h4>
|
||||
<hr />
|
||||
@Html.ValidationSummary("", new { @class = "text-danger" })
|
||||
<div class="form-group">
|
||||
@Html.LabelFor(m => m.Code, new { @class = "col-md-2 control-label" })
|
||||
<div class="col-md-10">
|
||||
@Html.TextBoxFor(m => m.Code, new { @class = "form-control" })
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-md-offset-2 col-md-10">
|
||||
<div class="checkbox">
|
||||
@Html.CheckBoxFor(m => m.RememberBrowser)
|
||||
@Html.LabelFor(m => m.RememberBrowser)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-md-offset-2 col-md-10">
|
||||
<input type="submit" class="btn btn-default" value="Submit" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
@Scripts.Render("~/bundles/jqueryval")
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
@model HPlusSports.Models.ExternalLoginListViewModel
|
||||
@using Microsoft.Owin.Security
|
||||
|
||||
<h4>Use another service to log in.</h4>
|
||||
<hr />
|
||||
@{
|
||||
var loginProviders = Context.GetOwinContext().Authentication.GetExternalAuthenticationTypes();
|
||||
if (loginProviders.Count() == 0) {
|
||||
<div>
|
||||
<p>
|
||||
There are no external authentication services configured. See <a href="https://go.microsoft.com/fwlink/?LinkId=403804">this article</a>
|
||||
for details on setting up this ASP.NET application to support logging in via external services.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
else {
|
||||
using (Html.BeginForm("ExternalLogin", "Account", new { ReturnUrl = Model.ReturnUrl })) {
|
||||
@Html.AntiForgeryToken()
|
||||
<div id="socialLoginList">
|
||||
<p>
|
||||
@foreach (AuthenticationDescription p in loginProviders) {
|
||||
<button type="submit" class="btn btn-default" id="@p.AuthenticationType" name="provider" value="@p.AuthenticationType" title="Log in using your @p.Caption account">@p.AuthenticationType</button>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
108
Ch07/07_02_End/Website/Features/Cart/Cart.cshtml
Normal file
108
Ch07/07_02_End/Website/Features/Cart/Cart.cshtml
Normal file
@ -0,0 +1,108 @@
|
||||
@model HPlusSports.Models.ShoppingCart
|
||||
|
||||
@{
|
||||
ViewBag.Title = "My Cart";
|
||||
}
|
||||
|
||||
@if (TempData.SuccessMessage() != null)
|
||||
{
|
||||
<div class="alert alert-success alert-dismissible" role="alert">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
@TempData.SuccessMessage()
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="cart row">
|
||||
|
||||
<div>
|
||||
<hr />
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-md-1">
|
||||
</th>
|
||||
<th class="col-md-6">
|
||||
Product
|
||||
</th>
|
||||
<th class="col-md-1">
|
||||
Quantity
|
||||
</th>
|
||||
<th class="col-md-2">
|
||||
Price
|
||||
</th>
|
||||
<th class="col-md-2">
|
||||
Subtotal
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in Model.Items)
|
||||
{
|
||||
<tr class="text-middle">
|
||||
<td>
|
||||
<a href="@Url.Action("Remove", "Cart", new { id = item.Id })">
|
||||
<i class="glyphicon glyphicon-remove text-danger"></i>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="@Url.Product(item.SKU)">
|
||||
<img class="product-image img-thumbnail" src="@Url.Action("Product", "Images", new { id = item.SKU })" />
|
||||
<span>@item.Name</span>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="@Url.Action("Add", "Cart", new { item.SKU, quantity = -1 })">
|
||||
<i class="glyphicon glyphicon-minus"></i>
|
||||
</a>
|
||||
@item.Quantity
|
||||
<a href="@Url.Action("Add", "Cart", new { item.SKU })">
|
||||
<i class="glyphicon glyphicon-plus"></i>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
@item.Price.ToString("C")
|
||||
</td>
|
||||
<td>
|
||||
@item.Total.ToString("C")
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="text-primary">
|
||||
<td colspan="4" class="text-right">
|
||||
Subtotal
|
||||
</td>
|
||||
<td>
|
||||
@Model.Subtotal.ToString("C")
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="text-muted">
|
||||
<td colspan="4" class="text-right">
|
||||
Tax
|
||||
</td>
|
||||
<td>
|
||||
@Model.Tax.ToString("C")
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="text-muted">
|
||||
<td colspan="4" class="text-right">
|
||||
Shipping
|
||||
</td>
|
||||
<td>
|
||||
@Model.Shipping.ToString("C")
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="text-primary text-large">
|
||||
<th colspan="4" class="text-right">
|
||||
Total
|
||||
</th>
|
||||
<th>
|
||||
@Model.Total.ToString("C")
|
||||
</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
132
Ch07/07_02_End/Website/Features/Cart/CartController.cs
Normal file
132
Ch07/07_02_End/Website/Features/Cart/CartController.cs
Normal file
@ -0,0 +1,132 @@
|
||||
using HPlusSports.Models;
|
||||
using System;
|
||||
using System.Data.Entity.Validation;
|
||||
using System.Linq;
|
||||
using System.Web.Mvc;
|
||||
|
||||
namespace HPlusSports.Controllers
|
||||
{
|
||||
public class CartController : Controller
|
||||
{
|
||||
private readonly HPlusSportsDbContext _context;
|
||||
|
||||
public CartController()
|
||||
: this(new HPlusSportsDbContext())
|
||||
{
|
||||
}
|
||||
|
||||
public CartController(HPlusSportsDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public ActionResult Index()
|
||||
{
|
||||
var cart = GetCart();
|
||||
|
||||
if (cart.Items.Any())
|
||||
return View("Cart", cart);
|
||||
|
||||
return View("EmptyCart");
|
||||
}
|
||||
|
||||
public ActionResult Add(string sku, int quantity = 1)
|
||||
{
|
||||
var product = _context.Products.FirstOrDefault(x => x.SKU == sku);
|
||||
|
||||
var cart = GetCart();
|
||||
|
||||
var item = cart.Items.FirstOrDefault(x => x.SKU == sku);
|
||||
|
||||
if (item == null)
|
||||
{
|
||||
item = new ShoppingCartItem
|
||||
{
|
||||
SKU = product?.SKU,
|
||||
Name = product?.Name,
|
||||
MSRP = (product?.MSRP).GetValueOrDefault(),
|
||||
Price = (product?.Price).GetValueOrDefault(),
|
||||
Quantity = quantity,
|
||||
};
|
||||
|
||||
cart.Items.Add(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
item.Quantity += quantity;
|
||||
}
|
||||
|
||||
if (item.Quantity <= 0)
|
||||
{
|
||||
cart.Items.Remove(item);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_context.SaveChanges();
|
||||
}
|
||||
catch (DbEntityValidationException ex)
|
||||
{
|
||||
var errors = ex.EntityValidationErrors.SelectMany(x => x.ValidationErrors);
|
||||
|
||||
foreach (var error in errors)
|
||||
{
|
||||
ModelState.AddModelError(error.PropertyName, error.ErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
TempData.SuccessMessage($"Successfully added {item.Name} to the cart");
|
||||
|
||||
return RedirectToAction("Index", "Cart");
|
||||
}
|
||||
|
||||
public ActionResult Remove(long id)
|
||||
{
|
||||
var cart = GetCart();
|
||||
|
||||
var item = cart.Items.FirstOrDefault(x => x.Id == id);
|
||||
|
||||
if (item == null)
|
||||
{
|
||||
ModelState.AddModelError("", "Missing or invalid cart item");
|
||||
return View();
|
||||
}
|
||||
|
||||
cart.Items.Remove(item);
|
||||
|
||||
_context.SaveChanges();
|
||||
|
||||
return RedirectToAction("Index", "Cart");
|
||||
}
|
||||
|
||||
private ShoppingCart GetCart()
|
||||
{
|
||||
var userId = GetUserId(this);
|
||||
|
||||
var cart =
|
||||
_context.ShoppingCarts
|
||||
.Include("Items")
|
||||
.FirstOrDefault(x => x.UserId == userId);
|
||||
|
||||
if (cart == null)
|
||||
{
|
||||
cart = new ShoppingCart { UserId = userId };
|
||||
_context.ShoppingCarts.Add(cart);
|
||||
_context.SaveChanges();
|
||||
}
|
||||
|
||||
cart.Recalculate();
|
||||
|
||||
return cart;
|
||||
}
|
||||
|
||||
// Overwriteable function for unit testing
|
||||
internal Func<Controller, string> GetUserId =
|
||||
(controller) => controller.User.Identity.Name;
|
||||
}
|
||||
}
|
||||
16
Ch07/07_02_End/Website/Features/Cart/EmptyCart.cshtml
Normal file
16
Ch07/07_02_End/Website/Features/Cart/EmptyCart.cshtml
Normal file
@ -0,0 +1,16 @@
|
||||
@model HPlusSports.Models.ShoppingCart
|
||||
|
||||
@{
|
||||
ViewBag.Title = "Cart";
|
||||
}
|
||||
|
||||
<div class="row">
|
||||
<div class="well well-lg">
|
||||
<p>Your shopping cart is empty!</p>
|
||||
<p>
|
||||
Head on over to
|
||||
@Html.ActionLink("the Product Catalog", "Index", "Products")
|
||||
to find something!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
6
Ch07/07_02_End/Website/Features/Home/About.cshtml
Normal file
6
Ch07/07_02_End/Website/Features/Home/About.cshtml
Normal file
@ -0,0 +1,6 @@
|
||||
@{
|
||||
ViewBag.Title = "About";
|
||||
}
|
||||
<h3>@ViewBag.Message</h3>
|
||||
|
||||
<p>Use this area to provide additional information.</p>
|
||||
16
Ch07/07_02_End/Website/Features/Home/Contact.cshtml
Normal file
16
Ch07/07_02_End/Website/Features/Home/Contact.cshtml
Normal file
@ -0,0 +1,16 @@
|
||||
@{
|
||||
ViewBag.Title = "Contact";
|
||||
}
|
||||
<h3>@ViewBag.Message</h3>
|
||||
|
||||
<address>
|
||||
One Microsoft Way<br />
|
||||
Redmond, WA 98052-6399<br />
|
||||
<abbr title="Phone">P:</abbr>
|
||||
425.555.0100
|
||||
</address>
|
||||
|
||||
<address>
|
||||
<strong>Support:</strong> <a href="mailto:Support@example.com">Support@example.com</a><br />
|
||||
<strong>Marketing:</strong> <a href="mailto:Marketing@example.com">Marketing@example.com</a>
|
||||
</address>
|
||||
57
Ch07/07_02_End/Website/Features/Home/HomeController.cs
Normal file
57
Ch07/07_02_End/Website/Features/Home/HomeController.cs
Normal file
@ -0,0 +1,57 @@
|
||||
using HPlusSports.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Web;
|
||||
using System.Web.Mvc;
|
||||
|
||||
namespace HPlusSports.Controllers
|
||||
{
|
||||
[RoutePrefix("home/{name}")]
|
||||
public class HomeController : Controller
|
||||
{
|
||||
private readonly HPlusSportsDbContext _context;
|
||||
|
||||
public HomeController()
|
||||
: this(new HPlusSportsDbContext())
|
||||
{
|
||||
}
|
||||
|
||||
public HomeController(HPlusSportsDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public ActionResult Index()
|
||||
{
|
||||
var categories =
|
||||
(
|
||||
from category in _context.Categories
|
||||
let count = _context.Products.Count(x => x.CategoryId == category.Id)
|
||||
select new { category, count }
|
||||
).ToDictionary(x => x.category, x => x.count);
|
||||
|
||||
return View(categories);
|
||||
}
|
||||
|
||||
[Route("about")]
|
||||
public ActionResult About(string name)
|
||||
{
|
||||
ViewBag.Message = "Your application description page, " + name;
|
||||
|
||||
return View();
|
||||
}
|
||||
|
||||
public ActionResult Broken(string error)
|
||||
{
|
||||
throw new Exception(error);
|
||||
}
|
||||
|
||||
public ActionResult Contact()
|
||||
{
|
||||
ViewBag.Message = "Your contact page.";
|
||||
|
||||
return View();
|
||||
}
|
||||
}
|
||||
}
|
||||
19
Ch07/07_02_End/Website/Features/Home/Index.cshtml
Normal file
19
Ch07/07_02_End/Website/Features/Home/Index.cshtml
Normal file
@ -0,0 +1,19 @@
|
||||
@model IDictionary<HPlusSports.Models.Category, int>
|
||||
|
||||
@{
|
||||
ViewBag.Title = "Categories";
|
||||
}
|
||||
|
||||
<div class="row">
|
||||
|
||||
@foreach (var category in Model.Keys)
|
||||
{
|
||||
<div class="category col-md-4 text-center">
|
||||
<a href="@Url.Action("Category", "Products", new { id = category.Key })">
|
||||
<img class="img-thumbnail" src="@Url.Action("Category", "Images", new { id = category.Key })" />
|
||||
<h3>@category.Name</h3>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
82
Ch07/07_02_End/Website/Features/Images/ImagesController.cs
Normal file
82
Ch07/07_02_End/Website/Features/Images/ImagesController.cs
Normal file
@ -0,0 +1,82 @@
|
||||
using System.Linq;
|
||||
using System.Web.Mvc;
|
||||
using HPlusSports.Models;
|
||||
|
||||
namespace HPlusSports.Controllers
|
||||
{
|
||||
public class ImagesController : Controller
|
||||
{
|
||||
private readonly HPlusSportsDbContext _context;
|
||||
|
||||
public ImagesController()
|
||||
: this(new HPlusSportsDbContext())
|
||||
{
|
||||
}
|
||||
|
||||
public ImagesController(HPlusSportsDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
// /images/category/{SKU}
|
||||
// Redirects to the main image for a category
|
||||
public ActionResult Category(string id)
|
||||
{
|
||||
var imageId =
|
||||
_context.Categories
|
||||
.Where(x => x.Key == id)
|
||||
.Select(x => x.ImageId)
|
||||
.FirstOrDefault();
|
||||
|
||||
return Image(imageId);
|
||||
}
|
||||
|
||||
// /images/product/{SKU}
|
||||
// Redirects to the main image for a product
|
||||
public ActionResult Product(string id)
|
||||
{
|
||||
var imageId =
|
||||
_context.Products
|
||||
.Where(x => x.SKU == id)
|
||||
.SelectMany(x => x.Images.Select(img => img.Id))
|
||||
.FirstOrDefault();
|
||||
|
||||
return Image(imageId);
|
||||
}
|
||||
|
||||
public ActionResult Image(long? id)
|
||||
{
|
||||
var image = _context.Images.FirstOrDefault(x => x.Id == id);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(image.Url))
|
||||
{
|
||||
return RedirectPermanent(image.Url);
|
||||
}
|
||||
|
||||
if (image?.Content == null)
|
||||
{
|
||||
image = GetPlaceholderImage();
|
||||
}
|
||||
|
||||
return File(image.Content, image.ContentType);
|
||||
}
|
||||
|
||||
|
||||
internal static volatile byte[] PlaceholderImageContent;
|
||||
|
||||
private Image GetPlaceholderImage()
|
||||
{
|
||||
if (PlaceholderImageContent == null)
|
||||
{
|
||||
var path = Server.MapPath("~/Content/placeholder.jpg");
|
||||
PlaceholderImageContent = System.IO.File.ReadAllBytes(path);
|
||||
}
|
||||
|
||||
return new Image
|
||||
{
|
||||
Content = PlaceholderImageContent,
|
||||
ContentType = "image/jpeg"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
85
Ch07/07_02_End/Website/Features/Inventory/Create.cshtml
Normal file
85
Ch07/07_02_End/Website/Features/Inventory/Create.cshtml
Normal file
@ -0,0 +1,85 @@
|
||||
@model HPlusSports.Models.CreateProductRequest
|
||||
|
||||
@{
|
||||
ViewBag.Title = "Add New Product";
|
||||
}
|
||||
|
||||
@using (Html.BeginForm())
|
||||
{
|
||||
@Html.AntiForgeryToken()
|
||||
|
||||
<div class="form-horizontal">
|
||||
<h4>Product</h4>
|
||||
<hr />
|
||||
@Html.ValidationSummary(true, "", new { @class = "text-danger" })
|
||||
<div class="form-group">
|
||||
@Html.LabelFor(model => model.CategoryId, "CategoryId", htmlAttributes: new { @class = "control-label col-md-2" })
|
||||
<div class="col-md-10">
|
||||
@Html.DropDownListFor(model => model.CategoryId, null, htmlAttributes: new { @class = "form-control" })
|
||||
@Html.ValidationMessageFor(model => model.CategoryId, "", new { @class = "text-danger" })
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@Html.LabelFor(model => model.SKU, htmlAttributes: new { @class = "control-label col-md-2" })
|
||||
<div class="col-md-10">
|
||||
@Html.EditorFor(model => model.SKU, new { htmlAttributes = new { @class = "form-control" } })
|
||||
@Html.ValidationMessageFor(model => model.SKU, "", new { @class = "text-danger" })
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })
|
||||
<div class="col-md-10">
|
||||
@Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })
|
||||
@Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@Html.LabelFor(model => model.Summary, htmlAttributes: new { @class = "control-label col-md-2" })
|
||||
<div class="col-md-10">
|
||||
@Html.EditorFor(model => model.Summary, new { htmlAttributes = new { @class = "form-control" } })
|
||||
@Html.ValidationMessageFor(model => model.Summary, "", new { @class = "text-danger" })
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@Html.LabelFor(model => model.Description, htmlAttributes: new { @class = "control-label col-md-2" })
|
||||
<div class="col-md-10">
|
||||
@Html.EditorFor(model => model.Description, new { htmlAttributes = new { @class = "form-control" } })
|
||||
@Html.ValidationMessageFor(model => model.Description, "", new { @class = "text-danger" })
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@Html.LabelFor(model => model.MSRP, htmlAttributes: new { @class = "control-label col-md-2" })
|
||||
<div class="col-md-10">
|
||||
@Html.EditorFor(model => model.MSRP, new { htmlAttributes = new { @class = "form-control" } })
|
||||
@Html.ValidationMessageFor(model => model.MSRP, "", new { @class = "text-danger" })
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@Html.LabelFor(model => model.Price, htmlAttributes: new { @class = "control-label col-md-2" })
|
||||
<div class="col-md-10">
|
||||
@Html.EditorFor(model => model.Price, new { htmlAttributes = new { @class = "form-control" } })
|
||||
@Html.ValidationMessageFor(model => model.Price, "", new { @class = "text-danger" })
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-md-offset-2 col-md-10">
|
||||
<input type="submit" value="Create" class="btn btn-default" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div>
|
||||
@Html.ActionLink("Back to List", "Index")
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@Scripts.Render("~/bundles/jqueryval")
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace HPlusSports.Models
|
||||
{
|
||||
public class CreateProductRequest
|
||||
{
|
||||
[Required]
|
||||
public long CategoryId { get; set; }
|
||||
|
||||
[Required]
|
||||
public string SKU { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Name { get; set; }
|
||||
|
||||
[Required]
|
||||
[DataType(DataType.Text)]
|
||||
public string Summary { get; set; }
|
||||
|
||||
[Required]
|
||||
[DataType(DataType.MultilineText)]
|
||||
public string Description { get; set; }
|
||||
|
||||
[Range(minimum: 0, maximum: double.MaxValue)]
|
||||
[DataType(DataType.Currency)]
|
||||
public double MSRP { get; set; }
|
||||
|
||||
[Range(minimum: 0, maximum: double.MaxValue)]
|
||||
[DataType(DataType.Currency)]
|
||||
public double Price { get; set; }
|
||||
}
|
||||
}
|
||||
93
Ch07/07_02_End/Website/Features/Inventory/Index.cshtml
Normal file
93
Ch07/07_02_End/Website/Features/Inventory/Index.cshtml
Normal file
@ -0,0 +1,93 @@
|
||||
@model IEnumerable<HPlusSports.Models.Product>
|
||||
|
||||
@{
|
||||
ViewBag.Title = "Inventory";
|
||||
}
|
||||
|
||||
@if (TempData.HasSuccessMessage())
|
||||
{
|
||||
<div class="alert alert-error alert-dismissible" role="alert">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
@TempData.SuccessMessage()
|
||||
</div>
|
||||
}
|
||||
else if (TempData.HasErrorMessage())
|
||||
{
|
||||
<div class="alert alert-error alert-dismissible" role="alert">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
@TempData.ErrorMessage()
|
||||
</div>
|
||||
}
|
||||
|
||||
<h2>
|
||||
@ViewBag.Title
|
||||
<a class="btn btn-primary btn-sm pull-right" href="@Url.Action("Create")">Add Product</a>
|
||||
</h2>
|
||||
|
||||
<table class="table tablesorter">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Category
|
||||
</th>
|
||||
<th>
|
||||
@Html.DisplayNameFor(model => model.SKU)
|
||||
</th>
|
||||
<th>
|
||||
@Html.DisplayNameFor(model => model.Name)
|
||||
</th>
|
||||
<th>
|
||||
@Html.DisplayNameFor(model => model.Price)
|
||||
</th>
|
||||
<th>
|
||||
@Html.DisplayNameFor(model => model.Description)
|
||||
</th>
|
||||
<th>
|
||||
Last Update
|
||||
</th>
|
||||
<th disabled></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in Model)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
@Html.DisplayFor(modelItem => item.Category.Name)
|
||||
</td>
|
||||
<td>
|
||||
@Html.DisplayFor(modelItem => item.SKU)
|
||||
</td>
|
||||
<td>
|
||||
@Html.DisplayFor(modelItem => item.Name)
|
||||
</td>
|
||||
<td>
|
||||
@Html.DisplayFor(modelItem => item.Price)
|
||||
<span class="clearfix text-muted">
|
||||
@Html.DisplayFor(modelItem => item.MSRP)
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
@Html.DisplayFor(modelItem => item.Description)
|
||||
</td>
|
||||
<td>
|
||||
<span class="text-muted">
|
||||
<span title="@item.LastUpdated.ToString()">
|
||||
@item.LastUpdated.ToShortDateString()
|
||||
</span>
|
||||
by
|
||||
@item.LastUpdatedUserId
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<a href="@Url.Action("Update", new { id = item.Id })">
|
||||
<i class="glyphicon glyphicon-pencil text-info"></i>
|
||||
</a>
|
||||
<a href="@Url.Action("Delete", new { id = item.Id })">
|
||||
<i class="glyphicon glyphicon-remove text-danger"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
142
Ch07/07_02_End/Website/Features/Inventory/InventoryController.cs
Normal file
142
Ch07/07_02_End/Website/Features/Inventory/InventoryController.cs
Normal file
@ -0,0 +1,142 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web.Mvc;
|
||||
using HPlusSports.Models;
|
||||
using HPlusSports.Requests;
|
||||
using HPlusSports.Services;
|
||||
using MediatR;
|
||||
|
||||
namespace HPlusSports.Controllers
|
||||
{
|
||||
[Authorize(Roles = UserRoles.Admin)]
|
||||
public class InventoryController : Controller
|
||||
{
|
||||
private HPlusSportsDbContext _context;
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
public InventoryController(
|
||||
HPlusSportsDbContext context,
|
||||
MediatR.IMediator mediator)
|
||||
{
|
||||
_context = context;
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
public ActionResult Index()
|
||||
{
|
||||
var products =
|
||||
_context.Products
|
||||
.OrderBy(x => x.CategoryId)
|
||||
.ThenBy(x => x.Name);
|
||||
|
||||
return View(products);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public ActionResult Create()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public ActionResult Create(CreateProductRequest request)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
var product = new Product
|
||||
{
|
||||
CategoryId = request.CategoryId,
|
||||
Description = request.Description,
|
||||
MSRP = request.MSRP,
|
||||
Name = request.Name,
|
||||
Price = request.Price,
|
||||
SKU = request.SKU,
|
||||
Summary = request.Summary,
|
||||
LastUpdated = DateTime.UtcNow,
|
||||
LastUpdatedUserId = GetUserId(this),
|
||||
};
|
||||
|
||||
_context.Products.Add(product);
|
||||
_context.SaveChanges();
|
||||
|
||||
TempData.SuccessMessage($"Successfully created \"{product.Name}\"");
|
||||
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public ActionResult Update(long id)
|
||||
{
|
||||
var existing = _context.Products.Find(id);
|
||||
|
||||
if (existing == null)
|
||||
{
|
||||
TempData.ErrorMessage($"Couldn't update product #\"{id}\": product not found!");
|
||||
}
|
||||
|
||||
return View(existing);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult> Update(UpdateProductRequest request)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
request.LastUpdatedUserId = GetUserId(this);
|
||||
|
||||
var response = await _mediator.Send(request);
|
||||
|
||||
if (!response.Success)
|
||||
{
|
||||
TempData.ErrorMessage(response.Message);
|
||||
return View();
|
||||
}
|
||||
|
||||
TempData.SuccessMessage(response.Message);
|
||||
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
public ActionResult Delete(long id)
|
||||
{
|
||||
var product = _context.Products.Find(id);
|
||||
|
||||
if (product != null)
|
||||
{
|
||||
_context.Products.Remove(product);
|
||||
_context.SaveChanges();
|
||||
|
||||
TempData.SuccessMessage($"Successfully deleted \"{product.Name}\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData.ErrorMessage($"Couldn't delete \"{product.Name}\": product not found!");
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
protected override void OnResultExecuting(ResultExecutingContext filterContext)
|
||||
{
|
||||
ViewData["CategoryId"] =
|
||||
_context.Categories
|
||||
.Select(x => new SelectListItem
|
||||
{
|
||||
Text = x.Name,
|
||||
Value = x.Id.ToString(),
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
// Overwriteable function for unit testing
|
||||
internal Func<Controller, string> GetUserId =
|
||||
(controller) => controller.User.Identity.Name;
|
||||
}
|
||||
}
|
||||
104
Ch07/07_02_End/Website/Features/Inventory/Update.cshtml
Normal file
104
Ch07/07_02_End/Website/Features/Inventory/Update.cshtml
Normal file
@ -0,0 +1,104 @@
|
||||
@model HPlusSports.Models.Product
|
||||
|
||||
@{
|
||||
ViewBag.Title = "Update Product";
|
||||
}
|
||||
|
||||
<h2>@ViewBag.Title</h2>
|
||||
|
||||
|
||||
@using (Html.BeginForm())
|
||||
{
|
||||
@Html.AntiForgeryToken()
|
||||
|
||||
<div class="form-horizontal">
|
||||
<h4>Product</h4>
|
||||
<hr />
|
||||
@Html.ValidationSummary(true, "", new { @class = "text-danger" })
|
||||
@Html.HiddenFor(model => model.Id)
|
||||
|
||||
<div class="form-group">
|
||||
@Html.LabelFor(model => model.CategoryId, "CategoryId", htmlAttributes: new { @class = "control-label col-md-2" })
|
||||
<div class="col-md-10">
|
||||
@Html.DropDownListFor(model => model.CategoryId, null, htmlAttributes: new { @class = "form-control" })
|
||||
@Html.ValidationMessageFor(model => model.CategoryId, "", new { @class = "text-danger" })
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@Html.LabelFor(model => model.SKU, htmlAttributes: new { @class = "control-label col-md-2" })
|
||||
<div class="col-md-10">
|
||||
@Html.EditorFor(model => model.SKU, new { htmlAttributes = new { @class = "form-control" } })
|
||||
@Html.ValidationMessageFor(model => model.SKU, "", new { @class = "text-danger" })
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })
|
||||
<div class="col-md-10">
|
||||
@Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })
|
||||
@Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@Html.LabelFor(model => model.Summary, htmlAttributes: new { @class = "control-label col-md-2" })
|
||||
<div class="col-md-10">
|
||||
@Html.EditorFor(model => model.Summary, new { htmlAttributes = new { @class = "form-control" } })
|
||||
@Html.ValidationMessageFor(model => model.Summary, "", new { @class = "text-danger" })
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@Html.LabelFor(model => model.Description, htmlAttributes: new { @class = "control-label col-md-2" })
|
||||
<div class="col-md-10">
|
||||
@Html.EditorFor(model => model.Description, new { htmlAttributes = new { @class = "form-control" } })
|
||||
@Html.ValidationMessageFor(model => model.Description, "", new { @class = "text-danger" })
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@Html.LabelFor(model => model.MSRP, htmlAttributes: new { @class = "control-label col-md-2" })
|
||||
<div class="col-md-10">
|
||||
@Html.EditorFor(model => model.MSRP, new { htmlAttributes = new { @class = "form-control" } })
|
||||
@Html.ValidationMessageFor(model => model.MSRP, "", new { @class = "text-danger" })
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@Html.LabelFor(model => model.Price, htmlAttributes: new { @class = "control-label col-md-2" })
|
||||
<div class="col-md-10">
|
||||
@Html.EditorFor(model => model.Price, new { htmlAttributes = new { @class = "form-control" } })
|
||||
@Html.ValidationMessageFor(model => model.Price, "", new { @class = "text-danger" })
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@Html.LabelFor(model => model.LastUpdated, htmlAttributes: new { @class = "control-label col-md-2" })
|
||||
<div class="col-md-10">
|
||||
@Html.DisplayFor(model => model.LastUpdated, new { htmlAttributes = new { @class = "form-control" } })
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@Html.LabelFor(model => model.LastUpdatedUserId, htmlAttributes: new { @class = "control-label col-md-2" })
|
||||
<div class="col-md-10">
|
||||
@Html.DisplayFor(model => model.LastUpdatedUserId, new { htmlAttributes = new { @class = "form-control" } })
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-md-offset-2 col-md-10">
|
||||
<input type="submit" value="Save" class="btn btn-default" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div>
|
||||
@Html.ActionLink("Back to List", "Index")
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@Scripts.Render("~/bundles/jqueryval")
|
||||
}
|
||||
61
Ch07/07_02_End/Website/Features/Products/MenuCart.cshtml
Normal file
61
Ch07/07_02_End/Website/Features/Products/MenuCart.cshtml
Normal file
@ -0,0 +1,61 @@
|
||||
@model ShoppingCart
|
||||
@{
|
||||
var cart = Model;
|
||||
}
|
||||
<a class="dropbtn nav-cart">
|
||||
<i class="glyphicon glyphicon-shopping-cart"></i>
|
||||
Cart
|
||||
@if (cart.Items.Any())
|
||||
{
|
||||
<span>
|
||||
(@cart.Items.Count() items)
|
||||
</span>
|
||||
}
|
||||
</a>
|
||||
<div class="dropdown-content">
|
||||
|
||||
@if (cart.Items.Count == 0)
|
||||
{
|
||||
<p>Your shopping cart is empty - keep shopping!</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table table-condensed">
|
||||
@foreach (var item in cart.Items)
|
||||
{
|
||||
<tr class="text-middle">
|
||||
<td>
|
||||
<a href="@Url.Action("Remove", "Cart", new { id = item.Id })">
|
||||
<i class="glyphicon glyphicon-remove text-danger"></i>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<img width="50" height="50" src="@Url.Action("Product", "Images", new { id = item.SKU })" alt="">
|
||||
</td>
|
||||
<td>
|
||||
<a href="@Url.Product(item.SKU)">
|
||||
@item.Name
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
@item.Quantity
|
||||
x
|
||||
@item.Price.ToString("C")
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
<tr>
|
||||
<th colspan="3" class="text-right">
|
||||
Subtotal:
|
||||
</th>
|
||||
<th class="text-right">
|
||||
@cart.Subtotal.ToString("C")
|
||||
</th>
|
||||
</tr>
|
||||
</table>
|
||||
}
|
||||
|
||||
<p class="buttons text-right">
|
||||
<a href="@Url.Action("Index", "Cart")" class="btn btn-default">View Cart</a>
|
||||
</p>
|
||||
</div>
|
||||
68
Ch07/07_02_End/Website/Features/Products/Product.cshtml
Normal file
68
Ch07/07_02_End/Website/Features/Products/Product.cshtml
Normal file
@ -0,0 +1,68 @@
|
||||
@model Product
|
||||
|
||||
@{
|
||||
ViewBag.Title = "";
|
||||
}
|
||||
|
||||
<div class="product row">
|
||||
|
||||
<div class="col-md-1">
|
||||
@if (Model.Images.Count > 1)
|
||||
{
|
||||
<ul class="list-group">
|
||||
@foreach (var image in Model.Images)
|
||||
{
|
||||
<li class="list-group-item">
|
||||
<img class="img img-thumbnail"
|
||||
src="@Url.Action("Image", "Images", new { id = image.Id })" />
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<img class="img img-thumbnail"
|
||||
src="@Url.Action("Product", "Images", new { id = Model.SKU })" />
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
|
||||
<h2>@Model.Name</h2>
|
||||
|
||||
<p>
|
||||
@{
|
||||
var rating = (HPlusSports.Models.ProductRating)ViewData["Rating"];
|
||||
}
|
||||
@Html.Rating(rating)
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<span class="price">@Html.DisplayFor(modelItem => modelItem.Price)</span>
|
||||
<span class="text-muted">@Html.DisplayFor(modelItem => modelItem.MSRP)</span>
|
||||
@{
|
||||
var discount = (Model.MSRP - Model.Price) / Model.MSRP;
|
||||
}
|
||||
@if (discount > .05)
|
||||
{
|
||||
<span class="discount label label-primary">
|
||||
@discount.ToString("P0") off!
|
||||
</span>
|
||||
}
|
||||
</p>
|
||||
|
||||
<p class="buy-button">
|
||||
<a class="btn btn-sm btn-warning" href="@Url.Action("Add", "Cart", new { Model.SKU })">
|
||||
Add To Cart
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<hr />
|
||||
|
||||
<p>
|
||||
@Html.DisplayFor(model => model.Summary)
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
103
Ch07/07_02_End/Website/Features/Products/ProductList.cshtml
Normal file
103
Ch07/07_02_End/Website/Features/Products/ProductList.cshtml
Normal file
@ -0,0 +1,103 @@
|
||||
@model ProductsListViewModel
|
||||
|
||||
@{
|
||||
var category = (Category)ViewData["Category"];
|
||||
ViewBag.Title = category.Name;
|
||||
}
|
||||
|
||||
<div class="paged-content">
|
||||
|
||||
<div class="row">
|
||||
|
||||
@foreach (var product in Model.Products)
|
||||
{
|
||||
<div class="product-summary col-md-3 text-center">
|
||||
<h4>
|
||||
<a href="@Url.Product(product.SKU)">
|
||||
<img class="product-image img-thumbnail" src="@Url.Action("Product", "Images", new { id = product.SKU })" />
|
||||
<span class="name">@product.Name</span>
|
||||
</a>
|
||||
</h4>
|
||||
<p>
|
||||
<span class="price">@Html.DisplayFor(x => product.Price)</span>
|
||||
<span class="text-muted">@Html.DisplayFor(x => product.MSRP)</span>
|
||||
@if (product.HasAdvertisableDiscount)
|
||||
{
|
||||
<span class="discount label label-primary">
|
||||
@Html.DisplayFor(x => product.Discount) off!
|
||||
</span>
|
||||
}
|
||||
</p>
|
||||
<p>
|
||||
@Html.Rating(product.Rating)
|
||||
</p>
|
||||
<p>
|
||||
<a class="btn btn-sm btn-warning" href="@Url.Action("Add", "Cart", new { product.SKU })">
|
||||
Add To Cart
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="pager row">
|
||||
|
||||
<div class="pull-left">
|
||||
<nav>
|
||||
<ul class="pager">
|
||||
<li>
|
||||
<span>
|
||||
@ViewBag.ResultsCount results
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 text-right">
|
||||
@if (ViewBag.PreviousPage != null)
|
||||
{
|
||||
<nav>
|
||||
<ul class="pager">
|
||||
<li>
|
||||
<a data-partial href="?page=@ViewBag.PreviousPage&count=@ViewBag.PageSize">
|
||||
<i class="glyphicon glyphicon-chevron-left"></i>
|
||||
Prev.
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="col-md-2 text-center">
|
||||
<nav>
|
||||
<ul class="pager">
|
||||
<li>
|
||||
<span>Page @ViewBag.CurrentPage of @ViewBag.PageCount</span>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="col-md-5 text-left">
|
||||
@if (ViewBag.NextPage != null)
|
||||
{
|
||||
<nav>
|
||||
<ul class="pager">
|
||||
<li>
|
||||
<a data-partial href="?page=@ViewBag.NextPage&count=@ViewBag.PageSize">
|
||||
Next
|
||||
<i class="glyphicon glyphicon-chevron-right"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
118
Ch07/07_02_End/Website/Features/Products/ProductsController.cs
Normal file
118
Ch07/07_02_End/Website/Features/Products/ProductsController.cs
Normal file
@ -0,0 +1,118 @@
|
||||
using System.Linq;
|
||||
using System.Web.Mvc;
|
||||
using HPlusSports.Extensions;
|
||||
using HPlusSports.Models;
|
||||
|
||||
namespace HPlusSports.Controllers
|
||||
{
|
||||
public class ProductsController : Controller
|
||||
{
|
||||
public const int DefaultPageSize = 8;
|
||||
|
||||
private readonly HPlusSportsDbContext _context;
|
||||
|
||||
public ProductsController(HPlusSportsDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public ActionResult Index(int? page = null, int? count = null)
|
||||
{
|
||||
return Category(null, page, count);
|
||||
}
|
||||
|
||||
[AllowPartialRendering]
|
||||
public ActionResult Category(string id, int? page = null, int? count = null)
|
||||
{
|
||||
var category = _context.Categories.FirstOrDefault(x => x.Key == id);
|
||||
|
||||
IQueryable<Product> products = _context.Products;
|
||||
|
||||
if (category == null)
|
||||
{
|
||||
products =
|
||||
products
|
||||
.OrderBy(x => x.CategoryId)
|
||||
.ThenBy(x => x.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
products =
|
||||
products
|
||||
.Where(x => x.CategoryId == category.Id)
|
||||
.OrderBy(x => x.Name);
|
||||
}
|
||||
|
||||
var skus = products.Select(x => x.SKU);
|
||||
var ratings = _context.GetProductRatings(skus);
|
||||
|
||||
ViewData["Ratings"] = ratings;
|
||||
ViewData["Category"] = category ?? new Category { Name = "All Categories" };
|
||||
|
||||
/**** Paging Logic ****/
|
||||
var model = products;
|
||||
var resultsCount = model.Count();
|
||||
var pageSize = count.GetValueOrDefault(DefaultPageSize);
|
||||
var currentPage = page.GetValueOrDefault(1);
|
||||
var pageCount = resultsCount / pageSize + (resultsCount % pageSize > 0 ? 1 : 0);
|
||||
var previousPage = (currentPage - 1 > 0) ? currentPage - 1 : (int?)null;
|
||||
var nextPage = (currentPage + 1 <= pageCount) ? currentPage + 1 : (int?)null;
|
||||
|
||||
model = model
|
||||
.Skip((currentPage - 1) * pageSize)
|
||||
.Take(pageSize);
|
||||
|
||||
ViewData["PageSize"] = pageSize;
|
||||
ViewData["ResultsCount"] = resultsCount;
|
||||
ViewData["CurrentPage"] = currentPage;
|
||||
ViewData["PageCount"] = pageCount;
|
||||
ViewData["PreviousPage"] = previousPage;
|
||||
ViewData["NextPage"] = nextPage;
|
||||
|
||||
/**** End Paging Logic ****/
|
||||
|
||||
var vm = new ProductsListViewModel
|
||||
{
|
||||
Products = products.Select(x => new ProductViewModel {
|
||||
MSRP = x.MSRP,
|
||||
Name = x.Name,
|
||||
Price = x.Price,
|
||||
SKU = x.SKU,
|
||||
Rating = ratings.FirstOrDefault(y => x.SKU == y.SKU),
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
return View("ProductList", vm);
|
||||
}
|
||||
|
||||
public ActionResult Product(string id)
|
||||
{
|
||||
var product = _context.FindProductBySku(id);
|
||||
|
||||
if (product == null)
|
||||
return HttpNotFound();
|
||||
|
||||
var imageIds =
|
||||
product
|
||||
.Images
|
||||
.Select(img => img.Id)
|
||||
.ToArray();
|
||||
|
||||
ViewData["Rating"] = _context.GetProductRating(product.SKU);
|
||||
ViewData["Category"] = product.Category;
|
||||
ViewData["Images"] = imageIds;
|
||||
|
||||
return View(product);
|
||||
}
|
||||
|
||||
[ChildActionOnly]
|
||||
public ActionResult MenuCart()
|
||||
{
|
||||
var userId = User.Identity.Name;
|
||||
var cart = _context.ShoppingCarts.FirstOrDefault(x => x.UserId == userId)
|
||||
?? new ShoppingCart { UserId = userId };
|
||||
return PartialView("MenuCart", cart);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Web;
|
||||
|
||||
namespace HPlusSports.Models
|
||||
{
|
||||
public class ProductsListViewModel
|
||||
{
|
||||
public IEnumerable<ProductViewModel> Products { get; set; }
|
||||
}
|
||||
|
||||
public class ProductViewModel
|
||||
{
|
||||
public const double MinimumAdvertisableDiscount = 0.05;
|
||||
|
||||
public string SKU { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
[DataType(DataType.Currency)]
|
||||
public double MSRP { get; set; }
|
||||
|
||||
[DataType(DataType.Currency)]
|
||||
public double Price { get; set; }
|
||||
|
||||
[DisplayFormat(DataFormatString = "{0:P0}")]
|
||||
public double? Discount
|
||||
{
|
||||
get { return (MSRP - Price) / MSRP; }
|
||||
}
|
||||
|
||||
public bool HasAdvertisableDiscount
|
||||
{
|
||||
get { return Discount > MinimumAdvertisableDiscount; }
|
||||
}
|
||||
|
||||
public ProductRating Rating { get; set; }
|
||||
}
|
||||
}
|
||||
10
Ch07/07_02_End/Website/Features/Products/_Rating.cshtml
Normal file
10
Ch07/07_02_End/Website/Features/Products/_Rating.cshtml
Normal file
@ -0,0 +1,10 @@
|
||||
@model ProductRating
|
||||
|
||||
<span title="@Model.Rating.GetValueOrDefault().ToString("N1")">
|
||||
@for (var i = 1; i <= 5; i++)
|
||||
{
|
||||
var starClass = (Model.Rating >= i) ? "star" : "star-empty";
|
||||
<span class="rating-star glyphicon glyphicon-@starClass"></span>
|
||||
}
|
||||
</span>
|
||||
<span>(@Model.ReviewCount)</span>
|
||||
13
Ch07/07_02_End/Website/Features/Shared/Error.cshtml
Normal file
13
Ch07/07_02_End/Website/Features/Shared/Error.cshtml
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>Error</title>
|
||||
</head>
|
||||
<body>
|
||||
<hgroup>
|
||||
<h1>Error.</h1>
|
||||
<h2>An error occurred while processing your request.</h2>
|
||||
</hgroup>
|
||||
</body>
|
||||
</html>
|
||||
72
Ch07/07_02_End/Website/Features/Shared/_Layout.cshtml
Normal file
72
Ch07/07_02_End/Website/Features/Shared/_Layout.cshtml
Normal file
@ -0,0 +1,72 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>@ViewBag.Title - H+ Sport</title>
|
||||
<link rel="dns-prefetch" href="//fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro" rel="stylesheet">
|
||||
@Styles.Render("~/Content/css")
|
||||
@Scripts.Render("~/bundles/modernizr")
|
||||
<link rel="icon" href="https://hplussport.com/wp-content/uploads/2015/12/cropped-HSport_01-32x32.png" sizes="32x32">
|
||||
</head>
|
||||
<body>
|
||||
<div class="navbar">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a href="~/" class="navbar-brand">
|
||||
<img src="~/Content/logo.png" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-6 text-center title navbar-header">
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
</div>
|
||||
<div class="navbar-collapse collapse">
|
||||
<ul class="nav navbar-nav pull-right">
|
||||
<li class="dropdown">
|
||||
@if (User.IsInRole(HPlusSports.Models.UserRoles.Admin))
|
||||
{
|
||||
<a class="dropbtn" href="@Url.Action("Index", "Inventory")">
|
||||
<i class="glyphicon glyphicon-list-alt"></i>
|
||||
Inventory
|
||||
</a>
|
||||
}
|
||||
</li>
|
||||
<li class="dropdown">
|
||||
@Html.Action("MenuCart", "Products")
|
||||
</li>
|
||||
<li>
|
||||
@if (User.Identity.IsAuthenticated)
|
||||
{
|
||||
<form method="post" action="@Url.Action("Logoff", "Account", new { area = "" })">
|
||||
@Html.AntiForgeryToken()
|
||||
<button type="submit" class="btn btn-default">Logout</button>
|
||||
</form>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div>
|
||||
<a href="@Url.Action("Login", "Account")" class="btn btn-default">Login</a>
|
||||
</div>
|
||||
}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="main" class="container">
|
||||
@RenderBody()
|
||||
</div>
|
||||
|
||||
@Scripts.Render("~/bundles/jquery")
|
||||
@Scripts.Render("~/bundles/bootstrap")
|
||||
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/jquery.tablesorter/2.30.1/js/jquery.tablesorter.min.js"></script>
|
||||
@Scripts.Render("~/bundles/site")
|
||||
@RenderSection("scripts", required: false)
|
||||
</body>
|
||||
</html>
|
||||
44
Ch07/07_02_End/Website/Features/Web.config
Normal file
44
Ch07/07_02_End/Website/Features/Web.config
Normal file
@ -0,0 +1,44 @@
|
||||
<?xml version="1.0"?>
|
||||
|
||||
<configuration>
|
||||
<configSections>
|
||||
<sectionGroup name="system.web.webPages.razor" type="System.Web.WebPages.Razor.Configuration.RazorWebSectionGroup, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
|
||||
<section name="host" type="System.Web.WebPages.Razor.Configuration.HostSection, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
|
||||
<section name="pages" type="System.Web.WebPages.Razor.Configuration.RazorPagesSection, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
|
||||
</sectionGroup>
|
||||
</configSections>
|
||||
|
||||
<system.web.webPages.razor>
|
||||
<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=5.2.3.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
|
||||
<pages pageBaseType="System.Web.Mvc.WebViewPage">
|
||||
<namespaces>
|
||||
<add namespace="System.Web.Mvc" />
|
||||
<add namespace="System.Web.Mvc.Ajax" />
|
||||
<add namespace="System.Web.Mvc.Html" />
|
||||
<add namespace="System.Web.Optimization"/>
|
||||
<add namespace="System.Web.Routing" />
|
||||
<add namespace="HPlusSports"/>
|
||||
<add namespace="HPlusSports.Models"/>
|
||||
</namespaces>
|
||||
</pages>
|
||||
</system.web.webPages.razor>
|
||||
|
||||
<appSettings>
|
||||
<add key="webpages:Enabled" value="false" />
|
||||
</appSettings>
|
||||
|
||||
<system.webServer>
|
||||
<handlers>
|
||||
<remove name="BlockViewHandler"/>
|
||||
<add name="BlockViewHandler" path="*" verb="*" preCondition="integratedMode" type="System.Web.HttpNotFoundHandler" />
|
||||
</handlers>
|
||||
</system.webServer>
|
||||
|
||||
<system.web>
|
||||
<compilation>
|
||||
<assemblies>
|
||||
<add assembly="System.Web.Mvc, Version=5.2.3.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
|
||||
</assemblies>
|
||||
</compilation>
|
||||
</system.web>
|
||||
</configuration>
|
||||
3
Ch07/07_02_End/Website/Features/_ViewStart.cshtml
Normal file
3
Ch07/07_02_End/Website/Features/_ViewStart.cshtml
Normal file
@ -0,0 +1,3 @@
|
||||
@{
|
||||
Layout = "~/Views/Shared/_Layout.cshtml";
|
||||
}
|
||||
Reference in New Issue
Block a user