Amin K Amin K - 7 months ago 387
Javascript Question

Implementing Jquery Ajax in Multi-step register forms based on splited viewmodels in ASP.NET MVC5

I've used Darin-Dimitrov's approach for making a multi-step register form which is explained Here and it works fine.
Now i want to handle submit events for Previous,Next and Finish buttons using jquery ajax instead of Html.Beginform().

Notes:


  • I'm using MVC 5 with .NET 4.5.2

  • I've fileupload and datetime properties in my second step viewmodel.



Here is my viewmodel

[Serializable]
public class RegisterWizardViewModel
{


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

public interface IStepViewModel
{

}

[Serializable]
public class RegisterStep1ViewModel : IStepViewModel
{
[Required]
[EmailAddress]
public string Email { get; set; }

[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
public string Password { get; set; }

[DataType(DataType.Password)]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }

}

[Serializable]
public class RegisterStep2ViewModel : IStepViewModel
{

[Display(Name = "FirstName", ResourceType = typeof(Resources.Resources))]
public string FirstName { get; set; }

[Display(Name = "LastName", ResourceType = typeof(Resources.Resources))]
public string LastName { get; set; }

[NonSerialized]
private HttpPostedFileBase _file;
public HttpPostedFileBase File
{
get
{
return _file;
}
set
{
_file = value;
}
}

[Display(Name = "BirthDay", ResourceType = typeof(Resources.Resources))]
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy/MM/dd}", ApplyFormatInEditMode = true)]
public DateTime? BirthDay { get; set; }

[Display(Name = "NationalCode", ResourceType = typeof(Resources.Resources))]
public int NationalCode { get; set; }

[Display(Name = "Gender", ResourceType = typeof(Resources.Resources))]
public bool IsMale { get; set; }

[Display(Name = "Mobile", ResourceType = typeof(Resources.Resources))]
public string MobilePhone { get; set; }

[Display(Name = "Country", ResourceType = typeof(Resources.Resources))]
public string Country { get; set; }

[Display(Name = "Address", ResourceType = typeof(Resources.Resources))]
public string Address { get; set; }

[MustBeTrue]
public bool CaptchaValid { get; set; }
}

}


Here is My Controller

[AllowAnonymous]
public ActionResult Index()
{
var wizard = new RegisterWizardViewModel();
wizard.Initialize();
return View(wizard);
}

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Index([Deserialize] RegisterWizardViewModel wizard, RegisterWizardViewModel.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
{

var model1 = wizard.Steps[0] as RegisterWizardViewModel.RegisterStep1ViewModel;
var model2 = wizard.Steps[1] as RegisterWizardViewModel.RegisterStep2ViewModel;


var uploadedFile = (model2.File != null && model2.File.ContentLength > 0) ? new byte[model2.File.InputStream.Length] : null;
if (uploadedFile != null)
{
model2.File.InputStream.Read(uploadedFile, 0, uploadedFile.Length);
}

var user = new ApplicationUser { UserName = model1.Email, Email = model1.Email, FirstName = model2.FirstName, LastName = model2.LastName, Image = uploadedFile , BirthDay = model2.BirthDay, IsMale = model2.IsMale, NationalCode = model2.NationalCode, MobilePhone = model2.MobilePhone, Country = model2.Country, Address = model2.Address };
var result = UserManager.Create(user, model1.Password);
if (result.Succeeded)
{
SignInManager.SignIn(user, isPersistent: false, rememberBrowser: false);

return Json(new { response = "Redirect", url = Url.Action("Index", "Home") });
}
else
{
AddErrors(result);
}

}

}
else if (!string.IsNullOrEmpty(Request["prev"]))
{
wizard.CurrentStepIndex--;
}

return View(wizard);
}


And i've used this jquery code for submit via ajax call in my registerwizard index view.

@section scripts{
<script src="~/Scripts/jquery.unobtrusive-ajax.min.js"></script>
<script type="text/javascript">
$(function () {
$('form').on("submit", function (e) {
e.preventDefault;
if ($(this).valid()) {
$.ajax({
url: this.action,
type: this.method,
data: $(this).serialize(),
success: function (result) {
window.location = result.url;
}
});
}
return false;
});
});

</script>

}


Well now the problem is controller doesnt recognize which button has been pressed and
Request["next"]
or
Request["prev"]
always return null and in case of model state being valid for first step (Email,pass,confirmpass), controller directly goes to creating user. its worth mentioning that since i couldnt get to second step via ajax call i dont know whether if file upload and datetime property sent to controller without any problem or not.

Update:

Here is Index View

@using (Html.BeginForm("Index", "RegisterWizard", FormMethod.Post, new { @class = "form-horizontal", role = "form", enctype = "multipart/form-data" }))
{
@Html.AntiForgeryToken()

@Html.Serialize("wizard", Model)

@Html.Hidden("StepType", Model.Steps[Model.CurrentStepIndex].GetType())
@Html.EditorFor(x => currentStep, null, "")


if (Model.CurrentStepIndex > 0)
{
<div class="col-xs-9 col-sm-6 col-md-5 col-lg-5" style="padding-right:0px;">
<input type="submit" class="btn btn-default" value="Previous" name="prev" />

</div>


}

if (Model.CurrentStepIndex < Model.Steps.Count - 1)
{
<div class="col-xs-10 col-sm-8 col-md-6" style="">
<input type="submit" class="btn btn-default" value="Next" name="next" style="float:left;"/>

</div>


}
else
{
<div class="col-xs-3 col-sm-6 col-md-7 col-lg-7" style="padding-right:0px;">

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

</div>


}
}

Answer

Well, in case of someone faces this issue and probably has similar problem i explain my workaround.
main issue recognizing which button has been submitted the form:
i've added one hidden input named Button and change its name dynamically via jquery on click event on each button and finally modified my controller to get this value from request.
followings are partial view which holds entire form and post action of my controller.

@using Microsoft.Web.Mvc
@model Models.RegisterWizardViewModel

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

@using (Html.BeginForm("Index", "RegisterWizard", FormMethod.Post, new { @class = "form-horizontal", role = "form", enctype = "multipart/form-data", @id = "mainRWF" }))
{
    @Html.AntiForgeryToken()
    @Html.Hidden("Button")
    @Html.Serialize("wizard", Model)
    @Html.Hidden("StepType", Model.Steps[Model.CurrentStepIndex].GetType())
    @Html.EditorFor(x => currentStep, null, "")

    if (Model.CurrentStepIndex > 0)
        {
           <div >
              <input type="submit"  value="Previous" name="prev" />
           </div>
        }

    if (Model.CurrentStepIndex < Model.Steps.Count - 1)
        {
          <div >
            <input type="submit"  value="Next" name="next" />
          </div>
        }
    else
        {
          <div >
            <input type="submit"  value="Submit" name="finish"/>
          </div>
        }
 }

...

    [ValidateAntiForgeryToken]
    public async Task<ActionResult> Index([Deserialize]RegisterWizardViewModel wizard, RegisterWizardViewModel.IStepViewModel step)
    {
       wizard.Steps[wizard.CurrentStepIndex] = step;

        if (ModelState.IsValid)
        {
            if (Request.Form.GetValues("Button").Contains("next"))
            {
                wizard.CurrentStepIndex++;

            }
            else if (Request.Form.GetValues("Button").Contains("prev"))
            {
                wizard.CurrentStepIndex--;
            }
            else
            {
                //Do stuff with received values
                return Json(new { response = "Success" });

            }

       }
       else if (Request.Form.GetValues("Button").Contains("prev"))
       {
           // Even if validation failed we allow the user to
           // navigate to previous steps
           wizard.CurrentStepIndex--;
       }
       else if (!ModelState.IsValid)
       {
           // If we got this far, something failed, redisplay form with errors by inserting this into html() of success func of ajax
           return PartialView("_IndexPartial", wizard);
       }

    // return next step 
    return PartialView("_IndexPartial", wizard);

}

and obviously i got problems with file upload! and chose html5 formData to handle uploading as data option of ajax call for finish button.
i would appriciate comments on this solution in order to make it better.

Comments