Introduction
When you create a FormView or ListView in ASP.NET and choose a data source, you get automatically generated templates which saves a lot of time. FormView templates are very basic and have no structure. ListView templates show as a grid by default but if you configure the ListView to display as "Single Row", it has the same basic FormView layout. The fields are separated by <br />
tags and the field controls are right next to the labels, which makes the form look jagged and strange.
While it's pretty easy to format the templates, if you have a lot of fields in your data source and are duplicating this formatting across multiple templates and for different FormViews/ListViews for the same data source, it can get pretty tedious and repetitive.
My solution is to create a Visual Studio macro to automatically take all the templates for a given FormView/ListView and wrap the field labels and controls in a basic asp:Table
layout.
Background
Visual Studio has "macros" which you can call while in the IDE. Macros give you the ability to write VB.NET code (not sure why it can't use C#?) to programmatically interact with the IDE. In this example, I'm interacting with whatever text the user has selected in the window that's open in the IDE. I'm sure my macro would work with 2005 and above, but my code uses .NET 3.5 functions in System.Linq
from .NET 3.5 so be aware that you'll need .NET 3.5 to make it work.
DTE
is the class used to interact with the IDE. In this example, I use DTE.ActiveDocument.Selection
to get the currently selected text in the active document. DTE
is late-bound and is really "EnvDTE80.DTE2
".
More information on macros is available here: http://msdn.microsoft.com/en-us/library/b4c73967%28v=vs.80%29.aspx.
Using the Code
Click View>Other Windows>Macro Explorer, or hit Alt+F8, to view the Macro Explorer. Right-click on MyMacros, click New Module, name it something you like, and click OK to create the Module. You should see the Module under MyMacros now, so double-click it to bring up the Visual Studio Macro IDE with your blank Module. Select all contents of the Module file and replace it with my code.
Save the code and close the Macro IDE, bringing you back to the Visual Studio IDE. Select all markup from the <FormView....>
or <ListView....>
begin tag until the end tag. The tags are being parsed as XML so remember, the start tag and end tag must match exactly with the same capitalization. In the Macro Explorer, right-click the FormatView
sub and click Run. You will be prompted to de-select any templates that you don't want updated, and then it will convert all of your templates into a table layout.
The Module has two primary methods, the main FormatView
Sub, which is what needs to be run, and the ConvertTemplateToTableLayout
Sub which does the actual parsing of the templates and formatting into a table layout.
Here is the FormatView()
Sub:
Sub FormatView()
Try
PopulateList()
Dim activeSelection As TextSelection = GetActiveText()
If (String.IsNullOrEmpty(activeSelection.Text.Trim())) Then
Throw New Exception("No markup is selected.")
End If
Dim viewControl As XElement = GetSelectedTextAsXml(activeSelection.Text)
Dim templateList As List(Of XElement) = GetTemplates(viewControl)
Dim templateChooserForm As New SelectTemplatesForm(templateList)
If (templateChooserForm.ShowDialog() = DialogResult.Cancel) Then
Return
Else
templateList = templateChooserForm.TemplatesToLoad
End If
For Each template As XElement In templateList
ConvertTemplateToTableLayout(template, _
viewControl.Elements()(0).Name.LocalName.ToLower())
Next
Dim newText As String = GetNewText(viewControl)
If (DTE.UndoContext.IsOpen) Then DTE.UndoContext.Close()
DTE.UndoContext.Open("FormatTemplates", False)
activeSelection.Delete()
activeSelection.Insert(newText, Nothing)
Try
activeSelection.DTE.ActiveDocument.Activate()
activeSelection.DTE.ExecuteCommand("Edit.FormatDocument")
activeSelection.DTE.ExecuteCommand("Edit.ToggleOutliningExpansion")
Catch ex As System.Runtime.InteropServices.COMException
Debug.WriteLine(ex.GetType().ToString() & vbNewLine & vbNewLine & ex.Message)
End Try
Catch ex As Exception
Dim errorMessage As String = ex.Message + System.Environment.NewLine + _
ex.StackTrace + System.Environment.NewLine
If (ex.InnerException IsNot Nothing) Then
errorMessage += System.Environment.NewLine + "Inner Exception:" + _
System.Environment.NewLine + ex.InnerException.ToString()
End If
Using newErrorMessageForm As New ErrorDialogForm(errorMessage, "Error")
newErrorMessageForm.ShowDialog()
End Using
Finally
If (DTE.UndoContext.IsOpen) Then DTE.UndoContext.Close()
End Try
End Sub
The code calls a few helpers along the way but the basic steps it performs are these:
First, the code gets the selected FormView
or ListView
text, parses it into an XElement
, and gets a list of templates used.
Next, it pops up a Windows Form to allow the user to uncheck the templates that they don't want updated.
Finally, it calls ConvertTemplateToTableLayout
for each selected template, outputs the results back into the IDE, and formats the output.
Before outputting the results, it opens a new UndoContext
, using DTE.UndoContext.Open("FormatTemplates", False)
, so if you don't like the results, you can click Undo and all of the changes will be undone all at once, bringing you back to your previous code.
There are two methods called after the resulting text is output back into the IDE used to format the results:
activeSelection.DTE.ExecuteCommand("Edit.FormatDocument")
activeSelection.DTE.ExecuteCommand("Edit.ToggleOutliningExpansion")
Edit.FormatDocument
does the same thing as if you were to click Edit>Advanced>Format Document, which just tabifies the document and makes it pretty. Edit.ToggleOutliningExpansion
just contracts all of the rows and tables so you can easily see the new templates.
The ConvertTemplateToTableLayout
Sub will be run once for each selected template. Here's the code:
Public Sub ConvertTemplateToTableLayout(ByRef template As XElement)
template.Nodes().Where(Function(n) TypeOf n Is XText AndAlso _
String.IsNullOrEmpty(DirectCast(n, XText).Value.Trim())).Remove()
template.Descendants().Where(Function(a) a.Name.LocalName = "br").Remove()
Dim templateNodes As IEnumerable(Of XNode)
If (templateType.ToLower() = "formview") Then
templateNodes = template.Nodes().ToArray()
ElseIf (templateType.ToLower() = "listview") Then
templateNodes = DirectCast(template.FirstNode, XElement).Nodes()
Else
Return
End If
Dim asp As XNamespace = "http://System.Web.UI.WebControls"
Dim tableElement As New XElement(asp + "Table", _
New XAttribute("runat", "server"), New XAttribute("id", "TableFields"))
Dim row As XElement = Nothing
For Each node In templateNodes
If (row IsNot Nothing AndAlso row.Attribute("id") IsNot Nothing _
AndAlso row.Attribute("id").Value = "CommandRow") Then
Dim commandCellElement As XElement = row.Elements()(0)
commandCellElement.Add(node)
ElseIf (TypeOf node Is XText) Then
Dim label As XText = DirectCast(node, XText)
label.Value = label.Value.Trim()
Dim rowID As String = template.Name.LocalName + _
label.Value.Trim().TrimEnd(Char.Parse(":")).Replace(".", _
"_").Replace(" ", "_") + "Row"
Dim cellElement As New XElement(asp + "TableCell", _
New XAttribute("CSSClass", "YOURCLASS"), label)
row = New XElement(asp + "TableRow", New XAttribute("id", rowID), _
New XAttribute("CSSClass", "YOURCLASS"))
row.Add(cellElement, System.Environment.NewLine)
tableElement.Add(row)
ElseIf (TypeOf node Is XElement) Then
Dim field As XElement = DirectCast(node, XElement)
If (row IsNot Nothing And False = field.Name.LocalName.Contains("Button")) Then
Dim cellElement As New XElement(asp + "TableCell", _
New XAttribute("CSSClass", "YOURCLASS"), field, System.Environment.NewLine)
row.Add(cellElement, System.Environment.NewLine)
Else
Dim commandCellElement As New XElement(asp + "TableCell", _
New XAttribute("id", "CommandCell"), field, System.Environment.NewLine)
row = New XElement(asp + "TableRow", New XAttribute("id", "CommandRow"))
row.Add(commandCellElement, System.Environment.NewLine)
tableElement.Add(row)
End If
End If
Next
template.Nodes.Remove()
If (templateType.ToLower() = "formview") Then
template.Add(tableElement)
ElseIf (templateType.ToLower() = "listview") Then
Dim outerTD As New XElement("td", New XAttribute("runat", "server"), tableElement)
template.Add(outerTD)
End If
End Sub
First it removes all <br />
nodes and blank or whitespace nodes in the template. Then it creates the asp XNamespace
; I just used "http://System.Web.UI.WebControls" since that's the programmatic namespace for the asp prefix, but the XML namespace could be anything you want.
Then it creates a new asp:Table
and loops through all of the XML nodes in the template. First we check to see if we are in the CommandRow
(created when we get to the end of the template), and if so, it just adds all nodes from that point to the CommandCell
.
If the node is an XText
, we know that it is the field label and we create a new TableRow
and TableCell
, add the node to the TableCell
, and add the TableRow
to the Table
.
If the node is an XElement
and the Name.LocalName
property doesn't contain "Button", we know we have a data source field control. So we create a new TableCell
, add the control node to it, and add the TableCell
to the already existing TableRow
.
If the node is an XElement
and it is a LinkButton
or Button
, we know we are at the end of the template and are getting into the Edit, Insert, Update, etc. buttons.
After all nodes in the template have been processed, they are all removed and replaced with the final Table XElement
.
Points of Interest
When I first started this macro, I recorded all of the keystrokes I made to modify each template. The macro used a lot of TextSelection
methods to select text and replace it with tags. It had to be run once for each template and it was very fragile. I think using XElement
to make the changes worked out much better but I couldn't figure out how to parse the asp tag prefixes. When I called XElement.Load
, it said that the asp prefix was not recognized or something like that. In the end, I just wrapped the selected text with an outer SelectedText
tag containing the xmlns
attribute, which made everything work correctly. After I finished processing, I just removed the SelectedText
tag again.
History
- 5/13/2011: First published.
- 5/13/2011: Added line to activate the document before sending the
Edit.FormatDocument
command.
- 5/16/2011: Fixed escaping and unescaping.
- 6/15/2011: Added ListView as supported control to format. Fixed template selection form to pop up on top of everything instead of popping up behind Visual Studio.