Codementor Events

View and View Model Modularization in a Large ASP.NET Core Application

Published Mar 15, 2019

It can be overwhelming to tackle building an entire V* (view, view model | controller) layer for an enterprise application, or even just a large web application in general. If you aren't careful, you can easily end up with a monolithic view layer that is impossible to maintain. We are all familiar with this monster, one with rampant logic in templates and inline scripting and styling. We are going to leave those discussions for another time and base the article off of a few working assumptions/goals:

  • Separate business logic from the view
  • Strongly type each partial view
  • Separate inline static files (JavaScript and CSS, I'm looking at you)
  • No full Razor implementation (No Razor Page with built in actions, we'll be using controllers)
  • Razor templating engine (we'll use some of the syntax provided by Razor)
Quick Disclaimer: This implementation approach may not be beneficial to everyone,
and it’s dependent upon the technology you’re implementing. This is just the
approach that I took to make my application more modular, in a clean and concise
way that is easy to maintain and scale. With that being said, let’s continue!

The Approach

Directory Structure and organization are very important parts of making your V* architecture more modular. For this approach, I break down the views into 2 main categories:

  • Top level views
  • Partial views

You can think of the top level view as the driver for your view; It handles including everything it needs to display one full section of your site. We’ll use the example of a Music Streaming Application Dashboard. I’ve created a simple mockup visual you can see below:
mock1.png

The above graphic shows a Music Streaming Dashboard View. The top level view acts as a driver to include all the partials it needs to display correctly. If we break this graphic up, we can see pretty clearly the different partials we’d need to make this work. Here’s how I have this view broken down:

- Dashboard
- - - Playlist
- - - - - Song (a list of songs inside the Playlist view)
- - - PlaybackControls

When we break it down in this way, we start to see how we can make the Dashboard more modular, as all the dashboard really does is provide the container in which the other partial views live (the driver-like analogy we used earlier).

Strongly Typed View Models

Strongly typing each view and partial view is the next step.

Why do this?

You want to strongly type your views and partial views because it provides a few specific advantages:

  • Compile time type checking
  • Data organization
    • exactly what data is supposed to accessible in each strongly typed view is known. This allows you to build your views and view models in parallel, making it easier to deconstruct what data is actually needed.
  • Maintainability
    • strongly typed views become inherently modular because they are split into logical chunks of common display items that are described by view models that describe exactly what data they need.
  • Modularity
    • When a section of an application is broken down, a hierarchy emerges and you’re able to deconstruct it into a top level view which includes partial views to drive the functionality as described by the figure above.
    • When you have modules separated like this, they become reusable elsewhere in your application if you ever need to recycle the functionality of a module.

How do we do this?

To do this, we need to back each View and Partial View with a proper ViewModel. Structurally, the ViewModels will look very similar to the organization of the Views. For example, to recycle a graphic from above:

- DashboardViewModel
- - - PlaylistViewModel
- - - - - SongViewModel
- - - PlaybackControlsViewModel
- DashboardView
- - - Playlist PartialView
- - - - - Song Partial View
- - - PlaybackControls Partial View

We can see that organizationally, they are basically mirror matches. This can help make logical sense of where everything lives in your application when modifying/adding data for new views you create and current views you’re working with.

Quick Disclaimer: We are going to be operating under the assumption that we are already inheriting a parent Layout for our top level Views.

Your Layout.cshtml file might look something like this:

<!DOCTYPE html>
<html>
 <head>
  <!-- top level styles for all inherited views to use -->
  <link rel="stylesheet" href="~/path/to/useful/styles/useful.min.css" />
  
  <!-- additional view level styles -->
  @RenderSection("styles", required: false)
 </head>
 <body class="insert-your-useful-body-class">
  <div class="insert-your-useful-container-class">
   @await Component.InvokeAsync("Header")
   @await Component.InvokeAsync("Sidebar")
   
   <div class="insert-your-useful-content-wrapper-class">       
    <section class="insert-your-useful-content-class">
     <!-- Your Page Content Here -->
     @RenderBody()
    </section>
   </div>
   
   @await Component.InvokeAsync("Footer")
   @await Component.InvokeAsync("ControlSidebar")
  </div>
  
  <!-- top level scripts for all inherited views to use -->
  <script src="~/path/to/useful/scripts/useful.min.js"></script>
  <!-- additional view level scripts -->
  @RenderSection("scripts", required: false)
 </body>
</html>

With this extendable layout being setup, you can now set up your top level view. I mentioned earlier that the top level View should act as a driver for all the partials required for the section of the application to function properly. For this, I recommend setting up the parent container in your top level view, in this example the DashBoardView.cshtml file:

@model MusicStreamingApplication.Models.DashboardViewModel
@{
    // the overall layout to inherit
    Layout = "_Layout";
}
@section styles {
    <!-- any additional styles for Dashboard -->
}
<!-- all the partials Dashboard requires -->
<partial name="~/Views/Dashboard/Playlist.cshtml" for="Playlist" />
<partial name="~/Views/Dashboard/PlaybackControls.cshtml" for="PlaybackControls" />
@section scripts {
    <!-- any additional scripts for Dashboard -->
}

Notice above how we’re passing the partial view exactly what view model it needs to work and display the correct data with the for attribute. for is shorthand for @Model. so the result of using for would map to @Model.Playlist for the first partial.

Moving on to the model example, your DashboardViewModel.cs would look something like this:

using MusicStreamingApplication.Models.PlaylistViewModel;
using MusicStreamingApplication.Models.PlaybackControlsViewModel;
namespace MusicStreamingApplication.Models.DashboardViewModel
{
    public class DashboardViewModel
    {
        public RequestViewModel(){}
        
        public PlaylistViewModel Playlist { get; set; }
        public PlaybackControlsViewModel PlaybackControls { get; set; }
    }
}

Furthermore, you can start to see how we are drilling down to the exact granularity each partial view needs. The PlaylistViewModel would then have a SongViewModel and so on.

Conclusion

Creating scalable and modular applications can be tough, but if you commit yourself to a specific approach it becomes much easier. Strongly typing your views allows you to mirror your data needed inside the view with the view model for each specific view and partial view respectively; Doing this makes your application more organized, modular, scalable, and provides additional compile time type checking benefits. Now, having said all of that, get back to work!

Discover and read more posts from Chris Hennighan
get started
post commentsBe the first to share your opinion
Show more replies