masalah proses pendaftaran multi-langkah di asp.net mvc (model tampilan terpisah, model tunggal)

117

Saya memiliki proses pendaftaran multi-langkah , yang didukung oleh satu objek di lapisan domain , yang memiliki aturan validasi yang ditentukan pada properti.

Bagaimana cara memvalidasi objek domain saat domain dibagi di banyak tampilan, dan saya harus menyimpan sebagian objek di tampilan pertama saat diposting?

Saya berpikir untuk menggunakan Sesi tetapi itu tidak mungkin karena prosesnya lama dan jumlah datanya tinggi, Jadi saya tidak ingin menggunakan sesi.

Saya berpikir untuk menyimpan semua data dalam db dalam memori relasional (dengan skema yang sama dengan db utama) dan kemudian membuang data itu ke db utama tetapi masalah muncul karena saya harus merutekan antar layanan (diminta dalam tampilan) yang bekerja dengan db utama dan db dalam memori.

Saya mencari solusi yang elegan dan bersih (lebih tepatnya praktik terbaik).

PEMBARUAN DAN Klarifikasi:

@Darin Terima kasih atas balasan Anda yang bijaksana, Itulah yang telah saya lakukan sampai sekarang. Tapi kebetulan saya mendapat permintaan yang memiliki banyak lampiran di dalamnya, saya mendesain Step2Viewmisalnya pengguna mana yang dapat mengunggah dokumen di dalamnya secara asinkron, tetapi lampiran itu harus disimpan dalam tabel dengan relasi referensial ke tabel lain yang seharusnya disimpan sebelumnya di Step1View.

Jadi saya harus menyimpan objek domain di Step1(sebagian), Tapi saya tidak bisa, karena objek Core Domain yang didukung yang dipetakan sebagian ke ViewModel Step1 tidak dapat disimpan tanpa props yang berasal dari konversi Step2ViewModel.

Jahan
sumber
@Jani, Apakah Anda pernah mengetahui potongan unggahan ini? Saya ingin memilih otak Anda. Saya sedang mengerjakan masalah ini.
Doug Chamberlain
1
Solusi di blog ini cukup sederhana dan mudah. Ini menggunakan div sebagai "langkah" dengan mengubah visibilitas dan validasi jquery yang tidak mengganggu.
Dmitry Efimenko

Jawaban:

229

Pertama, Anda tidak boleh menggunakan objek domain apa pun dalam tampilan Anda. Anda harus menggunakan model tampilan. Setiap model tampilan hanya akan berisi properti yang diperlukan oleh tampilan yang diberikan serta atribut validasi khusus untuk tampilan yang diberikan ini. Jadi jika Anda memiliki wizard 3 langkah, ini berarti Anda akan memiliki 3 model tampilan, satu untuk setiap langkah:

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

    ...
}

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

    ...
}

dan seterusnya. Semua model tampilan tersebut dapat didukung oleh model tampilan wizard utama:

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

maka Anda bisa memiliki tindakan pengontrol yang merender setiap langkah dari proses wizard dan meneruskan main WizardViewModelke tampilan. Saat Anda berada di langkah pertama di dalam tindakan pengontrol, Anda dapat menginisialisasi Step1properti. Kemudian di dalam tampilan Anda akan menghasilkan formulir yang memungkinkan pengguna untuk mengisi properti tentang langkah 1. Saat formulir dikirimkan, tindakan pengontrol akan menerapkan aturan validasi untuk langkah 1 saja:

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

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

Sekarang di dalam tampilan langkah 2 Anda bisa menggunakan Html.Serialize helper dari MVC futures untuk membuat serialisasi langkah 1 ke dalam bidang tersembunyi di dalam formulir (semacam Kondisi Tampilan jika Anda mau):

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

dan di dalam tindakan POST langkah2:

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

Begitu seterusnya hingga Anda mencapai langkah terakhir di mana Anda akan WizardViewModeldiisi dengan semua data. Kemudian Anda akan memetakan model tampilan ke model domain Anda dan meneruskannya ke lapisan layanan untuk diproses. Lapisan layanan mungkin melakukan aturan validasi itu sendiri dan seterusnya ...

Ada juga alternatif lain: menggunakan javascript dan meletakkan semua di halaman yang sama. Ada banyak plugin jquery di luar sana yang menyediakan fungsionalitas wizard ( Stepy bagus). Ini pada dasarnya adalah masalah menampilkan dan menyembunyikan div pada klien sehingga Anda tidak perlu lagi khawatir tentang status persisten di antara langkah-langkah tersebut.

