Polymorph Razor Views

As an object-orientation-spoiled programmer, when you come to use an HTML rendering engine such as Razor you will soon start to wish for the same couple of design advantages that object orientation brings along.

Using polymorphism in “class-ic” object orientation, e.g., you can do things like that (simplified code snippet – don’t try copy-paste):

class Person
{
    string FirstName { get; set; }
    string LastName { get; set; }
    virtual string FormatName()
    {
        return FirstName + " " + LastName;
    }
}

class PersonWithMiddleName : Person
{
    string MiddleName { get; set; }
    override string FormatName()
    {
        return FirstName + " " + MiddleName + " " + LastName;
    }
}

That means, you can write some generic code that iterates over a couple of Person objects and prints their names without caring how they are actually built. By default a Person would yield their first and last name when invoking FormatName, but if you need to print a middle name, too, you can simply subclass Person and override FormatName. When invoking FormatName on an instance of PersonWithMiddleName, even when it is known as Person only, it will apply the most special implementation found, here the one including the middle name.

The good news – you can get that in Razor, too

Razor has a concept that reminds me of the same type of polymorphism. Using Display Templates and Editor Templates you can define a general template to render a Person:

@* ~/EditorTemplates/Person.cshtml *@
@model Person
<div class="Person">@Model.FirstName @Model.LastName</div>

This template applies each time you use EditorFor with a Person type property:

