asp.net mvc (분할 뷰 모델, 단일 모델)의 다단계 등록 프로세스 문제


117

나는이 여러 단계의 등록 절차 a로 백업, 도메인 층에서 단일 개체 속성에 정의 된 유효성 검사 규칙을 가지고.

도메인이 여러보기로 분할되고 게시 될 때 첫 번째보기에서 개체를 부분적으로 저장해야하는 경우 도메인 개체의 유효성을 검사하려면 어떻게해야합니까?

세션 사용에 대해 생각했는데 프로세스가 길고 데이터 양이 많아서 가능하지 않아 세션을 사용하고 싶지 않습니다.

모든 데이터를 관계형 메모리 내 db (기본 db와 동일한 스키마 사용)에 저장 한 다음 해당 데이터를 기본 db로 플러시하는 것에 대해 생각했지만 문제가 발생하여 (뷰에서 요청 된) 메인 db 및 인 메모리 db.

우아하고 깨끗한 솔루션을 찾고 있습니다 (보다 정확하게는 모범 사례).

업데이트 및 설명 :

@Darin 사려 깊은 답장을 보내 주셔서 감사합니다. 그게 제가 지금까지 한 일입니다. 그러나 우연히 많은 첨부 파일이있는 요청이 있습니다. Step2View예를 들어 어떤 사용자가 문서를 비동기 적으로 업로드 할 수 있는지를 설계합니다 . 그러나 해당 첨부 파일은 이전에 저장해야하는 다른 테이블과 참조 관계가있는 테이블에 저장해야합니다. Step1View.

따라서 도메인 객체를 Step1(부분적으로) 저장해야 하지만, Step1의 ViewModel에 부분적으로 매핑 된 백업 된 Core Domain 객체는 변환 된 소품 없이는 저장할 수 없습니다 Step2ViewModel.


@Jani, 이것의 업로드 조각을 알아 냈습니까? 당신의 두뇌를 선택하고 싶습니다. 이 정확한 문제에 대해 작업 중입니다.
Doug Chamberlain

1
블로그 의 솔루션 은 매우 간단하고 간단합니다. 가시성과 눈에 띄지 않는 jquery 유효성 검사를 통해 div를 "단계"로 사용합니다.
Dmitry Efimenko 2012

답변:


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 단계보기 내에서 에서 MVC 선물 Html.Serialize 도우미 를 1 단계를 양식 내부의 숨겨진 필드로 직렬화 할 수 있습니다 (원하는 경우 ViewState의 일종).

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

2 단계의 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모든 데이터를 채울 마지막 단계에 도달 할 때까지 계속합니다 . 그런 다음 뷰 모델을 도메인 모델에 매핑하고 처리를 위해 서비스 계층에 전달합니다. 서비스 계층은 유효성 검사 규칙 자체를 수행 할 수 있습니다.

또 다른 대안이 있습니다. 자바 스크립트를 사용하고 모두 같은 페이지에 배치하는 것입니다. 마법사 기능을 제공하는 많은 jquery 플러그인 이 있습니다 ( 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);
    }
}

이 컨트롤러에 대한 몇 가지 언급 :

  • 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 2011 년

1
@Doug Chamberlain, 업데이트 된 답변을 참조하십시오. 나는 그것이 나의 초기 게시물보다 조금 더 명확하게 해주기를 바랍니다.
Darin Dimitrov 2011 년

20
+1 @Jani :이 답변에 대해 Darin에게 50 점을 주어야합니다. 매우 포괄적입니다. 그리고 그는 ;-) 뷰 모델이 아니라 도메인 모델을 사용의 필요성을 유지 관리
톰 챈 틀러에게

3
어디에서도 Deserialize 속성을 찾을 수 없습니다 ... 또한 mvccontrib의 codeplex 페이지에서이 94fa6078a115 by Jeremy Skinner 2010 년 8 월 1 일 오후 5시 55 분 0 사용되지 않는 Deserialize 바인더 제거 어떻게 하시겠습니까?
Chuck Norris

2
내 뷰의 이름을 Step1, Step2 등으로 지정하지 않았지만 문제를 발견했습니다. 내 이름은 더 의미있는 이름이지만 알파벳순은 아닙니다. 그래서 결국 잘못된 순서로 모델을 가져 왔습니다. IStepViewModel 인터페이스에 StepNumber 속성을 추가했습니다. 이제 WizardViewModel의 Initialize 메서드에서이를 기준으로 정렬 할 수 있습니다.
Jeff Reddy

13

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

11

Jquery를 사용하여 클라이언트에서 Complete Process 상태를 유지하는 것이 좋습니다.

