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:
Create a new assembly that contains the classes for a custom refactoring target.
Install and register the assembly, so that the refactoring target is available in Database Edition.
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
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.
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
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
In Solution Explorer, rename Class1.cs to SampleHelper.cs.
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.
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
On the File menu, click Save SampleHelper.cs.
Add a class named RawChangeInfo to the project.
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
On the File menu, click Save SampleHelper.cs.
Next, you will define the RenameReferenceTextContributor class.
To define the RenameReferenceTextContributor class
Add a class named RenameReferenceTextContributor to your project.
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
Change the namespace to MySamples.Refactoring:
namespace MySamples.Refactoring
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.
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.
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.
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
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.
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
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); }
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.
On the File menu, click Save RenameTextContributor.cs.
Next, you will configure and build the assembly.
To sign and build the assembly
On the Project menu, click RenameTextContributor Properties.
Click the Signing tab.
Click Sign the assembly.
In Choose a strong name key file, click <New>.
In the Create Strong Name Key dialog box, in Key file name, type MyRefKey.
(optional) You can specify a password for your strong name key file.
Click OK.
On the File menu, click Save All.
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
Create a folder named MyExtensions in the [Program Files]\Microsoft Visual Studio 9.0\VSTSDB\Extensions folder.
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
On the View menu, click Other Windows, and then click Command Window to open the Command window.
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
Press Enter.
Copy the resultant line to the Clipboard. The line should resemble the following:
"GeneratorAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=nnnnnnnnnnnnnnnn"
Open a plain-text editor, such as Notepad.
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.
Save the file as RenameTextContributor.extensions.xml in the [Program Files]\Microsoft Visual Studio 9.0\VSTSDB\Extensions\MyExtensions folder.
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
On the File menu, point to New, and click Project.
In the Project types list, expand the Database Projects node, and click SQL Server 2005.
In the Templates list, click SQL Server 2005 Database Project.
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
On the View menu, click Database Schema View.
In Schema View, expand the Schemas node, expand the dbo node, right-click the Tables node, point to Add, and click Table.
In the Add New Item dialog box, in Name, type employee.
Note
You are intentionally using a lowercase letter to begin the table name.
Click OK.
Expand the Tables node, right-click the employee node, point to Add, and click Primary Key.
In the Add New Item dialog box, in Name, type PK_Employee_column_1.
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
In Solution Explorer, right-click the database project node, point to Add, and click New Item.
In the Add New Item dialog box, in the Categories list, click Visual Studio Templates.
In the Templates list, click Text file.
In Name, type SampleText1.txt.
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.
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
In Schema View, right-click the employee table node, point to Refactor, and click Rename.
In the Rename dialog box, in New name, type [Person].
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.
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