.NET
Creating dynamic menus in EPiServer
My first post in almost a year! What is going on! "Been busy, things are happening, blah blah blah"…no excuse, really. I signed up for twitter almost a year ago, and I think I lost some focus in the blogging sphere. Anyway, this first post in 2010 is about EPiServer, the best CMS out there today!
The challenge
I want to create a more dynamic page tree than the standard EPiServer PageTree. More like an Accordion panel which has 2 or 3 levels (doesn't matter) and all available nodes are expanded by default. The real challenge is to get the whole page tree with all nodes expanded (not only the nodes which are selected) and setting correct class on the expanded items. Whether this is an accordion or another cool menu script is all the same.
The Solution
Start out with the default PageTree control in EPiServer controls.
<EPiServer:PageTree ID="PageTreeLeftMenu" runat="server" ExpandAll="true" NumberOfLevels="2">
<HeaderTemplate>
<div class="leftmenu"> <!– Header –>
</HeaderTemplate>
<IndentTemplate>
<ul> <!– Indent –>
</IndentTemplate>
<ItemHeaderTemplate>
<li <%# GetExpandedClass(Container.DataItem) %>>
</ItemHeaderTemplate>
<ExpandedTopTemplate>
<EPiServer:Property PropertyName="PageLink" runat="server" /> <!– ExpandedTop –>
</ExpandedTopTemplate>
<SelectedExpandedTopTemplate>
<EPiServer:Property PropertyName="PageLink" runat="server" CssClass="selected" /> <!– SelectedExpandedTop –>
</SelectedExpandedTopTemplate>
<ExpandedItemTemplate>
<EPiServer:Property PropertyName="PageLink" runat="server" /> <!– ExpandedItem –>
</ExpandedItemTemplate>
<SelectedExpandedItemTemplate>
<EPiServer:Property PropertyName="PageLink" runat="server" CssClass="selected" /> <!– SelectedExpandedItem –>
</SelectedExpandedItemTemplate>
<ItemFooterTemplate>
</li>
</ItemFooterTemplate>
<UnindentTemplate>
</ul> <!– UnIndent –>
</UnindentTemplate>
<FooterTemplate>
</div> <!– Footer –>
</FooterTemplate>
</EPiServer:PageTree>
Things to notice:
- "ExpandAll" is set to true. This will dump the whole tree, not just the nodes which are selected by the user.
- Only the *Expanded* templates are in use for Top and Item (ie.ExpandedTopTemplate). When using "ExpandAll" these are the templates used, not the regular Top Template and ItemTemplate.
- <%# GetExpandedClass(Container.DataItem) %> – This is the magic for setting the correct style on the expanded items. In addition there is a "selected" class on the selected menu item
The Codebehind looks like this:
protected void Page_Load(object sender, EventArgs e)
{
PageTreeLeftMenu.PageLink = LeftMenuPageLink;
PageTreeLeftMenu.DataBind();
}
protected PageReference LeftMenuPageLink
{
get
{
PageReference leftMenuPageLink = CurrentPage["LeftMenuRoot"] as PageReference;
return leftMenuPageLink;
}
}
protected string GetExpandedClass(object dataItem)
{
PageData page = dataItem as PageData;
if (page != null && PageTreeLeftMenu.OpenPages.Contains(page.PageLink))
return "class=\"expanded\"";
return string.Empty;
}
Things to notice:
- LeftmenuPageLink is just an example on how to set the start point for the left menu.
- GetExpandedClass is the method that sets the correct class on the <li>. This is done by checking if the page in the tree is available in PageTree.OpenPages. This is what makes it possible to make the dynamic menu. If the OpenPages method was not available, it would not be possible to highlight the expanded nodes without traversing each node or doing some javascripting.
The output should look something like this
43 <div class="leftcolumn">
44 <div class="leftmenu">
45 <!– Header –>
46 <ul>
47 <!– Indent –>
48 <li class="expanded"><a href="/en/Consulting/">Consulting</a>
49 <!– ExpandedTop –>
50 <ul>
51 <!– Indent –>
52 <li class="expanded"><a class="selected" href="/en/Consulting/Business-Consulting-/">
53 Business Consulting </a>
54 <!– SelectedExpandedItem –>
55 </li>
56 <li><a href="/en/Consulting/Project-and-Test-Management/">Project and Test
57 Management</a>
58 <!– ExpandedItem –>
59 </li>
60 <li><a href="/en/Consulting/IT-Consulting/">IT Consulting</a>
61 <!– ExpandedItem –>
62 </li>
63 <li><a href="/en/Consulting/Application-Development/">Application Development</a>
64 <!– ExpandedItem –>
65 </li>
66 </ul>
67 <!– UnIndent –>
68 </li>
69 <li><a href="/en/Solutions/">Solutions</a>
70 <!– ExpandedTop –>
71 <ul>
72 <!– Indent –>
73 <li><a href="/en/Solutions/ERP/">ERP</a>
74 <!– ExpandedItem –>
75 </li>
76 <li><a href="/en/Solutions/Content-Management/">Content Management</a>
77 <!– ExpandedItem –>
78 </li>
79 <li><a href="/en/Solutions/Oil-and-Gas/">Oil and Gas</a>
80 <!– ExpandedItem –>
81 </li>
82 <li><a href="/en/Solutions/Industry/">Industry</a>
83 <!– ExpandedItem –>
84 </li>
85 <li><a href="/en/Solutions/E-Business/">E-Business</a>
86 <!– ExpandedItem –>
87 </li>
88 <li><a href="/en/Solutions/Banking–Finance-Suite/">Banking & Finance
89 Suite</a>
90 <!– ExpandedItem –>
91 </li>
92 <li><a href="/en/Solutions/Public-Sector-Suite/">Public Sector Suite</a>
93 <!– ExpandedItem –>
94 </li>
95 </ul>
96 <!– UnIndent –>
97 </li>
98 <li><a href="/en/Outsourcing/">Outsourcing</a>
99 <!– ExpandedTop –>
100 <ul>
101 <!– Indent –>
102 <li><a href="/en/Outsourcing/ITO/">ITO</a>
103 <!– ExpandedItem –>
104 </li>
105 <li><a href="/en/Outsourcing/Applications-Management/">Applications Management</a>
106 <!– ExpandedItem –>
107 </li>
108 </ul>
109 <!– UnIndent –>
110 </li>
111 <li><a href="/en/TestTest/">TestTest</a>
112 <!– ExpandedTop –>
113 </li>
114 <li><a href="/en/TestTestTest/">TestTestTest</a>
115 <!– ExpandedTop –>
116 </li>
117 </ul>
118 <!– UnIndent –>
119 </div>
120 <!– Footer –>
121 </div>
The result will look something like this:
After adding the Accordion script, the Top items should collapse and only the expanded section should be visible. When moving the mouse over another section it should expand, but not highlight. The final result will look something like this.

Conclusion
Using dynamic menus could give the visitor a better experience. Whether its an Accordion panel or some other fancy scripted menu doesn't matter. Anyway you will have to dump the whole tree to a certain level (2 levels in this example) and set the correct classes on the elements to get the styling correct. The magic is the "PageTree.OpenPages" method which helps a lot in this scenario.
Use regular expressions in Visual Studio to clean up code
This is a reminder to myself.
Knut, remember that Regular Expressions are very handy to clean up a lot of messy code.
Here is an example:
The following function call results in a code analysis warning:
DBHelper.SetPropertyFromDB(m_City, dr("CITY"))
It's a helper function that returns the typed value from a DBValue. Not really useful these days with NHibernate or Typed Datasets, but it's code from an old application. Here is the signature of the code:
Shared Sub SetPropertyFromDB(ByRef pProperty As Object, ByVal value As Object)
The warning reported is:
"Warning: Implicit conversion from 'Object' to 'String' in copying the value of 'ByRef' parameter 'pProperty' back to the matching argument."
To fix this, I created a new method that returns the correct typed value instead of returning the referenced parameter and marked the old method "Obsolete":
Public Shared Function GetDBValue(ByVal pProperty As Object, ByVal dbValue As Object) As Object
Now, I had to rewrite all the calls to the function (several hundred calls). This included returning the typed value into the same variable as the first parameter in the function call, like this:
m_City = DBHelper.GetDBValue(m_City, dr("CITY"))
This is easily achieved by using Regular Expressions in the "Find and Replace" dialog in Visual Studio:
Find What:
DBHelper.SetPropertyFromDB\({.*},
(note: the \ escapes the ( and the expression group is marked by a {})
Replace with:
\1 = DBHelper.GetDBValue(\1,
(note: the \1 will represent the expression group)
Pretty simple, and very powerful
Extend the GridView
The default GridView class has by default a lot of great features. However, some important features are missing. One of the most important is the ability to display the grid even if it is empty.
Here is a great class extending the GridView with some additional features.
Web application absolute path
Just a reminder on how to get the absolute path for the webapplication in ASP.NET
HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority) + VirtualPathUtility.ToAbsolute(HttpContext.Current.Request.ApplicationPath)
This will return http://subdomain.domain.com/applicationname
You can drop those AppSettings with ApplicationPath
Using log4net in Web Applications – a real-life example
I have seen many different configurations for Log4Net. Log4Net is a very simple, but flexible framework and there are lot of ways to configure it. Here is a real-life example on how we are using in the applications I am working on.
First off, I won't say that this is the one and only way you should use Log4Net, but it handles most of the scenarios I can think of. Some people think that one should not write wrappers for the loggers and use the logging framework directly in the class, but to be honest I like the wrapper because it makes it very simple to use the logging framework without too much knowledge on how it works. I would say that 95% of what is being logged, is typically errors and debug/useful information. I can't see any problem using a wrapper as I get the information I need from the loggers.
Goals with this log configuration
- Most important is of course logging errors. All errors should be logged.
- Errors should be logged in a global file on the server that will role once a day.
- Errors should be sent by email.
- It should be possible to notify important information by email
- It should be easy for the developers to use the logging framework
- It should be possible to change log-level without restarting the application
Configuration and setup
Log4Net consists of only one DLL. Get the latest version and put it in your bin folder. Make a reference to it in the Web Application Project.
Log4net.config
Create a new config file in the root of your Web Application. Name it Log4Net.config and paste the following code:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />
</configSections>
<log4net>
<appender name="RollingLogFileAppender" type="log4net.Appender.RollingFileAppender">
<threshold value="INFO" />
<file value="Log\[applicationname].log" />
<appendToFile value="true" />
<rollingStyle value="Date" />
<datePattern value="'.'yyyyMMdd'.log'" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date [%thread] %-5level %logger %X{user} %X{url} - %message%newline" />
</layout>
</appender>
<appender name="SmtpAppenderError" type="log4net.Appender.SmtpAppender">
<!--Set threshold for this appender-->
<threshold value="ERROR" />
<to value="[someone]@[somewhere.com]" />
<from value="[someone]@[somewhere.com]" />
<subject value="Error from [applicationname]" />
<smtpHost value="[100.100.100.100]" />
<bufferSize value="1" />
<lossy value="false" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date %-5level %logger %X{user} %X{url} - %message%newline" />
</layout>
</appender>
<appender name="SmtpAppenderNotify" type="log4net.Appender.SmtpAppender">
<!--Set threshold for this appender-->
<threshold value="INFO" />
<to value="[someone]@[somewhere.com]" />
<from value="[someone]@[somewhere.com]" />
<subject value="Error from [applicationname]" />
<smtpHost value="[100.100.100.100]" />
<bufferSize value="1" />
<lossy value="false" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date - %message%newline" />
</layout>
</appender>
<root>
<level value="Error" />
</root>
<logger name="Application">
<!--Set level for this logger-->
<level value="INFO" />
<appender-ref ref="RollingLogFileAppender" />
<appender-ref ref="SmtpAppenderError" />
</logger>
<logger name="Notify">
<!--Set level for this logger-->
<level value="INFO" />
<appender-ref ref="RollingLogFileAppender" />
<appender-ref ref="SmtpAppenderNotify" />
</logger>
</log4net>
</configuration>
Some comments on the Log4net.config file:
- The RollingFileAppender is great for rolling the logfile once a day.
- It has a threshold of INFO, meaning it will log anything from the application except debug information
- The logfiles are created in a subdirectory of web root. This should be changed on the production server to not expose information about the application.
- The datePattern is specifying how the rolling files are named. The current day will be named "applicationname.log" and yesterdays file will be named "applicationname.log.20081211.log". The rolling file extension (.log) makes it easy to open the file in the same editor as the current log. All files will be sorted correctly when using the ISO style date in the filename.
- In the conversionPattern you will see two speacial entries with the following format: %X{name}. These are custom formats which can be used for special purposes. This configuration adds the authenticated username and the url which fails if there is an error. This is useful information.
- The SmtpAppenders are used for sending emails. It's really not my favourite configuration, spamming the developers or some other people with emails when some errors occur, but some like it.
- The threshold is different for the two SmtpAppenders, as is the subject of the email.
- The buffersize is set to 1 in the example, meaning that the email is sent right away when an error occur. Increasing the buffer will hold the email until the buffer is reached. This can be useful on the production server, especially for the Notify appender as it is probably not that important to notify right away.
- Lossy is set to false, also to send the email right away. It is possible to hold the email until an evaluator is triggered, for instance when a error occurs. Setting lossy to true requires an evaluator.
- It is possible to add filters to the appenders to determine which emails should be sent and which to drop.
- There are two specific loggers that the application will use. The root logger can be used to log 3.party libraries also using Log4Net. I like to use specific loggers in the application and enabling the root logger if I am tracing an error where I need more information from 3.party libraries, ie. NHibernate or other frameworks using Log4net. Remember to set the level for the logger. If not set, the default level is WARN, which means INFO messages will not be logged.
To be able to use the Log4Net.config file instead of putting everything into web.config, you will need to add the following line to AssemblyInfo.vb:
<Assembly: log4net.Config.XmlConfigurator(ConfigFile:="Log4Net.config", Watch:=True)>
The advantage of separating the Log4Net configuration is that it is possible to change the configuration of the logging without restarting the application by changing web.config. It also gives an better overview of the configuration, not having to browse through web.config to find the logging configuration.
Logging unhandled errors
All unhandled errors should be logged, which is pretty easy to achieve in web applications. Here is the Global.asax file:
Imports System.Web
Imports log4net
Imports MainLib
Public Class [Global]
Inherits System.Web.HttpApplication
Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
' Fires when the application is started
' Initialize the logger in this context.
LogManager.GetLogger(Me.GetType)
LogHandler.LogInfo("============================", LogHandler.LogType.General)
LogHandler.LogInfo(" Starting application", LogHandler.LogType.General)
LogHandler.LogInfo("============================", LogHandler.LogType.General)
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
' Fires when the session is started
End Sub
Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
' Fires at the beginning of each request
End Sub
Sub Application_AuthenticateRequest(ByVal sender As Object, ByVal e As EventArgs)
' Fires upon attempting to authenticate the use
End Sub
Sub Application_Error(ByVal sender As Object, ByVal e As EventArgs)
' Fires when an error occurs
LogHandler.LogError("Unhandled exception occured!",
HttpContext.Current.User, HttpContext.Current.Request.Url,
HttpContext.Current.Server.GetLastError())
End Sub
Sub Session_End(ByVal sender As Object, ByVal e As EventArgs)
' Fires when the session ends
End Sub
Sub Application_End(ByVal sender As Object, ByVal e As EventArgs)
' Fires when the application ends
End Sub
End Class
Comments on Global.asax
- I have added some information logging in the Application_Start event. This will trigger every time the applications starts or recycles. If this occurs often you probably have some problems with resources on the server, ie. too little memory available for the application.
- Application_Error occurs every time an unhandled error occurs. Use this to log all errors not logged elsewhere.
- I have to initialize the logging framework in Application_Start by calling
LogManager.GetLogger(Me.GetType). I'm not sure whether this is a bug or by design. It is not possible to let the LogHandler do the initialization. LogHandler is a wrapper class located in another assembly, maybe that is a problem, without really knowing why. LogManager is a class in Log4Net.
The LogHandler class
Imports System.Security.Principal
Imports log4net
Public Class LogHandler
Const _defaultApplicationLogger As String = "Application"
Public Enum LogType
General
Notify
End Enum
Public Shared Sub LogError(ByVal message As String, ByVal [error] As Exception)
Dim logger As ILog = LogManager.GetLogger(_defaultApplicationLogger)
If Not [error].InnerException Is Nothing Then
[error] = [error].InnerException
End If
If logger.IsErrorEnabled Then
logger.Error(message, [error])
End If
End Sub
Public Shared Sub LogError(ByVal message As String, _
ByVal user As IPrincipal, ByVal url As Uri, _
ByVal [error] As Exception)
SetOptionalParametersOnLogger(user, url)
LogError(message, [error])
End Sub
Public Shared Sub LogInfo(ByVal message As String, ByVal type As LogType)
Dim logger As ILog = Nothing
If type = LogType.Notify Then
logger = LogManager.GetLogger(LogType.Notify.ToString)
Else
logger = LogManager.GetLogger(_defaultApplicationLogger)
End If
If logger.IsInfoEnabled Then
logger.Info(message)
End If
End Sub
Public Shared Sub LogWarning(ByVal message As String, ByVal [error] As Exception)
Dim logger As ILog = LogManager.GetLogger(_defaultApplicationLogger)
If Not [error].InnerException Is Nothing Then
[error] = [error].InnerException
End If
If logger.IsWarnEnabled Then
logger.Warn(message, [error])
End If
End Sub
Public Shared Sub LogWarning(ByVal message As String, _
ByVal user As IPrincipal, ByVal url As Uri, _
ByVal [error] As Exception)
SetOptionalParametersOnLogger(user, url)
LogWarning(message, [error])
End Sub
Private Shared Sub SetOptionalParametersOnLogger(ByVal user As IPrincipal, ByVal url As Uri)
'set user to log4net context, so we can use %X{user} in the appenders
If Not user Is Nothing AndAlso user.Identity.IsAuthenticated Then
MDC.[Set]("user", user.Identity.Name)
End If
'set url to log4net context, so we can use %X{url} in the appenders
MDC.[Set]("url", url.ToString())
End Sub
End Class
Comments on LogHandler
- It's a pretty simple class which is easy to use. I could have added more wrapper methods with overload
- The LogType enum defines whether the message should be notified (emailed) or not.
- If I forget to set the level on the logger in the Log4Net.config, the logger.IsInfoEnabled will return false.
- I am using the MDC class in the Log4Net framework to add the custom entries in the logged message for user and url (see Log4Net.config above)
Handling an error and notifying with success
Sometimes when you know what could go wrong and you want to display a nice error message to the user, you could handle the error and display an errormessage to the user.
Example:
Private Function CreateUser() As User
Dim userName As String = TextBoxUserName.Text
Dim password As String = TextBoxPassword.Text
Try
Dim user As New User(userName, password)
LogHandler.LogInfo("Yahoo! User with username " & userName & " created.", LogHandler.LogType.Notify)
Return user
Catch iunex As InvalidUserNameException
PageTools.DisplayError(Page, iunex)
LogHandler.LogWarning("Error creating user with username " & userName, iunex)
Catch ipex As InvalidPasswordException
PageTools.DisplayError(Page, ipex)
LogHandler.LogWarning("Error creating user with password " & password, ipex)
End Try Return Nothing
End Function
Comments on CreateUser:
- If successful, a notification is sent by email by using the LogInfo method and using LogHandler.Logtype.Notify
- The User class will throw InvalidUserNameException or InvalidPasswordException in the credentials are invalid. I wish to display the errors to the user instead of displaying a general error page. In real life I would probably use some validation before calling the User constructor, but this is only an example.
- In addition to displaying the error to the user, the error is logged as a warning.This means it will be logged, but no email will be sent to the mailbox as this is not an error in the application.
- All other exceptions raised when creating the user will be caught in Application_Error in Global.asax and logged there.
Loginformation
Ok, now I have all the code I need to do decent logging. So what do I expect to see in the logfiles?
When building the application in DEBUG mode I will see the following in the logfile:
2008-12-11 16:56:24,453 [14] INFO Application (null) (null) - ============================
2008-12-11 16:56:24,468 [14] INFO Application (null) (null) - Starting application
2008-12-11 16:56:24,468 [14] INFO Application (null) (null) - ============================
2008-12-11 16:56:25,937 [14] WARN Application 3DX5G3J\knuth http://localhost/fdb/default.aspx -
Error creating user with username Knut Hamang
DAL.InvalidUserNameException: Username is already in use! Please select another username.
at DAL.User..ctor(String username, String password) in C:\Knut\code\Repository\Internal_Systems\fdb\trunk\DAL\User.vb:line 97
at fdb.default.CreateUser() in C:\Knut\code\Repository\Internal_Systems\fdb\trunk\Web\default.aspx.vb:line 69
Building in RELEASE mode I get the following information:
2008-12-11 16:59:21,406 [14] INFO Application (null) (null) - ============================
2008-12-11 16:59:21,421 [14] INFO Application (null) (null) - Starting application
2008-12-11 16:59:21,421 [14] INFO Application (null) (null) - ============================
2008-12-11 16:59:22,812 [14] WARN Application 3DX5G3J\knuth http://localhost/fdb/default.aspx -
Error creating user with username Knut Hamang
DAL.InvalidUserNameException: Username is already in use! Please select another username.
at DAL.User..ctor(String username, String password)
at fdb.default.CreateUser()
The difference is the stacktrace. I get the stacktrace in both cases, but I also get the line numbers in DEBUG mode. I can still read the trace and locate the method in the file that throws the error. The conclusion is that I get enough information to trace and fix the error, if there is one.
Please leave any comments on the configuration, and feel free to discuss different logging strategies for Web Applications.
bug: VS 2008 – Web Application Project opened as Web Site
I just bumped into a weird Visual Studio (aka. Vicious Studio) bug. Not the first, but this is a pretty annoying one. I have taken an old .NET 1.1 Web application and converted it to .NET 3.5 using the VS Conversion Wizard. Everything seems ok, and I convert to Web Site to a Web Application Project. No problems so far. I fix some issues, close the solution, do some other stuff. Some days later I open the solution again. I then have about 200 errors and the Web Application Project is opened as a Web Site. Also the designer and resx files are not connected to the asp and codebehind files:
Ok, I have to start digging into the project and solution files. Comparing them to other solutions using Web Application project files, I can't seem to find anything suspicious. After some googling with no result, I decide to stare at the screen for a while and use WinMerge to determine what the difference might be. Suddenly I find one minor difference:
The solution file that works:
Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "Clock", "Clock.vbproj", "{3B3D0F02-D310-4BFB-83AD-F62758BB8624}"
The one that fails:
Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "Calendar", "Calendar\", "{7B411329-B1CB-457F-A954-898DX16B85A6}"
That's it. The Calendar project reference is missing the whole path to the project file. When i change it to:
Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "Calendar", "Calendar\Calendar.vbproj", "{7B411329-B1CB-457F-A954-898DX16B85A6}"
it opens as a Web Application Project, and now the resx and designer files are properly connected:
New version of the Silverlight Calendar
Some time ago, I wrote a birthday calendar in Silverlight 1.0. It was purely JavaScript and somewhat restricted because of that. Later I upgraded it to Silverlight 1.1, the first version of Silverlight that used managed code. Silverlight 1.1 is now Silverlight 2.0, but there has been a lot of changes between those version.
So here is the latest version compatible With Silverlight 2.0 Beta 2. I also added Tim Rule's excellent glowing and shadow effects, which are just awesome!
Download it here.
Html to Pdf in .NET
I searched around for some OpenSource projects converting HTML to PDF, and stumbled upon a great library called iTextSharp . It's actually a Java library ported to .NET (typically isn't it?), and it has some really nice features. There are some good commercial products out there doing the same, but in my opinion this is core functionality, so if one can get it for free and even get the source, nothing is better than that!
Regarding PDF creation, iTextSharps main function is generating PDF from scratch, not converting them from HTML. The library has a really understandable API for developers that are not familiar with the PDF specification, me beeing one of them.
Playing around with the API and reading some news lists and forum posts, I finally managed to get a working sample on how to export a GridView to PDF. This is a pretty simple sample, but you can play around with the API to add more formatting and do your stuff. Actually you can export anything in the XHTML, providing the markup is legal. However, not all html tags are supported. The intention of the author was not to make a HTML2PDF converter, but more like create PDF's from HTML if the markup supports the engine. So you will probably not be able to convert dynamic content you do not have control over, but it's excellent for creating reports etc. Supported tags are: "ol ul li a pre font span br p div body table td th tr i b u sub sup em strong s strike h1 h2 h3 h4 h5 h6 img"
So how does it work, then? Well, first you need to get the latest version of iTextSharp. Just place the dll to your bin folder in your Web Application Project (you're not using Web Projects, are you
) and add a reference to it. Then create a new page, add a PlaceHolder to it, The placeholder will be the section of the HTML that you will export to PDF. You can add some more controls to the placeholder if you need. Inside the placeholder add a GridView and bind it to your datasource.
Add a ASP:Button to the page. This will trigger the export. The code when the button is clicked is doing all the exporting stuff:
protected void ButtonCreatePdf_Click(object sender, EventArgs e)
{
//Set content type in response stream
Response.ContentType = "application/pdf";
Response.AddHeader("content-disposition", "attachment;filename=FileName.pdf");
Response.Cache.SetCacheability(HttpCacheability.NoCache);
//Render PlaceHolder to temporary stream
System.IO.StringWriter stringWrite = new StringWriter();
System.Web.UI.HtmlTextWriter htmlWrite = new HtmlTextWriter(stringWrite);
PlaceholderPdf.RenderControl(htmlWrite);
StringReader reader = new StringReader(stringWrite.ToString());
//Create PDF document
Document doc = new Document(PageSize.A4);
HTMLWorker parser = new HTMLWorker(doc);
PdfWriter.GetInstance(doc, Response.OutputStream);
doc.Open();
try
{
//Create a footer that will display page number
HeaderFooter footer = new HeaderFooter(new Phrase("This is page: "), true)
{ Border = Rectangle.NO_BORDER };
doc.Footer = footer;
//Parse Html
parser.Parse(reader);
}
catch (Exception ex)
{
//Display parser errors in PDF.
//Parser errors will also be wisible in Debug.Output window in VS
Paragraph paragraph = new Paragraph("Error! " + ex.Message);
paragraph.SetAlignment("center");
Chunk text = paragraph.Chunks[0] as Chunk;
if (text != null)
{
text.Font.Color = Color.RED;
}
doc.Add(paragraph);
}
finally
{
doc.Close();
}
}
Almost there. Clicking the button will result in an exception:
Control 'GridView1' of type 'GridView' must be placed inside a form tag with runat=server.
This is because we are trying to render the PlaceHolder control in a stream and not in a WebForm. A neat .NET security feature to prevent Injection attacks. This exception is simply ignored by adding the following code to the page:
public override void VerifyRenderingInServerForm(Control control)
{
}
Now, clicking the button again results in another Exception:
RegisterForEventValidation can only be called during Render();
Again, a security feature of .NET. Ignore this excpetion by setting EnableEventValidation="False" in the Page header. If you are concerned about the security of the page, please check official documentation of the features that has been disabled for the page.
Now, clicking the button should result in a PDF document. If not, debug the application and check Debut Output window for exceptions. Probably the XHTML in the placeholder is not valid.
Note that I am using HTMLWorker class instead of the HtmlParser class in the iTextSharp library. According the the author, the HTMLParser is not supported. I tried both, and the HTMLWorker swallows a lot more HTML markup than the HtmlParser.
You probably also want to clean the HTML before parsing it with the HTMLWorker. Typically you want to remove javascript postbacks, anchors etc. from the GridView. This can be achieved with the following code:
string html = stringWrite.ToString();
html = Regex.Replace(html, "</?(a|A).*?>", "");
StringReader reader = new StringReader(html);
You can also extend the HTMLWorker class making it more specialized for your purpose. For instance it would be great to be able to define pagebreaks in the final PDF document. Simply create a new class inherited from HTMLWorker.
public class HTMLWorkerExtended : HTMLWorker
{
public HTMLWorkerExtended(IDocListener document) : base(document)
{}
public override void StartElement(String tag, Hashtable h)
{
if (tag.Equals("newpage"))
document.Add(Chunk.NEXTPAGE);
else
base.StartElement(tag, h);
}
}
Now, simply replace the HTMLWorker with the extended version and add a <newpage /> element to the HTML in the placeholder where you want a new page.
There are some css styles not supported by default. For instance it is not possible to set the background-color style for an image or tablecell/-row. The only solution for adding more style support is changing the iTextSharp source. It's pretty simple, however. Open \text\html\simpleparser\FactoryProperties.cs. In the InsertStyle method add the following code to the foreach loop:
else if (key.Equals(Markup.CSS_KEY_BGCOLOR))
{
Color c = Markup.DecodeColor(prop[key]);
if (c != null)
{
int hh = c.ToArgb() & 0xffffff;
String hs = "#" + hh.ToString("X06", NumberFormatInfo.InvariantInfo);
h["bgcolor"] = hs;
}
}
Update: Another example for adding border color to a table:
First, set the border style for the table, ie. style="border-color: #ff0000;"
Then again, you need to apply the style to FactoryProperties.cs file as in the example above.
else if (key.Equals(Markup.CSS_KEY_BORDERCOLOR))
{
Color c = Markup.DecodeColor(prop[key]);
if (c != null)
{
int hh = c.ToArgb() & 0xffffff;
String hs = "#" + hh.ToString("X06", NumberFormatInfo.InvariantInfo);
h["border-color"] = hs;
}
}
In addition you need to alter the output of the table as there is no default "bordercolor" style property in the IncTable class.
Open IncTable.cs and change the following code in the BuildTable method:
Existing code:
for (int row = 0; row < rows.Count; ++row) {
ArrayList col = (ArrayList)rows[row];
for (int k = 0; k < col.Count; ++k) {
table.AddCell((PdfPCell)col[k]);
}
}
Replace with:
String bordercolor = (String)props["border-color"];
for (int row = 0; row < rows.Count; ++row)
{
ArrayList col = (ArrayList)rows[row];
for (int k = 0; k < col.Count; ++k)
{
PdfPCell cell = (PdfPCell)col[k];
cell.BorderColor = Markup.DecodeColor(bordercolor);
table.AddCell(cell);
}
}
This will change the border color on the cell in the table. Hint: It could be wise to check if the cell already has a border color before overwriting it with the table border color.
Recompile and add the new DLL to your project.
Happy Coding!
Using the RegularExpressionValidator for validating length of text
The RegularExpressionValidator can be used for almost everything regarding validation of input fields. My favourites are validating Email, Dates and length of text in textareas. The only problem with the length validation, is validating text that includes newline. One would think that the regex: .{0,4000} would be an easy way to validate maximum length of 4000 characters. That is true, but the dot does not include NewLine (CheatSheet). The RegularExpressionValidator also does NOT have an option for parsing the text as single line, which is a common Regular Expression option.
Reading another blog I found an expression that includes newlines: ^(.|\s){0,n)$
The only problem with this is that IE 6/7 and FF freezes when trying to test the expression. Seems like this is a JavaScript/ECMAScript problem. Disabling the clientscript for the RegularExpressionValidator however would probably solve this. The expression works fine in RegexCoach.
A comment in the blog suggested in using: ^[\s\S]{0,n}$
This expression seems to work fine in the browser. Also seems to work fine with number of characters. So I think I will stick with that for now.
Silverlight 2.0 Beta is here!
Finally, the first beta version of Silverlight 2.0 is here! You can download it at :
This download includes the plugin, the SDK and the Visual Studio 2008 tools.
Rumours say that the final release will be in September, but we will just wait and see…
You would also want to download Microsoft Expression Blend 2.5 Preview which has support for Silverlight 2
Just waiting for Scott Guthrie to start blogging about the details
Search
Knut Hamang
Recent Posts
Recent Comments
- Jack on Html to Pdf in .NET
- Øyvind on Creating dynamic menus in EPiServer
- Jim Liddell on [Updated] Continuous Integration using an LCD-TV
- Arshika on Html to Pdf in .NET
- Arshika on Html to Pdf in .NET



