[JQuery]JQuery的FckEditor插件

mikel阅读(789)

What does it do?

 
 

Try it yourself

Conventional Submission (iFRAME)

This type of submission always works without the plugin.

 

Ajax Submission (DIV)

This type of submission would not work without this plugin.

(Ajax Form submission powered by the jQuery Form Plugin)

 
 

Download

All you really need is FKCEditor (DUH!), jQuery (of course) and the jQuery FCKEditor Plugin.

But if you want to submit your form via Ajax, then you need to get the jQuery Form Plugin by Malsup.

 
 

Usage

  • Method 1: replacing ALL textareas in a page with THE SAME settings

    <textarea name="fck1" cols="50" rows="6"></textarea>
    <textarea name="fck2" cols="50" rows="6"></textarea>
    <textarea name="fck3" cols="50" rows="6"></textarea>
    <script> $(function(){ $('textarea').fck({path: '/path/to/fck/directory/'}); }); </script>
    
  • Method 2: replacing SOME textareas in a page with THE SAME settings
    In this example, only textarea fck3 will not be converted into a FCKEditor.

    <textarea name="fck1" class="fck" cols="50" rows="6"></textarea>
    <textarea name="fck2" class="fck" cols="50" rows="6"></textarea>
    <textarea name="fck3" cols="50" rows="6"></textarea>
    <script> $(function(){ $('textarea.fck').fck({path: '/path/to/fck/directory/'}); }); </script>
    
  • Method 3: replacing SOME textareas in a page with DIFFERENT SETTINGS (see demo)

    <textarea name="fck1" cols="50" rows="6"></textarea>
    <textarea name="fck2" cols="50" rows="6"></textarea>
    <textarea name="fck3" cols="50" rows="6"></textarea>
    <script>
    $(function(){
    $.fck.config = {path: '/path/to/fck/directory/', height:300 };
    $('textarea#fck1').fck(/* default settings */);
    $('textarea#fck2').fck({ toolbar:'MyCustomerToolbar' });
    $('textarea#fck3').fck({ toolbar:'MyCustomerToolbar', height:200 });
    });
    </script>
    
 
 

Important pointers…

  • This is a working idea that hasn't yet been fully tested.
    Tested and works on Firefox 2, Opera, Safari (Win), IE7 and IE6 (all in WinXP SP2).
  • The same principle can be applied to TinyMCE, Codepress and any other rich-text editors.
    Any input regarding other rich-text editors is welcome.
  • The plugin will intercept known methods and install itself against related plugins (currently only the jQuery Form Plugin).
    Any input regarding integrating this plugin with other JQuery plugins is welcome.

[MVC]使用Forms Authentication实现用户注册、登录 (三)用户实体替换

mikel阅读(925)

IPrincipalIIdentity

  通过查阅文档,我们可以看到HttpContext.User属性的类型是IPrincipal接口。然而我们知道,接口通常是不能直接访问的,其背后必定隐藏了一个实现了该接口的对象。那么这个实际对象的类型是什么呢?

  让我们在前面示例的MasterPagePage_Init方法上加一个断点,再次运行程序,可以得到HttpContext.User属性的真正类型是System.Security.Principal.GenericPrincipal

  查看IPrincipal接口的定义,可以看到它只有一个公开属性——Identity,其类型是这里要提到的另外一个重要接口IIdentity。通过上面的断点跟踪,我们还能知道对于GenericPrincipal而言,其Identity属性的实际类型是GenericIdentity,也是位于System.Security.Principal命名空间中。

  由此,我们引出了.NET Framework中关于Principal(实体)的整个类型系统。所有这些类型都位于mscorlib.dll程序集中,由此也可以看出,这套模型隶属于整个系统的基石。

 

实现自己的IPrincipal

  要想用自己的实体对象替换HttpContext.User,就必须让自己的实体对象实现IPrincipal接口;通常还必须伴随着实现IIdentity接口。

  目前系统中有的是一个数据实体对象。一般而言,实现IPrincipal接口有一下两种方式——

l  编写单独的类型实现IPrincipal接口,并在其中包含数据实体对象;

l  修改数据实体对象使其实现IPrincipal接口。

  对于这两种方式而言,其Identity属性可以通过以下三种方式实现——

l  使用.NET Framework提供的GenericIdentity

l  创建自定义的类,实现Identity接口;

l  修改数据实体对象或自定义的实体类,让它们同时实现IPrincipalIIdentity接口。

  对于简单的应用程序而言,通常可以修改数据实体对象,使其同时实现IPrincipalIIdentity接口。而就复杂的分层架构应用程序,则建议在逻辑层创建分别实现了IPrincipalIIdentity接口的对象。本文的示例      明显属于前一种情况,因此我们考虑修改作为数据实体类的UserObject类,让其实现两个接口。以下是修改完毕的UserObject类:

 

public class UserObject : IPrincipal, IIdentity

{

     /// <summary>

     /// 用户名。

     /// </summary>

     public string Name;

 

     /// <summary>

     /// 密码散列值。

     /// </summary>

     public string PasswordHash;

 

     /// <summary>

     /// 密码salt值。

     /// </summary>

     public string PasswordSalt;

 

     #region IIdentity Members

 

     public string AuthenticationType

     {

         get

         {

              return "Froms";

         }

     }

 

     public bool IsAuthenticated

     {

         get

         {

              return true;

         }

     }

 

     string IIdentity.Name

     {

         get

         {

              return this.Name;

         }

     }

 

     #endregion

 

     #region IPrincipal Members

 

     public IIdentity Identity

     {

         get

         {

              return this;

         }

     }

 

     public bool IsInRole(string role)

     {

         return false;

     }

 

