Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
356 views
in Technique[技术] by (71.8m points)

asp.net mvc - How to completely decouple view and model in MVC

In my first example I have a Model like so:

public class GuestResponse
{
    [Required(ErrorMessage = "Please enter your name")]
    public string Name { get; set; }

    [Required(ErrorMessage = "Please enter your email")]
    [RegularExpression(".+\@.+\..+", ErrorMessage = "Please enter a valid email address")]
    public string Email { get; set; }

    public string Phone { get; set; }

    [Required(ErrorMessage = "Please specify whether you'll attend")]
    public bool? WillAttend { get; set; }
}

The Controller:

public class HomeController : Controller
{
    public ViewResult Index()
    {
        ViewData["greeting"] = (DateTime.Now.Hour < 12 ? "Good morning" : "Good afternoon");
        return View();
    }

    [HttpGet]
    public ViewResult RsvpForm()
    {
        return this.View();
    }

    [HttpPost]
    public ViewResult RsvpForm(GuestResponse guestResp)
    {
        if (ModelState.IsValid)
        {
            return this.View("Thanks", guestResp);
        }
        else
        {
            return this.View();
        }
    }
}

And a View:

@model GuestResponse
<body>
<div>
    <h1>
        Thank you,
        <%: Model.Name  %>.</h1>
    <p>
        <% if (Model.WillAttend == true)
           {  %>
        It's great that you're coming. The drinks are already in
        the fridge!
        <% }
           else
           {  %>
        Sorry to hear you can't make it, but thanks for letting
        us know.
        <% } %>
    </p>
</div>

What seems strange to me, is that the View is tightly coupled with the Model: it uses code like Model.WillAttend etc... So what happen if in a future time the Model changes? I should have to change all the snippets inside this particular View.

Suppose my view will show a registration page where I will show input for name, title, address1, address2 etc and all these fields will be bound to the model, but the model may not exist that time. So can I create a interface and model will implement that interface and the view will just import that interface instead of the model class? So create UI as a result IntelliSense will show name, title, address1, address2 etc when we type Model.?

What approach should I follow so two people separately can develop view and model? So when view will be created then model may not exist rather model will be created later. How it will be possible? Through an interface?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Separating your View From your View Model

Thinking deeply about this one for a second, it's not possible to decouple your View from your View Model. You can't start creating a web page without anticipating in some way or another what pieces of information are going to be displayed on the page and where - because that's precisely what writing HTML code IS. If you don't decide at least one of those two things, there isn't any HTML code to write. So if you have a page that displays information coming from your controller, you need to define a view.

The View Model that you pass to your view should represent only the data fields that are to be displayed for a single view (or partial view) only. It's not "decouple-able", because you will never require multiple implementations of it - it is free of logic and hence there is no other implementation of it. It's the other pieces of your application that require decoupling in order to make them reusable and maintainable.

Even if you used the dynamic ViewBag and used reflection to determine the properties that are contained within it to display your entire page dynamically, eventually you'd have to decide where that information is going to be displayed and in what order. If your are writing any of your HTML code anywhere other than within your view and related helpers, or executing anything other than display logic in your view, then you're probably breaking one of the fundamental principles of MVC.

All is not lost though, keep reading...

Developing a View Independently of the View Model

In terms two people separately developing your view and model independently (as you quite clearly asked in the question), it's completely fine to have a view with no model defined. Just remove the @model completely from the view, or comment it out ready to be uncommented later.

//@model RegistrationViewModel
<p>Welcome to the Registration Page</p>

Without a @model defined, you don't have to pass through a model from your controller to your view:

public class HomeController : Controller
{
    [HttpGet]
    public ActionResult Index()
    {
        // Return the view, without a view model
        return View();
    }
}

You can also use non-strongly typed versions of the the HTML helpers for MVC. So with a view @model defined, you might have written this:

@Html.LabelFor(m => m.UserName)
@Html.TextBoxFor(m => m.UserName)

Instead, use the versions without the For at the end of the name, these accept a string as a name instead of referring directly to your model:

@Html.Label("UserName")
@Html.TextBox("UserName")

You can update these later with the strongly typed versions of the helpers later on when you have a View Model finished for the page. This will make your code a bit more robust later on.


General Comments on Objects in ASP.NET MVC

On the back of the comments, I'll attempt to show you with code how I tend to lay out my code in MVC and the different objects that I use in order to separate things out... which will truly make your code more maintainable by multiple people. Sure, it is a bit of an investment in time, but it's well worth it in my opinion as you application grows out.

You should have different classes for different purposes, some cross layers and some reside in a specific layer and aren't accessed from outside those layers.

I normally have the following types of models with my MVC projects:

  • Domain Models - Models that represent the the rows in the database, I tend to manipulate these ONLY in my service layer because I use Entity Framework so I don't have a 'data access layer' as such.
  • DTOs - Data Transfer objects, for passing specific data between the Service Layer and UI Layer
  • View Models - Models that are just referenced within your view and controllers, you map your DTOs to these before you pass them to your view.

Here's how I make use of them (You asked for code, so here's an example that I just drummed together that is similar to yours, but just for a simple registration):

Domain Model

Here's a domain model that simply represents a User and it's columns as they are in the database. My DbContext uses domain models and I manipulate domain models in my Service Layer.

public User
{
    public string UserName { get; set; }
    public string Password { get; set; }
    public string Email { get; set; }
    public string Phone { get; set; }
}

Data Transfer Objects (DTOs)

Here are some data transfer objects that I map in my UI Layer in my controllers and pass to my Service Layer and vice versa. Look how clean they are, they should contain only the fields required for passing data back and forth between layers, each one should have a specific purpose, like to be received or returned by a specific method in your service layer.

public class RegisterUserDto()
{
    public string UserName { get; set; }
    public string Password { get; set; }
    public string Email { get; set; }
    public string Phone { get; set; }
}

public class RegisterUserResultDto()
{
    public int? NewUserId { get; set; }
}

View Models

Here's a view model which lives in my UI layer only. It is specific to a single view and is never touched within your service layer! You can use this for mapping the values that are posted back to your controller, but you don't have to - you could have a whole new model specifically for this purpose.

public class RegistrationViewModel()
{
    public string UserName { get; set; }
    public string Password { get; set; }
    public string Email { get; set; }
    public string Phone { get; set; }
}

Service Layer

Here's the code for the service layer. I have an instance of the DbContext which uses the Domain Models to represent the data. I map the response of the registration into a DTO that I created specifically for the response of the RegisterUser() method.

public interface IRegistrationService
{
    RegisterUserResultDto RegisterUser(RegisterUserDto registerUserDto);
}

public class RegistrationService : IRegistrationService
{
    public IDbContext DbContext;

    public RegistrationService(IDbContext dbContext)
    {
        // Assign instance of the DbContext
        this.DbContext = dbContext;
    }        

    // This method receives a DTO with all of the data required for the method, which is supposed to register the user
    public RegisterUserResultDto RegisterUser(RegisterUserDto registerUserDto)
    {
        // Map the DTO object ready for the data access layer (domain)
        var user = new User()
                   {
                       UserName = registerUserDto.UserName,
                       Password = registerUserDto.Password,
                       Email = registerUserDto.Email,
                       Phone = registerUserDto.Phone
                   };

        // Register the user, pass the domain object to your DbContext
        // You could pass this up to your Data Access LAYER if you wanted to, to further separate your concerns, but I tend to use a DbContext
        this.DbContext.EntitySet<User>.Add(user);
        this.DbContext.SaveChanges();

       // Now return the response DTO back
       var registerUserResultDto = RegisterUserResultDto()
       {
            // User ID generated when Entity Framework saved the `User` object to the database
            NewUserId = user.Id
       };

       return registerUserResultDto;
    }
}

Controller

In the controller we map a DTO to send up to the service layer and in return we receive a DTO back.

public class HomeController : Controller
{
    private IRegistrationService RegistrationService;

    public HomeController(IRegistrationService registrationService)
    {
        // Assign instance of my service
        this.RegistrationService = registrationService;
    }

    [HttpGet]
    public ActionResult Index()
    {
        // Create blank view model to pass to the view
        return View(new RegistrationViewModel());
    }

    [HttpPost]
    public ActionResult Index(RegistrationViewModel requestModel)
    {
        // Map the view model to the DTO, ready to be passed to service layer
        var registerUserDto = new RegisterUserDto()
        {
            UserName = requestModel.UserName,
            Password = requestModel.Password,
            Email = requestModel.Email,
            Phone = requestModel.Phone
        }

        // Process the information posted to the view
        var registerUserResultDto = this.RegistrationService.RegisterUser(registerUserDto);

        // Check for registration result
        if (registerUserResultDto.Id.HasValue)
        {
            // Send to another page?
            return RedirectToAction("Welcome", "Dashboard");
        }

        // Return view model back, or map to another view model if required?
        return View(requestModel);
    }
}

View

@model RegistrationViewModel
@{
    ViewBag.Layout = ~"Views/Home/Registration.cshtml"
}

<h1>Registration Page</h1>
<p>Please fill in the fields to register and click submit</p>

@using (Html.BeginForm())
{

    @Html.LabelFor(x => x.UserName)
    @Html.TextBoxFor(x => x.UserName)

    @Html.LabelFor(x => x.Password)
    @Html.PasswordFor(x => x.Password)

    @Html.LabelFor(x => x.Email)
    @Html.TextBoxFor(x => x.Email)

    @Html.LabelFor(x => x.Phone)
    @Html.TextBoxFor(x => x.Phone)

    <input type="submit" value="submit" />
}

Duplication of Code

You are quite right about what you said in the comments, there is a bit (or a lot) of object code duplication, but if you think about it, you need to do this if you want to truly separate them out:

View Models != Domain Models

In many cases the information you display on a view doesn't contain information from only a single domain model and some of the information should never make it down to your UI Layer because it should never be displayed to the application user - such as the hash of a user's password.

In your original example you have the model GuestResponse with validation attributes decorating the fields. If you made your GuestResponse object double up as a Domain Model and View Model, you have polluted your domain models with attributes that may only be relevant to your UI Layer or even a single page!

If you don't have tailored DTOs for your service layer methods, then when you add a new field to whatever class it is that the method returns, you'll have to update all of the other methods that return that particular class to include that piece of information too. Chances are you'll hit a point where you add a new field is only relevant or calculated in the one single method you're updating to returning it from? Having a 1:1 relat


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...