Walkthrough: Extending Database Rename Refactoring to Operate on Text Files

In this step-by-step topic, you will create, install, register, and test a new contributor for rename refactoring. This refactoring target will extend the capabilities of Visual Studio Team System Database Edition, to enable your database refactoring to rename references to database objects that are contained in text files in your database project.

When you add a new refactoring contributor to an existing type of refactoring, it must use an existing contributor input class.

This walkthrough illustrates the following tasks:

  1. Create a new assembly that contains the classes for a custom refactoring target.

  2. Install and register the assembly, so that the refactoring target is available in Database Edition.

  3. Create a simple database project to test that the refactoring target works as expected.

Prerequisites

You need the following components to complete this walkthrough:

  • You must have installed the general distribution release (GDR) for Visual Studio Team System 2008 Database Edition.

  • You must also have the Microsoft Visual Studio 2008 SDK installed on your computer. To download this kit, see this page on the Microsoft Web site: Visual Studio 2008 SDK Version 1.0.

Creating an Assembly with a Custom Refactoring Target

To create a custom refactoring target that enables rename refactoring to operate on text files, you must implement one class to provide a new RefactoringContributor:

  • RenameReferenceTextContributorContributor — This class builds the list of change proposals for the renamed symbol. The change proposals are for each reference that is contained in a text file that is in the database project.

Before you create this class, you will create a class library, add required references, and add some helper code that simplifies some of the code that you will write later in this walkthrough.

