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

In this step-by-step topic, you will create, install, register, and test a new type of database refactoring. This refactoring operation will convert the first character of the name of the specified object to uppercase, and it will update all references to that updated name.

This walkthrough illustrates the following tasks:

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

  2. Install and register the assembly, so that the refactoring type is available in Visual Studio Team System Database Edition.

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

Prerequisites

You need the following components to complete this walkthrough:

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

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

Creating an Assembly with a Custom Refactoring Type

To create a custom refactoring type that can convert the first letter of an object name to uppercase and then update all references to that object, you must implement six classes:

  • CasingRefactoringCommand — This class provides the command name for the refactoring menu, specifies on which model elements your refactoring type is available, and calls your refactoring operation when the user clicks the command.

  • CasingRefactoringOperation — This class specifies how your refactoring operation interacts with the Preview Window, specifies properties that describe the operation, and creates the CasingContributorInput class.

  • CasingContributorInput — This class stores input data to the CasingSymbolContributor class.

  • CasingSymbolContributor — This class builds the list of change proposals for the renamed symbol, and it also creates the CasingReferenceContributorInput class to handle updating the references to the object whose name is being changed.

  • CasingReferenceContributorInput — This class stores input data to the CasingReferenceContributor class.

  • CasingReferenceContributor — This class builds the list of change proposals associated with updating references to the renamed symbol.

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

