摘要:
在Windows客户端程序(WPF和Windows Forms)中使用Ninject和在控制台应用程序中使用Ninject没什么不同。在这些应用程序里我们不需要某些配置用来安装Ninject,因为在Windows客户端应用程序里,开发者可以控制UI组件的实例化(Forms或Windows),可以很容易地委托这种控制到Ninject。然而在Web应用程序里,就不同了,因为框架负责了实例化UI元素。因此,我们需要知道怎样告诉框架委托这种控制责任给Ninject。幸运的是,让ASP.NET MVC框架做这件事是很容易的,但是这又和Web Forms应用程序不同。
多亏了Ninject MVC扩展,我们甚至不需要麻烦地安装MVC框架来支持DI。反而,Ninject的MVC扩展将帮我们做这个。在这篇文章中,我们在ASP.NET MVC 3应用程序中使用Ninject。Ninject MVC扩展也支持其他版本的MVC框架。
还是继续使用上一篇文章的Solution,Demo.Northwind。
使用Ninject创建ASP.NET MVC应用程序
1. 创建MVC工程Demo.Northwind.MVC。
2. 使用NutGet Manager添加引用。
添加如下Ninject引用。
添加bootstrap引用(前端框架)和EntityFramework引用。
添加指向Demo.Northwind.Core的引用。
3. 修改根路径下的Web.config文件。
添加connectionStrings节
<connectionStrings>
<add name="NorthwindContext" providerName="System.Data.SqlClient" connectionString="Data Source=localhost;Initial Catalog=NORTHWND;Integrated Security=True" />
</connectionStrings>
4. 修改Ninject配置。
展开App_Start文件夹,发现自动添加了NinjectDependencyResolver.cs代码文件和NinjectWebCommon.cs代码文件。
打开NinjectWebCommon.cs文件,CreateKernel方法:
private static IKernel CreateKernel()
{
var kernel = new StandardKernel();
try
{
kernel.Bind<Func<IKernel>>().ToMethod(ctx => () => new Bootstrapper().Kernel);
kernel.Bind<IHttpModule>().To<HttpApplicationInitializationHttpModule>(); RegisterServices(kernel);
return kernel;
}
catch
{
kernel.Dispose();
throw;
}
}
CreateKernel方法就是是Ninject根控制的起始方法,他调用RegisterServices方法完成Ninject注入。
下面来看RegisterServices方法。RegisterServices方法调用System.Web.Mvc.DependencyResolver.SetResolver方法,传入一NinjectDependencyResolver对象,将DI控制任务委托给了NinjectDependencyResolver对象。
private static void RegisterServices(IKernel kernel)
{
System.Web.Mvc.DependencyResolver.SetResolver(new NinjectDependencyResolver(kernel));
}
再来看NinjectDependencyResolver.cs。
private readonly IKernel kernel; public NinjectDependencyResolver(IKernel kernelParam)
{
kernel = kernelParam;
AddBindings();
}
NinjectDependencyResolver类的AddBindings方法最终完成依赖注入。
修改AddBindings方法:
private void AddBindings()
{
kernel.Bind(x => x.FromAssembliesMatching("Demo.Northwind.*")
.SelectAllClasses().EndingWith("Repository")
.BindAllInterfaces());
}
5. 修改HomeController,添加Index、Edit这些Action。
using Demo.Northwind.Core.Interface;
using Demo.Northwind.Core.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc; namespace Demo.Northwind.MVC.Controllers
{
public class HomeController : Controller
{
private readonly ICustomerRepository repository; public HomeController(ICustomerRepository repository)
{
this.repository = repository;
} public ActionResult Index()
{
var customers = repository.GetAll();
return View(customers);
} public ActionResult Create()
{
return View("Edit", "");
} public ActionResult Edit(string customerId)
{
var customer = repository.Get(customerId);
return View(customer);
} [HttpPost]
public ActionResult Edit(Customer customer)
{
if (ModelState.IsValid)
{
repository.SaveCustomer(customer);
return RedirectToAction("Index");
}
return View();
} [HttpPost]
public ActionResult Delete(string customerId)
{
Customer deletedCustomer = repository.Delete(customerId);
if (deletedCustomer != null)
{
TempData["message"] = string.Format("{0} was deleted", deletedCustomer.CompanyName);
}
return RedirectToAction("Index");
}
}
}
HomeController使用构造函数注入方式,注入了ICustomerRepository对象。
private readonly ICustomerRepository repository; public HomeController(ICustomerRepository repository)
{
this.repository = repository;
}
6. 添加HomeController对应的View。
使用了bootstrap前端框架的css样式表。
_Layout.cshtml:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="~/Content/bootstrap.css" rel="stylesheet" />
<link href="~/Content/bootstrap-theme.css" rel="stylesheet" />
<link href="~/Content/ErrorStyles.css" rel="stylesheet" />
<title>@ViewBag.Title</title>
<style>
.navbar-right {
float: right !important;
margin-right: 15px;
margin-left: 15px;
}
</style>
</head>
<body>
<div class="row panel">
<div class="col-xs-12 col-sm-8">
@if (TempData["message"] != null)
{
<div class="alert alert-success">@TempData["message"]</div>
}
@RenderBody()
</div>
</div>
</body>
</html>
Index.cshtml:
@model IEnumerable<Demo.Northwind.Core.Model.Customer>
@{
ViewBag.Title = "Index";
Layout = "~/Views/Shared/_Layout.cshtml";
} <div class="panel panel-default">
<div class="panel-heading">
<h3>All Customers</h3>
</div>
<div class="panel-body">
<table class="table table-striped table-condensed table-bordered">
<tr>
<th class="text-right">ID</th>
<th>Company Name</th>
<th>City</th>
<th>Postal Code</th>
<th>Phone</th>
<th class="text-center">Actions</th>
</tr>
@foreach (var item in Model)
{
<tr>
<td class="text-right">@item.CustomerID</td>
<td>
@Html.ActionLink(item.CompanyName, "Edit", new
{
item.CustomerID
})
</td>
<td>@item.City</td>
<td>@item.PostalCode</td>
<td>@item.Phone</td>
<td class="text-center">
@using (Html.BeginForm("Delete", "Home"))
{
@Html.Hidden("CustomerID", item.CustomerID)
<input type="submit"
class="btn btn-default btn-xs"
value="Delete" />
}
</td>
</tr>
}
</table>
</div>
<div class="panel-footer">
@Html.ActionLink("Add a new customer", "Create", null, new { @class = "btn btn-default" })
</div>
</div>
Edit.cshtml:
@model Demo.Northwind.Core.Model.Customer
@{
ViewBag.Title = "Edit";
Layout = "~/Views/Shared/_Layout.cshtml";
}
@if (Model != null)
{
<h1>Edit @Model.CompanyName</h1>
}
else
{
<h1>Create</h1>
}
@using (Html.BeginForm("Edit", "Home", FormMethod.Post))
{
<div class="panel-body">
@foreach (var property in ViewData.ModelMetadata.Properties)
{
<div class="form-group">
<label>
@(property.DisplayName ?? property.PropertyName)
</label>
@if (property.PropertyName == "CustomerID" && Model != null)
{
@Html.TextBox(property.PropertyName, null, new { @class = "form-control", @readonly = "true" })
}
else
{
@Html.TextBox(property.PropertyName, null, new { @class = "form-control" })
}
@Html.ValidationMessage(property.PropertyName)
</div>
}
</div>
<div class="panel-footer">
<input type="submit" value="Save" class="btn btn-primary" />
@Html.ActionLink("Cancel and return to List", "Index", null, new { @class = "btn btn-default" })
</div>
}
7. 执行程序,得到运行结果。
Index页:
Edit页:
添加Log4Net日志分析
1. 通过NutGet Manager工具添加log4Net引用。
2. 修改应用程序配置,添加log4Net配置。
1)在根路径下的Web.config文件的appSettings里添加log4netConfigFile。
<add key="log4netConfigFile" value="log4net.xml" />
2)在根路径下添加log4net.xml文件。
<?xml version="1.0" encoding="utf-8" ?>
<log4net>
<appender name="FileAppender" type="log4net.Appender.FileAppender">
<file value="c:\LogFiles\Northwind.mvc.log" />
<appendToFile value="true" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date [%thread] %-5level %logger [%property{NDC}] - %message%newline" />
</layout>
</appender> <root>
<level value="DEBUG" />
<appender-ref ref="FileAppender" />
</root>
</log4net>
FileAppender指出使用文件系统日志。
appendFile支持日志文件路径。创建文件夹和文件:c:\LogFiles\Northwind.mvc.log。
设置log4net.xml文件的属性Copy to Output Directory。
3. 修改Global.asax.cs文件,注册Log4Net。
using log4net.Config;
using System;
using System.Configuration;
using System.IO;
using System.Web.Mvc;
using System.Web.Routing; namespace Demo.Northwind.MVC
{
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RouteConfig.RegisterRoutes(RouteTable.Routes); RegisterLog4Net();
} public static void RegisterLog4Net()
{
var log4NetConfigFile = ConfigurationManager.AppSettings["log4netConfigFile"];
var log4NetConfigPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, log4NetConfigFile);
XmlConfigurator.Configure(new FileInfo(log4NetConfigPath));
}
}
}
4. 修改NinjectDependencyResolver.cs文件。
修改AddBindings方法,添加Log4Net注入。
private void AddBindings()
{
kernel.Bind(x => x.FromAssembliesMatching("Demo.Northwind.*")
.SelectAllClasses().EndingWith("Repository")
.BindAllInterfaces()); kernel.Bind<ILog>().ToMethod(GetLogger);
}
绑定log4net接口ILog到工厂方法GetLogger。
下面添加GetLogger方法。
private static ILog GetLogger(IContext ctx)
{
var filterContext = ctx.Request.ParentRequest.Parameters
.OfType<FilterContextParameter>()
.SingleOrDefault();
return LogManager.GetLogger(filterContext == null ?
ctx.Request.Target.Member.DeclaringType :
filterContext.ActionDescriptor.ControllerDescriptor.ControllerType);
}
- 从参数IContext对象得到日志上下文对象的类型。
- log4net使用LogManager的静态方法GetLogger得到ILog对象:
public class MyClass
{
private static readonly ILog log = LogManager.GetLogger(typeof(MyApp));
}
5. 我们将使用特性修饰Action,进行写日志,因此添加LogAttribute特性类。
using System; namespace Demo.Northwind.MVC
{
public class LogAttribute : Attribute
{
public string LogLevel { get; set; }
}
}
LogAttribute特性类包含一个字符串类型的LogLevel属性。
6. 使用IActionFilter接口实现写日志功能。因此创建类LogFilter继承自IActionFilter接口。
using log4net;
using log4net.Core;
using System.Globalization;
using System.Web.Mvc; namespace Demo.Northwind.MVC
{
public class LogFilter : IActionFilter
{
private readonly ILog log;
private readonly Level logLevel; public LogFilter(ILog log, string logLevel)
{
this.log = log;
this.logLevel = log.Logger.Repository.LevelMap[logLevel];
} public void OnActionExecuting(ActionExecutingContext filterContext)
{
var message = string.Format(
CultureInfo.InvariantCulture,
"Executing action {0}.{1}",
filterContext.ActionDescriptor.ControllerDescriptor.ControllerName,
filterContext.ActionDescriptor.ActionName);
this.log.Logger.Log(typeof(LogFilter), this.logLevel, message, null);
} public void OnActionExecuted(ActionExecutedContext filterContext)
{
var message = string.Format(
CultureInfo.InvariantCulture,
"Executed action {0}.{1}",
filterContext.ActionDescriptor.ControllerDescriptor.ControllerName,
filterContext.ActionDescriptor.ActionName);
this.log.Logger.Log(typeof(LogFilter), this.logLevel, message, null);
}
}
}
- LogFilter类通过构造函数,注入log4Net的ILog接口对象。
- 通过构造函数传入logLevel字符串,并从log.Logger.Repository.LevelMap查找,得到log4net.Core.Level。构造函数的logLevel字符串参数值将被使用Ninject注入。
- IActionFilter接口方法OnActionExecuting和方法OnActionExecuted将在Action执行时和执行后触发。而参数ActionExecutingContext对象保存了Action的上下文信息。
7. 再次修改NinjectDependencyResolver.cs文件的AddBindings方法。
private void AddBindings()
{
kernel.Bind(x => x.FromAssembliesMatching("Demo.Northwind.*")
.SelectAllClasses().EndingWith("Repository")
.BindAllInterfaces()); kernel.Bind<ILog>().ToMethod(GetLogger); kernel.BindFilter<LogFilter>(FilterScope.Action, )
.WhenActionMethodHas<LogAttribute>()
.WithConstructorArgumentFromActionAttribute<LogAttribute>(
"logLevel",
attribute => attribute.LogLevel);
}
1)为了告诉MVC使用Ninject来解析一个过滤器,我们需要使用Kernel对象的方法BindFilter<TFilter>来注册这个过滤器:
Kernel.BindFilter<LogFilter>(FilterScope.Action, );
BindFilter方法的原型如下:
public static IFilterBindingWhenInNamedWithOrOnSyntax<T> BindFilter<T>(this IBindingRoot kernel, FilterScope scope, int? order);
FilterScope是一个枚举,指出过滤器用在什么东西之上。
2)WhenXXX方法用来定义在哪种情况下使用这个过滤器,Ninject提供了三个这样的方法:
• WhenControllerType: 这个方法只在指定类型的Controller上使用过滤器。
• WhenControllerHas: 这个方法在含有指定特性类型的Controller上使用过滤器。
• WhenActionMethodHas: 这个方法在含有指定特性类型的Action上使用过滤器。
代码中使用的是:WhenActionMethodHas<LogAttribute>(),指出在有LogAttribute特性修饰的Action上使用此过滤器。
3) WithXXX方法对过滤器注入参数。Ninject提供了四个这样的方法:
• WithConstructorArgumentFromActionAttribute: 从Action方法的特性Attribute得到构造函数参数。
• WithConstructorArgumentFromControllerAttribute: 从Controller类的特性Attribute得到构造函数参数。
• WithPropertyValueFromActionAttribute: 如果是属性注入,用Action方法的特性Attribute值来设置属性值。
• WithPropertyValueFromControllerAttribute: 如果是属性注入,用Controller类的特性Attribute值来设置属性值。
代码中使用的是:WithConstructorArgumentFromActionAttribute<LogAttribute>("logLevel", attribute => attribute.LogLevel);从Action的特性Attribute设置构造函数参数logLevel的值。
8. 再次运行程序,打开C:\LogFiles\Northwind.mvc.log文件,得到写出的日志。
数据验证
如果我们需要验证Domain的Customer对象,需要引入System.ComponentModel.DataAnnotations名称空间。
using System.ComponentModel.DataAnnotations; namespace Demo.Northwind.Core.Model
{
public class Customer
{
[Required]
[Display(Name = "Company ID")]
public string CustomerID { get; set; }
[Required]
[Display(Name ="Company Name")]
public string CompanyName { get; set; }
public string City { get; set; }
[Display(Name = "Postal Code")]
public string PostalCode { get; set; }
[StringLength()]
public string Phone { get; set; }
}
}
上面的修改能够验证一般的业务逻辑:不能为空,字符串长度。但是如果要求验证一个Customer对象的CompanyName在数据库中是否已经存在要怎么做呢?
同样使用给属性添加特性的办法,添加唯一性验证逻辑。
1. 首先添加自定义的InjectAttribute特性:
namespace Demo.Northwind.Core.Attribute
{
public class InjectAttribute : System.Attribute
{
}
}
2. 修改NinjectWebCommon.cs的CreateKernel方法。
private static IKernel CreateKernel()
{
var ninjectSettings = new NinjectSettings
{
InjectAttribute = typeof(Demo.Northwind.Core.Attribute.InjectAttribute)
};
var kernel = new StandardKernel(ninjectSettings);
try
{
kernel.Bind<Func<IKernel>>().ToMethod(ctx => () => new Bootstrapper().Kernel);
kernel.Bind<IHttpModule>().To<HttpApplicationInitializationHttpModule>(); RegisterServices(kernel);
return kernel;
}
catch
{
kernel.Dispose();
throw;
}
}
StandardKernel构造函数传入NinjectSettings对象,重写了默认的Ninject特性设置。
默认的Ninject的Inject特性如下:
[AttributeUsage(AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
public class InjectAttribute : Attribute
{
public InjectAttribute();
}
3. 创建UniqueCustomerIdAttribute特性类,继承自ValidationAttribute抽象类。
using Demo.Northwind.Core.Attribute;
using Demo.Northwind.Core.Interface;
using System;
using System.ComponentModel.DataAnnotations; namespace Demo.Northwind.Core
{
public class UniqueCustomerIdAttribute : ValidationAttribute
{
[Inject]
public ICustomerRepository Validator
{
get; set;
} public override bool IsValid(object value)
{
if (Validator == null)
{
throw new Exception("Validator is not specified.");
}
if (string.IsNullOrEmpty(value as string))
{
return false;
}
return Validator.ValidateUniqueness(value as string);
}
}
}
在属性public ICustomerRepository Validator上使用刚才定义的[Inject]特性实现DI注入,这里注入的是ICustomerRepository对象。
重写抽象类ValidationAttribute的Validate方法,添加验证逻辑。
4. 再次修改Customer类,为属性CompanyName添加UniqueCustomerId特性。
using System.ComponentModel.DataAnnotations; namespace Demo.Northwind.Core.Model
{
public class Customer
{
[Required]
[UniqueCustomerId]
[Display(Name = "Company ID")]
public string CustomerID { get; set; }
[Required]
[Display(Name ="Company Name")]
public string CompanyName { get; set; }
public string City { get; set; }
[Display(Name = "Postal Code")]
public string PostalCode { get; set; }
[StringLength()]
public string Phone { get; set; }
}
}
5. 再次运行程序,如果试图添加重复的记录,则会出现下面的页面: