연습: 텍스트 파일에서 작동하도록 데이터베이스 이름 바꾸기 리팩터링 확장

이 단계별 항목에서는 이름 바꾸기 리팩터링의 새 참가자를 만들고, 설치, 등록 및 테스트합니다. 이 리팩터링 대상은 데이터베이스 리팩터링을 통해 데이터베이스 프로젝트의 텍스트 파일에 포함된 데이터베이스 개체에 대한 참조 이름을 바꿀 수 있도록 Visual Studio Premium 또는 Visual Studio Ultimate의 기능을 확장합니다.

기존 리팩터링 형식에 새 리팩터링 참가자를 추가하는 경우 기존 참가자 입력 클래스를 사용해야 합니다.

이 연습에서는 다음 작업을 수행합니다.

  1. 사용자 지정 리팩터링 대상에 대한 클래스가 포함된 새 어셈블리를 만듭니다.

  2. Visual Studio Premium 또는 Visual Studio Ultimate에서 리팩터링 대상을 사용할 수 있도록 어셈블리를 설치하고 등록합니다.

  3. 리팩터링 대상이 예상대로 작동하는지 테스트할 간단한 데이터베이스 프로젝트를 만듭니다.

사전 요구 사항

이 연습을 완료하려면 다음 구성 요소가 필요합니다.

  • Visual Studio 2010 Premium 또는 Visual Studio 2010 Ultimate이 설치되어 있어야 합니다.

  • 또한 Visual Studio 2010 SDK가 컴퓨터에 설치되어 있어야 합니다. 이 키트는 Microsoft 웹 사이트의 Visual Studio 2010 SDK 페이지에서 다운로드할 수 있습니다.

사용자 지정 리팩터링 대상을 사용하여 어셈블리 만들기

이름 바꾸기 리팩터링이 텍스트 파일에서 작업할 수 있도록 하는 사용자 지정 리팩터링 대상을 만들려면 새 RefactoringContributor를 제공하는 클래스를 구현해야 합니다.

  • RenameReferenceTextContributorContributor - 이 클래스는 이름이 바뀐 기호에 대한 변경 제안 목록을 빌드합니다. 데이터베이스 프로젝트에 있는 텍스트 파일에 포함된 각 참조에 대해 변경 제안이 있습니다.

이 클래스를 만들기 전에 클래스 라이브러리를 만들고 필요한 참조를 추가한 다음 이 연습의 뒷부분에서 작성할 코드를 간소화하는 도우미 코드를 추가합니다.

클래스 라이브러리 및 도우미 코드를 만들려면

  1. C# 클래스 라이브러리 프로젝트를 만들고 이름을 RenameTextContributor.csproj로 지정합니다.

  2. 다음 .NET 어셈블리에 대한 참조를 추가합니다.

    • Microsoft.Data.Schema

    • Microsoft.Data.Schema.ScriptDom

    • Microsoft.Data.Schema.ScriptDom.sql

    • Microsoft.Data.Schema.Sql

  3. %Program Files%\Microsoft Visual Studio 10.0\VSTSDB 폴더에 있는 다음 어셈블리에 대한 참조를 추가합니다.

    • Microsoft.Data.Schema.Tools.Sql.dll

    • Microsoft.VisualStudio.Data.Schema.Package.dll

    • Microsoft.VisualStudio.Data.Schema.Package.Sql.dll

  4. Visual Studio 2010 SDK(소프트웨어 개발 키트)에서 다음 어셈블리에 대한 참조를 추가합니다.

    • Microsoft.VisualStudio.OLE.Interop.dll

    • Microsoft.VisualStudio.Shell.10.0.dll

    • Microsoft.VisualStudio.TextManager.Interop.dll

    • Microsoft.VisualStudio.Shell.Interop.dll

    • Microsoft.VisualStudio.Shell.Interop.8.0.dll

    • Microsoft.VisualStudio.Shell.Interop.9.0.dll

    • Microsoft.VisualStudio.Shell.Interop.10.0.dll

  5. 솔루션 탐색기에서 Class1.cs의 이름을 SampleHelper.cs로 바꿉니다.

  6. SampleHelper.cs를 두 번 클릭하여 코드 편집기에서 엽니다.

    참고

    이 도우미 클래스는 사용자 지정 리팩터링 형식 연습에서 사용된 도우미 텍스트와 같습니다. 해당 프로젝트의 소스 코드를 새 프로젝트에 복사하여 시간을 절약할 수 있습니다.

  7. 코드 편집기의 내용을 다음 코드로 바꿉니다.

    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.IO;
    using System.Runtime.InteropServices;
    using Microsoft.Data.Schema.SchemaModel;
    using Microsoft.Data.Schema.ScriptDom.Sql;
    using Microsoft.VisualStudio;
    using Microsoft.VisualStudio.Data.Schema.Package.Refactoring;
    using Microsoft.VisualStudio.Data.Schema.Package.UI;
    using Microsoft.VisualStudio.Shell.Interop;
    using Microsoft.VisualStudio.TextManager.Interop;
    
    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 IVsTextLines GetTextLines(string fullPathFileName)
            {
                System.IServiceProvider serviceProvider = DataPackage.Instance;
                IVsTextLines textLines = null;
                IVsRunningDocumentTable rdt = (IVsRunningDocumentTable)serviceProvider.GetService(typeof(SVsRunningDocumentTable));
    
                if (rdt != null)
                {
                    IVsHierarchy ppHier = null;
                    uint pitemid, pdwCookie;
                    IntPtr ppunkDocData = IntPtr.Zero;
                    try
                    {
                        rdt.FindAndLockDocument((uint)(_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;
            }
        }
    }
    
  8. 파일 메뉴에서 SampleHelper.cs 저장을 클릭합니다.

  9. RawChangeInfo라는 클래스를 프로젝트에 추가합니다.

  10. 코드 편집기에서 코드를 다음과 같이 업데이트합니다.

    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)
            {
                _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;
                }
            }
        }
    }
    
  11. 파일 메뉴에서 RawChangeInfo.cs 저장을 클릭합니다.

    다음으로, RenameReferenceTextContributor 클래스를 정의합니다.