To create the class library and helper code

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

  2. Add references to the following class libraries:

    • Microsoft.Data.Schema.dll

    • Microsoft.Data.Schema.ScriptDom.dll

    • Microsoft.Data.Schema.ScriptDom.sql.dll

    • Microsoft.Data.Schema.Sql.dll

    • Microsoft.VisualStudio.Data.Schema.Project.dll

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

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

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

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

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

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

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

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

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

    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.IO;
    using System.Runtime.InteropServices;
    using Microsoft.Data.Schema.ScriptDom.Sql;
    using Microsoft.VisualStudio;
    using Microsoft.VisualStudio.Data.Schema.Project.Common.UI;
    using Microsoft.VisualStudio.Data.Schema.Project.Refactoring;
    using Microsoft.VisualStudio.Shell.Interop;
    using Microsoft.VisualStudio.TextManager.Interop;
    using VsShell = Microsoft.VisualStudio.Shell.Interop;
    using VsTextMgr = Microsoft.VisualStudio.TextManager.Interop;
    using Microsoft.Data.Schema.SchemaModel;
    
    namespace MySamples.Refactoring
    {
        internal static class SampleHelper
        {
            public static String GetModelElementName(IModelElement modelElement)
            {
                SampleHelper.CheckNullArgument(modelElement, "modelElement");
                return modelElement.ToString();
            }
    
            /// <summary>
            /// Given a model element, returns its simple name.
            /// </summary>
            public static String GetModelElementSimpleName(IModelElement modelElement)
            {
                String separator = ".";
                String simpleName = String.Empty;
                String fullName = modelElement.ToString();
                String[] nameParts = fullName.Split(separator.ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
                if (nameParts.Length > 0)
                {
                    simpleName = nameParts[nameParts.Length - 1]; // last part 
                }
                if (simpleName.StartsWith("[") && simpleName.EndsWith("]"))
                {
                    simpleName = simpleName.Substring(1, simpleName.Length - 2);
                }
                return simpleName;
            }
    
            /// <summary>
            /// Find all files in the project with the specified file extension
            /// </summary>
            public static List<string> GetAllFilesInProject(IVsHierarchy solutionNode, string fileExtension, bool visibleNodesOnly)
            {
                List<string> files = new List<string>();
                if (null != solutionNode)
                {
                    EnumProjectItems(solutionNode, fileExtension, files, 
                                    VSConstants.VSITEMID_ROOT,  // item id of solution root node
                                    0,                          // recursion from solution node
                                    true,                       // hierarchy is Solution node
                                    visibleNodesOnly);          // visibleNodesOnly
                }
                return files;
            }
    
            /// <summary>
            /// Enumerates recursively over the hierarchy items.
            /// </summary>
            /// <param name="hierarchy">hierarchy to enmerate over.</param>
            /// <param name="fileExtension">type of files we need to collect from the project</param>
            /// <param name="files">list of file paths</param>
            /// <param name="itemid">item id of the hierarchy</param>
            /// <param name="recursionLevel">Depth of recursion. 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 VsTextMgr.IVsTextLines GetTextLines(string fullPathFileName)
            {
                System.IServiceProvider serviceProvider = DataPackage.Instance;
                VsTextMgr.IVsTextLines textLines = null;
                VsShell.IVsRunningDocumentTable rdt = (VsShell.IVsRunningDocumentTable)serviceProvider.GetService(typeof(VsShell.SVsRunningDocumentTable));
    
                if (rdt != null)
                {
                    VsShell.IVsHierarchy ppHier = null;
                    uint pitemid, pdwCookie;
                    IntPtr ppunkDocData = IntPtr.Zero;
                    try
                    {
                        rdt.FindAndLockDocument((uint)(VsShell._VSRDTFLAGS.RDT_NoLock), fullPathFileName, out ppHier, out pitemid, out ppunkDocData, out pdwCookie);
                        if (pdwCookie != 0)
                        {
                            if (ppunkDocData != IntPtr.Zero)
                            {
                                try
                                {
                                    // Get text lines from the doc data
                                    IVsPersistDocData docData = (IVsPersistDocData)Marshal.GetObjectForIUnknown(ppunkDocData);
    
                                    if (docData is IVsTextLines)
                                    {
                                        textLines = (IVsTextLines)docData;
                                    }
                                    else
                                    {
                                        textLines = null;
                                    }
                                }
                                catch (ArgumentException)
                                {
                                    // Do nothing here, it will return null stream at the end.
                                }
                            }
                        }
                        else
                        {
                            // The file is not in RDT, open it in invisible editor and get the text lines from it.
                            IVsInvisibleEditor invisibleEditor = null;
                            TryGetTextLinesAndInvisibleEditor(fullPathFileName, out invisibleEditor, out textLines);
                        }
                    }
                    finally
                    {
                        if (ppunkDocData != IntPtr.Zero)
                            Marshal.Release(ppunkDocData);
                    }
                }
                return textLines;
            }
    
            /// <summary>
            /// Open the file in invisible editor in the running
            /// documents table (RDT), and get text buffer from that editor.
            /// </summary>
            /// <param name="fullPathFileName">File name with full path.</param>
            /// <param name="spEditor">The result invisible editor.</param>
            /// <param name="textLines">The result text buffer.</param>
            /// <returns>True, if the file is opened correctly in invisible editor.</returns>
            private static bool TryGetTextLinesAndInvisibleEditor(string fullPathFileName, out IVsInvisibleEditor spEditor, out IVsTextLines textLines)
            {
                System.IServiceProvider serviceProvider = DataPackage.Instance;
                spEditor = null;
                textLines = null;
    
                // Need to open this file.  Use the invisible editor manager to do so.
                IVsInvisibleEditorManager spIEM;
                IntPtr ppDocData = IntPtr.Zero;
                bool result;
    
                Guid IID_IVsTextLines = typeof(IVsTextLines).GUID;
    
                try
                {
                    spIEM = (IVsInvisibleEditorManager)serviceProvider.GetService(typeof(IVsInvisibleEditorManager));
                    spIEM.RegisterInvisibleEditor(fullPathFileName, null, (uint)_EDITORREGFLAGS.RIEF_ENABLECACHING, null, out spEditor);
                    if (spEditor != null)
                    {
                        int hr = spEditor.GetDocData(0, ref IID_IVsTextLines, out ppDocData);
                        if (hr == VSConstants.S_OK && ppDocData != IntPtr.Zero)
                        {
                            textLines = Marshal.GetTypedObjectForIUnknown(ppDocData, typeof(IVsTextLines)) as IVsTextLines;
                            result = true;
                        }
                        else
                        {
                            result = false;
                        }
                    }
                    else
                    {
                        result = false;
                    }
                }
                finally
                {
                    if (ppDocData != IntPtr.Zero)
                        Marshal.Release(ppDocData);
                }
                return result;
            }
        }
    }
    
  6. On the File menu, click Save SampleHelper.cs.

  7. Add a class named RawChangeInfo to your project.

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

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    
    namespace MySamples.Refactoring
    {
        /// <summary>
        /// Helper class to encapsulate StartOffset, FragmentLength and change string from
        /// parser and SchemaAnalzyer.
        /// </summary>
        internal sealed class RawChangeInfo
        {
            private int _startOffset;
            private int _length;
            private string _oldText;
            private string _newText;
    
            public RawChangeInfo(int startOffset, int length, string oldText, string newText)
            {
                //ArgumentValidation.CheckForOutOfRangeException(startOffset, 0, int.MaxValue);
                //ArgumentValidation.CheckForOutOfRangeException(length, 0, int.MaxValue);
                //ArgumentValidation.CheckForNullReference(oldText, "oldText");
                //ArgumentValidation.CheckForNullReference(newText, "newText");
    
                _startOffset = startOffset;
                _length = length;
                _oldText = oldText;
                _newText = newText;
            }
    
            public int StartOffset
            {
                get
                {
                    return _startOffset;
                }
                set
                {
                    _startOffset = value;
                }
            }
    
            public int Length
            {
                get
                {
                    return _length;
                }
            }
    
            public string OldText
            {
                get
                {
                    return _oldText;
                }
            }
    
            public string NewText
            {
                get
                {
                    return _newText;
                }
                set
                {
                    _newText = value;
                }
            }
        }
      }
    
  9. On the File menu, click Save SampleHelper.cs.

    Next, you will define the CasingRefactoringCommand class.

To define the CasingRefactoringCommand class

  1. Add a class named CasingRefactorCommand to your project.

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

    using Microsoft.Data.Schema.Extensibility;
    using Microsoft.Data.Schema.SchemaModel;
    using Microsoft.Data.Schema.SchemaModel.Abstract;
    using Microsoft.Data.Schema.Sql.SchemaModel.SqlServer;
    using Microsoft.Data.Schema.Sql.SqlDsp;
    using Microsoft.VisualStudio.Data.Schema.Project.Refactoring;
    using Microsoft.VisualStudio.Shell.Interop;
    
  3. Change the namespace to MySamples.Refactoring:

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

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

    The attribute is used to specify the database schema providers with which this refactoring type is compatible. For this example, your new refactoring type will work with any provider that is derived from SqlDatabaseSchemaProvider. Your class inherits RefactoringSchemaViewNodeCommand. Inheriting from this base class indicates that your refactoring type is available on specified nodes in Schema View. Other types of refactoring can be defined that operate on file nodes and project nodes.

  5. Next, add the following override method to your class:

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

    This method provides the behavior when the user applies your refactoring command in Schema View.

  6. Next, add the following override method to your class:

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

    This method determines on which nodes in Schema View your refactoring command is available.

  7. Finally, add the following override method to your class:

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

    This method provides the friendly name for the refactoring command that appears on the refactoring menu.

  8. On the File menu, click Save CasingRefactoringCommand.cs.

    Next, you will define the CasingRefactoringOperation class.

To define the CasingRefactoringOperation class

  1. Add a class named CasingRefactoringOperation to your project.

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

    using System;
    using System.Diagnostics;
    using System.Globalization;
    using Microsoft.Data.Schema.SchemaModel;
    using Microsoft.Data.Schema.Sql.SchemaModel.SqlServer;
    using Microsoft.VisualStudio.Data.Schema.Project.Refactoring;
    using Microsoft.VisualStudio.Shell.Interop;
    
  3. Change the namespace to MySamples.Refactoring:

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

        internal class CasingRefactorOperation : RefactoringOperation
        {
    }
    

    Your class must inherit from RefactoringOperation.

  5. Add the following constant and member variable declarations to your class:

            #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;
    

    The private constants provide information about this operation that will be displayed in the Preview Window.

  6. Add the constructor for your class:

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

    The constructor initializes the operation name and the model element, if these were specified.

  7. Override the PreviewWindowInfo property to get the values that will appear in the Preview Window when the user applies your refactoring type:

            /// <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. Provide additional property definitions:

            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. Finally, override the OnGetContributorInput method:

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

    This method creates the ContributorInput that is passed to the refactoring contributors for this refactoring type.

  10. On the File menu, click Save CasingRefactoringOperation.cs.

    Next, you will define the CasingContributorInput class.

To define the CasingContributorInput class

  1. Add a class named CasingContributorInput to your project.

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

    using System;
    using Microsoft.Data.Schema.Sql.SchemaModel.SqlServer;
    using Microsoft.VisualStudio.Data.Schema.Project.Refactoring;
    
  3. Change the namespace to MySamples.Refactoring:

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

        internal class CasingContributorInput: ContributorInput
        {
        }
    

    Your class must inherit from ContributorInput.

  5. Define one additional private member variable:

            private ISqlModelElement _modelElement;
    

    This member is used to keep track of the model element upon which you are operating.

  6. Add the class constructor:

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

    The constructor initializes the model element.

  7. Add a read-only public property for the model element:

            /// <summary>
            /// Selected model element
            /// </summary>
            public ISqlModelElement ModelElement
            {
                get 
    
                {
                    return _modelElement;
                }
            }
    
  8. The Override Equals method provides a comparison that determines whether two CasingContributorInput objects are the same:

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

    For this contributor, the inputs will be considered the same if they apply to the same model element.

  9. Override the GetHashCode method:

            /// <summary>
            /// Override GetHashCode
            /// </summary>
            /// <returns></returns>
            public override int GetHashCode()
            {
                Int32 hash = _modelElement.GetHashCode();
                return hash;
            }
    
  10. On the File menu, click Save CasingContributorInput.cs.

    Next, you will define the CasingSymbolContributor class.

To define the CasingSymbolContributor class

  1. Add a class named CasingSymbolContributor to your project.

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

    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Globalization;
    using Microsoft.Data.Schema.Common;
    using Microsoft.Data.Schema.Extensibility;
    using Microsoft.Data.Schema.SchemaModel;
    using Microsoft.Data.Schema.ScriptDom.Sql;
    using Microsoft.Data.Schema.Sql.SchemaModel.SqlServer;
    using Microsoft.Data.Schema.Sql.SqlDsp;
    using Microsoft.VisualStudio;
    using Microsoft.VisualStudio.Data.Schema.Project.Refactoring;
    
  3. Change the namespace to MySamples.Refactoring:

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

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

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

  5. Define additional constants and private member variables:

            #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;
    

    The constants provide information that will appear in the Preview Window. The additional member is used to keep track of the preview group.

  6. Add the class constructor:

            #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
    

    The constructor initializes the model element, creating a new preview group and initializing its properties. You could also register icons here to be displayed in the Preview Window for specific file name extensions, and you can register a language service that is used to provide syntax coloring for files that have a specified extension.

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

            #region overrides
            /// <summary>
            /// Preview group for schema object files
            /// </summary>
            public override RefactoringPreviewGroup PreviewGroup
            {
                get
                {
                    return _previewGroup;
                }
                set
                {
                    _previewGroup = value;
                }
            }
    
  8. Override the ContributeChanges method to return a list of change proposals:

            /// <summary>
            /// Contribute to the change proposals
            /// </summary>
            /// <param name="input">contributor input</param>
            /// <returns>List of change proposals with corresponding contributor inputs</returns>
            protected override Tuple<IList<ChangeProposal>, IList<ContributorInput>> ContributeChanges(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. Next, create another type of 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;
            }
    

    This additional type of ContributorInput is used to handle all references to the element whose symbol was updated. This method will be called by the next method.

  10. Add a method to build a list of changes for the script that contains the definition for the symbol being refactored:

            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;
                ElementSource elementSource = modelElement.ElementSource;
                if (elementSource != null)
                {
                    fileFullPath = elementSource.CacheIdentifier;
                }
    
                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);
            }
    

    This method calls the AnalyzeScript method to process the script elements.

  11. Add the AnalyzeScript method:

            public static List<RawChangeInfo> AnalyzeScript(SqlSchemaModel dataSchemaModel, ISqlModelElement modelElement)
            {
                SampleHelper.CheckNullArgument(dataSchemaModel, "dataSchemaModel");
    
                // get element source
                ElementSource elementSource = modelElement.ElementSource;
                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;
            }
    

    This method acquires the source script for the element being refactored. The method then retrieves the SQL fragment for that source script. The method then creates a new change list, determines the identifier for the element (based on the type of fragment), and adds the new change to the change list.

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

    Next, you will define the CasingReferenceContributorInput class.

