multi-step registration process issues in asp.net mvc (split viewmodels, single model)

后端 未结 7 1182
走了就别回头了
走了就别回头了 2020-11-22 17:08

I have a multi-step registration process, backed by a single object in domain layer, which have validation rules defined on properties.

7条回答
  •  夕颜
    夕颜 (楼主)
    2020-11-22 17:24

    I wanted to share my own way of handling these requirements. I did not want to use SessionState at all, nor did I want it handled client side, and the serialize method requires MVC Futures which I did not want to have to include in my project.

    Instead I built an HTML Helper that will iterate through all of the properties of the model and generate a custom hidden element for each one. If it is a complex property then it will run recursively on it.

    In your form they will be posted to the controller along with the new model data at each "wizard" step.

    I wrote this for 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(this HtmlHelper html, Expression> expression)
            {
                return HiddenClassFor(html, expression, null);
            }
    
            public static MvcHtmlString HiddenClassFor(this HtmlHelper html, Expression> 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(HtmlHelper html, LambdaExpression expression, ModelMetadata metaData, IDictionary 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(this HtmlHelper html, Expression> expression)
            {
                return HiddenArrayFor(html, expression, null);
            }
    
            public static MvcHtmlString HiddenArrayFor(this HtmlHelper html, Expression> 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(HtmlHelper html, LambdaExpression expression, ModelMetadata metaData, IDictionary 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(this HtmlHelper html, Expression> expression)
            {
                return MinHiddenFor(html, expression, null);
            }
    
            public static MvcHtmlString MinHiddenFor(this HtmlHelper html, Expression> 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 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 _pair in htmlAttributes)
                    {
                        _input.MergeAttribute(_pair.Key, _pair.Value.ToString(), true);
                    }
                }
    
                return new MvcHtmlString(_input.ToString(TagRenderMode.SelfClosing));
            }
        }
    }
    

    Now for all steps of your "wizard" you can use the same base model and pass the "Step 1,2,3" model properties into the @Html.HiddenClassFor helper using a lambda expression.

    You can even have a back button at each step if you want to. Just have a back button in your form that will post it to a StepNBack action on the controller using the formaction attribute. Not included in the below example but just an idea for you.

    Anyways here is a basic example:

    Here is your MODEL

    public class WizardModel
    {
        // you can store additional properties for your "wizard" / parent model here
        // these properties can be saved between pages by storing them in the form using @Html.MinHiddenFor(m => m.WizardID)
        public int? WizardID { get; set; }
    
        public string WizardType { get; set; }
    
        [Required]
        public Step1 Step1 { get; set; }
    
        [Required]
        public Step2 Step2 { get; set; }
    
        [Required]
        public Step3 Step3 { get; set; }
    
        // if you want to use the same model / view / controller for EDITING existing data as well as submitting NEW data here is an example of how to handle it
        public bool IsNew
        {
            get
            {
                return WizardID.HasValue;
            }
        }
    }
    
    public class Step1
    {
        [Required]
        [MaxLength(32)]
        [Display(Name = "First Name")]
        public string FirstName { get; set; }
    
        [Required]
        [MaxLength(32)]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
    }
    
    public class Step2
    {
        [Required]
        [MaxLength(512)]
        [Display(Name = "Biography")]
        public string Biography { get; set; }
    }
    
    public class Step3
    {        
        // lets have an array of strings here to shake things up
        [Required]
        [Display(Name = "Your Favorite Foods")]
        public string[] FavoriteFoods { get; set; }
    }
    

    Here is your CONTROLLER

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

    Here are your VIEWS

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

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

    Step 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" },
            });
    
        
    }
    

提交回复
热议问题