Wednesday, March 12, 2008

Save to Word in ASP.NET

I was looking around for a code snippet to allow a user to save / export a print version of a web page to a Word file on the fly. This didn't need to be complex at all, but everything I found was either overwrought or would result in a Word document that displayed HTML tagging.

Here's an oversimplified method that works. You can build this up as needed. NOTE: Be sure to eliminate the extra space in the HTML tags below. Blogger's not behaving for me, so I had to insert them.

protected void Button1_Click(object sender, EventArgs e)
{
// Clear the response of any output
Response.Clear();
// Buffer this so the document comes down in one piece
Response.Buffer = true;
// You can do Excel, too, but I haven't tried that yet
Response.ContentType = "application/msword";
// You can use a variable instead of MyDoc, of course
Response.AddHeader("Content-Disposition", "inline;filename=MyDoc.doc");

StringBuilder myBuilder = new StringBuilder();
// This is the key bit versus other examples I've seen
// Adding the opening < html> etc forces Word to open this in Web Layout view
// If you use inline CSS, you can do much more
myBuilder.Append("< html>< head>< /head>< body>");
myBuilder.Append("< h1>Document Head< /h1>");
myBuilder.Append("< p>< strong>" + Label1.Text + "< /strong>< /p>");
myBuilder.Append("< p>" + TextBox1.Text + "< /p>");
// Do whatever else you need and append it as a string
myBuilder.Append("< /body>< /html>");

Response.Write(myBuilder);
Response.End();
Response.Flush();
}

Saturday, March 1, 2008

Save an ASP.NET TreeView Control to an XML File

Here are two methods to help you take a TreeView web control and save / export / serialize it to a file. Add these two statements to the top of the file:
using System.Xml;
using System.Text;
These two methods take the TreeView and recursively loop through all the TreeNode controls.
/// <summary>
/// Recurses through a TreeView web control exports the results
/// to an XML file nested to match the TreeView. Properties are
/// saved as attributes when a value is set.
/// <summary>
/// <param name="myTreeView">A TreeView web control.</param>
/// <param name="myFile">A file path and name with extension.</param>
protected void TreeViewToXml(System.Web.UI.WebControls.TreeView
myTreeView, string myFile)
{
try
{
XmlTextWriter xmlWriter = new XmlTextWriter(myFile, Encoding.UTF8);
xmlWriter.Formatting = Formatting.Indented;
xmlWriter.Indentation = 3;
xmlWriter.WriteStartDocument();
// Opens <TreeView>, the root node.
xmlWriter.WriteStartElement("TreeView");

// Go through child nodes.
ProcessNodes(myTreeView.Nodes, xmlWriter);

// </TreeView>, the root node.
xmlWriter.WriteEndElement();
xmlWriter.WriteEndDocument();

// Closes the object and saves the document to disk.
xmlWriter.Close();
}
catch
{
throw; // Throw any exceptions up the line.
}
}

/// <summary>
/// Recursively processes each TreeNode within a TreeNodeCollection
/// from a TreeView web control.
/// </summary>
/// <param name="myTreeNodes">A TreeNodeCollection from a TreeView web
/// control.</param>
/// <param name="myWriter">A XmlTextWriter being used to hold the XML
/// results from the TreeViewToXml method.</param>
protected void ProcessNodes(TreeNodeCollection myTreeNodes,
XmlTextWriter myWriter)
{
foreach (TreeNode thisNode in myTreeNodes)
{
myWriter.WriteStartElement("TreeNode"); // <TreeNode>.

// Go through each property and set it as an attribute if it exists.
if(!String.IsNullOrEmpty(thisNode.Text))
myWriter.WriteAttributeString("Text", thisNode.Text);
if (!String.IsNullOrEmpty(thisNode.ImageToolTip))
myWriter.WriteAttributeString("ImageToolTip",
thisNode.ImageToolTip);
if (!String.IsNullOrEmpty(thisNode.ImageUrl))
myWriter.WriteAttributeString("ImageUrl", thisNode.ImageUrl);
if (!String.IsNullOrEmpty(thisNode.NavigateUrl))
myWriter.WriteAttributeString("NavigateUrl",
thisNode.NavigateUrl);
if (!String.IsNullOrEmpty(thisNode.SelectAction.ToString()))
myWriter.WriteAttributeString("SelectAction",
thisNode.SelectAction.ToString());
if (!String.IsNullOrEmpty(thisNode.Target))
myWriter.WriteAttributeString("Target", thisNode.Target);
if (!String.IsNullOrEmpty(thisNode.ToolTip))
myWriter.WriteAttributeString("ToolTip", thisNode.ToolTip);
if (!String.IsNullOrEmpty(thisNode.Value))
myWriter.WriteAttributeString("Value", thisNode.Value);
if (!String.IsNullOrEmpty(thisNode.ValuePath))
myWriter.WriteAttributeString("ValuePath", thisNode.ValuePath);
if (thisNode.ShowCheckBox.HasValue)
myWriter.WriteAttributeString("ShowCheckBox",
thisNode.ShowCheckBox.ToString());
if (thisNode.Expanded.HasValue)
myWriter.WriteAttributeString("Expanded",
thisNode.Expanded.ToString());
myWriter.WriteAttributeString("Selected",
thisNode.Selected.ToString());
myWriter.WriteAttributeString("Checked",
thisNode.Checked.ToString());

// Recurse through any child nodes.
if (thisNode.ChildNodes.Count > 0)
ProcessNodes(thisNode.ChildNodes, myWriter);

myWriter.WriteEndElement(); // </TreeNode>.
}
}

