Introduction

When you create a FormView in ASP.NET and choose a data source, you get automatically generated templates which saves a lot of time. However, the templates are very basic and have no structure. 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 FormView's for the same data source it can get pretty tedious and repetitive.

My solution was to create a Visual Studio macro to automatically take all templates for a given FormView 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 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's 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....> begin tag until the </FormView> end tag. In the Macro Explorer, right-click the FormatFormView 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 FormatFormView 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 FormatFormView() Sub:

 Sub FormatFormView()
    Try
        ' 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 formView As XElement = GetSelectedTextAsXml(activeSelection.Text)
        ' Get the templates from the FormView
        Dim templateList As List(Of XElement) = GetTemplates(formView)
        ' 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)
        Next
        ' Convert the XDocument back into a string and overwrite selected text with new text
        Dim newText As String = GetNewText(formView)
        'If (ShowPreview(newText) = DialogResult.OK) Then
        ' 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 text, parses it into an XElement, and get's a list of templates used in the FormView.

Next it pops up a Windows Form to allow the user to uncheck the templates that they don't want updated. WARNING: Even though I call Me.TopMost in the Load event of the Form, it still sometimes pops up behind Visual Studio so you might have to click on the form in the taskbar to bring it in front of Visual Studio.

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()
    ' 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 template.Nodes
        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 field.Name.LocalName <> "LinkButton") Then
                Dim cellElement As New XElement(asp + "TableCell", New XAttribute("CSSClass", "YOURCLASS"), field, System.Environment.NewLine)
                row.Add(cellElement, System.Environment.NewLine)
            Else
                ' LinkButton'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()
    template.Add(tableElement)
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 is not "LinkButton" 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, 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 finished processing I just remove SelectedText tag again.

History 

First published: 5/13/2011 

Added line to activate the document before sending the Edit.FormatDocument command: 5/13/2011 

推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架
新浪微博粉丝精灵,刷粉丝、刷评论、刷转发、企业商家微博营销必备工具"