@* suppose a class Book { Person Author { get; set; } *@
@model Book
@Html.EditorFor(m => m.Author)

Now say we have a book with a PersonWithMiddleName author. Then the Person template is still applied, because PersonWithMiddleName inherits from Person. But we have the option to override the template for the special class:

@* ~/EditorTemplates/PersonWithMiddleName.cshtml *@
@model PersonWithMiddleName
<div class="Person">
@Model.FirstName @Model.MiddleName @Model.LastName
</div>

Now, whenever the book has a PersonWithMiddleName author, this template applies. For all other Person objects, the Person.cshtml applies.

We can take it a step further! “Base templates”

If you did not know of Editor Templates before, you might be so excited about them now that you want to try them out right away and cannot think clearly enough to head on reading. Just return later.

If you already did know Editor Templates you might have been wondering: Can we take it a step further? In C# the overriding method can call its base method to reuse the code of the base class:

class PersonWithMiddleName : Person
{
    string MiddleName { get; set; }
    override string FormatName()
    {
        return base.FormatName() + ", middle name: " + MiddleName;
    }
}

Can we do that with Editor Templates?

We can, making use of this helpful answer on stackoverflow.

@* ~/EditorTemplates/PersonWithMiddleName.cshtml *@
@Html.Partial("~/EditorTemplates/Person.cshtml", Model)
, middle name:
@MiddleName

At a first glance you might think it looks a bit clerky but it is not: Just as a subclass needs to reference its base class with its full class name including the namespace, we reference the “base template” using its full path. We simply pass that template the same model. Thus it also uses the exact same ViewData (e.g. the “HtmlFieldPrefix”). Thus we pass it the context, which is kind of analaguous to the “this” keyword in C#.

Template Method Pattern

Now we get to an even more interesting point: If we have inheritance and polymorphism at hand, can we use any of the popular object oriented design patterns?

I tried the template method pattern. It is very useful in this place because when you render a view it is pretty likely that you want to “inject” some additional HTML in the midst of the rendering output of the base template.

Taking the example from above, let us say we want to render FirstName + MiddleName + LastName again:

@* ~/EditorTemplates/Person.cshtml *@
@model Person
@{
    var extension1 = ViewData.ContainsKey("ExtensionPoint1")
        ? ViewData["ExtensionPoint1"] : string.Empty;
}
<div class="Person">
    @FirstName
    @Html.Raw(extension1)
    @LastName
</div>

The template basically renders FirstName + LastName but provides an “extension point” for inserting HTML between the two names. A pretty silly application of the pattern, but, you know, it is an example only.

The extension must be a string containing HTML. It is only rendered if you provide it.

@* ~/EditorTemplates/PersonWithMiddleName.cshtml *@
@model PersonWithMiddleName

@{
    var extension1 = @RenderMiddleName().ToString();
    ViewData["ExtensionPoint1"] = extension1;
}
@Html.Partial("~/EditorTemplates/Person.cshtml", Model)

@helper RenderMiddleName()
{
    @MiddleName
}

Our specialized template makes use of the Person.cshtml “base template” for rendering the parts that the base template already knows best how to render. But it combines that with its own specializd rendering by only injecting the part that is really different from the base template – the middle name.

We don’t define the extension1 string directly but use a Razor helper instead. This allows us to make use of the Razor syntax to define the part to inject rather than writing lenghy worms of plain HTML strings.

Conclusion

Using Editor Templates (and Display Templates) in a smart combination with Html.Partial and ViewData you can leverage a (maybe) unknown range of object-oriented techniques for building your web pages, allowing for a modular, clean design in this part of your web application just like in the rest of your application, thus improving code reuse and maintainability of your templates.

I think this is great news! What do you think? Have you been using this approach before? Do you have better ideas? Or do you see any issues with this approach?

Creating an excel-like grid for ASP.NET

I have been searching around for a while because I was in the need for an ASP.NET control similar to asp:GridView, but with following huge differences:

  • All rows and their cells must be in edit mode all the time. On postback (e.g. using a “Save” button), the data must be written back to the datasource.
    In GridView, in contrast, you need to hit a row’s  “Edit” to turn the row into edit mode, and hit the row’s “Update” button to save its changes to the datasource.
  • You should be able to navigate through the cells using the arrow keys.
    In GridView, this doesn’t even apply since only one row at a time is in edit mode.
  • You should be able to copy-paste multiple cells at a time from Excel into the grid. If you copy 3×2 cells in Excel, paste the cursor on a cell in the grid and Hit Ctrl+V, the cell values must be spread over the cells below and right from the selected cell.
    In GridView, in contrast, all cell values are pasted as a concatenated string into the selected cell.
  • The grid should work in IE, Firefox and possibly other browsers. Well, I have to admit that my solution presented here only works for IE so far, but it should be feasible to extend the code for Firefox and Safari.

Having searched for quite a long time, the only really interesting thing I found was Srikanth Reddy’s Blog “Creating an Excel Like GridView”. His solution was really close to what I needed, and I want to thank Srikanth in this place for sharing his work. The original blog is not available anymore, but there now is a copy of Srikanth’s post at: http://rschandrastechblog.blogspot.com/2010/11/client-gridview-iii-httpwwwaspboycomcat.html. Yet I decided to post the whole of my code (which is mostly to be attributed to Srikanth) in this blog.

His version didn’t support pasting cells either, but it was quite easy to extend it such that this is now supported, too.

Apart from the new features, I also changed the structure of the code a bit. Let’s have closer look at the new code:

1. General Methods for Navigation

  • There are 4 methods “getLeftNeighbor”, “getRightNeighbor”, “getAboveNeighbor” and “getBelowNeighbor”. They find and return the cell that is found right next to the given one.
  • The code to actually select a certain cell is found in “NavigateToCell”.
  • The code to format the previously selected and newly selected cells is found in the onfocus event. It’s the best place to do the formatting becausee it will take place no matter whether you click a cell, or use the arrows, tab or shift-tab to navigate to a neighbored cell.

2. Intercepting the paste event and spreading multiple values over the grid

When the user copies multiple cells from an Excel sheet and pastes it in a cell, you’d expect the values to be spread over multiple rows and columns, just like they would in Excel. However, the default behavior of a HTML textbox is to regard the string as a single value and paste everything in that particular text box.

Suppose that the user copies the following 3 x 2 cells from excel:

a b
c d
e f

These values are represented in the clipboard as a text formatted as “a\tb\r\nc\td\r\ne\tf\r\n“.

In order to have these values be spread over the appropriate text boxes we need to intercept the paste event, split the clipboard text into a 2-dimensional array of values and then reuse our get-neighbor methods to find the cells below and right from the selected cell:

    function tupelizeText(text) {
        text = text.replace(/\r\n/g, '\n');
        text = text.replace(/\r/g, '\n');

        var tmparray = text.split("\n");
        var result = new Array();

        for (var i = 0; i < tmparray.length - 1; i++) {
            result[i] = tmparray[i].split('\t');
        }

        return result;
    }

    function txt_paste(element) {
        var strPasteData = window.clipboardData.getData("Text");
        var values = tupelizeText(strPasteData);
        var leftAreaBorderCell = curCell;
        for (i = 0; i < values.length; i++) {
            var pasteCell = leftAreaBorderCell;
            for (j = 0; j < values[i].length; j++) {
                pasteCell.firstChild.innerText = values[i][j];

                if (getRightNeighbor(pasteCell) != null)
                    pasteCell = getRightNeighbor(pasteCell);
                else
                    break;
            }

            if (getLowerNeighbor(leftAreaBorderCell) != null)
                leftAreaBorderCell = getLowerNeighbor(leftAreaBorderCell);
            else
                break;
        }

        return false;
    }

Imagine the special case where the selection in our grid is placed such that not all values from the clipboard can be pasted. To cover this we always check whether there is another row below or another column at the right. If not, we break that particular iteration.

To register the onpaste event I added a line in the C# code. I also added the onfocus event and a css class:

txt.Attributes.Add("onfocus", "txt_focus(this)");
txt.Attributes.Add("onpaste", "return txt_paste(this)");
txt.Attributes.Add("class", "SpreadsheetView-TextBox");

So much about some particularities. I don’t (yet) take the effort to explain the parts that Srikanth already introduced… I trust in your own development skills 🙂 So as promised, the full code below.

I hope this inspires you. If there are things unclear, please let me know! And again, much credits to Srikanth Reddy for his post. If you improve or extend this code (e.g. make it work in Firefox and/or Safari), please let me know! If you publish this code or an adaption of it, please attribute and put a link on this blog post.

SpreadsheetView.ascx

<%@ Control Language="C#" AutoEventWireup="true"
CodeBehind="SpreadsheetView.ascx.cs"
Inherits="WebBased.SpreadsheetView" %>

<div id="divSpreadsheetView" onkeydown="divSpreadsheetView_keydown(event)">
<table ID="tblTable" cellspacing="0" cellpadding="0" runat="server">
<tr>
<td><input type="text" /></td>
<td><input type="text" /></td>
</tr>
<tr>
<td><input type="text" /></td>
<td><input type="text" /></td>
</tr>
</table>
</div>

<script language="javascript" type="text/javascript">

var curCell = null;

// Called when user clicks on the textbox of a SpreadsheetView cell, selects the cell.
function txt_focus(element) {
if (curCell != null) curCell.className = "SpreadsheetView-Cell";
curCell = element.parentNode;
curCell.className = "SpreadsheetView-SelectedCell";
curCell.firstChild.select();
}

// Returns the cell left from the given cell, or null if the given cell is the left-most.
function getLeftNeighbor(cell) {
if (cell.previousSibling != null) {
return cell.previousSibling;
} else {
return null;
}
}

// Returns the cell right from the given cell, or null if the given cell is the right-most.
function getRightNeighbor(cell) {
if (cell.nextSibling != null) {
return cell.nextSibling;
} else {
return null;
}
}

// Returns the cell above the given cell, or null if the given cell is the top-most (excluding header).
function getUpperNeighbor(cell) {
if (cell.parentNode.previousSibling.rowIndex != 0) {
return cell.parentNode.previousSibling.children[cell.cellIndex];
} else {
return null;
}
}

// Returns the cell below the given cell, or null if the given cell is the bottom-most.
function getLowerNeighbor(cell) {
if (cell.parentNode.nextSibling != null) {
return cell.parentNode.nextSibling.children[cell.cellIndex];
} else {
return null;
}
}

// Helper method to select a cell.
function NavigateToCell(cell) {
cell.firstChild.focus();
}

// Called when user hits an arrow key while focus is on the SpreadsheetView, selects a neighbor cell.
function divSpreadsheetView_keydown(event) {
var keyCode;

if (!event) event = window.event;

if (event.which) {
keyCode = event.which;
} else if (event.keyCode) {
keyCode = event.keyCode;
}

if (keyCode == 37 && getLeftNeighbor(curCell) != null) {
NavigateToCell(getLeftNeighbor(curCell));
}
else if (keyCode == 38 && getUpperNeighbor(curCell) != null) {
NavigateToCell(getUpperNeighbor(curCell));
}
else if (keyCode == 39 && getRightNeighbor(curCell) != null) {
NavigateToCell(getRightNeighbor(curCell));
}
else if (keyCode == 40 && getLowerNeighbor(curCell) != null) {
NavigateToCell(getLowerNeighbor(curCell));
}
}

// Register keydown event.
//document.getElementById("divSpreadsheetView").onkeydown = divSpreadsheetView_keydown;

// Parses a string containing a copy of multiple excel cells and returns the cells in an array of arrays.
function tupelizeText(text) {
text = text.replace(/\r\n/g, '\n');
text = text.replace(/\r/g, '\n');

var tmparray = text.split("\n");

var result = new Array();

// text copied from excel always ends with a \n too much, so ignore last element.
for (var i = 0; i < tmparray.length - 1; i++) {
result[i] = tmparray[i].split('\t');
}

return result;
}

// Called when user pasts text in a textbox. If consists of multiple cells, spreads the values among textboxes.
function txt_paste(element) {
var strPasteData = window.clipboardData.getData("Text");

if (curCell == element.parentNode) {
var values = tupelizeText(strPasteData);

// this variable will iterate down the rows but always be the left-most of the area to paste into
var leftAreaBorderCell = curCell;

for (i = 0; i < values.length; i++) {

// this variable will iterate through all cells of the row
var pasteCell = leftAreaBorderCell;

for (j = 0; j < values[i].length; j++) {
pasteCell.firstChild.innerText = values[i][j];

if (getRightNeighbor(pasteCell) != null) {
pasteCell = getRightNeighbor(pasteCell);
} else {
break;
}
}

if (getLowerNeighbor(leftAreaBorderCell) != null) {
leftAreaBorderCell = getLowerNeighbor(leftAreaBorderCell);
} else {
break;
}
}

return false;
}

return true;
}

</script>

SpreadsheetView.ascx.cs

using System;
using System.Web.UI.WebControls;
using System.Data;
using System.Web.UI.HtmlControls;
using System.Web.UI;

namespace WebBased
{
public partial class SpreadsheetView : System.Web.UI.UserControl
{
public DataTable Model { get; set; }

public override void DataBind()
{
tblTable.Rows.Clear();
GenerateHeaderRow(tblTable, Model);

for (int iRow = 0; iRow < Model.Rows.Count; iRow++)
{
GenerateDataRow(tblTable, iRow, Model);
}
}

protected void Page_Load(object sender, EventArgs e)
{
if (this.IsPostBack)
{
// Read data from UI back into DataTable
for (int iRow = 1; iRow < tblTable.Rows.Count; iRow++)
{
for (int iCol = 0; iCol < tblTable.Rows[iRow].Cells.Count; iCol++)
{
HtmlTableCell cell = tblTable.Rows[iRow].Cells[iCol];
TextBox txt = cell.Controls[0] as TextBox;
Model.Rows[iRow - 1][iCol] = txt.Text;
}
}
}
}

private static void GenerateDataRow(HtmlTable tblTable, int iRow, DataTable dtDataTable)
{
HtmlTableRow row = new HtmlTableRow();
tblTable.Rows.Add(row);
row.Attributes.Add("class", "SpreadsheetView-Row");

for (int iCol = 0; iCol < dtDataTable.Columns.Count; iCol++)
{
HtmlTableCell cell = new HtmlTableCell();
row.Cells.Add(cell);
cell.Attributes.Add("class", "SpreadsheetView-Cell");

TextBox txt = new TextBox();
txt.ID = String.Format("txt_{0}_{1}", iRow, iCol);
txt.Width = Unit.Percentage(100);
txt.Text = dtDataTable.Rows[iRow][iCol].ToString();
txt.Attributes.Add("onfocus", "txt_focus(this)");
txt.Attributes.Add("onpaste", "return txt_paste(this)");
txt.Attributes.Add("class", "SpreadsheetView-TextBox");

cell.Controls.Add(txt);
}
}

private static void GenerateHeaderRow(HtmlTable tblTable, DataTable dtDataTable)
{
HtmlTableRow rowHeader = new HtmlTableRow();
tblTable.Rows.Add(rowHeader);
rowHeader.Attributes.Add("class", "SpreadsheetView-HeaderRow");

for (int iCol = 0; iCol < dtDataTable.Columns.Count; iCol++)
{
HtmlTableCell cellHeader = new HtmlTableCell("th");
rowHeader.Cells.Add(cellHeader);
cellHeader.Attributes.Add("class", "SpreadsheetView-HeaderCell");

cellHeader.InnerText = dtDataTable.Columns[iCol].Caption;
}
}

}
}

Putting it into Action

To use the control, just add a tag to your asp.net code:

<%@ Register TagPrefix="x" TagName="SpreadsheetView" Src="~/SpreadsheetView.ascx" %>

<x:SpreadsheetView ID="SpreadsheetView1" runat="server"/>

and feed it with an empty grid of your required size:

DataTable dataTable = new DataTable();

dataTable.Columns.Add("A");
dataTable.Columns.Add("B");
dataTable.Columns.Add("C");
dataTable.Columns.Add("D");

dataTable.Rows.Add(new string[] { "", "", "", "" });
dataTable.Rows.Add(new string[] { "", "", "", "" });
dataTable.Rows.Add(new string[] { "", "", "", "" });
dataTable.Rows.Add(new string[] { "", "", "", "" });
dataTable.Rows.Add(new string[] { "", "", "", "" });

SpreadsheetView1.Model = dataTable;
SpreadsheetView1.DataBind();

For completeness, these are some of the resources I found before trying it myself: