也许这不是我需要的解决方案,但这就是我想要做的事情:
我有一个公司注册表,每个公司都需要一个管理用户。管理用户可以管理多个公司,因此在公司注册表中,您可以从下拉列表中选择现有用户。
公司视图模型看起来像这样:
public class CompanyViewModel {
[Required]
public string Name { get; set; }
// other properties...
public UserViewModel Administrator { get; set; }
public IEnumerable<UserViewModel> AvailableUsers { get; set; }
}
,用户视图模型如下所示:
public class UserViewModel {
[Required]
public string UserName { get; set; }
[Required]
public string Password { get; set; }
// other properties...
}
在公司注册视图中:
<div><input type="radiobutton" name="chooseuser" id="existing"/>Choose an Existing User:</div.
<div>@Html.DropDownListFor(m => m.Administrator.Id, Model.AvailableUsers.Select(u => new SelectListItem { Text = string.Format("{0} - {1} {2}", u.UserName, u.FirstName, u.LastName), Value = u.Id.ToString() }), "<Choose existing user>", new { id = "existingusers" })
</div>
<div><input type="radiobutton" name="chooseuser" id="createnew"/>Create a new User:</div>
<div><label>Username:</label> @Html.EditorFor(m => m.Administrator.UserName)</div>
通过javascript,根据单选按钮选择,禁用下拉列表并显示新用户表单,或者隐藏新用户表单并启用下拉列表。
按下save后,问题出在控制器保存操作中,如果选择现有用户且表单上没有填写任何数据,则ModelState.IsValid为false。如果用户选择输入新用户,则验证成功。
处理此问题的最佳方法是什么?
一种选择是将所有用户的所有数据加载到javascript中的数据结构中,当值在现有用户下拉列表中更改时,可以填充隐藏的“创建新”表单字段。但这似乎很蹩脚,因为密码将以纯文本形式放置html。我可以变得更加漂亮,并使用ajax作为“创建新”表单,并在保存新用户后在原始表单上填充用户ID,但如果可能的话,我想将其全部保存在一个表单中。
似乎很喜欢我理想情况下能够从数据库加载现有用户数据并在控制器保存操作中填充模型状态,但手动编写此代码(甚至使用反射)似乎很草率。如果有一个内置的方法来做这件事会很好。
有什么想法吗?
感谢。
答案 0 :(得分:4)
这是一个典型的场景,它完美地说明了声明性验证(a.k.a Data Annotations)的局限性。为了处理它,您可以编写一个自定义验证属性,该属性将应用于CompanyViewModel
而不是单个属性,并允许您根据用户选择的单选按钮执行验证逻辑(顺便说一下,您需要一个视图模型上的属性,表示单选按钮选择)。模型验证器的问题在于您可能难以处理错误突出显示。
这就是我使用FluentValidation.NET而不是数据注释的原因之一。这使我可以使验证逻辑远离模型并以强制方式完成。它还允许我根据视图模型上的某些属性的值来应用条件验证器(在这种情况下,这将是单选按钮选择)。
答案 1 :(得分:0)
您可能需要考虑自定义Modelbinder。
以下是我网站上的一些示例代码 - 这是购物车结帐页面的一部分 - 用户可以输入地址,但美国StateCd
已发送,非美国StateOrProvince
已发送。因此,我们查看国家/地区并删除不适用的其他属性的任何模型错误。
我认为这与你所描述的非常相似(你有两个场景需要不同的规则,但你想使用相同的模型)。
这里的重要代码是bindingContext.ModelState.Remove(...)
,它删除模型状态并允许IsValid
返回true。
public class AddressModelBinder : DefaultModelBinder
{
protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
base.OnModelUpdated(controllerContext, bindingContext);
// get the address to validate
var address = (Address)bindingContext.Model;
// remove statecd for non-us
if (address.IsUSA)
{
address.StateOrProvince = string.IsNullOrEmpty(address.StateCd) ? null : CountryCache.GetStateName(address.StateCd);
bindingContext.ModelState.Remove(bindingContext.ModelName + ".StateOrProvince");
}
else
{
address.StateCd = null;
bindingContext.ModelState.Remove(bindingContext.ModelName + ".StateCd");
}
// update country
address.Country = CountryCache.GetCountry(address.CountryCode, true).Name;
// validate US zipcode
if (address.CountryCode == "US")
{
if (new Regex(@"^\d{5}([\-]\d{4})?$", RegexOptions.Compiled).Match(address.ZipOrPostal ?? "").Success == false)
{
bindingContext.ModelState.AddModelError(bindingContext.ModelName + ".ZipOrPostal", "The value " + address.ZipOrPostal + " is not a valid zipcode");
}
}
// all other modelbinding attributes such as [Required] will be processed as normal
}
}
注意:您需要在global.asax中注册此模型绑定器。模型绑定组件足够智能,可以让您为模型的任何部分创建不同的模型绑定器(如果它包含不同的对象)。
ModelBinders.Binders[typeof(UI.Address)] = new AddressModelBinder();
希望这会有所帮助。我认为这适用于您的情况。