ปัญหากระบวนการลงทะเบียนหลายขั้นตอนใน asp.net mvc (โมเดลมุมมองแบบแยกโมเดลเดียว)


117

ฉันมีกระบวนการลงทะเบียนหลายขั้นตอนซึ่งได้รับการสนับสนุนจากไฟล์ออบเจ็กต์เดียวในเลเยอร์โดเมนซึ่งมีกฎการตรวจสอบที่กำหนดไว้ในคุณสมบัติ

ฉันจะตรวจสอบความถูกต้องของออบเจ็กต์โดเมนอย่างไรเมื่อโดเมนถูกแบ่งออกเป็นหลายมุมมองและฉันต้องบันทึกอ็อบเจ็กต์บางส่วนในมุมมองแรกเมื่อโพสต์

ฉันคิดเกี่ยวกับการใช้เซสชัน แต่ไม่สามารถทำได้เนื่องจากกระบวนการนี้ยาวและปริมาณข้อมูลสูงดังนั้นฉันจึงไม่ต้องการใช้เซสชัน

ฉันคิดเกี่ยวกับการบันทึกข้อมูลทั้งหมดในฐานข้อมูลในหน่วยความจำเชิงสัมพันธ์ (ที่มีสคีมาเดียวกันกับฐานข้อมูลหลัก) จากนั้นล้างข้อมูลนั้นไปยังฐานข้อมูลหลัก แต่ปัญหาที่เกิดขึ้นทำให้ฉันควรกำหนดเส้นทางระหว่างบริการ (ร้องขอในมุมมอง) ที่ทำงานกับ ฐานข้อมูลหลักและฐานข้อมูลในหน่วยความจำ

ฉันกำลังมองหาโซลูชันที่สวยงามและสะอาดตา (แนวทางปฏิบัติที่ดีที่สุดอย่างแม่นยำยิ่งขึ้น)

อัปเดตและชี้แจง:

@ ดารินขอบคุณสำหรับการตอบกลับที่รอบคอบนั่นคือสิ่งที่ฉันทำจนถึงตอนนี้ แต่บังเอิญฉันได้รับคำขอที่มีไฟล์แนบจำนวนมากฉันออกแบบStep2Viewเช่นผู้ใช้สามารถอัปโหลดเอกสารในแบบอะซิงโครนัสได้ แต่ไฟล์แนบเหล่านั้นควรบันทึกไว้ในตารางที่มีความสัมพันธ์อ้างอิงกับตารางอื่นที่ควรบันทึกไว้ก่อนในStep1View .

ดังนั้นผมจึงควรบันทึกวัตถุโดเมนStep1(บางส่วน) แต่ผมไม่สามารถทำให้ได้รับการสนับสนุนหลัก Domain วัตถุซึ่งเป็นแมปบางส่วนของขั้นที่ 1 ViewModel Step2ViewModelไม่สามารถบันทึกได้โดยไม่ต้องประกอบฉากที่มาจากแปลง


@Jani คุณเคยคิดหาชิ้นส่วนที่อัปโหลดนี้หรือไม่? ฉันอยากจะเลือกสมองของคุณ ฉันกำลังดำเนินการแก้ไขปัญหานี้
Doug Chamberlain

1
วิธีแก้ปัญหาในบล็อกนี้ค่อนข้างง่ายและตรงไปตรงมา มันใช้ div เป็น "ขั้นตอน" โดยเปลี่ยนการมองเห็นและการตรวจสอบ jquery ที่ไม่สร้างความรำคาญ
Dmitry Efimenko

คำตอบ:


229

อันดับแรกคุณไม่ควรใช้วัตถุโดเมนใด ๆ ในมุมมองของคุณ คุณควรใช้โมเดลมุมมอง โมเดลมุมมองแต่ละแบบจะมีเฉพาะคุณสมบัติที่จำเป็นสำหรับมุมมองที่กำหนดตลอดจนแอ็ตทริบิวต์การตรวจสอบความถูกต้องเฉพาะสำหรับมุมมองที่กำหนดนี้ ดังนั้นหากคุณมีวิซาร์ด 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);
}

