Yes, I know, the multiple file uploading in ASP.NET issue has been exhaustively covered already elsewhere. There are examples of multiple file uploading using ASP.NET user controls, there are examples using jQuery, there are examples using flash to provide progress feedback, and even examples in Silverlight.
However, what I wanted was a simple solution which allows the user to select a variable number of files, didn’t rely on postbacks in order to increase or decrease the number of files selected, and also allows the user to enter a description for each uploaded file. The description was to be saved to the database, and the file saved to the server’s file system (with the path stored in the database so the file can be subsequently retrieved).
The way the page should work was also straightforward: the user should initially be presented with a single file upload control and file description textbox, but they should have the option to add and remove additional rows. Here’s a quick screengrab to illustrate what we’re aiming for:
It is very possible to create an upload page of this type using a ListView or Repeater control, using similar code to that I outlined in an earlier article, however this would require additional postbacks for the user to add or remove files, which is not what I wanted.
The alternative is, of course, to use javascript to create the additional upload controls and textboxes. Not in itself a hugely tricky task, but there is one usability issue which needs to be resolved. As any ASP.NET developer already knows, any changes made to the page client-side are not persisted by the ASP.NET viewstate, so will disappear after the page posts back.
This is not particularly desirable behaviour in this case. Let’s say after the user adds several files and clicks the upload button, and I then want to validate the file extensions server side, in order to display a message to the user if they have uploaded a file with an invalid extension (I may want to exclude all .exe files, for example). If all the file input fields have been created using javascript, after the postback they will have disappeared, so the user will have to input the whole lot again if the validation fails. This would be a hugely irritating thing to force users to do.
The solution is to manually re-create the controls on each page load. This involves keeping track of the number of files which have been created each time the page posts back, and ensuring that the appropriate javascript is invoked to rebuild the table.
So, enough with the rambling: let’s get to the code.
I started off with some page markup and a little CSS:
<html xmlns="http://www.w3.org/1999/xhtml" >
<head id="Head1" runat="server">
<title>Upload Multiple Files</title>
<style type="text/css">
#tblFilesContainer
{
width: 80%;
border: solid 1px black;
}
#tblFilesContainer tr td
{
border: dashed 1px black;
}
#tblFilesContainer tr.noborder td
{
border: none;
}
#tblFilesContainer tr td input
{
width: 99%;
}
</style>
</head>
<body>
<form id="form1" runat="server" enctype="multipart/form-data" method="post">
<h3>Upload Files</h3>
<br/>
<table id="tblFilesContainer" style="width:80%; border: solid 1px black">
<tr class="noborder">
<td style="width: 10%"></td>
<td style="width: 30%"></td>
<td style="width: 10%"></td>
<td style="width: 30%"></td>
<td style="width: 20%"><a href="#" onclick="return addFile();" >Add File</a></td>
</tr>
</table>
<br />
<asp:Button ID="btnUpload" runat="server" Text="Upload" OnClick="btnUpload_Click" />
</form>
</body>
</html>
In the markup above, I have created a table which will act as the container for the file upload controls and description textboxes, a link which will act as the trigger for adding an additional file input control row to the table, and some very basic styles to add some borders to the table. Note that the link is executing a javascript function named addFiles() onclick (and also passing itself in as a parameter value): we’ll return to this shortly.
Finally, there is a button which will be used to submit the page and kick off the uploading- this is hooked up to a server-side event handler.
One thing to watch out for: you must remember to add the enctype="multipart/form-data" attribute to the form tag on the page. ASP.NET only adds this when one or more FileUpload controls exist on a given page. As all the file input fields are being created by javascript in this case, it won’t get added unless you do so manually.
It was then time to start with the javascript. The table which contains the upload controls has five cells on each row, so we’re going to need a bit of code which can create a table row with five cells, and insert it into the existing table, but leaving the last table row in the same position (as it contains the ‘Add File’ link). I started by placing the HTML markup for each cell into a javascript array in a script block:
<script type = "text/javascript">
var cellTemplates = new Array();
cellTemplates[0] = 'Description:';
cellTemplates[1] = '<input id="txtDescription{counter}" name="txtDescription{counter}" type="text" value="{value}" />';
cellTemplates[2] = 'Select File:';
cellTemplates[3] = '<input id="file{counter}" name="file{counter}" type="file" />';
cellTemplates[4] = '<a href="#" id="lnkRemoveFile{counter}" onclick="return removeFile(this);">Remove</a>';
var counter = 0;
ript>
This was to prevent having to do a whole bunch of messy string manipulation in the function which will add new rows to the table.
There is also a variable named ‘counter’ which is used to track the current file input row count.
Note the above HTML markup contains a couple of placeholders: {counter} and {value}. These placeholders are replaced at runtime with variable values. The {counter} placeholder is replaced with the value of the counter variable. This is to ensure all the controls are named sequentially to make it easier to retrieve their values server-side. The {value} placeholder is replaced with the file description- and this only occurs when the page is reloaded after a postback.
Now for the functions to add and remove file input rows. Here’s the full code:
//adds a file input row
function addFile(description) {
//increment counter
counter++;
var tbl = document.getElementById("tblFilesContainer");
var rowCount = tbl.rows.length;
var row = tbl.insertRow(rowCount - 1);
var cell;
var cellText;
for (var i = 0; i < cellTemplates.length; i++) {
cell = row.insertCell();
//format the cell template text
cellText = cellTemplates[i].replace(/\{counter\}/g, counter).replace(/\{value\}/g,
(description == null) ? '' : description);
cell.innerHTML = cellText;
}
}
//removes a file input row
function removeFile(ctrl) {
var tbl = document.getElementById("tblFilesContainer");
if (tbl.rows.length > 2)
tbl.deleteRow(ctrl.parentNode.parentNode.rowIndex);
}
The addFile() function starts by retrieving the current file input row count and incrementing it- it then adds the additional row to the table, by retrieving the template HTML markup from the cellTemplates array and replacing the placeholders with the appropriate values, as outlined above. It makes use of javascript’s regular expression find-and-replace functionality to inject the counter and description values. And don’t forget the ‘g’ modifier at the end of the regular expression; this results in a global find-and-replace (i.e. all matching instances of the search pattern are replaced- the default behaviour is just to replace the first instance found).
As mentioned above, when a ‘Remove File’ link is clicked, the link passes itself as the parameter value to the removeFile method. The method uses this to select the link’s parent node (i.e. the table cell it is in), and then the parent of that node (the table row) and removes it,
I added one additional javascript function to reinitialise all the textboxes after a page reload: here it is:
//creates all file inputs on page load
function createFiles(descriptions) {
for (var i = 0; i < descriptions.length; i++) {
addFile(descriptions[i]);
}
}
The method simply accepts in an array of file descriptions, and uses this to recreate each of the file input rows.
Then to the server-side code. Firstly, the handler for the upload button’s click event: this needed to extract the files and descriptions from the HttpRequest and pass all this information through to a business logic class for validation and saving. In my code, I am collecting all the files and descriptions first, and passing these through to the business logic layer in a single hit, but to keep the example concise, I have removed all this extraneous code to concentrate on getting the file and description. Here is the button click event handler in full:
protected void btnUpload_Click(object sender, EventArgs e)
{
int counter;
string description;
HttpPostedFile file;
foreach (string key in Request.Files.Keys)
{
//get a reference to the file
file = Request.Files[key];
if (file != null && file.ContentLength > 0)
{
//get the value of the description textbox
counter = int.Parse(key.Replace("file", string.Empty));
description = Request.Form["txtDescription" + counter.ToString()];
//save file and description here...
}
}
}
Because the file upload controls are description textboxes were named with matching integer values appended to their names, it is possible to iterate over the file input IDs and use each ID to find the relevant description in the Request.Form collection.
So everything is now in place to allow users to upload multiple files, but we still haven’t solved the original problem of maintaining state between postbacks.
The final code example fills in the last missing piece of functionality:
protected void Page_Load(object sender, EventArgs e)
{
//register the client scripts
this.RegisterFileInputScripts();
}
/// <summary>
/// Registers the startup scripts to create the file input controls.
/// </summary>
private void RegisterFileInputScripts()
{
if (!Page.IsPostBack)
{
//JS: create the initial file input control
Page.ClientScript.RegisterStartupScript(this.GetType(),
"CreateFileInput", "addFile();", true);
}
else
{
StringBuilder text = new StringBuilder();
int counter;
SortedList<int, string> descriptions = new SortedList<int,string>();
foreach (string key in Request.Files.Keys)
{
counter = int.Parse(key.Replace("file", string.Empty));
descriptions.Add(counter, (Request.Form["txtDescription" + counter.ToString()]));
}
//JS: add the file descriptions to a javascript array
text.Append("var descriptions = new Array(");
int i = 0;
foreach (KeyValuePair<int, string> item in descriptions)
{
text.AppendFormat("'{0}'", item.Value ?? string.Empty);
if (++i < descriptions.Keys.Count)
text.Append(",");
}
//JS: close the array declaration &invoke the createFiles method
text.Append("); createFiles(descriptions);");
//register the javascript
Page.ClientScript.RegisterStartupScript(this.GetType(), "RecreateFileInputs",
text.ToString(), true);
}
}
The code snippet above ensures that the addFile() method is called once when the page first loads, to create the initial file input control. And on each postback it iterates over the posted files collection, building up an array of file descriptions corresponding to each posted file, and finally registers a call to the createFiles javascript method, passing in an array containing all the descriptions.
And that's a wrap: a basic multiple file upload system, driven by client-side javascript rather than server-side code.
Here’s a quick tip for string cleaning in SQL. Let’s say you have a string, perhaps one being imported from another data source, and you want to trim all the leading and/or trailing non-alphanumeric characters. For example, you may have the input string:
',,, foo bar $$$'
Which needs to be cleaned before inserting into the database. Perhaps you want to import it as this:
'foo bar $$$'
Having removed all the leading junk. Or perhaps you want to import it as this:
'foo bar'
Having removed the garbage from both sides. One way you could do this is using a UDF to loop through the characters in the input string, for example:
1: create FUNCTION fn_ltrim_char
2: (
3: @input nvarchar(255),
4: @charToTrim char
5: )
6: RETURNS nvarchar(4000)
7: AS BEGIN
8:
9: DECLARE @counter int
10: SET @counter = 0
11:
12: DECLARE @output nvarchar(4000)
13: SET @output = ''
14:
15: WHILE @counter <= LEN(@input) BEGIN
16:
17: IF @output <> '' OR SUBSTRING(@input,@counter,1) <> @charToTrim
18: SET @output = @output + SUBSTRING(@input,@counter,1)
19:
20: SET @counter = @counter + 1
21:
22: END
23:
24: RETURN @output
25: END
Whilst there’s nothing really wrong with this approach, it does feel a little clunky. There’s a lot of duplication of data during the copy and the whole idea of having to write custom a UDF to do this feels a bit overkill.
So, is it possible to do this without resorting to a custom function? Well, yes. This can be achieved in a single line of SQL using the handy PATINDEX built-in function. Here’s an example of using PATINDEX to trim the leading non-alphanumeric characters from a string:
1: --trim leading non-alphanumeric chars
2: DECLARE @input nvarchar(4000)
3: SET @input = ',,, foo bar $$$'
4:
5: SELECT SUBSTRING(@input, PATINDEX('%[a-zA-Z0-9]%', @input), LEN(@input))
In the above code snippet, PATINDEX is used to find the first occurrence of an alphabetic or numeric character in the string (i.e. the first character we want to keep). Once this is found, it’s a straightforward task to extract a substring starting at this character, and discard the rest.
When it comes to trimming the trailing junk characters, however, things are not quite so simple. PATINDEX is used to search for the first given occurrence of a substring which matches the search pattern. Note: there is no out-of-the-box solution for finding the index of the last matching substring- not by using PATINDEX alone, at any rate. I did a (very) quick survey of other developers’ solutions to this problem, and came up with this one. However, this solution involves the use of another custom UDF, which is exactly what I wanted to avoid.
However, all was not lost just yet. Consider the following: we already have a solution that works for removing the leading non-alphanumeric characters, removing the trailing alphanumeric characters is basically the same problem in reverse, and T-SQL has a wonderful little built-in function which can REVERSE a string. So, actually, all we need to do to discover the last given occurrence of a matching substring in a string, is to:
- Reverse the string.
- Find the first occurrence of the search pattern in the string and store the index.
- Subtract that index from the whole string length to give you the same index relative to the end of the string.
Here’s an example of doing just that:
1: DECLARE @input nvarchar(4000)
2: SET @input = 'foo bar%&^$$$'
3:
4: --get the index of the last alphanumeric character
5: DECLARE @lastOccurrenceIndex int
6: SELECT @lastOccurrenceIndex = LEN(@input) - PATINDEX('%[a-zA-Z0-9]%', REVERSE(@input))
7:
8: --select everything up to & including the last alphanumeric character
9: SELECT SUBSTRING(@input, 1, @lastOccurrenceIndex + 1)
So, now we need to put the above technique into action to solve the problem at hand. The next example shows how to trim trailing non-alphanumeric characters using PATINDEX and REVERSE:
1: --trim trailing non-alphanumeric chars
2: DECLARE @input nvarchar(4000)
3: SET @input = ',,, foo bar $$$'
4:
5: SELECT REVERSE(SUBSTRING(REVERSE(@input), PATINDEX('%[a-zA-Z0-9]%', REVERSE(@input)), LEN(@input)))
And, finally, we can now combine both techniques to trim both leading and trailing non-alphanumeric characters, like so:
1: --trim both leading and trailing non-alphanumeric chars
2: DECLARE @input nvarchar(4000)
3: SET @input = ',,, foo bar $$$'
4:
5: SELECT REVERSE(SUBSTRING(REVERSE(SUBSTRING(@input, PATINDEX('%[a-zA-Z0-9]%', @input),
6: LEN(@input))), PATINDEX('%[a-zA-Z0-9]%', REVERSE(SUBSTRING(@input,
7: PATINDEX('%[a-zA-Z0-9]%', @input), LEN(@input)))), LEN(@input)))
And there we have it- a neatly cleaned string, with no need for any additional UDFs.
I have recently completed work on a WPF application which required a reasonable amount of interaction with Microsoft Outlook. Specifically, we needed to implement two operations: firstly, we needed the ability to generate a new email and open it in Outlook (without sending it); and secondly, we needed to send an email via Outlook behind the scenes, without any user interaction.
There were a couple of additional requirements affecting how this functionality could be implemented. One of these was a technical issue, and the other was mandated by the business for whom the application is intended.
The first stipulation was that whatever code we used to communicate with Outlook needed to support multiple versions of Office. Our end-users run a mix of Office 2003 and 2007, and the requirement was that those currently running Office 2003 should be able to upgrade to Office 2007 without having to install a new version of the WPF client application.
The second restriction was that all emails should be sent using the standard company branding; in particular, all emails needed to use their standard email signature.
In order to solve the first problem, we wanted to avoid adding a reference to the Outlook COM interop assembly. Each version of Office has its own Outlook COM interface implementation (and therefore its own version of the Primary Interop Assembly, which stores the assembly metadata which allows .NET to marshal the calls to and from the COM interface), and backwards compatibility is by no means guaranteed.
Normally, when developing for a specific Outlook version, a reference is added to the COM interface via the COM Components tab of the ‘Add Reference’ dialog in Visual Studio like so:
This gives you strongly typed access to the properties and methods of the Outlook interface classes. The alternative to this is late binding, and in C#, this means but one thing: reflection, and lots of it (until C# 4.0, anyways).
Of course, the first problem you have when attempting to use reflection alone for Office automation is how to get an instance of the relevant Office application interface without directly instantiating one. One easy way to do this is using the application’s registry ProgID, and the Type.GetTypeFromProgID() method. Here’s an example of how to do this for Outlook:
1: Type outlookAppType = Type.GetTypeFromProgID("Outlook.Application");
2: object appInstance = Activator.CreateInstance(outlookAppType);
Once you have a new instance of the interface, from there on in, it’s just a case of calling Type.InvokeMember() against the relevant object instance to invoke the operations by name. To illustrate, here’s how you create a new mail message using the above application instance:
1: object message = appInstance.GetType().InvokeMember("CreateItem", (BindingFlags.InvokeMethod),
2: null, appInstance, new object[] { 0 });
The above code results in a new Microsoft.Office.Interop.Outlook.MailItem instance, which is the COM interface class used to represent a mail message.
There were plenty of examples around on the web of how to use the Outlook Interface to send or open a message, so once I had discovered how to get a reference to an interface instance indirectly, converting the rest of the code to perform the functionality using reflection was relatively trivial.
The next problem was slightly more complex to solve. The email body content produced by the WPF application is all formatted as HTML, however when we attempted to send or open the email, we found that the company-specific email signature footer content was not appearing as part of the generated email body.
The solution to this consisted of two parts: firstly, there is a rather obscure (and entirely unhelpfully named) property on the MailItemClass named GetInspector. For some reason, presumably known only to those who designed it, the act of calling this property and getting the resulting value (even if you don’t use the returned value for anything) seems to make the difference between the email signature being incorporated into the email body or not.
Yes, that’s right: GetInspector. It’s all so obvious now- I can’t believe I missed it. 
However, there was one final hoop to jump though. The GetInspector() method loads the signature into the message body, however the signature is itself an HTML document. So now I had 2 HTML documents: the one produced by the application which needed to be sent, and the one containing the email signature which needed to be included.
The only sensible solution was to merge the two HTML documents. However you can’t simply append one to the other, otherwise you end up with a second opening HTML tag, after the first document’s closing HTML tag, which is not valid markup.
You need to ensure the HTML is well-formatted: the body contents of one document needs to be appended/prepended to the other to ensure we end up with valid HTML. I accomplished this by once again using the invaluable HTML Agility Pack’s HTML Parser. For anyone looking for a good .NET HTML Parser- this one comes highly recommended. Using this component, locating the body node of the HTML document produced by the WPF app, copying all of its child nodes to the HTML document produced by Outlook, then taking all the content of this combined document and using it as the email content was a breeze.
So now I had a class able to open/send messages via Outlook, without any COM references, which included the company-specific email signature- just the ticket! I also constructed the code to make it fairly generic, in order to leave open the option of adding support for other email clients at a future date.
As I mentioned earlier, each new version of Microsoft Office results in new COM interface versions for all of the Office applications, which may or may not be compatible with the old ones. So, whilst I am not claiming this solution is future proof, it does work with both Office 2003 and 2007, which was the intended aim.
The code, along with a test application, can be downloaded using the link at the top of the article.
Ran into a slightly problematic issue today: I was working on an SSRS report project which had been pulled down from source control. The project contains several reports, each of which contains a number of embedded images. I was tasked with developing a number of new reports, each of which had to follow the same styling.
All well and good so far, but I quickly realised that the source image files had not also been committed to version control; the images existed only within the XML of the report RDL files.
The SSRS designer doesn’t provide any method by which to copy embedded images from one report to another, so my problem was twofold: firstly, I needed to find a method to copy an embedded image between reports, and secondly, I needed to find a method to extract the embedded image from the RDL file so the image file itself could be committed to source control to make life easier for future developers working on the project.
The first problem has several simple solutions. The easiest of all is to just copy-and-paste an existing RDL file into the project directory, rename the copied file and then delete all of its content via the Business Intelligence Studio designer. This will result in a new, empty report, which can be used as a template- and it will still contain all the embedded images/custom code etc as the original.
Unfortunately (for me) I had already started development on one of the reports when I realised this was a problem, and I wanted to keep my existing report file, so I needed a way to import the embedded image into my existing report.
It turns out that this is also easy enough. In order to do this you can open the RDL file which contains the embedded images in notepad, navigate down the XML until you find the EmbeddedImages node, then copy this node and all of its children into the new report- to the same location, below the root Report node.
The embedded images node looks something like this:
1: <EmbeddedImages>
2: <EmbeddedImage Name="aircargo_jpg">
3: <MIMEType>image/jpeg</MIMEType>
4: <ImageData>/9j/4AAQSkZJRgABAQEAYABg...</ImageData>
5: </EmbeddedImage>
6: </EmbeddedImages>
The second problem was a bit trickier. Once again, SSRS doesn’t provide an out-of-the-box solution to enable you to save an existing embedded image to disk- once it has been embedded, there’s no straightforward way to get it back out again (this is true of both BIDS 2005 and BIDS 2008- hopefully Microsoft will add this functionality for the next SQL Server and BIDS release).
All the image data is in the XML; as you can see in the markup above, there is a parent EmbeddedImages node which contains a collection of EmbeddedImage nodes. Each embedded image has a Name attribute, a child node which describes the image MIME type, and another child node which contains the image binary contents encoded as a base64 string.
Ultimately, I came up with a little windows app to extract the images to file for me. The code loads a given RDL file into a new XmlDocument instance, locates the EmbeddedImages parent node, then iterates over each of the embedded image child nodes, writing the content to disk.
Although there have been many changes to the RDL XML structure between SSRS 2005 and SSRS 2008, the embedded images XML is the same in both, so this solution will work with either version.
Hopefully this utility will be of use to somebody- you can download the source code using the link at the top.
Here's a really short and simple bit of code that has the potential to be a bit of a time-saver.
The FindControl method of the Control class is used to find a specific child control of a given parent, searching by ID. This method, however, doesn't search the control hierarchy recursively: it searches the direct children of the specified parent control only.
While writing a recursive version of this method is trivial, a rather nice way to make the method reusable is to implement it as an extension method. Here's how it can be done:
namespace Boo.Web.UI.Extensions
{
public static class ControlExtensions
{
/// <summary>
/// recursively finds a child control of the specified parent.
/// </summary>
/// <param name="control"></param>
/// <param name="id"></param>
/// <returns></returns>
public static Control FindControlRecursive(this Control control, string id)
{
if (control == null) return null;
//try to find the control at the current level
Control ctrl = control.FindControl(id);
if (ctrl == null)
{
//search the children
foreach (Control child in control.Controls)
{
ctrl = FindControlRecursive(child, id);
if (ctrl != null) break;
}
}
return ctrl;
}
}
}
And to call it:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using Boo.Web.UI.Extensions;
namespace MyWebApp
{
public partial class WebForm1 : System.Web.UI.Page
{
public void Page_Load(object sender, EventArgs e)
{
//call the recursive FindControl method
Control ctrl = this.FindControlRecursive("my_control_id");
}
}
}
Don't forget to import the namespace within which the extensions method class is declared- or a compilation error will not long follow.
Now, all you need to do is import this same namespace for any pages/user controls/custom controls which need to use the recursive control search and you're good to go.
Doing it this way is of course logically no different to creating a static method in a util class, and passing both the parent control and the ID of the child control you want to find to it, but considering how commonly this functionality is required, it's nice to be able to tack it onto the control class itself, and extension methods provide an elegant way to accomplish this.
I have been playing around with the ASP.NET ListView and Repeater controls quite a bit recently, and thought that a simple calendar control with date range selection capabilities baked-in would make a good example of how these controls can be used to produce some pretty neat functionality without a great deal of code.
The System.Web.UI.WebControls namespace already contains a calendar control of course, which is itself quite feature-rich. Although it doesn’t offer support for date range selection out-of-the-box, it can quite easily be extended to do so. The built-in ASP.NET Calendar Control also has a wealth of properties which enable its layout and appearance to be tweaked as necessary. So really, the code for this article is by way of example only; the date range selection functionality is useful, or the code could be used as the starting point for a custom calendar control if you have a need to develop one.
The calendar is implemented as a UserControl, and the UserControl contains a Repeater control, nested within an HTML table. The repeater is used to generate the linkbuttons for each of the calendar days for the current month. The end result looks, as you might expect, not entirely dissimilar to the ASP.NET Calendar:
The binding of the repeater control is performed using two classes: CalendarMonth and CalendarWeek. The CalendarMonth class represents a single month within a specific year, and the code looks like this:
1: public class CalendarMonth : IEnumerable<DateTime>
2: {
3:
4: private List<DateTime> _days;
5:
6: public CalendarMonth(int month, int year)
7: {
8: this._days = new List<DateTime>();
9:
10: DateTime date = new DateTime(year, month, 1);
11: while (date.Month == month)
12: {
13: this._days.Add(date);
14: date = date.AddDays(1);
15: }
16: }
17:
18: #region IEnumerable<DateTime> Members
19:
20: public IEnumerator<DateTime> GetEnumerator()
21: {
22: return this._days.GetEnumerator();
23: }
24:
25: #endregion
26:
27: #region IEnumerable Members
28:
29: System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
30: {
31: return this._days.GetEnumerator();
32: }
33:
34: #endregion
35:
36: /// <summary>
37: /// Gets a collection of calendar weeks for the current month.
38: /// </summary>
39: /// <returns></returns>
40: public IList<CalendarWeek> GetWeeks()
41: {
42: List<CalendarWeek> weeks = new List<CalendarWeek>();
43: CalendarWeek currentWeek = null;
44: for (int i = 0; i < this._days.Count; i++)
45: {
46: if (currentWeek == null || this._days[i].DayOfWeek == DayOfWeek.Monday)
47: {
48: if (currentWeek != null)
49: weeks.Add(currentWeek);
50:
51: currentWeek = new CalendarWeek();
52: }
53:
54: currentWeek[this._days[i].DayOfWeek] = this._days[i];
55:
56: if (i == this._days.Count - 1)
57: weeks.Add(currentWeek);
58: }
59:
60: return weeks;
61: }
62: }
As you can see from the above, there isn’t a great deal of functionality exposed: just a single public method which returns a collection of CalendarWeek instances. It is the collection returned from this method which is bound to the repeater on the page. The CalendarWeek class is intended to represent a single Monday-Sunday period within a given CalendarMonth. The CalendarWeek class is coded like this:
1: public class CalendarWeek
2: {
3: private System.Collections.Specialized.ListDictionary _days;
4:
5: internal CalendarWeek()
6: {
7: this._days = new System.Collections.Specialized.ListDictionary();
8:
9: //add the days of the week
10: for (int i = 1; i < 7; i++)
11: {
12: this._days.Add((DayOfWeek)i, DateTime.MinValue);
13: }
14: this._days.Add(DayOfWeek.Sunday, DateTime.MinValue);
15: }
16:
17: /// <summary>
18: /// Indexer to supply access to the date values.
19: /// </summary>
20: /// <param name="day"></param>
21: /// <returns></returns>
22: public DateTime this[DayOfWeek day]
23: {
24: get { return (DateTime)this._days[day]; }
25: internal set { this._days[day] = value; }
26: }
27: }
Each CalendarWeek instance contains a collection of seven DateTime instances which can be accessed by day of week. In order to achieve this, the class uses the System.DayOfWeek enumeration: each day of the week is added as the key to a lightweight key/value collection. The assigning of DateTime instances to each day in the CalendarWeek occurs in the GetWeeks() method of the CalendarMonth class, when the collection of CalendarWeek instances is generated. The CalendarWeek exposes a public indexer to allow each date value to be accessed. Of course, not every month starts on a Monday and ends on a Sunday, so one or more date values in the CalendarWeek instance may not be initialised- and the UI uses this to work out whether a linkbutton should be displayed for each day.
So the UI first creates a CalendarMonth instance, gets a collection of CalendarWeek instances for the month, then binds this collection to the repeater.
The markup for the UserControl (which contains the parent HTML table for the calendar, and repeater control which generates the weeks) is structured as follows:
1: <asp:Panel ID="pnlContainer" runat="server">
2: <table class="<%# CalendarCssClass %>">
3: <tr>
4: <td>
5: <asp:LinkButton ID="lnkBack" runat="server" OnClick="calendar_Back"><<</asp:LinkButton>
6: </td>
7: <td colspan="5" style="text-align:center">
8: <asp:Label ID="lblMonth" runat="server"></asp:Label>
9: </td>
10: <td runat="server" id="tdForward">
11: <asp:LinkButton ID="lnkForward" runat="server" OnClick="calendar_Forward">>></asp:LinkButton>
12: </td>
13: </tr>
14: <tr>
15: <td><asp:Literal ID="litMonday" runat="server"></asp:Literal></td>
16: <td><asp:Literal ID="litTuesday" runat="server"></asp:Literal></td>
17: <td><asp:Literal ID="litWednesday" runat="server"></asp:Literal></td>
18: <td><asp:Literal ID="litThursday" runat="server"></asp:Literal></td>
19: <td><asp:Literal ID="litFriday" runat="server"></asp:Literal></td>
20: <td><asp:Literal ID="litSaturday" runat="server"></asp:Literal></td>
21: <td><asp:Literal ID="litSunday" runat="server"></asp:Literal></td>
22: </tr>
23: <asp:Repeater ID="rptCalendar" runat="server"
24: onitemdatabound="rptCalendar_ItemDataBound"
25: onitemcommand="rptCalendar_ItemCommand">
26: <ItemTemplate>
27: <tr class="<%# CurrentRowCssClass %>">
28: <td runat="server" id="tdMonday"><asp:LinkButton ID="lnkMonday" runat="server" CausesValidation="false"></asp:LinkButton></td>
29: <td runat="server" id="tdTuesday"><asp:LinkButton ID="lnkTuesday" runat="server" CausesValidation="false"></asp:LinkButton></td>
30: <td runat="server" id="tdWednesday"><asp:LinkButton ID="lnkWednesday" runat="server" CausesValidation="false"></asp:LinkButton></td>
31: <td runat="server" id="tdThursday"><asp:LinkButton ID="lnkThursday" runat="server" CausesValidation="false"></asp:LinkButton></td>
32: <td runat="server" id="tdFriday"><asp:LinkButton ID="lnkFriday" runat="server" CausesValidation="false"></asp:LinkButton></td>
33: <td runat="server" id="tdSaturday"><asp:LinkButton ID="lnkSaturday" runat="server" CausesValidation="false"></asp:LinkButton></td>
34: <td runat="server" id="tdSunday"><asp:LinkButton ID="lnkSunday" runat="server" CausesValidation="false"></asp:LinkButton></td>
35: </tr>
36: </ItemTemplate>
37: </asp:Repeater>
38: </table>
39: </asp:Panel>
And to complete the picture, the code which initialises each repeater item when the control is data bound:
1: /// <summary>
2: /// Invoked when a repeater item is being data bound.
3: /// </summary>
4: /// <param name="sender"></param>
5: /// <param name="e"></param>
6: protected void rptCalendar_ItemDataBound(object sender, RepeaterItemEventArgs e)
7: {
8: if (e.Item.ItemType == ListItemType.Item || e.Item.ItemType == ListItemType.AlternatingItem)
9: {
10: this.InitialiseCalendarItem(e.Item);
11: }
12: }
13:
14: /// <summary>
15: /// Initialise calendar repeater item.
16: /// </summary>
17: /// <param name="item"></param>
18: protected void InitialiseCalendarItem(RepeaterItem item)
19: {
20: CalendarWeek week = (CalendarWeek)item.DataItem;
21: DateTime date;
22: LinkButton lnk;
23:
24: for (int i = 0; i < 7; i++)
25: {
26: DayOfWeek day = (DayOfWeek)i;
27: date = week[day];
28:
29: //initialise th date linkbutton
30: lnk = (LinkButton)item.FindControl("lnk" + day.ToString());
31: lnk.Visible = !(date.Equals(DateTime.MinValue));
32: if (lnk.Visible)
33: {
34: lnk.Text = date.Day.ToString();
35: lnk.CommandArgument = date.ToString();
36: }
37:
38: if (!date.Equals(DateTime.MinValue))
39: {
40: if (this.DateSelectionMode == SelectionMode.Single)
41: {
42: //if the current day is selected, highlight it
43: DateTime selectedDate = this.SelectedDate;
44: if (!selectedDate.Date.Equals(DateTime.MinValue) && date.Date.Equals(selectedDate.Date))
45: {
46: this.HighlightDay(item, day);
47: }
48: }
49: else
50: {
51: //if the current day is contained in the date range, highlight it
52: DateRange selectedRange = this.SelectedDateRange;
53: if (!selectedRange.Equals(DateRange.MinValue) && selectedRange.Contains(date))
54: {
55: this.HighlightDay(item, day);
56: }
57: }
58: }
59: }
60: }
61:
62: /// <summary>
63: /// Highlights a selected day in the current item.
64: /// </summary>
65: /// <param name="item"></param>
66: /// <param name="day"></param>
67: protected void HighlightDay(RepeaterItem item, DayOfWeek day)
68: {
69: HtmlTableCell td = (HtmlTableCell)item.FindControl("td" + day.ToString());
70:
71: if (!string.IsNullOrEmpty(this.SelectedDateCssClass))
72: {
73: td.Attributes.Add("class", this.SelectedDateCssClass);
74: }
75: else
76: {
77: td.BgColor = "red";
78: }
79: }
As an extra bit of sugar coating, the abbreviated day names (shown at the top of the control) are set dynamically in the code-behind, based on the current culture settings:
1: /// <summary>
2: /// Sets the abbreviated day names for the current culture.
3: /// </summary>
4: private void SetDayNames()
5: {
6: Array values = Enum.GetValues(typeof(System.DayOfWeek));
7: DayOfWeek day;
8: int dayNumber;
9: Literal lit = null;
10:
11: foreach (object value in values)
12: {
13: day = (DayOfWeek)value;
14: dayNumber = (int)day;
15: lit = (Literal)this.FindControl("lit" + day.ToString());
16: lit.Text = System.Globalization.CultureInfo.CurrentUICulture.DateTimeFormat.AbbreviatedDayNames[dayNumber];
17: }
18: }
The rest of the code in the UserControl is concerned with the navigation between months, and tracking the selected date or date range, and I won’t go into any of the details here- suffice to say it’s all pretty straightforward. The UserControl also exposes a few properties (all of which persisted via ViewState) for setting CSS classes on the calendar, for setting the selected date (or date range). There’s also a property for switching the selection mode between single date and date range, and finally, a couple of events for notifying when a date or date range is selected.
In summary, we have a pretty elegant, and readily extensible, calendar control in fewer than 800 lines of code…not bad! The UserControl, class files, and a test page are included in the zip file which can be downloaded using the link at the top of the article.
This is another installment of how to use the ASP.NET ListView and Repeater to generate repeating groups of controls in ASP.NET pages in scenarios which often lead developers to add unnecessary complexity to their pages by resorting to dynamically generated controls.
Part 2 demonstrated using the ListView control to maintain the values in a repeating group of textboxes, using a collection of strings as a data source,and allowing the user to add rows as required.
This works well for simple scenarios, however…what happens if each row consists of multiple values which need to be persisted in separate controls, while also allowing the user to add and remove rows?
In this case, the simplest solution is often to use a GridView. The ASP.NET GridView has excellent out-of-the-box functionality for displaying and editing data in an HTML table, and much of what it lacks can be compensated for using template columns to define your own cell content template.
However, you will frequently find that the lack of control over the formatting of the items rules the GridView out. For example, you may want to display your controls over multiple table rows, or you may simply not want to use an HTML table at all. This is where the Repeater and ListView controls become indispensible: they provide the developer with full control over the layout of the items, and can be bound to any data source which implements IEnumerable.
This sample demonstrates the binding of an ASP.NET ListView to a DataTable, preserving the values across posts-back, and crucially, allowing the user to add and remove rows (a feature which is often a stumbling block for developers, as the number of questions on this topic on ASP.NET forums is testament to).
I have put together a hypothetical user administration page as an example, for a hypothetical web-based application used by a software company. The page is intended to display a list of users to system administrators, providing them with some bogstandard CRUD functionality, and allowing them to edit the role of each user, which is used to determine what parts of this imaginary system each user has access to.
The ListView markup might look something like this:
1: <asp:ListView ID="lvDynamicTextboxes" runat="server"
2: ItemPlaceholderID="itemPlaceholder"
3: onitemdatabound="lvDynamicTextboxes_ItemDataBound"
4: onitemcommand="lvDynamicTextboxes_ItemCommand">
5: <LayoutTemplate>
6: <table style="width: 50%" class="permissions">
7: <asp:PlaceHolder ID="itemPlaceholder" runat="server"></asp:PlaceHolder>
8: </table>
9: </LayoutTemplate>
10: <ItemTemplate>
11: <tr class="top">
12: <td>
13: Name:
14: </td>
15: <td colspan="2">
16: <asp:TextBox ID="txtName" runat="server" Width="90%">
17: </asp:TextBox>
18: </td>
19: <td>
20: <asp:LinkButton ID="lnkDelete" runat="server">Delete</asp:LinkButton>
21: </td>
22: </tr>
23: <tr class="bottom">
24: <td>
25: Role:
26: </td>
27: <td>
28: <asp:DropDownList ID="cboRole" runat="server">
29: <asp:ListItem Value="0">--select--</asp:ListItem>
30: <asp:ListItem Value="1">Developer</asp:ListItem>
31: <asp:ListItem Value="2">Team Leader</asp:ListItem>
32: <asp:ListItem Value="3">Manager</asp:ListItem>
33: </asp:DropDownList>
34: </td>
35: <td>
36: System Administrator:
37: </td>
38: <td>
39: <asp:CheckBox ID="chkSysAdmin" runat="server" />
40: </td>
41: </tr>
42: </ItemTemplate>
43: </asp:ListView>
And the output of the above markup should be a table which looks like this:

As you can see, each set of controls is split over 2 separate rows in the resulting HTML table, which instantly excludes GridView-based solutions. Each item in the grid consists of a textbox to hold the user’s name, a dropdown list to edit their role, a checkbox which specifies whether or not the user is a system administrator, and a LinkButton to delete a user.
In order to persist the values, I used a DataTable, though in a real-world application, I would probably use custom business objects for all but single-tier applications; although I do often use DataSets and DataTables in my data layer classes, I tend not to pass these beyond the data layer, as I don’t think a relational view of the data is much use to a UI in general. In order to hold the DataTable values in memory between posts-back, I used session state, rather than ViewState as in the previous article, because of the increased data size.
The functionality in the code-behind is very similar to the code in the previous article, so I won’t go over it in any great detail here, but will note a couple of important differences. As the sample application starts each time with a new dataset, and doesn’t persist its data between runs, I needed some code to generate a new DataTable to hold the values in the ListView controls which needed to be persisted. Creating a new DataTable programmatically is very straightforward, as you can see:
1: private DataTable CreateDataTable()
2: {
3: DataTable dt = new DataTable();
4: DataRow row;
5:
6: dt.Columns.Add("rowID", typeof(int));
7: dt.Columns[0].AutoIncrement = true;
8: dt.Columns[0].AutoIncrementSeed = dt.Columns[0].AutoIncrementStep = 1;
9:
10: dt.Columns.Add("staffMemberName", typeof(string));
11: dt.Columns.Add("roleName", typeof(string));
12: dt.Columns.Add("isSysAdmin", typeof(bool));
13:
14: //create a dummy row
15: row = dt.NewRow();
16: row[1] = row[2] = string.Empty;
17: row[3] = false;
18: dt.Rows.Add(row);
19:
20: return dt;
21: }
This method generates a new DataTable, with 4 columns: 3 of which will be used to persist the control values, and an autonumber ID field to act as a primary key, and behaves in the same way as a SQL Server Identity field, which means that the ID is automatically generated each time a new row is added to the DataTable. I also added an empty data row to the DataTable, so that when the ListView is initially bound, it appears with an extra row, ready for the user to input data into.
There are also a number of changes in the code which populates the control values when the ListView is data bound. As in the previous article, the key event to hook up for this is the ItemDataBound event. This event is raised as each item in the data source is bound to the ListView: it enables you to access the properties of the data source item, and the controls to which the values are being bound. You can, if you prefer, specify the values directly in your ASP.NET markup, however I prefer to keep the code and markup as separate as possible, and so I do all the binding of the controls in the code-behind. The event handler therefore looks like this:
1: protected void lvDynamicTextboxes_ItemDataBound(object sender, ListViewItemEventArgs e)
2: {
3:
4: if (e.Item is ListViewDataItem)
5: {
6: object fieldValue;
7:
8: ListViewDataItem item = (ListViewDataItem)e.Item;
9: int rowID = (int)DataBinder.Eval(item.DataItem, "rowID");
10:
11: //set the command argument of the delete row linkbutton
12: LinkButton lbt = (LinkButton)e.Item.FindControl("lnkDelete");
13: lbt.CommandArgument = rowID.ToString();
14: lbt.CommandName = "DELETEITEM";
15:
16: fieldValue = DataBinder.Eval(item.DataItem, "staffMemberName");
17: if (fieldValue != DBNull.Value && fieldValue.ToString().Length > 0)
18: {
19: TextBox txt = (TextBox)e.Item.FindControl("txtName");
20: txt.Text = fieldValue.ToString();
21: }
22:
23: fieldValue = DataBinder.Eval(item.DataItem, "roleName");
24: if (fieldValue != DBNull.Value && fieldValue.ToString().Length > 0)
25: {
26: DropDownList cbo = (DropDownList)e.Item.FindControl("cboRole");
27: cbo.Items.FindByText(fieldValue.ToString()).Selected = true;
28: }
29:
30: fieldValue = DataBinder.Eval(item.DataItem, "isSysAdmin");
31: if (fieldValue != DBNull.Value)
32: {
33: CheckBox chk = (CheckBox)e.Item.FindControl("chkSysAdmin");
34: chk.Checked = (bool)fieldValue;
35: }
36: }
37: }
The code above first checks to ensure the current ListView item being bound is a data item (as opposed to a header/footer item). It then uses DataBinder.Eval to return the value of each of the fields in the relevant DataTable row, and binds those values to the UI controls. One other important piece of functionality which occurs here is the setting of the command argument of the Delete User LinkButton. The command argument is set to the primary key of the row in the DataTable, and this is what enables the correct row to be deleted when the LinkButton is clicked.
As you can see in the ListView markup above, I have also subscribed to the ItemCommand event. This event is raised whenever a button is clicked within the ListView. The ListView control helpfully bubbles any click events of its child controls up, by raising its own ItemCommand event. The CommandArgument value (as set in the ItemDataBound event handler) is available at this point via the ListViewCommandEventArgs instance, and we can use this to retrieve the unique ID of the data row like this:
1: protected void lvDynamicTextboxes_ItemCommand(object sender, ListViewCommandEventArgs e)
2: {
3: if (e.CommandName.Equals("DELETEITEM"))
4: {
5: int rowID = int.Parse(e.CommandArgument.ToString());
6: this.DeleteRow(rowID);
7: }
8: }
All that is then left is to remove the specified row from the DataTable, and rebind the ListView control.
Finally, you might have noticed a couple of CSS classes applied to the HTML table inside the ListView markup above. This was used to delimit each of the pair of HTML table rows, to mark each one as a separate item, by setting the appropriate cell borders. The CSS to do this looks like this:
1: <style type="text/css">
2: table.permissions
3: {
4: border: solid 1px black;
5: border-collapse:collapse;
6: }
7:
8: table.permissions tr.top td
9: {
10: border-top: none;
11: }
12:
13: table tr.bottom td
14: {
15: border-bottom: solid 1px black;
16: }
17: </style>
The issue of how to code with dynamic controls is still one of the most popular questions on ASP.NET forums, and it’s unfortunate that many of the responses to these questions simply point the poster at one or more articles on dynamic control usage, rather than questioning why the poster feels they need to use them in the first place. I find in most cases, this is simply because they are not aware of the flexibility offered by the built-in ASP.NET controls, and not aware of the potential time-savers they can be. None of the code for the three articles in this series is at all complex. Let’s face it, when writing an application, there isn’t time to waste writing reams of logic just to populate an HTML table with contents, and a bit of time spent up-front investigating good databinding techniques can save you a lot of time further down the line.
The full source code can be downloaded using the link at the top of the article.
Wow... that's a lot of capitals and acronyms to squeeze into a single title (well, ok... one acronym, and two initialisms, if you want to split hairs). Anyhoo... after roundly dismissing the WPF WebBrowser control in an earlier article, I now find it's (ahem) not quite so bad after all.
I needed to create a WYSIWYG HTML editor in WPF for a project I'm currently working on. There are quite a few commercial WYSIWYG editors based on the Windows Forms WebBrowser control, and also quite a few examples of free ones on CodeProject etc, but I didn't spot any out there using the WPF WebBrowser control at the time.
We didn't really want another interoperability layer between our WPF app, and the ActiveX control which forms the backbone of the WPF WebBrowser, so we wanted to avoid the Windows Forms solutions- although it is quite possible to embed one of these onto a WPF Window using a WindowsFormsHost. In the end, we decided to roll our own, though it should be pointed out that it owes a debt of gratitude to the existing windows forms implementations such as the one linked to above.
The main issue to overcome when developing the control is the necessity of working with the Microsoft HTML Object Library COM component (MSHTML for short). If you want to execute any functionality against the document exposed by the WebBrowser control, you will have to start getting familiar with this component (for anyone who has not used it before- in Visual Studio, click add reference, navigate to the COM tab, scroll down till you see Microsoft HTML Object Library. Once added, you need to import the mshtml namespace in your class).
The document exposed by the WebBrowser control is initially null, until you browse to a valid URI, or load some content into the control using the NaviagteToString or NavigateToStream methods. Once loaded, the document is of type HTMLDocumentClass, which you will find in the mshtml namespace- once you have casted the document to this type, you can access its properties and methods.
It turns out there is a wealth of useful functionality available here which isn't exposed via the WebBrowser control itself- functionality for formatting html in the WebBrowser, and (crucially) for accessing the designMode property of the document, which effectively changes it from a read-only view of the HTML to an editable one which accepts user input (which is 90% of the work done already, in a single line of code!). Of course, the Windows Forms WebBrowser control exposed this property itself, and I'm sure that the WPF one will also do so in its next release, but for the time being, the only way to get at it is via the native HTMLDocument.
I also made the decision during development of the control to avoid any explicit non-.NET dependencies. In particular, this meant that I couldn't add a direct reference to the Microsoft HTML Object Library. Why did I do this? Well, for one thing, we work in a continuous integration environment, but our continuous integration build box has an older version of the MSHTML library, so every time I checked in the code with an MSHTML reference, it was breaking the build.
I solved this by late binding to the MSHTML library, however not the way you might think...
As a c# developer, my first instinct was to rewrite the code without the MSHTML reference using reflection to perform the late binding. However, I quickly found it was turning out to be a pretty ugly coding experience, with quite a lot of nested GetMethod and InvokeMember calls. I then remembered that VB.NET is able to do late binding at the language level: if you switch Option Strict off you essentially turn VB.NET almost into a scripting language, which is exactly what I wanted here. So I created a VB.NET wrapper for the HTMLDocument which exposes the functionality I need, which works very well indeed, and now consists of code several orders of magnitude more readable than the C# equivalents (although it will be possible to achieve the same thing in c# 4.0 when it comes out, using the new dynamic features).
All well and good so far... I now had the HTML document doing what I wanted, and I even nicked a nifty colour picker from the good folks at Microsoft, in order to enable font colour and highlight colour selection.
I exposed a few methods for loading HTML content: either from a specific URI, or from text content, or a stream. Then added a routed event which reports when the current HTML is edited.
The next task was to replicate a few pieces of functionality people generally expect to be available in HTML editors, but which isn’t provided via the MSHTML object model. The first is the ability to create ordered and unordered lists, the second is the inserting of hyperlinks, and the third is the ability to align the selected text. Each of these tasks, I accomplished with the help of the indispensible HTML Agility Pack HTML Parser. I used this to parse the HTML and manipulate the DOM of the loaded document directly.
Then, just when I thought I had it all cracked, I integrated it into the app, and found that the pesky ActiveX control was not quite behaving as expected. Every time the user edited the HTML document, then attempted to load a new one, a dialogue box was being generated prompting the user that the current content had changed, and asking them if they wanted to save this updated HTML content to disk, or discard the changes.
Given that our end users have most likely never even heard of HTML, let alone have any wish to save some to their disks, this was not good. Try as I might, I simply couldn't find any good way to suppress these dialogue boxes. Whatever properties I set on the document, or on the control, up it popped each time, like that hugely irritating paperclip in older versions of office.
Eventually, I found an article which had gone some way towards solving this exact problem, which provides a suitably tortuous hack which works by hooking the WndProc and listening out for the message which is raised when new windows are created. As the dialogue box is always opened with the same text in the title bar, each time a new window is created, its title bar text is examined, if it matches the dialogue window's text, the message is swallowed before the window is shown. Excellent!
However, one problem remained: because the window that is created by the control is an Yes/No/Cancel dialogue- if you just kill the window, it returns a dialogue result of 'Cancel' which results in nothing happening; remember the window is asking the user to confirm whether they want to A- save the updated html content to disk, B- discard the changes and continue, or C- cancel the loading of the new document. So, the final task was to find a way to programmatically click the 'No' button, so the modifications are discarded without the user ever seeing the window. This, I accomplished by way of the EnumChildWindows windows hook to recursively search through the elements on the dialogue window, until it finds the 'No' button, then the SendMessage win32 function to simulate a button click. This, finally, cured the problem of the recurring popup.
All in all, quite a bit of work to re-implement a lot of functionality which came for free on the Windows Forms HTML editor, but a good learning exercise along the way. The source code can be downloaded from the link at the top of the article.
Update (9/4/2009): I have uploaded version 1.1 of the editor, which fixes a couple of bugs around the alignment and formatting.
Update (20/06/2009): I have uploaded version 1.2 today with a couple more stability fixes.
This is another instalment of avoiding the use of dynamic controls in ASP.NET. The new ASP.NET ListView control is really rather neat; using this control obviates many of the problems which previously led developers to develop dynamic control-based pages, and can replace reams of nasty control creation loops with a simple and intuitive markup-based alternative.
One problem which is frequently addressed by developers is the display of hierarchical data. A simple solution to this is to use a treeview control. Whilst the built-in ASP.NET treeview control works well for a simple parent-child node structure- it doesn't support more complex scenarios, primarily because because it offers only limited control over the contents of each node.
What I wanted to do was display a drilldown matrix report, with hierarchical categories on the y-axis, and a pre-defined set of categories along the x-axis. I found that this can be a fairly painless task using a ListView.
To illustrate why this might be useful, take the example of a hypothetical multinational software development company; for the sake of argument, let's call this entirely fictional company Macrosoft.
Macrosoft has offices in several countries, each of which is working on a new product which the company initially aimed to release before the end of the financial year. Having roundly missed this deadline, they now aim to get it out in time for the Christmas sales.
Each location has its own team of made up of analysts, developers, designers, and testers, and each team is working on different features of the product.
Their operations director has asked for a report summarising how the product development is progressing at each of the locations. He wants to know if any work is currently outstanding in analysis, design, development, and testing, at each of these locations, and be able to drill down by region.
He also wants the report to be kept as simple as possible: a red mark to indicate outstanding work, a green one to indicate all is complete. Finally, he wants this report to be accessible via a web page. This will enable him to get an instant overview of the overall state of the product, and consequently hassle the regional managers whose departments are lagging behind.
Now, given the above (admittedly somewhat unlikely) scenario, my first though would normally be to do it in SQL Server Reporting Services. This would meet all the above requirements without having to worry about creating the report layout from scratch. But, let's say it has to be done in ASP.NET...
We want the final output to look something like this when fully expanded:
and each of the nodes on the left should initially appear as collapsed.
My implementation made use of a typed dataset, with a recursive relationship, put together as follows:
As you can see in the dataset designer above, each location has an ID and a name. A parent-child relationship has been created between locationID and parentID, which allows all the regions, towns, and office locations to be held within the same table. The table contains 4 boolean flags: anaysisComplete, designComplete, developmentComplete, and testingComplete: these indicate the state of the work at each location.
I also added a couple of fields which are used by the UI to store visual state: isExpanded, and indentationLevel. In a real-world application, I wouldn't mix data and display information in this way- these fields would be declared within, and managed by, the presentation layer. But to keep the example simple, I opted to store them along with the report data.
The ASP.NET markup contains a ListView which looks like this:
1: <asp:ListView ID="lvOffices" runat="server"
2: onitemdatabound="lvOffices_ItemDataBound">
3: <LayoutTemplate>
4: <table style="width:75%">
5: <tr>
6: <td></td>
7: <td style="width:15%">Analysis</td>
8: <td style="width:15%">Design</td>
9: <td style="width:15%">Development</td>
10: <td style="width:15%">Testing</td>
11: </tr>
12: <asp:PlaceHolder ID="itemPlaceholder" runat="server"></asp:PlaceHolder>
13: </table>
14: </LayoutTemplate>
15: <ItemTemplate>
16: <tr>
17: <td>
18: <asp:Literal ID="litIndent" runat="server"></asp:Literal>
19: <asp:LinkButton ID="lnkExpandCollapse" runat="server" CausesValidation="false" OnClick="expandCollapse_Click">+</asp:LinkButton>
20: <asp:Label ID="lblLocation" runat="server"></asp:Label>
21: </td>
22: <td><asp:Image ID="imgAnalysisState" runat="server" Visible="false" Height="20px" Width="20px" /></td>
23: <td><asp:Image ID="imgDesignState" runat="server" Visible="false" Height="20px" Width="20px" /></td>
24: <td><asp:Image ID="imgDevelopmentState" runat="server" Visible="false" Height="20px" Width="20px" /></td>
25: <td><asp:Image ID="imgTestingState" runat="server" Visible="false" Height="20px" Width="20px" /></td>
26: </tr>
27: </ItemTemplate>
28: </asp:ListView>
The 4 state images are set dynamically in the ItemDataBound event handler, based on the value of the appropriate boolean flag, and a linkbutton is included to trigger the drilldown.
The rest of the application works like this:
- The data is retrieved from the data store (in this case, a dataset instance populated with test data).
- The top level rows (i.e the rows where parentID is null) are extracted, and bound to the ListView
- Each time the user clicks the drilldown button, the isExpanded state is set for the appropriate row and the ListView control is re-bound
Each time the ListView control is re-bound, the appropriate data rows are extracted from the datatable, first by pulling out those where the parentID is null, then by recursively searching those for child rows where the isExpanded flag is set to true:
1: private void BindData(LocationData dataSet)
2: {
3: List<LocationData.LocationRow> dataSourceRows = new List<LocationData.LocationRow>();
4:
5: //gets a collection of top-level rows
6: IEnumerable<LocationData.LocationRow> topLevelRows = from LocationData.LocationRow locationRow
7: in dataSet.Location.Rows
8: where locationRow.IsparentIDNull()
9: select locationRow;
10:
11:
12: List<LocationData.LocationRow> rowsToBind = new List<LocationData.LocationRow>();
13:
14: //recursively builds a collection consisting of all the top level rows
15: //and any children where isExpanded = true in the data row
16: this.BuildDataSetRecursive(topLevelRows, ref rowsToBind, 0);
17:
18: //binds the locations listview
19: this.lvOffices.DataSource = rowsToBind;
20: this.lvOffices.DataBind();
21: }
22:
23: private void BuildDataSetRecursive(
24: IEnumerable<LocationData.LocationRow> inputRows,
25: ref List<LocationData.LocationRow> outputRows, int indentationLevel)
26: {
27: foreach (LocationData.LocationRow inputRow in inputRows)
28: {
29: inputRow.indentationLevel = indentationLevel;
30:
31: //add the current row to the output collection
32: outputRows.Add(inputRow);
33:
34: //if the row expanded state is set to true, add any children to the output collection.
35: if (inputRow.isExpanded)
36: {
37: //using the data relation on the typed dataset to find the children of the current row.
38: DataRow[] childRows = inputRow.GetChildRows("FK_Location_Location");
39:
40: if (childRows.Length > 0)
41: {
42: List<LocationData.LocationRow> locationChildRows = new List<LocationData.LocationRow>(
43: childRows.Cast<LocationData.LocationRow>());
44: this.BuildDataSetRecursive(locationChildRows, ref outputRows, indentationLevel + 1);
45: }
46: }
47: }
48: }
And the methods which toggle the isExpanded flag when the user clicks on the drilldown button complete the picture:
1: protected void expandCollapse_Click(object sender, EventArgs e)
2: {
3: LinkButton lbt = (LinkButton)sender;
4: int locationID = int.Parse(lbt.CommandArgument);
5:
6: LocationData ds = (LocationData)Cache["data"];
7: LocationData.LocationRow locationRow = ds.Location.FindBylocationID(locationID);
8:
9: //set the isExpanded state for the row
10: this.SetExpandState(locationRow, !locationRow.isExpanded);
11:
12: //persist the changes
13: ds.AcceptChanges();
14:
15: //update the label text
16: lbt.Text = (locationRow.isExpanded) ? "-" : "+";
17:
18: //rebind the listview
19: this.BindData(ds);
20: }
21:
22: private void SetExpandState(LocationData.LocationRow row, bool state)
23: {
24: row.isExpanded = state;
25: if (!state)
26: {
27: DataRow[] childRows = row.GetChildRows("FK_Location_Location");
28: foreach (DataRow childRow in childRows)
29: {
30: this.SetExpandState((LocationData.LocationRow)childRow, state);
31: }
32: }
33: }
So, in retrospect, I suppose this turned out to be not so much an example of the functionality of the ListView control (as you can see from the ListView markup above- there's not really much to it) as about traversing recursive relationships in datatables. But a potentially useful bit of code nonetheless...
The full source code for this article can be downloaded using the link at the top.
Here's a really quick tip: how to convert a DataTable to CSV, and write it dynamically to the response stream.
In ASP.NET, If you need to allow users to download the contents of a datatable in flat file format (i.e. CSV, TAB etc) you could do this by writing the data to a temporary file, then writing the resulting file to the response using TransmitFile. However, a quicker and less expensive method is to stream it directly. Here's a method which allows you to do just that:
/// <summary>
/// Writes a datatable in delimited file format to the response stream.
/// </summary>
/// <param name="dt"></param>
/// <param name="fileName"></param>
/// <param name="delimiter"></param>
private void WriteDelimitedData(DataTable dt, string fileName, string delimiter)
{
//prepare the output stream
Response.Clear();
Response.ContentType = "text/csv";
Response.AppendHeader("Content-Disposition",
string.Format("attachment; filename={0}", fileName));
//write the csv column headers
for (int i = 0; i < dt.Columns.Count; i++)
{
Response.Write(dt.Columns[i].ColumnName);
Response.Write((i < dt.Columns.Count - 1) ? delimiter : Environment.NewLine);
}
//write the data
foreach (DataRow row in dt.Rows)
{
for (int i = 0; i < dt.Columns.Count; i++)
{
Response.Write(row[i].ToString());
Response.Write((i < dt.Columns.Count - 1) ? delimiter : Environment.NewLine);
}
}
Response.End();
}
And here's an example of calling the above method with some test data:
//create a datatable to hold the test data
DataTable dt = new DataTable();
dt.Columns.Add("Column 1", typeof(string));
dt.Columns.Add("Column 2", typeof(string));
//generate some random data in the datatable
Random rnd = new Random();
for (int i = 0; i < 100; i++)
{
dt.Rows.Add(rnd.Next(1, 1000000).ToString(), rnd.Next(1, 1000000).ToString());
}
this.WriteDelimitedData(dt, "testdata.csv", ",");
Quick, and easy