RenameReferenceTextContributor 클래스를 정의하려면

  1. RenameReferenceTextContributor라는 클래스를 프로젝트에 추가합니다.

  2. 코드 편집기에서 using 문을 다음과 같이 업데이트합니다.

    using System;
    using System.Collections.Generic;
    using Microsoft.Data.Schema.Extensibility;
    using Microsoft.Data.Schema.Sql;
    using Microsoft.VisualStudio;
    using Microsoft.VisualStudio.Data.Schema.Package.Refactoring;
    using Microsoft.VisualStudio.Data.Schema.Package.Sql.Refactoring;
    
  3. 네임스페이스를 MySamples.Refactoring으로 변경합니다.

    namespace MySamples.Refactoring
    
  4. 클래스 정의를 다음과 같이 업데이트합니다.

        [DatabaseSchemaProviderCompatibility(typeof(SqlDatabaseSchemaProvider))]
        internal class RenameReferenceTextContributor : RefactoringContributor<RenameReferenceContributorInput>
        {
        }
    

    SqlDatabaseSchemaProvider에서 파생된 모든 데이터베이스 스키마 공급자와 이 참가자가 호환되도록 선언하는 특성을 지정합니다. 클래스가 RenameReferenceContributorInput에 대한 RefactoringContributor에서 상속해야 합니다.

  5. 추가 상수와 private 멤버 변수를 정의합니다.

            #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
    

    이러한 상수는 미리 보기 창에 표시될 정보를 제공합니다. 추가 멤버는 미리 보기 그룹과 변경되는 텍스트 파일 목록을 추적하는 데 사용됩니다.

  6. 클래스 생성자를 추가합니다.

            #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
    

    이 생성자는 새 미리 보기 그룹을 만들고 해당 속성을 초기화하여 모델 요소를 초기화합니다. 특정 파일 확장명에 대해 미리 보기 창에 표시할 아이콘을 여기서 등록할 수도 있습니다. 텍스트 파일의 경우 구문 강조 표시가 없으므로 텍스트 파일에 대한 언어 서비스는 등록하지 않습니다.

  7. 이 참가자를 만들 때 생성된 그룹을 반환하도록 PreviewGroup 속성을 재정의합니다.

            /// <summary>
            /// Preview group for text files
            /// </summary>
            public override RefactoringPreviewGroup PreviewGroup
            {
                get
                {
                    return _textPreviewGroup;
                }
                set
                {
                    _textPreviewGroup = value;
                }
            }
    
  8. 변경 제안 목록을 반환하도록 ContributeChanges(Boolean) 메서드를 재정의합니다.

            /// <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);
            }
    

    이 메서드는 GetAllChangesForAllTextFiles 메서드를 호출하여 대부분의 작업을 수행합니다.

  9. 프로젝트에 포함된 텍스트 파일 목록을 반복하고 각 파일의 변경 내용을 가져온 다음 변경 제안 목록에 집계하는 GetChangesForAllTextFiles 메서드를 추가합니다.

            /// <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);
            }
    
  10. 하나의 텍스트 파일에 포함된 변경 제안 목록을 반환하는 GetChangesForOneTextFileMethod 메서드를 구현합니다.

            /// <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;
            }
    

    리팩터링 대상이 Transact-SQL 스크립트나 스키마 개체가 아니므로 이 메서드는 Microsoft.Data.Schema.ScriptDom 또는 Microsoft.Data.Schema.Sql.SchemaModel의 형식이나 메서드를 사용하지 않습니다. 텍스트 파일의 기호에 사용할 수 있는 스키마 정보가 없으므로 이 리팩터링 대상은 텍스트 파일에 대해 찾기 및 바꾸기 구현을 제공합니다.

  11. 파일 메뉴에서 RenameTextContributor.cs 저장을 클릭합니다.

    다음으로, 어셈블리를 구성하고 빌드합니다.