ตอนนี้ในมุมมองขั้นตอนที่ 2 คุณสามารถใช้ตัวช่วย Html.Serializeจากฟิวเจอร์ส MVC เพื่อจัดลำดับขั้นตอนที่ 1 ลงในฟิลด์ที่ซ่อนอยู่ภายในแบบฟอร์ม (ประเภทของ ViewState หากคุณต้องการ):

@using (Html.BeginForm("Step2", "Wizard"))
{
    @Html.Serialize("Step1", Model.Step1)
    @Html.EditorFor(x => x.Step2)
    ...
}

และภายในการดำเนินการ POST ของขั้นตอนที่ 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);
}

ไปเรื่อย ๆ จนกว่าคุณจะไปถึงขั้นตอนสุดท้ายที่คุณจะมีไฟล์ WizardViewModelกรอกข้อมูลทั้งหมด จากนั้นคุณจะแมปโมเดลมุมมองกับโมเดลโดเมนของคุณและส่งต่อไปยังชั้นบริการเพื่อประมวลผล ชั้นบริการอาจดำเนินการตามกฎการตรวจสอบเองและอื่น ๆ ...

นอกจากนี้ยังมีทางเลือกอื่น: ใช้จาวาสคริปต์และวางทั้งหมดไว้ในหน้าเดียวกัน มีปลั๊กอิน jqueryมากมายที่ให้ฟังก์ชันวิซาร์ด ( Stepyเป็นสิ่งที่ดี) โดยพื้นฐานแล้วเป็นเรื่องของการแสดงและซ่อน div บนไคลเอนต์ซึ่งในกรณีนี้คุณไม่จำเป็นต้องกังวลเกี่ยวกับสถานะที่คงอยู่ระหว่างขั้นตอนอีกต่อไป

แต่ไม่ว่าคุณจะเลือกโซลูชันใดให้ใช้โมเดลมุมมองและดำเนินการตรวจสอบความถูกต้องกับโมเดลมุมมองเหล่านั้นเสมอ ตราบใดที่คุณใช้แอตทริบิวต์การตรวจสอบคำอธิบายประกอบข้อมูลในแบบจำลองโดเมนของคุณคุณจะต้องดิ้นรนอย่างหนักเนื่องจากโมเดลโดเมนไม่ได้รับการปรับให้เข้ากับมุมมอง


UPDATE:

ตกลงเนื่องจากความคิดเห็นมากมายฉันสรุปได้ว่าคำตอบของฉันไม่ชัดเจน และฉันต้องยอมรับ ขอผมลองอธิบายตัวอย่างเพิ่มเติม

เราสามารถกำหนดอินเทอร์เฟซที่โมเดลมุมมองขั้นตอนทั้งหมดควรใช้ (เป็นเพียงอินเทอร์เฟซเครื่องหมาย):

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);
    }
}

ข้อสังเกตสองประการเกี่ยวกับคอนโทรลเลอร์นี้:

  • การดำเนินการ Index POST ใช้[Deserialize]แอตทริบิวต์จากไลบรารี Microsoft Futures ดังนั้นตรวจสอบให้แน่ใจว่าคุณได้ติดตั้งMvcContribNuGet แล้ว นั่นเป็นเหตุผลว่าทำไมโมเดลมุมมองจึงควรตกแต่งด้วย[Serializable]แอตทริบิวต์
  • การดำเนินการ Index 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บางส่วน:

@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 ควรมีลักษณะอย่างไร ฉันจะออกจากการปรับปรุงนี้ในครั้งต่อไป :-)


1
@ Doug Chamberlain ฉันใช้AutoMapperเพื่อแปลงระหว่างโมเดลมุมมองและโมเดลโดเมน
Darin Dimitrov

1
@ คุณ Doug Chamberlain โปรดดูคำตอบที่อัปเดตของฉัน ฉันหวังว่ามันจะทำให้ทุกอย่างชัดเจนกว่าโพสต์แรกของฉัน
Darin Dimitrov

20
+1 @Jani: คุณต้องให้ Darin 50 คะแนนสำหรับคำตอบนี้ มันครอบคลุมมาก และเขาสามารถย้ำถึงความจำเป็นในการใช้ ViewModel ไม่ใช่แบบจำลองโดเมน ;-)
Tom Chantler