To define the CasingReferenceContributorInput class

  1. Add a class named CasingReferenceContributorInput to your project.

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

    using System;
    using Microsoft.Data.Schema.Sql.SchemaModel.SqlServer;
    using Microsoft.VisualStudio.Data.Schema.Project.Refactoring;
    
  3. Change the namespace to MySamples.Refactoring:

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

        internal class CasingReferenceContributorInput: ContributorInput
        {        
        }        
    

    Your class must inherit from ContributorInput.

  5. Define additional private member variables:

            private ISqlModelElement _modelElement;
            private RefactoringPreviewGroup _previewGroup;
    

    These members are used to keep track of the model element upon which you are operating and the preview group to which changes belong.

  6. Add the class constructor:

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

    The constructor initializes the model element.

  7. Add a read-only public property for the model element:

            /// <summary>
            /// Selected model element
            /// </summary>
            public ISqlModelElement ModelElement
            {
                get
                {
                    return _modelElement;
                }
            }
    
  8. Define an additional preview group for changes identified by this contributor:

            /// <summary>
            /// Preview group that change proposals belong to
            /// </summary>
            public RefactoringPreviewGroup SchemaObjectsPreviewGroup
            {
                get
                {
                    return _previewGroup;
                }
                set 
                { 
                    _previewGroup = value; 
                }
            }
    
  9. The Override Equals method provides a comparison that determines whether two CasingReferenceContributorInput objects are the same:

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

    For this contributor, the inputs will be considered the same if they apply to the same model element.

  10. Override the GetHashCode method:

            /// <summary>
            /// Override GetHashCode
            /// </summary>
            /// <returns></returns>
            public override int GetHashCode()
            {
                Int32 hash = _modelElement.GetHashCode();
                return hash;
            }
    
  11. On the File menu, click Save CasingReferenceContributorInput.cs.

    Next, you will define the CasingReferenceContributor class.

