我有一个多步注册过程,由域层中的single object支持,这些对象在属性上定义了验证规则。
当域被拆分到多个视图中时,我应该如何验证域对象,并且我必须在发布时将对象部分保存在第一个视图中?
我考虑过使用Sessions,但这是不可能的,因为过程很长,数据量也很大,所以我不想使用session。
我考虑过将所有数据保存在一个关系内存中的数据库中(与主数据库具有相同的模式),然后将这些数据刷新到主数据库中,但是出现了一些问题,因为我应该在使用主数据库和内存中数据库的服务(视图中请求的)之间进行路由。
我正在寻找一种优雅干净的解决方案(更准确地说,是一种最佳实践)。
更新和澄清:
@Darin谢谢你的深思熟虑的回复,这正是我到目前为止所做的。但顺便说一句,我收到了一个有许多附件的请求,我设计了一个Step2View
,例如,用户可以异步上传其中的文档,但这些附件应该保存在一个与另一个表有引用关系的表中,该表之前应该保存在Step1View
中。
因此,我应该将域对象保存在Step1
中(部分地),但我不能,因为部分映射到Step1的ViewModel的后端核心域对象如果没有来自转换后的Step2ViewModel
的道具就不能保存。
发布于 2011-06-19 23:59:18
首先,您不应该在视图中使用任何域对象。您应该使用视图模型。每个视图模型将只包含给定视图所需的属性,以及特定于该给定视图的验证属性。因此,如果您有3个步骤的向导,这意味着您将有3个视图模型,每个步骤一个:
public class Step1ViewModel
{
[Required]
public string SomeProperty { get; set; }
...
}
public class Step2ViewModel
{
[Required]
public string SomeOtherProperty { get; set; }
...
}
诸若此类。所有这些视图模型都可以由一个主向导视图模型来支持:
public class WizardViewModel
{
public Step1ViewModel Step1 { get; set; }
public Step2ViewModel Step2 { get; set; }
...
}
然后,您可以让控制器操作呈现向导过程的每个步骤,并将主WizardViewModel
传递给视图。当您处于控制器操作的第一步时,您可以初始化Step1
属性。然后,在视图中,您将生成允许用户填写有关步骤1的属性的表单。当表单提交时,控制器操作将仅应用步骤1的验证规则:
[HttpPost]
public ActionResult Step1(Step1ViewModel step1)
{
var model = new WizardViewModel
{
Step1 = step1
};
if (!ModelState.IsValid)
{
return View(model);
}
return View("Step2", model);
}
现在,在step 2视图中,您可以使用来自MVC futures的Html.Serialize helper,以便将step 1序列化为表单中的隐藏字段(如果您愿意,可以将其作为ViewState ):
@using (Html.BeginForm("Step2", "Wizard"))
{
@Html.Serialize("Step1", Model.Step1)
@Html.EditorFor(x => x.Step2)
...
}
在step2的POST操作中:
[HttpPost]
public ActionResult Step2(Step2ViewModel step2, [Deserialize] Step1ViewModel step1)
{
var model = new WizardViewModel
{
Step1 = step1,
Step2 = step2
}
if (!ModelState.IsValid)
{
return View(model);
}
return View("Step3", model);
}
依此类推,直到您进入最后一步,在该步骤中,您将使用所有数据填充WizardViewModel
。然后,您将把视图模型映射到您的域模型,并将其传递给服务层进行处理。服务层本身可能执行任何验证规则,等等……
还有另一种选择:使用javascript并将所有内容放在同一个页面上。有很多提供向导功能的jquery plugins (Stepy就是一个很好的例子)。这基本上就是在客户端显示和隐藏div的问题,在这种情况下,您不再需要担心步骤之间的持久化状态。
但是,无论您选择哪种解决方案,始终使用视图模型,并在这些视图模型上执行验证。只要您在域模型上粘贴数据注释验证属性,就会非常困难,因为域模型无法适应视图。
更新:
好吧,由于大量的评论,我得出的结论是我的答案不清楚。我必须同意。因此,让我尝试进一步阐述我的示例。
我们可以定义一个所有步骤视图模型都应该实现的接口(它只是一个标记接口):
public interface IStepViewModel
{
}
然后,我们将为向导定义3个步骤,每个步骤当然只包含它所需的属性以及相关的验证属性:
[Serializable]
public class Step1ViewModel: IStepViewModel
{
[Required]
public string Foo { get; set; }
}
[Serializable]
public class Step2ViewModel : IStepViewModel
{
public string Bar { get; set; }
}
[Serializable]
public class Step3ViewModel : IStepViewModel
{
[Required]
public string Baz { get; set; }
}
接下来,我们定义主向导视图模型,它由步骤列表和当前步骤索引组成:
[Serializable]
public class WizardViewModel
{
public int CurrentStepIndex { get; set; }
public IList<IStepViewModel> Steps { get; set; }
public void Initialize()
{
Steps = typeof(IStepViewModel)
.Assembly
.GetTypes()
.Where(t => !t.IsAbstract && typeof(IStepViewModel).IsAssignableFrom(t))
.Select(t => (IStepViewModel)Activator.CreateInstance(t))
.ToList();
}
}
然后我们转到控制器:
public class WizardController : Controller
{
public ActionResult Index()
{
var wizard = new WizardViewModel();
wizard.Initialize();
return View(wizard);
}
[HttpPost]
public ActionResult Index(
[Deserialize] WizardViewModel wizard,
IStepViewModel step
)
{
wizard.Steps[wizard.CurrentStepIndex] = step;
if (ModelState.IsValid)
{
if (!string.IsNullOrEmpty(Request["next"]))
{
wizard.CurrentStepIndex++;
}
else if (!string.IsNullOrEmpty(Request["prev"]))
{
wizard.CurrentStepIndex--;
}
else
{
// TODO: we have finished: all the step partial
// view models have passed validation => map them
// back to the domain model and do some processing with
// the results
return Content("thanks for filling this form", "text/plain");
}
}
else if (!string.IsNullOrEmpty(Request["prev"]))
{
// Even if validation failed we allow the user to
// navigate to previous steps
wizard.CurrentStepIndex--;
}
return View(wizard);
}
}
关于这个控制器有几点备注:
[Deserialize]
属性,因此请确保已安装MvcContrib
NuGet。这就是为什么视图模型应该用attribute[Serializable]
装饰的原因,POST操作将IStepViewModel
接口作为参数,因此我们需要一个自定义模型绑定器。下面是相关的模型绑定器:
public class StepViewModelBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
var stepTypeValue = bindingContext.ValueProvider.GetValue("StepType");
var stepType = Type.GetType((string)stepTypeValue.ConvertTo(typeof(string)), true);
var step = Activator.CreateInstance(stepType);
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => step, stepType);
return step;
}
}
此绑定器使用一个名为StepType的特殊隐藏字段,它将包含每个步骤的具体类型,我们将在每个请求时发送该字段。
此模型绑定器将在Application_Start
中注册
ModelBinders.Binders.Add(typeof(IStepViewModel), new StepViewModelBinder());
最后一个缺失的部分是视图。下面是主要的~/Views/Wizard/Index.cshtml
视图:
@using Microsoft.Web.Mvc
@model WizardViewModel
@{
var currentStep = Model.Steps[Model.CurrentStepIndex];
}
<h3>Step @(Model.CurrentStepIndex + 1) out of @Model.Steps.Count</h3>
@using (Html.BeginForm())
{
@Html.Serialize("wizard", Model)
@Html.Hidden("StepType", Model.Steps[Model.CurrentStepIndex].GetType())
@Html.EditorFor(x => currentStep, null, "")
if (Model.CurrentStepIndex > 0)
{
<input type="submit" value="Previous" name="prev" />
}
if (Model.CurrentStepIndex < Model.Steps.Count - 1)
{
<input type="submit" value="Next" name="next" />
}
else
{
<input type="submit" value="Finish" name="finish" />
}
}
这就是你需要做的所有事情。当然,如果您愿意,您可以通过定义自定义编辑器模板来个性化向导的某些或所有步骤的外观。例如,让我们在步骤2中执行此操作。因此,我们定义了一个~/Views/Wizard/EditorTemplates/Step2ViewModel.cshtml
partial:
@model Step2ViewModel
Special Step 2
@Html.TextBoxFor(x => x.Bar)
下面是该结构的外观:
当然,还有改进的空间。Index POST操作看起来像s..t。里面的代码太多了。进一步的简化将涉及到移动所有基础设施的东西,如索引,当前索引管理,将当前步骤复制到向导中,...放入另一个模型活页夹中。因此,我们最终会得到:
[HttpPost]
public ActionResult Index(WizardViewModel wizard)
{
if (ModelState.IsValid)
{
// TODO: we have finished: all the step partial
// view models have passed validation => map them
// back to the domain model and do some processing with
// the results
return Content("thanks for filling this form", "text/plain");
}
return View(wizard);
}
更多的是POST操作应该是什么样子的。我把这个改进留到下一次:-)
发布于 2012-08-10 18:34:56
作为对Amit Bagga的回答的补充,您将在下面找到我所做的。即使不那么优雅,我发现这种方式比Darin的答案更简单。
控制器:
public ActionResult Step1()
{
if (Session["wizard"] != null)
{
WizardProductViewModel wiz = (WizardProductViewModel)Session["wizard"];
return View(wiz.Step1);
}
return View();
}
[HttpPost]
public ActionResult Step1(Step1ViewModel step1)
{
if (ModelState.IsValid)
{
WizardProductViewModel wiz = new WizardProductViewModel();
wiz.Step1 = step1;
//Store the wizard in session
Session["wizard"] = wiz;
return RedirectToAction("Step2");
}
return View(step1);
}
public ActionResult Step2()
{
if (Session["wizard"] != null)
{
WizardProductViewModel wiz = (WizardProductViewModel)Session["wizard"];
return View(wiz.Step2);
}
return View();
}
[HttpPost]
public ActionResult Step2(Step2ViewModel step2)
{
if (ModelState.IsValid)
{
//Pull the wizard from session
WizardProductViewModel wiz = (WizardProductViewModel)Session["wizard"];
wiz.Step2 = step2;
//Store the wizard in session
Session["wizard"] = wiz;
//return View("Step3");
return RedirectToAction("Step3");
}
return View(step2);
}
public ActionResult Step3()
{
WizardProductViewModel wiz = (WizardProductViewModel)Session["wizard"];
return View(wiz.Step3);
}
[HttpPost]
public ActionResult Step3(Step3ViewModel step3)
{
if (ModelState.IsValid)
{
//Pull the wizard from session
WizardProductViewModel wiz = (WizardProductViewModel)Session["wizard"];
wiz.Step3 = step3;
//Save the data
Product product = new Product
{
//Binding with view models
Name = wiz.Step1.Name,
ListPrice = wiz.Step2.ListPrice,
DiscontinuedDate = wiz.Step3.DiscontinuedDate
};
db.Products.Add(product);
db.SaveChanges();
return RedirectToAction("Index", "Product");
}
return View(step3);
}
型号:
[Serializable]
public class Step1ViewModel
{
[Required]
[MaxLength(20, ErrorMessage="Longueur max de 20 caractères")]
public string Name { get; set; }
}
[Serializable]
public class Step2ViewModel
{
public Decimal ListPrice { get; set; }
}
[Serializable]
public class Step3ViewModel
{
public DateTime? DiscontinuedDate { get; set; }
}
[Serializable]
public class WizardProductViewModel
{
public Step1ViewModel Step1 { get; set; }
public Step2ViewModel Step2 { get; set; }
public Step3ViewModel Step3 { get; set; }
}
发布于 2011-06-24 04:58:14
我建议您使用Jquery在客户端维护完整流程的状态。
以
为例,我们有一个三步向导流程。
这样,您可以很容易地直接从表单post数据构建您的域对象,如果数据有错误,则返回包含所有错误消息的有效JSON,并在div中显示它们。
请拆分步骤
public class Wizard
{
public Step1 Step1 {get;set;}
public Step2 Step2 {get;set;}
public Step3 Step3 {get;set;}
}
public ActionResult Step1(Step1 step)
{
if(Model.IsValid)
{
Wizard wiz = new Wizard();
wiz.Step1 = step;
//Store the Wizard in Session;
//Return the action
}
}
public ActionResult Step2(Step2 step)
{
if(Model.IsValid)
{
//Pull the Wizard From Session
wiz.Step2=step;
}
}
以上只是一个演示,它将帮助您实现最终结果。在最后一步中,您必须创建域对象,并从向导对象中填充正确的值并将其存储到数据库中。
https://stackoverflow.com/questions/6402628
复制相似问题