problemy z wieloetapowym procesem rejestracji w asp.net mvc (podzielone modele widoków, pojedynczy model)


117

Mam wieloetapowy proces rejestracji , wspierany przez pojedynczy obiekt w warstwie domeny , który ma zdefiniowane reguły walidacji we właściwościach.

Jak sprawdzić poprawność obiektu domeny, gdy domena jest podzielona na wiele widoków, a po opublikowaniu muszę częściowo zapisać obiekt w pierwszym widoku?

Myślałem o użyciu sesji, ale nie jest to możliwe, ponieważ proces jest długi, a ilość danych jest duża, więc nie chcę używać sesji.

Myślałem o zapisaniu wszystkich danych w relacyjnej bazie danych w pamięci (z tym samym schematem co główna baza danych), a następnie opróżnieniu tych danych do głównej bazy danych, ale pojawiły się problemy, które powodują, że powinienem routować między usługami (żądanymi w widokach), które pracują z główna baza danych i baza danych w pamięci.

Szukam eleganckiego i czystego rozwiązania (a dokładniej najlepszej praktyki).

AKTUALIZACJA I WYJAŚNIENIE:

@Darin Dziękuję za przemyślaną odpowiedź, dokładnie to zrobiłem do tej pory. Ale nawiasem mówiąc mam żądanie, które ma wiele załączników, projektuję Step2Viewnp. Który użytkownik może załadować w nim dokumenty asynchronicznie, ale te załączniki powinny być zapisane w tabeli z referencyjnym odniesieniem do innej tabeli, która powinna być wcześniej zapisana w Step1View.

Dlatego powinienem zapisać obiekt domeny w Step1(częściowo), ale nie mogę, ponieważ obsługiwany obiekt Core Domain, który jest częściowo mapowany na ViewModel Step1, nie może zostać zapisany bez właściwości, które pochodzą z przekonwertowanych Step2ViewModel.


@Jani, czy kiedykolwiek wymyśliłeś, jaki fragment tego można przesłać? Chciałbym wybrać twój mózg. Pracuję dokładnie nad tym problemem.
Doug Chamberlain

1
Rozwiązanie na tym blogu jest dość proste i nieskomplikowane. Używa elementów div jako „kroków”, zmieniając ich widoczność i dyskretną walidację jQuery.
Dmitry Efimenko

Odpowiedzi:


229

Po pierwsze, nie powinieneś używać żadnych obiektów domeny w swoich widokach. Powinieneś używać modeli widoku. Każdy model widoku będzie zawierał tylko właściwości wymagane przez dany widok, a także atrybuty walidacji specyficzne dla tego danego widoku. Więc jeśli masz kreatora 3 kroków, oznacza to, że będziesz mieć 3 modele widoków, po jednym dla każdego kroku:

public class Step1ViewModel
{
    [Required]
    public string SomeProperty { get; set; }

    ...
}

public class Step2ViewModel
{
    [Required]
    public string SomeOtherProperty { get; set; }

    ...
}

i tak dalej. Wszystkie te modele widoków mogą być obsługiwane przez główny model widoku kreatora:

public class WizardViewModel
{
    public Step1ViewModel Step1 { get; set; }
    public Step2ViewModel Step2 { get; set; }
    ...
}

wtedy możesz mieć akcje kontrolera renderujące każdy krok procesu kreatora i przekazujące element main WizardViewModeldo widoku. Gdy jesteś na pierwszym kroku wewnątrz akcji kontrolera, możesz zainicjować Step1właściwość. Następnie w widoku można wygenerować formularz umożliwiający użytkownikowi wypełnienie właściwości dotyczących kroku 1. Po przesłaniu formularza akcja kontrolera zastosuje reguły walidacji tylko dla kroku 1:

[HttpPost]
public ActionResult Step1(Step1ViewModel step1)
{
    var model = new WizardViewModel 
    {
        Step1 = step1
    };

    if (!ModelState.IsValid)
    {
        return View(model);
    }
    return View("Step2", model);
}

Teraz w widoku kroku 2 możesz użyć pomocnika Html.Serialize z kontraktów terminowych MVC w celu serializacji kroku 1 do ukrytego pola w formularzu (rodzaj ViewState, jeśli chcesz):

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

