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

이 단계별 항목에서는 새로운 데이터베이스 리팩터링 형식을 만들고, 설치, 등록 및 테스트합니다. 이 리팩터링 작업에서는 지정된 개체 이름의 첫 번째 문자를 대문자로 변환하고 업데이트된 이름에 대한 모든 참조를 업데이트합니다.

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

  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 페이지에서 다운로드할 수 있습니다.

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

개체 이름의 첫 번째 문자를 대문자로 변환한 다음 해당 개체에 대한 모든 참조를 업데이트할 수 있는 사용자 지정 리팩터링 형식을 만들려면 다음 여섯 개의 클래스를 구현해야 합니다.

  • CasingRefactoringCommand — 이 클래스는 리팩터링 메뉴에 표시할 명령 이름을 제공하고, 리팩터링 형식을 사용할 수 있는 모델 요소를 지정하고, 사용자가 명령을 클릭하면 리팩터링 작업을 호출합니다.

  • CasingRefactoringOperation — 이 클래스는 리팩터링 작업이 미리 보기 창과 상호 작용하는 방식을 지정하고, 작업을 설명하는 속성을 지정하고, CasingContributorInput 클래스를 만듭니다.

  • CasingContributorInput — 이 클래스는 입력 데이터를 CasingSymbolContributor 클래스에 저장합니다.

  • CasingSymbolContributor — 이 클래스는 이름이 바뀐 기호에 대한 변경 제안 목록을 빌드하며, 또한 CasingReferenceContributorInput 클래스를 만들어 이름을 바꿀 개체에 대한 참조를 업데이트하는 작업을 처리합니다.

  • CasingReferenceContributorInput — 이 클래스는 입력 데이터를 CasingReferenceContributor 클래스에 저장합니다.

  • CasingReferenceContributor — 이 클래스는 이름이 바뀐 기호에 대한 참조 업데이트와 연결된 변경 제안의 목록을 빌드합니다.

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

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

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

  2. 다음 클래스 라이브러리에 대한 참조를 추가합니다.

    • Microsoft.Data.Schema.dll

    • Microsoft.Data.Schema.ScriptDom.dll

    • Microsoft.Data.Schema.ScriptDom.Sql.dll

    • Microsoft.Data.Schema.Sql.dll

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

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

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

    • Microsoft.VisualStudio.OLE.Interop.dll

    • Microsoft.VisualStudio.Shell.10.0.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

    • Microsoft.VisualStudio.TextManager.Interop.dll

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

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

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

    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. e.g. 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;
            }
        }
    }
    
  7. 파일 메뉴에서 SampleHelper.cs 저장을 클릭합니다.

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

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

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

    다음에는 CasingRefactoringCommand 클래스를 정의합니다.

CasingRefactoringCommand 클래스를 정의하려면

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

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

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

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

        [DatabaseSchemaProviderCompatibility(typeof(SqlDatabaseSchemaProvider))]
        internal class CasingRefactorCommand : RefactoringSchemaViewNodeCommand
        {
    }
    

    이 특성은 이 리팩터링 형식과 호환되는 데이터베이스 스키마 공급자를 지정하는 데 사용됩니다. 이 예제의 경우 새 리팩터링 형식은 SqlDatabaseSchemaProvider에서 파생된 공급자와 함께 사용됩니다. 클래스는 RefactoringSchemaViewNodeCommand를 상속합니다. 이 기본 클래스에서 상속하면 스키마 뷰의 지정한 노드에서 해당 리팩터링 형식을 사용할 수 있습니다. 파일 노드와 프로젝트 노드에서 작동하는 다른 리팩터링 형식도 정의할 수 있습니다.

  5. 다음에는 클래스에 다음과 같은 재정의 메서드를 추가합니다.

    public override void Execute(IDatabaseProjectNode currentProject, IModelElement selectedModelElement)
            {
                CasingRefactorOperation operation = new CasingRefactorOperation(currentProject, selectedModelElement);
                operation.DoOperation();
            }
    

    이 메서드는 사용자가 스키마 뷰에서 이 리팩터링 명령을 적용할 때의 동작을 제공합니다.

  6. 다음에는 클래스에 다음과 같은 재정의 메서드를 추가합니다.

            public override QueryStatusResult QueryStatus(IModelElement selectedModelElement)
            {
                if (selectedModelElement is IDatabaseColumnSource
                    || selectedModelElement is ISqlSimpleColumn
                    || selectedModelElement is ISqlProcedure
                    || selectedModelElement is ISqlFunction 
                    || selectedModelElement is ISqlIndex
                    || selectedModelElement is ISqlConstraint)
                {
                    return QueryStatusResult.Enabled;
                }
                else
                {
                    return QueryStatusResult.Invisible;
                }
            }
    

    이 메서드는 스키마 뷰에서 이 리팩터링 명령을 사용할 수 있는 노드를 결정합니다.

  7. 마지막으로, 클래스에 다음과 같은 재정의 메서드를 추가합니다.

            public override string Text
            {
                get { return "Make First Letter Uppercase"; }
            }
    

    이 메서드는 리팩터링 메뉴에 표시되는 리팩터링 명령 이름을 제공합니다.

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

    다음에는 CasingRefactoringOperation 클래스를 정의합니다.

CasingRefactoringOperation 클래스를 정의하려면

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

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

    using System;
    using System.Diagnostics;
    using System.Globalization;
    using Microsoft.Data.Schema.SchemaModel;
    using Microsoft.Data.Schema.Sql.SchemaModel;
    using Microsoft.VisualStudio.Data.Schema.Package.Project;
    using Microsoft.VisualStudio.Data.Schema.Package.Refactoring;
    
  3. 네임스페이스를 MySamples.Refactoring으로 변경합니다.

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

        internal class CasingRefactorOperation : RefactoringOperation
        {
    }
    

    이 클래스는 RefactoringOperation에서 상속해야 합니다.

  5. 다음 상수 및 멤버 변수 선언을 클래스에 추가합니다.

            #region Const
            private const string CasingRefactorOperationName = @"Make First Letter Uppercase";
            private const string OperationDescription = @"Make First Letter Uppercase";
            private const string OperationTextViewDescription = @"Preview changes:";
            private const string PreviewDialogTitle = @"Preview Changes - {0}";
            private const string ConfirmButtonText = @"&Apply";
            private const string CasingUndoDescription = @"Make first letter uppercase - {0}";
            #endregion
    
            private string _operationName;
            private PreviewWindowInfo _previewWindowInfo;
            private ISqlModelElement _modelElement;
    

    이러한 private 상수는 미리 보기 창에 표시될 이 작업에 대한 정보를 제공합니다.

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

    public CasingRefactorOperation(IDatabaseProjectNode currentProject, IModelElement selectedModelElement)
                : base(currentProject)
            {
                _operationName = CasingRefactorOperationName;
    
                if (selectedModelElement as ISqlModelElement != null)
                {
                    _modelElement = selectedModelElement as ISqlModelElement;
                }
            }
    

    이 생성자는 작업 이름과 모델 요소가 지정된 경우 이러한 항목을 초기화합니다.

  7. 사용자가 해당 리팩터링 형식을 적용할 때 미리 보기 창에 표시될 값을 가져오도록 PreviewWindowInfo 속성을 재정의합니다.

            /// <summary>
            /// Preview dialog information for this RenameRefactorOperation.
            /// </summary>
            protected override PreviewWindowInfo PreviewWindowInfo
            {
                get
                {
                    if (_previewWindowInfo == null)
                    {
                        _previewWindowInfo = new PreviewWindowInfo();
                        _previewWindowInfo.ConfirmButtonText = ConfirmButtonText;
                        _previewWindowInfo.Description = OperationDescription;
                        _previewWindowInfo.HelpContext = String.Empty;
                        _previewWindowInfo.TextViewDescription = OperationTextViewDescription;
                        _previewWindowInfo.Title = string.Format(CultureInfo.CurrentCulture,PreviewDialogTitle, CasingRefactorOperationName);
                    }
                    return _previewWindowInfo;
                }
            }
    
  8. 추가 속성 정의를 제공합니다.

            protected override string OperationName
            {
                get
                {
                    return _operationName;
                }
            }
    
            /// <summary>
            /// Undo Description used in undo stack
            /// </summary>
            protected override string UndoDescription
            {
                get
                {
                    return string.Format(CultureInfo.CurrentCulture,
                        CasingUndoDescription, SampleHelper.GetModelElementName(this.ModelElement));
                }
            }
    
            /// <summary>
            /// SchemaIdentifier of currently selected schema object
            /// </summary>
            public ISqlModelElement ModelElement
            {
                get
                {
                    return _modelElement;
                }
                set
                {
                    _modelElement = value;
                }
            }
    
  9. 마지막으로, OnGetContributorInput 메서드를 재정의합니다.

            /// <summary>
            /// According to different selected node, create different CasingContributorInput
            /// </summary>
            /// <returns></returns>
            protected override ContributorInput OnGetContributorInput()
            {
                ContributorInput input = null;
                SqlSchemaModel dataSchemaModel = this.CurrentDataSchemaModel as SqlSchemaModel;
    
                // You might choose to throw an exception here if 
                // schemaModel is null.
                Debug.Assert(dataSchemaModel != null, "DataSchemaModel is null.");
    
                // create contributor input used in this operation
                input = new CasingContributorInput(this.ModelElement);
                return input;
            }
    

    이 메서드는 이 리팩터링 형식의 리팩터링 참가자에 전달되는 ContributorInput을 만듭니다.

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

    다음에는 CasingContributorInput 클래스를 정의합니다.