어셈블리에 서명하고 빌드하려면

  1. 프로젝트 메뉴에서 RenameTextContributor 속성을 클릭합니다.

  2. 서명 탭을 클릭합니다.

  3. 어셈블리 서명을 클릭합니다.

  4. 강력한 이름 키 파일 선택에서 **<새로 만들기>**를 클릭합니다.

  5. 강력한 이름 키 만들기 대화 상자의 키 파일 이름에 MyRefKey를 입력합니다.

  6. (옵션) 강력한 이름 키 파일의 암호를 지정할 수 있습니다.

  7. 확인을 클릭합니다.

  8. 파일 메뉴에서 모두 저장을 클릭합니다.

  9. 빌드 메뉴에서 솔루션 빌드를 클릭합니다.

    다음으로, 어셈블리가 사용 가능한 테스트 조건으로 표시되도록 어셈블리를 설치하고 등록해야 합니다.

RenameTextContributor 어셈블리를 설치하려면

  1. %Program Files%\Microsoft Visual Studio 10.0\VSTSDB\Extensions 폴더에 MyExtensions라는 폴더를 만듭니다.

  2. 서명된 어셈블리(RenameTextContributor.dll)를 %Program Files%\Microsoft Visual Studio 10.0\VSTSDB\Extensions\MyExtensions 폴더에 복사합니다.

    참고

    XML 파일을 %Program Files%\Microsoft Visual Studio 10.0\VSTSDB\Extensions 폴더에 바로 복사하지 않는 것이 좋습니다. 대신 하위 폴더를 사용하면 Visual Studio에서 제공된 다른 파일을 실수로 변경하는 것을 방지할 수 있습니다.

    다음에는 기능 확장의 한 형식인 어셈블리가 Visual Studio에 표시되도록 등록해야 합니다.

RenameTextContributor 어셈블리를 등록하려면

  1. 보기 메뉴에서 다른 창, 명령 창을 차례로 클릭하여 명령 창을 엽니다.

  2. 명령 창에서 다음 코드를 입력합니다. FilePath 대신 컴파일된 .dll 파일의 경로 및 파일 이름을 입력합니다. 이때 경로 및 파일 이름을 따옴표로 묶습니다.

    참고

    기본적으로 컴파일된 .dll 파일의 경로는 YourSolutionPath\bin\Debug 또는 YourSolutionPath\bin\Release입니다.

    ? System.Reflection.Assembly.LoadFrom("FilePath").FullName
    
    ? System.Reflection.Assembly.LoadFrom(@"FilePath").FullName
    
  3. Enter 키를 누릅니다.

  4. 결과 줄을 클립보드로 복사합니다. 이 줄은 다음과 같습니다.

    "RenameTextContributor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=nnnnnnnnnnnnnnnn"
    
  5. 메모장과 같은 일반 텍스트 편집기를 엽니다.

  6. 사용자 어셈블리 이름, 공개 키 토큰 및 확장 형식을 지정하여 다음 정보를 제공합니다.

    <?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>
    

    RefactoringContributor에서 상속된 클래스를 등록합니다.

  7. %Program Files%\Microsoft Visual Studio 10.0\VSTSDB\Extensions\MyExtensions 폴더에 RenameTextContributor.extensions.xml로 파일을 저장합니다.

  8. Visual Studio를 닫습니다.

    다음에는 새 리팩터링 형식을 테스트할 간단한 데이터베이스 프로젝트를 만듭니다.