3
ฉันไม่พบแอตทริบิวต์ Deserialize ทุกที่ ... นอกจากนี้ในหน้า codeplex ของ mvccontrib ฉันพบ 94fa6078a115 โดย Jeremy Skinner 1 สิงหาคม 2553 เวลา 17:55 น. 0 ลบสารยึดเกาะ Deserialize ที่เลิกใช้แล้วคุณแนะนำให้ฉันทำอย่างไร
Chuck Norris

2
ฉันพบปัญหาในขณะที่ฉันไม่ได้ตั้งชื่อมุมมองของฉันขั้นที่ 1 ขั้นตอนที่ 2 ฯลฯ ... ของฉันตั้งชื่อสิ่งที่มีความหมายมากกว่า แต่ไม่เรียงตามตัวอักษร ดังนั้นฉันจึงได้รับแบบจำลองของฉันผิดลำดับ ฉันเพิ่มคุณสมบัติ StepNumber ในอินเตอร์เฟส IStepViewModel ตอนนี้ฉันสามารถจัดเรียงตามนี้ในวิธีการเริ่มต้นของ WizardViewModel
Jeff Reddy

13

เพื่อเสริมคำตอบของ Amit Bagga คุณจะพบด้านล่างสิ่งที่ฉันทำ แม้ว่าจะสง่างามน้อยกว่าฉันก็พบว่าวิธีนี้ง่ายกว่าคำตอบของดาริน

ตัวควบคุม:

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; }
    }

11

ฉันขอแนะนำให้คุณรักษาสถานะของกระบวนการที่สมบูรณ์บนไคลเอนต์โดยใช้ Jquery

ตัวอย่างเช่นเรามีกระบวนการ Three Step Wizard

  1. ผู้ใช้ที่นำเสนอด้วยขั้นตอนที่ 1 ซึ่งมีปุ่มกำกับว่า "ถัดไป"
  2. เมื่อคลิกถัดไปเราจะทำการร้องขอ Ajax และสร้าง DIV ที่เรียกว่า Step2 และโหลด HTML ลงใน DIV นั้น
  3. ในขั้นตอนที่ 3 เรามีปุ่ม "เสร็จสิ้น" เมื่อคลิกที่ปุ่มโพสต์ข้อมูลโดยใช้การโทร $ .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;
 }
}

ข้างต้นเป็นเพียงการสาธิตที่จะช่วยให้คุณบรรลุผลลัพธ์ ในขั้นตอนสุดท้ายคุณต้องสร้าง Domain Object และเติมค่าที่ถูกต้องจาก Wizard Object และ Store ลงในฐานข้อมูล


ใช่นั่นเป็นวิธีแก้ปัญหาที่น่าสนใจ แต่เรามีการเชื่อมต่ออินเทอร์เน็ตที่ไม่ดีในฝั่งไคลเอ็นต์และเขา / เธอควรส่งไฟล์จำนวนมากมาให้เรา เราจึงปฏิเสธวิธีแก้ปัญหานั้นก่อนหน้านี้
Jahan

คุณช่วยแจ้งปริมาณข้อมูลที่ลูกค้าจะอัปโหลดได้ไหม
Amit Bagga

หลายไฟล์เกือบสิบไฟล์แต่ละไฟล์เกือบ 1 MB
Jahan

5

วิซาร์ดเป็นเพียงขั้นตอนง่ายๆในการประมวลผลโมเดลง่ายๆ ไม่มีเหตุผลที่จะสร้างแบบจำลองหลายแบบสำหรับวิซาร์ด สิ่งที่คุณต้องทำคือสร้างโมเดลเดียวและส่งต่อระหว่างการดำเนินการในคอนโทรลเลอร์เดียว

public class MyModel
{
     [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
     public Guid Id { get; set };
     public string StepOneData { get; set; }
     public string StepTwoData { get; set; }
}

สหศึกษาข้างต้นนั้นโง่ง่ายดังนั้นให้แทนที่ช่องของคุณในนั้น ต่อไปเราจะเริ่มต้นด้วยการกระทำง่ายๆที่เริ่มต้นตัวช่วยสร้างของเรา