CasingContributorInput 클래스를 정의하려면

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

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

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

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

        internal class CasingContributorInput: ContributorInput
        {
        }
    

    이 클래스는 ContributorInput에서 상속해야 합니다.

  5. 추가 private 멤버 변수 하나를 정의합니다.

            private ISqlModelElement _modelElement;
    

    이 멤버는 작업을 수행할 모델 요소를 추적하는 데 사용됩니다.

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

            public CasingContributorInput(ISqlModelElement modelElement)
            {
                _modelElement = modelElement;
            }
    

    이 생성자는 모델 요소를 초기화합니다.

  7. 모델 요소에 대한 읽기 전용 public 속성을 추가합니다.

            /// <summary>
            /// Selected model element
            /// </summary>
            public ISqlModelElement ModelElement
            {
                get 
    
                {
                    return _modelElement;
                }
            }
    
  8. Override Equals 메서드는 두 개의 CasingContributorInput 개체가 동일한지를 확인하는 비교 기능을 제공합니다.

            /// <summary>
            /// Override Equals
            /// </summary>
            /// <param name="obj"></param>
            /// <returns></returns>
            public override bool Equals(object obj)
            {
                CasingContributorInput other = obj as CasingContributorInput;
                return _modelElement.Equals(other.ModelElement);
            }
    

    이 참가자의 경우 입력이 동일한 모델 요소에 적용되면 입력이 동일한 것으로 처리됩니다.

  9. GetHashCode 메서드를 재정의합니다.

            /// <summary>
            /// Override GetHashCode
            /// </summary>
            /// <returns></returns>
            public override int GetHashCode()
            {
                Int32 hash = _modelElement.GetHashCode();
                return hash;
            }
    
  10. 파일 메뉴에서 CasingContributorInput.cs 저장을 클릭합니다.

    다음에는 CasingSymbolContributor 클래스를 정의합니다.

