In last post I presented a logging server solution for the mobile platform using node.js and MongoDB. Today I will build a ASP.NET MVC application to display the logged data stored in MongoDB.
First download MongoDB latest driver from github. There're a few download options out there. I just used the msi installer, and two key dlls, MongoDB.Driver.dll and MongoDB.Bson.dll, were installed in my C:\Program Files (x86)\MongoDB\CSharpDriver 1.8.3\ folder. Then we are ready to create a ASP.NET MVC 3 or MVC 4 project using basic template in Visual Studio 2010, and add MongoDB.driver.dll and MongoDB.Bson.dll to the project References.
The mongoDB database can be running inside the same machine or in a separate machine. The only difference is the connection string, which follows the format of "mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]". In my example here I have one MongoDB instance running locally so my connection string is "mongodb://localhost:27017".
Next we create a log item model and a repository to interact with MongoDB with three actions available: search logs within a time span, get original log detail by its ID, and delete a log by its ID (for simplicity reason exception handling is skipped):
using System; using System.Collections.Generic; using System.Linq; using MongoDB.Driver; using MongoDB.Bson; using MongoDB.Driver.Builders; namespace LogsReporting { public class LogItem { public ObjectId ID { get; set; } public DateTime Date { get; set; } public string OS { get; set; } public string Version { get; set; } public string Level { get; set; } public string Manufacturer { get; set; } public string Model { get; set; } public string ScreenSize { get; set; } public string Language { get; set; } public string Orientation { get; set; } public string Timezone { get; set; } public string Message { get; set; } } public interface ILogsRepository { string GetOriginalLog(string id); void DeleteLog(string id); IEnumerable<LogItem> GetLogItems(DateTime from, DateTime to); } public class LogsRepository : ILogsRepository { private MongoCollection<BsonDocument> _collection; public LogsRepository() { string connectionString = "mongodb://localhost:27017"; MongoServer server = new MongoClient(connectionString).GetServer(); MongoDatabase db = server.GetDatabase("Data"); _collection = db.GetCollection("Logs"); } public string GetOriginalLog(string id) { ObjectId bsonId; if (ObjectId.TryParse(id, out bsonId)) { var item = _collection.FindOneById(bsonId); if (item != null) return item.ToString(); } return string.Empty; } public void DeleteLog(string id) { ObjectId bsonId; if (ObjectId.TryParse(id, out bsonId)) { _collection.Remove(Query.EQ("_id", bsonId)); } } public IEnumerable<LogItem> GetLogItems(DateTime from, DateTime to) { List<LogItem> logs = new List<LogItem>(); var docs = _collection.Find(Query.And(Query.GTE("time", from), Query.LTE("time", to))) .SetSortOrder(SortBy.Descending("time")); foreach (var doc in docs) { LogItem log = new LogItem() { ID = doc.GetValue("_id").AsObjectId, Date = TimeZone.CurrentTimeZone.ToLocalTime(doc.GetValue("time").ToUniversalTime()), OS = doc.GetValue("os", "").ToString(), Version = doc.GetValue("version", "").ToString(), Manufacturer = doc.GetValue("manufacturer", "").ToString(), Model = doc.GetValue("model", "").ToString(), Language = doc.GetValue("lang", "").ToString(), ScreenSize = doc.GetValue("screen", "").ToString(), Orientation = doc.GetValue("orientation", "").ToString(), Timezone = doc.GetValue("timezone", "").ToString(), Level = doc.GetValue("level", "").ToString(), Message = doc.GetValue("log", "").ToString() }; if (!string.IsNullOrEmpty(log.Message) && log.Message.Length > 40) log.Message = log.Message.Substring(0, 40) + " ..."; logs.Add(log); } return logs; } } }MongoDB is schemaless, and can store arbitrary format of data. For convenience let's assume the logged data has following fields: time, os, version, manufacturer, model, language, screen-size, orientation, time-zone, log-level, and log message (refer to Windows 8 logging & reporting). Only "time" field (time stamp) is mandatory here so we could safely filter log entries by time.
The controller provides three actions: show recent logs, show the detail of a given log and delete a log. For demo purpose we only show data logged in the past week:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; namespace LogsReporting { public class LogsController : Controller { private static readonly ILogsRepository _logs = new LogsRepository(); public ActionResult Index() { var logs = _logs.GetLogItems(DateTime.Now.AddDays(-7), DateTime.Now); return View(logs); } public string Detail(string id) { var originalLog = _logs.GetOriginalLog(id); return originalLog; } public ActionResult Delete(string id) { _logs.DeleteLog(id); return RedirectToAction("Index"); } } }The presentation layer is pretty straightforward: a grid view shows the logs' info with "Detail" and "Delete" links. Clicking "Detail" link will open up a new window to display the raw log message. A confirmation popup prompts for a "Delete" action:
@model IEnumerable<LogsReporting.LogItem> <script type="text/javascript"> function showDetailPopup(id) { var url = '@Url.Action("Detail")' + '/' + id; window.open(url, "detailWindow", 'width=600px,height=400px'); } </script> <h2 style="text-align: center;">Logs Reporting</h2> <table cellpadding="5px"> <tr> <th>@Html.DisplayNameFor(model => model.Date)</th> <th>@Html.DisplayNameFor(model => model.OS)</th> <th>@Html.DisplayNameFor(model => model.Version)</th> <th>@Html.DisplayNameFor(model => model.Manufacturer)</th> <th>@Html.DisplayNameFor(model => model.Model)</th> <th>@Html.DisplayNameFor(model => model.ScreenSize)</th> <th>@Html.DisplayNameFor(model => model.Language)</th> <th>@Html.DisplayNameFor(model => model.Orientation)</th> <th>@Html.DisplayNameFor(model => model.Timezone)</th> <th>@Html.DisplayNameFor(model => model.Level)</th> <th>@Html.DisplayNameFor(model => model.Message)</th> <th></th> </tr> @{int i = 0;} @foreach (var item in Model) { <tr style='font-size: 9pt; @(i++%2==0 ? "background-color: #bbbbbb" : "")'> <td>@Html.DisplayFor(modelItem => item.Date)</td> <td>@Html.DisplayFor(modelItem => item.OS)</td> <td>@Html.DisplayFor(modelItem => item.Version)</td> <td>@Html.DisplayFor(modelItem => item.Manufacturer)</td> <td>@Html.DisplayFor(modelItem => item.Model)</td> <td>@Html.DisplayFor(modelItem => item.ScreenSize)</td> <td>@Html.DisplayFor(modelItem => item.Language)</td> <td>@Html.DisplayFor(modelItem => item.Orientation)</td> <td>@Html.DisplayFor(modelItem => item.Timezone)</td> <td>@Html.DisplayFor(modelItem => item.Level)</td> <td>@Html.DisplayFor(modelItem => item.Message)</td> <td> <a href="#" onclick="showDetailPopup('@item.ID');">Details</a> | @Html.ActionLink("Delete", "Delete", new { id = item.ID }, new { onclick = "return confirm('Are you sure to delete this log entry?');" }) </td> </tr> } </table>
Following screen-shot shows how the view looks like:
Updated on Nov. 12 Just for fun I created a traditional ASP.NET web form .aspx page to show the exact same view:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="LogReporting.aspx.cs" Inherits="LogsReporting.LogReporting" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title>Logs Reporting</title> <style type="text/css"> h2 {text-align: center;} table {text-align: left; padding: 5px;} table td, .detail {font-size: 9pt; padding: 5px;} </style> <script src="Scripts/jquery-1.7.1.min.js" type="text/javascript"></script> <script type="text/javascript"> $(document).ready(function () { $("tr:odd").css("background-color", "#bbbbbb"); }); </script> </head> <body> <form id="form1" runat="server"> <div> <asp:Panel ID="gridPanel" runat="server"> <h2>Logs Reporting</h2> <asp:GridView ID="gvLogs" runat="server" AutoGenerateColumns="False" GridLines="None" OnRowDataBound="gvLogs_RowDataBound" EmptyDataText="No logs available"> <Columns> <asp:BoundField DataField="Date" HeaderText="Date"></asp:BoundField> <asp:BoundField DataField="OS" HeaderText="OS"></asp:BoundField> <asp:BoundField DataField="Version" HeaderText="Version"></asp:BoundField> <asp:BoundField DataField="Language" HeaderText="Language"></asp:BoundField> <asp:BoundField DataField="Manufacturer" HeaderText="Manufacturer"></asp:BoundField> <asp:BoundField DataField="Model" HeaderText="OS"></asp:BoundField> <asp:BoundField DataField="ScreenSize" HeaderText="Version"></asp:BoundField> <asp:BoundField DataField="Level" HeaderText="Level"></asp:BoundField> <asp:BoundField DataField="Message" HeaderText="Message"></asp:BoundField> <asp:TemplateField> <ItemTemplate> <asp:LinkButton ID="lbtDetail" runat="server" Text="Detail"></asp:LinkButton> <asp:LinkButton ID="lbtDelete" runat="server" Text="Delete" OnClick="lbtDelete_Click" OnClientClick="return confirm('Are you sure to delete this log entry?');"></asp:LinkButton> </ItemTemplate> </asp:TemplateField> </Columns> </asp:GridView> </asp:Panel> <asp:Panel ID="detailPanel" runat="server" CssClass="detail"> <asp:Literal ID="logDetail" runat="server"></asp:Literal> </asp:Panel> </div> </form> </body> </html>The code behind:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; using System.Configuration; using MongoDB.Driver; using MongoDB.Bson; using MongoDB.Driver.Builders; namespace LogsReporting { public partial class LogReporting : System.Web.UI.Page { private ILogsRepository _repo = new LogsRepository(); protected void Page_Load(object sender, EventArgs e) { bool showDetail = !string.IsNullOrEmpty(Request.QueryString["id"]); detailPanel.Visible = showDetail; gridPanel.Visible = !showDetail; if (showDetail) { logDetail.Text = _repo.GetOriginalLog(Request.QueryString["id"]); } else if (!IsPostBack) { var dataSource = _repo.GetLogItems(DateTime.Today.AddDays(-7), DateTime.Now); bindDataSource(dataSource); } } private void bindDataSource(IEnumerable<LogItem> logs) { gvLogs.DataSource = logs; gvLogs.DataBind(); } protected void gvLogs_RowDataBound(object sender, GridViewRowEventArgs e) { if (e.Row.RowType == DataControlRowType.DataRow) { LogItem item = e.Row.DataItem as LogItem; LinkButton lbtDetail = e.Row.FindControl("lbtDetail") as LinkButton; LinkButton lbtDelete = e.Row.FindControl("lbtDelete") as LinkButton; string detailUrl = string.Format("{0}?id={1}", Request.Url.AbsolutePath, item.ID); lbtDetail.Attributes.Add("onClick", "window.open('" + detailUrl + "', '', 'width=600,height=400')"); lbtDelete.CommandArgument = item.ID.ToString(); } } protected void lbtDelete_Click(object sender, EventArgs e) { var logID = Convert.ToString(((LinkButton)sender).CommandArgument); _repo.DeleteLog(logID); var dataSource = _repo.GetLogItems(DateTime.Today.AddDays(-7), DateTime.Now); bindDataSource(dataSource); } } }