Tapi apa pun solusi yang Anda pilih, selalu gunakan model tampilan dan lakukan validasi pada model tampilan tersebut. Selama Anda menempelkan atribut validasi anotasi data pada model domain Anda, Anda akan berjuang sangat keras karena model domain tidak disesuaikan dengan tampilan.


MEMPERBARUI:

Oke, karena banyaknya komentar saya menarik kesimpulan bahwa jawaban saya tidak jelas. Dan saya harus setuju. Jadi izinkan saya mencoba mengembangkan contoh saya lebih jauh.

Kita bisa mendefinisikan antarmuka yang harus diterapkan oleh semua model tampilan langkah (ini hanya antarmuka penanda):

public interface IStepViewModel
{
}

kemudian kami akan menentukan 3 langkah untuk wizard di mana setiap langkah tentu saja hanya akan berisi properti yang diperlukan serta atribut validasi yang relevan:

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

selanjutnya kita mendefinisikan model tampilan wizard utama yang terdiri dari daftar langkah-langkah dan indeks langkah saat ini:

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

Kemudian kita beralih ke pengontrol:

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

Beberapa komentar tentang pengontrol ini:

  • Tindakan Index POST menggunakan [Deserialize]atribut dari pustaka Microsoft Futures jadi pastikan Anda telah menginstal MvcContribNuGet. Itulah alasan mengapa model tampilan harus didekorasi dengan [Serializable]atribut
  • Tindakan Index POST mengambil argumen sebagai IStepViewModelantarmuka sehingga agar masuk akal kita memerlukan pengikat model kustom.

Berikut pengikat model terkait:

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

Binder ini menggunakan bidang tersembunyi khusus yang disebut StepType yang akan berisi tipe konkret dari setiap langkah dan yang akan kami kirimkan pada setiap permintaan.

Pengikat model ini akan didaftarkan di Application_Start:

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

Bagian terakhir dari teka-teki yang hilang adalah pandangannya. Berikut ~/Views/Wizard/Index.cshtmltampilan utamanya :

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

Dan hanya itu yang Anda butuhkan agar ini berhasil. Tentu saja jika Anda mau, Anda dapat mempersonalisasi tampilan dan nuansa beberapa atau semua langkah wizard dengan menentukan template editor kustom. Misalnya mari kita lakukan untuk langkah 2. Jadi kita mendefinisikan ~/Views/Wizard/EditorTemplates/Step2ViewModel.cshtmlparsial:

@model Step2ViewModel

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

Seperti inilah strukturnya:

masukkan deskripsi gambar di sini

Tentu masih ada ruang untuk perbaikan. Tindakan Indeks POST terlihat seperti s..t. Terlalu banyak kode di dalamnya. Penyederhanaan lebih lanjut akan melibatkan pemindahan semua hal infrastruktur seperti indeks, manajemen indeks saat ini, penyalinan langkah saat ini ke wizard, ... ke pengikat model lain. Sehingga akhirnya kita mendapatkan:

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

yang lebih merupakan tampilan tindakan POST. Saya meninggalkan perbaikan ini untuk waktu berikutnya :-)

Darin Dimitrov
sumber
1
@Doug Chamberlain, saya menggunakan AutoMapper untuk mengkonversi antara model tampilan dan model domain.
Darin Dimitrov
1
@Doug Chamberlain, silakan lihat jawaban terbaru saya. Saya harap ini membuat segalanya sedikit lebih jelas daripada posting awal saya.
Darin Dimitrov
20
+1 @ Jani: Anda benar-benar harus memberi Darin 50 poin untuk jawaban ini. Ini sangat komprehensif. Dan dia berhasil mengulangi kebutuhan untuk menggunakan ViewModel dan bukan model Domain ;-)
Tom Chantler
3
Saya tidak dapat menemukan atribut Deserialize di mana pun ... Juga di halaman codeplex mvccontrib, saya menemukan 94fa6078a115 ini oleh Jeremy Skinner 1 Agustus 2010 jam 5:55 PM 0 Hapus pengikat Deserialize yang sudah usang Apa yang Anda sarankan untuk saya lakukan?
Chuck Norris
2
Saya menemukan masalah ketika saya tidak memberi nama pandangan saya Step1, Step2, dll ... Pandangan saya diberi nama sesuatu yang lebih bermakna, tetapi tidak sesuai abjad. Jadi, saya akhirnya mendapatkan model saya dalam urutan yang salah. Saya menambahkan properti StepNumber ke antarmuka IStepViewModel. Sekarang saya bisa mengurutkan berdasarkan ini dalam metode Inisialisasi WizardViewModel.
Jeff Reddy
13