CasingSymbolContributor 클래스를 정의하려면

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

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

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

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

        [DatabaseSchemaProviderCompatibility(typeof(SqlDatabaseSchemaProvider))]
        internal class CasingSymbolContributor : RefactoringContributor<CasingContributorInput>
        {
        }
    

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

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

            #region Const
            private const string PreviewGroupFriendlyName = @"Schema Objects";
            private const string PreviewDescription = @"Uppercasing the first letter of schema object name and all references to this schema object.";
            private const string PreviewWarning = @"Changing casing of the schema object name may cause errors and warnings when your project is using case-sensitive collations. ";
            #endregion
    
            private RefactoringPreviewGroup _previewGroup;
    

    이러한 상수는 미리 보기 창에 표시될 정보를 제공합니다. 추가 멤버는 미리 보기 그룹을 추적하는 데 사용됩니다.

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

            #region ctor
            public CasingSymbolContributor()
            {
                _previewGroup = new RefactoringPreviewGroup(PreviewGroupFriendlyName);
                _previewGroup.Description = PreviewDescription;
                _previewGroup.WarningMessage = PreviewWarning;
                _previewGroup.EnableChangeGroupUncheck = false;
                _previewGroup.IncludeInCurrentProject = true;
    
                // the default icon will be used if do not register and icon for your file extensions
                //RefactoringPreviewGroup.RegisterIcon("sql", "SqlFileNode.ico");
                //RefactoringPreviewGroup.RegisterIcon(".dbproj", "DatabaseProjectNode.ico");
    
                // For some contributors, you might register a
                // language service here.
                //_previewGroup.RegisterLanguageService(".sql", ); 
    
                base.RegisterGeneratedInputType(typeof(CasingReferenceContributorInput));
            }
            #endregion
    

    이 생성자는 새 미리 보기 그룹을 만들고 해당 속성을 초기화하여 모델 요소를 초기화합니다. 또한 이 단계에서 특정 파일 확장명에 대해 미리 보기 창에 표시될 아이콘을 등록하고, 지정한 확장명을 갖는 파일에 구문 색 지정 기능을 제공하는 데 사용되는 언어 서비스도 등록할 수 있습니다.

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

            #region overrides
            /// <summary>
            /// Preview group for schema object files
            /// </summary>
            public override RefactoringPreviewGroup PreviewGroup
            {
                get
                {
                    return _previewGroup;
                }
                set
                {
                    _previewGroup = 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(CasingContributorInput input)
            {
                CasingContributorInput casingInput = input as CasingContributorInput;
                if (casingInput == null)
                {
                    throw new ArgumentNullException("input");
                }
    
                string projectFullName;
                casingInput.RefactoringOperation.CurrentProjectHierarchy.GetCanonicalName(VSConstants.VSITEMID_ROOT, out projectFullName);
    
                Tuple<IList<ChangeProposal>, IList<ContributorInput>> changes = GetChangesFromCurrentSymbolScript(
                        projectFullName,
                        casingInput,
                        casingInput.ModelElement,
                        true);
    
                return changes;
            }
    
            #endregion
    
  9. 다음에는 다른 형식의 ContributorInput을 만듭니다.

            /// <summary>
            /// Create a CasingReferenceContributorInput according to passed in CasingContributorInput
            /// </summary>
            /// <param name="orginalInput"></param>
            /// <returns></returns>
            internal ContributorInput CreateCasingReferenceInput(ContributorInput orginalInput)
            {
                CasingContributorInput casingInput = orginalInput as CasingContributorInput;
                Debug.Assert(casingInput != null, "casingInput is null");
    
                CasingReferenceContributorInput referenceInput = new CasingReferenceContributorInput(casingInput.ModelElement);
                referenceInput.SchemaObjectsPreviewGroup = this.PreviewGroup;
                referenceInput.RefactoringOperation = casingInput.RefactoringOperation;
                return referenceInput;
            }
    

    이 추가 형식의 ContributorInput은 기호가 업데이트된 요소에 대한 모든 참조를 처리하는 데 사용됩니다. 이 메서드는 다음 메서드에 의해 호출됩니다.

  10. 리팩터링되는 기호에 대한 정의가 들어 있는 스크립트에 대해 변경 목록을 빌드하는 메서드를 추가합니다.

            public Tuple<IList<ChangeProposal>, IList<ContributorInput>> GetChangesFromCurrentSymbolScript(
                string projectFullName,
                ContributorInput input,
                ISqlModelElement modelElement,
                Boolean defaultChecked)
            {
                SampleHelper.CheckNullArgument(input, "input");
                SampleHelper.CheckNullArgument(modelElement, "modelElement");
    
                SqlSchemaModel dataSchemaModel = input.RefactoringOperation.CurrentDataSchemaModel as SqlSchemaModel;
                Debug.Assert(dataSchemaModel != null, "DataSchemaModel is null.");
    
                List<ChangeProposal> allChanges = new List<ChangeProposal>();
    
                // list to hold all side effect contributor inputs
                List<ContributorInput> inputs = new List<ContributorInput>();
    
                string fileFullPath = null;
                ISourceInformation elementSource = modelElement.PrimarySource;
                if (elementSource != null)
                {
                    fileFullPath = elementSource.SourceName;
                }
    
                if (!string.IsNullOrEmpty(fileFullPath))
                {
                    List<RawChangeInfo> changes = AnalyzeScript(dataSchemaModel, modelElement);
    
                    // Convert the offsets returned from parser to the line based offsets
                    allChanges.AddRange(SampleHelper.ConvertOffsets(projectFullName, fileFullPath, changes, defaultChecked));
    
                    // Create a CasingReferenceContributorInput, anything reference this schema object
                    // need to contribute changes for this input.
                    inputs.Add(CreateCasingReferenceInput(input));
                }
                return new Tuple<IList<ChangeProposal>, IList<ContributorInput>>(allChanges, inputs);
            }
    

    이 메서드는 AnalyzeScript 메서드를 호출하여 스크립트 요소를 처리합니다.

  11. AnalyzeScript 메서드를 추가합니다.

            public static List<RawChangeInfo> AnalyzeScript(SqlSchemaModel dataSchemaModel, ISqlModelElement modelElement)
            {
                SampleHelper.CheckNullArgument(dataSchemaModel, "dataSchemaModel");
    
                // get element source
                ISourceInformation elementSource = modelElement.PrimarySource;
                if (elementSource == null)
                {
                    throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture,
                        "Cannot retrieve element source of {0}", SampleHelper.GetModelElementName(modelElement)));
                }
    
                // get sql fragment
                TSqlFragment fragment = elementSource.ScriptDom as TSqlFragment;
                if (fragment == null)
                {
                    throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture,
                        "Cannot retrieve script fragment of {0}", SampleHelper.GetModelElementName(modelElement)));
                }
    
                List<RawChangeInfo> changes = new List<RawChangeInfo>();
                Identifier id = null;
    
                if (fragment is CreateTableStatement)  // Table
                {
                    id = ((CreateTableStatement)fragment).SchemaObjectName.BaseIdentifier;
                }
                else if (fragment is CreateViewStatement)  // View
                {
                    id = ((CreateViewStatement)fragment).SchemaObjectName.BaseIdentifier;
                }
                else if (fragment is ColumnDefinition)   // Column
                {
                    id = ((ColumnDefinition)fragment).ColumnIdentifier;
                }
                else if (fragment is CreateProcedureStatement)  // Proc
                {
                    ProcedureReference procRef = ((CreateProcedureStatement)fragment).ProcedureReference;
                    if (procRef != null)
                    {
                        id = procRef.Name.BaseIdentifier;
                    }
                }
                else if (fragment is CreateFunctionStatement)    // Function
                {
                    id = ((CreateFunctionStatement)fragment).Name.BaseIdentifier;
                }
                else if (fragment is CreateIndexStatement)       // Index
                {
                    id = ((CreateIndexStatement)fragment).Name;
                }
                else if (fragment is Constraint)    // inline constraint
                {
                    id = ((Constraint)fragment).ConstraintIdentifier;
                }
                else if (fragment is AlterTableAddTableElementStatement)     // default/check constraints
                {
                    IList<Constraint> constraints = ((AlterTableAddTableElementStatement)fragment).TableConstraints;
                    Debug.Assert(constraints.Count == 1, string.Format("Only one constraint expected, actual {0}", constraints.Count));
                    id = constraints[0].ConstraintIdentifier;
                }
                else  // anything NYI
                {
                    Debug.WriteLine(string.Format("Uppercasing symbol of type {0} is not implemented yet.", fragment.GetType().Name));
                }
    
                string oldName = SampleHelper.GetModelElementSimpleName(modelElement);
                if (id != null && oldName.Length > 0)
                {
                    string newName = oldName.Substring(0, 1).ToUpper() + oldName.Substring(1); // upper casing the first letter
    
                    if (string.CompareOrdinal(oldName, newName) != 0)
                    {
                        RawChangeInfo change = SampleHelper.AddOffsestFromIdentifier(id, oldName, newName, true);
                        if (change != null)
                        {
                            changes.Add(change);
                        }
                    }
                }
                return changes;
            }
    

    이 메서드는 리팩터링되는 요소의 소스 스크립트를 가져옵니다. 그런 다음 해당 소스 스크립트에 대한 SQL 조각을 검색합니다. 다음으로 이 메서드는 새 변경 목록을 만들고, 조각 형식을 기반으로 해당 요소의 식별자를 확인하고, 변경 목록에 새 변경 내용을 추가합니다.

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

    다음에는 CasingReferenceContributorInput 클래스를 정의합니다.

