Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Use Visual Studio Macros to Format ListView or FormView Templates as Tables

0.00/5 (No votes)
15 Jun 2011 1  
The default FormView template has no structure. Use this Visual Studio macro to format templates as tables.

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()
        ' Make sure the user has selected some text
        Dim activeSelection As TextSelection = GetActiveText()
        If (String.IsNullOrEmpty(activeSelection.Text.Trim())) Then
            Throw New Exception("No markup is selected.")
        End If

        ' Create an XElement from the selected page markup
        Dim viewControl As XElement = GetSelectedTextAsXml(activeSelection.Text)

        ' Get the templates from the selected control
        Dim templateList As List(Of XElement) = GetTemplates(viewControl)

        ' Show a form to let the user choose which templates they want to replace
        Dim templateChooserForm As New SelectTemplatesForm(templateList)
        If (templateChooserForm.ShowDialog() = DialogResult.Cancel) Then
            Return
        Else
            templateList = templateChooserForm.TemplatesToLoad
        End If

        ' Convert each template to a table format
        For Each template As XElement In templateList
            ConvertTemplateToTableLayout(template, _
               viewControl.Elements()(0).Name.LocalName.ToLower())
        Next

        ' Convert the XDocument back into a string and overwrite selected text with new text
        Dim newText As String = GetNewText(viewControl)

        ' Create an undo context to undo the whole operation
        ' at once instead of having to undo each change individually
        If (DTE.UndoContext.IsOpen) Then DTE.UndoContext.Close()
        DTE.UndoContext.Open("FormatTemplates", False)

        ' Insert the new text
        activeSelection.Delete()
        activeSelection.Insert(newText, Nothing)

        ' Format the new text
        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
        'End If
    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.

ScreenShot.jpg

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)
    ' Remove all of the empty lines
    template.Nodes().Where(Function(n) TypeOf n Is XText AndAlso _
             String.IsNullOrEmpty(DirectCast(n, XText).Value.Trim())).Remove()
    ' Remove all "br" elements since we're now going to be using a table layout
    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
        ' skip the first <td> node
        templateNodes = DirectCast(template.FirstNode, XElement).Nodes()
    Else
        Return
    End If

    ' Create the asp: namespace and the table element to hold the new table rows and cells
    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
            ' Dealing with the label. Add a new row
            ' to the table and add a cell with the label.
            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
            ' Dealing with the field. Add a new cell to the row with the field.
            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
                ' Button's at the bottom of the template.
                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
        ' Add the outer td element back in
        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 &nbsp; 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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here