2014-06-28
在 Controller 中执行 Action 获取生成的 Html

最近需要实现一个功能,当页面上的数据发生变化时把变化之前的页面生成一个快照并保存到数据库,这样就可以在任何时候看到当时的页面内容。

比较简单的解决方法我想到2种:

  1. 把页面上的数据都保存下来,然后需要展示历史页面的时候就用这些历史数据配上当前的页面视图显示出来。

    这种方法有一个显而易见的问题:如果页面视图也发生了变化怎么办?另外一个数据一个数据的保存也很麻烦。

  2. 将整个页面的 html 保存下来。展示的时候只需要把 html 读出来就行,不需要什么额外的操作。

    这种方式看起来更简单、可行,可以说是真正的快照。一个需要注意的问题就是页面 html 的大小,如果页面太大,并且更新很频繁,那么很快就会占用数据库的大量空间。因此在保存之前进行一下数据压缩会比较好。这种用时间换空间的做法特别适用于此,因为在我们这个系统中,这些历史页面不会被经常、密集地访问。

获取页面的内容,其实就是执行一个特定 Controller 的 Action 并获取它生成的 Html 代码。如果是在一个 View 里,可以很容易的通过 HtmlHelper 的 Action 方法得到。但是如果是在一个 Controller 里呢?可以在 Controller 里用 HtmlHelper 的 Action 方法吗?答案当时可以,只要你为 HtmlHelper 提供了必要的信息——满足它的构造函数的需要。

HtmlHelper 类的构造函数需要的信息,大部分我们当前 Controller 本身就可以提供,对于一些当前 Controller 无法提供的,我们就造一个出来:

1
HtmlHelper helper = new HtmlHelper(new ViewContext(ControllerContext, new FakeView(), ViewData, TempData, System.IO.TextWriter.Null), new ViewPage());

其中HtmlHelper类的构造函数需要2个参数:ViewContextIViewDataController,对于IViewDataController,我们直接借用一个 ViewPage,因为ViewPage实现了IViewDataController。而ViewContext,我们需要继续满足它的构造函数要求。ViewContext的构造函数需要的参数中,除了IViewTextWriter,其它都可以直接利用我们当前 Controller 的ControllerContext, ViewDataTempData属性。而TextWriter,如代码所示,直接传个TextWriter.Null了事。IView 呢,我们可以构造一个空 View,就是这里的FakeView

1
2
3
4
5
6
7
public class FakeView : IView
{
public void Render(ViewContext viewContext, TextWriter writer)
{
throw new InvalidOperationException();
}
}

通过以上我们的努力,已经有了一个在 Controller 里可用的 HtmlHelper,接下来就像在 View 里一样,调用它的 Action 方法来得到需要的 Html 吧。

阅读此文

2014-05-09
为entity_framework的实体类属性添加自定义验证

ASP.NET MVC加上Entity Framework已经是微软平台下Web开发的利器。通过为实体类添加验证属性,可以非常方便的对用户输入从客户端和服务器端进行双重验证,同时这一套机制还极具扩展性,我们可以用标准方式添加我们自己的验证属性,使其行为和表现与标配的验证属性一致。下面我打算创建一个验证电话号码的验证属性。

这里其实没有什么复杂的逻辑,直接上代码会更清晰。首先我定义了一个PhoneType枚举,用来定义需要验证的电话号码类型。这里暂时只有一种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace CAF.Business.Validations
{
public enum PhoneType
{
/// <summary>
/// for example 123-456-7890
/// </summary>
Us334Dash
}
}

然后是验证属性类PhoneValidationAttribute,在定义实体类的时候就是通过这个属性(Attribute)来标识需要验证电话号码的实体类属性(Property)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;

namespace CAF.Business.Validations
{
/// <summary>
/// validate phone number, the default format is Us334Dash
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class PhoneValidationAttribute : ValidationAttribute
{
public PhoneType PhoneType { get; set; }

public PhoneValidationAttribute()
{
PhoneType = PhoneType.Us334Dash;
}

public override bool IsValid(object value)
{
if (value == null)
return true;

string phone = value.ToString();

if (phone == string.Empty)
return true;

string validationPattern = string.Empty;
switch (PhoneType)
{
case PhoneType.Us334Dash:
default:
validationPattern = @"^\d{3}-\d{3}-\d{4}$";
break;
}

Regex regex = new Regex(validationPattern);

return regex.IsMatch(phone);
}
}
}

需要说明的是,利用PhoneValidationAttribute类我们只能做到服务器端验证。那如何做到客户端验证呢?那需要一些额外的步骤。

首先,我们需要一个ModelValidator类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Mvc;
using CAF.Business.Validations;

namespace Synvata.Web.Validations
{
public class PhoneModelValidator : DataAnnotationsModelValidator<PhoneValidationAttribute>
{
private readonly PhoneType _PhoneType;
private readonly string _ErrMsg;

public PhoneModelValidator(ModelMetadata metadata, ControllerContext context, PhoneValidationAttribute attribute)
: base(metadata, context, attribute)
{
_PhoneType = attribute.PhoneType;
if (string.IsNullOrEmpty(attribute.ErrorMessage) == false)
_ErrMsg = attribute.ErrorMessage;
else
_ErrMsg = string.Format("{0} is not a valid phone number. Please enter in this format: nnn-nnn-nnnn", metadata.GetDisplayName());
}

public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
ModelClientValidationRule rule = new ModelClientValidationRule();
rule.ErrorMessage = _ErrMsg;
rule.ValidationType = "validatephone";
rule.ValidationParameters.Add("phonetype", _PhoneType.ToString());
yield return rule;
}
}
}

定义这个类的时候需要用到前面的PhoneValidationAttribute类,另外注意,定义默认的错误信息时,用到了metadata.GetDisplayName(),这样可以取到实体类属性上定义的”Display”属性(Attribute)的值。ModelClientValidationRuleValidationType属性和ValidationParameters是定义客户端jQuery Validation组件相关的验证函数和参数。

然后,我们需要在Global.asax.cs中的Application_Start方法内进行注册:

1
2
3
4
5
6
7
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(PhoneValidationAttribute), typeof(PhoneModelValidator));
}
}

最后就是添加客户端的验证函数,这里我偷懒了,没有用到phonetype参数:

1
2
3
4
5
6
7
8
9
10
$.validator.addMethod("validatephone", function (value, element, phonetype) {
if (this.optional(element))
return true;

var phoneRe = /^\d{3}-\d{3}-\d{4}$/
return phoneRe.test(value);

});

$.validator.unobtrusive.adapters.addSingleVal("validatephone", "phonetype");

这里需要2步,第一步,为jQuery Validation添加新的验证函数,第二步,把前面的验证函数注册到微软扩展的unobtrusive上,至于addSingleVal等方法的使用,可以看看源代码或者Google一下。

至此,大功告成。

阅读此文

2014-04-21
ASP.NET MVC 4项目升级到5.1后VS 2013的Razor编辑器功能不正常

本来以为可以终于开开心心地升级到 ASP.NET MVC 5.1了,谁知升级完毕之后在编辑 view 页面的时候,编辑器布满了红色波浪线,因为它无法识别各种HtmlHelper的方法了。按照说明,Visual Studio 2013 Update 1也是安装了的。经过各种尝试,耗费了无数时间,用Google把网络翻了一个底朝天,基本上断定是因为web.config配置的问题,可是不管怎么调整,都不好用。眼看就要对我的人生和理想失去信心时,突然灵机一动,也许是我改错了web.config文件,不是项目根目录的那个,而是views目录下的那个?!打开views目录下的web.config文件一看,果然如此!System.Web.WebPages.Razor引用的是2.0.0.0,System.Web.Mvc引用的是4.0.0.0。将System.Web.WebPages.Razor引用改为3.0.0.0,System.Web.Mvc引用改为5.1.0.0,保存,再重启VS 2013(也许是非必要),问题解决了~~

此一役暴露出两点问题。

  • 既然只是视图问题,并且料定是配置文件问题,我应该早点想到是views目录下的那个web.config具有最终效力。
  • 微软的升级过程还是有大大的瑕疵啊~
阅读此文