Untuk melengkapi jawaban Amit Bagga, Anda akan menemukan apa yang saya lakukan di bawah ini. Meskipun kurang elegan, saya menemukan cara ini lebih sederhana daripada jawaban Darwin.

Pengontrol:

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

Model:

 [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; }
    }
Arno 2501
sumber
11

Saya menyarankan Anda untuk mempertahankan status Proses Lengkap pada klien menggunakan Jquery.

Sebagai Contoh, kami memiliki proses Wisaya Tiga Langkah.

  1. Pengguna disajikan dengan Langkah1 yang memiliki tombol Berlabel "Berikutnya"
  2. Saat Mengklik Berikutnya Kami membuat Permintaan Ajax dan Membuat DIV bernama Step2 dan memuat HTML ke DIV itu.
  3. Di Step3 kami memiliki Tombol berlabel "Selesai" saat Mengklik tombol kirim data menggunakan $ .post panggilan.

Dengan cara ini Anda dapat dengan mudah membangun objek domain Anda langsung dari data formulir posting dan jika data memiliki kesalahan, kembalikan JSON yang valid yang menahan semua pesan kesalahan dan menampilkannya dalam sebuah div.

Harap pisahkan Langkah-langkahnya

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

Di atas hanyalah demonstrasi yang akan membantu Anda mencapai hasil akhir. Pada Langkah Terakhir Anda harus membuat Objek Domain dan mengisi nilai yang benar dari Objek Wizard dan Menyimpan ke dalam database.

Amit Bagga
sumber
Ya, Itu solusi yang menarik, tapi sayangnya kami memiliki koneksi internet yang buruk di sisi klien, dan dia harus mengirimkan banyak file kepada kami. jadi kami menolak solusi itu sebelumnya.
Jahan
Bisakah Anda memberi tahu saya volume data yang akan diunggah klien.
Amit Bagga
Beberapa file, hampir sepuluh, masing-masing berukuran hampir 1 MB.
Jahan
5

Wizard hanyalah langkah sederhana dalam memproses model sederhana. Tidak ada alasan untuk membuat beberapa model untuk wizard. Yang akan Anda lakukan hanyalah membuat model tunggal dan meneruskannya di antara tindakan dalam satu pengontrol.

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

Mahasiswa di atas bodoh sederhana jadi gantilah bidang Anda di sana. Selanjutnya kita mulai dengan tindakan sederhana yang memulai wizard kita.

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

Ini memanggil tampilan "WizardStep1.cshtml (jika menggunakan pisau cukur). Anda dapat menggunakan wizard buat template jika Anda mau. Kami hanya akan mengarahkan posting ke tindakan yang berbeda.

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

Hal yang perlu diperhatikan adalah kami akan memposting ini ke tindakan yang berbeda; tindakan WizardStep2

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

Dalam tindakan ini kami memeriksa apakah model kami valid, dan jika demikian kami mengirimkannya ke tampilan WizardStep2.cshtml kami yang lain kami kirimkan kembali ke langkah pertama dengan kesalahan validasi. Di setiap langkah kami mengirimkannya ke langkah berikutnya, memvalidasi langkah itu dan melanjutkan. Sekarang beberapa pengembang yang paham mungkin berkata baik kita tidak dapat berpindah di antara langkah-langkah seperti ini jika kita menggunakan atribut [Wajib] atau anotasi data lainnya di antara langkah-langkah tersebut. Dan Anda akan benar, jadi hapus kesalahan pada item yang belum diperiksa. seperti dibawah ini.

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

Akhirnya kita akan menyimpan model sekali ke penyimpanan data. Ini juga mencegah pengguna yang memulai panduan tetapi tidak menyelesaikannya untuk tidak menyimpan data yang tidak lengkap ke database.

Saya harap metode penerapan wizard ini jauh lebih mudah digunakan dan dipelihara daripada metode yang disebutkan sebelumnya.

Terima kasih sudah membaca.

Sayang
sumber
apakah Anda memiliki ini dalam solusi lengkap yang dapat saya coba? Terima kasih
mpora
5

