asp.net mvcのマルチステップ登録プロセスの問題(分割ビューモデル、単一モデル)


117

プロパティに検証ルールが定義されている、ドメインレイヤーの単一のオブジェクトに裏付けられたマルチステップ登録プロセスがあります。

ドメインが多数のビューに分割されている場合、ドメインオブジェクトを検証するにはどうすればよいですか。また、投稿時に最初のビューにオブジェクトを部分的に保存する必要がありますか?

セッションの使用を考えましたが、プロセスが長く、データ量が多いため、それは不可能です。そのため、セッションを使用したくありません。

リレーショナルインメモリデータベース(メインデータベースと同じスキーマを使用)にすべてのデータを保存し、そのデータをメインデータベースにフラッシュすることを考えましたが、問題が発生したのは、メインdbとインメモリdb。

エレガントでクリーンなソリューションを探しています(より正確にはベストプラクティスです)。

更新と説明:

@ダーリンあなたの思慮深い返事をありがとう、それはまさに今まで私がやったことでした。しかし、偶然にも多くの添付ファイルが含まれるリクエストがStep2Viewあります。たとえば、ユーザーがドキュメントを非同期でアップロードできるように設計しますが、それらの添付ファイルは、以前に保存されているはずの別のテーブルとの参照関係でテーブルに保存する必要がありますStep1View

したがって、ドメインオブジェクトをStep1(部分的に)保存する必要がありますが、できません。Step1のViewModelに部分的にマップされているバックされたコアドメインオブジェクトは、convertedからのプロップがないと保存できませんStep2ViewModel


@ジャニ、これのアップロードのピースを見つけた?あなたの脳を選びたいのですが。私はこの正確な問題に取り組んでいます。
Doug Chamberlain

1
このブログのソリューションは非常にシンプルで簡単です。divを「ステップ」として使用し、可視性と邪魔にならないjquery検証を無効にします。
Dmitry Efimenko 2012

回答:


229

まず、ビューでドメインオブジェクトを使用しないでください。ビューモデルを使用する必要があります。各ビューモデルには、特定のビューに必要なプロパティと、この特定のビューに固有の検証属性のみが含まれます。したがって、3つのステップウィザードがある場合、これは、各ステップに1つずつ、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 futuresのをステップ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プラグインは数多くあります(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

1
@Doug Chamberlain、私の最新の回答をご覧ください。最初の投稿よりも少し明確になったといいのですが。
Darin Dimitrov 2011年

20
+1 @Jani:あなたは本当にこの答えのためにダーリンに50ポイントを与える必要があります。それは非常に包括的です。そして、彼はなんとかドメインモデルではなくViewModelを使用する必要性を繰り返し表明しました;-)
Tom Chantler

3
Deserialize属性がどこにも見つからない...また、mvccontribのcodeplexページで、これを見つけます。
チャックノリス

2
ビューにステップ1、ステップ2などの名前を付けなかったのに問題が見つかりました。鉱山には、より意味のある名前が付けられていますが、アルファベット順ではありません。そのため、モデルを間違った順序で取得してしまいました。IStepViewModelインターフェイスにStepNumberプロパティを追加しました。これで、WizardViewModelのInitializeメソッドでこれでソートできます。
ジェフレディ

13

アミット・バグガの答えを補足するために、私がしたことを以下に示します。エレガントではないにしても、この方法は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. 「次へ」というラベルの付いたボタンがあるステップ1が表示されたユーザー
  2. [次へ]をクリックすると、Ajaxリクエストが作成され、Step2というDIVが作成され、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;
 }
}

上記は、最終結果を達成するのに役立つデモにすぎません。最後のステップでは、ドメインオブジェクトを作成し、ウィザードオブジェクトから正しい値を入力してデータベースに格納する必要があります。


はい、それは興味深い解決策ですが、残念ながらクライアント側のインターネット接続が貧弱であり、彼/彼女は私たちにたくさんのファイルを送信する必要があります。したがって、その解決策を以前に拒否しました。
Jahan

クライアントがアップロードするデータの量を教えてください。
アミットBagga

いくつかのファイル、ほぼ10、それぞれ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ビューに送信します。それ以外の場合は、検証エラーのあるステップ1に送り返します。各ステップで次のステップに送信し、そのステップを検証して次に進みます。今、一部の知識のある開発者は、[必須]属性または他のデータ注釈をステップ間で使用すると、このようなステップ間を移動できないとよく言うかもしれません。そして、あなたは正しいでしょう、それでまだチェックされていないアイテムのエラーを取り除いてください。以下のように。

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

最後に、モデルをデータストアに一度保存します。これにより、ユーザーはウィザードを開始したが、完了せずに不完全なデータをデータベースに保存しないようにすることもできます。

このウィザードを実装する方法は、前述のどの方法よりも使いやすく、維持しやすいことを願っています。

読んでくれてありがとう。


私が試すことができる完全なソリューションでこれを持っていますか?ありがとう
mpora

5

私はこれらの要件を処理する独自の方法を共有したいと思いました。SessionStateをまったく使用したくなかったし、クライアント側で処理したくもありませんでした。serializeメソッドには、プロジェクトに含めたくないMVC Futureが必要です。

代わりに、モデルのすべてのプロパティを反復処理し、それぞれのカスタム非表示要素を生成する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ヘルパーに渡すことができます。

必要に応じて、各ステップで[戻る]ボタンを使用することもできます。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
ビューモデルとコントローラーを提供して、ソリューションをさらに明確にできますか?
タイラーダーデン、2015

2

@Darinの回答からさらに情報を追加します。

各ステップに個別のデザインスタイルがあり、それぞれを個別の部分ビューに維持したい場合、または各ステップに複数のプロパティがある場合はどうなりますか?

使用中Html.EditorForは、部分ビューの使用に制限があります。

Sharedという名前のフォルダの下に3つの部分ビューを作成します。Step1ViewModel.cshtml , Step3ViewModel.cshtml , Step3ViewModel.cshtml

簡潔にするために、私が最初の空間ビューを投稿しただけですが、他のステップはダーリンの答えと同じです。

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

1つのオプションは、各ステップで収集されたデータを格納する同一のテーブルのセットを作成することです。次に、最後のステップですべてがうまくいけば、一時データをコピーして保存することで、実際のエンティティを作成できます。

他の方法は、Value Objectsステップごとに作成し、Cacheまたはに保存することですSession。次に、すべてがうまくいけば、それらからドメインオブジェクトを作成して保存できます。


1
反対票を投じた人たちも理由を述べるとよいでしょう。
マーティン

反対票を投じなかったが、あなたの答えは質問とは完全に無関係である。OPはウィザードの作成方法を尋ねていますが、応答を処理する方法については後ほど返信します。
認知症の

1
私は通常投票しませんが、投票するときは必ず投票します:-)
Suhail Mumtaz Awan
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.