Tag: ASP.NET MVC

Remove Unused View Engines

By default, ASP.NET MVC loads both the Razor and Web Forms View Engines. This can cause performance issues. Because ASP.NET will look for Web Forms views first, and switch to look for Razor views if Web Forms views cannot be found.

This can be resolved easily by adding the following 2 lines to the Application_Start method in Global.asax.cs.

ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new RazorViewEngine());

Implement Password Expiration Check in ASP.NET MVC

1. Create SkipPasswordExpirationCheckAttribute which represents to skip the Password Expiration Check

using System;

namespace Unfuddle.MvcWebApp.Web.Mvc
{
    [AttributeUsageAttribute(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
    public sealed class SkipPasswordExpirationCheckAttribute : Attribute
    {
    }
}

2. Create PasswordExpirationCheckAttribute attribute which inherited from AuthorizeAttribute

using System;
using System.Security.Principal;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using System.Web.Security;

namespace Unfuddle.MvcWebApp.Web.Mvc
{
    [AttributeUsageAttribute(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
    public class PasswordExpirationCheckAttribute : AuthorizeAttribute
    {
        private int _maxPasswordAgeInDay;

        public PasswordExpirationCheckAttribute(int maxPasswordAgeInDay = int.MinValue)
        {
            _maxPasswordAgeInDay = maxPasswordAgeInDay;
        }

        public override void OnAuthorization(AuthorizationContext filterContext)
        {
            if (!filterContext.ActionDescriptor.IsDefined(typeof(SkipPasswordExpirationCheckAttribute), inherit: true)
                && !filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(SkipPasswordExpirationCheckAttribute), inherit: true))
            {
                if (_maxPasswordAgeInDay != int.MinValue)
                {
                    IPrincipal user = filterContext.HttpContext.User;

                    if (user != null && user.Identity.IsAuthenticated)
                    {
                        MembershipUser membershipUser = Membership.GetUser(user.Identity.Name);

                        if (membershipUser != null)
                        {
                            TimeSpan timeSpan = DateTime.Today - membershipUser.LastPasswordChangedDate.Date;

                            if (timeSpan.Days >= _maxPasswordAgeInDay)
                            {
                                HttpContext httpContext = HttpContext.Current;
                                HttpContextBase httpContextBase = new HttpContextWrapper(httpContext);
                                RouteData routeData = new RouteData();
                                RequestContext requestContext = new RequestContext(httpContextBase, routeData);
                                UrlHelper urlHelper = new UrlHelper(requestContext);

                                filterContext.HttpContext.Response.Redirect(urlHelper.Action("ChangePassword", "Account", new { reason = "passwordExpired" }));
                            }
                        }
                    }
                }
            }

            base.OnAuthorization(filterContext);
        }
    }
}

3. Add a setting MaxPasswordAgeInDay in ~\Properties\Settings.settings

MaxPasswordAgeInDay in Settings
MaxPasswordAgeInDay in Settings

4. Register PasswordExpirationCheckAttribute in ~\App_Start\FilterConfig.cs

filters.Add(new PasswordExpirationCheckAttribute(Settings.Default.MaxPasswordAgeInDay));

5. Register SkipPasswordExpirationCheckAttribute in LogOn and ChangePassword action methods of the AccountController

[AllowAnonymous]
[SkipPasswordExpirationCheck]
public ActionResult LogOn()
{
	......
}

[AllowAnonymous]
[SkipPasswordExpirationCheck]
[HttpPost]
public ActionResult LogOn(LogOnViewModel model, string returnUrl)
{
	......
}

[AllowAnonymous]
[SkipPasswordExpirationCheck]
public ActionResult ChangePassword(string reason = null)
{
	......
}

[AllowAnonymous]
[SkipPasswordExpirationCheck]
[HttpPost]
public ActionResult ChangePassword(ChangePasswordViewModel model)
{
	......
}

References

Membership in ASP.NET MVC

1. Edit ~\web.config to include the following membership configuration.

<configuration>
	<connectionStrings>
		<clear />
		<add name="ADConnectionString" connectionString="LDAP://ldap.unfuddle.com/CN=Users,DC=unfuddle,DC=com" />
	</connectionStrings>
	<system.web>
		<authentication mode="Forms">
			<forms loginUrl="~/Account/LogOn" timeout="30" />
		</authentication>
		<membership defaultProvider="ADMembershipProvider">
			<providers>
				<clear />
				<add name="ADMembershipProvider" 
					type="System.Web.Security.ActiveDirectoryMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" 
					connectionStringName="ADConnectionString" connectionUsername="<domainName>\administrator" connectionPassword="password"
					applicationName="Unfuddle.MvcWebApp" />
			</providers>
		</membership>
	</system.web>
</configuration>

2. Setup Forms Authentication

3. In AccountController.LogOn action method, use Membership to perform the authentication.

public class AccountController : Controller
{
	//
	// POST: /Account/LogOn
	[AllowAnonymous]
	[HttpPost]
	public ActionResult LogOn(LogOnViewModel model, string returnUrl)
	{
		if (this.ModelState.IsValid && Membership.ValidateUser(model.UserName, model.Password))
		{
			FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
			if (this.Url.IsLocalUrl(returnUrl))
			{
				return Redirect(returnUrl);
			}
			else
			{
				return RedirectToAction("Index", "Home");
			}
		}

		// If we got this far, something failed, redisplay form
		this.ModelState.AddModelError("", "Incorrect user name or password.");
		return View(model);
	}
}

References

Forms Authentication in ASP.NET MVC

1. Edit ~\web.config to include the following forms-based authentication configuration.

<system.web>
	<authentication mode="Forms">
		<forms loginUrl="~/Account/LogOn" timeout="30" />
	</authentication>
</system.web>

2. Register AuthorizeAttribute in ~\App_Start\FilterConfig.cs.

filters.Add(new AuthorizeAttribute());

3. Add view model LogOnViewModel in ~\Models\Account\LogOnViewModel.cs.

public class LogOnViewModel
{
	[Required]
	[Display(Name = "User name")]
	public string UserName { get; set; }

	[Required]
	[DataType(DataType.Password)]
	[Display(Name = "Password")]
	public string Password { get; set; }

	[Display(Name = "Remember me?")]
	public bool RememberMe { get; set; }
}

4. Add controller AccountController and LogOn action methods for both HttpGet & HttpPost.

public class AccountController : Controller
{
	//
	// GET: /Account/LogOn
	[AllowAnonymous]
	public ActionResult LogOn()
	{
		LogOnViewModel model = new LogOnViewModel();

		return View(model);
	}

	//
	// POST: /Account/LogOn
	[AllowAnonymous]
	[HttpPost]
	public ActionResult LogOn(LogOnViewModel model, string returnUrl)
	{
		if (this.ModelState.IsValid && Membership.ValidateUser(model.UserName, model.Password))
		{
			FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
			if (this.Url.IsLocalUrl(returnUrl))
			{
				return Redirect(returnUrl);
			}
			else
			{
				return RedirectToAction("Index", "Home");
			}
		}

		// If we got this far, something failed, redisplay form
		this.ModelState.AddModelError("", "Incorrect user name or password.");
		return View(model);
	}

	//
	// POST: /Account/LogOff
	[HttpPost]
	public ActionResult LogOff()
	{
		FormsAuthentication.SignOut();

		return RedirectToAction("Index", "Home");
	}
}

5. Add view in ~\Views\Account\LogOn.cshtml.

@model SecurityApp.Models.Account.LogOnViewModel
@{
    Layout = null;
    ViewBag.Title = "Log On";

    ViewBag.ReturnUrl = Request["ReturnUrl"];
}
<!DOCTYPE html>
<html>
......
<body>
    <h2>@ViewBag.Title</h2>
    @using (Html.BeginForm(null, null, new { returnUrl = ViewBag.ReturnUrl }, FormMethod.Post))
    {
        @Html.AntiForgeryToken()
        @Html.ValidationSummary(true)<br />
        @Html.TextBoxFor(m => m.UserName, new { placeholder = Html.DisplayNameFor(m => m.UserName) })<br />
        @Html.PasswordFor(m => m.Password, new { placeholder = Html.DisplayNameFor(m => m.Password) })<br />
        @Html.CheckBoxFor(m => m.RememberMe)
        @Html.DisplayNameFor(m => m.RememberMe)<br />
        <button type="submit">Log On</button>
    }

    ......
</body>
</html>

6. In ~\Views\Shared\_Layout.cshtml, add a HTML form to handle log off.

@using (Html.BeginForm("LogOff", "Account", FormMethod.Post, new { id = "logOffForm" }))
{
	@Html.AntiForgeryToken()
}

7. In ~\Views\Shared\_Layout.cshtml, add a hyperlink to log off.

<a href="javascript:$('#logOffForm').submit()">Log Off</a>

References

ASP.NET MVC SiteMapPath using Site Map Provider & Bootstrap Breadcrumbs

1. Install Bootstrap for MVC 4 NuGet package. This package will install Twitter Bootstrap to the project and add bundling and minification to application start.

2. Install MvcSiteMapProvider NuGet package. This package is a SiteMapProvider implementation for the ASP.NET MVC framework.

3. In web.config, downsize the MvcSiteMapProvider configuration to the minimal. Keep it simple, stupid. (KISS)

<system.web>
  <siteMap defaultProvider="MvcSiteMapProvider">
    <providers>
      <clear />
      <add name="MvcSiteMapProvider" type="MvcSiteMapProvider.DefaultSiteMapProvider, MvcSiteMapProvider" siteMapFile="~/Mvc.Sitemap" />
    </providers>
  </siteMap>
</system.web>

4. Modify the ~\Mvc.sitemap file as the following.

<?xml version="1.0" encoding="utf-8" ?>
<mvcSiteMap xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xmlns="http://mvcsitemap.codeplex.com/schemas/MvcSiteMap-File-3.0"
            xsi:schemaLocation="http://mvcsitemap.codeplex.com/schemas/MvcSiteMap-File-3.0 MvcSiteMapSchema.xsd"
            enableLocalization="true">
  <mvcSiteMapNode title="Home" controller="Home" action="Index">
    <mvcSiteMapNode title="About" controller="Home" action="About" />
    <mvcSiteMapNode title="Contact" controller="Home" action="Contact" />
    <mvcSiteMapNode title="Administration" clickable="false">
      <mvcSiteMapNode title="User Mgmt" controller="Home" action="UserMgmt" />
      <mvcSiteMapNode title="Role Mgmt" controller="Home" action="RoleMgmt" />
    </mvcSiteMapNode>
    <mvcSiteMapNode title="Profile" clickable="false">
      <mvcSiteMapNode title="Change Password" controller="Home" action="ChangePassword" />
      <mvcSiteMapNode title="Separator" clickable="false" />
      <mvcSiteMapNode title="Sign Off" controller="Home" action="SignOff" />
    </mvcSiteMapNode>
  </mvcSiteMapNode>
</mvcSiteMap>

5. The Twitter Bootstrap package installed in step 1 wouldn’t add the style and script rendering to the layout file. You need to do it manually by modifying the _Layout.cshtml file as following.

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width" />
  <title>@ViewBag.Title</title>
  @Styles.Render("~/Content/css")
  @Styles.Render("~/Content/bootstrap")
  @Scripts.Render("~/bundles/modernizr")
</head>
<body>
  @RenderBody()

  @Scripts.Render("~/bundles/jquery")
  @Scripts.Render("~/bundles/bootstrap")
  @RenderSection("scripts", required: false)
</body>
</html>

6. Add a partial view file BootstrapSiteMapPathHelperModel.cshtml under ~\Views\Shared\DisplayTemplates to render the Site Map Path which fits Bootstrap’s Breadcrumbs specifications.

@model MvcSiteMapProvider.Web.Html.Models.SiteMapPathHelperModel
@using System.Web.Mvc.Html
@using System.Linq
@using MvcSiteMapProvider.Web.Html.Models

@if (Model.Nodes.Count != 1)
{
    @:
<ul class="breadcrumb">
foreach (var node in Model.Nodes)
{
    if (node == Model.Nodes.First())
    {
        continue;
    }

    if (node != Model.Nodes.Last())
    {
        string url = node.IsClickable ? node.Url : "#";

            @:
	<li><a href="@url">@node.Title</a><span class="divider">></span></li>
}
    else
    {
            @:
	<li class="active">@node.Title</li>
}
}

    @:</ul>
}

7. Add HomeController, and then add Index, About, Contact, UserMgmt, RoleMgmt & ChangePassword Views.

8. Edit the _Layout.cshtml file again to render the MvcSiteMap’s site map path using the Bootstrap site map path partial view.

......
<body>
	<div class="container">
		<div class="row">
			<div class="span12">
				<nav>
					@Html.MvcSiteMap().Menu("BootstrapMenuHelperModel")
				</nav>
			</div>
		</div>
		<!-- //row -->
		<div class="row">
			<div class="span12">
				@Html.MvcSiteMap().SiteMapPath("BootstrapSiteMapPathHelperModel")
			</div>
		</div>
		<!-- //row -->
		<div class="row">
			<div class="span12">
				@RenderBody()
			</div>
		</div>
		<!-- //row -->
	</div>
	......
</body>
......

9. Completed!!!

About Site Map Path
About Site Map Path

Administration > User Mgmt Site Map Path
Administration > User Mgmt Site Map Path

Profile > Change Password Site Map Path
Profile > Change Password Site Map Path

You can download the code sample here.

References

ASP.NET MVC Model Validation using Data Annotations

In ASP.NET MVC, there are several ways to validate the model data prior to saving the data into the data store.
1. Validate the model data explicitly
2. Implement the IValidateableObject interface
3. Specify Data Annotations [Recommended]

Data Annotation is recommended because there are built-in Data Annotations in .NET Framework. You don’t have to implement your own validation logic, instead specifying the Validation Data Annotation that you need. The Data Annotations specified support both server-side & client-side validation. In case the built-in cannot fulfill your requirements, you can also implement your own Data Annotation.

Built-in Data Annotations

Namespace: System.ComponentModel.DataAnnotations

Validation Attribute Description
CompareAttribute Compares two properties
CustomValidationAttribute Specifies a custom validation method
DataTypeAttribute
Data type of the data field
MaxLengthAttribute
Max. length of array or string data allowed
MinLengthAttribute
Min. length of array or string data allowed
RangeAttribute
Numeric range constraints for the data field value
RegularExpressionAttribute
Data field value must match the specified regular expression
RequiredAttribute
Data field value is required
StringLengthAttribute
Min. and max. length of characters that are allowed in a data field

Namespace: System.Web.Security

Validation Attribute Description
MembershipPasswordAttribute Validates whether a password field meets the current password requirements for the membership provider

Code Sample

using System;
using System.ComponentModel.DataAnnotations;
using System.Web.Security;

namespace AspNetMvc.Models
{
    public class User
    {
        [DataType(DataType.Text)]
        [StringLength(30, MinimumLength = 6)]
        [Required()]
        public string UserName { get; set; }

        [DataType(DataType.Password)]
        [StringLength(255, MinimumLength = 8)]
        [Required()]
        [MembershipPassword()]
        public string Password { get; set; }

        [Compare("Password")]
        [DataType(DataType.Password)]
        [StringLength(255, MinimumLength = 8)]
        [Required()]
        [MembershipPassword()]
        public string ConfirmPassword { get; set; }

        [DataType(DataType.EmailAddress)]
        [StringLength(128)]
        [Required()]
        public string Email { get; set; }

        [DataType(DataType.PhoneNumber)]
        [RegularExpression("^[0-9]{8}$")]
        [StringLength(32)]
        public string Phone { get; set; }

        [DataType(DataType.Date)]
        public DateTime Birthday { get; set; }

        [DataType(DataType.MultilineText)]
        [StringLength(255)]
        public string Remarks { get; set; }
    }
}

Screenshots

ASP.NET MVC Validation (Required)
ASP.NET MVC Validation (Required)
ASP.NET MVC Validation (StringLength, DataType, RegularExpression)
ASP.NET MVC Validation (StringLength, DataType, RegularExpression)
ASP.NET MVC Validation (Compare)
ASP.NET MVC Validation (Compare)

Client-side Validation
1. Edit the ~\web.config file as the following

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <appSettings>
    <add key="webpages:Version" value="2.0.0.0" />
    <add key="webpages:Enabled" value="false" />
    <add key="PreserveLoginUrl" value="true" />
    <add key="ClientValidationEnabled" value="true" />
    <add key="UnobtrusiveJavaScriptEnabled" value="true" />
  </appSettings>
</configuration>

2. Check the ~\App_Start\BundleConfig.cs file to include the following line

bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(
			"~/Scripts/jquery.validate.unobtrusive*",
			"~/Scripts/jquery.validate*"));

References

Missing Layout in ASP.NET MVC Area’s Views

In ASP.NET MVC, it’s quite common to expect that the site layout defined in ~\Views\Shared\_Layout.cshtml can be applied in every view in the site. However, when you create a area’s view in ASP.NET MVC, you may found that the site layout defined in ~\Views\Shared\_Layout.cshtml cannot be rendered in the area’s view.

There are 3 ways to resolve this problem:
1. Copy the ~\Views\Shared\_Layout.cshtml file to the ~\Views\Shared\ folder in each area
2. Copy the ~\Views\_ViewStart.cshtml file to the ~\Views\ folder in each area
3. (Recommended) Move the ~\Views\_ViewStart.cshtml file to the root folder ~\ and modify the ~\web.config file

Method 3 is recommended because it is the only “Do once solve all” solution. Don’t need to do any extra things when you create a new area. Don’t Repeat Yourself (DRY).

1. Copy and paste system.web.webPages.razor config section group and system.web.webPages.razor section in ~\Views\web.config to ~\web.config

<configSections>
	<sectionGroup name="system.web.webPages.razor" type="System.Web.WebPages.Razor.Configuration.RazorWebSectionGroup, System.Web.WebPages.Razor, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
		<section name="host" type="System.Web.WebPages.Razor.Configuration.HostSection, System.Web.WebPages.Razor, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
		<section name="pages" type="System.Web.WebPages.Razor.Configuration.RazorPagesSection, System.Web.WebPages.Razor, Version=2.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=4.0.0.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" />
		</namespaces>
	</pages>
</system.web.webPages.razor>

2. Move the ~\Views\_ViewStart.cshtml file to the root folder ~\

3. Make sure the layout file defined in ~\_ViewStart.cshtml is point to ~\Views\Shared\_Layout.cshtml

@{
    Layout = "~/Views/Shared/_Layout.cshtml";
}

4. Completed!!! The layout defined in ~\Views\Shared\_Layout.cshtml can now be rendered in the area’s view.

References
+ Sharing a single _ViewStart Across Areas in ASP.NET MVC
+ Asp.Net MVC 3 Areas and _ViewStart.cshtml Scope