i wewnątrz akcji POST kroku 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);
}

I tak dalej, aż dojdziesz do ostatniego kroku, w którym będziesz miał WizardViewModelwypełnione wszystkie dane. Następnie zmapujesz model widoku na model domeny i przekażesz go do warstwy usług w celu przetworzenia. Warstwa usług może sama wykonywać dowolne reguły walidacji i tak dalej ...

Jest też inna alternatywa: użycie javascript i umieszczenie wszystkiego na tej samej stronie. Istnieje wiele wtyczek jquery , które zapewniają funkcjonalność kreatora ( Stepy jest fajny). Zasadniczo jest to kwestia wyświetlania i ukrywania elementów div na kliencie, w którym to przypadku nie musisz już martwić się o utrzymywanie się stanu między krokami.

Ale bez względu na to, jakie rozwiązanie wybierzesz, zawsze używaj modeli widoków i przeprowadzaj walidację na tych modelach widoków. Dopóki trzymasz atrybuty walidacji adnotacji danych w modelach domeny, będziesz mieć bardzo duże problemy, ponieważ modele domeny nie są przystosowane do widoków.


AKTUALIZACJA:

OK, z uwagi na liczne komentarze dochodzę do wniosku, że moja odpowiedź nie była jasna. I muszę się zgodzić. Spróbuję więc dalej rozwinąć mój przykład.

Moglibyśmy zdefiniować interfejs, który powinny implementować wszystkie modele widoków krokowych (to tylko interfejs znaczników):

public interface IStepViewModel
{
}

następnie zdefiniowalibyśmy 3 kroki dla kreatora, gdzie każdy krok zawierałby oczywiście tylko te właściwości, których wymaga, jak również odpowiednie atrybuty walidacji:

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

następnie definiujemy główny model widoku kreatora, który składa się z listy kroków i aktualnego indeksu kroków:

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

Następnie przechodzimy do kontrolera:

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

Kilka uwag na temat tego kontrolera:

  • Akcja Index POST używa [Deserialize]atrybutów z biblioteki Microsoft Futures, więc upewnij się, że zainstalowano MvcContribpakiet NuGet. Z tego powodu modele widoków powinny być ozdobione [Serializable]atrybutem
  • Akcja Index POST przyjmuje jako argument IStepViewModelinterfejs, więc aby miało to sens, potrzebujemy niestandardowego spinacza modelu.

Oto powiązany segregator modelu:

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

Ten segregator używa specjalnego ukrytego pola o nazwie StepType, które będzie zawierać konkretny typ każdego kroku i które wyślemy na każde żądanie.

Ten model segregatora zostanie zarejestrowany w Application_Start:

ModelBinders.Binders.Add(typeof(IStepViewModel), new StepViewModelBinder());

Ostatnim brakującym elementem układanki są widoki. Oto główny ~/Views/Wizard/Index.cshtmlwidok:

@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" />
    }
}

I to wszystko, czego potrzebujesz, aby to działało. Oczywiście, jeśli chcesz, możesz spersonalizować wygląd i działanie niektórych lub wszystkich kroków kreatora, definiując niestandardowy szablon edytora. Na przykład zróbmy to dla kroku 2. Więc definiujemy ~/Views/Wizard/EditorTemplates/Step2ViewModel.cshtmlczęściową:

@model Step2ViewModel

Special Step 2
@Html.TextBoxFor(x => x.Bar)

Oto jak wygląda struktura:

wprowadź opis obrazu tutaj

Oczywiście jest miejsce na ulepszenia. Akcja Index POST wygląda następująco: s..t. Jest w nim za dużo kodu. Dalsze uproszczenie polegałoby na przeniesieniu wszystkich elementów infrastruktury, takich jak indeks, zarządzanie bieżącym indeksem, kopiowanie bieżącego kroku do kreatora… do innego segregatora modelu. Więc w końcu otrzymujemy:

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

czyli bardziej jak powinny wyglądać akcje POST. Zostawiam tę poprawę na następny raz :-)


1
@Doug Chamberlain, używam AutoMapper do konwersji między moimi modelami widoku i modelami domeny.
Darin Dimitrov

