Practical .NET

Structuring Views in ASP.NET MVC to Centralize Logic

A View without any code is probably impossible in any real-world application. But, by leveraging Partial Views, you can still separate your View-related code from your HTML.

In theory, in the Model-View-Controller pattern what distinguishes the View from the Model and the Controller is that the View has no logic. The ideal View isn't supposed to require any testing, other than a quick look to make sure everything's spelled correctly and in the right place. Complete data-binding solutions (such as Windows Presentation Foundation) achieve that; ASP.NET MVC does not.

We all know that the ASP.NET MVC Views we create contain logic we must test: loops to display repeating elements, If…Then blocks to include or omit pieces of HTML, and so on. And, as a result, we end up with logic in our Views that we must test to make sure it's doing the right thing in all circumstances.

There are some things you can do to reduce the logic in your application. For example, using typed Partial Views can eliminate the For…Each loops needed to generate repeating rows (as I showed in an earlier column). But that still leaves those nagging If…Then statements that are used to select which HTML is to be displayed.

The fundamental problem is that each .cshtml or .vbhtml file typically represents a variety of Views, each of which is a variation on the other. For example, on a current project, I have a single View containing a table with three variations:

  • One version displays all of the rows in the table as text. Each row has an edit and a delete button at the left-hand end. At the top of the table there's a plus sign for adding new rows.
  • A second version displays one of the existing rows with textboxes so that users can change the current data for the row. In this view, only the edited row has buttons (a save and a cancel button) and the plus sign for adding rows is suppressed.
  • The third version displays a new blank row at the end of the list, again with textboxes so that users can add a new row. As with the edit view, only the editable row has buttons (save and cancel, as before) and, again, the add button at the top of the form is suppressed.

Effectively, each row in the table consists of two parts (the buttons and the data) and three versions (display only, update existing data, add new data). I don't think this is an especially complex view -- I suspect that this example is, if anything, simpler than the Views you have to create. It does, however, require some logic to build the right View correctly. Where should you put that logic?

Two Design Choices
There are, at least, two solutions to handling the logic to create this View. One solution is to create one View for each variation and have the code in the Controller's Action method select the correct View. Obviously, there would be a lot of common HTML shared among the different variations, but Partial Views would be helpful in managing that duplication: The HTML that's common to all of the Views could be put into Partial Views and incorporated into each variation using the HtmlHelper's PartialView method.

However, a certain amount of logic is probably unavoidable even with this design. For example, in my sample application only one of the rows in the table has to be displayed with textboxes rather than plain text. Because each View displays all the rows in the table (all the rows that are in display mode and the one row in add/edit mode) then some If…Then logic in the part of the View that displays individual rows is unavoidable to pick which rows display with plain text and row is displayed with textboxes.

In addition, with this design, if another variation on the View is required, I have to rewrite my controller to handle that variation. This isn't the end of the world -- but I will have to create a test to determine if the Action method is returning the right View under the right circumstances.

Despite all those good reasons, the solution that I've come to prefer is the opposite of that "one View for each variation" design: I have a single, central View that holds all of the common HTML and I push the HTML for the variations into Partial Views. With this design, that one central View holds all of the unavoidable logic that selects the correct HTML for each variation.

With this design, the individual Partial Views have no logic at all and contain nothing but HTML. I like that division of labor. In fact, if I wanted to carry this design to its logical end, my central View would consist of nothing but the code to select the right combination of Partial Views. I found, however, that theoretically pure design resulted in more Partial Views than I wanted to deal with.

There's another reason I like this design: My Action method doesn't have to select between the different variations -- my Action method only has to know about my central View. If I need to make a change to my UI (other than displaying additional data), the only thing that changes are my Views, not my Controllers. This design delegates the work of assembling the UI to my central View and removes it from my Controllers, which makes sense to me.

Setting up the View
As with any View, I begin my central View by declaring the type of the object passed in the View's Model property. In this case, my DTO consists of an object with two properties: A row number and a collection of sales order headers (the class is called SalesOrder). The row number flags which of the rows (if any) is to be put in edit or update mode. In my Action method, if the user is adding a new sales order header or if there are no rows at all, I add an empty SalesOrder object to the end of the collection containing an empty SalesOrder object to support adding a new row.

In the code block at the start of the View, I determine if there are any rows to display. I also retrieve from the Model object the number of whatever row is to be displayed in edit mode. I use a couple of conventions for that row number: If the row number is -1 then no row is to be displayed with textboxes; if the row number is -99, it indicates that a new SalesOrder is being added. As this code shows, if there are no rows to be displayed, I automatically treat the page as if it were adding a new row:

@model SalesOrderDTO

@{ 
  int curRow = 0;
  int rownum;
  if (Model.Count() != 0)
  {
    rownum = Model.EditRow;
  }
  else
  {
    rownum = -99;
  }
}

Following the code block is the first of the minimal HTML that appears in my central View -- the opening html, body and form tags. This is also where I include any link or head elements for my View:

<html>
<body>
<form>

Assembling the View
The next part of the View's code confirms that no rows are to be edited or added. If that's the case, the code brings in a Partial View that displays the plus sign at the top of the form:

@if (rownum == -1 && rownum != -99)
{
  @Html.Partial("AddButton")
}

As with all of the Partial Views that make up this page, that AddButton Partial View has no logic in it: It consists of just the HTML to display the plus sign image.

Now I include a Partial View with the table tag and the column headers. I pass a new SalesOrder object to the Partial View so that I can use the HtmlHelper's DisplayNameFor method to extract the column names for the SalesOrder properties that I display in the table:

@Html.Partial("Headers", new SalesOrder)

Now it's time to start displaying the table. I roll through the collection of SalesOrders in my DTO object, keeping track of what row I'm on:

@foreach (var item in Model.SalesOrders)
{
  curRow += 1;  

Each row is built out of two sets of HTML:

  • One set controls what buttons are displayed.
  • The other set controls the display of the data in each row.

For each row, as shown in Listing 1, I check for one of three conditions. First, if no row is in update or add mode, I include the Partial View with the edit/remove buttons. Second, if the current row is the editable row or the current item has an ID of zero (indicating it's the added blank item), I include the partial View with the save and cancel buttons. Third, if some row is in update or add mode but not this row, I include some "spacer" HTML to flesh out the first column of the table. These Partial Views also contain the tr tag that starts a new row.

Listing 1: Assembling Partial Views into the Start of a Table Row
@if (rownum == -1)  {  
  @Html.Partial("ButtonsEditRemove", item);
}
else
{
  if (curRow == rownum || item.EventBroadCastID == 0)
  {                
    @Html.Partial("ButtonsSaveCancel", new EventBroadcasts());
  }
}
else
{
  @Html.Partial("ButtonsInvisible", item);
}

With the front part of the row taken care of, I use similar logic to bring in the Partial Views containing the HTML for the rest of the row. I have three kinds of rows: RowDisplayOnly for most rows, RowUpdate for a row that's changing existing data and RowNew for a row that's being added (these views include the closing tr tag for the row). Listing 2 shows the code.

Listing 2: Assembling the Rest of the Row
@if (curRow != rownum || rownum == -99)
{
  if (item.EventBroadCastID != 0)
  {
    @Html.Partial("RowDisplayOnly", item);
  }
  else
  {
    @Html.Partial("RowNew", item);
  }
}
else
{
  @Html.Partial("RowUpdate", item);
}

Finally, I close the table:

</table>

Following the end of this HTML, I have the closing tags for my html, body and form elements (typically, I'd put my script tags here, also):

</form>
</body>
</html>

My assumption that drives this design is that I can't eliminate code from my Views … but I can isolate it into my central View. When I need to make changes to my pages I either modify the HTML in my Partial Views or I modify the code in my central View that assembles those Partial Views. Either way, I don't have to touch my Controller's code. It's not a theoretically perfect solution, but it's good enough for the real world.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

  • Creating Reactive Applications in .NET

    In modern applications, data is being retrieved in asynchronous, real-time streams, as traditional pull requests where the clients asks for data from the server are becoming a thing of the past.

  • AI for GitHub Collaboration? Maybe Not So Much

    No doubt GitHub Copilot has been a boon for developers, but AI might not be the best tool for collaboration, according to developers weighing in on a recent social media post from the GitHub team.

  • Visual Studio 2022 Getting VS Code 'Command Palette' Equivalent

    As any Visual Studio Code user knows, the editor's command palette is a powerful tool for getting things done quickly, without having to navigate through menus and dialogs. Now, we learn how an equivalent is coming for Microsoft's flagship Visual Studio IDE, invoked by the same familiar Ctrl+Shift+P keyboard shortcut.

  • .NET 9 Preview 3: 'I've Been Waiting 9 Years for This API!'

    Microsoft's third preview of .NET 9 sees a lot of minor tweaks and fixes with no earth-shaking new functionality, but little things can be important to individual developers.

  • Data Anomaly Detection Using a Neural Autoencoder with C#

    Dr. James McCaffrey of Microsoft Research tackles the process of examining a set of source data to find data items that are different in some way from the majority of the source items.

Subscribe on YouTube