asp.net mvc'de çok adımlı kayıt işlemi sorunları (bölünmüş görünüm modelleri, tek model)


117

Bir sahip çok adımlı kayıt işlemi bir tarafından desteklenen, alan katmanda tek bir nesne özellikleri üzerinde tanımlanan doğrulama kuralları vardır.

Etki alanı birçok görünüme bölündüğünde etki alanı nesnesini nasıl doğrulamalıyım ve gönderildiğinde nesneyi ilk görünümde kısmen kaydetmem gerekir?

Oturumları kullanmayı düşündüm ama bu mümkün değil çünkü süreç uzun ve veri miktarı yüksek, bu yüzden oturumu kullanmak istemiyorum.

Tüm verileri ilişkisel bir bellek içi db'ye (ana db ile aynı şema ile) kaydetmeyi ve ardından bu verileri ana veri tabanına boşaltmayı düşündüm, ancak sorunlar ortaya çıktı, bunun nedeni ile çalışan hizmetler (görünümlerde talep edilen) arasında yönlendirme yapmam gerekiyor. ana db ve bellek içi db.

Zarif ve temiz bir çözüm arıyorum (daha doğrusu en iyi uygulama).

GÜNCELLEME VE AÇIKLAMA:

@Darin Düşünceli cevabınız için teşekkür ederim, şimdiye kadar yaptığım tam olarak buydu. Ancak bu arada, içinde birçok ek bulunan bir isteğim var, Step2Viewörneğin, hangi kullanıcının asenkron olarak içindeki belgeleri yükleyebileceğini tasarlıyorum , ancak bu ekler, daha önce kaydedilmiş olması gereken başka bir tabloyla referans ilişkisi olan bir tabloya kaydedilmelidir. Step1View.

Bu nedenle, etki alanı nesnesini Step1(kısmen) içine kaydetmeliyim , ancak kısmen Step1'in ViewModel'ine eşlenen desteklenen Core Domain nesnesinin dönüştürülmüş aksesuarlar olmadan kaydedilememesine neden olamam Step2ViewModel.


@Jani, Bunun yükleme parçasını hiç buldun mu? Beynini seçmek istiyorum. Tam da bu konu üzerinde çalışıyorum.
Doug Chamberlain

1
Bu çözüm blogunda oldukça basit ve yalındır. Görünürlüklerini ve göze batmayan jquery doğrulamalarını değiştirerek div'leri "adımlar" olarak kullanır.
Dmitry Efimenko

Yanıtlar:


229

Öncelikle, görünümlerinizde herhangi bir etki alanı nesnesi kullanmamalısınız. Görünüm modellerini kullanmalısınız. Her görünüm modeli, yalnızca verilen görünümün gerektirdiği özellikleri ve bu görünüme özgü doğrulama özniteliklerini içerecektir. Dolayısıyla, 3 adımlı sihirbazınız varsa bu, her adım için bir tane olmak üzere 3 görünüm modeliniz olacağı anlamına gelir:

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

    ...
}

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

    ...
}

ve bunun gibi. Tüm bu görünüm modelleri, bir ana sihirbaz görünüm modeli tarafından desteklenebilir:

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

daha sonra, sihirbaz sürecinin her adımını gerçekleştiren ve ana öğeyi WizardViewModelgörünüme geçiren denetleyici eylemlerine sahip olabilirsiniz . Denetleyici eyleminin ilk adımındayken, Step1özelliği başlatabilirsiniz . Ardından görünümün içinde, kullanıcının 1. adımla ilgili özellikleri doldurmasına izin veren formu oluşturursunuz. Form gönderildiğinde, denetleyici eylemi yalnızca 1. adım için doğrulama kurallarını uygular:

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

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

Şimdi 2. adım görünümünün içinde, 1. adımı formun içindeki gizli bir alana serileştirmek için MVC vadeli işlemlerinden Html.Serialize yardımcısını kullanabilirsiniz (isterseniz bir ViewState türü):

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

ve 2. adımın POST eyleminin içinde:

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

Ve böylece, WizardViewModeltüm verilerle doldurulacağınız son adıma gelene kadar devam edin . Ardından görünüm modelini etki alanı modelinizle eşler ve işlenmesi için hizmet katmanına aktarırsınız. Hizmet katmanı herhangi bir doğrulama kuralını kendisi gerçekleştirebilir ve bu böyle devam eder ...