To define the CasingReferenceContributor class

  1. Add a class named CasingReferenceContributor to your project.

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

    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using Microsoft.Data.Schema.Common;
    using Microsoft.Data.Schema.Extensibility;
    using Microsoft.Data.Schema.SchemaModel;
    using Microsoft.Data.Schema.ScriptDom.Sql;
    using Microsoft.Data.Schema.Sql.SchemaModel.SqlServer;
    using Microsoft.Data.Schema.Sql.SqlDsp;
    using Microsoft.VisualStudio;
    using Microsoft.VisualStudio.Data.Schema.Project.Refactoring;
    
  3. Change the namespace to MySamples.Refactoring:

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

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

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

  5. Define additional constants and private member variables:

            #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;
    

    The constants provide information that will appear in the Preview Window. The additional member is used to keep track of the preview group.

  6. Add the class constructor:

            public CasingReferenceContributor()
            {
            }
    
  7. Override the PreviewGroup property to return the group that was created when this contributor was created:

            #region overrides
            /// <summary>
            /// Preview group for text files
            /// </summary>
            public override RefactoringPreviewGroup PreviewGroup
            {
                get
                {
                    return _previewGroup;
                }
                set
                {
                    _previewGroup = value;
                }
            }
            #endregion
    
  8. Override the ContributeChanges method to return a list of change proposals:

            /// <summary>
            /// Contribute to the change proposals
            /// </summary>
            /// <param name="input">contributor input</param>
            /// <returns>List of change proposals with corresponding contributor inputs</returns>
            protected override Tuple<IList<ChangeProposal>, IList<ContributorInput>> ContributeChanges(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;
            }
    

    The ContributeChangesMethod calls the GetChangesFromReferencedSymbolScripts method.

  9. Implement the GetChangesFromReferencedSymbolScripts method to return a list of change proposals in scripts that contain references to the symbol being updated:

            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 (RelationshipEntrySource source in relationshipEntrySources)
                {
                    string fileFullPath = source.CacheIdentifier;
                    if (!string.IsNullOrEmpty(fileFullPath))
                    {
                        IList<RawChangeInfo> result = AnalyzeRelationshipEntrySource(dataSchemaModel, modelElement, source);
                        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);
            }
    

    This method retrieves a list of all the dependencies of the updated symbol. It then calls the AnalyzeRelationshipEntrySource method for each reference to identify any additional changes that are required.

  10. Add the AnalyzeRelationshipEntrySource method:

            public static IList<RawChangeInfo> AnalyzeRelationshipEntrySource(
                SqlSchemaModel dataSchemaModel,
                ISqlModelElement modelElement,
                RelationshipEntrySource 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;
            }
    

    This method retrieves a list of changes that must be made to script fragments that depend on the updated symbol.

  11. Add the GetDependentEntries method:

            /// <summary>
            ///  Get all relating relationship entries for the model element and its composing and hierarchical children
            /// </summary>
            internal static List<RelationshipEntrySource> GetDependentEntries(
                SqlSchemaModel dataSchemaModel,
                ISqlModelElement modelElement,
                bool ignoreComposedRelationship,
                bool includeChildDependencies)
            {
                SampleHelper.CheckNullArgument(dataSchemaModel, "dataSchemaModel");
                SampleHelper.CheckNullArgument(modelElement, "modelElement");
    
                List<RelationshipEntrySource> dependencies = new List<RelationshipEntrySource>();
    
                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 &&
                         relationshipType != ModelRelationshipType.ComposingRelationshipSurrogate))
                    {
                        ISqlModelElement relatingElement = entry.RelatingElement as ISqlModelElement;
                        Debug.Assert(relatingElement != null, "Relating element got from ModelStore is null.");
    
                        IList<RelationshipEntrySource> relationshipEntrySources = relatingElement.GetRelationshipEntrySources(entry);
                        if (relationshipEntrySources != null)
                        {
                            dependencies.AddRange(relationshipEntrySources);
                        }
                    }
                }
                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.GetRelatingRelationshipEntries();
                    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.GetRelatedRelationshipEntries())
                        {
                            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.RelatingElement, dataSchemaModel, visitElement, relationshipEntries, includeChildDependencies);
                            }
                        }
                    }
                }
            }
    
  12. On the File menu, click Save CasingReferenceContributor.cs.

    Next, you will configure and build the assembly.