     #endregion

}

 

  首先我们来看一下对IIdentity接口的实现。该接口要求三个属性——AuthenticationTypeIsAuthenticatedNameAuthenticationType表示该用户标识所使用的验证类型,这里返回的是“Forms”;IsAuthenticated属性表示当前用户是否已经通过验证(即是否已登录。在这个例子里,我们只针对已登录用户进行实体替换,所以这个属性总是返回true。通常,实际的Web应用程序编写时还有一种习惯,就是为未登录用户(称之为匿名用户)也提供一个用户实体对象,此时就需要为IsAuthenticated提供逻辑,判断用户是否已通过验证了。最后IIdentity接口还要求对象提供一个Name属性,在这里,由于已经存在了Name字段,因此才用“显示接口实现”来提供Name属性,返回对象自身的Name字段即可。

  接下来我们看一下IPrincipal接口的实现。该接口要求提供一个Identity属性和一个IsInRole方法。由于UserObject类本身已经实现了IIdentity接口,因此在Identity属性中直接reutren this即可。因为我们这个示例不涉及用户分组(角色)方面的技术,因此IsInRole方法总是返回false

 

用户实体替换

  用户实体替换即使用我们自己编写的类型的实例来替换HttpContext.User属性。实体替换应该发生在HttpApplicationPostAuthenticateRequest事件发生时,因为此时ASP.NET已经从客户端得到了用户凭证Cookie并进行了解密和校验。

  我们既可以编写一个HttpModule来处理PostAuthenticateRequest事件,也可以在Global..asax文件中添加时间处理器。这里为了简单,我们选择在Global.asax中添加如下事件处理器:

 

void Application_PostAuthenticateRequest(object sender, EventArgs e)

{

     HttpApplication app = (HttpApplication)sender;

     if(app.Context.User.Identity.Name != "")  // 仅在已登录时替换

     {

         UserObject user = DataAccess.GetUserByName(app.Context.User.Identity.Name);

         app.Context.User = user;

         Thread.CurrentPrincipal = user;

     }

}

 

  在这里我们首先进行了判断,如果用户已登录,才进行实体替换。当然你也可以选择未未登录用户也提供一个匿名用户实体。

  接下来,我们通过本来已经存放在HttpContext.User.Identity中的用户标识得到了数据实体对象,然后分别将其赋予HttpContext.UserThread.CurrentPrincipal

  至此,我们的示例代码就完工了。没有提到的是,完成了这一步之后,你就可以通过类似下面的代码在任何可以访问到HttpContext的地方获取用户实体了:

 

UserObject user = HttpContext.Current.User as UserObject;

if(user != null)

{

       // 可以使用user

}

else

{

       // 用户未登录

}

 

  需要注意,由于在这里我们仅对已登录用户进行了用户实体替换,所以代码使用as进行类型转换并结合if语句进行判断是必需的。

 

小结

  好吧,这一部分说的是用户实体替换。

[MVC]ASP.NET MVC Preview 4 分析 - 2. Filter

mikel阅读(830)

过滤器 (Filter) 是 MVC 的一个重要特征,它提供了非常灵活的扩展和解耦机制。P4 版的过滤器总算像那么回事了,同时做了更深度的划分,使得开发人员可以控制更多的细节和流程。
1. IActionFilter
主要用来干预 Action 的执行,我们可以通过上下文对象修改请求参数或者取消 Action 的执行。

public interface IActionFilter
{
  void OnActionExecuting(ActionExecutingContext filterContext);
  void OnActionExecuted(ActionExecutedContext filterContext);
}

默认实现是 ActionFilterAttribute,这也是我们开发自定义过滤器通常所用的基类。

public abstract class ActionFilterAttribute : FilterAttribute, IActionFilter, IResultFilter
{
  public virtual void OnActionExecuting(ActionExecutingContext filterContext)
  {
  }
  public virtual void OnActionExecuted(ActionExecutedContext filterContext)
  {
  }
  // …
}

ActionExecutingContext 有个很重要的属性 Cancel,通过它我们可以取消 Action 和后续 Filter 的执行。

public class ActionExecutingContext : ControllerContext
{
  // Methods
  public ActionExecutingContext(ControllerContext controllerContext, MethodInfo actionMethod,
    IDictionary<string, object> actionParameters);
  // Properties
  public MethodInfo ActionMethod { get; private set; }
  public IDictionary<string, object> ActionParameters { get; private set; }
  public bool Cancel { get; set; }
  public ActionResult Result { get; set; }
}
public class ActionExecutedContext : ControllerContext
{
  // Methods
  public ActionExecutedContext(ControllerContext controllerContext, MethodInfo actionMethod,
    bool canceled, Exception exception);
  // Properties
  public MethodInfo ActionMethod { get; private set; }
  public bool Canceled { get; private set; }
  public Exception Exception { get; private set; }
  public bool ExceptionHandled { get; set; }
  public ActionResult Result { get; set; }
}

举例说明一下。

public class TestController : Controller
{
  [Filter1(Order = 1)]
  [Filter2(Order = 2)]
  [Filter3(Order = 3)]
  public ActionResult Index()
  {
  }
}

当我们在 Filter2.OnActionExecuting 中执行取消操作时,那么实际输出效果应该就是下面这样子。

class Filter2 : Filter1
{
  public override void OnActionExecuting(ActionExecutingContext filterContext)
  {
    filterContext.Cancel = true;
  }
}

输出:
TestController.OnActionExecuting
Filter1.OnActionExecuting
Filter2.OnActionExecuting
Filter1.OnActionExecuted
TestController.OnActionExecuted
我 们发现 Action 和排在后面的 Filter3 没有被调用,同时 Filter2 的 OnActionExecuted 也没有被执行。由于 Filter.Order 最小值是 -1,而 Controller.Order = -1,因此默认情况下 Controller Filter 总是被调用执行。
2. IResultFilter
可以利用该类型过滤器修改输出结果,诸如视图引擎和输出数据等等。

public interface IResultFilter
{
  void OnResultExecuting(ResultExecutingContext filterContext);
  void OnResultExecuted(ResultExecutedContext filterContext);
}

其默认实现同样是 ActionFilterAttribute。

public abstract class ActionFilterAttribute : FilterAttribute, IActionFilter, IResultFilter
{
  // …
  public virtual void OnResultExecuting(ResultExecutingContext filterContext)
  {
  }
  public virtual void OnResultExecuted(ResultExecutedContext filterContext)
  {
  }
}

在 ResultExecutingContext 中也有个 Cancel 属性,通过它我们可以阻止 View 输出和后续 Filter 执行。

public class ResultExecutingContext : ControllerContext
{
  // Methods
  public ResultExecutingContext(ControllerContext controllerContext, ActionResult result);
  // Properties
  public bool Cancel { get; set; }
  public ActionResult Result { get; set; }
}
public class ResultExecutedContext : ControllerContext
{
  // Methods
  public ResultExecutedContext(ControllerContext controllerContext, ActionResult result,
    bool canceled, Exception exception);
  // Properties
  public bool Canceled { get; private set; }
  public Exception Exception { get; private set; }
  public bool ExceptionHandled { get; set; }
  public ActionResult Result { get; private set; }
}

同样在上文例子中的 Filter2.OnResultExecuting 中设置取消操作。

class Filter2 : Filter1
{
  public override void OnResultExecuting(ResultExecutingContext filterContext)
  {
    filterContext.Cancel = true;
  }
}

输出:
TestController.OnResultExecuting
Filter1.OnResultExecuting
Filter2.OnResultExecuting
Filter1.OnResultExecuted
TestController.OnResultExecuted
Action 返回的 View(ActionResult) 以及 Filter3、Filter2.OnResultExecuted 都没有被执行。
3. IAuthorizationFilter
这是新增的过滤器类型,它拥有最先执行的特权,用来实现登录验证操作。

public interface IAuthorizationFilter
{
  void OnAuthorization(AuthorizationContext filterContext);
}

默认实现是 AuthorizeAttribute。

public sealed class AuthorizeAttribute : FilterAttribute, IAuthorizationFilter
{
  // Methods
  public AuthorizeAttribute();
  public void OnAuthorization(AuthorizationContext filterContext);
  // Properties
  public string Roles { get; set; }
  public string Users { get; set; }
}

AuthorizeAttribute 通过对 HttpContext.User 进行一系列的判断操作,然后输出一个新增的 ActionResult —— HttpUnauthorizedResult,这算是其中最有价值的代码了。

public class HttpUnauthorizedResult : ActionResult
{
  public override void ExecuteResult(ControllerContext context)
  {
    // …
    // 401 is the HTTP status code for unauthorized access – setting this
    // will cause the active authentication module to execute its default
    // unauthorized handler
    context.HttpContext.Response.StatusCode = 401;
  }
}

上下文对象中也有个 Cancel,通过它可以告诉 ActionInvoker 停止后面的调用操作,跳转到登录页。

public class AuthorizationContext : ControllerContext
{
  // Methods
  public AuthorizationContext(ControllerContext controllerContext, MethodInfo actionMethod);
  // Properties
  public MethodInfo ActionMethod { get; private set; }
  public bool Cancel { get; set; }
  public ActionResult Result { get; set; }
}

详情可参考下面这些代码。

public sealed class AuthorizeAttribute : FilterAttribute, IAuthorizationFilter
{
  public void OnAuthorization(AuthorizationContext filterContext)
  {
    // …
    IPrincipal user = filterContext.HttpContext.User;
    if (!user.Identity.IsAuthenticated)
    {
      filterContext.Cancel = true;
      filterContext.Result = new HttpUnauthorizedResult();
      return;
    }
    if (!String.IsNullOrEmpty(Users))
    {
      IEnumerable<string> validNames = SplitString(Users);
      bool wasMatch = validNames.Any(name => String.Equals(name, user.Identity.Name,
        StringComparison.OrdinalIgnoreCase));
      if (!wasMatch)
      {
        filterContext.Cancel = true;
        filterContext.Result = new HttpUnauthorizedResult();
        return;
      }
    }
    if (!String.IsNullOrEmpty(Roles))
    {
      IEnumerable<string> validRoles = SplitString(Roles);
      bool wasMatch = validRoles.Any(role => user.IsInRole(role));
      if (!wasMatch)
      {
        filterContext.Cancel = true;
        filterContext.Result = new HttpUnauthorizedResult();
      }
    }
  }
}
public class ControllerActionInvoker
{
  public virtual bool InvokeAction(string actionName, IDictionary<string, object> values)
  {
    // …
    AuthorizationContext authContext = InvokeAuthorizationFilters(methodInfo,
      filterInfo.AuthorizationFilters);
    if (authContext.Cancel)
    {
      // not authorized, so don't execute the action method or its filters
      InvokeActionResult(authContext.Result ?? EmptyResult.Instance);
    }
    // …
  }
  protected virtual AuthorizationContext InvokeAuthorizationFilters(…)
  {
    // …
    AuthorizationContext context = new AuthorizationContext(ControllerContext, methodInfo);
    foreach (IAuthorizationFilter filter in filters)
    {
      filter.OnAuthorization(context);
      // short-circuit evaluation
      if (context.Cancel)
      {
        break;
      }
    }
    return context;
  }
}

ControllerActionInvoker 通过 InvokeAuthorizationFilters() 调用 AuthorizeAttribute,然后依次判断登录和角色信息。如果条件不符,则设置取消操作并返回一个 HttpUnauthorizedResult,这个 ActionResult 被 InvokeAciton 所激活,因此才有了 "context.HttpContext.Response.StatusCode = 401" 跳转。
4. IExceptionFilter
用于捕获 Filter 和 Action 执行异常,以便实现自定义错误页面。

public interface IExceptionFilter
{
  void OnException(ExceptionContext filterContext);
}

默认实现是 HandleErrorAttribute。

public sealed class HandleErrorAttribute : FilterAttribute, IExceptionFilter
{
  // Methods
  public HandleErrorAttribute();
  void OnException(ExceptionContext filterContext);
  // Properties
  public Type ExceptionType { get; set; }
  public string View { get; set; }
}

从上下文中我们可以获取具体的异常信息,可以使用 ExceptionHandled 来阻止后续 HandleErrorAttribute 执行,还可以设置一个 ActionResult 来显示错误页面。

public class ExceptionContext : ControllerContext
{
  // Methods
  public ExceptionContext(ControllerContext controllerContext, Exception exception);
  // Properties
  public Exception Exception { get; private set; }
  public bool ExceptionHandled { get; set; }
  public ActionResult Result { get; set; }
}
public sealed class HandleErrorAttribute : FilterAttribute, IExceptionFilter
{
  public void OnException(ExceptionContext filterContext)
  {
    // …
    if (controller == null || filterContext.ExceptionHandled)
    {
      return;
    }
    // …
    HandleErrorInfo model = new HandleErrorInfo(filterContext.Exception, controllerName, actionName);
    filterContext.Result = new ViewResult()
    {
      TempData = controller.TempData,
      ViewData = new ViewDataDictionary<HandleErrorInfo>(model),
      ViewEngine = controller.ViewEngine,
      ViewName = View,
    };
    filterContext.ExceptionHandled = true;
    filterContext.HttpContext.Response.Clear();
    filterContext.HttpContext.Response.StatusCode = 500;
  }
}

ControllerActionInvoker 使用 try…catch 来捕获执行异常,并通过 InvokeExceptionFilters 完成 OnException 调用。

public class ControllerActionInvoker
{
  public virtual bool InvokeAction(string actionName, IDictionary<string, object> values)
  {
    // …
    try
    {
      // …
    }
    catch (Exception ex)
    {
      // something blew up, so execute the exception filters
      ExceptionContext exceptionContext = InvokeExceptionFilters(ex, filterInfo.ExceptionFilters);
      if (!exceptionContext.ExceptionHandled)
      {
        throw;
      }
      InvokeActionResult(exceptionContext.Result);
    }
    // …
  }
  protected virtual ExceptionContext InvokeExceptionFilters(exception, filters)
  {
    // …
    ExceptionContext context = new ExceptionContext(ControllerContext, exception);
    foreach (IExceptionFilter filter in filters)
    {
      filter.OnException(context);
    }
    return context;
  }
}

附: Controller 实现了上述所有的过滤器类型。

public abstract class Controller :
  IActionFilter,
  IAuthorizationFilter,
  IController,
  IDisposable,
  IExceptionFilter,
  IResultFilter
{
}

—————–
本文仅分析一些调用细节,有关使用方法可参考 《ASP.NET MVC Preview 4 Release (Part 1) 》 (中文版)。

[MVC]Asp.net Mvc Framework 七 (Filter及其执行顺序)

mikel阅读(780)

应用于Action的Filter
ASP.NETMvc中当你有以下及类似以下需求时你可以使用Filter功能
判断登录与否或用户权限,决策输出缓存,防盗链,防蜘蛛,本地化设置,实现动态Action
filter是一种声明式编程方式,在ASP.NET MVC中它只能应用在Action上
Filter要继承于ActionFilterAttribute抽象类,并可以覆写void OnActionExecuting(FilterExecutingContext)和
void OnActionExecuted(FilterExecutedContext)这两个方法
OnActionExecuting是Action执行前的操作,OnActionExecuted则是Action执行后的操作
下面我给大家一个示例,来看看它的的执行顺序
首先我们先建立 一个Filter,名字叫做TestFilter

using System.Web.Mvc;

namespace MvcApplication2.Controllers
{
    
public class TestFilter : ActionFilterAttribute
    
{
        
public override void OnActionExecuting(FilterExecutingContext
           filterContext) 
{
            filterContext.HttpContext.Session[
"temp"+= "OnActionExecuting<br/>";
        }


        
public override void OnActionExecuted(FilterExecutedContext
            filterContext) 
{
            filterContext.HttpContext.Session[
"temp"+= "OnActionExecuted<br/>";
        }

    }

}

在这里我们在Session["temp"]上标记执行的顺序
我们在Controller中的Action中写以下代码

        [TestFilter]
        
public void Index() {
this.HttpContext.Session["temp"+= "Action<br/>";
            RenderView(
"Index");
        }

在这个Action执行的时候,我们也为Session["temp"]打上了标记.
最后是View的内容
很简单我们只写

<%=Session["temp"%>

这样我们就可以执行这个页面了
在第一次执行完成后,页面显示

OnActionExecuting
Action

这证明是先执行了OnActionExecuting然后执行了Action,我们再刷新一下页面
则得到

OnActionExecuting
Action
OnActionExecuted
OnActionExecuting
Action

这是因为OnActionExecuted是在第一次页面 显示后才执行,所以要到第二次访问页面时才能看到
Controller的Filter
Monorail中的Filter是可以使用在Controller中的,这给编程者带来了很多方便,那么在ASP.NET MVC中可不可以使用Controller级的Filter呢.不言自喻.
实现方法如下Controller本身也有OnActionExecuting与OnActionExecuted方法 ,将之重写即可,见代码

namespace MvcApplication2.Controllers
{
    
using System.Web.Mvc;
    
public class EiceController : Controller
    
{
        
public void Index() {        this.HttpContext.Session["temp"+= "Action<br/>";

            RenderView(
"Index");
        }

        
public void Index2() {
            
            RenderView(
"Index");
        }

        
protected override void OnActionExecuting(FilterExecutingContext
           filterContext) 
{
            filterContext.HttpContext.Session[
"temp"+= "OnActionExecuting<br/>";
        }


        
protected override void OnActionExecuted(FilterExecutedContext
            filterContext) 
{
            filterContext.HttpContext.Session[
"temp"+= "OnActionExecuted<br/>";
        }

    }

}

这里要注意一点,这两个方法 一定要protected
要是想多个Controller使用这个Filter怎么办?继承呗.
Filter的具体生存周期
这是官方站的一数据.

  1. 来自controller虚方法 的OnActionExecuting .

  2. 应用于当前Controller的Filter中的OnActionExecuting:

    先执行基类的,后执派生类的

  3. 执行应用于Action的Filter的OnActionExecuting顺序:
    先执行基类的,后执派生类的

  4. Action 方法

  5. 应用于Action的Filter的OnActionExecuted 的执行顺序

    先执行派生类的,后执行基类的

  6. 应用于当前Controller的Filter中的OnActionExecuted方法

    先执行派生类的,后执行基类的

  7. Controller中的虚方法 OnActionExecuted

示例可见http://quickstarts.asp.net/3-5-extensions/mvc/ActionFiltering.aspx
Asp.net Mvc Framework 系列

[MVC]ASP.NET MVC Tip #31: 给 Master Pages 和 User Co

mikel阅读(694)

原文地址:ASP.NET MVC Tip #31 – Passing Data to Master Pages and User Controls
原文作者:swalther

本文译者:QLeelulu

摘要:
在 这个Tip中,我会讨论给MasterPages和UserControls传递数据的4种策略。我会讲解通过code-behind类、通过使用 ActionFilter、通过调用局部方法、和通过使用抽象的Controller基类来传递数据。我推荐使用最后一种方法。

 

在这个Tip中,我推荐一种传递数据到MasterPages和UserControls的方法。但在提出我的建议前,我会先讲解一下这个问题的几种解决方法。

The Problem

想 象一下你要使用ASP.NET MVC框架来开发一个movie database application。你决定要在该应用的每一个页面上都显示一个电影分类的列表,这样,用户就可以方便的导航到他喜欢的分类。一旦你想该电影分类列表 显示在每一个页面,很自然的就会想到在MasterPage中显示这个列表。
你也决定在某些页面上显示一些热门的电影列表,但不是显示在所有的页面上。这个热门的电影列表是从数据库中随机的取出来的。你决定要通过用户控件来实现:就叫 FeaturedMovies control (见图 1).

图 1 – The Movie Database Application
clip_image002_thumb

问题就出现在这里。你需要在程序中给每一个页面的母版页传递电影分类列表数据。你需要给程序中的某些特定的页面的热门电影用户控件传递热门电影列表数据。你怎么实现这个呢?

Using a Code-Behind Class

最 通常的做法,但是是错误的,就是在code-behind class 中为你的MasterPage和FeaturedMovies用户控件取数据来解决这个问题。Listing 1 中的MasterPage显示code-behind class中的叫做Categories属性的电影分类列表。

Listing 1 – Site.Master

<%@ Master Language="C#" AutoEventWireup="true" CodeBehind="Site.master.cs" Inherits="Solution1.Views.Shared.Site" %>
<%@ Import Namespace="Solution1.Models" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<title>Movies</title>
<link href="http://www.cnblogs.com/Content/Site.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div class="page">
<div id="header">
<h1>Movie Database Application</h1>
</div>
<div id="main">
<div class="leftColumn">
<ul>
<% foreach (MovieCategory c in this.Categories)
{ %>
<li> <%= Html.ActionLink(c.Name, "Category", new {id=c.Id} )%></li>
<% } %>
</ul>
</div>
<div class="rightColumn">
<asp:ContentPlaceHolder ID="MainContent" runat="server" />
</div>
<br style="clear:both" />
<div id="footer">
Movie Database Application &copy; Copyright 2008
</div>
</div>
</div>
</body>
</html>

这个MasterPage的code-behind class在Listing 2 中。注意在Listing 2 中是直接通过LINQ2SQL来取数据的。

Listing 2 – Site.Master.cs

using System.Collections.Generic;
using System.Linq;
using Solution1.Models;
namespace Solution1.Views.Shared
{
public partial class Site : System.Web.Mvc.ViewMasterPage
{
protected IEnumerable<MovieCategory> Categories
{
get
{
var dataContext = new MovieDataContext();
return from c in dataContext.MovieCategories select c;
}
}
}
}

 

你同样可以为FeaturedMovies 用户控件来取数据。在FeaturedMovies code-behind class 从数据库中取热门电影的列表数据。

那么,为什么这错了呢?这当然好像一个简单的解决办法。它正常工作了,为什么还要抱怨?

这个解决方案的问题是MasterPage的code-behind class 中的代码是不可测试的。你不可以很方便的为Site类写单元测试,因为Site类是继承自ViewMasterPage类,而 ViewMasterPage类继承自Page类。The Page class relies on the HTTP Context object and all hope of isolating your code so that it can be tested goes away.

在开发ASP.NET MVC应用的时候,你应该尽量避免在你的程序中在code-behind class 中处理逻辑,尝试将所有的东西都放回到Controllers中。Controllers被设计为可测试的。

Using an Action Filter

所以让我们以另一种途径来解决这个传递数据给MasterPage或者view的问题。在这一节,我们创建一个ActionFilter来修改传递 给view的ViewData。这个方法你可以通过给controller添加一个或者多个action filter来修改由controller传递给view的ViewData。

这个ActionFilter,命名为[Partial] ,如Listing 3所示。

Listing 3 – ActionFilters\PartialAttribute.cs

using System;
using System.Reflection;
using System.Web.Mvc;
namespace Solution2.ActionFilters
{
public class PartialAttribute : ActionFilterAttribute
{
private string _partialClassName;
public PartialAttribute(string partialClassName)
{
_partialClassName = partialClassName;
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var viewData = (filterContext.Controller as Controller).ViewData;
ExecutePartial(_partialClassName, viewData);
}
private void ExecutePartial(string partialName, ViewDataDictionary viewData)
{
// Get partial type
var partialType = Type.GetType(partialName, true, true);
var partial = Activator.CreateInstance(partialType);
// Execute all public methods
var methods = partialType.GetMethods();
foreach (MethodInfo method in methods)
{
var pams = method.GetParameters();
if (pams.Length > 0 && pams[0].ParameterType == typeof(ViewDataDictionary))
method.Invoke(partial, new object[] { viewData });
}
}
}
}

 

当你添加[Partial] 到一个controller action的时候,这个ation filter会附加一些数据到view data中去。例如,有可以使用[Partial] 特性来添加电影分类列表的数据到view data中去以便在master page中显示。你也可以使用[Partial] 特性来添加热门电影列表到view data 中以使在FeaturedMovie 用户控件中得到该数据。

[Partial] 特性通过一个类名,实例化这个类,然后执行类里面所有的public方法(每一个方法都包含一个ViewDataDictionary参数), Listing 4 中的controller说明了你可以怎样使用[Partial] action filter来为不同的controller返回不同的ViewData。

Listing 4 – HomeController.cs

using System.Linq;
using System.Web.Mvc;
using Solution2.ActionFilters;
using Solution2.Models;
namespace Solution2.Controllers
{
[Partial("Solution2.Partials.Master")]
public class HomeController : Controller
{
[Partial("Solution2.Partials.Featured")]
public ActionResult Index()
{
return View();
}
public ActionResult Category(int id)
{
var dataContext = new MovieDataContext();
var movies = from m in dataContext.Movies where m.CategoryId == id select m;
return View("Category", movies);
}
}
}

注意到HomeController它本身是添加了[Partial] action filter的。由于[Partial] action filter应用到类上,在HomeController里面的每一个action执行的时候这个action filter都会执行的。在类级别上应用[Partial] 特性来为master page提供view data。

类级别上的[Partial]特性添加电影分类列表到view data中。[Partial]执行Solution2.Partials.Master 类中的方法,如Listing 5 所示。

Listing 5 – Master.cs

using System.Linq;
using System.Web.Mvc;
using Solution2.Models;
namespace Solution2.Partials
{
public class Master
{
public void AddViewData(ViewDataDictionary viewData)
{
var dataContext = new MovieDataContext();
var categories = from c in dataContext.MovieCategories select c;
viewData["master"] = categories;
}
}
}

 

AddViewData() 方法将categories 添加到key为"master"的view data dictionary中。master page从view data 中取出categories 并显示。

[Partial] 也可以添加到特定的action上,例如Listing 4 中的Index()方法。

然而这种从controller中传递数据给母版页和用户控件的解决方案错在哪里呢?这种方法的优于前面一种方法的地方在于他将获取数据的逻辑放回到controller中来处理了。ViewData在controller action 调用的时候会被修改。

无论怎样,这个解决方案还是挺不错的。通过使用[Partial] 特性,你可以为view data dictionary 添加更多的数据。例如,如果你决定你要添加一个新的用户控件到某一个页面,而这个新的用户控件需要一个不同的数据集,你可以很简单的添加一个新的 [Partial] 特性到正确的controller action上来添加新的数据到view data dictionary中去。

遗憾的是,只是一点点的遗憾,这个解决方案不是很容易进行单元测试。当你在一个单元测试里面执行action方法的时候ActionFilter并不会执行。所以,我们需要寻找一个不同的策略来解决这个问题。

Calling Partial Methods Directly

让我们进入到这个问题的第三个解决方案中。在这一节中,我们尝试通过直接在controller中来获取数据然后传递给母版页和用户控件来解决这个问题。在Listing 6 中是我们修改后的HomeController的代码。

Listing 6 – HomeController.cs (with partials logic)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Solution3.Models;
using Solution3.Partials;
namespace Solution3.Controllers
{
public class HomeController : Controller
{
public HomeController()
{
Master.AddViewData(this.ViewData);
}
public ActionResult Index()
{
Featured.AddViewData(this.ViewData);
return View();
}
public ActionResult Category(int id)
{
var dataContext = new MovieDataContext();
var movies = from m in dataContext.Movies where m.CategoryId == id select m;
return View("Category", movies);
}
}
}

注意到Listing 6 中的HomeController有一个构造函数。在构造函数中调Master.AddViewData() 来改变controller action中返回的view data。这个方法要在母版页中显示的view data。

Index()方法也改变了。在Index()方法里面,调用了Featured.AddViewData() 方法。这个方法为FeaturedMovies 用户控件添加必需的view data。由于FeaturedMovies 用户控件只在Index视图中呈现,而不在Categorys视图中呈现,所以不在构造函数中调用Featured.AddViewData() 方法。

这个解决方案的优点是非常容易进行单元测试。当你调用Index()方法,view data同时被Master和Featured的部分方法改变了。也就是说,你可以容易的测试你传递给母版页和用户控件的view data是否是正确的。

那么,这个解决方案错在哪里了呢?添加view data的所有逻辑都已经放到controller类中来处理了。这个解决方案已经比前面的两个方案要好很多了。这个解决方法的唯一的问题是它违背了单一责任原则。

根据单一责任原则,代码应该只有一个单一的理由去改变(code should have only a single reason to change)。然而,我们有很多原因要去改变Listing 8 中的Index()方法。如果我们也决定添加一个新的用户控件到Index视图中,而这个新的用户控件显示一个新的数据集,然后我们就需要去修改 Index() action了。

单一责任制原则背后的目的是你永远不要去改变已经在运作中的代码。改变代码通常意味着为你的应用带入一个bug。我们需要寻找一些途径来添加新的view data而不用修改我们的controller action。

Using Abstract Base Classes

这里是我对于这个问题的最后一个解决方案:我们将使用抽象的基类来改变从controller action返回来的view data。我现在要警告你这个是复杂的。我们需要创建好几个类。然而,每一个类都是单一职责的。每一个类都只是负责一种类型的view data 而已(见图2)。

Figure 2 – Class Hierarchy

clip_image004_thumb

我们将创建一个抽象基类,命名为ApplicationController ,改变view data divtionary来为我们的母版页添加所需的所有的view data(见Listing 7)。这个ApplicationController 在我们的程序中作为每一个controller的基类,而不单单是HomeController。

Listing 7 – ApplicationController

using System.Web.Mvc;
using Solution4.Partials;
namespace Solution4.Controllers
{
public abstract class ApplicationController : Controller
{
public ApplicationController()
{
Master.AddViewData(this.ViewData);
}
}
}

下一步,我们将要创建一个命名为HomeControllerBase 的抽象基类(见Listing 8).这个类包含了通常出现在HomeController的所有的应用逻辑。我们将会重写action方法来为特定的用户控件添加需要的view data。

Listing 8 – HomeControllerBase.cs

using System.Linq;
using System.Web.Mvc;
using Solution4.Models;
namespace Solution4.Controllers.Home
{
public abstract class HomeControllerBase : ApplicationController
{
public virtual ActionResult Index()
{
return View("Index");
}
public virtual ActionResult Category(int id)
{
var dataContext = new MovieDataContext();
var movies = from m in dataContext.Movies where m.CategoryId == id select m;
return View("Category", movies);
}
}
}

对于每一个用户控件,我们将会需要创建一个额外的抽象类。对于FeaturedMovies 用户控件,我们将会创建一个HomeControllerFeatured 类(见Listing 9)。对于PopularMovies 用户控件,我们将会创建一个HomeControllerPopular 类(见Listing 10)。

Listing 9 – HomeControllerFeatured.cs

using System.Web.Mvc;
namespace Solution4.Controllers.Home
{
public abstract class HomeControllerFeatured : HomeControllerBase
{
public override ActionResult Index()
{
var result = (ViewResult)base.Index();
Partials.Featured.AddViewData(result.ViewData);
return result;
}
}
}

 

Listing 10 – HomeControllerPopular.cs

using System.Web.Mvc;
namespace Solution4.Controllers.Home
{
public abstract class HomeControllerPopular : HomeControllerFeatured
{
public override System.Web.Mvc.ActionResult Category(int id)
{
var result = (ViewResult)base.Category(id);
Partials.Popular.AddViewData(result.ViewData);
return result;
}
}
}

最后,我们需要添加这个层次关系的最上面一个类。我们将会创建一个HomeController 类。这个类简单的继承自上面的其中一个基类(见Listing 11)。他本身并不包含应用逻辑。

HomeController 类这个层次关系中的唯一一个不是抽象类的。由于它不是抽象类,他的controller actions 可以被全世界调用(its controller actions can be invoked by the world)。

Listing 11 – HomeController.cs

namespace Solution4.Controllers.Home
{
public class HomeController : HomeControllerPopular
{
}
}

现在,你或许会受不了这么多的类。然而,这个解决方案的优点是我们已经很干净的分离了建造view data 的逻辑。每一个抽象类都具有单一的责任。我们的代码不再脆弱。

Summary

我并完全信服我自己的Tip。我仍然在尝试这通过使用action filter 来为我的master pages 和 user controls 添加view data。描述的最后一个解决,使用抽象基类,好像需要大量工作。我很好奇于这个问题的其他的解决方案。

[MVC]ASP.NET MVC unit Test

mikel阅读(755)

ASP.NET MVC Framework – Part 2: Testing

Published: 27 Mar 2008
By: Simone Chiaretta
Download Sample Code

In this article Simone will cover one of the main reasons for adopting the ASP.NET MVC framework: testability.

Introduction

In the first part of this series we introduced the MVC pattern and how Microsoft implemented it in the ASP.NET MVC framework. In this part we will cover one of the most important aspects of the framework: unit testing.

In the last article we saw how the MVC pattern allows a better separation of concerns and decreases the coupling between presentation logic and infrastructure code: these 2 features make it easier than before testing an ASP.NET web application.

Before looking at how to test a Controller Action let's have a brief overview of what unit testing is and why it matters.

Note: This article is based on the version of ASP.NET MVC released during the MIX08 conference at the beginning of March '08. Things may change in the future. It is also different from the version used for my previous article, so the code I showed may not work with the current code drop..

Unit testing: What it is and why it's important to do

Hundreds of books and thousands of articles have been written on this topic, but I'll try to summarize here the main concepts behind using unit testing.

From Wikipedia: "Unit testing is a procedure used to validate that individual units of source code are working properly. A unit is the smallest part of an application. […] In object-oriented programming the smallest unit is a method."

This short definition includes two important concepts:

  • Each test must be independent from the others
  • Each test should isolate a single part of the program and show that this individual part is correct

Why should we want to unit test an application? Mainly because it's an automated way to make sure it is always behaving the way it is supposed to behave. This way we can make changes to the code and refactor pieces of the application without the fear of breaking something somewhere else in the application. This is particularly useful with Agile development procedures.

One of the main concepts was that we should be able to isolate each single part of the program: this seems an easy concept in theory but can be difficult to apply. Usually a method in the Business Logic layer instantiates a class that lives in the Data Access layer. This in turn uses a data access provider to get the real data out of the database. And last, the web form that sits on top of everything uses the Business Logic passing the inputs collected from the user on the site.

Without taking the appropriate precautions, while testing the methods in the Business Logic we will test the Data Access layer and the data access provider as well. And this is against one of the main concepts of unit testing: isolation. Why is this that bad? Because if the Business Logic test failed we would not know whether the problem was really in the Business Logic, or in the procedure that retrieved the data from the DB, or maybe it was the DBA that erroneously deleted a stored procedure from the database.

The reason why we cannot isolate the part to be tested is that our Business Logic object is "dependent" on certain other objects in the Data Access layer. If we looked at how the application is designed we would see that the BL "knows" the DA through his real name at compile time (they are tightly coupled). The solution to the problem is to design the application so that the two components are loosely coupled: the BL "knows" the DA only through the interface it implements and the real dependency is defined only at runtime. This allows us to use a SQL Data Access provider in the real application and to use a fake Data Access provider that always returns the same results in our tests.

How ASP.NET MVC facilitates testing

We just saw that to easily test an application it's needed that all its components can be "faked" out, and this is achieved when the component is not just a class, but implements a well known interface or extent some kind of abstract base class.

In the first article of this series I said that the "classic" approach to developing web applications with .NET, the WebForm approach, is not easily testable: it's because the main class of ASP.NET, System.Web.UI.Page, has dependencies to HttpContext, HttpResponse, HttpRequest, HttpCookie and all the other objects coming from the fact that the Page is being executed inside a web server. And all these classes are "just" concrete classes, and cannot be replaced by fake implementations for the purpose of testing the web applications outside the context of the web server.

To address this issue, the ASP.NET MVC framework introduces a series of "abstractions" in order to remove all the direct references to the HTTP runtime. If you look at the API you will see that methods accept things like HttpContextBase, HttpRequestBase and so on. This way all the classes representing the HTTP runtime environment can be replaced by fake implementations that always return pre-baked responses instead of interacting with the web server.

From version P2 all these abstractions are located in a separate assembly called System.Web.Abstractions.dll.

But enough with theoretical talking, let's see how to test an MVC application.

Creating a test project

In the first article of the series we created a sample ASP.NET MVC web application using a project template provided with the ASP.NET 3.5 extensions: ASP.NET MVC Web Application. In version P2 they removed the ASP.NET MVC Web Application and Test project template and introduced a wizard that pops up and asks you whether you want to create a test project and which testing framework you want to use. It only supports Visual Studio Unit Test, but other testing framework can be added as well.

Let's create our test project in the File> New> Project, and selecting the ASP.NET MVC Web Application project. Notice that the description says Preview 2: if you don't see this then you are probably still running the first release. In this case you have to download and install the new version first.

Figure 1: New Project Dialog

images/fig01.jpg

After you select the ASP.NET MVC application, a new dialog window comes out.

Figure 2: Create Test Project dialog

images/fig02.jpg

Let's select Yes, generate a unit test project and choose Visual Studio Unit Test.

At this point the project template will create a solution with two projects, the first with the same sample application that we created in the first article, the second with a sample testing project, with an empty fixture for the HomeController.

Inside this class we will write tests to prove the correctness of the Controller Actions. We are going to write a test to verify that the Index action of the HomeController really asks for the view named "Index" to be rendered, and that the About action will redirect to the Index action. This is the code of the controller we are going to test:

Listing 1: Controller to be tested

  1. public class HomeController : Controller  
  2. {  
  3. public void Index()      {  
  4.          /* Call the Model here */  
  5.          RenderView("Index");  
  6.       }  
  7.   
  8. public void About()      {  
  9.          /* Call the Model here */  
  10.          RedirectToAction("Index");  
  11.       }  
  12. }  

There are a few possible approaches for testing a controller action:

  • using the Extract and override call pattern (also called Test-Specific Subclass)
  • replacing all the external dependencies with mocked objects

Extract and Override Call

This testing pattern comes from a practice used when testing legacy code that doesn't come with mockable interfaces or base classes. ASP.NET MVC is not "legacy" code and comes with mockable dependencies, but, as we will see later, the amount of code you have to write to mock everything is a bit more that one would expect, so for shorter tests I prefer this approach.

The concept behind this approach is that you extract the method that is using the dependency that you want to remove and then you override it with an implementation specific for the test you are writing.

In the application code we make calls to the RenderView and RedirectToAction methods. These ones have dependencies to a few other objects both in the ASP.NET MVC framework (IViewEngine, RouteData and ControllerContext) and in the HTTP runtime (HttpResponse and HttpContext) because in the real application they have to render a view and redirect to another URL. But for the purpose of this test we only need to store the name of the view we are going to render and invoke the method specified as the action we want to redirect to.

Listing 2: Test Specific Sub-Class

  1. internal class HomeControllerForTest : HomeController  
  2. {  
  3.    public string SelectedViewName { getprivate set; }  
  4.   
  5.    protected override void RenderView(string viewName,  
  6.                                       string masterName, object viewData)  
  7.    {  
  8.       SelectedViewName = viewName;  
  9.    }  
  10.   
  11.    protected override void RedirectToAction(  
  12.                          System.Web.Routing.RouteValueDictionary values)  
  13.    {  
  14.       string action = (string)values["action"];  
  15.       GetType().GetMethod(action).Invoke(thisnull);  
  16.    }  
  17. }  

With this test-specific class in place now we can test the controller class:

Listing 3: Testing using the test-specific sub-class

  1. [TestMethod]  
  2. public void AboutOverride()  
  3. {  
  4.    HomeControllerForTest controller = new HomeControllerForTest();  
  5.   
  6.    controller.About();  
  7.   
  8.    Assert.AreEqual("Index", controller.SelectedViewName,  
  9.                               "Should have redirected to Index");  
  10. }  
  11.   
  12. [TestMethod]  
  13. public void IndexOverride()  
  14. {  
  15.    HomeControllerForTest controller = new HomeControllerForTest();  
  16.   
  17.    controller.Index();  
  18.   
  19.    Assert.AreEqual("Index", controller.SelectedViewName,  
  20.                               "Should have selected Index");  
  21. }  

As you can see the code is pretty straightforward:

  • Instantiate the test specific class
  • Call the method we want to test
  • Assert that the Index view has been rendered

This approach works but can leave a bad taste in the mouths of some people, since here you are testing something that is different from what you are putting in the working application. Furthermore it's a trick used when testing legacy code not written with testability in mind while ASP.NET MVC is a new library written to be easily testable. A better and more elegant approach is the one that involves mocking dependencies.

Mock Objects

As we said earlier, everything that interacts with a controller is referenced through its interface so we can make our own implementation of the ViewEngine in order to test that the view that the controller wants to render is the one we are expecting.

Listing 4: FakeViewEngine

  1. public class FakeViewEngine : IViewEngine  
  2. {  
  3.     public ViewContext ViewContext { getprivate set; }  
  4.   
  5.     public void RenderView(ViewContext viewContext)  
  6.    {  
  7.       ViewContext = viewContext;  
  8.    }  
  9. }  

The only method that is specified inside the IViewEngine interface is RenderView. A real View Engine should select the view to be rendered based on the ViewContext passed, but for the purpose of this test we just need to store the ViewContext so that we can check that the ViewName is the one we are expecting.

But there is another step to be taken: even if we are not dealing with HttpContext and ControllerContext directly in our code, the framework needs this information in order to populate the ViewContext that is passed to the View Engine. So we have to mock them out.

I'll not discuss about what a mock object is in detail but here is a short definition: mock objects are objects that mimic the behavior of real objects and can also do some assertions on how methods are called. In the references you'll find some links to other articles on mocking, as well as on unit testing.

Let's have a look at how to test the Index controller action using mock objects. There are a few mocking framework around: I'm going to use RhinoMocks, but the same concepts apply to all of them.

Listing 5: Testing the Index action with RhinoMocks

  1. [TestMethod]  
  2. public void IndexMock()  
  3. {  
  4.    HomeController controller = new HomeController();  
  5.    var fakeViewEngine = new FakeViewEngine();  
  6.    controller.ViewEngine = fakeViewEngine;  
  7.   
  8.    MockRepository mocks = new MockRepository();  
  9.    using (mocks.Record())  
  10.    {  
  11.       mocks.SetFakeControllerContext(controller);  
  12.    }  
  13.    using (mocks.Playback())  
  14.    {  
  15.       controller.Index();  
  16.       Assert.AreEqual("Index", fakeViewEngine.ViewContext.ViewName,  
  17.                           "Should have selected Index");  
  18.    }  
  19. }  

First we created an instance of our controller class and set the view engine to use the FakeViewEngine we defined earlier.

Then RhinoMocks comes in:

  • We start setting expectations (mocks.Record).
  • We create a fake controller Context (We'll talk about this in a moment).

Then we put the mock repository in playback mode, we call the method we want to test and we assert that the ViewName that has been passed to the View Engine is the one we were expecting.

The SetFakeControllerContext method is a helper method that creates a fake HttpContext using RhinoMocks and then populates a controller context with all the other information needed by the framework code.

Listing 6: SetFakeControllerContext helper method

  1. public static void SetFakeControllerContext(this MockRepository mocks,  
  2.                                             Controller controller)  
  3. {  
  4.    var httpContext = mocks.FakeHttpContext();  
  5.    ControllerContext context = new ControllerContext(new RequestContext(httpContext,   
  6.                                                       new RouteData()), controller);  
  7.    controller.ControllerContext = context;  
  8. }  

This method, as well as other helper methods, is contained in the MvcMockHelpers class created by the ASP.NET MVC team.

In the code available for download there is a complete solution that includes a working sample of what we discussed here, and the testing method for the About action as well. The RedirectToAction method is still too coupled with the HTTP Runtime and in my opinion it still requires too much mocking. This is probably going to change in the next release so I prefer not writing the code here in the article. If you are curious have a look at the code.

Conclusions

In this article we introduced the problems related to unit testing and we saw how the ASP.NET MVC framework facilitates unit testing. We also saw how to test Controller Actions both using the Extract and Override Call approach and using Mock objects.

In the next article we will see how to pass data from the controller to the view and vice versa.

References

[MVC]ASP.NET MVC controller actions unit Test

mikel阅读(850)

This is how ASP.NET MVC controller actions should be unit tested

I'm upgrading my partywithpalermo.com website for the MVP Summit party, and I'm retrofitting my tests to use the March CTP of the MVC Framework.  I have the following action that I need to unit test:

public class MainController : ControllerBase
{
    private readonly IAttendeesRepository _repository;
 
    public MainController(IAttendeesRepository repository, IViewEngine viewEngine)
    {
        _repository = repository;
        ViewEngine = viewEngine;
    }
 
 
    public void Register(string name, string website, string comment)
    {
        var attendee = new Attendee(name, website, comment);
        _repository.SaveNewAttendee(attendee);
        RenderView("confirm", attendee);
    }
}

 

Note the explicit dependencies on IAttendeeRepository and IViewEngine.  That means that I'll be interacting with those two dependencies in this controller.  Here is my unit test (this passes, by the way):

 

[Test]
public void ShouldSaveAttendee()
{
    var repository = new FakeRepository();
 
    var mockViewEngine = new MockViewEngine();
    var controller = new MainController(repository, mockViewEngine);
    controller.ControllerContext = new ControllerContext(new RequestContext(new HttpContextStub(), new RouteData()), controller);
    controller.Register("Jeffrey.',", "http://www.jeffreypalermo.com?=&@%20", "this comment!?,'.");
 
    Attendee attendee = repository.SavedAttendee;
    Assert.That(attendee.Name, Is.EqualTo("Jeffrey.',"));
    Assert.That(attendee.Website, Is.EqualTo("http://www.jeffreypalermo.com?=&@%20"));
    Assert.That(attendee.Comment, Is.EqualTo("this comment!?,'."));
 
    Assert.That(mockViewEngine.ActualViewContext.ViewName, Is.EqualTo("confirm"));
    Assert.That(mockViewEngine.ActualViewContext.ViewData, Is.EqualTo(attendee));
}
 
private class HttpContextStub : HttpContextBase
{
 
}

 

This is a pretty straightforward unit test except for the line before calling the Register() method.  I have to use setter-injection to set a stubbed ControllerContext.  If I don't, the Register() method will bomb with an exception when the ViewContext is created, and the code tries to get an HttpContextBase off of the ControllerContext.  It'll throw a NullReferenceException.  My code doesn't really care about the ControllerContext or what is in it, but because of how the code is structured, I must use setter injection to break this dependency.  Note that testability is next up on the list of things to do for the MVC Framework team.

Preview2 (March CTP) was all about getting the routing engine out into its own assembly so that it can be used separate from MVC controllers.  Also, the MVC Framework is "binnable".  You can xcopy deploy it.  There is plenty of time to fix these things, and the team is working on it.  You can also be sure that I'll keep raising the issue because I've been test-driving code for three years, and it's instinct now to see what is easy and frictionless and separate it from code that is harder to test than it should be.  Overall, pretty good for a CTP.

 

The following is what I would like to write.  The following is how I would like my test to look.  Notice just the absence of the ControllerContext line.

[TestFixture]
public class MainControllerTester
{
    [Test]
    public void ShouldSaveAttendee()
    {
        var repository = new FakeRepository();
 
        var mockViewEngine = new MockViewEngine();
        var controller = new MainController(repository, mockViewEngine);
        controller.Register("Jeffrey", "http://www.jeffreypalermo.com", "this comment");
 
        Attendee attendee = repository.SavedAttendee;
        Assert.That(attendee.Name, Is.EqualTo("Jeffrey"));
        Assert.That(attendee.Website, Is.EqualTo("http://www.jeffreypalermo.com"));
        Assert.That(attendee.Comment, Is.EqualTo("this comment"));
 
        Assert.That(mockViewEngine.ActualViewContext.ViewName, Is.EqualTo("confirm"));
        Assert.That(mockViewEngine.ActualViewContext.ViewData, Is.EqualTo(attendee));
    }
 
    private class MockViewEngine : IViewEngine
    {
        public ViewContext ActualViewContext;
 
        public void RenderView(ViewContext viewContext)
        {
            ActualViewContext = viewContext;
        }
    }
 
    private class FakeRepository : IAttendeesRepository
    {
        public Attendee SavedAttendee;
 
        public IEnumerable<Attendee> GetAttendees()
        {
            throw new NotImplementedException();
        }
 
        public void SaveNewAttendee(Attendee attendee)
        {
            SavedAttendee = attendee;
        }
    }
}

 

Note that my unit test is concerned with explicit dependencies and doesn't know or care that I'm making use of a PROTECTED method named "RenderView" inside my action.  That detail doesn't matter because the interaction with the IViewEngine is what is important. 

I also understand that I could use the Legacy code pattern of "Extract and Override Call" (Feathers, p348).  Microsoft has already provided the extracted method, RenderView.  I can override the call to break the dependency, but that pattern is meant to be used as a dependency-breaking technique with legacy code.  If you haven't read Michael Feathers' book, Working Effectively with Legacy Code, you should order it right now.  It talks about all kinds of dependency-breaking techniques in order to write unit tests on existing code.  My goal in providing feedback to the MVC Framework team (and I provide feedback, believe me) is to have this framework be something that I would want to use.  There is plenty of time to make the necessary changes, and the team is working hard.

I revised this post a bit after chatting on the phone with Scott Guthrie this evening.  We'll see some testability improvements in the next drop, and the team is aiming to make drops around every 6 weeks or so.

Note:  I'm playing around with using "var" for locals.  Not sure if I like it yet.  We'll see.  No need to comment on the "vars".  Comment on controllers, actions, and unit tests.

[MVC]ASP.NET MVC Action Filter - 缓存与压缩

mikel阅读(742)

原文地址: ASP.NET MVC Action Filter – Caching and Compression

下载源码: Source.zip

关于Action Filter你可以参考我的另外一篇文章: ASP.NET MVC : Action过滤器(Filtering)

 

缓 存在开发高扩充性WEB程序的时候扮演着很重要的角色.我们可以将HTTP请求在一个定义的时间内缓存在用户的浏览器中,如果用户在定义的时间内请求同一 个URL,那么用户的请求将会从用户浏览器的缓存中加载,而不是从服务器.你可以在ASP.NET MVC应用程序中使用下面的Action Filter来实现同样的事情:

using System;
using System.Web;
using System.Web.Mvc;
public class CacheFilterAttribute : ActionFilterAttribute
{
/// <summary>
/// Gets or sets the cache duration in seconds. The default is 10 seconds.
/// </summary>
/// <value>The cache duration in seconds.</value>
public int Duration
{
get;
set;
}
public CacheFilterAttribute()
{
Duration = 10;
}
public override void OnActionExecuted(FilterExecutedContext filterContext)
{
if (Duration <= 0) return;
HttpCachePolicyBase cache = filterContext.HttpContext.Response.Cache;
TimeSpan cacheDuration = TimeSpan.FromSeconds(Duration);
cache.SetCacheability(HttpCacheability.Public);
cache.SetExpires(DateTime.Now.Add(cacheDuration));
cache.SetMaxAge(cacheDuration);
cache.AppendCacheExtension("must-revalidate, proxy-revalidate");
}
}

你可以好像下面一样在你的Controller Action 方法中使用这个Filter :

[CacheFilter(Duration = 60)]
public void Category(string name, int? page)

下面是在firebug中当 缓存Filter 没有应用的时候的截图 :

NoCache

下面的截图是应用了 Cache Filter 时候的截图 :

Cache

 

另外一个很重要的事情就是压缩.现在的浏览器都可以接收压缩后的内容,这可以节省大量的带宽.你可以在你的ASP.NET MVC 程序中应用下面的Action Filter 来压缩你的Response :

using System.Web;
using System.Web.Mvc;
public class CompressFilter : ActionFilterAttribute
{
public override void OnActionExecuting(FilterExecutingContext filterContext)
{
HttpRequestBase request = filterContext.HttpContext.Request;
string acceptEncoding = request.Headers["Accept-Encoding"];
if (string.IsNullOrEmpty(acceptEncoding)) return;
acceptEncoding = acceptEncoding.ToUpperInvariant();
HttpResponseBase response = filterContext.HttpContext.Response;
if (acceptEncoding.Contains("GZIP"))
{
response.AppendHeader("Content-encoding", "gzip");
response.Filter = new GZipStream(response.Filter, CompressionMode.Compress);
}
else if (acceptEncoding.Contains("DEFLATE"))
{
response.AppendHeader("Content-encoding", "deflate");
response.Filter = new DeflateStream(response.Filter, CompressionMode.Compress);
}
}
}

然后将这个Filter应用到你的Controller Action 中 :

[CompressFilter]
public void Category(string name, int? page)

下面是没有应用压缩的时候的截图 :

Uncompressed

下面的截图是应用了压缩Filter后的情形 :

Compressed

你当然也可以将这两个Filter都应用到同一个Action方法上,就好像下面所示 :

[CompressFilter(Order = 1)]
[CacheFilter(Duration = 60, order = 2)]
public void Category(string name, int? page)

下面是截图 :

Both

Enjoy!!!

下载源码: Source.zip

[DES]DES加密原理

mikel阅读(670)

DES加密算法的原理. (转)
一.本文實用於初學者,目的在於幫助大家熟悉一些系統底層的知識。
二.本文只是為了讓廣大網友共同提高一些基礎知識,本人決無賣弄之意,只供需要這方面知識的讀者閱讀,如果你是高手,或者不需要這方面知識,請跳過。
三.本文是一篇翻譯文章,如有雷同,敬請諒解。
四.本文歡迎傳抄轉載,但是不要用於任何商業用途。請尊重作者勞動,也歡迎來信交流 fnlq@263.net

 

【正文】

DES算法理論

本 世紀五十年代以來,密碼學研究領域出現了最具代表性的兩大成就。其中之一就是1971年美國學者塔奇曼 (Tuchman)和麥耶(Meyer)根據信息論創始人香農(Shannon)提出的「多重加密有效性理論」創立的,後於1977年由美國國家標準局頒 布的數據加密標準。
DES密碼實際上是Lucifer密碼的進一步發展。它是一種採用傳統加密方法的區組密碼。

它的算法是對稱的,既可用於加密又可用於解密。

美國國家標準局1973年開始研究除國防部外的其它部門的計算機系統的數據加密標準,於1973年5月15日和1974年8月27日先後兩次向公眾發出了徵求加密算法的公告。 加密算法要達到的目的通常稱為DES密碼算法要求主要為以下四點:

提 供高質量的數據保護,防止數據未經授權的洩露和未被察覺的修改;具有相當高的複雜性,使得破譯的開銷超過可能獲得的利益,同時又要便於理解和掌握 DES密碼體制的安全性應該不依賴於算法的保密,其安全性僅以加密密鑰的保密為基礎實現經濟,運行有效,並且適用於多種完全不同的應用。

1977年1月,美國政府頒布:採納IBM公司設計的方案作為非機密數據的正式數據加密標準(DES棗Data Encryption Standard)。

   目前在這裡,隨著三金工程尤其是金卡工程的啟動,DES算法在POS、ATM、磁卡及智能卡(IC卡)、加油站、高速公路收費站等領域被廣泛應用,以此 來實現關鍵數據的保密,如信用卡持卡人的PIN的加密傳輸,IC卡與POS間的雙向認證、金融交易數據包的MAC校驗等,均用到DES算法。

  DES算法的入口參數有三個:Key、Data、Mode。其中Key為8個字節共64位,是DES算法的工作密鑰;Data也為8個字節64位,是要被加密或被解密的數據;Mode為DES的工作方式,有兩種:加密或解密。

   DES算法是這樣工作的:如Mode為加密,則用Key 去把數據Data進行加密, 生成Data的密碼形式(64位)作為DES的輸出結果;如Mode為解密,則用Key去把密碼形式的數據Data解密,還原為Data的明碼形式(64 位)作為DES的輸出結果。在通信網絡的兩端,雙方約定一致的Key,在通信的源點用Key對核心數據進行DES加密,然後以密碼形式在公共通信網(如電 話網)中傳輸到通信網絡的終點,數據到達目的地後,用同樣的Key對密碼數據進行解密,便再現了明碼形式的核心數據。這樣,便保證了核心數據(如PIN、 MAC等)在公共通信網中傳輸的安全性和可靠性。

  通過定期在通信網絡的源端和目的端同時改用新的Key,便能更進一步提高數據的保密性,這正是現在金融交易網絡的流行做法。

  DES算法詳述

  DES算法把64位的明文輸入塊變為64位的密文輸出塊,它所使用的密鑰也是64位,其功能是把輸入的64位數據塊按位重新組合,並把輸出分為L0、R0兩部分,每部分各長32位,其置換規則見下表:

58,50,12,34,26,18,10,2,60,52,44,36,28,20,12,4,
  62,54,46,38,30,22,14,6,64,56,48,40,32,24,16,8,
  57,49,41,33,25,17, 9,1,59,51,43,35,27,19,11,3,
  61,53,45,37,29,21,13,5,63,55,47,39,31,23,15,7,

  即將輸入的第58位換到第一位,第50位換到第2位,…,依此類推,最後一位是原來的第7位。L0、R0則是換位輸出後的兩部分,L0是輸出的左 32位,R0 是右32位,例:設置換前的輸入值為D1D2D3……D64,則經過初始置換後的結果 為:L0=D550…D8;R0=D57D49…D7。

  經過26次迭代運算後。得到L16、R16,將此作為輸入,進行逆置換,即得到密文輸出。逆置換正好是初始置的逆運算,例如,第1位經過初始置換後,處於第40位,而通過逆置換,又將第40位換回到第1位,其逆置換規則如下表所示:

  40,8,48,16,56,24,64,32,39,7,47,15,55,23,63,31,
  38,6,46,14,54,22,62,30,37,5,45,13,53,21,61,29,
  36,4,44,12,52,20,60,28,35,3,43,11,51,19,59,27,
  34,2,42,10,50,18,58 26,33,1,41, 9,49,17,57,25,

放大換位表

  32, 1, 2, 3, 4, 5, 4, 5, 6, 7, 8, 9, 8, 9, 10,11,
  12,13,12,13,14,15,16,17,16,17,18,19,20,21,20,21,
  22,23,24,25,24,25,26,27,28,29,28,29,30,31,32, 1,

單純換位表

  16,7,20,21,29,12,28,17, 1,15,23,26, 5,18,31,10,
  2,8,24,14,32,27, 3, 9,19,13,30, 6,22,11, 4,25,

  在f(Ri,Ki)算法描述圖中,S1,S2…S8為選擇函數,其功能是把6bit數據變為4bit數據。下面給出選擇函數Si(i=1,2……8)的功能表:
選擇函數Si

S1:
  14,4,13,1,2,15,11,8,3,10,6,12,5,9,0,7,
  0,15,7,4,14,2,13,1,10,6,12,11,9,5,3,8,
  4,1,14,8,13,6,2,11,15,12,9,7,3,10,5,0,
  15,12,8,2,4,9,1,7,5,11,3,14,10,0,6,13,
S2:
  15,1,8,14,6,11,3,4,9,7,2,13,12,0,5,10,
  3,13,4,7,15,2,8,14,12,0,1,10,6,9,11,5,
  0,14,7,11,10,4,13,1,5,8,12,6,9,3,2,15,
  13,8,10,1,3,15,4,2,11,6,7,12,0,5,14,9,
S3:
  10,0,9,14,6,3,15,5,1,13,12,7,11,4,2,8,
  13,7,0,9,3,4,6,10,2,8,5,14,12,11,15,1,
  13,6,4,9,8,15,3,0,11,1,2,12,5,10,14,7,
  1,10,13,0,6,9,8,7,4,15,14,3,11,5,2,12,
S4:
  7,13,14,3,0,6,9,10,1,2,8,5,11,12,4,15,
  13,8,11,5,6,15,0,3,4,7,2,12,1,10,14,9,
  10,6,9,0,12,11,7,13,15,1,3,14,5,2,8,4,
  3,15,0,6,10,1,13,8,9,4,5,11,12,7,2,14,
S5:
  2,12,4,1,7,10,11,6,8,5,3,15,13,0,14,9,
  14,11,2,12,4,7,13,1,5,0,15,10,3,9,8,6,
  4,2,1,11,10,13,7,8,15,9,12,5,6,3,0,14,
  11,8,12,7,1,14,2,13,6,15,0,9,10,4,5,3,
S6:
  12,1,10,15,9,2,6,8,0,13,3,4,14,7,5,11,
  10,15,4,2,7,12,9,5,6,1,13,14,0,11,3,8,
  9,14,15,5,2,8,12,3,7,0,4,10,1,13,11,6,
  4,3,2,12,9,5,15,10,11,14,1,7,6,0,8,13,
S7:
  4,11,2,14,15,0,8,13,3,12,9,7,5,10,6,1,
  13,0,11,7,4,9,1,10,14,3,5,12,2,15,8,6,
  1,4,11,13,12,3,7,14,10,15,6,8,0,5,9,2,
  6,11,13,8,1,4,10,7,9,5,0,15,14,2,3,12,
S8:
  13,2,8,4,6,15,11,1,10,9,3,14,5,0,12,7,
  1,15,13,8,10,3,7,4,12,5,6,11,0,14,9,2,
  7,11,4,1,9,12,14,2,0,6,10,13,15,3,5,8,
  2,1,14,7,4,10,8,13,15,12,9,0,3,5,6,11,

在此以S1為例說明其功能,我們可以看到:在S1中,共有4行數據,命名為0,1、2、3行;每行有16列,命名為0、1、2、3,……,14、15列。

  現設輸入為: D=D1D2D3D4D5D6
令:列=D2D3D4D5
  行=D1D6

  然後在S1表中查得對應的數,以4位二進製表示,此即為選擇函數S1的輸出。下面給出子密鑰Ki(48bit)的生成算法

   從子密鑰Ki的生成算法描述圖中我們可以看到:初始Key值為64位,但DES算法規定,其中第8、16、……64位是奇偶校驗位,不參與 DES運算。故Key 實際可用位數便只有56位。即:經過縮小選擇換位表1的變換後,Key 的位數由64 位變成了56位,此56位分為C0、D0兩部分,各28位,然後分別進行第1次循環左移,得到C1、D1,將C1(28位)、D1(28位)合併得到56 位,再經過縮小選擇換位2,從而便得到了密鑰K0(48位)。依此類推,便可得到K1、K2、……、K15,不過需要注意的是,16次循環左移對 應的左移位數要依據下述規則進行:

循環左移位數
1,1,2,2,2,2,2,2,1,2,2,2,2,2,2,1
  以上介紹了DES算法的加密過程。DES算法的解密過程是一樣的,區別僅僅在於第一次迭代時用子密鑰K15,第二次K14、……,最後一次用K0,算法本身並沒有任何變化。
DES算法具有極高安全性,到目前為止,除了用窮舉搜索法對DES算法進行攻擊外,還沒有發現更有效的辦法。而56位長的密鑰的窮舉空間為256,這意味 著如果一台計算機的速度是每一秒種檢測一百萬個密鑰,則它搜索完全部密鑰就需要將近2285年的時間,可見,這是難以實現的,當然,隨著科學技術的發展, 當出現超高速計算機後,我們可考慮把DES密鑰的長度再增長一些,以此來達到更高的保密程度。

   由上述DES算法介紹我們可以看到:DES算法中只用到64位密鑰中的其中56位,而第8、16、24、……64位8個位並未參與DES運算, 這一點,向我們提出了一個應用上的要求,即DES的安全性是基於除了8,16,24,……64位外的其餘56位的組合變化256才得以保證的。因 此,在實際應用中,我們應避開使用第8,16,24,……64位作為有效數據位,而使用其它的56位作為有效數據位,才能保證DES算法安全可靠 地發揮作用。如果不瞭解這一點,把密鑰Key的8,16,24,….. .64位作為有效數據使用,將不能保證DES加密數據的安全性,對運用DES來達到保密作用的系統產生數據被破譯的危險,這正是DES算法在應用上的誤 區,是各級技術人員、各級領導在使用過程中應絕對避免的,而當今各金融部門及非金融部門,在運用DES工作,掌握DES工作密鑰Key的領導、主管們,極 易忽略,給使用中貌似安全的系統,留下了被人攻擊、被人破譯的極大隱患。
DES算法應用誤區的驗證數據

  筆者用Turbo C編寫了DES算法程序,並在PC機上對上述的DES 算法的應用誤區進行了騅,其驗證數據如下:

Key: 0x30 0x30 0x30 0x30……0x30(8個字節)
Data: 0x31 0x31 0x31 0x31……0x31(8個字節)
Mode: Encryption
結果:65 5e a6 28 cf 62 58 5f

   如果把上述的Key換為8個字節的0x31,而Data和Mode均不變,則執行DES 後得到的密文完全一樣。類似地,用Key:8個0x32和用Key:8個0x33 去加密Data (8 個0x31),二者的圖文輸出也是相同的:5e c3 ac e9 53 71 3b ba
我們可以得到出結論:
Key用0x30與用0x31是一樣的;
Key用0x32與用0x33是一樣的,……

  當Key由8個0x32換成8個0x31後,貌似換成了新的Key,但由於0x30和0x31僅僅是在第8,16,24……64有變化,而DES算法並不使用Key的第8,16,……64位作為Key的有效數據位,故:加密出的結果是一樣的。
DES解密的驗證數據:

Key: 0x31 0x31……0x31(8個0x31)
Data: 65 5e a6 28 cf 62 58 5f
Mode: Decryption
結果:0x31 0x31……0x31(8個0x31)

  由以上看出:DES算法加密與解密均工作正確。唯一需要避免的是:在應用中,避開使用Key的第8,16……64位作為有效數據位,從而便避開了DES 算法在應用中的誤區。
避開DES算法應用誤區的具體操作

   在DES密鑰Key的使用、管理及密鑰更換的過程中,應絕對避開DES 算法的應用誤區,即:絕對不能把Key的第8,16,24……64位作為有效數據位,來對Key 進行管理。這一點,特別推薦給金融銀行界及非金融業界的領導及決策者們,尤其是負責管理密鑰的人,要對此點予以高度重視。有的銀行金融交易網絡,利用定期 更換DES密鑰Key的辦法來進一步提高系統的安全性和可靠性,如果忽略了上述應用誤區,那麼,更換新密鑰將是徒勞的,對金融交易網絡的安全運行將是十分 危險的,所以更換密鑰一定要保證新Key與舊Key真正的不同,即除了第8,16,24,…64位外其它位數據發生了變化,請務必對此保持高度重視!