Başka bir alternatif daha var: javascript kullanmak ve hepsini aynı sayfaya koymak. Orada sihirbaz işlevselliği sağlayan birçok jquery eklentisi var ( Stepy güzel bir eklentidir ). Temelde bu, istemcide div'leri gösterme ve gizleme meselesidir; bu durumda artık adımlar arasında kalıcı durum hakkında endişelenmenize gerek kalmaz.

Ancak hangi çözümü seçerseniz seçin, her zaman görüntüleme modellerini kullanın ve bu görünüm modellerinde doğrulama gerçekleştirin. Etki alanı modellerinize veri ek açıklaması doğrulama özniteliklerini yapıştırdığınız sürece, etki alanı modelleri görünümlere uyarlanmadığı için çok zorlanacaksınız.


GÜNCELLEME:

Tamam, sayısız yorum nedeniyle cevabımın net olmadığı sonucuna vardım. Ve kabul etmeliyim. Öyleyse, örneğimi daha da geliştirmeye çalışayım.

Tüm adım görünümü modellerinin uygulaması gereken bir arayüz tanımlayabiliriz (bu sadece bir işaret arayüzü):

public interface IStepViewModel
{
}

daha sonra sihirbaz için, her adımın elbette yalnızca gerektirdiği özellikleri ve ilgili doğrulama niteliklerini içereceği 3 adım tanımlarız:

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

daha sonra, bir adım listesi ve geçerli bir adım dizininden oluşan ana sihirbaz görünüm modelini tanımlarız:

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

Sonra denetleyiciye geçiyoruz:

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

Bu denetleyici hakkında birkaç açıklama:

  • Dizin POST eylemi [Deserialize]Microsoft Futures kitaplığındaki öznitelikleri kullanır, bu nedenle MvcContribNuGet'i yüklediğinizden emin olun . Görünüm modellerinin [Serializable]öznitelikle dekore edilmesinin nedeni budur.
  • Index POST işlemi argüman olarak bir IStepViewModelarabirim alır, bu nedenle bunun anlamlı olması için özel bir model bağlayıcısına ihtiyacımız var.

İlişkili model bağlayıcı:

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

Bu bağlayıcı, her adımın somut türünü içeren ve her istek üzerine göndereceğimiz StepType adlı özel bir gizli alan kullanır.

Bu model bağlayıcı şurada kaydedilecek Application_Start:

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

Bulmacanın son eksik kısmı, görünümlerdir. İşte ana ~/Views/Wizard/Index.cshtmlgörünüm:

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

Ve bunu çalıştırmak için ihtiyacınız olan tek şey bu. Elbette isterseniz, özel bir düzenleyici şablonu tanımlayarak sihirbazın bazı veya tüm adımlarının görünümünü ve hissini kişiselleştirebilirsiniz. Örneğin 2. adım için yapalım. Yani bir ~/Views/Wizard/EditorTemplates/Step2ViewModel.cshtmlparçayı tanımlıyoruz :

@model Step2ViewModel

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

Yapı şu şekildedir:

görüntü açıklamasını buraya girin

Elbette iyileştirme için yer var. Dizin POST eylemi s..t gibi görünür. İçinde çok fazla kod var. Daha fazla basitleştirme, dizin, mevcut dizin yönetimi, mevcut adımın sihirbaza kopyalanması gibi tüm altyapı öğelerinin başka bir model bağlayıcısına taşınmasını içerecektir. Böylece sonunda şunu elde ederiz:

[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 eylemlerinin nasıl görünmesi gerektiği daha fazlasıdır. Bu iyileştirmeyi bir dahaki sefere bırakıyorum :-)


1
@Doug Chamberlain, AutoMapper'ı görünüm modellerim ve etki alanı modellerim arasında dönüşüm yapmak için kullanıyorum .
Darin Dimitrov

1
@Doug Chamberlain, lütfen güncellenmiş cevabıma bakın. Umarım her şeyi ilk yazımdan biraz daha netleştirir.
Darin Dimitrov

20
+1 @Jani: Bu cevap için Darin'e gerçekten 50 puan vermeniz gerekiyor. Çok kapsamlı. Ve Domain modellerini değil ViewModel'i kullanma ihtiyacını yinelemeyi başardı ;-)
Tom Chantler

