0

I have a user object:

namespace MySolution.Models
{
     public class MyUser
     {
        public Int32 Id { get; set; }
        [Required]
        public string CompanyName { get; set; }

        [Required]
        public string FirstName { get; set; }

        [Required]
        public string LastName { get; set; }

        [Required]
        [EmailAddress]
        public string Email { get; set; }
     }
}

Id is auto generated by SQL Server. I am using Dapper.

All these properties are required. However I want to split this into two parts - so the user is asked first for Email - then on the next 'page' for Company name, first name & last name.

The [HttpPost] controllers looks like this:

[HttpPost]
public ActionResult SignUp(MyUser myuser)
{
    if (ModelState.IsValid)
    {
        // Insert returns success
        if (MyUserRepo.InsertEmailPass(myuser))
        {
            // Successfully added user, go to next user section
            return RedirectToAction("SignUp2", myuser);
        }
        else
        {
            // Adding prospect failed
            ViewBag.Error = "Email already registered";
        }
    }

    return View(myuser);
} 


[HttpPost]
public ActionResult SignUp2(MyUser myuser)
{
    if (ModelState.IsValid)
    {
        // Insert returns success
        if (MyUserRepo.UpdateNameCopany(myuser))
        {
            // Successfully added myuser, go to thank you page
            return RedirectToAction("SignUpEnd");
        }
        else
        {
            // Adding prospect failed
            ViewBag.Error = "Something went wrong";
        }
    }
}

I want unobtrusive validation and all the built in MVC capabilities - such as if (ModelState.IsValid) - however, as I'm splitting this over 2 pages the model will never be valid on the first page - and unless I manually add email to the model on the second page there too (whereas all I need to do is a SQL update on first name, last name & company name (not email)- so adding email to the SQL un-necessarily).

This all feels rather 'hacky'. How can I do this and still use built in validation and ModelState etc?

I can find nothing about this on Google.

My aim is to do this 'correctly', with minimal code, in a clear way (& best practice?).

EDIT: I now have these 2 view models:

namespace MyNamespace.ViewModels
{
    public class SignUpViewModelPage1
    {
        public int Id { get; set; }

        [Required]
        [EmailAddress]
        public string Email { get; set; }

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

    public class SignUpViewModelPage2
    {
        public int Id { get; set; }

        [Required]
        public string CompanyName { get; set; }

        [Required]
        public string FirstName { get; set; }

        [Required]
        public string LastName { get; set; }
    }
}

The controllers:

[HttpPost]
public ActionResult SignUp(SignUpViewModelPage1 svmp1)
{
    if (ModelState.IsValid)
    {
        int Id = senderRepo.InsertEmailPass(svmp1);
        // Insert failure returns -1
        if (Id != -1)
        {
            // Successfully added user - go to page 2, pass Id
            return RedirectToAction("SignUp2", new { Id = Id });
        }
        else
        {
            // Adding user failed - probably duplicate email - tell user & pass invalid model back
            ViewBag.Error = "Email already registered";
        }
    }

    return View(svmp1);
}

public ActionResult SignUp2(int Id)
{
    return View();
}

[HttpPost]
public ActionResult SignUp2(SignUpViewModelPage2 svmp2)
{
    if (ModelState.IsValid)
    {
        // Insert success returns true
        if (senderRepo.UpdateNameCopany(svmp2))
        {
            // Successfully added user - go to success page
            return RedirectToAction("SignUpEnd");
        }
        else
        {
            // Adding user failed - tell user
            ViewBag.Error = "Email already registered";
        }
    }
    return View();
}

Does this all look OK? Any obvious errors or bad ways of doing things?

thx

niico
  • 11,206
  • 23
  • 78
  • 161
  • 3
    Doing it correctly would require not using entity models as viewmodels in the first place. Then you can apply @nick's answer. – CodeCaster Jun 06 '16 at 15:00
  • 1
    Yes, definitely. View models are the way to go. You should never really post directly to your entity classes, anyways. Doing so creates a whole host of issues. – Chris Pratt Jun 06 '16 at 15:06
  • I agree. See [ASP.NET MVC Model vs ViewModel](http://stackoverflow.com/questions/4061440/asp-net-mvc-model-vs-viewmodel) for some theory about how all of this should fit together. – NightOwl888 Jun 06 '16 at 15:14
  • any code examples with an answer appreciated ;) – niico Jun 06 '16 at 15:20
  • Why never post directly to entity classes - this seems like creating a lot of duplication @ChrisPratt where it's not necessary to have a view model? The implication there is also that annotating your entity models directly is also therefore not necessary. – niico Jun 06 '16 at 15:28
  • 2
    Entity classes are and should be designed to represent a database table structure, which more often than not is *not* the same as what a view would need. Indeed, the only data annotations that should exist on your entity are the ones that apply to the database table backing it. Anything view-specific should go on a view model. – Chris Pratt Jun 06 '16 at 16:05
  • 1
    The main issue with posting to an entity directly, though, is that it encourages bad practices: things like using `[Bind]` to exclude properties from the post, and saving the posted entity directly instead of updating properties on a version pulled fresh from the database. – Chris Pratt Jun 06 '16 at 16:21
  • thx. The first page does contain an email - I try and insert it into the database to check it's unique. So I will try and create the record on the first page - and update on the second. How to persist the record? (hidden id field?). There will be a period imbetween the post of the first & second page where names + co name are empty in the db, but I don't think that can be a avoided neatly? – niico Jun 06 '16 at 17:19
  • If I want to get the inserted Id and pass it to the second view - whats the best way to do that? – niico Jun 06 '16 at 17:44
  • I have updated the question with suggested view models and controllers - looks good? thx. – niico Jun 07 '16 at 13:11

3 Answers3

2

Your model should reflect what is on your page. I would create 2 models one for each page and do the normal validation.I would then merge the result into one model if need be.

nick
  • 104
  • 5
  • exactly - (to OP) the whole problem is due to the controllers not matching to models (validation etc.) - and wanting to bind directly to your db model (that others mentioned). I.e., you're having a 'design issue' here, and you basically answered it yourself (the 'splitting' is causing it - so don't). This (answer) is simple but the best advice. – NSGaga-mostly-inactive Jun 06 '16 at 15:52
  • I have updated the question with suggested view models and controllers - looks good? thx. – niico Jun 07 '16 at 13:11