새 리팩터링 참가자 테스트

데이터베이스 프로젝트를 만들려면

  1. 파일 메뉴에서 새로 만들기를 가리킨 다음 프로젝트를 클릭합니다.

  2. 설치된 템플릿에서 데이터베이스 노드를 확장하고 SQL Server 노드를 클릭합니다.

  3. 템플릿 목록에서 SQL Server 2008 데이터베이스 프로젝트를 클릭합니다.

  4. 확인을 클릭하여 기본 프로젝트 이름을 적용하고 프로젝트를 만듭니다.

    빈 데이터베이스 프로젝트가 만들어집니다.

기본 키가 있는 테이블을 추가하려면

  1. 보기 메뉴에서 데이터베이스 스키마 뷰를 클릭합니다.

  2. 스키마 뷰에서 Schemas 노드, dbo 노드를 차례로 확장하고 테이블 노드를 마우스 오른쪽 단추로 클릭한 다음 추가를 가리키고 테이블을 클릭합니다.

  3. 새 항목 추가 대화 상자의 이름에 employee를 입력합니다.

    참고

    의도적으로 테이블 이름을 소문자로 시작합니다.

  4. 확인을 클릭합니다.

  5. 테이블 노드를 확장하고 employee 노드를 마우스 오른쪽 단추로 클릭한 다음 추가를 가리키고 기본 키를 클릭합니다.

  6. 새 항목 추가 대화 상자의 이름에 PK_Employee_column_1을 입력합니다.

  7. 확인을 클릭합니다.

    다음으로, employee 테이블에 대한 참조가 포함된 텍스트 파일을 데이터베이스 프로젝트에 추가합니다.

테이블 이름이 포함된 텍스트 파일을 추가하려면

  1. 솔루션 탐색기에서 데이터베이스 프로젝트 노드를 마우스 오른쪽 단추로 클릭하고 추가를 가리킨 다음 새 항목을 클릭합니다.

  2. 새 항목 추가 대화 상자의 범주 목록에서 Visual Studio 템플릿을 클릭합니다.

  3. 템플릿 목록에서 텍스트 파일을 클릭합니다.

  4. 이름에 SampleText1.txt를 입력합니다.

  5. 코드 편집기에서 다음 텍스트를 추가합니다.

    This is documentation for the employee table.
    Any changes made to the employee table name should also be reflected in this text file.
    
  6. 파일 메뉴에서 SampleText1.txt 저장을 클릭합니다.

    다음으로, 새 리팩터링 형식을 사용하여 테이블 이름과 모든 참조를 변경합니다.

새 리팩터링 참가자를 사용하여 테이블 이름을 업데이트하려면

  1. 스키마 뷰에서 employee 테이블 노드를 마우스 오른쪽 단추로 클릭하고 리팩터링을 가리킨 다음 이름 바꾸기를 클릭합니다.

  2. 이름 바꾸기 대화 상자의 새 이름에 [Person]을 입력합니다.

  3. 변경 내용 미리 보기 대화 상자에서 텍스트 파일 그룹이 표시될 때까지 변경 그룹을 스크롤합니다.

    텍스트 파일에 있는 employee 인스턴스가 모두 표시됩니다. 이 연습에서 새 리팩터링 형식을 사용하도록 설정하는 클래스를 정의했습니다. 각 변경 옆에 있는 확인란을 선택합니다.

  4. 적용을 클릭합니다.

    테이블 이름이 스키마 뷰와 SampleText1.txt 파일 내용에서 모두 Person으로 업데이트됩니다.

다음 단계

추가 리팩터링 대상을 만들거나, 데이터베이스 프로젝트를 반복해서 변경하는 수고를 줄이기 위해 새 리팩터링 형식을 만들 수 있습니다.

참고 항목

작업

연습: 새로운 데이터베이스 리팩터링 형식을 만들어 대/소문자 변경

개념

사용자 지정 데이터베이스 리팩터링 형식 또는 대상 만들기

데이터베이스 코드 및 데이터 리팩터링