3
Seri durumdan çıkarma özelliğini hiçbir yerde bulamıyorum ... Ayrıca mvccontrib'in kod çokluğu sayfasında bu 94fa6078a115'i Jeremy Skinner tarafından 1 Ağustos 2010 17:55 tarihinde buluyorum 0 Kullanımdan kaldırılan Seriyi kaldırma bağlayıcısını kaldırın Ne yapmamı önerirsiniz?
Chuck Norris

2
Bir sorun buldum ancak görüşlerimi Adım1, Adım2, vb. İsimlendirmedim ... Benimki daha anlamlı bir şey olarak adlandırıldı, ancak alfabetik değil. Bu yüzden modellerimi yanlış sırayla aldım. IStepViewModel arabirimine bir StepNumber özelliği ekledim. Şimdi WizardViewModel'in Initialize yönteminde buna göre sıralayabilirim.
Jeff Reddy

13

Amit Bagga'nın cevabına ek olarak yaptığım şeyi aşağıda bulacaksınız. Daha az zarif olsa bile, bu yolu Darin'in cevabından daha basit buluyorum.

Denetleyici:

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

Modeller:

 [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 kullanarak istemcide Tam İşlem durumunu korumanızı öneririm.

Örneğin, Üç Adımlı Sihirbaz sürecimiz var.

  1. Kullanıcıya, üzerinde "Sonraki" olarak etiketlenmiş bir düğmenin bulunduğu 1. Adım sunulur
  2. İleri'ye tıkladığınızda bir Ajax İsteği oluşturur ve Adım2 adlı bir DIV oluştururuz ve HTML'yi bu DIV'ye yükleriz.
  3. 3. Adımda, "Bitti" etiketli bir Düğmeye sahibiz. Düğmeye tıklandığında verileri $ .post çağrısını kullanarak gönderin.

Bu şekilde, etki alanı nesnenizi doğrudan form postası verilerinden kolayca oluşturabilirsiniz ve verilerin hata içermesi durumunda, tüm hata iletisini içeren geçerli JSON döndürür ve bunları bir div öğesinde görüntüleyebilirsiniz.

Lütfen Adımları bölün

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

Yukarıdakiler, nihai sonuca ulaşmanıza yardımcı olacak bir tanıtımdır. Son Adımda, Etki Alanı Nesnesini oluşturmanız ve Sihirbaz Nesnesi'nden doğru değerleri veri tabanına kaydetmeniz gerekir.


Evet, bu ilginç bir çözüm, ancak maalesef müşteri tarafında zayıf bir internet bağlantımız var ve bize bir sürü dosya göndermesi gerekiyor. bu yüzden bu çözümü daha önce reddettik.
Jahan

Lütfen müşterinin yükleyeceği veri hacmini bana bildirir misiniz?
Amit Bagga

Birkaç dosya, neredeyse on, her biri yaklaşık 1 MB.
Cihan

5

Sihirbazlar, basit bir modeli işlemenin yalnızca basit adımlarıdır. Bir sihirbaz için birden fazla model oluşturmanın bir nedeni yoktur. Tek yapmanız gereken, tek bir model oluşturmak ve bunu tek bir denetleyicideki eylemler arasında aktarmaktır.

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

Yukarıdaki coed aptalca basittir, bu yüzden oradaki alanlarınızı değiştirin. Ardından, sihirbazımızı başlatan basit bir işlemle başlıyoruz.

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

Bu, "WizardStep1.cshtml görünümünü çağırır (bu jilet kullanılıyorsa). İsterseniz şablon oluşturma sihirbazını kullanabilirsiniz. Gönderiyi farklı bir eyleme yönlendireceğiz.

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

Burada dikkat edilmesi gereken nokta, bunu farklı bir eyleme göndereceğimizdir; WizardStep2 eylemi

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

Bu eylemde modelimizin geçerli olup olmadığını kontrol ediyoruz ve eğer öyleyse onu WizardStep2.cshtml görünümümüze gönderiyoruz, aksi takdirde doğrulama hataları ile birinci adıma geri gönderiyoruz. Her adımda bir sonraki adıma gönderiyoruz, o adımı doğruluyoruz ve devam ediyoruz. Şimdi bazı bilgili geliştiriciler, adımlar arasında [Gerekli] özniteliklerini veya diğer veri açıklamalarını kullanırsak bunun gibi adımlar arasında hareket edemeyeceğimizi söyleyebilir. Ve haklısınız, bu yüzden henüz kontrol edilmemiş öğelerdeki hataları kaldırın. aşağıdaki gibi.

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

Son olarak modeli bir kez veri deposuna kaydederdik. Bu ayrıca bir sihirbazı başlatan ancak tamamlamayan bir kullanıcının eksik verileri veritabanına kaydetmemesini de engeller.

Umarım bu sihirbaz uygulama yöntemini, kullanımı ve bakımı daha önce bahsedilen yöntemlerden çok daha kolay bulursunuz.

Okuduğunuz için teşekkürler.


bunu deneyebileceğim eksiksiz bir çözümde var mı? Teşekkürler
mpora

5

Bu gereksinimleri karşılamada kendi yöntemimi paylaşmak istedim. SessionState'i hiç kullanmak istemedim, istemci tarafında işlenmesini de istemedim ve serileştirme yöntemi projeme dahil etmek istemediğim MVC Futures'ı gerektiriyor.

Bunun yerine, modelin tüm özelliklerini yineleyecek ve her biri için özel bir gizli öğe oluşturacak bir HTML Yardımcısı oluşturdum. Karmaşık bir özellik ise, üzerinde özyinelemeli olarak çalışacaktır.

Formunuzda, her "sihirbaz" adımında yeni model verileriyle birlikte kontrolöre gönderilecektir.

Bunu MVC 5 için yazdım.

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

Şimdi "sihirbazınızın" tüm adımları için aynı temel modeli kullanabilir ve "Adım 1,2,3" model özelliklerini bir lambda ifadesi kullanarak @ Html.HiddenClassFor yardımcısına geçirebilirsiniz.

İsterseniz her adımda bir geri düğmesine bile sahip olabilirsiniz. Formunuzda, formaction özniteliğini kullanarak onu denetleyicideki bir StepNBack eylemine gönderecek bir geri düğmesine sahip olmanız yeterlidir. Aşağıdaki örneğe dahil değil, sadece sizin için bir fikir.

Her neyse, işte temel bir örnek:

İşte MODELİNİZ

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

İşte DENETLEYİCİNİZ

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

GÖRÜŞLERİNİZ

Aşama 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>
}

