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

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube