How to completely decouple view and model in MVC

后端 未结 4 549
伪装坚强ぢ
伪装坚强ぢ 2020-12-11 14:20

In my first example I have a Model like so:

public class GuestResponse
{
    [Required(ErrorMessage = \"Please enter your name\")]
    public string Name { g         


        
相关标签:
4条回答
  • Use a ViewModel

    A ViewModel is a class that is created specifically for the page. LoginViewModel for example.

    What is a view model

    The point of the ViewModel is to support the separation of concern, which is the main selling point of MVC. This separation allows your View and Models to safely evolve independently. You can protect the Model from any changes in the View, and you can protect the View from any changes in the Model.

    MVC on its own is very limited. Other layers have to be introduced in order to meet increasing complexity. Such as the ViewModel, Service, Domain, Infrastructure, Etc.

    0 讨论(0)
  • 2020-12-11 14:32

    What you have noticed is one of the weaknesses of the MVC model. And it's why some people are moving away from it. Instead we have a lot of other patterns that better decouple them.

    In MVVM you have a view and a model, but in between you have the modelview which should hold all the presentation logic. This completely decouples the view and the model.

    You can also do this with an interface, but that's another form of decoupling. Even if you decouple the classes you still have dependencies between the layers (the view layer and the model layer will have some link).

    0 讨论(0)
  • 2020-12-11 14:34

    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 relationship for DTO and service methods makes updating it a breeze, you don't have to worry about other methods that use the same DTO class.

    Also, if you think about it, having a single purpose class (DTO) especially written to return specific pieces of information back from a method on your service layer, you can glance at the returning class and understand exactly what it is that it is going to return. Whereas if you just bung in an object that 'fits the bill' like one your domain models which represents EVERYTHING in a row of one of your database tables, you don't know which pieces of information are relevant for that particular method and you're probably bringing back pieces of information that you don't need.

    If you use a Domain Model as your View Model, if you're not careful you can leave yourself open to overposting attacks. If someone who uses your application guesses the name of an additional field on your class, even if you don't provide a form element for it in the view anyone can post that value and have it save to the database. Having a View Model that only has fields tailored to your specific view means that you can restrict what will be processed on the server side without any special jiggery pokery. Oh, and you can see exactly what will be returned from your view without examining the view itself. Any view model sharing really makes things confusing when you're trying to work out what is and isn't supposed to be displayed or posted back from a view.

    There are loads of other reasons, I could go on all day on this topic it would seem. :P.

    I hope that helps to clear up a few things. Naturally this is all up for debate and I welcome it!

    0 讨论(0)
  • 2020-12-11 14:36

    My personal opinion is that you should go without a viewmodel. If what you're posting is exactly what you are going to store in the db why have a viewmodel? It's just another piece of code to maintain. For me that is duplication. Of course if you need to post something else then you model you always need a viewmodel. I'm not saying you should never have one. Just not when you don't need it. All arguments i've heard to always have a viewmodel is to avoid imaginary problems that may occur in the future. This fall under yagni. If and when you have a problem you can always create a viewmodel and replace your model for it in the controller. This is not impossible in any way. Quite simple actually. The typical case where i think you should try going without viewmodels is when you have a lot of crud type pages with a model, view and controller mapping to eachother. A viewmodel for each view just makes the application difficult to maintain since you need to change stuff in two places, and handle the complexity of mapping. Just don't put view related code in the model. If you need that, you need a viewmodel.

    0 讨论(0)
提交回复
热议问题