To create the class library and helper code

  1. Create a new C# class library project and name it RenameTextContributor.csproj.

    Note

    This walkthough uses Visual C#. However, all code samples are also provided for Visual Basic.

  2. Add references to the following .NET assemblies:

    • Microsoft.Data.Schema

    • Microsoft.Data.Schema.ScriptDom

    • Microsoft.Data.Schema.ScriptDom.sql

    • Microsoft.Data.Schema.Sql

    • Microsoft.VisualStudio.Data.Schema.Project

    • Microsoft.VisualStudio.Data.Schema.Project.Sql

  3. Add references to the following class libraries from the SDK:

    • C:\Program Files\Microsoft Visual Studio 2008 SDK\VisualStudioIntegration\Common\Assemblies\Microsoft.VisualStudio.OLE.Interop.dll

    • C:\Program Files\Microsoft Visual Studio 2008 SDK\VisualStudioIntegration\Common\Assemblies\Microsoft.VisualStudio.Shell.9.0.dll

    • C:\Program Files\Microsoft Visual Studio 2008 SDK\VisualStudioIntegration\Common\Assemblies\Microsoft.VisualStudio.Shell.Interop.dll

    • C:\Program Files\Microsoft Visual Studio 2008 SDK\VisualStudioIntegration\Common\Assemblies\Microsoft.VisualStudio.Shell.Interop.8.0.dll

    • C:\Program Files\Microsoft Visual Studio 2008 SDK\VisualStudioIntegration\Common\Assemblies\Microsoft.VisualStudio.Shell.Interop.9.0.dll

    • C:\Program Files\Microsoft Visual Studio 2008 SDK\VisualStudioIntegration\Common\Assemblies\Microsoft.VisualStudio.TextManager.Interop.dll

  4. In Solution Explorer, rename Class1.cs to SampleHelper.cs.

  5. Double-click SampleHelper.cs to open it in the code editor.

    Note

    This helper class is identical to the helper text that was used in the walkthrough for custom refactoring types. You can copy the source code from that project to the new project to save time.

  6. Replace the contents of the code editor with the following code:

    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.IO;
    using System.Runtime.InteropServices;
    using Microsoft.Data.Schema.ScriptDom.Sql;
    using Microsoft.VisualStudio;
    using Microsoft.VisualStudio.Data.Schema.Project.Common.UI;
    using Microsoft.VisualStudio.Data.Schema.Project.Refactoring;
    using Microsoft.VisualStudio.Shell.Interop;
    using Microsoft.VisualStudio.TextManager.Interop;
    using VsShell = Microsoft.VisualStudio.Shell.Interop;
    using VsTextMgr = Microsoft.VisualStudio.TextManager.Interop;
    using Microsoft.Data.Schema.SchemaModel;
    
    namespace MySamples.Refactoring
    {
        internal static class SampleHelper
        {
            public static String GetModelElementName(IModelElement modelElement)
            {
                SampleHelper.CheckNullArgument(modelElement, "modelElement");
                return modelElement.ToString();
            }
    
            /// <summary>
            /// Given a model element, returns its simple name.
            /// </summary>
            public static String GetModelElementSimpleName(IModelElement modelElement)
            {
                String separator = ".";
                String simpleName = String.Empty;
                String fullName = modelElement.ToString();
                String[] nameParts = fullName.Split(separator.ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
                if (nameParts.Length > 0)
                {
                    simpleName = nameParts[nameParts.Length - 1]; // last part 
                }
                if (simpleName.StartsWith("[") && simpleName.EndsWith("]"))
                {
                    simpleName = simpleName.Substring(1, simpleName.Length - 2);
                }
                return simpleName;
            }
    
            /// <summary>
            /// Find all files in the project with the specified file extension
            /// </summary>
            public static List<string> GetAllFilesInProject(IVsHierarchy solutionNode, string fileExtension, bool visibleNodesOnly)
            {
                List<string> files = new List<string>();
                if (null != solutionNode)
                {
                    EnumProjectItems(solutionNode, fileExtension, files, 
                                    VSConstants.VSITEMID_ROOT,  // item id of solution root node
                                    0,                          // recursion from solution node
                                    true,                       // hierarchy is Solution node
                                    visibleNodesOnly);          // visibleNodesOnly
                }
                return files;
            }
    
            /// <summary>
            /// Enumerates recursively over the hierarchy items.
            /// </summary>
            /// <param name="hierarchy">hierarchy to enmerate over.</param>
            /// <param name="fileExtension">type of files we need to collect from the project</param>
            /// <param name="files">list of file paths</param>
            /// <param name="itemid">item id of the hierarchy</param>
            /// <param name="recursionLevel">Depth of recursion. For example, if recursion started with the Solution
            /// node, then : Level 0 -- Solution node, Level 1 -- children of Solution, etc.</param>
            /// <param name="hierIsSolution">true if hierarchy is Solution Node. </param>
            /// <param name="visibleNodesOnly">true if only nodes visible in the Solution Explorer should
            /// be traversed. false if all project items should be traversed.</param>
            private static void EnumProjectItems(IVsHierarchy hierarchy,
                                                string fileExtension,
                                                List<string> files,
                                                uint itemid,
                                                int recursionLevel,
                                                bool hierIsSolution,
                                                bool visibleNodesOnly)  
            {
                int hr;
                IntPtr nestedHierarchyObj;
                uint nestedItemId;
                Guid hierGuid = typeof(IVsHierarchy).GUID;
    
    
                // Check first if this node has a nested hierarchy. 
                hr = hierarchy.GetNestedHierarchy(itemid, ref hierGuid, out nestedHierarchyObj, out nestedItemId);
                if (VSConstants.S_OK == hr && IntPtr.Zero != nestedHierarchyObj)
                {
                    IVsHierarchy nestedHierarchy = Marshal.GetObjectForIUnknown(nestedHierarchyObj) as IVsHierarchy;
                    Marshal.Release(nestedHierarchyObj);    
                    if (nestedHierarchy != null)
                    {
                        EnumProjectItems(nestedHierarchy, fileExtension, files,
                                        nestedItemId,
                                        recursionLevel,
                                        false,
                                        visibleNodesOnly);
                    }
                }
                else
                {
                    // Check if the file extension of this node matches 
                    string fileFullPath;
                    hierarchy.GetCanonicalName(itemid, out fileFullPath);
                    if (CompareExtension(fileFullPath, fileExtension))
                    {
                        // add matched file paths into the list
                        files.Add(fileFullPath);
                    }
    
                    recursionLevel++;
    
                    //Get the first child node of the current hierarchy being walked
                    object pVar;
                    hr = hierarchy.GetProperty(itemid,
                        ((visibleNodesOnly || (hierIsSolution && recursionLevel == 1) ?
                            (int)__VSHPROPID.VSHPROPID_FirstVisibleChild : (int)__VSHPROPID.VSHPROPID_FirstChild)),
                        out pVar);
                    ErrorHandler.ThrowOnFailure(hr);
                    if (VSConstants.S_OK == hr)
                    {
                        // Use Depth first search so at each level we recurse to check if the node has any children
                        // and then look for siblings.
                        uint childId = GetItemId(pVar);
                        while (childId != VSConstants.VSITEMID_NIL)
                        {
                            EnumProjectItems(hierarchy, fileExtension, files, childId, recursionLevel, false, visibleNodesOnly);
                            hr = hierarchy.GetProperty(childId,
                                ((visibleNodesOnly || (hierIsSolution && recursionLevel == 1)) ?
                                    (int)__VSHPROPID.VSHPROPID_NextVisibleSibling : (int)__VSHPROPID.VSHPROPID_NextSibling),
                                out pVar);
                            if (VSConstants.S_OK == hr)
                            {
                                childId = GetItemId(pVar);
                            }
                            else
                            {
                                ErrorHandler.ThrowOnFailure(hr);
                                break;
                            }
                        }
                    }
                }
            }
    
            /// <summary>
            /// Gets the item id.
            /// </summary>
            /// <param name="pvar">VARIANT holding an itemid.</param>
            /// <returns>Item Id of the concerned node</returns>
            private static uint GetItemId(object pvar)
            {
                if (pvar == null) return VSConstants.VSITEMID_NIL;
                if (pvar is int) return (uint)(int)pvar;
                if (pvar is uint) return (uint)pvar;
                if (pvar is short) return (uint)(short)pvar;
                if (pvar is ushort) return (uint)(ushort)pvar;
                if (pvar is long) return (uint)(long)pvar;
                return VSConstants.VSITEMID_NIL;
            }
    
            /// <summary>
            /// Check if the file has the expected extension.
            /// </summary>
            /// <param name="filePath"></param>
            /// <param name="extension"></param>
            /// <returns></returns>
            public static bool CompareExtension(string filePath, string extension)
            {
                bool equals = false;
                if (!string.IsNullOrEmpty(filePath))
                {
                    equals = (string.Compare(System.IO.Path.GetExtension(filePath), extension, StringComparison.OrdinalIgnoreCase) == 0);
                }
                return equals;
            }
    
            /// <summary>
            /// Read file content from a file
            /// </summary>
            /// <param name="filePath"> file path </param>
            /// <returns> file content in a string </returns>
            internal static string ReadFileContent(string filePath)
            {
                //  Ensure that the file exists first.
                if (!File.Exists(filePath))
                {
                    Debug.WriteLine(string.Format("Cannot find the file: '{0}'", filePath));
                    return string.Empty;
                }
    
                string content;
                using (StreamReader reader = new StreamReader(filePath))
                {
                    content = reader.ReadToEnd();
                    reader.Close();
                }
                return content;
            }
    
            /// <summary>
            ///  Check null references and throw
            /// </summary>
            /// <param name="obj"></param>
            /// <param name="?"></param>
            public static void CheckNullArgument(object obj, string objectName)
            {
                if (obj == null)
                {
                    throw new System.ArgumentNullException(objectName);
                }
            }
    
            /// <summary>
            /// Get offset of the fragment from an Identifier if the identifier.value matches the
            /// name we are looking for.
            /// </summary>
            /// <param name="identifier"></param>
            /// <param name="expectedName"></param>
            public static RawChangeInfo AddOffsestFromIdentifier(
               Identifier identifier,
                String expectedName,
                String newName,
                Boolean keepOldQuote)
            {
                RawChangeInfo change = null;
                if (identifier != null && String.Compare(expectedName,identifier.Value,true) == 0)
                {
                    if (keepOldQuote)
                    {
                        QuoteType newQuote = QuoteType.NotQuoted;
                        newName = Identifier.DecodeIdentifier(newName, out newQuote);
                        newName = Identifier.EncodeIdentifier(newName, identifier.QuoteType);
                    }
                    change = new RawChangeInfo(identifier.StartOffset, identifier.FragmentLength, expectedName, newName);
                }
                return change;
            }
    
            public static IList<ChangeProposal> ConvertOffsets(
                string projectFullName,
                string fileFullPath,
                List<RawChangeInfo> changes,
                bool defaultIncluded)
            {
                // Get the file content into IVsTextLines
                IVsTextLines textLines = GetTextLines(fileFullPath);
    
                int changesCount = changes.Count;
                List<ChangeProposal> changeProposals = new List<ChangeProposal>(changesCount);
                for (int changeIndex = 0; changeIndex < changesCount; changeIndex+)
                {
                    int startLine = 0;
                    int startColumn = 0;
                    int endLine = 0;
                    int endColumn = 0;
    
    
                    RawChangeInfo currentChange = changes[changeIndex];
                    int startPosition = currentChange.StartOffset;
                    int endPosition = currentChange.StartOffset + currentChange.Length;
                    int result = textLines.GetLineIndexOfPosition(startPosition, out startLine, out startColumn);
                    if (result == VSConstants.S_OK)
                    {
                        result = textLines.GetLineIndexOfPosition(endPosition, out endLine, out endColumn);
                        if (result == VSConstants.S_OK)
                        {
                            TextChangeProposal changeProposal = new TextChangeProposal(projectFullName, fileFullPath, currentChange.NewText);
                            changeProposal.StartLine = startLine;
                            changeProposal.StartColumn = startColumn;
                            changeProposal.EndLine = endLine;
                            changeProposal.EndColumn = endColumn;
                            changeProposal.Included = defaultIncluded;
                            changeProposals.Add(changeProposal);
                        }
                    }
    
                    if (result != VSConstants.S_OK)
                    {
                        throw new InvalidOperationException("Failed to convert offset");
                    }
                }
                return changeProposals;
            }
    
            /// <summary>
            /// Get IVsTextLines from a file.  If that file is in RDT, get text buffer from it.
            /// If the file is not in RDT, open that file in invisible editor and get text buffer
            /// from it.
            /// If failed to get text buffer, it will return null.        
            /// </summary>
            /// <param name="fullPathFileName">File name with full path.</param>
            /// <returns>Text buffer for that file.</returns>
            private static VsTextMgr.IVsTextLines GetTextLines(string fullPathFileName)
            {
                System.IServiceProvider serviceProvider = DataPackage.Instance;
                VsTextMgr.IVsTextLines textLines = null;
                VsShell.IVsRunningDocumentTable rdt = (VsShell.IVsRunningDocumentTable)serviceProvider.GetService(typeof(VsShell.SVsRunningDocumentTable));
    
                if (rdt != null)
                {
                    VsShell.IVsHierarchy ppHier = null;
                    uint pitemid, pdwCookie;
                    IntPtr ppunkDocData = IntPtr.Zero;
                    try
                    {
                        rdt.FindAndLockDocument((uint)(VsShell._VSRDTFLAGS.RDT_NoLock), fullPathFileName, out ppHier, out pitemid, out ppunkDocData, out pdwCookie);
                        if (pdwCookie != 0)
                        {
                            if (ppunkDocData != IntPtr.Zero)
                            {
                                try
                                {
                                    // Get text lines from the doc data
                                    IVsPersistDocData docData = (IVsPersistDocData)Marshal.GetObjectForIUnknown(ppunkDocData);
    
                                    if (docData is IVsTextLines)
                                    {
                                        textLines = (IVsTextLines)docData;
                                    }
                                    else
                                    {
                                        textLines = null;
                                    }
                                }
                                catch (ArgumentException)
                                {
                                    // Do nothing here, it will return null stream at the end.
                                }
                            }
                        }
                        else
                        {
                            // The file is not in RDT, open it in invisible editor and get the text lines from it.
                            IVsInvisibleEditor invisibleEditor = null;
                            TryGetTextLinesAndInvisibleEditor(fullPathFileName, out invisibleEditor, out textLines);
                        }
                    }
                    finally
                    {
                        if (ppunkDocData != IntPtr.Zero)
                            Marshal.Release(ppunkDocData);
                    }
                }
                return textLines;
            }
    
            /// <summary>
            /// Open the file in invisible editor in the running
            /// documents table (RDT), and get text buffer from that editor.
            /// </summary>
            /// <param name="fullPathFileName">File name with full path.</param>
            /// <param name="spEditor">The result invisible editor.</param>
            /// <param name="textLines">The result text buffer.</param>
            /// <returns>True, if the file is opened correctly in invisible editor.</returns>
            private static bool TryGetTextLinesAndInvisibleEditor(string fullPathFileName, out IVsInvisibleEditor spEditor, out IVsTextLines textLines)
            {
                System.IServiceProvider serviceProvider = DataPackage.Instance;
                spEditor = null;
                textLines = null;
    
                // Need to open this file.  Use the invisible editor manager to do so.
                IVsInvisibleEditorManager spIEM;
                IntPtr ppDocData = IntPtr.Zero;
                bool result;
    
                Guid IID_IVsTextLines = typeof(IVsTextLines).GUID;
    
                try
                {
                    spIEM = (IVsInvisibleEditorManager)serviceProvider.GetService(typeof(IVsInvisibleEditorManager));
                    spIEM.RegisterInvisibleEditor(fullPathFileName, null, (uint)_EDITORREGFLAGS.RIEF_ENABLECACHING, null, out spEditor);
                    if (spEditor != null)
                    {
                        int hr = spEditor.GetDocData(0, ref IID_IVsTextLines, out ppDocData);
                        if (hr == VSConstants.S_OK && ppDocData != IntPtr.Zero)
                        {
                            textLines = Marshal.GetTypedObjectForIUnknown(ppDocData, typeof(IVsTextLines)) as IVsTextLines;
                            result = true;
                        }
                        else
                        {
                            result = false;
                        }
                    }
                    else
                    {
                        result = false;
                    }
                }
                finally
                {
                    if (ppDocData != IntPtr.Zero)
                        Marshal.Release(ppDocData);
                }
                return result;
            }
        }
    }
    
    Imports System 
    Imports System.Collections.Generic 
    Imports System.Diagnostics 
    Imports System.IO 
    Imports System.Runtime.InteropServices 
    Imports Microsoft.Data.Schema.ScriptDom.Sql 
    Imports Microsoft.VisualStudio 
    Imports Microsoft.VisualStudio.Data.Schema.Project.Common.UI 
    Imports Microsoft.VisualStudio.Data.Schema.Project.Refactoring 
    Imports Microsoft.VisualStudio.Shell.Interop 
    Imports Microsoft.VisualStudio.TextManager.Interop 
    Imports VsShell = Microsoft.VisualStudio.Shell.Interop 
    Imports VsTextMgr = Microsoft.VisualStudio.TextManager.Interop 
    Imports Microsoft.Data.Schema.SchemaModel 
    
    Namespace MySamples.Refactoring 
        Friend Module SampleHelper 
            Private Sub New() 
            End Sub 
            Public Function GetModelElementName(ByVal modelElement As IModelElement) As [String] 
                SampleHelper.CheckNullArgument(modelElement, "modelElement") 
                Return modelElement.ToString() 
            End Function 
    
            ''' <summary> 
            ''' Given a model element, returns its simple name. 
            ''' </summary> 
            Public Function GetModelElementSimpleName(ByVal modelElement As IModelElement) As [String] 
                Dim separator As [String] = "." 
                Dim simpleName As [String] = [String].Empty 
                Dim fullName As [String] = modelElement.ToString() 
                Dim nameParts As [String]() = fullName.Split(separator.ToCharArray(), StringSplitOptions.RemoveEmptyEntries) 
                If nameParts.Length > 0 Then 
                        ' last part 
                    simpleName = nameParts(nameParts.Length - 1) 
                End If 
                If simpleName.StartsWith("[") AndAlso simpleName.EndsWith("]") Then 
                    simpleName = simpleName.Substring(1, simpleName.Length - 2) 
                End If 
                Return simpleName 
            End Function 
    
            ''' <summary> 
            ''' Find all files in the project with the specified file extension 
            ''' </summary> 
            Public Function GetAllFilesInProject(ByVal solutionNode As IVsHierarchy, ByVal fileExtension As String, ByVal visibleNodesOnly As Boolean) As List(Of String) 
                Dim files As New List(Of String)() 
                If solutionNode IsNot Nothing Then 
                    ' item id of solution root node 
                    ' recursion from solution node 
                    ' hierarchy is Solution node 
                        ' visibleNodesOnly 
                    EnumProjectItems(solutionNode, fileExtension, files, VSConstants.VSITEMID_ROOT, 0, True, _ 
                    visibleNodesOnly) 
                End If 
                Return files 
            End Function 
    
            ''' <summary> 
            ''' Enumerates recursively over the hierarchy items. 
            ''' </summary> 
            ''' <param name="hierarchy">hierarchy to enmerate over.</param> 
            ''' <param name="fileExtension">type of files we need to collect from the project</param> 
            ''' <param name="files">list of file paths</param> 
            ''' <param name="itemid">item id of the hierarchy</param> 
            ''' <param name="recursionLevel">Depth of recursion. For example, if recursion started with the Solution 
            ''' node, then : Level 0 -- Solution node, Level 1 -- children of Solution, etc.</param> 
            ''' <param name="hierIsSolution">true if hierarchy is Solution Node. </param> 
            ''' <param name="visibleNodesOnly">true if only nodes visible in the Solution Explorer should 
            ''' be traversed. false if all project items should be traversed.</param> 
            Private Sub EnumProjectItems(ByVal hierarchy As IVsHierarchy, ByVal fileExtension As String, ByVal files As List(Of String), ByVal itemid As UInteger, ByVal recursionLevel As Integer, ByVal hierIsSolution As Boolean, _ 
            ByVal visibleNodesOnly As Boolean) 
                Dim hr As Integer 
                Dim nestedHierarchyObj As IntPtr 
                Dim nestedItemId As UInteger 
                Dim hierGuid As Guid = GetType(IVsHierarchy).GUID 
    
    
                ' Check first if this node has a nested hierarchy. 
                hr = hierarchy.GetNestedHierarchy(itemid, hierGuid, nestedHierarchyObj, nestedItemId) 
                If VSConstants.S_OK = hr AndAlso IntPtr.Zero <> nestedHierarchyObj Then 
                    Dim nestedHierarchy As IVsHierarchy = TryCast(Marshal.GetObjectForIUnknown(nestedHierarchyObj), IVsHierarchy) 
                    Marshal.Release(nestedHierarchyObj) 
                    If nestedHierarchy IsNot Nothing Then 
                        EnumProjectItems(nestedHierarchy, fileExtension, files, nestedItemId, recursionLevel, False, _ 
                        visibleNodesOnly) 
                    End If 
                Else 
                    ' Check if the file extension of this node matches 
                    Dim fileFullPath As String 
                    hierarchy.GetCanonicalName(itemid, fileFullPath) 
                    If CompareExtension(fileFullPath, fileExtension) Then 
                        ' add matched file paths into the list 
                        files.Add(fileFullPath) 
                    End If 
    
                    recursionLevel += 1 
    
                    'Get the first child node of the current hierarchy being walked 
                    Dim pVar As Object 
                    hr = hierarchy.GetProperty(itemid, ((If(visibleNodesOnly OrElse (hierIsSolution AndAlso recursionLevel = 1), CInt(__VSHPROPID.VSHPROPID_FirstVisibleChild), CInt(__VSHPROPID.VSHPROPID_FirstChild)))), pVar) 
                    ErrorHandler.ThrowOnFailure(hr) 
                    If VSConstants.S_OK = hr Then 
                        ' Use Depth first search so at each level we recurse to check if the node has any children 
                        ' and then look for siblings. 
                        Dim childId As UInteger = GetItemId(pVar) 
                        While childId <> VSConstants.VSITEMID_NIL 
                            EnumProjectItems(hierarchy, fileExtension, files, childId, recursionLevel, False, _ 
                            visibleNodesOnly) 
                            hr = hierarchy.GetProperty(childId, (If((visibleNodesOnly OrElse (hierIsSolution AndAlso recursionLevel = 1)), CInt(__VSHPROPID.VSHPROPID_NextVisibleSibling), CInt(__VSHPROPID.VSHPROPID_NextSibling))), pVar) 
                            If VSConstants.S_OK = hr Then 
                                childId = GetItemId(pVar) 
                            Else 
                                ErrorHandler.ThrowOnFailure(hr) 
                                Exit While 
                            End If 
                        End While 
                    End If 
                End If 
            End Sub 
    
            ''' <summary> 
            ''' Gets the item id. 
            ''' </summary> 
            ''' <param name="pvar">VARIANT holding an itemid.</param> 
            ''' <returns>Item Id of the concerned node</returns> 
            Private Function GetItemId(ByVal pvar As Object) As UInteger 
                If pvar Is Nothing Then 
                    Return VSConstants.VSITEMID_NIL 
                End If 
                If TypeOf pvar Is Integer Then 
                    Return CUInt(CInt(pvar)) 
                End If 
                If TypeOf pvar Is UInteger Then 
                    Return CUInt(pvar) 
                End If 
                If TypeOf pvar Is Short Then 
                    Return CUInt(CShort(pvar)) 
                End If 
                If TypeOf pvar Is UShort Then 
                    Return CUInt(CUShort(pvar)) 
                End If 
                If TypeOf pvar Is Long Then 
                    Return CUInt(CLng(pvar)) 
                End If 
                Return VSConstants.VSITEMID_NIL 
            End Function 
    
            ''' <summary> 
            ''' Check if the file has the expected extension. 
            ''' </summary> 
            ''' <param name="filePath"></param> 
            ''' <param name="extension"></param> 
            ''' <returns></returns> 
            Public Function CompareExtension(ByVal filePath As String, ByVal extension As String) As Boolean 
                Dim equals As Boolean = False 
                If Not String.IsNullOrEmpty(filePath) Then 
                    equals = (String.Compare(System.IO.Path.GetExtension(filePath), extension, StringComparison.OrdinalIgnoreCase) = 0) 
                End If 
                Return equals 
            End Function 
    
            ''' <summary> 
            ''' Read file content from a file 
            ''' </summary> 
            ''' <param name="filePath"> file path </param> 
            ''' <returns> file content in a string </returns> 
            Friend Function ReadFileContent(ByVal filePath As String) As String 
                ' Ensure that the file exists first. 
                If Not File.Exists(filePath) Then 
                    Debug.WriteLine(String.Format("Cannot find the file: '{0}'", filePath)) 
                    Return String.Empty 
                End If 
    
                Dim content As String 
                Using reader As New StreamReader(filePath) 
                    content = reader.ReadToEnd() 
                    reader.Close() 
                End Using 
                Return content 
            End Function 
    
            ''' <summary> 
            ''' Check null references and throw 
            ''' </summary> 
            ''' <param name="obj"></param> 
            ''' <param name="?"></param> 
            Public Sub CheckNullArgument(ByVal obj As Object, ByVal objectName As String) 
                If obj Is Nothing Then 
                    Throw New System.ArgumentNullException(objectName) 
                End If 
            End Sub 
    
            ''' <summary> 
            ''' Get offset of the fragment from an Identifier if the identifier.value matches the 
            ''' name we are looking for. 
            ''' </summary> 
            ''' <param name="identifier"></param> 
            ''' <param name="expectedName"></param> 
            Public Function AddOffsestFromIdentifier(ByVal identifier__1 As Identifier, ByVal expectedName As [String], ByVal newName As [String], ByVal keepOldQuote As [Boolean]) As RawChangeInfo 
                Dim change As RawChangeInfo = Nothing 
                If identifier__1 IsNot Nothing AndAlso [String].Compare(expectedName, identifier__1.Value, True) = 0 Then 
                    If keepOldQuote Then 
                        Dim newQuote As QuoteType = QuoteType.NotQuoted 
                        newName = Identifier.DecodeIdentifier(newName, newQuote) 
                        newName = Identifier.EncodeIdentifier(newName, identifier__1.QuoteType) 
                    End If 
                    change = New RawChangeInfo(identifier__1.StartOffset, identifier__1.FragmentLength, expectedName, newName) 
                End If 
                Return change 
            End Function 
    
            Public Function ConvertOffsets(ByVal projectFullName As String, ByVal fileFullPath As String, ByVal changes As List(Of RawChangeInfo), ByVal defaultIncluded As Boolean) As IList(Of ChangeProposal) 
                ' Get the file content into IVsTextLines 
                Dim textLines As IVsTextLines = GetTextLines(fileFullPath) 
    
                Dim changesCount As Integer = changes.Count 
                Dim changeProposals As New List(Of ChangeProposal)(changesCount) 
                For changeIndex As Integer = 0 To changesCount - 1 
                    Dim startLine As Integer = 0 
                    Dim startColumn As Integer = 0 
                    Dim endLine As Integer = 0 
                    Dim endColumn As Integer = 0 
    
    
                    Dim currentChange As RawChangeInfo = changes(changeIndex) 
                    Dim startPosition As Integer = currentChange.StartOffset 
                    Dim endPosition As Integer = currentChange.StartOffset + currentChange.Length 
                    Dim result As Integer = textLines.GetLineIndexOfPosition(startPosition, startLine, startColumn) 
                    If result = VSConstants.S_OK Then 
                        result = textLines.GetLineIndexOfPosition(endPosition, endLine, endColumn) 
                        If result = VSConstants.S_OK Then 
                            Dim changeProposal As New TextChangeProposal(projectFullName, fileFullPath, currentChange.NewText) 
                            changeProposal.StartLine = startLine 
                            changeProposal.StartColumn = startColumn 
                            changeProposal.EndLine = endLine 
                            changeProposal.EndColumn = endColumn 
                            changeProposal.Included = defaultIncluded 
                            changeProposals.Add(changeProposal) 
                        End If 
                    End If 
    
                    If result <> VSConstants.S_OK Then 
                        Throw New InvalidOperationException("Failed to convert offset") 
                    End If 
                Next 
                Return changeProposals 
            End Function 
    
            ''' <summary> 
            ''' Get IVsTextLines from a file. If that file is in RDT, get text buffer from it. 
            ''' If the file is not in RDT, open that file in invisible editor and get text buffer 
            ''' from it. 
            ''' If failed to get text buffer, it will return null. 
            ''' </summary> 
            ''' <param name="fullPathFileName">File name with full path.</param> 
            ''' <returns>Text buffer for that file.</returns> 
            Private Function GetTextLines(ByVal fullPathFileName As String) As VsTextMgr.IVsTextLines 
                Dim serviceProvider As System.IServiceProvider = DataPackage.Instance 
                Dim textLines As VsTextMgr.IVsTextLines = Nothing 
                Dim rdt As VsShell.IVsRunningDocumentTable = DirectCast(serviceProvider.GetService(GetType(VsShell.SVsRunningDocumentTable)), VsShell.IVsRunningDocumentTable) 
    
                If rdt IsNot Nothing Then 
                    Dim ppHier As VsShell.IVsHierarchy = Nothing 
                    Dim pitemid As UInteger, pdwCookie As UInteger 
                    Dim ppunkDocData As IntPtr = IntPtr.Zero 
                    Try 
                        rdt.FindAndLockDocument(CUInt((VsShell._VSRDTFLAGS.RDT_NoLock)), fullPathFileName, ppHier, pitemid, ppunkDocData, pdwCookie) 
                        If pdwCookie <> 0 Then 
                            If ppunkDocData <> IntPtr.Zero Then 
                                Try 
                                    ' Get text lines from the doc data 
                                    Dim docData As IVsPersistDocData = DirectCast(Marshal.GetObjectForIUnknown(ppunkDocData), IVsPersistDocData) 
    
                                    If TypeOf docData Is IVsTextLines Then 
                                        textLines = DirectCast(docData, IVsTextLines) 
                                    Else 
                                        textLines = Nothing 
                                    End If 
                                Catch generatedExceptionName As ArgumentException 
                                    ' Do nothing here, it will return null stream at the end. 
                                End Try 
                            End If 
                        Else 
                            ' The file is not in RDT, open it in invisible editor and get the text lines from it. 
                            Dim invisibleEditor As IVsInvisibleEditor = Nothing 
                            TryGetTextLinesAndInvisibleEditor(fullPathFileName, invisibleEditor, textLines) 
                        End If 
                    Finally 
                        If ppunkDocData <> IntPtr.Zero Then 
                            Marshal.Release(ppunkDocData) 
                        End If 
                    End Try 
                End If 
                Return textLines 
            End Function 
    
            ''' <summary> 
            ''' Open the file in invisible editor in the running 
            ''' documents table (RDT), and get text buffer from that editor. 
            ''' </summary> 
            ''' <param name="fullPathFileName">File name with full path.</param> 
            ''' <param name="spEditor">The result invisible editor.</param> 
            ''' <param name="textLines">The result text buffer.</param> 
            ''' <returns>True, if the file is opened correctly in invisible editor.</returns> 
            Private Function TryGetTextLinesAndInvisibleEditor(ByVal fullPathFileName As String, ByRef spEditor As IVsInvisibleEditor, ByRef textLines As IVsTextLines) As Boolean 
                Dim serviceProvider As System.IServiceProvider = DataPackage.Instance 
                spEditor = Nothing 
                textLines = Nothing 
    
                ' Need to open this file. Use the invisible editor manager to do so. 
                Dim spIEM As IVsInvisibleEditorManager 
                Dim ppDocData As IntPtr = IntPtr.Zero 
                Dim result As Boolean 
    
                Dim IID_IVsTextLines As Guid = GetType(IVsTextLines).GUID 
    
                Try 
                    spIEM = DirectCast(serviceProvider.GetService(GetType(IVsInvisibleEditorManager)), IVsInvisibleEditorManager) 
                    spIEM.RegisterInvisibleEditor(fullPathFileName, Nothing, CUInt(_EDITORREGFLAGS.RIEF_ENABLECACHING), Nothing, spEditor) 
                    If spEditor IsNot Nothing Then 
                        Dim hr As Integer = spEditor.GetDocData(0, IID_IVsTextLines, ppDocData) 
                        If hr = VSConstants.S_OK AndAlso ppDocData <> IntPtr.Zero Then 
                            textLines = TryCast(Marshal.GetTypedObjectForIUnknown(ppDocData, GetType(IVsTextLines)), IVsTextLines) 
                            result = True 
                        Else 
                            result = False 
                        End If 
                    Else 
                        result = False 
                    End If 
                Finally 
                    If ppDocData <> IntPtr.Zero Then 
                        Marshal.Release(ppDocData) 
                    End If 
                End Try 
                Return result 
            End Function 
        End Module 
    End Namespace
    
  7. On the File menu, click Save SampleHelper.cs.

  8. Add a class named RawChangeInfo to the project.

  9. In the code editor, update the code to match the following:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    
    namespace MySamples.Refactoring
    {
        /// <summary>
        /// Helper class to encapsulate StartOffset, FragmentLength and change string from
        /// parser and SchemaAnalzyer.
        /// </summary>
        internal sealed class RawChangeInfo
        {
            private int _startOffset;
            private int _length;
            private string _oldText;
            private string _newText;
    
            public RawChangeInfo(int startOffset, int length, string oldText, string newText)
            {
                //ArgumentValidation.CheckForOutOfRangeException(startOffset, 0, int.MaxValue);
                //ArgumentValidation.CheckForOutOfRangeException(length, 0, int.MaxValue);
                //ArgumentValidation.CheckForNullReference(oldText, "oldText");
                //ArgumentValidation.CheckForNullReference(newText, "newText");
    
                _startOffset = startOffset;
                _length = length;
                _oldText = oldText;
                _newText = newText;
            }
    
            public int StartOffset
            {
                get
                {
                    return _startOffset;
                }
                set
                {
                    _startOffset = value;
                }
            }
    
            public int Length
            {
                get
                {
                    return _length;
                }
            }
    
            public string OldText
            {
                get
                {
                    return _oldText;
                }
            }
    
            public string NewText
            {
                get
                {
                    return _newText;
                }
                set
                {
                    _newText = value;
                }
            }
         }
      }
    
    Imports System 
    Imports System.Collections.Generic 
    Imports System.Linq 
    Imports System.Text 
    
    Namespace MySamples.Refactoring 
        ''' <summary> 
        ''' Helper class to encapsulate StartOffset, FragmentLength and change string from 
        ''' parser and SchemaAnalzyer. 
        ''' </summary> 
        Friend NotInheritable Class RawChangeInfo 
            Private _startOffset As Integer 
            Private _length As Integer 
            Private _oldText As String 
            Private _newText As String 
    
            Public Sub New(ByVal startOffset As Integer, ByVal length As Integer, ByVal oldText As String, ByVal newText As String) 
                'ArgumentValidation.CheckForOutOfRangeException(startOffset, 0, int.MaxValue); 
                'ArgumentValidation.CheckForOutOfRangeException(length, 0, int.MaxValue); 
                'ArgumentValidation.CheckForNullReference(oldText, "oldText"); 
                'ArgumentValidation.CheckForNullReference(newText, "newText"); 
    
                _startOffset = startOffset 
                _length = length 
                _oldText = oldText 
                _newText = newText 
            End Sub 
    
            Public Property StartOffset() As Integer 
                Get 
                    Return _startOffset 
                End Get 
                Set(ByVal value As Integer) 
                    _startOffset = value 
                End Set 
            End Property 
    
            Public ReadOnly Property Length() As Integer 
                Get 
                    Return _length 
                End Get 
            End Property 
    
            Public ReadOnly Property OldText() As String 
                Get 
                    Return _oldText 
                End Get 
            End Property 
    
            Public Property NewText() As String 
                Get 
                    Return _newText 
                End Get 
                Set(ByVal value As String) 
                    _newText = value 
                End Set 
            End Property 
        End Class 
    End Namespace
    
  10. On the File menu, click Save SampleHelper.cs.

    Next, you will define the RenameReferenceTextContributor class.

To define the RenameReferenceTextContributor class

  1. Add a class named RenameReferenceTextContributor to your project.

  2. In the code editor, update the using statements to match the following:

    using System;
    using System.Collections.Generic;
    using Microsoft.Data.Schema.Common;
    using Microsoft.Data.Schema.Extensibility;
    using Microsoft.Data.Schema.Sql.SqlDsp;
    using Microsoft.VisualStudio;
    using Microsoft.VisualStudio.Data.Schema.Project.Refactoring;
    using Microsoft.VisualStudio.Data.Schema.Project.Sql.Refactoring.Rename;
    
    Imports System 
    Imports System.Collections.Generic 
    Imports Microsoft.Data.Schema.Common 
    Imports Microsoft.Data.Schema.Extensibility 
    Imports Microsoft.Data.Schema.Sql.SqlDsp 
    Imports Microsoft.VisualStudio 
    Imports Microsoft.VisualStudio.Data.Schema.Project.Refactoring 
    Imports Microsoft.VisualStudio.Data.Schema.Project.Sql.Refactoring.Rename
    
  3. Change the namespace to MySamples.Refactoring:

    namespace MySamples.Refactoring
    
  4. Update the class definition to match the following:

        [DatabaseSchemaProviderCompatibility(typeof(SqlDatabaseSchemaProvider))]
        internal class RenameReferenceTextContributor : RefactoringContributor<RenameReferenceContributorInput>
        {
        }
    
    <DatabaseSchemaProviderCompatibility(GetType(SqlDatabaseSchemaProvider))> _ 
    Friend Class RenameReferenceTextContributor 
        Inherits RefactoringContributor(Of RenameReferenceContributorInput) 
    End Class
    

    Specify the attribute to declare that this contributor is compatible with any database schema providers that are derived from SqlDatabaseSchemaProvider. Your class must inherit from RefactoringContributor for the RenameReferenceContributorInput.

  5. Define additional constants and private member variables:

            #region const
            private const string TxtExtension = @".txt";
            private const string PreviewFriendlyName = @"Text Files";
            private const string PreviewDescription = @"Update text symbols in text files in the database project.";
            private const string PreviewWarningMessage = @"Updating text symbols in text files in the database project can cause inconsistency.";
    
            #endregion
    
            #region members
            private RefactoringPreviewGroup _textPreviewGroup;
            private List<String> _changingFiles;
            #endregion
    
    #Region "const" 
    Private Const TxtExtension As String = ".txt" 
    Private Const PreviewFriendlyName As String = "Text Files" 
    Private Const PreviewDescription As String = "Update text symbols in text files in the database project." 
    Private Const PreviewWarningMessage As String = "Updating text symbols in text files in the database project can cause inconsistency." 
    
    #End Region 
    
    #Region "members" 
    Private _textPreviewGroup As RefactoringPreviewGroup 
    Private _changingFiles As List(Of [String]) 
    #End Region
    

    The constants provide information that will appear in the Preview Window. The additional members are used to keep track of the preview group and the list of text files being changed.

  6. Add the class constructor:

            #region ctor
            public RenameReferenceTextContributor()
            {
                _textPreviewGroup = new RefactoringPreviewGroup(PreviewFriendlyName);
                _textPreviewGroup.Description = PreviewDescription;
                _textPreviewGroup.WarningMessage = PreviewWarningMessage;
                _textPreviewGroup.EnableChangeGroupUncheck = true;
                _textPreviewGroup.EnableChangeUncheck = true;
                _textPreviewGroup.DefaultChecked = false; 
                _textPreviewGroup.IncludeInCurrentProject = true;
    
                // This sample uses the default icon for the file,
                // but you could provide your own icon here.
                //RefactoringPreviewGroup.RegisterIcon(TxtExtension, "textfile.ico");
            }
            #endregion
    
    #Region "ctor" 
    Public Sub New() 
        _textPreviewGroup = New RefactoringPreviewGroup(PreviewFriendlyName) 
        _textPreviewGroup.Description = PreviewDescription 
        _textPreviewGroup.WarningMessage = PreviewWarningMessage 
        _textPreviewGroup.EnableChangeGroupUncheck = True 
        _textPreviewGroup.EnableChangeUncheck = True 
        _textPreviewGroup.DefaultChecked = False 
    
            ' This sample uses the default icon for the file, 
            ' but you could provide your own icon here. 
            'RefactoringPreviewGroup.RegisterIcon(TxtExtension, "textfile.ico"); 
        _textPreviewGroup.IncludeInCurrentProject = True 
    End Sub 
    #End Region
    

    The constructor initializes the model element, creating a new preview group and initializing its properties. You could also register icons here, to be displayed in the Preview Window for specific file name extensions. Because there is no syntax highlighting for text files, you do not register a language service for the text files.

  7. Override the PreviewGroup property to return the group that was created when this contributor was created:

            /// <summary>
            /// Preview group for text files
            /// </summary>
            public override RefactoringPreviewGroup PreviewGroup
            {
                get
                {
                    return _textPreviewGroup;
                }
                set
                {
                    _textPreviewGroup = value;
                }
            }
    
    ''' <summary> 
    ''' Preview group for text files 
    ''' </summary> 
    Public Overloads Overrides Property PreviewGroup() As RefactoringPreviewGroup 
        Get 
            Return _textPreviewGroup 
        End Get 
        Set(ByVal value As RefactoringPreviewGroup) 
            _textPreviewGroup = value 
        End Set 
    End Property
    
  8. Override the ContributeChanges method to return a list of change proposals:

            /// <summary>
            /// Contribute to the change proposals
            /// </summary>
            /// <param name="input">contributor input</param>
            /// <returns>List of change proposals with corresponding contributor inputs</returns>
            protected override Tuple<IList<ChangeProposal>, IList<ContributorInput>> ContributeChanges(RenameReferenceContributorInput input)
            {
                RenameReferenceContributorInput referenceInput = input as RenameReferenceContributorInput;
                if (referenceInput == null)
                {
                    throw new ArgumentNullException("input");
                }
    
                string projectFullName;
                referenceInput.RefactoringOperation.CurrentProjectHierarchy.GetCanonicalName(VSConstants.VSITEMID_ROOT, out projectFullName);
    
                return GetChangesForAllTextFiles(referenceInput, 
                                                projectFullName, 
                                                _textPreviewGroup.DefaultChecked, 
                                                out _changingFiles);
            }
    
            /// <summary>
            /// Contribute to the change proposals
            /// </summary>
            /// <param name="input">contributor input</param>
            /// <returns>List of change proposals with corresponding contributor inputs</returns>
            protected override Tuple<IList<ChangeProposal>, IList<ContributorInput>> ContributeChanges(RenameReferenceContributorInput input)
            {
                RenameReferenceContributorInput referenceInput = input as RenameReferenceContributorInput;
                if (referenceInput == null)
                {
                    throw new ArgumentNullException("input");
                }
    
                string projectFullName;
                referenceInput.RefactoringOperation.CurrentProjectHierarchy.GetCanonicalName(VSConstants.VSITEMID_ROOT, out projectFullName);
    
                return GetChangesForAllTextFiles(referenceInput, 
                                                projectFullName, 
                                                _textPreviewGroup.DefaultChecked, 
                                                out _changingFiles);
            }
    

    This method calls the GetAllChangesForAllTextFiles method to do most of the work.

  9. Override the WriteOperationReferenceLogData method to record your changes in the refactoring log file:

            protected override void WriteOperationReferenceLogData(System.Xml.XmlWriter writer)
            {
                const string XmlElement_TextReferences = "TextReferences";
    
                writer.WriteStartElement(XmlElement_TextReferences);
                if (_changingFiles != null)
                {
                    foreach (String filename in _changingFiles)
                    {
                        writer.WriteStartElement("Reference");
                        writer.WriteAttributeString("TextFilename", filename);
                        writer.WriteEndElement();
                    }
                }
                writer.WriteEndElement();
            }
    
    Protected Overloads Overrides Sub WriteOperationReferenceLogData(ByVal writer As System.Xml.XmlWriter) 
        Const XmlElement_TextReferences As String = "TextReferences" 
    
        writer.WriteStartElement(XmlElement_TextReferences) 
        If _changingFiles IsNot Nothing Then 
            For Each filename As [String] In _changingFiles 
                writer.WriteStartElement("Reference") 
                writer.WriteAttributeString("TextFilename", filename) 
                writer.WriteEndElement() 
            Next 
        End If 
        writer.WriteEndElement() 
    End Sub
    
  10. Add the GetChangesForAllTextFiles method to iterate over the list of text files that are contained in the project, obtain the changes for each file, and aggregate those changes into a list of change proposals:

            /// <summary>
            /// Get all changes from all text files.
            /// </summary>
            private static Tuple<IList<ChangeProposal>, IList<ContributorInput>> GetChangesForAllTextFiles(
                RenameReferenceContributorInput input,
                string projectFullName,
                bool defaultChecked,
                out List<String> changingFiles)
            {
                if (input == null)
                {
                    throw new ArgumentNullException("input");
                }
    
                changingFiles = new List<String>();
                List<ChangeProposal> allChanges = new List<ChangeProposal>();
    
                List<string> files = new List<string>();
                files = SampleHelper.GetAllFilesInProject(input.RefactoringOperation.CurrentProjectHierarchy, TxtExtension, false);
    
                // process the text files one by one
                if (files != null && files.Count > 0)
                {
                    int fileCount = files.Count;
    
                    // Get all the changes for all txt files.
                    for (int fileIndex = 0; fileIndex < fileCount; fileIndex+)
                    {
                        IList<ChangeProposal> changes =
                            GetChangesForOneTextFile(
                                        input,
                                        projectFullName,
                                        files[fileIndex],
                                        defaultChecked);
                        if (changes != null && changes.Count > 0)
                        {
                            allChanges.AddRange(changes);
                            changingFiles.Add(files[fileIndex]);
                        }
                    }
                }
                return new Tuple<IList<ChangeProposal>, IList<ContributorInput>>(allChanges, null);
            }
    
            /// <summary>
            /// Get all changes from all text files.
            /// </summary>
            private static Tuple<IList<ChangeProposal>, IList<ContributorInput>> GetChangesForAllTextFiles(
                RenameReferenceContributorInput input,
                string projectFullName,
                bool defaultChecked,
                out List<String> changingFiles)
            {
                if (input == null)
                {
                    throw new ArgumentNullException("input");
                }
    
                changingFiles = new List<String>();
                List<ChangeProposal> allChanges = new List<ChangeProposal>();
    
                List<string> files = new List<string>();
                files = SampleHelper.GetAllFilesInProject(input.RefactoringOperation.CurrentProjectHierarchy, TxtExtension, false);
    
                // process the text files one by one
                if (files != null && files.Count > 0)
                {
                    int fileCount = files.Count;
    
                    // Get all the changes for all txt files.
                    for (int fileIndex = 0; fileIndex < fileCount; fileIndex+)
                    {
                        IList<ChangeProposal> changes =
                            GetChangesForOneTextFile(
                                        input,
                                        projectFullName,
                                        files[fileIndex],
                                        defaultChecked);
                        if (changes != null && changes.Count > 0)
                        {
                            allChanges.AddRange(changes);
                            changingFiles.Add(files[fileIndex]);
                        }
                    }
                }
                return new Tuple<IList<ChangeProposal>, IList<ContributorInput>>(allChanges, null);
            }
    
  11. Implement the GetChangesForOneTextFileMethod method to return a list of change proposals that are contained in a single text file:

            /// <summary>
            /// Get all the change proposals from one text file.
            /// </summary>
            private static IList<ChangeProposal> GetChangesForOneTextFile(
                RenameReferenceContributorInput input,
                string projectFullName,
                string fileFullPath,
                bool defaultChecked)
            {
                const string separators = " \t \r \n \\()[]{}|.+-*/~!@#$%^&<>?:;";
    
                string fileContent = SampleHelper.ReadFileContent(fileFullPath);
    
                IList<ChangeProposal> changeProposals= null;
                if (string.IsNullOrEmpty(fileContent))
                {
                    // return empty change list
                    changeProposals = new List<ChangeProposal>();
                }
                else
                {
                    int startIndex = 0;
                    int maxIndex = fileContent.Length - 1;
                    string oldName = input.OldName;
                    int oldNameLength = oldName.Length;
                    List<RawChangeInfo> changes = new List<RawChangeInfo>();
                    while (startIndex < maxIndex)
                    {
                        // Text files do not understand schema object information
                        // We do just case-insensitive string match (without schema info)
                        // Only match whole word
                        int offset = fileContent.IndexOf(oldName, startIndex, StringComparison.OrdinalIgnoreCase);
    
                        // Cannot find match any more, stop the match
                        if (offset < 0)
                        {
                            break;
                        }
    
                        startIndex = offset + oldNameLength;
    
                        // match whole word: check before/after characters are separators
                        if (offset > 0)
                        {
                            char charBeforeMatch = fileContent[offset - 1];
                            // match starts in the middle of a token, discard and move on
                            if (!separators.Contains(charBeforeMatch.ToString()))
                            {
                                continue;
                            }
                        }
                        if (offset + oldNameLength < maxIndex)
                        {
                            char charAfterMatch = fileContent[offset + oldNameLength];
                            // match ends in the middle of a token, discard and move on
                            if (!separators.Contains(charAfterMatch.ToString()))
                            {
                                continue;
                            }
                        }
    
                        RawChangeInfo change = new RawChangeInfo(offset, oldNameLength, input.OldName, input.NewName);
                        changes.Add(change);
                    }
    
                    // convert index-based offsets to ChangeOffsets in ChangeProposals
                    changeProposals = SampleHelper.ConvertOffsets(
                        projectFullName,
                        fileFullPath,
                        changes,
                        defaultChecked);
                }
                return changeProposals;
            }
    
            /// <summary>
            /// Get all the change proposals from one text file.
            /// </summary>
            private static IList<ChangeProposal> GetChangesForOneTextFile(
                RenameReferenceContributorInput input,
                string projectFullName,
                string fileFullPath,
                bool defaultChecked)
            {
                const string separators = " \t \r \n \\()[]{}|.+-*/~!@#$%^&<>?:;";
    
                string fileContent = SampleHelper.ReadFileContent(fileFullPath);
    
                IList<ChangeProposal> changeProposals= null;
                if (string.IsNullOrEmpty(fileContent))
                {
                    // return empty change list
                    changeProposals = new List<ChangeProposal>();
                }
                else
                {
                    int startIndex = 0;
                    int maxIndex = fileContent.Length - 1;
                    string oldName = input.OldName;
                    int oldNameLength = oldName.Length;
                    List<RawChangeInfo> changes = new List<RawChangeInfo>();
                    while (startIndex < maxIndex)
                    {
                        // Text files do not understand schema object information
                        // We do just case-insensitive string match (without schema info)
                        // Only match whole word
                        int offset = fileContent.IndexOf(oldName, startIndex, StringComparison.OrdinalIgnoreCase);
    
                        // Cannot find match any more, stop the match
                        if (offset < 0)
                        {
                            break;
                        }
    
                        startIndex = offset + oldNameLength;
    
                        // match whole word: check before/after characters are separators
                        if (offset > 0)
                        {
                            char charBeforeMatch = fileContent[offset - 1];
                            // match starts in the middle of a token, discard and move on
                            if (!separators.Contains(charBeforeMatch.ToString()))
                            {
                                continue;
                            }
                        }
                        if (offset + oldNameLength < maxIndex)
                        {
                            char charAfterMatch = fileContent[offset + oldNameLength];
                            // match ends in the middle of a token, discard and move on
                            if (!separators.Contains(charAfterMatch.ToString()))
                            {
                                continue;
                            }
                        }
    
                        RawChangeInfo change = new RawChangeInfo(offset, oldNameLength, input.OldName, input.NewName);
                        changes.Add(change);
                    }
    
                    // convert index-based offsets to ChangeOffsets in ChangeProposals
                    changeProposals = SampleHelper.ConvertOffsets(
                        projectFullName,
                        fileFullPath,
                        changes,
                        defaultChecked);
                }
                return changeProposals;
            }
    

    Because the refactoring target is not a Transact-SQL (T-SQL) script or a schema object, this method does not use types or methods from either Microsoft.Data.Schema.ScriptDom or Microsoft.Data.Schema.SchemaModel. This refactoring target provides a find and replace implementation for text files, because symbols in text files do not have schema information available.

  12. On the File menu, click Save RenameTextContributor.cs.

    Next, you will configure and build the assembly.

To sign and build the assembly

  1. On the Project menu, click RenameTextContributor Properties.

  2. Click the Signing tab.

  3. Click Sign the assembly.

  4. In Choose a strong name key file, click <New>.

  5. In the Create Strong Name Key dialog box, in Key file name, type MyRefKey.

  6. (optional) You can specify a password for your strong name key file.

  7. Click OK.

  8. On the File menu, click Save All.

  9. On the Build menu, click Build Solution.

    Next, you must install and register the assembly so that it will appear as an available test condition.

To install the RenameTextContributor assembly

  1. Create a folder named MyExtensions in the [Program Files]\Microsoft Visual Studio 9.0\VSTSDB\Extensions folder.

  2. Copy your signed assembly (RenameTextContributor.dll) to the [Program Files]\Microsoft Visual Studio 9.0\VSTSDB\Extensions\MyExtensions folder.

    Note

    We recommend that you do not copy your XML files directly into the [Program Files]\Microsoft Visual Studio 9.0\VSTSDB\Extensions folder. If you use a subfolder instead, you will prevent accidental changes to the other files provided with Database Edition.

    Next, you must register your assembly, a type of feature extension, so that it will appear in Database Edition.

To register the RenameTextContributor assembly

  1. On the View menu, click Other Windows, and then click Command Window to open the Command window.

  2. In the Command window, type the following code. For FilePath, substitute the path and file name of your compiled .dll file. Include the quotation marks around the path and file name.

    Note

    By default, the path of your compiled .dll file is YourSolutionPath\bin\Debug or YourSolutionPath\bin\Release.

    ? System.Reflection.Assembly.LoadFrom(@"FilePath").FullName
    
    ? System.Reflection.Assembly.LoadFrom("FilePath").FullName
    
  3. Press Enter.

  4. Copy the resultant line to the Clipboard. The line should resemble the following:

    "GeneratorAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=nnnnnnnnnnnnnnnn"
    
  5. Open a plain-text editor, such as Notepad.

  6. Provide the following information, specifying your own assembly name, public key token, and extension type:

    <?xml version="1.0" encoding="utf-8" ?> 
    <extensions assembly="" version="1" xmlns="urn:Microsoft.Data.Schema.Extensions" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:Microsoft.Data.Schema.Extensions Microsoft.Data.Schema.Extensions.xsd">
      <extension type="MySamples.Refactoring.RenameReferenceTextContributor" 
    assembly="RenameTextContributor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=<enter key here>" enabled="true" />
    </extensions>
    

    Register the class that inherits from RefactoringContributor.

  7. Save the file as RenameTextContributor.extensions.xml in the [Program Files]\Microsoft Visual Studio 9.0\VSTSDB\Extensions\MyExtensions folder.

  8. Close Visual Studio.

    Next, you will create a very simple database project to test your new refactoring type.

Testing the New Refactoring Contributor

To create a database project

  1. On the File menu, point to New, and click Project.

  2. In the Project types list, expand the Database Projects node, and click SQL Server 2005.

  3. In the Templates list, click SQL Server 2005 Database Project.

  4. Click OK to accept the default project name and create the project.

    An empty database project is created.

To add a table with a primary key

  1. On the View menu, click Database Schema View.

  2. In Schema View, expand the Schemas node, expand the dbo node, right-click the Tables node, point to Add, and click Table.

  3. In the Add New Item dialog box, in Name, type employee.

    Note

    You are intentionally using a lowercase letter to begin the table name.

  4. Click OK.

  5. Expand the Tables node, right-click the employee node, point to Add, and click Primary Key.

  6. In the Add New Item dialog box, in Name, type PK_Employee_column_1.

  7. Click OK.

    Next, you will add a text file to your database project that contains a reference to the employee table.

To add a text file that contains the table name

  1. In Solution Explorer, right-click the database project node, point to Add, and click New Item.

  2. In the Add New Item dialog box, in the Categories list, click Visual Studio Templates.

  3. In the Templates list, click Text file.

  4. In Name, type SampleText1.txt.

  5. In the code editor, add the following text:

    This is documentation for the employee table.
    Any changes made to the employee table name should also be reflected in this text file.
    
  6. On the File menu, click Save SampleText1.txt.

    Next, you will use the new refactoring type to change the table name and all references to it.

To use the new refactoring contributor to update the table name

  1. In Schema View, right-click the employee table node, point to Refactor, and click Rename.

  2. In the Rename dialog box, in New name, type [Person].

  3. In the Preview Changes dialog box, scroll through the change groups until you see the Text Files group.

    Both instances of employee in the text file will be listed. You defined the classes that enable this new type of refactoring in this walkthrough. Select the check box next to each change.

  4. Click Apply.

    The table name is updated to be Person, both in Schema View and in the contents of the SampleText1.txt file.

Next Steps

You can create additional refactoring targets, or you can create new types of refactoring, to reduce the effort associated with making repetitive changes to your database project.

See Also

Tasks

Walkthrough: Creating a New Type of Database Refactoring to Change Casing

Concepts

Create Custom Database Refactoring Types or Targets

Refactor Database Code and Data

Terminology Overview of Database Edition