1

As others have mentioned, you should look at creating a ViewModel that represents your page inputs not your domain model.

From there, you have a few options:

1) Use something like bootstrap wizard that essentially hides/shows parts of your page until you submit. All of the properties of your single viewmodel are on your page, but just hidden by the wizard. This handles validation as well, pretty good stuff. Here is an example of it: Bootstrap Form Wizard Example. You can find more examples and download here.

2) Break up into several smaller ViewModels for each step of your forms.

I have recently used option 1 and it worked well for my project.

Papa Burgundy
  • 6,397
  • 6
  • 42
  • 48
  • thx - as I have an email address on page 1 - i need to try and add that to the database before doing anything else. How can I get the Id of the newly inserted record - and should that then be placed in a hidden field on page 2? – niico Jun 06 '16 at 17:46
  • Yes it is possible and yes you could place the return ID into a hidden field on the page. This may break up your ViewModels though if you are only validating an email address on your first action. Why do you need the ID before you can continue? If you are trying to just validate for uniqueness of the email, you can make an ajax that validates for you on the first step. Then allow them to continue. – Papa Burgundy Jun 06 '16 at 17:52
  • That would enable a race condition? I mean - super unlikely, but as a principle and good way of doing "this kind of thing" - my way is more 'correct'? I may use this technique in future - so best to get into good habits? – niico Jun 06 '16 at 21:23
  • Also - forgot to mention - the registration is split over 2 pages. Even if the user abandons page 2 I still want their email form page 1 (to email a reminder or take other action in future - they won't be lost if page 2 isn't immediately filled in). So I need email to immediately persist. The shorter a form is - the more likely someone is to fill it in - page 2 may put some people off initially. – niico Jun 21 '16 at 17:42
0

There are different approaches to this. If you want/need to do this through full page updates (as opposed to partial ones) you need to split your model on n parts, where n equals the steps of the wizard you like to present you users with. Then you need to come up with a way to store the data before actually register the user. In WebForms era I did this a lot by saving session relative data, which is not ready for the DB yet in hidden field(s) between round trips.

However I wouldn't recommend this approach as it somewhat clumsy and obsolete. Better approach is to design the wizard steps as partial views. Then on your login page designate a popup window and a button that reads Signup. There are plenty of JS libs offering popups. I use notorious Twitter Bootstrap or jQuery UI, but there are others if you like. So when the user click on Signup, you fed the designated popup with the first partial view and show it. You do this by making AJAX call back to the server. Of course the UI for the current step needs to provide button which loads the next step. You can even design one partial view for your model, load it at once, but show different parts of it, depending on the step we're currently in.

All this requires javascript. You can - correction - you should employ jQuery and request the partial views through AJAX. You can store each step's data in a local javascript variable. At the end of the wizard inspect the collected data and make another AJAX call to the sign-up method. On the server side, you don't have to change your model and leave the annotations in place.

You can google on AJAX, jQuery, partial views and so on. The internet is full of information on the subject.

Bozhidar Stoyneff
  • 3,576
  • 1
  • 18
  • 28