CasingReferenceContributorInput 클래스를 정의하려면

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

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

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

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

        internal class CasingReferenceContributorInput: ContributorInput
        {        
        }        
    

    이 클래스는 ContributorInput에서 상속해야 합니다.

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

            private ISqlModelElement _modelElement;
            private RefactoringPreviewGroup _previewGroup;
    

    이러한 멤버는 작업을 수행할 모델 요소와 변경 내용이 속하는 미리 보기 그룹을 추적하는 데 사용됩니다.

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

            public CasingReferenceContributorInput(ISqlModelElement modelElement)
            {
                _modelElement = modelElement;
            }
    

    이 생성자는 모델 요소를 초기화합니다.

  7. 모델 요소에 대한 읽기 전용 public 속성을 추가합니다.

            /// <summary>
            /// Selected model element
            /// </summary>
            public ISqlModelElement ModelElement
            {
                get
                {
                    return _modelElement;
                }
            }
    
  8. 이 참가자로 식별된 변경 내용에 대한 추가 미리 보기 그룹을 정의합니다.

            /// <summary>
            /// Preview group that change proposals belong to
            /// </summary>
            public RefactoringPreviewGroup SchemaObjectsPreviewGroup
            {
                get
                {
                    return _previewGroup;
                }
                set 
                { 
                    _previewGroup = value; 
                }
            }
    
  9. Override Equals 메서드는 두 개의 CasingReferenceContributorInput 개체가 동일한지를 확인하는 비교 기능을 제공합니다.

            /// <summary>
            /// Override Equals
            /// </summary>
            /// <param name="obj"></param>
            /// <returns></returns>
            public override bool Equals(object obj)
            {
                CasingContributorInput other = obj as CasingContributorInput;
                return _modelElement.Equals(other.ModelElement);
            }
    

    이 참가자의 경우 입력이 동일한 모델 요소에 적용되면 입력이 동일한 것으로 처리됩니다.

  10. GetHashCode 메서드를 재정의합니다.

            /// <summary>
            /// Override GetHashCode
            /// </summary>
            /// <returns></returns>
            public override int GetHashCode()
            {
                Int32 hash = _modelElement.GetHashCode();
                return hash;
            }
    
  11. 파일 메뉴에서 CasingReferenceContributorInput.cs 저장을 클릭합니다.

    다음에는 CasingReferenceContributor 클래스를 정의합니다.

CasingReferenceContributor 클래스를 정의하려면

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

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

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

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

    [DatabaseSchemaProviderCompatibility(typeof(SqlDatabaseSchemaProvider))]
        internal class CasingReferenceContributor : RefactoringContributor<CasingReferenceContributorInput>
        {
        }
    

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

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

            #region Const
            private const string PreviewGroupFriendlyName = @"Schema Objects";
            private const string PreviewDescription = @"Uppercasing the name of this schema object and all references to this schema object.";
            private const string PreviewWarning = @"Changing casing of the schema object name may cause errors and warnings when your project is using case-sensitive collations. ";
            #endregion
    
            private RefactoringPreviewGroup _previewGroup;
    

    이러한 상수는 미리 보기 창에 표시될 정보를 제공합니다. 추가 멤버는 미리 보기 그룹을 추적하는 데 사용됩니다.

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

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

            #region overrides
            /// <summary>
            /// Preview group for text files
            /// </summary>
            public override RefactoringPreviewGroup PreviewGroup
            {
                get
                {
                    return _previewGroup;
                }
                set
                {
                    _previewGroup = value;
                }
            }
            #endregion
    
  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(CasingReferenceContributorInput input)
            {
                // cast input into reference input
                CasingReferenceContributorInput casingReferenceInput = input as CasingReferenceContributorInput;
                if (casingReferenceInput == null)
                {
                    throw new ArgumentNullException("input");
                }
    
                // Make sure CasingReferenceContributor and CasingSymbolContributor for a same refactoring operation
                // share the same preview group instance.
                if (casingReferenceInput.SchemaObjectsPreviewGroup != null)
                {
                    _previewGroup = casingReferenceInput.SchemaObjectsPreviewGroup;
                }
    
                string projectFullName;
                casingReferenceInput.RefactoringOperation.CurrentProjectHierarchy.GetCanonicalName(VSConstants.VSITEMID_ROOT, out projectFullName);
    
                Tuple<IList<ChangeProposal>, IList<ContributorInput>> changes = GetChangesFromReferencedSymbolScripts(
                        projectFullName,
                        casingReferenceInput,
                        casingReferenceInput.ModelElement,
                        true
                   );
    
                return changes;
            }
    

    ContributeChangesMethod는 GetChangesFromReferencedSymbolScripts 메서드를 호출합니다.

  9. 업데이트되는 기호에 대한 참조가 들어 있는 스크립트에서 변경 제안 목록을 반환하는 GetChangesFromReferencedSymbolScripts 메서드를 구현합니다.

            public static Tuple<IList<ChangeProposal>, IList<ContributorInput>> GetChangesFromReferencedSymbolScripts(
                string projectFullName,
                ContributorInput input,
                ISqlModelElement modelElement,
                bool defaultChecked  // if the preview group is by default checked in the preview window
                )
            {
                SampleHelper.CheckNullArgument(input, "input");
    
                SqlSchemaModel dataSchemaModel = input.RefactoringOperation.CurrentDataSchemaModel as SqlSchemaModel;
                Debug.Assert(dataSchemaModel != null, "The DataSchemaModel is null for current Database project.");
    
                // Get all the changes for these schema objects that referencing the changed IModelElement.
                List<ChangeProposal> allChanges = new List<ChangeProposal>();
                Dictionary<string, List<RawChangeInfo>> fileChanges = new Dictionary<string, List<RawChangeInfo>>();
    
                List<RelationshipEntrySource> relationshipEntrySources = GetDependentEntries(dataSchemaModel, modelElement, true, true);
                foreach (var entry in relationshipEntrySources)
                {
                    string fileFullPath = entry.Item1.SourceName;
                    if (!string.IsNullOrEmpty(fileFullPath))
                    {
                        IList<RawChangeInfo> result = AnalyzeRelationshipEntrySource(dataSchemaModel, modelElement, entry.Item2, entry.Item1);
                        if (result != null)
                        {
                            List<RawChangeInfo> fileChange = null;
                            if (!fileChanges.TryGetValue(fileFullPath, out fileChange))
                            {
                                fileChange = new List<RawChangeInfo>();
                                fileChanges.Add(fileFullPath, fileChange);
                            }
                            fileChange.AddRange(result);
                        }
                    }
                }
    
                // Convert the offsets returned from ScriptDom to the line based offsets
                foreach (string fileFullPath in fileChanges.Keys)
                {
                    allChanges.AddRange(SampleHelper.ConvertOffsets(projectFullName,
                                                    fileFullPath,
                                                    fileChanges[fileFullPath],
                                                    defaultChecked));
                }
    
                // Change propagation is not considered in this sample. 
                // Thus the second value in the returned Tuple is set to null
                return new Tuple<IList<ChangeProposal>, IList<ContributorInput>>(allChanges, null);
            }
    

    이 메서드는 업데이트된 기호의 모든 종속성 목록을 검색합니다. 그런 다음 각 참조에 대해 AnalyzeRelationshipEntrySource 메서드를 호출하여 필요한 추가 변경 내용이 있는지 확인합니다.

  10. AnalyzeRelationshipEntrySource 메서드를 추가합니다.

            public static IList<RawChangeInfo> AnalyzeRelationshipEntrySource(
                SqlSchemaModel dataSchemaModel,
                ISqlModelElement modelElement,
                ISourceInformation relationshipEntrySource)
            {
                SampleHelper.CheckNullArgument(dataSchemaModel, "dataSchemaModel");
    
                List<Identifier> identifiers = new List<Identifier>();
    
                TSqlFragment fragment = relationshipEntrySource.ScriptDom as TSqlFragment;
    
                // handle expressions
                if (fragment is SelectColumn)
                {
                    Expression exp = ((SelectColumn)fragment).Expression;// as Expression;
                    fragment = exp as TSqlFragment;
                }
                else if (fragment is ExpressionWithSortOrder)
                {
                    Expression exp = ((ExpressionWithSortOrder)fragment).Expression; // as Expression;
                    fragment = exp as TSqlFragment;
                }
                else if (fragment is ExpressionGroupingSpecification)
                {
                    Expression exp = ((ExpressionGroupingSpecification)fragment).Expression; // as Expression;
                    fragment = exp as TSqlFragment;
                }
    
                // handle different fragment
                if (fragment is Identifier)
                {
                    identifiers.Add((Identifier)fragment); ;
                }
                else if (fragment is Column)
                {
                    identifiers.AddRange(((Column)fragment).Identifiers);
                }
                else if (fragment is ColumnWithSortOrder)
                {
                    identifiers.Add(((ColumnWithSortOrder)fragment).ColumnIdentifier);
                }
                else if (fragment is SchemaObjectName)
                {
                    identifiers.Add(((SchemaObjectName)fragment).BaseIdentifier);
                }
                else if (fragment is SchemaObjectTableSource)
                {
                    identifiers.Add(((SchemaObjectTableSource)fragment).SchemaObject.BaseIdentifier);
                }
                else if (fragment is SchemaObjectDataModificationTarget)
                {
                    identifiers.Add(((SchemaObjectDataModificationTarget)fragment).SchemaObject.BaseIdentifier);
                }
                else if (fragment is FunctionCall)
                {
                    FunctionCall funcCall = (FunctionCall)fragment;
                    IdentifiersCallTarget identsCallTarget = funcCall.CallTarget as IdentifiersCallTarget;
                    if (identsCallTarget != null)
                    {
                        identifiers.AddRange(identsCallTarget.Identifiers);
                    }
                    identifiers.Add(funcCall.FunctionName);
                }
                else if (fragment is ProcedureReference)
                {
                    SchemaObjectName procRefName = ((ProcedureReference)fragment).Name;
                    if (procRefName != null)
                    {
                        identifiers.Add(procRefName.BaseIdentifier);
                    }
                }
                else if (fragment is TriggerObject)
                {
                    SchemaObjectName triggerName = ((TriggerObject)fragment).Name;
                    if (triggerName != null)
                    {
                        identifiers.Add(triggerName.BaseIdentifier);
                    }
                }
                else if (fragment is FullTextIndexColumn)
                {
                    identifiers.Add(((FullTextIndexColumn)fragment).Name);
                }
                else if (fragment is SecurityTargetObject)
                {
                    identifiers.AddRange(((SecurityTargetObject)fragment).ObjectName.Identifiers);
                }
                else  // other types of fragments are not handled in this sample
                {
                    Debug.WriteLine(string.Format("Uppercasing referencing object of type {0} is not implemented yet.", fragment.GetType().Name));
                }
    
                List<RawChangeInfo> changes = new List<RawChangeInfo>();
                string oldName = SampleHelper.GetModelElementSimpleName(modelElement);
                if (identifiers.Count > 0 && oldName.Length > 0)
                {
                    string newName = oldName.Substring(0, 1).ToUpper() + oldName.Substring(1); // upper casing the first letter
    
                    if (string.CompareOrdinal(oldName, newName) != 0)
                    {
                        // list of changes for this relationship entry
                        RawChangeInfo change = null;
                        foreach (Identifier idf in identifiers)
                        {
                            change = SampleHelper.AddOffsestFromIdentifier(idf, oldName, newName, true);
                            if (change != null)
                            {
                                changes.Add(change);
                            }
                        }
                    }
                }
                return changes;
            }
    

    이 메서드는 업데이트된 기호에 의존하는 스크립트 조각에 대해 수행해야 하는 변경 내용 목록을 검색합니다.

  11. GetDependentEntries 메서드를 추가합니다.

            /// <summary>
            ///  Get all relating relationship entries for the model element and its composing and hierarchical children
            /// </summary>
            internal static List<System.Tuple<ISourceInformation, IModelRelationshipEntry>> GetDependentEntries(
                SqlSchemaModel dataSchemaModel,
                ISqlModelElement modelElement,
                bool ignoreComposedRelationship,
                bool includeChildDependencies)
            {
                SampleHelper.CheckNullArgument(dataSchemaModel, "dataSchemaModel");
                SampleHelper.CheckNullArgument(modelElement, "modelElement");
    
                var dependencies = new List<System.Tuple<ISourceInformation, IModelRelationshipEntry>>();
    
                List<IModelRelationshipEntry> relatingRelationships = new List<IModelRelationshipEntry>();
                GetDependentEntries(modelElement,
                                    dataSchemaModel,
                                    new Dictionary<IModelElement, Object>(),
                                    relatingRelationships,
                                    includeChildDependencies);
    
                foreach (IModelRelationshipEntry entry in relatingRelationships)
                {
                    ModelRelationshipType relationshipType = entry.RelationshipClass.ModelRelationshipType;
                    if (!ignoreComposedRelationship ||
                        (relationshipType != ModelRelationshipType.Composing))
                    {
                        ISqlModelElement relatingElement = entry.FromElement as ISqlModelElement;
                        Debug.Assert(relatingElement != null, "Relating element got from ModelStore is null.");
    
                        foreach (var si in relatingElement.GetRelationshipEntrySources(entry))
                        {
                            dependencies.Add(new System.Tuple<ISourceInformation, IModelRelationshipEntry>(si, entry));
                        }
                    }
                }
                return dependencies;
            }
    
            private static void GetDependentEntries(
                IModelElement modelElement,
                DataSchemaModel dataSchemaModel,
                Dictionary<IModelElement, Object> visitElement,
                List<IModelRelationshipEntry> relationshipEntries,
                Boolean includeChildDependencies)
            {
                if (modelElement != null &&
                    !visitElement.ContainsKey(modelElement))
                {
                    visitElement[modelElement] = null;
    
                    IList<IModelRelationshipEntry> relatingRelationships = modelElement.GetReferencingRelationshipEntries();
                    relationshipEntries.AddRange(relatingRelationships);
    
                    if (includeChildDependencies)
                    {
                        // First loop through all composed children of this element, and get their relationship entries as well
                        foreach (IModelRelationshipEntry entry in modelElement.GetReferencedRelationshipEntries())
                        {
                            if (entry.RelationshipClass.ModelRelationshipType == ModelRelationshipType.Composing)
                            {
                                GetDependentEntries(entry.Element, dataSchemaModel, visitElement, relationshipEntries, includeChildDependencies);
                            }
                        }
    
                        // Then loop through all hierarchical children of this element, add their dependents to the list.
                        foreach (IModelRelationshipEntry entry in relatingRelationships)
                        {
                            if (entry.RelationshipClass.ModelRelationshipType == ModelRelationshipType.Hierarchical)
                            {
                                GetDependentEntries(entry.FromElement, dataSchemaModel, visitElement, relationshipEntries, includeChildDependencies);
                            }
                        }
                    }
                }
            }
    

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

    다음에는 어셈블리를 구성하고 빌드합니다.

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

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

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

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

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

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

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

  7. 확인을 클릭합니다.

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

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

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

어셈블리 설치 및 등록

CasingRefactoringType 어셈블리를 설치하려면

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

  2. 서명된 어셈블리(CasingRefactoringType.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에 표시되도록 등록해야 합니다.

CasingRefactoringType 어셈블리를 등록하려면

  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. 결과 줄을 클립보드로 복사합니다. 이 줄은 다음과 같습니다.

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

    중요

    Windows Vista 및 Microsoft Windows Server 2008에서 Program Files 폴더에 파일을 저장할 수 있도록 관리자 원한으로 편집기를 엽니다.

  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.CasingRefactorCommand" 
    assembly=" CasingRefactoringType, Version=1.0.0.0, Culture=neutral, PublicKeyToken=<enter key here>" enabled="true" />
      <extension type="MySamples.Refactoring.CasingSymbolContributor" 
            assembly="CasingRefactoringType, Version=1.0.0.0, Culture=neutral, PublicKeyToken=<enter key here>" enabled="true" />
      <extension type="MySamples.Refactoring.CasingReferenceContributor" 
            assembly="CasingRefactoringType, Version=1.0.0.0, Culture=neutral, PublicKeyToken=<enter key here>" enabled="true" />
    </extensions>
    

    이 XML 파일을 사용하여 RefactoringCommand에서 상속된 클래스와 RefactoringContributor에서 파생된 모든 관련 클래스를 등록합니다.

  7. %Program Files%\Microsoft Visual Studio 10.0\VSTSDB\Extensions\MyExtensions 폴더에 CasingRefactoringType.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. 확인을 클릭합니다.

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

새 리팩터링 형식을 사용하여 테이블 이름을 업데이트하려면

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

    이 연습의 새 리팩터링 형식을 정의했습니다.

  2. 변경 내용 미리 보기 대화 상자에서 변경 내용을 검토하고 적용을 클릭합니다.

    테이블 이름이 Employee로 업데이트됩니다. 기본 키에 있는 이 테이블에 대한 참조도 업데이트됩니다.

다음 단계

사용자 고유의 추가 데이터베이스 리팩터링 형식을 만들 수 있습니다. 기존의 데이터베이스 리팩터링 형식을 추가 파일 또는 개체 형식에 대해서도 사용할 수 있도록 더 많은 참가자를 추가할 수도 있습니다.

참고 항목

작업

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

개념

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

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