Adım 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>
}

Aşama 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
Görünüm modelini ve denetleyiciyi sağlayarak çözümünüzü daha fazla açıklayabilir misiniz?
Tyler Durden

2

@ Darin'in cevabından daha fazla bilgi ekleniyor.

Ya her adım için ayrı bir tasarım stiliniz varsa ve her birini ayrı kısmi görünümde tutmak istiyorsanız ya da her adım için birden fazla özelliğiniz varsa ne olur?

Kullanım sırasında Html.EditorForkısmi görünümü kullanma sınırlamamız var.

Aşağıdaki Sharedklasör altında 3 Kısmi Görünüm oluşturun :Step1ViewModel.cshtml , Step3ViewModel.cshtml , Step3ViewModel.cshtml

Kısacası, sadece 1. patent görüşünü gönderiyorum, diğer adımlar Darin'in cevabı ile aynı.

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

    }
}

Daha iyi bir çözüm varsa, lütfen başkalarına bildirmek için yorum yapın.


-9

Seçeneklerden biri, her adımda toplanan verileri depolayacak bir dizi özdeş tablo oluşturmaktır. Son adımda her şey yolunda giderse, geçici verileri kopyalayıp depolayarak gerçek varlığı oluşturabilirsiniz.

Diğeri, Value Objectsher adım için oluşturmak ve sonra Cacheveya içinde depolamaktır Session. Sonra her şey yolunda giderse, Etki Alanı nesnenizi onlardan oluşturabilir ve kaydedebilirsiniz.


1
Olumsuz oy verenler de gerekçelerini açıklasalar iyi olur.
Martin

Size oy vermedim, Ama cevabınız soruyla tamamen alakasız. OP, arkadaki yanıtı nasıl ele alacağınıza yanıt verirken, sihirbazın nasıl oluşturulacağını soruyor.
Dementic

1
genellikle oy kullanmam, ama oy verdiğimde, olumlu oy kullandığından emin oluyorum :-)
Suhail Mumtaz Awan
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.