To sign and build the assembly

  1. On the Project menu, click CasingRefactoringType Properties.

  2. Click the Signing tab.

  3. Click Sign the assembly.

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

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

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

  7. Click OK.

  8. On the File menu, click Save All.

  9. On the Build menu, click Build Solution.

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

Installing and Registering the Assembly

To install the CasingRefactoringType assembly

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

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

    Note

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

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

To register the CasingRefactoringType assembly

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

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

    Note

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

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

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

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

    Important noteImportant Note:

    On Windows Vista and Microsoft Windows Server 2008, open the editor as an administrator so that you can save the file to your Program Files folder.

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

    <?xml version="1.0" encoding="utf-8" ?> 
    <extensions assembly="" version="1" xmlns="urn:Microsoft.Data.Schema.Extensions" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:Microsoft.Data.Schema.Extensions Microsoft.Data.Schema.Extensions.xsd">
      <extension type="MySamples.Refactoring.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>
    

    You use this XML file to register the class that inherits from RefactoringCommand and all related classes that are derived from RefactoringContributor.

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

  8. Close Visual Studio.

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

Testing the New Refactoring Type

To create a database project

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

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

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

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

    The empty database project is created.

To add a table with a primary key

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

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

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

    Note

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

  4. Click OK.

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

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

  7. Click OK.

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

To use the new refactoring type to update the table name

  1. In Schema View, right-click the employee table node, point to Refactor, and click Make First Letter Uppercase.

    You defined that new type of refactoring in this walkthrough.

  2. In the Preview Changes dialog box, review the changes, and then click Apply.

    The table name is updated to be Employee. The reference to that table in the primary key is also updated.

Next Steps

You can create your own additional types of database refactoring. You can also add more contributors to enable an existing type of database refactoring to operate on additional types of files or objects.

See Also

Tasks

Walkthrough: Extending Database Rename Refactoring to Operate on Text Files

Concepts

Create Custom Database Refactoring Types or Targets

Refactor Database Code and Data

Terminology Overview of Database Edition