Posted in Development on Saturday, Saturday, October 18, 2008 by Anthony Burns
I'm digging the new ASP.NET MVC framework. Big style. Transitioning from classic ASP to ASP.NET left me a bit frustrated with the abstraction introduced. It was supposed to be better for us developers that the stateless nature of HTTP was being hidden away, that the framework would take care of all the state management, leaving us time to eat pizza and drink beer; but that just left me fat and drunk. Thankfully I can now dive back into having full control of my html and post data, among other things, by using MVC.
However, I don't want to hand craft every single piece of html every time, and there's a lot of things day to day that I just want rendering without a lot of effort - tables being one of them, so I set about building a simple but relatively powerful table control.
Requirements: I wanted the table to render an IEnumerable I might pull straight from a Linq query to a html table; including the ability to make each row a link, to highlight a selected row and the facility to add columns to the end of the table containing action links such as "Delete".
After Googling 'asp net mvc controls' I discovered a few different ways of creating and rendering controls under the MVC framework. I finally opted to write an extension method to the HtmlHelper class, made available through the Html property on each MVC View.
Since this probably won't be the last control I'll build, I created an interface each control could implement:
interface ISimpleControl {
string Render(HtmlHelper helper)
}
I then created a small HtmlHelper extension method to render these simple controls:
public static string RenderControl(this HtmlHelper helper, ISimpleControl control)
{
return control.Render(helper);
}
I can now render any simple control using:
<%=Html.RenderControl(new MyFirstControl());%>
Because my datasource could be any type of object returned from a Linq query (including anonymous types) I have to use reflection to discover the properties of these objects in order to render the columns of the table. I opted to do this as soon as the data source is set and convert it to a DataTable for easy traversal when it comes to rendering the html:
private static DataTable GetDataTableFromIEnumerable(IEnumerable dataSource)
{
// Since IEnumerable doesn't expose an indexer to get the first element
object obj;
foreach (object o in data)
{
obj = o;
break;
}
if (obj == null) return null;
DataTable dataTable = new DataTable();
// Use reflection to create DataColumns for each of the properties in the object
Type type = obj.GetType();
PropertyInfo[] propertyInfo = type.GetProperties();
string[] keys = new string[propertyInfo.Length];
for (int i = 0; i < keys.Length; i++)
{
DataColumn column = new DataColumn(propertyInfo[i].Name, typeof(string));
dataTable.Columns.Add(column);
keys[i] = propertyInfo[i].Name;
}
// Iterate through the IEnumerable and populate the DataTable from
// each object using Reflection
foreach (object row in dataSource)
{
DataRow dataRow = dataTable.NewRow();
foreach (DataColumn column in dataTable.Columns)
{
object result = type.InvokeMember(column.ColumnName, BindingFlags.GetProperty,
null, row, null);
if (result != null)
{
dataRow[column] = result.ToString();
}
}
dataTable.Rows.Add(dataRow);
}
return dataTable;
}
The first thing I do is to get the first element in the IEnumerable to perform the reflection on. Since IEnumerables don't expose an indexer, the only way I could think to do this is by performing a foreach loop and exiting as soon as I have a copy of the first element. I then get a list of the properties exposed by the object and create matching DataColumns for the DataTable. The second foreach loop populates a DataRow from each object and appends it to the table.
It's highly probable that I'll make other controls that iterate over IEnumerable data sources in this manner further down the road, so I opted to extract this functionality into an abstract base class called DataControl. This control exposes a public DataSource property for setting the IEnumerable, which converts it into a DataTable available to any derived classes through a protected property:
public abstract class DataControl : ISimpleControl
{
protected DataTable DataTable { get; set; }
public IEnumerable DataSource
{
set
{
DataTable = GetDataTableFromIEnumerable(value);
}
}
...
}
A derived class could then be used with an IEnumerable from your ViewData like so:
<%=Html.RenderControl(new MyDataControl{ DataSource = (IEnumerable)ViewData["MyLinqResult"] });%>
All that's left to do is to inherit from that DataControl and override the Render method to render the html:
public override string Render(HtmlHelper helper)
{
if (DataTable == null) return "";
// Use either the columns requested, or all table columns if not specified
string[] keys = DisplayColumns ?? GetColumnKeys(DataTable);
StringBuilder header = new StringBuilder();
// Build the html for the table's header
header.AppendLine(" <tr>");
foreach (string key in keys)
{
header.AppendLine(" <th>" + key + "</th>");
}
header.AppendLine(" </tr>");
bool altRow = true;
StringBuilder data = new StringBuilder();
// Render the relevant Html for each Row in the table
foreach (DataRow row in DataTable.Rows)
{
// Add an 'Alt' Css class to alternating rows
string rowClass = altRow ? "Alt" : "";
data.AppendLine(" <tr class=\"" + rowClass + "\">");
// Render the Html for each column of this row
foreach (string key in keys)
{
data.AppendLine(" <td>" + row[key] + "</td>");
}
data.AppendLine(" </tr>");
// Switch whether or not this is an alternating row
altRow = altRow ? false : true;
}
// Piece together all of the Html and return it
StringBuilder html = new StringBuilder();
html.AppendLine("<table>");
html.Append(header);
html.Append(data);
html.AppendLine("</table>");
return html.ToString();
}
I've also added a DisplayColumns property of type string[] to the Table class that enables me to specify the columns to render, as I might not want to render all of the properties. In line 6 I check for the existence of this property and if it hasn't been set I call GetColumnKeys() which is a private method I've written that returns a string[] of the DataColumn names from the DataTable.
I then render the html for the table header, and follow that up by rendering each row from the DataTable. I add an "Alt" class to each alternating row so I can use Css to colour the alternating rows differently.
I can now render a table in my View with a single line of code:
<%=Html.RenderControl(new Table{ DisplayColumns = new string[] { "Name", "EMail" },
DataSource = (IEnumerable)ViewData["MyLinqResult"] });%>
I'll finish this control in the next article by adding the functionality to click a row to fire an action, highlight a selected row, and include further actions in additional columns at the end of the table.
You can download the full source code from this article here: MvcTableControl.zip
Posted in Development on Wednesday, Wednesday, May 21, 2008 by Anthony Burns
DISCLAIMER: I'm in the privileged position of being able to dictate which browser our users use to access our extranet at work - I insist that IE must be used, not because I think it's a better browser, but because everyone has it installed by default and they know how to use it. By restricting to only one browser, I can speed up development times by not having to test our pages across multiple browsers. Because of that, this article is for IE only, and hasn't even been tested outside of IE7. If it proves interesting/helpful to people, then I might do an update later that works cross-browser.
I developed a webform for a project at work recently, that had a lot of form elements and that needed a file upload component adding.
The form was already too busy from a UI perspective and there wasn't a great location to plop down a file input element and submit button to upload the file. I decided I'd prefer it if I could just have an "Upload File" link, that when clicked prompted the user to select a file and caused the form to submit. I knew this was possible because Hotmail use a similar concept for attaching files to an email.
After a quick Google I discovered that the file input element in IE had a Javascript click() method - this I could use to open a file select dialog and I could then use form.submit() to upload the file. Simple.
I knocked up a quick prototype html file with the following code:
1: <script type="text/javascript">
2:
3: function UploadFile()
4: {
5: var form = document.UploadForm;
6: form.FileUpload.click();
7: form.SubmitButton.click();
8: }
9:
10: </script>
11:
12: <form id="UploadForm" name="UploadForm">
13: <input type="file" id="FileUpload" name="FileUpload" />
14: <input type="submit" value="Upload" id="SubmitButton" name="SubmitButton" />
15: </form>
When the link was clicked IE prompted me to select a file, but after I'd selected a file, IE gave me a Javascript "Access is denied" error. Bollocks.
After a long time spent on Google, I discovered that IE doesn't allow you to programmatically click the file input control and submit the form. Why on earth you'd want to click the control but not submit the form is beyond me. Even more bizarre is the fact that IE WILL allow you to do this, if the form is contained inside an IFrame. Bizarre, but a solution nonetheless.
So I constructed an UploadFile.aspx file containing the code below.
1: <form id="UploadForm" runat="server" enctype="multipart/form-data">
2: <asp:FileUpload ID="FileUpload" runat="server" />
3: <asp:HiddenField ID="UploadPath" runat="server" />
4: <asp:HiddenField ID="UploadLinkId" runat="server" />
5: <asp:Button UseSubmitBehavior="true" ID="SubmitButton" runat="server" />
6: </form>
7:
8: <a href="javascript:document.UploadForm.FileUpload.click();document.UploadForm.submit();">Upload File</a>
The form contains a file upload element, a submit button and two hidden fields. I plan on using javascript to set these hidden fields, one to enable me to specify a subfolder to upload the file to, and one to keep a reference to the "Upload File" link.
I then created a TestForm.aspx file containing a link and a hidden IFrame to contain my UploadFile.asp page:
1: <head>
2: <title>One-Click Upload</title>
3: <script type="text/javascript" src="Javascript/Upload.js"></script>
4: </head>
5: <body>
6:
7: <p><a href="#" id="UploadLink" onclick="return UploadFile(this, '');">Upload File</a></p>
8:
9: <iframe src="UploadFile.aspx" frameborder="0" id="UploadFrame" name="UploadFrame" style="display: none">
10: </iframe>
11:
12: </body>
13: </html>
In order to tie the "Upload File" link and the upload form within the IFrame together, I wrote the following Javascript function:
1: function UploadFile(link, uploadPath) {
2:
3: var uploadStatus = '<b>Uploading, please wait...</b>';
4:
5: // Check to see if we are already uploading a file
6: if(link.innerHTML.toLowerCase() == uploadStatus.toLowerCase()) {
7: alert('A file is currently being uploaded, please wait for it to finish before uploading another.');
8: } else {
9:
10: // Set our hidden fields and cause IE to prompt for a file
11: var form = UploadFrame.document.UploadForm;
12: form.UploadPath.value = uploadPath;
13: form.UploadLinkId.value = link.id;
14: form.FileUpload.click();
15:
16: // If the user has selected a file then submit the form
17: if(form.FileUpload.value != '') {
18: form.SubmitButton.click();
19:
20: // Make a copy of the link text and change it to show that the file is being uploaded
21: link.rel = link.innerHTML;
22: link.innerHTML = uploadStatus;
23: }
24: }
25:
26: return false;
27: }
It takes a reference to the link, and a subfolder to upload the file to as parameters. When called, it sets the ID of the link (to be utilised later on) and the upload path in the hidden form and then causes IE to prompt for a file. If a file is selected, it then submits the form and changes the text of the link to indicate that the upload is in progress.
On a subsequent click, it detects that the link contains the new text and explains to the user that an upload is already in progress.
So clicking the "Upload File" link prompts the user to select a file and if a file is selected, it submits the form upload the file.
The code behind the UploadFile.aspx file handles the uploading of the file:
1: protected void Page_Load(object sender, EventArgs e)
2: {
3: if (IsPostBack)
4: {
5: string filePath = ConfigurationManager.AppSettings["OneClickUploadFilePath"];
6: if (UploadPath.Value != "")
7: {
8: filePath = Path.Combine(filePath, UploadPath.Value);
9: }
10:
11: if (!Directory.Exists(filePath))
12: {
13: Directory.CreateDirectory(filePath);
14: }
15:
16: FileUpload.SaveAs(Path.Combine(filePath, FileUpload.FileName));
17:
18: Body.Attributes["onload"] = "UploadFinished('" + UploadLinkId.Value + "');";
19: }
20: }
First we get the upload path from the Application Settings of the web.config file (obviously you set this yourself in your own web.config. This is then combined with any subfolder passed through the form, the directory is created if it doesn't already exist and the uploaded file is saved to it.
The final line sets the onload attribute to call a Javascript function called UploadFinished with the ID of the link received from the form. That function can be used to notify the user that the file has finished uploading. In my case I also wanted to offer the option of refreshing the page, as my pages contain a list of the uploaded files:
1: function UploadFinished(linkId)
2: {
3: if(confirm('The file has finished uploading, would you like to refresh the page?')) {
4: parent.location.reload(true);
5: } else {
6: var link = parent.document.getElementById(linkId);
7: if(link != null) {
8: link.innerHTML = link.rel;
9: }
10: }
11: }
First it asks the user if they would like to refresh the page and if they click "Yes" then the page reloads. If they click "No", then the function uses the link ID - that has been passed through the form - to set the link text back to its original text, thereby enabling the user to upload an additional file.
So there we have it: upload a file using a single link rather than a pair of ugly form controls.
Tagged as: asp.net, csharp, javascript