MVC Logging Part 6 – Controller / Views

This is part 6 of the MVC Logging series. Other articles in the series are:

Introduction

In the previous article we set up our model and the repositories that we need to fetch the data from the database. Now we need to create the necessary controllers and views so that we can present all of the consolidated logging messages on our log reporting dashboard page.

Creating our Logging Controller

The first thing we need to do is create a controller for our log reporting pages. Here are the steps to follow:

1. In the solution explorer, right-click the “Controllers” directory and select “Add -> Controller”.

2. Type in the name “LoggingController”, leave the check-box unticked and click “Add”.

At this point let’s take a minute to think about what we want to display on our Logging landing page.

We would like to:

* Display a filterable grid of all the log messages. The user should be able to filter by date, the name of the log provider (eg. NLog, Elmah etc), and the log level (eg. Debug, Info, Error etc)
* Allow the user to page through the results
* Allow the user to choose how many records to display per page.
* Allow the user to click on a row in the grid to view more detailed information about the log message.

OK, great! So it looks like we will need the following parameters for our view:

* Date Start
* Date End
* Log Provider Name
* Log Level
* Current Page Index
* Page Size

Let’s create a ViewModel called “LoggingIndexModel” to store all of these details. We’ll also combine the Start and End Dates into a string based representation called Period which will contain values like “Today”, Yesterday”, “Last Week” etc.

1. Create a new directory in the root of your website and name it “ViewModels”.

2. Add a new file called, “LoggingIndexModel.cs”.

3. Add the following code into the class:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

using MvcLoggingDemo.Models;
using MvcLoggingDemo.Services.Paging;

namespace MvcLoggingDemo.ViewModels
{
 public class LoggingIndexModel
 {
 public IPagedList<LogEvent> LogEvents { get; set; }

 public string LoggerProviderName { get; set; }
 public string LogLevel { get; set; }
 public string Period { get; set; }

 public int CurrentPageIndex { get; set; }
 public int PageSize { get; set; }

 public LoggingIndexModel()
 {
 CurrentPageIndex = 0;
 PageSize = 20;
 }
 }
}

Go ahead and modify our controller class so that it looks like the following :


using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel.Syndication;
using System.Web;
using System.Web.Mvc;

using MvcLoggingDemo.Helpers;
using MvcLoggingDemo.Models.Repository;
using MvcLoggingDemo.Models;
using MvcLoggingDemo.ViewModels;

using MvcLoggingDemo.Services.Paging;

namespace MySampleApp.Controllers
{
 [Authorize]
 public class LoggingController : Controller
 {
 private readonly ILogReportingFacade loggingRepository;

 public LoggingController()
 {
 loggingRepository = new LogReportingFacade();
 }

 public LoggingController(ILogReportingFacade repository)
 {
 loggingRepository = repository;
 }

 /// <summary>
 /// Returns the Index view
 /// </summary>
 /// <param name="Period">Text representation of the date time period. eg: Today, Yesterday, Last Week etc.</param>
 /// <param name="LoggerProviderName">Elmah, Log4Net, NLog, Health Monitoring etc</param>
 /// <param name="LogLevel">Debug, Info, Warning, Error, Fatal</param>
 /// <param name="page">The current page index (0 based)</param>
 /// <param name="PageSize">The number of records per page</param>
 /// <returns></returns>
 public ActionResult Index(string Period, string LoggerProviderName, string LogLevel, int? page, int? PageSize)
 {
 // Set up our default values
 string defaultPeriod = Session["Period"] == null ? "Today" : Session["Period"].ToString();
 string defaultLogType = Session["LoggerProviderName"] == null ? "All" : Session["LoggerProviderName"].ToString();
 string defaultLogLevel = Session["LogLevel"] == null ? "Error" : Session["LogLevel"].ToString();

 // Set up our view model
 LoggingIndexModel model = new LoggingIndexModel();

 model.Period = (Period == null) ? defaultPeriod : Period;
 model.LoggerProviderName = (LoggerProviderName == null) ? defaultLogType : LoggerProviderName;
 model.LogLevel = (LogLevel == null) ? defaultLogLevel : LogLevel;
 model.CurrentPageIndex = page.HasValue ? page.Value - 1 : 0;
 model.PageSize = PageSize.HasValue ? PageSize.Value : 20;

 TimePeriod timePeriod = TimePeriodHelper.GetUtcTimePeriod(model.Period);

 // Grab the data from the database
 model.LogEvents = loggingRepository.GetByDateRangeAndType(model.CurrentPageIndex, model.PageSize, timePeriod.Start, timePeriod.End, model.LoggerProviderName, model.LogLevel);

 // Put this into the ViewModel so our Pager can get at these values
 ViewData["Period"] = model.Period;
 ViewData["LoggerProviderName"] = model.LoggerProviderName;
 ViewData["LogLevel"] = model.LogLevel;
 ViewData["PageSize"] = model.PageSize;

 // Put the info into the Session so that when we browse away from the page and come back that the last settings are rememberd and used.
 Session["Period"] = model.Period;
 Session["LoggerProviderName"] = model.LoggerProviderName;
 Session["LogLevel"] = model.LogLevel;

 return View(model);
 }
}
}

Index View

Now right-click the Index method in the Controller and Add the Index View.

Choose the “Strongly typed view” option and select “MvcLoggingDemo.ViewModels.LoggingIndexModel” as the Type.

Select Empty for “View Type” and click “OK” to create a blank slate for our Index view.

Our Dashboard will allow users to see a list of errors, a chart of the errors and also a RSS feed of the errors so let’s add those options to the top of our Index page:

<div>
 View :
 <strong>List</strong>
 | <%: Html.ActionLink("Chart", "Chart")%>
 | <%: Html.ActionLink("RSS", "RssFeed", new { LoggerProviderName = Model.LoggerProviderName, Period = Model.Period, LogLevel = Model.LogLevel }, new { target = "_blank" })%>
 </div>

As our index page will be the list or grid based view we will need a way to filter the error messages to be displayed. So let’s start a HTML form and add some filtering fields to our view:

<div>
 <div>

 Logger : <%: Html.DropDownList("LoggerProviderName", new SelectList(MvcLoggingDemo.Helpers.FormsHelper.LogProviderNames, "Value", "Text"))%>

 Level : <%: Html.DropDownList("LogLevel", new SelectList(MvcLoggingDemo.Helpers.FormsHelper.LogLevels, "Value", "Text"))%>

 For : <%: Html.DropDownList("Period", new SelectList(MvcLoggingDemo.Helpers.FormsHelper.CommonTimePeriods, "Value", "Text"))%>

 <input id="btnGo" name="btnGo" type="submit" value="Apply Filter" />

 </div>
 </div>

We also need a header for our grid that will display the number of  messages found  and a way for the user to change the number of records displayed per page. Let’s add our grid header now:

<div>

 <div>
 <div>

 <span style="float: left">
 <%: string.Format("{0} records found. Page {1} of {2}", Model.LogEvents.TotalItemCount, Model.LogEvents.PageNumber, Model.LogEvents.PageCount)%>
 </span>

 <span style="float: right">
 Show <%: Html.DropDownList("PageSize", new SelectList(MvcLoggingDemo.Helpers.FormsHelper.PagingPageSizes, "Value", "Text"), new { onchange = "document.getElementById('myform').submit()" })%> results per page
 </span>

 <div style="clear: both"></div>

 </div>

 </div>

 <div>
 <div>
 <%= Html.Pager(ViewData.Model.LogEvents.PageSize, ViewData.Model.LogEvents.PageNumber, ViewData.Model.LogEvents.TotalItemCount, new { LogType = ViewData["LogType"], Period = ViewData["Period"], PageSize = ViewData["PageSize"] })%>
 </div>
 </div>

 </div>

 <% } %>

Notice how the paging helper using the information from the ViewData and also our need to add in the routing data to include information from our filters.

With the grid filter and the grid header all done we now need to turn our attention to the actual grid data. Add the following code to the view:

<% if (Model.LogEvents.Count() == 0) { %>

 <p>No results found</p>

 <% } else { %>

 <div>
 <table>
 <tr>
 <th></th>
 <th>
 #
 </th>
 <th>
 Log
 </th>
 <th>
 Date
 </th>
 <th style='white-space: nowrap;'>
 Time ago
 </th>
 <th>
 Host
 </th>
 <th>
 Source
 </th>
 <th>
 Message
 </th>
 <th>
 Type
 </th>
 <th>
 Level
 </th>
 </tr>

 <% int i = 0;  foreach (var item in Model.LogEvents)
 { %>

 <tralt" : "" %>">
 <td>
 <%: Html.ActionLink("Details", "Details", new { id = item.Id.ToString(), loggerProviderName = item.LoggerProviderName })%>
 </td>
 <td>
 <%: i.ToString() %>
 </td>
 <td>
 <%: item.LoggerProviderName%>
 </td>
 <td style='white-space: nowrap;'>
 <%: String.Format("{0:g}", item.LogDate.ToLocalTime())%>
 </td>
 <td style='white-space: nowrap;'>
 <%: item.LogDate.ToLocalTime().TimeAgoString()%>
 </td>
 <td>
 <%: item.MachineName%>
 </td>
 <td>
 <%: item.Source%>
 </td>
 <td>
 <pre><%: item.Message.WordWrap(80) %></pre>
 </td>
 <td>
 <%: item.Type%>
 </td>
 <td>
 <%: item.Level.ToLower().ToPascalCase() %>
 </td>
 </tr>

 <% } %>

 </table>
 </div>

 <% } %>

 <div>

 <div>
 <div>
 <%= Html.Pager(ViewData.Model.LogEvents.PageSize, ViewData.Model.LogEvents.PageNumber, ViewData.Model.LogEvents.TotalItemCount, new { LogType = ViewData["LogType"], Period = ViewData["Period"], PageSize = ViewData["PageSize"] })%>
 </div>
 </div>

 </div>