예를 들어 3 단계 마법사 프로세스가 있습니다.

  1. 의 사용자에게 "다음"이라는 레이블이 붙은 버튼이있는 Step1이 표시됩니다.
  2. 다음을 클릭하면 Ajax 요청을 만들고 Step2라는 DIV를 만들고 HTML을 해당 DIV에로드합니다.
  3. Step3에는 $ .post 호출을 사용하여 데이터를 게시하는 버튼을 클릭하면 "Finished"라는 버튼이 있습니다.

이렇게하면 양식 게시 데이터에서 직접 도메인 개체를 쉽게 빌드 할 수 있으며 데이터에 오류가있는 경우 모든 오류 메시지를 포함하는 유효한 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;
 }
}

위는 최종 결과를 달성하는 데 도움이되는 데모 일뿐입니다. 마지막 단계에서 도메인 개체를 만들고 마법사 개체에서 올바른 값을 채우고 데이터베이스에 저장해야합니다.


예, 그것은 흥미로운 해결책입니다.하지만 불행히도 클라이언트 측의 인터넷 연결이 좋지 않아서 많은 파일을 보내야합니다. 그래서 우리는 이전에 그 해결책을 거부했습니다.
자한

클라이언트가 업로드 할 데이터의 양을 알려주세요.
Amit Bagga 2011 년

여러 파일, 거의 10 개, 각각 거의 1MB.
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 (즉, razor를 사용하는 경우)"뷰가 호출됩니다. 원하는 경우 템플릿 만들기 마법사를 사용할 수 있습니다. 게시물을 다른 작업으로 리디렉션 할 것입니다.

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

주목할 점은 우리가 이것을 다른 작업에 게시 할 것이라는 것입니다. WizardStep2 액션

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

이 작업에서 우리는 모델이 유효한지 확인하고, 그렇다면 WizardStep2.cshtml보기로 보내지 않으면 유효성 검사 오류가있는 1 단계로 다시 보냅니다. 각 단계에서 우리는 그것을 다음 단계로 보내고 그 단계를 검증하고 계속 진행합니다. 이제 일부 정통한 개발자는 단계간에 [필수] 속성이나 다른 데이터 주석을 사용하면 이와 같은 단계간에 이동할 수 없다고 말할 수 있습니다. 그리고 당신이 옳을 것이므로 아직 확인되지 않은 항목의 오류를 제거하십시오. 아래와 같습니다.

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

마지막으로 모델을 데이터 저장소에 한 번 저장합니다. 이것은 또한 마법사를 시작하지만 완료하지 않은 사용자가 데이터베이스에 불완전한 데이터를 저장하지 않도록 방지합니다.

마법사를 구현하는이 방법이 이전에 언급 한 방법보다 사용 및 유지 관리가 훨씬 더 쉽다는 것을 알기를 바랍니다.

읽어 주셔서 감사합니다.


시도해 볼 수있는 완전한 솔루션에이 기능이 있습니까? 감사합니다
mpora 2016-07-28

5

이러한 요구 사항을 처리하는 방법을 공유하고 싶었습니다. 나는 SessionState를 전혀 사용하고 싶지 않았고 클라이언트 측에서 처리하기를 원하지 않았으며 serialize 메서드에는 프로젝트에 포함하고 싶지 않은 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));
        }
    }
}

이제 "마법사"의 모든 단계에서 동일한 기본 모델을 사용하고 "Step 1,2,3"모델 속성을 람다 식을 사용하여 @ Html.HiddenClassFor 도우미에 전달할 수 있습니다.

원하는 경우 각 단계에서 뒤로 버튼을 사용할 수도 있습니다. formaction 속성을 사용하여 컨트롤러의 StepNBack 작업에 게시 할 양식에 뒤로 버튼 만 있으면됩니다. 아래 예에는 포함되어 있지 않지만 귀하를위한 아이디어입니다.

어쨌든 여기에 기본적인 예가 있습니다.

모델은 다음과 같습니다.

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

여기에 컨트롤러가 있습니다.

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 부분보기 사용에 제한이 있습니다.

다음 Shared폴더 아래에 3 개의 부분 뷰를 만듭니다 .Step1ViewModel.cshtml , Step3ViewModel.cshtml , Step3ViewModel.cshtml

간결함을 위해 1st patial view를 게시하고 다른 단계는 Darin의 답변과 동일합니다.

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
나는 보통 투표하지 않지만, 내가 할 때, 내가 :-) 확실히 그 upvote에 할
수 하일 뭄 타즈 아완
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.