1
@Doug Chamberlain, zobacz moją zaktualizowaną odpowiedź. Mam nadzieję, że wszystko jest przez to trochę jaśniejsze niż mój pierwszy post.
Darin Dimitrov

20
+1 @Jani: naprawdę musisz dać Darinowi 50 punktów za tę odpowiedź. Jest bardzo wszechstronny. I udało mu się powtórzyć potrzebę używania ViewModel, a nie modeli Domain ;-)
Tom Chantler

3
Nigdzie nie mogę znaleźć atrybutu Deserialize ... Również na stronie codeplex mvccontrib znajduję to 94fa6078a115 autorstwa Jeremy Skinner 1 sierpnia 2010 o 17:55 0 Usuń przestarzały segregator Deserialize Co sugerujesz mi zrobić?
Chuck Norris

2
Znalazłem problem, ale nie nazwałam swoich widoków Krok 1, Krok 2 itd. Moje są nazywane bardziej znaczącymi, ale nie alfabetycznymi. Więc skończyło się na tym, że moje modele zostały ustawione w złej kolejności. Dodałem właściwość StepNumber do interfejsu IStepViewModel. Teraz mogę sortować według tego w metodzie Initialize WizardViewModel.
Jeff Reddy,

13

Aby uzupełnić odpowiedź Amita Baggi, poniżej znajdziesz to, co zrobiłem. Nawet jeśli jest mniej elegancki, wydaje mi się to prostsze niż odpowiedź Darina.

Kontroler :

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

Modele:

 [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

Sugerowałbym utrzymanie stanu Complete Process na kliencie za pomocą Jquery.

Na przykład mamy proces kreatora trzech kroków.

  1. Przedstawiono użytkownikowi Krok 1, na którym znajduje się przycisk „Dalej”
  2. Po kliknięciu Dalej tworzymy żądanie Ajax i tworzymy DIV o nazwie Step2 i ładujemy HTML do tego DIV.
  3. W kroku 3 mamy przycisk oznaczony „Zakończono” po kliknięciu przycisku opublikowanie danych przy użyciu wywołania $ .post.

W ten sposób możesz łatwo zbudować obiekt domeny bezpośrednio z danych posta formularza, aw przypadku, gdy dane zawierają błędy, zwróć poprawny kod JSON przechowujący wszystkie komunikaty o błędach i wyświetl je w div.

Podziel kroki

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

Powyższe to tylko demonstracja, która pomoże Ci osiągnąć efekt końcowy. W ostatnim kroku musisz utworzyć obiekt domeny i wprowadzić do bazy danych prawidłowe wartości z obiektu kreatora i magazynu.


Tak, to ciekawe rozwiązanie, ale niestety mamy słabe połączenie internetowe po stronie klienta i powinien wysłać nam kilka plików. więc odrzuciliśmy to rozwiązanie wcześniej.
Jahan

Czy możesz mi podać ilość danych, które klient zamierza przesłać.
Amit Bagga

Kilka plików, prawie dziesięć, każdy prawie 1 MB.
Jahan

5

Kreatorzy to tylko proste kroki w przetwarzaniu prostego modelu. Nie ma powodu, aby tworzyć wiele modeli dla kreatora. Wszystko, co musisz zrobić, to utworzyć pojedynczy model i przekazać go między akcjami w jednym kontrolerze.

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

Powyższy coed jest głupi prosty, więc zamień tam swoje pola. Następnie zaczynamy od prostej akcji, która uruchamia naszego kreatora.

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

Wywołuje to widok „WizardStep1.cshtml (jeśli używasz maszynki do golenia). Jeśli chcesz, możesz użyć kreatora tworzenia szablonów. Będziemy po prostu przekierowywać post do innej akcji.

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

Należy zauważyć, że będziemy to umieszczać w innej akcji; akcja WizardStep2

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

W tej akcji sprawdzamy, czy nasz model jest prawidłowy, a jeśli tak, wysyłamy go do naszego widoku WizardStep2.cshtml, w przeciwnym razie odsyłamy z powrotem do kroku pierwszego z błędami walidacji. W każdym kroku wysyłamy go do następnego kroku, zatwierdzamy ten krok i przechodzimy dalej. Niektórzy doświadczeni programiści mogą dobrze powiedzieć, że nie możemy przechodzić między krokami, takimi jak ten, jeśli używamy atrybutów [Wymagane] lub innych adnotacji danych między krokami. Miałbyś rację, więc usuń błędy w elementach, które nie zostały jeszcze sprawdzone. jak poniżej.

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

Na koniec zapiszemy model raz do magazynu danych. Zapobiega to również użytkownikowi, który uruchamia kreatora, ale go nie kończy, aby nie zapisać niekompletnych danych w bazie danych.

Mam nadzieję, że ta metoda implementacji kreatora jest dużo łatwiejsza w użyciu i utrzymaniu niż którakolwiek z wcześniej wymienionych metod.

Dziękuje za przeczytanie.


czy masz to w kompletnym rozwiązaniu, które mogę wypróbować? Dzięki
mpora

5

Chciałem podzielić się moim własnym sposobem radzenia sobie z tymi wymaganiami. W ogóle nie chciałem używać SessionState, ani nie chciałem obsługiwać go po stronie klienta, a metoda serializacji wymaga MVC Futures, którego nie chciałem uwzględniać w moim projekcie.

Zamiast tego zbudowałem pomocnika HTML, który będzie iterował przez wszystkie właściwości modelu i generował niestandardowy ukryty element dla każdego z nich. Jeśli jest to złożona właściwość, będzie na niej działać rekurencyjnie.

W Twoim formularzu zostaną one przesłane do kontrolera wraz z danymi nowego modelu na każdym kroku „kreatora”.

Napisałem to dla 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));
        }
    }
}

Teraz we wszystkich krokach swojego „kreatora” możesz użyć tego samego modelu podstawowego i przekazać właściwości modelu „Step 1,2,3” do helpera @ Html.HiddenClassFor przy użyciu wyrażenia lambda.

Jeśli chcesz, możesz nawet mieć przycisk Wstecz na każdym kroku. Wystarczy mieć w formularzu przycisk Wstecz, który wyśle ​​go do akcji StepNBack na kontrolerze przy użyciu atrybutu formaction. Nie uwzględniono w poniższym przykładzie, ale tylko pomysł dla Ciebie.

W każdym razie tutaj jest podstawowy przykład:

Oto Twój 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; }
}

Oto Twój KONTROLER

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

Oto Twoje WIDOKI

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

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

Krok 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
Czy mógłbyś dokładniej wyjaśnić swoje rozwiązanie, podając model widoku i kontroler?
Tyler Durden,

2

Dodawanie więcej informacji z odpowiedzi @ Darin.

A co, jeśli masz oddzielny styl projektowania dla każdego kroku i chcesz zachować każdy z nich w oddzielnym widoku częściowym, lub co, jeśli masz wiele właściwości dla każdego kroku?

Podczas korzystania Html.EditorFormamy ograniczenia do korzystania z częściowego widoku.

Utwórz 3 częściowe widoki w Sharedfolderze o nazwie:Step1ViewModel.cshtml , Step3ViewModel.cshtml , Step3ViewModel.cshtml

Dla zwięzłości zamieszczam tylko pierwszy widok częściowy, inne kroki są takie same jak odpowiedź Darina.

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" />

    }
}

Jeśli istnieje lepsze rozwiązanie, prosimy o komentarz, aby inni wiedzieli.


-9

Jedną z opcji jest utworzenie zestawu identycznych tabel, w których będą przechowywane dane zebrane w każdym kroku. Następnie w ostatnim kroku, jeśli wszystko pójdzie dobrze, możesz utworzyć rzeczywistą jednostkę, kopiując tymczasowe dane i przechowując je.

Innym jest tworzenie Value Objectsdla każdego kroku i przechowywanie w Cachelub Session. Jeśli wszystko pójdzie dobrze, możesz utworzyć z nich obiekt domeny i zapisać go


1
Byłoby miło, gdyby ludzie, którzy głosowali negatywnie, również podali swój powód.
Martin

Nie głosowałem na ciebie, ale twoja odpowiedź jest całkowicie nieistotna na pytanie. OP pyta, jak utworzyć kreatora, a ty odpowiadasz, jak postępować z odpowiedzią na odwrocie.
Dementic

1
Zwykle nie głosuję, ale kiedy to robię, upewniam się, że jest za :-)
Suhail Mumtaz Awan
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.