In the snippet above, first we check to see if there are any log messages to be displayed. If not we let the user know that no records were found.

Instead of just displaying the datetime that the log message was recorded we also add a column to display the amount of time that has elapsed since then in a user friendly way. We use a DateTime extension method called “TimeAgoString()” to accomplish this.

Another nice feature is that we would also like to word wrap the error messages. Whilst building this project I received several error messages that had very long sequences of unbroken characters. The WordWrap helper function will split very long strings into lines that have a maximum length that you specify. It will try to break the lines apart on words but if it encounters long unbroken text it will just split them where necessary. The end result is a nicely formatted display.

Also, when dealing with multiple log providers it is nice to display the Log Level consistently across all of them. In order to accomplish this we convert the Log Level to lower case and then to PascalCase using a string extension helper method.

And lastly, we repeat the paging at the bottom of the grid so that if the user is looking at 50 or 100 records at a time they do not need to scroll back up to the top of the grid to navigate to the next page of results.

Now let’s run the website, log in, and view our Log Reporting Dashboard page:

To wrap up this article let’s quickly create the details page for a log message.

Details View

1. The first step is to go to the “Logging” controller and add the following code underneath the “Index” action:

//
 // GET: /Logging/Details/5

 public ActionResult Details(string loggerProviderName, string id)
 {
 LogEvent logEvent = loggingRepository.GetById(loggerProviderName, id);

 return View(logEvent);
 }

2. Right-click the “Details” method in the “Controller” and choose “Add View”
Select “Strongly Typed View” and choose the “MvcLoggingDemo.Models.LogEvent” Type.
Select “Empty” for the “Content Type”
Click on “Add”

3. Add the following code to the “Details” page to the “MainContent” content placeholder:

<h2>Details</h2>

 <p>
 <%: Html.ActionLink("Back to List", "Index") %>
 </p>

 <fieldset>
 <legend>Fields</legend>

 <div>Id</div>
 <div><%: Model.Id %></div>

 <div>LogDate</div>
 <div><%: String.Format("{0:g}", Model.LogDate) %></div>

 <div>Name</div>
 <div><%: Model.LoggerProviderName %></div>

 <div>Source</div>
 <div><%: Model.Source %></div>

 <div>MachineName</div>
 <div><%: Model.MachineName %></div>

 <div>Type</div>
 <div><%: Model.Type %></div>

 <div>Level</div>
 <div><%: Model.Level %></div>

 <div>Message</div>
 <div>
 <pre><%: Model.Message.WordWrap(80) %></pre>
 </div>

 <div>StackTrace</div>
 <div><%: Model.StackTrace %></div>

 </fieldset>

 <% =FormsHelper.OutputXmlTableForLogging(Model.AllXml) %>

 <p>
 <%: Html.ActionLink("Back to List", "Index") %>
 </p>

Notice that we once again we use the WordWrap helper function to tidy up the error message

At the bottom of the page we use another helper method to display the server variables and cookies in a table.

Here is what the details page looks like :

Conclusion

That ends part 6 of this series. In this article we created the Logging controller and views for the “Index” and “Details” actions on the controller. In the next article we will add an RSS feed to our dashboard.

Download

The sourcecode for part 6 is on the Downloads tab of the associated Codeplex website

Tagged with: , , , ,
Posted in ASP.NET MVC
One comment on “MVC Logging Part 6 – Controller / Views
  1. Krokonoster says:

    GREAT tutorial, thanks!
    Using this I managed to implement a more basic logging system for a project I’m working on.
    Fiddled with it another day or two, and managed to build a fairly complete logging system for my own (experimental/sandbox/hobby) site.

Leave a comment