    public ActionResult WizardStep1()
    {
        return View(new MyModel());
    }

สิ่งนี้เรียกมุมมองว่า "WizardStep1.cshtml (หากใช้มีดโกนนั่นคือ) คุณสามารถใช้วิซาร์ดสร้างเทมเพลตได้หากต้องการเราจะเปลี่ยนเส้นทางโพสต์ไปยังการกระทำอื่น

<WizardStep1.cshtml>
@using (Html.BeginForm("WizardStep2", "MyWizard")) {

สิ่งที่ควรทราบก็คือเราจะโพสต์สิ่งนี้ไปยังการกระทำอื่น การดำเนินการ WizardStep2

    [HttpPost]
    public ActionResult WizardStep2(MyModel myModel)
    {
        return ModelState.IsValid ? View(myModel) : View("WizardStep1", myModel);
    }

ในการดำเนินการนี้เราตรวจสอบว่าโมเดลของเราถูกต้องหรือไม่และหากเป็นเช่นนั้นเราจะส่งไปยังมุมมอง WizardStep2.cshtml ของเราเราจะส่งกลับไปยังขั้นตอนที่หนึ่งพร้อมกับข้อผิดพลาดในการตรวจสอบความถูกต้อง ในแต่ละขั้นตอนเราจะส่งไปยังขั้นตอนถัดไปตรวจสอบความถูกต้องของขั้นตอนนั้นและดำเนินการต่อ ตอนนี้นักพัฒนาที่มีความเชี่ยวชาญบางคนอาจบอกว่าเราไม่สามารถย้ายไปมาระหว่างขั้นตอนเช่นนี้ได้หากเราใช้แอตทริบิวต์ [จำเป็น] หรือคำอธิบายประกอบข้อมูลอื่น ๆ ระหว่างขั้นตอน และคุณคิดถูกดังนั้นจึงควรลบข้อผิดพลาดของรายการที่ยังไม่ได้ตรวจสอบ เช่นด้านล่าง

    [HttpPost]
    public ActionResult WizardStep3(MyModel myModel)
    {
        foreach (var error in ModelState["StepTwoData"].Errors)
        {
            ModelState["StepTwoData"].Errors.Remove(error);
        }

ในที่สุดเราจะบันทึกโมเดลหนึ่งครั้งไปยังที่เก็บข้อมูล นอกจากนี้ยังป้องกันผู้ใช้ที่เริ่มต้นวิซาร์ด แต่ไม่เสร็จสิ้นเพื่อไม่ให้บันทึกข้อมูลที่ไม่สมบูรณ์ลงในฐานข้อมูล

ฉันหวังว่าคุณจะพบว่าวิธีการติดตั้งวิซาร์ดนี้ใช้งานและบำรุงรักษาได้ง่ายกว่าวิธีการใด ๆ ที่กล่าวถึงก่อนหน้านี้

ขอบคุณที่อ่าน.


คุณมีสิ่งนี้ในโซลูชันที่สมบูรณ์ที่ฉันสามารถลองได้หรือไม่ ขอบคุณ
mpora

5

ฉันต้องการแบ่งปันวิธีจัดการกับข้อกำหนดเหล่านี้ของตัวเอง ฉันไม่ต้องการใช้ SessionState เลยและฉันไม่ต้องการให้มันจัดการฝั่งไคลเอ็นต์และวิธีการทำให้เป็นอนุกรมต้องใช้ MVC Futures ซึ่งฉันไม่ต้องการให้รวมไว้ในโครงการของฉัน

แต่ฉันสร้างตัวช่วย HTML ที่จะวนซ้ำผ่านคุณสมบัติทั้งหมดของแบบจำลองและสร้างองค์ประกอบที่ซ่อนอยู่สำหรับแต่ละองค์ประกอบ ถ้ามันเป็นคุณสมบัติที่ซับซ้อนมันจะทำงานซ้ำกับมัน

ในแบบฟอร์มของคุณระบบจะโพสต์ไปยังคอนโทรลเลอร์พร้อมกับข้อมูลโมเดลใหม่ในแต่ละขั้นตอน "วิซาร์ด"

ฉันเขียนสิ่งนี้สำหรับ MVC 5

using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Web;
using System.Web.Routing;
using System.Web.Mvc;
using System.Web.Mvc.Html;
using System.Reflection;

namespace YourNamespace
{
    public static class CHTML
    {
        public static MvcHtmlString HiddenClassFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression)
        {
            return HiddenClassFor(html, expression, null);
        }

        public static MvcHtmlString HiddenClassFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression, object htmlAttributes)
        {
            ModelMetadata _metaData = ModelMetadata.FromLambdaExpression(expression, html.ViewData);

            if (_metaData.Model == null)
                return MvcHtmlString.Empty;

            RouteValueDictionary _dict = htmlAttributes != null ? new RouteValueDictionary(htmlAttributes) : null;

            return MvcHtmlString.Create(HiddenClassFor(html, expression, _metaData, _dict).ToString());
        }

        private static StringBuilder HiddenClassFor<TModel>(HtmlHelper<TModel> html, LambdaExpression expression, ModelMetadata metaData, IDictionary<string, object> htmlAttributes)
        {
            StringBuilder _sb = new StringBuilder();

            foreach (ModelMetadata _prop in metaData.Properties)
            {
                Type _type = typeof(Func<,>).MakeGenericType(typeof(TModel), _prop.ModelType);
                var _body = Expression.Property(expression.Body, _prop.PropertyName);
                LambdaExpression _propExp = Expression.Lambda(_type, _body, expression.Parameters);

                if (!_prop.IsComplexType)
                {
                    string _id = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(ExpressionHelper.GetExpressionText(_propExp));
                    string _name = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(ExpressionHelper.GetExpressionText(_propExp));
                    object _value = _prop.Model;

                    _sb.Append(MinHiddenFor(_id, _name, _value, htmlAttributes));
                }
                else
                {
                    if (_prop.ModelType.IsArray)
                        _sb.Append(HiddenArrayFor(html, _propExp, _prop, htmlAttributes));
                    else if (_prop.ModelType.IsClass)
                        _sb.Append(HiddenClassFor(html, _propExp, _prop, htmlAttributes));
                    else
                        throw new Exception(string.Format("Cannot handle complex property, {0}, of type, {1}.", _prop.PropertyName, _prop.ModelType));
                }
            }

            return _sb;
        }

        public static MvcHtmlString HiddenArrayFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression)
        {
            return HiddenArrayFor(html, expression, null);
        }

        public static MvcHtmlString HiddenArrayFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression, object htmlAttributes)
        {
            ModelMetadata _metaData = ModelMetadata.FromLambdaExpression(expression, html.ViewData);

            if (_metaData.Model == null)
                return MvcHtmlString.Empty;

            RouteValueDictionary _dict = htmlAttributes != null ? new RouteValueDictionary(htmlAttributes) : null;

            return MvcHtmlString.Create(HiddenArrayFor(html, expression, _metaData, _dict).ToString());
        }