Friday, February 22, 2008

Apache-Style Web Application Security (Almost) in ASP.NET with Web.config

I say "almost" because if it really was Apache-style, it would be easy. But this is Windows we're talking about, so it isn't.

What I wanted to do was pretty simple: launch a web app with anonymous access except for one directory to contain administrative controls, which I wanted to protect with the standard Login control and values stored in Web.config file.

For the Web.config settings, what I saw was all over the map in terms of what goes where, when, and why. Here's what I ended up using:

<configuration>
<!-- ... -->
<system.web>
<authentication mode="Forms">
<!-- When authentication is triggered, the login page will
be /EditLogin.aspx and the user will be sent to
/edit/default.aspx after a successful login attempt. -->
<forms loginUrl="EditLogin.aspx" defaultUrl="edit/default.aspx"
protection="Encryption">
<!-- Putting this into Web.config isn't exactly a security
best practice, but at least we'll use SHA1 encryption. -->
<credentials passwordFormat="SHA1">
<user name="user1" password="AAAAAAAAAAAAAAAAAAAAAAAAAAAA"/>
<user name="user2" password="BBBBBBBBBBBBBBBBBBBBBBBBBBBBB"/>
</credentials>
</forms>
</authentication>
<authorization>
<!-- This says that all users can get access to the app. -->
<allow users="*" />
</authorization>
</system.web>
<!--...-->
<!-- Anonymous access will be true for all directories except
this /edit directory. user1 and user2 can get access, but
all others will be denied. -->
<location allowOverride="false" path="edit">
<system.web>
<authorization>
<allow users="user1, user2"/>
<deny users="?"/>
</authorization>
</system.web>
</location>
</configuration>

Some documentation claims you can use ~/ in the loginUrl and defaultUrl and path definitions. Whenever I included it, the authentication settings were ignored. There are articles out there that start or end their path definitions with a /. This always threw a compile error for me. There are also articles that state you can nest an <authentication> element within <location>/<system.web>, but I always got a compile error on that, too.

I added a Login control to EditLogin.aspx, but I was initially under the impression that on submit the Web.config values would be used and the user would be redirected. This was incorrect. You need to set the Authenticate event handler in your login page.

protected void Login1_Authenticate(object sender,
AuthenticateEventArgs e)
{
if (FormsAuthentication.Authenticate(Login1.UserName,
Login1.Password))
FormsAuthentication.RedirectFromLoginPage(Login1.UserName,
false);
}
You'll also need to add OnAuthenticate="Login1_Authenticate" to the Login control.

I may have missed something obvious where I ran into errors, but I hope this prevents someone else from wasting a few hours of their day!

Wednesday, February 6, 2008

Free Information Retrieval Book Available

Introduction to Information Retrieval published by Cambridge University Press is available free online. Many thanks to the authors for this. They say it will remain free after the book is published. I may grab a copy of the PDFs...just in case.

Just reading the introduction makes me wish I paid more attention in math. I wonder if there are any math textbooks available on USENET? ;-)

Saturday, February 2, 2008

The Time Stamp of Your IIS Logs is Not Wrong

Looking at your IIS server logs and wondering why the time stamp doesn't seem to reflect the actual server time? If you have IIS set to write the logs in W3C extended log file format, the date is always in GMT. Microsoft does have a knowledge base article on this.

Here's a GMT conversion chart for the United States.