Trước tiên, bạn không nên sử dụng bất kỳ đối tượng miền trong quan điểm của bạn. Bạn nên sử dụng mô hình xem. Mỗi mô hình khung nhìn sẽ chỉ chứa các thuộc tính được yêu cầu bởi chế độ xem đã cho cũng như các thuộc tính xác thực cụ thể cho chế độ xem đã cho này. Vì vậy, nếu bạn có 3 bước thuật sĩ, điều này có nghĩa là bạn sẽ có 3 mô hình xem, mỗi mô hình cho mỗi bước:
public class Step1ViewModel
{
[Required]
public string SomeProperty { get; set; }
...
}
public class Step2ViewModel
{
[Required]
public string SomeOtherProperty { get; set; }
...
}
và như thế. Tất cả các mô hình khung nhìn này có thể được hỗ trợ bởi mô hình khung nhìn trình hướng dẫn chính:
public class WizardViewModel
{
public Step1ViewModel Step1 { get; set; }
public Step2ViewModel Step2 { get; set; }
...
}
sau đó bạn có thể có các hành động điều khiển hiển thị từng bước của quy trình trình hướng dẫn và chuyển chính WizardViewModel
cho chế độ xem. Khi bạn đang ở bước đầu tiên bên trong hành động của bộ điều khiển, bạn có thể khởi tạo thuộc Step1
tính. Sau đó, trong chế độ xem, bạn sẽ tạo biểu mẫu cho phép người dùng điền vào các thuộc tính về bước 1. Khi biểu mẫu được gửi, hành động của bộ điều khiển sẽ chỉ áp dụng quy tắc xác thực cho bước 1:
[HttpPost]
public ActionResult Step1(Step1ViewModel step1)
{
var model = new WizardViewModel
{
Step1 = step1
};
if (!ModelState.IsValid)
{
return View(model);
}
return View("Step2", model);
}
Bây giờ trong chế độ xem bước 2, bạn có thể sử dụng trình trợ giúp Html.Serialize từ tương lai MVC để tuần tự hóa bước 1 vào một trường ẩn bên trong biểu mẫu (sắp xếp ViewState nếu bạn muốn):
@using (Html.BeginForm("Step2", "Wizard"))
{
@Html.Serialize("Step1", Model.Step1)
@Html.EditorFor(x => x.Step2)
...
}
và bên trong hành động POST của bước 2:
[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);
}
Và cứ như vậy cho đến khi bạn đến bước cuối cùng, nơi bạn sẽ có WizardViewModel
đầy đủ tất cả dữ liệu. Sau đó, bạn sẽ ánh xạ mô hình xem vào mô hình miền của bạn và chuyển nó đến lớp dịch vụ để xử lý. Lớp dịch vụ có thể tự thực hiện bất kỳ quy tắc xác thực nào và v.v.
Ngoài ra còn có một cách khác: sử dụng javascript và đặt tất cả trên cùng một trang. Có rất nhiều plugin jquery cung cấp chức năng thuật sĩ ( Stepy là một plugin đẹp). Về cơ bản, đây là vấn đề hiển thị và ẩn div trên máy khách trong trường hợp bạn không còn phải lo lắng về trạng thái tồn tại giữa các bước.
Nhưng bất kể giải pháp nào bạn chọn luôn luôn sử dụng các mô hình xem và thực hiện xác nhận trên các mô hình xem đó. Chừng nào bạn còn gắn các thuộc tính xác thực chú thích dữ liệu trên các mô hình miền của mình, bạn sẽ phải vật lộn rất nhiều vì các mô hình miền không được điều chỉnh theo các khung nhìn.
CẬP NHẬT:
OK, do nhiều ý kiến tôi rút ra kết luận rằng câu trả lời của tôi không rõ ràng. Và tôi phải đồng ý. Vì vậy, hãy để tôi cố gắng xây dựng thêm ví dụ của tôi.
Chúng tôi có thể xác định một giao diện mà tất cả các mô hình xem bước nên thực hiện (đó chỉ là giao diện đánh dấu):
public interface IStepViewModel
{
}
sau đó chúng tôi sẽ xác định 3 bước cho trình hướng dẫn trong đó mỗi bước tất nhiên chỉ chứa các thuộc tính mà nó yêu cầu cũng như các thuộc tính xác thực có liên quan:
[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; }
}
tiếp theo, chúng tôi xác định mô hình khung nhìn trình hướng dẫn chính bao gồm danh sách các bước và chỉ mục bước hiện tại:
[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();
}
}
Sau đó, chúng tôi chuyển sang bộ điều khiển:
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);
}
}
Vài nhận xét về bộ điều khiển này:
- Hành động Index POST sử dụng các
[Deserialize]
thuộc tính từ thư viện Microsoft Futures, vì vậy hãy đảm bảo bạn đã cài đặt MvcContrib
NuGet. Đó là lý do tại sao các mô hình xem nên được trang trí với [Serializable]
thuộc tính
- Hành động Index POST lấy làm đối số một
IStepViewModel
giao diện để điều này có ý nghĩa, chúng ta cần một chất kết dính mô hình tùy chỉnh.
Đây là chất kết dính mô hình liên quan:
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;
}
}
Chất kết dính này sử dụng một trường ẩn đặc biệt có tên StepType sẽ chứa loại cụ thể của từng bước và chúng tôi sẽ gửi theo từng yêu cầu.
Chất kết dính mô hình này sẽ được đăng ký tại Application_Start
:
ModelBinders.Binders.Add(typeof(IStepViewModel), new StepViewModelBinder());
Phần còn thiếu cuối cùng của câu đố là các khung nhìn. Đây là ~/Views/Wizard/Index.cshtml
quan điểm chính :
@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" />
}
}
Và đó là tất cả những gì bạn cần để làm cho nó hoạt động. Tất nhiên, nếu bạn muốn, bạn có thể cá nhân hóa giao diện của một số hoặc tất cả các bước của trình hướng dẫn bằng cách xác định mẫu trình chỉnh sửa tùy chỉnh. Ví dụ: hãy làm điều đó cho bước 2. Vì vậy, chúng tôi xác định một ~/Views/Wizard/EditorTemplates/Step2ViewModel.cshtml
phần:
@model Step2ViewModel
Special Step 2
@Html.TextBoxFor(x => x.Bar)
Đây là cấu trúc trông như thế nào:
Tất nhiên là có chỗ để cải thiện. Hành động Index POST trông giống như s..t. Có quá nhiều mã trong đó. Một sự đơn giản hóa hơn nữa sẽ liên quan đến việc chuyển tất cả các công cụ cơ sở hạ tầng như chỉ mục, quản lý chỉ mục hiện tại, sao chép bước hiện tại vào trình hướng dẫn, ... vào một chất kết dính mô hình khác. Vì vậy, cuối cùng chúng tôi kết thúc với:
[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);
}
đó là cách các hành động POST sẽ trông như thế nào. Tôi sẽ để lại cải tiến này cho lần tiếp theo :-)