        private static StringBuilder HiddenArrayFor<TModel>(HtmlHelper<TModel> html, LambdaExpression expression, ModelMetadata metaData, IDictionary<string, object> htmlAttributes)
        {
            Type _eleType = metaData.ModelType.GetElementType();
            Type _type = typeof(Func<,>).MakeGenericType(typeof(TModel), _eleType);

            object[] _array = (object[])metaData.Model;

            StringBuilder _sb = new StringBuilder();

            for (int i = 0; i < _array.Length; i++)
            {
                var _body = Expression.ArrayIndex(expression.Body, Expression.Constant(i));
                LambdaExpression _arrayExp = Expression.Lambda(_type, _body, expression.Parameters);
                ModelMetadata _valueMeta = ModelMetadata.FromLambdaExpression((dynamic)_arrayExp, html.ViewData);

                if (_eleType.IsClass)
                {
                    _sb.Append(HiddenClassFor(html, _arrayExp, _valueMeta, htmlAttributes));
                }
                else
                {
                    string _id = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(ExpressionHelper.GetExpressionText(_arrayExp));
                    string _name = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(ExpressionHelper.GetExpressionText(_arrayExp));
                    object _value = _valueMeta.Model;

                    _sb.Append(MinHiddenFor(_id, _name, _value, htmlAttributes));
                }
            }

            return _sb;
        }

        public static MvcHtmlString MinHiddenFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression)
        {
            return MinHiddenFor(html, expression, null);
        }

        public static MvcHtmlString MinHiddenFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression, object htmlAttributes)
        {
            string _id = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(ExpressionHelper.GetExpressionText(expression));
            string _name = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(ExpressionHelper.GetExpressionText(expression));
            object _value = ModelMetadata.FromLambdaExpression(expression, html.ViewData).Model;
            RouteValueDictionary _dict = htmlAttributes != null ? new RouteValueDictionary(htmlAttributes) : null;

            return MinHiddenFor(_id, _name, _value, _dict);
        }

        public static MvcHtmlString MinHiddenFor(string id, string name, object value, IDictionary<string, object> htmlAttributes)
        {
            TagBuilder _input = new TagBuilder("input");
            _input.Attributes.Add("id", id);
            _input.Attributes.Add("name", name);
            _input.Attributes.Add("type", "hidden");

            if (value != null)
            {
                _input.Attributes.Add("value", value.ToString());
            }

            if (htmlAttributes != null)
            {
                foreach (KeyValuePair<string, object> _pair in htmlAttributes)
                {
                    _input.MergeAttribute(_pair.Key, _pair.Value.ToString(), true);
                }
            }

            return new MvcHtmlString(_input.ToString(TagRenderMode.SelfClosing));
        }
    }
}

ตอนนี้สำหรับทุกขั้นตอนของ "วิซาร์ด" ของคุณคุณสามารถใช้โมเดลพื้นฐานเดียวกันและส่งผ่านคุณสมบัติโมเดล "ขั้นตอนที่ 1,2,3" ไปยังตัวช่วย @ Html.HiddenClassFor โดยใช้นิพจน์แลมบ์ดา

คุณสามารถมีปุ่มย้อนกลับในแต่ละขั้นตอนได้หากต้องการ เพียงแค่มีปุ่มย้อนกลับในแบบฟอร์มของคุณที่จะโพสต์ไปยังแอ็คชัน StepNBack บนคอนโทรลเลอร์โดยใช้แอตทริบิวต์ formaction ไม่รวมอยู่ในตัวอย่างด้านล่าง แต่เป็นเพียงแนวคิดสำหรับคุณ

อย่างไรก็ตามนี่คือตัวอย่างพื้นฐาน:

นี่คือ MODEL ของคุณ

public class WizardModel
{
    // you can store additional properties for your "wizard" / parent model here
    // these properties can be saved between pages by storing them in the form using @Html.MinHiddenFor(m => m.WizardID)
    public int? WizardID { get; set; }

    public string WizardType { get; set; }

    [Required]
    public Step1 Step1 { get; set; }

    [Required]
    public Step2 Step2 { get; set; }

    [Required]
    public Step3 Step3 { get; set; }

    // if you want to use the same model / view / controller for EDITING existing data as well as submitting NEW data here is an example of how to handle it
    public bool IsNew
    {
        get
        {
            return WizardID.HasValue;
        }
    }
}

public class Step1
{
    [Required]
    [MaxLength(32)]
    [Display(Name = "First Name")]
    public string FirstName { get; set; }

    [Required]
    [MaxLength(32)]
    [Display(Name = "Last Name")]
    public string LastName { get; set; }
}

public class Step2
{
    [Required]
    [MaxLength(512)]
    [Display(Name = "Biography")]
    public string Biography { get; set; }
}

public class Step3
{        
    // lets have an array of strings here to shake things up
    [Required]
    [Display(Name = "Your Favorite Foods")]
    public string[] FavoriteFoods { get; set; }
}

นี่คือ CONTROLLER ของคุณ

public class WizardController : Controller
{
    [HttpGet]
    [Route("wizard/new")]
    public ActionResult New()
    {
        WizardModel _model = new WizardModel()
        {
            WizardID = null,
            WizardType = "UserInfo"
        };

        return View("Step1", _model);
    }

    [HttpGet]
    [Route("wizard/edit/{wizardID:int}")]
    public ActionResult Edit(int wizardID)
    {
        WizardModel _model = database.GetData(wizardID);

        return View("Step1", _model);
    }

    [HttpPost]
    [Route("wizard/step1")]
    public ActionResult Step1(WizardModel model)
    {
        // just check if the values in the step1 model are valid
        // shouldn't use ModelState.IsValid here because that would check step2 & step3.
        // which isn't entered yet
        if (ModelState.IsValidField("Step1"))
        {
            return View("Step2", model);
        }

        return View("Step1", model);
    }

    [HttpPost]
    [Route("wizard/step2")]
    public ActionResult Step2(WizardModel model)
    {
        if (ModelState.IsValidField("Step2"))
        {
            return View("Step3", model);
        }

        return View("Step2", model);
    }

    [HttpPost]
    [Route("wizard/step3")]
    public ActionResult Step3(WizardModel model)
    {
        // all of the data for the wizard model is complete.
        // so now we check the entire model state
        if (ModelState.IsValid)
        {
            // validation succeeded. save the data from the model.
            // the model.IsNew is just if you want users to be able to
            // edit their existing data.
            if (model.IsNew)
                database.NewData(model);
            else
                database.EditData(model);

            return RedirectToAction("Success");
        }

        return View("Step3", model);
    }
}

นี่คือมุมมองของคุณ

ขั้นตอนที่ 1

@model WizardModel

@{
    ViewBag.Title = "Step 1";
}

@using (Html.BeginForm("Step1", "Wizard", FormMethod.Post))
{
    @Html.MinHiddenFor(m => m.WizardID)
    @Html.MinHiddenFor(m => m.WizardType)

    @Html.LabelFor(m => m.Step1.FirstName)
    @Html.TextBoxFor(m => m.Step1.FirstName)

    @Html.LabelFor(m => m.Step1.LastName)
    @Html.TextBoxFor(m => m.Step1.LastName)

    <button type="submit">Submit</button>
}

ขั้นตอนที่ 2

@model WizardModel

@{
    ViewBag.Title = "Step 2";
}

@using (Html.BeginForm("Step2", "Wizard", FormMethod.Post))
{
    @Html.MinHiddenFor(m => m.WizardID)
    @Html.MinHiddenFor(m => m.WizardType)
    @Html.HiddenClassFor(m => m.Step1)

    @Html.LabelFor(m => m.Step2.Biography)
    @Html.TextAreaFor(m => m.Step2.Biography)

    <button type="submit">Submit</button>
}

ขั้นตอนที่ 3

@model WizardModel

@{
    ViewBag.Title = "Step 3";
}

@using (Html.BeginForm("Step3", "Wizard", FormMethod.Post))
{
    @Html.MinHiddenFor(m => m.WizardID)
    @Html.MinHiddenFor(m => m.WizardType)
    @Html.HiddenClassFor(m => m.Step1)
    @Html.HiddenClassFor(m => m.Step2)

    @Html.LabelFor(m => m.Step3.FavoriteFoods)
    @Html.ListBoxFor(m => m.Step3.FavoriteFoods,
        new SelectListItem[]
        {
            new SelectListItem() { Value = "Pizza", Text = "Pizza" },
            new SelectListItem() { Value = "Sandwiches", Text = "Sandwiches" },
            new SelectListItem() { Value = "Burgers", Text = "Burgers" },
        });

    <button type="submit">Submit</button>
}

1
คุณช่วยชี้แจงวิธีแก้ปัญหาของคุณเพิ่มเติมได้โดยการให้โมเดลมุมมองและคอนโทรลเลอร์หรือไม่
Tyler Durden

2

เพิ่มข้อมูลเพิ่มเติมจากคำตอบของ @ Darin

จะเป็นอย่างไรหากคุณมีสไตล์การออกแบบที่แยกจากกันสำหรับแต่ละขั้นตอนและต้องการให้แต่ละขั้นตอนดูแยกกันหรือถ้าคุณมีคุณสมบัติหลายอย่างสำหรับแต่ละขั้นตอน

ในขณะที่ใช้Html.EditorForเรามีข้อ จำกัด ในการใช้มุมมองบางส่วน

