problemas del proceso de registro de varios pasos en asp.net mvc (modelos de vista dividida, modelo único)

117

Tengo un proceso de registro de varios pasos , respaldado por un solo objeto en la capa de dominio , que tiene reglas de validación definidas en las propiedades.

¿Cómo debo validar el objeto de dominio cuando el dominio está dividido en muchas vistas y tengo que guardar el objeto parcialmente en la primera vista cuando se publica?

Pensé en usar Sessions, pero eso no es posible porque el proceso es largo y la cantidad de datos es alta, así que no quiero usar session.

Pensé en guardar todos los datos en una base de datos relacional en memoria (con el mismo esquema que la base de datos principal) y luego vaciar esos datos a la base de datos principal, pero surgieron problemas porque debería enrutar entre los servicios (solicitados en las vistas) que trabajan con el base de datos principal y base de datos en memoria.

Estoy buscando una solución elegante y limpia (más precisamente una buena práctica).

ACTUALIZACIÓN Y aclaración:

@Darin Gracias por su reflexiva respuesta, eso fue exactamente lo que hice hasta ahora. Pero, por cierto, tengo una solicitud que tiene muchos archivos adjuntos, diseño un, Step2Viewpor ejemplo, qué usuario puede cargar documentos de forma asincrónica, pero esos archivos adjuntos deben guardarse en una tabla con relación referencial a otra tabla que debería haberse guardado antes en Step1View.

Por lo tanto, debería guardar el objeto de dominio en Step1(parcialmente), pero no puedo, porque el objeto de dominio principal respaldado que se asigna parcialmente a un modelo de vista de Step1 no se puede guardar sin los accesorios que provienen de la conversión Step2ViewModel.

Jahan
fuente
@Jani, ¿alguna vez descubriste la parte de carga de esto? Me gustaría escoger tu cerebro. Estoy trabajando en este problema exacto.
Doug Chamberlain
1
La solución en este blog es bastante simple y directa. Utiliza divs como "pasos" cambiando su visibilidad y validación de jquery discreta.
Dmitry Efimenko

Respuestas:

229

Primero, no debería utilizar ningún objeto de dominio en sus vistas. Debería utilizar modelos de vista. Cada modelo de vista contendrá solo las propiedades requeridas por la vista dada, así como los atributos de validación específicos de esta vista. Entonces, si tiene un asistente de 3 pasos, esto significa que tendrá 3 modelos de vista, uno para cada paso:

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

    ...
}

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

    ...
}

y así. Todos esos modelos de vista podrían estar respaldados por un modelo de vista del asistente principal:

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

entonces podría tener acciones de controlador que representen cada paso del proceso del asistente y pasen el principal WizardViewModela la vista. Cuando esté en el primer paso dentro de la acción del controlador, puede inicializar la Step1propiedad. Luego, dentro de la vista, generaría el formulario que permite al usuario completar las propiedades sobre el paso 1. Cuando se envía el formulario, la acción del controlador aplicará las reglas de validación para el paso 1 únicamente:

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

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

Ahora, dentro de la vista del paso 2, puede usar el ayudante Html.Serialize de futuros MVC para serializar el paso 1 en un campo oculto dentro del formulario (una especie de ViewState si lo desea):

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

y dentro de la acción POST del paso 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);
}

Y así sucesivamente hasta que llegues al último paso donde tendrás el WizardViewModelrelleno con todos los datos. Luego, mapeará el modelo de vista a su modelo de dominio y lo pasará a la capa de servicio para su procesamiento. La capa de servicio podría realizar cualquier regla de validación por sí misma, etc.

También hay otra alternativa: usar javascript y poner todo en la misma página. Hay muchos complementos de jquery que brindan funcionalidad de asistente ( Stepy es uno bueno). Básicamente, se trata de mostrar y ocultar divs en el cliente, en cuyo caso ya no tendrá que preocuparse por el estado persistente entre los pasos.

Pero no importa qué solución elija, utilice siempre modelos de vista y realice la validación en esos modelos de vista. Mientras pegue atributos de validación de anotaciones de datos en sus modelos de dominio, tendrá muchas dificultades, ya que los modelos de dominio no se adaptan a las vistas.


ACTUALIZAR:

De acuerdo, debido a los numerosos comentarios, saco la conclusión de que mi respuesta no fue clara. Y debo estar de acuerdo. Permítanme intentar desarrollar más mi ejemplo.

Podríamos definir una interfaz que deberían implementar todos los modelos de vista de pasos (es solo una interfaz de marcador):

public interface IStepViewModel
{
}

luego definiríamos 3 pasos para el asistente donde cada paso, por supuesto, contendría solo las propiedades que requiere, así como los atributos de validación relevantes:

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

a continuación, definimos el modelo de vista del asistente principal, que consta de una lista de pasos y un índice de pasos actual:

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

Luego pasamos al controlador:

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

Un par de comentarios sobre este controlador:

  • La acción Index POST usa los [Deserialize]atributos de la biblioteca Microsoft Futures, así que asegúrese de haber instalado MvcContribNuGet. Esa es la razón por la que los modelos de vista deben decorarse con el[Serializable] atributo
  • La acción Index POST toma como argumento una IStepViewModelinterfaz, por lo que para que esto tenga sentido, necesitamos una carpeta de modelos personalizada.

Aquí está la carpeta de modelos asociada:

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

Esta carpeta utiliza un campo oculto especial llamado StepType que contendrá el tipo concreto de cada paso y que enviaremos en cada solicitud.

Este modelo de carpeta quedará registrado en Application_Start:

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

La última parte del rompecabezas que falta son las vistas. Aquí está la ~/Views/Wizard/Index.cshtmlvista principal :

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

Y eso es todo lo que necesita para que esto funcione. Por supuesto, si lo desea, puede personalizar la apariencia de algunos o todos los pasos del asistente definiendo una plantilla de editor personalizada. Por ejemplo, hagámoslo para el paso 2. Entonces definimos un ~/Views/Wizard/EditorTemplates/Step2ViewModel.cshtmlparcial:

@model Step2ViewModel

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

Así es como se ve la estructura:

ingrese la descripción de la imagen aquí

Por supuesto, hay margen de mejora. La acción Index POST se parece a s..t. Hay demasiado código en él. Una simplificación adicional implicaría mover todas las cosas de la infraestructura como el índice, la administración del índice actual, la copia del paso actual en el asistente, ... en otra carpeta de modelos. Para que finalmente terminemos con:

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

que es más cómo deberían verse las acciones POST. Dejo esta mejora para la próxima vez :-)

Darin Dimitrov
fuente
1
@Doug Chamberlain, uso AutoMapper para convertir entre mis modelos de vista y modelos de dominio.
Darin Dimitrov
1
@Doug Chamberlain, consulte mi respuesta actualizada. Espero que deje las cosas un poco más claras que mi publicación inicial.
Darin Dimitrov
20
+1 @Jani: realmente necesitas darle a Darin los 50 puntos por esta respuesta. Es muy completo. Y logró reiterar la necesidad de usar ViewModel y no modelos de dominio ;-)
Tom Chantler
3
No puedo encontrar el atributo Deserialize en ninguna parte ... También en la página del codeplex de mvccontrib encuentro este 94fa6078a115 por Jeremy Skinner 1 de agosto de 2010 a las 5:55 PM 0 Quitar la carpeta obsoleta Deserialize ¿Qué me sugiere que haga?
Chuck Norris
2
Encontré un problema mientras que no nombré mis vistas Step1, Step2, etc ... Las mías se nombran con algo más significativo, pero no alfabético. Entonces, terminé obteniendo mis modelos en el orden incorrecto. Agregué una propiedad StepNumber a la interfaz IStepViewModel. Ahora puedo ordenar por esto en el método Initialize de WizardViewModel.
Jeff Reddy
13

Para complementar la respuesta de Amit Bagga, encontrará a continuación lo que hice. Incluso si es menos elegante, encuentro esta forma más simple que la respuesta de Darin.

Controlador :

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

Modelos:

 [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
fuente
11

Le sugiero que mantenga el estado de Proceso completo en el cliente usando Jquery.

Por ejemplo, tenemos un proceso de asistente de tres pasos.

  1. El usuario se presenta con el Step1 en el que tiene un botón etiquetado como "Siguiente"
  2. Al hacer clic en Siguiente, hacemos una solicitud Ajax y creamos un DIV llamado Step2 y cargamos el HTML en ese DIV.
  3. En el Step3 tenemos un botón etiquetado como "Finalizado" al hacer clic en el botón publicar los datos usando $ .post call.

De esta manera, puede construir fácilmente su objeto de dominio directamente desde los datos de publicación del formulario y, en caso de que los datos tengan errores, devuelva un JSON válido que contenga todos los mensajes de error y los muestre en un div.

Divide los pasos

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

Lo anterior es solo una demostración que lo ayudará a lograr el resultado final. En el paso final, debe crear el objeto de dominio y completar los valores correctos del objeto del asistente y almacenar en la base de datos.

Amit Bagga
fuente
Sí, esa es una solución interesante, pero lamentablemente tenemos una mala conexión a Internet en el lado del cliente, y él / ella debería enviarnos un montón de archivos. por lo que rechazamos esa solución antes.
Jahan
¿Puede hacerme saber el volumen de datos que va a cargar el cliente?
Amit Bagga
Varios archivos, casi diez, cada uno de casi 1 MB.
Jahan
5

Los asistentes son simples pasos para procesar un modelo simple. No hay ninguna razón para crear varios modelos para un asistente. Todo lo que haría es crear un solo modelo y pasarlo entre acciones en un solo controlador.

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

La alumna anterior es estúpidamente simple, así que reemplace sus campos allí. A continuación, comenzamos con una acción simple que inicia nuestro asistente.

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

Esto llama a la vista "WizardStep1.cshtml (si usa una maquinilla de afeitar). Puede usar el asistente de creación de plantillas si lo desea. Simplemente redirigiremos la publicación a una acción diferente.

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

Lo importante es que publicaremos esto en una acción diferente; la acción WizardStep2

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

En esta acción verificamos si nuestro modelo es válido, y si es así lo enviamos a nuestra vista WizardStep2.cshtml, de lo contrario lo enviamos al paso uno con los errores de validación. En cada paso lo enviamos al siguiente paso, validamos ese paso y seguimos adelante. Ahora, algunos desarrolladores inteligentes podrían decir que no podemos movernos entre pasos como este si usamos atributos [Requeridos] u otras anotaciones de datos entre pasos. Y estaría en lo cierto, así que elimine los errores en los elementos que aún no se han verificado. como abajo.

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

Finalmente, guardaríamos el modelo una vez en el almacén de datos. Esto también evita que un usuario inicie un asistente pero no lo finalice para no guardar datos incompletos en la base de datos.

Espero que este método para implementar un asistente le resulte mucho más fácil de usar y mantener que cualquiera de los métodos mencionados anteriormente.

Gracias por leer.

Darroll
fuente
¿Tiene esto en una solución completa que pueda probar? Gracias
mpora
5

Quería compartir mi propia forma de manejar estos requisitos. No quería usar SessionState en absoluto, ni quería que se manejara en el lado del cliente, y el método de serialización requiere MVC Futures que no quería tener que incluir en mi proyecto.

En su lugar, construí un HTML Helper que iterará a través de todas las propiedades del modelo y generará un elemento oculto personalizado para cada una. Si es una propiedad compleja, se ejecutará de forma recursiva en ella.

En su formulario, se publicarán en el controlador junto con los datos del nuevo modelo en cada paso del "asistente".

Escribí esto para 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));
        }
    }
}

Ahora, para todos los pasos de su "asistente", puede utilizar el mismo modelo base y pasar las propiedades del modelo "Paso 1,2,3" al ayudante @ Html.HiddenClassFor utilizando una expresión lambda.

Incluso puede tener un botón de retroceso en cada paso si lo desea. Solo tenga un botón de retroceso en su formulario que lo publicará en una acción StepNBack en el controlador usando el atributo de formación. No se incluye en el siguiente ejemplo, pero es solo una idea para ti.

De todos modos, aquí hay un ejemplo básico:

Aquí está tu MODELO

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

Aquí está tu CONTROLADOR

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

Aquí están tus VISTAS

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

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

Paso 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>
}
ArcadeRenegado
fuente
1
¿Podría aclarar aún más su solución proporcionando el modelo de vista y el controlador?
Tyler Durden
2

Añadiendo más información de la respuesta de @ Darin.

¿Qué pasa si tiene un estilo de diseño separado para cada paso y desea mantener cada uno en una vista parcial separada o qué sucede si tiene varias propiedades para cada paso?

Mientras lo usamos, Html.EditorFortenemos una limitación para usar la vista parcial.

Cree 3 vistas parciales en la Sharedcarpeta llamada:Step1ViewModel.cshtml , Step3ViewModel.cshtml , Step3ViewModel.cshtml

Por brevedad, acabo de publicar la primera vista parcial, otros pasos son los mismos que la respuesta de 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" />

    }
}

Si hay alguna solución mejor, comente para informar a otros.

shaijut
fuente
-9

Una opción es crear un conjunto de tablas idénticas que almacenarán los datos recopilados en cada paso. Luego, en el último paso, si todo va bien, puede crear la entidad real copiando los datos temporales y almacenarlos.

Otro es crear Value Objectspara cada paso y almacenar luego en Cacheo Session. Luego, si todo va bien, puede crear su objeto de dominio a partir de ellos y guardarlo

Amila Silva
fuente
1
Sería bueno que las personas que votan en contra también dieran su razón.
Martin
No te voté en contra, pero tu respuesta es completamente irrelevante para la pregunta. El OP pregunta cómo crear el asistente, mientras usted responde sobre cómo manejar la respuesta en la parte posterior.
Dementic
1
Normalmente no voto, pero cuando lo hago, me aseguro de que sea un voto positivo :-)
Suhail Mumtaz Awan