Saya ingin berbagi cara saya menangani persyaratan ini. Saya tidak ingin menggunakan SessionState sama sekali, saya juga tidak ingin menangani sisi klien, dan metode serialisasi memerlukan MVC Futures yang tidak ingin saya sertakan dalam proyek saya.

Sebagai gantinya, saya membuat Pembantu HTML yang akan mengulangi semua properti model dan menghasilkan elemen tersembunyi khusus untuk masing-masing properti. Jika itu adalah properti yang kompleks maka itu akan berjalan secara rekursif di atasnya.

Dalam formulir Anda, mereka akan diposting ke controller bersama dengan data model baru di setiap langkah "wizard".

Saya menulis ini untuk 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));
        }
    }
}

Sekarang untuk semua langkah "wizard" Anda, Anda dapat menggunakan model dasar yang sama dan meneruskan properti model "Step 1,2,3" ke dalam pembantu @ Html.HiddenClassFor menggunakan ekspresi lambda.

Anda bahkan dapat memiliki tombol kembali di setiap langkah jika Anda mau. Cukup memiliki tombol kembali di formulir Anda yang akan mengirimkannya ke tindakan StepNBack pada pengontrol menggunakan atribut formaction. Tidak termasuk dalam contoh di bawah ini tetapi hanya sebuah ide untuk Anda.

Bagaimanapun ini adalah contoh dasar:

Inilah MODEL Anda

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

Ini CONTROLLER Anda

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

Berikut adalah PANDANGAN Anda

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

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

LANGKAH 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>
}
ArcadeRenegade
sumber
1
Dapatkah Anda memperjelas solusi Anda lebih lanjut dengan memberikan model tampilan dan pengontrol?
Tyler Durden
2

Menambah info lebih lanjut dari jawaban @ Darin.

Bagaimana jika Anda memiliki gaya desain terpisah untuk setiap langkah dan ingin mempertahankan masing-masing dalam tampilan parsial terpisah atau bagaimana jika Anda memiliki beberapa properti untuk setiap langkah?

Saat menggunakan Html.EditorForkami memiliki batasan untuk menggunakan tampilan parsial.

Buat 3 Tampilan Parsial di bawah Sharedfolder bernama:Step1ViewModel.cshtml , Step3ViewModel.cshtml , Step3ViewModel.cshtml

Singkatnya saya baru saja memposting tampilan patial pertama, langkah-langkah lainnya sama dengan jawaban Darin.

Step1ViewModel.cs

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

  public string LastName { get; set; }

  public string PhoneNo { get; set; }

  public string EmailId { get; set; }

  public int Age { get; set; }

 }

Step1ViewModel.cshtml

 @model WizardPages.ViewModels.Step1ViewModel

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

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


</div>

Index.cshtml

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

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

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

<h3>Step @(Model.CurrentStepIndex + 1) out of @Model.Steps.Count</h3>

@using (Html.BeginForm())
{
    @Html.Serialize("wizard", Model)

    @Html.Hidden("StepType", Model.Steps[Model.CurrentStepIndex].GetType())

    @Html.Partial(""+ viewName + "", currentStep);

    if (Model.CurrentStepIndex > 0)
    {

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

    }

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

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

    }
    else
    {

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

    }
}

Jika ada solusi yang lebih baik, beri komentar untuk memberi tahu orang lain.

shaijut
sumber
-9

Salah satu opsinya adalah membuat sekumpulan tabel identik yang akan menyimpan data yang dikumpulkan di setiap langkah. Kemudian pada langkah terakhir jika semuanya berjalan dengan baik Anda dapat membuat entitas nyata dengan menyalin data sementara dan menyimpannya.

Lainnya adalah membuat Value Objectsuntuk setiap langkah dan menyimpannya kemudian di Cacheatau Session. Kemudian jika semuanya berjalan dengan baik Anda dapat membuat objek Domain Anda dari mereka dan menyimpannya

Amila Silva
sumber
1
Alangkah baiknya jika orang yang down vote juga memberikan alasannya.
Martin
Tidak merendahkan suara Anda, Tapi jawaban Anda sama sekali tidak relevan dengan pertanyaan itu. OP menanyakan tentang cara membuat wizard, sementara Anda menjawab tentang cara menangani respons di belakang.
Dementik
1
saya biasanya tidak memilih, tetapi ketika saya melakukannya, saya memastikan upvote-nya :-)
Suhail Mumtaz Awan