สร้าง 3 มุมมองบางส่วนภายใต้Sharedโฟลเดอร์ชื่อ:Step1ViewModel.cshtml , Step3ViewModel.cshtml , Step3ViewModel.cshtml

เพื่อความกระชับฉันเพิ่งโพสต์มุมมองที่ 1 ขั้นตอนอื่น ๆ ก็เหมือนกับคำตอบของดาริน

Step1ViewModel.cs

[Serializable]
public class Step1ViewModel : IStepViewModel
{
  [Required]
  public string FirstName { get; set; }

  public string LastName { get; set; }

  public string PhoneNo { get; set; }

  public string EmailId { get; set; }

  public int Age { get; set; }

 }

Step1ViewModel.cshtml

 @model WizardPages.ViewModels.Step1ViewModel

<div class="container">
    <h2>Personal Details</h2>

    <div class="form-group">
        <label class="control-label col-sm-2" for="email">First Name:</label>
        <div class="col-sm-10">
            @Html.TextBoxFor(x => x.FirstName)
        </div>
    </div>
    <div class="form-group">
        <label class="control-label col-sm-2" for="pwd">Last Name:</label>
        <div class="col-sm-10">
            @Html.TextBoxFor(x => x.LastName)
        </div>
    </div>
    <div class="form-group">
        <label class="control-label col-sm-2" for="pwd">Phone No:</label>
        <div class="col-sm-10"> 
            @Html.TextBoxFor(x => x.PhoneNo)
        </div>
    </div>
    <div class="form-group">
        <label class="control-label col-sm-2" for="pwd">Email Id:</label>
        <div class="col-sm-10">
            @Html.TextBoxFor(x => x.EmailId)
        </div>
    </div>


</div>

Index.cshtml

@using Microsoft.Web.Mvc
@model WizardPages.ViewModels.WizardViewModel

@{
    var currentStep = Model.Steps[Model.CurrentStepIndex];

    string viewName = currentStep.ToString().Substring(currentStep.ToString().LastIndexOf('.') + 1);
}

<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.Partial(""+ viewName + "", currentStep);

    if (Model.CurrentStepIndex > 0)
    {

     <input type="submit" value="Previous" name="prev" class="btn btn-warning" />

    }

    if (Model.CurrentStepIndex < Model.Steps.Count - 1)
    {

      <input type="submit" value="Next" name="next" class="btn btn-info" />

    }
    else
    {

      <input type="submit" value="Finish" name="finish" class="btn btn-success" />

    }
}

หากมีวิธีแก้ไขที่ดีกว่านี้โปรดแสดงความคิดเห็นเพื่อแจ้งให้ผู้อื่นทราบ


-9

ทางเลือกหนึ่งคือการสร้างชุดของตารางที่เหมือนกันซึ่งจะจัดเก็บข้อมูลที่รวบรวมในแต่ละขั้นตอน จากนั้นในขั้นตอนสุดท้ายหากทุกอย่างเป็นไปด้วยดีคุณสามารถสร้างเอนทิตีจริงได้โดยการคัดลอกข้อมูลชั่วคราวและจัดเก็บ

อื่น ๆ คือการสร้างValue Objectsสำหรับแต่ละขั้นตอนและเก็บแล้วหรือCache Sessionจากนั้นถ้าทุกอย่างเป็นไปด้วยดีคุณสามารถสร้างวัตถุโดเมนของคุณจากพวกเขาและบันทึกไว้


1
คงจะดีไม่น้อยถ้าคนที่โหวตลงให้เหตุผลด้วย
Martin

ไม่ได้ลงคะแนนให้คุณ แต่คำตอบของคุณไม่เกี่ยวข้องกับคำถามอย่างสิ้นเชิง OP กำลังถามเกี่ยวกับวิธีสร้างวิซาร์ดในขณะที่คุณตอบกลับเกี่ยวกับวิธีจัดการการตอบกลับด้านหลัง
Dementic

1
ฉันมักจะไม่โหวต แต่เมื่อฉันทำฉันแน่ใจว่ามันเพิ่มคะแนน :-)
Suhail Mumtaz Awan
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.